简体   繁体   中英

Application hangs when calling printf to uart with bare metal raspberry pi

I am trying to implement a bare metal application on the raspberry pi and want to hook up stdout to the mini uart for debugging purposes.

I have followed the process outlined here and here

I have created a uart_putc function which seems to work perfectly, allowing me to print messages to my PC's COM port. I then implemented the _write syscall, making it call my uart_putc function for output. This works fine if I pass a single string literal into printf additional literal parameters or any non-literal parameters nothing is printed to the serial port and after a few calls, the application hangs.

Does anyone have any ideas what might be going wrong? Happy to provide further info if needed...

void uart_putc(char c)
{
    while(1)
    {
        if(aux[AUX_MU_LSR]&0x20) break;

        led_blink(); // Blink the LED off then on again to 
                     // make sure we aren't stuck in here
    }
    aux[AUX_MU_IO] = c;
}

...

int _write(int file, char* ptr, int len)
{
    int todo;

    for (todo = 0; todo < len; todo++) 
    {
        uart_putc(*ptr++);
    }
    return len;
}

...

while(1)
{
    printf("Hello World\r\n"); // Works
}

while(1)
{
    printf("Hello %s\r\n", "World"); // This doesn't print anything
                                     // and will hang after about five calls
}

char* s = (char*)malloc(sizeof(char) * 100); // Heap or stack it doesn't matter
strcpy(s, "Hello World\r\n");
while(1)
{
    printf(s); // This doesn't print anything
               // and will hang after about five calls
}

while(1)
{
    for (i = 0; i < 13; i++) 
    {
        uart_putc(s[i]);  // Works
    }
}

Update

I am using newlib and _write works correctly when called directly. snprintf seems to exhibit the same problem, ie

snprintf(s, 100, "hello world\r\n"); // Works
snprintf(s, 100, "hello %s\r\n", "world"); // Doesn't work

My implementation of _sbrk was nicked from the page referenced in my OP

char *heap_end = 0;
caddr_t _sbrk(int incr) {
    extern char heap_low; /* Defined by the linker */
    extern char heap_top; /* Defined by the linker */
    char *prev_heap_end;

    if (heap_end == 0)
    {
        heap_end = &heap_low;
    }
    prev_heap_end = heap_end;

    if (heap_end + incr > &heap_top)
    {
        /* Heap and stack collision */
        return (caddr_t)0;
    }

    heap_end += incr;
    return (caddr_t) prev_heap_end;
 }

Linker script

OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm",
          "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib");
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x8000)); . = SEGMENT_START("text-segment", 0x8000);
. = 0x8000;
 .ro : {
  *(.text.startup)
  *(.text)
  *(.rodata)
 }
 .rw : {
  *(.data)
  __bss_start__ = .;
  *(.bss)
  __bss_end__ = .;
  *(COMMON)
 }
 . = ALIGN(8);
 heap_low = .; /* for _sbrk */
 . = . + 0x10000; /* 64kB of heap memory */
 heap_top = .; /* for _sbrk */
 . = . + 0x10000; /* 64kB of stack memory */
 stack_top = .; /* for startup.s */
}

start.s

.section ".text.startup"

.global _start

_start:
    ldr sp, =stack_top

    // The c-startup
    b       _cstartup

_inf_loop:
    b       _inf_loop

Update 2

Further experiments involving snprintf:

snprintf(s, 100, "hello world\r\n"); // Works

snprintf(s, 100, "hello %s\r\n", "world"); // Doesn't work
snprintf(s, 100, "hello %d\r\n", 1); // Doesn't work

char s[100];
char t[100];

strcpy(s, "hello world\r\n");
snprintf(t, 100, s); // Doesn't work

This does not look like a UART problem, but rather a library problem.

If you want to make sure my assumption is correct, call _write() directly and see, if it works. Most probably it will. Also, I assume you are using newlib .

If _write() works as expected, the problem is limited into the upper layers of printf . Unfortunately printf is like an onion, you have to peel it layer-by-layer, and it'll make you cry.

Just for fun, a snippet from the newlib source code:

/*
 * Actual printf innards.
 *
 * This code is large and complicated...
 */

Luckily, there are still some ways to debug the problem without getting lost into vfprintf.c . Maybe the easiest starting point is to try snprintf() , because it lacks the memory management problems. The memory allocation code includes things such as sbrk which may be one problem. One would be tempted to think that the memory management is ok, as malloc() seemingly works, but that is not always the case. ( malloc() may look ok even if it gives wrong addresses, but memory corruption will happen.)

Let us know where you get with these debugging steps! (My educated guess is that sbrk does not work for some reason, and that wrecks memory management.)


Update As it seems that the problem is not in memory allocation -- at least not only in memory allocation -- we need to tackle the onion. I hope you are not wearing too heavy make-up... (This makes me cry, and I am not 100 % sure the analysis below is correct. So take it with a pinch of salt.)

What happens in newlib when printf is called? The story is in the newlib source in folder newlib/libc/stidio .

Layer 1: printf()

First, printf.c :

int
_DEFUN(printf, (fmt),
       const char *__restrict fmt _DOTS) 
{
  int ret;
  va_list ap;
  struct _reent *ptr = _REENT;

  _REENT_SMALL_CHECK_INIT (ptr);
  va_start (ap, fmt);
  ret = _vfprintf_r (ptr, _stdout_r (ptr), fmt, ap);
  va_end (ap);
  return ret;
}

