18
0
0

Linux 纯内存字符设备驱动开发

2026-08-25
2026-08-25
文章摘要
|

目标:在内存中开辟一块 1KB 的空间,用户态程序可以通过 echo "hello" > /dev/virtual_dev 写入数据,通过 cat /dev/virtual_dev 读出数据。


一、前言

Linux系统为了管理方便,将设备分成三种基本类型:

  • 字符设备

  • 块设备

  • 网络设备

1.字符设备:

字符(char)设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种特性。

字符设备驱动程序通常至少要实现open、close、read和write的系统调用。

字符设备可以通过文件节点来访问,比如/dev/tty1和/dev/lp0等。这些设备文件和普通文件之间的唯一差别在于对普通文件的访问可以前后移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道。然而,也存在具有数据区特性的字符设备,访问它们时可前后移动访问位置。例如framebuffer就是这样的一个设备,app可以用mmap或lseek访问抓取的整个图像。

字符设备只能一个字节一个字节的读写设备,读取数据需要按照前后顺序进行,几乎绝大多数的设备都是字符设备

二、字符设备架构是如何实现的?

在Linux的世界里面一切皆文件,所有的硬件设备操作到应用层都会被抽象成文件的操作,那么本质上字符设备驱动也是一个文件,在介绍本次项目中需要用到的几种驱动架构之前,先要聊一聊所有驱动架构都要有的东西。

1. 唯一的身份标识:设备号(Device Number)

在Linux设备中,所有设备都有自己的名字和一个代表该设备的一个数字编号,计算机通过该数字序号来识别是哪个设备,这个数字序号叫做:设备号

备注:输入命令“cat /proc/devices”可以查看当前已经被使用掉的设备号

设备号有主设备号次设备号之分,主设备号用来表示一个特定的驱动,次设备号用来管理下面的设备。在注册字符设备驱动之前需要先申请设备号。

了解一个代码最重要的手段就是看他的API和结构体,接下来先让我们来了解设备号的数据结构

(1)设备号的数据结构:dev_t

设备号类型为 dev_t ,dev_t 类型定义在内核源码/include/linux/types.h”,他本质上是一个uint32_t类型,它通过掩码操作来控制主次设备号,其中高 12 位表示主设备号,低 20 位表示次设备号。

#define MINORBITS 20 /*次设备号位数*/
#define MINORMASK ((1U << MINORBITS) - 1) /*次设备号掩码*/
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))/*dev 右移 20 位得到主设备号*/
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) /*与次设备掩码与,得到次设备号*/
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))/*MKDEV 宏将主设备号(ma)左移 20 位,然后与
次设备号(mi)相与,得到设备号*/

(2)设备号分配与释放 API(定义在 <linux/fs.h> 中)

1. 静态分配设备号
  • APIint register_chrdev_region(dev_t first, unsigned int count, char *name);

  • 参数

    • first:要分配的起始设备号(通常由 MKDEV() 生成,次设备号常设为 0)。

    • count:请求分配的连续设备号总数。

    • name:关联该编号范围的驱动或设备名称。

2.动态分配设备号
  • APIint alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, char *name);

  • 参数

    • dev:输出参数,成功分配后,申请到的起始设备号会保存在此指针指向的变量中。

    • baseminor:请求的第一个次设备号(通常为 0)。

    • count:请求分配的设备号数量。

    • name:关联该编号范围的驱动或设备名称。

3. 释放设备号
  • APIvoid unregister_chrdev_region(dev_t first, unsigned count);

  • 参数

    • first:要释放的起始设备号。

    • count:要释放的设备号数量(需与申请时的数量保持一致)。

2. 标准的生命周期管理:模块初始化与退出

字符设备驱动通常以内核模块(.ko)的形式存在,其生命周期由内核严格管理。开发者必须通过特定的宏向内核注册入口和出口函数,以完成资源的分配与释放。

