Intro to Firmware

This article is copied from this document here.

### Purpose

This document outlines the high level goals, expectations, and requirements when writing firmware for any of the components we design on the car. This is not specific to any one project.

### What is Firmware?

Firmware is code written for a microcontroller (a small, very dumb computer) that performs one or more actions to accomplish a goal.

For most firmware, it must be written in a way where it is directly aware of the hardware it is being run on. Higher level firmware can be written on top of low level firmware to abstract away the hardware directly, but most of our firmware is not that high level.

The vast majority of our firmware is written in C/C++/Arduino because it gives the most direct access to the hardware. Occasionally you’ll find other languages imported in (HTML/CSS/JS/Python), but we won’t go into that here.

The most common example of something a microcontroller would do is reading the output of a sensor and reporting the value it reads to a computer.

When performing the above sensor-reading task, there are a number of things to consider:

* How is the microcontroller reading this sensor output?
* How does it store the results of this output in memory?
* How often does it read this sensor output?
* How does it communicate this data to the user?
* How does the data need to be formatted?

To answer the above questions, one must understand the hardware the system exists on from at least a high level. To answer these questions in particular:

* The sensor output must be connected to the microcontroller (MCU) in some way. The MCU has a set of electrical connections directly to the processor called “pins” that it can use to interact with the world. The output of the sensor will usually be connected to one or more of these pins so the MCU can then interpret the voltage it measures on that pin. The most common type of sensor (an analog sensor) operates by fluctuating the voltage at that pin between two bounds where the value has some function which can be used to define what value we care about. For example, maybe you’re reading the temperature in a room and the sensor outputs values between 0C and 100C on the voltage range of 0V to 3V and the mapping between the two is linear. Using this, if the MCU reads a value of 1V from the sensor, this correlates to a value of ~33C. This means you can take the voltage you get from that pin, multiply it by 33, and you will always get (approximately) the right temperature. This can then be stored in a variable.
* Now we have the sensor value, but how does it store it? Some great questions to ask are: Do we need to log these sensor values over time? Or do we only care about the most recent value? What precision do we need for this value? Does it need to have decimal places (floats vs ints)? Does the value ever go negative or need to involve calculations with negative numbers (signed vs unsigned values)? Let’s say in our example that we don’t care about decimal values and the values will only be positive, then we’re going to be using an unsigned integer. Next, we just want to read the most recent value and report it to the user’s computer and we don’t care about storing the data (no moving average or anything), so we can just have one variable that stores this temperature value.
* The frequency you read a sensor output entirely depends on understanding why you’re reading the sensor in the first place. For example, if you’re reading the temperature of your apartment over time, that probably changes really slowly, so reading it once every few minutes should be more than enough. Conversely, if you’re measuring the temperature of a rocket engine, that can get really fast really quickly, so you might want to read that tens or hundreds of times per second. Let’s say we are reading the temperature of the battery in the car (which can get hot somewhat fast), so we’ll read it once per second. How do we do this? We write an IF statement that waits until the time since the last sensor reading is greater than a second to do it again. Jump to the “Time” section to learn more.
* While it’s great to read the sensor, if we never do anything with that data, it’s kind of useless. In our example, we want to give that data back to the user’s computer (which is very common). We’re going to do this using the serial communication interface, or just serial for short. If you’re using Arduino, you can do this by using Serial.print(). Before we can use serial, we have to tell the microcontroller how to set it up for us. The easiest way to do this in Arduino is using Serial.begin(115200) once at the beginning of your code where 115200 is the “baud rate,” or the rate at which data (symbols) is sent per second. After that, you can use Serial.print(“Hello World!”) to print out Hello World to your computer. You can see what it’s printing in Arduino by opening the Serial Monitor (in the top right corner, or Tools > Serial Monitor). If we want to print out our sensor, the Serial.print() command is really smart and will automatically convert our sensor value into a string to be printed. Let’s say we store our temperature in a variable called battery\_temp, then we could run Serial.print(battery\_temp) and you’ll see the value printed on your laptop! It doesn’t automatically move to a new line to print each new value, so you might be seeing 333333333…, you can have it print to a new line for every new value it sends by using Serial.println() instead in the same way.
* Now that we’re sending data back to the user’s computer, let’s get a little fancy with it! Let’s say we want to format the data as “Battery Temperature: X degrees C”. The simplest way to do this (especially if you’re new to coding) is to put a bunch of print statements together. We could write Serial.print(“Battery Temperature: “) then Serial.print(battery\_temp) then Serial.println(“ degrees C”) and that would get our desired result!
	+ If you’re familiar with printf, that works here too, so you could also do Serial.printf(“Battery Temperature: %d degrees C \r\n”, battery\_temp), but that breaks on some older MCUs just as a heads up

And that’s it! Assuming you already know some C++ basics, this should be all the basics you need to start writing basic firmware for microcontroller systems.

### Tasks

You don’t always just want your MCU to do one thing. Sometimes (most of the time, I’d argue) you want your MCU to be able to do multiple things. Let’s call anything the MCU has to do multiple times a “task”. This task could be anything from reading a sensor input to turning on an LED to sending a message to your computer and more. While it is always good coding practice to break up the actions your code is completing into separate functions for the sake of modularity and readability, this has additional benefits for MCUs. In particular, breaking up code into separate tasks allows you to control how frequently they occur relative to other tasks, something that will be discussed in more depth in the next section.

### Time

Unlike many programming languages where you simply want to execute a certain task as fast as possible, writing firmware becomes a game of resource management, and one of the most important resources is time. In particular, the MCU can only do so many things per second, so if the MCU tries to do two or more things at once, you as a programmer need to tell it how to split its time between the two. There are generally 3 accepted approaches for handling tasks on any microcontroller system:

1. Continuous Blocking
2. Non-Continuous Blocking
3. Non-Continuous Non-Blocking

Continuous Blocking is an approach where the MCU completes every task it has in order, one after the other, every iteration of the main while loop. This approach is very easy to implement (because you don’t have to think about timing), but it doesn’t let you control how often multiple tasks are occurring separately. You can control the speed of the whole loop by inserting a delay statement (which sits and does nothing for some duration of time before continuing), but it doesn’t let you do any one task more frequently or faster than any other task, so you might be wasting time performing a task  more often than is needed.

This approach is great for single-use systems which only have one job that you want to happen as fast as possible or at one set interval. Something like a basic data translator which takes in sensor data and transmits it to the CAN bus could use this approach without issue. It also has basically no overhead meaning it is using 100% of the MCU’s time actually doing the tasks you care about.

Non-Continuous Blocking is an approach which allows the MCU to execute different tasks with different frequencies by using timers (software or hardware timers). This approach is fairly easy to implement and allows a much higher level of control of the frequency with which tasks are occurring. For example, if we want to sample two sensors, one for acceleration and one for temperature, we might want to sample the accelerometer 100 times per second (100Hz) but the temperature only 1 time per second (1Hz). Rather than sampling the temperature sensor an extra 99 times per second when we don’t need to (as you would have to do with the continuous blocking approach), we can use that extra time for the MCU to do other useful things like process the data and transmit it to another MCU.

The most common way to implement this approach in software is to have 2 variables (or 2 members in one struct) for each timer:

1. A variable holding the last time the task was executed
2. A variable holding the time it should wait until the task should be executed again

Arduino (and most MCUs) have some built in function which can give you the time since the MCU last turned on. While this isn’t useful for knowing what time it really is in the world, this is very useful for knowing how long it has been since something else happened while the MCU has been on (eg. the last time the task was executed). Arduino includes two functions for this:

* millis() which returns the time since reset in milliseconds
* micros() which returns the time since reset in microseconds

Both of these are returned as an unsigned long, meaning micros() overflows in about 70 minutes and millis() overflows in about 50 days. The code below is a common, easy way to implement this in your code in software:

```
struct timer\_t {
 unsigned long last\_fired = 0;
 unsigned long duration = 1000;
} t1;

setup(){
 t1.duration = 100; //t1 will trigger every 100ms
 t1.last\_fired = millis(); //avoids misfires from long setup
}

loop(){
 //check if the next time the task should fire has passed
 if(t1.last\_fired + t1.duration <= millis()){
 //reset the timer reference
 t1.last\_fired = millis();
 //actually perform the task
 task\_1\_function\_call\_or\_something();
 }
}

```

Note: This can be done with hardware timers as well, but that varies significantly in implementation from MCU to MCU while this approach is uniform and infinitely scalable across all Arduino-similar MCUs.

The above code scales fairly well for a small to medium number of tasks and is the recommended approach for most introductory firmware solutions with more than one task. Since this approach is still blocking, it is recommended to avoid any large tasks if possible, as a single task that takes a long time can cause other tasks to have to skip multiple iterations. For example, if you’re sampling an accelerometer at 1000Hz and a temperature sensor at 1Hz, but it takes 50ms to sample the temperature sensor, that means the MCU will still spend 50ms not reading the accelerometer as it reads the temperature sensor. If you have a very small number of tasks which have to do something very time sensitive (like certain communication protocols may require), sometimes this can be handled using interrupts (tasks which the MCU can execute at any time when it receives a certain alert which will interrupt whichever task is being performed). However, a larger number of interrupts occurring can start to have negative effects on the performance of your firmware without proper management, leading into the final time management approach…

Non-Continuous Non-Blocking is an approach which allows tasks to dynamically be stopped and started in the middle of their execution to most efficiently meet the timing requirements of all the tasks in a system. Using the above temperature and accelerometer example, let’s say the accelerometer task only takes 10 microseconds to complete while the temperature still takes 50ms. Using this approach, the system would start the temperature task, realize that it is supposed to measure the accelerometer again, then it would pause the temperature task where it is, go perform the accelerometer task, then come back to the temperature task and continue working on it. While it means the temperature task might not get done as quickly (since it gets interrupted 50+ times while it executes), the system does meet all the accelerometer task requirements. It still meets the requirements for the temperature sensor as well because it can still perform this task once every second, but it just takes a little longer to get from the start to the finish. One other major difference with non-continuous non-blocking approaches is that, unlike with non-continuous blocking which had a timer to determine when to start the task, this approach cares when you need to end the task by. This is usually called the “deadline,” and there are whole fields of computer science research into the most efficient algorithms for scheduling various tasks to meet their deadlines most efficiently. Obviously this example is quite simple because there are only two tasks and the MCU still has plenty of time to get them done, but imagine how many hundreds or thousands of tasks your laptop is doing to let you read this right now!

The most common way of implementing this timing approach is with a Real Time Operating System (RTOS). These approaches are great because they allow massive scaling of hundreds or thousands of tasks which can all overlap highly efficiently, and that’s why your laptop and phone use them to function. The major downside of an RTOS and the major reason they aren’t used for applications with only a few dozen tasks or less is overhead and complexity. Unlike the second approach which could start up nearly instantly and operate with a trivial amount of management overhead, an RTOS needs to maintain a hypervisor which is tracking all of these tasks and their states in time as well as their upcoming deadlines to ensure all those deadlines are met. Beyond mere IF statements, it has to make decisions on how to most effectively manage its resources, and that also adds a lot of complexity which can make interacting with an RTOS very intimidating for someone new to programming. While these tradeoffs are worth it for a system which needs to always meet certain deadlines or maintain a large number of simultaneous tasks, this usually isn’t worth it for basic control systems like we use on formula.

Summary

All the timing approaches above have their own best use cases, so none are really better than any others.

