0%

STM32——串口通信,用电脑控制三色LED

硬件原理

串口通信基础

串口(UART,Universal Asynchronous Receiver/Transmitter)是嵌入式领域最常用的通信方式之一。它的原理其实很简单——用两根数据线就能实现两个设备之间的双向通信。

1
2
3
4
设备A               设备B
TX ──────────────── RX
RX ──────────────── TX
GND ─────────────── GND

串口通信的几个关键点:

概念 说明
TX / RX 发送引脚(Transmit)和接收引脚(Receive),交叉连接——A的TX接B的RX,A的RX接B的TX
共地(GND) 两设备的参考地要连在一起,否则电平基准不同,无法正常通信
波特率 每秒传输的比特数,两设备必须设置相同的波特率才能通信,常用值有 9600、115200
数据帧 通常由 1位起始位 + 8位数据位 + 1位停止位 组成,所以每传1字节实际需要10个比特

开发板硬件连接

我的 keysking 开发板上,串口转USB芯片(CH340)已经连接到了 USART2,并引出一个 Type-C 口。只需一根 Type-C 数据线(就是手机充电线)插到电脑上,就能同时给板子供电和进行串口通信。

三个 LED 的连接和之前一样:

LED 引脚 控制信号
🔴 红色 PB0 GPIO 高电平亮
🟢 绿色 PA7 GPIO 高电平亮
🔵 蓝色 PA6 GPIO 高电平亮

CubeMX 配置

新建项目

芯片选择 STM32F103C8T6,步骤和前几篇一样,略。

起手式:配置调试接口

在 System Core → SYS 中,将 Debug 设为 Serial Wire。

配置 USART2

前几篇的工程主要和 GPIO 打交道,这次接触了一个新外设——USART(Universal Synchronous/Asynchronous Receiver Transmitter,通用同步/异步收发器)。

在 Connectivity → USART2 中,将模式设为 Asynchronous(异步通信):

参数 值
Mode Asynchronous
Baud Rate 115200
Word Length 8 Bits(含 Parity)
Parity None
Stop Bits 1

image-20260729131113313

波特率(Baud Rate) 是串口通信最关键的参数,它定义了每秒钟传输多少个比特(bit)。

波特率必须一致,否则无法通信

串口通信是异步的——TX 和 RX 之间没有独立的时钟线来同步节奏。那两边怎么对齐”一个比特”的时间长度呢?

答案就是波特率。

发送端和接收端各自按事先约定好的波特率来”打拍子”:

1
2
3
4
5
发送端(115200波特率):|   8.68μs   |   8.68μs   |   8.68μs   |   ...
比特0 比特1 比特2

接收端(115200波特率):|   8.68μs   |   8.68μs   |   8.68μs   |   ...
比特0 比特1 比特2

两边都以 115200 的速率采样,就能把每个比特正确解码。

那如果波特率不一致呢? 比如发送端是 115200,接收端误设成了 9600:

1
2
3
发送端(115200):比特0  比特1  比特2  比特3  比特4  比特5  比特6  比特7
| 8.7μs| 8.7μs| 8.7μs| 8.7μs| 8.7μs| 8.7μs| 8.7μs| 8.7μs|
接收端(9600): |       104μs       |

接收端一个比特的时间窗口(104μs)比发送端实际发送一个比特的时间(8.7μs)长得多,结果就是——一个波特率采样周期里吞进了多个比特,解码出来的数据全是乱的。

实际调试中也很容易遇到这个问题。第一次打开串口助手时,如果收到的不是可读的文字,而是一堆乱七八糟的符号,先检查波特率是不是设对了——这是最常犯的错误。

数据帧结构与实际传输速率

串口传输 1 个字节(8 位数据),实际发出去的并不只是 8 个比特,而是这样的一个数据帧:

1
2
3
4
5
6
  起始位        8位数据(从低位到高位)         停止位
     |                                       |
     ↓                                       ↓
  ┌────┬──┬──┬──┬──┬──┬──┬──┬──┬──┬────┐
  │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 1 │
  └────┴──┴──┴──┴──┴──┴──┴──┴──┴──┴────┘
