目录
[0 前言](#0 前言)
[1 展示](#1 展示)
[1.1 源码](#1.1 源码)
[1.2 演示视频](#1.2 演示视频)
[1.3 题目展示](#1.3 题目展示)
[2 CubeMX配置(第十三届省赛第二场真题)](#2 CubeMX配置(第十三届省赛第二场真题))
[2.1 设置下载线](#2.1 设置下载线)
[2.2 HSE时钟设置](#2.2 HSE时钟设置)
[2.3 时钟树配置](#2.3 时钟树配置)
[2.4 生成代码设置](#2.4 生成代码设置)
[2.5 USART1](#2.5 USART1)
[2.5.1 基本配置](#2.5.1 基本配置)
[2.5.2 NVIC](#2.5.2 NVIC)
[2.5.3 DMA](#2.5.3 DMA)
[2.6 TIM](#2.6 TIM)
[2.6.1 TIM2](#2.6.1 TIM2)
[2.6.2 TIM4](#2.6.2 TIM4)
[2.6.3 TIM6](#2.6.3 TIM6)
[3 引脚配置](#3 引脚配置)
[4 代码相关定义、声明](#4 代码相关定义、声明)
[4.1 变量声明](#4.1 变量声明)
[4.2 函数声明](#4.2 函数声明)
[5 主要函数](#5 主要函数)
[5.1 LCD](#5.1 LCD)
[5.2 KEY](#5.2 KEY)
[5.3 EEPROM](#5.3 EEPROM)
[5.4 LED](#5.4 LED)
[5.5 PWM](#5.5 PWM)
[5.6 定时器回调函数](#5.6 定时器回调函数)
[5.7 串口回调函数](#5.7 串口回调函数)
[5.8 串口](#5.8 串口)
[5.9 上电初始化](#5.9 上电初始化)
[6 测试](#6 测试)
[7 做题感受](#7 做题感受)
菜狗上线~~~
0 前言
- 开发板:CT117E-M4(STM32G431RBT6)
- 软件环境:CubeMX + Keil5
- 涉及题目:第十三届蓝桥杯嵌入式省赛第二场真题
1 展示
1.1 源码
Gitee链接:
1.2 演示视频
B站链接:
1.3 题目展示
2 CubeMX配置(第十三届省赛第二场真题)
2.1 设置下载线
2.2 HSE时钟设置
2.3 时钟树配置
晶振一定要改成24M,晶振一定要改成24M,晶振一定要改成24M,主频80M
2.4 生成代码设置
2.5 USART1
2.5.1 基本配置
题目要求串口1波特率9600
2.5.2 NVIC
串口一定要使能NVIC
2.5.3 DMA
添加两个DMA即可
2.6 TIM
2.6.1 TIM2
TIM2用作PWM输出功能,通过引脚PA1输出
配置成2KHz的PWM
定时器2也要使能NVIC
2.6.2 TIM4
TIM4做基准定时器,10ms定时,专门用来按键扫描
预分频系数80-1
重装载值10000-1
定时器4也要使能NVIC
2.6.3 TIM6
定时器65用来实现倒计时5S,不开定时器6也可以用定时器4实现,这里我不想让倒计时和按键有联系,就多开了一个定时器,配置如下
定时器6也要使能NVIC
3 引脚配置
- 配置8个LED PD2 outpp PC8~PC15 outpp
- 配置4个按键 PA0、PB0、PB1、PB2 配置为上拉输入模式
- 配置串口1 PA9 PA10
- PWM PA1(TIM2-CH2)
4 代码相关定义、声明
4.1 变量声明
主要的变量定义如下所示,用了两个结构体,一个是写参数的,一个是写标志位的
cpp
/* 定义结构体 */
struct Param_TypeDef
{
u32 LED_Tick; // LED定时 函数减速
u32 LCD_Tick; // LCD定时 函数减速
u32 RX_Tick; // RX 定时 函数减速
u32 PWM_Tick; // PWM定时 函数减速
u32 EEP_Tick; // EEPROM 定时
u8 LED_State; // LED状态变量
u16 Set_PA1_Freq; //
u8 Set_PA1_Duty;
u8 Shop_Num_X; // 购买数量X
u8 Shop_Num_Y; // 购买数量Y
float Price_X; // 单价X
float Price_Y; // 单价Y
u8 REP_X; // 库存X
u8 REP_Y; // 库存Y
float All_Price; // 总价
u8 last_rep_X; // 上次的X
u8 last_rep_Y; // 上次的Y
float Last_Price_X; // 单价X
float Last_Price_Y; // 单价Y
};
struct Flag_TypeDef
{
bool LCD_Dir;
u8 LCD_View; // LCD界面
u8 Current_Platform; // 当前平台
bool Key4_Press; // KEY4按下
bool led2_state;
};
extern struct Param_TypeDef param;
extern struct Flag_TypeDef flag;
/* 定义结构体 */
// 定义状态机状态
#define SHOP 0
#define PRICE 1
#define REP 2
4.2 函数声明
cpp
/* 函数声明 */
void LED_proc(void);
void LCD_Disp_proc(void);
void Key_proc_Loop(void);
void Power_Init(void); // 上电初始化
void RX_Proc(void); // 串口接收函数
void PWM_Set_Proc(void);
void EEPROM_Proc(void);
5 主要函数
5.1 LCD
LCD一共有三个界面,分别是购买界面、单价界面、库存界面,分别题目要求到的标题和内容~
这里的MYLCD_printf()函数是我自己封装的
cpp
// LCD显示
void LCD_Disp_proc(void)
{
// 函数减速
if (uwTick - param.LCD_Tick < 50)
return;
param.LCD_Tick = uwTick;
// 执行任务
if (flag.LCD_View == SHOP)
{ // 购买界面
MYLCD_printf(Line1, " SHOP ");
MYLCD_printf(Line3, " X:%2d ", param.Shop_Num_X);
MYLCD_printf(Line4, " Y:%2d ", param.Shop_Num_Y);
}
else if (flag.LCD_View == PRICE)
{ // 单价界面
MYLCD_printf(Line1, " PRICE ");
MYLCD_printf(Line3, " X:%.1f ", param.Price_X);
MYLCD_printf(Line4, " Y:%.1f ", param.Price_Y);
}
else if (flag.LCD_View == REP)
{ // 库存界面
MYLCD_printf(Line1, " REP ");
MYLCD_printf(Line3, " X:%2d ", param.REP_X);
MYLCD_printf(Line4, " Y:%2d ", param.REP_Y);
}
}
正常的LCD显示语句,每次都要使用之前清零,再用sprint函数拼接字符串再调用LCD的显示函数,为了显示的更加简洁我用可变参数列表封装了这三行代码~代码如下
cpp
// // 使用之前先清除显示数组,再填写内容
// memset(LCD_Show_text, '\0', sizeof(LCD_Show_text));
// sprintf(LCD_Show_text, " LED:OFF ");
// LCD_DisplayStringLine(Line5, (uint8_t *)LCD_Show_text);
cpp
void MYLCD_printf(unsigned char linex, char *format, ...)
{
char LCD_Show_text[30];
memset(LCD_Show_text, '\0', sizeof(LCD_Show_text));
va_list arg; // 定义可变参数列表数据类型的变量arg
va_start(arg, format); // 从format开始,接收参数列表到arg变量
vsprintf(LCD_Show_text, format, arg); // 使用vsprintf打印格式化字符串和参数列表到字符数组中
LCD_DisplayStringLine(linex, (uint8_t *)LCD_Show_text);
va_end(arg); // 结束变量arg
// sprintf((char *)LCD_Show_text, str);
}
思路如下:
- //先清除数组的内容
- // 定义可变参数列表数据类型的变量arg
- // 从format开始,接收参数列表到arg变量
- // 使用vsprintf打印格式化字符串和参数列表到字符数组中
- //LCD显示
- // 结束变量arg
5.2 KEY
按键处理函数,使用01Stdio的按键扫描函数,放在定时器4里10ms扫描一次
按键1是切换界面
按键2是购买数量,注意越界回滚
按键3是购买单价,注意越界回滚,这个但是是浮点数,这里就有说到了,浮点数的2.0f其实不准确,如果判断当前价格>2.0了就参数回滚,会出现1.9直接跳变到2.0,所以我们把比较的数值改成2.01f就完美解决了~~~
按键4 计算总计,串口上传数据,发送完成,单价清零
cs
void Key_proc_Loop(void)
{
if (bkey[1].short_flag == 1)
{
flag.LCD_View++;
if (flag.LCD_View >= 3)
flag.LCD_View = 0; // 三个界面
bkey[1].short_flag = 0;
}
if (bkey[2].short_flag == 1)
{
if (flag.LCD_View == SHOP)
{ // 购买界面
param.Shop_Num_X++;
if (param.Shop_Num_X > param.REP_X) // 超过库存,越界回滚成0
param.Shop_Num_X = 0;
}
else if (flag.LCD_View == PRICE)
{ // 单价界面
param.Price_X += 0.1f;
if (param.Price_X > 2.01f) // 注意浮点数
param.Price_X = 1.0f; // 越界回滚
}
else if (flag.LCD_View == REP)
{ // 库存界面
param.REP_X++;
}
bkey[2].short_flag = 0;
}
if (bkey[3].short_flag == 1) //
{
if (flag.LCD_View == SHOP)
{ // 购买界面
param.Shop_Num_Y++;
if (param.Shop_Num_Y > param.REP_Y) // 超过库存,越界回滚成0
param.Shop_Num_Y = 0;
}
else if (flag.LCD_View == PRICE)
{ // 单价界面
param.Price_Y += 0.1f;
if (param.Price_Y > 2.01f) // 注意浮点数
param.Price_Y = 1.0f; // 越界回滚
}
else if (flag.LCD_View == REP)
{ // 库存界面
param.REP_Y++;
}
bkey[3].short_flag = 0;
}
if (bkey[4].short_flag == 1) //
{
if (flag.LCD_View == SHOP)
{ // 购买界面
flag.Key4_Press = 1;
param.REP_X -= param.Shop_Num_X;
param.REP_Y -= param.Shop_Num_Y;
param.All_Price = param.Shop_Num_X * param.Price_X + param.Shop_Num_Y * param.Price_Y;
memset(USART_tx_string, '\0', sizeof(USART_tx_string)); // 变量清零
sprintf(USART_tx_string, "X:%d,Y:%d,Z:%.1f", param.Shop_Num_X, param.Shop_Num_Y, param.All_Price);
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)USART_tx_string, strlen(USART_tx_string));
// 发送完成,单价清零
param.Shop_Num_X = 0;
param.Shop_Num_Y = 0;
}
bkey[4].short_flag = 0;
}
}
5.3 EEPROM
这个函数100ms调用一次,在函数里判断当前值和100ms以前的值是否相等,如果不相等,就说明数据变化了,就需要存储到EEPROM中,存到对应的地址里
要存储的单价是一位浮点数,乘以10变成整数存进去,读取的时候不要忘记除以10即可
这里的逻辑是先判断,后赋值,如果先赋值后判断,永远检测不出来数据跳变,这里要细细品味一下,有之前写的按键扫描的味道~
void EEPROM_Proc(void)
{
if (uwTick - param.EEP_Tick < 100)
return;
param.EEP_Tick = uwTick;
if (param.last_rep_X != param.REP_X)
{
EEPROM_WriteByte(0, param.REP_X); // 剩余数量X
HAL_Delay(10);
}
if (param.last_rep_Y != param.REP_Y)
{
EEPROM_WriteByte(1, param.REP_Y); // 剩余数量Y
HAL_Delay(10);
}
if (param.Last_Price_X != param.Price_X)
{
EEPROM_WriteByte(2, (param.Price_X * 10)); // 单价X
HAL_Delay(10);
}
if (param.Last_Price_Y != param.Price_Y)
{
EEPROM_WriteByte(3, (param.Price_Y * 10)); // 单价Y
HAL_Delay(10);
}
param.last_rep_X = param.REP_X;
param.last_rep_Y = param.REP_Y;
param.Last_Price_X = param.Price_X;
param.Last_Price_Y = param.Price_Y; // 延迟赋值
}
5.4 LED
LED显示函数,只需要写一次即可,我们只需要修改param.LED_State这个变量,就能达到控制哪一位LED亮灭的状态
LED显示多用位运算,非常巧妙~
cs
//LED驱动函数
void LED_Disp(unsigned char state)
{
HAL_GPIO_WritePin(GPIOC, 0xFF00, GPIO_PIN_SET); // 先全部熄灭 1
HAL_GPIO_WritePin(GPIOC, state << 8, GPIO_PIN_RESET); // 点亮 0
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET); // 锁存器置高,使能
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET); // 锁存器置低,失能
}
cs
// LED处理函数
void LED_proc(void)
{
static u8 led1, led2;
// 函数减速
if (uwTick - param.LED_Tick < 20)
return;
param.LED_Tick = uwTick;
led1 = flag.Key4_Press << 0; // 5S内是1
led2 = flag.led2_state << 1;
param.LED_State = led1 | led2;
LED_Disp(param.LED_State);
}
5.5 PWM
- 设置频率,设置占空比,只修改变量即可
- __HAL_TIM_SetAutoreload设置重装载值 ,80分频之后是1MHz = 1e6,用1e6/要设置的频率就是重装载值
- __HAL_TIM_SetCompare设置比较值 ,设置高电平时间,即设置占空比,比较值=重装载值*占空比
cs
void PWM_Set_Proc(void)
{
if (uwTick - param.PWM_Tick < 100)
return;
param.PWM_Tick = uwTick;
// 设置频率
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
param.Set_PA1_Freq = 2000; //2000Hz
__HAL_TIM_SetAutoreload(&htim2, 1e6 / param.Set_PA1_Freq - 1);
// 设置占空比
if (flag.Key4_Press == 1) // 5S内
param.Set_PA1_Duty = 30; //30%占空比
else
param.Set_PA1_Duty = 5; //5%占空比
__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, (1e6 / param.Set_PA1_Freq - 1) * param.Set_PA1_Duty / 100 + 1); //设置占空比函数
}
5.6 定时器回调函数
定时器4专门用来10ms扫描一次按键
定时器6用来处理LED的5S点亮,和0.1S闪烁,使用标志位判断即可,这些都是常规操作~
cs
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint16_t LED_cnt, LED_cnt2;
if (htim->Instance == TIM4)
{
key_serv_double(); // 10ms按键处理
}
if (htim->Instance == TIM6) // 10ms进入一次 处理倒计时
{
if (flag.Key4_Press == 1)
{
if (++LED_cnt >= 500)
{
LED_cnt = 0;
flag.Key4_Press = 0;
}
}
if ((param.REP_X == 0) && (param.REP_Y == 0)) // 如果XY的库存都等于0
{
if (++LED_cnt2 >= 10)
{
LED_cnt2 = 0; // 100ms
flag.led2_state = !flag.led2_state;
}
}
else
flag.led2_state = 0;
}
}
5.7 串口回调函数
cs
// 串口的接收 回调函数
char USART_tx_string[50];
char rxdata[100];
uint8_t RX_Str_Data;
unsigned char rx_pointer; // 自己定义的指针,判断接收到哪了
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) // 如果是USART1
{
param.RX_Tick = uwTick;
rxdata[rx_pointer++] = RX_Str_Data; // 接收到的字符串存放在这里
HAL_UART_Receive_DMA(&huart1, &RX_Str_Data, 1); // 最后这个参数只能写1
}
}
5.8 串口
cs
void RX_Proc(void)
{
if (uwTick - param.RX_Tick < 50)
return;
param.RX_Tick = uwTick;
// 执行任务
if (rx_pointer == 1 && rxdata[0] == '?') // 如果收到一个数据,并且是#
{
memset(USART_tx_string, '\0', sizeof(USART_tx_string)); // 变量清零
sprintf(USART_tx_string, "X:%.1f,Y:%.1f", param.Price_X, param.Price_Y);
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)USART_tx_string, strlen(USART_tx_string));
}
else if (rx_pointer > 0)
{
}
rx_pointer = 0; // 指针归位
memset(rxdata, '\0', sizeof(rxdata)); // 变量清零
}
5.9 上电初始化
- LCD初始化
- 按键初始化
- 定时器初始化
- PWM初始化
- 串口DMA发送初始化
- 串口DMA接收初始化
- 检测是否第一次上电,如果是第一次上电,就初始化库存和单价
cs
// 上电初始化
void Power_Init(void)
{
LED_Disp(0x00); // 关掉所有LED
LCD_Init(); // LCD初始化
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
LCD_DrawLine(120, 0, 320, Horizontal);
LCD_DrawLine(0, 160, 240, Vertical);
HAL_Delay(150);
LCD_Clear(Blue);
LCD_DrawRect(70, 210, 100, 100);
HAL_Delay(150);
LCD_Clear(Blue);
LCD_DrawCircle(120, 160, 50);
HAL_Delay(150);
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
KEY_GPIO_Init(); // 手动初始化,防止忘记配置CubeMX
// 定时器初始化
HAL_TIM_Base_Start_IT(&htim4);
HAL_TIM_Base_Start_IT(&htim6); // 用于倒计时
// // PWM初始化
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); // 打开定时器2 通道2
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)USART_tx_string, strlen(USART_tx_string));
HAL_UART_Receive_DMA(&huart1, &RX_Str_Data, 1); // IT改为DMA
// 参数初始化
if (EEPROM_ReadByte(111) != 11) // 第一次进入是不等于的
{
// 先执行这里的代码
// 第一次就先初始化
param.REP_X = 10; // 库存默认10
param.REP_Y = 10; // 库存默认10
param.Price_X = 1.0;
param.Price_Y = 1.0f;
EEPROM_WriteByte(111, 11); // 在111写111
}
else // 不是第一次上电
{
// 上电先读取EEPROM
param.REP_X = EEPROM_ReadByte(0);
HAL_Delay(1);
param.REP_Y = EEPROM_ReadByte(1);
HAL_Delay(1);
param.Price_X = EEPROM_ReadByte(2) / 10.0;
HAL_Delay(1);
param.Price_Y = EEPROM_ReadByte(3) / 10.0;
HAL_Delay(1);
}
}
6 测试
按键可以自行测试,这里我只展示串口部分的效果
7 做题感受
- 注意浮点数的比较 2.0f与2.01f的区别,看上面5.2 KEY 按键3的解释
- 串口收发不到数据,没使用rx_proc函数,然后重新配置了一下TX和RX的DMA就可以使用了
- PWM没有输出,使用定时器2通道2,我写成了通道1,所以没有输出...
- EEPROM存储的数据不能是小数;
- 试题中比较难的部分是如何判定设备是否是第一次启动 以及EEPROM连续读取需要一定的时间间隔,
- 总的来说,该试题还是比较简单的,就剩一些常见的解题模式框架,🚀🚀🚀!!!