Useful tips for learning embedded programming

Interested in learning embedded programming? Follow these handy learning tips.


Introduction

Technology-wise, it’s a great time we are living right now. With little money you can start your own workshop and learn about the fascinating world of embedded programming! Open-source tools, compilers, IDEs, low-cost development boards and thousands of articles and tutorials out there for you to start coding today.

So I came with this little list of tips and tricks that I remember I would have known when I started coding for embedded. The list is oriented to programmers like the girl/guy writing their very first lines of code up to experienced programmers from other remote galaxies like web developers. It may be useful to you if you are starting to develop for embedded systems, that go from the simplest Arduino board up to custom boards.

Most of the topics in the list are about the area I cover, that is, the microcontroller/bare-metal area of embedded programming (no high level languages or operating systems), a little on microelectronics and some other non-programming topics worth mentioning.

The list has no particular order and may be updated with new tips in the future.

Master C/C++

Let’s start with the most difficult tip. And yes, I know. It’s is a complicated topic. You may know that stating a preference for a language could lead you to be crucified by an online angry mob, but this is it: to master embedded programming you have to master C/C++. Specially C.

If you are already programming Arduinos, C/C++ is the language you are currently coding to make those LEDs blink. It’s a special C/C++ format that has some extra features to ease the learning curve, but it’s still C/C++.

C is very simple and powerful. You can make great things by just learning the basics. There is a caveat though: “with great power comes great responsibility”. This means that with C you have control over almost every aspect of your system, but also you can also easily introduce bugs.

“The C Programming Language” by Kernighan and Ritchie.

I think almost everyone in the industry is following the progress of the Rust programming language. It has some interesting features that should help to avoid the typical bugs that a developer may introduce. My impression is that, somehow, to appreciate Rust you may have to have some experience with C.

Even though, C has been and will be used as the language of choice for low-level, bare-metal system development, for many years now, with support of many compilers, IDEs and libraries. It’s an industry standard that may still rule for a while and because of this and the other mentioned reasons C/C++ should be of your interest.

Most of the available code for embedded you may find is in C/C++. As a matter of fact, all the little pieces of code you may find in this list are in C. Also consider that many (if not all) the IC manufacturers give support and libraries in C. For examples, if you want to integrate an accelerometer in your project, you might find a library you can use, and it will be in C.

After a while writing C/C++, you might be interested in the assembly code generated by the C/C++ compiler. This comes handy when you want to optimize the way you write code or perhaps write some routines in assembler to optimize your programs.

And a minimum of electronics

Embedded programming doesn’t end at the boundaries of your code editor. It’s a whole new world in which some expertise in electronics is required.

You don’t need the knowledge to design a board from scratch; just the minimum theory. As an example of what is required, I am going to (vaguely) tell about what starting a new embedded software project is about, and I will add the required skill:

  • I am handled a new board, and the electronics schematics (reading a schematic)
  • I see that there is an SD card connected to the microcontroller through SDIO (knowing about electrical signals and a the different buses)
  • I also see that there is a dedicated power supply for the SD that I have to turn-on with a GPIO (knowing about power supplies and GPIOs)
  • I see there is a pull-up resistor in the “SD card detect” signal (knowing about processing signals)
  • I might need to probe the “SD card detect” signal (using a multimeter and a oscilloscope)
  • I see there is an analog signal going into my microcontroller. The input has a resistor divider (knowing about analog signals, basic OHM’s law calculations)
  • There is a flip-flop that is used to reset another IC (basic components knowledge, like logic gates, operational amplifiers, etc.)

And that is just the basic knowledge. With time you will get to learn other topics like battery management, audio, power circuits, amplifiers, advanced communications like USB, Ethernet, Wireless, etc. But there is time for that…

Keep interrupt handlers fast

As the name suggests, what an interrupt does is to interrupt the normal execution flow of your program because an external (or internal) event happened. In presence of such events the microcontroller usually pass the control to a special function (the interrupt handler) in which you have to implement the logic to handle the interrupt.

I’m sure you are familiar with the following Arduino code, that in this case enables an interrupt on pin 2 and specifies that the interrupt handler is a function called MyInterruptHandler.

attachInterrupt(2, MyInterruptHandler, RISING);

