☰
STM32F103编码器程序实战:正交解码、定时器配置与调试避坑
2026/10/15 17:36:32 网站建设 项目流程

简介:面向嵌入式开发者的STM32F103编码器驱动资源,提供基于定时器输入捕获与编码器模式的完整工程,解决增量型编码器位置、方向与速度读取等常见控制需求。资源共934个文件,以C源文件、H头文件、启动汇编脚本及工程配置文件为主,另含链接脚本、文本说明等,压缩包约9.99MB,整体结构非常适合直接导入开发环境。目前已有2330人学习下载,适合刚接触编码器接口的STM32初学者,也可供电机控制、机器人定位等场景开发者参考。内容涉及TIM2/TIM3/TIM4定时器编码器模式初始化、A/B相脉冲差分解算、中断服务函数写法,以及信号滤波、噪声处理等工程细节,能够帮助读者快速掌握编码器硬件机制,并完成从配置到调试的完整流程,同时便于二次开发与学习研究。

1. stm32f103编码器程序到底在解决什么问题:从AB相脉冲到可靠的转速读数

做电机闭环控制时,stm32f103编码器程序往往是第一个卡住人的环节。很多从业者第一次接触编码器时,以为只要把A相、B相两根线接到定时器引脚上就能读到转速,实际调起来才发现,正反转跳变、计数溢出、上电随机值、干扰导致的毛刺,任何一个问题都足以让速度环崩溃。这中间真正需要做的,是把编码器的正交脉冲转换成带方向的计数、换算成物理转速,并用滤波和归零逻辑把这些数据变成控制环敢用的量。

编码器程序的核心价值就是把这个黑匣子打开:硬件上利用F103定时器的编码器接口做正交解码,软件上通过周期采样把计数值变成RPM,再针对实际工况处理反跳、溢出和滤波。它适合所有用增量式编码器做测速和定位的场合,包括直流电机闭环、舵轮底盘、机械臂关节和转台。无论你用HAL库还是标准库,思路是一致的,本篇文章就把这条路完整讲透,从选型到配置,从读数到避坑。

2. 编码器信号与定时器编码器模式:为什么AB相能同时给出方向和倍频

2.1 正交解码原理:A相B相之间的相位差如何变成计数方向

增量式编码器输出的两路方波信号A相和B相,在正常转动时相位差固定为90度。电机正转时A相领先B相90度,反转时B相领先A相90度。简单来说,方向信息就藏在边沿出现的先后顺序里。STM32F103的定时器编码器模式正是利用这一点,在A相和B相的上升沿和下降沿都进行采样,通过对引脚电平状态的组合判断,确定当前是正转还是反转,并据此增减计数器数值。

四倍频是这里最重要的概念。编码器原生的线数代表每圈输出的脉冲个数,例如1000线的编码器每圈A相和B相各输出1000个脉冲。但定时器编码器模式下,计数器会在AB两相每个边沿各触发一次计数,也就是每个脉冲周期内会额外识别出4个边沿变化,最终得到的计数值就是1000乘以4,也就是4000个计数脉冲每圈。用4000这个数值做运算,比直接用线数能获得四倍的位置分辨率。方向判断则由方向控制位自动完成,正转计数器递增、反转递减,不需要软件干预。

2.2 F103定时器编码器模式与寄存器配置要点

F103系列中,能够工作在编码器模式下的定时器不止一个,TIM1、TIM2、TIM3、TIM4、TIM5和TIM8都支持。实际项目里最常使用的是通用定时器TIM2、TIM3、TIM4,原因是它们不占高级定时器资源,且引脚布局更方便。编码器模式下定时器不再作为普通时基使用,CH1和CH2引脚被映射为编码器的A相和B相输入,计数器值直接反映编码器位置。

核心寄存器是TIMx_SMCR(从模式控制寄存器),其中SMS位段设为011即选择编码器模式。具体到编码器模式细分为三种:只在TI1边沿计数、只在TI2边沿计数、在TI1和TI2双边缘计数。工程中最常用的是第三种TIM_ENCODERMODE_TI12,它实现完整的四倍频。另一个关键寄存器是TIMx_CCER,控制输入捕获极性,通常设置为上升沿触发,但需要结合外部电路调整,这点在后面避坑章节会展开。

