——队列与中断)
上一节学习了xQueueSend()和xQueueReceive()实现了两个任务之间的数据传递。实际STM32程序中很多数据并不是由任务主动产生而是来自按键中断、串口接收中断、定时器中断等。中断发生后我们通常希望快速记录数据然后交给任务完成后续处理。这一节学习如何在中断服务函数中使用FreeRTOS队列。一、中断中为什么不能直接使用xQueueSend()任务中可以这样发送队列xQueueSend( DataQueue, data, pdMS_TO_TICKS(100) );但是在中断服务函数中不能直接这样使用。原因很简单xQueueSend()可能因为队列已满而让当前任务进入阻塞态。而中断服务函数不是任务没有运行态 → 阻塞态这样的任务状态转换也不能在中断里面等待100ms。因此FreeRTOS专门提供了中断版本xQueueSendFromISR();带有FromISR后缀的函数就是专门给中断服务函数使用的接口。二、xQueueSendFromISR()函数可以简单理解为BaseType_t xQueueSendFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );第一个参数xQueue表示队列句柄。第二个参数pvItemToQueue表示需要发送的数据地址。第三个参数pxHigherPriorityTaskWoken用于判断发送数据以后是否唤醒了一个优先级更高的任务。这个参数是中断中使用队列时需要重点理解的地方。三、创建一个按键消息队列首先定义QueueHandle_t KeyQueue;然后创建队列KeyQueue xQueueCreate( 5, sizeof(uint8_t) );这里创建一个长度为5的队列每条消息都是uint8_t类型。之后创建按键处理任务xTaskCreate( KeyTask, KeyTask, 128, NULL, 2, NULL );这个任务专门负责等待按键中断发送过来的数据。四、编写按键中断假设STM32F407的某个按键通过外部中断触发。中断服务函数可以写成void EXTI0_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; uint8_t KeyValue 1; if(EXTI_GetITStatus(EXTI_Line0) ! RESET) { xQueueSendFromISR( KeyQueue, KeyValue, xHigherPriorityTaskWoken ); EXTI_ClearITPendingBit(EXTI_Line0); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }按键触发以后进入EXTI中断 ↓ KeyValue 1 ↓ 发送到KeyQueue ↓ 退出中断这样中断只负责产生按键消息不负责执行复杂的按键处理程序。五、pxHigherPriorityTaskWoken是什么中断开始时先定义BaseType_t xHigherPriorityTaskWoken pdFALSE;假设此时KeyTask正在xQueueReceive();中等待按键数据因此处于阻塞态。按键中断发生后执行xQueueSendFromISR();队列中出现数据等待这个队列的KeyTask就可能从阻塞态变成就绪态如果被唤醒的任务优先级高于当前被中断的任务FreeRTOS会把xHigherPriorityTaskWoken设置为pdTRUE这说明现在已经有一个更高优先级的任务可以运行了。六、portYIELD_FROM_ISR()有什么用中断最后执行portYIELD_FROM_ISR(xHigherPriorityTaskWoken);如果xHigherPriorityTaskWoken pdTRUE那么退出中断以后就可以立即切换到刚刚被唤醒的高优先级任务。过程可以理解为普通任务运行 ↓ 按键中断发生 ↓ 发送队列数据 ↓ KeyTask被唤醒 ↓ 发现KeyTask优先级更高 ↓ 退出中断 ↓ 立即运行KeyTask这样就提高了系统响应速度。如果没有更高优先级任务被唤醒xHigherPriorityTaskWoken pdFALSE则不会因为这次操作产生额外的任务切换。七、按键处理任务任务中继续使用普通的xQueueReceive();例如void KeyTask(void *pvParameters) { uint8_t KeyValue; while(1) { if(xQueueReceive( KeyQueue, KeyValue, portMAX_DELAY ) pdPASS) { if(KeyValue 1) { GPIO_ToggleBits( GPIOE, GPIO_Pin_3 ); printf(Key Press\r\n); } } } }没有按键时KeyQueue为空 ↓ KeyTask阻塞不会一直查询按键。按键中断发生EXTI中断 ↓ xQueueSendFromISR() ↓ KeyQueue收到数据 ↓ KeyTask被唤醒 ↓ 处理按键这就是非常典型的中断产生事件任务处理事件。八、为什么不直接在中断里处理例如按键中断后需要printf();控制LED处理数据发送通信数据。如果全部放在中断服务函数中中断执行时间就会很长。这可能影响其他中断以及系统实时性。更合理的方法是中断 ↓ 快速获取数据 ↓ 发送队列 ↓ 退出中断 ↓ 任务处理复杂工作因此实际FreeRTOS工程中一般要求中断尽量短复杂处理交给任务。九、中断中使用队列的注意事项首先中断中不能使用普通xQueueSend();应该使用xQueueSendFromISR();类似地中断中还有xQueueReceiveFromISR();等专门的ISR接口。其次中断中不能使用vTaskDelay();因为中断不是任务不能进入阻塞态。最后使用FreeRTOS的FromISR接口时还需要正确配置STM32的NVIC中断优先级。并不是任意中断优先级都可以调用FreeRTOS API。关于FreeRTOS中断优先级和ISR安全API的具体规则后面学习中断安全API时再详细介绍。十、总结这一节学习了FreeRTOS队列在中断中的基本使用方法。任务中发送队列使用xQueueSend();中断中则使用xQueueSendFromISR();通过xHigherPriorityTaskWoken和portYIELD_FROM_ISR()还可以在中断唤醒更高优先级任务后及时进行任务切换。实际工程中推荐采用中断快速获取数据 ↓ 队列 ↓ 任务完成复杂处理这种方式既能够发挥中断响应速度快的特点又能避免在中断服务函数中执行大量耗时操作。