当前位置: 首页 > news >正文

STM32F103 学习笔记-21-串口通信(第5节)—串口2345代码移植和讲解

本章基于 STM32F103 标准外设库开发,从最基础的“串口是什么”讲起,通过生活化类比拆解硬件原理,配合逐行注释的可运行代码,让零基础读者读完就能实现单片机与电脑的双向通信。


一、什么是串口通信?

1.1 串口的本质:单片机的“电话”

串口(USART)是单片机和电脑之间最常用的通信方式,你可以把它想象成两个人打电话

  • TX(发送端):你的嘴巴,负责说话

  • RX(接收端):你的耳朵,负责听话

  • 全双工通信:两个人可以同时说话和听话(串口最常用的模式)

  • 半双工通信:同一时间只能一个人说,另一个人听

  • 单工通信:只能一个人说,另一个人只能听(比如广播)

1.2 串口通信的“约定”:通信参数

就像两个人打电话必须说同一种语言、语速差不多才能听懂一样,串口通信也需要双方提前约定好参数,否则会出现“鸡同鸭讲”的乱码。

参数

含义

最常用值

类比

波特率

每秒传输的二进制位数

115200

说话的速度,单位:比特/秒

数据位

每次传输的数据长度

8 位

每次说 8 个字

停止位

数据传输结束的标志

1 位

说完一句话停顿一下

校验位

检查数据传输是否出错

无校验

说完后确认对方有没有听清

注意:电脑和单片机的这四个参数必须完全一致,否则一定会出现乱码。


二、STM32 串口硬件原理

2.1 电脑和单片机之间的“翻译官”:CH340G

电脑只有 USB 接口,没有串口接口,而单片机只有串口接口,没有 USB 接口。所以需要一个翻译官把 USB 信号和串口信号互相转换,这个翻译官就是 CH340G 芯片。

电脑 USB 接口 ↔ CH340G 芯片 ↔ STM32 单片机串口 USB 信号 串口信号 串口信号

CH340G 的硬件连接非常简单:

  • CH340G 的 TX 引脚接单片机的 RX 引脚

  • CH340G 的 RX 引脚接单片机的 TX 引脚

  • 两者的 GND 引脚必须连接在一起(共地)

注意:TX 和 RX 必须交叉连接!这是新手最容易犯的错误。如果接反了,串口会完全没有反应。

2.2 STM32 串口内部结构

STM32F103 一共有 5 个串口:USART1、USART2、USART3、UART4、UART5。它们的内部结构基本相同,主要由以下几个部分组成:

┌─────────────────────────────────────┐ │ │ CPU ─────►│ 数据寄存器(DR) ◄──► 移位寄存器 │ ◄──► TX引脚 │ (一个地址对应两个寄存器) │ │ 发送寄存器(TDR) 接收寄存器(RDR) │ ◄──► RX引脚 │ │ ├─────────────────────────────────────┤ │ │ │ 波特率发生器(BRR) ◄─── 控制器 │ │ │ └─────────────────────────────────────┘
  • 数据寄存器(DR):相当于一个邮箱。发送数据时,CPU 把数据放进发送邮箱(TDR);接收数据时,CPU 从接收邮箱(RDR)里取数据。

  • 移位寄存器:把并行数据转换成串行数据发送出去,或者把接收到的串行数据转换成并行数据。

  • 波特率发生器:根据配置的波特率,产生串口通信需要的时钟信号。

  • 控制器:控制串口的工作模式、中断等。


三、串口初始化的核心步骤

串口初始化就像给手机设置通话功能,需要一步步完成以下操作:

  1. 给手机充电:使能 GPIO 和串口的时钟

  2. 设置手机模式:配置 TX 和 RX 引脚的工作模式

  3. 设置通话参数:配置波特率、数据位、停止位、校验位

  4. 开启来电提醒:配置接收中断(可选)

  5. 开机:使能串口

3.1 为什么要使能时钟?

STM32 的所有外设默认都是关闭的,这样可以节省功耗。就像你不用手机的时候会关机一样,使用外设之前必须先打开它的电源(时钟)。

  • USART1 挂载在APB2 总线上,时钟频率 72MHz

  • USART2、USART3、UART4、UART5 挂载在APB1 总线上,时钟频率 36MHz

注意:这是串口初始化最容易出错的地方!如果 USART2 用了 APB2 的时钟,串口会完全无法工作。

3.2 GPIO 引脚模式配置

  • TX 引脚:配置为复用推挽输出。因为 TX 需要主动输出高低电平,推挽输出可以提供足够的电流。

  • RX 引脚:配置为浮空输入。因为 RX 需要接收外部的信号,浮空输入可以准确检测外部的高低电平。


四、完整代码实现

4.1 头文件设计(bsp_usart.h)

我们把所有硬件相关的参数都用宏定义封装起来,这样以后切换串口时只需要修改宏定义即可。