Quite simple. If something is going wrong, it is either:

  • _REENT_SMALL_CHECK_INIT(ptr); or
  • handling of varargs

I don't think the re-entrancy is a problem here, so I would concentrate on the varargs. Maybe it would be a good idea to make a minimal varargs test code, which would then show if they are broken. (I do not see why they would be broken, but in an embedded system it is safer not to assume anything.)

Layer 2: _vfprintf_r()

This is an internal version of standard vfprintf (varargs-version of file- printf ) with re-entrant code. It is defined in vfprintf.c . It comes in several flavours depending on which switches have been used during the library compilation: STRING_ONLY (no memory allocation) and/or NO_FLOATING_POINT . I'll assume you have the full version, in which case the correct function can be found under the name _VFPRINTF_R (some #define ing has been going on).

The code is not too easy to read, first few hundred lines of the function consist of declaring many variables (depending on the compile options) and a dozen or so macros. However, the first thing the function really does is an endless loop to scan the format string for % . When it finds a \\0 instead, it does goto done; (yeah, it's got goto s, as well -- I'd like to throw this to code review...)

However, this gives us a clue: if we do not put any extra arguments, we just jump to done where we have some cleanup. This we can survive, but not the handling of any format arguments. So, let us look at where %s would end up. This is done as one would expect, there is a big switch(ch) ... . At s it says:

    case 's':
        cp = GET_ARG (N, ap, char_ptr_t);
        sign = '\0';
        if (prec >= 0) {
            /*
             * can't use strlen; can only look for the
             * NUL in the first `prec' characters, and
             * strlen () will go further.
             */
            char *p = memchr (cp, 0, prec);

            if (p != NULL) {
                size = p - cp;
                if (size > prec)
                    size = prec;
            } else
                size = prec;
        } else
            size = strlen (cp);

        break;

(Now, I have assumed you do not have multibyte string support MB_CAPABLE switched on in your newlib . If you have, the thing just got much more complicated.) The rest looks easy to debug ( strlen and memchr ), but the GET_ARG macro may be complicated -- again depending on your compile settings (if you have _NO_POS_ARGS , it is much simpler).

After the switch, the simple case (no padding in the format string) is:

PRINT (cp, size);

which is the printing macro. At least if the pointer cp is wrong, then odd things will happen.

The macro itself cannot be horrendously crazy, as we can print the simple case; only the arguments cause problems.

I am afraid this is a bit sticky to debug, but the symptoms point at something getting corrupted in memory. One thing to check is the return value of your printf . It should return the number of characters printed. Whether or not the return value is sane helps debugging the rest.

I'm sorry I'm so late to solve this problem for you. I'm the author of the valvers.com bare metal tutorials. The cause of the crash is due to something I was aware of, but hadn't had time to solve. Actually, I didn't know it would be the solution to your problem.

In short, the problem is that we're telling the toolchain that the processor is an ARM1176 and more importantly that the floating point unit is VFP, and we should use the hard-float ABI.

Using the VFP is an important option - it means we pick up the C library that's also been compiled with this option. Generally the VFP instructions aren't used and therefore do not trip us up. Clearly, portions of printf do use VFP instructions.

The reason this trips us up is because the startup assembler which is responsible for generating a good C runtime environment doesn't enable VFP, so when you reach a VFP instruction the processor jumps to the undefined instruction exception vector.

This is how I found out that this was the problem. I simple enabled the LED in any of the exception vectors and it lit when using printf formatting. Then it was a case of removing the LED calls in the exception vectors until it didn't light anymore. This happened in the "Undefined Instruction" exception. A quick search on the ARM site reveals the processor will go here if a VFP instruction is encountered and the VFP is not enabled. Hence, it reminded me to sort that out!

The Solution

There are a few things you need to do. You need to replicate the CMAKE_C_FLAGS to CMAKE_ASM_FLAGS in the CMakeLists.txt file so that the correct options are passed to the assembler, currently they are not! I will update the tutorials as soon as possible to fix this!

Just below the last set( CMAKE_C_FLAGS ... ) command in the CMakeLists.txt file add set( CMAKE_ASM_FLAGS ${CMAKE_C_FLAGS} ) which works okay because CMake uses gcc as the assembler.

Next, we need to modify the startup assembler file (in my tutorials armc-nnn-start.S) to enable the VFP. Insert the code below just above bl _cstartup

(This is directly off the TI Website )

// Enable VFP/NEON
// r1 = Access Control Register
MRC p15, #0, r1, c1, c0, #2
// enable full access for p10,11
ORR r1, r1, #(0xf << 20)
// ccess Control Register = r1
MCR p15, #0, r1, c1, c0, #2
MOV r1, #0
// flush prefetch buffer because of FMXR below
MCR p15, #0, r1, c7, c5, #4
// and CP 10 & 11 were only just enabled
// Enable VFP itself
MOV r0,#0x40000000
// FPEXC = r0
FMXR FPEXC, r0

You can find some information from ARM about this here .

Those changes are enough to get printf formatting working okay (I've tested it on the UART). If you have any further problems, don't hesitate to ask.

Lastly, sorry you've had grief because the startup code is not correct! The last thing I'd want to do is cost someone's time!!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM