Keil代码移植_轮询串口2

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

127.4.17Keil代码移植_轮询串口2

/*
 * 整体目的:把上一节基于 USART1 的轮询收发工程迁移到 USART2,并通过外置 USB 转 TTL 模块验证通信。
 * 整体设计:main.c 轮询接收 4 字节命令,收到 "LED1"/"LED2" 后翻转对应 LED;
 *           随后用 USART2 周期发送格式化的温湿度示例和 printf 测试文本。
 * 硬件依据:STM32F103ZET6 默认映射下,USART2_TX 位于 PA2,USART2_RX 位于 PA3;
 *           板载 CH340C 接的是 USART1,因此 USART2 必须通过 PA2、PA3 和 GND 外接 USB 转 TTL 模块。
 * 代码范围:以下列出本节需要理解和修改的项目层代码,来源于配套工程 KEIL/03UART2。
 *           CubeMX 模板版权头、空 USER CODE 区和未改动的中断文件从笔记中省略;
 *           官方 stm32f1xx_hal_uart.c 仍须加入 Keil 工程,但它是 HAL 库依赖,不属于本节逐行改写的业务源码。
 */

/* ==================== Core/Inc/main.h:本节用到的 LED 引脚别名 ==================== */

#define LED2_Pin GPIO_PIN_5                 /* 把 LED2 的引脚编号定义为 5,需与下方 GPIOE 端口组合成 PE5。 */
#define LED2_GPIO_Port GPIOE                /* 指定 LED2 接在 GPIOE;调用 HAL GPIO 接口时与 LED2_Pin 配套使用。 */
#define LED1_Pin GPIO_PIN_5                 /* 把 LED1 的引脚编号定义为 5,需与下方 GPIOB 端口组合成 PB5。 */
#define LED1_GPIO_Port GPIOB                /* 指定 LED1 接在 GPIOB;串口命令最终会翻转这个物理引脚。 */

/* ==================== Core/Inc/uart.h:USART2 对外接口 ==================== */

#ifndef __UART_H__                          /* 若尚未包含 uart.h,则进入头文件正文,防止重复定义。 */
#define __UART_H__                          /* 记录 uart.h 已被包含,后续重复 include 时将跳过正文。 */

#include "main.h"                           /* 引入 HAL 类型、GPIO 定义及 Error_Handler() 等工程公共声明。 */
#include <stdio.h>                          /* 引入 sprintf() 声明,printf 宏需要先把格式化文本写入缓冲区。 */

extern UART_HandleTypeDef huart2;           /* 声明 USART2 的 HAL 句柄;实体在 uart.c 中定义,其他文件可共享它。 */
extern uint8_t u_buf[256];                  /* 声明 printf 重定向使用的 256 字节发送缓冲区,存储空间也在 uart.c。 */

/*
 * 先用 sprintf() 把可变参数格式化到 u_buf,再按实际字符数阻塞发送。
 * 这是课程使用的简化宏:它没有长度保护,格式化结果超过 255 字节会越界;
 * 生产代码宜改用 snprintf(),并避免在中断或多任务并发环境中共享同一缓冲区。
 */
#define printf(...) HAL_UART_Transmit(       /* 用同名宏替代普通 printf,使调用结果从 USART2 发出。 */ \
    &huart2,                                 /* 指向 USART2 句柄,HAL 据此访问 USART2 寄存器和状态。 */ \
    (uint8_t *)u_buf,                        /* 把格式化后的字节缓冲区交给 HAL 发送接口。 */ \
    sprintf((char *)u_buf, __VA_ARGS__),     /* 写入格式化文本,并把返回的字符数作为待发送长度。 */ \
    0xFFFFU)                                 /* 最长等待 65535 ms;在此期间发送函数可能阻塞 CPU。 */

void MDK_USART2_UART_Init(void);            /* 声明本工程封装的 USART2 初始化函数,供 main.c 调用。 */

#endif /* __UART_H__ */                     /* 结束 uart.h 的头文件保护范围。 */

/* ==================== Core/Src/main.c:主流程和轮询收发 ==================== */

#include "main.h"                           /* 引入芯片 HAL 公共声明、引脚别名和 Error_Handler() 原型。 */
#include "gpio.h"                           /* 引入 MDK_GPIO_Init() 声明,用来初始化两路 LED。 */
#include "uart.h"                           /* 引入 huart2、printf 宏和 MDK_USART2_UART_Init() 声明。 */
#include <string.h>                         /* 引入 strcmp()、strlen() 和 memset() 的标准声明。 */