#ifndef __BSP_USART_H #define __BSP_USART_H #include "stm32f10x.h" #include <stdio.h> #define DEBUG_USART1 0 #define DEBUG_USART2 0 #define DEBUG_USART3 0 #define DEBUG_USART4 0 #define DEBUG_USART5 1 #if DEBUG_USART1 // 串口1-USART1 #define DEBUG_USARTx USART1 #define DEBUG_USART_CLK RCC_APB2Periph_USART1 #define DEBUG_USART_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // 串口1 USART GPIO 引脚宏定义 #define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOA) #define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_TX_GPIO_PORT GPIOA #define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_9 #define DEBUG_USART_RX_GPIO_PORT GPIOA #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_10 #define DEBUG_USART_IRQ USART1_IRQn #define DEBUG_USART_IRQHandler USART1_IRQHandler #elif DEBUG_USART2 //串口2-USART2 #define DEBUG_USARTx USART2 #define DEBUG_USART_CLK RCC_APB1Periph_USART2 #define DEBUG_USART_APBxClkCmd RCC_APB1PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // 串口2 USART GPIO 引脚宏定义 #define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOA) #define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_TX_GPIO_PORT GPIOA #define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_2 #define DEBUG_USART_RX_GPIO_PORT GPIOA #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_3 #define DEBUG_USART_IRQ USART2_IRQn #define DEBUG_USART_IRQHandler USART2_IRQHandler #elif DEBUG_USART3 //串口3-USART3 #define DEBUG_USARTx USART3 #define DEBUG_USART_CLK RCC_APB1Periph_USART3 #define DEBUG_USART_APBxClkCmd RCC_APB1PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // 串口3 USART GPIO 引脚宏定义 #define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOB) #define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_TX_GPIO_PORT GPIOB #define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_10 #define DEBUG_USART_RX_GPIO_PORT GPIOB #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_11 #define DEBUG_USART_IRQ USART3_IRQn #define DEBUG_USART_IRQHandler USART3_IRQHandler #elif DEBUG_USART4 //串口4-UART4 #define DEBUG_USARTx UART4 #define DEBUG_USART_CLK RCC_APB1Periph_UART4 #define DEBUG_USART_APBxClkCmd RCC_APB1PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // 串口4 USART GPIO 引脚宏定义 #define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOC) #define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_TX_GPIO_PORT GPIOC #define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_10 #define DEBUG_USART_RX_GPIO_PORT GPIOC #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_11 #define DEBUG_USART_IRQ UART4_IRQn #define DEBUG_USART_IRQHandler UART4_IRQHandler #elif DEBUG_USART5 //串口5-UART5 #define DEBUG_USARTx UART5 #define DEBUG_USART_CLK RCC_APB1Periph_UART5 #define DEBUG_USART_APBxClkCmd RCC_APB1PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // 串口5 USART GPIO 引脚宏定义 #define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD) #define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_TX_GPIO_PORT GPIOC #define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_12 #define DEBUG_USART_RX_GPIO_PORT GPIOD #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_2 #define DEBUG_USART_IRQ UART5_IRQn #define DEBUG_USART_IRQHandler UART5_IRQHandler #endif void USART_Config(void); void Usart_SendByte(USART_TypeDef* pUSARTx,uint8_t data); void Usart_sendHalfWord(USART_TypeDef * pUSARTx, uint16_t data); void Usart_SendArray(USART_TypeDef * pUSARTx, uint8_t *array, uint8_t size); void Usart_SendString(USART_TypeDef * pUSARTx, char *str); #endif /* __BSP_USART_H */

4.2 源文件实现(bsp_usart.c)

