第四章:内核驱动开发:高通平台Linux内核适配,设备树(DTS)配置与GPIO、I2C、SPI驱动实战

好,咱们进入实战环节了。前面几章聊的都是架构和原理,这一章咱们把手弄脏——直接写代码、改设备树、调驱动。

高通平台的Linux内核适配,说白了就是让Linux内核认识你的硬件。我刚开始做高通平台时,最头疼的就是设备树。那玩意儿看着像JSON,又像C语言的结构体,写错了还不报错,板子直接起不来。嗯,后来摸清了门道,其实没那么玄乎。

4.1 高通平台Linux内核适配思路

高通平台的Linux内核,跟主线Linux内核不太一样。高通有自己的代码仓库,叫msm-4.14msm-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驱动,内核里已经写好了。我们只需要在设备树里配置好,然后在用户空间通过sysfslibgpiod操作就行。

但我建议你写一个简单的内核模块来操作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_tableof_match_table都要填。前者用于ACPI或旧式板级代码,后者用于设备树匹配。我见过有人只填了of_match_table,结果在非设备树平台上驱动加载失败。

4.5 SPI驱动实战

SPI驱动跟I2C类似,但多了一个spi_transfer的概念。高通平台的SPI控制器也是QUPV3,配置方式跟I2C一样。

写SPI驱动时,最核心的是spi_messagespi_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-cpolspi-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这种现成的驱动代码。我当年就是靠读高通的原生驱动,一点点摸清了门道。

下一章,咱们聊聊性能优化和功耗管理。那才是高通平台真正体现价值的地方。