You are on page 1of 46

See discussions, stats, and author profiles for this publication at: https://www.researchgate.

net/publication/354022135

STM32 DAC TUTORIAL with application to system identification

Technical Report · August 2021


DOI: 10.13140/RG.2.2.10436.96647/5

CITATIONS READS
0 1,001

3 authors:

Mohsen Fallah Seyyed Alireza Davodi Navokh


Scania Ferdowsi University Of Mashhad
22 PUBLICATIONS   53 CITATIONS    3 PUBLICATIONS   0 CITATIONS   

SEE PROFILE SEE PROFILE

Mehran Mozaffari-Jovein
Ferdowsi University Of Mashhad
2 PUBLICATIONS   0 CITATIONS   

SEE PROFILE

Some of the authors of this publication are also working on these related projects:

Adaptive Active Control of Boring Bar Chatter View project

Simulation and control of voice coil actuator (VCA) View project

All content following this page was uploaded by Mohsen Fallah on 23 August 2021.

The user has requested enhancement of the downloaded file.


STM32 DAC TUTORIAL
| with application to system identification |

| Mohsen Fallah , Alireza Davodi , Mehran Mozaffari |

| Record of revision: V1.0 | Last modified on: 2021-08-20 |


Fallah M, Davodi A, Mozaffari M. STM32 DAC TUTORIAL: with application to system identification.
Mashhad: Ferdowsi University of Mashhad, CAD/CAM Laboratory; 2021. 45 p. Report No.: 1.
DOI: 10.13140/RG.2.2.10436.96647/5

2
1. Introduction

This brief tutorial is specifically written to assist the beginner users of STM32 boards in
order to learn the fundamentals of how to activate and work with the (Digital-to-Analog
Converter) DAC module. It is intended to show the different methods of using the DAC
module on the STM32 boards with application to signal generation for system
identification purposes. In this tutorial you will learn how to:

Download the STM32 software packages


Compile ARM CMSIS 4.5.0 DSP library in STM32CubeIDE
Configure and utilize TIM / DAC / GPIO modules of the microcontroller
Generate different wave forms for system identification applications
Program NUCLEO-F746ZG in STM32CubeIDE using C programming language
Excite the dynamics of a mechatronics system using the developed algorithms

All the texts highlighted in blue will direct you to the appropriate content that assist you to
enrich your understanding while reading this tutorial. In order to improve the content of
this tutorial, you can easily share your suggestions, ideas and improved codes or algorithms
with us by commenting on this document’s webpage or easily contacting us via email by
sending your queries to: mohsen.fallah@ymail.com.

3
2. STM32 Software Packages

The ST has introduced the STM32Cube ecosystem that is a combination of software tools
and embedded software libraries. It includes a wide range of PC software tools addressing
all the needs of a complete project. Inside the STM32Cube ecosystem, you have access to
the following software packages:

Table 1 Basic software packages inside STM32Cube ecosystem


This easy-to-use graphical user interface software generates
STM32CubeMX initialization C code for Cortex-M cores and generates the Linux
device tree source for Cortex-A cores.
It is an Integrated Development Environment. Based on open-
source solutions like Eclipse or the GNU C/C++ toolchain, this
IDE includes compilation reporting features and advanced debug
STM32CubeIDE
features. It also integrate additional features present in other tools
from the ecosystem, such as the HW and SW initialization and
code generation from STM32CubeMX.
It provides an easy-to-use and efficient environment for reading,
writing and verifying devices and external memories via a wide
STM32CubeProgrammer
variety of available communication media (JTAG, SWD, UART,
USB DFU, I2C, SPI, CAN).
Powerful monitoring tools that help developers fine-tune the
STM32CubeMonitor
behavior and performance of their applications in real-time.

In this tutorial, we utilized the STM32CubeIDE in order to program, compile and debug
the code on the STM32 board. With STM32CubeIDE, there is no need to download another
development environment such as MDK-Arm Keil. For each STM32 series, STM32Cube
MCU and MPU packages offer all the required embedded software bricks to operate the
available set of STM32 peripherals. Here, we have used the STM32 Nucleo-144
development board with STM32F746ZG MCU. Therefore, It is essential to download the
STM32Cube MCU Package for STM32F7 series.

4
The template of the code is generated in C language by the STM32CubeMX that is
included in the STM32CubeIDE software package. All selected peripheral devices of the
STM32 board are initialized inside this template code. The activation or deactivation of
these peripherals are done by using appropriate commands from STM32F7 HAL Drivers.

Apart from the STM32CubeMonitor, one can easily monitor run-time variables via the
STM Studio that offers a range of useful monitoring and visualization tools for STM32
microcontrollers. It is also recommended to install the STSW Link. In addition, for those
who are interested to deploy their application models in MATLAB and Simulink to STM32
MCUs, they can download the STM32 embedded target for MATLAB and Simulink.

Fig. 1 The STM32 Nucleo-144 board (NUCLEO-F746ZG)

3. STM32 Nucleo-144 development board with STM32F746ZG MCU

In this tutorial, we used one of the high-performance development boards from the STM32
family as shown in Fig. 1. The STM32 Nucleo-144 board (NUCLEO-F746ZG) provides

5
an affordable and flexible way for users to try out new concepts and build prototypes with
STM32 microcontrollers, choosing from the various combinations of performance, power
consumption and features.

4. Compile ARM CMSIS 4.5.0 DSP library in STM32CubeIDE

In the system identification and real-time control applications, it is always required to


process the digital signals before sending them to the DAC module or after reading them
from the ADC module. Threfore, the ARM CMSIS DSP library is always an indispensable
part of the C program that is being developed inside the STM32CubeIDE. An open-source
version of the ARM CMSIS library is always available on the GitHub. Here, we have
downloaded and used the mature version of the ARM CMSIS library 4.5.0.

After installing the STM32CubeIDE, launch the software from the link created on your
desktop. The software asks you to select a directory as the workspace (*), where all the
project files are generated. After a few seconds, the STM32CubeIDE Home window pops
up. In this window, click on Start new STM32 Project (*). At this step the software tries to
connect to the internet in order to download the latest files from the ST website. You may
skip this step by disabling your internet connection for a few seconds.

Afterwards, another window opens that ask you to select the STM32 target. In the Part
Number window type the name of the specific MCU for the STM32 board. Here, just type
STM32F746ZG so that the software shows all the active products (*). Afterwards, on the
MCU list select the STM32F746ZGTx and click on the Next button (*).

The next window sets up the basic features of the STM32 project (*) such as the project
name, targeted language (C), targeted binary type (Executable), targeted project type
(STM32Cube). After defining the project name, click on the Next button. In the subsequent
window (*), you are able to set the firmware package name and version as well as the code
generator option (Copy only the nessecary library files) and click on the Finish button.
Finally, the STM32CubeIDE project window will be launched after a few seconds (*).

6
In order to access the avialable software packages, click on Help > Manage Embedded
Software Packages (*). You can find the available STM32Cube MCU Packages for the
STM32F7 family in the opened window (*). If no package is available, you can either
download the latest package by clicking on the From Url button or install the previously
downloaded package by clicking on the From local button.

After downloading the ARM CMSIS library 4.5.0, copy the arm_math.h file from the
library path … \ CMSIS \ Include to the project path … \ Core \ Inc (*). Then, create a
folder named as libs in the main path of project directory (*). Afterwards, copy the file
libarm_cortexM7l_math from the library path … \ CMSIS \ Lib \ GCC (*) to the libs folder
in the project path (*).

In the Project Explorer window (*), right click on the project title at the top and then click
on the Properties tab or simply press Alt + Enter on the keyboard. Click on C / C++ Build
> Setting in the opened window. On the Tool Settings tab (*), find and click on the MCU
GCC Linker > Libraries. In Libraries (-l) section (*), click on the Add button and write the
expression arm_cortexM7l_math in the opened window. Additionally, in Library search
path (-L) section (*), click on the Add button and define the path to the libs folder in the
project path.

On the Tool Settings tab, click on MCU Setting. Open the Floating-point ABI drop menu
and select Mix HW / SW implementation (*). Finally, click on the Apply and Close button.
In the Project Explorer window, click on Core > Src > main.c. This is the main file for the
C program that will generally include all the user functions in any project. For initialization
of the STM32F7 board peripherals, some standard functions will be added to this file as
soon as we select and configure the required modules for our application via
STM32CubeMX. One remaining step for the activation of the DSP library is to add two
commands to the main.c file as shown below (*):

7
/* Private includes -----------------------------------------------------*/
/* USER CODE BEGIN Includes */
#define ARM_MATH_CM7
#include "arm_math.h"
/* USER CODE END Includes */