// bsp_usart.c #include "bsp_usart.h" /** * @brief 配置嵌套向量中断控制器 NVIC * @param 无 * @retval 无 */ static void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; /* 配置中断优先级分组为 2 */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); /* 配置 USART 为中断源 */ NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQn; /* 抢断优先级为 1 */ NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; /* 子优先级为 1 */ NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; /* 使能中断 */ NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; /* 初始化 NVIC */ NVIC_Init(&NVIC_InitStructure); } /** * @brief USART 初始化函数 * @param 无 * @retval 无 */ void USART_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 1. 使能串口时钟 DEBUG_USART_APBx(DEBUG_USART_CLK, ENABLE); // 2. 使能 GPIO 时钟 RCC_APB2PeriphClockCmd(DEBUG_USART_GPIO_CLK, ENABLE); // 3. 配置 TX 引脚为复用推挽输出 GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 引脚速度 50MHz GPIO_Init(DEBUG_USART_GPIO, &GPIO_InitStructure); // 4. 配置 RX 引脚为浮空输入 GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(DEBUG_USART_GPIO, &GPIO_InitStructure); // 5. 配置串口参数 USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE; // 波特率 115200 USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 数据位 8 位 USART_InitStructure.USART_StopBits = USART_StopBits_1; // 停止位 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_USARTx, &USART_InitStructure); // 6. 配置串口接收中断 USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE); // 7. 配置 NVIC 中断优先级 NVIC_Configuration(); // 8. 使能串口 USART_Cmd(DEBUG_USARTx, ENABLE); } /** * @brief 发送单个字节 * @param pUSARTx: 串口外设 * @param ch: 待发送字节 * @retval 无 */ void Usart_SendByte(USART_TypeDef * pUSARTx, uint8_t ch) { // 把数据写入发送数据寄存器 USART_SendData(pUSARTx, ch); // 等待发送数据寄存器为空(TXE 标志位置 1) while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); } /** * @brief 发送字符串 * @param pUSARTx: 串口外设 * @param str: 待发送字符串 * @retval 无 */ void Usart_SendString(USART_TypeDef * pUSARTx, char *str) { unsigned int k = 0; do { // 逐个发送字符串中的字符 Usart_SendByte(pUSARTx, *(str + k)); k++; } while (*(str + k) != '\0'); // 直到遇到字符串结束符'\0' // 等待所有数据发送完成(TC 标志位置 1) while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) == RESET); } /** * @brief 重定向 C 库函数 printf 到串口 * @param ch: 待发送字符 * @param f: 文件指针 * @retval 发送的字符 */ int fputc(int ch, FILE *f) { Usart_SendByte(DEBUG_USARTx, (uint8_t)ch); return ch; } /** * @brief 串口接收中断服务函数 * @param 无 * @retval 无 */ void DEBUG_USART_IRQHandler(void) { uint8_t ucTemp; // 检查是否是接收中断(RXNE 标志位置 1) if (USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE) != RESET) { // 读取接收到的数据 ucTemp = USART_ReceiveData(DEBUG_USARTx); // 把接收到的数据回显给电脑 Usart_SendByte(DEBUG_USARTx, ucTemp); } }

4.3 主函数(main.c)

// main.c #include "stm32f10x.h" #include "bsp_usart.h" int main(void) { // 初始化串口 USART_Config(); // 发送欢迎信息 Usart_SendString(DEBUG_USARTx, "这是一个串口回显实验\r\n"); Usart_SendString(DEBUG_USARTx, "你发送什么,单片机就会返回什么\r\n"); // 也可以使用 printf 发送 printf("printf 重定向成功!\r\n"); while(1) { // 主循环什么都不做,所有工作都在中断中完成 } }

五、printf 重定向详解

5.1 什么是 printf 重定向?

printf 是 C 语言标准库中的函数,默认是把数据输出到电脑的屏幕上。在单片机中,我们没有屏幕,所以需要把 printf 的输出重定向到串口,这样就可以用 printf 方便地打印调试信息了。

5.2 重定向的原理

printf 函数内部会调用 fputc 函数来输出单个字符。我们只需要重新实现 fputc 函数,让它把字符发送到串口,printf 就会自动把所有数据输出到串口了。

5.3 必须注意的事项

在 Keil MDK 中使用 printf 重定向,必须勾选“Use MicroLIB”选项

  1. 点击 Keil 工具栏的“魔术棒”按钮

  2. 切换到“Target”选项卡

  3. 勾选“Use MicroLIB”

  4. 点击“OK”保存设置

注意:如果不勾选这个选项,程序会编译失败或者运行时卡死。


六、串口接收的两种方式

6.1 查询方式(不推荐)

查询方式就是 CPU 一直不停地检查接收数据寄存器是否有数据。就像你一直盯着门口等快递,什么事都干不了。

// 查询方式接收数据 uint8_t Usart_ReceiveByte(USART_TypeDef * pUSARTx) { // 等待接收数据寄存器非空(RXNE 标志位置 1) while (USART_GetFlagStatus(pUSARTx, USART_FLAG_RXNE) == RESET); // 返回接收到的数据 return USART_ReceiveData(pUSARTx); }

6.2 中断方式(推荐)

中断方式就是 CPU 不用一直等,当有数据到来时,串口会给 CPU 发一个中断信号,CPU 再去处理数据。就像快递员给你打电话,你再去门口取快递,平时你可以干别的事。

这就是我们在代码中使用的方式,它可以大大节省 CPU 资源。


七、实验验证步骤

7.1 软件准备

  1. 安装 CH340G 驱动程序(如果电脑没有自动安装)

  2. 下载并打开串口助手(推荐使用野火串口助手)

7.2 硬件连接

  1. 用 USB 线连接开发板的“USB TO UART”接口和电脑

  2. 确保开发板上 USART1 的跳帽已经插上

