STM32F103 学习笔记-21-串口通信(第4节)—串口发送和接收代码讲解(中)

前言

各位零基础的STM32新手小伙伴,本节我们正式进入USART串口通信的实战编程阶段。在前序章节了解串口基本概念后,本节核心目标是:

  1. 理解STM32F103串口通信的硬件底层逻辑;

  2. 手把手完成串口驱动工程的基础搭建;

  3. 掌握USART1的完整初始化配置(GPIO+NVIC+串口模式);

  4. 编写并验证串口单字节发送函数;

  5. 理解代码中嵌入式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中导入文件

  1. 打开Keil MDK工程,在左侧"Project"窗口右键目标分组(如"User")→ 选择"Add Files to Group...";

  2. 找到新建的bsp_usart.c,选中并添加(bsp_usart.h无需添加,仅需配置路径)。

3.3 配置头文件路径(关键!新手易漏)

Keil编译器需要知道bsp_usart.h的位置,否则会报"头文件未找到"错误:

  1. 点击Keil顶部"魔术棒"图标(⚙️)→ 选择"C/C++"选项卡;

  2. 在"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);
}

核心知识点解析:

  1. static关键字:该函数仅能在bsp_usart.c中调用,其他文件无法访问,避免多个驱动文件的函数名冲突(嵌入式开发常用);

  2. GPIO_InitTypeDef:STM32标准库定义的GPIO初始化结构体,把引脚的"引脚号、模式、速率"封装成结构体,通过GPIO_Init函数一次性配置,比直接操作寄存器更简单;

  3. 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);
}

核心知识点解析:

  1. NVIC:嵌套向量中断控制器(Nested Vectored Interrupt Controller),是Cortex-M3内核的核心组件,用于管理所有中断的优先级和使能;

  2. 中断优先级分组:NVIC_PriorityGroup_2表示"2位抢占优先级(0~3)+2位子优先级(0~3)",整个工程只能配置一次(若多次配置,以最后一次为准);

  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);
}

核心知识点解析:

  1. 串口时钟:USART1挂载APB2总线 → 用RCC_APB2PeriphClockCmd;USART2~UART5挂载APB1 → 用RCC_APB1PeriphClockCmd(新手易踩坑:时钟总线错配会导致串口完全无响应);

  2. 通信参数:波特率、数据位、停止位、校验位必须与上位机串口调试助手完全一致,否则会出现"乱码"或"通信失败";

  3. USART_IT_RXNE:接收数据寄存器非空中断 → 当单片机收到1个字节数据时,会立即触发中断,后续可在中断服务函数中处理数据(本节先使能,下节讲解中断服务函数);

  4. 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 初始化关键注意事项(新手避坑清单)

  1. 时钟总线匹配:USART1→APB2,USART2~UART5→APB1,时钟使能函数必须对应;

  2. GPIO模式正确:TX=复用推挽输出,RX=浮空输入(错配会导致发送/接收失败);

  3. 通信参数一致:单片机与上位机的波特率、数据位等必须完全相同;

  4. 中断分组唯一:整个工程仅需调用一次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);
}

核心知识点解析:

  1. USART_TypeDef*:指针参数,指向STM32串口外设的寄存器结构体(如USART1、USART2),因此该函数可适配所有串口,通用性强;

  2. uint8_t:是unsigned char的别名(在stdint.h中定义),嵌入式开发中优先用uint8_t/uint16_t等固定长度类型,避免不同编译器的"char"长度差异;

  3. 等待发送完成:USART_FLAG_TXE是"发送数据寄存器空"标志位------当该位为RESET(0)时,说明上一个字节还在发送中;为SET(1)时,说明数据已从DR寄存器转移到发送移位寄存器,可写入下一个字节(避免数据覆盖丢失);

  4. 死循环等待:此处用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',勾选"十六进制显示"即可看原始值


六、本节核心小结

  1. 串口通信的前提是"电平匹配":USB→TTL需CH340转换,RS232→TTL需MAX3232转换;

  2. 串口初始化三大核心:GPIO引脚(TX=复用推挽、RX=浮空输入)、NVIC中断、串口模式(通信参数+时钟+使能);

  3. 宏定义封装硬件参数是嵌入式开发的最佳实践,可大幅提升代码可移植性;

  4. 单字节发送需等待TXE标志位,确保数据发送完成后再写入下一字节;

  5. 单片机与上位机的通信参数必须完全一致,否则会出现通信失败/乱码。


七、思考与拓展(新手进阶)

  1. 尝试修改宏定义,将USART1改为USART2,重新配置GPIO和时钟,实现USART2的单字节发送;

  2. 为什么TX引脚必须用"复用推挽输出",而不能用普通推挽输出(GPIO_Mode_Out_PP)?(提示:普通输出是GPIO控制,复用输出是串口外设控制);

  3. 若想发送多个字节(如"Hello STM32"),如何基于USART_SendByte封装一个字符串发送函数?


八、后续内容预告

下一节我们将讲解:

  1. 基于单字节发送函数,封装串口字符串/数组发送函数;

  2. 实现C语言标准库printf函数的串口重定向(直接用printf打印数据到上位机);

  3. 编写串口接收中断服务函数,实现上位机数据回传;

  4. 调试中断收发的常见问题与解决方法。


参考资料:《零死角玩转STM32》"USART串口通信"章节、STM32F10x数据手册

相关推荐
雾岛听蓝2 小时前
Qt操作指南:窗口组成与菜单栏
开发语言·经验分享·笔记·qt
EnglishJun3 小时前
ARM嵌入式学习(二十三)--- I2C总线和SPI总线
arm开发·学习
饭后一颗花生米3 小时前
2026 AI加持下前端学习路线:从入门到进阶,高效突破核心竞争力
前端·人工智能·学习
北山有鸟3 小时前
【学习笔记】MIPI CSI-2 协议全解析:从底层封包到像素解析
linux·驱动开发·笔记·学习·相机
就叫飞六吧4 小时前
企微组织架构同步到本地
笔记·企业微信
项目題供诗4 小时前
STM32-LED闪烁&LED流水灯&蜂鸣器(四)
stm32·单片机·嵌入式硬件
YCY^v^5 小时前
PSW、PFW、SPSW、SPFW 是信捷TouchWin
学习
Engineer邓祥浩5 小时前
JVM学习笔记(13) 第五部分 高效并发 第12章 Java内存模型与线程
jvm·笔记·学习
我命由我123455 小时前
程序员的心理学学习笔记 - 反刍思维
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法