Skip to content

03. UART "Hello World" - First Output from Our OS

Now that our kernel boots, let's add UART (serial communication) to see "Hello World!" on the screen.

What is UART?

UART (Universal Asynchronous Receiver/Transmitter) is a serial communication protocol. On the Raspberry Pi 4B, we use the Mini UART (UART1) which is mapped to GPIO pins 14 (TX) and 15 (RX).

Hardware Setup

Connecting a USB-to-TTL Cable

USB-TTL Cable Raspberry Pi GPIO
GND (Black) Pin 6 (GND)
RX (White) Pin 8 (GPIO 14, TX)
TX (Green) Pin 10 (GPIO 15, RX)

Don't Connect 5V!

The Raspberry Pi GPIO operates at 3.3V. Connecting 5V can damage the board.

Opening a Serial Terminal

screen /dev/ttyUSB0 115200

Use PuTTY or Tera Term: - Port: COM3 (or whatever shows up) - Baud: 115200

Understanding Memory-Mapped I/O (MMIO)

How does our code talk to the hardware? In Bare Metal, we use something called Memory-Mapped I/O.

The "Magic Mailbox" Analogy

Imagine a huge wall of mailboxes (Memory/RAM). - Most mailboxes are for storing letters (Data). - But some special mailboxes at the top are connected to hardware devices.

If you put a letter in the "Printer" mailbox, the printer starts printing. You don't "send" data; you just write to a specific memory address, and the hardware reacts.

graph LR
    CPU[CPU]

    subgraph "Memory Space"
        RAM["RAM (0x0 - 0xFDFFFFFF)"]
        MMIO["MMIO (0xFE000000+)"]
    end

    subgraph "Hardware"
        UART["UART Controller"]
        GPIO["GPIO Controller"]
    end

    CPU -->|Load/Store| RAM
    CPU -->|Load/Store| MMIO
    MMIO -->|Control| UART
    MMIO -->|Control| GPIO

Raspberry Pi 4B Memory Map

Address Range Description
0x00000000 - 0xFDFFFFFF RAM (System Memory)
0xFE000000 - 0xFFFFFFFF MMIO (Peripherals)

Pi 3 vs Pi 4 MMIO Base

  • Raspberry Pi 3: MMIO starts at 0x3F000000
  • Raspberry Pi 4: MMIO starts at 0xFE000000 (This is why we need different drivers!)

Implementing Mini UART

We will implement a driver for the Mini UART (UART1), which is easier to set up than the PL011 UART.

Initialization Steps

  1. Map MMIO: Define the base address (0xFE000000).
  2. GPIO Setup: Configure GPIO pins 14 (TX) and 15 (RX).
  3. UART Setup: Enable UART, set baud rate (115200), and 8-bit mode.

File: drivers/uart.c

UART Register Map

1
2
3
4
5
6
7
8
9
#define MMIO_BASE       0xFE000000
#define AUX_ENABLE      ((volatile uint32_t*)(MMIO_BASE + 0x215004))
#define AUX_MU_IO       ((volatile uint32_t*)(MMIO_BASE + 0x215040))
#define AUX_MU_IER      ((volatile uint32_t*)(MMIO_BASE + 0x215044))
#define AUX_MU_LCR      ((volatile uint32_t*)(MMIO_BASE + 0x21504C))
#define AUX_MU_MCR      ((volatile uint32_t*)(MMIO_BASE + 0x215050))
#define AUX_MU_LSR      ((volatile uint32_t*)(MMIO_BASE + 0x215054))
#define AUX_MU_CNTL     ((volatile uint32_t*)(MMIO_BASE + 0x215060))
#define AUX_MU_BAUD     ((volatile uint32_t*)(MMIO_BASE + 0x215068))

Implementing the UART Driver

File: drivers/uart.c

GPIO Configuration (The Switchboard)

The Raspberry Pi's pins (GPIO 14 & 15) can do many things. By default, they might be just simple inputs. We need to tell the GPIO Controller to connect these pins to the UART module.

This is called setting the Alternate Function.

graph TD
    Pin["Pin 14 (Physical)"]
    Switch{"Function Switch"}
    GPIO_In["Input Mode"]
    GPIO_Out["Output Mode"]
    UART_TX["UART TX (Alt5)"]

    Pin --- Switch
    Switch -->|Default| GPIO_In
    Switch -->|Alt5| UART_TX

