嵌入式学习——字符设备驱动的注册和调用流程

一、设备驱动的三大类型

字符设备按字节流(串口),块设备按数据块(硬盘),网络设备按数据包(网卡)。

1. 字符设备驱动

定义:以字节流为单位进行数据传输的设备驱动,数据按顺序读写,不支持随机访问。

特点:数据传输量小、读写频率高、操作简单、直接。典型应用包括键盘、鼠标、串口、触摸屏、LED、蜂鸣器等。

文件操作接口:open、read、write、release、ioctl、llseek。

设备标识:通过主设备号和次设备号唯一标识,设备文件存放在/dev目录下。

2. 块设备驱动

定义:以数据块为单位进行数据传输的设备驱动,数据按块(通常为512字节、4KB等)进行读写,支持随机访问。

特点:数据传输量大、读写效率高、支持缓冲区缓存、支持调度算法。典型应用包括硬盘、SSD、SD卡、eMMC、U盘、NAND Flash等。

文件操作接口:open、release、ioctl,以及块设备特有的请求处理函数。

调度算法:支持CFQ、Deadline、NOOP等多种I/O调度算法,优化读写性能。

3. 网络设备驱动

定义:用于网络通信的设备驱动,基于套接字进行数据收发,不遵循Linux文件系统的"一切皆文件"原则。

特点:数据以数据包形式传输、不直接对应/dev目录下的设备文件、使用网络接口名(如eth0、wlan0)标识。典型应用包括以太网卡、WiFi模块、蓝牙模块、4G/5G模块等。

操作接口:通过socket API和网络协议栈进行通信,使用ifconfig、ip等命令配置网络接口。

数据结构:核心数据结构为net_device结构体,包含网络接口的属性和操作函数指针。

二、从用户态到内核态

用户程序调用open(led)后,CPU从用户态切换到内核态,内核的VFS根据设备文件路径找到对应的设备节点,提取出主设备号100。内核通过这个设备号在设备驱动表中查找,找到已注册的led驱动。然后内核调用该驱动的open函数,最终操作硬件层的LED设备。执行成功后,内核返回文件描述符给用户程序。

触发入口

用户在应用程序中调用open("/dev/led", O_RDWR),C库函数触发系统调用指令(如SWI或SVC),CPU从用户态切换到内核态。

内核匹配驱动

内核的VFS根据路径找到设备文件,从inode中提取主设备号和次设备号。主设备号用于在设备驱动表中定位对应的驱动程序,次设备号用于区分同一驱动下的不同硬件实例。

驱动执行操作

内核通过主设备号找到已注册的驱动,调用驱动的open函数。该函数执行具体硬件操作,如检查设备状态、初始化硬件、寄存器映射等。

返回文件描述符

驱动执行成功后,内核分配一个文件描述符(非负整数)返回给用户程序。后续的read、write、ioctl、close等操作都通过该文件描述符定位对应的驱动函数。

三、设备驱动程序必须提供的四个要素

实现file_operations结构体,分配设备号,向内核注册驱动,创建设备节点。

1、实现操作方法

需要定义一个struct file_operations结构体,至少实现open、read、write、unlocked_ioctl、release(即close)等回调函数。具体步骤是先定义结构体函数指针,将对应的操作函数与驱动实现关联,然后在代码中逐个实现这些函数。在初始阶段,函数内部可以只做打印操作验证调用。

示例结构:

  • .owner = THIS_MODULE
  • .open = open
  • .read = read
  • .write = write
  • .release = close

2、分配/注册设备号

设备号用于内核识别和匹配驱动程序。可以采用两种方式:静态申请,使用已知空闲的主设备号;或者动态分配,由内核自动分配可用的设备号。

3、向内核注册驱动

设备节点位于/dev目录下,是用户空间程序访问驱动程序的重要入口。

有两种创建方式:手动创建,使用mknod命令生成设备节点;自动创建,在驱动中使用device_create函数由内核自动生成设备节点。

4、创建设备节点

设备节点位于/dev目录下,是用户空间程序访问驱动程序的重要入口。

有两种创建方式:手动创建,使用mknod命令生成设备节点;自动创建,在驱动中使用device_create函数由内核自动生成设备节点。

四、LED字符设备驱动示例

1、地址映射(ioremap)

Linux启用MMU后,CPU访问虚拟地址,寄存器物理地址不能直接访问。

作用:将物理地址映射到内核虚拟地址空间

使用:ioremap(物理地址, 大小)映射,iounmap()释放

复制代码
// 物理地址映射为虚拟地址
static void __iomem *GPIO1_DR;
GPIO1_DR = ioremap(0x0209C000, 4);

// 卸载时释放
iounmap(GPIO1_DR);

