news 2026/9/7 9:12:47

STM32串口重定向printf与scanf实现详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32串口重定向printf与scanf实现详解

简介:针对STM32神舟IV号开发板的UART2串口通信示例,这是一份基于库函数版工程的完整可运行程序。工程解决嵌入式开发中常见的printf输出与scanf输入重定向问题,通过将标准输入输出映射到UART2,让开发者能像在PC上一样方便地调试串口数据,适合需要掌握STM32串口通信及标准外设库用法的学习者参考。压缩包共161个文件,以C源码(28个.c)、头文件(29个.h)、汇编文件(32个.s)为核心,附带uvproj/uvopt工程配置、hex/axf可烧录固件、doc说明文档以及批处理清理脚本,整体3.07MB,目录结构清晰,便于按需取用。已有864人学习下载,资源内含可直接运行的工程和固件,并保存了调试过程文件。对照文档可深入理解UART2初始化、中断接收、字符输出重定向、波特率设置等关键环节,实际操作时还能看到串口输出并输入数据,从而快速掌握库函数版UART驱动开发要点。 相信不少刚接触STM32的朋友都有过这样的经历:点灯、按键、外部中断都玩得挺顺,结果一到“想用串口打印点调试信息”这一步就卡住了。尤其是习惯了C语言里printfscanf的写法,到了单片机里却发现printf根本不出来数据,或者一用scanf程序就卡死。我在神舟IV号开发板上用库函数版本把这两个功能完整跑通过,实测稳定,今天把整个实现思路、代码细节和踩过的坑一次性说清楚。

这篇内容的核心就是把串口2(USART2)配置好,然后通过重定向fputcfgetc,让C标准库的printfscanf直接工作在串口上。适合正在学STM32标准库、想把调试效率提起来的朋友,也适合那些在串口收发上老是“差最后一步”的新手参考。

1. 项目背景与整体设计思路

1.1 为什么要在STM32上重定向printf和scanf

很多人在电脑上写C语言时,printf就是往屏幕打印,scanf就是从键盘读入,这是标准库干的事。但到了STM32上,标准库并不知道“屏幕”和“键盘”是什么东西,这时候就需要我们告诉它:把串口当成屏幕和键盘。这就是重定向的本质——让标准库的输入输出底层接口,指向我们指定的串口硬件

有人可能会问:调试的时候直接看变量不就行了,为什么要费劲搞printf?实话说,串口打印在单片机开发里的意义非常大。程序跑飞了、某个变量的值不对、某段逻辑有没有执行到,printf打一行出来比啥都直观。而在交互式调试中,scanf能让你在运行中给单片机发指令、改参数,不用重新编译烧录,这在调PID参数或者协议解析的时候特别省时间。

神舟IV号这块板子用的是STM32F103ZET6,属于增强型系列,资源比较丰富,有5个串口。我选择串口2来做这个功能,主要是它和USB转串口芯片的连接在板子上是现成的,直接用杜邦线或者板载跳线就能跟电脑通信,不需要额外接USB转TTL模块。

1.2 库函数版本与寄存器版本、HAL库版本的取舍

现在网上STM32教程鱼龙混杂,有的是寄存器写法,有的是标准库写法,还有的是HAL库写法。神舟IV号配套的例程是标准的固件库(标准外设库),也就是常说的库函数版本。这个版本虽然官方已经不再更新,但对于学习来说,它的寄存器映射清晰、代码可读性好,比直接操作寄存器省事,又比HAL库的封装更容易理解底层原理。

至于为什么不用HAL库,不是因为HAL不好,而是神舟IV号老开发板的例程都是基于标准库写的,如果强行换HAL库,整个工程结构都要推翻重来。而且标准库的串口配置代码非常直观,GPIO初始化、串口初始化、中断配置三步走,逻辑清楚,特别适合作为理解UART工作原理的入门素材。如果你以后转到HAL库,理解了标准库这套流程,看HAL的UART_Init也就是换个函数名的事。

1.3 USART、UART的区别与串口2的硬件基础

标题里写的是UART,但STM32的串口外设全称其实叫USART(Universal Synchronous/Asynchronous Receiver/Transmitter)。多出来的这个S是指同步模式,也就是可以外接时钟线做同步通信。实际上我们用的异步串口通信,并不需要时钟线,只用TX和RX两根线。这个点很多入门教程会含糊带过,但面试的时候经常被问到。