Please keep in mind that the user code should be written in-between specific lines inside
the main.c file. For example, all user include files should be written between the /* USER
CODE BEGIN Includes */ and /* USER CODE END Includes */. The final step is to click on the

Project > Build All so that all the project files are compiled by the IDE (*). All files will
be built without any errors or warnings related to the addition of the DSP library to project
directory.

5. Generate different wave forms for system identification

The purpose of this tutorial is to create standard signals (harmonic, triangular and square
wave forms) with desired frequency bandwidth for external excitation of mechatronics
systems. The standard wave forms can have adjustable amplitude, mean and frequency
content (constant frequency or linearly varying frequency in the form of a chirp signal).
Two versions of the signal generator code was provided in this tutorial in order to show the
two different modes of interaction with the DAC module. In one version of the code, the
resolution of the generated wave is independent of the frequency (i.e. the number of data
points per period of the signal is fixed), while the time step of wave generation is constant
in the other version of the code. That is, there are a lower number of data points on each
period of the signal as we move from lower frequencies to higher ones. However, as a rule
of thumb, the input parameters of the code should be adjusted so that the highest frequency
of interest is generated by at least 20 data points across one period of the wave.

The mathematical equations for the standard signals (cosine, triangular, square wave
forms) are given in this section. The defining parameters of these signal are presented in
Table 2. We shall start with the harmonic cosine chirp signal that has linearly varying
frequency. The cosine chirp is simply defined by (*):
8
𝑆ℎ𝑐 (𝑡) = 𝐴 𝑐𝑜𝑠(2𝜋𝑓(𝑡)𝑡 + 𝜑0 ) + 𝐴0
𝛼 𝐹𝑓 − 𝐹𝑠 (1)
𝑓(𝑡) = 𝐹𝑠 + 𝑡. 𝛼=
2 𝑇𝑐

The square chirp signal is similarly computed as follows (*):

𝑆𝑠𝑐 (𝑡) = 𝐴 𝑠𝑔𝑛(𝑐𝑜𝑠(2𝜋𝑓(𝑡)𝑡 + 𝜑0 )) + 𝐴0 (2)

Where 𝑠𝑔𝑛( ) is the standard sign function (*). The triangular chirp signal can be expressed
by either of the following formulas (*):

2𝐴
𝑆𝑡𝑐 (𝑡) = − 𝑐𝑜𝑠 −1 (𝑐𝑜𝑠(2𝜋𝑓(𝑡)𝑡 + 𝜑0 )) + (𝐴 + 𝐴0 )
𝜋
(3)
2𝐴 𝜋
𝑆𝑡𝑐 (𝑡) = 𝑠𝑖𝑛−1 (𝑠𝑖𝑛(2𝜋𝑓(𝑡)𝑡 + 𝜑0 + )) + 𝐴0
𝜋 2

The linearly varying frequency term, 𝑓 (𝑡 ), for the sqaure and triangular signals is
calculated similar to that of harmonic chirp signal. The benchmark illustration of these
chirp signals are shown in Fig. 2. Since the DAC signal can not accept negative values and
the maximum output voltage of the STM board is 3.3 V, these signals are ploted with a
maximum chirp amplitude of 1.65 V and a mean value of 1.65 V. It is obvious that the
frequency content of the signal varies linearly with time.

Table 2 Parameters of chirp signals

Definition Param. Unit Definition Param. Unit

Chirp amplitude 𝐴 [𝑉] Chirp final frequency 𝐹𝑓 [𝐻𝑧]

Chirp mean value 𝐴0 [𝑉] Chirp period 𝑇𝑐 [𝑠]

Initial phase 𝜑0 [𝑟𝑎𝑑] Temporal variable 𝑡 [𝑠]

Chirp start frequency 𝐹𝑠 [𝐻𝑧] Chirp sweep rate 𝛼 [𝐻𝑧⁄𝑠]

9
Fig. 2 Different wave forms with linearly varying frequency content

6. Configure TIM / DAC / GPIO modules

In this section, we configure the basic modules required for the system identification
application. The essential parameters settings for the activation of TIM / DAC / GPIO
modules on the STM32F7 device are defined in details. Since two different versions of the
signal generator code are provided in this tutorial, you should pay attention to the details
of this section. Wherever we refer to Code 1 (Section 7.1), it means that we are going to
perform the DAC operation in single mode inside the timer global interrupt callback
function. However, when we refer to Code 2 (Section 7.2), it means that we are going to
perform the DAC operation in continuous mode using Direct Memory Access (DMA)
inside the DAC channel 1 callback function. These two operation modes (single and
continuous conversion modes) are different from each other in terms of parameters settings
as well as the HAL functions that we have used in the codes. The maximum speed of
communication with the DAC module also differs, but the two version of the signal
generator code were written so that they can produce similar results.

In order to activate the STM32 board peripherals, you should click on the file with .ioc
extention from the Project Explorer window so that the MCU of STM32 board shows up
on the right hand side (*). From now on, we are using the STM32CubeMX software, which
is integrated into the STM32CubeIDE software, for configuring the parameters of board
modules including TIM, DAC and GPIO.

10
6.1. RCC Parameters

In order to activate the High Speed Clock (HSE), we should select the System Core > RCC
from the Pinout and Configuration tab (*). In the RCC Mode window, set the HSE to
Crystal / Ceramic Resonator (*). We do not make any other changes to the RCC
Configuration window and leave all the parameters to their default values. The HSE Clock
is very stable, and it is recommended to activate it for system identification and real-time
control applications.

6.2. Clock Configuration

Then, open the Clock Configuration Tab to adjust the clock parameters via the STM32
clock tree. In this tab, you should make four changes to the clock tree. Firstly, set the Input
frequency for the HSE to 8 MHz. Secondly, select HSE from PLL Source Mux. Thirdly,
select the PLLCLK from the System Clock Mux. Finally, set the HCLK frequency to the
maximum value for the STM32F7 board, here, 216 MHz. Click on the Resolve Clock
Issues button on the top of this tab so that the software can find an admissible solution (*).
As a result of these modifications to the STM32 clock tree, the following frequencies are
adjusted for the APB1 and APB2 buses, as given in Table 3.

Table 3 Maximum frequency for APB1 / APB2 clocks

Buses Clocks Value Unit

APB1 peripheral clocks 54 [𝑀𝐻𝑧]

APB1 timer clocks 108 [𝑀𝐻𝑧]

APB2 peripheral clocks 108 [𝑀𝐻𝑧]

APB2 timer clocks 216 [𝑀𝐻𝑧]

11
The DAC module uses the APB1 peripheral clock, while the ADC module uses the APB2
peripheral clock. On the other hand, the TIM2 and TIM6, which are used in this tutorial,
utilize the APB1 timer clocks.

6.3. TIM 2 / TIM6 Parameters

In this project, we have activated two different timers, TIM2 and TIM6, for the DAC
operation, but we have used one of these timer during the execution of the code. Please
keep in mind that the parameter setting of the timers are different for Code 1 and Code 2,
as mentioned below. In order to activate the TIM2 module, we should firstly select the
Timers > TIM2 from the Pinout and Configuration tab (*). TIM2 is a general purpose timer.
In the TIM2 Mode window, set the Clock Source to Internal Clock (*). In the Parameter
Setting tab under Configuration window, set the Counter Settings > Prescaler (16 bits) to
1-1 and set the Counter Settings > Counter Period (32 bits) to 108-1. The Counter Settings
> auto-reload preload should be set to Enable (*). Then, switch to the NVIC Settings tab
under Configuration window and Enable the TIM2 global interrupt (*). These timer
settings are sufficient if you would like to execute Code 1. However, if you decide to
execute Code 2, please do the following configuration as well. Return to the Parameter
Settings tab under Configuration window. The Trigger Output (TRGO) Parameters >
Trigger Event Selection TRGO should be set to Update Event (*).

Now we should activate TIM6 module as well. To do so, we should firstly choose the
Timers > TIM6 from the Pinout and Configuration tab (*). TIM6 is a basic timer that is
specifically recommended for DAC operation. In fact, TIM6 is internally connected to the
DAC module and is able to drive it through its trigger outputs. In the TIM6 Mode window,
set the timer status to Activated (*). In the Parameter Setting tab under Configuration
window, set the Counter Settings > Prescaler (16 bits) to 1-1 and set the Counter Settings
> Counter Period (16 bits) to 108-1. The Counter Settings > auto-reload preload should
be set to Enable (*). Then, switch to the NVIC Settings tab under Configuration window
and Enable the TIM6 global interrupt (*). These timer settings are sufficient if you would

