A Guide to Transmitting Structures using STM32 UART

STM32 microcontrollers are among the most widely adopted microcontrollers in the domain of embedded systems. They are power-efficient and have a small package size. They come on a variety of form factors and are very well documented. In addition to this, they support a wide variety of peripherals like I2C, SPI and UART in a single module. This guide will help you send structs over the UART, and receive the struct using a python script. Normally, to send UART data, one would require a UART converter as STM32 does not have that builtin. I felt this was extremely limiting thus, in this guide we will be using an Arduino UNO to convert the data from the STM32 to our work-station.

Getting Started

Before starting, I will list all the software and hardware that I made use of in this project.

  • STM32F4 — Discovery Development Kit (STM32F407VGTx)
  • Arduino UNO
  • Arduino IDE
  • Keil MDK-ARM IDE
  • STM32-CubeMX
  • Jumper Wires (M-F)

Structures in Embedded Programming

Embedded Systems, by definition are designed to handle a single or a single group of tasks. As such, their inputs and outputs usually have a similar data fields and grouping all this into a structure, makes it very easy to handle the data as well as memory space in an embedded system

For example, the system configuration of the system is held in a user-defined structure. This can be configured on startup, and then if there is any change needed, referencing the struct from its address will not only help you save memory by avoiding duplicating the entire set of config values. On the more practical side, if you have a variety of sensors and their data needs to be transmitted or saved on every clock cycle, then grouping the data into a single structure makes it very easy to handle and transmit.

Setting Up the Project

In this tutorial, we will be creating a structure of randomly generated values and transmit that via the UART port of the STM32. So, first off, you need to set up your project. I have illustrated the steps needed to do this in another tutorial (https://medium.com/theteammavericks/a-beginners-guide-to-developing-on-stm32-b7fd38966aa0).

The only thing to keep in mind, is that we need to enable the UART port in CubeMX pin configuration. In my project I select the UART2 port. (This step is essential and mentioned in the tutorial above). UART option is available under the Connectivity Section. Set the USART2 Mode to Asynchronous Mode. You can also set the baud rate. In this case, I leave it at 9600 bits/s.

Code-Snippet for STM32

When you generate the code in STM32, the main.c file will have the following structure. You will need to add code in whichever section as per requirements.

/*
License Declaration
*/// Header File
#include "main.h"
// User Defined Header// Global Variable Declaration (Section 1)// Peripheral Initialization -> Handled by CubeMXint main(){
 // Program Initialization (Section 2)
 while(1){
   // Looping Code (Section 3)
 }
 // Handle End of Run (Section 4)
}

This the general template you will see when the code is generated. The variables declared in Section 1, will act as global variables. Now, we can sub-divide the project into the following tasks:

  • Create Structure (I will make one for collection of accelerometer data since STM32F4 has one builtin. You can program that and use the actual data but for this tutorial I will generate the data).
  • Make a Random Data Generator Function which returns a number within a given range. (Similar to the data a sensor might return)
  • Create a function to handle UART Transmission

Define Structure

(Section 1)
/*
* @brief Structure to hold and receive accelerometer data
* @param x,y,z axes data
* @size float -> 4B => x,y,z = 12B per instance of struct
*/

struct XL_Data{
 float x,y,z
}tx_data;

We will create tx_data as a global variable. This will make it easier to monitor its data in real-time in the watch window during debugging, as well as making it possible for us to pass its address to functions, thus preventing duplication of variables.

Random-Data-Generator function

(Section 1)
/**
* @brief Function to Generate Random Data
* @param float min, float max
*/

