news 2026/1/11 16:50:55

手把手教你用树莓派4b编写第一个字符设备驱动

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手把手教你用树莓派4b编写第一个字符设备驱动

从零开始:在树莓派4B上编写你的第一个字符设备驱动

你有没有想过,当你在终端里敲下echo "hello" > /dev/something的时候,数据是怎么“钻进”内核的?
又或者,当一个LED灯被程序控制亮起时,背后究竟发生了什么?

今天,我们就用树莓派4B这个强大的嵌入式平台,亲手打造一个最基础但完整的Linux字符设备驱动。不玩虚的,不跳步骤——从环境搭建到模块加载,再到用户空间读写验证,全程实战。

这不是理论课,而是一次真实的“内核探险”。准备好了吗?我们出发。


为什么选择树莓派4B来学驱动开发?

很多人初学驱动时会被单片机(如STM32)带偏:寄存器操作、裸机编程、烧录工具链……还没看到成果就放弃了。

而树莓派4B不一样。它运行的是完整版Linux系统(通常是64位ARM架构),支持动态加载内核模块,有成熟的调试工具和庞大的社区资源。更重要的是:

  • 它足够接近真实服务器环境,却又能让你触碰到底层硬件;
  • 可以直接在板子上编译代码,无需复杂的交叉编译配置;
  • 内核源码与头文件齐全,dmesg/proc/sys全部可用,调试体验极佳。

简单说:它是学习Linux设备驱动的理想沙盒


字符设备的本质:不只是“字符”,而是“流”

先澄清一个误解:“字符设备”不是只能处理文本的意思。它的核心特点是:按字节流方式访问,无缓存、顺序读写

常见的串口(UART)、键盘、音频输入输出设备都是典型的字符设备。它们不像块设备那样以扇区为单位进行随机访问,而是像水流一样,一滴一滴地进出。

在Linux中,每个字符设备都通过/dev目录下的节点暴露给用户空间。比如/dev/ttyS0是串口,/dev/random是随机数生成器。

我们要做的,就是创建这样一个节点:/dev/hello_char,并让它能响应read()write()系统调用。


驱动长什么样?从框架到细节

下面这段代码,就是我们的“Hello World”级字符设备驱动。别急着全看懂,我们一步步拆解。

#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> #define DEVICE_NAME "hello_char" #define CLASS_NAME "hello_class" static int major_number; static struct class* hello_class = NULL; static struct device* hello_device = NULL; static int dev_open(struct inode *, struct file *); static ssize_t dev_read(struct file *, char __user *, size_t, loff_t *); static ssize_t dev_write(struct file *, const char __user *, size_t, loff_t *); static int dev_release(struct inode *, struct file *); static struct file_operations fops = { .owner = THIS_MODULE, .open = dev_open, .read = dev_read, .write = dev_write, .release = dev_release, }; static struct cdev hello_cdev; static int __init hello_init(void) { dev_t dev_num = 0; // 1. 动态申请主设备号 if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) return -1; major_number = MAJOR(dev_num); // 2. 初始化 cdev 并绑定操作函数 cdev_init(&hello_cdev, &fops); hello_cdev.owner = THIS_MODULE; if (cdev_add(&hello_cdev, dev_num, 1) == -1) { unregister_chrdev_region(MKDEV(major_number, 0), 1); return -1; } // 3. 创建设备类(用于自动创建 /dev 节点) hello_class = class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(hello_class)) { cdev_del(&hello_cdev); unregister_chrdev_region(MKDEV(major_number, 0), 1); return PTR_ERR(hello_class); } // 4. 在 /dev 下创建设备节点 hello_device = device_create(hello_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME); if (IS_ERR(hello_device)) { class_destroy(hello_class); cdev_del(&hello_cdev); unregister_chrdev_region(MKDEV(major_number, 0), 1); return PTR_ERR(hello_device); } printk(KERN_INFO "Hello Char Driver loaded: major=%d\n", major_number); return 0; } static void __exit hello_exit(void) { device_destroy(hello_class, MKDEV(major_number, 0)); class_unregister(hello_class); class_destroy(hello_class); cdev_del(&hello_cdev); unregister_chrdev_region(MKDEV(major_number, 0), 1); printk(KERN_INFO "Goodbye from Hello Char Driver\n"); }

关键结构解析

file_operations:驱动的“接口契约”
static struct file_operations fops = { .owner = THIS_MODULE, .open = dev_open, .read = dev_read, .write = dev_write, .release = dev_release, };

