Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

View file
nameFirmware 102 - S22.pptx

Introduction

Welcome to firmware 103102! 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.

Info

NOTE: Message Aryan Kashem or Aditya Sen with your github username to get added to the repository

Info

NOTE: All of todays activities should be performed on your own branch off master. To set this up do the following:
git checkout main
git pull
git checkout -b fw_onboarding_[your name]

You are now on your own branch, which is up to date with master

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 STM32f1 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:

...

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.

...

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.

...

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 fwxvfw_onboarding/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.

...

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.

Code Block
breakoutModewide
breakoutWidth760
languagec
//
/**
 * @brief   Initializes GPIO globally by setting disabling JTAG and enabling all pinsGPIO toclocks
their default* state.@details ONLY // CALL ONCE or it will deinit all current settings. Change pin setting by calling
// gpio_init_pin.
StatusCode gpio_init(void);

// Initializes a GPIO pin by address.
StatusCode *          gpio_init_pin(const
GpioAddress *address, const@return GpioMode pin_mode, GpioState init_state);

// Set the pin state by address.
STATUS_CODE_OK if intialization succeeded
 */
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 itinit(void);

/**
 * @brief   Initializes a GPIO pin by address
 * @details GPIOs are configured to a specified mode, at the max refresh speed
 * @param   address Pointer to the stateGPIO //address
that is* passed@param 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.

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

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 control

  • To 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
languagec
#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
languagec
// 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).

  1. 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:

    1. Leave most things as default except for the Field “MODE”. Set this bit to a 0.

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

  2. 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 function i2c_read_reg and the appropriate arguments. The arguments are as follows:

    1. Port: The function signature contains a config struct. The struct has a member i2c_port so use this value as the port argument.

    2. Address: Same as the port, use the i2c_addr member as the address argument.

    3. Register: Look at ads1115.h. Within the ADS1115_Reg enum, there’s a list of registers. You want to read from the conversion register, so use that value as the argument.

    4. Buffer/reading: This is the buffer to which the converted value is read into. Pass the reading pointer into this.

    5. Bytes: This is the number of bytes of data you want to read back. How many bytes are in a uint16_t?

  3. 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 -2.048V 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 4.096V. 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!

...

pin_mode Pin configuration mode
 * @param   init_state Initial GPIO state for output pins
 * @return  STATUS_CODE_OK if pin initialization succeeded
 *          STATUS_CODE_INVALID_ARGS if one of the parameters are incorrect
 */
StatusCode gpio_init_pin(const GpioAddress *address, const GpioMode pin_mode, GpioState init_state);

/**
 * @brief   Initializes an alternate function for a GPIO pin by address
 * @details This initialization should be used I2C, SPI, UART and timers
 * @param   address Pointer to the GPIO address
 * @param   pin_mode Pin configuration mode
 * @param   alt_func Alternate function of the GPIO
 * @return  STATUS_CODE_OK if pin initialization succeeded
 *          STATUS_CODE_INVALID_ARGS if one of the parameters are incorrect
 */
StatusCode gpio_init_pin_af(const GpioAddress *address, const GpioMode pin_mode, GpioAlternateFunctions alt_func);

/**
 * @brief   Sets the GPIO pin to a valid state
 * @param   address Pointer to the GPIO address
 * @param   state GPIO state can either be HIGH/LOW
 * @return  STATUS_CODE_OK if pin writing succeeded
 *          STATUS_CODE_INVALID_ARGS if one of the parameters are incorrect
 */
StatusCode gpio_set_state(const GpioAddress *address, GpioState state);

/**
 * @brief   Toggles the GPIO
 * @param   address Pointer to the GPIO address
 * @return  STATUS_CODE_OK if pin toggling succeeded
 *          STATUS_CODE_INVALID_ARGS if one of the parameters are incorrect
 */
StatusCode gpio_toggle_state(const GpioAddress *address);

/**
 * @brief   Gets the GPIO state
 * @param   address Pointer to the GPIO address
 * @return  STATUS_CODE_OK if pin reading succeeded
 *          STATUS_CODE_INVALID_ARGS if one of the parameters are incorrect
 */
GpioState gpio_get_state(const GpioAddress *address);
  • 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.

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
breakoutModewide
breakoutWidth760
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 fw_onboarding/libraries/ms-common/inc/gpio_it.h.

This is the main method that is used for GPIO interrupts:

Code Block
breakoutModewide
breakoutWidth760
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.

FW 102 Homework - Multi-Channel ADC

Part 1 - Initializing the hardware

Here, we’re going to work on a program to toggle some LEDs! The goal is to get an LED to toggle every second.

1.1 - Setup

You should’ve already created your own git branch, from the note at the top of the page.

git checkout -b fw_onboarding_[your name]

Now moving onto the code, open up main.c within the fw_102_103 project (We will be using this code for FW 103!). This is a template 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 mcu.h at the top of our program to give us access to the functions and types related to MCU initialization

  • To control any hardware, we must first initialize our STM32 to a known state using mcu_init(). This initializes GPIO pins and system clocks

