极术小能手 · 2022年03月17日

【课程实验 LAB4】如何召唤"沉睡的软件":使用矩阵键盘控制数码管显示

实验前请点击获取配套资料

关于LAB4

  • 本实验对应实验指导书的第 7 章 "数据存储器与流水灯外设"
  • 本实验将以键盘模块实验为基础, 了解 CPU 的中断处理以及使用 C 语言高效地编程.
  • 实验过程中遇到不明白的地方, 强烈建议回看实验指导书第三章中关于 Cortex-M0 相关的异常与中断的处理过程.

使用矩阵键盘控制数码管显式

在这次试验中, 我们需要把矩阵键盘的全部 16 个按键连接到 IRQ0 端口上, 当任何一个按键被按下时, CPU 进入中断处理程序, 读取按键信息, 根据按下的按键编号在数码管上显示对应的数值.

外设简介

数码管显示的具体代码见工程文件夹下 "Segdisp.v、seg_led_decoder.v、seg_sel_decoder.v" 文件.

由于所有按键连接到同一个中断端口, 因此键盘外设除了基本的消抖检测模块以外, 还需要使用一个寄存器组将被按下的键盘的序号记录下来. 本节用到了矩阵键盘的全部按键, 因此不能像上一节中为了使用某一行的 4 个按键, 将该行的行信号线拉高. 为了实现对矩阵键盘全部 16 个按键的检测, 需要进行扫描.

由于 FPGA 板载时钟频率为 50MHz, 所以还需要先对时钟信号分频得到扫描时钟信号, 按扫描时钟信号周期性地改变行信号线的值. 当第一行行信号线为低电平时, 此时检测列信号的值便可判断出是第一行的某个按键被按下. 以此类推, 使用这种扫描方法可以检测矩阵键盘 16 个按键中是哪一个按键被按下 (扫描模块的代码可以参考实验指导书).

除了扫描模块以外, 键盘模块还包括消抖模块和记录按键信息的寄存器组 (具体代码可以参考实验指导书).

最后实现的键盘外设模块中, 把每个按键的脉冲信号 key_pulse 通过和运算以后得到键盘中断信号key_interrupt. 只要有一个按键被按下, 中断信号都将产生一个脉冲, 使CPU进入中段处理程序.

提示
可以看出, 本节介绍的方法中中断信号和上一节中的有很大不同, 应当认真体会!

SoC硬件部分

上一节中按键模块的中断信号为 4 位 key_interrupt 信号, 本节中该信号为 1 位, 因此在 CortexM0_SoC.v 文件做如下修改.

