邮箱:zhuimengcanyang@163.com 痴爱嵌入式技术的蜗牛
分类: 嵌入式
2015-09-25 09:48:20
多任务系统中存在一种潜在的风险。当一个任务在使用某个资源的过程中,即还没有完全结束对资源的访问时,便被切出运行态,使得资源处于非一致,不完整的状态。如果这个时候有另一个任务或者中断来访问这个资源,则会导致数据损坏或是其它相似的错误。
以下便是一些例子:
1. 访问外设
考虑如下情形,有两个任务都试图往一个LCD 中写数据:
? 任务A 运行,并往LCD 写字符串”Hello world”。
? 任务A 被任务B 抢占,但此时字符串才输出到”Hello w”。
? 任务B 往LCD 写”Abort, Retry, Fail?”,然后进入阻塞态。
? 任务A 从被抢占处继续执行,完成剩余的字符输出——“orld”。
现在LCD 显示的是被破坏了的字符串”Hello wAbort, Retry, Fail?orld”。
2. 读-改-写操作
程序清单57 展现的是一段C 代码和其等效的ARM7 汇编代码。可以看出,PORTA中的值先从内存读到寄存器,在寄存器中完成修改,然后再写回内存。这所是所谓的读-改-写操作。
/* The C code being compiled. */
155: PORTA |= 0x01;
/* The assembly code produced. */
0x00000264 481C LDR R0,[PC,#0x0070] ; Obtain the address of PORTA
0x00000266 6801 LDR R1,[R0,#0x00] ; Read the value of PORTA into R1
0x00000268 2201 MOV R2,#0x01 ; Move the absolute constant 1 into R2
0x0000026A 4311 ORR R1,R2 ; OR R1 (PORTA) with R2 (constant 1)
0x0000026C 6001 STR R1,[R0,#0x00] ; Store the new value back to PORTA
这是一个”非原子”操作,因为完成整个操作需要不止一条指令,所以操作过程可能被中断。考虑如下情形,两个任务都试图更新一个名为PORTA 的内存映射寄存器:
? 任务A 把PORTA 的值加载到寄存器中——整个流程的读操作。
? 在任务A 完成整个流程的改和写操作之前,被任务B 抢占。
? 任务B 完整的执行了对PORTA 的更新流程,然后进入阻塞态。
? 任务A 从被抢占处继续执行。其修改了一个PORTA 的拷贝,这其实只是寄存器在任务A 回写到PORTA 之前曾经保存过的值。
任务A 更新并回写了一个过期的PORTA 寄存器值。在任务A 获得拷贝与更新回写之间,任务B 又修改了PORTA 的值。而之后任务A 对PORTA 的回写操作,覆盖了任务B 对PORTA 进行的修改结果,效果上等同于破坏了PORTA 寄存器的值。虽然是以一个外围设备寄存器为例,但是整个情形同样适用于全局变量的读-改-写操作。
3. 变量的非原子访问
更新结构体的多个成员变量,或是更新的变量其长度超过了架构体系的自然长度(比如,更新一个16 位机上的32 位变量)均是非原子操作的例子。如果这样的操作被中
断,将可能导致数据损坏或丢失。
4. 函数重入
如果一个函数可以安全地被多个任务调用,或是在任务与中断中均可调用,则这个函数是可重入的。
每个任务都单独维护自己的栈空间及其自身在的内存寄存器组中的值。如果一个函数除了访问自己栈空间上分配的数据或是内核寄存器中的数据外,不会访问其它任何数据,则这个函数就是可重入的。
访问一个被多任务共享,或是被任务与中断共享的资源时,需要采用”互斥”技术以保证数据在任何时候都保持一致性。这样做的目的是要确保任务从开始访问资源就具有排它性,直至这个资源又恢复到完整状态。FreeRTOS 提供了多种特性用以实现互斥,但是最好的互斥方法(如果可能的话,任何时候都当如此)还是通过精心设计应用程序,尽量不要共享资源,或者是每个资源都通过单任务访问。基本临界区
本章期望让读者了解以下内容:
? 为什么,以及在什么时候有必要进行资源管理与控制。
? 什么是临界区。
? 互斥是什么意思。
? 挂起调度器有什么意义。
? 如何使用互斥量。
? 如何创建与使用守护任务。
? 什么是优先级反转,以及优先级继承是如何减小(但不是消除)其影响的。
基本临界区是指宏taskENTER_CRITICAL()与taskEXIT_CRITICAL()之间的代码区间。Critical Sections 也被称作Critical Regions。
/* 为了保证对PORTA寄存器的访问不被中断,将访问操作放入临界区。进入临界区 */
taskENTER_CRITICAL();
/* 在taskENTER_CRITICAL() 与 taskEXIT_CRITICAL()之间不会切换到其它任务。 中断可以执行,也允许嵌套,但只是针对优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断 – 而且这些中断不允许访问FreeRTOS API 函数. */
PORTA |= 0x01;
/* 我们已经完成了对PORTA的访问,因此可以安全地离开临界区了。 */
taskEXIT_CRITICAL();
在以前的例子中,调用打印函数printf()函数,在不同的任务当中,都有调用这个函数,所以当调用这个函数的时候,应该加上临界区保护。
/* 往stdout中写字符串,使用临界区这种原始的方法实现互斥。 */
taskENTER_CRITICAL();
printf( "%s", pcString );
taskEXIT_CRITICAL();
临界区是提供互斥功能的一种非常原始的实现方法。临界区的工作仅仅是简单地把中断全部关掉,或是关掉优先级在configMAX_SYSCAL_INTERRUPT_PRIORITY 及
以下的中断——依赖于具体使用的FreeRTOS 移植。抢占式上下文切换只可能在某个中断中完成,所以调用taskENTER_CRITICAL()的任务可以在中断关闭的时段一直保
持运行态,直到退出临界区。
临界区必须只具有很短的时间,否则会反过来影响中断响应时间。在每次调用taskENTER_CRITICAL()之后,必须尽快地配套调用一个taskEXIT_CRITICAL()。从这
个角度来看,对标准输出的保护不应当采用临界区,因为写终端在时间上会是一个相对较长的操作。本章中的示例代码会探索其它解决方案。
临界区嵌套是安全的,因为内核有维护一个嵌套深度计数。临界区只会在嵌套深度为0 时才会真正退出——即在为每个之前调用的taskENTER_CRITICAL()都配套调用
了taskEXIT_CRITICAL()之后。
也可以通过挂起调度器来创建临界区。挂起调度器有些时候也被称为锁定调度器。
注意区别:
(1)基本临界区保护一段代码区间不被其它任务或中断打断。
(2)由挂起调度器实现的临界区只可以保护一段代码区间不被其它任务打断,因为这种方式下,中断是使能的。
如果一个临界区太长而不适合简单地关中断来实现,可以考虑采用挂起调度器的方式。但是唤醒(resuming, or un-suspending)调度器却是一个相对较长的操作。所以评估哪种是最佳方式需要结合实际情况。
vTaskSuspendAll() API 函数互斥量(及二值信号量)
函数原型: void vTaskSuspendAll( void );xTaskResumeAll() API 函数
通过调用vTaskSuspendAll()来挂起调度器。挂起调度器可以停止上下文切换而不用关中断。如果某个中断在调度器挂起过程中要求进行上下文切换,则这个请求也会被挂起,直到调度器被唤醒后才会得到执行。
在调度器处于挂起状态时,不能调用FreeRTOS API 函数。
函数原型: portBASE_TYPE xTaskResumeAll( void );嵌套调用vTaskSuspendAll()和xTaskResumeAll()是安全的,因为内核有维护一个嵌套深度计数。调度器只会在嵌套深度计数为0 时才会被唤醒——即在为每个之前调用
返回值
在调度器挂起过程中,上下文切换请求也会被挂起,直到调度器被唤醒后才会得到执行。如果一个挂起的上下文切换请求在xTaskResumeAll()返回前得到执行,则函数返回pdTRUE。在其它情况下,xTaskResumeAll()返回pdFALSE。
的vTaskSuspendAll()都配套调用了xTaskResumAll()之后。
对于printf()函数,利用挂起和取消调度器。
/* Write the string to stdout, suspending the scheduler as a methodof mutual exclusion. */
vTaskSuspendScheduler();
printf( "%s", pcString );
xTaskResumeScheduler();
互斥量是一种特殊的二值信号量,用于控制在两个或多个任务间访问共享资源。单词MUTEX(互斥量)源于”MUTual EXclusion”。优先级反转
在用于互斥的场合,互斥量从概念上可看作是与共享资源关联的令牌。一个任务想要合法地访问资源,其必须先成功地得到(Take)该资源对应的令牌(成为令牌持有者)。当令牌持有者完成资源使用,其必须马上归还(Give)令牌。只有归还了令牌,其它任务才可能成功持有,也才可能安全地访问该共享资源。一个任务除非持有了令牌,否则不允许访问共享资源。
虽然互斥量与二值信号量之间具有很多相同的特性,但互斥量(互斥量用于互斥功能)完全不同于二值信号量(二值信号量用于同步)。
两者间最大的区别在于信号量在被获得之后所发生的事情:
(1)用于互斥的信号量必须归还。
(2)用于同步的信号量通常是完成同步之后便丢弃,不再归还。
这种机制纯粹是工作于应用程序作者制定的规则之下。任务不是在任何时候都可以访问资源是不需要理由的,因为这是所有任务达成的一致,除非它们能成为互斥量的持有者。
xSemaphoreCreateMutex() API 函数
互斥量是一种信号量。FreeRTOS 中所有种类的信号量句柄都保存在类型为xSemaphoreHandle 的变量中。
互斥量在使用前必须先创建。创建一个互斥量类型的信号量需要使用xSemaphoreCreateMutex() API 函数。
函数原型: xSemaphoreHandle xSemaphoreCreateMutex( void );
返回值
如果返回NULL 表示互斥量创建失败。原因是内存堆空间不足导致FreeRTOS 无法为互斥量分配结构数据空间。第五章提供更多关于内存管理方面的信息。
返回非NULL 值表示互斥量创建成功。返回值应当保存起来作为该互斥量的句柄。
例子-15:使用信号量重写vPrintString()
本例创建了一个新版本的vPrintString(),称为prvNewPrintString(),然后在多任务中调用这个新版函数。prvNewPrintString()具有与vPrintString()完全相同的功能,只是在实现上使用互斥量代替基本临界区来实现对标准输出的控制。
代码实现:
点击(此处)折叠或打开
- #include "led.h"
- #include "key.h"
- #include "exti.h"
- #include "delay.h"
- #include "sys.h"
- #include "usart.h"
- #include "timer.h"
- // FreeRTOS head file, add here.
- #include "FreeRTOS.h"
- #include "task.h"
- #include "queue.h"
- #include "list.h"
- #include "portable.h"
- #include "FreeRTOSConfig.h"
- #include "semphr.h"
- SemaphoreHandle_t xMutex;
- void board_Init(void)
- {
- LED_Init();
- uart_init(115200);
- // interrupt initialize
- NVIC_Configuration(); // bug fixed. by modify priority in FreeRTOSConfig.h file.
- }
- void PrintSring_Task(void *pvParameters)
- {
- char *pcStr = (char *)pvParameters;
- while(1)
- {
- // 1. enter critical section.
- xSemaphoreTake(xMutex, portMAX_DELAY);
- printf("%s", (char *)pcStr);
- // 2. exit critical section.
- xSemaphoreGive(xMutex);
- vTaskDelay(20/portTICK_RATE_MS);
- }
- }
- const char * printStr1 = "this is print string task 1.-------------------------------------------------\r\n";
- const char * printStr2 = "this is print string task 2.*************************************************\r\n";
- int main(void)
- {
- // board initialize.
- board_Init();
- printf("board initialize finish. \r\n");
- xMutex = xSemaphoreCreateMutex();
- if( xMutex != NULL )
- {
- // The semaphore was created successfully.
- // The semaphore can now be used.
- xTaskCreate(PrintSring_Task, "printStringTask1", configMINIMAL_STACK_SIZE, (void *)printStr1, 1, NULL);
- xTaskCreate(PrintSring_Task, "printStringTask2", configMINIMAL_STACK_SIZE, (void *)printStr2, 2, NULL);
- // start scheduler now
- vTaskStartScheduler();
- }
- return 0;
- }
打印结果:
打印的字符没有遭到破坏。
执行顺序:
上图也展现出了采用互斥量提供互斥功能的潜在缺陷之一。在这种可能的执行流程描述中,高优先级的任务2 竟然必须等待低优先级的任务1 放弃对互斥量的持有权。
高优先级任务被低优先级任务阻塞推迟的行为被称为”优先级反转”。这是一种不合理的行为方式,如果把这种行为再进一步放大,当高优先级任务正等待信号量的时候,一个介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行!这种最坏的情形在下图中进行展示。
优先级继承
优先级反转可能会产生重大问题。但是在一个小型的嵌入式系统中,通常可以在设计阶段就通过规划好资源的访问方式避免出现这个问题。
FreeRTOS 中互斥量与二值信号量十分相似——唯一的区别就是互斥量自动提供了一个基本的”优先级继承”机制。优先级继承是最小化优先级反转负面影响的一种方案死锁
——其并不能修正优先级反转带来的问题,仅仅是减小优先级反转的影响。优先级继承使得系统行为的数学分析更为复杂,所以如果可以避免的话,并不建议系统实现对优先级继承有所依赖。
优先级继承暂时地将互斥量持有者的优先级提升至所有等待此互斥量的任务所具有的最高优先级。持有互斥量的低优先级任务”继承”了等待互斥量的任务的优先级。这
种机制在下图中进行展示。互斥量持有者在归还互斥量时,优先级会自动设置为其原来的优先级。
由于最好是优先考虑避免优先级反转,并且因为FreeRTOS 本身是面向内存有限的微控制器,所以只实现了最基本的互斥量的优先级继承机制,这种实现假定一个任务
在任意时刻只会持有一个互斥量。
死锁是利用互斥量提供互斥功能的另一个潜在缺陷。Deadlock 有时候会被更戏剧性地称为”deadly embrace(抱死)”。
当两个任务都在等待被对方持有的资源时,两个任务都无法再继续执行,这种情况就被称为死锁。考虑如下情形,任务A 与任务B 都需要获得互斥量X 与互斥量Y 以完
成各自的工作:
1. 任务A 执行,并成功获得了互斥量X。这种情形的最终结局是,任务A 在等待一个被任务B 持有的互斥量,而任务B 也在等待一个被任务A 持有的互斥量。死锁于是发生,因为两个任务都不可能再执行下
2. 任务A 被任务B 抢占。
3. 任务B 成功获得了互斥量Y,之后又试图获取互斥量X——但互斥量X 已经被任务A 持有,所以对任务B 无效。任务B 选择进入阻塞态以等待互斥量X 被释放。
4. 任务A 得以继续执行。其试图获取互斥量Y——但互斥量Y 已经被任务B持有而对任务A 无效。任务A 也选择进入阻塞态以等待互斥量Y 被释放。
去了。
和优先级反转一样,避免死锁的最好方法就是在设计阶段就考虑到这种潜在风险,这样设计出来的系统就不应该会出现死锁的情况。于实践经验而言,对于一个小型嵌入式系统,死锁并不是一个大问题,因为系统设计者对整个应用程序都非常清楚,所以能够找出发生死锁的代码区域,并消除死锁问题。
守护任务提供了一种干净利落的方法来实现互斥功能,而不用担心会发生优先级反转和死锁。例子-16:
守护任务是对某个资源具有唯一所有权的任务。只有守护任务才可以直接访问其守护的资源——其它任务要访问该资源只能间接地通过守护任务提供的服务。
例16 提供了vPrintString()的另一种实现方法,这里采用了一个守护任务来管理对标准输出的访问。当一个任务想要往终端写信息的时候,其不能直接调用打印函数,而是将消息发送到守护任务。
守护任务使用了一个FreeRTOS 队列来对终端实现串行化访问。该任务内部实现不必考虑互斥,因为它是唯一能够直接访问终端的任务。
守护任务大部份时间都在阻塞态等待队列中有信息到来。当一个信息到达时,守护任务仅仅简单地将收到的信息写到标准输出上,然后又返回阻塞态,继续等待下一条信息地到来。
中断中可以写队列,所以中断服务例程也可以安全地使用守护任务提供的服务,从而把信息输出到终端。在本例中,一个心跳中断钩子函数用于每200 心跳周期就输出
一个消息。
心跳钩子函数(或称回调函数)由内核在每次心跳中断时调用。要挂接一个心跳钩子函数,需要做以下配置:
? 设置FreeRTOSConfig.h 中的常量configUSE_TICK_HOOK 为1。
? 提供钩子函数的具体实现,必须使用该函数名和原型: void vApplicationTickHook( void );
心跳钩子函数在系统心跳中断的上下文上执行,所以必须保证非常短小,适度占用栈空间,并且不要调用任何名字不带后缀”FromISR”的FreeRTOS API 函数。
代码实现
点击(此处)折叠或打开
- #include "led.h"
- #include "key.h"
- #include "exti.h"
- #include "delay.h"
- #include "sys.h"
- #include "usart.h"
- #include "timer.h"
- // FreeRTOS head file, add here.
- #include "FreeRTOS.h"
- #include "task.h"
- #include "queue.h"
- #include "list.h"
- #include "portable.h"
- #include "FreeRTOSConfig.h"
- #include "semphr.h"
- xQueueHandle xPrintQueue;
- static const char *pcStringToPrint[] = {
- "this is print string task 1.-------------------------------------------------\r\n",
- "this is print string task 2.*************************************************\r\n",
- "this is print string task 3.*************************************************\r\n",
- };
- void board_Init(void)
- {
- LED_Init();
- KEY_Init();
- //EXTIX_Init();
- uart_init(115200);
- //TIM3_Int_Init(10,7200);// 10kHZ, 1ms interrupt.
- // interrupt initialize
- NVIC_Configuration(); // bug fixed. by modify priority in FreeRTOSConfig.h file.
- }
- void keyScan_Task(void *pvParameters)
- {
- char key = 0x00;
- char i =0, sendInt = 0;
- while(1)
- {
- // add your key scan code here.
- keyScan();
- if((key = keyScan_readBuff()) != 0)
- {
- switch(key)
- {
- case ( KEY_CODE + SHORT_KEY):
- printf("send Int: ");
- for(i=0; i<5; i++)
- {
- printf("%d ", sendInt);
- //xQueueSendToBack(xIntegerQueue, &sendInt, 0); // send integer: 1, 2, 3, 4, 5;
- sendInt ++;
- }
- printf("\r\n");
- break;
- case ( KEY_CODE+FIRSTLONG_KEY_CODE):
- printf("long first pressed \r\n");
- break;
- case ( KEY_CODE+AFTERLONG_KEY_CODE):
- printf("long after pressed \r\n");
- break;
- }
- }
- vTaskDelay(10/portTICK_RATE_MS);
- }
- }
- void LED_task(void *pvParameters)
- {
- char state = 0;
- while(1)
- {
- ((state = !state) == 1) ? GREEN_ON() : GREEN_OFF();
- vTaskDelay(1000 / portTICK_RATE_MS);
- }
- }
- void Keeper_Task(void *pvParameters)
- {
- char *pcStr;
- while(1)
- {
- /* Wait to receive message from queue, the task will blocking until data coming into queue. */
- xQueueReceive(xPrintQueue, &pcStr, portMAX_DELAY);
- printf("%s", pcStr);
- }
- }
- void PrintSring_Task(void *pvParameters)
- {
- int iIndexToString;
- iIndexToString = (int)pvParameters;
- while(1)
- {
- /* send pointer data to queue, if full, then return */
- xQueueSendToBack(xPrintQueue, &(pcStringToPrint[iIndexToString]), 0);
- vTaskDelay(500/portTICK_RATE_MS);
- }
- }
- int main(void)
- {
- // board initialize.
- board_Init();
- printf("board initialize finish. \r\n");
- // create queues
- xPrintQueue = xQueueCreate(5, sizeof(char *));
- // ??????????·????¨????
- if(xPrintQueue != NULL)
- {
- xTaskCreate(PrintSring_Task, "PrintSring_Task1", configMINIMAL_STACK_SIZE, (void *)0, 1, NULL);
- xTaskCreate(PrintSring_Task, "PrintSring_Task2", configMINIMAL_STACK_SIZE, (void *)1, 2, NULL);
- /* create keeper task here.*/
- xTaskCreate(Keeper_Task, "PrintSring_Task2", configMINIMAL_STACK_SIZE, NULL, 0, NULL);
- // start scheduler now
- vTaskStartScheduler();
- }
- else
- {
- // semaphore create unsuccessful here. add your code.
- printf("semaphore create failed \r\n");
- }
- return 0;
- }
- void vApplicationTickHook(void)
- {
- static int iCount = 0;
- BaseType_t xHigherPrioritTaskWoken;
- // We have not woken a task at the start of the ISR.
- xHigherPrioritTaskWoken = pdFALSE;
- iCount ++;
- /* systick interrupt = 10ms. hook interrupt time = 10ms * 100 = 1s */
- if(iCount >= 100)
- {
- xQueueSendToFrontFromISR(xPrintQueue, &(pcStringToPrint[2]), &xHigherPrioritTaskWoken);
- iCount = 0;
- }
- }
- void TIM3_IRQHandler(void) //TIM3????, 1ms
- {
- static char state = 0;
- static int cnt = 0;
- unsigned long value;
- BaseType_t xHigherPriorityTaskWoken = pdFALSE;
- if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //?ì?é???¨??TIM????·??ú??·?:TIM ??????
- {
- TIM_ClearITPendingBit(TIM3, TIM_IT_Update); //????TIMx???????????í??:TIM ??????
- if(++cnt >= 100) // per 100ms
- {
- cnt = 0;
- ((state = !state) == 1) ? RED_ON() : RED_OFF();
- }
- }
- }
打印结果: