前言
在嵌入式开发中,串口调试是最基础且最重要的手段。本文将详细介绍如何在 STM32F407 上实现一个健壮的 USART 串口驱动,涵盖初始化配置、字符/字符串发送逻辑,以及如何重定向 printf 函数。
1. 串口初始化:底层配置步骤
要启动串口,必须遵循 STM32 的时钟树架构。以下是初始化 USART1(PA9/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 标志位。