static int __init helloworld_init(void) //驱动入口函数
{
  printk(KERN_EMERG "helloworld_init\r\n");//注意:内核打印用 printk 而不是 printf
  return 0;
}
static void __exit helloworld_exit(void) //驱动出口函数
{
  printk(KERN_EMERG "helloworld_exit\r\n");
}
module_init(helloworld_init); //注册入口函数
module_exit(helloworld_exit); //注册出口函数

__init 把函数放入 .init.text section,模块加载完成后该 section 的内存会被释放;__exit 标记的函数在静态编译进内核时会被丢弃。

3. 内核日志与声明许可证(License)

特殊的内核Debug调试函数

由于驱动运行在内核态,无法使用标准 C 库的 printf 函数,必须依赖内核提供的日志系统。

下面是常用的debug调试API:

 printk(KERN_EMERG "helloworld_exit\r\n");

我们可以看到文中出现了一个陌生的字段,KERN_EMERG ,这个说明的是内核日志的级别等级:

这些宏定义通常位于 <linux/kern_levels.h> 头文件中,用于指定 printk 输出信息的紧急程度。

/* ASCII 控制字符:标题开始 (Start Of Header),作为日志级别的前缀标识 */
#define KERN_SOH        "\001"
#define KERN_SOH_ASCII  '\001'
/* 0级 - 系统不可用 (System is unusable) */
#define KERN_EMERG      KERN_SOH "0"
/* 1级 - 必须立即采取行动 (Action must be taken immediately) */
#define KERN_ALERT      KERN_SOH "1"
/* 2级 - 严重情况 (Critical conditions) */
#define KERN_CRIT       KERN_SOH "2"
/* 3级 - 错误情况 (Error conditions) */
#define KERN_ERR        KERN_SOH "3"
/* 4级 - 警告情况 (Warning conditions) */
#define KERN_WARNING    KERN_SOH "4"
/* 5级 - 正常但重要的情况 (Normal but significant condition) */
#define KERN_NOTICE     KERN_SOH "5"
/* 6级 - 一般信息 (Informational) */
#define KERN_INFO       KERN_SOH "6"
/* 7级 - 调试级别信息 (Debug-level messages) */
#define KERN_DEBUG      KERN_SOH "7"
/* 默认内核日志级别 */
#define KERN_DEFAULT    ""

声明许可证是 Linux 内核强制要求的硬性规范

  • 辅助元数据:通常还会配套使用 MODULE_AUTHOR()MODULE_DESCRIPTION()MODULE_VERSION() 等宏,以完善模块的描述信息,便于后续维护。

许可证说明:

GPL/GPL v2(GNU 通用公共许可证):一种强制性开源许可协议,要求任何基于 GPL 的软件修改或衍生作品也必须以 GPL 许可协议发布。这保障了开源软件的自由和开放性;

BSD(伯克利软件分发协议):一种宽松的开源软件许可协议,允许开发者自由地使用、修改和分发开源软件,也允许将开源代码用于闭源商业项目,不要求派生软件使用相同协议。

MIT(麻省理工学院许可证):一种宽松的开源软件许可协议,允许开发者自由地使用、修改和分发开源代码,也允许将开源代码用于闭源商业项目,不要求派生软件使用相同协议。

MPL(Mozilla 公共许可证):一种混合许可协议,它允许开源和闭源软件的组合,但要求对 MPL 部分的源代码的修改必须开源。

从 linux-5.11 版本开始没有 MODULE_LICENSE 模块无法编译通过

MODULE_LICENSE("GPL v2"); //同意 GPL 开源协议
MODULE_AUTHOR("topeet"); //作者信息

4. 用户态与内核态的安全交互边界

你能看到这个文章,说明你是对Linux的基本架构是有所了解的,因此不过多介绍用户态与内核态

在 Linux 内核驱动开发中,用户态与内核态的安全交互是核心原则之一。驱动程序运行在内核空间,拥有最高权限,而应用程序运行在用户空间。为了防止用户空间的非法操作导致内核崩溃(Kernel Panic)或安全漏洞,两者之间的数据交换必须遵循严格的边界规则。

关键 API:安全数据拷贝

