Filter Case
When I wrote the MicroControllers book, I did not have space for all the demos that I would have liked to include. One of the things I had to postpone for the sake of space, was a demonstration of how modern MCUs can do a lot of infrastructure - without bothering the central core.
Recently I bought a new oscilloscope. It is not expensive, but nevertheless packed with amazing features - including a built-in function-generator and FFT, topped with ability to do Bode-plots. Looking for something to test this on, I decided to reuse the very small Cortex-M4 board from the MicroControllers book to implement a digital FIR filter. This even ties nicely with the IoT book, which has a chapter on Digital Signal Processing - with focus on FIR-filters. Thus, the demonstration here has the following priorities:
- Setup the peripheral infrastructure for digital filtering
- Analyse the performance of the oscilloscope
- Implement the actual DSP
The rest of this page is using the STM32 Nucleo-F334R8 board - with a Cortex-M4 with FPU, 64 kB Flash and 16 kB RAM running 64 MHz.
First simple solution
A FIR-filter is relatively simple to implement. Given a filter-length N, each input-sample triggers a calculation where the latest N samples are each multiplied with their respective filterconstant and all these multiplication results are added together and used as the output sample. I decided to use a Python trick in the indexing of the filter buffer, where the newest sample has the lowest (wrapping) index. This may be a bit confusing at first, but I like the concept. The main source for this implementation is found at the bottom of this page.
I got the filter-constants from ChatGPT. This is not optimal, but it works for demonstration purposes. I used to use programs from Iowa Hills for filter design, but these tools seem to be retired now. I can recommend plotting the constants (the filters impulse-response) in Excel - as below. If the plot doesn't look a bit like the sinc(x) function, you might want to reconsider the filter design.

In theory, the filterconstants are used in reverse order, but very often a symmetric filter is used, as this gives us linear phase - so it makes no difference in which direction we use the filter constants. In practice we accumulate the N products as we go along - using the holy grail of DSP and AI algorithms when possible - the Multiply-Accumulate aka MAC. As described in the MCU-book, this MCU does Single-Precision Floating-Point calculations very fast, so for simplicity I use floating-point arithmetic. Don't forget to make sure that the hardware FPU is enabled and properly configured in your MCU settings. Sometimes the easiest way to check this is to break into the debugger and inspect the assembly code to see if floating-point instructions are being used.
The simplest implementation is based on the following flow and infrastructure:
- A timer triggers an Analog-to-Digital Converter (ADC) at the sample-rate
- When the ADC is done converting, it triggers an interrupt
- Inside the interrupt we read the ADC, run the N MACs, and write the result to the Digital-to-Analog Converter (DAC) buffer
- The output sample is written from the buffer to the actual DAC-port at next tick of the timer (and at the same time we have step 1)
The "wiring" in the above is all done in STM32CubeMX - see the figures at the bottom of this page. When going back and forth between STM32CubeMX and STM32CubeIDE (or VS-Code etc), it is important that the user-written code stays within the marked sections. In one way using STM32CubeMX is nice and cool. It is however, also very vendor-specific, and if you don't use Windows line-endings, you may need to battle with git.
If you work daily with this stuff, you will prefer configuring directly in source-code. These days AI will help you here.
A bit more details (see also figures further down the page):
- Tim6 Counter is set to 3999 and the prescaler is set to 0. As the system-clock is 64 MHz, Fs becomes 64 MHz / (3999+1) = 16 kHz
- Tim6 Trigger event selection (TRG0) is set to "Update Event"
- ADC1 is set to single-ended - using pin PA0 - with 12-bit resolution. This allows an input between 0V and 3V3. I considered using differential input, which is the best way to keep down common-mode noise, but realized that this would give me issues with the scope and its probes, as these share GND. Please note that this choice is reversed later in this setup.
- ADC1 "External Trigger Conversion Source" is set to "Timer6 Trigger out Event" (Trigger detection on Rising edge)
- NVIC (Interrupt Controller): A checkmark is set for "ADC1 and ADC2 interrupts"
- DAC1 is set to have a buffer and to use "Timer6 Trigger out Event"
- PC3 is used as GPIO Output. It goes high during the ISR as ISR_MARK to allow the scope to show the time used inside the ISR. This pin sits nicely in a corner (pin-layout for the board is shown further down).
The table below summarizes the signal connections for the specific board. Note that ADC_IN2(-) and DAC2_OUT are not used yet.
| Signal | Description | CPU pin | Connector | Pin |
|---|---|---|---|---|
| GND | Ground | GND | CN7 | 19 |
| ISR_MARK | GPIO Output high in the ISR | PC3 | CN7 | 37 |
| ADC_IN1(+) | Analog input channel 1 (positive) | PA0 | CN7 | 28 |
| ADC_IN2(-) | Analog input channel 2 (negative) | PA1 | CN7 | 30 |
| DAC1_OUT | Filter Output | PA4 | CN7 | 32 |
| DAC2_OUT | Fixed output for ADC_IN2(-) | PA6 | CN5 | 5 |

