Starting from:
$30

$24

Lab 4: I2C and SPI Peripherals, and OS

Abstract

In this lab you will learn how to configure and use an OS on our B-L475E-IOT01A development board. OS tasks will be used to interact with the user (with a push-button and UART), as well as acquire data from a variety of sensors.

Deliverables for demonstration

    • C implementation of initializing, and reading four (4) different I2C sensors

    • C implementation of transmitting I2C sensor data over UART to a terminal

    • C implementation of the above using three different tasks in FreeRTOS

    • Final FreeRTOS application using QSPI flash

Grading

    • 30% Data acquisition from I2C sensors

    • 30% Transmitting over UART

    • 20% Initial implementation using FreeRTOS

    • 20% Final FreeRTOS application using QSPI flash

Changelog

    • 26-Oct-2023  Initial revision.

Overview

In this lab, we’ll be introduced to embedded real-time operating systems RTOS. The key advantage of an RTOS is the ability to control how often different parts of your program execute. By breaking main() into a number of tasks (i.e., threads), and using OS directives to put them to sleep, wake them up, coordinate between them, and set their relative importance, it is possible to ensure critical work is done in a timely fashion, preempting other, less critical work when necessary. While this is possible without an OS, an OS makes this significantly easier.

This lab will also introduce UART, I2C peripherals, and the on-board SPI flash chip; you’ll coordinate OS tasks that read I2C sensor values, print them to a terminal, and save them to flash. Which sensor value is printed will be controlled by another task that manages the push-button. Once again, much of this will be facilitated by a board support package (BSP).










© Brett H. Meyer    1
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)




Resources

HTS221 Datasheet: Temperature and humidity sensor

LIS3MDL Datasheet: Magnetometer

LPS22HB Datasheet: Pressure sensor

LSM6DSL Datasheet: Accelerometer and gyroscope

Quad-SPI interface on STM32 Microcontrollers and Microprocessors

Getting started with Octo-SPI and Hexadeca-SPI Interface on STM32 Microcontrollers

Part 1: UART and I2C Peripherals

First we’ll set up the UART and I2C serial bus, and find board support package drivers for the connected sensors.

Configuration

Initialization

I2C Sensors

I2C is a common interface for peripherals. I2C assigns each register on each peripheral a different address; I2C devices therefore listen (to addresses) and respond (with data) upon request. Our board has a number of sensors connected by I2C: a humidity and temperature sensor (HTS221); a 3-axis magnetometer (LIS3MDL); a 3D accelerometer and gyroscope (LSM6DSL); a barometer (LPS22HB); and, time-of-flight and gesture detection sensor (VL53L0X).

STM dramatically simplifies working with these peripherals by providing a board support package (BSP): functions for initializing and reading them have already been written. These functions also take care of scaling sensor outputs, a relief after Lab 2. All we need to do is configure the I2C pins, and include the appropriate source and header files in our project.

You’ll find the I2C interfaces under Connectivity in the chip features list on the left hand side. There are three or four, depending; refer to your development board manual to determine which to enable. No further configuration of the I2C interfaces is necessary.

UART

Fun fact: in previous semesters, the UART appeared much earlier, in Lab 3. However, when rewriting the labs for at-home completion when ECSE 444 was taught remotely, I couldn’t get it working correctly until much later, because I didn’t check the schematic.





© Brett H. Meyer    2
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)




UARTs are used to exchange data between computers using a serial link. They are often used to provide a user interface for configuring a device, but can also be used for computer-to-computer communication. Many development boards provide a UART-based virtual COM port to facilitate debugging, and more.

You will also find a number of UARTs and USARTs under Connectivity. Refer to your development board manual to determine which UART or USART serves this purpose on your board. (The presence of the S in USART indicates that the connection can be synchronous, too; its absence indicates that the interface is only asynchronous.) Enable the appropriate interface, and then check the schematic and adjust the pinout accordingly. No further configuration of the UART is necessary; however, you may need the parameters in Parameter Settings to configure your terminal in order to see the output from the UART.

Implementation

Reading I2C Sensors

The first step is to copy the board support package (BSP) files into your project. BSP functions

for each sensor are defined in one or more header files, named like

stm32l475e_iot01_hsensor.h (stm32l4s5i_iot01_hsensor.h); these must be included in

