一、硬件基础:按键模块解析
1. 开发板按键硬件
正点原子 IMX6ULL-Mini 开发板配备 3 个按键:
- 中间:复位按钮;
- 左侧:低功耗按钮;
- 右侧:用户可独立控制的试验按键(核心实操对象)。
2. 按键电气特性(关键!)
从底板原理图可知:
- 按键断开时 ,GPIO 引脚为高电平;
- 按键按下时 ,GPIO 引脚为低电平;
- 我们以 "用户试验按键" 为例,其对应引脚为
UART1_CTS_B(复用为GPIO1_IO18)。
二、轮询方式:按键驱动(基础版)
轮询是最简单的按键检测方式,核心是 "死循环检查引脚电平",适合入门但存在致命缺陷。
1. 按键初始化(GPIO1_IO18 配置)
需完成 4 步配置(参考 IMX6ULL 手册):
(1)复用功能配置
将UART1_CTS_B引脚复用为GPIO1_IO18:
c
运行
// 复用功能配置:ALT5 → GPIO1_IO18
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
MUX_MODE(低 4 位):0101(ALT5),指定引脚为 GPIO 功能;SION:0,关闭信号监控(输入模式无需开启)。
(2)电气特性配置
针对输入模式优化电气参数:
c
运行
// 电气特性配置:上拉、关闭输出驱动等
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF080);
参数解析(对应寄存器IOMUXC_SW_PAD_CTL_PAD_UART1_CTS_B):
| 位段 | 配置值 | 含义 |
|---|---|---|
| HYS(1 位) | 0 | 关闭迟滞(输入模式无需) |
| PUS(2 位) | 11 | 22KΩ 上拉(保证按键断开时为高电平) |
| PUE(1 位) | 1 | 选择 "上拉 / 下拉" 而非 "保持" |
| PKE(1 位) | 1 | 使能上拉 / 下拉功能 |
| ODE(1 位) | 0 | 关闭漏极开漏 |
| SPEED(2 位) | 10 | 中等速度(100MHz) |
| DSE(3 位) | 000 | 关闭输出驱动(输入模式无需) |
| SRE(1 位) | 0 | 慢摆率 |
(3)GPIO 方向配置(输入模式)
c
运行
// 配置GPIO1_IO18为输入模式(GDIR对应位清0)
GPIO1->GDIR &= ~(1 << 18);
GDIR寄存器:0 = 输入,1 = 输出。
(4)开启 GPIO1 时钟
GPIO1 所有引脚共用CCM_CCGR1时钟门,需先开启:
c
运行
// 开启GPIO1时钟(CCM_CCGR1对应位置1)
CCM->CCGR1 |= (3 << 26);
2. 轮询检测按键状态
c
运行
// 读取按键状态:按下返回1,未按下返回0
int key_read(void) {
// 检测GPIO1_IO18电平:低电平→按键按下
if ((GPIO1->DR & (1 << 18)) == 0) {
// 简单消抖(延时后再次检测)
delay(0x10000);
if ((GPIO1->DR & (1 << 18)) == 0) {
return 1;
}
}
return 0;
}
// 主循环轮询
int main(void) {
key_init(); // 按键初始化
led_init(); // LED初始化(用于按键反馈)
while (1) {
// 模拟大量复杂耗时业务(如0x7FFFFF次空循环)
delay(0x7FFFFF);
// 轮询检测按键
if (key_read()) {
led_flicker(); // 按键按下→LED闪烁
// 等待按键释放
while ((GPIO1->DR & (1 << 18)) == 0);
}
}
return 0;
}
3. 轮询方式的致命缺陷
当主循环承担大量、复杂、耗时 的业务(如上述delay(0x7FFFFF))时,会出现:
- 漏检按键:按键按下的瞬间,CPU 正执行耗时业务,未及时检测电平;
- 实时性差:在工业场景(如汽车刹车、工业急停按钮)中,漏检可能导致严重后果。
→ 解决方案:中断方式(CPU 无需主动轮询,按键按下时主动触发中断,CPU 立即响应)。
三、中断方式:按键驱动(进阶版)
1. 中断核心概念
中断是 CPU 的 "紧急响应机制":
- 外设(如 GPIO)发出中断请求;
- CPU 检查中断是否允许响应、优先级是否最高;
- 保护当前运行的 "现场"(寄存器、程序计数器等);
- 执行中断服务函数(ISR);
- 恢复现场,回到原任务继续执行。
对 IMX6ULL 而言,按键触发的是GPIO 外部中断(EINT) ,需经过GIC 中断控制器转发给 Cortex-A7 内核。
2. 中断控制器 GIC(核心!)
IMX6ULL 的 Cortex-A7 内核搭载GIC v2.0(通用中断控制器),是中断管理的 "中枢"。
(1)GIC v2.0 架构
GIC v2.0 主要分为两部分:
- Distributor(分发器):管理所有中断源,负责中断分发、优先级设置、中断屏蔽;
- CPU Interface(CPU 接口):每个内核对应一个,负责将分发器的中断转发给内核,处理中断响应 / 结束。
(2)中断源分类(GIC v2.0)
GIC 支持 1020 个中断源(0~1019),其中:
| 中断类型 | 编号范围 | 用途 |
|---|---|---|
| SGI(软件中断) | 0~15 | 多核通信(软件触发) |
| PPI(私有中断) | 16~31 | 每个内核独有的中断(如定时器、看门狗) |
| SPI(共享外设中断) | 32~1019 | 外设中断(如 GPIO、UART、I2C,我们的核心操作对象) |
(3)IMX6ULL 中断编号映射
- 参考《IMX6ULL 参考手册》:Cortex-A7 中断编号 0~128;
- 参考
MCIMX6Y2.h:IRQn_Type枚举定义了 0~159 的外设中断(含 GPIO 中断)。
3. 协处理器 CP15(Cortex-A7 核心)
Cortex-A7 的CP15 协处理器(CP0~CP15)负责系统控制,其中与中断相关的关键寄存器:
(1)CP15 c1:SCTLR(系统控制寄存器)
bit13(V):异常向量表基地址选择;- 0:正常向量表(0x00000000,可通过 VBAR 重映射);
- 1:高向量表(0xFFFF0000,不可重映射)。
bit12(I):指令 Cache 使能;- 0:关闭(复位默认);
- 1:开启。
操作示例(汇编):
armasm
; 读取SCTLR到r0
mrc p15, 0, r0, c1, c0, 0
; 关闭高向量表(V=0)
bic r0, r0, #(1 << 13)
; 开启指令Cache(I=1)
orr r0, r0, #(1 << 12)
; 写回SCTLR
mcr p15, 0, r0, c1, c0, 0
(2)CP15 c12:VBAR(向量基地址寄存器)
用于重映射异常向量表的基地址(如映射到 0x87800000):
c
运行
// 设置向量表基地址
void __set_VBAR(uint32_t addr) {
__asm__ volatile (
"mcr p15, 0, %0, c12, c0, 0"
: : "r" (addr)
);
}
// 调用示例:向量表映射到0x87800000
__set_VBAR(0x87800000);
(3)CP15 c15:CBAR(配置基地址寄存器)
存储 GIC 寄存器的物理基地址,是访问 GIC 的关键:
armasm
; 读取CBAR到r0(获取GIC基地址)
mrc p15, 4, r0, c15, c0, 0
4. 中断方式按键驱动实现
(1)步骤梳理
- 配置 GPIO1_IO18 为输入模式(同轮询);
- 配置 GPIO1_IO18 的中断触发方式(下降沿触发→按键按下);
- 配置 GIC:使能 GPIO1_IO18 对应的 SPI 中断、设置优先级;
- 编写中断服务函数(ISR):按键按下→LED 翻转;
- 开启 CPU 总中断(CPSIE I)。
(2)核心代码(关键片段)
c
运行
// 1. GPIO中断配置(GPIO1_IO18下降沿触发)
void gpio_irq_config(void) {
// 配置中断触发方式:下降沿
GPIO1->ICR1 |= (1 << 18); // ICR1:1=下降沿,0=上升沿
// 使能GPIO1_IO18中断
GPIO1->IMR |= (1 << 18);
}
// 2. GIC配置(使能GPIO1中断)
void gic_config(void) {
// 步骤1:获取GIC基地址(从CBAR读取)
uint32_t gic_base;
__asm__ volatile ("mrc p15, 4, %0, c15, c0, 0" : "=r" (gic_base));
// 步骤2:分发器配置(GICD)
GICD_Type *gicd = (GICD_Type *)(gic_base + 0x0000);
// 使能GPIO1对应的SPI中断(假设编号为66)
gicd->ISENABLER[2] |= (1 << (66 - 64));
// 设置中断优先级(0~255,数值越小优先级越高)
gicd->IPRIORITYR[66/4] |= (0x80 << ((66%4)*8));
// 将中断分发到CPU0
gicd->ITARGETSR[66/4] |= (0x01 << ((66%4)*8));
// 步骤3:CPU接口配置(GICC)
GICC_Type *gicc = (GICC_Type *)(gic_base + 0x2000);
// 设置CPU中断优先级掩码(0xFF=允许所有优先级)
gicc->PMR = 0xFF;
// 使能CPU接口中断
gicc->CTLR |= 0x01;
}
// 3. 中断服务函数(ISR)
void gpio1_irq_handler(void) {
// 检查是否是GPIO1_IO18中断
if (GPIO1->ISR & (1 << 18)) {
led_flicker(); // LED翻转
GPIO1->ISR |= (1 << 18); // 清除中断标志位
}
// 通知GIC中断处理完成
GICC_Type *gicc = (GICC_Type *)(GIC_BASE + 0x2000);
gicc->EOIR = gicc->IAR;
}
// 4. 主函数初始化
int main(void) {
clock_init();
led_init();
key_init(); // 基础GPIO配置(同轮询)
gpio_irq_config(); // GPIO中断配置
gic_config(); // GIC配置
// 开启CPU总中断
__asm__ volatile ("cpsie i");
// 主循环执行耗时业务(中断不影响)
while (1) {
delay(0x7FFFFF); // 大量耗时操作
}
return 0;
}
四、代码封装:降低耦合性(符合 OCP 原则)
工业级驱动需满足 "开闭原则"------ 对修改关闭、对扩展开放,核心是模块化封装 + 回调函数。
1. 核心思路
- 将 GPIO 操作封装为通用接口(初始化、读、写、中断配置);
- 将中断处理逻辑与业务逻辑解耦(通过回调函数注册);
- 中断模块独立封装,支持扩展不同外设的中断。
2. 封装后的通用接口
(1)GPIO 模块封装(gpio.h/gpio.c)
c
运行
// GPIO配置结构体
typedef struct {
uint8_t direction; // 0=输入,1=输出
uint8_t pull; // 0=无,1=上拉,2=下拉
uint8_t irq_type; // 0=无中断,1=上升沿,2=下降沿,3=双边沿
} gpio_config_t;
// GPIO初始化
extern void gpio_init(GPIO_Type *gpio, int pin, gpio_config_t *config);
// 读取GPIO电平
extern int gpio_read(GPIO_Type *gpio, int pin);
// 写入GPIO电平
extern void gpio_write(GPIO_Type *gpio, int pin, int value);
// GPIO中断注册回调函数
extern void gpio_irq_register(GPIO_Type *gpio, int pin, void (*callback)(void));
(2)中断模块封装(irq.h/irq.c)
c
运行
// 中断初始化(GIC+GPIO中断)
extern void irq_init(void);
// 注册中断服务函数
extern void irq_register(uint32_t irq_num, void (*handler)(void));
// 使能指定中断
extern void irq_enable(uint32_t irq_num);
(3)业务层调用(低耦合)
c
运行
// 按键业务回调函数(可灵活修改,无需改驱动)
void key_callback(void) {
led_flicker(); // 按键按下→LED翻转
}
int main(void) {
// 1. 初始化
clock_init();
led_init();
irq_init();
// 2. GPIO配置(按键)
gpio_config_t key_config = {
.direction = 0, // 输入
.pull = 1, // 上拉
.irq_type = 2 // 下降沿中断
};
gpio_init(GPIO1, 18, &key_config);
// 3. 注册按键中断回调
gpio_irq_register(GPIO1, 18, key_callback);
// 4. 开启总中断
__asm__ volatile ("cpsie i");
// 主循环执行其他业务
while (1) {
// 耗时业务逻辑
}
return 0;
}

五、关键总结
- 轮询 vs 中断:轮询简单但实时性差,适合低要求场景;中断无漏检、实时性高,是工业级场景的首选;
- GIC 核心:IMX6ULL 的中断需经过 GIC 分发器→CPU 接口,才能被 Cortex-A7 响应,需重点配置中断使能、优先级、CPU 目标;
- CP15 操作:协处理器 CP15 的 SCTLR(向量表)、VBAR(向量基地址)、CBAR(GIC 基地址)是中断配置的关键;
- 代码封装:通过通用 GPIO 接口 + 回调函数,降低驱动与业务的耦合性,符合 "开闭原则",便于扩展(如新增按键、修改业务逻辑)。