Benchmarking
Last updated
Last updated
When discussing the code architecture of a generic real-time audio device, we already remarked that if our processing callback is too slow with respect to the frequency of the DMA transfers, we will run into a condition called buffer underflow (or overflow, if you look at it from the point of view of the input DMA).
It's therefore very important to make sure that our processing is fast enough and find out if and where the code is using up a lot of time. Fortunately, the microcontroller provides us with functionalities that gives the possibility to monitor that.
The HAL
library includes a function uint32_t HAL_GetTick(void);
which will return the number of ticks since the start of the microcontroller in milliseconds. Unfortunately we cannot use this tool because a resolution of one millisecond is too large for most audio sampling frequencies. For instance, with MHz the period to perform one operation is, thus the micro-second granularity is way too slow.
In order to have a finer timebase, we will use the Nucelo's onboard timer, whose full technical details can be found here. Briefly, all computing boards (and microcontroller are no exception) possess an internal clock that provides a reference timebase signal; this timebase is usually generated by a crystal oscillator. The onboard timer is a roll-over counter that is incremented in lockstep with the timebase signal, often via a prescaler that can be used to lower its frequency, since the oscillator is usually very fast.
For our application, we will use a timer with a large counting capacity (32 bits) and we will set it to increment itself every microsecond.
To set up the timer we will use CubeMX and then regenerate the initialization code. Open the CubeMX file by double clicking the .ioc
file of the copied project it in the IDE project explorer.
In order to activate a timer, you need to set a "Clock Source". Open TIM2 in the Timers menu (TIM2 happens to be 32bit timer) and activate its clock by setting the Clock Source to "Internal Clock".
Next, we need to configure the timer in the configuration panel that appears:
TASK 1: Set the Prescaler value (in the figure above) in order to achieve a period for "TIM2", i.e. we want our timer to have aresolution.
Hint: Go to the "Clock Configuration" tab (from the main window pane) to see what is the frequency of the input clock to "TIM2". From this calculate the prescaler value to increase the timer's period to .
Set the Counter Period to 0xFFFFFFFF
; this ensures that the 32-bit timer counter only rolls around at its maximum value. You can leave the rest of the parameters as is for "TIM2". Finally, you can update the initialization code by saving the .ioc
file.
In order to use the timer we configured, we will define a couple of macros to start and stop the timer and a global variable to keep track of the time that elapses between calls. Between the USER CODE BEGIN PV
and USER CODE END PV
comment tags, add the following lines. Note the volatile
declaration for the timer, which underscores how this variable is a global variable modified by an interrupt service routine independently of the normal control flow of the rest of the code.
For instance, to benchmark the passthrough example, we can modify the Process function like so
In a real-time audio application the processing time cannot exceed the time between successive DMA calls; if this is not the case, we have a so-called buffer underflow which results in extremely corrupted audio. We will use our benchmarking timer to make sure we are within the limits.
TASK 2: In the passthrough example, the macro FRAMES_PER_BUFFER
determines the length of the DMA transfer. In our code, we set this length to 32 (stereo) samples.
What is the maximum processing time that we can afford in this case?
What if we change the value to 512 samples?
To check the actual time used by our processing function we will use an extremely convenient facility provided by the STM32 IDE, namely the possibility to monitor the live value of the variables in our code while the code runs.
Pull up the passthrough example and modify the processing function as shown in the previous section by inserting the START_TIMER and STOP_TIMER macros. Then launch the application in the debugger.
In the debugging window in the top right corner of the screen, select the "live variables" tag and add the variable timer_value_us.
You can see that the passthrough code takes about 33 microseconds to execute, which is well below the maximum available time. This is good news!
Are you ready to see the answer? :)