mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
319 lines
7.8 KiB
319 lines
7.8 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
/* |
|
* DesignWare PWM Controller driver |
|
* |
|
* Copyright (C) 2018-2020 Intel Corporation |
|
* |
|
* Author: Felipe Balbi (Intel) |
|
* Author: Jarkko Nikula <[email protected]> |
|
* Author: Raymond Tan <[email protected]> |
|
* |
|
* Limitations: |
|
* - The hardware cannot generate a 0 % or 100 % duty cycle. Both high and low |
|
* periods are one or more input clock periods long. |
|
*/ |
|
|
|
#include <linux/bitops.h> |
|
#include <linux/export.h> |
|
#include <linux/kernel.h> |
|
#include <linux/module.h> |
|
#include <linux/pci.h> |
|
#include <linux/pm_runtime.h> |
|
#include <linux/pwm.h> |
|
|
|
#define DWC_TIM_LD_CNT(n) ((n) * 0x14) |
|
#define DWC_TIM_LD_CNT2(n) (((n) * 4) + 0xb0) |
|
#define DWC_TIM_CUR_VAL(n) (((n) * 0x14) + 0x04) |
|
#define DWC_TIM_CTRL(n) (((n) * 0x14) + 0x08) |
|
#define DWC_TIM_EOI(n) (((n) * 0x14) + 0x0c) |
|
#define DWC_TIM_INT_STS(n) (((n) * 0x14) + 0x10) |
|
|
|
#define DWC_TIMERS_INT_STS 0xa0 |
|
#define DWC_TIMERS_EOI 0xa4 |
|
#define DWC_TIMERS_RAW_INT_STS 0xa8 |
|
#define DWC_TIMERS_COMP_VERSION 0xac |
|
|
|
#define DWC_TIMERS_TOTAL 8 |
|
#define DWC_CLK_PERIOD_NS 10 |
|
|
|
/* Timer Control Register */ |
|
#define DWC_TIM_CTRL_EN BIT(0) |
|
#define DWC_TIM_CTRL_MODE BIT(1) |
|
#define DWC_TIM_CTRL_MODE_FREE (0 << 1) |
|
#define DWC_TIM_CTRL_MODE_USER (1 << 1) |
|
#define DWC_TIM_CTRL_INT_MASK BIT(2) |
|
#define DWC_TIM_CTRL_PWM BIT(3) |
|
|
|
struct dwc_pwm_ctx { |
|
u32 cnt; |
|
u32 cnt2; |
|
u32 ctrl; |
|
}; |
|
|
|
struct dwc_pwm { |
|
struct pwm_chip chip; |
|
void __iomem *base; |
|
struct dwc_pwm_ctx ctx[DWC_TIMERS_TOTAL]; |
|
}; |
|
#define to_dwc_pwm(p) (container_of((p), struct dwc_pwm, chip)) |
|
|
|
static inline u32 dwc_pwm_readl(struct dwc_pwm *dwc, u32 offset) |
|
{ |
|
return readl(dwc->base + offset); |
|
} |
|
|
|
static inline void dwc_pwm_writel(struct dwc_pwm *dwc, u32 value, u32 offset) |
|
{ |
|
writel(value, dwc->base + offset); |
|
} |
|
|
|
static void __dwc_pwm_set_enable(struct dwc_pwm *dwc, int pwm, int enabled) |
|
{ |
|
u32 reg; |
|
|
|
reg = dwc_pwm_readl(dwc, DWC_TIM_CTRL(pwm)); |
|
|
|
if (enabled) |
|
reg |= DWC_TIM_CTRL_EN; |
|
else |
|
reg &= ~DWC_TIM_CTRL_EN; |
|
|
|
dwc_pwm_writel(dwc, reg, DWC_TIM_CTRL(pwm)); |
|
} |
|
|
|
static int __dwc_pwm_configure_timer(struct dwc_pwm *dwc, |
|
struct pwm_device *pwm, |
|
const struct pwm_state *state) |
|
{ |
|
u64 tmp; |
|
u32 ctrl; |
|
u32 high; |
|
u32 low; |
|
|
|
/* |
|
* Calculate width of low and high period in terms of input clock |
|
* periods and check are the result within HW limits between 1 and |
|
* 2^32 periods. |
|
*/ |
|
tmp = DIV_ROUND_CLOSEST_ULL(state->duty_cycle, DWC_CLK_PERIOD_NS); |
|
if (tmp < 1 || tmp > (1ULL << 32)) |
|
return -ERANGE; |
|
low = tmp - 1; |
|
|
|
tmp = DIV_ROUND_CLOSEST_ULL(state->period - state->duty_cycle, |
|
DWC_CLK_PERIOD_NS); |
|
if (tmp < 1 || tmp > (1ULL << 32)) |
|
return -ERANGE; |
|
high = tmp - 1; |
|
|
|
/* |
|
* Specification says timer usage flow is to disable timer, then |
|
* program it followed by enable. It also says Load Count is loaded |
|
* into timer after it is enabled - either after a disable or |
|
* a reset. Based on measurements it happens also without disable |
|
* whenever Load Count is updated. But follow the specification. |
|
*/ |
|
__dwc_pwm_set_enable(dwc, pwm->hwpwm, false); |
|
|
|
/* |
|
* Write Load Count and Load Count 2 registers. Former defines the |
|
* width of low period and latter the width of high period in terms |
|
* multiple of input clock periods: |
|
* Width = ((Count + 1) * input clock period). |
|
*/ |
|
dwc_pwm_writel(dwc, low, DWC_TIM_LD_CNT(pwm->hwpwm)); |
|
dwc_pwm_writel(dwc, high, DWC_TIM_LD_CNT2(pwm->hwpwm)); |
|
|
|
/* |
|
* Set user-defined mode, timer reloads from Load Count registers |
|
* when it counts down to 0. |
|
* Set PWM mode, it makes output to toggle and width of low and high |
|
* periods are set by Load Count registers. |
|
*/ |
|
ctrl = DWC_TIM_CTRL_MODE_USER | DWC_TIM_CTRL_PWM; |
|
dwc_pwm_writel(dwc, ctrl, DWC_TIM_CTRL(pwm->hwpwm)); |
|
|
|
/* |
|
* Enable timer. Output starts from low period. |
|
*/ |
|
__dwc_pwm_set_enable(dwc, pwm->hwpwm, state->enabled); |
|
|
|
return 0; |
|
} |
|
|
|
static int dwc_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm, |
|
const struct pwm_state *state) |
|
{ |
|
struct dwc_pwm *dwc = to_dwc_pwm(chip); |
|
|
|
if (state->polarity != PWM_POLARITY_INVERSED) |
|
return -EINVAL; |
|
|
|
if (state->enabled) { |
|
if (!pwm->state.enabled) |
|
pm_runtime_get_sync(chip->dev); |
|
return __dwc_pwm_configure_timer(dwc, pwm, state); |
|
} else { |
|
if (pwm->state.enabled) { |
|
__dwc_pwm_set_enable(dwc, pwm->hwpwm, false); |
|
pm_runtime_put_sync(chip->dev); |
|
} |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static void dwc_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm, |
|
struct pwm_state *state) |
|
{ |
|
struct dwc_pwm *dwc = to_dwc_pwm(chip); |
|
u64 duty, period; |
|
|
|
pm_runtime_get_sync(chip->dev); |
|
|
|
state->enabled = !!(dwc_pwm_readl(dwc, |
|
DWC_TIM_CTRL(pwm->hwpwm)) & DWC_TIM_CTRL_EN); |
|
|
|
duty = dwc_pwm_readl(dwc, DWC_TIM_LD_CNT(pwm->hwpwm)); |
|
duty += 1; |
|
duty *= DWC_CLK_PERIOD_NS; |
|
state->duty_cycle = duty; |
|
|
|
period = dwc_pwm_readl(dwc, DWC_TIM_LD_CNT2(pwm->hwpwm)); |
|
period += 1; |
|
period *= DWC_CLK_PERIOD_NS; |
|
period += duty; |
|
state->period = period; |
|
|
|
state->polarity = PWM_POLARITY_INVERSED; |
|
|
|
pm_runtime_put_sync(chip->dev); |
|
} |
|
|
|
static const struct pwm_ops dwc_pwm_ops = { |
|
.apply = dwc_pwm_apply, |
|
.get_state = dwc_pwm_get_state, |
|
.owner = THIS_MODULE, |
|
}; |
|
|
|
static int dwc_pwm_probe(struct pci_dev *pci, const struct pci_device_id *id) |
|
{ |
|
struct device *dev = &pci->dev; |
|
struct dwc_pwm *dwc; |
|
int ret; |
|
|
|
dwc = devm_kzalloc(&pci->dev, sizeof(*dwc), GFP_KERNEL); |
|
if (!dwc) |
|
return -ENOMEM; |
|
|
|
ret = pcim_enable_device(pci); |
|
if (ret) { |
|
dev_err(&pci->dev, |
|
"Failed to enable device (%pe)\n", ERR_PTR(ret)); |
|
return ret; |
|
} |
|
|
|
pci_set_master(pci); |
|
|
|
ret = pcim_iomap_regions(pci, BIT(0), pci_name(pci)); |
|
if (ret) { |
|
dev_err(&pci->dev, |
|
"Failed to iomap PCI BAR (%pe)\n", ERR_PTR(ret)); |
|
return ret; |
|
} |
|
|
|
dwc->base = pcim_iomap_table(pci)[0]; |
|
if (!dwc->base) { |
|
dev_err(&pci->dev, "Base address missing\n"); |
|
return -ENOMEM; |
|
} |
|
|
|
pci_set_drvdata(pci, dwc); |
|
|
|
dwc->chip.dev = dev; |
|
dwc->chip.ops = &dwc_pwm_ops; |
|
dwc->chip.npwm = DWC_TIMERS_TOTAL; |
|
dwc->chip.base = -1; |
|
|
|
ret = pwmchip_add(&dwc->chip); |
|
if (ret) |
|
return ret; |
|
|
|
pm_runtime_put(dev); |
|
pm_runtime_allow(dev); |
|
|
|
return 0; |
|
} |
|
|
|
static void dwc_pwm_remove(struct pci_dev *pci) |
|
{ |
|
struct dwc_pwm *dwc = pci_get_drvdata(pci); |
|
|
|
pm_runtime_forbid(&pci->dev); |
|
pm_runtime_get_noresume(&pci->dev); |
|
|
|
pwmchip_remove(&dwc->chip); |
|
} |
|
|
|
#ifdef CONFIG_PM_SLEEP |
|
static int dwc_pwm_suspend(struct device *dev) |
|
{ |
|
struct pci_dev *pdev = container_of(dev, struct pci_dev, dev); |
|
struct dwc_pwm *dwc = pci_get_drvdata(pdev); |
|
int i; |
|
|
|
for (i = 0; i < DWC_TIMERS_TOTAL; i++) { |
|
if (dwc->chip.pwms[i].state.enabled) { |
|
dev_err(dev, "PWM %u in use by consumer (%s)\n", |
|
i, dwc->chip.pwms[i].label); |
|
return -EBUSY; |
|
} |
|
dwc->ctx[i].cnt = dwc_pwm_readl(dwc, DWC_TIM_LD_CNT(i)); |
|
dwc->ctx[i].cnt2 = dwc_pwm_readl(dwc, DWC_TIM_LD_CNT2(i)); |
|
dwc->ctx[i].ctrl = dwc_pwm_readl(dwc, DWC_TIM_CTRL(i)); |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static int dwc_pwm_resume(struct device *dev) |
|
{ |
|
struct pci_dev *pdev = container_of(dev, struct pci_dev, dev); |
|
struct dwc_pwm *dwc = pci_get_drvdata(pdev); |
|
int i; |
|
|
|
for (i = 0; i < DWC_TIMERS_TOTAL; i++) { |
|
dwc_pwm_writel(dwc, dwc->ctx[i].cnt, DWC_TIM_LD_CNT(i)); |
|
dwc_pwm_writel(dwc, dwc->ctx[i].cnt2, DWC_TIM_LD_CNT2(i)); |
|
dwc_pwm_writel(dwc, dwc->ctx[i].ctrl, DWC_TIM_CTRL(i)); |
|
} |
|
|
|
return 0; |
|
} |
|
#endif |
|
|
|
static SIMPLE_DEV_PM_OPS(dwc_pwm_pm_ops, dwc_pwm_suspend, dwc_pwm_resume); |
|
|
|
static const struct pci_device_id dwc_pwm_id_table[] = { |
|
{ PCI_VDEVICE(INTEL, 0x4bb7) }, /* Elkhart Lake */ |
|
{ } /* Terminating Entry */ |
|
}; |
|
MODULE_DEVICE_TABLE(pci, dwc_pwm_id_table); |
|
|
|
static struct pci_driver dwc_pwm_driver = { |
|
.name = "pwm-dwc", |
|
.probe = dwc_pwm_probe, |
|
.remove = dwc_pwm_remove, |
|
.id_table = dwc_pwm_id_table, |
|
.driver = { |
|
.pm = &dwc_pwm_pm_ops, |
|
}, |
|
}; |
|
|
|
module_pci_driver(dwc_pwm_driver); |
|
|
|
MODULE_AUTHOR("Felipe Balbi (Intel)"); |
|
MODULE_AUTHOR("Jarkko Nikula <[email protected]>"); |
|
MODULE_AUTHOR("Raymond Tan <[email protected]>"); |
|
MODULE_DESCRIPTION("DesignWare PWM Controller"); |
|
MODULE_LICENSE("GPL");
|
|
|