在我们之前的学习中,队列,信号量,互斥量,事件组,任务通知,它们都有两套函数,在任务中使用或在中断中使用。
1.为什么要用两套函数?
情景1:
我们在写队列的时候等待100个Tick那这个任务就一定只执行了等待了100个Tick吗?并不一定!
我们假设一个场景,任务A写队列等待100个Tick,如果写成功就会唤醒任务B。假设任务B的优先级高于任务A,如果这个B不阻塞,那这个A就再也无法执行。
在任务里写了队列如果有高优先级任务会立马唤醒并切换!
因此在任务里写队列我们一定要考虑到众多影响程序运行的因素!
情景2:
我们在中断中写队列,没有使用中断API函数使用任务函数,成功写队列后唤醒高优先级任务B,要不要马上切换任务呢?会发生什么事呢?
中断函数我们要求的是尽快执行完,唤醒任务很简单只是去移动到对应的链表里。而切换任务就比较慢,它要保存现场和恢复涉及到寄存器大量的读写。并且任务是不会去打断中断的。就算我们在中断处理过程中去切换(启用调度器)高优先级的任务也是无法执行的。因此我们会在中断结束前去切换任务。
因此在中断中我们写队列只是去唤醒任务并不会马上进行切换,我们会使用一个变量(xHigherPriorityTaskWoken)来记录是否有更高优先级的任务被唤醒,在中断结束前根据这个变量判断是否需要切换任务。
在中断里只去唤醒并不会马上切换。
总结:由于在中断中的情况和在任务中写队列的情况有所不同因此提供了两套函数供使用者使用。
情景2再分析
在情境2中我们在中断结束之前开启了调度,如果没有开启调度会怎样呢?
在中断中没有去开启调度器,那中断结束后高优先级任务B并不立刻执行而是等到下一个Tick中断,在Tick中断里去启用调度器切换高优先级任务B。
问题回顾:我们在之前讲了在高优先级任务就绪后会抢占低优先级任务,注意这里我们是当低优先级任务执行时创建高优先级任务,高优先级任务抢占了低优先级任务,为什么呢?因为在创建任务的函数结束之前会启用调度器,调度器遍历就绪链表有更高级的任务就绪就会进行切换。
我们发现高优先级的任务B无法快速执行,这不符合实时操作系统的目的!
1.2. 两套API函数列表
类型 | 在任务中 | 在ISR中 |
---|---|---|
队列(queue) | xQueueSendToBack | xQueueSendToBackFromISR |
xQueueSendToFront | xQueueSendToFrontFromISR | |
xQueueReceive | xQueueReceiveFromISR | |
xQueueOverwrite | xQueueOverwriteFromISR | |
xQueuePeek | xQueuePeekFromISR | |
信号量(semaphore) | xSemaphoreGive | xSemaphoreGiveFromISR |
xSemaphoreTake | xSemaphoreTakeFromISR | |
事件组(event group) | xEventGroupSetBits | xEventGroupSetBitsFromISR |
xEventGroupGetBits | xEventGroupGetBitsFromISR | |
任务通知(task notification) | xTaskNotifyGive | vTaskNotifyGiveFromISR |
xTaskNotify | xTaskNotifyFromISR | |
软件定时器(software timer) | xTimerStart | xTimerStartFromISR |
xTimerStop | xTimerStopFromISR | |
xTimerReset | xTimerResetFromISR | |
xTimerChangePeriod | xTimerChangePeriodFromISR |
1.3. xHigherPriorityTaskWoken参数
xHigherPriorityTaskWoken的含义是:是否有更高优先级的任务被唤醒了。如果为pdTRUE,则意味着后面要进行任务切换。
还是以写队列为例。
任务A调用 xQueueSendToBack() 写队列,有几种情况发生:
- 队列满了,任务A阻塞等待,另一个任务B运行
- 队列没满,任务A成功写入队列,但是它导致另一个任务B被唤醒,任务B的优先级更高:任务B先运行
- 队列没满,任务A成功写入队列,即刻返回
可以看到,在任务中调用API函数可能导致任务阻塞、任务切换,这叫做"context switch",上下文切换。这个函数可能很长时间才返回,在函数的内部实现了任务切换。
xQueueSendToBackFromISR() 函数也可能导致任务切换,但是不会在函数内部进行切换,而是返回一个参数:表示是否需要切换,函数原型与用法如下:
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/* 用法示例 */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
pxHigherPriorityTaskWoken参数,就是用来保存函数的结果:是否需要切换
- *pxHigherPriorityTaskWoken等于pdTRUE:函数的操作导致更高优先级的任务就绪了,ISR应该进行任务切换
- *pxHigherPriorityTaskWoken等于pdFALSE:没有进行任务切换的必要
为什么不在"FromISR"函数内部进行任务切换,而只是标记一下而已呢?为了效率!示例代码如下:
void XXX_ISR()
{
int i;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(...); /* 被多次调用 */
}
}
ISR中有可能多次调用"FromISR"函数,如果在"FromISR"内部进行任务切换,会浪费时间。解决方法是:
- 在"FromISR"中标记是否需要切换
- 在ISR返回之前再进行任务切换
- 示例代码如下
void XXX_ISR()
{
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); /* 被多次调用 */
}
/* 最后再决定是否进行任务切换 */
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
}
上述的例子很常见,比如UART中断:在UART的ISR中读取多个字符,发现收到回车符时才进行任务切换。
在ISR中调用API时不进行任务切换,而只是在"xHigherPriorityTaskWoken"中标记一下,除了效率,还有多种好处:
- 效率高:避免不必要的任务切换
- 让ISR更可控:中断随机产生,在API中进行任务切换的话,可能导致问题更复杂
- 可移植性
- 在Tick中断中,调用 vApplicationTickHook() :它运行与ISR,只能使用"FromISR"的函数
使用"FromISR"函数时,如果不想使用xHigherPriorityTaskWoken参数,可以设置为NULL。
1.4 怎么切换任务
FreeRTOS的ISR函数中,使用两个宏进行任务切换:
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
或
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
这两个宏做的事情是完全一样的,在老版本的FreeRTOS中,
- portEND_SWITCHING_ISR 使用汇编实现
- portYIELD_FROM_ISR 使用C语言实现
新版本都统一使用portYIELD_FROM_ISR。
使用示例如下:
void XXX_ISR()
{
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); /* 被多次调用 */
}
/* 最后再决定是否进行任务切换
* xHigherPriorityTaskWoken为pdTRUE时才切换
*/
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
2. 中断的延迟处理
前面讲过,ISR要尽量快,否则:
- 其他低优先级的中断无法被处理:实时性无法保证
- 用户任务无法被执行:系统显得很卡顿
- 如果运行中断嵌套,这会更复杂,ISR越快执行越有助于中断嵌套
如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为2部分:
- ISR:尽快做些清理、记录工作,然后触发某个任务
- 任务:更复杂的事情放在任务中处理
这种处理方式叫"中断的延迟处理"(Deferring interrupt processing),处理流程如下图所示:
- t1:任务1运行,任务2阻塞
- t2:发生中断,
- 该中断的ISR函数被执行,任务1被打断
- ISR函数要尽快能快速地运行,它做一些必要的操作(比如清除中断),然后唤醒任务2
- t3:在创建任务时设置任务2的优先级比任务1高(这取决于设计者),所以ISR返回后,运行的是任务2,它要完成中断的处理。任务2就被称为"deferred processing task",中断的延迟处理任务。
- t4:任务2处理完中断后,进入阻塞态以等待下一个中断,任务1重新运行
3 .中断与任务间的通信
前面讲解过的队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。
要注意的是,在ISR中使用的函数要有"FromISR"后缀。
4 .示例: 优化实时性
本节代码为:29_fromisr_game,主要看DshanMCU-F103\driver_ir_receiver.c。
以前,在中断函数里写队列时,代码如下:
150 static void DispatchKey(struct ir_data *pidata)
151 {
152 #if 0
153 extern QueueHandle_t g_xQueueCar1;
154 extern QueueHandle_t g_xQueueCar2;
155 extern QueueHandle_t g_xQueueCar3;
156 xQueueSendFromISR(g_xQueueCar1, pidata, NULL);
157 xQueueSendFromISR(g_xQueueCar2, pidata, NULL);
158 xQueueSendFromISR(g_xQueueCar3, pidata, NULL);
159 #else
160 int i;
161 for (i = 0; i < g_queue_cnt; i++)
162 {
163 xQueueSendFromISR(g_xQueues[i], pidata, NULL);
164 }
165 #endif
166 }
假设当前运行的是任务A,它的优先级比较低,在它运行过程中发生了中断,中断函数调用了DispatchKey函数写了队列,使得任务B被唤醒了。任务B的优先级比较高,它应该在中断执行完后马上就能运行。但是上述代码无法实现这个目标,xQueueSendFromISR函数会把任务B调整为就绪态,但是不会发起一次调度。
需要如下修改代码:
150 static void DispatchKey(struct ir_data *pidata)
151 {
152 #if 0
153 extern QueueHandle_t g_xQueueCar1;
154 extern QueueHandle_t g_xQueueCar2;
155 extern QueueHandle_t g_xQueueCar3;
156 xQueueSendFromISR(g_xQueueCar1, pidata, NULL);
157 xQueueSendFromISR(g_xQueueCar2, pidata, NULL);
158 xQueueSendFromISR(g_xQueueCar3, pidata, NULL);
159 #else
160 int i;
161 BaseType_t xHigherPriorityTaskWoken = pdFALSE;
//同一个数据写多个队列
162 for (i = 0; i < g_queue_cnt; i++)
163 {
164 xQueueSendFromISR(g_xQueues[i], pidata, &xHigherPriorityTaskWoken);
165 }
166 portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
167 #endif
168 }
在第164行传入一个变量的地址:&xHigherPriorityTaskWoken,它的初始值是pdFALSE,表示无需发起调度。如果xQueueSendFromISR函数发现唤醒了更高优先级的任务,那么就会把这个变量设置为pdTRUE。
第166行,如果xHigherPriorityTaskWoken为pdTRUE,它就会发起一次调度。