...
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, 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.
An example GpioAddress struct is
Code Block |
---|
// GpioAddress struct
GpioAddress addr1 = {
.port = GPIO_PORT_B,
.pin = 5,
}; |
This is what the gpio_init_pin function looks like.
Code Block |
---|
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:
Code Block | ||
---|---|---|
| ||
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:
Code Block |
---|
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 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?Task *
- This is the task which is notified*Event
- An integer at which the selected task will be notified.
...
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 triggeringState:
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:
...
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:
Code Block |
---|
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:
Code Block |
---|
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 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?Task *
- This is the task which is notified*Event
- An integer at which the selected task will be notified.
FW 103 Homework - Multi-Channel ADC
Part 1 - Toggling a GPIO
Here, we’re going to work on a program to toggle some LEDs! The goal is to get an LED to toggle every 1 second.
1.1 - Setup
Create a new branch from fw_103_new
. To do this, run git checkout fw_103_new
, then run git checkout -b fw_999_fw_103_hw_your_name
.
Now moving onto the code, open up main.c
within the fw_103 project. This is a blank project for you to add to. You’ll notice a couple of files which we’ll be pulling in during the homework.
Let’s include the header
gpio.h
at the top of our program to give us access to the functions and types related to GPIO controlTo control GPIO pins, we should first initialize the module at the start of main using
gpio_init()
You should also delete
init_master_task()
and those empty functions, which we usually use for project code on the car
Code Block | ||
---|---|---|
| ||
#include "gpio.h"
...
int main() {
gpio_init();
...
} |
1.2 - Creating a task and setting a GPIO
Create a task We learned how to create tasks in FW 102, so take a look there to refresh on how to do that.
Now let’s initialize the pin which controls one of the LEDs on our controller board. As we learned above, a GPIO pin is associated with a port and pin. So, the first thing we need to do is go find our GPIO’s address, which is simply the combination of the port and pin. Let’s take a look at Altium to see what ports and pins the LEDs are wired up to (you may not have access but it’s not quite necessary yet, but feel free to request access so you can start exploring our hardware!).
...
In the image above, we can see the symbols for the four LEDs we have on our controller boards. Additionally, the net name clearly and easily tells us what address is wired to what colour LED (thanks hardware team!). Let’s pick PB3_LED_RED
. So we need to initialize the pin on port B and pin 3. Feel free to pick a different one if you would like.
To initialize the LED, you’ll need to do two things: create a struct to define the address, and initialize that address using gpio_init_pin
. See the code example below for how to initialize the LED, and look around our repo or ask in discord if you’d like more clarity.
Code Block | ||
---|---|---|
| ||
// GpioAddress struct
GpioAddress led_addr = {
.port = GPIO_PORT_B,
.pin = 3,
};
// !!!Initialize the pin before the while loop in your task!!! |
We’re almost there! Now that you’ve got the gpio module as well as a pin initialized - within the while
loop in the task you created earlier, call gpio_toggle_state
with the appropriate arguments and use the delay.h
header to set a 1 second delay between toggles. Don’t be afraid to ask questions if you get stuck! (hint: take a look at the leds project for some code examples)
Part 2 - Multi-Channel ADC
Woah, big topic jump. So we’ve initialized a GPIO, but we can’t do anything fun and interesting with it. So, let’s talk about one of our first peripherals, an ADC. We want to write some code that interacts with an external ADC using the I2C protocol (reading: Inter-Integrated Circuit (I2C) ). The deliverable is to create a program that takes a reading every 100ms. Don’t worry, there’ll be lots of code snippets!
An Analog to Digital Converter simply takes an analog signal (which is really just a signal at some voltage) and converts it into 1s and 0s that we can use to interpret the signal on our microcontroller. For further reading, take a look at ADCs here Analog Digital Converters (ADCs) .
2.1 - Let’s read a datasheet!
A datasheet is something that tells us everything we need to know about some component on our boards. Each component comes with a datasheet supplied by the manufacturer, and is basically a guide telling you “Everything you wanted to know, and never wanted to know, about your component”. During this homework, we’ll be looking at the datasheet of a multi-channel ADC so that we can send the component the appropriate commands to control and read it.
Quick note: there is a ton to learn so if you’re completely new to hardware and there’s a lot of new concepts, don’t worry and create a thread on discord and we’ll be happy to answer anything and everything
Let’s take a look at the datasheet, this is a component from Texas Instruments which you’re all familiar with. The part number is ADS1115. The ADC communicated over I2C. The specifics aren’t important right now, we just need to know that I2C is a protocol which defines a method of communicating between our MCU and the ADC.
Reminding ourselves of our goal as a firmware developer: We want to write code that interacts with the ADC, so what part of the datasheet helps us do that? Taking a look at the table of contents, there’s a programming section on page 22. Awesome, let’s take a look there.
This is a lot of text. It essentially boils down to this:
We need to send I2C commands to the device by performing the following steps
Initialize the device by:
Setting the config register which selects the appropriate ADC channel
Setting the low threshold
Setting the high threshold
Configure the ALRT pin (optional)
Read from the device by:
Performing a “read register” to the conversion register
Okay, so we sort of know what to do now. A lot of this is done for you in the files ads1115.h
and ads1115.c
, but you’ll need to read at least sections 9.6.3 and 9.6.4 to correctly complete the configuration code.
2.2 - Completing the driver
The header file ads1115.h
is fully completed for you. This defines all the functions needed to interface with the device. You will not need to call all of them in main.c
.
Take a look at ads1115.c
. There’s a few spots labelled as TODO
for you to fill out. Lets take a look at each (there are some marked as optional for the optional section 2.4 of the homework).
Setting the config to continuous mode - The call to I2C write register is done for you, what you need to figure out is what value to actually write. Head to page 28 and 29 of the datasheet. Table 8 lays out what each bit means within the 16-bit configuration value. Your job is to construct what 16 bits we’re writing to the ADC by reading the table. Here’s the steps:
Leave most things as default except for the Field “MODE”. Set this bit to a 0.
To do this, look at the value in the “Bit” column. This is the bit number the Field corresponds to. For “OS”, it is defined by just 1 bit, bit 15. See below that when writing out binary, bit 15 is the most significant (to the left) and bit 0 is the least (all the way to the right).
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Completing the read raw function - You’ll need to use the
i2c.h
header to include some I2C functions in your code. To read from the ADC, use the functioni2c_read_reg
and the appropriate arguments. The arguments are as follows:Port: The function signature contains a
config
struct. The struct has a memberi2c_port
so use this value as the port argument.Address: Same as the port, use the
i2c_addr
member as the address argument.Register: Look at
ads1115.h
. Within theADS1115_Reg
enum, there’s a list of registers. You want to read from the conversion register, so use that value as the argument.Buffer/reading: This is the buffer to which the converted value is read into. Pass the reading pointer into this.
Bytes: This is the number of bytes of data you want to read back. How many bytes are in a
uint16_t
?
Completing the read converted function - The ADC gives you a value between 0 and 65535, which is the
uin16_t
max value. Let’s assume in this driver that this represents a reading between 0 and 2.048V (as we had set in the config register). How would you convert the “raw” reading to a voltage? (Hint: think about what a reading of 1 means, that would be 1/65535 of 2.048V. Use that logic to convert the readings.
2.3 - Completing your main code
Nice, now that the driver is done we should be able to use it. Go back to your main.c
and #include "ads1115.h"
.
In the same task you made earlier, before the while loop, create the below structs. You are strongly encouraged to read or ask questions about why these are setup the way they are, but they are provided for simplicity.
Code Block |
---|
GpioAddress ready_pin = {
.port = GPIO_PORT_B,
.pin = GPIO_Pin_0,
};
ADS1115_Config config = {
.handler_task = <your_task_name>,
.i2c_addr = ADS1115_ADDR_GND,
.i2c_port = ADS1115_I2C_PORT,
.ready_pin = &ready_pin,
}; |
Now, you should be able to call ads1115_read_converted
with the correct arguments and get readings from an analog signal, converted into a readable voltage!
Congratulations, this is the main part of the onboarding, you are now free to pick up a task. Please make a pull request with this code and we’ll close it out and get you something to work on ASAP.