1. 蓝桥杯嵌入式开发入门指南
作为一名大二电气工程专业的学生,我在寒假期间系统学习了蓝桥杯嵌入式开发的相关知识。STM32G431RBT6作为蓝桥杯嵌入式竞赛的指定开发板,掌握其核心模块的开发方法至关重要。本文将详细介绍从环境搭建到各模块实现的完整流程,特别适合准备参加蓝桥杯嵌入式竞赛的同学快速上手。
嵌入式开发最令人头疼的就是各种外设的配置和调试。经过大量实践,我发现使用STM32CubeMX工具可以极大简化开发流程。这个图形化配置工具不仅能自动生成初始化代码,还能直观地展示各个外设的配置状态,避免了手动编写底层寄存器的繁琐工作。
2. 开发环境搭建与基础配置
2.1 硬件准备
蓝桥杯嵌入式开发板基于STM32G431RBT6微控制器,这是一款Cortex-M4内核的芯片,主频可达170MHz。开发板上集成了LED、按键、LCD显示屏、ADC输入、PWM输出等常用外设,完全满足竞赛需求。
在开始开发前,需要准备以下硬件:
- STM32G431RBT6开发板
- ST-Link调试器
- USB转串口模块(用于串口通信调试)
- 杜邦线若干
2.2 软件安装
开发环境搭建需要以下软件:
- Keil MDK-ARM:用于编写和调试代码
- STM32CubeMX:用于外设配置和代码生成
- STM32CubeG4 HAL库:提供硬件抽象层函数
- 串口调试助手:用于查看串口输出
安装完成后,建议先创建一个简单的LED闪烁项目测试开发环境是否正常工作。这个简单的测试能验证编译器、下载器和硬件的基本功能。
提示:在Keil中新建项目时,务必选择正确的芯片型号STM32G431RBTx,否则可能导致代码无法正常运行。
3. 串口通信实现详解
3.1 CubeMX串口配置
串口通信是嵌入式系统中最常用的调试和通信方式。在蓝桥杯嵌入式开发中,USART1通常用于与上位机通信。以下是详细的配置步骤:
- 打开CubeMX,选择STM32G431RBT6芯片
- 在Pinout视图中找到USART1,将Mode设置为"Asynchronous"
- 配置参数:波特率9600,8位数据位,无校验,1位停止位
- 确认PA9(TX)和PA10(RX)引脚已自动配置为USART功能
- 在NVIC Settings中使能USART1全局中断
- 生成代码并打开Keil工程
配置时需要注意:
- 波特率必须与通信对方一致,常用9600或115200
- 如果使用中断方式接收数据,必须开启NVIC中断
- 引脚配置错误会导致通信失败,务必核对原理图
3.2 串口发送实现
串口发送数据相对简单,HAL库提供了现成的发送函数。以下是实现周期性发送"Hello"字符串的示例代码:
c复制#include "string.h"
#include "main.h"
UART_HandleTypeDef huart1;
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
char text_uart[20] = "Hello\r";
while (1)
{
HAL_UART_Transmit(&huart1, (uint8_t*)text_uart, strlen(text_uart), 500);
HAL_Delay(500);
}
}
代码解析:
HAL_UART_Transmit是阻塞式发送函数,会等待发送完成或超时- 第四个参数500表示超时时间(ms),防止程序卡死
- 字符串结尾的"\r"是回车符,使串口助手自动换行显示
3.3 串口中断接收
中断方式接收数据效率更高,适合处理不定长数据。以下是实现串口回显功能的代码:
c复制uint8_t rxdat;
char receive_dat[30];
uint8_t pointer = 0;
int main(void)
{
// 初始化代码同上
HAL_UART_Receive_IT(&huart1, &rxdat, 1); // 启动中断接收
while (1) { /* 主循环不做事 */ }
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
receive_dat[pointer++] = rxdat;
if(rxdat == '\n') // 检测到换行符表示一帧结束
{
receive_dat[pointer-2] = '\0'; // 替换换行符为字符串结束符
HAL_UART_Transmit(&huart1, (uint8_t*)receive_dat, strlen(receive_dat), 500);
pointer = 0; // 重置指针
}
HAL_UART_Receive_IT(&huart1, &rxdat, 1); // 重新启动中断接收
}
}
关键点说明:
- 每次只接收1个字节,通过指针累加实现不定长接收
- 检测到换行符'\n'认为一帧结束,然后回发数据
- 回调函数中必须重新启用中断接收,否则只会接收一次
- 字符串处理要注意缓冲区溢出问题
4. 定时器应用开发
4.1 定时器基础概念
STM32的定时器功能强大,可用于定时、PWM生成、输入捕获等。理解以下两个核心参数至关重要:
-
预分频值(PSC):对定时器时钟进行分频
- 公式:实际分频系数 = PSC + 1
- 例如:PSC=39999,则分频系数为40000
-
自动重装值(ARR):定时器计数上限
- 计数器从0计数到ARR,然后产生溢出事件
- 实际计数值 = ARR + 1
定时器中断频率计算公式:
中断频率 = 定时器时钟 / ((PSC+1)*(ARR+1))
例如:
- 定时器时钟80MHz
- PSC=39999,ARR=1999
- 中断频率 = 80,000,000 / (40000*2000) = 1Hz
4.2 定时中断实现
使用CubeMX配置定时器中断的步骤:
- 选择TIM1(或其他定时器)
- Clock Source选择"Internal Clock"
- 配置Prescaler(PSC)和Counter Period(ARR)
- 开启定时器中断(NVIC Settings)
- 生成代码
实现1秒定时打印的代码示例:
c复制void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint8_t count = 0;
if(htim == &htim1)
{
count++;
if(count >= 10)
{
count = 0;
uint8_t send = 'A';
HAL_UART_Transmit(&huart1, &send, 1, 500);
}
}
}
注意事项:
- 使用前需调用
HAL_TIM_Base_Start_IT(&htim1)启动定时器 - 回调函数中避免耗时操作,防止影响定时精度
- 多个定时器共用同一回调函数,需通过句柄区分
5. PWM应用开发
5.1 PWM输出配置
PWM(脉宽调制)广泛用于控制LED亮度、电机速度等。开发板上PC8引脚连接LED,可作为PWM输出测试。配置步骤:
- 选择TIM3(或其他支持PWM的定时器)
- Channel3选择"PWM Generation CH3"
- 配置PSC和ARR(决定PWM频率)
- 设置Pulse值(决定占空比)
- 生成代码
关键参数关系:
- PWM频率 = 定时器时钟 / ((PSC+1)*(ARR+1))
- 占空比 = Pulse / (ARR+1)
实现LED呼吸灯效果的代码:
c复制__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_3, 500); // 初始占空比
while (1)
{
// 渐亮
for(int i=0; i<1000; i+=10)
{
__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_3, i);
HAL_Delay(10);
}
// 渐暗
for(int i=1000; i>0; i-=10)
{
__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_3, i);
HAL_Delay(10);
}
}
特别注意:
- 开发板LED是低电平点亮,因此占空比越大LED越暗
- 修改Pulse值后,需调用
__HAL_TIM_SetCompare更新 - PWM频率不宜过低,否则会观察到LED闪烁
5.2 PWM输入捕获
PWM输入捕获可用于测量外部信号的频率和占空比。开发板上的信号发生器输出连接到PB4引脚。配置步骤:
- 选择TIM2(或其他支持输入捕获的定时器)
- Channel1配置为"Input Capture direct mode"
- Channel2配置为"Input Capture indirect mode"
- 设置触发边沿(Channel1上升沿,Channel2下降沿)
- 开启捕获中断
- 生成代码
测量PWM频率和占空比的代码:
c复制uint32_t PWM1_T_Count = 0; // 周期计数值
uint32_t PWM1_D_Count = 0; // 高电平计数值
float PWM1_Duty = 0; // 占空比
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2)
{
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
PWM1_T_Count = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1) + 1;
PWM1_Duty = (float)PWM1_D_Count / PWM1_T_Count;
}
else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
{
PWM1_D_Count = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2) + 1;
}
}
}
测量原理:
- 上升沿触发时,读取计数器值作为周期
- 下降沿触发时,读取计数器值作为高电平时间
- 占空比 = 高电平时间 / 周期
6. ADC电压采集
6.1 单通道ADC配置
ADC(模数转换器)用于将模拟电压转换为数字值。STM32G431的ADC为12位,量程0-3.3V。配置步骤:
- 选择ADC1(或ADC2)
- 选择通道(如Channel5对应PA0引脚)
- 配置为"Single-ended"模式
- 设置采样时间和分辨率
- 生成代码
实现电压测量的代码:
c复制uint32_t get_adc(ADC_HandleTypeDef *hadc)
{
HAL_ADC_Start(hadc);
uint32_t adc = HAL_ADC_GetValue(hadc);
HAL_ADC_Stop(hadc);
return adc;
}
// 将ADC值转换为电压值(mV)
uint16_t adc_to_voltage(uint32_t adc_value)
{
return (3300 * adc_value) / 4095;
}
注意事项:
- 12位ADC的量程是0-4095(2^12-1)
- 转换公式:电压值 = (3300 * ADC值) / 4095(单位mV)
- 采样时间影响转换精度,噪声大时可适当增加
7. 按键处理
7.1 按键扫描基础
开发板上有4个独立按键,分别连接PB0、PB1、PB2和PA0。配置步骤:
- 将按键引脚配置为输入模式
- 根据硬件设计选择上拉或下拉
- 生成代码
简单按键扫描实现:
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;
return unKey_Val;
}
7.2 高级按键检测
实际应用中需要检测按键按下、释放、长按等事件。以下是改进的按键处理代码:
c复制void Key_Proc(void)
{
static uint32_t uwTick_Key_Set_Point = 0;
static uint8_t ucKey_Old = 0;
static uint32_t uwTick_Key_LongStart = 0;
uint8_t ucKey_Val = 0;
uint8_t unKey_Down = 0;
uint8_t ucKey_Up = 0;
uint8_t ucKey_LongPress = 0;
if((HAL_GetTick() - uwTick_Key_Set_Point) < 100) return;
uwTick_Key_Set_Point = HAL_GetTick();
ucKey_Val = Key_Scan();
unKey_Down = ucKey_Val & (ucKey_Old ^ ucKey_Val);
ucKey_Up = ~ucKey_Val & (ucKey_Old ^ ucKey_Val);
ucKey_Old = ucKey_Val;
if(unKey_Down)
{
// 按键按下事件处理
uwTick_Key_LongStart = HAL_GetTick();
}
if(ucKey_Val && (HAL_GetTick() - uwTick_Key_LongStart) >= 2000)
{
ucKey_LongPress = 1;
// 长按事件处理
}
if(ucKey_Up)
{
uwTick_Key_LongStart = HAL_GetTick();
ucKey_LongPress = 0;
// 按键释放事件处理
}
}
这段代码实现了:
- 按键消抖(100ms采样间隔)
- 按下和释放事件检测
- 长按检测(2秒以上)
- 状态机管理,避免重复触发
8. RTC实时时钟
8.1 RTC基础配置
RTC(实时时钟)可用于记录时间和日期。配置步骤:
- 激活RTC时钟源(通常使用LSE 32.768kHz)
- 设置Asynchronous Prescaler为127
- 设置Synchronous Prescaler为255
- 启用时钟和日历功能
- 生成代码
RTC初始化和时间读取:
c复制RTC_TimeTypeDef H_M_S_Time;
RTC_DateTypeDef Y_M_D_Date;
// 初始化后读取时间
HAL_RTC_GetTime(&hrtc, &H_M_S_Time, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &Y_M_D_Date, RTC_FORMAT_BIN);
注意事项:
- 即使不需要日期也必须调用
HAL_RTC_GetDate,否则时间不会更新 - 使用BIN格式读取,避免BCD转换问题
- RTC依赖后备电池,断电后需重新设置时间
9. I2C设备驱动
9.1 AT24C02 EEPROM驱动
AT24C02是2Kbit的EEPROM存储器,通过I2C接口通信。写入和读取函数实现:
c复制void iic_24c02_write(uint8_t *pucBuf, uint8_t ucAddr, uint8_t ucNum)
{
I2CStart();
I2CSendByte(0xa0); // 器件地址+写
I2CWaitAck();
I2CSendByte(ucAddr); // 存储地址
I2CWaitAck();
while(ucNum--)
{
I2CSendByte(*pucBuf++);
I2CWaitAck();
}
I2CStop();
HAL_Delay(5); // 写入周期等待
}
void iic_24c02_read(uint8_t *pucBuf, uint8_t ucAddr, uint8_t ucNum)
{
I2CStart();
I2CSendByte(0xa0); // 器件地址+写
I2CWaitAck();
I2CSendByte(ucAddr); // 存储地址
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1); // 器件地址+读
I2CWaitAck();
while(ucNum--)
{
*pucBuf++ = I2CReceiveByte();
if(ucNum)
I2CSendAck();
else
I2CSendNotAck();
}
I2CStop();
}
9.2 MCP4017数字电位器驱动
MCP4017是I2C接口的数字电位器,可用于调节电阻值。驱动函数实现:
c复制void write_resistor(uint8_t value)
{
I2CStart();
I2CSendByte(0x5E); // 器件地址+写
I2CWaitAck();
I2CSendByte(value); // 电阻值(0-127)
I2CWaitAck();
I2CStop();
}
uint8_t read_resistor(void)
{
uint8_t value;
I2CStart();
I2CSendByte(0x5F); // 器件地址+读
I2CWaitAck();
value = I2CReceiveByte();
I2CSendNotAck();
I2CStop();
return value;
}
I2C通信注意事项:
- 器件地址需参考数据手册,通常包含读写位
- 每次操作后要检查ACK信号
- 时序要严格符合规范,特别是起停信号
- 适当加入延时保证设备响应时间
10. 开发经验与技巧
经过一段时间的开发实践,我总结了以下经验教训:
-
CubeMX使用技巧
- 定期保存.ioc文件,避免配置丢失
- 修改配置后要重新生成代码
- 注意检查时钟树配置是否正确
-
调试技巧
- 善用串口打印调试信息
- 使用LED指示程序运行状态
- 遇到问题时,从最简单的测试程序开始排查
-
常见问题解决
- 程序卡在启动:检查时钟配置和中断向量表
- 外设不工作:检查时钟使能和引脚配置
- 通信失败:检查时序和地址设置
-
竞赛准备建议
- 熟练掌握常用模块的配置和使用
- 提前准备好常用功能的代码模板
- 注意时间管理,先完成基础功能再优化
在开发过程中,我最大的体会是:嵌入式开发既需要扎实的理论基础,也需要丰富的实践经验。遇到问题时,要善于查阅资料、分析现象,通过实验验证解决方案。蓝桥杯嵌入式竞赛不仅考察技术能力,也考验解决问题的综合能力。