视频教程请关注 B 站:"嵌入式Jerry"
一、背景引入:字符设备 + 实际硬件控制
在 Linux 驱动开发中,字符设备(Character Device)是一类通过 read
/ write
接口与用户空间进行数据交互的基本设备类型。今天我们将结合 i.MX8MP EVK 开发板的 gpio-leds
节点,构造一个简单、直观的字符设备驱动,实现对物理 LED 的开关控制。
这一过程不仅涉及字符设备驱动的基础流程,还能帮助我们理解设备树与实际硬件的结合方式。
二、设备树分析:找到目标 GPIO
开发板设备树 arch/arm64/boot/dts/freescale/imx8mp-evk.dts
中,预定义了一个 GPIO 控制的 LED 节点:
dts
gpio-leds {
compatible = "gpio-leds";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpio_led>;
status {
label = "yellow:status";
gpios = <&gpio3 16 GPIO_ACTIVE_HIGH>;
default-state = "on";
};
};
这段节点中定义了一个黄色 LED,使用 gpio3_16
进行控制。GPIO 的编号计算方式为:
GPIO 编号 = bank × 32 + offset = 3 × 32 + 16 = 112
我们将在驱动中直接控制该 GPIO(编号 112)进行点亮和熄灭。
三、字符设备驱动实现
我们不再依赖 gpio-leds
子系统,而是直接注册一个字符设备,通过 echo 1 > /dev/ledchar
控制物理 LED 点亮,echo 0
熄灭。
3.1 驱动代码
c
// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define GPIO_LED_NUM 112 // gpio3_16 -> 3*32 + 16
static dev_t devt;
static struct cdev cdev;
static struct class *led_class;
static ssize_t led_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
char kbuf[4];
if (copy_from_user(kbuf, buf, len)) // 从用户空间拷贝数据到内核
return -EFAULT;
gpio_set_value(GPIO_LED_NUM, kbuf[0] == '1' ? 1 : 0); // 设置 GPIO 电平
return len;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.write = led_write, // 用户空间 write 映射此函数
};
static int __init led_dev_init(void)
{
gpio_request(GPIO_LED_NUM, "led_gpio"); // 请求 GPIO 控制权
gpio_direction_output(GPIO_LED_NUM, 1); // 设置为输出方向,初始高电平
alloc_chrdev_region(&devt, 0, 1, "led_char"); // 分配设备号
cdev_init(&cdev, &led_fops); // 初始化字符设备结构
cdev_add(&cdev, devt, 1); // 注册字符设备
led_class = class_create(THIS_MODULE, "led_class"); // 创建 sysfs 类
device_create(led_class, NULL, devt, NULL, "ledchar"); // 创建设备节点 /dev/ledchar
pr_info("ledchar device init done\n");
return 0;
}
static void __exit led_dev_exit(void)
{
gpio_set_value(GPIO_LED_NUM, 0); // 卸载前熄灭 LED
gpio_free(GPIO_LED_NUM); // 释放 GPIO
device_destroy(led_class, devt);
class_destroy(led_class);
cdev_del(&cdev);
unregister_chrdev_region(devt, 1);
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("嵌入式Jerry");
3.2 编译说明
使用 Makefile 编译为模块:
makefile
obj-m += ledchar.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
加载模块:
bash
insmod ledchar.ko
控制 LED:
bash
echo 1 > /dev/ledchar # 点亮 LED
echo 0 > /dev/ledchar # 熄灭 LED
四、字符设备和设备树的关联解读
❗ 重点:我们没有直接解析设备树节点 ,而是"硬编码"了 GPIO 号
112
。
这是一种"直接控制硬件资源"的方式,适用于教学或特定场景,但不具备移植性和灵活性。在生产环境中,我们推荐:
- 使用
of_find_node_by_name
等接口动态获取设备树中的gpio
; - 或者用 platform_driver 的形式自动注册字符设备,配合
of_device_id
匹配设备树。
五、字符设备与设备模型的区别与融合
项目 | 字符设备 | 设备模型 |
---|---|---|
本质 | 文件操作接口 | 系统框架结构 |
接口表现 | file_operations (read/write 等) | device、driver、bus 等结构体 |
功能侧重 | 面向用户空间的数据操作 | 面向系统资源管理与驱动匹配 |
是否依赖 | 可独立存在 | 统一纳入 driver model 框架更佳 |
字符设备是数据访问的"通道",而设备模型是驱动统一管理的"系统总线"。两者并不冲突,在现代驱动开发中,常常将字符设备纳入设备模型管理之中。
六、实战拓展建议
-
下一步建议学习内容:
- 如何通过
platform_driver
注册字符设备; - 如何用
device_create_file()
添加 sysfs 接口; - 将 GPIO 控制"封装成子系统"接口(如 LED 子系统);
- 如何通过
-
推荐实战目标:
- 为一个温度传感器或输入按键编写字符驱动;
- 理解
udev
如何自动创建设备节点; - 进一步结合设备树进行设备节点注册。
七、总结
本篇训练以字符设备驱动为核心,结合 i.MX8MP EVK 实际 GPIO,完成从驱动编写、设备注册、到用户空间控制的完整流程。关键点如下:
- 使用
alloc_chrdev_region
/cdev_add
完成字符设备注册; - 通过
gpio_request
/gpio_set_value
操作 GPIO; - 构建了
/dev/ledchar
控制节点,实现最基础的内核设备控制通道。
接下来我们将深入 platform_driver 与 sysfs 接口,逐步构建更完整的驱动结构。
📢 视频教程请关注 B 站:"嵌入式Jerry"
📚 本系列每日更新,持续训练,打造 Linux 驱动开发硬核能力!