7.3 串口助手设置

  1. 选择正确的串口号(可以在设备管理器中查看)

  2. 设置波特率为 115200

  3. 设置数据位为 8,停止位为 1,校验位为无

  4. 点击“打开串口”按钮

7.4 下载程序并测试

  1. 编译程序并下载到开发板

  2. 此时串口助手应该会收到欢迎信息

  3. 在串口助手的发送框中输入任意字符,点击“发送”

  4. 串口助手的接收框中会显示你发送的字符


八、常见问题与避坑指南

8.1 串口完全没有输出

可能原因及解决方法:

  1. 跳帽未插:检查开发板上 USART1 的跳帽是否已经插上

  2. TX 和 RX 接反:确认 CH340G 的 TX 接单片机的 RX,RX 接单片机的 TX

  3. 时钟总线配置错误:USART1 在 APB2 总线,其他串口在 APB1 总线

  4. 串口助手未打开:确认串口助手已经打开了正确的串口号

  5. 程序未下载:确认程序已经成功下载到开发板

8.2 串口输出乱码

可能原因及解决方法:

  1. 参数不匹配:确认串口助手和代码中的波特率、数据位、停止位、校验位完全一致

  2. 系统时钟配置错误:如果系统时钟不是 72MHz,串口波特率会不准

  3. CH340G 驱动问题:重新安装 CH340G 驱动程序

8.3 只能发送不能接收

可能原因及解决方法:

  1. RX 引脚配置错误:确认 RX 引脚配置为浮空输入

  2. 接收中断未使能:确认调用了 USART_ITConfig 函数使能了接收中断

  3. 中断服务函数名错误:确认中断服务函数名和头文件中定义的一致


九、小结

  1. 串口是单片机和电脑之间最常用的通信方式,本质是全双工的串行通信

  2. 串口通信的四个参数(波特率、数据位、停止位、校验位)必须完全一致

  3. CH340G 是 USB 转串口芯片,负责电脑和单片机之间的信号转换

  4. 串口初始化的核心步骤:使能时钟→配置 GPIO→配置串口参数→配置中断→使能串口

  5. 推荐使用中断方式接收数据,可以大大节省 CPU 资源

  6. 使用 printf 重定向需要勾选 Keil 中的“Use MicroLIB”选项


参考出处

  1. 《零死角玩转 STM32F103-指南者》第 21 章 USART 串口通信

  2. STM32F103 官方参考手册 RM0008

  3. 野火 STM32 串口通信教学视频

http://www.jsqmd.com/news/786368/

相关文章:

  • CANN/ops-rand API 实现状态
  • React聊天机器人组件集成指南:从UI定制到AI后端连接
  • 从特征工程到深度学习:AI视网膜疾病诊断的技术演进与工程实践
  • 脑机接口与LLM融合:EEGChat项目实现脑电信号到文本的意图解码
  • 【C++】stackqueuedequepriority_queue深度剖析
  • Codex Mac 安装报错解决教程(应用程序“Codex“无法打开)
  • 第一行代码--初步学习--UI开发--ListView
  • 自动化立体仓库系统项目施工要点
  • Win系统实现网络转发与端口映射:从 IPEnableRouter 到 RRAS 完整步骤
  • 如何快速掌握Blender插件io_scene_psk_psa:虚幻引擎PSK/PSA格式完整指南
  • 数据泄露已成网络安全新热点!成因、危害、溯源防御全方位深度解析
  • 从黑盒模型到因果反事实解释:构建可解释AI的实践路径
  • AI定价算法中的市场分配与合谋机制解析
  • Vatee外汇合规资质值得信赖吗?监管框架完善吗?
  • 基于大语言模型的互动游戏:提示词工程与AI游戏引擎设计
  • CANN/catlass GEMM恒等块调度
  • 2026年Q2北京铝镁锰板实力厂家盘点:廊坊铝硕金属制品有限公司深度解析 - 2026年企业推荐榜
  • JavaScript while 循环详解
  • Chainlit:快速构建AI应用界面的Python框架,无缝集成LangChain与OpenAI
  • 基于粒子群优化算法的微电网调度(光伏、储能、电动车、电网交互)(Matlab代码实现)
  • 线上推广公司怎么选?2026五家主流服务商全景评测与商家决策手册 - GEO优化
  • 2026 国内大模型 API 中转选型笔记:从接入成本到长期维护的几个观察
  • Bean 什么时候会被销毁?
  • 如何创建一个 Springboot Starter
  • OpenClaw 用户如何快速配置 Taotoken 聚合端点实现多模型调用
  • 【2026最新版|收藏备用】用Skill简化大模型知识库连接,小白程序员入门必看
  • Dify工作流实战:构建HR与网络安全AI应用脚本库
  • 09-扩展知识——05. date 类 - 处理日期
  • 基于Kubernetes的AI应用控制平面:kiro-acp架构解析与实践指南
  • Bean 会被 JVM 回收吗?