3、Linux驱动基础:GPIO、I2C、SPI、UART驱动框架与设备树配置

好,咱们进入第三章。这一章我打算聊聊嵌入式Linux驱动里最基础、也最常用的几个外设:GPIO、I2C、SPI、UART。说实话,不管你是做音频处理还是语音识别,最终都绕不开跟这些硬件打交道。R329这颗芯片,它的音频编解码、麦克风阵列、甚至跟外部功放的通信,底层全是靠这些东西撑起来的。

我个人习惯,学驱动先别急着看代码,先把“设备树”这个概念搞明白。因为现在的Linux内核,尤其是ARM架构的,基本都靠设备树来描述硬件。你想想看,没有设备树,内核怎么知道你板子上接了哪些外设?

3.1 设备树:硬件的“户口本”

设备树(Device Tree),说白了就是一个描述硬件信息的文本文件。它的后缀是.dts,编译后变成.dtb,内核启动时解析它。嗯,这里要注意:设备树不是驱动代码,它只是告诉内核“我这里有这么一个I2C设备,地址是0x1A”。

我在项目中遇到过好几次,板子死活起不来,最后发现是设备树里某个GPIO的pinmux配错了。所以,设备树配错了,驱动写得再好也没用。

一个典型的设备树节点长这样:

i2c0: i2c@0x01c2ac00 {
    compatible = "allwinner,sun8i-i2c";
    reg = <0x01c2ac00 0x400>;
    interrupts = <0 7 4>;
    clocks = <&ccu CLK_BUS_I2C0>;
    resets = <&ccu RST_BUS_I2C0>;
    pinctrl-names = "default";
    pinctrl-0 = <&i2c0_pins>;
    status = "disabled";

    codec: audio-codec@0x1a {
        compatible = "es8316";
        reg = <0x1a>;
        #sound-dai-cells = <0>;
    };
};

你看,compatible属性就是驱动和设备的“接头暗号”。内核启动时,会遍历所有设备节点,找到匹配的驱动。这个机制,我建议你好好理解一下。

小技巧: 调试设备树时,可以用 dtc -I dtb -O dts 命令把编译好的dtb反编译回dts,看看实际生效的配置跟你写的是否一致。我经常这么干。

3.2 GPIO驱动:最基础的“开关”

GPIO,通用输入输出。说白了就是控制某个引脚输出高电平或低电平,或者读取它的电平状态。在R329上,GPIO驱动通常通过gpiolib框架来实现。

你不需要自己从头写GPIO驱动,内核已经帮你封装好了。你只需要在设备树里声明:

&pio {
    gpio-leds {
        compatible = "gpio-leds";
        status-led {
            label = "status";
            gpios = <&pio 7 10 GPIO_ACTIVE_HIGH>;  // PH10
            linux,default-trigger = "heartbeat";
        };
    };
};

然后在用户空间,通过/sys/class/gpio或者libgpiod库来操作。我个人习惯用libgpiod,因为它更现代,而且不用处理那些过时的sysfs接口。

我曾经踩过一个坑:R329的某个GPIO默认有内部上拉,但我没注意,结果读到的电平一直是高。后来查了数据手册才发现。所以,配置GPIO时,上下拉状态一定要确认清楚。

3.3 I2C驱动:音频编解码器的“桥梁”

I2C是一种两线制的串行总线,一根时钟线(SCL),一根数据线(SDA)。在R329上,I2C主要用于配置音频编解码芯片,比如ES8316、ES7210这些。

I2C驱动的核心是i2c_driver结构体。你注册一个驱动,内核会在设备树里找compatible匹配的设备。匹配上了,就调用你的probe函数。

static const struct i2c_device_id es8316_id[] = {
    { "es8316", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, es8316_id);

static const struct of_device_id es8316_of_match[] = {
    { .compatible = "es8316" },
    { }
};
MODULE_DEVICE_TABLE(of, es8316_of_match);

static struct i2c_driver es8316_i2c_driver = {
    .driver = {
        .name = "es8316",
        .of_match_table = es8316_of_match,
    },
    .probe = es8316_i2c_probe,
    .remove = es8316_i2c_remove,
    .id_table = es8316_id,
};

你看,of_match_table就是用来匹配设备树的。驱动加载后,你就可以通过i2c_smbus_read_byte_datai2c_smbus_write_byte_data来读写寄存器了。

注意: I2C通信时,如果从设备没应答(NACK),驱动会返回错误。我在调试ES7210时遇到过,原来是地址写错了——7位地址和8位地址搞混了。R329的I2C控制器用的是7位地址,但有些数据手册写的是8位,你得自己右移一位。

3.4 SPI驱动:高速数据传输的“高速公路”

SPI比I2C快,四线制(MOSI、MISO、SCLK、CS),全双工。在R329上,SPI可以用来连接外部Flash、显示屏,或者某些高速ADC/DAC。

SPI驱动的框架跟I2C类似,但更简单一些。你注册一个spi_driver,然后在probe里设置好SPI设备的模式、频率、位宽等参数。

static struct spi_driver my_spi_driver = {
    .driver = {
        .name = "my_spi_device",
        .of_match_table = my_spi_of_match,
    },
    .probe = my_spi_probe,
    .remove = my_spi_remove,
};

设备树里配置SPI设备:

&spi0 {
    status = "okay";
    num-cs = <1>;
    cs-gpios = <&pio 2 4 GPIO_ACTIVE_LOW>;  // PC4

    spidev@0 {
        compatible = "my_spi_device";
        reg = <0>;
        spi-max-frequency = <10000000>;
    };
};

我记得有一次,SPI通信速率上不去,波形都变形了。后来发现是R329的SPI时钟极性(CPOL)和相位(CPHA)没配对。这个一定要跟从设备的数据手册对清楚。

3.5 UART驱动:调试与通信的“老伙计”

UART,串口。做嵌入式开发,这玩意儿就是你的“生命线”。打印日志、调试内核、跟蓝牙模块通信,全得靠它。

R329内部有多个UART控制器,通常UART0被用作调试串口。驱动框架是serial_core,你基本不用动,内核已经帮你搞定了。你需要做的,就是在设备树里配置好引脚和波特率。

&uart0 {
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_pins>;
    status = "okay";
};

然后,你就可以用minicom或者screen连上去看了。嗯,这里有个小建议:调试时,把内核的earlyprintk打开,这样在串口驱动还没加载时就能看到启动信息。我当年排查一个内核崩溃问题,全靠这个。

核心总结: GPIO、I2C、SPI、UART这四种驱动,在R329上的实现框架是统一的。设备树是硬件描述,驱动是软件逻辑。你只要掌握了设备树的写法,再理解驱动框架的probe/remove流程,剩下的就是查数据手册、配寄存器了。说白了,嵌入式Linux驱动开发,七分靠设备树,三分靠代码。

好,这一章就到这里。下一章,我会带你实战一下R329上的I2C音频编解码器驱动,把ES8316跑起来。到时候,你会看到音频数据是怎么从麦克风进到内核的。