如果我对本翻译内容享有所有权。允许任何人复制使用本文章,不会收取任何费用。如有平台向你收取费用与本人无任何关系
第十一章 . 开发者支持
章节介绍和范围
这章的重点是包括一堆有助于高效开发的功能集:
- 提供一个应用程序的行为观察器
- 重点优化项
- 在错误发生的地方捕获错误
configASSERT()函数
在C语言中,assert()宏用于验证程序中的断言。这里的断言是一个C语言的表达式,如果表达式等于false(0),断言就会认为存在异常。比如列表163就是测试pxMyPointer是不是NULL的断言。
// 用标准的C语言断言宏测试pxMyPointer是不是为NULL。列表163
/* 测试pxMyPointer是不是NULL的断言
assert(pxMyPointer != NULL);
程序开发者会提供一个断言宏的实现,以方便指定断言失败时的处理方式。
FreeRTOS源代码没有调用assert(),因为assert()不能为所有可以编译FreeRTOS的设备上得到支持。作为替代,FreeRTOS源代码包含很多调用configASSERT()的宏。程序开发者可以在FreeRTOSConfig.h中定义它,通常都和标准的assert()宏差不多。
一个失败的断言必须作为一个致命错误处理。不要试图在有一个断言失败时继续执行下一行代码。
用configASSERT()立即捕获和识别许多源代码中的错误,可以提高开发效率。强烈建议定义好configASSERT(),当开发和调试一个FreeRTOS程序时。
定义了configASSERT()会在运行调试时提供很多帮助,但也会增加程序大小,也会降低程序执行效率。如果没有提供一个configASSERT()的定义,那么就会使用默认的空定义,所有的调用configASSERT()都会被C语言预处理时删除。
定义configASSERT()的例子
configASSERT()的定义列在列表164,只有在程序被调试控制器执行时才会使用。一旦有任何行的代码断言失败就会中断执行,断言失败的行,在调试对话暂停的时候由调试器显示出来。
// 当调试器控制执行时,一个简单有用的configASSERT()定义。列表164
/* 禁用中断,使tick中断停止执行,然后加入一个无限的循环,这样就不会执行断言失败后的代码。如果硬件支持调试退出指令,那么用调试退出指令代替这里的无限循环*/
#define configASSERT(x) if((x) == 0)(taskDISABLE_INTERRUPTS(); for(;;);)
列表165中的断言在程序没有在调试器控制下执行时更有用。它打印或记录断言失败的行。断言失败的行会用标准C语言的__FILE__宏(源文件名字)和__LINE__宏(源文件中的行数)识别。
// 记录源代码断言失败的configASSERT()定义。列表165
/* 这个函数只能定义在C源码文件中,而不是FreeRTOSConfig.h中 */
void vAssertCalled(const char *pcFile, uint32_t ulLine){
/* 在这个函数里,pcFile保存捕获到错误行的源码文件名字,ulLine保存这个文件中的行号。pcFile和ulLine可以在进入无限循环之前,打印出来或记录下来*/
RecordErrorInformationHere(pcFile, ulLine);
/* 禁用中断,tick中断停止执行,然后进入无限循环,使断言失败的行后面的代码都不能得到执行 */
taskDISABLE_INTERRUPTS();
for(;;);
}
/* ----------------------------------------- */
/* 下面的两行需要放在FreeRTOSConfig.h中 */
extern vAssertCalled(const char *pcFile, uinte32_t ulLine);
#define configASSERT(x) if((x) == 0) vAssertCalled(__FILE__, __LINE__)
FreeRTOS+Trace
FreeRTOS+Trace是一个由伙伴公司Percepio提供的,代码运行时的分析和优化工具。
FreeRTOS+Trace会捕获有价值的动态信息,然后将捕获的信息用图形化的方式展示出来。它也能同步展示多个视图。
捕获的重要信息对于分析,解决重大问题,或者简单的优化一个FreeRTOS程序都非常有用。
FreeRTOS+Trace可以和传统的调试器一起使用,而且以一个更高时间基础的视角补充了传统调试器的调试界面。
FreeRTOS追踪包含20多种具有互联关系的视图
调试相关勾子函数
分配失败勾子
分配失败的勾子函数在第2章(堆空间管理)有介绍。
定义了一个自动分配失败的勾子函数,确保自动创建一个任务,队列,信号量或事件组失败的时候,程序开发者可以立即注意到。
堆栈溢出勾子函数
更加详细的堆栈溢出勾子函数在12.3(堆栈溢出)有更多介绍。
定义一个堆栈溢出勾子函数,确保开发者注意到分配给堆栈的空间是否超出了堆栈的限制。
运行时观察和任务状态信息
任务运行统计
任务运行统计提供了各个任务运行的处理器时间数量。一个任务的运行时间是自程序被执行以来,任务处在运行状态的总时间。
运行时间统计可用于程序项目开发的不同阶段的分析和调试提示。它提供的消息只有在统计的运行时间没有溢出时才有效。注意收集运行统计时间会增加任务上下文切换时间。
可以通过调用uxTaskGetSystemState()函数来包含二进制运行统计时间信息。也可以通过调用vTaskGetRunTimeStats()获取运行时间统计信息,但这时的信息是人能够看懂的ASCII码表。
运行时间统计
运行时间统计需要统计tick周期片数量。所以不能将tick用作运行时间统计时钟,而是需要使用程序代码提供的时钟。推荐以高于tick中断频率10到100次的频率,生成一个运行时间统计时钟。运行时间统计时钟越快,统计就会越精确,但时间的值也会更快速的溢出。
理想中,这里的时间值是一个完全独立的硬件计数器产生的32位数字,读取这个值也不会推迟其他的处理操作。如果因为硬件或者时钟速度原因,不能满足这种理想条件。可以用下面几种性能更低的技术替代:
- 配置一个外设,按照期望的运行时间统计时钟周期生成一个周期的中断,然后用一个数字计量中断生成的次数,作为运行时间统计时钟。
如果只是将这个中断用作运行时间统计时钟,非常没有效率。因此,如果程序已经使用了一个合适的周期中断,那么有一个简单又高效的办法,在已经存在的中断中增加一个生成中断计数值。 - 用一个16位独立外设的当前值生成一个32位的值,将生成的16位作为32位的后16位。这样使用时的溢出值就是这个32位的最后16位。相当于一个16位值,最大65535。
也可以进行一些适当或稍微复杂的操作。为生成一个运行时间统计时钟,可以组合RTOS的tick和ARM Cortex-M的Systick的当前值。一些从FreeRTOS下载的演示程序就是这样做的。
配置程序收集运行时间统计信息
表54展示了收集任务运行时间统计需要的宏。起初这些宏是包含在FreeRTOS的port部分的,因此这些宏都以port为前缀,但更好的实践是将它们定义在FreeRTOSConfig.h中。
表54,收集任务时间运行统计用到的宏
宏 | 描述 |
---|---|
configGENERATE_RUN_TIME_STATS | 在FreeRTOSConfig.h中这个宏必须设置为1。这个宏设置为1,调度器会在合适实际调用这个表中的其他宏。 |
portCONFIGURE_TINER_FOR_RUN_TIME_STATS() | 无论用哪个外设给运行时间统计作为时钟,都需要提供这个宏,给它进行初始化 |
portGET_RUN_TIME_COUNTER_VALUE() 或portALT_GET_RUN_TIME_COUNTER_VALUE(Time) | 必须提供两个宏中的一个,用于返回当前时钟统计计数值。这个值是自程序开始运行以来所有值。如果用第一个宏,它必须返回当前时钟值。如果用第二个宏,它必须设置Time参数为当前时钟值。 |
- | - |
uxTaskGetSystemState()函数
uxTaskGetSystemState()为每个在调度器控制下的任务提供一个状态信息的快照。它以一个`TaskStatus_t`格式的数组提供信息,每个任务在数组中都有一个索性值。`TaskStatus_t`在列表167和表56中有描述。
// uxTaskGetSystemState()函数原型。列表166
UBaseType_t uxTaskGetSystemState(TaskStatus_t * const pxTaskStatusArray, const UBaseType_t uxArraySize, uint32_t * const pulTotalRunTime);
# 表55,uxTaskGetSystemState()参数和返回值
pxTaskStatusArray: 一个指向TaskStatus_t的指针。这个数组中至少为每一个任务分配一个TaskStatus_t的结构描述。可以使用uxTaskGetNumberOfTasks()获取任务数量。列表167中有展示TaskStatus_t结构体,列表56描述了它的成员。
uxArraySize: pxTaskStatusArray的数组长度。一个长度是pxTaskStatusArray中包含的TaskStatus_t的结构体数量的索引,并不仅仅是数组长度。
pulTotalRunTime: 如果FreeRTOSConfig.h中的configGENERATE_RUN_TIME_STATS设置为1,那么*pulTotalRunTime会被uxTaskGetSystemState()设置为自项目开启以来总的运行时间。pulTotalRunTime是一个可选项,不用时可以设置为NULL。
返回值: uxTaskGetSystemState()会返回TaskStatus_t的数量,这个返回值需要和uxTaskGetNumber()函数相同,但如果传给给他的uxArraySize太小会返回0。
// TaskStatus_t结构体。列表167
typedef struct xTask_STATUS {
TaskHandle_t xHandle;
const char *pcTaskName;
UBaseType_t xTaskNumber;
eTaskState eCurrentState;
UBaseType_t uxCurrentPriority;
UBaseType_t uxBasePriority;
uint32_t ulRunTimeCounter;
uint16_t usStackHighWaterMark;
}TaskStatus_t;
# TaskStatus_t 结构成员
xHandle: 结构中信息关联的任务句柄
pcTaskName: 任务人类可以阅读的任务名称
xTaskNumber: 每个任务都有一个唯一的xTaskNumber值。如果程序在运行时创建或者删除一个任务,对于之前删除过的任务,旧任务和新任务就有相同任务句柄。xTaskNumber这个时候就可以用来,让程序和内核调试者区分任然可用的任务和已经删除后创建的任务
eCurrentState: 保存任务状态的枚举。eCurrentState可以是eRunning,eReady,eBlock,eSuspended,eDeleted中的一个。任务只有调用vTaskDelete()以后的一个很短的周期后,被置为eDeleted状态。因为这段时间就是空闲任务释放之前在栈上分配给任务结构的空间。在这之后任务不再以任何形式存在,也不能再以任何方式使用它的句柄
uxCurrentPriority: 在运行uxTaskGetSystemState()时任务的优先级。uxCurrentPriority的优先级只能比程序设计时任务优先级高。在章节7.3互斥锁和二进制信号量中,任务在出现优先级继承时,可能出现短暂的优先级提升。
uxBasePriority: 程序开发者初始化任务时的优先级。只有FreeRTOSConfig.h中的configUSE_MUTEXES为1时才可用。
ulRunTimeCounter: 任务被创建以来,总共使用的时间。一个时间作为一个绝对时间,这个时间开发者用于运行时间统计。只有在FreeRTOSConfig.h中的configGENERATE_RUN_TIME_STATS设置为1时可用。
usStackHighWaterMark: 任务栈最高水位标志。它用于指定自任务创建以来,任务保有的最大栈空间数量。它是一个标识,一旦任务使用的栈空间溢出,就会关闭这个任务。任务关闭值就是0,已经栈溢出的任务会被关闭,usStackHighWaterMark是以字节为单位的。
vTaskList()帮助函数
vTaskList()提供和uxTaskGetSystemState()相似的任务状态信息,但它会以人类可以阅读的ASCII方式,而不是二进制数组方式提供。
vTaskList()是一个处理器加强函数,会让调度器暂停几个周期,推荐只在调试模式下使用,不要在正式的项目中使用。
只有在FreeRTOSConfig.h中的configUSE_TRACE_FACLITY设置为1时才能使用vTaskList函数。
// vTaskList()函数原型。列表168
void vTaskList(signed char *pcWriteBuffer);
/* 参数
* pcWriteBuffer: 人类可以阅读的任务状态字符缓存指针。这个缓存必须比要保存的信息需要空间大,因为它不会自动分配空间
*/
图88就是一个用vTaskList()生成的一个输出,在这里的输出中:
- 第一列是任务名字
- 第二列是任务状态,'R’表示就绪,'B’表示阻塞,'S’表示暂停,'D’表示被删除。任务只会在删除状态一小会儿,这一小会儿时间就是调用vTaskDelete()和空闲任务释放任务占用的栈空间的时间。在这之后任务就不在以任何形式存在,也不在可以用句柄引用。
- 第三列是任务优先级
- 第四列是任务栈最高水位标志。可以看表56查看更多信息
- 第五列是任务唯一值。查看表56中的xTaskNumber获取很多信息
# vTaskList()的输出实例。图88
tcpip R 3 393 0
Tmr Svc R 3 111 48
QconsB1 R 1 143 3
QproB5 R 0 144 7
QconsB6 R 0 143 8
polSER1 R 0 145 11
polSER2 R 0 145 12
vTaskGetRunTimeStats帮助函数
vTaskGetRunTimeStats()函数会将手机的运行时间统计,以人类可读的ASCII格式输出。
vTaskGetRunTimeStats()是一个扩展函数,会暂停调度器几个周期。所以,只推荐在调试的时候使用,不要在实时系统的实际产品中使用。
只有在FreeRTOSConfig.h中的configGENERATE_RUN_TIME_STATS和configUSE_STATS_FORMATTING_FUNCTIONS都是1的时候才可以使用。
// vTaskGetRunTimeStats()原型。列表169
void vTaskGetRunTimeStats(singed char *pcWriteBuffer);
/* 参数
* pcWriteBuffer: 人类可以阅读的任务状态字符缓存指针。这个缓存必须比要保存的信息需要空间大,因为它不会自动分配空间
*/
图89就是一个vTaskGetRunTimeStats()的输出例子。在输出中:
- 每一行都是一个单独的任务
- 第一列是任务名字
- 第二列是任务花费在运行状态的绝对计数值,可以查看表56的ulRunTimeCounter参数
- 第三列是一个任务总运行时间占总启动时间的百分比。这个百分比只能小于等于100%,因为最多也只能是一直在运行这个任务,而往往还需要运行其他任务
# vTaskGetRunTimeStats()输出例子。图89
PolSEM1 994 <1%
PolSEM2 23248 1%
GenQ 194479 16%
MuLow 3690 <1%
Rec3 229450 18%
CNT1 242720 19%
PeekL 94 <1%
CNT_INC 165 <1%
CNT2 243166 20%
SUSP_RX 243166 20%
IDLE 55 <1%
一个例子,生成和显示运行时间统计
这里假设使用一个16位计时器生成一个32位运行时统计时钟。这个计数器配置来每当16位值到达最大值,就生成一个中断,就是创建一个溢出中断。中断服务程序会计算值溢出的次数。
这个32位的值创建后,将中断溢出的次数作为2个32位值中最重要的字节(高16位),当前的16位计数值作为作为低16位值存储起来。中断服务程序伪代码列中列表170中。
//16位溢出中断够本计数。列170
void TimeOverflowInterruptHandler(void){
/* 只是对中断进行计数 */
ulOverflowCount++;
/* 清除中断 */
ClearTimerInterrupt();
}
列表171列出了在FreeRTOSConfig.h中加入一些宏,使能运行时间统计。
// 在FreeRTOSConfig.h中加入一些宏,使能运行时间统计。列表171
/* 设置configGENERATE_RUN_TIME_STATS为1,使能运行时间统计。当定义好这个值的时候,portCONFIGURE_TINER_FOR_RUN_TIME_STATS,portGET_RUN_TIME_COUNTER_VALUE或portALT_GET_RUN_TIME_COUNTER_VALUE都要定义好*/
#define configGENERATE_RUN_TIME_STATS 1
/* portCONFIGURE_TINER_FOR_RUN_TIME_STATS() 定义来调用设置假设的16位计数器 */
void vSetupTimerForRunTimeStats(void);
#define portCONFIGURE_TINER_FOR_RUN_TIME_STATS() vSetupTimerForRunTimeStats()
/* portALT_GET_RUN_TIME_COUNTER_VALUE()定义来设置当前运行时间计数或时间值的参数。返回的时间值是一个32位的值,它是通过将16位的时间溢出计数值移位到32位值的前2个字节,再将它和16位计数器的当前计数值按位或 */
#define portALT_GET_RUN_TIME_COUNTER_VALUE(ulCountValue) \
{ \
extern volatile unsigned long ulOverflowCount; \
/* 暂停计数器,使读取这值的时候才不会变化*/ \
PauseTimer(); \
/* 计数器已经溢出次数会向左移位16后,放入32位的值中*/ \
ulCountValue = (ulOverflowCount << 16UL); \
/* 当前的16位计数值直接与上面的值进行按位或 */ \
ulCountValue |= (unsigned long)ReadTimerCount(); \
/* 恢复计数器运行 */ \
ResumeTimer(); \
}
列表172中的任务会每5秒打印一次收集的运行时间统计。
// 打印运行时间统计。列表172\
/* 为了清晰,这里没有调用fflush()刷新屏幕 */
static void prvStatsTask(void *pvParameters){
TickType_t xLastExecutionTime;
/* 用于保存运行时间统计的字符要足够大。这里声明为静态的确保它不会被分配到任务堆栈上。这柆这个函数就是非可重入的。 */
static signed char cStringBuffer[512];
/* 这个任务每5秒运行一次 */
const TickType_t xBlockPerod = pdMS_TO_TICKS(5000);
/* 初始化最后运行时间为当前时间。这是唯一一次需要直接写入这个值的地方。之后都会用vTaskDelayUntil()函数更新它的值 */
xLastExecutionTime = xTaskGetTickCount();
/* 和大多数任务一样,这个任务在一个无限循环中实现 */
for(;;){
/* 等待下一个时间点到来 */
vTaskDelayUntil(&xLastExecutionTime, xBlockPerod);
/* 生成一个运行时间统计文本表。它的大小需要小于cStringBuffer长度*/
vTaskGetRunTimeStats(cStringBuffer);
/* 打印出运行时间统计表头 */
printf("\nTask\t\tAbs\t\t\t%%\n");
printf("----------------------------------------------------");
/* 打印运行时间统计表实际信息。表数据有多个行,所以vPrintMultipleLine()函数用于代替printf()。vPrintMultipleLine()只是对每个单独的行用printf()打印,确保终端行缓存正常工作 */
vPrintMultipleLine(cStringBuffer);
}
}
追踪勾子宏
追踪勾子宏是那些在FreeRTOS源代码中的按键指针。默认它们是空的,不会生成任何代码,也不会有运行负担。开发者可以通过下面的方式覆盖默认的空实现:
- 加入新的代码到FreeRTOS,而不改变FreeRTOS源代码
- 在目标硬件上通过可理解的方式输入具体的运行时序信息。FreeRTOS源代码中使用了很多追踪宏,让它们可以用来创建一个完整且详细的调度器追踪和分析日志。
可用的追踪宏
这里不会有太多的细节介绍每一个追踪宏。表59列出了这个宏的子信,它们对程序开发者非常有用。
表59中的有很多关于pxCurrentTCB的描述。pxCurrentTCB是FreeRTOS一个私有的变量,它保存在运行状态的任务句柄,FreeRTOS源代码,task.c源文件中都有对这个宏的使用。
# 一些最常用的追踪宏。表59
traceTASK_INCREMENT_TICK(xTickCount): 被tick中断调用,tick计数增加之后,新的tick计数就会写入到xTickCount宏中
traceTASK_SWITCHED_OUT(): 选择一个任务运行之前调用。这时pxCurrentTCB包括了将要离开运行状态的任务的句柄
traceTASK_SWITCHED_IN(): 选择一个任务运行之后调用。这时pxCurrentTCB包括进入运行状态的任务句柄
traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue): 任务试图从空队列读取数据或试图获取一个空的信号量和互斥锁,进入到阻塞状态立即调用这个宏。这里的pxQueue传递目标队列,信号量或互斥锁的句柄
traceBLOCKING_ON_QUEUE_SEND(pxQueue): 任务试图发送数据给一个已经满的队列,而进入阻塞,立即调用这个宏。pxQueue参数传递这个队列的句柄
traceQUEUE_SEND(pxQueue): 由xQueueSend(),xQueueSendToFront,xQueueSendToBack()或一些信号量释放函数调用,当队列发送或信号量释放成功时调用。pxQueue参数传递目标队列或信号量句柄
traceQUEUE_SEND_FAILED(pxQueue): 由xQueueSend(),xQueueSendToFront,xQueueSendToBack()或一些信号量释放函数调用,当队列发送或信号量释放失败时调用。如果队列在期望的时间内一直都是满的队列发送或信号量释放就会失败。pxQueue参数是目标队列或信号量的句柄
traceQUEUE_RECEIVE(pxQueue): 由队列接收或一些信号量获取函数调用。当队列接收或信号量获取成功时调用它。pxQueue就是传递的目标队列或信号量的句柄
traceQUEUE_RECEIVE_FAILED(pxQueue): 由队列接收或一些信号量获取函数调用。当队列接收或信号量获取失败时调用它。如果队列或信号量为空,而且在期望的时间内一直没有新的数据,那么队列接收和信号量获取就会失败。pxQueue就是传递的目标队列或信号量的句柄
traceQUEUE_SEND_FROM_ISR(pxQueue): 当发送操作成功,由xQueueSendFromISR()调用。pxQueue就是传递的目标队列句柄
traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue): 当发送操作失败,由xQueueSendFromISR()调用。如果队列已经满了,发送操作就会失败。pxQueue就是传递的目标队列句柄
traceQUEUE_RECEIVE_FROM_ISR(pxQueue): 当接收操作成功,由xQueueReceiveFromISR()调用。pxQueue就是传递的目标队列句柄
traceQUEUE_RECEIVE_FROM_ISR_FAILED(pxQueue): 当接收操作失败,由xQueueReceiveFromISR()调用。如果队列已经为空,接收操作就会失败。pxQueue就是传递的目标队列句柄
traceTASK_DELAY_UNTIL(): 在调用任务进入阻塞状态之前,由vTaskDelayUntil()立即调用
traceTASK_DELAY(): 在调用任务进入阻塞状态之前,由vTaskDelay()立即调用
定义追踪勾子宏
每个追踪勾子宏都有一个默认的空定义。这个定义可以被FreeRTOSConfig.h中的新定义覆盖。如果定义一个更长或复杂的追踪宏,可以将它放在一个新的头文件中,然后在FreeRTOSConfig.h中包含它就可以了。
实际开发者的最佳实践。FreeRTOS有一个严格的隐藏规则。可以将用户代码加入到FreeRTOS源代码中,因此这些追踪宏的数据格式和那些普通用户数据不同:
- 在FreeRTOS和task.c源代码中,任务句柄上一个指向任务控制块的数据结构指针。在FreeRTOS和task.c源代码之外,任务句柄是一个空指针。
- 在FreeRTOS和queu.c源代码中,队列句柄是一个指向描述队列数据结构的指针。在FreeRTOS和queue.c源代码之外,队列是一个空指针
这里需要极其注意,如果通过追踪指针直接访问FreeRTOS私有的数据结构,不同FreeRTOS之间私有数据结构可能会有差异
FreeRTOS前台调试插件
一些IDE中也提供了FreeRTOS的前台调试插件,这里列出的可能还不够详尽:
- Eclipse(StateViewer)
- Eclipse(ThreadSpy);
- IAR
- ARM DS-5
- Atollic TrueStudio
- Microchip MPLAB
- iSYSTEM WinlDEA