Hello World Link to heading

So far we calculated the IBRD and FBRD now we just have to inform the CPU about those new values.

We can find that the UART Address Map is located at 0x7E20100 which is actually located at 0x3F20100 in our case, because the 0x7E.. addresses are for peripherals like the GPU.

The IBRD and FBRD are located at the memory offset of 0x24 and 0x28.

Awesome can we just set it now? No! In the BCM2835 Peripherals we can find under the LCRH Register(p. 184) a note:

NOTE: The UART_LCRH, UART_IBRD, and UART_FBRD registers must not be changed:

  • when the UART is enabled

  • when completing a transmission or a reception when it has been programmed to become disabled.

Configuration Overview Link to heading

There are a few things that have to be configured and all the information is located in different locations of the documentation so I’ll cut it short and give you a list of the order we have to do things:

  1. Disable UART
  2. Clear UART FIFO by disabling it
  3. Write IBRD and FBRD
  4. Set word length to 8 bits and enable FIFO again
  5. Enable UART and enable transmission

1. Disable UART Link to heading

Okay, let’s start by disabling UART so we can write IBRD, and FBRD.

 1const UART0_CR: u32 = 0x3F20_1030;
 2const UART0_CR_UARTEN: u32 = 1 << 0;
 3
 4//...
 5
 6fn uart_enable(enable: bool) {
 7    unsafe {
 8        let mut cr = core::ptr::read_volatile(UART0_CR as *mut u32);
 9
10        if enable {
11            cr |= UART0_CR_UARTEN;
12        } else {
13            cr &= !UART0_CR_UARTEN;
14        }
15
16        core::ptr::write_volatile(UART0_CR as *mut u32, cr);
17    }
18}

Here, we read the current Control Register value, flip the first bit depending on wether we want to enable or disable UART, and write it back.

Note: Please note that the code on the repo may differ from the one shown here.
1pub fn configure_uart() {
2    let baud_div_times_64 = (UART_CLK * 4) / BAUD;
3
4    let ibrd = baud_div_times_64 / 64;
5    let fbrd = baud_div_times_64 % 64;
6
7    unsafe {
8        uart_enable(false);
9        //...

2. Disable UART FIFO Link to heading

The FIFO is used as a temporary storage before sending data over the UART connection. It’s the same logic as the uart_enable just with a different Register and offset.

 1const UART0_LCRH: u32 = 0x3F20_102c;
 2const UART0_LCRH_FEN: u32 = 1 << 4;
 3
 4//...
 5
 6fn uart_fifo_enable(enable: bool) {
 7    unsafe {
 8        let mut lcrh = core::ptr::read_volatile(UART0_LCRH as *mut u32);
 9
10        if enable {
11            lcrh |= UART0_LCRH_FEN;
12        } else {
13            lcrh &= !UART0_LCRH_FEN;
14        }
15
16        core::ptr::write_volatile(UART0_LCRH as *mut u32, lcrh);
17    }
18}

And now we just have to call it in our configuration function:

 1pub fn configure_uart() {
 2    let baud_div_times_64 = (UART_CLK * 4) / BAUD;
 3
 4    let ibrd = baud_div_times_64 / 64;
 5    let fbrd = baud_div_times_64 % 64;
 6
 7    unsafe {
 8        uart_enable(false);
 9        uart_fifo_enable(false);
10        //...

3. Updating IBRD and FBRD Link to heading

We’ll just write it inline into the corresponding registers.

 1const UART0_IBRD: u32 = 0x3F20_1024;
 2const UART0_FBRD: u32 = 0x3F20_1028;
 3
 4pub fn configure_uart() {
 5        //...
 6        uart_fifo_enable(false);
 7
 8        core::ptr::write_volatile(UART0_IBRD as *mut u32, ibrd);
 9        core::ptr::write_volatile(UART0_FBRD as *mut u32, fbrd);
10        //...

4. Set word length and enable FIFO Link to heading

The word length decides on how many bits get sent in one UART frame. Here we have multiple different configurations but we’ll use 8 bits aka. 0b11.

register value Word bit count
0b11 8 bits
0b10 7 bits
0b01 6 bits
0b00 5 bits

In this case we will overwrite the entire LCRH register.

1fn uart_set_lcrh(wlen: u32, enable_fifo: bool) {
2    unsafe {
3        let mut value = (wlen & 0b11) << 5;
4        if enable_fifo {
5            value |= UART0_LCRH_FEN;
6        }
7        core::ptr::write_volatile(UART0_LCRH as *mut u32, value);
8    }
9}

And again add it to our configuration function:

1pub fn configure_uart() {
2        //...
3        core::ptr::write_volatile(UART0_FBRD as *mut u32, fbrd);
4
5        uart_set_lcrh(0b11, true);
6        //...

5. Enable transmission and UART Link to heading

 1pub fn configure_uart() {
 2    let baud_div_times_64 = (UART_CLK * 4) / BAUD;
 3
 4    let ibrd = baud_div_times_64 / 64;
 5    let fbrd = baud_div_times_64 % 64;
 6
 7    unsafe {
 8        uart_enable(false);
 9        uart_fifo_enable(false);
10
11        core::ptr::write_volatile(UART0_IBRD as *mut u32, ibrd);
12        core::ptr::write_volatile(UART0_FBRD as *mut u32, fbrd);
13
14        uart_set_lcrh(0b11, true);
15
16        let mut cr = core::ptr::read_volatile(UART0_CR as *mut u32);
17        cr |= UART0_CR_UARTEN | UART0_CR_TXE;
18
19        core::ptr::write_volatile(UART0_CR as *mut u32, cr);
20    }
21}

Let’s print Link to heading

Now at last we just need one more function to write to UART

 1const UART0_DR: u32 = 0x3F20_1000;
 2
 3const UART0_FR: u32 = 0x3F20_1018;
 4const UART0_FR_TXFF: u32 = 1 << 5;
 5
 6//...
 7
 8pub fn print(s: &str) {
 9    for byte in s.bytes() {
10        unsafe {
11            // Wait until FIFU is not full anymore
12            while core::ptr::read_volatile(UART0_FR as *const u32) & UART0_FR_TXFF != 0 {}
13
14            // Write our byte to the FIFO
15            core::ptr::write_volatile(UART0_DR as *mut u32, byte as u32);
16        }
17    }
18    // Wait till uart is not busy anymore
19    unsafe { while (core::ptr::read_volatile(UART0_FR as *const u32) >> 3) & 0b1 != 0 {} }
20}

Now in our main we can call uart_configure and print.

pub unsafe extern "C" fn _start() {
    //...
    main();
}

fn main() {
    uart::configure_uart();
    loop{
        print("Hello World!\r\n");
    }
}

Okay lets start the simulator and see what happens:

> qemu-system-aarch64 \
  -M raspi3b \
  -cpu cortex-a53 \
  -serial stdio \
  -sd sd.img \
  -display none \
  -kernel ../target/aarch64-unknown-none/release/kernel8.img
Hello World!
Hello World!
Hello World!
Hello World!
...

via GIPHY

BUT WAIT

it still doesn’t work on the real Raspberry PI???

Yeah, we have to configure the GPIO pins first before we can use UART on real hardware.

Git Repository Link to heading