13-Linux驱动开发-中断子系统

硬件架构的根本差异

Cortex-M4(MCU部分): 跑实时操作系统(RTOS)或裸机,追求极低的延迟。

Cortex-A7(MPU部分): 跑 Linux,追求高吞吐量和多任务管理。

html 复制代码
STM32 MCU (裸机/RTOS) 的逻辑:
	中断控制器(NVIC)和 CPU 是紧耦合的。
	中断来了 -> CPU 直接跳转执行 ISR。
	逻辑简单,不需要软件去"映射"中断号。
STM32MP1 (Linux/Cortex-A7) 的逻辑:
	CPU 和外设之间隔着一个复杂的 GIC(通用中断控制器)。
	GIC 不仅仅是开关中断,它负责路由。MP157 是双核 A7,GIC 要决定按键中断是发给 CPU0 还是 CPU1。
	在写驱动时,虽然不用手写寄存器配置 GIC,但必须理解中断亲和性(Affinity)(这个中断归哪个核管),这对多核系统调试非常关键。
Linux 内核的抽象
html 复制代码
解决的是"软硬映射"的问题。
物理中断号 (HW IRQ) vs 虚拟中断号 (Virtual IRQ):
	GIC 视角:按键可能连接到 GIC 的第 35 号 SPI 中断线(物理号)。
	Linux 视角:内核不会直接用物理号,而是会把它映射成一个软件上的中断号。
	逻辑递进:你在驱动代码中用 request_irq(irq_num, ...) 时,填入的一定是虚拟中断号。
	DTS(设备树)描述物理连接 -> 内核解析 DTS -> 自动映射生成虚拟号 -> 驱动调用 platform_get_irq 获取虚拟号。
中断

中断处理的执行流

html 复制代码
顶半部 (Top Half):
	逻辑:用 request_irq 注册的那个函数。
	要求:快进快出。只做最简单的事(比如读取寄存器状态,清除中断标志)。
	原因:顶半部运行时,往往会关中断,时间太长会把系统卡死。
底半部 (Bottom Half):
	逻辑:如果按键按下后,你需要打印大量日志、或者读写文件、或者做复杂的逻辑判断。
	实现:要使用 tasklet、workqueue(工作队列)或者 threaded_irq。

顶半部/底半部机制的核心,就是把"紧急的硬件响应"和"耗时的软件处理"剥离开来,保证系统对外界事件始终保持高响应速度。

中断的三种类型

html 复制代码
SGI (Software Generated Interrupt) - 软中断
	ID范围: 0 ~ 15
	专门用于 CPU 核心之间通信(IPI)。比如 Core0 想告诉 Core1 "该醒醒了"或者"调度任务",就写寄存器触发一个 SGI。
PPI (Private Peripheral Interrupt) - 私有外设中断
	ID范围: 16 ~ 31
	某些外设是每个 CPU 核心独有的。最典型的就是 Generic Timer (通用定时器)。每个核都有自己的定时器中断,互不干扰。
SPI (Shared Peripheral Interrupt) - 共享外设中断
	ID范围: 32 ~ 1019
	所有外部外设(GPIO按键、串口、I2C、网卡)产生的中断都是 SPI。之所以叫"共享",是因为它们可以被路由到任意一个 CPU 核心去处理。
注意点容易导致内核崩溃 (Kernel Panic)
html 复制代码
1.中断上下文中不能休眠
	现象:在中断处理函数(ISR)中,你不能调用 ssleep(), msleep(), copy_to_user(), kmalloc(..., GFP_KERNEL) 等可能导致线程挂起(Sleep/Block)的函数。
	后果:操作系统会直接崩溃(Oops),因为中断上下文没有进程描述符,一旦休眠,CPU 就不知道该切回到哪里去了。
	因此在处理按键消抖时,绝对不能用 mdelay 或 msleep 死等,必须使用内核定时器。
2.硬件去抖 vs 软件去抖
	现状:机械按键一定会有抖动(按下一次,电平跳变多次)。
	如果只依赖硬件中断触发,必须配合软件定时器去抖。否则按一下,中断会触发几十次,导致系统负载瞬间飙升。
	驱动逻辑通常是:中断触发 -> 关闭该中断 -> 开启定时器(如20ms)-> 定时器到期 -> 读取电平确认 -> 重新开启中断。
