snprintf()函数在使用newlib nano库时打印出垃圾浮点数

10

我正在运行一个带有ARM Cortex-M3 (STM32F205)的裸机嵌入式系统。当我尝试使用snprintf()函数处理浮点数时,例如:

float f;

f = 1.23;
snprintf(s, 20, "%5.2f", f);

我得到了垃圾数据存入s中。格式似乎是正确的,也就是说,垃圾数据是一个由数字、小数点和两位小数组成的格式良好的字符串。然而,如果我重复使用snprintf函数,该字符串可能会在两次调用之间更改。

浮点数运算似乎工作正常,并且snprintf函数适用于整数,例如:

snprintf(s, 20, "%10d", 1234567);

我使用带有-u _printf_float链接器开关的newlib-nano实现。编译器是arm-none-eabi-gcc

我非常怀疑存在内存分配问题,因为整数可以正常打印,而浮点数则好像在过程中被损坏了。 printf系列函数调用malloc处理浮点数而不是整数。

在这个上下文中我所使用的唯一不属于newlib的代码片段是我的_sbrk(),它是malloc所需的。

caddr_t _sbrk(int incr)
{
  extern char _Heap_Begin; // Defined by the linker.
  extern char _Heap_Limit; // Defined by the linker.

  static char* current_heap_end;
  char* current_block_address;

  // first allocation
  if (current_heap_end == 0)
      current_heap_end = &_Heap_Begin;

  current_block_address = current_heap_end;

  // increment and align to 4-octet border
  incr = (incr + 3) & (~3);
  current_heap_end += incr;

  // Overflow?
  if (current_heap_end > &_Heap_Limit)
    {
    errno = ENOMEM;
    current_heap_end = current_block_address;
    return (caddr_t) - 1;
    }

  return (caddr_t)current_block_address;
}
据我跟踪的情况,这应该是有效的。似乎从来没有人使用负增量进行调用,但我猜这是由于newlib的malloc设计。唯一有点奇怪的是第一次对_sbrk的调用具有零增量。(但这可能只是malloc对堆起始地址的好奇心。)堆和栈不应该相互冲突,因为两者共有大约60 KiB的RAM。链接脚本可能有点疯狂,但至少堆和栈的地址似乎是正确的。

2
请注意这些是“double”,而不是“float”。不知道这是否重要,反正你无论如何都不能将“float”传递给“snprintf()”。 - unwind
4
snprintf() 的原型是 int snprintf(char *restrict s, size_t n, const char *restrict format, ...);。你调用函数的方式与原型不符。请确认是否已经添加了头文件 <stdio.h>,并开启了所有的编译警告。 - pmg
1
确定你的正确代码使用的是 snprintf() 而不是 sprintf() 吗? - chux - Reinstate Monica
2
我对实现细节不是太熟悉,但以下内容引起了一些轻微的怀疑:%f 通常涉及将 float 转换为 double;EABI 强制要求 double 的 8 字节对齐;而你的 _sbrk() 只在分配时强制执行 4 字节对齐。这对于 printf()malloc() 的内部机制有多大影响可能取决于具体情况,但进行实验应该很简单。 - Notlikethat
你能告诉我们添加了浮点支持到printf的-u_printf_float会占用多少额外的闪存空间吗?我正试图为一个16kB的芯片(STM32F030F4P6)进行编译,但是二进制文件似乎太大了(约20 kB)。 - Christoph
显示剩余5条评论
2个回答

14

由于可能会有其他人遇到同样的问题,我在自己的问题下发布了答案。不过,是@Notlikethat的评论提供了正确的答案。

这是“你不可偷窃”的一课。我借用了STMCubeMX代码生成器附带的gcc链接脚本。不幸的是,该脚本连同启动文件一起损坏了。

原始链接脚本的相关部分:

_estack = 0x2000ffff;

以及在启动脚本中的对应项:

Reset_Handler:  
  ldr   sp, =_estack     /* set stack pointer */
...

g_pfnVectors:
  .word  _estack
  .word  Reset_Handler
...

第一个中断向量位置(0)应始终指向启动堆栈顶部。当达到复位中断时,它还会加载堆栈指针。(据我所知,后者是不必要的,因为硬件在调用复位处理程序之前无论如何都会从第0个向量重新加载SP。)

Cortex-M堆栈指针应始终指向堆栈中的最后一项。在启动时,堆栈中没有任何项,因此指针应指向实际内存上方的第一个地址,例如0x020010000。使用原始链接器脚本将堆栈指针设置为0x0200ffff,实际结果是sp = 0x0200fffc(硬件强制进行字对齐堆栈)。之后,堆栈将被错误地对齐4个单位。

我通过删除_estack的常量定义并将其替换为下面显示的_stacktop来更改了链接器脚本。内存定义已经存在。我只是改变了名称以查看该值的使用情况。

MEMORY
{
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 128K
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 64K
}

_stacktop = ORIGIN(RAM) + LENGTH(RAM);

此后_stacktop的值为0x20010000,我的数字漂亮地浮动着...任何使用双精度参数的外部(库)函数都可能出现同样的问题,因为ARM Cortex ABI规定,在调用外部函数时,堆栈必须对齐到8个八位字节。


在Nucleo F401的CubeMX上,STM32示例有同样的问题,您为我节省了很多时间。谢谢。 - rom1nux
我在我的psoc上遇到了同样的问题,它也使用了newlib-nano。你能否详细说明一下你是如何解决的?我之前没有调整过链接脚本的经验。 - Typhaon

1

snprintf的第二个参数是大小。您可能需要查看此示例http://www.cplusplus.com/reference/cstdio/snprintf/

/* snprintf example */
#include <stdio.h>

int main ()
{
  char buffer [100];
  int cx;

  cx = snprintf ( buffer, 100, "The half of %d is %d", 60, 60/2 );

  snprintf ( buffer+cx, 100-cx, ", and the half of that is %d.", 60/2/2 );

  puts (buffer);

  return 0;
}

好的回答,是的,我真的可能犯了那个错误...但在那种情况下,gcc会(再次)告诉我我很愚蠢。所以,这次问题似乎更深层次地存在于newlib中 - 或者可能存在于链接器脚本中。 - DrV

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