snprintf() prints garbage floats with newlib nano
Asked Answered
D

2

10

I am running a bare metal embedded system with an ARM Cortex-M3 (STM32F205). When I try to use snprintf() with float numbers, e.g.:

float f;

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

I get garbage into s. The format seems to be honored, i.e. the garbage is a well-formed string with digits, decimal point, and two trailing digits. However, if I repeat the snprintf, the string may change between two calls.

Floating point mathematics seems to work otherwise, and snprintf works with integers, e.g.:

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

I use the newlib-nano implementation with the -u _printf_float linker switch. The compiler is arm-none-eabi-gcc.

I do have a strong suspicion of memory allocation problems, as integers are printed without any hiccups, but floats act as if they got corrupted in the process. The printf family functions call malloc with floats, not with integers.

The only piece of code not belonging to newlib I am using in this context is my _sbrk(), which is required by 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;
}

As far as I have been able to track, this should work. It seems that no-one ever calls it with negative increments, but I guess that is due to the design of the newlib malloc. The only slightly odd thing is that the first call to _sbrk has a zero increment. (But this may be just malloc's curiosity about the starting address of the heap.)

The stack should not collide with the heap, as there is around 60 KiB RAM for the two. The linker script may be insane, but at least the heap and stack addresses seem to be correct.

Duyne answered 26/2, 2015 at 15:15 Comment(10)
Note that those are doubles, not floats. No idea if it matters, you can't pass a float to snprintf() anyway.Anglim
The prototype for snprintf() is int snprintf(char *restrict s, size_t n, const char *restrict format, ...); You're not calling the function according to the prototype. Did you #include <stdio.h> and compiled with all warnings enabled?Winnipegosis
Sure your true code is using snprintf() and not sprintf()?Beadruby
@unwind: A good point, but AFAIK printf is a variadic function automatically promoting floats to doubles. (At least gcc does not complain about the formats with its pedantic settings.) With my original example (literal constant 1.23) the argument is a double anyway, but in the revised example it is a single-precision float. I am not sure about newlib nano's innards, I would guess it rather keeps floats as floats because doubles are quite laborious in embedded systems (but this is just a guess).Duyne
@chux: Yep. If I have enough performance to use any printf in an embedded system, I have enough performance to count the characters to avoid any stupid accidents. IMHO, sprintf should not exist; it is just plain dangerous.Duyne
It is just that you had snprintf(s, "%.2f", 1.23) and I was suspecting that instead of not getting/ignoring compiler warnings, there was a code cut/paste error.Beadruby
@pmg: Thank you for spotting the typo! Yes, almost all warnings are on, as well as -pedantic. I have been forced to drop the -Werror, as I use the STM32 HAL, which unfortunately triggers an awful number of padding and conversion warnings.Duyne
I'm not too familiar with the implementation details, but the following raises some mild suspicions: %f normally involves converting float to double; the EABI mandates 8-byte alignment for double; your _sbrk() only enforces 4-byte alignment on what it hands out. How much that matters probably depends on the guts of printf() and malloc(), but it should be straightforward to experiment with.Whipperin
@Notlikethat: That sounds possible. I did not mention it in my question, but I actually tried an 8-octet aligned heap without any change. However, as the arguments are passed in stack, this might be a stack alignment problem. I will have to check that path as well, thanks!Duyne
Can you tell us how much extra flash space is occupied by adding floating point support to printf with -u _printf_float? I'm trying to compile for a 16kB part (STM32F030F4P6) and the binary seems to be too large (about 20 kB).Saber
D
14

As it may happen that someone else gets bitten by the same bug, I post an answer to my own question. However, it was @Notlikethat 's comment which suggested the correct answer.

This is a lesson of Thou shall not steal. I borrowed the gcc linker script which came with the STMCubeMX code generator. Unfortunately, the script along with the startup file is broken.

The relevant part of the original linker script:

_estack = 0x2000ffff;

and its counterparts in the startup script:

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

g_pfnVectors:
  .word  _estack
  .word  Reset_Handler
...

The first interrupt vector position (at 0) should always point to the startup stack top. When the reset interrupt is reached, it also loads the stack pointer. (As far as I can say, the latter one is unnecessary as the HW anyway reloads the SP from the 0th vector before calling the reset handler.)

The Cortex-M stack pointer should always point to the last item in the stack. At startup there are no items in the stack and thus the pointer should point to the first address above the actual memory, 0x020010000 in this case. With the original linker script the stack pointer is set to 0x0200ffff, which actually results in sp = 0x0200fffc (the hardware forces word-aligned stack). After this the stack is misaligned by 4.

I changed the linker script by removing the constant definition of _estack and replacing it by _stacktop as shown below. The memory definitions were there before. I changed the name just to see where the value is used.

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

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

After this the value of _stacktop is 0x20010000, and my numbers float beautifully... The same problem could arise with any external (library) function using double length parameters, as the ARM Cortex ABI states that the stack must be aligned to 8 octets when calling external functions.

Duyne answered 27/2, 2015 at 9:42 Comment(2)
Same problem with STM32 sample on CubeMX for Nucleo F401,You save me a lot of time. ThanksBenthamism
I got the same problem with my psoc, which also uses newlib-nano. Is there a chance that you can elaborate on how you fixed it. I haven't worked with adjusting linker scripts beforeAult
S
1

snprintf accepts size as second argument. You might want to go through this example 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;
}
Scalariform answered 26/2, 2015 at 16:3 Comment(1)
Good answer, and yes, I could have really made that mistake... But in that case gcc would have (once again) told me I am stupid. So, this time the problem seems to lie somewhere deeper in newlib - or possibly in linker scripts.Duyne

© 2022 - 2024 — McMap. All rights reserved.