欢迎光临
我们一直在努力

linux内核驱动开发

Linux内核基本知识

Linux 驱动的分类

  • 字符设备驱动 — 鼠标/键盘,在对应 /dev 目录下有设备文件

  • 网络设备驱动 — 网卡,使用 ifconfig 进行配置

  • 块设备驱动 — eMMC、SD卡

最基本框架

#include <linux/init.h>
#include <linux/module.h>

// 驱动入口函数
static int hello_init(void)
{
return 0;
}

// 驱动出口函数
static void hello_exit(void)
{
return;
}

module_init(hello_init); // 告诉Linux,这个驱动入口函数是那个装载入口函数
module_exit(hello_exit); // 告诉Linux,这个驱动出口函数是那个装载出口函数

source insight === 只是代码阅读工具

驱动代码从下向上读

调试硬件

之前学硬件开发(功能开发 功能和你业务代码是杂糅在一起 比如你要开灯 串口通信 + led的控制 + 自定义协议 + 解析协议实现对应的 led的控制)单片机
裸机 ==== 寄存器操作
hal库操作 ==== cubmx hal库函数
freeRTOS实时操作系统
linux驱动开发
1、只给应用层提供对应操作硬件设备的接口,具体怎么实现什么功能怎么使用应用层管理 美颜相机(只给你提供相机接口(打开 读取 关闭))
Linux一切皆文件 /对应设备文件,/dev 去操作对应的硬件设备
2、Linux可以是开发 多任务去访问设备

Samba远程链接虚拟机

特点虚拟机共享文件夹SambaVSCode Remote-SSH
依赖 依赖虚拟机软件(VMware Tools) 依赖 Samba 服务 依赖 SSH 服务
传输方式 虚拟机专用通道 网络协议(SMB) 网络协议(SSH)
主要用途 临时传文件 文件共享(类 NAS) 代码开发、调试
权限控制 较弱 精细 基于 SSH 用户权限
访问设备 仅限宿主机 局域网任何设备 仅限发起 SSH 连接的设备
实时性 需要手动刷新 需要手动刷新 实时同步,即时生效
开发体验 差(需手动拷贝) 一般(需手动拷贝) 极佳(本地化体验)

内核编译

由于驱动的代码是在内核层运行的;但是gcc是在应用层运行的;所以gcc不能编译驱动代码

所以要用linux源码的编译系统Makefile编译

内核编译思想: Linux 内核源码的编译系统可以编译我们编写的模块代码。

两种方式:

第一种(产品发布阶段)

把自己写的驱动源码拷贝到Linux内核源码树下,进行对应的配置编译,编译进内核。

通过编写Makefile和Kconfig文件,并执行make menuconfig配置界面来选择是否将驱动编译进内核。

第二种(产品研发阶段)

自己编译Makefile,然后利用Linux内核的编译系统,编译自己的模块代码。

Makefile告诉内核编译系统怎么编译你的.c源码,内核编译系统根据内核源码树里的规则和配置,最终生成一个可以在ARM开发板上加载的.ko驱动模块文件。

模块的内核编译makefile文件讲解

在linux编译系统(Makefile)里面KERNELRELEASE第一次调用的时候不赋值;第二次调用的时候才赋值;

ifeq ($(KERNELRELEASE),) 用于判断当前是否处于内核构建系统的第二次调用:如果 KERNELRELEASE 为空,说明是第一次被用户手动调用,此时执行主机端配置(如指定交叉编译工具链和目标平台);若不为空,则说明已被内核构建系统调用,此时执行模块编译(obj-m)。

KERNELRELEASE当前正在编译的内核版本号字符串(如 5.15.0-91-generic),用于标识模块编译目标的内核源码树版本。

我们写的makefile会被两次调用;

第一次我们在终端执行make的时候makefile被第一次调用;

第二次调用是linux编译系统调用我们写的makefile;

第二次调用是由 make -C $(BUILD_SYSTEM) M=$(MODULE_PATH) modules 这条命令触发的,它会进入内核源码树的 Makefile,而内核的 Makefile 会递归调用当前目录的 Makefile 来编译具体的模块目标(obj-m),所以第二次是内核构建系统主动、自动地重新读取并执行你的 Makefile。

所以通过KERNELRELEASE来判断是第一次还是第二次调用

自己编译的Makefile如下:

ifeq ($(KERNELRELEASE),)
ARM_BUILD_SYSTEM=/home/hqyj/fs4412/linux.3.14/linux-3.14
X86_BUILD_SYSTEM=/lib/modules/$(shell uname -r)/build
MODULE_PATH=$(shell pwd)

