一、牢骚
这一套题是我做过的所有赛题中觉得考察知识点最广,小细节最多,坑也最多的一套题,即使已经有一定的基础,但这套题中的一些要求还是第一次尝试去实现,也是分享和巩固一下自己的方法。这次题目实现了包括EERPOM,DS18B20,数码管这些扩展板通信函数的撰写,文本高亮的设置,按键长按快速变动,中断优先级的研究,输入捕获的准确性等等知识点,也是我收获最多的一套题,希望这篇文章能帮助自己巩固。
二、功能实现
功能非常繁杂,我们一个一个来研究。
LCD模块
要求:
LCD显示还是使用常用模版就可以实现,代码如下:
char text[20];
sprintf(text," DATA");
LCD_DisplayStringLine(Line1,(uint8_t *)text);
第二个界面的比较通道显示可以通过标志位来判断显示的内容是AO1还是AO2,全部代码如下:
下面的代码有一些更改背景色的操作,这里先不做解释,这是为实现按键的高亮需求,具体见按键模块。
void lcd_proc(void)
{
char text[20];
if(key[0].key0_sreen==0)
{
sprintf(text," Main ");
LCD_DisplayStringLine(Line1,(uint8_t *)text);
sprintf(text," AO1:%.2fV ",AO1);
LCD_DisplayStringLine(Line2,(uint8_t *)text);
sprintf(text," AO2:%.2fV ",AO2);
LCD_DisplayStringLine(Line3,(uint8_t *)text);
sprintf(text," PWM2:%d%% ",(int)(pwm2*100));
LCD_DisplayStringLine(Line4,(uint8_t *)text);
sprintf(text," Temp:%.2fC ",Temp);
LCD_DisplayStringLine(Line5,(uint8_t *)text);
sprintf(text," N:%d ",N);
LCD_DisplayStringLine(Line6,(uint8_t *)text);
}
else
{
sprintf(text," Para ");
LCD_DisplayStringLine(Line1,(uint8_t *)text);
if(key[1].key1_select==0)
{
LCD_SetBackColor(Yellow); //高亮显示
}
sprintf(text," T:%d ",T);
LCD_DisplayStringLine(Line3,(uint8_t *)text);
LCD_SetBackColor(Black); //最后要设置回来
if(key[2].key2_3_mode==0)
{
if(key[1].key1_select==1)
{
LCD_SetBackColor(Yellow);
}
sprintf(text," X:AO1 ");
}
else if(key[2].key2_3_mode==1)
{
if(key[1].key1_select==1)
{
LCD_SetBackColor(Yellow);
}
sprintf(text," X:AO2 ");
}
LCD_DisplayStringLine(Line4,(uint8_t *)text);
LCD_SetBackColor(Black);
}
}
数码管
要求:
首先注意是以两秒为间隔更换显示,所以涉及到定时器的定时,其次就是要自行实现数码管的写操作函数,具体的实现细节大家可以去找教程看看,我就不细说了,数码管的实现代码如下:
#include "nixie.h"
uint16_t nixie_num[12]={0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f,0x77,0x39,};
void nixie_proc(uint8_t num1,uint8_t num2,uint8_t num3)
{
uint8_t temp;
temp=nixie_num[num3];
for(int i=0;i<8;i++)
{
if(temp & 0x80)
{
SER_H;
}
else
{
SER_L;
}
SCK_H;
temp<<=1;
SCK_L;
}
temp=nixie_num[num2];
for(int i=0;i<8;i++)
{
if(temp & 0x80)
{
SER_H;
}
else
{
SER_L;
}
SCK_H;
temp<<=1;
SCK_L;
}
temp=nixie_num[num1];
for(int i=0;i<8;i++)
{
if(temp & 0x80)
{
SER_H;
}
else
{
SER_L;
}
SCK_H;
temp<<=1;
SCK_L;
}
RCK_H; //并行输出
RCK_L;
}
.h文件中包含了上诉代码的一些宏定义,具体如下:
#ifndef __NIXIE_H
#define __NIXIE_H
#include "main.h"
#define SER_H HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET)
#define SER_L HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET)
#define RCK_H HAL_GPIO_WritePin(GPIOA,GPIO_PIN_2,GPIO_PIN_SET)
#define RCK_L HAL_GPIO_WritePin(GPIOA,GPIO_PIN_2,GPIO_PIN_RESET)
#define SCK_H HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_SET)
#define SCK_L HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_RESET)
void nixie_proc(uint8_t num1,uint8_t num2,uint8_t num3);
#endif
在具体的显示部分,通过2s定时器的标志位来确定要显示的内容,并且利用除法和取余来确定每一位的数字,具体代码如下:
void nix_proc(void)
{
if(count2s)//为1的情况
{
if(key[2].key2_3_mode==0)
{
nixie_proc(10,0,1); //10代表A
}
else if(key[2].key2_3_mode==1)
{
nixie_proc(10,0,2);
}
}
else
{
nixie_proc(11,T/10,T%10); //11代表C
}
}
KEY模块
要求:
按键的检测同样使用之前文章的状态机+定时器中断,但是这里的长按逻辑需要改变,因为这里的长按逻辑不需要抬起,所以我们之前的长按逻辑不兼容,具体是在状态机的1阶段去检测presscount,如果在对应的情况下按键3或者按键4已经达到长按情况就直接开始实现快速步进的功能,同时在此种情况抬起时就将所有状态量复位而不进入最后的二阶段,具体代码如下:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM1)
{
key[0].key_value=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].key_value=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].key_value=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].key_value=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
for(int i=0;i<4;i++)
{
switch(key[i].single_state)
{
case 0:
{
if(key[i].key_value==0)
{
key[i].press_count++;
if(key[i].press_count>3)
{
key[i].single_state=1;
}
}
}
break;
case 1:
{
if(key[i].key_value==0)
{
key[i].press_count++;
if((i==2 || i==3) && key[0].key0_sreen==1 && key[1].key1_select==0)
{
if(key[i].press_count>200)
{
if(i==2)
{
T=T+1>40?40:T+1;
}
else if(i==3)
{
T=T-1<20?20:T-1;
}
}
}
}
else if(key[i].key_value==1)
{
key[i].release_count++;
key[i].single_state=2;
if((i==2 || i==3) && key[0].key0_sreen==1 && key[i].press_count>200 && key[1].key1_select==0)//大于200且再屏幕1的按键情况直接置0
{
key[i].single_state=0;
key[i].press_count=0;
key[i].release_count=0;
}
}
}
break;
case 2:
{
if(key[i].key_value==1)
{
key[i].release_count++;
if(key[i].release_count>3)
{
if(key[i].press_count>200)
{
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<=200)
{
switch(key[i].double_state)
{
case 0:
{
key[i].double_state=1;
key[i].double_count=0;
}
break;
case 1:
{
key[i].key_double_flag=1;
key[i].double_state=0;
}
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;
}
}
}
}
}
按键功能实现:
key1:界面显示标志位的切换,注意在界面切换的同时,还需检测参数是否变量,如果变化将变化次数加1并且写入EEPROM。
key2:在参数界面生效,选择更改的参数并且对应参数行要高亮,选择并不困难,思路和key1一样,这里讲一下高亮如何实现,可以看上面的lcd代码,我的思路是检查key2对应的标志位数值,根据数值选择要高亮的行,在显示之前更改背景色,在显示完目标后将背景色换为黑色以避免影响其他行的显示,具体如下:
key3和key4:同样在参数设置界面生效,并且根据key2选择的模式来确定更改的操作,如果选择T,key3实现温度加,key4温度减(注意不要超过数值限度)。如果选择X即通道参数,key3和key4实现的都是模式切换的功能,并且操作的是同一个标志位。
按键功能实现全部代码如下:
void key_proc(void)
{
for(int i=0;i<4;i++)
{
if(key[i].key_short_flag)
{
key[i].key_short_flag=0;
switch(i)
{
case 0:
{
key[0].key0_sreen = key[0].key0_sreen+1>1?0:key[0].key0_sreen+1;
//退出参数配置时检测参数并同步存储
if(key[0].key0_sreen==1)
{
old_t = T;
old_mode = key[2].key2_3_mode;
}
else if(key[0].key0_sreen == 0) //参数变动的话计数+1
{
if(old_mode!=key[2].key2_3_mode)
{
N++;
}
if(old_t!=T)
{
N++;
}
eeprom_write(0x01,N);
key[1].key1_select=0;
}
}
break;
case 1:
{
if(key[0].key0_sreen == 1)
{
key[1].key1_select = key[1].key1_select+1>1?0:key[1].key1_select+1;
}
}
break;
case 2:
{
if(key[0].key0_sreen==1)
{
if(key[1].key1_select==0) //key1为0改变温度
{
T = T+1>40?40:T+1;
}
else
{
key[2].key2_3_mode = !key[2].key2_3_mode;
}
}
}
break;
case 3:
{
if(key[0].key0_sreen==1)
{
if(key[1].key1_select==0) //key1为0改变温度
{
T = T-1<20?20:T-1;
}
else
{
key[2].key2_3_mode = !key[2].key2_3_mode;
}
}
}
break;
}
}
}
}
测量模块
测量的内容很多,具体如下:
1.RP5和RP6使用多通道AD转换,注意这里的一点是,一定要在转换完成就使用计数值计算电压,否则后面计算会出错,具体代码:
void measure_proc(void)
{
//AD转换
float count_m[2];
for(int i=0;i<2;i++)
{
uint16_t count;
count=HAL_ADC_GetValue(&hadc2);//AD转换获得的值立马就要使用,不能存储等到退出转换后再使用
HAL_ADC_PollForConversion(&hadc2,50);
HAL_ADC_Start(&hadc2);
count_m[i]=count*3.3/4096;
}
AO2 = count_m[0];
AO1 = count_m[1];
// //还要测温
// static uint16_t temp_count=0;
// temp_count = ds18b20_read();
// Temp = temp_count*0.0625;
}
2.PWM测量使用输入捕获即可,代码如下:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
{
static uint32_t count_all=0,count_high=0;
count_all = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
count_high = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
__HAL_TIM_SetCounter(htim,0);
pwm2 = (float)count_high /count_all;
if(count_high==0 || count_all==0)
{
pwm2=0;
}
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_2);
}
}
3.测量温度,官方提供的DS18B20的库函数中并没有提供器件的温度读取函数,我们需要自己写,具体命令格式可以网上去看看教程,这里将自己需要写的函数给出(此函数可以实现温度值的读取功能):
uint16_t ds18b20_read(void)
{
uint8_t val[2];
uint8_t i = 0;
uint16_t x = 0;
ow_reset();
ow_byte_wr(OW_SKIP_ROM);
ow_byte_wr(DS18B20_CONVERT);
//delay_us(750000);
ow_reset();
ow_byte_wr( OW_SKIP_ROM );
ow_byte_wr ( DS18B20_READ );
for ( i = 0 ; i < 2; i++)
{
val[i] = ow_byte_rd();
}
x = val[1]; //鍏堣鍒扮殑鏄珮浣?
x <<= 8;
x |= val[0];
return x;
}
在实现读函数后,我们需要对温度进行读取,这里我放在定时器中进行读取,具体原因我会在本模块最后进行讲解,代码如下(只需关注最后3行即可):
if(htim->Instance == TIM4) //中断优先级最高
{
if(autoflag)
{
count_1s++;
if(count_1s>=20)
{
count1s=1;
count_1s=0;
}
}
else
{
count_1s=0;
}
//led灯
if(count200ms)
{
count_200ms++;
if(count_200ms>=4)
{
led2_state=!led2_state;
count_200ms=0;
}
}
else
{
count_200ms=0;
led2_state=0;
}
static uint16_t result=0;
result = ds18b20_read();
Temp = result * 0.0625;
}
4.EEPROM的参数存储可读取,同样没有给出具体的器件函数,我们需要根据底层函数来自己实现器件的上层,主要包括EERPOM的读写函数,代码如下:
读函数(最后一位0写1读):
uint8_t eeprom_read(uint8_t addr)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStop();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
uint8_t rec = I2CReceiveByte();
I2CSendAck();
I2CStop();
return rec;
}
写函数:
void eeprom_write(uint8_t addr,uint8_t data)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CSendByte(data);
I2CWaitAck();
I2CStop();
}
测量的内容就这些,但是这里还要提一个非常关键的点,也是我觉得本届题目最不容易解决的问题,就是优先级的问题,DS18B20对时序的要求非常严格,如果其在读取温度的过程中被其他中断终止会造成读取数值错误的情况,所以,我将其放在中断函数中进行读取。经过测试,发现输入捕获对优先级的要求高于DS18B20,所以将输入捕获中断优先级设为最高也就是TIM3,其次,DS18B20对优先级的需求也较高,所以我将其优先级设为次高,而按键检测和串口中断的定时器和串口中断相对要求不高,所以将优先级设置为最低,只有通过上面的设计才能够保证温度测量的数值能够正确,PWM输入的占空比测量稳定以及按键和串口功能也能完整实现,cubemx设置如下(数值越小优先级越高):
串口模块
要求:
1.自动上报:根据上报条件进行判断并上报即可,注意当前的通道是AO1还是AO2,并且就是这里对PWM测量的稳定性要求很高,所以将输入捕获涉及定时器中断优先级设置为最高,并且注意上报的间隔为1s
2.数组召唤功能:通过前几期所教的串口不定长接收接收到字符串数据后就可以使用strcmp对字符串进行检测,并按照要求进行相应内容的输出即可,注意改写fputc函数以保证printf函数能够通过串口输出。模块代码如下:
void uart_proc(void)
{
if(rec_flag)
{
rec_flag=0;
rec_buf[rec_n]='\0';//结尾加上结束符
rec_n=0;
if(strcmp(rec_buf,"ST\r\n")==0)
{
printf("$%.2f\r\n",Temp);
}
if(strcmp(rec_buf,"PARA\r\n")==0)
{
if(key[2].key2_3_mode==0)
{
printf("#%d,AO%d\r\n",T,1);
}
else
{
printf("#%d,AO%d\r\n",T,2);
}
}
}
if(key[2].key2_3_mode == 0)//AO1
{
if(AO1>pwm2*(float)3.3)
{
autoflag = 1;
}
else if(AO1<pwm2*(float)3.3)
{
autoflag = 0;
}
}
else
{
if(AO2>pwm2*(float)3.3)
{
autoflag=1;
}
else if(AO2<pwm2*(float)3.3)
{
autoflag = 0;
}
}
if(autoflag)
{
if(count1s)
{
count1s = 0;
printf("$%.2f\r\n",Temp);
}
}
LED模块
要求:
只要前面的模块实现了,led就是通过前面的标志位对led的亮灭状态进行改变,注意led8的0.2s闪烁用定时器实现即可。代码如下:
void led_proc(void)
{
for(int i=0;i<8;i++)
{
led_state[i]=0;
}
if(autoflag)
{
led_state[0]=1;
}
if(Temp>T)
{
count200ms=1;
led_state[7]=led2_state;
}
else
{
count200ms=0;
}
for(int i=0;i<8;i++)
{
if(led_state[i]==1)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8<<i,GPIO_PIN_RESET);//低电平亮
}
else
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8<<i,GPIO_PIN_SET);
}
}
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}
EEPROM模块
EEPROM的使用注意在于一开始的读取和从参数切换到数据界面后改变次数的改写两个部分,其中改写次数在key模块的中有实现,而读取只需要在while循环之前将对应地址的数据读取到N就行。
至此,第十届的全部模块都已经实现。
三、总结
这一届比赛试题在我看来体量大涉及知识面很广并且考察逻辑也比较复杂,对很多器件的实现时序也进行考察,逻辑关系也进行考察,并且还需分析中断优先级的先后以保证功能正确实现,有时功能无法实现可能不是代码逻辑或者器件的问题,而是该器件或功能对时序的完整度要求很高,例如本套中DS18B20和输入捕获,他们是不允许在执行过程中被其他中断程序打断太久的,所以他们需要高优先级,而按键检测的定时器不需要,因为其本来就是一个多状态的过程,串口接受中断的优先级应该略高于按键检测但低于前两个模块,如果大家在做题过程中遇到此类问题,可以尝试调整优先级的顺序来解决。