嵌入式Linux设备树与字符设备驱动开发实战:从原理到调通

嵌入式Linux设备树与字符设备驱动开发实战:从原理到调通

嵌入式Linux开发的入门门槛在"设备树"和"字符设备驱动"这两座山。很多人能跟着教程把LED灯点亮,但换一个开发板就完全不会改了。问题出在不理解设备树和驱动模型的底层逻辑,只是在抄代码。

这篇文章从设备树的基本概念讲起,到字符设备驱动的完整框架代码,再到编译加载和调试。不是念PPT,是把我实际在STM32MP157和全志H3上调试驱动的经验记录下来。

一、设备树:硬件描述的语言

1.1 为什么需要设备树

Linux内核需要知道硬件上接了什么设备、用什么引脚、什么时钟、什么中断。以前这些信息硬编码在内核源码的arch目录下,换一个板子就要改内核源码,维护成本极高。

设备树(DTS)把硬件描述从内核代码中抽离出来。内核代码只定义"怎么操作设备",设备树告诉内核"有哪些设备、在哪、参数是什么"。这样同一份内核代码可以适配不同硬件,只需要换一份DTS文件。

1.2 DTS基本语法

/ {
    // 根节点
    model = "My IoT Board";
    compatible = "myvendor,iot-board";

    // 选择了别名,方便引用
    aliases {
        serial0 = &uart1;
        i2c0 = &i2c1;
    };

    // LED节点
    leds {
        compatible = "gpio-leds";
        status = "okay";

        led0 {
            label = "system-led";
            gpios = <&gpioa 5 GPIO_ACTIVE_LOW>;
            default-state = "off";
        };
    };

    // 自定义设备节点
    my_sensor: sensor@40 {
        compatible = "myvendor,my-sensor";
        reg = <0x40>;
        interrupt-parent = <&gpiof>;
        interrupts = <3 IRQ_TYPE_EDGE_FALLING>;
        clock-frequency = <400000>;
        status = "okay";
    };
};

1.3 关键属性解析

属性 作用 示例
compatible 匹配驱动 “myvendor,my-sensor”
reg 设备地址 <0x40>
interrupts 中断配置 ❤️ IRQ_TYPE_EDGE_FALLING>
status 是否启用 “okay” 或 “disabled”
gpios GPIO引用 <&gpioa 5 GPIO_ACTIVE_LOW>

compatible属性是设备树和驱动匹配的关键。内核驱动注册时会声明自己支持的compatible字符串,设备树中的compatible与之匹配后,驱动的probe函数就会被调用。

二、字符设备驱动框架

2.1 驱动的生命周期

一个字符设备驱动从加载到卸载,经历以下步骤:

模块加载 → 注册主设备号 → 创建设备节点 → 初始化硬件 → 等待用户态调用。

2.2 完整驱动代码

以下是一个传感器字符设备驱动的完整框架,支持open/read/write/release操作:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/of_device.h>
#include <linux/i2c.h>

#define DEVICE_NAME "my_sensor"
#define BUF_SIZE 64

struct sensor_dev {
    struct cdev cdev;
    struct i2c_client *client;
    char rx_buf[BUF_SIZE];
    int data_len;
};

static int sensor_open(struct inode *inode, struct file *file)
{
    struct sensor_dev *dev = container_of(inode->i_cdev,
                                         struct sensor_dev, cdev);
    file->private_data = dev;
    return 0;
}

static ssize_t sensor_read(struct file *file, char __user *buf,
                           size_t count, loff_t *offset)
{
    struct sensor_dev *dev = file->private_data;
    int bytes_to_copy;

    if (*offset >= dev->data_len)
        return 0;

    bytes_to_copy = min(count, (size_t)(dev->data_len - *offset));

    if (copy_to_user(buf, dev->rx_buf + *offset, bytes_to_copy))
        return -EFAULT;

    *offset += bytes_to_copy;
    return bytes_to_copy;
}

static ssize_t sensor_write(struct file *file, const char __user *buf,
                           size_t count, loff_t *offset)
{
    struct sensor_dev *dev = file->private_data;

    if (count > BUF_SIZE)
        count = BUF_SIZE;

    if (copy_from_user(dev->rx_buf, buf, count))
        return -EFAULT;

    dev->data_len = count;

    // 发送数据到I2C设备
    i2c_master_send(dev->client, dev->rx_buf, count);
    return count;
}

static int sensor_release(struct inode *inode, struct file *file)
{
    return 0;
}

static const struct file_operations sensor_fops = {
    .owner   = THIS_MODULE,
    .open    = sensor_open,
    .read    = sensor_read,
    .write   = sensor_write,
    .release = sensor_release,
};

// I2C驱动的probe函数
static int sensor_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct sensor_dev *dev;
    dev_t devno;
    int ret;

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

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

    // 分配设备号
    ret = alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
    if (ret < 0)
        return ret;

    // 初始化cdev
    cdev_init(&dev->cdev, &sensor_fops);
    dev->cdev.owner = THIS_MODULE;
    ret = cdev_add(&dev->cdev, devno, 1);
    if (ret)
        goto err_cdev;

    // 创建设备节点
    dev->class = class_create(THIS_MODULE, DEVICE_NAME);
    device_create(dev->class, &client->dev, devno, NULL, DEVICE_NAME);

    pr_info("sensor driver loaded, major=%d\n", MAJOR(devno));
    return 0;

