STM32F103CBT6搭配EC11旋转编码器输入不可靠问题求助
问题描述
基于STM32F103CBT6设计HID游戏手柄PCB,配置16MHz晶振、18个按键、4个EC11旋转编码器。按键功能正常,但编码器存在以下问题:
- 部分旋转动作未被识别
- 偶尔出现引脚跳变
- 甚至双引脚同时触发输入
编码器接线:中间引脚接地,另外两引脚接MCU引脚:
- 编码器1:PB8/PB9
- 编码器2:PB10/PB11
- 编码器3:PB12/PB13
- 编码器4:PB14/PB15
所有引脚配置为带内部上拉的GPIO输入。
尝试过两种方案均效果不佳:
- 轮询方式读取编码器状态(代码见下方),识别效果差
- 单编码器中断方案(仅实现单个编码器),设一个引脚为下降沿中断,另一个为GPIO输入,仍存在旋转时输入跳变问题
原轮询代码
#define ROT_PINS 8 #define ROT_BUTTONS 8 GPIO_TypeDef* rotary_ports[ROT_BUTTONS] = {GPIOB, GPIOB, GPIOB, GPIOB, GPIOB, GPIOB, GPIOB, GPIOB}; uint16_t rotary_pins[ROT_BUTTONS] = {GPIO_PIN_8, GPIO_PIN_9, GPIO_PIN_10, GPIO_PIN_11, GPIO_PIN_12, GPIO_PIN_13, GPIO_PIN_14, GPIO_PIN_15}; struct ButtonReport { uint8_t report_id; uint32_t buttons; } __attribute__((packed)); struct ButtonReport buttonReport; void SCANALL(void) { uint8_t targetRow, targetCol; // Clear the buttonReport struct memset(&buttonReport, 0, sizeof(buttonReport)); // Scan encoders (knoppen 1-8) for (int i = 0; i < 8; i++) { uint8_t rotaryStatus = HAL_GPIO_ReadPin(rotary_ports[i], rotary_pins[i]); if (rotaryStatus == GPIO_PIN_RESET) { buttonReport.buttons |= (1 << i); } else { buttonReport.buttons &= ~(1 << i); } } // Stuur het rapport met de knopstatussen buttonReport.report_id = 1; // Stel het rapport-ID in als dat nodig is USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t*)&buttonReport, sizeof(buttonReport)); }
原中断代码
struct ButtonReport { uint8_t report_id; uint32_t buttons; } __attribute__((packed)); struct ButtonReport buttonReport; int encoderState = 0; int lastEncoderState = 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_8 || GPIO_Pin == GPIO_PIN_9) { encoderState = (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) << 1) | HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_9); int encoderChange = lastEncoderState - encoderState; // Clear button status initially buttonReport.buttons = 0; if (encoderChange == 1 || encoderChange == -3) { // Clockwise rotation - Simulate Button 1 press buttonReport.report_id = 1; // Set the report ID (if required) buttonReport.buttons |= 0x01; // Set Button 1 as pressed } else if (encoderChange == -1 || encoderChange == 3) { // Counterclockwise rotation - Simulate Button 2 press buttonReport.report_id = 1; // Set the report ID (if required) buttonReport.buttons |= 0x02; // Set Button 2 as pressed } lastEncoderState = encoderState; // Send the HID report to the PC USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t*)&buttonReport, sizeof(buttonReport)); } } static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; /* USER CODE BEGIN MX_GPIO_Init_1 */ /* USER CODE END MX_GPIO_Init_1 */ /* GPIO Ports Clock Enable */ __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /*Configure GPIO pins : PB10 PB12 PB14 PB8 */ GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_12|GPIO_PIN_14|GPIO_PIN_8; GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); /*Configure GPIO pins : PB11 PB13 PB15 PB9 */ GPIO_InitStruct.Pin = GPIO_PIN_11|GPIO_PIN_13|GPIO_PIN_15|GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); /* EXTI interrupt init*/ HAL_NVIC_SetPriority(EXTI9_5_IRQn, 0, 0); HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0); HAL_NVIC_EnableIRQ(EXTI15_10_IRQn); /* USER CODE BEGIN MX_GPIO_Init_2 */ /* USER CODE END MX_GPIO_Init_2 */ }
解决建议
硬件层面优化
- 增加硬件滤波电路:EC11编码器的A、B输出引脚与GND之间并联100nF陶瓷电容,或在引脚到MCU之间串联1kΩ电阻+100nF电容组成RC滤波,抑制机械抖动和电磁干扰导致的信号跳变。
- 优化PCB布线:编码器走线尽量短,远离电源、晶振等干扰源;避免A、B引脚走线平行过长,减少串扰;采用大面积地平面,降低接地阻抗。
- 排查编码器硬件质量:更换不同批次/品牌的EC11编码器测试,排除机械触点磨损、内部结构不良导致的信号异常。
软件层面优化
1. 轮询方案改进(适合低转速场景)
原轮询代码错误地将编码器的A/B引脚当作独立按键处理,未利用相位关系识别旋转,也无消抖逻辑。改进方向:
- 同时读取每个编码器的A/B引脚状态,通过相位变化判断旋转方向
- 增加软件消抖,连续多次读取状态一致才判定有效
- 设置5-10ms的轮询周期,平衡响应速度与抗干扰能力
改进后的轮询示例代码:
#define ENCODER_COUNT 4 typedef struct { GPIO_TypeDef* portA; uint16_t pinA; GPIO_TypeDef* portB; uint16_t pinB; uint8_t lastState; int16_t count; } Encoder_t; Encoder_t encoders[ENCODER_COUNT] = { {GPIOB, GPIO_PIN_8, GPIOB, GPIO_PIN_9, 0, 0}, {GPIOB, GPIO_PIN_10, GPIOB, GPIO_PIN_11, 0, 0}, {GPIOB, GPIO_PIN_12, GPIOB, GPIO_PIN_13, 0, 0}, {GPIOB, GPIO_PIN_14, GPIOB, GPIO_PIN_15, 0, 0} }; struct ButtonReport buttonReport; void SCAN_ENCODERS(void) { memset(&buttonReport, 0, sizeof(buttonReport)); buttonReport.report_id = 1; for(int i=0; i<ENCODER_COUNT; i++){ uint8_t currA = HAL_GPIO_ReadPin(encoders[i].portA, encoders[i].pinA); uint8_t currB = HAL_GPIO_ReadPin(encoders[i].portB, encoders[i].pinB); uint8_t currState = (currA << 1) | currB; if(currState != encoders[i].lastState){ HAL_Delay(1); // 1ms消抖延时 currA = HAL_GPIO_ReadPin(encoders[i].portA, encoders[i].pinA); currB = HAL_GPIO_ReadPin(encoders[i].portB, encoders[i].pinB); currState = (currA << 1) | currB; if(currState == encoders[i].lastState) continue; uint8_t transition = (encoders[i].lastState << 2) | currState; switch(transition){ case 0b0001: case 0b0111: case 0b1110: case 0b1000: encoders[i].count++; buttonReport.buttons |= (1 << (i*2)); break; case 0b0010: case 0b1011: case 0b1101: case 0b0100: encoders[i].count--; buttonReport.buttons |= (1 << (i*2 + 1)); break; default: break; } encoders[i].lastState = currState; } } USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t*)&buttonReport, sizeof(buttonReport)); }
2. 中断方案改进
原中断代码仅监听单引脚下降沿,易丢失相位变化,且未做消抖处理。改进方向:
- 所有编码器A/B引脚设置为双边沿中断,确保捕获全部相位变化
- 添加时间戳消抖,过滤间隔小于5ms的重复中断
- 中断中仅更新编码器状态,HID报告发送放在主循环或定时器中断中处理,避免中断阻塞
- 使用状态机跟踪A/B相位变化,准确识别旋转方向
改进后的中断示例代码:
#define ENCODER_COUNT 4 typedef struct { uint8_t lastState; int16_t count; uint32_t lastIntTime; } Encoder_t; Encoder_t encoders[ENCODER_COUNT] = {0}; struct ButtonReport buttonReport; // 初始化1ms定时器用于HID报告更新 void MX_TIM2_Init(void) { TIM_HandleTypeDef htim2; htim2.Instance = TIM2; htim2.Init.Prescaler = 16000-1; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 1-1; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim2); HAL_TIM_Base_Start_IT(&htim2); } // 定时器回调:定期发送HID报告 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM2){ memset(&buttonReport, 0, sizeof(buttonReport)); buttonReport.report_id = 1; for(int i=0; i<ENCODER_COUNT; i++){ if(encoders[i].count > 0){ buttonReport.buttons |= (1 << (i*2)); encoders[i].count--; }else if(encoders[i].count < 0){ buttonReport.buttons |= (1 << (i*2 + 1)); encoders[i].count++; } } USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t*)&buttonReport, sizeof(buttonReport)); } } // EXTI中断回调 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { uint32_t currTime = HAL_GetTick(); Encoder_t* currEncoder = NULL; uint8_t currA, currB, currState; switch(GPIO_Pin){ case GPIO_PIN_8: case GPIO_PIN_9: currEncoder = &encoders[0]; break; case GPIO_PIN_10: case GPIO_PIN_11: currEncoder = &encoders[1]; break; case GPIO_PIN_12: case GPIO_PIN_13: currEncoder = &encoders[2]; break; case GPIO_PIN_14: case GPIO_PIN_15: currEncoder = &encoders[3]; break; default: return; } if(currTime - currEncoder->lastIntTime < 5) return; currEncoder->lastIntTime = currTime; if(GPIO_Pin % 2 == 0){ currA = HAL_GPIO_ReadPin(GPIOB, GPIO_Pin); currB = HAL_GPIO_ReadPin(GPIOB, GPIO_Pin + 1); }else{ currB = HAL_GPIO_ReadPin(GPIOB, GPIO_Pin); currA = HAL_GPIO_ReadPin(GPIOB, GPIO_Pin - 1); } currState = (currA << 1) | currB; uint8_t transition = (currEncoder->lastState << 2) | currState; switch(transition){ case 0b0001: case 0b0111: case 0b1110: case 0b1000: currEncoder->count++; break; case 0b0010: case 0b1011: case 0b1101: case 0b0100: currEncoder->count--; break; default: break; } currEncoder->lastState = currState; } // 修改GPIO初始化:所有编码器引脚设为双边沿中断 static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15; GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); HAL_NVIC_SetPriority(EXTI9_5_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); HAL_NVIC_SetPriority(EXTI15_10_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI15_10_IRQn); }
3. 其他优化点
- HID报告优化:将编码器映射为HID轴(如滚轮轴、X/Y轴),而非模拟按键,能更平滑处理连续旋转,避免按键触发频率限制。
- 调试辅助:添加串口打印,输出编码器的A/B状态、旋转计数等信息,快速定位跳变或漏识别的具体原因。
内容的提问来源于stack exchange,提问作者Djowwie
相关产品推荐
相关产品推荐

