我想尝试使用调试基于微控制器的项目是一件坏事printf()
。
我知道您没有预定义的位置可以输出到该位置,并且它可能会消耗宝贵的引脚。同时,我已经看到人们使用UART TX引脚来通过自定义DEBUG_PRINT()
宏输出到IDE终端。
printf
,当然,实现所需的所有代码printf
都将链接到可执行文件。但这是因为代码使用了它,而不是因为标头。
我想尝试使用调试基于微控制器的项目是一件坏事printf()
。
我知道您没有预定义的位置可以输出到该位置,并且它可能会消耗宝贵的引脚。同时,我已经看到人们使用UART TX引脚来通过自定义DEBUG_PRINT()
宏输出到IDE终端。
printf
,当然,实现所需的所有代码printf
都将链接到可执行文件。但这是因为代码使用了它,而不是因为标头。
Answers:
我可以提出一些使用printf()的缺点。请记住,“嵌入式系统”的范围可以从具有数百字节程序存储器的东西到具有千兆字节RAM和TB级非易失性存储器的成熟的机架安装QNX RTOS驱动的系统。
它需要在某个地方发送数据。也许您已经在系统上具有调试或编程端口,也许您没有。如果您不这样做(或者您所拥有的那个不起作用),那就不是很方便。
它并不是在所有情况下都是轻量级的功能。如果您的微控制器只有几个K的内存,那么这可能是一件大事,因为在printf中进行链接可能会单独吞噬4K。如果您拥有32K或256K微控制器,则可能不是问题,更不用说拥有大型嵌入式系统了。
查找与内存分配或中断有关的某些类型的问题几乎没有用处,或者在不包括语句的情况下可以更改程序的行为。
这对于处理时间敏感的东西是毫无用处的。使用逻辑分析仪和示波器,协议分析仪甚至是模拟器会更好。
如果您有一个大型程序,并且在更改周围的printf语句并进行更改时必须重新编译很多次,则可能会浪费大量时间。
这样做的好处-这是一种以预格式化的方式输出数据的快速方法,每个C程序员都知道如何使用-学习曲线为零。如果您需要为要调试的Kalman滤波器吐出一个矩阵,最好以MATLAB可以读取的格式吐出它。这肯定比在调试器或仿真器中一次查看RAM位置要好。 。
我认为这不是箭袋中的无用箭头,但应与gdb或其他调试器,仿真器,逻辑分析仪,示波器,静态代码分析工具,代码覆盖率工具等一起谨慎使用。
printf()
实现都不是线程安全的(即,不可重入),它不是交易杀手,而是在多线程环境中使用时要记住的一点。
除了一些其他好的答案外,以串行波特率向端口发送数据的行为相对于循环时间而言可能会非常慢,并且会影响其余程序的运行方式(任何调试都可以)处理)。
正如其他人告诉您的那样,使用这种技术没有什么“不好”的地方,但是与许多其他调试技术一样,它也有其局限性。只要您知道并可以解决这些局限性,它就会非常方便地帮助您正确编写代码。
嵌入式系统通常具有一定的不透明性,这使调试成为一个问题。
尝试printf
在微控制器上使用时会遇到两个主要问题。
首先,将输出通过管道传输到正确的端口可能很痛苦。不总是。但是某些平台比其他平台困难。某些配置文件的文档记录可能很差,可能需要进行大量实验。
第二个是记忆。完整的printf
库可以是很大的。有时您不需要所有格式说明符,但可以使用专门的版本。例如,stdio.h
AVR提供的包含三个不同printf
的,大小和功能不同的。
由于所有提到的功能的全部实现都变得很大,
vfprintf()
因此可以使用链接器选项选择三种不同的口味。默认值vfprintf()
实现除浮点转换之外的所有上述功能。提供了一个最小化版本,vfprintf()
该版本仅实现了非常基本的整数和字符串转换功能,但是只能#
使用转换标志来指定附加选项(这些标志已从格式规范中正确解析,但随后被忽略)。
我有一个实例,其中没有库可用,并且内存最少。因此,我别无选择,只能使用自定义宏。但是,是否使用printf
确实是满足您要求的一种。
为了补充Spehro Pefhany所说的“对时间敏感的东西”:让我们举个例子。假设您有一个陀螺仪,您的嵌入式系统每秒从该陀螺仪进行1000次测量。您想调试这些测量,所以需要将它们打印出来。问题:将它们打印出来会导致系统太忙,无法每秒读取1,000个测量值,这会导致陀螺仪的缓冲区溢出,从而导致读取(并打印)损坏的数据。因此,通过打印数据,您已损坏了数据,使您认为实际上可能没有读取数据时存在错误。所谓的heisenbug。
不使用printf()进行调试的更大原因是,它通常效率低下,不足且不必要。
效率低下:相对于小型微控制器,printf()和Kin使用大量闪存和RAM,但实际调试中效率较低。更改记录的内容需要重新编译和重新编程目标,这会减慢该过程。它还会用完一个UART,否则您可能会用它来完成有用的工作。
不足:您只能通过串行链接输出太多细节。如果程序挂起,您将不知道确切的位置,只有最后完成的输出。
不必要:可以对许多微控制器进行远程调试。JTAG或专有协议可用于暂停处理器,查看寄存器和RAM,甚至更改正在运行的处理器的状态而无需重新编译。这就是为什么即使在具有大量空间和功能的PC上,调试器通常也比打印语句更好的调试方法。
不幸的是,对于新手来说,最常见的微控制器平台Arduino没有调试器。AVR支持远程调试,但是Atmel的debugWIRE协议是专有的,没有记录。您可以使用官方的开发板来使用GDB进行调试,但是如果您有这种担心,那么您可能就不必再担心Arduino了。
printf()不能单独工作。它调用了许多其他函数,并且如果堆栈空间很小,则可能根本无法使用它来调试接近堆栈限制的问题。根据编译器和微控制器的不同,格式字符串也可以放置在内存中,而不是从闪存中引用。如果您在代码中加上printf语句,则可能会加起来很大。这是Arduino环境中的一个大问题-使用数十或数百个printf语句的初学者突然遇到看似随机的问题,因为他们正在用堆栈覆盖其堆。
即使想要将数据吐出到某种形式的日志控制台,该printf
功能通常也不是一种很好的方法,因为它需要检查传递的格式字符串并在运行时对其进行解析;即使代码从不使用以外的任何格式说明符%04X
,控制器通常也将需要包含解析任意格式字符串所需的所有代码。根据使用的确切控制器,使用类似以下代码的效率可能更高:
void log_string(const char *st)
{
int ch;
do
{
ch = *st++;
if (ch==0) break;
log_char(ch);
} while(1);
}
void log_hexdigit(unsigned char d)
{
d&=15;
if (d > 9) d+=7;
log_char(d+'0');
}
void log_hexbyte(unsigned char b)
{ log_hexdigit(b >> 4); log_hexdigit(b); }
void log_hexi16(uint16_t s)
{ log_hexbyte(s >> 8); log_hexbyte(s); }
void log_hexi32(uint32_t i)
{ log_hexbyte(i >> 24); log_hexbyte(i >> 16); log_hexbyte(i >> 8); log_hexbyte(i); }
void log_hexi32p(uint32_t *p) // On a platform where pointers are less than 32 bits
{ log_hexi32(*p); }
在某些PIC单片机上,log_hexi32(l)
可能需要9条指令,可能需要17条指令(如果l
在第二个存储区中),而log_hexi32p(&l)
可能需要2 log_hexi32p
条指令。该函数本身可以写成大约14条指令,因此,如果调用两次,它将为自己付费。
有一个其他答案都没有提到的一点:在基本的微型程序中(例如,只有main()循环,并且可能随时运行几个ISR,而不是多线程OS),如果它崩溃/停止/获取卡在一个循环中,您的打印功能将根本不会发生。
另外,人们说“不要使用printf”或“ stdio.h占用大量空间”,但没有给出太多替代方案-Embedded.kyle提到了简化的替代方案,而这恰恰是您应该做的事情当然可以在基本的嵌入式系统上进行。从UART中喷出几个字符的基本例程可能是几个字节的代码。