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

On bare-metal systems, we control hardware by reading/writing to specific memory addresses.

Raspberry Pi 4B Memory Map

Device Base Address
GPIO 0xFE200000
Mini UART 0xFE215000

Legacy vs RPi 4

  • Raspberry Pi 1-3: 0x3F000000
  • Raspberry Pi 4: 0xFE000000

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: kernel/kernel.c

GPIO Configuration

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

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

Building and Testing

1. Build the Kernel

cd os-rasp/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 os-rasp 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!