7-1 轮询模式数据收发
1. ⽬标
本⽰例将会演⽰ UART 阻塞模式数据发送、接收接⼝的使⽤。这类接⼝是最简单、直观的,⾮常适合 第⼀个⼊⻔类⽰例。由于是接⼝是阻塞模式,就必须以轮询的⽅式进⾏使⽤。
🎯 本⽰例将实现以下串⼝数据收、发逻辑:
-
串⼝配置为:波特率= 115200bps , 8 位数据,奇校验,停⽌位= 1 ,⽆流控。
-
使⽤串⼝阻塞模式接⼝接收串⼝上位机发送的测试数据。
-
使⽤串⼝阻塞模式接⼝将接收到的数据回传到串⼝上位机。
-
使⽤绿⾊ LED 指⽰灯闪烁状态表⽰串⼝正在进⾏数据传输。
2. 原理图分析
- 需要⼀个可以与电脑连接的串⼝。如 下图 所⽰是开发板的串⼝电路,该电路使⽤了⼀个 USB 转
USART 的芯⽚ CH340K 。这个转换电路就等效于 " USB 转 UART TTL " 模块,已经在电路
板上实现了转换功能,就不需要再使⽤额外的转换模块。
原因:你的电脑(尤其是笔记本)已经没有原⽣串⼝了,但开发板的主控芯⽚(如STM32)只认识
TTL串⼝电平信号,⼆者⽆法直接对话,所以需要⼀个"翻译官"

