一、题目分析
相信搜索本题的同学手里肯定有题目了,我就不把完整题目放出了,这里着重对一些题目的要求进行分析。由下面总体硬件框图可以看出:主要设计LCD,LED,按键,串口,扩展板资源,前面4个是老生常淡的问题了,那么应该着重扩展资源考察了什么内容。
由题目知道,考察的是1路脉冲,2路PWM,一路AD(光敏电阻),幸运,这些也都是省赛涉及的知识点,在对所使用模块有一个整体的了解后,我们才能更清晰的去分析和实现每一个框架,总体要求如下:
二、模块实现
这部分我将从每个模块入手,一个一个的进行实现。
LCD
要求:
LCD的解决方法在我之前的15届省赛题解 已经说过解决思路了,这个模块就是套模版,谈不上有什么逻辑,这里就再说一次,也就是采用下面这几行代码就可完成一行字符的显示。
char text[20];
sprintf(text," DATA");
LCD_DisplayStringLine(Line1,(uint8_t *)text);
本题显示唯独需要注意一点就是采集模式的显示需要根据标志位来确定,大致思路如下:
if(key[2].key2_mode_flag==0)
{
sprintf(text," mode:A ");
}
else
{
sprintf(text," mode:B ");
}
LCD_DisplayStringLine(Line8,(uint8_t *)text);
同时给大家一个小建议,我选择在写显示模版的同时就在该函数的上面定义需要的变量,变量就以显示的参数命名,这样即直观也可以防止后续忘记定义变量。
代码:
void lcd_proc()
{
char text[20];
if(light_off)
{
light_off=0;
sprintf(text," lightoff ");
LCD_DisplayStringLine(Line9,(uint8_t *)text);
}
else
{
sprintf(text," ");
LCD_DisplayStringLine(Line9,(uint8_t *)text);
}
if(key[0].key0_screen_flag==0)
{
sprintf(text," DATA ");
LCD_DisplayStringLine(Line1,(uint8_t *)text);
sprintf(text," a:%.1f ",a);
LCD_DisplayStringLine(Line2,(uint8_t *)text);
sprintf(text," b:%.1f ",b);
LCD_DisplayStringLine(Line3,(uint8_t *)text);
sprintf(text," f:%dHz ",f);
LCD_DisplayStringLine(Line4,(uint8_t *)text);
sprintf(text," ax:%d ",ax);
LCD_DisplayStringLine(Line6,(uint8_t *)text);
sprintf(text," bx:%d ",bx);
LCD_DisplayStringLine(Line7,(uint8_t *)text);
if(key[2].key2_mode_flag==0)
{
sprintf(text," mode:A ");
}
else
{
sprintf(text," mode:B ");
}
LCD_DisplayStringLine(Line8,(uint8_t *)text);
}
else
{
sprintf(text," PARA ");
LCD_DisplayStringLine(Line1,(uint8_t *)text);
sprintf(text," Pax:%d ",Pax);
LCD_DisplayStringLine(Line2,(uint8_t *)text);
sprintf(text," Pbx:%d ",Pbx);
LCD_DisplayStringLine(Line3,(uint8_t *)text);
sprintf(text," Pf:%d ",Pf);
LCD_DisplayStringLine(Line4,(uint8_t *)text);
}
}
按键
要求:
按键采集:定时器+状态机。具体思路和实现细节同样参考:15届省赛
按键实现逻辑:本题按键实现逻辑并不复杂,只需注意两个点:1.注意key2和key3的数值超过范围。2.注意每个按键生效的前提条件(不满足条件时不生效)
代码:
void key_proc(void)
{
for(int i=0;i<4;i++)
{
if(key[i].key_short_press==1)
{
key[i].key_short_press=0;
switch(i)
{
case 0:
{
key[0].key0_screen_flag = key[0].key0_screen_flag+1>1?0:key[0].key0_screen_flag+1;
}
break;
case 1:
{
if(key[0].key0_screen_flag==1)
{
Pax = Pax+10>60?10:Pax+10;
Pbx = Pbx+10>60?10:Pbx+10;
}
}
break;
case 2:
{
if(key[0].key0_screen_flag==0)
{
key[2].key2_mode_flag = key[2].key2_mode_flag+1>1?0:key[2].key2_mode_flag+1;
}
else
{
Pf = Pf+1000>10000?1000:Pf+1000;
}
}
break;
case 3:
{
if(key[2].key2_mode_flag==0)
{
//采集一次数据
trigger_flag = 1;
}
}
break;
}
}
}
}
LED
要求:
同样在开始之前将所有LED状态置为暗,按照要求将对应LED置亮即可。前四个LED不同多少,这里分析一下第四个LED亮的条件,我在下图中以a代表α,b代表β,我们设夹角为k,通过
三角关系可以得到:k+90+a-b=180,即k=90-a+b,要求该夹角小于10灯亮,即90-a+b<10;即代表LED5亮。
代码:
void led_proc(void)
{
for(int i=0;i<8;i++)
{
led_state[i]=0;
}
if(ax>Pax)
{
led_state[0]=1;
}
if(bx>Pbx)
{
led_state[1]=1;
}
if(f>Pf)
{
led_state[2]=1;
}
if(key[2].key2_mode_flag==0)
{
led_state[3]=1;
}
if(90-a+b<10)
{
led_state[4]=1;
}
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);
}
串口
要求:
本题串口要求看着是很多的,但不用担心,重点在于实现不定长接收,只要能正确接收到数据,其他都不是串口的事情,而是其他模块的衔接,所以我们这里重点将串口不定长接收的思路。
思路:用一个5ms长度的定时中断来检测串口接受是否结束,如果结束即完成串口接受任务。
具体如何实现呢?分两个模块,串口中断和5ms周期的定时器中断,串口中断每当接收数据便进入,并且在进入后进行喂狗,将5ms定时器中断的计数值置0防止该中断的发生,当该中断发生时,代表没有喂狗即串口中断模块没有发生,代表接受完成。至于为何是5ms,这是可以通过波特率计算的,但是5ms是肯定足够检测串口接收中止了,大家尽管用这个间隔。
串口中断代码:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
__HAL_TIM_SetCounter(&htim4,0);//喂狗
if(rec_n==0)
{
__HAL_TIM_CLEAR_FLAG(&htim4,TIM_FLAG_UPDATE);
HAL_TIM_Base_Start_IT(&htim4);
}
rec_buf[rec_n++]=rec;
HAL_UART_Receive_IT(huart,&rec,1);
}
}
5ms定时器中断
if(htim->Instance == TIM4)
{
HAL_TIM_Base_Stop_IT(htim);
rec_flag=1;
}
串口接受后,来实现每个串口对应的功能:
功能1:查询当前值,直接将当前记录的角度进行发送即可,记录角度的代码在后面的扩展模块进行数据采集时会有。
功能2:查询历史角度数据,可以看到,这里要求5个数据,所以我们在后续的采集模块一定要存储5个历史数据,至于如何使其按时间排序,可以使用循环数组来实现,这里后续会说。此外我们输出这个数组不能直接用原数组输出,可采用一个新数组将数据进行复制再输出,因为实现是环形数组存储,所以复制也按照环形数组复制。
功能3:排序输出,这里涉及到一个小的排序算法,大家采用自己喜欢的即可,我就直接用的冒泡排序然后输出,这个我为何说要用一个新数组来输出,因为不会影响原数组的按时间存储的特性。
代码如下(记得重写fputc函数):
void uart_proc()
{
float array_trans[5];
int n_trans=0;
if(rec_flag==1)
{
rec_flag=0;
rec_buf[rec_n]='\0';
rec_n=0;//复位收尾工作
if(strcmp(rec_buf,"a?")==0)
{
printf("a:%.1f",array_a[n-1<0?4:n-1]);
}
else if(strcmp(rec_buf,"b?")==0)
{
printf("b:%.1f",array_b[n-1<0?4:n-1]);//n是对应第几个数,下标是n-1
}
//按时间
else if(strcmp(rec_buf,"aa?")==0)
{
//先采集先输出
n_trans=n;
uint8_t temp_n=0;
while(n_trans != n-1)
{
array_trans[temp_n++]=array_a[n_trans];
n_trans = n_trans+1>4?0:n_trans+1;
}
array_trans[temp_n]=array_a[n_trans];
printf("aa:%.1f-%.1f-%.1f-%.1f-%.1f",array_trans[0],array_trans[1],array_trans[2],array_trans[3],array_trans[4]);
}
else if(strcmp(rec_buf,"bb?")==0)
{
//先采集先输出
n_trans=n;
uint8_t temp_n=0;
while(n_trans != n-1)
{
array_trans[temp_n++]=array_b[n_trans];
n_trans = n_trans+1>4?0:n_trans+1;
}
array_trans[temp_n]=array_b[n_trans];
printf("bb:%.1f-%.1f-%.1f-%.1f-%.1f",array_trans[0],array_trans[1],array_trans[2],array_trans[3],array_trans[4]);
}
else if(strcmp(rec_buf,"qa?")==0)
{
//先采集先输出
n_trans=n;
uint8_t temp_n=0;
while(n_trans != n-1)
{
array_trans[temp_n++]=array_a[n_trans];
n_trans = n_trans+1>4?0:n_trans+1;
}
array_trans[temp_n]=array_a[n_trans];
//冒泡排序
for(uint8_t i=0;i<4;i++)
{
for(uint8_t j=i+1;j<5;j++)
{
if(array_trans[j]<array_trans[i])
{
float temp;
temp = array_trans[j];
array_trans[j]=array_trans[i];
array_trans[i]=temp;
}
}
}
printf("qa:%.1f-%.1f-%.1f-%.1f-%.1f",array_trans[0],array_trans[1],array_trans[2],array_trans[3],array_trans[4]);
}
else if(strcmp(rec_buf,"qb?")==0)
{
//先采集先输出
n_trans=n;
uint8_t temp_n=0;
while(n_trans != n-1)
{
array_trans[temp_n++]=array_b[n_trans];
n_trans = n_trans+1>4?0:n_trans+1;
}
array_trans[temp_n]=array_b[n_trans];
//冒泡排序
for(uint8_t i=0;i<4;i++)
{
for(uint8_t j=i+1;j<5;j++)
{
if(array_trans[j]<array_trans[i])
{
float temp;
temp = array_trans[j];
array_trans[j]=array_trans[i];
array_trans[i]=temp;
}
}
}
printf("qb:%.1f-%.1f-%.1f-%.1f-%.1f",array_trans[0],array_trans[1],array_trans[2],array_trans[3],array_trans[4]);
}
else
{
printf("error");
}
}
}
扩展数据采集
要求:
AD转换按照正常思路采集即可,脉冲也按照之前讲过的输入捕获的方式采集即可。这里主要讲解双PWM波的采集,之前可能有人(包括我)一直使用双通道来采集PWM,但遇到这道题,发现必须用单通道才行,因为要么通道冲突,要么基础定时器只有一个通道。所以今天分享单通道实现PWM采集的方法。
单通道采集PWM具体思路:采用状态机,总共三个状态,开始为上升沿触发,触发后记录当前值,并改为下降沿触发,下降沿触发时记录第二个值,此时比较第二个值与第一个值的关系,如果大于,相减即得高电平计数值,如果小于,代表计数已经溢出从零重新计数,此时高点平计数值=定时器计数上限-第一次计数值+第二次计数值。此时又改为上升沿触发进行状态上,同理求得下降沿计数值,最后就可以获得对应pwm波的占空比(这个方法需要注意一个问题,定时器的周期要保证大于探测信号的上升沿或下降沿周期之一,否则会出现套圈,当然具体是否有问题我也没使用。只不过如果pwm波的周期很大,大到其上升沿就可以走完定时器的整个计数周期,那么此时获得的高电平数值可能就会少整整定时器计数周期的数值)。
代码如下:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
//pa1的信号频率
if(htim->Instance==TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
{
uint32_t num=0;
num = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
__HAL_TIM_SetCounter(htim,0);
f=80000000/num;
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_2);
}
if(htim->Instance==TIM3)
{
//pa6,这种方法可能会有超过周期的风险,所以定时器的输入要经过一定的分频进入才行
if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)
{
switch(pa6_state)
{
case 0:
{
pa6_va1 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
__HAL_TIM_SET_CAPTUREPOLARITY(htim,TIM_CHANNEL_1,TIM_INPUTCHANNELPOLARITY_FALLING);
pa6_state++;
}
break;
case 1:
{
pa6_va2 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
if(pa6_va2>pa6_va1)
{
pa6_h = pa6_va2-pa6_va1;
}
else
{
pa6_h = 0xffff-pa6_va1+pa6_va2;
}
pa6_va1 = pa6_va2;
__HAL_TIM_SET_CAPTUREPOLARITY(htim,TIM_CHANNEL_1,TIM_INPUTCHANNELPOLARITY_RISING);
pa6_state++;
}
break;
case 2:
{
pa6_va2 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
if(pa6_va2>pa6_va1)
{
pa6_l = pa6_va2-pa6_va1;
}
else
{
pa6_l = 0xffff-pa6_va1+pa6_va2;
}
duty6 = pa6_h*100.0 / (pa6_h+pa6_l);
pa6_state=0;
}
break;
}
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_1);
}
if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_2)
{
switch(pa7_state)
{
case 0:
{
pa7_va1 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
__HAL_TIM_SET_CAPTUREPOLARITY(htim,TIM_CHANNEL_2,TIM_INPUTCHANNELPOLARITY_FALLING);
pa7_state++;
}
break;
case 1:
{
pa7_va2 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
if(pa7_va2>pa7_va1)
{
pa7_h = pa7_va2-pa7_va1;
}
else
{
pa7_h = 0xffff-pa7_va1+pa7_va2;
}
pa7_va1 = pa7_va2;
__HAL_TIM_SET_CAPTUREPOLARITY(htim,TIM_CHANNEL_2,TIM_INPUTCHANNELPOLARITY_RISING);
pa7_state++;
}
break;
case 2:
{
pa7_va2 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
if(pa7_va2>pa7_va1)
{
pa7_l = pa7_va2-pa7_va1;
}
else
{
pa7_l = 0xffff-pa7_va1+pa7_va2;
}
duty7 = pa7_h*100.0 / (pa7_h+pa7_l);
pa7_state=0;
}
break;
}
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_2);
}
}
}
上诉还对采集模式有要求,注意频率是持续采集,但PWM是根据信号采集,模式a通过按键,模式b通过光敏电阻的AD值。注意这里的光敏电阻采集同样采集的是一次跳变,同15届省赛的相同,不是变暗后一直采集,而是采集从亮到暗的一次变换。
这里还实现前面所说的环形数组存储,即通过一个大小为5的数组存储采集的数据,数组的下一个数据永远是在当前数据之后采集的,当然必须遵循环形,也就是当下标大于4会自动变为0,即4的下一个数据存放在下标0位置。
代码:
void measure_proc(void)
{
static bool once_flag=1;
uint32_t lightValue=0;
static uint32_t old_lightValue=0;
HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);
lightValue = HAL_ADC_GetValue(&hadc2);
HAL_ADC_Start(&hadc2);//重新开始
// char text[20];
// sprintf(text,"lightValue:%d ",lightValue);
// LCD_DisplayStringLine(Line5,(uint8_t *)text);
if(old_lightValue!=lightValue)//只会刷新一次
{
if(old_lightValue<2800 && lightValue>2800)
{
if(key[2].key2_mode_flag==1)
{
trigger_flag=1;
light_off=1;
}
}
old_lightValue = lightValue;
}
if(trigger_flag)
{
trigger_flag=0;
//对占空比进行处理
if(duty6<=10)
{
a=0;
}
else if(duty6<=90 && duty6>10)
{
a=2.25*duty6 - 22.5;
}
else
{
a=180;
}
if(duty7<=10)
{
b=0;
}
else if(duty7<=90 && duty7>10)
{
b=1.125*duty7 - 11.25;
}
else
{
b=90;
}
//记录5次,环形记录
array_a[n]=a;
array_b[n]=b;
n = (n+1)%5;
if(once_flag)
{
once_flag=0;
a_old=a;
b_old=b;//第一次无差值
}
//记录变化
ax = (a_old-a)>0?(a_old-a):(a-a_old);
a_old = a;
bx = (b_old-b)>0?(b_old-b):(b-b_old);
b_old = b;
}
}
最后大家注意数据的初始状态:
至此,十二届国赛全部模块完成。
三、总结
这是我准备国赛的第一套题,对我来说的难点在于单通道PWM采集,因为之前一直使用双通道,现在迫不得已采用了单通道,学到了新的思想。其次就是环形数组的实现,在下标上容易出现一些小错误,以及对串口不定长接收的复习,对光敏电阻的熄灭对应的ad值的检测等等。做题时还需细心,各模块的耦合应该减少,使代码的结构能够更清晰,少一些冗余代码,注意并记忆每个功能模块实现所需要的函数以及步骤。
学海无涯苦作舟。 如果有朋友需要整套源码,可以评论区或者私信,看到后提供。