copy_from_user
  • 功能:将数据从用户空间缓冲区安全地复制到内核空间缓冲区。

  • 原型unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);

  • 参数说明

    • to:内核空间的目标缓冲区地址。

    • from:用户空间的源缓冲区地址(必须带有 __user 标记)。

    • n:需要复制的字节数。

  • 返回值返回未能复制的字节数。如果返回值为 0,表示复制完全成功;如果返回值大于 0,表示复制过程中发生了错误(如用户指针无效),此时驱动应返回 -EFAULT 给用户态。

copy_to_user
  • 功能:将数据从内核空间缓冲区安全地复制到用户空间缓冲区。

  • 原型unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

  • 参数说明

    • to:用户空间的目标缓冲区地址(必须带有 __user 标记)。

    • from:内核空间的源缓冲区地址。

    • n:需要复制的字节数。

  • 返回值:同上,返回未能复制的字节数。0 表示成功。

到此为止,我们介绍完了基本内容,接下来我们来讨论字符设备的通用框架!


二、字符设备驱动的常用框架

1. 标准 cdev 框架

(1)cdev的数据结构

struct cdev {
	struct kobject kobj; //Linux 内核设备模型实现的核心
	struct module *owner; //module 是 Linux 内核用于描述一个模块,通常使用“THIS_MODULE”初始化
	const struct file_operations *ops; //字符设备驱动为用户提供的操作接口,驱动中必须实现
	struct list_head list; //链表
	dev_t dev; //表示设备号
	unsigned int count;//次设备对应的连续次设备号的数量
} __randomize_layout;

从上文的cdev中可以看到,我们必须实现的有操作接口 file_operations ,描述模块owner!


file_operations 结构体

我们可以看到,这个结构体封装了很多的函数指针,我们必须要实现的是open与release函数,和一个结构体owner,其他的可以根据需求来选择性实现,在C99标准中,我们引入了复合字面量方法,这也让这种赋值变的简单,在此处我们一定要严格遵守函数指针要求的结构!

  • owner,是指向当前模块的指针,通常使用 THIS_MODULE 初始化

  • llseek,当 VFS 需要修改文件当前读写位置的时候会被调用

  • read,应用程序调用 read 或相关系统调用时被调用,完成设备的读操作

  • write,应用程序调用 read 或相关系统调用时被调用,完成设备的写操作

  • poll,应用程序调用 select 或 poll 时被调用,用来检查设备的状态判断其是否可被读写

  • unlocked_ioctl,当应用程序调用 ioctl 时被调用,用来实现一些控制操作,使用较灵活

  • compat_ioctl,在 64 位系统中运行 32 位应用程序时,应用程序调用 ioctl 时被调用

  • mmap,应用程序调用 mmap 时被调用,将内核空间内存映射到用户空间,用于提高设备的工作效率。

  • open,应用程序调用 open 时被调用,完成设备的打开及一些初始化工作

  • release,应用程序调用 close 时被调用,完成设备的关闭工作

  • fasync,应用程序中当文件启用异步模式时,通过 fcntl 设置 FASYNC 标志是被调用

最后我们在这两个结构体的最后可以看到一个__randomize_layout字段。

在高版本内核中很多结构体定义的最后都加了这个字段,这个字段意思是是结构体随机化,其目的是增加内核安全性。编译器在编译时会将结构体成员的顺序打乱,从而使攻击者无法通过结构体的偏移进行攻击,基于这个原因开启了结构体随机化的结构体在进行赋值或初始化是就需要指定成员名称赋值,否则会出现不可预知的错误。


到此为止,必要的参数我们已经说完了,但是还有几个参数没有说到,这里我们来详细的说一下

struct kobject kobj;这里指的是一个设备模型基本框架,主要的作用算是一个万能基类,通过它,将原本散乱的硬件设备、驱动程序、总线等,用面向对象的思想统一组织了起来。 该内容将会在后续章节中讲到!

struct list_head list;这里指的是内核链表管理节点,典型的侵入式双向链表节点,它主要用于内核内部对字符设备的组织和管理。

