FreeRTOS学习笔记十一【中断管理-下】
计数信号量
介绍
与二值信号量类似,计数信号量可以看作是长度大于1的队列,任务只关心队列中的项数,而不关心队列中的数据。要使用计数信号量需将FreeRTOSConfig.h中的configUSE_COUNTING_SEMAPHORES设置为1。每次"given"信号量时,使用其队列中的一处空间,队列中的项数就是信号量的计数值。计数信号量常用于下列两种情况:
- 对事件计数(后面的任务通知会更加有效)
在这种情况下,事件处理程序会在每次事件发生时"given"信号量,即信号量的计数值在每次"given"时递增,每次处理事件时任务会"take"信号量,此时信号量的计数值会递减。因此,计数值就是已发生事件和已处理事件之间的差值。所以计数信号量的初始计数值(创建时)应该设置为0。其机制如下图。
- 资源管理
在这种情况下,计数值指示的是可用资源的数量。要获得对资源的控制,任务必须先"take"信号量(递减信号量的计数值),当计数值达到0时,就没有空闲资源,当一个任务使用完资源时,需要"given"信号量,此时信号量的计数值递增。用于资源管理的信号量,它的计数值在初始时应设置为可用资源的数量。
xSemaphoreCreateCounting()
在使用计数信号量之前需要先创建它,创建计数信号量的API原型如下:
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount );
参数:
- uxMaxCount
信号量计数的最大值。类似于队列中队列的长度。
当信号量用于计数事件时,uxMaxCount为可记录的最大事件数。
当信号量用于管理资源时,uxMaxCount为可用资源的总数。 - uxInitialCount
创建信号量后它的初始计数值。
当信号用于计数事件时,uxInitialCount应该设置为0,因为创建信号量时还未发生任何事件。
当信号量用于资源管理时,uxInitialCount应该设置为uxMaxCount,因为创建信号量时,所有资源都可用。
返回值:
- NULL
创建信号量失败,没有足够的内存空间用于创建该信号量。 - 非NULL
创建信号量成功,返回的时计数信号量的句柄。
示例
/* 产生软件中断的任务,示例中用于模拟,实际使用中可能是硬件中断 */
static void vPeriodicTask( void *pvParameters )
{
const TickType_t xDelay500ms = pdMS_TO_TICKS( 500UL );
for( ;; )
{
/* 阻塞500ms,然后发送软件中断 */
vTaskDelay( xDelay500ms );
/* 发送软件中断,输出提示信息 */
vPrintString( "Periodic task - About to generate an interrupt.\r\n" );
vPortGenerateSimulatedInterrupt( mainINTERRUPT_NUMBER );
vPrintString( "Periodic task - Interrupt generated.\r\n\r\n\r\n" );
}
}
/* 延迟处理任务 */
static void vHandlerTask( void *pvParameters )
{
for( ;; )
{
/* 获取信号量,如果信号量不可用,则阻塞等待 */
xSemaphoreTake( xCountingSemaphore, portMAX_DELAY );
/* 处理事件 */
vPrintString( "Handler task - Processing event.\r\n" );
}
}
/* ISR */
static uint32_t ulExampleInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
/* xHigherPriorityTaskWoken 指向的变量在使用前需要初始化为pdFALSE */
xHigherPriorityTaskWoken = pdFALSE;
/* 发送信号量 */
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
/* 根据xHigherPriorityTaskWoken 判断是否需要执行调度 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
int main( void )
{
/* 创建计数信号量 */
xCountingSemaphore = xSemaphoreCreateCounting( 10, 0 );
/* 创建成功 */
if( xCountingSemaphore!= NULL )
{
/* 创建延迟处理任务 */
xTaskCreate( vHandlerTask, "Handler", 1000, NULL, 3, NULL );
/* 创建产生软件中断的任务,示例中用于模拟,实际使用中可能是硬件中断 */
xTaskCreate( vPeriodicTask, "Periodic", 1000, NULL, 1, NULL );
/* 设置ISR函数,示例中用于模拟,实际使用中可能是硬件中断 */
vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulExampleInterruptHandler );
/* 启动调度 */
vTaskStartScheduler();
}
for( ;; );
}
运行结果:
将工作推迟到RTOS守护程序任务
介绍
到目前为止提出的延迟中断处理的示例都要求应用程序为每个需要延迟处理的中断创建延迟处理任务。这里介绍一种无需为每个中断创建单独任务的方法,使用xTimerPendFunctionCallFromISR() API函数将中断处理延迟到RTOS的守护程序任务中,这种方式称为集中式延迟中断处理。
在FreeRTOS学习笔记九【软件定时器】中描述了与软件定时器相关的API函数如何向定时器命令队列上的守护程序任务发送命令。 xTimerPendFunctionCall()和xTimerPendFunctionCallFromISR()函数使用相同的计时器命令队列向守护程序任务发送“执行函数”命令。然后,在守护程序任务的上下文中执行发送到守护程序任务的函数。
集中式延迟中断处理有以下几个优点:
- 降低资源使用率
它消除了为每个延迟处理中断创建单独任务的需要。 - 简化用户模型
这些函数都是标准的C函数。
但集中式延迟中断处理也有以下几个缺点:
- 灵活性较低
无法单独设置每个延迟中断处理任务的优先级。每个延迟中断处理函数都以守护程序任务的优先级执行。如FreeRTOS学习笔记九【软件定时器】所述,守护程序任务的优先级由FreeRTOSConfig.h中的configTIMER_TASK_PRIORITY设置。 - 实时性差
xTimerPendFunctionCallFromISR()将命令发送到计时器命令队列的后面。在通过xTimerPendFunctionCallFromISR()将“执行函数”命令发送到队列之前,守护程序任务将处理已存在于定时器命令队列中的命令。
不同的中断具有不同的约束,因此通常在应用程序中将两个方式结合使用。
xTimerPendFunctionCallFromISR()
xTimerPendFunctionCallFromISR()是xTimerPendFunctionCall()的中断安全版本。 这两个API函数都允许将提供的函数放在RTOS守护程序任务中执行,要执行的函数和函数的输入参数都将发送到定时器命令队列上。它的原型如下:
BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend,
void *pvParameter1,
uint32_t ulParameter2,
BaseType_t *pxHigherPriorityTaskWoken );
参数:
- xFunctionToPend
要执行函数的指针。 - pvParameter1
传递给xFunctionToPend函数的第一个参数,它是一个void指针,可以传递任务类型参数。 - ulParameter2
传递给xFunctionToPend函数的第二个参数。 - pxHigherPriorityTaskWoken
前面已有介绍(FreeRTOS学习笔记十【中断管理-上】)。
返回值:
- pdPASS
"执行函数"的命令成功写入定时器命令队列。 - pdFAIL
如果定时器命令队列为满,则无法将"执行函数"命令写入定时器命令队列。
示例
/* 产生软件中断的任务,示例中用于模拟,实际使用中可能是硬件中断 */
static void vPeriodicTask( void *pvParameters )
{
const TickType_t xDelay500ms = pdMS_TO_TICKS( 500UL );
for( ;; )
{
/* 阻塞500ms,然后发送软件中断 */
vTaskDelay( xDelay500ms );
/* 发送软件中断,输出提示信息 */
vPrintString( "Periodic task - About to generate an interrupt.\r\n" );
vPortGenerateSimulatedInterrupt( mainINTERRUPT_NUMBER );
vPrintString( "Periodic task - Interrupt generated.\r\n\r\n\r\n" );
}
}
/* 守护程序任务中执行的函数 */
static void vDeferredHandlingFunction( void *pvParameter1, uint32_t ulParameter2 )
{
/* 处理事件 */
vPrintStringAndNumber( "Handler function - Processing event ", ulParameter2 );
}
/* ISR函数 */
static uint32_t ulExampleInterruptHandler( void )
{
static uint32_t ulParameterValue = 0;
BaseType_t xHigherPriorityTaskWoken;
/* 使用前需要初始为pdFALSE */
xHigherPriorityTaskWoken = pdFALSE;
/* 将指定的函数传入守护程序任务中执行 */
xTimerPendFunctionCallFromISR( vDeferredHandlingFunction, /* 执行的函数 */
NULL, /* 未使用 */
ulParameterValue, /* 递增 */
&xHigherPriorityTaskWoken );
ulParameterValue++;
/* 通过xHigherPriorityTaskWoken判断是否需要重新调度程序 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
int main( void )
{
const UBaseType_t ulPeriodicTaskPriority = configTIMER_TASK_PRIORITY - 1;
/* 创建模拟中断的任务,实际中为处理的硬件中断 */
xTaskCreate( vPeriodicTask, "Periodic", 1000, NULL, ulPeriodicTaskPriority, NULL );
vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulExampleInterruptHandler );
vTaskStartScheduler();
for( ;; );
}
运行结果:
任务调度情况:
在ISR中使用队列及其注意事项
二值信号量和计数信号量用于同步(传递)事件。 队列用于传递事件和传输数据。在ISR中使用队列,必须使用它安全版的API,它的使用和普通股版类似,但是没有阻塞操作,同时多了一个判断是否需要重新调度程序的参数,前面已有相应介绍,这里就不在介绍API,介绍一下在ISR中使用队列的注意事项。
队列提供了一种简单方便的方法将数据从ISR传递到任务中,但是如果中断的发送频率高,则队列的效率不高。因此,建议只在中断发生频率不高的ISR中使用队列来传输数据。下面介绍几种在ISR更高效的方法:
- 使用直接内存访问(DMA)接收和缓存数据(如UART、IIC、SPI等),DMA运行时灭有软件开销,然后使用任务通知(后面介绍)来解除等待数据的任务。
- 将每个接收到字符复制到线程安全的RAM缓存区中,同样使用任务通知来解除等待数据的任务。
- 直接在ISR中处理接收到的字符,然后在使用队列将处理后的结果发生给任务。
中断嵌套
有时会将任务优先级与中断优先级混淆,这里将介绍中断优先级,它是ISR之间执行的优先级,和分配给任务的优先级无关。ISR何时执行有硬件决定,任务何时执行有软件决定,当执行ISR时任务会被中断(暂停执行的任务,去执行ISR),但任务不能抢占ISR。
支持中断嵌套的移植需要在FreeRTOSConfig.h中定义一个或两个常量,其中configMAX_SYSCALL_INTERRUPT_PRIORITY和configMAX_API_CALL_INTERRUPT_PRIORITY都定义了相同的属性。 较旧的FreeRTOS使用configMAX_SYSCALL_INTERRUPT_PRIORITY,较新的FreeRTOS使用configMAX_API_CALL_INTERRUPT_PRIORITY。
常量 | 描述 |
---|---|
configMAX_SYSCALL_INTERRUPT_PRIORITY | 设置中断安全的API函数的最高中断优先级 |
configMAX_API_CALL_INTERRUPT_PRIORITY | 同上 |
KERNEL_INTERRUPT_PRIORITY | 设置滴答中断中断优先级,并且必须设置为最低中断优先级。如果也没有使用configMAX_SYSCALL_INTERRUPT_PRIORITY(或configMAX_API_CALL_INTERRUPT_PRIORITY)常量,然后任何中断安全的API函数的中断也必须以configKERNEL_INTERRUPT_PRIORITY定义的优先级执行。 |
每个中断都有一个数字优先级和逻辑优先级:
- 数字优先级
数字优先级只是分配给中断优先级的编号。如,中断分配优先级为7,则数字优先级为7,如果分配为100,则数字优先级为100。 - 逻辑优先级
中断的逻辑优先级描述了中断与其他中断之间的优先级。如果同时发生两个不同优先级的中断,则处理器将先执行优先级高的中断的ISR。如果先发生了一个优先级较低的中断,在发生一个优先级高的中断,则优先级高的中断将会打断低优先级中断的ISR,在高优先级的ISR执行完后继续执行低优先级中断的ISR(这就是中断嵌套),但是如果优先级高的中断先发生,优先级低的后发生,则高优先级的ISR不会被打断。
中断的数字优先级和逻辑优先级之间的关系取决于处理的构架,在某些处理器上,分配给中断的数字优先级越高,中断的逻辑优先级就越高,而在一些处理器上,分配的数字优先级越高,中断的逻辑优先级就越低。
如下图描述的场景:
- 处理器有7个中断优先级。
- 数字优先级越高的中断,逻辑优先级也越高。
- configKERNEL_INTERRUPT_PRIORITY设置为1。
- configMAX_SYSCALL_INTERRUPT_PRIORITY设置为3.
图中: - 当内核或应用程序位于关键部分代码(后面介绍)内时,将阻止执行优先级1到3的中断。同时优先级1到3的中断ISR中可以调用中断安全版的API函数。
- 优先级大于等于4的中断不受关键部分代码的影响,因此调度程序所做的任何操作都不会阻止这些中断立即执行(但要在硬件本身限制范围内)。以这些优先级执行的ISR不能使用任何FreeRTOS API函数。
- 需要非常严格的时序要求的功能(如电机控制)将使用高于configMAX_SYSCALL_INTERRUPT_PRIORITY的优先级,来确保调度程序不会在中断响应时间中影响时序。