ARM《10》_01_字符设备驱动基础、学习开发字符驱动内核程序、总结规律和模板
0、前言:
- 这是一篇以问题为导向,的技术贴!
- 学习 字符设备驱动开发 的基础知识实现简单的基础案例;
基础概念库:
一、内核驱动相关知识概览:

二、linux内核驱动开发基础知识
- 【很重要】Linux 内核主要遵循 C89(ANSI C,1989 年制定的 C 标准)编程规范,对 C99 及以上标准的特性支持有限,若使用 C99 特有的语法,很可能会触发编译(make)错误。这是内核开发的一个重要约定,主要由历史原因和兼容性需求决定。
1、Linux 内核中驱动的分类:
- 块设备驱动:用于管理以“块”为单位读写数据的设备,数据块大小一般是512字节或更大,例如硬盘、u盘、sd卡,光盘等;支持随机访问,内核会提供缓存机制;如果涉及存储开发,需要学习;
- 网络设备驱动:用于管理实现网络通信的设备,不直接对应文件系统中的节点,而是通过网络协议栈工作,例如以太网网卡、蓝牙模块、4G/5G模组等;数据传输单位是“帧”,需要处理网络协议(TCP、IP)的封装和解封装,工作机制和其他两类差异比较大;从事嵌入式网络开发、网关设备或无线通信设备开发时,该类型驱动是核心技能。
- 字符设备驱动【适合入门】 :应用场景是串口、按键、LED、ADC、传感器等大量常用外设都属于字符设备,学习后能快速对接实际硬件;字符设备驱动涉及的内核 API(如设备号申请、file_operations 结构体、中断处理),是学习其他类型驱动的基础。
2、linux中字符设备驱动基础知识点:
-
linux系统中设备文件和普通文件的区别:
①、普通文件(Regular File)本质:存储实际数据的文件(文本、二进制、图片、程序等),数据以字节流形式保存在磁盘、分区等存储介质中。存在形式:真实占用磁盘空间,内容由用户或程序写入,删除后空间会被释放;
②、设备文件(Device File)本质:用户空间与硬件设备(或内核虚拟设备)交互的接口,不存储实际数据,而是通过文件操作(read/write等)映射到设备驱动的功能。 存在形式:通常位于 /dev 目录下,不占用磁盘空间(仅占用一个 inode 节点记录元信息),删除后重新创建设备节点即可恢复功能。 典型场景:/dev/sda(硬盘设备)、/dev/ttyS0(串口设备)、/dev/null(虚拟空设备)、/dev/zero(虚拟零设备)等。 -
在Linux系统中一切皆是文件,字符设备亦如此。 在/dev目录下可以看到很多创建好的设备节点,如下图所示:

-
字符设备基础概念:
1、字符设备:按字节流顺序读写的设备(如虚拟设备、串口等),对应文件系统中的一个设备节点(如/dev/mydevice)。
2、设备号:每个字符设备由 “主设备号(major)+ 次设备号(minor)” 唯一标识,主设备号标识驱动类型,次设备号标识具体设备。
3、file_operations 结构体:驱动与用户空间交互的 “桥梁”,里面定义了 read/write/open/close 等操作的函数指针。
3、字符设备核心结构体
- 1、struct cdev:字符设备的核心结构体,用于绑定设备操作函数并注册到内核,包含owner(所属模块)、ops(指向file_operations)等成员。
- 2、file_operations:驱动与用户空间交互的 “函数表”,核心成员包括:
- open/release:设备打开 / 关闭时的回调(如初始化资源、统计打开次数)。
- read/write:用户读写设备时的回调(需用copy_to_user/copy_from_user完成内核与用户空间的数据拷贝)。
- unlocked_ioctl:提供设备控制接口(如设置设备参数,需处理命令码)。
- owner:通常设为THIS_MODULE,防止模块在被使用时卸载。
4、字符设备的设备号管理
- 设备号的组成:
- 主设备号(major):标识驱动类型(如串口驱动共用一个主设备号),范围 0~255(动态分配可更大)。
- 次设备号(minor):标识同一驱动管理的多个设备(如/dev/ttyS0和/dev/ttyS1的次设备号为 0 和 1)。
- 设备号类型dev_t:通过MKDEV(major, minor)组合主 / 次设备号,MAJOR(dev_t)/MINOR(dev_t)拆分。
- 设备号的申请与释放
- 静态申请:register_chrdev_region(devno, count, name)(指定主设备号,需确保不冲突)。
- 动态分配:alloc_chrdev_region(&devno, first_minor, count, name)(推荐,内核自动分配主设备号)。
- 释放:unregister_chrdev_region(devno, count)(模块退出时必须调用,避免资源泄漏)。
5、设备注册与注销
- 字符设备的注册流程:申请设备号(register_chrdev_region或alloc_chrdev_region)。初始化cdev:cdev_init(&my_cdev, &fops)(绑定file_operations)。注册到内核:cdev_add(&my_cdev, devno, count)(count为设备数量)。
- 注销流程:从内核移除设备:cdev_del(&my_cdev)。释放设备号:unregister_chrdev_region(devno, count)(与注册时的count一致)。
6、用户空间交互
- 数据拷贝:内核空间与用户空间严格隔离,必须通过内核提供的函数拷贝数据:
- copy_to_user(dst, src, n):从内核空间(src)拷贝n字节到用户空间(dst),返回 0 表示成功。
- copy_from_user(dst, src, n):从用户空间(src)拷贝n字节到内核空间(dst),返回 0 表示成功。
禁止直接访问用户空间指针(可能因内存分页或权限导致崩溃)。
- 设备节点:用户空间通过/dev目录下的设备节点访问设备,需用mknod创建:
- sudo mknod /dev/xxx c major minor(c表示字符设备)。
- 自动创建设备节点:通过udev规则(用户空间)或内核class机制(class_create/device_create),避免手动执行mknod。
★7、开发流程:

