Linux内核模块配置DMA:内存到PWM FIFO带中断回调的实现步骤咨询
刚好在项目里做过类似的内存到PWM FIFO的DMA带中断回调实现,结合Linux内核DMA Engine API给你梳理下完整的实操步骤,都是经过验证的流程:
核心实现步骤
下面的步骤完全针对你的场景(内核空间、MEM→PWM FIFO、DMA完成中断回调):
1. 申请并配置DMA通道
首先要拿到和你的PWM控制器绑定的DMA通道,并且配置slave传输参数:
- 用
dma_cap_mask_t定义需要的DMA能力(这里需要DMA_SLAVE,因为是内存到外设的固定地址传输); - 通过
dma_request_channel()请求通道,需要自定义一个过滤器函数来匹配PWM控制器对应的DMA请求线(过滤器函数可以从设备树里的dma-names或者平台数据里获取匹配规则); - 填充
struct dma_slave_config,关键参数包括:direction = DMA_MEM_TO_DEV(内存到设备);dst_addr:PWM FIFO的物理地址(从设备寄存器手册或设备树里获取);dst_addr_width:PWM FIFO的数据位宽(比如32位就是DMA_SLAVE_BUSWIDTH_4_BYTES);- 如果PWM控制器需要特定的突发传输长度,设置
dst_maxburst。
2. 分配DMA安全内存
因为DMA直接访问物理内存,必须使用内核提供的DMA连贯内存:
- 用
dma_alloc_coherent()分配,它会返回虚拟地址(供内核读写)和DMA地址(供硬件访问); - 不要用普通的
kmalloc(),否则会有缓存一致性问题,导致DMA读到旧数据。
3. 准备DMA传输描述符并设置中断回调
这一步是实现中断触发的关键:
- 用
dmaengine_prep_slave_single()(连续内存块)或dmaengine_prep_slave_sg()(分散/聚集列表)创建传输描述符; - 必须在flags里加上
DMA_PREP_INTERRUPT,告诉DMA控制器传输完成后触发中断; - 给描述符的
callback和callback_param赋值:callback就是你要在DMA完成后执行的函数,callback_param是传递给回调的私有数据(比如你的设备私有结构体)。
4. 提交并启动DMA事务
- 用
dmaengine_submit()把描述符提交到DMA通道的队列; - 用
dma_async_issue_pending()启动队列里的传输,DMA硬件开始工作。
5. 回调函数内的后续处理
回调函数运行在中断上下文,所以要注意:
- 不能调用会睡眠的函数(比如
kmalloc(GFP_KERNEL)、mutex_lock()),必须用原子操作或自旋锁; - 在回调里可以加载下一个数据向量,重新创建DMA描述符、设置回调、提交并启动,实现连续传输;
- 如果需要通知用户空间,可以用
wake_up_interruptible()唤醒等待队列。
关键代码示例
私有数据结构体
#define NUM_BLOCKS 4 // 假设我们有4个循环的PWM波形块 #define PWM_FIFO_PHYS_ADDR 0x12345678 // 替换成你的PWM FIFO物理地址 struct pwm_dma_priv { struct dma_chan *dma_chan; struct device *dev; dma_addr_t dma_buf; // DMA硬件可见的地址 void *virt_buf; // 内核可见的虚拟地址 size_t buf_len; // 每个波形块的长度 int current_block; // 当前传输的块索引 spinlock_t lock; // 保护状态变量的自旋锁 };
DMA完成回调函数
static void pwm_dma_complete_cb(void *param) { struct pwm_dma_priv *priv = param; unsigned long flags; // 中断上下文必须用自旋锁保护临界区 spin_lock_irqsave(&priv->lock, flags); // 切换到下一个波形块 priv->current_block = (priv->current_block + 1) % NUM_BLOCKS; dma_addr_t next_src = priv->dma_buf + (priv->current_block * priv->buf_len); // 重新准备DMA传输描述符 struct dma_async_tx_descriptor *tx = dmaengine_prep_slave_single( priv->dma_chan, PWM_FIFO_PHYS_ADDR, next_src, priv->buf_len, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT | DMA_CTRL_ACK ); if (!tx) { dev_err(priv->dev, "Failed to prepare next DMA transfer\n"); spin_unlock_irqrestore(&priv->lock, flags); return; } // 重新绑定回调 tx->callback = pwm_dma_complete_cb; tx->callback_param = priv; // 提交并启动下一次传输 dmaengine_submit(tx); dma_async_issue_pending(priv->dma_chan); spin_unlock_irqrestore(&priv->lock, flags); // 可选:唤醒用户空间等待队列 // wake_up_interruptible(&priv->wait_queue); }
初始化流程
static int pwm_dma_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct pwm_dma_priv *priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; spin_lock_init(&priv->lock); priv->dev = dev; priv->buf_len = 1024; // 每个波形块1KB // 1. 请求DMA通道 dma_cap_mask_t mask; dma_cap_zero(mask); dma_cap_set(DMA_SLAVE, mask); // 这里的pwm_dma_filter是自定义的过滤器函数,匹配PWM对应的DMA通道 priv->dma_chan = dma_request_channel(mask, pwm_dma_filter, dev); if (!priv->dma_chan) { dev_err(dev, "No available DMA channel for PWM\n"); return -ENODEV; } // 2. 配置DMA slave参数 struct dma_slave_config config = {0}; config.direction = DMA_MEM_TO_DEV; config.dst_addr = PWM_FIFO_PHYS_ADDR; config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; config.dst_maxburst = 4; // 每次突发传输4个32位数据,根据PWM控制器调整 int ret = dmaengine_slave_config(priv->dma_chan, &config); if (ret) { dev_err(dev, "DMA slave config failed: %d\n", ret); dma_release_channel(priv->dma_chan); return ret; } // 3. 分配DMA连贯内存 priv->virt_buf = dma_alloc_coherent(dev, priv->buf_len * NUM_BLOCKS, &priv->dma_buf, GFP_KERNEL); if (!priv->virt_buf) { dev_err(dev, "Failed to allocate DMA buffer\n"); dma_release_channel(priv->dma_chan); return -ENOMEM; } // 填充PWM波形数据到内存(比如正弦波、方波等) fill_pwm_waveforms(priv->virt_buf, priv->buf_len * NUM_BLOCKS); // 4. 准备第一个DMA传输 struct dma_async_tx_descriptor *tx = dmaengine_prep_slave_single( priv->dma_chan, PWM_FIFO_PHYS_ADDR, priv->dma_buf, priv->buf_len, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT | DMA_CTRL_ACK ); if (!tx) { dev_err(dev, "Failed to prepare initial DMA transfer\n"); dma_free_coherent(dev, priv->buf_len * NUM_BLOCKS, priv->virt_buf, priv->dma_buf); dma_release_channel(priv->dma_chan); return -ENOMEM; } // 设置回调 tx->callback = pwm_dma_complete_cb; tx->callback_param = priv; // 提交并启动传输 dmaengine_submit(tx); dma_async_issue_pending(priv->dma_chan); // 保存私有数据到设备 dev_set_drvdata(dev, priv); // 别忘了配置PWM控制器:开启FIFO DMA请求,设置FIFO阈值等 pwm_enable_dma_request(dev); return 0; }
重要注意事项
- PWM侧配置:必须在PWM控制器中开启DMA请求(比如设置寄存器使能FIFO空时触发DMA传输),否则DMA硬件不会收到触发信号;
- 缓存一致性:如果用
dma_map_single()映射普通内存,必须调用dma_sync_single_for_device()确保数据被刷到物理内存; - 中断上下文限制:回调函数里绝对不能做睡眠操作,所有操作必须是原子的;
- 资源清理:模块卸载时要调用
dmaengine_terminate_all()停止DMA传输,然后释放通道和内存; - 错误处理:每一步都要检查返回值,避免资源泄漏。
内容的提问来源于stack exchange,提问作者fdellorso




