Useful Reference: Broadcom BCM2835 ARM Peripherals Manual (PDF)
⭠ Back to Home

UART0 and printf

In this section, we will focus on configuring the GPIO pins to enable UART0 output on the Raspberry Pi 3B.


Setting GPIO14 and GPIO15 to ALT0 for UART0

selector = get32(GPFSEL1);
selector &= ~(7 << 12);                   // Clear bits 14:12 for GPIO14
selector |=  (4 << 12);                   // Set bits 14:12 to ALT0
selector &= ~(7 << 15);                   // Clear bits 17:15 for GPIO15
selector |=  (4 << 15);                   // Set bits 17:15 to ALT0
put32(GPFSEL1, selector);

What is ALT0?

On the Raspberry Pi, most GPIO pins are multiplexed, which means each pin can perform multiple functions depending on how you configure it. In our case, we will be configuring it for UART.

Each GPIO pin supports several alternate functions named ALT0, ALT1, ALT2, and so on. These correspond to different internal hardware blocks. For example:

What is GPFSEL1?

GPFSEL stands for GPIO Function Select which are registers that determine what function each GPIO pin performs. There are six of these registers: GPFSEL0 through GPFSEL5. Each register controls 10 GPIO pins and each pin requires 3 bits to set its function.

GPFSEL1 covers GPIO pins 10 through 19. We care about:

The function values are as follows:

Explanation of the Code

After this configuration, GPIO14 and GPIO15 are connected to the UART0 hardware block inside the SoC. This allows us to send and receive serial data using the UART0 peripheral.

Disabling Pull-Up/Down Resistors

// Disable pull-up/down for all GPIO pins & delay for changes to take effect
    put32(GPPUD, 0);
    delay(150);
    put32(GPPUDCLK0, (1 << 14) | (1 << 15));
    delay(150);
    put32(GPPUDCLK0, 0);

If a GPIO pin is set as an input and nothing is connected to it, the voltage level on that pin can float around randomly. This is called a "floating" pin. Because there is no solid electrical signal driving it high or low, it might pick up random electrical noise, which can cause your code to see unpredictable 1s and 0s.

To deal with this, the Raspberry Pi (like many microcontrollers) lets you enable small internal resistors called pull-up or pull-down resistors. These help gently pull the pin toward a default value when nothing else is connected. A pull-up resistor makes the pin read as a 1, and a pull-down makes it read as a 0, unless another device overrides it.

But for UART communication, we do not want any internal resistor interfering. The TX and RX lines are already being actively driven by both the Pi and the other device, like your computer. Since both ends are in full control of the signal, we want the line to be left completely untouched. Having an internal resistor pulling the line in a certain direction could cause small distortions in the signal and lead to unreliable communication.

That is why we explicitly disable both pull-up and pull-down resistors on GPIO14 and GPIO15.

To do that, the Broadcom SoC (page 101) requires a specific sequence to disable pull resistors:

Explanation of the Code

This is how the Raspberry Pi’s hardware expects pull-up/down settings to be configured. If you skip it or apply it incorrectly, there is a chance UART output will be unstable.

Setting the Baud Rate

put32(UART0_IBRD, 26);  // Integer part of baud rate divisor
put32(UART0_FBRD, 3);   // Fractional part of baud rate divisor

These two lines configure the baud rate for UART0, which determines how fast data is sent and received over the serial line. The UART clock on the Raspberry Pi 3B is typically set to 48 MHz, and we want a standard baud rate of 115200 bits per second for serial communication.

Baud Rate Divisor Formula

The UART uses a clock divider to compute the baud rate from the source clock. The equation is:

$$ \text{BaudDiv} = \frac{\text{UART_CLK}}{16 \times \text{BaudRate}} $$

For a 48 MHz UART clock and a target baud rate of 115200:

$$ \text{BaudDiv} = \frac{48{,}000{,}000}{16 \times 115200} \approx 26.041666\ldots $$

Breaking That Into Registers

These two values together configure UART0 to produce a baud rate close to 115200. If the values are off, the receiving end may misinterpret the signal which would result in a lot of garbled text.

UART0_LCRH – Line Control Register

put32(UART0_LCRH, (1 << 4) | (1 << 5) | (1 << 6)); // UARTEN, TXE, RXE

This register sets the format of the data being transmitted and received over UART.

Each FIFO is a 16-byte queue that temporarily holds data as it's sent or received. Without FIFO enabled, the UART can only hold a single byte at a time in each direction, meaning the CPU must read or write each character exactly when it arrives or is ready to send—any delay might cause data loss or missed bytes.