三、linux系统分层
1、概念解释:
- 应用层(用户态):
Linux 中所有 ELF 可执行、动态库、解释器、容器进程都跑在用户空间,使用glibc/musl封装的 POSIX API,通过 syscall 指令陷入内核;它们只能看到自己的虚拟地址空间,任何对硬件或全局资源的访问都必须经过系统调用。 - 系统调用层(内核态):
Linux 内核源码里的 kernel/entry/ 与 include/linux/syscalls.h 提供的系统调用入口集合,共 400 余条(如 sys_read、sys_open),用户态执行 syscall 指令后 CPU 切换到 Ring 0,先到达该层做寄存器保存、参数检查、内核栈切换,再分发到对应的内核子系统;执行完毕把返回值写入 rax 并 sysret 回到用户态。 - 内核(内核态):
即 Linux 内核本身,包括进程调度器(CFS)、内存管理(slab、页表、反向映射)、虚拟文件系统(VFS)、网络协议栈(TCP/IP、Netfilter)、设备驱动、中断/异常处理、cgroups、seccomp 等,所有代码运行在内核空间,可直接访问物理地址和设备寄存器,系统调用层是其对外暴露的唯一受控接口。
2、类比说明:把 Linux 想象成一座办公大厦
- 应用层 → 普通员工在自己的工位(用户空间)干活,想打印、开空调、订会议室,只能填“服务申请单”(系统调用)。
- 系统调用层 → 前台接待(大厦唯一对外窗口),负责验单、登记、刷卡,把人或请求送进后厨机房,本身不碰打印机/空调。
- 内核层 → 物业维修队 + 机房 + 配电室,真正接电线、换墨盒、调温度,再把结果送回前台,前台再转给员工。
四、内核中常见错误码:
- 错误码的返回形式:驱动中需返回 “负的错误码”(如 -EINVAL),内核会自动转换为用户空间的 errno(正数)。

具体问题解决:
案例 1:极简虚拟字符设备(入门)
- 跑通 “模块加载→设备注册→用户访问” 的完整流程,建立驱动基础认知。加载驱动时自动申请设备号、注册 cdev;支持open/close操作,记录设备被打开的次数(通过dmesg查看);用户通过ls /dev/xxx确认设备节点,用cat/echo测试(虽无读写逻辑,但能验证设备存在)。
- 用内核内存数组模拟 “硬件存储区”,无需真实存储芯片。
- 检查是否已安装内核源码或内核头文件(用于编译模块),若存在则正常;否则安装。

- 有基本的编译工具(gcc、make);
first.c文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h> // file_operations结构体
#include <linux/cdev.h> // 字符设备结构体// 设备名称(自定义,用于标识设备)
#define DEV_NAME "myfirstdev"// 全局变量
static int major = 0; // 主设备号(0表示自动分配)
static int open_count = 0; // 记录设备打开次数
static struct cdev my_cdev; // 字符设备结构体// 1. 实现open函数(用户打开设备时调用)
static int mydev_open(struct inode *inode, struct file *filp) {open_count++; // 每次打开,计数+1// 内核打印(用KERN_INFO级别,dmesg可查看)printk(KERN_INFO "[myfirstdev] open: 第%d次打开设备\n", open_count);return 0; // 成功返回0
}// 2. 实现release函数(用户关闭设备时调用)
static int mydev_release(struct inode *inode, struct file *filp) {printk(KERN_INFO "[myfirstdev] release: 设备已关闭\n");return 0; // 成功返回0
}// 3. 定义文件操作集合(绑定open/release)
static struct file_operations mydev_fops = {.owner = THIS_MODULE, // 所属模块(防止模块被意外卸载).open = mydev_open, // 绑定open函数.release = mydev_release // 绑定release函数
};// 定义入口函数
static int __init my_cd1_init(void)
{ int ret;dev_t devno; // 设备号(主+次)// 步骤A:申请设备号(自动分配)// 参数:设备号指针、起始次设备号、设备数量、设备名称ret = alloc_chrdev_region(&devno, 0