2. Getting Started: Plotting Signals with Logic Analyzer

2.1 Enabling the Floating Point Unit (FPU)

Floating Point Unit (FPU) is a specialized component in the ARM Cortex-M4 core that's designed to handle arithmetic operations on single-precision floating-point numbers. It is part of the Cortex-M4 core, so we need to consult the generic user guide for Cortex-M4 devices (the STM32 reference manual might not have the information we need). In DUI0553, Section 4.6, we find the following diagram:

fpu_memory_diagram

The Floating Point Unit (FPU) in the ARM Cortex-M4 core helps perform floating-point operations fast. To access and use the FPU, certain bits in the CPACR register need to be set. This register is located in the System Control Space (SCS), a dedicated region for controlling system-wide settings and functionalities.

The term "co-processor" is used for the FPU because in historical ARM architectures, co-processors were additional processing units or interfaces designed to handle specific tasks, complementing the main CPU. The FPU fits this description as it specifically handles floating-point arithmetic, which is a specialized computational task.

Now, why do we set CP10 and CP11 bits to enable the FPU?

In the ARM Cortex-M4, the FPU interface corresponds to both the CP10 and CP11 co-processor fields. These fields in the CPACR register determine the access levels to the FPU. By setting both CP10 and CP11, we're ensuring full access to all FPU functionalities. If not properly set, attempts to execute floating-point instructions could result in access violation faults.

To enable the FPU, use the following code:

// Enabling the FPU
SCB->CPACR |= (0xFU << 20);

This code ensures that both the CP10 and CP11 fields are set, granting complete access to the FPU.


2.2 Create a simple signal and plot it

#include "stm32f4xx.h"

#define HZ_5_SIG_LEN        301
loat _5hz_signal[HZ_5_SIG_LEN]=
{
    0,0.30902,0.58779, // rest of 5 Hz signal ...
};

float g_in_sig_sample;

static void plot_input_signal(void);
static void pseudo_delay(int dly);

int main()
{
    // Enabling the FPU: Get full access to CP10 and CP11
    SCB->CPACR |= (0xFU << 20);

    while(1)
    {
        plot_input_signal();
        pseudo_delay(9000);
    }
}

static void plot_input_signal(void)
{
    int i;
    for (i = 0; i < HZ_5_SIG_LEN; i++)
    {
        g_in_sig_sample = _5hz_signal[i];
    }
}

static void pseudo_delay(int dly)
{
    for (int j = 0; j < dly; j++)
    {
        // delay
    }
}

Here, we have a 5 Hz signal that we want to plot. We have a function plot_input_signal() that loops through the signal and plots each sample. We also have a function pseudo_delay() that acts as a delay loop. We will use this function to slow down the execution of the program so that we can see the signal on the logic analyzer.


2.3 Plotting the signal

Enable Serial Wire Viewer (SWV)

To enable SWV, go to Run > Debug Configurations.... In the Debug tab, select STM32 Cortex-M C/C++ Application and click on New launch configuration (the icon with a plus sign). In the Main tab, select the project name and the application name. In the Debugger tab > Serial Wire Viewer section, check Enable. Also, make sure the Core Clock Frequency is set to 16 MHz. Click Apply and Debug to start debugging.

Run in debug mode

Build the program, and run it in debug mode. This will switch to the Debug perspective.

Open SWV Data Trace Timeline Graph

In the Debug perspective, go to Window > Show View > Other.... In the Show View window, select SWV Data Trace Timeline Graph. Click Open to open the view.

Start SWV Data Trace

Start recording the data trace by clicking on the Start SWV Data Trace button in the SWV Data Trace Timeline Graph view (should be a red circle). This will start recording the data trace.

Run the program

Click on the Resume button in the Debug view (should be a green triangle). This will start running the program. You should see the signal being plotted in the SWV Data Trace Timeline Graph view. Zoom in to see the signal better.


2.4 Developing UART Driver for Plotting Signals

As shown in Section 2.3, we can plot the signal using the SWV Data Trace Timeline Graph. However, this can be very limited because we can only plot it in the STM32CubeIDE. We cannot plot it in other applications. To plot the signal in other applications, we need to send the signal data to the computer. We can do this using the UART peripheral.

Introduction to UART

UART stands for Universal Asynchronous Receiver/Transmitter. It is a serial communication protocol that is used to transmit and receive data. It is asynchronous because it does not use a clock signal to synchronize the data transfer. Instead, it uses a start bit and a stop bit to indicate the start and end of a data frame. It is a universal protocol because it is used in many microcontrollers and computers.

Take a look at the datasheet for the STM32F446 Series, Section 2.1, we can see Figure 3. STM32F446xC/E block diagram shows that there are 4 USART (USART1, USART2, USART3 and USART6) and 2 UART transmitters (UART4, and UART5).

STM32F446xC/E_block_diagram

For simplicity, we will use UART2, which is typically already connected to the ST-Link debugger on the STM32 boards. This means that we can use the ST-Link debugger to send and receive data from the computer.

To programmaticaly access the UART peripheral without using HAL, let's create two files: uart.h and uart.c. In uart.c, let's create a function:

void uart2_tx_init(void)
{
    /***** Configure UART GPIO Pin *****/
    // 1. Enable clock access to GPIOA

    // 2. Set PA2 mode to alternate function mode

    // 3. Set PA2 alternate function type to UART_TX(AF07)

    /***** Configure UART *****/
    // 1. Enable clock access to UART2

    // 2. Configure baud rate

    // 3. Configure the transfer direction

    // 4. Enable UART module

}

In the following sections, we will fill in the code for each step. We will use the reference manual for the STM32F446RE MCU to find the registers and bits that we need to configure.

Configure UART GPIO Pin

Using GPIO pins in microcontrollers usually follows a structured workflow to ensure the pins function as intended. Since the UART2's TX functionality is mapped to the pins of GPIOA, we need to configure the GPIOA pins first. The workflow is as follows:

  1. Enable the Clock: Before you can configure or use a GPIO port (like GPIOA, GPIOB, etc.), you need to enable its clock. This is done through the RCC (Reset and Clock Control) registers. For instance, enabling the clock for GPIOA is often done by setting a specific bit in the RCC->AHB1ENR register.

  2. Set the Mode: Each GPIO pin can operate in different modes, such as:

    • Input
    • Output
    • Alternate Function (e.g., UART, SPI, I2C)
    • Analog You select the desired mode using the MODER register of the specific GPIO port.
  3. Further Configurations: Depending on the mode you choose, you might need to configure additional settings. For instance, if you choose the "Alternate Function" mode, you'll need to specify which alternate function you're interested in (e.g., UART_TX, SPI_MOSI). This is done using the AFR registers.

In this section, we are going to walk through these 3 steps to configure the UART2 TX pin (PA2).

1. Enable clock access to GPIOA

Different hardware blocks (modules or peripherals) within the microcontroller can operate independently, and to conserve power and reduce interference, they can be selectively powered on or off. The clock signal is essentially the "heartbeat" that drives these modules, and it might be turned off to save power. So, before we can use a peripheral, we need to enable its clock signal.

In STM32 series, UART2's TX and RX functionalities are mapped to the pins of GPIOA. Before we can use or configure any pins on GPIOA, we must ensure the clock to GPIOA is enabled. Otherwise, any operations or configurations on those pins won't take effect.

To enable the clock to GPIOA, you access the AHB1ENR register in the RCC (Reset and Clock Control) module and set the GPIOAEN bit. In the reference manual, search for AHB1ENR. You will find the following diagram:

AHB1ENR

For this diagram, we can see that GPIOAEN is bit 0. We can add it as a macro in uart.c for readability:

#define GPIOAEN     (1U<<0)

In the uart2_tx_init() funciton, add the following code:

    /***** Configure UART GPIO Pin *****/
    // 1. Enable clock access to GPIOA
    RCC->AHB1ENR |= GPIOAEN;

2. Set PA2 mode to alternate function mode

Most GPIO pins on STM32 microcontrollers can be configured in several modes, one of which is the "alternate function" mode. It's called "alternate function" because it allows the pin to perform a specialized function (like UART, I2C, or SPI communication) as opposed to the general input or output functions. By consulting the "Alternate function" table in the STM32F446 datasheet, we can confirm that UART2 TX is indeed mapped as an alternate function to GPIOA Pin 2 (the specific alternate function, AF7, will be addressed later):

Alternate_function

To configure Pin 2 for its alternate function, one needs to set the appropriate bits in the MODER register. Referring to the reference manual and search for "MODER". We will see the associated "GPIOx_MODER" diagram:

GPIOx_MODER

We can deduce that the bits for MODER2 should be set to '0b10' to enable the alternate function mode. This is achieved in code with:

    // 2. Set PA2 mode to alternate function mode
    GPIOA->MODER &= ~(1U<<4);
    GPIOA->MODER |=  (1U<<5);

3. Set PA2 alternate function type to UART_TX(AF07)

