4、GPIO中断编程:中断控制器GIC简介、GPIO中断触发方式、中断服务函数编写、中断优先级配置
好,咱们进入第四章。这一章讲中断,算是驱动开发里比较核心的一块了。我刚开始做嵌入式那会儿,总觉得轮询也能凑合用,直到项目里需要处理高速数据流,CPU被占得死死的,才意识到中断有多重要。说白了,中断就是让外设在有事的时候主动通知CPU,而不是让CPU一直傻等着。
4.1 中断控制器GIC简介
ARM架构里,中断管理靠的是GIC(Generic Interrupt Controller)。GIC负责接收各个外设的中断信号,然后统一分发给CPU核心。你想想看,如果没有GIC,每个外设都直接连CPU,那CPU得有多少个引脚才够用?
GIC主要分两代:GICv2和GICv3。Cortex-A系列处理器大多用GICv2,Cortex-A53/A72这些64位处理器开始支持GICv3。咱们课程基于Cortex-A7,所以重点讲GICv2。
| 特性 | GICv2 | GICv3 |
|---|---|---|
| 支持CPU数量 | 最多8个 | 无限制 |
| 中断号范围 | 0-1019 | 0-65535 |
| 寄存器接口 | 内存映射 | 内存映射 + 系统寄存器 |
| 中断分组 | Group 0/1 | Group 0/1/S |
GIC内部有两个关键模块:分发器(Distributor)和CPU接口(CPU Interface)。分发器负责中断的使能、优先级配置、触发方式设置;CPU接口则负责把中断信号送给具体的CPU核心。
核心概念:GIC把中断分为三类——SPI(共享外设中断)、PPI(私有外设中断)、SGI(软件触发中断)。GPIO中断通常属于SPI,中断号在32-1019之间。
4.2 GPIO中断触发方式
GPIO中断的触发方式,说白了就是告诉GIC:什么情况下算一次中断事件。ARM芯片的GPIO模块通常支持以下几种触发方式:
- 上升沿触发:信号从低到高跳变时触发
- 下降沿触发:信号从高到低跳变时触发
- 双边沿触发:上升沿和下降沿都触发
- 高电平触发:信号保持高电平时持续触发
- 低电平触发:信号保持低电平时持续触发
我的经验:按键检测一般用下降沿触发,配合消抖处理。如果是电平触发,记得在中断服务函数里清除中断标志,否则会一直触发,CPU就卡死了。我曾经在一个项目里忘了清标志,结果系统启动后直接死循环,查了半天才发现问题。
配置触发方式,通常操作GPIO控制器的几个寄存器。以某款Cortex-A7芯片为例:
/* GPIO中断触发方式配置 */
#define GPIO_BASE 0x11000000
#define GPIO_ISR 0x00 /* 中断触发类型寄存器 */
#define GPIO_IBE 0x04 /* 双边沿使能寄存器 */
#define GPIO_IEV 0x08 /* 中断触发极性寄存器 */
void gpio_irq_set_trigger(int pin, int trigger_mode)
{
uint32_t reg_val;
switch(trigger_mode) {
case TRIGGER_RISING_EDGE:
/* 设置电平触发,上升沿 */
reg_val = readl(GPIO_BASE + GPIO_ISR);
reg_val &= ~(1 << pin); /* 边沿触发 */
writel(reg_val, GPIO_BASE + GPIO_ISR);
reg_val = readl(GPIO_BASE + GPIO_IEV);
reg_val |= (1 << pin); /* 上升沿 */
writel(reg_val, GPIO_BASE + GPIO_IEV);
break;
case TRIGGER_FALLING_EDGE:
reg_val = readl(GPIO_BASE + GPIO_ISR);
reg_val &= ~(1 << pin);
writel(reg_val, GPIO_BASE + GPIO_ISR);
reg_val = readl(GPIO_BASE + GPIO_IEV);
reg_val &= ~(1 << pin); /* 下降沿 */
writel(reg_val, GPIO_BASE + GPIO_IEV);
break;
case TRIGGER_HIGH_LEVEL:
reg_val = readl(GPIO_BASE + GPIO_ISR);
reg_val |= (1 << pin); /* 电平触发 */
writel(reg_val, GPIO_BASE + GPIO_ISR);
reg_val = readl(GPIO_BASE + GPIO_IEV);
reg_val |= (1 << pin); /* 高电平 */
writel(reg_val, GPIO_BASE + GPIO_IEV);
break;
}
}
4.3 中断服务函数编写
中断服务函数(ISR)是中断处理的灵魂。写ISR有几个铁律,我刚开始写驱动时踩过不少坑,这里给你总结一下:
ISR编写铁律:
- ISR要短小精悍,不要在中断里做复杂运算
- ISR里不能调用可能导致睡眠的函数(如mutex_lock、kmalloc)
- 必须清除中断标志,否则会反复触发
- 如果需要处理大量数据,用中断底半部(tasklet/workqueue)
在Linux内核里注册GPIO中断的流程是这样的:
/* GPIO中断服务函数示例 */
static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
struct gpio_dev *dev = (struct gpio_dev *)dev_id;
/* 1. 读取中断状态,确认中断源 */
uint32_t status = readl(dev->base + GPIO_MIS);
if (!(status & (1 << dev->pin)))
return IRQ_NONE;
/* 2. 清除中断标志 */
writel((1 << dev->pin), dev->base + GPIO_IC);
/* 3. 处理中断事件(这里只做标记,实际处理放到底半部) */
dev->irq_count++;
schedule_work(&dev->work);
/* 4. 返回IRQ_HANDLED表示已处理 */
return IRQ_HANDLED;
}
/* 中断注册 */
static int gpio_irq_init(struct platform_device *pdev)
{
struct gpio_dev *dev = platform_get_drvdata(pdev);
int ret;
/* 获取中断号 */
dev->irq = platform_get_irq(pdev, 0);
if (dev->irq < 0)
return dev->irq;
/* 注册中断 */
ret = request_irq(dev->irq, gpio_irq_handler,
IRQF_TRIGGER_FALLING, "gpio_key", dev);
if (ret) {
dev_err(&pdev->dev, "request_irq failed\n");
return ret;
}
return 0;
}
这里有个细节:request_irq的第三个参数是中断标志,可以指定触发方式。我个人习惯在设备树里配好触发方式,代码里用IRQF_TRIGGER_NONE,让内核自动解析设备树配置。
4.4 中断优先级配置
中断优先级决定了多个中断同时到达时,CPU先处理哪个。GICv2支持256级优先级(0-255),数值越小优先级越高。嗯,这里要注意:不同芯片厂商的实现可能不一样,有的只支持16级或32级。
优先级配置主要在GIC的分发器里操作:
/* GIC优先级配置 */
#define GIC_DIST_BASE 0x10400000
#define GICD_IPRIORITYR 0x400 /* 优先级寄存器基址 */
void gic_set_irq_priority(int irq, int priority)
{
uint32_t reg_offset;
int byte_offset;
uint32_t reg_val;
/* 每个中断占用8位,4个中断占一个32位寄存器 */
reg_offset = GICD_IPRIORITYR + (irq / 4) * 4;
byte_offset = (irq % 4) * 8;
reg_val = readl(GIC_DIST_BASE + reg_offset);
reg_val &= ~(0xFF << byte_offset);
reg_val |= (priority & 0xFF) << byte_offset;
writel(reg_val, GIC_DIST_BASE + reg_offset);
}
| 优先级值 | 含义 | 典型用途 |
|---|---|---|
| 0-31 | 最高优先级 | 系统时钟、紧急事件 |
| 32-95 | 高优先级 | 高速外设(DMA、以太网) |
| 96-159 | 中优先级 | GPIO、UART、I2C |
| 160-255 | 低优先级 | 按键、温度传感器 |
避坑指南:我曾经在一个项目里把GPIO中断优先级设得比系统定时器还高,结果导致系统调度紊乱,任务无法按时切换。记住:系统定时器中断优先级一定要高于普通外设中断,否则系统会变得不稳定。
配置完优先级后,还要设置中断目标CPU。GICv2里每个中断可以路由到特定的CPU核心:
/* 设置中断目标CPU */
#define GICD_ITARGETSR 0x800
void gic_set_irq_cpu(int irq, int cpu_mask)
{
uint32_t reg_offset;
int byte_offset;
uint32_t reg_val;
reg_offset = GICD_ITARGETSR + (irq / 4) * 4;
byte_offset = (irq % 4) * 8;
reg_val = readl(GIC_DIST_BASE + reg_offset);
reg_val &= ~(0xFF << byte_offset);
reg_val |= (cpu_mask & 0xFF) << byte_offset;
writel(reg_val, GIC_DIST_BASE + reg_offset);
}
多核处理器里,一般把中断分散到不同核心上,避免某个核心负载过高。比如网卡中断给CPU0,GPIO中断给CPU1,这样能提升整体性能。
好了,这一章的内容就这些。中断编程的核心就是理解GIC的工作机制,正确配置触发方式,写好ISR,再合理分配优先级。把这些搞明白了,大部分外设中断驱动你都能拿下来。