这是Linux驱动的核心结构体,定义了用户空间可以对设备执行哪些操作。你可以把它理解成一个“回调表”——当应用程序调用open("/dev/hello_char", ...)时,内核就会去执行你注册的dev_open函数。

注意:.owner = THIS_MODULE必须加上,否则可能导致模块卸载时出错。

cdev:内核中的字符设备抽象

struct cdev是内核用来管理字符设备的数据结构。我们需要:
1. 用cdev_init()把它和file_operations绑定;
2. 用cdev_add()注册到系统中。

一旦注册成功,内核就知道:“哦,主设备号X对应的设备,它的读写方法在这里。”

设备号管理:动态分配才是王道
alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME)

这行代码让内核帮我们找一个空闲的主设备号。比起硬编码(比如指定为240),这种方式更安全,避免与其他驱动冲突。

如果你好奇当前系统有哪些设备号已被占用,可以查看:

cat /proc/devices

编译它!Makefile怎么写?

别小看这个Makefile,它是连接应用层和内核的关键桥梁。

obj-m += hello_char.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean install: sudo insmod hello_char.ko uninstall: sudo rmmod hello_char reload: uninstall install

关键点说明:

  • obj-m += hello_char.o:表示要构建一个可加载模块(obj-y是静态编译进内核)。
  • -C $(KDIR):进入内核源码目录(实际是符号链接指向头文件和Makefile)。
  • M=$(PWD):告诉内核,“我的模块代码在这儿,请帮我编译”。

✅ 小贴士:确保已安装raspberrypi-kernel-headers包,否则/lib/modules/$(uname -r)/build会不存在!


上手实操:四步完成驱动部署

假设你已经登录树莓派4B,并把hello_char.cMakefile放在同一目录下。

第一步:安装依赖

sudo apt update sudo apt install build-essential raspberrypi-kernel-headers -y

第二步:编译模块

make

如果一切顺利,你会看到生成了几个文件,其中最重要的是:

  • hello_char.ko—— 这就是我们要加载的内核模块!

第三步:加载模块

sudo insmod hello_char.ko

此时还没有报错?太好了!接下来检查日志:

dmesg | tail -2

你应该能看到类似输出:

[ +0.000001] Hello Char Driver loaded: major=240

同时确认设备节点是否存在:

ls /dev/hello_char

如果没有这个文件,说明device_create失败了,回去查dmesg日志定位问题。

第四步:测试读写功能

写个简单的测试程序试试看:

// test_app.c #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { int fd = open("/dev/hello_char", O_RDWR); if (fd < 0) { perror("Failed to open device"); return -1; } char buf[64]; read(fd, buf, sizeof(buf)); printf("From kernel: %s", buf); write(fd, "Hi Kernel!", 10); close(fd); return 0; }

编译并运行:

gcc test_app.c -o test_app sudo ./test_app

再看看内核日志:

dmesg | tail

你应该会看到:

Device opened by PID 1234 Received from user: Hi Kernel! Device closed

恭喜!你刚刚完成了一次完整的用户空间 ↔ 内核空间数据交互!


常见坑点与调试秘籍

新手写驱动最容易栽在以下几个地方:

insmod: invalid module format

最常见的原因:内核版本不匹配

解决办法:

uname -r # 查看当前运行的内核版本 dpkg -l | grep headers # 确保安装了对应版本的 headers

如果系统刚升级过内核但没重启,也可能导致头文件路径不一致。


/dev/hello_char没有自动创建

检查dmesg输出是否有如下错误:

class_create: error -19

这通常是因为class_create调用失败。可能原因是模块名字重复或内存不足(极少见)。确保之前已正确卸载旧模块:

sudo rmmod hello_char

也可以手动触发udev重建:

sudo udevadm trigger

❌ 用户程序权限被拒绝

默认情况下,新设备节点只有root可读写。解决方法有两个:

  1. 临时用sudo测试;
  2. 永久方案:添加udev规则。

创建文件/etc/udev/rules.d/99-hello-char.rules

KERNEL=="hello_char", GROUP="dialout", MODE="0666"

然后重新加载规则:

sudo udevadm control --reload-rules sudo udevadm trigger

下次插入设备就会自动设置权限。


copy_to_user返回非零值

记住:所有来自用户空间的指针都不可信

必须使用copy_to_user(dest_in_user, src_in_kernel, len)copy_from_user(dest_in_kernel, src_in_user, len)来安全拷贝数据。

