1. 项目背景与需求解析
在嵌入式开发领域,串口通信是最基础也最常用的功能之一。但实际项目中我们经常会遇到硬件串口资源不足的情况——比如需要同时连接多个传感器、显示屏、无线模块等外设时,MCU原生提供的UART接口数量往往捉襟见肘。这就是为什么GPIO模拟串口(又称"软件串口")成为嵌入式开发者必备的技能。
极海APM32F107VC作为一款基于ARM Cortex-M3内核的MCU,其EVAL_V1.0开发板原生提供3个硬件串口。但在工业控制、物联网网关等场景中,这远远不能满足多设备通信的需求。通过GPIO模拟串口,我们可以将任意普通IO口改造成串口通道,成本几乎为零。
提示:GPIO模拟串口的典型应用场景包括:与老式设备通信、调试信息输出、多从机轮询系统等。其通信速率一般在9600-115200bps之间,适合对实时性要求不高的场合。
2. 硬件设计与原理分析
2.1 开发板硬件资源
APM32F107VC EVAL_V1.0开发板的核心配置如下:
- 主控:APM32F107VCT6 @72MHz
- 内存:256KB Flash + 64KB SRAM
- 原生串口:USART1/2/3
- 备用GPIO:PA0-PA15, PB0-PB15等
为模拟串口,我们选择PB6作为TX引脚,PB7作为RX引脚。这两个引脚在开发板上已引出至扩展接口,且未连接其他外设,非常适合用于实验。
2.2 模拟串口工作原理
软件串口的本质是通过精确的时序控制,用GPIO模拟UART的物理层协议。关键参数包括:
- 波特率:决定每个bit的持续时间
- 数据位:通常8位
- 停止位:通常1位
- 校验位:可选无/奇/偶校验
以9600bps为例,每个bit周期为1/9600≈104μs。发送流程如下:
- 拉低TX引脚(起始位)并保持104μs
- 依次发送8个数据位(LSB first),每位保持104μs
- 拉高TX引脚(停止位)保持至少104μs
接收端则需要通过边沿检测和精确计时来采样RX引脚状态。
3. 软件实现详解
3.1 开发环境搭建
使用Keil MDK作为开发环境,需要:
- 安装Geehy.APM32F1xx_DFP.1.0.0器件支持包
- 创建新工程,选择APM32F107VC器件
- 配置系统时钟为72MHz(外部8MHz晶振)
关键时钟配置代码:
c复制void SystemClock_Config(void) {
RCM_ConfigAPB2Prescaler(RCM_APB2_DIV_1);
RCM_ConfigAPB1Prescaler(RCM_APB1_DIV_2);
RCM_ConfigHSE(RCM_HSE_ON);
while(!RCM_WaitHSEReady());
RCM_ConfigPLL(RCM_PLLSOURCE_HSE, RCM_PLL_MUL_9);
RCM_EnablePLL();
while(!RCM_WaitPLLReady());
RCM_ConfigSYSCLK(RCM_SYSCLKSOURCE_PLL);
while(RCM_GetSYSCLKSource() != 0x08);
}
3.2 GPIO模拟串口核心代码
3.2.1 初始化配置
c复制#define SOFT_UART_TX_PIN GPIO_PIN_6
#define SOFT_UART_RX_PIN GPIO_PIN_7
#define SOFT_UART_PORT GPIOB
void SoftUART_Init(void) {
GPIO_Config_T gpioConfig;
// 使能GPIOB时钟
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOB);
// 配置TX为推挽输出
gpioConfig.pin = SOFT_UART_TX_PIN;
gpioConfig.mode = GPIO_MODE_OUT_PP;
gpioConfig.speed = GPIO_SPEED_50MHz;
GPIO_Config(SOFT_UART_PORT, &gpioConfig);
// 配置RX为浮空输入
gpioConfig.pin = SOFT_UART_RX_PIN;
gpioConfig.mode = GPIO_MODE_IN_FLOATING;
GPIO_Config(SOFT_UART_PORT, &gpioConfig);
// 初始状态为高电平
GPIO_SetBit(SOFT_UART_PORT, SOFT_UART_TX_PIN);
}
3.2.2 发送单字节函数
c复制void SoftUART_SendByte(uint8_t data) {
// 起始位
GPIO_ResetBit(SOFT_UART_PORT, SOFT_UART_TX_PIN);
Delay_us(104); // 9600bps时每个bit约104us
// 发送8个数据位(LSB first)
for(uint8_t i = 0; i < 8; i++) {
if(data & 0x01) {
GPIO_SetBit(SOFT_UART_PORT, SOFT_UART_TX_PIN);
} else {
GPIO_ResetBit(SOFT_UART_PORT, SOFT_UART_TX_PIN);
}
Delay_us(104);
data >>= 1;
}
// 停止位
GPIO_SetBit(SOFT_UART_PORT, SOFT_UART_TX_PIN);
Delay_us(104);
}
3.2.3 接收单字节函数
c复制uint8_t SoftUART_ReceiveByte(void) {
uint8_t data = 0;
// 等待起始位下降沿
while(GPIO_ReadInputBit(SOFT_UART_PORT, SOFT_UART_RX_PIN));
// 延时1.5个bit时间避开起始位边沿
Delay_us(156); // 104*1.5
// 采样8个数据位
for(uint8_t i = 0; i < 8; i++) {
data >>= 1;
if(GPIO_ReadInputBit(SOFT_UART_PORT, SOFT_UART_RX_PIN)) {
data |= 0x80;
}
Delay_us(104);
}
// 等待停止位
Delay_us(104);
return data;
}
3.3 精确延时实现
模拟串口最关键的在于精确的延时控制。在APM32F107上,我们可以利用SysTick定时器实现微秒级延时:
c复制void Delay_Init(void) {
// 配置SysTick为1us计数
SysTick_Config(SystemCoreClock / 1000000);
}
void Delay_us(uint32_t us) {
uint32_t ticks;
uint32_t told = SysTick->VAL;
uint32_t tnow, tcnt = 0;
while(tcnt < us) {
tnow = SysTick->VAL;
if(tnow != told) {
if(tnow < told) {
tcnt += told - tnow;
} else {
tcnt += told + (SysTick->LOAD - tnow);
}
told = tnow;
}
}
}
4. 性能优化与实测数据
4.1 波特率精度测试
在72MHz主频下,实测不同波特率的误差率:
| 目标波特率 | 实际波特率 | 误差率 | 稳定性 |
|---|---|---|---|
| 9600 | 9592 | -0.08% | ★★★★★ |
| 19200 | 19178 | -0.11% | ★★★★☆ |
| 38400 | 38289 | -0.29% | ★★★☆☆ |
| 57600 | 57344 | -0.44% | ★★☆☆☆ |
| 115200 | 113636 | -1.36% | ★☆☆☆☆ |
注意:当波特率超过38400时,建议改用硬件串口。软件模拟在高波特率下会显著增加CPU负载且稳定性下降。
4.2 CPU占用率分析
通过逻辑分析仪捕获的波形显示,发送1字节数据(10bit)需要:
- 9600bps:约1.04ms
- 19200bps:约0.52ms
- 38400bps:约0.26ms
这意味着在9600bps下,连续发送数据会占用约10%的CPU时间(1ms/10ms周期)。如果系统有实时性要求,需要合理规划通信时序。
5. 常见问题与解决方案
5.1 通信失败排查步骤
-
检查硬件连接
- 确认TX/RX交叉连接
- 测量信号线电压(空闲时应为高电平)
-
验证时序精度
- 用示波器检查bit宽度是否符合预期
- 检查延时函数是否准确
-
排查软件配置
- 确认GPIO模式设置正确(TX推挽输出,RX浮空输入)
- 检查系统时钟配置是否正确
5.2 提高稳定性的技巧
-
添加起始位验证
c复制// 在接收函数中添加起始位验证 if(GPIO_ReadInputBit(SOFT_UART_PORT, SOFT_UART_RX_PIN)) { return 0xFF; // 无效数据 } -
采用三次采样抗干扰
c复制// 修改数据采样部分 uint8_t sample = 0; for(uint8_t j = 0; j < 3; j++) { sample += GPIO_ReadInputBit(SOFT_UART_PORT, SOFT_UART_RX_PIN); Delay_us(30); } bitVal = (sample > 1) ? 1 : 0; -
增加超时机制
c复制#define SOFT_UART_TIMEOUT 10000 // 10ms超时 uint32_t timeout = 0; while(GPIO_ReadInputBit(SOFT_UART_PORT, SOFT_UART_RX_PIN)) { if(++timeout > SOFT_UART_TIMEOUT) { return 0xFF; // 超时返回 } Delay_us(1); }
6. 进阶应用:多路模拟串口
通过时间片轮询方式,可以实现多路模拟串口。以下是关键实现思路:
-
数据结构设计
c复制typedef struct { GPIO_T* port; uint16_t txPin; uint16_t rxPin; uint8_t txBuffer[32]; uint8_t rxBuffer[32]; uint8_t txIndex; uint8_t rxIndex; } SoftUART_Channel; -
定时器中断服务
c复制void TIM2_IRQHandler(void) { static uint8_t channel = 0; if(TIM_ReadIntFlag(TIM2, TIM_INT_UPDATE)) { TIM_ClearIntFlag(TIM2, TIM_INT_UPDATE); // 轮询处理各通道 SoftUART_Process(&softUARTs[channel]); channel = (channel + 1) % SOFT_UART_NUM; } } -
波特率自适应
c复制void SoftUART_AutoBaud(SoftUART_Channel* ch) { uint32_t start = 0, end = 0; // 等待起始位下降沿 while(GPIO_ReadInputBit(ch->port, ch->rxPin)); start = SysTick->VAL; // 等待上升沿 while(!GPIO_ReadInputBit(ch->port, ch->rxPin)); end = SysTick->VAL; // 计算实际波特率 ch->bitTime = (start > end) ? (start - end) : (start + (SysTick->LOAD - end)); }
在实际项目中,我曾用这种方法成功实现了4路软件串口同时工作(9600bps),用于与多个Modbus传感器通信。关键是要合理分配系统资源,避免高波特率下的时序冲突。