arm_module:
make -C $(ARM_BUILD_SYSTEM) M=$(MODULE_PATH) ARCH=arm CROSS_COMPILE=arm-linux- modules

x86_module:
make -C $(X86_BUILD_SYSTEM) M=$(MODULE_PATH) modules

clean:
make -C $(ARM_BUILD_SYSTEM) M=$(MODULE_PATH) clean
make -C $(X86_BUILD_SYSTEM) M=$(MODULE_PATH) clean

install:
cp ./Hello-driver.ko ~/fs4412/rootfs/
else
obj-m = Hello-driver.o //新的驱动只需要改这里
endif

虚拟机(也是linux系统)的内核源码的编译系统在 /lib/modules/$(uname -r)/build 目录下,它是一个指向内核源码树的符号链接。

uname -r 来获取当前内核版本号

modules 是 Makefile 里的一个编译目标,执行它告诉编译系统只生成内核模块(.ko文件),而不是编译整个内核。

驱动模块操作指令

sudo insmod hello-driver.ko安装驱动文件

dmesg:查看驱动;用于查看内核日志,可以查看驱动运行过程中通过 printk 打印的信息,适用于调试

sudo dmesg -c:清除内核日志

sudo rmmod hello-driver卸载驱动文件

lsmod:列出当前有哪些驱动;用于列出当前已加载的驱动模块,查看驱动是否成功加载;

modinfo hello-driver.ko 查看驱动的信息;用于查看驱动文件(.ko)本身携带的元信息(如作者、许可证、设备别名等),无需加载即可查询。

编译内核操作系统

make ARCH=arm exynos_defconfig        //导入配置;会清除菜单里面的配置

make menuconfig

make ARCH=arm CROSS_COMPILE=arm-linux- uImage

驱动传参

作用

1.设置驱动相关参数;比如设置缓冲区大小

2.设置完全校验;防止驱动被盗用

方法

1.传递普通的参数

module_param(name, type, perm);

name: 传递的进去的名称
type: 类型
perm: 参数读写的权限 (S_IRUSR) (可以进去看一下 /sys/module/驱动名/parameters/)

static int a = 0;
static char *my_char = "a"; //对于字符来说只能传字符指针
static int count = 0;
static int number[10];
module_param(a, int, 0644);
module_param(my_char, charp, 0644);

insmod hello.ko a=1

2.传递数组

module_param_array(name, type, nump, perm);

name: 传递的进去的名称
type: 类型
nump: 实际传入进去的参数个数
perm: 参数读写的权限 (S_IRUSR) (可以进去看一下 /sys/module/驱动名/parameters/)

module_param_array(number, int, &count, 0644);

字符设备驱动框架

字符设备和块设备会生成设备文件

应用层操作设备文件从而操作硬件设备;通过linux提供的框架将open和硬件接口联系

对于应用层和接口

函数接口通过函数指针来规定返回值和参数规定死;上层代码只认这个,底层通过注册回调函数来填充实现,调用时通过指针间接执行,从而实现接口标准化、逻辑解耦和硬件抽象。

// 1. 规定接口(函数指针类型)
typedef int (*key_operation_t)(int key_code, int value);

// 2. 上层框架只认这个接口
struct key_driver {
key_operation_t report_key; // 函数指针作为接口
};

// 3. 底层驱动实现具体功能(回调函数)
int my_key_report(int key_code, int value) {
printk("Key %d = %d\\n", key_code, value);
return 0;
}

// 4. 注册:把实现绑定到接口
struct key_driver drv;
drv.report_key = my_key_report; // 函数指针指向具体实现

// 5. 调用:硬件触发时通过函数指针调用
drv.report_key(1, 1); // 实际执行的是 my_key_report

对硬件来说只写操作设备的接口

由于对于上层来说只有设备文件;所以要设备文件和接口关联;通过linux设备框架来关联

函数接口通过结构体file_operations规定;里面存放了很多函数;

高类聚低耦合:把相关的紧密放一起(高内聚),不相关的别扯上关系(低耦合)

内核open设备文件的驱动查找流程

应用层范围,设备号就藏在设备文件中

创建设备文件

mknod 设备文件名 设备文件类型 主设备号 次设备号
mknod /dev/led c 77 0

内核用inode和file这两个结构体来记录信息为了方便查找,提高效率。

inode 这个结构体,第一次打开这个设备时,对应的 i_cdev 是没有值的,发现对应的指针为空,它就会通过设备号去查找,去找对应的底下的 cdev,找到之后会把对应的 cdev 的地址放到 i_cdev 中,第二次 open 打开这个设备文件的时候,对应的 i_cdev 已经记录了,就可以通过 i_cdev 找到。

我们对应打开文件之后,一般都是读写,那对应的 read 和 write 第一个参数都是文件描述符,你通过文件描述符找到的就是 file 结构体,从而找到 file_operations,如果没有file 这个结构体,需要重新找一遍,大大提高了对应的效率。

当驱动被加载时,它会向内核注册自己的主设备号和一组操作函数(file_operations)。当用户程序调用 open 打开 /dev 下的对应设备文件时,内核通过文件中的主设备号找到并绑定该驱动,之后用户程序对该文件描述符的读写操作,就会自动被内核转发到驱动注册的对应函数,从而实现对硬件的控制。

cdev

把写好的接口函数写入cdev里面去

cdev结构体:描述通用的字符设备结构体;不然一个设备一个结构体太占内存了

几个设备提供几个函数接口;然后把函数接口放到cdev结构体中

struct cdev {
struct kobject kobj; /* 内嵌内核对象,用于设备模型管理(sysfs、引用计数) */
struct module *owner; /* 指向拥有此设备的模块(通常为 THIS_MODULE),防止模块被卸载 */
const struct file_operations *ops; /* 文件操作函数集(open/read/write/ioctl 等),驱动核心接口 */
struct list_head list; /* 链表节点,用于将设备挂入内核全局链表或哈希表 */
dev_t dev; /* 设备号(主设备号 + 次设备号),唯一标识设备 */
unsigned int count; /* 次设备号数量,表示该设备占用的连续次设备号范围大小 */
};
//主要填充dev和ops

常见的内存分配函数

malloc 分配的空间"不连续";虚拟地址连续,物理地址可能不连续

kmalloc 分配的空间"连续";虚拟地址连续,物理地址也连续

┌─────────────────────────────────────┐ 高地址
│ 内核空间 │ ← 内核代码、数据、kmalloc分配在这里
│ (1GB on 32-bit Linux) │
├─────────────────────────────────────┤
│ 栈区 (Stack) │ ← 局部变量、函数调用
├─────────────────────────────────────┤
│ | | │
│ | | (内存映射区) │ ← mmap、共享库
│ | | │
├─────────────────────────────────────┤
│ 堆区 (Heap) │ ← malloc分配在这里(用户空间)
├─────────────────────────────────────┤
│ BSS段 │ ← 未初始化全局/静态变量
├─────────────────────────────────────┤
│ 数据段 (Data) │ ← 已初始化全局/静态变量
├─────────────────────────────────────┤
│ 代码段 (Text) │ ← 程序指令、只读
└─────────────────────────────────────┘ 低地址

devm_kzalloc 分配在内核空间的 slab 堆区,属于内核态内存,物理连续且自动管理生命周期。

devm_kzalloc 为 key 结构体分配并清零内存,且内存生命周期绑定到 pdev->dev,设备注销时自动释放。

设备号

设备号是内核查找cdev的唯一索引,主设备号标识设备类型,次设备号区分同类型的不同设备。


驱动的标识:设备号

  • 12bit(主设备号)+ 20bit(次设备号)= 32bit

  • 主设备号:标识一类设备

  • 次设备号:为了区分同类设备的不同设备

特性ls -l /devcat /proc/devices
查看对象 /dev 目录下的具体设备文件(节点) 内核中已注册的设备驱动程序(由主设备号标识)
展示内容 主设备号 和 次设备号 仅主设备号,以及对应的驱动名称
信息维度 个体信息:每个设备文件是一个独立的个体,拥有自己完整的设备号 (major, minor) 类别信息:展示有哪些驱动类别(主设备号)已被内核识别,不关心每个类别下的具体个体(次设备号)
适用场景 当你想知道某一个特定设备文件(如 /dev/sda1)的设备号时 当你想知道系统支持哪些类型的设备,或验证一个驱动程序是否已成功加载时

驱动核心思想

向应用层的程序提供设备的操作函数接口;如何让应用层的程序找到底层驱动提供的接口

linux应用程序和驱动程序相互关系

底层写驱动的对应的本质:为上层提供接口,函数接口,硬件的函数接口。

向上层应用工程师提供函数接口的方法,函数怎么写?怎么规定写法

在c语言中函数指针来限制函数的写法

在c++有语法可以对子类函数接口来进行限制,多态的场合写的纯虚函数,父类定义纯虚函数,相当于规定“必须有这个功能”

总结:

我们写驱动就是给别人提供一种函数接口,给上层的工程师调用,但是上层工程师不知道我们今天给提供了什么接口函数,不知道函数名,调用不了,但是他必须要调用我们函数,这个时候能想到的就是函数指针。底层驱动把具体的函数地址赋值给这个指针,这样就能调用函数

内核发展

字符驱动框架

2.4 linux kernel 字符驱动框架

LED驱动缺点:
可移植性差,原因:驱动中包含了特定平台的硬件信息(华清板子 + ccynos4412芯片),如果是其他平台,硬件信息会有差异,所以驱动无法直接使用!

总线、设备、驱动框架

2.6 linux kernel 总线、设备、驱动框架

总线、设备、驱动 => 增加驱动的可扩展性和可移植性
设备的信息从驱动中分离出来,我们需要在操作系统中,添加设备和驱动两部分。
设备中包含设备的信息(资源),驱动中包含的是操作设备函数接口。
为了让驱动最终能操作我们的硬件设备,我们在驱动中必须获取设备的信息(资源)。

设备驱动=设备+驱动;驱动中不再直接包含设备的信息

设备和驱动是分离的,它们都会向内核总线(如 platform 总线)注册。当任何一方(无论是设备先来,还是驱动先来)注册时,内核都会触发一次总线上的匹配扫描,寻找名称或ID匹配的对方。一旦匹配成功,操作系统就会自动调用驱动中实现的 probe 函数。

当设备和驱动匹配成功后,内核会将这个设备的详细信息(包括设备树解析出的资源、名称、ID等)封装成 platform_device 结构体,并通过 probe 函数的参数传递给驱动。

在 probe 函数里,开发者只需要使用操作系统提供的通用API(如 platform_get_resource、devm_clk_get 等),就能从匹配到的设备信息中获取硬件资源(地址、中断、GPIO等),而无需关心这些信息是如何被记录的。

设备与驱动匹配成功后,二者通过指针相互绑定(如 pdev->driver 指向驱动,drv->devices 链接设备),形成 “你中有我,我中有你” 的关联。

在Linux内核中,总线本质上是两个用于管理和匹配的链表,分别挂载着该总线上的设备和驱动;当设备或驱动注册时,内核通过总线遍历这两个链表进行匹配,匹配成功则调用驱动的probe函数。

设备树

cd /home/hqyj/fs4412/linux.3.14/linux-3.14/
vim arch/arm/boot/dts/exynos4412-fs4412.dts

驱动文件(.ko)从 NFS 根文件系统读取,所以更新后立即生效;设备树(.dtb)在 U-Boot 阶段从 eMMC/SD 卡分区读取,所以即使 NFS 目录中的 .dtb 文件更新了也不会生效。改用 TFTP 加载设备树即可解决

make dtbs 必须在 Linux 内核源码的根目录下执行; exynos4412-fs4412.dts 文件正好在这个源码树中。

早期没有设备树只有镜像文件;包含我们上面写的配置信息

设备的信息是针对于特定平台的,如果我们在Linux内核中包含太多设备信息,则Linux内核移植性就会变差。引入设备树之后,设备的信息的描述不在是以代码的形式存在于Linux内核源代码中,这种做法实际上是将设备的信息,从Linux内核中独立出来,单独描述

Linux内核在启动的时候,要求把设备树文件传递给它。它拿到设备树之后,会解析设备树文件,从而识别设备信息。

//@ 后面通常跟设备基地址
fs4412-led@11000c40{
compatible = "fs4412-led";
reg = <0x11000c40 8>;
};

 Linux 内核platform总线上设备与驱动的匹配规则
 <1>设备树中的compatible属性与驱动中指定的of_match_table中的compatible进行匹配
    如果没有匹配成功:
 <2>如果驱动中有id_table,则拿id_table中记录的名字与设备的名字匹配
    如果驱动中没有id_table,则拿驱动的名字与设备的名字(platform_device 结构体中 name 字段)匹配

of 是 "Open Firmware"(开放固件)设备树 

设备树后缀三个设备文件

.dtb:二进制文件;给操作系统用的可执行文件

.dts:.c文件;设备信息源文件;给工程师看的

.dtsi:.h文件;设备树的头文件

写设备树步骤

写设备树需要看驱动

1.拷贝设备树

通过当前设备树的头文件找出所有设备树文件;然后拷贝出来到电脑的共享文件夹中;

因为在内核(linux)里面查会有很多设备树文件,不方便看

2.找厂家的GPIO控制器节点

每个GPIO控制器必包含gpio-controller属性

# 在设备树目录下递归搜索 gpio-controller 属性
cd 源码/arch/arm/boot/dts/
grep "gpio-controller" * -nR

