欢迎光临
我们一直在努力

课时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()。

本节目标

  • 能说出串口通信要几根线、为什么两边的波特率和数据格式必须完全一致,并照着 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(),让上面两个入口都走它:

    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/串口通信实验/ 下):

    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(完整代码)

    #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(完整代码)

    #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(完整代码)

    #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. 怎么编译下载(照着做就能跑)

    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 定时器:定时中断
    赞(0)
    未经允许不得转载:171主机测评 » 课时03 嵌入式技术应用设计实训 | 串口通信:USART 收发二进制 与 printf
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址