Getting started with FIR Signal Processing

STM32CubeMX2

1. Introduction: Why using ADC with FIR Filtering

1.1. Role of an FIR Filter

An FIR filter is used in digital signal processing to remove unwanted frequencies from a signal. Its main purpose is to improve measurement accuracy by reducing noise and suppressing parasitic components. At the same time, an FIR filter preserves the useful part of the signal, which makes the output cleaner and easier to analyze.

1.2. Real-Time Processing Chain

A typical embedded DSP system relies on a continuous processing chain:

Figure 1: Processing chain

  • : the frequency of the input signal.

: the sampling frequency.

: the cutoff frequency of the filter.

The input signal with frequency is sampled at a frequency , and the filter removes frequency components above the cutoff frequency .

1.3. Advantages of STM32C5 Architecture

The STM32C5 (and STM32C4) microcontrollers are well suited for this type of application due to several hardware and software features:

  • LPDMA (Direct Memory Access) enables automatic data transfers between ADC, memory, and DAC without CPU involvement.
  • Timer-triggered ADC sampling ensures precise and deterministic sampling frequency, which is essential for DSP.
  • DAC with DMA support provides continuous and smooth signal output.
  • Arm®Cortex®-M33 core with Floating-Point Unit (FPU) supports IEEE-754 floating-point arithmetic in single precision, allowing direct use of DSP algorithms without fixed-point constraints.

2. Global Approach

2.1. Requirements

Step Question Yes No
0.1 Do you have the official documentation for STM32C5 and NUCLEO-C562RE?

(Reference Manual, Datasheet, User Manual, DMA Application Notes)

Go to Step 0.2 Download and read the documentations
0.2 Do you have STM32CubeMX2 installed? Go to Step 0.3 Download from:

STM32CubeMX2

0.3 Do you use a development environment Visual Studio Code ? Go to Step 0.4 Install Visual Studio Code:

VS Code

0.4 Are you able to generate a basic STM32 project using CubeMX2? Go to Step 0.5 Refer to Getting started with STM32C5.

2.2. Project overview

This project implements a real-time digital signal processing pipeline on STM32C4/C5 series while using a board: NUCLEO-C562RE. The implementation relies on:

  • HAL2 driver architecture
  • CMSIS-DSP library for signal processing
  • DMA-based data transfer
  • Timer-based synchronization

2.3. Development Environment and Tools

2.3.1. Configuration tool in Cubemx2

STM32CubeMX2 is used to define the global system configuration:

  • Clock configuration: In this example, the system clock frequency is set to 144 MHz, and the ADC kernel clock frequency is set to 36 MHz.


Figure 2: Clock frequency


Figure 3: Kernel clock frequency

  • Timer Trigger configuration: in this case, TIM6 is selected, and the output frequency is set to 200000
Figure 4: Timer Configuration
  • Peripheral initialization (ADC, DAC) and DMA (LPDMA) routing:
    • ADC:

ADC1 channel 9 is selected here. The sampling time must be configured so that the total conversion time remains less than the trigger period. The trigger frequency is of 200 kHz, so the trigger period is 5.0µs. One must select a sampling time according to the Reference Manual with a total time conversion that is less than 5.0µs

Figure 5: Total conversion time


The ADC sampling time is set to 25 ADC clock cycles. Then, enable the trigger source and select TIM6_TRGO. Finally, enable the DMA request, use the Circular transfer mode. Set both Source and Destination data widths to Half-word.


ADC Configuration
Select ADC channel


Trigger Configuration
Configure TIM6 TRGO


DMA Configuration
Enable LPDMA transfer

  • DAC:

DAC1 channel 1 is selected. The DAC resolution is kept identical to the ADC resolution to ensure consistency between the sampled input signal and the reconstructed output signal. Then, the Trigger source must be activated and set TIM6_TRGO. Enabling the output buffer provides a low output impedance and allows direct driving of loads. When disabled, it offers more flexibility at the cost of requiring an external buffering stage. Refer to the Datasheet for more details.

Figure 9: DAC Channels

To finish, enable the DMA channel1, in Circular transfer mode. Set Source data widths to Half-word.

Figure 10: DMA Configuration
  • DMA:

Verify that both peripherals are in request mode and that each one has an LPDMA channel.

