php网站怎么做集群,什么是微网站系统,设计好的制作网站,wordpress 5编辑器使用更多干货推荐可以去牛客网看看#xff0c;他们现在的IT题库内容很丰富#xff0c;属于国内做的很好的了#xff0c;而且是课程刷题面经求职讨论区分享#xff0c;一站式求职学习网站#xff0c;最最最重要的里面的资源全部免费#xff01;#xff01;#xff01;点击进… 更多干货推荐可以去牛客网看看他们现在的IT题库内容很丰富属于国内做的很好的了而且是课程刷题面经求职讨论区分享一站式求职学习网站最最最重要的里面的资源全部免费点击进入--------------》跳转接口 更多干货推荐可以去牛客网看看他们现在的IT题库内容很丰富属于国内做的很好的了而且是课程刷题面经求职讨论区分享一站式求职学习网站最最最重要的里面的资源全部免费点击进入--------------》跳转接口 目录第十一章FreeRTOS 其他任务API 函数11.1 任务相关API 函数预览11.2 任务相关API 函数详解11.3 任务壮态查询API 函数实验11.3.1 实验程序设计11.3.2 程序运行结果分析11.4 任务运行时间信息统计实验11.4.1 相关宏的设置11.4.2 实验程序设计第十二章FreeRTOS 时间管理12.1 FreeRTOS 延时函数12.2 FreeRTOS 系统时钟节拍第十三章FreeRTOS 队列13.1 队列简介13.2 队列结构体13.3 队列创建13.3.1 函数原型13.3.2 队列创建函数详解13.3.3 队列初始化函数13.3.4 队列复位函数13.4 向队列发送消息13.4.1 函数原型13.4.2 任务级通用入队函数13.4.3 中断级通用入队函数13.5 队列上锁和解锁13.6 从队列读取消息13.7 队列操作实验13.7.1 实验程序设计13.7.2 程序运行结果分析第十四章FreeRTOS 信号量14.1 信号量简介14.2 二值信号量14.2.1 二值信号量简介14.2.2 创建二值信号量14.2.3 二值信号量创建过程分析14.2.4 释放信号量14.2.5 获取信号量14.3 二值信号量操作实验14.3.1 实验程序设计14.3.2 程序运行结果分析14.4 计数型信号量14.4.1 计数型信号量简介14.4.2 创建计数型信号量14.4.3 计数型信号量创建过程分析14.4.4 释放和获取计数信号量14.5 计数型信号量操作实验14.5.1 实验程序设计14.5.2 程序运行结果分析14.6 优先级翻转14.7 优先级翻转实验14.7.1 实验程序设计14.7.2 程序运行结果分析14.8 互斥信号量14.8.1 互斥信号量简介14.8.2 创建互斥信号量14.8.4 释放互斥信号量14.8.5 获取互斥信号量14.9 互斥信号量操作实验14.9.1 实验程序设计14.9.2 程序运行结果分析14.10 递归互斥信号量14.10.1 递归互斥信号量简介14.10.2 创建互斥信号量14.10.3 递归信号量创建过程分析14.10.4 释放递归互斥信号量14.10.5 获取递归互斥信号量14.10.6 递归互斥信号量使用示例第十五章FreeRTOS 软件定时器15.1 软件定时器简介15.2 定时器服务/Daemon 任务15.2.1 定时器服务任务与队列15.2.2 定时器相关配置15.3 单次定时器和周期定时器15.4 复位软件定时器15.5 创建软件定时器15.6 开启软件定时器15.7 停止软件定时器15.8 软件定时器实验15.8.1 实验程序设计15.8.2 程序运行结果分析第十一章FreeRTOS 其他任务API 函数
前面几章我们花费了大量的精力来学习FreeRTOS 的任务管理但是真正涉及到的与任务 相关的API 函数只有那么几个。但是FreeRTOS 还有很多与任务相关的API 函数不过这些API 函数大多都是辅助函数了本章我们就来看一下这些与任务相关的其他的API 函数。本章分为 如下几部分 11.1 任务相关API 函数预览 11.2 任务相关API 函数详解 11.3 任务壮态查询API 函数实验 11.4 任务运行时间壮态统计实验
11.1 任务相关API 函数预览
先通过一个表11.1.1 来看一下这些与任务相关的其他API 函数都有哪些 这些API 函数在FreeRTOS 官网上都有如图11.1.2 所示
11.2 任务相关API 函数详解
1、函数uxTaskPriorityGet() 此函数用来获取指定任务的优先级要使用此函数的话宏INCLUDE_uxTaskPriorityGet 应 该定义为1函数原型如下
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask )参数 xTask要查找的任务的任务句柄。 返回值获取到的对应的任务的优先级。
2、函数vTaskPrioritySet() 此函数用于改变某一个任务的任务优先级要使用此函数的话宏 INCLUDE_vTaskPrioritySet 应该定义为1函数原型如下
void vTaskPrioritySet( TaskHandle_t xTask,UBaseType_t uxNewPriority )参数 xTask要查找的任务的任务句柄。 uxNewPriority: 任务要使用的新的优先级可以是0~ configMAX_PRIORITIES – 1。 返回值无。
3、uxTaskGetSystemState() 此函数用于获取系统中所有任务的任务壮态每个任务的壮态信息保存在一个TaskStatus_t 类型的结构体里面这个结构体里面包含了任务的任务句柄、任务名字、堆栈、优先级等信息 要使用此函数的话宏configUSE_TRACE_FACILITY 应该定义为1函数原型如下
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray,const UBaseType_t uxArraySize,uint32_t * const pulTotalRunTime )参数 pxTaskStatusArray指向TaskStatus_t 结构体类型的数组首地址每个任务至少需要一个 TaskStatus_t 结构体任务的数量可以使用函数 uxTaskGetNumberOfTasks()。结构体TaskStatus_t 在文件task.h 中有如下 定义
typedef struct xTASK_STATUS
{TaskHandle_t xHandle; //任务句柄const char * pcTaskName; //任务名字UBaseType_t xTaskNumber; //任务编号eTaskState eCurrentState; //当前任务壮态eTaskState 是一个枚举类型UBaseType_t uxCurrentPriority; //任务当前的优先级UBaseType_t uxBasePriority; //任务基础优先级uint32_t ulRunTimeCounter;//任务运行的总时间StackType_t * pxStackBase; //堆栈基地址uint16_t usStackHighWaterMark; //从任务创建以来任务堆栈剩余的最小大小此//值如果太小的话说明堆栈有溢出的风险。
} TaskStatus_t;
uxArraySize: 保存任务壮态数组的数组的大小。 pulTotalRunTime: 如果configGENERATE_RUN_TIME_STATS 为1 的话此参数用来保存系 统总的运行时间。 返回值统计到的任务壮态的个数也就是填写到数组pxTaskStatusArray 中的个 数此值应该等于函数uxTaskGetNumberOfTasks()的返回值。如果参数 uxArraySize 太小的话返回值可能为0。 4、函数vTaskGetInfo() 此函数也是用来获取任务壮态的但是是获取指定的单个任务的壮态的任务的壮态信息 填充到参数pxTaskStatus 中这个参数也是TaskStatus_t 类型的。要使用此函数的话宏 configUSE_TRACE_FACILITY 要定义为1函数原型如下
void vTaskGetInfo( TaskHandle_t xTask,TaskStatus_t * pxTaskStatus,BaseType_t xGetFreeStackSpace,eTaskState eState )参数 xTask要查找的任务的任务句柄。 pxTaskStatus: 指向类型为TaskStatus_t 的结构体变量。 xGetFreeStackSpace: 在结构体TaskStatus_t 中有个字段usStackHighWaterMark 来保存自任务 运行以来任务堆栈剩余的历史最小大小这个值越小说明越接近堆栈溢 出但是计算这个值需要花费一点时间所以我们可以通过将 xGetFreeStackSpace 设置为pdFALSE 来跳过这个步骤当设置为pdTRUE 的时候就会检查堆栈的历史剩余最小值。 eState: 结构体TaskStatus_t 中有个字段eCurrentState 用来保存任务运行壮态 这个字段是eTaskState 类型的这是个枚举类型在task.h 中有如下定 义
typedef enum
{eRunning 0, //运行壮态eReady, //就绪态eBlocked, //阻塞态eSuspended, //挂起态eDeleted, //任务被删除eInvalid //无效
} eTaskState;获取任务运行壮态会耗费不少时间所以为了加快函数vTaskGetInfo()的执行 速度结构体TaskStatus_t 中的字段eCurrentState 就可以由用户直接赋值 参数eState 就是要赋的值。如果不在乎这点时间那么可以将eState 设置为 eInvalid这样任务的壮态信息就由函数vTaskGetInfo()去想办法获取。 返回值无。 5、函数xTaskGetApplicationTaskTag() 此函数用于获取任务的Tag(标签)值任务控制块中有个成员变量pxTaskTag 来保存任务的 标签值。标签的功能由用户自行决定此函数就是用来获取这个标签值的FreeRTOS 系统内核 是不会使用到这个标签的。要使用此函数的话宏configUSE_APPLICATION_TASK_TAG 必须为 1函数原型如下
TaskHookFunction_t xTaskGetApplicationTaskTag( TaskHandle_t xTask )参数 xTask要获取标签值的任务对应的任务句柄如果为NULL 的话就获取当前正在运 行的任务标签值。 返回值任务的标签值。
6、函数xTaskGetCurrentTaskHandle() 此函数用于获取当前任务的任务句柄其实获取到的就是任务控制块在前面讲解任务创 建函数的时候说过任务句柄就是任务控制。如果要使用此函数的话宏 INCLUDE_xTaskGetCurrentTaskHandle 应该为1函数原型如下
TaskHandle_t xTaskGetCurrentTaskHandle( void )参数无 返回值当前任务的任务句柄。 7、函数xTaskGetHandle() 此函数根据任务名字获取任务的任务句柄在使用函数xTaskCreate()或xTaskCreateStatic() 创建任务的时候都会给任务分配一个任务名函数xTaskGetHandle()就是使用这个任务名字来 查询其对应的任务句柄的。要使用此函数的话宏INCLUDE_xTaskGetHandle 应该设置为1此 函数原型如下
TaskHandle_t xTaskGetHandle( const char * pcNameToQuery )参数 pcNameToQuery任务名C 语言字符串。 返回值 NULL没有任务名pcNameToQuery 所对应的任务。 其他值任务名pcNameToQuery 所对应的任务句柄 8、函数xTaskGetIdleTaskHandle() 此函数用于返回空闲任务的任务句柄要使用此函数的话宏 INCLUDE_xTaskGetIdleTaskHandle 必须为1函数原型如下
TaskHandle_t xTaskGetIdleTaskHandle( void )参数无 返回值空闲任务的任务句柄。
9、函数uxTaskGetStackHighWaterMark() 每个任务都有自己的堆栈堆栈的总大小在创建任务的时候就确定了此函数用于检查任 务从创建好到现在的历史剩余最小值这个值越小说明任务堆栈溢出的可能性就越大 FreeRTOS 把这个历史剩余最小值叫做“高水位线”。此函数相对来说会多耗费一点时间所以 在代码调试阶段可以使用产品发布的时候最好不要使用。要使用此函数的话宏 INCLUDE_uxTaskGetStackHighWaterMark 必须为1此函数原型如下
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask )参数 xTask要查询的任务的任务句柄当这个参数为NULL 的话说明查询自身任务(即调用 函数uxTaskGetStackHighWaterMark()的任务)的“高水位线”。 返回值任务堆栈的“高水位线”值也就是堆栈的历史剩余最小值。
10、函数eTaskGetState() 此函数用于查询某个任务的运行壮态比如运行态、阻塞态、挂起态、就绪态等返回 值是个枚举类型。要使用此函数的话宏INCLUDE_eTaskGetState 必须为1函数原型如下
eTaskState eTaskGetState( TaskHandle_t xTask )参数 xTask要查询的任务的任务句柄。 返回值返回值为eTaskState 类型这是个枚举类型在文件task.h 中有定义前面讲解 函数vTaskGetInfo()的时候已经讲过了。
11、函数pcTaskGetName() 根据某个任务的任务句柄来查询这个任务对应的任务名函数原型如下
char *pcTaskGetName( TaskHandle_t xTaskToQuery )参数 xTaskToQuery要查询的任务的任务句柄此参数为NULL 的话表示查询自身任务(调 用函数pcTaskGetName())的任务名字 返回值返回任务所对应的任务名。
12、函数xTaskGetTickCount() 此函数用于查询任务调度器从启动到现在时间计数器xTickCount 的值。xTickCount 是系统 的时钟节拍值并不是真实的时间值。每个滴答定时器中断xTickCount 就会加1一秒钟滴答 定时器中断多少次取决于宏configTICK_RATE_HZ。理论上xTickCount 存在溢出的问题但是 这个溢出对于FreeRTOS 的内核没有影响但是如果用户的应用程序有使用到的话就要考虑溢 出了。什么时候溢出取决于宏configUSE_16_BIT_TICKS当此宏为1 的时候xTixkCount 就是 个16 位的变量当为0 的时候就是个32 位的变量。函数原型如下
TickType_t xTaskGetTickCount( void )参数无。 返回值时间计数器xTickCount 的值。
13、函数xTaskGetTickCountFromISR() 此函数是xTaskGetTickCount()的中断级版本用于在中断服务函数中获取时间计数器 xTickCount 的值函数原型如下
TickType_t xTaskGetTickCountFromISR( void )参数无。 返回值时间计数器xTickCount 的值。
14、函数xTaskGetSchedulerState() 此函数用于获取FreeRTOS 的任务调度器运行情况运行关闭还是挂起要使用此函 数的话宏INCLUDE_xTaskGetSchedulerState 必须为1此函数原型如下
BaseType_t xTaskGetSchedulerState( void )参数无。 返回值 taskSCHEDULER_NOT_STARTED调度器未启动调度器的启动是通过函数 vTaskStartScheduler() 来完成所以在函数 vTaskStartScheduler() 未调用之前调用函数 xTaskGetSchedulerState()的话就会返回此值。 taskSCHEDULER_RUNNING调度器正在运行。 taskSCHEDULER_SUSPENDED调度器挂起。
15、函数uxTaskGetNumberOfTasks() 此函数用于查询系统当前存在的任务数量函数原型如下
UBaseType_t uxTaskGetNumberOfTasks( void )参数无。 返回值当前系统中存在的任务数量此值挂起态的任务阻塞态的任务就绪态的任务 空闲任务运行态的任务。
16、函数vTaskList() 此函数会创建一个表格来描述每个任务的详细信息如图11.2.1 所示 表中的信息如下 Name创建任务的时候给任务分配的名字。 State任务的壮态信息B 是阻塞态R 是就绪态S 是挂起态D 是删除态。 Priority任务优先级。 Stack任务堆栈的“高水位线”就是堆栈历史最小剩余大小。 Num任务编号这个编号是唯一的当多个任务使用同一个任务名的时候可以通过此 编号来做区分。 函数原型如下
void vTaskList( char * pcWriteBuffer )参数 pcWriteBuffer保存任务壮态信息表的存储区。存储区要足够大来保存任务状态信息表。 返回值无
17、函数vTaskGetRunTimeStats() FreeRTOS 可以通过相关的配置来统计任务的运行时间信息任务的运行时间信息提供了 每个任务获取到CPU 使用权总的时间。函数vTaskGetRunTimeStats()会将统计到的信息填充到 一个表里面表里面提供了每个任务的运行时间和其所占总时间的百分比如图11.2.2 所示 函数vTaskGetRunTimeStats() 是一个很实用的函数要使用此函数的话宏 configGENERATE_RUN_TIME_STATS 和configUSE_STATS_FORMATTING_FUNCTIONS 必须 都为1。如果宏configGENERATE_RUN_TIME_STATS 为1 的话还需要实现一下几个宏定义 ●portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()此宏用来初始化一个外设来 提供时间统计功能所需的时基一般是定时器/计数器。这个时基的分辨率一定要比FreeRTOS 的系统时钟高一般这个时基的时钟精度比系统时钟的高10~20 倍就可以了。 ●portGET_RUN_TIME_COUNTER_VALUE()或者 portALT_GET_RUN_TIME_COUNTER_VALUE(Time)这两个宏实现其中一个就行 了这两个宏用于提供当前的时基的时间值。 函数原型如下
void vTaskGetRunTimeStats( char *pcWriteBuffer )参数 pcWriteBuffer保存任务时间信息的存储区。存储区要足够大来保存任务时间信息。 返回值无
18、函数vTaskSetApplicationTaskTag() 此函数是为高级用户准备的此函数用于设置某个任务的标签值这个标签值的具体函数 和用法由用户自行决定FreeRTOS 内核不会使用这个标签值如果要使用此函数的话宏 configUSE_APPLICATION_TASK_TAG 必须为1函数原型如下
void vTaskSetApplicationTaskTag( TaskHandle_t xTask,TaskHookFunction_t pxHookFunction )参数 xTask要设置标签值的任务此值为NULL 的话表示设置自身任务的标签值。 pxHookFunction要设置的标签值这是一个TaskHookFunction_t 类型的函数指针但是 也可以设置为其他值。 返回值无
19、函数SetThreadLocalStoragePointer() 此函数用于设置线程本地存储指针的值每个任务都有它自己的指针数组来作为线程本地 存储使用这些线程本地存储可以用来在任务控制块中存储一些应用信息这些信息只属于任 务自己的。线程本地存储指针数组的大小由宏 configNUM_THREAD_LOCAL_STORAGE_POINTERS 来决定的。如果要使用此函数的话宏 configNUM_THREAD_LOCAL_STORAGE_POINTERS 不能为0宏的具体值是本地存储指针 数组的大小函数原型如下
void vTaskSetThreadLocalStoragePointer( TaskHandle_t xTaskToSet,BaseType_t xIndex,void * pvValue )参数 xTaskToSet要设置线程本地存储指针的任务的任务句柄如果是NULL 的话表示设置任 务自身的线程本地存储指针。 xIndex要设置的线程本地存储指针数组的索引。 pvValue: 要存储的值。 返回值无
20、函数GetThreadLocalStoragePointer() 此函数用于获取线程本地存储指针的值如果要使用此函数的话宏 configNUM_THREAD_LOCAL_STORAGE_POINTERS 不能为0函数原型如下
void *pvTaskGetThreadLocalStoragePointer( TaskHandle_t xTaskToQuery,BaseType_t xIndex )参数 xTaskToSet要获取的线程本地存储指针的任务句柄如果是NULL 的话表示获取任务自 身的线程本地存储指针。 xIndex要获取的线程本地存储指针数组的索引。 返回值获取到的线程本地存储指针的值。
11.3 任务壮态查询API 函数实验
11.3.1 实验程序设计
FreeRTOS 与任务相关的API 函数中有很多是与任务壮态或者信息查询有关的比如函数 uxTaskGetSystemState()、vTaskGetInfo()、eTaskGetState()和vTaskList()。本实验我们就来学习这
些函数的使用方法。 1、实验目的 学习使用FreeRTOS 与任务壮态或者信息查询有关的API 函数包括uxTaskGetSystemState()、 vTaskGetInfo()、eTaskGetState()和vTaskList()。 2、实验设计 本实验设计三个任务start_task、led0_task 和query_task 这三个任务的任务功能如下 start_task用来创建其他2 个任务。 led0_task 控制LED0 灯闪烁提示系统正在运行。 query_task 任务壮态和信息查询任务在此任务中学习使用与任务的壮态和信息查询有 关的API 函数。 实验需要一个按键KEY_UP这四个按键的功能如下 KEY_UP: 控制程序的运行步骤。 3、实验工程 FreeRTOS 实验11-1 FreeRTOS 任务壮态或信息查询。 4、实验程序与分析 ●任务设置 实验中任务优先级、堆栈大小和任务句柄等的设置如下
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数#define LED0_TASK_PRIO 2 //任务优先级
#define LED0_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Led0Task_Handler; //任务句柄
void led0_task(void *pvParameters); //任务函数#define QUERY_TASK_PRIO 3 //任务优先级
#define QUERY_STK_SIZE 256 //任务堆栈大小
TaskHandle_t QueryTask_Handler; //任务句柄
void query_task(void *pvParameters); //任务函数
char InfoBuffer[1000]; //保存信息的数组
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键LCD_Init(); //初始化LCDPOINT_COLOR RED;LCD_ShowString(30,10,200,16,16,ATK STM32F103/407);LCD_ShowString(30,30,200,16,16,FreeRTOS Examp 11-1);LCD_ShowString(30,50,200,16,16,Task Info Query);LCD_ShowString(30,70,200,16,16,ATOMALIENTEK);LCD_ShowString(30,90,200,16,16,2016/11/25);//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )start_task, //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(TaskHandle_t* )StartTask_Handler); //任务句柄vTaskStartScheduler(); //开启任务调度
}
在main 函数中我们主要完成硬件的初始化在硬件初始化完成以后创建了任务start_task() 并且开启了FreeRTOS 的任务调度。 ●任务函数
//led0 任务函数
void led0_task(void *pvParameters)
{while(1){LED0~LED0;vTaskDelay(500); //延时500ms也就是500 个时钟节拍}
}
//query 任务函数
void query_task(void *pvParameters)
{u32 TotalRunTime;UBaseType_t ArraySize,x;TaskStatus_t *StatusArray;//第一步函数uxTaskGetSystemState()的使用printf(/********第一步函数uxTaskGetSystemState()的使用**********/\r\n);ArraySizeuxTaskGetNumberOfTasks(); //获取系统任务数量(1)StatusArraypvPortMalloc(ArraySize*sizeof(TaskStatus_t)); //申请内存(2)if(StatusArray!NULL) //内存申请成功{ArraySizeuxTaskGetSystemState((TaskStatus_t* )StatusArray, (3)(UBaseType_t )ArraySize,(uint32_t* )TotalRunTime);printf(TaskName\t\tPriority\t\tTaskNumber\t\t\r\n);for(x0;xArraySize;x){//通过串口打印出获取到的系统任务的有关信息比如任务名称、//任务优先级和任务编号。printf(%s\t\t%d\t\t\t%d\t\t\t\r\n, (4)StatusArray[x].pcTaskName,(int)StatusArray[x].uxCurrentPriority,(int)StatusArray[x].xTaskNumber);}}vPortFree(StatusArray); //释放内存(5)printf(/**************************结束***************************/\r\n);printf(按下KEY_UP 键继续!\r\n\r\n\r\n);while(KEY_Scan(0)!WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下//第二步函数vTaskGetInfo()的使用TaskHandle_t TaskHandle;TaskStatus_t TaskStatus;printf(/************第二步函数vTaskGetInfo()的使用**************/\r\n);TaskHandlexTaskGetHandle(led0_task); //根据任务名获取任务句柄。(6)//获取LED0_Task 的任务信息vTaskGetInfo((TaskHandle_t )TaskHandle, //任务句柄(7)(TaskStatus_t* )TaskStatus, //任务信息结构体(BaseType_t )pdTRUE, //允许统计任务堆栈历史最小剩余大小(eTaskState )eInvalid); //函数自己获取任务运行壮态//通过串口打印出指定任务的有关信息。printf(任务名: %s\r\n,TaskStatus.pcTaskName); (8)printf(任务编号: %d\r\n,(int)TaskStatus.xTaskNumber);printf(任务壮态: %d\r\n,TaskStatus.eCurrentState);printf(任务当前优先级: %d\r\n,(int)TaskStatus.uxCurrentPriority);printf(任务基优先级: %d\r\n,(int)TaskStatus.uxBasePriority);printf(任务堆栈基地址: %#x\r\n,(int)TaskStatus.pxStackBase);printf(任务堆栈历史剩余最小值:%d\r\n,TaskStatus.usStackHighWaterMark);printf(/**************************结束***************************/\r\n);printf(按下KEY_UP 键继续!\r\n\r\n\r\n);while(KEY_Scan(0)!WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下//第三步函数eTaskGetState()的使用eTaskState TaskState;char TaskInfo[10];printf(/***********第三步函数eTaskGetState()的使用*************/\r\n);TaskHandlexTaskGetHandle(query_task); //根据任务名获取任务句柄。(9)TaskStateeTaskGetState(TaskHandle); //获取query_task 任务的任务壮态(10)memset(TaskInfo,0,10); //数组清零switch((int)TaskState) (11){case 0:sprintf(TaskInfo,Running);break;case 1:sprintf(TaskInfo,Ready);break;case 2:sprintf(TaskInfo,Suspend);break;case 3:sprintf(TaskInfo,Delete);break;case 4:sprintf(TaskInfo,Invalid);break;}printf(任务壮态值:%d,对应的壮态为:%s\r\n,TaskState,TaskInfo); (12)printf(/**************************结束**************************/\r\n);printf(按下KEY_UP 键继续!\r\n\r\n\r\n);while(KEY_Scan(0)!WKUP_PRES) delay_ms(10); //等待KEY_UP 键按下//第四步函数vTaskList()的使用printf(/*************第三步函数vTaskList()的使用*************/\r\n);vTaskList(InfoBuffer); //获取所有任务的信息(13)printf(%s\r\n,InfoBuffer); //通过串口打印所有任务的信息(14)while(1){LED1~LED1;vTaskDelay(1000); //延时1s也就是1000 个时钟节拍}
}(1)、使用函数uxTaskGetNumberOfTasks()获取当前系统中的任务数量因为要根据任务数 量给任务信息数组StatusArray 分配内存。注意这里StatusArray 是个指向TaskStatus_t 类型的 指针但是在使用的时候会把他当作一个数组来用。 (2)、调用函数pvPortMalloc()给任务信息数组StatusArray 分配内存数组是TaskStatus_t 类 型。 (3)、调用函数uxTaskGetSystemState()获取系统中所有任务的信息并将获取到的信息保存 在StatusArray 中。 (4)、通过串口将获取到的所有任务的部分信息打印出来这里并没有把所获取到的信息都 输出只是将任务的任务名、任务优先级和任务编号做了输出。 (5)、任务信息数组StatusArray 使用完毕释放其内存。 (6)、调用函数xTaskGetHandle()根据任务名来获取任务句柄这里获取任务名为“led0_task”的任务句柄。我们在创建任务的时候一般都会保存任务的句柄如果保存了任务句柄的话就可 以直接使用。 (7)、调用函数vTaskGetInfo()获取任务名为“led0_task”的任务信息任务信息保存在 TaskStatus 中。获取任务信息的时候允许统计任务堆栈历史最小剩余大小任务的运行壮态也是 由函数vTaskGetInfo()来统计。 (8)、通过串口输出获取到的任务led0_task 的任务信息。 (9)、通过函数xTaskGetHandle()获取任务名为“query_task”的任务句柄。 (10)、调用函数eTaskGetState()获取任务的运行壮态。 (11)、通过函数eTaskGetState()获取到的任务运行壮态是个枚举类型eTaskState枚举类型 不同的值表示不同的含义这里用字符串来描述这些枚举值的含义。 (12)、通过串口输出任务query_task 的运行壮态信息。 (13)、调用函数vTaskList()统计所有任务的信息统计出来的任务信息存储在缓冲区 InfoBuffer 中这些任务信息以表格的形式呈现 (14)、通过串口输出保存在缓冲区InfoBuffer 中的任务信息。
11.3.2 程序运行结果分析
编译并下载实验代码到开发板中打开串口调试助手然后按照任务函数query_task()中的 步骤一步步的测试分析。 ●第一步 第一步是学习uxTaskGetSystemState()函数的使用通过此函数获取到系统中当前所有任务 的信息并且通过串口输出其中的一部分信息这里我们输出了任务名、任务的优先级和任务 的编号如图11.3.2.1 所示 从图11.3.2.1 可以看出空闲任务的任务优先级最低为0定时器服务任务优先级最高为 31。而且任务的优先级和任务的编号是不同的优先级是用户自行设定的而编号是根据任务 创建的先后顺序来自动分配的。 ●第二步 第二步是通过函数vTaskGetInfo()来获取任务名为“led0_task”的任务信息并通过串口输 出如图11.3.2.2 所示 图11.3.2.2 中可以看出有关任务led0_task 的详细信息包括任务名、任务编号、任务当前 优先级、任务基优先级任务堆栈基地址和堆栈历史剩余最小值。 ●第三步 第三步通过函数eTaskGetStat()来任务query_task 的运行壮态串口调试助手显示如图 11.3.2.3 所示 ●第四步 第四步是通过函数vTaskList()获取系统中当前所有任务的详细信息并且将这些信息按照 表格的形式组织在一起存放在用户提供的缓冲区中例程中将这些信息放到了缓冲区InfoBuffer 中。最后通过串口输出缓冲区InfoBuffer 中的信息如图11.3.2.4 所示 图11.3.2.4 中各列的含义我们在讲解函数vTaskList()的时候已经详细的讲解过了。
11.4 任务运行时间信息统计实验
FreeRTOS 可以通过函数vTaskGetRunTimeStats()来统计每个任务使用CPU 的时间以及所 使用的时间占总时间的比例。在调试代码的时候我们可以根据这个时间使用值来分析哪个任务 的CPU 占用率高然后合理的分配或优化任务。本实验我们就来学习如何使用FreeRTOS 的这 个运行时间壮态统计功能。
11.4.1 相关宏的设置
要使用此功能的话宏configGENERATE_RUN_TIME_STATS 必须为1还需要在定义其他 两个宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()配置一个高精度定时器/计数器提供时基。 portGET_RUN_TIME_COUNTER_VALUE()读取时基的时间值。 这三个宏在FreeRTOSConfig.h 中定义如下:
#define configGENERATE_RUN_TIME_STATS 1
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ConfigureTimeForRunTimeStats()
#define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTimeTicks其中函数ConfigureTimeForRunTimeStats()和变量FreeRTOSRunTimeTicks 在timer.c 里面定义如下
//FreeRTOS 时间统计所用的节拍计数器
volatile unsigned long long FreeRTOSRunTimeTicks;//初始化TIM3 使其为FreeRTOS 的时间统计提供时基
void ConfigureTimeForRunTimeStats(void)
{//定时器3 初始化定时器时钟为72M分频系数为72-1所以定时器3 的频率//为72M/721M自动重装载为50-1那么定时器周期就是50usFreeRTOSRunTimeTicks0;TIM3_Int_Init(50-1,72-1); //初始化TIM3
}函数ConfigureTimeForRunTimeStats()其实就是初始化定时器因为时间统计功能需要用户 提供一个高精度的时钟这里使用定时器3。前面在讲函数vTaskGetRunTimeStats()的时候说过 这个时钟的精度要比FreeRTOS 的系统时钟高大约10~20 倍即可。FreeRTOS 系统时钟我们配 置的是1000HZ周期1ms这里我们将定时器3 的中断频率配置为20KHZ周期50us刚好 是系统时钟频率的20 倍。 定时器3 初始化函数如下
//通用定时器3 中断初始化
//这里时钟选择为APB1 的2 倍而APB1 为36M
//arr自动重装值。
//psc时钟预分频数
//这里使用的是定时器3!
void TIM3_Int_Init(u16 arr,u16 psc)
{TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能//定时器TIM3 初始化TIM_TimeBaseStructure.TIM_Period arr;TIM_TimeBaseStructure.TIM_Prescaler psc;TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1;TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; //TIM 向上计数模式TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure);TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3 中断,允许更新中断//中断优先级NVIC 设置NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; //TIM3 中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; //先占优先级1 级NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; //从优先级0 级NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; //IRQ 通道被使能NVIC_Init(NVIC_InitStructure); //初始化NVIC 寄存器TIM_Cmd(TIM3, ENABLE); //使能TIM3
}
//定时器3 中断服务函数
void TIM3_IRQHandler(void)
{if(TIM_GetITStatus(TIM3,TIM_IT_Update)SET) //溢出中断{FreeRTOSRunTimeTicks; //运行时间统计时基计数器加一}TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
FreeRTOSRunTimeTicks 是个全局变量用来为时间统计功能提供时间在定时器3 的中断 服务函数中进行更新。
11.4.2 实验程序设计
1、实验目的 学习使用FreeRTOS 运行时间壮态统计函数vTaskGetRunTimeStats()的使用。 2、实验设计 本实验设计四个任务start_task、task1_task 、task2_task 和RunTimeStats_task这四个任 务的任务功能如下 start_task用来创建其他3 个任务。 task1_task 应用任务1控制LED0 灯闪烁并且刷新LCD 屏幕上指定区域的颜色。 task2_task 应用任务2控制LED1 灯闪烁并且刷新LCD 屏幕上指定区域的颜色。 RunTimeStats_task获取按键值当KEY_UP 键按下以后就调用函数vTaskGetRunTimeStats() 获取任务的运行时间信息并且将其通过串口输出到串口调试助手上。 实验需要一个按键KEY_UP用来获取系统中任务运行时间信息。 3、实验工程 FreeRTOS 实验11-2 FreeRTOS 任务运行时间统计。 4、实验程序与分析 ●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数
#define TASK2_TASK_PRIO 3 //任务优先级
#define TASK2_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task2Task_Handler; //任务句柄
void task2_task(void *pvParameters); //任务函数
#define RUNTIMESTATS_TASK_PRIO 4 //任务优先级
#define RUNTIMESTATS_STK_SIZE 128 //任务堆栈大小
TaskHandle_t RunTimeStats_Handler; //任务句柄
void RunTimeStats_task(void *pvParameters); //任务函数
char RunTimeInfo[400]; //保存任务运行时间信息
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键LCD_Init(); //初始化LCDPOINT_COLOR RED;LCD_ShowString(30,10,200,16,16,ATK STM32F103/407);LCD_ShowString(30,30,200,16,16,FreeRTOS Examp 11-2);LCD_ShowString(30,50,200,16,16,Get Run Time Stats);LCD_ShowString(30,70,200,16,16,ATOMALIENTEK);LCD_ShowString(30,90,200,16,16,2016/11/25);//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )start_task, //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(TaskHandle_t* )StartTask_Handler); //任务句柄vTaskStartScheduler(); //开启任务调度
}在main 函数中我们主要完成硬件的初始化在硬件初始化完成以后创建了任务start_task() 并且开启了FreeRTOS 的任务调度。 ●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建TASK1 任务xTaskCreate((TaskFunction_t )task1_task,(const char* )task1_task,(uint16_t )TASK1_STK_SIZE,(void* )NULL,(UBaseType_t )TASK1_TASK_PRIO,(TaskHandle_t* )Task1Task_Handler);//创建TASK2 任务xTaskCreate((TaskFunction_t )task2_task,(const char* )task2_task,(uint16_t )TASK2_STK_SIZE,(void* )NULL,(UBaseType_t )TASK2_TASK_PRIO,(TaskHandle_t* )Task2Task_Handler);//创建RunTimeStats 任务xTaskCreate((TaskFunction_t )RunTimeStats_task,(const char* )RunTimeStats_task,(uint16_t )RUNTIMESTATS_STK_SIZE,(void* )NULL,(UBaseType_t )RUNTIMESTATS_TASK_PRIO,(TaskHandle_t* )RunTimeStats_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters)
{u8 task1_num0;POINT_COLOR BLACK;LCD_DrawRectangle(5,110,115,314); //画一个矩形LCD_DrawLine(5,130,115,130); //画线POINT_COLOR BLUE;LCD_ShowString(6,111,110,16,16,Task1 Run:000);while(1){task1_num; //任务执1 行次数加1 注意task1_num1 加到255 的时候会清零LED0!LED0;LCD_Fill(6,131,114,313,lcd_discolor[task1_num%14]); //填充区域LCD_ShowxNum(86,111,task1_num,3,16,0x80); //显示任务执行次数vTaskDelay(1000); //延时1s也就是1000 个时钟节拍}
}
//task2 任务函数
void task2_task(void *pvParameters)
{u8 task2_num0;POINT_COLOR BLACK;LCD_DrawRectangle(125,110,234,314); //画一个矩形LCD_DrawLine(125,130,234,130); //画线POINT_COLOR BLUE;LCD_ShowString(126,111,110,16,16,Task2 Run:000);while(1){task2_num; //任务2 执行次数加1 注意task1_num2 加到255 的时候会清零LED1!LED1;LCD_ShowxNum(206,111,task2_num,3,16,0x80); //显示任务执行次数LCD_Fill(126,131,233,313,lcd_discolor[13-task2_num%14]); //填充区域vTaskDelay(1000); //延时1s也就是1000 个时钟节拍}
}
//RunTimeStats 任务
void RunTimeStats_task(void *pvParameters)
{u8 key0;while(1){keyKEY_Scan(0);if(keyWKUP_PRES){memset(RunTimeInfo,0,400); //信息缓冲区清零vTaskGetRunTimeStats(RunTimeInfo); //获取任务运行时间信息(1)printf(任务名\t\t\t 运行时间\t 运行所占百分比\r\n);printf(%s\r\n,RunTimeInfo); (2)}vTaskDelay(10); //延时10ms也就是1000 个时钟节拍}
}(1)、调用函数vTaskGetRunTimeStats()获取任务运行时间信息此函数会统计任务的运行时 间并且将统计到的运行时间信息按照表格的形式组织在一起并存放在用户设置的缓冲区里面 缓冲区的首地址通过参数传递给函数vTaskGetRunTimeStats()。 (2)、通过串口输出统计到的任务运行时间信息。 11.4.3 程序运行结果分析 编译并下载实验代码到开发板中打开串口调试助手按下KEY_UP 按键输出任务的运行 时间信息如图11.4.3.1 所示 图11.4.3.1 任务运行时间信息 要注意函数vTaskGetRunTimeStats()相对来说会很耗时间所以不要太过于频繁的调用此 函数测试阶段可以使用此函数来分析任务的运行情况。还有就是运行时间不是真正的运行时 间真正的时间值要乘以50us。
第十二章FreeRTOS 时间管理
在使用FreeRTOS 的过程中我们通常会在一个任务函数中使用延时函数对这个任务延时 当执行延时函数的时候就会进行任务切换并且此任务就会进入阻塞态直到延时完成任务 重新进入就绪态。延时函数属于FreeRTOS 的时间管理本章我们就来学习一些FreeRTOS 的这 个时间管理过程看看在调用延时函数以后究竟发生了什么任务是如何进入阻塞态的在延 时完成以后任务又是如何从阻塞态恢复到就绪态的本章分为如下几部分 12.1 FreeRTOS 延时函数 12.3 FreeRTOS 系统时钟节拍
12.1 FreeRTOS 延时函数
12.1 函数vTaskDelay() 学习过UCOSIII 的朋友应该知道在UCOSIII 中延时函数OSTimeDly()可以设置为三种模 式相对模式、周期模式和绝对模式。在FreeRTOS 中延时函数也有相对模式和绝对模式不 过在FreeRTOS 中不同的模式用的函数不同其中函数vTaskDelay()是相对模式(相对延时函数) 函数vTaskDelayUntil()是绝对模式(绝对延时函数)。函数vTaskDelay()在文件tasks.c 中有定义 要使用此函数的话宏INCLUDE_vTaskDelay 必须为1函数代码如下
void vTaskDelay( const TickType_t xTicksToDelay )
{BaseType_t xAlreadyYielded pdFALSE;//延时时间要大于0。if( xTicksToDelay ( TickType_t ) 0U ) (1){configASSERT( uxSchedulerSuspended 0 );vTaskSuspendAll(); (2){traceTASK_DELAY();prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE ); (3)}xAlreadyYielded xTaskResumeAll(); (4)}else{mtCOVERAGE_TEST_MARKER();}if( xAlreadyYielded pdFALSE ) (5){portYIELD_WITHIN_API(); (6)}else{mtCOVERAGE_TEST_MARKER();}
}(1)、延时时间由参数xTicksToDelay 来确定为要延时的时间节拍数延时时间肯定要大 于0。否则的话相当于直接调用函数portYIELD()进行任务切换。 (2)、调用函数vTaskSuspendAll()挂起任务调度器。 (3) 、调用函数prvAddCurrentTaskToDelayedList() 将要延时的任务添加到延时列表 pxDelayedTaskList 或者pxOverflowDelayedTaskList() 中。后面会具体分析函数 prvAddCurrentTaskToDelayedList()。 (4)、调用函数xTaskResumeAll()恢复任务调度器。 (5)、如果函数xTaskResumeAll()没有进行任务调度的话那么在这里就得进行任务调度。
(6)、调用函数portYIELD_WITHIN_API()进行一次任务调度。 12.2 函数prvAddCurrentTaskToDelayedList() 函数prvAddCurrentTaskToDelayedList()用于将当前任务添加到等待列表中函数在文件 tasks.c 中有定义缩减后的函数如下
static void prvAddCurrentTaskToDelayedList( TickType_t x TicksToWait,const BaseType_t xCanBlockIndefinitely )
{TickType_t xTimeToWake;const TickType_t xConstTickCount xTickCount; (1)
#if( INCLUDE_xTaskAbortDelay 1 ){//如果使能函数xTaskAbortDelay()的话复位任务控制块的ucDelayAborted 字段为//pdFALSE。pxCurrentTCB-ucDelayAborted pdFALSE;}
#endifif( uxListRemove( ( pxCurrentTCB-xStateListItem ) ) ( UBaseType_t ) 0 ) (2){portRESET_READY_PRIORITY( pxCurrentTCB-uxPriority, uxTopReadyPriority ); (3)}else{mtCOVERAGE_TEST_MARKER();}
#if ( INCLUDE_vTaskSuspend 1 ){if( ( xTicksToWait portMAX_DELAY ) ( xCanBlockIndefinitely ! pdFALSE ) )(4){vListInsertEnd( xSuspendedTaskList, ( pxCurrentTCB-xStateListItem ) ); (5)}else{xTimeToWake xConstTickCount xTicksToWait; (6)listSET_LIST_ITEM_VALUE( ( pxCurrentTCB-xStateListItem ), \ (7)xTimeToWake );if( xTimeToWake xConstTickCount ) (8){vListInsert( pxOverflowDelayedTaskList, ( pxCurrentTCB-\ (9)xStateListItem ) );}else{vListInsert( pxDelayedTaskList, ( pxCurrentTCB-xStateListItem ) ); (10)if( xTimeToWake xNextTaskUnblockTime ) (11){xNextTaskUnblockTime xTimeToWake; (12)}else{mtCOVERAGE_TEST_MARKER();}}}}/***************************************************************************//****************************其他条件编译语句*******************************//***************************************************************************/
}(1)、读取进入函数prvAddCurrentTaskToDelayedList()的时间点并保存在xConstTickCount 中 后面计算任务唤醒时间点的时候要用到。xTickCount 是时钟节拍计数器每个滴答定时器中断 xTickCount 都会加一。 (2)、要将当前正在运行的任务添加到延时列表中肯定要先将当前任务从就绪列表中移除。 (3)、将当前任务从就绪列表中移除以后还要取消任务在uxTopReadyPriority 中的就绪标记。 也就是将uxTopReadyPriority 中对应的bit 清零。 (4) 、延时时间为最大值portMAX_DELAY 并且xCanBlockIndefinitely 不为 pdFALSE(xCanBlockIndefinitely 不为pdFALSE 的话表示允许阻塞任务)的话直接将当前任务添 加到挂起列表中任务就不用添加到延时列表中。 (5)、将当前任务添加到挂起列表xSuspendedTaskList 的末尾。 (6)、计算任务唤醒时间点也就是(1)中获取到的进入函数prvAddCurrentTaskToDelayedList() 的时间值xConstTickCount 加上延时时间值xTicksToWait。 (7)、将计算到的任务唤醒时间点值xTimeToWake 写入到任务列表中壮态列表项的相应字 段中。 (8)、计算得到的任务唤醒时间点小于xConstTickCount说明发生了溢出。全局变量 xTickCount 是TickType_t 类型的这是个32 位的数据类型因此在用xTickCount 计算任务唤 醒时间点xTimeToWake 的时候的肯定会出现溢出的现象。FreeRTOS 针对此现象专门做了处理 在FreeROTS 中定义了两个延时列表xDelayedTaskList1 和xDelayedTaskList2并且也定义了两 个指针pxDelayedTaskList 和pxOverflowDelayedTaskList 来访问这两个列表在初始化列表函数 prvInitialiseTaskLists() 中指针pxDelayedTaskList 指向了列表xDelayedTaskList1 指针 pxOverflowDelayedTaskList 指向了列表xDelayedTaskList2。这样发生溢出的话就将任务添加到 pxOverflowDelayedTaskList 所指向的列表中如果没有溢出的话就添加到pxDelayedTaskList 所 指向的列表中。 (9)、如果发生了溢出的话就将当前任务添加到pxOverflowDelayedTaskList 所指向的列表中。 (10)、如果没有发生溢出的话就将当前任务添加到pxDelayedTaskList 所指向的列表中。 (11)、xNextTaskUnblockTime 是个全局变量保存着距离下一个要取消阻塞的任务最小时 间点值。当xTimeToWake 小于xNextTaskUnblockTime 的话说明有个更小的时间点来了。 (12)、更新xNextTaskUnblockTime 为xTimeToWake。 12.3 函数vTaskDelayUntil() 函数vTaskDelayUntil()会阻塞任务阻塞时间是一个绝对时间那些需要按照一定的频率 运行的任务可以使用函数vTaskDelayUntil()。此函数再文件tasks.c 中有如下定义
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,const TickType_t xTimeIncrement )
{TickType_t xTimeToWake;BaseType_t xAlreadyYielded, xShouldDelay pdFALSE;configASSERT( pxPreviousWakeTime );configASSERT( ( xTimeIncrement 0U ) );configASSERT( uxSchedulerSuspended 0 );vTaskSuspendAll(); (1){const TickType_t xConstTickCount xTickCount; (2)xTimeToWake *pxPreviousWakeTime xTimeIncrement; (3)if( xConstTickCount *pxPreviousWakeTime ) (4){if( ( xTimeToWake *pxPreviousWakeTime ) ( xTimeToWake \ (5)xConstTickCount ) ){xShouldDelay pdTRUE; (6)}else{mtCOVERAGE_TEST_MARKER();}}else{if( ( xTimeToWake *pxPreviousWakeTime ) || ( xTimeToWake \ (7)xConstTickCount ) ){xShouldDelay pdTRUE; (8)}else{mtCOVERAGE_TEST_MARKER();}}*pxPreviousWakeTime xTimeToWake; (9)if( xShouldDelay ! pdFALSE ) (10){traceTASK_DELAY_UNTIL( xTimeToWake );prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );(11)}else{mtCOVERAGE_TEST_MARKER();}}xAlreadyYielded xTaskResumeAll(); (12)if( xAlreadyYielded pdFALSE ){ortYIELD_WITHIN_API();}else{mtCOVERAGE_TEST_MARKER();}
}参数 pxPreviousWakeTime上一次任务延时结束被唤醒的时间点任务中第一次调用函数 vTaskDelayUntil 的话需要将pxPreviousWakeTime 初始化进入任务的while()循环体的时间点值。 在以后的运行中函数vTaskDelayUntil()会自动更新pxPreviousWakeTime。 xTimeIncrement任务需要延时的时间节拍数(相对于pxPreviousWakeTime 本次延时的节 拍数)。 (1)、挂起任务调度器。 (2)、记录进入函数vTaskDelayUntil()的时间点值并保存在xConstTickCount 中。 (3)、根据延时时间xTimeIncrement 来计算任务下一次要唤醒的时间点并保存在 xTimeToWake 中。可以看出这个延时时间是相对于pxPreviousWakeTime 的也就是上一次任务 被唤醒的时间点。pxPreviousWakeTime、xTimeToWake、xTimeIncrement 和xConstTickCount 的 关系如图12.3.1 所示。 如12.3.1 中(1)为任务主体也就是任务真正要做的工作(2)是任务函数中调用 vTaskDelayUntil()对任务进行延时(3)为其他任务在运行。任务的延时时间是xTimeIncrement 这个延时时间是相对于pxPreviousWakeTime 的可以看出任务总的执行时间一定要小于任务的 延时时间xTimeIncrement也就是说如果使用vTaskDelayUntil()的话任务相当于任务的执行周 期永远都是xTimeIncrement而任务一定要在这个时间内执行完成。这样就保证了任务永远按 照一定的频率运行了这个延时值就是绝对延时时间因此函数vTaskDelayUntil()也叫做绝对 延时函数。 (4)、根据图12.3.1 可以看出理论上xConstTickCount 要大于pxPreviousWakeTime 的但 是也有一种情况会导致xConstTickCount 小于pxPreviousWakeTime那就是xConstTickCount 溢 出了 (5)、既然xConstTickCount 都溢出了那么计算得到的任务唤醒时间点肯定也是要溢出的 并且xTimeToWake 肯定也是要大于xConstTickCount 的。这种情况如图12.3.2 所示 (6)、如果满足(5)条件的话就将pdTRUE 赋值给xShouldDelay标记允许延时。 (7)、还有其他两种情况一只有xTimeToWake 溢出二都没有溢出。只有xTimeToWake 溢出的话如图12.3.3 所示 都不溢出的话就如图12.3.1 所示这两种情况都允许进行延时。 (8)、将pdTRUE 赋值给xShouldDelay标记允许延时。 (9)、更新pxPreviousWakeTime 的值更新为xTimeToWake为本函数的下一次执行做准 备。 (10)、经过前面的判断允许进行任务延时。 (11)、调用函数prvAddCurrentTaskToDelayedList()进行延时。函数的第一个参数是设置任务 的阻塞时间前面我们已经计算出了任务下一次唤醒时间点了那么任务还需要阻塞的时间就 是下一次唤醒时间点xTimeToWake 减去当前的时间xConstTickCount。而在函数vTaskDelay()中 只是简单的将这参数设置为xTicksToDelay。 (12)、调用函数xTaskResumeAll()恢复任务调度器。 函数vTaskDelayUntil()的使用方法如下
void TestTask( void * pvParameters )
{TickType_t PreviousWakeTime;//延时50ms但是函数vTaskDelayUntil()的参数需要设置的是延时的节拍数不能直接//设置延时时间因此使用函数pdMS_TO_TICKS 将时间转换为节拍数。const TickType_t TimeIncrement pdMS_TO_TICKS( 50 );PreviousWakeTime xTaskGetTickCount(); //获取当前的系统节拍值for( ;; ){/******************************************************************//*************************任务主体*********************************//******************************************************************///调用函数vTaskDelayUntil 进行延时vTaskDelayUntil( PreviousWakeTime, TimeIncrement);}
}其实使用函数vTaskDelayUntil()延时的任务也不一定就能周期性的运行使用函数 vTaskDelayUntil()只能保证你按照一定的周期取消阻塞进入就绪态。如果有更高优先级或者中断的话你还是得等待其他的高优先级任务或者中断服务函数运行完成才能轮到你。这个绝对延时只是相对于vTaskDelay()这个简单的延时函数而言的。
12.2 FreeRTOS 系统时钟节拍
不管是什么系统运行都需要有个系统时钟节拍前面已经提到多次了xTickCount 就是 FreeRTOS 的系统时钟节拍计数器。每个滴答定时器中断中xTickCount 就会加一xTickCount 的 具体操作过程是在函数xTaskIncrementTick()中进行的此函数在文件tasks.c 中有定义如下
BaseType_t xTaskIncrementTick( void )
{TCB_t * pxTCB;TickType_t xItemValue;BaseType_t xSwitchRequired pdFALSE;//每个时钟节拍中断(滴答定时器中断)调用一次本函数增加时钟节拍计数器xTickCount 的//值并且检查是否有任务需要取消阻塞。traceTASK_INCREMENT_TICK( xTickCount );if( uxSchedulerSuspended ( UBaseType_t ) pdFALSE ) (1){const TickType_t xConstTickCount xTickCount 1; (2)//增加系统节拍计数器xTickCount 的值当为0也就是溢出的话就交换延时和溢出列//表指针值。xTickCount xConstTickCount;if( xConstTickCount ( TickType_t ) 0U ) (3){taskSWITCH_DELAYED_LISTS(); (4)}else{mtCOVERAGE_TEST_MARKER();}//判断是否有任务延时时间到了任务都会根据唤醒时间点值按照顺序(由小到大的升//序排列)添加到延时列表中这就意味这如果延时列表中第一个列表项对应的任务的//延时时间都没有到的话后面的任务就不用看了肯定也没有到。if( xConstTickCount xNextTaskUnblockTime ) (5){for( ;; ){if( listLIST_IS_EMPTY( pxDelayedTaskList ) ! pdFALSE ) (6){//延时列表为空,设置xNextTaskUnblockTime 为最大值。xNextTaskUnblockTime portMAX_DELAY; (7)break;}else{//延时列表不为空获取延时列表的第一个列表项的值根据判断这个值//判断任务延时时间是否到了如果到了的话就将任务移除延时列表。pxTCB ( TCB_t * )\ (8)listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );xItemValue \ (9)listGET_LIST_ITEM_VALUE( ( pxTCB-xStateListItem ) );if( xConstTickCount xItemValue ) (10){//任务延时时间还没到但是xItemValue 保存着下一个即将解除//阻塞态的任务对应的解除时间点所以需要用xItemValue 来更新//变量xNextTaskUnblockTimexNextTaskUnblockTime xItemValue; (11)break;}else{mtCOVERAGE_TEST_MARKER();}//将任务从延时列表中移除( void ) uxListRemove( ( pxTCB-xStateListItem ) ); (12)//任务是否还在等待其他事件如信号量、队列等如果是的话就将这些//任务从相应的事件列表中移除。相当于等待事件超时退出if( listLIST_ITEM_CONTAINER( ( pxTCB-xEventListItem ) ) !\ (13) NULL ){( void ) uxListRemove( ( pxTCB-xEventListItem ) ); (14)}else{mtCOVERAGE_TEST_MARKER();}//将任务添加到就绪列表中prvAddTaskToReadyList( pxTCB ); (15)
#if ( configUSE_PREEMPTION 1 ){//使用抢占式内核判断解除阻塞的任务优先级是否高于当前正在//运行的任务优先级如果是的话就需要进行一次任务切换!if( pxTCB-uxPriority pxCurrentTCB-uxPriority ) (16){xSwitchRequired pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}
#endif /* configUSE_PREEMPTION */}}}//如果使能了时间片的话还需要处理同优先级下任务之间的调度
#if ( ( configUSE_PREEMPTION 1 ) ( configUSE_TIME_SLICING 1 ) )(17){if( listCURRENT_LIST_LENGTH( ( \pxReadyTasksLists[ pxCurrentTCB-uxPriority ] ) ) ( UBaseType_t ) 1 ){xSwitchRequired pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}
#endif//使用时钟节拍钩子函数
#if ( configUSE_TICK_HOOK 1 ){if( uxPendedTicks ( UBaseType_t ) 0U ){vApplicationTickHook(); (18)}else{mtCOVERAGE_TEST_MARKER();}}
#endif /* configUSE_TICK_HOOK */}else //任务调度器挂起(19){uxPendedTicks; (20)
#if ( configUSE_TICK_HOOK 1 ){vApplicationTickHook();}
#endif}
#if ( configUSE_PREEMPTION 1 ){if( xYieldPending ! pdFALSE ) (21){xSwitchRequired pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}
#endif /* configUSE_PREEMPTION */return xSwitchRequired; (22)
}(1)、判断任务调度器是否被挂起。 (2)、将时钟节拍计数器xTickCount 加一并将结果保存在xConstTickCount 中下一行程 序会将xConstTickCount 赋值给xTickCount相当于给xTickCount 加一。 (3)、xConstTickCount 为0说明发生了溢出 (4)、如果发生了溢出的话使用函数taskSWITCH_DELAYED_LISTS 将延时列表指针 pxDelayedTaskList 和溢出列表指针pxOverflowDelayedTaskList 所指向的列表进行交换函数 taskSWITCH_DELAYED_LISTS()本质上是个宏在文件tasks.c 中有定义将这两个指针所指向 的列表交换以后还需要更新xNextTaskUnblockTime 的值。 (5)、变量xNextTaskUnblockTime 保存着下一个要解除阻塞的任务的时间点值如果 xConstTickCount 大于xNextTaskUnblockTime 的话就说明有任务需要解除阻塞了。 (6)、判断延时列表是否为空。 (7)、如果延时列表为空的话就将xNextTaskUnblockTime 设置为portMAX_DELAY。 (8)、延时列表不为空获取延时列表第一个列表项对应的任务控制块。 (9)、获取(8)中获取到的任务控制块中的壮态列表项值。 (10)、任务控制块中的壮态列表项值保存了任务的唤醒时间点如果这个唤醒时间点值大于 当前的系统时钟(时钟节拍计数器值)说明任务的延时时间还未到。 (11)、任务延时时间还未到而且xItemValue 已经保存了下一个要唤醒的任务的唤醒时间 点所以需要用xItemValue 来更新xNextTaskUnblockTime。 (12)、任务延时时间到了所以将任务先从延时列表中移除。 (13)、检查任务是否还等待某个事件比如等待信号量、队列等。如果还在等待的话就任务 从相应的事件列表中移除。因为超时时间到了 (14)、将任务从相应的事件列表中移除。 (15)、任务延时时间到了并且任务已经从延时列表或者事件列表中已经移除。所以这里需 要将任务添加到就绪列表中。 (16)、延时时间到的任务优先级高于正在运行的任务优先级所以需要进行任务切换了标 记xSwitchRequired 为pdTRUE表示需要进行任务切换。 (17)、如果使能了时间片调度的话还要处理跟时间片调度有关的工作具体过程参考9.6 小节。 (18)、如果使能了时间片钩子函数的话就执行时间片钩子函数vApplicationTickHook()函 数的具体内容由用户自行编写。 (19)、如果调用函数vTaskSuspendAll()挂起了任务调度器的话在每个滴答定时器中断就不 不会更新xTickCount 了。取而代之的是用uxPendedTicks 来记录调度器挂起过程中的时钟节拍 数。这样在调用函数xTaskResumeAll()恢复任务调度器的时候就会调用uxPendedTicks 次函数 xTaskIncrementTick()这样xTickCount 就会恢复并且那些应该取消阻塞的任务都会取消阻塞。 函数xTaskResumeAll()中相应的处理代码如下
BaseType_t xTaskResumeAll( void )
{TCB_t *pxTCB NULL;BaseType_t xAlreadyYielded pdFALSE;configASSERT( uxSchedulerSuspended );taskENTER_CRITICAL();/************************************************************************//****************************省略部分代码********************************//************************************************************************/UBaseType_t uxPendedCounts uxPendedTicks;if( uxPendedCounts ( UBaseType_t ) 0U ){//do-while()循环体循环次数为uxPendedTicksdo{if( xTaskIncrementTick() ! pdFALSE ) //调用函数xTaskIncrementTick{xYieldPending pdTRUE; //标记需要进行任务调度。}else{mtCOVERAGE_TEST_MARKER();}--uxPendedCounts; //变量减一} while( uxPendedCounts ( UBaseType_t ) 0U );uxPendedTicks 0; //循环执行完毕uxPendedTicks 清零}else{mtCOVERAGE_TEST_MARKER();}/************************************************************************//****************************省略部分代码********************************//************************************************************************/taskEXIT_CRITICAL();return xAlreadyYielded;
}(20)、uxPendedTicks 是个全局变量在文件tasks.c 中有定义任务调度器挂起以后此变量 用来记录时钟节拍数。 (21)、有时候调用其他的API 函数会使用变量xYieldPending 来标记是否需要进行上下文切 换后面具体遇到具体分析。 (22)、返回xSwitchRequired 的值xSwitchRequired 保存了是否进行任务切换的信息如果 为pdTRUE 的话就需要进行任务切换pdFALSE 的话就不需要进行任务切换。函数 xPortSysTickHandler()中调用xTaskIncrementTick()的时候就会判断返回值并且根据返回值决定 是否进行任务切换。
第十三章FreeRTOS 队列
在实际的应用中常常会遇到一个任务或者中断服务需要和另外一个任务进行“沟通交流” 这个“沟通交流”的过程其实就是消息传递的过程。在没有操作系统的时候两个应用程序进行 消息传递一般使用全局变量的方式但是如果在使用操作系统的应用中用全局变量来传递消息 就会涉及到“资源管理”的问题。FreeRTOS 对此提供了一个叫做“队列”的机制来完成任务与 任务、任务与中断之间的消息传递。本章我们就来学习FreeRTOS 队列本章分为如下几部分 13.1 队列简介 13.2 队列结构体 13.3 队列创建 13.4 向队列发送消息 13.5 队列上锁和解锁 13.6 从队列读取消息 13.7 队列操作实验
13.1 队列简介
队列是为了任务与任务、任务与中断之间的通信而准备的可以在任务与任务、任务与中 断之间传递消息队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之 间要交流的数据保存在队列中叫做队列项目。队列所能保存的最大数据项目数量叫做队列的 长度创建队列的时候会指定数据项目的大小和队列的长度。由于队列用来传递消息的所以 也称为消息队列。FreeRTOS 中的信号量的也是依据队列实现的所以有必要深入的了解 FreeRTOS 的队列。 1、数据存储 通常队列采用先进先出(FIFO)的存储缓冲机制也就是往队列发送数据的时候(也叫入队)永 远都是发送到队列的尾部而从队列提取数据的时候(也叫出队)是从队列的头部提取的。但是 也可以使用LIFO 的存储缓冲也就是后进先出FreeRTOS 中的队列也提供了LIFO 的存储缓 冲机制。 数据发送到队列中会导致数据拷贝也就是将要发送的数据拷贝到队列中这就意味着在 队列中存储的是数据的原始值而不是原数据的引用(即只传递数据的指针)这个也叫做值传 递。学过UCOS 的同学应该知道UCOS 的消息队列采用的是引用传递传递的是消息指针。 采用引用传递的话消息内容就必须一直保持可见性也就是消息内容必须有效那么局部变量 这种可能会随时被删掉的东西就不能用来传递消息但是采用引用传递会节省时间啊因为不 用进行数据拷贝。 采用值传递的话虽然会导致数据拷贝会浪费一点时间但是一旦将消息发送到队列中原 始的数据缓冲区就可以删除掉或者覆写这样的话这些缓冲区就可以被重复的使用。FreeRTOS 中使用队列传递消息的话虽然使用的是数据拷贝但是也可以使用引用来传递消息啊我直接 往队列中发送指向这个消息的地址指针不就可以了这样当我要发送的消息数据太大的时候就 可以直接发送消息缓冲区的地址指针比如在网络应用环境中网络的数据量往往都很大的 采用数据拷贝的话就不现实。 1、多任务访问 队列不是属于某个特别指定的任务的任何任务都可以向队列中发送消息或者从队列中 提取消息。 2、出队阻塞 当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间这个阻塞时间就是当任 务从队列中读取消息无效的时候任务阻塞的时间。出队就是就从队列中读取消息出队阻塞是 针对从队列中读取消息的任务而言的。比如任务A 用于处理串口接收到的数据串口接收到数 据以后就会放到队列Q 中任务A 从队列Q 中读取数据。但是如果此时队列Q 是空的说明 还没有数据任务A 这时候来读取的话肯定是获取不到任何东西那该怎么办呢任务A 现在 有三种选择一二话不说扭头就走二要不我在等等吧等一会看看说不定一会就有数 据了三死等死也要等到你有数据选哪一个就是由这个阻塞时间决定的这个阻塞时间 单位是时钟节拍数。阻塞时间为0 的话就是不阻塞没有数据的话就马上返回任务继续执行接 下来的代码对应第一种选择。如果阻塞时间为0~ portMAX_DELAY当任务没有从队列中获 取到消息的话就进入阻塞态阻塞时间指定了任务进入阻塞态的时间当阻塞时间到了以后还 没有接收到数据的话就退出阻塞态返回任务接着运行下面的代码如果在阻塞时间内接收到 了数据就立即返回执行任务中下面的代码这种情况对应第二种选择。当阻塞时间设置为 portMAX_DELAY 的话任务就会一直进入阻塞态等待直到接收到数据为止这个就是第三种选择。 3、入队阻塞 入队说的是向队列中发送消息将消息加入到队列中。和出队阻塞一样当一个任务向队 列发送消息的话也可以设置阻塞时间。比如任务B 向消息队列Q 发送消息但是此时队列Q 是 满的那肯定是发送失败的。此时任务B 就会遇到和上面任务A 一样的问题这两种情况的处 理过程是类似的只不过一个是向队列Q 发送消息一个是从队列Q 读取消息而已。 4、队列操作过程图示 下面几幅图简单的演示了一下队列的入队和出队过程。 ●创建队列 图13.1.1 中任务A 要向任务B 发送消息这个消息是x 变量的值。首先创建一个队列并 且指定队列的长度和每条消息的长度。这里我们创建了一个长度为4 的队列因为要传递的是 x 值而x 是个int 类型的变量所以每条消息的长度就是int 类型的长度在STM32 中就是4 字节即每条消息是4 个字节的。 ●向队列发送第一个消息 图13.1.2 中任务A 的变量x 值为10将这个值发送到消息队列中。此时队列剩余长度就是 3 了。前面说了向队列中发送消息是采用拷贝的方式所以一旦消息发送完成变量x 就可以再 次被使用赋其他的值。 ●向队列发送第二个消息 图13.1.3 中任务A 又向队列发送了一个消息即新的x 的值这里是20。此时队列剩余长 度为2。 ●从队列中读取消息 图13.1.4 中任务B 从队列中读取消息并将读取到的消息值赋值给y这样y 就等于10 了。任务B 从队列中读取消息完成以后可以选择清除掉这个消息或者不清除。当选择清除这个 消息的话其他任务或中断就不能获取这个消息了而且队列剩余大小就会加一变成3。如果 不清除的话其他任务或中断也可以获取这个消息而队列剩余大小依旧是2。
13.2 队列结构体
有一个结构体用于描述队列叫做Queue_t这个结构体在文件queue.c 中定义如下
typedef struct QueueDefinition
{int8_t *pcHead; //指向队列存储区开始地址。int8_t *pcTail; //指向队列存储区最后一个字节。int8_t *pcWriteTo; //指向存储区中下一个空闲区域。union{int8_t *pcReadFrom; //当用作队列的时候指向最后一个出队的队列项首地址UBaseType_t uxRecursiveCallCount;//当用作递归互斥量的时候用来记录递归互斥量被//调用的次数。} u;List_t xTasksWaitingToSend; //等待发送任务列表那些因为队列满导致入队失败而进//入阻塞态的任务就会挂到此列表上。List_t xTasksWaitingToReceive; //等待接收任务列表那些因为队列空导致出队失败而进//入阻塞态的任务就会挂到此列表上。volatile UBaseType_t uxMessagesWaiting; //队列中当前队列项数量也就是消息数UBaseType_t uxLength; //创建队列时指定的队列长度也就是队列中最大允许的//队列项(消息)数量UBaseType_t uxItemSize; //创建队列时指定的每个队列项(消息)最大长度单位字节volatile int8_t cRxLock; //当队列上锁以后用来统计从队列中接收到的队列项数//量也就是出队的队列项数量当队列没有上锁的话此字//段为queueUNLOCKEDvolatile int8_t cTxLock; //当队列上锁以后用来统计发送到队列中的队列项数量//也就是入队的队列项数量当队列没有上锁的话此字//段为queueUNLOCKED
#if( ( configSUPPORT_STATIC_ALLOCATION 1 ) \( configSUPPORT_DYNAMIC_ALLOCATION 1 ) )uint8_t ucStaticallyAllocated; //如果使用静态存储的话此字段设置为pdTURE。
#endif
#if ( configUSE_QUEUE_SETS 1 ) //队列集相关宏struct QueueDefinition *pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY 1 ) //跟踪调试相关宏UBaseType_t uxQueueNumber;uint8_t ucQueueType;
#endif
} xQUEUE;
typedef xQUEUE Queue_t;老版本的FreeRTOS 中队列可能会使用xQUEUE 这个名字新版本FreeRTOS 中队列的名 字都使用Queue_t。
13.3 队列创建
13.3.1 函数原型
在使用队列之前必须先创建队列有两种创建队列的方法一种是静态的使用函数 xQueueCreateStatic()另一个是动态的使用函数xQueueCreate()。这两个函数本质上都是宏 真正完成队列创建的函数是xQueueGenericCreate()和xQueueGenericCreateStatic()这两个函数 在文件queue.c 中有定义这四个函数的原型如下。 1、函数xQueueCreate() 此函数本质上是一个宏用来动态创建队列此宏最终调用的是函数xQueueGenericCreate() 函数原型如下
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,UBaseType_t uxItemSize)参数 uxQueueLength要创建的队列的队列长度这里是队列的项目数。 uxItemSize队列中每个项目(消息)的长度单位为字节 返回值 其他值队列创捷成功以后返回的队列句柄 NULL: 队列创建失败。 2、函数xQueueCreateStatic() 此函数也是用于创建队列的但是使用的静态方法创建队列队列所需要的内存由用户自 行分配此函数本质上也是一个宏此宏最终调用的是函数xQueueGenericCreateStatic()函数 原型如下
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,UBaseType_t uxItemSize,uint8_t * pucQueueStorageBuffer,StaticQueue_t * pxQueueBuffer)参数 uxQueueLength要创建的队列的队列长度这里是队列的项目数。 uxItemSize队列中每个项目(消息)的长度单位为字节 pucQueueStorage: 指向队列项目的存储区也就是消息的存储区这个存储区需要用户自 行分配。此参数必须指向一个uint8_t 类型的数组。这个存储区要大于等 于(uxQueueLength * uxItemsSize)字节。 pxQueueBuffer: 此参数指向一个StaticQueue_t 类型的变量用来保存队列结构体。 返回值 其他值队列创捷成功以后的队列句柄 NULL: 队列创建失败。 3、函数xQueueGenericCreate() 函数xQueueGenericCreate()用于动态创建队列创建队列过程中需要的内存均通过 FreeRTOS 中的动态内存管理函数pvPortMalloc()分配函数原型如下
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,const uint8_t ucQueueType )参数 uxQueueLength要创建的队列的队列长度这里是队列的项目数。 uxItemSize队列中每个项目(消息)的长度单位为字节。 ucQueueType队列类型由于FreeRTOS 中的信号量等也是通过队列来实现的创建信号 量的函数最终也是使用此函数的因此在创建的时候需要指定此队列的用途 也就是队列类型一共有六种类型 queueQUEUE_TYPE_BASE 普通的消息队列 queueQUEUE_TYPE_SET 队列集 queueQUEUE_TYPE_MUTEX 互斥信号量 queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量 queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量 queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量 函数xQueueCreate() 创建队列的时候此参数默认选择的就是 queueQUEUE_TYPE_BASE。 返回值 其他值队列创捷成功以后的队列句柄 NULL: 队列创建失败。 4、函数xQueueGenericCreateStatic() 此函数用于动态创建队列创建队列过程中需要的内存需要由用户自行分配好函数原型 如下
QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,uint8_t * pucQueueStorage,StaticQueue_t * pxStaticQueue,const uint8_t ucQueueType )参数 uxQueueLength要创建的队列的队列长度这里是队列的项目数。 uxItemSize队列中每个项目(消息)的长度单位为字节 pucQueueStorage: 指向队列项目的存储区也就是消息的存储区这个存储区需要用户自 行分配。此参数必须指向一个uint8_t 类型的数组。这个存储区要大于等 于(uxQueueLength * uxItemsSize)字节。 pxStaticQueue: 此参数指向一个StaticQueue_t 类型的变量用来保存队列结构体。 ucQueueType队列类型。 返回值 其他值队列创捷成功以后队列句柄 NULL: 队列创建失败。
13.3.2 队列创建函数详解
最终完成队列创建的函数有两个一个是静态方法的xQueueGenericCreateStatic()另外一 个就是动态方法的xQueueGenericCreate() 。我们来详细的分析一下动态创建函数 xQueueGenericCreate()静态方法大同小异大家可以自行分析一下。函数xQueueGenericCreate() 在文件queue.c 中有如下定义
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,const uint8_t ucQueueType )
{Queue_t *pxNewQueue;size_t xQueueSizeInBytes;uint8_t *pucQueueStorage;configASSERT( uxQueueLength ( UBaseType_t ) 0 );if( uxItemSize ( UBaseType_t ) 0 ){//队列项大小为0那么就不需要存储区。xQueueSizeInBytes ( size_t ) 0;}else{//分配足够的存储区确保随时随地都可以保存所有的项目(消息)xQueueSizeInBytes ( size_t ) ( uxQueueLength * uxItemSize ); (1)}pxNewQueue ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) xQueueSizeInBytes ); (2)//内存申请成功if( pxNewQueue ! NULL ){pucQueueStorage ( ( uint8_t * ) pxNewQueue ) sizeof( Queue_t ); (3)
#if( configSUPPORT_STATIC_ALLOCATION 1 ){//队列是使用动态方法创建的所以队列字段ucStaticallyAllocated 标//记为pdFALSE。pxNewQueue-ucStaticallyAllocated pdFALSE;}
#endifprvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, \ (4)ucQueueType, pxNewQueue );}return pxNewQueue;
}(1)、队列是要存储消息的所以必须要有消息的存储区函数的参数uxQueueLength 和 uxItemSize 指定了队列中最大队列项目(消息)数量和每个消息的长度两者相乘就是消息存储 区的大小。 (2)、调用函数pvPortMalloc()给队列分配内存注意这里申请的内存大小是队列结构体和队 列中消息存储区的总大小。 (3)、计算出消息存储区的首地址(2)中申请到的内存是队列结构体和队列中消存储区的总 大小队列结构体内存在前紧跟在后面的就是消息存储区内存。 (4)、调用函数prvInitialiseNewQueue()初始化队列。 可以看出函数xQueueGenericCreate()重要的工作就是给队列分配内存当内存分配成功以 后调用函数prvInitialiseNewQueue()来初始化队列。
13.3.3 队列初始化函数
队列初始化函数prvInitialiseNewQueue()用于队列的初始化此函数在文件queue.c 中有定 义函数代码如下
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, //队列长度const UBaseType_t uxItemSize, //队列项目长度uint8_t * pucQueueStorage, //队列项目存储区const uint8_t ucQueueType, //队列类型Queue_t * pxNewQueue ) //队列结构体
{//防止编译器报错( void ) ucQueueType;if( uxItemSize ( UBaseType_t ) 0 ){//队列项(消息)长度为0说明没有队列存储区这里将pcHead 指向队列开始地址pxNewQueue-pcHead ( int8_t * ) pxNewQueue;}else{//设置pcHead 指向队列项存储区首地址pxNewQueue-pcHead ( int8_t * ) pucQueueStorage; (1)}//初始化队列结构体相关成员变量pxNewQueue-uxLength uxQueueLength; (2)pxNewQueue-uxItemSize uxItemSize;( void ) xQueueGenericReset( pxNewQueue, pdTRUE ); (3)
#if ( configUSE_TRACE_FACILITY 1 ) //跟踪调试相关字段初始化{pxNewQueue-ucQueueType ucQueueType;}
#endif /* configUSE_TRACE_FACILITY */
#if( configUSE_QUEUE_SETS 1 ) //队列集相关字段初始化{pxNewQueue-pxQueueSetContainer NULL;}
#endif /* configUSE_QUEUE_SETS */traceQUEUE_CREATE( pxNewQueue );
}(1)、队列结构体中的成员变量pcHead 指向队列存储区中首地址。 (2)、初始化队列结构体中的成员变量uxQueueLength 和uxItemSize这两个成员变量保存 队列的最大队列项目和每个队列项大小。 (3)、调用函数xQueueGenericReset()复位队列。PS:发一句牢骚绕来绕去的函数调了一 个又一个的。
13.3.4 队列复位函数
队列初始化函数prvInitialiseNewQueue()中调用了函数xQueueGenericReset()来复位队列 函数xQueueGenericReset()代码如下
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{Queue_t * const pxQueue ( Queue_t * ) xQueue;configASSERT( pxQueue );taskENTER_CRITICAL();{//初始化队列相关成员变量pxQueue-pcTail pxQueue-pcHead ( pxQueue-uxLength * pxQueue-\ (1)uxItemSize );pxQueue-uxMessagesWaiting ( UBaseType_t ) 0U;pxQueue-pcWriteTo pxQueue-pcHead;pxQueue-u.pcReadFrom pxQueue-pcHead ( ( pxQueue-uxLength - \( UBaseType_t ) 1U ) * pxQueue-uxItemSize );pxQueue-cRxLock queueUNLOCKED;pxQueue-cTxLock queueUNLOCKED;if( xNewQueue pdFALSE ) (2){//由于复位队列以后队列依旧是空的所以对于那些由于出队(从队列中读取消//息)而阻塞的任务就依旧保持阻塞壮态。但是对于那些由于入队(向队列中发送//消息)而阻塞的任务就不同了这些任务要解除阻塞壮态从队列的相应列表中//移除。if( listLIST_IS_EMPTY( ( pxQueue-xTasksWaitingToSend ) ) pdFALSE ){if( xTaskRemoveFromEventList( ( pxQueue-\xTasksWaitingToSend ) ) ! pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}else{//初始化队列中的列表vListInitialise( ( pxQueue-xTasksWaitingToSend ) ); (3)vListInitialise( ( pxQueue-xTasksWaitingToReceive ) );}}taskEXIT_CRITICAL();return pdPASS;
}(1)、初始化队列中的相关成员变量。 (2)、根据参数xNewQueue 确定要复位的队列是否是新创建的队列如果不是的话还需要 做其他的处理 (3)、初始化队列中的列表xTasksWaitingToSend 和xTasksWaitingToReceive。 至此队列创建成功比如我们创建一个有4 个队列项每个队列项长度为32 个字节的队 列TestQueue创建成功的队列如图13.3.4.1 所示
13.4 向队列发送消息
13.4.1 函数原型
创建好队列以后就可以向队列发送消息了FreeRTOS 提供了8 个向队列发送消息的API 函数如表13.4.1 所示 1、函数xQueueSend()、xQueueSendToBack()和xQueueSendToFront() 这三个函数都是用于向队列中发送消息的这三个函数本质都是宏其中函数xQueueSend() 和xQueueSendToBack()是一样的都是后向入队即将新的消息插入到队列的后面。函数 xQueueSendToToFront()是前向入队即将新消息插入到队列的前面。然而这三个函数最后都 是调用的同一个函数xQueueGenericSend()。这三个函数只能用于任务函数中不能用于中断 服务函数中断服务函数有专用的函数它们以“FromISR”结尾这三个函数的原型如下
BaseType_t xQueueSend( QueueHandle_t xQueue,const void * pvItemToQueue,TickType_t xTicksToWait);BaseType_t xQueueSendToBack(QueueHandle_t xQueue,const void* pvItemToQueue,TickType_t xTicksToWait);BaseType_t xQueueSendToToFront(QueueHandle_t xQueue,const void *pvItemToQueue,TickType_t xTicksToWait);参数 xQueue队列句柄指明要向哪个队列发送数据创建队列成功以后会返回此队列的 队列句柄。 pvItemToQueue指向要发送的消息发送时候会将这个消息拷贝到队列中。 xTicksToWait阻塞时间此参数指示当队列满的时候任务进入阻塞态等待队列空闲的最大 时间。如果为0 的话当队列满的时候就立即返回当为portMAX_DELAY 的 话就会一直等待直到队列有空闲的队列项也就是死等但是宏 INCLUDE_vTaskSuspend 必须为1。
返回值 pdPASS向队列发送消息成功 errQUEUE_FULL: 队列已经满了消息发送失败。 2、函数xQueueOverwrite() 此函数也是用于向队列发送数据的当队列满了以后会覆写掉旧的数据不管这个旧数据 有没有被其他任务或中断取走。这个函数常用于向那些长度为1 的队列发送消息此函数也是 一个宏最终调用的也是函数xQueueGenericSend()函数原型如下
BaseType_t xQueueOverwrite(QueueHandle_t xQueue,const void * pvItemToQueue);参数 xQueue队列句柄指明要向哪个队列发送数据创建队列成功以后会返回此队列的 队列句柄。 pvItemToQueue指向要发送的消息发送的时候会将这个消息拷贝到队列中。 返回值 pdPASS向队列发送消息成功此函数也只会返回pdPASS因为此函数执行过程中不 在乎队列满不满满了的话我就覆写掉旧的数据总之肯定能成功。 3、函数xQueueGenericSend() 此函数才是真正干活的上面讲的所有的任务级入队函数最终都是调用的此函数此函数 也是我们后面重点要讲解的先来看一下函数原型
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,const void * const pvItemToQueue,TickType_t xTicksToWait,const BaseType_t xCopyPosition )参数 xQueue队列句柄指明要向哪个队列发送数据创建队列成功以后会返回此队列的 队列句柄。 pvItemToQueue指向要发送的消息发送的过程中会将这个消息拷贝到队列中。 xTicksToWait阻塞时间。 xCopyPosition: 入队方式有三种入队方式 queueSEND_TO_BACK后向入队 queueSEND_TO_FRONT前向入队 queueOVERWRITE覆写入队。 上面讲解的入队API 函数就是通过此参数来决定采用哪种入队方式的。 返回值 pdTRUE向队列发送消息成功 errQUEUE_FULL: 队列已经满了消息发送失败。
4、函数xQueueSendFromISR()、 xQueueSendToBackFromISR()、 xQueueSendToFrontFromISR() 这三个函数也是向队列中发送消息的这三个函数用于中断服务函数中。这三个函数本质 也宏其中函数xQueueSendFromISR ()和xQueueSendToBackFromISR ()是一样的都是后向入 队即将新的消息插入到队列的后面。函数xQueueSendToFrontFromISR ()是前向入队即将新 消息插入到队列的前面。这三个函数同样调用同一个函数xQueueGenericSendFromISR ()。这三 个函数的原型如下
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,const void * pvItemToQueue,BaseType_t * pxHigherPriorityTaskWoken);BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,const void * pvItemToQueue,BaseType_t * pxHigherPriorityTaskWoken);BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,const void * pvItemToQueue,BaseType_t * pxHigherPriorityTaskWoken);
参数 xQueue队列句柄指明要向哪个队列发送数据创建队列成功以后会返回此队列的 队列句柄。 pvItemToQueue指向要发送的消息发送的时候会将这个消息拷贝到队列中。 pxHigherPriorityTaskWoken标记退出此函数以后是否进行任务切换这个变量的值由这 三个函数来设置的用户不用进行设置用户只需要提供一 个变量来保存这个值就行了。当此值为pdTRUE 的时候在退 出中断服务函数之前一定要进行一次任务切换。 返回值 pdTRUE向队列中发送消息成功 errQUEUE_FULL: 队列已经满了消息发送失败。 我们注意观察可以看出这些函数都没有设置阻塞时间值。原因很简单这些函数都是在 中断服务函数中调用的并不是在任务中所以也就没有阻塞这一说了 5、函数xQueueOverwriteFromISR() 此函数是xQueueOverwrite()的中断级版本用在中断服务函数中在队列满的时候自动覆 写掉旧的数据此函数也是一个宏实际调用的也是函数xQueueGenericSendFromISR()此函 数原型如下
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,const void * pvItemToQueue,BaseType_t * pxHigherPriorityTaskWoken);
此函数的参数和返回值同上面三个函数相同。 6、函数xQueueGenericSendFromISR() 上面说了4 个中断级入队函数最终都是调用的函数xQueueGenericSendFromISR()这是真 正干活的主啊也是我们下面会详细讲解的函数先来看一下这个函数的原型如下
BaseType_t xQueueGenericSendFromISR(QueueHandle_t xQueue,const void* pvItemToQueue,BaseType_t* pxHigherPriorityTaskWoken,BaseType_t xCopyPosition);参数 xQueue队列句柄指明要向哪个队列发送数据创建队列成功以后会返回此队列的 队列句柄。 pvItemToQueue指向要发送的消息发送的过程中会将这个消息拷贝到队列中。 pxHigherPriorityTaskWoken标记退出此函数以后是否进行任务切换这个变量的值由这 三个函数来设置的用户不用进行设置用户只需要提供一 个变量来保存这个值就行了。当此值为pdTRUE 的时候在退 出中断服务函数之前一定要进行一次任务切换。 xCopyPosition: 入队方式有三种入队方式 queueSEND_TO_BACK后向入队 queueSEND_TO_FRONT前向入队 queueOVERWRITE覆写入队。 返回值 pdTRUE向队列发送消息成功 errQUEUE_FULL: 队列已经满了消息发送失败。
13.4.2 任务级通用入队函数
不管是后向入队、前向入队还是覆写入队最终调用的都是通用入队函数 xQueueGenericSend()这个函数在文件queue.c 文件中由定义缩减后的函数代码如下
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,const void * const pvItemToQueue,TickType_t xTicksToWait,const BaseType_t xCopyPosition )
{BaseType_t xEntryTimeSet pdFALSE, xYieldRequired;TimeOut_t xTimeOut;Queue_t * const pxQueue ( Queue_t * ) xQueue;for( ;; ){taskENTER_CRITICAL(); //进入临界区{//查询队列现在是否还有剩余存储空间如果采用覆写方式入队的话那就不用在//乎队列是不是满的啦。if( ( pxQueue-uxMessagesWaiting pxQueue-uxLength ) ||\ (1)( xCopyPosition queueOVERWRITE ) ){traceQUEUE_SEND( pxQueue );xYieldRequired prvCopyDataToQueue( pxQueue, pvItemToQueue,\ (2)xCopyPosition );/**************************************************************************//**************************省略掉与队列集相关代码**************************//**************************************************************************/{//检查是否有任务由于等待消息而进入阻塞态if( listLIST_IS_EMPTY( ( pxQueue-xTasksWaitingToReceive ) ) \(3)pdFALSE ){if( xTaskRemoveFromEventList( ( pxQueue-\ (4)xTasksWaitingToReceive ) ) ! pdFALSE ){//解除阻塞态的任务优先级最高因此要进行一次任务切换queueYIELD_IF_USING_PREEMPTION(); (5)}else{mtCOVERAGE_TEST_MARKER();}}else if( xYieldRequired ! pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}taskEXIT_CRITICAL();return pdPASS; (6)}else{if( xTicksToWait ( TickType_t ) 0 ) (7){//队列是满的并且没有设置阻塞时间的话就直接返回taskEXIT_CRITICAL();traceQUEUE_SEND_FAILED( pxQueue );return errQUEUE_FULL; (8)}else if( xEntryTimeSet pdFALSE ) (9){//队列是满的并且指定了任务阻塞时间的话就初始化时间结构体vTaskSetTimeOutState( xTimeOut );xEntryTimeSet pdTRUE;}else{//时间结构体已经初始化过了mtCOVERAGE_TEST_MARKER();}}}taskEXIT_CRITICAL(); //退出临界区vTaskSuspendAll(); (10)prvLockQueue( pxQueue ); (11)//更新时间壮态检查是否有超时产生if( xTaskCheckForTimeOut( xTimeOut, xTicksToWait ) pdFALSE ) (12){if( prvIsQueueFull( pxQueue ) ! pdFALSE ) (13){traceBLOCKING_ON_QUEUE_SEND( pxQueue );vTaskPlaceOnEventList( ( pxQueue-xTasksWaitingToSend ), \ (14)xTicksToWait );prvUnlockQueue( pxQueue ); (15)if( xTaskResumeAll() pdFALSE ) (16){portYIELD_WITHIN_API();}}else{//重试一次prvUnlockQueue( pxQueue ); (17)( void ) xTaskResumeAll();}}else{//超时产生prvUnlockQueue( pxQueue ); (18)( void ) xTaskResumeAll();traceQUEUE_SEND_FAILED( pxQueue );return errQUEUE_FULL; (19)}}
}(1)、要向队列发送数据肯定要先检查一下队列是不是满的如果是满的话肯定不能发送 的。当队列未满或者是覆写入队的话就可以将消息入队了。 (2)、调用函数prvCopyDataToQueue()将消息拷贝到队列中。前面说了入队分为后向入队、 前向入队和覆写入队他们的具体实现就是在函数prvCopyDataToQueue()中完成的。如果选择 后向入队queueSEND_TO_BACK 的话就将消息拷贝到队列结构体成员pcWriteTo 所指向的队 列项拷贝成功以后pcWriteTo 增加uxItemSize 个字节指向下一个队列项目。当选择前向入 队queueSEND_TO_FRONT 或者queueOVERWRITE 的话就将消息拷贝到u.pcReadFrom 所指向 的队列项目同样的需要调整u.pcReadFrom 的位置。当向队列写入一个消息以后队列中统计当 前消息数量的成员uxMessagesWaiting 就会加一但是选择覆写入队queueOVERWRITE 的话还 会将uxMessagesWaiting 减一这样一减一加相当于队列当前消息数量没有变。 (3) 、检查是否有任务由于请求队列消息而阻塞阻塞的任务会挂在队列的 xTasksWaitingToReceive 列表上。 (4)、有任务由于请求消息而阻塞因为在(2)中已将向队列中发送了一条消息了所以调用 函数xTaskRemoveFromEventList()将阻塞的任务从列表xTasksWaitingToReceive 上移除并且把 这个任务添加到就绪列表中如果调度器上锁的话这些任务就会挂到列表xPendingReadyList 上。 如果取消阻塞的任务优先级比当前正在运行的任务优先级高还要标记需要进行任务切换。当函 数xTaskRemoveFromEventList()返回值为pdTRUE 的话就需要进行任务切换。 (5)、进行任务切换。 (6)、返回pdPASS标记入队成功。 (7)、(2)到(6)都是非常理想的效果即消息队列未满入队没有任何障碍。但是队列满了以 后呢首先判断设置的阻塞时间是否为0如果为0 的话就说明没有阻塞时间。 (8)、由(7)得知阻塞时间为0那就直接返回errQUEUE_FULL标记队列已满就可以了。 (9)、如果阻塞时间不为0 并且时间结构体还没有初始化的话就初始化一次超时结构体变量 调用函数vTaskSetTimeOutState()完成超时结构体变量xTimeOut 的初始化。其实就是记录当前 的系统时钟节拍计数器的值xTickCount 和溢出次数xNumOfOverflows。 (10)、任务调度器上锁代码执行到这里说明当前的状况是队列已满了而且设置了不为0 的阻塞时间。那么接下来就要对任务采取相应的措施了比如将任务加入到队列的 xTasksWaitingToSend 列表中。 (11)、调用函数prvLockQueue()给队列上锁其实就是将队列中的成员变量cRxLock 和 cTxLock 设置为queueLOCKED_UNMODIFIED。 (12)、调用函数xTaskCheckForTimeOut()更新超时结构体变量xTimeOut并且检查阻塞时 间是否到了。 (13)、阻塞时间还没到那就检查队列是否还是满的。 (14) 、经过(12) 和(13) 得出阻塞时间没到而且队列依旧是满的那就调用函数 vTaskPlaceOnEventList()将任务添加到队列的xTasksWaitingToSend 列表中和延时列表中并且 将任务从就绪列表中移除。注意如果阻塞时间是portMAX_DELAY 并且宏 INCLUDE_vTaskSuspend 为1 的话函数vTaskPlaceOnEventList() 会将任务添加到列表 xSuspendedTaskList 上。 (15)、操作完成调用函数prvUnlockQueue()解锁队列。 (16)、调用函数xTaskResumeAll()恢复任务调度器 (17)、阻塞时间还没到但是队列现在有空闲的队列项那么就在重试一次。 (18)、相比于第(12)步阻塞时间到了那么任务就不用添加到那些列表中了那就解锁队 列恢复任务调度器。 (19)、返回errQUEUE_FULL表示队列满了。
13.4.3 中断级通用入队函数
讲完任务级入队函数再来看一下中断级入队函数xQueueGenericSendFromISR()其他的中 断级入队函数都是靠此函数来实现的。中断级入队函数和任务级入队函数大同小异函数代码 如下
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue,const void * const pvItemToQueue,BaseType_t * const pxHigherPriorityTaskWoken,const BaseType_t xCopyPosition )
{BaseType_t xReturn;UBaseType_t uxSavedInterruptStatus;Queue_t * const pxQueue ( Queue_t * ) xQueue;portASSERT_IF_INTERRUPT_PRIORITY_INVALID();uxSavedInterruptStatus portSET_INTERRUPT_MASK_FROM_ISR();{if( ( pxQueue-uxMessagesWaiting pxQueue-uxLength ) ||\ (1)( xCopyPosition queueOVERWRITE ) ){const int8_t cTxLock pxQueue-cTxLock; (2)traceQUEUE_SEND_FROM_ISR( pxQueue );( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition ); (3)//队列上锁的时候就不能操作事件列表队列解锁的时候会补上这些操作的。if( cTxLock queueUNLOCKED ) (4){/**************************************************************************//**************************省略掉与队列集相关代码**************************//**************************************************************************/if( listLIST_IS_EMPTY( ( pxQueue-xTasksWaitingToReceive ) ) \ (5)pdFALSE ){if( xTaskRemoveFromEventList( ( pxQueue-\ (6)xTasksWaitingToReceive ) ) ! pdFALSE ){//刚刚从事件列表中移除的任务对应的任务优先级更高所以标记要进行任务切换if( pxHigherPriorityTaskWoken ! NULL ){*pxHigherPriorityTaskWoken pdTRUE; (7)}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}}else{//cTxLock 加一这样就知道在队列上锁期间向队列中发送了数据pxQueue-cTxLock ( int8_t ) ( cTxLock 1 ); (8)}xReturn pdPASS; (9)}else{traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );xReturn errQUEUE_FULL; (10)}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
return xReturn;
}(1)、队列未满或者采用的覆写的入队方式这是最理想的壮态。 (2)、读取队列的成员变量xTxLock用于判断队列是否上锁。 (3)、将数据拷贝到队列中。 (4)、队列上锁了比如任务级入队函数在操作队列中的列表的时候就会对队列上锁。 (5)、判断队列列表xTasksWaitingToReceive 是否为空如果不为空的话说明有任务在请求 消息的时候被阻塞了。 (6)、将相应的任务从列表xTasksWaitingToReceive 上移除。跟任务级入队函数处理过程一 样。 (7)、如果刚刚从列表xTasksWaitingToReceive 中移除的任务优先级比当前任务的优先级高 那么标记pxHigherPriorityTaskWoken 为pdTRUE表示要进行任务切换。如果要进行任务切换 的话就需要在退出此函数以后退出中断服务函数之前进行一次任务切换。 (8)、如果队列上锁的话那就将队列成员变量cTxLock 加一表示进行了一次入队操作在 队列解锁(prvUnlockQueue())的时候会对其做相应的处理。 (9)、返回pdPASS表示入队完成。 (10)、如果队列满的话就直接返回errQUEUE_FULL表示队列满。
13.5 队列上锁和解锁
在上面讲解任务级通用入队函数和中断级通用入队函数的时候都提到了队列的上锁和解锁 队列的上锁和解锁是两个API 函数prvLockQueue()和prvUnlockQueue()。首先来看一下队列上 锁函数prvLockQueue()此函数本质上就是一个宏定义如下
#define prvLockQueue( pxQueue ) \taskENTER_CRITICAL(); \
{ \if( ( pxQueue )-cRxLock queueUNLOCKED ) \{ \( pxQueue )-cRxLock queueLOCKED_UNMODIFIED; \} \if( ( pxQueue )-cTxLock queueUNLOCKED ) \{ \( pxQueue )-cTxLock queueLOCKED_UNMODIFIED; \} \
} \
taskEXIT_CRITICAL()prvLockQueue()函数很简单就是将队列中的成员变量cRxLock 和cTxLock 设置为 queueLOCKED_UNMODIFIED 就行了。 在来看一下队列的解锁函数prvUnlockQueue()函数如下
static void prvUnlockQueue( Queue_t * const pxQueue )
{//上锁计数器(cTxLock 和cRxLock)记录了在队列上锁期间入队或出队的数量当队列//上锁以后队列项是可以加入或者移除队列的但是相应的列表不会更新。taskENTER_CRITICAL();{//处理cTxLock。int8_t cTxLock pxQueue-cTxLock;while( cTxLock queueLOCKED_UNMODIFIED ) (1){/**************************************************************************//**************************省略掉与队列集相关代码**************************//**************************************************************************/{//将任务从事件列表中移除if( listLIST_IS_EMPTY( ( pxQueue-xTasksWaitingToReceive ) ) \ (2)pdFALSE ){if( xTaskRemoveFromEventList( ( pxQueue-\ (3)xTasksWaitingToReceive ) ) ! pdFALSE ){//从列表中移除的任务优先级比当前任务的优先级高因此要//进行任务切换。vTaskMissedYield(); (4)}else{mtCOVERAGE_TEST_MARKER();}}else{break;}}--cTxLock; (5)}pxQueue-cTxLock queueUNLOCKED; (6)}taskEXIT_CRITICAL();//处理cRxLock。taskENTER_CRITICAL();{int8_t cRxLock pxQueue-cRxLock;while( cRxLock queueLOCKED_UNMODIFIED ) (7){if( listLIST_IS_EMPTY( ( pxQueue-xTasksWaitingToSend ) ) pdFALSE ){if( xTaskRemoveFromEventList( ( pxQueue-xTasksWaitingToSend ) ) !\pdFALSE ){vTaskMissedYield();}else{mtCOVERAGE_TEST_MARKER();}--cRxLock;}else{break;}}pxQueue-cRxLock queueUNLOCKED;}taskEXIT_CRITICAL();
}(1)、判断是否有中断向队列发送了消息,在13.2.3 小节讲解中断级通用入队函数的时候说 了如果当队列上锁的话那么向队列发送消息成功以后会将入队计数器cTxLock 加一。 (2)、判断列表xTasksWaitingToReceive 是否为空如果不为空的话就要将相应的任务从列 表中移除。 (3)、将任务从列表xTasksWaitingToReceive 中移除。 (4)、如果刚刚从列表xTasksWaitingToReceive 中移除的任务优先级比当前任务的优先级高 那么就要标记需要进行任务切换。这里调用函数vTaskMissedYield()来完成此任务函数 vTaskMissedYield()只是简单的将全局变量xYieldPending 设置为pdTRUE。那么真正的任务切换 是在哪里完成的呢在时钟节拍处理函数xTaskIncrementTick()中此函数会判断xYieldPending 的值从而决定是否进行任务切换具体内容可以参考12.2 小节。 (5)、每处理完一条就将cTxLock 减一直到处理完所有的。 (6)、当处理完以后标记cTxLock 为queueUNLOCKED也就说cTxLock 是没有上锁的了。 (7)、处理完cTxLock 以后接下来就要处理xRxLock 了处理过程和xTxLock 很类似大 家自行分析一下。
13.6 从队列读取消息
有入队就有出队出队就是从队列中获取队列项(消息)FreeRTOS 中出队函数如表13.6.1.1所示 1、函数xQueueReceive() 此函数用于在任务中从队列中读取一条(请求)消息读取成功以后就会将队列中的这条数 据删除此函数的本质是一个宏真正执行的函数是xQueueGenericReceive()。此函数在读取消 息的时候是采用拷贝方式的所以用户需要提供一个数组或缓冲区来保存读取到的数据所读 取的数据长度是创建队列的时候所设定的每个队列项目的长度函数原型如下
BaseType_t xQueueReceive(QueueHandle_t xQueue,void * pvBuffer,TickType_t xTicksToWait);参数 xQueue队列句柄指明要读取哪个队列的数据创建队列成功以后会返回此队列的 队列句柄。 pvBuffer保存数据的缓冲区读取队列的过程中会将读取到的数据拷贝到这个缓冲区 中。 xTicksToWait阻塞时间此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最 大时间。如果为0 的话当队列空的时候就立即返回当为portMAX_DELAY 的话就会一直等待直到队列有数据也就是死等但是宏 INCLUDE_vTaskSuspend 必须为1。 返回值 pdTRUE从队列中读取数据成功。 pdFALSE: 从队列中读取数据失败。 2、函数xQueuePeek() 此函数用于从队列读取一条(请求)消息只能用在任务中此函数在读取成功以后不会将 消息删除此函数是一个宏真正执行的函数是xQueueGenericReceive()。此函数在读取消息的 时候是采用拷贝方式的所以用户需要提供一个数组或缓冲区来保存读取到的数据所读取的 数据长度是创建队列的时候所设定的每个队列项目的长度函数原型如下
BaseType_t xQueuePeek(QueueHandle_t xQueue,void * pvBuffer,TickType_t xTicksToWait);参数 xQueue队列句柄指明要读取哪个队列的数据创建队列成功以后会返回此队列的 队列句柄。 pvBuffer保存数据的缓冲区读取队列的过程中会将读取到的数据拷贝到这个缓冲区 中。 xTicksToWait阻塞时间此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最 大时间。如果为0 的话当队列空的时候就立即返回当为portMAX_DELAY 的话就会一直等待直到队列有数据也就是死等但是宏 INCLUDE_vTaskSuspend 必须为1。 返回值 pdTRUE从队列中读取数据成功。 pdFALSE: 从队列中读取数据失败。 3、函数xQueueGenericReceive() 不管是函数xQueueReceive() 还是xQueuePeek() 最终都是调用的函数 xQueueGenericReceive()此函数是真正干事的函数原型如下
BaseType_t xQueueGenericReceive(QueueHandle_t xQueue,void* pvBuffer,TickType_t xTicksToWaitBaseType_t xJustPeek)参数 xQueue队列句柄指明要读取哪个队列的数据创建队列成功以后会返回此队列的 队列句柄。 pvBuffer保存数据的缓冲区读取队列的过程中会将读取到的数据拷贝到这个缓冲区 中。 xTicksToWait阻塞时间此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最 大时间。如果为0 的话当队列空的时候就立即返回当为portMAX_DELAY 的话就会一直等待直到队列有数据也就是死等但是宏 INCLUDE_vTaskSuspend 必须为1。 xJustPeek标记当读取成功以后是否删除掉队列项当为pdTRUE 的时候就不用删除 也就是说你后面再调用函数xQueueReceive()获取到的队列项是一样的。当为 pdFALSE 的时候就会删除掉这个队列项。 返回值 pdTRUE从队列中读取数据成功。 pdFALSE: 从队列中读取数据失败。 4、函数xQueueReceiveFromISR() 此函数是xQueueReceive()的中断版本用于在中断服务函数中从队列中读取(请求)一条消 息读取成功以后就会将队列中的这条数据删除。此函数在读取消息的时候是采用拷贝方式的 所以需要用户提供一个数组或缓冲区来保存读取到的数据所读取的数据长度是创建队列的时 候所设定的每个队列项目的长度函数原型如下
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,void* pvBuffer,BaseType_t * pxTaskWoken);参数 xQueue队列句柄指明要读取哪个队列的数据创建队列成功以后会返回此队列的 队列句柄。 pvBuffer保存数据的缓冲区读取队列的过程中会将读取到的数据拷贝到这个缓冲区 中。 pxTaskWoken标记退出此函数以后是否进行任务切换这个变量的值是由函数来设置的 用户不用进行设置用户只需要提供一个变量来保存这个值就行了。当此值 为pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。 返回值 pdTRUE从队列中读取数据成功。 pdFALSE: 从队列中读取数据失败。
5、函数xQueuePeekFromISR() 此函数是xQueuePeek()的中断版本此函数在读取成功以后不会将消息删除此函数原型 如下
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,void * pvBuffer)参数 xQueue队列句柄指明要读取哪个队列的数据创建队列成功以后会返回此队列的 队列句柄。 pvBuffer保存数据的缓冲区读取队列的过程中会将读取到的数据拷贝到这个缓冲区 中。 返回值 pdTRUE从队列中读取数据成功。 pdFALSE: 从队列中读取数据失败。 出队函数的具体过程和入队函数类似具体的过程就不在详细的分析了有兴趣的大家 自行对照着源码看一下就可以了。
13.7 队列操作实验
13.7.1 实验程序设计
1、实验目的 学习使用FreeRTOS 的队列相关API 函数学会如何在任务或中断中向队列发送消息或者 从队列中接收消息。 2、实验设计 本实验设计三个任务start_task、task1_task 、Keyprocess_task 这三个任务的任务功能如下 start_task用来创建其他2 个任务。 task1_task 读取按键的键值然后将键值发送到队列Key_Queue 中并且检查队列的剩 余容量等信息。 Keyprocess_task 按键处理任务读取队列Key_Queue 中的消息根据不同的消息值做相 应的处理。 实验需要三个按键KEY_UP、KEY2 和KEY0不同的按键对应不同的按键值任务 task1_task 会将这些值发送到队列Key_Queue 中。 实验中创建了两个队列Key_Queue 和Message_Queue队列Key_Queue 用于传递按键值 队列Message_Queue 用于传递串口发送过来的消息。 实验还需要两个中断一个是串口1 接收中断一个是定时器2 中断他们的作用如下 串口1 接收中断接收串口发送过来的数据并将接收到的数据发送到队列Message_Queue 中。 定时器2 中断定时周期设置为500ms在定时中断中读取队列Message_Queue 中的消息并 将其显示在LCD 上。 3、实验工程 FreeRTOS 实验13-1 FreeRTOS 队列操作实验。 4、实验程序与分析 ●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 256 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数#define KEYPROCESS_TASK_PRIO 3 //任务优先级
#define KEYPROCESS_STK_SIZE 256 //任务堆栈大小
TaskHandle_t Keyprocess_Handler; //任务句柄
void Keyprocess_task(void *pvParameters); //任务函数
//按键消息队列的数量
#define KEYMSG_Q_NUM 1 //按键消息队列的数量(1)
#define MESSAGE_Q_NUM 4 //发送数据的消息队列的数量(2)
QueueHandle_t Key_Queue; //按键值消息队列句柄
QueueHandle_t Message_Queue; //信息队列句柄(1)、队列Key_Queue 用来传递按键值的也就是一个u8 变量所以队列长度为1 就行了。 并且消息长度为1 个字节。 (2)、队列Message_Queue 用来传递串口接收到的数据队列长度设置为4每个消息的长 度为USART_REC_LEN(在usart.h 中有定义)。 ●其他应用函数 在main.c 中还有一些其他的函数如下
//用于在LCD 上显示接收到的队列的消息
//str: 要显示的字符串(接收到的消息)
void disp_str(u8* str)
{LCD_Fill(5,230,110,245,WHITE); //先清除显示区域LCD_ShowString(5,230,100,16,16,str);
}
//加载主界面
void freertos_load_main_ui(void)
{POINT_COLOR RED;LCD_ShowString(10,10,200,16,16,ATK STM32F103/407);LCD_ShowString(10,30,200,16,16,FreeRTOS Examp 13-1);LCD_ShowString(10,50,200,16,16,Message Queue);LCD_ShowString(10,70,220,16,16,KEY_UP:LED1 KEY0:Refresh LCD);LCD_ShowString(10,90,200,16,16,KEY1:SendMsg KEY2:BEEP);POINT_COLOR BLACK;LCD_DrawLine(0,107,239,107); //画线LCD_DrawLine(119,107,119,319); //画线LCD_DrawRectangle(125,110,234,314); //画矩形POINT_COLOR RED;LCD_ShowString(0,130,120,16,16,DATA_Msg Size:);LCD_ShowString(0,170,120,16,16,DATA_Msg rema:);LCD_ShowString(0,210,100,16,16,DATA_Msg:);POINT_COLOR BLUE;
}
//查询Message_Queue 队列中的总队列数量和剩余队列数量
void check_msg_queue(void)
{u8 *p;u8 msgq_remain_size; //消息队列剩余大小u8 msgq_total_size; //消息队列总大小taskENTER_CRITICAL(); //进入临界区msgq_remain_sizeuxQueueSpacesAvailable(Message_Queue);//得到队列剩余大小(1)msgq_total_sizeuxQueueMessagesWaiting(Message_Queue)\ (2)uxQueueSpacesAvailable(Message_Queue);//得到队列总大小总大小使用剩余的。pmymalloc(SRAMIN,20); //申请内存sprintf((char*)p,Total Size:%d,msgq_total_size); //显示DATA_Msg 消息队列总的大小LCD_ShowString(10,150,100,16,16,p);sprintf((char*)p,Remain Size:%d,msgq_remain_size); //显示DATA_Msg 剩余大小LCD_ShowString(10,190,100,16,16,p);myfree(SRAMIN,p); //释放内存taskEXIT_CRITICAL(); //退出临界区
}定时器9 的中断服务函数会调用函数disp_str()在LCD 上显示从队列Message_Queue 接收 到的消息。函数freertos_load_main_ui() 就是在屏幕上画出实验的初始UI 界面。函数 check_msg_queue()用于查询队列Message_Queue 的相关信息比如队列总大小队列当前剩余 大小。 (1)、调用函数uxQueueSpacesAvailable()获取队列Message_Queue 的剩余大小。 (2)、调用函数uxQueueMessagesWaiting()获取队列当前消息数量也就是队列的使用量将 其与函数uxQueueSpacesAvailable()获取到的队列剩余大小相加就是队列的总大小。 ●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键BEEP_Init(); //初始化蜂鸣器LCD_Init(); //初始化LCDTIM2_Int_Init(5000,7200-1); //初始化定时器2周期500msmy_mem_init(SRAMIN); //初始化内部内存池freertos_load_main_ui(); //加载主UI//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )start_task, //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(TaskHandle_t* )StartTask_Handler); //任务句柄vTaskStartScheduler(); //开启任务调度
}●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建消息Key_QueueKey_QueuexQueueCreate(KEYMSG_Q_NUM,sizeof(u8)); (1)//创建消息Message_Queue,队列项长度是串口接收缓冲区长度Message_QueuexQueueCreate(MESSAGE_Q_NUM,USART_REC_LEN); (2)//创建TASK1 任务xTaskCreate((TaskFunction_t )task1_task,(const char* )task1_task,(uint16_t )TASK1_STK_SIZE,(void* )NULL,(UBaseType_t )TASK1_TASK_PRIO,(TaskHandle_t* )Task1Task_Handler);//创建TASK2 任务xTaskCreate((TaskFunction_t )Keyprocess_task,(const char* )keyprocess_task,(uint16_t )KEYPROCESS_STK_SIZE,(void* )NULL,(UBaseType_t )KEYPROCESS_TASK_PRIO,(TaskHandle_t* )Keyprocess_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters)
{u8 key,i0;BaseType_t err;while(1){keyKEY_Scan(0); //扫描按键if((Key_Queue!0)(key)) //消息队列Key_Queue 创建成功,并且按键被按下{errxQueueSend(Key_Queue,key,10); (3)if(errerrQUEUE_FULL) //发送按键值{printf(队列Key_Queue 已满数据发送失败!\r\n);}}i;if(i%100) check_msg_queue();//检Message_Queue 队列的容量(4)if(i50){i0;LED0!LED0;}vTaskDelay(10); //延时10ms也就是10 个时钟节拍}
}
//Keyprocess_task 函数
void Keyprocess_task(void *pvParameters)
{u8 num,key;while(1){if(Key_Queue!0){//请求消息Key_Queueif(xQueueReceive(Key_Queue,key,portMAX_DELAY)) (5){switch(key) (6){case WKUP_PRES: //KEY_UP 控制LED1LED1!LED1;break;case KEY2_PRES: //KEY2 控制蜂鸣器BEEP!BEEP;break;case KEY0_PRES: //KEY0 刷新LCD 背景num;LCD_Fill(126,111,233,313,lcd_discolor[num%14]);break;}}}vTaskDelay(10); //延时10ms也就是10 个时钟节拍}
}(1)、在使用队列之前要先创建队列调用函数xQueueCreate()创建队列Key_Queue队列 长度为1每个队列项(消息)长度为1 个字节。 (2) 、同样的创建队列Message_Queue 队列长度为4 每个队列项( 消息) 长度为 USART_REC_LENUSART_REC_LEN 为50。 (3)、获取到按键键值以后就调用函数xQueueSend()发送到队列Key_Queue 中由于只有一 个队列Key_Queue 只有一个队列项所以此处也可以用覆写入队函数xQueueOverwrite()。 (4)、调用函数check_msg_queue()检查队列信息并将相关的信息显示在LCD 上如队列 总大小队列剩余大小等。 (5)、调用函数xQueueReceive()获取队列Key_Queue 中的消息 (6)、变量key 保存着获取到的消息也就是按键值这里根据不同的按键值做不同的处理。
●中断初始化及处理过程 本实验用到了两个中断串口1 的接收中断和定时器9 的定时中断串口1 的具体配置看 基础例程中的串口实验就可以了这里要将串口中断接收缓冲区大小改为50如下
#define USART_REC_LEN 50 //定义最大接收字节数50
#define EN_USART1_RX 1 //使能1/禁止0串口1 接收还要注意由于要在中断服务函数中使用FreeRTOS 中的API 函数所以一定要注意中断 优先级的设置这里设置如下
void uart_init(u32 bound)
{//GPIO 端口设置GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA,ENABLE); //使能USART1GPIOA 时钟//USART1_TX GPIOA.9GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; //PA.9GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; //复用推挽输出GPIO_Init(GPIOA, GPIO_InitStructure);//初始化GPIOA.9//USART1_RX GPIOA.10 初始化GPIO_InitStructure.GPIO_Pin GPIO_Pin_10;//PA10GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING;//浮空输入GPIO_Init(GPIOA, GPIO_InitStructure);//初始化GPIOA.10//Usart1 NVIC 配置NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority7 ;//抢占优先级7NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; //子优先级0NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; //IRQ 通道使能NVIC_Init(NVIC_InitStructure); //根据指定的参数初始化VIC 寄存器//USART 初始化设置USART_InitStructure.USART_BaudRate bound; //串口波特率USART_InitStructure.USART_WordLength USART_WordLength_8b;//字长为8 位数据格式USART_InitStructure.USART_StopBits USART_StopBits_1; //一个停止位USART_InitStructure.USART_Parity USART_Parity_No; //无奇偶校验位USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None;USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; //收发模式USART_Init(USART1, USART_InitStructure); //初始化串口1USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断USART_Cmd(USART1, ENABLE); //使能串口1
}注意红色代码的串口中断优先级设置这里设置抢占优先级为7子优先级为0。这个优先 级中可以调用FreeRTOS 中的API 函数。串口1 的中断服务函数如下
void USART1_IRQHandler(void) //串口1 中断服务程序
{u8 Res;BaseType_t xHigherPriorityTaskWoken;if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET){Res USART_ReceiveData(USART1); //读取接收到的数据if((USART_RX_STA0x8000)0) //接收未完成{if(USART_RX_STA0x4000) //接收到了0x0d{if(Res!0x0a)USART_RX_STA0; //接收错误,重新开始else USART_RX_STA|0x8000; //接收完成了}else //还没收到0X0D{if(Res0x0d)USART_RX_STA|0x4000;else{USART_RX_BUF[USART_RX_STA0X3FFF]Res ;USART_RX_STA;if(USART_RX_STA(USART_REC_LEN-1))USART_RX_STA0;}}}}//就向队列发送接收到的数据if((USART_RX_STA0x8000)(Message_Queue!NULL)) (1){//向队列中发送数据xQueueSendFromISR(Message_Queue,USART_RX_BUF,xHigherPriorityTaskWoken);(2)USART_RX_STA0;//清除数据接收缓冲区USART_RX_BUF,用于下一次数据接收memset(USART_RX_BUF,0,USART_REC_LEN); (3)//如果需要的话进行一次任务切换portYIELD_FROM_ISR(xHigherPriorityTaskWoken); (4)}
}(1)、判断是否接收到数据如果接收到数据的话就要将数据发送到队列Message_Queue 中 (2)、调用函数xQueueSendFromISR()将串口接收缓冲区USART_RX_BUF[]中接收到的数据 发送到队列Message_Queue 中。 (3)、发送完成以后要将串口接收缓冲区USART_RX_BUF[]清零。 (4) 、如果需要进行任务调度的话在退出串口中断服务函数之前调用函数 portYIELD_FROM_ISR()进行一次任务调度。 在定时2 的中断服务函数中请求队列Message_Queue 中的数据并将请求到的消息显示在 LCD 上定时器2 的定时周期设置为500ms定时器初始化很简单唯一要注意的就是中断优 先级的设置如下
//中断优先级NVIC 设置
NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; //TIM2 中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 8; //先占优先级4 级
NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; //从优先级0 级
NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; //IRQ 通道被使能
NVIC_Init(NVIC_InitStructure); //初始化NVIC 寄存器定时器2 的中断服务函数是重点代码如下
extern QueueHandle_t Message_Queue; //信息队列句柄
extern void disp_str(u8* str);
//定时器2 中断服务函数
void TIM2_IRQHandler(void)
{u8 *buffer;BaseType_t xTaskWokenByReceivepdFALSE;BaseType_t err;if(TIM_GetITStatus(TIM2,TIM_IT_Update)SET) //溢出中断{buffermymalloc(SRAMIN,USART_REC_LEN); (1)if(Message_Queue!NULL){memset(buffer,0,USART_REC_LEN); //清除缓冲区(2)//请求消息Message_QueueerrxQueueReceiveFromISR(Message_Queue,buffer,xTaskWokenByReceive); (3)if(errpdTRUE) //接收到消息{disp_str(buffer); //在LCD 上显示接收到的消息(4)}}myfree(SRAMIN,buffer); //释放内存(5)//如果需要的话进行一次任务切换portYIELD_FROM_ISR(xTaskWokenByReceive); (6)}TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //清除中断标志位
}(1)、从队列中获取消息也是采用数据拷贝的方式所以我们要先准备一个数据缓冲区用来 保存从队列中获取到的消息。这里通过动态内存管理的方式分配一个数据缓冲区这个动态内 存管理方法是ALIENTEK 编写的具体原理和使用方法请参考基础例程中的内存管理实验。当 然了也可以使用FreeRTOS 提供的动态内存管理函数。直接提供一个数组也行这个数据缓 冲区的大小一定要和队列中队列项大小相同比如本例程就是USART_REC_LEN。 (2)、清除缓冲区。 (3)、调用函数xQueueReceiveFromISR()从队列Message_Queue 中获取消息。 (4)、如果获取消息成功的话就调用函数disp_str()将获取到的消息显示在LCD 上。 (5)、使用完成以后就释放(3)中申请到的数据缓冲区内存。 (6) 、如果需要进行任务调度的话在退出定时器的中断服务函数之前调用函数 portYIELD_FROM_ISR()进行一次任务调度。
13.7.2 程序运行结果分析
编译并下载实验代码到开发板中打开串口调试助手LCD 默认显示如图13.7.2.1 所示 通过串口调试助手给开发板发送一串字符串比如“ALIENTEK”由于定时器9 会周期 性的读取队列Message_Queue 中的数据当读取成功以后就会将相应的数据显示在LCD 上 所以LCD 上会显示字符串“ALENTEK”,如图13.7.2.3 所示 通过串口向开发板发送数据的时候注意观察队列Message_Queue 剩余大小的变化最后按 不同的按键看看有什么反应是否和我们的代码中设置的相同。
第十四章FreeRTOS 信号量
信号量是操作系统中重要的一部分信号量一般用来进行资源管理和任务同步FreeRTOS 中信号量又分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量。不同的信号量其 应用场景不同但有些应用场景是可以互换着使用的本章我们就来学习一下FreeRTOS 的信 号量本章分为如下几部分 14.1 信号量简介 14.2 二值信号量 14.3 二值信号量操作实验 14.4 计数型信号量 14.5 计数型信号量操作实验 14.6 优先级翻转 14.7 优先级翻转实验 14.8 互斥信号量 14.9 互斥信号量操作实验 14.10 递归互斥信号量
14.1 信号量简介
信号量常常用于控制对共享资源的访问和任务同步。举一个很常见的例子某个停车场有 100 个停车位这100 个停车位大家都可以用对于大家来说这100 个停车位就是共享资源。 假设现在这个停车场正常运行你要把车停到这个这个停车场肯定要先看一下现在停了多少车 了还有没有停车位当前停车数量就是一个信号量具体的停车数量就是这个信号量值当 这个值到100 的时候说明停车场满了。停车场满的时你可以等一会看看有没有其他的车开出停 车场当有车开出停车场的时候停车数量就会减一也就是说信号量减一此时你就可以把车 停进去了你把车停进去以后停车数量就会加一也就是信号量加一。这就是一个典型的使用 信号量进行共享资源管理的案例在这个案例中使用的就是计数型信号量。再看另外一个案例 使用公共电话我们知道一次只能一个人使用电话这个时候公共电话就只可能有两个状态 使用或未使用如果用电话的这两个状态作为信号量的话那么这个就是二值信号量。 信号量用于控制共享资源访问的场景相当于一个上锁机制代码只有获得了这个锁的钥匙 才能够执行。 上面我们讲了信号量在共享资源访问中的使用信号量的另一个重要的应用场合就是任务 同步用于任务与任务或中断与任务之间的同步。在执行中断服务函数的时候可以通过向任务 发送信号量来通知任务它所期待的事件发生了当退出中断服务函数以后在任务调度器的调度 下同步的任务就会执行。在编写中断服务函数的时候我们都知道一定要快进快出中断服务函 数里面不能放太多的代码否则的话会影响的中断的实时性。裸机编写中断服务函数的时候一 般都只是在中断服务函数中打个标记然后在其他的地方根据标记来做具体的处理过程。在使 用RTOS 系统的时候我们就可以借助信号量完成此功能当中断发生的时候就释放信号量中 断服务函数不做具体的处理。具体的处理过程做成一个任务这个任务会获取信号量如果获 取到信号量就说明中断发生了那么就开始完成相应的处理这样做的好处就是中断执行时间 非常短。这个例子就是中断与任务之间使用信号量来完成同步当然了任务与任务之间也可 以使用信号量来完成同步。 FreeRTOS 中还有一些其他特殊类型的信号量比如互斥信号量和递归互斥信号量这些具 体遇到的时候在讲解。有关信号量的知识在FreeRTOS 的官网上都有详细的讲解包括二值信 号量、计数型信号量、互斥信号量和递归互斥信号量我们下面要讲解的这些涉及到理论性的 知识都是翻译自FreeRTOS 官方资料感兴趣的可以去官网看原版的英文资料。
14.2 二值信号量
14.2.1 二值信号量简介
二值信号量通常用于互斥访问或同步二值信号量和互斥信号量非常类似但是还是有一 些细微的差别互斥信号量拥有优先级继承机制二值信号量没有优先级继承。因此二值信号 另更适合用于同步(任务与任务或任务与中断的同步)而互斥信号量适合用于简单的互斥访问 有关互斥信号量的内容后面会专门讲解本节只讲解二值信号量在同步中的应用。 和队列一样信号量API 函数允许设置一个阻塞时间阻塞时间是当任务获取信号量的时 候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同一 一个信号量上的话那么优先级最高的哪个任务优先获得信号量这样当信号量有效的时候高优 先级的任务就会解除阻塞状态。 二值信号量其实就是一个只有一个队列项的队列这个特殊的队列要么是满的要么是空 的这不正好就是二值的吗任务和中断使用这个特殊队列不用在乎队列中存的是什么消息只需要知道这个队列是满的还是空的。可以利用这个机制来完成任务与中断之间的同步。 在实际应用中通常会使用一个任务来处理MCU 的某个外设比如网络应用中一般最简 单的方法就是使用一个任务去轮询的查询MCU 的ETH(网络相关外设如STM32 的以太网 MAC)外设是否有数据当有数据的时候就处理这个网络数据。这样使用轮询的方式是很浪费 CPU 资源的而且也阻止了其他任务的运行。最理想的方法就是当没有网络数据的时候网络任 务就进入阻塞态把CPU 让给其他的任务当有数据的时候网络任务才去执行。现在使用二值 信号量就可以实现这样的功能任务通过获取信号量来判断是否有网络数据没有的话就进入 阻塞态而网络中断服务函数(大多数的网络外设都有中断功能比如STM32 的MAC 专用DMA 中断通过中断可以判断是否接收到数据)通过释放信号量来通知任务以太网外设接收到了网络 数据网络任务可以去提取处理了。网络任务只是在一直的获取二值信号量它不会释放信号 量而中断服务函数是一直在释放信号量它不会获取信号量。在中断服务函数中发送信号量 可以使用函数xSemaphoreGiveFromISR()也可以使用任务通知功能来替代二值信号量而且使 用任务通知的话速度更快代码量更少有关任务通知的内容后面会有专门的章节介绍。 使用二值信号量来完成中断与任务同步的这个机制中任务优先级确保了外设能够得到及 时的处理这样做相当于推迟了中断处理过程。也可以使用队列来替代二值信号量在外设事 件的中断服务函数中获取相关数据并将相关的数据通过队列发送给任务。如果队列无效的话 任务就进入阻塞态直至队列中有数据任务接收到数据以后就开始相关的处理过程。下面几 个步骤演示了二值信号量的工作过程。 1、二值信号量无效 在图14.2.1.1 中任务Task 通过函数xSemaphoreTake()获取信号量但是此时二值信号量无 效所以任务Task 进入阻塞态。 2、中断释放信号量 此时中断发生了在中断服务函数中通过函数xSemaphoreGiveFromISR()释放信号量因此信号量变为有效。 3、任务获取信号量成功 由于信号量已经有效了所以任务Task 获取信号量成功任务从阻塞态解除开始执行相 关的处理过程。
4、任务再次进入阻塞态 由于任务函数一般都是一个大循环所以在任务做完相关的处理以后就会再次调用函数 xSemaphoreTake()获取信号量。在执行完第三步以后二值信号量就已经变为无效的了所以任务 将再次进入阻塞态和第一步一样直至中断再次发生并且调用函数xSemaphoreGiveFromISR() 释放信号量。
14.2.2 创建二值信号量
同队列一样要想使用二值信号量就必须先创建二值信号量二值信号量创建函数如表14.2.2 所示
1、函数vSemaphoreCreateBinary () 此函数是老版本FreeRTOS 中的创建二值信号量函数新版本已经不再使用了新版本的 FreeRTOS 使用xSemaphoreCreateBinary()来替代此函数这里还保留这个函数是为了兼容那些 基于老版本FreeRTOS 而做的应用层代码。此函数是个宏具体创建过程是由函数 xQueueGenericCreate()来完成的在文件semphr.h 中有如下定义
void vSemaphoreCreateBinary( SemaphoreHandle_t xSemaphore )参数 xSemaphore保存创建成功的二值信号量句柄。 返回值 NULL: 二值信号量创建失败。 其他值: 二值信号量创建成功。 2、函数xSemaphoreCreateBinary() 此函数是vSemaphoreCreateBinary()的新版本新版本的FreeRTOS 中统一用此函数来创建 二值信号量。使用此函数创建二值信号量的话信号量所需要的RAM 是由FreeRTOS 的内存管 理部分来动态分配的。此函数创建好的二值信号量默认是空的也就是说刚创建好的二值信号 量使用函数xSemaphoreTake()是获取不到的此函数也是个宏具体创建过程是由函数 xQueueGenericCreate()来完成的函数原型如下
SemaphoreHandle_t xSemaphoreCreateBinary( void )参数 无。 返回值 NULL: 二值信号量创建失败。 其他值: 创建成功的二值信号量的句柄。 3、函数xSemaphoreCreateBinaryStatic() 此函数也是创建二值信号量的只不过使用此函数创建二值信号量的话信号量所需要的 RAM 需要由用户来分配此函数是个宏具体创建过程是通过函数xQueueGenericCreateStatic() 来完成的函数原型如下
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer )参数 pxSemaphoreBuffer此参数指向一个StaticSemaphore_t 类型的变量用来保存信号量结构体。 返回值 NULL: 二值信号量创建失败。 其他值: 创建成功的二值信号量句柄。
14.2.3 二值信号量创建过程分析
上一小节讲了三个用于二值信号量创建的函数两个动态的创建函数和一个静态的创建函 数。本节就来分析一下这两个动态的创建函数静态创建函数和动态的类似就不做分析了。 首先来看一下老版本的二值信号量动态创建函数vSemaphoreCreateBinary()函数代码如下
#if( configSUPPORT_DYNAMIC_ALLOCATION 1 )
#define vSemaphoreCreateBinary( xSemaphore ) \
{ \( xSemaphore ) xQueueGenericCreate( ( UBaseType_t ) 1, \ (1)semSEMAPHORE_QUEUE_ITEM_LENGTH, \queueQUEUE_TYPE_BINARY_SEMAPHORE ); \if( ( xSemaphore ) ! NULL ) \{ \( void ) xSemaphoreGive( ( xSemaphore ) ); \ (2)} \
}
#endif
(1)、上面说了二值信号量是在队列的基础上实现的所以创建二值信号量就是创建队列的 过程。这里使用函数xQueueGenericCreate()创建了一个队列队列长度为1队列项长度为0 队列类型为queueQUEUE_TYPE_BINARY_SEMAPHORE也就是二值信号量。 (2)、当二值信号量创建成功以后立即调用函数xSemaphoreGive()释放二值信号量此时新 创建的二值信号量有效。 在来看一下新版本的二值信号量创建函数xSemaphoreCreateBinary()函数代码如下
#if( configSUPPORT_DYNAMIC_ALLOCATION 1 )
#define xSemaphoreCreateBinary() \xQueueGenericCreate( ( UBaseType_t ) 1, \semSEMAPHORE_QUEUE_ITEM_LENGTH, \queueQUEUE_TYPE_BINARY_SEMAPHORE ) \
#endif可以看出新版本的二值信号量创建函数也是使用函数xQueueGenericCreate()来创建一个类 型为queueQUEUE_TYPE_BINARY_SEMAPHORE、长度为1、队列项长度为0 的队列。这一步 和老版本的二值信号量创建函数一样唯一不同的就是新版本的函数在成功创建二值信号量以 后不会立即调用函数xSemaphoreGive()释放二值信号量。也就是说新版函数创建的二值信号量 默认是无效的而老版本是有效的。 大家注意看创建的队列是个没有存储区的队列前面说了使用队列是否为空来表示二值 信号量而队列是否为空可以通过队列结构体的成员变量uxMessagesWaiting 来判断。
14.2.4 释放信号量
释放信号量的函数有两个如表14.2.4.1 所示 同队列一样释放信号量也分为任务级和中断级。还有不管是二值信号量、计数型信号 量还是互斥信号量它们都使用表14.2.4.1 中的函数释放信号量递归互斥信号量有专用的释 放函数。 1、函数xSemaphoreGive() 此函数用于释放二值信号量、计数型信号量或互斥信号量此函数是一个宏真正释放信 号量的过程是由函数xQueueGenericSend()来完成的函数原型如下
BaseType_t xSemaphoreGive( xSemaphore )参数 xSemaphore要释放的信号量句柄。 返回值 pdPASS: 释放信号量成功。 errQUEUE_FULL: 释放信号量失败。 我们再来看一下函数xSemaphoreGive()的具体内容此函数在文件semphr.h 中有如下定义
#define xSemaphoreGive( xSemaphore ) \xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), \NULL, \semGIVE_BLOCK_TIME, \queueSEND_TO_BACK ) \可以看出任务级释放信号量就是向队列发送消息的过程只是这里并没有发送具体的消息 阻塞时间为0(宏semGIVE_BLOCK_TIME 为0)入队方式采用的后向入队。具体入队过程第十 三章已经做了详细的讲解入队的时候队列结构体成员变量uxMessagesWaiting 会加一对于二值信号量通过判断uxMessagesWaiting 就可以知道信号量是否有效了当uxMessagesWaiting 为 1 的话说明二值信号量有效为0 就无效。如果队列满的话就返回错误值errQUEUE_FULL提 示队列满入队失败。 2、函数xSemaphoreGiveFromISR() 此函数用于在中断中释放信号量此函数只能用来释放二值信号量和计数型信号量绝对 不能用来在中断服务函数中释放互斥信号量此函数是一个宏真正执行的是函数 xQueueGiveFromISR()此函数原型如下
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,BaseType_t * pxHigherPriorityTaskWoken)参数 xSemaphore要释放的信号量句柄。 pxHigherPriorityTaskWoken标记退出此函数以后是否进行任务切换这个变量的值由这 三个函数来设置的用户不用进行设置用户只需要提供一 个变量来保存这个值就行了。当此值为pdTRUE 的时候在退 出中断服务函数之前一定要进行一次任务切换。 返回值 pdPASS: 释放信号量成功。 errQUEUE_FULL: 释放信号量失败。 在中断中释放信号量真正使用的是函数xQueueGiveFromISR()此函数和中断级通用入队 函数xQueueGenericSendFromISR() 极其类似只是针对信号量做了微小的改动。函数 xSemaphoreGiveFromISR()不能用于在中断中释放互斥信号量因为互斥信号量涉及到优先级继 承的问题而中断不属于任务没法处理中断优先级继承。大家可以参考第十三章分析函数 xQueueGenericSendFromISR()的过程来分析xQueueGiveFromISR()。
14.2.5 获取信号量
获取信号量也有两个函数如表14.2.5.1 所示 同释放信号量的API 函数一样不管是二值信号量、计数型信号量还是互斥信号量它们 都使用表14.2.5.1 中的函数获取信号量 1、函数xSemaphoreTake() 此函数用于获取二值信号量、计数型信号量或互斥信号量此函数是一个宏真正获取信 号量的过程是由函数xQueueGenericReceive ()来完成的函数原型如下
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,
TickType_t xBlockTime)参数 xSemaphore要获取的信号量句柄。 xBlockTime: 阻塞时间。 返回值 pdTRUE: 获取信号量成功。 pdFALSE: 超时获取信号量失败。 再来看一下函数xSemaphoreTake ()的具体内容此函数在文件semphr.h 中有如下定义
#define xSemaphoreTake( xSemaphore, xBlockTime ) \
xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), \NULL, \( xBlockTime ), \pdFALSE ) \获取信号量的过程其实就是读取队列的过程只是这里并不是为了读取队列中的消息。在 第十三章讲解函数xQueueGenericReceive()的时候说过如果队列为空并且阻塞时间为0 的话就 立即返回errQUEUE_EMPTY表示队列满。如果队列为空并且阻塞时间不为0 的话就将任务 添加到延时列表中。如果队列不为空的话就从队列中读取数据(获取信号量不执行这一步)数 据读取完成以后还需要将队列结构体成员变量uxMessagesWaiting 减一然后解除某些因为入 队而阻塞的任务最后返回pdPASS 表示出对成功。互斥信号量涉及到优先级继承处理方式 不同后面讲解互斥信号量的时候在详细的讲解。 2、函数xSemaphoreTakeFromISR () 此函数用于在中断服务函数中获取信号量此函数用于获取二值信号量和计数型信号量 绝对不能使用此函数来获取互斥信号量此函数是一个宏真正执行的是函数xQueueReceiveFromISR ()此函数原型如下
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t * pxHigherPriorityTaskWoken)参数 xSemaphore要获取的信号量句柄。 pxHigherPriorityTaskWoken标记退出此函数以后是否进行任务切换这个变量的值由这 三个函数来设置的用户不用进行设置用户只需要提供一 个变量来保存这个值就行了。当此值为pdTRUE 的时候在退 出中断服务函数之前一定要进行一次任务切换。 返回值 pdPASS: 获取信号量成功。 pdFALSE: 获取信号量失败。 在中断中获取信号量真正使用的是函数xQueueReceiveFromISR ()这个函数就是中断级 出队函数当队列不为空的时候就拷贝队列中的数据(用于信号量的时候不需要这一步)然后 将队列结构体中的成员变量uxMessagesWaiting 减一如果有任务因为入队而阻塞的话就解除 阻塞态当解除阻塞的任务拥有更高优先级的话就将参数pxHigherPriorityTaskWoken 设置为 pdTRUE最后返回pdPASS 表示出队成功。如果队列为空的话就直接返回pdFAIL 表示出队失 败这个函数还是很简单的。
14.3 二值信号量操作实验
14.3.1 实验程序设计
1、实验目的 二值信号量的使命就是同步完成任务与任务或中断与任务之间的同步。大多数情况下都 是中断与任务之间的同步。本节就学习一下如何使用二值信号量来完成中断与任务之间的同步。 2、实验设计 本节我们设计一个通过串口发送指定的指令来控制开发板上的LED1 和BEEP 开关的实验 指令如下(不区分大小写) LED1ON打开LED1。 LED1OFF关闭LED1。 BEEPON打开蜂鸣器。 BEEPOFF关闭蜂鸣器。 这些指令通过串口发送给开发板指令是不分大小写的开发板使用中断接收当接收到 数据以后就释放二值信号量。任务DataProcess_task()用于处理这些指令任务会一直尝试获取 二值信号量当获取到信号量就会从串口接收缓冲区中提取这些指令然后根据指令控制相应 的外设。 本实验设计三个任务start_task、task1_task 、DataProcess_task 这三个任务的任务功能如 下 start_task用来创建其他2 个任务。 task1_task 控制LED0 闪烁提示系统正在运行。 DataProcess_task 指令处理任务根据接收到的指令来控制不同的外设。 实验中还创建了一个二值信号量BinarySemaphore 用于完成串口中断和任务 DataProcess_task 之间的同步。 3、实验工程 FreeRTOS 实验14-1 FreeRTOS 二值信号量操作实验。 4、实验程序与分析 ●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TASK1_TASK_PRIO 2 //任务优先级
#define TASK1_STK_SIZE 256 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数
#define DATAPROCESS_TASK_PRIO 3 //任务优先级
#define DATAPROCESS_STK_SIZE 256 //任务堆栈大小
TaskHandle_t DataProcess_Handler; //任务句柄
void DataProcess_task(void *pvParameters); //任务函数
//二值信号量句柄
SemaphoreHandle_t BinarySemaphore; //二值信号量句柄
//用于命令解析用的命令值
#define LED1ON 1
#define LED1OFF 2
#define BEEPON 3
#define BEEPOFF 4
#define COMMANDERR 0XFF
●其他应用函数
//将字符串中的小写字母转换为大写
//str:要转换的字符串
//len字符串长度
void LowerToCap(u8 *str,u8 len)
{u8 i;for(i0;ilen;i){if((96str[i])(str[i]123)) //小写字母str[i]str[i]-32; //转换为大写}
}
//命令处理函数将字符串命令转换成命令值
//str命令
//返回值: 0XFF命令错误其他值命令值
u8 CommandProcess(u8 *str)
{u8 CommandValueCOMMANDERR;if(strcmp((char*)str,LED1ON)0) CommandValueLED1ON;else if(strcmp((char*)str,LED1OFF)0) CommandValueLED1OFF;else if(strcmp((char*)str,BEEPON)0) CommandValueBEEPON;else if(strcmp((char*)str,BEEPOFF)0) CommandValueBEEPOFF;return CommandValue;
}函数LowerToCap()用于将串口发送过来的命令中的小写字母统一转换成大写字母这样就 可以在发送命令的时候不用区分大小写因为开发板会统一转换成大写。函数CommandProcess() 用于将接收到的命令字符串转换成命令值比如命令“LED1ON”转换成命令值就是0(宏 LED1ON 为0)。 ●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键BEEP_Init(); //初始化蜂鸣器LCD_Init(); //初始化LCDmy_mem_init(SRAMIN); //初始化内部内存池POINT_COLORRED;LCD_ShowString(10,10,200,16,16,ATK STM32F103/407);LCD_ShowString(10,30,200,16,16,FreeRTOS Examp 14-1);LCD_ShowString(10,50,200,16,16,Binary Semap);LCD_ShowString(10,70,200,16,16,Command data:);//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )start_task, //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(TaskHandle_t* )StartTask_Handler); //任务句柄vTaskStartScheduler(); //开启任务调度
}
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建二值信号量BinarySemaphorexSemaphoreCreateBinary();//创建TASK1 任务xTaskCreate((TaskFunction_t )task1_task,(const char* )task1_task,(uint16_t )TASK1_STK_SIZE,(void* )NULL,(UBaseType_t )TASK1_TASK_PRIO,(TaskHandle_t* )Task1Task_Handler);//创建TASK2 任务xTaskCreate((TaskFunction_t )DataProcess_task,(const char* )keyprocess_task,(uint16_t )DATAPROCESS_STK_SIZE,(void* )NULL,(UBaseType_t )DATAPROCESS_TASK_PRIO,(TaskHandle_t* )DataProcess_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//task1 任务函数
void task1_task(void *pvParameters)
{while(1){LED0!LED0;vTaskDelay(500); //延时500ms也就是500 个时钟节拍}
}
//DataProcess_task 函数
void DataProcess_task(void *pvParameters)
{u8 len0;u8 CommandValueCOMMANDERR;BaseType_t errpdFALSE;u8 *CommandStr;POINT_COLORBLUE;while(1){if(BinarySemaphore!NULL){errxSemaphoreTake(BinarySemaphore,portMAX_DELAY);//获取信号量(1)if(errpdTRUE) //获取信号量成功{lenUSART_RX_STA0x3fff; //得到此次接收到的数据长度CommandStrmymalloc(SRAMIN,len1); //申请内存sprintf((char*)CommandStr,%s,USART_RX_BUF);CommandStr[len]\0; //加上字符串结尾符号LowerToCap(CommandStr,len); //将字符串转换为大写(2)CommandValueCommandProcess(CommandStr); //命令解析(3)if(CommandValue!COMMANDERR) //接收到正确的命令{LCD_Fill(10,90,210,110,WHITE); //清除显示区域LCD_ShowString(10,90,200,16,16,CommandStr);//在LCD 上显示命令printf(命令为:%s\r\n,CommandStr);switch(CommandValue) //处理命令(4){case LED1ON:LED10;break;case LED1OFF:LED11;break;case BEEPON:BEEP1;break;case BEEPOFF:BEEP0;break;}}else{printf(无效的命令请重新输入!!\r\n);}USART_RX_STA0;memset(USART_RX_BUF,0,USART_REC_LEN);//串口接收缓冲区清零myfree(SRAMIN,CommandStr); //释放内存}}else if(errpdFALSE){vTaskDelay(10); //延时10ms也就是10 个时钟节拍}}
}(1) 、使用函数xSemaphoreTake() 获取二值信号量BinarySemaphore 延时时间为 portMAX_DELAY。 (2)、调用函数LowerToCap()将命令字符串中的小写字母转换成大写的。 (3)、调用函数CommandProcess()处理命令字符串其实就是将命令字符串转换为命令值。 (4)、根据不同的命令值执行不同的操作如开关LED1开关BEEP。 ●中断初始化及处理过程 本实验中串口1 是通过中断方式来接收数据的所以需要初始化串口1串口的初始化很 简单前面已经讲了很多次了。不过要注意串口1 的中断优先级因为我们要在串口1 的中断 服务函数中使用FeeRTOS 的API 函数本实验设置串口1 的抢占优先级为7子优先级为0 如下
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority7 ; //抢占优先级7
NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; //IRQ 通道使能
NVIC_Init(NVIC_InitStructure); //初始化NVIC 寄存器串口1 的中断服务函数如下
extern SemaphoreHandle_t BinarySemaphore; //二值信号量句柄
void USART1_IRQHandler(void) //串口1 中断服务程序
{u8 Res;BaseType_t xHigherPriorityTaskWoken;if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET){Res USART_ReceiveData(USART1);//(USART1-DR); //读取接收到的数据if((USART_RX_STA0x8000)0)//接收未完成{if(USART_RX_STA0x4000)//接收到了0x0d{if(Res!0x0a)USART_RX_STA0;//接收错误,重新开始else USART_RX_STA|0x8000; //接收完成了}else //还没收到0X0D{if(Res0x0d)USART_RX_STA|0x4000;else{USART_RX_BUF[USART_RX_STA0X3FFF]Res ;USART_RX_STA;if(USART_RX_STA(USART_REC_LEN-1))USART_RX_STA0;}}}}//释放二值信号量if((USART_RX_STA0x8000)(BinarySemaphore!NULL)){//释放二值信号量xSemaphoreGiveFromISR(BinarySemaphore,xHigherPriorityTaskWoken); (1)portYIELD_FROM_ISR(xHigherPriorityTaskWoken);//如果需要的话进行一次任务切换}
}
(1) 、当串口接收到数据以后就调用函数xSemaphoreGiveFromISR() 释放信号量 BinarySemaphore。
14.3.2 程序运行结果分析
编译并下载实验代码到开发板中打开串口调试助手通过串口调试助手发送命令比如 命令“led1ON”开发板接收到命令以后就会将命令中的小写字母转换为大写并且显示在LCD上如图14.3.2.1 所示 当命令正确的时候LED1 就会亮同时开发板向串口调试助手发送经过大小写转换后的命 令字符串如图14.3.2.2 所示 当命令错误的时候开发板就会向串口调试助手发送命令错误的提示信息比如我们发送 “led1_off”这个命令,串口调试助手显示如图14.3.2.3 所示
14.4 计数型信号量
14.4.1 计数型信号量简介
有些资料中也将计数型信号量叫做数值信号量二值信号量相当于长度为1 的队列那么 计数型信号量就是长度大于1 的队列。同二值信号量一样用户不需要关心队列中存储了什么 数据只需要关心队列是否为空即可。计数型信号量通常用于如下两个场合 1、事件计数 在这个场合中每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数 值)其他任务会获取信号量(信号量计数值减一信号量值就是队列结构体成员变量 uxMessagesWaiting)来处理事件。在这种场合中创建的计数型信号量初始计数值为0。 2、资源管理 在这个场合中信号量值代表当前资源的可用数量比如停车场当前剩余的停车位数量。 一个任务要想获得资源的使用权首先必须获取信号量信号量获取成功以后信号量值就会减 一。当信号量值为0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量 释放信号量以后信号量值会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量 比如停车场一共有100 个停车位那么创建信号量的时候信号量值就应该初始化为100。
14.4.2 创建计数型信号量
FreeRTOS 提供了两个计数型信号量创建函数如表14.4.2.1 所示 1、函数xSemaphoreCreateCounting() 此函数用于创建一个计数型信号量所需要的内存通过动态内存管理方法分配。此函数本质是一个宏真正完成信号量创建的是函数xQueueCreateCountingSemaphore()此函数原型如 下
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount )参数 uxMaxCount计数信号量最大计数值当信号量值等于此值的时候释放信号量就会失败。 uxInitialCount计数信号量初始值。 返回值 NULL: 计数型信号量创建失败。 其他值: 计数型信号量创建成功返回计数型信号量句柄。 2、函数xSemaphoreCreateCountingStatic() 此函数也是用来创建计数型信号量的使用此函数创建计数型信号量的时候所需要的内存 需要由用户分配。此函数也是一个宏真正执行的是函数xQueueCreateCountingSemaphoreStatic() 函数原型如下
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t * pxSemaphoreBuffer )参数 uxMaxCount计数信号量最大计数值当信号量值等于此值的时候释放信号量就会失败。 uxInitialCount计数信号量初始值。 pxSemaphoreBuffer指向一个StaticSemaphore_t 类型的变量用来保存信号量结构体。 返回值 NULL: 计数型信号量创建失败。 其他值: 计数型号量创建成功返回计数型信号量句柄。
14.4.3 计数型信号量创建过程分析
这里只分析动态创建计数型信号量函数xSemaphoreCreateCounting()此函数是个宏定义 如下
#if( configSUPPORT_DYNAMIC_ALLOCATION 1 )
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) \
xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) ) \
#endif可以看出真正干事的是函数xQueueCreateCountingSemaphore()此函数在文件queue.c 中 有如下定义
QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount,const UBaseType_t uxInitialCount )
{QueueHandle_t xHandle;configASSERT( uxMaxCount ! 0 );configASSERT( uxInitialCount uxMaxCount );xHandle xQueueGenericCreate( uxMaxCount,\ (1)queueSEMAPHORE_QUEUE_ITEM_LENGTH, \queueQUEUE_TYPE_COUNTING_SEMAPHORE );if( xHandle ! NULL ){( ( Queue_t * ) xHandle )-uxMessagesWaiting uxInitialCount; (2)traceCREATE_COUNTING_SEMAPHORE();}else{traceCREATE_COUNTING_SEMAPHORE_FAILED();}return xHandle;
}(1)、计数型信号量也是在队列的基础上实现的所以需要调用函数xQueueGenericCreate() 创建一个队列队列长度为uxMaxCount 对列项长度为 queueSEMAPHORE_QUEUE_ITEM_LENGTH( 此宏为0) 队列的类型为 queueQUEUE_TYPE_COUNTING_SEMAPHORE表示是个计数型信号量。 (2)、队列结构体的成员变量uxMessagesWaiting 用于计数型信号量的计数根据计数型信 号量的初始值来设置uxMessagesWaiting。
14.4.4 释放和获取计数信号量
计数型信号量的释放和获取与二值信号量相同具体请参考14.2.3 和14.2.4 小节。
14.5 计数型信号量操作实验
14.5.1 实验程序设计
1、实验目的 计数型信号量一般用于事件计数和资源管理计数型信号量在这个场景中的使用方法基本 一样本实验就来学习一下计数型信号量在事件计数中的使用方法。 2、实验设计 本实验中用KEY_UP 按键来模拟事件当KEY_UP 按下以后就表示事件发生当检测到 KEY_UP 按下以后就释放计数型信号量按键的检测和信号量的释放做成一个任务。另外一个 任务获取信号量当信号量获取成功以后就刷新LCD 上指定区域的背景颜色并且显示计数型 信号量的值。 本实验设计三个任务start_task、SemapGive_task 、SemapTake_task 这三个任务的任务功 能如下 start_task用来创建其他2 个任务。 SemapGive_task 获取按键状态当KEY_UP 键按下去以后就释放信号量CountSemaphore 此任务还用来控制LED0 的亮灭来提示程序正在运行中。 SemapTake_task 获取信号量CountSemaphore当获取信号量成功以后就刷新LCD 指定 区域的背景色。 实验中创建了一个计数型信号量CountSemaphore此信号量用于记录KEY_UP 按下的次 数。硬件部分需要用到KEY_UP 按键用于模拟事件发生。 3、实验工程 FreeRTOS 实验14-2 FreeRTOS 计数型信号量操作实验。 4、实验程序与分析 ●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define SEMAPGIVE_TASK_PRIO 2 //任务优先级
#define SEMAPGIVE_STK_SIZE 256 //任务堆栈大小
TaskHandle_t SemapGiveTask_Handler; //任务句柄
void SemapGive_task(void *pvParameters); //任务函数
#define SEMAPTAKE_TASK_PRIO 3 //任务优先级
#define SEMAPTAKE_STK_SIZE 256 //任务堆栈大小
TaskHandle_t SemapTakeTask_Handler; //任务句柄
void SemapTake_task(void *pvParameters); //任务函数
//计数型信号量句柄
SemaphoreHandle_t CountSemaphore;//计数型信号量
//LCD 刷屏时使用的颜色
int lcd_discolor[14]{ WHITE, BLACK, BLUE, BRED,GRED, GBLUE, RED, MAGENTA,GREEN, CYAN, YELLOW, BROWN,BRRED, GRAY };
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键BEEP_Init(); //初始化蜂鸣器LCD_Init(); //初始化LCDmy_mem_init(SRAMIN); //初始化内部内存池POINT_COLOR RED;LCD_ShowString(30,10,200,16,16,ATK STM32F103/407);LCD_ShowString(30,30,200,16,16,FreeRTOS Examp 14-2);LCD_ShowString(30,50,200,16,16,Count Semaphore);LCD_ShowString(30,70,200,16,16,ATOMALIENTEK);LCD_ShowString(30,90,200,16,16,2016/11/25);POINT_COLOR BLACK;LCD_DrawRectangle(5,110,234,314);LCD_DrawLine(5,130,234,130);POINT_COLOR RED;LCD_ShowString(30,111,200,16,16,COUNT_SEM Value: 0);POINT_COLOR BLUE;//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )start_task, //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(TaskHandle_t* )StartTask_Handler); //任务句柄vTaskStartScheduler(); //开启任务调度
}
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建计数型信号量CountSemaphorexSemaphoreCreateCounting(255,0); (1)//创建释放信号量任务xTaskCreate((TaskFunction_t )SemapGive_task,(const char* )semapgive_task,(uint16_t )SEMAPGIVE_STK_SIZE,(void* )NULL,(UBaseType_t )SEMAPGIVE_TASK_PRIO,(TaskHandle_t* )SemapGiveTask_Handler);//创建获取信号量任务xTaskCreate((TaskFunction_t )SemapTake_task,(const char* )semaptake_task,(uint16_t )SEMAPTAKE_STK_SIZE,(void* )NULL,(UBaseType_t )SEMAPTAKE_TASK_PRIO,(TaskHandle_t* )SemapTakeTask_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//释放计数型信号量任务函数
void SemapGive_task(void *pvParameters)
{u8 key,i0;u8 semavalue;BaseType_t err;while(1){keyKEY_Scan(0); //扫描按键if(CountSemaphore!NULL) //计数型信号量创建成功{switch(key){case WKUP_PRES:errxSemaphoreGive(CountSemaphore);//释放计数型信号量(2)if(errpdFALSE){printf(信号量释放失败!!!\r\n);}//获取计数型信号量值semavalueuxSemaphoreGetCount(CountSemaphore); (3) LCD_ShowxNum(155,111,semavalue,3,16,0);//显示信号量值(4)break;}}i;if(i50){i0;LED0!LED0;}vTaskDelay(10); //延时10ms也就是10 个时钟节拍}
}
//获取计数型信号量任务函数
void SemapTake_task(void *pvParameters)
{u8 num;u8 semavalue;while(1){xSemaphoreTake(CountSemaphore,portMAX_DELAY); //等待数值信号量(5)num;semavalueuxSemaphoreGetCount(CountSemaphore); //获取数值信号量值(6)LCD_ShowxNum(155,111,semavalue,3,16,0); //显示信号量值LCD_Fill(6,131,233,313,lcd_discolor[num%14]); //刷屏LED1!LED1;vTaskDelay(1000); //延时1s也就是1000 个时钟节拍}
}(1)、要使用计数型信号量首先肯定是要先创建调用函数xSemaphoreCreateCounting()创 建一个计数型信号量CountSemaphore。计数型信号量计数最大值设置为255由于本实验中计 数型信号量是用于事件计数的所以计数型信号量的初始值设置为0。如果计数型信号量用于 资源管理的话那么事件计数型信号量的初始值就应该根据资源的实际数量来设置了。 (2) 、如果KEY_UP 键按下的话就表示事件发生了事件发生以后就调用函数 xSemaphoreGive()释放信号量CountSemaphore。 (3)、调用函数uxSemaphoreGetCount()获取信号量CountSemaphore 的信号量值释放信号 量的话信号量值就会加一。函数uxSemaphoreGetCount()是用来获取信号量值的这个函数是个 宏是对函数uxQueueMessagesWaiting()的一个简单封装其实就是返回队列结构体成员变量 uxMessagesWaiting 的值。 (4)、在LCD 上显示信号量CountSemaphore 的信号量值可以直观的观察信号量的变化过 程。 (5)、调用函数xSemaphoreTake()获取信号量CountSemaphore。 (6)、同样的在获取信号量以后调用函数uxSemaphoreGetCount()获取信号量值并且在LCD 上显示出来因为获取信号量成功以后信号量值会减一。
14.5.2 程序运行结果分析
编译并下载实验代码到开发板中默认情况下LCD 显示如图14.5.2.1 所示。 按下KEY_UP 键释放信号量CountSemaphore注意观察LCD 上的信号量值的变化。信号 量有效以后任务SemapTake_task()就可以获取到信号量获取到信号量的话就会刷新LCD 指定 区域的背景色。释放信号量的话信号量值就会增加获取信号量的话信号量值就会减少如图 14.5.2.2 所示 当信号量值减为0 的时候就表示信号量无效任务SemapTake_task()获取信号量失败任 务因此进入阻塞态LCD 指定区域背景色刷新停止看起来任务就好像是“停止”运行了。
14.6 优先级翻转
在使用二值信号量的时候会遇到很常见的一个问题——优先级翻转优先级翻转在可剥夺 内核中是非常常见的在实时系统中不允许出现这种现象这样会破坏任务的预期顺序可能 会导致严重的后果图14.6.1 就是一个优先级翻转的例子。 (1) 任务H 和任务M 处于挂起状态等待某一事件的发生任务L 正在运行。 (2) 某一时刻任务L 想要访问共享资源在此之前它必须先获得对应该资源的信号量。 (3) 任务L 获得信号量并开始使用该共享资源。 (4) 由于任务H 优先级高它等待的事件发生后便剥夺了任务L 的CPU 使用权。 (5) 任务H 开始运行。 (6) 任务H 运行过程中也要使用任务L 正在使用着的资源由于该资源的信号量还被任务 L 占用着任务H 只能进入挂起状态等待任务L 释放该信号量。 (7) 任务L 继续运行。 (8) 由于任务M 的优先级高于任务L当任务M 等待的事件发生后任务M 剥夺了任务 L 的CPU 使用权。 (9) 任务M 处理该处理的事。 (10) 任务M 执行完毕后将CPU 使用权归还给任务L。 (11) 任务L 继续运行。 (12) 最终任务L 完成所有的工作并释放了信号量到此为止由于实时内核知道有个高 优先级的任务在等待这个信号量故内核做任务切换。 (13) 任务H 得到该信号量并接着运行。 在这种情况下任务H 的优先级实际上降到了任务L 的优先级水平。因为任务H 要一直 等待直到任务L 释放其占用的那个共享资源。由于任务M 剥夺了任务L 的CPU 使用权使 得任务H 的情况更加恶化这样就相当于任务M 的优先级高于任务H导致优先级翻转。
14.7 优先级翻转实验
14.7.1 实验程序设计
1、实验目的 在使用二值信号量的时候会存在优先级翻转的问题本实验通过模拟的方式实现优先级翻 转观察优先级翻转对抢占式内核的影响。 2、实验设计 本实验设计四个任务start_task、high_task 、middle_task low_task这四个任务的任务 功能如下 start_task用来创建其他3 个任务。 high_task 高优先级任务会获取二值信号量获取成功以后会进行相应的处理处理完 成以后就会释放二值信号量。 middle_task 中等优先级的任务一个简单的应用任务。 low_task低优先级任务和高优先级任务一样会获取二值信号量获取成功以后会进行 相应的处理不过不同之处在于低优先级的任务占用二值信号量的时间要久一点(软件模拟占 用)。 实验中创建了一个二值信号量BinarySemaphore高优先级和低优先级这两个任务会使用这 个二值信号量。 3、实验工程 FreeRTOS 实验14-3 FreeRTOS 优先级翻转实验。 4、实验程序与分析 ●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define LOW_TASK_PRIO 2 //任务优先级
#define LOW_STK_SIZE 256 //任务堆栈大小
TaskHandle_t LowTask_Handler; //任务句柄
void low_task(void *pvParameters); //任务函数
#define MIDDLE_TASK_PRIO 3 //任务优先级
#define MIDDLE_STK_SIZE 256 //任务堆栈大小
TaskHandle_t MiddleTask_Handler; //任务句柄
void middle_task(void *pvParameters); //任务函数
#define HIGH_TASK_PRIO 4 //任务优先级
#define HIGH_STK_SIZE 256 //任务堆栈大小
TaskHandle_t HighTask_Handler; //任务句柄
void high_task(void *pvParameters); //任务函数
//二值信号量句柄
SemaphoreHandle_t BinarySemaphore; //二值信号量
//LCD 刷屏时使用的颜色
int lcd_discolor[14]{ WHITE, BLACK, BLUE, BRED,GRED, GBLUE, RED, MAGENTA,GREEN, CYAN, YELLOW, BROWN,BRRED, GRAY };●main()函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
BEEP_Init(); //初始化蜂鸣器
LCD_Init(); //初始化LCD
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR RED;
LCD_ShowString(30,10,200,16,16,ATK STM32F103/407);
LCD_ShowString(30,30,200,16,16,FreeRTOS Examp 14-3);
LCD_ShowString(30,50,200,16,16,Priority Overturn);
LCD_ShowString(30,70,200,16,16,ATOMALIENTEK);
LCD_ShowString(30,90,200,16,16,2016/11/25);
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )start_task, //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建二值信号量BinarySemaphorexSemaphoreCreateBinary(); (1)//二值信号量创建成功以后要先释放一下if(BinarySemaphore!NULL)xSemaphoreGive(BinarySemaphore); (2)//创建高优先级任务xTaskCreate((TaskFunction_t )high_task,(const char* )high_task,(uint16_t )HIGH_STK_SIZE,(void* )NULL,(UBaseType_t )HIGH_TASK_PRIO,(TaskHandle_t* )HighTask_Handler);//创建中等优先级任务xTaskCreate((TaskFunction_t )middle_task,(const char* )middle_task,(uint16_t )MIDDLE_STK_SIZE,(void* )NULL,(UBaseType_t )MIDDLE_TASK_PRIO,(TaskHandle_t* )MiddleTask_Handler);//创建低优先级任务xTaskCreate((TaskFunction_t )low_task,(const char* )low_task,(uint16_t )LOW_STK_SIZE,(void* )NULL,(UBaseType_t )LOW_TASK_PRIO,(TaskHandle_t* )LowTask_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//高优先级任务的任务函数
void high_task(void *pvParameters)
{u8 num;POINT_COLOR BLACK;LCD_DrawRectangle(5,110,115,314); //画一个矩形LCD_DrawLine(5,130,115,130); //画线POINT_COLOR BLUE;LCD_ShowString(6,111,110,16,16,High Task);while(1){vTaskDelay(500); //延时500ms也就是500 个时钟节拍num;printf(high task Pend Sem\r\n);xSemaphoreTake(BinarySemaphore,portMAX_DELAY); //获取二值信号量(3)printf(high task Running!\r\n);LCD_Fill(6,131,114,313,lcd_discolor[num%14]); //填充区域LED1!LED1;xSemaphoreGive(BinarySemaphore); //释放信号量(4)vTaskDelay(500); //延时500ms也就是500 个时钟节拍}
}
//中等优先级任务的任务函数
void middle_task(void *pvParameters)
{u8 num;POINT_COLOR BLACK;LCD_DrawRectangle(125,110,234,314); //画一个矩形LCD_DrawLine(125,130,234,130); //画线POINT_COLOR BLUE;LCD_ShowString(126,111,110,16,16,Middle Task);while(1){num;printf(middle task Running!\r\n);LCD_Fill(126,131,233,313,lcd_discolor[13-num%14]); //填充区域LED0!LED0;vTaskDelay(1000); //延时1s也就是1000 个时钟节拍}
}
//低优先级任务的任务函数
void low_task(void *pvParameters)
{static u32 times;while(1){xSemaphoreTake(BinarySemaphore,portMAX_DELAY); //获取二值信号量(5)printf(low task Running!\r\n);for(times0;times20000000;times) //模拟低优先级任务占用二值信号量(6){taskYIELD(); //发起任务调度}xSemaphoreGive(BinarySemaphore); //释放二值信号量(7)vTaskDelay(1000); //延时1s也就是1000 个时钟节拍}
}(1)、调用函数xSemaphoreCreateBinary()创建二值信号量。 (2)、默认创建的二值信号量是无效的这里需要先调用函数xSemaphoreGive()释放一次二 值信号量。否则任务high_task()和low_task()都会获取不到信号量。 (3)、高优先级任务调用函数xSemaphoreTake()获取二值信号量。 (4)、使用完以后需要调用函数xSemaphoreGive()释放二值信号量。 (5)、低优先级任务获取二值信号量BinarySemaphore。 (6)、低优先级任务模拟长时间占用二值信号量。 (7)、低优先级任务释放二值信号量。
14.7.2 程序运行结果分析
编译并下载实验代码到开发板中打开串口调试助手默认情况下LCD 显示如图14.7.2.1 所示。
图14.7.2.1 LCD 默认画面 从LCD 上不容易看出优先级翻转的现象我们可以通过串口很方便的观察优先级翻转 串口输出如图14.7.2.2 所示。 为了方便分析我们将串口输出复制出来如下
LCD ID:5510
middle task Running!
low task Running! (1)
high task Pend Sem (2)
middle task Running! (3)
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
middle task Running!
high task Running! (4)
middle task Running!
high task Pend Sem(1)、low_task 任务获取到二值信号量BinarySemaphore 开始运行。 (2)、high_task 获取信号量BinarySemaphore但是此时信号量BinarySemaphore 被任务 low_task 占用着因此high_task 就要一直等待直到low_task 任务释放信号量BinarySemaphore。 (3)、由于high_task 没有获取到信号量BinarySemaphore只能一直等待红色部分代码中 high_task 没有运行而middle_task 一直在运行给人的感觉就是middle_task 的任务优先级高 于high_task。但是事实上high_task 任务的任务优先级是高于middle_task 的这个就是优先级 反转 (4)、high_task 任务因为获取到了信号量BinarySemaphore 而运行 从本例程中可以看出当一个低优先级任务和一个高优先级任务同时使用同一个信号量 而系统中还有其他中等优先级任务时。如果低优先级任务获得了信号量那么高优先级的任务 就会处于等待状态但是中等优先级的任务可以打断低优先级任务而先于高优先级任务运行 (此时高优先级的任务在等待信号量所以不能运行)这是就出现了优先级翻转的现象。 既然优先级翻转是个很严重的问题那么有没有解决方法呢有这就要引出另外一种信 号量——互斥信号量
14.8 互斥信号量
14.8.1 互斥信号量简介
互斥信号量其实就是一个拥有优先级继承的二值信号量在同步的应用中(任务与任务或中 断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在 互斥访问中互斥信号量相当于一个钥匙当任务想要使用资源的时候就必须先获得这个钥匙 当使用完资源以后就必须归还这个钥匙这样其他的任务就可以拿着这个钥匙去使用资源。 互斥信号量使用和二值信号量相同的API 操作函数所以互斥信号量也可以设置阻塞时间 不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优 先级的任务使用而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不 过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级这个过程就是 优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间并且将已经出现的 “优先级翻转”的影响降到最低。 优先级继承并不能完全的消除优先级翻转它只是尽可能的降低优先级翻转带来的影响。 硬实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函数中 原因如下 ●互斥信号量有优先级继承的机制所以只能用在任务中不能用于中断服务函数。 ●中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
14.8.2 创建互斥信号量
FreeRTOS 提供了两个互斥信号量创建函数如表14.8.2.1 所示 1、函数xSemaphoreCreateMutex() 此函数用于创建一个互斥信号量所需要的内存通过动态内存管理方法分配。此函数本质是一个宏真正完成信号量创建的是函数xQueueCreateMutex()此函数原型如下 SemaphoreHandle_t xSemaphoreCreateMutex( void ) 参数 无。 返回值 NULL: 互斥信号量创建失败。 其他值: 创建成功的互斥信号量的句柄。 2、函数xSemaphoreCreateMutexStatic() 此函数也是创建互斥信号量的只不过使用此函数创建互斥信号量的话信号量所需要的 RAM 需要由用户来分配此函数是个宏具体创建过程是通过函数xQueueCreateMutexStatic () 来完成的函数原型如下
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer )参数 pxMutexBuffer此参数指向一个StaticSemaphore_t 类型的变量用来保存信号量结构体。 返回值 NULL: 互斥信号量创建失败。 其他值: 创建成功的互斥信号量的句柄。 14.8.3 互斥信号量创建过程分析 这里只分析动态创建互斥信号量函数xSemaphoreCreateMutex ()此函数是个宏定义如下 #define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX ) 可以看出真正干事的是函数xQueueCreateMutex()此函数在文件queue.c 中有如下定义
QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )
{Queue_t *pxNewQueue;const UBaseType_t uxMutexLength ( UBaseType_t ) 1, uxMutexSize ( UBaseType_t ) 0;pxNewQueue ( Queue_t * ) xQueueGenericCreate( uxMutexLength, uxMutexSize,\ (1)ucQueueType );prvInitialiseMutex( pxNewQueue ); (2)return pxNewQueue;
}(1)、调用函数xQueueGenericCreate()创建一个队列队列长度为1队列项长度为0队列 类型为参数ucQueueType。由于本函数是创建互斥信号量的所以参数ucQueueType 为 queueQUEUE_TYPE_MUTEX。
(2)、调用函数prvInitialiseMutex()初始化互斥信号量。 函数prvInitialiseMutex()代码如下
static void prvInitialiseMutex( Queue_t *pxNewQueue )
{if( pxNewQueue ! NULL ){//虽然创建队列的时候会初始化队列结构体的成员变量但是此时创建的是互斥//信号量因此有些成员变量需要重新赋值尤其是那些用于优先级继承的。pxNewQueue-pxMutexHolder NULL; (1)pxNewQueue-uxQueueType queueQUEUE_IS_MUTEX; (2)//如果是递归互斥信号量的话。pxNewQueue-u.uxRecursiveCallCount 0; (3)traceCREATE_MUTEX( pxNewQueue );//释放互斥信号量( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U,\queueSEND_TO_BACK );}else{traceCREATE_MUTEX_FAILED();}
}(1)和(2)、这里大家可能会疑惑队列结构体Queue_t 中没有pxMutexHolder 和uxQueueType 这两个成员变量吖这两个东西是哪里来的妖孽这两个其实是宏专门为互斥信号量准备的 在文件queue.c 中有如下定义
#define pxMutexHolder pcTail
#define uxQueueType pcHead
#define queueQUEUE_IS_MUTEX NULL当Queue_t 用于表示队列的时候pcHead 和pcTail 指向队列的存储区域当Queue_t 用于表 示互斥信号量的时候就不需要pcHead 和pcTail 了。当用于互斥信号量的时候将pcHead 指向 NULL 来表示pcTail 保存着互斥队列的所有者pxMutexHolder 指向拥有互斥信号量的那个任 务的任务控制块。重命名pcTail 和pcHead 就是为了增强代码的可读性。 (3)、如果创建的互斥信号量是互斥信号量的话还需要初始化队列结构体中的成员变量 u.uxRecursiveCallCount。 互斥信号量创建成功以后会调用函数xQueueGenericSend()释放一次信号量说明互斥信号 量默认就是有效的互斥信号量创建完成以后如图14.8.3.1 所示
14.8.4 释放互斥信号量
释放互斥信号量的时候和二值信号量、计数型信号量一样都是用的函数 xSemaphoreGive()(实际上完成信号量释放的是函数xQueueGenericSend())。不过由于互斥信号量 涉及到优先级继承的问题所以具体处理过程会有点区别。使用函数xSemaphoreGive()释放信 号量最重要的一步就是将uxMessagesWaiting 加一而这一步就是通过函数 prvCopyDataToQueue() 来完成的释放信号量的函数xQueueGenericSend() 会调用 prvCopyDataToQueue()。互斥信号量的优先级继承也是在函数prvCopyDataToQueue()中完成的 此函数中有如下一段代码
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue,const void * pvItemToQueue,const BaseType_t xPosition )
{BaseType_t xReturn pdFALSE;UBaseType_t uxMessagesWaiting;uxMessagesWaiting pxQueue-uxMessagesWaiting;if( pxQueue-uxItemSize ( UBaseType_t ) 0 ){
#if ( configUSE_MUTEXES 1 ) //互斥信号量{if( pxQueue-uxQueueType queueQUEUE_IS_MUTEX ) (1){xReturn xTaskPriorityDisinherit( ( void * ) pxQueue-pxMutexHolder );(2)pxQueue-pxMutexHolder NULL; (3)}else{mtCOVERAGE_TEST_MARKER();}}
#endif /* configUSE_MUTEXES */}/*********************************************************************//*************************省略掉其他处理代码**************************//*********************************************************************/pxQueue-uxMessagesWaiting uxMessagesWaiting 1;return xReturn;
}(1)、当前操作的是互斥信号量。 (2)、调用函数xTaskPriorityDisinherit()处理互斥信号量的优先级继承问题。 (3)、互斥信号量释放以后互斥信号量就不属于任何任务了所以pxMutexHolder 要指向 NULL。 在来看一下函数xTaskPriorityDisinherit() 是怎么具体的处理优先级继承的函数 xTaskPriorityDisinherit()代码如下
BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder )
{TCB_t * const pxTCB ( TCB_t * ) pxMutexHolder;BaseType_t xReturn pdFALSE;if( pxMutexHolder ! NULL ) (1){//当一个任务获取到互斥信号量以后就会涉及到优先级继承的问题正在释放互斥//信号量的任务肯定是当前正在运行的任务pxCurrentTCB。configASSERT( pxTCB pxCurrentTCB );configASSERT( pxTCB-uxMutexesHeld );( pxTCB-uxMutexesHeld )--; (2)//是否存在优先级继承如果存在的话任务当前优先级肯定和任务基优先级不同。if( pxTCB-uxPriority ! pxTCB-uxBasePriority ) (3){//当前任务只获取到了一个互斥信号量if( pxTCB-uxMutexesHeld ( UBaseType_t ) 0 ) (4){if( uxListRemove( ( pxTCB-xStateListItem ) ) ( UBaseType_t ) 0 ) (5){taskRESET_READY_PRIORITY( pxTCB-uxPriority ); (6)}else{mtCOVERAGE_TEST_MARKER();}//使用新的优先级将任务重新添加到就绪列表中traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB-uxBasePriority );pxTCB-uxPriority pxTCB-uxBasePriority; (7)/* Reset the event list item value. It cannot be in use forany other purpose if this task is running, and it must berunning to give back the mutex. */listSET_LIST_ITEM_VALUE( ( pxTCB-xEventListItem ), \ (8)( TickType_t ) configMAX_PRIORITIES - \( TickType_t ) pxTCB-uxPriority );prvAddTaskToReadyList( pxTCB ); (9)xReturn pdTRUE; (10)}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}return xReturn;
}(1)、函数的参数pxMutexHolder 表示拥有此互斥信号量任务控制块所以要先判断此互斥 信号量是否已经被其他任务获取。 (2)、有的任务可能会获取多个互斥信号量所以就需要标记任务当前获取到的互斥信号量 个数任务控制块结构体的成员变量uxMutexesHeld 用来保存当前任务获取到的互斥信号量个 数。任务每释放一次互斥信号量变量uxMutexesHeld 肯定就要减一。 (3)、判断是否存在优先级继承如果存在的话任务的当前优先级肯定不等于任务的基优先 级。 (4)、判断当前释放的是不是任务所获取到的最后一个互斥信号量因为如果任务还获取了 其他互斥信号量的话就不能处理优先级继承。优先级继承的处理必须是在释放最后一个互斥信 号量的时候。 (5)、优先级继承的处理说白了就是将任务的当前优先级降低到任务的基优先级所以要把 当前任务先从任务就绪表中移除。当任务优先级恢复为原来的优先级以后再重新加入到就绪表 中。 (6)、如果任务继承来的这个优先级对应的就绪表中没有其他任务的话就将取消这个优先级 的就绪态。 (7)、重新设置任务的优先级为任务的基优先级uxBasePriority。 (8)、复位任务的事件列表项。 (9)、将优先级恢复后的任务重新添加到任务就绪表中。 (10)、返回pdTRUE表示需要进行任务调度。
14.8.5 获取互斥信号量
获取互斥信号量的函数同获取二值信号量和计数型信号量的函数相同都是 xSemaphoreTake()(实际执行信号量获取的函数是xQueueGenericReceive())获取互斥信号量的 过程也需要处理优先级继承的问题函数xQueueGenericReceive()在文件queue.c 中有定义在 第十三章讲解队列的时候我们没有分析这个函数本节就来简单的分析一下这个函数缩减后 的函数代码如下
BaseType_t xQueueGenericReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait, const BaseType_t xJustPeeking )
{BaseType_t xEntryTimeSet pdFALSE;TimeOut_t xTimeOut;int8_t *pcOriginalReadPosition;Queue_t * const pxQueue ( Queue_t * ) xQueue;for( ;; ){taskENTER_CRITICAL();{const UBaseType_t uxMessagesWaiting pxQueue-uxMessagesWaiting;//判断队列是否有消息if( uxMessagesWaiting ( UBaseType_t ) 0 ) (1){pcOriginalReadPosition pxQueue-u.pcReadFrom;prvCopyDataFromQueue( pxQueue, pvBuffer ); (2)if( xJustPeeking pdFALSE ) (3){traceQUEUE_RECEIVE( pxQueue );//移除消息pxQueue-uxMessagesWaiting uxMessagesWaiting - 1; (4)
#if ( configUSE_MUTEXES 1 ) (5){if( pxQueue-uxQueueType queueQUEUE_IS_MUTEX ){pxQueue-pxMutexHolder (6)( int8_t * ) pvTaskIncrementMutexHeldCount();}else{mtCOVERAGE_TEST_MARKER();}}
#endif /* configUSE_MUTEXES *///查看是否有任务因为入队而阻塞如果有的话就需要解除阻塞态。if( listLIST_IS_EMPTY( ( pxQueue-xTasksWaitingToSend ) ) (7)pdFALSE ){if( xTaskRemoveFromEventList( ( pxQueue-xTasksWaitingToSend ) ) ! pdFALSE ){//如果解除阻塞的任务优先级比当前任务优先级高的话就需要//进行一次任务切换queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}else (8){traceQUEUE_PEEK( pxQueue );//读取队列中的消息以后需要删除消息pxQueue-u.pcReadFrom pcOriginalReadPosition;//如果有任务因为出队而阻塞的话就解除任务的阻塞态。if( listLIST_IS_EMPTY( ( pxQueue-xTasksWaitingToReceive ) ) (9)pdFALSE ){if( xTaskRemoveFromEventList( ( pxQueue-xTasksWaitingToReceive ) ) ! pdFALSE ){//如果解除阻塞的任务优先级比当前任务优先级高的话就需要//进行一次任务切换queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else{mtCOVERAGE_TEST_MARKER();}}taskEXIT_CRITICAL();return pdPASS;}else //队列为空(10){if( xTicksToWait ( TickType_t ) 0 ){//队列为空如果阻塞时间为0 的话就直接返回errQUEUE_EMPTYtaskEXIT_CRITICAL();traceQUEUE_RECEIVE_FAILED( pxQueue );return errQUEUE_EMPTY;}else if( xEntryTimeSet pdFALSE ){//队列为空并且设置了阻塞时间需要初始化时间状态结构体。vTaskSetTimeOutState( xTimeOut );xEntryTimeSet pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}}taskEXIT_CRITICAL();vTaskSuspendAll();prvLockQueue( pxQueue );//更新时间状态结构体并且检查超时是否发生if( xTaskCheckForTimeOut( xTimeOut, xTicksToWait ) pdFALSE ) (11){if( prvIsQueueEmpty( pxQueue ) ! pdFALSE ) (12){traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );
#if ( configUSE_MUTEXES 1 ){if( pxQueue-uxQueueType queueQUEUE_IS_MUTEX ) (13){taskENTER_CRITICAL();{vTaskPriorityInherit( ( void * ) pxQueue-pxMutexHolder );(14)}taskEXIT_CRITICAL();}else{mtCOVERAGE_TEST_MARKER();}}
#endifvTaskPlaceOnEventList( ( pxQueue-xTasksWaitingToReceive ), (15)xTicksToWait );prvUnlockQueue( pxQueue );if( xTaskResumeAll() pdFALSE ){portYIELD_WITHIN_API();}else{mtCOVERAGE_TEST_MARKER();}}else{//重试一次prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else{prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();if( prvIsQueueEmpty( pxQueue ) ! pdFALSE ){traceQUEUE_RECEIVE_FAILED( pxQueue );return errQUEUE_EMPTY;}else{mtCOVERAGE_TEST_MARKER();}}}
}(1)、队列不为空可以从队列中提取数据。 (2)、调用函数prvCopyDataFromQueue()使用数据拷贝的方式从队列中提取数据。 (3)、数据读取以后需要将数据删除掉。 (4)、队列的消息数量计数器uxMessagesWaiting 减一通过这一步就将数据删除掉了。 (5)、表示此函数是用于获取互斥信号量的。 (6)、获取互斥信号量成功需要标记互斥信号量的所有者也就是给pxMutexHolder 赋值 pxMutexHolder 应该是当前任务的任务控制块。但是这里是通过函数 pvTaskIncrementMutexHeldCount()来赋值的此函数很简单只是将任务控制块中的成员变量 uxMutexesHeld 加一表示任务获取到了一个互斥信号量最后此函数返回当前任务的任务控 制块。 (7)、出队成功以后判断是否有任务因为入队而阻塞的如果有的话就需要解除任务的阻塞 态如果解除阻塞的任务优先级比当前任务的优先级高还需要进行一次任务切换。 (8)、出队的时候不需要删除消息。 (9)、如果出队的时候不需要删除消息的话那就相当于刚刚出队的那条消息接着有效既然 还有有效的消息存在队列中那么就判断是否有任务因为出队而阻塞如果有的话就解除任务 的阻塞态。同样的如果解除阻塞的任务优先级比当前任务的优先级高的话还需要进行一次任 务切换。 (10)、上面分析的都是队列不为空的时候那当队列为空的时候该如何处理呢处理过程和 队列的任务级通用入队函数xQueueGenericSend()类似。如果阻塞时间为0 的话就就直接返回 errQUEUE_EMPTY表示队列空如果设置了阻塞时间的话就进行相关的处理。 (11)、检查超时是否发生如果没有的话就需要将任务添加到队列的xTasksWaitingToReceive 列表中。 (12)、检查队列是否继续为空如果不为空的话就会在重试一次出队。 (13)、表示此函数是用于获取互斥信号量的。 (14)、调用函数vTaskPriorityInherit()处理互斥信号量中的优先级继承问题如果函数 xQueueGenericReceive()用于获取互斥信号量的话此函数执行到这里说明互斥信号量正在被其 他的任务占用。此函数和14.8.4 小节中的函数xTaskPriorityDisinherit()过程相反。此函数会判断 当前任务的任务优先级是否比正在拥有互斥信号量的那个任务的任务优先级高如果是的话就 会把拥有互斥信号量的那个低优先级任务的优先级调整为与当前任务相同的优先级 (15)、经过(12)步判断队列依旧为空那么就将任务添加到列表xTasksWaitingToReceive 中。 在上面的分析中红色部分就是当函数xQueueGenericReceive()用于互斥信号量的时候的处 理过程其中(13)和(14)条详细的分析了互斥信号量优先级继承的过程。我们举个例子来简单的 演示一下这个过程假设现在有两个任务HighTask 和LowTaskHighTask 的任务优先级为4 LowTask 的任务优先级为2。这两个任务都会操同一个互斥信号量MutexLowTask 先获取到互 斥信号量Mutex。此时任务HighTask 也要获取互斥信号量Mutex任务HighTask 调用函数 xSemaphoreTake()尝试获取互斥信号量Mutex发现此互斥信号量正在被任务LowTask 使用 并且LowTask 的任务优先级为2比自己的任务优先级小因为任务HighTask 就会将LowTask 的任务优先级调整为与自己相同的优先级即4然后任务HighTask 进入阻塞态等待互斥信号 量有效。
14.9 互斥信号量操作实验
14.9.1 实验程序设计
1、实验目的 学习使用互斥信号量并且观察互斥信号量是否可以解决或者缓解优先级翻转。 2、实验设计 本实验在“FreeRTOS 实验14-3 FreeRTOS 优先级翻转实验”的基础上完成只是将其中的 二值信号量更换为互斥信号量其他部分完全相同。 3、实验工程 FreeRTOS 实验14-4 FreeRTOS 互斥信号量操作实验。 4、实验程序与分析 本实验是在实验“FreeRTOS 实验14-3 FreeRTOS 优先级翻转实验”的基础上修改的除了 任务函数以外其他的部分都相同。 ●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建互斥信号量MutexSemaphorexSemaphoreCreateMutex(); (1)//创建高优先级任务xTaskCreate((TaskFunction_t )high_task,(const char* )high_task,(uint16_t )HIGH_STK_SIZE,(void* )NULL,(UBaseType_t )HIGH_TASK_PRIO,(TaskHandle_t* )HighTask_Handler);//创建中等优先级任务xTaskCreate((TaskFunction_t )middle_task,(const char* )middle_task,(uint16_t )MIDDLE_STK_SIZE,(void* )NULL,(UBaseType_t )MIDDLE_TASK_PRIO,(TaskHandle_t* )MiddleTask_Handler);//创建低优先级任务xTaskCreate((TaskFunction_t )low_task,(const char* )low_task,(uint16_t )LOW_STK_SIZE,(void* )NULL,(UBaseType_t )LOW_TASK_PRIO,(TaskHandle_t* )LowTask_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//高优先级任务的任务函数
void high_task(void *pvParameters)
{u8 num;POINT_COLOR BLACK;LCD_DrawRectangle(5,110,115,314); //画一个矩形LCD_DrawLine(5,130,115,130); //画线POINT_COLOR BLUE;LCD_ShowString(6,111,110,16,16,High Task);while(1){vTaskDelay(500); //延时500ms也就是500 个时钟节拍num;printf(high task Pend Sem\r\n);xSemaphoreTake(MutexSemaphore,portMAX_DELAY); //获取互斥信号量(2)printf(high task Running!\r\n);LCD_Fill(6,131,114,313,lcd_discolor[num%14]); //填充区域LED1!LED1;xSemaphoreGive(MutexSemaphore); //释放信号量(3)vTaskDelay(500); //延时500ms也就是500 个时钟节拍}
}
//中等优先级任务的任务函数
void middle_task(void *pvParameters)
{u8 num;POINT_COLOR BLACK;LCD_DrawRectangle(125,110,234,314); //画一个矩形LCD_DrawLine(125,130,234,130); //画线POINT_COLOR BLUE;LCD_ShowString(126,111,110,16,16,Middle Task);while(1){num;printf(middle task Running!\r\n);LCD_Fill(126,131,233,313,lcd_discolor[13-num%14]); //填充区域LED0!LED0;vTaskDelay(1000); //延时1s也就是1000 个时钟节拍}
}
//低优先级任务的任务函数
void low_task(void *pvParameters)
{static u32 times;while(1){xSemaphoreTake(MutexSemaphore,portMAX_DELAY); //获取互斥信号量(4)printf(low task Running!\r\n);for(times0;times20000000;times) //模拟低优先级任务占用互斥信号量(5){taskYIELD(); //发起任务调度}xSemaphoreGive(MutexSemaphore); //释放互斥信号量(6)vTaskDelay(1000); //延时1s也就是1000 个时钟节拍}
}(1)、调用函数xSemaphoreCreateMutex()创建互斥信号量MutexSemaphore。 (2)、任务high_task 获取互斥信号量。 (3)、互斥信号量使用完成以后一定要释放 (4)、任务low_task 获取互斥信号量MutexSemaphore阻塞时间为portMAX_DELAY。 (5)、模拟任务low_task 长时间占用互斥信号量portMAX_DELAY。 (6)、任务low_task 使用完互斥信号量释放
14.9.2 程序运行结果分析
编译并下载实验代码到开发板中打开串口调试助手串口调试助手如图14.9.2.1 所示
图14.9.2.1 串口调试助手 为了方便分析我们将串口调试助手中的数据复制下来如下
LCD ID:5510
middle task Running! (1)
low task Running! (2)
high task Pend Sem (3)
high task Running! (4)
middle task Running!
high task Pend Sem
high task Running!
middle task Running!
low task Running!(1)、middle_task 任务运行。 (2)、low_task 获得互斥信号量运行。 (3)、high_task 请求信号量在这里会等待一段时间等待low_task 任务释放互斥信号量。 但是middle_task 不会运行。因为由于low_task 正在使用互斥信号量所以low_task 任务优先 级暂时提升到了与high_task 相同的优先级这个优先级比任务middle_task 高所以middle_task 任务不能再打断low_task 任务的运行了 (4)、high_task 任务获得互斥信号量而运行。 从上面的分析可以看出互斥信号量有效的抑制了优先级翻转现象的发生。
14.10 递归互斥信号量
14.10.1 递归互斥信号量简介
递归互斥信号量可以看作是一个特殊的互斥信号量已经获取了互斥信号量的任务就不能 再次获取这个互斥信号量但是递归互斥信号量不同已经获取了递归互斥信号量的任务可以 再次获取这个递归互斥信号量而且次数不限一个任务使用函数xSemaphoreTakeRecursive() 成功的获取了多少次递归互斥信号量就得使用函数xSemaphoreGiveRecursive()释放多少次比 如某个任务成功的获取了5 次递归信号量那么这个任务也得同样的释放5 次递归信号量。 递归互斥信号量也有优先级继承的机制所以当任务使用完递归互斥信号量以后一定要记 得释放。同互斥信号量一样递归互斥信号量不能用在中断服务函数中。 ●由于优先级继承的存在就限定了递归互斥信号量只能用在任务中不能用在中断服务 函数中 ●中断服务函数不能设置阻塞时间。 要使用递归互斥信号量的话宏configUSE_RECURSIVE_MUTEXES 必须为1
14.10.2 创建互斥信号量
FreeRTOS 提供了两个互斥信号量创建函数如表14.10.2.1 所示 1、函数xSemaphoreCreateRecursiveMutex() 此函数用于创建一个递归互斥信号量所需要的内存通过动态内存管理方法分配。此函数 本质是一个宏真正完成信号量创建的是函数xQueueCreateMutex ()此函数原型如下
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void )参数 无。 返回值 NULL: 互斥信号量创建失败。 其他值: 创建成功的互斥信号量的句柄。 2、函数xSemaphoreCreateRecursiveMutexStatic() 此函数也是创建递归互斥信号量的只不过使用此函数创建递归互斥信号量的话信号量所 需要的RAM 需要由用户来分配此函数是个宏具体创建过程是通过函数 xQueueCreateMutexStatic ()来完成的函数原型如下
SemaphoreHandle_t xSemaphoreCreateRecursiveMutexStatic( StaticSemaphore_t *pxMutexBuffer )参数 pxMutexBuffer此参数指向一个StaticSemaphore_t 类型的变量用来保存信号量结构体。 返回值 NULL: 互斥信号量创建失败。 其他值: 创建成功的互斥信号量的句柄。
14.10.3 递归信号量创建过程分析
这里只分析动态创建互斥信号量函数xSemaphoreCreateRecursiveMutex ()此函数是个宏 定义如下
#define xSemaphoreCreateRecursiveMutex()
xQueueCreateMutex( queueQUEUE_TYPE_RECURSIVE_MUTEX )可以看出真正干事的是函数xQueueCreateMutex()互斥信号量的创建也是用的这个函数 只是在创建递归互斥信号量的时候类型选择为queueQUEUE_TYPE_RECURSIVE_MUTEX。具 体的创建过程参考14.8.3 小节。
14.10.4 释放递归互斥信号量
递归互斥信号量有专用的释放函数xSemaphoreGiveRecursive()此函数为宏如下 #define xSemaphoreGiveRecursive( xMutex ) xQueueGiveMutexRecursive( ( xMutex ) ) 函数的参数就是就是要释放的递归互斥信号量真正的释放是由函数 xQueueGiveMutexRecursive()来完成的此函数代码如下
BaseType_t xQueueGiveMutexRecursive( QueueHandle_t xMutex )
{BaseType_t xReturn;Queue_t * const pxMutex ( Queue_t * ) xMutex;configASSERT( pxMutex );//检查递归互斥信号量是不是被当前任务获取的要释放递归互斥信号量的任务肯定是当//前正在运行的任务。因为同互斥信号量一样递归互斥信号量的获取和释放要在同一个//任务中完成如果当前正在运行的任务不是递归互斥信号量的拥有者就不能释放if( pxMutex-pxMutexHolder ( void * ) xTaskGetCurrentTaskHandle() ) (1){traceGIVE_MUTEX_RECURSIVE( pxMutex );( pxMutex-u.uxRecursiveCallCount )--; (2)if( pxMutex-u.uxRecursiveCallCount ( UBaseType_t ) 0 ) (3){( void ) xQueueGenericSend( pxMutex, NULL, \ (4)queueMUTEX_GIVE_BLOCK_TIME, queueSEND_TO_BACK );}else{mtCOVERAGE_TEST_MARKER();}xReturn pdPASS; (5)}else{xReturn pdFAIL; (6)traceGIVE_MUTEX_RECURSIVE_FAILED( pxMutex );}return xReturn;
}(1)、哪个任务获取到的递归互斥信号量哪个任务就释放要释放递归互斥信号量的任务 肯定是当前正在运行的任务。检查这个任务是不是递归互斥信号量的拥有者如果不是的话就 不能完成释放。 (2)、uxRecursiveCallCount 减一uxRecursiveCallCount 用来记录递归信号量被获取的次数。 由于递归互斥信号量可以被一个任务多次获取因此在释放的时候也要多次释放但是只有在 最后一次释放的时候才会调用函数xQueueGenericSend()完成释放过程其他的时候只是简单的 将uxRecursiveCallCount 减一即可。 (3)、当uxRecursiveCallCount 为0 的时候说明是最后一次释放了。 (4)、如果是最后一次释放的话就调用函数xQueueGenericSend()完成真正的释放过程。阻塞 时间是queueMUTEX_GIVE_BLOCK_TIME宏queueMUTEX_GIVE_BLOCK_TIME 为0。 (5)、递归互斥信号量释放成功返回pdPASS。 (6)、递归互斥信号量释放未成功返回pdFAIL。 由于递归互斥信号量可以被一个任务重复的获取因此在释放的时候也要释放多次但是 只有在最后一次释放的时候才会调用函数xQueueGenericSend()完成真正的释放。其他释放的话 只是简单的将uxRecursiveCallCount 减一。
14.10.5 获取递归互斥信号量
递归互斥信号量的获取使用函数xSemaphoreTakeRecursive()此函数是个宏定义如下
#define xSemaphoreTakeRecursive( xMutex, xBlockTime )
xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )函数第一个参数是要获取的递归互斥信号量句柄第二个参数是阻塞时间。真正的获取过 程是由函数xQueueTakeMutexRecursive()来完成的此函数如下
BaseType_t xQueueTakeMutexRecursive( QueueHandle_t xMutex, //要获取的信号量TickType_t xTicksToWait )//阻塞时间
{BaseType_t xReturn;Queue_t * const pxMutex ( Queue_t * ) xMutex;configASSERT( pxMutex );traceTAKE_MUTEX_RECURSIVE( pxMutex );if( pxMutex-pxMutexHolder ( void * ) xTaskGetCurrentTaskHandle() ) (1){( pxMutex-u.uxRecursiveCallCount ); (2)xReturn pdPASS;}else{xReturn xQueueGenericReceive( pxMutex, NULL, xTicksToWait, pdFALSE ); (3)if( xReturn ! pdFAIL ){( pxMutex-u.uxRecursiveCallCount ); (4)}else{raceTAKE_MUTEX_RECURSIVE_FAILED( pxMutex );}}return xReturn;
}
(1)、判断当前要获取递归互斥信号量的任务是不是已经是递归互斥信号量的拥有者。通过 这一步就可以判断出当前任务是第一次获取递归互斥信号量还是重复获取。 (2)、如果当前任务已经是递归互斥信号量的拥有者那就说明任务已经获取了递归互斥信 号量本次是重复获取递归互斥信号量那么就简单的将uxRecursiveCallCount 加一然后返回 pdPASS 表示获取成功。 (3)、如果任务是第一次获取递归互斥信号量的话就需要调用函数xQueueGenericReceive() 完成真正的获取过程。 (4)、第一次获取递归互斥信号量成功以后将uxRecursiveCallCount 加一。
14.10.6 递归互斥信号量使用示例
互斥信号量使用很简单就不做专门的实验了FreeRTOS 官方提供了一个简单的示例大 家可以参考一下示例如下
SemaphoreHandle_t RecursiveMutex; //递归互斥信号量句柄
//某个任务中创建一个递归互斥信号量
void vATask( void * pvParameters )
{//没有创建创建递归互斥信号量之前不要使用RecursiveMutex xSemaphoreCreateRecursiveMutex(); //创建递归互斥信号量for( ;; ){/************任务代码**************/}
}
//任务调用的使用递归互斥信号量的功能函数。
void vAFunction( void )
{/**********其他处理代码*****************/if( xMutex ! NULL ){//获取递归互斥信号量阻塞时间为10 个节拍if( xSemaphoreTakeRecursive( RecursiveMutex, 10 ) pdTRUE ){/***********其他处理过程*************///这里为了演示所以是顺序的获取递归互斥信号量但是在实际的代码中肯定//不是这么顺序的获取的真正的代码中是混合着其他程序调用的。xSemaphoreTakeRecursive( RecursiveMutex, ( TickType_t ) 10 );xSemaphoreTakeRecursive( RecursiveMutex, ( TickType_t ) 10 );//任务获取了三次递归互斥信号量所以就得释放三次xSemaphoreGiveRecursive( RecursiveMutex);xSemaphoreGiveRecursive( RecursiveMutex);xSemaphoreGiveRecursive( RecursiveMutex);//递归互斥信号量释放完成可以被其他任务获取了}else{/**********递归互斥信号量获取失败***********/}}
}第十五章FreeRTOS 软件定时器
定时器可以说是每个MCU 都有的外设有的MCU 其定时器功能异常强大比如提供PWM、 输入捕获等功能。但是最常用的还是定时器最基础的功能——定时通过定时器来完成需要周 期性处理的事务。MCU 自带的定时器属于硬件定时器不同的MCU 其硬件定时器数量不同 因为要考虑成本的问题。FreeRTOS 也提供了定时器功能不过是软件定时器软件定时器的精 度肯定没有硬件定时器那么高但是对于普通的精度要求不高的周期性处理的任务来说够了。 当MCU 的硬件定时器不够的时候就可以考虑使用FreeRTOS 的软件定时器本章就来学习一 下FreeRTOS 的软件定时器本章分为如下几部分 15.1 软件定时器简介 15.2 定时器服务/Daemon 任务 15.3 单次定时器和周期定时器 15.4 复位软件定时器 15.5 创建软件定时器 15.6 开启软件定时器 15.7 停止软件定时器 15.8 软件定时器实验
15.1 软件定时器简介
1、软件定时器概述 软件定时器允许设置一段时间当设置的时间到达之后就执行指定的功能函数被定时器 调用的这个功能函数叫做定时器的回调函数。回调函数的两次执行间隔叫做定时器的定时周期 简而言之当定时器的定时周期到了以后就会执行回调函数。 2、编写回调函数的注意事项 软件定时器的回调函数是在定时器服务任务中执行的所以一定不能在回调函数中调用任 何会阻塞任务的API 函数比如定时器回调函数中千万不能调用vTaskDelay() 、 vTaskDelayUnti()还有一些访问队列或者信号量的非零阻塞时间的API 函数也不能调用。
15.2 定时器服务/Daemon 任务
15.2.1 定时器服务任务与队列
定时器是一个可选的、不属于FreeRTOS 内核的功能它是由定时器服务(或Daemon)任务 来提供的。FreeRTOS 提供了很多定时器有关的API 函数这些API 函数大多都使用FreeRTOS 的队列发送命令给定时器服务任务。这个队列叫做定时器命令队列。定时器命令队列是提供给 FreeRTOS 的软件定时器使用的用户不能直接访问图15.2.1 描述了这个过程 图15.2.1 左侧部分属于用户应用程序的一部分并且会在某个用户创建的用户任务中调用。 图中右侧部分是定时器服务任务的任务函数定时器命令队列将用户应用任务和定时器服务任 务连接在一起。在这个例子中应用程序调用了函数xTimerReset()结果就是复位命令会被发 送到定时器命令队列中定时器服务任务会处理这个命令。应用程序是通过函数xTimerReset() 间接的向定时器命令队列发送了复位命令并不是直接调用类似xQueueSend()这样的队列操作 函数发送的。
15.2.2 定时器相关配置
上一小节我们知道了软件定时器有一个定时器服务任务和定时器命令队列这两个东西肯 定是要配置的配置方法和我们前面讲解的FreeRTOSCofig.h 一样而且相关的配置也是放到 文件FreeRTOSConfig.h 中的涉及到的配置如下 1、configUSE_TIMERS 如果要使用软件定时器的话宏configUSE_TIMERS 一定要设置为1当设置为1 的话定时 器服务任务就会在启动FreeRTOS 调度器的时候自动创建。 2、configTIMER_TASK_PRIORITY 设置软件定时器服务任务的任务优先级可以为0~( configMAX_PRIORITIES-1)。优先级 一定要根据实际的应用要求来设置。如果定时器服务任务的优先级设置的高的话定时器命令 队列中的命令和定时器回调函数就会及时的得到处理。 3、configTIMER_QUEUE_LENGTH 此宏用来设置定时器命令队列的队列长度。 4、configTIMER_TASK_STACK_DEPTH 此宏用来设置定时器服务任务的任务堆栈大小单位为字不是字节对于STM32 来说 一个字是4 字节。由于定时器服务任务中会执行定时器的回调函数因此任务堆栈的大小一定 要根据定时器的回调函数来设置。
15.3 单次定时器和周期定时器
软件定时器分两种单次定时器和周期定时器单次定时器的话定时器回调函数就执行一 次比如定时1s当定时时间到了以后就会执行一次回调函数然后定时器就会停止运行。对 于单次定时器我们可以再次手动重新启动(调用相应的API 函数即可)但是单次定时器不能自 动重启。相反的周期定时器一旦启动以后就会在执行完回调函数以后自动的重新启动这样 回调函数就会周期性的执行。图15.3.1 描述了单次定时器和周期定时器的不同 图中Timer1 为单次定时器定时器周期为100Timer2 为周期定时器定时器周期为200。
15.4 复位软件定时器
有时候我们可能会在定时器正在运行的时候需要复位软件定时器复位软件定时器的话会重新计算定时周期到达的时间点这个新的时间点是相对于复位定时器的那个时刻计算的并 不是第一次启动软件定时器的那个时间点。图15.4.1 演示了这个过程Timer1 是单次定时器 定时周期是5s 在图15.4.1 中我们展示了定时器复位过程这是一个通过按键打开LCD 背光的例子我们 假定当唤醒键被按下的时候应用程序打开LCD 背光当LCD 背光点亮以后如果5s 之内唤醒键 没有再次按下就自动熄灭。如果在这5s 之内唤醒键被按下了LCD 背光就从按下的这个时刻 起再亮5s。 FreeRTOS 提供了两个API 函数来完成软件定时器的复位如表15.4.1 所示 1、函数xTimerReset() 复位一个软件定时器此函数只能用在任务中不能用于中断服务函数此函数是一个宏 真正执行的是函数xTimerGenericCommand()函数原型如下
BaseType_t xTimerReset( TimerHandle_t xTimer,
TickType_t xTicksToWait )参数 xTimer要复位的软件定时器的句柄。 xTicksToWait设置阻塞时间调用函数xTimerReset ()开启软件定时器其实就是向定时器命 令队列发送一条tmrCOMMAND_RESET 命令既然是向队列发送消息那 肯定会涉及到入队阻塞时间的设置。 返回值 pdPASS: 软件定时器复位成功其实就是命令发送成功。 pdFAIL: 软件定时器复位失败命令发送失败。 2、函数xTimerResetFromISR() 此函数是xTimerReset()的中断版本此函数用于中断服务函数中此函数是一个宏真正 执行的是函数xTimerGenericCommand()函数原型如下
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
BaseType_t * pxHigherPriorityTaskWoken );参数 xTimer要复位的软件定时器的句柄。 pxHigherPriorityTaskWoken记退出此函数以后是否进行任务切换这个变量的值函数会自 动设置的用户不用进行设置用户只需要提供一个变量来保 存这个值就行了。当此值为pdTRUE 的时候在退出中断服务函 数之前一定要进行一次任务切换。 返回值 pdPASS: 软件定时器复位成功其实就是命令发送成功。 pdFAIL: 软件定时器复位失败命令发送失败。
15.5 创建软件定时器
使用软件定时器之前要先创建软件定时器软件定时器创建函数如表15.5.1 所示 1、函数xTiemrCreate() 此函数用于创建一个软件定时器所需要的内存通过动态内存管理方法分配。新创建的软 件定时器处于休眠状态也就是未运行的。函数xTimerStart() 、xTimerReset() 、xTimerStartFromISR() 、xTimerResetFromISR() 、xTimerChangePeriod() 和 xTimerChangePeriodFromISR()可以使新创建的定时器进入活动状态此函数的原型如下
TimerHandle_t xTimerCreate( const char * const pcTimerName,TickType_t xTimerPeriodInTicks,UBaseType_t uxAutoReload,void * pvTimerID,TimerCallbackFunction_t pxCallbackFunction )参数 pcTimerName软件定时器名字名字是一串字符串用于调试使用。 xTimerPeriodInTicks 软件定时器的定时器周期单位是时钟节拍数。可以借助portTICK_PERIOD_MS 将ms 单位转换为时钟节拍数。举个例子定 时器的周期为100 个时钟节拍的话那么xTimerPeriodInTicks 就为 100当定时器周期为500ms 的时候xTimerPeriodInTicks 就可以设置 为(500/ portTICK_PERIOD_MS)。 uxAutoReload设置定时器模式单次定时器还是周期定时器当此参数为pdTRUE 的时候表示创建的是周期定时器。如果为pdFALSE 的话表示创建的 是单次定时器。 pvTimerID定时器ID 号一般情况下每个定时器都有一个回调函数当定时器定 时周期到了以后就会执行这个回调函数。但是FreeRTOS 也支持多个 定时器共用同一个回调函数在回调函数中根据定时器的ID 号来处 理不同的定时器。 pxCallbackFunction定时器回调函数当定时器定时周期到了以后就会调用这个函数。 返回值 NULL: 软件定时器创建失败。 其他值: 创建成功的软件定时器句柄。 2、函数xTimerCreateStatic() 此函数用于创建一个软件定时器所需要的内存需要用户自行分配。新创建的软件定时器 处于休眠状态也就是未运行的。函数xTimerStart()、xTimerReset()、xTimerStartFromISR()、 xTimerResetFromISR()、xTimerChangePeriod()和xTimerChangePeriodFromISR()可以使新创建的 定时器进入活动状态此函数的原型如下
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,TickType_t xTimerPeriodInTicks,UBaseType_t uxAutoReload,void * pvTimerID,TimerCallbackFunction_t pxCallbackFunction,StaticTimer_t * pxTimerBuffer )参数 pcTimerName软件定时器名字名字是一串字符串用于调试使用。 xTimerPeriodInTicks 软件定时器的定时器周期单位是时钟节拍数。可以借助 portTICK_PERIOD_MS 将ms 单位转换为时钟节拍数。举个例子定 时器的周期为100 个时钟节拍的话那么xTimerPeriodInTicks 就为 100当定时器周期为500ms 的时候xTimerPeriodInTicks 就可以设置 为(500/ portTICK_PERIOD_MS)。 uxAutoReload设置定时器模式单次定时器还是周期定时器当此参数为pdTRUE 的时候表示创建的是周期定时器。如果为pdFALSE 的话表示创建的 是单次定时器。 pvTimerID定时器ID 号一般情况下每个定时器都有一个回调函数当定时器定 时周期到了以后就会执行这个回调函数。当时FreeRTOS 也支持多个 定时器共用同一个回调函数在回调函数中根据定时器的ID 号来处 理不同的定时器。 pxCallbackFunction定时器回调函数当定时器定时周期到了以后就会调用这个函数。 pxTimerBuffer参数指向一个StaticTimer_t 类型的变量用来保存定时器结构体。 返回值 NULL: 软件定时器创建失败。 其他值: 创建成功的软件定时器句柄。
15.6 开启软件定时器
如果软件定时器停止运行的话可以使用FreeRTOS 提供的两个开启函数来重新启动软件定 时器这两个函数表15.6.1 所示 1、函数xTimerStart() 启动软件定时器函数xTimerStartFromISR()是这个函数的中断版本可以用在中断服务函 数中。如果软件定时器没有运行的话调用函数xTimerStart()就会计算定时器到期时间如果软 件定时器正在运行的话调用函数xTimerStart()的结果和xTimerReset()一样。此函数是个宏真 正执行的是函数xTimerGenericCommand函数原型如下
BaseType_t xTimerStart( TimerHandle_t xTimer,
TickType_t xTicksToWait )参数 xTimer要开启的软件定时器的句柄。 xTicksToWait设置阻塞时间调用函数xTimerStart()开启软件定时器其实就是向定时器命令 队列发送一条tmrCOMMAND_START 命令既然是向队列发送消息那肯 定会涉及到入队阻塞时间的设置。 返回值 pdPASS: 软件定时器开启成功其实就是命令发送成功。 pdFAIL: 软件定时器开启失败命令发送失败。 2、函数xTimerStartFromISR() 此函数是函数xTimerStart()的中断版本用在中断服务函数中此函数是一个宏真正执行 的是函数xTimerGenericCommand()此函数原型如下
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
BaseType_t * pxHigherPriorityTaskWoken );参数 xTimer要开启的软件定时器的句柄。 pxHigherPriorityTaskWoken标记退出此函数以后是否进行任务切换这个变量的值函数会自动设置的用户不用进行设置用户只需要提供一个变量来 保存这个值就行了。当此值为pdTRUE 的时候在退出中断服务 函数之前一定要进行一次任务切换。 返回值 pdPASS: 软件定时器开启成功其实就是命令发送成功。 pdFAIL: 软件定时器开启失败命令发送失败。
15.7 停止软件定时器
既然有开启软件定时器的API 函数那么肯定也有停止软件定时器的函数FreeRTOS 也 提供了两个用于停止软件定时器的API 函数如表15.7.1 所示 1、函数xTimerStop() 此函数用于停止一个软件定时器此函数用于任务中不能用在中断服务函数中此函数 是一个宏真正调用的是函数xTimerGenericCommand()函数原型如下
BaseType_t xTimerStop ( TimerHandle_t xTimer,
TickType_t xTicksToWait )参数 xTimer要停止的软件定时器的句柄。 xTicksToWait设置阻塞时间调用函数xTimerStop()停止软件定时器其实就是向定时器命令 队列发送一条tmrCOMMAND_STOP 命令既然是向队列发送消息那肯定 会涉及到入队阻塞时间的设置。 返回值 pdPASS: 软件定时器停止成功其实就是命令发送成功。 pdFAIL: 软件定时器停止失败命令发送失败。 1、函数xTimerStopFromISR() 此函数是xTimerStop()的中断版本此函数用于中断服务函数中此函数是一个宏真正执 行的是函数xTimerGenericCommand()函数原型如下
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
BaseType_t * pxHigherPriorityTaskWoken );参数 xTimer要停止的软件定时器句柄。 pxHigherPriorityTaskWoken标记退出此函数以后是否进行任务切换这个变量的值函数会 自动设置的用户不用进行设置用户只需要提供一个变量来保存这个值就行了。当此值为pdTRUE 的时候在退出中断服务 函数之前一定要进行一次任务切换。 返回值 pdPASS: 软件定时器停止成功其实就是命令发送成功。 pdFAIL: 软件定时器停止失败命令发送失败。
15.8 软件定时器实验
15.8.1 实验程序设计
1、实验目的 学习FreeROTS 软件定时器的使用包括软件定时器的创建、开启和停止。 2、实验设计 本实验设计两个任务start_task 和timercontrol_task 这两个任务的任务功能如下 start_task用来创建任务timercontrol_task 和两个软件定时器。 timercontrol_task控制两个软件定时器的开启和停止。 实验中还创建了两个软件定时器AutoReloadTimer_Handle 和OneShotTimer_Handle这两 个定时器分别为周期定时器和单次定时器。定时器AutoReloadTimer_Handle 的定时器周期为 1000 个时钟节拍(1s)定时器OneShotTimer_Handle 的定时器周期为2000 个时钟节拍(2s)。 3、实验工程 FreeRTOS 实验15-1 FreeRTOS 软件定时器实验。 4、实验程序与分析 ●任务设置
#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 256 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TIMERCONTROL_TASK_PRIO 2 //任务优先级
#define TIMERCONTROL_STK_SIZE 256 //任务堆栈大小
TaskHandle_t TimerControlTask_Handler; //任务句柄
void timercontrol_task(void *pvParameters); //任务函数TimerHandle_t AutoReloadTimer_Handle; //周期定时器句柄
TimerHandle_t OneShotTimer_Handle; //单次定时器句柄
void AutoReloadCallback(TimerHandle_t xTimer); //周期定时器回调函数
void OneShotCallback(TimerHandle_t xTimer); //单次定时器回调函数
//LCD 刷屏时使用的颜色
int lcd_discolor[14]{ WHITE, BLACK, BLUE, BRED,GRED, GBLUE, RED, MAGENTA,GREEN, CYAN, YELLOW, BROWN,BRRED, GRAY };
●main()函数
int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(); //延时函数初始化uart_init(115200); //初始化串口LED_Init(); //初始化LEDKEY_Init(); //初始化按键BEEP_Init(); //初始化蜂鸣器LCD_Init(); //初始化LCDmy_mem_init(SRAMIN); //初始化内部内存池POINT_COLOR RED;LCD_ShowString(30,10,200,16,16,ATK STM32F103/407);LCD_ShowString(30,30,200,16,16,FreeRTOS Examp 15-1);LCD_ShowString(30,50,200,16,16,KEY_UP:Start Tmr1);LCD_ShowString(30,70,200,16,16,KEY0:Start Tmr2);LCD_ShowString(30,90,200,16,16,KEY1:Stop Tmr1 and Tmr2);LCD_DrawLine(0,108,239,108); //画线LCD_DrawLine(119,108,119,319) ; //画线POINT_COLOR BLACK;LCD_DrawRectangle(5,110,115,314); //画一个矩形LCD_DrawLine(5,130,115,130); //画线LCD_DrawRectangle(125,110,234,314); //画一个矩形LCD_DrawLine(125,130,234,130); //画线POINT_COLOR BLUE;LCD_ShowString(6,111,110,16,16,AutoTim:000);LCD_ShowString(126,111,110,16,16,OneTim: 000);//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )start_task, //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(TaskHandle_t* )StartTask_Handler); //任务句柄vTaskStartScheduler(); //开启任务调度
}
●任务函数
//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建软件周期定时器, 周期定时器周期1s(1000 个时钟节拍)周期模式AutoReloadTimer_HandlexTimerCreate((const char* )AutoReloadTimer, (1)(TickType_t )1000,(UBaseType_t )pdTRUE,(void* )1,(TimerCallbackFunction_t)AutoReloadCallback);//创建单次定时器, 单次定时器周期2s(2000 个时钟节拍)单次模式OneShotTimer_HandlexTimerCreate((const char* )OneShotTimer, (2)(TickType_t )2000,(UBaseType_t )pdFALSE,(void* )2,(TimerCallbackFunction_t)OneShotCallback);//创建TASK1 任务xTaskCreate((TaskFunction_t )timercontrol_task,(const char* )timercontrol_task,(uint16_t )TIMERCONTROL_STK_SIZE,(void* )NULL,(UBaseType_t )TIMERCONTROL_TASK_PRIO,(TaskHandle_t* )TimerControlTask_Handler);vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//TimerControl 的任务函数
void timercontrol_task(void *p_arg)
{u8 key,num;while(1){//只有两个定时器都创建成功了才能对其进行操作if((AutoReloadTimer_Handle!NULL)(OneShotTimer_Handle!NULL)){key KEY_Scan(0);switch(key){case WKUP_PRES: //当key_up 按下的话打开周期定时器xTimerStart(AutoReloadTimer_Handle,0); //开启周期定时器(3)printf(开启定时器1\r\n);break;case KEY0_PRES: //当key0 按下的话打开单次定时器xTimerStart(OneShotTimer_Handle,0); //开启单次定时器(4)printf(开启定时器2\r\n);break;case KEY1_PRES: //当key1 按下话就关闭定时器xTimerStop(AutoReloadTimer_Handle,0); //关闭周期定时器(5)xTimerStop(OneShotTimer_Handle,0); //关闭单次定时器printf(关闭定时器1 和2\r\n);break;}}num;if(num50) //每500msLED0 闪烁一次{num0;LED0!LED0;}vTaskDelay(10); //延时10ms也就是10 个时钟节拍}
}(1)、调用函数xTimerCreate()创建定时器AutoReloadTimer_Handle这是一个周期定时器 定时周期是1000 个时钟节拍在本例程中就是1s。 (2)、调用函数xTimerCreate()创建定时器OneShotTimer_Handle这是一个单次定时器定 时器周期为2000 个时钟节拍在本例程中就是2s。 (3)、当KEY_UP 按下以后调用函数xTimerStart()开启周期定时器AutoReloadTimer_Handle。 (4)、当KEY0 按下以后调用函数xTimerStart()开启单次定时器OneShotTimer_Handle。 (5)、当KEY1 按下以后就调用函数xTimerStop()同时关闭定时器AutoReloadTimer_Handle 和OneShotTimer_Handle。 ●定时器回调函数
//周期定时器的回调函数
void AutoReloadCallback(TimerHandle_t xTimer)
{static u8 tmr1_num0;tmr1_num; //周期定时器执行次数加1LCD_ShowxNum(70,111,tmr1_num,3,16,0x80); //显示周期定时器的执行次数LCD_Fill(6,131,114,313,lcd_discolor[tmr1_num%14]); //填充区域
}
//单次定时器的回调函数
void OneShotCallback(TimerHandle_t xTimer)
{static u8 tmr2_num 0;tmr2_num; //周期定时器执行次数加1LCD_ShowxNum(190,111,tmr2_num,3,16,0x80); //显示单次定时器执行次数LCD_Fill(126,131,233,313,lcd_discolor[tmr2_num%14]); //填充区域LED1!LED1;printf(定时器2 运行结束\r\n);
}15.8.2 程序运行结果分析
编译并下载实验代码到开发板中默认情况下LCD 显示如图15.8.2.1 所示。 当按下KEY0 键以后定时器OneShotTimer_Handle 开始运行当定时器计时时间到了以后 就会调用函数OneShotCallback()屏幕右侧区域的背景色会被刷新为其他颜色背景颜色刷新 完成以后定时器OneShotTimer_Handle 就会停止运行。当按下KEY_UP 键的话定时器 AutoReloadTimer_Handle 就会开始运行定时器周期到了以后屏幕左侧区域的背景色会被刷新 为其他颜色。由于定时器AutoReloadTimer_Handle 是周期定时器所以不会停止运行除非按 下KEY1 键同时关闭定时器AutoReloadTimer_Handle 和OneShotTimer_Handle。