news 2026/7/15 19:56:15

linux内核驱动开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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:查看驱动

sudo rmmod hello-driver卸载驱动文件

lsmod:列出当前有哪些驱动

modinfo hello-driver.ko 查看驱动的信息

编译内核操作系统

make ARCH=arm exynos_defconfig //导入配置

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找到。

我们对应打开文件之后,一般都是读写,那对应的readwrite第一个参数都是文件描述符,你通过文件描述符找到的就是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

kmalloc和malloc对比

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

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

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

设备号

设备号是内核查找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函数

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

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

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

设备树

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

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

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

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

(芯片厂商的gp2)定义了一个“管家”(GPIO 控制器),并规定:谁想用我的服务,必须给我2 个参数(引脚号 + 标志)。

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

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

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

查找到的信息如下

所以我们的GPIO配置如下

按照芯片的GPIO控制器规定,去找管家申请服务:<&gpx2 7 0>——“管家,我要用你的第 7 号引脚,标志是 0”。到这一步我们代码就不需要寄存器操作了

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

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

此时用链表;of_gpio_led

不论应用层open的是哪一个LED设备的设备文件,在LED的驱动中调用的都是同一个led_open函数,那么如何在驱动的led_open函数中区分不同的LED设备进行操作?

回答:
led_open函数中,通过file参数中的f_inode指针找到当前打开的设备号(dev_t),然后根据设备号在驱动中查找对应的私有数据结构体(如led_dev),从而区分操作不同的 LED。

中断(Interrupt

CPU处理事情:

1.轮询方式:不断的查询是否要处理事情,如果需要则处理。由于很多时候条件不满足,CPU不需要处理,这样浪费了CPU的时间。

2.中断方式:当需要CPU处理的时候,产生一个信号,打断CPU正在做的事情。让CPU处理当前的事情,当CPU处理完处理完当前事件之后,回到打断之前的地方接着执行。

中断处理需要注意的地方:

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芯片手册/文档:低电平触发

写程序

先调用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");

接着就是设备树匹配表

/* 设备树匹配表:用于定义该驱动支持哪些硬件设备 */ 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(of, key_of_match)“把我的名片贴在外面,让大家知道我能修这种设备”(写在模块元数据,供外部工具识别)

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

modinfo key_driver.ko

然后就是驱动匹配和分离函数;

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

//第一种:获取中断资源 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

4.中断上半部和下半部
将中断处理函数中需要做的事情,分成两部分,在不同的函数中完成。
中断处理函数中完成的事情 ,是上半部(屏蔽外面的中断)。
而另外一个函数中完成的事情,是下半部(不屏蔽外面的中断)。

打游戏(进程)<----------(中断)
吃饭:重要(上半部)
吃虾:耗时时间长(下半部:进程上下文或中断上下文)<----------女友电话(中断)

上半部:
中断处理函数,不可以被打断


下半部:
可以被打断,可以在中断上下文或进程上下文执行


问:上半部什么时候执行?
答:中断产生,就执行中断处理函数(上半部)

问:下半部什么时候执行?
答:在合适的时间点执行(一般是在上半部结束的时候,开始触发下半部)

问:什么事情放在上半部,什么事情放在下半部?
答:紧急的事情,耗费时间不多,我们放在上半部
耗时的时间长或需要休眠,我们放在下半部

网卡(轮询或中断) 中断-----> 网卡收到数据 (以太网头|ip头|tcp/udp头|用户数据)

5.下半部的实现机制
软中断(理解原理) tasklet(掌握) workqueue(掌握)

注意:
<1>软中断相关的函数接口,Linux 内核并没有导出,所以在驱动中不可以直接使用
<2>tasklet是基于软中断实现
<3>workqueue它是进程上下文的下半部机制

软中断

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

查看是否导出

EXPORT_SYMBOL

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

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

Input子系统

主要是对外部事件的感知;相当于外部事件的输入;向上层应用提供接口

主要组成部分包括:

1 、 输入核心层 ( I nput core )

2 、 输入事件层 ( I nput Events )

3 、 设备驱动程序层 ( I nput Device Driver )

Linux 内核中注册了多个事件处理 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 ) 或 i octl 控制 LED 开关, 是硬件驱动与用户空间控制的中继。

Input 子系统框架

input_dev : 是硬件驱动层, 代表 一个 i nput 设备。

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

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

Linux 内核中维护了一个名为 i nput_handler_list 的链表, 每个 i nput_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)]; // 开关事件码表 }

在注册 i nput_dev 前需要初始化一些必要的数据包括 i nput_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同步事件;

硬件

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/15 19:55:39

AM3358 GPMC异步模式与LPDDR接口时序设计及PCB实战指南

1. 项目概述与核心价值 在嵌入式系统开发中&#xff0c;尤其是基于TI Sitara系列处理器&#xff08;如AM3358-EP&#xff09;的设计&#xff0c;外部存储器的接口设计往往是决定系统稳定性与性能的关键一环。通用内存控制器&#xff08;GPMC&#xff09;和LPDDR内存接口&#x…

作者头像 李华
网站建设 2026/7/15 19:55:08

【2013-12-11】ubuntu系统使用QQ

[历史归档] 本文原发布于 cstriker1407.info 个人博客&#xff0c;内容为历史存档&#xff0c;仅供参考。 发布时间&#xff1a; 2013-12-11 &#xff5c; 标题&#xff1a;ubuntu系统使用QQ &#xff5c; 分类&#xff1a; 操作系统 / linux &#xff5c; 标签&#xff…

作者头像 李华
网站建设 2026/7/15 19:52:37

智能体哪家好?从技术架构、安全合规到落地成本全面对比——2026企业级AI Agent选型深度指南

在2026年7月的今天&#xff0c;人工智能产业已完成从“生成式AI”向“行动式AI”的跨越。智能体&#xff08;AI Agent&#xff09;不再仅仅是能够对话的聊天机器人&#xff0c;而是具备感知、决策、执行与记忆能力的数字员工。随着《人工智能拟人化互动服务管理暂行办法》的正式…

作者头像 李华
网站建设 2026/7/15 19:52:10

条款10:优先选用限定作用域enum,而非不限定作用域enum

目录 条款10&#xff1a;优先选用限定作用域enum&#xff0c;而非不限定作用域enum&#xff08;Prefer scoped enum to unscoped enum&#xff09; 优点分析 优点1&#xff08;作用域与命名空间&#xff09;&#xff1a;enum class可以避免&#xff08;外围&#xff09;名字…

作者头像 李华
网站建设 2026/7/15 19:51:33

每天认识一种投资品类:期权

文章目录1.什么是期权&#xff1f;2.期权的基本要素3.期权的两种基本类型4.期权的买方与卖方5.期权的价格由什么组成&#xff1f;6.期权的主要策略7.期权的核心风险8.期权 vs 期货9.谁适合交易期权&#xff1f;10.总结参考文献期权&#xff0c;是一种赋予买方在未来约定时间内&…

作者头像 李华