开发板: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):
| 外设 | 所在总线 | 说明 |
|---|---|---|
| USART1 | APB2(最高 72MHz) | 高级同步/异步串行接口 |
| USART2 / USART3 | APB1(最高 36MHz) | 通用 |
| UART4 / UART5 | APB1 | 通用异步(无同步) |
一个巧妙的点:TDR 和 RDR 被映射到同一个物理地址(DR,偏移 0x04)。官方没披露具体实现,推测是通过读写操作类型(r / w)来区分访问哪个寄存器——写 DR 就是写 TDR,读 DR 就是读 RDR。USART_DR 虽然是 32 位,但实际只用 9 位(数据位最多 9 位)。
5.3 三个关键标志位(本章的灵魂)
| 标志位 | 全称 | 含义 |
|---|---|---|
| TXE | Transmit data registerEmpty | TDR 空了(数据已转到移位寄存器)→ 置 1,此时才能写下一个数据;写 DR 后自动清 0 |
| TC | TransmissionComplete | 一帧发完且 TDR 空 → 置 1,表示彻底发完了 |
| RXNE | Read data registerNot Empty | RDR 有数据 → 置 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 配置要点
- 开启 SWD 调试(老规矩,否则第二次烧录失败);
- HSE + 时钟树配 72MHz(别忘,见第 6 课);
- 配置 USART1:Mode 选Asynchronous(异步);波特率115200;字长9 Bits(8 数据 + 1 校验);奇校验(Odd);停止位1;无硬件流控;过采样16(默认即可)。使能 USART1 后 CubeMX 会自动把 PA9/PA10 配成 USART1_TX/RX;
- 配一个 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 /* ... 声明内容 ... */ #endif7.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 核心要点
- UART 四大特点对应四组概念:串行(≠UART)、异步(无时钟线)、1对1、全双工(独立 Tx/Rx)。
- 波特率 ≠ 比特率,但 USART 里相等:因为二进制码元一个码元只能带 1 bit。
- 帧格式是"约定":空闲高电平 → 起始位拉低(下降沿)→ 数据位 LSB 先发 → 校验位 → 停止位拉高。每帧都有起始位和停止位。
- 奇偶校验只能查奇数个错误:偶数个翻转查不出来,且不能纠错。
- 三个标志位是收发的心脏:TXE(能写)、RXNE(能读)、TC(发完)——阻塞模式就是轮询它们。
- 配置 USART 三件事:开 USART 时钟 + 配 GPIO 复用(PA9 复用推挽、PA10 输入)+ 配波特率/字长/校验/停止位。
- 阻塞收发两个接口:
HAL_UART_Receive/HAL_UART_Transmit,注意发送的 timeout 别传 HAL_MAX_DELAY(会因等 TC 丢数据)。 - printf 重定向 = 重写 fputc/fgetc:底层封装 + 过滤
\r,再勾选 MicroLIB。 - BSP 模块化三件套:
#ifndef防重复包含、extern "C"兼容 C++、加头文件搜索路径。 - 串口是嵌入式调试的生命线:printf 打日志 + 上位机发命令,是后续所有章节的标配工具。