Inspecting with Oscilloscope
I AC-coupled the scope channels to make the signals stay neatly at the center. In reality they swing between 0 and 3.3 Volts. In the following we use the cursors - readout on the right side...
The PC3 output is the yellow signal - ISR_MARK in the source. It goes high when the interrupt routine starts, and low when it's done. This is a simple way to assure that the initial part of the infrastructure (Timer-->ADC-->interrupt) is working. We see that the duty-cycle is approximately 46% (29 us/ 62.6 us * 100%). This means that the 32-tap FIR filter leaves resources for other work, whereas e.g., a 64 tap filter would probably be too much.

The period of the ISR_MARK signal is the Fs - sampling frequency. The readout on the scope says 15.97 kHz - close enough to the target 16 kHz.
The blue output signal doesn't look too good. This is mainly because there is no "reconstruction filter" - a lowpass-filter that smoothes out the steps of the DAC. We clearly see a "staircase" where the horisontal width of each step matches the period of the yellow signal - the sampling period (Delta-T).
Apart from the staircase apperance, we also see some high-frequency noise. This may stem from some of the many higher frequency digital signals in the setup. My probe connections are long and will pickup noise.
When the generator is set to generate a 2kHz sine or a 750 kHz sine I see the amplitude fall - proving that the 1 kHz Bandpass filter implemented by the FIR is working. Instead of manually finding the -3dB points, we can do a Bode plot to learn more about the filter.

Bode Plot Measurement
At my first many attempts, the Bode plot was completely messed up. I realized that the offset on the Generator that allowed me to work with voltages between approximately 0 and the supply voltage was not available in the Bode measurement.
I needed the Generator signal to be a true sine with 0V DC - even though the board did not really allow negative signals.Using Differential Input on ADC1
The trick was to use the differential input on ADC1 that I originally dropped. By setting up another DAC to produce a fixed 1.65V output, connected to ADC1- I could have a no-DC sine with up to 1.65V amplitude on ADC+. The generator signal would still be connected to ADC1+ and the probe Ground would still be connected to the board ground (not ADC1-). However, when the ADC is used in differential mode, the A/D-converter gives half the count for a given change in voltage. ST writes:
ADC Differential conversion result calculation:
- An offset of half of full range is added to keep the full dynamic range of differential voltage.
- Channels voltage is divided by 2, resolution is lowered of 1 bit.
- Diff conversion result = mid-range + (channel_high-channel_low)/2
The figure below - from an ST community site - illustrates the need for a different scaling in the differential input setup.

As can be seen in the figure, the differential input consists of two signals, one connected to the positive input and the other to the negative input of the ADC. When both sides are allowed to independently vary between 0 and 3.3V, the differential input can range from -3.3V to +3.3V. This is a 6.6V peak-to-peak range. And that is why the ADC needs to give half the count for a given change in voltage in differential mode.
The source-code at the bottom of this page reflects the above when calculating the DAC-output.
Please note that differential mode is very often used "balanced" - like a seesaw, where one input goes up while the other goes down. In some ADC-implementations the average of the two inputs must not move a lot, but we are OK in STM32 architectures.
Necessary setup changes were the extra DAC configuration and the change to differential mode on ADC1. They were made in STM32CubeMX, and the source was regenerated and updated manually with the correct initial sample out - once.
Unfortunately, the Bode plot was still completely "random".
Browsing the various fora and discussion boards, I found complaints like mine. I therefore downloaded new firmware and updated the scope from ver 1.04 to 1.05 - after reading the release notes, that specifically mentioned the Bode plot issue. The following self-calibration took very long. However, the Bode-plot now seems to work correctly. I also had a problem terminating the Bode-plot properly. This has also improved.

