ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32 EC11编码器驱动:定时器编码器模式原理与实现

STM32 EC11编码器驱动:定时器编码器模式原理与实现 简介EC11编码器基于STM32程序源码是一份面向嵌入式初学者的完整可运行工程。资源以STM32标准外设库实现EC11增量式编码器的数据采集、方向判断、角度与速度计算并通过串口向上位机实时发送较好解决了编码器接入MCU时的信号处理与通信难题。压缩包内共120个文件核心包括32个h头文件、30个c源文件、8个s汇编文件另有Keil工程配置uvprojx/uvoptx及hex、axf等编译输出文件便于直接打开测试与二次开发整体仅2.23MB。目前已有4139人在线学习与下载。源码结构清晰包含初始化配置、中断服务函数、数据处理与串口发送模块内容预览中可见多份STM32标准外设库文件表明工程基于ST官方的标准外设固件库代码注释齐全适合学习定时器输入捕获、外部中断、USART通信及编码器工作机理也可在此基础上扩展更多传感器应用。1. 项目概述与选型思路1.1 EC11编码器能做什么EC11是一款带按键功能的机械旋转编码器这玩意儿在嵌入式项目里出镜率极高数字音量旋钮、菜单选择、频率调节、电机位置微调、无人机云台角度微调几乎凡是需要人机交互的地方都能看到它的身影。和电位器相比EC11输出的是数字脉冲信号不会因为机械磨损导致阻值漂移寿命长、精度稳定而且天生适合接单片机。我之前在手头一个项目里需要做旋钮调参要在10个参数之间来回切换并微调数值。最开始图省事想用电位器加ADC采样后来发现两个问题一是电位器旋转超过一圈后数值会跳变二是环境干扰会导致ADC读数不稳。换EC11之后用定时器编码器模式读取方向判断和计数都由硬件完成固件只负责读寄存器彻底告别了中断风暴和毛刺困扰。这篇文章就是把这一整套基于STM32的EC11驱动方案完整记录下来从原理到代码再到实际踩坑。1.2 为什么用STM32的定时器编码器模式EC11内部是两个触点旋转时输出两路相位相差90度的方波信号也就是常说的A相和B相。判断旋转方向的办法就是看这两路信号的相位关系正转时A相领先B相90度反转时反过来。最朴素的读法是用两个外部中断一个信号变化时去读另一个的电平从而判断方向并计数。这种办法能跑但在高速旋转时中断频率极高而且机械触点抖动会造成大量误触发光消抖就可能写完一屏代码。STM32的定时器自带编码器接口模式本质上是把定时器的两个输入捕获通道配置成同时监测A、B两路信号由硬件根据两路信号的边沿组合自动判断方向并增减计数值。CPU只需要在合适的时机读一个16位或32位的计数器完全不参与边沿判断。这种方式把方向识别、倍频计数、消抖处理全部下沉到硬件可靠性高且CPU占用几乎为零。这就是我在项目里最终采用的方案。2. 程序整体设计与代码实现2.1 CubeMX配置要点我习惯先用STM32CubeMX生成工程框架再往里面填业务代码。这里以STM32F103系列为例把配置步骤完整过一遍。首先确定定时器通道。EC11的A、B两相分别接定时器的CH1和CH2比如TIM3的PA6和PA7或者TIM4的PB6和PB7。需要注意所选引脚不能和调试口、晶振引脚冲突。在CubeMX里将这两个GPIO配置为复用推挽输出模式不对是复用输入模式内部上拉打开映射到对应的定时器通道。接下来在定时器配置页面选择Encoder Mode。这里的参数有几个关键选项Combined Channels选择Encoder Mode TI1 and TI2表示同时使用TI1和TI2两个通道。Encoder Mode有三种TI1、TI2、TI1 and TI2。最常用的是TI1 and TI2这就是4倍频模式计数器在每个正交周期内计4个数分辨率最高。如果只需要1倍频选TI1或TI2计数器每个完整周期只计1个数。Input Filter这是硬件消抖的关键参数。EC11机械触点在旋转时会有1到3毫秒的抖动滤波器设置为0x0F对应约13.2微秒的采样窗口效果比较理想太短消不掉抖动太长又会丢失快速旋转的脉冲。Prescaler和Period如果不使用定时器溢出中断这两项可以保持默认的0和65535让计数器自由计数。按键部分就简单了EC11的按键引脚接一个普通GPIO输入内部上拉按下为低电平。由于按键同样存在机械抖动需要在软件里做消抖或者在CubeMX里打开GPIO的施密特触发器滤波后者在F1系列上不做要求所以一般靠软件。2.2 HAL库驱动代码CubeMX生成工程后核心驱动代码其实很短。直接看关键部分这是定时器编码器模式的初始化状态CubeMX已经搞定不需要手写寄存器TIM_Encoder_InitTypeDef sEncoderConfig {0}; sEncoderConfig.EncoderMode TIM_ENCODERMODE_TI12; sEncoderConfig.IC1Polarity TIM_ICPOLARITY_RISING; sEncoderConfig.IC1Selection TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC1Prescaler TIM_ICPSC_DIV1; sEncoderConfig.IC1Filter 0x0F; sEncoderConfig.IC2Polarity TIM_ICPOLARITY_RISING; sEncoderConfig.IC2Selection TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC2Prescaler TIM_ICPSC_DIV1; sEncoderConfig.IC2Filter 0x0F; htim3.Instance TIM3; htim3.Init.Prescaler 0; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 65535; HAL_TIM_Encoder_Init(htim3);初始化之后启动编码器接口HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL);读取当前计数值int16_t encoder_value (int16_t)__HAL_TIM_GET_COUNTER(htim3);用__HAL_TIM_GET_COUNTER读回来的是一个无符号数但在编码器模式下硬件会自动根据旋转方向做加减计数向上计数溢出时会回绕所以只要把这个值强制转换成int16_t就自然得到了有符号的相对位移。顺时针旋转为正、逆时针为负具体取决于A、B两相的接线顺序如果发现方向反了后面会讲怎么处理。如果需要在某个位置清零直接写计数器__HAL_TIM_SET_COUNTER(htim3, 0);2.3 主循环中读取位置的封装实际项目里不推荐在主循环里直接操作计数器更合理的做法是写一层驱动接口隔离硬件细节。下面是我常用的封装思路。// 编码器驱动头文件的关键部分 typedef struct { TIM_HandleTypeDef *htim; int16_t position; int16_t last_position; int16_t delta; } Encoder_Handle_t; void Encoder_Init(Encoder_Handle_t *enc, TIM_HandleTypeDef *htim); void Encoder_Update(Encoder_Handle_t *enc); int16_t Encoder_Get_Delta(Encoder_Handle_t *enc);// 编码器驱动源文件 void Encoder_Update(Encoder_Handle_t *enc) { int16_t current (int16_t)__HAL_TIM_GET_COUNTER(enc-htim); enc-delta current - enc-position; enc-position current; } int16_t Encoder_Get_Delta(Encoder_Handle_t *enc) { return enc-delta; }主循环里每次调用Encoder_Update就能得到自上次调用以来旋转了多少格正负代表方向。这种做法把计数器的绝对位置变成相对位移上层业务逻辑只需要关心“这次旋转了多少”而不用管计数器累加到了多少也不会出现因为计数器溢出导致的位置错乱。3. 核心细节解析与实操要点3.1 方向判定与倍频计数到底怎么回事EC11的输出信号是正交方波一个完整的旋转刻度对应一个正交周期。正转时A相上升沿出现时B相为低电平反转时A相上升沿出现时B相为高电平。STM32编码器模式做的事情就是在这两路信号的每个边沿都去采样另一路信号的电平根据组合状态判断方向并加一或减一。选择TI1 and TI2模式时一个正交周期内有4个边沿计数器会加4或减4也就是4倍频。选择只用一个通道时每个正交周期只有2个边沿可检测就是2倍频如果再配置一下边沿极性只在一个通道的固定边沿计数就是1倍频。这里有一个新手常踩的坑4倍频模式下旋转一格计数值变化是4不是1。很多人在显示屏上看到一格跳4个数以为是编码器或者代码有问题其实这是正常的。如果你希望一格对应一个数就得用1倍频模式或者在软件里把delta除以4。不同项目需求不同没有绝对好坏我是做菜单选择时用1倍频做高精度微调时用4倍频各取所需。3.2 硬件消抖与软件消抖的配合EC11是机械触点开关旋转时触点接触瞬间会产生高频抖动。STM32的输入滤波器本质上是一个数字低通滤波器它会持续采样引脚电平只有在连续若干次采样都稳定在高电平或低电平后才认为信号有效。CubeMX里的Input Filter参数对应的是一段持续采样窗口数值越大滤波时间越长抗抖效果越好但也会引入信号延迟。实测下来对于普通的EC11编码器Filter设为0x0F是性价比比较高的选择。如果你的旋钮手感偏“肉”抖动严重可以往0x2F调如果机构特别干净利落0x05也够用。按键的消抖我习惯在软件里处理简单可靠。思路是检测到按键引脚为低后开启一个10到20毫秒的软件延时或者用定时器轮询延时后再读取一次如果还是低电平才确认按下。这个逻辑用状态机写最稳不阻塞主循环。typedef enum { KEY_STATE_IDLE, KEY_STATE_PRESSED, KEY_STATE_CONFIRM, KEY_STATE_RELEASE } Key_State_t; void Key_Scan(void) { static Key_State_t state KEY_STATE_IDLE; static uint32_t tick_start 0; uint8_t level HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin); switch (state) { case KEY_STATE_IDLE: if (level 0) { tick_start HAL_GetTick(); state KEY_STATE_PRESSED; } break; case KEY_STATE_PRESSED: if (level 0 (HAL_GetTick() - tick_start) 15) { state KEY_STATE_CONFIRM; // 按键确认事件在这里执行按下逻辑 } else if (level 1) { state KEY_STATE_IDLE; } break; case KEY_STATE_CONFIRM: if (level 1) { state KEY_STATE_RELEASE; } break; case KEY_STATE_RELEASE: if (level 0) { // 按键松开触发释放事件 state KEY_STATE_IDLE; } break; default: state KEY_STATE_IDLE; break; } }3.3 一个容易被忽略的问题编码器接在JTAG/SWD引脚上STM32的部分引脚默认复用为调试功能比如PA13到PA15、PB3、PB4这些它们对应SWDIO、SWCLK、JTDO、JTRST等。如果你手头的板子恰好把EC11接到了这些引脚上程序下载一次后第二次就连接不上调试器了运气不好还会报“no target found”一类的链接错误。解决方法是初始化后第一时间禁用JTAG只保留SWD。在标准库下用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)HAL库下可以直接操作AFIO寄存器。禁用之后这些引脚才能正常当GPIO用。不过更好的做法是选型时直接避开调试脚省得给自己找麻烦。4. 实操过程与核心环节实现4.1 一个完整的参数调节应用实例说了这么多理论直接上一个完整的应用场景用EC11实现一个带参数切换和微调功能的小系统。假设设备有3个参数需要调节短按按键切换参数旋转旋钮调节当前参数值。硬件连接如下EC11引脚STM32引脚说明A相PA6TIM3_CH1编码器正交输入B相PA7TIM3_CH2编码器正交输入按键PB0输入上拉按下低电平GNDGND共地主循环逻辑如下#define PARAM_COUNT 3 #define PARAM_STEP 1 #define PARAM_MAX 100 #define PARAM_MIN 0 static int16_t param_values[PARAM_COUNT] {50, 30, 80}; static uint8_t current_param 0; void Process_Encoder(void) { Encoder_Update(enc); int16_t delta Encoder_Get_Delta(enc); // 按一格一个单位换算4倍频模式下需要除以4 delta delta / 4; if (delta ! 0) { param_values[current_param] delta * PARAM_STEP; if (param_values[current_param] PARAM_MAX) { param_values[current_param] PARAM_MAX; } if (param_values[current_param] PARAM_MIN) { param_values[current_param] PARAM_MIN; } Display_Refresh(); } } void Process_Key(void) { static uint8_t key_event_pending 0; if (key_event_pending) { key_event_pending 0; current_param (current_param 1) % PARAM_COUNT; Display_Refresh(); } }4.2 旋钮手感与显示刷新策略这里有个经验之谈显示刷新不要放在Encoder_Update里面而是放在主循环的固定周期任务里。如果每检测到一次旋转就立刻刷新屏幕高频旋转时光刷屏就能把CPU吃满。我的做法是维护一个“脏标志”Encoder_Update检测到delta变化时置位这个标志主循环里以50毫秒为周期检查一次标志需要刷新才执行显示更新。这样既不会丢失旋转事件也不会因为刷新太频繁导致界面卡顿。对于带OLED屏的项目这个策略尤其重要。SSD1306这种屏通过I2C或SPI刷新一帧画面要几毫秒到十几毫秒旋转飞快时你想一帧不落全刷出来根本不现实保证最终位置追上旋钮就够了中间的过程帧能省就省。4.3 首次上电调试的正确步骤第一次接线调试时别急着写一大堆业务逻辑先把最基础的动作跑通。第一步只接EC11的A、B两相和GND写一个最简单的程序在上电后把计数器的值通过串口打印出来。手动缓慢旋转旋钮观察串口输出是否随着旋转方向增减。第二步快速旋转旋钮看是否有漏计数的情况。这时候重点观察快速旋转时计数器是否连续变化。如果发现快转时数值跳变大概率是滤波参数太大边沿被滤掉了如果慢转时数值乱跳则是滤波不足有抖动混进来。第三步按住按键旋转旋钮看是否会出现额外的计数。有些EC11在按压状态下旋转由于内部结构受力信号会变得不干净需要适当加大滤波参数。调试过程中建议打开逻辑分析仪抓A、B相的波形看细节没有逻辑分析仪就靠串口打印计数变化。我在第一次做编码器调试时就因为没看波形以为是代码写错了折腾了半天才发现是A、B两相线序接反了方向反了而已。5. 常见问题与排查技巧实录5.1 旋转方向反了怎么解决方向反是个老问题。A相和B相信号本身就是对称的接反后唯一的差别就是方向判断逻辑相反。修正方法有两种一是硬件上把A、B两根线对调二是软件上把计数结果取反。接反的时候计数器是均匀变化的话软件里加一行代码就行int16_t Encoder_Get_Delta(Encoder_Handle_t *enc) { return -enc-delta; }具体用哪种方式取决于产品结构是否方便改线。量产的板子不方便动硬件软件取反最省事如果是调试阶段的飞线改线更直接代码可以保持原样。5.2 旋转时数值抖动、往回跳变这是EC11项目里最折磨人的问题之一。现象是正向旋转时计数器偶尔会先加几个数又减回来几个数整体趋势没问题但有回退或者跳变。排查思路按优先级排列供电稳定性。EC11的信号抖动很多时候不是编码器本身的问题而是电源纹波太大。试着在STM32的VDD和GND之间就近加一个100nF的陶瓷电容如果条件允许再并一个10uF的电解电容。上拉电阻。EC11内部没有上拉需要外部上拉。STM32内部的上拉电阻大约在30到50千欧在某些场合偏弱容易受干扰。外部加一个10千欧的上拉到3.3V信号质量会有明显改善。滤波电容。在EC11的A、B两相各对地加一个10nF到100nF的电容可以有效吸收机械抖动和空间耦合的干扰。电容加的时候尽量靠近编码器插座放置。滤波参数。前面提过Input Filter数值如果前面几步都做了还是抖可以试着加到这个值。5.3 进入低功耗模式后编码器失效如果项目有低功耗需求需要特别注意进入STOP模式前定时器的编码器接口会停止计数唤醒后计数器停留在进入低功耗前的值期间用户旋转旋钮的动作完全丢失。解决办法有两种思路。一是唤醒后不依赖计数器值而是主动读取A、B两相的当前电平结合之前的计数方向做一个推算。二是在睡眠期间通过外部中断唤醒A相或B相的任意边沿都能唤醒MCU然后在中断里恢复编码器模式并继续计数。方案二更可靠但代码复杂度高一些需要在低功耗模式的配置里把编码器对应的GPIO设置为EXTI中断源同时确保进入低功耗前定时器时钟处于可以被唤醒后快速恢复的状态。5.4 在RTOS环境下计数器读值异常如果项目跑FreeRTOS这类RTOS要注意任务优先级和读取时机的问题。编码器的读取任务如果优先级太低可能会长时间得不到调度导致累计delta过大或者出现中间状态的信号丢失。我的经验是两个任务配合一个高优先级的短任务只做Encoder_Update和delta清零另一个正常优先级的任务消费delta值做业务逻辑和界面更新。这样可以保证编码器的实时性同时不影响其他任务的运行。另外在多任务环境下如果多个任务都读取或修改同一个编码器计数值需要考虑临界区保护。FreeRTOS里用taskENTER_CRITICAL和taskEXIT_CRITICAL或者用互斥量保护这段代码避免数据竞争的问题。6. 使用过程中的额外经验最后聊几个实践中摸索出来的细节问题。EC11的机械寿命一般在3万次旋转左右这个数值对于消费类产品够用但工业场景可能要选择更耐用的型号。我遇到过一款便宜货用了不到两个月就开始出现一段旋转多计数的情况换了稍微好一点的型号后问题就消失了。主控端代码一样差别全在编码器本体的触点质量和内部结构设计上。还有一个习惯是初始化时把计数器清零但要注意ENCODER模式下的计数器清零和普通定时器清零是同一套代码我在第一次做的时候还犯过一个小错就是清零之前忘了停掉定时器结果清零后立刻又被一个悬空引脚干扰出了几个计数。所以可靠的做法是按顺序停定时器、清零、再启动定时器HAL_TIM_Encoder_Stop(htim3, TIM_CHANNEL_ALL); __HAL_TIM_SET_COUNTER(htim3, 0); HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL);最后再说一个小技巧。如果系统的界面菜单层级很深想要通过单击和双击来区分不同功能EC11的按键在结构上通常可以支持这种操作。这时消抖时间不宜太长否则识别不出快速双击。我的经验是消抖10毫秒、双击判定窗口300毫秒是个比较平衡的配置。如果你在做一个需要精确调整参数的设备甚至可以给按键加上长按连续加速的功能按住不停数值自动越滚越快。这些都是在上层业务里做的和底层驱动无关但可以明显提升使用体验。本文还有配套的精品资源点击获取
返回列表