You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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控制器传输完成后触发中断;
  • 给描述符的callbackcallback_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

火山引擎 最新活动