部分含义
grep Linux 下的文本搜索工具(Global Regular Expression Print)
"gpio-controller" 设备树中声明某个硬件节点为 GPIO 控制器的标签,告诉内核“我能管理 GPIO 引脚,其他设备可以通过我申请使用具体引脚”。
* 表示当前目录下的所有文件(作为搜索范围)
-n 显示行号(在匹配结果中显示该行在文件中的行号)
-R 递归搜索(Recursive,进入所有子目录继续搜索)

就可以找到GPIO控制器节点,并规定了使用该控制器时,需要提供2个参数

//gpx2:gpx2前面的是标签名:方便其他节点引用;后面的是节点名
gp2: gp2 {
gpio-controller;
//#xxx-cells = <数字>
//含义:xxx 这个信息,需要使用多少个 32-bit 数字(u32)来表示。
#gpio-cells = <2>;

interrupt-controller;
#interrupt-cells = <2>;
};

3.确认参数含义

 实在不知道怎么写在源码/Documentation/devicetree/bindings:有各个设备树的解释

grep "gpio-cells" * -nR | grep "厂家名字"

查找到的信息如下

还不知道的话在arch/arm/boot/dts/里面查看这个节点中别人是怎么写的

# 进入设备树目录
cd arch/arm/boot/dts/

# 搜索某个节点或属性的用法(以 GPIO 为例)
grep "gpios =" * -nR | grep "&gpx"

# 或者搜索特定控制器的引用
grep "&gpx2" * -nR

4.根据原理图来确定管脚

知道哪个是GPIO控制节点然后根据自己的设备电路图找到对应的节点

5.引用该节点

在自己的设备树中(linux-3.14/arch/arm/boot/dts)引用该节点

我们创建了多个设备文件;那么驱动怎么操作设备文件获取不同的设备信息

全局的pled只能一次记录一个设备的信息;其他的就丢了;此时用链表

驱动通过led_open函数的inode参数获取次设备号,并用该次设备号在设备信息链表/数组中定位到对应的私有数据结构,从而区分并操作不同的LED设备。

中断(Interrupt)

注意

1.中断打断其他程序的执行,所以中断处理的时候需要尽可能的快,不能在中断处理过程中做耗时很长的事情。

2.中断打断的当前的程序执行,所以在中断处理的时候,需要先保存现场(CPU的状态和CPU内部寄存器的值:压栈保存)在中断处理结束的时候,则恢复现场。

概念

1.中断源:产生中断的源头

2.中断号:是SOC芯片厂家对SOC芯片内部中断源的编号

3.中断处理函数:中断产生之后,需要调用执行的函数

4.中断控制器:控制中断的优先级、中断是否被允许产生

5.内部中断和外部中断:

内部中断:SOC芯片内部控制器产生的中断

外部中断:SOC芯片外部管脚通过电平触发产生的中断

高电平触发、低电平触发、上升沿触发、下降沿触发、双边沿触发

Linux 中断分为三类:

软件中断(内核代码主动触发,用于异步任务处理)、

私有中断(PPI)(仅绑定的特定 CPU 核心接收,如 CPU 本地定时器)

共享中断(SPI)(中断信号可以发给多个 CPU 核心,由系统决定由哪个核心处理。可被多个外设共用,但同一中断号上多个设备需共享一个 ISR,由驱动自己判断谁触发了中断)。

当 GPIO、定时器(Timer)、ADC 等外设模块产生中断信号后,这些信号会先经过各自内部的子中断控制器(如 Combiner)进行初步合并或管理,随后统一汇总到中断控制器(GIC,Generic Interrupt Controller)。GIC 作为中断枢纽,负责对每个中断源进行编号、使能、屏蔽和优先级仲裁,并根据配置将中断请求分类为普通中断(IRQ)或快速中断(FIQ),最终分发给ARM 核心(ARM Core)。ARM 核接收到中断信号后,会立即保存当前程序的执行上下文(现场),然后跳转到异常处理向量表,执行对应的中断服务例程(ISR),处理完后再恢复现场并返回被打断的程序继续执行。

查找设备树

先在自己设备树目录下查找看别人怎么写的

grep "interrupt-parent" * -nR

然后在linux-share/learn-driver/dts自己的设备树下查找;看看芯片厂家怎么写的

grep "interrupt-controller" * -nR

combiner要看芯片手册支不支持

就近原则:谁直接控制我们中断就写谁;我们选图三

实在不知道怎么找在芯片的参考文档linux-3.14/Documentation/devicetree/bindings中找

grep "#interrupt-cells" * -nR | grep exynos

先找板子再找厂家;从范围小的找起

参数值来源
第一个参数(中断号) 1 硬件原理图:按键连接在 GPX1_1 引脚,对应中断编号 1
第二个参数(触发方式) 8 芯片手册/文档:低电平触发

