EC11旋转编码器是工业控制领域最常见的增量式编码器之一,它的机械结构设计非常巧妙。我拆解过多个不同品牌的EC11,发现内部结构大同小异——核心部件是一个带有导电触点的旋转圆盘和两个弹性接触片。当你旋转旋钮时,触点会与A、B相位的接触片交替接通,产生特定的脉冲序列。
实际测量波形时,用示波器可以清晰看到:顺时针旋转时,A相位的上升沿总是领先B相位90度;逆时针时则相反,B相位会领先A相位90度。这种相位差关系是方向判断的关键。我在早期项目中犯过一个错误:没有给编码器引脚加上10kΩ上拉电阻,导致信号边沿不清晰,经常误触发。后来加了RC滤波(典型值1kΩ+0.1μF)后稳定性大幅提升。
EC11的机械特性也值得注意:
STM32的定时器编码器模式简直就是为旋转编码器量身定制的功能。我对比过用外部中断和GPIO轮询的方案,发现定时器编码器模式有三大优势:
以TIM2为例,其编码器模式工作原理是这样的:
这里有个关键点容易被忽略:ARR寄存器值需要根据应用场景合理设置。比如做音量控制时,我通常设为100,这样计数值可以直接映射为音量百分比。而在需要高精度定位的场景,则建议使用16位最大值65535。
下面是我在多个项目中验证过的完整配置流程,使用STM32CubeMX+HAL库:
c复制/* EC11引脚定义 */
#define ENC_A_GPIO_Port GPIOA
#define ENC_A_Pin GPIO_PIN_0 // TIM2_CH1
#define ENC_B_GPIO_Port GPIOA
#define ENC_B_Pin GPIO_PIN_1 // TIM2_CH2
c复制TIM_HandleTypeDef htim2;
TIM_Encoder_InitTypeDef sEncoderConfig;
void MX_TIM2_Init(void)
{
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 100; // 音量范围0-100
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12;
sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
sEncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sEncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1;
sEncoderConfig.IC1Filter = 6; // 适当滤波消除抖动
sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
sEncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
sEncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1;
sEncoderConfig.IC2Filter = 6;
if (HAL_TIM_Encoder_Init(&htim2, &sEncoderConfig) != HAL_OK)
{
Error_Handler();
}
HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);
}
c复制void Update_Encoder_Value(void)
{
static int16_t last_count = 0;
int16_t current_count = (int16_t)TIM2->CNT;
if(current_count > last_count) {
printf("顺时针旋转,当前值:%d\n", current_count);
}
else if(current_count < last_count) {
printf("逆时针旋转,当前值:%d\n", current_count);
}
last_count = current_count;
// 处理计数器溢出
if(current_count >= 100) {
TIM2->CNT = 100;
}
else if(current_count <= 0) {
TIM2->CNT = 0;
}
}
在实际项目中,我遇到过几个典型问题:
问题1:计数值跳变不稳定
问题2:快速旋转时漏计数
问题3:上电初始值不准
c复制__HAL_TIM_SET_COUNTER(&htim2, 50); // 初始设为中间值
对于需要更高精度的场景,可以采用以下优化方案:
在智能家居项目中,我发现EC11的机械特性可以被创造性利用:
长按功能实现
c复制if(HAL_GPIO_ReadPin(ENC_SW_GPIO_Port, ENC_SW_Pin) == GPIO_PIN_RESET) {
uint32_t press_time = HAL_GetTick();
while(HAL_GPIO_ReadPin(ENC_SW_GPIO_Port, ENC_SW_Pin) == GPIO_PIN_RESET);
if(HAL_GetTick() - press_time > 1000) {
printf("长按触发\n");
}
}
加速滚动检测
通过检测两次旋转的时间间隔,可以实现类似鼠标滚轮的加速效果:
c复制uint32_t last_tick = 0;
float speed_factor = 1.0f;
void Handle_Encoder_Rotation(void)
{
uint32_t current_tick = HAL_GetTick();
uint32_t interval = current_tick - last_tick;
if(interval < 50) { // 快速旋转
speed_factor = 3.0f;
} else {
speed_factor = 1.0f;
}
// 应用加速因子
int16_t delta = (current_count - last_count) * speed_factor;
last_tick = current_tick;
}
经过多个项目验证,这些优化措施效果显著:
c复制// 配置DMA将TIM2->CNT传输到内存变量
hdma_tim2_up.Instance = DMA1_Stream1;
hdma_tim2_up.Init.Channel = DMA_CHANNEL_3;
hdma_tim2_up.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_tim2_up.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tim2_up.Init.MemInc = DMA_MINC_ENABLE;
hdma_tim2_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_tim2_up.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_tim2_up.Init.Mode = DMA_CIRCULAR;
hdma_tim2_up.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_tim2_up);
__HAL_LINKDMA(&htim2, hdma[TIM_DMA_ID_UPDATE], hdma_tim2_up);
HAL_TIM_Base_Start_DMA(&htim2, (uint32_t*)&encoder_value, 1);
c复制// 配置唤醒中断
HAL_GPIO_Init(ENC_A_GPIO_Port, &GPIO_InitStruct);
HAL_GPIO_Init(ENC_B_GPIO_Port, &GPIO_InitStruct);
__HAL_GPIO_EXTI_CLEAR_IT(ENC_A_Pin | ENC_B_Pin);
HAL_NVIC_SetPriority(EXTI0_1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_1_IRQn);