《嵌入式操作系统》_高级字符设备驱动_20260316

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表示操作的单位。

  1. 外层:({ ... }) ------ 语句表达式(GCC 扩展)。这不是标准 C,是 GCC 编译器扩展,叫语句表达式。作用:允许在宏里写多条语句。最后一行表达式的值,会作为整个宏的返回值。简单说:
    ({ A; B; C; }) 执行 A、B,最终返回 C 的值。

  2. u32 __v = readl_relaxed(c);u32 :32 位无符号整数(unsigned int)。__v :临时变量,内核常用 __ 表示内部临时变量。readl_relaxed(c) :宽松版读寄存器。从地址 c 读取一个 32 位值。没有内存屏障,不保证顺序。这一步只是把硬件寄存器的值读进来,但不保证时序安全

  3. __iormb(); ------ 最重要:内存屏障(读屏障)。__iormb():Input/Output Read Memory Barrier

读内存屏障。保证:屏障之前的读操作,一定在屏障之后的操作之前完成。作用:防止 CPU 乱序执行,确保硬件寄存器读取完成后,再执行后面的代码。

  1. __v;作为语句表达式的返回值。整个宏最终返回:从地址 c 读取的 32 位值
cpp 复制代码
#define readl(c)		({ u32 __v = readl_relaxed(c); __iormb(); __v; })
  1. __iowmb() = I/O Write Memory Barrier。写屏障,必须放在写操作之前!作用:保证在执行本次写寄存器之前,所有前面的写操作都已经完成。

  2. 第二行: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;
}
相关推荐
顶妙WMS海外仓管理系统2 小时前
Shopify卖家破910万,海外仓如何对接Shopify独立站?
运维·产品运营
优美的赫蒂2 小时前
香橙派5plus单独编译内核安装时的报错记录
linux·rk3588·orangepi
·醉挽清风·2 小时前
学习笔记—Linux—文件系统
linux·笔记·学习
IMPYLH3 小时前
Linux 的 chmod 命令
linux·运维·服务器
迷茫青年3 小时前
带你进入linux的世界,linux基础知识讲解
linux
北京智和信通3 小时前
面向超融合的全域监控与一体化运维方案
运维·网管软件·超融合监控·超融合运维
艾莉丝努力练剑3 小时前
【MYSQL】MYSQL学习的一大重点:数据库基础
linux·运维·服务器·数据库·c++·学习·mysql
会喷火才能叫火山3 小时前
本地搭建AI相关步骤
linux·运维·ai·centos
齐齐大魔王3 小时前
虚拟机网络无法连接
linux·网络·c++·python·ubuntu