STM32 零基础可移植教程 09:串口收一行命令,用 led on 控制 LED
上一篇我们把 USART RX 中断跑通了。
电脑发一个字符,STM32 能收到,并且能打印:
bash
RX: A, hex: 0x41
这一步说明串口接收链路没问题。
但真实项目里,我们很少只收一个字符。更常见的是在串口助手里输入一行命令:
bash
led on
led off
led toggle
然后让板子执行对应动作。
这一篇就把单字节接收往前推一步:
bash
用 USART RX 中断接收一行命令,然后解析命令控制 LED
本篇还是不讲 DMA、不讲 IDLE、不讲复杂协议。先把"收一行字符串"这件事跑通。
本篇目标
最终现象:
串口助手输入:
bash
led on
STM32 返回:
bash
> led on
OK: LED on
同时 LED 点亮。
输入:
bash
led off
STM32 返回:
bash
> led off
OK: LED off
同时 LED 熄灭。
输入:
bash
help
STM32 返回命令列表。
本篇跑通标准:
-
Keil 编译通过;
-
串口能打印启动信息;
-
串口助手输入
help后能看到命令列表; -
输入
led on后 LED 点亮; -
输入
led off后 LED 熄灭; -
输入
led toggle后 LED 翻转; -
能说清楚一行命令是怎么从一个个字节拼出来的;
-
能说清楚换 USART、换 LED 引脚时要改哪里。
准备工作
建议从上一篇工程复制一份,改名为:
bash
09_usart_line_command
上一篇已经完成:
-
USART 基本配置;
-
USART global interrupt;
-
HAL_UART_Receive_IT()单字节中断接收; -
printf()串口输出; -
串口助手接线和参数设置。
这一篇在它的基础上加两个东西:
还需要第 02 篇里的 LED 应用层代码:
bash
Core/Inc/app_led.h
Core/Src/app_led.c
如果你的工程里还没有这两个文件,本篇后面也会给出。
硬件连接
本篇用到两个硬件功能:
bash
USART
LED
USART 接线仍然是:
|
STM32
|
USB 转 TTL
|
| --- | --- |
|
USART_TX
|
RXD
|
|
USART_RX
|
TXD
|
|
GND
|
GND
|
LED 如果是板载 LED,先看原理图确认接到哪个 GPIO。
如果是外接 LED,注意串联限流电阻。
本系列仍然建议把 LED 引脚的 CubeMX User Label 设置为:
bash
LED
这样 CubeMX 会在 main.h 里生成:
bash
#define LED_Pin ...
#define LED_GPIO_Port ...
应用层代码就能直接用 LED_Pin 和 LED_GPIO_Port。


先理解一行命令怎么收
上一章我们每次只收 1 个字节。
比如电脑发送:
bash
led on
STM32 实际收到的不是"一整句",而是一个个字节:
bash
l
e
d
空格
o
n
回车
换行
所以我们要做一件事:
bash
每收到 1 个字节,就先放进缓冲区
遇到 \r 或 \n,说明一行结束
然后把这一行交给命令解析函数
led on 这行命令在缓冲区里大概是这样:
bash
['l']['e']['d'][' ']['o']['n']['\0']
最后这个 \0 很重要。
C 语言字符串必须以 \0 结束,否则 strcmp()、printf("%s") 这些函数不知道字符串到哪里结束。
本篇的处理方式是:
bash
app_uart 只负责接收一行字符串
app_command 只负责解析这一行命令
app_led 只负责控制 LED
main.c 只负责把几个模块串起来
不要把所有逻辑都堆在 main.c。
这样后面扩展 beep、adc read、pwm 50 这些命令时,也比较清楚。

CubeMX 配置步骤
1. USART 保持上一篇配置
USART 仍然配置为:
bash
Mode: Asynchronous
Baud Rate: 115200
Word Length: 8 Bits
Parity: None
Stop Bits: 1
Hardware Flow Control: None
也就是:
bash
115200, 8N1
并且要打开:
bash
USARTx global interrupt
如果这一项忘了勾,串口打印可能正常,但接收回调不会进。

2. LED 引脚保持 GPIO Output
LED 引脚配置为:
bash
GPIO_Output
GPIO 参数仍然建议:
|
配置项
|
推荐值
|
| --- | --- |
|
GPIO output level
|
默认关闭 LED 的电平
|
|
GPIO mode
|
Output Push Pull
|
|
GPIO Pull-up/Pull-down
|
No pull-up and no pull-down
|
|
Maximum output speed
|
Low
|
|
User Label
|
LED
|
LED 是高电平亮还是低电平亮,由 app_led.c 里的宏处理。

