简介:欧姆龙E6B2编码器驱动程序基于STM32F1系列与HAL库编写,面向工业自动化和嵌入式开发者,用于精确读取编码器转速与角度,适用于速度、位置反馈控制场景。资源包共215个文件,大小仅1.29MB,主要包含C/H源码、Keil工程配置(uvprojx/uvoptx)、编译产物(axf/hex/o)以及说明文档(txt/pdf)等,便于直接查看程序逻辑、重新编译或移植到其他F1型号。已有73人学习下载。驱动围绕定时器输入捕获与外部中断实现脉冲计数,并封装了用户接口函数,内容预览显示核心代码基于标准HAL库的定时器与GPIO模块,代码结构清晰,对理解编码器原理和HAL库事件响应机制有较好的参考价值,适合需要快速上手旋转测量开发的工程人员。
1. E6B2编码器的信号特征与驱动程序在 STM32 HAL 库里的定位
同样是驱动,Windows 那边是数字签名、代码 31、设备管理器;到了 STM32 这边,驱动程序就是一套 HAL 库代码。欧姆龙 E6B2 增量编码器输出的 A/B/Z 正交脉冲,经定时器编码器模式解码成计数器值,用户在应用层把计数器差换算成 rpm,这就是"欧姆龙编码器E6B2驱动程序 STM32HAL库转速读取"的全部内容。做闭环步进电机、输送带定位、电机转速反馈的人都会遇到这个需求。下面按接线、HAL 库配置、转速公式、稳定性处理、校准验证的顺序把它讲透。
2. STM32 定时器编码器模式:转速读取的 HAL 库配置
E6B2 输出两路相位差 90° 的方波 A 和 B,Z 相每转一圈出一个脉冲。转速读取的本质是:统计单位时间内有多少个正交边沿。STM32 的定时器内置正交解码器,可以在硬件层面完成这件事,不需要 CPU 介入。先讲清楚为什么这是最稳的方案,再给 CubeMX 里的具体配置。
2.1 为什么是定时器编码器模式,而不是 GPIO 外部中断
不少入门资料教的是把 A、B 两相接外部中断,在中断服务函数里判断相位差来加减计数。这个方法在低速下能跑,但有两个硬伤。
第一是中断频率。以 600P/R 的 E6B2 为例,4 倍频后每转产生 2400 个计数。3000rpm 时每秒就有 120000 个计数,对应 120kHz 的中断频率。Cortex-M3 处理一次中断至少几十个周期,这就把 CPU 全部吃掉了,还要算上压栈、出栈和判断方向的指令。第二是外部中断不能硬件判断方向,必须在中断里读另一个引脚的 IO 电平,而 A/B 切换瞬间的电平不稳定,处理不当就会出现方向误判、计数来回跳。
定时器编码器模式把这两件事都收进硬件:正交解码器根据 A/B 相位关系自动决定 CNT 递增还是递减,CPU 完全不参与边沿处理,只在需要时读一次 CNT。对 600P/R、4 倍频的场景,计数器最高可跑到 100kHz 以上而零 CPU 开销,这也是实际项目里最常用的接法。
2.2 CubeMX 里的编码器参数:从 A/B 相到 TIM 的完整配置
以 STM32F103 的 TIM3 为例,A 相默认映射到 PA6(CH1),B 相映射到 PA7(CH2)。CubeMX 里把 TIM3 的 Combined Channels 设为 Encoder Mode,时钟源选 Internal Clock,然后按下面这张表填参数。
| 参数 | 设置值 | 说明 |
|---|---|---|
| Encoder Mode | TI1 and TI2 | 两个通道都计数,实现 4 倍频 |
| IC1 Polarity | Rising | A 相上升沿计数 |
| IC2 Polarity | Rising | B 相上升沿计数 |
| Input Filter | 0xF | 输入滤波,滤掉窄毛刺 |
| Prescaler | 0 | 不分频,避免丢计数 |
| Period (ARR) | 0xFFFF | 16 位自由运行,溢出由软件处理 |
生成代码后,tim.c 里会多出一段编码器初始化,核心结构体长这样:
TIM_Encoder_InitTypeDef sEncoderConfig = {0}; sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 两通道都计数 sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING; // A相上升沿 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 = 0xFFFF; HAL_TIM_Encoder_Init(&htim3, &sEncoderConfig);EncoderMode 选 TIM_ENCODERMODE_TI12 意味着 A、B 两路信号的全部边沿都参与计数,一个完整正交周期 CNT 变化 4,这就是 4 倍频的由来。IC1Filter 和 IC2Filter 是输入采样滤波器,0x0F 代表用内部时钟做最长窗口采样,能吸收掉接触抖动和线束串扰带来的窄脉冲,代价是信号最高频率略有下降,对 E6B2 常规应用完全够用。Prescaler 保持 0,编码器模式下分频会直接损失计数精度,不要动它。
2.3 NPN 集电极开路与上拉:接线层的三个细节
E6B2-CWZ6C 这类型号的输出级是 NPN 集电极开路,输出管导通时把信号线拉到低电平,释放时靠外部上拉电阻把线拉回高电平。于是有三个接线细节直接决定驱动能不能稳定工作。
注意:3.3V 的 STM32 系统,上拉电阻接 3.3V,别接 24V。
上拉电阻取 3.3kΩ 到 10kΩ 之间。阻值太大,信号上升沿变缓,高速时边沿达不到 STM32 的 VIH 要求;太小则灌电流偏大。其次,编码器线缆的屏蔽层要单端接地,A/B/Z 三根信号线和电源线分开走,远离电机动力线,否则电机启动瞬间的共模干扰会直接串进定时器输入,表现就是转速偶尔跳一个大数。最后,先用示波器看 A/B 波形再进定时器——正常情况下 A、B 是干净的方波,边沿无振铃,相位差 90°,如果上升沿缓得像斜坡,先调上拉电阻而不是改代码。
3. 转速读取驱动代码:从 HAL 库定时器计数到物理转速
配置完成后,驱动要做的事情只有三件:启动编码器接口、周期读取 CNT、按公式换算转速。HAL 库使用的常见顺序是先 Init 再 Start,最后在回调里取数据。下面这段代码是完整的最小驱动骨架。
3.1 HAL 编码器驱动的最小结构:启动、采样、读取
uint16_t last_cnt = 0; void Speed_Init(void) { __HAL_TIM_SET_COUNTER(&htim3, 0); // 上电清零,避免随机初值 HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL); // 启动编码器接口 HAL_TIM_Base_Start_IT(&htim6); // 100ms 周期采样定时器 } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { uint16_t cur_cnt = __HAL_TIM_GET_COUNTER(&htim3); // 读当前计数值 Speed_Calculate(cur_cnt); } }HAL_TIM_Encoder_Start 的第二个参数传 TIM_CHANNEL_ALL,表示同时启用两路输入捕获,缺一个通道编码器不工作。上电先清零 CNT 是为了避免第一次读取拿到一个无意义的随机值。采样周期放在 TIM6 的更新中断里,100ms 一次,既不挤占 CPU,又能保证转速更新的实时性。
3.2 转速换算公式:为什么是 ΔCNT × 常数
先明确单位:编码器规格书上的 600P/R 是指每圈 600 个脉冲周期,4 倍频后每圈产生 2400 个计数。设采样周期为 T_s(秒),两次采样间计数器差值 ΔCNT,则:
- 圈数 = ΔCNT / (4 × PPR)
- 每秒圈数 = ΔCNT / (4 × PPR × T_s)
- 转速 rpm = ΔCNT × 60 / (4 × PPR × T_s)
以 600P/R、T_s = 0.1s 代入,常数为 60 / (4 × 600 × 0.1) = 0.25,即每个计数对应 0.25rpm。下面这张表列出常见分辨率下的换算常数,方便查。
| 分辨率 PPR | 采样周期 T_s | 每计数对应 rpm | 100rpm 时 ΔCNT |
|---|---|---|---|
| 200 | 0.1s | 0.75 | 133 |
| 600 | 0.1s | 0.25 | 400 |
| 1000 | 0.1s | 0.15 | 667 |
| 2000 | 0.1s | 0.075 | 1333 |
换算代码只有三行:
void Speed_Calculate(uint16_t cur_cnt) { int16_t delta_cnt = (int16_t)(cur_cnt - last_cnt); // 处理16位回绕 last_cnt = cur_cnt; float rpm = (float)delta_cnt * 60.0f / (4.0f * 600.0f * 0.1f); // 以 600P/R、100ms 采样为例,1 count = 0.25 rpm }delta_cnt 用 int16_t 接收差值。当计数器从 65535 正向回绕到 0 时,uint16 运算结果是 1,强转 int16 后仍然是 1;反向从 0 走到 65535 时结果是 -1。这个技巧让 16 位计数器的回绕在单次采样内不产生跳变,省去额外的溢出判断。
3.3 方向判断与 Z 相归零
反转时 delta_cnt 为负,rpm 自动为负,不需要额外处理。如果想单独知道方向,HAL 提供了现成的宏:
if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim3)) { // 当前正在反向计数 } else { // 当前正在正向计数 }这个宏读的是 TIM_CR1 的 DIR 位,硬件实时更新,比自己在中断里判断 A/B 电平可靠得多。Z 相一般接一个外部中断引脚,用于机械零点捕捉:转动到 Z 相脉冲时置一个标志位,主循环里决定是否清零 CNT,不要在中断里直接写计数器。
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == ZERO_PIN) { zero_flag = 1; // 通知主循环处理 } }4. STM32 转速读取的稳定性处理:溢出补偿、零速死区、低通滤波
公式和代码都对了,装到设备上还是会遇到三种常见现象:高速时转速突然变成巨大负值、停止时读数在 ±1rpm 附近跳、速度环响应看着很毛糙。这三个问题分别对应计数器溢出、停转抖动和滤波缺失,逐个处理。
4.1 16 位计数器溢出:高速场景的 65535 翻车
16 位定时器 CNT 在 0 到 65535 之间循环。正向转到 65535 后下一个脉冲回 0,反向转到 0 后下一个脉冲跳 65535。上一节用 int16_t 强转解决了单次采样内差值不大于 32767 的情况,但高速大采样周期时差值可能超过这个范围。例如 2000P/R、4 倍频、100ms 采样,3000rpm 时 ΔCNT = 40000,int16 装不下。
最省事的方案是换用 32 位定时器,F103 的 TIM2/TIM5 支持 32 位计数,ARR 设为 0xFFFFFFFF,再用 8 年也溢不出来。如果硬件已经固定用 TIM3/TIM4,就必须在更新中断里做溢出累计:
volatile int32_t overflow_count = 0; // 初始化用 Start_IT 替代 Start HAL_TIM_Encoder_Start_IT(&htim3, TIM_CHANNEL_ALL); void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { // 编码器模式下更新事件代表越过边界 if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim3)) { overflow_count--; // 反向越过边界 } else { overflow_count++; // 正向越过边界 } } } int32_t GetAbsCnt(uint16_t cur) { return ((int32_t)overflow_count << 16) + cur; }提示:编码器模式下更新事件并不只在正向溢出时发生,反向计数从 0 跳回 65535 同样会触发,所以必须在回调里读 DIR 位决定 overflow_count 加减方向,否则高速反转时会差出整整 65536 个计数。
4.2 停转抖动与零速死区
编码器停在一个临界位置时,A/B 信号在阈值附近抖动,CNT 会在相邻两个值之间来回跳。反映到转速上就是明明电机没转,读数却在 ±0.5rpm 附近波动。解决手段是零速死区:当 |ΔCNT| 小于某个阈值时,直接把转速视为 0。
#define ZERO_DEAD_CNT 4 // 100ms内不超过4个计数,约1rpm if (abs(delta_cnt) < ZERO_DEAD_CNT) { rpm = 0.0f; }阈值不是拍脑袋定的。先在电机静止时让程序连续打印 1 分钟的原始 delta_cnt,记下抖动最大值,然后把死区设成最大抖动值的 1.5 到 2 倍。上面代码里的 4 对应 600P/R、100ms 采样下约 1rpm 的死区,如果现场抖动更大,调到 8 或 10 都可以,代价是低速段会有一段"读数归零"的盲区。
4.3 低通滤波参数怎么选
死区处理了零速,但转动过程中单次采样的计数本身就有 ±1 个计数的量化噪声。100ms 采样、600P/R 时每个计数折合 0.25rpm,量化噪声约 0.25rpm,直接送给 PID 会让输出抖动。一阶低通滤波是性价比最高的做法:
speed_filt = alpha * rpm + (1.0f - alpha) * speed_filt;alpha 越大跟随越快、噪声越明显,alpha 越小越平滑、滞后越大。给闭环步进电机的速度环做反馈时,滤波造成的滞后会直接变成相位裕度的损失,不能为了读数好看把 alpha 压得太低。
| alpha | 近似时间常数 | 适用场景 |
|---|---|---|
| 0.5 | 约 0.2s | 对动态响应要求高的速度环 |
| 0.2 | 约 0.5s | 通用电机转速显示与反馈 |
| 0.05 | 约 2s | 远程监控、数值稳定优先 |
滤波的先后顺序也有讲究:先做溢出补偿,再做死区判断,最后做滤波。顺序反了会导致死区过滤后的零点在滤波输出端被重新拉高,出现"明明置零了,读数还在缓慢回落"的怪现象。
5. 信号发生器校准技巧:验证 E6B2 转速读取精度
驱动写完,第一件事不是直接上电机,而是用信号发生器做定频校准。这能把编码器、接线和转速公式三者的误差分离开,任何一步有问题都会在对比数据里暴露出来。
5.1 用信号发生器模拟 A/B 相定频校准
把信号发生器的两路输出分别接到 PA6 和 PA7,A 路输出方波,B 路输出同频方波但相位延迟四分之一周期——模拟编码器正转。E6B2 的 NPN 输出和信号发生器的推挽输出电平特性不同,但进入定时器后都是高/低电平,采样逻辑完全一致。
| A 相频率 | 等效转速(600P/R) | 100ms 期望 ΔCNT |
|---|---|---|
| 10 Hz | 1 rpm | 4 |
| 100 Hz | 10 rpm | 40 |
| 1 kHz | 100 rpm | 400 |
| 4 kHz | 400 rpm | 1600 |
校准前把代码里的死区和滤波临时关掉,打印原始 delta_cnt。以 1kHz 为例,每 100ms 应读到 400 个计数,允许 ±1 个计数的波动。如果偏差超过 1%,优先怀疑信号发生器的频率精度或相位差设置,其次查上拉电阻和输入滤波配置。测完正转再交换 A/B 相序测反转,delta_cnt 应为负值,绝对值与正转一致。
5.2 三个检查点:电平、相位、毛刺
校准数据对不上时,按下面三个检查点逐个排查。
第一是电平。用示波器量 PA6/PA7 引脚,高电平要大于 0.7 × VDD,低电平要小于 0.3 × VDD。如果高电平只有 2V,检查上拉是否接对,以及编码器供电是否正常。第二是相位。A 超前 B 90° 是正转,滞后 90° 是反转;相位差偏离超过 5° 会导致部分边沿被方向判断为抖动,计数偏少。第三是毛刺。电机启动瞬间线束上容易出现振铃,若示波器看到边沿附近有超过 1 个周期的回跳,把 IC1Filter 往大调,或者在编码器输出端加一级 RC 滤波。先把信号发生器的频率从低到高扫一遍,记录每个频点的 ΔCNT 误差,误差超过一个计数就回头查电平和相位,直到高转速不丢脉冲、低转速不抖动,这套驱动就算真正落地了。
本文还有配套的精品资源,点击获取