#电子工程师入门:USART 串口协议全解析(从原理到实战)

作为电子工程师初学者,串口通信是我们接触硬件交互的 “第一道门槛”—— 小到单片机与电脑传数据,大到传感器与控制器交互,都离不开串口。本文基于 STM32F103C8T6,从 “为什么用串口” 到 “亲手写代码收发数据”,用大白话 + 实操细节,帮你彻底搞懂 USART 串口协议,附带流程图和接线图,跟着做就能上手!

一、先搞懂:通信接口的 “底层逻辑”

我们做电子开发,本质是让 “两个设备说话”—— 比如单片机告诉电脑 “当前温度 25℃”,或电脑命令单片机 “点亮 LED”。要实现对话,就得先明确 “说什么”(数据)和 “怎么说”(协议)。

1.1 为什么选 USART 串口?先看通信接口对比

市面上有 USART、I2C、SPI、CAN 等多种接口,初学者不用全学,先搞懂它们的核心区别,才知道什么时候该用串口:

接口类型关键引脚通信方向(双工)时钟方式电平类型适用场景设备数量
USARTTX(发)、RX(收)全双工(同时互传)异步(无时钟线)单端(需共地)单片机 - 电脑、单片机 - 模块(如蓝牙)点对点(2 个设备)
I2CSCL(时钟)、SDA(数据)半双工(轮流传)同步(需时钟)单端多传感器(如温湿度、陀螺仪)多设备(1 主多从)
SPISCLK(时钟)、MOSI(主发从收)、MISO(主收从发)、CS(片选)全双工同步单端高速设备(如显示屏、Flash)多设备(1 主多从)
CANCAN_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/+5V0V单片机内部常用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 关键参数:决定 “说话速度” 和 “清晰度”

除了帧结构,还要约定两个关键参数,不然设备会 “听不懂”:

  1. 波特率:“说话速度”,即每秒传输的 “位数”(bit),常见值 9600bps、38400bps、115200bps。
  • 例:9600bps 表示每秒传 9600 位,1 帧 10 位的话,每秒能传 960 帧(约 960 字节);

  • 注意:通信双方必须用相同波特率!比如 STM32 设 9600,电脑串口助手也要设 9600,不然数据会乱码。

  1. 数据模式:数据的 “显示形式”,分两种:
  • HEX 模式(十六进制):传原始数据(如 0x55、0xA0),适合传感器数据(如温湿度、陀螺仪);

  • 文本模式(字符):传 ASCII 码对应的字符(如 0x41 对应 ‘A’、0x31 对应 ‘1’),适合命令交互(如电脑发 “LED_ON” 控制单片机)。

2.3 数据采样:设备怎么 “准确听清楚”?

串口是 “异步通信”(没有时钟线同步),设备怎么保证 “在正确的时间读数据”?靠 “波特率时钟采样”。

