8
votes

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.

2
Note that those are doubles, not floats. No idea if it matters, you can't pass a float to snprintf() anyway.unwind
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?pmg
Sure your true code is using snprintf() and not sprintf()?chux - Reinstate Monica
@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).DrV
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.Notlikethat

2 Answers

12
votes

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.

1
votes

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;
}