1. 为什么需要IO模拟串口在实际的嵌入式开发中我们经常会遇到硬件资源不足的情况。比如手头的STM32芯片只有1-2个硬件串口但项目需要连接GPS模块、蓝牙模块、传感器等多个外设硬件串口数量就捉襟见肘了。这时候用普通IO口模拟串口就成了一个经济实用的解决方案。我第一次遇到这个问题是在做一个智能农业监测项目时需要同时连接土壤传感器、气象站和LoRa无线模块但使用的STM32F103C8T6只有2个硬件串口。当时尝试过用软件模拟第三个串口结果因为时序控制不精准导致数据错乱后来通过优化定时器才解决问题。2. 串口通信的基本原理2.1 串口数据帧结构串口通信的核心在于时序控制。一个标准的UART数据帧包含起始位1位低电平0数据位5-9位通常8位校验位可选奇校验/偶校验停止位1-2位高电平1以最常见的8-N-1格式8位数据、无校验、1位停止位为例传输一个字节AASCII 0x41的波形如下0 1 0 0 0 0 0 1 1 | | | | | | | | | 起始 D0 D1 D2 D3 D4 D5 D6 D7 停止2.2 波特率与时序关键波特率决定了每位数据的持续时间。例如9600bps每位持续 1/9600 ≈ 104μs115200bps每位持续 8.68μs在IO模拟实现中定时器的精度直接决定通信可靠性。我曾用STM32的SysTick做延时在115200波特率下误差太大后来改用定时器硬件中断才稳定。3. 硬件与软件设计对比3.1 硬件串口的优势自动处理时序CPU占用低支持DMA传输内置FIFO缓冲错误检测机制完善3.2 软件模拟的特点优点不占用硬件串口资源引脚选择灵活缺点CPU占用高尤其高速波特率精度依赖定时器无硬件错误检测实测数据对比STM32F10372MHz指标硬件串口软件模拟(定时器)最大波特率4.5Mbps115200bpsCPU占用率1%~15% (115200)字节间隔任意需严格定时4. 具体实现步骤4.1 硬件资源准备以STM32F103为例需要2个GPIOTX输出、RX输入1个外部中断用于RX下降沿检测2个定时器1个用于发送延时1个用于接收采样推荐配置TX推挽输出模式RX上拉输入模式配置下降沿触发中断4.2 发送功能实现发送时序的关键是精确控制每位数据的持续时间。以9600bps发送一个字节为例void IO_UART_SendByte(uint8_t data) { // 起始位 TX_PIN 0; Delay_us(104); // 数据位LSB first for(int i0; i8; i) { TX_PIN (data i) 0x01; Delay_us(104); } // 停止位 TX_PIN 1; Delay_us(104); }实测建议对于高于9600的波特率建议用定时器中断代替Delay_us因为函数调用开销会导致时序偏差。4.3 接收功能实现接收更复杂需要RX引脚配置下降沿中断检测到起始位后启动定时器在每位中间时刻采样// 外部中断服务函数 void EXTI_IRQHandler() { if(检测到下降沿) { 关闭外部中断(); 启动定时器(104/2 μs); // 半位时间后采样 } } // 定时器中断服务函数 void TIM_IRQHandler() { static uint8_t bitCount 0; static uint8_t rxData 0; if(bitCount 0) { // 应处于起始位中间验证是否为低电平 if(RX_PIN 1) { // 错误重新开启外部中断 开启外部中断(); return; } } else if(bitCount 8) { // 数据位采样 rxData | (RX_PIN (bitCount-1)); } else { // 停止位验证 if(RX_PIN 0) { // 帧错误处理 } // 完成接收 处理接收数据(rxData); 开启外部中断(); return; } bitCount; 重置定时器(104μs); }5. 关键问题与优化5.1 波特率精度问题普通延时函数的误差会累积。解决方案使用硬件定时器TIM2/TIM3等在中断服务函数中直接操作IO对于高速波特率考虑使用PWM模式生成精确时序5.2 中断冲突处理当系统中有多个中断源时设置模拟串口中断为较高优先级在关键时序段禁用其他中断使用RTOS时保持中断服务函数尽可能简短5.3 代码优化技巧发送优化// 预先计算好各时间点的寄存器值 uint32_t tx_timings[10] {0,104,208,...}; void TIM_IRQHandler() { static uint8_t state 0; switch(state) { case 0: TX_PIN0; break; // 起始位 case 1...8: TX_PIN(data(state-1))1; break; case 9: TX_PIN1; break; // 停止位 } state; if(state 9) state 0; }接收抗干扰在每位采样时多次读取3取2添加简单的CRC校验设置超时机制如1.5个字符时间无数据视为帧结束6. 完整代码示例以下是基于STM32标准外设库的实现框架// iousart.h #ifndef __IO_USART_H #define __IO_USART_H #include stm32f10x.h #define IO_USART_BAUDRATE 9600 #define BIT_DURATION (1000000/IO_USART_BAUDRATE) // us void IO_USART_Init(void); void IO_USART_SendByte(uint8_t data); uint8_t IO_USART_ReceiveByte(void); #endif// iousart.c #include iousart.h #include stm32f10x_gpio.h #include stm32f10x_tim.h #include stm32f10x_exti.h #define TX_PIN GPIO_Pin_9 #define RX_PIN GPIO_Pin_10 #define GPIO_PORT GPIOB static volatile uint8_t rx_data 0; static volatile uint8_t rx_flag 0; void IO_USART_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_InitStruct; EXTI_InitTypeDef EXTI_InitStruct; NVIC_InitTypeDef NVIC_InitStruct; // 1. 时钟使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 2. GPIO配置 GPIO_InitStruct.GPIO_Pin TX_PIN; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIO_PORT, GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin RX_PIN; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIO_PORT, GPIO_InitStruct); // 3. 外部中断配置 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource10); EXTI_InitStruct.EXTI_Line EXTI_Line10; EXTI_InitStruct.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStruct.EXTI_Trigger EXTI_Trigger_Falling; EXTI_InitStruct.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStruct); // 4. 定时器配置 TIM_InitStruct.TIM_Period BIT_DURATION * 72 / 1000; // 72MHz主频 TIM_InitStruct.TIM_Prescaler 0; TIM_InitStruct.TIM_ClockDivision 0; TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_InitStruct); // 5. NVIC配置 NVIC_InitStruct.NVIC_IRQChannel EXTI15_10_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStruct.NVIC_IRQChannelSubPriority 1; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); NVIC_InitStruct.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStruct.NVIC_IRQChannelSubPriority 2; NVIC_Init(NVIC_InitStruct); } void IO_USART_SendByte(uint8_t data) { // 实现同上 } uint8_t IO_USART_ReceiveByte(void) { while(!rx_flag); // 等待接收完成 rx_flag 0; return rx_data; } // 中断服务函数 void EXTI15_10_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line10) ! RESET) { EXTI_ClearITPendingBit(EXTI_Line10); // 检测到起始位启动定时器 TIM_SetCounter(TIM3, 0); TIM_Cmd(TIM3, ENABLE); } } void TIM3_IRQHandler(void) { static uint8_t bit_count 0; static uint8_t temp_data 0; if(TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); if(bit_count 0) { // 验证起始位 if(GPIO_ReadInputDataBit(GPIO_PORT, RX_PIN) ! Bit_RESET) { TIM_Cmd(TIM3, DISABLE); return; } } else if(bit_count 8) { // 采样数据位 temp_data | (GPIO_ReadInputDataBit(GPIO_PORT, RX_PIN) (bit_count-1)); } else { // 停止位 TIM_Cmd(TIM3, DISABLE); rx_data temp_data; rx_flag 1; bit_count 0; temp_data 0; return; } bit_count; } }7. 实际应用建议波特率选择对于72MHz主频的STM32F103建议不超过115200bps低功耗设备建议使用9600bps引脚选择避免使用JTAG/SWD复用引脚长距离传输时添加RS232/RS485转换芯片错误处理添加超时重传机制重要数据使用校验和或CRC多串口管理可以为每个虚拟串口创建独立的数据结构使用状态机模式管理通信流程我在工业控制器项目中曾用这种方法实现了4路串口1硬件3模拟关键是要做好各通道的优先级管理。当多个外设同时通信时适当降低非关键通道的波特率可以保证系统稳定性。