Introduction
Welcome to firmware 102! By now you should’ve completed your environment setup and run the command to build and run a project on x86. Just a note, some specific libraries and configuration is needed for this session. Make sure you checkout to the FW 102 Branch, and then create your own branch off of that with your name.
In this lesson you’ll learn about the fundamental of embedded systems development. We’ll be covering:
Hardware Crash course
GPIO Functionality
Interrupts
Timers
We will be using a separate branch for this module and the associated homework.
Run git checkout fw102-s2022
from the fwxv folder. The libraries on this branch are old versions, and have since been changed in our new repo to become compatible with FreeRTOS. However, the old ones are very useful for teaching some of the key embedded concepts
Hardware Crash Course
There is a lot of overlap between firmware and hardware, so it is important that we build a conception of how all of our electrical projects work. Everything that we receive as inputs to our programs, or send as outputs to the surrounding system is in the form of digital or analog electric signals which have some inherent meaning in the real world.
Embedded systems in general are comprised of a computer, an electrical interface, and a surrounding system, and a solar car is no different. We will discuss each of the building blocks specific to our system.
1. Microcontrollers
So far, we’ve only been dealing with running software on your PC, but in reality our software runs on a separate “mini computer” in the car. These are Microcontrollers (aka. MCU, controller, STM, STM32, Integrated Circuit [IC], or other). The one that was used for the MSXIV design is an STM32f0 chip, shown below:
The metal bits sticking out are called “pins”, and are essentially just wires which receive/send all signals to or from the MCU. Each pin has a specific purpose, and are what connect the computation part to the rest of the electrical system. The programs that we write can be loaded onto one of these bad boys, which then runs the code as soon as it receives power. These are like the CPU in your laptop, but 50x cheaper and simpler. The other major difference is that the microcontroller is a comprehensive computer (instead of just a CPU), with memory and peripherals included.
2. Controller boards
There’s a bit of a problem with the chips seen above, in that they are effectively useless by themselves. You have no way of regulating voltages and currents into them, so they will fry as soon as you connect them up to a solar panel or whatever else you are trying to control. If you’ve used Arduinos, STM32 or any other development board, you’ll know that they provide a circuit board with the microcontroller. For us this is our “controller board” which is a PCB (Printed Circuit Board) designed in-house by the Hardware team. You can see what it looks like below:
The microcontroller is in the middle. Surrounding it are various electrical components, ICs, resistors and capacitors, and a “mezzanine” or breakout wiring plug on the far right. Each pin on this mezzanine is connected to a pin on the MCU.
Luckily we don’t need to worry about many of these components. We just need to know which pin (by number) connects where. Controller boards are the same across all the projects, and we allot one to each.
3. System Boards
While the controller board is great, it doesn’t allow us to interact with the broader system. For that we need other PCBs. The example below is from an old version of our steering system. It provides plugs and connections which we would connect to a rotary encoder, for example, as well as doing any electrical adjustment or signal processing to make sure we don’t fry our controller boards.
These boards provide the connections needed to make the signals in the system available to the controller board, which in turn makes it available to the MCU. You can see there is a plug on the right, this is where the mezzanine seen on the controller board plugs in.
Outlined in red below is the controller board mount, so you can imagine the above picture being flipped over and placed on top.
Pictured below is a controller board placed into its mount. You can also see many other components connected to the system board, almost all of which are connected directly to the MCU.
4. Flashing Controller Boards
You might be saying “Hey, we’ve got some neat controller boards with little computers on them, but how am I supposed to get my code on them?”. Great question, thanks for asking.
MCU’s provide protocols for transferring programs into the flash memory. This is a variation of EEPROM (electrically erasable programmable read-only memory). This is non-volatile memory (Will stay the same even when the device loses power), and it is where we store our program (don’t worry too much about the specifics).
For us, what this means is that we need a method of connecting our computer to the MCU and transferring the data over. Luckily, ST Microelectronics provides a super sweet and easy method of doing this over USB, called “ST-Link”. For us, this requires a few components:
There are 3 main components here. The usb connector piece is called a programmer. There is a cable which connects the pins on the programmer to the pins on the MCU, and a jumper wire which provides power to the board. This creates a direct connection between your computer directly and the microcontroller.
If you ever work with a hardware member to test your code, they’ll be using one of these to “program” a controller board. Programming a controller board means writing your code into its memory, then rebooting it so it runs your code. We have command line arguments to do this, so it’s really not too tough to figure this out!
Embedded Concepts
Before we get to writing actual firmware, we will discuss a few concepts that are used in almost all embedded systems. These are:
GPIOs
Interrupts
Timers
GPIOs
GPIOs, or General Purpose Input Outputs are a specific type of pin on an a microcontroller which can be programmed to perform certain functionality. This GPIO Tutorial provides a great intro to the idea of what they are and how our GPIO library works. Essentially we can use them for peripheral interfaces (like I2C, SPI, CAN) or as inputs or outputs to control/read from the system.
Our GPIO Library can be found at fwxv/libraries/ms-common/inc/gpio.h
, and has the following types and functions available:
Types:
GpioAddress
- This is how we can refer to a specific pin on the MCU. The address is comprised of a port and a pin.
GpioDir
- The direction of the pin. This determines whether it is an input or an output. (There is also open-drain but don't worry about this for now)
GpioState
- These are the possible states of the GPIO when reading it or setting it to a logical value. High represents a pin state of 3.3V, Low represents a state of 0.0V.
GpioRes
- This sets the default position of the the pin to low (pulldown) or high (pullup).
// Initializes GPIO globally by setting all pins to their default state. ONLY // CALL ONCE or it will deinit all current settings. Change setting by calling // gpio_init_pin. StatusCode gpio_init(void); // Initializes a GPIO pin by address. StatusCode gpio_init_pin(const GpioAddress *address, const GpioSettings *settings); // Set the pin state by address. StatusCode gpio_set_state(const GpioAddress *address, GpioState state); // Toggles the output state of the pin. StatusCode gpio_toggle_state(const GpioAddress *address); // Gets the value of the input register for a pin and assigns it to the state // that is passed in. StatusCode gpio_get_state(const GpioAddress *address, GpioState *input_state);
gpio_init()
must be called at the start of your main function. It effectively sets up the gpio library, and initializes it for use in your programgpio_init_pin()
method configures a pin at a specific address to a specific functionality, and direction (input or output). The default is just to treat the pin state as a binary value (Logical 1 if it is above a certain voltage threshold, or 0 if below).gpio_set_state()
andgpio_toggle_state()
are used when the pin is treated as an output. gpio_set_state sets the state of the pin to high (a logical 1, or 3.3V), or low (logical 0, or 0V)/gpio_get_state()
reads the current state of the pin as an input. If the voltage is high, then it returns GPIO_STATE_HIGH, otherwise GPIO_STATE_LOW
To get a GPIO pin ready for use, we need to do 3 things.
Call gpio_init() at the start of your main program.
Call gpio_init_pin() for the specific pin you want to use. You need to pass into this function the settings you are trying to configure your pin with, and the address of the pin you are trying to configure.
Call the function to interact with the GPIO pin. This could be gpio_get_state() if the pin is configured as an input, or gpio_<set/toggle>_state() if the pin is setup as an output.
Task 1 - GPIO toggle program
For our first task we are going to modify the program from last week to toggle some LEDs! If you remember from last week, we used a delay to print to the screen every second. We will now toggle the state of an LED every second.
Setup
We need to do a few things to get setup (If you don’t have your project from last week, create a new one, and add a main.c file):
We need to include “gpio.h” at the top of our program, as this allows us to use all the GPIO types and functions discussed above
We need to call gpio_init() (with no parameters) at the start of our main function.
Next we need to configure our GPIOs. We can build a GPIO settings struct in our main function (after we called gpio_init). LEDs are just a specific pin on the board, which can have a High (light on) or Low State. The difference is that we can see when they are on/off! To use our LEDs on the board, we need to configure them with the following settings:
GPIO_DIR_OUT (the LEDs are an output)
GPIO_STATE_LOW (the lights will start off)
GPIO_ALTFN_NONE (just treat as normal GPIOs)
GPIO_RES_NONE (Don’t worry about setting a default state)
There are places for each of these parameters in the settings struct, see if you can figure out which is which! If you are not sure how to create a struct of a certain type, read through this article or ask a lead.
The next step is calling gpio_init_pin for each of our LED pins (there are 4). The following are the addresses for each.
LED | Port | Pin |
---|---|---|
1 | GPIO_PORT_B | 5 |
2 | GPIO_PORT_B | 4 |
3 | GPIO_PORT_B | 3 |
4 | GPIO_PORT_A | 15 |
You will need to create an address struct for each pin (same format as the settings struct). However, since the settings are the same for all of them, you will only need one of those structs.
This is what the gpio_init_pin function looks like.
gpio_init_pin(const GpioAddress *address, const GpioSettings *settings);
As you can see, it takes a pointer to an address, and a pointer to settings. We will need to call this function 4 times (for each address), passing it pointers to the correct structs in the correct position.
Operation
Now that we’ve got the setup out of the way, the actual functionality of the program is much easier. All we are going to do is, in our while loop, call gpio_toggle_state() for each pin, with a 1 second delay in between them. I will leave you to your own devices for this part but don’t be afraid to ask if you get stuck
Once this is done, make sure you can build and run your project using the scons sim command, and then send your main.c file to a lead to run it on hardware!
Interrupts
Interrupts are a somewhat complex topic, but we can discuss them solely in terms of how we use them in our system. (For a video version of this content, see here).
When a program is running, instructions will execute sequentially, starting with the entry point to the program (the main function). However, sometimes we want to change the flow of execution based on external or internal events. This is where interrupts come in. They “interrupt” the normal flow of execution, and cause a separate set of instructions to be executed.
One of the main interrupt sources in our system are caused when external signals change the state of one of our GPIO pins. This could be a signal from a sensor indicating that data is ready to be collected, or many other things, but we will look at the case of a button press.
Assuming that everyone has used a button before (I really hope), you know that a button is generally pressed, or not pressed. In electrical systems, this press will change the state of a circuit to open (no current) or closed (Current flows). This is what a button looks like in a schematic:
S1 is the button itself, which is connected to a 3V input, and the circuit which mediates the signal for the Microcontroller (Fun Fact for any EE friends: the capacitor prevents “bouncing”, which occurs when the button is released and oscillates between on and off). The BTN1 line that goes off to the right connects to a pin of the MCU which we can use to read the button state. When the button is pressed, the circuit will connect, and the pin will go to 3.3V which is the high state.
So let’s say we want to read the button state and use it in our program. We know that the button state is a one or a zero, so after initializing our GPIO to the correct settings we can continuously call gpio_get_state to see if the pin connected to the button has gone high, something like this:
GpioState out_state; while(true) { // Sets the value of out_state to current state of input pin at my_address gpio_get_state(&my_address, &out_state); if (out_state == GPIO_STATE_HIGH) { // Do button processing prv_turn_on_light(); } }
However, this is not a good approach. We are continuously needing to check the state of the button, which means we can’t do other things in our program. Ideally, we would wait until the button is pressed, and then do the processing we need without having to do continual checking. We can do this with interrupts!
Our library for configuring and using interrupts with gpios can be found at fwxv/libraries/ms-common/inc/gpio_it.h
.
This is the main method that is used for GPIO interrupts:
StatusCode gpio_it_register_interrupt(const GpioAddress *address, const InterruptSettings *settings, InterruptEdge edge, GpioItCallback callback, void *context);
This method registers an interrupt to trigger a “callback function” when a certain event occurs. As you can see, it’s got quite a few parameters! Let’s talk about each one:
GpioAddress
- This one we've already seen. This is the specific pin which we are configuring the interrupt forInterruptSettings
- Don't worry too much about this one. It will pretty much always be
type = INTERRUPT_TYPE_INTERRUPT, and priority = INTERRUPT_PRIORITY_NORMALInterruptEdge
- This is important. Interrupts need to detect a change of state to trigger. This is called an "edge". If we want the interrupt to trigger when the pin goes from high to low, we set the edge to INTERRUPT_EDGE_FALLING. If we want it from low to high, it can be set to INTERRUPT_EDGE_RISING, or INTERRUPT_EDGE_RISING_FALLING to trigger on either. Which one is it for the button?GpioItCallback
- This is the function which will be called when the interrupt is triggered*void *context
- This can be set to NULL for now. Effectively it allows you to pass a data type to the callback when it is called.
*This is what is called a function pointer type. Basically, it is expecting a function with a certain return type and parameters. For the gpio_it callback, this must be a function of the following form:void prv_my_gpio_it_func(GpioAddress *address, void *context) {...}
You can simply pass the name of this function as the GpioItCallback parameter
Task 2 - Interrupt Driven LEDs
For this task, we are going to add to our program to use a button to change the state our LEDs.
Setup
First we need to add a couple things that we need now that we are using interrupts.
We need to include “interrupt.h”, “gpio_it.h” and “wait.h”
We need to call interrupt_init() at the start of our main function, right above gpio_init()
We will also need to do another GPIO initialization for our button pin (all of the LED initializations can stay the same). It will need a different GpioSettings struct with the following parameters:
Direction:
GPIO_DIR_IN
- This pin is acting as an inputState:
GPIO_STATE_LOW
- This could also be left blank, it only really matters for outputsResistor:
GPIO_RES_PULLDOWN
- This part is important. If we don’t use a resistor, the pin is liable to float (which means exist in a voltage somewhere between low and high, bad for us). By using a pulldown resistor, whenever the button is not pressed the pin is guaranteed to be low, preventing faulty interrupt triggeringAlt function:
GPIO_ALTFN_NONE
- Don't need to worry about this, just regular GPIO operation
Operation
We need to alter the functionality of our program as well. First we must create a callback function for our interrupt. This should have the following prototype (the return type and parameters must be the same, but the name can be whatever you like):
void prv_my_gpio_it_callback(GpioAddress *address, void *context) {...}
Next, move all of the gpio_toggle_state() function calls to inside this callback. This will cause the lights to change states whenever the button is pressed.
Next we need to register our interrupt using the gpio_register_interrupt() function. See if you can figure this part out based on the description above. It should come right after the gpio initializations. Use port A and pin 7 for your button.
Finally, remove the delay calls, and change your while true loop at the bottom of your function to the following:
while(true) { wait(); }
This will prevent the program from exiting while we wait for an interrupt to happen. Make sure your code compiles and runs on your computer, and then take it to a lead to run it on hardware!
(If you are doing the training remote or cannot access hardware, you can use the function gpio_trigger_interrupt() to mock the button behaviour. By adding this function call and a delay_ms() to your while true loop, it will trigger the interrupt periodically.
Timers
Another really important embedded concept is that of time, and having functionality that is time-dependent. We’ve gone into this a little bit by using our delay library. We can call the delay function to cause the program to wait for a certain period, and this allows us to make something every fixed amount of time.
This delay function (in the way that we’ve been using it) is what’s known as a busy wait. Since we know how fast a cpu can perform one operation based on it’s clock speed (usually on the order of a few gigahertz) we can how many operations we need to perform to pass a certain amount of time. This is how the delay_ms function we have used works.
The issue with this is that it by nature blocks the cpu with useless computation until that time has passed. This is super power consumptive and bad practice. If we want to use periodic functionality in our projects, we need to use our other timer solution, interrupt-driven software timers.
Defined in fwxv/libraries/ms-common/inc/soft_timer.h
, this library allows you to create and start a software timer. Effectively, this timer will run in the background until it completes, at which time it will trigger an interrupt and call a callback function (pretty much exactly the same way that gpio_it works).
To start a timer, all you have to do is call the following function:
soft_timer_start(uint32_t duration_ms, SoftTimerCallback callback, SoftTimer *timer)
duration_ms
- the time that the soft_timer will wait for before triggering the callbackcallback
- the callback to be called. The prototype is slightly different from the GpioItCallback, but otherwise it’s the same ideatimer
- Allows you to select a specific timer, for our purposes can just be set to NULL. (One will be created for you automatically)
Task 3 - Timed LED Flashing
For our last task in this session, we are going to change our interrupt-driven LED program to use software timers to turn off and on every second.
Setup
We need to include “soft_timers.h” to be able to use the timer functionality. It does require interrupts to be initialized (interrupt_init()) but we have that already in our program.
Operation
We still want our callback function to do the same thing, but we need to change the prototype to fit the template of a SoftTimerCallback. It is shown below. I will leave it to you to try and figure out what you need to make the function look like. The syntax used is to define a type of function pointer with a return type, and specific parameter types.
// Soft timer callback, called when soft timer expire typedef void (*SoftTimerCallback)(SoftTimerId id);
Next, you can remove the button initialization (we will still need the LED initializations), and replace it with the call to soft_timer_start(). See if you can figure out what to put in the parameters yourself 😁
Once you’ve setup your soft_timer you can try to compile and run your program. A quick hint, you will need to have a call to soft_timer start in more than one place, since the timer will only run once. Can you figure out a good place to put it?
Once you think you’re done, take your code to a lead, and they will take a look on it and try and run it on hardware.
Conclusion
Congrats! You’ve finished all the tasks for FW 102! You have created your first real firmware programs. For some context, this is what a main file from the steering project in the old car looked like:
In the next tutorial, we’ll be starting an introduction to FreeRTOS. Thanks for coming out, and looking forward to seeing you there!