嵌入式 Linux 驱动开发:从设备树到字符设备的全链路调试
一、驱动开发最怕的不是写代码,是调不出错
一块新的传感器板子接到 i.MX8 上,I2C 通信不上。设备树配了,驱动注册了,i2cdetect能看到设备地址,但读出来的数据全是 0xFF。是设备树配置错了?是 I2C 时序不对?是传感器没初始化?还是硬件上拉电阻选错了?排查了两天,最后发现是设备树里 reg 属性写成了 16 进制但驱动按 10 进制解析。
嵌入式 Linux 驱动开发的特点是:代码量不大,但调试链路极长。从设备树配置到内核驱动匹配,从总线通信到寄存器读写,从用户空间系统调用到内核空间中断处理,任何一个环节出错都会导致功能异常,而错误现象往往指向错误的方向。本文从设备树到字符设备,完整梳理驱动开发的全链路,重点放在调试方法而非代码模板。
二、驱动加载的全链路机制
2.1 从设备树到驱动匹配
设备树(Device Tree)是硬件描述的标准格式。内核启动时解析设备树,为每个节点创建 platform_device。驱动注册时声明 compatible 字符串,内核通过 compatible 匹配 device 和 driver。
flowchart TD A[DTS源文件] --> B[dtc编译为DTB] B --> C[Bootloader加载DTB到内存] C --> D[内核解析DTB] D --> E[创建platform_device] E --> F[遍历已注册的driver] F --> G{compatible匹配?} G -->|匹配| H[调用driver.probe()] G -->|不匹配| I[继续遍历] H --> J[probe中初始化硬件] J --> K[注册字符设备/sysfs] K --> L[用户空间可访问] style A fill:#4dabf7,color:#fff style G fill:#ffd43b,color:#333 style L fill:#51cf66,color:#fff2.2 匹配失败的常见原因
匹配失败是最常见的问题,原因通常有三:
第一,compatible 字符串不一致。设备树写"vendor,sensor123",驱动写"vendor,sensor-123",一个连字符的差异导致匹配失败。
第二,设备树节点没有对应的总线。I2C 设备必须挂在 I2C 控制器节点下,如果直接放在根节点,内核不会为它创建 i2c_client。
第三,内核配置未启用对应驱动。驱动代码存在,但CONFIG_xxx未设为y或m,编译时被排除。
三、字符设备驱动的完整实现
3.1 I2C 传感器驱动框架
#include <linux/module.h> #include <linux/init.h> #include <linux/i2c.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/mutex.h> #include <linux/delay.h> #define DRIVER_NAME "my_sensor" #define DEVICE_NAME "my_sensor" /* 传感器寄存器定义 */ #define REG_WHO_AM_I 0x0F #define REG_CTRL1 0x20 #define REG_DATA_X 0x28 #define REG_DATA_Y 0x2A #define REG_DATA_Z 0x2C #define WHO_AM_I_VALUE 0x3F /* 传感器数据结构 */ struct sensor_data { int16_t x; int16_t y; int16_t z; }; /* 驱动私有数据 */ struct my_sensor_dev { struct i2c_client *client; /* I2C客户端 */ struct cdev cdev; /* 字符设备 */ dev_t devt; /* 设备号 */ struct class *class; /* 设备类 */ struct device *device; /* 设备节点 */ struct mutex lock; /* 互斥锁 */ bool initialized; }; /* I2C寄存器读取 */ static int sensor_read_reg( struct i2c_client *client, u8 reg, u8 *buf, int len ) { struct i2c_msg msgs[2]; int ret; /* 第一条消息:发送寄存器地址 */ msgs[0].addr = client->addr; msgs[0].flags = 0; /* 写操作 */ msgs[0].len = 1; msgs[0].buf = ® /* 第二条消息:读取数据 */ msgs[1].addr = client->addr; msgs[1].flags = I2C_M_RD; /* 读操作 */ msgs[1].len = len; msgs[1].buf = buf; ret = i2c_transfer(client->adapter, msgs, 2); if (ret < 0) { dev_err(&client->dev, "I2C读取失败: reg=0x%02x, ret=%d\n", reg, ret); return ret; } return 0; } /* I2C寄存器写入 */ static int sensor_write_reg( struct i2c_client *client, u8 reg, u8 value ) { u8 buf[2] = { reg, value }; int ret; ret = i2c_master_send(client, buf, 2); if (ret < 0) { dev_err(&client->dev, "I2C写入失败: reg=0x%02x, ret=%d\n", reg, ret); return ret; } return 0; } /* 传感器初始化 */ static int sensor_init(struct my_sensor_dev *dev) { struct i2c_client *client = dev->client; u8 who_am_i; int ret; /* 读取WHO_AM_I寄存器验证设备 */ ret = sensor_read_reg(client, REG_WHO_AM_I, &who_am_i, 1); if (ret < 0) { dev_err(&client->dev, "读取WHO_AM_I失败\n"); return ret; } if (who_am_i != WHO_AM_I_VALUE) { dev_err(&client->dev, "设备ID不匹配: 期望0x%02x, 实际0x%02x\n", WHO_AM_I_VALUE, who_am_i); return -ENODEV; } dev_info(&client->dev, "传感器识别成功: WHO_AM_I=0x%02x\n", who_am_i); /* 配置控制寄存器:启用所有轴,设置ODR */ ret = sensor_write_reg(client, REG_CTRL1, 0x77); if (ret < 0) { dev_err(&client->dev, "配置CTRL1失败\n"); return ret; } /* 等待传感器稳定 */ msleep(50); dev->initialized = true; dev_info(&client->dev, "传感器初始化完成\n"); return 0; } /* 读取传感器数据 */ static int sensor_read_data( struct my_sensor_dev *dev, struct sensor_data *data ) { struct i2c_client *client = dev->client; u8 buf[6]; int ret; if (!dev->initialized) { return -EPERM; } /* 连续读取6字节(X/Y/Z各2字节) */ ret = sensor_read_reg(client, REG_DATA_X | 0x80, buf, 6); if (ret < 0) { return ret; } /* 拼接16位数据(小端序) */ >/* 设备树节点:传感器挂在I2C1总线上 */ &i2c1 { status = "okay"; my_sensor@1e { compatible = "vendor,my-sensor"; reg = <0x1e>; /* I2C 7位地址 */ vdd-supply = <®_3v3>; interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; }; };3.3 调试命令速查
# 检查设备树节点是否被内核解析 ls /proc/device-tree/i2c1/my_sensor@1e/ # 检查I2C总线上的设备 i2cdetect -y 1 # 手动读写I2C寄存器 i2cget -y 1 0x1e 0x0f # 读取WHO_AM_I i2cset -y 1 0x1e 0x20 0x77 # 写入CTRL1 # 查看驱动注册信息 dmesg | grep my_sensor # 查看设备节点 ls -la /dev/my_sensor # 用户空间读取数据 cat /dev/my_sensor | hexdump -C # 查看内核日志中的I2C错误 dmesg | grep -i "i2c.*error\|i2c.*timeout\|i2c.*nak"四、驱动调试的常见陷阱
4.1 设备树 reg 地址的进制混淆
设备树中reg = <0x1e>是 16 进制,但i2c_client->addr在内核中是十进制表示。如果驱动中硬编码了地址比较(如if (client->addr == 30)),0x1e=30 恰好一致;但如果地址是 0x20,十进制是 32,硬编码if (client->addr == 20)就会匹配失败。永远不要在驱动中硬编码 I2C 地址,使用设备树的 reg 属性。
4.2 I2C 通信的时序陷阱
I2C 多字节读取时,寄存器地址的最高位需要置 1(地址自增模式)。不同传感器的自增位位置不同,有的在 bit7,有的在 bit0。读出来的数据全是同一字节的重复,通常是自增位没设对。
另一个常见问题是上拉电阻。I2C 总线需要外部上拉,典型值 4.7kΩ。上拉太大,信号上升沿变慢,通信失败;上拉太小,功耗增大。多设备共享总线时,上拉电阻需要并联计算。
4.3 适用与禁用场景
适用场景:自定义硬件的 Linux 驱动开发、I2C/SPI 传感器驱动、需要用户空间接口的设备控制。
禁用场景:已有内核驱动的标准设备(直接用现有驱动)、对实时性要求极高的控制(Linux 非实时,应使用 RTOS 或 PREEMPT_RT)、资源极度受限的 MCU(不适合跑 Linux)。
五、总结
嵌入式 Linux 驱动开发的核心链路是:设备树描述硬件→内核匹配驱动→probe 初始化硬件→注册字符设备→用户空间访问。每个环节都有独立的调试方法:设备树用/proc/device-tree/验证,I2C 通信用i2cdetect/i2cget验证,驱动匹配用dmesg验证,字符设备用/dev/节点验证。调试的关键是逐环节排查,不要跳步——如果设备树节点都没解析出来,去调 I2C 时序是浪费时间。I2C 驱动最常见的坑是寄存器地址自增位和上拉电阻,遇到读出全 0xFF 或数据重复时优先检查这两项。最后,驱动代码的稳定性取决于错误处理的完整性——每次 I2C 传输都必须检查返回值,每次用户空间拷贝都必须用copy_to_user/copy_from_user,不要图省事用memcpy。