课时03 嵌入式技术应用设计实训 | 串口通信:USART 收发二进制 与 printf

嵌入式技术应用设计实训 | 串口通信: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()。

本节目标

  1. 能说出串口通信要几根线、为什么两边的波特率和数据格式必须完全一致,并照着 3.1 的步骤认准板载 ST-Link 带出的虚拟串口(STLink Virtual COM Port)、把串口助手连上板子
  2. 能照着 UART1_Init() 说出串口初始化的三步(开时钟 → 配 PA9/PA10 复用 → 配 115200-8-N-1),并把模式 1 跑通,在串口助手里看到 printf 打印的计数
  3. 能说出 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

操作步骤(照着做就能跑):

  1. 插上板子 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 号(截图待补)
  2. Keil 打开工程 :课时文档/代码/课时03-串口通信:USART 收发与 printf/串口通信实验/USER/UART.uvprojx;Options for Target → Debug 选 ST-Link Debugger;再到 Options for Target → Target 确认勾上了 Use MicroLIB (没勾,模式 1 的 printf 不会出声)
  3. 选模式 :打开 SYSTEM/usart/usart.h,把开头的 #define APP_MODE 改成 1(调试打印)或 2(二进制收发)
  4. 编译下载 :Build(F7)确认 0 Error(s), 0 Warning(s) → Download(F8)确认 Programming Done. Verify OK.
  5. 串口助手连上 :打开串口助手(正点原子 XCOM 或任意串口调试助手),选第 1 步记下的那个 COM 号 ,波特率 115200、数据位 8、停止位 1、校验 无、流控 无,打开串口
  6. 按复位键看现象:对照 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 的小程序:

  1. 必须用 APP_MODE 2(二进制方式)实现;不许改成发字符串命令 (如 LED0 ON)。理由要能说出来:定长帧不用解析字符串,上位机也好写
  2. 命令帧固定 2 字节:[灯号][亮灭]------灯号 0x00~0x07 对应 DS0~DS7,亮灭 0x00 = 灭、0x01 = 亮;上电时 8 个灯全灭
  3. 命令只影响指定的那一个灯 :DS3 亮着的时候发 00 01,DS3 不能受影响
  4. 收到不合法数据(灯号 > 7、亮灭 > 1)不许死机、不许错乱:丢弃这一帧;能当新帧头就重新起帧
  5. 保留唯一的发送出口 uart_send() :代码里不许再出现 HAL_UART_Transmit(...) 裸调用,也不许在回调里直接写 GPIOx 寄存器来点灯(统一走 led_set())
  6. 编译输出 0 Error(s), 0 Warning(s)
  7. 加分项 (任选,做几项算几项):
    • 命令非法时回一个字节 0xEE,上位机就知道"这条没被接受"(握手/应答的入门)
    • byte0 = 0xFF 表示"全部灯":FF 01 全亮、FF 00 全灭
    • byte1 = 0x02 表示"取反":03 02 把 DS3 在亮/灭之间翻一次
  8. 挑战项 :把帧升级成 0xAA + 灯号 + 亮灭 + 校验和(4 字节),并写一句"为什么真实协议要有帧头和校验"

5.2 分步提示

  1. 先把基础版跑通 :APP_MODE 改成 2,串口助手勾 HEX,发 00 01 看 DS0 亮------跑不通就别往下做
  2. 改灯统一走 led_set():所有点灯动作都调用它,回调里只负责"解析命令",不碰寄存器
  3. 把判断写清楚 :fidx == 0 时只认 0x00~0x07;fidx == 1 时只认 0x00/0x01,其余按 5.1 第 4 条处理
  4. 加分项一件一件加 :先加 0xEE 回码(用 uart_send() 发 1 字节),再加 0xFF 全部灯,最后加取反
  5. 挑战项:在现有 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 才出声)

相关文档

相关推荐
阿钱真强道20 小时前
课时02 嵌入式技术应用设计实训 | GPIO 输入与外部中断:按键
外部中断·gpio·按键·nvic·stm32f103·嵌入式入门·消抖
阿钱真强道4 天前
课时00 嵌入式技术应用设计实训 | 开发环境搭建与开发板认识
git·hal库·keil·vs code·开发环境搭建·stm32f103·st-link
阿钱真强道4 天前
课时01 嵌入式技术应用设计实训 | GPIO 输出:跑马灯与蜂鸣器
hal库·keil·gpio·蜂鸣器·跑马灯·推挽输出·stm32f103
lpfasd12315 天前
展厅中控总体架构
go·串口·中控
sunoo-22916 天前
【嵌入式裸机开发Day04】i.MX6ULL 蜂鸣器驱动详解:从引脚地址查找到代码实现全流程
arm开发·单片机·嵌入式硬件·学习·串口·裸机
橘色的喵18 天前
USB枚举、协商、传输:以USB相机为例看识别与格式协商
串口·usb·设备枚举
xx~t22 天前
嵌入式——单片机2
嵌入式硬件·学习·51单片机·串口·蜂鸣器
sunoo-22922 天前
【51单片机实战】温控下位机完整实现:DS18B20采集 + 自定义二进制串口协议 + PWM声光告
笔记·学习·51单片机·串口
橘色的喵22 天前
XCOM:一个 4 MB 的串口调试工具
串口·lua·串口工具·xcom