第四章:内核驱动开发:高通平台Linux内核适配,设备树(DTS)配置与GPIO、I2C、SPI驱动实战
好,咱们进入实战环节了。前面几章聊的都是架构和原理,这一章咱们把手弄脏——直接写代码、改设备树、调驱动。
高通平台的Linux内核适配,说白了就是让Linux内核认识你的硬件。我刚开始做高通平台时,最头疼的就是设备树。那玩意儿看着像JSON,又像C语言的结构体,写错了还不报错,板子直接起不来。嗯,后来摸清了门道,其实没那么玄乎。
4.1 高通平台Linux内核适配思路
高通平台的Linux内核,跟主线Linux内核不太一样。高通有自己的代码仓库,叫msm-4.14、msm-4.19之类的。你拿到的BSP包,里面已经包含了大部分驱动。
我们要做的,不是从零写驱动,而是做三件事:
- 配置内核选项:把需要的驱动模块编译进去
- 修改设备树:告诉内核你的硬件长什么样
- 调试和适配:让驱动跑起来,跑得稳
我个人习惯,拿到一个新平台,先看arch/arm64/boot/dts/qcom/目录下的dts文件。高通给每个平台都提供了参考设计,比如sm8250.dtsi。你的板子文件,比如my-board.dts,会include这个公共文件,然后覆盖或添加自己的节点。
核心原则:不要直接修改.dtsi文件。那是芯片厂商的参考设计。你的板级配置写在.dts文件里,通过&节点名的方式引用和覆盖。
4.2 设备树(DTS)配置实战
设备树,说白了就是硬件的"户口本"。内核启动时,通过设备树知道:哪个GPIO接了LED,哪个I2C总线上挂了传感器,哪个SPI设备是触摸屏。
我遇到过最坑的一次,就是设备树里GPIO的gpio-hog配置错了,导致某个引脚被强制拉高,外设死活不工作。查了两天才发现。
4.2.1 设备树基本结构
一个典型的设备树节点长这样:
// 在 my-board.dts 中
&qupv3_se0_i2c {
status = "okay";
/* 挂载一个I2C传感器 */
bmi160@68 {
compatible = "bosch,bmi160";
reg = <0x68>;
interrupt-parent = <&tlmm>;
interrupts = <84 2>; /* GPIO84, 下降沿触发 */
vdd-supply = <&pm8150_l17>;
vddio-supply = <&pm8150_l6>;
};
};
这里要注意几个关键字段:
| 字段 | 含义 | 我的经验 |
|---|---|---|
compatible | 驱动匹配的关键字 | 一定要跟驱动里的of_match_table完全一致 |
reg | 设备地址或片选号 | I2C是7位地址,SPI是片选索引 |
interrupts | 中断配置 | 高通平台常用&tlmm作为中断控制器 |
status | 使能状态 | 默认是disabled,记得改成okay |
4.2.2 GPIO配置的坑
高通平台的GPIO,通过pinctrl子系统管理。你想想看,一个GPIO既能当普通IO,又能当I2C的SCL,还能当UART的TX。怎么区分?靠pinctrl配置。
/* 在 dts 中配置 GPIO 功能 */
&tlmm {
/* 定义一组引脚配置 */
my_gpio_default: my_gpio_default {
mux {
pins = "gpio42";
function = "gpio"; /* 作为普通GPIO */
};
config {
pins = "gpio42";
drive-strength = <2>; /* 2mA驱动能力 */
bias-pull-up; /* 上拉 */
output-high; /* 默认输出高电平 */
};
};
};
我曾经踩过的坑:pinctrl的节点名不能重复。如果你在多个地方定义了同名pinctrl节点,编译器不会报错,但只有最后一个生效。我那次就是两个驱动都用了default这个名字,结果其中一个的引脚配置被覆盖了,外设工作异常。
4.3 GPIO驱动实战
高通平台的GPIO驱动,内核里已经写好了。我们只需要在设备树里配置好,然后在用户空间通过sysfs或libgpiod操作就行。
但我建议你写一个简单的内核模块来操作GPIO。为什么?因为用户空间操作有延迟,不适合高频场景。我在项目中做过一个手势传感器,需要微秒级的GPIO中断响应,用户空间根本扛不住。
4.3.1 一个简单的GPIO驱动示例
#include <linux/module.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of.h>
struct my_gpio_dev {
struct gpio_desc *led_gpio;
struct gpio_desc *btn_gpio;
};
static int my_gpio_probe(struct platform_device *pdev)
{
struct my_gpio_dev *dev;
struct device *dev_ptr = &pdev->dev;
dev = devm_kzalloc(dev_ptr, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
/* 获取GPIO,使用设备树中的名字 */
dev->led_gpio = devm_gpiod_get(dev_ptr, "led", GPIOD_OUT_LOW);
if (IS_ERR(dev->led_gpio))
return PTR_ERR(dev->led_gpio);
dev->btn_gpio = devm_gpiod_get(dev_ptr, "button", GPIOD_IN);
if (IS_ERR(dev->btn_gpio))
return PTR_ERR(dev->btn_gpio);
/* 设置GPIO标签,方便调试 */
gpiod_set_consumer_name(dev->led_gpio, "my-led");
platform_set_drvdata(pdev, dev);
return 0;
}
static const struct of_device_id my_gpio_of_match[] = {
{ .compatible = "mycompany,my-gpio-device" },
{ /* 结束标记 */ }
};
MODULE_DEVICE_TABLE(of, my_gpio_of_match);
static struct platform_driver my_gpio_driver = {
.probe = my_gpio_probe,
.driver = {
.name = "my_gpio",
.of_match_table = my_gpio_of_match,
},
};
module_platform_driver(my_gpio_driver);
对应的设备树节点:
my_gpio_device {
compatible = "mycompany,my-gpio-device";
led-gpios = <&tlmm 42 GPIO_ACTIVE_HIGH>;
button-gpios = <&tlmm 43 GPIO_ACTIVE_LOW>;
};
小技巧:用devm_gpiod_get而不是gpio_request。前者是设备资源管理,驱动卸载时自动释放GPIO,不会漏掉。我早期写驱动时习惯手动管理,结果经常忘记释放,导致第二次insmod失败。
4.4 I2C驱动实战
高通平台的I2C控制器叫QUPV3(Qualcomm Universal Peripheral)。每个QUPV3可以配置成I2C、SPI或UART。设备树里通过function字段指定。
写I2C驱动,核心是填充i2c_driver结构体。我拿一个温度传感器举例:
static int my_temp_probe(struct i2c_client *client)
{
struct i2c_adapter *adapter = client->adapter;
int ret;
/* 检查I2C功能是否支持 */
if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA))
return -EIO;
/* 读取设备ID,确认设备存在 */
ret = i2c_smbus_read_byte_data(client, 0x0F);
if (ret < 0)
return ret;
dev_info(&client->dev, "Temperature sensor found, ID=0x%02x\n", ret);
return 0;
}
static const struct i2c_device_id my_temp_id[] = {
{ "my_temp_sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_temp_id);
static const struct of_device_id my_temp_of_match[] = {
{ .compatible = "mycompany,my-temp-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, my_temp_of_match);
static struct i2c_driver my_temp_driver = {
.driver = {
.name = "my_temp",
.of_match_table = my_temp_of_match,
},
.probe = my_temp_probe,
.id_table = my_temp_id,
};
module_i2c_driver(my_temp_driver);
注意:I2C驱动的id_table和of_match_table都要填。前者用于ACPI或旧式板级代码,后者用于设备树匹配。我见过有人只填了of_match_table,结果在非设备树平台上驱动加载失败。
4.5 SPI驱动实战
SPI驱动跟I2C类似,但多了一个spi_transfer的概念。高通平台的SPI控制器也是QUPV3,配置方式跟I2C一样。
写SPI驱动时,最核心的是spi_message和spi_transfer。我做过一个SPI接口的ADC驱动,采样率要求1MHz,用spi_sync_transfer一次性发送和接收:
static int my_adc_read(struct spi_device *spi, u16 *value)
{
u8 tx_buf[2] = {0x00, 0x00}; /* 命令字节 */
u8 rx_buf[2] = {0};
struct spi_transfer transfer = {
.tx_buf = tx_buf,
.rx_buf = rx_buf,
.len = 2,
.speed_hz = 1000000, /* 1MHz */
.bits_per_word = 8,
.cs_change = 0,
};
struct spi_message msg;
int ret;
spi_message_init(&msg);
spi_message_add_tail(&transfer, &msg);
ret = spi_sync(spi, &msg);
if (ret < 0)
return ret;
*value = (rx_buf[0] << 8) | rx_buf[1];
return 0;
}
我曾经踩过的坑:SPI的时钟极性(CPOL)和相位(CPHA)必须跟外设匹配。高通平台默认是模式0(CPOL=0, CPHA=0),但很多传感器用的是模式3(CPOL=1, CPHA=1)。设备树里通过spi-cpol和spi-cpha属性设置。我那次忘了配,读出来的数据全是0xFF,折腾了一下午才发现是时序不匹配。
4.6 调试技巧与避坑指南
驱动写好了,怎么调试?我总结几个实用方法:
- 看内核日志:
dmesg | grep my_driver,看probe有没有成功 - 检查设备树:
ls /sys/firmware/devicetree/base/,看你的节点有没有被解析 - 查看GPIO状态:
cat /sys/kernel/debug/gpio,看引脚方向、电平对不对 - I2C探测:
i2cdetect -y 0,看设备地址有没有响应 - SPI回环测试:把MOSI和MISO短接,发数据看能不能收回来
嗯,这里要注意一点。高通平台的debugfs默认可能没挂载。你需要在内核配置里打开CONFIG_DEBUG_FS,然后手动挂载:mount -t debugfs none /sys/kernel/debug。
最后说一句,驱动开发没有捷径。多读高通官方的Documentation目录下的文档,多看drivers/i2c/busses/i2c-qup.c这种现成的驱动代码。我当年就是靠读高通的原生驱动,一点点摸清了门道。
下一章,咱们聊聊性能优化和功耗管理。那才是高通平台真正体现价值的地方。