Now that we've set the mode of Pin 2 to alternate function mode, we need to specify which alternate function we want to use. This is done by setting the appropriate bits in the AFR register. We see from the "Alternate function" table in the STM32F446 datasheet above that UART2 TX is mapped to AF7.

Referring to the reference manual and search for "AFR". We will see that there are actually "AFRL" and "AFRH". This is because, due to the number of GPIO pins and their potential alternate functions, a single 32-bit register wouldn't suffice to map them all. Therefore, STM32 divides this configuration into two registers, where pins 0 to 7 are configured in the AFRL, and pins 8 to 15 are configured in the AFRH. Since we are configuring Pin2, we will use the diagram of AFRL:

AFRL

We can deduce that the bits for AFRL2 should be set to '0b0111' to enable the UART2 TX alternate function. This is achieved in code with:

    // 3. Set PA2 alternate function type to UART_TX(AF07)
    GPIOA->AFR[0] |=(1U<<8);
    GPIOA->AFR[0] |=(1U<<9);
    GPIOA->AFR[0] |=(1U<<10);
    GPIOA->AFR[0] &=~(1U<<11);

Configure UART

Once we have configured the GPIO pin, we can now configure the UART peripheral. Setting up UART (or USART) on STM32 microcontrollers typically follows a sequence of steps to ensure the peripheral operates correctly:

  1. Clock Configuration: The UART module itself needs a clock to function.
  2. UART Configuration:

    • Set Baud Rate: This determines the speed of data transfer. As discussed earlier, you don't set the baud rate directly but calculate a divider.
    • Configure other UART settings: (we will not touch these settings in this example)
      • Word Length: Determine how many data bits per frame (often 8 or 9).
      • Stop Bits: Usually 1 or 2.
      • Parity: None, even, or odd.
      • Flow Control: None, RTS, CTS, or both.
    • Set Direction: Define if the UART module is going to be used for transmitting, receiving, or both.
    • Enable UART module: This activates the UART so it can start transmitting/receiving data.
  3. Interrupts (optional):

    • If you're using UART interrupts (for instance, to be notified when data is received), you'd also set up the UART interrupt at this point.
  4. Transmit/Receive Data:

    • Once everything is set up, you can begin transmitting and/or receiving data.

1. Enable clock access to UART2

Take a look at the memory diagram at the top, we can see that the UART2 is part of the APB1 bus. So, we need to enable the clock to APB1 bus. In the reference manual, search for APB1ENR. You will find the following diagram:

RCC_APB1ENR

We can see that USART2EN is bit 17. We can add it as a macro in uart.c for readability:

#define UART2EN     (1U<<17)

Then in the uart2_tx_init() funciton, add the following code:

    /***** Configure UART *****/
    // 1. Enable clock access to UART2
    RCC->APB1ENR |= UART2EN;

2. Configure baud rate

The baud rate is the rate at which the UART transmits data. It is the number of symbols transmitted per second. The baud rate is usually set to 9600, 19200, 38400, 57600, or 115200. In this example, we will set the baud rate to 115200. Let's define some useful macros:

#define SYS_FREQ        (16000000)
#define APB1_CLK        (SYS_FREQ)
#define UART_BAUDRATE   (115200)

Then inside the uart2_tx_init() function, add the following code:

    // 2. Configure baud rate
    USART2->BRR = ((APB1_CLK + (UART_BAUDRATE/2U))/UART_BAUDRATE);

To set up UART communication, we need to configure the baud rate. The baud rate determines how fast data is sent over UART. The BRR (Baud Rate Register) of USART2 is used to set this rate.

The equation provided is a simplified version of how we calculate the value to be put into the BRR for the desired baud rate, given the peripheral's clock (APB1_CLK). The addition of (UART_BAUDRATE/2U) is a common technique to ensure that the result is rounded to the nearest whole number (it's essentially rounding by averaging).

So, to summarize, this piece of code is setting up the baud rate for USART2 to allow it to communicate at 115200 bits per second, assuming it's being clocked at 16 MHz. The reason we can't just pass the desired baud rate (like 115200) directly to USART2->BRR is because the BRR doesn't store the baud rate itself. Instead, it stores a divider value that the USART hardware uses to derive the baud rate from the peripheral clock.

3. Configure the transfer direction

UART can be configured to transmit, receive, or both. To configure the transfer direction, we need to set the TE bit in the control register 1 (CR1). In the reference manual, search for "USART_CR". We will see the following diagram:

USART_CR1

We will configure it to transmit (TX). In the diagram above, we see that TE is bit 3. We can add it as a macro in uart.c for readability:

#define CR1_TE      (1U<<3)

Then, inside the uart2_tx_init() function, add the following code:

    // 3. Configure the transfer direction
    USART2->CR1 = CR1_TE;

4. Enable UART module

Lastly, we need to enable the UART module. To do this, we need to set the UE bit in the control register 1 (CR1). From the USART_CR1 diagram above, we see that UE is bit 13. We can add it as a macro in uart.c for readability:

#define CR1_UE      (1U<<13)

Then, inside the uart2_tx_init() function, add the following code:

    // 4. Enable UART module
    USART2->CR1 |= CR1_UE;

Overwrite the __io_putchar() function

The __io_putchar() function is used by the printf() function to print characters to the console. We need to overwrite this function to send the characters to the UART2 TX pin instead of the console. To do this, we need to add the following code to uart.c:

#define SR_TXE          (1U<<7)

int __io_putchar(int ch)
{
    uart2_write(ch);
    return ch;
}

static void uart2_write(int ch)
{
    /***** Make sure transmit data register is empty *****/
    while(!(USART2->SR & SR_TXE)) {}

    /***** Write to the transmit data register *****/
    USART2->DR = (ch &0xFF);
}

To understand this code, let's navigate to the USART_SR (SR = Status Register)register in the reference manual:

USART_SR

The TXE bit at 7 tells us if the transmit data register is empty. If it is empty, that is, if the last character has been transferred to the shift register, then we can write to the transmit data register (DR = Data Register). If it is not empty, then we need to wait until it is empty before we can write to the transmit data register.

The (ch &0xFF) operation ensures that only the least significant 8 bits (i.e., one byte) of ch are written to the data register. This is particularly useful if, for some reason, ch contains more than 8 bits of data. This operation effectively truncates any extra bits.

Putting it all together

In the end, we should have the following:

#include "uart.h"

#define GPIOAEN         (1U<<0)
#define UART2EN         (1U<<17)

#define SYS_FREQ        (16000000)
#define APB1_CLK        (SYS_FREQ)
#define UART_BAUDRATE   (115200)

#define CR1_TE          (1U<<3)
#define CR1_UE          (1U<<13)

#define SR_TXE          (1U<<7)

void uart2_tx_init(void)
{
    /***** Configure UART GPIO Pin *****/
    // 1. Enable clock access to GPIOA
    RCC->AHB1ENR |= GPIOAEN;

    // 2. Set PA2 mode to alternate function mode
    GPIOA->MODER &=!(1U<<4);
    GPIOA->MODER |= (1U<<5);

    // 3. Set PA2 alternate function type to UART_TX(AF07)
    GPIOA->AFR[0] |=(1U<<8);
    GPIOA->AFR[0] |=(1U<<9);
    GPIOA->AFR[0] |=(1U<<10);
    GPIOA->AFR[0] &=~(1U<<11);

    /***** Configure UART *****/
    // 1. Enable clock access to UART2
    RCC->APB1ENR |= UART2EN;

    // 2. Configure baud rate
    USART2->BRR = ((APB1_CLK + (UART_BAUDRATE/2U))/UART_BAUDRATE);

    // 3. Configure the transfer direction
    USART2->CR1 = CR1_TE;

    // 4. Enable UART module
    USART2->CR1 |= CR1_UE;
}

int __io_putchar(int ch)
{
    uart2_write(ch);
    return ch;
}

static void uart2_write(int ch)
{
    /***** Make sure transmit data register is empty *****/
    while(!(USART2->SR & SR_TXE)) {}

    /***** Write to the transmit data register *****/
    USART2->DR = (ch &0xFF);
}

Test the UART driver

We can use a simple program to test the UART driver.

#include "stm32f4xx.h"
#include "uart.h"
#include <stdio.h>

int main()
{
    // Initialize UART2
    uart2_tx_init();

    while(1)
    {
        printf("Hello from STM32... \n\r");
    }
}

Build, run, and open the serial monitor (I use the Arduino IDE serial monitor). Remember to set the baud rate to 115200. You should see the following:

serial_monitor_result


2.5 Plotting the signal using UART

We can now plot the 5 Hz signal using UART. We will use the same code from Section 2.2, but we will rewrite the main.c program:

#include "stm32f4xx.h"
#include "signals.h"
#include "uart.h"
#include <stdio.h>

extern float _5hz_signal[HZ_5_SIG_LEN];

static void fpu_enable(void);
static void pseudo_delay(int dly);