unsigned int count; 次设备号范围与设备数量,这个成员记录了该字符设备驱动所管理的设备数量或占用的次设备号范围。

(2) cdev 核心操作 API

cdev核心API

这是字符设备驱动的核心,负责将设备号与具体的操作函数绑定并注册到内核。

初始化 cdevvoid cdev_init(struct cdev *cdev, const struct file_operations *fops);

添加 cdev 到内核int cdev_add(struct cdev *p, dev_t dev, unsigned count);

从内核移除 cdevvoid cdev_del(struct cdev *p);

自动创建设备节点 API

为了让用户态程序能够方便地通过 /dev/ 目录访问设备,通常需要配合 udevmdev 自动创建设备节点,而不是手动执行 mknod 命令。

  • 创建设备类struct class *class_create(struct module *owner, const char *name);

    • /sys/class/ 下创建一个设备类目录。

  • 销毁设备类void class_destroy(struct class *cls);

    • 卸载驱动时销毁对应的设备类。

  • 创建设备节点struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);

    • 在指定的类下创建设备,并触发 udev/mdev 自动在 /dev/ 目录下生成对应的设备文件。

  • 销毁设备节点void device_destroy(struct class *class, dev_t devt);

    • 卸载驱动时删除自动生成的设备节点。

2. misc 杂项设备框架(标准cdev框架的封装)

(1) 前言

杂项设备属于特殊的一种字符型设备,是对字符设备的一种封装,本质也是字符设备

在 Linux 中,把无法归类的五花八门的设备定义成杂项设备。相较于字符设备,杂项设备有以下两个优点:

  • 节省主设备号:杂项设备的主设备号固定为 10,当系统中注册了多个杂项设备驱动时,只需使用子设备号进行区分。

  • 使用简单:杂项设备驱动本身包含创建设备节点操作,不需要额外使用 class_create()函数和 device_create()函数实现自动创建设备节点操作。只需要填充驱动中的 file_operations 结构体中的成员即可。

(2) misc的数据结构

一般只需要填充 miscdevice 结构体中 minor、name、fops 这三个成员变量。

int minor;(次设备号)

  • 作用:用于区分同一主设备号下的不同设备实例。杂项设备共享固定的主设备号 10,通过不同的次设备号来标识具体设备。

  • 用法:通常推荐将其设置为 MISC_DYNAMIC_MINOR(宏定义值为 255),让内核在注册时自动分配一个空闲的次设备号,以避免冲突。

const char *name;(设备名称)

  • 作用:设备的标识符。当设备成功注册后,内核会自动在 /dev/ 目录下生成一个与该名称同名的设备节点文件(例如 /dev/my_misc_dev)。

const struct file_operations *fops;(文件操作集合)

struct list_head list;(内核链表节点)

struct device *parent;(父设备指针)

  • 作用:如果该杂项设备是某个父设备的子设备,此字段指向其父设备结构体。通常用于设备树中表示设备的层次结构,一般设为 NULL

struct device *this_device;(当前设备指针)不作讨论

const struct attribute_group **groups;(属性组指针)

  • 作用:指向属性组结构体指针数组。用于在 sysfs 虚拟文件系统(如 /sys/class/misc/)中暴露和管理该设备的属性,方便用户空间通过文件系统接口查看或修改设备状态。

const char *nodename;(兼容旧版节点名)

  • 作用:设备的节点名称,通常用于设备树的设备节点标识。

umode_t mode;(文件权限模式)

  • 作用:定义该杂项设备在 /dev/ 目录下生成的设备文件的访问权限(例如设置为 0666 表示允许所有用户读写)。

(3) misc的核心API

注册杂项设备

  • APIint misc_register(struct miscdevice *misc);

  • 功能:向内核注册一个杂项设备。该函数会自动完成次设备号的分配(如果指定了 MISC_DYNAMIC_MINOR)、设备节点的创建(在 /dev/ 下生成文件)以及将设备挂载到内核的杂项设备链表中。

  • 参数说明

    • misc:指向开发者定义并初始化好的 struct miscdevice 结构体指针。

  • 返回值:成功返回 0;失败返回负的错误码。

