【STM32】基于寄存器/库函数的串口(USART)驱动封装:从单字节到字符串发送
前言在嵌入式开发中串口调试是最基础且最重要的手段。本文将详细介绍如何在 STM32F407 上实现一个健壮的 USART 串口驱动涵盖初始化配置、字符/字符串发送逻辑以及如何重定向 printf 函数。1. 串口初始化底层配置步骤要启动串口必须遵循 STM32 的时钟树架构。以下是初始化 USART1PA9/PA10的标准流程关键步骤时钟使能开启 GPIOA 和 USART1 的时钟注意它们分属不同的总线。引脚映射配置 GPIO 模式为复用模式AF并指定复用为串口功能。参数配置设置波特率115200、数据位8b、停止位1b和无校验。中断配置配置 NVIC 并使能接收中断RXNE用于实现异步数据接收。2. 完整驱动代码实现以下是封装好的驱动代码。usart.c C #include usart.h #include stm32f4xx.h /** * brief USART1 初始化配置函数 * details 配置 PA9(TX) 和 PA10(RX) 为复用功能并设置波特率 115200 */ void USART_Init_Config(void) { USART_InitTypeDef USART_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 使能 GPIOA 和 USART1 的外设时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 2. 初始化 GPIO 模式 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; // 复用功能模式 GPIO_InitStructure.GPIO_OType GPIO_OType_PP; // 推挽输出 GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; // 上拉模式 GPIO_InitStructure.GPIO_Speed GPIO_High_Speed; // 响应速度 GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置引脚复用映射 GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); // PA9 - TX GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); // PA10 - RX // 4. 配置 USART 参数 USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_Init(USART1, USART_InitStructure); // 5. 配置 NVIC 串口中断优先级 NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0x1; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority 0x1; // 响应优先级 NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 6. 使能接收非空中断 (RXNE) 并开启串口 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); } /** * brief 发送单字节数据 * param USARTx: 串口号ch: 发送的字符 */ void Usart_SendByte(USART_TypeDef * USARTx , uint16_t ch) { USART_SendData(USARTx, ch); // 等待 TXE(发送数据寄存器空) 标志位置位确保数据已从 DR 移入移位寄存器 while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) RESET); } /** * brief 发送字符串 * param str: 字符串首地址 */ void Usart_SendString(USART_TypeDef * USARTx , char* str) { uint16_t k 0; do { Usart_SendByte(USARTx, *(str k)); k; } while(*(str k) ! \0); // 整个字符串循环结束后等待 TC(发送完成) 标志位 // 确保最后一帧数据也完全物理离开引脚这在 RS485 或切换省电模式前至关重要 while (USART_GetFlagStatus(USARTx, USART_FLAG_TC) RESET); } /** * brief printf 重定向 */ int fputc(int ch, FILE *stream) { USART_SendData(USART1, (uint16_t)ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }3. 核心原理探究TXE vs TC 标志位很多同学在写发送函数时分不清 TXE 和 TC。TXE (Transmit Data Register Empty)当数据从 TDR 寄存器移动到“移位寄存器”时置位。这意味着你可以往串口里塞下一个字节了但上一个字节未必发完了。TC (Transmission Complete)当整个数据帧包括停止位都从“移位寄存器”通过物理引脚发完时置位。经验法则在循环发送字节时检测 TXE为了效率在关闭串口或切换 RS485 方向前检测 TC为了数据完整性。4. 调试小贴士printf 无输出请检查你的工程配置中是否勾选了 Use MicroLIB。乱码问题多半是外部晶振HSE频率配置与代码中的 HSE_VALUE 不匹配或者波特率算错。上电首字节乱码尝试在初始化完成后手动清除 TC 标志位。