12
like to execute Code 1. However, if you decide to execute Code 2, please do the following
configuration as well. Return to the Parameter Settings tab under Configuration window.
The Trigger Output (TRGO) Parameters > Trigger Event Selection TRGO should be set
to Update Event (*).

6.4. DAC Parameters

In this section, we have activated the DAC module for Code 1 and Code 2 and have set its
parameters accordingly. Firstly, we should select the Analog > DAC from the Pinout and
Configuration tab (*). The NUCLEO board has two separate channels for its DAC module.
In the DAC Mode window, select the OUT1 Configuration (*). This DAC channel
corresponds to pin PA4 on the MCU of STM32 board. In the Parameter Setting tab under
Configuration window, there is no need to make any other changes if you would like to
execute Code 1. The DAC Out1 Settings > Output Buffer is set by default to Enable.
However, if you are tempted to execute Code 2, set the following configurations as well.
The DAC Out1 Settings > Trigger should be set to Timer 2 Trigger Out event (*). In
addition, switch to the DMA Settings tab under Configuration window and click on the
Add button. Then, select DAC1 from the drop menu (*). On the DMA Request Settings set
the Mode to Circular. The Data Width is set to Half Word as the STM32 board DAC
module has a maximum resolution of 12-bits (*).

6.5. GPIO Parameters

In this section, we have activated three GPIO pins that are connected to the on-board LEDs
of the STM32 NUCLEO board. Move the pointer to the PB0 pin of the MCU and press the
left button of your mouse. This pin is located on the bottom edge of the MCU. Then select
GPIO_Output from the appeared drop menu (*). This will activate the green LED on the
board. Similarly, move the pointer to the PB7 pin located on the right edge of the MCU
and repeat the same steps to activate the blue LED on the board (*). Finally, move to the
top edge of the MCU and find the PB14 corresponding to the red LED on the board. Repeat
the same steps to activate this LED as well (*).
13
6.6. Generate Code

All the parameters settings are now adjusted correctly. Please note that we introduced some
extra settings for Code 2 in the TIM and DAC modules. In fact, the settings for Code 1
was less than the ones for Code 2. In the former, the DAC operation is performed in single
mode, yet in the latter the DAC operation is done in DMA mode which requires more
settings. In order to generate the standard code that adds the initialization functions for the
activated modules and pins to the main.c file, click on Project > Generate Code from the
menu bar (*). In order to create a report about the project, you can simply click on Project
> Generate Report (*). A report file with .pdf extension is added to the project directory
that includes a description of all the settings you have made for the activation of peripherals
and timers in the STM32CubeMX environment.

7. Program NUCLEO-F746ZG in STM32CubeIDE using C language

If you take a close look to the main.c file, you will understand that the declaration of
initialization functions for STM32 modules are added before the int main(void) function
(*), while their definitions are provided after the int main(void) function (*). These
functions are also called inside the int main(void) function (*) for initiation of the TIM /
DAC / GPIO modules on the board. We have adopted the same approach for developing
our C code, as you will notice in the rest of this section.

The initialization functions for STM32 modules are briefly described in this paragraph.
The clock configuration is done at the beginning of the user code inside the
void SystemClock_Config(void) function. static void MX_GPIO_Init(void) is the

function for configuration of the activated GPIO pins. static void MX_DAC_Init is the
function for initialization of DAC module as well as configuration of DAC channel 1. The
other functions static void MX_TIM2_Init(void) and static void MX_TIM6_Init(void)
adjust the parameters of TIM2 and TIM6 modules and initialize these timers. You may
make slight changes to the C codes written inside these initialization functions during the
development of your program if you are familiar with the STM32F7 HAL Drivers. But,
14
generally, if you would like to make changes to these functions without getting involved
with the C code syntaxes, one straightforward solution is to click on the file with .ioc
extention from the Project Explorer window and make modifications to the activated
modules and pins of the MCU via STM32CubeMX environment. Then, you can regenerate
the code as we did in Section 6. In order to keep the written user codes unchanged, you
should write them between specific lines defined by USER CODE BEGIN and USER CODE END
expressions in the main.c file, so that whenever the project code is regenrated all user codes
will be maintained unchanged (without being deleted). The rest of this section will provide
a breif description on the two different versions of the signal generator code developed for
this DAC tutorial.

7.1 Code 1 (DAC with timer global interrupt feature)

In this code, we have activated the DAC module in single mode. The DAC value is
continuously updated inside the timer callback function by using the timer global interrupt
feature. The first section of the code defines all the variables that describe the chirp signal
parameters as well as TIM and DAC parameters. These variables are defined globally and
therfore can be called by all functions.

/* USER CODE BEGIN PV */


// CHIRP SIGNAL PARAMETERS
enum {COS,TRI,SQR}; // Wave Type [COS = Harmonic , TRI = Triangular , SQR = Square]
uint8_t waveID = 0; // Wave Form Identifier [0 = COS , 1 = TRI , 2 = SQR]
uint8_t Nchrip = 0; // Number of Completed Chirps [-]
uint8_t Nrepeat = 50; // Number of Repeats [-]
uint8_t chvar; // Check Variable
double A = 1.65; // Dynamic Amplitude of Signal [V]
double A0 = 1.65; // Vertical Offset Value [V]
double Phi0 = 0.0; // Horizontal Offset Value [rad]
double Nc = 20; // Number of Samples per Period of Wave with Maximum Frequency [Ff]
double Fs = 10.0; // Chirp Start Frequency [Hz]
double Ff = 1000.0; // Chirp End Frequency [Hz]
double Tc = 2.5; // Chirp Half Period [s]

15
double Tcmax; // Max Allowable Chirp Half Period [s]
double Alfa = 0; // Frequency Sweep Rate [Hz/s]
double Cf = 0; // Maximum Allowable Frequency [Hz]
double St = 0; // Analog Wave Values [0 - 3.3 [V]]
double dt = 0; // Time Step [s]
double t = 0; // Time Variable [s]
double Ft = 0; // Wave Frequency Variable [Hz]
// TIM PARAMETERS
enum {TIMER2,TIMER6}; // Timer Type [TIMER2 = htim2 , TIMER6 = htim6]
uint8_t TIMID = 1; // Timer Identifier [0 = TIMER2 , 1 = TIMER6]
uint32_t MyClkFreq = 108000000; // Fixed APB1 Clock Frequency [Hz]
uint32_t MyPrescaler = 4-1; // Minimum Prescaler Value [cycles]
uint32_t MyPeriod = 108-1; // Fixed Period Value [cycles]
uint32_t TIMcount = 0; // Timer Counter
uint32_t TRESET = 0; // Timer Reset Index
// DAC PARAMETERS
#define data_size 100000 // Maximum Buffer Size for DAC Array [samples]
uint16_t DACValue[data_size] = {0}; // DAC Array for Storage of Wave Values [0 - 4095]
/* USER CODE END PV */

The variable MyClkFreq is equal to the maximum frequency of the APB1 timer clocks,
which is equal to 108 MHz in this project. The Period Counter or its equivalent variable,
MyPeriod, was set to 108-1, and the value of Prescaler or its equivalent variable,

MyPrescaler, will be then adjusted to satisfy the following equation:

𝑀𝑦𝐶𝑙𝑘𝐹𝑟𝑒𝑞 = (𝑀𝑦𝑃𝑒𝑟𝑖𝑜𝑑 + 1) × (𝑀𝑦𝑃𝑟𝑒𝑠𝑐𝑎𝑙𝑒𝑟 + 1) × 𝑁𝑐 × 𝐹𝑓 (4)

MyClkFreq and MyPeriod must have their fixed values. Nc and Ff are the driving variables

defined by the user and, finally, MyPrescaler is the driven variable that its value should be
computed from Eq. 4.

The data_size is a fixed value defining the buffer size of DACValue array. The values of
chirp signal are stored in this array before being sent to the DAC module. The DACValue
array is initialized to zero. It will be later filled with chirp signal values inside the
int main(void) function.

16
The next step is to define the user function prototypes, as shown below. The code includes
eight private functions that will be later introduced after the int main(void) function.
These user functions are declared right after the declaration of initialization functions for
the STM32 modules.

/* USER CODE BEGIN PFP */


void MY_BLINK(void); // Function : Blinking Green LED //
void MY_INPUT_CHECK(void); // Function : Checks the Validity of Input Variables //
void MY_TIM_DAC_SET(uint8_t TID); // Function : Sets TIM and DAC Parameters //
void MY_STOP_DAC(uint8_t TID); // Function : Stops DAC-TIM Operation //
uint16_t A2D_VAL(double ASIGVAL); // Function : Computes Digital Value from Analog Value //
int8_t SGN_VAL(double ASIGVAL); // Function : Computes Sign of Input Values //
double MY_WAVGEN_FD(void); // Function : Computes the Analog Values of Different Wave Forms //
void MY_DAC_UPDATE(uint32_t* TCOUNT); // Function : Writes the next data to DAC channel //
/* USER CODE END PFP */

