STM32普通IO模拟串口:从原理到实战,解决硬件资源不足的困境
2026/7/15 3:05:39 网站建设 项目流程

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位停止位)为例,传输一个字节'A'(ASCII 0x41)的波形如下:

0 1 0 0 0 0 0 1 1 | | | | | | | | | 起始 D0 D1 D2 D3 D4 D5 D6 D7 停止

2.2 波特率与时序关键

波特率决定了每位数据的持续时间。例如:

  • 9600bps:每位持续 1/9600 ≈ 104μs
  • 115200bps:每位持续 8.68μs

在IO模拟实现中,定时器的精度直接决定通信可靠性。我曾用STM32的SysTick做延时,在115200波特率下误差太大,后来改用定时器硬件中断才稳定。

3. 硬件与软件设计对比

3.1 硬件串口的优势

  • 自动处理时序,CPU占用低
  • 支持DMA传输
  • 内置FIFO缓冲
  • 错误检测机制完善

3.2 软件模拟的特点

  • 优点:不占用硬件串口资源,引脚选择灵活
  • 缺点:CPU占用高(尤其高速波特率),精度依赖定时器,无硬件错误检测

实测数据对比(STM32F103@72MHz):

指标硬件串口软件模拟(定时器)
最大波特率4.5Mbps115200bps
CPU占用率<1%~15% (115200)
字节间隔任意需严格定时

4. 具体实现步骤

4.1 硬件资源准备

以STM32F103为例,需要:

  • 2个GPIO(TX输出、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 i=0; i<8; i++) { TX_PIN = (data >> i) & 0x01; Delay_us(104); } // 停止位 TX_PIN = 1; Delay_us(104); }

实测建议:对于高于9600的波特率,建议用定时器中断代替Delay_us,因为函数调用开销会导致时序偏差。

4.3 接收功能实现

接收更复杂,需要:

  1. RX引脚配置下降沿中断
  2. 检测到起始位后,启动定时器
  3. 在每位中间时刻采样
// 外部中断服务函数 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 代码优化技巧

  1. 发送优化
// 预先计算好各时间点的寄存器值 uint32_t tx_timings[10] = {0,104,208,...}; void TIM_IRQHandler() { static uint8_t state = 0; switch(state) { case 0: TX_PIN=0; break; // 起始位 case 1...8: TX_PIN=(data>>(state-1))&1; break; case 9: TX_PIN=1; break; // 停止位 } state++; if(state > 9) state = 0; }
  1. 接收抗干扰
  • 在每位采样时多次读取(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. 实际应用建议

  1. 波特率选择

    • 对于72MHz主频的STM32F103,建议不超过115200bps
    • 低功耗设备建议使用9600bps
  2. 引脚选择

    • 避免使用JTAG/SWD复用引脚
    • 长距离传输时添加RS232/RS485转换芯片
  3. 错误处理

    • 添加超时重传机制
    • 重要数据使用校验和或CRC
  4. 多串口管理

    • 可以为每个虚拟串口创建独立的数据结构
    • 使用状态机模式管理通信流程

我在工业控制器项目中曾用这种方法实现了4路串口(1硬件+3模拟),关键是要做好各通道的优先级管理。当多个外设同时通信时,适当降低非关键通道的波特率可以保证系统稳定性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询