本实验设计基于正点电子STM32 NANO开发板,利用中断机制进行定时,可以正向、反向计时,计时结束后有灯光、声音提醒,可以通过按键、红外遥控进行控制、设定时间。
将软件下载到开发板中,开发板显示初始计时起点。
数码管上前7位都可做时间显示,以右下角红点分割时、分、秒、0.1秒。
最右边一位以字母的形式指示计时器状态:
按下KEY1或遥控器上的播放键,计时开始,数码管最后一位闪动。再次按下后计时暂停,停止闪动。
按下KEY_UP或遥控器上的电源键,计时回到初始状态
按下KEY_0或遥控器上的右方向键切换到正向计时,按下KEY_2或遥控器上的左方向键切换到倒计时。
按下遥控器上的ALIENTEK键,进入时间设置,此时可以对每一位的数字进行设置,使用VOL-和VOL+选择数字,相应位置的数字会闪动。此时按下任意数字键,或按上、下键即可调整数字大小,再次按下删除键退出设置。
按下遥控器上的删除键,进入其他设置,再次按下删除键进入下一项设置,直到退出。
目前可以设置的项有
理想的计时步长是100ms~1h,
使用TIM4定时器中断,定时为2ms,每次调用回调函数时,刷新数码管显示,检查接收到的红外信号、检查是否应该更新时间值。
实验总共用到了三个中断:按键外部中断,红外外部中断,定时器中断。
适合的中断优先级应当是外部中断>定时器中断,这是为了保证能正确捕捉红外输入。考虑到需要由红外中断引起的操作都会重置/启停计时,所以不需要考虑外部中断对计时精度的影响。
使用TIM4,抢占优先级设为1,子优先级3
HAL_NVIC_SetPriority(TIM4_IRQn,1,3);
使用TIM3输入捕获来测量脉宽,抢占优先级0,子优先级4:
HAL_NVIC_SetPriority(TIM3_IRQn,0,4);
输入捕获测量脉宽的原理:
中间要处理因为定时器溢出而导致的寄存器自动清零的情况。
程序开始运行时,会根据SEGS
和DROP
计算出应该采取的时间步长,即更新数码管显示的数字的时间间隔,这样有利于节省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递减,所以操作数字并不复杂。
数码管一次只能点亮一位数字(实际上,一次点亮数字的各个笔画也是依靠移位寄存器才能实现,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);
程序提供了设置结束状态钩子endhook
和end
,end
是设置项自己定义,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。