wire [31:0] IRQ;
wire [3:0] key_interrupt;
/*Connect the IRQ with keyboard*/
assign IRQ = {28'b0,key_interrupt};
/***************************/

改为:

wire [31:0] IRQ;
wire key_interrupt;
/*Connect the IRQ with keyboard*/
assign IRQ = {31'd0,key_interrupt};
/***************************/

然后在上一节实验的 vivado 约束文件的基础上添加对数码管的约束.

启动代码与 C 编程

由于中断信号的变化, 我们需要对启动代码进行修改. 上一节中已经介绍过 _main 函数和 __user_initial_stackheap 函数, 在本节中我们只需要修改中断向量表与中断复位函数即可.

修改__Vector中断向量表如下:

__Vectors     DCD     __initial_sp              ; Top of Stack
                DCD     Reset_Handler             ; Reset Handler
                DCD     0                           ; NMI Handler
                DCD     0                          ; Hard Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                           ; SVCall Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                        ; PendSV Handler
                DCD     0                          ; SysTick Handler
                DCD     Keyboard_Handler        ; IRQ0 Handler

现在只有一个中断复位函数, 因此还需要修改中断复位函数的入口:

KEY0_Handler    PROC
                EXPORT KEY0_Handler            [WEAK]
               IMPORT KEY0
               PUSH    {R0,R1,R2,LR}
                BL        KEY0
               POP        {R0,R1,R2,PC}
                ENDP

KEY1_Handler    PROC
                EXPORT KEY1_Handler            [WEAK]
               IMPORT KEY1
               PUSH    {R0,R1,R2,LR}
                BL        KEY1
               POP        {R0,R1,R2,PC}
                ENDP

KEY2_Handler    PROC
                EXPORT KEY2_Handler            [WEAK]
               IMPORT KEY2
               PUSH    {R0,R1,R2,LR}
                BL        KEY2
               POP        {R0,R1,R2,PC}
                ENDP

KEY3_Handler    PROC
                EXPORT KEY3_Handler            [WEAK]
               IMPORT KEY3
               PUSH    {R0,R1,R2,LR}
                BL        KEY3
               POP        {R0,R1,R2,PC}
                ENDP

改为:

Keyboard_Handler    PROC
                    EXPORT Keyboard_Handler            [WEAK]
                   IMPORT KEY_ISR
                   PUSH    {R0,R1,R2,LR}
                    BL        KEY_ISR
                   POP    {R0,R1,R2,PC}
                    ENDP

然后, 我们需要定义外设的地址, 以及自己实现的函数, 参考 CMSIS 编写自己头文件. 具体代码见

"/Task5/keil/code_def.h".

#include <stdint.h>

//INTERRUPT DEF
#define NVIC_CTRL_ADDR (*(volatile unsigned *)0xe000e100)

//KEYBOARD DEF
#define Keyboard_keydata_clear (*(volatile unsigned *)0x40000000)

//SEGDISP DEF
#define Segdisp_data (*(volatile unsigned *)0x40000010)

void KEY_ISR(void);

其中 Keyboard_keydata_clear 即连到之前介绍的键盘模块中的 clear 端口, 用于按键信息寄存器组的清零. Segdisp_data 为数码管上需要显示的数据. 中断处理函数 KEY_ISR 将变量 key_flag 的值改为 1, 用于 C 语言程序中判断是否有按键被按下产生了中断, 具体见

"/Task5/keil/keyboard.c".

void KEY_ISR(void)
{
    key_flag = 1;
}

最后, 编写主函数文件, 具体见 "/Task5/keil/main.c", 需要注意的是, 在中断启动之前, 我们需要使能所用到的中断.

#include "code_def.h"
#include <string.h>
#include <stdint.h>

extern uint32_t key_flag;

int main()
{   
    NVIC_CTRL_ADDR    = 1;

    while(1){
          while(!key_flag);
            uint32_t din;
            din = Keyboard_keydata_clear;
            int i = 0;
            int ans = 0;
            for (i = 0; i < 16; i++) {
                if ((din >> i) & 1) {
                     ans = i;
                    Segdisp_data = 16 + ans; //enable
                    break;
                }
            }
        key_flag = 0;
        Keyboard_keydata_clear = 1;
    }
}

当任一按键被按下后, 键盘模块会产生中断信号, CPU 进入中断处理程序, key_flag 的值变为 1. 此时, main 函数中内层 while 循环被跳过, 开始读取键盘的值. 键盘寄存器组的值被读到变量 din 中, 接着用一个 for 循环找出哪一位为 1, 记录在变量 ans 中, 同时向数码管外设写值 16+ans, 跳出循环. 因为数码管模块中 5 位输入数据最高位是使能信号 (表示数据输入是否有效), 因此在写入低四位的数据基础上, 还需要加上 16, 让使能信号有效. 最后就是在每次循环处理完成之后将 key_flag 等变量以及键盘外设的寄存器组清零.

调试与运行结果

将相关的 Verilog 文件添加到 Vivado 工程中, 将 Vivado 生成的比特流 (bitstream) 文件下载到 FPGA 开发板上, 使用 Keil 调试. 在按下按键时, 可以看到数码管上显示了按键编号 (最左边的数码管), 之前的显示结果右移一位.

END

文章来源:

推荐内容

更多内容请关注微处理器系统结构与嵌入式系统设计专栏
推荐阅读
关注数
117
内容数
20
电子科技大学示范性微电子学院开设的「微处理器系统结构与嵌入式系统设计」课程配套实验,原链接:[链接]
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息