以下是使用标准库配置F103定时器编码器模式的初始化代码,这段代码可以直接套用到自己的工程中:

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; // 使能定时器3和GPIO端口时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA6、PA7复用为TIM3的CH1、CH2 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 时基配置,计数器周期设到最大 TIM_TimeBaseStructure.TIM_Period = 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 先按普通输入捕获配置CH1和CH2 TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInit(TIM3, &TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInit(TIM3, &TIM_ICInitStructure); // 关键步骤:调用编码器接口配置,设置TI1和TI2双边缘计数 TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); // 计数器清零并启动 TIM_SetCounter(TIM3, 0); TIM_Cmd(TIM3, ENABLE);

这段代码的核心顺序值得注意。GPIO必须配置为输入模式,编码器脉冲来自外部,不需要复用推挽输出。TIM_EncoderInterfaceConfig需要放在TIM_ICInit之后调用,否则内部会重写CCER寄存器,导致之前配置的捕获极性被覆盖。TIM_Period设置成0xFFFF是因为F103定时器计数器是16位,满量程65535正好配合int16_t读取做有符号处理,这个技巧在后面的速度和方向判定中非常关键。

2.3 外部中断计数方式与定时器编码器模式的边界

有一种常见做法是使用外部中断,把A相接EXTI引脚,B相接另一个GPIO,在A相中断里读B相电平判断方向。这种方案在低速、低精度场合可以工作,但存在两个硬伤:一是中断频繁触发会导致CPU占用率过高,1000线编码器四倍频在3000RPM时每秒产生20万次中断,单片机会把大量时间耗在中断上下文切换上;二是两个引脚的中断优先级和响应延迟不一致时,方向判断容易出错。

因此我的建议很直接:只要片上有空闲的通用定时器,就优先使用定时器编码器模式。它不占用CPU,硬件自动完成方向判断和计数,可靠性远高于中断方式。外部中断方案只适合那些定时器资源全部占满、且编码器转速很低(比如手动调节的旋钮)的场景。如果你用的是带Z相(零位信号)的编码器,依然可以把Z相接EXTI做机械零点捕获,这是互补关系,不是竞争关系。

3. 用HAL库配置stm32f103编码器程序:CubeMX参数与初始化代码

3.1 在配置工具中完成定时器和GPIO的设置

使用HAL库开发stm32f103编码器程序,工程创建阶段有几个参数直接影响后续能否正常工作。打开STM32CubeMX,首先要选中可用的定时器,例如TIM3,在Mode栏中将Combined Channels配置为Encoder Mode,此时CH1和CH2会被自动占用。定时器参数里,Prescaler设置为0,Counter Period设置为65535,这个配置可以让16位计数器有最大计数范围,且读取时直接用int16_t强制转换就能得到有符号数值。

输入捕获参数中,Channel1和Channel2的Polarity都设置为Rising Failing,实际对应的是TI1和TI2的双边沿计数。IC Filter建议设为较高值,比如15,因为绝大多数工业场景中存在电机换向火花或电磁干扰造成的毛刺脉冲,数字滤波器可以滤除宽度极窄的杂散信号。这里特别注意,不要在Encoder Mode下手动修改Preload Enable或Input Capture的Prescaler,保持默认即可,这些参数在编码器接口模式下会被从模式控制逻辑屏蔽掉。

GPIO设置页中,確認PA6和PA7(以TIM3为例)被配置为输入模式,这里推荐配置为上拉输入,因为多数编码器输出是开漏结构,外部需要上拉电阻,而STM32内部的上拉电阻在信号质量要求不高的场合可以代替外部电阻。不过如果编码器驱动能力弱且连线较长,外部上拉依然必不可少,后面会提到信号完整性相关的坑。

3.2 HAL库初始化代码与启动逻辑

配置工具生成的代码会自动生成MX_TIM3_Init函数,核心初始化是TIM_Encoder_InitTypeDef结构体的配置。为了便于理解,这里直接列出该函数的关键部分:

TIM_Encoder_InitTypeDef sEncoderConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 0; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 65535; htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; htim3.Init.RepetitionCounter = 0; sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sEncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1; sEncoderConfig.IC1Filter = 15; sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING; sEncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1; sEncoderConfig.IC2Filter = 15; HAL_TIM_Encoder_Init(&htim3, &sEncoderConfig);

初始化和启动是两步。HAL_TIM_Encoder_Init只完成寄存器配置,这时候定时器还没有开始计数,需要调用启动函数并且传入两个通道标志,表明同时启用CH1和CH2的输入捕获。如果你只启动了一个通道,另一个通道的电平变化不会被处理。更隐蔽的问题是,如果在启动前忘记调用__HAL_TIM_ENABLE对应的底层实现,计数器会一直停在0。

HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);