以 115200bps、50MHz 系统时钟为例:

  1. 波特率对应的 “位周期”= 系统时钟 / 波特率 = 50,000,000 / 115200 ≈ 434 个时钟周期(即 1 位数据持续 434 个时钟);

  2. 设备会在 “每位数据的中间时刻” 采样(如第 217 个时钟周期),这样能避开边缘干扰,保证数据准确;

  3. 起始位的采样更严格:会连续采样多次(如 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 引脚(接收)挂载总线常用场景
USART1PA9PA10APB2(72MHz)和电脑通信(高速)
USART2PA2PA3APB1(36MHz)和模块通信(如蓝牙)
USART3PB10PB11APB1(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) {

&#x20;   // 1. 开启时钟(USART1和GPIOA都在APB2总线上)

&#x20;   RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_USART1, ENABLE);

&#x20;   RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_GPIOA, ENABLE);

&#x20;   // 2. 初始化GPIO(TX:PA9复用推挽输出,RX:PA10上拉输入)

&#x20;   GPIO\_InitTypeDef GPIO\_InitStruct;

&#x20;   // TX引脚(PA9):复用推挽输出(串口发送需要GPIO复用为USART功能)

&#x20;   GPIO\_InitStruct.GPIO\_Pin = GPIO\_Pin\_9;

&#x20;   GPIO\_InitStruct.GPIO\_Mode = GPIO\_Mode\_AF\_PP;  // 复用推挽

&#x20;   GPIO\_InitStruct.GPIO\_Speed = GPIO\_Speed\_50MHz;

&#x20;   GPIO\_Init(GPIOA, \&GPIO\_InitStruct);

&#x20;   // 3. 初始化USART1参数

&#x20;   USART\_InitTypeDef USART\_InitStruct;

&#x20;   USART\_InitStruct.USART\_BaudRate = 9600;         // 波特率9600

&#x20;   USART\_InitStruct.USART\_WordLength = USART\_WordLength\_8b;  // 8位数据

&#x20;   USART\_InitStruct.USART\_StopBits = USART\_StopBits\_1;       // 1位停止

&#x20;   USART\_InitStruct.USART\_Parity = USART\_Parity\_No;           // 无校验

&#x20;   USART\_InitStruct.USART\_HardwareFlowControl = USART\_HardwareFlowControl\_None;  // 无硬件流控

&#x20;   USART\_InitStruct.USART\_Mode = USART\_Mode\_Tx;  // 只开启发送模式(先做发送)

&#x20;   USART\_Init(USART1, \&USART\_InitStruct);

&#x20;   // 4. 开启USART1

&#x20;   USART\_Cmd(USART1, ENABLE);

}

// 发1个字节:等待发送完成再返回

void Serial\_SendByte(uint8\_t Byte) {

&#x20;   USART\_SendData(USART1, Byte);  // 把字节写入发送寄存器

&#x20;   // 等待发送完成(TXE标志位:发送寄存器为空,表示数据已发出去)

&#x20;   while (USART\_GetFlagStatus(USART1, USART\_FLAG\_TXE) == RESET);

}

// 发数组:逐个字节发送

void Serial\_SendArray(uint8\_t \*Array, uint16\_t Length) {

&#x20;   for (uint16\_t i = 0; i < Length; i++) {

&#x20;       Serial\_SendByte(Array\[i]);

&#x20;   }

}

// 发字符串:以'\0'为结束符(如"Hello")

void Serial\_SendString(char \*String) {

&#x20;   for (uint8\_t i = 0; String\[i] != '\0'; i++) {

&#x20;       Serial\_SendByte(String\[i]);

&#x20;   }

}

// 辅助函数:计算X的Y次方(用于发数字时拆分位数)

uint32\_t Serial\_Pow(uint32\_t X, uint32\_t Y) {

&#x20;   uint32\_t Result = 1;

&#x20;   while (Y--) {

&#x20;       Result \*= X;

&#x20;   }

&#x20;   return Result;

}

// 发数字(字符形式):如123→'1''2''3'

void Serial\_SendNumber(uint32\_t Number, uint8\_t Length) {

&#x20;   for (uint8\_t i = 0; i < Length; i++) {

&#x20;       // 拆分每一位:如123,Length=3,i=0时取1(123/10^(3-0-1)%10=123/100%10=1)

&#x20;       uint8\_t Digit = Number / Serial\_Pow(10, Length - i - 1) % 10;

&#x20;       Serial\_SendByte(Digit + '0');  // 转成字符(如1→'1',加ASCII偏移0x30)

&#x20;   }

}

// 重定向printf:让printf输出到串口(默认printf输出到屏幕,单片机没有屏幕)

int fputc(int ch, FILE \*f) {

&#x20;   Serial\_SendByte(ch);  // 把printf的字符通过串口发出去

&#x20;   return ch;

}

// 封装printf:支持可变参数(如Serial\_Printf("温度:%d℃\r\n", 25))

void Serial\_Printf(char \*format, ...) {

&#x20;   char String\[100];  // 缓存格式化后的字符串

&#x20;   va\_list Arg;       // 可变参数列表

&#x20;   va\_start(Arg, format);  // 从format开始解析参数

&#x20;   vsprintf(String, format, Arg);  // 把参数格式化成字符串

&#x20;   va\_end(Arg);  // 释放参数列表

&#x20;   Serial\_SendString(String);  // 发送字符串

}
4.1.3 主函数(main.c):测试发送
\#include "stm32f10x.h"

\#include "Serial.h"  // 包含串口函数

\#include "Delay.h"   // 延时函数(自己写或用库)

int main(void) {

&#x20;   Serial\_Init();  // 初始化USART1(9600bps)

&#x20;   // 测试发送:发字节、数组、字符串、数字、printf

&#x20;   Serial\_SendByte('A');  // 发字符'A'(ASCII 0x41)

&#x20;  &#x20;

&#x20;   uint8\_t MyArray\[] = {0x42, 0x43, 0x44};  // 发数组:B、C、D

&#x20;   Serial\_SendArray(MyArray, 3);

&#x20;  &#x20;

&#x20;   Serial\_SendString("\r\nHello STM32!\r\n");  // 发字符串(\r\n是换行)

&#x20;  &#x20;

&#x20;   Serial\_SendNumber(123, 3);  // 发数字123(字符形式)

&#x20;  &#x20;

&#x20;   printf("\r\nprintf测试:%d℃\r\n", 25);  // 重定向printf

&#x20;  &#x20;

&#x20;   Serial\_Printf("封装printf测试:%s\r\n", "串口发送成功!");  // 封装printf

&#x20;   while (1) {

&#x20;       // 循环发送(可选)

&#x20;       Serial\_Printf("循环发送:%d\r\n", \_\_HAL\_GetTick()/1000);  // 每秒发一次时间

&#x20;       Delay\_ms(1000);  // 延时1秒

&#x20;   }

}

4.2 硬件调试:接线 + 串口助手设置

代码写好后,要 “让硬件跑起来”,步骤如下:

4.2.1 接线图(实物)
STM32F103C8T6USB 转串口模块(CH340)功能
PA9(TX)RXSTM32 发→模块收
PA10(RX)TXSTM32 收→模块发(本次暂不用)
GNDGND共地(必须接!)
3.3V3.3V给 STM32 供电(模块接电脑 USB 取电)

注意:如果 STM32 用其他方式供电(如 ST-Link),就不用接模块的 3.3V,只接 TX、RX、GND。

4.2.2 串口助手设置(以 “串口助手 V1.1” 为例)
  1. 安装 CH340 驱动:模块插电脑 USB,打开 “设备管理器”,查看 “端口(COM 和 LPT)”,确认有 “USB-SERIAL CH340(COMx)”(x 是串口号,如 COM3);

  2. 打开串口助手:

  • 串口号:选设备管理器里的 COMx(如 COM3);

  • 波特率:9600(和代码里一致);

  • 数据位:8;

  • 停止位:1;

  • 校验位:无;

  • 接收模式:文本模式(先看字符,后期再试 HEX 模式);

  1. 下载代码:用 ST-Link 把代码下载到 STM32;

  2. 打开串口:点击 “打开串口”,就能看到 STM32 发送的数据,如下:

ABCHello STM32!

123

printf测试:25℃

封装printf测试:串口发送成功!

循环发送:0

循环发送:1

循环发送:2

4.3 常见问题:初学者必踩的坑

  1. 串口助手没数据
  • 检查接线:TX 和 RX 是否交叉?GND 是否接了?

  • 检查波特率:代码和串口助手是否一致(如都是 9600)?

  • 检查 USART 初始化:TX 引脚是否设为 “复用推挽输出”(不是普通推挽!)?

  1. 数据乱码
  • 波特率不匹配:比如代码设 115200,助手设 9600;

  • 电平不匹配:直接用 STM32 的 TTL 接电脑 RS232(需加电平转换芯片,如 MAX232);

  1. 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) {

&#x20;   // 1. 开启时钟(USART1、GPIOA、NVIC)

&#x20;   RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_USART1, ENABLE);

&#x20;   RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_GPIOA, ENABLE);

&#x20;   // 2. 初始化GPIO(TX:PA9复用推挽,RX:PA10上拉输入)

&#x20;   GPIO\_InitTypeDef GPIO\_InitStruct;

&#x20;   // TX(PA9)

&#x20;   GPIO\_InitStruct.GPIO\_Pin = GPIO\_Pin\_9;

&#x20;   GPIO\_InitStruct.GPIO\_Mode = GPIO\_Mode\_AF\_PP;

&#x20;   GPIO\_InitStruct.GPIO\_Speed = GPIO\_Speed\_50MHz;

&#x20;   GPIO\_Init(GPIOA, \&GPIO\_InitStruct);

&#x20;   // RX(PA10):上拉输入(避免悬空干扰)

&#x20;   GPIO\_InitStruct.GPIO\_Pin = GPIO\_Pin\_10;

&#x20;   GPIO\_InitStruct.GPIO\_Mode = GPIO\_Mode\_IPU;  // 上拉输入

&#x20;   GPIO\_Init(GPIOA, \&GPIO\_InitStruct);

&#x20;   // 3. 初始化USART1(增加接收模式)

&#x20;   USART\_InitTypeDef USART\_InitStruct;

&#x20;   USART\_InitStruct.USART\_BaudRate = 9600;

&#x20;   USART\_InitStruct.USART\_WordLength = USART\_WordLength\_8b;

&#x20;   USART\_InitStruct.USART\_StopBits = USART\_StopBits\_1;

&#x20;   USART\_InitStruct.USART\_Parity = USART\_Parity\_No;

&#x20;   USART\_InitStruct.USART\_HardwareFlowControl = USART\_HardwareFlowControl\_None;

&#x20;   USART\_InitStruct.USART\_Mode = USART\_Mode\_Tx | USART\_Mode\_Rx;  // 同时开启收发

&#x20;   USART\_Init(USART1, \&USART\_InitStruct);

&#x20;   // 4. 配置中断(开启RXNE中断,即接收数据寄存器非空中断)

&#x20;   USART\_ITConfig(USART1, USART\_IT\_RXNE, ENABLE);  // 开启中断源

&#x20;   // 5. 配置NVIC(中断优先级)

&#x20;   NVIC\_PriorityGroupConfig(NVIC\_PriorityGroup\_2);  // 优先级分组2(2位抢占+2位响应)

&#x20;   NVIC\_InitTypeDef NVIC\_InitStruct;

&#x20;   NVIC\_InitStruct.NVIC\_IRQChannel = USART1\_IRQn;  // USART1中断通道

&#x20;   NVIC\_InitStruct.NVIC\_IRQChannelCmd = ENABLE;  // 使能通道

&#x20;   NVIC\_InitStruct.NVIC\_IRQChannelPreemptionPriority = 1;  // 抢占优先级1

&#x20;   NVIC\_InitStruct.NVIC\_IRQChannelSubPriority = 1;  // 响应优先级1

&#x20;   NVIC\_Init(\&NVIC\_InitStruct);

&#x20;   // 6. 开启USART1

&#x20;   USART\_Cmd(USART1, ENABLE);

}