我们的板子上有usb转换成串口协议的CH340K模块
通过电脑 USB 与开发板的连接⽰意图如 下图 所⽰。虽然电脑使⽤的是 USB 接⼝,但对于软件应⽤ 来讲其实是串⼝,这是为了解决现在多数电脑都没有标配串⼝的问题。
3. 需求分析
-
需要⼀个绿⾊ LED 指⽰灯,⽤于指⽰串⼝数据交互状态
-
需要使⽤ USART 阻塞模式接收串⼝上位机发送的数据
-
需要使⽤ USART 阻塞模式发送数据到串⼝上位机⼯具
4. 学习⽬标
-
掌握串⼝基本参数的配置⽅法( CubeMX 相关配置以及代码实现 )
-
掌握串⼝阻塞模式发送接⼝的使⽤⽅法
-
掌握串⼝阻塞模式接收接⼝的使⽤⽅法
-
掌握串⼝上位机⼯具软件的使⽤⽅法
5. ⼯程配置
5.1 使⽤ CubeMX 创建⼯程
我们开发板的预留的仿真器调试接⼝是 SWD ( Serial Wire Debug ) ,在如下图中 所标识区域 选择 " Serial Wire " 。
🚧 注意! 这⼀步⾮常重要.
如果没有修改这⾥, 使⽤了默认的 No Debug, 第⼀次烧录程序可以成功, 但是后续烧录程序就
会烧录失败
🕶 DAP vs SWD DAP Link是⼀种调试器硬件,⽽SWD是⼀种调试通信协议;DAP Link通过SWD协议与⽬标芯 ⽚通信。
• DAP 全称是 Debug Access Port (调试访问端⼝),是⼀个物理仿真器
• SWD 全称是 Serial Wire Debug (串⾏线调试)
• 两者的关系类似
DAP=考试的时候坐在座位上的同学本⼈
SWD=⽤来传递纸条的通道+约定好的⼿势或者暗号
当在 CubeMX 中错误配置或禁⽌ debug 接⼝( 如: JTAG / SWD )时,烧录完程序后后导致⽆
法通过调试器连接⽬标 STM32 设备。就是说DAP LINK下载器需要在 SWD的协议下才能发挥作用
5.2 开启外部时钟源
在如下图所标识区域选择 " Crystal / Ceramic Resonator " 表⽰选择外部晶振做为 HSE
5.3 配置时钟树
我们开发板上是⼀个 8MHz 的 HSE 晶振,如下图标识区域所⽰,需要选通 8MHz 晶振作为时钟 源,并将系统运⾏最⼤频率配置到 72MHz 。
5.4 配置USART1
在通信部分选择 USART1 ,选择异步通信模式, STM32CubeMX 默认就选择了对应的针脚(为什
么?直接回答)
使能了串⼝就会⾃动配置对应的管脚 ⻅附录《针脚复⽤清单》 
在串口这里我们配置异步通信,这个也是串口模块常用的通信配置,GPIO配置使⽤默认的即可,现场确认参数模式
配置 USART1 相关参数 
如上所⽰串⼝的基本参数配置项,各配置项的含义如下:
• 图中 ① 标识区域,配置为异步模式 ( Asynchronous ) 。 STM32 的 USART 还⽀持很多其他的
模式,最核⼼的 UART 通信模式就选择 Asynchronous 模式。
• 图中 ② 标识区域,不使⽤硬件流控制信号,只使⽤了 Rx / Tx 信号。
• 图中 ③ 标识区域,配置波特率为 115200 ,这个波特率是⼀个⾮常常⻅的值。
• 图中 ④ 标识区域,数据⻓度配置为 9Bits ,其中 8Bits 是数据 1Bits 是校验位。
• 图中 ⑤ 标识区域,配置为奇校验模式( Odd )。
• 图中 ⑥ 标识区域,配置停⽌位占 1 位 ( 时间⻓度 )。
• 图中 ⑦ 标识区域,配置为数据收发模式,STM32 的 USART 还⽀持配置成 "只接收" 或 "只发
送" 模式,通常直接配置为收发模式( Receive and Transmit )即可。
• 图中 ⑧ 标识区域,使⽤默认配置值即可。过采样( OverSampling ) 是 USART 接收器⽤来检
测和识别输⼊信号的技术。它决定了 USART 在⼀个位周期内对接收信号进⾏多少次采样来判断该
位的逻辑状态。
5.5 配置指⽰ LED 灯
就选择LED1吧,这个不解释
5.6 项⽬配置并⽣成
6. 代码编写
cpp
代码块
1 /* USER CODE BEGIN PV */uint8_t recv_byte = 0;
uint32_t cnt = 0;
/* USER CODE END PV */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if (HAL_OK == HAL_UART_Receive(&huart1, &recv_byte, 1, HAL_MAX_DELAY))
{
// HAL_Delay(100); // bug测试
// 这⾥有坑,为了看到效果,不建议设置为HAL_MAX_DELAY,因为发送要等待TC==1,这样会
导致HAL_UART_Receive接收变慢,但是发送端是⼀直在发送的
// 这样可能会丢失数据,设置为0,直接交给硬件然后⽴即返回进⾏接收
HAL_UART_Transmit(&huart1, &recv_byte, 1, 0);
} c
nt++;
if (cnt >= 10) // 收到10个字节就翻转⼀下
{
cnt = 0;
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_8);
} /
* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
这里是收发10次收发以后进行电平的反转,细节就是发送的时候不可以用HAL_MAX_DELAY因为这样会导致接收的时间不够,数据接受不完整。
6.1.2 HAL_UART_Receive 函数
HAL库函数 HAL_UART_Receive 的主要核⼼逻辑如下:
在接收数据个数还未达到期望值 ( Size ) 时,通过轮询等待接收缓冲⾮空标志 RXNE 置位(1),⼀旦
置位(1)即从 DR 寄存器取数并按字⻓ / 校验掩码提取有效位( 9 位⽆校验⽤ 0x01FF , 9 位或
8 位有校验⽤ 0x00FF , 8 位有校验⽤ 0x007F ),写⼊⽤⼾缓冲区、移动指针并递减计数直
⾄接收完成。
该函数的具体功能与⽤法
✨ 函数 HAL_UART_Receive 说明: 1
. 函数概述
以阻塞⽅式接收指定数量的数据元素( 逐个元素等待、逐个元素读取 )。
2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 2 . 函数原型
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef
*huart,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
- 参数说明
◦ huart : UART 句柄,需已完成初始化并处于就绪状态。
◦ pData :接收缓冲区指针。 9 位⽆校验时按 uint16_t* 使⽤,否则按
uint8_t* 使⽤。
◦ Size :要接收的数据元素个数( 不是字节数 )。 9 位⽆校验时是 u16 元素个
数,其它情形是 u8 元素个数。
◦ Timeout :超时时间( 毫秒 )。与 HAL_GetTick() 配合判定;
HAL_MAX_DELAY 表⽰⽆限等待。
4 . 返回值
◦ HAL_OK :接收成功( 读满 Size 个元素 )。
◦ HAL_TIMEOUT :在等待 RXNE 标志期间超时。
◦ HAL_BUSY :接收状态⾮就绪( 已有接收过程正在进⾏ )。
◦ HAL_ERROR :⼊参⾮法( pData == NULL 或 Size == 0 )。