Most of the times, and unless you know exactly what you are doing, the code executed by interrupt handlers should be the fastest, most optimized code you should ever write. It has to quickly do what is supposed to do and return immediately, with no delays.

Why?

While an interrupt is active, some microcontrollers may block other interrupts from happening. This means that while you are handling one interrupt, another interrupt happening at the same time may get lost, and this is bad.

Modern microcontrollers have a sort of (limited) queue for interrupts: triggered interrupts with high priority can preempt (interrupt) the execution of other interrupts with a lower priority. In this case you may or may not experience interrupts being lost, but this is no excuse for bad interrupt handling.

Another reason is that some interrupts are triggered very often. The same interrupt may keep coming while the previous interrupt is still being handled. This may set the new interrupt as pending and as soon as the current interrupt is done being handled, there will be another interrupt to handle, and thus your main program is barely executed or never executed at all.

In short: avoid doing delays or heavy, time-consuming tasks inside interrupt handlers, like long loops or accessing peripherals like reading a sensor through I2C directly in the interrupt handler:

uint8_t value;

void setup()
{
    Wire.begin();
    attachInterrupt(1, MyInterruptHandler, RISING);
}

void loop()
{

}

void MyInterruptHandler()
{
    // Example of functions you don't want
    // to call from an interrupt handler.

    // Read I2C
    Wire.requestFrom(0xAB, 1);

    // Wait for the device to answer
    while (!Wire.available());

    // Read
    value = Wire.read();    
}

For the above case, a non-exhaustive but valid alternative could be to set a variable when the interrupt happens, to be then checked in your main loop and read/write from/to the I2C device outside the interrupt handler.

uint8_t value;
volatile bool interrupt_requested = false;

void setup()
{
    Wire.begin();
    attachInterrupt(1, MyInterruptHandler, RISING);
}

void loop()
{
    if (interrupt_requested)
    {
        // Optionally: disabling/enabling interrupts when setting this variable
        interrupt_requested = false;

        // Read I2C
        Wire.requestFrom(0xAB, 1);

        // Wait for the device to answer
        while (!Wire.available());

        // Read
        value = Wire.read(); 
    }
}

void MyInterruptHandler()
{
    interrupt_requested = true;
}

Use the volatile keyword

This item of the list is also related to interrupts. It probably happened to you that, for some strange reason, a variable didn’t change its value as you expected. Often this behavior may have something to do with the way you are handling the variable in an interrupt handler. People coming from multithreaded environments might be aware of this problem as well.

The following code is an example of what you may experience: even after the interrupt is triggered, some code is never executed.

// Set this variable when the interrupt happens
bool event = false;
uint8_t value = 0;

void setup()
{
    Serial.begin(9600);
    attachInterrupt(1, MyInterruptFunction, RISING);
}

void loop()
{
    // Wait for interrupt to happen
    while (!event);

    // Even if the interrupt happens,
    // THIS CODE IS NEVER EXECUTED !!!!
    Serial.println("Event detected!");
    event = false;
    value++;
}

void MyInterruptFunction()
{
    event = true;
}

Explanation

So why part of the code is never executed? When the program enters the loop() function, there is the following piece of code:

while (!event);

This is an infinite loop that checks if the event variable is set to true. When you compiled the program, the compiler has generated instructions to:

  1. Copy the contents of the event variable (from RAM) and put it into a register.
  2. Check the value of the register.
  3. If the value of the register is true, then the infinite loop is exited and the program flow continues. If the value is false, then the cycle repeats from step 2.

Note that the operation that reads the contents of the event variable and put it into the register is done only once (step 1). The following steps of the infinite loop will just check if the value of the register changes (steps 2 and 3). Do you notice the problem here?

Even if the interrupt is triggered and the event variable is set to true, the value in the register will never change. This makes the program to get stuck in the infinite loop. What we really need is a way to tell the compiler to generate instructions in a way the event variable content is copied into the register and evaluated in every loop cycle.

Here comes the volatile keyword to save the day.

// Declare this variable as volatile
volatile bool event = false;

This way you are telling the compiler that the variable may change anytime, forcing the compiler to generate instructions to read the real value of the event variable from RAM and to store it in the register before comparing it in every cycle of the loop. Of course, it generates more code and the loop will be “slower”, but in this case, it is required.