// 新增:获取接收标志位(主程序用,避免直接操作全局变量)

uint8\_t Serial\_GetRxFlag(void) {

&#x20;   if (Serial\_RxFlag == 1) {

&#x20;       Serial\_RxFlag = 0;  // 清零标志位,准备下次接收

&#x20;       return 1;

&#x20;   }

&#x20;   return 0;

}

// 新增:获取接收数据

uint8\_t Serial\_GetRxData(void) {

&#x20;   return Serial\_RxData;

}

// 新增:USART1中断服务函数(硬件触发时自动调用)

void USART1\_IRQHandler(void) {

&#x20;   // 检查是否是RXNE中断(接收数据寄存器非空)

&#x20;   if (USART\_GetITStatus(USART1, USART\_IT\_RXNE) == SET) {

&#x20;       Serial\_RxData = USART\_ReceiveData(USART1);  // 读取接收数据

&#x20;       Serial\_RxFlag = 1;  // 置接收完成标志位

&#x20;       USART\_ClearITPendingBit(USART1, USART\_IT\_RXNE);  // 清除中断标志位(必须!)

&#x20;   }

}

// 以下是实战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) {

&#x20;   Serial\_Init();  // 初始化USART1(带中断)

&#x20;   LED\_Init();     // 初始化LED(PA0推挽输出)