The description of user defined functions are expressed after the int main(void) function.
The void MY_BLINK(void) function only sets and resets the green LED on the board. The
void MY_INPUT_CHECK(void) function defines the extremum values for the chirp signal

parameters, and if the user input parameters override these extremum values, this function
will modify the inputs. At the end of this function, all LEDs will be turned on for a second
as a signal to the user that some inputs were not within the appropriate bounds and have
been corrected. The chirp sweep rate variable, Alfa, is defined in this function based upon
the user input. Please keep in mind that Tc is assumed to be the half chirp period. That is,
the signal generator should sweep the frequency range from Fs to Ff within 2Tc seconds.
In Code 1, the time step of the generated wave is fixed to dt. The function
void MY_TIM_DAC_SET(uint8_t TID) activates the user defined timer. The variable TIMID

defines which timer, either TIM2 or TIM6, should be started in interrupt mode. The
void MY_TIM_UPDATE(uint8_t TID) is the function that updates the prescaler of the

activated timer. The value of prescaler is set to the value of MyPrescaler as defined in
void MY_INPUT_CHECK(void) function. Since the value of dt is fixed, the value of prescaler

is also fixed during the execution of Code 1. The next user defined function

17
void MY_STOP_DAC(uint8_t TID) stops the TIM and DAC modules to finish the execution

of program. The uint16_t A2D_VAL(double ASIGVAL) function converts the analog input
value, 𝑥𝑎 , within the range of [0 – 3.3] V to its corresponding 12-bits digital output value,
𝑥𝑑 , within the range of [0 – 4095]. The simple conversion formula is as follows:

𝑥𝑎
𝑥𝑑 = × 4095 (5)
3.3

The function int8_t SGN_VAL(double ASIGVAL) converts the analog input value to its
corresponding sign value as the output. This function is only used for the generation of
square wave form and its definition is as follows:

1 𝑖𝑓 𝑥𝑎 > 0
𝑠𝑔𝑛(𝑥𝑎 ) = { 0 𝑖𝑓 𝑥𝑎 = 0 (6)
−1 𝑖𝑓 𝑥𝑎 < 0

The function double MY_WAVGEN_FD(void) generates the appropriate wave form based
upon the user input. The variable waveID defines the shape of the wave form to be
generated. Inside this function, Eqs. 1-3 are expressed in terms of chirp signal parameters
for each type of wave form.

/* USER CODE BEGIN 4 */


///// Function : Blinking Green LED /////
// Input : ---- //
// Output: ---- //
void MY_BLINK(void)
{
for (int ii = 0 ; ii < 10 ; ii++)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);
HAL_Delay(200);
}
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);
}
///// Function : Checks the Input Variables /////

18
// Input : ---- //
// Output: ---- //
void MY_INPUT_CHECK(void)
{
// Set Extremum Values //
chvar = 0; // Check Variable
double Amax = 1.65;
double A0max = 3.30;
double Vmax = 3.30;
double Ncmin = 20;
double Fsmin = 1;
double Fsmax = data_size / Ncmin;
double Ffmin = 1;
double Ffmax = data_size / Ncmin;
Tcmax = data_size / Ff / Nc / 2;
// Set Wrong Parameters to their Default Values //
(waveID > 2) ? (waveID = 2 , chvar = 1) : 0;
(A < 0) ? (A = -A , chvar = 1) : 0;
(A > Amax) ? (A = Amax , chvar = 1) : 0;
(A0 < 0) ? (A0 = -A0 , chvar = 1) : 0;
(A0 > A0max) ? (A0 = A0max , chvar = 1) : 0;
(A > A0) ? (A = A0 , chvar = 1) : 0;
((A + A0) > Vmax) ? (A = (Vmax - A0) , chvar = 0) : 0;
(Nc < Ncmin) ? (Nc = Ncmin , chvar = 1) : 0;
(Fs < Fsmin) ? (Fs = Fsmin, chvar = 1) : 0;
(Fs > Fsmax) ? (Fs = Fsmax, chvar = 1) : 0;
(Ff < Ffmin) ? (Ff = Ffmin , chvar = 1) : 0;
(Ff > Ffmax) ? (Ff = Ffmax , chvar = 1) : 0;
(Fs > Ff) ? (Ff = Fs , chvar = 1) : 0;
(Tc < 0) ? (Tc = -Tc , chvar = 1) : 0;
(Tc > Tcmax) ? (Tc = Tcmax , chvar = 1) : 0;
(TIMID > 1) ? (TIMID = 0 , chvar = 1) : 0;
(MyClkFreq != 108000000) ? (MyClkFreq = 108000000 , chvar = 1) : 0;
MyPrescaler = (uint32_t)((MyClkFreq / (MyPeriod + 1) / Ff / Nc ) - 1);
(MyPrescaler < 4-1) ? (MyPrescaler = 4-1 , chvar = 1) : 0;
(MyPeriod != 108-1) ? (MyPeriod = 108-1 , chvar = 1) : 0;

19
if (chvar == 1)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7 | GPIO_PIN_14 | GPIO_PIN_0 , GPIO_PIN_SET);
HAL_Delay(1000);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7 | GPIO_PIN_14 | GPIO_PIN_0 , GPIO_PIN_RESET);
}
// Set Other Parameters //
Alfa = (Ff - Fs) / Tc / 2; // Frequency Sweep Rate [Hz/s]
// Maximum Allowable Frequency [Hz]
Cf = ((double)MyClkFreq / (double)(MyPeriod + 1) / (double)(MyPrescaler + 1));
dt = (double)(1 / Cf); // Time Step [s] // dt = (double)(1 / Ff / Nc);
TRESET = (uint32_t)(Nc * Ff * 2 * Tc); // Timer Reset Index
}
///// Function : Modifies TIM and DAC Parameters /////
// Input : uint8_t TID = Timer Identifier //
// Output: --- //
void MY_TIM_DAC_SET(uint8_t TID)
{
if (TID == 0)
{
htim2.Init.Prescaler = MyPrescaler;
htim2.Init.Period = MyPeriod;
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start_IT(&htim2);// Start the TIM2 Module in Interrupt Mode
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7 , GPIO_PIN_SET);// Set the Blue LED
}
else if (TID == 1)
{
htim6.Init.Prescaler = MyPrescaler;
htim6.Init.Period = MyPeriod;
HAL_TIM_Base_Init(&htim6);
HAL_TIM_Base_Start_IT(&htim6); // Start the TIM6 Module in Interrupt Mode
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14 , GPIO_PIN_SET);// Set the RED LED
}
}
///// Function : Updates TIM Parameters /////
// Input : uint8_t TID = Timer Identifier //