main.c. These functions call other functions in other files; these must also be copied into your

project.

The BSP files can be found in the same directory as the CMSIS-DSP libraries we used in Lab 1, e.g., STM32Cube/Repository/STM32Cube_FW_L4_V1.18.0/Drivers/BSP/

Select the files from the directory for your development board, and copy them into the appropriate directories in your project. You’ll also need files from the Components/ directory.

Choose one thing to measure using each of the following: HTS221, LIS3MDL, LSM6DSL, and LPS22HB; e.g., the HTS221 can output either temperature or humidity. Pick one (e.g., humidity). Note that while you can configure the I2C interface in STM32CubeIDE, the code generation process does not initialize the peripherals. Do so by calling the appropriate initialization functions (e.g., BSP_HSENSOR_Init()) in main.c.

Now write code to read each sensor value (four of them) at 10 Hz.

You may find B-L475E-IOT01 BSP Driver Reference useful in identifying the appropriate functions to use in each case. Note that this is not official STM documentation, but has been generated automatically from the BSP source and header files. Otherwise, if you’re more comfortable crawling source and header files, start with stm32l475e_iot01*.* (stm32l4s5i_iot01*.*).



© Brett H. Meyer    3
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)




Displaying Sensor Values on UART

Now we want to print sensor values to a terminal. Refer to the HAL user manual for the functions required to work with the UART. Choose one sensor value to display, and call the appropriate HAL function to transmit it over UART. Be sure to clearly indicate which sensor value is being displayed.

Note: you can use the stdio and string libraries to assist you here. However, additional configuration and care is required if you want to work with:

    • printf; this requires that you overwrite __weak implementations of low-level IO functions to redirect output to UART. This is doable, but is not covered here. sprintf is an alternative that almost works out of the box.

    • sprintf(buf, “Look, it’s a floating point number: %.2f”, temp);

Formatting floating point numbers requires that you change a compiler flag; STM32CubeIDE will direct you to the appropriate place, and this works fine until we incorporate an operating system. I still haven’t figured out how to get floating point numbers to print when using FreeRTOS (Part 2); my solution casts all floats to integers before displaying.

In order to send or receive data over UART, you will need to have an appropriate serial terminal program installed. There are many such programs, and they vary from platform to platform. However, it is also possible to install a terminal in Eclipse. Select the Help pulldown menu in STM32CubeIDE, and then Eclipse Marketplace.






























© Brett H. Meyer    4
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)







































Search for “terminal” and install TM Terminal.
































© Brett H. Meyer    5
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)




Then, when you are in the Debug perspective, select the Window pulldown menu, and Show View > Other. Choose Terminal.




















This will add a new tab.








Click  to connect. That will open a new window. Select Serial Terminal, and then the appropriate serial port:
    • On Linux, ...???

    • On OS X, it’ll be something like /dev/cu.usbmodemxxxxx

    • On Windows, …???

The rest of the parameters should be set appropriately by default, but it is always a good idea to compare them with the configuration in your IOC.






















© Brett H. Meyer    6
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)
































Click OK to connect, and the terminal will begin to display whatever your MCU is sending over UART.

If in macOS you cannot find a port as illustrated above, first confirm that such a port is active.

Open the Terminal program, and ls /dev/cu.*. You should see something like:

bhm-macbook-pro-2016:dev bhm$ ls cu.*

cu.Bluetooth-Incoming-Port cu.SOC

cu.MALS    cu.usbmodem14603

If you do, then you may need to use an alternative serial terminal. SerialTools is available for free on the App Store, and works out of the box. Simply select the appropriate port and connect.

Windows 10 users may have some trouble using the integrated terminal, in particular, identifying the appropriate COM port. First, ensure that you have checked the schematic and have USART1 assigned to the appropriate pins. If you find you still can’t output to the terminal, go to the Windows 10 Device Manager. Under Ports (COM & LPT) you should see "STMicroelectronics STLink Virtual COM Port (COM##)" as pictured below (where ## is the number assigned by your PC).












© Brett H. Meyer    7
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)




If you observe this, and still cannot get the integrated terminal to work, please download and install a third party COM terminal. There are many such programs available; we recommend Docklight. A trial version is freely accessible and should satisfy your needs for this lab.

