3. 设备树配置:I2S控制器节点、DMA通道绑定、时钟与GPIO配置、codec节点实例

设备树,说白了就是Linux内核用来描述硬件拓扑的“说明书”。你想想看,内核要驱动一个I2S音频接口,它得知道:I2S控制器挂在哪条总线上?用哪个DMA通道?主时钟从哪来?GPIO怎么配?Codec的I2C地址是多少?这些信息,全写在设备树里。

我个人习惯,拿到一块新板子,第一件事就是读它的设备树。不是看原理图,就是看dts。因为设备树里写的,就是硬件工程师和驱动开发者之间的“契约”。

3.1 I2S控制器节点

先看一个典型的I2S控制器节点。以三星Exynos系列为例,它的I2S控制器在设备树里长这样:

i2s0: i2s@3830000 {
    compatible = "samsung,s5pv210-i2s";
    reg = <0x03830000 0x100>;
    interrupts = <GIC_SPI 28 IRQ_TYPE_LEVEL_HIGH>;
    dmas = <pdma0 9>, <pdma0 8>;
    dma-names = "tx", "rx";
    clocks = <&clock CLK_I2S0>, <&clock CLK_SCLK_I2S0>;
    clock-names = "i2s", "i2s_opclk0";
    pinctrl-names = "default";
    pinctrl-0 = <&i2s0_bus>;
    #sound-dai-cells = <0>;
    status = "okay";
};

这里每个字段都有讲究:

属性 含义 我在项目中踩过的坑
compatible 匹配驱动的字符串 拼写错一个字母,驱动就加载不上
reg 寄存器基地址和范围 地址写错,内核直接panic
interrupts 中断号、类型 中断号对不上,音频卡顿无声音
dmas / dma-names DMA通道绑定 顺序反了,录音变成播放
clocks / clock-names 时钟源配置 时钟频率不对,采样率全乱套
pinctrl 引脚复用配置 GPIO没配成I2S功能,波形出不来
#sound-dai-cells DAI链接标识 漏了这个,ASoC无法绑定
⚠️ 注意: 我曾经在一个项目里,因为reg属性里的地址范围写小了(只写了0x80,实际寄存器需要0x100),结果访问到后面的寄存器时,内核报了一堆“external abort”。查了两天才发现是设备树的问题。所以,地址范围一定要和芯片手册对清楚。

3.2 DMA通道绑定

I2S传输数据,靠的是DMA。CPU如果一条一条地去搬音频数据,那啥也别干了。所以,设备树里必须把DMA通道绑对。

看上面例子里的这两行:

dmas = <pdma0 9>, <pdma0 8>;
dma-names = "tx", "rx";

这里的意思是:

  • 第一个DMA通道(pdma0的9号请求线)用于发送(tx)
  • 第二个DMA通道(pdma0的8号请求线)用于接收(rx)

你想想看,如果我把dma-names写成"rx", "tx",那播放音乐时,DMA会从录音通道拿数据,出来的声音就是一片噪音。嗯,我确实见过有人这么干过。

💡 小技巧: 我个人习惯,在写DMA绑定时,先确认芯片手册里I2S的DMA请求线编号。有些芯片的tx和rx请求线是连续的(比如8和9),有些则不是。最好在dts里加上注释,方便后来人。

3.3 时钟与GPIO配置

时钟是I2S的心脏。没有正确的时钟,采样率就是空中楼阁。

设备树里时钟配置的核心是clocks和clock-names:

clocks = <&clock CLK_I2S0>, <&clock CLK_SCLK_I2S0>;
clock-names = "i2s", "i2s_opclk0";

这里有两个时钟:

  • "i2s":I2S控制器的总线时钟(APB时钟),用于寄存器访问
  • "i2s_opclk0":I2S的操作时钟(位时钟的根时钟),用于生成BCLK和LRCLK

