Contiki开发3:调试平台与Debug系统
曾经掉进的坑
7年前,我们在PC104(Intel x86系统中面向嵌入式的CPU)平台开发Linux+miniGUI的产品,奇怪的是,每当测试人员多次点击触摸屏后,程序崩溃了。当然,是应用进程崩溃,Linux还运行良好。附带说一句,像Linux这种多进程系统还是很稳定的,哪怕一个用户进程挂掉,其他进程仍正常运行;在这方面,一般的RTOS无法比拟。
当时这种bug极大地打击我们的士气,记得我给家人打电话:程序员这碗饭,可能吃不下去了。直到部门经理请来一个高手,他说,miniGUI有一个测试版本和日志打印,你们使能这2个Debug功能。果真,他这个方法,让我们很快找到错误根源----miniGUI的调色板有一个指针为空!要知道,这个函数已经被调用到了第8层,如果没有自动捕捉,不敢想象如何解决它。
这个事件,让我们汲取了宝贵的经验,优秀的系统和生命一样:它能运行在测试或正式版本(如:人们正常工作和生病住院),它能捕捉异常(如:人体疼痛和发烧),它能与人对话(如:患者把自己不适之处告诉医生)。直到今天,我们依然保持如下的调试和Debug,它给我们带来的红利是:信心,质量和速度。
1 维护2个版本
尽管现代的IDE(集成开发环境)给嵌入式开发带来诸多便捷(如断点白盒测试,软件性能剖析等),然而还有如下需求:
1. 系统希望能够自动捕捉一些致命的错误,这需要设计一个ASSERT系统;
2. 像黑盒和集成测试它更希望系统持续运行,这样日志打印能清晰反馈系统运行轨迹和关键数据的计算;
当然,以上DEBUG系统仅包含在“测试版本”中,不应该包含在“发布版本”。因此,需要建立全局控制宏:0=测试版本,1=发布版本。
#define REL_VER 0 /* 0=debug; 1=release */
2 捕捉异常
当嵌入式软件遇到一些“它认为不可能发生”的异常时,包括:指针为空、除数为0、数组下标越界、硬件紊乱等,不但需要报警,还需要停止系统运行。这种情况下,ASSERT系统可以有效捕捉此类异常,当然它仅包含在“测试版本中”。(一个高质量产品的“发布版本”软件是绝对不允许此类异常存在)。
#undef ASSERT /* remove existing definition */
#if REL_VER
#define ASSERT(test) NULL
#else
extern void _AssertUART(const char *, unsigned int);
#if (UART == PRINT_WAY)
#define ASSERT(test) \
if (test) \
NULL; \
else \
_AssertUART(__FILE__, __LINE__)
#else
#define ASSERT(test) assert(test)
#endif
#endif
从上面代码清单可知:
仅当“测试版本”时ASSERT()才编译成执行代码,如果定向到UART,一旦捕捉到异常,它将通过UART打印“文件名”和“行号”;如果定向到标准C,它将直接调用assert()库函数。
_AssertUART()函数将异常所在的“文件名”和“行号”解释成字符串,并通过UART口打印。
3 让程序说话
嵌入式软件程序员一定遇到这样的需求:
1. 测试某个模块时,希望开启打印,查看程序执行流和数据计算;而该模块测试完毕后,希望关闭打印,这称为日志打印的“模块控制”。
2. 当系统加入新代码后运行异常,需要对这些没有彻底测试的新代码开启日志打印;还需要对程序状态和执行流进行打印,这称为日志打印的“类型控制”。
3. 有时候,希望程序打印后停止运行,以方便检查上下文,即“停止系统”。
4. 异常是分级别的,包括:“警告/严重/致命”,希望能打印该级别的缺陷报告,这称之为日志打印的“级别控制”。
锐米通信的日志打印满足上述要求,它和普通的C语言库函数printf()相似:
实例1
RIME_DBG( WAKE_TIME_DBG,
"current=%ldms, the next rtc alarm=%ldms.\r\n",
lCurMs,
lAlarmMs);
希望使能上述日志打印:#defineWAKE_TIME_DBG RIME_DBG_ON
希望关闭上述日志打印:#defineWAKE_TIME_DBG RIME_DBG_OFF
实例2(级别=警告)
RIME_DBG( RIME_DBG_LEVEL_WARNING,
"SafeBlockThread(): event buffer is full.\r\n" );
实例3(级别=严重)
RIME_DBG( RIME_DBG_LEVEL_SERIOUS,
"network_AlarmByRTC(): set RTC alarm error!\r\n" );
我们一起来看看上述日志打印的实现。
3.1 打印开关
日志打印使用了8-bit的控制,如下图所示,它们一起实现4种打印开关:模块开关、类型控制、停止系统和级别控制。
模块开关的宏定义如下:
/* flag for RIME_DBG to enable that debugmessage */
#define RIME_DBG_ON 0x80U
/* flag for RIME_DBG to disable that debugmessage */
#define RIME_DBG_OFF 0x00U
类型控制的宏定义如下:
/* flag for RIME_DBG indicating a tracingmessage (to follow program flow) */
#define RIME_DBG_TRACE 0x40U
/* flag for RIME_DBG indicating a statedebug message (to follow module states) */
#define RIME_DBG_STATE 0x20U
/* flag for RIME_DBG indicating newly addedcode, not thoroughly tested yet */
#define RIME_DBG_FRESH 0x10U
停止系统的宏定义如下:
/** flag for RIME_DBG to halt afterprinting this debug message */
#define RIME_DBG_HALT 0x08U
级别控制的宏定义如下:
/** lower two bits indicate debug level
* -0 all
* -1 warning
* -2 serious
* -3 severe
*/
#define RIME_DBG_LEVEL_ALL 0x00
#define RIME_DBG_LEVEL_WARNING 0x01 /* badchecksums, dropped packets, ... */
#define RIME_DBG_LEVEL_SERIOUS 0x02 /*memory allocation failures, ... */
#define RIME_DBG_LEVEL_SEVERE 0x03
#define RIME_DBG_MASK_LEVEL 0x03
3.2 打印端口
一般说来,嵌入式设备的日志打印定向到UART口(也有定向到Ethernet),因此该函数与设备相关联,为方便移植将其分成2部分:解析变参和端口输出,如下代码所示,它定向到UART口打印日志。
#include <stdarg.h>
#define SIZE_DBG_BUF 128
static INT8U s_abyDbgBuf[SIZE_DBG_BUF];
void dbg_PrintfArg(const char *p_chFormat, ...)
{
ASSERT(NULL != p_chFormat);
INT8U byLen;
va_list args;
if (!s_bEnablePrint)
{
return; /* Exit procedure if DISABLE print */
}
/* Process a variable number of arguments into array */
va_start(args, p_chFormat);
byLen = vsnprintf((char *)s_abyDbgBuf, SIZE_DBG_BUF, p_chFormat, args);
va_end(args);
/* TX debug string through COMM */
dp_Tx(s_abyDbgBuf, byLen);
return;
}
3.3 日志打印宏定义
#if DBG_VER
/** print debug message only if debugmessage type is enabled...
* ANDis of correct type AND is at least RIME_DBG_LEVEL
*/
#define RIME_DBG(dbg, fmt, args...) \
do \
{ \
if ( ((dbg) & RIME_DBG_ON) && \
((dbg) & RIME_DBG_TYPES_ON)&& \
(RIME_DBG_MIN_LEVEL <=(int16_t)((dbg) & RIME_DBG_MASK_LEVEL)) ) \
{ \
dbg_PrintfArg(fmt, ##args); \
} \
if ((dbg) & RIME_DBG_HALT) \
{ \
while (1) ; \
} \
}while (0)
#else
#define RIME_DBG(dbg, fmt, args...)
#endif
4 测试之过
理想情况下,测试不能干扰系统的运行,而日志打印的特点包括2个:可能突发地打印一批信息;打印信息总会使系统运行变慢。
大部分嵌入式设备使用UART口执行日志打印,UART口是慢速通信设备,如果前一条日志没有打印完毕,打印当前日志的进程只能阻塞等待,这样就会干扰软件的正常运行。
为此,需要设计一个环形缓冲区,它结合DMA可以实现“无阻塞日志打印”。
详情请链接:http://blog.csdn.net/jiangjunjie_2005/article/details/50807498