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_data和i2c_smbus_write_byte_data来读写寄存器了。
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打开,这样在串口驱动还没加载时就能看到启动信息。我当年排查一个内核崩溃问题,全靠这个。
probe/remove流程,剩下的就是查数据手册、配寄存器了。说白了,嵌入式Linux驱动开发,七分靠设备树,三分靠代码。
好,这一章就到这里。下一章,我会带你实战一下R329上的I2C音频编解码器驱动,把ES8316跑起来。到时候,你会看到音频数据是怎么从麦克风进到内核的。