并且一定要检查返回值:

if (copy_to_user(buf, message, len)) return -EFAULT; // 拷贝失败,可能是非法地址

否则一旦传入野指针,轻则模块崩溃,重则引发内核oops。


后续拓展:从玩具到实用

你现在拥有的是一个“回声驱动”,但它只是起点。下一步你可以尝试:

🔧 控制GPIO灯

将树莓派上的一个GPIO引脚映射为输出,修改write函数实现:

write(fd, "on", 2); // 点亮LED write(fd, "off", 3); // 熄灭LED

需要用到gpio_request,gpio_direction_output,gpio_set_value等API。

📡 添加ioctl命令

除了读写,还可以通过ioctl实现更复杂的控制,例如设置超时、查询状态等。

只需在file_operations中添加.unlocked_ioctl成员,并在驱动中实现对应逻辑。

📈 结合sysfs暴露参数

利用kobjectdevice_create_file,可以在/sys/class/hello_class/hello_char/下创建属性文件,实现运行时配置调整。


写在最后:每一个驱动,都是人与机器的对话

你写的每一行驱动代码,其实都在建立一种沟通协议:
用户空间说:“我想读点东西。”
内核回应:“好,给你一段字符串。”
用户又说:“我要写入数据。”
内核记下:“收到,已打印日志。”

这种跨越层级的对话,正是操作系统最迷人的地方。

而你在树莓派4B上完成的这一次实践,虽然只是一个简单的字符设备,但它打通了从用户程序到内核模块的整条链路。这条路走下去,你能接触到中断处理、DMA传输、设备树解析、电源管理……最终成为一名真正的嵌入式系统开发者。

所以,别停下。现在就去改改代码,试着让它做点更有意思的事吧!

如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/8 17:24:32

3D球体抽奖系统实战指南:5步打造沉浸式年会体验

log-lottery是一个基于Vue3Three.js构建的3D动态抽奖应用&#xff0c;专为年会、庆典活动设计。它通过3D球体旋转动画模拟真实抽奖过程&#xff0c;结合古风主题设计&#xff0c;为用户提供前所未有的沉浸式体验。本文将带你从零开始&#xff0c;快速掌握这个项目的核心功能和使…

作者头像 李华
网站建设 2026/1/9 18:20:43

Windows虚拟显示器终极配置指南:从安装到高级应用全解析

Windows虚拟显示器终极配置指南&#xff1a;从安装到高级应用全解析 【免费下载链接】Virtual-Display-Driver Add virtual monitors to your windows 10/11 device! Works with VR, OBS, Sunshine, and/or any desktop sharing software. 项目地址: https://gitcode.com/gh_…

作者头像 李华
网站建设 2026/1/10 18:42:25

PingFang SC Regular:现代设计必备的中文字体资源

PingFang SC Regular&#xff1a;现代设计必备的中文字体资源 【免费下载链接】PingFangSCRegular字体资源下载 探索PingFang SC Regular字体的魅力&#xff0c;这是一套专为现代设计和开发需求打造的中文字体。本资源库提供了多种格式的字体文件&#xff0c;包括eot、otf、svg…

作者头像 李华
网站建设 2026/1/6 22:39:17

艾尔登法环存档编辑终极指南:3步解锁完美游戏体验

艾尔登法环存档编辑终极指南&#xff1a;3步解锁完美游戏体验 【免费下载链接】ER-Save-Editor Elden Ring Save Editor. Compatible with PC and Playstation saves. 项目地址: https://gitcode.com/GitHub_Trending/er/ER-Save-Editor 还在为错过关键剧情道具而懊恼&a…

作者头像 李华
网站建设 2026/1/11 11:47:11

从研究到生产:TensorFlow全流程支持实战指南

从研究到生产&#xff1a;TensorFlow全流程支持实战指南 在一家电商公司&#xff0c;算法团队刚刚完成了一个商品图像分类模型的训练。他们在本地用 PyTorch 快速迭代&#xff0c;准确率达到了98%。然而当把模型交给工程团队部署时&#xff0c;却遇到了问题&#xff1a;推理延…

作者头像 李华
网站建设 2026/1/9 2:24:02

基于springboot + vue宠物医院管理系统

宠物医院管理 目录 基于springboot vue宠物医院系统 一、前言 二、系统功能演示 详细视频演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue宠物医院系统 一、前言 博主介绍…

作者头像 李华