Ubuntu/Linux users can identify the port of connection, using $dmesg command.






For example, in the image above, ttyACM0 is identified as the ST-LINK connection port.

Once you have identified the port, open the connection to UART using minicom using the following command,
$ sudo minicom -D /dev/ttyACM0

If the configuration is correctly done, you’ll be able to see the UART logs on this mincom console.

Changing Sensors with the Push-button

Now extend your implementation such that each time the button is pressed, data from a different sensor is displayed.

Ensure that your program works before moving on, as debugging basic functionality is significantly more difficult once the OS is running, too.

Part 2: CMSIS OS and FreeRTOS

The key advantage of an embedded operating system is that it makes it easy to more carefully control when different parts of our program execute. For instance, perhaps we want to sample one sensor at 1 Hz, another at 10 Hz, and another at 100 Hz. Maybe we only want to check that a button has been pressed every 500 ms. And perhaps we want to log data (to display or otherwise take action) any time a new sample is taken. Implementing this with a single main(), even with timers and interrupts, may make it difficult to meet performance requirements.

Configuration and Implementation

Open your project IOC file again; on the left hand side there is a Middleware section, in which you will find FreeRTOS. Select it, and choose CMSIS_V1 mode. There are many parameters available to configure FreeRTOS; we will leave everything set to default, with the exception of Tasks and Queues. Mutexes, and Timers and Semaphores may also be of interest, depending on how you wish to communicate between tasks and synchronize access to shared resources and data. Strictly speaking, however, they are not necessary for this assignment.




© Brett H. Meyer    8
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)




The first thing to do once CMSIS_V1 is enabled is change the timebase of the system. FreeRTOS uses the SysTick clock to determine when to perform context switches; this makes using HAL_Delay based on SysTick problematic: FreeRTOS wants a relatively low priority timer (because context switches should not interrupt interrupts), but HAL requires a relatively high priority timer (so timekeeping continues even during interrupts). Choose SYS from System Core on the left hand side, and change Timebase Source to another timer. Good choices are TIM6, TIM7, TIM16, and TIM17. These timers have relatively less functionality than the others.

Your objective now is to run your application in three tasks, rather than out of a single main()

function:

    • One task that determines when the button has been pressed, and changes the mode of the application to output data from the next sensor in the sequence;

    • One task that transmits this data to the terminal using the virtual com port UART; and,

    • One task that reads sensor data.

Once you enable FreeRTOS, regenerating your code will create the first (default) task automatically. It is started by the OS just before main() enters its infinite while loop; at this point, the infinite while loop should be unreachable. My recommendation is that you get everything working again in this single task before you begin to create additional tasks and migrate functionality into them. The task is named defaultTask and is started when the OS calls a function in main.c, StartDefaultTask().

In your task, you’ll see a new function, osDelay(...). This functions much like

HAL_Delay(...), with a key difference: osDelay(...) puts a thread to sleep, handing control back to the OS; HAL_Delay(...) is blocking, pausing all user code execution. You need osDelay(...) to allow other threads of the same priority to execute; the delay you put in determines how long before the thread wakes and can execute again.

Note: for some reason that I cannot determine, osDelay(...) calls should appear at the beginning of the for(;;) loop, not the end.

When you’re ready, return to your IOC, and the FreeRTOS configuration, and add more tasks under Tasks and Queues. Double click anywhere on the default task to pull up its configuration.
















© Brett H. Meyer    9
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)

























Rename the task to something more descriptive, and update its entry function accordingly. The rest of the parameters can be left as is. As always, these changes will automatically update your code when you generate it.

Now add another task; pick suitable task and entry function names. The rest of the parameters can be left as is. Again, my recommendation is that you move functionality into this task, and get everything working again, before repeating this process to add a third task.