3.中断标志位的返回值
	中断处理函数必须返回 irqreturn_t 类型。
	IRQ_HANDLED:告诉内核"这个中断是我处理的"。
	IRQ_NONE:告诉内核"这个中断不是我的"(用于共享中断线的情况)。
	如果驱动一直返回 IRQ_NONE 但中断确实在发生,内核会检测到中断风暴,为了保护系统,会强行屏蔽掉这个中断源。
4.并发与竞争
	场景:如果你的按键中断修改了一个全局变量,而你的应用程序也在读取这个变量。
	注意:在中断中保护共享资源,不能使用互斥锁(Mutex)(因为Mutex可能导致休眠),只能使用自旋锁(Spinlock)。
5.设备树中的触发方式
	在 DTS 中配置中断时,interrupts 属性里的标志位(如 IRQ_TYPE_EDGE_FALLING 下降沿触发)必须与硬件电路匹配。
	如果引脚没有外部上拉/下拉电阻,必须在 pinctrl(引脚控制器)节点中配置内部上拉/下拉,否则引脚处于浮空状态,手指还没碰到按键,中断就会乱跳。
相应代码

设备树插件代码

html 复制代码
/dts-v1/;
/plugin/;
//#include "../stm32mp157c.dtsi"
#include <dt-bindings/pinctrl/stm32-pinfunc.h>
#include <dt-bindings/input/input.h>
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

 / {
     fragment@0 {
          target-path = "/";
         __overlay__ { 
            button_interrupt {
            	compatible = "button_interrupt";
            	pinctrl-names = "default";
            	pinctrl-0 = <&pinctrl_button>;
            	button_gpio = <&gpiob 13 GPIO_ACTIVE_LOW>;  //默认低电平,按键按下高电平
            	status = "okay";
				interrupts-extended = <&gpiob 13 IRQ_TYPE_EDGE_RISING>;
				interrupt-names = "button_interrupt";				
            	//interrupt-parent = <&gpiob>;
            	//interrupts = <13 IRQ_TYPE_EDGE_RISING>;     // 指定中断,触发方式为上升沿触发。
            };   
         };
     };

     fragment@1 {
         target = <&pinctrl>;
         __overlay__ { 
        pinctrl_button: buttongrp {
				pins {
					pinmux = <STM32_PINMUX('B', 13, GPIO)>;
					drive-push-pull;
				};
			};
         };
     };
 };

驱动代码

c 复制代码
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>


#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/irq.h>
#include <linux/of_irq.h>

#include "interrupt.h"


/*------------------字符设备内容----------------------*/
#define DEV_NAME "button"
#define DEV_CNT (1)

static dev_t button_devno;		 //定义字符设备的设备号
static struct cdev button_chr_dev; //定义字符设备结构体chr_dev
struct class *class_button;		 //保存创建的类
struct device *device_button;		 // 保存创建的设备


struct device_node	*button_device_node = NULL;  //定义按键设备节点结构体
unsigned  button_GPIO_number = 0;  //保存button使用的GPIO引脚编号
u32  interrupt_number = 0;         // button 引脚中断编号
atomic_t   button_status = ATOMIC_INIT(0);  //定义整型原子变量,保存按键状态 ,设置初始值为0



struct work_struct button_work;  //定义工作结构体

/*定义工作执行函数*/
void work_hander(struct work_struct  *work)
{
	int counter = 1;
	mdelay(200);
	printk(KERN_ERR "work_hander counter = %d  \n", counter++);
	mdelay(200);
	printk(KERN_ERR "work_hander counter = %d  \n", counter++);
	mdelay(200);
	printk(KERN_ERR "work_hander counter = %d  \n", counter++);
	mdelay(200);
	printk(KERN_ERR "work_hander counter = %d  \n", counter++);
	mdelay(200);
	printk(KERN_ERR "work_hander counter = %d  \n", counter++);
}



/*定义按键中断服务函数*/
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
	/*按键状态加一*/
	atomic_inc(&button_status);
	schedule_work(&button_work);  //触发工作

	return IRQ_HANDLED;
}


