news 2026/10/25 1:54:55

嵌入式入门(四)USART 串口深度解析:单片机第一次与电脑对话

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式入门(四)USART 串口深度解析:单片机第一次与电脑对话

开发板:STM32F103ZET6

工具链:Keil MDK + VSCode + STM32CubeMX + HAL 库

上一篇:嵌入式入门(三)RCC 与时钟树深度解析

前面几章,GPIO 让我们能"控制"外设、时钟让整个芯片"动"起来——但这一切都发生在芯片内部。从这一章开始,你的单片机第一次和外界对话:它能通过串口把数据发到电脑,也能接收电脑发来的指令。这是嵌入式开发的分水岭——串口是所有调试、日志、上位机交互的基石。

1:通信的基本概念

1.1 UART是什么

UART= Universal Asynchronous Receiver/Transmitter,通用异步收发器。它是一种用于设备间串行通信的硬件电路——数据按位顺序,一次传输 1 位,逐次发送。

1.2 UART怎么接线

UART 由2 根数据线 + 1 根地线组成:

线作用
Tx发送数据(Transmit)
Rx接收数据(Receive)
GND公共地

接线铁律:Tx 接对端 Rx,Rx 接对端 Tx,双方 GND 共地。因为要构建双向信道,且没有主从之分,地位对等。

助记:x 就是 exchange(交换)的意思,而 'x' 的形状本身就特别像 Tx/Rx 交叉相连的接法。

1.3 UART的四大特点

UART 有四个核心特点,对应四组易混概念:

1.3.1串行 vs 并行

串行并行
数据线1 条(或 1 对)多条(8/16/32 条)
传输方式一 bit 接一 bit 依次传一次传完所有位
缺点速度相对慢线多成本高、速率受限、距离短、扩展性差、功耗大

并行通信的致命伤:所有位必须同时有效才能被正确采样,但物理上做不到绝对同步,所以时钟反而要放慢;加上串扰严重,只能用于板级短距离。所以 STM32 通信领域,串行是绝对主流。

注意:串行 ≠ UART。SPI、I2C、CAN、UART 都是串行通信技术,UART 只是其中一种。顺带记名词:位(bit,最小单位)、字节(byte=8bit)、半字(16bit,uint16_t)、字(32bit,uint32_t,CPU 一次处理的单位)。

1.3.2同步 vs 异步

  • 同步通信:有一根独立的时钟线,靠时钟的上升沿/下降沿采样数据;
  • 异步通信:没有时钟线,靠收发双方约定好每个 bit 占用多长时间来区分每一位(对 UART 来说,这个约定就是波特率)。

核心判据:区分同步还是异步,只看有没有时钟线。UART 没有同步时钟线,所以是异步(A = Asynchronous)。

1.3.3一对一 vs 一对多

UART 一个串口单元只能和另一个串口单元通信(1对1)。1对多(一个主机带多个从机)是 I2C、SPI 的活,后面会讲。

1.3.4单工 / 半双工 / 全双工

模式传输方向类比
单工只能单向(A→B 或 B→A 二选一)吵架吵输了,我不说了,你说你的
半双工能双向,但同一时刻只能一个方向正常沟通:我说你听,你说我听
全双工同时双向(A→B 且 B→A)吵架:你说你的,我说我的,同时进行

UART 有独立的 Tx、Rx 线,所以是全双工。但注意:基于 UART 扩展的RS-485 是半双工。

2:USART与"四方关系"

2.1 USART比UART多了一个"同步"

STM32 手册里都是USART,不是 UART。区别:

  • UART:UniversalAsynchronousReceiver Transmitter(通用异步收发器);
  • USART:UniversalSynchronous AsynchronousReceiver Transmitter(通用同步异步收发器)。

USART 多了一个synchronous(同步)功能——可以额外给 UART 提供一根时钟线,实现同步传输。但这个同步串口模式应用场景极罕见,几乎不用。

结论:忽略同步功能后,USART == UART。入门阶段只学标准的异步通信,后续表述统一用 USART。