Notes

    • Debugging with an OS is painful. Set breakpoints at the beginning of your tasks; chances are that problems originate there, and not in the OS itself, even if the call stack appears to suggest otherwise.

    • Debugging systems with persistent RAM can be painful, too. Remember: if you don’t power the board off, data from earlier runs may be resident in memory, and accessed (because C lets you touch anything not explicitly protected). This is especially true of dynamically allocated memory on the heap (the default for tasks), since the heap is not initialized (unlike statically allocated variables).

    • Hard faults are the segmentation faults of embedded systems. If your code accesses memory that it shouldn’t, encounters a stack overflow, or some other problem (including trying to format floating point numbers, or inconsistent configuration of peripherals, for instance), a hard fault interrupt will be triggered. It is difficult to work out what code caused the interrupt; single-stepping can be quite useful.

    • If you are using sprintf or similar functions to format floating point numbers, I have not yet figured out how to get this to work with FreeRTOS. If you do, let me know; otherwise, cast to int.

    • Don’t forget that debugging changes the relative timing of events; something may work with breakpoints and break without them; tracing with ITM is useful in these cases, as this has fewer side effects.




© Brett H. Meyer    10
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)




    • Complex functions from standard libraries may require a larger stack (because of nested function calls) than provided by default; you can change the stack size for each task, or the minimum for all tasks. If you set the minimum too high, however, tasks may silently fail to start; an X instead of a ✔ in front of FreeRTOS Heap Usage indicates you’re allocating too much memory (though STM32CubeIDE will not prevent you from generating code like this).

    • The location of osDelay(...) appears to matter. I’m not sure why! Make them the first thing that happens inside each task’s for(;;) loop.
    • If all else fails, start over, with your working code from before enabling FreeRTOS. That’s what I had to do, and I’m still not sure why things didn’t work the first time, or why they are working now.

Part 3: SPI Flash

The 64 Mb on-board Flash chip substantially expands the storage capacity of our development board. However, Flash RAM is unusual: read and write accesses work differently, for instance; furthermore, the SPI interface works differently than the I2C interface used above. Whereas I2C assigns addresses to peripheral registers, SPI uses a chip-select signal to identify the targeted peripheral and commands indicating the desired operations.

Configuration

QSPI operations can take some time to complete. To assist with debugging, it is worthwhile considering using LEDs as progress and/or error indicators. The green LED on PB14 red LED on PE3 may be useful for this purpose. Note that the LED on PE3 lights when the GPIO output is low.

QSPI Flash

Flash memory, a non-volatile storage technology, operates fundamentally differently from the on- and off-chip memory we’re used to programming with, RAM. RAM can be read and written in any order, at any address, at any time (provided that it is powered). Reading and writing takes about the same amount of time, and is generally fast.

Flash, on the other hand, must be manipulated with greater care. Before flash can be written, it must first be erased, block by (e.g., 64KB) block. This sets all bits in the block to 1. The block can then be programmed, during which any 0s are set. Erasing and writing are power intensive and slow; reading is generally faster, though still not as fast as RAM.

Our board provides a quad (octo) serial peripheral interface (QSPI) to an on-board 64 Mbit flash chip. QSPI implements a synchronous serial connection to a peripheral using four data lines, a clock signal, and chip select signal. SPI peripherals are interacted with using commands. Commands have a large number of fields that specify the desired behavior: should the device


© Brett H. Meyer    11
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)




return data, or save new data? What address should be used? Once set, the command and parameters are sent to the device. A large number of parameters are available for configuring a variety of commands. For a lot more details, see Quad-SPI interface on STM32 MCUs Application Note.

Frankly, it’s a bit overwhelming.

Fortunately, STMicroelectronics has provided a BSP, which defines functions that simplify a number of basic operations, including setting device-specific parameters such as memory size.

In this case, all we need to do to configure the device is enable the appropriate pins.

B-L475E-IOT01A

From the peripheral list on the left hand side of the IOC configuration display, choose Connectivity, and then QUADSPI. In Mode and under Single Bank, select Quad SPI Line. This will enable six pins. Two are set correctly: clock (QUADSPI_CLK) and chip select (QUADSPI_NCS), which are mapped to PE10 and PE11. The IO pins, however, are not. Refer to the schematic in board user manual to identify the appropriate pins for the four IO signals. Once you’ve remapped the signals, generate your code.

B-L4S5I-IOT01A

From the peripheral list on the left hand side of the IOC configuration display, choose Connectivity, and then OCTOSPI1. In Mode, select Quad SPI. This does not enable any pins! Refer to the pinout list in the board user manual to enable QUADSPI_CLK, QUADSPI_NCS, and QUADSPI_BK1_IOn, n in {0, … 3}, for the flash memory. Note that while the manual labels the signals as QUADSPI_xxx, STM32CubeIDE will name them OCTOSPIM_P1_xxx. (And that, for example, three different pins can be selected for OCTOSPIM_P1_CLK, but only one is connected to the flash memory!)

