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中定义的名字。
dmas的<phandle>是否正确。可以用ls -l /sys/class/dma/查看系统中有哪些DMA控制器,确认phandle指向的DMA控制器确实存在。
4.4 dmaengine API使用:配置与启动传输
拿到DMA通道后,我们需要配置传输参数。对于I2S音频流,通常是循环传输(cyclic mode),因为音频数据是连续不断的。
配置DMA传输的步骤:
- 准备DMA描述符:调用
dmaengine_prep_dma_cyclic函数。 - 提交描述符:调用
dmaengine_submit。 - 启动传输:调用
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 |
使能传输完成中断 | 忘记设置会导致回调函数不被调用 |
buffer_size和period_size必须与ALSA音频缓冲区配置保持一致。否则,音频数据会断流或者产生噪音。我建议在probe阶段就把这些参数计算好,存到设备结构体中。
4.5 知识结构图:I2S控制器驱动初始化流程
下面这张图总结了从设备树解析到DMA传输启动的完整流程。你可以把它当作一个检查清单,写代码时对照着看。
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通道的状态。如果传输卡住了,先去看看这个文件,能快速定位问题。