Programming Industrial Embedded Systems, 2017./2018.: Laboratory Exercise 1
Programming Industrial Embedded Systems, 2017./2018.: Laboratory Exercise 1
- Laboratory exercise 1
Author: Hrvoje Džapo
Contents
1. Required hardware, software and additional literature ................................................................. 3
2. Development environment installation .......................................................................................... 4
2.1. Keil MDK-ARM uVision installation ......................................................................................... 4
2.2. Adding support for STM32F4 family through the Pack Installer utility................................... 4
2.3. Installing drivers and upgrading STM32F4DISCOVERY board firmware ................................. 7
2.4. Verifying system installation and running an example ........................................................... 8
3. Creating a new project .................................................................................................................. 16
3.1. Creating a new project in uV5 using run-time environment manager ................................. 16
3.2. STM32F4 Standard Peripheral Library................................................................................... 21
3.3. Creating STM32F4 Standard Peripheral Library-based project ............................................. 23
3.4. Creating project for static linking with STM32F4 Standard Peripheral Library ..................... 33
4. General-purpose input/output (GPIO) ports ................................................................................. 37
4.1. Step 1: Create a new project ................................................................................................. 37
4.2. Step 2: Determine the GPIO pins connected to on-board LEDs ............................................ 37
4.3. Step 3: Initialization of GPIO interface .................................................................................. 37
4.4. Step 4: Avoiding hard-coded constants via #define statements ........................................... 43
4.5. Step 5: Appropriate use of header files and module decomposition to build larger programs
43
4.6. Step 6: Adding Blinky functionlity ......................................................................................... 47
4.7. Lab outcomes: ....................................................................................................................... 49
5. Timers and interrupts .................................................................................................................... 50
5.1. Step 1: Create new project from template ........................................................................... 50
5.2. Step 2: Configure timer ......................................................................................................... 51
5.3. Step 3: Modify the main module to make LEDs blink with strict timing ............................... 59
5.4. Lab outcomes: ....................................................................................................................... 60
6. Serial communication .................................................................................................................... 61
6.1. Step 1: Create a new project from template......................................................................... 61
6.2. Step 2: Configure USART1 peripheral and ISR ....................................................................... 61
6.3. Step 3: Implement high-level API functions for sending and receiving characters............... 67
6.4. Step 4: Implement the loopback functionality in the main module ..................................... 68
6.5. Lab outcomes: ....................................................................................................................... 69
1
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
2
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Hardware:
- STM32F4DISCOVERY development kit,
- USB-mini cable,
- UART/USB bridge1
Software:
- Keil MDK-ARM uVision development environment2 (version 5 (uV5))
o integrated development environment (IDE) for ARM microcontrollers
- STM32F4 Standard Peripheral Library3 (STSW-STM32065)
o high-level library (hardware abstraction layer, HAL) for accessing on-chip
microcontroller resources and peripherals
Additional literature:
- uVision 5 MDK getting started manual (Getting Started - uVision 5 MDK.pdf)
- STM32F4DISCOVERY board user manual (UM1472 - Discovery kit for STM32F407.pdf)
- STM32F4DISCOVERY board training materials (STMicroelectronics Cortex-M4 Training
STM32F407.pdf)
- STM32F4 microcontroller family reference manual (STM32F4 Reference manual.pdf)
- STM32F4 microcontroller family programming manual (STM32F3 and STM32F4 Series Cortex-
M4 programming manual.pdf)
- STM32F407VG microcontroller datasheet (STM32F407VG.pdf)
- STM32F2xx Standard Peripheral Library reference (Description of STM32F2xx Standard
Peripheral Library.pdf)
- Keil application note 230 „STM32F4 Lab for MDK v5.1“ (apnt_230.pdf) + source code with
example projects (apnt_230.zip)
Additional literature for lab exercises is packed conveniently in the archive PIES_2017_Lab1_Pack.rar,
available from FER web page http://www.fer.unizg.hr/predmet/ppius, under the section „Laboratory
exercises“. This lab guideline will address above references at some points and it is advisable to read
referenced parts of the literature whenever further clarifications and instructions are needed.
1
Needed for the part of lab related to serial communication with personal computer.
2
Available from www.keil.com. Free version has 32kB limit on compiled code size (sufficient for lab exercises).
These instructions apply for version Keil MDK 5.21A. Other releases may have some differences which are not
covered in these instructions.
3
Available from http://www.st.com/en/embedded-software/stsw-stm32065.html
3
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
2.2. Adding support for STM32F4 family through the Pack Installer utility
After installation of Keil MDK-ARM uVision, the Pack Installer is automatically executed. It allows
installation of additional software support (libraries, headers etc.), targeting specific silicon vendor
ARM microcontroller family (so called "Device Family Pack" (DFP) packages). The Pack Installer main
window in shown in Fig 1.
On the left side there is a list (tree) of supported microcontroller families and devices, and on the
right side there are software components which can be installed for the corresponding selection in
device tree on the left side. If you click on „STM32F4 Series“ node as shown in Fig. 2 the right side list
will be populed with choices for software that can be installed via Pack Installer for that
microcontroller family. Among many possible choice for components, the minimum requirement is
to install Keil::STM32F4xx_DFP, which is a software package that contains all necessary libraries and
components for building programs for STM32F4 microcontroller family based targets. Action button
„Install“ indicates that Keil::STM32F4xx_DFP component have not been installed yet. Click on Install
button next to Keil::STM32F4xx_DFP to install the package (note: Internet connection must be active
to let the Pack Installer download appropriate files from online repository).
4
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
5
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Figure 3: Pack installer main window - after successful installation of Keil::STM32F4xx_DFP package
The result after successful installation of Keil::STM32F4xx_DFP component is shown in Fig. 3. - „Up to
date“ label indicates that the latest available version of selected component is successfully installed.
The location where DFP pack (Device Family Pack) is actually installed is:
C:\Keil_v5\ARM\PACK\Keil\STM32F4xx_DFP
That location contains everything needed to develop programs for STM32F4 family. Some of the
most important directories are explained in Tab 1. The content of DFP installation directory should
not be manually managed or altered.
Note: It is possible to download a DFP package file to local storage (HDD) and install it offline,
without Internet connection. DFP packs can be downloaded from http://www.keil.com/dd2/pack/.
DFP packs can be installed off-line by calling the option File-Import from the Pack Installer program
main menu.
Packer installer can be also executed from within Keil MDK environment by clicking the button shown
in Fig. 4.
6
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Directory Description
CMSIS programming interface4
\CMSIS
(device-independant layer)
\Drivers lower level hardware abstraction layers
\Drivers\BSP board support packages (BSP)
\Drivers\BSP\STM32F4-
BSP for STM32F4-Discovery board
Discovery
CMSIS programming interface
\Drivers\BSP\CMSIS
(device-dependant layer)
\Drivers\BSP\STM32F4xx_HAL_ STM32CubeF4
Driver (low-level hardware abstaction layer (HAL))
\MDK\Boards\ST\STM32F4-
Blinky example for STM32F4-Discovery board
Discovery\Blinky
\Projects\STM32F4- Software examples for STM32F4-Discovery board (various
Discovery\Examples peripherals)
Next step is to install necessary drivers and software support for running STM32F4DISCOVERY under
Windows environment5. More details about the board driver installation can be found in documents:
• UM1472 - Discovery kit for STM32F407.pdf
• STMicroelectronics Cortex-M4 Training STM32F407.pdf
4
CMSIS stands for Cortex Microcontroller Software Interface Standard – standardized, vendor-independent
hardware abstraction layer for the Cortex-M processor series, built on top of lower level hardware abstraction
libraries to provide unified programming interface for different manufacturers using the same ARM
architecture
5
All instructions will refer to Windows 7, 64 bit edition, unless otherwise noted. Keil IDE is not supported under
Linux or Mac OS X. It is advisable to use virtual machine with Windows OS if you use some of these operating
systems.
7
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Board drivers installation procedure is simple but care must be taken in the following steps:
1. Before plugging the STM32F4DISCOVERY board into computer for the first time, USB drivers
must be installed by running the stlink_winusb_install.bat (in administrator mode) from
C:\Keil_v5\ARM\STLink\USBDriver folder.
2. If you plug your board into computer before installing USB drivers as described in the
previous step, Windows OS will recognize the board but fail to install drivers properly,
indicating with message that drivers have not been installed properly; in this case you must
do the following:
o find improperly installed STM32F4DISCOVERY board drivers in Windows Device
Manager (device called ST-Link/V2),
o update USB driver pointing to the following location:
C:\Keil_v5\ARM\STLink\USBDriver
3. After these steps USB drivers for STM32F4DISCOVERY board should work properly.
4. It is advisable to upgrade board firmware (with board plugged into computer) by using the
utility C:\Keil_v5\ARM\STLink\ST-LinkUpgrade.exe; this step may be necessary in some cases
because uVision IDE will not work with older board firmware revisions.
Note: The STM32F4DISCOVERY board contains on-board ST-Link/V2 programming and debugging
interface, that can be used both for programming/debugging the on-board microcontroller and
external target (through the expansion connector by setting the appropriate jumpers, refer to
UM1472 - Discovery kit for STM32F407.pdf for more details about external target programming).
After following the steps above, we are ready to start developing firmware for STM32F4DISCOVERY
board. Before proceeding, it is advisable to verify that toolchain and drivers are properly installed
and the board is functional. First, we have to check whether we have installed appropriate DFP for
our microcontroller family. Right after installation, Keil MDK-ARM uVision does not support any
microcontroller family out-of-the-box. In this case, when you call Project - New Vision Project option
from the main menu you will see the following window:
8
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
On the left side there is a list of all installed supported microcontroller families. In this case the list is
empty because no DFP support has been installed. It is necessary either to manually run Pack
Installer from Keil MDK-ARM uVision installation folder or to press the icon (indicated by a red
rectangle in a figure below) to call it from IDE:
After installing DFP for STM32F4 family, the support for this type of microcontroller is added to Keil
MDK-ARM uVision environment. When you try to create a new project, the following window will
appear, showing that now we have support for the STM32F4 family (if we develop firmware for
STM32F4DISCOVERY board, the part STM32F407VG must be chosen to match on-board processor):
However, instead of creating a new project at this point from scratch, we shall import an existing
working example just to verify that the board is functional. The easiest way to test whether the board
is functional is to import Blinky example from example folder as denoted in Tab. 1:
..\MDK\Boards\ST\STM32F4-Discovery\Blinky
9
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
4. Select Project – Rebuild all target files... option:
10
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
5. Upon a successful project build, the target board must be programmed. Before programming, the
programmer and debugger interface must be configured. Select Flash - Configure Flash Tools...
option:
Make sure that options in Debug tab are set as shown in Fig. 8:
11
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Notes:
• Use ST-Link debugger - otherwise program will run in simulator; make sure that ST-Link
debugger is chosen from list because this is the hardware interface available on board (it will
not work with any other interface from the drop-down combo),
• Load Application at Startup, Run to Main
Next, connect the board to computer, click on Settings button and check "Debug" tab settings. The
window should look like shown in Fig. 9:
Make sure your device is recognized (code in SW Device window) and that you use SW port (not
default JTAG!). Next check the setting on "Flash Download" tab:
12
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
If Programming Algorithm list is empty, your board will not be programmed and you will receive an
error message. In this case you must provide programming algorithm for your board by clicking Add
button on Flash Download tab and selecting STM32F4xx Flash/1M/On-chip Flash:
Next, check the settings on Utilites tab of Flash - Configure Flash Tools... option:
Make sure the Update Target before Debugging option is checked, otherwise your target board will
not automatically be updated (reprogrammed) before every debugging session. Call File - Save All
option to ensure everything is saved.
13
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Now run option Debug - Start/Stop Debug Session (Ctrl + F5):
...
Program Size: Code=10888 RO-data=592 RW-data=84 ZI-data=3348
".\Flash\Blinky.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed: 00:00:05
Load "C:\\Keil_v521A\\ARM\\PACK\\Keil\\STM32F4xx_DFP\\2.10.0\\MDK\\Boards\\ST\\STM32F4-
Discovery\\Blinky\\Flash\\Blinky.axf"
Erase Done.
Programming Done.
Verify OK.
Application running ...
Flash Load finished at 10:04:25
After running the program, it will pause at the first statement in main() function, as shown in Fig. 12.
In this example this statement is osKernelInitialize(), which initializes CMSIS-RTOS (real-time
operating system) that is used to make the example. In this lab exercise we shall not use CMSIS-
RTOS, but for purpose of testing the board we shall use this RTOS-based Blinky example just to check
the whole toolchain is running fine.
14
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
At this point everything works but program is halted. Just press F5 to start program execution and
observe the board.
If LEDs are flashing and rotating - congratulations, you may proceed to lab exercises!
Note: you can use breakpoints to stop program execution, watch variable values etc. More
information on these debugging features can be found in Getting Started - uVision 5 MDK.pdf.
15
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
A new project is created by clicking Project - New Vision Project and providing the project name and
folder. In a process of project file creation, you must choose the exact microcontroller part you will
use (STM32F407VG6 in case of STM32F4DISCOVERY board):
Then the next window appears where you can select various program components (managed run-
time environment):
6
Microcontroller part can be chosen only if an appropriate DFP package has been installed
16
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
In managed run-time environment window you can select what software components and libraries
you need for development of your application. Some of components that you may include are the
following:
• Board Support - program support for peripherals, LEDs, buttons etc. for various development
boards (BSP (board support package) level libraries); in this case STM32F4 Discovery Kit
board can be selected,
• CMSIS - Cortex® Microcontroller Software Interface Standard - vendor-independent hardware
abstraction layer (HAL) for Cortex-M microcontrollers; it simplifies and unifies software
development cycle with an emphasis on cross-platform portability of code among different
silicon vendors; this is high-level library on a layer above vendor-specific HAL libraries; the
CMSIS layer will not be used in this laboratory exercise,
• Device / Startup / STM32Cube Framework (API) / STM32Cube HAL - hardware abstraction
layer (HAL) targeting STM32F4 family; this layer is vendor-specific (STMicroelectronics) and
provides high-level access to microcontroller resources and peripherals without a need for
individual register access programming7; this laboratory exercise will focus on vendor-specific
HAL-level ARM programming (but provided from different library, as explained later),
• Drivers, File System, Graphics, Network, USB - advanced libraries and services that will not be
covered in this lab.
Run-time environment manager enables easy inclusion of various libraries and services, while taking
care of all dependancies and helping user to select all needed libraries for the solution.
This laboratory exercise will introduce HAL programming paradigm for STM32F4 microcontroller
family8. In order to prepare project for using the STMCubeF4 HAL libraries, the following components
must be selected:
CMSIS
Core
Device
Startup
Device
STM32Cube Framework (API)
Classic
Device
STM32Cube HAL
Common
Cortex
GPIO
PWR
RCC
7
HAL-level programming paradigm (using high-level C peripheral driver functions) provides an extra layer of
abstraction compared to the register-level programming paradigm (using direct hardware register access, that
is commonly encountered in firmware development for microcontrollers, especially for 8- and 16-bit families);
register-level approach results in smaller and more optimized code, but HAL-level approach speeds up the
development and reduces chances for introducing bugs; these paradigms are not mutually exclusive and they
can be freely mixed within the same project as necessary
8
Although the rest of the chapter shows how to setup STMCubeF4 firmware package HAL in the last revision of
Keil MDK-ARM uVision (to demonstrate how to start a new project in uV5), this lab exercise will not use
STMCubeF4 HAL, and the guidelines will be based on an older HAL library (STM32F4xx Standard Peripheral
Library), for the reasons explained later
17
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
These components are required to write simple minimum program example, e.g. one that use only
GPIO pins. After clicking OK button, the following files are shown in Project window:
Files marked with small yellow mark are linked files in Keil uVision IDE installation (DFP pack), while
other files
RTE_Device.h
startup_stm32f407xx.s
stm32f4xx_hal_conf.h
system_stm32f4xx.c
18
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
-ID:/Keil_v521A/ARM/PACK/Keil/STM32F4xx_DFP/2.10.0/Drivers/STM32F4xx_HAL_Driver/Inc
-D__UVISION_VERSION="521" -D_RTE_ -DSTM32F407xx --C99
-o .\Objects\*.o --omf_browse .\Objects\*.crf --depend .\Objects\*.d
This kind of information is usually managed by makefiles or command-line arguments, but run-time
manager helps developers to track all dependancies more easily. On this tab one can e.g. define
preprocessor symbols, add additional include paths (for libraries not controlled by run-time manager)
etc.
After selecting the appropriate components, programming and debugging interface must be
configured through the Flash - Configure Flash Tools... option (refer to notes given in chapter
Verifying system installation and running an example).
At this point, we still cannot build an executable because we do not have minimum amount of
application code (C source file with main() function - predefined entry point symbol needed by
linker). So the next step is to add main.c to the project.
Create new empty file (File - New), click File - Save As... and name the file main.c (save the file in
project root folder - same location as *.uvprojx). Please note that newly created *.c file will not be
automatically included in your project build (if you try building the project at this point linker will
complain that it cannot find main() function). You need to add main.c by right-clicking Source Group 1
node and selecting Add Existing Files to Group... option:
19
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
After adding the file it will appear in the source tree:
int main()
{
int i;
i = 10;
while(1);
}
Press F7 to Build target. Set breakpoint to line "i = 10;" (either by positioning cursor in that line and
pressing F9 or double clicking on pane on the left side of the code window - red dot should appear):
Press Ctrl + F5 to program target and run program - program should run and hit the breakpoint.
20
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
At this point we have functional project that compiles, produces executable code, successfully
programss FLASH memory of microcontroller, and has the hardware debug capabilities checked by
hitting the breakpoint. This is the minimum environment setup that you need in order to start
making programs for microcontroller.
ST Microelectronics provides two HAL APIs9 for programming their Cortex-M microcontrollers:
As mentioned earlier, it is necessary first to install appropriate DFP in order to use a certain
microcontroller in a project in Keil MDK. For STM32F4 microcontroller family the official Keil DFP
comes bundled with STM32CubeF4 firmware package HAL library. This is the latest revision of HAL
library provided by STMicroelectronics for STM32F4 line of microcontrollers.
9
API - Application Program Interface
10
Link: http://www.st.com/en/embedded-software/stsw-stm32065.html
11
Link: http://www.st.com/en/development-tools/stm32cubemx.html
21
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
However, although the STM32CubeF4 firmware package HAL is well integrated into the latest Keil
MDK-ARM uVision releases through the run-time environment manager, there are some drawbacks
for using this library and run-time environment manager at this point:
• uVision run-time environment manager performs many „behind the scenes magic“ and
generating intermediate files that should not be altered by user, thus imposing non-standard
and proprietary workflow that is not compatible with other C compilers and integrated
development environments (such as widely used gcc compiler),
• this vendor imposed workflow is also toolset version dependant and „magic“ behind run-
time environment manager hides the mechanisms for building complex solutions composed
of many loosely-coupled libraries with large number of C files; before using such „point-and-
click“ automized building environment, students should get familiar with a process how to
setup a project from scratch and build environment manually, without a help of run-time
environment manager.
Therefore, this lab exercise we shall use older, but stable and well supported HAL layer library
STM32F4 Standard Peripheral Library instead of STM32CubeF4 firmware package HAL library, due to
codebase maturity, documentation, large number of community-provided examples, etc. We shall
not rely on a "magic" behind the run-time environment manager, and learn how to setup project
environment, resolving all dependancies from the scratch. The STM32F4 Standard Peripheral Library
provides support for all currently available STM32F4 family microcontrollers.
The easiest way to start using STM32F4 Standard Peripheral Library is to use project templates
contained in
Extract this folder from archive and load the project from \uVision\Template.uvproj project file. This
project template was built from scratch by using the source code provided from the library source.
How to make these templates yourself will be explained in further text.
The main advantage of this approach is that the project build does not depend on components
installed by Packer Installer on different machines. All needed libraries are under the project
template source tree and the project is portable between different machines. Otherwise, problems
with building the project on different machines may arise if appropriate run-time environment
components are not installed in a toolchain. It is not required to install STM32F4 Standard Peripheral
Library in toolchain on another target machine because everything is self contained in project
directory.
Template Empty Template - Src includes the whole STM32F4 Standard Peripheral Library in a source
code. The advantage of this approach is that one can easily see the library source code and adapt it in
case it is necessary (although it is not recommended to make any substantial changes on proven and
22
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
stable low-level library). The main disadvantage is that the rebuild all process re-compiles the whole
library every time from scratch, what is a time consuming process.
Template Empty Template - Lib includes the STM32F4 Standard Peripheral Library as a precompiled
library (LIB) that is statically linked with the rest of application source code. ST Microelectronics does
not provide prebuilt STM32F4 Standard Peripheral Library but it is easy to make one as it will be
explained later. The advantage of using precompiled library is drastically reduced build time when
rebuilding the project.
The next chapter will show how to build a project template from the scratch.
The first step is to create a new empty project based on the chosen STM32F4 microcontroller. Select
option Project - New Vision Project and select STM32F407VG microcontoller:
From Run-Time Environment window select only the component CMSIS - CORE (as shown in Fig. 13).
This component provides minimum infrastructure for building STM32F4xxx target.
At this point we did not include any HAL libraries for accessing peripherals.
Let the project root be \Template directory (at some arbitrary parent path) and the project filename
Template.uvprojx.
23
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
After accepting Run-Time Environment configuration, the Keil MDK window should look like this:
24
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Project folder looks like this:
The project now contains basic components for building STM32F4 targets.
Next step is to integrate the STM32F4 Standard Peripheral Library with project. Download and
extract en.stm32f4_dsp_stdperiph_lib.zip12 to some arbitrary location (e.g. C:\TempLib). Source tree
looks like shown in Fig. 14.
In a project source tree (assuming \Template is a project root folder) make a subfolder \Lib. Copy
folders and files from the extracted en.stm32f4_dsp_stdperiph_lib.zip to \Template\Lib, as described
in Tab. 2.
The only exception is a file stm32f4xx_conf.h in the last row, which can be copied directly to the
project root folder.
12
Available from http://www.st.com/en/embedded-software/stsw-stm32065.html, filename may differ from
denoted in text above in different versions; in this guidelines we refer always to revision 1.7.1.
25
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Table 2: Folders and files to be copied from STM32F4 Standard Peripheral Library source tree to project \Lib folder
\Libraries\CMSIS\Device\ST\STM32F4xx\Include stm32f4xx.h
system_stm32f4xx.h
\Libraries\CMSIS\Device\ST\STM32F4xx\Source\Tem system_stm32f4xx.c
plates
\Libraries\CMSIS\Device\ST\STM32F4xx\Source\Tem startup_stm32f40_41
plates\arm xxx.s
\Libraries\CMSIS\Include *.*
\Libraries\STM32F4xx_StdPeriph_Driver\inc *.h
\Libraries\STM32F4xx_StdPeriph_Driver\src *.c
\Project\STM32F4xx_StdPeriph_Templates stm32f4xx_conf.h
26
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Step 3 - Adding necessary files to your project build
\Lib\CMSIS\Device\ST\STM32F4xx\Source\Templates\arm
The assembly startup file is needed for proper initialization of the microcontroller before entering
the main() function.
In some cases the default stack size 0x0400 (1024 bytes) is not enough and this is the place where
the client programmer can set the stack size.
Adding files in the Project window is an important step because in this way the source files are
included in the build. Files residing in project tree are not automatically included in the build unless
you explicitely add them to the Project window as described! The action of adding file will
automatically update Keil internal make file.
13
It is not necessary to add to this group, this is just an example.
27
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Following the same procedure, add the files
\stm32f4xx_conf.h
\Lib\CMSIS\Device\ST\STM32F4xx\Include\system_stm32f4xx.h
\Lib\CMSIS\Device\ST\STM32F4xx\Source\Templates\system_stm32f4xx.c
indicates that a file has read-only attribute in the file system. It is not possible to edit this file to
change settings and save changes to disk. In order to make a file editable from IDE you must change
this attribute to read/write (from external program, e.g. via Windows Explorer or command line
interface).
Then add an empty file main.c to root folder \Template and Source Group 1.
28
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Next, we need to add STM32F4 Standard Peripheral Library source files. First, make new source
group called STM_Lib (right click on Target 1, Manage Project Items, add group in Groups list):
Now add all C sources of STM32F4 Standard Peripheral Library implementation to STM_Lib group
(right click + Add Existing Files, Shift + click to select all C sources from the directory
\Lib\STM32F4xx_StdPeriph_Driver\src):
Note: at this point it is advisable not to include the file stm32f4xx_fmc.c in the build yet (it can easily
be removed by right click + remove or added manually later if needed). This file contains
implementation of functions for managing external memory resources and it needs some
preprocesor directives to be defined, otherwise the project will not build. Since we do not use
external memory in this lab and we don't want to dig into details about specific settings for
preprocesor definitions for external memory controller, this file can be left out at the moment.
29
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Now all the necessary source files have been added to the project. However, if you try to build the
project at this point, you will get many compile errors because header files cannot be resolved. Since
we haven't install STM32F4 Standard Peripheral Library using Packer installer, the compiler cannot
automatically find the path(s) to library include files. Moreover, some preprocessor directives need
to be globally defined for all include files.
The next step is to set up build options by specifying include folders and other build parameters.
Two important fields must be set to properly configure the project build:
30
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
In Preprocessor Symbols box add the following definitions:
Preprocessor symbols are actually #define directives that are automatically applied to all header files
and C sources in the build, without a need to specify them manually for each file.
• STM32F40_41xxx – defines a microcontroller part used in target (this is a valid value for
STM32F407 microcontroller),
• USE_STDPERIPH_DRIVER – directive that controls whether the application code use HAL or
direct register access; must be defined when using STM32F4 Standard Peripheral Library,
• HSE_VALUE=8000000 - directive instructing the library code that the external crystal 8 MHz is
used (this is a correct value for STM32F4DISCOVERY board, it may differ for other boards);
HSE stands for high-speed external oscillator source; the library assumes the default crystal
25 MHz and without this directive all timings depending on the actual value of the on-board
crystal might not work properly (this especially applies to serial communication!).
These preprocessor symbols are needed to properly compile code using STM32F4 Standard
Peripheral Library.
This will add the following (relative) folders where compiler will look for include files:
.\
Lib\CMSIS\Include
Lib\CMSIS\Device\ST\STM32F4xx\Include
Lib\STM32F4xx_StdPeriph_Driver\inc
Note: „.\“ refers to a project root folder where the project definition file resides.
If include file search paths are not specified, the compiler will not know where to look for header
files.
31
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
}
main.c: 1 warning, 0 errors
compiling stm32f4xx_cec.c...
compiling system_stm32f4xx.c...
compiling misc.c...
compiling stm32f4xx_crc.c...
compiling stm32f4xx_cryp.c...
....
compiling stm32f4xx_wwdg.c...
linking...
Program Size: Code=676 RO-data=408 RW-data=0 ZI-data=1632
".\Objects\Template.axf" - 0 Error(s), 1 Warning(s).
Build Time Elapsed: 00:00:21
If you properly followed the instructions, the project should build without errors (except for possibly
some non-critical warnings).
Before trying the code on a development board, adjust the settings on Debug and Utilities tabs to use
STLINK/V2 debuger interface as explained before. Program the target board and run the code as
previously explained. The result is shown in Fig. 16.
If you are able to run program on a target development hardware - congratulations! At this point
now you have fully functional project template which is ready to control microcontroller peripherals
through the HAL library as it will be described in subsequent chapters.
32
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
3.4. Creating project for static linking with STM32F4 Standard Peripheral
Library
During a development cycle, it is often necessary to run periodically a project rebuild operation.
Since the full source code of the STM32F4 Standard Peripheral Library in the previous chapter is
included in project, the project rebuild operation will force also rebuilding the whole library from
source, what is an unnecessary and time consuming step.
The problem of rebuilding the whole library from source each time when the project rebuild option is
invoked can be avoided if the STM32F4 Standard Peripheral Library is statically linked with the
application code. Application code can be statically linked against object code library (*.lib file),
which can be built in a similar way like normal executable code as explained earlier. STM32F4
Standard Peripheral Library distribution contains the source code, but not prebuilt *.lib files that can
be added to project for static linking. Therefore, first we must build STM32F4 Standard Peripheral
Library and then include it in our project template. Both steps will be explained in details in this
chapter.
Step 1 – Creating STM32F4 Standard Peripheral Library (*.lib) project and building library
Prepare new project template by repeating all steps as decribed in chapter 3.3, respecting the
following differences:
• change the project Output from „Create Executable“ to „Create Library“, as shown in Fig. 17.
• remove all files from „Source Group 1“ which are shown in Fig. 15.
33
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
The easiest way to quickly set up library project is to copy project created in chapter 3.3, change the
output type and remove unnecessary files from the build:
startup_stm32f40_41xxx.s
stm32f4xx_conf.h
system_stm32f4xx.h
system_stm32f4xx.c
main.c
The project window should look like this (without files in Source Group 1 group):
Upon successful build a library file Template.lib will be created as an output in .\Objects folder. This
library file contains relocatable object code of the whole STM32F4 Standard Peripheral Library, which
can be included in another project for static linking against the application code, without a need to
rebuild the whole library from source each time the project is rebuilt.
Note: If you build a library without specifying correct value of external crystal (HSE_VALUE=8000000),
serial communication in target application using the *.lib may not work properly.
34
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Step 2 – Creating project template that uses static linking with STM32F4 Standard Peripheral Library
Prepare new project template by repeating all steps as decribed in chapter 3.3, respecting the
following differences:
• remove all C files from STM_Lib source group from build; you can also safely delete the
whole subfolder:
\Lib\STM32F4xx_StdPeriph_Driver\src
since we do not need C implementation files anymore because that code will be included in
*.lib file
• copy *.lib file built in a previous step to the project root folder (or other location under the
source tree) (for example Template.lib or you can rename file to reflect content and version,
e.g. STM_Peripheral_Lib_v1.7.1.lib)
By examining the build output one can notice that no longer all library source files are rebuilt and
that build time is now much faster.
35
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
The prebuilt project template is available in course materials PIES_2017_Lab1_Pack.rar | Empty
Template – Lib as a reference of the final result. This project template is useful for starting new
projects. The project build does not depend on components managed by Run-Time Environment
Manager or Packer Installer on local machine and it will build consistently among machines with
different toolchain configuration.
Tip: If you use the project template with full HAL library source code or the project template for
building the library, you will have an easy access to the whole HAL API through the Keil MDK-ARM
uVision IDE:
If you click on Functions tab in a Project window, the tree structure (kind of "function browser") will
be shown, with a list of all functions contained in each C source file. By expanding each node (C
source) you can see all functions in that file, and access the source code of each function by double-
clicking on it. Functions tab provides quick and convenient way to learn library API or just to quickly
browse your application code. This enables programmer to easily see how HAL functions are
implemented, which registers they access, what defines and structures are defined in the library,
how to use the function parameters and library modules etc. It is also very useful to read comments
at the beginning of each C file because it contains a short overview and guidelines how to use each
module in HAL library.
In all subsequent examples, it is assumed that you use either one of the project templates described
above, or you make your own based on the same procedure.
36
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Assignment:
Make Blinky program that will use HAL-level library (STM32F4 Standard Peripheral Library) to blink
on-board LEDs.
Guidelines:
The easiest way to create a new project is to copy one of project templates described in previous
chapter to some folder (e.g. ..\Lab_GPIO). Add an empty main.c file to your project root folder.
In order to control the on-board LEDs, it is first necessary to determine to which GPIO pins these
LEDs are connected. STM32F4DISCOVERY board user manual14 defines LEDs positions on page 16. We
are concerned with four user-controlled LEDs:
Each microcontroller program must not exit the main() function because there is no other program
environment (e.g. OS) where this function should "return" to. Moreover, the microcontroller
program without interaction with peripherals rarely can serve some useful purpose so the next step
would be to add support for GPIO peripheral, as an example of the simplest peripheral.
14
UM1472 - Discovery kit for STM32F407.pdf
37
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Programming each microcontroller requires reading a great deal of literature before even getting
started. Relevant datasheets are usually organized in a way that there is (a) user guide covering the
microcontroller family15, (b) datasheet for specific microcontroller part16, and (c) the programming
manual17. These manuals and datasheets describe microcontroller hardware and low-level
programming interface, without any references to HAL library. This is the prerequisite to understand
how to use the HAL library, which is described in a separate manual18.
Although the amount of information in all mentioned relevant documents can be intimidating on a
first sight, these materials do not have to be read completely before getting started. A good way to
speed-up the initial application development is to study examples provided on installation path
x:\Keil_v5\ARM\Pack\Keil\STM32F4xx_DFP\2.2.0\Projects
It is also useful to take a look on Functions tab in a project template with the whole HAL library
included as a C source, to quickly learn the most important API functions.
In our new project, first we have to include HAL support for GPIO configuration. We shall do GPIO
initialization in gpio_init() function:
#include <stm32f4xx.h> // common stuff
#include <stm32f4xx_gpio.h> // gpio control
#include <stm32f4xx_rcc.h> // reset and clocking
void gpio_init()
{
}
int main(void)
{
gpio_init();
while(1);
}
It is intuitive and reasonable to assume that the header file stm32f4xx_gpio.h will contain all function
prototypes needed for GPIO control. However, as we shall see soon, it is also necessary to include
stm32f4xx_rcc.h header (Reset and clock control (RCC) peripheral), because each GPIO port (more
generally, every peripheral) has to be explicitely assigned a clock to be functional. Moreover, a
common header containing important definitions stm32f4xx.h should always be included.
Most of simple microcontrollers (such as 8051, AVR, PIC, MSP430 etc.) are easily configured simply
by writing hexadecimal values into corresponding hardware registers. Such approach is possible with
ARM microcontrollers too, but since ARM microcontrollers are more complex, the register-level
application programming approach tends to be tedious, time-consuming and error-prone process.
Thus, HAL-level library is a preferred way of application development that provides faster
development with less bugs. STM32F4 HAL libraries (both STM32 Cube and STM32F4 Standard
15
RM0090 Reference manual STM32F405xx/07xx, STM32F415xx/17xx, STM32F42xxx and STM32F43xxx
advanced ARM®-based 32-bit MCUs (STM32F4 Reference manual.pdf)
16
STM32F405xx / STM32F407xx datasheet (STM32F407VG.pdf)
17
PM0214 Programming manual STM32F3 and STM32F4 Series Cortex®-M4 programming manual (STM32F3
and STM32F4 Series Cortex-M4 programming manual.pdf).
18
UM1061 Description of STM32F2xx Standard Peripheral Library (Description of STM32F2xx Standard
Peripheral Library.pdf)
38
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Peripheral Library) are open-source and user can see what is going on „behind the scenes“. When
necessary, it is possible to mix HAL- and register-level programming style.
Let us take a closer look on gpio_init() function. First we shall configure the orange LED GPIO pin
control. After the reset, all GPIO pins are in inactive state (open drain) and GPIO ports are not
clocked. We have to (a) provide clock for the chosen GPIO port, and (b) configure the chosen pin
settings. This is achieved by the following code:
void gpio_init()
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOD, &GPIO_InitStruct);
}
Let us see what each part of code above means. As one can notice, we do not access processor
registers directly, neither we write any "magic" hex number constants to these registers, and we use
the high level HAL approach instead. First let us see the definition of GPIO_InitTypeDef structure (in
header file stm32f4xx_gpio.h), which is rather self-explanatory:
typedef struct
{
uint32_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */
GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIOMode_TypeDef */
GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIOSpeed_TypeDef */
GPIOOType_TypeDef GPIO_OType; /*!< Specifies the operating output type for the selected pins.
This parameter can be a value of @ref GPIOOType_TypeDef */
GPIOPuPd_TypeDef GPIO_PuPd; /*!< Specifies the operating Pull-up/Pull down for the selected pins.
This parameter can be a value of @ref GPIOPuPd_TypeDef */
} GPIO_InitTypeDef;
typedef enum
{
GPIO_Mode_IN = 0x00, /*!< GPIO Input Mode */
GPIO_Mode_OUT = 0x01, /*!< GPIO Output Mode */
GPIO_Mode_AF = 0x02, /*!< GPIO Alternate function Mode */
GPIO_Mode_AN = 0x03 /*!< GPIO Analog Mode */
} GPIOMode_TypeDef;
typedef enum
{
GPIO_Speed_2MHz = 0x00, /*!< Low speed */
GPIO_Speed_25MHz = 0x01, /*!< Medium speed */
GPIO_Speed_50MHz = 0x02, /*!< Fast speed */
GPIO_Speed_100MHz = 0x03 /*!< High speed on 30 pF (80 MHz Output max speed on 15 pF) */
} GPIOSpeed_TypeDef;
typedef enum
{
GPIO_OType_PP = 0x00,
GPIO_OType_OD = 0x01
} GPIOOType_TypeDef;
typedef enum
{
GPIO_PuPd_NOPULL = 0x00,
GPIO_PuPd_UP = 0x01,
GPIO_PuPd_DOWN = 0x02
} GPIOPuPd_TypeDef;
39
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Pins that are used in a structure are defined via pin masks:
#define GPIO_Pin_0 ((uint16_t)0x0001) /* Pin 0 selected */
#define GPIO_Pin_1 ((uint16_t)0x0002) /* Pin 1 selected */
#define GPIO_Pin_2 ((uint16_t)0x0004) /* Pin 2 selected */
#define GPIO_Pin_3 ((uint16_t)0x0008) /* Pin 3 selected */
#define GPIO_Pin_4 ((uint16_t)0x0010) /* Pin 4 selected */
#define GPIO_Pin_5 ((uint16_t)0x0020) /* Pin 5 selected */
#define GPIO_Pin_6 ((uint16_t)0x0040) /* Pin 6 selected */
#define GPIO_Pin_7 ((uint16_t)0x0080) /* Pin 7 selected */
#define GPIO_Pin_8 ((uint16_t)0x0100) /* Pin 8 selected */
#define GPIO_Pin_9 ((uint16_t)0x0200) /* Pin 9 selected */
#define GPIO_Pin_10 ((uint16_t)0x0400) /* Pin 10 selected */
#define GPIO_Pin_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_Pin_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_Pin_13 ((uint16_t)0x2000) /* Pin 13 selected */
#define GPIO_Pin_14 ((uint16_t)0x4000) /* Pin 14 selected */
#define GPIO_Pin_15 ((uint16_t)0x8000) /* Pin 15 selected */
Before considering how to use these typedefs and defines, let us see the function call
RCC_AHB1PeriphClockCmd:
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
This function is contained in module stm32f4xx_rcc.c. If we open this source file, we shall see the
function definition:
/**
* @brief Enables or disables the AHB1 peripheral clock.
* @note After reset, the peripheral clock (used for registers read/write access)
* is disabled and the application software has to enable this clock before
* using it.
* @param RCC_AHBPeriph: specifies the AHB1 peripheral to gates its clock.
* This parameter can be any combination of the following values:
* @arg RCC_AHB1Periph_GPIOA: GPIOA clock
* @arg RCC_AHB1Periph_GPIOB: GPIOB clock
* @arg RCC_AHB1Periph_GPIOC: GPIOC clock
* @arg RCC_AHB1Periph_GPIOD: GPIOD clock
* @arg RCC_AHB1Periph_GPIOE: GPIOE clock
* @arg RCC_AHB1Periph_GPIOF: GPIOF clock
* @arg RCC_AHB1Periph_GPIOG: GPIOG clock
* @arg RCC_AHB1Periph_GPIOG: GPIOG clock
* @arg RCC_AHB1Periph_GPIOI: GPIOI clock
* @arg RCC_AHB1Periph_CRC: CRC clock
* @arg RCC_AHB1Periph_BKPSRAM: BKPSRAM interface clock
* @arg RCC_AHB1Periph_CCMDATARAMEN CCM data RAM interface clock
* @arg RCC_AHB1Periph_DMA1: DMA1 clock
* @arg RCC_AHB1Periph_DMA2: DMA2 clock
* @arg RCC_AHB1Periph_ETH_MAC: Ethernet MAC clock
* @arg RCC_AHB1Periph_ETH_MAC_Tx: Ethernet Transmission clock
* @arg RCC_AHB1Periph_ETH_MAC_Rx: Ethernet Reception clock
* @arg RCC_AHB1Periph_ETH_MAC_PTP: Ethernet PTP clock
* @arg RCC_AHB1Periph_OTG_HS: USB OTG HS clock
* @arg RCC_AHB1Periph_OTG_HS_ULPI: USB OTG HS ULPI clock
* @param NewState: new state of the specified peripheral clock.
* This parameter can be: ENABLE or DISABLE.
* @retval None
*/
This function serves to enable or disable clock for some peripheral connected to AHB1 bus interface.
Since the orange LED is connected to the Port D, which is connected to the AHB1 bus, we need to use
this function to provide peripheral clock. Please note that there are also other peripheral clock
enable functions, depending on bus interface:
void RCC_AHB2PeriphClockCmd(uint32_t RCC_AHB2Periph, FunctionalState NewState)
void RCC_AHB3PeriphClockCmd(uint32_t RCC_AHB3Periph, FunctionalState NewState)
void RCC_AHB3PeriphClockCmd(uint32_t RCC_AHB3Periph, FunctionalState NewState)
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState) etc.
40
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
These functions have exactly the same meaning but they affect peripherals connected to their
respective bus interfaces. For example, Port D cannot be enabled clock with function
RCC_AHB2PeriphClockCmd because it is not connected to AHB2 interface. For each peripheral consult
the microcontroller documentation and/or comments in stm32f4xx_rcc.c to determine to which bus
interface every peripheral is connected.
In function call
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
Almost all function arguments in HAL rely either on custom typedef structures or symbolic constants
defines like in this case. Typdefs and constants are defined in header files (along with API function
interfaces), while function implementations are in C module files (along with detailed comments how
to use these functions).
Hint: Although the document "UM1061 Description of STM32F2xx Standard Peripheral Library"
provides detailed definition of the whole HAL API, it is somewhat easier to locate functions and
explanations using project template and Functions tab in Keil MDK IDE.
After enabling the clock to Port D, it is necessary to initialize GPIO descriptor structure:
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
In this case we want to initialize pin PD13 as output, push-pull type (instead of open-drain), with no
internal pull-up or pull-down resistor. We set fast I/O speed (50 MHz setting). Slower speeds are
desirable in cases when speed is not needed to mitigate electromagnetic intereference (EMI) issues
due to fast rise time of output signal.
41
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
After the settings in GPIO descriptor structure are configured, we apply them to the chosen GPIO
port (Port D in this case) and pin (pin 13 ) using the call to GPIO_Init API function:
GPIO_Init(GPIOD, &GPIO_InitStruct);
The first argument is of type GPIO_TypeDef. Data type GPIO_TypeDef can be found in header
stm32f4xx.h:
typedef struct
{
__IO uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */
__IO uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */
__IO uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */
__IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */
__IO uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */
__IO uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */
__IO uint16_t BSRRL; /*!< GPIO port bit set/reset low register, Address offset: 0x18 */
__IO uint16_t BSRRH; /*!< GPIO port bit set/reset high register, Address offset: 0x1A */
__IO uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */
__IO uint32_t AFR[2]; /*!< GPIO alternate function registers, Address offset: 0x20-0x24 */
} GPIO_TypeDef;
The C structure GPIO_InitTypeDef contains logical fields which are other typedefs, enums or defines,
and has not direct connection with microcontroller hardware registers. The meaning of
GPIO_TypeDef structure is slightly different as it maps directly to hardware addresses of registers
controlling GPIO port. The GPIO_TypeDef structure contains an "addressing template" with
appropriate offsets and enables access to hardware registers conveniently through the C struct
fields. Since e.g. all port control register banks have the same structure in memory address space, it
is sufficient to provide base address of each port control register bank to map appropriate port with
symbolic identifier. For example, the port D is accessed through the GPIO_TypeDef-typed pointer on
predefined address:
In code, the symbol GPIOD refers to the typed pointer pointing to the address GPIOD_BASE, which is
defined as:
/*!< Peripheral base address in the alias region */
#define PERIPH_BASE ((uint32_t)0x40000000)
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000)
#define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00)
HAL API provides convenient mechanism of abstraction and relieves the programmer a burden of
calculating the addresses of each individual register by hand. The same principles are used in all
other parts of the HAL library.
42
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOD, &GPIO_InitStruct);
}
we can see that it would be rather tedious and error-prone to change e.g. LED location from PD13 to
PA10. The problem is even more emphasized when the number of GPIO pins rises. Therefore, it is a
good programming practice to avoid any hard-coded constants in your application code and move all
hardware-specific references to #define statements. Let us consider the following code:
#include <stm32f4xx_gpio.h> // gpio control
#include <stm32f4xx_rcc.h> // reset anc clocking
void gpio_init()
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_AHB1PeriphClockCmd(LED3_ORANGE_RCC_GPIOx, ENABLE);
GPIO_InitStruct.GPIO_Pin = LED3_ORANGE_PinNumber;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(LED3_ORANGE_GPIOx, &GPIO_InitStruct);
}
int main(void)
{
gpio_init();
while(1);
}
The code is almost the same like in the previous version but nothing in application code is now hard-
coded. There is a single place where you change your port pin definitions. For example, if you want to
move orange LED to some external LED connected on PA10, this can be achieved without affecting
the application code at all, only #define statements:
#define LED3_ORANGE_RCC_GPIOx RCC_AHB1Periph_GPIOA
#define LED3_ORANGE_GPIOx GPIOA
#define LED3_ORANGE_PinNumber GPIO_Pin_10
The application code is unaffected by this change and program is now easy to modify and maintain.
4.5. Step 5: Appropriate use of header files and module decomposition to build
larger programs
It is a good programming practice to keep different functional parts of programs separated. Each
module should have well defined functionality and consistent interface for easy interfacing with
43
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
other modules. We shall demonstrate how to decouple GPIO initialization from the main program
module and how to import this functionality into main module.
First we need to add to project two source files main.c (main application) and gpio.c (GPIO module).
In order to consistently reference C modules among each other, the accompanying header files
main.h and gpio.h have to be created and added to the project. It is good practice to keep C sources
and headers under different source groups, as shown in example in Fig. 18:
The application headers (gpio.h, main.h) are placed in Header source group, while application
implementation files (gpio.c, main.c) are placed in Source group. System files (stm32f4xx_conf.h,
system_stm32f4xx.h, system_stm32f4xx.c and startup_stm32f0_41xxx.s) are moved to BSP (Board
Support Package) group to keep them separated from the application code. The reference to linked
library (STM_Peripheral_Lib_v1.7.1.lib) is placed into STM_Lib source group.
44
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Code in header files and C modules is given below:
/* gpio.h */
#ifndef GPIO_H
#define GPIO_H
void gpio_init(void);
#endif
/* main.h */
#ifndef MAIN_H
#define MAIN_H
#endif
/* gpio.c */
#include <gpio.h>
void gpio_init()
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_AHB1PeriphClockCmd(LED3_ORANGE_RCC_GPIOx, ENABLE);
GPIO_InitStruct.GPIO_Pin = LED3_ORANGE_PinNumber;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(LED3_ORANGE_GPIOx, &GPIO_InitStruct);
RCC_AHB1PeriphClockCmd(LED4_GREEN_RCC_GPIOx, ENABLE);
GPIO_InitStruct.GPIO_Pin = LED4_GREEN_PinNumber;
GPIO_Init(LED4_GREEN_GPIOx, &GPIO_InitStruct);
RCC_AHB1PeriphClockCmd(LED5_RED_RCC_GPIOx, ENABLE);
GPIO_InitStruct.GPIO_Pin = LED5_RED_PinNumber;
GPIO_Init(LED5_RED_GPIOx, &GPIO_InitStruct);
RCC_AHB1PeriphClockCmd(LED6_BLUE_RCC_GPIOx, ENABLE);
GPIO_InitStruct.GPIO_Pin = LED6_BLUE_PinNumber;
GPIO_Init(LED6_BLUE_GPIOx, &GPIO_InitStruct);
/* main.c */
#include <main.h>
int main(void)
{
gpio_init();
while(1);
}
45
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
The header file gpio.h contains all definitions and function prototype declarations that we want to
use in external modules. The structure
#ifndef GPIO_H
#define GPIO_H
...
#endif GPIO_H
is called include guards, which prevents recursive inclusion of the same header file within a single
translation unit (C file). By convention, at the beginning of the file it is checked whether the symbol
(<uppercase(filename)>_H) has been already encountered in the current translation unit, thus
preventing the collision with already parsed identifiers. Lack of include guards would require very
careful use of include files, and make it even impossible to resolve complex include schemes in larger
libraries and programs in many cases. Next, we include all header files needed for translation unit
gpio.c:
#include <stm32f4xx.h> // common stuff
#include <stm32f4xx_gpio.h> // gpio control
#include <stm32f4xx_rcc.h> // reset anc clocking
When gpio.c includes gpio.h, it will automatically include these referenced header files. The block of
#define statements:
#define LED3_ORANGE_RCC_GPIOx RCC_AHB1Periph_GPIOD
#define LED4_GREEN_RCC_GPIOx RCC_AHB1Periph_GPIOD
#define LED5_RED_RCC_GPIOx RCC_AHB1Periph_GPIOD
#define LED6_BLUE_RCC_GPIOx RCC_AHB1Periph_GPIOD
extends the definition of the on-board LEDs, to include all four LEDs present on STM32F4DISCOVERY
development board. Finally, the line
void gpio_init();
is not only the forward declaration of function gpio_init() in gpio.c, it also serves to any module that
includes gpio.h to know declaration of gpio_init() function. The actual function code is not necessary
for compilation (only function declaration from header), and actual machine code for the function
will be resolved by linker either from the source code (if C file is present in a project) or precompiled
static library (depending on project configuration).
to automatically reference all needed STM32F4 HAL functions, peripheral definitions (provided by
#define statements) and forward function declarations (this is useful on a module-level only if some
module functions uses them before function body implementations in C source file).
46
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
The include file main.h has include guards and the following include statements:
#include <stm32f4xx.h> // common stuff
#include <stm32f4xx_gpio.h> // gpio control
#include <stm32f4xx_rcc.h> // reset anc clocking
#include <gpio.h>
The first three includes are reduntant in this case because they are already present in gpio.h includes.
However, if we rely on gpio.h inclusion, use HAL functions, and at some later point we decide not to
use gpio.h anymore, then we need to bring back two first lines to reference back HAL library. The
shown approach is more robust and independant on whether we remove some header include later
or not. Thanks to include guards, it does not matter that we include the same two header files in
gpio.h, because include guards are present in all header files of STM32F4 HAL library.
The structure of the main.c program is now much more clear and oriented to application logic:
#include <main.h>
int main(void)
{
gpio_init();
while(1);
}
It is good practice to decouple peripheral access from program logic and contain all code accessing
the peripherals in separate C modules to keep application logic clean of platform-specific code,
making it less error-prone and easier to maintain and port. This approach of building applications by
decoupling hardware access from program logic will be maintained in the rest of lab exercises.
At this point our program initialized GPIO to control four on-board LEDs. In the next step we shall
make the program to periodically turn on and off all four LED diodes. In gpio.h add the following
constants:
#define LED3_ORANGE_ID 1
#define LED4_GREEN_ID 2
#define LED5_RED_ID 3
#define LED6_BLUE_ID 4
These constants are user-defined identifiers of each on-board LED. Each LED will be turned on and off
by referencing the constant to driver function, instead of hard-coding individual pins to what the
LEDs are actually wired. Add the following function to gpio.h:
47
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
case LED6_BLUE_ID:
GPIO_WriteBit(LED6_BLUE_GPIOx, LED6_BLUE_PinNumber, bitValue);
break;
}
This function will turn ON and OFF the chosen LED, referenced by LED ID define. It uses HAL function:
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal)
where BitVal specifies the value to be written to the selected bit (Bit_RESET clears the port pin, and
Bit_SET sets the port pin). Note that any changes in pin assigment to LEDs can be changed in #defines
in gpio.h without affecting a single line of code in gpio.c implementation file.
Note that for integer values an explicit signed and size type identifier uint8_t is used. It is good
practice to clearly define for each integer value whether it is signed or unsigned, and what size is it.
Definitions of integer types are contained in stdint.h, which is already included in stm32f4xx.h.
gpio_init();
while(1)
{
for(i=0;i<1000000;i++);
gpio_led_state(LED3_ORANGE_ID, 1); // turn on
gpio_led_state(LED4_GREEN_ID, 1); // turn on
gpio_led_state(LED5_RED_ID, 0); // turn off
gpio_led_state(LED6_BLUE_ID, 0); // turn off
for(i=0;i<1000000;i++);
gpio_led_state(LED3_ORANGE_ID, 0); // turn off
gpio_led_state(LED4_GREEN_ID, 0); // turn off
gpio_led_state(LED5_RED_ID, 1); // turn on
gpio_led_state(LED6_BLUE_ID, 1); // turn on
}
The code will alternatively turn on and off pairs of on-board LEDs. Application code in main.c is
completely decoupled from any platform-specific hardware calls. Any changes in hardware wiring to
LEDs are configured in #define statements in gpio.h at a single point, without affecting both function
implementation code in gpio.c and application logic in main.c. Pauses for blinking LEDs are
implemented by simple for loops that provide no strict timings but this shortcoming will be corrected
soon in the next chapter.
48
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
• working Keil MDK-ARM uVision Blinky project, following previously elaborated guidelines;
the project must be buildable on any computer with installed Keil MDK uVision IDE (uV5),
and must not rely on installed HAL library support on local Keil installation (i.e. with HAL
library must be embedded in project, as explained),
• the project must be ready to be transferred to STM32F4DISCOVERY development board
connected to a demonstration computer and ready for practical demonstration,
• students must understand all steps in building the solution and must be able to demonstrate
individual steps on demand.
Copying other students' solutions without understanding how they work will be properly
sanctioned!
49
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Assignment:
Make on-board LEDs to blink in programmable time intervals. Use timer peripheral to the measure
time periods accurately and interrupts to trigger actions on timer overflow event.
Guidelines:
The previous chapter described in details how to structure a program and use HAL libraries. The
following chapter(s) will describe only parts of solutions related to each specific topic, not repeating
all the steps for building the solution as explained earlier.
Following the description in previous chapter(s), create new HAL-based project and add files main.c,
gpio.c, and timer.c, with accompanying header files. The easiest way to accomplish this is to copy the
project from the previous chapter in a separate folder and add timer.h/c files. The project should
look like this:
Important: Assignments in each chapter must be implemented in separate uVision projects! This
means that "General-purpose input/output (GPIO) ports" part of the lab exercise is one uVision
project, while "Timers and interrupts" is another. You may use parts of the previous solutions for
assignments in subsequent chapters.
50
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
STM32F4 contains multiple types of timers with various advanced capabilities (see RM0090
Reference manual for STM32F4 microcontroller family for more details). In this exercise we shall use
only simple timer feature, which generates an overflow event that can be either polled or trigger an
interrupt. It is sufficient to use one of the general-purpose timers (TIM2-TIM5), which are described
in chapter 18 of RM0090 Reference manual.
/* timer.h */
#ifndef TIMER_H
#define TIMER_H
void timer2_init(void);
uint32_t timer2_get_millisec(void);
void timer2_wait_millisec(uint32_t ms);
#endif
This header file will provide an interface to our high-level timer API to be readily used in the main
program module, decoupling hardware implementation details from the application logic. In timer
module we need to additionally include stm32f4xx_tim.h header file, containing STM32F4 HAL
functions related to timers. Our tiny timer wrapper API will expose the following functions to other
modules:
• void timer2_init(void) - TIM2 timer initialization (along with helper variables); calling
this function for the first time will initialize and start timer, and any subsequent call will
restart the timer,
• uint32_t timer2_get_millisec(void) - number of milliseconds elapsed since the last
call of timer2_init() function,
• void timer2_wait_millisec(uint32_t ms) - blocking call for waiting predefined
number of milliseconds.
There are multiple ways to achieve the described functionality of the proposed API. In this example
we shall do the following:
• initialize TIM2 timer to overflow every 1 ms (one millisecond software timer resolution) and
generate an interrupt at each overflow,
• use module-level global variable to keep track of number of overflows; the variable will be
incremented in TIM2 interrupt service routine (ISR) each millisecond; this will enable to keep
track of time and provide an easy implementation of software timer for measuring arbitrary
time intervals with millisecond resolution,
• enable the main module to read the elapsed time since the last timer restart in a non-
blocking fashion,
• enable the main module to block execution for exact number of milliseconds.
51
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Let us first implement the function for timer configuration. Add the following code to timer.c source
file:
#include <timer.h>
uint32_t timer2_Ticks_Millisec;
void timer2_init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // init def struct for timer
NVIC_InitTypeDef NVIC_InitStructure; // init def struct for NVIC
uint16_t TimerPeriod; // 16-bit value because ARR register is 16-bit (although TIM2 is 32-bit!)
RCC_ClocksTypeDef RCC_Clocks; // for reading current clock setting - useful
uint32_t APB1_CLK; // APB1 clock - max. 42 MHz!
// Compute the value to be set in ARR register to generate signal frequency at 1.000 kHz (timebase 1 ms)
// Warning - 168 MHz - this is default processor core frequency but it is NOT freq. at which peripherals
// are clocked!
// TimerPeriod = (uint16_t)((SystemCoreClock / 10000 ) - 1);
// for 1 kHz division should be 1000 and ARR == 168000, but this cannot be represented with uint16;
// therefore, it is necessery to divide by 10x more (10000) with prescaler x10;
// to derive correct frequency, first check APB1 clock:
RCC_GetClocksFreq(&RCC_Clocks); // fill query struct
APB1_CLK = RCC_Clocks.PCLK1_Frequency; // WARNING: APB1 is "slow" peripheral interface that with
// SYSCLK = 168 MHz cannot be faster than 42 MHz (APB1 domain!) (pp. 213)
// by default it is set to 13.44 MHz, and 13.44 MHz : 1000 = 13440.0, what would be acceptable for reload
// without prescaler
// Page 213:
// The timer clock frequencies are automatically set by hardware. There are two cases:
// 1. If the APB prescaler is 1, the timer clock frequencies are set to the same frequency as
// that of the APB domain to which the timers are connected.
// 2. Otherwise, they are set to twice (×2) the frequency of the APB domain to which the
// timers are connected.
// => therefore, we need to multiplay APB1_CLK with 2:
TimerPeriod = (uint16_t)(((APB1_CLK * 2)/ 1000 ) - 1); // 1 kHz
First we need to include timer.h header and add the global variable:
uint32_t timer2_Ticks_Millisec;
This variable is global within the timer.c module (i.e. it is visible to all functions contained in a timer.c
compilation unit) but it will not be available to functions outside of timer.c module (because we did
not put definition of timer2_Ticks_Millisec in timer.h header). This is intended behaviour because we
do not want external module to directly access the variable that keeps track of time within timer.c
module.
52
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
However, if we define global variable with the same name in some other module (e.g. in main.c), the
program will not compile and linker will complain about multiple variable definitions with the same
name.
There are situations when we want to let other modules to access the global variable contained in
some compilation unit. If we wanted to export timer2_Ticks_Millisec variable and let some other
module to use it, in timer.h header file we should declared it like this:
/* timer.h */
#ifndef TIMER_H
#define TIMER_H
...
extern uint32_t timer2_Ticks_Millisec;
...
#endif
All modules that use timer.h header will now see global variable timer2_Ticks_Millisec and will be
able to access it from their function code. It is very important to use extern keyword in header file
because it will prevent allocating storage for variable - the extern keyword serves only to declare the
symbol name. Although it would be possible to manually add the line
in each module that wants to see timer2_Ticks_Millisec variable, this is not a good programming
practice because all symbol name exports (whether they are variables or functions) should be kept in
the accompanying header file. Another rule is that header files should never allocate any storage,
what is achieved by enforcing extern keyword for each variable export declaration.
Next we consider the timer2_init() TIM2 initialization routine. Just like in GPIO example, we shall use
two helper structs of custom type TIM_TimeBaseInitTypeDef and NVIC_InitTypeDef to initialize TIM2
timer and NVIC interrupt controller block for TIM2, respectively. The first step is to enable clock to
TIM2 peripheral which is connected to APB1 bus interface:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // enable TIM2 peripheral clock
Since we shall use the ISR, we must ensure that TIM2 interrupt is disabled before we configure
interrupts:
TIM_ITConfig(TIM2, TIM_IT_Update, DISABLE); // ensure that interrupt is disabled
TIM2 will be used in counter mode (counting up) and configured to overflow every 1 ms. In order to
provide the correct registers values, first we need to determine the speed at which TIM2 peripheral
is clocked. System clock (SYSCLK) is set during the processor initialization stage in file
system_stm32f4xx.c. The function SetSysClock() contains the code to set the system clock. By default,
the processor core runs at the maximum frequency 168 MHz19. However, this is not the frequency
that clocks all parts of the microcontroller. The chapter 7 "Reset and clock control for STM32Fxx" in
family user guide describes the clock system.
19
Care must be taken because default values in STM32F4 Standard Peripheral Library will produce system clock
of 168 MHz only for external crystal 25 MHz (see system_stm32f4xx.c explanation in header);
STM32F4DISCOVERY board uses 8 MHz and system clock will be scaled down by factor 3.125 using default
library values.
53
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
It should be noted that:
"Several prescalers are used to configure the AHB frequency, the high-speed APB (APB2) and the low-
speed APB (APB1) domains. The maximum frequency of the AHB domain is 168 MHz. The maximum
allowed frequency of the high-speed APB2 domain is 84 MHz. The maximum allowed frequency of the
low-speed APB1 domain is 42 MHz" (TIM2 is connected to APB1!)"
and
"The timer clock frequencies are automatically set by hardware. There are two cases:
1. If the APB prescaler is 1, the timer clock frequencies are set to the same frequency as that of the
APB domain to which the timers are connected.
2. Otherwise, they are set to twice (×2) the frequency of the APB domain to which the timers are
connected."
To ensure what is the exact clock frequency at which APB1 peripherals are clocked, it is useful to
check it by calling the function
RCC_GetClocksFreq(&RCC_Clocks);
APB1_CLK = RCC_Clocks.PCLK1_Frequency;
With default settings in system_stm32f4xx.c this frequency is 13.44 MHz. To calculate the constant
that needs to be written in TIM2 autoreload register we need to use the formula:
The result is stored as unsigned 16-bit integer because TIM2 ARR register (auto-reload register) is
16-bit wide (see datasheet). Finally, we need to configure struct of type TIM_TimeBaseInitTypeDef
defined in stm32f4xx_tim.h:
typedef struct
{
uint16_t TIM_Prescaler; /*!< Specifies the prescaler value used to divide the TIM clock.
This parameter can be a number between 0x0000 and 0xFFFF */
uint32_t TIM_Period; /*!< Specifies the period value to be loaded into the active
Auto-Reload Register at the next update event.
This parameter must be a number between 0x0000 and 0xFFFF. */
uint8_t TIM_RepetitionCounter; /*!< Specifies the repetition counter value. Each time the RCR
downcounter
reaches zero, an update event is generated and counting restarts
from the RCR value (N).
This means in PWM mode that (N+1) corresponds to:
- the number of PWM periods in edge-aligned mode
- the number of half PWM period in center-aligned mode
This parameter must be a number between 0x00 and 0xFF.
@note This parameter is valid only for TIM1 and TIM8. */
} TIM_TimeBaseInitTypeDef;
54
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Counter modes are defined in the same header file:
#define TIM_CounterMode_Up ((uint16_t)0x0000)
#define TIM_CounterMode_Down ((uint16_t)0x0010)
#define TIM_CounterMode_CenterAligned1 ((uint16_t)0x0020)
#define TIM_CounterMode_CenterAligned2 ((uint16_t)0x0040)
#define TIM_CounterMode_CenterAligned3 ((uint16_t)0x0060)
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
"In upcounting mode, the counter counts from 0 to the auto-reload value (content of the TIMx_ARR
register), then restarts from 0 and generates a counter overflow event.".
and call TIM2 initialization function with reference to TIM2 resource (typed pointer to register bank
struct) and content of TIM2 configuration helper struct:
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
Now TIM2 has defined time base and counting mode. Next, we shall configure TIM2 peripheral to
generate interrupt request to NVIC on counter overflow:
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_ITConfig function enables or disables the specified TIM interrupts. The second parameter
defines flags referring to events that can cause an IRQ (see the function source code comments in
stm32f4xx_tim.c):
* @param TIM_IT: specifies the TIM interrupts sources to be enabled or disabled.
* This parameter can be any combination of the following values:
* @arg TIM_IT_Update: TIM update Interrupt source
* @arg TIM_IT_CC1: TIM Capture Compare 1 Interrupt source
* @arg TIM_IT_CC2: TIM Capture Compare 2 Interrupt source
* @arg TIM_IT_CC3: TIM Capture Compare 3 Interrupt source
* @arg TIM_IT_CC4: TIM Capture Compare 4 Interrupt source
* @arg TIM_IT_COM: TIM Commutation Interrupt source
* @arg TIM_IT_Trigger: TIM Trigger Interrupt source
* @arg TIM_IT_Break: TIM Break Interrupt source
*
* @note For TIM6 and TIM7 only the parameter TIM_IT_Update can be used
* @note For TIM9 and TIM12 only one of the following parameters can be used: TIM_IT_Update,
* TIM_IT_CC1, TIM_IT_CC2 or TIM_IT_Trigger.
* @note For TIM10, TIM11, TIM13 and TIM14 only one of the following parameters can
* be used: TIM_IT_Update or TIM_IT_CC1
* @note TIM_IT_COM and TIM_IT_Break can be used only with TIM1 and TIM8
The TIM_IT_Update parameter will trigger interrupt request to NVIC following the counter overflow.
However, this will still not trigger interrupt service routine to run, only interrupt request to NVIC. To
enable interrupt we need to configure NVIC by calling NVIC_Init() function on properly filled
NVIC_InitTypeDef structure:
// Enable the TIM2 global Interrupt
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
55
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
NVIC_InitTypeDef structure is defined in misc.h:
typedef struct
{
uint8_t NVIC_IRQChannel; /*!< Specifies the IRQ channel to be enabled or disabled.
This parameter can be an enumerator of @ref IRQn_Type
enumeration (For the complete STM32 Devices IRQ Channels
list, please refer to stm32f4xx.h file) */
uint8_t NVIC_IRQChannelPreemptionPriority; /*!< Specifies the pre-emption priority for the IRQ channel
specified in NVIC_IRQChannel. This parameter can be a value
between 0 and 15 as described in the table @ref MISC_NVIC_Priority_Table
A lower priority value indicates a higher priority */
uint8_t NVIC_IRQChannelSubPriority; /*!< Specifies the subpriority level for the IRQ channel
specified NVIC_IRQChannel. This parameter can be a value
between 0 and 15 as described in the table @ref MISC_NVIC_Priority_Table
A lower priority value indicates a higher priority */
} IRQn_Type;
Priority level is set to maximum by setting the preemption priority and subpriority to zero. Now the
TIM2 interrupt request to NVIC will actually generate interrupt service routine (ISR) call upon TIM2
overflow. The final step is to run TIM2 peripheral:
TIM_Cmd(TIM2, ENABLE);
Before running the timer we shall reset global variable millisecond counter for later use:
timer2_Ticks_Millisec = 0;
How to write ISR routine and connect the code with interrupt vector table?
Interrupt vector table is defined in startup file startup_stm32f40_41xxx.s (assembly code). Let us
consider important parts of startup file:
; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
56
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
PRESERVE8
THUMB
; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog
DCD PVD_IRQHandler ; PVD through EXTI Line detection
DCD TAMP_STAMP_IRQHandler ; Tamper and TimeStamps through the EXTI line
DCD RTC_WKUP_IRQHandler ; RTC Wakeup through the EXTI line
DCD FLASH_IRQHandler ; FLASH
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line0
DCD EXTI1_IRQHandler ; EXTI Line1
DCD EXTI2_IRQHandler ; EXTI Line2
DCD EXTI3_IRQHandler ; EXTI Line3
DCD EXTI4_IRQHandler ; EXTI Line4
DCD DMA1_Stream0_IRQHandler ; DMA1 Stream 0
DCD DMA1_Stream1_IRQHandler ; DMA1 Stream 1
DCD DMA1_Stream2_IRQHandler ; DMA1 Stream 2
DCD DMA1_Stream3_IRQHandler ; DMA1 Stream 3
DCD DMA1_Stream4_IRQHandler ; DMA1 Stream 4
DCD DMA1_Stream5_IRQHandler ; DMA1 Stream 5
DCD DMA1_Stream6_IRQHandler ; DMA1 Stream 6
DCD ADC_IRQHandler ; ADC1, ADC2 and ADC3s
DCD CAN1_TX_IRQHandler ; CAN1 TX
DCD CAN1_RX0_IRQHandler ; CAN1 RX0
DCD CAN1_RX1_IRQHandler ; CAN1 RX1
DCD CAN1_SCE_IRQHandler ; CAN1 SCE
DCD EXTI9_5_IRQHandler ; External Line[9:5]s
DCD TIM1_BRK_TIM9_IRQHandler ; TIM1 Break and TIM9
DCD TIM1_UP_TIM10_IRQHandler ; TIM1 Update and TIM10
DCD TIM1_TRG_COM_TIM11_IRQHandler ; TIM1 Trigger and Commutation and TIM11
DCD TIM1_CC_IRQHandler ; TIM1 Capture Compare
DCD TIM2_IRQHandler ; TIM2
DCD TIM3_IRQHandler ; TIM3
DCD TIM4_IRQHandler ; TIM4
...
The startup file contains some important initialization stuff that must be taken into account to
properly write the application code. Very important setting is the stack size:
Stack_Size EQU 0x00000400
The stack size is set to 1024 bytes by default. This size must provide enough space to hold nested
function call stack frames, each containing function parameters, local variables etc. Although the
default setting is sufficient for most cases, in some situations the stack size must be manually
adjusted to accomodate need for deeply nested function calls, functions with large amount of local
variables storage etc.
57
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Next important thing is the interrupt vector table (IVT), that is placed at the memory address
0x00000000 after reset. Each DCD directive allocates (consecutive) 4 bytes of memory, starting with
__initial_sp on address 0x00000000, Reset_Handler ISR function address at 0x00000004 etc. The DCD
directives follow the structure of the interrupt vector table as defined by Cortex-M processor
architecture. For our case the interesting entry is
DCD TIM2_IRQHandler ; TIM2
This positional IVT entry is given symbolic name TIM2_IRQHandler. If we define in any included
compilation unit (i.e. C source file) a function with exactly the same name, the linker will insert the
address of that function into interrupt vector table entry at this place. We can edit startup file and
write another name for TIM2 ISR if we wish, but then we also need to call TIM2 ISR function that
name in our code.
The linker will take care to insert the address of this function into appropriate IVT slot, which will
cause IRQ to redirect code execution into this function. The ISR does the following (please see the
HAL library source code and STM32F4 family user guide for further explanations):
• checks whether the counter overflow event caused the interrupt to be triggered
(TIM_IT_Update event - other events could have triggered TIM2 interrupt if it was set up in a
different way, but they would be ignored),
• if interrupt was caused by auto-reload counter overflow, simply clear the interrupt pending
bit (counter will start counting the next cycle automatically in auto-reload mode),
• increment global variable milliseconds counter.
The ISR basically does nothing but incrementing global variable timer2_Ticks_Millisec, enabling the
application code to keep track on elapsed time since the last TIM2 initialization.
Although the application code could simply read the content of timer2_Ticks_Millisec global variable,
it is not desirable because this variable could be changed at any time by interrupt (this is a shared
resource). Each access to the shared resource must be done within critical section and protected
against the non-atomic behaviour. It would be advisable not to read this variable directly but to use
helper function which contains critical section. Therefore, we do not export timer2_Ticks_Millisec for
outside modules and use helper function to read the value:
uint32_t timer2_get_millisec()
{
uint32_t value;
NVIC_DisableIRQ(TIM2_IRQn);
value = timer2_Ticks_Millisec;
NVIC_EnableIRQ(TIM2_IRQn);
return value;
}
Since the variable timer2_Ticks_Millisec is shared resource that could be changed by TIM2 interrupt,
it should be protected by granting exclusive access to function timer2_get_millisec(). At the
58
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
beginning of critical section, the TIM2 interrupt must be disabled. Then we read the content of
timer2_Ticks_Millisec variable into local copy and enable TIM2 interrupt again. Since the variable
value is locally visible, it is safe to enable interrupt after copying the global variable into local storage
and return local copy to the outside caller function. The application code will use this function to
read elapsed number of milliseconds at any moment in atomic way.
Another function that can be called from the user application code is timer2_wait_millisec(). This
function will produce a blocking delay for supplied number of milliseconds, using the previously
implemented function timer2_get_millisec(). It will first read the elapsed number of milliseconds and
wait in a loop until the predefined delay elapses. The atomicity of access to the global variable is
satisfied by calling a timer2_get_millisec() function that already provides critical section for reading
the shared resource.
t1 = timer2_get_millisec();
while(1)
{
t2 = timer2_get_millisec();
if ((t2 - t1) >= ms) break;
if (t2 < t1) break; // almost never occur, once in 49 days
}
}
5.3. Step 3: Modify the main module to make LEDs blink with strict timing
Now that we have defined timer.h and timer.c files, we can easily modify the main.c module from the
previous chapter to enable strictly timed LED blinking. The main.h looks like this:
/* main.h */
#ifndef MAIN_H
#define MAIN_H
#endif
We include timer.h header to import TIM2 API. We also define a constant DELAY_MS which
determines the LED blinking interval. To change this interval it is sufficient only to change the #define
statement, without affecting the code in main.c.
59
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
The main.c module now looks like this:
/* main.c */
#include <main.h>
int main(void)
{
gpio_init();
timer2_init();
while(1)
{
timer2_wait_millisec(DELAY_MS);
gpio_led_state(LED3_ORANGE_ID, 1); // turn on
gpio_led_state(LED4_GREEN_ID, 1); // turn on
gpio_led_state(LED5_RED_ID, 0); // turn off
gpio_led_state(LED6_BLUE_ID, 0); // turn off
timer2_wait_millisec(DELAY_MS);
gpio_led_state(LED3_ORANGE_ID, 0); // turn off
gpio_led_state(LED4_GREEN_ID, 0); // turn off
gpio_led_state(LED5_RED_ID, 1); // turn on
gpio_led_state(LED6_BLUE_ID, 1); // turn on
}
}
The code is compact, easy to understand, decoupled from hardware implementation details, and
without hard-coded constants. First we initialize GPIO by calling the function gpio_init(). Then we
initialize TIM2 peripheral and NVIC by calling the function timer2_init(). The blocking delay is realized
by calling the function timer2_wait_millisec(DELAY_MS), where delay can be changed in main.h
header.
• working Keil MDK-ARM uVision project, following the guidelines for this part of lab exercise,
with the same remarks like in the previous chapter regarding the project building and
transfering to STM32F4DISCOVERY development board
60
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
6. Serial communication
Assignment:
Make a loopback serial interface that will receive characters from a computer terminal (e.g.
HyperTerm) and echo back all received characters. Use USART1 (Universal synchronous asynchronous
receiver transmitter) interface for communication with computer. As STM32F4DISCOVERY board
does not have RS-232 interface implemented on board, and most of modern computers do not have
RS-232 port, it is advisable to use either UART/USB bridge interface or UART/RS232 + RS232/USB
interfaces to connect with computer. Set the communication parameters to 115200, 8, N, 1. The
solution must use interrupt ISR for buffering the incoming characters, while sending the characters
do not have to be buffered.
Guidelines:
Make a new project based on template discussed in previous chapters. Add new empty files main.c/h
and usart.c/h.
In the header file usart.h we shall include header files, create necessary symbol definitions, and API
function prototypes for exporting the serial communication API to other modules:
/* usart.h */
#ifndef USART_H
#define USART_H
#define BUFSIZE 16
#define BAUDRATE 115200
#endif
Beside all previously explained header files, now we also include stm32f4xx_usart.h with all
necessary HAL functions and definitions for control of on-chip USART peripherals. We define the size
of first-in first-out (FIFO) buffer for received characters (BUFSIZE = 16), and baudrate setting
(BAUDRATE = 115200), that can be easily changed by modifying #define values in this header file. For
a minimum USART1 high-level API interface we define three functions:
• USART1_Init() - function that initializes USART1 peripheral and corrsponding GPIO pins,
• USART1_SendChar() - a high-level function that sends a single character through the USART1
interface; the function will block if USART1 is not ready to transmit and will wait until it is
ready; the function guarantees that character will be sent, but does not provide any
guarantees about the timing,
61
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
At the beginning of usart.c module, we first add the internal global variables that will take care of
received characters buffering and interface transmission ready state information:
/* usart.c */
#include <usart.h>
// RX FIFO buffer
char RX_BUFFER[BUFSIZE];
int RX_BUFFER_HEAD, RX_BUFFER_TAIL;
// TX state flag
uint8_t TxReady;
The character array RX_BUFFER holds all unprocessed incoming characters. The buffer is organized as
a FIFO queue using static C character array. Therefore, we need auxiliary variables pointing to head
(RX_BUFFER_HEAD) and tail (RX_BUFFER_TAIL) to implement queue functionality with static array.
This global RX_BUFFER queue is filled with characters by USART1 ISR as they arrive. USART1 ISR is
executed on each received character, and its only job is to place newly received character in
RX_BUFFER FIFO (ISR must be very short, and provide only minimum necessary work with I/O
registers). The main program will dequeue characters from RX_BUFFER FIFO in a main control loop.
Buffering of incoming characters enables less strict timings in the main control loop, which can be
even more relaxed by increasing the FIFO depth (BUFSIZE constant).
The meaning of the TxReady flag will be explained as follows. At the beginning, the USART1 is not
sending any characters, and it is ready to send a new one (TxReady=1). At some moment we decide
to transmit a new character through the USART interface. Depending on the baudrate, transmission
of a single character will take some time, and any new characters must be written in data buffer after
we complete transmission of the previous one (for that reason, just before the character
transmission, we set the flag TxReady=0, signaling to the main program that it must not try to send
any new characters while TxReady=0). At the moment the serial interface complete with sending one
character, the USART1 ISR will be triggered, signaling that the USART interface has just finished with
transmission of the current character (this is the moment when USART interface is ready to receive a
next character for transmission, and the main program will be informed about this through the
TxReady flag, that ISR will change to TxReady=1). Then the main program sends a new character,
setting TxReady=0 and waiting the ISR to reset it to TxReady=1, upon end of the character
transmission.
This scenario holds for all microcontrollers with hardware USART FIFO length of one character. Some
microcontrollers have hardware FIFO depth of more than one character, and some (like STM32F4
family) provide hardware FIFO in RAM by means of DMA. In this example will shall use only software
buffering of received characters, and no support for buffering of transmitted characters (i.e. sending
them one by one in a blocking manner as described, without corrupting characters by prematurely
writes to USART data transmit registers).
62
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Before going any futher, we must implement the function for USART1 initialization:
// init USART1
void USART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
NVIC_InitTypeDef NVIC_InitStructure;
// enable peripheral clocks (note: different bus interfaces for each peripheral!)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
// set alternate function to USART1 (from multiple possible alternate function choices)
GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_USART1); // pins will automatically be assigned
to TX/RX - refer to datasheet to see AF mappings
GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_USART1);
// set interrupt triggers for USART1 ISR (but do not enable USART1 interrupts yet)
USART_ITConfig(USART1, USART_IT_TXE, DISABLE);// should be disbled
USART_ITConfig(USART1, USART_IT_TC, ENABLE); // transmission completed event (for reseting TxReady
flag)
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // character received (to trigger buffering of new
character)
GPIO_InitTypeDef struct will be used to define TX and RX USART1 pins on GPIO port,
NVIC_InitTypeDef will be used to setup USART1 interrupt in NVIC, and USART_InitTypeDef will be
used to configure USART1 parameters.
First we must provide clock to the USART1 peripheral (connected to APB2 bus), but also to the
Port B, because TX and RX pins will be mapped to this port:
// enable peripheral clocks (note: different bus interfaces for each peripheral!)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
63
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Next we set up GPIO pins that will be mapped to USART1.TX (PB6), and USART1.RX (PB7):
// map port B pins for alternate function
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; // Pins 6 (TX) and 7 (RX) will be used for USART1
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF; // GPIO pins defined as alternate
function
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; // I/O pins speed (signal rise time)
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; // push-pull output
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP; // activates pullup resistors
GPIO_Init(GPIOB, &GPIO_InitStruct); // set chosen pins
By default, PB6 and PB7 are plain GPIO pins without any corrspondance with USART1. How to know
what GPIO pins we can be used as USART1 TX and RX pins?
This cannot be answered just by reading STM32F4 family guide, because there are too many
different microcontrollers sub-types and IC packages, that differ in number of pins, exposed GPIO
ports etc. Therefore, we need to open the datasheet of the microcontroller we use (e.g.
STM32F407VG), and find the chapter „Pinouts and pin description“. In the table 7 „STM32F40x pin
and ball definitions“ there are all pin definitions, along with alternate functions, for all package types.
For microcontroller on STM32F4DISCOVERY board (STM32F407VG) we need to refer to LQFP100
package. In the column „Alternate functions“ we can see all possible alternate functions that can be
assigned to some pin. For LQFP100 package and pins PB6 and PB7 we can see:
We see all alternate functions that can be assigned to PB6 and PB7, and we could also use other pins
(e.g. PA9 for USART1_TX etc.). Because we decided to use PB6 and PB7, we must refer to these pins
(GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7), and change their mode to alternate function
(GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF). Because there are few possible alternate functions
for these pins, we must decide which one to use:
GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_USART1);
It is sufficient to tell that we use USART1 on PB6 and PB7, and correct functions will be automatically
assigned to each pin (because it is uniquely defined by AF mapping which pin will be TX and RX,
respectivelly).
After setting the GPIO pin mappings, we need to set up USART1 peripheral:
// use USART_InitStruct to config USART1 peripheral
USART_InitStruct.USART_BaudRate = BAUDRATE; // set baudrate from define
USART_InitStruct.USART_WordLength = USART_WordLength_8b;// 8 data bits
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 1 stop bit
USART_InitStruct.USART_Parity = USART_Parity_No; // no parity check
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // no HW control flow
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; // enable both character transmit and
receive
USART_Init(USART1, &USART_InitStruct); // set USART1 peripheral
64
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
We set the baudrate (provided by BAUDRATE define statement), data frame description (8 bits, 1
stop bit, no parity), and without using hardware flow control. We must also excplicitely define what
capabilities we use (receive and transmit).
As we will be using interrupt driven I/O, we must define what events will trigger ISR:
USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
USART_ITConfig(USART1, USART_IT_TC, ENABLE);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
The USART_IT_TC („Transmission Complete“) flag will enable interrupt to be triggered at the end of
character transmission (to signal to the main program that USART1 is ready to send the next
character), and USART_IT_RXNE („Received Data Ready to be Read“) flag will enable interrupt to be
triggered when a new character arrives. USART_IT_TXE flag („Transmit Data Register Empty“) should
be disabled in this application because it does not have the same (desired) functionality as
USART_IT_TC flag. The meaning of flags can be found in STM32F4 family guide (pp. 990, table 146.
„USART interrupt requests“).
meaning that our USART1 interface is ready to send a new character (TxReady = 1), and receive
characters FIFO is empty (RX_BUFFER_HEAD = RX_BUFFER_TAIL = 0).
After we finish the initialization of USART1 peripheral, we need to implement the corresponding ISR:
void USART1_IRQHandler(void)
{
static char rx_char;
static char rx_head;
// RX event
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
rx_char = USART_ReceiveData(USART1);
// check for buffer overrun:
rx_head = RX_BUFFER_HEAD + 1;
if (rx_head == BUFSIZE) rx_head = 0;
if (rx_head != RX_BUFFER_TAIL)
{
// adding new char will not cause buffer overrun:
RX_BUFFER[RX_BUFFER_HEAD] = rx_char;
RX_BUFFER_HEAD = rx_head; // update head
}
}
65
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
// TX event
if (USART_GetITStatus(USART1, USART_IT_TC) == SET)
{
USART_ClearITPendingBit(USART1, USART_IT_TC);
TxReady = 1;
}
}
The ISR function name USART1_IRQHandler is already defined in the startup file interrupt vector
table definition so it recommended to use exactly this name for ISR. When ISR fires, we need to
determine the interrupt request cause:
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
means that ISR was fired upon the end of the last character transmission. When we receive a
character, we need to do the following:
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
rx_char = USART_ReceiveData(USART1);
// check for buffer overrun:
rx_head = RX_BUFFER_HEAD + 1;
if (rx_head == BUFSIZE) rx_head = 0;
if (rx_head != RX_BUFFER_TAIL)
{
// adding new char will not cause buffer overrun:
RX_BUFFER[RX_BUFFER_HEAD] = rx_char;
RX_BUFFER_HEAD = rx_head; // update head
}
}
First we need to clear interrupt pending bit. Then we use USART_ReceiveData HAL API function to
read newly received character from USART1 interface (data register). We put this character in
temporarily rx_char variable. What we want to do with a new character is to put it into global
received characters queue, and finish ISR job as fast as possible, doing minimum interaction with
peripheral registers, without doing any actual application-specific job. The rest of the code above will
check whether the newly received character will cause the buffer overrun, and put it into the buffer
only if there is a space left.
When sending a new character from the main program, we shall automatically set TxReady = 0 (we
shall see how a bit later), and prevent ourselves from sending a new character before the previous
one has not been fully transmitted (i.e. until we detect that TxReady = 1). The flag is set back to
TxReady = 1 in ISR when character is transmitted:
// TX event
if (USART_GetITStatus(USART1, USART_IT_TC) == SET)
{
USART_ClearITPendingBit(USART1, USART_IT_TC);
TxReady = 1;
}
66
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
6.3. Step 3: Implement high-level API functions for sending and receiving
characters
After we properly initialized USART1 peripheral and prepared logic in ISR regarding the global buffers
and flags, we can finally implement high-level API functions for sending and receiving characters from
the main program:
void USART1_SendChar(char c)
{
while(!TxReady);
USART_SendData(USART1, c);
TxReady = 0;
}
int USART1_Dequeue(char* c)
{
int ret;
ret = 0;
*c = 0;
NVIC_DisableIRQ(USART1_IRQn);
if (RX_BUFFER_HEAD != RX_BUFFER_TAIL)
{
*c = RX_BUFFER[RX_BUFFER_TAIL];
RX_BUFFER_TAIL++;
if (RX_BUFFER_TAIL == BUFSIZE) RX_BUFFER_TAIL = 0;
ret = 1;
}
NVIC_EnableIRQ(USART1_IRQn);
return ret;
}
Let us consider the function USART1_SendChar(). This function will try to send a character c by first
checking whether some other character is still being transmitted (state of the TxReady flag). If
TxReady=1, the function will transmit the character (i.e. write it into the transmit buffer data register
of USART1 interface) by means of USART_SendData() HAL function. As soon as a new character is
written to USART1 data buffer, the program will set TxReady=0, and that flag will be set to
TxReady=1 by ISR upon end of character transmission. If we call again USART1_SendChar()
immediatelly after this, the function will block in while loop on TxReady=0 flag, until ISR unblocks it
upon end of the previous character transmission. This is an example of unbuffered character sending
with blocking, meaning that we send characters one by one (without buffering), in a polling manner
(blocking on TxReady flag until the previous character is transmitted). Note that it would be easily
possible to buffer outgoing character stream, and automatically send the remaining characters in
buffer in ISR on USART_IT_TC event, but this is not done in this example for sake of simplicity.
The second function fetches single character (the oldest one) from the receive characters FIFO
buffer. Since the receive buffer RX_BUFFER is a global variable that can be accessed both from the
main function and USART1 interrupt service routine, care must be taken because this resource is
shared between two lines of code execution. Therefore, any attempt in the main function to access
or alter the values in RX_BUFFER or RX_BUFFER_HEAD and RX_BUFFER_TAIL, should be done in
atomic way within the critical section where USART1 ISR must be temporarily disabled. The critical
section is realized by simply enabling and disabling the USART1 ISR:
NVIC_DisableIRQ(USART1_IRQn);
...
NVIC_EnableIRQ(USART1_IRQn);
67
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
Within a critical section, we first check whether the buffer is not empty, and in that case we fetch the
oldest value to byref parameter (char *c), adjust the value of the buffer tail, and set the ret flag to 1,
indicating that receive buffer was not empty when USART1_Dequeue() function was called:
if (RX_BUFFER_HEAD != RX_BUFFER_TAIL)
{
*c = RX_BUFFER[RX_BUFFER_TAIL];
RX_BUFFER_TAIL++;
if (RX_BUFFER_TAIL == BUFSIZE) RX_BUFFER_TAIL = 0;
ret = 1;
}
If the receive buffer is empty, the function returns 0, and the content of byref parameter char* c
should be ignored.
The main.c module source code implementing the loopback interface using high-level functions from
usart.c is very simple:
/* main.c */
#include <main.h>
int main(void)
{
char c;
USART1_Init();
while(1)
{
if (USART1_Dequeue(&c) != 0)
{
USART1_SendChar(c);
}
}
}
The main.h header contains all necessary common definitions and imports the usart.c module
functionality. Calling the function USART1_Init() will initialize USART1 peripheral (communication
settings), GPIO pins, interrupt service routine (IRQ triggers, ISR processing), and housekeeping
variables internal to usart.c module (TxReady flag and RX_BUFFER queue). If we want to change
anything in the current USART1 setup, we can do that in source code of usart.h and usart.c.
The main function contains an infinite loop that continously check for incoming characters.
Characters are buffered to RX_BUFFER in USART1 ISR, and the main loop does not directly poll
hardware registers, neither it must finish the current character processing before arrival of the new
character. Timing restrictions are relaxed because ISR takes care of fast characters buffering into
temporary memory storage. Since the RX_BUFFER is a memory resource shared between the main
program and the USART1 ISR, reading and writing variables and data related to RX_BUFFER must be
68
Programming industrial embedded systems, 2017./2018. - Laboratory exercise 1
Author: Hrvoje Džapo
done in atomic way. Therefore, the usart.h header does not expose RX_BUFFER related variables
outside of the module to prevent direct access in non-atomic way from external modules. The
atomicity of RX_BUFFER access is achieved by implementing the critical section in USART1_Dequeue()
function that wraps all internal operations on RX_BUFFER, and returns the result of operation and
read character, if any.
If the character was successfully read, it is sent through UART1 interface right away. This is done by
calling USART1_SendChar() function, which will automatically block until USART1 interface is ready
for transmission (i.e. last character is completele transmitted), put the next character into outgoing
buffer of USART1 interface, and mark TxReady value flag to 0, to prevent new character transmission
until the current transfer is still in progress.
• working Keil MDK-ARM uVision project, following the guidelines for this part of lab exercise,
with the same remarks as in the previous chapters regarding the project building and
transfering to STM32F4DISCOVERY development board,
• program that demonstrates loopback functionality,
• extend the code example in a way that the program exits the loopback functionality when it
receives 'x' character from PC, and sends back the message „now exiting loopback mode“;
write your own printf() function to send message (i.e. null-terminated string) through the
USART1 interface, based on functions presented in this part of exercise.
Note: If you are using the UART/USB interface based on Silabs CP2102 shown in the picture below,
you need to connect RXD signal to PB6 (TX), and TXD signal to PB7 (RX).
69