static int button_open(struct inode *inode, struct file *filp)
{
	int error = -1;
	


	/*获取按键 设备树节点*/
	button_device_node = of_find_node_by_path("/button_interrupt");
	if(NULL == button_device_node)
	{
		printk("of_find_node_by_path error!");
		return -1;
	}

	/*获取按键使用的GPIO*/
	button_GPIO_number = of_get_named_gpio(button_device_node ,"button_gpio", 0);
	if(0 == button_GPIO_number)
	{
		printk("of_get_named_gpio error");
		return -1;
	}

	/*申请GPIO  , 记得释放*/
	error = gpio_request(button_GPIO_number, "button_gpio");
	if(error < 0)
	{
		printk("gpio_request error");
		gpio_free(button_GPIO_number);
		return -1;
	}

	error = gpio_direction_input(button_GPIO_number);//设置引脚为输入模式

	/*获取中断号*/
	interrupt_number = irq_of_parse_and_map(button_device_node, 0);
	printk("\n irq_of_parse_and_map! =  %d \n",interrupt_number);


	/*申请中断, 记得释放*/
	error = request_irq(interrupt_number,button_irq_hander,IRQF_TRIGGER_RISING,"button_interrupt",device_button);
	if(error != 0)
	{
		printk("request_irq error");
		free_irq(interrupt_number, device_button);
		return -1;
	}


	/*初始化button_work*/
	INIT_WORK(&button_work, work_hander);

	/*申请之后已经开启了,切记不要再次打开,否则运行时报错*/
	// // enable_irq(interrupt_number);

	return 0;

}

static int button_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int error = -1;
	int button_countervc = 0;

	/*读取按键状态值*/
	button_countervc = atomic_read(&button_status);

	/*结果拷贝到用户空间*/
	error = copy_to_user(buf, &button_countervc, sizeof(button_countervc));
	if(error < 0)
	{
		printk("copy_to_user error");
		return -1;
	}

	/*清零按键状态值*/
	atomic_set(&button_status,0);
	return 0;
}

/*字符设备操作函数集,.release函数实现*/
static int button_release(struct inode *inode, struct file *filp)
{
	/*释放申请的引脚,和中断*/
	gpio_free(button_GPIO_number);
	free_irq(interrupt_number, device_button);
	return 0;
}



/*字符设备操作函数集*/
static struct file_operations button_chr_dev_fops = {
	.owner = THIS_MODULE,
	.open = button_open,
	.read = button_read,
	.release = button_release};

/*
*驱动初始化函数
*/
static int __init button_driver_init(void)
{
	int error = -1;

	/*采用动态分配的方式,获取设备编号,次设备号为0,*/
	error = alloc_chrdev_region(&button_devno, 0, DEV_CNT, DEV_NAME);
	if (error < 0)
	{
		printk("fail to alloc button_devno\n");
		goto alloc_err;
	}

	/*关联字符设备结构体cdev与文件操作结构体file_operations*/
	button_chr_dev.owner = THIS_MODULE;
	cdev_init(&button_chr_dev, &button_chr_dev_fops);

	/*添加设备至cdev_map散列表中*/ 
	error = cdev_add(&button_chr_dev, button_devno, DEV_CNT);
	if (error < 0) 
	{
		printk("fail to add cdev\n");
		goto add_err;
	}

	class_button = class_create(THIS_MODULE, DEV_NAME);                         //创建类
	device_button = device_create(class_button, NULL, button_devno, NULL, DEV_NAME);//创建设备 DEV_NAME 指定设备名,

	return 0;

add_err:
	unregister_chrdev_region(button_devno, DEV_CNT);    // 添加设备失败时,需要注销设备号
	printk("\n error! \n");
	
alloc_err:
	return -1;
}



/*
*驱动注销函数
*/
static void __exit button_driver_exit(void)
{
	pr_info("button_driver_exit\n");
	/*删除设备*/
	device_destroy(class_button, button_devno);		   //清除设备
	class_destroy(class_button);					   //清除类
	cdev_del(&button_chr_dev);					       //清除设备号
	unregister_chrdev_region(button_devno, DEV_CNT);   //取消注册字符设备
}



module_init(button_driver_init);
module_exit(button_driver_exit);