&#x20;   Serial\_SendString("请发送数据:\r\n");  // 提示电脑发送

&#x20;   while (1) {

&#x20;       // 检查是否有接收数据

&#x20;       if (Serial\_GetRxFlag() == 1) {

&#x20;           RxData = Serial\_GetRxData();  // 获取接收的字节

&#x20;          &#x20;

&#x20;           // 处理数据:比如收到'A'点亮LED,收到'B'熄灭LED

&#x20;           if (RxData == 'A') {

&#x20;               LED1\_ON();  // PA0置高,点亮LED

&#x20;               Serial\_Printf("收到'A',LED已点亮\r\n");

&#x20;           } else if (RxData == 'B') {

&#x20;               LED1\_OFF();  // PA0置低,熄灭LED

&#x20;               Serial\_Printf("收到'B',LED已熄灭\r\n");

&#x20;           } else {

&#x20;               Serial\_Printf("收到未知数据:0x%02X\r\n", RxData);  // 显示十六进制

&#x20;           }

&#x20;       }

&#x20;   }

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

&#x20;   RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_GPIOA, ENABLE);

&#x20;   GPIO\_InitTypeDef GPIO\_InitStruct;

&#x20;   GPIO\_InitStruct.GPIO\_Pin = GPIO\_Pin\_0;