组成 长度 说明
起始位 1 bit 拉低电平,告诉接收端”要开始发了”
数据位 8 bit 真正的数据(从最低位开始传)
停止位 1 bit 拉高电平,告诉接收端”这一帧结束了”

传 1 字节实际需要发 10 个比特(1起始 + 8数据 + 1停止)。

所以 115200 波特率下:

1
115200 ÷ 10 = 11520 字节/秒

一秒大约能传 1 万多个字节,对于发送几个字节的指令来说绰绰有余。

常见的波特率

波特率 每秒理论传输字节数 适用场景
9600 960 字节 长距离通信、低速设备(GPS模块等)
38400 3840 字节 一般传感器通信
115200 11520 字节 最常用,STM32 调试默认值
921600 92160 字节 高速通信(需短线、低干扰环境)

115200 是 STM32 学习中最常用的波特率,它足够快,又不会因为线太长或干扰而出错。

配置完成后,可以看到 PA2(TX)和 PA3(RX)被自动设置为 USART2 的收发引脚。

配置 LED 引脚(GPIO 输出)

三个 LED 的 GPIO 配置和之前完全一样:

引脚 功能 User Label 其他参数
PB0 GPIO_Output LED_RED 推挽输出、无上下拉、低速
PA7 GPIO_Output LED_GREEN 推挽输出、无上下拉、低速
PA6 GPIO_Output LED_BLUE 推挽输出、无上下拉、低速

生成代码

Project Manager 设置:

设置项 值
Project Name Serial
Toolchain / IDE CMake

点击 Generate Code,用 VS Code 打开。


VS Code 中编写核心代码

main.h 中的宏定义

CubeMX 根据 User Label 生成了以下宏:

1
2
3
4
5
6
7
// LED 引脚定义
#define LED_BLUE_Pin GPIO_PIN_6
#define LED_BLUE_GPIO_Port GPIOA
#define LED_GREEN_Pin GPIO_PIN_7
#define LED_GREEN_GPIO_Port GPIOA
#define LED_RED_Pin GPIO_PIN_0
#define LED_RED_GPIO_Port GPIOB

通信协议设计

电脑和 STM32 要”聊天”,得先约定好聊天的格式——这就是通信协议。

打个比方:你和朋友约好,每次发消息都用 “人名 + 动作” 的格式。

  • 张三 去开门 → 张三去开门
  • 李四 坐下 → 李四坐下

谁要是发 今天天气真好,对方就听不懂了——因为不符合约定的格式。

串口通信也是一样的道理。串口只负责把字节从 A 传到 B,它不管这些字节是什么意思。意思是我们自己约定的。

这个项目中,我规定电脑发给 STM32 的每条指令都是 2个字节:

字节顺序 含义 取值
第1个字节 选择哪个小灯 R(红色)、G(绿色)、B(蓝色)
第2个字节 开还是关 0(关闭)、1(打开)

例如:

  • 发 R1 → STM32 拆开看:第1字节是 R(红色灯),第2字节是 1(打开)→ 红色灯亮
  • 发 G0 → 第1字节是 G(绿色灯),第2字节是 0(关闭)→ 绿色灯灭
  • 发 B1 → 蓝色灯亮

同时,STM32 收到指令后,会把收到的 2 个字节原样发回电脑,作为”我收到了”的确认。

receiveData 数组设成 uint8_t receiveData[2],就是因为协议规定一条指令刚好 2 个字节,多一个少一个都不行。

main.c 核心代码

打开 main.c,在 USER CODE BEGIN 2 中定义接收缓冲区:

1
2
3
4
/* USER CODE BEGIN 2 */
// 存放接收数据的数组(协议规定指令长度为2字节)
uint8_t receiveData[2];
/* USER CODE END 2 */

