Skip to main content
Akshay Pai
a computer human at a rave

Making The Raspberry Pi Blink

Akshay Pai 6 months ago

In the last post, we covered ELFs and memory layouts. I concluded the article with the promise of walking through an example program in execution to help visualize things better.

a gif of a muppet that reads "I lied"

I’m sorry but this new stuff seemed more appealing 😔. We could probably follow it up in a separate post but for now, let’s jump straight to the Raspberry Pi!

Plugging it in

an image of a raspberry pi that has a red blinking light

Nothing happened. Why? For starters, we have no output devices for the Raspberry Pi to convey any information to us. And more importantly, we don’t have any software on this Raspberry Pi that could put our output devices to use.

Computers come with very little pre-packaged code; stuff like performing basic checks to ensure the hardware is functional. But apart from that, it has just enough code to load your operating system. The operating system then takes care of everything else.

For Raspberry Pi, I think the sequence of operations looks something like this

  1. Perform hardware checks
  2. Load the operating system from the SD card with a file named kernel8.img.

Let’s start with the operating system for our Raspberry Pi; Raspbian; an operating system built by the company manufacturing Raspberry Pis. I’ve downloaded this onto an SD card. This is what the contents look like.

an image of the contents of the sd card

My guess is as good as yours on what these files are. But I do recognize the kernel8.img file on the SD card. That is the ELF executable of the operating system that is run when the Raspberry Pi is booted. In other words, it’s the “.exe file” of our operating system. (It uses an ELF file type because Raspbian is built on top of Linux).

If I boot the Raspberry Pi with the SD card plugged in, the logo pops up on the display alongside a welcome message.

an image of raspberry pi writing to a connected lcd display

Your operating system converted the hardware you had into a fully functioning computer ready to be used. So how does the operating system do this? What does the code interacting with hardware look like?

Write your own OS

I need to start this section with a disclaimer. I have next to no practical experience working with hardware. A whole lot of this is new to me; best practices are not expected to be followed and mistakes are expected to be made ^_^.

With that, let’s get started. In our previous post, we’ve seen what ELF files are, and how they are combined to form an executable file. We’ll also start using C rather than writing this in assembly. I mean… I’m bored, but I’m not that bored.

Let’s start with a bare minimal operating system that does nothing.

void main() {
    while(1);  // Loop infinitely; do nothing
}

(Note: Programs start execution at the main function by default). Let’s build this into an ELF file, load it into our SD card, and see what happens.

the same raspberry pi with nothing on the connected lcd screen

Aaaand lights out. Obviously, our operating system right now does nothing. It might not look like much but right now, our CPU is working pretty hard on that infinite loop. However, some proof of life would be nice. We could start writing code that prints “Hello World!” onto the screen. That would be a great way to verify that our operating system is working as expected. However, writing to a display isn’t as straightforward as we’d imagine. We’ll get to it in a future post but for now, let’s start small.

Making an LED Blink

Taking a look at the Raspberry Pi model again, we see the 40-Pin General-Purpose Input/Output GPIO. We’re going to control these pins to make our Raspberry Pi do something useful.

an image of the model of the raspberry pi

Let’s take a closer look at the layout for these pins (found this in the documentation).

an image of the raspberry pi pin layouts from pin 1 to 40 with markings of what the pin does

Each of these pins has a base function. For example, Pin 1 is marked as 3V3 power. This means that Pin 1 is at a +3.3 Voltage. Similarly, Pin 6 is marked Ground or 0V. So if I connect an LED bulb marked for 3.3V across Pin 1 and 6, I should expect it to glow.

a raspberry pi with a bulb connected that is currently glowing

Some pins are also labelled with a GPIO number, like Pin 3 marked as GPIO 2. These pins are of interest. The pins marked GPIO can be programmatically controlled. For example, your code could set the pin to output 3.3V (HIGH) or ground the pin to 0V (LOW). So if I connected the same LED across Pin 3 (GPIO 2) and Pin 6 (Ground) and alternated Pin 3 between 3.3V and 0V with a pause between each switch, I’d make the LED blink!

Then, the last piece of the puzzle would be, how do we control Pin 3 (GPIO 2) with code. For this, we’ll need a quick detour to explain Memory Mapped IO.

Memory Mapped IO

As covered in an earlier post, your RAM has memory addresses which refer to the memory locations in your RAM. They’re almost like coordinates on your RAM. Your CPU reads/writes to your RAM via memory addresses; “store the number 50 at address 0x80000000 (hexadecimal)” or “read the value present at address 0x125AF724”. However, your CPU also reserves some addresses that don’t really correspond to any location in the RAM. Instead, these addresses map to your IO devices. When you read/write to these addresses, your CPU knows you’re trying to access hardware and takes care of interacting with the hardware. For example, your CPU documentation could specify that, on writing the number 1 to address 0x70000000, it sets the GPIO 2 pin to 3.3V.

