GD32 USART — printf 重定向

GD32 GD32F30x_Firmware_Library V3.0.3 USART printf 重定向 示例教学

USART — printf 重定向

固件库: GD32F30x_Firmware_Library V3.0.3 芯片: GD32F307 (GD32307C-EVAL-V1.1 开发板) 源文件: GD32F30x_Firmware_Library_V3.0.3/Examples/USART/Printf/main.c


功能简介

这个例程要解决一个非常朴素但极其重要的问题:让 MCU “说话”

在电脑上写 C 程序,一句 printf("hello") 就能在黑窗口看到字。但在 MCU 上,printf 默认不知道该把字符送到哪里去 —— 芯片上既没有屏幕也没有控制台。

本例程做的事就是printf 的出口"接"到串口 USART0 上。接好之后,你在代码里写的每一句 printf,都会通过 PA9 引脚以 115200 波特率发出去,被电脑上的串口助手接收并显示。

这项技能的价值有多大? 打个比方:不会 printf 之前,调试程序就像闭着眼睛在黑屋里找东西,只能靠"灯亮不亮"猜;学会 printf 之后,屋里的灯打开了,你能直接看到每个变量的值、程序走到了哪一行、中断进了几次。

程序运行后只做一件事:打印一句 a usart transmit test example!,然后进入死循环。看起来平淡无奇,但重点全在"怎么接通"的那几行配置上


硬件准备

器材 说明
GD32307C-EVAL-V1.1 开发板 或任意 GD32F30x 系列板子
USB 转串口模块 CH340、CP2102、FT232 任选,几块钱一个
杜邦线 3 根 接 TX / RX / GND
电脑串口助手 SSCOM、XCOM、Putty、VOFA+ 均可

接线(新手最容易错的地方)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    GD32 开发板                    USB 转串口模块
  ┌──────────────┐               ┌──────────────┐
  │  PA9  (TX) ──┼───────────────┼──→ RX        │
  │  PA10 (RX) ──┼───────────────┼──← TX        │
  │  GND       ──┼───────────────┼─── GND       │
  └──────────────┘               └──────┬───────┘
                                        │ USB
                                     ┌──▼──┐
                                     │ 电脑 │
                                     └─────┘

必须记住的两条规则:

  1. TX 接 RX,RX 接 TX(交叉接)。想想两个人打电话,你的嘴要对着对方的耳朵。TX 接 TX 是两个人都在说话,谁也听不见。
  2. GND 必须接! 串口靠"电压高低"传数据,而"高"和"低"是相对于地的。不共地就没有共同的电压参考,通信必然失败或极不稳定。

偷懒技巧:GD32307C-EVAL 板子上有一个自带的 USB 转串口芯片,把跳线帽按丝印插好就能直接用板上的 USB 口通信,不用外接模块。查一下你板子的原理图看有没有这个便利。

串口助手设置

参数
波特率 115200
数据位 8
停止位 1
校验位 None
流控

完整代码

第一部分:main.c

 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
#include "gd32f30x.h"          /* 芯片总头文件 */
#include "gd32f307c_eval.h"    /* 开发板 BSP */
#include <stdio.h>             /* printf 的声明就在这里 */

/*!
    \brief      主函数
    \param[in]  无
    \param[out] 无
    \retval     无
*/
int main(void)
{
    /* ===== 第 1 步:打开 GPIOA 的时钟 =====
       USART0 的 TX/RX 默认在 PA9/PA10 上,所以要开 GPIOA */
    rcu_periph_clock_enable(RCU_GPIOA);

    /* ===== 第 2 步:打开 USART0 的时钟 =====
       GPIO 时钟和外设时钟是两套独立的开关,必须都打开 */
    rcu_periph_clock_enable(RCU_USART0);

    /* ===== 第 3 步:把 PA9 配成"复用推挽输出",交给 USART0 当 TX 用 =====
       注意是 AF_PP 而不是 OUT_PP!
       AF = Alternate Function,意思是"这根脚不听我的,听外设的" */
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);

    /* ===== 第 4 步:把 PA10 配成"浮空输入",交给 USART0 当 RX 用 =====
       接收脚是被动听电平,所以配输入模式 */
    gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10);

    /* ===== 第 5 步:配置 USART0 参数 ===== */
    usart_deinit(USART0);                                     /* 先复位到默认状态(8-N-1) */
    usart_baudrate_set(USART0, 115200U);                      /* 波特率 115200 */
    usart_receive_config(USART0, USART_RECEIVE_ENABLE);       /* 使能接收器 */
    usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);     /* 使能发送器 */
    usart_enable(USART0);                                     /* 总开关:使能 USART0 */

    /* ===== 第 6 步:直接用 printf 打印 =====
       \n 换行,\r 回车。串口助手里要两个都有才会正确换行到行首 */
    printf("\n\ra usart transmit test example!\n\r");

    /* 打印完就在这里死循环,什么也不做 */
    while(1);
}