int main()
{
    // Enable FPU
    fpu_enable();

    // Initialize UART2
    uart2_tx_init();

    int i;
    while(1)
    {
        for (i = 0; i < HZ_5_SIG_LEN; i++)
        {
            printf("%f\r\n",_5hz_signal[i]);
        }
        pseudo_delay(9000);
    }
}

static void fpu_enable(void)
{
    // Enabling the FPU: Get full access to CP10 and CP11
    SCB->CPACR |= (0xFU << 20);
}

static void pseudo_delay(int dly)
{
    for (int j = 0; j < dly; j++) {}
}

The STM32CubeIDE might give you a warning that says "The float formatting support is not enable...". To solve this, Go to `Project > Properties > C/C++ Build > Settings > Tool Settings and check "Use float with printf from newlib-nano (-u _printf_float)."

printf_float_setting

Build, run, and use the Arduino IDE serial plotter to visualize the signal. Remember to set the baud rate to 115200. You should see the following:

serial_plotter_result


2.6 Integrating the CMSIS-DSP Library

The CMSIS-DSP library is a collection of over 60 common DSP functions that are optimized for ARM Cortex-M processors.

Download the CMSIS-DSP Library

Go to the GitHub Repository of STM32CubeF4 and download the following:

  • "Drivers/CMSIS/DSP/" directory: Put it in the "chip_headers/CMSIS" directory.
  • "Drivers/CMSIS/Lib/" directory: Put it in the "chip_headers/CMSIS" directory.

Add Include Paths

Right-click on the Src folder in the project explorer. Select Properties > C/C++ General > Paths and Symbols. In the Includes tab, click on Add... and add the following include paths (relative to the workplace):

  • "chip_headers/CMSIS/DSP/Include"
  • "chip_headers/CMSIS/Include"
  • "chip_headers/CMSIS/Device/ST/STM32F4xx/Include"

Add Symbols to the Project

Right-click on the Src folder in the project explorer. Select Properties > C/C++ General > Paths and Symbols. In the Symbols tab, click on Add... and add the following symbols:

  • ARM_MATH_CM4: This symbol tells the CMSIS-DSP library that our target processor is an ARM Cortex-M4.
  • __FPU_PRESENT set to 1U: This symbol indicates that our ARM Cortex-M4 has a floating-point unit (FPU), which can accelerate certain DSP operations by handling floating-point calculations more efficiently.

We just added the "Drivers/CMSIS/Lib/" to the project in the previous step, but we still need to include the library in the build process. To do this, go to Project > Properties > C/C++ Build > Settings > Tool Settings > MCU GCC Linker > Libraries.

In the Libraries (-l) section, click on Add... and add arm_cortexM4lf_math to the list of libraries. The "lf" means that the library is compiled for the little-endian floating-point unit (FPU) of the ARM Cortex-M4.

In the Library search path (-L) section, click on Add... and use the "workspace" option to navigate to the "Drivers/CMSIS/Lib/GCC" directory. Click OK to add the directory to the list of library search paths.

In the end, you should have the following:

link_dsp_lib


2.7 Testing the CMSIS-DSP float32_t

float32_t is a typedef used in the CMSIS-DSP library to represent 32-bit floating-point numbers. It's essentially synonymous with the standard C float data type. It is used for ensuring portability and readability.

Now we have added another signal of type float32_t in signals.c:

float32_t input_signal_f32_1kHz_15kHz[KHZ1_15_SIG_LEN] =
{
+0.0000000000f, ...
};

And in main.c, we have the following code:

#include "stm32f4xx.h"
#include "signals.h"
#include "uart.h"
#include "arm_math.h"
#include <stdio.h>

extern float32_t input_signal_f32_1kHz_15kHz[KHZ1_15_SIG_LEN];

static void fpu_enable(void);
static void pseudo_delay(int dly);

int main()
{
    // Enable FPU
    fpu_enable();

    // Initialize UART2
    uart2_tx_init();

    int i;
    while(1)
    {
        for (i = 0; i < KHZ1_15_SIG_LEN; i++)
        {
            printf("%f\r\n", input_signal_f32_1kHz_15kHz[i]);
        }
        pseudo_delay(9000);
    }
}

static void fpu_enable(void)
{
    // Enabling the FPU: Get full access to CP10 and CP11
    SCB->CPACR |= (0xFU << 20);
}

static void pseudo_delay(int dly)
{
    for (int j = 0; j < dly; j++) {}
}

The build should be successful, indicating that we have successfully integrated the CMSIS-DSP library. We can now use the CMSIS-DSP library to perform DSP operations on the signal.

serial_plotter_result2