This is one case in which the volatile is useful, but there are many other that you may or may not come across. Like for example polling on microcontroller peripheral registers:

while (FLASH->SR & FLASH_FLAG_BSY);

The above code waits for the FLASH controller on an STM32F10x microcontroller to be ready after doing some flash operation (not important for this example). This is signaled by setting a flag (a bit) to 1 in the SR register of the FLASH controller. This flag is automatically set by the hardware on completion, so we may potentially face the same problem as above.

But just to be sure, let’s go and take a look at how is defined the SR field of the FLASH structure:

typedef struct
{
    // ... more fields here ... 
    __IO uint32_t SR;
    // ... more fields here ...
} FLASH_TypeDef;

Note the __IO. We search it’s definition and we see that the __IO keyword means:

#define __IO volatile

This translates to:

typedef struct
{
    // ... more fields here ... 
    volatile uint32_t SR;
    // ... more fields here ...
} FLASH_TypeDef;

So we can be sure the compiler will generate the required instructions to evaluate the real value on every loop cycle.

The way the compiler generates this kind of code depends on factors like the architecture you are using and how the compiler is optimizing the code at that specific point. You may also note that code compiled for debug may not present the aforementioned problem, but it does when the code is compiled for release, that is, changing the flags for optimization (-O0, -O3, -Os, etc.) when compiling.

Despite compilers nowadays are very intelligent and try their best to avoid doing operations that will waste processor time (like accessing memory). Sometimes these optimizations can carry all kinds of side effects.

As a side note, do not abuse the usage of the volatile keyword when is not required, as it will waste processor time by reading from memory when it can be otherwise optimized.

Know your microcontroller

This one here is not a programming tip per se, but it worths a mention. The natural course of actions after mastering a known and popular development board (for example, an Arduino) is to try and roll your own board, or maybe to try that fancy new microcontroller you’ve read it has some cool feature but that unfortunately it doesn’t has yet the support of a big community ready to help you.

As you leave the comfortable Arduino world (or any other highly integrated ecosystem), you enter into the unknown. You used to sit down and write code without worrying too much about what is happening down in the microcontroller’s guts, but this is an approach that can’t be used anymore.

So, you have put the development environment together. You’ve downloaded, installed and configured the toolchain. The program now compiles and runs fine but for some reason that elusive UART keeps refusing to output some characters. Now what?

This I’m about to say may sound trivial for people with years in the industry: embedded programming it’s more than just coding, you also need to start learning about the microcontroller you are using, datasheet at hand. Realizing about this part of the equation is a crucial moment for the to-be embedded developer. I know, it’s scary, it requires a lot of time (I’ve been there too!), but I assure you it will be the source of many satisfying Aha! moments.

You may feel alone

Following the “Know your microcontroller” epiphany, this is also the moment you realize that you may start facing problems that only apply to you, and only you.

Let’s say that you have a bug. But not a bug like a loose pointer somewhere but a bug that is more like an UART that stops working mysteriously after a random amount of time…

Your Google searches yield few or no results at all (and Google is pretty bad in helping with technical stuff lately). The users of the forum where you asked for help are requesting the entire code or your project, or a snippet to reproduce the problem, but it’s really too much code to just pinpoint the actual problem or it is a problem that only applies to the platform you are using (that might not be so popular or completely custom), so you just know that the real solution will not come from a forum answer. The problem may be so specific that even your more experienced colleagues are not able to help you.

So your program has a bug, but the real issue here is that you are in front of a new type of problem: you are alone…

The key is to not despair. Know that 99% of the times the bug was introduced by you, so it’s better to start looking very carefully at your code. Try to reduce your program to the bare minimum needed to be able to reproduce the problem, even if this means to start a new dummy project.

I feel your pain and, to help you in your non-despairing attempts, I write here the most common causes for these kind of bugs:

  • Sequence and timing: some peripherals need a specific sequence to be started, operated and stopped. This is mainly the first cause of problems. For example a delay is needed between two initialization steps. This kind of information is (or should be) present in the datasheet of the peripheral (internal or external) you are using. The same applies to the handling of the interrupt flags, like an interrupt that nevers gets cleared because you forgot to set a bit in some register. In any of these cases you can take a look at the original code provided by the microcontroller manufacturer (like libraries and examples) and see if you are following the same sequence and timing.

  • A hardware problem: sometimes is just as simple as that. Perhaps there is a broken component or a missing resistor somewhere. Here you can try and compare your program running in another board, if possible. Once confirmed that is a hardware problem be careful to not start blaming the hardware every time an unexplained problem occurs. Before you know, you will be quickly searching for power supply ripples, missing pull-up/down resistors, comparing two boards, looking for power supply rise times, etc. Many times, at the end of the day, it is just a software problem.

  • A silicon bug: this means that there is something wrong with the design of the microcontroller/chip, and it’s pretty normal. Silicon bugs are for real and not so uncommon. Go to the microcontroller website and download the errata document. There you can find a list of known silicon bugs and how to workaround them.

  • A compiler bug: these exist, I can assure that, but are improbable. In doubt, you can start disassembling your program (viewing the assembler instructions generated by your compiler), but it can be a waste of time and patience. It’s better to rewrite the code in a very different way and see if it changes the behavior, but this can also lead to not really understand what is going on. With time, you might start predicting what would be the output of the compiler (in what machine-code regards) so you will be efficient in hunting down compiler bugs.

Dynamic allocation issues

You’ll often write code for what is called “bare metal” systems. That is, there is no underlying operating system to handle things for us. And for what memory allocation regards, there’s just a very primitive memory allocator available (if any).

You can allocate a new chunk of memory by calling the malloc() function, or instantiating objects with the new operator (that results, in most cases, in a malloc() call).

Many malloc() implementations may fragment the memory. This means that when you request new memory with malloc(), even if you know that there is available memory, the malloc() call may fail.

This happens mostly because the heap management available in the standard libraries for embedded are not so sophisticated like the ones you can find in desktop/server environments (managed by an OS). If you allocate/deallocate random chunks of memory (especially not in order) in you’ll eventually fragment the memory.

Dynamic memory allocation is not a bad thing itself. Many will argue to death that you should not use it in embedded systems, but from my perspective, it’s safe to use it if you allocate a buffer once and never free the memory.

For example: let’s say that your system has to always expose an UART for receiving commands, and that the user can dynamically adjust the command buffer size at startup using a configuration file. Since the UART will live for the entire execution of the program, then you can malloc() the buffer accordingly to the supplied size parameter, and the allocation will be safe since the buffer will never be freed.

Just remember to keep your data structures and their memory allocations routines under control. That is, you have to think about the possibility (evenly remote) that a malloc() call may fail, and thus your have code has to be prepared to deal with that error, or better, avoid that situation.

Keep your code simple

Let’s say you are an experienced high-level programmer facing your first microcontroller project. It is a very tempting idea the possibility of writing code that can be reused in a lot of future circumstances, you know, like a big generic library that does X for any possible device in existence. You must not… at least for now.

Let me try to explain why with a (slightly redacted) anecdote: one day I received some code that had to do some trivial task. Let’s say it had to configure a microcontroller GPIO as an output and set it in a logically high state, but it wasn’t working. I know that such a task is pretty straightforward and can be done with a couple of lines of code, but instead the code was composed of a namespace with classes defining logic levels, used by the GpioPin class, referenced by the GpioPort class, and so on.

I have to say it is not the wrong approach! But it could be if you are at your first steps in embedded programming. Writing this kind of code will distract you from the main goal: making the thing work and understanding why!

Like the example above about GPIOs, my advice is that if you are writing code for your first robot, don’t try to right away encapsulate the code into a GenericRobot class to be used in any microcontroller. There are so many variants and constraints like available flash and low level configurations that is too much to handle. Again, I am not saying it can’t or shouldn’t be done, but just keep this approach for later.

Debugging is also another reason to avoid these complex objects. Have in mind that you will be mostly dealing with headless devices (no display or user interface). You are lucky if you can debug with a JTAG, less lucky if you have only a serial port, definitely unlucky if you have to debug these monster classes with bare LEDs. Consider that most of the time you won’t be there to see it fail, you won’t have logs and you’ll have to guess what’s going on by just analyzing side effects.

So keep it simple and learn to love global functions and variables!

On third-party libraries

Now it’s the turn of 3rd party libraries. Like the previous item from the list, I’m talking about libraries that handle hardware.

There are many good libraries out there, specially if written for established hardware platforms like the Arduino boards. But if you want to use them in a hardware that is even slightly different, you may face some problems.

Dealing with third-party libraries that handle hardware is very different from downloading a package from npm. Sometimes libraries need to be completed with additional code to make them work in your platform. Sometimes libraries have to be heavily modified.

An example for a typical support ticket here at Artekit:

  • Alice: I’ve purchased X sensor and it’s not working with Arduino Nano.
  • Bob: Mmm. Your code seems OK, but I see you are using SuperFastI2C library. Have you tried without it?
  • Alice: I tried again without SuperFastI2C and now the sensor works fine. Strange, since the exact same code using SuperFastI2C works OK on Arduino UNO.

Including libraries that don’t handle hardware may be a problem too. For example, a library that does dynamic allocations that you are not aware of: it might work on some hardware that has a lot of RAM (and thus nobody reported a problem yet) but in a combination of your hardware with your program flow the library makes the available memory to end.

The message is: don’t rely on the fact that using X library means there are 100% chances that the thing will work.

Using the right tools

Embedded systems have an intrinsic physical interaction with the world around them. Your microcontroller will react to external events and it can influence the behavior of other systems. While in programming environments like web/desktop development you don’t usually need any other tool than the ones already running in your PC/MAC, for embedded programming it is different.

Very often, the only way to debug problems related to that physical interaction with the external world is through the use of external, hardware tools. Soon you’ll realize that it would be easier to debug that misteriously mute I2C bus if you could only measure what’s going on with those microcontroller signals.

In short, be prepared to measure voltage/current, analyze signals and debug in-target.

Nowadays you can find the tools you need for very affordable prices (when comparing prices with those from 20 or 25 years ago). And there are also companies (like us) that provide some quasi-vertical solutions to different problems of the embedded developer: for example, do you want to quickly check if there is communication going through your RS232 cable? There is the Artekit AK-RS232-TESTER.

Don’t worry, the tools you’ll be using right now are not the same as those that folks at Apple use to calibrate the iPhone antenna, so here is a list of what you need as your first toolset:

  • A stable power supply. Better if you can find one with adjustable voltage/current.
  • A multimeter.
  • Basic soldering iron.

For a second, intermediate toolset you need:

  • a way to debug your code while it runs. Here are some:
    • For ARM and ARM Cortex there are many JTAG flashers/debuggers availables (for example the Artekit AK-LINK-2).
    • For ARM Cortex there is the cheaper CMSIS-DAP JTAG alternative (for example the Artekit AK-CMSIS-DAP).
    • PIC users can use pickit3 interfaces.
  • a logic analyzer. For me, it’s optional since I barely use it.

A complete toolset include the most expensive tool:

  • an oscilloscope: problem seen, problem solved.
  • a soldering station, with tips that can reach smaller SMD components pads. This will allow you to solder wires and measure signals with the oscilloscope hands-free.

If you are a hardware designer, you’ll probably have some of these tools already. If you are not, you just need the minimal tools to be able to catch and solve bugs (and eventually confirm that the hardware designer messed it up again! and thus entering the spiral of working around hardware problems through software hacks… but that’s a topic for another article!).

Another thing that I feel to advise if you are starting to debug your very first issues in electronics/programming: you don’t need the absolutely best tools in the world. Don’t worry if the oscilloscope doesn’t have super sampling speed, the best accuracy or if a multimeter can’t measure 10kV power lines. You are not going to determine in the near future if there is some ringing in a DDR4 line. It’s not going to make the difference right now. Purchase it already!

Keep your goals attainable

There are so many great tutorials and projects out there, with very detailed instructions, that will guide you step-by-step on how create and set-up that cloud-based, WiFi garage door opener in no-time.

But this time you want to apply a different approach: you want to do it your way, from scratch, or you have had an idea for a project that nobody thought to make a tutorial yet.

You have my admiration…

The following may sound demoralizing, but intention of the message is to try to help and avoid the most common reason for abandoning embedded and general programming: frustration.

Just be sure that the project you want to do is at the reach of your skills. Think about the question you may ask in a forum when dealing with the first problems. How far is that question from the global objective you want to reach? If the question is about something fundamental, chances are that you are not ready for it.

For example, here is an extremely dramatized version of the type of questions seen in forums and from support in general: “Hello, I want to build an International Space Station and put it in orbit but I don’t know if I have to use Phillips or Torx screws, can you help me?”

The outcome in most cases is that the person will probably get tired/bored/frustrated and then give up with space station building at some insurmountable point.

If you were on a course about embedded programming/electronics, your teacher would gradually lead you towards more complex projects according to the progress of your skills. But since you may be learning by yourself, there is nobody around to incrementally set the bar higher.

There is nothing bad in being project-ambitious (we try to encourage that whenever we can) but you have to know that sometimes the road to completion may require hard work and commitment; that it will no be as easy as following a tutorial (but the gratification will be greater by several orders of magnitude).

Indent your code

A beginner in software development (embedded or otherwise) should set as one of the primary objectives to write clean, indented code.

Code indentation is one of the most important (non-written) rules of programming. Your C/C++ compiler doesn’t really care if your code is indented or not, but it will help you to better spot errors/mistakes when reviewing the code and when debugging. You’ll also be happy to be able to understand your own code once you open the project again a month later.

Even if the following code is syntactically correct, there is an error:

#include <SD.h>
bool goodToGo = false;
void setup()
{
uint32_t count = 10;
Serial.begin(9600);
 if (SD.begin())
{
goodToGo = true;
}else{
goodToGo = false;
}
if (goodToGo)
{
Serial.println("Waiting 10 seconds");
while(count)
count--;
delay(1000);
}
else
{
Serial.println("SD error");
}
}

Now here there error becomes more evident:

#include <SD.h>

bool goodToGo = false;

void setup()
{
    uint32_t count = 10;

    Serial.begin(9600);

    goodToGo = SD.begin();

    if (goodToGo)
    {
        Serial.println("Waiting 10 seconds");

        // Catch ya!
        while(count)
            count--;

        delay(1000);

    } else {
        Serial.println("SD error");
    }
}

Indentation is part of a larger group called programming styles. You can find many programming styles out there (and a lot of debate too!) so go and find one that feels comfortable to you!

Code indentation is also requested by many user forums. They usually will request to enclose your code between some special tags so everyone can quickly understand the code and provide help.

Well indented and clean code also shows others that you love and care about what you do; that it’s not something you are doing just “to get through it, somehow”.

You don’t need an RTOS

I don’t know why (and other people giving customer support may agree with me) many believe that programs will work better if they could run them within an RTOS (Real Time Operating System) environment. Perhaps it is related to the fact that their code always ran on top of an Operating System.

While I strongly support the use of RTOSes (here at Artekit Labs we use one that I wrote), running an RTOS will not make your code work better or faster, instead it will run slower, and you will have to deal with new problems related to concurrency and, believe me, you don’t want these problems right now (while in your firsts steps in embedded programming).

My advice for you could be to start learning about RTOSes just for academic purposes (plain curiosity) and to slowly mastering them but being aware and certain that it will not make your code work faster or better.

Do mistakes and learn from them

I’ve been there too, 20 years ago. I burned a pin of my Renesas M16C in-circuit emulator by measuring it with a multimeter. That lesson had cost me several hundreds of USD. Or the time I’ve spent a couple of weeks chasing some silly bug. There is only one way to monetize those losses, and that is by learning from them.

Don’t be afraid to make mistakes. In embedded programming and electronics, mistakes also mean physical mistakes, like burning a component or designing a board that doesn’t work as it should. It happened to all of us.

The thing is that you don’t have to make those mistakes to let you down. I think the proper word here would be perseverance.

In this closing tip, I would like to wish you a lot of mistakes, because making mistakes means two things:

  • that you are doing something, and that you are making rich use of your time (in Italy there is this saying: “chi non fa non sbaglia”, that would be something like: “those who do nothing do not make mistakes”)
  • and that you are going to understand and learn something valuable.

Make mistakes, learn, and most of all, have fun!