1.注册字符设备驱动的新接口
1.1 register_chrdev_region
手动指定 主设备号 + 起始次设备号 + 数量,向内核申请占用一段设备号。
cpp
int register_chrdev_region(
dev_t first, // 起始设备号(MAJOR + MINOR)
unsigned int count, // 要申请的设备号数量
const char *name // 设备名称(显示在 /proc/devices)
);
用法:
cpp
// 主设备号=240,次设备号从0开始,申请2个设备号
dev_t dev = MKDEV(240, 0);
register_chrdev_region(dev, 2, "my_char_dev");
手动指定设备号(你自己选主设备号),成功返回 0,失败返回错误码
1.2 alloc_chrdev_region
alloc_chrdev_region:自动分配设备号,原型:
cpp
int alloc_chrdev_region(
dev_t *dev, // 输出:分配好的起始设备号
unsigned baseminor, // 起始次设备号(通常填 0)
unsigned count, // 申请设备号数量
const char *name // 设备名
);
成功返回 0,失败返回错误码,分配好的设备号存在 *dev 里。用法:
cpp
dev_t dev;
// 次设备号从0开始,分配2个设备号
alloc_chrdev_region(&dev, 0, 2, "my_char_dev");
// 可以打印看看内核分配的主设备号
printk("分配主设备号:%d\n", MAJOR(dev));
1.3 cdev
是个结构体。设备号只是一个「编号」,真正代表字符设备的是 cdev。四个关键函数:
cpp
// 1. 动态申请 struct cdev 结构体的内存空间
struct cdev *cdev_alloc(void);
// 2. 初始化 cdev,绑定 file_operations
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
// 3. 向内核添加 cdev,让设备生效
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);
// 4. 卸载时删除 cdev
void cdev_del(struct cdev *cdev);
1.4 dev_t
dev_t 是内核中保存设备号的数据类型,其实就是个无符号整数。
cpp
typedef unsigned int __kernel_dev_t;
typedef __kernel_dev_t dev_t;
这 32 位分成两部分:主设备号(Major) :前12 位。次设备号(Minor):后20 位
内核不允许直接操作整数,但是给你准备好了三个函数MKDEV MAJOR MINOR
cpp
//MKDEV:把主、次设备号 合成 dev_t
dev_t dev = MKDEV(major, minor);
//MAJOR:从 dev_t 里 取出主设备号
unsigned int major = MAJOR(dev);
//MINOR:从 dev_t 里 取出次设备号
unsigned int minor = MINOR(dev);
2.代码实现
cpp
//1. 申请设备号
dev_t dev_id;
struct cdev *my_cdev;
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init init\n");
/* 老旧的注册方式,已经不推荐使用了,建议使用cdev_add来注册字符设备
mymajor = register_chrdev(mymajor, DEV_NAME, &test_fops);
if (mymajor < 0) {
printk(KERN_ERR "Failed to register character device\n");
return -EINVAL;
}
printk(KERN_INFO "chrdev_init success...,my major=%d\n", mymajor);
*/
alloc_chrdev_region(&dev_id, 0, 1, DEV_NAME);
printk(KERN_INFO "chrdev_init_region success...,my major=%d,my minor=%d\n", MAJOR(dev_id), MINOR(dev_id));
//2. 初始化cdev结构体
my_cdev = cdev_alloc();
cdev_init(my_cdev, &test_fops);
//3. 将cdev结构体添加到内核中
cdev_add(my_cdev, dev_id, 1);
request_mem_region(PHYS_ADDR_GPJ0CON, 4, "test_chrdev");
request_mem_region(PHYS_ADDR_GPJ0DAT, 4, "test_chrdev");
vGPJ0CON = ioremap(PHYS_ADDR_GPJ0CON, 4);
vGPJ0DAT = ioremap(PHYS_ADDR_GPJ0DAT, 4);
*vGPJ0CON = 0x11111111; //清除原来的设置
*vGPJ0DAT = ((0<<3)|(0<<4)|(0<<5)); //默认输出0
return 0;
}
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
/* 老旧的注销方式,已经不推荐使用了,建议使用cdev_del来注销字符设备
unregister_chrdev(mymajor, DEV_NAME);
*/
*vGPJ0DAT = ((1<<3)|(1<<4)|(1<<5));
iounmap(vGPJ0CON);
iounmap(vGPJ0DAT);
release_mem_region(PHYS_ADDR_GPJ0CON, 4);
release_mem_region(PHYS_ADDR_GPJ0DAT, 4);
cdev_del(my_cdev);
unregister_chrdev_region(dev_id, 1);
}
完美实现。
3.倒影式编程
万一你申请完设备号之后驱动崩溃了怎么办,比如cdev初始化失败、cdev添加内核失败、驱动卸载失败。此时没有释放设备号,这个设备号就是丢了。
那怎么办?倒影式编程,驱动每一次醋错误验证,都有按次序编写处理程序。
cpp
//1. 申请设备号
dev_t dev_id;
struct cdev *my_cdev;
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init init\n");
int retval = alloc_chrdev_region(&dev_id, 0, 1, DEV_NAME);
if (retval) {
printk(KERN_ERR "Failed to allocate character device region\n");
goto flag1;
}
printk(KERN_INFO "chrdev_init_region success...,my major=%d,my minor=%d\n", MAJOR(dev_id), MINOR(dev_id));
//2. 初始化cdev结构体
my_cdev = cdev_alloc();
cdev_init(my_cdev, &test_fops);
//3. 将cdev结构体添加到内核中
retval = cdev_add(my_cdev, dev_id, 1);
if (retval) {
printk(KERN_ERR "Failed to add character device\n");
goto flag2;
}
if(!request_mem_region(PHYS_ADDR_GPJ0CON, 4, "test_chrdev")){
goto flag3;
}
if(!request_mem_region(PHYS_ADDR_GPJ0DAT, 4, "test_chrdev")){
goto flag4;
}
vGPJ0CON = ioremap(PHYS_ADDR_GPJ0CON, 4);
vGPJ0DAT = ioremap(PHYS_ADDR_GPJ0DAT, 4);
*vGPJ0CON = 0x11111111; //清除原来的设置
*vGPJ0DAT = ((0<<3)|(0<<4)|(0<<5)); //默认输出0
goto flag0;
flag4:
release_mem_region(PHYS_ADDR_GPJ0CON, 4);
flag3:
cdev_del(my_cdev);
flag2:
unregister_chrdev_region(dev_id, 1);
flag1:
return -EINVAL;
flag0:
return 0;
}
4.udev(mdev)
我不想自己用mknod自己创建设备文件怎么办,用udev。
udev就是个应用层程序。设备文件属于是应用层的东西,你不能在内核中创建它,这不合适,所以用应用层的软件创建设备文件。
实现原理:内核驱动与udev之间有一套信息传输机制(netlink协议)
=========================================================================
Netlink 是 Linux 特有的、基于 AF_NETLINK 套接字的内核 - 用户态双向通信机制,是现代 Linux 内核与用户空间交互的事实标准,用于替代传统 ioctl、procfs 等单工 / 低效方式。
通信模型:全双工、异步、支持多播;内核可主动推送事件(如网卡 up/down、USB 热插拔)。
API 兼容性:用户态用标准 socket API(socket/bind/sendmsg/recvmsg),内核态用专用 netlink API。
协议家族:通过 socket(AF_NETLINK, SOCK_RAW, protocol) 的第三个参数指定用途,如 NETLINK_ROUTE(路由 / 网卡)、NETLINK_KOBJECT_UEVENT(设备事件)、NETLINK_GENERIC(通用扩展)。
消息格式:固定头 struct nlmsghdr + 自描述属性 nlattr,支持灵活扩展。
=========================================================================
4.1自启动
/etc/init.d/rcS文件中有初始化代码
bash
echo /sbin/mdev > /proc/sys/kernel/hotplug
# 配置内核热插拔机制:告诉内核,当有硬件设备热插拔(如插入U盘、网卡)时,调用 /sbin/mdev 处理
# /proc/sys/kernel/hotplug 是内核的热插拔命令配置节点,写入 mdev 路径后
# 内核会自动触发mdev 管理设备
mdev -s
# 初始化 mdev 设备管理:
# mdev 是嵌入式系统中替代 udev 的轻量级设备管理工具
# -s 参数表示 "scan",扫描 /sys 目录下的所有设备信息
# 自动在 /dev 目录下创建对应的设备节点(如 /dev/ttyS0、/dev/sda1 等)
# 这一步是嵌入式系统识别硬件设备的关键,没有设备节点,应用程序无法访问硬件
但是驱动中需要使用专门的接口。
4.2 class_creat和device_creat
class_creat :创建设备类,成功返回 class 指针,失败返回错误指针。在 /sys/class/ 下创建一个设备分类,比如:/sys/class/net/(网卡类)。/sys/class/tty/(串口类)你自己创建的 /sys/class/my_class/。
owner:固定填 THIS_MODULE
name:类名(自定义)
cpp
struct class *class_create(
struct module *owner,
const char *name
);
**device_create:**创建设备节点,生成 /dev/xxx 设备文件,用户态程序直接操作这个文件就能和驱动通信。
class:class_create 创建的类
parent:父设备,没有填 NULL
devt:设备号(主设备号 + 次设备号)
fmt:设备名 → /dev/xxx
cpp
struct device *device_create(
struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...
);
配套销毁函数:
cpp
// 销毁设备
device_destroy(my_class, devt);
// 销毁类
class_destroy(my_class);
4.3 代码实现
cpp
//1. 申请设备号
static dev_t dev_id;
static struct cdev *my_cdev;
static struct class *test_class;
static int __init chrdev_init(void)
{
//接下来创建设备节点,udev会根据这个设备节点来创建对应的设备文件
test_class = class_create(THIS_MODULE, "test_chrdev_class");
if (IS_ERR(test_class)) {
printk(KERN_ERR "Failed to create class\n");
goto flag5;
}
if (IS_ERR(device_create(test_class, NULL, dev_id, NULL, "test"))) {
printk(KERN_ERR "Failed to create device\n");
goto flag6;
}
printk(KERN_INFO "Device node created successfully\n");
}
static void __exit chrdev_exit(void)
{
//删除设备节点和类
device_destroy(test_class, dev_id);
class_destroy(test_class);
}
5.动态映射结构体方式操控寄存器
单独的寄存器地址映射很简单,但当寄存器数量太大时,管理和控制变得过于复杂。面对地址连续的外设寄存器,我们一般使用结构体的方式来控制。
cpp
typedef struct GPJ0_REGS {
unsigned int GPJ0CON;
unsigned int GPJ0DAT;
} GPJ0_REGS;
GPJ0_REGS *vGPJ0Regs;
static int __init chrdev_init(void)
{
if(!request_mem_region(PHYS_ADDR_GPJ0CON, 8, "test_chrdev")){
return -EINVAL;
}
vGPJ0Regs = ioremap(PHYS_ADDR_GPJ0CON, 8);
vGPJ0Regs->GPJ0CON = 0x11111111; //清除原来的设置
vGPJ0Regs->GPJ0DAT = ((0<<3)|(0<<4)|(0<<5)); //默认输出0
}
static void __exit chrdev_exit(void)
{
vGPJ0Regs->GPJ0DAT = ((1<<3)|(1<<4)|(1<<5));
iounmap(vGPJ0Regs);
release_mem_region(PHYS_ADDR_GPJ0CON, 8);
}
ok完全成功,寄存器多了之后比较方便。
6.内核提供的读写寄存器的函数
我们之前采用的方式是使用解引用指向io地址的指针的方式与外设互动,还是太原始了,用指针不安全,不同平台之间不好移植。哈弗架构中io与内存统一编址,冯诺依曼架构中不统一编址。
内核封装的函数如下:

