465 lines
11 KiB
C
465 lines
11 KiB
C
/*
|
|
* Copyright (c) 2019 Intel Corporation
|
|
* Copyright (c) 2022 Microchip Technololgy Inc.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#define DT_DRV_COMPAT microchip_xec_pwm
|
|
|
|
#include <errno.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/drivers/pwm.h>
|
|
#ifdef CONFIG_SOC_SERIES_MEC172X
|
|
#include <zephyr/drivers/clock_control/mchp_xec_clock_control.h>
|
|
#include <zephyr/drivers/interrupt_controller/intc_mchp_xec_ecia.h>
|
|
#endif
|
|
#include <zephyr/drivers/pinctrl.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <zephyr/pm/device.h>
|
|
|
|
#include <soc.h>
|
|
|
|
LOG_MODULE_REGISTER(pwm_mchp_xec, CONFIG_PWM_LOG_LEVEL);
|
|
|
|
/* Minimal on/off are 1 & 1 both are incremented, so 4.
|
|
* 0 cannot be set (used for full low/high output) so a
|
|
* combination of on_off of 2 is not possible.
|
|
*/
|
|
#define XEC_PWM_LOWEST_ON_OFF 4U
|
|
/* Maximal on/off are UINT16_T, both are incremented.
|
|
* Multiplied by the highest divider: 16
|
|
*/
|
|
#define XEC_PWM_HIGHEST_ON_OFF (2U * (UINT16_MAX + 1U) * 16U)
|
|
|
|
#define XEC_PWM_MIN_HIGH_CLK_FREQ \
|
|
(MCHP_PWM_INPUT_FREQ_HI / XEC_PWM_HIGHEST_ON_OFF)
|
|
#define XEC_PWM_MAX_LOW_CLK_FREQ \
|
|
(MCHP_PWM_INPUT_FREQ_LO / XEC_PWM_LOWEST_ON_OFF)
|
|
/* Precision factor for frequency calculation
|
|
* To mitigate frequency comparision up to the first digit after 0.
|
|
*/
|
|
#define XEC_PWM_FREQ_PF 10U
|
|
/* Precision factor for DC calculation
|
|
* To avoid losing some digits after 0.
|
|
*/
|
|
#define XEC_PWM_DC_PF 100000U
|
|
/* Lowest reachable frequency */
|
|
#define XEC_PWM_FREQ_LIMIT 1 /* 0.1hz * XEC_PWM_FREQ_PF */
|
|
|
|
struct pwm_xec_config {
|
|
struct pwm_regs * const regs;
|
|
uint8_t pcr_idx;
|
|
uint8_t pcr_pos;
|
|
const struct pinctrl_dev_config *pcfg;
|
|
};
|
|
|
|
struct xec_params {
|
|
uint32_t on;
|
|
uint32_t off;
|
|
uint8_t div;
|
|
};
|
|
|
|
struct pwm_xec_data {
|
|
uint32_t config;
|
|
};
|
|
|
|
#define NUM_DIV_ELEMS 16
|
|
|
|
static const uint32_t max_freq_high_on_div[NUM_DIV_ELEMS] = {
|
|
48000000,
|
|
24000000,
|
|
16000000,
|
|
12000000,
|
|
9600000,
|
|
8000000,
|
|
6857142,
|
|
6000000,
|
|
5333333,
|
|
4800000,
|
|
4363636,
|
|
4000000,
|
|
3692307,
|
|
3428571,
|
|
3200000,
|
|
3000000,
|
|
};
|
|
|
|
static const uint32_t max_freq_low_on_div[NUM_DIV_ELEMS] = {
|
|
100000,
|
|
50000,
|
|
33333,
|
|
25000,
|
|
20000,
|
|
16666,
|
|
14285,
|
|
12500,
|
|
11111,
|
|
10000,
|
|
9090,
|
|
8333,
|
|
7692,
|
|
7142,
|
|
6666,
|
|
6250,
|
|
};
|
|
|
|
static uint32_t xec_compute_frequency(uint32_t clk, uint32_t on, uint32_t off)
|
|
{
|
|
return ((clk * XEC_PWM_FREQ_PF)/((on + 1) + (off + 1)));
|
|
}
|
|
|
|
static uint16_t xec_select_div(uint32_t freq, const uint32_t max_freq[16])
|
|
{
|
|
uint8_t i;
|
|
|
|
if (freq >= max_freq[3]) {
|
|
return 0;
|
|
}
|
|
|
|
freq *= XEC_PWM_LOWEST_ON_OFF;
|
|
|
|
for (i = 0; i < 15; i++) {
|
|
if (freq >= max_freq[i]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
static void xec_compute_on_off(uint32_t freq, uint32_t dc, uint32_t clk,
|
|
uint32_t *on, uint32_t *off)
|
|
{
|
|
uint64_t on_off;
|
|
|
|
on_off = (clk * 10) / freq;
|
|
|
|
*on = ((on_off * dc) / XEC_PWM_DC_PF) - 1;
|
|
*off = on_off - *on - 2;
|
|
}
|
|
|
|
static uint32_t xec_compute_dc(uint32_t on, uint32_t off)
|
|
{
|
|
int dc = (on + 1) + (off + 1);
|
|
|
|
/* Make calculation in uint64_t since XEC_PWM_DC_PF is large */
|
|
dc = (((uint64_t)(on + 1) * XEC_PWM_DC_PF) / dc);
|
|
|
|
return (uint32_t)dc;
|
|
}
|
|
|
|
static uint16_t xec_compare_div_on_off(uint32_t target_freq, uint32_t dc,
|
|
const uint32_t max_freq[16],
|
|
uint8_t div_a, uint8_t div_b,
|
|
uint32_t *on_a, uint32_t *off_a)
|
|
{
|
|
uint32_t freq_a, freq_b, on_b, off_b;
|
|
|
|
xec_compute_on_off(target_freq, dc, max_freq[div_a],
|
|
on_a, off_a);
|
|
|
|
freq_a = xec_compute_frequency(max_freq[div_a], *on_a, *off_a);
|
|
|
|
xec_compute_on_off(target_freq, dc, max_freq[div_b],
|
|
&on_b, &off_b);
|
|
|
|
freq_b = xec_compute_frequency(max_freq[div_b], on_b, off_b);
|
|
|
|
if ((target_freq - freq_a) < (target_freq - freq_b)) {
|
|
if ((*on_a <= UINT16_MAX) && (*off_a <= UINT16_MAX)) {
|
|
return div_a;
|
|
}
|
|
}
|
|
|
|
if ((on_b <= UINT16_MAX) && (off_b <= UINT16_MAX)) {
|
|
*on_a = on_b;
|
|
*off_a = off_b;
|
|
|
|
return div_b;
|
|
}
|
|
|
|
return div_a;
|
|
}
|
|
|
|
static uint8_t xec_select_best_div_on_off(uint32_t target_freq, uint32_t dc,
|
|
const uint32_t max_freq[16],
|
|
uint32_t *on, uint32_t *off)
|
|
{
|
|
int div_comp;
|
|
uint8_t div;
|
|
|
|
div = xec_select_div(target_freq, max_freq);
|
|
|
|
for (div_comp = (int)div - 1; div_comp >= 0; div_comp--) {
|
|
div = xec_compare_div_on_off(target_freq, dc, max_freq,
|
|
div, div_comp, on, off);
|
|
}
|
|
|
|
return div;
|
|
}
|
|
|
|
static struct xec_params *xec_compare_params(uint32_t target_freq,
|
|
struct xec_params *hc_params,
|
|
struct xec_params *lc_params)
|
|
{
|
|
struct xec_params *params;
|
|
uint32_t freq_h = 0;
|
|
uint32_t freq_l = 0;
|
|
|
|
if (hc_params->div < NUM_DIV_ELEMS) {
|
|
freq_h = xec_compute_frequency(
|
|
max_freq_high_on_div[hc_params->div],
|
|
hc_params->on,
|
|
hc_params->off);
|
|
}
|
|
|
|
if (lc_params->div < NUM_DIV_ELEMS) {
|
|
freq_l = xec_compute_frequency(
|
|
max_freq_low_on_div[lc_params->div],
|
|
lc_params->on,
|
|
lc_params->off);
|
|
}
|
|
|
|
if (abs((int)target_freq - (int)freq_h) <
|
|
abs((int)target_freq - (int)freq_l)) {
|
|
params = hc_params;
|
|
} else {
|
|
params = lc_params;
|
|
}
|
|
|
|
LOG_DBG("\tFrequency (x%u): %u", XEC_PWM_FREQ_PF, freq_h);
|
|
LOG_DBG("\tOn %s clock, ON %u OFF %u DIV %u",
|
|
params == hc_params ? "High" : "Low",
|
|
params->on, params->off, params->div);
|
|
|
|
return params;
|
|
}
|
|
|
|
static void xec_compute_and_set_parameters(const struct device *dev,
|
|
uint32_t target_freq,
|
|
uint32_t on, uint32_t off)
|
|
{
|
|
const struct pwm_xec_config * const cfg = dev->config;
|
|
struct pwm_regs * const regs = cfg->regs;
|
|
bool compute_high, compute_low;
|
|
struct xec_params hc_params;
|
|
struct xec_params lc_params;
|
|
struct xec_params *params;
|
|
uint32_t dc, cfgval;
|
|
|
|
dc = xec_compute_dc(on, off);
|
|
|
|
compute_high = (target_freq >= XEC_PWM_MIN_HIGH_CLK_FREQ);
|
|
compute_low = (target_freq <= XEC_PWM_MAX_LOW_CLK_FREQ);
|
|
|
|
LOG_DBG("Target freq (x%u): %u and DC %u per-cent",
|
|
XEC_PWM_FREQ_PF, target_freq, (dc / 1000));
|
|
|
|
if (compute_high) {
|
|
if (!compute_low
|
|
&& (on <= UINT16_MAX)
|
|
&& (off <= UINT16_MAX)) {
|
|
hc_params.on = on;
|
|
hc_params.off = off;
|
|
hc_params.div = 0;
|
|
lc_params.div = UINT8_MAX;
|
|
|
|
goto done;
|
|
}
|
|
|
|
hc_params.div = xec_select_best_div_on_off(
|
|
target_freq, dc,
|
|
max_freq_high_on_div,
|
|
&hc_params.on,
|
|
&hc_params.off);
|
|
LOG_DBG("Best div high: %u (on/off: %u/%u)",
|
|
hc_params.div, hc_params.on, hc_params.off);
|
|
} else {
|
|
hc_params.div = UINT8_MAX;
|
|
}
|
|
|
|
if (compute_low) {
|
|
lc_params.div = xec_select_best_div_on_off(
|
|
target_freq, dc,
|
|
max_freq_low_on_div,
|
|
&lc_params.on,
|
|
&lc_params.off);
|
|
LOG_DBG("Best div low: %u (on/off: %u/%u)",
|
|
lc_params.div, lc_params.on, lc_params.off);
|
|
} else {
|
|
lc_params.div = UINT8_MAX;
|
|
}
|
|
done:
|
|
regs->CONFIG &= ~MCHP_PWM_CFG_ENABLE;
|
|
|
|
cfgval = regs->CONFIG;
|
|
|
|
params = xec_compare_params(target_freq, &hc_params, &lc_params);
|
|
if (params == &hc_params) {
|
|
cfgval &= ~MCHP_PWM_CFG_CLK_SEL_100K;
|
|
} else {
|
|
cfgval |= MCHP_PWM_CFG_CLK_SEL_100K;
|
|
}
|
|
|
|
regs->COUNT_ON = params->on;
|
|
regs->COUNT_OFF = params->off;
|
|
cfgval &= ~MCHP_PWM_CFG_CLK_PRE_DIV(0xF);
|
|
cfgval |= MCHP_PWM_CFG_CLK_PRE_DIV(params->div);
|
|
cfgval |= MCHP_PWM_CFG_ENABLE;
|
|
|
|
regs->CONFIG = cfgval;
|
|
}
|
|
|
|
static int pwm_xec_set_cycles(const struct device *dev, uint32_t channel,
|
|
uint32_t period_cycles, uint32_t pulse_cycles,
|
|
pwm_flags_t flags)
|
|
{
|
|
const struct pwm_xec_config * const cfg = dev->config;
|
|
struct pwm_regs * const regs = cfg->regs;
|
|
uint32_t target_freq;
|
|
uint32_t on, off;
|
|
|
|
if (channel > 0) {
|
|
return -EIO;
|
|
}
|
|
|
|
if (flags & PWM_POLARITY_INVERTED) {
|
|
regs->CONFIG |= MCHP_PWM_CFG_ON_POL_LO;
|
|
}
|
|
|
|
on = pulse_cycles;
|
|
off = period_cycles - pulse_cycles;
|
|
|
|
target_freq = xec_compute_frequency(MCHP_PWM_INPUT_FREQ_HI, on, off);
|
|
if (target_freq < XEC_PWM_FREQ_LIMIT) {
|
|
LOG_DBG("Target frequency below limit");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if ((pulse_cycles == 0U) && (period_cycles == 0U)) {
|
|
regs->CONFIG &= ~MCHP_PWM_CFG_ENABLE;
|
|
} else if ((pulse_cycles == 0U) && (period_cycles > 0U)) {
|
|
regs->COUNT_ON = 0;
|
|
regs->COUNT_OFF = 1;
|
|
} else if ((pulse_cycles > 0U) && (period_cycles == 0U)) {
|
|
regs->COUNT_ON = 1;
|
|
regs->COUNT_OFF = 0;
|
|
} else {
|
|
xec_compute_and_set_parameters(dev, target_freq, on, off);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int pwm_xec_get_cycles_per_sec(const struct device *dev,
|
|
uint32_t channel, uint64_t *cycles)
|
|
{
|
|
ARG_UNUSED(dev);
|
|
|
|
if (channel > 0) {
|
|
return -EIO;
|
|
}
|
|
|
|
if (cycles) {
|
|
/* User does not have to know about lowest clock,
|
|
* the driver will select the most relevant one.
|
|
*/
|
|
*cycles = MCHP_PWM_INPUT_FREQ_HI;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef CONFIG_PM_DEVICE
|
|
static int pwm_xec_pm_action(const struct device *dev, enum pm_device_action action)
|
|
{
|
|
const struct pwm_xec_config *const devcfg = dev->config;
|
|
struct pwm_regs * const regs = devcfg->regs;
|
|
struct pwm_xec_data * const data = dev->data;
|
|
int ret = 0;
|
|
|
|
switch (action) {
|
|
case PM_DEVICE_ACTION_RESUME:
|
|
ret = pinctrl_apply_state(devcfg->pcfg, PINCTRL_STATE_DEFAULT);
|
|
if (ret != 0) {
|
|
LOG_ERR("XEC PWM pinctrl setup failed (%d)", ret);
|
|
}
|
|
|
|
/* Turn on PWM only if it is ON before sleep */
|
|
if ((data->config & MCHP_PWM_CFG_ENABLE) == MCHP_PWM_CFG_ENABLE) {
|
|
|
|
regs->CONFIG |= MCHP_PWM_CFG_ENABLE;
|
|
|
|
data->config &= (~MCHP_PWM_CFG_ENABLE);
|
|
}
|
|
break;
|
|
case PM_DEVICE_ACTION_SUSPEND:
|
|
if ((regs->CONFIG & MCHP_PWM_CFG_ENABLE) == MCHP_PWM_CFG_ENABLE) {
|
|
/* Do copy first, then clear mode. */
|
|
data->config = regs->CONFIG;
|
|
|
|
regs->CONFIG &= ~(MCHP_PWM_CFG_ENABLE);
|
|
}
|
|
|
|
ret = pinctrl_apply_state(devcfg->pcfg, PINCTRL_STATE_SLEEP);
|
|
/* pinctrl-1 does not exist. */
|
|
if (ret == -ENOENT) {
|
|
ret = 0;
|
|
}
|
|
break;
|
|
default:
|
|
ret = -ENOTSUP;
|
|
}
|
|
return ret;
|
|
}
|
|
#endif /* CONFIG_PM_DEVICE */
|
|
|
|
static const struct pwm_driver_api pwm_xec_driver_api = {
|
|
.set_cycles = pwm_xec_set_cycles,
|
|
.get_cycles_per_sec = pwm_xec_get_cycles_per_sec,
|
|
};
|
|
|
|
static int pwm_xec_init(const struct device *dev)
|
|
{
|
|
const struct pwm_xec_config * const cfg = dev->config;
|
|
int ret = pinctrl_apply_state(cfg->pcfg, PINCTRL_STATE_DEFAULT);
|
|
|
|
if (ret != 0) {
|
|
LOG_ERR("XEC PWM pinctrl init failed (%d)", ret);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#define XEC_PWM_CONFIG(inst) \
|
|
static struct pwm_xec_config pwm_xec_config_##inst = { \
|
|
.regs = (struct pwm_regs * const)DT_INST_REG_ADDR(inst), \
|
|
.pcr_idx = (uint8_t)DT_INST_PROP_BY_IDX(inst, pcrs, 0), \
|
|
.pcr_pos = (uint8_t)DT_INST_PROP_BY_IDX(inst, pcrs, 1), \
|
|
.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(inst), \
|
|
};
|
|
|
|
#define XEC_PWM_DEVICE_INIT(index) \
|
|
\
|
|
static struct pwm_xec_data pwm_xec_data_##index; \
|
|
\
|
|
PINCTRL_DT_INST_DEFINE(index); \
|
|
\
|
|
XEC_PWM_CONFIG(index); \
|
|
\
|
|
PM_DEVICE_DT_INST_DEFINE(index, pwm_xec_pm_action); \
|
|
\
|
|
DEVICE_DT_INST_DEFINE(index, &pwm_xec_init, \
|
|
PM_DEVICE_DT_INST_GET(index), \
|
|
&pwm_xec_data_##index, \
|
|
&pwm_xec_config_##index, POST_KERNEL, \
|
|
CONFIG_PWM_INIT_PRIORITY, \
|
|
&pwm_xec_driver_api);
|
|
|
|
DT_INST_FOREACH_STATUS_OKAY(XEC_PWM_DEVICE_INIT)
|