void SystemClock_Config(void);              /* 提前声明系统时钟配置函数,函数实体位于本文件后部。 */

uint8_t UART_Recv[5];                       /* 分配 5 字节接收区:4 字节命令后保留 1 字节全局初值 0 作为字符串结尾。 */
uint8_t DHT11_Buf[30];                      /* 分配 30 字节发送区,保存本节写死的温湿度格式化示例。 */

int main(void)                              /* 程序入口;启动文件完成栈和数据段初始化后调用这里。 */
{                                           /* main() 函数体开始。 */
    HAL_Init();                             /* 复位 HAL 外设状态并建立 1 ms SysTick,为超时计时等 HAL 功能打基础。 */
    SystemClock_Config();                   /* 把系统时钟配置为 72 MHz,并设置 APB1/APB2 分频。 */
    MDK_GPIO_Init();                        /* 开启相关 GPIO 时钟,将 PB5、PE5 初始化为 LED 输出。 */
    MDK_USART2_UART_Init();                 /* 初始化 USART2 及 PA2/PA3,后续收发必须使用同一个 huart2。 */

    while (1)                               /* 裸机程序持续轮询;循环不会主动退出。 */
    {                                       /* 一次“接收命令并发送状态文本”的循环开始。 */
        HAL_UART_Receive(                    /* 以阻塞轮询方式等待串口数据,返回值未保存。 */
            &huart2,                         /* 指定从 USART2 接收,而不是上一工程的 USART1。 */
            UART_Recv,                       /* 把收到的字节依次写入 5 字节接收数组的前 4 字节。 */
            4U,                              /* 每次期望接收恰好 4 字节,正好容纳 "LED1" 或 "LED2"。 */
            1000U);                          /* 最多等待 1000 ms;超时后仍继续执行后面的比较和发送。 */

        if (strcmp("LED1",                   /* 比较接收字符串是否与命令 "LED1" 完全相同。 */
                   (char *)UART_Recv) == 0)  /* strcmp() 返回 0 表示相等;强制转换用于匹配 char 指针参数。 */
        {                                   /* “LED1”命令处理分支开始。 */
            HAL_GPIO_TogglePin(              /* 翻转 LED1 当前输出:高变低、低变高。 */
                LED1_GPIO_Port,              /* 选择 LED1 所在的 GPIOB 端口。 */
                LED1_Pin);                   /* 选择端口中的 5 号引脚,即 PB5。 */
            memset(UART_Recv,                /* 命令处理后清空已接收的 4 个字符,避免下一轮重复匹配。 */
                   '\0',                     /* 用数值 0 填充,它也是 C 字符串的结束标志。 */
                   4U);                      /* 只清前 4 字节;第 5 字节作为结尾本来就是 0。 */
        }                                   /* “LED1”命令处理分支结束。 */

        if (!strcmp("LED2",                  /* strcmp 相等时返回 0,逻辑非运算后变成真。 */
                    (char *)UART_Recv))      /* 将接收缓冲区按以 0 结尾的字符串与 "LED2" 比较。 */
        {                                   /* “LED2”命令处理分支开始。 */
            HAL_GPIO_TogglePin(              /* 翻转 LED2 当前输出电平。 */
                LED2_GPIO_Port,              /* 选择 LED2 所在的 GPIOE 端口。 */
                LED2_Pin);                   /* 选择端口中的 5 号引脚,即 PE5。 */
            memset(UART_Recv,                /* 清除刚刚处理的命令,避免残留内容影响下一次判断。 */
                   '\0',                     /* 以空字符覆盖原有字节。 */
                   4U);                      /* 清除本轮实际接收的 4 字节。 */
        }                                   /* “LED2”命令处理分支结束。 */

        sprintf((char *)DHT11_Buf,           /* 把浮点示例转换为可由串口发送的 ASCII 文本。 */
                "H2:%.1f T2:%.2f\n",        /* 湿度保留 1 位小数、温度保留 2 位小数,并在末尾换行。 */
                30.1,                        /* 对应第一个 %.1f,输出为 30.1;这里是写死的演示值。 */
                36.5);                       /* 对应第二个 %.2f,输出为 36.50;这里同样不是真实传感器数据。 */

        HAL_UART_Transmit(                   /* 以阻塞轮询方式发送格式化后的温湿度文本。 */
            &huart2,                         /* 使用 USART2 句柄,数据会从 PA2/USART2_TX 输出。 */
            DHT11_Buf,                       /* 指向刚由 sprintf() 写好的发送缓冲区。 */
            strlen((char *)DHT11_Buf),       /* 计算字符串长度,不把末尾的 '\0' 发送出去。 */
            1000U);                          /* 最多等待 1000 ms 完成发送。 */

        printf("Hellow world wfeng %d!\n",  /* 通过 uart.h 的宏再发送一行测试文本;Hellow 是配套源码原文。 */
               123);                         /* 将整数 123 代入 %d。 */
    }                                       /* 本轮结束,立即返回 while 顶部再次等待 4 字节命令。 */
}                                           /* main() 语法边界;正常运行不会从无限循环返回。 */