6.1 writel和readl
b\w\l表示操作的单位。
-
外层:({ ... }) ------ 语句表达式(GCC 扩展)。这不是标准 C,是 GCC 编译器扩展,叫语句表达式。作用:允许在宏里写多条语句。最后一行表达式的值,会作为整个宏的返回值。简单说:
({ A; B; C; }) 执行 A、B,最终返回 C 的值。 -
u32 __v = readl_relaxed(c);u32 :32 位无符号整数(unsigned int)。__v :临时变量,内核常用 __ 表示内部临时变量。readl_relaxed(c) :宽松版读寄存器。从地址 c 读取一个 32 位值。没有内存屏障,不保证顺序。这一步只是把硬件寄存器的值读进来,但不保证时序安全。
-
__iormb(); ------ 最重要:内存屏障(读屏障)。__iormb():Input/Output Read Memory Barrier
读内存屏障。保证:屏障之前的读操作,一定在屏障之后的操作之前完成。作用:防止 CPU 乱序执行,确保硬件寄存器读取完成后,再执行后面的代码。
- __v;作为语句表达式的返回值。整个宏最终返回:从地址 c 读取的 32 位值
cpp
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; })
-
__iowmb() = I/O Write Memory Barrier。写屏障,必须放在写操作之前!作用:保证在执行本次写寄存器之前,所有前面的写操作都已经完成。
-
第二行:writel_relaxed(v,c); ------ 真正写寄存器
v:要写入的 32 位数据 value
c:硬件寄存器 地址 address
writel_relaxed:宽松版写入,只负责把值丢到硬件地址,没有内存屏障
cpp
#define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })
6.2 iowrite32和ioread32
与writel、readl没有本质区别

