FreeRTOS+STM32下旋转编码器中断丢失致角度计算异常的解决问询
问题描述
我有一台持续旋转的伺服电机,连接了用于计算精确旋转角度的旋转编码器,需求是电机旋转90度后停止。目前通过中断信号计算旋转编码器的角度,但有时会出现中断调用丢失的情况,导致角度计算错误。推测是中断发生时伺服电机相关进程运行所致,手动旋转编码器时角度显示始终正确。请问是否有办法对中断进行排队以避免丢失?我使用的是FreeRTOS和STM32。
现有代码
中断处理程序伪代码
volatile int rotationAngle = 0; void handleRotaryEncoderInterrupt(uint8_t index) { GPIO_PinState clk = HAL_GPIO_ReadPin(clkPort, clkPin); GPIO_PinState dt = HAL_GPIO_ReadPin(dtPort, dtPin); if(clk == GPIO_PIN_SET && dt == GPIO_PIN_RESET) { rotationCount++; rotationAngle = rotationCount * STEPS_TO_ANGLE; } }
伺服控制任务伪代码
void myTask() { while(1) { myServo.duty = MOVE_FORWARD; while(rotationAngle % 90 != 0) { osDelay(100); } myServo.duty = STOP; osDelay(10000); } }
解决方法
针对中断丢失的问题,结合FreeRTOS和STM32的特性,可通过以下方式处理:
1. 调整中断优先级,避免被抢占
- 在STM32中,将旋转编码器的中断优先级设置为高于伺服电机控制相关的任务和中断(比如PWM输出中断)。注意FreeRTOS的
configMAX_SYSCALL_INTERRUPT_PRIORITY宏定义了可调用FreeRTOS API的最高中断优先级,若后续要在ISR中使用FreeRTOS接口,需保证编码器中断优先级不低于该值。 - 使用HAL库的
HAL_NVIC_SetPriority()函数配置编码器中断的抢占优先级和子优先级,确保它能打断伺服相关进程。
2. 用FreeRTOS队列实现中断事件排队
核心思路是让ISR只做最短必要操作,把编码器状态变化事件发送到队列,由专门任务处理角度计算,彻底避免ISR被阻塞导致的中断丢失。
- 修改后的ISR示例:
#include "FreeRTOS.h" #include "queue.h" QueueHandle_t xEncoderQueue; void handleRotaryEncoderInterrupt(uint8_t index) { GPIO_PinState clk = HAL_GPIO_ReadPin(clkPort, clkPin); GPIO_PinState dt = HAL_GPIO_ReadPin(dtPort, dtPin); // 打包CLK和DT状态为一个整数发送到队列 uint16_t encoderState = ((uint16_t)clk << 8) | dt; BaseType_t xHigherPriorityTaskWoken = pdFALSE; // ISR中必须使用FromISR版本的队列API xQueueSendFromISR(xEncoderQueue, &encoderState, &xHigherPriorityTaskWoken); // 若唤醒了更高优先级任务,触发上下文切换 portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }
- 新增编码器处理任务:
volatile int rotationCount = 0; volatile int rotationAngle = 0; void vEncoderProcessingTask(void *pvParameters) { uint16_t encoderState; GPIO_PinState lastClk = GPIO_PIN_RESET; while(1) { // 阻塞等待队列中的编码器事件 if(xQueueReceive(xEncoderQueue, &encoderState, portMAX_DELAY) == pdTRUE) { GPIO_PinState clk = (GPIO_PinState)(encoderState >> 8); GPIO_PinState dt = (GPIO_PinState)(encoderState & 0xFF); // 采用边沿检测的可靠解码逻辑(原代码仅检测电平,易漏步) if(lastClk == GPIO_PIN_RESET && clk == GPIO_PIN_SET) { if(dt == GPIO_PIN_RESET) { rotationCount++; } else { // 如需支持反转,添加减计数逻辑 // rotationCount--; } rotationAngle = rotationCount * STEPS_TO_ANGLE; } lastClk = clk; } } }
- 初始化队列和任务:
// 创建可存储10个事件的队列,数量可根据编码器转速调整 xEncoderQueue = xQueueCreate(10, sizeof(uint16_t)); if(xEncoderQueue != NULL) { // 给编码器任务设置比伺服任务更高的优先级 xTaskCreate(vEncoderProcessingTask, "EncoderTask", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, NULL); }
3. 优化ISR执行时间
- 原ISR中直接计算角度,若伺服相关中断优先级更高,会打断编码器ISR。需尽量缩短ISR执行时间,只做状态采集和事件发送,把复杂计算放到任务中。
- 避免在ISR中调用耗时函数,比如HAL库中非原子操作的接口,确保ISR能快速退出。
4. 改进伺服任务的触发逻辑
原任务用while循环加osDelay(100)轮询角度,存在100ms延迟,且易因中断丢失错过停止时机。改用FreeRTOS事件组实现精准通知:
- 修改后的伺服任务:
EventGroupHandle_t xAngleEventGroup; #define ANGLE_REACHED_BIT (1 << 0) void myTask() { while(1) { myServo.duty = MOVE_FORWARD; // 阻塞等待角度达标事件 xEventGroupWaitBits(xAngleEventGroup, ANGLE_REACHED_BIT, pdTRUE, pdFALSE, portMAX_DELAY); myServo.duty = STOP; osDelay(10000); } }
- 在编码器处理任务中添加事件触发逻辑:
void vEncoderProcessingTask(void *pvParameters) { uint16_t encoderState; GPIO_PinState lastClk = GPIO_PIN_RESET; int lastAngle = -1; while(1) { if(xQueueReceive(xEncoderQueue, &encoderState, portMAX_DELAY) == pdTRUE) { // 解码逻辑同上... rotationAngle = rotationCount * STEPS_TO_ANGLE; // 仅当角度首次达到90倍数时发送事件,避免重复触发 if(rotationAngle % 90 == 0 && rotationAngle != lastAngle) { xEventGroupSetBits(xAngleEventGroup, ANGLE_REACHED_BIT); lastAngle = rotationAngle; } } } }
- 初始化事件组:
xAngleEventGroup = xEventGroupCreate();
内容的提问来源于stack exchange,提问作者Davithbul
相关产品推荐
相关产品推荐

