1. 项目概述
在嵌入式开发中,GPIO模拟通讯是一种常见的低成本数据传输方案。相比UART、SPI等标准外设,GPIO模拟通讯具有以下优势:
- 硬件资源占用少,仅需普通IO口
- 协议完全自定义,灵活性高
- 适用于低速短距离数据传输场景
本文将基于STM32F103C8T6(Blue Pill开发板)演示如何通过GPIO实现自定义协议的半双工通讯。核心思路是通过定时器精确控制高低电平持续时间来编码数据,利用外部中断捕获信号边沿实现数据接收。
2. 硬件配置与初始化
2.1 CubeMX基础配置
使用STM32CubeMX进行硬件初始化配置:
- 创建新工程,选择STM32F103C8系列
- 系统时钟配置为72MHz(外部8MHz晶振通过PLL倍频)
- SYS设置中Debug选择Serial Wire(SWD)
- GPIO配置:
- PA0:推挽输出模式,无上下拉,高速模式
- PA1:输入模式,上拉电阻,双边沿触发中断
注意:GPIO速度设置会影响信号边沿陡峭程度。对于1MHz以下的低速通讯,选择"低速"即可降低功耗;若需更高频率,则应选择"高速"模式。
2.2 定时器配置
需要配置两个定时器:
- TIM2:用于发送时的us级延时
- TIM3:用于接收时的时间戳记录
具体参数:
- 时钟源:内部时钟
- Prescaler:71(72MHz/(71+1)=1MHz)
- Counter Period:65535(16位最大值)
- 自动重载:Enable
c复制// 定时器初始化代码示例(CubeMX自动生成)
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 65535;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
2.3 中断配置
在NVIC中启用EXTI1中断:
- EXTI Line1 interrupt
- 抢占优先级:2
- 子优先级:0
经验:中断优先级不宜设置过高,避免影响其他关键中断(如系统定时器)。若系统中有多个中断,需合理规划优先级。
3. 通讯协议设计
3.1 数据包格式
采用帧结构传输,每包包含:
- 帧头:0xAA(1字节)
- 数据段:高8位 + 低8位(2字节)
- 校验和:帧头+数据段和的低8位(1字节)
- 帧尾:0x55(1字节)
c复制// 协议宏定义
#define PACKET_HEAD 0xAA
#define PACKET_TAIL 0x55
#define PACKET_LEN 5
#define BITS_PER_BYTE 8
3.2 时序参数定义
通过不同高电平时长区分信号类型:
- 同步信号:2000us高电平
- 数据1:1000us高电平
- 数据0:500us高电平
- 位间隔:300us低电平
- 容差:±100us
c复制#define SYNC_HIGH_US 2000
#define BIT1_HIGH_US 1000
#define BIT0_HIGH_US 500
#define BIT_LOW_US 300
#define TOLERANCE_US 100
4. 发送端实现
4.1 基础延时函数
利用TIM2实现us级延时:
c复制void delay_us(uint32_t us) {
__HAL_TIM_SET_COUNTER(&htim2, 0);
__HAL_TIM_ENABLE(&htim2);
while(__HAL_TIM_GET_COUNTER(&htim2) < us);
__HAL_TIM_DISABLE(&htim2);
}
注意:此延时方式会阻塞CPU,实际项目中建议使用DMA或中断实现非阻塞延时。
4.2 数据发送流程
- 发送同步信号(2000us高电平)
- 依次发送帧头、数据高字节、数据低字节、校验和、帧尾
- 每个字节从高位开始发送
- 位与位之间插入300us低电平间隔
c复制void GPIO_SendPacket(uint16_t data) {
// 拆分数据
uint8_t data_h = (data >> 8) & 0xFF;
uint8_t data_l = data & 0xFF;
// 发送同步头
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
delay_us(SYNC_HIGH_US);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
delay_us(BIT_LOW_US);
// 发送数据包
send_byte(PACKET_HEAD);
send_byte(data_h);
send_byte(data_l);
send_byte(calculate_checksum(data_h, data_l));
send_byte(PACKET_TAIL);
}
5. 接收端实现
5.1 中断回调处理
在EXTI中断回调中解析信号:
c复制void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if(GPIO_Pin != GPIO_PIN_1) return;
GPIO_PinState level = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1);
uint32_t current_time = GetMicros();
uint32_t duration = current_time - last_edge_time;
if(level == GPIO_PIN_RESET) { // 下降沿
if(duration > SYNC_HIGH_US - TOLERANCE_US) {
// 检测到同步头,重置接收状态
rx_state = RX_STATE_HEAD;
rx_buffer_index = 0;
rx_bit_count = 0;
} else {
// 解析数据位
decode_bit(duration);
}
}
last_edge_time = current_time;
}
5.2 数据位解码
根据高电平时长判断数据位:
c复制void decode_bit(uint32_t duration) {
uint8_t bit = 0;
if(abs(duration - BIT1_HIGH_US) <= TOLERANCE_US) {
bit = 1;
} else if(abs(duration - BIT0_HIGH_US) <= TOLERANCE_US) {
bit = 0;
} else {
// 无效时序,丢弃当前帧
rx_state = RX_STATE_IDLE;
return;
}
// 存储到位缓冲区
bit_buffer = (bit_buffer << 1) | bit;
rx_bit_count++;
if(rx_bit_count >= BITS_PER_BYTE) {
rx_buffer[rx_buffer_index++] = bit_buffer;
rx_bit_count = 0;
bit_buffer = 0;
if(rx_buffer_index >= PACKET_LEN) {
// 完整帧接收完成
process_packet();
}
}
}
6. 抗干扰优化
6.1 硬件措施
- 增加RC滤波(如100Ω电阻串联+100nF电容对地)
- 缩短传输距离(建议<20cm)
- 使用双绞线降低干扰
6.2 软件措施
- 三次采样法:对每个电平变化进行多次采样确认
- 动态调整容差:根据信号质量自动调整TOLERANCE_US
- 前导码检测:增加16位前导码0xAAAA提高同步可靠性
c复制// 改进的同步头检测
#define PREAMBLE_LEN 2
uint8_t preamble[PREAMBLE_LEN] = {0xAA, 0xAA};
int check_preamble() {
for(int i=0; i<PREAMBLE_LEN; i++) {
if(rx_buffer[i] != preamble[i]) {
return 0;
}
}
return 1;
}
7. 性能测试与优化
7.1 传输速率测试
- 单字节传输时间:(500/1000 + 300) * 8 = 6.4ms/7.2ms
- 5字节数据包:约35ms
- 有效速率:约1.4kbps
提示:可通过缩短位间隔时间提升速率,但需考虑GPIO翻转速度限制。
7.2 稳定性测试方法
- 连续发送1000个随机数据包
- 统计误码率和丢包率
- 在不同环境温度下测试(-20℃~60℃)
c复制// 测试代码示例
void test_loop() {
uint16_t test_data = 0;
while(1) {
GPIO_SendPacket(test_data);
HAL_Delay(50);
test_data = (test_data + 1) % 65536;
}
}
8. 扩展应用
8.1 多节点组网
通过片选信号实现1对多通讯:
- 主设备控制CS线选通从设备
- 每个从设备有唯一ID
- 协议中增加地址字段
8.2 数据加密
增加简单的异或加密:
c复制uint8_t encrypt_byte(uint8_t data) {
return data ^ 0x5A;
}
8.3 与上位机通信
通过USB转TTL模块连接PC:
- 在STM32端实现USB CDC协议
- 使用Python编写测试脚本
- 添加数据包解析显示功能
python复制# Python接收示例
import serial
ser = serial.Serial('COM3', 115200)
while True:
data = ser.read(5)
if data[0] == 0xAA and data[4] == 0x55:
value = (data[1] << 8) | data[2]
print(f"Received: {value}")
在实际项目中,GPIO模拟通讯虽然简单,但需要注意电平转换、信号隔离等实际问题。我曾在一个工业传感器项目中采用类似方案,通过增加光电隔离器件,通讯距离成功延长到3米且稳定运行。关键是要根据具体应用场景调整时序参数和抗干扰措施。