接下来站在博主的视角来完成这次模拟题。首先,通读题目后我大致能大致能获取到如下信息:要完成一个密码锁的项目,用户通过串口通讯来设置三位密码,使用按键来输入密码。那我的思路先根据硬件框图搭出程序的大致框架,比如他这里的框图包括LED指示灯、LCD显示、串口通讯、按键输入功能,至于控制信号输出我我不知道所指,但第一感觉是输出PWM信号,那就可以先不管,等后面完善题目具体要求的时候看到了再补充就行。
开始搭建框架,打开Cube根据手册配置相应的GPIO。新建一个工程,搜索选择所用的STM32G431RB芯片→在RCC中打开时钟,然后配置时钟树→工程文件设置(文件名等)→以硬件框图模块依次配置GPIO口,打开串口1→生成文件
在生成的工程文件目录下,新建一个文件夹命名为bsp,在其中新建各模块的.c和.h文件。
在工程中添加一个文件夹bsp并将上面的.c文件添加进文件夹。在各模块.c文件中包含其.h文件,在.h文件中把国际框架写好,编译。接下来依次写各模块的代码,依次编译下载验证。
首先key.c,在cube中配置TIM4(前面备赛的经验),
#include "key.h"
struct KEY_STA key_sta[4] = {0}; //定义四个按键
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //定时器溢出中断回调函数
{
if(htim->Instance == TIM4) //判断是不是定时器4导致的中断
{
key_sta[0].status = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0); //先将按键GPIO的电平读出
key_sta[1].status = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key_sta[2].status = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key_sta[3].status = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
}
for(uint8_t i = 0;i < 4;i++) //分三个步骤来消抖
{
switch(key_sta[i].step)
{
case 0:
{
if(key_sta[i].status == 0) //若检测到电平为低
key_sta[i].step ++; //进入状态二
}
break;
case 1:
{
if(key_sta[i].status == 0) //若到了下次定时器中断电平仍未低
{
key_sta[i].flag = 1; //则断定是按键按下
key_sta[i].step ++; //进入状态三
}
else
key_sta[i].step = 0 ; //若不是按键按下则回到状态一
}
break;
case 2:
{
if(key_sta[i].status == 1) //当按键松开后
key_sta[i].step = 0 ; //回到状态一
}
break;
}
}
}
#ifndef _KEY_H
#define _KEY_H
#include "main.h"
#include //结构体定义的有布尔数据类型,需要包含这个头文件
struct KEY_STA //定义一个储存按键状态的结构体
{
bool flag; //按键按下标志
uint8_t status; //电平值
uint8_t step; //状态
};
#endif
led.c文件,用一个函数来指定对应led亮灭
#include "led.h"
void led_dis(uint8_t num) //定义一个选中led亮灯的函数,num为8位
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET); //拉高全部引脚,让led都熄灭
HAL_GPIO_WritePin(GPIOC,num << 8,GPIO_PIN_RESET); //因为led是PC8-15也就是高八位有效,所以先让num左移八位再拉低
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET); //锁存
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}
#ifndef _LED_H
#define _LED_H
#include "main.h"
void led_dis(uint8_t num);
#endif
有了按键和led模块,就可以结合起来验证一下实物是不是按照我们的想法实现的。先在main.c中包含所有模块的.h文件,发现警告没有找到文件(我打开了实时检测),要在魔术棒里把我们的文件所在路径添加到include path下。将
if(key_sta[0].flag == 1)
{
led_dis(0x80);
key_sta[0].flag = 0;
}
加入while/* USER CODE BEGIN 3 */
下,这里遇到key_sta的报错,因为这是在key.c中定义的结构体数组,要想在main.c中使用,需要在/* USER CODE BEGIN PTD */
下加入extern struct KEY_STA key_sta[4];
。编译无误,下载,显示NO LINK,需要在魔术棒的debug中选择。
再次下载,发现按下B1后LD8没有亮。发现在主函数中没有开启定时器中断,在/* USER CODE BEGIN 2 */
下加入HAL_TIM_Base_Start_IT(&htim4);
。编译下载,发现8个led上电全亮,在cube中将PC8-15设置为高电平后得以解决。
接下来写LCD显示模块。在比赛时给的数据包里会有LCD的例程,我们可以直接调用封装好的函数。首先将数据包里关于LCD的模块函数lcd.c/lcd.h/fonts.h复制到bsp文件夹中,并在工程文件中添加。打开其中LCD的工程文件,阅读代码查漏补缺,将初始化代码LCD_Init();
加入main.c中。在/* USER CODE BEGIN WHILE */
下加入 LCD_Clear(Black); LCD_SetBackColor(Black); LCD_SetTextColor(White);
来初始化屏幕界面。按题目所给要求,需要显示两个界面以区分输入密码前后,具体可见代码注释。
#include "dis_pro.h"
uint8_t view = 0; //定义一个变量来控制界面的切换
uint8_t temp[30] = {0};
void dis_pro()
{
if(view == 0) //通过按键来切换
{
sprintf((char *)temp," P S D"); //将待显示数据放入数组
LCD_DisplayStringLine(Line2,temp); //显示数组中的数据
sprintf((char *)temp," B 1 :");
LCD_DisplayStringLine(Line4,temp);
sprintf((char *)temp," B 2 :");
LCD_DisplayStringLine(Line5,temp);
sprintf((char *)temp," B 3 :");
LCD_DisplayStringLine(Line6,temp);
}
if(view == 1)
{
sprintf((char *)temp," S T A");
LCD_DisplayStringLine(Line2,temp);
sprintf((char *)temp," F :");
LCD_DisplayStringLine(Line4,temp);
sprintf((char *)temp," D :");
}
}
#ifndef _DIS_PRO_H
#define _DIS_PRO_H
#include "stdio.h"
#include "main.h"
#include "lcd.h"
void dis_pro(void);
#endif
当然,如果有报错说某某函数或变量没有定义,那就在相应的.h文件中添加相关声明或即可。当然在key.c中还定义了一个按键功能函数
void key_pro()
{
if(key_sta[3].flag == 1)
{
view ++;
if(view > 1)
view = 0;
LCD_Clear(Black);
key_sta[3].flag = 0;
}
}
下载验证,结果可观。
接下来写串口通信模块。
#include "uart.h"
uint8_t dat = 0; //创建变量来接收单个数据
uint8_t data[30] = {0}; //创建数组来储存接收的数据
uint8_t pointer = 0; //创建一个光标来指示当前所在数组的位置
uint8_t a[2] = {'1'}; //接收串口设置的密码的第一位
uint8_t b[2] = {'2'}; //接收串口设置的密码的第二位
uint8_t c[2] = {'3'}; //接收串口设置的密码的第三位
uint8_t d[5] = {0};
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) //串口接收中断回调函数
{
data[pointer++] = dat; //将当前接收到的单个数据存入数组
HAL_UART_Receive_IT(huart,&dat,1); //开启串口接收中断继续接收
}
void uart_pro() //定义一个函数实现串口的具体功能
{
if(pointer > 0)
{
if(pointer == 7) //若接收到七个数据,就把数据分放便于后续比较
{
sscanf((char*)data,"%3s-%1s%1s%1s",d,a,b,c); //将data这个数组中的数据按照后面定义的格式分别放入dabc四个数组中
}
else
{
char text[30];
sprintf(text,"ERROR");
HAL_UART_Transmit(&huart1,(uint8_t *)text,strlen(text),50);//若未接收完则向用户发送一个erro
}
pointer = 0; //光标回首位
memset(data,0,30); //数组归零
}
}
#ifndef _UART_H
#define _UART_H
#include "main.h"
#include "stdio.h"
#include "usart.h"
#include "string.h"
void uart_pro(void);
#endif
前面分析的基本模块现已基本实现,接下来仔细去看题目的要求,对各模块的具体功能要求。
首先,按键输入@,0-9几个数来模拟输入密码的过程,与串口设置的密码进行比对,B4按下时若密码输入正确,则切换界面。初始状态下三位密码显示皆为@。主要需要修改的就是按键模块。
void key_pro()
{
if(key_sta[0].flag == 1) //B1按键
{
if(n_1[0] == '@') //根据ASCII码表先将字符@改为0
n_1[0] -= 17;
n_1[0]++; //按键自增
if(n_1[0]>57) //超过9后变回@,@-0-9
n_1[0] = '@';
key_sta[0].flag = 0; //标志位归零
}
if(key_sta[1].flag == 1) //B2
{
if(n_2[0] == '@')
n_2[0] -= 17;
n_2[0]++;
if(n_2[0]>57)
n_2[0] = '@';
key_sta[1].flag = 0;
}
if(key_sta[2].flag == 1) //B3
{
if(n_3[0] == '@')
n_3[0] -= 17;
n_3[0]++;
if(n_3[0]>57)
n_3[0] = '@';
key_sta[2].flag = 0;
}
if(key_sta[3].flag == 1) //B4
{
if((a[0] == n_1[0]) && (b[0] == n_2[0]) && (c[0] == n_3[0])) //判断密码是否输入正确
{
view ++; //切换界面
if(view > 1)
view = 0;
LCD_Clear(Black);
key_sta[3].flag = 0;
}
}
}
下载发现初始密码可以解锁,但无法通过按键进行修改。问题出在写串口的时候,没有在main.c中开启串口接收中断,在/* USER CODE BEGIN 2 */
下加入HAL_UART_Receive_IT(&huart1,&dat,1);
开启即可。验证无误。
题目要求,输入密码成功后要有信号输出,前面看不懂的那个模块用处就在于此。使用PA1完成脉冲输出功能,没输入正确密码前PA1输出的是1KHz的方波信号,密码正确则变为2KHz 占空比为10%的方波信号。持续五秒后,又变回原来的信号以及密码输入界面。
那我们去cube中先配置PA1为TIM_CH2。因为初始为1KHz,分频80,重装载1000。而因为我们要在LCD上显示频率和占空比,计算频率可以随便用上升沿或下降沿,捕获到的相当于PWM波一个周期的时间,用80000000(时钟频率)/80(分频系数)/time(捕获到的时间)=频率。而占空比则是利用下降沿的时间除以整个周期时间,则为占空比。所以可以配置一个输入捕获(如PB4),利用其中两个通道来分别捕获上升沿和下降沿的时间来计算频率值。
再次生成代码并打开工程文件。在key.c中写入输入捕获中断回调函数。
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) //输入捕获中断回调函数
{
if(htim->Instance == TIM3)
{
time_1 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1); //读出计时值
time_2 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
__HAL_TIM_SetCounter(htim,0); //计时值归零
frq_1 = (80000000/80)/time_1; //计算当前频率
zk = (time_2/time_1)*100; //计算占空比(下降沿时间除以上升沿时间)
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_1); //打开输入捕获定时器
HAL_TIM_IC_Start_IT(htim,TIM_CHANNEL_2);
}
}
有了之前的教训,不要忘记在主函数中分别开启PWM信号和输入捕获中断。这里先不去管具体的功能,将frq_1和zk放入dis_pro.c中显示,想测试一下这个信号输出模块是否能成功。果不其然,代码逻辑是1000hz的频率,但显示出来就是233这个数,占空比为0。结果问题出在对变量的定义上,这里的代码没有给出变量定义,但文末我会附上整个工程文件。我写代码有个问题是在变量的定义上,不会去细想,这个数据的本质,只一心想着这个代码逻辑,问就是uint8_t类型,也不会报错,但出了问题也确实给我整嘛了。这里,time应该是double类型,而频率应该是uint16_t类型。
根据题目要求,密码输入正确后,频率会从4k变为2k且占空比为10%。在key_pro()
函数中增改如下代码
if(key_sta[3].flag == 1) //B4
{
if((a[0] == n_1[0]) && (b[0] == n_2[0]) && (c[0] == n_3[0])) //判断密码是否输入正确
{
LCD_Clear(Black); //输入正确则清屏切换界面
view ++;
__HAL_TIM_SET_AUTORELOAD(&htim2,530); //设置频率为2k
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,50); //设置占空比
HAL_Delay(5000);
__HAL_TIM_SET_AUTORELOAD(&htim2,999); //恢复频率为1k
if(view > 1)
view = 0;
key_sta[3].flag = 0;
}
}
编译无误,下载验证。再有,题目有信号输出精度要求,但是我按照这样的思路写出来他的频率已经超出精度范围了。我猜测是因为tim3开了两个通道的的缘故,因为一开始我没写占空比的测量,也就只开了一个捕获上升沿的通道,频率精度是正常的。这里可以调高函数中的参数值(如上面的代码__HAL_TIM_SET_AUTORELOAD
的参数我设置为了530)。
最后,LED功能部分。题目要求密码验证成功,LED1点亮五秒后熄灭,而如果暑促密码>=3次,LED2以100毫秒的间隔闪烁,五秒后熄灭。在key_pro()
函数中增改如下代码
if(key_sta[3].flag == 1) //B4
{
if((a[0] == n_1[0]) && (b[0] == n_2[0]) && (c[0] == n_3[0])) //判断密码是否输入正确
{
LCD_Clear(Black); //输入正确则清屏切换界面
view ++;
__HAL_TIM_SET_AUTORELOAD(&htim2,530); //设置频率为2k
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,50); //设置占空比
led_dis(0x01); //LED1亮5s
HAL_Delay(5000);
__HAL_TIM_SET_AUTORELOAD(&htim2,999); //恢复频率为1k
led_dis(0x00);
if(view > 1)
view = 0;
}
else
{
wrong ++; //错误次数
if(wrong >= 3)
{
for(uint8_t i = 0; i < 50; i++) //闪烁五秒
{
led_dis(0x02);
HAL_Delay(100);
led_dis(0x00);
HAL_Delay(100);
}
}
}
key_sta[3].flag = 0;
}
下载后的现象是,输入密码正确后会黑屏五秒,led会亮。问题就出在HAL_Delay()
这个函数中,他利用的原理就是让程序一直卡在一个while循环中等待,导致其他的操作执行不了。具体可以参照这个视频的讲解。这里的修改也是参考了视频所述的代码逻辑,代码如下:
if(key_sta[3].flag == 1) //B4
{
if((a[0] == n_1[0]) && (b[0] == n_2[0]) && (c[0] == n_3[0])) //判断密码是否输入正确
{
wrong = 0;
__HAL_TIM_SET_AUTORELOAD(&htim2,530); //设置频率为2k
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,50); //设置占空比
led_dis(0x01); //LED1亮5s
view ++;
LCD_Clear(Black); //输入正确则清屏切换界面
uwTick_pwm = uwTick;
while(uwTick - uwTick_pwm < 5000)//定义了一个变量,uwTick属于配置文件中定义好的变量,每隔1ms增加1
{
dis_pro(); //一开始uwTick_pwm为0,所以这样就相当于延时了5s
}
__HAL_TIM_SET_AUTORELOAD(&htim2,999);
led_dis(0x00);
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,0);
LCD_Clear(Black); //输入正确则清屏切换界面
view ++;
if(view > 1)
view = 0;
n_1[0] = '@';
n_2[0] = '@';
n_3[0] = '@';
key_sta[3].flag = 0;
}
else
{
wrong ++; //错误次数
if(wrong >= 3)
{
for(uint8_t i = 0; i < 25; i++) //闪烁五秒
{
led_dis(0x02);
HAL_Delay(100);
led_dis(0x00);
HAL_Delay(100);
}
}
key_sta[3].flag = 0;
}
这样的操作解决了黑屏问题,但是在延时5s回到密码输入界面时,频率应改回1k,经测验频率不太对。这里我是试错来设置参数值调出的1k和50%的占空比,一直找不到问题在哪里,欢迎小伙伴来一起讨论。
全部文件附上:
链接:https://pan.baidu.com/s/1MFJNhFBoXwUAyG4MwiPW0A?pwd=olm6
提取码:olm6