IMX6ULL 裸机开发进阶:按键驱动(轮询→中断)+ 中断控制器 GIC 实战

一、硬件基础:按键模块解析

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 的 "紧急响应机制":

  1. 外设(如 GPIO)发出中断请求
  2. CPU 检查中断是否允许响应、优先级是否最高;
  3. 保护当前运行的 "现场"(寄存器、程序计数器等);
  4. 执行中断服务函数(ISR)
  5. 恢复现场,回到原任务继续执行。

对 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.hIRQn_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)步骤梳理
  1. 配置 GPIO1_IO18 为输入模式(同轮询);
  2. 配置 GPIO1_IO18 的中断触发方式(下降沿触发→按键按下);
  3. 配置 GIC:使能 GPIO1_IO18 对应的 SPI 中断、设置优先级;
  4. 编写中断服务函数(ISR):按键按下→LED 翻转;
  5. 开启 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;
}

五、关键总结

  1. 轮询 vs 中断:轮询简单但实时性差,适合低要求场景;中断无漏检、实时性高,是工业级场景的首选;
  2. GIC 核心:IMX6ULL 的中断需经过 GIC 分发器→CPU 接口,才能被 Cortex-A7 响应,需重点配置中断使能、优先级、CPU 目标;
  3. CP15 操作:协处理器 CP15 的 SCTLR(向量表)、VBAR(向量基地址)、CBAR(GIC 基地址)是中断配置的关键;
  4. 代码封装:通过通用 GPIO 接口 + 回调函数,降低驱动与业务的耦合性,符合 "开闭原则",便于扩展(如新增按键、修改业务逻辑)。
相关推荐
Y1rong2 小时前
STM32之SysTick
stm32·单片机·嵌入式硬件
Y1rong2 小时前
STM32之定时器(一)
stm32·单片机·嵌入式硬件
广药门徒2 小时前
WINDOWS 10如何在PCB 等软件自由使用快捷键
嵌入式硬件
代码游侠3 小时前
学习笔记——GPIO按键与中断系统
c语言·开发语言·arm开发·笔记·嵌入式硬件·学习·重构
__万波__3 小时前
STM32L475按键中断实验
stm32·单片机·嵌入式硬件
破晓单片机3 小时前
STM32单片机分享:智能恒温箱系统
stm32·单片机·嵌入式硬件·智能家居
小痞同学3 小时前
【铁头山羊STM32】HAL库 6.中断部分
stm32·单片机·嵌入式硬件
你爱写程序吗(新H)3 小时前
基于单片机的洗衣机控制系统设计 单片机洗衣机控制(设计+文档)
c语言·汇编·单片机·嵌入式硬件·matlab
淘晶驰AK3 小时前
新手学单片机,主要是玩,学什么好?
单片机·嵌入式硬件