没有使用标准库,能否在C/C++中将内容输出到控制台?

12
我正在使用ARM微处理器编程,并尝试通过UART使用打印语句进行调试。 我不想仅为了调试而添加库。 有没有一种方法可以在没有/的情况下打印到控制台? 是否有可能编写自己的函数?
或者,我可以使用DMA控制器并直接写入UART来完成此操作。 但是如果可能的话,我想避免这样做。 使用内置的测试功能“回显”或“远程循环”我知道UART已正确配置。

2
是的,这是可能的 - 你可以编写自己的输出例程,找到一个小型的独立的partial printf()实现,或者编写必要的后端支持来启用这些函数从嵌入式libc(可能随同你的工具链一起提供)运行在你的平台上。 - Chris Stratton
谢谢。我听说嵌入式libc的新库还不错。不过,我首先会寻找部分printf()。 - Sam
@ChrisStratton:这实际上取决于操作系统。原生的操作系统例程可能就是标准库。 - Mooing Duck
4个回答

10

简短回答:是的,完全可以同时实现你的两种解决方案。

如果您想支持所有数据类型和格式,printf函数会相当复杂。但是,写出能够输出字符串或整数的代码并在几个不同的进制下运行并不难(大多数人只需要十进制和十六进制,但是有了十进制和十六进制后,八进制可能只需要另外3-4行代码)。

通常,printf的编写方式如下:

 int printf(const char *fmt, ...)
 {
     int ret;
     va_list args; 

     va_start(args, fmt)
     ret = do_xprintf(outputfunc, NULL, fmt, args); 
     va_end(args);
     return ret;
}

然后do_xprintf()为所有变体(如printf,sprintf,fprintf等)完成所有繁重的工作

int do_xprintf(void (*outputfunc)(void *extra, char c), void *extra, const char *fmt, va_list args)
{
    char *ptr = fmt;
    while(1)
    {
       char c = *ptr++;

       if (c == '%')
       {
            c = *ptr++; // Get next character from format string. 
            switch(c)
            {
               case 's': 
                  char *str = va_arg(args, const char *);
                  while(*str)
                  {
                      count++;
                      outputfunc(extra, *str);
                      str++;
                  }
                  break; 
               case 'x': 
                  base = 16;
                  goto output_number;

               case 'd':
                  base = 10;
         output_number:
                  int i = va_arg(args, int);
                  // magical code to output 'i' in 'base'. 
                  break;

               default:
                  count++;
                  outputfunc(extra, c);
                  break;
         }
         else
             count++;
             outputfunc(extra, c);
     }
     return count;
 }                

现在,您只需要填写以上代码的一些部分并编写一个outputfunc()函数,将其输出到串行端口即可。

请注意这只是粗略的草图,我相信代码中可能会有一些错误 - 如果您想支持浮点数或“宽度”,则需要做更多的工作......

(关于额外参数的说明 - 对于FILE *输出,应该是文件指针;对于sprintf,您可以传递缓冲区和缓冲区中的位置等结构体信息)


非常感谢。我是少数只需要十进制/十六进制的人之一。我正在使用定点表示法,并希望验证我的结果。这将使事情变得更容易。感谢您的帮助! - Sam
为什么output_number是一个case语句内的标签,而不是一个普通函数呢? - Lundin
@Lundin:只是因为打字更短。除非编译器真的很聪明并意识到两个函数调用之间唯一的区别是输入参数,否则它也可能会产生更短的代码。 - Mats Petersson

3
“控制台”这个概念在特定系统的上下文之外并没有太大意义。通常,在嵌入式程序中,没有真正的控制台概念。
您需要找到一种从系统中获取数据的方法。如果您想使用UART,并且没有使用像GNU / Linux这样的高级操作系统,则需要编写自己的I / O驱动程序。通常,这意味着通过寄存器写入首先配置所需的波特率/奇偶校验/流控制的UART。对于任何类型的强大IO,您都希望它是中断驱动的,因此您需要编写tx和rx的ISRs,利用循环缓冲区。
完成后,您可以编写类似Mats所示的自己的printf。

谢谢!我明白了。最初我试图解决这个问题,因为我遇到了一些问题。但是我发现,对于我的处理器,在每次写入tx缓冲区后,我必须访问状态寄存器。如果不这样做,它会停止运行。现在我正在处理波特率问题,但是一旦我解决了问题,我肯定会使用Mats的东西。感谢您的帮助。 - Sam

2
由于通过嵌入式系统中的串口打印信息会修改主程序的时间,我发现最好的解决方案是发送一个由2个字节编码的小消息(有时1个字节也可以),然后使用PC上的程序来解码这些消息并提供必要的信息,包括统计数据和任何您可能需要的内容。这样,我只在主程序中添加了一点点开销,并让PC处理消息的艰苦任务。也许可以像这样做:
- 1个字节的消息:位7:4 = 模块ID,位3:0 = 调试信息。 - 2个字节的消息:位15:12 = 模块ID,位11:8 = 调试信息,位7:0 = 数据。
然后,在PC软件中,您需要声明一个表格,其中包含将映射到给定模块ID/调试信息对的消息,并使用它们打印在屏幕上。虽然这种方法不像类似于伪printf函数那样灵活,因为您需要在PC上有一个固定的消息集来解码,但它不会增加太多开销,正如我之前提到的一样。希望能帮到您。Fernando

1
我发现将字符排队到循环缓冲区中,然后通过uart传输寄存器上的轮询例程进行排空是我的首选方法来进行背景调试。
排队例程基于字符、字符串和可变大小(转换为十六进制或定宽十进制)。豪华缓冲例程可以使用保留字符指示溢出。
这种方法对目标操作的开销/影响最小,可以在中断例程中谨慎使用,并且这个想法易于转移,因此我已经忽略了我使用过的所有系统上的调试器。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接