我曾经遇到一个案例:板子播放16kHz采样率的音频没问题,但一换到48kHz就变调。查了半天,发现是i2s_opclk0的父时钟被设成了固定的24.576MHz,而这个频率无法通过分频精确得到48kHz的BCLK。后来改了时钟树,问题解决。

GPIO配置则通过pinctrl子系统完成:

pinctrl-names = "default";
pinctrl-0 = <&i2s0_bus>;

对应的引脚节点通常在另一个dtsi文件里:

i2s0_bus: i2s0-bus {
    samsung,pins = "gpi1-0", "gpi1-1", "gpi1-2", "gpi1-3";
    samsung,pin-function = <3>;  // I2S功能
    samsung,pin-pud = <0>;       // 无上下拉
    samsung,pin-drv = <0>;       // 驱动强度
};
🔑 关键点: 时钟和GPIO的配置,一定要和原理图、芯片手册三方对照。我曾经见过一个项目,原理图上I2S的SCLK引脚标的是GPIOA12,但设备树里配成了GPIOA13,结果I2S总线死活没有时钟输出。

3.4 Codec节点实例

Codec节点通常挂在I2C总线下,因为大多数Codec都是通过I2C来配置寄存器的。来看一个WM8960的实例:

&i2c1 {
    wm8960: wm8960@1a {
        compatible = "wlf,wm8960";
        reg = <0x1a>;
        clocks = <&clk IMX8MM_CLK_SAI3_ROOT>;
        clock-names = "mclk";
        #sound-dai-cells = <0>;
        wlf,shared-lrclk;
    };
};

这里要注意几个点:

  • reg = <0x1a>:这是Codec的I2C地址。WM8960的7位地址是0x1a(如果ADDR引脚接地)。我见过有人写成0x34(那是8位地址),结果I2C探测不到设备。
  • clocks:Codec的主时钟(MCLK),通常由SoC的某个时钟输出提供。频率一般是256×采样率或512×采样率。
  • wlf,shared-lrclk:这是WM8960特有的属性,表示左右声道共用LRCLK。不是所有Codec都需要,具体看数据手册。

完整的音频设备树,还需要一个sound节点来把I2S控制器和Codec绑定在一起:

sound {
    compatible = "simple-audio-card";
    simple-audio-card,name = "wm8960-audio";
    simple-audio-card,format = "i2s";
    simple-audio-card,bitclock-master = <&sound_master>;
    simple-audio-card,frame-master = <&sound_master>;
    simple-audio-card,widgets =
        "Speaker", "Speaker",
        "Microphone", "Mic Jack";
    simple-audio-card,routing =
        "Speaker", "OUT3",
        "Mic Jack", "MIC1";

    sound_master: simple-audio-card,cpu {
        sound-dai = <&i2s0>;
    };

    simple-audio-card,codec {
        sound-dai = <&wm8960>;
    };
};

这个sound节点,说白了就是告诉内核:I2S控制器和Codec是一对,它们之间用I2S格式通信,主时钟由I2S控制器提供。

3.5 知识结构总览

下面这张图,把整个设备树配置的脉络梳理清楚了:

I2S音频设备树配置全景图 设备树根节点 I2S控制器节点 DMA通道绑定 Codec节点 compatible / reg interrupts clocks / clock-names pinctrl dmas = <phandle req> dma-names = "tx","rx" 顺序:先tx后rx compatible / reg (I2C地址) clocks (MCLK) #sound-dai-cells Sound Card 节点 ⚠️ 三者缺一不可,否则音频无法工作

从这张图可以看得很清楚:I2S控制器、DMA通道、Codec节点,三者通过sound节点汇聚到一起。任何一个环节配置错误,音频流都跑不通。

💡 调试建议: 设备树配置完成后,可以在内核启动日志里搜索i2s、dma、wm8960(或你的Codec名字)来确认节点是否被正确解析。如果看到OF: fdt: not found之类的错误,那就是设备树没写对。