前言:

        本文介绍在嵌入式软件开发中的一种调试技巧,提供一种简单高效的方法将开发过程中的Debug调试信息在正式发行软件版本中一键删除。(其实是在正式发行软件版本中不编译那部分代码,但是那部分代码在软件中依然存在)

Tips:

        本文讲解的Debug调试信息指的是在嵌入式软件开发中,下位机(例如:单片机开发板)通过某些通信方式将某些信息输出到上位机(例如:PC机)以便于开发人员直观检查CPU运行过程中系统出现了哪些问题、系统运行到了哪一段代码等等。这里常用的通信方式有串口,并且在Debug调试这项功能中作者常用的是下位机向上位机进行单项信息传输。当然,输出的Debug调试信息是开发者在程序中自主设定的,作者经常在一些重要的条件分支判断中使用Debug调试信息,以此来判断CPU进入到哪个代码段中运行了。除此之外,Debug调试还有其他用途,这里我们就不做介绍了,有感兴趣的小伙伴们自行查询资料进行学习。

 一、常规情况下的Debug调试信息输出方法

        1.直接使用printf函数将Debug调试信息进行打印输出,代码如下:
int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	SystemCoreClockUpdate();
	Delay_Init();
	USART_Printf_Init(921600);	
	printf("SystemClk:%d\r\n",SystemCoreClock);
	printf( "ChipID:%08x\r\n", DBGMCU_GetCHIPID() );
	printf("This is printf example\r\n");

	while(1)
    {

	}
}
        2.串口调试助手显示如下:
3.问题引出:

        Q1:Debug调试信息遍布整个项目工程(即很多代码文件中都存在Debug调试信息输出的需求),那么其中有很多Debug调试信息仅仅是我们开发过程中的需要,开发完成后用户又不需要这些调试信息。

        Q2:开发完后发行的项目工程中那么多printf输出,很消耗CPU资源,也很耗时。你不妨想想,很多嵌入式设备(例如:以单片机为主控的设备)在裸机中使用while大循环不停地printf输出,是不是对整个系统的实时性造成了很大的影响。即使是使用了操作系统,那还不是万变不离其宗,我们设备在逻辑上不还是跑在while大循环中,不还是会对系统的实时性造成很大的影响。

        屏幕前的你是不是在想:嘿嘿嘿,这还不简单,全部删掉或者注释掉。那么我只能说你的项目代码量太小了,稍微大型的一些项目Debug调试信息那简直就是“漫天飞舞”。写出来容易,到最后想手动删掉或者注释掉一堆这些调试信息。嗯~~~,那显得有点不太聪明了呦!

       

        下面来提出一些改进措施。

二、经过改进的Debug调试信息输出方法

        1.通过判断宏定义后进行条件编译的方式控制printf函数是否将Debug调试信息进行打印输出,代码如下(可输出):
#define DEBUG_LOG 
/*********************************************************************
 * @fn      main
 *
 * @brief   Main program.
 *
 * @return  none
 */
int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	SystemCoreClockUpdate();
	Delay_Init();
	USART_Printf_Init(921600);
#ifdef DEBUG_LOG	
	printf("SystemClk:%d\r\n",SystemCoreClock);
	printf( "ChipID:%08x\r\n", DBGMCU_GetCHIPID() );
#endif

	while(1)
    {
		#ifdef DEBUG_LOG
			printf("This is printf example\r\n");
			Delay_Ms(1000);
		#endif
	}
}
        2.串口调试助手显示如下(可输出):
        3.通过判断宏定义后进行条件编译的方式控制printf函数是否将Debug调试信息进行打印输出,代码如下(不可输出):
// #define DEBUG_LOG 
/*********************************************************************
 * @fn      main
 *
 * @brief   Main program.
 *
 * @return  none
 */
int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	SystemCoreClockUpdate();
	Delay_Init();
	USART_Printf_Init(921600);
#ifdef DEBUG_LOG	
	printf("SystemClk:%d\r\n",SystemCoreClock);
	printf( "ChipID:%08x\r\n", DBGMCU_GetCHIPID() );
#endif

	while(1)
    {
		#ifdef DEBUG_LOG
			printf("This is printf example\r\n");
			Delay_Ms(1000);
		#endif
	}
}
        4.串口调试助手显示如下(不可输出):
5.问题引出:

        我们在这里使用宏定义DEBUG_LOG来对Debug调试信息部分的代码来做条件编译。如果我们定义了DEBUG_LOG宏定义,那么我们就输出Debug调试信息;反之,亦然。

        很棒😎,到这里已经可以给自己鼓掌👏了,但是还没到开香槟的地步哦😂!

        

        Q:我们写一条printf语句就要判断一下宏定义DEBUG_LOG,是不是很繁琐。我知道,又有“大聪明”要站出来说:一点也不麻烦啊。行吧,但是你要知道,稍大一些的项目中就可能存在数以百计的printf语句,更别说真正意义上的大型项目有多少条这样的调试语句了,那也就意味着我们要做很多次对宏定义DEBUG_LOG的条件判断。都说到这个地步了,你要是还不嫌麻烦,那我铁定不劝你接着往下看了,直接关掉本篇文档,等吃到苦头的时候记得回来接着往下看哦。

        

 三、作者认为最高效最简单的Debug调试信息输出方法

