This is only a preview of the August 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:
Articles in this series:
Items relevant to "180-230V DC Motor Speed Controller, part two":
Articles in this series:
|
Circuit Surgery
Regular clinic by Ian Bell
Topics in digital signal processing –
Implementing DSP on a microcontroller, part three
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.
In the previous couple of articles, we
have been working through an example
microcontroller-based DSP implementation. Before getting to the implementation
of a sinc filter, we are looking at individual
aspects of the system: the digital-to-analog
converter (DAC), analog-to-digital converter (ADC), sample timing generation
and data transfer.
Last month’s article concluded with
the DAC controlled by an interrupt timer
triggering a callback function at the sampling frequency. This code can easily be
adapted to read the ADC in the callback
function instead. To illustrate basic operation of the ADC, the values read from
the ADC can be fed directly to the DAC
(no filtering or other processing will be
implemented to begin with).
Both devices use 12-bit samples and
operate within the same analog signal
voltage range. Therefore, no conversion
or adaption is required. If the ADC data
is passed directly to the DAC, it should
recreate the same signal that was fed to
the ADC, subject to the normal limitations
of sampled signal processing in terms of
Analog
In
Antialiasing
filter
Sample and
hold
maximum input signal frequency and
quantisation effects.
We will start with a simple implementation, then consider how the operating
speed can be improved using direct
memory access (DMA).
ADCs on the microcontroller
The microcontroller on the B-L475EIOT01A board we are using contains three
12-bit ADCs. These have a maximum
sampling rate of 5Msps (megasamples per
second). The ADCs have multiple input
channels connected to different pins or
internal signals on the microcontroller
via multiplexers.
The Device Configuration Tool in the
STM32CubeIDE app can be used to set
which channel each ADC will read from
(although the code can change this at
any time). ADC1 is most convenient for
us because it can be connected to pins
PC0 to PC5, which are on Arduino Uno
header CN4. This is shown in Figs.2(a)
& 2(b), both extracts from the PDF at
https://pemag.au/link/ac5j
U1A
No other circuitry is connected to
23
ARD.D1-UART4_TX
PA0/WKUP1
PB0
24
these pins.
ADCARD.D0-UART4_RX
input protection25 PA1
PB1
PA2
PB2
ARD.D10-SPI_SSN/PWM
26 can handle
The default configuration of a new
The microcontroller’s
ADC
PB3/SWO
PA3
ARD.D4
29
PB4
PA4
ARD.D7
30 supply voltSTM32CubeIDE project for the B-L475Evoltages
from 0V to 3.3V (the
PB5
PA5
ARD.D13-SPI1_SCK/LED1
31
PB6
PA6
ARD.D12-SPI1_MISO
IOT01A board already has pins PC0 to
age
of
the
microcontroller
32 and on-chip
PB7
ARD.D11-SPI1_MOSI/PWM
PA7
67
PA8
PB8
SPBTLE-RF-RST
PC5 configured as ADC1 inputs. All that
ADCs). This should be well
68 within the
PA9
PB9
USB_OTG_FS_VBUS
is needed then is to configure ADC1 USB_N
to
capabilities
of a lab signal 69
generator,
but
PA10
PB10
USB_OTG_FS_ID
70
PA11
USB_OTG_FS_DM
PB11
71
USB_P the signal from other sources
use the required pin.USB_OTG_FS_DP
We will connect
may need PB12
PA12
72
PA13/SWDIO
PB13
SYS_JTMS-SWDIO
76
ADC1 channel 1 (IN1) to pin PC0 (CN4
amplification
PB14
SYS_JTCK-SWCLKor attenuation. PA14/SWCLK
77
PA15
PB15
ARD.D9-PWM
header pin A5). This can be done by set15
ARD.A5-ADC
PC0
PC8
ting the Mode for IN1 of ADC1 to “IN1
16
ARD.A4-ADC
PC1
PC9
17
ARD.A3-ADC
PC2
PC10
Single-ended” using the Configuration
18
ARD.A2-ADC
PC3
PC11
33
Tool in STM32CubeIDE, as shown in
ARD.A1-ADC
PC4
PC12
34
ARD.A0-ADC
PC5
PC13/WKUP2
63
Fig.3 (the past two articles explain how
VL53L0X_XSHUT
PC6
PC14-OSC32_IN
64
VL53L0X_GPIO1_EXTI7
PC7
PC15-OSC32_OUT
to do this in more detail).
STM32L475VGTx
The DAC’s update time of around 3μs
Fig.2(a): the ADC-capable pins on the
indicates a maximum sampling rate of
STM32 microcontroller.
330kHz. This is slower than the ADC, so
CN4
the DAC limits the speed of a combined
Digital
ADC
Digital
processing
ARD.A0-ADC
ARD.A1-ADC
ARD.A2-ADC
ARD.A3-ADC
ARD.A4-ADC
ARD.A5-ADC
Analog
DAC
Fig.1: a generic digital signal processing (DSP) system structure.
32
system. This sampling rate is more than
adequate for experimenting with signals
in the audio range, which is what we
will focus on.
Common sampling rates for audio include 44.1kHz (CD), 48kHz (DVD), 96kHz
and 192kHz. A minimum sampling rate
of around 40kHz is the Nyquist rate for
the highest frequency human hearing
can perceive, about 20kHz. Higher sampling frequencies reduce the demands
on anti-aliasing and reconstruction filters (as discussed in earlier articles) and
may increase fidelity.
To observe ADC operation, a signal
must be applied to it. Any signal generator capable of delivering signals in
the appropriate voltage and frequency range could be used. This includes
standalone lab signal generators, those
built into oscilloscope and even computer sound cards.
Ideally, we want to automate measuring
the frequency response by coordinating
signal generation with amplitude measurement. We will discuss this later.
Reconstruction
filter
Out
1
2
3
4
5
6
A0
A1
A2
A3
A4
A5
AIN
W
e are looking at various
Header 6X1_Female_SMD
Fig.2(b): the ADC-capable pins are
routed to header CN4.
Practical Electronics | August | 2025
35
36
37
89
90
91
92
93
95
96
47
48
51
52
53
54
65
66
78
79
80
7
8
9
Fig.3: enabling the ADC1, IN1 input in the STM32CubeIDE Configuration Tool.
An alternative is to remove any DC from
the input with a coupling capacitor (C1 in
Fig.4) and set the ADC input DC level to half
the supply voltage using an equal-resistor
potential divider (R1 and R2). The signal’s
peak voltage must be less than 1.65V to
avoid clipping (3.3V peak-to-peak).
An op amp circuit could also be used to
apply the offset, which would buffer the
source signal, but that is more complex
and unnecessary for running simple tests.
The potential divider resistors load the
source, so their values should not be too
low. Their specific values are not particularly critical, and could range from 10s
to 100s of kilohms. The potential divider
resistors, resistor R3 and the coupling capacitor (value C) form a low-pass filter, in
which R1 and R2 act in parallel.
The effective resistance (RP ) is half the
potential divider resistor value (50kΩ for
Fig.4), ignoring R3, which does not have
a significant impact. The cutoff frequency
is 1 ÷ (2π RP C ), which is 3.2Hz for the
values shown, which is below the low
end of the audio range. So audio signals
will not be affected to any great extent.
The parallel R1/R2 combination also
forms a potential divider for the AC input
signal with the current-limiting resistor,
R3. If R3 is much smaller than RP, this
effect is minimal.
not able to sink sufficient current, the
Accidentally applying a voltage outside
supply voltage can be pulled up by the
the 0-3.3V range is a possibility when
input signal. If this is expected to be a
experimenting and this may damage the
problem, a zener diode (D3 in figure 4)
chip. One solution to this problem is to
can be used to keep the voltage on the
use diodes to clamp the input voltage
supply from rising too much.
close to the supply or ground if a potenIn the schematic, D3 is set up as a 3.9V
tially damaging input is applied.
zener using the quick “ako” (a kind of)
Fig.4 shows an LTspice schematic for
method of borrowing a different model
circuitry that can be used to connect
(1N750, 4.7V) and changing the breakthe signal source to the ADC, including
down voltage parameter (there is no 3.9V
a model of the ADC input. Diodes D1
zener in the library).
and D2 provide the input voltage proThis does not necessarily give a fully
tection; other parts of the circuit will be
ADC sampling
realistic model; however, the diode never
discussed shortly.
Fig.4 includes a representation of the
conducts in the simulation shown as it
If the input voltage (signal InADC)
ADC’s input (inside the dashed box).
is connected across an ideal 3.3V source
goes above 3.3V or below 0V by more
This comprises the sampling capacitor
(the microcontroller/ADC supply). It is
than the diode forward (switch-on)
(CADC ), switch and the resistance (RADC ) of
included to illustrate the option of using
voltage, the relevant diode conducts
wiring and switching networks between
a zener diode here.
preventing significant further voltage
the microcontroller’s pin and sampling
excursion at the ADC input. Schottky
capacitor. The STM32L475xx datasheet
diodes (such as BAT54) are suited to
specifies CADC as 5pF, but not RADC ; howADC input signal
this purpose due to their low forward
ever, other sources indicate 6kΩ.
Typically, the input to a DSP system is
voltage (about 0.3V).
The switch in the schematic is not a siman AC signal centred on 0V, so the signal
ARD.D3-PWM/INT1_EXTI0
Ordinary silicon diodes3V3
can be used,
ulation switch model – this is just an open
has both positive and negative peaks. The
ARD.D6-PWM
ARD.D8
but have a have a larger forward voltage
circuit as far as LTspice is concerned, so
ADC on the development board we are
SYS_JTDO-SWO
ARD.D5-PWM
R8
R9
(about
0.7V).
The
absolute
maximum
the simulation reflects the state of the cirusing
has
an
input
range
of
0-3.3V,
so
a
SPSGRF-915-SPI3_CSN
2K2
2K2
ST-LINK-UART1_TX
input
voltage
for
most
inputs
on
the
cuit when the ADC is not taking a sample.
DC
offset
needs
to
be
added
to
the
AC
ST-LINK-UART1_RX
microcontroller is 4V, so either typeARD.D15-I2C1_SCL
of
When the ADC takes a sample, the
signal – the result should be a signal cenARD.D14-I2C1_SDA
diode would work in this situation, INTERNAL-I2C2_SCL
but
switch closes briefly, and the sampling
tred on half the ADC input range (3.3V
INTERNAL-I2C2_SDA
silicon diodes would be marginal.
capacitor is charged. For the ADC to
÷ 2 = 1.65V).
ISM43362-BOOT0
ISM43362-WAKEUP
R10 leakage
R11
obtain an accurate result, the sampling
There are various ways to achieve this;
LED2Schottky diodes have higher
2K2
2K2
SPSGRF-915-SDN
currents, which can cause problems with
capacitor (CADC ) must have sufficient time
for example, lab function generators can
LSM3MDL_DRDY_EXTI8
linearity when protecting signal inputs,
usually apply a DC offset. However, this
to charge to the input voltage. It charges
LED3(WIFI) & LED4(BLE)
3V3
INTERNAL-SPI3_SCK
but the lower forward voltage
provides
is not possible with an audio line output,
from the signal source via any resistance
INTERNAL-SPI3_MISO
better protection.
such as from a PC sound card.
in the signal path.
INTERNAL-SPI3_MOSI
BUTTON_EXTI13
A resistor in series with the signal
C14
R12
generator
output (R3 inGND
Fig.4) limits the
current 0R
in the diodes
5.1pF to a safe level if a
X2
high voltage isNX3215SA-32.768K
applied. The required
C15level of overvoltage
value depends on the
GND
that might occur and
the diode’s current
5.1pF
handling. However, there may also be
an impact on ADC speed and accuracy,
which we will return to.
A possible problem with the diode
clamp is that the supply has to sink current during an overvoltage input. If it is
Fig.4: an LTspice schematic of ADC input protection, biasing and an ADC input model.
Practical Electronics | August | 2025
33
Fig.6: simulation results from circuit in Fig.4, with the
overvoltage clipped by protection diodes
Fig.5: simulation results from circuit in Fig.4, with a close-tomaximum amplitude input signal to the ADC.
This includes the source impedance,
any inserted resistors (eg, R3 in Fig.4) and
the on-chip resistance of the sampling
switch, wiring and signal multiplexers
(RADC ). There is also a parasitic capacitance of the wiring (CP in Fig.4), which
must be charged to the source voltage
before the sample is taken.
The value of CP in Fig.4 is somewhat
arbitrary – in reality, it will depend on
the physical structure of the circuit.
The sampling time required depends
on the desired accuracy – the more bits in
the conversion, the closer the voltage on
CADC must get to the source voltage. Looking at this the other way around, given
a specific sampling time and required
accuracy, there is a certain maximum
resistance that can be tolerated.
The STM32L475xx datasheet (https://
pemag.au/link/ac5i) specifies the maximum external resistance (designated RAIN)
for a range of scenarios – specifically,
different sample times and the required
resolution (the ADC has 12 bits, but you
do not have to use the full resolution).
If the default sampling time is insufficient, the time can be set using the
microcontroller’s SMPR registers. For
the default fastest sampling rate at 12
bits of resolution, the maximum RAIN is
100Ω (as used for R3).
Fig.7 shows the frequency response of
the ADC input network. This confirms the
low-frequency -3dB point is at about 3.2Hz.
The response starts to decrease just below
10MHz due to the parasitic capacitance, CP.
As noted, this has a somewhat arbitrary
value here, but unless there is a large capacitance in the circuit, it will not limit
the maximum frequency which can be
processed by the ADC. That is more
likely to be determined by the achievable sampling rate.
Using the DAC & ADC together
Last month, we finished with an
STM32CubeIDE project in which a codegenerated waveform was produced by the
DAC. The sample timing was controlled
by interrupts from a hardware timer.
This example will use the same approach for timing and DAC use. The timer
and DAC configuration are the same as
the final project from last month, while
the ADC is configured as described above
and shown in Fig.3.
The previously used DAC set and start
values are not needed in this project, but
a variable (Value) is required to transfer
the sample value from the ADC to the DAC
(Listing 1, line 62). This is initialised to
half the 12-bit range (corresponds with
zero signal input).
Variables are also declared for the
timer prescaler, period and frequency
(Listing 1, lines 64-66). These values are
used to reinitialise the timer to obtain
the required sampling period (Listing 1,
lines 137-144) in the Use Code 2 section,
as was discussed last month. The DAC
and timer are also started in this section
(Listing 1, lines 133 and 146), again as
described last month.
The DAC is updated by code that runs
when a timer interrupt occurs. To achieve
this, the code is placed in an interrupt
service routine (ISR) function named
HAL_TIM_PeriodElapsedCallback(),
which is added to the User Code 4 section near the end of the autogenerated
main.c file (Listing 2).
The code in the function must check
which timer caused the interrupt before
dealing with it. This is done by checking
the identity of the timer handle passed to
the function (Listing 2, line 833), which
in this case is Timer 6.
After confirming Timer 6, the code in
the ISR reads the ADC and passes this
value directly to the DAC using the Value
variable (Listing 2, lines 836 to 843). As
Simulations
Fig.5 shows the results of a transient simulation of the circuit in Fig.4 with a 1kHz,
3.2V peak-to-peak sinewave source signal
centred on 0V. The DC shift to 1.65V can
be observed on the InADC signal. This is
close to the maximum amplitude input.
Fig.6 shows the same signals with a
source amplitude of 5V peak-to-peak (V1
sine amplitude at 2.5V). This causes the
ADC input to be clipped by the protection diodes, limited at -0.3V and +3.6V.
34
Fig.7: the simulated frequency response of the ADC input circuit in Fig.4.
Practical Electronics | August | 2025
in previous examples, a GPIO pin is toggled when the DAC is updated to allow
us to observe the timing of the sampling
process.
To get an ADC reading directly under
program control, it is necessary to call
the HAL_ADC_Start function each time
a conversion is required (at the time the
input sample it to be taken). The function
only requires one parameter: the ADC
handle (hadc1 in this case).
The conversion process is not instantaneous; the result can only be obtained after
the conversion has finished. Calling the
function HAL_ADC_PollForConversion
will wait for the ADC to complete the
current conversion. The function has
two parameters: the handle and a timeout value in millisecond.
After the timeout period, the function will stop waiting for the ADC and
the code will continue running even if
the ADC has not responded. Using the
#defined value HAL_MAX_DELAY for the
timeout parameter effectively sets an infinite timeout period.
Once it returns from the function call
to HAL_ADC_PollForConversion,
the HAL_ADC_GetValue function can
be used to obtain the conversion result.
This function only takes the ADC handle
as a parameter.
The three functions mentioned above
are called in sequence in the timer interrupt callback function (ISR). They replace
the waveform generation code in the previous projects. The code to write to the
DAC and generate a pulse on the GPIO
output is the same as in DAC projects.
61
62
63
64
65
66
…
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/* USER CODE BEGIN PV */
uint32_t Value = 2048; // Current signal value
uint32_t Prescaler = 1; // Timer prescaler setting
float Period;
// Timer period in us
float Frequency = 48.0; // 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
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
HAL_TIM_Base_Start_IT(&htim6);
/* USER CODE END 2 */
Listing 1: variables and user initialisation code for the ADC to DAC transfer.
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == &htim6)
{
// Read the ADC to get the value to output to the DAC
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1,HAL_MAX_DELAY);
Value = HAL_ADC_GetValue(&hadc1);
// 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, Value);
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_RESET);
}
}
/* USER CODE END 4 */
Listing 2: reading values from the ADC and writing them to the DAC.
Program execution results
The DAC output from running the code
in Listings 1 & 2 was obtained using an
oscilloscope and is shown in Fig.8. The
sampling frequency was 48kHz, as per
Listing 1. A 3.2V peak-to-peak 2.5kHz
sinewave, with 0V DC offset, was applied
to the ADC input via the circuit shown
in Fig.4 (using R1, R2, R3, C1, D1 & D2).
Fig.8 shows the signal source and the
DAC output. There is no filter on the DAC
output, so we can see the stepped waveform. We can see that the input varies
around 0V, and the DAC output has a
1.65V offset. Fig.9 shows the DAC output
and timing pulse, for which the oscilloscope measured a frequency of 47.99kHz
– within the expected crystal precision.
Fig.8: the ADC input (blue) and DAC output (red) signals using the code in Listings 1 & 2.
Fig.9: how the timing pulses (blue) relate to the DAC output (red).
Practical Electronics | August | 2025
35
I-bus
I-Code
D-bus
D-Code
Flash interface
Cortex-M4 processor
with FPU
S-bus
SRAM1
Bus matrix
Bus Master
Bus Master
DMA1
SRAM2
FMC and QuadSPI
Ch.1
Ch.1
Ch.2
AHB2 peripherals
with DMA capability:
ADC1, ADC2, ADC3, AES,
HASH & DCMI
DMA
Ch.7
Ch.7
A HB 1
CRC
DMA2
Ch.1
Ch.2
TSC
DMA
Reset and clock control (RCC)
Ch.7
Bus Master
Fig.10: the DMA
and memory bus
structure within an
ARM Cortex M4
processor.
APB2 peripherals
with DMA capability:
DFSDM1, SAI1, SAI2, TIM1,
TIM8, TIM15, TIM16, TIM17,
USART1, SPI1, SDMMC
Bridge
APB1
1
APB1 peripherals
with DMA capability:
SWPMI1, LPUART1,
DAC_CH1, DAC_CH2, I2C1,
I2C2, I2C3, I2C4, USART2,
USART3, UART4, UART5,
SPI2, SPI3, TIM2, TIM3, TIM4,
TIM5, TIM6, TIM7
DMA requests
Experiments with increasing the sampling frequency showed a limitation of
this example. The maximum sampling
frequency is just over 60kHz, although
we know the converters can operate
faster than this.
The speed is limited by the individual software-controlled ADC conversions
– the time taken for the processor to interact with the ADC and DAC (initiating
ADC conversion, polling for conversion
and reading ADC, writing DAC).
Going faster with DMA
In the previous example, the code
initiates ADC conversion, waits for it
to complete, reads the ADC value into
memory and then writes it to the DAC.
All of this takes significant time, which
could otherwise be spent doing things
that make better use of the processor’s
capabilities – such as applying signal
processing functions to the data.
Using DMA, it is possible to eliminate
the need for the processor to perform
ADC-to-memory and memory-to-DAC
data transfers. DMA operates in parallel with the processor’s activities, using
separate hardware. The DMA hardware
controls the read/write processes for the
data converters and memory, leaving the
processor free to do other things.
With DMA, the processor has more time
to perform signal processing operations,
and the sampling rates can be faster.
36
Bridge
APB2
2
In general, the movement of data to and
from memory with DMA can take place
as a one-off process on a single batch of
data (normal mode) or as a continuous
process (circular mode). Continuous circular operation is generally required for
real-time DSP applications.
In this mode, the DMA system constantly
reads/writes from/to a block of memory
(often referred to as a buffer), with each
sample read from, or written to, the next
address in sequence. When the write or
read address reaches the end of the buffer,
it returns to the start – it constantly circles
through the set of memory locations in the
buffer, hence ‘circular mode’.
Processor bus architecture
Processors use buses to interact with
their memory and peripheral devices,
such as ADCs and DACs. A bus is a
collection of signals arranged so that
multiple subsystems can communicate
with each other.
Early and very basic processors may
use the well-known shared bus based on
tristate buffers (buffers able to drive a line
high, low or not drive it at all). The processor sets up an address on the bus’s address
wires and asserts a read or write signal. The
Data to or from the processor is placed on
the bus’s data wires via tristate buffers.
Tristate outputs allow multiple devices to put data on the same wires
– the design has to ensure only one
data source is outputting to the data
lines at any time.
Tristate buses are suited for basic systems where the processor, memory and
peripherals are separate chips or boards,
as they are efficient in terms of wiring and
connectors. The buses on modern systemon-chip (SoC) devices (such as advanced
microcontrollers) are significantly more
complex and use a different approaches.
Fig.10 shows the microcontroller’s
bus and DMA architecture, an annotated version of the DMA block diagram in
ST’s RM0351 Reference manual (https://
pemag.au/link/ac6r), which covers various SMT32L4 processors.
In a basic system, the bus is controlled
solely by the processor. With more complex systems, more than one subsystem
can control the bus – these are known
as bus masters. In Fig.10, we see three
blocks labelled as bus masters: the processor and two DMA controllers. The
ARM M4 processor contains three bus
masters, for instruction (I), data (D) and
system (S) (for peripheral) operations.
The Bus Matrix is a large routing switch
that allows connections to be made from
the bus masters to the other systems
(memory and peripherals). This is a more
effective and lower-power approach than
a tristate bus – the complexity of “wiring”
involved is much less of an issue on-chip
than it would be trying to do a similar
thing at a board or backplane level.
When there are multiple bus masters,
there is always a possibility that more
than one master will want to access a
given resource at the same time – the
Bus Matrix subsystem controls which
master has access at any time, using a
process called bus arbitration.
DMA operations are managed by DMA
controllers. These are bus masters that
operate independently of the processor
(although what they do is configured by
the processor). They can move data via the
bus system separately from the processor
so, for example, they can transfer values
from the ADC directly into memory, or
data from memory directly from the DAC.
DMA transfers can be triggered by events
in the peripherals, such a ADC conversion completion. For ADCs and DACs,
the timing of the conversions can also
be controlled by timers, as we previously
did. However, with DMA, this does not
require timer interrupts to the processor.
Instead, the timers directly control the
sampling (eg, trigger an ADC conversion in hardware), and hence when the
DMA transfers occur. The DMA controllers can trigger interrupts; for example,
when they reach the end of a block of
memory they are accessing, so the processor can handle it or refill the buffer
with more data.
Fig.10 shows that there are different
Practical Electronics | August | 2025
types of bus connecting the peripherals:
the AHB (Advanced High-performance
Bus) and APB (Advanced Peripheral Bus).
AHB is for high-speed transfer, while
APB is for lower-bandwidth, lower-power
peripherals. A bridge is a subsystem for
connecting different buses (buses with
different speeds, protocols, number of
bits etc).
Using DMA
To use DMA with the ADC and DAC
in an STM32CubeIDE project, we need
to apply the appropriate configuration
settings for the ADC and DAC. The DMA
unit has to be enabled, and settings need
to be made to configure DMA operation as
required for the project. HAL library functions are used to start the DMA process.
Code to handle data coming from the
ADC via DMA, or provide values for the
DAC via DMA, is placed in HAL library
callback functions (similar to those used
for timer interrupts).
On the microcontroller chip used here,
DMA transfers occur in either word (32bit), or half-word (16-bit) increments. The
code here uses 32-bit integers (uint32_t
data type), to be compatible with the HAL
ADC and DAC functions. Therefore, the
data width used by the DMA needs to be
configured to use words to match this.
The ADC and DAC have resolutions of
12 bits, but this data is processed in code
using 32-bit values. These values would
fit in half-words, but that would require
additional, unnecessary processing steps.
Continuous circular DMA operation
is set up via the ADC, DAC and Timer
settings in the Configuration Tool. The
buffer memory size and location are set
up in code. We will use a new project
and configure it for DMA.
Timer, ADC & DAC settings
Select Timer 6 (TIM6) in the Configuration Tool and check “Activated” in the
Mode section of the Parameter settings,
as before. Different timer settings are
required in this project to directly trigger the ADC and DAC conversions from
the timer. In the Parameter Settings tab
in the configuration section, set “Trigger Event Selection” to “Update Event”
(see Fig.11).
This will enable the timer’s trigger
output signal, which will be used to activate the ADC and DAC conversions,
and hence the DMA transfers of their
data. The timer interrupt is not enabled
in this example.
The ADC is enabled in Single-ended
mode, as in Fig.3. The DMA is configured
by clicking the Add button in the DMA
settings tab in the ADC configuration (see
Fig.12). This will display a drop-down
menu under the DMA Request column.
Select ADC1 from this.
Practical Electronics | August | 2025
Fig.11: the timer settings for our DMA project.
Fig.12: configuring the ADC1 DMA settings.
In the DMA Request Settings, set Mode
to Circular and both “Data Width” values
to Word, as discussed before. Also click on
Low in the Priority column for ADC1 and
change it to High in the drop-down menu.
Click on the Parameter Settings tab in
the ADC configuration. Enable “DMA
Continuous Requests”, then Set “External Trigger Conversion Source” to “Timer
6 Trigger Out event”.
The DAC is enabled by setting “OUT2
connected to” to “only to external pin”
mode, as previously. In the parameter
settings tab of the DAC Configuration,
Trigger is set to “Timer 6 Trigger Out
event” so the timer triggers the DAC.
The DAC DMA settings are very similar to the ADC – select DAC-CH2 under
DMA Request after adding the request
and set circular mode, then the word
32
33
34
35
36
…
65
66
67
68
69
70
71
data widths and high priority, as with
the ADC.
DMA code
The ADC and DAC both require an
area of memory to store the data that is
being read (ADC buffer) and written (DAC
buffer). In coding terms, these need to be
declared as arrays of 32-bit integers in
the User PV section (Listing 3, lines 6667). An ADC buffer and a DAC buffer of
the same size are required.
We need to use the buffer size and half
size values throughout the code, so these
values are #defined in the User PD section
to make them easy to change (Listing 3,
lines 34-35). This example uses 100-word
buffers. The prescaler, period and frequency values are defined and used as in the
previous examples (Listing 3, lines 68-70).
/* Private define ------------------------------------*/
/* USER CODE BEGIN PD */
#define BUF_SIZE 100
#define HALF_BUF_SIZE 50
/* USER CODE END PD */
/* USER CODE BEGIN PV */
uint32_t ADC_buffer[BUF_SIZE];
uint32_t DAC_buffer[BUF_SIZE];
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 */
Listing 3: #defines and definitions for the DMA project.
37
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/* USER CODE BEGIN 2 */
// Start ADC and DAC with DMA and timer to trigger ADC and DAC
HAL_ADC_Start_DMA(&hadc1, ADC_buffer, BUF_SIZE);
HAL_DAC_Start_DMA(&hdac1,DAC_CHANNEL_2,DAC_buffer,BUF_SIZE,DAC_ALIGN_12B_R);
// Calculate TIM6 counter period (ARR) from required 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
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
HAL_TIM_Base_Start(&htim6);
/* USER CODE END 2 */
Listing 4: the initialisation code for the DMA project.
The DMA controller operations to read
data from the ADC and write to the DAC
need to be started. This is achieved using
two functions; HAL_ADC_Start_DMA
and HAL_DAC_Start_DMA, called during
initialisation in the User Code 2 section
(Listing 4, lines 141-142).
The HAL_ADC_Start_DMA function requires three parameters: the ADC handle,
the buffer array and the buffer size. The
HAL_DAC_Start_DMA function requires
five parameters: the DAC handle and
channel number, the buffer and buffer
size and a data alignment specifier.
The data alignment specifier is the same
as that discussed in the first DAC example
and the same value, DAC_ALIGN_12B_R,
will be used here.
Also, the same code as before will be
used to configure and start the timer
(TIM6) after configuring the sampling
rate (Listing 4, lines 144-154).
With DMA running, the processor
can read the ADC values directly from
memory (from the ADC buffer), but in the
code, we need to know which values are
valid when, because the buffer is being
constantly overwritten with new data by
the DMA controller.
This is handled by two callback functions,
HAL_ADC_ConvHalfCpltCallback
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
/* USER CODE BEGIN 4 */
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)
{
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_SET);
for (int i= 0; i < HALF_BUF_SIZE; i++)
{
DAC_buffer[i] = ADC_buffer[i];
}
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_RESET);
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_SET);
for (int i= HALF_BUF_SIZE; i < BUF_SIZE; i++)
{
DAC_buffer[i] = ADC_buffer[i];
}
HAL_GPIO_WritePin(GPIOB, ARD_D8_Pin, GPIO_PIN_RESET);
}
/* USER CODE END 4 */
Listing 5: the DMA callback functions.
38
(the buffer half-full callback) as well as
HAL_ADC_ConvCpltCallback (the
buffer full callback). A separate callback
is not required to write to the DAC – we
can write to the DAC buffer during the
ADC callbacks as long as everything is
correctly coordinated.
Like the timer interrupt code, the DMA
buffer callback functions are placed in
the User Code 4 section (see Listing 5).
In both functions in this example, all the
data from the relevant half of the ADC
buffer is transferred to the DAC buffer
(copying the content of the ADC buffer
array to the corresponding location in
the DAC buffer array).
This will replicate the ADC input
waveform on the DAC output. The code
to achieve this is a simply a for-loop iterating over all the array locations in
the relevant half of the buffer and performing the copy operation (Listing 5,
lines 860-863 and 870-873). No code
is required to directly read the ADC or
write to the DAC as this is done by the
DMA hardware.
The DMA callbacks work similarly to
the timer interrupt callbacks in previous examples. The function names are
predefined, and users write their own
code in functions with these names to
implement the operations required when
the relevant events occur.
As the name implies, the ADC buffer
half-full callback runs when the first half
of the ADC buffer has been filled with
new data. For a while after this time,
the data will not change in the first half
of the buffer (because the second half is
being written to). During this time, data
from the first half of the buffer can safely
be read by the processor.
The ADC and DAC buffers are running
in synchronicity – the buffers are set up
to be the same size, and the configuration setup detailed above causes the ADC
and DAC conversions to be triggered at
the same time (by Timer 6).
Thus, during the ADC half-full callback, data can be written to the first
half of the DAC buffer by the processor.
It is safe to do because, at this time, the
DMA system is writing to the DAC from
the second half of the DAC buffer. The
buffer-full callback is symmetrical with
the half-full callback, so it is used to process data in the second half of the buffer.
The callback functions are also used
to set the ARD_D8_Pin digital output
before starting each buffer transfer and
reset it afterwards. This will allow the
timing of the code to be monitored, as
was done in previous projects (Listing 5,
lines 859, 864, 869 & 874).
Program execution results
Fig.13 shows the ADC and DAC signals
from the oscilloscope for the DMA project with a similar setup to that used for
Fig.8, except the sampling frequency is
now 96kHz. The shorter step lengths can
be observed on the DAC waveform. Another difference is the time offset of the
waveforms – the samples are processed
after the buffer has been half filled, which
results in a delay from input to output.
Fig.14 shows the DAC output and the
timing pulses from the oscilloscope for
the DMA project. Unlike in the previous
example, the timing pulses do not directly
indicate the sampling. The pulses show
when the processor is running code to
transfer data between the ADC and DAC
in the callback functions.
When the pulses are high (logic 1),
transfer is taking place. At other times,
the processor is effectively idle in this
example – it is just looping in the main
while loop, not doing anything specific.
The behaviour of the timing pulses is illustrated in more detail in Fig.15.
There are two callback functions, so
the timing pulses represent alternate
callbacks – half full and full buffer. The
length of the timing pulse is the time
taken to run one of the callback functions
(to process half the buffer). This is related to the processing speed, buffer size
and the amount of code in the callback.
Practical Electronics | August | 2025
Full callback
Digital waveform
indicating when
callback is running
Half-full callback
Full callback
...
Half-full callback
Voltage
Time to run
one callback
... Idle Idle Idle Idle
...
Idle ...
Time
Time for full buffer to
be written by DMA
Fig.15: timing pulses related to callback
function execution and DMA transfers.
Full callback
Half-full callback
Full callback
Half-full callback
Time
to run
Digital
waveform
Fig.13: the
ADC
input and
DAC
output
one callback
indicating when
waveforms
for
the
code
in
Listings
3-5.
callback is running
Voltage
Fig.14: the timing pulses and DAC output
...
...
for the code in Listings 3-5.
The time between alternate positive
edges is the time taken for a full buffer to
be written/read by the DMA system. This
is related to the buffer size and sample
rate. The total time taken to run two
callback functions must be less than the
time taken to write/read the full buffer.
As previously discussed, the DAC settling time of 3µs implies a maximum
sampling rate of about 330kHz (the ADC
is faster). Experiments show that the DMA
processing code can run at sampling rates
of 1MHz or more – compare this with the
60kHz limit for the first example.
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)
At excess
rates, the DAC
... Idlesampling
Idle Idle Idle Idle ...
output looks OK, with a smooth slow
Time
input waveform, such as a relatively low
Time for fullHowever,
buffer to there is refrequency sinewave.
be written by DMA
duced output amplitude. As this is faster
than the DAC can operate correctly, it
would be a poor choice in a real design.
However, this result serves to illustrate
the effectiveness of the DMA in speeding up signal processing.
PE
|