4. 车载网络与通信协议

驱动开发,说白了就是让操作系统认识你的硬件。座舱系统里那么多外设——触摸屏、摄像头、音频编解码器、CAN控制器——每个都需要驱动来“翻译”硬件信号。我做了这么多年嵌入式,见过太多硬件调不通、驱动加载失败的问题,根源往往就是没搞懂这三个基础:字符设备框架、platform总线、设备树匹配。

这一章,咱们就把这三块骨头啃下来。

4.1 字符设备驱动框架

Linux驱动分三大类:字符设备、块设备、网络设备。座舱里90%的外设都是字符设备——数据按字节流读写,像串口、I2C、SPI、GPIO都是。我刚开始写驱动时,第一个练手的就是字符设备,因为它结构最清晰,也最容易理解内核的“文件即设备”思想。

4.1.1 核心数据结构:file_operations

字符设备驱动的灵魂,就是 file_operations 结构体。它定义了设备文件能支持哪些操作:

static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .unlocked_ioctl = my_ioctl,
};

每个成员都是一个函数指针。用户空间调用 open()、read()、write() 时,内核会通过这个结构体找到对应的驱动函数。嗯,这里要注意:.owner 必须赋值为 THIS_MODULE,防止模块被卸载时还有进程在使用它。

4.1.2 设备号与注册流程

每个字符设备在内核里都有一个唯一的设备号——主设备号 + 次设备号。主设备号标识驱动类型,次设备号标识具体设备。

步骤 函数/宏 说明
1 alloc_chrdev_region() 动态分配设备号(推荐,避免冲突)
2 cdev_init() 初始化 cdev 结构体,绑定 file_operations
3 cdev_add() 向内核注册字符设备
4 class_create() 创建设备类(自动生成 /sys/class 目录)
5 device_create() 创建设备节点(自动生成 /dev 下的文件)

我个人习惯用动态分配设备号,省心。以前在项目里手动指定主设备号,结果跟另一个模块冲突了,调试了一下午才发现——从那以后我再也不偷懒了。

关键点:字符设备驱动的本质就是“注册 + 回调”。你在模块初始化时注册设备号、绑定操作函数,内核在用户访问设备文件时回调你的函数。就这么简单。

4.1.3 完整的驱动骨架

static int __init my_driver_init(void)
{
    // 1. 分配设备号
    alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    
    // 2. 初始化cdev
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    
    // 3. 注册字符设备
    cdev_add(&my_cdev, dev_num, 1);
    
    // 4. 创建类与设备节点
    my_class = class_create(THIS_MODULE, "my_class");
    device_create(my_class, NULL, dev_num, NULL, "my_device");
    
    return 0;
}

static void __exit my_driver_exit(void)
{
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
}

module_init(my_driver_init);
module_exit(my_driver_exit);

这段代码是所有字符设备驱动的模板。你写座舱里的GPIO按键驱动、LED驱动、甚至简单的传感器驱动,都可以直接套这个架子。

4.2 platform总线驱动模型

字符设备框架解决了“怎么操作设备”的问题,但没解决“设备在哪里、怎么找到它”的问题。在嵌入式系统里,很多设备不挂在PCIe、USB这类可枚举的总线上,而是直接集成在SoC内部——比如I2C控制器、SPI控制器、DMA控制器。它们怎么被驱动发现?

答案就是 platform总线。说白了,它是内核虚拟出来的一条总线,专门用来管理那些“没有真实总线”的设备。

4.2.1 总线、设备、驱动

platform总线模型有三个核心角色:

  • platform_bus:虚拟总线,负责匹配设备和驱动
  • platform_device:描述硬件资源(地址、中断、DMA通道等)
  • platform_driver:实现驱动的 probe/remove 逻辑

匹配成功后,内核自动调用驱动的 probe() 函数。你在 probe 里做硬件初始化、注册字符设备、申请中断——所有“干活”的代码都放在这里。

