4. I2S控制器驱动开发(上):设备树绑定、platform driver注册、DMA资源获取与dmaengine API使用

好,我们正式开始写I2S控制器驱动。这一章是基础,也是后面所有调试工作的前提。说白了,就是先把驱动框架搭起来,让内核知道“这里有块I2S硬件”,然后给它配上DMA通道。

我个人习惯,写任何外设驱动,第一步永远是看数据手册和原理图。别急着敲代码,先把硬件管脚、时钟、中断、DMA请求线搞清楚。你想想看,如果连I2S的TX/RX接在哪个DMA控制器上都搞错了,后面调试起来得多痛苦。

4.1 设备树绑定:告诉内核你的硬件长什么样

设备树(Device Tree)是Linux内核用来描述硬件拓扑的数据结构。对于I2S控制器,我们需要在设备树中声明它的寄存器基址、中断号、DMA通道、时钟源等信息。

一个典型的I2S控制器设备树节点长这样:

i2s0: i2s@ff210000 {
    compatible = "vendor,soc-i2s", "simple-i2s";
    reg = <0x0 0xff210000 0x0 0x1000>;
    interrupts = <GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clkc CLK_I2S0>, <&clkc CLK_I2S0_BCLK>;
    clock-names = "i2s", "bclk";
    dmas = <&dma0 2>, <&dma0 3>;
    dma-names = "tx", "rx";
    #sound-dai-cells = <0>;
    status = "disabled";
};

这里有几个关键点:

  • compatible:驱动匹配的关键字符串。我建议把SoC型号和通用名都写上,方便复用。
  • reg:寄存器物理地址和大小。注意64位平台要用两个<0x0 0x...>。
  • dmas / dma-names:DMA通道编号和名字。顺序很重要,tx在前,rx在后。
  • #sound-dai-cells:这个属性告诉ALSA框架,这个DAI(数字音频接口)需要几个参数。一般填0就够了。
我曾经踩过的坑: 设备树中dmas的通道号写错了,导致DMA请求一直挂起。后来用cat /proc/interrupts发现DMA中断根本没触发。排查了半天,最后发现是设备树里把tx和rx的DMA通道号写反了。所以,设备树写完后,一定要用dtc -I dtb -O dts反编译确认一遍。

4.2 platform driver注册:让内核找到你的驱动

设备树写好了,接下来就是写驱动。I2S控制器在Linux内核中通常被抽象为一个platform device。我们需要注册一个platform driver,当设备树节点被解析时,内核会自动调用驱动的probe函数。

驱动注册的核心结构体是platform_driver:

static const struct of_device_id i2s_of_match[] = {
    { .compatible = "vendor,soc-i2s", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, i2s_of_match);

static struct platform_driver i2s_driver = {
    .probe  = i2s_probe,
    .remove = i2s_remove,
    .driver = {
        .name = "soc-i2s",
        .of_match_table = i2s_of_match,
        .pm = &i2s_pm_ops,
    },
};

module_platform_driver(i2s_driver);

这里要注意:

  • of_match_table必须与设备树中的compatible字符串一致。
  • module_platform_driver是一个宏,它帮我们完成了module_init和module_exit的注册。
  • 电源管理回调pm建议加上,哪怕只是空实现。我在项目中遇到过,不加pm_ops,系统休眠唤醒后I2S控制器状态就乱了。

4.3 DMA资源获取:从设备树到dmaengine

probe函数里,最重要的事情之一就是获取DMA资源。我们使用dmaengine API来请求DMA通道。dmaengine是Linux内核中通用的DMA控制器驱动框架,它屏蔽了不同DMA控制器的硬件差异。

获取DMA通道的典型代码:

struct i2s_dev *i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL);
if (!i2s)
    return -ENOMEM;

i2s->tx_dma_chan = dma_request_slave_channel(&pdev->dev, "tx");
if (!i2s->tx_dma_chan) {
    dev_err(&pdev->dev, "Failed to request TX DMA channel\n");
    return -ENODEV;
}