然后在 while(1) 中编写主逻辑:

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
28
29
30
31
32
33
34
/* USER CODE BEGIN WHILE */
while (1)
{
// 1. 等待接收2字节指令(阻塞模式)
HAL_UART_Receive(&huart2, receiveData, 2, HAL_MAX_DELAY);

// 2. 将收到的指令原样回复给电脑
HAL_UART_Transmit(&huart2, receiveData, 2, 100);

// 3. 设置小灯亮灭状态变量
GPIO_PinState state = GPIO_PIN_SET; // 默认高电平(亮)
if (receiveData[1] == '0') // 如果第二字节是'0'
{
state = GPIO_PIN_RESET; // 改为低电平(灭)
}

// 4. 根据第一字节选择控制哪个小灯
switch (receiveData[0])
{
case 'R':
HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, state);
break;
case 'G':
HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, state);
break;
case 'B':
HAL_GPIO_WritePin(LED_BLUE_GPIO_Port, LED_BLUE_Pin, state);
break;
}
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

代码讲解

用到的HAL函数:

函数 作用
HAL_UART_Receive(&huart2, buffer, size, timeout) STM32从串口2接收指定字节数的数据
HAL_UART_Transmit(&huart2, buffer, size, timeout) STM32从串口2发送指定字节数的数据
HAL_GPIO_WritePin(port, pin, state) 老朋友了,设置GPIO输出电平

四个参数的详解(以 HAL_UART_Receive 为例):

参数 含义 本项目中填的值
串口句柄指针 操作哪个串口 &huart2(USART2的地址,取地址符不能忘)
数据缓冲区指针 接收到的数据存到哪里 receiveData(数组名本身就是指针)
接收长度 要接收多少个字节 2(我们规定一条指令就是2字节,所以只收2个)
超时时间 等多久没收到就放弃 HAL_MAX_DELAY(无限等待,直到收到数据)

核心逻辑:

  1. 阻塞接收——HAL_UART_Receive 会一直等待,直到收到2字节数据才往下执行。这是串口的轮询模式(也叫阻塞模式),代码简单直接,但缺点是一旦收不到数据,程序就卡在这了。

  2. 数据回显——收到后立刻用 HAL_UART_Transmit 发回电脑。这在调试时非常有用——看到回声,就说明通信链路是通的。

  3. 指令解析——判断第二字节 receiveData[1] 是 '0' 还是 '1',来决定 state 是 GPIO_PIN_RESET 还是 GPIO_PIN_SET。

  4. 开关控制——用 switch 根据第一字节决定控制哪个引脚,把 state 写进去。


解读 MX_USART2_UART_Init —— USART 的配置代码

回到 CubeMX 生成的代码,看看 USART2 到底是怎么被初始化的。这个函数在 main.c 中,main() 里调用 MX_USART2_UART_Init() 完成串口配置。

来看它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void MX_USART2_UART_Init(void)
{
huart2.Instance = USART2; // (1)
huart2.Init.BaudRate = 115200; // (2)
huart2.Init.WordLength = UART_WORDLENGTH_8B; // (3)
huart2.Init.StopBits = UART_STOPBITS_1; // (4)
huart2.Init.Parity = UART_PARITY_NONE; // (5)
huart2.Init.Mode = UART_MODE_TX_RX; // (6)
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; // (7)
huart2.Init.OverSampling = UART_OVERSAMPLING_16; // (8)
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
}

先认识一个重要的结构体:huart2

1
UART_HandleTypeDef huart2;

huart2 是一个串口句柄(Handle),可以理解为 USART2 这个外设的”身份证”。

它身上挂了一堆信息:

  • 是哪个串口(Instance 指向 USART2 的寄存器基地址)
  • 配置参数(波特率、数据格式等都在 Init 结构体里)
  • 运行状态(发送/接收过程中用到的缓存、标志位等)

后面所有操作 USART2 的 HAL 函数,第一个参数都要传 &huart2——就是要这张”身份证”来告诉函数”你操作的是 USART2”。

逐行解析:

行 代码 对应 CubeMX 配置 说明
(1) .Instance = USART2 (自动) 告诉 HAL 库要操作的是 USART2 这个外设,不是 USART1 或 USART3。Instance 指向 USART2 的寄存器基地址(0x40004400)
(2) .BaudRate = 115200 Baud Rate 下拉框 设置波特率。CubeMX 会根据你选的数值算出分频系数,写入串口的波特率寄存器(USART_BRR)。我们在前面花了大篇幅讲波特率,这里就是把这个数值落实到代码里
(3) .WordLength = UART_WORDLENGTH_8B Word Length 下拉框 一帧数据中数据位的长度。UART_WORDLENGTH_8B 表示 8 位数据。还有 UART_WORDLENGTH_9B(9位),但 8 位最常用
(4) .StopBits = UART_STOPBITS_1 Stop Bits 下拉框 停止位长度。UART_STOPBITS_1 = 1 位停止位。还有 0.5 位、2 位等选项,但标准 UART 通信通常就是 1 位
(5) .Parity = UART_PARITY_NONE Parity 下拉框 校验位——用于检测数据传输是否出错。NONE 表示不启用校验。还有 UART_PARITY_EVEN(偶校验)和 UART_PARITY_ODD(奇校验)
(6) .Mode = UART_MODE_TX_RX (自动) 开启收发双工。UART_MODE_TX_RX = 同时支持发送和接收。如果只发不收,可以设 UART_MODE_TX
(7) .HwFlowCtl = UART_HWCONTROL_NONE (默认) 硬件流控制——是否额外用 RTS/CTS 引脚来控制数据流。简单的 TTL 串口不需要,设为 NONE
(8) .OverSampling = UART_OVERSAMPLING_16 (默认) 过采样率——接收端用多快的频率去采样 RX 引脚上的电平。16 倍过采样意味着每个比特采 16 次,抗干扰能力强

(3)(4)(5)合起来构成了数据帧格式:

1
起始位(1) + 数据位(8) + 校验位(无) + 停止位(1) = 一帧共10比特

这就是我们之前在”数据帧结构”部分讲到的 UART 帧格式——CubeMX 里的取值,最终通过这里写进了代码。

HAL_UART_Init 内部做了什么?

HAL_UART_Init 拿到 huart2 结构体后,会做三件核心的事:

  1. 复位 USART2——把 USART2 的寄存器恢复到默认状态
  2. 配置 USART2 的寄存器——根据 huart2.Init 里的参数,计算对应的分频系数、帧格式等,写入 USART2 的寄存器(USART_BRR、USART_CR1、USART_CR2、USART_CR3)
  3. 开启 USART2——使能 USART2 外设,让它开始工作

如果以后想脱离 CubeMX 手写串口配置,本质上就是把上面那 8 行初始化代码,直接翻译成寄存器的读写操作。


解读 MX_GPIO_Init —— 引脚配置与复用功能

MX_GPIO_Init() 不只配置了三个 LED 引脚,还配置了 USART2 的 TX/RX 引脚(PA2、PA3)。不过 TX/RX 的配置不是在 GPIO_Init 里显式写的,而是由 HAL_UART_Init 内部自动完成的——这是因为 USART2 的 TX/RX 引脚是复用功能(Alternate Function),不是普通的 GPIO。

先看 LED 引脚的配置

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
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};

/* GPIO Ports Clock Enable — 打开 GPIO 时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE(); // 给 GPIOA 供时钟(PA6、PA7、PA2、PA3)
__HAL_RCC_GPIOB_CLK_ENABLE(); // 给 GPIOB 供时钟(PB0)

/* 初始电平:LED 全部默认熄灭 */
HAL_GPIO_WritePin(GPIOA, LED_BLUE_Pin | LED_GREEN_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_RESET);

/* 配置 LED_BLUE + LED_GREEN(都在 GPIOA 上,可以一次配置) */
GPIO_InitStruct.Pin = LED_BLUE_Pin | LED_GREEN_Pin; // PA6 | PA7
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

/* 配置 LED_RED(在 GPIOB 上,要分开配置) */
GPIO_InitStruct.Pin = LED_RED_Pin; // PB0
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LED_RED_GPIO_Port, &GPIO_InitStruct);
}

这部分和之前流水灯博客里的 GPIO 配置完全一样,不再重复解释。值得注意的新东西只有两个:

为什么 GPIOA 上的两个 LED 可以一次配完?

HAL_GPIO_Init 的 Pin 成员支持按位或组合多个引脚:

1
GPIO_InitStruct.Pin = LED_BLUE_Pin | LED_GREEN_Pin;  // = GPIO_PIN_6 | GPIO_PIN_7

