为什么printf()对调试嵌入式系统不利?


16

我想尝试使用调试基于微控制器的项目是一件坏事printf()

我知道您没有预定义的位置可以输出到该位置,并且它可能会消耗宝贵的引脚。同时,我已经看到人们使用UART TX引脚来通过自定义DEBUG_PRINT()宏输出到IDE终端。


12
谁告诉你这很糟糕?“通常不是最好的”与不合格的“坏”是不同的。
Spehro Pefhany 2014年

6
所有这些都讨论了有多少开销,如果您需要做的只是输出“我在这里”消息,则根本不需要printf,而只是一个将字符串发送到UART的例程。这样,加上初始化UART的代码可能不到100个字节的代码。增加输出几个十六进制值的能力并不会增加太多。
tcrosley 2014年

7
@ChetanBhargava-C头文件通常不向可执行文件添加代码。它们包含声明;如果其余代码未使用声明的内容,则不会链接这些内容的代码。如果使用printf,当然,实现所需的所有代码printf都将链接到可执行文件。但这是因为代码使用了它,而不是因为标头。
Pete Becker

2
@ChetanBhargava你并不需要,甚至还包括<stdio.h>中,如果你滚你自己的简单的常规输出像我所描述的字符串(字符输出到UART,直到你看到一个“\ 0”)“
tcrosley

2
@tcrosley我认为,如果您有一个好的现代编译器,并且在没有格式字符串gcc的简单情况下使用printf,而大多数其他人则用更有效的简单调用替换了它,它可能起了很多作用。

Answers:


24

我可以提出一些使用printf()的缺点。请记住,“嵌入式系统”的范围可以从具有数百字节程序存储器的东西到具有千兆字节RAM和TB级非易失性存储器的成熟的机架安装QNX RTOS驱动的系统。

  • 它需要在某个地方发送数据。也许您已经在系统上具有调试或编程端口,也许您没有。如果您不这样做(或者您所拥有的那个不起作用),那就不是很方便。

  • 它并不是在所有情况下都是轻量级的功能。如果您的微控制器只有几个K的内存,那么这可能是一件大事,因为在printf中进行链接可能会单独吞噬4K。如果您拥有32K或256K微控制器,则可能不是问题,更不用说拥有大型嵌入式系统了。

  • 查找与内存分配或中断有关的某些类型的问题几乎没有用处,或者在不包括语句的情况下可以更改程序的行为。

  • 这对于处理时间敏感的东西是毫无用处的。使用逻辑分析仪和示波器,协议分析仪甚至是模拟器会更好。

  • 如果您有一个大型程序,并且在更改周围的printf语句并进行更改时必须重新编译很多次,则可能会浪费大量时间。

这样做的好处-这是一种以预格式化的方式输出数据的快速方法,每个C程序员都知道如何使用-学习曲线为零。如果您需要为要调试的Kalman滤波器吐出一个矩阵,最好以MATLAB可以读取的格式吐出它。这肯定比在调试器或仿真器中一次查看RAM位置要好。 。

我认为这不是箭袋中的无用箭头,但应与gdb或其他调试器,仿真器,逻辑分析仪,示波器,静态代码分析工具,代码覆盖率工具等一起谨慎使用。


3
大多数printf()实现都不是线程安全的(即,不可重入),它不是交易杀手,而是在多线程环境中使用时要记住的一点。
JRobert

1
@JRobert提出了一个很好的观点..即使在没有操作系统的环境中,也很难对ISR进行很多有用的直接调试。当然,如果您在ISR中执行printf()或浮点数学运算,则该方法可能已关闭。
Spehro Pefhany 2014年

@JRobert在多线程环境(在无法使用逻辑分析仪和示波器的硬件设置中)工作的软件开发人员有哪些调试工具?
Minh Tran

1
过去,我已经开发了自己的线程安全的printf();。使用赤脚puts()或putchar()等价物将非常简洁的数据吐出到终端;将二进制数据存储在测试运行后转储并解释的数组中;使用I / O端口使LED闪烁或生成脉冲以使用示波器进行定时测量;向D / A吐出一个数字并用VOM进行测量...该列表与您的想象力一样长,而与预算一样大!:)
JRobert

19

除了一些其他好的答案外,以串行波特率向端口发送数据的行为相对于循环时间而言可能会非常慢,并且会影响其余程序的运行方式(任何调试都可以)处理)。

正如其他人告诉您的那样,使用这种技术没有什么“不好”的地方,但是与许多其他调试技术一样,它也有其局限性。只要您知道并可以解决这些局限性,它就会非常方便地帮助您正确编写代码。

嵌入式系统通常具有一定的不透明性,这使调试成为一个问题。