MODULE_LICENSE("GPL");

代码拆解:
第一维度:核心机制(顶半部与底半部)

顶半部 (Top Half):button_irq_hander

c 复制代码
/*定义按键中断服务函数*/
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
    /*按键状态加一*/
    atomic_inc(&button_status);
    
    /* 核心动作:调度底半部 */
    schedule_work(&button_work);  //触发工作

    return IRQ_HANDLED; // 告诉内核:这个中断我处理了
}

底半部 (Bottom Half):work_hander

html 复制代码
/*定义工作执行函数*/
void work_hander(struct work_struct  *work)
{
    int counter = 1;
    /* 核心特征:可以休眠 */
    mdelay(200); // 延时 200ms
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
    mdelay(200);
    // ... 后面还有很多延时
}

关联

html 复制代码
全局定义了 struct work_struct button_work; 用于描述一个工作项。
绑定函数(在 button_open 中):
	/*初始化button_work,将结构体与底半部函数绑定*/
	INIT_WORK(&button_work, work_hander);
触发调度(在顶半部中):
	/* 告诉内核:把这个工作项加入执行队列,尽快执行 */
	schedule_work(&button_work);

第二维度:生命周期管理

驱动的生命周期分为两个层次:模块的加载/卸载,和设备的打开/关闭。

模块加载与卸载 (init/exit)

c 复制代码
static int __init button_driver_init(void) { ... }
static void __exit button_driver_exit(void) { ... }
/*
申请字符设备号,注册字符设备 (cdev_add),在 /sys/class 下创建类,最终在 /dev 下生成设备文件 button。
*/

设备的打开与释放 (open/release)

这里是真正的硬件操作

c 复制代码
static int button_open(struct inode *inode, struct file *filp)
{
    // 1. 解析设备树,找到节点
    button_device_node = of_find_node_by_path("/button_interrupt");
    // 2. 获取 GPIO 编号
    button_GPIO_number = of_get_named_gpio(...);
    // 3. 申请 GPIO 资源并设置为输入
    gpio_request(button_GPIO_number, ...);
    gpio_direction_input(button_GPIO_number);
    
    // 4. 关键:将 GPIO 映射为 Linux 虚拟中断号
    interrupt_number = irq_of_parse_and_map(button_device_node, 0);

    // 5. 核心:申请中断,注册顶半部函数 button_irq_hander
    // IRQF_TRIGGER_RISING 指定上升沿触发
    request_irq(interrupt_number, button_irq_hander, IRQF_TRIGGER_RISING, ...);

    // 6. 初始化底半部工作项
    INIT_WORK(&button_work, work_hander);
    
    return 0;
}

第三维度:数据交互(用户空间如何知道按键)

c 复制代码
atomic_t  button_status = ATOMIC_INIT(0); // 定义原子变量

// 顶半部 ISR 中:
atomic_inc(&button_status); // 按下一次,变量加 1

// read 操作中:
static int button_read(struct file *filp, char __user *buf, ...)
{
    int button_countervc = 0;
    /*读取按键状态值*/
    button_countervc = atomic_read(&button_status);

    /*结果拷贝到用户空间*/
    copy_to_user(buf, &button_countervc, sizeof(button_countervc));

    /*读完后清零*/
    atomic_set(&button_status,0);
    return 0;
}

第四维度:细节

html 复制代码
1.用 atomic_t (原子变量)的目的
	button_status 这个变量,它会被两个"并发"的实体访问:
		中断处理函数 (ISR):随时可能发生,修改它。
		button_read 函数:用户进程在读取它。
	如果使用普通的 int 变量,可能会出现竞态条件(Race Condition)。例如,read 函数刚读到一半,中断来了把它修改了,导致读出的数据错乱。使用 atomic_t 和对应的 atomic_inc/atomic_read 函数,能保证这些操作是原子的(不可分割的),从而安全地在中断和进程之间共享数据。
2.设备树依赖
	button_device_node = of_find_node_by_path("/button_interrupt");
	button_GPIO_number = of_get_named_gpio(button_device_node ,"button_gpio", 0);
	interrupt_number = irq_of_parse_and_map(button_device_node, 0);
	这个驱动依赖于一个配套的设备树插件(.dtbo)。在设备树中,必须定义一个名为 /button_interrupt 的节点,并且该节点需要指定 button_gpio 属性,以及中断控制器父节点等信息。内核会根据设备树的描述,自动帮你找到对应的 GPIO 引脚和物理中断线,并映射为驱动可用的虚拟中断号。