3. 生成 Keil 工程
配置完成后点击:
bash
GENERATE CODE
打开 Keil 后先编译一次,确认 CubeMX 生成代码没问题。
完整代码
本篇一共有 3 个应用模块:
bash
Core/Inc/app_uart.h
Core/Src/app_uart.c
Core/Inc/app_command.h
Core/Src/app_command.c
Core/Inc/app_led.h
Core/Src/app_led.c
app_uart 负责串口收一行。
app_command 负责解析命令。
app_led 负责控制 LED。
1. Core/Inc/app_uart.h
bash
#ifndef APP_UART_H
#define APP_UART_H
#include "main.h"
#include <stdint.h>
#ifndef APP_UART_LINE_MAX
#define APP_UART_LINE_MAX 64u
#endif
void App_UART_Init(void)
;
void App_UART_Print(const char *text)
;
void App_UART_PrintBytes(const uint8_t *data, uint16_t len)
;
HAL_StatusTypeDef App_UART_StartReceiveIT(void)
;
void App_UART_OnRxCplt(UART_HandleTypeDef *huart)
;
uint8_t App_UART_ReadLine(char *line, uint16_t size)
;
uint8_t App_UART_TakeOverflow(void)
;
#endif
这里的:
bash
#define APP_UART_LINE_MAX 64u
表示一行命令最多 63 个有效字符,最后 1 个位置留给 \0。
新手阶段够用了。
2. Core/Src/app_uart.c
bash
#include "app_uart.h"
#include <stdio.h>
#include <string.h>
#ifndef APP_UART_HANDLE
#define APP_UART_HANDLE huart1
#endif
extern
UART_HandleTypeDef APP_UART_HANDLE;
static
uint8_t
s_rx_it_byte =
0
;
static
char
s_build_line[APP_UART_LINE_MAX];
static
char
s_ready_line[APP_UART_LINE_MAX];
static
uint16_t
s_build_len =
0
;
static
uint8_t
s_drop_line =
0
;
static
volatile
uint8_t
s_line_ready =
0
;
static
volatile
uint8_t
s_line_overflow =
0
;
static void App_UART_SaveReadyLine(void)
{
uint16_t
i;
if
((s_build_len ==
0u
) || (s_line_ready !=
0u
))
{
s_build_len =
0
;
return
;
}
s_build_line[s_build_len] =
'\0'
;
for
(i =
0
; i <= s_build_len; i++)
{
s_ready_line[i] = s_build_line[i];
}
s_line_ready =
1u
;
s_build_len =
0
;
}
void App_UART_Init(void)
{
s_rx_it_byte =
0
;
s_build_len =
0
;
s_drop_line =
0
;
s_line_ready =
0
;
s_line_overflow =
0
;
memset
(s_build_line,
0
,
sizeof
(s_build_line));
memset
(s_ready_line,
0
,
sizeof
(s_ready_line));
}
void App_UART_Print(const char *text)
{
if
(text ==
NULL
)
{
return
;
}
HAL_UART_Transmit(&APP_UART_HANDLE,
(
uint8_t
*)text,
(
uint16_t
)
strlen
(text),
HAL_MAX_DELAY);
}
void App_UART_PrintBytes(const uint8_t *data, uint16_t len)
{
if
((data ==
NULL
) || (len ==
0u
))
{
return
;
}
HAL_UART_Transmit(&APP_UART_HANDLE,
(
uint8_t
*)data,
len,
HAL_MAX_DELAY);
}
HAL_StatusTypeDef App_UART_StartReceiveIT(void)
{
return
HAL_UART_Receive_IT(&APP_UART_HANDLE, &s_rx_it_byte,
1u
);
}
void App_UART_OnRxCplt(UART_HandleTypeDef *huart)
{
uint8_t
ch;
if
(huart ==
NULL
)
{
return
;
}
if
(huart->Instance != APP_UART_HANDLE.Instance)
{
return
;
}
ch = s_rx_it_byte;
if
((ch ==
'\r'
) || (ch ==
'\n'
))
{
s_drop_line =
0
;
App_UART_SaveReadyLine();
}
else
if
(s_drop_line !=
0u
)
{
/* Drop bytes until the next line ending after an overflow. */
}
else
if
(s_build_len < (APP_UART_LINE_MAX -
1u
))
{
s_build_line[s_build_len] = (
char
)ch;
s_build_len++;
}
else
{
s_build_len =
0
;
s_drop_line =
1u
;
s_line_overflow =
1u
;
}
(
void
)HAL_UART_Receive_IT(&APP_UART_HANDLE, &s_rx_it_byte,
1u
);
}
uint8_t App_UART_ReadLine(char *line, uint16_t size)
{
uint16_t
i;
if
((line ==
NULL
) || (size ==
0u
))
{
return
0u
;
}
if
(s_line_ready ==
0u
)
{
return
0u
;
}
__disable_irq();
for
(i =
0
; i < (size -
1u
); i++)
{
line[i] = s_ready_line[i];
if
(s_ready_line[i] ==
'\0'
)
{
break
;
}
}
line[size -
1u
] =
'\0'
;
s_line_ready =
0u
;
__enable_irq();
return
1u
;
}
uint8_t App_UART_TakeOverflow(void)
{
uint8_t
overflow;
__disable_irq();
overflow = s_line_overflow;
s_line_overflow =
0u
;
__enable_irq();
return
overflow;
}
int fputc(int ch, FILE *f)
{
uint8_t
data = (
uint8_t
)ch;
HAL_UART_Transmit(&APP_UART_HANDLE, &data,
1u
, HAL_MAX_DELAY);
return
ch;
}
这一段里最关键的是:
bash
if
((ch ==
'\r'
) || (ch ==
'\n'
))
串口助手按下回车,常见会发送 \r、\n 或 \r\n。
我们遇到回车或换行,就认为一行结束。
还有这句不能少:
bash
(
void
)HAL_UART_Receive_IT(&APP_UART_HANDLE, &s_rx_it_byte,
1u
);
它会重新开启下一次单字节接收。
少了它,还是只能收一次。
3. Core/Inc/app_command.h
bash
#ifndef APP_COMMAND_H
#define APP_COMMAND_H
void App_Command_Init(void)
;
void App_Command_ProcessLine(const char *line)
;
#endif
4. Core/Src/app_command.c
bash
#include "app_command.h"
#include "app_led.h"
#include <stdio.h>
#include <string.h>
static void App_Command_PrintHelp(void)
{
printf
(
"Commands:\r\n"
);
printf
(
" help - show this help\r\n"
);
printf
(
" led on - turn LED on\r\n"
);
printf
(
" led off - turn LED off\r\n"
);
printf
(
" led toggle - toggle LED\r\n"
);
}
void App_Command_Init(void)
{
}
void App_Command_ProcessLine(const char *line)
{
if
(line ==
NULL
)
{
return
;
}
if
(line[
0
] ==
'\0'
)
{
return
;
}
printf
(
"> %s\r\n"
, line);
if
(
strcmp
(line,
"help"
) ==
0
)
{
App_Command_PrintHelp();
}
else
if
(
strcmp
(line,
"led on"
) ==
0
)
{
App_LED_On();
printf
(
"OK: LED on\r\n"
);
}
else
if
(
strcmp
(line,
"led off"
) ==
0
)
{
App_LED_Off();
printf
(
"OK: LED off\r\n"
);
}
else
if
(
strcmp
(line,
"led toggle"
) ==
0
)
{
App_LED_Toggle();
printf
(
"OK: LED toggle\r\n"
);
}
else
{
printf
(
"ERR: unknown command\r\n"
);
printf
(
"Type help and press Enter\r\n"
);
}
}
这篇先用最简单的:
bash
strcmp
()
它要求命令完全匹配。
所以:
bash
led on
可以识别。
但:
bash
LED ON
led on
led on
暂时不处理。
这不是不能做,而是这篇先把主线跑通。大小写忽略、去掉多余空格、参数解析,后面可以慢慢加。
5. Core/Inc/app_led.h
bash
#ifndef APP_LED_H
#define APP_LED_H
#include "main.h"
void App_LED_Init(void)
;
void App_LED_On(void)
;
void App_LED_Off(void)
;
void App_LED_Toggle(void)
;
#endif
6. Core/Src/app_led.c
bash
#include "app_led.h"
#ifndef LED_GPIO_Port
#error "LED_GPIO_Port is not defined. Set the LED pin User Label to LED in CubeMX."
#endif
#ifndef LED_Pin
#error "LED_Pin is not defined. Set the LED pin User Label to LED in CubeMX."
#endif
#ifndef APP_LED_ON_LEVEL
#define APP_LED_ON_LEVEL GPIO_PIN_RESET
#endif
#ifndef APP_LED_OFF_LEVEL
#define APP_LED_OFF_LEVEL GPIO_PIN_SET
#endif
void App_LED_Init(void)
{
App_LED_Off();
}
void App_LED_On(void)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, APP_LED_ON_LEVEL);
}
void App_LED_Off(void)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, APP_LED_OFF_LEVEL);
}
void App_LED_Toggle(void)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
默认这里按"低电平亮"写。
如果你的 LED 是高电平亮,改成:
bash
#define APP_LED_ON_LEVEL GPIO_PIN_SET
#define APP_LED_OFF_LEVEL GPIO_PIN_RESET
把 .c 文件加入 Keil 工程
在 Keil 工程树里右键:
bash
Application/User/Core
选择:
bash
Add Existing Files to Group 'Application/User/Core'
加入:
bash
Core/Src/app_uart.c
Core/Src/app_command.c
Core/Src/app_led.c
如果忘了添加,常见会报:
bash
undefined symbol App_Command_ProcessLine
undefined symbol App_UART_ReadLine
undefined symbol App_LED_On