第二部分:真正的"魔法"—— fputc 重写

上面的 main.c 里其实少了最关键的一块拼图。 printf 凭什么就知道要往 USART0 送数据?

秘密在 gd32f307c_eval.c 文件的末尾,官方 BSP 里已经帮你写好了这一段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* 重定向 C 库函数 printf 到串口,重定向后可使用 printf 函数 */
int fputc(int ch, FILE *f)
{
    /* 把这一个字符塞进 USART0 的发送数据寄存器 */
    usart_data_transmit(EVAL_COM0, (uint8_t)ch);

    /* 死等:一直查询 TBE 标志,直到发送缓冲区空出来
       说明这个字符已经被硬件接走了,可以返回处理下一个字符 */
    while(RESET == usart_flag_get(EVAL_COM0, USART_FLAG_TBE));

    return ch;
}

就这 3 行,是整个例程的灵魂所在。 下面详细解释它的原理。


代码分段讲解

第 1 段:printf 重定向的原理(本篇最重要的部分)

C 标准库的 printf 内部并不是一个整体,它是分层的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
你的代码:  printf("value = %d\r\n", 100);
       ┌──────────────────────────────┐
       │  printf 格式化引擎(库里已实现) │
       │  负责解析 %d %s %f,          │
       │  把参数转成字符串               │
       │  → "value = 100\r\n"         │
       └──────────────┬───────────────┘
                      │ 逐个字符调用
       ┌──────────────────────────────┐
       │  fputc(int ch, FILE *f)      │  ← 这是"底层出口"
       │  库里只留了个空壳(弱定义),      │     我们要自己实现!
       │  需要用户告诉它"送到哪儿去"       │
       └──────────────┬───────────────┘
                      │ 我们的实现
       ┌──────────────────────────────┐
       │  usart_data_transmit()       │
       │  → PA9 引脚 → 串口线 → 电脑    │
       └──────────────────────────────┘

核心理解printf 负责"把复杂的格式化字符串变成一串普通字符",fputc 负责"把一个字符送出去"。前者标准库已经写好了(那部分逻辑跟硬件无关),后者必须由我们提供(因为只有我们知道板子上用哪个串口)。

这个设计叫重定向(Retargeting),是 C 标准库为嵌入式预留的钩子。

第 2 段:为什么 TX 用 AF_PP,RX 用 IN_FLOATING

1
2
gpio_init(GPIOA, GPIO_MODE_AF_PP,       GPIO_OSPEED_50MHZ, GPIO_PIN_9);   /* TX */
gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10);  /* RX */

这是新手第二大坑(第一大是忘开时钟)。

PA9 (TX) 为什么必须是 GPIO_MODE_AF_PP

如果配成普通的 GPIO_MODE_OUT_PP,那这根引脚的电平就由你的程序通过 gpio_bit_set() 控制。但我们希望它由 USART0 外设硬件自动按波特率翻转 —— 每秒 11 万多次,程序哪来得及一位一位地写?

AF_PP 就是告诉 GPIO 控制器:"这根脚的控制权我交出去了,谁申请谁用"。USART0 使能后自动接管 PA9,按 115200 的时序精确输出每一位。

