290 lines
6.9 KiB
C
290 lines
6.9 KiB
C
/*
|
|
* Copyright (c) 2022 Nick Ward <nix.ward@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#define DT_DRV_COMPAT nxp_pca9685_pwm
|
|
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/drivers/i2c.h>
|
|
#include <zephyr/drivers/pwm.h>
|
|
#include <zephyr/sys/util_macro.h>
|
|
#include <zephyr/kernel.h>
|
|
|
|
#include <zephyr/logging/log.h>
|
|
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);
|