STM32F103ZET6的USART2引脚是PA2(TX)和PA3(RX),复用推挽输出。需要留意的是,虽然芯片本身支持5V容忍,但从稳定性和电平匹配角度,还是建议按3.3V逻辑来设计,毕竟STM32是3.3V供电的器件。神舟IV号板载的USB转串口芯片一般用的是CH340或类似方案,板上已经做了电平转换,直接接线就能用。

2. 串口2的初始化配置与时钟使能细节

2.1 库函数版本初始化代码的完整骨架

串口初始化的第一步是开时钟。STM32的外设使用前必须开启对应时钟,这一点和8位单片机差别很大。串口2挂载在APB1总线上,所以要使能RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE),同时PA2和PA3是GPIOA的引脚,需要使能RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)。注意,GPIO是挂在APB2上的,和串口的APB1不是一回事,漏掉任何一个时钟都会导致外设不工作。

然后是GPIO的配置。PA2配置为复用推挽输出(GPIO_Mode_AF_PP),PA3配置为浮空输入(GPIO_Mode_IN_FLOATING)。这里有个细节:TX引脚必须配置为复用推挽,因为要由串口外设控制引脚电平输出;RX引脚因为是接收外部信号,配置为浮空输入即可。有些人把TX误配成通用推挽输出(GPIO_Mode_Out_PP),结果数据发不出去,就是这个原因。

2.2 波特率、数据位、停止位的参数选择逻辑

USART初始化的参数结构体USART_InitTypeDef里,几个关键参数的含义要理解透彻:

  • USART_BaudRate:波特率,我用的是115200。这个速率和调试助手的默认设置一致,无需额外修改就能直接通信。如果要更远距离传输或者抗干扰,可以降到9600,但要保证收发双方一致。
  • USART_WordLength:数据位长度,选择USART_WordLength_8b,即8个数据位。这是最通用的配置,ASCII字符刚好用一个字节表示。
  • USART_StopBits:停止位,USART_StopBits_1。1位停止位是默认配置,绝大多数串口设备都支持。
  • USART_Parity:校验位,USART_Parity_No。无校验,因为串口调试助手默认也是无校验。
  • USART_Mode:收发模式,设置为USART_Mode_Rx | USART_Mode_Tx,同时开启接收和发送,因为我们需要printf输出,也需要scanf输入。
  • USART_HardwareFlowControl:硬件流控,USART_HardwareFlowControl_None。不使用RTS/CTS,三线制通信(TX、RX、GND)足够。

这里插一句关于波特率的理解。115200的意思是每秒传输115200个比特位,那么传一个字节(10个bit,包含起始位和停止位)大约需要86.8微秒。如果你的程序在中断里处理的事情耗时超过这个时间,就有可能导致下一次数据到来时没有及时响应,这就是波特率不能一味求高的原因。

配置完成后,调用USART_Cmd(USART2, ENABLE)使能串口,并且开启接收中断USART_ITConfig(USART2, USART_IT_RXNE, ENABLE)。注意,如果不开启接收中断,scanf就没法在运行时接收数据,这也是有人移植了代码却发现scanf不生效的原因之一。

3. printf与scanf重定向的实现及原理

3.1 fputc和fgetc的本质:重定向的关键入口

C标准库中的printf最终会调用fputc来输出单个字符,scanf最终会调用fgetc来读取单个字符。但标准库默认的fputcfgetc是面向PC屏幕和键盘的,到了嵌入式环境,我们必须自己实现这两个函数,把字符输出到串口、从串口读取字符。这就是“重定向”的全部秘密。

需要重写的函数如下:

int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET); USART_SendData(USART2, (uint8_t)ch); return ch; } int fgetc(FILE *f) { while (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) == RESET); return (int)USART_ReceiveData(USART2); }

fputc里为什么要while等待USART_FLAG_TXE?因为USART_SendData只是把数据写入发送数据寄存器,真正移位发送出去还需要时间。如果前一个字节还没发完就写入下一个字节,会造成数据覆盖,输出就会乱码或者丢字节。TXE标志位表示发送数据寄存器为空,等它置位再写入下一个数据,这样就能保证每个字节都被完整发送。

fgetc里等待的是USART_FLAG_RXNE,表示接收数据寄存器非空。也就是说,用户调用scanf的时候,如果串口没有数据进来,程序会一直停在这里等待,这就是为什么有时候使用scanf感觉程序“卡死了”——其实它是在等你往串口助手发送数据。

3.2 使用MicroLIB和KEIL工程配置的要点

在KEIL MDK环境下,默认的C标准库比较大,而且为了支持文件操作会引入半主机(Semihosting)模式。半主机模式是ARM调试器提供的一种机制,让开发板上的程序通过调试器在PC上执行输入输出操作。如果不关闭半主机模式,printf输出会走到调试器而不是串口,就会出现“程序能编译能下载,但串口就是没数据”的现象。