记住这条:任何要交给外设自动驱动的输出引脚,都必须配 AF 模式。 做 PWM 输出、SPI 时钟、I2C 都是同理。

PA10 (RX) 为什么是输入模式?

接收是"听",引脚被动感知外部电平变化,所以是输入。IN_FLOATING(浮空)适用于对方(USB 转串口模块)已经在主动驱动这根线的场合。

GPIO_MODE_IPU(上拉输入)也可以,而且更保险 —— 万一线断了或对方掉电,上拉能保证引脚是稳定的高电平(串口空闲态是高),不会因为浮空乱跳而产生假的起始位。实际项目推荐用 GPIO_MODE_IPU

第 3 段:usart_deinit() 为什么要先调用

1
2
usart_deinit(USART0);
usart_baudrate_set(USART0, 115200U);

usart_deinit() 是"把 USART0 的所有寄存器恢复到芯片复位后的初始值"。

为什么要这一步? 两个原因:

  1. 保证起点干净。如果程序之前用过 USART0(比如 Bootloader 用过、或者你反复初始化),寄存器里可能残留旧配置。deinit 相当于"格式化"。
  2. 拿到已知的默认值。复位后 USART 的默认配置正好是 8 位数据、无校验、1 位停止位,也就是最常用的 8-N-1。所以官方例程里连 usart_word_length_set()usart_parity_config()usart_stop_bit_set() 都不用写了 —— deinit 之后它们已经是我们想要的值。

如果你需要非标准配置(比如 9 位数据 + 偶校验),就要在 deinit 之后显式设置:

1
2
3
4
5
usart_deinit(USART0);
usart_baudrate_set(USART0, 115200U);
usart_word_length_set(USART0, USART_WL_9BIT);      /* 9 位数据 */
usart_parity_config(USART0, USART_PM_EVEN);        /* 偶校验 */
usart_stop_bit_set(USART0, USART_STB_1BIT);        /* 1 位停止 */

第 4 段:发送/接收/总使能,三个开关的关系

1
2
3
usart_receive_config(USART0,  USART_RECEIVE_ENABLE);    /* 接收器开关 */
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);   /* 发送器开关 */
usart_enable(USART0);                                   /* 外设总开关 */

可以理解成一个带三个开关的收音机:

  • usart_transmit_config → “喇叭"开关(REN 位)
  • usart_receive_config → “麦克风"开关(TEN 位)
  • usart_enable → 整机"电源"开关(UEN 位)

电源不开,喇叭和麦克风开了也没用。所以 usart_enable() 必须调用,而且习惯上放在最后。

如果你的项目只需要打印(不接收),可以只开发送省点功耗:

1
2
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
usart_enable(USART0);

第 5 段:\n\r 到底该写哪个

1
printf("\n\ra usart transmit test example!\n\r");

这是个历史遗留的坑:

转义符 名字 ASCII 作用(源自打字机)
\n LF (Line Feed) 0x0A 换行:纸往上滚一行,但打字头横向位置不变
\r CR (Carriage Return) 0x0D 回车:打字头回到行首,但不换行

所以:

  • 只发 \n → 有些串口助手里会看到"阶梯状"输出(每行开头越来越靠右)
  • 只发 \r → 后面的字会把前面的字覆盖掉,永远显示在第一行
  • \r\n 两个 → 才是标准的"回到行首 + 换到下一行”

实际写法建议统一用 "\r\n"(先回车再换行,符合 Windows/串口通用惯例)。本例程写成 "\n\r" 顺序反了,虽然大部分串口助手都能正确处理,但不是标准写法。

第 6 段:fputc 里 while(TBE) 的位置很讲究

1
2
3
4
5
6
int fputc(int ch, FILE *f)
{
    usart_data_transmit(EVAL_COM0, (uint8_t)ch);                  /* 先塞数据 */
    while(RESET == usart_flag_get(EVAL_COM0, USART_FLAG_TBE));    /* 后等标志 */
    return ch;
}

注意顺序是"先发后等”,不是"先等后发"。 为什么这样也能工作?

USART 内部有两级寄存器:

1
2
3
    你写入 ──→ [发送数据寄存器 TDATA] ──→ [移位寄存器] ──→ TX 引脚
                        ↑                      ↑
                   TBE 标志监控这里      TC 标志监控这里
  • 复位后 TDATA 是空的,TBE 本来就是 1,所以第一次直接写入没问题。
  • 写入后硬件立即把数据挪到移位寄存器,TBE 重新置 1。
  • 我们 while 等的就是这个"挪走了"的信号,等到之后就可以放心返回,让 printf 送下一个字符。

这种写法的效率其实相当不错:CPU 等待的只是"从 TDATA 挪到移位寄存器"这一小段时间,而不是等整个字节 8.68μs × 10 位全部发完。移位寄存器在慢慢发的同时,CPU 已经可以准备下一个字符了 —— 相当于一个 1 字节深的流水线。

注意 USART_FLAG_TBEUSART_FLAG_TC 的区别。如果你做 RS485 通信,发完要立刻把收发芯片切回接收方向,这时必须等 TC(Transmission Complete,最后一位真的离开引脚了),用 TBE 会把最后一个字节截断。这是 RS485 项目的经典 bug。


实验现象

  1. 按上面的接线图接好 3 根线。
  2. 编译下载程序。
  3. 电脑上打开串口助手,选对 COM 口,波特率设 115200,点击"打开串口"。
  4. 按开发板上的复位键

串口助手窗口会立刻显示:

1
a usart transmit test example!

注意:这句话只在复位/上电时打印一次。如果你打开串口助手之前板子已经跑起来了,是看不到的 —— 按一下复位键就有了。

想看到持续输出?改成这样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include "systick.h"

int main(void)
{
    uint32_t counter = 0;

    systick_config();                   /* 别忘了配 SysTick,delay_1ms 才能用 */
    /* ...... 上面那些 USART 配置照抄 ...... */

    while(1){
        printf("usart printf counter: %d\r\n", counter++);
        delay_1ms(1000);                /* 每秒打印一次 */
    }
}

串口助手会每秒多一行:

1
2
3
4
usart printf counter: 0
usart printf counter: 1
usart printf counter: 2
...

看到数字持续增长,说明程序在正常跑 —— 这也是一种最简单的"程序心跳"监控手段。


常见问题

Q1:串口助手一个字都收不到

按顺序排查:

  1. COM 口选错了。打开设备管理器 → 端口(COM 和 LPT),确认 CH340 是 COM 几。多个串口设备时很容易选错。
  2. TX/RX 没交叉。把两根线互换试试,这是最常见的问题。
  3. 忘了共地。GND 必须接,不接可能偶尔收到几个乱码字符。
  4. 打开串口助手后没复位。程序只打印一次,错过就没了。
  5. 驱动没装好。CH340 在某些 Windows 上需要手动装驱动,设备管理器里有黄色感叹号就是这个问题。
  6. fputc 没有被编译进去。检查工程里有没有把 gd32f307c_eval.c 加进编译列表。如果没有,printf 会链接到库里的空实现,什么都不输出。

Q2:收到的全是乱码(锟斤拷ÿÿ 这类)

几乎 100% 是波特率不匹配

  1. 串口助手波特率改成 115200,逐一试试 9600 / 38400 / 115200,如果某个波特率下能看到部分正常字符,说明主频有偏差。
  2. 实际主频和 SystemCoreClock 不一致。波特率是由外设时钟分频得到的,如果外部晶振起振失败,芯片自动跑内部 8MHz RC,实际波特率就全错了。加一句验证:
1
printf("SystemCoreClock = %d\r\n", SystemCoreClock);

如果这句本身就是乱码,那说明确实是时钟问题。改用调试器看 SystemCoreClock 变量的值,GD32F307 应该是 120000000。

  1. 晶振频率宏配错了。检查 gd32f30x.h 里的 HXTAL_VALUE 是否和板子上实际的晶振一致(EVAL 板常见 8MHz 或 25MHz)。晶振是 8MHz 但宏写 25MHz,波特率会差 3 倍多。

Q3:程序卡死在 printf 那一行