Figure 11: DMA overview


In this project, Visual Studio Code is used as IDE. After completing the configuration, open the project settings, select CMake as the project format, and then click Generate IDE Project.


2.3.2. DSP Implementation

2.3.2.1. Project Setup

After generating the project using STM32CubeMX2 with the CMake option enabled, open the project in your preferred IDE (in this case, Visual Studio Code). The generated project already contains:

  • HAL drivers
  • Peripheral initialization (ADC, DAC, DMA, TIM)
  • Startup and linker files

At this stage, the project compiles but does not yet include any DSP processing. To keep the project modular and maintainable, a dedicated module for signal processing must be created.
Step 1: Create an application of filtering name filter. Add two new files in repertory of your project: filter.c and filter.h.

Step 2: Define the Header File In filter.h, declare the public interface of the DSP module:

#ifndef FILTER_H
#define FILTER_H

#include <stdint.h>

void filter_init(void);
void filter_process(void);

#endif

Step 3: Implement the Source File In filter.c, define the basic structure:

#include "filter.h"

void filter_init(void)
{
    // TO DO: Initialize FIR structure (CMSIS-DSP) and peripherals
}

void filter_process(void)
{
    // TO DO: Apply FIR filtering here
}

Step 4: At this stage, only placeholders are defined. When using CMake, newly created files must be explicitly added in project repertory: cmake>files.cmake

# file-format: 1.0.0
if(CMAKE_BUILD_TYPE STREQUAL "debug_GCC_NUCLEO-C562RE")
  target_sources(${CMAKE_PROJECT_NAME} PRIVATE main.c main.h filter.c filter.h) //Files added: filter.c filter.h

Type the command "build all".

2.3.2.2. Add the DSP Module

To perform efficient signal processing, the CMSIS-DSP library provided by ARM® is integrated. This library contains optimized implementations of DSP algorithms (FIR, FFT, etc.), specifically designed for Arm®Cortex®-M cores.
The CMSIS-DSP library is not always included by default in STM32CubeMX2 projects. It is retrieved it from another STM32 package (for example STM32U5, which includes a complete CMSIS tree).
Instead of copying the CMSIS-DSP library from another STM32Cube package, it is also possible (and recommended for reproducibility) to retrieve it directly from the official STMicroelectronics GitHub repository.

Step 1: Access the Repository The CMSIS-DSP library is available to STMicroelectronics GitHub repository. This repository is maintained by ST and provides:

  • The latest DSP implementations
  • Optimizations for Arm®Cortex®-M cores
  • Regular updates and bug fixes

Step 2: Download the Library You can either:


Step 3: Extract Only Required Folders From the downloaded repository, keep only: Include and Source

Step 4: Integrate into Project Copy these folders into your project:

Figure 12: Folder DSP


Step 5: Add Include Directories The compiler must know where to find CMSIS-DSP headers. Edit your CMakeLists.txt into the repertory project and add ${CMAKE_SOURCE_DIR}/Drivers/CMSIS/DSP/Include :

target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC
  # Add additional include directories here
  ${CMAKE_SOURCE_DIR}/Drivers/CMSIS/DSP/Include
)

Step 6: Add DSP Source Files CMSIS-DSP is not header-only, so source files must be compiled.
All source files can be added, but for simplicity, only the required files are selected. They are placed in cmake>files.cmake:

  target_sources(${CMAKE_PROJECT_NAME} PRIVATE
      ${CMAKE_SOURCE_DIR}/Drivers/CMSIS/DSP/Source/FilteringFunctions/arm_fir_f32.c #added
      ${CMAKE_SOURCE_DIR}/Drivers/CMSIS/DSP/Source/FilteringFunctions/arm_fir_init_f32.c #added
    )

Step 7: Define CMSIS-DSP Macro To use CMSIS-DSP with a Arm®Cortex®-M33 core, a specific macro must be defined. In this project, it is added in CMakeLists.txt:

target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC
   ARM_MATH_CM33 #added
)

This macro tells the CMSIS-DSP library which Cortex core is used to enable the appropriate optimized implementations

Step 8: Include CMSIS-DSP in Code To use CMSIS-DSP functions in your project, include the main header file. In filter.c:

#include "arm_math.h"

This header gives access to:

  • FIR functions (arm_fir_f32)
  • DSP structures
  • math utilities

Result: If the configuration is correct: the project compiles DSP functions are available

2.4. Filter Integration

2.4.1. Theoretical Background

Before implementing the filter, it is important to define the main DSP parameters.

  • The system operates with: . This sampling frequency is defined by the timer trigger (TIM6 → ADC/DAC).
  • The Nyquist condition states:. So theoretically:
  • The FIR filter performs: Where: .
    • is input
    • is output
    • are coefficients

2.4.2. Methodology

To ensure a correct behavior, the system is validated step by step. This section presents the full DSP implementation in a structured and reproducible way.

1. Defines and Global Variables

  • The following definitions control the DSP behavior:
#include "filter.h"
#include "mx_tim6.h"
#include "mx_adc1.h"
#include "mx_dac1.h"

#define N 32U
#define halfN (N / 2U)
#define NUM_TAPS 31U

N : total buffer size
halfN : half-buffer (used for real-time processing)
NUM_TAPS : FIR filter length

  • Peripheral Handles:
static hal_adc_handle_t *pADC = NULL;
static hal_dac_handle_t *pDAC = NULL;
static hal_tim_handle_t *pTimer6 = NULL;
  • DMA Buffers:
volatile uint16_t adc_buffer[N];
volatile uint16_t dac_buffer[N];

Used by DMA in circular mode and hared between ADC/DAC and processing

  • FIR Buffers:
static float32_t filt_in[N];
static float32_t filt_out[N];
  • FIR Instance:
arm_fir_instance_f32 filter1;
static float32_t State[halfN + NUM_TAPS - 1U];
  • Control Flags:
volatile int flag_half = 0;
volatile int flag_full = 0;

Set in interrupts

2. Initialization
All system initialization is done in:

void filter_init(void)
  • Retrieve handles
  • Start ADC, Calibration and Start DMA Conversion
  • Start DAC and Start DMA Conversion
  • Initialize FIR
  • Start Timer

After this step, these peripherals are running continuously:

void filter_init(void)
{
//handles
    pADC = mx_adc1_gethandle();
    pDAC = mx_dac1_gethandle();
    pTimer6 = mx_tim6_gethandle();

//Start ADC, Calibration and Start DMA Conversion
    HAL_ADC_Start(pADC);
    HAL_ADC_Calibrate(pADC);
    HAL_ADC_REG_StartConv_DMA(pADC, (uint8_t *)adc_buffer, N*2);

//  Start DAC and Start DMA Conversion
    HAL_DAC_StartChannel(pDAC, HAL_DAC_CHANNEL_1);
    HAL_DAC_StartChannel_DMA(pDAC, HAL_DAC_CHANNEL_1, (const uint8_t *)dac_buffer, N*2);

//  Initialize FIR
    arm_fir_init_f32(&filter1, NUM_TAPS, Coeffs, State, halfN);

//  Start Timer
    HAL_TIM_Start(pTimer6);
}

3. Processing Architecture
The processing is split into two parts:

  • Interrupt callbacks:
void HAL_ADC_REG_DataTransferHalfCallback(...)
{
    flag_half = 1;
}

void HAL_ADC_REG_DataTransferCpltCallback(...)
{
    flag_full = 1;
}
  • Main loop processing:
void filter_process(void)
{
  if (flag_half)
  {
    flag_half = 0;
    process_half_buffer();
  }

  if (flag_full)
  {
    flag_full = 0;
    process_Cplt_buffer();
  }
}

This ensures real-time execution and no blocking in interrupts. With process_half_buffer and process_Cplt_buffer, the DSP processing functions are applied to the DMA buffers.

4. Main Application (main.c)
In the main function, the else loop must be deleted, to allow the integration of a created application or function.

/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
/* Private functions prototype -----------------------------------------------*/

/**
  * brief:  The application entry point.
  * retval: none but we specify int to comply with C99 standard
  */