注销杂项设备

  • APIint misc_deregister(struct miscdevice *misc);

  • 功能:从内核中注销一个杂项设备。该函数会自动释放次设备号、删除 /dev/ 下的设备节点,并将设备从内核链表中移除。

  • 参数说明

    • misc:指向要注销的 struct miscdevice 结构体指针。

  • 返回值:成功返回 0;失败返回负的错误码。

3. Platform 平台设备框架

(1) 前言

platform 总线与 I2C 总线、USB 总线不同,他是 Linux 内核中虚拟出来的总线,在嵌入式 Linux 系统中,适用于 platform bus 的设备驱动通常是一些与硬件平台紧密相关的设备,如 SOC 内部的设备或通过特定总线连接的外部设备。

device 代表硬件设备,driver 代表控制硬件设备的软件,bus 则是管理它们的总线结构,将它们组织在一起,并提供一些标准的访问方式。

下图为该架构的示意图

让我们分别来看这三个核心组成部分的数据结构:

(2) platform_device 数据结构

  • const char *name;:设备名称,用于与 platform_driver 进行匹配。内核通过比较设备名与驱动名来决定是否绑定。

  • int id;:设备实例 ID。当系统中存在多个同名设备时(如多个 UART),用于区分不同实例。通常设为 -1 表示自动分配。

  • bool id_auto;:标识 id 是否由内核自动分配。若为 true,则 id 字段无效,内核会自动生成唯一 ID。

  • struct device dev;:内嵌的通用设备结构体,是所有设备模型的基类,包含设备的父设备、驱动指针、电源管理信息等。

  • u64 platform_dma_mask;:指定该平台设备的 DMA 地址掩码,用于限制 DMA 传输的地址范围,确保 DMA 操作在硬件支持的地址空间内进行。

  • u32 num_resources;:设备所拥有的资源数量,如内存区域、中断号等。

  • struct resource *resource;:指向资源数组的指针,每个 resource 结构体描述一个硬件资源(如内存基地址、中断号、DMA 通道等)。这是驱动获取硬件信息的主要来源。

  • const struct platform_device_id *id_entry;:指向匹配成功的 platform_device_id 条目,用于支持更复杂的匹配逻辑(如通配符匹配)。

  • char *driver_override;:允许用户空间或内核代码强制指定一个驱动名称,绕过默认的匹配机制,常用于调试或动态绑定驱动。

  • struct mfd_cell *mfd_cell;:指向 MFD(多功能设备)单元的描述符。当一个物理设备包含多个功能模块时,MFD 框架会为每个功能创建一个 platform_device,此字段用于关联回原始的 MFD 单元。

  • struct pdev_archdata archdata;:架构特定的数据扩展字段,用于存放特定于 CPU 架构的附加信息,如平台特定的配置数据。

综合来看,结构体的内容不是我们都需要的,一个是struct device dev;另一个是const char *name;另一个是struct resource *resource;

resource 结构体(include/linux/ioport.h):

source insight又抽风了跳转不进去,只能去ubuntu里看了

在 Linux 内核中,struct resource 结构体用于描述设备所需的资源,如内存区域、中断等。它的主要成员包括:

  • start: 表示资源的起始地址。对于内存区域,start 表示地址的物理或虚拟起始位置。

    • 对于中断资源,start 表示中断号。对于 DMA 资源,start 表示 DMA 通道。

  • end: 表示资源的结束地址。对于 I/O 端口和内存区域,end 表示地址的物理或虚拟结束位置。

    • 对于中断资源和 DMA 资源,end 通常与 start 相等。

  • name: 一个字符串,表示资源的名称或标识符。

  • flags: 表示资源的属性和特性,可以包括以下标志位:

    • IORESOURCE_MEM: 表示该资源是内存区域。

    • IORESOURCE_IRQ: 表示该资源是中断。

    • IORESOURCE_DMA: 表示该资源是 DMA 通道。

  • 以上的很多字段需要自己去ioport.h去查找

