What is Embedded Software?

From R@M Wiki

Embedded Software, sometimes called Firmware, is code written for a device with highly specialized purpose which usually has significantly weaker power consumption when compared to a desktop or laptop. Take charging a device with a USB-C cable for example. USB-C specifies a power delivery protocol for this exact purpose. The protocol is complex enough that both ends need a device to faithfully execute the logic of the protocol, so more likely than not, there is a small microcontroller (MCU, or a small CPU) chip running some C or Assembly code living inside the power brick that you plug into the wall, and another one living inside of the device you are charging.

Embedded vs. Desktop Software[edit | edit source]

So now we consider the code running on that device. Someone had to write that code and give it to that microcontroller to execute. But how does that actually work out? On a desktop we write our code, save it to a file, compile it if necessary, get back an executable, and then we just execute it. Breaking down these steps will help understand the steps for embedded software, because it actually really isn't that much different.

Writing a Program for a Desktop[edit | edit source]

Let's consider a Hello World program written in C that I wish to run on my desktop running Linux with an Intel CPU (with x86_64 architecture):

/* hello.c */

#include <stdio.h>

int main(void) {
    printf("Hello World!\n");
    return 0;
}

But this is is just raw text, so now I have to compile it, that is, convert my raw text C source file into a binary file containing machine code that my CPU understands. That command is just

gcc hello.c -o hello

Now to run my program I just run

./hello

and I should see Hello World! pop up in my terminal. There is a lot of stuff going on behind the scenes though; in particular that printf("Hello World!\n");is carrying a lot of weight.

How does the compiler know what the function printf does?

Well, the function prototype for printf lives in stdio.h, so we at least know what arguments printf takes and what it returns (which happens to be an int not a void, fun fact), but the source code, or function definition, has to live somewhere, right? And it does. You might recall that printf and many other functions are provided by the C standard library, a collection of functions that the C standard guarantees you have access to. The source code for printf and all these other C standard functions are compiled into a library file called libc.a or libc.so. When you run gcc to compile your programs like above, libc is linked by default because most programs rely heavily on these functions. But we're not done yet.

What exactly does printf do?

Perhaps a better question is How would you or I go about writing any of the C standard functions? The C standard functions make it so that code written for Linux, Windows, macOS, iOS, Android, video game consoles, or even embedded environments, which depend on one or more C standard functions, can call them, and they just work. Anything that the C standard mandates must happen for any of those functions will happen regardless of the platform you are writing C code for. Fortunately most platforms provide more "basic" versions for these C standard functions to depend upon. These are called system calls (syscalls). On Linux for example, terminal output is categorized under binary reading and writing (which is really just moving bits from one place to another), which is handed by the syscalls read and write. So for the sake of completeness, printf really just does the following:

  • Convert the numeric inputs into their text representation (integers, floats, and pointer addresses to their ASCII string representation)
  • Concatenate all the strings together as indicated by the format string argument
  • Call write to output the text to the file descriptor servicing stdout

All of this, and we are still not done yet. There is still one big question:

How does the terminal (emulator) display text to the screen?

Now the terminal looks up the text in the buffer for stdout, calls a lookup function corresponding a character's ASCII encoding to the font used by the terminal (which is just a collection of pixels), those pixels are sent to the video encoding chip on the motherboard, which then converts that data to format that the screen understands and can present.

The point of this exercise is to identify the layers of abstraction that software is built on. C desktop application code depends on C standard library functions, which in turn depends on system calls provided by the operating system, which finally depends on the CPU and its physical connections to other components on the motherboard. Embedded software lives right above that CPU/physical connections layer. There are no C functions or system calls to depend upon (unless you implement them yourself, of course), you are directly what happens with the hardware inside the CPU and how it interfaces with its pins.

Exercise

You can actually see these dependencies yourself on Linux since most of these libraries are dynamically linked. Compile the above code and try ldd hello to see its dynamic dependencies. All .so files are libraries, try running ldd on them to see how far this structure goes.

