随笔

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

概要

本实验设计基于正点电子 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电平同时翻转,不知道会出现什么情况。

时序图
网上随便找的一个时序图

(0)

本文由 橙叶博客 作者:FrankGreg 发表,转载请注明来源!

热评文章

发表评论