解决办法有两个:一是勾选KEIL魔术棒(Options for Target)里的Target标签页的“Use MicroLIB”选项,MicroLIB是ARM提供的高度精简版C库,默认不启用半主机模式,重定向fputcfgetc后就能正常工作;二是在代码中实现_sys_exit等函数来禁用半主机模式。我推荐用MicroLIB,操作简单,生成的代码也更小。

需要补充的是,勾选了MicroLIB之后,标准输入输出函数的行为会有所简化,比如不支持浮点数格式化时需要额外配置。但如果我们只是打印整数和字符串,MicroLIB足够用了。如果你要打印浮点数,可以在C/C++标签页的Define里加上MICROLIB宏,或者在fputc内部用vsprintf手动格式化,这是后话了。

另外有个KEIL的细节值得注意:勾选MicroLIB后有时编译会报__use_no_semihosting相关的错误,根本原因是工程里某些文件因为混合编译导致库选择不一致。遇到这种情况,检查一下所有源文件是否都参与了编译,然后把Include Path和宏定义清理干净,一般就能解决。

3.3 中文乱码与字符编码问题处理

串口输出中文出现乱码,是很多人遇到过的问题。原因很简单:KEIL编辑器默认的编码方式可能和串口助手的解码方式不一致。具体来说,KEIL中某些版本默认使用ANSI(GB2312)保存文件,而串口助手通常用UTF-8解析收到的字节流,编码不一致自然乱码。

解决方案有几种:一是修改串口调试助手的解码方式为GBK/GB2312;二是在KEIL的Edit→Configuration→Editor里,把Encoding改为UTF-8后重新输入中文字符串;三是最稳妥的办法——尽量不在单片机端输出中文,调试信息一律用英文加十六进制数值。我个人推荐英文,因为中文串口调试助手在编码处理上各软件水平不一,英文能从根本上避免这类问题。

如果你的产品确实需要在显示屏或上位机显示中文,那是另一套方案,涉及字库和编码转换,这里不展开,但要知道串口输出中文乱码不是单片机的问题,而是编码协商的问题。

4. 实操验证:从编译烧录到串口收发测试

4.1 完整测试代码与接线准备

在神舟IV号上测试时,我用的测试代码逻辑很简单:上电后先打印一条提示信息,然后进入循环,等待用户通过scanf输入一个整数,再把它原样打印出来。如果程序运行正常,你会看到串口助手先显示一行提示,你输入数字后它会回显。

#include "stm32f10x.h" #include <stdio.h> void USART2_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART2, &USART_InitStructure); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_Cmd(USART2, ENABLE); } int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET); USART_SendData(USART2, (uint8_t)ch); return ch; } int fgetc(FILE *f) { while (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) == RESET); return (int)USART_ReceiveData(USART2); } int main(void) { int num = 0; USART2_Config(); printf("USART2 printf/scanf test ready!\r\n"); while (1) { printf("Please input a number: "); scanf("%d", &num); printf("You entered: %d\r\n", num); } }

接线方面,神舟IV号如果板载USB转串口和USART1相连,而我们要用USART2,就需要确认板上的跳线或者使用杜邦线将PA2、PA3连接到USB转串口芯片的RXD、TXD端。注意是交叉连接:单片机的TX接USB转串口的RX,单片机的RX接USB转串口的TX,然后GND必须共地。不一致的连接是最常见的调试失败原因,没有之一。

4.2 实际测试流程与现象记录

下载程序后打开串口调试助手,选择对应的COM口,波特率设置为115200,数据位8,停止位1,无校验位,无硬件流控。打开串口后按下开发板复位键,调试助手会立即收到一行USART2 printf/scanf test ready!。这说明TX方向已经打通。

接着在发送框输入一个数字,比如123,点击发送。程序里的scanf在等待数据,收到后会继续执行,然后打印You entered: 123。如果回显正确,说明RX方向也正常,整个收发链路完全打通。

但是有个细节必须提醒:使用串口调试助手测试scanf时,很多软件默认发送的数据末尾带着\n(换行),而scanf("%d", &num)在遇到非数字字符时会停止读取,如果你发送的是123\n,程序会读取到数字123,然后换行符残留在接收缓冲区。下一次循环执行scanf时,\n会被当作格式不匹配的字符处理,导致读取失败。这个问题的解决办法是,在串口助手里发送时选择“发送新行”时注意看是否带了CRLF,或者改进scanf的格式,比如在%d后面加上%*c跳过非数字字符。这是真正写过的人才会发现的坑。