2.2四方关系:串口,MCU,GPIO,板子

串口是 MCU 的片上外设(挂在 APB 总线上),类似"网卡和网卡通信,但网卡属于你的电脑"。而 USART 要对外收发信号,必须复用 GPIO 引脚:

USART1(片上外设) ──复用── GPIO(PA9=TX, PA10=RX) ──引脚── CH340(USB转串口) ── USB ── 电脑

细节:配置 USART,一定伴随 GPIO 的配置(复用推挽输出/输入)。这也是第 5 章"复用 GPIO"概念的第一次实战落地。

2.3通信的本质是"约定"

设备间要真正通信,双方必须遵守相同的规则:每秒收发多少数据、一次发多少、哪里开始、哪里结束。就像人和人通信要都说普通话、不说方言。

约定好的规则 = 协议,在单片机通信里更喜欢叫"帧格式"。这条思想适用于 USART,也适用于未来所有通信方式。

3:比特率 vs 波特率

3.1 比特率(Bit Rate)

每秒传送的比特位个数,也叫传信率,侧重"传递有效信息的速度",单位bit/s(bps)。

一个程序员容易踩的坑:家里的"千兆宽带",这个"兆"是比特率,且是十进制的(K=1000、M=10⁶、G=10⁹,不是 1024)。所以 1000Mbit/s 实际下载速度 = 1000Mbit ÷ 8 =125MB/s(这里的 M 仍是 10⁶)。而文件大小里的 KB 才是二进制的(1KB = 1024B)。

3.2波特率(Baud Rate)

每秒传送的码元(符号)个数,也叫传码率,单位Baud(Bd)。

关键区别在"码元":一个码元可以承载 1 bit,也可以承载多个 bit。举例:

张三和李四约定扔球通信,不同颜色的球代表不同信息:蓝=00、绿=01、黄=10、红=11(共 4 种状态,用 2 bit 表示)。张三每秒扔 1 个球,波特率 = 1 Bd;但每个球(码元)携带 2 bit 信息,所以比特率 = 2 bps。

关系:比特率 = 波特率 × 每个码元携带的 bit 数。

3.3 为什么USART里的波特率=比特率

因为 USART 用高低电平两种状态传码元,属于二进制码元——一个码元只能携带 1 bit。所以波特率 = 比特率。这也是为什么很多工具设置波特率时单位写成 b/s 也不奇怪(虽然严格说不严谨)。

波特率最本质的含义:它决定了单个信号会持续多长时间——即"1 个 bit 占用的时间 T"。对发送方,"你打算多长时间发一个数据、持续发多久";对接收方,"你打算多长时间收一个数据"。这就是异步通信里"没有时钟线也能对齐每一位"的关键。

STM32 的 USART 常用波特率:9600 Bd或115200 Bd。通信双方设置相同的波特率,就解决了"收发快慢不一致"的问题。

4:USART帧格式

波特率解决了"快慢",但接收方还需要知道:数据什么时候开始?有多少位?什么时候结束?这就靠帧格式来约定。

4.1帧的五个组成部分

一帧 UART 数据(以最常用的 8 数据位 + 1 校验位 + 1 停止位为例)长这样:

空闲 起始 D0 D1 D2 D3 D4 D5 D6 D7 校验 停止 ──────┐ ┌─────┐ ┌───── 高电平 │ │ │ … 数据位(LSB 先发)… │ 低电平 └──┘ └───────────────────────────────┘ ↑1bit ← 下降沿标志新帧开始 → 发送 0x55 = 0101 0101(LSB first):先发 bit0=1,再 bit1=0 … 最后 bit7=0
部分电平作用
空闲态(Idle)高电平(逻辑 1)无数据时线路保持高电平
起始位(Start)低电平(逻辑 0),持续 1 bit下降沿(1→0)标志新帧开始,接收端靠它同步
数据位(Data)8 或 9 位承载实际数据,LSB 先发
校验位(Parity)可选简单错误检测
停止位(Stop)高电平,1 / 1.5 / 2 bit标志帧结束,之后回到空闲