&#x20;   GPIO\_InitStruct.GPIO\_Mode = GPIO\_Mode\_Out\_PP;

&#x20;   GPIO\_InitStruct.GPIO\_Speed = GPIO\_Speed\_50MHz;

&#x20;   GPIO\_Init(GPIOA, \&GPIO\_InitStruct);

&#x20;   GPIO\_ResetBits(GPIOA, GPIO\_Pin\_0);  // 初始熄灭

}

void LED1\_ON(void) {

&#x20;   GPIO\_SetBits(GPIOA, GPIO\_Pin\_0);

}

void LED1\_OFF(void) {

&#x20;   GPIO\_ResetBits(GPIOA, GPIO\_Pin\_0);

}

5.3 硬件调试:测试接收功能

  1. 接线:在实战 1 的基础上,确保 STM32 的 PA0 接了 LED(串联 1k 电阻限流);

  2. 串口助手设置:和实战 1 一致(9600bps,文本模式);

  3. 发送数据:在串口助手发送区输入 ‘A’,点击 “发送”,LED 点亮,串口助手接收区显示 “收到 ‘A’,LED 已点亮”;

  4. 再发送 ‘B’,LED 熄灭,接收区显示 “收到 ‘B’,LED 已熄灭”;

  5. 发送其他字符(如 ‘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) {

&#x20;   // 和实战2一致,不变...

}

// 新增:发送数据包(包头0xFF + 4字节数据 + 包尾0xFE)

void Serial\_SendPacket(void) {

&#x20;   Serial\_SendByte(0xFF);                // 包头

&#x20;   Serial\_SendArray(Serial\_TxPacket, 4); // 4字节数据

&#x20;   Serial\_SendByte(0xFE);                // 包尾

}

// 重写中断服务函数:用状态机接收数据包

void USART1\_IRQHandler(void) {

&#x20;   static uint8\_t RxState = 0;  // 静态变量:记录接收状态(0=等包头,1=收数据,2=等包尾)

&#x20;   static uint8\_t pRxPacket = 0; // 静态变量:记录接收数据的索引(0\~3)

&#x20;   uint8\_t RxData;

&#x20;   if (USART\_GetITStatus(USART1, USART\_IT\_RXNE) == SET) {

&#x20;       RxData = USART\_ReceiveData(USART1);

&#x20;       switch (RxState) {

&#x20;           case 0:  // 状态0:等待包头(0xFF)

&#x20;               if (RxData == 0xFF) {

&#x20;                   RxState = 1;    // 收到包头,切换到收数据状态

&#x20;                   pRxPacket = 0;  // 重置数据索引

&#x20;               }

&#x20;               break;

&#x20;           case 1:  // 状态1:接收4字节数据

&#x20;               Serial\_RxPacket\[pRxPacket] = RxData;

&#x20;               pRxPacket++;

&#x20;               if (pRxPacket >= 4) {  // 收够4字节

&#x20;                   RxState = 2;      // 切换到等包尾状态

&#x20;               }

&#x20;               break;

&#x20;           case 2:  // 状态2:等待包尾(0xFE)

&#x20;               if (RxData == 0xFE) {

&#x20;                   RxState = 0;      // 收到包尾,回到初始状态

&#x20;                   Serial\_RxFlag = 1;// 置接收完成标志

&#x20;               }

&#x20;               break;

&#x20;       }

&#x20;       USART\_ClearITPendingBit(USART1, USART\_IT\_RXNE);

&#x20;   }

}