/* ==================== Core/Src/main.c:72 MHz 系统时钟 ==================== */

void SystemClock_Config(void)               /* 配置振荡器、PLL、系统时钟及总线分频。 */
{                                           /* SystemClock_Config() 函数体开始。 */
    RCC_OscInitTypeDef RCC_OscInitStruct = {0}; /* 清零振荡器配置结构体,避免未赋值字段含随机数据。 */
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /* 清零时钟树配置结构体。 */

    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; /* 本工程选择外部高速晶振 HSE。 */
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;                   /* 打开 HSE 振荡器。 */
    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;    /* HSE 送入 PLL 前不分频。 */
    RCC_OscInitStruct.HSIState = RCC_HSI_ON;                   /* 同时保持内部高速振荡器开启。 */
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;               /* 开启 PLL 倍频器。 */
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;       /* 选择 HSE 作为 PLL 输入源。 */
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;               /* 8 MHz HSE 乘 9 得到 72 MHz PLL 时钟。 */

    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)       /* 应用振荡器配置,并检查 HAL 返回状态。 */
    {                                                          /* 振荡器启动或 PLL 配置失败时进入异常分支。 */
        Error_Handler();                                       /* 停止正常业务,避免在错误时钟下继续运行。 */
    }                                                          /* 振荡器错误判断结束。 */

    RCC_ClkInitStruct.ClockType =                               /* 指定下面要同时配置四类时钟。 */
        RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK |             /* 包括 AHB/HCLK 与系统时钟 SYSCLK。 */
        RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;              /* 还包括 APB1/PCLK1 与 APB2/PCLK2。 */
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;   /* 选择 72 MHz PLL 输出作为系统时钟。 */
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;          /* AHB 不分频,因此 HCLK 为 72 MHz。 */
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;           /* APB1 分频为 36 MHz,USART2 位于这条总线。 */
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;           /* APB2 不分频,PCLK2 为 72 MHz。 */

    if (HAL_RCC_ClockConfig(                                   /* 应用系统与总线时钟配置。 */
            &RCC_ClkInitStruct,                                /* 传入上面填写的时钟树结构体。 */
            FLASH_LATENCY_2) != HAL_OK)                        /* 72 MHz 时使用 2 个 Flash 等待周期,并检查结果。 */
    {                                                          /* 时钟切换失败时进入异常分支。 */
        Error_Handler();                                       /* 禁止程序带着未知时钟状态继续运行。 */
    }                                                          /* 总线时钟错误判断结束。 */
}                                                              /* 系统时钟配置函数结束。 */

/* ==================== Core/Src/gpio.c:LED 输出初始化 ==================== */

#include "gpio.h"                           /* 引入 GPIO 初始化声明及 main.h 中的 LED 引脚别名。 */

