嵌入式 Linux 驱动开发:从设备树到字符设备的全链路调试

一、驱动开发最怕的不是写代码,是调不出错
一块新的传感器板子接到 i.MX8 上,I2C 通信不上。设备树配了,驱动注册了,i2cdetect 能看到设备地址,但读出来的数据全是 0xFF。是设备树配置错了?是 I2C 时序不对?是传感器没初始化?还是硬件上拉电阻选错了?排查了两天,最后发现是设备树里 reg 属性写成了 16 进制但驱动按 10 进制解析。
嵌入式 Linux 驱动开发的特点是:代码量不大,但调试链路极长。从设备树配置到内核驱动匹配,从总线通信到寄存器读写,从用户空间系统调用到内核空间中断处理,任何一个环节出错都会导致功能异常,而错误现象往往指向错误的方向。本文从设备树到字符设备,完整梳理驱动开发的全链路,重点放在调试方法而非代码模板。
二、驱动加载的全链路机制
2.1 从设备树到驱动匹配
设备树(Device Tree)是硬件描述的标准格式。内核启动时解析设备树,为每个节点创建 platform_device。驱动注册时声明 compatible 字符串,内核通过 compatible 匹配 device 和 driver。
2.2 匹配失败的常见原因
匹配失败是最常见的问题,原因通常有三:
第一,compatible 字符串不一致。设备树写 "vendor,sensor123",驱动写 "vendor,sensor-123",一个连字符的差异导致匹配失败。
第二,设备树节点没有对应的总线。I2C 设备必须挂在 I2C 控制器节点下,如果直接放在根节点,内核不会为它创建 i2c_client。
第三,内核配置未启用对应驱动。驱动代码存在,但 CONFIG_xxx 未设为 y 或 m,编译时被排除。
三、字符设备驱动的完整实现
3.1 I2C 传感器驱动框架
c
#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位数据(小端序) */
data->x = (int16_t)(buf[1] << 8 | buf[0]);
data->y = (int16_t)(buf[3] << 8 | buf[2]);
data->z = (int16_t)(buf[5] << 8 | buf[4]);
return 0;
}
/* 字符设备操作:open */
static int sensor_open(struct inode *inode, struct file *filp)
{
struct my_sensor_dev *dev;
/* 从inode获取私有数据 */
dev = container_of(inode->i_cdev, struct my_sensor_dev, cdev);
filp->private_data = dev;
return 0;
}
/* 字符设备操作:read */
static ssize_t sensor_read(
struct file *filp,
char __user *buf,
size_t count,
loff_t *f_pos
) {
struct my_sensor_dev *dev = filp->private_data;
struct sensor_data data;
int ret;
if (count < sizeof(data)) {
return -EINVAL;
}
mutex_lock(&dev->lock);
ret = sensor_read_data(dev, &data);
mutex_unlock(&dev->lock);
if (ret < 0) {
return ret;
}
/* 拷贝到用户空间 */
if (copy_to_user(buf, &data, sizeof(data))) {
return -EFAULT;
}
return sizeof(data);
}
/* 字符设备操作:ioctl */
static long sensor_ioctl(
struct file *filp,
unsigned int cmd,
unsigned long arg
) {
struct my_sensor_dev *dev = filp->private_data;
int ret = 0;
mutex_lock(&dev->lock);
switch (cmd) {
case 0x1001: /* 重新初始化 */
ret = sensor_init(dev);
break;
default:
ret = -ENOTTY;
break;
}
mutex_unlock(&dev->lock);
return ret;
}
/* 文件操作集合 */
static const struct file_operations sensor_fops = {
.owner = THIS_MODULE,
.open = sensor_open,
.read = sensor_read,
.unlocked_ioctl = sensor_ioctl,
};
/* 设备树匹配表 */
static const struct of_device_id my_sensor_of_match[] = {
{ .compatible = "vendor,my-sensor" },
{ /* 哨兵 */ }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);
/* I2C设备ID表(非设备树场景) */
static const struct i2c_device_id my_sensor_id[] = {
{ "my_sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_sensor_id);
/* probe函数:驱动匹配成功后调用 */
static int my_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_sensor_dev *dev;
int ret;
dev_info(&client->dev, "probe开始, addr=0x%02x\n", client->addr);
/* 分配私有数据 */
dev = devm_kzalloc(&client->dev, sizeof(*dev), GFP_KERNEL);
if (!dev) {
return -ENOMEM;
}
dev->client = client;
mutex_init(&dev->lock);
i2c_set_clientdata(client, dev);
/* 初始化传感器 */
ret = sensor_init(dev);
if (ret < 0) {
dev_err(&client->dev, "传感器初始化失败: %d\n", ret);
return ret;
}
/* 动态分配设备号 */
ret = alloc_chrdev_region(&dev->devt, 0, 1, DEVICE_NAME);
if (ret < 0) {
dev_err(&client->dev, "分配设备号失败\n");
return ret;
}
/* 初始化字符设备 */
cdev_init(&dev->cdev, &sensor_fops);
dev->cdev.owner = THIS_MODULE;
ret = cdev_add(&dev->cdev, dev->devt, 1);
if (ret < 0) {
dev_err(&client->dev, "添加字符设备失败\n");
goto err_cdev;
}
/* 创建设备类和设备节点 */
dev->class = class_create(DEVICE_NAME);
if (IS_ERR(dev->class)) {
ret = PTR_ERR(dev->class);
goto err_class;
}
dev->device = device_create(dev->class, &client->dev,
dev->devt, dev, DEVICE_NAME);
if (IS_ERR(dev->device)) {
ret = PTR_ERR(dev->device);
goto err_device;
}
dev_info(&client->dev, "驱动加载成功, major=%d, minor=%d\n",
MAJOR(dev->devt), MINOR(dev->devt));
return 0;
err_device:
class_destroy(dev->class);
err_class:
cdev_del(&dev->cdev);
err_cdev:
unregister_chrdev_region(dev->devt, 1);
return ret;
}
/* remove函数:驱动卸载时调用 */
static void my_sensor_remove(struct i2c_client *client)
{
struct my_sensor_dev *dev = i2c_get_clientdata(client);
device_destroy(dev->class, dev->devt);
class_destroy(dev->class);
cdev_del(&dev->cdev);
unregister_chrdev_region(dev->devt, 1);
dev_info(&client->dev, "驱动卸载完成\n");
}
/* I2C驱动结构体 */
static struct i2c_driver my_sensor_driver = {
.driver = {
.name = DRIVER_NAME,
.of_match_table = my_sensor_of_match,
},
.probe = my_sensor_probe,
.remove = my_sensor_remove,
.id_table = my_sensor_id,
};
module_i2c_driver(my_sensor_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Developer");
MODULE_DESCRIPTION("My Sensor I2C Driver");
3.2 设备树配置
dts
/* 设备树节点:传感器挂在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 调试命令速查
bash
# 检查设备树节点是否被内核解析
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。