1. 项目背景与核心思路
在嵌入式硬件开发中,编码器测速是一个常见需求。但实际开发时,我们经常会遇到手头没有实物编码器的情况。这时候,用GPIO模拟编码器信号就成了一个非常实用的调试手段。我最近在做一个电机控制项目时就遇到了这个问题,经过反复实验,总结出一套可靠的模拟方案。
这个方案的核心在于:利用STM32的两个GPIO引脚,按照编码器A/B相的时序规律翻转电平,模拟出真实的旋转编码器信号。然后通过定时器的编码器模式接口接收这些信号,实现方向判断和速度计算。整个过程不需要任何外部硬件,只需要一块STM32开发板就能完成所有测试。
提示:这种模拟方法特别适合在项目前期验证算法逻辑,或者在教学演示中展示编码器工作原理。但要注意,模拟信号的精度和稳定性无法与真实编码器相比,最终产品还是需要接入实际编码器。
2. 硬件设计与原理分析
2.1 编码器信号特性解析
工业编码器通常输出两路正交信号(A相和B相),它们的相位差可以反映旋转方向:
- 正转时:A相上升沿对应B相低电平(相位差90°)
- 反转时:A相上升沿对应B相高电平(相位差-90°)
通过检测这两个信号的边沿变化,我们可以确定旋转方向和脉冲数量。STM32的定时器编码器模式正是基于这个原理设计的。
2.2 定时器编码器模式工作原理
STM32的定时器在编码器模式下,会将两个输入通道(TI1和TI2)作为A/B相信号输入。内部通过边沿检测和逻辑判断,自动完成以下功能:
- 方向判断:根据两路信号的相位关系确定正反转
- 计数累加:每个有效边沿触发计数器增减
- 溢出处理:16位计数器溢出时产生中断
这种硬件级的编码器接口大大减轻了CPU负担,让测速更加精准高效。
2.3 模拟信号生成方案设计
要实现可靠的模拟,需要解决三个关键问题:
- 时序控制:A/B相信号必须严格遵循90°相位差
- 方向模拟:正反转时的信号顺序要正确对应
- 速度调节:脉冲频率要能反映不同转速
我的解决方案是使用三个定时器分工协作:
- TIM4:1ms定时,用于生成基础脉冲
- TIM3:编码器接口模式,接收模拟信号
- TIM2:100ms定时,用于速度计算
3. STM32CubeMX详细配置
3.1 时钟树配置要点
时钟是定时器精度的基础,必须正确配置:
- 选择HSE作为时钟源(通常接8MHz晶振)
- 配置PLL将主频提升到72MHz(STM32F103最大值)
- 确保APB1定时器时钟为72MHz(不要分频)
具体参数:
- HCLK = 72MHz
- PCLK1 = 36MHz
- PCLK2 = 72MHz
- APB1定时器时钟 = 72MHz(因为有×2倍频)
3.2 定时器关键参数计算
3.2.1 TIM4(脉冲生成定时器)
- 目标:每1ms产生一次中断
- 计算公式:ARR = (定时周期×时钟频率)/(PSC+1) -1
- 参数:
- PSC = 71 → 分频后时钟=1MHz
- ARR = 999 → (999+1)/1MHz = 1ms
3.2.2 TIM3(编码器接口定时器)
- 模式:Encoder Mode TI1 and TI2
- 参数:
- PSC = 0(不分频,保证计数精度)
- ARR = 65535(16位最大值,减少溢出)
3.2.3 TIM2(速度计算定时器)
- 目标:每100ms计算一次速度
- 参数:
- PSC = 7199 → 分频后时钟=10kHz
- ARR = 999 → (999+1)/10kHz = 100ms
3.3 GPIO配置注意事项
- 将PB0和PB1配置为推挽输出模式
- 确保TIM3的编码器输入引脚(PA6/PA7)配置为浮空输入
- 为方便调试,建议将USART1配置为异步模式(115200bps)
4. 代码实现与逻辑解析
4.1 模拟信号生成算法
c复制// 正转时序:{1,0}→{1,1}→{0,1}→{0,0}
const uint8_t forward_seq[4][2] = {{1,0}, {1,1}, {0,1}, {0,0}};
// TIM4中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM4) {
// 按正转时序翻转GPIO
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, forward_seq[encoder_state][0]);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, forward_seq[encoder_state][1]);
encoder_state = (encoder_state + 1) % 4;
}
}
4.2 方向判断与速度计算
c复制// TIM2中断回调函数(100ms定时)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM2) {
uint16_t cnt = TIM3->CNT; // 获取当前计数值
int total = 0;
// 计算总脉冲数(考虑溢出)
if(n >= 0) {
total = cnt + n * 65536; // 正转
} else {
total = (65536 - cnt) - (n + 1) * 65536; // 反转
}
// 计算转速(脉冲/秒)
double speed = total / 0.1;
printf("Speed: %.1f pulses/s, Direction: %s\n",
speed, n>=0 ? "Forward" : "Reverse");
// 重置计数器
TIM3->CNT = 0;
n = 0;
}
}
4.3 中断优先级配置
由于涉及多个定时器中断,必须合理设置优先级:
- TIM4(脉冲生成):最高优先级(PreemptionPriority=0)
- TIM3(编码器计数):中优先级(PreemptionPriority=1)
- TIM2(速度计算):最低优先级(PreemptionPriority=2)
这样可以确保脉冲生成的时序精确性不受其他中断影响。
5. 实测效果与问题排查
5.1 典型测试数据
| 模拟状态 | 理论速度(pulses/s) | 实测速度(pulses/s) | 误差率 |
|---|---|---|---|
| 正转低速 | 250 | 248 | 0.8% |
| 正转高速 | 1000 | 992 | 0.8% |
| 反转低速 | 250 | 246 | 1.6% |
| 反转高速 | 1000 | 987 | 1.3% |
5.2 常见问题与解决方案
问题1:计数方向与实际不符
现象:正转时计数减少,反转时计数增加
原因:A/B相引脚接反
解决:交换PA6和PA7的连接,或在代码中修改Encoder Mode为反向模式
问题2:速度计算波动大
现象:连续测量的速度值差异较大
原因:定时器中断被其他高优先级任务阻塞
解决:
- 提高TIM2中断优先级
- 减少中断服务程序中的耗时操作
- 适当增大计算周期(如从100ms改为200ms)
问题3:高速时计数丢失
现象:转速越高,测量值偏差越大
原因:GPIO翻转速度跟不上
解决:
- 改用寄存器直接操作GPIO(替代HAL库)
- 降低模拟信号的最高频率
- 使用更高主频的STM32型号
6. 进阶优化建议
6.1 提高测量精度
- 使用DMA传输计数:通过DMA将定时器计数值自动传输到内存,避免中断延迟
- 动态调整采样周期:高速时缩短计算间隔,低速时延长间隔
- 添加数字滤波:对连续多次测量结果进行滑动平均处理
6.2 扩展功能实现
- 位置闭环控制:将测速结果作为反馈,实现PID速度控制
- 多编码器支持:利用STM32多个定时器同时监测多路编码器
- 无线传输:通过蓝牙或WiFi将速度数据发送到上位机
6.3 性能优化技巧
- 使用LL库替代HAL库:减少函数调用开销,提高GPIO翻转速度
- 关闭调试接口:在最终产品中禁用SWD接口,释放GPIO资源
- 优化电源管理:根据实际需求调整CPU主频,降低功耗
在实际项目中,我发现这种模拟方法虽然简单,但有几个关键点需要注意:一是GPIO翻转速度要足够快,否则高速时会丢失脉冲;二是中断优先级要合理配置,避免速度计算影响脉冲生成的时序精度;三是测量周期不宜过短,否则会引入较大计算误差。经过多次调整参数,最终我实现的模拟编码器在0-2000 pulses/s范围内误差可以控制在2%以内,完全满足前期开发和教学演示的需求。