void MDK_GPIO_Init(void)                    /* 配置本实验需要的 PB5、PE5 两路 LED 输出。 */
{                                           /* GPIO 初始化函数体开始。 */
    GPIO_InitTypeDef GPIO_InitStruct = {0}; /* 清零 GPIO 配置结构体,随后重复填写并用于两只 LED。 */

    __HAL_RCC_GPIOE_CLK_ENABLE();           /* 开启 GPIOE 外设时钟,否则无法配置和驱动 PE5。 */
    __HAL_RCC_GPIOA_CLK_ENABLE();           /* 开启 GPIOA 时钟;USART2 的 PA2、PA3 也需要它。 */
    __HAL_RCC_GPIOB_CLK_ENABLE();           /* 开启 GPIOB 外设时钟,否则无法配置和驱动 PB5。 */

    HAL_GPIO_WritePin(                      /* 在切换为输出模式前先写入 LED2 的初始锁存电平。 */
        LED2_GPIO_Port,                     /* 选择 GPIOE。 */
        LED2_Pin,                           /* 选择 PE5。 */
        GPIO_PIN_SET);                      /* 输出高电平;本板 LED 低电平点亮,因此上电先熄灭。 */
    HAL_GPIO_WritePin(                      /* 同样预置 LED1 的初始输出电平。 */
        LED1_GPIO_Port,                     /* 选择 GPIOB。 */
        LED1_Pin,                           /* 选择 PB5。 */
        GPIO_PIN_SET);                      /* 输出高电平,使 LED1 初始保持熄灭。 */

    GPIO_InitStruct.Pin = LED2_Pin;         /* 本轮准备配置 PE5。 */
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; /* 设为通用推挽输出,可主动输出高、低电平。 */
    GPIO_InitStruct.Pull = GPIO_PULLUP;     /* 请求上拉;F1 输出模式主要由 ODR 驱动,初值仍应显式设置。 */
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; /* LED 无需高速翻转,使用低速输出可减小边沿干扰。 */
    HAL_GPIO_Init(                          /* 把上述模式真正写入 PE5 的 GPIO 配置寄存器。 */
        LED2_GPIO_Port,                     /* 选择 GPIOE 端口。 */
        &GPIO_InitStruct);                  /* 传入 GPIO 配置结构体地址。 */

    GPIO_InitStruct.Pin = LED1_Pin;         /* 复用同一结构体,只把目标引脚改为 PB5。 */
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; /* 将 PB5 配置为通用推挽输出。 */
    GPIO_InitStruct.Pull = GPIO_PULLUP;     /* 保留与 LED2 相同的上拉配置。 */
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; /* 同样使用低速输出。 */
    HAL_GPIO_Init(                          /* 把配置写入 PB5 对应寄存器。 */
        LED1_GPIO_Port,                     /* 选择 GPIOB 端口。 */
        &GPIO_InitStruct);                  /* 传入已更新的配置结构体地址。 */
}                                           /* GPIO 初始化函数结束。 */

/* ==================== Core/Src/uart.c:USART2 参数、时钟和引脚 ==================== */

#include "uart.h"                           /* 引入 UART HAL 类型、huart2 对外声明和初始化函数原型。 */

uint8_t u_buf[256];                         /* 定义 printf 宏使用的全局缓冲区,静态存储期使其初始值全为 0。 */
UART_HandleTypeDef huart2;                  /* 定义 USART2 HAL 句柄,保存实例、参数、状态及错误码。 */

void MDK_USART2_UART_Init(void)             /* 填写 USART2 通信参数并让 HAL 完成外设初始化。 */
{                                           /* USART2 初始化函数体开始。 */
    huart2.Instance = USART2;               /* 绑定真实 USART2 寄存器基地址,这是从 USART1 迁移的关键项。 */
    huart2.Init.BaudRate = 115200;          /* 设置波特率为 115200 bit/s,串口助手必须使用相同数值。 */
    huart2.Init.WordLength = UART_WORDLENGTH_8B; /* 每帧包含 8 个数据位。 */
    huart2.Init.StopBits = UART_STOPBITS_1; /* 每帧使用 1 个停止位。 */
    huart2.Init.Parity = UART_PARITY_NONE;  /* 不使用奇偶校验位。 */
    huart2.Init.Mode = UART_MODE_TX_RX;     /* 同时开启发送器和接收器。 */
    huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 不使用 RTS/CTS 硬件流控,只接 TX、RX、GND。 */
    huart2.Init.OverSampling = UART_OVERSAMPLING_16; /* 每位按 16 倍过采样,供接收判决和波特率生成使用。 */

    if (HAL_UART_Init(&huart2) != HAL_OK)   /* 初始化 UART;HAL 内部还会回调下方的 HAL_UART_MspInit()。 */
    {                                       /* UART 参数无效或初始化失败时进入错误分支。 */
        Error_Handler();                    /* 停止正常流程,便于定位初始化错误。 */
    }                                       /* UART 初始化结果判断结束。 */
}                                           /* USART2 参数初始化函数结束。 */