err_cdev:
    unregister_chrdev_region(devno, 1);
    return ret;
}

static int sensor_remove(struct i2c_client *client)
{
    struct sensor_dev *dev = i2c_get_clientdata(client);

    device_destroy(dev->class, dev->cdev.dev);
    class_destroy(dev->class);
    cdev_del(&dev->cdev);
    unregister_chrdev_region(dev->cdev.dev, 1);
    return 0;
}

// 设备树匹配表
static const struct of_device_id sensor_of_match[] = {
    { .compatible = "myvendor,my-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);

static struct i2c_driver sensor_driver = {
    .driver = {
        .name = "my_sensor",
        .of_match_table = sensor_of_match,
    },
    .probe = sensor_probe,
    .remove = sensor_remove,
};

module_i2c_driver(sensor_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("huwang");
MODULE_DESCRIPTION("Sensor character device driver");

2.3 代码要点解析

container_of宏 :从inode->i_cdev指针反推到sensor_dev结构体。这是Linux内核最常用的"从成员指针找父结构体"技巧。

copy_to_user/copy_from_user :用户空间和内核空间内存隔离,不能直接memcpy,必须用这对函数做安全拷贝。

of_match_table :这是设备树匹配的核心。驱动声明"我支持myvendor,my-sensor",设备树里有相同compatible的节点时,probe函数被调用。

三、编译与加载

3.1 Makefile

obj-m += my_sensor.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	make -C $(KDIR) M=$(PWD) modules
clean:
	make -C $(KDIR) M=$(PWD) clean

3.2 加载和验证

# 编译
make

# 加载模块
sudo insmod my_sensor.ko

# 检查设备节点
ls -l /dev/my_sensor
# crw------- 1 root root 243, 0 Sep 17 10:00 /dev/my_sensor

# 测试读写
echo "test_data" > /dev/my_sensor
cat /dev/my_sensor

# 查看内核日志
dmesg | tail -5

3.3 验证I2C通信

用i2c-tools确认设备是否在总线上可见:

# 扫描I2C总线
i2cdetect -y 1
#      0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
# 00:                         -- -- -- -- -- -- -- --
# ...
# 40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

0x40地址出现说明硬件连接正常。如果设备不可见,检查设备树中I2C控制器的status是否为okay、上拉电阻是否到位。

四、调试技巧

4.1 devicetree overlay调试

修改设备树后不需要重启内核。使用设备树覆盖(DTS overlay)可以在运行时加载硬件描述:

# 编译overlay
dtc -O dtb -o sensor_overlay.dtbo -@ sensor_overlay.dts

# 加载overlay
mkdir -p /sys/kernel/config/device-tree/overlays/sensor
cat sensor_overlay.dtbo > /sys/kernel/config/device-tree/overlays/sensor/dtbo

4.2 串口调试

调试嵌入式Linux驱动时,串口日志是第一手信息。但不同芯片的串口调试体验差异很大。做中兴微、ASR、展锐芯片的随身WiFi产品时,串口AT指令集各不相同,调试效率被这个环节严重拖累。

虎王科技开源的hardware_tool(gitee.com/zesso/hardware_tool)就是为解决这个问题设计的。它是一个基于PHP的Web串口调试平台,支持多芯片AT指令模板、数据实时回显和固件升级操作。在调试嵌入式设备时,不用在终端窗口间来回切换,在浏览器里统一管理串口通信,效率提升明显。

4.3 ftrace动态跟踪

ftrace可以在不修改内核代码的情况下跟踪函数调用:

# 启用函数跟踪
echo function > /sys/kernel/debug/tracing/current_tracer
echo sensor_probe > /sys/kernel/debug/tracing/set_ftrace_filter
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 触发probe后查看trace
cat /sys/kernel/debug/tracing/trace

五、常见问题排查

probe不执行 :99%的原因是compatible不匹配。用of_node命令检查设备树中的compatible和驱动中的of_match_table是否完全一致,包括大小写和下划线。

i2c_transfer返回-ENXIO :设备地址不对或者硬件没连接好。先用i2cdetect扫描确认。

设备节点不出现 :检查class_create和device_create是否返回错误。内核日志中通常会有报错信息。

copy_to_user崩溃 :内核空间的数据没有正确初始化,或者buf指针来自mmap映射的异常地址。确保数据来源可靠。


设备树和字符设备驱动是嵌入式Linux的两道门槛,但一旦理解了"设备树描述硬件、驱动实现操作、compatible做匹配"这条主线,换什么板子都能上手。上面的代码和调试方法都是从实际项目里提炼出来的。写完这篇发现篇幅不小,如果对你有帮助就收藏一下方便翻阅,踩坑记录我会持续补充,关注我不错过后面的platform驱动和中断管理专题。

posted @ 2026-09-26 00:01  虎王科技  阅读(1)  评论(0)    收藏  举报