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:
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).
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:
-
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. -
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.
-
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:
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):
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:
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:
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:
- Clock Configuration: The UART module itself needs a clock to function.
-
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.
-
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.
-
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:
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:
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:
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:
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)."
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:
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 to1U
: 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.
Link the CMSIS-DSP Library
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:
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.