Preparing to Write Programs for Embedded Systems[edit | edit source]

So when we write code for an embedded system you are starting at the lowest possible level discussed above. It is your responsibility to decide what libraries, if any, to use to fill in bits and pieces of each layer. For example, if you were to write the absolute bare minimum code necessary to blink an LED on a development board, the compiler would complain that certain syscalls like read, write, and seek are not implemented.

This comes with some downsides, there is some more "boilerplate code" (code that you have to include but doesn't really do anything too insightful) than one may interact with compared to a desktop application, but the upside is that the logic is only as complicated as the underlying electronics of the board, even when working with libraries that might attempt to abstract some of the electronic logic away.

Learning how to understand the electronic logic of an embedded system is a crucial part of embedded software development. This is a skill that these lessons will attempt to help you get started, but nothing really beats just sitting down and struggling through a bunch of manuals to learn. Fortunately learning the hardware and register functions of an MCU isn't too different than reading over the documentation for an API in application software development.

But first let's look over the hardware of a typical embedded device (at least one like the ones we use here).

Embedded Device Hardware Layout[edit | edit source]

If you have ever built a desktop computer or looked inside of one you might know that the hardware for such a machine has the distinct components neatly laid out physically into their own sections, with the motherboard connecting them all together. Such components might be the CPU, RAM, HDDs or SSDs, I/O ports, additional peripherals slotted into PCIe connectors, and so on. Embedded devices, on the other hand, need to be compact, and thus do not have the option to have all their components spread out on the motherboard. Instead, many manufacturers opt to manufacture their hardware in a System-on-a-Chip (SoC) form factor.

An SoC is a hardware layout in which a bunch of the above components are embedded inside the physical MCU. This means that inside of the physical black chip is a computing unit, non-volatile flash storage (think like a really fast and small SSD), RAM, and a bunch of other registers for I/O that break out of the outer chip to be connected to other components in the greater device (such as a USB connector, or an LED).

With this layout in mind it hopefully becomes more clear how embedded software gets to the MCU. The manufacturer decides to allocate a set of I/O pins that can be interfaced in such a way (by a development machine like a laptop or desktop or even by other embedded devices) so that the compiled binary can be copied to the flash memory (in a process called flashing) at a particular location that the MCU knows where to execute from when starting up.

Embedded Device Operation[edit | edit source]

Registers[edit | edit source]

Now that we somewhat understand the hardware and are able to flash the device, and we have it powered up, how do we get it to actually do stuff? One way to think about it is that the SoC is like a giant state machine, and editing the registers on the device allow it to change state and actually interface with the outside world. A common pattern in embedded development is that the programmer first configures the peripheral (that is, the cluster of hardware responsible for servicing some function, such as USB, AES encryption, UART, or even the system clock, for example) by editing some bits in a register. Then once configuration is done, the user sets a bit in another register, which locks the configuration and starts up the hardware for that peripheral.

Interrupts[edit | edit source]

Say we configured our device to enable a UART peripheral (which is a communication protocol that is way simpler than USB), how does our MCU know when the device on the other end is attempting to send something over? One option is to just ask the hardware every 100 ms. If there is something there, read the data, if not, just sleep. While this is an okay solution, it is expensive and slow. What if we want to do some other computation while waiting around for incoming data on the UART connection? This is what interrupts solve.

Embedded in the CPU is the functionality to interrupt normal computation when some event occurs, and jump immediately to the function responsible for responding to that event. After that function concludes, the CPU jumps back to where the normal code left off and proceeds as normal.

So an alternative solution using interrupts in this scenario would be to write an interrupt function for the UART peripheral which likely copies data from one buffer to another so that the user application is able to use the received data.

Ready to Go![edit | edit source]

Keeping all of this in mind, we are ready to get started setting up the development environment as well as how to interpret all the documentation that comes with an embedded device.