嵌入式软件-usart篇

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二进制
H0x480100 1000
E0x450100 0101
L0x4C0100 1100
O0x4F0100 1111

📦 传输过程(逐字符):

  1. MCU 把 "H" 的 8 位值 01001000 同时放到 D0~D7 数据线上;

  2. 控制线拉高:WR=1(写使能);

  3. 外设在同一时刻读取所有 8 位数据;

  4. 控制线拉低,准备下一个字符;

  5. 重复以上过程,依次送出 "E""L""L""O"

🔧 需要硬件支持:

  • 至少 8 根数据线;

  • 外设和主控要有并行接口芯片

  • 控制时序精确,否则容易读错。


✅ UART 串行通信发送 "HELLO"

👇 情景:

使用 UART 只需 1 根 TX 线发送,按照帧格式一位一位发送。

帧格式(常见 8N1):

  • 1 个起始位(低电平)

  • 8 个数据位

  • 1 个停止位(高电平)

📦 传输过程(以 "H" 为例):

ASCII "H" = 0x48 = 01001000

UART帧发送顺序为:起始位 000010010停止位 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 硬件控制器做了这件事

它在内部悄悄完成这些事:

  1. 在数据前面自动加一个起始位 0

  2. 然后把 01001000 每一位“排队发送”

  3. 最后再加一个停止位 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_DRUSART 写数据寄存器成功的数据被锁存,RXNE = 1
8️⃣ HAL 函数检测到 RXNEHAL 库从 DR 中取出这个字节,放进你准备的 rx_buf[]


2、你没写的、但实际发生的 “电路动作”:

  • 边沿检测器 检查 RX 是否变成低电平;

  • 定时器 精确采样每一位;

  • 移位寄存器 把采样来的0/1排成字节;

  • 校验逻辑 判断帧是不是完整;

  • RXNE标志位 表示“这字节已经到了”。

这些全部是 STM32芯片里的硬件逻辑电路完成的,不需要 CPU 运算。


3、总结一句话记忆法:

🧠 “我只是写了个接收函数,真正干活的是 USART 硬件,它能识别电平变换、按位接收、合成字节、通知我收好了。”


七、对比并行通信 vs UART 接收逻辑

对比项并行通信(Parallel)UART 串行通信
数据线数量多达8根以上只需1根TX/RX
同步方式写控制线 + 地址线起始位 + 波特率
速率控制所有数据同时送出每位按时钟节拍发出
结构复杂度高(线多,成本高)简洁(线少,控制少)

先写到这,后续继续总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值