* Continuous Blocking is the simplest and quickest to implement and has no overhead, so it is best used for single-task applications.
* Non-Continuous Blocking adds a small amount of overhead and complexity but gains amazing flexibility with the timing of multiple tasks simultaneously at the cost of intermittently missing a few tasks.
* Non-Continuous Non-Blocking can handle hundreds or thousands of tasks without missing a single deadline, but this comes with a significant increase in overhead, startup time, and code complexity.

### Good Practices

These are basically the same thing as most other coding languages, so if you’re an expert coder, feel free to give this section a pass.

Modularity

Try to break your code down into as many sub-function as possible where reasonable. If you do this in conjunction with good code documentation, this makes it a lot easier for other people to come in and modify existing to improve functionality without risking breaking some other totally separate part of the code. This also allows files to be broken up into many smaller files which are much easier to find based on a specific purpose. Rather than having to know what the code that reads a temperature sensor looks like and where it is in the main file, you can usually just navigate to a file called temperature\_sensor or maybe a function called temperature\_sensor in a file called sensors to find exactly what you need. This also gives the benefit of making code a lot less “wet”, meaning the code repeats itself as little as possible and only requires code to be changed in one place to implement that change everywhere in the code.

Code Documentation

PLEASE write notes to explain what your code is doing. They don’t have to be crazy, and you don’t have to explain every little line of what you’re doing, but make it sufficient for someone who has never seen this block of code before to understand what it’s doing without spending 20 minutes looking at it. Ideally your code would be written in a way where it explains itself and doesn’t need many comments, but sometimes code gets weird, so please explain it where needed. Otherwise, you’ll write some code that kind of works sometimes, then someone will come back 5 months later to edit it and not understand what’s going on nor how it works and they’ll spend 6 hours reverse engineering how and why it works. That’s a poor use of everyone’s time, so even if you’re the only person working on that code right now, still write notes. Other people (and maybe even future you) will thank you.

Document Anything Else Needed for Compile

This is less of an issue with industry coding platforms like MPLab or Composer Studio, but using Arduino is a double edged sword. On one hand, it’ll help you out a lot by trying magically fix your include statements when it compiles your code--this is great because it helps beginners start developing a lot faster without getting stuck in complicated #include and scope compiler errors. On the other hand, Arduino allows you to add files to a library which it can then pull from when compiling, so even if you send someone else your code, that doesn’t mean they’ll be able to compile it successfully unless they’ve also added those same files to their library.

While this usually isn’t an issue when a group of people have all been working on a project for a while, this can be really disencouraging for new coders who load up a repository and immediately get hit with a compiler error and don’t know what to do. The easiest way to solve this is to:

1. Keep a list of instructions for compiling the code at the top of the main file, including any other libraries or MCUs or software or whatever they need to compile it and upload it successfully
2. Rather than installing files to your library, add it directly to the code you’re working with. This isn’t always a good idea, particularly for larger and more complicated files with large dependency trees, but if you have to download a one-off random .c file from some dude’s github from 2017, it might be better to put it in the code files directly so everyone else working on the project doesn’t now need to go hunting for that same file to add to their library.

Use GitHub and Push Often

If you’re new to coding in general, you might not be familiar with GitHub. It’s an amazing online platform that allows you to maintain versions of your code and share it all through the cloud magically for free. If you want more information about how to start using it, contact Isabel Zhong (or whoever the current Software Lead is) for more info. If a GitHub tutorial has been created, link it here!

The most important thing about working on Formula is that we’re a team. That also means that other people should have access to all your work as much as possible, so if suddenly your laptop dies one day we won’t have just lost all your work. By always keeping your code up to date in GitHub, you don’t risk losing your code, we can always go back to previous versions of code which did work in case new changes have issues, and it lets other people access and work on the same code base as you simultaneously. If you’re already familiar with GitHub, I don’t think I need to stress enough just how great GitHub is (and how many other awesome features it adds, like branching), so take advantage of it--it’ll make everyone’s life easier. This is how real firmware development works in industry, so not only does it make your life easier, but it also better prepares you for future internships and jobs.