我的经验:写platform驱动时,一定要把硬件资源(寄存器基址、中断号)从 device 里拿,不要硬编码。这样同一个驱动可以支持多个同类型设备,只需要改设备树就行。

4.2.2 platform_driver 结构

static struct platform_driver my_platform_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_device",
        .of_match_table = my_of_match,
    },
};

module_platform_driver(my_platform_driver);

module_platform_driver() 是个宏,它帮你把 module_init 和 module_exit 都包好了。我刚开始用的时候还自己写 init/exit,后来发现这个宏更干净。

4.3 设备树与驱动匹配机制

设备树(Device Tree)是座舱系统开发里绕不开的东西。它用文本文件描述硬件拓扑结构,内核在启动时解析它,自动创建设备节点。你想想看,如果没有设备树,每换一个硬件平台就得重新编译内核——那得多痛苦?

4.3.1 设备树的基本语法

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

/ {
    soc {
        i2c@ff110000 {
            compatible = "vendor,i2c-controller";
            reg = <0xff110000 0x1000>;
            interrupts = <0 56 4>;
            clocks = <&clk 12>;
        };
    };
};
  • compatible:匹配字符串,驱动通过它找到设备
  • reg:寄存器地址和大小
  • interrupts:中断号、类型
  • clocks:时钟源

4.3.2 匹配机制:compatible 的优先级

驱动和设备树怎么配对?核心就是 of_match_table:

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,i2c-controller", },
    { .compatible = "vendor,i2c-controller-v2", },
    { /* sentinel */ }
};

内核匹配时,按以下顺序查找:

  1. 设备树节点的 compatible 属性
  2. 驱动的 of_match_table 列表
  3. 如果匹配成功,调用 probe()

避坑指南:我曾经在项目里把 compatible 字符串写错了一个字母——"vendor,i2c" 写成了 "vendor,i2c"。内核不会报错,驱动就是不加载。调试了两天才发现是拼写问题。所以,compatible 字符串必须一字不差,包括大小写和标点。

4.3.3 从设备树获取资源

在 probe 函数里,你可以用标准API获取设备树里的资源:

static int my_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;
    int irq;
    
    // 获取寄存器地址
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    
    // 获取中断号
    irq = platform_get_irq(pdev, 0);
    
    // 注册字符设备...
    return 0;
}

devm_ 系列API是 managed 版本,设备移除时自动释放资源。我建议你尽量用这些函数,能省掉不少 cleanup 代码。

4.4 知识结构总览

下面这张图把三者的关系串起来了:

驱动开发基础结构图 驱动开发基础:三块基石 字符设备驱动 platform总线模型 设备树匹配 核心要素 • file_operations 结构体 • 设备号分配与注册 • cdev_init / cdev_add • class_create / device_create 核心要素 • platform_device • platform_driver • probe / remove 回调 • 资源获取 API 核心要素 • compatible 匹配 • reg / interrupts • of_match_table • 设备树解析 API 三者结合:设备树描述硬件 → platform总线匹配 → 字符设备提供操作接口 开发流程总结 1. 编写设备树文件(.dts),描述硬件资源 2. 编写 platform_driver,定义 of_match_table 3. 在 probe 中注册字符设备,实现 file_operations 4. 编译、加载、测试——用户空间通过 /dev/xxx 访问硬件

4.5 实战要点总结

写座舱驱动时,记住这几条:

  • 字符设备是基础——所有数据交互最终都落到 read/write/ioctl 上
  • platform总线是桥梁——它把设备树里的硬件描述和驱动代码连接起来
  • 设备树是配置——硬件资源写在 .dts 里,驱动代码里只拿不写

一句话总结:设备树告诉内核“有什么硬件”,platform总线负责“配对”,字符设备驱动负责“干活”。三个环节缺一不可,搞懂了它们,座舱里90%的外设驱动你都能拿下。