5分钟学习笔记(FreeRTOS)(六)
1.任务通知比信号量轻量主要是因为任务通知是直接通知某一个任务不需要额外创建队列、信号量这类中间内核对象。使用信号量时需要先创建 SemaphoreHandle_t 对应的信号量对象。中断或任务通过 Give 信号量使等待该信号量的任务解除阻塞。这个过程中信号量是一个独立的同步对象。而任务通知是任务本身自带的通知机制。每个任务在创建后其任务控制块 TCB 中就包含任务通知值。其他任务或中断可以直接向目标任务发送通知目标任务通过 ulTaskNotifyTake() 或 xTaskNotifyWait() 等 API 阻塞等待通知。因此在一对一通知场景下例如 ADC DMA 完成通知 AdcTask、USART IDLE 中断通知 UartRxTask任务通知通常比二值信号量更省 RAM也更快。FreeRTOS 官方文档也说明直接任务通知解除阻塞任务比使用二值信号量解除阻塞任务快 45%并且使用更少 RAM。但是任务通知主要适合一对一通知。如果多个任务等待同一个同步对象或者需要一个公共同步对象二值信号量仍然更合适。2.ulTaskNotifyTake()ulTaskNotifyTake() 用于让任务等待自己的任务通知值常用来把任务通知当作更轻量的二值信号量或计数信号量使用。当任务通知值为 0 时如果调用 ulTaskNotifyTake() 并设置了阻塞时间当前任务会进入 Blocked 状态等待其他任务或中断发送通知。 当其他任务调用 xTaskNotifyGive()或中断调用 vTaskNotifyGiveFromISR() 后目标任务的通知值会增加。如果通知值大于 0阻塞等待的任务会解除阻塞。uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);ulTaskNotifyTake() 的第一个参数决定退出函数时如何处理通知值。如果参数为 pdTRUE退出时将通知值清零这种方式类似二值信号量适合只关心事件是否发生的场景。xClearCountOnExit pdTRUEulTaskNotifyTake(pdTRUE, portMAX_DELAY);//任务等到通知后退出函数时把通知值清零。这就像二值信号量。比如中断连续通知了几次任务醒来后只处理一次通知值 3 ulTaskNotifyTake(pdTRUE, ...) 退出后通知值 0例如USART IDLE 中断通知 UartRxTask ADC DMA 完成通知 AdcTask 按键中断通知 KeyTask如果参数为 pdFALSE退出时将通知值减 1这种方式类似计数信号量适合统计事件发生次数的场景。xClearCountOnExit pdFALSEulTaskNotifyTake(pdFALSE, portMAX_DELAY);//任务等到通知后退出函数时通知值减 1。这就像计数信号量。比如中断连续通知了 3 次通知值 3任务调用一次ulTaskNotifyTake(pdFALSE, portMAX_DELAY);退出后通知值 2再调用一次通知值 1再调用一次通知值 0ulTaskNotifyTake() 的第二个参数是阻塞等待时间单位是 tick。如果等待超时仍没有通知函数返回 0。3.示例app_adc_dma_notify.h#ifndef __APP_ADC_DMA_NOTIFY_H #define __APP_ADC_DMA_NOTIFY_H #ifdef __cplusplus extern C { #endif #include stdint.h /* 如果你只采 1 路 ADC ADC_CHANNEL_NUM 改成 1 如果你采 3 路 ADC ADC_CHANNEL_NUM 3 */ #define ADC_CHANNEL_NUM 3 #define ADC_SAMPLE_PER_CH 16 #define ADC_DMA_BUF_LEN (ADC_CHANNEL_NUM * ADC_SAMPLE_PER_CH) void App_ADC_DMA_TaskCreate(void); uint16_t App_ADC_GetChAvg(uint8_t ch); #ifdef __cplusplus } #endif #endifapp_adc_dma_notify.c#include app_adc_dma_notify.h #include FreeRTOS.h #include task.h #include main.h #include adc.h #include stdint.h static uint16_t g_adc_dma_buf[ADC_DMA_BUF_LEN]; /* ADC任务句柄。 */ static TaskHandle_t g_adc_task_handle NULL; volatile uint32_t g_adc_dma_cplt_count 0; volatile uint32_t g_adc_notify_count 0; volatile uint32_t g_adc_process_count 0; static void AdcTask(void *argument); static void App_ADC_DMA_Start(void); static void Adc_DMA_DataProcess(void); /* 创建 ADC 处理任务 */ void App_ADC_DMA_TaskCreate(void) { BaseType_t ret; ret xTaskCreate(AdcTask, AdcTask, 256, NULL, 3, g_adc_task_handle); if(ret ! pdPASS) { Error_Handler(); } } /* 启动 ADC DMA */ static void App_ADC_DMA_Start(void) { if(HAL_ADC_Start_DMA(hadc1, (uint32_t *)g_adc_dma_buf, ADC_DMA_BUF_LEN) ! HAL_OK) { Error_Handler(); } } /* ADC 任务第一次进入任务时启动 ADC DMA。后面一直等待 DMA 完成通知。 */ static void AdcTask(void *argument) { (void)argument; /* 任务启动后再启动 ADC DMA。这样可以保证任务句柄已经有效调度器也已经开始运行。 */ App_ADC_DMA_Start(); while(1) { /* 等待 DMA 完成通知。 pdTRUE 收到通知后把通知值清零。 类似二值信号量用法。 portMAX_DELAY 没有通知就一直阻塞等待。 */ ulTaskNotifyTake(pdTRUE, portMAX_DELAY); /* 能执行到这里说明 DMA 完成中断通知了本任务。 */ Adc_DMA_DataProcess(); g_adc_process_count; } } /* ADC DMA 数据处理 */ static void Adc_DMA_DataProcess(void) { //处理函数 } /* ADC DMA 传输完成回调函数。 这个函数运行在中断上下文。 中断里不要做大量数据处理只通知任务。 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { BaseType_t xHigherPriorityTaskWoken pdFALSE; if(hadc-Instance ADC1) { g_adc_dma_cplt_count; if(g_adc_task_handle ! NULL) { vTaskNotifyGiveFromISR(g_adc_task_handle, xHigherPriorityTaskWoken); g_adc_notify_count; } /* 如果本次通知唤醒了更高优先级任务 中断退出后立即进行一次任务切换。 */ portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }4.总结任务通知适合 1. ADC DMA 完成通知 AdcTask 2. USART IDLE 中断通知 UartRxTask 3. CAN 接收中断通知 CanRxTask 4. 定时器中断通知 ControlTask 5. 按键中断通知 KeyTask 任务通知不适合 1. 多个任务等待同一个同步对象 2. 需要传递具体数据内容 3. 保护共享资源 4. 等待多个复杂条件组合然后对应选择一对一唤醒任务通知 传数据队列 保护资源Mutex 多个事件条件事件组 资源计数计数信号量