009.中断实践之实现按键测试|千篇笔记实现嵌入式全栈/裸机篇

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,烧录,运行,

依次按下所有按键,

相关推荐
披着羊皮不是狼2 小时前
将Ubuntu从C盘移动到D盘
linux·运维·ubuntu
无聊大侠hello world2 小时前
Yu-AI-Agent 项目(AI 恋爱大师智能体) · 学习笔记
人工智能·笔记·学习
Proxy_ZZ02 小时前
从零实现LDPC比特翻转译码器:C语言实战与底层逻辑解析
c语言·算法
XINVRY-FPGA2 小时前
XC7VX690T-2FFG1157I Xilinx AMD Virtex-7 FPGA
arm开发·人工智能·嵌入式硬件·深度学习·fpga开发·硬件工程·fpga
l2ohvef2 小时前
Windows 7 虚拟机 VMware Tools 安装失败:无法自动安装VSock 驱动程序
linux·运维·服务器
嵌入式×边缘AI:打怪升级日志2 小时前
从零开始写Linux字符设备驱动:一个不操作硬件的Hello驱动
linux·运维·数据库
CheerWWW2 小时前
C++学习笔记——箭头运算符、std::vector的使用、静态链接、动态链接
c++·笔记·学习
Benszen2 小时前
Ansible自动化运维实战
linux·运维·自动化·ansible
ZhiqianXia2 小时前
Pytorch 学习笔记(17):decompositions.py —— 算子分解的百科全书
pytorch·笔记·学习