1. 项目概述
作为一名嵌入式开发者,串口通信是我们最常打交道的功能之一。记得我第一次调试STM32串口时,整整花了两天时间才让开发板成功和电脑"对话"。今天,我将通过一个完整的实例,带大家实现STM32F407与电脑之间的字符串双向通信。这个实验看似简单,但包含了串口开发的所有核心要素:硬件连接、参数配置、中断处理以及实际调试中的各种"坑"。
本次实验使用的是STM32F407开发板(正点原子或野火均可),通过USB转TTL模块(CH340/CP2102)与电脑连接。我们将配置USART1工作在115200波特率(8数据位、1停止位、无校验),实现以下功能:
- STM32上电后自动发送欢迎字符串到电脑
- 电脑通过串口助手发送的字符串能被STM32接收并回传
- 采用中断方式接收数据,避免轮询造成的资源浪费
2. 实验准备
2.1 硬件清单详解
主控板选择:
推荐使用正点原子或野火的STM32F407开发板,它们都带有标准的USART1接口(PA9/TX,PA10/RX)。我手头用的是野火F407挑战者V2板,其USART1已经通过CH340G芯片引出到Type-C接口,可以直接用USB线连接电脑,省去了外接USB转TTL模块的麻烦。
USB转TTL模块选型:
如果开发板没有内置USB转串口芯片,就需要外接模块。市面上常见的有:
- CH340系列:性价比高,驱动安装简单
- CP2102:稳定性更好,但价格稍贵
- PL2303:老牌芯片,但Win10后驱动支持不佳
特别注意:一定要确保模块支持3.3V电平!有些模块是5V电平的,直接连接会损坏STM32的IO口。
连线示意图:
code复制STM32F407 USB转TTL模块
PA9(TX) ------ RX
PA10(RX) ------ TX
GND ------ GND
注意这里是交叉连接(TX接RX,RX接TX),新手最容易犯的错误就是直连。
2.2 软件环境搭建
开发工具链:
- Keil MDK-ARM V5:建议安装最新版(我用的5.38),安装时记得勾选STM32F4的设备支持包
- STM32CubeMX:虽然不是必须,但可以用来快速生成初始化代码
- 串口调试助手:推荐使用野火fireTools、SecureCRT或Putty
驱动安装要点:
- 连接USB转TTL模块后,在设备管理器中查看是否识别
- 如果出现黄色感叹号,需要手动安装驱动(CH340驱动下载地址:www.wch.cn/downloads/CH341SER_EXE.html)
- 安装完成后,记下分配的COM口号(如COM3),后续串口助手需要选择这个端口
2.3 实验目标拆解
我们的核心目标是建立双向通信链路,具体分解为:
- 初始化配置:
- 时钟使能:USART1挂载在APB2总线,GPIO在AHB1总线
- GPIO模式:PA9复用推挽输出,PA10上拉输入
- 通信参数:115200波特率,8位数据,1位停止,无校验
- 发送功能:
- 上电后自动发送"这是一个串口中断接收回显实验\n"
- 实现字符串发送函数Usart_SendString()
- 接收功能:
- 配置接收中断(NVIC优先级分组2,抢占优先级1)
- 在中断服务函数中实现回显功能
3. 核心原理深度解析
3.1 USART通信协议详解
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)是STM32中最灵活的通信接口之一。我们常用的"串口"实际指的是它的异步模式(UART),主要特点:
- 异步传输:不需要时钟线,依靠双方约定的波特率同步
- 全双工:可以同时收发数据
- 帧结构:起始位(1) + 数据位(8) + 停止位(1)是常见组合
波特率计算:
STM32的波特率计算公式为:
code复制波特率 = fCK / (16 * USARTDIV)
其中fCK是USART时钟(APB2时钟84MHz),USARTDIV是一个16位浮点数(整数部分存于DIV_Mantissa,小数部分存于DIV_Fraction)。
以115200波特率为例:
code复制USARTDIV = 84000000 / (16 * 115200) = 45.5729
DIV_Mantissa = 45 = 0x2D
DIV_Fraction = 0.5729 * 16 ≈ 9 = 0x09
所以BRR寄存器应设置为0x2D9。
3.2 STM32F407的USART1硬件架构
USART1在STM32F407中的特殊之处:
- 挂载在APB2总线(最高84MHz),其他USART在APB1(42MHz)
- 引脚固定为PA9(TX)/PA10(RX),不支持重映射
- 具有独立的中断向量(USART1_IRQn)
GPIO配置要点:
- TX引脚应配置为复用推挽输出(GPIO_Mode_AF,GPIO_OType_PP)
- RX引脚建议配置为上拉输入(GPIO_PuPd_UP),增强抗干扰能力
- 必须调用GPIO_PinAFConfig()将引脚映射到USART1功能
3.3 中断驱动设计
相比轮询方式,中断接收有显著优势:
- 不占用CPU时间,只在数据到达时触发处理
- 可以及时响应数据,避免丢失
关键中断标志:
- RXNE(接收缓冲区非空):数据到达时自动置1
- TC(发送完成):一帧数据发送完成后置1
NVIC配置步骤:
- 设置优先级分组(如NVIC_PriorityGroup_2)
- 配置USART1中断通道
- 使能中断通道
4. 代码实现逐行解析
4.1 硬件抽象层实现
创建bsp_debug_usart.h头文件,集中管理硬件相关定义:
c复制#ifndef __DEBUG_USART_H
#define __DEBUG_USART_H
#include "stm32f4xx.h"
// 硬件配置宏定义
#define DEBUG_USART USART1
#define DEBUG_USART_CLK RCC_APB2Periph_USART1
#define DEBUG_USART_BAUDRATE 115200
// TX引脚配置(PA9)
#define DEBUG_USART_TX_GPIO_PORT GPIOA
#define DEBUG_USART_TX_GPIO_CLK RCC_AHB1Periph_GPIOA
#define DEBUG_USART_TX_PIN GPIO_Pin_9
#define DEBUG_USART_TX_AF GPIO_AF_USART1
// RX引脚配置(PA10)
#define DEBUG_USART_RX_GPIO_PORT GPIOA
#define DEBUG_USART_RX_GPIO_CLK RCC_AHB1Periph_GPIOA
#define DEBUG_USART_RX_PIN GPIO_Pin_10
#define DEBUG_USART_RX_AF GPIO_AF_USART1
// 中断相关
#define DEBUG_USART_IRQHandler USART1_IRQHandler
#define DEBUG_USART_IRQ USART1_IRQn
// 函数声明
void Debug_USART_Config(void);
void Usart_SendByte(USART_TypeDef *pUSARTx, uint8_t ch);
void Usart_SendString(USART_TypeDef *pUSARTx, char *str);
#endif
4.2 USART初始化函数
在bsp_debug_usart.c中实现完整的初始化流程:
c复制void Debug_USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 1. 使能时钟
RCC_AHB1PeriphClockCmd(DEBUG_USART_TX_GPIO_CLK | DEBUG_USART_RX_GPIO_CLK, ENABLE);
RCC_APB2PeriphClockCmd(DEBUG_USART_CLK, ENABLE);
// 2. 配置GPIO
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_PIN;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 3. 引脚复用映射
GPIO_PinAFConfig(DEBUG_USART_TX_GPIO_PORT, GPIO_PinSource9, DEBUG_USART_TX_AF);
GPIO_PinAFConfig(DEBUG_USART_RX_GPIO_PORT, GPIO_PinSource10, DEBUG_USART_RX_AF);
// 4. USART参数配置
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(DEBUG_USART, &USART_InitStructure);
// 5. 中断配置
NVIC_Configuration();
USART_ITConfig(DEBUG_USART, USART_IT_RXNE, ENABLE);
// 6. 使能USART
USART_Cmd(DEBUG_USART, ENABLE);
}
4.3 中断服务函数实现
接收中断的核心处理逻辑:
c复制void USART1_IRQHandler(void)
{
uint8_t ch;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
ch = USART_ReceiveData(USART1); // 读取接收到的数据
USART_SendData(USART1, ch); // 回传数据(回显)
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); // 等待发送完成
}
}
4.4 主函数实现
main.c中的简单测试代码:
c复制int main(void)
{
Debug_USART_Config(); // 初始化USART
// 发送欢迎信息
Usart_SendString(DEBUG_USART, "这是一个串口中断接收回显实验\n");
while(1)
{
// 主循环可以处理其他任务
// 串口接收通过中断自动处理
}
}
5. 调试问题与解决方案
5.1 接收不到数据
现象:电脑发送数据,但STM32没有任何反应,中断从未触发。
排查过程:
- 检查硬件连接:用万用表测量PA9-PA10电压,发送数据时应有电平变化
- 检查波特率:确保双方都是115200,误差不超过3%
- 检查中断配置:NVIC是否使能,中断优先级设置是否正确
根本原因:开发板的DAP调试器与串口冲突。野火板子的DAP芯片会占用USART信号线。
解决方案:
- 断开DAP连接(拔掉USB线)
- 单独使用UART接口连接USB转TTL模块
- 或者修改电路,避免信号冲突
5.2 接收到乱码数据
现象:能进入中断,但接收到的数据与发送的不一致。
可能原因:
- 波特率不匹配(最常见)
- 停止位/校验位设置不一致
- 电气干扰导致数据错误
诊断方法:
- 使用示波器测量实际波特率
- 检查USART_InitStructure所有参数
- 尝试降低波特率测试(如改为9600)
典型案例:
c复制// 错误的引脚复用配置
#define DEBUG_USART_RX_AF GPIO_Mode_AF // 错误!应该是GPIO_AF_USART1
这种错误会导致GPIO没有正确映射到USART功能,虽然能进中断,但数据是错的。
5.3 数据丢失问题
现象:高速传输时(如115200波特率连续发送),部分数据丢失。
原因分析:
- 中断响应不及时
- 缓冲区溢出
- 发送/接收函数阻塞时间过长
优化方案:
- 提高中断优先级
- 使用DMA传输(后续实验会介绍)
- 实现环形缓冲区
6. 进阶优化建议
6.1 添加printf重定向
方便调试,可以重定向printf到USART:
c复制#include <stdio.h>
int fputc(int ch, FILE *f)
{
USART_SendData(DEBUG_USART, (uint8_t)ch);
while(USART_GetFlagStatus(DEBUG_USART, USART_FLAG_TC) == RESET);
return ch;
}
使用时需要勾选"Use MicroLIB"选项。
6.2 实现命令解析
扩展中断服务函数,实现简单命令行接口:
c复制#define MAX_CMD_LEN 64
char cmd_buf[MAX_CMD_LEN];
uint8_t cmd_idx = 0;
void USART1_IRQHandler(void)
{
char ch;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
ch = USART_ReceiveData(USART1);
if(ch == '\r' || ch == '\n') // 回车键
{
cmd_buf[cmd_idx] = '\0';
process_command(cmd_buf); // 处理命令
cmd_idx = 0;
}
else if(cmd_idx < MAX_CMD_LEN-1)
{
cmd_buf[cmd_idx++] = ch;
}
USART_SendData(USART1, ch); // 回显
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
}
6.3 波特率自动检测
对于需要兼容不同设备的应用,可以实现波特率自动检测:
c复制void AutoBaudRateDetection(void)
{
uint32_t counter = 0;
// 配置PA10为浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NONE;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 等待起始位下降沿
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_10) == Bit_SET);
// 测量起始位持续时间
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_10) == Bit_RESET)
{
counter++;
delay_us(1);
}
// 计算波特率(起始位通常为1位时间)
uint32_t baudrate = 1000000 / counter; // 假设delay_us精确
// 重新配置USART
USART_InitStructure.USART_BaudRate = baudrate;
USART_Init(USART1, &USART_InitStructure);
}
7. 工程实践建议
-
代码组织规范:
- 硬件相关代码放在bsp目录下
- 业务逻辑放在app目录
- 使用头文件保护宏防止重复包含
-
错误处理机制:
- 添加超时判断(如while循环添加计数器)
- 实现错误码返回机制
- 关键操作添加assert断言
-
功耗优化:
- 不需要通信时进入低功耗模式
- 使用DMA减少CPU干预
- 动态调整波特率(低速时降低功耗)
-
抗干扰设计:
- 添加硬件滤波电容
- 软件实现数据校验(如CRC)
- 使用屏蔽线缆长距离传输
通过这个完整的串口实验,我们不仅掌握了基本的通信功能实现,还深入理解了STM32的USART工作原理。在实际项目中,串口往往是系统调试和外部通信的重要接口,建议读者在此基础上继续探索:
- 多串口协同工作
- Modbus协议实现
- 与无线模块(如ESP8266)对接
- 高速通信(使用DMA+环形缓冲区)
遇到问题时,记住三板斧:查硬件连接、验配置参数、看状态标志。祝大家的串口通信一次成功!
