1.动机
最近两天蓝桥杯嵌入式省赛的成绩下来了,没有多久就要进行国赛,写下本文是为了回顾并复习下当时考试的知识,并且为后面的国赛开始做准备。作者的代码主要也以实现功能为主,可能部分功能实现代码比较冗余异或变量的声明以及函数的定义不够合理,也愿意听取各位的指导。
2.题解
首先是题目的要求如下:
2.1功能分析
根据题目的硬件框图,我们可以知道比赛涉及到的模块主要包括:
1、定时器输入捕获。2、按键输入。3、LCD显示。4、LED显示
根据题目的要求我们可以先进行cubemx的配置:
本次比赛需要配置模块相对之前少了很多,只需要配置:1.输入捕获相关的两个定时器。2.按键检测消抖的定时器。3.LCD的引脚。4、按键的引脚。5、LED的引脚以及使能引脚。具体如下:
按键检测消抖定时器:
输入捕获的两个定时器:
按键、LCD、LED的引脚配置
注意:
基础的配置包括:SYS选择串口进行调试,RCC的外部时钟选择外部晶振,如果想要提高输入捕获的精度,可将输入捕获对应的中断等级调高,这样能保证采集第一时间进行。
在完成cubemx的配置后生成工程,我们进入keil进行代码逻辑的编写。
2.2功能实现
本部分我将一张图一张图的分析并给出实现思路。
2.2.1显示功能
由题目可知,本题涉及到三个界面的显示,界面的显示实现原理一样,我以一个界面为例讲
解:
首先明确显示要求:
大家可以记住显示一行字符的通用模版:
char text[20];
sprintf(text," DATA");
LCD_DisplayStringLine(Line1,(uint8_t *)text);
上面三行代码就可以实现对应字符串在屏幕的某一行进行显示。
在明白基础的显示过后,我们注意到题目中的要求还存在单位的切换,即要根据数值大小动态决定是否切换单位,我这里的思路是先判断数值,再以数值相应的形式对内容进行组织,最后显示,代码如下:
sprintf(text," DATA");
LCD_DisplayStringLine(Line1,(uint8_t *)text);
if(freqa<1000 && freqa>=0)
{
sprintf(text," A=%dHz ",freqa);
}
else if(freqa>=1000)
{
sprintf(text," A=%.2fKHz ",freqa/1000.0);
}
else
{
sprintf(text," A=NULL ");
}
LCD_DisplayStringLine(Line3,(uint8_t *)text);
其他的页面也可以以上面的类似的思路进行实现,此时页面的制作完成了,可能会有疑问即如果将各个页面在何时显示进行区分,这里不用着急,在后面讲到按键实现时再提具体实现。
2.2.2按键模块
按键我分按键的检测和按键的功能实现两部分来讲:
按键检测:因为我一直按照同时可以检测单击,双击,长按的方式去写,所以比赛时也是一样的。这里按键检测采用的思路是定时器中断结合状态机实现,具体思路是:在按键按下时开始计时,如果经过10ms的中断3次,按键仍然按下,表示按键不是误触确实被按下,按键进入状态1,此状态目的在于记录按键持续被按下的时间,当按键松开进入状态2,状态2先进行松开的消抖(参考按下),随后判断按键计数值区分是长按还是短按,长按直接响应。如果是短按,注意:如果不考虑实现双击,这里也可以将短按标志直接置位。但如果考虑双击,就会进入双击的状态机,首先将双击状态置为1。随后在外面对双击按下进行计数,如果在350ms内按键按下了第二次,代表双击置位,如果没有就将双击的标志位全部复位,并且置单击的标志位。
上诉文字可能比较多,但是大家仔细分析一定会收获不少,具体代码如下:
按键结构体定义:
struct key_st
{
bool keyvalue;
bool key_short_flag;
bool key_long_flag;
bool key_double_flag;
uint16_t press_count;
uint16_t release_count;
uint16_t double_count;
uint16_t single_state;
uint16_t double_state;
uint16_t key4_sreen;
uint16_t key3_change;
};
按键单击,双击,长按具体实现(在定时器1的中断函数中实现):
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
key[0].keyvalue=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].keyvalue=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].keyvalue=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].keyvalue=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
for(int i=0;i<4;i++)
{
switch(key[i].single_state)
{
case 0:
{
if(key[i].keyvalue == 0)
{
key[i].press_count++;
if(key[i].press_count>3)
{
key[i].single_state=1;
}
}
}
break;
case 1:
{
if(key[i].keyvalue == 0)
{
key[i].press_count++;
}
else if(key[i].keyvalue == 1)
{
key[i].release_count++;
key[i].single_state=2;
}
}
break;
case 2:
{
if(key[i].keyvalue == 1)
{
key[i].release_count++;
if(key[i].release_count>3)
{
if(key[i].press_count>=100)
{
key[i].key_long_flag=1;
key[i].single_state=0;
key[i].press_count=0;
key[i].release_count=0;
}
else if(key[i].press_count<100)
{
switch(key[i].double_state)
{
case 0:
{
key[i].double_count=0;
key[i].double_state=1;
}
break;
case 1:
{
key[i].double_state=0;
key[i].key_double_flag=1;
}
break;
}
key[i].single_state=0;
key[i].press_count=0;
key[i].release_count=0;
}
}
}
}
break;
}
if(key[i].double_state==1)
{
key[i].double_count++;
if(key[i].double_count>35) //超过双击等待时间
{
key[i].key_short_flag=1;
key[i].double_state=0;
key[i].double_count=0;
}
}
}
}
按键的功能:这个需要针对题目具体分析
本题按键功能适合从后往前实现:
key4:切换页面,通过按键就实现了页面的切换,这里的思路是在按键结构体中定义一个屏幕的标志位并且由key4控制,即上上图中的 uint16_t key4_sreen;并且在屏幕显示模块就可以根据标志位的值显示对应模块。
key3:同样按键key4的思路设立相应的标志位来进行表征状态,只不过此时要所处的界面来实现对应的功能。并且不要忽视到长按功能的实现。
key2和key1:是简单的减加操作,关键在于要通过前面的一系列标志位的数值确定加减的数值并且保证不能超过这些变量的取值范围,取值范围如下:
注意,按键3的要求有2个隐含要求,到参数界面默认调整PD,到数据界面默认显示频率,这两个要求可以通过按键4切换显示模式实现,在切换显示模式后检测是否在前面要求的两个界面,如果不在,就将相应标志位复位,保证下次进入时按初始状态进行显示和调整。
至此,按键部分分析完成,具体代码如下:
void key_proc()
{
for(int i=0;i<4;i++)
{
if(key[i].key_short_flag==1)
{
key[i].key_short_flag=0;
switch(i)
{
case 0:
{
switch(change_select)
{
case 0:
{
PD = PD+100>1000?1000:PD+100;
}
break;
case 1:
{
PH = PH+100>100000?100000:PH+100;
}
break;
case 2:
{
PX = PX+100>1000?1000:PX+100;
}
break;
}
}
break;
case 1:
{
switch(change_select)
{
case 0:
{
PD = PD-100<100?100:PD-100;
}
break;
case 1:
{
PH = PH-100<1000?1000:PH-100;
}
break;
case 2:
{
PX = PX-100<-1000?-1000:PX-100;
}
break;
}
}
break;
case 2:
{
if(key[3].key4_sreen == 1) //参数界面功能
{
change_select = change_select+1>2?0:change_select+1;
}
else if(key[3].key4_sreen == 0)//数据界面功能
{
key[2].key3_change=!key[2].key3_change;
}
}
break;
case 3:
{
key[3].key4_sreen = key[3].key4_sreen+1>2?0:key[3].key4_sreen+1;
if(key[3].key4_sreen!=0)
{
key[2].key3_change=0;
}
if(key[3].key4_sreen!=1)
{
change_select=0;
}
}
break;
}
}
if(key[i].key_long_flag && i==2)
{
key[i].key_long_flag=0;
NHA=0;
NHB=0;
NDA=0;
NDB=0;
}
}
}
2.2.3LED模块块
led模块实现非常简单,根据相应状态的标志位对对应led进行置位就可以,这里我分享下自己实现的思路:建立一个led的状态数组来标识每个led状态,每次初始化都置0,满足相应相应情况的位置置1,此实现有两个好处:1.不同考虑led的熄灭状态,一开始会将全部led置暗。2.最后通过移位统一对led的状态进行写。
具体代码如下:
void led_proc()
{
for(int i=0;i<8;i++)
{
led_state[i]=0;
}
if(key[3].key4_sreen==0)
{
led_state[0]=1;
}
if(freqa>PH)
{
led_state[1]=1;
}
if(freqb>PH)
{
led_state[2]=1;
}
if(NDA>=3 || NDB>=3)
{
led_state[7]=1;
}
for(int i=0;i<8;i++)
{
if(led_state[i])
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8<<i,GPIO_PIN_RESET);
}
else
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8<<i,GPIO_PIN_SET);
}
}
//使能led设置
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}
2.2.4双通道频率测量功能
这算是本次比赛最难的一个部分,里面有一些小细节需要注意,主要考察一些实现逻辑。首
先看要求:
功能1:频率测量,这里使用基本的输入捕获频率测量模版即可,代码如下:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)//输入捕获1
{
uint32_t count=0;
count = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
__HAL_TIM_SetCounter(htim,0);
freqa_m = 8000000.0 / count;
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_1);
}
//两个输出捕获
if(htim->Instance==TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)//输入捕获1
{
uint32_t count=0;
count = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
__HAL_TIM_SetCounter(htim,0);
freqb_m = 1000000.0 / count;
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_1);
}
}
功能2:频率校准,此功能也比较简单,即在对测量频率以PX进行校正,需注意一点,我们后面使用的显示的全部频率都是这个校准后的频率,而不是校准前的频率。
功能3:频率超限,这个功能看似简单,实际陷阱挺多。首先:这个频率超限不是超过上限后一直计数,而是记录超过限度的上升沿,这两种情况的实现方式是不同的,其次,本功能存在一个很微小的坑,即当输入频率不改变,而限度从高于输入频道变化到低于输入频率,这一次输入频率的超限也要计入在内(这是我的理解)。实现的思路是通过一个旧值来保存原频率值和原上限值,当旧值和新值不同且满足上升沿要求时就会计数。
功能4:频率突变,这里的解法我不确定,网上有说这3s是滑动的窗口,但我不这么认为,我认为是固定的3s,当然固定3s实现比滑动更简单,在3s内统计最大值和最小值,当达到3s时计算差值与允许波动范围进行比较来判断是否突变,注意在判断完一次后,要将最大值和最小值复位,防止影响一下个3s区间的检测。
功能2,3,4的代码实现如下:
void freq_proc()
{
static int max_a=0,min_a=20000,max_b=0,min_b=20000;
static int freqa_old=0,freqb_old=0;
static int PH_old=5000;
if(count100ms_flag_1)
{
count100ms_flag_1=0;
freqa = freqa_m+PX;//频率单位是hz
freqb = freqb_m+PX;
ta = 1000000/(float)freqa;//周期初始单位是us
tb = 1000000/(float)freqb;
}
if(freqa_old != freqa)
{
if(freqa_old<PH && freqa>PH)
{
NHA++;
}
freqa_old = freqa;
}
if(freqb_old != freqb)
{
if(freqb_old<PH && freqb>PH)
{
NHB++;
}
freqb_old = freqb;
}
if(PH_old!=PH)
{
if(freqa<PH_old && freqa>PH)
{
NHA++;
}
if(freqb<PH_old && freqb>PH)
{
NHB++;
}
PH_old = PH;
}
//频率小于0无效,上面的超限可以自动滤除,但是突变不行
if(capture_flag) //5s计时
{
if(freqa>=0)
{
if(freqa>max_a)
{
max_a=freqa;
}
else if(freqa<min_a)
{
min_a=freqa;
}
}
if(freqb>=0)
{
if(freqb>max_b)
{
max_b=freqb;
}
else if(freqb<min_b)
{
min_b=freqb;
}
}
}
else
{
capture_flag=1;
if(max_a-min_a>PD && freqa>=0)
{
NDA++;
}
if(max_b-min_b>PD && freqb>=0)
{
NDB++;
}
max_a=0; //这种初始化不会有影响
min_a=20000;
max_b=0;
min_b=20000;
}
}
至此题目要求所有功能模块实现,我们还需注意初始状态的设置(通过赋初值)
性能的要求:
主要关注5,6条:第5条,通过一个100ms的定时来实现100ms进行一次频率的探测,在上诉freq_proc中有实现。第6条:规定lcd刷新的时间为100ms,实现同上,在上面lcd_proc中有实现。
为防止界面之间的内容残留,这里还写了一个界面刷新函数,当检测到界面改变时,将界面刷新一次,有两种刷新方式:1.使用LCD_clear函数(但是耗时)2.使相应行显示空格,笨拙但是程序性能更好(自行选择)代码如下:
void screen_flash_proc()
{
static uint8_t old_sreen=0;
if(old_sreen!=key[3].key4_sreen)
{
//LCD_Clear(Black);//这样耗时间
LCD_DisplayStringLine(Line0,(uint8_t *)" ");
LCD_DisplayStringLine(Line1,(uint8_t *)" ");
LCD_DisplayStringLine(Line2,(uint8_t *)" ");
LCD_DisplayStringLine(Line3,(uint8_t *)" ");
LCD_DisplayStringLine(Line4,(uint8_t *)" ");
LCD_DisplayStringLine(Line5,(uint8_t *)" ");
LCD_DisplayStringLine(Line6,(uint8_t *)" ");
LCD_DisplayStringLine(Line7,(uint8_t *)" ");
old_sreen = key[3].key4_sreen;
}
}
至此,第十五省赛要求的所有模块已经实现。
3.感想
本次比赛的成绩不错自己也很高兴,希望后续能好好准备国赛再接再厉,在这里也写下自己学习的感想。万事都可熟能生巧,首先要熟悉各个模块功能的实现原理,在学习完每个模块后,就开始上手做整套的真题,通过真题学习代码之间的逻辑以及各种功能实现的不同写法,一开始的一两套会很困难,但熬过来就好了,学海无涯苦作舟。
如果有朋友需要整套源码,可以评论区或者私信,看到后提供。