ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32CubeMX配置正交编码器:从原理到代码的电机测速完整指南

STM32CubeMX配置正交编码器:从原理到代码的电机测速完整指南 很多人第一次在STM32上做电机测速、云台角度回读、或者精确定位的时候都会被“正交编码器”这四个字劝退。其实用STM32CubeMX配置正交编码器也就是十几分钟的事。我们不需要自己写边沿捕获、不需要开外部中断一个个数脉冲只要把定时器切到Encoder Mode剩下的硬件全帮你干完了。这篇文章我把从原理到CubeMX实操再到代码读取的完整流程都整理出来几个容易踩的坑也标注在最前面希望能帮想用编码器测速或者定位的朋友少走弯路。1. 项目背景与整体设计思路1.1 为什么选择STM32CubeMX来配置做嵌入式开发的老手都知道早期用标准外设库配编码器模式是个繁琐的活要手动计算定时器的时钟树、要配置GPIO的复用功能、要设置TI1和TI2的映射关系、还要处理极性反相、滤波器那一堆寄存器位。光是搞清楚TIM的SMCR寄存器里SMS位写100还是101就能耗掉半天时间。STM32CubeMX把这个过程变成了图形化点选。你只需要在定时器的配置页里把Combined Channels改成Encoder Mode勾上TI1和TI2两个通道软件会自动帮你把GPIO复用、时钟使能、寄存器初始化代码全部生成好。这带来的好处不只是省时间更重要的是减少了人为配置出错的概率。我之前在项目里手动配过一次复用功能把PA8当成通道输入用结果引脚电平死活不变化排查半天才发现复用映射选错了。用CubeMX后这类低级错误基本可以杜绝。另外很多团队现在用STM32CubeMX管理整个工程的时钟、外设、中间件如果你要在同一个项目里同时用ADC采集、串口调试、PWM输出和编码器输入用CubeMX统一生成代码后续维护起来也方便很多。特别是换芯片型号的时候直接修改.ioc文件重新生成即可不用手动移植外设初始化代码。1.2 正交编码器的基本工作原理正交编码器也叫增量式编码器输出的是A、B两路相位相差90度的方波信号。电机转动时A、B两路信号会产生规律的相位关系正转时A相超前B相90度反转时B相超前A相90度。芯片正是通过检测这个相位关系来判断方向再通过累计边沿数量来计算位移。常见的编码器每转一圈会输出固定的脉冲数比如1000线编码器一圈就是1000个脉冲但这只是A相或者B相的脉冲数。如果我们把A、B两路的上升沿和下降沿全部利用起来一个脉冲周期内就能记录到4个状态变化也就是大家常说的4倍频。这样1000线的编码器实际分辨率可以达到每圈4000个计数值。除了A、B两路很多编码器还有Z相也叫零位信号每转一圈输出一个脉冲通常用来做回零定位。Z相一般会接到另外一个定时器通道上或者用普通IO配合外部中断这个我们在后面专门讲。1.3 方案选型硬件定时器模式还是外部中断计数很多初学者看到编码器信号第一反应是用外部中断来数脉冲。外部中断的方式确实也能实现计数A相触发中断在中断里判断B相的电平高低进而决定计数加一还是减一。但这个方案有几个硬伤第一编码器在高速旋转时脉冲频率会很高假设3000转的电机配合500线的编码器4倍频之后每秒就有10万个计数事件外部中断根本忙不过来还会拖垮主程序第二中断响应本身有延迟和抖动容易漏脉冲或者误判方向导致计数不准。定时器的Encoder Mode是纯硬件逻辑实现的不需要CPU参与。定时器内部持续不断地对TI1和TI2进行采样然后根据两路信号的边沿组合来自动更新计数器的值。无论电机转到多快只要脉冲频率不超过定时器时钟上限计数就不会丢。这就把CPU解放出来了主循环该干什么干什么读取计数值的时候直接拿计数器寄存器就行。所以在电机控制、自动化定位这类场景下硬件的编码器模式几乎是唯一合理的选择这也是我推荐你使用STM32CubeMX来配置的根本原因。2. 核心原理定时器编码器模式到底在做什么2.1 编码器模式的采样逻辑STM32的通用定时器TIM2、TIM3、TIM4、TIM5等都支持编码器接口模式。开启这个模式后定时器的CH1和CH2引脚自动变成编码器输入内部逻辑会同时监测这两个引脚的电平变化。我要强调一个容易误解的点编码器模式不是在两个引脚的上升沿计数这么简单它内部有一个状态机。TI1和TI2一共可以构成4种电平组合00、01、10、11电机转动时这4种状态按固定顺序循环变化。正转时状态的顺序是00→01→11→10或者反过来反转时顺序完全颠倒。定时器检测到状态变化后根据变化的方向决定计数器CNT是加一还是减一。这就是为什么编码器模式天然就能区分正反转不需要你去写逻辑判断。在CubeMX里Encoder Mode有三个选项Encoder Mode TI1、Encoder Mode TI2、Encoder Mode TI1 and TI2。前两个模式只在单路信号的边沿计数相当于2倍频或者1倍频。最后一个是同时利用两路信号的所有边沿也就是4倍频。绝大多数场景我都建议选TI1 and TI2把分辨率拉到最高这样测速和定位的精度都好一些。2.2 计数方向与倍频的关系编码器模式下的倍频取决于你配置的是单通道还是双通道。打个比方TI1模式只在A相的上升沿计数B相只用来判断方向这样每个完整脉冲周期只计1次TI2模式同理在B相边沿计数也是每个脉冲周期计1次TI1 and TI2模式A、B两相的上升沿和下降沿几乎都会触发计数每个脉冲周期计4次。这里有个细节值得注意在双通道模式下也不是每个状态跳变都计数。硬件会在特定的边沿组合下更新计数器确保一个完整循环内恰好增加4个计数。我实际测试过在TI1 and TI2模式下让电机正转一圈计数值增加的数量正好等于编码器线数乘以4误差为零。计数方向方面ARR和CNT配合使用。默认配置下正转CNT向上计数反转CNT向下计数。这个方向可以通过交换A、B两相接线来反转也可以在CubeMX里直接改通道的极性来实现。我建议前期调试的时候保持默认方向等确定电机正反转的定义之后再决定要不要在设计上反转。2.3 溢出与计数量程设计编码器模式使用的还是定时器的16位或者32位计数器。以16位定时器为例CNT的范围是0到65535。编码器正转时CNT递增超过65535就会溢出反转时CNT递减低于0会下溢。如果不对溢出做处理你拿到的计数值就是一个模65536的循环量单圈以内没问题多圈累计就会出错。解决多圈计数有两个思路。第一个思路把ARR设置为编码器每圈的4倍频值这样每转一圈CNT刚好从0走到ARR然后触发更新事件在更新中断里用一个软件变量记录圈数。这个方案的好处是CNT直接对应当前圈内的绝对位置定位控制特别方便。第二个思路把ARR设置为最大值65535CNT纯粹当作一个相对量来读每次读取后用上一次的值做差值得到增量再配合溢出方向来修正圈数。我个人做速度测量更喜欢第二种方式因为速度只关心增量圈数修正逻辑本身也不复杂。做绝对位置定位则更适合第一种方式代码更直观。具体的代码实现我放在后面的章节里详细展开。3. STM32CubeMX实操配置步骤3.1 新建工程与引脚规划打开STM32CubeMX新建工程选择你手头的芯片型号。这里以常见的STM32F103C8T6为例它的TIM2、TIM3、TIM4都支持编码器模式引脚资源也比较充裕。我建议把编码器的A相接TIMx_CH1B相接TIMx_CH2。以TIM3为例A相接PA6TIM3_CH1B相接PA7TIM3_CH2。如果你要用Z相做回零那Z相可以接到PB0这类普通IO配合外部中断或者定时器输入捕获来处理。确定引脚后在CubeMX里把PA6和PA7两个引脚点击一下在弹出的菜单中选择TIM3_CH1和TIM3_CH2。CubeMX会自动推导出引脚需要用到的复用功能非常方便。这里要注意编码器输入不需要配置上拉或者下拉定时器模式会自动处理引脚的电气特性。如果编码器输出类型是开漏那外部需要加合适的上拉电阻这个跟CubeMX配置无关是硬件设计层面的问题。3.2 定时器参数配置详解配置好引脚之后在左侧Categories列表里找到Timers点击TIM3进入配置界面。首先要确认两个关键参数Clock Source选择Internal Clock这个不用动它是定时器的时基时钟编码器计数不依赖它产生PWM但时基时钟决定了计数频率上限。然后在Combined Channels下拉框里选择Encoder Mode这时界面会出现TI1和TI2相关选项把两个通道都勾选成Input capture direct mode。Encoder Mode下拉框选择TI1 and TI2。接着往下看参数配置这里有几个参数需要仔细设置Prescaler预分频编码器模式下预分频器默认设为0也就是说每个计数事件对应一个边沿不做分频。改成非零值可以达到一个类似于软件降采样的效果但一般没这个必要。Counter Period自动重载值ARR根据你的量程需求来填。如果做单圈定位填每圈的4倍频数值比如500线编码器填2000如果做增量测量填65535。Input Filter输入滤波器这个参数很关键后面讲抗干扰的时候我会详细展开。调试初期可以填0不滤波等确认基本功能正常后再根据现场情况调整。Polarity极性默认Rising Edge就行。如果发现方向反了我建议先交换A/B接线来验证而不是急着改极性。配置完参数之后记得在Project Manager里设置好工程名、路径、IDE类型MDK-ARM或者IAR选好HAL固件库版本然后点击Generate Code生成工程。3.3 生成代码与基础验证生成完工程后打开工程在main.c的main函数里找到MX_TIM3_Init()函数里面就是CubeMX自动生成的配置代码。你会看到类似这样的结构static void MX_TIM3_Init(void) { TIM_Encoder_InitTypeDef sConfig {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; sConfig.EncoderMode TIM_ENCODERMODE_TI12; sConfig.IC1Polarity TIM_ICPOLARITY_RISING; sConfig.IC1Selection TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler TIM_ICPSC_DIV1; sConfig.IC1Filter 0; sConfig.IC2Polarity TIM_ICPOLARITY_RISING; sConfig.IC2Selection TIM_ICSELECTION_DIRECTTI; sConfig.IC2Prescaler TIM_ICPSC_DIV1; sConfig.IC2Filter 0; HAL_TIM_Encoder_Init(htim3, sConfig); }这个初始化函数把定时器配置成了编码器接口模式。看一下EncoderMode那行是TIM_ENCODERMODE_TI12说明两个通道双沿都在计数这就是我们想要的4倍频模式。基础验证的方法很简单接上编码器手动慢慢旋转电机轴然后在调试器里观察TIM3的CNT寄存器值。如果正转时数值增加、反转时数值减小说明配置正确。如果方向反了交换A/B线再试。如果数值乱跳那就要检查接线、滤波和供电了。4. 代码实现与数据处理4.1 读取计数值使用HAL库读取编码器计数值非常简单一行代码就行int16_t count (int16_t)__HAL_TIM_GET_COUNTER(htim3);为什么这里用一个强制类型转换因为CNT寄存器是一个16位的无符号数范围0到65535。但我们知道编码器的计数是有方向的正转加、反转减。如果直接把寄存器值赋给一个int16_t类型变量硬件上的二进制补码转换会帮我们把这个循环区间映射到-32768到32767这样读取出来的值就是一个带正负的“相对位置”。这个技巧在很多编码器应用里非常实用。以500线编码器4倍频为例一圈2000个计数。选ARR为65535时只要单圈内正反两个方向从零点出发的累计变化量不超过32767个计数相当于16圈以上读取出来的int16_t值就能直接当作带符号的位置量使用不需要额外判断溢出。当然如果你要做多圈累计或者长时间运行单靠这个技巧是不够的必须处理溢出事件。4.2 增量转速计算测速的本质就是计算单位时间内计数值的变化量。我常用的方法是在一个固定时间间隔比如10ms内读取两次计数值做差得到增量再换算成转速。伪代码逻辑如下uint16_t last_count __HAL_TIM_GET_COUNTER(htim3); uint32_t last_time HAL_GetTick(); float speed_rpm 0.0f; while (1) { delay_ms(10); uint16_t now_count __HAL_TIM_GET_COUNTER(htim3); uint32_t now_time HAL_GetTick(); int16_t delta_count (int16_t)(now_count - last_count); uint32_t delta_time_ms now_time - last_time; // 每秒计数增量 delta_count * 1000 / delta_time_ms // 转速RPM 每秒计数增量 / (每圈计数量) * 60 float delta_per_sec (float)delta_count * 1000.0f / (float)delta_time_ms; speed_rpm delta_per_sec / (encoder_ppr * 4.0f) * 60.0f; last_count now_count; last_time now_time; }这里需要注意一个细节delta_count的计算用int16_t类型是因为两次采样之间电机可能正转也可能反转直接用uint16_t做差再转换会出现方向丢失的问题。先用int16_t强制转换再利用二进制补码的特性就能正确得到带符号的增量值。这个技巧我在项目里用了很多次非常可靠。还有一个设计取舍采样周期越短速度响应越快但低速时的量化误差也越大。比如10ms采样周期下低速时每个周期可能只增加1到2个计数测出来的速度是一跳一跳的。如果应用对低速平稳性有要求可以加大采样周期比如用50ms代价是动态响应变慢。具体选多大要根据你电机的额定转速和控制周期来权衡。4.3 多圈计数与溢出处理如果你的应用需要连续多圈累计就必须处理溢出问题。我推荐的方案是开启定时器更新中断在中断里维护一个软件计数器。以ARR65535为例正转溢出时CNT从65535跳到0方向向上反转下溢时CNT从0跳到65535方向向下。在更新中断里判断方向把软件计数器加一或减一。代码上先在CubeMX里使能TIM3的全局中断NVIC然后在回调函数里写逻辑volatile int32_t encoder_overflow_count 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { // 通过DIR位判断计数方向 if ((TIM3-CR1 TIM_CR1_DIR) ! 0) { encoder_overflow_count--; // 向下计数产生更新事件 } else { encoder_overflow_count; // 向上计数产生更新事件 } } }完整的位置值可以这样计算// 读取当前CNT和溢出计数值 uint16_t cnt __HAL_TIM_GET_COUNTER(htim3); int32_t total_count (int32_t)encoder_overflow_count * 65536 (int32_t)(uint16_t)cnt;这个方案的关键在于定时器必须配置成更新事件由溢出或者下溢触发要确认CubeMX里Update interrupt是使能状态同时NVIC里对应中断线打开。否则中断不触发软件计数就永远不更新。另一种更精细的做法是不同时读取CNT和overflow_count可能导致数据不同步因为中断可能在读取过程中触发。严谨的代码应该用临界区保护一下比如先关中断读CNT再开中断。不过在实际应用里如果你的采样频率远低于中断频率不同步造成的误差通常可以忽略我一般只在要求极高的场合才加临界区保护。4.4 Z相零位与回零逻辑增量式编码器的特点是没有绝对位置上电之后你不知道当前角度在哪。如果需要回零就要用到Z相信号。Z相每转一圈输出一个脉冲这个脉冲的位置与编码器的机械位置严格对应。处理Z相有两种常用方式。第一种是把Z相接在另一个定时器的输入捕获通道上开启输入捕获中断捕获到Z相脉冲时记录当前位置并清零计数器。第二种是直接把Z相接普通IO口用外部中断来处理。我比较推荐第二种方式因为回零逻辑往往不只是清零计数器这么简单。实际应用中通常需要先低速反转找Z相找到之后还要让电机停在某个特定位置比如Z相之后的第N个计数处这个过程中你可能还要控制电机启停放普通IO中断里处理更灵活。一个典型的回零流程设置一个低速目标速度让电机朝一个固定方向旋转。等待Z相外部中断触发表示已经找到零位标记。在Z相中断里把编码器计数值清零同时记录当前圈数。如果需要停在精确的机械零位则继续以极低速度运动直到计数值等于某一预设偏移量后停止。这个流程看起来简单实际调试时还是有不少坑。比如Z相信号宽度一般只有几微秒外部中断配置为双边沿触发时容易产生毛刺再比如回零过程中电机过冲导致Z相脉冲被漏掉。所以我之前在代码里都会加一个超时保护如果10秒内没有找到Z相就认为回零失败重新开始。这些细节在博文后面的问题排查部分也会提到。5. 常见问题与排查技巧实录5.1 计数异常问题速查表我把实际调试中遇到的高频问题整理成了表格每个问题都标注了排查思路。这省去了不少重复排查的时间现象可能原因排查方法计数值一直为0引脚复用未生效确认PA6/PA7是否选成TIM3_CH1/CH2重新生成代码计数值不增加只跳变编码器供电或接线松动用示波器/万用表量A/B相电平确认有方波信号计数方向反了A/B接反交换编码器A/B输出线或在CubeMX里改极性低速计数正常高速丢步输入滤波过大减小IC1Filter/IC2Filter或检查硬件上拉电阻计数值来回抖动编码器信号噪声大开启输入滤波、使用屏蔽线、检查共地上电后第一次计数异常定时器未启动在初始化后调用HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL)多圈累计数值不对溢出中断未处理检查NVIC是否使能验证溢出回调是否触发特别强调一下启动定时器这步。CubeMX生成代码后很多人忘了启动编码器模式。如果你在main里没调用HAL_TIM_Encoder_Start计数器是静止的读数永远不变。HAL库不会自动启动编码器模式必须手动调用。这个是我见过最多新手踩的坑。还有一点编码器模式的输入频率上限和定时器时钟有关。以F103的72MHz定时器时钟为例理论上可以处理的编码器频率远超实际电机能产生的脉冲频率所以基本不用考虑超限问题。但如果你用低速时钟跑高频编码器确实可能出现采样漏边沿的情况这个可以通过提高定时器时钟或者降低编码器线数来解决。5.2 抗干扰与布线经验编码器信号在工业现场最容易受到干扰。电机驱动线、电源线都是干扰源如果编码器线跟这些线走在一起很容易把毛刺耦合进A/B信号里。CubeMX里提供的输入滤波功能是抗干扰的第一道防线。在编码器模式下IC1Filter和IC2Filter的单位是定时器时钟周期可以设置一个范围。以72MHz时钟为例Filter值设为比如8大约能滤掉110ns以的窄脉冲。这个参数并不是越大越好因为滤波会引入信号延迟过高时在高速编码器应用中会造成计数偏差。我的经验是先从比较小的值开始试比如4到8现场有示波器的话直接观察滤波前后的波形。硬件层面的措施也不能缺少。我给几个在实际产品上验证过的建议编码器线使用双绞屏蔽线屏蔽层单点接地。编码器供电独立走线不要和电机电源共用一根线。在单片机端的A/B引脚各加一个100Ω串联电阻和10nF对地电容做简单RC滤波。编码器与单片机之间确保共地地线压差过大时信号电平可能出现不确定性。软件上还有一个技巧读计数值时连续读两次如果两次结果一致再使用。因为计数器可能在读取过程中发生变化这种读取竞争问题在高频计数时可能出现。用HAL库的__HAL_TIM_GET_COUNTER读取时本质上就是一个16位读取操作在部分硬件上可能触发锁存机制但保险起见连续两次读取加校验的逻辑开销很小值得加上。5.3 调试心得与经验总结最后分享一点我的个人体会。先说调试工具我强烈建议手头常备一个带逻辑分析仪或者双通道示波器。编码器调试中八成的问题看一眼A/B相波形就能定位比盲改代码快得多。如果没有示波器也可以用两个LED分别接A/B相手动慢慢转电机观察LED点亮的顺序来判断相序是否正常这个方法在野外或者实验室没有仪器的时候特别好用。另外在写测速代码时我习惯先用固定占空比开环驱动电机然后在串口打印计数值验证方向、量程、滤波都正确之后再上闭环控制。这种分步验证的思路能最大程度上避免“开环都转不动闭环更是一团糟”的局面。还有一个容易忽略的点编码器的机械安装精度会直接影响读数稳定性。联轴器松动、电机轴径向跳动、编码器码盘污染这些机械问题在软件上无论怎么调都救不回来。如果排除了所有电气和软件问题但计数还是不稳定不妨检查一下编码器的机械固定情况。做嵌入式就是这样软件上看起来简单的一个外设真正稳定可靠地用起来需要从原理到接线再到调试方法全面考虑。希望这篇文章能把STM32CubeMX配置正交编码器这条路帮你蹚平你接下来要做的就是拿一块板子、一个电机按照步骤操作一遍有问题随时对照排查表来定位。
返回列表