int main(void)
{
  /** System Init: this code placed in targets folder initializes your system.
    * It calls the initialization (and sets the initial configuration) of the peripherals.
    * You can use STM32CubeMX to generate and call this code or not in this project.
    * It also contains the HAL initialization and the initial clock configuration.
    */
  if (mx_system_init() != SYSTEM_OK)
  {
    return (-1);
  }
  else //delete
  { //delete
   /*
    * You can start your application code here
    */
    mx_adc1_init();
    mx_dac1_init();
    mx_tim6_init();

    filter_init();

    while (1){}
  }//delete
} /* end main */

Then, add your define, prototypes filter_init(); and filter_process();.
Your function main looks like:

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "filter.h"
#include <stdint.h>

/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
/* Private functions prototype -----------------------------------------------*/

/**
  * brief:  The application entry point.
  * retval: none but we specify int to comply with C99 standard
  */
int main(void)
{
  /** System Init: this code placed in targets folder initializes your system.
    * It calls the initialization (and sets the initial configuration) of the peripherals.
    * You can use STM32CubeMX to generate and call this code or not in this project.
    * It also contains the HAL initialization and the initial clock configuration.
    */
  if (mx_system_init() != SYSTEM_OK)
  {
    return (-1);
  }

   /*
    * You can start your application code here
    */
    mx_adc1_init();
    mx_dac1_init();
    mx_tim6_init();

    filter_init();

    while (1)
    {
    filter_process();
    }
 
} /* end main */

This creates a continuous DSP loop.

5. Header File filter.h

#ifndef FILTER_H
#define FILTER_H
#include <stdint.h>

typedef struct hal_adc_handle_s hal_adc_handle_t;
void filter_init(void);
void filter_process(void);
void process_half_buffer();
void process_Cplt_buffer();
void HAL_ADC_REG_DataTransferCpltCallback(hal_adc_handle_t *hadc);
void HAL_ADC_REG_DataTransferHalfCallback(hal_adc_handle_t *hadc);

#endif

2.4.3. Operating Modes

The following Modes 1 and 2 are optional; it is possible to skip them and to go directly to the filtering step Mode 3.

2.4.3.1. Mode 1: DAC Signal Generation

Here, the objective is to validate the DAC output by the DMA and triggered by the Timer. In dac_buffer[N] , set the values of a discrete sine wave sampled over 32 points, centered around 1600, with an amplitude of about 1400.
So, in the variables section, the following variables appear:

//TO KEEP > DATA CONFIRMED
//SINUS DATA with half amplitude and F=6kHz when trigger at 200kHz
volatile uint16_t dac_buffer[N]={ 1600U, 1873U, 2136U, 2378U, 2590U, 2764U, 2893U, 2973U,
    3000U, 2973U, 2893U, 2764U, 2590U, 2378U, 2136U, 1873U,
    1600U, 1327U, 1064U, 822U, 610U, 436U, 307U, 227U,
    200U, 227U, 307U, 436U, 610U, 822U, 1064U, 1327U
};
//TO KEEP > DATA CONFIRMED

The processing is empty:

void process_half_buffer() {}
void process_Cplt_buffer() {}

Here is an illustration of DAC output signal:

Figure 13: DAC Signal Generation

2.4.3.2. Mode 2: ADC → DAC (No Filter)

Here, the objective is to validate the acquisition chain. The chain is divided into two parts. One copy per half.
Processing:

/**** PROCESS HALF BUFFER *******/
void process_half_buffer()
{
  for (uint32_t i = 0U; i < halfN; i++)
  {
    dac_buffer[i] = adc_buffer[i]; //First half
  }
}
void process_Cplt_buffer()
{
  for (uint32_t i = 0; i < halfN; i++)
  {
   dac_buffer[halfN + i] = adc_buffer[halfN + i];  //Second Half
  }
}

For an input sine wave at 10 Hz, with a 1.65 V offset and 1 V amplitude, here is an illustration of the ADC input signal and the DAC output signal.

Figure 14: ADC to DAC without filter

2.4.3.3. Mode 3: ADC → FIR → DAC

This part is the most important, the objective is to enable DSP filtering. We use one of the online tools to generate des coefficients :

 Tfilter Free online FIR filter designer
FIIIR (FIR filter designer)

FIIIR is chosen in this example:


Figure 15: Kernel clock frequency

The output is the Filter code:

h = {
    0.000000000000000000,
    0.000000000000000000,
    -0.002313091418640871,
    0.000000000000000003,
    0.016424145425452383,
    -0.000000000000000008,
    -0.066842322383936212,
    0.000000000000000016,
    0.302741567245869259,
    0.499979402262510975,
    0.302741567245869259,
    0.000000000000000016,
    -0.066842322383936240,
    -0.000000000000000008,
    0.016424145425452393,
    0.000000000000000003,
    -0.002313091418640874,
    0.000000000000000000,
    0.000000000000000000,
};

These coefficients can be used in the variable coeffs
. In an FIR filter, each tap corresponds to a coefficient applied to a delayed input sample; therefore, the number of taps is equal to the number of coefficients.

static float32_t Coeffs[NUM_TAPS] = {
    0.000000000000000000,
    0.000000000000000000,
    -0.002313091418640871,
    0.000000000000000003,
    0.016424145425452383,
    -0.000000000000000008,
    -0.066842322383936212,
    0.000000000000000016,
    0.302741567245869259,
    0.499979402262510975,
    0.302741567245869259,
    0.000000000000000016,
    -0.066842322383936240,
    -0.000000000000000008,
    0.016424145425452393,
    0.000000000000000003,
    -0.002313091418640874,
    0.000000000000000000,
    0.000000000000000000,
};
  • FIR Processing Steps

For each block of data, the processing is:

1. ADC → float conversion: Convert integer ADC values to floating-point format

    filt_in[i] = (float32_t)adc_buffer[i]; //First half

2. FIR filtering: Apply FIR convolution using CMSIS-DSP

arm_fir_f32(&filter1, filt_in, filt_out, halfN);

3. Saturation: Ensure the signal remains in DAC range

if (y < 0.0f) y = 0.0f;
if (y > 4095.0f) y = 4095.0f;

4. Write to DAC buffer: Output the filtered signal

dac_buffer[i] = (uint32_t)y;

5. Implementation

/**** PROCESS HALF BUFFER *******/
void process_half_buffer()
{
  // Step 1: ADC → float
  for (uint32_t i = 0U; i < halfN; i++)
  {
    filt_in[i] = (float32_t)adc_buffer[i];
  }

  // Step 2: FIR
  arm_fir_f32(&filter1, filt_in, filt_out, halfN);

  // Step 3 & 4: Clamp + DAC output
  for (uint32_t i = 0U; i < halfN; i++)
  {
    float32_t y = filt_out[i];

    if (y < 0.0f)
    {
      y = 0.0f;
    }
    else if (y > 4095.0f)
    {
      y = 4095.0f;
    }

    dac_buffer[i] = (uint32_t)y;
  }
}

/**** PROCESS FULL BUFFER *******/
void process_Cplt_buffer()
{
  // Step 1: ADC → float
  for (uint32_t i = 0; i < halfN; i++)
  {
    filt_in[i] = (float32_t)adc_buffer[halfN + i];
  }

  // Step 2: FIR
  arm_fir_f32(&filter1, filt_in, filt_out, halfN);

  // Step 3 & 4: Clamp + DAC output
  for (uint32_t i = 0; i < halfN; i++)
  {
    float32_t y = filt_out[i];

    if (y < 0.0f)
    {
      y = 0.0f;
    }
    else if (y > 4095.0f)
    {
      y = 4095.0f;
    }

    dac_buffer[halfN + i] = (uint32_t)y;
  }
}

For an input sine wave at 30000 Hz, with a 1.65 V offset and 1 V amplitude, here is an illustration of the ADC input signal sampled to to 200000 Hz and the DAC output signal filtered with the Cutoff frequency to 50000 Hz and Transition bandwidth to 50000 Hz.

Figure 16: ADC to DAC with filter FIR

3. Results

3.1. The interpretation of the results

The measured magnitude response clearly demonstrates the expected behavior of a low-pass FIR filter.

  • The gain remains approximately constant between 1 kHz and 30 kHz, close to 0 dB, indicating a well-preserved passband.
  • A slight gain increase (up to ~+1.25 dB) is observed in the low-frequency range, which is consistent with passband ripple due to the finite number of taps.
  • The response starts to decrease between 35 kHz and 40 kHz, indicating the beginning of the filter transition region.
  • At 50 kHz, the measured gain reaches approximately −3.2 dB, placing this frequency near the effective cutoff point of the implemented filter.
  • Beyond 50 kHz, the attenuation increases rapidly, reaching approximately −7.0 dB at 55 kHz and −12.4 dB at 60 kHz.
  • The measured results therefore confirm that the filter effectively attenuates high-frequency components while maintaining a nearly constant gain within the passband.

