基于STM32的普通GPIO模拟串口UART(代码开源)
前言:本文为手把手教学的基于 STM32 的普通 GPIO 模拟串口 UART 教程,使用 STM32F103ZET6 作为核心 MCU 进行操作。串口 UART 这项外设可以说是嵌入式工程师日常项目研发过程中百分百可能使用到的外设,但很多情况下 MCU 的硬件 UART 受限亦或是 GPIO 引脚被占用导致无法正常使用硬件 URAT 进行打印调试代码 。软件 UART 在这时就起到了至关重要的作用,作者将在本篇博客中提供本人日常使用且方便移植的软件 UART 代码。希望这篇博文能给读者朋友的工程项目给予些许帮助,Respect(代码开源)!
硬件与软件:STM32F103ZET6、CH340、WCH COMTransmit
项目结果图:


一、串口 UART 概述
1.1 什么是串口
UART(Universal Asynchronous Receiver Transmitter)是最常用的设备对设备(串行)通信协议之一。它是 Arduino 板用来与计算机通信的协议。它允许异步串行通信,其中数据格式和传输速度是可配置的。它是最早的串行协议之一,尽管它在许多地方被 SPI 和 I2C 所取代,但它仍然广泛用于低速和低吞吐量应用,因为它非常简单,低成本且易于实现。
UART 很棒的地方:只用两根线——TX(发送)和 RX(接收) 就能搞定通信,比 SPI 和 I2C 少了好几根线,布线也省心。

1.2 串口如何传输数据
串口 UART 的通信规则说白就是:两端 UART 对着干:一个负责发,一个负责收。

例如:MCU 开发板要发送数据到一个蓝牙模块:内部数据是并行的,UART 发射端会加上一些特殊标志(起始位、校验位、停止位),然后一个个按位发出去;接收端 UART 再把这些位收集起来,去掉包边,还原成原始数据送出去。
由于串口 UART 没有时钟同步信号,靠的是两边提前约定好的波特率来保持节奏一致。波特率就是每秒传多少个 bit,比如 9600bps、115200bps 都是常见值。UART 协议中,一个数据包长这样:(1)、1 位起始位(Start Bit)(2)、5~9 位数据(Data Bit)(3)、可选的 1 位奇偶校验位(Parity Bit)(4)、1~2 位停止位(Stop Bit)

1. Start Bit 起始位
在空闲状态下,TX 引脚是高电平(逻辑 1),当有数据要发时,先拉低一位(逻辑 0),告诉接收端“准备好了”,这就是起始位。
2. Data Bit 数据位
真正的数据部分,一般是 8 位,也有的 UART 支持 5~9 位可选,最常见的是 8 位无校验。
3. Parity Bit 校验位(可选)
为了检测传输过程中的错误,UART 可以加个奇偶校验位(Parity Bit)。
比如“偶校验”就要求总共有偶数个 1,这样接收方可以根据收到的位数判断有没有出错。
注意:这不是真正的纠错,只能报错,不能“纠错”,跟 CRC、FEC 那种比还是简单得多。
4. Stop Bit 停止位
表示数据包结束。通常是 1 位或 2 位逻辑高电平,给接收方缓口气,也防止包和包之间连在一起。
串口 UART 发送数据的流程图:

二、GPIO 模拟 UART 简述
2.1 软件 UART 核心原理
GPIO 模拟串口(软件 UART),核心:通过软件精准控制 GPIO 引脚的高低电平 + 严格的延时,模拟标准 UART 异步串行通信的时序规则,完全不依赖 STM32 的硬件 UART 外设,所有时序逻辑由 CPU 执行代码完成。
✅ 标准 UART 异步通信时序(重中之重,模拟的核心依据)
串口通信为异步通信(无时钟线),数据按「位」为单位串行传输,空闲状态总线为高电平,完整的 1 帧数据格式(最常用,99% 场景):起始位(1位,低电平) → 数据位(8位,低位先发) → 停止位(1位,高电平)
补充说明:无校验位,波特率可选 9600/4800/19200,是模拟串口的最佳选型,带校验位会增加代码复杂度,不推荐。
✅ 波特率与位周期的换算关系
串口波特率 = 每秒传输的位数,比如最常用的 9600bps → 每秒传输 9600 位数据位周期 = 1 / 波特率 → 9600 波特率的位周期 ≈ 104.167μs这是软件延时的核心数值:模拟串口的每一位电平,都必须持续 1 个位周期的时间,延时精准度直接决定通信是否成功
软件 UART 的优势:摆脱 STM32 硬件 UART 外设的引脚限制,任意通用 GPIO 口都可以模拟串口的 TX 发送、RX 接收;无需配置硬件 UART 的寄存器、中断、DMA 等,无外设占用;完美解决「硬件串口数量不足」的场景(比如 STM32F103 只有 3 个硬件 UART,但需要连接多个串口设备)。
2.2 软件 UART 关键前提条件
1、硬件 GPIO 选型:
STM32任意通用 GPIO均可,无特殊要求,推荐配置:
发送引脚(TX):配置为 推挽输出模式 (GPIO_Mode_Out_PP)接收引脚(RX):配置为 上拉输入模式 (GPIO_Mode_IPU) → 串口空闲为高电平,上拉可防止悬空电平抖动,避免误触发

2、核心依赖:精准的微秒级延时函数
软件串口的灵魂是精准延时,模拟串口的所有时序错误、通信乱码,99% 都是因为微秒 (μs) 级延时不准导致的。必须在工程中实现一个 void delay_us(uint32_t us) 函数,要求:误差<5%,支持 1~200μs 的精准延时。
3、波特率选型建议
推荐使用 ≤9600bps 的波特率,最高不建议超过 19200bps。原因:波特率越高,位周期越短(19200bps 的位周期≈52μs),对延时精度、CPU 主频的要求越高,STM32 主频 72MHz 下,9600bps 是兼顾速度和稳定性的最优解。
补充说明:上述只是根据理论估计,但作者实际工作中软件 UART 都460800开搞,也没有多大的影响。故读者朋友们可以根据自己实际的情况去模拟软件 UART 进行使用!
2.3 软件 UART 的收发特性
GPIO 模拟串口的发送 (TX) 和接收 (RX) 的稳定性完全不同,这是硬件特性决定的,必须知晓:
✅ 模拟 TX(GPIO 输出,发送数据)→ 稳定性:★★★★★(非常稳定)
串口发送是主动行为:由 MCU 主动控制 GPIO 引脚拉低 / 拉高,配合精准延时即可完成时序输出,只要延时函数精准,发送数据的正确率是 100%,和硬件串口几乎无区别。
✅ 模拟 RX(GPIO 输入,接收数据)→ 稳定性:★★★★(良好,满足 99% 场景)
串口接收是被动行为:MCU 需要实时检测 GPIO 引脚的电平变化,识别「起始位的下降沿(高→低)」,再逐位采样数据。
优点:通过「中断 + 定时器采样」的方式可以实现精准接收;缺点:相比硬件 UART,CPU 占用率稍高(需要轮询 / 中断检测电平);核心技巧:中位采样法 → 检测到起始位后,延时1.5 个位周期再开始采样每一位数据,避开电平跳变的抖动区,极大提升接收正确率。
三、STM32CubeMX 配置
1、RCC配置外部高速晶振(精度更高)——HSE;

2、SYS配置:Debug设置成Serial Wire(否则可能导致芯片自锁);

3、GPIO配置:PB7(TX):配置为推挽输出模式 ;PB8(RX):配置为上拉输入模式;

4、时钟树配置

5、工程配置

四、代码实现
4.1 GPIO 模拟 UART
作者编写的软件 UART 代码都在 debug_software_uart.c 文件中,根据作者上方关于软件 UART 的描述其核心的代码为精准的 us 定时器,故作者编写了 HAL_Delay_Us 函数。根据实际需求,作者编写了 void SoftUART_SendByte(uint8_t data)、void SoftUART_SendString(uint8_t *str)、uint8_t SoftUART_ReceiveByte(void)、void SoftUART_SendDigit(uint8_t num)、void SoftUART_SendNum(int32_t num, uint8_t base) 和 void SoftUART_printf(const char *fmt, ...) 函数。上述编写的功能性 API 函数基本可以满足读者朋友需要的软件串口 UART 功能。
由于使用 GPIO 模拟出软件 UART 的功能,肯定需要实现 us 级别的 GPIO 电平延迟。这里作者编写了一个兼容性很好的 HAL_Delay_Us 函数,代码如下:

SoftUART_SendByte(uint8_t data) 函数是编写的软件 UART 发送字节函数,此函数使用 HAL_Delay_Us 函数与GPIO电平高低来模拟出 UART 协议中规定的数字信号。SoftUART_SendByte(uint8_t data) 函数的代码逻辑如下:本质就是简单的模拟 UART 通信格式!

uint8_t SoftUART_ReceiveByte 函数为软件串口 - 接收函数(核心,中位采样法,高正确率):采用查询式接收 + 中位采样法,是模拟接收的最优实现方式,兼顾代码简洁和接收稳定性,无丢包、无乱码,适合绝大多数场景(大部分时候软件 UART 的应用还是以发送类函数为主)
核心接收逻辑:
1、等待串口空闲:RX 引脚为高电平;
2、检测起始位:RX 引脚出现「高→低」的下降沿,确认起始位;
3、中位采样:延时 1.5 * BIT_DELAY_US 后开始采样,避开电平抖动区,采样点为每一位的正中间,最稳定;
4、逐位采样 8 位数据,低位先存,高位后存;
5、等待停止位:采样完成后,RX 引脚恢复高电平,确认接收完成。

debug_software_uart.c:
/********************************** (C) COPYRIGHT ******************************* * File Name : debug_software_uart.c * Author : 混分巨兽龙某某 * Version : V1.0.0 * Date : 2025/10/15 * Description : USE GPIO to Simulate The Software UART ********************************************************************************/ #include "debug_software_uart.h" #include "main.h" #include <stdarg.h> /******************************************************************************* * Function Name : HAL_Delay_Us * Description : The microsecond delay function created by the system clock * Input : Microsecond * Return : None *******************************************************************************/ void HAL_Delay_Us(uint32_t us) { uint32_t ticks; uint32_t told, tnow, tcnt = 0; /* The number of clocks required for calculation = the number of microseconds of delay * the number of clocks per microsecond */ ticks = us * (SYSCLK / 1000000); told = SysTick->VAL; while (1) { tnow = SysTick->VAL; if (tnow != told) { if (tnow < told) tcnt += told - tnow; else tcnt += SysTick->LOAD - tnow + told; told = tnow; if (tcnt >= ticks) break; } } } /******************************************************************************* * Function Name : SoftUART_Init * Description : Software UART Init * Input : None * Return : None *******************************************************************************/ void SoftUART_Init(void) { // 第一部分已经使用STM32CubeMX配置GPIO属性 // 串口空闲状态:TX引脚置高 TX_HIGH(); } /******************************************************************************* * Function Name : SoftUART_SendByte * Description : Software serial port transmission of bytes * Input : Data * Return : None *******************************************************************************/ void SoftUART_SendByte(uint8_t data) { uint8_t i = 0; // 1. 发送起始位:低电平,持续1个位周期 TX_LOW(); HAL_Delay_Us(BIT_DELAY_US); // 2. 发送8位数据位:低位先发,依次发送每一位 for(i = 0; i < 8; i++) { if(data & 0x01) // 当前位是1,TX置高 { TX_HIGH(); } else // 当前位是0,TX置低 { TX_LOW(); } HAL_Delay_Us(BIT_DELAY_US); // 每一位持续1个位周期 data >>= 1; // 右移一位,准备发送下一位 } // 3. 发送停止位:高电平,持续1个位周期 TX_HIGH(); HAL_Delay_Us(BIT_DELAY_US); // 4. 回到空闲状态(高电平),无需额外操作 } /******************************************************************************* * Function Name : SoftUART_SendString * Description : Software serial port transmission of byte string * Input : Str * Return : None *******************************************************************************/ void SoftUART_SendString(uint8_t *str) { while(*str != '\0') { SoftUART_SendByte(*str); str++; } } /******************************************************************************* * Function Name : SoftUART_ReceiveByte * Description : Software serial port receiving bytes * Input : None * Return : None *******************************************************************************/ uint8_t SoftUART_ReceiveByte(void) { uint8_t i = 0; uint8_t recv_data = 0; // 1. 等待空闲:RX为高电平,跳过无效电平 while(RX_READ() == 0); // 2. 检测起始位下降沿:RX从高→低,确认起始位开始 if(RX_READ() == 1) { while(RX_READ() == 1); // 等待起始位的下降沿 // 3. 中位采样核心:延时1.5个位周期,跳到起始位的中间位置,避开抖动 HAL_Delay_Us(BIT_DELAY_US * 1.5); // 4. 采样8位数据位,低位先发,依次存入recv_data for(i = 0; i < 8; i++) { recv_data >>= 1; // 右移一位,准备存下一位 if(RX_READ() == 1) // 采样当前位为1 { recv_data |= 0x80; // 最高位置1 } HAL_Delay_Us(BIT_DELAY_US); // 每采样一位,延时1个位周期 } // 5. 等待停止位:RX恢复高电平,完成1帧数据接收 while(RX_READ() == 0); return recv_data; } return 0xFF; // 无数据接收/超时,返回无效值 } /******************************************************************************* * Function Name : SoftUART_SendDigit * Description : Send a single decimal digit from 0 to 9 (core, convert the digit into ASCII code for transmission) * Input : num:The value to be sent * Return : None *******************************************************************************/ void SoftUART_SendDigit(uint8_t num) { // 数字0-9的ASCII码是 0x30~0x39,直接相加即可转换 SoftUART_SendByte(num + 0x30); } /******************************************************************************* * Function Name : SoftUART_SendNum * Description : Send any integer value, supporting decimal/hexadecimal, and supporting negative numbers * Input : num:The value to be sent; base:The base system * Return : None *******************************************************************************/ void SoftUART_SendNum(int32_t num, uint8_t base) { uint8_t buf[32]; // 存储数字的每一位 uint8_t i = 0; uint32_t temp = num; // 处理【负数】,仅十进制有效 if(num < 0 && base == 10) { SoftUART_SendByte('-'); // 发送负号 temp = -num; // 取绝对值 } // 把数字按进制拆分成单个字符,存入缓冲区(逆序) do { uint8_t digit = temp % base; if(digit < 10) buf[i++] = digit; // 0-9 直接存数字 else buf[i++] = digit -10 + 'a'; // 10-15 转小写a-f temp /= base; }while(temp != 0); // 逆序发送缓冲区的数字,得到正确顺序 if(i == 0) SoftUART_SendDigit(0); // 数值为0时,直接发送0 else { while(i--) { if(buf[i] < 10) SoftUART_SendDigit(buf[i]); else SoftUART_SendByte(buf[i]); } } } /******************************************************************************* * Function Name : SoftUART_printf * Description : Simulated serial port formatting for printing, its usage is the same as printf. It supports: %d %u %x %X %c %s * Input : *Fmt * Return : None *******************************************************************************/ void SoftUART_printf(const char *fmt, ...) { va_list args; va_start(args, fmt); // 解析可变参数 while(*fmt != '\0') { if(*fmt == '%') // 识别格式化占位符 { fmt++; switch(*fmt) { case 'd': // 有符号十进制整数 SoftUART_SendNum(va_arg(args, int), 10); break; case 'u': // 无符号十进制整数 SoftUART_SendNum(va_arg(args, uint32_t), 10); break; case 'x': // 小写十六进制整数 SoftUART_SendNum(va_arg(args, uint32_t), 16); break; case 'X': // 大写十六进制整数 (A-F) { char *p = "0123456789ABCDEF"; uint32_t num = va_arg(args, uint32_t); uint8_t buf[32],i=0; do{buf[i++] = p[num%16];num/=16;}while(num); if(i==0)SoftUART_SendByte('0'); else while(i--) SoftUART_SendByte(buf[i]); break; } case 'c': // 单个字符 SoftUART_SendByte(va_arg(args, int)); break; case 's': // 字符串 SoftUART_SendString((uint8_t*)va_arg(args, char*)); break; default: // 未知占位符,直接发送原字符 SoftUART_SendByte(*fmt); break; } } else // 普通字符,直接发送 { SoftUART_SendByte(*fmt); } fmt++; } va_end(args); // 结束可变参数解析 } debug_software_uart.h:
/********************************** (C) COPYRIGHT ******************************* * File Name : debug_software_uart.h * Author : 混分巨兽龙某某 * Version : V1.0.0 * Date : 2025/10/15 * Description : USE GPIO to Simulate The Software UART ********************************************************************************/ #ifndef __DEBUG_SOFTWARE_UART_H #define __DEBUG_SOFTWARE_UART_H #include "stm32f1xx_hal.h" #ifdef __cplusplus extern "C" { #endif /********************* 软件串口配置宏 *********************/ #define SYSCLK 72000000 #define SOFT_UART_TX_PIN GPIO_PIN_7 #define SOFT_UART_RX_PIN GPIO_PIN_8 #define SOFT_UART_PORT GPIOB // 波特率配置,修改此处即可切换,推荐9600 #define SOFT_UART_BAUD 9600 // 位周期计算:1/波特率,单位us,自动换算 #define BIT_DELAY_US (1000000 / SOFT_UART_BAUD) /********************* 引脚电平操作宏 *********************/ #define TX_HIGH() HAL_GPIO_WritePin(SOFT_UART_PORT, SOFT_UART_TX_PIN, GPIO_PIN_SET) //TX引脚置高 #define TX_LOW() HAL_GPIO_WritePin(SOFT_UART_PORT, SOFT_UART_TX_PIN, GPIO_PIN_RESET) //TX引脚置低 #define RX_READ() HAL_GPIO_ReadPin(SOFT_UART_PORT, SOFT_UART_RX_PIN) //读取RX引脚电平 /********************* 函数声明 *********************/ void HAL_Delay_Us(uint32_t us); void SoftUART_Init(void); void SoftUART_SendByte(uint8_t data); void SoftUART_SendString(uint8_t *str); uint8_t SoftUART_ReceiveByte(void); void SoftUART_SendDigit(uint8_t num); void SoftUART_SendNum(int32_t num, uint8_t base); void SoftUART_printf(const char *fmt, ...); #ifdef __cplusplus } #endif #endif4.2 main 函数
作者在 main 函数中进行编写代码的功能性测试,包含:有符号整型变量打印、无符号整型变量打印、十六进制变量打印、浮点型变量打印、字符变量打印以及串口数据接收。上述功能基本上可以满足读者朋友日常使用中的软件 UART 功能性需求!
main.c:
/** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ uint8_t recv_data; int16_t int_num = -128; // 有符号整型变量 uint32_t uint_num = 12345; // 无符号整型变量 uint16_t hex_num = 0x1A2B; // 十六进制变量 float float_num = 25.689f; // 浮点型变量 char ch = 'A'; // 字符变量 /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); /* USER CODE BEGIN 2 */ SoftUART_Init(); // 初始化软件 UART 的GPIO /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ /********** 测试1:纯字符串打印 **********/ SoftUART_SendString("=== GPIO模拟串口 数值打印测试 ===\r\n"); /********** 测试2:格式化打印 字符串+变量 组合(重点)**********/ SoftUART_printf("有符号整数:%d\r\n", int_num); SoftUART_printf("无符号整数:%u\r\n", uint_num); SoftUART_printf("十六进制数(小写):0x%x\r\n", hex_num); SoftUART_printf("十六进制数(大写):0x%X\r\n", hex_num); SoftUART_printf("单个字符:%c\r\n", ch); SoftUART_printf("混合打印:温度=%d℃,编号=%u\r\n",28, 666); /********** 测试3:串口接收并打印 **********/ recv_data = SoftUART_ReceiveByte(); if(recv_data != 0xFF) { SoftUART_SendByte(recv_data); } HAL_Delay(200); } /* USER CODE END 3 */ }五、软件 URAT 使用


作者使用沁恒微电子的串口上位机 WCH COMTransmit 软件针对编写的软件 UART 功能性测试,结果如上图所示。代码可以很稳定的实现软件 UART 的数据打印与字符接收。当然,本篇博客的代码是以 us 级别的电平信号进行操作的,如果借助 NOP 函数可以实现更高波特率的软件 UART!
六、代码开源
代码地址:基于STM32的普通GPIO模拟串口软件UART代码资源-ZEEKLOG下载
如果积分不够的朋友,点波关注,评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!