基于工作队列(Workqueue)的 Linux 中断驱动程序的完整生命周期和执行分层
html 复制代码
阶段一:初始化与资源准备 (Setup Phase)还没有按键按下,系统处于准备状态。
	用户态启动:位于最上层蓝色区域的应用程序启动 (App Start)。
	发起调用:应用调用标准的 POSIX 接口 open("/dev/button", ...)。
	进入内核:通过系统调用 (System Call),执行流陷入内核态的进程上下文 (Process Context, 浅橙色区域)。
	驱动响应 (button_open):
		驱动程序中的 button_open 函数被执行。
		它负责"建立资源"(Sets up resources),包括申请 GPIO 引脚,最重要的是调用 request_irq 注册中断处理函数,并初始化工作队列项。此时,硬件中断被使能,系统开始准备接收按键事件。
阶段二:常规数据读取 (Normal Operation Phase)应用程序主动查询按键状态的过程。
	用户态读取:应用程序在一个循环中调用 read(...),试图获取按键数据。
	驱动响应 (button_read):
		访问共享数据:它去读取位于中央的"全局共享数据"区域 (Global Shared Data),查看原子变量 atomic_t 的值(即按键次数)。
		返回数据:将读到的次数拷贝回用户空间,并将内核中的计数器清零。
	业务处理:应用程序拿到数据后进行业务逻辑处理 (Process Logic),然后再次循环读取。
阶段三:异步中断事件处理 (The Asynchronous Interrupt Core)描述了当物理按键被按下时发生的瞬间。
	硬件触发 (最底层灰色区域):手指按下物理按键 (Physical Button Press),硬件产生电平信号触发中断 (Hardware Trigger)。
	顶半部执行 (Top Half - 红色醒目区域):
		CPU 立即暂停当前任务,跳转执行注册的中断服务函数 button_irq_hander。
		关键约束 (CRITICAL! NOTE: NO SLEEP!):此时处于中断上下文,绝对禁止休眠或执行耗时操作。
		动作 A (原子操作):快速将"全局共享数据"中的原子计数器加 1 (atomic increment)。
		动作 B (调度底半部):调用 schedule_work,将耗时的后续处理任务"挂"到内核的工作队列中,然后立即退出顶半部。
	底半部执行 (Bottom Half - 右下灰色区域):
		当系统调度器认为合适时,内核工作线程 (Kernel Worker Thread) 会被唤醒。
		它执行之前被调度的 work_hander 函数。
		此时运行在进程上下文,可以放心地休眠(执行去抖动延时 mdelay、打印复杂日志等)。
阶段四:资源释放与清理 (Cleanup Phase)应用程序退出时的收尾工作。
用户态关闭:应用程序完成任务,调用 close()。
驱动响应 (button_release):运行在进程上下文。
	执行"清理资源"操作 (Clean up resources)。最重要的是调用 free_irq 释放中断号,并释放 GPIO 资源,确保下次加载驱动时不会冲突。
相关推荐
福尔摩斯张1 小时前
Linux进程间通信(IPC)机制深度解析与实践指南
linux·运维·服务器·数据结构·c++·算法
cookies_s_s2 小时前
项目--协程库(C++)前置知识篇
linux·服务器·c++
不过普通话一乙不改名2 小时前
Linux 网络发包的极致之路:从普通模式到 AF_XDP ZeroCopy
linux·运维·网络
jquerybootstrap2 小时前
大地2000转经纬度坐标
linux·开发语言·python
x***13392 小时前
如何在Linux中找到MySQL的安装目录
linux·运维·mysql
4***17543 小时前
linux 网卡配置
linux·网络·php
南林yan3 小时前
tcpdump
linux·tcpdump
偶像你挑的噻4 小时前
Linux应用开发-9-信号
linux·stm32·嵌入式硬件
Gene_20224 小时前
搭建自己的小车 —— 运行livox mid_360
linux·ubuntu