This model of virtualizing the hardware with memory addresses is referred to as memory-mapped IO. It makes it awfully convenient to write code. You write to these memory addresses as you would for reading/writing data to RAM and your CPU takes care of the rest.

Manipulating the pin

At this point, we’re ready to manipulate the pin to make our LED blink. Let’s refer to the documentation on the memory address of the GPIO Pins. Taking a look at the BCM2711 peripherals documentation (the CPU used on our Raspberry PI), we see several registers along with their memory address and function, we’ll need several registers but let’s just focus on one for now.

a snippet of the documentation of each register in raspberry pi

The documentation further explains what GPSET0 does here.

a description of what gpset0 register does. covered in the following lines

tldr; to set the GPIO Pin n to a high (3.3V) voltage, we have to set the nth bit of this memory address to a 1. So in our case, setting this memory address to binary 00000000 00000000 00000000 00000100 (2 in decimal) would set GPIO Pin 2 to High. There’s a similar memory address GPCLR0 for which setting the same value would clear GPIO Pin 2. Let’s see what the code looks like.

enum {
    BASE = 0xfe200000,  // Base address of the registers according to the docs
    GPFSEL0 = BASE + 0x00,  // Offset from the base (refer to register view image)
    GPSET0 = BASE + 0x1c,
    GPCLR0 = BASE + 0x28,
};

volatile unsigned int *register_gpfsel0 = (volatile unsigned int*) GPFSEL0;
volatile unsigned int *register_gpset0  = (volatile unsigned int*) GPSET0;
volatile unsigned int *register_gpclr0  = (volatile unsigned int*) GPCLR0;

void clear_pin(int pin_number) {
    *register_gpclr0 |= 1 << pin_number;
}

void set_pin_as_output(int pin_number) {
    *register_gpfsel0 |= 1 << (3 * pin_number);
}

void set_high(int pin_number) {
    *register_gpset0 |=  1 << pin_number;
}

void spin_wait(unsigned long n) {
    for (unsigned long i = 0; i < n; i++);
}

void sleep() {
    spin_wait(1000000);
}


void blink_led() {
    set_high(2);
    sleep();
    clear_pin(2);
    sleep();
}


void main()
{
    set_pin_as_output(2);
    while (1) {
        blink_led();
    }
}

Code Walkthrough

There’s quite a lot going on here. Let’s break it up into pieces to better understand.

enum {
    BASE = 0xfe200000,  // Base address of the registers according to the docs
    GPFSEL0 = BASE + 0x00,  // Offset from the base (refer to register view image)
    GPSET0 = BASE + 0x1c,
    GPCLR0 = BASE + 0x28,
};

volatile unsigned int *register_gpfsel0 = (volatile unsigned int*) GPFSEL0;
volatile unsigned int *register_gpset0  = (volatile unsigned int*) GPSET0;
volatile unsigned int *register_gpclr0  = (volatile unsigned int*) GPCLR0;

We start by creating three variables register_gpfsel0, register_gpset0 and register_gpclr0 that point to the memory addresses specified by GPCLR0, GPSET0 and GPCLR0 respectively. These are the addresses that we found in the documentation. Essentially, we’ve created handy variables to read/write to these memory-mapped addresses.

void set_high(int pin_number) {
    *register_gpset0 |=  1 << pin_number;
}

We then created several functions that abstract away the registers and make it more intuitive to use these addresses. The set_high , for example, sets the right bit for a given pin_number to a 1. We do this based on the documentation specified for the GPSET0 register. For example, calling the functionset_high(2) sets the bit associated with GPIO 2 to a 1, consequently setting GPIO 2 to a high voltage.

void blink_led() {
    set_high(2);
    sleep();
    clear_pin(2);
    sleep();
}


void main()
{
    set_pin_as_output(2);
    while (1) {
        blink_led();  // call blink_led function in an infite loop
    }
}

Putting it all together, our new main function initializes GPIO 2 and goes into an infinite loop that calls the blink_led function. What does this function do? It first sets GPIO 2 to a HIGH, proceeds to sleep for a fixed duration, clears GPIO 2 and goes straight back to sleep.

Repeat this an infinite amount of times and you should get a blinking LED light. Let’s build this and plug our new operating system into the Raspberry Pi.

a gif of the raspberry pi and the led blinking

Success! Proof of life for our “operating system”!

Wrapping it up

At this point, we’ve successfully manipulated some hardware with code even if it’s just to blink an LED bulb. However, that’s pretty much all our operating system supports. It’d be cool if our operating system could do a little more than what it is currently capable of. But for now, we’ve covered enough to call it a day.