这段启动代码要把TIM_CHANNEL_ALL作为第二个参数传入。从底层实现来看,它实际是在设置TIM_CR1的CEN位以及CCER中两个通道的使能位。若在调试中遇到计数器不递增,先用调试器查看htim3.Instance->CR1寄存器是否已被置1,再检查CCER对应的CE1和CE2位是否有效,比凭空改配置高效得多。

3.3 周期采样读数与中断回调的重心

HAL库模式下,读取编码器计数值的推荐做法是调用__HAL_TIM_GET_COUNTER(&htim3),这个宏会直接返回16位寄存器值。很多初学者会尝试用HAL_TIM_IC_CaptureCallback这类回调去读取计数,这是理解偏差。编码器接口模式下,捕获中断主要用于触发事件,并不需要每次捕获都在回调里做业务处理。真正需要的是固定的时间基准采样:用另一个定时器(比如TIM6)产生周期中断,在中断服务函数里读取当前编码器计数。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { int16_t current_count = (int16_t)__HAL_TIM_GET_COUNTER(&htim3); int16_t delta = current_count - last_count; last_count = current_count; // delta 就是本次采样周期内的计数增量,有符号 // 正数代表正转,负数代表反转 process_speed(delta); } }

这里的int16_t转换是关键点。由于计数器设置为自动回绕模式,当正转越过65535时会跳变到0,如果直接用无符号数值计算增量,在临界点会出现一次误判。强制转换为有符号类型后,从65534跳到0会得到增量-2,正好对应反转2个脉冲。这个有符号读法在电机只朝一个方向转时看不出差别,一旦做正反转控制,它就成了保命代码。前置的采样周期建议在10到50毫秒之间,周期过短会导致低速时增量太小、量化噪声明显;周期过长会让转速反馈滞后,速度环容易振荡。

4. 标准库版本stm32f103编码器程序:从寄存器理解每一行的作用

4.1 标准库初始化与中断通道配置

很多存量项目还在用标准外设库,这类工程的編碼器程序初始化代码写法与HAL库略有不同,但底层寄存器的操作逻辑完全一致。标准库初始化完成后,需要额外注意中断优先级分组。编码器本身不需要中断,但周期采样定时器中断若与电机驱动PWM更新中断共用优先级分组,可能出现采样抖动。建议把采样定时器的抢占优先级设低,也就是数值设置较大,避免它打断PWM反馈控制的关键时序。

一个完整可用的定时器4编码器接口初始化函数如下:

void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInit(TIM4, &TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInit(TIM4, &TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM4, 0); TIM_Cmd(TIM4, ENABLE); }

这段标准库代码与HAL库代码实现相同功能,但可以看到标准库模式下配置逻辑更为线性,完全按寄存器手册的顺序走下来。TIM_EncoderInterfaceConfig内部会操作SMCR寄存器切换从模式,这一步完成后定时器就启动了编码器接口功能。有个容易忽略的细节:GPIO_Mode_IPU上拉输入只能针对开漏输出的编码器,对于推挽输出的编码器,上拉并不影响电平,但也不会造成损坏,可以直接沿用。

4.2 定时器中断采样实现平滑速度计算

标准库方式下,速度采样使用TIM6定时中断。初始化TIM6时,预分频设置为72减1,使计数频率达到1MHz,自动重装值决定了中断周期。例如设置自动重装值为10000,中断周期就是10毫秒。10毫秒间隔对一般电机控制已经足够,测得增量后通过识别符号来确定方向,同时计算瞬时速度。需要说明的是,编码器接口模式下计数器本身不产生中断,它只是默默计数,所以采样功能必须依赖另一个定时器。

void TIM6_IRQHandler(void) { if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM6, TIM_IT_Update); int16_t current_count = (int16_t)TIM_GetCounter(TIM4); int16_t delta = current_count - last_count; last_count = current_count; // 转换成转速,lines是编码器线数 // speed单位为RPM float rpm = (float)delta * 60.0f / (lines * 4.0f * sample_period); // sample_period 单位为秒,如0.01 // 一阶低通滤波,alpha取0.6~0.9 filtered_rpm = filtered_rpm * alpha + rpm * (1.0f - alpha); } }

这段代码把计数值差转换为物理转速,本质是一个标定过程。这里用(float)delta做类型提升,避免整数除法截断误差。分母中的lines * 4对应编码器线数与四倍频的乘积,如果编码器的配置不是TI12四倍频模式,这里的乘数要同步调整。采样周期与速度量纲必须匹配,若sample_period与定时器实际中断周期不一致,最后算出的转速会整体偏差一个固定比例。

4.3 正反转判断与方向数据的重要应用

编码器计数值增量正负给出了转动方向。将增量值直接用于PID控制器是常见做法,例如速度环误差等于目标增量减去当前增量。但直接使用原始增量会有抖动问题,因为在电机堵转或低速时,单个采样周期里可能只有0到1个脉冲,方向在正负之间来回跳动。此时不能只看单次增量符号,而是要用速度滤波值判断。个人经验是引入一个小阈值,绝对值低于阈值时把速度按零处理,这在机械抱闸或反向间隙大的传动装置上效果明显。

const int16_t pulse_deadzone = 2; if (delta > pulse_deadzone) { direction = 1; effective_delta = delta; } else if (delta < -pulse_deadzone) { direction = -1; effective_delta = -delta; } else { direction = 0; effective_delta = 0; }

这种带死区的方向判定在低速场景会牺牲一点真实角度信息,但它阻止了机械振动引发的方向抖动,避免速度环输出在正反转之间高频切换。用HAL库时这段逻辑依然适用,因为读取方式和数据结构完全一样。死区阈值并非越大越好,要根据编码器线数与采样周期的配合来确定,这个数值通常由堵转实验确定。

5. stm32f103编码器程序调试避坑:5个高频现场与处理办法

5.1 现象:手动转动编码器,计数器在正反转临界点乱跳

手动慢慢转动编码器,发现计数器不是稳定递增或递减,而是在某个位置来回跳动,反转时会先增加好几个脉冲再开始减少。这种情况在低速手动测试时特别明显,原因是输入信号存在毛刺,或者AB两相边沿在临界位置时出现了短时抖动,计数器把抖动误判为反向脉冲。

原因多数来自两个方向:一是编码器线缆较长且没有屏蔽层,电机驱动电流通过地线串入信号线;二是定时器输入捕获未启用数字滤波器。解决方法是调整ICFilter滤波参数到较高档位,同时检查编码器A相B相是否各自串联了几百欧姆的限流电阻,有时候串联一个小电阻能有效抑制反射振铃。

// HAL库中修改滤波器强度,数值0~15 sEncoderConfig.IC1Filter = 15; sEncoderConfig.IC2Filter = 15;

数字滤波器的工作原理是,输入信号必须连续稳定经过若干个采样周期才被认定有效跳变,从而滤除窄脉冲干扰。滤波值越大,容忍的干扰宽度越宽,但同时会引入几十纳秒的输入延迟,对于低速编码器应用完全没有任何影响。如果调整后依然乱跳,用示波器看信号线上是否叠加有几十兆赫的振铃,必要时在靠近MCU端对地并联100pF电容。

5.2 现象:初始化后读取计数器始终为0

初始化完成后,转动编码器,COUNT寄存器始终没有变化。这是编码器调试中出现频率最高的问题。代码本身没报错,但计数就是不走。首先排查硬件连接方向,CH1接A相、CH2接B相,如果接反计数器还是会动,所以这里多半是根本没收到脉冲信号。

常见原因在于GPIO复用功能配置出错,很多人参考GPIO输出配置,把编码器引脚配置成了复用推挽输出,实际上编码器引脚必须作为输入。F103的GPIO模式中,GPIO_Mode_IPU表示上拉输入,GPIO_Mode_IPD表示下拉输入,编码器接口模式的GPIO设置为上拉输入即可。如果确认GPIO无误,下一步检查定时器时钟是否已经开启,RCC外设时钟未使能时读写寄存器不会报错,但计数器永远不会运行。

if ((TIM3->CR1 & TIM_CR1_CEN) == 0) { // 定时器未启动,检查是否有执行启动函数 }

遇到这类问题,建议用调试器实时查看TIM3->CR1寄存器和GPIOA->IDR寄存器。人为转动编码器时,IDR对应引脚电平应当出现高低变化。如果IDR没有变化,说明信号根本没到达引脚,问题在硬件连线或者编码器供电;如果IDR有变化而计数器不变,问题在定时器配置或者通道映射。

5.3 现象:计数器在某一方向越过极值时符号反转,速度突变

当编码器计数不断增加,从65535溢出到0之后,速度计算出现一次负向尖峰。这个问题在只用无符号类型读取计数器时必然出现。比如当前计数值是65533,下一周期读到3,无符号差值是6,似乎合理,但如果电机朝正转方向一直在走,从65533到3意味着实际增量应该是6,这个计算还行,但临界值附近多走几步就乱套。实际上用无符号计算在溢出瞬间差值为负却变成很大正数。

解决办法就是我在前面反复强调的int16_t有符号类型转换。利用C语言强制类型转换时的高位截断规则,65535转换后是-1,65534是-2,这样两次读数之差在数学上就是正确的增量。这个方法的前提是定时器周期设置为0xFFFF,任何两次采样周期内转动产生的脉冲数要小于32768,对电机控制场景这个条件基本自动满足。

int16_t ia = (int16_t)__HAL_TIM_GET_COUNTER(&htim3); int16_t diff = ia - last;

5.4 现象:转速波动大,低速时甚至出现正负交替

转速反馈信号噪声明显,PID控制器输出也在振荡。我在实际调试中见过很多这种情况,程序逻辑完全正确,但速度波形就是不好看。原因往往在于采样周期太短,低速工况下一个采样周期内只能捕获到几个脉冲,量化误差本来就大;再加上电机电流环带来的机械振动,导致读数忽高忽低。这是量化噪声和机械谐振的叠加,不是编码器本身坏了。

处理路线分为两层。软件层把采样周期从10毫秒调整到50毫秒,或者加大一阶低通滤波的alpha系数。第二层检查机械安装,编码器联轴器是否偏心,这会导致每圈计数不均,快速转动时能看到明显的周期性波动。公式层面的调整就是滤波参数:

if (delta == 0) { rpm = 0; } else { rpm = (float)delta * 60.0f / (lines * 4.0f * sample_period); } filtered_rpm = filtered_rpm * 0.85f + rpm * 0.15f;

alpha取0.85意味着当前采样值权重只有15%,滤波输出主要积累历史值,响应会变慢但波形平滑。速度环调试时先以平滑为准,后续再逐步调高实时性。低通滤波还有一个重要作用是消除反向间隙,这种机械间隙表现为电机已经反转,但负载端还在原地不动,编码器读数短暂无变化,滤完之后控制量不会突然反向。

5.5 现象:上电瞬间计数器是随机值,导致电机启动瞬间误动作

系统上电时,编码器计数寄存器初始值不确定,可能来自上一次的残留值,也可能是上电毛刺误触发的计数值。如果这个值直接参与速度环运算,电机上电瞬间可能会猛冲一段距离。根本原因是因为没有建立机械零点,也没有在初始化阶段对计数器做强制归零。

解决方式有两种。要求不高的场合,在初始化函数末尾调用__HAL_TIM_SET_COUNTER(&htim3, 0)或者标准库里的TIM_SetCounter(TIM3, 0),强制清零。如果系统要求上电自动找零,则需要接入Z相信号,在Z相中断里把计数值清零,这是绝对零点的唯一可靠来源。示波器观察Z相脉冲时,注意Z相通常每圈只出现一次,且宽度与编码器线数相关,别把它当成普通脉冲信号。

6. 编码器程序的进阶验证技巧:用PWM信号模拟编码器输入做自检

编码器程序写完,不能直接装到电机上凭感觉判断对错,更好的做法是在实验室里用另一个定时器产生两路相位差90度的PWM信号来模拟编码器输出。这个验证方法能把编码器程序与机械系统完全解耦,定位问题到底在程序还是在机械安装。用F103的某个定时器,比如TIM2的CH3和CH4,分别输出两路频率相同但相位差可调的方波,手动控制PWM周期即可模拟正转和反转。如果板上有空闲定时器资源,这个测试代码完全值得写进你自己的调试工具集。

// 模拟编码器信号输出:TIM2_CH3和TIM2_CH4 void Encoder_Signal_Sim_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 999; TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_Toggle; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 500; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_OC2Init(TIM2, &TIM_OCInitStructure); }

相位差实现的方式是错开两个通道的初始计数值,CH4的CCR设置不同于CH3,或者直接修改CCER中的CC4P极性来制造反相关系。实际操作中,改PWM频率就等于改编码器转速,改OCPolarity就等于换向,这种纯净的信号源能让你快速验证采样周期和四倍频系数是否算错。用两块板子或者一根杜邦线把这个信号接到编码器输入引脚,对照串口打印的转速值与理论值是否一致,偏差在1%以内就算程序调通。如果这一步完全正确,装机后还出现读数异常,问题就在线缆质量和机械环节,与程序无关。

我在自己调试某移动底盘时就靠这招排除了一个隐蔽问题。当时电机带负载后速度反馈毛刺严重,一度怀疑是编码器坏了,后来用PWM模拟信号测试发现程序读数完全正常,才意识到是电机驱动板的地线与编码器线束扎在一起,造成共模干扰。重新整理线束并加入滤波后问题消失。编码器程序调试最忌讳的就是对着怀疑猜,用独立信号源把软件逻辑验证干净,再处理硬件问题,效率高很多。希望这个trick能帮你少走一次弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询