STM32 实验 实现一个计时器

Posted by 橙叶 on Sun, Sep 27, 2020

STM32 定时器实验

概要

本实验设计基于正点电子STM32 NANO开发板,利用中断机制进行定时,可以正向、反向计时,计时结束后有灯光、声音提醒,可以通过按键、红外遥控进行控制、设定时间。

使用方法

基本使用

将软件下载到开发板中,开发板显示初始计时起点。

数码管上前7位都可做时间显示,以右下角红点分割时、分、秒、0.1秒。

最右边一位以字母的形式指示计时器状态:

  • d:倒计时(Decrease)
  • i:正计时(Increase)
  • s:设置(Setting)

按下KEY1或遥控器上的播放键,计时开始,数码管最后一位闪动。再次按下后计时暂停,停止闪动。

按下KEY_UP或遥控器上的电源键,计时回到初始状态

按下KEY_0或遥控器上的右方向键切换到正向计时,按下KEY_2或遥控器上的左方向键切换到倒计时。

设置

正计时终点/倒计时起点

按下遥控器上的ALIENTEK键,进入时间设置,此时可以对每一位的数字进行设置,使用VOL-和VOL+选择数字,相应位置的数字会闪动。此时按下任意数字键,或按上、下键即可调整数字大小,再次按下删除键退出设置。

其他设置

按下遥控器上的删除键,进入其他设置,再次按下删除键进入下一项设置,直到退出。

目前可以设置的项有

  1. 计时步长:数码管显示rang x,按动遥控器上的上下方向键可调整值
  2. 蜂鸣器:数码管显示bEEP x,x为0或1,当x显示为0时,蜂鸣器静音,计时结束时只有灯光效果;当x显示为1时,计时结束时将伴随蜂鸣器声音。

设计思路

理想的计时步长是100ms~1h,

使用TIM4定时器中断,定时为2ms,每次调用回调函数时,刷新数码管显示,检查接收到的红外信号、检查是否应该更新时间值。

中断设计

实验总共用到了三个中断:按键外部中断,红外外部中断,定时器中断。

适合的中断优先级应当是外部中断>定时器中断,这是为了保证能正确捕捉红外输入。考虑到需要由红外中断引起的操作都会重置/启停计时,所以不需要考虑外部中断对计时精度的影响。

定时器中断

使用TIM4,抢占优先级设为1,子优先级3

HAL_NVIC_SetPriority(TIM4_IRQn,1,3);

红外中断

使用TIM3输入捕获来测量脉宽,抢占优先级0,子优先级4:

HAL_NVIC_SetPriority(TIM3_IRQn,0,4); 

输入捕获测量脉宽的原理:

  1. 假设对于一个高电平,首先设置定时器通道上升沿捕获
  2. 第一次捕获(到上升沿)后,将CNT值清零,重新配置为定时器通道为下降沿捕获,等待第二次捕获
  3. 第二次捕获时得到此时的CNT,即为高电平的持续时间

中间要处理因为定时器溢出而导致的寄存器自动清零的情况。

时间步长控制

程序开始运行时,会根据SEGSDROP计算出应该采取的时间步长,即更新数码管显示的数字的时间间隔,这样有利于节省CPU的资源。

int step = 0; 
// 初始化步进量
int Set_Step(){
    u8 drop = DROP;
    // 从DROP的位数计算最小步进、
    // 例如,DROP=0 100ms步进
    //      DROP=1 1s 步进
    // 这里要根据DROP,计算激活时间刷新的间隔,
    int step=1;   // step等于100ms
    // 前二位里,一位多等10倍
    u8 i;
    if(drop<=2){
        step = 1;
        for(i=0;i<2 && drop;i++){
            step*=10;
            drop--;
        }
        return step*50;
    }
    switch(drop){
        case 3:    // 按一分钟计
                step = 600;
            break;
        case 4:    // 按十分钟计
            step = 600*10;
            break;
        default:   // 按一小时计
            for(i=drop;i<SEGS-1;i++){
                step += 60*600;
            }
    }
    return step*50;   // 换算成定时器间隔
}

数字显示

刷新数码管包括两部分:

  1. 刷新数码管,使数码管保持点亮
  2. 刷新数码管显示的数字

为了方便在数码管中显示数字,将每位数字分别存储为一个数组的各个元素。因为计时功能只会涉及数字按1递减,所以操作数字并不复杂。

刷新数码管

数码管一次只能点亮一位数字(实际上,一次点亮数字的各个笔画也是依靠移位寄存器才能实现,CPU每次只能改变一个引脚的电平),每次刷新数码管时,只需要依次将数组里的每位数字依次刷新到数码管中的想相应位,在肉眼看时如同是在同时显示。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    u8 i = 0;
    if(htim==(&TIM4_Handler))
    {
            Display_Mode();
            if(setting){
                // 设置显示
                In_Setting_Mode();
            }else{
                for(i=0;i<SEGS-DROP;i++){
                    // 数码管
                    LED_Write_Data(SMG_num_map[display_queue[i]]+(i==SEGS-6)+(i==SEGS-4)+ (i==SEGS-2),i);
                    LED_Refresh();
                }
            }
            LED_Refresh();//刷新数码管
        if(timing) t1++;
        if(t1==step)// step是通过SEGS和DROP计算出的
        {
            if(timing){
                if(forward==-1) Dec_Time();
                else Inc_Time();
            }
            t1=1;
        }
    }else if(htim->Instance==TIM3)
    {
        // 红外部分
    }
}

