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 接口 + 回调函数,降低驱动与业务的耦合性,符合 "开闭原则",便于扩展(如新增按键、修改业务逻辑)。
相关推荐
国科安芯12 小时前
航天器多路并联大功率电源系统设计与ASP4644均流特性分析
单片机·嵌入式硬件·fpga开发·安全性测试
茯苓gao14 小时前
嵌入式开发笔记:CANopen相关移位运算与通信协议术语详解
网络·嵌入式硬件·学习·信息与通信
梁朝辉17 小时前
STM32H750VBT6中ADCINP与INN什么区别
stm32·单片机·嵌入式硬件
省四收割者18 小时前
一文详解信号完整性(1)
python·嵌入式硬件·数学建模·信息与通信·信号处理·智能硬件
崇山峻岭之间19 小时前
单片机无感FOC驱动之ADC
单片机·嵌入式硬件
Ligocious19 小时前
1.点亮一颗小小的LED
单片机·嵌入式硬件
KaifuZeng1 天前
通信与接口协议面试八、CAN通信
单片机·嵌入式硬件·面试·通信与接口协议
智源单片机设计1 天前
基于单片机的直流电机双闭环调速系统设计
单片机·嵌入式硬件
KaifuZeng2 天前
通信与接口协议面试一、基本概念
嵌入式硬件·通信与接口协议
wuyk5552 天前
24. C 语言模块化:不是拆几个.c 文件那么简单
c语言·开发语言·stm32·单片机