几个要点:

  • 为什么空闲是高电平、起始位要拉低?因为线路默认高电平,那"开始"的标志就只能是拉低——用一次下降沿宣告"新帧来了"。
  • LSB first:最低有效位先发。比如发1011(十进制 11),实际发送顺序是1 1 0 1(先发 bit0=1)。这是为了让接收端能更快开始处理数据。
  • 停止位为什么必须拉高?如果上一帧发完不强制拉高,那下一帧的起始位(拉低)就可能没有"从高到低"的下降沿,无法被识别。强制拉高就保证了每帧之间至少有一个高电平间隔。让一切具有确定性,是 USART 追求的目标。

易错点:每一帧都必须有完整的起始位和停止位。发 5 个 8 位数据 = 5 个独立帧 = 5 个起始位 + 5 个停止位,而不是"8 个数据位打包一起发"。

4.2 奇偶校验:只能发现"奇数个"错误

校验位让"数据位 + 校验位"里 1 的个数保持奇(奇校验)或偶(偶校验):

  • 奇校验,数据11110000(4 个 1)→ 校验位设 1 → 总共 5 个 1(奇数);
  • 偶校验,数据11110000(4 个 1)→ 校验位设 0 → 总共 4 个 1(偶数)。

局限性:奇偶校验只能检测奇数个bit 翻转错误(1 位、3 位…),偶数个翻转(2 位、4 位)查不出来,也无法纠错。课件用"一袋苹果"打比方:偶校验约定"好苹果必须是偶数个",坏了 1 个(奇数)能发现,坏了 2 个(偶数)就误以为正常。

4.3 帧格式与波特率

帧结构1 起始 + 8 数据 + 0 校验 + 1 停止 = 10 bit:

9600 Baud → 9600 bps ÷ 10 bits/byte = 960 字节/秒 115200 Baud → 115200 ÷ 10 = 11520 字节/秒

这就是为什么 115200 比 9600 快得多——波特率越高,单位时间传的信息越多。

5:硬件结构与三个标志位

5.1 TDR,RDR与位移寄存器

USART 的原理框图很庞杂,但入门只看最核心的数据通路:

发送: CPU 写数据 ──写──> [TDR 发送数据寄存器] ──转移──> [移位寄存器] ──逐位──> Tx 引脚 ↑ TXE==1 才能写 ↑ 发完 TC=1 接收: Rx 引脚 ──逐位──> [移位寄存器] ──转移──> [RDR 接收数据寄存器] ──读──> CPU 读数据 ↑ RXNE==1 才能读

USART 有两个用户可见的数据寄存器——TDR(发送数据寄存器)和RDR(接收数据寄存器),而收发移位寄存器在硬件内部,对用户不可见。发送时:写 TDR → 自动平移到移位寄存器 → 逐位移出;接收时:逐位移入移位寄存器 → 收满自动拷贝到 RDR → 用户读 RDR。

5.2 5 个串口与寄存器地址

STM32F103ZET6 有5 个串口,USART1 地址0x4001 3800(1KB),7 个寄存器(SR/DR/BRR/CR1/CR2/CR3/GTPR):

外设所在总线说明
USART1APB2(最高 72MHz)高级同步/异步串行接口
USART2 / USART3APB1(最高 36MHz)通用
UART4 / UART5APB1通用异步(无同步)

一个巧妙的点:TDR 和 RDR 被映射到同一个物理地址(DR,偏移 0x04)。官方没披露具体实现,推测是通过读写操作类型(r / w)来区分访问哪个寄存器——写 DR 就是写 TDR,读 DR 就是读 RDR。USART_DR 虽然是 32 位,但实际只用 9 位(数据位最多 9 位)。

5.3 三个关键标志位(本章的灵魂)

标志位全称含义
TXETransmit data registerEmptyTDR 空了(数据已转到移位寄存器)→ 置 1,此时才能写下一个数据;写 DR 后自动清 0
TCTransmissionComplete一帧发完且 TDR 空 → 置 1,表示彻底发完了
RXNERead data registerNot EmptyRDR 有数据 → 置 1,此时才能读;读 DR 后自动清 0