20
// Output: --- //
void MY_TIM_UPDATE(uint8_t TID)
{
if (TID == 0)
{
htim2.Init.Prescaler = MyPrescaler;
HAL_TIM_Base_Init(&htim2);
}
else if (TID == 1)
{
htim6.Init.Prescaler = MyPrescaler;
HAL_TIM_Base_Init(&htim6);
}
}
///// Function : Stops DAC-TIM Operation /////
// Input : uint8_t TID = Timer Identifier //
// Output: --- //
void MY_STOP_DAC(uint8_t TID)
{
if (TID == 0)
{
HAL_TIM_Base_Stop_IT(&htim2); // Stop the TIM Module
HAL_DAC_Stop(&hdac, DAC_CHANNEL_1); // Stop the DAC Module
}
else if (TID == 1)
{
HAL_TIM_Base_Stop_IT(&htim6); // Stop the TIM Module
HAL_DAC_Stop(&hdac, DAC_CHANNEL_1); // Stop the DAC Module
}
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7 | GPIO_PIN_14 | GPIO_PIN_0 , GPIO_PIN_RESET);
}
///// Function : Computes Digital Value from Analog Value /////
// Input : double ASIGVAL = Analog Wave Value [0 - 3.3 [V]] //
// Output: uint16_t tempVal = Digital Wave Value [0 - 4095] //
uint16_t A2D_VAL(double ASIGVAL)
{

21
int16_t tempVal = (ASIGVAL / 3.30) * 4095; // Analog to Digital Conversion
(tempVal > 4095) ? (tempVal = 4095) : 1; // Upper Limit for Signal Saturation
(tempVal < 0) ? (tempVal = 0) : 1; // Lower Limit for Signal Saturation
return tempVal;
}
///// Function : Computes Sign of Input Values /////
// Input : double ASIGVAL = Analog Wave Value [0 - 3.3 [V]] //
// Output: int8_t ------- = Sign of Wave Value {0,1,-1} //
int8_t SGN_VAL(double ASIGVAL)
{
return (ASIGVAL == 0) ? 0 : ( (0 <= ASIGVAL) - (ASIGVAL <= 0) );
}
///// Function : Computes the Analog Values for Different Wave Forms /////
// This Function does Consider the Frequency of Wave Explicitly //
// Input : -------- = void //
// Output: double St = Analog Wave Value [0 - 3.3 [V]] //
double MY_WAVGEN_FD(void)
{
if (waveID == COS) // HARMONIC Wave Value [V]
{ St = A * cos( 2.0 * (double)PI * Ft * t + Phi0 ) + A0;}
else if(waveID == TRI) // TRIANGULAR Wave Value [V]
{ St = ((2.0 * A) / (double)PI) * asin( sin( 2.0 * (double)PI * Ft * t + Phi0
+ ((double)PI / 2.0) ) ) + A0;}
else if(waveID == SQR) // SQUARE Wave Value [V]
{ St = A * (double)SGN_VAL( cos( 2.0 * (double)PI * Ft * t + Phi0 ) ) + A0;}
return St;
}
///// Function : Writes the next data to DAC channel /////
// Input : -------- = void //
// Output: -------- = void //
void MY_DAC_UPDATE(uint32_t* TCOUNT)
{
*TCOUNT += 1;
//t += dt; // Update Time Variable [s]
(*TCOUNT == TRESET) ? (*TCOUNT = 0 , Nchrip += 1) : 1;
// Write the next value to DAC channel

22
HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,DACValue[*TCOUNT]);
}
///// Function : HAL_TIM_PeriodElapsedCallback /////
// If the timer is activated in interrupt mode the //
// HAL_TIM_PeriodElapsedCallback() function is executed //
// every time step and the user can add his own code inside //
// this function which should be run very quickly //
// Input : htim = TIM Handle //
// Output: ---- = void //
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
UNUSED(htim);
MY_DAC_UPDATE(&TIMcount);
}
/* USER CODE END 4 */

The two most important user defined functions of Code 1 are explained here. Firstly, the
function void MY_DAC_UPDATE(uint32_t* TCOUNT) adds one unit to the value of timer
counter variable, TIMcount, and then send the appropriate value of signal from DACValue
array to the DAC module by using the HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,
DAC_ALIGN_12B_R,DACValue[*TCOUNT]) function. It resets the timer counter after TRESET

iterations, and the chirp signal is repeated from the beginning again. Nchrip is the counter
which calculates the number of chirp signals that has been sent to the DAC module so far.

Secondly, the void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)

function, which is originally defined in stm32f7xx_hal_tim.c, is the callback function


that is continuously executed after during iteration of time. Since there is only one timer
active in the code, the input to this function can be ignored. The user defined function
void MY_DAC_UPDATE(uint32_t* TCOUNT) is repeatedly called inside this function so that

the values of chirp signal stored inside the DACValue array are continuously sent to the
DAC module with the approprite time step dt.

The last section of the code should be written inside the int main(void) function. This
includes two separate code blocks located outside and inside of the infinite loop. The first
23
block of code, located outside the infinite loop, is responsible for filling of DACValue array
with appropriate signal values, start of timer inside the MY_TIM_DAC_SET(TIMID) function
followed by start of the DAC channel 1 by using the appropriate command
HAL_DAC_Start(&hdac,DAC_CHANNEL_1) .

/* USER CODE BEGIN 2 */


///// CHECK INPUT AND GENERATE DAC WAVE /////
MY_BLINK( );
MY_INPUT_CHECK( ); // Check Input Variables
for (int nt = 1 ; nt <= (int)TRESET ; nt++)
{
t = (double)nt * dt ; // Update Time [s]
Ft = (double)(Fs + 0.5 * Alfa * t); // Update Frequency Variable [Hz]
St = MY_WAVGEN_FD( ); // Analog Wave Value [0 - 3.3 [V]]
DACValue[nt-1] = A2D_VAL(St); // Digital Wave Value [0 - 4095]
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET);
}
MY_BLINK( );
///// START THE DAC OPERATION /////
t = 0; // Reset the Time Variable [s]
MY_TIM_DAC_SET(TIMID); // Modify TIM-DAC Parameters and Start the TIM Module
HAL_DAC_Start(&hdac,DAC_CHANNEL_1); // Start the DAC Module
/* USER CODE END 2 */