So what do we see in the Bode plot
- Even though I select a linear sweep, I still get a log frequency axis.
- The red graph is the magnitude. It does have a maximum around 1 kHz - at 0 dB. The rolloff is not very steep as 32-taps is not much. It appears to fall off more on the right side than on the left - but that is probably the log-axis fooling us.
- The green graph is the phase. It seems to fall linearly as expected - at least in the pass-band- wrapping at 180 degrees - but again a bit hard to say due to the log axis.
STM32CubeMX setup
This is described above, but the screendumps may help.







Scope quirks and features
Measuring
Before going into the quirks: This is a nice piece of equipment, and it can do a lot!
- Sometimes the cursors become inoperable. Restarting the scope makes it work - but also aborts a lot of work. Looking at the figures, I may need to be better at telling the scopewhich channel the cursors are to work on.
- I bought the S-model because it has a builtin signal-generator and can use this to make Bode plots. I was under the impression that you can select the generator output as a reference. That is not directly possible - you need to have a measurement channel allocated for this. It makes kind of sense to measure at the electronics instead of assuming that the output is unchanged by various impedance scenarios. Still, it would be nice to do a fast measurement with less cabling and saving a measurement channel.
- The signal generator offers the ability to set an offset. This is very important when working with a "single-rail" setup, where the signal can only swing in one direction from a reference point (usually ground). Unfortunately, there is no way to set an offset when doing the Bode-plot measurement.
- The Bode plot makes the generator do a sweep. However, the generator alone cannot sweep.
- When not able to use the Bode-plot and there is no sweep, you need to resort to manual measurements by adjusting the frequency of the signal generator and recording the response at each step. This is time-consuming, but is helped a bit by the fact that the "flex-knob" on the scope allows for adjustments of the frequency, so you may skip some typing.
Connectivity
It is very interesting to get screen dumps to your PC. I saw a YouTube video where a guy (see links) used the HDMI-signal connected to an external TV-card - and had an extra output from the TV-card connected to an extra screen. Very nice!
You may also use the USB-slave connector on the back of the scope. Unfortunately this requires a massive, old chinese software-package. Not always popular with IT.
For this case-story, I ended up saving screen-dumps on the scope's own drive - followed by copying to a USB-stick (front connector) and then copying from USB-stick to the PC. The "Quick" button on the scope can be configured in many ways. The default is to save screendumps on disk with running-numbers. Quite handy.
Then there is the Ethernet (LAN) connector - giving acces to a web-page, where downloads, remote control and screen-shots are possible. I had some issue with this. At first none of the settings seemed to work at all. Reading the manual (also on the scope), I learned that the network settings dialog could be pushed up and then an "Apply" button would appear - tada! See below. Even this did not help, but now I resorted to all software developers backup: "Have you tried turning it off and on again?" - with power-cable out etc. This worked, and I was finally able to access the web interface reliably. I got it to work directly with a PC - with AutoIP as well as Static IP configurations.