void HAL_UART_MspInit(                      /* HAL_UART_Init() 调用的底层硬件支持初始化回调。 */
    UART_HandleTypeDef *uartHandle)          /* 接收当前待初始化的 UART 句柄,可能对应不同 USART 实例。 */
{                                           /* MSP 初始化函数体开始。 */
    GPIO_InitTypeDef GPIO_InitStruct = {0}; /* 清零临时 GPIO 配置结构体。 */

    if (uartHandle->Instance == USART2)     /* 只在本次句柄确实绑定 USART2 时执行以下配置。 */
    {                                       /* USART2 专属底层配置开始。 */
        __HAL_RCC_USART2_CLK_ENABLE();      /* 开启 APB1 上的 USART2 外设时钟;漏掉它时串口无法工作。 */
        __HAL_RCC_GPIOA_CLK_ENABLE();       /* 开启 GPIOA 时钟,以便配置 PA2 和 PA3。 */

        GPIO_InitStruct.Pin = GPIO_PIN_2;   /* 选择 PA2,它是默认映射下的 USART2_TX。 */
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; /* 将 PA2 设为复用推挽输出,由 USART2 而非普通 GPIO 驱动。 */
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; /* 使用高速输出能力以保证串口边沿质量。 */
        HAL_GPIO_Init(                      /* 应用 PA2 的 TX 配置。 */
            GPIOA,                          /* 选择 GPIOA 端口。 */
            &GPIO_InitStruct);              /* 传入 PA2 配置结构体地址。 */

        GPIO_InitStruct.Pin = GPIO_PIN_3;   /* 选择 PA3,它是默认映射下的 USART2_RX。 */
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT; /* 将 PA3 配置为输入,让 USART2 接收外部模块的 TXD。 */
        GPIO_InitStruct.Pull = GPIO_NOPULL; /* 不启用内部上下拉,空闲高电平由对端串口发送器维持。 */
        HAL_GPIO_Init(                      /* 应用 PA3 的 RX 配置。 */
            GPIOA,                          /* PA3 同样属于 GPIOA。 */
            &GPIO_InitStruct);              /* 传入更新后的配置结构体地址。 */
    }                                       /* USART2 专属底层配置结束。 */
}                                           /* MSP 初始化回调结束。 */

一、回顾上节内容

1.1 添加urt.c文件

手工 Keil 工程需要把 stm32f1xx_hal_uart.c 加入编译,并把业务层 uart.c 加入 Core/Src 分组。两者作用不同:

  • HAL 驱动实现 HAL_UART_Init/Receive/Transmit;
  • 业务层文件定义 huart1/huart2、初始化参数和 GPIO MSP。

仅有头文件声明不能生成函数实体,链接时仍会报告 undefined symbol。

1.2 修改配置文件

stm32f1xx_hal_conf.h 使用条件编译控制外设模块:

#define HAL_UART_MODULE_ENABLED

#ifdef HAL_UART_MODULE_ENABLED
#include "stm32f1xx_hal_uart.h"
#endif

宏被注释时,相关声明和代码不参与编译,编辑器可能显示为灰色。启用实际使用的模块、关闭完全不用的模块,可以减少固件和编译负担,但必须保证依赖关系完整。

二、任务

本节将 USART1 轮询工程迁移到 USART2。软件和硬件都要同步改变:句柄、实例、初始化函数、外设时钟、引脚以及电脑连接通路。

2.1 关闭上一个工程

2.1.1 复制工程改名

复制 03UART1 并命名为 03UART2。打开工程后先确认 Keil 实际加载的是副本中的工程文件,避免在两个相似目录之间编辑错目标。

2.1.2 找到main函数

从 main.c 的初始化和所有 HAL 调用开始搜索 huart1、USART1、MDK_USART1_UART_Init,再检查 uart.h 和 uart.c。不能只批量替换数字,还要根据 STM32F103 的真实硬件映射逐项确认。

2.1.2.1 修改头文件

uart.h 中应声明:

extern UART_HandleTypeDef huart2;
void MDK_USART2_UART_Init(void);

若 printf 宏或 _write() 也通过 USART2 输出,其内部句柄必须使用 huart2。声明、定义和调用三处名称必须一致。

2.1.2.2 修改函数声明

主函数初始化调用改成 MDK_USART2_UART_Init(),所有轮询收发调用使用 &huart2。SystemClock_Config() 无需仅因切换串口而改名,但要确保 APB1 时钟满足 USART2 波特率计算。

2.1.2.3 修改urt.c文件