8
+1为“嵌入式系统具有一定的不透明度”。尽管我担心只有在嵌入式方面有丰富经验的人才能理解此声明,但它确实为情况提供了一个简洁明了的总结。实际上,它接近“嵌入式”的定义。
njahnke 2014年

5

尝试printf在微控制器上使用时会遇到两个主要问题。

首先,将输出通过管道传输到正确的端口可能很痛苦。不总是。但是某些平台比其他平台困难。某些配置文件的文档记录可能很差,可能需要进行大量实验。

第二个是记忆。完整的printf库可以是很大的。有时您不需要所有格式说明符,但可以使用专门的版本。例如,stdio.h AVR提供的包含三个不同printf的,大小和功能不同的。

由于所有提到的功能的全部实现都变得很大,vfprintf()因此可以使用链接器选项选择三种不同的口味。默认值vfprintf()实现除浮点转换之外的所有上述功能。提供了一个最小化版本,vfprintf()该版本仅实现了非常基本的整数和字符串转换功能,但是只能#使用转换标志来指定附加选项(这些标志已从格式规范中正确解析,但随后被忽略)。

我有一个实例,其中没有库可用,并且内存最少。因此,我别无选择,只能使用自定义宏。但是,是否使用printf确实是满足您要求的一种。


请下注者解释一下我的答案中有什么不正确,以便在以后的项目中避免我的错误吗?
Embedded.kyle

4

为了补充Spehro Pefhany所说的“对时间敏感的东西”:让我们举个例子。假设您有一个陀螺仪,您的嵌入式系统每秒从该陀螺仪进行1000次测量。您想调试这些测量,所以需要将它们打印出来。问题:将它们打印出来会导致系统太忙,无法每秒读取1,000个测量值,这会导致陀螺仪的缓冲区溢出,从而导致读取(并打印)损坏的数据。因此,通过打印数据,您已损坏了数据,使您认为实际上可能没有读取数据时存在错误。所谓的heisenbug。


大声笑!“ heisenbug”真的是一个技术术语吗?我想这与粒子状态的测量和海森堡原理有关
Zeta.Investigator

3

不使用printf()进行调试的更大原因是,它通常效率低下,不足且不必要。

效率低下:相对于小型微控制器,printf()和Kin使用大量闪存和RAM,但实际调试中效率较低。更改记录的内容需要重新编译和重新编程目标,这会减慢该过程。它还会用完一个UART,否则您可能会用它来完成有用的工作。

不足:您只能通过串行链接输出太多细节。如果程序挂起,您将不知道确切的位置,只有最后完成的输出。

不必要:可以对许多微控制器进行远程调试。JTAG或专有协议可用于暂停处理器,查看寄存器和RAM,甚至更改正在运行的处理器的状态而无需重新编译。这就是为什么即使在具有大量空间和功能的PC上,调试器通常也比打印语句更好的调试方法。

不幸的是,对于新手来说,最常见的微控制器平台Arduino没有调试器。AVR支持远程调试,但是Atmel的debugWIRE协议是专有的,没有记录。您可以使用官方的开发板来使用GDB进行调试,但是如果您有这种担心,那么您可能就不必再担心Arduino了。


您不能使用函数指针来处理正在记录的内容,并增加一堆灵活性吗?
Scott Seidman 2014年

3

printf()不能单独工作。它调用了许多其他函数,并且如果堆栈空间很小,则可能根本无法使用它来调试接近堆栈限制的问题。根据编译器和微控制器的不同,格式字符串也可以放置在内存中,而不是从闪存中引用。如果您在代码中加上printf语句,则可能会加起来很大。这是Arduino环境中的一个大问题-使用数十或数百个printf语句的初学者突然遇到看似随机的问题,因为他们正在用堆栈覆盖其堆。


2
尽管我很感谢downvote本身提供的反馈,但是如果不同意的人解释了此答案的问题,对我和其他人会更有帮助。我们都在这里学习和分享知识,考虑分享您的知识。
亚当·戴维斯

3

即使想要将数据吐出到某种形式的日志控制台,该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条指令,因此,如果调用两次,它将为自己付费。


2

有一个其他答案都没有提到的一点:在基本的微型程序中(例如,只有main()循环,并且可能随时运行几个ISR,而不是多线程OS),如果它崩溃/停止/获取卡在一个循环中,您的打印功能将根本不会发生

另外,人们说“不要使用printf”或“ stdio.h占用大量空间”,但没有给出太多替代方案-Embedded.kyle提到了简化的替代方案,而这恰恰是您应该做的事情当然可以在基本的嵌入式系统上进行。从UART中喷出几个字符的基本例程可能是几个字节的代码。


如果您的printf没有发生,您已经学到了很多有关代码存在问题的地方。
Scott Seidman 2014年

假设可能只有一个printf,是的。但是在调用printf()将所有内容从UART中取出时,中断可能会触发数百次
John U
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.