因为 GPIO_PIN_6 是 0x0040,GPIO_PIN_7 是 0x0080,按位或后得到 0x00C0。HAL_GPIO_Init 内部会逐个检查这些位,把所有对应的引脚都配成同样的参数。

但如果引脚分布在不同的 GPIO 端口(比如 PA6 在 GPIOA,PB0 在 GPIOB),就必须分开配置,因为每次调用 HAL_GPIO_Init 的第二个参数是指定端口的。

USART2 的 TX/RX 引脚是怎么配置的?

观察芯片原理图会发现,USART2 的 TX 和 RX 分别占用 PA2 和 PA3。image-20260729135232431

但在 MX_GPIO_Init 里,并没有看到配置 PA2 和 PA3 的代码。那它们是怎么变成串口引脚的呢?

关键在 HAL_UART_Init(&huart2) 内部——它调用了一个更底层的函数 HAL_UART_MspInit()(MSP = MCU Support Package),这个函数在 stm32f1xx_hal_msp.c 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
if (huart->Instance == USART2) {
/* 使能 USART2 时钟 */
__HAL_RCC_USART2_CLK_ENABLE();

/* 配置 PA2(TX) 为复用推挽输出 */
GPIO_InitStruct.Pin = GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // ← 复用推挽输出
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

/* 配置 PA3(RX) 为复用浮空输入 */
GPIO_InitStruct.Pin = GPIO_PIN_3;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // ← 浮空输入
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}

这段代码会在 HAL_UART_Init 中被自动调用。这里出现了前几篇没见过的新模式——**GPIO_MODE_AF_PP(复用推挽输出)**。

模式 含义 什么时候用
GPIO_MODE_OUTPUT_PP 普通推挽输出 直接控制 LED 这类外设
GPIO_MODE_AF_PP 复用功能推挽输出 引脚的控制权交给片上外设(如 USART 的 TX 引脚)
GPIO_MODE_INPUT 普通浮空输入 读取外部信号(如按键、USART 的 RX 引脚)

普通输出和复用输出的区别:

1
2
3
4
5
6
7
普通输出(OUTPUT_PP):
CPU → GPIO 输出数据寄存器 → 引脚
你可以用 WritePin 直接控制

复用输出(AF_PP):
CPU → USART 发送寄存器 → USART 硬件自动控制 → 引脚
引脚交给 USART 外设管理,不需要你手动 WritePin

对于 USART2 的 TX 引脚(PA2),它发送数据的时机和数据内容都是由 USART 外设硬件决定的,不是由 CPU 用 HAL_GPIO_WritePin 控制的。所以必须配成 AF_PP,把引脚的控制权”让渡”给 USART 外设。

RX 引脚(PA3)同理——它负责接收外部信号到 USART 接收寄存器,所以配成普通的输入模式,让 USART 可以读取引脚电平。

这就是 GPIO 的复用功能(Alternate Function)——同一个引脚,在不同配置下可以属于不同的片上外设。配成普通 GPIO 时,你可以用 WritePin/ReadPin 控制它;配成 AF 时,它归某个外设管。这也是为什么从这篇开始,CubeMX 需要配置 Debug 模式为 Serial Wire——因为一旦开启了 USART 的复用功能,就会影响到 SWD 调试引脚的相关配置。

配置参数的对应关系总结

把 CubeMX 里的配置和生成的代码对应起来:

CubeMX 设置 出现在哪个函数 底层操作
USART2 Mode = Asynchronous MX_USART2_UART_Init USART_CR1 寄存器的 UE、TE、RE 位
Baud Rate = 115200 MX_USART2_UART_Init 计算分频系数 → 写入 USART_BRR 寄存器
PA2 (TX) HAL_UART_MspInit GPIOA_CRL 寄存器,配置为 AF_PP
PA3 (RX) HAL_UART_MspInit GPIOA_CRL 寄存器,配置为 输入模式
LED_RED (PB0) MX_GPIO_Init GPIOB_CRL 寄存器,配置为 推挽输出 50MHz

常见编译警告及处理

如果你直接编译,可能会遇到几个警告,这是 CubeMX 生成代码时常见的情况:

警告1:strlen 未声明