6.3代码实现:
cpp
typedef struct GPJ0_REGS {
unsigned int GPJ0CON;
unsigned int GPJ0DAT;
} GPJ0_REGS;
GPJ0_REGS *vGPJ0Regs;
//1. 申请设备号
static dev_t dev_id;
static struct cdev *my_cdev;
static struct class *test_class;
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init init\n");
int retval = alloc_chrdev_region(&dev_id, 0, 1, DEV_NAME);
if (retval) {
printk(KERN_ERR "Failed to allocate character device region\n");
return -EINVAL;
}
printk(KERN_INFO "chrdev_init_region success...,my major=%d,my minor=%d\n", MAJOR(dev_id), MINOR(dev_id));
//2. 初始化cdev结构体
my_cdev = cdev_alloc();
cdev_init(my_cdev, &test_fops);
//3. 将cdev结构体添加到内核中
retval = cdev_add(my_cdev, dev_id, 1);
if (retval) {
printk(KERN_ERR "Failed to add character device\n");
return -EINVAL;
}
if(!request_mem_region(PHYS_ADDR_GPJ0CON, 8, "test_chrdev")){
return -EINVAL;
}
vGPJ0Regs = ioremap(PHYS_ADDR_GPJ0CON, 8);
writel(0x11111111,vGPJ0Regs);
writel(((0<<3)|(0<<4)|(0<<5)),&vGPJ0Regs->GPJ0DAT);
//vGPJ0Regs->GPJ0CON = 0x11111111; //清除原来的设置
//vGPJ0Regs->GPJ0DAT = ((0<<3)|(0<<4)|(0<<5)); //默认输出0
//接下来创建设备节点,udev会根据这个设备节点来创建对应的设备文件
test_class = class_create(THIS_MODULE, "test_chrdev_class");
if (IS_ERR(test_class)) {
printk(KERN_ERR "Failed to create class\n");
return -EINVAL;
}
if (IS_ERR(device_create(test_class, NULL, dev_id, NULL, "test"))) {
printk(KERN_ERR "Failed to create device\n");
return -EINVAL;
}
printk(KERN_INFO "Device node created successfully\n");
return 0;
}