第一个参数 是串口的句柄,pDate是输入的参数数组,Size是传入的字节长度,Timeout是经过的最长时间长度,我们读取完数据寄存器DR会自动清空,这个步骤是硬件帮我们完成的。

拆开讲(STM32 F1 的 USART)
- **CPU 这边:写 TDR,走 APB 时钟**
STM32F1 的 USART 挂载在 APB2(USART1)/APB1(USART2/3)。
CPU 内核通过总线,**以 APB 时钟速度**,往 `USART_DR(HAL里叫TDR)` 写入 1 字节数据。
这个动作只是一次寄存器写入,是 CPU 总线操作,非常快。
- **USART 移位发送:波特率时钟**
USART 内部有一个波特率发生器(由 APB 时钟 + BRR 分频得到),产生**移位时钟**。
移位寄存器按照这个波特时钟,一位一位把 bit 推到 TX 引脚,比如 9600 波特就是每约 104us 出 1bit。
两个时钟源来源:都源自 APB,但频率完全解耦:
APB 时钟(比如 72M)是给寄存器访问用;波特时钟是 APB 经过 BRR 分频后的低速时钟(9600、115200)。
为什么可以分开跑(发送缓存机制)
USART 发送有两级缓冲:
`TDR(DR) ←→ 移位寄存器`
-
CPU 写数据到 TDR,只要 TDR 不为空,`TXE`标志位就清零;
-
一旦**移位寄存器空了,硬件自动把 TDR 里面的数据搬运进移位寄存器,然后置 `TXE=1`,通知 CPU:可以写下一个字节了。
重点:
移位寄存器在慢悠悠按波特率往外发 bit 的时候,CPU 完全可以继续往 TDR 填下一包数据(只要 TXE 标志允许)。
也就是:
移位发 bit(波特时钟驱动) 和 CPU 写 TDR(内核 / APB 总线)是并行的,不是等发完这一整帧 CPU 才能写第二个。
举个例子(115200,1 字节 10bit:起始 + 8 数据 + 停止)
发 1 字节大概需要 10/115200 ≈ 86.8us。
但是 CPU 写 TDR 寄存器,72M APB 下,只需要**几个 ns**就写完了。
只要 TXE 标志及时置 1,CPU 可以连续填充,不需要等待一帧发送完成。
和你之前代码的关联
-
轮询发送 `HAL_UART_Transmit`:里面循环等 `TXE`,等 TDR 空才继续填;
-
如果你理解这个两级缓冲,就能明白:为什么 USART 支持**连续发送、流水线**,中断发送就是利用 `TXE` 中断,在移位发送的同时 CPU 提前填充 TDR。
容易踩的误区
❌ 不是两套完全独立的晶振,**根时钟都是 APB**,但是经过分频之后,**工作节拍相互独立**,寄存器写入和 bit 移位可以同时进行,互不等待。
❌ 不是写 TDR 就直接送到 TX 引脚,中间隔了移位寄存器,这就是并行的根源。
cpu向TDR写入的时候和usart移位的时钟是不一样的,可以分别进行工作,就是向DR写入的速率是APB2决定的速度很快,向串口发送的移位寄存器里面写是是经过分频的速度慢
统一编制,和寄存器的顺序严格对应
6.1.3 HAL_UART_Transmit 函数
库函数 HAL_UART_Transmit 的主要核⼼逻辑如下:
根据配置选择按 uint16_t ( 9 位⽆校验,掩码 0x01FF )或 uint8_t ( 其余情况,掩码
0xFF )发送;每次发送循环先等待 TXE 标志置位,再将当前数据写⼊ DR 寄存器并递减计数;
全部写完后再等待 TC 标志表⽰发送完成。
该函数的具体功能与⽤法 ✨ 函数 HAL_UART_Transmit 说明: 1
. 函数概述
以阻塞⽅式发送指定数量的数据元素,逐元素等待、逐元素写寄存器。
- 函数原型
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef
*huart,
const uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
3 . 参数说明
◦ huart : UART 句柄,需已初始化且处于就绪状态。
◦ pData :待发送数据缓冲区指针。 9 位⽆校验时按 const uint16_t* 解析,
否则按 const uint8_t* 。
◦ Size :要发送的数据元素个数( ⾮字节数 )。 9 位⽆校验时是 u16 元素数,其
余是 u8 元素数。
◦ Timeout :超时时间(毫秒)。与 HAL_GetTick() 配合判定;
HAL_MAX_DELAY 表⽰⽆限等待。
4 . 返回值
◦ HAL_OK :成功发送完 Size 个元素并等待 TC 完成。
◦ HAL_TIMEOUT :等待 TXE 或 TC 期间超时。
◦ HAL_BUSY :发送状态⾮就绪( 已有发送正在进⾏ )。
◦ HAL_ERROR :⼊参⾮法( pData == NULL 或 Size == 0 )。
细节:
• 这两个函数就是阻塞式串⼝收发数据的函数
cpp
typedef struct
{
uint32_t BaudRate; // 波特率
uint32_t WordLength; // 数据位长度
uint32_t StopBits; // 停止位
uint32_t Parity; // 校验位
uint32_t Mode; // 收发模式
uint32_t HwFlowCtl; // 硬件流控
uint32_t OverSampling; // 过采样
} UART_InitTypeDef;
cpp
typedef struct
{
__IO uint32_t SR; /*!< USART 状态寄存器,地址偏移:0x00 */
__IO uint32_t DR; /*!< USART 数据寄存器,地址偏移:0x04 */
__IO uint32_t BRR; /*!< USART 波特率寄存器,地址偏移:0x08 */
__IO uint32_t CR1; /*!< USART 控制寄存器1,地址偏移:0x0C */
__IO uint32_t CR2; /*!< USART 控制寄存器2,地址偏移:0x10 */
__IO uint32_t CR3; /*!< USART 控制寄存器3,地址偏移:0x14 */
__IO uint32_t GTPR; /*!< USART 保护时间与预分频寄存器,地址偏移:0x18 */
} USART_TypeDef;
这个就是我们的句柄定义成的结构体,在这个其中我们关注第一个和第二个结构体,第一个结构体里面定义着串口的各种寄存器,第二个结构体里面是我们要配置的各种参数
✅8 位无校验 / 9 位模式:&0xFF,读取全部低 8 位 • ✅8 位带奇偶校验:&0x7F,剔除最高位的校验位,只拿 7 位有效数据
`HAL_UART_Receive(&huart1, &recv_byte, 1, HAL_MAX_DELAY)` 阻塞读一个字节,本质就是**循环等待 SR 寄存器的 RXNE 标志位(第 5 位)变成 1**;RXNE=1 代表硬件收到数据放到 DR 寄存器了,这时才去读 DR 拿数据。
一层一层往下拆解
1. 调用 `UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout)`
参数含义:
-
`huart`:串口句柄
-
`UART_FLAG_RXNE`:要等待的标志,就是 SR 寄存器里的 RXNE 位
-
`RESET`:我们要等到【这个标志不等于 RESET,也就是变成 SET (1)】
>
> 函数逻辑:只要标志 == RESET(0),就一直循环等,超时就退出返回失败。
2. 里面的 while 循环
while ((__HAL_UART_GET_FLAG(huart, Flag) ? SET : RESET) == Status)翻译成大白话:
只要 `__HAL_UART_GET_FLAG` 判断出来 RXNE 是 0(没收到数据),就一直卡在 while 循环里。
3. 宏 `__HAL_UART_GET_FLAG`
#define __HAL_UART_GET_FLAG(HANDLE, FLAG) (((HANDLE)->Instance->SR & (FLAG)) == (FLAG))
拆解:1. `HANDLE->Instance->SR`:访问串口状态寄存器 SR(就是前面 USART_TypeDef 里第一个寄存器)
-
`& FLAG`:和 RXNE 对应的掩码做按位与
-
判断结果是不是等于掩码:
-
等于 → RXNE 位是 1,返回 true(SET)
-
不等于 → RXNE 位是 0,返回 false(RESET)
4. RXNE 是什么(图里的方框)
RXNE 是 SR 寄存器的第 5 位:
-
**RXNE = 1**:移位寄存器已经把收到的数据搬到 DR 数据寄存器里了,现在 DR 里有有效数据,可以读了
-
**RXNE = 0**:还没有收到新数据,DR 是空的
>
> 重点特性:**一旦你读取 DR 寄存器,硬件会自动把 RXNE 清零**,准备接收下一个字节。
5. 宏定义关系
#define UART_FLAG_RXNE ((uint32_t)USART_SR_RXNE)
#define USART_SR_RXNE_Pos (5U)
#define USART_SR_RXNE_Msk (0x1UL << USART_SR_RXNE_Pos)
#define USART_SR_RXNE USART_SR_RXNE_Msk
就是把 1 左移 5 位,也就是二进制 `100000`(0x20),对应 SR 的 bit5。
6. 超时机制
`UART_WaitOnFlagUntilTimeout` 不是死等:
-
拿进入函数时的系统滴答时间 `tickstart`
-
while 每一轮都会对比当前时间,如果超过设定的 Timeout 时间,直接跳出循环,返回非 HAL_OK,上层就知道接收超时失败。
你这里写 `HAL_MAX_DELAY`,就是无限等,永远不会超时。
完整流程串起来(接收 1 字节)
-
调用 `HAL_UART_Receive`
-
进入等待函数,不停读 `huart->Instance->SR`,检查 bit5(RXNE)
-
外部发数据过来,串口硬件收到,硬件自动把 RXNE 置 1
-
`__HAL_UART_GET_FLAG` 检测到 RXNE=1,跳出 while 循环
-
HAL 底层代码去读取 `huart->Instance->DR`,把数据拿出来存到 `recv_byte`
-
✅ 读 DR 这个动作,硬件自动清除 RXNE 标志,准备收下一个字节
和你上一段代码的关联
跳出等待之后,马上就会执行你上一张图那段判断:
判断是 9 位 / 8 位无校验 / 8 位带校验,然后用 `0xFF` 或者 `0x7F` 掩码,从 DR 里把有效数据摘出来,丢掉校验位。
补充两个容易踩坑的知识点
-
RXNE 清零方式:读 DR 自动清零,这是硬件行为,不用你手动写寄存器清
-
阻塞接收的缺点:卡在 while 循环的时候,CPU 啥别的事情都干不了,这就是阻塞
结论:如果SR寄存器里面是RESET结果是1就是等效SET!=RESET(state)就退出while循环,如果是SET就一直循环,UART_WaitOnFlagUntilTimeout 有这个函数就也不是死等
和上面的逻辑是一样的一直检测sr的第七位是否是1
最后还需要等待tc标志位是1才是全部发完
加延时就会出现数据丢失的情况
cpp
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;
}
}
}
`while(1)` 死循环
目的:**过滤掉回车符 `\r`**
很多串口工具(Windows)换行是 `\r\n` 两个连续字节:回车 + 换行
-
`\r` = CR 回车,ASCII 0x0D
-
`\n` = LF 换行,ASCII 0x0A
这段代码逻辑:**收到 `\r` 就丢掉,继续循环等下一个字符;其他字符直接返回**
效果:Windows 发的 `\r\n`,经过这个函数之后,`\r` 被吞掉,上层程序只会收到 `\n`,统一换行符,避免识别两行换行。
7. 串⼝上位机⼯具使⽤介绍
7.1.1 软件安装
下载链接:https://sourceforge.net/projects/comtool/ 该软件是免安装的,将软件包解压缩开即可。如 下图 所⽰,解压缩后: comtool_windows_vxx / comtool / comtool.exe 就是可执⾏⽂件,双击即可打开软件。 
7.1.2 第⼀次启动配置
1. 将语⾔修改为中⽂,如 下图 所⽰选择 "中⽂ 简体" 。
2. 将不使⽤的⻚⾯关掉
如 下图 所⽰,可以将 "协议"、"终端"、"图标" ⻚⾯关闭掉,只留下 "收发" ⻚⾯。
7.1.3 ⼯作界⾯介绍