数字刷新

通过定义一个外部整数变量,计算定时器中断触发的次数,达到一定次数后,刷新时间并重置次数。可以将计时间隔扩大到定时器中断设置值(2ms)的整数倍。我们可以通过这种方式来控制时间步长。因为每次刷新时间需要进行较多的计算,如果需要显示的时间位数不需要那么多(比如只需要计到1分钟),就可以通过改变重置这个次数阈值来控制刷新时间的间隔。

刷新数字时,需要对时间进行递减或递增,不过只需要考虑每次递减/递增1,所以实现比较简单。这主意秒和分钟要采取60进位的方式,也就是它们的第二位是6进制,这里用_isRestrictedPos(int t)函数来区分不同进制的位。

// 时间递增函数
void Inc_Time(){
    if(If_Time_Up()){
        Go_Boom();
        return;
    }
    // 设置了DROP后,从没有隐藏掉的第一位开始计算
    for(int8_t i=SEGS-1-DROP;i>=0;i--){
        if(_isRestrictedPos(i)){
            u8 t = display_queue[i]+1;
            if(t>5) display_queue[i] = t%6;
            else{
                display_queue[i] = t;
                return;
            }
        }
        else{
            u8 t = display_queue [i]+1;
            if(t>9) display_queue[i] = t%10;
            else{
                display_queue[i] = t;
                return;
            }
        }
    }
}

// 时间递减函数 void Dec_Time(){ if(If_Time_End()){ Go_Boom(); return; } // 设置了DROP后,从没有隐藏掉的第一位开始计算 for(int8_t i=SEGS-1-DROP;i>=0;i–){ if(_isRestrictedPos(i)){ int8_t t = display_queue [i]-1; if(t<0) display_queue[i] = 5; else{ display_queue[i] = t; return; } } else{ int8_t t = display_queue [i]-1; if(t<0) display_queue[i] = 9; else{ display_queue[i] = t; return; } } } }

u8 _isRestrictedPos(int8_t i){ // 根据DROP左移 return i==SEGS-5 || i == SEGS - 3; }

判断计时是否结束,对于递增计时的情况,只需要从左到右与预设的时间依次比较,大于则返回;对于递减的情况,只需要判断各位是否已为0即可。

其他设置的实现

(此处的设置均指”其他设置“而不是时间设置)

为了更好的方便对程序进行设置,在setting.c中实现了对计时步长和蜂鸣器的设置,并且还可以很方便地扩展到更多设置项。

通过一个Setting结构,包装一个设置项的不同部分。为了方便对设置的管理,为设置项定义了一系列生命周期。

struct Setting {
    u8 value;               // 设置值,在大多数情况下保留不用
    init_fun init;          // 进入该设置项前调用
    display_fun display;    // 用于在数码管上显示字符,在定时器循环中被调用
    setting_op opr;         // 用于接受和处理来自遥控器的输入
    end_fun end;            // changed=1时,离开该设置项时被调用
    end_fun endhook;        // changed=1时,允许外部程序自定义离开设置项时的动作,该函数在end前被调用
    end_fun final;          // 无论change为何,均会被调用。
    u8 changed;             // 设置项是否改变
};

下面是各个生命周期的函数定义:

typedef void (*display_fun)(void);
typedef void (*init_fun)(void);
typedef void (*end_fun)(void);
typedef void (*setting_op)(u8);

程序提供了设置结束状态钩子endhookendend是设置项自己定义,endhook则由外部程序设置。如果设置项将自己标记为changed=1,就会到调用这两个函数。

例如BEEP项:

struct Setting beep = {0, BEEP_init, BEEP_Option, BEEP_Opr, BEEP_End, 0, 0};

void BEEP_init(){
} void BEEP_Option(){ enum _Alphabet ts[] = {SMG_B, SMG_E, SMG_E, SMG_P, SMG_EMT, SMG_1}; ts[5] = allow_beep?SMG_1:SMG_0; Display_Text(ts, 6);
} void BEEP_Opr(u8 key){ switch(key){ case K_PLY: allow_beep = !allow_beep; break; } } void BEEP_End(){ }

外部程序使用void Setting_End_Hook_SET(struct Setting* target, end_fun end)来自定义离开设置项(并且设置项已被修改)时的动作,例如修改步长后,应当重新计算计时器激活的间隔并重置时间:

void System_Reset(){
    Set_Step();     // 重新计算步长
    Timer_Reset();  // 重置时间
}

void xtimer_init(void){ Set_Step(); Timer_Reset(); // 注册设置钩子 Setting_End_Hook_SET(&range, System_Reset); }

计时精度问题

测试时发现计时存在精度问题,每三十分钟偏差约1s。对于粗略的应用来说,这个精度还算可以接受,如果要实现更稳定、准确的计时,则需要使用RTC。

不足

  • 代码整洁度:在具体编写代码时,由于缺乏经验,使用了过多的外部变量来控制状态,虽然实现了功能,修改起来却是一团糟
  • 蜂鸣器发声太单调
  • 设置内容有限
  • 按键功能单一:遥控器上的每一个按键只能控制唯一的功能,没有复用。
  • 计时精度:计时存在偏差,应该不是逻辑而是硬件的问题,RTC更适合作为解决方案。


comments powered by Disqus