2、MTK平台驱动架构:从HAL到内核的完整链路

好,咱们今天聊聊MTK平台的驱动架构。说实话,我刚接触MTK平台时,也被它那套分层体系搞得有点晕。但摸清楚之后你会发现,这套设计其实挺讲究的——它把Android的HAL层、Linux内核驱动、还有硬件总线协议串成了一条完整的链路。

我个人习惯把MTK的驱动架构想象成「三层楼」:

  • 顶楼:HAL层,负责跟Android框架打交道
  • 中间层:内核驱动框架,负责具体的硬件操作
  • 底楼:I2C/SPI总线,负责物理通信

而设备树(DTS)就是连接这三层楼的「施工图纸」。下面咱们一层层拆开看。

2.1 MTK HAL层架构

MTK的HAL层,说白了就是Android HAL规范在MTK芯片上的具体实现。它位于Android Framework和内核驱动之间,起到「翻译官」的作用。

核心结构

  • hardware/libhardware/include/hardware/sensors.h:定义传感器HAL的标准接口
  • vendor/mediatek/proprietary/hardware/sensors/:MTK私有的传感器HAL实现
  • 每个传感器类型对应一个HAL模块(如加速度计、陀螺仪、磁力计)

我记得第一次调试MTK的传感器HAL时,踩过一个坑:MTK的HAL层默认会做一次「数据校准」,但有些第三方传感器芯片自己也有校准逻辑。两者叠加,数据反而乱了。后来我直接在HAL层把MTK的校准关掉,只用芯片自带的。

关键点:MTK HAL层通过 hw_get_module() 加载传感器模块,然后调用 sensors_poll_device_t 结构体中的函数指针来操作硬件。

HAL层的典型调用流程是这样的:

  1. Framework调用 sensors_poll_device_t->activate() 使能传感器
  2. HAL层通过 ioctl()read() 与内核驱动通信
  3. 数据从内核驱动上报到HAL层,再通过 sensors_event_t 结构体返回给Framework

个人经验:调试HAL层时,建议在 sensors_poll_device_t->poll() 函数里加打印,看数据是否真的从内核上来了。我曾经花了两天查一个「传感器没数据」的问题,最后发现是HAL层的线程优先级太低,被系统调度卡住了。

2.2 Linux内核驱动框架

内核驱动这块,MTK基本遵循标准的Linux驱动模型,但加了一些自家的「调味料」。

标准框架

  • 字符设备驱动:通过 file_operations 结构体实现 open/read/write/ioctl
  • 输入子系统:传感器通常注册为 input_dev,通过 input_event 上报数据
  • IIO子系统:MTK部分传感器也支持Industrial I/O框架

你想想看,为什么MTK要用输入子系统?因为Android的EventHub直接监听 /dev/input 节点,用输入子系统可以省去一层适配。嗯,这里要注意:MTK的传感器驱动通常注册为 input_polled_dev,通过定时器轮询读取数据。

我曾经在项目中遇到一个诡异问题:传感器数据偶尔会「跳变」。查了两天,发现是内核驱动的轮询线程和中断处理函数发生了竞态。解决办法很简单——加一把自旋锁。

避坑指南:MTK平台的内核驱动中,ioctl 命令号定义在 mtk_sensor_ioctl.h 里。不同版本的MTK平台,命令号可能不一样。我曾经因为用了旧平台的命令号,导致新平台上的传感器完全无法控制。

2.3 I2C/SPI总线在MTK上的应用

传感器和主控之间的通信,99%的情况靠I2C或SPI。MTK平台对这两种总线的支持很成熟,但有些细节你得注意。

I2C总线

  • MTK的I2C控制器驱动在 drivers/i2c/busses/i2c-mt65xx.c
  • 支持标准模式(100kHz)和快速模式(400kHz)
  • 传感器通常使用7位地址,地址在DTS中配置

SPI总线

  • MTK的SPI控制器驱动在 drivers/spi/spi-mt65xx.c
  • 支持4种模式(CPOL/CPHA组合)
  • 传感器通常使用模式0或模式3

我个人习惯:能用I2C就别用SPI。为什么?I2C的接线少(只需要SDA和SCL),而且MTK的I2C驱动稳定性更好。SPI虽然速度快,但多一根CS线,布局布线时容易出问题。

看一个典型的I2C传感器驱动初始化代码:

static int sensor_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct sensor_data *data;
    int ret;

    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    i2c_set_clientdata(client, data);
    data->client = client;

    /* 检查芯片ID */
    ret = i2c_smbus_read_byte_data(client, REG_CHIP_ID);
    if (ret < 0) {
        dev_err(&client->dev, "failed to read chip id\n");
        return ret;
    }

    /* 注册输入设备 */
    data->input_dev = devm_input_allocate_device(&client->dev);
    // ... 后续初始化
}

经验之谈:I2C通信失败时,别急着怀疑驱动代码。先用示波器抓一下SDA和SCL的波形。我遇到过好几次,问题出在I2C上拉电阻没焊,或者阻值不对。

2.4 设备树(DTS)配置基础

设备树,就是告诉内核「我的板子上接了哪些外设,它们怎么连的」。MTK平台对DTS的依赖非常重,几乎所有硬件配置都在DTS里完成。

DTS文件结构

  • arch/arm64/boot/dts/mediatek/mtxxxx.dtsi:芯片级定义(SoC内部外设)
  • arch/arm64/boot/dts/mediatek/mtxxxx-evb.dts:板级定义(具体开发板)
  • arch/arm64/boot/dts/mediatek/mtxxxx-project.dts:项目级定义(你的产品)

一个典型的I2C传感器DTS节点长这样:

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;  /* 400kHz快速模式 */

    sensor@68 {
        compatible = "vendor,sensor-name";
        reg = <0x68>;           /* I2C 7位地址 */
        interrupt-parent = <&pio>;
        interrupts = <10 IRQ_TYPE_EDGE_FALLING>;
        pinctrl-names = "default", "sleep";
        pinctrl-0 = <&sensor_pins_default>;
        pinctrl-1 = <&sensor_pins_sleep>;
    };
};

这里有几个关键点:

  • compatible:驱动匹配的关键字,必须和驱动中的 of_match_table 一致
  • reg:I2C设备地址,注意是7位地址左移1位后的值
  • interrupts:中断配置,MTK的GPIO中断号从0开始
  • pinctrl:引脚复用配置,MTK平台必须配,否则I2C不通

曾经踩过的坑:我在一个项目里把I2C地址写成了0xD0(8位地址格式),但内核期望的是7位地址0x68。结果驱动一直probe失败,查了两天才发现是地址格式问题。记住:DTS里I2C地址用7位格式!

另外,MTK平台还有一个特殊的 mediatek,pull-up 属性,用来配置I2C引脚内部上拉。如果你的板子上I2C上拉电阻没焊,可以试试这个属性——但我不建议依赖它,毕竟内部上拉的驱动能力有限。

小结

嗯,这一章的内容就这些。总结一下:

  • HAL层是Android和内核的桥梁,MTK有自己的实现套路
  • 内核驱动用输入子系统或IIO框架,注意竞态问题
  • I2C/SPI是传感器通信的主力,调试时多用示波器
  • DTS配置要仔细,地址、中断、pinctrl一个都不能少

下一章咱们会深入讲传感器数据流的具体实现,包括中断上报和轮询策略的选择。到时候我会分享一个实际项目中的性能调优案例,挺有意思的。