本篇文章包含的内容
笔者在学习过程中参考了以下课程视频,但是这些课程中的代码并不是完全正确合理的,有些是逻辑错误,有些是可能不适用于比赛答题思路(以笔者拙见)。笔者也在实践和学习过程中融合了自己的思考,对代码逻辑进行了改进。基础较为薄弱的同学可以先学习这些课程。
一、建立工程
1.1 利用官方例程自建工程
比赛开始,不需要自建工程,直接复制一份下发资料中的../5-液晶驱动参考程序/HAL_06_LCD
工程,打开CubeMX进行配置。
在这里很容易遇到工程和CubeMX版本不匹配的问题,如下图所示,点击中间的Migrate即可。
紧接着离线加载开发库。打开CubeMX上方Help窗口中的Manage embedded software packages,点击左下角的From local…导入本地.zip
文件(无需解压),找到开发资料中的../4-库文件/stm32cube_fw_g4_v120.zip
文件,选择后等待解压完成即可。
找到工程管理Project Manager中的Mcu and Firmware Package,取消勾选Use Default Firmware Location,并在下方的固件路径中选择刚刚安装的文件包。安装好的固件包默认存储在C:\Users\DELL\STM32Cube\Repository
。笔者这里拿到的资料中的固件包版本为STM32Cube_FW_G4_V1.2.0
(实际上已经更新到1.5.2了,不知道为什么官方不更新固件包) (线上比赛资料包已下发,发现官方提供了V1.2.0和V1.4.0两个版本的固件包,但依然不是最新版本)。这时,点击GENERATE CODE就可以自动生成工程文件了。直接打开工程,编译烧录,测试拿到手的开发板是否可以正常亮屏。正常而言,此时会执行官方的例程。
这里使用V1.2.0的固件包有可能会遇到以下Bug (所以正式比赛时还是希望能使用最新的固件包):
- Keil的Pack Installer中找不到STM32G4系列器件。笔者练习时采用的方法是直接在官网下载安装对应的CMSIS器件包,目前还不清楚如果比赛过程中遇到这个问题该如何解决。
- 编译后提示报错:
Undefined symbol HAL_PWREx_DisableUCPDDeadBattery (referred from stm32g4xx_hal_msp.o).
。目前原因未知,解决方法是双击定位到目标函数位置(stm32g4xx_hal_msp.c
),将HAL_PWREx_DisableUCPDDeadBattery
改为HAL_PWREx_DisableUSBDeadBatteryPD
,之后再编译烧录,发现报错消失,可以正常烧录。
现在可以简单测试一下程序了。在main
函数中删除历程中有关LCD的代码,初始化LCD屏幕后在第一行显示一行字符:
LCD_Clear(Blue);
LCD_SetBackColor(Blue);
LCD_SetTextColor(White);
LCD_DisplayStringLine(Line0 ,(unsigned char *)"Hello, Blue Bridge.");
1.2 自行建立工程
虽然利用官方历程建立工程省去了很多配置麻烦,节省了时间,但是使用官方历程很容易因为版本不匹配产生很多未知的Bug。所以熟悉自行建立工程的步骤还是很有必要的。
1.2.1 STM32CubeMX配置
-
打开STM32CubeMX,点击File下的New Project…,选择器件STM32G431RBT6后打开工程设置界面。
-
配置RCC,打开高速外部时钟。找到System Core下的RCC配置窗口,在High Speed Clock(HSE)窗口选择
Crystal/Ceramic Resonator
。
-
配置时钟树,输入时钟设置为
24
MHz(和CT1117E-M4开发板保持一致),PLL Source Mux选择HSE
,System Clock Mux选择PLLCLK
,HCLK输入80
后按回车(选择的值可以改变,但是例程中都是80,这里选80比较稳妥),CubeMX会自动计算完成对应配置。
-
更改SYS的Debug选项。找到System Core下的SYS窗口,将Debug设置为串行线
Serial Wire
,这样可以保证在烧录程序后不会出现烧录错误。
-
在Project Manager下的Project窗口中更改工程名,工程文件夹和IDE。注意工程名和文件夹路径都不能包含中文字符。
-
在下面的Mcu and Firmware Package窗口选择合适的固件开发包。目前固件开发包已经更新到V1.5.2的版本,但是蓝桥官方提供的固件包仍然是V1.2.0的版本(也有可能是其他版本)。既然已经自建工程了,我认为使用最新的固件包可能更好,所以在这里保持默认。如果出问题也可以使用回较老的版本。
-
在Code Generator中的Generated files勾选第一项
Generate peripheral initialization as a pair of '.c/.h' files per peripheral
,其余保持默认。 -
点击GENERATE CODE生成代码,打开工程。
1.2.2 Keil5配置
打开工程后,可以先编译一次,如果有问题就先解决Bug,没有问题就可以进行下面的配置。
- 点击魔术棒(工程选项)在Debug窗口下设置调试器为
CMSIS-DAP Debugger
。点击Settings,打开Flash Download窗口,勾选Reset and Run
选项。点击OK退出设置。
- 编译工程,确认没有错误没有警告,之后就可以开始编写代码了。
1.3 BSP(板级支持包)的建立
建立BSP(Board Support Package)是结构化编程的基础。
- 在工程文件夹中创建一个文件夹
bsp
。 - 打开Keil 5工程,添加bsp组(Group)到工程目录中。
- 在魔术棒选项中,找到C/C++,将
bsp
组添加到Include Paths
中。
1.4 模板编写
笔者习惯使用模板,将重复性很高的代码只写一遍,充分利用Keil 5提高代码编辑效率。在Settings中找到Text Templates。在这里分享使用频率最高的几个模板。
|
字符表示双击载入模板后光标悬停的位置。
ifndef(main.h)
:头文件中使用
#ifndef __|_H_
#define ___H_
#include "main.h"
#endif
@brief
:函数注释模板(比赛时很可能来不及写注释,但是以防万一还是准备一下)
/**
* @brief |
* @param
* @retval
*/
二、模块代码编写
2.1 LED
2.1.1 基础操作
通过以下方法可以实现一个“指哪打哪”的LED显示函数。
led.h
#ifndef __LED_H_
#define __LED_H_
#include "main.h"
void BSP_LED_Disp(uint8_t LED_data);
#endif
led.c
#include "led.h"
uint8_t LED_DATA_SAVE;
void BSP_LED_Lock(void)
{
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}
void BSP_LED_Unlock(void)
{
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
}
void BSP_LED_Disp(uint8_t LED_data)
{
BSP_LED_Unlock();
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_All, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOC, LED_data << 8, GPIO_PIN_RESET);
BSP_LED_Lock();
LED_DATA_SAVE = LED_data;
}
2.1.2 单独操作某个LED灯而不影响其他LED
笔者在备赛过程中,还编写过如下的操作单个LED的函数。下面的函数即使在理论上可行,但是被实践证实是不可用的。因为在蓝桥杯赛题中经常涉及让LED灯以固定周期闪烁的操作,这时如果采用下面的函数单独操作某IO口,并在定时中断回调函数中操作LED灯,就可能会出现当LCD屏幕显示期间GPIOC的电平混乱,此时该函数打开了PD2,就会导致LED灯显示错乱。
typedef enum
{
LED_ON = 1,
LED_OFF = 0,
LED_TOGGLE = 2
} LED_State;
void BSP_LED_SigleLED(uint8_t LED_num, LED_State state)
{
BSP_LED_Unlock();
if (state == LED_ON)
{
HAL_GPIO_WritePin(GPIOC, 0x0001 << (LED_num + 7), GPIO_PIN_RESET);
}
else if (state == LED_OFF)
{
HAL_GPIO_WritePin(GPIOC, 0x0001 << (LED_num + 7), GPIO_PIN_SET);
}
else if (state == LED_TOGGLE)
{
HAL_GPIO_TogglePin(GPIOC, 0x0001 << (LED_num + 7));
}
BSP_LED_Lock();
}
如果想在任何地方单独操作某LED,下面的方法更合理,也更安全,通过笔者的实践证实是可行的。这里仅将LED1的操作列出,其他的LED操作也是同理的,通过与或操作改写此时的LED即可。
BSP_LED_Disp(LED_DATA_SAVE | 0x01); // 仅点亮LED1,不影响其他位
BSP_LED_Disp(LED_DATA_SAVE & 0xFE); // 仅熄灭LED1,不影响其他位
2.2 LCD
2.2.1 基础操作
在CubeMX对照开发板原理图把对应引脚配置为GPIO_Output
模式即可。复制样例工程中的lcd.c
,lcd.h
,fonts.h
到bsp文件夹下。在main.h
中添加#include "lcd.h"
,main函数中初始化后即可显示一些字符:
/* USER CODE BEGIN Init */
LCD_Init();
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
LCD_Clear(Black);
LCD_SetTextColor(White);
LCD_SetBackColor(Black);
LCD_DisplayStringLine(Line1, (unsigned char*)" DATA ");
/* USER CODE END 2 */
但是蓝桥杯官方并没有提供实时的变量显示函数,所以需要我们自行编写。在这里借助sprintf
函数将格式化字符串复制到目标字符串中:
int i = 5;
float f = 12.3;
char text[30];
sprintf(text, "%d %.2f", i, f);
LCD_DisplayStringLine(Line2, (unsigned char*)text);
2.2.2 LCD屏幕使用注意事项
需要特别注意:不要频繁刷新LCD屏幕,CT117E上的LCD屏幕刷新速度较慢,频繁刷新会造成显示内容闪烁的问题。具体的解决方式笔者将在后文给出。
关于操作LCD屏幕LED闪烁的问题,可以参考以下几点:
- 在官方给出的lcd库函数中找到
LCD_WriteReg()
、LCD_WriteRAM_Prepare()
、LCD_WriteRAM()
三个函数,在三个函数体开始暂存GPIOC->ODR
的值,并在函数结束后恢复。- 一定要确保在不操作LED时PD2处于低电平状态。
2.3 按键模块
2.3.1 短按识别
蓝桥杯赛题中程序任务颇多,不能通过传统的暴力延时来消抖,所以需要通过定时扫描按键实现消抖。这里涉及STM32定时器TIM的使用,可以参考博主的博客 STM32学习笔记(四)丨TIM定时器及其应用(定时中断、内外时钟源选择)了解TIM的定时器的工作原理,这里仅对CubeMX配置定时器的步骤作一个简要介绍。
-
根据产品手册配置相关的GPIO引脚为GPIO_Input模式,并将上下拉模式设置为上拉(Pull-Up);
-
激活定时器。由于这里仅仅需要简单的定时中断功能,所以使用一个通用定时器即可。在这里使用TIM3。配置时需要设置时钟源(基本定时器时钟源只能是内部时钟)、设置PSC和ARR,打开NVIC中断。
-
CubeMX中的配置完成,生成代码,打开工程。在bsp文件夹中创建两个文件
interrupt.c
和interrupt.h
,写好对应的模板格式,在stm32g4xx_hal_tim.h
中找到定时器中断回调函数的声明(第一个HAL_TIM_PeriodElapsedCallback
就是中断回调函数):
/** @defgroup TIM_Exported_Functions_Group9 TIM Callbacks functions
* @brief TIM Callbacks functions
* @{
*/
/* Callback in non blocking modes (Interrupt and DMA) *************************/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
void HAL_TIM_PeriodElapsedHalfCpltCallback(TIM_HandleTypeDef *htim);
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim);
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim);
void HAL_TIM_IC_CaptureHalfCpltCallback(TIM_HandleTypeDef *htim);
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim);
void HAL_TIM_PWM_PulseFinishedHalfCpltCallback(TIM_HandleTypeDef *htim);
void HAL_TIM_TriggerCallback(TIM_HandleTypeDef *htim);
void HAL_TIM_TriggerHalfCpltCallback(TIM_HandleTypeDef *htim);
void HAL_TIM_ErrorCallback(TIM_HandleTypeDef *htim);
- 编写中断回调函数。在函数
HAL_TIM_PeriodElapsedCallback
中,所有的定时器定时中断请求发生后都会调用这个函数,所以需要首先判断这个中断请求是否来自TIM3。在下面的历程中,主要编程思想是通过一个简易的状态机来实现消抖。
interrupt.h
#ifndef __INTERRUPT_H_
#define __INTERRUPT_H_
#include "main.h"
/**
* @brief 按键状态结构体
*/
struct keys
{
uint8_t judge_state; // 状态机标志,0: 检测到一个低电平;1:确实被按下;2:等待抬起
uint8_t key_state; // 按键是否被按下(信号来自GPIO输入)
uint8_t key_isPressed; // 按键被按下标志位
};
extern struct keys key[];
// void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
#endif
interrupt.c
#include "interrupt.h"
struct keys key[4] = {
0, 0, 0};
/**
* @brief TIM定时器定时中断回调函数
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3)
{
key[0].key_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
key[1].key_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);
key