1. 要做什么?
MINI2440上有6个按键,我们要做一个测试程序,当按下按键的时候,串口打印对应的按键按下的消息。
要包含中断的知识,就要用中断的方法去实现,
为了使用中断实现,我们需要搭建一个完整的中断流程框架(中断异常的现场保护、返回地址计算、中断分发、基础操作API实现等)
2. 具体思路
测试程序和中断框架的两条流程设计:

代码框图:

3. 怎么实现?
3.1 框架层面
3.1.1 irq_handler.c
创建common/irq_handler.c文件,
实现irq框架初始化,
C
/// @brief 中断控制器初始化
/// 设置IRQ模式的SP, 禁用IRQ/FIQ
void irq_handler_init()
{
unsigned long cpsr;
__asm__ volatile (
// 读取cpsr
"mrs %0, cpsr\n\t"
// 禁用IRQ/FIQ, 切换至IRQ模式
"msr cpsr_c, #0b11010010\n\t"
// 设置IRQ模式的SP
"ldr sp, =0x40001000\n\t"
// 恢复原来模式和原始中断状态
"msr cpsr, %0\n\t"
: "=&r" (cpsr)
:
: "sp", "cc", "memory"
);
// 清除可能的中断挂起
SUBSRCPND = SUBSRCPND;
SRCPND = SRCPND;
INTPND = INTPND;
// 屏蔽所有中断
INTMSK = 0xFFFFFFFF;
INTSUBMSK = 0xFFFFFFFF;
}
实现irq处理函数,作用是中断分发,
C
// 默认中断处理函数 (弱实现,用户可重写)
void __attribute__((weak)) irq_eint0_handler(void) { }
void __attribute__((weak)) irq_eint1_handler(void) { }
void __attribute__((weak)) irq_eint2_handler(void) { }
void __attribute__((weak)) irq_eint3_handler(void) { }
void __attribute__((weak)) irq_eint4_7_handler(void) { }
void __attribute__((weak)) irq_eint8_23_handler(void) { }
// ...略...
// 中断处理函数表
static irq_handler_t irq_handlers[32] = {
[IRQ_EINT0] = irq_eint0_handler,
[IRQ_EINT1] = irq_eint1_handler,
[IRQ_EINT2] = irq_eint2_handler,
[IRQ_EINT3] = irq_eint3_handler,
[IRQ_EINT4_7] = irq_eint4_7_handler,
[IRQ_EINT8_23] = irq_eint8_23_handler,
// ...略...
};
/// @brief IRQ中断处理函数
/// 中断分发
void __irq_handler()
{
// 获取中断源偏移
uint32_t offset = INTOFFSET;
if (offset >= 32 || NULL == irq_handlers[offset])
return ;
// 清除对应的中断标志位
SRCPND = (1 << offset);
INTPND = (1 << offset);
// 分发
irq_handlers[offset]();
}
我们使用了weak函数技巧,实现了所有中断处理函数的存根,然后应用层根据需要,覆盖实现对应的处理函数~
还要实现一些公共的API,
C
/// @brief CPU中断使能控制
void irq_enable(bool enable)
{
unsigned long old_cpsr = disable_irq_save();
if (!enable)
old_cpsr |= (1 << 7);
else
old_cpsr &= ~(1 << 7);
restore_irq_mask(old_cpsr);
}
/// @brief 恢复IRQ Mask
void restore_irq_mask(unsigned long old_cpsr)
{
__asm__ volatile (
"msr cpsr, %0\n\t"
:
: "r" (old_cpsr)
: "cc", "memory"
);
}
/// @brief 禁用IRQ并保存当前cpsr
unsigned long disable_irq_save()
{
unsigned long old_cpsr;
__asm__ volatile (
/* 读取cpsr */
"mrs %0, cpsr\n\t"
/* 禁止IRQ */
"orr r1, %0, #(1 << 7)\n\t"
"msr cpsr_c, r1\n\t"
: "=&r" (old_cpsr)
:
// 这里损坏部声明了r1, 表示我弄脏了r1, 这样编译器会将r1自动压栈保护
: "cc", "memory", "r1"
);
return old_cpsr;
}
/// @brief 中断源使能控制
void irq_src_enable(irq_src_t src, bool enable)
{
unsigned long _save = disable_irq_save();
SRCPND = (1 << src);
INTPND = (1 << src);
if (enable) {
INTMSK &= ~(1 << src);
} else {
INTMSK |= (1 << src);
}
restore_irq_mask(_save);
}
我们用了大量的的内联汇编技巧,可以参考章节:
007.GNU C内联汇编杂谈|千篇笔记实现嵌入式全栈/裸机篇
3.1.2 start.s
回到common/start.s,进行修改,
注释掉原来的irq: b ., 重新实现irq段,
并且设置cpsr确保处于SVC模式,然后调整SVC模式下的栈顶,为IRQ模式预留512字节栈空间,
C
@irq: b .
fiq: b .
@ irq现场保护和恢复
irq:
@ 修正返回地址:
@ ARM9流水线取指 (Fetch) -> 译码 (Decode) -> 执行 (Execute) -> 访存 (Memory) -> 回写 (Write)
@ LR(异常触发时PC) = 触发点PC + 8
@ 那么目标返回点PC = 触发点PC + 4 = (LR - 8) + 4 = LR - 4
sub lr, lr, #4
@ 压栈保护通用寄存器和lr
stmdb sp!, {r0-r12, lr}
@ 跳转至C分发函数
bl __irq_handler
@ 恢复现场退出中断
@ ^表示将SPSR>CPSR, 实现模式切换
ldmia sp!, {r0-r12, pc}^
.section .text
.global _start
_start:
@ 关闭看门狗
ldr r0, =WTCON
ldr r1, =0
str r1, [r0]
@ 关中断且确保处于SVC模式
msr cpsr_c, 0b11010011
@ 设置栈顶(给IRQ SP预留512空间)
ldr sp, =0x40000E00
3.2 应用方面
看原理图,