需要核对的标识符:

USART1工程USART2工程
huart1huart2
USART1USART2
MDK_USART1_UART_InitMDK_USART2_UART_Init
__HAL_RCC_USART1_CLK_ENABLE__HAL_RCC_USART2_CLK_ENABLE
PA9 / PA10PA2 / PA3

批量替换后仍要人工检查注释和 GPIO。配套源码的注释把 PA3 写成 USART3_RX,但实际配置和芯片复用功能均为 PA3/USART2_RX。

2.1.2.4 修改函数实体

USART2 初始化核心:

huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;

MSP 中开启 USART2 和 GPIOA 时钟,PA2 配置为复用推挽 TX,PA3 配置为浮空/无上下拉输入 RX。USART2 位于 APB1,HAL 使用 PCLK1 计算 BRR;USART1 位于 APB2,使用 PCLK2。

2.2 编译并查看效果

程序通过编译只说明符号和类型基本正确,不能证明实际线路已经连接。板载 Type-C/CH340C 仍连接 USART1 的 PA9/PA10,切换到 USART2 后,原 USB 口不会自动转接到 PA2/PA3。

2.3 问题所在

开发板原理图中的固定连接:

板载CH340C TXD ──> PA10 / USART1_RX
板载CH340C RXD <── PA9  / USART1_TX

PA2/PA3 只引到芯片和排针,没有接入板载 CH340C。因此需要一个外置 USB 转 TTL 模块。

2.4 硬件改变

USART2 使用 3.3 V TTL/CMOS 电平。外置模块应工作在兼容的 IO 电压,不能把 RS-232 正负电压直接接到 MCU。

如果开发板已经单独供电,通常只接 GND、TXD、RXD;不要再把模块 5 V 与板上电源盲目相连,避免两个电源互相反灌。

三、串口模块的连接

正确交叉连接:

USB转TTL模块                STM32F103
TXD  ────────────────────> PA3 / USART2_RX
RXD  <──────────────────── PA2 / USART2_TX
GND  ───────────────────── GND

为什么 TX 要接 RX:发送端输出的串行位流必须进入接收端输入。为什么必须共地:TTL 是单端信号,接收器通过信号线相对 GND 的电压判断 0/1;没有共同参考电位,逻辑阈值就不可靠。

flowchart LR
    PC[电脑USB] <-->|USB协议| CH340[外置CH340]
    CH340 -- TXD --> PA3[PA3 USART2_RX]
    PA2[PA2 USART2_TX] -- TXD --> CH340
    CH340 --- GND[共同GND]
    GND --- MCU[STM32地]

四、串口助手设置

插入外置模块后,系统会增加一个 COM 口。关闭旧串口连接,选择新 COM 口并设置 115200、8 数据位、无校验、1 停止位、无流控。

若能打开但没有数据,依次检查:

  1. TX/RX 是否交叉;
  2. GND 是否共地;
  3. 模块逻辑电压是否兼容;
  4. 代码是否真正调用 MDK_USART2_UART_Init();
  5. USART2 时钟和 GPIOA 时钟是否开启;
  6. 串口助手是否选中了外置模块对应的 COM 口。

五、修改时钟问题

USART2 必须执行 __HAL_RCC_USART2_CLK_ENABLE()。PA2、PA3 属于 GPIOA,因此还需 __HAL_RCC_GPIOA_CLK_ENABLE()。

若使用 USART3 的默认 PB10/PB11,则要开启 USART3 和 GPIOB 时钟;若使用重映射引脚,还需配置 AFIO 重映射。不能只根据“串口编号”机械更换 GPIO 端口。

六、投屏控制灯

外接线路正确后,串口助手发送 LED1 或 LED2,USART2 接收 4 字节,主循环比较命令并翻转 PB5/PE5 LED。发送端使用 huart2 后,格式化温湿度和 printf 输出也应从外置模块的 COM 口出现。

LED 电路仍是低电平有效;切换 USART 不会改变 LED 的 GPIO 或电气逻辑。

七、串口助手问题

浮点格式:

格式符示例值 30.1结果
%f30.130.100000
%.1f30.130.1
%.2f30.130.10

小数位数控制只影响文本格式,不影响 UART 波特率。Keil 使用的 C 库也可能默认裁剪浮点 printf;若 %f 无法输出,应检查所用运行库和链接选项,或用定点整数格式减少 Flash 占用。