蓝桥杯嵌入式模板创建(STM32 CubeMx简单使用教程)
蓝桥杯嵌入式新板模板创建&简单经验分享
补充在最前:
以下原文是22年还未毕业时写的,仅在把板子二手卖给别人的时候和某学弟问我的时候给别人分享了这份笔记(学弟比我牛多了之后拿了国一)。
那时经验不多,现在也由于工作使用的芯片不同已很久没有使用CubeMX了,因此文章可能有很多错漏之处,欢迎在评论区指出。
当时整理的资料,包含22年以前的赛题:
链接:https://pan.baidu.com/s/1I8QjNOwCHjpuWItRn0aYvQ?pwd=1234 提取码:1234 复制这段内容后打开百度网盘手机App,操作更方便哦
备注在前: uint8_t 即 unsigned char(总忘
typedef unsigned char uint8_t;
本模板不保证完全正确
目录
0. RCC 时钟树
时钟树如下图红框处设置,最后生成的是80MHZ就对了
输出配置1
输出配置2
1. GPIO
1.1 LED
cubemx配置
配置引脚 PC8~15 输出output - 8LED
输出
需要手打的部分
//gpio.h
void LED_Disp(unsigned char ucLed);
// gpio.c
//函数名: LED_Disp
//函数功能: LD8-LED1对应ucLed的8个位
//传入参数: unsigned char ucLed
//返回值: 无
void LED_Disp(unsigned char ucLed)
{
//将所有的灯熄灭
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 , GPIO_PIN_SET);
// 说明:要使用GPIOC控制灯,需要使PD2引脚产生一个下降沿
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
//根据ucLed的数值点亮相应的灯
HAL_GPIO_WritePin(GPIOC, ucLed << 8, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}
1.2 KEY
cubemx配置
配置引脚 PA0、PB0~2 输入input - 4KEY
需要手打的部分
// gpio.h
unsigned char Key_Scan(void);
// gpio.c
unsigned char Key_Scan(void)
{
unsigned char unKey_Val = 0;
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET){
unKey_Val = 1;
}
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET){
unKey_Val = 2;
}
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET){
unKey_Val = 3;
}
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){
unKey_Val = 4; //PA0对应按键B4
}
return unKey_Val;
}
// main.c
// 减速变量
__IO uint32_t uwTick_KEY = 0;
// 按键扫描专用变量
unsigned char Key_Val, Key_Up, Key_Down, Key_Old;
void Key_Proc(void)
{
// 减速
if((uwTick-uwTick_KEY)<100) return ;
uwTick_KEY = uwTick;
Key_Val = Key_Scan();
Key_Down = Key_Val & (Key_Old ^ Key_Val);
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);
Key_Old = Key_Val;
if (Key_Down == 1){
LED_Disp(0x01);
}
if (Key_Down == 2){
LED_Disp(0x02);
}
if (Key_Down == 3){
LED_Disp(0x04);
}
if (Key_Down == 4){
LED_Disp(0x08);
}
}
关于按键的三行代码:https://blog.csdn.net/qq_43012492/article/details/107676658
2. LCD显示屏
官方会提供例程,把lcd.c
、lcd.h
和fonts.h
加进自己的工程就好了
LCD液晶屏幕一行可显示20个英文字符,共10行
需要自己写的部分
// main.c 头文件别多加fonts
#include "lcd.h"
// 减速变量
__IO uint32_t uwTick_LCD = 0;
int main(){
LCD_Init();
LCD_Clear(White);
LCD_SetTextColor(Black);
LCD_SetBackColor(White);
while(1){
LCD_Proc();
}
}
void LCD_Proc(void){
// 减速
if((uwTick - uwTick_LCD) < 100) return ;
uwTick_LCD = uwTick;
sprintf((char *)str, "Hello, world!");
LCD_DisplayStringLine(Line0, str);
}
3. UART串口
cubemx配置
相关变量定义
//main.c
__IO uint32_t uwTick_UART1;
int counter = 0;
char str[40];
u8 rx_buffer;
// uart.c
UART_HandleTypeDef huart1;
// uart.h
#include "main.h"
extern UART_HandleTypeDef huart1;
void UART1_Init(void);
需要自己手写的功能函数
开发板通过串口发送数据,主机接收
// 串口发数据(开发板发送)
void UART1_Proc()
{
//减速
if(uwTick-uwTick_UART1 < 500) return;
uwTick_UART1 = uwTick;
sprintf(str, "%04d : hello\n", counter);
HAL_UART_Transmit(&huart1, (unsigned char *)str, strlen(str), 50);
if(++counter == 10000) counter = 0;
}
主机发送数据,开发板串口接收数据
// stm32g4xx_it.c
extern UART_HandleTypeDef huart1;
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
}
// mian.c
int main(){
// ...初始化等等
// 开中断
HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);
while(1){
// ...
}
}
// 串口接收中断回调函数 【重要 需要记住名称】
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 功能 ...
HAL_UART_Receive_IT(&huart1, &rx_buffer, 1); // 结尾需要重新开中断
}
如果串口中断函数中用到了HAL_Delay
进行延时,要注意系统时钟优先级需高于串口中断优先级
4. IIC
比赛时会提供IIC的HAL库代码,直接加到工程里就行,不需要配置cubemx
需要修改i2c-hal.c
文件中的I2CWaitACK
函数末尾段代码
原来的
SDA_Output_Mode();
SCL_Output(0);
delay1(DELAY_TIME);
return SUCCESS;
修改后
SCL_Output(0);
delay1(DELAY_TIME);
SDA_Output_Mode();
return SUCCESS;
需要自己手动写的功能函数(EEPROM和可编程电阻读写)
4.1 EEPROM 24c02
//24c02的相关代码
//写EEPROM
void iic_24c02_write(unsigned char *pucBuf, unsigned char ucAddr, unsigned char ucNum)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(ucAddr);
I2CWaitAck();
while(ucNum --){
I2CSendByte(*pucBuf++);
I2CWaitAck();
}
I2CStop();
delay1(500);
}
读24c02
//从EEPROM读
void iic_24c02_read(unsigned char *pucBuf, unsigned char ucAddr, unsigned char ucNum)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(ucAddr);
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
while(ucNum --){
*pucBuf++ = I2CReceiveByte();
if(ucNum)
I2CSendAck();
else
I2CSendNotAck();
}
I2CStop();
}
写和读函数连续使用中间需要延时。
// main.c
#include "i2c.h"
//EEPROM的相关变量
unsigned char EEPROM_String1[5] = {0x11, 0x22, 0x33, 0x44, 0x55};
unsigned char EEPROM_String2[5] = {0};
int main(){
// 建议初始化函数写在main函数里靠后的位置,实测会有影响
I2CInit();
// EEPROM测试
iic_24c02_write(EEPROM_String1, 0, 5);
HAL_Delay(1);
iic_24c02_read(EEPROM_String2, 0, 5);
while(1){
LCD_Proc();
}
}
void LCD_Proc(){
// IIC-读写EEPROM测试
sprintf((char*)str, "EE:%02X,%02X,%02X,%02X,%02X",EEPROM_String2[0],EEPROM_String2[1],EEPROM_String2[2],EEPROM_String2[3],EEPROM_String2[4]);
LCD_DisplayStringLine(Line2, str);
}
注意:实际比赛中会考到EEPROM是否为第一次上电的判断问题,请仔细阅读题目相关要求。
4.2 可编程电阻MCP4017 (扩展板)
省赛不用扩展板,可以不看
写MCP4017
//可编程电阻MCP4017的相关代码
//写阻值
void write_resistor(uint8_t value)
{
I2CStart();
I2CSendByte(0x5E);
I2CWaitAck();
I2CSendByte(value);
I2CWaitAck();
I2CStop();
}
读MCP4017
//读阻值
uint8_t read_resistor()
{
uint8_t value;
I2CStart();
I2CSendByte(0x5F);
I2CWaitAck();
value = I2CReceiveByte();
I2CSendNotAck();
I2CStop();
return value;
}
测试代码
// main.c
#include "i2c.h"
//MCP4017相关变量
uint8_t res;
int main(){
I2CInit();
//MCP4017测试
write_resistor(0x10);
res = read_resistor();
while(1){
LCD_Proc();
}
}
void LCD_Proc(){
// IIC-读写可编程电阻 MCP4017
sprintf((char *)str, "RES_K:%5.2fK", 0.7874*res);
LCD_DisplayStringLine(Line3, (uint8_t *)str);
sprintf((char *)str, "VOLTAGE:%6.3fV", 3.3*((0.7874*res)/(0.7874*res+10)));
LCD_DisplayStringLine(Line4, (uint8_t *)str);
}
5. ADC
cubemx配置如下
ADC1 通道11 引脚PB12
ADC2 通道15 引脚PB15
ADC1和ADC2配置一样(下图要手动改的只有异步时钟 Asy)
延长采样时间
由于ADC时钟设置为异步,因此时钟应该设置为来自PLLP 锁相环
头文件
// adc.h
#include "main.h"
extern ADC_HandleTypeDef hadc1;
void ADC1_Init(void);
void ADC2_Init(void);
uint16_t getADC1(void); // 引脚PB12 R38
uint16_t getADC2(void); // 引脚PB15 R37
需要自己写的相关函数
// adc.c
// 如果使用cubemx工程则直接把两个函数写在main.c里也可
ADC_HandleTypeDef hadc1;
ADC_HandleTypeDef hadc2;
// 获取ADC1的值
uint16_t getADC1(void)
{
uint16_t adc = 0;
HAL_ADC_Start(&hadc1);
adc = HAL_ADC_GetValue(&hadc1);
return adc;
}
// 获取ADC2的值
uint16_t getADC2(void)
{
uint16_t adc = 0;
HAL_ADC_Start(&hadc2);
adc = HAL_ADC_GetValue(&hadc2);
return adc;
}
使用获取到的ADC数值
// main.c 中的 LCD_Proc
void LCD_Proc(){
// ADC测试
sprintf((char *)str, "ADC1-R38:%6.2fV", 3.3*getADC1()/4096.0);
LCD_DisplayStringLine(Line5, (uint8_t *)str);
sprintf((char *)str, "ADC2-R37:%6.2fV", 3.3*getADC2()/4096.0);
LCD_DisplayStringLine(Line6, (uint8_t *)str);
}
// 简单说明
// ADC为12位 2^12=4096 开发板电压为3.3v
// 将3.3V/4096 * 获取到的ADC数值 即为实际电压值
ADC1 对应旋钮R38, ADC2对应旋钮R37
6. TIM
6.1 基本定时器 TIM6/7
说明:这一部分其实没咋用过,考的都是PWM,像题目要求控制LED间隔0.1s闪烁的用系统时钟uwTick参数就足以实现。
CubeMX配置
选择【Activated】使能TIM6, 下方PSC设置分频,ARR设置计数值, 计数模式为up(向上计数)。
TIM6时钟来源为系统时钟(APB2),80MHz分频后为10kHz,计数满1000就触发中断,相当于频率为10Hz,每0.1秒触发一次中断。
不需要配置引脚,但需要配置中断,且中断优先级要改为2
需要手动写的部分
// main.c
#include "tim.h"
int main(){
// ...
TIM6_Init();
HAL_TIM_Base_Start_IT(&htim6); // 开定时器中断,定时器计数到达ARR时中断
}
// 基本定时器TIM6更新中断回调函数【需要记住名字】
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6){ // 如果用到了多个定时器的中断则需要有该if判断
i++;
HAL_TIM_Base_Start_IT(&htim6); // 中断回调函数结尾一定要再开中断
}
}
// 功能测试
void LCD_Proc(){
// ...
sprintf((char *)Lcd_String, "TIM6_COUNT: %03d", (unsigned int)i);
LCD_DisplayStringLine(Line0, (uint8_t *)Lcd_String);
}
开中断函数 HAL_TIM_Base_Start_IT
,比赛时记不清可以在stm32g4xx_hal_tim.c
里ctrl+f搜索start
6.2 通用定时器 TIM2/3/4/15/16/17
6.2.1 测量1路PWM(1路PWM输入)
需要用到定时器的输入捕获模式、从模式
cubemx配置
使用的是TIM3的通道1, 对应引脚PB4
需要使能中断,修改中断优先级
(GPIO要配置为输入复用模式,默认就是不用改)
KEIL
需要自己写的代码
// main.c
#include "tim.h"
// TIM3的PWM输出相关变量
uint16_t PWM1_CNT;
int main(){
// ......
//初始化TIM3 打开中断 并设置定时器输入捕获
TIM3_Init();
HAL_TIM_Base_Start(&htim3);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
}
//TIM3输入捕获中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3){
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
PWM1_CNT = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1)+1;
}
}
}
// 测试代码
void LCD_Proc(){
// PWM输入测试
sprintf((char *)str, "PWM1_COUNT:%06d", (unsigned int)1000000/PWM1_CNT);
LCD_DisplayStringLine(Line7, (uint8_t *)str);
}
6.2.2 测量2路PWM频率和占空比
cubemx配置
这里和测量1路PWM的一样,TIM3的部分不再赘述
配置第二路 TIM2, 引脚为PA15
参数配置
同样需要开中断,修改中断优先级
需要手打的部分
// main.c
#include "tim.h"
//PWM1相关变量
uint16_t PWM1_CNT;
uint16_t PWM2_CNT;
uint16_t PWM1_DUTY;
uint16_t PWM2_DUTY;
float PWM1_DR;
float PWM2_DR;
int main(){
// .....
//初始化TIM3 打开中断 并设置定时器输入捕获
TIM3_Init();
HAL_TIM_Base_Start(&htim3);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
//初始化TIM2 二路PWM通道输入
TIM2_Init();
HAL_TIM_Base_Start(&htim2);
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2);
}
// 定时器输入捕获中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2)
{
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
PWM2_CNT = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1)+1;
PWM2_DR = (float)PWM2_DUTY/PWM2_CNT;
}
else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
{
PWM2_DUTY = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2)+1;
}
}
if(htim->Instance == TIM3)
{
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
PWM1_CNT = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1)+1;
PWM1_DR = (float)PWM1_DUTY/PWM1_CNT;
}
else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
{
PWM1_DUTY = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2)+1;
}
}
}
void LCD_Proc(){
// PWM输入测试
sprintf((char *)str, "PWM1:%6d,%4.2f%%", (unsigned int)1000000/PWM1_CNT, PWM1_DR*100);
LCD_DisplayStringLine(Line7, (uint8_t *)str);
sprintf((char *)str, "PWM2:%6d,%4.2f%%", (unsigned int)1000000/PWM2_CNT, PWM2_DR*100);
LCD_DisplayStringLine(Line8, (uint8_t *)str);
}
6.2.3 方波输出
可以使用一个定时器的两个通道输出两个不同频率的方波
cubemx配置
此处使用TIM4的通道1和通道2,引脚对应PA11、PA12
参数配置
需要使能中断,修改中断优先级
需要手打的部分
// main.c
#include "tim.h"
int main(){
// ...
//初始化TIM4 输出方波
TIM4_Init();
HAL_TIM_Base_Start(&htim4);
HAL_TIM_OC_Start_IT(&htim4, TIM_CHANNEL_1);
HAL_TIM_OC_Start_IT(&htim4, TIM_CHANNEL_2);
}
// 定时器输出比较中断回调函数【方波】
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM4)
{
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1,(__HAL_TIM_GetCounter(htim)+100)); // 5kHZ
}
else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
{
__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_2,(__HAL_TIM_GetCounter(htim)+500)); // 1kHZ
}
}
}
6.2.4 输出2路PWM
cubemx配置
第一路PWM输出, 定时器TIM17通道1, 引脚PA7
参数配置
无需使能中断
第二路PWM输出配置, TIM16通道1, 引脚PA6。配置和第一路一致。
参数配置
同样无需使能中断
需要手打的部分
// main.c
int main(){
// TIM16 PWM输出
HAL_TIM_Base_Start(&htim16);
HAL_TIM_OC_Start_IT(&htim16, TIM_CHANNEL_1); // PA6
// TIM17 PWM输出
HAL_TIM_Base_Start(&htim17);
HAL_TIM_OC_Start_IT(&htim17, TIM_CHANNEL_1); // PA7
}
其他
// 修改占空比/频率通常用到以下两个函数
__HAL_TIM_SET_AUTORELOAD(&htim16, arr); // 控制频率 arr即自动重装载数值
__HAL_TIM_SET_COMPARE(&htim16, TIM_CHANNEL_1, pulse); //控制占空比 pulse/arr即为占空比
6.3 高级定时器 TIM1/8
用通用的就够了,一般用不上高级的
7. RTC 时钟
cubemx配置
使能RTC时钟和日历
配置时钟树 时钟来自外部晶振(可以看到分频后为750kHZ, 之后还要分频成1HZ)
配置分频
750k/125/6000 = 1HZ
时分秒的初始值可以在此设置
日期也可初始化(年的数值范围是0-99)
RTC模块的使用(需手打代码)
// main.c
//RTC专用变量
RTC_TimeTypeDef time;
RTC_DateTypeDef date;
uint8_t second;
void LCD_Proc(){
// ...
// RTC 测试
second = time.Seconds;
// 直接调用HAL库函数即可,获取时间和日期的两个函数【!必须同时使用】,否则会出现bug
HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BCD);
HAL_RTC_GetDate(&hrtc, &date, RTC_FORMAT_BCD);
if(second != time.Seconds){
sprintf((char*)str, "%02x:%02x:%02x", time.Hours, time.Minutes, time.Seconds);
LCD_DisplayStringLine(Line0, (uint8_t *)str);
sprintf((char*)str, "20%02x-%02x-%02x", date.Year, date.Month, date.Date);
LCD_DisplayStringLine(Line1, (uint8_t *)str);
}
}
经验分享
关于是否要买教程这件事
个人觉得,如果之前有stm32开发基础且备赛时间比较多的人可以不买教程完全自学,主要学cubemx的使用,当然买教程也是可以的,买教程相当于是花钱省了自己找资料的时间。
我备赛是买了蚂蚁工厂的教程,只跟了基础部分,模板创建部分快速的略过了。因为今年国赛比较特殊,没考扩展板,所以后面扩展板创建和国赛模板创建的部分我都没看。如果跟蚂蚁的教程走的话,不建议完全参考他使用移植的方法,参考思路就好,直接用cubemx生成的工程就可以了,代码写在begin和end之间就不会被覆盖。
如果要用移植的方法,这里有一些函数比如MspInit
之类的,实际也要一起移植(移植真的又慢又麻烦,没必要)
比赛相关
省赛会考的内容就是以上模板创建提到的那些,屏幕+LED+按键这仨是必考,这一块的模板创建必须烂熟于心(其实所有内容都要记下来)。其他要注意的就是PWM输出这一部分,这是省赛最常考的,要熟悉PWM输出的频率和占空比调节,练习的时候没有示波器的人可以网购个逻辑分析仪,不用太贵的能用就行,我用的NanoDLA(30元左右),当然能用示波器的还是用示波器,比逻辑分析仪舒服多了。其他的比如I2C和串口就是死记,用法都大差不差,建议有时间的把往年省赛题都做一遍,多练练模板。
今年国赛没用扩展板,理论上考的内容也是省赛的那些,综合性和难度要强一节,基本上述模板的所有模块都考到了,因为我也没拿好成绩,所以就不多说了。
另外客观题(15分 程序85)我基本都是蒙的所以也不好给出建议。客观考的范围很宽泛,数电、模电、开发板相关的等等,主要靠自己积累,开发板相关的赛时可以看官方发的资料进行查找,争国一的还是最好多做准备。
建议比赛前一天晚上保证良好的睡眠和比较好的精神状态,这一点还挺重要的,好的状态对编程思路有很大的帮助。我省赛和国赛时完全是两个状态,我因为国赛撞了期末就没准备蓝桥杯,当时又焦虑又疲惫,比完还以为白给了,最后混到了国三。如果我以我国赛的状态去比省赛肯定就白给了。
其他要注意的就是如果跟我一样是线上比赛的,比赛要保证网络畅通,前置摄像头对准自己,这样自己偶尔回头能看到自己的手机屏幕,就能知道自己有没有掉线了,偶尔卡顿监考老师不会说什么。比赛要用Chrome浏览器的ACMcoder插件,安装好赛前几天需要上线测试,官网下载的准考证上会有相关注意事项,跟着官方的要求做就行。
调试的时候发现自己的代码写好了屏幕没反应不要着急,先按复位键看看,经常有人忘记设置Keil工程的“Reset and Run”配置项,以为自己代码出问题。实际比赛中是一定要配置好Keil工程的这一项的。