FreeRTOS学习笔记(四) 中断管理

大部分freertos api函数都有两个版本, 区别在于名称后面的FromISR

带有改后缀的, 称为中断安全函数

为什么执行同一功能的函数必须设计为两个版本呢

其根本原因是因为 在函数调用之中 如果使更高优先级的任务进入到就绪态, 该任务则会抢占当前任务

而在中断中不能进行上下文切换

那为了解决这个问题, freertos的设计采用了将其分为两个函数的做法

这样的优势是:

代码不用再去实现 辨别函数来源的逻辑 即不用去判定其是从中断中调用的还是从任务中调用的

确保传入的参数都是该函数会使用的, 在任务中和中断中调用函数时使用的api函数参数可能不同

不同freertos移植版本执行上下文机制不同

兼容不同架构, 中断和硬件有关

缺点呢:

在可能同时被中断和任务调用的函数中无法确定使用哪个函数

解决办法是使用 延迟中断处理, 即把中断处理函数统一由任务或进程守护任务调用, 而这些任务由中断进行唤醒

那么中断安全函数是如何解决无法在中断中切换上下文的呢

中断安全函数都有一个统一的参数xHigherPriorityTaskWoken

这个参数有两个值, pdTRUE和pdFALSE

通常初始化为pdFALSE

在执行完一个api函数(中断安全版)之后, 如果导致更高优先级的任务变为就绪态

则在api函数内部会将该参数改为pdTRUE

最后在ISR结尾处通过调用portYIELD_FROM_ISR( xHigherPriorityTaskWoken )来决定是否跳转

portYIELD_FROM_ISR是如何起作用的呢?

该函数会先判定woken参数是否为true, 如果为true则会悬起一个pendSV异常

这会导致当该ISR推出后 引起一个上下文切换

这样就避免了在中断中进行任务切换

示例如下, 使用的api函数为二进制信号量相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static uint32_t ulExampleInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
/* The xHigherPriorityTaskWoken parameter must be initialized to
pdFALSE as it will get set to pdTRUE inside the interrupt safe
API function if a context switch is required. */
xHigherPriorityTaskWoken = pdFALSE;
/* 'Give' the semaphore to unblock the task, passing in the address of
xHigherPriorityTaskWoken as the interrupt safe API function's
pxHigherPriorityTaskWoken parameter. */
xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken ); //二进制信号量
/* Pass the xHigherPriorityTaskWoken value into portYIELD_FROM_ISR().
If xHigherPriorityTaskWoken was set to pdTRUE inside
xSemaphoreGiveFromISR() then calling portYIELD_FROM_ISR() will request
a context switch. If xHigherPriorityTaskWoken is still pdFALSE then
calling portYIELD_FROM_ISR() will have no effect. Unlike most FreeRTOS
ports, the Windows port requires the ISR to return a value - the return
statement is inside the Windows version of portYIELD_FROM_ISR(). */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

在讲信号量之前先来了解下延迟中断处理

由于ISR运行时会导致内核的短暂停止

这可能会导致 任务的开始时间和执行时间的抖动

所以必须保证ISR逻辑尽量简短

但世事总不那么如意

在必须处理复杂逻辑时该怎么办呢

就只能使用延迟中断处理了

延迟中断处理在检测到中断时 唤醒处理函数

然后在处理函数中进行逻辑的处理

ISR中只需要写唤醒函数的代码即可

11

那么ISR要如何唤醒一个任务呢

这里我们使用的是 信号量(官方推荐的是使用任务通知)

信号量分为二进制信号量和计数信号量

二进制信号量可以看作一个队列长度为1的, 队列内容无关的队列, 那么计数信号量就是队列长度不为1的, 队列内容无关的队列

使用流程:

创建一个信号量

中断处理任务中调用take(获取信号量函数)

在take发现信号量中没有信号时 该任务进入阻塞状态

当检测到中断发生

ISR中调用give(写信号量函数)

此时内核发现该信号量有值了, 通知中断处理任务进入就绪态

如果中断处理任务的优先级大于现有任务

则会将xHigherPriorityTaskWoken置为pdTRUE, 然后ISR返回后切换上下文为中断处理任务

中断处理任务take到信号量

执行中断处理逻辑

在下一轮执行时, take再次发现信号量为空, 进入阻塞态

9

二进制信号量的问题也很明显

在当中断处理函数take信号量之前 如果再次发生了一个中断

那么第二个中断将被忽略 于是有个计数信号量

作用机制是相同的, 只是多了能够处理多个中断

10

api函数:

1
SemaphoreHandle_t xSemaphoreCreateBinary( void );
1
2
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait
);
1
2
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken );
1
2
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount );

上文还提到可以将中断处理工作延时给守护进程任务

优点在于:

能够降低资源浪费率, 无需为每个延迟中断创建单独任务

简化用户模型: 延迟中断处理函数是一个标准的C函数

缺点在于:

由于都是适用的守护进程任务, 那么无法为每个中断处理设置单独的优先级

守护进程执行函数是通过队列来获取所需要的执行函数的, 可能ISR结束后无法立即执行中断处理函数

1
2
3
4
5
6
7
BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t
xFunctionToPend,
void *pvParameter1,
uint32_t ulParameter2,
BaseType_t *pxHigherPriorityTaskWoken ); //在中断中向进程守护任务发送一个函数执行指令

void vPendableFunction( void *pvParameter1, uint32_t ulParameter2 ); //函数原型 既可以使用指针类型的参数 也可以使用非指针类型参数

ISR中也是可以使用队列的, 但如果只是用来传递信号 那还是信号量相对更佳(最佳的还是 任务通知)

在进程中使用队列也需要使用ISR专用的中断安全api函数

在UART程序中, 可以使用队列来接收UART传送过来的数据, 然后通过延迟中断处理计数 在任务中进行处理

中断是可以嵌套的

每个中断源都有数值优先级和逻辑优先级的属性

逻辑优先级高的能抢占逻辑优先级低的中断 这点和裸机一样

逻辑优先级由数字优先级控制

在CM3中, 数字优先级低的逻辑优先级越高, 这点也和裸机一样

12

对于越高的优先级对ISR的执行事件越严格, 具体表现为是否能够使用api函数

CM3中最高循序使用8位来指定每个中断优先级

通常只使用八位数字的一部分

使用规范则是左对齐, 未使用的低有效位通常置为1