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

嵌入式 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:#fff

2.2 匹配失败的常见原因

匹配失败是最常见的问题,原因通常有三:

第一,compatible 字符串不一致。设备树写 "vendor,sensor123",驱动写 "vendor,sensor-123",一个连字符的差异导致匹配失败。

第二,设备树节点没有对应的总线。I2C 设备必须挂在 I2C 控制器节点下,如果直接放在根节点,内核不会为它创建 i2c_client。

第三,内核配置未启用对应驱动。驱动代码存在,但 CONFIG_xxx 未设为 ym,编译时被排除。

三、字符设备驱动的完整实现

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   = &reg;

    /* 第二条消息:读取数据 */
    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 = <&reg_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