为了与常规轮询方式对比,我们选择两路按键使用轮询的方式,四路按键使用外部中断的方式,
C
/**
* @file key.c
* @brief 按键测试
* 部分采用轮询, 部分采用中断
* @pins:
* KEY1(轮询): EINT8/GPG0
* KEY2(轮询): EINT11/GPG3
* KEY3(中断): EINT13/GPG5
* KEY4(中断): EINT14/GPG6
* KEY5(中断): EINT15/GPG7
* KEY6(中断): EINT19/GPG11
*/
新建key/main.c,
实现main函数,用于配置和逻辑处理,
C
int main()
{
// 配置为INPUT
GPIO_SET_MODE(GPIOGCON, 0, 0b00, 2);
GPIO_SET_MODE(GPIOGCON, 3, 0b00, 2);
// 配置为EINT
GPIO_SET_MODE(GPIOGCON, 5, 0b10, 2);
GPIO_SET_MODE(GPIOGCON, 6, 0b10, 2);
GPIO_SET_MODE(GPIOGCON, 7, 0b10, 2);
GPIO_SET_MODE(GPIOGCON, 11, 0b10, 2);
// 上拉
GPIO_SET_PULLUP(GPIOGUP, 0, 0);
GPIO_SET_PULLUP(GPIOGUP, 3, 0);
GPIO_SET_PULLUP(GPIOGUP, 5, 0);
GPIO_SET_PULLUP(GPIOGUP, 6, 0);
GPIO_SET_PULLUP(GPIOGUP, 7, 0);
GPIO_SET_PULLUP(GPIOGUP, 11, 0);
// 外部中断配置 EINT13/14/15/19
// 下降沿触发
EXTINT1 &= ~((7 << 20) | (7 << 24) | (7 << 28));
EXTINT2 &= ~(7 << 12);
EXTINT1 |= (0b010 << 20) | (0b010 << 24) | (0b010 << 28);
EXTINT2 |= (0b010 << 12);
// 外部中断屏蔽
// 屏蔽其它所有外部中断
EINTMASK = ~((1 << 13) | (1 << 14) | (1 << 15) | (1 << 19));
// 使能EINT8_23中断
irq_src_enable(IRQ_EINT8_23, true);
// 使能CPU中断
irq_enable(true);
bool _key1_st_old = false;
bool _key2_st_old = false;
int count_for_key1 = 0;
int count_for_key2 = 0;
while (1) {
// 10ms周期
easy_delay_ms(10);
count_ref ++;
if (event_key3) {
uart0_printf("KEY3: pressed\n");
event_key3 = false;
}
if (event_key4) {
uart0_printf("KEY4: pressed\n");
event_key4 = false;
}
if (event_key5) {
uart0_printf("KEY5: pressed\n");
event_key5 = false;
}
if (event_key6) {
uart0_printf("KEY6: pressed\n");
event_key6 = false;
}
// 轮询检测KEY1/KEY2
// 100ms去抖
bool key1_st = GPIO_GET_VALUE(GPIOGDAT, 0) ? true : false;
bool key2_st = GPIO_GET_VALUE(GPIOGDAT, 3) ? true : false;
if (key1_st != _key1_st_old) {
if (++count_for_key1 > DEBOUNCE_COUNT) {
if (!(_key1_st_old = key1_st))
uart0_printf("KEY1: pressed\n");
count_for_key1 = 0;
}
} else {
count_for_key1 = 0;
}
if (key2_st != _key2_st_old) {
if (++count_for_key2 > DEBOUNCE_COUNT) {
if (!(_key2_st_old = key2_st))
uart0_printf("KEY2: pressed\n");
count_for_key2 = 0;
}
} else {
count_for_key2 = 0;
}
}
return 0;
}
然后,
覆盖实现中断源处理函数,就是通过外部中断挂起寄存器来判断是哪里按键触发,
C
///@brief EINT8_23 中断处理函数
void irq_eint8_23_handler(void)
{
unsigned long _pend = EINTPEND;
EINTPEND = _pend; // 清除中断标志
if (_pend & (1 << 13)) {
if ((count_ref - count_for_key3) > DEBOUNCE_COUNT_EINT) {
event_key3 = true;
count_for_key3 = count_ref;
}
}
if (_pend & (1 << 14)) {
if ((count_ref - count_for_key4) > DEBOUNCE_COUNT_EINT) {
event_key4 = true;
count_for_key4 = count_ref;
}
}
if (_pend & (1 << 15)) {
if ((count_ref - count_for_key5) > DEBOUNCE_COUNT_EINT) {
event_key5 = true;
count_for_key5 = count_ref;
}
}
if (_pend & (1 << 19)) {
if ((count_ref - count_for_key6) > DEBOUNCE_COUNT_EINT) {
event_key6 = true;
count_for_key6 = count_ref;
}
}
}
完整的代码,见当前TAG:9-isr-key
4. 效果
编译,make key,
生成key.bin,烧录,运行,
依次按下所有按键,