为什么需要标志位?USART 发送不是一蹴而就的,是一步一步发的。如果用户在它没发完时又往 TDR 写数据,就会覆盖掉旧数据。所以 USART 必须提供状态信息,把"能不能写/能不能读"的责任告诉用户——这就是 TXE 和 RXNE 存在的意义。

5.4 阻塞模式

所谓阻塞式收发,就是轮询标志位:发送时检测 TXE,若TXE==0(TDR 忙)就一直卡在那等;接收时检测 RXNE,若RXNE==0(没数据)就一直等。这种"卡住等标志位"的模式就是阻塞(Blocking)。

6:实战:轮询模式收发

6.1 硬件:CH340K 当"翻译官"

开发板上用CH340K(USB 转 TTL 串口芯片)。为什么需要它?因为你的电脑(尤其笔记本)已经没有原生串口,而 STM32 只认 TTL 电平信号,二者无法直接对话,所以需要一个"翻译官"把 USB 转成 TTL 串口。

6.2 CubeMX 配置要点

  1. 开启 SWD 调试(老规矩,否则第二次烧录失败);
  2. HSE + 时钟树配 72MHz(别忘,见第 6 课);
  3. 配置 USART1:Mode 选Asynchronous(异步);波特率115200;字长9 Bits(8 数据 + 1 校验);奇校验(Odd);停止位1;无硬件流控;过采样16(默认即可)。使能 USART1 后 CubeMX 会自动把 PA9/PA10 配成 USART1_TX/RX;
  4. 配一个 LED 指示(如 PF8)表示数据传输。

扩展:过采样(OverSampling)是什么?它决定 USART 在一个位周期内对接收信号采样多少次来判断逻辑状态。16 倍过采样意味着每个 bit 采样 16 次(取中间几次最可靠的),能更抗噪声地识别数据位。这也是后面讲到的波特率计算公式PCLK / (16 × BaudRate)里那个 16 的由来。

6.3 代码实现(轮询回显)

核心就两个 HAL 接口:收 1 字节、再原样发回去,并让 LED 闪烁指示传输:

/* USER CODE BEGIN PV */ uint8_t recv_byte = 0; uint32_t cnt = 0; /* USER CODE END PV */ while (1) { if (HAL_OK == HAL_UART_Receive(&huart1, &recv_byte, 1, HAL_MAX_DELAY)) { // 这里有个坑:发送的 timeout 建议传 0,不要传 HAL_MAX_DELAY // 因为发送要等 TC==1 才算完成,若死等会让接收变慢,而发送端一直在发,就会丢数据 HAL_UART_Transmit(&huart1, &recv_byte, 1, 0); } cnt++; if (cnt >= 10) // 收到 10 个字节就翻转一次 LED { cnt = 0; HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_8); } }

那个 timeout 的坑:HAL_UART_Transmit的最后一个参数是超时时间。如果传HAL_MAX_DELAY(无限等待),发送函数会死等TC==1(全部发完)才返回——这期间接收被堵住,而发送方一直在发,数据就会丢失。所以这里传0,把数据丢给硬件后立即返回,回到接收循环。

6.4HAL接口详解

HAL_UART_Receive—— 阻塞接收指定数量的数据元素:

HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
  • huart:UART 句柄;pData:接收缓冲区指针;Size:要接收的元素个数(不是字节数);Timeout:超时时间(ms),HAL_MAX_DELAY表示无限等待。
  • 内部逻辑:轮询等待RXNE置位 → 从 DR 取数并按字长/校验掩码提取有效位 → 写入缓冲区,直到收满Size个。
  • 返回:HAL_OK收满 /HAL_TIMEOUT超时 /HAL_BUSY忙 /HAL_ERROR入参非法。

HAL_UART_Transmit—— 阻塞发送指定数量的数据元素:

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);
  • 内部逻辑:每发一个字节先等TXE置位 → 写入 DR → 递减计数;全部写完再等TC表示发送完成。
  • 返回:HAL_OK/HAL_TIMEOUT/HAL_BUSY/HAL_ERROR。

6.5 源码分析:初始化到底干了什么

6.5.1 USART1的物理地址与结构体

#define USART1 ((USART_TypeDef *)USART1_BASE) #define USART1_BASE (APB2PERIPH_BASE + 0x00003800UL) #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000UL) #define PERIPH_BASE 0x40000000UL typedef struct { __IO uint32_t SR; /*!< 状态寄存器,偏移 0x00 */ __IO uint32_t DR; /*!< 数据寄存器(TDR/RDR 共址),偏移 0x04 */ __IO uint32_t BRR; /*!< 波特率寄存器,偏移 0x08 */ __IO uint32_t CR1; /*!< 控制寄存器 1,偏移 0x0C */ __IO uint32_t CR2; /*!< 控制寄存器 2,偏移 0x10 */ __IO uint32_t CR3; /*!< 控制寄存器 3,偏移 0x14 */ __IO uint32_t GTPR; /*!< 保护时间和预分频寄存器,偏移 0x18 */ } USART_TypeDef;

这套"结构体指针映射物理地址"的写法,和 GPIO、RCC 一模一样。

6.5.2 初始化参数(CubeMX 图形配置翻译成的 C 代码):

static void MX_USART1_UART_Init(void) { huart1.Instance = USART1; /* 寄存器基地址 */ huart1.Init.BaudRate = 115200; /* 波特率 115200 */ huart1.Init.WordLength = UART_WORDLENGTH_9B; /* 字长 9 位(8 数据 + 1 校验) */ huart1.Init.StopBits = UART_STOPBITS_1; /* 停止位 1 位 */ huart1.Init.Parity = UART_PARITY_ODD; /* 奇校验 */ huart1.Init.Mode = UART_MODE_TX_RX; /* 收发使能 */ huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */ huart1.Init.OverSampling = UART_OVERSAMPLING_16; /* 16 倍过采样 */ if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } }

6.5.3 时钟与GPIO使能(Msplnit)——关键看这里,串口 + 配套引脚都要开:

void HAL_UART_MspInit(UART_HandleTypeDef* huart) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if(huart->Instance==USART1) { /* Peripheral clock enable */ __HAL_RCC_USART1_CLK_ENABLE(); // 使能串口时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // 使能串口配套引脚时钟 /**USART1 GPIO Configuration PA9 ------> USART1_TX PA10 ------> USART1_RX */ GPIO_InitStruct.Pin = GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出(TX) GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入(RX) GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); } }

三个要点串联了前面所有知识:①开 USART1 时钟(第 6 课 RCC);②PA9 配复用推挽(第 5 课"复用 GPIO");③PA10 配输入。USART 是挂在 APB2 上的,所以 USART1 用 PCLK2(72MHz)当波特率基准。

6.6 上位机工具:COMTool

验证需要上位机工具COMTool(sourceforge 免安装软件)。用它发数据给板子、看板子回传的数据。首次用记得把语言改成"中文简体",关闭不需要的页面只留"收发"页。

7:实战:printf 重定向与多文件模块化

7.1 嵌入式也能用printf吗

学 C 语言时大家都习惯用printf打印日志,但 MCU 没有标准显示器。还能用printf吗?能——因为嵌入式单板几乎都有串口,我们让printf通过串口打印,这就叫"重定向到串口"。

原理很巧妙:printf底层是通过调用fputc一次写一个字符的,scanf/fgets底层是通过fgetc一次读一个字符的。所以只要重写这两个底层函数,就能把printf的输出导向串口、把scanf的输入接到串口——上层printf/scanf的用法完全不用改。

7.2重写fputc/fgetc

int fputc(int ch, FILE *f) { uint8_t data = (uint8_t)ch; if (HAL_UART_Transmit(&huart1, &data, 1, HAL_MAX_DELAY) == HAL_OK) { return ch; } else { return EOF; } } int fgetc(FILE *f) { uint8_t ch = 0; while (1) { if (HAL_UART_Receive(&huart1, &ch, 1, HAL_MAX_DELAY) != HAL_OK) { return EOF; } // 忽略 \r,只保留 \n if (ch != '\r') { return (int)ch; } } }

为什么fgetc要过滤\r?因为 Windows 的换行是\r\n两个字符。串口发来"回车"时,上层(比如fgets)只认\n作换行,所以把\r过滤掉、只保留\n,避免读出来混入多余的\r。

7.3 MicroLIB:为什么烧进去没反应

重写了fputc/fgetc,烧录后却没有任何输出?原因:没有勾选MicroLIB,编译器用的是 C 默认的标准库行为(内部其实出错了,只是没报出来)。

MicroLIB是 ARM 为资源极度受限的设备(Cortex-M 微控制器)定制的迷你版标准 C 库,用来替代庞大的 Newlib/Glibc。要用printf,就必须在 Keil 的工程选项里勾选"Use MicroLIB"。

7.4模块化多文件:BSP

随着代码变多,把所有东西堆在main.c里会越来越乱。正确做法是按功能拆成模块:

  • BSP_Usart_Redir.c / .h:串口重定向驱动;
  • BSP_LED.c / .h:LED 驱动。

BSP = Board Support Package(板级支持包),负责把"硬件细节"封装起来,让应用层不用关心具体引脚——上一章按键那节我们已经见过这个思想,这次正式落地成独立文件。

几个工程化要点:

7.4.1 防重复包含(头文件固定骨架)

#ifndef __BSP_USART_REDIR_H #define __BSP_USART_REDIR_H /* ... 声明内容 ... */ #endif

7.4.2extern "C"(C++ 兼容):

#ifdef __cplusplus extern "C" { #endif /* 函数声明 */ #ifdef __cplusplus } #endif

为什么需要它?C++ 编译器会给函数"改名"(名字修饰,比如func变成_Z4funcv),而 C 编译器不改名。加上extern "C"就是告诉 C++ 编译器"别改名,按 C 的规则找函数",否则链接时会因为名字不匹配而报错。HAL 库头文件里也有它,因为用户可能用 C++ 开发。

7.4.3头文件搜索路径

#include的本质是把头文件拷贝进源文件,拷贝前编译器得先找到它,所以要在 Keil 的 C/C++ 选项里添加头文件所在路径。

7.4.4自己新建的文件不会被 CubeMX 覆盖

CubeMX 重新生成只会动它管理的文件,你在BSP_Drivers里自己建的文件是安全的。

7.5 串口控制LED灯:第一个完整闭环

最终效果:用串口发led1/led2/.../all命令,控制对应的 LED 闪烁——这是"上位机远程控制板子"的第一个完整闭环。

用fgets读一行命令,再用strncasecmp忽略大小写匹配:

void Usart_Ctl_LED() { char who[32]; fgets(who, 31, stdin); if (strncasecmp(who, "LED1", strlen("LED1")) == 0) { printf("LED1 开始闪烁...\n"); LED_Link(LED1); } if (strncasecmp(who, "LED2", strlen("LED2")) == 0) { printf("LED2 开始闪烁...\n"); LED_Link(LED2); } /* LED3 / LED4 / ALL 同理 */ }

走到这里你会发现:7-1 里那个手写的"按行读取"思路,正是fgets的标准行为——现在不用自己写 ReadLine 了,C 库已经帮你封装好了。

8:总结

8.1 知识体系回顾

通信基本概念 ├── 串行 vs 并行:STM32 通信主流是串行(串行 ≠ UART) ├── 同步 vs 异步:看有没有独立时钟线(UART 无 → 异步) ├── 1对1 vs 1对多:UART 是 1对1 └── 单工/半双工/全双工:UART 独立 Tx/Rx → 全双工 USART 与四方关系 ├── USART = UART + 同步(忽略同步则等价) └── 串口/MCU/GPIO/板子:USART 复用 GPIO(PA9=TX, PA10=RX) 比特率 vs 波特率 ├── 比特率 = 传信率(bit/s),宽带里的 K/M/G 是十进制 ├── 波特率 = 传码率(Baud),码元可承载多 bit └── USART 二进制码元 → 波特率 = 比特率 帧格式(五部分) ├── 空闲(高)→ 起始位(低,下降沿)→ 数据位(LSB 先发)→ 校验位 → 停止位(高) ├── 每帧都有起始位 + 停止位 └── 奇偶校验:只能查奇数个错误,不能纠错 硬件结构与标志位 ├── TDR / RDR + 移位寄存器(移位寄存器对用户不可见) ├── 5 个串口:USART1 挂 APB2(72MHz),其余挂 APB1(36MHz) ├── 标志位:TXE(TDR空)/ TC(发完)/ RXNE(RDR非空) └── 阻塞模式 = 轮询标志位 实战 ├── 7-1 轮询收发:HAL_UART_Receive / Transmit + CH340 + COMTool └── 7-2 重定向:重写 fputc/fgetc + MicroLIB + BSP 模块化

8.2 核心要点

  1. UART 四大特点对应四组概念:串行(≠UART)、异步(无时钟线)、1对1、全双工(独立 Tx/Rx)。
  2. 波特率 ≠ 比特率,但 USART 里相等:因为二进制码元一个码元只能带 1 bit。
  3. 帧格式是"约定":空闲高电平 → 起始位拉低(下降沿)→ 数据位 LSB 先发 → 校验位 → 停止位拉高。每帧都有起始位和停止位。
  4. 奇偶校验只能查奇数个错误:偶数个翻转查不出来,且不能纠错。
  5. 三个标志位是收发的心脏:TXE(能写)、RXNE(能读)、TC(发完)——阻塞模式就是轮询它们。
  6. 配置 USART 三件事:开 USART 时钟 + 配 GPIO 复用(PA9 复用推挽、PA10 输入)+ 配波特率/字长/校验/停止位。
  7. 阻塞收发两个接口:HAL_UART_Receive/HAL_UART_Transmit,注意发送的 timeout 别传 HAL_MAX_DELAY(会因等 TC 丢数据)。
  8. printf 重定向 = 重写 fputc/fgetc:底层封装 + 过滤\r,再勾选 MicroLIB。
  9. BSP 模块化三件套:#ifndef防重复包含、extern "C"兼容 C++、加头文件搜索路径。
  10. 串口是嵌入式调试的生命线:printf 打日志 + 上位机发命令,是后续所有章节的标配工具。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/25 1:52:24

Codex 本地运行更简单,一键包封装环境变量和依赖

前言 很多想体验 Codex 的开发者&#xff0c;卡在前期环境部署环节。手动安装 Node.js、配置依赖&#xff0c;还要敲大量命令行&#xff0c;对于新手十分不友好&#xff0c;很容易出现各种报错。 Codex 一键安装包把环境配置、依赖安装、客户端部署全部打包封装。不需要手动配…

作者头像 李华
网站建设 2026/10/25 1:51:53

企业级 AI 底层搭建详解:网关选型、部署与治理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/25 1:49:34

不用安装 Node,Codex 免配置一键包快速部署教程

前言 很多想体验 Codex 的开发者&#xff0c;卡在前期环境部署环节。手动安装 Node.js、配置依赖&#xff0c;还要敲大量命令行&#xff0c;对于新手十分不友好&#xff0c;很容易出现各种报错。 Codex 一键安装包把环境配置、依赖安装、客户端部署全部打包封装。不需要手动配…

作者头像 李华
网站建设 2026/10/25 1:49:33

Codex 一键安装包,免去 Node 环境配置烦恼

前言 很多想体验 Codex 的开发者&#xff0c;卡在前期环境部署环节。手动安装 Node.js、配置依赖&#xff0c;还要敲大量命令行&#xff0c;对于新手十分不友好&#xff0c;很容易出现各种报错。 Codex 一键安装包把环境配置、依赖安装、客户端部署全部打包封装。不需要手动配…

作者头像 李华