作为电子工程师初学者,串口通信是我们接触硬件交互的 “第一道门槛”—— 小到单片机与电脑传数据,大到传感器与控制器交互,都离不开串口。本文基于 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(PA9)
RX(PA10)
GND
设备1(如STM32)
设备2的GND
no
生成失败,请重试
豆包
你的 AI 助手,助力每日工作学习
- 单向通信(如只需要单片机发数据给电脑):只需接 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)
回到空闲状态,等下一帧
起始位:1位低电平(标志开始)
数据位:8位(低位在前,如0x55是01010101,先传最低位1)
停止位:1位高电平(标志结束)
note
生成失败,请重试

豆包
你的 AI 助手,助力每日工作学习
- 起始位:必须是低电平!用来区分 “空闲” 和 “开始传数据”—— 空闲时数据线是高电平,一旦变低,设备就知道 “要开始收数据了”;
- 数据位:可以是 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 次是低电平,才确认是 “真起始位”,防止噪声干扰。
预览
代码
位周期(434个时钟)0~108时钟边缘区域(不采样,防干扰)109~325时钟中间区域(采样点:217时钟,读数据)326~434时钟边缘区域(不采样)1位数据的采样过程(115200bps,50MHz时钟)
位周期(434个时钟)0~108时钟边缘区域(不采样,防干扰)109~325时钟中间区域(采样点:217时钟,读数据)326~434时钟边缘区域(不采样)1位数据的采样过程(115200bps,50MHz时钟)

豆包
你的 AI 助手,助力每日工作学习
三、STM32 USART 硬件解析:从框图到引脚
学完协议,再看 STM32 的 USART 外设 ——STM32F103C8T6 有 3 个 USART(USART1、USART2、USART3),我们重点用 USART1 做实战。
3.1 USART 核心框图:数据 “怎么在芯片里走”?
STM32 的 USART 不是 “一根线通到底”,而是有完整的 “发送 / 接收链路”,初学者不用深究细节,记住核心流程即可:
预览
代码
发送端:CPU/内存
发送数据寄存器TDR
发送控制器+波特率发生器
发送移位寄存器(把并行数据转串行)
TX引脚(PA9)发出去
RX引脚(PA10)收数据
接收移位寄存器(把串行转并行)
接收控制器+波特率发生器
接收数据寄存器RDR
接收端:CPU/内存
note
生成失败,请重试
发送端:CPU/内存
发送数据寄存器TDR
发送控制器+波特率发生器
发送移位寄存器(把并行数据转串行)
TX引脚(PA9)发出去
RX引脚(PA10)收数据
接收移位寄存器(把串行转并行)
接收控制器+波特率发生器
接收数据寄存器RDR
接收端:CPU/内存
note
生成失败,请重试

豆包
你的 AI 助手,助力每日工作学习
- 发送时: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 转串口模块):
plaintext
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):声明函数
c
运行
#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):实现功能
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):测试发送
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 发送的数据,如下:
plaintext
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 暂停当前工作去处理接收,处理完再回来。
接收流程如下:
预览
代码
电脑发数据→STM32 RX引脚
USART接收寄存器RDR存数据
触发RXNE中断(接收寄存器非空)
CPU暂停主程序,进入中断服务函数
读取RDR数据,存入变量
置接收标志位(告诉主程序“有数据了”)
CPU返回主程序,处理数据(如点亮LED)
电脑发数据→STM32 RX引脚
USART接收寄存器RDR存数据
触发RXNE中断(接收寄存器非空)
CPU暂停主程序,进入中断服务函数
读取RDR数据,存入变量
置接收标志位(告诉主程序“有数据了”)
CPU返回主程序,处理数据(如点亮LED)

豆包
你的 AI 助手,助力每日工作学习
5.2 代码实现:中断配置 + 接收处理
在实战 1 的基础上修改代码,增加中断和 LED 控制。
5.2.1 头文件(Serial.h):增加接收相关声明
c
运行
#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):增加中断配置和中断服务函数
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
c
运行
#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 驱动,可简单写一个:
c
运行
// 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)
c
运行
// 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):测试数据包收发
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),用串口实现 “单片机 - 模块 - 电脑” 的完整交互。
记住:串口是电子开发的 “通用接口”,学会它,你就能和大多数硬件 “对话” 了!遇到问题别慌,先查接线、再查代码、最后查参数,多试几次就能掌握。
2626

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