所以写出来的设备树是

写程序

1.先调用platform总线设备结构体;然后注册和注销设备

注册设备还可以用以下代码

/*
* 功能:将 platform_driver 结构体注册到内核平台总线
*module_platform_driver() 是一个宏,它会在预处理阶段自动展开成一段完整的代码
*同时包含了注册(module_init)和注销(module_exit)两部分。
* 等价于在 module_init 中调用 platform_driver_register()
* 并在 module_exit 中调用 platform_driver_unregister()
*/
module_platform_driver(key_driver);

/*
* MODULE_AUTHOR:声明驱动作者信息
* 可通过 modinfo 命令查看
*/
MODULE_AUTHOR("HQYJ");

/*
* MODULE_LICENSE:声明模块许可证(内核强制要求)
*/
MODULE_LICENSE("GPTL");

2.设备树匹配表

/* 设备树匹配表:用于定义该驱动支持哪些硬件设备 */
static const struct of_device_id key_of_match[] = {
{ .compatible = "fs4412-key" },
{ /* Sentinel */ } // 空哨兵,表示表结束
};

/* 将设备树匹配表导出为模块设备表,供内核在加载时识别 */
MODULE_DEVICE_TABLE(of, key_of_match);

在设备树中,你只能看到 compatible = "snps,dw-apb-uart" 这个节点属性;而 MODULE_DEVICE_TABLE() 的产物在设备树中看不到,只能在模块文件(.ko)的元数据中看到

//modinfo 命令用于查看 Linux 内核模块(.ko 文件)的详细信息
modinfo key_driver.ko

3.驱动匹配和分离函数

其中驱动匹配函数需要获取资源

//第一种:获取中断资源
res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
if(!res) {
printk("Fail to platform_get_resource:IRQ\\n");
return -ENODEV;
}

printk("IRQ number:%d\\n",res->start);
printk("interrupt-name:%s\\n",res->name);
printk("interrupt flags:%#x\\n",(unsigned int)res->flags);

//第二种:自己解析设备树节点获取中断号
irq_number = irq_of_parse_and_map(np,0);
if(!irq_number) {
printk("Fail to irq_of_parse_and_map\\n");
return -ENODEV;
}

printk("irq number:%d\\n",irq_number);

共享中断

多个驱动程序申请的是同一个中断号(多个设备请求的中断是同一个中断的时候)
此时中断产生的时候,操作系统是不区分那个设备产生了中断,操作系统做法是将这个中断号关联
所有中断处理函数全部调用一次,所以此时中断处理函数必须能判别是否是自己的设备产生的中断,如果不是,立即返回,如果是,则做中断处理。

<1>注册中断的时候,需要IRQF_SHARED标志
<2>给中断处理函数传递的参数必须是唯一的,不能是NULL

全局的irq_desc数组按中断号(0到NR_IRQS-1)索引,每个irq_desc结构体记录对应中断号、指向特定中断控制器操作函数集(irq_chip,如VIC0/VIC1)的指针,以及一个由irqaction结构体组成的链表。irqaction用于描述具体的中断处理动作,包含处理函数handler、传递给处理函数的参数、中断标志、设备名称,并通过next指针支持多个设备共享同一中断号,当发生中断时,内核会通过该中断号找到对应的irq_desc,进而调用其irq_chip中的控制函数(如enable/disable)并遍历执行action链表上的所有处理函数。

中断上半部和下半部

将中断处理函数中需要做的事情,分成两部分,在不同的函数中完成。

上半部是中断处理函数,在中断上下文中执行,屏蔽中断、不可被打断,负责紧急任务

下半部在中断使能状态下执行,可被打断,并在中断上下文或进程上下文中运行,负责耗时的剩余工作。

上半部在中断产生时立即执行,在中断上下文中运行且屏蔽中断,只负责紧急、快速的任务(如清中断标志、拷贝硬件数据);

下半部则在合适的时机(通常上半部结束后触发)执行,不屏蔽中断、可被打断,负责耗时较长或可能需要休眠的剩余工作(如数据处理、协议解析)。

以网卡为例,中断触发后,上半部快速拷贝数据包并触发下半部,下半部随后解析以太网头、IP头、TCP/UDP头及用户数据。这样设计既保证了中断响应的实时性,又避免长时间屏蔽中断导致系统性能下降。

下半部的实现机制

软中断

分析到这里,要告诉大家一个不幸的消息,Linux 内核并没有导出open_softirq、raise_softirq函数的符号,也就说我们无法以动态加载模块的方式使用软中断。如果大家需要使用软中断,可以修改内核代码,导出这两个函数的符号,然后重新编译内核。