// 其他发送函数不变...
6.2.2 主函数(main.c):测试数据包收发
\#include "stm32f10x.h"

\#include "Serial.h"

\#include "Delay.h"

\#include "OLED.h"  // 用OLED显示数据包(可选)

int main(void) {

&#x20;   Serial\_Init();

&#x20;   OLED\_Init();  // 初始化OLED(128x64)

&#x20;   OLED\_ShowString(1, 1, "Tx:");  // 第一行显示发送数据

&#x20;   OLED\_ShowString(3, 1, "Rx:");  // 第三行显示接收数据

&#x20;   // 初始化发送数据包

&#x20;   Serial\_TxPacket\[0] = 0x01;

&#x20;   Serial\_TxPacket\[1] = 0x02;

&#x20;   Serial\_TxPacket\[2] = 0x03;

&#x20;   Serial\_TxPacket\[3] = 0x04;

&#x20;   OLED\_ShowHexNum(2, 1, Serial\_TxPacket\[0], 2);  // 显示发送数据

&#x20;   OLED\_ShowHexNum(2, 4, Serial\_TxPacket\[1], 2);

&#x20;   OLED\_ShowHexNum(2, 7, Serial\_TxPacket\[2], 2);

&#x20;   OLED\_ShowHexNum(2, 10, Serial\_TxPacket\[3], 2);

&#x20;   while (1) {

&#x20;       // 接收数据包并回发

&#x20;       if (Serial\_GetRxFlag() == 1) {

&#x20;           // 在OLED显示接收数据

&#x20;           OLED\_ShowHexNum(4, 1, Serial\_RxPacket\[0], 2);

&#x20;           OLED\_ShowHexNum(4, 4, Serial\_RxPacket\[1], 2);

&#x20;           OLED\_ShowHexNum(4, 7, Serial\_RxPacket\[2], 2);

&#x20;           OLED\_ShowHexNum(4, 10, Serial\_RxPacket\[3], 2);

&#x20;           // 回发相同的数据包

&#x20;           Serial\_SendArray(Serial\_RxPacket, 4);

&#x20;           Serial\_Printf("\r\n收到数据包,已回发\r\n");

&#x20;       }

&#x20;       // 每隔3秒发送一次默认数据包(可选)

&#x20;       Serial\_SendPacket();

&#x20;       Serial\_Printf("\r\n发送默认数据包\r\n");

&#x20;       Delay\_ms(3000);

&#x20;   }

}

6.3 测试:用串口助手发送 HEX 数据包

  1. 串口助手设置:发送模式改为 “HEX 模式”;

  2. 发送数据包:在发送区输入 “FF 0A 0B 0C 0D FE”(包头 0xFF + 数据 0x0A~0x0D + 包尾 0xFE);

  3. 查看结果:STM32 接收后,OLED 显示接收数据,串口助手收到回发的 “0A 0B 0C 0D”,同时每隔 3 秒收到默认数据包 “FF 01 02 03 04 FE”。

七、总结:初学者的串口学习路线

  1. 基础阶段:搞懂 USART 的硬件接线(TX/RX 交叉、共地)和帧结构(起始 + 数据 + 停止);

  2. 入门阶段:实现 “单发单收”(STM32 发数据给电脑,电脑发数据给 STM32 控制 LED);

  3. 进阶阶段:学习数据包收发(HEX 包用于传感器,文本包用于命令交互);

  4. 实战阶段:结合模块(如蓝牙、GPS),用串口实现 “单片机 - 模块 - 电脑” 的完整交互。

记住:串口是电子开发的 “通用接口”,学会它,你就能和大多数硬件 “对话” 了!遇到问题别慌,先查接线、再查代码、最后查参数,多试几次就能掌握。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小范好好学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值