作为电子工程师初学者,串口通信是我们接触硬件交互的 “第一道门槛”—— 小到单片机与电脑传数据,大到传感器与控制器交互,都离不开串口。本文基于 STM32F103C8T6,从 “为什么用串口” 到 “亲手写代码收发数据”,用大白话 + 实操细节,帮你彻底搞懂 USART 串口协议,附带流程图和接线图,跟着做就能上手!
一、先搞懂:通信接口的 “底层逻辑”
我们做电子开发,本质是让 “两个设备说话”—— 比如单片机告诉电脑 “当前温度 25℃”,或电脑命令单片机 “点亮 LED”。要实现对话,就得先明确 “说什么”(数据)和 “怎么说”(协议)。
1.1 为什么选 USART 串口?先看通信接口对比
市面上有 USART、I2C、SPI、CAN 等多种接口,初学者不用全学,先搞懂它们的核心区别,才知道什么时候该用串口:
| 接口类型 | 关键引脚 | 通信方向(双工) | 时钟方式 | 电平类型 | 适用场景 | 设备数量 |
|---|---|---|---|---|---|---|
| USART | TX(发)、RX(收) | 全双工(同时互传) | 异步(无时钟线) | 单端(需共地) | 单片机 - 电脑、单片机 - 模块(如蓝牙) | 点对点(2 个设备) |
| I2C | SCL(时钟)、SDA(数据) | 半双工(轮流传) | 同步(需时钟) | 单端 | 多传感器(如温湿度、陀螺仪) | 多设备(1 主多从) |
| SPI | SCLK(时钟)、MOSI(主发从收)、MISO(主收从发)、CS(片选) | 全双工 | 同步 | 单端 | 高速设备(如显示屏、Flash) | 多设备(1 主多从) |
| CAN | CAN_H、CAN_L(差分) | 半双工 | 异步 | 差分(抗干扰强) | 汽车、工业控制(长距离) | 多设备 |
初学者重点记:USART 串口的优势是 “简单、低成本”—— 只需 2 根线(TX/RX),就能实现两个设备互传数据,特别适合 “短距离、低速” 的场景(比如单片机和电脑调试),是入门首选。
1.2 USART 串口的 “硬件基础”:接线 + 电平
要让两个设备通过 USART 通信,硬件上必须满足两个核心条件:正确接线和匹配电平。
1.2.1 接线图:TX 和 RX 必须 “交叉”
USART 通信至少需要 2 根数据线(TX 发送、RX 接收)和 1 根地线(GND 共地),接线原则是 “发送接接收,接收接发送”,具体如下:
graph LR
A[设备1(如STM32)] -->|TX(PA9)| B[设备2(如USB转串口模块)的RX]
A -->|RX(PA10)| B[设备2的TX]
A -->|GND| B[设备2的GND]
note over A,B: 关键:TX和RX交叉连接,GND必须共地!
-
单向通信(如只需要单片机发数据给电脑):只需接 TX(设备 1)→ RX(设备 2)和 GND,RX 可以不接;
-
双向通信(如电脑发命令、单片机回数据):必须接 TX-RX 交叉 + GND;
-
供电:两个设备各自独立供电(如 STM32 接 3.3V,USB 转串口接 5V),不用互相接 VCC。
1.2.2 电平标准:别让 “电压语言” 不互通
电平是设备 “表达 0 和 1 的方式”,不同设备可能用不同电平,直接接会烧芯片!串口常用 3 种电平:
| 电平标准 | 1 的表示 | 0 的表示 | 特点 | 常见设备 |
|---|---|---|---|---|
| TTL 电平 | +3.3V/+5V | 0V | 单片机内部常用 | STM32、51 单片机 |
| RS232 电平 | -3~-15V | +3~+15V | 电脑串口(老式)常用 | 台式机 DB9 接口 |
| RS485 电平 | 两线压差 + 2~+6V | 两线压差 - 2~-6V | 差分信号(抗干扰强) | 工业传感器、长距离设备 |
初学者必踩坑:电脑 USB 口是 USB 电平,STM32 是 TTL 电平,直接接会烧芯片!必须用 “USB 转串口模块”(如 CH340 模块)做电平转换,模块一侧接电脑 USB(USB 电平),另一侧接 STM32 的 TX/RX(TTL 电平)。
二、USART 串口协议:数据 “怎么发、怎么收”?
搞懂硬件后,还要明确 “数据的格式”—— 就像说话要 “一句一句说”,串口数据也要按固定格式传输,不然设备会 “听糊涂”。
2.1 串口数据帧:“一句话” 的结构
USART 串口传输数据时,会把 1 个字节(如 0x55、字符 ‘A’)包装成 “一帧”,就像说话的 “一句话”,结构如下(以 “8 位数据 + 1 位停止位 + 无校验” 为例):
graph TD
A[空闲状态:高电平] --> B[起始位:1位低电平(标志开始)]
B --> C[数据位:8位(低位在前,如0x55是01010101,先传最低位1)]
C --> D[停止位:1位高电平(标志结束)]
D --> A[回到空闲状态,等下一帧]
note over B,C,D: 一帧总长度=10位(起始1+数据8+停止1)
-
起始位:必须是低电平!用来区分 “空闲” 和 “开始传数据”—— 空闲时数据线是高电平,一旦变低,设备就知道 “要开始收数据了”;
-
数据位:可以是 8 位或 9 位(初学者选 8 位足够),按 “低位在前” 传输(比如数据 0x41 是 ‘A’,二进制 01000001,先传最低位 1,再传次低位 0…… 最后传最高位 0);
-
校验位(可选):用来检查数据是否传错,初学者可以先不用(选 “无校验”),后期做可靠通信再加(奇校验 / 偶校验);
-
停止位:必须是高电平!用来告诉设备 “这一帧结束了”,防止和下一帧混淆,可选 0.5/1/1.5/2 位(初学者选 1 位即可)。
2.2 关键参数:决定 “说话速度” 和 “清晰度”
除了帧结构,还要约定两个关键参数,不然设备会 “听不懂”:
- 波特率:“说话速度”,即每秒传输的 “位数”(bit),常见值 9600bps、38400bps、115200bps。
-
例:9600bps 表示每秒传 9600 位,1 帧 10 位的话,每秒能传 960 帧(约 960 字节);
-
注意:通信双方必须用相同波特率!比如 STM32 设 9600,电脑串口助手也要设 9600,不然数据会乱码。
- 数据模式:数据的 “显示形式”,分两种:
-
HEX 模式(十六进制):传原始数据(如 0x55、0xA0),适合传感器数据(如温湿度、陀螺仪);
-
文本模式(字符):传 ASCII 码对应的字符(如 0x41 对应 ‘A’、0x31 对应 ‘1’),适合命令交互(如电脑发 “LED_ON” 控制单片机)。
2.3 数据采样:设备怎么 “准确听清楚”?
串口是 “异步通信”(没有时钟线同步),设备怎么保证 “在正确的时间读数据”?靠 “波特率时钟采样”。
以 115200bps、50MHz 系统时钟为例:
-
波特率对应的 “位周期”= 系统时钟 / 波特率 = 50,000,000 / 115200 ≈ 434 个时钟周期(即 1 位数据持续 434 个时钟);
-
设备会在 “每位数据的中间时刻” 采样(如第 217 个时钟周期),这样能避开边缘干扰,保证数据准确;
-
起始位的采样更严格:会连续采样多次(如 3 次),至少 2 次是低电平,才确认是 “真起始位”,防止噪声干扰。
timeline title 1位数据的采样过程(115200bps,50MHz时钟) section 位周期(434个时钟) 0~108时钟 : 边缘区域(不采样,防干扰) 109~325时钟 : 中间区域(采样点:217时钟,读数据) 326~434时钟 : 边缘区域(不采样)
三、STM32 USART 硬件解析:从框图到引脚
学完协议,再看 STM32 的 USART 外设 ——STM32F103C8T6 有 3 个 USART(USART1、USART2、USART3),我们重点用 USART1 做实战。
3.1 USART 核心框图:数据 “怎么在芯片里走”?
STM32 的 USART 不是 “一根线通到底”,而是有完整的 “发送 / 接收链路”,初学者不用深究细节,记住核心流程即可:
graph LR
A[发送端:CPU/内存] --> B[发送数据寄存器TDR]
B --> C[发送控制器+波特率发生器]
C --> D[发送移位寄存器(把并行数据转串行)]
D --> E[TX引脚(PA9)发出去]
F[RX引脚(PA10)收数据] --> G[接收移位寄存器(把串行转并行)]
G --> H[接收控制器+波特率发生器]
H --> I[接收数据寄存器RDR]
I --> J[接收端:CPU/内存]
note over C,H: 波特率发生器是核心,保证收发速度一致
-
发送时:CPU 把数据写入 TDR,硬件自动把并行数据转成串行(按帧结构),从 TX 发出去;
-
接收时:RX 收到串行数据,硬件自动转成并行,存入 RDR,CPU 再读 RDR 获取数据;
-
波特率发生器:由 STM32 的时钟(如 USART1 用 APB2 时钟 72MHz)分频得到,通过配置寄存器 BRR 设置波特率。
3.2 STM32F103C8T6 USART 引脚图(必背!)
USART 的引脚是 “固定复用” 的,初学者要记准常用引脚,接错了就没数据:
| USART 外设 | TX 引脚(发送) | RX 引脚(接收) | 挂载总线 | 常用场景 |
|---|---|---|---|---|
| USART1 | PA9 | PA10 | APB2(72MHz) | 和电脑通信(高速) |
| USART2 | PA2 | PA3 | APB1(36MHz) | 和模块通信(如蓝牙) |
| USART3 | PB10 | PB11 | APB1(36MHz) | 和模块通信 |
引脚实物接线图(以 USART1 为例,STM32→USB 转串口模块):
STM32F103C8T6最小系统板 USB转串口模块(CH340)
PA9(TX)--------------------RX
PA10(RX)-------------------TX
GND--------------------------GND
3.3V-------------------------3.3V(给STM32供电,模块接电脑USB取电)
注意:USB 转串口模块的 VCC 不要接 STM32 的 VCC!模块从电脑 USB 取 5V,STM32 用 3.3V,混接会烧芯片,两者独立供电,只共 GND 即可。
四、实战 1:串口发送数据(STM32→电脑)
学会原理后,先从 “简单的” 开始 —— 让 STM32 发数据给电脑,用串口助手查看,步骤分 “写代码” 和 “硬件调试”。
4.1 代码核心:初始化 + 发送函数
我们用 STM32 标准库编写,核心是 “初始化 USART 和 GPIO”+“写发送函数”,代码带详细注释,初学者能直接复制用。
4.1.1 头文件(Serial.h):声明函数
\#ifndef \_\_SERIAL\_H
\#define \_\_SERIAL\_H
\#include \<stdio.h> // 用于printf重定向
// 函数声明:初始化、发字节、发数组、发字符串、发数字、printf封装
void Serial\_Init(void); // USART初始化
void Serial\_SendByte(uint8\_t Byte); // 发1个字节
void Serial\_SendArray(uint8\_t \*Array, uint16\_t Length); // 发数组
void Serial\_SendString(char \*String); // 发字符串
void Serial\_SendNumber(uint32\_t Number, uint8\_t Length); // 发数字(字符形式)
void Serial\_Printf(char \*format, ...); // 封装printf(支持格式化输出)
\#endif
4.1.2 源文件(Serial.c):实现功能
\#include "stm32f10x.h"
\#include \<stdio.h>
\#include \<stdarg.h> // 用于printf可变参数
// USART1初始化:波特率9600,8位数据,1位停止,无校验
void Serial\_Init(void) {
  // 1. 开启时钟(USART1和GPIOA都在APB2总线上)
  RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_USART1, ENABLE);
  RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_GPIOA, ENABLE);
  // 2. 初始化GPIO(TX:PA9复用推挽输出,RX:PA10上拉输入)
  GPIO\_InitTypeDef GPIO\_InitStruct;
  // TX引脚(PA9):复用推挽输出(串口发送需要GPIO复用为USART功能)
  GPIO\_InitStruct.GPIO\_Pin = GPIO\_Pin\_9;
  GPIO\_InitStruct.GPIO\_Mode = GPIO\_Mode\_AF\_PP; // 复用推挽
  GPIO\_InitStruct.GPIO\_Speed = GPIO\_Speed\_50MHz;
  GPIO\_Init(GPIOA, \&GPIO\_InitStruct);
  // 3. 初始化USART1参数
  USART\_InitTypeDef USART\_InitStruct;
  USART\_InitStruct.USART\_BaudRate = 9600; // 波特率9600
  USART\_InitStruct.USART\_WordLength = USART\_WordLength\_8b; // 8位数据
  USART\_InitStruct.USART\_StopBits = USART\_StopBits\_1; // 1位停止
  USART\_InitStruct.USART\_Parity = USART\_Parity\_No; // 无校验
  USART\_InitStruct.USART\_HardwareFlowControl = USART\_HardwareFlowControl\_None; // 无硬件流控
  USART\_InitStruct.USART\_Mode = USART\_Mode\_Tx; // 只开启发送模式(先做发送)
  USART\_Init(USART1, \&USART\_InitStruct);
  // 4. 开启USART1
  USART\_Cmd(USART1, ENABLE);
}
// 发1个字节:等待发送完成再返回
void Serial\_SendByte(uint8\_t Byte) {
  USART\_SendData(USART1, Byte); // 把字节写入发送寄存器
  // 等待发送完成(TXE标志位:发送寄存器为空,表示数据已发出去)
  while (USART\_GetFlagStatus(USART1, USART\_FLAG\_TXE) == RESET);
}
// 发数组:逐个字节发送
void Serial\_SendArray(uint8\_t \*Array, uint16\_t Length) {
  for (uint16\_t i = 0; i < Length; i++) {
  Serial\_SendByte(Array\[i]);
  }
}
// 发字符串:以'\0'为结束符(如"Hello")
void Serial\_SendString(char \*String) {
  for (uint8\_t i = 0; String\[i] != '\0'; i++) {
  Serial\_SendByte(String\[i]);
  }
}
// 辅助函数:计算X的Y次方(用于发数字时拆分位数)
uint32\_t Serial\_Pow(uint32\_t X, uint32\_t Y) {
  uint32\_t Result = 1;
  while (Y--) {
  Result \*= X;
  }
  return Result;
}
// 发数字(字符形式):如123→'1''2''3'
void Serial\_SendNumber(uint32\_t Number, uint8\_t Length) {
  for (uint8\_t i = 0; i < Length; i++) {
  // 拆分每一位:如123,Length=3,i=0时取1(123/10^(3-0-1)%10=123/100%10=1)
  uint8\_t Digit = Number / Serial\_Pow(10, Length - i - 1) % 10;
  Serial\_SendByte(Digit + '0'); // 转成字符(如1→'1',加ASCII偏移0x30)
  }
}
// 重定向printf:让printf输出到串口(默认printf输出到屏幕,单片机没有屏幕)
int fputc(int ch, FILE \*f) {
  Serial\_SendByte(ch); // 把printf的字符通过串口发出去
  return ch;
}
// 封装printf:支持可变参数(如Serial\_Printf("温度:%d℃\r\n", 25))
void Serial\_Printf(char \*format, ...) {
  char String\[100]; // 缓存格式化后的字符串
  va\_list Arg; // 可变参数列表
  va\_start(Arg, format); // 从format开始解析参数
  vsprintf(String, format, Arg); // 把参数格式化成字符串
  va\_end(Arg); // 释放参数列表
  Serial\_SendString(String); // 发送字符串
}
4.1.3 主函数(main.c):测试发送
\#include "stm32f10x.h"
\#include "Serial.h" // 包含串口函数
\#include "Delay.h" // 延时函数(自己写或用库)
int main(void) {
  Serial\_Init(); // 初始化USART1(9600bps)
  // 测试发送:发字节、数组、字符串、数字、printf
  Serial\_SendByte('A'); // 发字符'A'(ASCII 0x41)
   
  uint8\_t MyArray\[] = {0x42, 0x43, 0x44}; // 发数组:B、C、D
  Serial\_SendArray(MyArray, 3);
   
  Serial\_SendString("\r\nHello STM32!\r\n"); // 发字符串(\r\n是换行)
   
  Serial\_SendNumber(123, 3); // 发数字123(字符形式)
   
  printf("\r\nprintf测试:%d℃\r\n", 25); // 重定向printf
   
  Serial\_Printf("封装printf测试:%s\r\n", "串口发送成功!"); // 封装printf
  while (1) {
  // 循环发送(可选)
  Serial\_Printf("循环发送:%d\r\n", \_\_HAL\_GetTick()/1000); // 每秒发一次时间
  Delay\_ms(1000); // 延时1秒
  }
}
4.2 硬件调试:接线 + 串口助手设置
代码写好后,要 “让硬件跑起来”,步骤如下:
4.2.1 接线图(实物)
| STM32F103C8T6 | USB 转串口模块(CH340) | 功能 |
|---|---|---|
| PA9(TX) | RX | STM32 发→模块收 |
| PA10(RX) | TX | STM32 收→模块发(本次暂不用) |
| GND | GND | 共地(必须接!) |
| 3.3V | 3.3V | 给 STM32 供电(模块接电脑 USB 取电) |
注意:如果 STM32 用其他方式供电(如 ST-Link),就不用接模块的 3.3V,只接 TX、RX、GND。
4.2.2 串口助手设置(以 “串口助手 V1.1” 为例)
-
安装 CH340 驱动:模块插电脑 USB,打开 “设备管理器”,查看 “端口(COM 和 LPT)”,确认有 “USB-SERIAL CH340(COMx)”(x 是串口号,如 COM3);
-
打开串口助手:
-
串口号:选设备管理器里的 COMx(如 COM3);
-
波特率:9600(和代码里一致);
-
数据位:8;
-
停止位:1;
-
校验位:无;
-
接收模式:文本模式(先看字符,后期再试 HEX 模式);
-
下载代码:用 ST-Link 把代码下载到 STM32;
-
打开串口:点击 “打开串口”,就能看到 STM32 发送的数据,如下:
ABCHello STM32!
123
printf测试:25℃
封装printf测试:串口发送成功!
循环发送:0
循环发送:1
循环发送:2
4.3 常见问题:初学者必踩的坑
- 串口助手没数据:
-
检查接线:TX 和 RX 是否交叉?GND 是否接了?
-
检查波特率:代码和串口助手是否一致(如都是 9600)?
-
检查 USART 初始化:TX 引脚是否设为 “复用推挽输出”(不是普通推挽!)?
- 数据乱码:
-
波特率不匹配:比如代码设 115200,助手设 9600;
-
电平不匹配:直接用 STM32 的 TTL 接电脑 RS232(需加电平转换芯片,如 MAX232);
- printf 没输出:
-
勾选 “Use MicroLIB”:Keil 里点击 “Options for Target”→“Target”→勾选 “Use MicroLIB”(STM32 标准库需要这个精简库支持 printf);
-
重定向函数是否写对:fputc 函数是否正确调用 Serial_SendByte?
五、实战 2:串口接收数据(电脑→STM32)
发送学会后,再实现 “接收”—— 电脑发命令(如 “LED_ON”),STM32 接收后点亮 LED,这是 “双向通信” 的基础。
5.1 核心思路:中断接收(不占用 CPU)
如果用 “查询方式” 接收(不断检查是否有数据),会浪费 CPU 资源,初学者推荐用 “中断方式”:有数据来时,硬件自动触发中断,CPU 暂停当前工作去处理接收,处理完再回来。
接收流程如下:
graph TD
A[电脑发数据→STM32 RX引脚] --> B[USART接收寄存器RDR存数据]
B --> C[触发RXNE中断(接收寄存器非空)]
C --> D[CPU暂停主程序,进入中断服务函数]
D --> E[读取RDR数据,存入变量]
E --> F[置接收标志位(告诉主程序“有数据了”)]
F --> G[CPU返回主程序,处理数据(如点亮LED)]
5.2 代码实现:中断配置 + 接收处理
在实战 1 的基础上修改代码,增加中断和 LED 控制。
5.2.1 头文件(Serial.h):增加接收相关声明
\#ifndef \_\_SERIAL\_H
\#define \_\_SERIAL\_H
\#include \<stdio.h>
// 全局变量:接收数据和标志位(extern供其他文件使用)
extern uint8\_t Serial\_RxData; // 存接收的1个字节
extern uint8\_t Serial\_RxFlag; // 接收完成标志(1=有数据,0=无)
// 原有函数声明
void Serial\_Init(void);
void Serial\_SendByte(uint8\_t Byte);
void Serial\_SendArray(uint8\_t \*Array, uint16\_t Length);
void Serial\_SendString(char \*String);
uint32\_t Serial\_Pow(uint32\_t X, uint32\_t Y);
void Serial\_SendNumber(uint32\_t Number, uint8\_t Length);
void Serial\_Printf(char \*format, ...);
// 新增函数:获取接收标志和数据
uint8\_t Serial\_GetRxFlag(void);
uint8\_t Serial\_GetRxData(void);
\#endif
5.2.2 源文件(Serial.c):增加中断配置和中断服务函数
\#include "stm32f10x.h"
\#include \<stdio.h>
\#include \<stdarg.h>
// 定义全局变量:接收数据和标志位
uint8\_t Serial\_RxData;
uint8\_t Serial\_RxFlag;
void Serial\_Init(void) {
  // 1. 开启时钟(USART1、GPIOA、NVIC)
  RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_USART1, ENABLE);
  RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_GPIOA, ENABLE);
  // 2. 初始化GPIO(TX:PA9复用推挽,RX:PA10上拉输入)
  GPIO\_InitTypeDef GPIO\_InitStruct;
  // TX(PA9)
  GPIO\_InitStruct.GPIO\_Pin = GPIO\_Pin\_9;
  GPIO\_InitStruct.GPIO\_Mode = GPIO\_Mode\_AF\_PP;
  GPIO\_InitStruct.GPIO\_Speed = GPIO\_Speed\_50MHz;
  GPIO\_Init(GPIOA, \&GPIO\_InitStruct);
  // RX(PA10):上拉输入(避免悬空干扰)
  GPIO\_InitStruct.GPIO\_Pin = GPIO\_Pin\_10;
  GPIO\_InitStruct.GPIO\_Mode = GPIO\_Mode\_IPU; // 上拉输入
  GPIO\_Init(GPIOA, \&GPIO\_InitStruct);
  // 3. 初始化USART1(增加接收模式)
  USART\_InitTypeDef USART\_InitStruct;
  USART\_InitStruct.USART\_BaudRate = 9600;
  USART\_InitStruct.USART\_WordLength = USART\_WordLength\_8b;
  USART\_InitStruct.USART\_StopBits = USART\_StopBits\_1;
  USART\_InitStruct.USART\_Parity = USART\_Parity\_No;
  USART\_InitStruct.USART\_HardwareFlowControl = USART\_HardwareFlowControl\_None;
  USART\_InitStruct.USART\_Mode = USART\_Mode\_Tx | USART\_Mode\_Rx; // 同时开启收发
  USART\_Init(USART1, \&USART\_InitStruct);
  // 4. 配置中断(开启RXNE中断,即接收数据寄存器非空中断)
  USART\_ITConfig(USART1, USART\_IT\_RXNE, ENABLE); // 开启中断源
  // 5. 配置NVIC(中断优先级)
  NVIC\_PriorityGroupConfig(NVIC\_PriorityGroup\_2); // 优先级分组2(2位抢占+2位响应)
  NVIC\_InitTypeDef NVIC\_InitStruct;
  NVIC\_InitStruct.NVIC\_IRQChannel = USART1\_IRQn; // USART1中断通道
  NVIC\_InitStruct.NVIC\_IRQChannelCmd = ENABLE; // 使能通道
  NVIC\_InitStruct.NVIC\_IRQChannelPreemptionPriority = 1; // 抢占优先级1
  NVIC\_InitStruct.NVIC\_IRQChannelSubPriority = 1; // 响应优先级1
  NVIC\_Init(\&NVIC\_InitStruct);
  // 6. 开启USART1
  USART\_Cmd(USART1, ENABLE);
}
// 新增:获取接收标志位(主程序用,避免直接操作全局变量)
uint8\_t Serial\_GetRxFlag(void) {
  if (Serial\_RxFlag == 1) {
  Serial\_RxFlag = 0; // 清零标志位,准备下次接收
  return 1;
  }
  return 0;
}
// 新增:获取接收数据
uint8\_t Serial\_GetRxData(void) {
  return Serial\_RxData;
}
// 新增:USART1中断服务函数(硬件触发时自动调用)
void USART1\_IRQHandler(void) {
  // 检查是否是RXNE中断(接收数据寄存器非空)
  if (USART\_GetITStatus(USART1, USART\_IT\_RXNE) == SET) {
  Serial\_RxData = USART\_ReceiveData(USART1); // 读取接收数据
  Serial\_RxFlag = 1; // 置接收完成标志位
  USART\_ClearITPendingBit(USART1, USART\_IT\_RXNE); // 清除中断标志位(必须!)
  }
}
// 以下是实战1中的发送函数,不变...
void Serial\_SendByte(uint8\_t Byte) { ... }
void Serial\_SendArray(uint8\_t \*Array, uint16\_t Length) { ... }
void Serial\_SendString(char \*String) { ... }
uint32\_t Serial\_Pow(uint32\_t X, uint32\_t Y) { ... }
void Serial\_SendNumber(uint32\_t Number, uint8\_t Length) { ... }
int fputc(int ch, FILE \*f) { ... }
void Serial\_Printf(char \*format, ...) { ... }
5.2.3 主函数(main.c):接收数据并控制 LED
\#include "stm32f10x.h"
\#include "Serial.h"
\#include "Delay.h"
\#include "LED.h" // 自己写的LED驱动(PA0接LED)
uint8\_t RxData; // 存接收的数据
int main(void) {
  Serial\_Init(); // 初始化USART1(带中断)
  LED\_Init(); // 初始化LED(PA0推挽输出)
  Serial\_SendString("请发送数据:\r\n"); // 提示电脑发送
  while (1) {
  // 检查是否有接收数据
  if (Serial\_GetRxFlag() == 1) {
  RxData = Serial\_GetRxData(); // 获取接收的字节
   
  // 处理数据:比如收到'A'点亮LED,收到'B'熄灭LED
  if (RxData == 'A') {
  LED1\_ON(); // PA0置高,点亮LED
  Serial\_Printf("收到'A',LED已点亮\r\n");
  } else if (RxData == 'B') {
  LED1\_OFF(); // PA0置低,熄灭LED
  Serial\_Printf("收到'B',LED已熄灭\r\n");
  } else {
  Serial\_Printf("收到未知数据:0x%02X\r\n", RxData); // 显示十六进制
  }
  }
  }
}
5.2.4 LED 驱动(LED.c/LED.h,可选)
如果没有 LED 驱动,可简单写一个:
// LED.h
\#ifndef \_\_LED\_H
\#define \_\_LED\_H
\#include "stm32f10x.h"
void LED\_Init(void);
void LED1\_ON(void);
void LED1\_OFF(void);
\#endif
// LED.c
\#include "stm32f10x.h"
void LED\_Init(void) {
  RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_GPIOA, ENABLE);
  GPIO\_InitTypeDef GPIO\_InitStruct;
  GPIO\_InitStruct.GPIO\_Pin = GPIO\_Pin\_0;
  GPIO\_InitStruct.GPIO\_Mode = GPIO\_Mode\_Out\_PP;
  GPIO\_InitStruct.GPIO\_Speed = GPIO\_Speed\_50MHz;
  GPIO\_Init(GPIOA, \&GPIO\_InitStruct);
  GPIO\_ResetBits(GPIOA, GPIO\_Pin\_0); // 初始熄灭
}
void LED1\_ON(void) {
  GPIO\_SetBits(GPIOA, GPIO\_Pin\_0);
}
void LED1\_OFF(void) {
  GPIO\_ResetBits(GPIOA, GPIO\_Pin\_0);
}
5.3 硬件调试:测试接收功能
-
接线:在实战 1 的基础上,确保 STM32 的 PA0 接了 LED(串联 1k 电阻限流);
-
串口助手设置:和实战 1 一致(9600bps,文本模式);
-
发送数据:在串口助手发送区输入 ‘A’,点击 “发送”,LED 点亮,串口助手接收区显示 “收到 ‘A’,LED 已点亮”;
-
再发送 ‘B’,LED 熄灭,接收区显示 “收到 ‘B’,LED 已熄灭”;
-
发送其他字符(如 ‘C’),接收区显示 “收到未知数据:0x43”(0x43 是 ‘C’ 的 ASCII 码)。
六、进阶:串口收发 “数据包”(解决多字节通信)
如果要传多个数据(如 “温度 25℃+ 湿度 60%”),单个字节不够用,这时候需要 “打包”—— 把多个字节做成 “数据包”,带包头、包尾,防止数据混乱。
6.1 两种数据包格式:HEX 包和文本包
初学者常用两种数据包,根据场景选择:
| 数据包类型 | 格式示例 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| HEX 包(二进制) | 包头 0xFF + 数据 0x19,0x3C + 包尾 0xFE | 解析简单、速度快 | 不直观、易和包头包尾重复 | 传感器数据(温湿度、陀螺仪) |
| 文本包 | 包头 @ + 命令 “LED_ON” + 包尾 \r\n | 直观、易调试 | 解析慢、占空间 | 命令交互(如控制 LED、电机) |
6.2 实战:收发 HEX 数据包(固定包长)
以 “包头 0xFF + 4 字节数据 + 包尾 0xFE” 为例,实现 STM32 接收数据包后,回发相同数据包。
6.2.1 代码实现(Serial.c/Serial.h)
// Serial.h
\#ifndef \_\_SERIAL\_H
\#define \_\_SERIAL\_H
\#include \<stdio.h>
// 全局变量:发送/接收数据包(4字节数据)
extern uint8\_t Serial\_TxPacket\[4];
extern uint8\_t Serial\_RxPacket\[4];
extern uint8\_t Serial\_RxFlag;
// 新增:发送数据包函数
void Serial\_SendPacket(void);
// 原有函数声明...
void Serial\_Init(void);
void Serial\_SendByte(uint8\_t Byte);
void Serial\_SendArray(uint8\_t \*Array, uint16\_t Length);
void Serial\_SendString(char \*String);
uint32\_t Serial\_Pow(uint32\_t X, uint32\_t Y);
void Serial\_SendNumber(uint32\_t Number, uint8\_t Length);
void Serial\_Printf(char \*format, ...);
uint8\_t Serial\_GetRxFlag(void);
\#endif
// Serial.c
\#include "stm32f10x.h"
\#include \<stdio.h>
\#include \<stdarg.h>
// 定义数据包变量
uint8\_t Serial\_TxPacket\[4] = {0x01, 0x02, 0x03, 0x04}; // 默认发送数据
uint8\_t Serial\_RxPacket\[4]; // 接收数据包
uint8\_t Serial\_RxFlag;
void Serial\_Init(void) {
  // 和实战2一致,不变...
}
// 新增:发送数据包(包头0xFF + 4字节数据 + 包尾0xFE)
void Serial\_SendPacket(void) {
  Serial\_SendByte(0xFF); // 包头
  Serial\_SendArray(Serial\_TxPacket, 4); // 4字节数据
  Serial\_SendByte(0xFE); // 包尾
}
// 重写中断服务函数:用状态机接收数据包
void USART1\_IRQHandler(void) {
  static uint8\_t RxState = 0; // 静态变量:记录接收状态(0=等包头,1=收数据,2=等包尾)
  static uint8\_t pRxPacket = 0; // 静态变量:记录接收数据的索引(0\~3)
  uint8\_t RxData;
  if (USART\_GetITStatus(USART1, USART\_IT\_RXNE) == SET) {
  RxData = USART\_ReceiveData(USART1);
  switch (RxState) {
  case 0: // 状态0:等待包头(0xFF)
  if (RxData == 0xFF) {
  RxState = 1; // 收到包头,切换到收数据状态
  pRxPacket = 0; // 重置数据索引
  }
  break;
  case 1: // 状态1:接收4字节数据
  Serial\_RxPacket\[pRxPacket] = RxData;
  pRxPacket++;
  if (pRxPacket >= 4) { // 收够4字节
  RxState = 2; // 切换到等包尾状态
  }
  break;
  case 2: // 状态2:等待包尾(0xFE)
  if (RxData == 0xFE) {
  RxState = 0; // 收到包尾,回到初始状态
  Serial\_RxFlag = 1;// 置接收完成标志
  }
  break;
  }
  USART\_ClearITPendingBit(USART1, USART\_IT\_RXNE);
  }
}
// 其他发送函数不变...
6.2.2 主函数(main.c):测试数据包收发
\#include "stm32f10x.h"
\#include "Serial.h"
\#include "Delay.h"
\#include "OLED.h" // 用OLED显示数据包(可选)
int main(void) {
  Serial\_Init();
  OLED\_Init(); // 初始化OLED(128x64)
  OLED\_ShowString(1, 1, "Tx:"); // 第一行显示发送数据
  OLED\_ShowString(3, 1, "Rx:"); // 第三行显示接收数据
  // 初始化发送数据包
  Serial\_TxPacket\[0] = 0x01;
  Serial\_TxPacket\[1] = 0x02;
  Serial\_TxPacket\[2] = 0x03;
  Serial\_TxPacket\[3] = 0x04;
  OLED\_ShowHexNum(2, 1, Serial\_TxPacket\[0], 2); // 显示发送数据
  OLED\_ShowHexNum(2, 4, Serial\_TxPacket\[1], 2);
  OLED\_ShowHexNum(2, 7, Serial\_TxPacket\[2], 2);
  OLED\_ShowHexNum(2, 10, Serial\_TxPacket\[3], 2);
  while (1) {
  // 接收数据包并回发
  if (Serial\_GetRxFlag() == 1) {
  // 在OLED显示接收数据
  OLED\_ShowHexNum(4, 1, Serial\_RxPacket\[0], 2);
  OLED\_ShowHexNum(4, 4, Serial\_RxPacket\[1], 2);
  OLED\_ShowHexNum(4, 7, Serial\_RxPacket\[2], 2);
  OLED\_ShowHexNum(4, 10, Serial\_RxPacket\[3], 2);
  // 回发相同的数据包
  Serial\_SendArray(Serial\_RxPacket, 4);
  Serial\_Printf("\r\n收到数据包,已回发\r\n");
  }
  // 每隔3秒发送一次默认数据包(可选)
  Serial\_SendPacket();
  Serial\_Printf("\r\n发送默认数据包\r\n");
  Delay\_ms(3000);
  }
}
6.3 测试:用串口助手发送 HEX 数据包
-
串口助手设置:发送模式改为 “HEX 模式”;
-
发送数据包:在发送区输入 “FF 0A 0B 0C 0D FE”(包头 0xFF + 数据 0x0A~0x0D + 包尾 0xFE);
-
查看结果:STM32 接收后,OLED 显示接收数据,串口助手收到回发的 “0A 0B 0C 0D”,同时每隔 3 秒收到默认数据包 “FF 01 02 03 04 FE”。
七、总结:初学者的串口学习路线
-
基础阶段:搞懂 USART 的硬件接线(TX/RX 交叉、共地)和帧结构(起始 + 数据 + 停止);
-
入门阶段:实现 “单发单收”(STM32 发数据给电脑,电脑发数据给 STM32 控制 LED);
-
进阶阶段:学习数据包收发(HEX 包用于传感器,文本包用于命令交互);
-
实战阶段:结合模块(如蓝牙、GPS),用串口实现 “单片机 - 模块 - 电脑” 的完整交互。
记住:串口是电子开发的 “通用接口”,学会它,你就能和大多数硬件 “对话” 了!遇到问题别慌,先查接线、再查代码、最后查参数,多试几次就能掌握。
2644

被折叠的 条评论
为什么被折叠?



