DAJ提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、并行通信(Parallel Communication)
🧩 起点 —— 最初的硬件通信方式
-
每个数据位占用一根引脚,如 8 位数据需要 D0~D7 共 8 根线;
-
再加上地址线、控制线,总线繁琐、引脚多。
❌ 问题:
-
硬件成本高:需要大量GPIO和电缆;
-
长距离不可靠:并行信号容易失步;
-
难以扩展:适用于板内芯片通信,不适合主板和外设。
🔁 于是:为了节省引脚、提高可靠性,工程师想到:能不能只用一两根线来通信?于是——UART 诞生。
二、UART —— 最朴素的点对点通信
🧩 背景
最初的设备之间,只需要最简单的数据收发——一个发、一个收。
✅ 特点
-
点对点、无地址、无需主从结构;
-
无需时钟,靠起始位和停止位同步;
-
易用、开销小、调试方便。
❌ 局限
-
只能两点之间通信,无法扩展多设备;
-
没有总线控制、没有仲裁机制;
-
设备之间容易冲突。
🔁 于是,需求推动了总线型多设备通信的需求出现——I2C诞生。(下篇讲)
三、并行通信和UART的区别
【并行通信 vs UART 串行通信图解】如上图所示:并行通信需要多条数据线同时传输一个字节,而UART只用一条数据线逐位串行发送,并通过起始位和停止位完成同步。
四、并行通信vsUART举例
✅ 并行通信发送 "HELLO"
👇 情景:
你用 8 根数据线(D0~D7)连接外设,想发送 ASCII 字符 "H"、"E"、"L"、"L"、"O"。
每个字符占 8 位(1 字节),比如:
字符 | ASCII | 二进制 |
---|---|---|
H | 0x48 | 0100 1000 |
E | 0x45 | 0100 0101 |
L | 0x4C | 0100 1100 |
O | 0x4F | 0100 1111 |
📦 传输过程(逐字符):
-
MCU 把
"H"
的 8 位值01001000
同时放到 D0~D7 数据线上; -
控制线拉高:
WR=1
(写使能); -
外设在同一时刻读取所有 8 位数据;
-
控制线拉低,准备下一个字符;
-
重复以上过程,依次送出
"E"
、"L"
、"L"
、"O"
;
🔧 需要硬件支持:
-
至少 8 根数据线;
-
外设和主控要有并行接口芯片;
-
控制时序精确,否则容易读错。
✅ UART 串行通信发送 "HELLO"
👇 情景:
使用 UART 只需 1 根 TX 线发送,按照帧格式一位一位发送。
帧格式(常见 8N1):
-
1 个起始位(低电平)
-
8 个数据位
-
1 个停止位(高电平)
📦 传输过程(以 "H" 为例):
ASCII "H"
= 0x48
= 01001000
UART帧发送顺序为:起始位 0
→ 0
→ 0
→ 0
→ 1
→ 0
→ 0
→ 1
→ 0
→ 停止位 1
(也就是:0 00010010 1
)
每位按设定波特率(如 9600bps)逐位发送,只用一根线。
剩下的 "E"
"L"
"L"
"O"
依次发送,每个都是独立的一帧。
五、基于cubemx配置UART
-
打开 CubeMX,使能 USART1(或其他串口);
-
设置参数:
-
波特率:9600
-
数据位:8
-
停止位:1
-
校验位:None
-
模式:TX
-
-
生成代码,CubeMX 会自动配置
MX_USART1_UART_Init()
。
代码示例(main.c 中使用 HAL_UART_Transmit):
#include "main.h"
#include "usart.h" // CubeMX 生成的串口初始化头文件
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init(); // 初始化 UART1
const char *msg = "HELLO\r\n";
while (1)
{
HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), HAL_MAX_DELAY);
HAL_Delay(1000); // 每秒发送一次
}
}
__HAL_RCC_USART1_CLK_ENABLE(); // 打开串口时钟
//gpio配置
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
// 串口参数初始化(在 usart.c 里):
huart1.Instance = USART1;
huart1.Init.BaudRate = 9600;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX;
HAL_UART_Init(&huart1);
大家有没有想过,我们只是在调用了一下hal库的函数,为什么就可以完成数据的发送 ,你心里在想:“我明明只写了一行代码,连什么是 0 什么是 1 都没写,那数据怎么就真的变成了0和1通过TX脚发出去了?”
HAL_UART_Transmit(&huart1, (uint8_t *)"H", 1, HAL_MAX_DELAY);
第一步:你给了“内容”
你传进去的是 "H"
,也就是一个字符。
字符 "H" 对应 ASCII 值是 0x48,二进制是:01001000
也就是说你告诉芯片:“我要把这个 8 位二进制数据发送出去”。
第二步:HAL库把它交给 USART 外设
HAL_UART_Transmit() 会把这个数据 0x48 写进一个专用的寄存器:
USARTx->DR = 0x48;
USART 控制器是 MCU 里面的一个模块,它专门负责按照 UART 协议,把你给它的数据“打包成帧”并发出去。
在usart中断处理中,按以下去处理
发送数据寄存器(TDR):
是 CPU 往 USART 硬件“投递数据”的地方
就像你把信件投进邮箱,接下来由“邮局”处理。
USARTx->TDR = 'o'; // 你写了这行
CPU 把 'o' 放进 TDR,然后就去干别的事了。
发送移位寄存器(Shift Register):
这是 USART 真正“把数据一个位一个位从 TX 发出去”的地方。
当 TDR 里的数据准备好了,USART 会把它搬进移位寄存器,逐位发送出去(每位按波特率控制间隔)。
第三步:USART 硬件控制器做了这件事
它在内部悄悄完成这些事:
-
在数据前面自动加一个起始位
0
-
然后把
01001000
每一位“排队发送” -
最后再加一个停止位
1
也就是说:
你要发的电平 = 0(起始) 0 0 0 1 0 0 1 0(数据位,从低位到高位) 1(停止)
共 10 个电平,每个位按波特率发出,比如 9600bps,就每 104μs 发送 1 位。
第四步:这些位,真的变成了 TX 引脚上的高低电平
MCU 的 TX 引脚(比如 PA9)原来是普通 GPIO,现在被设置为 “复用模式”,连到了 USART 硬件控制器的 TX 输出端。
所以,当 USART 硬件想发 1,就让这个引脚拉高电平(3.3V);想发 0,就让引脚拉低电平(0V)。你用示波器测 PA9,你会看到这样的波形:
_________ ___ ___ ___ ________
| | | | | | | | |
___| |___| |_| |_____| |___| --> 时间轴(每位宽度 = 104μs)
↑ 起始位(0) ↑ 数据位01001000 ↑ 停止位(1)
这就是你真正想看到的 “01001000” 变成电平从 PA9 发出去的瞬间。
步骤 | 发生了什么 |
---|---|
1️⃣ 你调用 HAL_UART_Transmit() ,传入 'H' | |
2️⃣ HAL 库把 'H' = 0x48 写入 USART 数据寄存器 | |
3️⃣ USART 硬件自动打包成帧(起始位 + 数据位 + 停止位) | |
4️⃣ USART 控制 TX 引脚,逐位把 0/1 变成电平输出 | |
5️⃣ 你用示波器测 TX 就能看到这些 0/1 波形 |
电平 ↓
3.3V ──────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌──────────
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
0V ______|_____|____|___|____|_|____|_|____|_|____|_|____|_|____|___|_________
起始 D0 D1 D2 D3 D4 D5 D6 D7 停止位
(0) (0) (0) (1)(0)(0)(1)(0) (1)
六、UART代码中的本质
🌟“我只写了几行 HAL 库代码,就能通过串口接收到数据,但我并没有自己写 ‘接收0和1’ 的代码,那到底是谁在做这些事?这些0和1是从哪来的?USART硬件怎么就知道什么时候开始、什么时候结束?”
1、关键结论
STM32内部有一块名为“USART”的硬件模块,它是一套固定的电路逻辑,专门负责识别串口电平、解析数据帧,并把0和1拼成字节传给你;你用HAL只是调动它,真正干活的是它。
// 声明为 volatile,确保 CPU 正确识别每次标志位的更新
volatile uint8_t uart_send_flag = 0;
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
uart_send_flag = 1; // 发送完一个字节,标志位为 1,表示可以发送下一个字节
}
while (1)
{
// 执行其他任务,例如点灯任务
HAL_GPIO_TogglePin(LED_PIN);
HAL_Delay(500); // 点灯闪烁
// 检查标志位,确认是否可以发送下一个字节
if (uart_send_flag)
{
uart_send_flag = 0; // 重置标志位
HAL_UART_Transmit(&huart2, &data[index], 1, HAL_MAX_DELAY); // 发送下一个字节
}
}
1、数据是怎么从TX引脚变成你软件中的字符的?全过程如下👇
步骤 | 谁在做 | 原理和作用 |
---|---|---|
1️⃣ 软件调用 HAL_UART_Receive() | 你写代码 | 只是告诉 USART:“我要收多少个字节” |
2️⃣ USART 硬件开始监听 RX 引脚 | USART 电路 | 一直盯着 RX,等它从高电平跳到低电平(起始位) |
3️⃣ 识别起始位 | USART 边沿检测器 | 看到 RX 引脚拉低,说明一帧开始了 |
4️⃣ 定时采样每一位 | USART 波特率定时器 | 根据设定波特率(如9600bps),每 104μs 采一次数据 |
5️⃣ 拼出 8 个数据位 | USART 移位寄存器 | 把0和1一位位塞进寄存器,得到一个完整字节 |
6️⃣ 检查停止位 | USART 帧校验电路 | 停止位应为1,如果不是说明帧有问题 |
7️⃣ 数据进入 USART_DR | USART 写数据寄存器 | 成功的数据被锁存,RXNE = 1 |
8️⃣ HAL 函数检测到 RXNE | HAL 库 | 从 DR 中取出这个字节,放进你准备的 rx_buf[] 中 |
2、你没写的、但实际发生的 “电路动作”:
-
有 边沿检测器 检查 RX 是否变成低电平;
-
有 定时器 精确采样每一位;
-
有 移位寄存器 把采样来的0/1排成字节;
-
有 校验逻辑 判断帧是不是完整;
-
有 RXNE标志位 表示“这字节已经到了”。
这些全部是 STM32芯片里的硬件逻辑电路完成的,不需要 CPU 运算。
3、总结一句话记忆法:
🧠 “我只是写了个接收函数,真正干活的是 USART 硬件,它能识别电平变换、按位接收、合成字节、通知我收好了。”
七、对比并行通信 vs UART 接收逻辑
对比项 | 并行通信(Parallel) | UART 串行通信 |
---|---|---|
数据线数量 | 多达8根以上 | 只需1根TX/RX |
同步方式 | 写控制线 + 地址线 | 起始位 + 波特率 |
速率控制 | 所有数据同时送出 | 每位按时钟节拍发出 |
结构复杂度 | 高(线多,成本高) | 简洁(线少,控制少) |