嵌入式技术应用设计实训 | 串口通信:USART 收发二进制 与 printf
本课程开源地址(Gitee) :https://gitee.com/fujianxinxi/qianrushishixundianzi.git
课件、示例代码与验收脚本都在该仓库,可直接
git clone或下载 ZIP 使用。B站配套视频合集 :https://space.bilibili.com/572740825/lists?sid=9228122
合集《嵌入式技术应用设计实训|STM32F103 从点灯到智能家居》:每课配一节视频,可先看现象演示,再照着做。
开发板资料A盘(百度网盘) :https://pan.baidu.com/s/1_EXrjRUewwmGMgcy38zEZg(提取码:
zbri)正点原子官方资料下载 :http://www.openedv.com/docs/boards/stm32/zdyz_stm32f103_nano.html
模块一 基础外设开发 · 2 学时 · 环境:Keil MDK5 + VS Code + 正点原子 STM32F103 Nano V1.3
一句话概述 :从"串口是几根线、字怎么发出去"入手,把 USART1 的初始化一步步写在 main.c 里(开时钟 → PA9/PA10 复用 → 115200-8-N-1 → 中断),再用一个 APP_MODE 宏切换两种现象:模式 1 用 printf 打印调试信息,模式 2 收 2 字节二进制命令帧控制 8 个 LED;并把厂家例程"printf 走裸寄存器、回显走 HAL"的两条发送路收拢成唯一的 uart_send()。
本节目标
- 能说出串口通信要几根线、为什么两边的波特率和数据格式必须完全一致,并照着 3.1 的步骤认准板载 ST-Link 带出的虚拟串口(
STLink Virtual COM Port)、把串口助手连上板子 - 能照着
UART1_Init()说出串口初始化的三步(开时钟 → 配 PA9/PA10 复用 → 配 115200-8-N-1),并把模式 1 跑通,在串口助手里看到printf打印的计数 - 能说出
printf为什么会跑到串口上(fputc重定向 + MicroLIB),并解释本课为什么要把"发送"收拢成唯一的uart_send()
一、原理说明


串口是单片机和电脑之间最朴素的一条数据通道:它只用两根线,一根负责发(TX)、一根负责收(RX),再加上一根地线;数据一个位一个位地按约定好的快慢送出去,这个"快慢"就是波特率。STM32F103 里的 USART1 已经把最麻烦的串并转换做在硬件里了,我们要做的只有三件事:把引脚配成复用功能交给它管、把两边参数设成一样(115200-8-N-1)、然后往数据寄存器里写字节。而"把信息打到电脑上看"这件事,平时用 printf 最省事------可 printf 自己一点也不懂串口,它只是把内容拼成字符串,再一个字符一个字符地交给 fputc(),所以只要重写 fputc() 让它把字符写进 USART1,printf 的输出就跑到电脑上了。这节课还要顺手治一个"病":厂家例程里 printf 走裸寄存器、回显又走 HAL_UART_Transmit,同一个串口两套写法,看着很乱;本课把两条路合并到唯一的 uart_send(),以后不管发文本还是发二进制命令帧,都只走这一条。
二、讲解内容
2.1 知识讲解
1)串口凭什么能对上:靠"两边约定同一个节拍"
串口是异步 通信:它不像 I²C 那样单独拉一根时钟线,而是靠两边约定同一个波特率 来对齐时间。一帧数据的格式是固定的:1 位起始位 + 8 位数据 + 1 位停止位 (没有校验位)。接收方看到起始位就开始按波特率采样,采满 8 位、见到停止位,这一字节就算收到了。所以只要保证两边都是 115200-8-N-1,数据就能对上;波特率差一点,收到的就是乱码。
2)板子上怎么接:两根线全都板载好了
| 板载部分 | 开发板引脚 | 说明 |
|---|---|---|
| USART1 发送 TX | PA9 | 复用推挽输出:引脚交给 USART1 去拉高拉低 |
| USART1 接收 RX | PA10 | 复用输入:外部电平直接进 USART1 |
| 板载 ST-Link 的虚拟串口 | PA9 / PA10(板上已连好) | 下载与串口共用一根 USB 线 :插上电脑后,设备管理器 → 端口里出现 STMicroelectronics STLink Virtual COM Port (COMx)------不用接杜邦线,也不用装 CH340 之类的串口驱动 |
注意 :板子上的 ST-Link 一只管两件事------下载 和串口 。串口不是从"另一个 USB 口"出来的,而是这根 USB 线里附带的虚拟串口;设备管理器 → 端口里那个
STLink Virtual COM Port (COMx)才是串口助手要选的口(别选成上面的 ST-Link 设备)。
3)两边参数必须一模一样
| 参数 | 本课取值 | 说明(串口助手要选成一样) |
|---|---|---|
| 波特率 | 115200 | 每秒 115200 个位;两边不一致 = 乱码 |
| 数据位 | 8 | 一个字节 8 位 |
| 停止位 | 1 | 一帧结尾 1 位 |
| 校验位 | 无(None) | 本课不用校验 |
| 流控 | 无(None) | 不用 RTS/CTS |
4)printf 重定向:三件套 + 一个勾
printf 是 C 标准库的函数,在嵌入式里它默认想走"半主机"(把输出交回调试器),而板子单跑时没有调试器接着,程序就会停在那里。要让它走 USART1,需要下面三件事再加 Keil 里的一个勾:
| 位置 | 代码 | 作用 |
|---|---|---|
| 文件开头 | #pragma import(__use_no_semihosting) |
声明不用半主机模式 |
| 补标准库要的符号 | struct __FILE{int handle;};、FILE __stdout;、void _sys_exit(int x){x=x;} |
让编译能通过 |
重写 fputc() |
uart_putc((u8)ch); |
关键 :printf 最终就是一个字符一个字符地调它 |
| Keil 选项 | Options for Target → Target → 勾上 Use MicroLIB |
这个勾不勾,printf 就是不出声,本课最常见的坑 |
5)本课为什么要把它"收拢成一条路"
厂家例程里,往串口写东西有两条路:printf 走 fputc() 里的裸寄存器;回显走 HAL_UART_Transmit(),后面还跟着一句多余的"等 TC"。初学的困惑就出在这儿------**同一个 USART1,怎么冒出两套写法?**本课的做法是抽出一个最底层 uart_putc(),让上面两个入口都走它:
text
printf ─┐
├──> uart_putc() ──> USART1 (全课唯一的发送路径)
uart_send() ─┘
这样既保住了 printf 的方便,又只有一条真正碰硬件的路;后面课时17 做"串口命令与集成"时,这套写法能直接用。
6)本课两个模式:一个宏,两选一
板子上只有一个串口,同一时刻只能干一件事。本课用 APP_MODE 宏把两种用法做成编译期二选一 (改 usart.h 开头那一行,重新编译下载即可):
APP_MODE |
现象 | 串口助手怎么设 | 谁在用 |
|---|---|---|---|
| 1(默认) | printf 打印文本:上电两行欢迎语,之后每 500 ms 一行 运行中... count = n,DS0 同步闪 |
波特率 115200,文本方式看 | 本课主讲,也是以后各课的"调试手段" |
| 2 | 收 2 字节二进制命令帧控制 8 个灯:发 00 01 → DS0 亮 |
波特率 115200,勾"十六进制发送" | 实验三的基础 |
**为什么不做成两个独立开关、让 printf 和二进制同时开着?**因为调试打印出来的文本字节会混进二进制数据流里,上位机按帧解析必然错位。一个串口,同一时刻只留一条路------这是本课的设计前提。
7)二进制命令帧:2 个字节说清一件事
| 字节 | 含义 | 取值 |
|---|---|---|
| 第 1 字节 | 哪个 LED | 0x00~0x07 → DS0~DS7 |
| 第 2 字节 | 亮还是灭 | 0x00 = 灭,0x01 = 亮 |
发 00 01 = DS0 亮;发 03 00 = DS3 灭。收发双方约定好"第 1 个字节永远是灯号、第 2 个字节永远是开关" ,这就是一个最小可用的通信协议。它比"发字符串 LED0 ON 再去解析"省事得多:不用分割字符串、不用比大小写,两行判断就干完了。
8)为什么本课把串口初始化写在 main.c 里
厂家例程把初始化拆在 uart_init() 和 HAL_UART_MspInit() 两处,学生点进去只看到 HAL_UART_Init() 一句,时钟和引脚都藏在另一个文件里,像个黑盒。本课把它全部写进 main.c 的 UART1_Init(),和课时01 点灯、课时02 读按键是同一个套路:
| 步骤 | 干什么 | 本课的代码 | 不做会怎样 |
|---|---|---|---|
| ① 开时钟 | 给 GPIOA、USART1、AFIO 使能时钟 | __HAL_RCC_USART1_CLK_ENABLE(); 等三行 |
后面写寄存器全部无效,且不报错 |
| ② 配引脚 | PA9 复用推挽输出、PA10 复用输入 | GPIO_MODE_AF_PP / GPIO_MODE_AF_INPUT |
引脚还挂在 GPIO 上,串口既收不到也发不出 |
| ③ 配参数 | 115200-8-N-1,收发都开 | UART1_Handler.Init.* + HAL_UART_Init() |
参数不一致 = 全是乱码 |
| ④(模式 2)使能中断 | 放行 USART1_IRQn 并启动一次接收 |
HAL_NVIC_EnableIRQ() + HAL_UART_Receive_IT() |
引脚能收到数据,但 CPU 永远不去取 |
9)定长帧的"软肋"也要讲清
2 字节定长帧好写好用,但它有个天生弱点:一旦少收一个字节,收发双方的"帧边界"就永远错开了 ,之后每 2 个字节都会被拼成一条错误命令。本课的处理是"不合法就丢掉、能当新帧头就重来"(见 HAL_UART_RxCpltCallback() 里那几行判断),能自愈一部分;真正的解决办法是给帧加帧头 (如 0xAA)和校验和------那是实验三的挑战项。
2.2 代码示例
本课只改两个文件 (都在 课时文档/代码/课时03-串口通信:USART 收发与 printf/串口通信实验/ 下):
text
USER/main.c ← 串口初始化(UART1_Init)、中断收发、两个模式的 main()
SYSTEM/usart/usart.h ← APP_MODE 开关 + 对外接口声明
SYSTEM/usart/usart.c ← 串口底层:uart_putc / uart_send / fputc
工程里其余文件(HARDWARE/LED、SYSTEM/sys、SYSTEM/delay、HALLIB、CORE)都是现成的,不用动。
SYSTEM/usart/usart.h(完整代码)
c
#ifndef __USART_H
#define __USART_H
#include "stdio.h"
#include "sys.h"
//////////////////////////////////////////////////////////////////////////////////
// 《嵌入式技术应用设计实训》课时03 串口通信:USART 收发与 printf
// 本文件基于正点原子 ALIENTEK NANO STM32F103 厂家例程(串口通信实验-HAL库版本)改写,
// 原例程版权归广州市星翼电子科技有限公司所有。
//
// 本课把厂家例程"同一个串口两套写法"的问题收拢成一条路:
// printf ─┐
// ├──> uart_putc() ──> USART1 (全课只有这一条发送路径)
// uart_send() ─┘
// 串口的初始化不在这里,全部写在 USER/main.c 的 UART1_Init() 里,看得见、不是黑盒。
//////////////////////////////////////////////////////////////////////////////////
/* ==================== 演示模式选择(改这一行即可) ====================
1 = 调试打印 :printf 往串口打印文本,人在串口助手里看(本课主讲)
2 = 二进制收发 :串口收 2 字节命令帧,控制 8 个 LED 亮灭(实验三的基础)
改完必须重新编译下载,否则板子上跑的还是上一个程序。
==================================================================== */
#define APP_MODE 1
#if (APP_MODE != 1) && (APP_MODE != 2)
#error "APP_MODE 只能填 1 或 2,请修改 usart.h 开头的 APP_MODE 宏"
#endif
#define RXBUFFERSIZE 1 // 中断方式一次收几个字节(本课收 1 个)
extern UART_HandleTypeDef UART1_Handler; // USART1 句柄:usart.c 里定义、main.c 里初始化
extern u8 aRxBuffer[RXBUFFERSIZE]; // 中断接收暂存,每次 1 字节
void uart_send(const u8 *buf, u16 len); // 本课唯一的发送出口:printf 和命令帧都走它
#endif
SYSTEM/usart/usart.c(完整代码)
c
#include "sys.h"
#include "usart.h"
//////////////////////////////////////////////////////////////////////////////////
// 《嵌入式技术应用设计实训》课时03 串口通信:USART 收发与 printf ------ 串口底层驱动
// 本文件基于正点原子 ALIENTEK NANO STM32F103 厂家例程(串口通信实验-HAL库版本)改写,
// 原例程版权归广州市星翼电子科技有限公司所有。
//
// 本文件只干一件事:把字节"发出去",而且全课只有这一条发送路径
// ① uart_putc() 最底层:等上一个字节发完,再把一个字节写进数据寄存器
// ② uart_send() 对外唯一出口:printf、二进制命令帧、调试回显都走它
// ③ fputc() 只在模式 1 里,把 printf 的输出接到 uart_putc() 上
//
// 注意:串口的"初始化"(时钟、引脚、波特率、中断)不在这里,
// 全部写在 USER/main.c 的 UART1_Init() 里,课上要能一眼看全,不是黑盒。
//////////////////////////////////////////////////////////////////////////////////
UART_HandleTypeDef UART1_Handler; // USART1 句柄
u8 aRxBuffer[RXBUFFERSIZE]; // 中断接收暂存,每次 1 字节
// 最底层:发 1 个字节
// SR 的 bit6 是 TC(Transmission Complete,发送完成标志)。
// 等它为 1,说明上一个字节已经发完,这时再写 DR 才不会把上一个数据冲掉
static void uart_putc(u8 ch)
{
while ((USART1->SR & 0x40) == 0); // 等 TC = 1:上一个字节发完了
USART1->DR = ch; // 写数据寄存器,硬件自动一个位一个位发出去
}
// 对外唯一出口:发一串字节
// 厂家例程里 printf 走裸寄存器、回显又走 HAL_UART_Transmit,同一个串口两套写法;
// 本课把两条路合并到这里------以后不管发文本还是发二进制帧,都只调这一个函数
void uart_send(const u8 *buf, u16 len)
{
while (len--) uart_putc(*buf++);
while ((USART1->SR & 0x40) == 0); // 等最后一个字节真正发完再返回
}
#if (APP_MODE == 1)
// -------------------- printf 重定向(只有调试模式才接进来) --------------------
// printf 自己不懂串口:它先把内容拼成字符串,再一个字节一个字节地调用 fputc()。
// 所以只要重写 fputc(),printf 的输出就进了 USART1。
// Keil 里必须勾选 Options for Target → Target → Use MicroLIB,否则不生效。
#pragma import(__use_no_semihosting) // 不使用半主机模式,免得卡在 BKPT 上
struct __FILE // 标准库要求有这个类型
{
int handle;
};
FILE __stdout;
void _sys_exit(int x) // 屏蔽半主机相关的退出函数
{
x = x;
}
int fputc(int ch, FILE *f)
{
uart_putc((u8)ch); // 就这一句:和 uart_send() 走同一条路
return ch;
}
#endif
USER/main.c(完整代码)
c
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
//////////////////////////////////////////////////////////////////////////////////
// 《嵌入式技术应用设计实训》课时03 串口通信:USART 收发与 printf
// 本文件基于正点原子 ALIENTEK NANO STM32F103 厂家例程(串口通信实验-HAL库版本)改写,
// 原例程版权归广州市星翼电子科技有限公司所有。
//
// 硬件:正点原子 STM32F103 Nano V1.3(全部板载,不用额外接线)
// USART1:PA9 = TX,PA10 = RX,板上已连到板载 ST-Link 虚拟串口,下载与串口共用一根 USB 线
// 8 个 LED:PC0~PC7(DS0~DS7),低电平点亮
//
// 本课两个模式(改 usart.h 开头的 APP_MODE 切换,换模式要重新编译下载):
// 模式 1 调试打印 :printf 打印文本,串口助手用"文本"方式看
// 模式 2 二进制收发 :收 2 字节命令帧控制 LED,串口助手要勾"十六进制发送"
//
// 本课特意把串口初始化写在 UART1_Init() 里,而不是藏在驱动文件里:
// 复用功能的时钟、引脚、波特率、中断,一步步都看得见,不是黑盒。
//////////////////////////////////////////////////////////////////////////////////
// ------------------------------------------------------------------------------
// 串口 1 初始化(USART1,115200-8-N-1)
// 和课时01 点灯、课时02 读按键是同一个套路,分三步走:
// ① 开时钟:USART1 挂在 APB2,TX/RX 引脚在 GPIOA
// ② 配引脚:PA9 = TX 复用推挽输出;PA10 = RX 复用输入
// ③ 配参数:波特率、数据位、停止位、校验位、流控、收发方向
// ------------------------------------------------------------------------------
static void UART1_Init(u32 bound)
{
GPIO_InitTypeDef GPIO_Initure;
/* ① 开时钟(不开时钟,后面写寄存器全部无效,而且不报错------课时01 踩过的坑) */
__HAL_RCC_GPIOA_CLK_ENABLE(); // PA9 / PA10 在 GPIOA
__HAL_RCC_USART1_CLK_ENABLE(); // USART1 自己
__HAL_RCC_AFIO_CLK_ENABLE(); // STM32F1 的复用功能要经过 AFIO
/* ② 配引脚:把 PA9、PA10 交给 USART1 去管 */
GPIO_Initure.Pin = GPIO_PIN_9; // PA9 = TX
GPIO_Initure.Mode = GPIO_MODE_AF_PP; // 复用推挽输出:引脚交给外设去拉
GPIO_Initure.Pull = GPIO_PULLUP; // 上拉
GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH; // 高速
HAL_GPIO_Init(GPIOA, &GPIO_Initure);
GPIO_Initure.Pin = GPIO_PIN_10; // PA10 = RX
GPIO_Initure.Mode = GPIO_MODE_AF_INPUT; // 复用输入
HAL_GPIO_Init(GPIOA, &GPIO_Initure);
/* ③ 配参数:两边必须一模一样,否则收到的全是乱码 */
UART1_Handler.Instance = USART1; // 用串口 1
UART1_Handler.Init.BaudRate = bound; // 波特率:115200
UART1_Handler.Init.WordLength = UART_WORDLENGTH_8B; // 8 位数据位
UART1_Handler.Init.StopBits = UART_STOPBITS_1; // 1 位停止位
UART1_Handler.Init.Parity = UART_PARITY_NONE; // 无校验
UART1_Handler.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 无硬件流控
UART1_Handler.Init.Mode = UART_MODE_TX_RX; // 收发都开
HAL_UART_Init(&UART1_Handler); // 真正把上面参数写进寄存器
#if (APP_MODE == 2)
/* ④ 模式 2 要用中断收数据:先给 NVIC 排优先级并放行,再启动一次"收 1 字节" */
HAL_NVIC_SetPriority(USART1_IRQn, 3, 3); // 抢占 3、子 3(数值越大优先级越低)
HAL_NVIC_EnableIRQ(USART1_IRQn); // 放行 USART1 中断通道
HAL_UART_Receive_IT(&UART1_Handler, aRxBuffer, RXBUFFERSIZE); // 开一块 1 字节的地,等数据上门
#endif
}
#if (APP_MODE == 2)
// ------------------------------------------------------------------------------
// 模式 2:命令帧格式(定长 2 字节;串口助手要勾"十六进制发送")
// 第 1 字节:哪个 LED,0x00~0x07 → DS0~DS7
// 第 2 字节:亮还是灭,0x00 = 灭,0x01 = 亮
// 例:发 "00 01" → DS0 亮;发 "03 00" → DS3 灭
// ------------------------------------------------------------------------------
static u8 frame[2]; // 攒命令用的 2 字节小缓冲
static u8 fidx = 0; // 已经攒了几个字节(0 或 1)
// 点亮/熄灭第 n 个灯(PC0~PC7,本板低电平点亮)
static void led_set(u8 n, u8 on)
{
HAL_GPIO_WritePin(GPIOC, (uint16_t)(1U << n), on ? GPIO_PIN_RESET : GPIO_PIN_SET);
}
// USART1 中断入口:向量表按这个名字找它(课时02 的中断入口也是写在 main.c 里)
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&UART1_Handler); // HAL 负责清标志位,再调下面的回调
}
// 收完 1 个字节进这里(HAL 里的弱函数,我们在 main.c 重写一份)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
u8 ch = aRxBuffer[0]; // 这一次收到的那个字节
if (fidx == 0) // 正在等"灯号"
{
if (ch <= 7) // 合法的灯号:收下,接着等"亮/灭"
{
frame[0] = ch;
fidx = 1;
}
}
else // 正在等"亮/灭"
{
if (ch <= 1) // 合法的开关值:命令成立,执行
{
frame[1] = ch;
led_set(frame[0], ch);
fidx = 0;
}
else if (ch <= 7) // 不像开关值、倒像新的灯号:拿它重新起一帧
{
frame[0] = ch;
fidx = 1;
}
else // 都不是:整帧作废,重新等
{
fidx = 0;
}
}
HAL_UART_Receive_IT(huart, aRxBuffer, RXBUFFERSIZE); // 重新开一次接收,等下一个字节
}
#endif
int main(void)
{
u16 times = 0; // 模式 1 用的打印计数
HAL_Init(); // 初始化 HAL 库:顺带配好 1ms 时基
Stm32_Clock_Init(RCC_PLL_MUL9); // 系统时钟:倍频到 72MHz
delay_init(72); // 初始化延时函数
LED_Init(); // 初始化 8 个 LED(PC0~PC7),上电默认全灭
UART1_Init(115200); // 初始化串口 1:115200-8-N-1
#if (APP_MODE == 1)
/* ==================== 模式 1:调试打印(本课主讲) ====================
串口助手:选对 COM 口、波特率 115200、数据位 8、停止位 1、无校验
现象:上电打印两行欢迎语,之后每 500ms 打印一行 "运行中... count = n",
同时 DS0 每 500ms 翻转一次(LED 心跳,说明程序在跑) */
printf("\r\n");
printf("嵌入式技术应用设计实训 | 课时03 串口通信\r\n");
printf("模式 1:printf 调试打印(115200-8-N-1)\r\n");
printf("\r\n");
while (1)
{
printf("运行中... count = %d\r\n", times++); // printf → fputc → uart_putc,同一条路
LED0 = !LED0; // LED 心跳
delay_ms(500);
}
#elif (APP_MODE == 2)
/* ==================== 模式 2:二进制收发(实验三的基础) ==================
串口助手:选对 COM 口、115200,并勾上"十六进制发送"
现象:上电 8 个灯全亮 0.3 秒再全灭(自检:程序在跑、8 路灯都好),
之后安静等命令:发 "00 01" → DS0 亮,发 "00 00" → DS0 灭 */
HAL_GPIO_WritePin(GPIOC, (uint16_t)0x00FF, GPIO_PIN_RESET); // 8 个灯全亮
delay_ms(300);
HAL_GPIO_WritePin(GPIOC, (uint16_t)0x00FF, GPIO_PIN_SET); // 8 个灯全灭
while (1)
{
/* 什么都不用做:命令一字节一字节地来,中断里把两个字节攒齐就改灯
(这就是中断的好处------CPU 不用一直盯着串口,课时02 已经体会过一次) */
}
#endif
}
代码里最值得盯住的 4 处
| 位置 | 为什么这么写 |
|---|---|
UART1_Init() 里的三步 |
开时钟 → 配 PA9/PA10 复用 → 配 115200-8-N-1;和课时01 点灯同一个套路,看得见 |
uart_putc() 里的 while ((USART1->SR & 0x40) == 0); |
等 TC(发送完成)再写 DR;printf 和 uart_send() 都走它,全课只有这一条发送路 |
HAL_UART_RxCpltCallback() 里的三行判断 |
灯号合法才收下、开关值合法才执行、不合法就丢掉重来------这就是"2 字节协议"的全部逻辑 |
while (1) { } 空着的主循环(模式 2) |
中断把主循环解放了:命令一来就进中断改灯,CPU 不用盯着串口(课时02 的思想复用) |
三、实验操作与现象
3.1 接线与操作步骤
本课不用接杜邦线 :USART1 的 TX/RX 在板上已经连到板载 ST-Link 了,你只要插上板子的 USB 线------下载和串口共用这一根线。
| 设备 | 接到哪 | 说明 |
|---|---|---|
| 板载 ST-Link(下载 + 串口一体) | 板上标着 ST-Link 的 USB 口 → 电脑 USB 口 | 一根线管两件事:下载程序 + 串口收发;设备管理器 → 端口里就是 STMicroelectronics STLink Virtual COM Port (COMx) |
| 板上的 SWD 排针 | (本课程不接) | 备用口:只有换外接 下载器时才接 SWDIO / SWCLK / GND |
操作步骤(照着做就能跑):
- 插上板子 USB 线、记下 COM 号 :板载 ST-Link 的驱动在课时00 已经装好(没装就装一次 ST-Link 驱动);把板子上标着 ST-Link 的 USB 口用 USB 线连到电脑,打开
设备管理器 → 端口(COM 和 LPT),看到STMicroelectronics STLink Virtual COM Port (COMx)就对了,把 x 记下来

图注:设备管理器里能看到 STLink Virtual COM Port 及其 COM 号(截图待补) - Keil 打开工程 :
课时文档/代码/课时03-串口通信:USART 收发与 printf/串口通信实验/USER/UART.uvprojx;Options for Target → Debug选ST-Link Debugger;再到Options for Target → Target确认勾上了Use MicroLIB(没勾,模式 1 的printf不会出声) - 选模式 :打开
SYSTEM/usart/usart.h,把开头的#define APP_MODE改成1(调试打印)或2(二进制收发) - 编译下载 :
Build(F7)确认0 Error(s), 0 Warning(s)→Download(F8)确认Programming Done. Verify OK. - 串口助手连上 :打开串口助手(正点原子 XCOM 或任意串口调试助手),选第 1 步记下的那个 COM 号 ,波特率
115200、数据位8、停止位1、校验无、流控无,打开串口 - 按复位键看现象:对照 3.2 的现象逐条核对;模式 1 用"文本"方式看,模式 2 勾上"十六进制发送"
串口助手怎么设(两种模式对照,这是本课最容易翻车的地方)
| 模式 1 调试打印 | 模式 2 二进制收发 | |
|---|---|---|
| 显示方式 | 文本(ASCII) | 看十六进制(HEX) |
| 发送区 | 不用发(板子只打印) | 勾"十六进制发送" ,输入 0001 或 00 01 |
| 常见错误 | 没勾 Use MicroLIB → 一个字都没有 | 没勾 HEX → 00 01 被当成 4 个字符 0 0 空格 0 1,灯不动 |
3.2 观察到的现象
本课现象以随课视频演示为准(视频里会把两个模式连着跑一遍),文档不逐条配截图;你自己做完请拍一张或录一段,作为提交物。
APP_MODE |
看到什么算成功 | 怎么核对 |
|---|---|---|
| 1 调试打印(默认) | 上电后串口助手里先出现两行欢迎语,随后每 500 ms 出现一行 运行中... count = 0、= 1、= 2......;同时板上 DS0 每 500 ms 翻转一次 |
计数连续递增不跳号 ;波特率选错时是一屏乱码(����),不是没有输出 |
| 2 二进制收发 | 上电时 8 个灯全亮 0.3 秒再全灭 ;勾 HEX 发 00 01 → DS0 亮 ;发 00 00 → DS0 灭 ;发 03 01 → DS3 亮 |
灯号与命令一一对上 ;发非法数据(如 09 01、00 05)灯不动;其它灯保持原状态 |
图注:模式 1,串口助手里 count 一行行递增

图注:模式 2,十六进制发送 00 01 后 DS0 亮

看不到现象怎么办(按最可能的顺序查):
| 现象 | 最可能的原因 | 怎么处理 |
|---|---|---|
| 串口助手一个字都没有 | ① 没勾 Use MicroLIB ② COM 口选错 ③ 波特率选错 ④ 串口没"打开" |
先确认勾了 MicroLIB,再逐条对 COM 号和 115200-8-N-1,最后点"打开串口" |
有一屏乱码(�����) |
两边波特率或数据格式不一致 | 串口助手改成 115200 / 8 / 1 / 无校验 / 无流控 |
| 设备管理器里没有 COM 口 | 板载 ST-Link 驱动没装 / USB 线只能充电、不带数据 | 装 ST-Link 驱动;换一根能传数据的 USB 线,并确认插在板子标着 ST-Link 的那个 USB 口上 |
| 模式 2 发命令灯不动 | 没勾"十六进制发送" | 勾上 HEX,重新发 00 01(发的是 2 个字节,不是 4 个字符) |
| 模式 2 只有第一次能点灯 | 忘了重新"武装"接收(HAL_UART_Receive_IT 只在 UART1_Init 里开了一次) |
确认 HAL_UART_RxCpltCallback() 最后一行有重新调用 HAL_UART_Receive_IT() |
改了 APP_MODE 却没变化 |
只保存没重新编译下载 | Build(F7)+ Download(F8),再按复位 |
执行前后对比(改一个数,看一眼差别)
| 操作 | 改之前 | 改之后 |
|---|---|---|
模式 1:把 UART1_Init(115200) 改成 UART1_Init(9600),串口助手仍设 115200 |
一屏正常文字 | 一屏乱码------这就是"两边节拍必须一致"的直观证据 |
模式 2:把 #define APP_MODE 从 2 改成 1 |
发 HEX 命令能点灯 | 变成打印文本,串口助手要切回文本方式看 |
四、学生练习
课堂上按顺序做,一条一条改、一条一条看现象:
| 编号 | 任务 | 提示 | 预期结果 |
|---|---|---|---|
| P3.1 | 亲眼看"节拍必须一致" | 模式 1 里把 UART1_Init(115200) 改成 UART1_Init(9600),串口助手仍设 115200;再把串口助手也改成 9600 |
第一次一屏乱码;两边都改成 9600 后恢复正常 |
| P3.2 | 打印自己的信息 | 在模式 1 的欢迎语里换成自己的名字/学号,再加一行 printf 打印专业班级 |
上电就在串口助手里看到自己的信息 |
| P3.3 | 同一数值两种格式 | 加一行 printf("count=%d, HEX=%02X\r\n", times, times); |
十进制与十六进制并排打印,%02X 能补前导 0 |
| P3.4 | 点亮指定的灯 | 模式 2 里依次发 00 01、07 01、03 01、03 00 |
DS0、DS7、DS3 依次亮起,DS3 再灭,其它灯一直不受影响 |
| P3.5 | 亲眼看"少一句就废" | 把 HAL_UART_RxCpltCallback() 最后那句 HAL_UART_Receive_IT(...) 注释掉,重新下载 |
只能点一次灯,之后再发命令没反应------这就是"只收一次的坑" |
| P3.6 | 挑战:加一条"全部灯"命令 | 自己约定:byte0 = 0xFF 表示"全部灯",byte1 决定亮灭;在回调里循环调用 led_set(i, on) |
发 FF 01 八个灯全亮,发 FF 00 全灭 |
五、大作业(实验三:串口命令控制 8LED)
本作业对应课程大纲里的实验三「串口命令控制 8LED」,当堂完成、当堂验收;做不完的课后接着做,下次课开课先收。
5.1 任务书
把课上模式 2 的"2 字节点一个灯",做成一个能用二进制命令完整控制 8 个 LED 的小程序:
- 必须用
APP_MODE 2(二进制方式)实现;不许改成发字符串命令 (如LED0 ON)。理由要能说出来:定长帧不用解析字符串,上位机也好写 - 命令帧固定 2 字节:
[灯号][亮灭]------灯号0x00~0x07对应 DS0~DS7,亮灭0x00= 灭、0x01= 亮;上电时 8 个灯全灭 - 命令只影响指定的那一个灯 :DS3 亮着的时候发
00 01,DS3 不能受影响 - 收到不合法数据(灯号 > 7、亮灭 > 1)不许死机、不许错乱:丢弃这一帧;能当新帧头就重新起帧
- 保留唯一的发送出口
uart_send():代码里不许再出现HAL_UART_Transmit(...)裸调用,也不许在回调里直接写GPIOx寄存器来点灯(统一走led_set()) - 编译输出
0 Error(s), 0 Warning(s) - 加分项 (任选,做几项算几项):
- 命令非法时回一个字节
0xEE,上位机就知道"这条没被接受"(握手/应答的入门) byte0 = 0xFF表示"全部灯":FF 01全亮、FF 00全灭byte1 = 0x02表示"取反":03 02把 DS3 在亮/灭之间翻一次
- 命令非法时回一个字节
- 挑战项 :把帧升级成
0xAA + 灯号 + 亮灭 + 校验和(4 字节),并写一句"为什么真实协议要有帧头和校验"
5.2 分步提示
- 先把基础版跑通 :
APP_MODE改成2,串口助手勾 HEX,发00 01看 DS0 亮------跑不通就别往下做 - 改灯统一走
led_set():所有点灯动作都调用它,回调里只负责"解析命令",不碰寄存器 - 把判断写清楚 :
fidx == 0时只认0x00~0x07;fidx == 1时只认0x00/0x01,其余按 5.1 第 4 条处理 - 加分项一件一件加 :先加
0xEE回码(用uart_send()发 1 字节),再加0xFF全部灯,最后加取反 - 挑战项:在现有 2 字节前面加帧头、后面加校验和(校验和可以就用"三个字节相加取低 8 位"),收到帧头才开始收数据,校验不对就丢弃
5.3 现象与验收标准
| 验收项 | 标准 |
|---|---|
| 单灯控制 | 上电全灭;发 00 01 → DS0 亮;发 00 00 → DS0 灭 |
| 8 路都能控 | 对 DS0~DS7 各发一次"亮"、一次"灭",8 个灯都能正确响应 |
| 互不影响 | 让 DS2 与 DS5 同时亮着,再发 07 01,DS2、DS5 不受影响 |
| 非法数据 | 发 09 01、00 05、FF FF,灯不动、程序不卡死,之后继续发合法命令仍然有效 |
| 代码结构 | 只有 uart_send() 一个发送出口;点灯只经 led_set() |
| 编译下载 | 0 Error(s), 0 Warning(s);Programming Done. Verify OK. |
| 加分/挑战 | 按 5.1 第 7、8 条逐项现场演示(做多少算多少) |
5.4 提交物
- 代码:
课时文档/代码/课时03-串口通信:USART 收发与 printf/串口通信实验/USER/main.c与SYSTEM/usart/usart.c、usart.h - 现象:一段 20 秒以内的视频(依次发
00 01、07 01、03 01、03 00,让老师看清"哪个命令亮哪个灯"),或按命令前后的两张照片 - 一句话说明:"为什么本课的命令用 2 个字节的二进制,而不是发
LED0 ON这样的字符串?"
六、本节小结与下节预告
本节把串口这条线完整走了一遍:先把它的底子认清------两根线(TX/RX)+ 一个共同节拍(115200-8-N-1,两边必须一致) ;再把 USART1 的初始化一步步写在 main.c 里 (开时钟 → PA9/PA10 复用 → 配参数 → 中断),不再是黑盒;然后用 printf 重定向 (fputc + MicroLIB)把调试信息打到电脑上,并顺手把厂家例程里"printf 走裸寄存器、回显走 HAL_UART_Transmit"的两条发送路,收拢成唯一的 uart_send();最后用 APP_MODE 2 体验了二进制命令帧 ------2 个字节(灯号 + 亮灭)就能控制 8 个灯。三句话记住本课:两边参数一致才有话说 ;printf 的出口是 fputc ;一个串口,同一时刻只留一条路。下节进入定时器:用硬件定时中断把"计时"做准(TIM + 中断),也顺便给串口打印加上精确的时间戳。
本节函数与宏速查
| 名称 | 一句话作用 |
|---|---|
UART1_Init(bound) |
串口 1 初始化(本课写在 main.c):开时钟 → 配 PA9/PA10 → 配 8N1 →(模式 2)开中断 |
HAL_UART_Init(&UART1_Handler) |
把 Init.* 里的参数真正写进 USART1 寄存器 |
HAL_UART_Receive_IT(huart, buf, n) |
以中断方式启动一次接收;每收完一次都要再调一次,否则只收一次 |
HAL_UART_IRQHandler(&UART1_Handler) |
HAL 的中断处理:清标志位,再转回调 |
HAL_UART_RxCpltCallback(huart) |
弱函数,收满一次数据后被调用;本课的"解析命令"就写在这里 |
uart_send(buf, len) |
本课唯一的发送出口 ,printf 和命令帧都走它 |
uart_putc(ch)(static) |
最底层发 1 字节:等 TC 再写 USART1->DR |
fputc(ch, f) |
printf 的出口;重写它,printf 才走串口 |
printf("...%d...") |
格式化打印;Keil 里必须勾 Use MicroLIB 才出声 |
APP_MODE |
演示模式开关:1 调试打印 / 2 二进制收发 |
USART1->SR & 0x40 |
读状态寄存器 bit6(TC,发送完成) |
GPIO_MODE_AF_PP / GPIO_MODE_AF_INPUT |
把引脚交给外设:复用推挽输出 / 复用输入 |
附录
A. 怎么编译下载(照着做就能跑)
text
1. 接线:板子上标着 ST-Link 的 USB 口用一根线插电脑(供电 + 下载 + 串口都走它,不用杜邦线、不用装串口驱动)
2. Keil 打开:课时文档/代码/课时03-串口通信:USART 收发与 printf/串口通信实验/USER/UART.uvprojx
3. 选下载器:Options for Target → Debug → ST-Link Debugger → OK
4. 勾 MicroLIB:Options for Target → Target → 勾 Use MicroLIB(模式 1 的 printf 才出声)
5. 选模式 :SYSTEM/usart/usart.h 开头 #define APP_MODE 1(调试打印)或 2(二进制收发)
6. 编译 :Build(F7),确认 0 Error(s), 0 Warning(s)
7. 下载 :Download(F8),确认 Programming Done. Verify OK.
8. 看现象 :串口助手选对 COM 口、115200-8-N-1 并打开串口,按 RST:
模式 1 → 一行行打印 count;模式 2 → 勾 HEX 发 00 01,DS0 亮
B. 本课代码
本课只改两个文件(完整代码见 2.2 节)。工程里其余文件都是厂家例程现成的,不用动:
| 文件 | 作用 |
|---|---|
USER/UART.uvprojx |
Keil 工程文件(用 Keil 打开这个) |
USER/main.c |
本课自写:UART1_Init()(串口初始化)、USART1_IRQHandler()、HAL_UART_RxCpltCallback()、两个模式的 main() |
SYSTEM/usart/usart.h |
APP_MODE 开关、uart_send() 声明、UART1_Handler / aRxBuffer 外部声明 |
SYSTEM/usart/usart.c |
串口底层:uart_putc()、uart_send()、模式 1 的 fputc() 重定向 |
USER/stm32f1xx_it.c |
中断入口的"官方住处";本课把 USART1_IRQHandler 写在 main.c(与课时02 一致),这个文件不用改 |
HARDWARE/LED/led.c、led.h |
LED 驱动:PC0~PC7 配成推挽输出并上电置全灭,提供 LED0~LED7 宏 |
SYSTEM/sys、SYSTEM/delay |
时钟配置、SysTick 延时(delay_ms) |
HALLIB/、CORE/ |
STM32F1 HAL 库与启动文件 |
OBJ/ |
编译输出(已加入 .gitignore,不进仓库) |
C. 串口参数与命令速查(贴在板子旁边用)
| 项目 | 值 |
|---|---|
| 串口 | USART1 |
| 引脚 | PA9 = TX 、PA10 = RX(板上已连到板载 ST-Link 的虚拟串口,下载与串口共用一根 USB 线) |
| 上位机串口 | 设备管理器 → 端口 → STMicroelectronics STLink Virtual COM Port (COMx)(就是下载器那根 USB 线带出来的) |
| 参数 | 115200 / 8 位数据 / 1 位停止 / 无校验 / 无流控 |
| 模式 1 上位机设置 | 文本(ASCII)方式看;板子只打印,不用发 |
| 模式 2 上位机设置 | 勾"十六进制发送",发 2 个字节 |
| 模式 2 命令示例 | 00 01 → DS0 亮;00 00 → DS0 灭;03 01 → DS3 亮;07 01 → DS7 亮 |
| 非法命令 | 灯号 > 0x07 或亮灭 > 0x01:丢弃这一帧,灯不动 |
| Keil 必须勾 | Options for Target → Target → Use MicroLIB(模式 1 的 printf 才出声) |
相关文档
- 课程大纲(课时03 在"一、基础外设开发模块",实验三同名)
- 课程文档写作规范(0.1 教学环境约定)
- 上一课:课时02 GPIO 输入与外部中断:按键
- 下一课:课时04 定时器:定时中断