本章目的
- 哪些 FreeRTOS 的 API 函数可以在中断服务例程中使用。
- 延迟中断方案是处何实现的。
- 如何创建和使用二值信号量以及计数信号量。
- 二值信号量和计数信号量之间的区别。
- 何利用队列在中断服务例程中把数据传入传出。
- 一些 FreeRTOS 移植中采用的中断嵌套模型。
1.延迟中断处理
采用二值信号量同步
二值信号量可以在某个特殊的中断发生时,让任务解除阻塞,相当于让任务与中断
同步。这样就可以让中断事件处理量大的工作在同步任务中完成,中断服务(ISR)
中只是快速处理少部份工作。 如此,中断处理可以说是被”推迟(deferred)”到一个”处理
(handler)”任务。
如果某个中断处理要求特别紧急,其延迟处理任务的优先级可以设为最高,以保证
延迟处理任务随时都抢占系统中的其它任务。
延迟处理任务对一个信号量进行带阻塞性质的”==take==”调用,意思是进入阻塞态以等
待事件发生。当事件发生后, ISR 对同一个信号量进行”==give==”操作,使得延迟处理任务
解除阻塞,从而事件在延迟处理任务中得到相应的处理。
创建二值信号量
void vSemaphoreCreateBinary( xSemaphoreHandle xSemaphore ); //创建的信号量
//需要说明的是 vSemaphoreCreateBinary()在实现上是一个宏,所以信号量变量应当直接传入,而不是传址。
获取信号量 Take
==所有类型的信号量都可以调用函数 xSemaphoreTake()来获取。==
但 xSemaphoreTake()不能在中断服务例程中调用。中断中使用xSemaphoreTakeFromISR。
portBASE_TYPE xSemaphoreTake( xSemaphoreHandle xSemaphore, portTickType xTicksToWait );
//xSemaphore:获取得到的信号量
//阻塞超时时间。
//返回值:
1. pdPASS
2. pdFALSE
给予信号量 Give
==所有类型的信号量都可以调用函数 xSemaphoreGive()来给与/归还。== 但 xSemaphoreGive()不能在中断服务例程中调用。中断中使用xSemaphoreGiveFromISR。
portBASE_TYPE xSemaphoreGiveFromISR( xSemaphoreHandle xSemaphore, //给出的信号量
portBASE_TYPE *pxHigherPriorityTaskWoken ); //输出值:如 果 xSemaphoreGiveFromISR() 将 此 值 设 为pdTRUE,
//则在中断退出前应当进行一次上下文切换。这样才能保证中断直接返回就绪态任务中优先级最高的任务中。
利用二值信号量对任务和中断进行同步示例
本例在中断服务例程中使用一个二值信号量让任务从阻塞态中切换出来——从效
果上等同于让任务与中断进行同步。
一个简单的周期性任务用于每隔 500 毫秒产生一个软件中断。
整个执行流程可以描述为:
- 中断产生。
- 中断服务例程启动,给出信号量以使延迟处理任务解除阻塞。
- 当中断服务例程退出时,延迟处理任务得到执行。延迟处理任务做的第一件事便是
获取信号量。 - 延迟处理任务完成中断事件处理后,试图再次获取信号量——如果此时信号量无效,
任务将切入阻塞待等待事件发生。
static void vPeriodicTask( void *pvParameters )
{
for( ;; )
{
/* 此任务通过每500毫秒产生一个软件中断来”模拟”中断事件 */
vTaskDelay( 500 / portTICK_RATE_MS );
/* 产生中断,并在产生之前和之后输出信息,以便在执行结果中直观直出执行流程 */
vPrintString( "Periodic task - About to generate an interrupt.\r\n" );
__asm{ int 0x82 } /* 这条语句产生中断 */
vPrintString( "Periodic task - Interrupt generated.\r\n\r\n\r\n" );
}
}
static void vHandlerTask( void *pvParameters )
{
/* As per most tasks, this task is implemented within an infinite loop. */
for( ;; )
{
/* 使用信号量等待一个事件。信号量在调度器启动之前,也即此任务执行之前就已被创建。任务被无超
时阻塞,所以此函数调用也只会在成功获取信号量之后才会返回。此处也没有必要检测返回值 */
xSemaphoreTake( xBinarySemaphore, portMAX_DELAY );
/* 程序运行到这里时,事件必然已经发生。本例的事件处理只是简单地打印输出一个信息 */
vPrintString( "Handler task - Processing event.\r\n" );
}
}
static void __interrupt __far vExampleInterruptHandler( void )
{
static portBASE_TYPE xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE;
/* 'Give' the semaphore to unblock the task. */
xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken );
if( xHigherPriorityTaskWoken == pdTRUE )
{
/* 给出信号量以使得等待此信号量的任务解除阻塞。如果解出阻塞的任务的优先级高于当前任务的优先
级 – 强制进行一次任务切换,以确保中断直接返回到解出阻塞的任务(优选级更高)。
说明:在实际使用中, ISR中强制上下文切换的宏依赖于具体移植。此处调用的是基于Open Watcom DOS
移植的宏。其它平台下的移植可能有不同的语法要求。对于实际使用的平台,请参如数对应移植自带的示
例程序,以决定正确的语法和符号。
*/
portSWITCH_CONTEXT();
}
}
int main( void )
{
/* 信号量在使用前都必须先创建。本例中创建了一个二值信号量 */
vSemaphoreCreateBinary( xBinarySemaphore );
/* 安装中断服务例程 */
_dos_setvect( 0x82, vExampleInterruptHandler );
/* 检查信号量是否成功创建 */
if( xBinarySemaphore != NULL )
{
/* 创建延迟处理任务。此任务将与中断同步。延迟处理任务在创建时使用了一个较高的优先级,以保证
中断退出后会被立即执行。在本例中,为延迟处理任务赋予优先级3 */
xTaskCreate( vHandlerTask, "Handler", 1000, NULL, 3, NULL );
/* 创建一个任务用于周期性产生软件中断。此任务的优先级低于延迟处理任务。每当延迟处理任务切出
阻塞态,就会抢占周期任务*/
xTaskCreate( vPeriodicTask, "Periodic", 1000, NULL, 1, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
/* 如果一切正常, main()函数不会执行到这里,因为调度器已经开始运行任务。但如果程序运行到了这里,
很可能是由于系统内存不足而无法创建空闲任务。第五章会提供更多关于内存管理的信息 */
for( ;; );
}
2.计数信号量
==一个==二值信号量最多只可以锁存一个中断事件。在锁存的事
件还未被处理之前,如果还有中断事件发生,那么后续发生的中断事件将会丢失。如果
用计数信号量代替二值信号量,那么,这种丢中断的情形将可以避免。
(一个二值信号量最多只能锁存一个中断事件)
计数信号量可以看作是深度大于1的队列。计数信号量每次被给出(Given),
其队列中的另一个空间将会被使用。队列中的有效数据单元个数就是信号量的”计数(Count)”值。
计数信号量有以下两种典型用法
-
事件计数
每次事件发生时,中断服务例程都会“给出(Give)”信号量——信号
量在每次被给出时其计数值加 1。延迟处理任务每处理一个任务都会”获取(Take)”一次
信号量——信号量在每次被获取时其计数值减 1。信号量的计数值其实就是已发生事件
的数目与已处理事件的数目之间的差值。 -
资源管理
信号量的计数值用于表示可用资源的数目。一个任务要获取资源的
控制权,其必须先获得信号量——使信号量的计数值减 1。当计数值减至 0,则表示没
有可用资源。当任务利用资源完成工作后,将给出(归还)信号量——使信号量的计数值
加 1。
创建计数信号量
xSemaphoreHandle xSemaphoreCreateCounting( unsigned portBASE_TYPE uxMaxCount, //最大计数值
unsigned portBASE_TYPE uxInitialCount ); //信号量的初始计数值
//如果返回非 NULL 值,则表示信号量创建成功。此值应当被保存起来作为这个的信号量的句柄。
利用计数信号量对任务和中断进行同步示例
xCountingSemaphore = xSemaphoreCreateCounting( 10, 0 );
static void __interrupt __far vExampleInterruptHandler( void )
{
static portBASE_TYPE xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE;
/* 多次给出信号量。第一次给出时使得延迟处理任务解除阻塞。后续给出用于演示利用被信号量锁存事件,
以便延迟处理任何依序对这些中断事件进行处理而不会丢中断。用这种方式来模拟处理器产生多个中断,尽管
这些事件只是在单次中断中模拟出来的 */
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
if( xHigherPriorityTaskWoken == pdTRUE )
{
/* 给出信号量以使得等待此信号量的任务解除阻塞。如果解出阻塞的任务的优先级高于当前任务的优先
级 – 强制进行一次任务切换,以确保中断直接返回到解出阻塞的任务(优选级更高)。
说明:在实际使用中, ISR中强制上下文切换的宏依赖于具体移植。此处调用的是基于Open Watcom DOS
移植的宏。其它平台下的移植可能有不同的语法要求。对于实际使用的平台,请参如数对应移植自带的示
例程序,以决定正确的语法和符号。
*/
portSWITCH_CONTEXT();
}
}
//每次中断发生后,延迟处理任务处理了中断生成的全部三个事件
3.在中断服务中使用队列
xQueueSendToFrontFromISR(), xQueueSendToBackFromISR()与 xQueueReceiveFromISR()
分别是 xQueueSendToFront(), xQueueSendToBack()与 xQueueReceive()的中断安全
版本,专门用于中断服务例程中。
portBASE_TYPE xQueueSendToFrontFromISR( xQueueHandle xQueue, //目标队列的句柄
void *pvItemToQueue //发送数据的指针
portBASE_TYPE *pxHigherPriorityTaskWoken ); //返回优先级关系
portBASE_TYPE xQueueSendToBackFromISR( xQueueHandle xQueue,
void *pvItemToQueue
portBASE_TYPE *pxHigherPriorityTaskWoken
);
有效使用队列
- 将接收到的字符先缓存到内存中。当接收到一个传输完成消息,或是检测到传输中断后,使用信号量让某个任务解除阻塞,这个任务将对字符缓存进行处理。
- 在中断服务中直接解析接收到的字符,然后通过队列将解析后经解码得到的命令发
送到处理任务
利用队列在中断服务中发送或接收数据示例
static void vIntegerGenerator( void *pvParameters )
{
portTickType xLastExecutionTime;
unsigned portLONG ulValueToSend = 0;
int i;
/* 初始化变量,用于调用 vTaskDelayUntil(). */
xLastExecutionTime = xTaskGetTickCount();
for( ;; )
{
/* 这是个周期性任务。进入阻塞态,直到该再次运行的时刻。此任务每200毫秒执行一次 */
vTaskDelayUntil( &xLastExecutionTime, 200 / portTICK_RATE_MS );
/* 连续五次发送递增数值到队列。这此数值将在中断服务例程中读出。中断服务例程会将队列读空,所
以此任务可以确保将所有的数值都发送到队列。因此不需要指定阻塞超时时间 */
for( i = 0; i < 5; i++ )
{
xQueueSendToBack( xIntegerQueue, &ulValueToSend, 0 );
ulValueToSend++;
}
/* 产生中断,以让中断服务例程读取队列 */
vPrintString( "Generator task - About to generate an interrupt.\r\n" );
__asm{ int 0x82 } /* This line generates the interrupt. */
vPrintString( "Generator task - Interrupt generated.\r\n\r\n\r\n" );
}
}
static void __interrupt __far vExampleInterruptHandler( void )
{
static portBASE_TYPE xHigherPriorityTaskWoken;
static unsigned long ulReceivedNumber;
/* 这些字符串被声明为static const,以保证它们不会被定位到ISR的栈空间中,即使ISR没有运行它们也是存
在的 */
static const char *pcStrings[] =
{
"String 0\r\n",
"String 1\r\n",
"String 2\r\n",
"String 3\r\n"
};
xHigherPriorityTaskWoken = pdFALSE;
/* 重复执行,直到队列为空 */
while( xQueueReceiveFromISR( xIntegerQueue,&ulReceivedNumber,&xHigherPriorityTaskWoken ) != errQUEUE_EMPTY )
{
/* 截断收到的数据,保留低两位(数值范围0到3).然后将索引到的字符串指针发送到另一个队列 */
ulReceivedNumber &= 0x03;
xQueueSendToBackFromISR( xStringQueue,
&pcStrings[ ulReceivedNumber ],
&xHigherPriorityTaskWoken );
}
/* 被队列读写操作解除阻塞的任务,其优先级是否高于当前任务?如果是,则进行任务上下文切换 */
if( xHigherPriorityTaskWoken == pdTRUE )
{
/* 说明:在实际使用中, ISR中强制上下文切换的宏依赖于具体移植。此处调用的是基于Open Watcom
DOS移植的宏。其它平台下的移植可能有不同的语法要求。对于实际使用的平台,请参如数对应移植自带
的示例程序,以决定正确的语法和符号。 */
portSWITCH_CONTEXT();
}
}
static void vStringPrinter( void *pvParameters )
{
char *pcString;
for( ;; )
{
/* Block on the queue to wait for data to arrive. */
xQueueReceive( xStringQueue, &pcString, portMAX_DELAY );
/* Print out the string received. */
vPrintString( pcString );
}
}
int main( void )
{
/* 队列使用前必须先创建。本例中创建了两个队列。一个队列用于保存类型为unsigned long的变量,另一
个队列用于保存类型为char*的变量。两个队列的深度都为10。在实际应用中应当检测返回值以确保队列创建
成功 */
xIntegerQueue = xQueueCreate( 10, sizeof( unsigned long ) );
xStringQueue = xQueueCreate( 10, sizeof( char * ) );
/* 安装中断服务例程。 */
_dos_setvect( 0x82, vExampleInterruptHandler );
/* 创建任务用于往中断服务例程中发送数值。此任务优先级为1 */
xTaskCreate( vIntegerGenerator, "IntGen", 1000, NULL, 1, NULL );
/* 创建任务用于从中断服务例程中接收字符串,并打印输出。此任务优先级为2 */
xTaskCreate( vStringPrinter, "String", 1000, NULL, 2, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
/* 如果一切正常, main()函数不会执行到这里,因为调度器已经开始运行任务。但如果程序运行到了这里,
很可能是由于系统内存不足而无法创建空闲任务。第五章会提供更多关于内存管理的信息 */
for( ;; );
}
4.中断嵌套
中断嵌套需要在 FreeRTOSConfig.h 中定义表 详细列出的一个或两个常量。
configKERNEL_INTERRUPT_PRIORITY
设置系统心跳时钟的中断优先级。如果在移植中没有使用常量configMAX_SYSCALL_INTERRUPT_PRIORITY,
那么需要调用中断安全版本 FreeRTOS API的中断都必须运行在此优先级上。
configMAX_SYSCALL_INTERRUPT_PRIORITY
设置中断安全版本 FreeRTOS API 可以运行的最高中断优先级。
建立一个全面的中断嵌套模型需要设置 configMAX_SYSCALL_INTERRUPT_PRIRITY
为比 configKERNEL_INTERRUPT_PRIORITY 更高的优先级。
- 处于中断优先级 1 到 3(含)的中断会被内核或处于临界区的应用程序阻塞执行, 但是
它们可以调用中断安全版本的 FreeRTOS API 函数。 - 处于中断优先级 4 及以上的中断不受临界区影响,所以其不会被内核的任何行为阻
塞,可以立即得到执行——这是由微控制器本身对中断优先级的限定所决定的。通
常 需 要 严 格 时 间 精 度 的 功 能 ( 如 电 机 控 制 ) 会 使 用 高 于
configMAX_SYSCALL_INTERRUPT_PRIRITY 的优先级,以保证调度器不会对其中断响
应时间造成抖动。 - 不需要调用任何 FreeRTOS API 函数的中断,可以自由地使用任意优先级。
==Cortex M3 使用低优先级号数值表示逻辑上的高优先级中断。== 比如
STM32, ST 的驱动库中将最低优先级指定为 15,而最高优先级指定为 0。