The second block of code, located inside the infinite loop, is responsible for stopping the
TIM and DAC modules after the chirp signal has been repeated for Nrepeat times. By
adding the above code blocks to the main.c file, Code 1 is ready to be compiled and loaded
to the STM32 board. Please pay attention to how we have added each block of the code to
their appropriate places for the user code in the main.c file. For example, all the user
defined fucntions are written after the /* USER CODE BEGIN 4 */ and before the /* USER CODE

END 4 */. In order to load the code to the board, connect the STM32 NUCLEO board to
your PC or laptop and after building the project (*), click Run > Run from the menu bar
(*). In the Edit Configuration window (*), click the Ok button and the code will be loaded
to the board after a couple of seconds.
24
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
///// FINISH THE DAC OPERATION /////
if (Nchrip == Nrepeat)
{
MY_STOP_DAC(TIMID);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

In order to verify the output signal from DAC module, a digital USB oscilloscope,
HANTEK 6022B, is used to read the output chirp signal from the STM32 board with 100
kHz sampling rate. The chirp signal parameters for the code are reported in Table 4. Please
keep in mind that since the data_size is a fixed value, when we increase the number of
points per period of the wave, Nc, the value of half chirp period, Tc, is decreased so that the
number of data points required for the whole chirp signal does not exceed data_size.

Table 4 Parameters of chirp for verification of Code 1

Param. Value Unit Param. Value Unit

𝐴 1.65 [𝑉] 𝐹𝑓 1000 [𝐻𝑧]

𝐴0 1.65 [𝑉] 𝑇𝑐 2.5,1,0.5 [𝑠]

𝜑0 0 [𝑟𝑎𝑑] 𝑁𝑐 20,50,100 [−]

𝐹𝑠 10 [𝐻𝑧] 𝛼 198,495,990 [𝐻𝑧⁄𝑠]

The time domain plots of chirp signals as well as its DFT plots are illustrated in Fig. 3 and
Fig. 4, respectively. All chirp signals have a reasonably flat response curve over the entire
frequency bandwidth, regardless of the Nc values. Our observations show that we are

25
capable of performing DAC operation with a maximum frequency of 250 kHz by using
Code 1. That is, a 10 kHz signal with 25 points on each period of the wave can be created.

Fig. 3 Time domain plots of chirp signals with different wave forms: Harmonic (black), Square
(blue) and Triangular (red)

Fig. 4 Discrete Fourier Transform (DFT) plots of chirp signals with different wave forms:
Harmonic (black), Square (blue) and Triangular (red)

26
7.2 Code 2 (DAC in DMA mode with timer TRGO feature)

In this code, we have activated the DAC module in DMA mode which facilitate faster
transfer of data from memory to peripheral. The time scale of the DAC operation is
continuously updated inside the DAC channel 1 callback function. The TRGO feature
allows the timer to generate the external conversion trigger for the DAC channel. Please
pay attention that there were extra settings associated with the activation of DMA for the
DAC module as well as the activation of TRGO feature for the TIM module, which were
previously described in Section 6. That is, the initialization functions for the STM32
modules are partly different from the ones used in Code 1. An initialization function named
static void MX_DMA_Init(void) is added to the other functions, which enables the DMA

settings for the DAC module. The first section of the code defines all the variables that
describe the chirp signal parameters as well as TIM and DAC parameters. These variables
are defined globally and therfore can be called by all functions.

/* USER CODE BEGIN PV */


// Comment the following line in the Function: static void MX_DAC_Init(void)
DAC_ChannelConfTypeDef sConfig = {0}; // The sConfig should be Defined Globally
// CHIRP SIGNAL PARAMETERS
enum {COS,TRI,SQR}; // Wave Type [COS = Harmonic , TRI = Triangular , SQR = Square]
uint8_t waveID = 0; // Wave Form Identifier [0 = COS , 1 = TRI , 2 = SQR]
uint8_t Nchrip = 0; // Number of Completed Chirps [-]
uint8_t Nrepeat = 50; // Number of Repeats [-]
uint8_t chvar; // Check Variable
double A = 1.65; // Dynamic Amplitude of Signal [V]
double A0 = 1.65; // Vertical Offset Value [V]
double Phi0 = 0.0; // Horizontal Offset Value [rad]
double Fs = 10.0; // Chirp Start Frequency [Hz]
double Ff = 1000.0; // Chirp End Frequency [Hz]
double Tc = 2.5; // Chirp Half Period [s]
double Alfa = 0; // Frequency Sweep Rate [Hz/s]
double Cf = 0; // Maximum Allowable Frequency [Hz]
double St = 0; // Analog Wave Values [0 - 3.3 [V]]

27
double deltat = 0; // Time Period of Wave [s]
double t = 0; // Time Variable [s]
double Ft = 0; // Wave Frequency Variable [Hz]
// TIMER PARAMETERS
enum {TIMER2,TIMER6}; // Timer Type [TIMER2 = htim2 , TIMER6 = htim6]
uint8_t TIMID = 1; // Timer Identifier [0 = TIMER2 , 1 = TIMER6]
uint32_t MyClkFreq = 108000000; // Fixed APB1 Clock Frequency [Hz]
uint32_t MyPrescaler = 1-1; // Minimum Prescaler Value [cycles]
uint32_t MyPeriod = 108-1; // Fixed Period Value [cycles]
// DAC PARAMETERS
#define data_size 20 // Maximum Buffer Size for DAC Array [samples] [20-100]
uint16_t DACValue[data_size] = {0}; // DAC Array for Storage of Wave Values [0 - 4095]
/* USER CODE END PV */

The variable MyClkFreq is equal to the maximum frequency of the APB1 timer clocks,
which is equal to 108 MHz in this project. The Period Counter or its equivalent variable,
MyPeriod, was set to 108-1. MyClkFreq and MyPeriod must have their fixed values. The

value of Prescaler or its equivalent variable, MyPrescaler, will be later adjusted inside the
body of code.

Some changes are made to the input variables. First of all, a variable named as sConfig,
which is originally defined as a local variable inside the DAC initialization function
static void MX_DAC_Init(void), is introduced here as a global variable. This is due to

the fact that one of the user defined functions will later call this variable during the
execution of Code 2. Secondly, the data_size defines the number of samples per period
of wave and is equivalent to the variable Nc as defined in Code 1. The minimum
recommended value for data_size is equal to 20 samples. By increasing the value of
data_size, the wave curve will become smoother over its period. Here, we do not store all

the chirp data points inside the DACValue array. Only the data points corresponding to one
period of the harmonic, square or triangular wave is stored inside the DACValue array, and
the chirp signal is created by sending this wave periodically to the DAC module while
properly changing the time period (or equivalently the frequency) of the signal.

28
The next step is to define the user function prototypes, as shown below. The code includes
eight private functions that will be later introduced after the int main(void) function. The
user defined functions are declared right after the declaration of initialization functions for
the STM32 modules. All these functions do similar tasks as the ones presented inside the
Code 1. Some of these functions, like void MY_BLINK(void), uint16_t A2D_VAL(double
ASIGVAL) and int8_t SGN_VAL(double ASIGVAL), were not changed at all. But other

functions may have slight modifications. Enthusiast readers may compare these functions
with the previous ones mentioned in Section 7.1. Most importantly, the function
double MY_WAVGEN_FIN(void) generates the data points located on one period of the wave

with the frequency of 1 Hz. The frequency of this wave will be adaptively adjusted to the
frequency bandwidth of the chirp signal by tuning the value of MyPrescaler during the
execution of the code. This adaptive tuning of wave’s frequency according to the chirp
signal’s frequency bandwidth occurs inside the function void MY_DAC_UPDATE(void).

/* USER CODE BEGIN PFP */


void MY_BLINK(void); // Function : Blinking Green LED //
void MY_INPUT_CHECK(void); // Function : Checks the Validity of Input Variables //
void MY_TIM_DAC_SET(uint8_t TID); // Function : Sets TIM and DAC Parameters //
void MY_STOP_DAC(uint8_t TID); // Function : Stops DAC-TIM Operation //
uint16_t A2D_VAL(double ASIGVAL); // Function : Computes Digital Value from Analog Value //
int8_t SGN_VAL(double ASIGVAL); // Function : Computes Sign of Input Values //
double MY_WAVGEN_FIN(void); // Function : Computes the Analog Values of Different Wave Forms //
void MY_DAC_UPDATE(void); // Function : Writes the next wave to DAC channel //
/* USER CODE END PFP */

Before expressing the user defined functions, we must make a slight change to the
initialization function for the DAC module. Inside the static void MX_DAC_Init(void)
function, we should disable the local definition of variable sConfig, since it was
deliberately expressed as a global variable at the beginning of the code.

29
static void MX_DAC_Init(void)
{
/* USER CODE BEGIN DAC_Init 0 */
/* USER CODE END DAC_Init 0 */
// DAC_ChannelConfTypeDef sConfig = {0};
/* USER CODE BEGIN DAC_Init 1 */
/* USER CODE END DAC_Init 1 */
/** DAC Initialization
*/
hdac.Instance = DAC;
if (HAL_DAC_Init(&hdac) != HAL_OK)
{
Error_Handler();
}
/** DAC channel OUT1 config
sConfig.DAC_Trigger = DAC_TRIGGER_T2_TRGO;
sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
if (HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN DAC_Init 2 */
/* USER CODE END DAC_Init 2 */
}

Please note that the sConfig.DAC_Trigger is set according to the user defined timer, either
TIM2 or TIM6, inside the void MY_TIM_DAC_SET(uint8_t TID) function. This is the reason
why we defined sConfig as a global variable and not a local one. The description of the
user defined functions are expressed after the int main(void) function.

/* USER CODE BEGIN 4 */


///// Function : Blinking Green LED /////
// Input : ---- //
// Output: ---- //

30
void MY_BLINK(void)
{
for (int ii = 0 ; ii < 10 ; ii++)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);
HAL_Delay(200);
}
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);
}
///// Function : Checks the Input Variables /////
// Input : ---- //
// Output: ---- //
void MY_INPUT_CHECK(void)
{
// Set Extremum Values //
chvar = 0; // Check Variable
double Amax = 1.65;
double A0max = 3.30;
double Vmax = 3.30;
double Fsmin = 1;
double Fsmax = 2000;
double Ffmin = 1;
double Ffmax = 2000;
double Tcmax = 50;
// Set Wrong Parameters to their Default Values //
(waveID > 2) ? (waveID = 2 , chvar = 1) : 0;
(A < 0) ? (A = -A , chvar = 1) : 0;
(A > Amax) ? (A = Amax , chvar = 1) : 0;
(A0 < 0) ? (A0 = -A0 , chvar = 1) : 0;
(A0 > A0max) ? (A0 = A0max , chvar = 1) : 0;
(A > A0) ? (A = A0 , chvar = 1) : 0;
((A + A0) > Vmax) ? (A = (Vmax - A0) , chvar = 1) : 0;
(Fs < Fsmin) ? (Fs = Fsmin , chvar = 1) : 0;
(Fs > Fsmax) ? (Fs = Fsmax , chvar = 1) : 0;
(Ff < Ffmin) ? (Ff = Ffmin , chvar = 1) : 0;
(Ff > Ffmax) ? (Ff = Ffmax , chvar = 1) : 0;

31
(Fs > Ff) ? (Ff = Fs , chvar = 1) : 0;
(Tc < 0) ? (Tc = -Tc , chvar = 1) : 0;
(Tc > Tcmax) ? (Tc = Tcmax , chvar = 1) : 0;
(TIMID > 1) ? (TIMID = 0 , chvar = 1) : 0;
(MyClkFreq != 108000000) ? (MyClkFreq = 108000000 , chvar = 1) : 0;
(MyPrescaler != 1-1) ? (MyPrescaler = 1-1 , chvar = 1) : 0;
(MyPeriod != 108-1) ? (MyPeriod = 108-1 , chvar = 1) : 0;
if (chvar == 1)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7 | GPIO_PIN_14 | GPIO_PIN_0 , GPIO_PIN_SET);
HAL_Delay(1000);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7 | GPIO_PIN_14 | GPIO_PIN_0 , GPIO_PIN_RESET);
}
// Set Other Parameters //
Alfa = (Ff - Fs) / Tc / 2; // Frequency Sweep Rate [Hz/s]
// Maximum Allowable Frequency [Hz]
Cf = ((double)MyClkFreq / (double)(MyPeriod + 1) / (double)data_size);
//(Ff > Cf) ? (Ff = Cf) : 1;
}
///// Function : Modifies TIM and DAC Parameters /////
// Input : uint8_t TID = Timer Identifier //
// Output: --- //
void MY_TIM_DAC_SET(uint8_t TID)
{
if (TID == 0)
{
htim2.Init.Prescaler = MyPrescaler;
htim2.Init.Period = MyPeriod;
HAL_TIM_Base_Init(&htim2);
sConfig.DAC_Trigger = DAC_TRIGGER_T2_TRGO;
HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1);
HAL_TIM_Base_Start(&htim2);// Start the TIM2 Module
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7 , GPIO_PIN_SET);// Set the Blue LED
}
else if (TID == 1)
{
htim6.Init.Prescaler = MyPrescaler;

32
htim6.Init.Period = MyPeriod;
HAL_TIM_Base_Init(&htim6);
sConfig.DAC_Trigger = DAC_TRIGGER_T6_TRGO;
HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1);
HAL_TIM_Base_Start(&htim6); // Start the TIM6 Module
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14 , GPIO_PIN_SET);// Set the RED LED
}
}
///// Function : Updates TIM Parameters /////
// Input : uint8_t TID = Timer Identifier //
// Output: --- //
void MY_TIM_UPDATE(uint8_t TID)
{
if (TID == 0)
{
htim2.Init.Prescaler = MyPrescaler;
HAL_TIM_Base_Init(&htim2);
}
else if (TID == 1)
{
htim6.Init.Prescaler = MyPrescaler;
HAL_TIM_Base_Init(&htim6);
}
}
///// Function : Stops DAC-TIM Operation /////
// Input : uint8_t TID = Timer Identifier //
// Output: --- //
void MY_STOP_DAC(uint8_t TID)
{
if (TID == 0)
{
HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1); // Stop the DAC Module in DMA Mode
HAL_TIM_Base_Stop(&htim2); // Stop the TIM Module
}
else if (TID == 1)
{

33
HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1); // Stop the DAC Module in DMA Mode
HAL_TIM_Base_Stop(&htim6); // Stop the TIM Module
}
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7 | GPIO_PIN_14 | GPIO_PIN_0 , GPIO_PIN_RESET);
}
///// Function : Computes Digital Value from Analog Value /////
// Input : double ASIGVAL = Analog Wave Value [0 - 3.3 [V]] //
// Output: uint16_t tempVal = Digital Wave Value [0 - 4095] //
uint16_t A2D_VAL(double ASIGVAL)
{
int16_t tempVal = (ASIGVAL / 3.30) * 4095; // Analog to Digital Conversion
(tempVal > 4095) ? (tempVal = 4095) : 1; // Upper Limit for Signal Saturation
(tempVal < 0) ? (tempVal = 0) : 1; // Lower Limit for Signal Saturation
return tempVal;
}
///// Function : Computes Sign of Input Values /////
// Input : double ASIGVAL = Analog Wave Value [0 - 3.3 [V]] //
// Output: int8_t ------- = Sign of Wave Value {0,1,-1} //
int8_t SGN_VAL(double ASIGVAL)
{
return (ASIGVAL == 0) ? 0 : ( (0 <= ASIGVAL) - (ASIGVAL <= 0) );
}
///// Function : Computes the Analog Values for Different Wave Forms /////
// This Function does not Consider the Frequency of Wave Explicitly //
// Input : -------- = void //
// Output: double St = Analog Wave Value [0 - 3.3 [V]] //
double MY_WAVGEN_FIN(void)
{
if (waveID == COS) // HARMONIC Wave Value [V]
{ St = A * cos( 2.0 * (double)PI * t + Phi0 ) + A0;}
else if(waveID == TRI) // TRIANGULAR Wave Value [V]
{ St = ((2.0 * A) / (double)PI) * asin( sin( 2.0 * (double)PI * t + Phi0
+ ((double)PI / 2.0) ) ) + A0;}
else if(waveID == SQR) // SQUARE Wave Value [V]
{ St = A * (double)SGN_VAL( cos( 2.0 * (double)PI * t + Phi0 ) ) + A0;}
return St;

34
}
///// Function : Writes the next wave to DAC channel /////
// Input : -------- = void //
// Output: -------- = void //
void MY_DAC_UPDATE(void)
{
MY_TIM_UPDATE(TIMID);
// Compute Period of Pervious Wave [s]
deltat = (double)(((double)MyPrescaler + 1) / Cf);
// Update Time Variable [s]
( t > (2.0 * Tc) ) ? ( t = 0 , Nchrip += 1 ) : ( t += deltat );
Ft = (double)(Fs + Alfa * t); // Update Frequency Variable [Hz]
(Ft > Ff) ? (Ft = Ff) : 1; // Check Frequency Variable
(Ft > Cf) ? (Ft = Cf) : 1; // Check Frequency Variable
MyPrescaler = (uint32_t)((Cf / Ft) - 1); // Compute Prescaler Value [-]
// Compute Prescaler Value [-] If DAC signal failed use this formula
// MyPrescaler = (roundf(Cf / Ft) - 1);
}
///// Function : HAL_DAC_ConvCpltCallbackCh1 /////
// At The end of data transfer HAL_DAC_ConvCpltCallbackCh1() //
// function is executed and the user can add his own code //
// Input : hdac = DAC Handle //
// Output: ---- = void //
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac)
{
UNUSED(hdac);
MY_DAC_UPDATE( );
}
/* USER CODE END 4 */

