HC32 串口轮询 — 收发数据与 printf 打印

HC32F460 USART 轮询收发实验:配置 USART2 为 115200-8-N-1,实现回显(收到啥发回啥),并重定向 fputc 让 printf 直接往串口打印。

串口轮询:收发数据与 printf 打印

源码路径:example/HC32F460JEUA/2.usart/uart_polling/source/main.c

1. 功能简介

这个例子做两件事:

  1. 回显(Echo):你在电脑串口助手里发一个字符,单片机原样发回来。发 A 收到 A,发 hello 收到 hello。这是验证串口通不通的经典测试。
  2. printf 打印:程序启动时会往串口打印一句 test,证明 printf 已经被成功「接」到串口上了。

「轮询」是什么意思?就是 CPU 不停地去问:「有数据来了吗?有数据来了吗?」——问到了就处理。这和「中断」相对(中断是数据来了主动通知 CPU)。轮询最简单直观,适合入门;缺点是 CPU 一直在原地打转问,干不了别的事。

学会这个例子,你就拥有了串口调试能力——以后任何程序都能用 printf 把内部状态打到电脑上看,这是嵌入式开发最基本的生存技能。


2. 硬件准备

用 HC32F460JEUA KIT 开发板,板载有 USB 转串口,一根 USB 线搞定,不用额外接线。

1
2
3
4
5
6
7
8
9
   电脑                          HC32F460 开发板
 ┌───────┐   USB线    ┌──────────────────────────┐
 │       │◀─────────▶│ USB口 ─ CH340等转串口芯片 │
 │ 串口  │            │            │              │
 │ 助手  │            │      ┌─────┴─────┐        │
 └───────┘            │      │ PA15 (TX) │──发──▶ │
                      │      │ PA10 (RX) │◀─收──  │
                      │      └───────────┘        │
                      └──────────────────────────┘

如果你的板子没有板载转串口芯片,就要外接一个 USB-TTL 模块(如 CH340、CP2102),接法是交叉连

1
2
3
4
   USB-TTL 模块          HC32F460
      TX  ───────────────  PA10 (RX)     模块的发,接单片机的收
      RX  ───────────────  PA15 (TX)     模块的收,接单片机的发
      GND ───────────────  GND           共地!必须接,否则不通

两个新手必踩的坑

  1. TX 接 RX,RX 接 TX(交叉),不是 TX 接 TX。想想「我的嘴对着你的耳朵」。
  2. 一定要共地(GND 相连),否则没有共同参考电平,通信必失败。

电脑端准备一个串口助手软件(如 sscom、友善串口助手、或 VSCode 串口插件),波特率设 115200


3. 完整代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
#include "hc32_ddl.h"
#include "stdio.h"                       // 用 printf 必须包含

/* ---------- 串口参数定义 ---------- */
#define USART_CH          (M4_USART2)    // 用 USART2
#define USART_BAUDRATE    (115200ul)     // 波特率 115200

/* RX 引脚:PA10,复用为 USART2 的接收 */
#define USART_RX_PORT     (PortA)
#define USART_RX_PIN      (Pin10)
#define USART_RX_FUNC     (Func_Usart2_Rx)

/* TX 引脚:PA15,复用为 USART2 的发送 */
#define USART_TX_PORT     (PortA)
#define USART_TX_PIN      (Pin15)
#define USART_TX_FUNC     (Func_Usart2_Tx)

/* ---------- 串口接收错误处理 ---------- */
static void UsartRxErrProcess(void)
{
    /* 帧错误:清掉标志 */
    if (Set == USART_GetStatus(USART_CH, UsartFrameErr))
        USART_ClearStatus(USART_CH, UsartFrameErr);
    /* 校验错误:清掉标志 */
    if (Set == USART_GetStatus(USART_CH, UsartParityErr))
        USART_ClearStatus(USART_CH, UsartParityErr);
    /* 溢出错误(没及时读,新数据把旧的冲了):清掉标志 */
    if (Set == USART_GetStatus(USART_CH, UsartOverrunErr))
        USART_ClearStatus(USART_CH, UsartOverrunErr);
}

/* ---------- 关键:重定向 fputc,让 printf 走串口 ---------- */
int fputc(int ch, FILE *f)
{
    USART_SendData(M4_USART2, (uint8_t)ch);                   // 发一个字节
    while(Reset == USART_GetStatus(M4_USART2, UsartTxEmpty)); // 等它发完
    return ch;
}