Implementation

The first step is to copy the BSP files into your project:

    • stm32l475e_iot01_qspi.c (stm32l4s5i_iot01_qspi.c)

    • stm32l475e_iot01_qspi.h (stm32l4s5i_iot01_qspi.h)

    • mx25r6435f.h

and include stm32l475e_iot01_qspi.h in main.c.

Then, add a call to BSP_QSPI_Init() after USER CODE BEGIN 2. A HAL handle hqspi will be generated for use in QSPI functions. This has been configured according to the parameters in IOC, which we did not change from their defaults. BSP_QSPI_Init() configures a different handle (defined in stm32l475e_iot01_qspi.c) with parameters corresponding to the particular


© Brett H. Meyer    12
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)




device on the board (defined in mx25r6435f.h). When you call other BSP_QSPI functions, it uses this second, appropriately configured handle.

Erase, Write, Read

At this point, it is worthwhile experimenting with the BSP_QSPI functions. Again, flash must be erased before it can be written. Three erase functions are available to you, which will erase a single sector (4k bytes), block (64KB), or the entire chip. It is worth noting that:

    • Erasing the entire chip is time consuming, and not recommended (it is unnecessary in this lab).

    • The sector erase function is non-blocking, meaning that the processor will not wait for the erase to complete before continuing to execute from main().

    • As such, block erase is probably the most convenient for the purposes of this lab.

I recommend trying to erase a block, write data to it, and read the data back out to confirm that the write operation was successful. I also recommend using the following structure to make BSP_QSPI function calls:

if (BSP_QSPI_Read(data, readAddr, size) != QSPI_OK)

Error_Handler();

This ensures that your Error_Handler() function (which is empty upon generation) is called if anything goes wrong with the function.

Note! The data types of data, readAddr, and size are uint8_t*, uint32_t, and uint32_t, respectively. Carefully refer to the BSP header files and function prototypes.

My Error_Handler() for this lab is:

HAL_GPIO_WritePin(LEDError_GPIO_Port, LEDError_Pin, GPIO_PIN_RESET); __BKPT();

This code turns on a red LED, and then halts the debugger with a breakpoint instruction. I have added similar code to a number of interrupt handlers in stm32l4xx_it.c, e.g., HardFault_Hander(), which is called under a variety of circumstances.














© Brett H. Meyer    13
McGill ECE    ECSE 444: Lab 4

Rev 1.0 (2023-10-24)




Part 4: Putting it all together

Modify your application in Part 2:

    • Buffer and write sampled measurements for all sensors (whether displayed or not) to flash. Note that the BSP functions are unlikely to be thread-safe (i.e., multiple simultaneous calls from different threads are unlikely to play nicely together).

    • As before, when the button is pressed, cycle through each sensor and display sampled values, but add an additional state.

    • After all sensors have been cycled through with button presses, display summary statistics calculated from the logged values in flash. For each sensor, display: number of samples, sample mean, and sample variance.

    • A subsequent button press should begin displaying the real-time values from the first sensor once more.

Deliverables

Your demo is limited to 10 minutes. Be sure to highlight top-level software structure and program flow. When applicable, it is useful to highlight that your software computes correct partial and final values.

Your demo will be graded by assessing, for each part above, the correctness of the observed behavior, and the correctness of your description of that behavior.

Grading

The breakdown of grading is as follows:

    • 30% Data acquisition from I2C sensors

    • 30% Transmitting over UART

    • 20% Initial implementation using FreeRTOS

    • 20% Final FreeRTOS application using QSPI flash

Each part of the demo will be graded for (a) clarity, (b) technical content, and (c) correctness:

    • 1pt    clarity: the demo is clear and easy to follow

    • 1pt    technical content: correct terms are used to describe your software

    • 3pt    correctness: given an input, the correct output is clearly demonstrated

Submission

Please submit, on MyCourses, your:

    • Source code used to demo (only files you modified, including IOC file).





© Brett H. Meyer    14

More products