The two most important user defined functions of Code 2 are explained here. Firstly, the
function void MY_DAC_UPDATE(void) sets the MyPrescaler value of the timer via
MY_TIM_UPDATE(TIMID). Then, it computes the time interval, deltat, corresponding to one

period of the wave and incrementally add it to the time variable, t. The time variable will
be reset to zero after each 2Tc seconds and the value of Nchrip is incremented by one unit,

35
meaning that one chirp signal is successfully sent to the DAC module. The instantaneous
frequency of the chirp signal, Ft, is computed based upon the instance of time, t, and the
next value for the MyPrescaler variable is computed by using the following equation:

𝐶𝑓
𝑀𝑦𝑃𝑟𝑒𝑠𝑐𝑎𝑙𝑒𝑟 = ( ) − 1
𝐹𝑡
(7)
𝑀𝑦𝐶𝑙𝑘𝐹𝑟𝑒𝑞
𝐶𝑓 =
(𝑀𝑦𝑃𝑒𝑟𝑖𝑜𝑑 + 1) × 𝑑𝑎𝑡𝑎_𝑠𝑖𝑧𝑒

This equation is equivalent to Eq. 4, if one assumes that the variable data_size is the same
as the variable Nc.

Secondly, the void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac) function,


which is originally defined in stm32f7xx_hal_dac.c, is the callback function for channel
1 of the DAC module. This function is continuously executed after each completion of the
DMA operation. The input to this function can be ignored, as there is only one DAC module
available on the STM32 board. The user defined function void MY_DAC_UPDATE(void) is
repeatedly called inside this function so that one period of the wave stored inside the
DACValue array is continuously sent to the DAC module with the approprite frequency.

The value of MyPrescaler is constantly updated based upon the frequency bandwidth of
the chirp signal, as mentioned above.

The last section of the code should be written inside the int main(void) function. This
include two separate code blocks located outside and inside of the infinite loop. The first
block of code, located outside the infinite loop, is responsible for filling of DACValue array
with appropriate signal values, start of DAC channel 1 in DMA mode by using the
appropriate command HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)

DACValue, (uint32_t)data_size, DAC_ALIGN_12B_R) followed by start of timer inside the

MY_TIM_DAC_SET(TIMID) function. If you are using the MDK-Arm Keil environment

instead of the STM32CubeIDE, please note that the TIM module should be started prior to
the DAC module.

36
/* USER CODE BEGIN 2 */
///// CHECK INPUT AND GENERATE DAC WAVE /////
MY_BLINK( );
MY_INPUT_CHECK( ); // Check Input Variables
for (int nt = 1 ; nt <= (int)data_size ; nt++)
{
t = (double)nt / (double)data_size;
St = MY_WAVGEN_FIN( ); // Analog Wave Value [0 - 3.3 [V]]
DACValue[nt-1] = A2D_VAL(St); // Digital Wave Value [0 - 4095]
}
MY_BLINK( );
///// START THE DAC-DMA OPERATION /////
t = 0; // Reset the Time Variable [s]
// Start the DAC Module in DMA Mode
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)DACValue,
(uint32_t)data_size, DAC_ALIGN_12B_R);
MY_TIM_DAC_SET(TIMID); // Modify TIM-DAC Parameters and Start the TIM Module
/* USER CODE END 2 */