main.c 调用方式
1. Includes 区域
找到:
bash
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
改成:
bash
/* USER CODE BEGIN Includes */
#include "app_led.h"
#include "app_uart.h"
#include "app_command.h"
#include <stdio.h>
/* USER CODE END Includes */
2. 初始化区域
确认 CubeMX 初始化函数已经先执行:
bash
MX_GPIO_Init();
MX_USART1_UART_Init();
然后在:
bash
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
里面添加:
bash
/* USER CODE BEGIN 2 */
App_LED_Init();
App_UART_Init();
App_Command_Init();
App_UART_StartReceiveIT();
printf
(
"\r\nUSART command test start\r\n"
);
printf
(
"Type help and press Enter\r\n"
);
/* USER CODE END 2 */
如果你用的是 USART2,CubeMX 初始化函数可能是:
bash
MX_USART2_UART_Init();
这没关系,但 app_uart.c 里的 APP_UART_HANDLE 也要改成 huart2。
3. while 循环区域
找到:
bash
while
(
1
)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
/* USER CODE END 3 */
}
改成:
bash
while
(
1
)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
char
line[APP_UART_LINE_MAX];
if
(App_UART_TakeOverflow() !=
0u
)
{
printf
(
"ERR: command too long\r\n"
);
}
if
(App_UART_ReadLine(line,
sizeof
(line)) !=
0u
)
{
App_Command_ProcessLine(line);
}
HAL_Delay(
10
);
/* USER CODE END 3 */
}
主循环做三件事:
-
看看命令有没有超长;
-
看看有没有收到一整行;
-
如果收到,就交给命令解析模块。
4. USART 接收完成回调
找到:
bash
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
添加:
bash
/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
App_UART_OnRxCplt(huart);
}
/* USER CODE END 4 */
如果你的工程里已经有这个函数,不要重复写第二个。
把:
bash
App_UART_OnRxCplt(huart);
合并到已有函数里即可。
串口助手设置
串口助手参数仍然是:
bash
115200
8 data bits
1 stop bit
None parity
None flow control
发送命令时,要让串口助手发送换行。
常见设置叫:
bash
发送新行
加回车换行
发送 \r\n
不同串口助手名字不一样。
如果没有这个选项,你也可以手动发送:
bash
led on\r\n
本篇代码是看到 \r 或 \n 才认为一行结束。
如果你只输入 led on,但没有发送回车或换行,STM32 会一直等着,不会执行命令。

