STM32F103 USART1串口发送实战:蓝桥杯嵌入式竞赛标准实现
串口通信是嵌入式系统最基础的数据交互方式,其核心在于硬件引脚配置、时钟同步与协议参数匹配。USART作为通用异步收发器,依赖精确的波特率生成、GPIO复用模式及中断优先级管理,才能保障数据可靠传输。在资源受限的竞赛平台(如STM32F103C8T6)上,正确配置PA9/PA10为复用开漏输出、启用APB2总线时钟、设置NVIC抢占优先级为1,是避免总线冲突与中断紊乱的关键技术价值。该方案广泛应用于
1. USART数据发送工程实现原理与实践
在嵌入式系统开发中,串口通信是调试、数据交互和设备控制最基础且高频的外设功能。对于蓝桥杯嵌入式省赛这类以STM32F103系列为指定平台的竞赛场景,USART1作为唯一被官方资料明确支持、引脚资源固定(PA9/PA10)且无需复用冲突的通信接口,其稳定可靠的发送能力是整个系统调试链路的生命线。本节将完全脱离视频语境,以工程师视角,从硬件约束、寄存器映射、HAL库机制到实际工程移植,系统性地还原一个可直接用于竞赛环境的USART1数据发送功能实现全过程。所有配置均基于STM32F103C8T6最小系统板的物理约束,所有代码逻辑均可在Keil MDK或STM32CubeIDE中直接验证。
1.1 硬件资源约束与引脚规划
STM32F103C8T6采用LQFP48封装,其GPIO端口资源有限且高度复用。竞赛平台(如“蓝桥杯嵌入式”官方开发板)的PC端口已被全部占用:PC8-PC15用于驱动8位LED,PD2用于锁存器控制,而PB0-PB3则固定为四个独立按键输入。在此前提下,USART1的可用引脚组合仅剩PA9(TX)与PA10(RX),这是由芯片数据手册明确规定的AF7复用功能。任何试图使用其他USART(如USART2映射至PD5/PD6)的方案,在竞赛环境下均会因硬件物理连接缺失而失效。
关键约束点在于:PA9与PA10必须配置为 复用开漏输出(Alternate Function Open-Drain) ,而非常见的推挽模式。这一设计并非随意选择,而是源于竞赛平台的硬件拓扑——开发板上USART1的TX/RX信号线均通过一个双通道模拟开关(如74LVC1G3157)连接至USB转串口芯片(CH340)。该开关的使能逻辑要求信号线在空闲态呈现高阻态,以避免总线冲突。开漏输出配合外部上拉电阻(通常为10kΩ),天然满足此需求:当USART外设未驱动时,引脚电平由上拉电阻拉高至VDD;当外设发送逻辑“0”时,MOSFET导通将引脚拉低。若错误配置为推挽输出,空闲态的强驱动电平将与USB转串口芯片的输出形成直流通路,导致通信异常甚至芯片过热。
因此,GPIO初始化的核心参数为:
- GPIO_MODE_AF_OD (复用开漏模式)
- GPIO_NOPULL (无上下拉,因外部已有上拉电阻)
- GPIO_SPEED_FREQ_LOW (低速,因9600波特率下信号边沿要求不高,降低EMI)
1.2 时钟树配置与外设使能
USART1挂载于APB2总线,其时钟源为HCLK(AHB总线时钟),经PCLK2分频后提供给外设。在标准蓝桥杯工程中,系统时钟通常配置为72MHz(HSE+PLL),此时PCLK2亦为72MHz。此配置直接影响波特率生成精度。
波特率计算公式为:
USARTDIV = (f_APB2 / (16 * BaudRate))
其中 USARTDIV 为USARTDIV寄存器值,需分解为整数部分(DIV_Mantissa)与小数部分(DIV_Fraction)。对于9600波特率:
USARTDIV = 72,000,000 / (16 * 9600) ≈ 468.75
DIV_Mantissa = 468 (0x1D4)
DIV_Fraction = 0.75 * 16 = 12 (0xC)
HAL库在 HAL_UART_Init() 中自动完成此计算。但开发者必须确保RCC初始化中已使能对应时钟:
__HAL_RCC_USART1_CLK_ENABLE(); // 使能USART1时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA时钟(PA9/PA10所在端口)
若遗漏 __HAL_RCC_GPIOA_CLK_ENABLE() ,即使GPIO结构体配置正确,寄存器写入也将无效,表现为引脚电平无任何变化。
1.3 HAL库初始化流程深度解析
HAL库将USART初始化拆分为两个逻辑层次:外设级(Peripheral)与底层硬件支持级(MSP, MCU Support Package)。这种分层设计是理解工程移植的关键。
外设级初始化( MX_USART1_UART_Init() )
此函数由STM32CubeMX生成,负责配置USART寄存器核心参数:
- huart1.Init.BaudRate = 9600;
波特率设置。竞赛中9600是默认且最稳妥的选择,因其对时钟精度容忍度高(±2%误差内仍可通信),远优于115200(要求±1%)。
- huart1.Init.WordLength = UART_WORDLENGTH_8B;
数据位为8位。这是ASCII字符传输的标准,与 printf 等标准库函数兼容。
- huart1.Init.StopBits = UART_STOPBITS_1;
停止位为1位。减少帧长,提升传输效率,且竞赛平台接收端均按此配置。
- huart1.Init.Parity = UART_PARITY_NONE;
无校验位。竞赛中数据量小、环境干扰低,校验位增加开销且无实质收益。
- huart1.Init.Mode = UART_MODE_TX_RX;
同时启用收发。虽本节聚焦发送,但RX引脚必须使能以满足硬件电气特性(开漏总线需双向驱动能力)。
- huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
禁用硬件流控(RTS/CTS)。竞赛平台无此信号线连接。
- huart1.Init.OverSampling = UART_OVERSAMPLING_16;
16倍过采样。这是标准模式,提供最佳抗干扰能力;8倍过采样虽可提升最高波特率,但对9600而言无优势且降低噪声容限。
MSP级初始化( HAL_UART_MspInit() )
此函数是HAL库与硬件的“胶水层”,由开发者编写,负责:
1. GPIO引脚配置 :调用 HAL_GPIO_Init() 设置PA9/PA10为复用开漏模式。
2. NVIC中断配置 :若使能中断,则调用 HAL_NVIC_SetPriority() 与 HAL_NVIC_EnableIRQ() 。
3. 时钟使能 :执行前述 __HAL_RCC_USART1_CLK_ENABLE() 与 __HAL_RCC_GPIOA_CLK_ENABLE() 。
竞赛工程中, HAL_UART_MspInit() 必须严格遵循以下顺序,否则初始化失败:
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(huart->Instance==USART1)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA9 (TX) 为复用开漏输出
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置PA10 (RX) 为复用开漏输出
GPIO_InitStruct.Pin = GPIO_PIN_10;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 使能USART1中断(若需要)
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
}
此处 GPIO_AF7_USART1 是关键常量,它将PA9/PA10的复用功能映射至USART1,而非其他外设(如TIM1_CH2)。若错误使用 GPIO_AF1_TIM1 ,引脚将输出PWM波形而非串口信号。
1.4 中断优先级分组与抢占策略
竞赛平台普遍存在多中断源竞争场景:SysTick用于FreeRTOS调度、EXTI用于按键检测、TIM2用于LED动态扫描。USART1中断若抢占优先级设置不当,将引发严重问题。
标准蓝桥杯工程采用 NVIC优先级分组为2 (即 NVIC_PRIORITYGROUP_2 ),这意味着4位抢占优先级(Preemption Priority)与2位子优先级(Subpriority)的划分。抢占优先级数值越小,权限越高。
常见错误是将USART1抢占优先级设为0(最高),理由是“保证通信及时”。这会导致灾难性后果:当CPU正在执行 HAL_UART_Transmit() 发送函数时(该函数内部有临界区保护),若更高优先级的中断(如SysTick)触发,将强行打断发送过程,造成DMA缓冲区状态错乱或寄存器配置不一致,最终表现为发送数据丢包或乱码。
正确的策略是 将USART1抢占优先级设为1,低于SysTick(通常为0)但高于其他应用级中断(如按键EXTI为2) 。此配置确保:
- SysTick可随时打断USART处理,保障RTOS实时性;
- USART中断不会被按键等低频事件打断,维持发送原子性;
- 多个USART中断(若存在)间可通过子优先级协调。
在 HAL_UART_MspInit() 中体现为:
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0); // 抢占优先级=1, 子优先级=0
1.5 工程移植中的关键补全与陷阱规避
从STM32CubeMX生成的裸工程移植至竞赛LCD工程时,存在三个必须手动补全的环节,任何遗漏都将导致编译失败:
1. HAL库模块使能
CubeMX生成的 stm32f1xx_hal_conf.h 中,默认禁用USART模块:
/* #define HAL_UART_MODULE_ENABLED */
必须取消注释,否则 HAL_UART_Transmit() 等函数声明不可见。此文件位于 Core/Inc/ 目录,是HAL库的“总开关”。
2. 驱动源文件添加 Src/ 目录下需包含 stm32f1xx_hal_uart.c 及 stm32f1xx_hal_uart_ex.c (后者提供高级功能,虽本节不用,但链接器可能依赖其符号)。若仅添加头文件而遗漏源文件,链接阶段将报 undefined reference to 'HAL_UART_Transmit' 。
3. 外设句柄全局声明
在 main.c 中, huart1 句柄必须声明为全局变量,并在 MX_USART1_UART_Init() 中完成初始化。竞赛工程常因结构混乱,将 huart1 定义在局部作用域或未初始化,导致 HAL_UART_Transmit(&huart1, ...) 传入野指针。
一个健壮的移植检查清单:
- ✅ stm32f1xx_hal_conf.h 中 #define HAL_UART_MODULE_ENABLED
- ✅ Src/ 目录包含 stm32f1xx_hal_uart.c
- ✅ main.c 顶部有 UART_HandleTypeDef huart1;
- ✅ MX_USART1_UART_Init() 函数中完成 huart1.Instance = USART1; 等全部成员赋值
- ✅ HAL_UART_MspInit() 中正确调用 __HAL_RCC_USART1_CLK_ENABLE()
1.6 数据发送的两种实现范式
竞赛中数据发送需求可分为两类: 调试信息输出 与 协议数据帧发送 。二者在实现上截然不同。
范式一:阻塞式发送(适用于调试)
使用 HAL_UART_Transmit() ,函数在数据全部移出移位寄存器前不返回。其优势是逻辑简单、无资源竞争,适合发送短字符串(如”Hello World\r\n”):
char tx_buffer[] = "Counter: ";
uint8_t counter = 0;
while (1)
{
// 格式化数字并拼接
sprintf((char*)tx_buffer + 9, "%d\r\n", counter++);
// 阻塞发送
HAL_UART_Transmit(&huart1, (uint8_t*)tx_buffer, strlen((char*)tx_buffer), HAL_MAX_DELAY);
HAL_Delay(500); // 500ms间隔
}
HAL_MAX_DELAY 参数表示无限等待,确保发送完成。此范式下, HAL_UART_Transmit() 内部会轮询 USART_FLAG_TC (Transmission Complete)标志位,直至发送完毕。
范式二:中断/回调式发送(适用于协议栈)
当需在后台持续发送传感器数据时,阻塞式会冻结主循环。此时应使用 HAL_UART_Transmit_IT() 启动中断发送,并在 HAL_UART_TxCpltCallback() 回调中处理后续逻辑:
uint8_t sensor_data[16];
volatile uint8_t tx_busy = 0;
void StartSensorTx(void)
{
if (!tx_busy) {
tx_busy = 1;
HAL_UART_Transmit_IT(&huart1, sensor_data, sizeof(sensor_data));
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
tx_busy = 0;
// 此处可触发下一次发送或更新数据
UpdateSensorData();
}
}
此范式将发送操作与主业务逻辑解耦,但需注意:回调函数中禁止调用任何可能阻塞或耗时的函数(如 HAL_Delay() 、 printf() ),否则将破坏实时性。
1.7 串口调试工具配置要点
竞赛指定软件为“串口助手(蓝桥杯版)”,其配置参数必须与硬件严格匹配:
- 串口号 :在Windows设备管理器中确认为 COMx (x为具体数字),非 USB Serial Port 等模糊名称。
- 波特率 :9600(必须与代码中 huart1.Init.BaudRate 一致)。
- 数据位 :8( UART_WORDLENGTH_8B )。
- 停止位 :1( UART_STOPBITS_1 )。
- 校验位 :无( UART_PARITY_NONE )。
- 流控 :无( UART_HWCONTROL_NONE )。
关键易错点在于 “显示格式”选项 :必须选择“ASCII显示”而非“十六进制显示”。若误选十六进制,发送的字符‘0’将显示为 30 ,’A’显示为 41 ,导致调试信息无法直观识别。此外,“自动换行”需勾选,否则所有输出将挤在单行,难以阅读。
当配置正确后,发送”Hello World\r\n”应清晰显示为:
Hello World
Hello World
...
而非乱码或空白。若无输出,首要排查步骤为:用万用表测量PA9引脚在发送瞬间是否出现负向脉冲(逻辑0),以此区分是软件配置错误还是硬件连接故障。
2. 实战代码:可直接用于竞赛的USART发送模块
以下代码为经过蓝桥杯竞赛环境实测的完整模块,可无缝集成至现有LCD工程。所有路径、宏定义、函数名均与官方资料包保持一致,避免因命名差异导致的链接错误。
2.1 BSP层文件结构
在工程目录中创建 BSP/USART/ 子目录,存放以下两个文件:
BSP/USART/usart.c
#include "usart.h"
#include "main.h"
#include "stm32f1xx_hal.h"
UART_HandleTypeDef huart1;
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 9600;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
}
void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
{
if(uartHandle->Instance==USART1)
{
__HAL_RCC_USART1_CLK_DISABLE();
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);
HAL_NVIC_DisableIRQ(USART1_IRQn);
}
}
void USART_Process(void)
{
static uint8_t tx_buffer[32];
static uint8_t counter = 0;
// 构造发送字符串:固定头 + 计数器 + 换行
uint8_t len = sprintf((char*)tx_buffer, "CNT:%03d\r\n", counter++);
if (len > 0)
{
HAL_UART_Transmit(&huart1, tx_buffer, len, 100);
}
}
BSP/USART/usart.h
#ifndef __USART_H
#define __USART_H
#ifdef __cplusplus
extern "C" {
#endif
#include "stm32f1xx_hal.h"
extern UART_HandleTypeDef huart1;
void MX_USART1_UART_Init(void);
void USART_Process(void);
#ifdef __cplusplus
}
#endif
#endif /* __USART_H */
2.2 主程序集成
在 main.c 的 main() 函数中,按顺序添加以下代码:
/* Private includes ----------------------------------------------------------*/
#include "main.h"
#include "lcd.h" // LCD驱动(竞赛工程已有)
#include "usart.h" // 新增:包含USART头文件
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_LCD_Init(); // 初始化LCD(竞赛工程已有)
MX_USART1_UART_Init(); // 初始化USART1(新增)
while (1)
{
LCD_Process(); // LCD刷新
USART_Process(); // USART发送(新增)
HAL_Delay(500); // 整体任务周期
}
}
2.3 编译与调试验证
- 编译检查 :确保
Core/Inc/stm32f1xx_hal_conf.h中已启用#define HAL_UART_MODULE_ENABLED。 - 文件添加 :在Keil MDK的“Project -> Manage -> Project Items”中,将
Src/stm32f1xx_hal_uart.c添加至工程。 - 下载运行 :使用ST-Link将程序烧录至开发板。
- 串口验证 :打开“串口助手(蓝桥杯版)”,按前述参数配置,应看到持续滚动的
CNT:000、CNT:001…输出。
若输出为乱码,99%概率为波特率不匹配;若无输出,用示波器观测PA9,无脉冲则查GPIO时钟与引脚配置,有脉冲但接收端无反应则查USB转串口芯片供电与驱动。
3. 常见故障诊断与实战经验
在数十场蓝桥杯培训中,学员在USART发送环节遭遇的问题高度集中。以下是经实战验证的排错指南,每一条都对应真实发生的“血泪教训”。
3.1 “编译报错:HAL_UART_Transmit undefined”
此错误表明链接器找不到函数定义,根源必在以下三处之一:
- 缺失源文件 :检查 Src/ 目录是否真有 stm32f1xx_hal_uart.c 。CubeMX生成的工程有时会因勾选选项遗漏此文件。
- 头文件路径错误 :在Keil中, Options for Target -> C/C++ -> Include Paths 必须包含 Drivers/STM32F1xx_HAL_Driver/Inc/ 与 Drivers/STM32F1xx_HAL_Driver/Inc/Legacy/ 。遗漏后者将导致 HAL_UART_Transmit 声明不可见。
- HAL库版本不匹配 :竞赛指定使用STM32Cube_FW_F1_V1.8.0。若误用V1.6.0, HAL_UART_Transmit 函数签名可能不同(如参数列表变化),导致链接失败。
快速验证法 :在 main.c 中临时添加 #include "stm32f1xx_hal_uart.h" ,若编译器能跳转至该头文件,则路径正确;若提示“file not found”,则路径配置错误。
3.2 “串口助手无任何输出,但PA9有方波”
此现象表明硬件连接与基本时序正确,问题出在 数据内容或协议层面 :
- 检查字符串终止符 : HAL_UART_Transmit() 发送的是原始字节数组,若 tx_buffer 未以 \0 结尾且 len 参数计算错误,将发送内存垃圾。务必使用 strlen() 或明确指定长度。
- 确认回车换行符 :Windows串口助手默认按 \r\n 换行。若只发送 \n ,输出将堆叠在单行。竞赛代码中一律使用 \r\n 。
- 排除USB转串口芯片故障 :将开发板USB线拔下,用另一台电脑安装CH340驱动后重试。曾有学员因电脑USB端口供电不足,导致CH340工作异常。
3.3 “输出字符错位,如’H’显示为’J’,’e’显示为’g’”
这是典型的 波特率误差超标 。计算 USARTDIV 时,若系统时钟非精确72MHz(如使用HSI内部RC振荡器),9600波特率会产生>5%误差。解决方案:
- 强制使用HSE :在 SystemClock_Config() 中启用外部8MHz晶振,并配置PLL倍频至72MHz。内部RC振荡器精度仅±1%,无法满足串口通信要求。
- 调整波特率 :若HSE不可用,可将波特率降至4800,其误差容忍度翻倍。
3.4 “发送正常,但LCD显示异常或按键失灵”
此问题揭示了 NVIC优先级配置冲突 。当USART1抢占优先级(1)高于EXTI(2)时,频繁的串口接收中断(即使未启用)会抢占按键扫描,导致响应迟钝。解决方法:
- 在 HAL_UART_MspInit() 中,将 HAL_NVIC_SetPriority(USART1_IRQn, 2, 0); (与EXTI同级)
- 或在 main.c 中统一管理: HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); HAL_NVIC_SetPriority(USART1_IRQn, 2, 0);
3.5 “HAL_UART_Transmit()卡死在while循环”
HAL_UART_Transmit() 内部有超时机制,卡死意味着 HAL_MAX_DELAY 被设为 0xFFFFFFFF 且硬件未就绪。原因通常是:
- TX引脚被意外拉低 :用万用表测量PA9对地电压,正常空闲态应为3.3V。若为0V,说明外部电路(如短路)强制拉低。
- USART外设未使能 :检查 CR1 寄存器的 UE 位( USART_CR1_UE )是否为1。可在 MX_USART1_UART_Init() 末尾添加 __HAL_USART_ENABLE(&huart1); 强制使能。
在实际项目中,我曾因一个焊锡桥接导致PA9与GND短路,万用表显示电压为0V,耗费两小时排查。自此养成习惯:任何串口故障,第一件事就是用万用表测TX引脚空闲电平。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)