Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 16 Next »

Introduction

Welcome to firmware 103! By now you should’ve completed your environment setup, run the command to build and run a project on x86 and learned a bit about FreeRTOS. In this lesson you’ll learn about the fundamental of embedded systems development. We’ll be covering:

  • Hardware Crash course

  • GPIO Functionality

  • Interrupts

  • Timers

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 (on or off) or analog (a voltage in some continuous range) 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.

GpioMode- What we will use the pin for (input, output, analog, etc)

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.

// 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 GpioMode pin_mode, GpioState init_state);

// 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 program

  • gpio_init_pin() method configures a pin at a specific address to a specific functionality, and mode. 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() and gpio_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.

  1. Call gpio_init() at the start of your main program.

  2. 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.

  3. 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, with the scons new --project=<project_name>):

  • 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() at the start of our main function (if your code doesn’t work this is probably why)

1. Create new GPIO_LED task

Create a new task using the lessons learned from FW102, in which we will execute our GPIO functionality. If you don’t remember how to do this, take a look back at the lesson and your previous code.

2. Setup the GPIOs for use

Next we need to configure our GPIOs. This will happen before the while(true) loop in our new task.

We can build a GPIO settings struct, using the GPIO addresses of our LEDs on the board. These 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!

We will need to configure each of them individually (call gpio_init for each of the addresses in the table below). The mode we should use for each is GPIO_OUTPUT_PUSH_PULL, and they can be initialized with either GPIO_STATE_HIGH/LOW.

These are the pin addresses:

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 a different GpioAddress struct for each pin.

This is what the gpio_init_pin function looks like.

StatusCode gpio_init_pin(const GpioAddress *address, const GpioMode pin_mode, GpioState init_state);

As you can see, it takes a pointer to an address, and configuration params. 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,
                                      const Event event, const Task *task);

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 for

  • InterruptSettings - Don't worry too much about this one. It will pretty much always be
    type = INTERRUPT_TYPE_INTERRUPT, and priority = INTERRUPT_PRIORITY_NORMAL

  • InterruptEdge - 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?

  • Task * - This is the task which is notified*

  • Event - An integer at which the selected task will be notified.

*Our interrupt framework uses Direct to Task notifications.
Essentially, this is an array of 32 bits that each task has, which can be signalled at a certain bit position when an event occurs, and then the task can check its notification value to see if an event has occurred.

When initializing an interrupt, you pass it the task you want to signal when it occurs, and the Event (bit position) that it will be signalled at. In the task, you can then call notify_get(&notification) which will get the value of the notification, and notify_check(&notification, interrupt_event) to see if a given interrupt has been triggered.

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” and “gpio_it.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 with the others for our button pin (all of the LED initializations can stay the same).

Use port A and pin 7 for your button address

  • Mode: GPIO_INPUT_PULL_DOWN - This pin is acting as an input. Additionally, it will be pulled to a logic low by an internal resistor. 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 triggering

  • State: GPIO_STATE_LOW - This could also be left blank, it only really matters for outputs

We will need to call gpio_it_register_interrupt() in our main function as well to initialize our interrupt. Let's arbitrarily define a BTN_INT_EVENT as 5 at the top of our program and initialize our interrupt with the name of our gpio task as the Task pointer, and the event as BTN_INT_EVENT.

Operation

We need to alter the functionality of our program as well. First we must add a check for our notification value. In our while loop of our gpio_task, add a line to get the notification, and use the check notification function to see if our BTN_INT_EVENT is in the notification value. Only if this function returns true should we toggle our pins.

(If you are doing the training remote or cannot access hardware (currently we don’t have boards with buttons), you can use the function gpio_it_trigger_interrupt() to mock the button behaviour. Add this function the the slow cycle, and every time it is called your interrupt should be triggered.

Conclusion

Congrats! You’ve finished all the tasks for FW 103! 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:

Thanks for coming out, and looking forward to seeing you there!

  • No labels