int32_t main(void)
{
    uint16_t u16RxData;
    en_result_t enRet = Ok;

    /* 要开的 USART 时钟(这里一次把 USART1~4 都开了)*/
    uint32_t u32Fcg1Periph = PWC_FCG1_PERIPH_USART1 | PWC_FCG1_PERIPH_USART2 |
                             PWC_FCG1_PERIPH_USART3 | PWC_FCG1_PERIPH_USART4;

    /* 串口初始化参数:8 数据位、1 停止位、无校验 —— 即 8-N-1 */
    const stc_usart_uart_init_t stcInitCfg = {
        UsartIntClkCkNoOutput,   // 时钟不从引脚输出
        UsartClkDiv_1,           // 时钟不分频
        UsartDataBits8,          // 8 位数据
        UsartDataLsbFirst,       // 低位先发
        UsartOneStopBit,         // 1 位停止位
        UsartParityNone,         // 无校验
        UsartSampleBit8,         // 8 倍采样
        UsartStartBitFallEdge,   // 起始位下降沿检测
        UsartRtsEnable,
    };

    BSP_CLK_Init();              // 系统时钟初始化(波特率算得准就靠它)
    BSP_LED_Init();              // LED 初始化

    /* 关掉 TDI/TDO 调试脚的复用(因为它们可能和串口引脚冲突)*/
    PORT_DebugPortSetting(TDI, Disable);
    PORT_DebugPortSetting(TDO_SWO, Disable);

    /* 开 USART 外设时钟 */
    PWC_Fcg1PeriphClockCmd(u32Fcg1Periph, Enable);

    /* 引脚复用为串口功能 */
    PORT_SetFunc(USART_RX_PORT, USART_RX_PIN, USART_RX_FUNC, Disable);
    PORT_SetFunc(USART_TX_PORT, USART_TX_PIN, USART_TX_FUNC, Disable);

    /* 初始化 UART,失败就卡死(方便发现问题)*/
    enRet = USART_UART_Init(USART_CH, &stcInitCfg);
    if (enRet != Ok) { while (1){} }

    /* 设置波特率,失败也卡死 */
    enRet = USART_SetBaudrate(USART_CH, USART_BAUDRATE);
    if (enRet != Ok) { while (1){} }

    /* 使能收发 */
    USART_FuncCmd(USART_CH, UsartRx, Enable);
    USART_FuncCmd(USART_CH, UsartTx, Enable);

    BSP_LED_On(LED_RED);         // 点红灯表示初始化完成
    printf("test");              // 用 printf 打印,验证重定向成功

    while (1)
    {
        /* 轮询:接收缓冲区非空 = 有新数据来了 */
        if (Set == USART_GetStatus(USART_CH, UsartRxNoEmpty))
        {
            u16RxData = USART_RecData(USART_CH);   // 读出这个字节

            /* 等发送缓冲区空了,再把它发回去(回显)*/
            while (Reset == USART_GetStatus(USART_CH, UsartTxEmpty)){}
            USART_SendData(USART_CH, u16RxData);
        }

        UsartRxErrProcess();     // 顺手处理可能的接收错误
    }
}

4. 代码分段讲解

4.1 fputc 重定向——printf 能用的秘密

1
2
3
4
5
6
int fputc(int ch, FILE *f)
{
    USART_SendData(M4_USART2, (uint8_t)ch);
    while(Reset == USART_GetStatus(M4_USART2, UsartTxEmpty));
    return ch;
}

这是整个例子里最值得记住的一段

C 语言的 printf 底层最终会一个字符一个字符地调用 fputc() 把内容「打」出去。默认情况下 fputc 不知道该往哪打(单片机又没显示器)。我们只要自己重写一个 fputc,在里面写「把这个字符从串口发出去」,printf 就自动改道走串口了。

从此以后,printf("温度 = %d\n", temp); 打印的内容就会出现在电脑串口助手里。这就是嵌入式调试的「打印大法」。

Keil (MDK) 环境下用这个方法,通常还要在工程里勾选 Use MicroLIB(微库),否则 printf 会很大甚至链接不过。

4.2 初始化结构体 = 串口参数