这个结构体实际上是很关键的,虽然Linux的上层有很多的API能够控制它,但是在本次工程中,这个将会用来定义一片连续的内存空间。

计算公式:如果你有一个大小为 size 的内存块,起始地址是 start,那么 end 必须等于 start + size - 1

一个注意事项:在 Platform Device 中,几乎永远是物理地址Platform Device 的定义通常发生在驱动加载之前(或者设备树解析阶段)。此时,该外设的内存还没有被映射到内核的虚拟地址空间。

内核里其实有 resource_size(res) 这个宏可以直接算出大小,不需要手动算。

(2) platform_driver 数据结构:


停停停,到这里如果你没有看到其他资料,你应该已经很晕了。怎么又 Device 又 Driver 的?其实,这就是隔离的思想

设备的描述信息与具体的驱动被彻底分开,这样可以让更多的驱动和设备相互兼容。在 STM32 中,我们使用 HAL 库直接做开发,被迫要使用回调函数等思想把驱动内容拉回用户操作层,但这本质上依然是强耦合的。后来,我们借鉴了 C++ 的多态思想,采用函数指针实现了应用程序与驱动层的隔离。

但到了 Linux 中,事情变得更加复杂。另一个角度的 HAL 库(即底层硬件)也在一直变化,这导致没有一个固定下来的东西。Linux 的前辈们采用了这样的思想:他们既让设备可以动态加载,也让驱动可以动态加载。简单来说,无论我底板内的资源怎么变化,总会有不变的;无论底层怎么变化,我的驱动总有不变的,我总会找到一套可以复用的代码。 这也是这套体系主要的思想。

但是仍有不足。一方面,你在上述的学习中就可以体会到,这种强解耦的理念会导致代码的复杂度增高,对开发者的要求也提升了很多!

从上述的讨论中,我们会理解到:驱动一般不会变,或者说可以穷尽;但是底板(开发板)的资源是可以随意改变的。那么我们该如何优雅地把“变”的部分从代码中剥离出去呢?

为了解决这个痛点,Linux 社区引入了一个革命性的概念——设备树(Device Tree)。设备树本质上是一种纯文本的数据结构(.dts 文件)。它把原本写在 C 语言里的硬件描述全部抽离出来,用一种类似 JSON 的树状文本格式表达。这样一来,不变的驱动代码被编译进了内核,而“变化”的硬件描述被编译成了独立的二进制文件(.dtb)。当底层硬件发生变化时,我们只需要修改设备树文本,重新编译出新的 .dtb 文件即可,驱动代码一行都不用动!这,才是 Linux 前辈们实现极致隔离的最终答案。

设备树本身是一个极其庞大且独立成体系的知识点,它甚至值得用整整一章的篇幅去详细拆解。但在本节,我们暂不陷入那些繁琐的语法细节中。在这里提及设备树,只是为了帮大家建立起一个完整的上帝视角——让你在未来面对复杂的驱动代码时,清楚地知道它们究竟从何而来,又将去往何处。


我们回到对于结构体的解析,如果你学过STM32等单片机开发中的多态思想,那么你很快就会理解,这是在封装出来一层驱动层

  • driver:指向 struct device_driver 结构体的指针,用于与驱动程序关联的设备驱动信息。

    • driver.name: 驱动名称

  • id_table:指向一个数组,描述了与驱动程序匹配的平台设备的标识符信息。通过匹配设备的标识符,决定是否加载该驱动程序。

  • driver.of_match_table:指向一个数组,描述了与驱动程序匹配的设备树节点信息。通过匹配设备树节点,决定是否加载该驱动程序。

  • probe:指向一个函数,当与该驱动程序匹配的平台设备被注册时调用。该函数负责初始化和配置与平台设备相关的资源,并将其与驱动程序进行关联。当有多个平台设备与驱动匹配时,probe 函数会被执行多次。比如设备有四个串口,就会定义四个 platform_device,驱动会与四个 platform_device 分别匹配,probe会执行四次,分别对每一个 platform_device 代表的串口进行初始化。

  • remove:指向一个函数,当与该驱动程序匹配的平台设备被注销时调用。该函数负责清理和释放与平台设备相关的资源,并断开与驱动程序的关联。

  • shutdown:指向一个函数,当系统关机或重启时调用。该函数用于处理与平台设备相关的关闭操作,确保设备在系统关机或重启前得到正确的关闭。

  • suspend:指向一个函数,当系统进入睡眠状态时调用。该函数负责暂停与平台设备相关的操作,以便系统进入睡眠状态。

  • resume:指向一个函数,当系统从睡眠状态唤醒时调用。该函数负责恢复与平台设备相关的操作,以便系统从睡眠状态恢复正常工作。

