STM32 综合实验 拆弹指令 实验报告(19 广电工 GYS)

Posted by 橙叶 on Sun, Dec 15, 2019

概要

本实验设计基于正点电子 STM32 NANO 开发板,利用中断机制进行定时、检测按键动作。

设计任务

设计实验版的软件,包括以下功能

  1. 系统上电后,LED数码管上,显示剩余时间 29.9 ,表示 炸弹剩余的爆炸时间
  2. 每经过0.1秒,数码管上倒计时显示减少0.1
  3. 当倒计时达到 00.0 秒时,在LED数码管上绘制一个动态图样表示炸弹爆炸
  4. 在倒计时的过程中,如果用户在KEY0和KEY1上,按下了如下按键序列,则倒计时停止
  5. 按键序列为,KEY0,KEY0,KEY1,KEY1,KEY0,KEY1,KEY0,KEY1,当测到正确的序列时,倒计时的计数,应该停下来,清晰的显示剩余时间

设计思路

开发板已经提供了一个8位数码管、5枚按键(KEY0~KEY2、KEY_UP,本实验只用到KEY0和KEY1)、8颗LED(LED0~LED7)

因为本次实验需要控制数码管按时间变化,同时需要随时读取按键状态,所以使用CPU的中断机制完成本实验。

  1. 设置中断,分别设置定时器中断和外部中断。根据实验要求“每经过0.1秒,数码管上的倒计时显示减少0.1”需要考虑计时的精确度,所以应该使用定时器中断;如果把控制数码管的逻辑写到main函数的while(1)循环中,每次循环delay一定时间,就很难确保每次更新数字的间隔是0.1s。
  2. 数码管显示函数,首先使用数码管段码查询工具找到0~9所对应的段码,并且可以发现如果要显示小数点,只需要段码+1。因为实验不要求修改时间,所以可以直接用一个长度为3的数组,索引从0到2依次是十位、个位、十分位。最初打算直接对数组做运算,后来发现先转换成一个三位整数,然后-1,最后把每一位放回数组即可。

实验原理

定时器中断原理

计数器起到了分频的作用。 以下图为例,分频系数psc为9,计数器对输入的时钟信号进行计数,每个CLK脉冲计数器+1,直至达到设置的最大值9,溢出信号OV为1,每十个时钟周期输出一次OV=1,相当于将CLK的频率分成了原来的1/10。

定时器

同时,使用脉冲信号计数器对OV信号计数,当达到自动装载值(arr)时产生一个中断事件,所以定时的间隔为: $$\frac{psc+1}{\rm 72Mhz}\times(arr+1)$$ 在执行中断处理函数时,计数器仍然在不断工作,因为在触发中断后通过使能使得中断处理函数不会被自己打断,所以只要中断处理函数执行时间少于2ms,计时就是准确的。

移位寄存器和数码管

移位寄存器:每个SCK的上升沿,数据向右移动一位。 移位寄存器

用到的函数:

  • LED_Write_Data:将8bit的串行段选信息按一定时序输入到移位寄存器中(段选)。
  • LED_Wei:使用3-8译码器选择其中一位。
  • LED_Refresh:用UPDATE信号将数据锁存到数据缓冲寄存器中(位于上方的寄存器)。

软件设计

中断初始化

中断初始化有两部分:定时器中断和外部中断,此处直接在正点电子的实验例程上做修改