First, we configure GPIO pins 14 and 15 to use "Alt Function 5" (UART mode):

void uart_init() {
    register uint32_t r;

    /* Enable mini UART */
    *AUX_ENABLE |= 1;

    /* Disable TX/RX during setup */
    *AUX_MU_CNTL = 0;

    /* 8-bit mode */
    *AUX_MU_LCR = 3;

    /* Set baud rate to 115200 */
    *AUX_MU_BAUD = 541;  // For 115200 @ 500MHz core frequency

    /* Map UART1 to GPIO 14 (TX) and 15 (RX) */
    r = *GPFSEL1;
    r &= ~((7<<12)|(7<<15));  // Clear GPIO 14, 15
    r |= (2<<12)|(2<<15);     // Set Alt5
    *GPFSEL1 = r;

    /* Disable pull-up/down */
    *GPPUD = 0;
    delay(150);
    *GPPUDCLK0 = (1<<14)|(1<<15);
    delay(150);
    *GPPUDCLK0 = 0;

    /* Enable TX/RX */
    *AUX_MU_CNTL = 3;
}

Sending Characters

void uart_send(char c) {
    /* Wait for TX FIFO to be ready */
    while(!(*AUX_MU_LSR & 0x20));
    *AUX_MU_IO = c;
}

void uart_puts(const char *s) {
    while(*s) {
        if(*s == '\n') uart_send('\r');  // Convert \n to \r\n
        uart_send(*s++);
    }
}

The Main Function

File: kernel/main.c

1
2
3
4
5
6
7
8
void kernel_main() {
    uart_init();
    uart_puts("Hello World from Bare Metal RPi OS!\n");

    while(1) {
        uart_send( *AUX_MU_IO );  // Echo back received characters
    }
}

Simplified Code

The code above is a simplified version to demonstrate the core concept. The actual kernel/main.c in the repository includes additional features like a shell, command processing, and support for other hardware (which we'll add in later chapters).

Building and Testing

1. Build the Kernel

cd simpian-os/build
make

Verify kernel8.img was created.

2. Prepare the SD Card

Copy these files to the SD card: - bootcode.bin - start4.elf - fixup4.dat - kernel8.img (your kernel)

3. Connect Serial Cable

Connect the USB-to-TTL cable as described above.

4. Power On

  1. Insert the SD card
  2. Open the serial terminal (screen /dev/ttyUSB0 115200)
  3. Power on the Raspberry Pi

You should see:

Hello World from Bare Metal RPi OS!

5. Test Echo

Type any key in the terminal. It should echo back!

Troubleshooting

Mini UART Baud Rate Calculation

The Mini UART (UART1) baud rate depends on the GPU core frequency, not a fixed clock. The formula is:

baudrate = core_freq / (8 * (AUX_MU_BAUD + 1))

For Raspberry Pi 4 at 500MHz core frequency:

115200 = 500000000 / (8 * (541 + 1))

Therefore: AUX_MU_BAUD = 541

Critical Requirements: - Your config.txt must include core_freq_min=500 to lock the GPU core frequency - Without this, dynamic frequency scaling will cause garbled output even if your code is correct - See Article 02 for complete config.txt configuration

If changing baud rate or core frequency: - For 9600 baud @ 500MHz: AUX_MU_BAUD = 6509
- For 115200 @ 250MHz: AUX_MU_BAUD = 270 - Always recalculate using the formula above

Problem Solution
No output Check wiring (TX/RX might be swapped)
Garbled characters Verify config.txt has core_freq_min=500 and initial_turbo=0 (see warning above)
Wrong baud rate Recalculate AUX_MU_BAUD using formula with actual core frequency
Garbled text Wrong baud rate (should be 115200)
Raspberry Pi won't boot Missing firmware files (start4.elf, etc.)

Complete Source Code

The full implementation is available in the simpian-os repository:

What's Next?

Congratulations! You've successfully: - ✅ Set up a bare-metal development environment - ✅ Written ARM64 assembly boot code - ✅ Implemented a UART driver - ✅ Printed "Hello World" from your own OS!

Future topics: - Exception handling and interrupts - Timers - Memory management (MMU) - Multitasking

Stay tuned for more articles!