(3) 关键API:

1. 平台设备的注册与注销 API

这一组 API 负责将硬件描述信息(即 platform_device)挂载到虚拟的 platform 总线上,等待驱动来匹配。

  • 注册设备int platform_device_register(struct platform_device *pdev);

    • 功能:向内核注册一个平台设备。内核会将其加入总线设备列表,并触发总线匹配机制。

    • 返回值:成功返回 0,失败返回负错误码。

  • 注销设备void platform_device_unregister(struct platform_device *pdev);

    • 功能:从内核中移除该设备,断开其与驱动的绑定,并释放相关资源。

2. 平台驱动的注册与注销 API

这一组 API 负责将驱动逻辑(即 platform_driver)注册到内核,使其具备被匹配和触发的能力。

  • 注册驱动int platform_driver_register(struct platform_driver *drv);

    • 功能:向内核注册一个平台驱动。内核会将其加入总线驱动列表,并主动扫描已注册的设备进行匹配。

    • 便捷宏:在实际开发中,常使用 module_platform_driver(driver_name); 宏来替代手动的 module_initmodule_exit,一行代码即可完成驱动的自动注册与注销。

  • 注销驱动void platform_driver_unregister(struct platform_driver *drv);

    • 功能:从内核中移除该驱动,解除所有匹配设备的绑定。

3. 核心资源获取 API

当设备与驱动匹配成功后,内核会调用驱动的 probe 函数。在 probe 函数中,驱动需要“认领”设备树或板级文件中定义的硬件资源。

  • 获取资源描述符struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);

    • 功能:从 platform_device 的资源数组中,按类型和索引获取资源。

    • 参数type 指定资源类型(如 IORESOURCE_MEM 获取内存,IORESOURCE_IRQ 获取中断);num 指定同类型资源的索引(从 0 开始)。

  • 获取中断号int platform_get_irq(struct platform_device *dev, unsigned int num);

    • 功能:专门用于获取中断号,内部封装了 platform_get_resource,并自动处理了设备树中中断控制器的映射(翻译),比直接获取资源更方便。

4. 内存映射 API(配合 resource 使用)

通过 platform_get_resource 获取的 startend物理地址,CPU 无法直接访问,必须映射为虚拟地址。

  • 申请并映射内存void __iomem *devm_ioremap_resource(struct device *dev, struct resource *res);

    • 功能这是推荐使用的“二合一”函数。它内部自动完成了 request_mem_region(申请资源独占权)和 ioremap(物理地址转虚拟地址)两步操作。

在这里,我们看到了一个新出现的段关键字__iomem,这里我们要讲__iomem__user 的对比说明,在上文,__user出现在copy_from_user的函数中

  • __iomem:指向I/O内存空间(MMIO),必须用 readl/writel 访问

  • __user:指向用户空间内存,必须用 copy_from_user/copy_to_user 访问

到此为止,我们已经讲解完了字符设备驱动的实现架构,但是仍有很多的地方并没有讲解到,因为他们不属于基本字符设备驱动的实现,我将这部分内容相隔离开,接下来我们来追求一些细枝末节的相关内容。


支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

Linux 纯内存字符设备驱动开发
/archives/linux-chun-nei-cun-zi-fu-she-bei-qu-dong-kai-fa
作者
Sharkey
发布于
2026-08-25
许可协议
CC BY-NC-SA 4.0

评论