// 定时器中断函数 @timer.c
void TIM3_Init(u16 arr,u16 psc)
{  
    TIM3_Handler.Instance=TIM3;                          // 通用定时器3
    TIM3_Handler.Init.Prescaler=psc;                     // 分频系数
    TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP;    // 向上计数器
    TIM3_Handler.Init.Period=arr;                        // 自动装载值
    TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;// 时钟分频因子
    TIM3_Handler.Init.
    ReloadPreload = TIM_
    RELOAD_PRELOAD_ENABLE;// 使能自动重载
    HAL_TIM_Base_Init(&TIM3_Handler);

    HAL_TIM_Base_Start_IT(&TIM3_Handler); // 使能定时器3更新中断 TIM_IT_UPDATE   
}

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
    if(htim->Instance==TIM3)
    {
        __HAL_RCC_TIM3_CLK_ENABLE();            // 使能TIM3时钟
        HAL_NVIC_SetPriority(TIM3_IRQn,1,3);    // 设置中断优先级,响应优先级1,子优先级3
        HAL_NVIC_EnableIRQ(TIM3_IRQn);          // 开启TIM3中断
    }
}
// 外部中断初始化 @exit.c
void EXTI_Init(void)
{
    GPIO_InitTypeDef GPIO_Initure;

    __HAL_RCC_GPIOC_CLK_ENABLE();               // 开启GPIOC时钟

    GPIO_Initure.Pin=GPIO_PIN_8|GPIO_PIN_9;     // PC8、PC9
    GPIO_Initure.Mode=GPIO_MODE_IT_FALLING;     // 下降沿触发
    GPIO_Initure.Pull=GPIO_PULLUP;              // 上拉
    HAL_GPIO_Init(GPIOC,&GPIO_Initure);     

    // 中断线8、9-PC8、9
    // 因为外部中断回调函数执行时间超过2ms,保证计时准确,应该将外部中断设为低优先级
    HAL_NVIC_SetPriority(EXTI9_5_IRQn,3,0);     // 抢占优先级为3,子优先级为0
    HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); 
}

引用文件

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "smg.h"
#include "timer.h"
#include "key.h"
#include "exti.h"

变量部分

段码与数字映射表 SMG_num_map

c u8 SMG_num_map[] = {0xfc,0x60,0xda,0xf2,0x66,0xb6,0xbe,0xe0,0xfe,0xf6}; // 对应的数字 0 1 2 3 4 5 6 7 8 9 将数字作为索引即可取得对应的段码。在获得的段码的基础上+1可以显示小数点。

剩余时间

用一个3个元素的数组表示剩余的时间

u8 display_queue[] = {2,9,9};

全局状态变量

  1. timing: 表示是否还在倒计时,初始为1,当输入正确的按键序列或计时结束后设为0
  2. boom:炸弹是否已爆炸,初始为0,当计时结束后设为1,控制数码管的函数检测到后播放爆炸动画。

计时变量t1,t2

  • t1用于每0.1刷新数码管显示的时间

  • t2用于控制炸弹爆炸动画每一帧的持续时间

预设按键序列和指针

u8 passcode[] = {0,0,1,1,0,1,0,1};
u8 pointer = 0;

passcode为正确的案件序列,每按下一次按键,将该按键的值与pointer指针所指向元素比较,相同则pointer+1,不同则回退指针到0。

main()函数

分别初始化LED、数码管、定时器、外部中断。

int main(void)
{

    HAL_Init();                     // 初始化HAL库    
    Stm32_Clock_Init(RCC_PLL_MUL9); // 设置时钟,72M
    delay_init(72);                 // 初始化延时函数
    uart_init(115200);              // 初始化串口
    LED_Init();                     // 初始化LED
    LED_SMG_Init();                 // 初始化数码管   
    TIM3_Init(19,7199);             // 初始化定时器,自动装载值为20-1,分频系数为7200-1
    EXTI_Init();                                        //³õʼ»¯ÍⲿÖжÏ
    while(1){   
    }
}

其他函数

reduce_time:设置倒计时函数

每调用一次reduce_time()函数, 若倒计时未结束, 数码管应显示时间-0.1。 若倒计时结束, 将boom设为1,timing设为0,即炸弹爆炸,计时停止。

u8 display_queue[] = {2,9,9};
u8 t2=0,t1=0;
void reduce_time(){
    // 将display_queue数组转换为整数
    int num = 100*display_queue[0] + 10*display_queue[1] + display_queue[2];
    num-=1;  // 数字-1
    if(num+1){ 
        // 重设display_queue
        display_queue[0] = num/100;
        display_queue[1] = num%100/10;
        display_queue[2] = num%10;
    }else{
        // 设置timing 为 0 ,即停止计时,boom=1,炸弹爆炸
        timing = 0;
        boom = 1;
        t2 = 0;
    }
}

HAL_TIM_PeriodElapsedCallback:计时器中断回调函数

