【STM32G4】【CubeMX+HAL库】第十五届蓝桥杯嵌入式组编程备赛指南(2024)


  笔者在学习过程中参考了以下课程视频,但是这些课程中的代码并不是完全正确合理的,有些是逻辑错误,有些是可能不适用于比赛答题思路(以笔者拙见)。笔者也在实践和学习过程中融合了自己的思考,对代码逻辑进行了改进。基础较为薄弱的同学可以先学习这些课程。


一、建立工程

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 (所以正式比赛时还是希望能使用最新的固件包)

  1. Keil的Pack Installer中找不到STM32G4系列器件。笔者练习时采用的方法是直接在官网下载安装对应的CMSIS器件包,目前还不清楚如果比赛过程中遇到这个问题该如何解决。
  2. 编译后提示报错: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配置

  1. 打开STM32CubeMX,点击File下的New Project…,选择器件STM32G431RBT6后打开工程设置界面。

  2. 配置RCC,打开高速外部时钟。找到System Core下的RCC配置窗口,在High Speed Clock(HSE)窗口选择Crystal/Ceramic Resonator
    在这里插入图片描述

  3. 配置时钟树,输入时钟设置为24MHz(和CT1117E-M4开发板保持一致),PLL Source Mux选择HSE,System Clock Mux选择PLLCLK,HCLK输入80后按回车(选择的值可以改变,但是例程中都是80,这里选80比较稳妥),CubeMX会自动计算完成对应配置。
    请添加图片描述

  4. 更改SYS的Debug选项。找到System Core下的SYS窗口,将Debug设置为串行线Serial Wire,这样可以保证在烧录程序后不会出现烧录错误。
    在这里插入图片描述

  5. 在Project Manager下的Project窗口中更改工程名,工程文件夹和IDE。注意工程名和文件夹路径都不能包含中文字符。
    在这里插入图片描述

  6. 在下面的Mcu and Firmware Package窗口选择合适的固件开发包。目前固件开发包已经更新到V1.5.2的版本,但是蓝桥官方提供的固件包仍然是V1.2.0的版本(也有可能是其他版本)。既然已经自建工程了,我认为使用最新的固件包可能更好,所以在这里保持默认。如果出问题也可以使用回较老的版本。
    在这里插入图片描述

  7. 在Code Generator中的Generated files勾选第一项Generate peripheral initialization as a pair of '.c/.h' files per peripheral,其余保持默认。

  8. 点击GENERATE CODE生成代码,打开工程。

1.2.2 Keil5配置

  打开工程后,可以先编译一次,如果有问题就先解决Bug,没有问题就可以进行下面的配置。

  1. 点击魔术棒(工程选项)在Debug窗口下设置调试器为CMSIS-DAP Debugger。点击Settings,打开Flash Download窗口,勾选Reset and Run选项。点击OK退出设置。

请添加图片描述
在这里插入图片描述

  1. 编译工程,确认没有错误没有警告,之后就可以开始编写代码了。

1.3 BSP(板级支持包)的建立

  建立BSP(Board Support Package)是结构化编程的基础。

  1. 在工程文件夹中创建一个文件夹bsp
  2. 打开Keil 5工程,添加bsp组(Group)到工程目录中。
  3. 在魔术棒选项中,找到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.clcd.hfonts.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闪烁的问题,可以参考以下几点:

  1. 在官方给出的lcd库函数中找到LCD_WriteReg()LCD_WriteRAM_Prepare()LCD_WriteRAM()三个函数,在三个函数体开始暂存GPIOC->ODR的值,并在函数结束后恢复。
  2. 一定要确保在不操作LED时PD2处于低电平状态。

2.3 按键模块

2.3.1 短按识别

  蓝桥杯赛题中程序任务颇多,不能通过传统的暴力延时来消抖,所以需要通过定时扫描按键实现消抖。这里涉及STM32定时器TIM的使用,可以参考博主的博客 STM32学习笔记(四)丨TIM定时器及其应用(定时中断、内外时钟源选择)了解TIM的定时器的工作原理,这里仅对CubeMX配置定时器的步骤作一个简要介绍。

  1. 根据产品手册配置相关的GPIO引脚为GPIO_Input模式,并将上下拉模式设置为上拉(Pull-Up);
    在这里插入图片描述
    在这里插入图片描述

  2. 激活定时器。由于这里仅仅需要简单的定时中断功能,所以使用一个通用定时器即可。在这里使用TIM3。配置时需要设置时钟源(基本定时器时钟源只能是内部时钟)、设置PSC和ARR,打开NVIC中断
    请添加图片描述
    在这里插入图片描述

  3. CubeMX中的配置完成,生成代码,打开工程。在bsp文件夹中创建两个文件interrupt.cinterrupt.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);
  1. 编写中断回调函数。在函数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
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Include everything

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值