Figure 17: Data measured

3.2. Demonstrate the Performance

3.2.1. Experimental validation

The filter performance can be validated through the measured results:

  • The passband preserves the input signal amplitude, confirming that useful frequencies are not distorted.
  • A significant attenuation is observed around 50 kHz, indicating that the transition region of the filter is correctly positioned.
  • The transition from passband to stopband is clearly visible, demonstrating correct filter shaping.
  • The attenuation beyond the transition region confirms that high-frequency noise or unwanted components are effectively removed.
  • It is important to note that the cutoff frequency defined during filter design does not exactly correspond to the -3 dB point.
  • Due to the finite number of taps and the large transition bandwidth used in this implementation, the attenuation at the cutoff frequency is greater than -3 dB in the theoretical response.
  • However, the experimental measurements show a -3 dB point around 50 kHz, confirming that the transition region is correctly centered.

Figure 19: signal input 1Khz and filtered signal

Figure 20: signal input 60Khz and filtered signal

3.2.2. Comparison between theory and measurements

The experimental gain matches the expected behavior of the designed FIR filter.
Minor deviations in gain (≈ ±1 dB) are acceptable and mainly due to:

  • Finite number of taps
  • Windowing method (Blackman)
  • Measurement uncertainties

Theoretical FIR Magnitude Response

Measured Magnitude Response

Figure 21: Bode magnitude comparison of the FIR filter showing theoretical response and measured data.

3.3. Limitations

3.3.1. Phase measurement limitations

At higher frequencies, the phase measurement becomes unreliable:

  • The measured delay may exceed one signal period.
  • This introduces ambiguity in phase computation.
  • The modulo operation (±π or ±180°) causes artificial discontinuities.

This explains:

  • the sudden jump observed at 60 kHz
  • the incorrect phase values at higher frequencies

Figure 22: Signal observed at 80 kHz showing phase ambiguity due to the measured delay exceeding one signal period.

3.3.2. Filter design limitations

Due to the finite number of taps used in this implementation, the low-pass FIR filter cannot achieve an ideal frequency response with an abrupt cutoff. Instead, the attenuation increases progressively around the cutoff frequency, resulting in a relatively wide transition region. Consequently, frequencies near the cutoff frequency are only partially attenuated rather than being completely rejected. This behavior is a common limitation of low-order FIR filters and reflects the trade-off between implementation complexity and frequency selectivity.

3.3.3. Other limitations include

  • ADC and DAC resolution 12-bit: This introduces quantization error, which can slightly distort the signal and reduce precision.
  • Measurement accuracy: The precision of instrument can affect the reliability of the phase and amplitude measurements, especially for small variations.
  • Limited number of taps: The use of a limited number of taps (19) results in a wider transition band, increased passband ripple, and reduced stopband attenuation compared to an ideal filter. This explains the deviation from the theoretical response, as a higher number of taps would improve frequency selectivity at the cost of increased computational load.

4. Conclusion

This project demonstrates the implementation of a complete real-time DSP chain on STM32C5, combining ADC acquisition, FIR filtering using CMSIS-DSP, and DAC reconstruction, all synchronized through DMA and timer triggering.

The experimental results confirm the expected behavior of the FIR low-pass filter: the passband is preserved, the transition region is correctly positioned around cutoff frequency, and high-frequency components are effectively attenuated. The comparison between theoretical and measured responses shows good overall agreement, validating both the filter design and its embedded implementation.

However, several limitations have been identified. The cutoff frequency does not strictly correspond to the -3 dB point due to the finite number of taps and the large transition bandwidth. Phase measurements also become unreliable at higher frequencies due to ambiguity in delay estimation. Additionally, quantization effects and measurement uncertainties introduce small deviations in the results.

Overall, this work highlights the efficiency of the STM32C5 platform for real-time DSP applications, while emphasizing the practical constraints associated with FIR filter design and measurement in embedded systems.