编译、下载和验证
先编译:
bash
Build / F7
确认:
bash
0 Error(s)
下载后,复位开发板。
串口助手应该看到:
bash
USART command test start
Type help and press Enter
输入:
bash
help
正常返回:
bash
> help
Commands:
help - show this help
led on - turn LED on
led off - turn LED off
led toggle - toggle LED
输入:
bash
led on
正常返回:
bash
> led on
OK: LED on
LED 应该点亮。
输入:
bash
led off
正常返回:
bash
> led off
OK: LED off
LED 应该熄灭。
输入:
bash
led toggle
LED 应该翻转一次。

移植到其他板子的修改点
这篇的移植点主要有 9 个。
|
要改的地方
|
为什么要改
|
在哪里改
|
| --- | --- | --- |
|
USART 实例
|
不同板子可能用 USART1/2/3
|
CubeMX Connectivity
|
|
TX/RX 引脚
|
不同开发板接线不同
|
CubeMX Pinout 和原理图
|
|
USART 中断
|
收命令依赖 RX 中断
|
CubeMX NVIC Settings
|
|
串口句柄
| huart1
、huart2 不同
| app_uart.c
的 APP_UART_HANDLE
|
|
LED 引脚
|
不同板子 LED GPIO 不同
|
CubeMX Pinout
|
|
LED User Label
|
代码依赖 LED_Pin 和 LED_GPIO_Port
|
CubeMX GPIO 页面
|
|
LED 有效电平
|
有些高电平亮,有些低电平亮
| app_led.c
宏
|
|
命令长度
|
命令更长时缓冲区要变大
| APP_UART_LINE_MAX |
|
命令内容
|
不同项目命令不同
| app_command.c |
换板子的推荐顺序:
-
先确认 USART 接到哪个 USB 串口或外接模块;
-
CubeMX 配置对应 USART,并打开 global interrupt;
-
确认
stm32xx_it.c里有HAL_UART_IRQHandler(&huartx); -
修改
APP_UART_HANDLE; -
配置 LED GPIO Output,User Label 填
LED; -
根据原理图修改 LED 有效电平;
-
Keil 添加
app_uart.c、app_command.c、app_led.c; -
串口助手发送
help验证命令入口; -
再发送
led on、led off验证硬件动作。
常见问题排查
1. 串口有启动信息,但输入命令没反应
优先检查:
|
优先检查
|
具体方法
|
| --- | --- |
|
串口助手有没有发送回车换行
|
需要 \r 或 \n 结束一行
|
|
RX 线有没有接
|
USB-TTL TXD 接 STM32 RX
|
|
USART global interrupt 有没有打开
|
CubeMX NVIC Settings
|
|
是否调用 App_UART_StartReceiveIT()
|
放在 USART 初始化后
|
|
回调是否存在
| HAL_UART_RxCpltCallback()
调用 App_UART_OnRxCplt()
|
最常见的其实是第一条:
bash
只发送了 led on,没有发送回车或换行
STM32 还在等"一行结束",所以不会执行。
2. 输入 led on,返回 unknown command
先看串口回显:
bash
> led on
如果回显不是这个,比如多了空格:
bash
> led on
或者大小写不同:
bash
> LED ON
那本篇代码暂时不会识别。
当前命令必须完全匹配:
bash
help
led on
led off
led toggle
后面如果要做得更智能,可以加去空格、转小写、参数解析。
3. 只能执行一次命令,第二次没反应
优先检查 App_UART_OnRxCplt() 里有没有重新开启接收:
bash
(
void
)HAL_UART_Receive_IT(&APP_UART_HANDLE, &s_rx_it_byte,
1u
);
少了这句,就只收一次。
4. 编译报 LED_GPIO_Port is not defined
说明 CubeMX 没有生成:
bash
LED_Pin
LED_GPIO_Port
解决方法:
-
回 CubeMX;
-
找到 LED GPIO;
-
User Label 改成
LED; -
重新 Generate Code。
如果你的标签叫 LED_Red,那生成的是:
bash
LED_Red_Pin
LED_Red_GPIO_Port
要么把 CubeMX 标签改成 LED,要么把 app_led.c 改成你实际的宏名。
5. 编译报 undefined symbol App_Command_ProcessLine
通常是 app_command.c 没加入 Keil 工程。
把这个文件加入:
bash
Core/Src/app_command.c
然后重新编译。
6. 输入命令后返回 OK,但 LED 不亮
这说明串口命令链路是通的,问题在 LED 部分。
优先检查:
-
LED 引脚是否选对;
-
LED 的 User Label 是否为
LED; -
LED 有效电平是否写反;
-
LED 是否是板载低电平亮;
-
外接 LED 是否有串联电阻;
-
App_LED_Init()是否在初始化区域调用。
可以先在 USER CODE BEGIN 2 里临时写:
bash
App_LED_On();
如果这样 LED 也不亮,就先回到第 02 篇排查 LED。
7. 输入很长一串后提示 command too long
本篇一行命令默认最大长度是:
bash
#define APP_UART_LINE_MAX 64u
如果超过长度,程序会丢弃这一行,并打印:
bash
ERR: command too long
这是为了防止数组越界。
如果你的命令确实需要更长,可以改大:
bash
#define APP_UART_LINE_MAX 128u
但不要无限改大,MCU 的 RAM 是有限的。
本篇小结
这一篇我们把 USART 接收从"一个字节"升级成了"一行命令"。
你现在至少应该知道:
-
串口命令不是一次性自动变成字符串的;
-
STM32 是一个字节一个字节收;
-
遇到
\r或\n才认为一行结束; -
C 字符串需要
\0结尾; -
app_uart负责收行,app_command负责解析,app_led负责控制硬件; -
命令必须和
strcmp()里的字符串完全一致; -
串口助手必须发送回车或换行;
-
换板子时重点改 USART、LED 引脚、句柄和有效电平。
下一篇我们开始进入时间类外设:
STM32 定时器基础:不用 HAL_Delay 也能 1 秒做一次事。
到这一步,程序就不只是"主循环里延时等待",而是开始用定时器中断做周期任务了。