用调试器单步,如果卡在 fputcwhile(TBE) 循环里出不来,说明 USART 硬件没真正工作:

  1. 忘了 usart_enable(USART0)。总开关没开,TBE 永远不会置位。
  2. 忘了 rcu_periph_clock_enable(RCU_USART0)。外设时钟没开,寄存器写不进去。
  3. 用了 EVAL_COM0 但它不是 USART0。检查 gd32f307c_eval.hEVAL_COM0 的定义,跟你 main 里配置的串口要一致。这是"配了 USART0 但 fputc 往 USART1 写"的经典错配。

Q4:printf("%f", 3.14) 打不出小数

浮点打印需要额外的库支持,两种解决办法:

  1. Keil MDK:勾选 Options for Target → Target → Use MicroLIB。MicroLIB 是精简版 C 库,体积小且默认支持基本浮点格式化。
  2. 不用浮点 printf(推荐,省 Flash 也省时间):
1
2
3
4
float temp = 25.63f;
/* 手动拆成整数部分和小数部分 */
printf("temp = %d.%02d\r\n", (int)temp, (int)(temp * 100) % 100);
/* 输出:temp = 25.63 */

嵌入式里浮点 printf 会额外链接几 KB 代码,而且执行慢,能避免就避免。

Q5:为什么有的教程写 fputc,有的写 _write__io_putchar

这取决于你用的工具链的 C 库实现:

工具链 / C 库 需要重写的函数
Keil MDK + ARMCC / MicroLIB int fputc(int ch, FILE *f)
GCC + newlib (标准) int _write(int fd, char *ptr, int len)
GCC + newlib-nano int __io_putchar(int ch)(配合 #define PUTCHAR_PROTOTYPE
IAR size_t __write(int handle, const unsigned char *buf, size_t size)

GCC 下的通用写法:

1
2
3
4
5
6
7
8
9
int _write(int fd, char *ptr, int len)
{
    int i;
    for(i = 0; i < len; i++){
        usart_data_transmit(USART0, (uint8_t)ptr[i]);
        while(RESET == usart_flag_get(USART0, USART_FLAG_TBE));
    }
    return len;
}

Q6:printf 太慢,影响程序实时性怎么办

printf阻塞式的,一句 50 个字符的打印在 115200 下要占用 CPU 约 4.3ms。在中断服务函数或高实时性循环里频繁 printf 会严重拖慢系统。

三个优化方向:

  1. 提高波特率。改成 921600 能快 8 倍,现代 USB 转串口芯片都支持。代码只改一个数字。
  2. 改用 DMA 发送。把要打印的内容先写入缓冲区,然后让 DMA 后台慢慢发,CPU 立刻返回。参考本模块的 DMA_transmitter&receiver 示例。
  3. 正式版本关掉打印。用宏开关一键屏蔽所有调试输出:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#define DEBUG_ENABLE   1

#if DEBUG_ENABLE
    #define DBG_LOG(fmt, ...)   printf("[%s:%d] " fmt "\r\n", \
                                       __func__, __LINE__, ##__VA_ARGS__)
#else
    #define DBG_LOG(fmt, ...)   ((void)0)     /* 发布版本编译后完全消失,零开销 */
#endif

/* 使用 */
DBG_LOG("adc value = %d", adc_val);
/* 输出:[main:42] adc value = 2048 */

这个 DBG_LOG 宏自动带上函数名和行号,是实际项目里非常好用的调试工具,强烈建议抄进你自己的工程模板。


小结

知识点 要记住的核心
printf 重定向原理 库负责格式化,你负责实现 fputc 把单个字符送出去
TX 引脚模式 必须 GPIO_MODE_AF_PP,交给外设驱动
RX 引脚模式 GPIO_MODE_IN_FLOATING 或更保险的 GPIO_MODE_IPU
两套时钟 GPIO 时钟 + USART 时钟,都要开
deinit 的意义 复位到干净状态,且默认就是 8-N-1
三个使能开关 发送器、接收器、外设总开关,缺一不可
\r\n 回车 + 换行都要发,顺序推荐 \r\n
TBE vs TC TBE 是缓冲区空,TC 是真正发完;RS485 切方向必须用 TC
乱码 = 波特率 看到乱码第一反应查波特率和主频配置

USART0 配置五步曲(背下来,以后写串口不用查手册):

1
2
3
4
5
6
7
8
9
rcu_periph_clock_enable(RCU_GPIOA);                    /* 1. GPIO 时钟 */
rcu_periph_clock_enable(RCU_USART0);                   /* 2. USART 时钟 */
gpio_init(GPIOA, GPIO_MODE_AF_PP, ..., GPIO_PIN_9);    /* 3. TX 复用推挽 */
gpio_init(GPIOA, GPIO_MODE_IPU,   ..., GPIO_PIN_10);   /* 4. RX 上拉输入 */
usart_deinit(USART0);                                  /* 5. 参数 + 使能 */
usart_baudrate_set(USART0, 115200U);
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
usart_enable(USART0);

拓展应用

printf 通了之后,你的能力会发生质变。下面这些项目都建立在串口之上:

入门级(现在就能做)

  • 串口监控台:把 ADC 采样值、按键状态、定时器计数值全部实时打印出来,做一个"MCU 状态仪表盘"。这是排查任何 bug 的第一手段。
  • 参数打印式调试法:在每个函数入口打印 printf("enter %s\r\n", __func__),程序跑飞时看最后打印的是哪个函数,立刻定位问题范围。
  • 开机自检报告:上电依次检测 Flash、外部 RAM、EEPROM、传感器,把每一项的 OK/FAIL 打印成一份漂亮的启动日志。产品级设备都有这个。

进阶级(配合中断接收)

  • 串口命令行(Shell):接收电脑发来的命令字符串,解析后执行。比如输入 led on 就点灯,adc read 就返回采样值,help 列出所有命令。这是嵌入式项目里最实用的一个功能,让你不用重新编译就能测试各个模块。
  • 上位机通信协议:设计自己的数据帧(如 帧头 0xAA + 长度 + 命令 + 数据 + CRC16 + 帧尾),配合 C# / Python / Qt 写的上位机软件做双向通信。这是绝大多数电子产品的标准架构。
  • 串口 Bootloader(IAP):通过串口接收新的固件 bin 文件,写入 Flash 后跳转执行,实现"不用拆机、不用下载器就能升级程序"。结合本站的 FMC 擦写示例就能做出来。
  • AT 指令驱动模块:用串口驱动 ESP8266 WiFi 模块、SIM800 4G 模块、HC-05 蓝牙模块。发 AT+CWJAP="ssid","pwd" 就能联网,是 IoT 项目的必经之路。

数据分析级

  • VOFA+ / SerialPlot 实时波形显示:用固定格式打印数据(如 printf("%f,%f\r\n", a, b)),上位机自动画成实时曲线。调 PID 参数时看着曲线整理超调和振荡,比盲调效率高 10 倍。
  • 数据记录仪:把传感器数据按 CSV 格式打印(printf("%d,%d,%d\r\n", t, temp, humi)),串口助手保存成文件后直接用 Excel 打开画图分析。
  • GPS 数据解析:GPS 模块通过串口不断吐出 NMEA 格式语句($GPGGA,...),解析后就能得到经纬度、海拔、卫星数,做定位记录仪、车载追踪器。

工业实战级

  • Modbus RTU 从站:在串口上实现 Modbus 协议,你的板子立刻能被任何工业组态软件(如组态王、WinCC)识别和读写。这是工控行业的通用语言。
  • RS485 多机总线:加一颗 MAX485 芯片把 TTL 电平转成差分信号,通信距离从 1 米延长到 1200 米,还能挂 32 个节点组成总线网络。注意发送完必须等 TC 标志再切换方向。
  • 产品出厂测试治具:通过串口下发测试指令,自动完成各项功能检测并输出报告,一台治具测几千台产品。

下一步该学什么Transmitter&receiver_interrupt(学会中断接收,让 MCU 能"听")→ IDLE_receive_interrupt(处理不定长数据帧,这是实际项目最常用的接收方案)→ DMA_transmitter&receiver(高效大数据传输)。

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