查看是否导出

EXPORT_SYMBOL

tasklet

tasklet 是基于软中断实现的。Linux 内核支持的 10 种软中断中,HI_SOFTIRQ 和 TASKLET_SOFTIRQ 就是用来实现 tasklet 的。

软中断不能直接使用,Linux 内核为了方便开发者,在软中断之上封装了 tasklet 接口,使用更简单。所以,除非对性能要求特别高,否则都应使用 tasklet。

work queue

工作队列(work queue)是另一种将工作推后执行的形式,它把工作交由一个内核线程去执行,在进程上下文运行,但不能访问用户空间。其最大特点是允许重新调度甚至睡眠。

选择工作队列还是软中断/tasklet,可遵循以下规则:

  • 推后执行的任务需要睡眠 → 只能选工作队列;

  • 任务需要延时指定时间再触发 → 选工作队列(可利用timer延时);

  • 任务需要在一个tick内处理 → 选软中断或tasklet(可抢占普通进程和内核线程);

  • 任务对延迟时间无要求(无关紧要的任务) → 选工作队列。

  • 另外,如果需要用一个可以重新调度的实体来执行下半部,应使用工作队列。它是唯一能在进程上下文运行的下半部机制,也只有它可以睡眠。这在需要获取大量内存、获取信号量、执行阻塞式I/O操作时非常有用。

    下半部机制上下文复杂度执行性能顺序执行保障
    软中断 中断
    (需要自己确保软中断的执行顺序及锁机制)

    (全部自己实现,便于调优)
    没有
    tasklet 中断
    (提供了简单的接口来使用软中断)
    同类型不能同时执行
    工作队列 进程
    (在进程上下文中运行,与写用户程序差不多)
    没有
    (和进程上下文一样被调度)

    Input子系统

    Input 子系统用于管理各类输入设备(如按键、触摸屏、鼠标等),负责对外部事件的感知与上报。它将硬件层产生的原始输入事件,经过内核驱动和核心层处理后,通过统一的接口(如 /dev/input/eventX)提供给用户空间应用程序使用。

    事件处理 handler 驱动模块

    evdev : 通用输入事件接口, 将不同设备的输入 ( 如键盘、 鼠标、 触摸屏 ) 转换为统一的事件格式。 用户 可以通过设备文件 /dev/eventX 或 /dev/input/eventX 读取事件。 evdev 次设设备号范围: 64-95 和 256-1024 , 如果设备数量不超过 32 , 使用静态保留区范围 64-95 , 如 果超过 32 个, 则在 256-1024 之间动态分配。次设备号不一定,但主设备号是一定的

    mousedev : 一个早期的鼠标事件接口, 支持 PS/2 接口的鼠标, 现在鼠标基本上都是 USB 或蓝牙接口 的, 他们使用了 HID 接口, HID 后文会单独介绍。

    joydev : 专为游戏手柄、 摇杆设计, 处理多轴、 按键事件, 生成 /dev/input/jsX 设备节点。 提供标准游 戏设备接口, 支持校准和力反馈功能, 方便游戏或输入工具直接访问。

    input_leds : 管理输入设备的 LED 状态 ( 如键盘的 Caps Lock 、 Num Lock )通过 sysfs ( 如 /sys/class/leds ) 或 ioctl 控制 LED 开关, 是硬件驱动与用户空间控制的中继。

    Input 子系统框架

    input_dev :硬件驱动层, 代表一个 input 设备。

    input_handler : 事件处理层, 代表一个事件处理器。

    input_handle : 核心层, 代表一个配对的 input 设备与 input 事件处理器。

    设备匹配流程

    Linux 内核中维护了一个名为 input_handler_list 的链表, 每个 input_handler 都会被注册到这个链表 上, 而链表是通过类型为 list_head 的成员 node 串联起来的。

    匹配成功后

    输入设备结构体

    struct input_dev {
    unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)]; // 设备属性
    unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 事件类型表
    unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 按键事件码表
    unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; // 相对坐标事件码表
    unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; // 绝对坐标事件码表
    unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; // 混杂事件码表
    unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; // led灯事件码表
    unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; // 声音事件码表
    unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; // 力反馈事件码表
    unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; // 开关事件码表
    }

    在注册 input_dev前需要初始化一些必要的数据包括 input_dev的evbit 、keybit 、absbit 和relbit等几个重要成员,evbit代表当前设备支持哪些事件类型, 其他几个表示当前设备支持那些事件码。 比如一个按键驱动需要初始化 evbit 和 keybit ,evbit 的 EV_KEY 位要置位,keybit 的键值位要置位。

    使用 set_bit 这个函数进行操作:

    void set_bit(unsigned int nr, volatile unsigned long *p);

    //示例:
    set_bit(EV_KEY, input_dev->evbit); // 支持按键事件
    set_bit(KEY_1, input_dev->keybit); // 支持数字1键
    set_bit(KEY_2, input_dev->keybit); // 支持数字2键

    input能支持哪些类型事件

    struct input_event {
    __kernel_ulong_t __sec; // 上报时间秒
    __kernel_ulong_t __usec; // 上报时间微秒
    __u16 type; // 上报事件类型
    __u16 code; // 上报事件编码
    __s32 value; // 上报事件值
    }

    Input 子系统常用接口

    /* 手动分配 input_dev 结构体 */
    struct input_dev *input_allocate_device(void);

    /* 释放 input_dev 结构体 */
    void input_free_device(struct input_dev *dev);

    /* 自动管理 input_dev 结构体,当 dev 被释放时 input_dev 结构体自动释放 */
    struct input_dev *devm_input_allocate_device(struct device *dev);

    /* 注册 input_dev 到系统中的 input 核心层 */
    int input_register_device(struct input_dev *dev);

    /* 在 input 核心层注销 input_dev */
    void input_unregister_device(struct input_dev *dev);

    具体的事件还需要下面的函数

    /* 上报一个按键事件 */
    void input_report_key(struct input_dev *dev, unsigned int code, int value);

    /* 上报一个相对坐标事件 */
    void input_report_rel(struct input_dev *dev, unsigned int code, int value);

    /* 上报一个绝对坐标事件 */
    void input_report_abs(struct input_dev *dev, unsigned int code, int value);

    /* 标记一组输入事件的结束 */
    void input_sync(struct input_dev *dev);

    /* 上报触摸点信息 */
    void input_mt_slot(struct input_dev *dev, int slot);

    /* 添加新触摸点 */
    bool input_mt_report_slot_state(struct input_dev *dev, unsigned int tool_type, bool active);

    /* 同步触摸事件 */
    void input_mt_sync(struct input_dev *dev);

    在中断章节里我们已经讲解了按键驱动的编写,这里只需要在其基础上增加输入设备相关代码即可,调整部分包括:

    1、定义input_dev结构体
    2、probe函数中的修改
    通过input_allocate_device为input_dev分配内存并初始化;
    设置input_dev的evbit成员和keybit的成员;
    通过input_register_device将input_dev注册到系统中;
    3、remove函数的修改
    通过input_unregister_device将input_dev从系统中注销;
    通过input_free_device将input_dev释放掉;
    4、中断处理函数的修改
    判断具体按键,通过input_report_key将按键事件提交给上层,一次按键需要同步两个事件包括按下和抬起;
    调用input_sync同步事件;

    引入 input 子系统,让你的按键从"内核调试工具"升级为"系统标准输入设备"!

    • 之前:按键只有内核开发者能看见(看 dmesg)

    • 之后:按键所有人都能用(应用程序 + 系统服务 + GUI)

    加载驱动 (insmod)

    module_init → key_driver_init()

    platform_driver_register(&key_driver) // 向平台总线注册驱动

    平台总线根据 of_match_table 匹配设备树节点

    匹配成功 → key_probe() 被调用

    1. 分配 key_data 结构体(存储 irq 和 input 指针)
    2. 获取中断资源(中断号、触发方式)
    3. 分配 input 设备(为应用层提供接口)
    4. 设置 input 设备(设备名、支持事件类型、支持按键)
    5. 注册 input 设备(生成 /dev/input/eventX)
    6. 保存 key_data 到 platform(方便 remove 时使用)
    7. 注册中断(绑定中断号和中断处理函数)

    驱动加载完成,等待按键中断

    按下 K2/K3 → 触发硬件中断

    CPU 跳转到 key_interrupt() 执行(中断上下文)

    1. input_report_key(…, KEY_2, 1) → 上报按下事件
    2. input_sync() → 同步事件到应用层
    3. msleep(20) → 防抖延时
    4. input_report_key(…, KEY_2, 0) → 上报释放事件
    5. input_sync() → 再次同步
    6. printk 打印日志

    返回 IRQ_HANDLED(告诉内核中断已处理)

    应用层通过 read("/dev/input/eventX") 收到按键事件

    卸载驱动 (rmmod)

    module_exit → key_driver_exit()

    platform_driver_unregister()

    key_remove() 被调用

    1. 获取 key_data 结构体
    2. input_unregister_device() → 注销 input 设备

    驱动卸载完成

    赞(0)
    未经允许不得转载:171主机测评 » linux内核驱动开发
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址