如果你在测试发送功能时写了类似 strlen(message) 的代码,编译器会提示 strlen 未声明。

1
#include <string.h>   // strlen 在这个头文件中

警告2:类型不匹配

HAL_UART_Transmit 的第二个参数要求 uint8_t* 类型,但如果你传了一个 char* 数组,会有类型不匹配的警告:

1
2
3
4
5
6
// 错误写法
char message[] = "Hello World";
HAL_UART_Transmit(&huart2, message, strlen(message), 100); // 类型警告

// 正确写法:强转
HAL_UART_Transmit(&huart2, (uint8_t*)message, strlen(message), 100);

实际上 char 和 uint8_t 在底层都是 8 位数据,类型不同只是因为语义不同——不转换也能运行,但编译器会警告。养成强转的习惯,对代码严谨性有好处。

硬件连接

代码写完后,还需要把 STM32 和电脑连接起来。

keysking 学习板已经集成了串口转 USB 芯片(CH340),连接到了 USART2,并引出了一个 Type-C 接口。所以只需要:

1
开发板 Type-C 口  ←→  电脑 USB 口(用手机数据线)

如果是使用其他开发板(比如最小系统板),则需要额外的 USB转TTL模块:

1
2
3
STM32 TX (PA2)  ←→  模块 RX
STM32 RX (PA3) ←→ 模块 TX
STM32 GND ←→ 模块 GND

串口助手的使用

要让电脑和 STM32 通信,还需要一个串口调试助手。这里使用的是 keysking 开发的波特律动串口助手,它的好处是在网页上就能进行调试,无需下载。

连接步骤:

  1. 选择串口——在串口助手中点击”选择串口”,找到对应的 COM 口

    • 学习板用户选择 USB Serial(CH340)
    • 不确定是哪个的话,可以插拔数据线,看哪个选项随之出现/消失
  2. 设置波特率——设为 115200(与代码中的设置一致)

  3. 设置显示模式——将接收显示模式设为 ASCII码模式,否则默认的 Hex 模式会显示一堆十六进制数

如果找不到串口,说明需要安装相关的串口驱动。

可以在波特律动串口助手界面左下角,点击三横线的按钮,找到常见驱动下载

image-20260729132825021


编译 & 烧录

  1. VS Code 底部工具栏点击齿轮图标 → 生成
  2. 连接 ST-Link,点击运行和调试 → 选择 STM32Cube:STM32 Launch STLink GDB Server
  3. 点击继续按钮,程序开始运行

如果遇到烧录问题,检查 Debug 模式是否设为了 Serial Wire。


效果展示

烧录成功后,在串口助手的发送区输入:

发送内容 效果
R1 🔴 红色小灯亮起
R0 🔴 红色小灯熄灭
G1 🟢 绿色小灯亮起
G0 🟢 绿色小灯熄灭
B1 🔵 蓝色小灯亮起
B0 🔵 蓝色小灯熄灭

同时,串口助手的接收区会显示 STM32 返回的指令内容(回显)。

效果展示视频:

https://www.bilibili.com/video/BV1NV3y6YEYv/?vd_source=5a111e8d49f70e532d36ddb73be3d0aa


轮询模式的局限性

当前代码使用的是串口的轮询模式(阻塞模式),这种模式有明显的缺点:

  1. HAL_UART_Receive 会卡住程序——在收到数据之前,main loop 里后面的代码全都不执行
  2. 接收固定长度——必须收到恰好 2 字节才能继续,如果电脑发了 3 个字节,第三个字节就留在缓冲区里了
  3. 无法处理多任务——如果还想同时做其他事(比如检测按键),就做不到了

解决方法——中断模式和 DMA 模式,会在后面的博客中学习。


总结

这次项目是 STM32 学习路上的一个重要里程碑——从”板子自己玩”进步到”板子和电脑对话”。

新知识点 说明
USART 外设配置 CubeMX 中配置串口的波特率、数据格式等参数
HAL_UART_Receive / HAL_UART_Transmit 串口收发的 HAL 函数,阻塞模式
通信协议设计 定义 PC 和 STM32 之间的指令格式
串口助手 调试串口通信的必备工具
回显机制 收到数据立即发回,确认通信正常

参考资源