DMA
DMA - Direct Memory Access - can relieve the MCU core of some transport work (but may reserve the buses while doing so). Modern MCUs can tie DMA together directly with HW-events like when a buffer is full or an ADC is done converting.
For each DMA transport we have a contigues buffer with interrupts for "Half Transfer" and "Transfer Complete". This effectively become a buffer-switch - a ping-pong system - where the buffer halves take turns on being filled and emptied.
A FIR-filter is constantly sliding through it's buffer, which does not fit directly with a buffer-switching concept. Instead we can build on top of the existing solution. Earlier, we calculated one new output-sample, everytime we had an input. Now, lets collect 8 samples and then calculate 8 samples out. Then when there are 8 new samples ready we repeat. We could also gather 100 samples before switching buffers, but that will add a latency of 100 sample-periods. I guess 16 samples latency will be a good balance between latency and efficiency. It all depends on the application. The shortest latency comes with the solution we saw earlier.
The pseudo code becomes something like:
- Wait for a DMA interrupt with 8 new samples from ADC
- for (i=0; i < 8; i++)
- x = ADC_DMA_buffer[i];
- DAC_DMA_buffer[i] = fir_round(x);
- DAC DMA starts outputting data
- Repeat (toggling between the two halves of the buffer)
Note that with the above we use DMA for both ADC and DAC, but only the ADC triggers interrupts. The DAC simply outputs data as it is filled by the DMA.
Once again I went into STM32CubeMX to configure the DMA and ADC settings. These are the changes:- Removed "ADC and ADC2 interrupts" in the NVIC configuration
- Added DMA (default ch1) for ADC1. Default is Peripheral-to-memory. Click on the line and also choose "Circular"
- Added DMA (default ch3) for DAC1. Default is Memory-to-peripheral. Click on the line and also choose "Circular"
- Enabled "DMA continous requests"for ADC1
- No changes to GPIO - keeping the ISR_MARK from earlier
After letting STMCubeMX generate the basic settings, I added a 16-sample DMA buffer for both ADC and DAC and initialization of the DAC buffer in the user code section. I also added callback functions for handling DMA transfer complete and half-transfer events.
Note, that when the first interrupt from the ADC arrives with samples in the first half buffer, it is important to start writing to the first half of the DAC buffer. As the first 8 ADC-samples were collected, 8 zero-samples were written to the DAC from its buffer. When we got the first ADC-interrupt, the ADC started sampling into the second half and the DAC started writing out its second half. Thus, if we write to the second half of the DAC-buffer we will write to the buffer that is currently being output by the DAC, which is not what we want.
The above means that the DMA concept introduces a latency corresponding to the full buffer length. To this we must add a latency corresponding to half the FIR number of taps. This amounts to 16+16 = 32 samples of latency.
Measuring on the DMA-case

As the source-code shows, we are using the same ISR_MARK signal as before - now in two interrupts. This time I measured the DC voltage at the output using a Digital Voltmeter - right side of the scope. The scope says DC is 1.486V and as the DeltaY cursors show the peak-peak voltage is 3.357V, this means a duty cycle of approximately 44%.
Earlier we saw that the duty cycle was 46% when we had one interrupt per sample, and now with 8 samples per interrupt it has decreased to approximately 44%.
The above is not completely fair. When we look at the ISR_MARK signal, we do not include the time it takes to switch into the interrupts. However, in the current setup, we do not use the FPU outside of the interrupt, so the "lazy FPU stacking" mechanism means no FPU stacking occurs (more on this in the MCU book). The bare interrupt latency is 12 cycles - a low number because the MCU is dimensioned for this. If we look at 1 ms - 16 samples @ 16 kHz- we get:
-
The one-sample-per-interrupt spends 16 interrupts*29us = 464 us. To this we add 16 interrupts*12 cycles/64 MHz = 3 us, giving a total of approximately 467 us.
-
For the DMA-case, the duty cycle is 44% = 440 us. To this we add 2 interrupts* 12 cycles/64 MHz = 0.375 us, giving a total of approximately 440 us.
So, in terms of total latency, including the interrupt overhead, the difference between the one-sample-per-interrupt and the DMA-case is minimal. I would say that the one-sample-per-interrupt approach is surprisingly efficient, and with it's smaller latency - 17 samples versus 32 - in many cases will be preferred.
Speaking of latency: It would be nice to verify the latency with the scope. The Bode-plot may do that, but somehow I do not fully trust it. So I measured the delay from input to output instead. At first it looked very small, but when measuring on a sine it's hard to separate one top of a sine wave from another! The latency may be more than the period of the sine. So I turned down the generator frequency until the time between input and output peak stopped moving, indicating the true latency.

In the figure above, the generator frequency is down to 250 Hz, and I needed 10 times the sensitivity on the oscilloscope to see the now filter-damped output. The DeltaX-cursor says 2.06 ms from input to output. This is not a very accurate placing of cursors, but will have to do. I expected a 32-sample delay - which corresponds to 32/16 kH = 2 ms. Not bad.
I also did a Bode-plot for the DMA-based filter. I must say I dont really trust the phase-measurement - anyway, here it is:

Source Code
Note how main.c is a mixture of code generated by STM32CubeMX and custom user code. It is clear that the user code sections are carefully placed to avoid being overwritten by the code generator. In the longer run you may want to give up the ability to use STM322CubeMX on your project - or maybe have a small test-project alongside for experiments. At least commit to git before usage, and do a git diff afterwards.
I should also say that main.c is not the only file that is touched by STMCube32MX, but it is the only file I wrote code into.
The source below is the final code after selecting differential input for the ADC - AND using DMA for data transfer. I have used comments to show the intermediate steps (single-ended inputs and one-interrupt-per-sample). Generally I use the // style comments, while STM32CubeMX uses the /* */ style for generated code.
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2026 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
#define FILTER_LEN 32U
#define INPUT_LEN 32U /* Must be 2**n */
#define INPUT_MASK (INPUT_LEN - 1U) /* 31 */
#define DMA_SIZE 16
#define DMA_HALF (DMA_SIZE/2)
// DAC and ADC are 12 bit - using up 16-bit "half-words"
static uint16_t adc_buffer[DMA_SIZE];
static uint16_t dac_buffer[DMA_SIZE];
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
DAC_HandleTypeDef hdac1;
DAC_HandleTypeDef hdac2;
DMA_HandleTypeDef hdma_dac1_ch1;
TIM_HandleTypeDef htim6;
UART_HandleTypeDef huart2;
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);
static void MX_DAC1_Init(void);
static void MX_TIM6_Init(void);
static void MX_USART2_UART_Init(void);
static void MX_DAC2_Init(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
float input_buf[INPUT_LEN];
/* Fs = 16000 Hz
* 32 taps, symmetric / linear phase
* Passband: 900 ... 1100 Hz
* |H(1000 Hz)| = 1.000000 = 0 dB
* Group delay: 15.5 samples = 0.96875 ms
*/
const float filter[FILTER_LEN] = {
8.8373698637e-03f,
8.4401607913e-03f,
7.4514210016e-03f,
3.6490905018e-03f,
-5.0424191635e-03f,
-1.9207062988e-02f,
-3.7033800403e-02f,
-5.4288719813e-02f,
-6.5287685151e-02f,
-6.4621345160e-02f,
-4.9081385089e-02f,
-1.9117566171e-02f,
2.0729983966e-02f,
6.2661332782e-02f,
9.7532872005e-02f,
1.1731013139e-01f,
1.1731013139e-01f,
9.7532872005e-02f,
6.2661332782e-02f,
2.0729983966e-02f,
-1.9117566171e-02f,
-4.9081385089e-02f,
-6.4621345160e-02f,
-6.5287685151e-02f,
-5.4288719813e-02f,
-3.7033800403e-02f,
-1.9207062988e-02f,
-5.0424191635e-03f,
3.6490905018e-03f,
7.4514210016e-03f,
8.4401607913e-03f,
8.8373698637e-03f
};
/**
* This is one round of the FIR filter
* It is run exactly once per input sample
* It is completely the same in single-interrupt and in DMA modes.
* Note that it stores the newest sample at the lowest (wrapping) index in the buffer
* The loop has:
* - index i into the filter - running a full filter from start to end
* - index idx into the input_buffer - running through the full buffer from newest to oldest sample
*/
uint16_t fir_round(uint16_t adc_sample)
{
static uint32_t write_idx = 0U;
float x = (float) (adc_sample - 2048) / 2048.0f; // Signed float from unsigned data
float y = 0.0f;
input_buf[write_idx] = x;
uint32_t read_idx = write_idx;
for (uint32_t i = 0U; i < FILTER_LEN; ++i)
{
y += filter[i] * input_buf[read_idx];
/* Forward through memory = backward in sample age. */
read_idx = (read_idx + 1U) & INPUT_MASK;
}
/* Next new sample occupies the preceding buffer slot. */
write_idx = (write_idx - 1U) & INPUT_MASK;
float dac_value = 2048.0f + 2* 2048.0f * y; // Double up because of the differential mode!
if (dac_value < 0.0f)
{
dac_value = 0.0f;
}
else if (dac_value > 4095.0f)
{
dac_value = 4095.0f;
}
return (uint16_t)(dac_value + 0.5f);
}
void fir_buffer_init()
{
for (int i=0; i < INPUT_LEN; i++)
input_buf[i] = 0.0f;
}
// Note that to run the buffer-switching of the two halfes of the DMA
// the latency ends up corresponding to the full buffer
// While the first 8 samples are being picked up by the ADC, the DAC also emits its first 8
// When the ADC gives us these first 8 for processsing, the DAC starts emitting the second half
// - and we start filling the first half. So a full DAC buffer len out before meaningful data
void dac_buffer_init()
{
for (uint32_t i = 0; i < DMA_SIZE; i++)
{
dac_buffer[i] = 2048; // 0 value
}
}
// So-called WEAK callbacks are used here
// The weak implementation is in the framework
// If we define same signature - our implementation "wins"
/**
* This is the ISR callback, that the framework calls once per input sample
* In the simple adc-interrupt-calc-dac solution we basically do everything here.
*/
/*
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
ISR_MARK_GPIO_Port->BSRR = ISR_MARK_Pin; // Set pin high
uint16_t adc_sample = HAL_ADC_GetValue(hadc);
uint16_t dac_sample = fir_round(adc_sample);
HAL_DAC_SetValue(&hdac1,
DAC_CHANNEL_1,
DAC_ALIGN_12B_R,
dac_sample);
ISR_MARK_GPIO_Port->BRR = ISR_MARK_Pin; // Set pin low
}
}
*/
int adc_count1 = 0;
int adc_count2 = 0;
// We consume half an ADC DMA-buffer here and put it to the DAC DMA buffer
// We have as many sample-periods to do the work as we have samples
// First half of the buffers
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
ISR_MARK_GPIO_Port->BSRR = ISR_MARK_Pin; // Set pin high
// ADC first half is ready.
// DAC first half has just been consumed and is safe to overwrite.
adc_count1++;
for (int i=0; i < DMA_HALF; i++)
{
uint16_t adc_sample = adc_buffer[i];
dac_buffer[i] = fir_round(adc_sample);
}
ISR_MARK_GPIO_Port->BRR = ISR_MARK_Pin; // Set pin low
}
}
// Second half of the buffers
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
adc_count2++;
ISR_MARK_GPIO_Port->BSRR = ISR_MARK_Pin; // Set pin high
// ADC second half is ready.
// DAC second half has just been consumed and is safe to overwrite.
for (int i=DMA_HALF; i < DMA_SIZE; i++)
{
uint16_t adc_sample = adc_buffer[i];
dac_buffer[i] = fir_round(adc_sample);
}
ISR_MARK_GPIO_Port->BRR = ISR_MARK_Pin; // Set pin low
}
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
// Clearing the buffers can be done early
fir_buffer_init();
dac_buffer_init();
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_DAC1_Init();
MX_TIM6_Init();
MX_USART2_UART_Init();
MX_DAC2_Init();
/* USER CODE BEGIN 2 */
// HAL_DAC_Start(&hdac1, DAC_CHANNEL_1); // Replaced by HAL_DAC_Start_DMA
// HAL_DAC_SetValue(&hdac1,
// DAC_CHANNEL_1,
// DAC_ALIGN_12B_R,
// 0U);
// The second DAC is fixed - no interrupts or DMA
HAL_DAC_Start(&hdac2, DAC_CHANNEL_1);
// Constant output - half full scale. NB! no DMA for this
// Externally connected to ADC_IN2 (-)
HAL_DAC_SetValue(&hdac2,
DAC_CHANNEL_1,
DAC_ALIGN_12B_R,
2048U);
// Used when each conversion generates an interrupt
//HAL_ADC_Start_IT(&hadc1);
HAL_DAC_Start_DMA(&hdac1,
DAC_CHANNEL_1,
(uint32_t *)dac_buffer,
DMA_SIZE,
DAC_ALIGN_12B_R);
HAL_ADC_Start_DMA(&hadc1,
(uint32_t *)adc_buffer,
DMA_SIZE);
/* Start the timer last: it begins both sampling and DAC updates */
HAL_TIM_Base_Start(&htim6);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL16;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC12;
PeriphClkInit.Adc12ClockSelection = RCC_ADC12PLLCLK_DIV1;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief ADC1 Initialization Function
* @param None
* @retval None
*/
static void MX_ADC1_Init(void)
{
/* USER CODE BEGIN ADC1_Init 0 */
/* USER CODE END ADC1_Init 0 */
ADC_MultiModeTypeDef multimode = {0};
ADC_ChannelConfTypeDef sConfig = {0};
/* USER CODE BEGIN ADC1_Init 1 */
/* USER CODE END ADC1_Init 1 */
/** Common config
*/
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T6_TRGO;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DMAContinuousRequests = ENABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
hadc1.Init.LowPowerAutoWait = DISABLE;
hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
/** Configure the ADC multi-mode
*/
multimode.Mode = ADC_MODE_INDEPENDENT;
if (HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode) != HAL_OK)
{
Error_Handler();
}
/** Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SingleDiff = ADC_DIFFERENTIAL_ENDED;
sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
sConfig.OffsetNumber = ADC_OFFSET_NONE;
sConfig.Offset = 0;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN ADC1_Init 2 */
/* USER CODE END ADC1_Init 2 */
}
/**
* @brief DAC1 Initialization Function
* @param None
* @retval None
*/
static void MX_DAC1_Init(void)
{
/* USER CODE BEGIN DAC1_Init 0 */
/* USER CODE END DAC1_Init 0 */
DAC_ChannelConfTypeDef sConfig = {0};
/* USER CODE BEGIN DAC1_Init 1 */
/* USER CODE END DAC1_Init 1 */
/** DAC Initialization
*/
hdac1.Instance = DAC1;
if (HAL_DAC_Init(&hdac1) != HAL_OK)
{
Error_Handler();
}
/** DAC channel OUT1 config
*/
sConfig.DAC_Trigger = DAC_TRIGGER_T6_TRGO;
sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
if (HAL_DAC_ConfigChannel(&hdac1, &sConfig, DAC_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN DAC1_Init 2 */
/* USER CODE END DAC1_Init 2 */
}
/**
* @brief DAC2 Initialization Function
* @param None
* @retval None
*/
static void MX_DAC2_Init(void)
{
/* USER CODE BEGIN DAC2_Init 0 */
/* USER CODE END DAC2_Init 0 */
DAC_ChannelConfTypeDef sConfig = {0};
/* USER CODE BEGIN DAC2_Init 1 */
/* USER CODE END DAC2_Init 1 */
/** DAC Initialization
*/
hdac2.Instance = DAC2;
if (HAL_DAC_Init(&hdac2) != HAL_OK)
{
Error_Handler();
}
/** DAC channel OUT1 config
*/
sConfig.DAC_Trigger = DAC_TRIGGER_NONE;
sConfig.DAC_OutputSwitch = DAC_OUTPUTSWITCH_ENABLE;
if (HAL_DAC_ConfigChannel(&hdac2, &sConfig, DAC_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN DAC2_Init 2 */
/* USER CODE END DAC2_Init 2 */
}
/**
* @brief TIM6 Initialization Function
* @param None
* @retval None
*/
static void MX_TIM6_Init(void)
{
/* USER CODE BEGIN TIM6_Init 0 */
/* USER CODE END TIM6_Init 0 */
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* USER CODE BEGIN TIM6_Init 1 */
/* USER CODE END TIM6_Init 1 */
htim6.Instance = TIM6;
htim6.Init.Prescaler = 0;
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 3999;
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM6_Init 2 */
/* USER CODE END TIM6_Init 2 */
}
/**
* @brief USART2 Initialization Function
* @param None
* @retval None
*/
static void MX_USART2_UART_Init(void)
{
/* USER CODE BEGIN USART2_Init 0 */
/* USER CODE END USART2_Init 0 */
/* USER CODE BEGIN USART2_Init 1 */
/* USER CODE END USART2_Init 1 */
huart2.Instance = USART2;
huart2.Init.BaudRate = 38400;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
huart2.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart2.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART2_Init 2 */
/* USER CODE END USART2_Init 2 */
}
/**
* Enable DMA controller clock
*/
static void MX_DMA_Init(void)
{
/* DMA controller clock enable */
__HAL_RCC_DMA1_CLK_ENABLE();
/* DMA interrupt init */
/* DMA1_Channel1_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(ISR_MARK_GPIO_Port, ISR_MARK_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin : B1_Pin */
GPIO_InitStruct.Pin = B1_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pin : ISR_MARK_Pin */
GPIO_InitStruct.Pin = ISR_MARK_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(ISR_MARK_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pin : LD2_Pin */
GPIO_InitStruct.Pin = LD2_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LD2_GPIO_Port, &GPIO_InitStruct);
/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */