| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright 2022 Markus Gothe <markus.gothe@genexis.eu> |
| * Copyright 2025 Christian Marangi <ansuelsmth@gmail.com> |
| * |
| * Limitations: |
| * - Only 8 concurrent waveform generators are available for 8 combinations of |
| * duty_cycle and period. Waveform generators are shared between 16 GPIO |
| * pins and 17 SIPO GPIO pins. |
| * - Supports only normal polarity. |
| * - On configuration the currently running period is completed. |
| * - Minimum supported period is 4 ms |
| * - Maximum supported period is 1s |
| */ |
| |
| #include <linux/array_size.h> |
| #include <linux/bitfield.h> |
| #include <linux/bitmap.h> |
| #include <linux/err.h> |
| #include <linux/io.h> |
| #include <linux/iopoll.h> |
| #include <linux/math64.h> |
| #include <linux/mfd/syscon.h> |
| #include <linux/module.h> |
| #include <linux/mod_devicetable.h> |
| #include <linux/platform_device.h> |
| #include <linux/pwm.h> |
| #include <linux/regmap.h> |
| #include <linux/types.h> |
| |
| #define AIROHA_PWM_REG_SGPIO_LED_DATA 0x0024 |
| #define AIROHA_PWM_SGPIO_LED_DATA_SHIFT_FLAG BIT(31) |
| #define AIROHA_PWM_SGPIO_LED_DATA_DATA GENMASK(16, 0) |
| |
| #define AIROHA_PWM_REG_SGPIO_CLK_DIVR 0x0028 |
| #define AIROHA_PWM_SGPIO_CLK_DIVR GENMASK(1, 0) |
| #define AIROHA_PWM_SGPIO_CLK_DIVR_32 FIELD_PREP_CONST(AIROHA_PWM_SGPIO_CLK_DIVR, 3) |
| #define AIROHA_PWM_SGPIO_CLK_DIVR_16 FIELD_PREP_CONST(AIROHA_PWM_SGPIO_CLK_DIVR, 2) |
| #define AIROHA_PWM_SGPIO_CLK_DIVR_8 FIELD_PREP_CONST(AIROHA_PWM_SGPIO_CLK_DIVR, 1) |
| #define AIROHA_PWM_SGPIO_CLK_DIVR_4 FIELD_PREP_CONST(AIROHA_PWM_SGPIO_CLK_DIVR, 0) |
| |
| #define AIROHA_PWM_REG_SGPIO_CLK_DLY 0x002c |
| |
| #define AIROHA_PWM_REG_SIPO_FLASH_MODE_CFG 0x0030 |
| #define AIROHA_PWM_SERIAL_GPIO_FLASH_MODE BIT(1) |
| #define AIROHA_PWM_SERIAL_GPIO_MODE_74HC164 BIT(0) |
| |
| #define AIROHA_PWM_REG_GPIO_FLASH_PRD_SET(_n) (0x003c + (4 * (_n))) |
| #define AIROHA_PWM_REG_GPIO_FLASH_PRD_SHIFT(_n) (16 * (_n)) |
| #define AIROHA_PWM_GPIO_FLASH_PRD_LOW GENMASK(15, 8) |
| #define AIROHA_PWM_GPIO_FLASH_PRD_HIGH GENMASK(7, 0) |
| |
| #define AIROHA_PWM_REG_GPIO_FLASH_MAP(_n) (0x004c + (4 * (_n))) |
| #define AIROHA_PWM_REG_GPIO_FLASH_MAP_SHIFT(_n) (4 * (_n)) |
| #define AIROHA_PWM_GPIO_FLASH_EN BIT(3) |
| #define AIROHA_PWM_GPIO_FLASH_SET_ID GENMASK(2, 0) |
| |
| /* Register map is equal to GPIO flash map */ |
| #define AIROHA_PWM_REG_SIPO_FLASH_MAP(_n) (0x0054 + (4 * (_n))) |
| |
| #define AIROHA_PWM_REG_CYCLE_CFG_VALUE(_n) (0x0098 + (4 * (_n))) |
| #define AIROHA_PWM_REG_CYCLE_CFG_SHIFT(_n) (8 * (_n)) |
| #define AIROHA_PWM_WAVE_GEN_CYCLE GENMASK(7, 0) |
| |
| /* GPIO/SIPO flash map handles 8 pins in one register */ |
| #define AIROHA_PWM_PINS_PER_FLASH_MAP 8 |
| /* Cycle(Period) registers handles 4 generators in one 32-bit register */ |
| #define AIROHA_PWM_BUCKET_PER_CYCLE_CFG 4 |
| /* Flash(Duty) producer handles 2 generators in one 32-bit register */ |
| #define AIROHA_PWM_BUCKET_PER_FLASH_PROD 2 |
| |
| #define AIROHA_PWM_NUM_BUCKETS 8 |
| /* |
| * The first 16 GPIO pins, GPIO0-GPIO15, are mapped into 16 PWM channels, 0-15. |
| * The SIPO GPIO pins are 17 pins which are mapped into 17 PWM channels, 16-32. |
| * However, we've only got 8 concurrent waveform generators and can therefore |
| * only use up to 8 different combinations of duty cycle and period at a time. |
| */ |
| #define AIROHA_PWM_NUM_GPIO 16 |
| #define AIROHA_PWM_NUM_SIPO 17 |
| #define AIROHA_PWM_MAX_CHANNELS (AIROHA_PWM_NUM_GPIO + AIROHA_PWM_NUM_SIPO) |
| |
| struct airoha_pwm_bucket { |
| /* Concurrent access protected by PWM core */ |
| int used; |
| u32 period_ticks; |
| u32 duty_ticks; |
| }; |
| |
| struct airoha_pwm { |
| struct regmap *regmap; |
| |
| DECLARE_BITMAP(initialized, AIROHA_PWM_MAX_CHANNELS); |
| |
| struct airoha_pwm_bucket buckets[AIROHA_PWM_NUM_BUCKETS]; |
| |
| /* Cache bucket used by each pwm channel */ |
| u8 channel_bucket[AIROHA_PWM_MAX_CHANNELS]; |
| }; |
| |
| /* The PWM hardware supports periods between 4 ms and 1 s */ |
| #define AIROHA_PWM_PERIOD_TICK_NS (4 * NSEC_PER_MSEC) |
| #define AIROHA_PWM_PERIOD_MAX_NS (1 * NSEC_PER_SEC) |
| /* It is represented internally as 1/250 s between 1 and 250. Unit is ticks. */ |
| #define AIROHA_PWM_PERIOD_MIN 1 |
| #define AIROHA_PWM_PERIOD_MAX 250 |
| /* Duty cycle is relative with 255 corresponding to 100% */ |
| #define AIROHA_PWM_DUTY_FULL 255 |
| |
| static void airoha_pwm_get_flash_map_addr_and_shift(unsigned int hwpwm, |
| u32 *addr, u32 *shift) |
| { |
| unsigned int offset, hwpwm_bit; |
| |
| if (hwpwm >= AIROHA_PWM_NUM_GPIO) { |
| unsigned int sipohwpwm = hwpwm - AIROHA_PWM_NUM_GPIO; |
| |
| offset = sipohwpwm / AIROHA_PWM_PINS_PER_FLASH_MAP; |
| hwpwm_bit = sipohwpwm % AIROHA_PWM_PINS_PER_FLASH_MAP; |
| |
| /* One FLASH_MAP register handles 8 pins */ |
| *shift = AIROHA_PWM_REG_GPIO_FLASH_MAP_SHIFT(hwpwm_bit); |
| *addr = AIROHA_PWM_REG_SIPO_FLASH_MAP(offset); |
| } else { |
| offset = hwpwm / AIROHA_PWM_PINS_PER_FLASH_MAP; |
| hwpwm_bit = hwpwm % AIROHA_PWM_PINS_PER_FLASH_MAP; |
| |
| /* One FLASH_MAP register handles 8 pins */ |
| *shift = AIROHA_PWM_REG_GPIO_FLASH_MAP_SHIFT(hwpwm_bit); |
| *addr = AIROHA_PWM_REG_GPIO_FLASH_MAP(offset); |
| } |
| } |
| |
| static u32 airoha_pwm_get_period_ticks_from_ns(u32 period_ns) |
| { |
| return period_ns / AIROHA_PWM_PERIOD_TICK_NS; |
| } |
| |
| static u32 airoha_pwm_get_duty_ticks_from_ns(u32 period_ns, u32 duty_ns) |
| { |
| return mul_u64_u32_div(duty_ns, AIROHA_PWM_DUTY_FULL, period_ns); |
| } |
| |
| static u32 airoha_pwm_get_period_ns_from_ticks(u32 period_tick) |
| { |
| return period_tick * AIROHA_PWM_PERIOD_TICK_NS; |
| } |
| |
| static u32 airoha_pwm_get_duty_ns_from_ticks(u32 period_tick, u32 duty_tick) |
| { |
| u32 period_ns = period_tick * AIROHA_PWM_PERIOD_TICK_NS; |
| |
| /* |
| * Overflow can't occur in multiplication as duty_tick is just 8 bit |
| * and period_ns is clamped to AIROHA_PWM_PERIOD_MAX_NS and fit in a |
| * u64. |
| */ |
| return DIV_U64_ROUND_UP(duty_tick * period_ns, AIROHA_PWM_DUTY_FULL); |
| } |
| |
| static int airoha_pwm_get_bucket(struct airoha_pwm *pc, int bucket, |
| u64 *period_ns, u64 *duty_ns) |
| { |
| struct regmap *map = pc->regmap; |
| u32 period_tick, duty_tick; |
| unsigned int offset; |
| u32 shift, val; |
| int ret; |
| |
| offset = bucket / AIROHA_PWM_BUCKET_PER_CYCLE_CFG; |
| shift = bucket % AIROHA_PWM_BUCKET_PER_CYCLE_CFG; |
| shift = AIROHA_PWM_REG_CYCLE_CFG_SHIFT(shift); |
| |
| ret = regmap_read(map, AIROHA_PWM_REG_CYCLE_CFG_VALUE(offset), &val); |
| if (ret) |
| return ret; |
| |
| period_tick = FIELD_GET(AIROHA_PWM_WAVE_GEN_CYCLE, val >> shift); |
| *period_ns = airoha_pwm_get_period_ns_from_ticks(period_tick); |
| |
| offset = bucket / AIROHA_PWM_BUCKET_PER_FLASH_PROD; |
| shift = bucket % AIROHA_PWM_BUCKET_PER_FLASH_PROD; |
| shift = AIROHA_PWM_REG_GPIO_FLASH_PRD_SHIFT(shift); |
| |
| ret = regmap_read(map, AIROHA_PWM_REG_GPIO_FLASH_PRD_SET(offset), |
| &val); |
| if (ret) |
| return ret; |
| |
| duty_tick = FIELD_GET(AIROHA_PWM_GPIO_FLASH_PRD_HIGH, val >> shift); |
| *duty_ns = airoha_pwm_get_duty_ns_from_ticks(period_tick, duty_tick); |
| |
| return 0; |
| } |
| |
| static int airoha_pwm_get_generator(struct airoha_pwm *pc, u32 duty_ticks, |
| u32 period_ticks) |
| { |
| int best = -ENOENT, unused = -ENOENT; |
| u32 duty_ns, best_duty_ns = 0; |
| u32 best_period_ticks = 0; |
| unsigned int i; |
| |
| duty_ns = airoha_pwm_get_duty_ns_from_ticks(period_ticks, duty_ticks); |
| |
| for (i = 0; i < ARRAY_SIZE(pc->buckets); i++) { |
| struct airoha_pwm_bucket *bucket = &pc->buckets[i]; |
| u32 bucket_period_ticks = bucket->period_ticks; |
| u32 bucket_duty_ticks = bucket->duty_ticks; |
| |
| /* If found, save an unused bucket to return it later */ |
| if (!bucket->used) { |
| unused = i; |
| continue; |
| } |
| |
| /* We found a matching bucket, exit early */ |
| if (duty_ticks == bucket_duty_ticks && |
| period_ticks == bucket_period_ticks) |
| return i; |
| |
| /* |
| * Unlike duty cycle zero, which can be handled by |
| * disabling PWM, a generator is needed for full duty |
| * cycle but it can be reused regardless of period |
| */ |
| if (duty_ticks == AIROHA_PWM_DUTY_FULL && |
| bucket_duty_ticks == AIROHA_PWM_DUTY_FULL) |
| return i; |
| |
| /* |
| * With an unused bucket available, skip searching for |
| * a bucket to recycle (closer to the requested period/duty) |
| */ |
| if (unused >= 0) |
| continue; |
| |
| /* Ignore bucket with invalid period */ |
| if (bucket_period_ticks > period_ticks) |
| continue; |
| |
| /* |
| * Search for a bucket closer to the requested period |
| * that has the maximal possible period that isn't bigger |
| * than the requested period. For that period pick the maximal |
| * duty cycle that isn't bigger than the requested duty_cycle. |
| */ |
| if (bucket_period_ticks >= best_period_ticks) { |
| u32 bucket_duty_ns = airoha_pwm_get_duty_ns_from_ticks(bucket_period_ticks, |
| bucket_duty_ticks); |
| |
| /* Skip bucket that goes over the requested duty */ |
| if (bucket_duty_ns > duty_ns) |
| continue; |
| |
| if (bucket_duty_ns > best_duty_ns) { |
| best_period_ticks = bucket_period_ticks; |
| best_duty_ns = bucket_duty_ns; |
| best = i; |
| } |
| } |
| } |
| |
| /* Return an unused bucket or the best one found (if ever) */ |
| return unused >= 0 ? unused : best; |
| } |
| |
| static void airoha_pwm_release_bucket_config(struct airoha_pwm *pc, |
| unsigned int hwpwm) |
| { |
| int bucket; |
| |
| /* Nothing to clear, PWM channel never used */ |
| if (!test_bit(hwpwm, pc->initialized)) |
| return; |
| |
| bucket = pc->channel_bucket[hwpwm]; |
| pc->buckets[bucket].used--; |
| } |
| |
| static int airoha_pwm_apply_bucket_config(struct airoha_pwm *pc, unsigned int bucket, |
| u32 duty_ticks, u32 period_ticks) |
| { |
| u32 mask, shift, val; |
| u32 offset; |
| int ret; |
| |
| offset = bucket / AIROHA_PWM_BUCKET_PER_CYCLE_CFG; |
| shift = bucket % AIROHA_PWM_BUCKET_PER_CYCLE_CFG; |
| shift = AIROHA_PWM_REG_CYCLE_CFG_SHIFT(shift); |
| |
| /* Configure frequency divisor */ |
| mask = AIROHA_PWM_WAVE_GEN_CYCLE << shift; |
| val = FIELD_PREP(AIROHA_PWM_WAVE_GEN_CYCLE, period_ticks) << shift; |
| ret = regmap_update_bits(pc->regmap, AIROHA_PWM_REG_CYCLE_CFG_VALUE(offset), |
| mask, val); |
| if (ret) |
| return ret; |
| |
| offset = bucket / AIROHA_PWM_BUCKET_PER_FLASH_PROD; |
| shift = bucket % AIROHA_PWM_BUCKET_PER_FLASH_PROD; |
| shift = AIROHA_PWM_REG_GPIO_FLASH_PRD_SHIFT(shift); |
| |
| /* Configure duty cycle */ |
| mask = AIROHA_PWM_GPIO_FLASH_PRD_HIGH << shift; |
| val = FIELD_PREP(AIROHA_PWM_GPIO_FLASH_PRD_HIGH, duty_ticks) << shift; |
| ret = regmap_update_bits(pc->regmap, AIROHA_PWM_REG_GPIO_FLASH_PRD_SET(offset), |
| mask, val); |
| if (ret) |
| return ret; |
| |
| mask = AIROHA_PWM_GPIO_FLASH_PRD_LOW << shift; |
| val = FIELD_PREP(AIROHA_PWM_GPIO_FLASH_PRD_LOW, |
| AIROHA_PWM_DUTY_FULL - duty_ticks) << shift; |
| return regmap_update_bits(pc->regmap, AIROHA_PWM_REG_GPIO_FLASH_PRD_SET(offset), |
| mask, val); |
| } |
| |
| static int airoha_pwm_consume_generator(struct airoha_pwm *pc, |
| u32 duty_ticks, u32 period_ticks, |
| unsigned int hwpwm) |
| { |
| bool config_bucket = false; |
| int bucket, ret; |
| |
| /* |
| * Search for a bucket that already satisfies duty and period |
| * or an unused one. |
| * If not found, -ENOENT is returned. |
| */ |
| bucket = airoha_pwm_get_generator(pc, duty_ticks, period_ticks); |
| if (bucket < 0) |
| return bucket; |
| |
| /* Release previous used bucket (if any) */ |
| airoha_pwm_release_bucket_config(pc, hwpwm); |
| |
| if (!pc->buckets[bucket].used) |
| config_bucket = true; |
| pc->buckets[bucket].used++; |
| |
| if (config_bucket) { |
| pc->buckets[bucket].period_ticks = period_ticks; |
| pc->buckets[bucket].duty_ticks = duty_ticks; |
| ret = airoha_pwm_apply_bucket_config(pc, bucket, |
| duty_ticks, |
| period_ticks); |
| if (ret) { |
| pc->buckets[bucket].used--; |
| return ret; |
| } |
| } |
| |
| return bucket; |
| } |
| |
| static int airoha_pwm_sipo_init(struct airoha_pwm *pc) |
| { |
| u32 val; |
| int ret; |
| |
| ret = regmap_clear_bits(pc->regmap, AIROHA_PWM_REG_SIPO_FLASH_MODE_CFG, |
| AIROHA_PWM_SERIAL_GPIO_MODE_74HC164); |
| if (ret) |
| return ret; |
| |
| /* Configure shift register chip clock timings, use 32x divisor */ |
| ret = regmap_write(pc->regmap, AIROHA_PWM_REG_SGPIO_CLK_DIVR, |
| AIROHA_PWM_SGPIO_CLK_DIVR_32); |
| if (ret) |
| return ret; |
| |
| /* |
| * Configure the shift register chip clock delay. This needs |
| * to be configured based on the chip characteristics when the SoC |
| * apply the shift register configuration. |
| * This doesn't affect actual PWM operation and is only specific to |
| * the shift register chip. |
| * |
| * For 74HC164 we set it to 0. |
| * |
| * For reference, the actual delay applied is the internal clock |
| * feed to the SGPIO chip + 1. |
| * |
| * From documentation is specified that clock delay should not be |
| * greater than (AIROHA_PWM_REG_SGPIO_CLK_DIVR / 2) - 1. |
| */ |
| ret = regmap_write(pc->regmap, AIROHA_PWM_REG_SGPIO_CLK_DLY, 0); |
| if (ret) |
| return ret; |
| |
| /* |
| * It is necessary to explicitly shift out all zeros after muxing |
| * to initialize the shift register before enabling PWM |
| * mode because in PWM mode SIPO will not start shifting until |
| * it needs to output a non-zero value (bit 31 of led_data |
| * indicates shifting in progress and it must return to zero |
| * before led_data can be written or PWM mode can be set). |
| */ |
| ret = regmap_read_poll_timeout(pc->regmap, AIROHA_PWM_REG_SGPIO_LED_DATA, val, |
| !(val & AIROHA_PWM_SGPIO_LED_DATA_SHIFT_FLAG), |
| 10, 200 * USEC_PER_MSEC); |
| if (ret) |
| return ret; |
| |
| ret = regmap_clear_bits(pc->regmap, AIROHA_PWM_REG_SGPIO_LED_DATA, |
| AIROHA_PWM_SGPIO_LED_DATA_DATA); |
| if (ret) |
| return ret; |
| ret = regmap_read_poll_timeout(pc->regmap, AIROHA_PWM_REG_SGPIO_LED_DATA, val, |
| !(val & AIROHA_PWM_SGPIO_LED_DATA_SHIFT_FLAG), |
| 10, 200 * USEC_PER_MSEC); |
| if (ret) |
| return ret; |
| |
| /* Set SIPO in PWM mode */ |
| return regmap_set_bits(pc->regmap, AIROHA_PWM_REG_SIPO_FLASH_MODE_CFG, |
| AIROHA_PWM_SERIAL_GPIO_FLASH_MODE); |
| } |
| |
| static int airoha_pwm_config_flash_map(struct airoha_pwm *pc, |
| unsigned int hwpwm, int index) |
| { |
| unsigned int addr; |
| u32 shift; |
| int ret; |
| |
| airoha_pwm_get_flash_map_addr_and_shift(hwpwm, &addr, &shift); |
| |
| /* negative index means disable PWM channel */ |
| if (index < 0) { |
| /* |
| * If we need to disable the PWM, we just put low the |
| * GPIO. No need to setup buckets. |
| */ |
| return regmap_clear_bits(pc->regmap, addr, |
| AIROHA_PWM_GPIO_FLASH_EN << shift); |
| } |
| |
| ret = regmap_update_bits(pc->regmap, addr, |
| AIROHA_PWM_GPIO_FLASH_SET_ID << shift, |
| FIELD_PREP(AIROHA_PWM_GPIO_FLASH_SET_ID, index) << shift); |
| if (ret) |
| return ret; |
| |
| return regmap_set_bits(pc->regmap, addr, AIROHA_PWM_GPIO_FLASH_EN << shift); |
| } |
| |
| static int airoha_pwm_config(struct airoha_pwm *pc, struct pwm_device *pwm, |
| u32 period_ticks, u32 duty_ticks) |
| { |
| unsigned int hwpwm = pwm->hwpwm; |
| int bucket, ret; |
| |
| bucket = airoha_pwm_consume_generator(pc, duty_ticks, period_ticks, |
| hwpwm); |
| if (bucket < 0) |
| return bucket; |
| |
| ret = airoha_pwm_config_flash_map(pc, hwpwm, bucket); |
| if (ret) { |
| pc->buckets[bucket].used--; |
| return ret; |
| } |
| |
| __set_bit(hwpwm, pc->initialized); |
| pc->channel_bucket[hwpwm] = bucket; |
| |
| /* |
| * SIPO are special GPIO attached to a shift register chip. The handling |
| * of this chip is internal to the SoC that takes care of applying the |
| * values based on the flash map. To apply a new flash map, it's needed |
| * to trigger a refresh on the shift register chip. |
| * If a SIPO is getting configuring , always reinit the shift register |
| * chip to make sure the correct flash map is applied. |
| * Skip reconfiguring the shift register if the related hwpwm |
| * is disabled (as it doesn't need to be mapped). |
| */ |
| if (hwpwm >= AIROHA_PWM_NUM_GPIO) { |
| ret = airoha_pwm_sipo_init(pc); |
| if (ret) { |
| airoha_pwm_release_bucket_config(pc, hwpwm); |
| return ret; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static void airoha_pwm_disable(struct airoha_pwm *pc, struct pwm_device *pwm) |
| { |
| /* Disable PWM and release the bucket */ |
| airoha_pwm_config_flash_map(pc, pwm->hwpwm, -1); |
| airoha_pwm_release_bucket_config(pc, pwm->hwpwm); |
| |
| __clear_bit(pwm->hwpwm, pc->initialized); |
| |
| /* If no SIPO is used, disable the shift register chip */ |
| if (!bitmap_read(pc->initialized, |
| AIROHA_PWM_NUM_GPIO, AIROHA_PWM_NUM_SIPO)) |
| regmap_clear_bits(pc->regmap, AIROHA_PWM_REG_SIPO_FLASH_MODE_CFG, |
| AIROHA_PWM_SERIAL_GPIO_FLASH_MODE); |
| } |
| |
| static int airoha_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm, |
| const struct pwm_state *state) |
| { |
| struct airoha_pwm *pc = pwmchip_get_drvdata(chip); |
| u32 period_ticks, duty_ticks; |
| u32 period_ns, duty_ns; |
| |
| if (!state->enabled) { |
| airoha_pwm_disable(pc, pwm); |
| return 0; |
| } |
| |
| /* Only normal polarity is supported */ |
| if (state->polarity == PWM_POLARITY_INVERSED) |
| return -EINVAL; |
| |
| /* Exit early if period is less than minimum supported */ |
| if (state->period < AIROHA_PWM_PERIOD_TICK_NS) |
| return -EINVAL; |
| |
| /* Clamp period to MAX supported value */ |
| if (state->period > AIROHA_PWM_PERIOD_MAX_NS) |
| period_ns = AIROHA_PWM_PERIOD_MAX_NS; |
| else |
| period_ns = state->period; |
| |
| /* Validate duty to configured period */ |
| if (state->duty_cycle > period_ns) |
| duty_ns = period_ns; |
| else |
| duty_ns = state->duty_cycle; |
| |
| /* Convert period ns to ticks */ |
| period_ticks = airoha_pwm_get_period_ticks_from_ns(period_ns); |
| /* Convert period ticks to ns again for cosistent duty tick calculation */ |
| period_ns = airoha_pwm_get_period_ns_from_ticks(period_ticks); |
| duty_ticks = airoha_pwm_get_duty_ticks_from_ns(period_ns, duty_ns); |
| |
| return airoha_pwm_config(pc, pwm, period_ticks, duty_ticks); |
| } |
| |
| static int airoha_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm, |
| struct pwm_state *state) |
| { |
| struct airoha_pwm *pc = pwmchip_get_drvdata(chip); |
| int ret, hwpwm = pwm->hwpwm; |
| u32 addr, shift, val; |
| u8 bucket; |
| |
| airoha_pwm_get_flash_map_addr_and_shift(hwpwm, &addr, &shift); |
| |
| ret = regmap_read(pc->regmap, addr, &val); |
| if (ret) |
| return ret; |
| |
| state->enabled = FIELD_GET(AIROHA_PWM_GPIO_FLASH_EN, val >> shift); |
| if (!state->enabled) |
| return 0; |
| |
| state->polarity = PWM_POLARITY_NORMAL; |
| |
| bucket = FIELD_GET(AIROHA_PWM_GPIO_FLASH_SET_ID, val >> shift); |
| return airoha_pwm_get_bucket(pc, bucket, &state->period, |
| &state->duty_cycle); |
| } |
| |
| static const struct pwm_ops airoha_pwm_ops = { |
| .apply = airoha_pwm_apply, |
| .get_state = airoha_pwm_get_state, |
| }; |
| |
| static int airoha_pwm_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct airoha_pwm *pc; |
| struct pwm_chip *chip; |
| int ret; |
| |
| chip = devm_pwmchip_alloc(dev, AIROHA_PWM_MAX_CHANNELS, sizeof(*pc)); |
| if (IS_ERR(chip)) |
| return PTR_ERR(chip); |
| |
| chip->ops = &airoha_pwm_ops; |
| pc = pwmchip_get_drvdata(chip); |
| |
| pc->regmap = device_node_to_regmap(dev_of_node(dev->parent)); |
| if (IS_ERR(pc->regmap)) |
| return dev_err_probe(dev, PTR_ERR(pc->regmap), "Failed to get PWM regmap\n"); |
| |
| ret = devm_pwmchip_add(dev, chip); |
| if (ret) |
| return dev_err_probe(dev, ret, "Failed to add PWM chip\n"); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id airoha_pwm_of_match[] = { |
| { .compatible = "airoha,en7581-pwm" }, |
| { /* sentinel */ } |
| }; |
| MODULE_DEVICE_TABLE(of, airoha_pwm_of_match); |
| |
| static struct platform_driver airoha_pwm_driver = { |
| .driver = { |
| .name = "pwm-airoha", |
| .probe_type = PROBE_PREFER_ASYNCHRONOUS, |
| .of_match_table = airoha_pwm_of_match, |
| }, |
| .probe = airoha_pwm_probe, |
| }; |
| module_platform_driver(airoha_pwm_driver); |
| |
| MODULE_AUTHOR("Lorenzo Bianconi <lorenzo@kernel.org>"); |
| MODULE_AUTHOR("Markus Gothe <markus.gothe@genexis.eu>"); |
| MODULE_AUTHOR("Benjamin Larsson <benjamin.larsson@genexis.eu>"); |
| MODULE_AUTHOR("Christian Marangi <ansuelsmth@gmail.com>"); |
| MODULE_DESCRIPTION("Airoha EN7581 PWM driver"); |
| MODULE_LICENSE("GPL"); |