当发生定时器中断时,此函数被调用。每2ms被调用一次

//定时器中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    u8 i = 0;
    if(htim==(&TIM3_Handler))
    {
            if(!boom)
                for(i = 0;i<3;i++){
                    // 显示数码
                    LED_Write_Data(SMG_num_map[display_queue[i]] + (1==i),i);
                    LED_Refresh();
                }
            else{
                // 绘制爆炸动画
                Go_Boom();
            }
            LED_Write_Data(0x00,2);
            LED_Refresh();
        t1++;
        if(t1==50) //每100ms刷新一次
        {
            // 测试计时是否结束
            if(timing) reduce_time();
            t1=0;
        }
    }
}

HAL_GPIO_EXTI_Callback:外部中断回调函数

// 正确的按键序列
u8 passcode[] = {0,0,1,1,0,1,0,1};
// 指针,指向下一按键的正确值
u8 pointer = 0;
// 当按下的按键正确时,对应位置的LED点亮
u8 LED = 0x0;
// 在终端服务程序中需要做的事情
// 在HAL库中所有外部中断都会调用此函数
// GPIO_Pin:中断引脚号
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    u8 t=0;
    delay_ms(10);       // 消抖
    switch(GPIO_Pin)
    {
        // 检查按下的按键
        case GPIO_PIN_8:
                    if(KEY0==0)
                    {
                        t = 1;
                    }
                    break;
                case GPIO_PIN_9:
                    if(KEY1==0){
                        t = 2;
                    }
                    break;
                default:
                    t = 0;
    }
    if(t){
        if(passcode[pointer]==t-1){
            LED++;
            // 按键正确,点亮对应的LED灯
            HAL_GPIO_WritePin(GPIOC, LED, GPIO_PIN_RESET);
            LED<<=1;
            if(pointer<7) pointer++;  // 指针右移一位
            else{
                timing = 0;
                delay_ms(500);
                // 按键序列全部正确,所有LED熄灭
                HAL_GPIO_WritePin(GPIOC, 0xff, GPIO_PIN_SET);
                // printf("Mission completed!");
            }
        }else{
            // LED闪烁效果,表示密码错误
            HAL_GPIO_WritePin(GPIOC, 0xff, GPIO_PIN_SET);
            delay_ms(500);
            HAL_GPIO_WritePin(GPIOC, 0xff, GPIO_PIN_RESET); 
            delay_ms(500);
            HAL_GPIO_WritePin(GPIOC, 0xff, GPIO_PIN_SET);
            // 回退指针
            pointer = 0;
            LED = 0x00;
        }
    }
}

效果: 按键

Go_Boom:在数码管上绘制爆炸动画

// 用于控制每帧动画的连续变化
u8 offset=0;
// 绘制爆炸图像
void Go_Boom(){
    u8 i = 0;
    for(i=0;i<offset;i++){ 
        if(i==0){
            LED_Write_Data(0x60, i+4);
            LED_Refresh();
            LED_Write_Data(0x0c, 3-i);
            LED_Refresh();
        }
        else{ 
            LED_Write_Data(0x9c, 3-i);
            LED_Refresh();
            LED_Write_Data(0xf0, i+4);
            LED_Refresh();
        }
    }
    if(t2==200){
        if(offset<4) offset++;
        else boom = 0;
        t2 = 0;
    }
    t2++;
}

效果:

爆炸效果

改进空间

由于时间仓促,经验不足,所以本实验还有许多改进空间。

  • 按键抖动 软件中已有防抖处理(触发中断后delay_ms(10)) ,但是仍然容易出现抖动的情况。

疑问

在实验过程中,有一些不解的地方。

  • 例程中将外部中断线设置为上拉、下降沿触发,这样的话按下按键后,得到KEY=0;修改为下拉、上升沿触发之后却无法触发中断。
  • 关于移位寄存器中的边沿D触发器,当时钟信号翻转时,Q和时钟信号的翻转是否同步?如果同步,如何保证后面几位的值正确传递,但是看时序图的话是同步的,如果按时序图的话,后面一个D触发器的CLK和D电平同时翻转,不知道会出现什么情况。
时序图 网上随便找的一个时序图

comments powered by Disqus