4.3 用USART中断实现对scanf的增强(可选)

如果你觉得标准scanf在串口调试助手里用起来别扭,可以考虑用USART接收中断配合环形缓冲区,自己实现一个简单的接收解析函数。这样就不需要依赖fgetc的阻塞等待,程序可以一边干别的事一边判断有没有数据到达。

实际情况中,我在产品代码里基本都是自己写接收解析逻辑,scanf大多数时候只在学习阶段用。原因是scanf的阻塞特性在主循环里会拖慢系统响应,而且它的格式解析有时不符合嵌入式场景的简单需求。但作为学习和功能验证,把scanf跑通能帮你深刻理解标准库重定向的原理,理解了原理,以后写自己的解析函数就是轻而易举的事。

5. 常见问题与排查技巧实录

5.1 编译报错:Error: L6915E / no stm32 target found

用KEIL开发STM32时,有几种报错让人很头疼,其中一个是error: no stm32 target found! if your product embeds debug authentication。这个报错通常是调试器连接不上芯片导致的,和代码本身无关。检查顺序是:确认ST-Link或J-Link是否正确插入电脑USB口;确认接线是否正确,特别是SWDIO、SWCLK、GND、3.3V四根线;确认KEIL的Debug选项里选择的调试器型号与实际使用的一致;最后按一下开发板的复位键再尝试下载,有时候芯片卡死在某种低功耗或异常状态,复位能救回来。

如果用的是ST-Link Utility或者CUBE Programmer这类独立烧录工具,报No STM32 Target还要检查是否设置了读保护(RDP)。神舟IV号这类老开发板如果之前烧过别人的程序,可能开启了读保护,要用ST-Link Utility先解除保护再烧录。初始化全片擦除有时也能解决这类问题。

5.2 printf输出乱码或完全没有输出

没有输出的排查优先级,我建议按这个顺序来:

  • 串口号对不对?设备管理器里看看COM口,特别是FT232R这类USB转串口芯片,驱动异常时在设备管理器会显示黄色感叹号。重新安装驱动即可。
  • 波特率是否一致?115200的两端都要是115200,不能只改一头。
  • 是否勾选MicroLIB?没勾的话printf默认走了半主机模式。
  • TX引脚是否接对?PA2是TX,要接到USB转串口的RX端。
  • 复位了吗?串口助手打开后,单片机的程序可能在你打开串口前就已经跑过printf了,错过了数据。先打开串口再按复位,保证能看到启动信息。

输出乱码的情况,除了前面说的中文编码问题,还有可能是波特率两边不一致导致的位错误。比如发送端实际是115200,接收端设置成9600,那每个字节的采样点就会错位,出来的就是一堆乱码符号。

5.3 scanf卡死、输入无效或读到错误值

scanf卡死,先确认接收中断有没有打开。虽然fgetc本身就是靠查询RXNE标志来判断有没有数据,但如果串口接收中断未使能,某些情况下RXNE标志的置位行为会异常,导致fgetc永远等不到有效数据。

输入无效或者读到的值不对,多半是发送的数据格式问题。scanf("%d", &num)要求接收缓冲区里的数据必须首先是数字字符,如果你用串口助手发送的是十六进制格式字符,比如发送0x7B,那它只会读到开头的0,然后因为后面的x不匹配而停止。scanf解析遇到不匹配字符时不会跳过它,而是把它留在缓冲区,造成后续读取接连失败。

解决办法是:利用scanf("%d%*c", &num)这种方式,%*c表示跳过一个非数字字符,可以吞掉数字后面的换行符。或者用while(getchar() != '\n');手动清空缓冲区,这个做法在PC的C语言教程里也常见,但很多人没意识到串口场景下同样适用。

5.4 芯片发热、引脚电平异常等硬件排查

如果软件配置看起来都对,但串口就是不通,硬件排查也必不可少。先用万用表确认PA2在程序运行后是否有3.3V左右的电平变化,如果PA2始终为低,说明串口根本没初始化成功,或者程序根本没跑到初始化的地方。再用示波器或逻辑分析仪看printf发送时的波形,如果波形完全平直无翻转,把问题定位在软件;如果有波形但是幅度太小,检查电平转换芯片的工作电压是否正常。

USB转TTL芯片不稳定也是常见坑,比如FT232R驱动在Win10下有时需要手动更新到最新版,CH340在某些精简系统上也需要手动安装驱动。驱动问题往往表现为“设备管理器能看到COM口但一打开就报占用或设备不存在”。遇到这种情况,把驱动卸载干净后重新安装,比在网上乱搜一圈有效得多。

