/* * Copyright (c) 2022 Nick Ward * * SPDX-License-Identifier: Apache-2.0 */ #define DT_DRV_COMPAT nxp_pca9685_pwm #include #include #include #include #include #include LOG_MODULE_REGISTER(pwm_pca9685, CONFIG_PWM_LOG_LEVEL); #define OSC_CLOCK_MAX 50000000 #define CHANNEL_CNT 16 #define ADDR_MODE1 0x00 #define SLEEP BIT(4) #define AUTO_INC BIT(5) #define RESTART BIT(7) #define ADDR_MODE2 0x01 #define OUTDRV_TOTEM_POLE BIT(2) #define OCH_ON_ACK BIT(3) #define ADDR_LED0_ON_L 0x06 #define ADDR_LED_ON_L(n) (ADDR_LED0_ON_L + (4 * n)) #define LED_FULL_ON BIT(4) #define LED_FULL_OFF BIT(4) #define BYTE_N(word, n) ((word >> (8 * n)) & 0xFF) #define ADDR_PRE_SCALE 0xFE /* See PCA9585 datasheet Rev. 4 - 16 April 2015 section 7.3.5 */ #define OSC_CLOCK_INTERNAL 25000000 #define PWM_STEPS 4096 #define PRE_SCALE_MIN 0x03 #define PRE_SCALE_MAX 0xFF #define PRE_SCALE_DEFAULT 0x1E #define PWM_PERIOD_COUNT_PS(pre_scale) (PWM_STEPS * (pre_scale + 1)) /* * When using the internal oscillator this corresponds to an * update rate of 1526 Hz */ #define PWM_PERIOD_COUNT_MIN PWM_PERIOD_COUNT_PS(PRE_SCALE_MIN) /* * When using the internal oscillator this corresponds to an * update rate of 24 Hz */ #define PWM_PERIOD_COUNT_MAX PWM_PERIOD_COUNT_PS(PRE_SCALE_MAX) /* * When using the internal oscillator this corresponds to an * update rate of 200 Hz */ #define PWM_PERIOD_COUNT_DEFAULT PWM_PERIOD_COUNT_PS(PRE_SCALE_DEFAULT) /* * Time allowed for the oscillator to stabilize after the PCA9685's * RESTART mode is used to wake the chip from SLEEP. * See PCA9685 datasheet section 7.3.1.1 */ #define OSCILLATOR_STABILIZE K_USEC(500) struct pca9685_config { struct i2c_dt_spec i2c; bool outdrv_open_drain; bool och_on_ack; bool invrt; }; struct pca9685_data { struct k_mutex mutex; uint8_t pre_scale; }; static int set_reg(const struct device *dev, uint8_t addr, uint8_t value) { const struct pca9685_config *config = dev->config; uint8_t buf[] = {addr, value}; int ret; ret = i2c_write_dt(&config->i2c, buf, sizeof(buf)); if (ret != 0) { LOG_ERR("I2C write [0x%02X]=0x%02X: %d", addr, value, ret); } else { LOG_DBG("[0x%02X]=0x%02X", addr, value); } return ret; } static int get_reg(const struct device *dev, uint8_t addr, uint8_t *value) { const struct pca9685_config *config = dev->config; int ret; ret = i2c_write_read_dt(&config->i2c, &addr, sizeof(addr), value, sizeof(*value)); if (ret != 0) { LOG_ERR("I2C write [0x%02X]=0x%02X: %d", addr, *value, ret); } return ret; } static int set_pre_scale(const struct device *dev, uint8_t value) { struct pca9685_data *data = dev->data; uint8_t mode1; int ret; uint8_t restart = RESTART; k_mutex_lock(&data->mutex, K_FOREVER); /* Unblock write to PRE_SCALE by setting SLEEP bit to logic 1 */ ret = set_reg(dev, ADDR_MODE1, AUTO_INC | SLEEP); if (ret != 0) { goto out; } ret = get_reg(dev, ADDR_MODE1, &mode1); if (ret != 0) { goto out; } if ((mode1 & RESTART) == 0x00) { restart = 0; } ret = set_reg(dev, ADDR_PRE_SCALE, value); if (ret != 0) { goto out; } /* Clear SLEEP bit - See datasheet section 7.3.1.1 */ ret = set_reg(dev, ADDR_MODE1, AUTO_INC); if (ret != 0) { goto out; } k_sleep(OSCILLATOR_STABILIZE); ret = set_reg(dev, ADDR_MODE1, AUTO_INC | restart); if (ret != 0) { goto out; } out: k_mutex_unlock(&data->mutex); return ret; } static int pca9685_set_cycles(const struct device *dev, uint32_t channel, uint32_t period_count, uint32_t pulse_count, pwm_flags_t flags) { const struct pca9685_config *config = dev->config; struct pca9685_data *data = dev->data; uint8_t buf[5] = { 0 }; uint32_t led_off_count; int32_t pre_scale; int ret; ARG_UNUSED(flags); if (channel >= CHANNEL_CNT) { LOG_WRN("channel out of range: %u", channel); return -EINVAL; } pre_scale = DIV_ROUND_UP((int64_t)period_count, PWM_STEPS) - 1; if (pre_scale < PRE_SCALE_MIN) { LOG_ERR("period_count %u < %u (min)", period_count, PWM_PERIOD_COUNT_MIN); return -ENOTSUP; } else if (pre_scale > PRE_SCALE_MAX) { LOG_ERR("period_count %u > %u (max)", period_count, PWM_PERIOD_COUNT_MAX); return -ENOTSUP; } /* Only one output frequency - simple conversion to equivalent PWM */ if (pre_scale != data->pre_scale) { data->pre_scale = pre_scale; ret = set_pre_scale(dev, pre_scale); if (ret != 0) { return ret; } } /* Adjust PWM output for the resolution of the PCA9685 */ led_off_count = DIV_ROUND_UP(pulse_count * PWM_STEPS, period_count); buf[0] = ADDR_LED_ON_L(channel); if (led_off_count == 0) { buf[4] = LED_FULL_OFF; } else if (led_off_count < PWM_STEPS) { /* LED turns on at count 0 */ buf[3] = BYTE_N(led_off_count, 0); buf[4] = BYTE_N(led_off_count, 1); } else { buf[2] = LED_FULL_ON; } return i2c_write_dt(&config->i2c, buf, sizeof(buf)); } static int pca9685_get_cycles_per_sec(const struct device *dev, uint32_t channel, uint64_t *cycles) { ARG_UNUSED(dev); ARG_UNUSED(channel); *cycles = OSC_CLOCK_INTERNAL; return 0; } static const struct pwm_driver_api pca9685_api = { .set_cycles = pca9685_set_cycles, .get_cycles_per_sec = pca9685_get_cycles_per_sec, }; static int pca9685_init(const struct device *dev) { const struct pca9685_config *config = dev->config; struct pca9685_data *data = dev->data; uint8_t value; int ret; (void)k_mutex_init(&data->mutex); if (!i2c_is_ready_dt(&config->i2c)) { LOG_ERR("I2C device not ready"); return -ENODEV; } ret = set_reg(dev, ADDR_MODE1, AUTO_INC); if (ret != 0) { LOG_ERR("set_reg MODE1: %d", ret); return ret; } value = 0x00; if (!config->outdrv_open_drain) { value |= OUTDRV_TOTEM_POLE; } if (config->och_on_ack) { value |= OCH_ON_ACK; } ret = set_reg(dev, ADDR_MODE2, value); if (ret != 0) { LOG_ERR("set_reg MODE2: %d", ret); return ret; } return 0; } #define PCA9685_INIT(inst) \ static const struct pca9685_config pca9685_##inst##_config = { \ .i2c = I2C_DT_SPEC_INST_GET(inst), \ .outdrv_open_drain = DT_INST_PROP(inst, open_drain), \ .och_on_ack = DT_INST_PROP(inst, och_on_ack), \ .invrt = DT_INST_PROP(inst, invert), \ }; \ \ static struct pca9685_data pca9685_##inst##_data; \ \ DEVICE_DT_INST_DEFINE(inst, pca9685_init, NULL, \ &pca9685_##inst##_data, \ &pca9685_##inst##_config, POST_KERNEL, \ CONFIG_PWM_INIT_PRIORITY, \ &pca9685_api); DT_INST_FOREACH_STATUS_OKAY(PCA9685_INIT);