The second block of code, located inside the infinite loop, is responsible for stopping the
DAC and TIM modules after the chirp signal has been repeated for Nrepeat times.

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
///// FINISH THE DAC-DMA OPERATION /////
if (Nchrip == Nrepeat)
{
MY_STOP_DAC(TIMID);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

37
By adding the above code blocks to the main.c file, Code 2 is ready to be compiled and
loaded to the STM32 board. Please pay attention to how we have added each block of the
code in the apprporite places for the user code in the main.c file. For example, all the user
input variables are written after the /* USER CODE BEGIN PV */ and before the /* USER CODE

END PV */. In order to load the code to the board, connect the STM32 NUCLEO board to
your PC or laptop and after building the project (*), click Run > Run from the menu bar
(*). In the Edit Configuration window (*), click the Ok button and the code will be loaded
to the board after a couple of seconds.

Table 5 Parameters of chirp for verification of Code 2

Param. Value Unit Param. Value Unit

𝐴 1.65 [𝑉] 𝐹𝑓 1000 [𝐻𝑧]

𝐴0 1.65 [𝑉] 𝑇𝑐 2.5 [𝑠]

𝜑0 0 [𝑟𝑎𝑑] data_size 20,50,100 [−]

𝐹𝑠 10 [𝐻𝑧] 𝛼 198 [𝐻𝑧⁄𝑠]

In order to verify the output signal from DAC module, a digital USB oscilloscope,
HANTEK 6022B, is used to read the output chirp signal from the STM32 board with 100
kHz sampling rate. The chirp signal parameters for the code are reported in Table 5. Here,
all the input variables are held as constant values except for data_size. Please keep in mind
that the data_size is increased from 20 to 100 in order to investigate the effect of
increasing the resolution of wave on the quality of generated chirp signal. An acceptable
chirp signal will uniformly excite all frequencies across the entire frequency bandwidth
(please refer to Fig. 4).

38
Fig. 5 Time domain plots of chirp signals with different wave forms: Harmonic (black), Square
(blue) and Triangular (red)

Fig. 6 Discrete Fourier Transform (DFT) plots of chirp signals with different wave forms:
Harmonic (black), Square (blue) and Triangular (red)

The time domain plots of chirp signals as well as its DFT plots are illustrated in Fig. 5 and
Fig. 6, respectively. As the number of data points located on one period of the wave

39
increases, the flatness of the DFT response over the entire frequency range degrades.
Except for the minumum value of data_size, the chirp signal can not properly excite all
frequencies over the desired bandwidth. When the value of data_size is increased, the
maximum allowable frequency that can be generated by the STM32 board decreases, and
the prescaler value approches the marginal value of zero as Cf approaches Ft (please refer
to Eq. 7). In this situation, a narrower range of prescaler values is associated with the entire
frequency bandwidth of the chirp signal. Therefore, only a few discrete frequencies from
the bandwidth are excited. But, when the value of data_size is decreased, the values of
prescaler is shifted to higher ones, and a wider range of prescaler values is associated with
the entire frequency bandwidth of the chirp signal. So, although it may seem counter-
intuitive, when we use the DAC module in DMA mode, a wave with a lower resolution is
more appropriate for generation of a flat chirp signal with a relatively high frequency
bandwidth.

However, if one would like to create a single frequency signal rather than a chirp signal,
i.e. Ft = Fs, Code 2 is very efficient and precise. Because the resolution of the single
frequency wave can be remarkably improved by increasing the value of data_size. Our
observations show that we are capable of performing DAC operation with a maximum
frequency of 1 MHz by using Code 2, which is well beyond the maximum frequency
achieved by Code 1. This means that we are capable of creating a 1000 Hz harmonic signal
with data_size of 1000. In other words, the fixed time step for this single frequency signal
is 1e-6 second, which is quite notable. Similarly, a 40 kHz signal with 25 points on each
period of the wave can be created by this approach.

8. Excite the dynamics of a high frequency fatigue test machine

In this section, our signal generator code is used to excite the dynamics of a high frequency
fatigue test machine over its operating bandwidth. This mechatronics system is currently
under development at the CAD / CAM laboratory of Ferdowsi University of Mashhad.

40
a b

c d
Fig. 7 The experimental setup for the benchmark system identification test: a) STM32 NUCLEO
board b) the high frequency fatigue test machine c) the power amplifier of the shaker and the
PC d) the result of system identification test

It is composed of three subcomponents including the actuator, the sensor and the structural
frame. An electrodynamic shaker from TENLEE Piezotronics Inc., model MS-100, with
maximum force capacity of 100 N and frequency bandwidth of [0 – 4] kHz is used as the
actutor. The linear amplifier, model LA-200, drives this actuator. A miniature compression
and tension load cell from DACELL, model UMI, with maximum force capacity of 196 N

41
is used as the sensor. The dynamic strain amplifier, model DN-AM100, is used as the signal
conditioner for the load cell’s output signal. It has a frequency bandwidth of [0 – 2] kHz.

Fig. 8 The time domain plots of the input chirp signal and the sensor output signal for the high
frequency fatigue test machine – comparison of the two algorithms

The system identification tests are conducted for the frequency range of [100 – 1000] Hz
by using both Code 1 and Code 2. As it is illustrated in Fig. 8, the dynamics of the high
frequency fatigue test machine is well-excited by the input chirp signal. The comparison
of time domain results reveals that both codes had reasonably identical performances,
despite the differences in the underlying algorithms. The dominant resonance peaks
emerging in the sensor’s output signal are highly likely to be related to the structural mode
shapes of the mechine’s frame. The Discrete Fourier Transform (DFT) plots for the input
chirp signal as well as the sensor output signal are depicted in Fig. 9. Both codes are able
to generate a chirp signal that excites all the frequencies across the desired spectrum. Yet,
the flatness of chirp signal created by Code 1 is slightly better than Code 2. Despite such
discrepancies, the DFT plots of sensor signal are reasonably identical.

42
Fig. 9 The Discrete Fourier Transform (DFT) plots of the input chirp signal and the sensor output
signal for the high frequency fatigue test machine – comparison of the two algorithms

If one takes a close look to the DFT plot of the sensor signal, it is obvious that the most
approprite frequency interval for conducting the real-time control experiments on this high
frquency fatigue test machine is [300 – 600] Hz. Because there are no structural resonances
across this region which may negatively interfere with the control action. Additionally, the
mechatronics system has a reasonably flat response over this interval, which facilitates the
controller design.

In order to connect the STM32 NUCLEO board to the electrodynamic actuator, we should
properly connect the wires to the GND and DAC pins of the board. The MCU pins
associated with DAC OUT1, DAC OUT2 and GND are shown in Fig. 10. The pins PA4
and PA5 are associated with DAC OUT1 and DAC OUT2, respectively. These pins can be
easily accessed from connector pins 17 and 10 located on CN7. Similarly, the GND pins,
PD2 and PG2, are attached to the connector pins 11 and 13 of CN8.

43
Fig. 10 Hardware layout and configuration for the STM32 NUCLEO board

9. Conclusions

In this tutorial, we specifically described the application of DAC module in STM32F7


boards. We tried to explain the procedure of developing two different signal generation
algorithms, while activating, adjusting and interacting with the DAC / TIM / GPIO modules
of the STM32 NUCLEO-F746ZG board. Firstly, the DAC operation was performed in
single mode, where the subsequent discrete values of the generated signal were sent to the
DAC module during the global interrupts of the timer module. The maximum allowable
frequency of DAC operation in single mode was 250 kHz. As another method, the DAC
operation was performed in DMA mode, by using the TRGO feature of the timer module.
Having direct access to the memory for faster transfer of signal points to the DAC module,
we were able to increase the maximum frequency of DAC operation by fourfold reaching

44
the staggering value of 1 MHz. Code 1 is more memory intensive as opposed to Code 2,
due to allocation of a much larger buffer size for the DACValue array. Yet, it is capable of
exciting all the desired frequencies across the chirp bandwidth, which leads to a reasonably
flat DFT response for the generated chirp signal. Code 2, on the other hand, provides a
higher percision for generation of single frequency signals, while providing a faster transfer
of data from memory to the DAC module as well as keeping the memory usage to a
minimum.

10. References

In order to study and learn more about the other modules and features of the STM32F7
boards, you may read or watch the following benchmark references:

10.1 Official ST Documents

Description of STM32F7 HAL and Low-layer drivers (UM1905)


STM32 Nucleo-144 boards (UM1974)
STM32F75xxx and STM32F74xxx advanced Arm®-based 32-bits MCUs (RM0385)
STM32F746xx Datasheet
Audio and waveform generation using the DAC in STM32 products (AN3126)

10.2 STM32 Books

Mastering STM32
Programming with STM32

10.3 STM32 Videos

Mutex Embedded Education

45

View publication stats

You might also like