-
如 上图 中 ① 标识区域,是串⼝参数的配置区域,学习过串⼝基础知识后这些参数⼀⽬了然。
-
如 上图 中 ② 标识区域,是串⼝接收设置区域,例如:数据格式( ⼆进制、字符串 ),⾃动换⾏, 显⽰时间戳。
-
如 上图 中 ③ 标识区域,是串⼝发送设置区域,数据格式( ⼆进制、字符串 ),转义字符⾃动解 析,循环发送等。
-
如 上图 中 ④ 标识区域,是历史发送数据缓存区域。 5. 如 上图 中 ⑤ ⑦ 标识区域,是数据发送区域,包括数据区和发送按钮。
-
如 上图 中 ⑥ ⑧ 标识区域,是数据接收区域,包括接收到的数据显⽰区域和显⽰区域清除按钮。
8. 下载验证
8.1 ⽰例验证
1. 把编译好的程序下载到开发板并复位。
- 上位机按照 下图 所⽰配置( 串⼝号请根据电脑识别结果选择 )。
3. 输⼊任何测试字符串,点击 " 发送 " 按钮,开始发送测试数据,可以看到如 下图 接收显⽰区域所⽰的每次发送都有⼀条相同的数据回传。
9. 源码分析
9.1 Usart1物理地址
cpp
#define USART1 ((USART_TypeDef *)USART1_BASE)#define USART1_BASE (APB2PERIPH_BASE + 0x00003800UL)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000UL)
#define PERIPH_BASE 0x40000000UL /*!< Peripheral base address in the
alias region */
typedef struct
{
__IO uint32_t SR; /*!< USART Status register,
Address offset: 0x00 */
__IO uint32_t DR; /*!< USART Data register,
Address offset: 0x04 */
__IO uint32_t BRR; /*!< USART Baud rate register,
Address offset: 0x08 */
__IO uint32_t CR1; /*!< USART Control register 1,
Address offset: 0x0C */
__IO uint32_t CR2; /*!< USART Control register 2,
Address offset: 0x10 */
__IO uint32_t CR3; /*!< USART Control register 3,
Address offset: 0x14 */
__IO uint32_t GTPR; /*!< USART Guard time and prescaler register,
Address offset: 0x18 */
} USART_TypeDef;
串口是片上外设和我们之前讲的统一地址偏置原则是相符合的
9.2 初始化
9.2.1 结构体初始化
cpp
代码块
static void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1; /* UART 寄存器基
地址 = USART1 */
huart1.Init.BaudRate = 115200; /* 波特率 115200
*/
huart1.Init.WordLength = UART_WORDLENGTH_9B; /* 字⻓ 9 位(含校
验),有效数据 8 位 */
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 倍过采样 */
f (HAL_UART_Init(&huart1) != HAL_OK) /* 调⽤ HAL 初始
化,失败则进⼊错误处理 */
{
Error_Handler();
} /
* USER CODE BEGIN USART1_Init 2 */
/* USER CODE END USART1_Init 2 */
}
cpp
代码块
/**
* @brief UART 句柄结构体定义
* HAL 库使⽤该结构体管理 UART 外设的所有通信状态和参数
*/
typedef struct __UART_HandleTypeDef
{
USART_TypeDef *Instance; /*!< UART 寄存器基地址指针,指
向 USART1/USART2/... 外设寄存器 */
UART_InitTypeDef Init; /*!< UART 通信参数配置:波特率、
数据位、校验位、停⽌位等 */
const uint8_t *pTxBuffPtr; /*!< 指向 UART 发送缓冲区的指针
(要发送的数据源) */
uint16_t TxXferSize; /*!< UART 本次发送的总字节数 */
__IO uint16_t TxXferCount; /*!< UART 当前剩余待发送字节数
(递减计数器) */
uint8_t *pRxBuffPtr; /*!< 指向 UART 接收缓冲区的指针
(存放接收到的数据) */
uint16_t RxXferSize; /*!< UART 本次接收的总字节数 */
__IO uint16_t RxXferCount; /*!< UART 当前剩余待接收字节数
(递减计数器) */
__IO HAL_UART_RxTypeTypeDef ReceptionType; /*!< 当前正在进⾏的接收类型(单次
接收/连续接收) */
__IO HAL_UART_RxEventTypeTypeDef RxEventType; /*!< 接收事件类型(⽤于 DMA 传输
完成事件等) */
DMA_HandleTypeDef *hdmatx; /*!< UART 发送 DMA 句柄指针,使
⽤ DMA 发送时有效 */
DMA_HandleTypeDef *hdmarx; /*!< UART 接收 DMA 句柄指针,使
⽤ DMA 接收时有效 */
HAL_LockTypeDef Lock; /*!< 资源锁定对象,防⽌ HAL 函数
被重⼊(中断嵌套冲突) */
__IO HAL_UART_StateTypeDef gState; /*!< UART 全局状态(包括发送状
态):
例如:
HAL_UART_STATE_READY / BUSY / TIMEOUT 等 */
__IO HAL_UART_StateTypeDef RxState; /*!< UART 接收状态:
例如:
HAL_UART_STATE_READY / BUSY_RX / TIMEOUT 等 */
__IO uint32_t ErrorCode; /*!< UART 错误码,记录发⽣过的错
误类型(如溢出、帧错误、噪声等) */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
/* 以下为回调函数指针,当启⽤回调注册功能时可⽤ */
void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 发送半完成回调(DMA 半传输中断) */
void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 发送完成回调 */
void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 接收半完成回调(DMA 半传输中断) */
void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 接收完成回调 */
void (* ErrorCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 错误回调 */
void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 中⽌操作完成回调 */
void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 中⽌发送完成回调 */
void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 中⽌接收完成回调 */
void (* WakeupCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 唤醒回调(低功耗模式唤醒) */
void (* RxEventCallback)(struct __UART_HandleTypeDef *huart, uint16_t Pos);
/*!< UART 接收事件回调(⽤于中断接收时每收到⼀个字节触发) */
void (* MspInitCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 硬件初始化回调(MSP 层) */
void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart); /*!<
UART 硬件反初始化回调(MSP 层) */
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
} UART_HandleTypeDef
我们现在就重点关心结构体里面的不看DMA等相关的
cpp
代码块
/**
* @brief UART 初始化结构体,⽤于配置 UART 通信参数
*/
typedef struct
{
uint32_t BaudRate; /*!< 波特率,计算公式:IntegerDivider = PCLK / (16 *
BaudRate) */
uint32_t WordLength; /*!< 数据位数,可选 @ref UART_Word_Length */
uint32_t StopBits; /*!< 停⽌位数量,可选 @ref UART_Stop_Bits */
uint32_t Parity; /*!< 校验模式(⽆/奇/偶),可选 @ref UART_Parity
@note 使能校验时,校验位插⼊ MSB 位置 */
uint32_t Mode; /*!< 收发模式使能,可选 @ref UART_Mode */
uint32_t HwFlowCtl; /*!< 硬件流控使能,可选 @ref
UART_Hardware_Flow_Control */
uint32_t OverSampling; /*!< 过采样倍数(8 或 16),可选 @ref
UART_Over_Sampling */
} UART_InitTypeDef;
这个是句柄中第二个结构体定义
9.2.2 标志位选项设置
调1到2个标志位选项设置寄存器的过程即可,现场随便找
HAL_UART_Init
UART_SetConfig(huart); // 在这⾥
9.2.3 USART1时钟使能
我必须看到USAER1被使能,开始⼯作,并且,必须保证配套的GPIOx也必须被使能
cpp
代码块
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(huart->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/* 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;
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;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
}
它复用的GPIO不会在MX_GPIO里面初始化,会在串口初始化里面进行,这样也是减少耦合性
9.3 阻塞接收
核⼼代码就两⾏,搞定就能懂⾮常简单
cpp
代码块
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart,
Timeout) != HAL_OK)
. 字⻓ = 9B(含校验):帧结构为 8位数据 + 1位校验,有效数据在 DR[7:0] * 2. 字⻓ =
8B 且⽆校验:帧结构为 8位数据,有效数据在 DR[7:0]
if ((huart->Init.WordLength == UART_WORDLENGTH_9B) || ((huart->Init.WordLength
== UART_WORDLENGTH_8B) && (huart->Init.Parity == UART_PARITY_NONE)))
10. 轮询模式使⽤⽅法总结 
11.未来期许:
在下期我们将讲述串口的重定向工作,也就是第一个通讯模块串口的完结篇,我们在本章节主要讲述了串口初始化的底层代码,收发函数的参数配置,我们在后续的通讯模块会时刻拿出串口来进行对比。