1. 项目概述:为什么需要模拟UART?
在嵌入式开发中,UART(通用异步收发传输器)是最基础、最常用的通信接口之一,用于设备间的串行数据交换。无论是单片机与传感器对话,还是通过USB转串口模块与PC通信,都离不开它。然而,在实际项目中,你可能会遇到一个尴尬的局面:硬件资源用尽了。主控芯片的硬件UART外设数量是有限的,比如一个常见的STM32F103系列,可能只有2-3个硬件UART。当你的项目需要连接GPS模块、蓝牙模块、调试串口等多个设备时,硬件UART口就显得捉襟见肘了。
这时候,“使用GPIO模拟UART”这项技术就派上了大用场。它的核心思想很简单:既然硬件UART的本质是按照特定时序(波特率、数据位、停止位、校验位)在两根线上(TX发送,RX接收)收发高低电平,那么我们完全可以用两个通用的输入输出引脚(GPIO),通过软件精确控制它们的电平翻转时序,来“模仿”硬件UART的行为。这相当于用软件的“软实力”去弥补硬件的“硬缺口”。
这种做法听起来像是“穷人的办法”,但它绝不仅仅是应急之策。首先,它赋予了设计极大的灵活性。你可以将“软串口”分配到任意可用的GPIO上,布线更方便。其次,它是对底层通信协议一次绝佳的学习机会。当你亲手用代码实现每一位的起始、数据、停止的时序控制时,会对UART协议有刻骨铭心的理解,这是看一百遍数据手册都换不来的。当然,它也有明显的代价:会持续占用CPU资源进行位时序的精确控制,对高波特率支持有限,且稳定性通常不如硬件UART。但对于一些非实时性要求、低速通信(如9600bps的传感器数据读取)的场景,它无疑是一个成本低廉且可靠的解决方案。
2. UART协议核心与GPIO模拟原理拆解
要模拟一个东西,必须先吃透它的规则。UART通信是一种异步、串行、全双工的通信方式。说人话就是:通信双方没有统一的时钟线来同步,数据是一位接一位地在单根线上传输,并且发送和接收可以同时进行(需要两根独立的线)。其核心在于对时序的精确约定,这个约定就是通信协议。
2.1 UART帧格式:一帧数据的“样貌”
一帧完整的UART数据,绝不是简单地把8位数据发出去就完事了。它有一个标准的“包装”格式。我们以最常见的配置(1位起始位,8位数据位,无校验位,1位停止位,波特率9600)为例,拆解一帧数据:
- 空闲状态:在无数据传输时,TX和RX线都保持在高电平(逻辑‘1’)。
- 起始位:这是一个低电平(逻辑‘0’),持续1个位的时间。它像一声哨响,告诉接收方:“注意,我要开始发数据了!”这是帧同步的关键。
- 数据位:紧接着起始位之后,就是实际的有效数据,从最低位(LSB)开始发送,依次到最高位(MSB)。每一位都占用1个位的时间,用高电平表示‘1’,低电平表示‘0’。
- 停止位:在数据位之后,是一个高电平(逻辑‘1’),持续1个(或1.5个,2个)位的时间。它标志着本帧数据的结束,并将线路拉回空闲状态,为下一帧数据做准备。
用波形图来理解最直观:一条原本持续为高的电平线,突然被拉低一个位时间(起始位),然后根据数据内容高低起伏8个位时间(数据位),最后再拉高至少一个位时间(停止位)。这个高低变化的序列,就是我们要用GPIO去精确复现的“舞蹈动作”。
2.2 关键参数:波特率与位时间
“位时间”是整个模拟过程的“节拍器”。它由“波特率”决定。波特率(Baud Rate)指的是每秒传输的符号数,在UART中,一个符号就是一位(bit)。所以,波特率9600意味着每秒传输9600个位。
位时间(Bit Time) = 1 / 波特率。
对于9600波特率:位时间 = 1 / 9600 ≈ 104.17 微秒(μs)。 对于115200波特率:位时间 = 1 / 115200 ≈ 8.68 微秒(μs)。
注意:这里的计算是理论值。在实际软件模拟中,我们通常用定时器来产生这个时间间隔。但软件指令执行本身有开销,所以实际设置的定时器中断周期可能需要略小于理论位时间,或者在中断服务程序中,采样点要设置在位的中间时刻,以规避信号边沿的抖动,这是提高稳定性的关键技巧。
2.3 GPIO模拟的底层逻辑
模拟发送(TX)相对简单:程序控制一个GPIO引脚为输出模式,然后按照帧格式,依次在计算好的时间点,将引脚电平设置为高或低。难点在于精确的延时。
模拟接收(RX)则复杂得多:程序需要将一个GPIO引脚设置为输入模式,并持续检测其电平。一旦检测到起始位的下降沿,就要启动一个定时器,在后续每个位时间的中间点(例如,起始位开始后的1.5个位时间处)去“采样”引脚电平,将其认定为数据位。这要求代码对起始边沿的检测非常灵敏,且定时器的时序必须高度精确。
核心挑战:在无硬件协助下,用软件实现微秒级的精确延时和边沿检测,同时不能阻塞主程序其他任务的运行。这通常需要借助定时器中断和外部中断(或引脚电平变化中断)来实现。
3. 模拟UART的软件架构与实现方案选择
用GPIO模拟UART,在软件上有几种不同复杂度和资源占用率的实现方案。选择哪种,取决于你的主控芯片性能、可用的外设资源以及对通信实时性的要求。
3.1 纯延时轮询法:最简单粗暴
这是最入门级的方法,适合对时序要求不严、波特率很低(如1200, 2400)且系统简单的场景。
- 发送:在发送每个位之前,用一个
for循环或while循环进行空操作,实现粗略的延时,然后设置GPIO电平。 - 接收:在一个循环里不断读取RX引脚,检测到低电平后,延时半个位时间(确认是否为起始位),然后每隔一个位时间采样一次数据。
- 优点:代码极其简单,不依赖任何外设。
- 致命缺点:
- 阻塞式:在发送或接收一帧数据期间,CPU被完全占用,无法执行其他任何任务。
- 极不精确:软件循环延时受编译器优化、中断打断等因素影响巨大,波特率误差大,通信极不稳定。
- 无法实现全双工:很难同时进行发送和接收。
实操心得:除非是教学演示或者驱动一个慢到令人发指的器件,否则在实际项目中应避免使用这种方法。它几乎无法用于任何可靠的数据交换。
3.2 定时器中断驱动法:最主流可靠
这是最常用、最可靠的软件模拟UART实现方式。其核心思想是利用一个定时器产生精确的位时间基准中断,用一个状态机来管理发送和接收流程。
- 发送流程:
- 当上层应用有数据要发送时,将数据帧(含起始位、数据位、停止位)按位放入一个缓冲区(或队列)。
- 开启定时器中断,周期设置为位时间。
- 在定时器中断服务程序(ISR)中,从缓冲区取出下一个要发送的位,将其输出到TX GPIO。发送完一帧后,关闭定时器中断或进入空闲状态。
- 接收流程:
- 将RX GPIO配置为输入,并使能其外部中断或引脚变化中断,触发条件为下降沿(检测起始位)。
- 当检测到起始位下降沿时,立即关闭该外部中断(防止干扰),并启动定时器。关键点:将第一次定时器中断的触发时间设置为1.5个位时间。这样,第一个中断到来时,正好位于第一个数据位(LSB)的中间,此时采样电平最稳定。
- 在定时器ISR中,采样RX引脚电平,存入接收缓冲区。采样8次(对应8个数据位)后,再采样停止位(可用于校验),然后重置状态,重新使能外部中断,等待下一帧。
- 优点:
- 高精度:依赖硬件定时器,时序非常精准。
- 非阻塞:发送和接收过程在中断中完成,主循环可以处理其他任务。
- 可实现全双工:使用两个独立的定时器和状态机,可以同时模拟发送和接收。
- 缺点:
- 需要占用一个硬件定时器资源。
- 中断频率较高(例如9600波特率时,中断频率为9600Hz),对CPU有一定负载。
3.3 高级方案:DMA+PWM/定时器(发送优化)
对于发送端,如果主控芯片支持,可以有更高效的方案,进一步解放CPU。
- PWM模拟TX:将GPIO配置为复用功能,连接到定时器的PWM输出通道。将要发送的帧(高低电平序列)预先转换成一个电平数组(1为高,0为低)。然后利用DMA,将这个数组自动搬运到定时器的比较寄存器(CCR)中。定时器会根据CCR值自动控制输出电平的翻转。这样,发送一整帧甚至多帧数据,完全不需要CPU干预。
- 定时器+DMA模拟TX:与PWM类似,但更灵活。将GPIO配置为普通输出,用定时器更新中断触发DMA,由DMA将电平数组逐个搬运到GPIO的输出数据寄存器(ODR)。这也实现了“零CPU开销”的发送。
- 优点:发送效率极高,CPU占用率几乎为零。
- 缺点:实现复杂,需要芯片支持,且通常只优化了发送端,接收端仍需用定时器中断法。
方案选择建议:对于绝大多数应用,定时器中断驱动法是最佳平衡点。它提供了可靠的精度和适中的资源占用,代码结构清晰,易于移植和调试。下文将重点围绕这种方法展开。
4. 基于定时器中断的模拟UART详细实现
我们以ARM Cortex-M内核的MCU(如STM32)为例,使用一个基本定时器(如TIM6/TIM7)或通用定时器,来详细实现一个9600波特率、8N1格式的模拟UART。
4.1 硬件与软件准备
- 硬件连接:
- 选择两个GPIO引脚,例如PA5作为模拟TX,PA6作为模拟RX。
- 确保这两个引脚是普通IO,没有被复用到其他特殊功能上。
- 软件准备:
- 初始化系统时钟,特别是定时器的时钟源(如APB1)。
- 配置GPIO:TX引脚为推挽输出,初始化为高电平(空闲状态)。RX引脚为上拉输入(或浮空输入,外部加上拉电阻),以便稳定检测高电平和下降沿。
- 配置一个定时器,用于产生位时间中断。以72MHz系统时钟,9600波特率为例:
- 位时间 = 1/9600 ≈ 104.17μs。
- 定时器时钟预分频(PSC)设置为71,则计数器时钟 = 72MHz / (71+1) = 1MHz,即每计数一次为1μs。
- 自动重装载值(ARR)设置为103,则定时器中断周期 = (103+1) * 1μs = 104μs,与理论值104.17μs误差极小,在可接受范围内。
- 配置RX引脚对应的外部中断线(如EXTI),触发方式为下降沿触发。
4.2 发送状态机实现
发送过程是一个典型的状态机。我们定义几个状态:TX_IDLE(空闲),TX_START(发送起始位),TX_DATA(发送数据位),TX_STOP(发送停止位)。
// 发送状态枚举 typedef enum { TX_STATE_IDLE, TX_STATE_START_BIT, TX_STATE_DATA_BITS, TX_STATE_STOP_BIT } tx_state_t; // 发送控制结构体 typedef struct { tx_state_t state; uint8_t data_byte; // 要发送的字节 uint8_t bit_index; // 当前发送到第几位 (0-7) uint8_t stop_bits; // 停止位个数 } soft_uart_tx_t; volatile soft_uart_tx_t soft_uart_tx; // 启动发送函数 (供上层调用) void soft_uart_send_byte(uint8_t data) { // 如果发送器忙,可以在这里选择等待或返回错误 if(soft_uart_tx.state != TX_STATE_IDLE) { // 处理忙状态,例如丢包或等待 return; } soft_uart_tx.data_byte = data; soft_uart_tx.bit_index = 0; soft_uart_tx.state = TX_STATE_START_BIT; soft_uart_tx.stop_bits = 1; // 1位停止位 // 手动执行第一次状态切换:输出起始位 SOFT_UART_TX_GPIO_PORT->BSRR = SOFT_UART_TX_PIN << 16; // 输出低电平 (起始位) // 启动定时器,开始发送流程 __HAL_TIM_SET_AUTORELOAD(&htim6, BIT_TIME_TICKS - 1); // 设置位时间 HAL_TIM_Base_Start_IT(&htim6); } // 定时器中断服务程序 (发送部分) void TIM6_IRQHandler(void) { if(__HAL_TIM_GET_FLAG(&htim6, TIM_FLAG_UPDATE) != RESET) { __HAL_TIM_CLEAR_FLAG(&htim6, TIM_FLAG_UPDATE); switch(soft_uart_tx.state) { case TX_STATE_START_BIT: // 起始位已发送完毕,切换到发送数据位状态 soft_uart_tx.state = TX_STATE_DATA_BITS; // 输出LSB (bit0) if(soft_uart_tx.data_byte & 0x01) { SOFT_UART_TX_GPIO_PORT->BSRR = SOFT_UART_TX_PIN; // 输出高 } else { SOFT_UART_TX_GPIO_PORT->BSRR = SOFT_UART_TX_PIN << 16; // 输出低 } break; case TX_STATE_DATA_BITS: soft_uart_tx.bit_index++; if(soft_uart_tx.bit_index < 8) { // 发送下一个数据位 if(soft_uart_tx.data_byte & (1 << soft_uart_tx.bit_index)) { SOFT_UART_TX_GPIO_PORT->BSRR = SOFT_UART_TX_PIN; } else { SOFT_UART_TX_GPIO_PORT->BSRR = SOFT_UART_TX_PIN << 16; } } else { // 8位数据发送完毕,切换到停止位 soft_uart_tx.state = TX_STATE_STOP_BIT; SOFT_UART_TX_GPIO_PORT->BSRR = SOFT_UART_TX_PIN; // 输出高 (停止位) } break; case TX_STATE_STOP_BIT: // 停止位发送完毕,回到空闲状态 soft_uart_tx.state = TX_STATE_IDLE; HAL_TIM_Base_Stop_IT(&htim6); // 停止定时器 // 可以在这里触发一个发送完成回调函数 break; case TX_STATE_IDLE: default: // 不应该进入此分支,安全起见停止定时器 HAL_TIM_Base_Stop_IT(&htim6); break; } } }4.3 接收状态机实现
接收过程同样是一个状态机,但由外部中断启动。状态包括:RX_IDLE(等待起始位),RX_START(确认起始位),RX_DATA(接收数据位),RX_STOP(接收停止位)。
// 接收状态枚举 typedef enum { RX_STATE_IDLE, RX_STATE_START_BIT, RX_STATE_DATA_BITS, RX_STATE_STOP_BIT } rx_state_t; // 接收控制结构体 typedef struct { rx_state_t state; uint8_t data_byte; uint8_t bit_index; uint8_t stop_bits; } soft_uart_rx_t; volatile soft_uart_rx_t soft_uart_rx; volatile uint8_t rx_buffer[RX_BUF_SIZE]; volatile uint16_t rx_write_idx = 0; // RX引脚外部中断服务程序 void EXTI9_5_IRQHandler(void) { // 假设RX引脚在EXTI Line6上 if(__HAL_GPIO_EXTI_GET_FLAG(SOFT_UART_RX_PIN) != RESET) { __HAL_GPIO_EXTI_CLEAR_FLAG(SOFT_UART_RX_PIN); // 只有在空闲状态检测到下降沿才认为是起始位 if(soft_uart_rx.state == RX_STATE_IDLE) { // 禁用外部中断,防止在接收过程中被干扰 HAL_NVIC_DisableIRQ(EXTI9_5_IRQn); soft_uart_rx.state = RX_STATE_START_BIT; soft_uart_rx.data_byte = 0; soft_uart_rx.bit_index = 0; soft_uart_rx.stop_bits = 1; // 关键步骤:设置定时器在1.5个位时间后首次中断,用于采样第一个数据位的中点 __HAL_TIM_SET_COUNTER(&htim7, 0); __HAL_TIM_SET_AUTORELOAD(&htim7, (BIT_TIME_TICKS * 3 / 2) - 1); // 1.5倍位时间 HAL_TIM_Base_Start_IT(&htim7); } } } // 定时器7中断服务程序 (用于接收采样) void TIM7_IRQHandler(void) { if(__HAL_TIM_GET_FLAG(&htim7, TIM_FLAG_UPDATE) != RESET) { __HAL_TIM_CLEAR_FLAG(&htim7, TIM_FLAG_UPDATE); switch(soft_uart_rx.state) { case RX_STATE_START_BIT: // 1.5个位时间后,理论上位于第一个数据位中点,应采样数据位 // 但为了状态机统一,我们先切换到数据位状态,并立即采样 soft_uart_rx.state = RX_STATE_DATA_BITS; __HAL_TIM_SET_AUTORELOAD(&htim7, BIT_TIME_TICKS - 1); // 后续按1个位时间采样 // 注意:这里不break,继续执行RX_STATE_DATA_BITS的逻辑进行第一次采样 case RX_STATE_DATA_BITS: // 采样当前RX引脚电平 if(HAL_GPIO_ReadPin(SOFT_UART_RX_GPIO_PORT, SOFT_UART_RX_PIN) == GPIO_PIN_SET) { soft_uart_rx.data_byte |= (1 << soft_uart_rx.bit_index); } soft_uart_rx.bit_index++; if(soft_uart_rx.bit_index >= 8) { // 8位数据接收完毕,切换到停止位状态 soft_uart_rx.state = RX_STATE_STOP_BIT; } break; case RX_STATE_STOP_BIT: // 采样停止位电平(可用于帧错误检测) // uint8_t stop_bit = HAL_GPIO_ReadPin(...); // if(stop_bit != 1) { /* 帧错误处理 */ } // 一帧接收完成,存入缓冲区 if(rx_write_idx < RX_BUF_SIZE) { rx_buffer[rx_write_idx++] = soft_uart_rx.data_byte; } else { // 缓冲区溢出处理 } // 重置状态,重新使能外部中断,等待下一帧 soft_uart_rx.state = RX_STATE_IDLE; HAL_TIM_Base_Stop_IT(&htim7); HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); break; case RX_STATE_IDLE: default: HAL_TIM_Base_Stop_IT(&htim7); HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); break; } } }关键技巧:接收时序的“1.5倍起始位时间”是软件模拟UART稳定性的灵魂。它确保了第一次采样点位于第一个数据位的正中央,避开了由于起始边沿检测抖动或波特率微小偏差导致的位边界区域,大大提高了容错率。
5. 性能优化、稳定性提升与高级功能
一个基础的模拟UART跑起来后,我们还需要考虑如何让它更健壮、更高效。
5.1 波特率自适应性调整
硬件UART有自动波特率检测功能,软件模拟也可以尝试实现简易版本。一种思路是:在通信开始前,由主机发送一个特定的同步字节(如0x55,二进制为01010101)。从机通过测量两个上升沿或下降沿之间的时间间隔,可以反推出实际的位时间,从而动态调整定时器的ARR值。这需要用到输入捕获功能,实现起来较复杂,但对主从机时钟偏差较大的场景很有用。
5.2 缓冲区与流量控制
上面的示例代码使用了简单的全局变量作为缓冲区,这在中断服务程序中直接读写存在风险(虽然volatile可以缓解)。更稳健的做法是使用环形缓冲区(Ring Buffer)。
- 发送环形缓冲区:上层应用将待发送的字节放入发送环形缓冲区。发送状态机(在定时器中断中)从缓冲区中取出字节发送。这样,上层可以连续写入多个字节,而不会阻塞。
- 接收环形缓冲区:接收状态机将收到的字节放入接收环形缓冲区。主循环从缓冲区中读取并处理数据。这解决了中断中不能进行复杂处理以及数据覆盖的问题。
// 简易环形缓冲区实现示例 typedef struct { uint8_t *buffer; uint16_t size; volatile uint16_t head; // 写索引 volatile uint16_t tail; // 读索引 } ring_buffer_t; // 判断缓冲区是否满/空,以及读写操作都需要考虑索引回绕5.3 错误检测与处理
硬件UART有帧错误、溢出错误等标志,软件模拟也可以加入:
- 帧错误:在停止位采样时,如果电平不是高电平,则说明可能存在噪声、波特率不匹配或线路问题,可以置位一个帧错误标志。
- 溢出错误:当接收环形缓冲区已满,但又有新数据到来时,应置位溢出错误标志,并选择丢弃新数据或覆盖旧数据。
- 超时错误:如果在一帧数据接收过程中,长时间没有收到下一个位(例如,超过10个位时间),可以认为通信中断,应复位接收状态机,重新等待起始位。这可以通过一个看门狗定时器实现。
5.4 降低CPU中断负载
高波特率下,定时器中断非常频繁。为了减轻CPU负担,可以:
- 使用DMA辅助发送(如前文所述)。
- 如果芯片支持,将定时器中断优先级设置为较低,避免打断更关键的任务。
- 在满足时序要求的前提下,尽量使用低波特率。
6. 实测调试技巧与常见问题排查
理论实现后,真正的挑战在调试环节。你需要一个逻辑分析仪或者一个带串口功能的示波器,这是调试数字时序的“眼睛”。
6.1 调试工具与连接
- 逻辑分析仪:这是最佳选择。将通道1连接到你的模拟TX引脚,通道2连接到模拟RX引脚。设置好采样率和触发条件(如TX引脚下降沿触发)。可以清晰看到每一位的波形、宽度,以及起始位、数据位、停止位的结构。对比发送的数据和波形是否一致。
- USB转TTL串口模块:将你的模拟UART的TX、RX与模块的RX、TX交叉连接,模块的USB端插入电脑。用串口调试助手(如SecureCRT、Putty、或者免费的SSCOM)发送数据给你的模拟UART,并接收模拟UART发回的数据。这是功能验证最直接的方法。
- 示波器:如果只有示波器,可以设置为单次触发,触发条件为下降沿,观察一帧数据的波形。测量位时间是否准确。
6.2 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| 完全收不到数据 | 1. 引脚配置错误(输入/输出弄反)。 2. 电平不匹配(3.3V vs 5V)。 3. 外部中断未正确触发。 4. 定时器未启动或配置错误。 | 1. 用万用表或点灯法确认引脚电平能否被控制/读取。 2. 确认通信双方电平标准,必要时加电平转换电路。 3. 在外部中断服务函数入口处设置一个断点或翻转一个测试引脚,看是否进入。 4. 检查定时器时钟源、分频、ARR值计算是否正确。 |
| 收到乱码 | 1.波特率不匹配(最常见)。 2. 数据位/停止位配置不一致。 3. 采样点位置不对(特别是接收)。 4. 中断被更高优先级中断长时间阻塞。 | 1.用逻辑分析仪测量实际位时间,与理论值对比。调整定时器ARR值。 2. 确认双方都是8N1格式。检查停止位发送和采样逻辑。 3.重点检查接收首次采样是否为1.5个位时间。逻辑分析仪看采样点是否在位的中间。 4. 检查系统中其他中断服务程序的执行时间,优化代码或调整中断优先级。 |
| 只能接收单个字节,不能连续接收 | 1. 接收状态机在完成一帧后没有正确复位到IDLE状态。 2. 外部中断在接收完成后没有重新使能。 3. 停止位处理异常导致状态机卡住。 | 1. 在接收完成(停止位后)的逻辑里,确保状态state被重置为RX_STATE_IDLE。2. 确认 HAL_NVIC_EnableIRQ被调用。3. 即使停止位错误,也应强制复位状态机,并记录错误。 |
| 高波特率下(如115200)通信不稳定 | 1. 中断响应时间、执行时间接近或超过位时间。 2. 系统主频太低。 3. 没有使用优化等级(如-O1, -O2)。 | 1. 用逻辑分析仪测量中断服务程序的实际执行时间。必须远小于位时间(115200时位时间约8.7μs)。 2. 提升MCU主频。 3. 在编译器设置中开启优化,减少不必要的开销。 4.考虑放弃纯软件模拟,或使用硬件辅助方案(PWM+DMA)。 |
| 发送数据正常,但对方设备无响应 | 1. 对方设备需要特定的初始化命令或协议。 2. 硬件流控(RTS/CTS)未处理。 3. 线路干扰或接触不良。 | 1. 先用电脑串口助手确认你的模拟UART发送的数据格式完全正确。 2. 查阅对方设备手册,确认通信协议。 3. 如果对方有流控引脚,尝试将其固定为有效电平(如使能)。 4. 检查硬件连接,缩短导线,或加上拉电阻。 |
6.3 一个宝贵的避坑经验:中断嵌套与优先级
在混合了外部中断(起始位检测)和定时器中断(位采样)的系统中,中断优先级设置不当会导致灾难。一个典型的坑是:定时器中断优先级高于外部中断。
假设定时器中断正在执行采样,此时一个噪声脉冲触发了外部中断(下降沿)。如果外部中断优先级更高,它会打断定时器中断,导致采样时序完全错乱。因此,必须将用于位采样的定时器中断优先级设置为高于用于起始位检测的外部中断优先级。这样,一旦接收流程开始,定时器中断就不会被意外的起始位检测打断,保证了采样时序的完整性。
我个人在多个项目中的体会是,GPIO模拟UART是一项“知其然更要知其所以然”的基本功。它迫使你去关注最底层的时序细节,这种经验对理解任何通信协议都有莫大帮助。虽然现在MCU的UART外设越来越多,但在引脚紧张、需要动态配置通信端口或者进行协议教学时,它依然是一个非常有价值的工具。最后一个小技巧:在项目初期,不妨用硬件UART进行开发和调试,待稳定后,再将代码移植到模拟UART上,并做好充分的测试,这样可以大大降低开发风险。