float genXLvals(float min, float max){
 return ((rand() % ((max-min+1)) + min);
}

This function will return a value which lies in between the min and max values.

UART and HAL Libraries in STM32

The UART operation is a very complex procedure, no matter which micro-controller you are using. Fortunately, STM32 has libraries called LL and HAL. HAL stands for High Abstraction Layer and as the name implies, these libraries handle the entire bit-wise operations and the user only has to pass the right data to the functions.

When we select USART in CubeMX, it will add the corresponding HAL library to our projects and we can directly call those function from our code.

(Section 1)
/*
* @brief Function to convert and send data over UART
* @param XL_Data, UART Handle Typedef
*/

void sendData(struct XL_Data * data, UART_HandleTypeDef * huart){
// UART can only send unsigned int
 // Thus we need to convert our struct data

 char buffer[sizeof(data)]; // Create a char buffer of right size

// Copy the data to buffer
 memcpy(buffer, &data, sizeof(data)); // Copy and convert the data
 // Ideally buffer will be 12B long, 4B for each axes data// Now we can finally send this data

 HAL_UART_Transmit(huart, (uint8_t *)buffer, sizeof(buffer), 50);
// The last param is timeout duration in ms
}

The Main Code

Now that we have all our basic functions covered, we need to setup the while loop to send the data continuously.

(Section 3)
// Indicate Start of Sequence with "S"
HAL_UART_Transmit(&huart, (uint8_t*)"S", sizeof("S"), 50);// Populate the Accelerometer Structure
tx_data.x = genXLvals(0, 1024);
tx_data.y = genXLvals(0, 1024);
tx_data.z = genXLvals(0, 1024);
// Send the Data over UART
sendData(&tx_data, &huart2); // huart2 is auto-generated for USART2// Indicate End of Sequence with "Z"
HAL_UART_Transmit(&huart, (uint8_t*)"Z", sizeof("Z"), 50);
HAL_Delay(100); // Delay for 100ms

Note: We have selected 9600 baud rate. Thus, it mean 9600 bits can be sent each second which is 1200 bytes. The clock freq. of STM32 is 8 MHz. Thus, to prevent an overflow of data buffer, we add a delay of 100 ms. Remember that the data being transmitted is 12 bytes in size and thus can at max accommodate 100 transmissions each second.

Configuring the Arduino

USART2 Pins become highlighted in green (PA2 -> Tx) ; (PA3 -> Rx). Thus, the PA2 pin will be connected to the Rx Pin on Arduino (Pin 0), and PA3 pin would be connected to the Tx Pin on Arduino (Pin 1). Some people may experience issues due to the different operating voltages of Arduino and STM32, but I did not face such issues. If you do, you will need to connect the VCC of the STM32F4 board to the 3V3 port on the Arduino as well as the GND pins on both the Arduino and STM32F4 board.

Code

// USART Code on Arduino
void setup(){
 Serial.begin(9600); // Setup Baud Rate
}
void loop(){
 // We will not handle the Serial input in the loop
 // Instead we will use the serialEvent function

}/*
 SerialEvent occurs whenever a new data comes in the hardware   serial RX. This
 routine is run between each time loop() runs, so using delay inside loop can
 delay response. Multiple bytes of data may be available.
*/

void serialEvent(){
 while(Serial.available()){
   char inChar = (char)Serial.read(); // Read Incoming Byte
   Serial.print(inChar); // This will be read in Python
   // Remainder of Handling will be done in python
}

Handling Structures in Python

Python is an extremely versatile programming language and is very easy to use. However, in most scenarios data is collected on Embedded Devices and sensors. They usually send data in the form of bytes and python handles that easily. However, the easier method is to transmit the data as a complete structure and then use the struct module in python to handle it.

# Class for Handling the handling of Serial Dataimport structclass ReadFromSerial(object):
 
   def __init__(self, port):
       self.port = port
       self.vals = []

   def read_one_struct(self):
       self.read = False
       while not self.read:
           myByte = self.port.read(1)
           if myByte == b'S':  # Look for Start indicator
               
data = self.port.read_until(b'Z') # Z indicates Stop
               dat = data[1:len(data)-1]
               # unpack the dat into struct
               
try:
                   """ '<' indicates small endian format
                       'f' indicates float type => 4B groups formed
                       3x 'f' indicates there are 3 vars to be
                       unpacked. => 12 bytes in all.
                       This must match the size of dat.
                       Hence this section is in try-catch block.
                       Normally, it will never face any issue,
                       but there is always a chance of noise.
                   """

                   new_values = struct.unpack('<fff', dat)
                   self.read = True
                   self.vals.append(list(new_values))
                   return True
               except KeyboardInterrupt:
                   return False # Stop Program
               except:
                   return True # To ignore struct error

# Program Loopif __name__ == "__main__":
   import serial
   s = serial.Serial('COM17') # Check COM port in device manager
   
opClass = ReadFromSerial(s)
   while opClass.read_one_value():
       pass
   s.close() # Close Serial Port
   
data = opClass.valsThe entire python code is to be placed in the same file, main.py. The program loop here lets you choose the duration for which you want the data. Once we exit the loop, then the entire incoming data is available for our direct use.

Concluding Remarks

I hope that this blog has provided you with a conclusive guide with regards to how to send data in structures over UART and receive them using python as the Serial receiver. Using Arduino, we can avoid the need to rely on expensive modules like JTAG-TTL converters.

Finally, this also can be used by those who prefer to use python but need to interface with microcontrollers for data.

Useful Links