本套试题较为常规,试题主要需要使用的模块有:LCD、LED、按键、定时器输入捕获功能、采集光照传感器的值以及串口,其中最重要的是串口收发数据以及定时器的输入捕获功能,其余的各个部分还算比较常规、比较简单。下面咱就一起来看看这届赛题的题解吧!
通过查询产品手册知,LED的引脚为PC8~PC15,外加锁存器74HC573需要用到的引脚PD2。(由于题目要求除LED1、LED2、LED3、LED4外的其他LED都处于熄灭状态,此处特意将所有的LED都初始化以便于管理其他的LED灯)
CubeMX配置:
代码样例
由于G431的所有LED都跟锁存器74HC573连接,因此每次更改LED状态时都需要先打开锁存器,写入数据后再关闭锁存器。
/*****************************************************
* 函数功能:改变所有LED的状态
* 函数参数:
* char LEDSTATE: 0-表示关闭 1-表示打开
* 函数返回值:无
******************************************************/
void changeAllLedByStateNumber(char LEDSTATE)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_8
|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12,(LEDSTATE==1?GPIO_PIN_RESET:GPIO_PIN_SET));
//打开锁存器 准备写入数据
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
//关闭锁存器 锁存器的作用为 使得锁存器输出端的电平一直维持在一个固定的状态
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}
/*****************************************************
* 函数功能:根据LED的位置打开或者是关闭LED
* 函数参数:
* uint16_t LEDLOCATION:需要操作LED的位置
* char LEDSTATE: 0-表示关闭 1-表示打开
* 函数返回值:无
******************************************************/
void changeLedStateByLocation(uint16_t LEDLOCATION,char LEDSTATE)
{
HAL_GPIO_WritePin(GPIOC,LEDLOCATION,(LEDSTATE==1?GPIO_PIN_RESET:GPIO_PIN_SET));
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}
试题要求的LED显示其条件都比较单一,在满足点亮条件时直接点亮,否则,就直接熄灭即可:
/**************************************************
* 函数功能:LED工作函数
* 函数参数:无
* 函数返回值:无
***************************************************/
void ledPro(void)
{
// a角度变化大于其参数
if(data.ax > para.Pax)
changeLedStateByLocation(LED1,1);
else
changeLedStateByLocation(LED1,0);
// b角度变化大于其参数
if(data.bx > para.Pbx)
changeLedStateByLocation(LED2,1);
else
changeLedStateByLocation(LED2,0);
// f频率大于其参数
if(data.f > para.Pf)
changeLedStateByLocation(LED3,1);
else
changeLedStateByLocation(LED3,0);
// 处于模式A
if(data.mod == 'A')
changeLedStateByLocation(LED4,1);
else
changeLedStateByLocation(LED4,0);
// 角度a-b>10
if(data.a - data.b > 10)
changeLedStateByLocation(LED5,1);
else
changeLedStateByLocation(LED5,0);
}
样例代码
由于LCD的相关代码在官方给的比赛资源数据包中存在,因此,可以直接调用资源包中的.c、.h文件来完成LCD的相关初始化以及显示。这是一个简单的LCD初始化函数,其功能是将LCD显示屏初始化为一个背景色为黑色、字体颜色为白色的屏幕,具体代码如下:
/******************************************************************************
* 函数功能:LCD初始化
* 函数参数:无
* 函数返回值:无
*******************************************************************************/
void lcdInit(void)
{
//HAL库的初始化
LCD_Init();
//设置LCD的背景色
LCD_Clear(Black);
//设置LCD字体颜色
LCD_SetTextColor(White);
//设置LCD字体的背景色
LCD_SetBackColor(Black);
}
在显示时,可以借助于sprintf()
函数将需要显示的数据格式成一个字符串,再在LCD上显示这个字符串。
char temp[20];
sprintf(temp," mode:%c ",data.mod);
LCD_DisplayStringLine(Line8,(u8*)temp);
为了操作LED与LCD显示方便,不让其相互干扰,小编这里对LCD进行了部分源码改写,使得每次LCD显示时不改变LED的显示状态,具体的方法各位可以点击查看【蓝桥杯】一文解决蓝桥杯嵌入式开发板(STM32G431RBT6)LCD与LED显示冲突问题,并讲述LCD翻转显示。
通过查询产品手册知,开发板上的四个按键引脚为PB0~PB2、PA0。
CubeMX配置
样例代码
由于主板上的按键数量较少,因此小编这里的按键读取操作相对简单粗暴,其实现步骤为:
- 步骤一:判断按键是否按下以及按键锁是否打开,在两者同时满足的情况下进入下一步;
- 步骤二:关闭按键锁并且延时10ms,实现按键的延时消抖;
- 步骤三:再次读取每个按键的值,判断按键按下的位置;
- 步骤四:读取每个按键的状态,如果都处于松开状态就打开按键锁;
具体代码实现:
/*********************************************
* 函数功能:按键扫描 含按键消抖 无长按短按设计
* 函数参数:无
* 函数返回值:按键的位置
* 返回值说明:B1-1 B2-2 B3-3 B4-4
*********************************************/
unsigned char scanKey(void)
{
//按键锁
static unsigned char keyLock = 1;
//记录按键消抖时间
// static uint16_t keyCount = 0;
//按键按下
if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == RESET || HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == RESET
|| HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == RESET || HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET)
&& keyLock == 1){
//给按键上锁 避免多次触发按键
keyLock = 0;
//按键消抖 这里最好不要使用延时函数进行消抖 会影响系统的实时性
// if(++keyCount % 10 < 5) return 0;
// if(HAL_GetTick()%15 < 10) return 0;
HAL_Delay(10);
//按键B1
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == RESET){
return 1;
}
//按键B2
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == RESET){
return 2;
}
//按键B3
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == RESET){
return 3;
}
//按键B4
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET){
return 4;
}
}
//按键松开
if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == SET && HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == SET
&& HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == SET && HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == SET)
&& keyLock == 0){
//开锁
keyLock = 1;
}
return 0;
}
当然了,在本届试题中按键逻辑不仅仅是判断按键是否按下这么简单,主板上的每个按键都具有其特有的功能,下面就一起来看看吧!
每个按键的功能:
- 按键B1:具有切换显示界面的功能;
- 按键B2:具有改变角度变化参数的功能;
- 按键B3:具有改变频率参数以及频率采集触发模式的功能;
- 按键B4:具有在按键触发模式下刷新角度数据采集的功能;
样例代码实现:
/* -------------------------------- begin -------------------------------- */
/**
* @Name sysInit
* @brief 按键逻辑函数
* @param None
* @retval None
* @author 黑心萝卜三条杠
* @Data 2023-03-24
**/
/* -------------------------------- end -------------------------------- */
static void keyPro(void)
{
static char mod[2] = {0,0};
unsigned char keyValue = scanKey();
switch(keyValue){
case 1:
// 切换LCD显示界面
mod[0] ^= 1;
break;
case 2:
// 增加角度变化参数
if(mod[0] == 1)
{
para.Pax += 10;
para.Pbx += 10;
if(para.Pax == 70)
para.Pax = 10;
if(para.Pbx == 70)
para.Pbx = 10;
}
break;
case 3:
// 增加频率参数
if(mod[0] == 1)
{
para.Pf += 1000;
if(para.Pf == 11000)
para.Pf = 1000;
}
// 切换触发模式
else if(mod[0] == 0)
{
mod[1] ^= 1;
data.mod = 'A' + mod[1];
}
break;
case 4:
// 数据刷新
if(data.mod == 'A')
falg = 1;
break;
default: break;
}
// LCD显示
lcdDisplay(mod[0]);
}
CubeMX配置
在写本届试题的串口接收PC数据时需要注意:PC发送不定长的数据。这时就需要按位储存PC发送过来的数据,每次处理完成后就清空历史数据。
CubeMX配置
配置时一定一定记得改引脚!!!
样例代码
本程序中小编使用的是中断接收PC发送的数据其函数原型为:
// 函数原型:
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
// 参数解析:
UART_HandleTypeDef *huart:串口通道;
uint8_t *pData:存放数据的buff;
uint16_t Size:一次接收数据的长度
在使用时还需要使用该函数“中断初始化”,否则不能够进入中断接收数据;
下面就是一个串口接收不定长数据的demo:
/**********************************************串口相关************************************/
//定义一个串口信息的结构
uint8_t ucRxbuff[4];
uint8_t _ucRxbuff[1],lenBuff = 0;
/***使用HAL_UART_Receive_IT中断接收数据 每次接收完成数据后就会执行该函数***/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1){
ucRxbuff[lenBuff++%3] = _ucRxbuff[0];
// 重新使能中断
HAL_UART_Receive_IT(huart,(uint8_t *)&_ucRxbuff,sizeof(_ucRxbuff));
}
}
虽然说是不定长数据,但是其能够接收的数据最长为3个字节。由于题中串口接收的有效数据最长为3字节,因此此处小编也将其设置成3字节。
题中要求串口功能不仅仅是接收数据这么简单,其还需要能够解析串口接收的数据,并且以此为指令将合适的结果发送给PC。下面就是小编写的一个简单的数据处理demo:
/* -------------------------------- begin -------------------------------- */
/**
* @Name usartPro
* @brief 串口处理函数
* @param None
* @retval None
* @author 黑心萝卜三条杠
* @Data 2023-03-24
**/
/* -------------------------------- end -------------------------------- */
static void usartPro(void)
{
char temp [40];
// 去除一些不非正常情况的结果
if(strlen((char*)ucRxbuff) == 0)
return ;
// 查询当前数据
if(strcmp((char*)ucRxbuff,"a?") == 0)
sprintf(temp,"a:%1.f\r\n",data.a);
else if(strcmp((char*)ucRxbuff,"b?") == 0)
sprintf(temp,"b:%1.f\r\n",data.b);
// 查询历史数据(按时间顺序)
else if(ucRxbuff[0]=='a' && ucRxbuff[1]=='a' && ucRxbuff[2]=='?')
sprintf(temp,"aa:%1.f-%1.f-%1.f-%1.f-%1.f\r\n",oldData[0][0],oldData[0][1],oldData[0][2],oldData[0][3],oldData[0][4]);
else if(ucRxbuff[0]=='b' && ucRxbuff[1]=='b' && ucRxbuff[2]=='?')
sprintf(temp,"bb:%1.f-%1.f-%1.f-%1.f-%1.f\r\n",oldData[1][0],oldData[1][1],oldData[1][2],oldData[1][3],oldData[1][4]);
// 查询历史数据(按大小)
else if(ucRxbuff[0]=='q' && ucRxbuff[1]=='a' && ucRxbuff[2]=='?')
{
sort(oldData[0],5,sortData);
sprintf(temp,"aa:%1.f-%1.f-%1.f-%1.f-%1.f\r\n",sortData[0],sortData[1],sortData[2],sortData[3],sortData[4]);
}
else if(ucRxbuff[0]=='q' && ucRxbuff[1]=='b' && ucRxbuff[2]=='?')
{
sort(oldData[1],5,sortData);
sprintf(temp,"bb:%1.f-%1.f-%1.f-%1.f-%1.f\r\n",sortData[0],sortData[1],sortData[2],sortData[3],sortData[4]);
}
// 接收到其他数据
else
sprintf(temp,"error\t\n");
// 串口数据发送
HAL_UART_Transmit(&huart1,(uint8_t*)temp,sizeof(char)*strlen(temp),20);
memset(ucRxbuff,0,sizeof(ucRxbuff));
lenBuff = 0;
}
光照传感器的数据获取基于ADC数据获取,因此这里只要合理初始化ADC及其通道即可。这里小编在启用ADC通道后只修改了一个地方——采样周期。
样例代码
这里的ADC获取函数没有什么高大上的,与寻常的ADC获取函数一样:
// 外部声明的ADC2
extern ADC_HandleTypeDef hadc2;
/* -------------------------------- begin -------------------------------- */
/**
* @Name getADC
* @brief 获取光敏电阻的阻值
* @param None
* @retval float:转换后的值
* @author 黑心萝卜三条杠
* @Data 2023-03-25
**/
/* -------------------------------- end -------------------------------- */
float getR47ADC(void)
{
uint16_t temp = 0;
HAL_ADC_Start(&hadc2);
temp = HAL_ADC_GetValue(&hadc2);
return temp/(4096 - temp) * 10;
}
CubeMX配置
本届试题定时器的主要功能为输入捕获,要不就是捕获频率,要不就是捕获其占空比。
捕获频率时的配置:
利用输入捕获功能计算占空比时的配置:
这里需要注意:利用输入捕获功能计算频率与占空比其触发方式上是不一样的,前者是单沿触发,后者是双沿触发。由于PA6、PA7两个引脚的捕获功能配置上是一样的,因此此处只放置一个配置示图。
样例代码
小编在网上看到了某些大佬测量占空比时只测量了两次,即一次上升沿与一次下降沿,然后就可以得到PWM的占空比。但是这里小编使用的是测量三次,因为小编认为测量两次并不能够计算出占空比,详细的解析各位可以查看这篇文章使用定时器输入捕获功能测量占空比及频率。(当然了这是我个人的观点,如果大家认为有啥不对之处,欢迎大家留言或私信)
那么这里小编就不过多赘述啦,上面的文章写的确实很清楚了。哦对了,定时器的输入捕获需要自己使用函数HAL_TIM_IC_Start_IT(&htim16,TIM_CHANNEL_1);
打开的嗷。
/**********************************************定时器输入捕获相关************************************/
//记录定时器上升沿 下降沿计数值
struct date
{
//记录计数值 偶数为上升沿 计数的下降沿
uint32_t count[3];
//用于记录当前应该是上升沿触发还是下将沿触发
char edge_flag;
//记录数据采集数量
int number;
};
// 保存定时器输入捕获时需要的相关变量以及最终结果
struct date time16Data,time3Data;
// 保存PWM的数据值 RP[0]-PWM1的值 RP[1]-PWM2的值
uint32_t RP[4] = {0,0,0,0};
// 定时器的回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
// 保存TIMx_CCR的值
uint32_t cclValue = 0;
// 定时器16时执行该段
if(htim->Instance == TIM16)
{
//本次为上升沿触发
if(time16Data.edge_flag%2==0)
{
//获取本次上升沿计数值
time16Data.count[time16Data.number++] = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
//采集一次下降沿、两次上升沿数据完成
if(time16Data.number == 3)
{
RP[0] = (((time16Data.count[1]-time16Data.count[0])*1.0)/(time16Data.count[2]-time16Data.count[0]))*100;
//将定时器的计数值设置成0
__HAL_TIM_SetCounter(htim,0);
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
//重新开启定时器
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_1);
time16Data.edge_flag = 0;
time16Data.number = 0;
}
else
{
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
//将下次触发方式设置为下降沿触发
time16Data.edge_flag += 1;
}
}
//下降沿触发
else
{
//读取本轮的计数值
time16Data.count[time16Data.number++] = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
//修改触发方式为上升沿触发
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
time16Data.edge_flag += 1;
}
HAL_TIM_IC_Start_IT(&htim16, TIM_CHANNEL_1);
}
// 定时器3时执行该段
if(htim->Instance == TIM3)
{
//本次为上升沿触发
if(time3Data.edge_flag%2==0)
{
//获取本次上升沿计数值
time3Data.count[time3Data.number++] = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
//采集一次下降沿、两次上升沿数据完成
if(time3Data.number == 3)
{
RP[1] = (((time3Data.count[1]-time3Data.count[0])*1.0)/(time3Data.count[2]-time3Data.count[0]))*100;
//将定时器的计数值设置成0
__HAL_TIM_SetCounter(htim,0);
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_2, TIM_INPUTCHANNELPOLARITY_RISING);
//重新开启定时器
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_2);
time3Data.edge_flag = 0;
time3Data.number = 0;
}
else
{
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_2, TIM_INPUTCHANNELPOLARITY_FALLING);
//将下次触发方式设置为下降沿触发
time3Data.edge_flag += 1;
}
}
//下降沿触发
else
{
//读取本轮的计数值
time3Data.count[time3Data.number++] = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
//修改触发方式为上升沿触发
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_2, TIM_INPUTCHANNELPOLARITY_RISING);
time3Data.edge_flag += 1;
}
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);
}
// 定时器2时执行该段
if(htim->Instance == TIM2)
{
cclValue = __HAL_TIM_GET_COUNTER(&htim2);
__HAL_TIM_SetCounter(&htim2, 0);
RP[2] = 1000000 / cclValue;
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2);
}
}
(大家看到这个中断处理函数是不是非常不爽,非常多的重复代码,主要是小编暂时还没想到更好的书写方式。)
这里的刷新函数高度符合题意,并且代码逻辑简单,这里不做过多描述,大家可以直接看代码:
/* -------------------------------- begin -------------------------------- */
/**
* @Name getData
* @brief 数据更新函数
* @param None
* @retval None
* @author 黑心萝卜三条杠
* @Data 2023-03-24
**/
/* -------------------------------- end -------------------------------- */
static void getData(void)
{
int flagPro = 0;
float temp = 0;
// 更新频率
data.f = RP[2];
// 触发模式
if(data.mod == 'A' && falg == 1)
{
falg = 0;
flagPro = 1;
}
// 环境光照值触发
else if(data.mod == 'B' )
{
lightData[0] = getR47ADC();
if(lightData[0]-lightData[1] > 10)
flagPro = 1;
// 保存本次光照值
lightData[1] = lightData[0];
}
// 采集了数据 需要更新角度值
if(flagPro == 1)
{
// 根据折线图转换角度a
if(0 < RP[0] && RP[0] <= 10)
temp = 0;
else if(10 < RP[0] && RP[0] < 90)
temp = (RP[0]-10)*9*1.0/4;
else
temp = 180;
// 求与上次角度的差值
data.ax = myAbs(temp,data.a);
// 保存本次数据
data.a = temp;
// 更新历史数据
oldData[0][oldCount[0]] = temp;
if(++oldCount[0] == 5) oldCount[0] = 0;
// 根据折线图转换角度b
if(0 < RP[1] && RP[1] <= 10)
temp = 0;
else if(10 < RP[1] && RP[1] < 90)
temp = (RP[1]-10)*9*1.0/8;
else
temp = 90;
// 求与上次角度的差值
data.bx = myAbs(temp,data.b);
// 保存本次数据
data.b = temp;
// 更新历史数据
oldData[1][oldCount[1]] = temp;
if(++oldCount[1] == 5) oldCount[1] = 0;
}
}
(尽管文章中使用定时器测量PWM的占空比理论上小编非常自信,但是小编使用示波器看过PWM输出的波形,这玩意非常乱,小编也不知道其结果到底正不正确,如果大家有好的方法欢迎大家留言或私信嗷!)
这里的排序函数主要用于指令qa?
与 qb?
,只有接收到这两个指令才会调用这个函数排序。这里可能大家回想:为什么不每次一获取到新数据就进行排序,那就有可能不用写一个0(n2)的排序算法了?其实小编有想过这种情况,但是这样子写会占用更多的CPU资源。一种极端的情况:开机以后PC一直不发送这两个指令给主板,那么主板的排序是不是会没啥用呢?
/* -------------------------------- begin -------------------------------- */
/**
* @Name sort
* @brief 对长度为len的数组data进行不改变原数组顺序的排序 将结果保存在resData
* @param float*data:源数据 int len:源数据长度 float*resData:排序后结果
* @retval None
* @author 黑心萝卜三条杠
* @Data 2023-03-24
**/
/* -------------------------------- end -------------------------------- */
static void sort(float*data,int len,float*resData)
{
// 复制原数组到新数组中
memcpy(resData, data, len * sizeof(float));
// 冒泡排序
for (int i = 0; i < len - 1; ++i) {
for (int j = i + 1; j < len; ++j) {
if (resData[i] > resData[j]) {
int temp = resData[i];
resData[i] = resData[j];
resData[j] = temp;
}
}
}
}
求绝对值函数用于求最近两次角度的差值,这里的差值肯定是一个正数,因此小编就写一个求绝对值的玩意。
/* -------------------------------- begin -------------------------------- */
/**
* @Name myAbs
* @brief 自己写的求绝对值函数
* @param float a:参与求绝对值的数1 float b:参与求绝对值的数2
* @retval None
* @author 黑心萝卜三条杠
* @Data 2023-03-24
**/
/* -------------------------------- end -------------------------------- */
static float myAbs(float a,float b)
{
float temp = a-b;
if(temp >0) return temp;
else return -temp;
}
下边是小编个人整理出来免费的蓝桥杯嵌入式福利,有需要的童鞋可以自取哟!