Versions Compared

Key

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

This content was presented May 23rd, 2020

...

View file
nameFirmware 102 - S22.pptx

Introduction

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

  1. Running code

  2. Writing code

  3. Tests + Validation

  4. GitHub and JIRA process

Recap

First, as a quick recap of the last lesson, here’s what you should know:

  • Firmware is code that runs as soon as a computer is powered on

...

The non bolded lines in the above diagram are the projects we have as firmware, the bolded lines are categories

  • We work remotely by using x86 versions of our libraries, which don’t necessarily work the same as the real versions we use on hardware

  • We have a 6 step process for completing a project, half of which is testing

  • We collaborate using GitHub and JIRA

Running code

Let’s talk about what “running on hardware” actually means. 

Microcontrollers

At the end of the day, this is what runs your code:

...

  • 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)

...

Each pin has . 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, but at a high level, the pins and are what connect the computation part to the other electrical parts rest of the car. Looking back at the firmware system overview, each project is programmed onto electrical system. The programs that we write can be loaded onto one of these bad boys, which then runs the code upon being powered, as discussed previouslyas soon as it receives power. These are like the CPU in your laptop, but 50x cheaper and simpler.

Hardware interlude

Looking at things from a different perspective, if you have to solder (gluing parts onto the board) these onto a PCB, you probably hate your life. One small thing goes wrong and you have to redo the whole board. Also, boards often undergo revisions from human error or design errors, so every time you make a new version of a board you’d have to redo the soldering for this. This is a PCB.

...

Instead, we have a universal controller board. Pictured below is the underside of the board.

...

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.

...

We have lots of these. So, hardware makes all their boards compatible with this one controller board, then we can reuse them for every project. The pins on the STM are passed through the connections in the slot so it can access the board’s functionality.

Getting code onto the controller boards

This doesn’t matter so much if you’re working remotely, but it’s still good to have a conceptual idea of how your code runs on real hardware.

(this isn’t our hardware, just a picture I found online)

...

The usb looking thing pictured above is called a programmer: it connects your computer directly to pins on the microcontroller.

Ours connects to the pins outlined below:

...

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.

Writing code

Next, let’s talk about actually writing  code! We’ll go through a simple example to show how our programs are laid out.

...

This is from the project controller_board_blinking_leds, which you can find here: https://github.com/uw-midsun/firmware_xiv/blob/master/projects/controller_board_blinking_leds/src/main.c . Don’t worry too much about the details, I just want to point out the main building blocks. This is an example project, but there’s some patterns we always follow.

1. We use an infinite loop structure. This is so we never have to restart the controller board. Otherwise the program would end and the controller board would just do nothing until it was restarted.

...

2. If a function takes a lot of arguments, we group them all together in a struct. If you ever see a struct called “settings”, it’s for wrapping together variables that would have to be passed as arguments. The parts the struct is used for are underlined in green.

...

3. We use libraries for doing things like interacting with hardware. Here we see the GPIO (general purpose input/output) library in action. Libraries are code someone else has written that help you do something. Here, the GPIO library sends the controller instructions to toggle a pin on and off. The GPIO library calls are underlined in blue. We’ll be covering exactly what this library does in more detail in a later lesson

The fact that we use libraries is actually really important to letting us work remotely - recall how we use different versions of the libraries depending on if we’re running the program on our computer or on the STM.

Looking at a real project, we split our files into three folders:

...

Any .c files go in src (source), any .h files go in inc (include), and any test files go in test. The rules.mk file is for compiling the project, but you shouldn’t need to worry about it for a while.

...

Pictured above: header files should include public functions, while the source .c files may include extra private functions.

...

Here, the red underline is a private helper function not in the header file, while the blue underline is the function from the header file. Private functions should be prefixed prv_.

A full list of our coding standards are here to review at your leisure: Coding Standards

Tests + Validation

There are three parts to this. Yes, testing is that important.

  • Unit testing: testing small parts of our code by writing more code. Ideally, each .c file other than main should have an associated test file that tests the logic. These test files are meant to be run on x86. They’re run every time you open a pull request on GitHub.

...

Here’s an example of a unit test from the tests for our GPIO library. It sets up the pin, asserts that it was set up correctly, then ensures checking the state returns the value it was initially set to (notice that in the init settings the state is HIGH).

Info

Our testing framework is Unity (not the game engine). Every test file has three parts:

  • A setup_test function, which is run before every test. It initializes all the libraries the tests need.

  • A teardown_test function, which is run after every test. Usually it’s empty, but it still has to be there.

  • Test functions, which is any function that starts with test_. They can use assertions like TEST_ASSERT_EQUAL to test conditions.

  • Hardware testing: running the project on the actual hardware. Also, creating validation projects that test specific parts of the hardware, but not necessarily implementing the logic. We call these smoke tests. More documentation can be found here: Smoke Tests .

  • Integration testing: Wiring a bunch of boards together and making sure they play nice. Hopefully by this stage our code is well tested and the logic is bullet proof, but there’s always something that comes up here.