6. 从调试功能到工程实践的延伸建议

printfscanf在串口2上跑通,只是嵌入式串口应用的第一步。我在实际项目中用这套思路做了不少扩展,有几个方向值得继续深入。

一是分段日志分级输出。在产品代码中,可以用宏控制不同调试级别的输出,比如DEBUG_INFODEBUG_WARNDEBUG_ERROR,通过一个全局变量或者条件编译决定哪些信息需要打印。这样调试阶段开全量日志,发布阶段只保留错误日志,不用大改代码。

二是使用printf格式化的高级技巧。除了%d%s这些基础格式,%x适合打印十六进制寄存器值,%p可以打印指针地址,%.2f能控制浮点输出精度。在理解串口重定向的基础上,你可以构造一个统一的日志函数log_printf(const char *file, int line, const char *fmt, ...),在打印信息前自动添加文件名、行号和时间戳,这在排查复杂逻辑问题时能节省大量时间。

三是串口协议帧设计。scanf适合人类手动输入调试,但真正和设备通信时必须用协议帧。常见的设计是帧头(如0xAA 0x55)、长度字节、数据域、校验和(累加和或CRC)。这个阶段,你已经不应该用scanf来解析数据了,而是在接收中断里把数据存入环形缓冲区,在主循环里按状态机解析完整的帧。理解了本文的重定向原理和串口底层机制,设计自己的协议栈会顺理成章。

四是直接操作寄存器理解更深层行为。库函数虽然封装好了,但如果你想知道某个寄存器位到底做了什么,可以用调试器打开外设寄存器视图,比对USART_Init执行前后CR1、CR2、BRR等寄存器的变化。尤其是BRR寄存器,它直接决定了波特率的分频系数,理解了APB1时钟频率(默认36MHz)和分频计算的关系,你就不会好奇为什么某些波特率有误差了。

从整个项目来看,串口调通的最大价值不在于“能打印了”,而在于你明白了标准库和硬件外设之间的桥梁是怎么搭起来的。以后无论是换到HAL库,还是换到别的芯片平台,核心思路都是相通的。哪怕去用ESP32、用树莓派Pico,本质上都是重新实现底层的字符输入输出函数,只是函数名和API形式不同而已。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 9:09:20

微电网IEC104主站客户端开发实战:从协议解析到嵌入式迁移

简介&#xff1a;电力行业广泛使用的远动通信协议IEC104&#xff0c;这套Java实现的主站客户端程序专为微电网管理系统设计&#xff0c;面向需要掌握协议底层交互与工程代码的开发者。程序基于TCP/IP实现应答式数据传输&#xff0c;覆盖遥信、遥测上行与遥控、遥调下行链路&…

作者头像 李华
网站建设 2026/9/7 9:08:33

AI教材写作工具大揭秘,高效完成专业教材编写,教师必备干货

高校教材编写过程中&#xff0c;既要保证内容的原创性&#xff0c;又必须遵守相关规范&#xff0c;这一直是个难题。很多人担心&#xff0c;直接用好教材里的优质内容&#xff0c;查重率会太高&#xff1b;但自己写的话&#xff0c;又害怕说得不够严谨或者出现错误。尤其是在AI…

作者头像 李华
网站建设 2026/9/7 9:05:52

Claude Code Agent Teams实战:多智能体协作的企业项目调研流水线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 9:05:45

Android RTSP拉流实战:基于libvlc的播放器集成与测试

简介&#xff1a;这是一份面向安卓开发者的RTSP实时流媒体播放示例工程&#xff0c;演示如何借助VLC核心库在应用中播放RTSP实时流视频&#xff0c;适合需要实现局域网监控、直播拉流等场景的开发者参考。压缩包共三十六个文件&#xff0c;大小约一百三十八千字节&#xff0c;以…

作者头像 李华
网站建设 2026/9/7 9:05:20

GD32F407+RT-Thread驱动SGM58031高精度ADC实战解析

简介&#xff1a;基于GD32F407与RT-Thread的SGM58031驱动代码包&#xff0c;面向嵌入式驱动开发及物联网应用开发者&#xff0c;解决在RT-Thread环境下快速接入SGM58031、实现16路AD采样的实际问题。包体仅3KB&#xff0c;共3个文件&#xff1a;SConscript构建脚本、drv_sgm580…

作者头像 李华
网站建设 2026/9/7 9:04:57

基于SpringBoot的行李寄存管理系统毕业设计项目源码

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华