完整嵌入式中断系统开发笔记(从0基础到实践)
第一部分:硬件平台与开发环境
1.1 开发板简介![]()
IMX6ULL Mini开发板
CPU:ARM Cortex-A7 单核处理器
内存:内部RAM(0x80000000 - 0x8FFFFFFF)
外设:GPIO、UART、I2C、SPI等
按键:GPIO1_IO18(用户按键,按下为低电平)
1.2 开发工具链
# Makefile关键配置 cross_compiler = arm-linux-gnueabihf- # ARM交叉编译器 cc = $(cross_compiler)gcc # C编译器 ld = $(cross_compiler)ld # 链接器 objcopy = $(cross_compiler)objcopy # 二进制转换工具 objdump = $(cross_compiler)objdump # 反汇编工具
第二部分:系统启动流程详解
2.1 异常向量表(start.S核心部分)
/* * 异常向量表 - ARM架构规定必须放在0x00或0xFFFF0000地址 * 每个异常向量占用4字节(一条指令) */ .global _start _start: ldr pc, =_reset_handler /* 0x00: 复位异常 - 系统启动入口 */ ldr pc, =_undef_handler /* 0x04: 未定义指令异常 */ ldr pc, =_software_handler /* 0x08: 软件中断异常(SWI) */ ldr pc, =_prefect_handler /* 0x0C: 预取指异常 */ ldr pc, =_data_abort_handler /* 0x10: 数据访问异常 */ nop /* 0x14: 保留 */ ldr pc, =_irq_handler /* 0x18: IRQ中断 - 外设中断入口 */ ldr pc, =_fiq_handler /* 0x1C: FIQ中断 - 快速中断入口 */ /* 其他异常处理(简单死循环) */ _undef_handler: b _undef_handler _software_handler: b _software_handler _prefect_handler: b _prefect_handler _data_abort_handler: b _data_abort_handler _fiq_handler: b _fiq_handler
2.2 复位处理程序(系统初始化)
_reset_handler: /* 1. 禁用IRQ中断(防止初始化过程被打断) */ cpsid i /* 2. 配置CP15协处理器的SCTLR寄存器 */ mrc p15, 0, r0, c1, c0, 0 /* 读取SCTLR */ bic r0, r0, #(1 << 13) /* 清除bit13:使用VBAR设置异常向量表 */ orr r0, r0, #(1 << 12) /* 设置bit12:使能指令缓存 */ mcr p15, 0, r0, c1, c0, 0 /* 写回SCTLR */ /* 3. 设置IRQ模式的栈指针 */ cps #0x12 /* 切换到IRQ模式(模式编码0x12) */ ldr sp, =0x82000000 /* IRQ栈地址:0x82000000 */ /* 4. 设置SYS模式的栈指针 */ cps #0x1F /* 切换到SYS模式(模式编码0x1F) */ ldr sp, =0x84000000 /* SYS栈地址:0x84000000 */ /* 5. 重新使能IRQ中断 */ cpsie i /* 6. 初始化BSS段(清零未初始化的全局变量) */ bl _bss_init /* 7. 跳转到C语言main函数 */ b main /* BSS段初始化函数 */ _bss_init: ldr r0, =__bss_start /* BSS段起始地址(来自链接脚本) */ ldr r1, =__bss_end /* BSS段结束地址 */ mov r2, #0 /* 清零值 */ _bss_clear_loop: cmp r0, r1 /* 比较是否到达结束地址 */ bge _bss_clear_done /* 如果r0 >= r1,完成 */ str r2, [r0], #4 /* 清零4字节内存并地址加4 */ b _bss_clear_loop _bss_clear_done: bx lr /* 返回 */
2.3 IRQ中断处理程序
_irq_handler: /* 1. 调整返回地址(ARM架构要求) */ sub lr, lr, #4 /* 2. 保存现场:保存所有工作寄存器 */ stmfd sp!, {r0-r12, lr} /* 3. 获取GIC基地址并读取中断号 */ mrc p15, 4, r1, c15, c0, 0 /* 读取CBAR(GIC基地址) */ add r1, r1, #0x2000 /* CPU接口偏移 */ ldr r0, [r1, #0xC] /* 读取IAR寄存器获取中断号 */ /* 4. 保存中断号和GIC基地址 */ stmfd sp!, {r0, r1} /* 5. 切换到SYS模式调用C中断处理函数 */ cps #0x1F /* 切换到SYS模式 */ stmfd sp!, {lr} /* 保存SYS模式的lr */ bl system_interrupt_handler /* 调用C语言中断处理函数 */ ldmfd sp!, {lr} /* 恢复SYS模式的lr */ /* 6. 切换回IRQ模式 */ cps #0x12 /* 7. 恢复中断号和GIC基地址 */ ldmfd sp!, {r0, r1} /* 8. 发送EOI(中断处理结束) */ str r0, [r1, #0x10] /* 写入EOIR寄存器 */ /* 9. 恢复现场并返回 */ ldmfd sp!, {r0-r12, pc}^ /* ^表示恢复CPSR */第三部分:链接脚本详解
3.1 imx6ull.lds完整解析
SECTIONS { /* 程序入口地址 - IMX6ULL内部RAM起始地址 */ . = 0x87800000; /* 代码段 */ .text : { obj/start.o /* 确保启动代码在最前面 */ *(.text) /* 所有目标文件的.text段 */ } /* 只读数据段(4字节对齐) */ .rodata ALIGN(4) : { *(.rodata*) /* 所有只读数据 */ } /* 已初始化数据段(4字节对齐) */ .data ALIGN(4) : { *(.data) /* 所有已初始化全局变量 */ } /* BSS段(未初始化数据) */ __bss_start = .; /* 定义BSS开始符号 */ .bss ALIGN(4) : { *(.bss) /* 未初始化全局变量 */ *(COMMON) /* 公共块 */ } __bss_end = .; /* 定义BSS结束符号 */ }关键概念解释:
.text段:存放程序代码
.rodata段:存放只读数据(如字符串常量)
.data段:存放已初始化的全局变量
.bss段:存放未初始化的全局变量(运行时清零)
ALIGN(4):4字节对齐,提高访问效率
第四部分:中断系统实现
4.1 中断控制器GIC初始化
/* 在core_ca7.h中实现的GIC初始化 */ FORCEDINLINE __STATIC_INLINE void GIC_Init(void) { uint32_t i; uint32_t irqRegs; /* 1. 获取GIC基地址 */ GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL); /* 2. 获取中断寄存器数量 */ irqRegs = (gic->D_TYPER & 0x1FUL) + 1; /* 3. 禁用所有中断 */ for (i = 0; i < irqRegs; i++) gic->D_ICENABLER[i] = 0xFFFFFFFFUL; /* 4. 设置中断优先级 */ gic->C_PMR = (0xFFUL << (8 - __GIC_PRIO_BITS)) & 0xFFUL; /* 优先级掩码 */ gic->C_BPR = 7 - __GIC_PRIO_BITS; /* 二进制点寄存器 */ /* 5. 启用GIC分发器和CPU接口 */ gic->D_CTLR = 1UL; /* 启用分发器 */ gic->C_CTLR = 1UL; /* 启用CPU接口 */ }4.2 中断管理模块
/* interrupt.h - 中断管理头文件 */ #ifndef _INTERRUPT_H_ #define _INTERRUPT_H_ #include "MCIMX6Y2.h" /* 定义中断处理函数指针类型 */ typedef void (*irq_handler_t)(void); /* 全局中断向量表(160个中断源) */ extern irq_handler_t Vector_table[160]; /* 函数声明 */ extern void system_interrupt_init(void); extern int system_interrupt_register(IRQn_Type irq, irq_handler_t handler); extern void system_interrupt_handler(IRQn_Type irq); #endif/* interrupt.c - 中断管理实现 */ #include "interrupt.h" #include "core_ca7.h" /* 全局中断向量表 */ irq_handler_t Vector_table[160]; /* 中断系统初始化 */ void system_interrupt_init(void) { /* 1. 重新映射异常向量表到0x87800000 */ __set_VBAR(0x87800000); /* 2. 初始化GIC中断控制器 */ GIC_Init(); } /* 中断注册函数 */ int system_interrupt_register(IRQn_Type irq, irq_handler_t handler) { /* 参数检查 */ if (irq > PMU_IRQ2_IRQn || irq < IOMUXC_IRQn) return -1; /* 中断号超出范围 */ if (handler == NULL) return -2; /* 处理函数为空 */ /* 注册中断处理函数 */ Vector_table[irq] = handler; return 0; } /* 中断处理函数(由汇编调用) */ void system_interrupt_handler(IRQn_Type irq) { /* 查找并调用注册的中断处理函数 */ if (Vector_table[irq] != NULL) Vector_table[irq](); }第五部分:GPIO按键中断实现
5.1 按键初始化与中断配置
/* key.c - 按键驱动实现 */ #include "key.h" #include "MCIMX6Y2.h" #include "fsl_iomuxc.h" #include "core_ca7.h" #include "interrupt.h" #include "led.h" #include "beep.h" /* 按键中断处理函数 */ void key_irq_handler(void) { /* 1. 检查是否是GPIO1_18产生的中断 */ if ((GPIO1->ISR & (1 << 18)) != 0) { /* 2. 执行中断处理:切换LED和蜂鸣器状态 */ led_nor(); /* 切换LED状态 */ beep_nor(); /* 切换蜂鸣器状态 */ /* 3. 清除中断标志位(重要!) */ GPIO1->ISR |= (1 << 18); } } /* 按键初始化函数 */ void key_init(void) { /* 1. 配置GPIO引脚复用功能 */ IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0); /* 2. 配置引脚电气特性 */ IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF0B0); /* 3. 配置为输入模式 */ GPIO1->GDIR &= ~(1 << 18); /* 4. 配置GPIO中断触发方式(双边沿触发) */ GPIO1->ICR2 |= (3 << 4); /* 位[5:4]=11表示双边沿触发 */ /* 5. 使能GPIO中断 */ GPIO1->IMR |= (1 << 18); /* 解除中断屏蔽 */ /* 6. 在GIC中使能GPIO1中断 */ GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn); /* 7. 设置中断优先级 */ GIC_SetPriority(GPIO1_Combined_16_31_IRQn, 0); /* 8. 注册中断处理函数 */ system_interrupt_register(GPIO1_Combined_16_31_IRQn, key_irq_handler); } /* 按键轮询检测函数(备用) */ int key_check(void) { /* 读取按键状态:0表示按下,1表示释放 */ return ((GPIO1->DR & (1 << 18)) == 0); }5.2 LED和蜂鸣器驱动
/* led.c - LED驱动 */ #include "led.h" #include "MCIMX6Y2.h" #include "fsl_iomuxc.h" void led_init(void) { /* 配置GPIO1_03为输出模式 */ IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0); IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0); GPIO1->GDIR |= (1 << 3); led_off(); /* 初始状态为关闭 */ } void led_on(void) { GPIO1->DR &= ~(1 << 3); } /* 低电平点亮 */ void led_off(void) { GPIO1->DR |= (1 << 3); } /* 高电平熄灭 */ void led_nor(void) { GPIO1->DR ^= (1 << 3); } /* 翻转状态 *//* beep.c - 蜂鸣器驱动 */ #include "beep.h" #include "MCIMX6Y2.h" #include "fsl_iomuxc.h" void beep_init(void) { /* 配置GPIO5_01为输出模式 */ IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, 0); IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, 0x10B0); GPIO5->GDIR |= (1 << 1); beep_off(); /* 初始状态为关闭 */ } void beep_on(void) { GPIO5->DR &= ~(1 << 1); } /* 低电平响 */ void beep_off(void) { GPIO5->DR |= (1 << 1); } /* 高电平静音 */ void beep_nor(void) { GPIO5->DR ^= (1 << 1); } /* 翻转状态 */第六部分:主程序与构建系统
6.1 主程序main.c
#include "MCIMX6Y2.h" #include "fsl_iomuxc.h" #include "led.h" #include "beep.h" #include "key.h" #include "core_ca7.h" #include "interrupt.h" /* 时钟门控初始化 - 使能所有外设时钟 */ void clock_cg_init(void) { CCM->CCGR0 = 0XFFFFFFFF; CCM->CCGR1 = 0XFFFFFFFF; CCM->CCGR2 = 0XFFFFFFFF; CCM->CCGR3 = 0XFFFFFFFF; CCM->CCGR4 = 0XFFFFFFFF; CCM->CCGR5 = 0XFFFFFFFF; CCM->CCGR6 = 0XFFFFFFFF; } /* 简单延时函数 */ void g_delay(unsigned int t) { while(t--); } /* 主函数 */ int main(void) { /* 1. 初始化中断系统 */ system_interrupt_init(); /* 2. 使能所有外设时钟 */ clock_cg_init(); /* 3. 初始化各个硬件模块 */ led_init(); beep_init(); key_init(); /* 包含中断配置 */ /* 4. 主循环 */ while(1) { /* 模拟复杂任务处理 */ g_delay(0x7FFFF); /* 注意:按键检测不再需要轮询! * 中断会自动处理按键事件 */ } return 0; }6.2 Makefile构建系统
# 目标文件名 target = key # 交叉编译工具链 cross_compiler = arm-linux-gnueabihf- # 工具定义 cc = $(cross_compiler)gcc ld = $(cross_compiler)ld objcopy = $(cross_compiler)objcopy objdump = $(cross_compiler)objdump # 头文件目录 incdirs = bsp imx6ull include = $(patsubst %, -I%, $(incdirs)) # 源文件目录 srcdirs = bsp project # 自动查找所有源文件 cfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.c)) sfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.S)) # 去除目录路径 cfilenodir = $(notdir $(cfiles)) sfilenodir = $(notdir $(sfiles)) # 生成目标文件名 cobjs = $(patsubst %, obj/%, $(cfilenodir:.c=.o)) sobjs = $(patsubst %, obj/%, $(sfilenodir:.S=.o)) objs = $(cobjs) $(sobjs) # 设置VPATH让make自动查找源文件 VPATH = $(srcdirs) # 主目标:生成bin文件 $(target).bin : $(objs) $(ld) -Timx6ull.lds -o$(target).elf $^ $(objcopy) -O binary -S -g $(target).elf $@ $(objdump) -D $(target).elf > $(target).dis @echo "构建完成: $(target).bin" # 汇编文件编译规则 $(sobjs) : obj/%.o : %.S @mkdir -p obj $(cc) -Wall -nostdlib -c $(include) -o $@ $< # C文件编译规则 $(cobjs) : obj/%.o : %.c @mkdir -p obj $(cc) -Wall -nostdlib -c $(include) -o $@ $< # 清理规则 .PHONY : clean clean: rm -rf $(objs) $(target).elf $(target).bin $(target).dis obj/ @echo "已清理" # 烧录规则(需要根据实际情况调整) load: ./../imxdownload ./$(target).bin /dev/sdb第七部分:关键概念总结
7.1 中断处理流程
按键按下 ↓ GPIO检测到电平变化 ↓ GPIO设置中断标志位 ↓ GIC接收中断请求 ↓ GIC发送中断信号给CPU ↓ CPU保存现场,跳转到irq_handler ↓ 汇编代码获取中断号,调用C处理函数 ↓ key_irq_handler()执行:切换LED和蜂鸣器 ↓ 清除中断标志,恢复现场 ↓ 返回主程序继续执行
7.2 轮询 vs 中断对比
| 特性 | 轮询方式 | 中断方式 |
|---|---|---|
| 响应速度 | 慢(依赖轮询间隔) | 快(立即响应) |
| CPU占用 | 高(持续检查) | 低(事件驱动) |
| 实时性 | 差 | 优秀 |
| 复杂度 | 简单 | 较复杂 |
| 适用场景 | 简单应用 | 实时系统 |
7.3 调试技巧
LED辅助调试:在关键位置添加LED状态变化
串口输出:添加串口打印调试信息
逐步验证:
先验证GPIO基本功能
再验证中断触发
最后验证中断处理
7.4 常见问题
中断不触发:检查GIC配置、GPIO中断使能
中断频繁触发:检查消抖处理、中断标志清除
系统卡死:检查中断服务函数执行时间、栈溢出
中断处理不执行:检查中断向量表、VBAR设置
第八部分:从0开始的开发步骤
8.1 环境搭建
安装ARM交叉编译工具链
准备IMX6ULL开发板
连接串口调试工具
8.2 代码编写顺序
编写Makefile和链接脚本
编写启动代码start.S
实现基本的LED驱动(验证环境)
实现GIC和中断管理框架
实现按键中断驱动
集成所有模块测试
8.3 测试验证
# 1. 编译 make clean make # 2. 烧录到SD卡 make load # 3. 观察现象 # - 按下按键:LED和蜂鸣器状态切换 # - 主循环中的延时不影响按键响应