在linux驱动里实现gpio产生任意脉冲
1.简介
以前在使用stm32时,通过安富莱的手册了解到过一种任意gpio产生pwm的方法,其原理是通过dma搬运数据到gpio寄存器,从而达到产生任意脉冲。操作逻辑如下:
内存数据 ----> gpio寄存器
| |
定时器 --------> DMA --------> gpio --------> 产生脉冲
触发
定时器:这里作为DMA的触发源存在,其决定了发送脉冲的最大频率
内存数据:设置好的内存数据用于控制gpio是高电平还是低电平,
例如:(1)0,1,0,1交错发送就能得到50%占空比,定时器频率/2,的pwm;
(2)连续发送0,0,0,1就能得到25%占空比,定时器频率/4,的pwm;
DMA:DMA通过控制搬运数据长度,就能实现控制发出脉冲的数量。
gpio:一般的芯片都有专门控制gpio输出的寄存器,该方法的本质是控制输出的寄存器替换,io复用成pwm。
2.环境
由于寄存器和芯片强相关,所以我这里提供的源码基本100%是无法在其他环境下直接用的,这里更多的是提供出一种已实现的思路。
kernel:5.10
mcu:renesas RZ/G2L
我这里是要控制一个rgb灯,其时序要求如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
我要发送24bit数据去控制该灯颜色
我这里由于实际硬件的一些bug,实际运行方式和简介里介绍的流程有区别,但利用dma的思路是类似的,修改后就失去了控制任意gpio的特点,但更方便控制占比。以下是我代码里的操作逻辑:
内存数据 ----> 定时器比较寄存器
| |
DMA <-------- 定时器 --------> gpio --------> 产生脉冲
触发 pwm
定时器:这里作为DMA的触发源存在,同时也控制着gpio发出pwm
定时器比较寄存器:该寄存器用于控制pwm的占空比,为0时pwm为低电平,为最大值时pwm为高电平。
当定时器运行完一次计数时,触发溢出中断,该中断触发dma搬运数据,dma修改了占空比。我这里的0,1是不同占空比,所以修改不同占空比就直接设置好了高低电平(理论上讲高低电平识别0,1的话,改成0%占空比或100%占空比就可以了)。如果要停止传输,只需要在dma传输完成时停止pwm输出就可以了。
这种操作方法本质上与简介中的方法是一样的,很容易切换回简介中的方法。
3.源码
由于写的时候只考虑在这个工程里用一下,所以代码也没做什么优化,能跑就行
pwm-dma.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <linux/string.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/of_device.h>
#include <linux/pwm.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h>
#include <linux/cdev.h>
/*GPIO*/
#define GPIO_BASE (0X11030000)
/*p18*/
#define P18 (GPIO_BASE + 0X22)
#define PM18 (GPIO_BASE + 0X144)
#define PMC18 (GPIO_BASE + 0X222)
#define GTCCRA (0x1004804C)
#define LEDRGB_CNT 1
#define LEDRGB_NAME "led_rgb"
typedef struct
{
void __iomem *p;
void __iomem *pm;
void __iomem *pmc;
} gpio_iomem_t;
static gpio_iomem_t p18_iomem;
#define DMA_TRANS_LEN 400
//static char dma_data[DMA_TRANS_LEN] = {0};
struct pwm_dma_data {
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
struct pwm_device *pwm;
struct device *dev;
struct dma_chan *tx_ch;
uint32_t *buf1;
uint32_t *buf2;
struct scatterlist sg[2];
struct sg_table tx;
};
int dev_major = 0;
int dev_minor = 0;
static void gpio_config(struct platform_device *pdev)
{
int err;
int PWN1_EN = 265 ; //P18_1
err = gpio_request(PWN1_EN, "SPK_SHUTDOWN");
if (err < 0) {
dev_info(&pdev->dev, "%s: gpio_request(%d) for SPK_SHUTDOWN failed\n",
__FUNCTION__, PWN1_EN);
return -1;
}
err = gpio_direction_output(PWN1_EN, 0);
if (err < 0) {
dev_info(&pdev->dev, "%s: gpio_direction_output(%d) for SPK_SHUTDOWN failed\n",
__FUNCTION__, PWN1_EN);
gpio_free(PWN1_EN);
return -1;
}
}
static void jhd_pwm_config(struct platform_device *pdev, struct pwm_dma_data *pd)
{
struct pwm_device *pwm;
struct pwm_state state;
int ret;
pwm = devm_pwm_get(&pdev->dev, NULL);
if (IS_ERR(pwm) && PTR_ERR(pwm) != -EPROBE_DEFER) {
dev_err(&pdev->dev, "unable to request PWM, trying legacy API\n");
// pwm = pwm_request(data->pwm_id, "pwm-backlight");
}
if (IS_ERR(pwm)) {
ret = PTR_ERR(pwm);
if (ret != -EPROBE_DEFER)
dev_err(&pdev->dev, "unable to request PWM\n");
goto err_pwm;
}
pd->pwm = pwm;
/* Sync up PWM state. */
pwm_init_state(pd->pwm, &state);
state.duty_cycle = 0;
printk("period:%lld, duty_cycle:%lld, enabled:%d\n", state.period, state.duty_cycle, state.enabled);
state.enabled = true;
// state.enabled = true;
ret = pwm_apply_state(pd->pwm, &state);
if (ret) {
dev_err(&pdev->dev, "failed to apply initial PWM state: %d\n", ret);
goto err_pwm;
}
// void __iomem *gpt_int;
// gpt_int = ioremap(0x10048038, 4);
// u32 reg32 = 1;
// reg32 = readb(gpt_int);
// printk("gpt_int=%08x\n", reg32);
// reg32 = 0x41;
// writeb(reg32, gpt_int);
// reg32 = 0;
// reg32 = readb(gpt_int);
// printk("gpt_int=%08x\n", reg32);
return;
err_pwm:
printk("err pwm\n");
return;
}
static int dma_transfer(struct pwm_dma_data *pd, struct sg_table *tx);
static void set_rgb(uint32_t* buff, uint8_t r, uint8_t g, uint8_t b)
{
uint8_t mask = 1;
uint8_t i;
for(i = 0; i < 8; i++ ) {
buff[i] = ((mask << (7 - i)) & g) ? 60 : 30;
buff[i+8] = ((mask << (7 - i)) & r) ? 60 : 30;
buff[i+16] = ((mask << (7 - i)) & b) ? 60 : 30;
}
}
static void dma_complete(void *arg)
{
// static uint64_t i = 0;
// if(i % 2000 == 0) {
// printk("dma_complete 500\n");
// }
struct pwm_dma_data *pd = (struct pwm_dma_data*)arg;
// dma_transfer(pd, &(pd->tx));
// printk("dma_complete\n");
dma_unmap_sg(pd->dev, pd->sg, 2, DMA_MEM_TO_DEV);
pwm_disable(pd->pwm);
kfree(pd->buf1);
kfree(pd->buf2);
}
static int dma_transfer(struct pwm_dma_data *pd, struct sg_table *tx)
{
int ret;
struct dma_async_tx_descriptor *desc = NULL;
desc = dmaengine_prep_slave_sg(pd->tx_ch, tx->sgl,
tx->nents, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!desc) {
ret = -EAGAIN;
goto dma_transfer_err;
}
desc->callback = dma_complete;
desc->callback_param = pd;
// printk("dma_config 5\n");
ret = dma_submit_error(dmaengine_submit(desc));
if (ret) {
dmaengine_terminate_sync(pd->tx_ch);
goto dma_transfer_err;
}
dma_async_issue_pending(pd->tx_ch);
return 0;
dma_transfer_err:
return ret;
}
static int led_rgb_open(struct inode *inode, struct file *filp)
{
struct pwm_dma_data *pd = container_of(inode->i_cdev, struct pwm_dma_data, cdev);
filp->private_data = pd;
return 0;
}
static ssize_t led_rgb_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
struct pwm_dma_data *pd = filp->private_data;
int ret;
uint8_t databuf[3];
ret = copy_from_user(databuf, buf, cnt);
if(ret < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
printk("r:%d, g:%d, b:%d\n", databuf[0], databuf[1], databuf[2]);
pd->buf1 = kzalloc(DMA_TRANS_LEN, GFP_DMA);
pd->buf2 = kzalloc(DMA_TRANS_LEN, GFP_DMA);
set_rgb(pd->buf1, databuf[0], databuf[1], databuf[2]);
set_rgb(pd->buf2, databuf[0], databuf[1], databuf[2]);
// set_rgb(pd->buf1, 255, 0, 0);
// set_rgb(pd->buf2, 255, 0, 0);
int i = 0;
for(i = 24; i < DMA_TRANS_LEN/4; i=i+1) {
pd->buf1[i] = 0;
pd->buf2[i] = 0;
}
sg_init_table(pd->sg, 2);
sg_set_buf(&(pd->sg[0]), pd->buf1, DMA_TRANS_LEN);
sg_set_buf(&(pd->sg[1]), pd->buf2, DMA_TRANS_LEN);
pd->tx.sgl = pd->sg;
pd->tx.nents = 2;
ret = dma_map_sg(pd->dev, pd->sg, 2, DMA_MEM_TO_DEV);
if (ret < 0) {
dev_err(pd->dev, "dma_map_sg failed\n");
ret = -ENODEV;
return ret;
}
dma_transfer(pd, &(pd->tx));
struct pwm_state state;
pwm_init_state(pd->pwm, &state);
state.duty_cycle = 0;
state.enabled = true;
ret = pwm_apply_state(pd->pwm, &state);
if (ret) {
dev_err(pd->dev, "failed to apply initial PWM state: %d\n", ret);
}
return 0;
}
static struct file_operations led_rgb_fops = {
.owner = THIS_MODULE,
.open = led_rgb_open,
.write = led_rgb_write,
};
static int dma_config(struct platform_device *pdev, struct pwm_dma_data *pd)
{
int ret;
struct dma_slave_config config;
pd->tx_ch = dma_request_slave_channel(&pdev->dev, "tx");
if (!pd->tx_ch) {
dev_info(&pdev->dev, "tx dma alloc failed\n");
return -ENODEV;
}
printk("dma_config 1\n");
// pd->tx_buf = dma_alloc_coherent(&pdev->dev, DMA_TRANS_LEN,
// &pd->tx_dma_buf,
// GFP_KERNEL);
// if (!pd->tx_buf) {
// ret = -ENOMEM;
// goto dma_config_err;
// }
pd->buf1 = kzalloc(DMA_TRANS_LEN, GFP_DMA);
pd->buf2 = kzalloc(DMA_TRANS_LEN, GFP_DMA);
// u8 reg8_1, reg8_0;
int i = 0;
// reg8_1 = readb(p18_iomem.p);
// reg8_1 |= (0x01);
// reg8_0 = reg8_1 & (~(0x01));
// for(i = 0; i < 24; i=i+1) {
// pd->buf1[i] = 60;
// pd->buf2[i] = 60;
// // pd->buf2[i+1] = 30;
// // pd->buf2[i+1] = reg8_0;
// }
set_rgb(pd->buf1, 0x00, 0xff, 0x00);
set_rgb(pd->buf2, 0x00, 0xff, 0x00);
for(i = 24; i < DMA_TRANS_LEN/4; i=i+1) {
pd->buf1[i] = 0;
// pd->buf2[i] = 0;
// pd->buf2[i+1] = 30;
// pd->buf2[i+1] = reg8_0;
}
sg_init_table(pd->sg, 2);
sg_set_buf(&(pd->sg[0]), pd->buf1, DMA_TRANS_LEN);
sg_set_buf(&(pd->sg[1]), pd->buf2, DMA_TRANS_LEN);
pd->tx.sgl = pd->sg;
pd->tx.nents = 2;
ret = dma_map_sg(&pdev->dev, pd->sg, 2, DMA_MEM_TO_DEV);
if (ret < 0) {
dev_err(&pdev->dev, "dma_map_sg failed\n");
ret = -ENODEV;
goto dma_config_err;
}
printk("dma_config 2\n");
memset(&config, 0, sizeof(config));
config.direction = DMA_MEM_TO_DEV;
config.dst_addr = GTCCRA;
config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
printk("dma_config 3\n");
ret = dmaengine_slave_config(pd->tx_ch, &config);
if (ret < 0) {
dev_err(&pdev->dev, "rx dma channel config failed\n");
ret = -ENODEV;
goto dma_config_err;
}
printk("dma_config 4\n");
dma_transfer(pd, &(pd->tx));
return 0;
dma_config_err:
return ret;
}
static int pwm_dma_probe(struct platform_device *pdev)
{
printk("pwm_dma_probe\n");
struct pwm_dma_data *pd;
int ret;
pd = devm_kzalloc(&pdev->dev, sizeof(struct pwm_dma_data), GFP_KERNEL);
if (!pd) {
ret = -ENOMEM;
goto err_alloc;
}
pd->dev = &(pdev->dev);
gpio_config(pdev);
dma_config(pdev, pd);
jhd_pwm_config(pdev, pd);
// dma_transfer();
if(dev_major) {
dev_minor++;
pd->devid = MKDEV(dev_major, dev_minor);
register_chrdev_region(pd->devid, LEDRGB_CNT, LEDRGB_NAME);
} else {
alloc_chrdev_region(&(pd->devid), 0, LEDRGB_CNT, LEDRGB_NAME);
dev_major = MAJOR(pd->devid);
dev_minor = MINOR(pd->devid);
}
cdev_init(&(pd->cdev), &led_rgb_fops);
cdev_add(&(pd->cdev), pd->devid, LEDRGB_CNT);
pd->class = class_create(THIS_MODULE, LEDRGB_NAME);
if (IS_ERR(pd->class)) {
return PTR_ERR(pd->class);
}
pd->device = device_create(pd->class, NULL, pd->devid, NULL, LEDRGB_NAME);
if (IS_ERR(pd->device)) {
return PTR_ERR(pd->device);
}
platform_set_drvdata(pdev, pd);
printk("pwm_dma_probe end\n");
return 0;
err_alloc:
dev_err(&pdev->dev, "error\n");
return ret;
}
static int pwm_dma_remove(struct platform_device *pdev)
{
printk("pwm_dma_remove\n");
return 0;
}
static const struct of_device_id pwm_dma_of_match[] = {
{
.compatible = "pwm_dma",
},
{/* Sentinel */},
};
MODULE_DEVICE_TABLE(of, pwm_dma_of_match);
static struct platform_driver pwm_dma_driver = {
.driver = {
.name = "pwm_dma",
.of_match_table = pwm_dma_of_match,
},
.probe = pwm_dma_probe,
.remove = pwm_dma_remove,
};
static int __init pwm_dma_driver_init(void)
{
printk("pwm_dma_driver_init\n");
return platform_driver_register(&pwm_dma_driver);
}
static void __exit pwm_dma_driver_exit(void)
{
printk("pwm_dma_driver_exit\n");
platform_driver_unregister(&pwm_dma_driver);
}
late_initcall(pwm_dma_driver_init);
module_exit(pwm_dma_driver_exit);
MODULE_DESCRIPTION("pwm_dma Driver");
MODULE_LICENSE("GPL v2");
设备树
rgb_led:rgb_led{
compatible = "pwm_dma";
status = "okay";
pwms = <&gpt0 0 12500>;
dmas = <&dmac 0x14d7>;
dma-names = "tx";
};
gpt0_pins: gpt0 {
pinmux = <RZG2L_PORT_PINMUX(18, 0, 2)>; /* Channel A */
};
4.解析
(如果感觉我解析不清晰的话,可以把代码扔给chatgpt看看)
从probe看起,总共有5块内容
(1)给 struct pwm_dma_data 变量分配空间,主要用于write操作
(2)gpio_config,设置gpio,我这个rgb灯还等操作下gpio才能供电
(3)dma_config:dma配置
(4)jhd_pwm_config:配置pwm设置
(5)注册驱动
和主题强相关的就只有(3)、(4)
4.1 dma_config
static int dma_config(struct platform_device *pdev, struct pwm_dma_data *pd)
{
int ret;
struct dma_slave_config config;
pd->tx_ch = dma_request_slave_channel(&pdev->dev, "tx");
if (!pd->tx_ch) {
dev_info(&pdev->dev, "tx dma alloc failed\n");
return -ENODEV;
}
pd->buf1 = kzalloc(DMA_TRANS_LEN, GFP_DMA);
pd->buf2 = kzalloc(DMA_TRANS_LEN, GFP_DMA);
int i = 0;
set_rgb(pd->buf1, 0x00, 0xff, 0x00);
set_rgb(pd->buf2, 0x00, 0xff, 0x00);
for(i = 24; i < DMA_TRANS_LEN/4; i=i+1) {
pd->buf1[i] = 0;
}
sg_init_table(pd->sg, 2);
sg_set_buf(&(pd->sg[0]), pd->buf1, DMA_TRANS_LEN);
sg_set_buf(&(pd->sg[1]), pd->buf2, DMA_TRANS_LEN);
pd->tx.sgl = pd->sg;
pd->tx.nents = 2;
ret = dma_map_sg(&pdev->dev, pd->sg, 2, DMA_MEM_TO_DEV);
if (ret < 0) {
dev_err(&pdev->dev, "dma_map_sg failed\n");
ret = -ENODEV;
goto dma_config_err;
}
memset(&config, 0, sizeof(config));
config.direction = DMA_MEM_TO_DEV;
config.dst_addr = GTCCRA;
config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
ret = dmaengine_slave_config(pd->tx_ch, &config);
if (ret < 0) {
dev_err(&pdev->dev, "rx dma channel config failed\n");
ret = -ENODEV;
goto dma_config_err;
}
dma_transfer(pd, &(pd->tx));
return 0;
dma_config_err:
return ret;
}
dma_request_slave_channel: 该函数是dma_request_chan的封装,功能是获取dma通道,这里指定获取名为"tx"的dma通道。在设备树中,我有以下定义
dmas = <&dmac 0x14d7>;
dma-names = "tx";
0x14d7 是通过硬件手册确认的值,这里设置的是GPT0上溢时触发DMA传输
dma_map_sg:做dma地址的映射,按我理解用dma_map_single传这种单段数据会比较好些,但我用的这颗mcu的时候,驱动里没实现dma_map_single,便连续传两次相同数据。这里具体传的数据什么意思就不做解释了,感兴趣的可以看源码,里面的指需要根据寄存器设置。
dmaengine_slave_config:最主要的是里面的config参数。
DMA_MEM_TO_DEV:指内存搬运到寄存器;
GTCCRA:正常来说这里应该是是GPIO控制输出电平的寄存器地址,但我用的时候由于频率要求高(这里gpio翻转速度跟不上,设定频率),便把寄存器换成定时器的比较寄存器,效果就变成了固定周期里控制占空比,专门控制一小段时间内的gpio脉冲,这样时序也能满足我控制的需求;
DMA_SLAVE_BUSWIDTH_4_BYTES:是传输数据位宽,我这个寄存器是32bit所以对应4 bytes。
dma_transfer:是我定义的dma启动函数,我这里被要求开机是要控制灯量,所以要设置dma启动
4.2 dma_transfer源码
static int dma_transfer(struct pwm_dma_data *pd, struct sg_table *tx)
{
int ret;
struct dma_async_tx_descriptor *desc = NULL;
desc = dmaengine_prep_slave_sg(pd->tx_ch, tx->sgl,
tx->nents, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!desc) {
ret = -EAGAIN;
goto dma_transfer_err;
}
desc->callback = dma_complete;
desc->callback_param = pd;
// printk("dma_config 5\n");
ret = dma_submit_error(dmaengine_submit(desc));
if (ret) {
dmaengine_terminate_sync(pd->tx_ch);
goto dma_transfer_err;
}
dma_async_issue_pending(pd->tx_ch);
return 0;
dma_transfer_err:
return ret;
}
dmaengine_prep_slave_sg:固定流程,获取传输描述符
dma_complete:在传输描述符里设置传输完成回调。
dmaengine_submit:固定流程,提交描述符到DMA engine的等待队列,但不会启动DMA操作。
dma_async_issue_pending:触发dma传输,但在这里由于定时器还没启动,dma缺少触发源,所以目前还没真正开始搬运数据。
4.3 jhd_pwm_config
static void jhd_pwm_config(struct platform_device *pdev, struct pwm_dma_data *pd)
{
struct pwm_device *pwm;
struct pwm_state state;
int ret;
pwm = devm_pwm_get(&pdev->dev, NULL);
if (IS_ERR(pwm) && PTR_ERR(pwm) != -EPROBE_DEFER) {
dev_err(&pdev->dev, "unable to request PWM, trying legacy API\n");
// pwm = pwm_request(data->pwm_id, "pwm-backlight");
}
if (IS_ERR(pwm)) {
ret = PTR_ERR(pwm);
if (ret != -EPROBE_DEFER)
dev_err(&pdev->dev, "unable to request PWM\n");
goto err_pwm;
}
pd->pwm = pwm;
/* Sync up PWM state. */
pwm_init_state(pd->pwm, &state);
state.duty_cycle = 0;
printk("period:%lld, duty_cycle:%lld, enabled:%d\n", state.period, state.duty_cycle, state.enabled);
state.enabled = true;
ret = pwm_apply_state(pd->pwm, &state);
if (ret) {
dev_err(&pdev->dev, "failed to apply initial PWM state: %d\n", ret);
goto err_pwm;
}
return;
err_pwm:
printk("err pwm\n");
return;
}
devm_pwm_get: 从设备树获取pwm数据, 这里实际设置的频率是80khz
state.duty_cycle:是将占空比设置成 0,设置成 0 后就不会有电平输出
state.enabled = true: 将pwm状态设置成启动
pwm_apply_state:应用设置好的pwm状态。到这里pwm启动,dma才开始正常传输数据。
4.4 dma_complete
static void dma_complete(void *arg)
{
struct pwm_dma_data *pd = (struct pwm_dma_data*)arg;
dma_unmap_sg(pd->dev, pd->sg, 2, DMA_MEM_TO_DEV);
pwm_disable(pd->pwm);
kfree(pd->buf1);
kfree(pd->buf2);
}
传输完成后关闭pwm,并释放buf
5.总结
这里提供了个在linux下运用DMA让gpio产生可控脉冲数量的思路。