i2s->rx_dma_chan = dma_request_slave_channel(&pdev->dev, "rx");
if (!i2s->rx_dma_chan) {
    dev_err(&pdev->dev, "Failed to request RX DMA channel\n");
    dma_release_channel(i2s->tx_dma_chan);
    return -ENODEV;
}

dma_request_slave_channel函数会根据设备树中的dmas和dma-names属性,自动找到对应的DMA控制器并返回一个通道句柄。第二个参数就是dma-names中定义的名字。

小技巧: 如果DMA通道请求失败,先检查设备树中dmas的<phandle>是否正确。可以用ls -l /sys/class/dma/查看系统中有哪些DMA控制器,确认phandle指向的DMA控制器确实存在。

4.4 dmaengine API使用:配置与启动传输

拿到DMA通道后,我们需要配置传输参数。对于I2S音频流,通常是循环传输(cyclic mode),因为音频数据是连续不断的。

配置DMA传输的步骤:

  1. 准备DMA描述符:调用dmaengine_prep_dma_cyclic函数。
  2. 提交描述符:调用dmaengine_submit。
  3. 启动传输:调用dma_async_issue_pending。

代码示例:

struct dma_async_tx_descriptor *tx_desc;

tx_desc = dmaengine_prep_dma_cyclic(
    i2s->tx_dma_chan,
    i2s->dma_addr,          // DMA缓冲区物理地址
    i2s->buffer_size,       // 缓冲区总大小
    i2s->period_size,       // 周期大小(每次中断传输的数据量)
    DMA_MEM_TO_DEV,         // 方向:内存到设备
    DMA_PREP_INTERRUPT);    // 使能传输完成中断

if (!tx_desc) {
    dev_err(dev, "Failed to prepare cyclic DMA\n");
    return -ENOMEM;
}

tx_desc->callback = i2s_dma_callback;  // 设置回调函数
tx_desc->callback_param = i2s;

dmaengine_submit(tx_desc);
dma_async_issue_pending(i2s->tx_dma_chan);

这里有几个参数需要特别留意:

参数 说明 常见错误
buffer_size DMA缓冲区总大小,必须是period_size的整数倍 不是整数倍会导致DMA传输异常
period_size 每次中断传输的数据量,通常对应一个音频帧 过小会导致中断频繁,CPU负载高
DMA_PREP_INTERRUPT 使能传输完成中断 忘记设置会导致回调函数不被调用
核心要点: DMA传输的buffer_size和period_size必须与ALSA音频缓冲区配置保持一致。否则,音频数据会断流或者产生噪音。我建议在probe阶段就把这些参数计算好,存到设备结构体中。

4.5 知识结构图:I2S控制器驱动初始化流程

下面这张图总结了从设备树解析到DMA传输启动的完整流程。你可以把它当作一个检查清单,写代码时对照着看。

I2S控制器驱动初始化流程 设备树节点解析 platform driver匹配 probe函数调用 DMA通道请求 DMA传输配置 启动DMA传输 关键检查点 • compatible字符串是否匹配 • dmas通道号是否正确 • dma-names顺序是否一致 DMA配置参数 • buffer_size = period_size * n • 方向:MEM_TO_DEV / DEV_TO_MEM • 使能DMA_PREP_INTERRUPT

4.6 避坑指南:DMA资源获取的常见问题

写DMA相关代码时,有几个坑我反复踩过,这里列出来,你遇到了可以直接对照排查:

  • DMA通道请求失败:先确认设备树中dmas的phandle指向的DMA控制器驱动是否已经加载。可以用ls /sys/class/dma/查看。
  • DMA传输完成回调不触发:检查dmaengine_prep_dma_cyclic是否传了DMA_PREP_INTERRUPT标志。我曾经漏掉这个,回调函数死活不执行。
  • 音频数据错位:检查DMA传输方向。TX是DMA_MEM_TO_DEV,RX是DMA_DEV_TO_MEM。写反了会导致数据全乱。
  • 系统休眠唤醒后DMA不工作:在pm_ops的resume回调中重新配置DMA通道。DMA控制器的状态在休眠时可能会丢失。
调试小工具: 在/sys/kernel/debug/dmaengine/目录下,可以看到每个DMA通道的状态。如果传输卡住了,先去看看这个文件,能快速定位问题。