1
2
3
4
5
6
7
8
const stc_usart_uart_init_t stcInitCfg = {
    ...
    UsartDataBits8,      // 8 位数据
    ...
    UsartOneStopBit,     // 1 位停止位
    UsartParityNone,     // 无校验
    ...
};

这几行就是把「8-N-1」翻译成代码。要和串口助手那头设的一模一样。

4.3 波特率单独设

1
USART_SetBaudrate(USART_CH, 115200ul);

注意波特率不在初始化结构体里,而是单独一个函数设置。它内部会根据当前系统时钟频率算出分频系数,所以前面 BSP_CLK_Init() 必须先调,否则算出来的波特率是错的(→ 乱码)。

4.4 主循环:轮询回显

1
2
3
4
5
6
if (Set == USART_GetStatus(USART_CH, UsartRxNoEmpty))  // 收到数据了吗?
{
    u16RxData = USART_RecData(USART_CH);               // 读出来
    while (Reset == USART_GetStatus(USART_CH, UsartTxEmpty)){}  // 等能发
    USART_SendData(USART_CH, u16RxData);               // 原样发回
}

这就是「轮询」的精髓:CPU 在 while(1) 里反复检查 UsartRxNoEmpty 标志。一旦有数据,就读出来再发回去,形成回显。

4.5 错误处理别漏

UsartRxErrProcess() 检查三种错误(帧错误、校验错误、溢出错误)并清除标志。溢出错误(Overrun)尤其重要:如果数据来得太快、CPU 没及时读走,新数据会把旧数据冲掉并置位溢出标志,不清的话串口可能就卡住不再收了。


5. 实验现象

  1. 上电后,红灯亮(初始化完成的标志)。
  2. 电脑串口助手里立刻收到一行 test(printf 打印的)。
  3. 你在串口助手里发送任意字符,会原样收到同样的字符(回显)。发 123123

6. 常见问题

Q1:串口助手全是乱码 锟斤拷 最经典的问题,按顺序排查:

  • 串口助手波特率是不是 115200?(第一嫌疑)
  • 数据位/停止位/校验是不是 8-N-1?
  • 代码里 BSP_CLK_Init() 调了没?没调时钟没对,波特率就是错的。

Q2:什么都收不到?

  • 串口助手选的 COM 口对不对?(设备管理器里看 CH340 是哪个 COM)
  • 外接 USB-TTL 的话,TX/RX 交叉了吗?GND 接了吗?
  • 红灯亮了吗?没亮说明初始化就卡住了(可能 USART_UART_Init 返回失败,被 while(1) 困住)。

Q3:printf 打印的中文乱码?

  • 串口助手的编码要和源文件编码一致(一般选 GBK 或 UTF-8 试试)。英文一般不会乱。

Q4:能发但收不到回显?

  • 检查 RX 引脚(PA10)复用配了没、接线对不对。发得出去说明 TX 没问题,收不到就是 RX 那条链路的事。

Q5:printf 打印很卡、或者程序变得很大编不过?

  • Keil 里勾上 Use MicroLIB

7. 小结

  • 串口轮询 = CPU 反复查 UsartRxNoEmpty 标志,有数据就处理。
  • 配置顺序:开时钟 → 配引脚复用 → USART_UART_InitUSART_SetBaudrate → 使能收发
  • 重写 fputc 就能让 printf 走串口,这是头号调试技能
  • 乱码找波特率,收不到找接线和 COM 口。
  • 别忘处理接收错误(尤其溢出)。

8. 拓展应用

串口是通往无数项目的大门:

  • 调试打印:任何项目都能用 printf 输出变量、状态、日志。学会它,你的开发效率翻倍。
  • 上位机通信:单片机采集数据(温度、电流),通过串口发给电脑上的 Python/C# 程序画曲线、存数据库。
  • 蓝牙 / WiFi 模块:HC-05 蓝牙、ESP8266 WiFi 这类模块都是串口通信的,学会串口就能用手机/网络远程控制单片机。
  • GPS 模块:GPS 定位模块输出的 NMEA 数据也是走串口,接进来解析就能做定位设备。
  • 多机通信:两块单片机用串口互联,一主一从传数据。
  • 命令行控制:做一个简单的串口命令解析器,在串口助手里敲 led onled off 就能控制设备,非常适合调试和演示。

下一步建议:把轮询升级成中断接收,CPU 就不用死等了,可以边收数据边干别的事。原理和「按键中断」那章相通。

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计