1. 项目背景与需求解析
在嵌入式开发中,串口通信是最基础也最常用的外设之一。但实际项目中,我们经常会遇到硬件串口资源不足的情况——比如需要同时连接多个传感器、调试接口、无线模块等。这时候,用GPIO模拟串口(Software UART)就成了一个实用的解决方案。
我最近在使用极海APM32F107VC开发板时,就遇到了这样的需求:板载的硬件串口已经全部占用,但还需要一个额外的调试输出通道。于是决定用PD13和PD14这两个GPIO引脚,通过软件模拟实现9600bps的串口通信。
硬件背景:APM32F107VC与STM32F107是Pin to Pin兼容的,但内核不同,需要专用的SDK开发包。这也是很多开发者容易踩坑的地方。
2. 串口协议核心原理拆解
2.1 异步串口通信的本质
异步串口通信的核心特点是无时钟同步,完全依赖双方预先约定好的波特率。这意味着:
- 时序精度要求极高:每个位的持续时间必须严格等于波特率倒数(如9600bps时,每位104.17μs)
- 帧结构明确:每帧数据必须包含起始位、数据位、可选的校验位和停止位
- 空闲状态约定:没有数据传输时,线路保持高电平
2.2 关键时间参数计算
以9600bps、8N1(8数据位、无校验、1停止位)配置为例:
| 参数 | 计算公式 | 结果 |
|---|---|---|
| 位周期(T_bit) | 1/9600 | 104.17μs |
| 帧时间 | (1起始 + 8数据 + 1停止) × T_bit | 1.042ms |
| 接收采样点 | 起始位下降沿后1.5×T_bit | 156.25μs |
c复制// 计算波特率对应的微秒延时(72MHz系统时钟示例)
#define BAUDRATE 9600
#define BIT_TIME_US (1000000/BAUDRATE) // 104us
2.3 发送端实现要点
发送一个字节的流程必须严格遵循以下顺序:
- 起始位:拉低电平并保持104.17μs
- 数据位(LSB优先):
- 从bit0到bit7依次发送
- 每个位保持精确的104.17μs电平
- 停止位:拉高电平并保持至少104.17μs
关键细节:所有延时必须使用硬件定时器实现,简单的for循环延时会因为中断干扰导致累积误差。
3. 硬件与工程配置实战
3.1 开发环境搭建
极海APM32系列需要使用专用SDK,与STM32的HAL库不兼容。具体步骤:
- 从极海官网下载:
- APM32F10x_SDK_V1.0
- Keil器件支持包
- 基于官方例程
USART_Interrupt创建新工程 - 文件结构调整:
bash复制
Project/ ├── CMSIS/ ├── APM32F10x_StdPeriphDriver/ ├── User/ │ ├── main.c │ ├── soft_uart.c │ └── ... └── MDK/ └── Project.uvprojx
3.2 引脚配置代码
选择PD13(TX)、PD14(RX)作为模拟串口引脚:
c复制// GPIO初始化配置
void SOFT_UART_GPIO_Init(void)
{
GPIO_Config_T config;
// TX引脚(PD13)推挽输出
config.mode = GPIO_MODE_OUT_PP;
config.speed = GPIO_SPEED_50MHz;
APM_GPIO_Config(GPIOD, GPIO_PIN_13, &config);
APM_GPIO_SetBit(GPIOD, GPIO_PIN_13); // 初始高电平
// RX引脚(PD14)浮空输入
config.mode = GPIO_MODE_IN_FLOATING;
APM_GPIO_Config(GPIOD, GPIO_PIN_14, &config);
// 配置RX引脚的外部中断
EXTI_Config_T extiConfig;
extiConfig.line = EXTI_LINE_14;
extiConfig.mode = EXTI_MODE_INTERRUPT;
extiConfig.trigger = EXTI_TRIGGER_FALLING;
extiConfig.lineCmd = ENABLE;
APM_EXTI_Config(&extiConfig);
// 配置NVIC
NVIC_EnableIRQ(EXTI15_10_IRQn);
}
4. 核心功能实现详解
4.1 发送功能实现
发送功能需要考虑不同数据类型的处理:
c复制// 发送单个字节
void SoftUART_SendByte(uint8_t data)
{
__disable_irq(); // 关闭中断保证时序
// 起始位
APM_GPIO_ResetBit(GPIOD, GPIO_PIN_13);
Delay_US(BIT_TIME_US);
// 数据位(LSB first)
for(int i=0; i<8; i++) {
if(data & 0x01)
APM_GPIO_SetBit(GPIOD, GPIO_PIN_13);
else
APM_GPIO_ResetBit(GPIOD, GPIO_PIN_13);
data >>= 1;
Delay_US(BIT_TIME_US);
}
// 停止位
APM_GPIO_SetBit(GPIOD, GPIO_PIN_13);
Delay_US(BIT_TIME_US);
__enable_irq();
}
// 发送字符串(带长度检查)
void SoftUART_SendString(char *str, uint32_t max_len)
{
while(*str && max_len--) {
SoftUART_SendByte(*str++);
}
}
4.2 接收功能实现
接收端采用"起始位中断+定时器采样"的方案:
-
硬件中断检测起始位:
c复制void EXTI15_10_IRQHandler(void) { if(APM_EXTI_ReadStatus(EXTI_LINE_14)) { APM_EXTI_ClearStatus(EXTI_LINE_14); // 启动定时器在1.5T后采样 TIM_ConfigBase(TIM5, 1.5*BIT_TIME_US); TIM_Enable(TIM5); } } -
定时器中断采样数据:
c复制void TIM5_IRQHandler(void) { static uint8_t bit_count = 0; static uint8_t rx_data = 0; if(TIM_ReadStatus(TIM5, TIM_STATUS_UPDATE)) { TIM_ClearStatus(TIM5, TIM_STATUS_UPDATE); if(bit_count == 0) { // 起始位验证 if(APM_GPIO_ReadBit(GPIOD, GPIO_PIN_14) == 0) { bit_count++; } } else if(bit_count <= 8) { // 数据位采样 rx_data >>= 1; if(APM_GPIO_ReadBit(GPIOD, GPIO_PIN_14)) { rx_data |= 0x80; } bit_count++; } else { // 停止位检测 if(APM_GPIO_ReadBit(GPIOD, GPIO_PIN_14)) { g_rx_buffer = rx_data; // 存入接收缓冲区 g_rx_flag = 1; // 置位接收完成标志 } bit_count = 0; TIM_Disable(TIM5); } } }
5. 性能优化与问题排查
5.1 时序精度提升方案
实测发现简单的Delay_US函数在72MHz主频下会有约±2μs的误差,改进方案:
-
使用硬件定时器:
c复制void Delay_US(uint16_t us) { TIM_SetCounter(TIM6, 0); TIM_Enable(TIM6); while(TIM_ReadCounter(TIM6) < us); TIM_Disable(TIM6); } -
校准方法:
- 用逻辑分析仪测量实际位宽
- 调整定时器预分频值补偿误差
- 公式:
实际周期 = (ARR+1)*(PSC+1)/CLK
5.2 常见问题与解决
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 接收数据错位 | 波特率不匹配 | 检查系统时钟和定时器配置 |
| 只能接收部分字节 | 中断优先级冲突 | 调整TIM和EXTI中断优先级 |
| 发送数据丢失 | 未关闭全局中断 | 在关键时序段使用__disable_irq() |
| 逻辑分析仪显示毛刺 | GPIO速度过高 | 降低GPIO输出速度到10MHz |
6. 实测效果与性能分析
通过逻辑分析仪捕获的0x55发送波形(二进制01010101):
code复制起始位 | D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | 停止位
低 | 高 | 低 | 高 | 低 | 高 | 低 | 高 | 低 | 高
实测参数:
- 位宽度:104.2μs ±0.8μs
- 帧间隔:1.042ms
- 最大稳定波特率:115200bps(误差<3%)
资源占用对比:
| 方案 | ROM占用 | RAM占用 | CPU负载 |
|---|---|---|---|
| 硬件UART | 1.2KB | 128B | <1% |
| 本方案 | 3.8KB | 256B | ~15% |
实际项目中建议:仅在硬件资源紧张时使用,高波特率(>57600)下建议增加硬件流控。