1.对于以上问题,作者认为最好的解决方法如下:

        在第二个解决方法中我们已经初步达到一键控制调试信息是否输出的要求了,我们在其中纠结的是在大型项目中我们要重复的去对printf语句做宏定义DEBUG_LOG的条件判断,这在我们调试开发的过程中是不可取的。开发者应该把更多的精力放在核心功能的实现上,而不是重复造这种简单低级的“轮子”。

        解决这种问题的方法其实很简单,既然我们不想重复去做宏定义的条件判断,那么我们就让他对整个项目中的所有调试语句只做一次条件判断好了。并且,我们还可以定制化我们输出的Debug调试信息形式(例如:输出当前Debug调试语句所在的文件,函数名,行号等等)。

        接下来,正片开始,且听我娓娓道来。

        2.作者认为Debug调试最好的处理代码(可输出),如下:
int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	SystemCoreClockUpdate();
	Delay_Init();
	USART_Printf_Init(921600);	
	debug_info("SystemClk:%d MHz",(SystemCoreClock/1000000));
	debug_info("ChipID:%08x", DBGMCU_GetCHIPID());
	debug_printf("\n");
	while(1)
    {
		debug_printf("This is a debug_printf ouput");
		debug_error("This is a debug_error ouput");
		debug_info("This is a debug_info ouput");
		printf("This is a print ouput\r\n");
		Delay_Ms(1000);
	}
}
/**********************************************************UserAddBegin***************************************************************/
#define _DEBUG_LOG_
#ifdef _DEBUG_LOG_

#define debug_printf(s,...)	printf(s"\r\n",##__VA_ARGS__)
#define debug_info(format,...)   printf("[info]:Fun--> <%s()>   Line--> (%d)   Info--> {"format"}\r\n",__func__,__LINE__,##__VA_ARGS__)
#define debug_error(format,...)  printf("[error]:File--> %s   Fun--> <%s()>   Line--> (%d)   Info--> {"format"}\r\n",__FILE__,__func__,__LINE__,##__VA_ARGS__)

#else

#define debug_printf(s,...)
#define debug_info(s,...)
#define debug_error(format,...)

#endif
/**********************************************************UserAddEnd****************************************************************/
        3.串口调试助手显示如下(可输出):
        4.作者认为Debug调试最好的处理代码(不可输出),如下:
/**********************************************************UserAddBegin***************************************************************/
// #define _DEBUG_LOG_r
#ifdef _DEBUG_LOG_

#define debug_printf(s,...)	printf(s"\r\n",##__VA_ARGS__)
#define debug_info(format,...)   printf("[info]:Fun--> <%s()>   Line--> (%d)   Info--> {"format"}\r\n",__func__,__LINE__,##__VA_ARGS__)
#define debug_error(format,...)  printf("[error]:File--> %s   Fun--> <%s()>   Line--> (%d)   Info--> {"format"}\r\n",__FILE__,__func__,__LINE__,##__VA_ARGS__)

#else

#define debug_printf(s,...)
#define debug_info(s,...)
#define debug_error(format,...)

#endif
/**********************************************************UserAddEnd****************************************************************/
        5.串口调试助手显示如下(不可输出):

6.详解:

        我们通过_DEBUG_LOG_这个宏定义对debug_printf(),debug_info(),debug_error()调试信息输出语句进行全局的控制。如果定义了_DEBUG_LOG_宏定义,那么就将这三个语句编译进最终生成的二进制文件中;否则,不编译进最终生成的二进制文件中。从而实现了对Debug调试信息的一键删除或者保留。

        通过上面的代码很容易看出其实ebug_printf(),debug_info(),debug_error()调试信息输出语句其实就是printf的重新定义,但是不同于常规的printf,这里的printf是一种开发人员定制化的语句形式。

        debug_printf:这里使用了可变参数宏,...表示可变参数,而##__VA_ARGS__是用来处理可变参数的部分。当没有可变参数时,##的作用是去掉前面的逗号,防止语法错误。例如,如果调用debug_printf("Hello"),会被展开成printf("Hello""\r\n",),不过因为##的存在,当没有可变参数时,后面的逗号会被删掉,变成printf("Hello\r\n")。这样就避免了编译错误。        

        debug_info:注意到格式字符串中使用了__func__和__LINE__这两个预定义宏,分别代表当前函数名和行号。用户提供的format会被放在大括号{}里面,然后加上换行。可变参数同样用##__VA_ARGS__处理。例如,调用debug_info("value=%d", 5)会被展开成printf("[info]:Fun--> <func_name()> Line--> (123) Info--> {"value=%d"}\r\n", "func_name", 123, 5),        

        debug_error:这里的func_name和123是实际调用位置的函数名和行号。

这里用了__FILE__宏,代表当前源文件的文件名。调用这个宏的时候,会输出错误信息,包含文件名、函数名、行号,以及用户提供的格式和参数。例如,debug_error("open failed")会被展开成printf("[error]:File--> file.c Fun--> <func()> Line--> (456) Info--> {"open failed"}\r\n", "file.c", "func", 456),输出对应的错误信息。

        printf:我们并没有对单独的printf语句做处理,所以,_DEBUG_LOG_这个宏定义对单独使用printf没有任何影响。

        那么这么做的意义想必大家在看完上面的内容应该有所了解了吧,这样处理调试信息更加简单、方便、易用、更直观展示并且信息量也更大,对于开发人员来说是非常有必要的。

声明:转载请注明出处。 

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