-
Notifications
You must be signed in to change notification settings - Fork 4
Blinking an LED
Blinking an LED seems simple at first, but in order to do it one needs to know a lot about C, microcontrollers and the structure of the CU InSpace code.
The avionics code already has a blinking LED. If we want to do one ourselves, we will need to remove that code first. To do this open main.c and do the following steps:
- Remove lines 434 to 453 in the
main()function (the big block of lines that all start withgpio) - Remove what is now lines 483 through 496 of
main.c(the section ofmain_loop()that flashes the LEDs)
At this point the project should still build (though there might be a couple warnings, ignore them). If you run the project on a board, no LEDs should flash.
The CU InSpace code is split into a number of modules. Each module provides a set of functions which are used to interact with it. Many modules provide an initialization function which is called once when the avionics board first starts and a service function which is called repeatedly for as long as the avionics system is running. In order to blink an LED we will create a module with these two functions. Our initialization function will configure the pin that the LED is connected to and our service function will cause the LED to blink at a regular interval.
We need to create new two files in the src directory of the CU InSpace repository for our module, blinky.c and blinky.h. The C file contains all of the functionality of the module, the header file (blinky.h) contains information about the module which needs to be accessible to other parts of the code. Our header file will contain a declaration for each of our module's functions.
Before, we write any of the code though, there are a few formalities that needed in every file. Firstly, our coding standards specify that every file must start with a comment in a certain format which gives some information about the file. The comment for blinky.h should look like this:
/**
* @file blinky.h
* @desc Module to blink an LED
* @author Your Name Here
* @date 2019-10-01
* Last Author:
* Last Edited On:
*/The comment for blinky.c will be the same, but with .c in the name instead of .h.
/**
* @file blinky.c
* @desc Module to blink an LED
* @author Your Name Here
* @date 2019-10-01
* Last Author:
* Last Edited On:
*/In our header file, we also need a set of preprocessor directives called an include guard. These lines prevent an issues which could occur if a header was included multiple times. They look like this:
#ifndef blinky_h
#define blinky_h
// All the code in the header file goes in here
#endif /* blinky_h */In every header file we should also include the file global.h, this header contains some definitions which are used by every module in the project. At this point blinky.h should look like this:
/**
* @file blinky.h
* @desc Module to blink an LED
* @author Samuel Dewan
* @date 2019-10-01
* Last Author:
* Last Edited On:
*/
#ifndef blinky_h
#define blinky_h
#include "global.h"
#endif /* blinky_h */Now we can add our function declarations to blink.h. The declaration for the Blinky module's initialization function should look like this:
extern void init_blinky (void);When a c file which includes our header is compiled, this line lets the compiler know that there is a function somewhere named init_blinky which does not require any arguments to be passed to it and which does not return a value. This allows functions in the c file which included our header to call our functions.
The definition for the service function for the module, which will be called repeatedly, is very similar:
extern void blinky_service (void);Our coding standards also require a comment which describes each function in a header file, after adding these comments (which are very small because our function do not take any arguments or return any values) the completed blink.h will be as follows.
/**
* @file blinky.h
* @desc Module to blink an LED
* @author Samuel Dewan
* @date 2019-10-01
* Last Author:
* Last Edited On:
*/
#ifndef blinky_h
#define blinky_h
#include "global.h"
/**
* Configure a beautiful blinking light.
*/
extern void init_blinky (void);
/**
* Service for light blinking to be called in every iteration of the main loop.
*/
extern void blinky_service (void);
#endif /* blinky_h */Now we have declared our functions but we still need to write the function definitions in blinky.c. The definitions contain the functions' code, which tells the compiler what the functions should do. For now we will just write some empty definitions for our functions, blinky.c will end up like this:
/**
* @file blinky.c
* @desc Module to blink an LED
* @author Your Name Here
* @date 2019-10-01
* Last Author:
* Last Edited On:
*/
#include "blinky.h"
void init_blinky (void)
{
}
void blinky_service (void)
{
}We always include the header file for our module in the C file. This makes the function, variable, and type declarations from our header file available in our C file.
We have now create a module, but it's function are not being called anywhere. In order for our module to do anything we need to make sure that it is being called. To do this we need to add three lines to main.c.
First we need to include blinky.h so that the functions we declared will be visible in main.c. Add the following line near the top of main.c, where the other includes are:
#include "blinky.h"Then we need to call our initialization function, add the following line near the end of the main() function before the infinite loop (around line 459):
init_blinky();Finally, we need our module's service function to be called in the main loop. Add this line at the beginning of the main_loop() function (near line 485):
blinky_service();Now our module is integrated into the avionics code. The init_blinky() function will be called once when the avionics board is first turned on and the blink_service() function will be called repeatedly for as long as the avionics is powered.
Now that our service is set up, we can write the code that will blink the LED. Two ways of doing this are explained bellow.
The first method uses the GPIO module which provides functions that abstract over the details of controlling IO pins. This is how we control GPIO in most cases as it hides much of the complexity of GPIO and it lets us control IO pins without having to know whether they are microcontroller pins, pins on the MCU board's IO expander or even pins on one of the radio modules.
The second method is controlling the microcontroller's GPIO pin directly. While this is not how we would normally control an LED, it is a good example to learn about GPIO pins and memory mapped peripherals.
In order to use the GPIO module, we will need to include gpio.h. We will also include config.h in order to get access to a macro which defines the location of the debug LED. In blinky.c, below #include "blinky.h" add the following two lines.
#include "config.h"
#include "gpio.h"Before we can turn on the LED we need to configure the GPIO pin that it is connected to as an output. The GPIO module provides a function for this, in init_blinky() we need to call the gpio_set_pin_mode() function to configure the debug LED pin as an output. The function call with the proper arguments looks like this:
// Configure the DEBUG0 LED as an output with high drive strength
gpio_set_pin_mode(DEBUG0_LED_PIN, GPIO_PIN_OUTPUT_STRONG);In the blinky_service() function we need to toggle the value of the LED pin at a certain interval. To do this we will use a variable called millis from global.h. There is code in main.c which causes this variable to be incremented once every millisecond. In our service function we can keep track of the last time that we toggled the LED and compare that time to the current value of millis to know when we need to toggle the LED again. When it's time to toggle the LED we can use the gpio_toggle_output() function.
After adding this logic to the blinky_service() function the complete blinky.c should look like this:
/**
* @file blinky.c
* @desc Module to blink an LED
* @author Your Name Here
* @date 2019-10-01
* Last Author:
* Last Edited On:
*/
#include "blinky.h"
#include "config.h"
#include "gpio.h"
void init_blinky (void)
{
// Configure the DEBUG0 LED as an output with high drive strength
gpio_set_pin_mode(DEBUG0_LED_PIN, GPIO_PIN_OUTPUT_STRONG);
}
void blinky_service (void)
{
static uint32_t last_blink_time = 0;
// Check if enough time has elapsed since the last blink
if ((millis - last_blink_time) > 250) {
// Update the last time at which the LED was toggled
last_blink_time = millis;
// Toggle the LED
gpio_toggle_output(DEBUG0_LED_PIN);
}
}This code should build without any errors and when downloaded to the MCU board it will toggle the DEBUG0 LED four times per second.
In order to blink the LED without using the GPIO library we will need to control the microcontroller's PORT peripheral directly. We do this by writing to the PORT peripheral's registers.
The SAMD21 microcontroller's GPIO pins are split into two ports, port A and port B. Each port has a number of pins. The DEBUG0 LED on the revision A and B MCU boards is connected to port B pin 15.
Peripheral registers can be thought of as special variables that are used to control peripherals. Registers have memory addresses which means that they can be accessed from C using pointers. The meaning of the values for a register are given in the data sheet, often small groups of bits or every individual bit of a register will have its own meaning. Peripherals are controlled by writing different values to their control registers, many peripherals also provide data back to the processor by changing the values of registers.
Section 23 of the SAMD21 data sheet (available on our data sheets page) describes the PORT peripheral, which is used to control GPIO pins. From this section we find that in order to use a pin as an output we need to write a 1 to the bit for that pin the the PORT peripheral's DIR register. We can then control the value of the pin with the OUT register. There is also a PINCFG register for each pin, we can use a bit in that register to enable high drive strength for our LED pin which will allow the LED to use more current.
The microcontroller's data sheet provides us the memory addresses for the registers of each peripheral. In C, we can cast these addresses to pointers in order to manipulate the registers. Casting the address of the PORTB DIR register looks like this:
(volatile uint32_t*)0x41004480The pointer is cast to a uint32_t since this register is 32 bits wide. Some registers are 8 or 16 bits wide and they would need be cast to an appropriately sized type. The type has the volatile qualifier to indicate to the compiler that the value of this register could change in a way that the compiler cannot predict. When a variable is declared as volatile the compile knows not to make any optimizations which assume that the value of the variable doesn't change. If we don't use the volatile keyword for registers the compiler could decide that some of our operations on the register are unnecessary (since we change the register, but we might never actually read back the value after having changed it) and remove the operations entirely. The volatile keyword also indicated that the variable should not be cached or stored in a CPU register.
Once we have this pointer it can be used like a pointer to a normal variable, assigning a value to the register would look like this:
(*((volatile uint32_t*)0x41004480)) = 5;Not that we have to dereference the pointer to change its value. To make things easier to use we often define macros for registers which we can read from and write to just like variables, for example:
#define PORTB_DIR (*((volatile uint32_t *)(0x41004480)))
uint32_t x = PORTB_DIR;
PORTB_DIR = 7;The DIR and OUT registers for PORT B have a bit for each pin in the port. We want to configure only a single pin so we will have to change only one bit of the register without affecting any others. To do this in C we use the bitwise and, or and xor operators.
To set a bit, we can use the or operator. We take the current value of the register, or it with a variable that has only the bit that we want set in it and then assign the result to the register.
#define PORTB_DIR (*((volatile uint32_t *)(0x41004480)))
PORTB_DIR = PORTB_DIR | (1 << 15);
// Is equivilent to:
PORTB_DIR = PORTB_DIR | 0b00000000000000001000000000000000;
// It can be shortened to:
PORTB_DIR |= (1 << 15);The or operation is performed for each bit individually, the bits that are zeros in our mask will still have whatever value they originally had (x | 0 = x) and the bit which is a one in our mask will become a one in the register no matter what value it had beforehand (x | 1 = 1).
To clear a bit, we use the and operator in a similar way.
#define PORTB_DIR (*((volatile uint32_t *)(0x41004480)))
PORTB_DIR = PORTB_DIR & ~(1 << 15);
// Is equivilent to:
PORTB_DIR = PORTB_DIR & 0b11111111111111110111111111111111;
// It can be shortened to:
PORTB_DIR &= ~(1 << 15);The and operation is performed on each bit individually, the bits that are ones in our mask will still have whatever value they originally had (x & 1 = x) and the bit which is a zero in our mask will become a zero in the register no matter what value it has beforehand (x & 0 = 0).
Toggling a bit works the same way as setting a bit, but with the xor operator. This works because any bits xored with zero will be unchanged while any bits xored with one are flipped.
In order to the LED pin an output we need to set the corresponding bit in port B's DIR register. We also want to set a bit in the PINCFG register for our LED pin (address 0x410044cf) to enable high drive strength. Our init_blinky() function looks like this:
void init_blinky (void)
{
// Configure the DEBUG0 LED as an output
(*((volatile uint32_t *)(0x41004480))) |= (1 << 15);
// Enable high drive strength for DEBUG0 (bit 6 is DRVSTR)
(*((volatile uint8_t *)(0x410044cf))) |= (1 << 6);
}To toggle the LED we will use the same logic we used with the GPIO library, but instead of calling gpio_toggle_output() we can use the xor operator to flip the bit for the DEBUG0 LED in the OUT register. The final file looks like this:
/**
* @file blinky.c
* @desc Module to blink an LED
* @author Your Name Here
* @date 2019-10-01
* Last Author:
* Last Edited On:
*/
#include "blinky.h"
void init_blinky (void)
{
// Configure the DEBUG0 LED as an output
(*((volatile uint32_t *)(0x41004480))) |= (1 << 15);
// Enable high drive strength for PB15 (bit 6 is DRVSTR)
(*((volatile uint8_t *)(0x410044cf))) |= (1 << 6);
}
void blinky_service (void)
{
static uint32_t last_blink_time = 0;
// Check if enough time has elapsed since the last blink
if ((millis - last_blink_time) > 250) {
// Update the last time at which the LED was toggled
last_blink_time = millis;
// Toggle the LED by flipping bit in PORTB's OUT register
(*((volatile uint32_t *)(0x41004490))) ^= (1 << 15);
}
}Keeping track of all of the register addresses for all of the peripherals in even a moderately sized microcontroller is a difficult task. Luckily most manufacturers provide header files which define macros for all of the registers in a particular part. ARM microcontrollers generally come with headers in ARM's CMSIS (Cortex Microcontroller Software Interface Standard) format. These headers not only provide macros for the addresses of registers and the bits within them, they also provide structures which define the layouts of the registers in each peripheral and provide bitfields for all of the individual fields of each register. If the Blinky service was written using the CMSIS headers for the SAMD21 it would look like this:
/**
* @file blinky.c
* @desc Module to blink an LED
* @author Your Name Here
* @date 2019-10-01
* Last Author:
* Last Edited On:
*/
#include "blinky.h"
void init_blinky (void)
{
// Configure the DEBUG0 LED as an output
PORT->Group[1].DIR.reg |= (1 << 15);
// Enable high drive strength for DEBUG0 (bit 6 is DRVSTR)
PORT->Group[1].PINCFG[15].bit.DRVSTR = 1;
}
void blinky_service (void)
{
static uint32_t last_blink_time = 0;
// Check if enough time has elapsed since the last blink
if ((millis - last_blink_time) > 250) {
// Update the last time at which the LED was toggled
last_blink_time = millis;
// Toggle the LED by flipping bit in PORTB's OUT register
PORT->Group[1].OUT.reg ^= (1 << 15);
}
}