蓝桥杯嵌入式新板模板创建&简单经验分享
补充在最前:
以下原文是22年还未毕业时写的,仅在把板子二手卖给别人的时候给别人分享了这份笔记。
那时经验不多,现在也由于工作使用的芯片不同已很久没有使用CubeMX了,因此文章可能有很多错漏之处,欢迎在评论区指出。
备注在前: uint8_t 即 unsigned char(总忘
typedef unsigned char uint8_t;
本模板不保证完全正确
目录
- 蓝桥杯嵌入式新板模板创建&简单经验分享
- 0. RCC 时钟树
- 1. GPIO
- 1.1 LED
- 1.2 KEY
- 2. LCD显示屏
- 3. UART串口
- 4. IIC
- 4.1 EEPROM 24c02
- 4.2 可编程电阻MCP4017 (扩展板)
- 5. ADC
- 6. TIM
- 6.1 基本定时器 TIM6/7
- 6.2 通用定时器 TIM2/3/4/15/16/17
- 6.2.1 测量1路PWM(1路PWM输入)
- 6.2.2 测量2路PWM频率和占空比
- 6.2.3 方波输出
- 6.2.4 输出2路PWM
- 6.3 高级定时器 TIM1/8
- 7. RTC 时钟
- 经验分享
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();elseI2CSendNotAck();}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-读写可编程电阻 MCP4017sprintf((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.cint 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库函数即可,获取时间和日期的两个函数【!必须同时使用】,否则会出现bugHAL_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工程的这一项的。