嵌入式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驱动和中断管理专题。

浙公网安备 33010602011771号