This is only a preview of the July 2025 issue of Practical Electronics. You can view 0 of the 80 pages in the full issue. Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
Items relevant to "180-230V DC Motor Speed Controller":
Articles in this series:
Items relevant to "Repurposing the Mains Power-Up Sequencer":
Articles in this series:
Items relevant to "Intelligent Dual Hybrid Power Supply,.Part 2":
|
Circuit Surgery
Regular clinic by Ian Bell
Topics in digital signal processing –
Implementing DSP on a microcontroller, part two
W
e are looking at various
topics related to digital signal
processing (DSP).
DSP covers a wide range of electronics
applications where signals are manipulated, analysed, generated, stored or
displayed as digital data but originate
from and/or are converted to real-world
signals for interaction with humans or
other parts of the physical world.
Fig.1 shows the key elements of a
generic DSP system with a signal path
from an analog input via digital processing to an analog output.
This does not necessarily represent
every DSP system (not all have all the
parts shown), but it serves as a reference for the various subsystems we
will look at.
Last month, we introduced an example
microcontroller-based DSP implementation that we will be working through.
The aim is to implement a sinc filter,
which we discussed in detail in earlier articles.
We will use a DSP-capable processor
from the STM32 family from ST Microelectronics on a B-L475E-IOT01A
development board. However, other
suitable processors and boards from
the STM32 series could also be used.
Last month, we gave an overview
of some relevant aspects of the development board and ST’s development
software (STM32CubeIDE), relating this
to the generic DSP structure in Fig.1.
We will not give a full step-by-step
walk-through on using the software.
Therefore, if you would like to try these
examples and are not already familiar with STM32CubeIDE, you should
look at some online tutorials that cover
basic projects, such as LED blinkers, to
gain familiarity with the development
tools.
Analog
In
Antialiasing
filter
Sample and
hold
STM32CubeIDE can automatically
generate the code required to initialise
the microcontroller, configure pin usage
and set-up on-chip peripheral hardware,
such as the digital-to-analog converter
(DAC). The requirements for a given
project are set via the Device Configuration Tool.
On saving the configuration, the software generates or updates the main
code file for the project, writing the
code required to set up the processor
for the chosen development board and
any user options from the Configuration
Tool.
The software installation includes a
library of functions, referred to as the
Hardware Abstraction Layer (HAL).
This makes it easy to control the microcontroller and its peripherals without
having detailed low-level knowledge
of its hardware, such as knowing what
binary codes to write to specific registers to achieve a hardware operation.
These are used in the automatically generated configuration code, and
should also be used where appropriate in user-written code (eg, to write a
value to the DAC).
Before looking at the implementation
of filters, we will discuss individual aspects of the system: the DAC and ADC
(analog-to-digital converter), sample
timing and data transfer. We will look
at simple approaches and their disadvantages and more advanced approaches
that make better use of the microcontroller’s resources.
We will provide code examples to illustrate the principles discussed. These
can be run as fully operational projects
on the development board.
First we will consider controlling
the DAC, which leads naturally to how
sample timing can be organised.
Digital
ADC
Digital
processing
Fig.1: a generic digital signal processing (DSP) system structure.
6
Reconstruction
filter
To test the basic operation of the DAC,
we can make it generate a simple waveform and view it on an oscilloscope.
Our final goal is to implement a filter,
not a waveform synthesis system, so
we will keep this very straightforward
and just generate a stepped sawtooth
(see Fig.2). This is simple to code, and
includes some large jumps in output
voltage, which will show how fast the
DAC output can change.
The step waveform is generated by
initially setting the DAC output to
DAC_Start, then subtracting DAC_Step
each time a new value is required until
the current value written to the DAC
(DAC_Value) is less than the step value.
At this point, the value will be set
back to DAC_Start, and the process is
repeated.
We will use relatively large steps;
stepping the DAC through every code
value would result in a long, slow slope
that may be difficult to resolve to individual steps on a basic oscilloscope.
The waveform in Fig.2 is shown as
a positive-only voltage, as this is what
is available on the development board.
The DAC output voltage range is determined by its reference voltage inputs.
On the B-L475E-IOT01A1 board the
VREF+ pin is connected to the 3.3V
analog power supply (VDDA) and the
VREF- pin is connected to ground (0V),
as per page 3 of the schematic (https://
pemag.au/link/ac5j).
The DAC output voltage therefore
ranges from 0V to 3.3V. The DAC is a 12-bit
device, so the maximum for DAC_Value
Voltage
DAC_Start
DAC_Step
Analog
DAC
Initial DAC setup
Out
Sample period
Time
Fig.2: the desired test signal from our DAC.
Practical Electronics | July | 2025
is 2 12 – 1 = 4095, and the output
voltage is given by:
DAC_OUT = VREF+ × DAC_Value ÷ 4095
So, in this case:
DAC_OUT = 3.3 × DAC_Value ÷ 4095
It is interesting to see how fast the
DAC can be updated, so initially, no
attempt will be made to set the sample
period to a specific value. The code
will comprise a free-running loop to
calculate and apply the waveform described above.
It is useful to have a digital output to
indicate when a new value was sent to
the DAC. Bringing a digital output high
before a function is called to update
the DAC and bringing it low again afterwards will result in waveforms as
shown in Fig.3. The time between corresponding edges of the pulse waveform
in successive cycles will be equal to the
sampling time.
The code required to create the waveforms in Figs.2 & 3 can be defined by the
pseudocode shown in Listing 1.
Auto-generated and user code
Last month, we looked at the microcontroller’s DAC hardware and
development board schematic, and
determined that we will use DAC1’s
output channel 2 on pin PA5. To briefly recap, this requires changing the pin
usage of PA5 to DAC1_OUT2 in the
Device Configuration Tool.
We also need to configure the DAC
itself (DAC1 in the Analog section of
the Categories list). Here, in the Mode
panel we change “OUT2 connected
to” from “Disable” to “only to external pin” – this will enable the DAC
and route its output signal to the pin.
After making these changes, save the
configuration and that will generate C
code for the project.
The generated main.c file contains
about 700 lines of code, not all of which
is directly relevant to this project. A lot
of it configures the processor to be compatible with the development board. As
is typical for embedded software, there
is initialisation/configuration code that
Voltage
Digital waveform
indicating DAC update
DAC output
Sample period
Time
Fig.3: pulses from an output help us
measure the DAC output’s update timing.
Practical Electronics | July | 2025
runs once, upon board reset or powerup, after which the program enters an
infinite loop where the main operations
are performed.
The generated main loop is initially
empty, so the program will initialise
the hardware and then enter an infinite
loop that does nothing.
We need to add code before the main
loop to define any variables and such
that we require, plus code to perform
any initialisation that is not automatically generated. We also need to write
the code for the main loop. We can add
functions to the file and call these from
our code.
As well as the main loop, we can
also write code that is triggered to run
by events in the hardware – we will
be discussing this in more detail later.
Listing 2 shows the code added to the
auto-generated main.c file to implement
the stepped DAC waveform, which is
based on Listing 1. The variables discussed above are declared on lines 59
to 61. The code for the main loop is on
lines 127 to 142.
All of our code is placed between
corresponding “User Code” start and
end comments. We must always do
this to prevent the code we add from
being overwritten by any configuration
updates. Some user code sections are expected to be used for specific purposes;
for example, “PV” (Listing 2, line 58) is
for private variables. Others are more
general and just numbered.
The main loop has two user sections,
making it relatively easy to mistakenly
add code between the sections (Listing
2, line 130), which would be erased after
58
59
60
61
62
…
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
Initialize DAC
Start DAC converting
DAC_Value = DAC_Start
while (1)
{
if (DAC_Value < DAC_Step)
DAC_Value = DAC_Start
else
DAC_Value = DAC_Value - DAC_Step
Set digital output to 1
Set DAC output to DAC_Value
Set digital output to 0
}
Listing 1: pseudocode for generating a
stepped sawtooth waveform.
making any configuration changes. So
avoid doing that.
Starting the DAC
Before the DAC can be used, it must
be initialised to operate as needed by
the design. The registers that control its
operation, and the hardware on the chip
that controls signal routing, must be set
up as required. Most of this work is done
by the code that is automatically generated by the Device Configuration Tool.
The generated main.c file contains
the definition of a global variable called
hdac1, ie:
DAC_HandleTypeDef hdac1;
This variable is used to reference the
DAC whenever we perform an operation on it using the HAL code library
functions.
A function named MX_DAC1_Init
is called during the initialisation part
of the microcontroller software, along
/* USER CODE BEGIN PV */
uint32_t DAC_Start = 4095; // Value at peak of sawtooth wave
uint32_t DAC_Value = 4095; // Current DAC output value
uint32_t DAC_Step = 255;
// Change in DAC value at each step (sample)
/* USER CODE END PV */
/* USER CODE BEGIN 2 */
HAL_DAC_Start(&hdac1, DAC_CHANNEL_2);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// Calculate next sawtooth waveform value for DAC
if (DAC_Value < DAC_Step)
DAC_Value = DAC_Start;
else
DAC_Value = DAC_Value - DAC_Step;
// Update the DAC and pulse the D8 pin to help measure timing
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_SET);
HAL_DAC_SetValue (&hdac1, DAC_CHANNEL_2, DAC_ALIGN_12B_R, DAC_Value);
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_RESET);
}
/* USER CODE END 3 */
Listing 2: user code for controlling the DAC.
7
Timing pulse
DAC output
Fig.4: the output from the DAC (blue) and timing pulses (red) from running the code in Listing 2, as captured by an oscilloscope.
with various other peripheral configuration functions. It assigns the hdac1
handle, initialises the DAC and configures output channel 2 in accordance
with our configuration settings. The
function code itself can be found about
around a third of the way through the
generated main.c file.
While the auto-generated code initialises the DAC and its output channel,
we still must start it operating (performing conversions) by calling the
HAL_DAC_Start function. This function
needs to be added to the code before the
main while() loop (Listing 2, line 122).
HAL_DAC_Start requires two parameters. The first is the handle mentioned
above, while the second specifies the
DAC channel to use. We’re using channel 2, identified by DAC_CHANNEL_2,
a value that is #defined in the HAL
library.
It is possible to do all this from scratch
by finding details of the relevant definitions, functions and how to use them
in the Description of STM32L4/L4+
HAL and low-layer drivers user manual
(https://pemag.au/link/ac5h).
Controlling the DAC output
To set the output voltage of the
DAC, we use the HAL library function
HAL_DAC_SetValue (Listing 2, line 140).
This function has four parameters. The
first two are the DAC handle address
and channel, as for the start function.
The third is a data alignment specifier, while the fourth is the DAC output
value.
The DAC is a 12-bit bit type, but the
value parameter is 32-bits, so the alignment specifier determines which of the
32 bits (eg, leftmost 12 or rightmost 12)
are the DAC value.
The code in Listing 2 uses the rightmost (lower) 12 bits, as this means the
DAC value is equal to the numerical
value of the parameter. However, care
8
must be taken to keep the parameter in
the valid range of 0 to 4095.
The value DAC_ALIGN_12B_R is defined in the library to set right 12-bit
alignment.
The digital output is set using the
HAL_GPIO_WritePin function, which
takes three parameters. The first identifies the GPIO port of the pin to be
controlled (Port A to Port H); GPIOA to
GPIOH are #defined to reference these
ports. In this case, port B is being used
(the pin used is PB2, ie, Port B, pin 2).
The next parameter is the pin number.
The file main.h, which is also automatically created as part of the project,
has #defines for the board signals, relating them to the pins. So the value
ARD_D8_Pin can be used here. It would
also be possible to use GPIO_PIN_2, as
that is #defined in the HAL library for
pin 2.
The final parameter is the state to
set the pin to; GPIO_PIN_RESET and
GPIO_PIN_SET are #defined for logic
low and high, respectively.
58
59
60
61
62
63
…
89
90
91
92
93
…
138
139
140
141
142
143
144
145
146
147
Execution results
I captured the DAC output from
running the code in Listing 2 on an oscilloscope, as shown in Fig.4; it clearly
corresponds with Figs.2 & 3. The timing
pulse has a frequency of 564kHz (a
1.77μs period), as measured by the oscilloscope. Remember that we are not
setting or controlling this sampling
rate – the DAC update is free-running.
The waveform is not quite as it should
be, because the DAC takes longer than
one sample period to change its output
for the largest step; Fig.5 shows this in
detail. The largest step is labelled as step
1, with others following from this. The
positive edge of the timing pulse indicates the approximate time at which a
new value is written to the DAC to request a new output value.
For the large step (step 1), the request
for the next value (step 2) occurs before
the output has finished changing. Thus,
the DAC output does not quite reach
the final value for step 1 before it starts
changing to the step 2 value. Step 2
/* USER CODE BEGIN PV */
uint32_t DAC_Start = 4095; // Value at peak of sawtooth wave
uint32_t DAC_Value = 4095; // Current DAC output value
uint32_t DAC_Step = 255;
// Change in DAC value at each step (sample)
uint32_t LoopDelay = 51;
// Loop delay to set sample period
/* USER CODE END PV */
int main(void)
{
/* USER CODE BEGIN 1 */
uint32_t i; // Loop counter for delay loop
/* USER CODE END 1 */
// Update the DAC and pulse the D8 pin to help measure timing
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_SET);
HAL_DAC_SetValue (&hdac1, DAC_CHANNEL_2, DAC_ALIGN_12B_R, DAC_Value);
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_RESET);
// Add delay
for (i=0; i<=LoopDelay; i++){}
}
/* USER CODE END 3 */
Listing 3: adding a delay to the DAC loop.
Practical Electronics | July | 2025
Step 1
requested
Step 1 almost Step 3
achieved
requested
Step 2
Step 2
Step 3
requested
achieved
achieved
Timing pulse
DAC output
Fig.5: a zoom-in on the waveforms in Fig.4, allowing the timing to be seen more clearly.
to step 3 operates correctly due to the
much smaller output voltage change.
We have no reason to expect that letting this DAC run freely would give
perfect results, but this serves to show
that we need to take account of the
DAC’s capabilities.
Setting the sampling period
The microcontroller datasheet indicates that the DAC could take up to 3μs
to change its output, so a 1.77μs sample
period is too short. We could slow the
DAC update down by adding a delay
to the main loop.
The HAL_Delay() function is potentially useful here; inserting a call
to this in the loop could be used to
control the duration of each iteration.
However, HAL_Delay() only provides
delays in milliseconds – so it can only
be used to control sampling at 1ms intervals (a 1kHz sampling frequency or
lower), which is far too slow for many
applications.
Another problem we have is that the
HAL_Delay() function is not particularly accurate with respect to the passed
delay parameter, particularly with smaller delays. A call of HAL_Delay(D)
produces a delay between D and D+1
milliseconds.
This is because it is implemented
using a free-running hardware counter,
and the +1 effect is due to ensuring the
delay is at least D, even if the function
is called just before the next counter
update occurs.
If the HAL_Delay() function is called
repeatedly with a short gap between
calls (as would happen in a loop with
little other code), the calls will occur
just after a counter update, so the delay
will be close to D+1 each time.
We can add shorter delays using a forloop that counts for a specified number
of iterations and adjust this to control
the timing (see Listing 3, line 144). To
use the loop, we need to declare a loop
variable (Listing 3, line 92) and set a
value for the number of iterations to
use (Listing 3, line 62).
Fig.6 shows the output of the DAC
with the for-loop delay added. The
slower sampling rate means that the
DAC is able to reach the required output
level well before the next value is
requested, even for the largest step. By
experiment, a value of 51 for LoopDelay
resulted in a sampling frequency of
96.4kHz, the closest to 96kHz (a commonly used sample frequency for audio)
that could be obtained.
This illustrates a problem with this
approach – it is difficult to precisely
set the sample frequency. LoopDelay
values of 50 and 52 gave 97.9kHz and
94.9kHz, respectively, so 51 was the best
option, but it is not really good enough.
Another potential problem is that
if the other code in the loop includes
conditional operations (eg, if… then…
else…), the number of instructions encountered on each loop iteration could
be different, resulting in a varying delay
value. This is bad news if we are implementing DSP that relies on a fixed
sampling rate.
In conclusion, adding a delay to a loop
like this is a crude technique that does
not allow precise control of timing, although it is easy to do.
Hardware timers
Accurate time measurement can be
readily achieved using hardware timers.
These are digital counter circuits on the
microcontroller that are clocked using
specific, accurately controlled frequencies. Such counter/timers can be started
and stopped by the processor, and their
count values can be set to specific values
or read by the processor.
The value in a counter incremented at
a precise rate is directly proportional to
the time elapsed since it started counting (or we can calculate the difference
from when it was previously read). This
can be used make time or frequency
Timing pulse
DAC output
Fig.6: the DAC waveform with a sampling rate (frequency) of 96kHz.
Practical Electronics | July | 2025
9
10
/1
80
SYSCLK (MHz)
80
AHB Prescaler
/1
80
PCLK1
80
80MHz max
AHB2 Prescaler
/1
80
80 APB1 peripheral clocks (MHz)
80 APB1 timer clocks (MHz)
X1
80
PCLK2
80MHz max
>
measurements of external events, or to
control operations or events that must
be accurately timed. Both one-off and
repetitive timed operations can be controlled this way.
The previously mentioned function
HAL_Delay() uses count difference
values, but does not meet our requirements primary due to the fact that only
millisecond timing is available.
To measure or control time with a
timer, we need to set the frequency at
which it is clocked. In STM32CubeIDE,
this can be checked (and, if necessary,
modified) in the Clock Configuration tab
of the Device Configuration Tool. This
provides a schematic of the complete
clock system of the microcontroller,
which is quite complex.
The microcontroller datasheet (https://
pemag.au/link/ac5i) provides full details on the clock system.
The block schematic diagram in the
Clock Configuration tab of the Configuration Tool shows that the timers are
connected to the APB1/APB2 timer
clocks that run at the 80MHz system
clock (SYSCLK) frequency by default
(APB is Advanced Peripheral Bus). Fig.7
shows the relevant part of the diagram.
The timer clock frequency can be
reduced using the timer’s prescaler (the
APB1/APB2 prescalers in Fig.7). A prescaler is a circuit that reduces a high
frequency to a lower one by an integer
factor. The longer the period that needs
timing, the lower the counter clock frequency needs to be for the counter to
be able to count to a number within its
range in the desired period.
The prescaler facilitates a very wide
range of timing capability for the on-chip
timers. For this project, dividing by 8
in the prescaler will make the counter
count at 10MHz (one count every 0.1μs),
which will facilitate easy calculation of
delay values in the code.
There are several different timer types
on STM32 microcontrollers (basic,
general purpose, advanced etc). The
microcontroller documentation provides details on this, allowing users
to determine which best fits their requirements. We need a basic timer, and
PCLK1
80MHz max
>
Listing 4: pseudocode for using a timer to
control the DAC output loop rate.
80 FCLK cortex clock (MHz)
AHB1 Prescaler
>
Initialize DAC
Initialize Timer
Start DAC converting
Start Timer running
while(1)
{
Reset Timer to zero
Calculate new DAC value
Update the DAC
Wait until the Timer has reached
the required count value
}
80 APB2 peripheral clocks (MHz)
80 APB2 timer clocks (MHz)
X1
Fig.7: timer connections in the microcontroller’s clock tree.
timer 6 (TIM6) on our microcontroller
fulfils this role.
Before using the timer, we need to
set it up using the Configuration Tool.
To do this, go to the Pinout & Configuration tab of the Device Configuration
Tool, click on Timers in the Categories
list to expand it, then click on TIM6.
Click on the Activated checkbox, which
will cause the configuration settings to
be displayed (Fig.8).
The frequency is divided by one plus
the value entered into the prescaler
configuration, so entering zero gives
no frequency reduction (divide by 1).
To set divide-by-eight, a value of seven
is required in the “Prescaler (PSC – 16
bits value)” field of the configuration
(Parameter Settings tab).
It is common practice for STM32CubeIDE users to write this as “8-1” (eight
minus one) to make the required division value more obvious to read.
Using the timer
We will start with a simple use of the
timer, similar to the previous example
and outlined by the pseudocode in
Listing 4. A timer is set to start counting
at the beginning of the loop. As this is
separate hardware, it is not affected by
running other code, including the DAC
update code. Once the DAC is updated,
the code waits for the counter to reach a
specific value before finishing the loop.
The time taken to execute each loop
iteration, equal to the DAC sampling
rate, is the time taken for the counter
to reach the specified count value, so:
DAC_sample_period = counter_period
× delay_count_value
The sample period must be set to be
longer than the time it takes to update
the DAC and check the timer at least
once; otherwise, the required delay time
will already have passed before the waiting part is reached, and the timer will
not be in control of how long the loop
takes to iterate.
It is not a problem if the code has condition operations and may take different
times on each iteration (unlike the forloop delay) as long as the slowest case
is fast enough. If follows that we do not
need to know exactly how long the DAC
update code takes, as the wait-for-timer
step will control the loop iteration time.
However, the delay time must also be
longer than the time the DAC output
voltage takes to change, which is likely
to be longer than the time taken to run
the ‘update DAC’ code (as seen in the
previous examples).
Timer-based DAC code
This example was created as a new
Fig.8: the microcontroller’s timer configuration settings in the IDE.
Practical Electronics | July | 2025
STM32CubeIDE project, with the pins
and DAC configured as in the previous
example and Timer 6 configured as just
discussed. The DAC waveform was calculated in the same way as before, so
the same variables are declared for this,
along with Time_Delay for setting the
required counter value in the waiting
step (Listing 5, lines 61–64).
The timer is started in the User Code
2 section, along with the DAC. Timer 6
is referenced using the htim6 handle
in the same way as hdac1 is used for
the DAC (Listing 5, lines 126 & 127).
The main loop uses the same code as
before to calculate the DAC values and
set its output and timing pulse (Listing 5,
lines 140–148). As indicated in the
pseudocode, we need to start the counter from zero in each loop iteration. This
can be done by writing zero directly to
the count register (Listing 5, line 137).
To check if the counter has reached the
required value for the delay, its current
value can be read using a HAL function
__HAL_TIM_GET_COUNTER(&htim6)
(Listing 5, line 151).
For a 96kHz sampling rate, we need a
sample period of 1 ÷ 96kHz = 10.417μs.
Given that the counter increments at
0.1μs intervals, we should use a value
of 104 for Time_Delay. However, experiments indicated that 101 gives the
closest value at 96.7kHz. The DAC
waveform is essentially the same as
shown in Fig.6.
60
61
62
63
64
65
…
125
126
127
128
…
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
The loop delay is about 0.3μs longer
than the timer value used. This is due
to the time between the counter reaching its ultimate value and it being reset.
During this time, the processor is executing the code that controls the loop
iterations for the waiting and main loops.
Using a prescaler value of 8 makes
the time increment nice round 0.1μs
steps, but this limits the choice of frequencies, particularly for higher values.
Interrupts
This example makes use of a hardware
timer, but not in the most efficient way.
Software-based delays such as using
for-loops or the HAL_Delay() function
are referred to as blocking code – they
tie up the processor when it could be
doing more useful operations.
Although in the second example,
the DAC is updated while the timer is
independently running, we still have
blocking code constantly checking the
timer.
A better approach it to use an interrupt to automatically trigger some
specific code to run (in this case, the
DAC update) when the required time
has elapsed (the DAC sampling time
in this case).
In general, an interrupt is an input to
a processor that causes it to stop what
it is doing and respond to the interrupt
event by running some specific code.
After completing the interrupt code,
/* USER CODE BEGIN PV */
uint32_t DAC_Start = 4095; // Value at peak of sawtooth wave
uint32_t DAC_Value = 4095; // Current DAC output value
uint32_t DAC_Step = 255;
// Change in DAC value at each step (sample)
uint32_t Time_Delay = 101; // TIM6 timer delay count
/* USER CODE END PV */
/* USER CODE BEGIN 2 */
HAL_DAC_Start(&hdac1, DAC_CHANNEL_2);
HAL_TIM_Base_Start(&htim6);
/* USER CODE END 2 */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
htim6.Instance->CNT = 0; // Reset counter
// Calculate next sawtooth waveform value for DAC
if (DAC_Value < DAC_Step)
DAC_Value = DAC_Start;
else
DAC_Value = DAC_Value - DAC_Step;
// Update the DAC and pulse the D8 pin to help measure timing
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_SET);
HAL_DAC_SetValue (&hdac1, DAC_CHANNEL_2, DAC_ALIGN_12B_R, DAC_Value);
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_RESET);
// Wait until the TIM6 counter reaches the time delay before iterating
while (__HAL_TIM_GET_COUNTER(&htim6) < Time_Delay);
}
/* USER CODE END 3 */
Listing 5: using a timer to control the DAC’s sample update rate timing.
Practical Electronics | July | 2025
called an Interrupt Service Routine
(ISR), the processor returns to the previous activity exactly where it left off.
Interrupts can be triggered by any
event that can be created or detected
by hardware, either internal (on-chip)
or external to the processor (such as a
digital input pin changing state).
For timers, an interrupt can be generated after a specific elapsed time,
causing the ISR code to run at that
time. To produce a signal from a DAC,
a hardware timer can be used to generate an interrupt at regular intervals (at
the DAC sampling rate) with the ISR
configured to set the DAC value.
Like other hardware setups, the Device
Configuration Tool is used to control
what conditions can cause interrupts
in a design – for example, a timer can
be set to cause an interrupt when it
reaches a particular count value.
In a later article, we will see how data
transfer from a memory buffer to a DAC
can be achieved automatically without
having to explicitly write to the DAC in
our code. This is called direct memory
access (DMA). We just have to periodically update the buffer with the DAC
waveform; interrupts can be arranged
to occur when this needs doing.
Callbacks
The STM32 code libraries include
all the general code required to make
ISRs work correctly (eg, to enable the
processor to switch safely from what it
was doing to the ISR and back). However, the libraries cannot contain the
code the developer wants to run when
a specific interrupt occurs.
To achieve this, the library defines
special ‘callback’ functions that the
user can define to handle their specific
part of the ISR.
Callback functions are widely used
in software in a situation where there
is code that needs to do ‘something’
when a particular condition occurs, but
at the time of writing the code managing the condition detection (eg, by the
microcontroller library developer), it
is not known what that ‘something’ is.
A mechanism is required that can
later define what the ‘something’ is to
run without changing the general condition/interrupt handling code. There
are a few ways to do this; one is to
define the name of a function that will
get called when the ‘something’ needs
to be done.
Effectively, the function already exists
but does nothing; however, if another
function with the same name is written (by the user) then it will be run
instead. The user simply writes the
required code in a function with the
predefined name, and it automatically
11
Fig.9: the timer settings for interrupt use (a portion of the Parameter tab is at the top, while the NVIC tab is below it).
runs when the related condition (eg, an
interrupt) occurs.
The STM32 HAL library provides
many predefined callback functions. In
this case, we want an interrupt after a
time period has elapsed, as measured by
a timer, so we use the function named
HAL_TIM_PeriodElapsedCallback().
This function is called when various
timers trigger an interrupt, so the code
has to check which timer caused it to
be called.
A timer handle is passed to the function as a parameter named htim. This
can be used to find out which timer
caused the callback to run. For example, the code if (htim == &htim6)
will only run what follows that statement if it was Timer 6. Full details are
shown in the example code later.
set PSC = 8 (as previously) and ARR =
100 (entered as 100-1 in the configuration), the period between interrupt calls
is (100 × 8) ÷ 80MHz = 10μs.
This will be the sampling period if
the timer period elapsed ISR callback
is used to update the DAC (a sampling
frequency of 100kHz).
To set this up in the Device Configuration Tool, select TIM6, check that it
is activated and has the prescaler value
set to 8-1 as previously. In the Parameter Settings tab, set the Counter Period
to 100-1, as shown in Fig.9. Leave other
values as their defaults. In the NVIC
(Nested Vectored Interrupt Controller)
Settings Tab, enable the global interrupt for TIM6 by checking the Enable
box (see Fig.9).
Configuring a timer for interrupts
In the examples discussed so far,
the timer is set up via the Device Configuration Tool. This is fine in some
situations, but it might be useful to
be able set the sampling frequency in
user code (eg, if it needs to vary). We
can look at the auto-generated code to
find out how to configure the timer (or
check the datasheet).
The function MX_TIM6_Init() is
called before the main loop, along with
various other hardware initialisation
functions. The code of the function itself
can be found about halfway through the
auto-generated main.c file.
In the MX_TIM6_Init() function,
The period of a timer can be set in
the Device Configuration Tool and is
referred to as the Count Period or the
AutoReload Register (ARR) value. The
counter elapsed interrupt occurs once
every ARR counts of the counter. As
discussed above, the clock period of
the counter is given by the APBx timer
clock (f APB) divided by the prescaler
(PSC) value.
Thus, the period for one count increment is PSC ÷ fAPB. The interrupt occurs
every ARR counts, so the period between
interrupt calls is (ARR × PSC) ÷ fAPB. For
example, knowing fAPB = 80MHz, if we
144
145
156
147
148
149
150
151
152
153
Configuring the timer with code
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_TogglePin(GPIOB, LED2_Pin);
HAL_Delay(500);
}
/* USER CODE END 3 */
Listing 7: blinking the LED simultaneously with the DAC waveform generation.
12
various values are set via the htim6
handle, based on the values entered
via the Device Configuration Tool.
For example, the period value of 100
discussed above is set using the code
htim6.Init.Period = 100-1;
After this, HAL_TIM_Base_Init()
is called to initialise the timer with the
appropriate values.
The function returns a status value,
which should equal HAL_OK. An error
handling function, Error_Handler(),
is called if there is a problem.
Similar code can be used again later (in
user code) to change the timer settings.
Code can also be written to calculate
the counter parameter values from other
data; for example, the period value
can be calculated, based on having
the required frequency (in kHz) and
prescaler values, using P e r i o d =
(80000.0/Prescaler)/Frequency;
where 80000 is the 80MHz APBx timer
clock frequency in kHz.
Timer-Interrupt DAC project code
This example was again created as a
new STM32CubeIDE project. The configuration of the pins and DAC was as in
the previous example, and Timer 6 was
set up in the Configuration Tool as just described. The DAC waveform is calculated
as in the previous examples, so uses the
same variables (Listing 6, lines 61–63).
Other variables are declared for prescaler, period and frequency, to allow
these to be manipulated in code as just
discussed (Listing 6, lines 64–66). The
period and frequency are floating-point
values to ensure accurate calculation.
Before the main while() loop, in the
User Code 2 section, the DAC is started
as previously (Listing 6, line 128). Then
the period configuration value for the
timer is calculated as just described,
and the prescaler and period values are
set via the htim6 handle, followed by
reinitialisation of the timer.
Practical Electronics | July | 2025
The timer is then started (Listing 6,
lines 130–140), at which point interrupts will be start being generated at
timer period intervals. In this example,
the prescaler value is fixed for simplicity, but code could be written to set the
prescaler to the most suitable value for
a given frequency.
The DAC waveform and timing pulse
code are the same as in previous examples, but these are now placed in the
HAL_TIM_PeriodElapsedCallback()
ISR in the User Code 4 section, near
the end of the auto-generated main.c
file (Listing 6, lines 750–767). As discussed above, the ISR checks that Timer
6 caused the interrupt (Listing 6, line
753) before running the DAC code.
The DAC and timing pulse waveforms
are again as in Fig.6, but now the measured frequency is very close to the
required value (measured at 96.02kHz
by the oscilloscope) due to the lower
prescaler value.
The main while() loop does not contain
any code in this version, but to illustrate
the processor performing other operations when it is not serving the timer
interrupt we can add LED blinking code
to the main loop, as shown in Listing 7.
This toggles a digital output pin connected to one LED on the development
board every half second (500ms), using
the HAL_Delay() function. The LED
blinking does not have any effect on the
DAC’s sampling frequency.
PE
60
61
62
63
64
65
66
67
…
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
…
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
/* USER CODE BEGIN PV */
uint32_t DAC_Start = 4095; // Value at peak of sawtooth wave
uint32_t DAC_Value = 4095; // Current DAC output value
uint32_t DAC_Step = 255;
// Change in DAC value at each step (sample)
uint32_t Prescaler = 1;
// Timer prescaler setting
float Period;
// Timer period in us
float Frequency = 96.0;
// DAC sample frequency in kHz
/* USER CODE END PV */
/* USER CODE BEGIN 2 */
HAL_DAC_Start(&hdac1, DAC_CHANNEL_2);
// Calculate TIM6 counter period (ARR) from required DAC sample frequency
// Clock frequency is 80 MHz
Period = (80000.0/Prescaler)/Frequency; // F in KHz
htim6.Init.Prescaler = Prescaler-1;
htim6.Init.Period = Period-1;
// Reinitialise TIM6 with new values and start it
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
Listing 6: using timer
}
interrupts to control the DAC’s
HAL_TIM_Base_Start_IT(&htim6);
/* USER CODE END 2 */
sample update rate timing.
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == &htim6)
{
// Calculate next sawtooth waveform value for DAC
if (DAC_Value <= DAC_Step)
DAC_Value = DAC_Start;
else
DAC_Value = DAC_Value - DAC_Step;
// Update the DAC and pulse the D8 pin to help measure timing
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_SET);
HAL_DAC_SetValue (&hdac1, DAC_CHANNEL_2, DAC_ALIGN_12B_R, DAC_Value);
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_RESET);
}
}
/* USER CODE END 4 */
JTAG Connector Plugs Directly into PCB!!
No Header!
No Brainer!
Our patented range of Plug-of-Nails™ spring-pin cables plug directly
into a tiny footprint of pads and locating holes in your PCB, eliminating
the need for a mating header. Save Cost & Space on Every PCB!!
Solutions for: PIC . dsPIC . ARM . MSP430 . Atmel . Generic JTAG . Altera
Xilinx . BDM . C2000 . SPY-BI-WIRE . SPI / IIC . Altium Mini-HDMI . & More
www.PlugOfNails.com
Tag-Connector footprints as small as 0.02 sq. inch (0.13 sq cm)
|