Code Block
breakoutModewide
breakoutWidth760
languagec
#include "mcu.h"
...

int main() {
  mcu_init();
  ...
}

1.2 - Initializing a GPIO

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. Lets take a look at an electrical schematic from our teams Altium:

...

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
breakoutModewide
breakoutWidth760
languagec
// GpioAddress struct
static GpioAddress blinky_gpio = {
  .port = GPIO_PORT_B,
  .pin = 3,
};

gpio_init_pin(&blinky_gpio, GPIO_OUTPUT_PUSH_PULL, GPIO_STATE_LOW);
// !!!Initialize the pin before the while loop in main or before tasks_start()!!!

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 FW 102 and 103 deliverable is to create a program that takes a reading every 100ms. FW 102 will focus on setting up communication with the external ADC chip! 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 8.1.3 and table 8-3 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 with FW103 START and FW103 END 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).

  1. In the ads1115_init() function - 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 25 and 26 of the datasheet. Table 8-3 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:

    1. Leave most things as default except for the Field “MODE”. Set this bit to a 0. Hint: Set the OS bit to 0. This is because our ADS1115 will never be in the ‘power down’ state

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

  2. 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 function i2c_read_reg and the appropriate arguments. The arguments are as follows:

    1. Port: The function signature contains a config struct. The struct has a member i2c_port so use this value as the port argument.

    2. Address: Same as the port, use the i2c_addr member as the address argument.

    3. Register: Look at ads1115.h. Within the ADS1115_Reg enum, there’s a list of registers. You want to read from the conversion register, so use that value as the argument.

    4. Buffer/reading: This is the buffer to which the converted value is read into. Pass the reading pointer into this.

    5. Bytes: This is the number of bytes of data you want to read back. How many bytes are in a uint16_t?

  3. Completing the read converted function - The ADC gives you a SIGNED 16 BIT value between 0 and 32768, which is the int16_t max value. Let’s assume in this driver that this represents a reading between -2.048V 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/32768 of 2.048V. Use that logic to convert the readings)

    Hint General ADC formula:
    Converted_voltage = ( Raw_reading Max_reading ) * Max Voltage reading

  4. In the ads1115_init() function - Setting the low and high threshold values to 0V and 1.5V, respectively - Remember that this driver represents a reading between -2.048V and 2.048V (as we had set in the config register using a raw value between 0 - 32768. How can you convert the converted voltage values to a raw reading (Hint: It is the inverse of the previous step)

  5. The last step is to complete the ads1115_select_channel() function. There are 4 channels on the ADS1115. Please make sure you are properly selecting the channel on every read_raw operation! The way most ADCs work is that they have an ANALOG INPUT POSITIVE (AINp) and an ANALOG INPUT NEGATIVE (AINn). They compare the positive and negative voltages to return a value. For our application, ANALOG INPUT NEGATIVE (AINn) will always be GND! This means you can set bit 14, and only modify bit 11/12 based on the channel.
    We have left some code that preserves the value of the config registers. Please make sure you are not clearing the read contents of the config register when selecting a channel!

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

Declare 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
breakoutModewide
breakoutWidth760
static I2CSettings i2c_settings = {
  .scl = { .port = GPIO_PORT_B, .pin = 7U },
  .sda = { .port = GPIO_PORT_B, .pin = 6U },
  .speed = I2C_SPEED_STANDARD
};

static GpioAddress ready_pin = {
  .port = GPIO_PORT_B,
  .pin = 0U,
};

static ADS1115_Config ads1115_cfg = {
  .i2c_addr = ADS1115_ADDR_GND,
  .i2c_port = ADS1115_I2C_PORT,
  .ready_pin = &ready_pin,
};

You should also initialize I2C, and then the ADS1115 before tasks_start() or a while (true) loop like this:

Code Block
breakoutModewide
breakoutWidth760
i2c_init(ADS1115_I2C_PORT, &i2c_settings);
ads1115_init(&ads1115_cfg, ADS1115_ADDR_GND, &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, you are now ready for FW 103!

Final Step: Pushing to the remote repository

On your computer you have a local version of your branch. When you make commits these are stored on this local version. To make them available to other repository members (ie the leads for getting your stuff marked), you need to push them to the remote. You can do this with a

git push or git push origin <your branch name>

 You may need to also run git push --set-upstream origin <your branch name> if it is your first time pushing.

Your changes are now viewable on Github, and you can now go and open a pull request. A pull request is when you submit your completed code for review, and then to be merged into the master branch upon approval. Your code should be fully functional and tested when you do this.

Info

You will need to get added to the repo. Send your github user name to the lead and ask to be added when you get to this step.

To open a pull request, once you push your changes you can go to https://github.com/uw-midsun/fw_onboarding . If you pushed your changes correctly you should see something like this:

...

You can click the compare and pull request button, add a title and description, and open your pull request for review.

2.4 - Interrupts (Optional) - WIP