前言
各位零基础的STM32新手小伙伴,本节我们正式进入USART串口通信的实战编程阶段。在前序章节了解串口基本概念后,本节核心目标是:
-
理解STM32F103串口通信的硬件底层逻辑;
-
手把手完成串口驱动工程的基础搭建;
-
掌握USART1的完整初始化配置(GPIO+NVIC+串口模式);
-
编写并验证串口单字节发送函数;
-
理解代码中嵌入式C语言的核心用法(宏定义、结构体、库函数调用)。
全程以"概念→原理→代码→验证"的逻辑展开,所有代码均可直接编译调试,配套指南者/霸道开发板,跟着操作就能完成第一个串口通信实验!
一、本章实验规划
1.1 实验目标(新手友好版)
本章设计2个递进式实验,从"基础通信"到"外设控制",逐步掌握串口核心用法:
实验序号 实验名称 核心目标
实验1 USART中断收发实验 实现"单片机→电脑"发送数据、"电脑→单片机"接收数据并回传(本节完成发送部分)
实验2 串口指令控制RGB灯实验 电脑下发指令(如"ON"/"OFF"),单片机解析指令并控制RGB灯亮灭
1.2 必备工具
-
软件:Keil MDK 5.x(STM32F103编译环境)、官方串口调试助手、CH340 USB转串口驱动;
-
硬件:指南者/霸道STM32F103开发板、USB数据线(连接开发板"USB to UART"接口)。
二、串口通信硬件基础(新手必懂)
2.1 为什么需要"电平转换"?(生活化类比)
电脑USB接口是USB电平(高电平3.3V/低电平0V,但通信协议是USB),STM32串口引脚是TTL电平(高电平3.3V/低电平0V,通信协议是UART),两者"语言格式"不同,就像普通话和方言,需要"翻译官"------CH340G芯片(开发板板载)完成电平/协议转换。
关键知识点
-
驱动要求:必须安装CH340驱动(开发板配套光盘/官网下载),安装时右键"以管理员身份运行"(新手易踩坑:不装驱动电脑识别不到串口);
-
核心引脚(USART1默认):
-
PA9(USART1_TX):单片机"说话"的引脚(发送);
-
PA10(USART1_RX):单片机"听"的引脚(接收);
-
-
硬件连接规则:TX接RX、RX接TX(交叉连接),开发板通过跳帽默认完成,无需手动改。
2.2 开发板串口资源与跳帽配置(新手避坑)
STM32F103共支持5个串口(USART1~USART3 + UART4~UART5),不同串口的"挂载总线""默认引脚"不同,新手优先掌握USART1(与电脑通信最常用)。
2.2.1 串口资源速查表(新手版)
串口编号 英文全称 默认收发引脚 挂载总线 板载默认用途
USART1 Universal Synchronous/Asynchronous Receiver/Transmitter 1 PA9(TX)/PA10(RX) APB2 连接CH340,与电脑上位机通信
USART2 Universal Synchronous/Asynchronous Receiver/Transmitter 2 PA2(TX)/PA3(RX) APB1 霸道板默认接RS232/485,指南者仅引脚引出
USART3~UART5 - 见备注 APB1 引脚引出,需杜邦线自定义连接
2.2.2 跳帽配置(新手只记默认场景)
-
新手默认场景:用USART1与电脑通信 → 保持PA9/PA10与CH340的跳帽"插上"即可;
-
进阶场景(用其他串口):先拔USART1跳帽,再用杜邦线把CH340的TX接目标串口RX、CH340的RX接目标串口TX(新手暂时不用管)。
2.3 RS232电平转换(了解即可)
霸道板额外板载MAX3232芯片,可将STM32的TTL电平转为RS232电平(DB9接口),指南者无此电路;新手初期无需使用,重点掌握CH340+USART1的组合即可。
三、串口工程基础搭建(手把手操作)
新手第一步先搭好工程框架,避免后续代码混乱,步骤如下(每一步配新手提示):
3.1 新建串口驱动文件
在你的STM32工程目录下,新建2个文件:
-
bsp_usart.c:存放串口驱动的核心代码(初始化、发送/接收函数);
-
bsp_usart.h:存放宏定义、函数声明(头文件)。
新手提示:"bsp"是"板级支持包(Board Support Package)"的缩写,是嵌入式开发的通用命名规范,方便区分不同外设的驱动文件。
3.2 Keil中导入文件
-
打开Keil MDK工程,在左侧"Project"窗口右键目标分组(如"User")→ 选择"Add Files to Group...";
-
找到新建的bsp_usart.c,选中并添加(bsp_usart.h无需添加,仅需配置路径)。
3.3 配置头文件路径(关键!新手易漏)
Keil编译器需要知道bsp_usart.h的位置,否则会报"头文件未找到"错误:
-
点击Keil顶部"魔术棒"图标(⚙️)→ 选择"C/C++"选项卡;
-
在"Include Paths"栏点击"..."→ 添加bsp_usart.h所在的文件夹路径 → 点击"OK"保存。
3.4 头文件基础配置(C语言知识点)
在bsp_usart.h中添加"条件编译保护"(防止头文件重复包含),并引入核心头文件:
#ifndef __BSP_USART_H
#define __BSP_USART_H
#include "stm32f10x.h"
/* 后续添加宏定义、函数声明 */
#endif /* __BSP_USART_H */
C语言知识点:#ifndef/#define/#endif是头文件保护的标准写法,避免多次包含同一头文件导致"重复定义"错误(比如main.c和bsp_usart.c都包含该头文件时,编译器只会编译一次)。
3.5 源文件引入头文件
在bsp_usart.c开头添加:
#include "bsp_usart.h"
3.6 首次编译(排查基础错误)
点击Keil顶部"Build"按钮(🔨),如果编译无报错,说明工程框架搭建完成;若报错,优先排查:
-
头文件路径是否配置正确;
-
文件名是否拼写错误(如bsp_usart写成bsp_usart1);
-
条件编译符号是否匹配(如__BSP_USART_H是否前后一致)。
四、USART串口初始化编程详解(核心)
串口初始化是通信正常的关键,需完成GPIO配置、NVIC中断配置、串口模式配置三大步骤,我们用"宏定义封装+函数拆分"的方式编写代码(新手易维护、易移植)。
4.1 串口硬件相关宏定义(C语言:宏定义的优势)
在bsp_usart.h中添加以下宏定义,把所有"硬件相关参数"封装起来------后续要切换串口(如USART2),只需改宏定义,无需改核心函数!
/************************* USART1 硬件相关宏定义 *************************/
#define DEBUG_USART_TX_GPIO_PORT GPIOA
#define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_9
#define DEBUG_USART_TX_GPIO_CLK RCC_APB2Periph_GPIOA
#define DEBUG_USART_RX_GPIO_PORT GPIOA
#define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_10
#define DEBUG_USART_RX_GPIO_CLK RCC_APB2Periph_GPIOA
#define DEBUG_USART USART1
#define DEBUG_USART_CLK RCC_APB2Periph_USART1
#define DEBUG_USART_IRQn USART1_IRQn
#define DEBUG_USART_IRQHandler USART1_IRQHandler
#define DEBUG_USART_BAUDRATE 115200
C语言知识点:宏定义(#define)的核心优势是"一次定义、多处使用、统一修改",嵌入式开发中常用于封装硬件参数(引脚、时钟、中断号等),大幅提升代码可移植性。
新手提示:USART1挂载在APB2总线,USART2~UART5挂载在APB1总线 → 时钟使能函数要对应(后续代码会体现)。
4.2 完整初始化函数实现(逐行解析)
初始化函数拆分为3个子函数(GPIO、NVIC、串口模式),最终封装为统一入口,代码写在bsp_usart.c中:
4.2.1 子函数1:GPIO引脚配置(串口TX/RX引脚初始化)
static void USART_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(DEBUG_USART_TX_GPIO_CLK | DEBUG_USART_RX_GPIO_CLK, ENABLE);
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
}
核心知识点解析:
-
static关键字:该函数仅能在bsp_usart.c中调用,其他文件无法访问,避免多个驱动文件的函数名冲突(嵌入式开发常用);
-
GPIO_InitTypeDef:STM32标准库定义的GPIO初始化结构体,把引脚的"引脚号、模式、速率"封装成结构体,通过GPIO_Init函数一次性配置,比直接操作寄存器更简单;
-
GPIO模式:
-
TX引脚:GPIO_Mode_AF_PP(复用推挽输出)→ "复用"表示引脚不是普通GPIO,而是串口外设的输出;"推挽"表示能输出高低电平,驱动外部电路;
-
RX引脚:GPIO_Mode_IN_FLOATING(浮空输入)→ 无需上拉/下拉,直接接收外部TTL电平信号,适配串口通信场景。
-
4.2.2 子函数2:NVIC中断配置(接收中断用)
static void USART_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
核心知识点解析:
-
NVIC:嵌套向量中断控制器(Nested Vectored Interrupt Controller),是Cortex-M3内核的核心组件,用于管理所有中断的优先级和使能;
-
中断优先级分组:NVIC_PriorityGroup_2表示"2位抢占优先级(0~3)+2位子优先级(0~3)",整个工程只能配置一次(若多次配置,以最后一次为准);
-
抢占优先级 vs 子优先级:
-
抢占优先级:高优先级中断可"打断"低优先级中断(比如串口中断可打断定时器中断);
-
子优先级:同抢占优先级的中断,按子优先级顺序响应(无抢占关系)。
-
4.2.3 子函数3:串口工作模式配置
static void USART_Mode_Config(void)
{
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(DEBUG_USART_CLK, ENABLE);
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_Tx | USART_Mode_Rx;
USART_Init(DEBUG_USART, &USART_InitStructure);
USART_ITConfig(DEBUG_USART, USART_IT_RXNE, ENABLE);
USART_Cmd(DEBUG_USART, ENABLE);
}
核心知识点解析:
-
串口时钟:USART1挂载APB2总线 → 用RCC_APB2PeriphClockCmd;USART2~UART5挂载APB1 → 用RCC_APB1PeriphClockCmd(新手易踩坑:时钟总线错配会导致串口完全无响应);
-
通信参数:波特率、数据位、停止位、校验位必须与上位机串口调试助手完全一致,否则会出现"乱码"或"通信失败";
-
USART_IT_RXNE:接收数据寄存器非空中断 → 当单片机收到1个字节数据时,会立即触发中断,后续可在中断服务函数中处理数据(本节先使能,下节讲解中断服务函数);
-
USART_Cmd:串口总开关,必须最后调用------所有配置完成后,再开启串口外设,避免配置过程中串口异常。
4.2.4 初始化入口函数(整合所有子函数)
void USART_Config(void)
{
USART_GPIO_Config();
USART_NVIC_Config();
USART_Mode_Config();
}
4.3 声明初始化函数(头文件)
在bsp_usart.h中添加函数声明,让main.c能调用该函数:
void USART_Config(void);
4.4 初始化关键注意事项(新手避坑清单)
-
时钟总线匹配:USART1→APB2,USART2~UART5→APB1,时钟使能函数必须对应;
-
GPIO模式正确:TX=复用推挽输出,RX=浮空输入(错配会导致发送/接收失败);
-
通信参数一致:单片机与上位机的波特率、数据位等必须完全相同;
-
中断分组唯一:整个工程仅需调用一次NVIC_PriorityGroupConfig,多次调用会覆盖配置。
五、串口单字节发送函数实现与验证
初始化完成后,先实现"单字节发送"功能(最简单、最易验证),为后续字符串发送、printf重定向打基础。
5.1 单字节发送函数实现
在bsp_usart.c中编写通用的单字节发送函数(适配所有串口):
void USART_SendByte(USART_TypeDef* USARTx, uint8_t data)
{
USART_SendData(USARTx, data);
while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);
}
核心知识点解析:
-
USART_TypeDef*:指针参数,指向STM32串口外设的寄存器结构体(如USART1、USART2),因此该函数可适配所有串口,通用性强;
-
uint8_t:是unsigned char的别名(在stdint.h中定义),嵌入式开发中优先用uint8_t/uint16_t等固定长度类型,避免不同编译器的"char"长度差异;
-
等待发送完成:USART_FLAG_TXE是"发送数据寄存器空"标志位------当该位为RESET(0)时,说明上一个字节还在发送中;为SET(1)时,说明数据已从DR寄存器转移到发送移位寄存器,可写入下一个字节(避免数据覆盖丢失);
-
死循环等待:此处用while等待是"轮询方式",适合单字节发送场景,简单且可靠(新手优先掌握)。
5.2 声明发送函数(头文件)
在bsp_usart.h中添加函数声明:
void USART_SendByte(USART_TypeDef* USARTx, uint8_t data);
5.3 主函数调用示例(main.c)
在main.c中调用初始化和发送函数,实现单片机向上位机发送数据:
#include "stm32f10x.h"
#include "bsp_usart.h"
int main(void)
{
USART_Config();
USART_SendByte(DEBUG_USART, 'a');
// USART_SendByte(DEBUG_USART, 100);
while(1)
{
}
}
5.4 上位机验证步骤(手把手)
步骤1:硬件连接
-
保持开发板USART1跳帽默认连接;
-
用USB数据线连接开发板"USB to UART"接口与电脑;
-
给开发板上电(电源灯亮起)。
步骤2:确认串口驱动
-
打开电脑"设备管理器"→ 展开"端口(COM和LPT)";
-
查看是否有"USB-SERIAL CH340 (COMx)"(x是数字,如COM3);
-
若无该设备:重新安装CH340驱动,或换USB线/USB接口。
步骤3:串口调试助手配置
-
打开官方串口调试助手;
-
端口选择:设备管理器中看到的COMx;
-
参数配置:波特率115200、数据位8、停止位1、校验位无、流控无;
-
点击"打开串口"(按钮变为"关闭串口"表示成功)。
步骤4:烧录程序并验证
-
在Keil中编译工程(无报错)→ 烧录程序到开发板;
-
按下开发板"复位键";
-
串口调试助手"接收区"会显示字符a;
-
若测试发送十进制100:勾选调试助手"十六进制显示",接收区会显示0x64(100的十六进制)。
5.5 常见问题排查(新手必看)
问题现象 排查方向
上位机无任何数据接收 1. 串口是否打开;2. COM口是否选对;3. 跳帽是否连接USART1;4. 串口参数是否匹配;5. 程序是否烧录成功
接收数据为乱码 1. 波特率不匹配;2. 数据位/停止位/校验位不匹配;3. 开发板时钟配置错误(新手暂不考虑)
发送十进制数显示乱码 串口调试助手默认"字符显示",十进制100对应ASCII字符'd',勾选"十六进制显示"即可看原始值
六、本节核心小结
-
串口通信的前提是"电平匹配":USB→TTL需CH340转换,RS232→TTL需MAX3232转换;
-
串口初始化三大核心:GPIO引脚(TX=复用推挽、RX=浮空输入)、NVIC中断、串口模式(通信参数+时钟+使能);
-
宏定义封装硬件参数是嵌入式开发的最佳实践,可大幅提升代码可移植性;
-
单字节发送需等待TXE标志位,确保数据发送完成后再写入下一字节;
-
单片机与上位机的通信参数必须完全一致,否则会出现通信失败/乱码。
七、思考与拓展(新手进阶)
-
尝试修改宏定义,将USART1改为USART2,重新配置GPIO和时钟,实现USART2的单字节发送;
-
为什么TX引脚必须用"复用推挽输出",而不能用普通推挽输出(GPIO_Mode_Out_PP)?(提示:普通输出是GPIO控制,复用输出是串口外设控制);
-
若想发送多个字节(如"Hello STM32"),如何基于USART_SendByte封装一个字符串发送函数?
八、后续内容预告
下一节我们将讲解:
-
基于单字节发送函数,封装串口字符串/数组发送函数;
-
实现C语言标准库printf函数的串口重定向(直接用printf打印数据到上位机);
-
编写串口接收中断服务函数,实现上位机数据回传;
-
调试中断收发的常见问题与解决方法。
参考资料:《零死角玩转STM32》"USART串口通信"章节、STM32F10x数据手册