2、用户态数据拷贝

驱动不能直接访问用户空间内存。

copy_from_user:用户数据→内核空间

copy_to_user:内核数据→用户空间

复制代码
char kbuf[32] = {0};
// 用户空间 → 内核空间
copy_from_user(kbuf, buf, len);

// 内核空间 → 用户空间  
copy_to_user(buf, kbuf, len);

3、指令解析

用strcmp比较用户指令,匹配后调用硬件操作函数

复制代码
if (!strcmp(kbuf, "on")) {
    led_on();
} else if (!strcmp(kbuf, "off")) {
    led_off();
}

4、file_operations结构体

定义驱动支持的操作函数:open、read、write、release、unlocked_ioctl

复制代码
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

5、设备号管理

主设备号:区分设备种类

次设备号:区分同类的不同个体

注册:register_chrdev_region()(静态)或alloc_chrdev_region()(动态)

注销:unregister_chrdev_region()

复制代码
dev_t dev_num;
// 动态分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "led");
// 注销设备号
unregister_chrdev_region(dev_num, 1);

6、字符设备注册

cdev_init:初始化cdev结构体,绑定file_operations

cdev_add:向内核添加字符设备

cdev_del:删除字符设备

复制代码
struct cdev led_cdev;
// 初始化并添加
cdev_init(&led_cdev, &led_fops);
cdev_add(&led_cdev, dev_num, 1);
// 删除
cdev_del(&led_cdev);

7、设备节点创建

手动:mknod命令

自动:class_create创建类,device_create创建设备,自动生成/dev/节点

复制代码
struct class *led_class;
struct device *led_device;

// 创建类
led_class = class_create(THIS_MODULE, "led_class");
// 创建设备
led_device = device_create(led_class, NULL, dev_num, NULL, "led");
// 销毁
device_destroy(led_class, dev_num);
class_destroy(led_class);

8、硬件操作函数

led_on:设置寄存器对应位为1(点亮)

led_off:设置寄存器对应位为0(熄灭)

需要操作GPIO配置寄存器(如方向寄存器、数据寄存器)

复制代码
void led_on(void)
{
    u32 val = readl(GPIO1_DR);
    val |= (1 << 3);    // 设置位3为1
    writel(val, GPIO1_DR);
}

void led_off(void)
{
    u32 val = readl(GPIO1_DR);
    val &= ~(1 << 3);   // 设置位3为0
    writel(val, GPIO1_DR);
}

9、模块加载与卸载

module_init:指定初始化函数(加载时调用)

module_exit:指定退出函数(卸载时调用)

复制代码
module_init(led_init);   // 加载入口
module_exit(led_exit);   // 卸载出口

10、GPL协议声明

MODULE_LICENSE:声明GPL协议

MODULE_AUTHOR:作者信息

MODULE_DESCRIPTION:模块描述

复制代码
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("LED Driver");

总结:

ioremap映射地址,copy_from_user拿数据,strcmp解析指令,file_ops定义接口,cdev注册设备,class_create自动建节点,readl/writel操作硬件。

补:vim的ctags使用方法

生成索引文件

在源码根目录下执行ctags -R命令,会递归遍历所有子目录,生成一个名为tags的索引文件。使用vim打开文件时,工作路径需要和tags索引文件所在路径保持一致,否则vim无法找到索引。

跳转与回退

使用Ctrl + ]可以跳转到光标所在函数或变量的定义处。使用Ctrl + o可以回退到跳转前的位置。

相关推荐
xinhuanjieyi2 小时前
极语言让ai学习的方法
开发语言·学习
念恒123062 小时前
Python(复杂判断)
python·学习
happymaker06262 小时前
MyBatis学习日记——DAY03(手写MyBatis框架实现简单功能)
学习
山楂树の2 小时前
原生 WebGL + Canvas 实现鱼眼图像去畸变(Shader逐像素计算)
图像处理·数码相机·学习·程序人生
**蓝桉**2 小时前
容器服务学习笔记
笔记·学习
乔代码嘚2 小时前
Agentic-KGR:多智能体强化学习驱动的知识图谱本体渐进式扩展技术
人工智能·学习·大模型·知识图谱·ai大模型·大模型学习·大模型教程
zhangrelay4 小时前
三分钟云课实践速通--模拟电子技术-模电--SimulIDE
linux·笔记·学习·ubuntu·lubuntu
木木_王4 小时前
嵌入式Linux学习 | 数据结构 (Day05) 栈与队列详解(原理 + C 语言实现 + 实战实验 + 易错点剖析)
linux·c语言·开发语言·数据结构·笔记·学习
OSwich4 小时前
【 Godot 4 学习笔记】数组(Array)
笔记·学习·godot