With FIFOs enabled, the CPU doesn’t have to respond immediately to every character. The transmit FIFO can queue up to 16 bytes to be sent, and the receive FIFO can store up to 16 bytes that were received while the CPU was busy. This improves reliability and reduces how often the CPU must service the UART.

If you want to visually see this, there is this really cool interactive tool from Dr. Valvano's Intro to Embedded Systems Class (ECE319K) (scroll to Interactive Tool 9.4): UART FIFO Demo – Dr. Valvano's Intro to Embedded Systems .

So this line of code configures UART0 for standard 8-bit data transmission and enables internal buffering, making it easier to work with in a bare metal environment.

Enabling UART0 - Transmit and Receive

put32(UART0_CR, (1 << 0) | (1 << 8) | (1 << 9)); // UARTEN, TXE, RXE

Once we've finished configuring the UART peripheral (pins, baud rate, data format, etc.), the final step is to turn it on. This is done by writing to the UART0_CR register, which controls the high-level behavior of the UART hardware.

In this case, we are setting three specific bits:

By writing all three bits at once using bitwise OR, we enable the UART, transmitter, and receiver simultaneously:

put32(UART0_CR, (1 << 0) | (1 << 8) | (1 << 9));

This completes our UART initialization and makes it fully operational. If we wanted to print characters to the serial console, we could now write to the UART’s transmit register, and the data would be sent out over GPIO14 (TX). Similarly, the UART is now ready to receive data on GPIO15 (RX), which we could read from the receive register (which is a hint at how printf works!).

Reading and Writing with UART

Now that UART0 is configured and enabled, we can start communicating through it by sending and receiving individual characters. The following functions form the core of our low-level serial I/O layer. They allow us to interact with a terminal, print debug messages, or even build a command-line shell.

char uart_getc(void)

while (get32(UART0_FR) & (1 << 4)) {
    // wait for data
}
return (char)(get32(UART0_DR) & 0xFF);

void uart_putc(char c)

while (get32(UART0_FR) & (1 << 5));
put32(UART0_DR, c);

void uart_puts(const char* str)

while (*str) {
    uart_putc(*str++);
}

Together, these three functions form the basic tools you need to do meaningful I/O in a bare-metal environment. They give you visibility into what your kernel is doing — even before a screen or debugger is available.

Wiring Up printf

Now that we have basic UART functionality with uart_putc, we can hook it up to a lightweight printf implementation to make formatted output much easier to work with. I won't go too deep into the internals of how printf works, but at a high level, all it does is take your format string (things like %d, %x, etc.), process the arguments, and output each character one by one using a function you provide — in our case, uart_putc. So effectively, printf is just a wrapper that formats a string and passes the characters to UART. Instead of focusing on the string formatting logic in printf.cpp, let's look at how printf is wired up to actually send data to the UART.

In kernel.cpp, we initialize printf like this:

init_printf(nullptr, uart_putc_wrapper);

This tells the printf system to use our uart_putc_wrapper function to write characters. Here's what that function looks like:

void uart_putc_wrapper(void* p, char c) {
    (void)p; // Unused
    if (c == '\n') {
        uart_putc('\r'); // Carriage return for terminals
    }
    uart_putc(c);
}

When you call printf("Hello, world!\n"), the internal implementation walks through each character of the formatted string and sends it one by one using your uart_putc_wrapper — which ultimately talks to the UART hardware.

With this setup, you now have formatted text output directly from your bare-metal kernel (no screen or OS required).

Using printf in Exception Handlers

One of the best parts about having printf working in a bare-metal environment is that you can now use it inside your exception handlers. This is incredibly helpful when something goes wrong and you want to know exactly what caused it.

For example, here’s what our exception handler might look like now:

extern "C" void exc_handler(unsigned long type, unsigned long esr, 
                            unsigned long elr, unsigned long spsr, 
                            unsigned long far) {
    printf("\n=== Exception Handler Triggered ===\n");
    printf("Type    : %lu\n", type);
    printf("ESR_EL1 : 0x%lx\n", esr);
    printf("ELR_EL1 : 0x%lx\n", elr);
    printf("SPSR_EL1: 0x%lx\n", spsr);
    printf("FAR_EL1 : 0x%lx\n", far);

    while (1); // halt
}

With this in place, if your code triggers a synchronous exception or an invalid memory access, the handler will print out a full register dump over UART. That means you can immediately see the cause of the fault, what kind it was, where it happened, what the CPU state was, and what memory address was involved.

Before printf, debugging these issues meant blinking LEDs, setting up semihosting, or just guessing. Now, we can see the information from serial output.