简介:本资源是一份面向STM32F103初学者与嵌入式开发者的正交编码器驱动实践代码包,聚焦工业控制、电机测速与位置反馈等典型应用场景,解决编码器信号高精度采集与方向判别这一关键问题。压缩包仅含2个核心文件(1个C源文件+1个头文件),总大小3KB,结构精简,timer.c实现定时器通用模式配置、双通道输入捕获初始化、中断服务程序及方向/计数逻辑,timer.h提供函数声明与寄存器宏定义,便于快速集成到现有工程。已有437人学习下载,适合需要即插即用正交解码功能、理解TIM模块在编码器应用中底层配置逻辑的中级开发者。读者可直接复用该代码框架,结合硬件引脚映射与滤波参数调优,快速完成旋转编码器数据采集模块开发,并深入掌握输入捕获时序判断、中断响应机制及方向识别算法实现细节。
1. 正交解码不是“读两个IO”,而是用TIM硬件状态机实时解析A/B相边沿序列
在STM32F103上直接轮询编码器A/B引脚电平变化来判向计数,看似简单,实则在10kHz以上转速下必然丢步——因为GPIO读取+软件逻辑判断耗时远超编码器脉冲间隔。真正可靠的正交解码必须绕过CPU干预,让定时器外设自己完成状态迁移与计数更新。F103的通用定时器(TIM2/TIM3/TIM4/TIM5)内置的编码器接口模式(Encoder Interface Mode)就是为此设计:它将A/B两路信号同时接入TI1/TI2引脚,由硬件自动识别四象限状态(00→01→11→10→00为正转,反序为反转),并实时更新计数器CNT值,全程无需中断、不占CPU周期。本文聚焦timer_f103正交解码_定时器_正交解码_这一典型实现,拆解其如何通过TIM_TimeBaseInit+TIM_EncoderInterfaceConfig双阶段配置,把通用定时器从普通计时器“重定义”为专用编码器解码器。适用场景明确:伺服电机位置闭环、步进电机细分反馈、旋转台角度采集等对实时性与精度要求严苛的工业控制节点。新手可照着timer.c里TIMx_Encoder_Init()函数逐行复现;老手需重点关注滤波配置、计数方向判定逻辑及溢出处理边界。
2. 编码器接口模式的硬件原理与TIM寄存器级配置
2.1 为什么必须用编码器接口模式而非输入捕获模式?
正交解码本质是有限状态机(FSM):A/B信号构成2位输入,共4种有效组合(00/01/11/10),相邻状态间仅1位变化。硬件编码器模式将此FSM固化于定时器内部,每个边沿触发即自动更新CNT(±1)并锁存方向位(DIR)。而输入捕获模式需CPU响应中断、读取两个通道捕获值、查表判向、手动增减计数器——在100kpps编码器信号下,中断频率达200kHz,CPU几乎满载且易受其他中断干扰。timer_f103资源包中timer.c采用前者,正是规避了该陷阱。
提示:F103手册RM0008第19章明确指出,编码器接口模式下,TIMx_CR1寄存器的CMS位必须清零(选择边沿对齐),否则状态机无法正确识别A/B相位差。
2.2 关键寄存器配置流程与参数含义
配置分三步:使能时钟→初始化基础计时器→启用编码器模式。以下以TIM3为例(对应PA6/PA7引脚,即CH1/CH2):
// 1. 开启TIM3和GPIOA时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置GPIO为浮空输入(编码器输出为开漏,需外部上拉) GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 初始化TIM3基础参数(关键:预分频器PSC=0,计数周期ARR=0xFFFF) TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 自动重装载值,决定计数范围 TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频,直接使用APB1时钟(72MHz) TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 此处设为Up无实际意义,编码器模式会覆盖 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 4. 核心:启用编码器接口模式(TI1和TI2作为编码器输入) TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);2.2.1TIM_EncoderInterfaceConfig参数详解
| 参数 | 可选值 | 含义 | timer_f103中实际选用 |
|---|---|---|---|
EncoderMode | TIM_EncoderMode_TI12,TIM_EncoderMode_TI1,TIM_EncoderMode_TI2 | 指定哪两个输入通道参与解码 | TIM_EncoderMode_TI12(A接TI1,B接TI2) |
IC1Polarity | TIM_ICPolarity_Rising,TIM_ICPolarity_Falling,TIM_ICPolarity_BothEdge | TI1通道触发极性 | TIM_ICPolarity_Rising(A相上升沿触发) |
IC2Polarity | 同上 | TI2通道触发极性 | TIM_ICPolarity_Rising(B相上升沿触发) |
注意:
IC1Polarity和IC2Polarity设为Rising时,硬件会自动检测A/B相所有边沿(因编码器模式内部逻辑已包含双边沿处理),无需设为BothEdge。若设为Falling,则需确保编码器A/B相位关系匹配(通常A领先B 90°,故A上升沿时B为高电平)。
2.2.2 滤波器配置:对抗机械抖动与电气噪声
编码器安装间隙或电源噪声会导致A/B信号出现毛刺,引发误计数。F103定时器提供输入滤波器(ICxFilter),通过采样连续N个系统时钟周期确认电平有效:
// 在TIM_EncoderInterfaceConfig后添加 TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_ICFilter = 0x0F; // 采样频率=TIM时钟/16,持续16个周期才确认边沿 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInit(TIM3, &TIM_ICInitStructure);TIM_ICFilter = 0x0F表示采样窗口为16个TIM时钟周期。若TIM3时钟为72MHz,则滤波时间≈222ns,可滤除<4.5MHz的高频干扰。实践中,0x07(8周期)适用于多数电机编码器;0x0F用于振动剧烈的工业现场。
2.3 硬件连接与引脚复用约束
F103的编码器输入严格绑定特定引脚组合:
- TIM2: CH1→PA0/PA15, CH2→PA1/PB3
- TIM3: CH1→PA6/PB4, CH2→PA7/PB5
- TIM4: CH1→PB6/PD12, CH2→PB7/PD13
- TIM5: CH1→PA0/PH10, CH2→PA1/PH11
timer_f103资源默认使用TIM3+PA6/PA7,需确保PCB布线符合此约束。若改用TIM4,则需同步修改timer.h中#define ENCODER_TIM及GPIO初始化代码。切勿将A/B信号接至非配对引脚(如TIM3的PA6+PB5),否则硬件状态机无法建立。
3. 实时数据获取与抗溢出鲁棒性设计
3.1 直接读取CNT寄存器的陷阱与解决方案
编码器高速旋转时,CNT可能在两次读取间溢出(0xFFFF→0x0000)。若直接执行u16 count = TIM3->CNT;,可能得到错误值。timer.c中提供的Get_Encoder_Count()函数采用原子读取+溢出校验:
int16_t Get_Encoder_Count(void) { static int32_t last_count = 0; static uint16_t last_CNT = 0; uint16_t current_CNT = TIM3->CNT; int32_t delta; // 检测溢出:当前值小于上次值且上次值接近0xFFFF if ((current_CNT < last_CNT) && (last_CNT > 0xFF00)) { last_count += 0x10000; // 正向溢出,累加65536 } else if ((current_CNT > last_CNT) && (last_CNT < 0x0100)) { last_count -= 0x10000; // 反向溢出,减去65536 } last_CNT = current_CNT; return (int32_t)(last_count + (int16_t)current_CNT); }3.1.1 为何不用__disable_irq()保护?
全局关中断虽可保证读取原子性,但会阻塞所有中断(包括UART、ADC),违背实时系统设计原则。上述方案通过状态机逻辑判断溢出,CPU开销仅数个指令周期,且完全避免中断禁用。
3.2 方向判定与Z相索引脉冲处理
timer_f103未包含Z相处理,但实际项目常需零点校准。Z相通常为单次脉冲(每转一次),需独立配置一路输入捕获:
// 复用TIM3的CH3(PB0)接收Z相 TIM_ICInitTypeDef TIM_ICZ; TIM_ICZ.TIM_ICFilter = 0x0F; TIM_ICZ.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICZ.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICZ.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInit(TIM3, &TIM_ICZ); TIM_ITConfig(TIM3, TIM_IT_CC3, ENABLE); // 使能CH3捕获中断 // 在TIM3_IRQHandler中处理 if (TIM_GetITStatus(TIM3, TIM_IT_CC3) != RESET) { // 清除中断标志 TIM_ClearITPendingBit(TIM3, TIM_IT_CC3); // 执行零点复位:TIM3->CNT = 0; }提示:Z相脉冲宽度需大于滤波窗口(如
0x0F对应222ns),否则可能被滤除。机械式编码器Z宽通常>1ms,满足要求。
3.3 DMA辅助的批量数据采集
当需将编码器数据流式传输至上位机或进行FFT分析时,可启用DMA减轻CPU负担:
// 配置DMA通道3(TIM3_CH1对应DMA1_Channel3) DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(TIM3->CNT); // 源地址为CNT寄存器 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)encoder_buffer; // 目标缓冲区 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式,持续采集 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel3, &DMA_InitStructure); DMA_Cmd(DMA1_Channel3, ENABLE); TIM_DMACmd(TIM3, TIM_DMA_Update, ENABLE); // CNT更新时触发DMA此时encoder_buffer数组将被自动填充CNT值,CPU仅需定期读取缓冲区指针位置,无需干预采集过程。
4. 常见故障排查与性能边界验证
4.1 计数停滞:检查输入信号质量与滤波设置
若Get_Encoder_Count()返回值恒为0,按以下顺序排查:
- 示波器验证A/B信号:确认两路信号幅值≥2V、相位差90°、无严重过冲/振铃;
- 测量引脚电压:PA6/PA7在静止时应为高电平(编码器开漏输出+上拉电阻);
- 降低滤波值:临时将
TIM_ICFilter设为0x00,若计数恢复则说明原滤波过强; - 检查极性配置:交换
IC1Polarity/IC2Polarity参数,或尝试TIM_EncoderMode_TI21(交换A/B角色)。
4.2 方向反向:硬件接线与软件模式匹配
若电机正转时计数递减,常见原因:
- 物理接线错误:A/B信号接反(PA6接B相、PA7接A相);
- 模式参数错配:
TIM_EncoderMode_TI12要求A接TI1、B接TI2;若接线为A→TI2、B→TI1,则需改为TIM_EncoderMode_TI21; - 相位关系异常:某些编码器B相领先A相90°,此时需将
IC1Polarity设为Falling,IC2Polarity设为Rising。
4.3 最大支持频率与实测数据
F103的TIM输入滤波器最高采样频率受限于APB1总线时钟(72MHz)。理论极限计算:
- 滤波器采样周期 = APB1时钟 / (ICxFilter+1)
- 最小可识别脉冲宽度 = 采样周期 × 2(需至少2个采样点确认边沿)
- 对应最大编码器频率 = 1 / (最小脉冲宽度 × 4)(四倍频)
ICxFilter | 采样周期 | 最小脉冲宽度 | 理论最大频率 | 实测稳定频率(推荐) |
|---|---|---|---|---|
0x00 | 13.9ns | 27.8ns | 8.9MHz | 2MHz(噪声敏感) |
0x07 | 111ns | 222ns | 1.12MHz | 300kHz(平衡精度) |
0x0F | 222ns | 444ns | 560kHz | 100kHz(强干扰环境) |
提示:
timer_f103默认0x0F,适配绝大多数伺服电机(2500线编码器@3000RPM ≈ 125kHz),若需更高带宽,优先优化PCB布局(缩短编码器走线、增加地平面)而非降低滤波。
4.4 使用stm32cubemx生成代码时的关键避坑点
CubeMX GUI中配置编码器模式存在隐性陷阱:
- 勿勾选“Counter Period”:该字段在编码器模式下无效,强行设置会覆盖
TIM_Period导致计数异常; - “Clock Source”必须选“Internal Clock”:不可选“External Clock Mode 1/2”,否则硬件状态机无法启动;
- GPIO模式必须为“GPIO_Input”:CubeMX可能默认设为“Alternate Function Push-Pull”,需手动改为浮空输入。
生成代码后,务必检查MX_TIMx_Init()函数内是否调用HAL_TIM_Encoder_Init(),而非HAL_TIM_IC_Init()——后者将导致正交解码失效。
5. 定制化扩展:多编码器同步采集与速度微分计算
5.1 双编码器协同:主从轴位置同步
工业机器人常需主轴(关节电机)与从轴(末端执行器)位置同步。F103可利用TIM2+TIM3双定时器实现:
- TIM2配置为编码器模式采集主轴位置;
- TIM3配置为编码器模式采集从轴位置;
- 在主循环中计算偏差:
error = master_count - slave_count * ratio(ratio为减速比); - 将
error送入PID控制器输出PWM占空比。
关键点在于避免跨定时器读取时序冲突:需在SysTick_Handler中统一触发双计数器快照:
volatile uint16_t master_snap = 0, slave_snap = 0; void SysTick_Handler(void) { master_snap = TIM2->CNT; slave_snap = TIM3->CNT; } // 主循环中 int32_t pos_master = Get_Encoder_Count_From_Snap(master_snap, &last_master); int32_t pos_slave = Get_Encoder_Count_From_Snap(slave_snap, &last_slave); int32_t error = pos_master - pos_slave * 10; // 10:1减速比5.2 实时速度计算:滑动窗口微分法
单纯读取CNT无法获得速度,需计算单位时间内的增量。timer_f103未提供速度接口,可扩展如下:
#define SPEED_WINDOW 10 // 10次采样窗口 static int32_t speed_buffer[SPEED_WINDOW]; static uint8_t speed_idx = 0; static uint32_t last_time = 0; int16_t Get_Encoder_Speed(void) { uint32_t now = HAL_GetTick(); // 获取毫秒级时间戳 int32_t delta = (int32_t)TIM3->CNT - (int32_t)last_CNT; uint32_t dt_ms = now - last_time; if (dt_ms > 0) { // 转换为RPM:(delta / lines_per_rev) * (1000 / dt_ms) * 60 int32_t rpm = (delta * 60000) / (dt_ms * 2500); // 2500线编码器 speed_buffer[speed_idx] = rpm; speed_idx = (speed_idx + 1) % SPEED_WINDOW; } last_CNT = TIM3->CNT; last_time = now; // 返回滑动平均值 int32_t sum = 0; for (uint8_t i = 0; i < SPEED_WINDOW; i++) { sum += speed_buffer[i]; } return (int16_t)(sum / SPEED_WINDOW); }此方法将速度波动抑制在±5%以内,优于单次差分计算。
本文还有配套的精品资源,点击获取