When working remotely, hardware testing will occur by either asking a hardware member to flash your code and then telling you the results, or by video calling and debugging the project together.

To test over virtualbox, the command make test PROJECT=<project> PLATFORM=x86 can be used to run the specific test.

GitHub and JIRA process

JIRA

...

In JIRA we use what are called tickets for tracking tasks. There are five locations a ticket can be:

...

When tickets are created, they go to the backlog. Assigned means there’s a deadline, in progress means the ticket is currently being worked on. We’ll talk about code review in the GitHub portion. After code review is complete, the ticket is moved to done.

Tickets are then organized into ‘Epics’: groups of tickets that go together. Pictured above, Front Power Distribution in yellow is an epic, with two tickets falling under that epic.

We want to keep JIRA tickets just for concrete tasks. If you need to have a meeting, don’t create a JIRA ticket for it, note it somewhere else like on confluence.

Each JIRA ticket has a number, which will be important for GitHub.

For now, only the leads will create tickets. If you find an issue, report it to a lead.

GitHub

Once you’re assigned a ticket on JIRA, the next step is to create a branch on GitHub. If this is unfamiliar to you, read up on git or watch Arshan’s video lectures on the subject.

Branches should be prefixed soft_xyz, where xyz is the ticket number, and should have a descriptive name following. For example, soft_148_center_console_fault_handling.

Once you complete your task, you’ll open a pull request. This lets others see your changes before merging them into our master firmware branch. Before you do, make sure to run ‘make lint’ and ‘make format’ on your box! This formats your code and checks for formatting errors.

When you open a pull request, our continuous integration tool called Travis runs. This basically runs your code, and makes sure all the unit tests pass.

...

The blue outline means that Travis successfully ran your code and that all unit tests passed. The red highlight means changes were requested, so the person who opened the pull request needs to make those changes before merging it in.

This is where the code review part comes in: we’ll look through your changes and maybe ask for edits to be made. Once that’s done, you can merge in your changes and delete the branch.

Conclusion

That was a lot to get through, hopefully you made it! In conclusion, here’s what you should now have an understanding of:

  1. How we run code on hardware

  2. How we structure our files and projects

  3. How we test and validate our code

  4. How we use JIRA and GitHub to manage tasks

In the next tutorial, we’ll be covering how to read datasheets and schematics, as well as our basic libraries for GPIO, interrupts, and ADCs.

Homework

Follow as much of the Software 101 modules as you can while working remotely. Start here: Module 2: Hello World

Notes: Don’t worry too much about GDB debugging, in reality print statements (LOG_DEBUG) is very often used. Also, don’t worry about the “Blinking LED - Registers” section, it’s more advanced and definitely not necessary going forward.

Next, create a new project in a new branch soft_999_{firstname_lastname}_fw_102 that does the following:

  • Defines a struct that stores two uint8_t counters, counter_a and counter_b. typedef this struct Counters.

  • Uses a single soft timer to increment counter_a every half a second, and counter_b every second.

  • LOG_DEBUG the value of each counter whenever they’re updated.

  • Doesn’t use any global variables

  • Implements all code in main.c

Your output should look something like this:

...

If you run into problems, let someone know! No need to bash your head against a wall.

Hints:

  • Refer to the blinking LED tutorial for how to use soft timers.

  • Use a while (true) {} loop to keep the program running.

  • Don’t forget to start another soft timer within your soft timer function call.

  • In your main function, create a counters struct, and pass a pointer to that struct to the soft_timer_start_... call as the context.

  • Check the supplementary material: Firmware 102 Supplementary Material for more hints on how to use logging and soft timers

Once you’re done with the project, open a pull request and request a code review from a lead. We won’t actually merge your pull request, but it’ll be a good chance to learn how code review works.

Make sure to run make format to autoformat your code and make lint to catch any linting errors.

[Optional]: watch Arshan’s videos on GPIO, interrupts, and ADCs.

GPIO: https://www.youtube.com/watch?v=JXQVHOYHbdk&list=PLwHCeNgf9lKdt6LN6D54__moOb4Mkj5NQ&index=9

Interrupts:

https://www.youtube.com/watch?v=zGIE8bLAFqQ&list=PLwHCeNgf9lKdt6LN6D54__moOb4Mkj5NQ&index=10

ADCs:

https://www.youtube.com/watch?v=pIitjg4jfJg&list=PLwHCeNgf9lKdt6LN6D54__moOb4Mkj5NQ&index=11The 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.

Code Block
languagec
// 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.

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 to run your LEDs. 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!

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.

2.4 - Interrupts (Optional) - WIP