0% found this document useful (0 votes)
1K views333 pages

EAGLE Book

Uploaded by

Ruclaudino
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1K views333 pages

EAGLE Book

Uploaded by

Ruclaudino
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 333

EAGLE Book

— The Guide to the EAGLE Project —

EAGLE TA Team

v1.1 – 17 Sept 2024


Contents

List of Tutorials and Exercises vii

Part 1. Project Overview 1


Chapter 1. Introduction 2
1. The EAGLE Project 2
2. Modules 2
3. Workspaces 3
Chapter 2. The Drone Platform 5
1. Introduction 5
2. The EAGLE frame 5
3. The Zybo board 6
4. The motion sensors 8
5. The camera 8
6. Remote control 8
7. Local speed control 11
8. Power 11
9. Conclusion 13
Chapter 3. Simultaneous Wireless Information & Power Transfer (SWIPT) 14
1. Reading Guide 14
2. SWIPT task 15
3. Power transfer 16
4. Data transfer 17
5. System layout 17
6. Milestones 17
7. Tools and components 20
Chapter 4. Attitude and Navigation Control (ANC) 22
1. Introduction 22
2. Task List 23
3. Reading Guide 26
Chapter 5. Image Processing (IMP) 29
1. Introduction 29
2. Task List 29
3. Reading Guide 31
Chapter 6. Cryptography (CRYPT) 32
1. Introduction 32
2. Roadmap 32
3. Milestones 34
ii
CONTENTS iii

4. EAGLE Communication Protocol 35


5. Hardware Accelerators and HW/SW Co-design 42
6. Test Resources 45

Chapter 7. Communications (COMMS) 47


1. Introduction 47
2. Milestones 49
3. Reading Guide 50

Part 2. Background Information 51

Chapter 8. Introduction 52

Chapter 9. Linux Commandline 53


1. Introduction 53
2. Environment variables 53
3. Commonly used commands 54
4. Networking commands 56
5. Tips and tricks 57

Chapter 10. Programming Languages – C 59


1. Types & Variables 59
2. Functions 62
3. Structs 64
4. Enumerations 65
5. File Structure, Header Files and Compilation 66
6. Advanced Topics 67

Chapter 11. Programming Languages – Matlab 69


1. Coding in Matlab 69
2. Classes 81
3. The MEX interface 83
4. Code generation 86

Chapter 12. Programming Languages – C++ 92


1. The C++ Language 92
2. Types & Variables 93
3. Functions 103
4. Operators 107
5. Control structures 109
6. Structs & Classes 113
7. Enumerations 120
8. Input/Output 121
9. Compilation 123
10. CMake 129
11. Documentation 129
12. Miscellaneous 130
13. Advanced Topics 134
14. Where to go next? 139

Chapter 13. Programming Languages – Verilog 140


1. Introduction 140
CONTENTS iv

2. Verilog, the abstract simulation language 141


3. Verilog as a hardware description language 148
4. Verilog for testbenches 156
Chapter 14. Introduction to Networking 160
1. Networking Essentials 160
2. Sockets 160
Chapter 15. Wireless power transfer theory 165
1. Introduction 165
2. Wireless magnetic resonant power transfer: some theory 165
3. Series vs parallel resonance 166
4. Two-coil versus four-coil systems 169
5. Efficiency of power transfer 172
Chapter 16. Autopilot Framework 177
1. Overview 177
2. Coordinate systems 178
3. Remote controller 179
4. IMU configuration 179

Part 3. Workspace Tutorials 180


Chapter 17. Introduction 181
Chapter 18. PCB Simulations and Design
SWIPT 182
1. Setting up analog and digital co-simulation 182
2. PCB design 199
Chapter 19. Vivado Hardware Design Project
Zhenda Zhang 202
1. Setting up 202
2. Creating and testing a hardware project 202
3. Simulating and debugging a hardware project 206
4. Using IP cores and block designs 208
5. Hardware-software interfacing 210
6. The EAGLE Vivado project 213
7. Working from Home 214
8. SWIPT IP Blocks 215
9. Programming the Zybo for SWIPT 219
Chapter 20. Vitis Software Design Project
Zhenda Zhang 221
1. Setting up 221
2. Creating a project 221
3. Compiling, running and debugging code 226
4. Deploying standalone code on a Zybo 229
Chapter 21. Zybo – Crypto Project
Zhenda Zhang 232
1. Verilog Introductory Exercise 232
2. AXI peripherals for custom hardware-software interfaces 236
CONTENTS v

3. Running C code locally using Vitis 240


4. Running C code on the Zybo in Linux 241

Chapter 22. Zybo – Linux


Pieter Pas 246
1. Booting Linux on the Zybo 246
2. Installing Python packages to the Zybo 249

Chapter 23. Python – Image Processing


Eli Verwimp 252
1. Introduction 252
2. Installation 252
3. The basics 253

Chapter 24. Raspberry Pi


Simon Bos 255
1. Hardware 255
2. Software 256
3. Connecting 256
4. Configuring the RPi 257
5. Internet connectivity 258
6. Poweroff 258
7. Python in a venv environment 258
8. Autorunning Python Scripts 259
9. Troubleshooting 259

Chapter 25. Matlab Project


Alexander Bodard 260
1. Setting up 260
2. The MEX Wrapper 265
3. Building a Simulator 271
4. The Python wrapper 274
5. Receiving information from other workspaces 275

Chapter 26. Zybo – Autopilot Project


Pieter Pas 276
1. Setting up 276
2. Working with VS Code 276
3. Debugging and deploying code 277
4. Remote development using VSCode 280
5. Getting to know the framework 281
6. Communicating with other modules 283

Chapter 27. Drone Platform


Arne Symons 285
1. Overview 285
2. Debugging components 286
3. Changing components 287

Part 4. Information for Teaching Assistants 289

Chapter 28. Content guidelines 290


CONTENTS vi

Chapter 29. LateX guidelines 292


1. Tutorial and Exercise environments 292
2. Emphasizing 292
3. Displaying code snippets 293
4. General typesetting 294
5. Installation 294
6. Compilation 294
Chapter 30. Zybo – Linux Build 296
1. Introduction 296
2. The U-Boot bootloader 297
3. The Linux kernel 302
4. The Buildroot userspace 305
5. Installation and final notes 310
Chapter 31. Advanced Tutorials 313
1. Integrated Logic Analyzer (ILA) 313
2. The AXI Verification IP 314
3. Running CRYPT on the Zybo in Baremetal 316
4. CRYPT Makefile build system 318
5. Debugging CRYPT on the Zybo in Linux using gdb 320
Bibliography 323
List of Tutorials and Exercises

Chapter 9: Linux Commandline


9.2.1 Tutorial (Using environment variables) 53

Chapter 14: Networking Introduction


14.2.1 Tutorial 163

Chapter 18: PCB Design


18.1.1 Tutorial (LTSpice to Spice-netlist) 182
18.1.2 Tutorial (RC-network) 185
18.1.3 Tutorial (Subcircuits and parameters) 187
18.1.4 Tutorial (Simple Counter) 192
18.1.5 Tutorial (Add Analog Circuits to Simple Counter) 195

Chapter 19: Vivado Hardware Design Project


19.1.1 Tutorial (Starting Vivado) 202
19.2.1 Tutorial (Creating a Vivado hardware project) 202
19.2.2 Tutorial (Adding source code to Vivado) 203
19.2.3 Tutorial (Synthesizing the Verilog design and generating a
bitstream) 204
19.2.4 Tutorial (Inspecting the results of a design) 204
19.2.5 Tutorial (Testing the Verilog design on a Zybo) 206
19.3.1 Tutorial (Simulating a Verilog module) 206
19.4.1 Tutorial (Creating an IP core from a Vivado Verilog project) 208
19.4.2 Tutorial (Creating and editing a block design) 209
19.4.3 Tutorial (Editing an already exported IP core) 209
19.5.1 Tutorial (Creating a hardware-software interfacing block design) 210
19.5.2 Tutorial (Exporting the hardware design as a hardware platform) 212
19.6.1 Tutorial (Opening the EAGLE Vivado project) 213
19.6.1 Exercise (Kill switch) 213
19.6.2 Exercise (Tuning knob) 213
19.6.3 Exercise (Changing the default LED configuration) 213
19.6.4 Exercise (Checking the I/O ports of the design) 213
19.6.5 Exercise (Modifying the address space in the Address Editor) 214
19.6.6 Exercise (Modifying the EAGLE project) 214
19.7.1 Tutorial (Local installation of Vivado) 214
vii
LIST OF TUTORIALS AND EXERCISES viii

19.7.2 Tutorial (Working remotely on ESAT computers) 214


19.8.1 Tutorial (Add verilog files) 218
19.9.1 Tutorial (SWIPT on Zybo) 219
19.9.2 Tutorial (Accessing AXI-interface registers on the Zybo) 220

Chapter 20: Vitis Software Design Project


20.1.1 Tutorial (Starting Vitis) 221
20.2.1 Tutorial (Creating a Vitis platform project) 221
20.2.2 Tutorial (Creating a C/C++ application project for bare-metal Zybo) 222
20.2.3 Tutorial (Using the AXI GPIO interface in Vitis) 225
20.3.1 Tutorial (Compiling code in Vitis) 226
20.3.2 Tutorial (Running your code on bare-metal Zybo) 226
20.3.3 Tutorial (Launching a serial terminal) 227
20.3.4 Tutorial (Debugging code in Vitis) 228
20.4.1 Tutorial (Creating a BOOT.BIN file from a Vitis hardware project) 229
20.4.2 Tutorial (Booting a BOOT.BIN file on a microSD card) 230
20.4.3 Tutorial (Booting a BOOT.BIN file on QSPI flash memory) 230

Chapter 21: Zybo – Crypto Project


21.1.1 Tutorial (Road map of hardware design in Verilog) 235
21.2.1 Tutorial (Creating a new AXI4-Lite peripheral) 236
21.2.2 Tutorial (Debugging a custom AXI-Lite peripheral) 239
21.3.1 Tutorial (Creating a local C/C++ project) 240
21.3.2 Tutorial (Importing the CRYPT code in a local C/C++ project in
Vitis) 240
21.3.3 Tutorial (Running and debugging the CRYPT C code locally) 240
21.4.1 Tutorial (Creating a C/C++ project for Linux on the Zybo) 241
21.4.2 Tutorial (Importing the CRYPT code in Vitis for Zybo Linux) 242
21.4.3 Tutorial (Running your code on Linux on the Zybo) 243

Chapter 22: Zybo – Linux


22.1.1 Tutorial (Configure SSH to use public key authentication) 246
22.1.2 Tutorial (Booting Linux on the Zybo) 246
22.1.3 Tutorial (Connect to the Linux shell of the Zybo using USB) 246
22.1.4 Tutorial (Connect to the Linux shell of the Zybo over SSH) 247
22.1.5 Tutorial (Cleanly shutting down the Zybo) 248
22.1.6 Tutorial (Creating a BOOT.bin file for booting a Linux system) 248
22.2.1 Tutorial (Downloading a pre-built Python package) 249
22.2.2 Tutorial (Building a pure-Python package locally) 249
22.2.3 Tutorial (Building a Python package with native code locally) 249
22.2.4 Tutorial (Installing a Python package to the Zybo) 250
LIST OF TUTORIALS AND EXERCISES ix

22.2.1 Exercise (Installing a Python package locally) 250

Chapter 23: Python – Image Processing


23.2.1 Tutorial (Installation on your own device) 252
23.3.1 Exercise (Running a Python script (Easy)) 253
23.3.1 Tutorial (Numpy) 253
23.3.2 Tutorial (OpenCV) 253

Chapter 24: Raspberry Pi


24.2.1 Tutorial (Install OS) 256
24.3.1 Tutorial (Connecting to the RPi using SSH) 256
24.3.2 Tutorial (Finding the IP address of the RPi) 257
24.4.1 Tutorial (Setting the hostname and password) 257
24.4.2 Tutorial (Configure SSH to use public key authentication) 257
24.5.1 Tutorial (Connect to Eduroam via wpa supplicant) 258
24.5.2 Tutorial (Setup internet sharing from laptop) 258
24.6.1 Tutorial (Powering off the Rasperry Pi) 258
24.7.1 Tutorial (Running Python with OpenCV) 258
24.8.1 Tutorial (Using autostart.sh) 259

Chapter 25: Matlab Project


25.1.1 Tutorial (Installation of the C++ compiler) 260
25.1.2 Tutorial (Path configuration) 261
25.1.3 Tutorial (Installation of Python interpreter) 262
25.1.4 Tutorial (Installation of CMake) 263
25.1.5 Tutorial (Installation of Visual Studio Code) 263
25.1.6 Tutorial (Installation of Doxygen) 264
25.2.1 Tutorial (Setting up the C++ wrapper) 265
25.2.2 Tutorial (The Control class) 266
25.2.1 Exercise (Codegeneration and template files (Beginner)) 268
25.2.2 Exercise (Investigating the Control class (Intermediate)) 270
25.2.3 Exercise (Debugging C++ and MEX code in VSCode (Advanced)) 271
25.3.1 Exercise (Building the complete simulator. (Advanced)) 271
25.3.1 Tutorial (Example script) 273
25.3.2 Exercise (Random number generation (Advanced)) 273
25.4.1 Tutorial (Setting up your Matlab environment) 274
25.4.2 Tutorial (Reloading Python modules) 275
25.5.1 Exercise (Parsing Log Data (Intermediate)) 275
25.5.2 Exercise (Inter-process communication (Intermediate)) 275

Chapter 26: Zybo – Autopilot Framework


26.2.1 Tutorial (Opening the ANC Projects in VS Code) 276
LIST OF TUTORIALS AND EXERCISES x

26.2.1 Exercise (Navigating the code efficiently) 277


26.3.1 Tutorial (Installing the cross-compilation tools) 277
26.3.2 Tutorial (Building the Autopilot project for the Zybo) 277
26.3.3 Tutorial (Running the tests and examples on the Zybo) 277
26.3.4 Tutorial (Running the Autopilot software on the Zybo) 278
26.3.1 Exercise (Using SSH to upload the compiled files (Beginner)) 278
26.3.2 Exercise (Connecting a remote debug session (Beginner)) 279
26.4.1 Tutorial (Remote development using an ESAT machine) 280
26.5.1 Tutorial (Doxygen documentation) 281
26.5.1 Exercise (Bypassing the controller (Beginner)) 281
26.5.2 Exercise (Controlling the LEDs (Beginner)) 282
26.5.3 Exercise (AHRS and friends (Beginner)) 282
26.6.1 Exercise (Logging Data (Beginner)) 283
26.6.2 Exercise (Inter-process communication (Intermediate)) 283
26.6.3 Exercise (Printing the Logging Data (Beginner)) 284
26.6.4 Exercise (Sending Vision Data (Intermediate)) 284

Chapter 27: Drone Platform


27.1.1 Exercise (Understanding the drone platform (Easy)) 285
27.2.1 Tutorial (Calibrating the ESCs) 286
27.2.1 Exercise (Visualizing the sensor measurements) 286
27.3.1 Tutorial (Changing the ESCs) 287
27.3.2 Tutorial (Changing the IMU) 287

Chapter 29: LateX guidelines


29.1.1 Tutorial (Adding tutorials) 292
29.1.2 Tutorial (Adding exercises (Beginner)) 292

Chapter 31: Advanced Tutorials


31.1.1 Tutorial (Recording FPGA signals on hardware) 313
31.3.1 Tutorial (Importing the CRYPT code into a baremetal Zybo
project) 316
31.3.2 Tutorial (Running and debugging the CRYPT bare-metal C code) 317
31.4.1 Tutorial (Compiling CRYPT code using the Makefile) 318
31.4.2 Tutorial (Configuring and tweaking the Makefile-based build) 318
31.4.3 Tutorial (Selecting the FPGA interface target) 319
31.5.1 Tutorial (Using gdbserver on the Zybo) 320
Part 1

Project Overview
CHAPTER 1

Introduction

1. The EAGLE Project


The EAGLE project consists of many components. A detailed description of
the project is provided in H01Q6 EAGLE project description.pdf, but we provide
a concise overview here. The main mission is to create a drone that is able to
autonomously fly towards a final destination. There it lands and provides power
and mission data a LED wall. The destination and trajectory that must be followed
is unknown to the drone, so it has to follow a trail of QR codes. To see these and
determine its current location, the drone has access to a camera. During flight,
mission data should also transferred to a ground station wirelessly.
Completing the full mission is a major challenge, therefore the main mission is
split up into modules. These modules will each need to develop several components
that need to work together. The tools with which to build these components are
referred to as workspaces throughout this document.
Section 2 provides a brief overview of each module and its task. Chapter 2
covers the drone platform and the hardware that it is comprised of. A detailed
task description and reading guide are then provided in Chapter 3–7. A high–level
description of the workspaces is provided. Tutorials related to each workspace are
then gathered in Part 3. Part 2 contains background information, like tips and tricks
on using certain programming languages.
As you may notice, there is a lot of information to process in this document.
Note however that no single student is required to read this document in its entirety.
Instead, each module will require some of the information provided here and use
it to develop their components. Therefore each module chapter contains a reading
guide, which will indicate which parts are relevant for you specifically and when you
will need them.

2. Modules
To succesfully complete the mission, you will need to split your teams into parts.
These parts are listed below. Each has their own set of documentation and experts.
You can find the point of entry for the documentation in this part. The contact
information of all the experts is provided on Toledo.
(1) Simultaneous Wireless Information and Power Transfer (SWIPT) This
module concerns the landing platform. They (i) enable inductive power
transfer to the LED wall; and (ii) use the existing power link to simulata-
neously communicate information (e.g., mission data, link efficiency).
(2) Attitude and Navigation Control (ANC) This module concerns the flight
control of the drone. Controllers are developed in a hierarchical fashion:
(i) an attitude controller stabilizes the drone around a given orientation;
(ii) altitude control stabilizes the drone at a given height; and (iii) naviga-
tion/position control drives the drone along the QR trail.
2
3. WORKSPACES 3

(3) Image Processing (IMP): This module concerns the vision system of the
drone. A camera feed is used to (i) determine the position of the drone;
(ii) detect and parse QR codes.
(4) Cryptography (CRYPT): This module concerns the contents of the QR
codes. They should be (i) verified to not be malicious; and (ii) decrypted
into a target coordinate.
(5) Communications (COMMS): This module concerns the interface between
the other modules and the ground station. Specifically (i) a communication
bus should be developed in order to stream data between modules; (ii) a
video feed along with flight data should be passed to the ground station.

Tip 1.2.1

Each module chapter has a section called reading guide a. If you are unsure where
to start with reading this document, you can begin by reading this section for
the module you will work on.
a
For Cryptography, you can refer to the roadmap section.

3. Workspaces
In practice the modules listed in the previous section will develop their compo-
nents in several workspaces. These are usually linked to one or more IDEs that can
be used to develop code (e.g., Vitis and Visual Studio Code for C++ development).
We provide an overview of the workspaces below.
✓ PCB Design is the environment in which the PCB for SWIPT is designed;
✓ Vivado Hardware Design is the main project that runs on the Zybo. Vivado
is mostly used for programming the FPGA, however it also interfaces with
the C++ workspace for the Zybo;
✓ Vitis Software Design is the main tool where the embedded software is
developed. It is used for the Crypt project;
✓ Zybo – Crypt Project contains C code that runs on the Linux core of the
Zybo and handles decryption of the QR codes;
✓ Zybo – Linux instructions for booting Linux on the Zybo;
✓ Python – Image Processing (⋆) is where development of the localization and
QR detection algorithms occurs;
✓ RPi Project implements localization algorithm, QR extraction and com-
munication with the ground station;
✓ Matlab Project (⋆) implements simulation tools that mimic the code that
runs on the Zybo and the RPi. It can be used to develop/debug controllers,
the localization algorithm and QR decryption;
✓ Zybo – Autopilot Project contains the Autopilot framework that runs on
the Zybo, interfaces with the sensors (e.g. RC, IMU and Sonar) and drives
the motors of the drone;
✓ Drone Platform includes all drone hardware. Eventually all components
should be deployed on this platform.
The workspaces marked with a (⋆) are known as developer workspaces. Here
software/hardware is developed which is later deployed in other workspaces. The
3. WORKSPACES 4

software developed in the Image Processing workspace will be deployed as a compo-


nent of the RPi Project and the controllers developed in the Matlab project will be
deployed in the Autopilot project through code generation.
Since most of the development of the communication framework happens from
scratch, no explicit tutorials are given. Instead a general chapter on networking is
added in Part 2. For more information, see Chapter 7.

Tip 1.3.1

Keep these workspaces in mind when setting up your Git repositories and team
structure. This will save time when integrating between the different modules.
We provide an overview of the different workspaces and how they interact with
the modules in Tbl. 1.

SWIPT ANC IMP CRYPT COMMS


PCB X
Vivado X (X) X
Vitis X X
Zybo – Crypt X
Zybo – Linux X X (X) X X
Python – IMP X
RPi X X
Matlab – Simulator X
Zybo – Autopilot (X) X (X) (X) (X)
Drone D D D D D

Table 1. Overview of overlap between modules and workspaces.


Legend: X marks a module developing components in this workspace;
(X) marks integration of a module with others in this workspace; D
marks deployment on the drone.

Keep the cells marked (X) in mind during the project. The fact that these are
sometimes ignored until the end of the year often causes several issues when inte-
grating components. Several tutorials and exercises describing integration between
components are listed in Part 3.
CHAPTER 2

The Drone Platform

1. Introduction
This chapter gives a brief overview of the different EAGLE hardware modules
that are used to build a smart drone. Software, programming aspects and algorithms
are covered in other chapters.
First we introduce the bare frame of the drone in Section 2. The Zybo board,
that contains the brains and control of the EAGLE, is presented in Section 3. Then,
we subsequently cover the IMU (Section 4), the camera (Section 5), the remote
control (Section 6), the speed control (Section 7) and finally the power distribution
(Section 8). This should give you enough information about the basic hardware
infrastructure.

2. The EAGLE frame


A broad range of possibilities exist when considering drone frames, which vary
in weight, size, building materials, etc. For the EAGLE drone, we have chosen the
Hobbyking X650F frame (see Fig. 1). It is a high quality folding-arm quadcopter
frame, built from light weight yet rigid glass fiber and aluminum. It is basically a
bare frame able to accomodate all the required equipment and with some protection
against crashes. Further, it allows accomodating a wide range of motors and their
ESCs in ventilated mount for optimal cooling. Additional protection may be added,
but the weight is always a limiting factor when building a drone. Small central
plates (in plexi glass) have been incorporated to ease the screwing of all hardware
on the drone. The whole system is thus more solid and the risk of hardware damage
due to crashes is reduced.
The technical specifications and requirements are enumerated hereafter. Further
details can be found at http://www.hobbyking.com/hobbyking/store/uh viewitem.
asp?idproduct=29600.
Technical specifications:
✓ Weight: 598 g (frame only)
✓ Width: 550 mm
✓ Height: 265 mm
✓ Ground clearance: 155 mm (bottom of frame), 185 mm (camera mount
rails)
✓ Motor Bolt Holes: 16∼30 mm
Requirements:
✓ Your own 4 channel transmitter and receiver
✓ Multi-Rotor control board
✓ 20∼40A brushless ESC x 4
✓ 3S 2200∼3000mAh lipoly battery
✓ 28/35 900-1100kV brushless motor x 4
5
3. THE ZYBO BOARD 6

Figure 1. The Hobbyking frame is a good trade-off between cost,


size and cargo power.

✓ 9∼11 inch propeller x 4 (2 standard/2 reverse rotation)

3. The Zybo board


The brain of the EAGLE is a Xilinx Zynq-7000 SoC. It consists of two pow-
erful ARM Cortex-A9 cores and a powerful FPGA. We work with the Zybo de-
velopment board from Digilent, with lots of processing power and a good set of
input/output (IO) options. A thorough list of the Zybo specifications can be
found at https://digilent.com/reference/programmable-logic/zybo/start or https:
//digilent.com/reference/programmable-logic/zybo-z7/start. Details on the parts
relevant for the project are compiled in the EAGLE Zybo documentation.
Some teams will receive original Zybo boards, while others will get the newer Z7
revision. For our purposes, there is no significant difference between the Zybo and
the Zybo Z7, but you will need to be careful to select the right hardware files for
your specific board, because some of the clock frequencies and GPIO connections
are different.

3.1. IO on the Zybo. To connect the inertial measurement unit (IMU), the
motor speed controllers, the camera, the receiver for the remote controller, the
wireless video link, etc. we need many inputs/outputs. Can you already figure them
out? Where would you connect your camera? Where the wireless link? Which
busses or interfaces do you use? It is a quite challenging and complex system with
many design choices to make.
We connect a Raspberry Pi with a camera using the Ethernet port. Fig. 2 details
the IO used for EAGLE. See also the official pinout from the manufacturer: Zybo,
Zybo Z7; and match them with the pins defined in the constraints file in Vivado:
3. THE ZYBO BOARD 7

sounder
ADCinn_14 ADCinn_6
ADCinp_14 ADCinp_6

JA

RC_kill_test
testpin
Sonar

JB

RC_induc�ve RC_mode RC_tuner RC_killswitch


RC_thro�le
SD card

RC_rotx
connec�on

JC
HDMI

RC_rotz RC_roty
connec�on

Motor FL Motor FR Motor BL Motor BR


SWIPT 3
ethernet

JD
SWIPT 2
connec�on

pin1
pin7
Power

SWIPT 1

pin2
pin8
connec�on

connec�on

SWIPT 0

pin3
pin9
UART

USB

JE

pin10
pin4

IMU: I2C SDA IMU: I2C CLK Int_imu


JF

GND
GND
3V3
3V3

Figure 2. A Digilent Zybo development board with a Zynq-7000


SoC, with documented IO connections.

Zybo, Zybo Z7. If you want to better understand the design choices, do not hesitate
to ask the experts.

3.2. Programming and using the Zybo. How and where would you run all
the needed software? The FPGA of the Zynq can be programmed in Verilog and
VHDL (using Xilinx Vivado), and the ARM cores are programmed in C and C++
(using Xilinx Vitis) or in other higher-level programming languages running on a
Linux operating system. So, for EAGLE, we have Hardware and Software work to
be done.
6. REMOTE CONTROL 8

Figure 3. Inertial Measurement Unit

Figure 4. Camera + raspberry pi

4. The motion sensors


The Inertial Measurement Unit (IMU) consists of several sensors that measure
the motion of a device. An IMU detects the acceleration using one or more ac-
celerometers (usually three). In addition, it detects changes in the orientation of the
device (yaw, pitch and roll) using one or more gyroscopes (again three). Some IMUs
also includes a magnetometer, which can be used for calibration against orientation
drift, but this is not used in EAGLE.
You’ll get one of three possible IMUs: LSM9DS1, LSM6DS0, or MPU6050.
These IMUs connect to the Zybo over the I2 C bus. They also have an interrupt pin
that lets the software on the Zybo know that a new measurement is available.

5. The camera
The camera is connected to a raspberry pi, (see Fig. 4) that outputs a compressed
and uncompressed video stream. The raspberry pi is connected to the Zybo using
an ethernet connection and is connected to the ground station using a wireless
connection.

6. Remote control
A remote control is needed to control the EAGLE when not flying autonomously.
Besides, even when flying autonomously, a kill switch is needed to shut down the
motors. The kill switch functionality is embedded in the FPGA, with some safety
procedures, so it should work reliably! Obviously, it should only be used when the
drone is really out of control.
For the remote control, we make use of Taranis Q X7 and it’s schematic is shown
in Fig. 5. The remote control has the following control options:
6. REMOTE CONTROL 9

Figure 5. Remote control schematic

✓ The killswitch, located on the left of the controller, labelled as ”SA” in the
schematic.
✓ The inductive mode switch, located on the right of the controller, labelled
as ”SD” in the schematic.
✓ The modeswitch, located just left of the inductive mode switch, labelled in
the schematic as ”SC”.
✓ The tuning knob, the rotating knob left of the modeswitch, labelled as
”S2”. This knob allows you to tune a parameter of the control system
dynamically, without having to rewrite any code.
The remote control transmits in 9 channels combined in one PPM (Pulse Position
Modulation) signal towards its receiver. The RC receiver attached on the drone
frame is FrSky X8R (see Fig. 6). In the receiver the PPM signal is decomposed
in 9 independent PWM (Pulse Width Modulation) channels. Each PWM signal is
captured in the Zybo via the PMOD (Peripheral Modules) connectors and written
in the RAM by a dedicated PWM-block.

6.1. Killswitch. The killswitch is located in the upper left corner of the front
panel (labelled as ”SA” in Fig. 5). This three-position switch is a safety measure
incorporated in the drone. It is directly connected to a hardware block on the FPGA:
If the switch is flicked (topmost position of the killswitch), the outputs towards the
ESCs and the engines are tied to ground. This safety feature is a necessity, students
always need to turn this on when not operating the drone or when approaching the
drone.
Additionally there are some more safety checks inside this hardware block: a
pulse is transmitted from the Autopilot software, if for some reason this pulse is not
received by the block (e.g. the software is in a deadlock or infinite loop) the drone
kills itself, reducing dangerous behavior.
6. REMOTE CONTROL 10

Figure 6. Receiver

6.2. Modeswitch. The Modeswitch is located second to the upper right corner
of the front panel (labelled as ”SC” in Fig. 5). It is used to be able to switch between
three determined scenarios:
✓ Manual flight
When the switch is in the up position, the RC is being used to control the
drone.
✓ Automated altitude control
This mode corresponds to the middle position of the switch. In this mode,
the drone maintains altitude autonomously, but the pilot has to take care
of the navigation.
✓ Automated flight This mode is engaged with the mode switch in the lowest
position. First, the values of the RC are copied to a new set of registers
from which the control team gets its inputs (as to not have zero-values
inside these registers). After that, the camera starts transmitting its video
stream to the Zybo and video processing on the Linux core takes care of
calculating new output values for the motors.
6.3. Inductive mode switch. By flicking the other switch (labelled as ”SD”
in Fig. 5) on the middle or lowest position, we engage the wireless power and
information transfer. All outputs for the motors are tied to the ground (to limit
interference) and another PMOD pin starts transmitting a PWM wave towards the
on-board coils. This is only to be done once landed on the final land spot. While
flying, engaging in this mode will kill the motors, and the drone will fall to the
ground.
6.4. Arming procedure Drone. If the drone boots correctly, it is not able
to fly immediately. First we have to arm the drone. This is done by keeping the left
(throttle) stick on the RC in the lower right corner for some seconds (the second
LED on the zybo should be lit when armed and extinguished when disarmed, or
when a beeper is attached to pin JB7, two consecutive beeps will be produced).
When keeping the throttle stick in the lower left corner for a few seconds, the drone
gets disarmed and no flight should be possible (again, sounder creates beeps and led
8. POWER 11

is extinguished). More information can be found in the document about flying the
EAGLE.

7. Local speed control


The motors of the drone are current driven by three-phase PWM signals with
a high peak to peak value. Unfortunately the Zybo cannot provide these large
currents, therefore ESCs are used as a connection module. The ESCs convert the
single-phase PWM signal of the Zybo to a high-frequency and three-phase PWM
signal with a larger peak-to-peak value and send it to the motors. The three phase
system is required to be able to let the propellers rotate inside the magnets of the
motor.
Summarizing: The Zybo generates a PWM-signal per motor, which is captured
by the ESC. The ESC transforms the signal into a three-phased PWM signal with
the same duty cycle as the input, but higher in both magnitude and frequency.

8. Power
In this section, we briefly introduce the batteries (handle them with care!), the
power distribution and finally, the wireless power transfer of the EAGLE.

8.1. Power in: Battery. The main power source for the drone and its periph-
erals are LiPo batteries (see Fig. 7). LiPo batteries are commonly used in drone
applications or mobile phones. They have a large capacity, almost no self-discharge,
are capable of delivering high currents and have practically no ’memory effect’.
Mostly LiPo batteries consist of different cells placed in series, typically noted by
xS where x is a number representing the amount of cells in series. Precautions must
be taken NOT to charge these batteries with standard power supplies or standard
battery chargers: when not balancing the voltages, one cell can easily become over-
loaded and explode or catch fire! Therefore specific LiPo chargers must be used,
which measure the voltage of every cell independently and make sure they are not
overloaded. Also you must avoid the battery run flat/empty, as this may also destroy
your LiPo battery.
The provided batteries are 3S (11.1 V nominal) batteries with a capacity of
5.2 Ah. When charging them always verify that the charger recognised them as
3S batteries and set charge current to no more then 5.2 A. Do not discharge the
batteries at more then 52 A (10x their capacity or 10 C) for a long time (the drones
have a 50 A slow fuse)
To summarize:
(1) Always charge the batteries with the correct LiPo chargers and use a LiPo
safety bag.
(2) Always unplug the batteries when not using and store them in the safety
bag.
(3) Never allow the batteries to run dry.
(4) Never charge the batteries when there is no-one around.
(5) Always verify the correct charger settings: 3S (11.1 V nominal) and max
5.2 A charging current.
(6) Always connect the balance plug with the black wire on the - sign and with
pin 4 unused.
8. POWER 12

Figure 7. From the battery to the power distribution board to the


ESCs and motors.

8.2. Power Distribution. It is clear by now that many boards need to be


powered by just one battery. Some parts need 5 V, the ESCs need 12 V and lots
of amps, therefore we use a separate board. The Power distribution board has two
output circuits to be able to attach all necessary components at the right voltage:
✓ 12VDC :
✓ ESCs need a lot of amps so they are directly connected to the battery,
the Power distribution board helps for a clean looking connection.
✓ 5VDC :
✓ Zybo
✓ RC receiver
All other boards can be chosen to be powered by the Zybo: By using the 3.3 V
connections on the PMOD Connectors. The Zybo itself can be powered via an
USB-connection or an external power-supply (the battery in our case). It must be
noted that if the Zybo is powered via USB, it has less power to connect peripherals.
When connecting lots of peripherals while powering through USB voltage brownouts
may occur. This is easily detectable since the Zybo reboots in that case.

8.3. Power out: Simultaneous wireless power and information transfer


(SWIPT). The drone should be able to fly autonomously and land so that the
transmitter and receiver for the SWIPT are close and aligned enough to enable
power and data transfer. The transferred power must be enough to power the a
LED logo (similar to that of Fig. 8) and a LCD screen. The LCD screen must
display some piece of information, which will be communicated during the course
sessions. It is part of the task to make sure that the required information is correctly
exchanged between receiver and transmitter.
It must be kept in mind that the data and the power transfer are not separate
but need to occur through the same inductive channel.
9. CONCLUSION 13

To make sure that the power output is high enough to turn on these devices, the
system design must be as efficient as possible.
The transmitter circuit must be mounted on and fly with the drone itself. The
power source is the 12V DC battery, which after landing stops powering the flying
drone to supply power to the SWIPT circuit. Parts of the transmitting circuit should
be controlled by the four SWIPT outputs on the zybo. The receiver circuit is placed
somewhere near the landing spot and is connected to the load.

Figure 8. Wireless power transfer used to power a LED logo

9. Conclusion
This chapter provides a very high-level overview of the EAGLE hardware com-
ponents and helps you get started. If all the hardware parts are brought together
in the EAGLE frame, the system will resemble the one represented in Fig. 9.

Figure 9. This is how the complete EAGLE looks like.


CHAPTER 3

Simultaneous Wireless Information & Power Transfer


(SWIPT)

1. Reading Guide
This chapter is the starting point of the EAGLE SWIPT task. SWIPT stands
for Simultaneous Wireless Information and Power Transfer and comprises two dis-
tinct parts inductive data transfer (IDT) and inductive power transfer (IPT). We
strongly recommend that you read this document first as it contains high-
level information that gives you a clear idea of what the task is about.

The other information relevant for swipt in the EAGLE book:

✓ Chapter 15 Wireless power transfer theory: a Wireless Power Transfer the-


ory chapter, on how to achieve efficient power transfer with resonant coils.
✓ Chapter 18 PCB Simulations and Design: tutorials to get you started with
Spice and Verilog simulations and a list of PCB requirements and common
design mistakes to avoid. This is the chapter you will mostly need in the
first semester.
✓ Chapter 19 Vivado Hardware Design Project: information on how to use
Vivado in general and for the SWIPT assignment. You will only need
Vivado in the second semester.
✓ Chapter 20 Vitis Software Design Project: information on how to use Vi-
tis. Relevant until tutorial 20.3.2. You will only need Vitis in the second
semester.

We advise that, after familiarising yourself with the eagle project and the SWIPT
task, you start reading the power transfer theory chapter 15. This will help you
formulate an idea of the SWIPT system. Based on this you should start looking
for the needed components, this will be both analog (circuit) and digital (control)
components. Once you have insight in the needed analog circuit, you can try to
simulate it in LT-spice. Here you will be able to verify the basic functioning of
the circuit. Later you can also add digital simulations in Cadence, for this you can
follow the tutorial on co-simulation in chapter 18.
At T2 you are required to hand in a PCB, when you are ready to start the
design, you should read chapter 18 on PCB design requirements and commonly
made mistakes.
For T3 you will use the Zybo to implement your digital control on the FPGA.
The needed information to get started with this can be found in chapter 19. It is also
useful to look at chapter 20 as some software is needed to get the FPGA runnning.
Documents that are available to provide more in-depth information next the the
EAGLE book:
14
2. SWIPT TASK 15

✓ SWIPT Book inductive powering: a reference work on inductive power


transfer. You can scan the book for information you need, as it can help
you in your calculations to design the circuit.
✓ Papers on wireless power transfer in the articles directory on git (same
directory as the book).
✓ A list of hardware components to help you choose the ones you need for
your system 7.3. Most of their datasheets can also be found in the datasheet
directory on git.
These documents can be accessed when needed. There are many more documents
and files that will be used throughout the year, and they will be explained at the
right time, when you follow the EAGLE book as explained above.

2. SWIPT task
At the end of its journey across the QR-code guide route, the drone’s purpose
is to light up the ESAT LED logo situated at the landing spot. At first, this might
seem an easy task, but of course the drone cannot properly fly whilst being attached
to the LED logo with a power cable. Further, establishing an electrical connection
upon landing requires good accuracy during the descent and would fail very prone
to failure. To ensure a reliable link, wireless power transfer is the best option. If
you can make it work of course!
Assuming you have a working system, you should be interested (as TAs are)
in how the system performs and how it behaves under varying conditions. To that
end, status information about the link has to be displayed on a LCD screen at the
landing spot. You will find out that some of this data is only available in the part
of the system mounted on the drone itself. Hence, it requires a data link between
the power transmitter on the drone and the power receiver at the landing spot. The
goal is to use the ’existing’ power link simultaneously as a data link to transmit the
necessary information.
After a data link between the drone and the landing spot is established, other
sub-teams might have some relevant data to display as well. You should thus facili-
tate an interface between your system and the other system(s) on the drone. This
should allow you to display an almost unlimited amount of status information from
the drone. The TAs want to see, at least, the information of one other submodule,
for example the coordinates of the trajectory the drone followed to reach the landing
spot. Of course, you can display more data as well.
Oh, and one more thing ... It wouldn’t be an engineering task without some
optimizations. Since the battery of the drone is a limited energy source, you should
try to minimize the overall power consumption of your system. This means consid-
ering the efficiency of your power link during its design and minimizing the power
consumption of your receiver board. Further, you should avoid to send more power
than strictly necessary to run the receiver, LCD and LED logo, as any surplus power
is just wasted. However, sending the precise amount of power is tricky as the trans-
mission losses may vary and the transmitter thus needs to adapt its output power
accordingly. To do so, the transmitter should know how much power is actually
received by the receiver. This requires that data is transmitted both to and from
the receiver. The one-directional data link must thus be upgraded to a bi-directional
link.
3. POWER TRANSFER 16

3. Power transfer
The goal of the design of the wireless power transfer system is to power the
led-logo at the landing spot. The power transfer should be able to bridge the gap
between the power transmitter coil and power receiver coil. For the drone to land
safely on or next to the receiver coil, you should assume a distance around 20 cm
between the two coils. The receiving end of the system uses the transmitted power
to decode the information you have sent and displays that information on the LCD
display. At the same time, the LED logo will light-up to confirm your success. To
finish this task successfully you must understand Ampere’s and Faraday’s laws.
Ampere’s law states that a magnetic field is produced around a conductor carry-
ing electric current with a strength proportional to the current. Faraday’s law states
that an alternating magnetic field can induce an electromotive force (EMF) in a con-
ductor that is proportional to the magnetic field strength and its rate of change. An
alternating current circulates through the transmitter coil and generates an alter-
nating magnetic field which induces an electromotive force in the receiver coil, as
seen in Fig. 1. This leads to a current flowing through the end load that is connected
to the coil.
To design your system you need to decide:
(1) The number of coils, the dimensions of the coils, the number of turns per
coil.
(2) The resonant circuit topology (SS, SP, PP, PS) (where S refers to Series
and P refers to parallel, see theory document for further details).
(3) Values for the inductance L and capacitance C of the coils at the desired
resonance frequency f , the following relation holds:
1
(3.1) f= √ .
2π LC
Once resonance is achieved it is crucial to consider some/all the losses that
might occur in the system. Almost all losses find their origin in (small) deviations
from the ideal theoretical situation. For instance, in your simulations/calculation
you will probably assume a fixed distance between the coils and a perfect parallel
alignment. However, in real life the two coils might be misaligned or closer/further
away than planned. This will interfere with your mutual inductance, cause the
resonance frequency to shift and lead to sub-optimal performance of your system, to
the point where the system might completely fail. To allow a robust power transfer
you thus need to incorporate an algorithm that can adaptively tune the resonance
frequency in your system.

Figure 1. Resonant power transfer system.


6. MILESTONES 17

4. Data transfer
The LCD-screen at the landing spot should be used to display status information
of the otherwise invisible power link and the drone’s trajectory. The power link
information should include (but is not limited to) transmitted/received power, link
efficiency, resonant frequency and remaining battery voltage. Obviously, not all this
information is readily available at the receiver side. Hence a data link between
the transmitter and receiver on top of the power link is required. This data link
should be able to transmit all the necessary information from the transmitter with
an update-rate of about 1 Hz.
Digital communication over a channel (in this case a wireless inductive link) cor-
responds to transmitting logic zeros and ones. The maximum capacity of a channel
(the maximum amount of bits that can be transferred per second) is governed by
Shannon’s theorem:
 
S
(4.1) C = BW · log2 1 + .
N
S
In this formula, BW is the bandwidth and N is the signal-to-noise ratio (SNR) of
the channel. This means that in order to increase the data rate, either the BW
or the SNR should be increased. To achieve the desired update-rate, the bit-rate
should be high enough and thus the right balance between BW and SNR should be
determined.
Of course, the data link should not interfere with the powering part of the task.
This means that the receiver board should always be powered, it is not acceptable
to have periods where the LCD screen or the LED logo are turned off. Further, the
data link effect on the efficiency of the power transfer should be minimal and the
data link should be able to work alongside your resonance tracking algorithm.
In order to transmit data over the power link, the data has to be modulated on
top of this link. Several modulation techniques exist (AM, FM, PM, OOK, ...) each
with their own advantages and drawbacks.

5. System layout
The system is made up of a transmitter circuit and a receiver circuit, as illus-
trated in Fig. 2. On the transmitter circuit, you connect the battery of the drone
to a printed circuit board (PCB) that will generate the appropriate signal to send
through to the transmitter coil. On the receiver, there will be a receiving coil con-
nected to a PCB that will power the load (LCD and LED logo). The design of these
boards and the placement of the coils (perpendicular/parallel to the ground) are
completely up to you (design parameter!). The PCBs should include all the com-
ponents you need to achieve both resonant wireless power transfer (with adaptive
frequency tuning) and data transfer.

6. Milestones
Two abbreviations are used to shorten the milestone description:
✓ BF: basic functionality of the task. This includes successful power transfer
that turns on the LCD and LED logo and tracks the resonance frequency
when conditions change.
✓ OF: optimized functionality of the task. On top of BF, OF is the successful
addition of data transfer to the system. Via this data transfer, information
about other modules should be able to be displayed on the LCD screen.
6. MILESTONES 18

Figure 2. Rough system layout.

The receiver should also be able to send some information back, in order to
minimize the transmitted power so that maximum power efficiency can be
reached. Such a bi-directional communication link also helps to ensure that
under changing conditions the transmitter adapts the transmitted power
correctly to keep the receiver working.
The milestones are split over four evaluation moments, T1-T4. The dates of these
evaluation moments are available in the general EAGLE calendar (check Toledo).
✓ T1 – Understand & plan phase.
The goal is to deliver block diagrams for the transmitter and receiver PCB
that show a good understanding of the task and its challenges. The dia-
grams should include as much as possible details on the electrical compo-
nents and hardware/software usage. Note that T1 is not graded, but some
criteria are provided nonetheless.
✓ Failing:
No block diagram has been made. OR
The block diagram shows that the task at hand has not been studied
in detail.
✓ Struggling:
The design in the block diagrams does not meet BF.
OR
The block diagrams lack vital details.
✓ Sufficient:
The design in the block diagrams meets BF.
✓ Advanced:
The design in the block diagrams meets OF.
✓ Exceptional:
The design in the block diagrams exceeds OF and proposes a useful
innovation to the system.
✓ T2 – Modeling phase.
Further improve your design by modeling it via simulations and/or bread-
board setups. Once confident on your design, implement and submit the
transmitter and receiver PCBs. See PCB-related documentation for details
on how to submit your PCB designs.
6. MILESTONES 19

✓ Failing:
There are no simulation results to indicate that the design works.
OR
The PCB designs for transmitter and receiver have not been submitted.
✓ Struggling:
Simulation results do not fully support the PCB designs.
OR
PCB designs have been submitted but one of the following conditions
applies:
✓ Not DRC clean.
✓ Fundamental mistakes that will impede the design’s functional-
ity.
✓ PCB is ’ok’ but has been submitted more than 3 times for review
by the TAs.
✓ Sufficient:
Simulation results support the BF of the PCB designs (i.e. a working
power transfer + frequency tracking algorithm).
AND
PCB designs with no obvious issues have been submitted.
✓ Advanced:
Simulation results support the OF of the PCB designs (i.e. a working
power transfer + frequency tracking algorithm and communication
between transmitter and receiver in both directions is possible).
AND
PCB designs without obvious issues have been submitted.
✓ Exceptional:
Simulation results exceed OF and demonstrate a useful innovation to
the system.
AND
PCB designs without obvious issues have been submitted.
✓ T3 – Component phase.
Solder, test and debug your (PCB) design. Make modifications where nec-
essary and improve if possible. Present a standalone functional WPT mod-
ule.
✓ Failing:
System cannot power LCD and LED logo.
✓ Struggling:
System powers LCD and LED logo but BF is not fully achieved.
✓ Sufficient:
System achieves working power transfer to power LCD and LED logo
+ working frequency tracking algorithm.
OR
+ Communication between transmitter and receiver is possible.
✓ Advanced:
System achieves OF.
✓ Exceptional:
System exceeds OF and implements/demonstrates a useful innovation
to the task.
7. TOOLS AND COMPONENTS 20

✓ T4 – Integration phase.
Integrate your system with the rest of the drone. Provide a reliable interface
to communicate with the rest of the drone. Use this link to display the
drone’s route and some information from the other subteams. See the
integration milestone document for the specific milestones.

7. Tools and components


To build the system you need some tools and components. Some are provided
in your EAGLE locker/toolbox, these mainly include tools and wires. Others are
provided by the TAs and some you might have to find yourself.
7.1. Provided components. At 7.3 you find a list of components that are
made available by the TAs during the sessions. You are advised to look through
this list to build your system as the TAs believe that it contains most of the key
components you will need.
Aside from this list you have also access to the in-house stock from CDE with
your CDE-card. They provide several common components such as resistors and
capacitors over a range of values, on their website you can find a complete list. Your
card is not limited but usage is monitored!

Note that some components get damaged rather easily if you use them incor-
rectly. Although this can happen to anyone, please try to avoid it! If throughout
the project the TAs come to the disappointing conclusion that you make
no effort to respect your components, i.e. you keep destroying the same
component(s) without reflecting on what you are actually doing, this will
result in a lower grade on material handling.
7.2. Requesting other components. Besides the provided components list,
you can look up components yourself where ever you want. However, to request/order
them, their are some limitations you should take in to account.
✓ Orders can only be placed through farnell or rs-components.
✓ Components must be available in the UK/EU/BE stock.
✓ Orders need revision by one of the TAs.
✓ Orders placed before 4pm typically arrive the next workday.
This should only be used for components that are not available already. Often,
when you would like a resistor or capacitor with an exotic value, you are probably
able to achieve the same functionality with similar resistors or capacitors from CDE
(or a combination of them).
7.3. List of components. The SWIPT system components available at CDE
can be bought with your CDE-card. All components that CDE has in stock can be
found at: https://www.esat.kuleuven.be/cde/Stockartikelen%20Mag%20E%20CDE.
pdf.
Besides, some additional components are available in the EAGLE lab:
✓ Arduino Nano
✓ Zybo Board (shared with other modules)
✓ Power MOSFETs IRFSL7440PbF
✓ MOSFET driver ICs IR2110
✓ 1mF decouple capacitor
✓ Copper wire
7. TOOLS AND COMPONENTS 21

✓ Surface Mount Power Inductor ELT3KN028C


✓ Current Transformer 56100C
✓ LM2575HVT-5/12 Buck (Step Down) Switching Regulator 5V/12V.
✓ A standard opamp which you can use for multiple purposes.
CHAPTER 4

Attitude and Navigation Control (ANC)

1. Introduction
1.1. Overall task. The goal of the Attitude and Navigation Control (ANC)
module is to design a system that allows the EAGLE drone to autonomously move
through a sequence of checkpoints indicated by QR codes. This system is called
Autopilot 1. The controller will be implemented in Matlab first, where it will be
validated in simulation. Afterwards, the controller code is ported to the C++ frame-
work. The provided code can already configure the platform, read sensors, output
to the motors and LEDs, etc. A description of the autopilot functionality is given
in the remainder of this section.
1.2. Flight modes. The autopilot system has three flight modes: manual,
altitude-hold and autonomous. The flight mode can be changed by flipping the
3-position switch on the remote controller (RC).
✓ In manual mode, the pilot has control of the drone’s reference orientation
and the thrust. It is the task of the attitude controller to stabilize the drone
at the reference orientation using data from the inertial measurement unit
(IMU).
✓ In altitude-hold mode, the pilot only has control of the drone’s reference
orientation. The altitude controller takes care of the thrust in order to keep
the drone at a constant altitude, using data from the sonar.
✓ In autonomous mode, the pilot has no control of the drone. The goal
of autonomous mode is to first loiter for 10-15 seconds, then navigate a
QR trail, and finally land on the landing platform (on the last QR code).
To do this, the altitude controller takes care of the thrust, and the posi-
tion controller must create an appropriate reference orientation to send to
the attitude controller, using data from the image processing (IMP) mod-
ule. Furthermore, the autonomous navigation system generates a reference
trajectory of target positions to navigate from one QR code to the next.
1.3. Control systems. As mentioned before, our autopilot contains three con-
trol systems: attitude, altitude and position. This is illustrated in Figure 1.
The innermost control system controls the drone’s attitude. In the first stage, it
receives a reference orientation (i.e., a pitch ϕ and roll θ) from the RC. The attitude
control system should produce an appropriate signal to the motors to steer the drone
toward the reference orientation. In a later stage, it will receive this reference from
the position control system — this system produces reference orientations in order
to steer the drone toward a reference position (xr , yr ).
Somewhat independent of the other two systems is the altitude control system.
This control system adjusts the thrust to keep the drone at a constant altitude.
1An autopilot is a flight controller with additional functionality (e.g. altitude control, au-
tonomous flight, ...).
22
2. TASK LIST 23

uc — manual mode
remote control
 
ux
manual & attitude hold mode — (φ, θ)  uy 
target uz
(xr , yr ) orientation
measurement (φ, θ)
(x, y)
camera view position attitude motors
uc

integration with:
– crypto: target
– vision: measurement
– comms: logging/gui
altitude

Figure 1. General outline of the autopilot system.

Milestone: T1
1.1. Material reading + introduction to LQR/KF. 1 week
1.2. Planning. 1 week
Milestone: T2
2.1. Develop attitude controller/observer in simulation environment. 2 weeks
2.2. Finish the StartUp C++ project. 1 week
2.3. Implement, test and tune attitude controller. 4 weeks
2.4. Develop altitude controller/observer in simulation environment. 2 weeks
Milestone: T3
3.1. Implement, test and tune altitude controller. 2 weeks
3.2. Develop position controller/observer in simulation environment. 1 week
3.3. Implement and validate position controller to loiter continuously. 4 weeks
Milestone: T4
4.1. Test and tune position controller to loiter continuously . 3 weeks
4.2. Track reference trajectory. 1 week
4.3. Generate trajectories from QR codes. 2 weeks

Using these three control systems, we will be able to steer the drone to any desired
point (xr , yr , zr ).

2. Task List
2.1. Task overview. This project will span two semesters. In the first semes-
ter, the main objective is to fly the quadcopter, make it respond to commands from
the RC and implement a logging system which will be of high importance in the
second semester. This logging system is not included in the milestones explicitly
since it is considered an integration deadline and will therefore involve collaboration
with other subteams. In the spring semester, the focus will be on the implementa-
tion of an altitude controller, loiter mode and navigation controller and of course
the final deliverable which is a fully autonomous quadcopter. The duration of each
task which is shown below is only an estimate, and teams may choose to distribute
their time and resources differently.
2.2. Detailed description of T1 tasks. The following tasks should be com-
pleted before the T1 deadline.
2. TASK LIST 24

✓ Task 1.1. Material reading + introduction to LQR/KF. An introductory


video on linear quadratic regulation (LQR) and observer design using the
Kalman filter (KF) is provided on Toledo in the Videos folder of P&O:
EAGLE - Elektrotechniek. Besides this video, there are certain documents
you need to study. These can be found in section 3.
✓ Task 1.2. Planning. In addition to studying the above material, you need
to prepare a plan for your subsequent developments. Your group should
discuss
(1) how your Matlab code will be organised,
(2) what simulations you need to run,
(3) what objectives you need to reach in simulation before you test the
generated C++ code on the drone,
(4) what unit tests you will create and which tool you will use to create
them (MEX/Google Test),
(5) who does what and when (timeline and collaboration scheme). A useful
tool for organizing collaborative projects is Trello.
The result of this initial planning phase will have to be altered throughout
the project since some decisions can not be made right away (e.g.: the
Matlab code structure may not be clear until you experiment with your
simulation environment).
Point (5), is very important. ANC is especially challenging in terms
of integration, so it is vital that you begin with that as soon as possible.
Clear communication with the IMP and COMMS modules is especially
important. As soon as everyone in the ANC group is comfortable with the
LQR/KF control theory, you should aim to parallelize as much as possible:
For example, development on the altitude controller can already start before
attitude is finished, the logger can be worked on independently, one person
can work on making the simulator more realistic (correct modelling of sensor
noise, sampling frequency, model noise), and so on. Do not procrastinate
the C++ development in your planning, make sure at least one person is
familiar with the framework by the time development starts. You don’t
have to wait for the attitude simulations in Matlab to be complete before
starting on the StartUp project and the code generation.
2.3. Detailed description of T2 tasks. The following tasks should be com-
pleted before the T2 deadline.
✓ Task 2.1. Develop attitude controller/observer in simulation environment.
✓ Step 1: Identify the system inputs, states and outputs for the attitude
and navigation control loops; determine the desired equilibrium point
of hovering; linearize the system dynamics about this point; discretize
the linear dynamics; compare the linearized system with the nonlinear
one (in simulation2).
✓ Step 2: Design an LQR/KF attitude control system; determine and
plot the poles of the controller and the observer; account for input con-
straints (by saturating the input in your simulations); perform closed-
loop simulations using the nonlinear continuous-time system; fine-tune
2In your simulations, you have to test your controllers against the continuous-time nonlinear
model of the quadcopter and account for modelling errors and measurement noise for your simu-
lations to be realistic. You may find details in the slides of the course and Ch. 11. Pay special
attention to the functions ss, dlqr, dlqe and kalman.
2. TASK LIST 25

the controller; is it resilient to measurement noise and modelling er-


rors? Plot closed-loop system trajectories and appraise your design
choices.
✓ Task 2.2. Finish the StartUp C++ project. The given Autopilot project is
quite involved, it consists of over a hundred C++ source files and thousands
of lines of code. To make the learning curve a little less steep, we provide
a much smaller C++ project to allow you to familiarize yourself with the
language and the tools. You’ll have to submit your code (with pass/fail
grading) for T2.
✓ Task 2.3. Implement, validate, test and tune attitude controller. Develop
a code generation system (Codegen) to automatically generate C++ code
from Matlab. The chapters on Matlab and C++ in Part 2 can aid you
in this process. Simulate the generated controller using MEX and run unit
tests before running it on the Zybo3. Verify that the motors react to the
signals they receive from the controller. Without the propellers attached
to the motors, hold the quadcopter, tilt it and rotate it. Verify that the
motors respond accordingly.
Now, test the behaviour of the quadcopter on the gimbal vise. Fine-
tune the attitude controller by performing simple maneuvers with the RC
(the first “flights” should be on the gimbal vise). All flights should take
place in the specially designated area following all safety measures.
There are many repetitive tasks during the implementation phase (i.e.
updating controller tunings, generating the updated code, running unit
tests, creating boot images ...). It is therefore recommended to develop
a good workflow. You can use CMake to automate the entire build and
deployment process (e.g. automatically run the code generator and build
the C++ code if the Matlab code code changed), use tasks in VS Code4 to
automate common tasks (e.g. transferring files to the drone), set up SSH
to easily access the drone remotely, use a network socket to change tunings
on the fly, and so on. Other suggestions are to have a designated pilot and
assistant for drone preparation, streamline the logger early so you have
feedback of what’s going on in the software, learn how to use a debugger ...

✓ Task 2.4. Develop altitude controller/observer in simulation environment.


Design, simulate, tune and analyze the altitude controller in Matlab. This
should be very similar to task 2.1.

2.4. Detailed description of T3 tasks. The following tasks should be com-


pleted before the T3 deadline.
✓ Task 3.1. Implement, validate, test and tune altitude controller. Use the
Codegen from Task 2.3 to generate C++ code for your altitude controller.
Validate the generated controller using MEX and run unit tests before
running it on the Zybo. Note that for this task, the pre-processing of sonar
measurements is of very high importance. Before flying in altitude-hold
mode, verify that the altitude observer is receiving good measurements.
Then enable the controller, but don’t actually feed the output to the motors
just yet. Fly in manual mode and verify that the control actions generated

3Development board of the drone, see Section 3 for details.


4https://code.visualstudio.com/Docs/editor/tasks# custom-tasks
3. READING GUIDE 26

by the altitude controller are sensible. Finally, test and tune the altitude
controller.
✓ Task 3.1. Develop position controller/observer in simulation environment.
Design, simulate, tune and analyze the position controller in Matlab. This
should be very similar to tasks 2.1 and 2.4.
✓ Task 3.3. Implement and validate position controller to loiter continuously.
Implement an “observer” that provides position and linear velocity esti-
mates and an LQR controller to steer the position of the quadcopter to a
desired position. This “observer” doesn’t necessarily have to be the output
of a kalman filter. Kalman filters use system matrices that are discretized
at a certain frequency and therefore don’t work well when position esti-
mates from the vision module come in at a non constant frequency. It is
left to the team to decide what the best estimator of the state is.
Validate the position controller by integrating it with your simulation
environment using MEX or alternatives.

2.5. Detailed description of T4 tasks. The following tasks should be com-


pleted before the T4 deadline.
✓ Task 4.1. Test and tune position controller to loiter continuously. Test the
controller and the observer developed in task 3.3 on the drone. This part
of the project calls for close collaboration with the vision team, since you
must ensure that the data being sent is good enough for flight (i.e. at least
10Hz on the Zybo, and no excessive jumps in position estimate). Make
sure to use realistic timings in your simulator to make the behaviour as
consistent with reality as possible.
✓ Task 4.2. Track reference trajectory. Provide a trajectory to the quadcopter
and command it to follow it. For example, command it to move from its
initial position a few squares away and back.
✓ Task 4.3. Generate trajectories from QR codes. This is the final stage of
development where the paths are generated from the coordinates provided
by the QR code decryption. Students should add redundancies in the design
to make sure that the drone is able to reliably detect QR codes.

3. Reading Guide
As you probabily already noticed, there is a repeating pattern in the development
of the three controllers (i.e., attitude, altitude, position). You always proceed in the
phases (i) develop the controller in simulation; (ii) implement the controller; (iii)
validate the controller. We will go through each of these stages and provide an
overview of what documentation is relevant to complete this phase.
Note that everything in Ch. 25 and Ch. 26 relates to ANC. Therefore all tutorials
in these chapters will be relevant at some point. So even if we do not mention a
tutorial in these chapters below, note that it could still be of interest.

3.1. Development & simulation. In this phase you will develop your con-
trollers and observers, and decide on a preliminary tuning using a simulator.
✓ This phase applies to Tasks 2.1., 2.4., 3.2.
✓ The EAGLE control slides contain most of the theoretical background re-
quired: Module information/ANC/theory.
It explains LQR/KF, quaternions and the attitude/altitude and position
3. READING GUIDE 27

models. A video is also provided on toledo in the Videos folder of P&O:


EAGLE - Elektrotechniek, where a TA goes through these slides in detail.
✓ The folder Module information/ANC also contains other background mate-
rial for the interested reader like articles on attitude control and datasheets
as well as some slides on LQR and the Madgwick filter used to estimate
the orientation of the drone from IMU measurements.
✓ In Part 2 you can also find information on the programming languages
used throughout the project. Specifically Ch. 11 contains extremely useful
tips on coding in Matlab and various example scripts for linearization,
discretization, developing controllers and simulating systems. We strongly
advise to go through the examples in this chapter.
✓ Altitude and position: once you start developing the hierarchical control
scheme for altitude and navigation, you will need to include sensor timings
in your simulator. Ex. 25.3.1 goes through the basic components of such a
simulator.
✓ Tut. 25.3.1 describes the Matlab demo script.

3.2. Implementation. In this phase you will implement your control laws in
the Autopilot framework running on the drone.
✓ This phase applies to Tasks 2.3., 3.1., 4.1.
✓ Development of the controllers happens in C++. See Ch. 12 for a primer
on the language, and finish the StartUp assignment. Get familiar with the
tools, VS Code, the testing framework, SSH, using the debugger, sanitizers,
etc.
✓ The provided framework is documented in Ch. 16 and in the generated
doxygen documentation.
✓ In the first phase of the project, you will run your C++ code through Mat-
lab instead of directly on the drone. To learn how to call the given C++
framework through MEX, follow Tut. 25.2.1 and Tut. 25.2.2.
✓ Once you know how to run code through MEX, it is time to develop your
own C++ implementation of your controllers. These components will be
implemented using principles of code generation. See Ch. 11 for a high
level introduction and Ex. 25.2.1 for more ANC specific instructions.
✓ To easily navigate the code and ease the development of your C++ code, you
should open the Autopilot and Simulator frameworks in VS Code. Tutorial
26.2.1 gives detailed instructions how to do this.
✓ To get familiar with the provided framework, Ex. 26.5.1 is a good place to
start.
✓ Learn how to use the logger and to communicate with other processes in
Exercises 26.6.1 to 26.6.3.
✓ Ex. 19.6.1 and Ex. 19.6.2 introduce the kill switch and tuner knob of the
remote controller.

3.3. Validation. In this phase you will validate your implementation. First in
simulation, then on the real drone platform.
✓ This phase applies to Tasks 2.3., 3.1., 4.1., 4.2., 4.3.
✓ Before running your autopilot implementation on the drone it is useful to
validate it in simulation. To do so, a MEX wrapper is provided
(a) A general introduction to MEX is provided in Ch. 11. ANC specific
info is provided in Ch. 25.
3. READING GUIDE 28

(b) Tut. 25.1.1 guides you through installing a C++ compiler that is usable
by Matlab.
(c) Tut. 25.2.1 describes basic usage of the provided MEX wrapper.
(d) Tut. 25.2.2 describes the main class that acts as the wrapper of the
C++ MainControllers function.
(e) Ex. 25.2.3 provides info on debugging MEX.
(f) Ex. 25.2.2 provides tips on how to understand and modify the MEX
wrapper for the project.
✓ Once you are confident that your implementation is safe and predictable,
you can run the code on the drone itself. This is described in Tut. 26.3.3
and 26.3.4.
✓ The tutorials in Ch. 27 are relevant for debugging hardware issues.
CHAPTER 5

Image Processing (IMP)

1. Introduction
1.1. Overall task. Cameras are a popular sensor for drones: they are rela-
tively cheap and lightweight, and convey a lot of information that can also easily
be interpreted by humans. The camera mounted on the EAGLE drone is oriented
downwards: it is mounted beneath the drone base looking down at the ground.
When the drone is hovering, the camera’s optical axis is (roughly) aligned with the
direction of the gravitational field and (roughly) perpendicular to the ground plane.
In the context of EAGLE, the camera will be the main sensor for performing the
mission. It will be used both for localization of the drone (with respect to a grid of
red lines on the ground floor), as well as for the detection and decoding of QR codes
containing instructions for the drone.
1.2. Top Level Goals. This module has two goals. First, it is the localizer
for the drone. It needs to tell the drone where it is. Algorithms executing this task
have to be designed, implemented and tested. The algorithms have to be integrated
with the outer control loop of the drone’s navigation module, which requires the
algorithm to run in real-time. To do this, the algorithm and its implementation
have to be efficient because the drone’s Raspberry Pi only has limited resources.
The second task tells the drone where to go. This is done by scanning and reading
QR-codes, using an existing library. The image processing module is critical for the
EAGLE mission. Without it, autonomous flight is impossible, as the drone will have
no idea where it is, and no idea where it needs to go.
1.2.1. Localization. To help with the localization, we put a rectangular grid of
red lines on the ground floor. See Figure 1. The localization is only important when
the drone is in ‘autonomous’ flying mode. When the drone is controlled by the pilot
through the remote control, no image processing is required. Before switching to
autonomous mode, the drone should be hovering in a stable manner above one cell
of the grid. This cell will then be considered as coordinate (0, 0).
1.2.2. QR detection and decoding. While in autonomous flight, the drone will
encounter mostly empty grid cells. However, some of them will contain QR-codes.
These codes contain encrypted instructions, which tell you where to go. You can
use a library (ZBar ) to detect and convert this code to a bitmap. The information
in the bitmap will still be encrypted, and has to be deciphered by the cryptography
module. Because the cryptography module does not run on the Raspberry Pi, you
will have to send to encrypted string from the Pi to the Zybo.

2. Task List
2.1. Overview. The project will span two semesters. The first semester is
centered around developing a correct and efficient algorithm for localization, using
the grid of red lines. This algorithm has to be developed, implemented and tested.
29
2. TASK LIST 30

In the second semester, QR detection has to be integrated into the algorithm. The
resulting algorithm should run at high frame rates on the Raspberry Pi on the drone.
Finally, the algorithm has to be integrated with the other modules to achieve to final
goal of autonomous navigation. Below some of the subtasks are explained in more
detail, more or less in chronological order. Though, you should always refer to the
milestones to know what should be ready when.

✓ Material reading: First you’ll have to get familiar with the task and the
role of image processing module in the overall project. See Section 3 for an
overview of all tutorials and information of this module.
✓ Familiarize with OpenCV : OpenCV is a library for image processing, that
will be very useful for implementing the localization and QR-detection
tasks. It works with both Python and C++, but for EAGLE we will use
Python. Tutorial 23.3.2 serves as an introduction and provides links to tu-
torials with example programs containing several basic functions like I/O
and visualization. Go over these examples step-by-step to get familiar with
the library and its basic operations.
✓ Camera calibration: You may notice that the lines in Figure 1 are not
straight. This is due to radial distortion, as explained in the document on
image formation and camera models, see the reading guide below. This
effect can be compensated for by determining the camera’s internal param-
eters, and calibrating it. Once these parameters are known, a frame can
be ‘un-warped’ making the lines in the image straight again. This can be
done using OpenCV. However, this step is optional: it is also possible to
developed a robust algorithm that can cope with curved lines and does not
rely on straight lines.
✓ Red line detection: One of the first steps in the localization task consists of
detecting the red lines in the input image. Implement a filter that selects
all red pixels in the images, turning the original RGB image into a binary
image with ‘1’ values of (most of) the red line pixels and ‘0’ everywhere
else. Then, write an algorithm that localizes the horizontal and vertical
lines in the binary image and displays the result on screen.
✓ Localization: Design, implement, debug and test the full localization algo-
rithm on a PC, before porting it to the Raspberry Pi (be eware that your
PC is much faster than a RPi). Timing the different steps may be a good
idea. Optimize the code where needed. Some example videos are provided
(See reading guide below). Don’t test only with the given video’s, make
your own video’s for more testing. Once the algorithm works good enough
on video’s, start testing it on live camera feed. Before starting to work on
the RPi, it might be usefull to read Chapter 24.
✓ Improve robustness: Additional image processing building blocks or re-
finements may be needed, depending on the overall design of your team’s
system and tests thereof. This may include tracking corners for more accu-
rate localization or determining the orientation of the lines to compensate
for misalignment of the drone with the grid.
✓ QR code detection: QR detection does not need to be implemented by
yourselves. You can use the ZBar library for this. You have to extract the
QR code from a video frame and pass the information to the cryptography
module.
3. READING GUIDE 31

Figure 1. A rectangular grid of red lines, drawn on the floor. To


be used by the drone to localize itself.

✓ Integration: So far, the localization algorithm just wrote the coordinates


to a file or displayed them on screen. For integration with navigation con-
trol, the interface and communication have to be defined and implemented.
Likewise, the binary image generated by the QR detection module has to
be integrated with the decoding and decryption done by the cryptography
module.

3. Reading Guide
This section will give an overview of the available materials for IMP and where
to find them. Not all of them are equally important, but none of them are useless.
✓ Python and OpenCV: see the tutorials in Chapter 23 on how to install and
use Python and OpenCV in an Anaconda environment.
✓ Camera models: See the imp-camera-models.pdf file in the IMP gitlab.
In this file you will learn the relation between image, camera and world
coordinates. As well as information about radial distortions, the reason
why lines sometimes appear curved in the image.
✓ Recorded images and videos: A set of videos are provided, on which the
red line detection and localization can be tested. You can access this folder
from the ESAT PC’s by opening the Files application. Press Ctrl+L and
enter the path (don’t forget the first slash!):
/esat/tiger/tverelst/eagle/students/videos/
✓ Raspyberry Pi: read about the Raspyberry PI and how to use it in Chapter
24
✓ Linux: read about basic linux commands in Chapter 9.
CHAPTER 6

Cryptography (CRYPT)

1. Introduction
EAGLE drones are meant to fly autonomously along an instructed path. Such
a path consists of a series of waypoints. At each waypoint, drones are given one or
multiple new high-level instructions, for example, a new target waypoint position
or a landing instruction. In a real-world scenario, waypoints may be spread across
an adversarial environment. To prevent any mischievous actions, there has to be a
mechanism that ensures that the instructions are issued by the authorized parties
only. Any form of tampering with the instructions, malicious or otherwise, compro-
mises the EAGLE’s mission. Hence, the integrity of the instruction content has to
be ensured as well. Lastly, the instruction content itself must be kept confidential
to prevent potential tracking or disclosure of the following waypoints.
The goal of the CRYPT module is to implement this communication protocol.
The initial step will be to implement the protocol in software using the provided
cryptographic operations. The next step is to move from purely software-based
design to hardware/software (HW/SW) co-design. The customized FPGA logic can
significantly accelerate the execution of the timing critical cryptographic operations.
This implies implementing parts of these operations in hardware, while the rest
resides in software. To achieve this, a hardware accelerator needs to be modeled in
the hardware description language Verilog and implemented in the FPGA. Finally,
hardware and software need to be integrated to provide the necessary functionality
of the cryptographic module.
Before starting the CRYPT module, you should read the following chapters:
✓ Chapter 13 gives you Verilog background.
✓ Chapter 20 and Chapter 19 give you an introduction to the Xilinx tools.
We will use these tools to implement the HW/SW co-design.
Follow the above material, complete the Verilog introductory exercise and have it
running on the Zybo by yourself. This will help you understand Hardware and
Software (HW/SW) co-design.
The rest of this chapter will give you an overview of the cryptographic protocol
and the tasks of the CRYPT module. Then, the objective for the first semester is
to implement the EAGLE communication protocol in software. You will be able to
run the prototype of the communication protocol on Zybo under embedded Linux.
In the second semester, the objective will be to implement the hardware accelerator
for this protocol. You will see the advantages as well as the trade-offs of HW/SW
co-design on Zybo.

2. Roadmap
In this section, we list the steps we deem important for the successful completion
of the crypto module. Task 1.1 to 2.3 and part of 3.1 is supposed to be finished in
the first semester. If you finish these tasks early, you can start working on the
32
2. ROADMAP 33

tasks of the second semester. The project is seen as a whole, so the tasks are not
independent of each other.

Tutorials and Material Reading. Task 1.1 should be completed before T1


deadline.
✓ Task 1.1 Material reading. Get acquainted with this chapter and the tasks
of the crypto module.
✓ Task 1.2 Introduction to Verilog, Vivado and Vitis. First read the Vivado
and Vitis chapters in Chapter 19 and Chapter 20. Evaluate your knowledge
about Verilog and read up on the information in Chapter 13 if needed.
✓ Task 2.1 Hands-on Tutorial. Complete the Verilog introductory exercise
described in Section 1 and have it running on the Zybo. This task gives
you hands-on experience with implementing HW/SW co-design on Zybo
and getting familiar with the toolchain used in this module.

EAGLE Protocol and Software Implementation. In the evaluation of T2,


your understanding of the EAGLE protocol of Task 2.2 and the software implemen-
tation on Zybo of Task 2.3 are the subjects. After that, you start the implementation
of the hardware accelerator.
✓ Task 2.2 EAGLE protocol. Carefully read the description of the communi-
cation protocol in Section 4. You should also apprehend the cryptographic
concepts and the used notation.
✓ Task 2.3 Implement and test the EAGLE communication protocol in soft-
ware. Implement a prototype of the communication protocol in C by using
the provided software libraries for cryptographic computation. Your code
should be deployed on the Zybo under Linux. To set up the CRYPT C
project, refer to the information in Chapter 21. For background on the C
programming language, refer to Chapter 10.

Hardware Accelerator and HW/SW Co-design. The following tasks should


be completed before the T3 deadline.
✓ Task 3.1 Implement and test the hardware accelerator for Keccak-f [400]
with a testbench.
(1) Get acquainted with Keccak Keccak specifications summary, FIPS
PUB 202 - SHA-3 Standard and Ketje CAESAR submission: Ketje
v2
(2) Use the C code for Keccak to generate the test benches for Keccak-f
[400] permutation and intermediate rounds.
(3) Implement the step mappings in Verilog for Keccak-f [400] permuta-
tion and test against your test vectors.
(4) Implement the Keccak-f [400] permutation in Verilog.
✓ Task 3.2 Implement and test the hardware accelerator for Keccak-p(*) [400,
nr] with a testbench.
✓ Task 3.3 Optimizations. Modify your HW accelerator to get better perfor-
mance. You can find some tips and recommendations in Section 5. Chapter
21 Section 2 describes the AXI (Advanced eXtensible Interface) protocol,
which is a way to communicate between the CPU and the hardware accel-
erator through Memory-mapped I/O (MMIO). You will use your design as
an AXI peripheral so that the computed result can be read by the CPU.
3. MILESTONES 34

Implementation and Integration to EAGLE Framework.


✓ Task 4.1 Implementation on Zybo. You need to implement your hardware
accelerator into the Vivado “drone” project and add the accelerator as an
IP core. Run your framework on the Zybo with the accelerator enabled,
you should see significant speedups. Next, you need to ensure the correct
formats of inputs and outputs of your Python wrapper.
✓ Task 4.2 Integration to EAGLE framework. This requires communication
with the other modules in your team and might require additional changes
in the code of your communication protocol.

3. Milestones
3.1. Milestone: T1 (Tasks 1.x). No evaluation for the modules in T1.
3.2. Milestone: T2 (Tasks 2.x). The goal of this milestone is to complete the
software implementation of the communication protocol and start on the hardware
accelerator.
Failing.
✓ The C prototype of the communication protocol does not work yet on an
ESAT machine.
Struggling.
✓ The C prototype of the communication protocol works on an ESAT ma-
chine, but not on the Zybo.
Sufficient.
✓ The C prototype of the communication protocol works on an ESAT ma-
chine, using the provided software library for cryptography.
✓ The C prototype of the communication protocol works on the Zybo under
embedded Linux, using the provided wrapper.
✓ Established initial interface requirements with IMP and ANC teams – re-
ceiving QR codes and forwarding of the commands.
✓ Implementate the five step-mappings for a round of Keccak-f[400] and test
the round function in behavioral simulation.
Advanced.
✓ Well structured, commented and modular C code.
✓ Error handling (flyback) for the invalid or malicious QR codes, and correctly
returning the highest-priority decrypted command
✓ Students have a good understanding of the fundamental cryptographic con-
cepts and the rationale behind them.
3.3. Milestone: T3 (Tasks 3.x). The goal of this milestone is to accelerate
the cryptographic operations in hardware with the AXI interface.
Failing.
✓ Hardware accelerator for Keccak-f [400] does not work in behavioral sim-
ulations.
Struggling.
✓ Hardware accelerator for Keccak-p(*)[400,nr] does not work in behavioral
simulations.
✓ A tested hardware accelerator for Keccak-f [400] in simulations.
4. EAGLE COMMUNICATION PROTOCOL 35

Sufficient.
✓ A tested hardware accelerator for Keccak-f [400] on Zybo Linux.
✓ A tested hardware accelerator for for Keccak-p(*)[400,nr] in simulations.
✓ Students understand the advantages and trade-offs of HW acceleration in
HW/SW co-design.
✓ Students can receive decoded data required for integration with IMP and
send decrypted waypoints to COMMS/ANC.
Advanced.
✓ Find bottleneck and do meaningful optimizations on the accelerator which
is tested on the Zybo with a custom HW/SW interface.
3.4. Milestone: T4 (Tasks 4.x). Integration tasks are evaluated with either
1pt or 2pts for completing a part of the EAGLE missions. For crypto, these points
can be earned by completing:
1pt The Eaglecrypt protocol is correctly integrated on the Zybo to receive QR
inputs from IMP and send decrypted instructions to ANC. The Eaglecrypt
protocol is part of the Python threading framework on the Linux core. The
team can move a hand-held drone over at least 2 QR codes. The location
of the drone is estimated and target positions are decoded from the QR
codes. The results are clearly shown on a display, including an indication
when a target position has been reached
2pt The Eaglecrypt protocol is extended to use hardware-accelerated hashing
and decryption on the Zybo FPGA part. The speedup is demonstrated.

4. EAGLE Communication Protocol


The goal of the cryptographic module of the EAGLE drones is to ensure se-
cure communication with waypoints and to translate them to the “LQR navigation
control” commands internal to the drone. Each waypoint contains a QR code with
encrypted instructions. The waypoints are placed on the rectangular grid on the
floor, as shown on the layout depicted in Figure 1. The crypto module of the EA-
GLE drone receives encrypted instructions by reading the QR code, decrypts and
authenticates them and then forwards them to the EAGLE control module.
A cropped image of the QR code (e.g. “qr.png”) is to be obtained from the
image processing team. Firstly data has to be decoded from the QR code. QR code
is a 2D bar code. In this particular case, it holds base64 encoded binary of the
waypoint messages. Base64 decoding is most easily performed in Python.
Performing base64 decoding results in a binary string (byte array) that conforms
to the waypoint message format described in Listing 6.1 (c.f. Section 4.1). It contains
instructions encrypted using different keys.
Each waypoint message contains one or more commands for each drone. This
can be viewed as if the waypoint was broadcasting the message to all the drones.
Therefore all commands need to be parsed and decrypted. Every instruction for
a drone is encrypted using a key derived from the drone’s master key and seed
material available at each waypoint. Your task is to obtain the instructions for your
drone. Attempts to decrypt instructions encrypted with any other key will result
in a decryption failure. Successfully decrypted instructions, ones meant for your
drone, have to be translated to “LQR navigation control” commands internal to
the drone and forwarded for execution. In case multiple instructions are issued to
a drone, the highest-priority instruction should be executed, where 0 denotes the
4. EAGLE COMMUNICATION PROTOCOL 36

Figure 1. Rectangular grid.

highest priority. This means that the command with priority 0 is executed when
there is another command priority 1. If more commands of the same priority are
issued on the same QR code an error must be reported. In the incidence of such
a QR code, the drone must be instructed to fly back to the original point where
it took off. This implies that you will need to implement a mechanism to store
all previous instructions and the take-off position within the framework. Note that
priority sorting and the flyback protocol are only required by students aiming for
advanced and exceptional grades. Figure 2 depicts the flow of this task. To verify
the correctness of your implementation, we provide several test QR codes and a
binary file with a drone master key (see Section 6). The QR codes are already
base64 decoded for use in your C protocol. Note that this key is only for testing
purposes and that each EAGLE team will get the unique (and secret) master key
before the final demo in the second semester.
4. EAGLE COMMUNICATION PROTOCOL 37

Figure 2. Secure communication task overview.

Tip 6.4.1

Important notes:
✓ If the drone is instructed to fly from Waypoint 1 to Waypoint 2, it
should ignore any and all QR codes it flies over on its way to Waypoint
2.
✓ We assume that the drone starts its mission from one of the QR codes.
The starting QR code can be arbitrarily chosen.

4.1. Message Format. The format of the waypoint message is given using
C-like pseudo-code in Listing 6.1. It defines how each byte of the message should be
interpreted. Plainly speaking (x,y) coordinates of a waypoint can uniquely identify
it in a given moment in time. Nevertheless, in practice, a waypoint may be updated
to a newer version, or corrupted by an adversary. To account for the uniqueness of
waypoints in time as well as space, we include the wid field. Commands for different
drones are encrypted under different keys. The format of the encrypted instructions
is given in Listing 6.2. A message can contain an arbitrary number of instructions,
hence the length of the EncInst* insts is determined by the uint8 n insts field of
the message. The nonce field contains the first 9 bytes of the complete nonce. Since
the nonce has to be unique for each instruction, the complete nonce is formed by
concatenating the 10th byte, which is different for each instruction and represents
the order of the instruction in the array, starting from 0 (e.g, the 5th instruction
has to be decrypted by appending byte ’x04’ to the 9 bytes from Listing 6.1).

Listing 6.1. Waypoint message format.


struct␣{
␣␣␣uint8_t␣nonce[9];␣// 72−bit nonce
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// = array of 9 bytes (uint8)
␣␣␣uint8_t␣wx;␣// x−coordinate of the waypoint
␣␣␣uint8_t␣wy;␣// y−coordinate of the waypoint
␣␣␣uint_t␣wid[2];␣// waypoint ID (2 bytes)
␣␣␣uint8_t␣n_inst;␣// number of instructions
␣␣␣// array of encrypted instructions = starting point of
␣␣␣// the array with arbitrary number of encrypted
␣␣␣// instructions
␣␣␣EncInst*␣insts;
}␣WaypointMessage;
4. EAGLE COMMUNICATION PROTOCOL 38

Listing 6.2. Encrypted instruction format.


struct␣{
␣␣␣␣␣␣␣uint8␣n_bytes;␣// byte size of the remaining fields
␣␣␣␣␣␣␣uint8*␣ct;␣// bytes of the instruction ciphertext
␣␣␣␣␣␣␣uint8␣tag[4];␣// 32−bit tag
}␣EncInst;

Tip 6.4.2

Note that parameters in the encrypted instruction format are chosen for simplic-
ity and educational purposes. For example, using 32-bit tags provides virtually
no security against forgery.

4.2. Instruction Format. We use a simple binary format for encoding drone
instructions. At this moment the set of instructions is limited to only two. Never-
theless, we choose to make room for future upgrades. Table 1 contains the encoding
information and valid priority levels for all the drone instructions. Students should
check that these priority levels are correctly issued, as well as that next wx and
next wy fall within the grid. In the case of faulty instructions, the drone should
land in place. Students aiming for advanced/exceptional grades should initiate fly-
back instead. Instructions formats are presented using C-like pseudo-code in the
Listing 6.3.
Listing 6.3. Drone instruction format.
struct␣{
␣␣␣␣␣␣␣␣uint8_t␣priority;␣// command priority
␣␣␣␣␣␣␣␣uint8_t␣instruction;␣// type of the instruction
␣␣␣␣␣␣␣␣switch␣(instruction)␣{
␣␣␣␣␣␣␣␣case␣GOTO:␣// GOTO = 0x70
␣␣␣␣␣␣␣␣␣␣␣␣// x−coordinate of the next waypoint
␣␣␣␣␣␣␣␣␣␣␣␣uint8_t␣next_wx;
␣␣␣␣␣␣␣␣␣␣␣␣// y−coordinate of the next waypoint
␣␣␣␣␣␣␣␣␣␣␣␣uint8_t␣next_wy;
␣␣␣␣␣␣␣␣case␣LAND:␣// LAND = 0x7f
␣␣␣␣␣␣␣␣␣␣␣␣;␣// no additional data is needed
}

Table 1. Drone instructions.

Instruction Mnemonic Instruction Code Valid Priority Level


Fly to the next waypoint GOTO 0x70 0x00 – 0x80
Land LAND 0x7f 0xff

4.3. Cryptographic Primitives. Trying to secure a system by obscuring its


details is a common pitfall of many applications. Good practice is to always design
secure systems based on publicly known primitives, which undergo rigorous scrutiny
of the academic and industrial research communities. Any and all primitives created
otherwise (e.g. by some companies or agencies) must never be relied on without
public review. This section introduces two cryptographic primitives that will be
used by EAGLE cryptographic module.
4. EAGLE COMMUNICATION PROTOCOL 39

Authenticated Encryption. Authenticated Encryption (AE) schemes are symmetric-


key schemes. Symmetry entails that both parties (in this case waypoint and drone)
are in possession of the same secret key. Given sensitive information in form of
plaintext, and any data associated with it, AE schemes produce a ciphertext and a
cryptographic tag. Associated data is any data that accompanies a plaintext. In the
case of the EAGLE messages, associated data are coordinates of the current way-
point, waypoint ID and the number of instructions. Although attackers may read
its content they should never be able to make any alterations to it (e.g. rewrite,
erase, or swap between two messages). Hence, this data must be authenticated along
with the plaintext. The ciphertext is the encrypted value of the plaintext. Similarly
to checksum-based integrity checks (e.g. parity check) value of the cryptographic
tag can be used to validate integrity and authenticity. Mathematical properties of
AE schemes ensure that a unique tag can be generated for the given data and the
selected key, with significant probability. Of course, an adversary may always try an
exhaustive search of all possible keys. Luckily, for a secret key large enough, in this
case, 128-bit, this might take a lifetime of the universe to execute. In other words,
only users in possession of the secret key can generate a valid tag. AE schemes are
deterministic algorithms, hence encrypting the same plaintext with the same key re-
sults in the same ciphertext. This may lead to attacks and inevitably discloses that
the same message is sent to potential adversaries. To prevent this vulnerability for
each encryption using one secret key a Number-used-ONCE (nonce) has to be used.
AE and its inverse – AD (authenticated decryption) are depicted in Figure 3. The
sender (performing AE) takes as input a secret key, a nonce, associated data (that
are authenticated but not encrypted) and plaintext. It produces a message which
consists of ciphertext and a tag, authenticating both the associated data and the
plaintext. The receiver (performing AD) takes as input the secret key, the nonce, the
associated data, the ciphertext, and the tag. She can then decrypt the ciphertext
and check whether it is authentic.
EAGLE crypto module is using KetjeSr as an authenticated encryption scheme
for the protection of the drone instructions. KetjeSr is a lightweight instance (vari-
ant) of Ketje, an authenticated sponge-based encryption scheme, which builds on
top of Keccak-p permutation. A detailed description of KetjeSr can be found in
CAESAR submission: Ketje v2.

Nonce
Plaintext
Auth. Data
Plaintext’
Plaintext
Decryptk
Tag’
Encryptk

Ciphertext
Tag =

Figure 3. Overview of AE and AD.

4.4. Cryptographic Hash Functions. Cryptographic hash functions take in-


puts of arbitrary size and produce an output of fixed length, computationally in-
distinguishable from a random value. Since this presents a form of compression,
the output is also known as a digest. One of the important properties of secure
4. EAGLE COMMUNICATION PROTOCOL 40

cryptographic hash functions is one-wayness. This means that, given the digest, it
is computationally infeasible to find the corresponding input value.
Keccak is a family of hash functions, based on sponge construction. The un-
derlying function of the sponge construction is a Keccak-f permutation. Keccak is
the basis of the SHA–3 hashing standard standardized by NIST. For EAGLE crypto
module, we will be using Keccak [128,272] (M || 01) sponge function, based on a
lightweight Keccak-f [400] permutation. More information on Keccak can be found
in Keccak specifications summary, The Keccak reference and the SHA-3 standard
FIPS PUB 202 - SHA-3 Standard: Permutation-Based Hash and Extendable-Output
Functions. These documents are available in your repository.
For the software implementation, you do not need to know all the details of
Keccak and KetjeSr, as you are provided with functions that implement them for
you. However, for later implementation of the hardware accelerator, you will need
to understand well how both of these primitives work.
4.5. Key Management. In practice, key management (generation and dis-
tribution of secret keys) is one of the main problems. EAGLE drones are not an
exception. Providing a single key to all drones would protect the system from out-
side attackers. Nevertheless, because of the symmetry in the knowledge of parties,
some drone owners could become mischievous and try to track other drones or cre-
ate false commands for them. To prevent this, each drone needs to have a different
secret key. Hence, for n drones in the system, n keys need to be stored “in the
back-end”. This prevents the aforementioned attacks as long as the drone keys are
secure. Adversaries that manage to extract a drone key, either from the drone itself
or from a single waypoint would be able to compromise the entire instructed path.
To prevent this, a separate key should be stored for each waypoint, for each drone.
Hence, for n drones and m waypoints in the system, n × m keys need to be stored
“in the back-end”. To overcome these big storage requirements, a key-derivation
function (KDF) will be used.
Each drone is assigned a single 128-bit master key MK. The master key is also
often referred to as the long-term key. Each waypoint key is derived from the master
key as given in Listing 6.4. Here wx, wy and wid are the byte encodings of fields
with the same names from Listing 6.1. Lastly, hash is the hash function with two
arguments: 1. string of bytes to be hashed (arbitrary size); 2. size of the digest (in
number-of-bytes).

Listing 6.4. Pseudo code for the waypoint key derivation.


wpk␣=␣hash(concatenate(MK,␣wx,␣wy,␣wid),␣16)
for␣i␣=␣1␣to␣16383
␣␣␣␣wpk␣=␣hash(concatenate(wpk,␣wx,␣wy,␣wid),␣16)
end␣for

4.6. QR Code Decoding. As an input to your module, the image processing


module (IMP) should provide you with a base64-decoded byte stream of a QR
code. You should agree on how to do this with the members of your IMP team. For
prototyping purposes, you can assume that the base64-decoded binary text file of
the waypoint message is provided to you. See Section 6 for information on where to
find .dat files for testing your code. However, if eventually you need to use ZBar QR
decoding, you can invoke it as a standalone binary using the subprocess Python
module. Listing 6.5 shows how to invoke ZBar on your ESAT machine.
4. EAGLE COMMUNICATION PROTOCOL 41

Listing 6.5. Example of QR code decoding using ZBar.


# zbarimg <image_name>
# image_name: name of the image file, *.pdf or *.png extension
$␣zbarimg␣wp0.qr.pdf
QR␣-Code:XaSij2sTGeR3HAMA8AAIB0muTSOIXnYHRhvNPKnRUQcSw...
scanned␣1␣barcode␣symbols␣from␣1␣images␣in␣0.01␣seconds
# option --raw can be used to remove the heading QR-Code:
$␣zbarimg␣--raw␣wp0.qr.pdf
XaSij2sTGeR3HAMA8AAIB0muTSOIXnYHRhvNPKnRUQcSw...
scanned␣1␣barcode␣symbols␣from␣1␣images␣in␣0.01␣seconds
# as outputs are rather long you might want to redirect
# them to a file
$␣zbarimg␣--raw␣wp0.qr.pdf␣>␣wp0.qr.pdf.dat
scanned␣1␣barcode␣symbols␣from␣1␣images␣in␣0.01␣seconds
# then you may display them in terminal using
$␣cat␣wp0.qr.pdf.dat
XaSij2sTGeR3HAMA8AAIB0muTSOIXnYHRhvNPKnRUQcSw...
# for more help you can invoke
$␣␣zbarimg␣-h

4.7. File structure. Before diving into implementing the crypto protocol in
C, have look at the files that are described in README.md. In particular, there are C
source and header files EagleCryptProtocol{.c,.h} to get you started. There is
also the file main.c which shows how to read the example QR codes. This file also
shows how the encrypt, decrypt and hash functions can be used, which could give
you a starting point for your Eaglecrypt protocol.
Finally, there is the EagleCrypt.py Python file. This file runs the executables
compiled from your C protocol. It is useful when your C protocol is running un-
der embedded Linux, to communicate with the IMP thread. Python calls C code
through the subprocess module as separate processes. You should build and find
the executables in the directory ./zybo/. Follow Tutorial 20.3.1 to build and run
your code in an IDE like Xilinx Vitis. You can also use Makefile to generate
the target through the command make all. In EagleCrypt.py, the correct .so
file is loaded by correctly setting the variables. There are separate library files for
Zybo with a pure software implementation or hardware-accelerated version. Setting
RUNNING ON ZYBO to False lets you test the code on a local Linux ESAT machine.
Follow closely Section 4.1 and Section 4.2 that describe message and instruction
formats. Information about the location of the 128-bit drone master key and QR
code test vectors can be found in Section 6. Your script should open and process
these files in a way described by the communication protocol to obtain the highest
priority decrypted instruction. Students implementing the flyback protocol should
also implement in Python which will use your communication protocol module, store
all instructions it gets from it and forward the right instructions to the next level
module. This is needed to be able to instruct the drone to return to its original
position in case of error. Pay attention that you will probably need to adjust this
module later when the integration with other EAGLE modules starts. When you run
your code on Zybo, you will notice that it takes more than 20 seconds to finish. Most
of this time is spent performing computationally intensive cryptographic operations.
5. HARDWARE ACCELERATORS AND HW/SW CO-DESIGN 42

5. Hardware Accelerators and HW/SW Co-design


Tto accelerate hashing and authenticated decryption, you will have to implement
parts of these operations in hardware. First, you will have to implement the round-
based Keccak-f [400] permutation, which is used as the underlying permutation
of the Keccak [128,272] (M || 01) sponge function. After verifying its correctness
in simulation, you will integrate this module in HW/SW co-design, so that the
software part can correctly communicate with the permutation. The next step will
be to adjust your implementation of Keccak-f [400] permutation so that it can be also
used by KetjeSr for authenticated decryption. This means that the new permutation
will be Keccak-p(*) [400, nr ], since it should be able to execute a variable number
of rounds and also implement twisted version of the permutation. Finally, you
will need to integrate the new implementation with your software and verify the
correctness of the whole design. To achieve even higher execution speedups, you
will have to modify your implementation to include other operations of the Sponge
and MonkeyDuplex constructions (e.g., message xor-ing, padding). Alternatively,
you can go for modifications of the basic Keccak-p(*) [400, nr ] implementation that
will reduce latency or hardware consumption.

5.1. Implementation of Keccak-f [400] Permutation. The red rectangle


in Figure 4 shows which part of the Sponge construction should be implemented in
hardware. Note that all f blocks in this figure represent the same hardware module,
just called from software at different moments of the execution. The rest of this
section gives you steps that you should follow to implement this module. First read
all of them to understand the bigger picture. It might be useful to draw a simple
block diagram of the design before beginning. If you have any questions about these
steps, do not hesitate to contact your TAs.

Figure 4. The Sponge construction (from FIPS PUB 202 - SHA-3


Standard).

✓ First make sure to understand concepts such as Keccak-f state, round, step
mappings, iterated permutation and sponge constructions (Chapters 3 and
4 from FIPS PUB 202 - SHA-3 Standard). It is also crucial to understand
how bits of a string that represents the state of the Keccak permutation are
mapped to the corresponding state array (see Section 3.1.2 in FIPS PUB
202 - SHA-3 Standard).
✓ The top Verilog module of your implementation should instantiate 2 mod-
ules – controller and datapath. The interface of your top module is given
in Listing 6.6.
5. HARDWARE ACCELERATORS AND HW/SW CO-DESIGN 43

Listing 6.6. Interface of Keccak-f [400] top module.


module␣KeccakF400Permutation(
input␣i_clk,␣// clock
input␣i_rst,␣// synchronous reset, active high
input␣i_start,␣// start permutation signal
input␣[399:0]␣i_v_state,␣// input state to the permutation
output␣[399:0]␣o_v_state,␣// output state of the permutation
output␣o_done␣// permutation done signal
);

✓ Implement combinatorial logic which will be part of the datapath and which
implements 5 step-mappings of a single round (round-based implementa-
tion). The interface of this module is given in Listing 6.7. For step map-
pings, you will also need round constants and offsets which you can find on
the path given in Section 6.

Listing 6.7. Interface of Keccak-f [400] round module.


module␣KeccakF400Round(
input␣[399:0]␣i_v_state,␣// input state to the round
input␣[4:0]␣i_v_current_round,␣// vector signal that indicates
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// current round number
output␣[399:0]␣o_v_state,␣// output state of the round
);

✓ Verify the correctness of the round logic in simulation by using intermediate


values of the computations provided on the path given in Section 6. You
will need to write your own test benches.
✓ Beside this round logic, your datapath should also contain a register for
storing the result of one round processing and a multiplexer for selecting
the input to the round logic. The input is either the output of the round
register or the input from the top module. The complete interface of the
datapath is given in Listing 6.8.

Listing 6.8. Interface of Keccak-f [400] datapath.


module␣KeccakF400Permutation_datapath(
input␣i_clk,␣// clock
input␣i_rst,␣// synchronous reset, active high
input␣[399:0]␣i_v_state,␣// input state to the round
input␣i_w_mux_sel,␣// multiplexer select signal
input␣i_w_en_reg,␣// register enable signal
input␣[4:0]␣i_v_current_round,␣// vector signal that indicates
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// current round number
output␣[399:0]␣o_v_state,␣// output state of the round
);

✓ Implement the controller for your permutation that will use the previously
implemented datapath for 1 round to perform the full permutation. The
controller should wait for the high value of signal i start and then start
running the round the necessary number of times. The controller should
also indicate to the datapath which round is currently executing. After
the last round, the controller should signalize to the top module that the
permutation is finished by setting high the signal o done during 1 clock
cycle. Your task is to make the interface for the controller and implement
FSM that can perform the required operations.
5. HARDWARE ACCELERATORS AND HW/SW CO-DESIGN 44

✓ Finally, connect your controller and datapath in the top-level module and
test it by using the provided test bench (again see Section 6).

5.2. Implementation of Keccak-p(*) [400, nr ] Permutation. Red rect-


angles in Figure 4 show which parts of the MonkeyDuplex construction should be
implemented in hardware. The rest of this section gives you steps that you should
follow to implement this module.

Figure 5. The MonkeyDuplex construction (from CAESAR sub-


mission: Ketje v2 ).

✓ Since KetjeSr uses a permutation with a variable number of rounds (for


start, step and stride) you will need to modify your top module as shown
in the Listing 6.9.

Listing 6.9. Interface of Keccak-p(*) [400, nr ] top module.


module␣Keccak_P_mod_400Permutation(
input␣i_clk,␣// clock
input␣i_rst,␣// synchronous reset, active high
input␣i_start,␣// start permutation signal
input␣[399:0]␣i_v_state,␣// input state to the permutation
input␣[4:0]␣i_v_numberOfRounds,␣// vector signal that indicates
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// how many permutations should
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// be executed
output␣[399:0]␣o_v_state,␣// output state of the permutation
output␣o_done␣// permutation done signal
);

✓ Modify the datapath of your implementation so that it can also perform


the twisted permutation Keccak-p*, as specified in the Ketje documentation
CAESAR submission: Ketje v2 . Note that because of this, you will need
to add an extra input to the datapath from the controller to signalize which
permutation should be executed.
✓ You will also need to change the FSM in the controller so that it can
execute one round of Keccak-p(*) the requested number of times. This
FSM should also have an additional signal to the datapath to indicate which
permutation is executed – Keccak-p or its twisted version – Keccak-p*.
✓ Connect your controller and datapath together in the top-level module and
test it by using the provided test bench (see Section 6).
6. TEST RESOURCES 45

✓ Because the interface of the top module has been changed, you will also
need a different IP core project that correctly instantiates your new imple-
mentation (see step 14 in the Section 2).
5.3. Improved implementations. The hardware accelerator implemented so
far is round-based and enables only the execution of the permutations in hardware
while other steps for hashing and authenticated decryption are performed in soft-
ware. The performance of your design can be further improved by executing more
operations in hardware and reducing the communication overhead between SW and
HW. Additionally, you can also try to reduce the latency or area consumption of the
permutation itself. To be able to improve your HW accelerator, get familiar with
the Sponge construction – how it uses Keccak-f permutation and how the padding
and the message partitions are applied. Further, you should understand how Mon-
keyWrap mode works and its calls to MonkeyDuplex. Explore trade-offs between the
flexibility of your implementation and performance. Note that these changes might
require modifications of the provided hardware interface and C functions that are
used to enable communication between HW and SW.
5.3.1. AXI interface. To make interfacing between software and your HW accel-
erator easier, we provide you with a Verilog module that instantiates your accelerator
and connects it to the AXI interface (Eagle to AXI interface), as well as necessary
functions in C that will transfer data between software and hardware. This is an
AXI-full interface, whereas in the calculator exercise you used an AXI-lite interface.
Students aiming for advanced/exceptional grades should build an AXI-lite interface
for their Keccak accelerator, similar to that of the calculator exercise. Optionally,
students can modify the provided C functions as well, but they can also decide to
keep the same memory mappings.

6. Test Resources
6.1. Drone Master Key. Test master key for all groups:
<root>/Module_software/CRYPTO/Tests/droneMasterKey0.bin

6.2. Test QR Codes. PNG versions of the test QR codes (base64 encoded):
<root>/Module_software/CRYPTO/Tests/QR_tests/wp0.qr.png
<root>/Module_software/CRYPTO/Tests/QR_tests/wp1.qr.png
<root>/Module_software/CRYPTO/Tests/QR_tests/wp2.qr.png
<root>/Module_software/CRYPTO/Tests/QR_tests/wp3.qr.png

Text files with the output of ZBar (base64 decoded):


<root>/Module_software/CRYPTO/Tests/QR_tests/wp0.qr.dat
<root>/Module_software/CRYPTO/Tests/QR_tests/wp1.qr.dat
<root>/Module_software/CRYPTO/Tests/QR_tests/wp2.qr.dat
<root>/Module_software/CRYPTO/Tests/QR_tests/wp3.qr.dat

6.3. Resources for HW Implementations. Keccak-f [400] Round Constants


and Offsets:
<root>/Module_software/CRYPTO/Tests/KeccakF400_RCs_Offsets.txt

Intermediate values for Keccak-f [400]:


<root>/Module_software/CRYPTO/Tests/
␣␣␣␣KeccakF400_TVs/KeccakF400_IntermediateValues.txt
6. TEST RESOURCES 46

Keccak-f [400] test bench:


<root>/Module_software/CRYPTO/Tests/
␣␣␣␣KeccakF400TVs/KeccakF400Permutation_tb.v

Intermediate values for Keccak-p* [400,nr ]:


<root>/Module_software/CRYPTO/Tests/
␣␣␣␣␣␣␣␣KeccakP400_twisted_TVs/KeccakP400_twisted_IntermediateValues.txt

Keccak-p(*) [400, nr ] test bensec:


<root>/Module_software/CRYPTO/Tests/
␣␣␣␣␣␣␣␣Keccak_P_mod_400TVs/Keccak_P_mod_400Permutation_tb.v
CHAPTER 7

Communications (COMMS)

1. Introduction
The goal of the Communications (COMMS) module is to provide a communi-
cation network and framework integrating all EAGLE modules and to provide an
adaptable communication link to the ground station over which the video stream
and telemetry data (overall state of the drone) is passed. Additionally, a graphi-
cal user interface (GUI) to display the video stream and telemetry data should be
developed.
1.1. System overview. (See Fig. 1.) The drone consists of two main plat-
forms: a Raspberry Pi with attached camera module (see Ch. 24) and the Zybo
Board (see Ch. 27). These are connected via ethernet. The Raspberry Pi on the
drone (RPI-drone) is connected to the Raspberry Pi on the ground (RPI-ground)
over Wi-Fi. This communication should operate in the 5GHz spectrum as to not
interfere with the remote controller of the drone (2.4GHz). This Wi-Fi link should
allow for a low-latency video stream, transmission of drone telemetry data and
transmission of ground station commands. Note that in this Wi-Fi link, one of the
Raspberry Pi’s should be configured as access point and one as a client. Finally,
RPI-ground is connected via ethernet to the ground station (any computer with an
ethernet port and monitor, i.e. a student laptop). The main purpose of the ground
station is to act as a screen to show the video stream and telemetry data. This means
that you are free to divide ground station functionalities between RPI-ground and
the extra computer; the end results should be shown on the computer.

Figure 1. System diagram

47
1. INTRODUCTION 48

1.2. Task overview.


(1) Establishing a Local Area Network (LAN): First, a robust and reliable
local area network (LAN) should be thoughtfully designed and implemented.
The LAN should (indirectly) connect all network nodes shown in Fig. 1 using
the IP stack. For this task, you should amongst other things consider IP ad-
dress assignment, providing routing tables at the appropriate nodes etc. This
is the most essential part of the whole module, since without a reliable network
no communication and integration between modules is possible. Additionally,
this task enables remote debugging and development, which will also be critical
during the project.
(2) Adaptable drone-to-ground Wi-Fi link: The Wi-Fi link (setup in the pre-
vious task) should allow for a low-latency video stream, transmission of drone
telemetry data and transmission of ground station commands. In this task, the
link needs to be characterized and continuously adapted to ensure a proper/op-
timal operation and avoid loss of connectivity even when the spectrum gets
congested.
(3) Integrated communication framework: On top of the network, a communi-
cation framework should be developed which allows the modules to communicate
with each other (in a standardized manner). For this task, you are free to use
existing libraries, messaging patterns etc.
(4) Graphical User Interface (GUI): A GUI should be developed which shows
the video stream and real-time drone telemetry data (e.g. altitude, current
speed, current position etc.) on the ground station. Furthermore, commands
and parameters should be able to be passed from the GUI to the drone. For
this task, you are free to choose programming language(s), you can use existing
visualization libraries and you can interpret GUI as you want (e.g. desktop
application, web application) etc. However, think about extensibility, available
packages in programming languages, development ease-of-use etc.

1.3. Integration to other modules. The COMMS module is the network


that ties all modules together. For modules which are reactive (only execute when
some communication is received), COMMS is responsible for starting the subpro-
cesses / threads / . . . of those modules. When integrating with the other modules,
make sure to synchronise with your colleagues (discuss data types, serialization
format etc.)! Following guidelines should be followed when integrating with other
modules:
✓ IMP: For this integration, you are quite free. However, note that the IMP
programs are written in Python and run on RPI-drone. Capturing the
camera images can be executed by either IMP or COMMS.
✓ CRYPT: Until T2, the CRYPT programs are written in C and run on
the Linux core of the Zybo. As mentioned at the very end of Ch. 21,
CRYPT provides an EagleCrypt.py script which interfaces Python with
the C code. (This by executing the C code as a subprocess and using
the standard input, output and error as communication busses.) From T3
onwards, the CRYPT programs are split up in SW & HW (the FPGA).
The interface between SW & HW is explained in Ch. 19, §5.
✓ SWIPT: The SWIPT module runs completely on the FPGA. The required
interface between SW & HW is the same as the one used for CRYPT for
T3. Therefore, you will be able to reuse a lot of code / knowledge.
2. MILESTONES 49

✓ ANC: ANC runs on the Zybo as a special Linux program with highest
real-time priority. For inter-process communication, ANC already provides
a great solution with implementation, based on Unix domain sockets. You
are free to modify this solution and implementation based on your needs.
Note that the implementation currently includes all required code to com-
municate IMP (vision) data and to log ANC data. For other module com-
munication (i.e. CRYPTO), you will have to duplicate some files (see Ex.
26.6.4 to explore the files enabling communication of vision data). Usefull
references / comments for this integration:
✓ ANC inter-process communication documentation
✓ ANC logging documentation
✓ We advise to perform all exercises listed in Ch. 26, §6.
Some guidelines / comments for working with the Zybo:
✓ By now, it is clear that multiple programs will run asynchronously on the
Zybo (and Raspberry Pi). An example Python framework to start and man-
age different threads is given at Module software/ZYBO/examples/threads.
However, do not start / integrate the ANC code using this framework! Note
that more recent versions of Python include more higher-level and modern
packages to write asynchronous code; most notably concurrent.futures
and asyncio.
✓ Read Ch. 22 to boot Linux and connect to the shell.
✓ See Ex. 26.3.1 on uploading files (e.g. programs, configuration files etc.) to
the Zybo using SSH. In this exercise, pay attention to the /media directory
to which files are copied. This is because only changes to the /media
directory are stored persistently (on the microSD card). Changes to other
places on the file system will be lost when rebooting.
✓ The provided Zybo image contains the utilities ifup/ifdown and dhcpcd
to configure networking (with the files /etc/network/interfaces and
/etc/dhcpcd.conf respectively). DHCP is configured on the provided im-
age. If static IP addressing is required, we recommend to customize the
/etc/network/interfaces file (used by ifup/ifdown) and put the result-
ing file on the SD card (at /media). See /media/autostart.sh for details.

2. Milestones
Please see the provided milestones document for the division between failing,
struggling, sufficient and advanced marks.

2.1. T2.
Stable network configuration and video stream via Wi-Fi. For this milestone, it
is expected to have a full network configuration which is able to connect all the
nodes. In particular:
✓ Remote access to the Zybo over the Wi-Fi link.
✓ A ground station showing a hiqh quality, low latency live video feed from
the drone.
✓ Ground station stores telemetry data and displays data from other modules.

T3 (two milestones).
3. READING GUIDE 50

Performance analysis of autonomous tuning of the video quality and telemetry


information presented in a GUI. For this milestone, more advanced features using
the network need to be implemented:
✓ Tune the configuration parameters autonomously to improve your video
streaming performance, with minimal packet error rate and latency.
✓ Easily switch between different video qualities and adapt link accordingly.
✓ Easily switch between manual and adaptive video stream tuning.
✓ Interactive GUI showing the current wireless configuration parameters and
implementing the previous mentioned features.
✓ GUI on a ground station showing live telemetry parameters in an intuitive
manner: altitude, attitude, speed, location, power transfer information . . .
✓ GUI on a ground station showing live network metrics: network latency,
throughput . . .
✓ Provide an interface to the image processing module to send their data to
the autopilot.
Providing an interface to three chosen modules (other than COMMS) in the
system to allow them to communicate. For this milestone communication between
COMMS and three chosen modules will need to be implemented:
✓ The GUI shows the useful information of the three chosen modules: alti-
tude, speed, location, SWIPT, QR code results . . .
✓ Provide an easy to use interface for the three chosen modules so they can
send data to the necessary destination.
✓ The communication for operation critical links should be reliable and fast.
✓ The GUI can modify parameters of the three chosen modules both for
debugging and control purposes.
T4.
Providing an interface to all modules in the system to allow them to communi-
cate. This milestone extends the integration milestone of T3 to cover all modules.

3. Reading Guide
✓ You should familiarize yourselves with the Linux commandline from day
one. As a starting point, chapter Ch. 9 briefly describes how it works and
the most common commands you will be needing.
✓ Ch. 9, §4 details networking utilities which you will need throughout the
project.
✓ Read Ch. 24, information about the Raspberry Pi, in the first week. This
documentation is essential for this module since most work / development
will be performed on the Raspberry Pi.
✓ Read about networking essentials (Ch. 14, §1) when setting up the LAN.
✓ When starting the integration between modules, 1) read about sockets
(Ch. 14, §2), 2) talk to your colleagues to agree on APIs, data structures
etc. and 3) read the documentation of the other modules.
Part 2

Background Information
CHAPTER 8

Introduction

This part contains background information on the different platforms involved in


the project. This consists of information on programming languages that are used,
but also a primer to the Linux command line, and information that is specific to
some modules. For example, an overview of the autopilot framework is provided as
well as an introduction to networking.

52
CHAPTER 9

Linux Commandline

This guide is meant to give a general introduction to the Linux command-line


and most commonly used commands. If you are already familiar with Linux, you
can most probably skip this tutorial. To follow the tutorials in this section, either:
✓ Have a Linux distribution (e.g. Ubuntu) as your operating system.
✓ Have MacOS as your operating system (both Linux and MacOS are Unix-
like OSes, and provide similar / the same shells, i.e. command-line inter-
preters).
✓ On Windows, install Windows Subsystem for Linux (WSL) and install a
Linux distribution of choice from the Microsoft Store. See https://docs.
microsoft.com/en-us/windows/wsl/install-win10 for instructions. Note: if
choosing this option, using WSL 2 is recommended.
✓ Use one of the provided Raspberry Pi’s remotely (using ssh / Putty).

1. Introduction
When you open a Unix shell, by connecting remotely via ssh or by opening an
application such as Gnome Terminal (on Ubuntu), Konsole (on Kubuntu), Terminal
(on MacOS) etc., you are usually greeted by a prompt: user@hostname:~$. After
this prompt, you input commands and execute them by pressing ENTER. The
program displaying the prompt and interpreting and executing commands is on
many distributions by default bash1. Similar to other shells, bash includes some
shortcuts by pressing designated key combinations:
✓ TAB: (tab completion) automatically fill in partially typed commands; very
usefull to autofill filenames and folders.
✓ ARROW-UP: cycle through previously entered commands.
✓ CTRL-R: searching through previously entered commands.
✓ CTRL-C: stop a running command.
✓ CTRL-D: exit bash; usefull for terminating an ssh session.

2. Environment variables
The bash shell has a notion of environment variables, which have a similar use
as variables in programming languages. See Tutorial 9.2.1 for usage.
Tutorial 9.2.1 (Using environment variables) — Set an environment variable using
the command VARIABLE=value (no spaces around =, all caps is best practice). Reference
an environment variable in a command using $VARIABLE. Example:
pi@raspberrypi:~$␣EXAMPLE_VAR=example
pi@raspberrypi:~$␣echo␣$EXAMPLE_VAR
example

1See https://tldp.org/LDP/Bash-Beginners-Guide/html/Bash-Beginners-Guide.html for a


deep-dive in bash
53
3. COMMONLY USED COMMANDS 54

Use the command export␣EXAMPLE_VAR=example such that the variables are available
to processes spawned by bash. Example with Python:
pi@raspberrypi:~$␣export␣EXAMPLE_VAR=example
pi@raspberrypi:~$␣python␣-q
>>>␣import␣os
>>>␣os.getenv("EXAMPLE_VAR")
'example'

Commonly used environment variables are:


✓ $HOME: references the home directory of the current user.
✓ $PATH: tells the shell which directories to search for executable files. Adding
custom directories to the path is done like this: PATH=$PATH:/path/to/custom/dir.
To display all currently set environment variables, you can use the env command.

3. Commonly used commands

Tip 9.3.1

If unsure about how to use a specific command, for many commands you can use
i) <command>␣--help to output instructions on using the command ii) man␣<command>
to search the local manual database iii) Google man <command> to search for
an online manual page.

Tip 9.3.2

If you notice you have insufficient permissions to execute a command, you can
try running it as the root user with sudo␣<command>.

Tip 9.3.3

If unsure about the location of the executable file corresponding to a specific


command, use which␣<command>.

3.1. Filesystem Navigation. The following are the most commonly used
commands to navigate the filesystem via the shell:
✓ pwd: show the current working directory
✓ cd␣<path>: change the current working directory to path
✓ ls␣<path>: list the path directory (commonly used flags are -al)
There are two types of path arguments:
✓ Absolute path (a path starting with a /): indicates a path starting from
the root directory of your device.
✓ Relative path (a path starting without a /): indicates a path starting
from your current working directory.
Following characters have a special meaning when used as path segments:
✓ ˜(a tilde): means the home directory of the current user.
✓ . (a single dot): means the current directory.
✓ .. (two dots): means the parent of the current directory.
3. COMMONLY USED COMMANDS 55

Tip 9.3.4

Using TAB to autocomplete file and directory names is much advised.

3.2. File & directory management.


3.2.1. Reading a file.
✓ cat␣<filename>: print the complete contents of the file.
✓ head␣<filename>: print the first lines of the file (use the -n flag for specifying
the number of lines)
✓ tail␣<filename>: print the last lines of the file (use the -n flag for specifying
the number of lines)
✓ less␣<filename>: scroll through the file.
✓ file␣<filename>: determine the file type of the file.
3.2.2. Manipulating a file / directory.
✓ touch␣<path>: change the timestamp of the file or directory. Usefull for
creating empty files.
✓ mkdir␣<path>: make an empty directory at the specified path.
✓ rmdir␣<path>: remove the empty directory at the specified path.
✓ cp␣<source>␣<dest>: copy the source file or directory to the destination file
or directory.
✓ mv␣<source>␣<dest>: move the source file or directory to the destination file
or directory.
✓ rm␣<path>: remove the specified file or directory (use option -r to recursively
remove a directory).
3.2.3. Creating / Editing a file using an editor.
✓ nano␣<filename>: open the file which you want to create or edit with nano.
To save your changes, use CTRL-O followed by ENTER to confirm the
filename to write. To quit nano while saving your changes, use CTRL-X
followed by Y followed by ENTER. To quit without saving changes, use
CTRL-X followed by N. All the other available commands are listed at
the bottom of the nano interface (r̂epresenting CTRL). More info: https://
www.codexpedia.com/text-editor/nano-text-editor-command-cheatsheet/.
✓ vim␣<filename>: open the file which you want to create or edit with vim.
Note that this is only aimed at more advanced users. Learning how to use
vim is an investment: when starting it will go slow, however when mastered
you will be much faster than with a regular editor such as nano. More info:
https://www.openvim.com/ and https://danielmiessler.com/study/vim/.
3.2.4. Copying a file to / from a remote device. Transferring files to / from a
remote device is possible in a lot of different ways, e.g. via FTP, SFTP, scp, samba,
WebDAV etc. The common factor between these methods is the use of a server on
the remote device which must accept the files. To transfer files from your laptop to
the raspberry pi, one advised way is using scp (secure copy protocol). This because
it operates over SSH (which is already enabled on the raspberry pi). Other utilities
operating over SSH are SFTP and rsync.
✓ scp␣<source>␣<target>: copies the file from source to target. To have source
or transfer be the remote host, specify them as: user@address:/path/to/target.
For example, to copy from your local laptop to the raspberry pi:
4. NETWORKING COMMANDS 56

scp␣/path/to/source␣pi@raspberrypi:/path/to/target.
Or copy from an ESAT machine to your local laptop:
scp␣[email protected]:path/to/source␣path/to/target
See Tutorial 24.4.2 to configure SSH, so that you don’t have to type the password
every time you use ssh or scp.
3.2.5. Ownership.
✓ chmod␣<path> Change the file or directory mode bits, e.g. make a file exe-
cutable. Read more at: https://en.wikipedia.org/wiki/Chmod.
For example if you just used scp to transfer a binary, but you can’t execute
it. Then you can make the file executable with: chmod␣+x␣<path>
✓ chown␣<path> Change the file or directory owner.

3.3. Package management. Different Linux distributions have different pack-


age managers, i.e. programs which install, remove, upgrade, . . . packages. Since the
Raspberry Pi OS is derived from Debian, the package manager is called apt. Be-
cause apt changes system files, you will have to run it with sudo. The most usefull
subcommands are:
✓ sudo␣apt␣install␣<package>: install a package.
✓ sudo␣apt␣remove␣<package>: remove a package.
✓ sudo␣apt␣update: update the list of available packages.
✓ sudo␣apt␣upgrade: upgrade the system by installing/upgrading packages.
✓ apt␣search␣<query>: search in package descriptions for the query. Tip: Googling
apt␣<package> often also gives information about (installing) the package.

3.4. Others.
✓ htop: interactive process viewer.
✓ find: search for files in a directory hierarchy.
✓ grep: print lines that match patterns (see also fgrep).
✓ systemctl: control and view the status of services.
e.g. systemctl␣status␣ssh
✓ sed: filter and transform text.

4. Networking commands
The ip is a powerfull command for networking on Linux. It is the successor of
the ifconfig, which you can find in some older guides. The command can, among
other things:
✓ Show the configuration of all network interfaces or only a specific interface:
ip␣address
ip␣address␣show␣<interface>
✓ Configure the IP address for a network interface:
ip␣address␣add␣<ip>/<netmask>␣dev␣<interface>
e.g., ip␣address␣add␣192.168.1.1/24␣dev␣wlan0
✓ Show the link layer information for all interafaces or for a specific interface:
ip␣link
ip␣link␣show␣<interface>
✓ Show the routing table (the first entry is the default route):
ip␣route
5. TIPS AND TRICKS 57

✓ Add a route to the routing table (make subnet <ip>/<netmask> is reachable


via ip address gateway on interface interface):
ip␣route␣add␣<ip>/<netmask>␣via␣<gateway>␣dev␣<interface>
e.g., ip␣route␣add␣192.168.1.1/24␣via␣192.168.2.1␣dev␣wlan0
✓ Set the default gateway of your device. Keep in mind that your device can
have only one default gateway:
ip␣route␣add␣default␣via␣<gateway>
e.g., ip␣route␣add␣default␣via␣192.168.2.1
✓ Enable or disable an interface:
ip␣link␣set␣<interface>␣up
ip␣link␣set␣<interface>␣down
✓ Show the MAC address of all connected devices (usefull for debugging):
ip␣neighbour␣show
More explanation about the ip command:
✓ https://www.geeksforgeeks.org/ip-command-in-linux-with-examples/
✓ https://www.tecmint.com/ip-command-examples/
Other usefull commands:
✓ ping␣<destination>: check connectivity to the specified host (ip address or
domain name).
✓ traceroute␣<destination>: check the route trace to the specified host (ip
address or domain name).
✓ host␣<domain_name>: perform a dns lookup for the specified domain name
(see dig for a more powerfull utility).
✓ curl and wget: download remote content over various protocols.
✓ vnstat: monitor network traffic. It can show statistics, such as band-
width consumption, over a period of time or live real-time statistics. The
tool supports export data to a JSON format. More info: https://www.
booleanworld.com/monitor-network-traffic-linux-using-vnstat/

5. Tips and tricks

Tip 9.5.1

If you have a long-running command, such as installing a package, and do not


want it to stop when the ssh session is stopped (e.g. due to a broken network
link), execute the command inside a screen virtual terminal. Detach from a
screen virtual terminal using CTRL-A CTRL-D. Reconnect to a screen virtual
terminal using screen␣-r. More information: https://wiki.archlinux.org/title/
GNU Screen.

Tip 9.5.2

When the ssh session keeps hanging and CTRL-C and CTRL-D does not work,
type ENTER . (enter-tilde-dot) to close the connection.

Tip 9.5.3
5. TIPS AND TRICKS 58

To redirect output from a command from a file use command␣>␣command_output.txt.


More information: https://tldp.org/LDP/abs/html/io-redirection.html

Tip 9.5.4

You can use the ~/.bashrc file to configure the bash terminal.
CHAPTER 10

Programming Languages – C

In this chapter, we provide a brief introduction to programming in C.

1. Types & Variables


1.1. The C Type System. C is a strongly and statically typed language.
This means that all variables have a certain type that is known at compile time,
functions take parameters of known types, and return a known type. The compiler
makes sure that you call functions with the correct types of arguments. In weakly
typed languages like Python or JavaScript, no type checking happens beforehand:
all types are checked at run time.
C offers many built-in fundamental types, such as int, float and bool, and
additional user-defined types using structs and enums.

1.2. Variables. To create a variable, you specify the type, followed by the
variable name. For example:
float␣pi;␣␣␣␣␣␣␣␣// declaration of variable "pi" of type `float`
int␣index;␣␣␣␣␣␣␣// declaration of variable "index" of type `int`

In the example, the variables were only declared, not initialized. If you try to
read the value without initializing them first, the value is undetermined. You can
initialize them together with the initialization, or separately:
float␣a␣=␣1.2;␣␣// declaration and initialization
float␣b;␣␣␣␣␣␣␣␣// declaration
b␣=␣1.3;␣␣␣␣␣␣␣␣// initialization

1.3. Arrays. Arrays are contiguous lists of data of the same type.
A C-style array is a simple array that consists of N contiguously allocated objects
of a certain type. Valid indices for an array of size N range from 0 to N − 1. The
index of the first element is 0.
int␣myArray[3]␣=␣{1,␣2,␣3};␣␣␣// Specify the size yourself
int␣anotherArray[]␣=␣{4,␣5,␣6};␣␣// Let the compiler count
myArray[0];␣␣␣␣␣// == 1
myArray[1];␣␣␣␣␣// == 2
myArray[2];␣␣␣␣␣// == 3
myArray[3];␣␣␣␣␣// OUT OF BOUNDS
myArray[9001];␣␣// OUT OF BOUNDS
myArray[-1];␣␣␣␣// OUT OF BOUNDS

When you access an array out of bounds, most of the time you don’t get a
compilation error. However, your program won’t work as expected, as you’re reading
or even writing to memory that belongs to other parts of the program. If you
overwrite other variables or pieces of memory that are crucial to the operation of
59
1. TYPES & VARIABLES 60

the program, it might crash, or do something unexpected, or you might not even
notice it.
Under the hood, a C-style array is just a pointer to the first element, this has
some important implications:
✓ You cannot return a local array from a function
✓ You cannot pass an array to a function by value, only by reference (pointer)
✓ The sizeof keyword on an array parameter in a function always returns
the size of the pointer, not the actual size of the array
✓ You cannot assign one array to another, you can only assign elements
// Don't do any of this

// 1.
int␣[]␣functionReturningArray()␣{
␣␣␣␣int␣a[]␣=␣{1,␣2,␣3};
␣␣␣␣return␣a;␣// ERROR: 'a' goes out of scope!
}

// 2.
void␣fun(int␣array[])␣{
␣␣␣␣array[0]␣=␣42;
}
int␣array[]␣=␣{1,␣2,␣3};
fun(array);
// The array is now {42, 2, 3}, because the function
// operates on the actual array, not on a copy of it

// 3.
int␣sum(int␣array[])␣{
␣␣␣␣int␣sum␣=␣0;
␣␣␣␣size_t␣size␣=␣sizeof(array)␣/␣sizeof(array[0]);
␣␣␣␣// Wrong: sizeof(array) is the same as sizeof(int*)
␣␣␣␣for␣(size_t␣i␣=␣0;␣i␣<␣size;␣i++)
␣␣␣␣␣␣␣␣sum␣+=␣array[i];
␣␣␣␣return␣sum;
}

// 4.
int␣a[]␣=␣{10,␣20,␣30};
int␣b[]␣=␣a;␣␣// ERROR

1.4. Type Aliases. You can create aliases for types. This can be useful to
make the intent of your code clear to the reader, or if you have to use the same long
type name very often. This can be performed using the typedef keyword:
typedef␣float[3]␣array3f;

1.5. Pointers. Explaining pointers from scratch is not the goal of this docu-
ment. It’s an important topic, but it’s not critical to understand.
If the concept of pointers is new to you, it might be interesting to read some
online tutorials like this one or watch this video. Otherwise, just remember that a
pointer is a variable that holds the address of another variable. To get or set the
value of the variable pointed to by the pointer, you have to dereference the pointer
using the dereference operator *. You can get the address of a variable using the
address-of operator &.
1. TYPES & VARIABLES 61

int␣i␣=␣0;␣␣␣// A normal variable 'i'


int␣*p␣=␣&i;␣// A pointer 'p', initialized with the address of 'i'
*p␣=␣42;␣␣␣␣␣// Dereference pointer 'p' to assign value 42 to 'i'
// i == 42
// *p == 42

A null pointer is a pointer that doesn’t point to any other variable. Dereferencing
it will crash your program. You’ll probably get a segmentation error if you are
running your program on a platform with an OS. On the Zybo, your program will
probably just stop without any information as to why.
int*␣p␣=␣NULL;
*p␣=␣42;␣␣// BUG: Dereferencing a null pointer: Segmentation Fault

1.6. List of Fundamental Types.


✓ Signed Integer Types A signed integer can represent a finite set of posi-
tive and negative whole numbers. The actual size of these types is platform-
dependent, and overflow behavior is undefined. Examples of signed integer
types are:
short int, int, long int, long long int
Often times, you want to make sure that the type is large enough to hold a
certain number. In that case, you can use the fixed-width integers defined
in the stdint.h header:
int8 t, int16 t, int32 t, int64 t
✓ Unsigned Integer Types An unsigned integer can represent positive
whole numbers. The actual size of these types is platform-dependent as
well, but unlike their signed counterparts, overflow behavior is defined to
be modulo 2n , where n is the number of bits of the representation. This
means that for a 16-bit unsigned integer, 65535 + 1 = 0. Examples of
unsigned integer types are:
unsigned short int, unsigned int, unsigned long int
Additionally, stdint.h or cstdint also defines the fixed-width unsigned
integers:
uint8 t, uint16 t, uint32 t, uint64 t
✓ Character Types There are three distinct character types. If you use
math on these types in your code, keep in mind that char is usually un-
signed on ARM processors, and signed on x86 processors, however, the
signedness of char is never guaranteed to be one or another. Examples of
character types are:
unsigned char, signed char, char
There are some wide character types to accommodate for UTF-16 and
UTF-32 character sets as well, but unless you’re writing code for Windows,
you don’t have to worry about these, and you can just use UTF-8 saved as
normal chars.
✓ Floating Point Types There are single, double and extended precision
floating point types available, respectively:
float, double, long double
The first two usually follow the IEEE-754 specification, and are 32 and 64
bits wide. Operations on these types are carried out in hardware by the
Zybo’s FPU. There is no standard representation for the extended precision
type. On 32-bit Zybo it’s the same size as double precision, but on the 64-bit
2. FUNCTIONS 62

Raspberry Pi 3 an x86 64 computer, it can be a quad-precision IEEE-574


(if you use GCC, for example).
✓ The Boolean Type The bool type is probably the simplest, it can hold
just one of two possible values, true or false. To use this type, the header
stdbool.h needs to be included.
1.7. Scope & Lifetime. Every variable has a region of code where it can
be accessed, called its scope. Usually, the scope is limited by the curly braces ()
surrounding a block of code:
{
␣␣␣␣int␣a␣=␣5;
}
int␣b␣=␣a␣+␣6;␣␣// ERROR: 'a' was not declared in this scope

Variables also have a lifetime, the time when it is in a valid state and it can
be used. For local, automatic variables, the lifetime is the same as the scope. For
example:
int␣*ptr;
{
␣␣␣␣int␣a␣=␣5;
␣␣␣␣ptr␣=␣&a;␣␣// 'ptr' now points to 'a'
}␣␣// the lifetime of 'a' ends
int␣b␣=␣*ptr;␣␣// BUG: you're trying to read the value of 'a',
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// but its lifetime is over, the memory 'ptr'
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// points to is no longer bound to 'a'

An exception are static local variables: their lifetime doesn’t end when their
scope ends, their value is retained across function calls:
unsigned␣int␣count()␣{
␣␣␣␣static␣unsigned␣int␣counter␣=␣0;␣␣// 'counter' is initialized only the
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// first time the function is called
␣␣␣␣counter++;
␣␣␣␣return␣counter;
}␣␣// scope of 'counter' ends, lifetime doesn't end

count();␣␣// == 1
count();␣␣// == 2
count();␣␣// == 3

2. Functions
2.1. Declaration vs Definition. Similar to variables, functions have a decla-
ration and a definition.
// Declaration / prototype
float␣pi();

// Definition / implementation
float␣pi()␣{
␣␣␣␣return␣3.14159265;
}

The declaration (also called signature or prototype) contains all of the infor-
mation necessary to use the function: its name, the number and types of arguments
it takes, and the type it returns.
2. FUNCTIONS 63

The definition, on the other hand, provides an implementation for the function,
containing the actual code that is executed when the function is called.
Function declarations go into header files, so they can be included and used by
other files. Function implementations go into implementation files, so that they are
only compiled once. Another benefit is that the users of a function or library only
have to glance at the header file to know how to use the functions, without worrying
about how it is implemented. It’s a good idea to add documentation on how to use
the function in the header as well.
MyMath.h
/// Returns the value of pi.
float␣get_pi();

MyMath.c
#include␣<MyMath.h>

float␣get_pi()␣{
␣␣␣␣return␣3.14159265;
}

To use this function in other files, you just need to include the header file:
Other.c
#include␣<MyMath.h>

// tau = 2pi
float␣get_tau()␣{
␣␣␣␣return␣2␣*␣get_pi();
}

2.2. Function Arguments. Functions can also take arguments. To create a


function that takes arguments, just add one or more parameters to its declaration.
Each parameter specifies the type of the argument that can be passed, and a name
that can be used inside of the function definition.
/// Returns the maximum of the given floats.
float␣max(float␣a,␣float␣b)␣{
␣␣␣␣if␣(a␣>␣b)␣{
␣␣␣␣␣␣␣␣return␣a;
␣␣␣␣}␣else␣{
␣␣␣␣␣␣␣␣return␣b;
␣␣␣␣}
}

Tip 10.2.1

The proper way to specify that a function has no arguments, is to specify void
as argument in the declaration:
float␣pi(void);

If this void keyword is not specified, calling the function with a nonzero number
of arguments will still compile but they won’t be accessible to the function you
are calling, while when using void, a compiler error will happen.
3. STRUCTS 64

An essential thing to notice is that the variables a and b are passed by value.
Passing by value means a copy of the variables is created, and that copy is used in
the function. This means that if you change the value of a or b, the original variable
(in the calling code) will not be changed.
Alternatively, a variable can be passed by pointer. This does basically the
same thing as passing by reference, but it can make the code a little harder to read,
since you must always dereference the pointer before using it. You create a pointer
by adding an asterisk * after the type. For example, int* is a pointer to an integer.
Here’s a summary:
void␣byVal(int␣x);␣␣␣// copy is made, original value can't be changed
void␣byPnt(int*␣x);␣␣// original value can be changed

void␣increment_ptr(int*␣i)␣{
␣␣␣␣(*i)++;
}

int␣myInt␣=␣3;
increment_ptr(&myInt);

2.3. Return Values. As shown above, the first part of the function signature
is the return type. This is type of the value that is returned by the function, which
can be for example any type from §1.6, an array,. . . Lastly, it can be void, meaning
nothing is returned.
However, it is not possible to return multiple values like in Matlab. To get
around this, you can return a struct. This is explained in section §6.2.

3. Structs
A data structure is a group of data elements grouped together under one name.
These data elements, known as members, can have different types and different
lengths. Data structures can be declared using the following syntax.
struct␣StructName␣{
␣␣␣␣int␣␣␣val1;
␣␣␣␣bool␣␣val2;
␣␣␣␣float␣val3;
␣␣␣␣...
};

The members of a struct variable can be modified using:


mystruct.val1␣=␣0;
mystruct.val2␣=␣false;
mystruct.val3␣=␣0.0;

as its members are public by default.


An instance can be created using aggregate initialization:
struct␣StructName␣mystruct␣=␣{1,␣true,␣1.0,␣..};

As you can see, struct variables always need to have struct <StructName> as
type name, unlike primitive types, which only need the type name. It is also required
in function arguments and return types:
struct␣point␣point_add(struct␣point␣a,␣struct␣point␣b);

A typedef can be used to abbreviate this:


4. ENUMERATIONS 65

typedef␣struct␣point␣point_t;

The alias can also be made while defining the struct type:
typedef␣struct␣{
␣␣␣␣int␣x;
␣␣␣␣int␣y;
}␣point_t;

4. Enumerations
An enumeration is a type whose value is restricted to a range of integers. Enu-
merations can be declared using the following syntax.
enum␣FlightMode␣{
␣␣␣␣Uninitialized␣=␣0,
␣␣␣␣Manual␣=␣1,
␣␣␣␣AltitudeHold␣=␣2,
␣␣␣␣Autonomous␣=␣3,
};

Much like structs, variable declarations of enums also need the enum keyword:
enum␣FlightMode␣currentMode␣=␣Manual;
enum␣FlightMode␣NextMode(enum␣FlightMode␣current,␣struct␣InputData*␣inputs);

There are two things that should be mentioned. First, values 0, 1, ... are
not required. They will automatically be numbered by the compiler if you don’t
explicitly type them. The first enumerator has a default value of 0 if you don’t
specify it. When using an enum in the context of state machines, it may be wise
to ensure the ”initial state” has the value 0, so that you don’t end up in a random
state when you default-initialize an instance of the enum.
Secondly, the enumerators are global, so clashes between enumerator names can
occur when not taken into account:
enum␣DroneState␣{
␣␣␣␣Idle,
␣␣␣␣TakingOff,
␣␣␣␣Flying,
␣␣␣␣Landing,
};

enum␣SomeOtherState␣{
␣␣␣␣Waiting,
␣␣␣␣Running,
␣␣␣␣Idle,␣␣␣␣␣␣␣␣␣␣␣␣␣␣// Name collision with DroneState::Idle
};

// Should i = 0 (DroneState) or 2 (SomeOtherState)?


int␣state␣=␣Idle;

Therefore, it is common practice to prepend the name of the enum type to all
enumerator names to avoid clashes:
enum␣DroneState␣{
␣␣␣␣DroneState_Idle,
␣␣␣␣DroneState_TakingOff,
␣␣␣␣DroneState_Flying,
␣␣␣␣DroneState_Landing,
5. FILE STRUCTURE, HEADER FILES AND COMPILATION 66

};

enum␣SomeOtherState␣{
␣␣␣␣SomeOtherState_Waiting,
␣␣␣␣SomeOtherState_Running,
␣␣␣␣SomeOtherState_Idle,
};

enum␣DroneState␣state␣=␣DroneState_Idle;

5. File Structure, Header Files and Compilation


There are two main types of files in a C project: header files and implementation
files.
Headers contain the declarations of functions, variables and classes that need to
be available to other parts of the code. The extension .h is often used for C header
files.
Implementation files contain the definition of the variables, functions and meth-
ods declared in the header files, as well as some helper functions that are needed in
that implementation file only. The extension .c is generally used for C implemen-
tation files.

5.1. Header Files. To use the functions declared in a header file, the header
is included by the preprocessor:
#include␣<SomeHeaderInTheIncludePath.h>
#include␣"SomeHeaderInTheSameFolder.h"

Angle brackets (<>) are used for files that can be found in the include path of
the compiler, like standard library headers, system headers, and headers of other
libraries. Quotes are used to include header files that are in the same directory as
the file that includes them. The preprocessor doesn’t do anything special, it just
replaces the #include statement with a copy of the contents of the header file.
Because one header can include many more headers, and these headers could
also include other headers, the dependency graphs can be rather large, and some
headers will be included twice.
Including the same file twice is bad, because then all of its contents are dupli-
cated, and this can result in compilation errors. To get around this problem, you
add the following line to all of your header files to tell the preprocessor to only
include the file once:
#pragma␣once

5.2. Implementation Files. Each implementation file is compiled separately


into an object file. When all files have been compiled, the linker stitches all of the
object files together to create the final executable.
This means that if two different implementation files include the same header
file, its contents are compiled twice. If there are implementations in the header,
there are now two compiled implementations of the same function. The linker won’t
know which one to use, and will throw a ”multiple definitions” error.
If you get these kinds of errors, check that your header files only contain decla-
rations.
6. ADVANCED TOPICS 67

5.3. Inline Functions. If you really want to have function implementations


in a header file (because it’s a very simple or short function, or for performance),
you can use inline functions. For free functions, you add the inline keyword in
front of the function declaration/definition. For methods, you can just add the
implementation inside of the class, and they will be inline automatically.
#pragma␣once

static␣inline␣int␣mySimpleFunction(int␣a)␣{
␣␣␣␣return␣2␣*␣a;
}

class␣C␣{
␣␣public:
␣␣␣␣void␣setC(int␣c)␣{␣this->c␣=␣c;␣}
␣␣␣␣int␣getC()␣const␣{␣return␣this->c;␣}
␣␣private:
␣␣␣␣int␣c␣=␣0;
};

The keyword static is required when using inline functions to avoid problems
which would cause either cryptic compiler errors, or mysterious runtime crashes.
Explaining the reason behind this falls outside the scope of this tutorial.

6. Advanced Topics
6.1. Const. The keyword const means that the value will not change during
its lifetime. It does not make your code any more efficient. Rather, it is a tool to
help you, the programmer, to be sure that certain variables are immutable. With
const, you make a promise to the compiler that you will not change the value, and
if you break this promise, the compiler will complain.
Variables can be labelled const. These variables must have their declaration
and definition in one statement.
const␣float␣x␣=␣1;

Function parameters can also be labelled const. This is essential knowledge


if you are passing an argument by reference: if the parameter is labelled const, you
can be sure your variable isn’t changed during the function call.
void␣myFunction(const␣float*␣x,␣float␣y);

This can be useful when the type of the argument would be very large, as making
a copy in order to make the function call would cause a slowdown.
6.2. Returning Multiple Values. It is not possible for a function to return
multiple values like in Python or MATLAB. To get around this, you can return a
struct.
/// Struct containing an integer, boolean and float.
struct␣ReturnValues␣{
␣␣␣␣int␣␣␣val1,
␣␣␣␣bool␣␣val2,
␣␣␣␣float␣val3,
};

struct␣ReturnValues␣myFunction(void)␣{
␣␣␣␣return␣{1,␣true,␣1.0};␣// ReturnValues{1, true, 1.0} is also correct
6. ADVANCED TOPICS 68

void␣otherFunction(void)␣{
␣␣␣␣struct␣ReturnValues␣r␣=␣myFunction();
␣␣␣␣int␣␣␣v1␣=␣r.val1;
␣␣␣␣bool␣␣v2␣=␣r.val2;
␣␣␣␣float␣v3␣=␣r.val3;
}
CHAPTER 11

Programming Languages – Matlab

This chapter contains some basic information related to coding in Matlab. We


consider good practices, simulation of dynamics, code generation basics and object
oriented programming.

1. Coding in Matlab
1.1. Good practices. There are certain well-established good practices in soft-
ware development which are essential for this project as it is a collaborative project,
it involves several layers of abstraction (acquisition of measurements, filtering, atti-
tude control, altitude control, navigation and many more) and it is likely that you
will need to experiment with alternative approaches.
We recommend going through this Wikipedia article on “Best coding practices.”
More specific to the EAGLE project, we would highlight the following points (not
in order of importance):
(1) Use a private source code versioning system (preferably based on git) and
actively collaborate using it. Add README files, make branches when
necessary, write meaningful commit messages. Do not forget to add a
.gitingore file and do not track binary files.
(2) Document your functions properly and clearly. If you change the function
template (inputs/outputs), update the documentation too.
(3) Write comments as you code.
(4) Do not comment-out code (use git).
(5) Write unit tests and opt for 100% coverage. Write functional tests for your
module and integration tests for the whole project. Do not test using print
statements.
(6) Do not use globals (e.g., the global keyword in Matlab). Use functions
and classes instead. Similarly, in C, do not use global variables (use static
file-scope variables).
(7) Establish a meaningful naming convention (and make sure all team mem-
bers abide by it).
(8) Before you use any third-party functionality, read the documentation and
make sure you understand how it works. For example, in Matlab, not
spending 30 minutes time to study the documentation of ss and lqr or
kalman may delay the progress of the project for weeks.
(9) Establish your coding conventions before you begin coding. Take some
time with your team member and write down what functionality you need
to implement, sketch out a workflow, identify the entities of your software
and — if you work with classes — sketch a UML.
(10) Create milestones and use an issue tracker to know at every moment what
needs to be done and who is responsible for what.
69
1. CODING IN Matlab 70

You may also further read the articles Matlab programming style guidelines and
Best Practices for Scientific Computing. Part of the grading will be based on your
adherence to good development practices.

Tip 11.1.1

Simulink does not combine well with code versioning (git), this is why it should
not be used. Additionally, prefer functions over scripts. It is not clear how a
script will change your workspace, while with a function you know exactly what
arguments it takes and what values it returns.

Tip 11.1.2

When using Matlab on an ESAT PC, you will notice that the keyboard short-
cuts are not as expected. See item 1 for the fix.

1.2. General purpose functionality. Let us start by an overview of certain


core functionality of Matlab. This is a list of basic Matlab functions that you
need to be aware of before we proceed:
(1) ones, zeros, eye: generates matrices with ones, the zero matrix and the
identity matrix of specified dimensions. For example zeros(4,5); will
generate a 4 × 5 zero matrix.
(2) eig: returns an array with the eigenvalues of a matrix. When called with
two output arguments it also returns its eigenvectors. See also the similar
function eigs.
(3) rank: returns the rank of a matrix. The second input argument is a tol-
erance. When used with two input arguments it returns the number of
singular values of the given matrix which are above the specified tolerance.
(4) Generating random numbers: randn(n,1) generates a vector of normally
distributed random numbers which are uncorrelated. Use mvnrnd(mu,
sigma) to generate a random sample from the vector-valued distribution
N (µ, Σ) (cf. Ex. 25.3.2).
(5) The backslash operator (\) is used to solve linear systems. When we need
to solve a linear system of the form Ax = b where x = A−1 b (assuming that
A is invertible), it is very bad practice to run x = inv(A)*b;. Matlab will
indeed give a warning. The reason is that not only is inv computationally
expensive, but it is also numerically unstable and may lead to bad results.
Besides, Ax = b may have solutions without A being invertible or even
square. Instead, we should be using the backslash operator: x=A\b.
(6) find: finds all elements in a vector or matrix which satisfy a condition, e.g.,
find(A>0) returns an array of indices i1, i2, ... in A so that A(i1),
A(i2), ... are positive.
(7) save saves the current workspace in a binary .mat file that can be later
retrieved with load. You may select which variables to store (instead of
saving the whole workspace). Binary files are typically not added in code
versioning systems, but it’s wise to back them up somewhere.
(8) Use help and doc to access the documentation of a Matlab function. By
writing comments in the beginning of your function implementations, you
create documentation that is accessible via help.
1. CODING IN Matlab 71

(9) csvwrite/csvread: read/write from/to CSV files.


(10) savejson/loadjson: saves structures to JSON files and loads data from
JSON files. Certain restrictions apply; e.g., it is not possible to store struc-
tures with function handles or anonymous functions. These functions are
part of jsonlab which is not part of Matlab’s distribution.
Function handles are data types which “store associations to functions,” are it
is reported in the official documentation page. The allow functions to be treated
as variables, so they can be passed to functions as input arguments, they can be
constructed by functions, they can be combined and so on. Several core Matlab
functions such as ode45, which we use in Section 1.5, or the popular fmincon and
fminunc expect function handles as input arguments.
Function handles can be constructed on the fly as follows:
% Constuct the function f(x) = sin(x):
f␣=␣@(x)␣sin(x);

% We may then call f:


y␣=␣f(0.1);

% We may constuct function handles with two arguments


g␣=␣@(x,y)␣cos(x␣+␣y.^2);
z␣=␣g([1;2],␣[-2;0]);

These are known as anonymous functions. But we may also create a handle from
any Matlab function. This is similar to creating an alias for a function:
myrand␣=␣@randn;
y␣=␣myrand(3,4);␣␣% calls `randn`.

Note that although we may create copies of function handles (myrand2 = myrand),
handle arithmetic is limited. For instance, we may not do fun = f1 + f2; for two
function handles f1 and f2. We may, however, do fun = @(x) f1(x) + f2(x);.
Function handles can be passed to other functions. For instance, Matlab’s
Rb
integral is a function that computes the definite integral a f (x)dx for a function f
which is passed as a handle. Here is an example:
% Create a handle from `sin`
f␣=␣@sin;
% Compute the integral of `sin(x)` from 0 to 2*pi
% The result is practically 0.
intF␣=␣integral(f,␣0,␣2*pi);

% But we may also create an anonymous function and


% pass it to `integral`:
g␣=␣@(s)␣sin(2*s)␣+␣cos(s);
intG␣=␣integral(g,␣0,␣1);

Warning 11.1.1

When running Matlab on laptops in Linux you might see the message MEvent.
CASE! in the command window as you use the touchpad to scroll. You may
disable it using !synclient HorizTwoFingerScroll=0.
1. CODING IN Matlab 72

1.3. The Symbolic Toolbox. The symbolic toolbox of Matlab can be used
to compute Jacobians and determine the linearisation of a nonlinear system:
% Define the symbols
x␣=␣sym('x',␣[3,␣1],␣'real');␣% 3 states
u␣=␣sym('u',␣[2,␣1],␣'real');␣% 2 inputs

% Define the nonlinear system dynamics


f␣=␣[␣␣sin(x(1))*cos(x(2))␣+␣(1+x(1))*u(1);
␣␣␣␣␣␣␣sin(x(2))^2␣+␣x(2)*u(2)␣+␣u(1)
␣␣␣␣␣␣␣x(3)␣+␣2*cos(x(2))*x(1)␣+␣u(2)␣␣␣␣␣␣];

% Make sure that the origin is an equilibrium point


f00␣=␣double(subs(f,␣[x;u],␣zeros(5,1)));
assert(all(f00==0),␣...
␣␣␣␣'the␣origin␣is␣not␣an␣equilibrium␣point');

% Compute the Jacobian of f with respect to x and u:


Jfx␣=␣jacobian(f,x);
Jfu␣=␣jacobian(f,u);

% Find the linearization of the system at the origin:


A␣=␣double(subs(Jfx,␣[x;u],␣zeros(5,1)));
B␣=␣double(subs(Jfu,␣[x;u],␣zeros(5,1)));

Here we used jacobian to determine a symbolic Jacobian matrix of a nonlinear


function and we computed the values Jx f (0, 0) and Ju f (0, 0) using subs. Note that
the result of subs is still a symbolic value. This is why we apply double on it to
get a numeric value.
Symbolic functions can be cast as function handles (anonymous functions) using
matlabFunction. It is often convenient to pass the optional parameter Vars to
specify the exact order of variables that the function handle should expect. We use
matlabFunction in Section 1.6, but here is a short example:
% System dimensions
nx␣=␣2;␣% number of states
nu␣=␣1;␣% number of inputs

% Define the symbols `x` and `u` for the state and input
% variables
x␣=␣sym('x',␣[nx,␣1],␣'real');
u␣=␣sym('u',␣'real');

% Define the system dynamics:


% f = [ x1^2 + x1 + u + 2*x2
% x2^2 + x2/10 − x1 ]
A␣=␣[1␣2;␣-1␣0.1];
f␣=␣A*x␣+␣x.^2␣+␣[1;0]*u;

% Construct a function handle from `f` where the first


% input argument is the state `x` and the second it the
% input `u`
fHandle␣=␣matlabFunction(f,␣'Vars',␣{x,␣u});

% Now we may call fHandle as follows:


xDot␣=␣fHandle([.1;-.2],␣0.1);

% We may further create a \matlab{} function from `f` and


1. CODING IN Matlab 73

% save it to a file ('functDynamics.m')


matlabFunction(f,␣'File',␣'functDynamics',␣...
␣␣␣␣'Vars',␣{x,␣u},␣'Outputs',␣{'dyn'});

1.4. Plotting. The command plot is used to create plots in Matlab. It is


good practice to plot setting linewidth to 2 or 3 for the result to look clearer. Using
a grid allows us to understand the plots better (use grid on;). Add axis labels us-
ing xlabel('my axis label [unit]'); and ylabel('my y axis [unit]');. In-
crease the font size using set(0,'DefaultAxesFontSize',12);. If you need to
overlay multiple lines use hold on;. This is useful when, for example, you need to
plot the actual system states xk and their estimates x̂k in the same plot. Once no
more data needs to be added to the plot, switch off the hold mode using hold off;.
Use axis tight; for the axes to be tight, or use the command axis([XMIN XMAX
YMIN YMAX]) to specify the axes’ limits.
Legends can be added with the command legend, e.g., legend('x', 'y'); will
clear a legend with two entries. Furthermore, legend accepts the option 'Location'
that allows us to specify the position of the legend (e.g., SouthEast, NorthEast, etc).
The Matlab documentation1 explains how to modify the line colors, style (solid,
dashed, etc), add markers and a lot more. For example plot(x, y, '-.ob'); plots
y versus x using a dash-dot line (-.), places circular markers (o) at the data points,
and colors both line and marker blue (b).
If you need to create multiple plots there are two main options. One is to use
figure; which start a new figure. You can then plot your data, use hold on; to
plot multiple data in the same figure. Then, call figure; again to create a new
figure. You may assign numerical IDs to the figures — e.g., figure(101);, 200, etc.
If you need to export a figure from Matlab you have several options. You can
get high quality graphics (that you can use to make posters or reports) by going
to File, then Export Setup, Rendering, choose painters (vector format) next
to Custom renderer (check the box), the Export and choose EPS under Files of
Type. Choose a filename and save the figure in EPS format. Save the figure in .fig
format too. Note that the size of the window when saving determines the size of the
output EPS figure.
The second option is to use matlab2tikz and export the figure in Tikz format and
include it in your LaTeX code. This way you can create high-quality customisable
graphics for your posters and reports.
You may have multiple plots in the same figure using subplot. There exist also
various types of plots. You might want to have a look at stairs.

1.5. Simulating. We need to simulate a parametric dynamical system (in our


case, the attitude and translational dynamics of a quadcopter). The first step in
that direction is to create a structure with the system parameters. In fact, it is
wise to make a function that returns the quadcopter parameters (its mass, moments
of inertia, thrust coefficients, etc) together with a perturbation factor (or, more
precisely, perturbation factors). Let us have a look at a very simple example where
the system dynamics is described by:
ẋ1 = a cos(x2 )x1 + bu
ẋ2 = cx22 − x31 ,

1Read more about the line specification and colour specification.


1. CODING IN Matlab 74

where a, b and c are some parameters with nominal values a = 1, b = 0.5 and
c = 0.1. We construct a Matlab function which returns a structure p as follows
function␣p␣=␣systemParameters(f)
%SYSTEMPARAMETERS returns a structure with the system
%parameters, possibly perturbed by a factor f.
%
%Syntax:
% p = systemParameters()
% p = systemParameters(f)
%
%Input arguments:
% f: perturbation parameter
% (optional −−− default value: 0)
%
%Output arguments:
% p structure with the system parameters.
% − a : explain what this parameter is and
% specify its units of measurement.
% − b : ditto
% − c : ditto
%
%See also:
% systemDynamics
%

if␣nargin␣==␣0,␣f␣=␣0;␣end
p.a␣=␣1␣*␣(1+f*randn);
p.b␣=␣0.5␣*␣(1+f*randn);
p.c␣=␣0.1␣*␣(1+f*randn);

Note that several lines are dedicated to explaining the input and output arguments
of this function. Documentation is an essential and inextricable part of software
development.
The perturbation factor f allows us to create random systems (which are “close”
to the original system) by perturbing the system parameters by multiplying by
1+f*randn. This is just an example; the form of the perturbation is application-
specific. Next, we define the system dynamics:
function␣xdot␣=␣systemDynamics(t,x,u,p)
%SYSTEMDYNAMICS retuns the derivative of x, f(t,x,u)
%at time t, state x and for an input value u. The system
% parameters are provided also as an input argument.
%
%Syntax:
% xdot = systemDynamics(t,x,u)
% xdot = systemDynamics(t,x,u,p)
%
%Input arguments:
% t : time
% x : state
% u : input
% p : system parameters
%
%Ouput arguments:
% xdot : value of f(t,x,u)
%
%See also:
1. CODING IN Matlab 75

% systemParameters
%

if␣nargin␣<␣4,␣p␣=␣systemParameters();␣end
xdot␣=␣[␣p.a␣*␣cos(x(2))*x(1)␣+␣p.b␣*␣u;
␣␣␣␣␣␣␣␣␣p.c␣*␣x(2)^2␣-␣x(1)^3];

Let us now simulate the above dynamical starting from an initial state x(0) = x0
and using a control function u(t, x).
In order to simulate the system dynamics in continuous time, we use ode45; a
solver for non-stiff differential equations. We need to specify the closed-loop function
f (t, x, u) = f (t, x, u(t, x)) as a function handle with two arguments: t and x (type
help ode45 for details). Here, let u(t, x) = sin(t) and we simulate the system
ẋ1 = a cos(x2 )x1 + b sin(t),
ẋ2 = cx22 − x31 .

% Initial condition x(t0) = x0


t0␣=␣0;
x0␣=␣[1;-1];

% Input signal
u␣=␣@(t,x)␣sin(t);

% The system dynamics is described by


sysPars␣=␣systemParameters();
functDynamics␣=␣@(t,x)␣systemDynamics(...
␣␣␣␣t,␣x,␣u(t,x),␣sysPars);

% Simulate the system using ode45


tmax␣=␣5;
[t,x]␣=␣ode45(@(t,x)␣functDynamics(t,x),␣[t0␣tmax],␣x0);

% Plot the system trajectory


plot(t,␣x,␣'linewidth',␣2);
grid␣on;
xlabel('Time');
ylabel('State');

1.6. Systems and control. Matlab offers several functions that facilitate
controller and observer design including discretisation routines and LQR/KF design.
We first need to define the system dynamics in state space form. We typically begin
by defining the continuous-time linearised dynamics of the form
d
dt x = Ax + Bu
y = Cx + Du.
Let us give an example where we define a random continuous-time system
nx␣=␣4;␣␣␣% number of states
nu␣=␣3;␣␣␣% number of inputs
ny␣=␣2;␣␣␣% number of outputs

A␣=␣randn(nx,␣nx);
B␣=␣randn(nx,␣nu);
C␣=␣randn(ny,␣nx);
D␣=␣randn(ny,␣nu);
1. CODING IN Matlab 76

systemContinuous␣=␣ss(A,␣B,␣C,␣D);

We may now discretise the continuous-time system and obtain the discrete-time
representation
xk+1 = Ad xk + Bd uk ,
yk = C d x k + D d u k ,
for a given sampling period Ts > 0. For that, we use Matlab’s c2d. Here is an
example of use where discretise the above continuous system with sampling rate
200Hz, that is, sampling period Ts = 1/200s.
Ts␣=␣1/200;
systemDiscrete␣=␣c2d(systemContinuous,␣Ts,␣'zoh');
Ad␣=␣systemDiscrete.A;
Bd␣=␣systemDiscrete.B;
Cd␣=␣systemDiscrete.C;
Dd␣=␣systemDiscrete.D;

The last argument means that we the discretisation is based on a zero-order hold
element. The sampling time is also stored in systemDiscrete.Ts.
If we want to define a discrete-time system in the first place, then, we must use
ss with -1 as a fifth argument, that is, systemDiscrete = ss(Ad, Bd, Cd, Dd,
-1);. This is useful when designing Kalman fitlers using kalman.
Moreover, ss allows to specify the names of the state, input and output variable.
Read the documentation for details (type help ss or doc ss).
The controllability and observability matrices of a system are returned by ctrb
and obsv respectively. Here is an example where we check whether a given discrete-
time system is controllable:
% For given matrices Ad, Bd, Cd, Dd:
systemDiscrete␣=␣ss(Ad,␣Bd,␣Cd,␣Dd,␣-1);
ctrbSystemDiscrete␣=␣ctrb(systemDiscrete);
% Assert that the system is controllable
assert␣(␣rank(ctrbSystemDiscrete,␣1e-6)␣==␣size(Ad,␣1),␣...
␣␣'Error:␣The␣system␣is␣not␣controllable');

% The same can be achieved using:


ctrbSystemDiscrete␣=␣ctrb(Ad,␣Bd);

To design an LQR controller for a discrete-time system with matrices (A, B,


C, D) we may use dlqr. Here is an example of use:
% System data
A␣=␣[1␣1;␣0␣0.5];
B␣=␣[0;1];
C␣=␣[1␣0.1];
D␣=␣0.1;

% System dimensions
nx␣=␣length(A);␣␣␣% number of states
nu␣=␣size(B,2);␣␣␣% number of inputs
ny␣=␣size(C,1);␣␣␣% number of outputs

% First check whether the system is controllable:


if␣rank(ctrb(A,B),1e-6)~=nx,
␣␣error('System␣not␣controllable');
1. CODING IN Matlab 77

end

% Define the weight matrices Q and R


Q␣=␣eye(nx);
R␣=␣2*eye(nu);
Kcontroller␣=␣-dlqr(A,B,Q,R,0);

% Assert that Kcontroller is a stabilising gain


assert(␣all(abs(eig(A+B*Kcontroller))␣<=␣1-1e-7),...
␣␣'K␣is␣not␣stabilising');

It is important to underline that dlqr returns a matrix K so that the feedback law
u(x) = −Kx solve the LQR optimisation problem (not u(x) = Kx). This is why
there is a minus in the code above. We have added an assertion to verify that the
eigenvalues of A + BK are indeed within the unit circle.
In fact, dlqr returns up to 3 arguments. The second one is the associated
positive definite symmetric matrix P which defines the optimal infinite-horizon cost
function J ⋆ = x′ P x. The third argument is an array of the eigenvalues of A + BK
in our notational convention (again, recall that dlqr returns −K).
Matlab’s kalman enables us to design Kalman filters for linear dynamical sys-
tems. Suppose we have the discrete-time linear time-invariant system
xk+1 = Axk + Buk + Gwk ,
yk = Cxk + Duk + Hwk + vk ,
where wk and vk are zero-mean independent random processes which follow the
normal distribution with
E[wk wk′ ] = Q,
E[vk vk′ ] = R,
E[wk vk′ ] = N.
In order to call kalman for the above discrete-time system we need to construct a
discrete-time dynamical system with matrices (A, [B G], C, [D H]).
Function kalman allows to specify which outputs are deterministic, that is,
known without error.
Let us give a complete example where we design an LQR controller and a KF
observer for a continuous-time nonlinear system with 4 states, 1 input and 3 output
variables. The system state is x = (x1 , x2 , x3 , x4 ) ∈ ℜ4 . The system dynamics is
described in continuous time by
x2 + x21
 
d a(x3 − x1 ) − b(x4 − x2 ) + c sin(x2 ) sin(x3 ) + x22 
(1.1) x=  =: f (x, u),
dt  x4 + x23 − sin2 (x1 ) 
a b a b
u + 10 x1 + 10 x2 − 10 x3 − 10 x4
with parameters a = 2, b = 0.5 and c = 0.1. The system output is described by
 
1 0 0 0
y= x,
0 0 1 0
that is, we measure x1 and x3 . We shall describe how noise affects the system state
and dynamics in discrete time in what follows.
We start by defining the nonlinear system in Matlab and linearising it using
the symbolic toolbox:
1. CODING IN Matlab 78

% Define the system dimensions


nx␣=␣4;␣% number of states
nu␣=␣1;␣% number of inputs
ny␣=␣2;␣% number of outputs

% Define the system parameters


a␣=␣2;
b␣=␣0.5;
c␣=␣0.1;

% Define the symbols xSymb (states) and uSymb (inputs)


xSymb␣=␣sym('x',␣[nx,␣1],␣'real');
uSymb␣=␣sym('u',␣[nu,␣1],␣'real');

% Define the nonlinear dynamics


f␣=␣[xSymb(2)␣+␣xSymb(1)^2
␣␣␣␣␣a*(xSymb(3)-xSymb(1))␣+␣b*(xSymb(4)-xSymb(2))␣+␣...
␣␣␣␣␣␣␣␣␣c*sin(xSymb(2))*sin(xSymb(3))␣+␣xSymb(2)^2
␣␣␣␣␣xSymb(4)␣+␣xSymb(3)^2␣+␣sin(xSymb(1))^2
␣␣␣␣␣uSymb␣+␣a*xSymb(1)/10␣+␣b*xSymb(2)/10␣-␣...
␣␣␣␣␣a*xSymb(3)/10␣-␣b*xSymb(4)/10];

% Define matrices C and D


C␣=␣[1␣0␣0␣0;
␣␣␣␣␣0␣0␣1␣0];
D␣=␣[0;0];

% Symbolic Jacobians:
Jfx␣=␣jacobian(f,␣xSymb);
Jfu␣=␣jacobian(f,␣uSymb);

% Linearise f at the origin:


A␣=␣double(subs(Jfx,␣[xSymb;␣uSymb],␣zeros(nx+nu,1)));
B␣=␣double(subs(Jfu,␣[xSymb;␣uSymb],␣zeros(nx+nu,1)));

It would be good to test whether the pair (A, B) is controllable and the pair (A, C)
is observable. We may now discretise the linearised system (with matrices (A, B,
C, D)) using c2d specifying the sampling time Ts .
% Define the linearised system using `ss`.
% This is a continuous−time system
linearisedSystem␣=␣ss(A,␣B,␣C,␣D);

% Discretise the linearised system:


Ts␣=␣0.4;
ZOH␣=␣c2d(linearisedSystem,␣Ts,'zoh');
[Ad,␣Bd,␣Cd,␣Dd]␣=␣ssdata(ZOH);

Next, we apply LQR controller to the discretised system with given matrices Q and
R:
% We choose matrices R and Q to be diagonal. R is a scalar
% since nu = 1 and Q can have zeros on its diagonal
% (it is only required to be positive semidefinite)
R␣␣=␣1;
Q␣␣=␣diag([1␣0.1␣1␣0.1]);
Kcontroller␣␣=␣-dlqr(Ad,␣Bd,␣Q,␣R);␣% mind the minus sign
assert(␣all(abs(eig(Ad+Bd*Kcontroller))␣<␣1-1e-6),␣...
1. CODING IN Matlab 79

␣␣␣␣'A+BK␣not␣stable');

We shall now design a Kalman filter for the discrete-time which has the form2
xk+1 = Ad xk + Bd (uk + wk )
yk = Cxk + vk .
This formulation means that the control command uk is sent to an actuator which
introduces noise to the system. In the case of the quadcopter, that would be that
the ESCs have not been modelled perfectly. The term vk reflects the measurement
error on yk . Following the notation we introduced above, it is G = Bd and H = 0.
The observer has the form
x̂k+1 = Ad x̂k + L(C x̂k − yk ) + Bd uk .

% We specify the covariances for `v` and `w`


covarSensors␣␣=␣0.02^2␣*␣eye(ny);
covarDynamics␣=␣0.0001^2␣*␣eye(nu);

% Next we build the Kalman filter:


kalmanSystem␣␣␣␣␣␣=␣ss(Ad,␣[Bd␣Bd],␣C,␣[D␣D],␣-1);
[~,␣Lobserver]␣␣␣=␣kalman(kalmanSystem,␣covarDynamics,␣...
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣covarSensors,␣0);
Lobserver␣=␣-Lobserver;

% Let us make sure that A+Lobserver*C is a stable matrix


assert(␣all(abs(eig(Ad+Lobserver*Cd))␣<␣1-1e-6),␣...
␣␣␣␣'A+LC␣not␣stable');

Last, we simulate the system


% Construct a function handle out of the symbolic function `f`
fun␣=␣matlabFunction(f,␣'Vars',␣{xSymb␣,␣uSymb});

% We define the initial condition and initial state estimate


x␣␣␣␣=␣[0.5;␣-0.1;␣0.1;␣-0.1]/10;
xEst␣=␣x;

T␣=␣100;␣% simulation time

% Initialise caches for the state and its estimates. We will


% store the sequence of states and state estimates in a
% matrix to be able to plot them afterwards.
% Here, we pre−allocate memory.
xCache␣=␣zeros(nx,␣T);
xEstCache␣=␣zeros(nx,T);

% Put the initial state and estimate in the cache.


xCache(:,1)␣=␣x;
xEstCache(:,1)␣=␣xEst;

% We simulate the system...


for␣k=1:T,
␣␣␣␣% Noises
␣␣␣␣w␣=␣mvnrnd(0,␣covarDynamics)';
␣␣␣␣v␣=␣mvnrnd(zeros(ny,1),␣covarSensors)';

2When using the exact linearisation method assuming a zero-order hold element, C = C and
d
Dd = D. Note also that it is very common to have D = 0.
1. CODING IN Matlab 80

0.25
State
0.2 Estimate

0.15

State, Estimate
0.1

0.05

-0.05

20 40 60 80 100
Discrete time

Figure 1. Actual and estimated states x1 and x̂1 for the above
Kalman filter. The figure is produced using plot with linewidth=2
and using grid on and axis tight. The graphic was exported
in EPS format. The legend was added with legend('State',
'Estimate');.

␣␣␣␣% Compute the control action using the estimated state


␣␣␣␣u␣=␣Kcontroller␣*␣xEst;

␣␣␣␣% Simulate the nonlinear system using ode45


␣␣␣␣% starting from x and for time Ts.
␣␣␣␣% The actuation that is applied to the system
␣␣␣␣% is u + w (perturbed by random noise)
␣␣␣␣[~,xi]␣=␣ode45(@(t,s)␣fun(s,␣u␣+␣w),␣[0,␣Ts],␣x);
␣␣␣␣x␣=␣xi(end,:)';

␣␣␣␣% Take the system output


␣␣␣␣y␣=␣Cd␣*␣x␣+␣Dd␣*␣u␣+␣v;

␣␣␣␣% Update the state estimates


␣␣␣␣xEst␣=␣Ad␣*␣xEst␣+␣Lobserver␣*␣(Cd␣*␣xEst␣-␣y)␣+␣Bd␣*␣u;

␣␣␣␣% Store the states in the cache


␣␣␣␣xEstCache(:,k+1)␣=␣xEst;
␣␣␣␣xCache(:,k+1)␣=␣x;
end

We may now plot the actual states against their estimates as shown in Figure 1. It
is, furthermore, interesting to see how (or whether) the controller and the observer
still work if the actual system parameters are different from their nominal values
(without re-designing the controller/observer). This will be part of your design and
simulations for the EAGLE project. For example, you have measured that the thrust
coefficient is ĉt = 0.1 and you have design a controller and an observer which — in
simulations for the nominal system — seem to work satisfactorily. Will the closed-
loop system still work well if the actual value is ct = 0.09? What if the quadcopter
is slightly heavier or lighter?
2. CLASSES 81

Tip 11.1.3

Note that the simulator code described above will not easily generalize for the
hierarchical controllers we will be developing at later stages in the project. Refer
to Ex. 25.3.1 for details.

1.7. Operations with quaternions. Matlab offers a collection of functions


to perform operations with quaternions:
(1) quatnorm(q): returns ∥q∥
(2) quatnormalize(q): returns q/∥q∥
(3) quatmultiply(p,q): performs p ⊗ q
(4) quatdivide: multiply by inverse quaternion
(5) quatconj: conjugate quaternion
(6) quatinv: returns the inverse quaternion
(7) quatrotate: rotates a vector by (the conjugate of) a quaternion. Note,
however, that because Matlab uses the JPL convention, we need to provide
quatconj(q) to quadrotate instead of q.
(8) atan2: four-quadrant arctangent. Use this function instead of atan.

2. Classes
The object-oriented programming approach is great way to organise our code
and provide easy-to-use functionality, clear abstractions and extensibility. MATLAB
supports classes and class hierarchy. Classes carry field definitions and methods, both
of which can be either private, that is, hidden from the end-user or public. Only
the functionality that is absolutely necessary for the user should be exported (made
public), while the rest should be stored internally.
First, we need to identify the attributes (fields) of our class. These are best to
remain hidden (private). The user should not be allowed to access them directly,
let alone, modify them. We will see that this prevents the user from updating these
attributes with erroneous values.
Let us have a look at a simple example of how classes can be used before we
discuss how we can construct them. Instances of classes are called objects and are
constructed as mypet = Dog('Charlie');: this command will construct an instance
of Dog which we store in the variable mypet whose name is 'Charlie'. This object
has a name which is stored internally and cannot be accessed directly (we cannot
access mypet.name). We need to be able to get the name of mypet. This should
be supported by a function called getName(), so mypet.getName() will return the
string 'Charlie'. Say we need to change the name of mypet to 'Bob'. We cannot
do mypet.name='Bob' as if mypet were a structure, because the field name is hidden
(private) and cannot be accessed from outside the class. Changing name should be
supported by a function of the form mypet.setName('Bob'). By doing so, it is now
possible to (i) document the behaviour of getName() and setName() and (ii) throw
an exception if the provided name is not acceptable (e.g., it is not a string).
It is probably best to understand how classes work through an example. Let us
construct a simple class which implements a linear state observer, that is, a linear
time-invariant dynamical system with state x̂k . For classes to have an internal
state, they need to be derived from handle. Here is an example of a class called
EagleObserver:
2. CLASSES 82

classdef␣EagleObserver␣<␣handle
% EAGLEOBSERVER is a class which (...)
% Detailed documentation goes here

␣␣␣␣% Private properties (not accessible from outside)


␣␣␣␣properties␣(Access␣=␣private)
␣␣␣␣␣␣␣␣xest;␣␣% current state estimate
␣␣␣␣␣␣␣␣Ad;␣␣␣␣% Matrix Ad of the discrete−time system
␣␣␣␣␣␣␣␣Bd;␣␣␣␣% Matrix Bd of the discrete−time system
␣␣␣␣␣␣␣␣Cd;␣␣␣␣% Matrix Cd of the discrete−time system
␣␣␣␣␣␣␣␣Dd;␣␣␣␣% Matrix Dd of the discrete−time system
␣␣␣␣␣␣␣␣L;␣␣␣␣␣% Gain of the linear observer
␣␣␣␣end␣% ... end of properties

␣␣␣␣methods␣(Access␣=␣private)
␣␣␣␣␣␣␣␣% '''''''''''''''''''''''''''''
␣␣␣␣␣␣␣␣% Methods for class use only
␣␣␣␣␣␣␣␣% '''''''''''''''''''''''''''''
␣␣␣␣end␣% ... end of private methods

␣␣␣␣methods␣(Access␣=␣public)
␣␣␣␣␣␣␣␣% '''''''''''''''''''''''''''''
␣␣␣␣␣␣␣␣% Public class methods go here
␣␣␣␣␣␣␣␣% − Constructor
␣␣␣␣␣␣␣␣% − Getters and Setters
␣␣␣␣␣␣␣␣% − Other methods
␣␣␣␣␣␣␣␣% '''''''''''''''''''''''''''''
␣␣␣␣end␣% ... end of public properties

end␣% ... end of class definition

There are two placeholders where we may add our custom methods. Classes are
saved in folder which start with the @ symbol followed by the class name. For
instance, this class is saved in @EagleObserver/ EagleObserver.m.
An essential part of a class is its constructor with which we may create instances
of that class. A constructor initialised the attributes of the class from user-specified
data. This is how a constructor for EagleObserver may look:

function␣o␣=␣EagleObserver(Ad,␣Bd,␣Cd,␣Dd,␣L,␣xest0)
␣␣␣␣%This is the constructor of EagleObserver
␣␣␣␣%
␣␣␣␣%Provide documentation
␣␣␣␣%

␣␣␣␣if␣nargin~=6,
␣␣␣␣error('Exactly␣6␣arguments␣must␣be␣provided');
␣␣␣␣end
␣␣␣␣% Test whether (Ad, Bd, Cd, Dd) are compatible
␣␣␣␣o.xest␣=␣xest0;
␣␣␣␣o.Ad␣=␣Ad;
␣␣␣␣o.Bd␣=␣Bd;
␣␣␣␣o.Cd␣=␣Cd;
␣␣␣␣o.Dd␣=␣Dd;
␣␣␣␣o.L␣=␣L;
end
3. THE MEX INTERFACE 83

A constructor is typically — except for certain special cases — a public method


so that users can use it to create instances of the class. Here, users can create
instances of EagleObserver specifying the system matrices Ad, Bd, Cd, Dd, the
observer gain L and the initial state estimate xest0. These data are stored in internal
attributes of the class (to which the user does not have access). We may construct
objects of EagleObserver by o = EagleObserver(Ad, Bd, Cd, Dd, L, xest0);.
Note that all functions must be terminated with an end.
In order to have access to the current state estimate that the EagleObserver
stores we need a getter method:
function␣x␣=␣getCurrentStateEstimate(o)
%GETCURRENTSTATEESTIMATE returns the current state
%estimate which is stored internally

x␣=␣o.xest;
end

Whatever method we implement, its first argument must be the object on which it
operates. We then invoke the method as xest = o.getCurrentStateEstimate();.
Let us now introduce a public method with which we can update the internally
stored state estimate by providing the output yk and the input uk . This will update
x̂ with Ad x̂ + L(ŷ − y) + Bd u, where ŷ = Cd x̂ + Dd u. We will in fact delegate the
computation of ŷ to a private method which is of no interest to the user. We then
implement the public method:
function␣updateStateEstimate(o,␣y,␣u)
␣␣␣␣yest␣=␣o.estimateY(u);
␣␣␣␣o.xest␣=␣o.Ad␣*␣o.xest␣+␣...
␣␣␣␣␣␣␣␣o.L␣*␣(yest␣-␣y)␣+␣...
␣␣␣␣␣␣␣␣o.Bd␣*␣u;
end

and the private method


function␣yest␣=␣estimateY(o,␣u)
␣␣␣␣yest␣=␣o.Cd␣*␣o.xest␣+␣o.Dd␣*␣u;
end

If the class has several function definitions, to better organise the source code, we
may define them in separate files as explained in the documentation.
There is a lot more one can do using classes. We may overload disp to customise
how the objects display, or even overload operators to be able to add, subtract,
multiply objects.

3. The MEX interface


3.1. Calling C++ code from Matlab. Eventually, you will create a C++ im-
plementation of your Matlab controller and observer. Before we can run this code
on the Zybo, we must be certain that the implementation is correct. For this we use
MEX. A MEX interface allows us to call C++ functions from MATLAB. For that we
need to implement a C++ function which maps the input arguments from MATLAB
to C++ and the output arguments from C++ back to MATLAB.
We provide a MEX wrapper for the mainControllers function. Tutorials related
to its usage are provided in §2 of Chapter 25. The techniques used to construct this
wrapper are described in this section. Specifically we consider (i) the wrapper class,
acting as the point of entry and (ii) printing debug info using MexHelper.
3. THE MEX INTERFACE 84

3.2. The Wrapper class. The MEX wrapper class is the point of entry for
the Matlab code. It stores a pointer to the Matlab engine that can be used to
execute methods in Matlab from C++. We could define a header for such a class
as follows.
#pragma␣once

#include␣"mex.hpp"
#include␣"mexAdapter.hpp"
#include␣<string>

using␣namespace␣matlab::mex;
using␣namespace␣matlab::data;

using␣matlab_engine_t␣=␣matlab::engine::MATLABEngine;

class␣MexFunction␣:␣public␣Function␣{
␣␣␣␣private:
␣␣␣␣␣␣␣␣// pointer to matlab instance
␣␣␣␣␣␣␣␣std::shared_ptr<matlab_engine_t>␣matlabPtr;

␣␣␣␣␣␣␣␣// array factory


␣␣␣␣␣␣␣␣ArrayFactory␣factory;

␣␣␣␣public:
␣␣␣␣␣␣␣␣MexFunction()␣{
␣␣␣␣␣␣␣␣␣␣␣␣matlabPtr␣=␣getEngine();
␣␣␣␣␣␣␣␣}

␣␣␣␣␣␣␣␣void␣operator()(
␣␣␣␣␣␣␣␣␣␣␣␣ArgumentList␣outputs,␣ArgumentList␣inputs
␣␣␣␣␣␣␣␣);

␣␣␣␣␣␣␣␣void␣error(std::string␣msg)␣{
␣␣␣␣␣␣␣␣␣␣␣␣matlabPtr->feval(
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣u"error",␣0,␣std::vector<Array>({
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣factory.createScalar(msg)␣})
␣␣␣␣␣␣␣␣␣␣␣␣);
␣␣␣␣␣␣␣␣}
};

The error method is used to raise exceptions.


The method operator() is the point of entry for Matlab. The arguments of
this method are ArgumentList objects. These can be indexed like normal c arrays.
Its elements are usually of type Array. These are the base type of all Matlab types
in C++. One can check its type using the getType method. You can find a list of
all the types on the ArrrayType page.
Consider for example the following source code for the operator method:
#include␣<MexFunction.hpp>

using␣namespace␣matlab::mex;
using␣namespace␣matlab::data;

void␣MexFunction::operator()(
␣␣␣␣ArgumentList␣outputs,␣ArgumentList␣inputs
)␣{
␣␣␣␣// check the type of the arguments
3. THE MEX INTERFACE 85

␣␣␣␣if␣(inputs.size()␣!=␣2)
␣␣␣␣␣␣␣␣error("invalid␣nb␣of␣arguments.");
␣␣␣␣if␣(inputs[0].getType()␣!=␣ArrayType::CHAR)
␣␣␣␣error("invalid␣first␣argument.");
␣␣␣␣if␣(inputs[1].getType()␣!=␣ArrayType::DOUBLE)
␣␣␣␣␣␣␣␣error("invalid␣second␣argument.");

␣␣␣␣// convert to c++ types


␣␣␣␣std::string␣command␣=␣CharArray(inputs[0]).toAscii();
␣␣␣␣double␣x␣=␣inputs[1][0];

␣␣␣␣// process input


␣␣␣␣// <...>
}

We implemented our method to expect a character array as the first argument and
an integer array as the second argument.
Output variables meanwhile are created using the ArrayFactory member. This
one has a number of create member functions, which can be used to create Matlab
types (i.e. Array objects). We can for example create a struct and populate it with
double arrays as follows:

// process input
auto␣result␣=␣factory.createStructArray(
␣␣␣␣{1},␣{"x",␣"y"}
);
result[0]["x"]␣=␣factory.createArray<double>(
␣␣␣␣{1,␣1},␣{2*x}
);
result[0]["y"]␣=␣factory.createArray<double>(
␣␣␣␣{2,␣1},␣{3*x,␣5*x}
);

// assign to output
outputs[0]␣=␣result;

3.3. Compiling MEX code. We can use the example from the previous folder.
Create an empty folder and open Matlab there. Create the file MexFunction.hpp
and MexFunction.cpp containing the header and source for our MexFunction class
respectively. You can then compile using:

mex␣MexFunction.cpp␣-I./

The first argument is the source file. You can replace it by a list of source files
seperated by spaces if you need multiple. The part after -I is the include folder.
Make sure that you have installed a C++ compiler before executing this command
(cf. Tut. 25.1.1).
You will find that a new file is created in your folder (extension .mexa64 on
Linux and .mexw64 on Windows). This file contains your method. You can execute
it using the usual Matlab syntax:

result␣=␣MexFunction('hello',␣2);
4. CODE GENERATION 86

3.4. Matlab Documentation. As a finishing touch we need to write docu-


mentation for our function, which should be accessible from Matlab when typing
help MexFunction. For that, we need to create a .m file with no functionality, just
containing documentation:
function␣outputs␣=␣MexFunction(inputs)
%FUNCTIONTOTEST_MEX ... (documentation)
%
%Syntax:
% outputs = MexFunction(inputs)
%
%Input arguments:
% inputs ...
%
%Output arguments:
% outputs ...
%

% Built−in function

3.5. Printing using MexHelper. We already showed how to print errors in


Matlab using the error method. However, to make printing more convenient the
provided framework comes with the MexHelper class. Example usage is as follows:
#include␣<mex/helper/MexHelper.hpp>
void␣somemethod()␣{
␣␣␣␣// Print some string
␣␣␣␣MainMex.display("hello␣world!");
␣␣␣␣// Use the stream option for printing strings
␣␣␣␣std::ostringstream␣stream;
␣␣␣␣stream␣<<␣"hello"␣<<␣"␣"␣<<␣"world"␣<<␣"!"␣<<␣std::endl;
␣␣␣␣MainMex.display(stream);
␣␣␣␣// Throw some error (ends execution)
␣␣␣␣MainMex.displayError("something␣went␣wrong");
}

4. Code generation
4.1. Introduction. Before we proceed with the technical details, let us have a
look at Fig. 2. In MATLAB you have implemented a design framework that allows
you to compute a controller gain K and a Kalman gain L alongside other results
(for example, related to reference tracking). The main goal is to write C++ code
that will allow you to run your controller and observer on the Zybo. A few basic
principles we need to take into consideration:
First, the code needs to be optimized; we are not aiming at making a generic
implementation for controller/observer design and implementation. We rather need
a custom implementation of a controller/observer for this specific system. This
dictates certain design choices which we shall discuss in what follows (e.g., we will
not use dynamic memory allocation anywhere and we will not have any for loops).
To this end, we will use a function like print affine, described in Section 4.2.
Second, we need a C++ implementation that is dependency-free and can run
on Zybo as well as on a PC (so that we are able to test it easily). This requires:
(i) a header (.hpp) file where we will declare the function signatures and (ii) an
implementation (.cpp) file that implements the functions declared in the header
4. CODE GENERATION 87

MATLAB Simulation Framework C++ Autopilot Framework

Code Unit Unit


Simulation Implementation
Generation Tests = Tests

MEX google call your auto-generated


print_affine
Interface test C++ code

Dependency-free
C++ implementation

.hpp file containing


function signatures

.cpp file containing code


generated by MATLAB

Figure 2. The design of the controller and observer for attitude


control has been carried out in MATLAB. In MATLAB, you need to
generate C++ code which will be called (i) by the EAGLE framework
which will run on Zybo and (ii) via a MEX interface which will allow
you to call it from Matlab (mainly for testing purposes).

file. The (auto-generated) C++ code should be very simple and, of course, it should
be tested.
Last, our overall design should be done in such a way that if we need to change
any of the controller/observer parameters (e.g., matrices Q and R) it should be
very easy and really straightforward to generate C++ code which can run on Zybo.
You will have to tweak the controller and observer parameters and run experiments
on your quadcopter several times in order to fine tune all parameters. This means
that under no circumstances should we have to copy pieces of code from one file to
another, or have to do anything else manually.

4.2. Code generation principles. In the EAGLE project you will have to
generate C++ code from Matlab which will be integrated into the Autopilot project
to be run on the Zybo. For that purpose, you need to be able to modify the tuning
parameters of your controllers/observers, build new controller/observer gains and
with an one-line command generate C++ code.
This code needs to have a mutable and an immutable part. The immutable part
contains pieces of code which do not change if you modify the tuning parameters.
Header files, for example, should be immutable. The mutable part consists of those
parts of the code which change when you modify the tuning parameters.
Examples of mutable code are controller and observer functions. A very handy
utility for code generation is a function that generates code which performs the
operation y ← Ax + b, over a constant matrix A and variables x, y correspond to
4. CODE GENERATION 88

memory positions. The variable b is then a constant vector. Note that y cannot
point to the same memory position as x!
An example implementation of such a method is provided in
eaglesim.codegen.print affine.m. Here is an example of use:
4. CODE GENERATION 89

import␣eaglesim.codegen.print_affine;
A␣=␣randn(3,2);␣␣% random matrix
b␣=␣randn(3,1);␣␣% random vector
filep␣=␣1;␣␣␣␣␣␣␣% print to the system output
print_affine(filep,␣'y',␣'x',␣A,␣b);

The output of this function will be C++ code of the following form, assuming you
are working with basic arrays.
y[0]␣=␣(0.537667)␣*␣x[0]␣+␣(0.862173)␣*␣x[1]␣+␣(-0.433592);
y[1]␣=␣(␣1.83389)␣*␣x[0]␣+␣(0.318765)␣*␣x[1]␣+␣(␣0.342624);
y[2]␣=␣(-2.25885)␣*␣x[0]␣+␣(-1.30769)␣*␣x[1]␣+␣(␣␣␣3.5784);

However, it is advised to use the structs from the math module. Assuming y is a
Vec3f and x is a Vec2f. You would then call print affine as:
import␣eaglesim.codegen.print_affine;
A␣=␣randn(3,2);␣␣% random matrix
b␣=␣randn(3,1);␣␣% random vector
filep␣=␣1;␣␣␣␣␣␣␣% print to the system output

% get struct indices


y␣=␣{'y.x',␣'y.y',␣'y.z'};
x␣=␣{'x.x',␣'x.y'};

% print the operation


print_affine(filep,␣y,␣x,␣A,␣b);

The output is then


y.x␣=␣(-0.303441)␣*␣x.x␣+␣(0.888396)␣*␣x.y␣+␣(-0.809499);
y.y␣=␣␣(0.293871)␣*␣x.x␣+␣(-1.14707)␣*␣x.y␣+␣(␣-2.94428);
y.z␣=␣(-0.787283)␣*␣x.x␣+␣(-1.06887)␣*␣x.y␣+␣(␣␣1.43838);

If you’re feeling up to a challenge, you can extend the math module to implement
matrix-vector multiplication. Be careful when doing so. Memory management in
C++ is challenging. Avoid commands like new and read up on passing by reference.
You should be aware of the difference between heap and stack memory too.
We would recommend first reworking the Vector.hpp file to use templates, then
creating a Matrix.hpp file using the template <rows, columns>. Example code
would then look like:
Matrixf<3,2>␣A␣=␣Matrixf<3,2>{{
␣␣␣␣{+0.333510833066,␣+0.391353604433},
␣␣␣␣{+0.451679418928,␣-0.130284653146},
␣␣␣␣{+0.183689095862,␣-0.476153016619},
}};
y␣=␣-1.5␣*␣y␣+␣A␣*␣x;

The method print affine also supports writing to a file. To do so simply


replace filep = 1; with
filep␣=␣fopen('test.cpp',␣'w');

Don’t forget to close the file afterwards with fclose(filep);.


The function print affine is just an example of how you can perform a calcu-
lation in Matlab, convert it to C++ code and write it to a file. Your team should
discuss what function(s) would be best to generate your controller/observer. The
4. CODE GENERATION 90

ultimate goal is to create three files whose functions will be called by your control sys-
tems: AttitudeCodegen.cpp, AltitudeCodegen.cpp and PositionCodegen.cpp.
Of course we cannot construct an entire file with just print affine. We want
to print to specific positions in a file, following the mutable–immutable principle.
The method eaglesim.codegen.process template until.m provides a basic tool
for doing so.
For example we could try producing the following code:
#include␣<math/Vector.hpp>

Vec3f␣evaluate(Vec2f␣x)␣{
␣␣␣␣// initialize y
␣␣␣␣Vec3f␣y␣=␣{};

␣␣␣␣// evaluate y = A*x + b


␣␣␣␣y.x␣=␣(-0.303441)␣*␣x.x␣+␣(0.888396)␣*␣x.y␣+␣(-0.809499);
␣␣␣␣y.y␣=␣␣(0.293871)␣*␣x.x␣+␣(-1.14707)␣*␣x.y␣+␣(␣-2.94428);
␣␣␣␣y.z␣=␣(-0.787283)␣*␣x.x␣+␣(-1.06887)␣*␣x.y␣+␣(␣␣1.43838);

␣␣␣␣// return result


␣␣␣␣return␣y;
}

We only want to print the lines below // evaluate y = A*x + b. So it makes to


define a template that looks like:
#include␣<math/Vector.hpp>

Vec3f␣evaluate(Vec2f␣x)␣{
␣␣␣␣// initialize y
␣␣␣␣Vec3f␣y␣=␣{};

␣␣␣␣// evaluate y = A*x + b


␣␣␣␣//<!calc y>

␣␣␣␣// return result


␣␣␣␣return␣y;
}
//<!end of file>

We assume it is called matvec.cpp.template. The marker //<!calc y> will deter-


mine where we place the output of print affine. It is important that this string
appears nowhere else in the file.
Turning the template into the desired cpp file would then happen as follows
import␣eaglesim.codegen.print_affine;
import␣eaglesim.codegen.process_template_until;
A␣=␣randn(3,2);␣␣% random matrix
b␣=␣randn(3,1);␣␣% random vector

% open files
finID␣=␣fopen('matvec.cpp.template',␣'r');
foutID␣=␣fopen('matvec.cpp',␣'w');

% get struct indices


y␣=␣{'y.x',␣'y.y',␣'y.z'};
x␣=␣{'x.x',␣'x.y'};
4. CODE GENERATION 91

% parse the template


process_template_until(finID,␣foutID,␣'//<!calc␣y>');

% add y = A*x + b
print_affine(filep,␣y,␣x,␣A,␣b);

% process the rest of the file.


process_template_until(...
␣␣␣␣finID,␣foutID,␣'//<!end␣of␣file>');

% close the files


fclose(finID);␣fclose(foutID);

% display the result


type('matvec.cpp');

We also added Ex. 25.2.1 for some specific instructions related to codegeneration
for the provided framework.
CHAPTER 12

Programming Languages – C++

This chapter is not intended as a full tutorial on C++. It is rather a hitchhiker’s


guide to C++ programming for the EAGLE project.

1. The C++ Language

C++ is a compiled programming language in the C family. It originated from an


improved version of the C programming language, with added language support
for object-oriented programming (OOP). Although C++ was based on C when it
was created back in 1985, both languages have diverged since then. However, the
compatibility between the two languages is still remarkable. For that reason, most
of the concepts introduced in Ch. 10 still hold in C++, and many C programs can
still be compiled using a C++ compiler.

1.1. Programming style. Despite their syntactical similarities, the C++ pro-
gramming style is very different from the style used in C development. Many stan-
dard practices in C, such as manual memory management or unsafe type casts, are
actually considered bad practice in C++. Whereas C is an imperative and procedural
language, C++ also supports other paradigms, such as object-oriented programming
(classes and inheritance), generic programming (template classes and functions) and
functional programming (first-class (lambda) functions and closures).

1.2. The C++ standard library. In addition to the core language, an im-
portant part of C++ is the standard library. This library contains data struc-
tures and containers (such as strings, dynamic arrays, maps (dictionaries), linked
lists, queues, tuples, etc.), algorithms (such as sorting, binary search, heap op-
erations, transform/reduce, etc.), and many other utilities such as mathematical
functions, random generators, regular expressions, file I/O and filesystem opera-
tions, utilities for date and time handling, utilities for memory management, sup-
port for concurrency and parallelism, and much more. You can find an overview on
https://en.cppreference.com/w/cpp.

1.3. Different versions. C++ was first standardized in 1998, this version is
known as C++98. In 2003, C++03 resolved some issues in the original standard.
C++11, released in 2011, came with many new features and improvements over the
previous versions. It is usually regarded as the start of “modern C++”. Since C++11,
a new version is released every three years, the latest version being C++20. The
EAGLE Autopilot project uses C++17, because the compilers installed on the ESAT
computers do not support all of the latest C++20 features yet. A large portion is
supported though, so if you think you can benefit from these features, you can always
compile your code in C++20 mode by changing the appropriate compiler options.
92
2. TYPES & VARIABLES 93

2. Types & Variables

Knowledge of basic programming concepts such as variables and data types is as-
sumed. You can always fall back on the Basic programming in Python textbook
from the first bachelor.

2.1. The C++ Type System. C++ is a statically typed language. This means
that every variable has a fixed data type that is known at compile time. Functions
take parameters of known types and return a known type. The compiler makes
sure that you call functions with the correct types of arguments and ensures that
conversions between types are valid.
There are built-in fundamental types such as int, float and bool, standard library
types such as std::string or std::vector, and user-defined types using structs
and classes.

2.2. Variables. To create a variable, you specify the type, followed by the
variable name. For example:
float␣altitude;␣// definition of variable with the name
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// 'altitude' of the data type 'float'
int␣index;␣␣␣␣␣␣// definition of variable with the name
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// 'index' of the data type 'int'

In the example above, the variables were only defined, not initialized. The variables
were given a name, but weren’t assigned any value yet. You are not allowed to read
from uninitialized variables.
There are many ways to initialize a variable in C++, we’ll start with the simplest
one, using the assignment operator (=):
float␣a␣=␣1.2;␣// definition and initialization
float␣b;␣␣␣␣␣␣␣// definition
b␣=␣1.3;␣␣␣␣␣␣␣// initialization

After this piece of code, the variable a now contains the value 1.2, and the variable
b contains the value 1.3.
Whenever possible, try to immediately initialize all variables when you define them.
A second way to initialize a variable is using parentheses (()). For example, the
following is equivalent to the initialization of a in the previous snippet:
float␣a(1.2);␣// definition and initialization

Initialization using parentheses is more flexible than the assignment operator (=),
because it allows you to pass multiple arguments, for example:
std::complex<float>␣c(0.5,␣-0.5);␣// 0.5 − 0.5i

The parentheses should never be emtpy 1.


Finally, a third way to initialize variables is using curly braces ({}):
1This would declare a function, for example, float a(); is a function with the name a that
returns a value of type float and accepts no arguments.
2. TYPES & VARIABLES 94

float␣a␣{1.2};
std::complex<float>␣c␣{0.5,␣-0.5};␣// 0.5 − 0.5i

An advantage of initialization using curly braces is that it disallows so-called nar-


rowing conversions, where conversion from one type to another would cause loss of
information. For example, initializing an integer with a floating point value would
cause the decimal part to be truncated, and this is not allowed when using curly
braces (which usually is a good thing):
int␣i␣{1.5};␣// error: narrowing conversion of '1.5e+0' from
␣␣␣␣␣␣␣␣␣␣␣␣␣// 'double' to 'int'

Braces are also used for initializing containers, for example:


std::vector<float>␣v␣{0.1,␣0.2,␣0.3};␣// A vector containing three
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// floating point values
std::tuple<int,␣float>␣t␣{42,␣3.14};␣␣// A tuple consisting of an
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// int and a float
std::string␣str␣{'a',␣'b',␣'c'};␣␣␣␣␣␣// The string "abc"
std::map<std::string,␣std::string>␣dict␣{␣// A dictionary with
␣␣␣␣{"key",␣"value"},␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// two entries, mapping
␣␣␣␣{"foo",␣"bar"},␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// "key" to "value" and
};␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// "foo" to "bar"

You can also leave the braces emtpy, this is called value initialization. Do note the
difference between the following examples:
int␣i;␣␣␣␣// value of i is undefined
int␣j␣{};␣// value of j is zero
int␣f();␣␣// f is a function without arguments returning an int

Some guidelines recommend always using curly braces, some recommend using using
the assignment operator if you only have to provide a single value (e.g. integers)
and curly braces when you have to provide more than one value (e.g. complex
numbers, containers or user-defined data types). Curly braces are usually preferred
over parentheses because they allow you to catch narrowing conversions.
You can combine the assignment operator and curly braces, but the differences are
quite subtle. If you’re interested, you can start your journey down this rabbit hole
on https://en.cppreference.com/w/cpp/language/initialization.

Warning 12.2.1

In some cases, especially with containers, it does make a difference whether you
use parentheses or curly braces, for example:
std::string␣good(33,␣'a');
std::string␣bad␣{33,␣'a'};

The value of good is the string “aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa” (the


character ‘a’ repeated 33 times). On the other hand, the value of bad is “!a”, a
two-character string where the first character is the ASCII character represented
by the value 33 (the exclamation point), and the second character is ‘a’. a
2. TYPES & VARIABLES 95

The different initialization strategies are an unfortunate consequence of how the


language evolved, but issues like in the string example above are rather rare in
actual code.
aIf you look at the documentation for std::string on
https://en.cppreference.com/w/cpp/string/basic string/basic string, you’ll see that good was
initialized using the std::string(size t count, char ch) constructor (2), and bad was ini-
tialized using the std::string(std::initializer list<char> ilist) constructor (9).

2.3. Literals. Literals are constant values directly expressed in the source code.
For example, in the previous section, the number 33 is an integer literal, and ‘a’ is
a character literal.

2.3.1. Integer literals. Integer literals start with a number and don’t contain a
decimal point. The number 123 is an integer literal and its type is int. Different
prefixes can be used to specify the base: 0x for hexadecimal (base 16) and 0b for
binary (base 2). If the integer literal starts with a leading zero, it is interpreted as
octal (base 8). You can use single quotes as a digit separator to make long numbers
more readable.
For example:
print(123);␣␣// "123" (base 10)
print(0xF);␣␣// "15" (base 16)
print(0b11);␣// "3" (base 2)
print(012);␣␣// "10" (base 8)
print(1'000'000);
print(0b1010'0101'1010'0101);

Suffixes can be used to change the data type of the literal, for example, the type of
1u is unsigned int, and the type of 1ll is long long. There are specific rules for
the case when the value of the integer literal is larger than the maximum value that
can be represented by the int data type.
For a full overview, see https://en.cppreference.com/w/cpp/language/integer literal.

2.3.2. Floating-point literals. Floating-point literals start with a digit and con-
tain a decimal point or an exponent. For example, 3.14 and 1e-3 are floating point
literals, their type is double. Suffixes can change the type of the literal: 1.f is of
type float and 1.l is of type long double.
print(3.14);␣// "3.14"
print(1e-3);␣// "0.001"

2.3.3. Character literals. A character literal is a single character2 delimited by


single quotes. For example, ’a’ is a character literal, its type is char. On most
modern systems, characters are represented by an 8-bit integer, encoded as 7-bit
ASCII.
Non-printable characters or the single quote character itself have to be escaped
using a backslash when used in a character literal. For example, the literal '\n'
is a literal representation of the newline character, '\'' is a literal single quote
2In most cases.
2. TYPES & VARIABLES 96

character, and '\\' is a literal backslash. A full list of escape sequences can be
found at https://en.cppreference.com/w/cpp/language/escape.
Prefixes exist for different character encodings like UTF-8, UTF-16 and UTF-32 or
platform-specific wide characters.
For a full overview, see https://en.cppreference.com/w/cpp/language/character literal.

2.3.4. String literals. A string literal is a sequence of characters delimited by


double quotes. For example, "abc" is a string literal, its type is const char[4] or
immutable array of four characters (see §2.4.1 and §2.8).
The array representing the string literal is terminated by a null character, so the
total length is one more than the number of characters in the literal. String literals
are read-only, you cannot change their characters at run time.
Similar to character literals, escape sequences can be used to represent special char-
acters, double quotes and backslashes. Prefixes exist for different character encod-
ings.
Multiple adjacent string literals are concatenated, for example, "ab" "cd" "ef" is
equivalent to "abcdef".
Raw string literals make it easy to enter multi-line strings and don’t require escape
sequences to encode quotes or newlines.
See more information, see https://en.cppreference.com/w/cpp/language/string literal.

2.3.5. User-defined literals. C++ supports user-defined string literals for things
like units or domain-specific languages. The standard library defines multiple of
these literal suffixes, for example:
#include␣<chrono>
using␣namespace␣std::chrono_literals;
std::chrono::duration␣interval␣=␣2min␣+␣30s;

#include␣<complex>
using␣namespace␣std::complex_literals;
std::complex<double>␣c␣=␣0.5␣-␣0.5i;

To enable these literals, you have to introduce them into the current scope using
using namespace (§12.1).
For more information, see https://en.cppreference.com/w/cpp/language/user literal.

2.4. Arrays. Arrays are contiguous lists of data of the same type. C++ supports
C-style arrays, and the standard library has an std::array container as well.

2.4.1. C-style Arrays. A C-style array consists of N objects of a the same type,
contiguously laid out in memory. The elements of an array of size N can be accessed
using indices from 0 to N − 1 (inclusive). The index of the first element is 0, the
index of the last element is N − 1.
Variables of array type are declared using square brackets ([]) after the variable
name. Between the brackets, you can specify the number of elements in the array. If
you immediately initialize the array, the compiler will count the number of elements
in the initializer list for you, so you don’t have to specify the size yourself.
2. TYPES & VARIABLES 97

int␣myArray[3]␣␣␣␣␣=␣{1,␣2,␣3};␣// Specify the size yourself


int␣anotherArray[]␣=␣{4,␣5,␣6};␣// Let the compiler count the
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// elements for you

Accessing (or indexing) an array is done using the array subscript operator ([]).
The argument to this operator (the number between the brackets) is the index.
As mentioned earlier, this index is zero-based: myArray[0] is the first element of
myArray, myArray[1] is the second element, and myArray[2] is the third and last
element. Trying to acces the non-existent element at index 3 is not allowed. Note
that even though the size of the array is 3, the maximum allowed index is 2.
For performance reasons, no bounds checking on array accesses is performed. If you
do try to access an index that outside of the bounds of an array, you don’t get an error
message, but you will probably corrupt some other part of your program. If you’re
lucky, the program will crash and you know you’ve made a mistake somewhere. If
you’re unlucky, the program will keep on running, only to give incorrect results later.
print(myArray[0]);␣// "1"
print(myArray[1]);␣// "2"
print(myArray[2]);␣// "3"
print(myArray[3]);␣// bug

C-style arrays have some serious drawbacks that are explained in Section 13.2, so it
is recommended to instead use C++ standard library arrays, which will be discussed
in the following section.

2.4.2. C++ Standard Library Arrays. The C++ standard library has many differ-
ent containers, including an array container. It solves many of the problems with
C-style arrays.
We’ll rewrite the previous example using C++ arrays:
#include␣<array>

std::array<int,␣3>␣myArray␣=␣{1,␣2,␣3};
std::array␣anotherArray␣␣␣␣=␣{4,␣5,␣6};

The first argument between the angle brackets (<>) is the type of the elements in
the array, and the second argument is the size of the array. Similar to C-style arrays
above, you don’t have to specify the number of elements if an initializer is given.
The compiler will not only count the number of elements for you, it will also infer
the type of the elements.
Accessing the elements of an array is done using the same array subscript operator
([]) as before, e.g. myArray[1] is the second element. Using an index that is greater
than or equal to the size of the array is still a bug.

2.5. Type Aliases. You can create aliases for data types. This can be useful
to make the intent of your code clear to the reader, or if you have to use the same
long type name very often. Type aliases are defined using the using keyword:
using␣AttitudeReference␣=␣Quaternion;
using␣array3f␣=␣std::array<float,␣3>;
2. TYPES & VARIABLES 98

In legacy code or code that was ported from C, you might come across the typedef
keyword, which also defines a type alias:
typedef␣Quaternion␣AttitudeReference;
typedef␣std::array<float,␣3>␣array3f;

The using syntax is clearer and typedef has some limitations, so you should prefer
using using in new code.
Type aliases are weak aliases, this means that they are just a different name for the
same type, the compiler still sees them as the same type, so you cannot overload on
two aliases of the same type, for example (see §3.5).

2.6. Pointers. Explaining pointers from scratch is not the goal of this docu-
ment. It’s an important topic, but it’s not critical to understand the given code of
the EAGLE project.
If the concept of pointers is new to you, it might be interesting to read some online
tutorials like this one, this one or watch this video from the Stanford CS Education
Library. Otherwise, just remember that a pointer is a variable that holds the address
of another variable. To get or set the value of the variable pointed to by the pointer,
you have to dereference the pointer using the dereference operator (*). You can get
the address of a variable using the address-of operator (&).
int␣i␣=␣7;␣␣␣// A normal variable 'i'
int␣*p␣=␣&i;␣// Assign the address of the variable 'i' to the
␣␣␣␣␣␣␣␣␣␣␣␣␣// pointer 'p'
print(*p);␣␣␣// "7", dereference the pointer 'p' to read the
␣␣␣␣␣␣␣␣␣␣␣␣␣// value of 'i'
*p␣=␣42;␣␣␣␣␣// Dereference the pointer 'p' and to assign the
␣␣␣␣␣␣␣␣␣␣␣␣␣// value 42 to the variable 'i'
print(*p);␣␣␣// "42"
print(i);␣␣␣␣// "42", the value of 'i' was modified through 'p'

A null pointer is a pointer that doesn’t point to any other variable. Dereferencing it
is a bug. You’ll probably get a segmentation error if you are running your program
on a platform with an OS. When running bare-metal on the Zybo, your program
will probably just stop without any indication as to why.
int*␣p␣=␣nullptr;
*p␣=␣42;␣␣// bug: Dereferencing a null pointer

2.7. References. Using pointers can be cumbersome, because you need to


dereference them every time you want to access the value of the pointee3. Luckily,
C++ has another concept: references. A reference behaves like a pointer, because it
refers to another variable and you can use it to change the value of the referenced
value, but you don’t have to dereference it. The reference becomes like an alias for
the original variable.
int␣i␣=␣7;␣␣// A normal variable 'i'
int␣&r␣=␣i;␣// Initialize the reference 'r' to refer to the
␣␣␣␣␣␣␣␣␣␣␣␣// variable 'i'
print(r);␣␣␣// "7", 'r' acts as an alias of 'i'

3The variable being pointed to by the pointer.


2. TYPES & VARIABLES 99

r␣=␣42;␣␣␣␣␣// Use the reference 'r' to assign the


␣␣␣␣␣␣␣␣␣␣␣␣// value 42 to the variable 'i'
print(r);␣␣␣// "42"
print(i);␣␣␣// "42", the value of 'i' was modified through 'r'

Unlike with pointers, there’s no such thing as a “null reference”. You can assume
that a reference always refers to something. Once a reference has been created, you
cannot change it to refer to a different variable.

2.8. Const. The const qualifier can be added to a type to make it immutable
or read-only. For example, a variable of type const int cannot be modified after
it has been created:
const␣int␣i␣=␣7;
i␣=␣42;␣// error: assignment of read−only variable 'i'

One reason to use the const qualifier is to allow for better optimization of your
code. If the compiler knows that some variables remain constant, it can emit more
efficient code. A second (and perhaps even more important) advantage of const
is that it makes it easier for programmers to reason about the code: knowing that
some variables will not change lowers the overall cognitive load, and allows the
programmer to focus on the important parts of the code and on the variables that
do change.

2.8.1. Const and pointers. The const qualifier can also be added to pointers:
const␣int␣i␣=␣7;
const␣int␣*p␣=␣&i;␣// type of 'p' is 'pointer to const int'
*p␣=␣42;␣// error: assignment of read−only location '*p'

A pointer-to-const can point to a non-const variable. This means that that specific
pointer cannot be used to modify the value of the variable, but that doesn’t mean
that the variable cannot be modified directly (or by other pointers):
int␣i␣=␣7;
const␣int␣*p␣=␣&i;␣// type of 'p' is 'pointer to const int'
i␣=␣42;␣␣// ok: i is not const
*p␣=␣43;␣// error: assignment of read−only location '*p'

The opposite is not true, a pointer-to-non-const cannot point to a const variable.


Otherwise you could use this pointer to modify the value of a immutable variable,
which is obviously not allowed.
const␣int␣i␣=␣7;
int␣*p␣=␣&i;␣// error: invalid conversion from 'const int*'
␣␣␣␣␣␣␣␣␣␣␣␣␣// to 'int*'

Even though a pointer-to-const cannot modify the variable it points to, the pointer
itself is still mutable, so it can be reassigned to point to a different variable:
const␣int␣i␣=␣7;
const␣int␣*p␣=␣&i;
print(*p);␣// "7"
const␣int␣j␣=␣8;
p␣=␣&j;␣␣␣␣// ok
2. TYPES & VARIABLES 100

print(*p);␣// "8"

To prevent this, you can add a const qualifier to the pointer type itself (instead of
to the type of the pointee). The syntax rules say that the const before the asterisk
(i.e. next to the type of the pointee) applies to the mutability of the pointee, and
the const after the asterisk (i.e. next to the name of the pointer) applies to the
mutability of the pointer itself.
int␣i␣=␣7;
int␣*const␣p␣=␣&i;␣// type of 'p' is 'const pointer to
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// non−const int'
print(*p);␣// "7"
*p␣=␣42;␣␣␣// ok
print(*p);␣// "42"
int␣j␣=␣8;
p␣=␣&j;␣// error: assignment of read−only variable 'p'

You can also combine the two, to create an immutable pointer that points to an
immutable value:
const␣int␣i␣=␣7;
const␣int␣*const␣p␣=␣&i;␣// type of 'p' is 'const pointer to
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// const int'
*p␣=␣42;␣// error: assignment of read−only location '*p'
const␣int␣j␣=␣8;
p␣=␣&j;␣␣// error: assignment of read−only variable 'p'

In conclusion:

int * : mutable pointer to mutable integer


const int * : mutable pointer to immutable integer
int *const : immutable pointer to mutable integer
const int *const : immutable pointer to immutable integer

2.9. List of Built-In Types.

✓ Signed Integer Types A signed integer can represent a finite set of posi-
tive and negative whole numbers. The actual size of these types is platform-
dependent, and signed integer overflow is undefined behavior (you are not
allowed to perform a calculation that results in a number that is too large
to be represented by the given signed integer type). Examples of signed
integer types are:
short int, int, long int, long long int
Often times, you want to make sure that the type is large enough to hold
a certain number. In that case, you can use the fixed-width integer types
defined in the <cstdint> header:
int8 t, int16 t, int32 t, int64 t
✓ Unsigned Integer Types An unsigned integer can represent positive
whole numbers. The actual size of these types is platform-dependent as
well, but unlike their signed counterparts, overflow behavior is defined
to be modulo 2n , where n is the number of bits of the representation.
This means that for an 8-bit unsigned integer, uint8 t(256) == 0 because
256 ≡ 0 mod 28 . Examples of unsigned integer types are:
2. TYPES & VARIABLES 101

unsigned short int, unsigned int, unsigned long int


Additionally, <cstdint> also defines the fixed-width unsigned integer types:
uint8 t, uint16 t, uint32 t, uint64 t
✓ Character Types There are three distinct character types. If you use
math on these types in your code, keep in mind that char is usually un-
signed on ARM processors, and signed on x86 processors. Examples of
character types are:
unsigned char, signed char, char
There are some wide character types to accommodate for UTF-16 and
UTF-32 character sets as well, but unless you’re writing code for Windows,
you don’t have to worry about these, and you can just use UTF-8 saved as
normal chars.
✓ Floating Point Types There are single, double and extended precision
floating point types available, respectively:
float, double, long double
The first two usually follow the IEEE-754 specification, and are 32 and 64
bits wide. Operations on these types are carried out in hardware by the
Zybo’s FPU. There is no standard representation for the extended precision
long double type. On the Zybo it’s the same size as double precision, but
on a Raspberry Pi in 64-bit mode or an x86 64 computer, it can be a
quad-precision IEEE-574 (if you use GCC, for example).
✓ The Boolean Type The bool type is probably the simplest, it can hold
just one of two possible values, true or false.
✓ Other Types
✓ size t – unsigned integer type for representing a size
✓ ptrdiff t – signed integer type to represent the difference between
two pointers
✓ std::byte – non-integer type for representing raw memory or binary
data

2.10. Type deduction. The exact type of a variable might not be that impor-
tant in a certain contexts, or the type name could be very long. In those cases, you
can use the auto keyword instead of a type name to let the compiler deduce the type
of an expression for you. It is important to note that the variables declared with
auto still have a fixed, well-defined data type, it’s just determined by the compiler
instead of specified by the programmer.
std::complex<double>␣c␣{0.5,␣-0.5};
auto␣z␣=␣c␣*␣2.;␣// the type of 'z' is 'std::complex<double>'

auto will never deduce a reference type:


int␣i␣=␣42;
int␣&r␣=␣i;
auto␣j␣=␣r;␣// type of 'j' is 'int', not 'int &'
␣␣␣␣␣␣␣␣␣␣␣␣// 'j' is a copy of 'i', not an alias

2.11. Scope. Every variable has a region of code where it can be accessed,
called its scope. Usually, the scope is delimited by the curly braces ({}) surrounding
a block of code:
2. TYPES & VARIABLES 102

{
␣␣␣␣int␣a␣=␣5;
}
int␣b␣=␣a␣+␣6;␣// error: 'a' was not declared in this scope

Variables outside of any curly braces are said to be part of the global scope and
are called global variables. They are available inside of all other scopes.
If a nested scope declares a variable with the same name as a variable in the outer
scope, all references to this variable name in the inner scope refer to the variable in
the inner scope, not to the variable in the outer scope. This is called shadowing.
int␣a␣=␣42;
{
␣␣␣␣int␣a␣=␣5;␣// this local variable 'a' shadows the variable
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// with the same name on the first line
␣␣␣␣a␣=␣a␣+␣1;␣// this refers to 'a' on the third line
␣␣␣␣print(a);␣␣// "6" (also refers to 'a' on the third line)
}
print(a);␣// "42" (refers to 'a' on the first line)

2.12. Lifetime. Variables also have a lifetime, the period of time when it is in
a valid state and it can be used. For local (automatic) variables, the lifetime is the
same as the scope. For example:
int␣*ptr;
{
␣␣␣␣int␣a␣=␣5;␣// the lifetime of 'a' starts here
␣␣␣␣ptr␣=␣&a;␣␣// 'ptr' now points to 'a'
}␣// the lifetime of 'a' ends here
int␣b␣=␣*ptr;␣␣// bug: you're trying to read the value of 'a',
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// but its lifetime has ended, the variable 'a' no
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// longer exists, so you are not allowed to access
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// it through the pointer 'ptr',

When the pointer ptr in the previous example no longer points to a valid variable,
it is said to be dangling.
An exception are static local variables: their lifetime doesn’t end when their scope
ends, their value is retained across function calls:
unsigned␣int␣count()␣{
␣␣␣␣static␣unsigned␣int␣counter␣=␣0;␣␣// 'counter' is initialized
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// once, the first time the
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// function is called
␣␣␣␣counter++;
␣␣␣␣return␣counter;
}␣// scope of 'counter' ends here, its lifetime doesn't

print(count());␣␣// "1"
print(count());␣␣// "2"
print(count());␣␣// "3"
3. FUNCTIONS 103

3. Functions

A function is a named piece of code that accepts zero or more arguments as inputs
and optionally produces a result as an output. Functions are called using the function
call operator (()). For example, sqrt(16) is a function call expression that calls
the function named sqrt with the argument 16. When evaluated, the result of this
expression is 4.0. When a function is called, values from the scope of the caller are
passed to the callee (the function being called) as arguments, the code in the body of
the function is executed (with access to these arguments), and the result is returned
to the caller, where it is available as the result of the function call expression.

3.1. Declaration vs Definition. Functions have a declaration and a defini-


tion. The following example first declares and then defines a function with the name
“get pi” that takes no arguments and returns a value of type float.
// Declaration / prototype
float␣get_pi();

// Definition / implementation
float␣get_pi()␣{
␣␣␣␣return␣3.14159265;
}

The declaration (also called the prototype) contains all of the information necessary
to use (call) the function: its name, the number and types of arguments it takes,
and the type it returns.
The definition provides an implementation for the function, containing the actual
code that is executed when the function is called. The code between the curly braces
({}) of the implementation is called the function body.
To call a function, at least a declaration must be available somewhere higher up in
the source file. For example, consider these three cases:
float␣get_pi()␣{␣// declaration and definition
␣␣␣␣return␣3.14159265;
}

float␣get_tau()␣{
␣␣␣␣return␣2␣*␣get_pi();␣// works: declaration (and definition)
}␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// of get_pi are available

If a function wasn’t declared yet, a definition also serves as a declaration, because


it also contains all information necessary to perform a function call.
float␣get_tau()␣{
␣␣␣␣return␣2␣*␣get_pi();␣// error: get_pi was not declared
}

float␣get_pi()␣{
␣␣␣␣return␣3.14159265;
}

float␣get_pi();␣// declaration

float␣get_tau()␣{
3. FUNCTIONS 104

␣␣␣␣return␣2␣*␣get_pi();␣// works: declaration is available


}

float␣get_pi()␣{␣// definition
␣␣␣␣return␣3.14159265;
}

3.2. Function Arguments. Functions can also take arguments. To create a


function that takes arguments, just add one or more parameters to its declaration.
Each parameter specifies the type of the argument that can be passed, and a name
that can be used to refer to these values inside of the function definition. For
example:
// Define a function that computes the logarithm
// base b of the value x.
float␣log_base(float␣x,␣float␣b)␣{
␣␣␣␣return␣std::log2(x)␣/␣std::log2(b);
}

// Call the function with arguments x=1000 and b=10,


// and print the result.
print(log_base(1000,␣10));␣// "3"

This example relies on the C++ standard library function std::log2 that is included
with your compiler.

3.2.1. Passing arguments by value. In the previous example, the arguments x


and b are passed by value. Passing by value means that a copy of the argument is
created, and this copy is used inside of the function. This means that if you change
the value of x or b, the original variable in the calling code will not be changed.

3.2.2. Passing arguments by reference. Another way to pass arguments to func-


tions is passing by reference. It means that (a reference to) the original variable
is used in the function, without creating a copy.
For example, changing i inside of the following increment function will affect the
variable being passed into the function because the argument is passed by reference.
On the other hand, the argument to badIncrement is passed by value, so inside of
the function you only have access to a local copy of the value that was passed in.
/// Adds one to the given integer.
void␣increment(int␣&i)␣{
␣␣␣␣i␣=␣i␣+␣1;
}

/// Fails to add one to the given integer.


void␣badIncrement(int␣i)␣{␣␣// 'i' is a local copy
␣␣␣␣i␣=␣i␣+␣1;␣// Local copy 'i' is incremented, not the original
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// integer in the calling code.
}

int␣myInt␣=␣3;
increment(myInt);
print(myInt);␣␣␣␣␣␣␣␣// "4"
increment(myInt);
3. FUNCTIONS 105

print(myInt);␣␣␣␣␣␣␣␣// "5"
badIncrement(myInt);
print(myInt)␣␣␣␣␣␣␣␣␣// "5" (still!)

Some objects are expensive to copy, e.g. long strings or big matrices. Passing
these objects by value each time would be expensive, so they are often passed by
reference-to-const instead to avoid the copy. Inside of the function, the argument
is simply an alias of the variable owned by the callee, it doesn’t have to be copied.
As explained in §2.2, the keyword const indicates that the function cannot modify
the target of the reference. This is a promise you make towards the user of your
function, saying “even though I accept this argument by reference, I will not attempt
to modify it”. Const correctness is very important, it helps you to catch bugs and
makes your code easier to understand. If your function accepts an argument by
reference and if it doesn’t intend to modify that argument, the argument should be
marked const, without exception.
For example, the following function accepts a string by reference-to-const to count
the occurrences of the given character. Since you don’t modify the string to count
characters, the argument should be const.
ptrdiff_t␣count_occurrences(const␣std::string␣&str,␣char␣c)␣{
␣␣␣␣return␣std::count(str.begin(),␣str.end(),␣c);
}

std::string␣msg␣=␣"The␣eagle␣has␣landed!";
print(count_occurrences(msg,␣'a'));␣// "3"

Note that even though the msg variable is defined as non-const, you can call the
count occurrences that accepts the string as a reference-to-const. This is the
expected behavior, as explained in §2.2.
The opposite would be problematic:
// non−const−correct
ptrdiff_t␣count_occurrences(std::string␣&str,␣char␣c)␣{
␣␣␣␣return␣std::count(str.begin(),␣str.end(),␣c);
}

const␣std::string␣msg␣=␣"The␣eagle␣has␣landed!";
print(count_occurrences(msg,␣'a'));␣// error: binding reference
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// of type 'std::string&' to
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// 'const string' discards
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// qualifiers

Tip 12.3.1

Try to practice const correctness from the very beginning, it will make your
code easier to reason about and will force you to think about your design more
carefully. Fixing const correctness in an existing non-const-correct code base
can be quite tedious, so it’s better to do it right the first time.

3.2.3. Passing arguments as pointers. Lastly, you can pass a pointer to a vari-
able to the function. This does basically the same thing as passing by reference,
3. FUNCTIONS 106

but it can make the code a little harder to read, since you must always dereference
the pointer before using it. An advantage is that pointers can be null, so they can
be used for optional arguments. When accepting a pointer as an argument and
you don’t intend to modify the pointee, the argument should be a pointer-to-const,
similar to the const correctness we discussed for references.

Here’s a summary:
void␣f(int␣␣x);␣// local copy, original value can't be changed
void␣f(int␣&x);␣// no copy, original value can be changed
void␣f(int␣*x);␣// no copy, original value can be changed
void␣f(const␣int␣&x);␣// no copy, original value can't be changed
void␣f(const␣int␣*x);␣// no copy, original value can't be changed

3.3. Default arguments. The declaration of a function can specify default


values for the rightmost arguments, using an equal sign after the argument name,
followed by the default value. If the function is called without specifying a value for
these arguments, the default value is used:
void␣default_example(int␣a,␣int␣b␣=␣42,␣int␣c␣=␣-1)␣{
␣␣␣␣print("a␣=",␣a,␣"b␣=",␣b,␣"c␣=",␣c);
}

default_example(1,␣2,␣3);␣// prints "a = 1 b = 2 c = 3"


default_example(1,␣2);␣␣␣␣// prints "a = 1 b = 2 c = −1"
default_example(1);␣␣␣␣␣␣␣// prints "a = 1 b = 42 c = −1"

3.4. Return Values. As shown above, the first part of the function signature
is the return type, this is type of the value that is returned by the function. A
special case is the return type void, indicating that the function does not return
any values.
Inside of the function definition, the return value is set using the return keyword,
and this also transfers control back to the caller. One function can contain multiple
return statements in different paths of execution. For example:
// Division that returns zero if the divisor is zero.
int␣safe_division(int␣dividend,␣int␣divisor)␣{
␣␣␣␣if␣(divisor␣==␣0)
␣␣␣␣␣␣␣␣return␣0;
␣␣␣␣return␣dividend␣/␣divisor;
}

It is not possible to return multiple values like in Matlab. To get around this, you
can return a struct or a tuple. This is explained in section §6.2.
Return types of functions can be deduced using the auto keyword (§2.10):
// the return type of 'times_two' is 'std::complex<double>'
auto␣times_two(std::complex<double>␣c)␣{
␣␣␣␣return␣c␣*␣2.;
}
4. OPERATORS 107

3.5. Function Overloading. You can have multiple functions with the same
name, as long as they have different types or numbers of parameters. The compiler
will then make sure that the right function gets called, depending on the types of
the arguments you pass to it. This is called function overloading.
void␣myFunction(int␣i)␣{
␣␣␣␣print("int",␣i);
}

void␣myFunction(int␣i,␣int␣j)␣{
␣␣␣␣print("two␣ints",␣i,␣j);
}

void␣myFunction(float␣f)␣{
␣␣␣␣print("float",␣f);
}

myFunction(42);␣␣␣␣// prints "int 42"


myFunction(1,␣2);␣␣// prints "two ints 1 2"
myFunction(3.14);␣␣// prints "float 3.14"

3.6. Operator Overloading. In C++, operators like +, -, ==, etc. are just
functions with a special name, so you can provide your own overloads if you want to.
The signature of the operator functions is return type operator⋆(type1 arg1,
type2 arg2), where ⋆ is the operator you want to overload. The language already
has a set of overloads of these operator functions for the different built-in types,
because adding integers is different than adding complex numbers, for example, so
1 + 1 has to execute a different addition function than 0.5 + 0.5i.
As an example, if you want to overload the addition operator for your custom Vec3f
type (a vector of three floats), you could do the following:
Vec3f␣operator+(Vec3f␣a,␣Vec3f␣b)␣{
␣␣␣␣return␣{
␣␣␣␣␣␣␣␣a.x␣+␣b.x,
␣␣␣␣␣␣␣␣a.y␣+␣b.y,
␣␣␣␣␣␣␣␣a.z␣+␣b.z,
␣␣␣␣};
}

More information on operators and a list of operators you can overload can be found
on https://en.cppreference.com/w/cpp/language/operators.

4. Operators

Most operators in C++ are fairly straightforward, but some of them might be a bit
surprising when coming from languages like Matlab or Python.

+a : unary plus sign


-a : unary minus sign

a + b : addition
a - b : subtraction
a * b : multiplication
4. OPERATORS 108

a / b : division
a % b : remainder
Note that division of integers results in another integer, with the quotient
truncated towards zero, e.g. 3 / 2 == 1 != 1.5 4. If one of the operands
is a floating point number, the quotient will also be a floating point number,
e.g. 3.0 / 2 == 1.5.
There is no operator for exponentiation.

a << b : left bitshift


a >> b : right bitshift

∼a : bitwise not
a & b : bitwise and
a | b : bitwise or
a ^ b : bitwise exclusive or

!a : logical not
a && b : logical and
a || b : logical or
The logical and and or operators have shortcutting behavior, which means
that in a && b, b is not evaluated if a is false, and in a || b, b is not
evaluated if a is true.

a == b : equal to
a != b : not equal to
a < b : less than
a <= b : less than or equal to
a > b : greater than
a >= b : greater than or equal to

++a : prefix increment


--a : prefix decrement
a++ : postfix increment
a-- : postfix decrement
The prefix increment (decrement) operator increments (decrements) the
given variable and then returns the new value.
The postfix increment (decrement) operator increments (decrements) the
given variable and then returns the old value.

a ? b : c : conditional operator
If a is true, a ? b : c evaluates to b, otherwise it evaluates to c.

The language also includes compound assignment operators that combine the
assignment operator with one of the operators above, for example, a += b is equiv-
alent to a = a + b.

4This truncation towards zero is not the same as the flooring division in Python:
in C++, -3 / 2 == -1, but in Python -3 // 2 == -2.
5. CONTROL STRUCTURES 109

The comma operator is a curious remnant from C: in the expression a, b, the


result of a is discarded and the result of b is returned. Even though its result is
discarded, the expression a is evaluated, so any assignments or other side effects of
the evaluation will be executed. Useful applications of this operator are rare, you can
usually avoid it by separating the two operands into two independent statements.

4.1. Operator precedence. The precedence (order of operations) of the arith-


metic operators is as expected: parenthesized expressions first, then unary plus and
minus, followed by multiplication and division, and finally addition and subtraction.
The bitwise operators can sometimes lead to surprising results, for example,
value & mask == check is evaluated as value & (mask == check) which is al-
most definitely a bug. Always add parentheses if you’re unsure about the prece-
dence, e.g. (value & mask) == check.
For a complete overview of the C++ operators and their precedence, see
https://en.cppreference.com/w/cpp/language/operator precedence.

5. Control structures

5.1. If. An if statement evaluates one of two branches, based on the value of
a (parenthesized) condition:
if␣(<condition>)
␣␣␣␣<true-branch-statement>
else
␣␣␣␣<false-branch-statement>

The else clause is optional.


For example, the following function returns the largest of two numbers:
float␣max(float␣a,␣float␣b)␣{
␣␣␣␣if␣(a␣>␣b)
␣␣␣␣␣␣␣␣return␣a;
␣␣␣␣else
␣␣␣␣␣␣␣␣return␣b;
}

If a branch consists of multiple statements, you can use a compound statement ({})
to group them:
float␣max(float␣a,␣float␣b)␣{
␣␣␣␣if␣(a␣>␣b)␣{
␣␣␣␣␣␣␣␣print("b␣is␣smaller");
␣␣␣␣␣␣␣␣return␣a;
␣␣␣␣}␣else␣{
␣␣␣␣␣␣␣␣print("a␣is␣smaller");
␣␣␣␣␣␣␣␣return␣b;
␣␣␣␣}
}

There is no elif keyword, but the else branch can be another if statement:
int␣compare(int␣a,␣int␣b)␣{
␣␣␣␣if␣(a␣>␣b)
␣␣␣␣␣␣␣␣return␣1;
5. CONTROL STRUCTURES 110

␣␣␣␣else␣if␣(a␣<␣b)
␣␣␣␣␣␣␣␣return␣-1;
␣␣␣␣else␣// a == b
␣␣␣␣␣␣␣␣return␣0;
}

5.2. While. A while loop repeatedly executes a statement as long as the (paren-
thesized) condition evaluates to true. The condition is checked before executing the
body of the loop.
while␣(<condition>)
␣␣␣␣<loop-body>

For example, the following function uses a while loop to compute n!:
unsigned␣long␣long␣factorial(unsigned␣n)␣{
␣␣␣␣unsigned␣long␣long␣result␣=␣1;
␣␣␣␣while␣(n␣>␣1)␣{
␣␣␣␣␣␣␣␣result␣*=␣n;
␣␣␣␣␣␣␣␣--n;
␣␣␣␣}
␣␣␣␣return␣result;
}

5.3. Do-While. A do-while loop is similar to a normal while loop, but the
condition is checked after executing the body of the loop. This means that the loop
body will execute at least once.
do
␣␣␣␣<loop-body>
while␣(<condition>);

For example, the following function repeatedly tries to send a message. As long as
sending the message fails, it tries again, repeating the body until it succeeds:
void␣send_with_retry(const␣std::string␣&message)␣{
␣␣␣␣bool␣success;
␣␣␣␣do␣{
␣␣␣␣␣␣␣␣success␣=␣try_send(message);
␣␣␣␣}␣while␣(!success);
}

The curly braces around the loop body in the previous snippet could be omitted
because it consists of just a single statement.

5.4. For. A for loop is syntactic sugar for a while loop. First the initialization
is executed, then the condition is checked, if the condition evaluates to true, the
loop body is executed, and the iteration expression is evaluated. The condition is
checked again, repeating the body and the iteration expression until the condition
evaluates to false.
for␣(<initialization>;␣<condition>;␣<iteration>)
␣␣␣␣<loop-body>

The for loop above is roughly equivalent to the following while loop:
5. CONTROL STRUCTURES 111

<initialization>;
while␣(<condition>)␣{
␣␣␣␣<loop-body>
␣␣␣␣<iteration>;
}

A common application of for loops is iterating over a range of integers. For example:
unsigned␣sum_of_integers_to(unsigned␣n)␣{
␣␣␣␣unsigned␣sum␣=␣0;
␣␣␣␣for␣(unsigned␣i␣=␣1;␣i␣<=␣n;␣++i)
␣␣␣␣␣␣␣␣sum␣+=␣i;
␣␣␣␣return␣sum;
}
print(sum_of_integers_to(100));␣// "5050"

It can also be used to iterate over the elements of an array:


std::array␣arr␣{10,␣20,␣30};
for␣(size_t␣i␣=␣0;␣i␣<␣arr.size();␣++i)
␣␣␣␣print(arr[i]);
// prints "10" "20" "30"

Manually iterating over indices like this is verbose and error-prone, so to iterate over
containers, range-based for loops are usually preferred.

5.5. Range-For. A range-based for loop executes the body for every element
in a range. This range is often a container like an array or a string, but it can
be much more general, and you can define custom iteration behavior for your own
user-defined types. The syntax of a range-based for loop is similar to a normal for
loop:
for␣(<range-declaration>␣:␣<range-expression>)
␣␣␣␣<loop-body>

The <range-declaration> declares a loop variable that will be initialized to an


element of the range on each iteration. The <range-expression> is the range to
be iterated over.
For example, to iterate over an array like in the previous snippet, you could use:
std::array␣arr␣{10,␣20,␣30};
for␣(int␣element␣:␣arr)
␣␣␣␣print(element);
// prints "10" "20" "30"

In the example above, the element loop variable will be a copy of each element of
the array. You can also declare element as a reference instead, this is very similar
to how function arguments can be passed by value or by reference. In the following
example, each element is accessed by reference, so this reference can be used to write
to the actual array, e.g. to set all elements in the array to zero:
std::array␣arr␣{10,␣20,␣30};
for␣(int␣&element␣:␣arr)
␣␣␣␣element␣=␣0;
print(arr␣==␣std::array␣{0,␣0,␣0});␣// "true"
5. CONTROL STRUCTURES 112

Similarly to function arguments, you can use a reference-to-const to prevent expen-


sive copies:
std::array<std::string,␣3>␣arr␣{"These",␣"could␣be",␣"very␣long"};
for␣(const␣std::string␣&element␣:␣arr)
␣␣␣␣print(element);
// prints "These" "could be" "very long"

In many cases it is easiest to let the compiler infer the type for you, by using the
auto keyword:
std::array<std::string,␣3>␣arr␣{"These",␣"could␣be",␣"very␣long"};
for␣(const␣auto␣&element␣:␣arr)
␣␣␣␣print(element);
// prints "These" "could be" "very long"

Tip 12.5.1

If you’re familiar with Python, note how C++’s range-based for loop is very
similar to Python’s for ... in loop:
C++
std::array␣arr␣{10,␣20,␣30};
for␣(int␣element␣:␣arr)
␣␣␣␣print(element);
// prints "10" "20" "30"

Python
arr␣=␣[10,␣20,␣30]
for␣element␣in␣arr:
␣␣␣␣print(element)
# prints "10" "20" "30"

5.6. Switch. A switch statement is a generalization of an if statement: it


checks the value of the given integer condition expression and jumps to the case
label that corresponds to that value. The code after the label is executed until a
break keyword is encountered. If the condition value doesn’t match any of the case
labels, the switch jumps to the default label.
switch␣(<condition>)␣{
␣␣␣␣case␣<value-1>:
␣␣␣␣␣␣␣␣<statements-1>
␣␣␣␣case␣<value-2>:
␣␣␣␣␣␣␣␣<statements-2>
␣␣␣␣<...>
␣␣␣␣default:
␣␣␣␣␣␣␣␣<statements-default>
}

For example:
6. STRUCTS & CLASSES 113

void␣switch_example(int␣conditionvalue)␣{
␣␣␣␣switch␣(conditionvalue)␣{
␣␣␣␣␣␣␣␣case␣0:
␣␣␣␣␣␣␣␣␣␣␣␣print("condition␣is␣zero");
␣␣␣␣␣␣␣␣␣␣␣␣break;
␣␣␣␣␣␣␣␣case␣1:
␣␣␣␣␣␣␣␣␣␣␣␣print("condition␣is␣one");
␣␣␣␣␣␣␣␣␣␣␣␣break;
␣␣␣␣␣␣␣␣case␣6:
␣␣␣␣␣␣␣␣␣␣␣␣print("condition␣is␣six");
␣␣␣␣␣␣␣␣␣␣␣␣break;
␣␣␣␣␣␣␣␣default:
␣␣␣␣␣␣␣␣␣␣␣␣print("unknown␣condition");
␣␣␣␣}
}

switch_example(0);␣␣␣␣// prints "condition is zero"


switch_example(1);␣␣␣␣// prints "condition is one"
switch_example(2);␣␣␣␣// prints "unknown condition"
switch_example(6);␣␣␣␣// prints "condition is six"
switch_example(-999);␣// prints "unknown condition"

An interesting property of switches is that if you omit the break keyword, the execu-
tion just continues in the code below any following labels. This is called fallthrough.
void␣switch_example(int␣conditionvalue)␣{
␣␣␣␣switch␣(conditionvalue)␣{
␣␣␣␣␣␣␣␣case␣0:␣␣print("case␣0");␣␣[[fallthrough]];
␣␣␣␣␣␣␣␣case␣1:␣␣print("case␣1");␣␣[[fallthrough]];
␣␣␣␣␣␣␣␣default:␣print("default");␣[[fallthrough]];
␣␣␣␣␣␣␣␣case␣6:␣␣print("case␣6");␣␣break;
␣␣␣␣␣␣␣␣case␣7:␣␣print("case␣7");␣␣break;
␣␣␣␣}
}

switch_example(0);␣// prints "case 0" "case 1" "default" "case 6"


switch_example(1);␣// prints "case 1" "default" "case 6"
switch_example(2);␣// prints "default" "case 6"
switch_example(6);␣// prints "case 6"
switch_example(7);␣// prints "case 7"
switch_example(8);␣// prints "default" "case 6"

Since forgetting to add a break (which implicitly results in fallthrough) is a common


bug, it is highly recommend to annotate your switches with the [[fallthrough]]
attribute if you intend the case to fall through into the next. If you then enable
the -Wextra or -Wimplicit-fallthrough warning options of your compiler, it will
warn you if you forget to add break; or [[fallthrough]]; to your switch cases.

6. Structs & Classes

6.1. Structs. A data structure or struct for short is a group of data elements
grouped together in a single record. These data elements, known as data members
or member variables, can have different types. Data structures can be declared using
the following syntax:
6. STRUCTS & CLASSES 114

// Define a struct with type name 'Person' and 3 member variables:


struct␣Person␣{
␣␣␣␣std::string␣firstName;
␣␣␣␣std::string␣lastName;
␣␣␣␣int␣␣␣␣␣␣␣␣␣age;
};

An instance of the struct can be created using aggregate initialization:


// Create an instance of the struct type 'Person', with the
// variable name 'p' and its three members initialized to the
// given values:
Person␣p␣{"John",␣"Doe",␣42};

Compare this syntax to the float a {1.2}; and std::complex c {0.5, -0.5};
initializations from §2.2.
The member variables of this instance can be accessed using the member access
operator (.):
p.firstName␣=␣"Jane";
p.age␣=␣41;
print(p.firstName);␣// "Jane"

6.2. Member functions. Structs and classes can be used to combine data
and behavior, variables and functions. Member functions of a struct or class have
a similar syntax to the free functions we covered in §3, the main difference is that
member functions are always called on an instance, and all members of that instance
are accessible from within the function through the special this pointer that points
to the instance. Since this is a pointer, members are accessed using the -> operator
instead of the . operator.
struct␣Person␣{
␣␣␣␣std::string␣firstName;
␣␣␣␣std::string␣lastName;
␣␣␣␣int␣␣␣␣␣␣␣␣␣age;

␣␣␣␣std::string␣getFullName()␣const␣{
␣␣␣␣␣␣␣␣return␣this->firstName␣+␣"␣"␣+␣this->lastName;
␣␣␣␣}
};

Member functions are called on an instance using the same member access operator
(.) as data members:
Person␣p␣{"John",␣"Doe",␣42};
print(p.getFullName());␣// "John Doe"

If there is no ambiguity with local variables, the this-> in front of member variables
and functions inside of a member function is implicit and can be left out to improve
readability:
struct␣Person␣{
␣␣␣␣// [...]
␣␣␣␣std::string␣getFullName()␣const␣{
␣␣␣␣␣␣␣␣return␣firstName␣+␣"␣"␣+␣lastName;
6. STRUCTS & CLASSES 115

␣␣␣␣}
};

6.3. Classes. Classes and structs are very similar, the only difference is the
default access specifier of the members 5. Access specifiers will be covered in the
next section.
The syntax for classes is exactly the same as for structs, just with the keyword class
instead of struct:
class␣ClassName␣{
␣␣␣␣int␣membervar;
␣␣␣␣float␣memberfun()␣{␣/* ... */␣}
};

6.4. Access specifiers. An important principle in OOP is encapsulation, where


access to some data is restricted. In C++, encapsulation can be achieved using the
access specifiers public, private and protected. Public members of a struct/class
can be accessed without restrictions from inside and outside of the struct/class,
whereas private members are only accessible inside of the struct/class itself 6. Pro-
tected members are similar to private members, but they are also accessible by
structs/classes that inherit from the current struct/class. Inheritance and OOP are
beyond the scope of this tutorial, so we won’t go into the details here.
The default access specifier for structs is public, that explains why we were able
to access the firstName and getFullName() members of the Person struct in the
previous examples. We could make this explicit by adding the access specifier to the
struct definition:
struct␣Person␣{
␣␣public:
␣␣␣␣std::string␣firstName;
␣␣␣␣std::string␣lastName;
␣␣␣␣int␣␣␣␣␣␣␣␣␣age;

␣␣␣␣std::string␣getFullName()␣const␣{
␣␣␣␣␣␣␣␣return␣this->firstName␣+␣"␣"␣+␣this->lastName;
␣␣␣␣}
};

All members declared below the public: specifier and before the next specifier will
be publicly accessible.
Private member variables are often used to enforce some kind of class invariant.
As an example, consider a struct representing a car: a possible invariant of a car
could be that the current speed is below the maximum speed. If the speed variable
were a public member, a user of the struct could set the speed to any value, possibly
violating the invariant. A common approach to prevent this is to define public getter
and setter functions that access a private member variable:
5And of the base classes when using inheritance.
6Or by friend functions and structs/classes:
https://en.cppreference.com/w/cpp/language/friend.
6. STRUCTS & CLASSES 116

const␣float␣maxspeed␣=␣160;
class␣Car␣{
␣␣private:␣// optional
␣␣␣␣float␣speed;

␣␣public:
␣␣␣␣void␣setSpeed(float␣newspeed)␣{
␣␣␣␣␣␣␣␣if␣(newspeed␣>␣maxspeed)
␣␣␣␣␣␣␣␣␣␣␣␣throw␣std::out_of_range("invalid␣speed");␣// error
␣␣␣␣␣␣␣␣speed␣=␣newspeed;
␣␣␣␣}

␣␣␣␣float␣getSpeed()␣const␣{
␣␣␣␣␣␣␣␣return␣speed;
␣␣␣␣}
};

6.5. Constructors. A constructor is a special function that creates new in-


stances of a struct/class. It is defined as a member function with the same name as
the struct/class and without a return type. Unlike normal functions, constructors
cannot be called explicitly, they are called automatically when a new instance of a
class is created, e.g. when initializing a variable.
The Car class from the previous section has an issue: since the speed member is
private, we can no longer initialize it using aggregate initialization like we did with
the Person struct with public members earlier:
// Try to create a car and initialize the speed to 30:
Car␣c␣{30};␣// error: no matching constructor for
␣␣␣␣␣␣␣␣␣␣␣␣// initialization of 'Car'
␣␣␣␣␣␣␣␣␣␣␣␣// (because Car::speed is private)

If we want to initialize a car with a given speed, we’ll have to write a constructor
that accepts the speed as an argument:
class␣Car␣{
␣␣private:
␣␣␣␣float␣speed;

␣␣public:
␣␣␣␣Car(float␣initialspeed)␣{␣// constructor
␣␣␣␣␣␣␣␣setSpeed(initialspeed);
␣␣␣␣}

␣␣␣␣void␣setSpeed(float␣newspeed)␣{
␣␣␣␣␣␣␣␣if␣(newspeed␣>␣maxspeed)
␣␣␣␣␣␣␣␣␣␣␣␣throw␣std::out_of_range("invalid␣speed");␣// error
␣␣␣␣␣␣␣␣speed␣=␣newspeed;
␣␣␣␣}

␣␣␣␣float␣getSpeed()␣const␣{
␣␣␣␣␣␣␣␣return␣speed;
␣␣␣␣}
};

Car␣c␣{30};␣// ok: implicitly calls the constructor Car::Car(30)


6. STRUCTS & CLASSES 117

print(c.getSpeed());␣// "30"

6.5.1. Special constructors and other special member functions. A first special
constructor is the default constructor: it is a constructor that doesn’t take any
arguments. In the Car example, we might decide to initialize the speed to zero if it
is not specified:
class␣Car␣{
␣␣private:
␣␣␣␣float␣speed;

␣␣public:
␣␣␣␣Car()␣{
␣␣␣␣␣␣␣␣setSpeed(0);
␣␣␣␣}

␣␣␣␣Car(float␣initialspeed)␣{
␣␣␣␣␣␣␣␣setSpeed(initialspeed);
␣␣␣␣}

␣␣␣␣// [...]
};

Car␣c␣{};␣// ok: implicitly calls the constructor Car::Car()


print(c.getSpeed());␣// "0"

In the previous snippet there are two constructors with different numbers of argu-
ments. This is not an issue, because constructors can be overloaded just like normal
functions.
Other types of special constructors are the copy constructor and the move construc-
tor, often implemented in conjunction with the copy and move assignment operators.
These special member functions allow you to define custom behavior for copying or
moving instances of the class. You don’t need to implement these for basic classes
because the compiler will implicitly create default implementations for you. If you
do find yourself in need of a non-trivial copy/move constructor/assignment operator,
make sure you abide by the rule of three/five/zero.

6.5.2. Delegating constructors. In the previous snippet, both constructors do


essentially the same thing, just with a different value for the initial speed. To prevent
code duplication, we could use a delegating constructor. A delegating constructor
specifies another constructor (the one that actually does the work) after a colon and
before the (often empty) curly braces of the function body:
class␣Car␣{
␣␣private:
␣␣␣␣float␣speed;

␣␣public:
␣␣␣␣Car()␣:␣Car{0}␣{}

␣␣␣␣Car(float␣initialspeed)␣{
␣␣␣␣␣␣␣␣setSpeed(initialspeed);
␣␣␣␣}
6. STRUCTS & CLASSES 118

␣␣␣␣// [...]
};

6.5.3. Member initializer lists. Constructors often just use their arguments to
initialize member variables. Member initializer lists provide a clean syntax to do
this 7.
Consider a simplified struct for representing quaternions. You might want a default
constructor that creates an identity quaternion, as well as a constructor that takes
four values to initialize the four components of the quaternion:
struct␣Quaternion␣{
␣␣␣␣float␣w;
␣␣␣␣float␣x;
␣␣␣␣float␣y;
␣␣␣␣float␣z;

␣␣␣␣Quaternion()␣{
␣␣␣␣␣␣␣␣w␣=␣1;
␣␣␣␣␣␣␣␣x␣=␣0;
␣␣␣␣␣␣␣␣y␣=␣0;
␣␣␣␣␣␣␣␣z␣=␣0;
␣␣␣␣}

␣␣␣␣Quaternion(float␣init_w,␣float␣init_x,
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣float␣init_y,␣float␣init_z)␣{
␣␣␣␣␣␣␣␣w␣=␣init_w;
␣␣␣␣␣␣␣␣x␣=␣init_x;
␣␣␣␣␣␣␣␣y␣=␣init_y;
␣␣␣␣␣␣␣␣z␣=␣init_z;
␣␣␣␣}
};

Quaternion␣q1␣{};
// q1.w == 1, q1.x == q1.y == q1.z == 0
Quaternion␣q2␣{0.1,␣0.2,␣0.3,␣0.4};
// q2.w == 0.1, q2.x == 0.2, q1.y == 0.3, q1.z == 0.4

This can be simplified by using member initializer lists. These have syntax similar
to delegating constructors, but instead of the class name, the colon is now followed
by the names of the member variables to initialize, with the value to initialize the
member with between parentheses or curly braces (similar to the initialization of
normal variables in §2.2).
struct␣Quaternion␣{
␣␣␣␣float␣w;
␣␣␣␣float␣x;
␣␣␣␣float␣y;
␣␣␣␣float␣z;

␣␣␣␣Quaternion()␣:␣w{1},␣x{0},␣y{0},␣z{0}␣{}

␣␣␣␣Quaternion(float␣init_w,␣float␣init_x,

7Member initializer lists can also improve performance by avoiding unnecessary copies, and
there are situations where you have to use them, e.g. when initializing member variables that are
references or types without a default constructor.
6. STRUCTS & CLASSES 119

␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣float␣init_y,␣float␣init_z)
␣␣␣␣␣␣:␣w{init_w},␣x{init_x},␣y{init_y},␣z{init_z}␣{}
};

You can simplify it even further by using the same name for the constructor argu-
ments as for the member variables:
␣␣␣␣Quaternion(float␣w,␣float␣x,␣float␣y,␣float␣z)
␣␣␣␣␣␣:␣w{w},␣x{x},␣y{y},␣z{z}␣{}

Even though there are two variables with the same name (the constructor arguments
and the member variables), there is no ambiguity: the member initializer list syntax
w{w} means “initialize the member variable w with the value of the argument w”.

6.5.4. Default member initializers. Member variables can be given default val-
ues that will be used to initialize them if they are not explicitly initialized in a
constructor’s member initializer list. This default value is called a default member
initializer.
Consider the following struct:
struct␣S␣{
␣␣␣␣int␣a;
␣␣␣␣int␣b;
␣␣␣␣int␣c;
␣␣␣␣S(int␣a)␣:␣a{a},␣b{-1},␣c{-2}␣{}
␣␣␣␣S(int␣a,␣int␣b)␣:␣a{a},␣b{b},␣c{-2}␣{}
␣␣␣␣S(int␣a,␣int␣b,␣int␣c)␣:␣a{a},␣b{b},␣c{c}␣{}
};

Notice how the default value of -2 for c occurs twice. If we ever want to change
it, we might forget to change it in both constructors. Instead, it’s a good idea to
specify the default values for the members as follows:
struct␣S␣{
␣␣␣␣int␣a;
␣␣␣␣int␣b␣=␣-1;␣// provide a default member initializer
␣␣␣␣int␣c␣=␣-2;
␣␣␣␣S(int␣a)␣:␣a{a}␣{}
␣␣␣␣S(int␣a,␣int␣b)␣:␣a{a},␣b{b}␣{}
␣␣␣␣S(int␣a,␣int␣b,␣int␣c)␣:␣a{a},␣b{b},␣c{c}␣{}
};

The member initializer list of the first constructor doesn’t include initializers for b
and c, so the compiler automatically initializes them to -1 and -2 respectively. The
second constructor does initialize b in its member initializer list, so this initializer is
used instead of the default value of -1.
Default values can be specified using the assignment operator or using curly braces
(similar to the initialization of normal variables in §2.2), but specifying the default
value using parentheses is not allowed. The following is equivalent to the previous
snippet (except for the fact that this also performs a check for narrowing conver-
sions):
struct␣S␣{
␣␣␣␣int␣a;
7. ENUMERATIONS 120

␣␣␣␣int␣b␣{-1};␣// provide a default member initializer


␣␣␣␣int␣c␣{-2};
␣␣␣␣S(int␣a)␣:␣a{a}␣{}
␣␣␣␣S(int␣a,␣int␣b)␣:␣a{a},␣b{b}␣{}
␣␣␣␣S(int␣a,␣int␣b,␣int␣c)␣:␣a{a},␣b{b},␣c{c}␣{}
};

Tip 12.6.1

The previous example is just to explain the concept of default values for member
variables. In a simple case like this it would be better to use default arguments
for the constructor:
struct␣S␣{
␣␣␣␣int␣a;
␣␣␣␣int␣b;
␣␣␣␣int␣c;
␣␣␣␣S(int␣a,␣int␣b␣=␣-1,␣int␣c␣=␣-2)
␣␣␣␣␣␣:␣a{a},␣b{b},␣c{c}␣{}
};

7. Enumerations

An enumeration is a type whose value is restricted to a discrete range of possible


values, represented by integers. Enumerations can be defined using the following
syntax.
enum␣class␣FlightMode␣{
␣␣␣␣Uninitialized␣=␣0,
␣␣␣␣Manual␣␣␣␣␣␣␣␣=␣1,
␣␣␣␣AltitudeHold␣␣=␣2,
␣␣␣␣Autonomous␣␣␣␣=␣3,
};

FlightMode␣mode␣=␣FlightMode::Autonomous;
if␣(mode␣==␣FlightMode::Manual)␣{
␣␣␣␣// [...]
}

The enumerator values 0, 1, ... are not required. They will automatically be num-
bered by the compiler if you don’t explicitly write them out. The first enumerator
has a default value of 0 if you don’t specify it, each enumerator will have the value
of the previous enumerator plus one.
Enumerations are often used in combination with switch statements (§5.6).
The keyword class is not required, but we recommend using it as it prevents clash-
ing enumerator names between enumerations, and it prevents accidental conversions
from and to integers for improved type safety.
enum␣NavigationState␣{
␣␣␣␣Idle,
␣␣␣␣TakingOff,
␣␣␣␣Flying,
8. INPUT/OUTPUT 121

␣␣␣␣Landing,
};

enum␣CryptoState␣{
␣␣␣␣Idle,␣␣␣␣␣␣␣// error: redefinition of enumerator 'Idle'
␣␣␣␣Waiting,
␣␣␣␣Decrypting,
␣␣␣␣Finished,
};

Adding class solves this problem:


enum␣class␣DroneState␣{
␣␣␣␣Idle,
␣␣␣␣TakingOff,
␣␣␣␣Flying,
␣␣␣␣Landing,
};

enum␣class␣CryptoState␣{
␣␣␣␣Idle,
␣␣␣␣Waiting,
␣␣␣␣Decrypting,
␣␣␣␣Finished,
};

8. Input/Output

8.1. Printing to the console. The print function we’ve used in the examples
so far doesn’t really exist as part of the language or the standard library (although
you could easily write it yourself if you wanted to). Instead, the idiomatic way to
print text to the console or read user input from the console is to use iostreams.
There are three main streams:

✓ std::cout: the standard output, prints to the console


✓ std::cerr: the standard error output, prints to the console (with a differ-
ent file descriptor), used for error messages
✓ std::cin: the standard input, to read input from the console

Printing and reading to or from iostreams is done using the stream insertion and
extraction operators, << and >>. For example, to print the string “Hello, world!” to
the standard output, you would use:
#include␣<iostream>

std::cout␣<<␣"Hello,␣world!";

You can chain these operators to print multiple things:


std::string␣firstname␣=␣"John"
std::string␣lastname␣␣=␣"Doe";
std::cout␣<<␣"Hello,␣"␣<<␣firstname␣<<␣'␣'␣<<␣lastname␣<<␣'!';
// "Hello, John Doe!"
8. INPUT/OUTPUT 122

The standard output is buffered, which means that the characters you write to
std::cout are not sent to the console immediately (this improves performance).
When looking at C++ code examples online, you will probably come across std::endl.
This is an output manipulator that writes a newline character and flushes the buffer:
std::cout␣<<␣"Hello,␣world!"␣<<␣std::endl;

You usually don’t need to flush the output after each line, doing so might reduce
the performance. Instead, you can just use the newline character (‘\n’) to end the
line without flushing the buffer:
std::cout␣<<␣"Hello,␣world!\n";

There are many other input/output manipulators, to configure the number of digits
of floating point numbers to print, to set the base (decimal, hexadecimal, etc.) for
printing integers, and so on. See https://en.cppreference.com/w/cpp/io/manip for
an overview.
The insertion and extraction operators are overloaded for many built-in data types,
so you can print integers, floats, std::strings, and so on. You can also overload
the operators for your own user-defined data types so they can easily be printed or
converted to a string representation. For example:
#include␣<ostream>

struct␣CustomData␣{
␣␣␣␣int␣i;
};

std::ostream␣&operator<<(std::ostream␣&os,␣CustomData␣cd)␣{
␣␣␣␣os␣<<␣"CustomData("␣<<␣cd.i␣<<␣")";
␣␣␣␣return␣os;
}

The arguments to the insertion operator are a reference to the stream to print to,
and the object to print. It should return a reference to the same output stream.

8.2. Stringstreams. Instead of printing to the console, you can also write to
a stringstream that can later be converted to a string once the entire message is
finished. For example:
#include␣<sstream>

std::string␣to_string(CustomData␣cd)␣{
␣␣␣␣std::ostringstream␣ss;
␣␣␣␣ss␣<<␣"The␣string␣representation␣is:␣'"␣<<␣cd␣<<␣"'";
␣␣␣␣return␣ss.string();
}

CustomData␣cd␣{42};
std::string␣cd_str␣=␣to_string(cd);
print(cd_str);␣// "The string representation is: 'CustomData(42)'"
9. COMPILATION 123

9. Compilation

The compiler converts your C++ source code into an executable binary, performing
many optimizations along the way.

9.1. Compilers. Popular compilers on Linux are GCC (the GNU Compiler
Collection) and Clang, both are open source. OSX has Apple Clang, which is Apple’s
flavor of Clang that’s included with their Xcode IDE. You can also install GCC using
homebrew. On Windows, the default compiler is the MSVC (Microsoft Visual C++)
compiler included in Microsoft’s Visual Studio IDE, but you can also use Clang or
GCC (e.g. through MinGW).
For this guide, we’ll use GCC on Linux. On Debian-based systems like Ubuntu, it
can be installed using:
sudo␣apt␣install␣g++

9.2. Hello, world! In this section, we’ll compile and run a simple “hello world”
example.
Open a text editor (e.g. Gedit or Visual Studio Code) and create a new file with the
name “hello.cpp”. The “.cpp” extension indicates that the file contains C++ source
code. Add the following content and save the file:
#include␣<iostream>

int␣main()␣{
␣␣␣␣std::cout␣<<␣"Hello,␣world!"␣<<␣std::endl;
}

The code is in a function called “main”. This function is special, because it is the
main entry point of your program, it will be called automatically when you execute
the program.
Next, open a terminal in the folder where you saved the file, and compile the program
using GCC:
g++␣hello.cpp␣-o␣hello

The -o option specifies the output filename. Next, execute the compiled program
using:
./hello

It should print the text “Hello, world!” to the console.

9.3. Useful compiler options.

9.3.1. Warnings. It is highly recommended to turn on compiler warnings. It will


help you to catch mistakes early. You can find a full overview of warning options on
https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html.
Let’s add an unused variable to “hello.cpp” so it will cause a warning:
9. COMPILATION 124

#include␣<iostream>

int␣main()␣{
␣␣␣␣int␣unused;
␣␣␣␣std::cout␣<<␣"Hello,␣world!"␣<<␣std::endl;
}

For most purposes, you should enable the -Wall and -Wextra options, for example:
g++␣hello.cpp␣-o␣hello␣-Wall␣-Wextra

The compiler should now warn you that the variable “unused” is not used anywhere:
hello.cpp:␣In␣function␣`int␣main()':
hello.cpp:4:9:␣warning:␣unused␣variable␣`unused'␣[-Wunused-variable]
␣␣␣␣4␣|␣␣␣␣␣int␣unused;
␣␣␣␣␣␣|␣␣␣␣␣␣␣␣␣^~~~~~

Many of the warnings enabled by these options hint at a serious issue with your
code, so it is a good idea to treat warnings as errors, using the -Werror option:
g++␣hello.cpp␣-o␣hello␣-Wall␣-Wextra␣-Werror

hello.cpp:␣In␣function␣`int␣main()':
hello.cpp:4:9:␣error:␣unused␣variable␣`unused'␣[-Werror=unused-variable]
␣␣␣␣4␣|␣␣␣␣␣int␣unused;
␣␣␣␣␣␣|␣␣␣␣␣␣␣␣␣^~~~~~
cc1plus:␣all␣warnings␣being␣treated␣as␣errors

Tip 12.9.1

Try to maintain a zero tolerance on warnings from the very beginning. This
allows you to catch mistakes early. The intersection of programs that both com-
pile successfully while also containing bugs is very large: use your compiler’s
diagnostics and warnings to move some programs from the “buggy but compil-
ing successfully” category to the “buggy and not compiling” category so you
can fix these bugs. Furthermore, no one wants to spend time fixing hundreds
of warnings in a code base that has been worked on for weeks or months, write
warning-free code from the start.
It is highly recommended to enable the -Werror flag from the start as well.

The warning for unused variables can be quite useful, for example, if a variable is
unused, this could be because you copy-and-pasted a piece of code and forgot to
rename some of the variables. During development, though, there will sometimes be
harmless unused variables in the (unfinished) function you’re working on. It would
be a bit annoying if these warnings were treated as errors. For this purpose, the
compiler has options to disable -Werror for specific warnings, while still treating
other warnings as errors. For example:
g++␣hello.cpp␣-o␣hello␣-Wall␣-Wextra␣-Werror
␣␣␣␣-Wno-error=unused-variable
9. COMPILATION 125

hello.cpp:␣In␣function␣`int␣main()':
hello.cpp:4:9:␣warning:␣unused␣variable␣`unused'␣[-Wunused-variable]
␣␣␣␣4␣|␣␣␣␣␣int␣unused;
␣␣␣␣␣␣|␣␣␣␣␣␣␣␣␣^~~~~~

9.3.2. C++ version. As mentioned in the introduction, there are different ver-
sions of the C++ standard. Different compilers use different default versions, so it’s
recommended to explicitly specify the version you need. The EAGLE Autopilot
project uses C++17, so we use the -std=c++17 flag:
g++␣hello.cpp␣-o␣hello␣-Wall␣-Wextra␣-Werror␣-std=c++17

9.3.3. Optimization. By default, compiler optimizations are disabled, resulting


in very slow and large binaries. The following options allow you to tweak the com-
piler’s optimization settings:

-O0 : No optimizations (default).


-O1 : Optimize.
-O2 : Optimize even more.
-O3 : Optimize yet more.
-Os : Like -O2 but it excludes optimizations that often increase the code size.
-Og : Optimize the debugging experience.
-Ofast : Like -O3 but breaks standard compliance.

Higher optimization levels require more memory during compilation and compilation
takes longer, but this is well worth the increase in performance you get at run time.
Usually -O3 is not much faster than -O2. You probably shouldn’t be using -Ofast,
especially not as a global option, as it can break valid C++ code that relies on certain
properties of floating point math, and the option might introduce data races into
otherwise race-free multithreading code.
For a detailed overview of these options, see
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html.

9.4. Header files and implementation files. C++ source code consists of
two main types of files: header files (with extension .hpp) and implementation
files (with extension .cpp). Header files contain the public API of libraries, such
as function declarations, class definitions, type aliases, etc. Header files are not
compiled on their own, they are included in implementation files. The implemen-
tation files contain the actual implementation, such as function definitions. Each
implementation file will be compiled separately.

9.5. The compilation process. The C++ compilation process consists of three
main stages:

(1) Preprocessing: simple text replacement preprocessing is applied to all


implementation files, preprocessor macros (#define) are expanded, sections
of code can be conditionally removed (#if, #ifdef), the content of header
files (#include) is pasted into the implementation files, and so on.
9. COMPILATION 126

(2) Compilation: each preprocessed implementation file is compiled inde-


pendently and is converted into an object file, a binary file that contains
(among other things) the machine code of the functions defined in the given
implementation file.
(3) Linking: the object files from all implementation files are combined, un-
defined symbols (functions, variables) are resolved by searching for their
definitions in other object files or external libraries. Finally all functions
and variables are laid out in memory as defined by the linker script. The
end result is an executable binary file.

Warning 12.9.1

You should never compile a header file directly, and you should never #include
an implementation file.

9.6. Header Files. To use the functions, classes or variables declared in a


header file, the #include preprocessor directive is used. As mentioned earlier, the
preprocessor only performs simple text replacement: when it encounters a #include
<filename> directive, it searches for that file, reads the contents, and pastes the
contents in the current file, replacing the original #include directive. This process
is repeated recursively, because header files can include other header files.
File names can be delimited by two kinds of brackets:
#include␣<SomeHeaderInTheIncludePath.hpp>
#include␣"SomeHeaderInTheSameFolder.hpp"

✓ Angle brackets (<>) are used for files that can be found in the include path
of the compiler, like standard library headers, system headers, and headers
of other libraries.
✓ Quotes ("") are used to include header files that are in the same directory
as the file that includes them.

Because one header can include many more headers, and these headers could also
include other headers, the dependency graphs can be rather large, and some headers
will inevitably be included twice. For example, the include graph for Quaternion.hpp
looks like this:

Quaternion.hpp

math/Vector.hpp math/Degrees.hpp

math/cmath

Notice how Quaternion.hpp includes the cmath file twice, once through Vector.hpp
and once through Degrees.hpp. Including the same header file twice is bad, because
then all of its contents are duplicated during preprocessing, and this can result in
9. COMPILATION 127

compilation errors. To get around this problem, you have to add the following line
to all of your header files to tell the preprocessor to only include the file once:
#pragma␣once

When this is added to the cmath file from the example above, the preprocessor will
do the following: it first includes Vector.hpp, then recursively include cmath. Next,
when it includes Degrees.hpp, it recursively visits cmath, but it knows that cmath
was already included in Quaternion.hpp before, and the #pragma once directive will
cause it to not include it a second time.

9.7. Implementation Files. Each implementation file is compiled separately


and the linker stitches all of the compiled object files together. This means that
if two separate implementation files include the same header file, the contents of
the header are compiled twice. If there are definitions in the header (e.g. global
variables or function implementations), there are now two compiled definitions of
the same variable or function. The final binary can only contain a single definition
of each symbol: the linker won’t know which one to use, and will throw a “multiple
definition” error.
If you get these kinds of errors, check that your header files only contain declarations
(or inline functions), not definitions.

9.8. Inline Functions. If you really want to have function definitions in a


header file (because it’s a very simple or short function, or for performance reasons),
you can use inline functions. For free functions, you add the inline keyword in
front of the function definition. For methods, you can just add the implementation
inside of the class definition, and they will be considered inline automatically.
#pragma␣once

inline␣int␣mySimpleFunction(int␣a)␣{
␣␣␣␣return␣2␣*␣a;
}

class␣C␣{
␣␣public:
␣␣␣␣void␣setC(int␣c)␣{␣this->c␣=␣c;␣}
␣␣␣␣int␣getC()␣const␣{␣return␣this->c;␣}

␣␣private:
␣␣␣␣int␣c␣=␣0;
};

Inline functions won’t cause a “multiple definition” error, the linker will just use one
of the (possibly many) implementations available in the object files. You do have to
make sure that all definitions are the same, otherwise you’re still in violation of the
one definition rule (ODR).

9.9. Example with multiple files. In the simple “hello world” example from
§9.2, there was only one file, and GCC’s g++ command handled all three stages
(preprocessing, compilation, linking) for us. Let’s now consider a more interesting
example that consists of multiple files.
9. COMPILATION 128

Create the following directory and file structure:

.
say_hello/
include/
say_hello.hpp
src/
say_hello.cpp
src/
hello.cpp

The “say hello” folder contains a simple library that defines a say hello function.
This function will be used in the main function in hello.cpp.
say hello.hpp
#pragma␣once

#include␣<string_view>

/// Print a greeting message directed at `who'.


///
/// @param who
/// The person or entity to greet.
void␣say_hello(std::string_view␣who);

say hello.cpp
#include␣<say_hello.hpp>

#include␣<iostream>

void␣say_hello(std::string_view␣who)␣{
␣␣␣␣std::cout␣<<␣"Hello,␣"␣<<␣who␣<<␣"!"␣<<␣std::endl;
}

hello.cpp
#include␣<say_hello.hpp>

int␣main()␣{
␣␣␣␣say_hello("world");
}

To compile these source files into a program, we’ll have to preprocess and compile
say hello.cpp and hello.cpp, and then link everything into an executable binary.
To preprocess and compile an implementation file into an object file without linking,
GCC’s -c flag is used (compile). Additionally, we need to tell the preprocessor
where it can find the <say hello.hpp> header file. This is done using GCC’s -I
flag (include path).
Preprocessing and compilation
# Preprocess & compile each implementation file to an object file
g++␣-c␣say_hello/src/say_hello.cpp␣-Isay_hello/include␣-o␣say_hello.o␣-Wall␣-Wextra␣
-Werror␣-O3␣-std=c++17
11. DOCUMENTATION 129

g++␣-c␣src/hello.cpp␣-Isay_hello/include␣-o␣hello.o␣-Wall␣-Wextra␣-Werror␣-O3␣-
std=c++17

Linking
# Link all object files together into a binary
g++␣say_hello.o␣hello.o␣-o␣hello

You can now run the program again using ./hello and it will print “Hello, world!”.

10. CMake

As you can imagine after seeing the previous example, the commands for compiling
large applications quickly become quite complex, and executing them manually each
time becomes quite laborious. When you’re dealing with dependencies between mul-
tiple libraries, things get complicated quickly. Besides, you don’t want to recompile
everything after changing just a single file, and you might want to speed up the
compilation by compiling multiple files in parallel.
Tools like CMake provide a solution for these issues. CMake allows you to specify the
different components of your project and the dependencies between them, and it will
generate the right compiler commands for you, you can build your entire project with
just a singe command. The CMake configuration files are platform-independent, so
you can easily switch to different compilers or operating systems.
In fact, CMake is used quite often throughout the Autopilot project. Therefore, it is
definitely worth it to take a look at this Modern CMake tutorial by Henry Schreiner.
You can find many other resources online, but make sure that they are up-to-date
(at least CMake version 3.12).

11. Documentation

Proper documentation is an essential part of writing good code. Here is the conven-
tion that is used in the Autopilot project.

11.1. In-Line Comment. If you want to add some explanation to a line/sec-


tion of code, you should use //. The following example comes from MedianFilter.
// Calculate the median of the buffer by sorting it just enough
// to find the middle element. A copy should be made to keep the
// order of the original ring buffer intact.
std::array␣sorted␣=␣previousInputs;
auto␣halfWay␣␣␣␣␣␣=␣sorted.begin()␣+␣N␣/␣2;
std::nth_element(sorted.begin(),␣halfWay,␣sorted.end());

If you need many of these comments, it might be an indication that the code should
be refactored into a function with a meaningful name, for example.
Don’t just write what the code does, you can figure that out based on the code
itself, instead, try to explain why or how the code works. For example, the following
comment tells you absolutely nothing and should be removed:
int␣i␣=␣42;␣// create a variable 'i' and assign 42 to it
12. MISCELLANEOUS 130

11.2. Doxygen Comments. The Autopilot project uses a tool called doxygen
to automatically generate documentation based on annotations in the source code.
To have a comment appear in the generated documentation, you should use either
/** ... */, /// or ///<. The first should be used for long descriptions; the
second should be used for short descriptions; and the third should be used for short
descriptions, but on the same line.
For example:
/**
* \@brief <short−description>
*
* <extended−description>
*
* @param a
* <param−description>
* @param b
* <param−description>
* @return <return−description>
*/
float␣function1(float␣a,␣float␣b);

/// <short−description>
float␣function2(float␣a,␣float␣b);

/// <short−description>
float␣x;

float␣y;␣///< <short−description>

You can use /// over multiple lines if the description is too long.
Doxygen comments can contain annotations or commands. A few have already
been shown above. Here’s a short list of some of the most important commands:

✓ @brief: short description


✓ @param: parameter + description
✓ @return: return description
✓ @retval: case-by-case return description
✓ @note: creates a note to draw attention

For a full overview, see https://www.doxygen.nl/manual/commands.html.

12. Miscellaneous

12.1. Namespaces. In large code bases, you might come across a situation
where different libraries or sections of the code define a function, variable or type
with the same name. As a result of this name collision, the code will fail to compile
or link, since each symbol can only have one definition.
To get around this issue, you can wrap each component of the code base in its own
namespace.
For example, let’s say both the Autopilot and the Crypto modules define an “init”
function:
12. MISCELLANEOUS 131

Autopilot.hpp
#pragma␣once
void␣init();

Crypto.hpp
#pragma␣once
void␣init();

main.cpp
#include␣<Autopilot.hpp>
#include␣<Crypto.hpp>

int␣main()␣{
␣␣␣␣init();␣// which 'init' function is called here?
}

This will fail to compile with an error saying error: redefinition of ‘void
init()’, or with a multiple definition linker error.
By moving the code of each module into their own namespace, these issues are
avoided:

Autopilot.hpp
#pragma␣once
namespace␣autopilot␣{
void␣init();
}␣// namespace autopilot

Crypto.hpp
#pragma␣once
namespace␣crypto␣{
void␣init();
}␣// namespace crypto

main.cpp
#include␣<Autopilot.hpp>
#include␣<Crypto.hpp>

int␣main()␣{
␣␣␣␣autopilot::init();
␣␣␣␣crypto::init();
}

The scope resolution operator (::) is used to specify the namespace a name belongs
to. Namespaces can be nested:
namespace␣crypto␣{
namespace␣encryption␣{
std::vector<std::byte>␣encrypt(std::string_view␣message);
}␣// namespace encryption
}␣// namespace crypto
12. MISCELLANEOUS 132

In some cases it might be a bit verbose to type out the fully qualified name each
time. If there’s no ambiguity, you can use the using keyword to bring a name from
a different namespace into the current scope, for example:
int␣main()␣{
␣␣␣␣using␣crypto::encryption::encrypt;
␣␣␣␣// ...
␣␣␣␣encrypt(message);␣// calls crypto::encryption::encrypt()
}

It is highly recommended to limit the scope of these using declarations, and you
should never write them in the global namespace in header files.
You can also bring all names from another namespace into the current scope with a
using directive:
int␣main()␣{
␣␣␣␣using␣namespace␣crypto::encryption;
␣␣␣␣// ...
␣␣␣␣encrypt(message);␣// calls crypto::encryption::encrypt()
}

Again, limit the scope of these, and never add them to the global scope in header
files.
You can also limit the amount of characters you need to type while still getting the
benefits of namespaces by using namespace aliases:
int␣main()␣{
␣␣␣␣namespace␣encr␣=␣crypto::encryption;
␣␣␣␣// ...
␣␣␣␣encr::encrypt(message);␣// calls crypto::encryption::encrypt()
}

12.2. Mathematical functions. Most mathematical functions (e.g. sin, cos,


square root, logarithms) are declared in the <cmath> header. This is the C++ version
of a C header, and it usually contains both the C functions and C++ functions: the
C functions are in the global namespace (e.g. sqrt) and the C++ functions are in
the std namespace (e.g. std::sqrt). It is very important to always use the C++
functions: the C functions do not support overloading, so you have to choose the
function with the right name for the given argument type.
For example: the C function abs (absolute value) only supports arguments of type
int, if you call it with a floating point argument, it will be implicitly converted
to an integer, truncating the result. Similarly, the C function sqrt only supports
double, if you use it with arguments of type float, you’re wasting processing power
by using a double-precision function and then converting it to single-precision again.
In C, the correct solution would be to use the fabs (floating point absolute value)
and sqrtf (square root float) functions respectively, but this is very error prone.
Thanks to C++’s function overloading, it automatically selects the right version of
the function based on the argument types, but you have to use the functions in the
std namespace, not the C functions in the global namespace.
#include␣<cmath>
12. MISCELLANEOUS 133

print(abs(-1.5));␣␣␣␣␣␣// "1", bad, C function for integers


print(std::abs(-1.5));␣// "1.5", good, overloaded C++ function

float␣r1␣=␣sqrt(16.f);␣␣␣␣␣␣// bad, double−precision C function


float␣r2␣=␣std::sqrt(16.f);␣// good, single−precision C++ function

If you need these math functions often, you can use a using declaration:
using␣std::abs;
print(abs(-1.5));␣␣␣␣␣␣// "1.5", ok, calls std::abs

12.3. Exceptions. C++ supports exceptions for error handling. Exceptions can
be raised using the throw keyword and propagate upwards through the call stack
untill a catch block for the type of exception is encountered. If a raised exception
is not caught, the program is terminated.
#include␣<stdexcept>␣// std::domain_error
#include␣<iostream>␣␣// std::cerr

// Custom exception type that inherits from std::domain_error


struct␣division_by_zero_error␣:␣std::domain_error␣{
␣␣␣␣division_by_zero_error()
␣␣␣␣␣␣:␣std::domain_error("division␣by␣zero")␣{}
};

int␣safe_divide(int␣dividend,␣int␣divisor)␣{
␣␣␣␣if␣(divisor␣==␣0)
␣␣␣␣␣␣␣␣throw␣division_by_zero_error();
␣␣␣␣return␣dividend␣/␣divisor;
}

int␣main()␣{
␣␣␣␣try␣{
␣␣␣␣␣␣␣␣safe_divide(100,␣0);
␣␣␣␣}␣catch␣(division_by_zero_error␣&e)␣{
␣␣␣␣␣␣␣␣std::cerr␣<<␣e.what()␣<<␣'\n';␣// print the error message
␣␣␣␣}␣catch␣(std::exception␣&e)␣{␣// catches any other exceptions
␣␣␣␣␣␣␣␣std::cerr␣<<␣e.what()␣<<␣'\n';
␣␣␣␣}␣catch␣(...)␣{␣//␣catches␣anything␣else
␣␣␣␣␣␣␣␣// (you can throw arbitrary types)
␣␣␣␣␣␣␣␣std::cerr␣<<␣"unknown␣error\n";
␣␣␣␣␣␣␣␣throw;␣// rethrow the exception we just caught
␣␣␣␣}
}

Polymorphic exceptions (e.g. exceptions that inherit from std::exception) should


always be caught by reference, not by value. Types without virtual functions can
be caught by value, but this is very rare.
Resources such as memory or open files are cleaned up automatically when an ex-
ception occurs 8, so there is no need for a finally block like in Java, for example.

8Provided you are using classes that make use of RAII.


13. ADVANCED TOPICS 134

Warning 12.12.1

Be careful when using exceptions in embedded contexts. Imagine what would


happen if your Autopilot software encounters an uncaught exception while it is
flying . . .

13. Advanced Topics

13.1. Templates. Sometimes, you want a certain function or class to be de-


fined for a variety of types, not just one. For example, you want to be able to define
a function that computes the sum of integers, floating point numbers, vectors, ma-
trices, etc., or maybe a data type or container that can store a linked list of integers,
a linked list of tasks, a linked list of waypoints in the EAGLE mission. In that case,
you can use generic programming, where the types of function arguments or member
variables are generic. In C++, generic programming is done by defining function or
class templates.
For example, consider a function max that returns the larger of two numbers. A
basic implementation could look like this:
float␣max(float␣a,␣float␣b)␣{
␣␣␣␣if␣(a␣<␣b)
␣␣␣␣␣␣␣␣return␣b;
␣␣␣␣else
␣␣␣␣␣␣␣␣return␣a;
}

This function works fine for values of type float, but what if you also want it to
support arguments of type double or int? Or even more generally, any type that
you can compare using the less than operator?
In that case, we need some way to indicate that the argument can be of any type,
let’s call this type wildcard “T”. Using this wildcard, we could write the function
signature as T max(T a, T b). The way such wildcards are defined in C++ is by
using the template keyword. In the following snippet, we first declare a wildcard T
that can be any type name. Then we use that wildcard in the function definition:
template␣<typename␣T>␣// wildcard T
T␣max(T␣a,␣T␣b)␣{
␣␣␣␣if␣(a␣<␣b)
␣␣␣␣␣␣␣␣return␣b;
␣␣␣␣else
␣␣␣␣␣␣␣␣return␣a;
}

T is called a template argument. Notice the parallels between normal function


arguments and template arguments, e.g. an integer argument can be declared as
“int i”, , and a template argument as “typename T”: you first specify what kind
of entity you are declaring, followed by a placeholder name for it. In case of the
former, the kind of entity is “an integer”, and its name is “i”, in case of the latter,
the kind of entity is “a data type”, and the name of this type is referred to as “T”.
Just like “i” is a placeholder for a specific integer value that will be provided when
13. ADVANCED TOPICS 135

the function is called, “T” is a placeholder for a specific type that will be provided
when the template is instantiated (more on this later).
In the previous example, “template <typename T> T max(T a, T b)” is not actu-
ally a function, it is a function template: you first have to fill in a type name in place
of T to turn the function template into an actual function. This is called template
argument substitution, where the generic type T is substituted by a concrete type,
e.g. int or double.
When you call a function template, for example,
int␣m␣=␣max(4,␣5);

the compiler first deduces the types of the arguments in a process called template
argument deduction. In this case, both arguments are integers, so it infers that the
generic type “T” must be “int”. Next, the compiler grabs the function template
definition, and substitutes all placeholders “T” by “int”. Once all placeholders have
been replaced, the result is a normal function. This is called template instantiation:
the function template “T max(T, T)” has been instantiated with T = int and the
result is a function “int max(int, int)”.
In some scenarios, the compiler cannot deduce the type T, for example, what should
the type be if you call max(4, 5.5)? One argument has type int, and the other has
type double, T cannot be replaced by both at the same time. To help the compiler,
you can explicitly specify the types of the template parameters. This is done using
angle brackets (<>), for example, max<double>(4, 5.5) will replace T by double.
It is important to realize that template instantiation happens entirely at compile
time. This is because data types are checked by the compiler, not at run time. For
this reason, the definition of the function template must be available to the compiler
at the point where it is used, it cannot be defined in a different implementation
file. For this reason, function templates are usually defined in header files, not in
implementation files.
In a template declaration, the keywords “typename” and “class” have the same
meaning.
Templates are not limited to functions, you can also have class templates. For
example, the standard library has a class template “std::list”, and its template
parameter is the type of the elements you want in the list. This means that you can
use the same class template and function templates to create lists of integers, lists
of strings, or even lists of lists.
Templates have many use cases, for example, in the Autopilot project they are used
for the exponential moving average filter class. The EMA is used to filter both single
floating point numbers, and vectors of floating point numbers. Instead of copying
and pasting the class for each type, it’s less error-prone to use a class template:
template␣<class␣InOutType,␣class␣ScalarType␣=␣float>
class␣EMA␣{
␣␣public:
␣␣␣␣EMA(ScalarType␣alpha,␣InOutType␣initial␣=␣{})␣:␣alpha(alpha),␣y(initial)␣{}

␣␣␣␣InOutType␣operator()(InOutType␣x)␣{
␣␣␣␣␣␣␣␣return␣y␣+=␣alpha␣*␣(x␣-␣y);
13. ADVANCED TOPICS 136

␣␣␣␣}

␣␣private:
␣␣␣␣ScalarType␣alpha;
␣␣␣␣InOutType␣y;
};

As you can see, you can have multiple template parameters, and you can add a
default value (similar to normal function arguments).
Not all template parameters have to be types, you can also use values. For example,
the second parameter of std::array is the number of elements. This number be-
comes part of the type, which allows the compiler to perform more rigid type checks.
Also, the size will be known at compile time, so an array can be allocated on the
stack.
std::array<float,␣3>

13.2. Arrays Revisited. C-style arrays have some serious limitations and
rough edges:

✓ You cannot return an array from a function


✓ You cannot pass an array to a function by value, only by reference
✓ Arrays have the nasty habit of implicitly decaying to a pointer to the first
element, losing all size information
✓ The sizeof keyword on an array parameter in a function always returns
the size of the pointer, not the actual size of the array
✓ You cannot assign one array to another, you can only assign elements
✓ You cannot compare arrays using the equality operator (==)
✓ You cannot initialize class members of C-style array type in a member
initializer list
// Don't do any of this

// 1.
// Arrays cannot be returned from functions:
int␣[]␣functionReturningArray()␣{␣// error
␣␣␣␣int␣a[]␣=␣{1,␣2,␣3};
␣␣␣␣return␣a;
}

// 2.
// Arrays cannot be passed by value:
void␣fun(int␣array[])␣{
␣␣␣␣array[0]␣=␣42;
}
int␣array[]␣=␣{1,␣2,␣3};
fun(array);
// The array is now {42, 2, 3}, because the function operates
// on the actual array, not on a copy of it.

// 3.
// When used as a function argument, 'int array[]' is exactly
// equivalent to 'int array*' (i.e. a pointer to the first
13. ADVANCED TOPICS 137

// element), even though it doesn't look like a pointer:


int␣sum(int␣array[])␣{
␣␣␣␣int␣sum␣=␣0;
␣␣␣␣size_t␣size␣=␣sizeof(array)␣/␣sizeof(array[0]);
␣␣␣␣// Wrong: sizeof(array) is the same as sizeof(int*)
␣␣␣␣for␣(size_t␣i␣=␣0;␣i␣<␣size;␣i++)
␣␣␣␣␣␣␣␣sum␣+=␣array[i];
␣␣␣␣return␣sum;
}

// 4.
// Arrays cannot be copied using the assignment operator:
int␣a[]␣=␣{10,␣20,␣30};
int␣b[]␣=␣a;␣// error

C++’s std::array container solves most of these problems:


// 1.
std::array<int,␣3>␣functionReturningArray()␣{
␣␣␣␣std::array␣a␣=␣{1,␣2,␣3};
␣␣␣␣return␣a;
}

// 2.
void␣fun(std::array<int,␣3>␣array)␣{␣␣// argument by value
␣␣␣␣array[0]␣=␣42;␣␣// operates on the local copy of the array
}
std::array<int,␣3>␣array␣=␣{1,␣2,␣3};
fun(array);␣␣// 'array' didn't change, it's still {1, 2, 3}

// 3.
int␣sum(std::array<int,␣3>␣array)␣{
␣␣␣␣int␣sum␣=␣0;
␣␣␣␣for␣(size_t␣i␣=␣0;␣i␣<␣array.size();␣++i)
␣␣␣␣␣␣␣␣sum␣+=␣array[i];
␣␣␣␣return␣sum;
}

// 4.
std::array<int,␣3>␣a␣=␣{10,␣20,␣30};
std::array<int,␣3>␣b␣=␣a;␣␣// Does what you would expect (creates
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// a copy of 'a')

Function (3.) in the snippet above can be improved even further:

✓ Use a template function so it can be used for arrays of any type and of any
size.
✓ Pass the argument by reference-to-const to avoid expensive copies of large
arrays.
✓ Initialize the sum variable using value initialization so it can be used with
any type T.
✓ Use a range-based for loop to avoid manually iterating over all indices,
leaving no room for index out of bounds errors.
// 3. (improved)
template␣<class␣T,␣size_t␣N>
T␣sum(const␣std::array<T,␣N>␣&array)␣{
13. ADVANCED TOPICS 138

␣␣␣␣T␣sum␣{};
␣␣␣␣for␣(const␣T␣&element␣:␣array)
␣␣␣␣␣␣␣␣sum␣+=␣element;
␣␣␣␣return␣sum;
}

13.3. Returning multiple values from a function. It is not possible for


a function to return multiple values like in Matlab. To get around this, you can
return a struct.
/// Struct containing an integer, boolean and double.
struct␣ReturnValues␣{
␣␣␣␣int␣␣␣␣val1,
␣␣␣␣bool␣␣␣val2,
␣␣␣␣double␣val3,
};

ReturnValues␣return_struct_example()␣{
␣␣␣␣return␣{1,␣true,␣1.2};␣// return three values
}

ReturnValues␣r␣=␣return_struct_example();
int␣␣␣␣v1␣=␣r.val1;
bool␣␣␣v2␣=␣r.val2;
double␣v3␣=␣r.val3;
print(v1,␣v2,␣v3);␣// "1 true 1.2"

You can also return multiple values using a tuple.


#include␣<tuple>␣// import std::tuple

std::tuple<int,␣bool,␣double>␣return_tuple_example()␣{
␣␣␣␣return␣{1,␣true,␣1.2};␣// return three values
}

std::tuple<int,␣bool,␣double>␣r␣=␣return_tuple_example();
int␣␣␣␣v1␣=␣std::get<0>(r);
bool␣␣␣v2␣=␣std::get<1>(r);
double␣v3␣=␣std::get<2>(r);
print(v1,␣v2,␣v3);␣// "1 true 1.2"

The code above is rather verbose, we can make it easier to read by using the auto
keyword for automatic return type deduction, and std::make tuple to deduce the
type of the tuple elements.
auto␣return_tuple_example()␣{
␣␣␣␣return␣std::make_tuple(1,␣true,␣1.2);
}

The auto keyword can also help simplify the calling code:
auto␣r␣=␣return_tuple_example();
int␣␣␣␣v1␣=␣std::get<0>(r);
bool␣␣␣v2␣=␣std::get<1>(r);
double␣v3␣=␣std::get<2>(r);
print(v1,␣v2,␣v3);␣// "1 true 1.2"
14. WHERE TO GO NEXT? 139

Extracting the values from the tuple using std::get is still quite cumbersome, we
can improve this code using structured bindings:
// Initialize v1, v2 and v3 with the three values in the
// tuple returned by 'return_tuple_example'
auto␣[v1,␣v2,␣v3]␣=␣return_tuple_example();
print(v1,␣v2,␣v3);␣// "1 true 1.2"

If you want to assign the tuple elements to existing variables, you can use std::tie:
int␣␣␣␣v1;
bool␣␣␣v2;
double␣v3;
// Initialize v1, v2 and v3 with the three values in the
// tuple returned by 'return_tuple_example'
std::tie(v1,␣v2,␣v3)␣=␣return_tuple_example();
print(v1,␣v2,␣v3);␣// "1 true 1.2"

If you don’t need all tuple elements, you can use std::ignore:
int␣␣␣␣v1;
bool␣␣␣v2;
// Initialize v1 and v2 with the first two values in the
// tuple returned by 'return_tuple_example', ignore the
// third value
std::tie(v1,␣v2,␣std::ignore)␣=␣return_tuple_example();
print(v1,␣v2);␣// "1 true"

14. Where to go next?

This tutorial is by no means complete, its goal is to give a general overview and an
introduction to the basic concepts of the language. If you want to dig deeper, you
can start with the following resources:
✓ www.learncpp.com: up-to-date, extensive and free online C++ tutorial, pro-
motes good practices
✓ en.cppreference.com: complete language reference, for looking up specific
answers or just exploring what the standard library has to offer
✓ CppCon: Back to Basics (2019, 2020): playlists of excellent conference talks
about specific topics
✓ isocpp.org/faq: Frequently asked questions by topic
✓ C++ Core Guidelines: official set of guidelines for using C++ well
✓ C++ Weekly: playlist with many short videos about specific topics
Resources to avoid:
× Most YouTube channels: many are outdated or teach bad practices
× geeksforgeeks.org: low-quality example code, promotes bad practices
× www.w3schools.com/cpp: simplistic, example-based tutorial
× www.cplusplus.com/reference: the reference is outdated, use
en.cppreference.com instead (the cplusplus.com tutorials aren’t that bad)
CHAPTER 13

Programming Languages – Verilog

1. Introduction
In this chapter, we will explain the basics of Hardware Description Languages
(HDLs), and Verilog in particular.
HDLs are very different from software programming languages. Software codes
operate with a sequence of statements that are executed one after another. The
processor understands a sequence of simple assembly instructions that are derived
from the codes and are sequential in the same way as the high-level code.
In contrary, HDLs define registers containing the sequential logic state, and
combinatorial logic between registers to determine the value during the next clock
cycle. All the computations happen in parallel. The end result is implemented on
an FPGA using flip-flops (FFs) as memory, lookup tables (LUTs) to implement logic
functions, and wires connecting these parts. Therefore, hardware, which is FPGA
logic in this project, operates very differently from software, and thus the language
descriptions differ a lot as well. The difference is illustrated in Fig. 1.

Figure 1. Software compared to Register Transfer Level (RTL)


logic circuit

Verilog, one of these HDLs, was originally meant as a language to simulate


actions happening on events, in their abstract sense. It was then used by the hard-
ware industry to describe hardware logic. Other languages used in the industry are
VHDL, SystemVerilog (which is to Verilog like C++ is to C), and SystemC. In the
EAGLE project, we will use Verilog.
The process of converting HDL code into hardware is called synthesis, it is the
equivalent of compiling (or interpreting) software languages. Note that Verilog code
140
2. VERILOG, THE ABSTRACT SIMULATION LANGUAGE 141

that works properly in the behavioral simulator can not always be meaningfully
translated into Register Transfer Level (RTL) hardware.
We will first start with a language overview of Verilog, then cover how Verilog
is used to describe electronics hardware, after which we finish with an explanation
of how to use Verilog for simulations.
Additionally, it is also encouraged to use other tutorials, books or online re-
sources because some features of Verilog are not covered in this tutorial. Quick
Start Guide to Verilog [LaM19] is s highly recommended guide, which is available
here or in the EAGLE-student repository. You should also always try to search for
your problem online, as there are many online resources available. Websites like this
one, especially its “tidbits” section, which explains some of the odd behavior found
in Verilog which can easily cause bugs. Some features of Verilog are not covered in
this tutorial.
Students can also refer to the IEEE Verilog standards [06]. Students who have
not taken a digital electronics or digital design course can refer to [Ash07] for a more
detailed treatment of the topic.

2. Verilog, the abstract simulation language


2.1. Data types. Verilog fundamentally operates on bits, each bit can have
four values1: zero (0), one (1), high-impedance (z) and “don’t care” (x). The
meaning of 0 and 1 is rather intuitive, but the last two are less so. A bit has value
z when it is used, but not assigned to. Assigning x to a bit means the value is
unimportant and undefined, the tool that synthesizes the Verilog code can choose
one itself, in order to make the implementation more efficient.
Bits can be grouped together, using arbitrary bit indices. For example, a byte
(of 8 bits) would be written as [7:0] in the type declaration, as the index of the
most significant bit is 7, while the LSb index is 0. These groupings can be combined
once more to yield larger ones, for example, {byte hi, byte lo} to form a 16-bit
word from two bytes. Single bits or ranges of bits can be accessed using array index
notation: somebyte[3:2].
2.2. Integer literals. Groups of bits form integer values. Writing down these
integers is slightly more complicated than in eg. C, as they consist of four parts: the
width (the number of bits the integer has), an apostrophe (’), the base (or ‘radix’)
which can be b (binary), d (decimal) or h (hexadecimal), and finally the integer
value itself. Have some examples:
Listing 13.1. Various integer literal examples
1'b0␣␣␣␣␣␣␣␣␣␣␣␣// the (binary) bit '0'
1'b1␣␣␣␣␣␣␣␣␣␣␣␣// the (binary) bit '1'
8'b1␣␣␣␣␣␣␣␣␣␣␣␣// the number '1', as an 8−bit value
8'd1␣␣␣␣␣␣␣␣␣␣␣␣// the number '1', as an 8−bit value, using decimal notation
8'd2␣␣␣␣␣␣␣␣␣␣␣␣// the number '2', as an 8−bit value
8'b2␣␣␣␣␣␣␣␣␣␣␣␣// ERROR: '2' cannot be used in a binary (b) base!
8'd32␣␣␣␣␣␣␣␣␣␣␣// the number '32', as an 8−bit value
8'h20␣␣␣␣␣␣␣␣␣␣␣// same as above, but using a hexadecimal (h) base
8'hfx␣␣␣␣␣␣␣␣␣␣␣// equal to 8'b1111xxxx
8'bz0␣␣␣␣␣␣␣␣␣␣␣// this is equal to 8'bzzzzzzz0: the values 'z' and 'x' are
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// extended to the higher bits, while 0 is inserted if the
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// highest bit written down is 0 or 1

1https://en.wikipedia.org/wiki/Four-valued logic
2. VERILOG, THE ABSTRACT SIMULATION LANGUAGE 142

32'h04000_4002␣␣// a 32−bit wide hexadecimal number. Underscores can be used


␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// to separate and group digits
'd0␣␣␣␣␣␣␣␣␣␣␣␣␣// zero value, the number of bits is inferred from the context
0␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// A decimal base is assumed by default
d0␣␣␣␣␣␣␣␣␣␣␣␣␣␣// ERROR: This is a variable name, not 'decimal zero'

2.3. Nets and wire. Verilog has ‘variables’, but they behave very differently
from software variables: the latter is where values can be placed, and read out or
modified at a later time. In HDLs, ‘variables’ are names given to a particular point
or node of the circuit, often called nets. Nets have drivers and users. Drivers are
circuits that give the net a value, while users use the value on the net in other
circuits, as shown in Fig. 2.

Figure 2. Nets, drivers and users

Verilog has two types of nets: wires and regs. Wires have a single, fixed as-
signment or driver, and can only be used to express combinatory logic circuits.
Connecting a driver to a wire is done using the assign keyword:
Listing 13.2. wire example 1
wire␣mynet;
assign␣mynet␣=␣out;

assign␣in1␣=␣mynet;
assign␣in2␣=␣mynet;

The above example is the Verilog code that generates the circuit shown in Fig.
2. Below is a slightly more complex example:
Listing 13.3. wire example 2
wire␣[7:0]␣somebyte;␣␣// this wire consists of 8 bits
// assign 'flag' to the lowest bit, and a constant value to the others
assign␣somebyte␣=␣{7'd42,␣flag};

wire␣[1:0]␣twobits;
2. VERILOG, THE ABSTRACT SIMULATION LANGUAGE 143

// extract bits 4 to 1, add 1, and put it in 'twobits'


// WARNING: bits 4 and 3 of 'somebyte' will be discarded, only bits 2
// and 1 will be used in 'twobits', as it is only 2 bits wide
assign␣twobits␣=␣somebyte[4:1]␣+␣1;␣// same as somebyte[2:1] + 1

2.4. reg and procedural assignment.


2.4.1. Introduction. reg is the second variable type in Verilog. One might expect
that, while a wire represents combinatorial logic, a reg must represent a register or
a memory cell used in sequential logic. However, this is not the case,2 regs play a
completely different role: they are nets assigned to in a procedural assignment block,
which is the block starting from the keyword always. Such a block of code is always
executed, possibly reacting to another event. In a procedural code block, complex
structures such as if- and for-statements can be used.
During synthesis, a reg and its associated procedural assignment block can be
converted into combinatorial logic, sequential logic, or, if it is non-synthesizable,
nothing at all, hopefully, accompanied by an error, all depending on what exactly
the Verilog code is doing. Therefore, carefree use of regs can cause hard-to-fix bugs
that only show up when testing on real hardware, and pass simulation tests. §3
elaborates on this and how to avoid it.
In Verilog, regs can be assigned to in two ways: non-blocking and blocking
assignments, using resp. the <= and = operators.
Non-blocking assignments always use the old and incoming values on the right-
hand side of the expression. Consecutive non-blocking assignments happen concur-
rently and their order is interchangeable. When assigning to a reg in a non-blocking
manner, only one procedural block can be assigned to that reg.
Blocking assignments work very differently: they are guaranteed to be in order,
and multiple procedural blocks can be assigned to the same reg in a blocking manner,
as this solves race conditions when two procedures would write to the same reg at
the same moment.
More information on non-blocking compared to blocking assignments can be
found here. In general, non-blocking assignments are recommended in designing
your hardware. Sections §3 and §4 will elaborate on which assignment style is most
appropriate in which situation.
2.4.2. Combinatorial reg usage. Now we finally have covered enough matter to
give code examples of the structure of a procedural assignment block used to drive
a reg. We’ll start with the simplest case: an always procedure:
Listing 13.4. reg example
reg␣[7:0]␣myreg;
always␣(*)␣begin
␣␣␣␣myreg␣<=␣somebyte␣^␣8'haa;
end

The code in an always procedure is, as the name suggests, always executed.
In this case, it means that myreg will always have the value somebyte ^ 8’haa
assigned to it.
Does this have any meaningful difference from Listing 13.5? It does not, and
this example shows that the reg keyword is unrelated to hardware registers: it is
simply a Verilog language construct.
2
https://danluu.com/why-hardware-development-is-hard/
2. VERILOG, THE ABSTRACT SIMULATION LANGUAGE 144

Listing 13.5. wire equivalent of Listing 13.4


wire␣[7:0]␣mywire;
assign␣mywire␣=␣somebyte␣^␣8'haa;

However, by using a procedural block, we can use more complex decision-making


code to assign a value to a reg, then compared to assigning a single expression to a
wire, as shown in this example:
Listing 13.6. More complex reg example
reg␣[7:0]␣myreg;
integer␣i;
always␣(*)␣begin
␣␣␣␣if␣(flag)␣myreg␣<=␣8'b0;
␣␣␣␣else␣if␣(flag2)␣begin
␣␣␣␣␣␣␣␣for␣(i␣=␣0;␣i␣<=␣7;␣i␣=␣i␣+␣1)␣begin
␣␣␣␣␣␣␣␣␣␣␣␣myreg[i]␣<=␣some32bit[i*3␣+␣2]␣^␣(i␣&␣1);
␣␣␣␣␣␣␣␣end
␣␣␣␣end␣else␣myreg␣<=␣somebyte␣^␣8'haa;
end

Here, the value of myreg depends on multiple flags, and, in one case, uses a for
loop to extract specific bits from a larger word. However, this for loop behaves very
differently from one in a software language! Instead of executing the iterations one
by one, the loop is expanded into eight concurrent assignments of the separate bits
of myreg. This is also why a non-blocking assignment is used: a blocking assignment
would give the loop a completely different meaning!
Also, note the data type of the loop iterator i: it is given the type integer,
which cannot be meaningfully used for nets, but instead is used for “compile-time”
code expansions such as for loops as used here.
The entire code snippet could technically be expressed using an assign state-
ment, as it is still functionally equivalent, however, using the latter would have
proven quite cumbersome to do. This is one reason to use reg in some cases.
2.4.3. Sequential reg usage. However, we still have not found a way to declare
registers, memory cells, or sequential logic. For this, we need to introduce another
concept: sensivity lists, specified using the @ symbol. always blocks have the option
to only run when specified nets change value. This way, logic can be synchronized
to a clock signal, creating clocked flip-flops:
Listing 13.7. D-flip-flop
reg␣[7:0]␣d_flipflop;
always␣@(posedge␣clk)␣begin
␣␣␣␣if␣(enable)␣d_flipflop␣<=␣newvalue;
␣␣␣␣else␣d_flipflop␣<=␣d_flipflop;
end

On a positive clock edge, the next value is put into d flipflop if enable is
high. Otherwise, the value is retained. This a clocked D-flip-flop! We finally have a
building block to create sequential logic.
Clock cycle counters are constructed very similarly:
Listing 13.8. A simple counter with save functionality
reg␣[31:0]␣counter;
reg␣[31:0]␣countersave;
wire␣save;
2. VERILOG, THE ABSTRACT SIMULATION LANGUAGE 145

always␣@(posedge␣clk,␣negedge␣nrst)␣begin
␣␣␣␣if␣(!nrst)␣begin
␣␣␣␣␣␣␣␣counter␣<=␣32'd0;
␣␣␣␣␣␣␣␣countersave␣<=␣32'd0;
␣␣␣␣end␣else␣begin
␣␣␣␣␣␣␣␣counter␣<=␣counter␣+␣1;
␣␣␣␣␣␣␣␣if␣(save)␣countersave␣<=␣counter;
␣␣␣␣␣␣␣␣else␣countersave␣<=␣countersave;
␣␣␣␣end
end

This example also has a reset signal available, to reset the counter. Note that it
is active-low, and still has an effect even if the clock signal is disabled, as nrst is in
the sensitivity list. This is an asynchronous reset, if it weren’t in the list, it would
have been a synchronous reset. When save is asserted, the counter value is saved
in another register.
Additionally, note the use of non-blocking assignments: even though counter is
used in the countersave assignment after being assigned to, the old value will still
be used, instead of the incremented one.
2.4.4. Other reg usage. It is also possible to put other signals in the sensitivity
list or to check for both a positive and a negative edge. For example:
Listing 13.9. A simple latch
reg␣[7:0]␣myreg;
always␣@(enable)␣begin
␣␣␣␣if␣(enable)␣myreg␣<=␣newvalue;
␣␣␣␣else␣myreg␣<=␣latch;
end

Here, myreg is assigned to any change of enable. This forms a latch.


What happens if we also add newvalue to the sensitivity list?
Listing 13.10. Not a latch
reg␣[7:0]␣myreg;
always␣@(enable,␣newvalue)␣begin
␣␣␣␣if␣(enable)␣myreg␣<=␣newvalue;
␣␣␣␣else␣myreg␣<=␣latch;
end

Now myreg is assigned to again on any change. There is not any different from
specifying no sensitivity list, and this synthesizes back into combinatorial logic. @(*)
is a shorthand for a sensitivity list for every signal used in the procedure. Normally
the sensitive list is @(*) unless a latch is intensionally designed.
2.5. Modules. Verilog has a module system, to group functionality into sep-
arate units to keep code clean and reusable. They define a ‘sub-circuit’, and thus,
instead of having arguments and a return value like software subroutines, they have
input and output nets that can continuously change value. As an example, let’s take
Listing 13.8, enhance it slightly, and create a module from it:
Listing 13.11. Example module: a timer
module␣timer(input␣clk,␣input␣nrst,␣input␣enable,␣input␣[31:0]␣ncyc,␣output␣alarm);
␣␣␣␣reg␣[31:0]␣counter;
␣␣␣␣always␣@(posedge␣clk,␣negedge␣nrst)␣begin
␣␣␣␣␣␣␣␣if␣(!nrst)␣counter␣<=␣'d0;
2. VERILOG, THE ABSTRACT SIMULATION LANGUAGE 146

␣␣␣␣␣␣␣␣else␣if␣(counter␣==␣ncyc)␣counter␣<=␣counter;
␣␣␣␣␣␣␣␣else␣if␣(enable)␣counter␣<=␣counter␣+␣1;
␣␣␣␣␣␣␣␣else␣counter␣<=␣counter;
␣␣␣␣end

␣␣␣␣assign␣alarm␣=␣(counter␣==␣ncyc);
endmodule

The module has, besides the clock and reset signal, one input and one output,
respectively ncyc and alarm: after ncyc cycles of being enabled, the module will
raise the alarm signal, until it is reset.
Module inputs and outputs by default have type wire. It is also possible to have
them be a reg:
Listing 13.12. Example timer using a reg output
module␣timer(input␣clk,␣input␣nrst,
␣␣␣␣␣␣␣␣␣␣␣␣␣input␣enable,␣input␣[31:0]␣ncyc,
␣␣␣␣␣␣␣␣␣␣␣␣␣output␣reg␣alarm);

␣␣␣␣reg␣[31:0]␣counter;
␣␣␣␣always␣@(posedge␣clk,␣negedge␣nrst)␣begin
␣␣␣␣␣␣␣␣if␣(!nrst)␣counter␣<=␣'d0;
␣␣␣␣␣␣␣␣else␣if␣(counter␣==␣ncyc)␣counter␣<=␣counter;
␣␣␣␣␣␣␣␣else␣if␣(enable)␣counter␣<=␣counter␣+␣1;
␣␣␣␣␣␣␣␣else␣counter␣<=␣counter;
␣␣␣␣end

␣␣␣␣always␣@(*)␣alarm␣<=␣(counter␣==␣ncyc);
endmodule

Modules can also be given parameters: these can be used to enable or disable
extra features, change data widths, etc. They can also be given default values. Here
is an example:
Listing 13.13. Parameterized example timer
module␣timer
#(parameter␣CWIDTH␣=␣32)
(
␣␣␣␣input␣clk,␣input␣nrst,
␣␣␣␣input␣enable,␣input␣[(CWIDTH-1):0]␣ncyc,
␣␣␣␣output␣reg␣alarm
);

␣␣␣␣reg␣[(CWIDHT-1):0]␣counter;
␣␣␣␣always␣@(posedge␣clk,␣negedge␣nrst)␣begin
␣␣␣␣␣␣␣␣if␣(!nrst)␣counter␣<=␣'d0;
␣␣␣␣␣␣␣␣else␣if␣(counter␣==␣ncyc)␣counter␣<=␣counter;
␣␣␣␣␣␣␣␣else␣if␣(enable)␣counter␣<=␣counter␣+␣1;
␣␣␣␣␣␣␣␣else␣counter␣<=␣counter;
␣␣␣␣end

␣␣␣␣always␣@(*)␣alarm␣<=␣(counter␣==␣ncyc);
endmodule

Including a module in another design is relatively straightforward:


Listing 13.14. Using the timer module
2. VERILOG, THE ABSTRACT SIMULATION LANGUAGE 147

reg␣enable;
wire␣[23:0]␣ncyc;
wire␣alarm;

assign␣ncyc␣=␣24'd424242;

timer␣#(CWIDTH=24)␣mytimer(clk,␣nrst,␣enable,␣ncyc,␣alarm);

always␣@(*)␣enable␣<=␣!alarm;

This will run the timer once.


2.6. Functions. Verilog also has functions, but these act differently than those
in software languages: in Verilog, functions are always pure functions, that is, they
can only use their arguments to calculate a return value. They are not allowed to
modify anything else. Verilog does not support recursive functions.
Functions are declared as follows:
Listing 13.15. Function example
function
␣␣␣␣integer␣inc_mod5(integer␣n);␣begin
␣␣␣␣␣␣␣␣inc_mod5␣=␣(n␣==␣'d4)␣?␣'d0␣:␣(n␣+␣'d1);
␣␣␣␣end
endfunction

After which the function can be used in an expression.


assign␣nextstate␣=␣inc_mod5(state);

2.7. generate. This is the final Verilog language feature we will cover here. It
can be used to replicate blocks, much like a for loop in a procedure, but it can
also be used outside one, to import multiple instances of a module, for example.
However, instead of using an integer variable as the loop iterator, it has to be
declared genvar. Here is an example declaring eight timers:
Listing 13.16. Using generate to import multiple instances of the
same module
reg␣[7:0]␣enable;
wire␣[(64*8-1):0]␣ncyc;
wire␣[7:0]␣alarm;

genvar␣i;
generate
␣␣␣␣for␣(i␣=␣0;␣i␣<=␣7;␣i␣=␣i␣+␣1)␣begin
␣␣␣␣␣␣␣␣// assign different ncyc values to different instances
␣␣␣␣␣␣␣␣assign␣ncyc[64*(i+1)-1:64*i]␣=␣'d424242␣+␣i␣*␣8;

␣␣␣␣␣␣␣␣// different instances have different widths


␣␣␣␣␣␣␣␣timer␣#(CWIDTH␣=␣((i+1)*4))␣mytimer(
␣␣␣␣␣␣␣␣␣␣␣␣clk,␣nrst,␣enable[i],
␣␣␣␣␣␣␣␣␣␣␣␣ncyc[64*i+(i+1)*4-1␣:␣64*i],
␣␣␣␣␣␣␣␣␣␣␣␣alarm[i]
␣␣␣␣␣␣␣␣);

␣␣␣␣␣␣␣␣// and wire up the enable/alarm signals


␣␣␣␣␣␣␣␣always␣@(*)␣enable[i]␣<=␣!alarm[i];
␣␣␣␣end
3. VERILOG AS A HARDWARE DESCRIPTION LANGUAGE 148

endgenerate

3. Verilog as a hardware description language


3.1. Introduction. As the previous section has already touched upon, many
valid Verilog constructs cannot be meaningfully translated to hardware. However,
to elaborate on this, we need to know what exactly our hardware and synthesis tools
are capable of.
While an FPGA allows one to lay out circuits of gates and registers on a chip,
not all possible gates or registers are allowed: typically, Look-Up Tables (LUTs)
are used to implement arbitrary logic gates for a limited set of inputs and the only
registers it has available are flip-flops.
To avoid many of the pitfalls present in Verilog, it is often easier and more
productive to first lay out the hardware — on paper if needed — and then transcribe
that design into Verilog. During the process, the following rules of thumb form a
good lead:
No latches: Unless you look intensionally for latch primitives, there should not be
latches synthesized in a synchronous design.
Only clock and reset signals in sensitivity lists: as otherwise latches or non-
synthesizable constructs can be created easily. Specifically, because regis-
ters on FPGA are synthesized with initial values, most reset signals are
not needed. Take the following example:
Listing 13.17. Complex sensitivity list example
reg␣[7:0]␣myreg;
always␣@(newvalue)␣begin
␣␣␣␣myreg[newvalue]␣<=␣enable;
end

How would this be implemented, electronically? The synthesis tool would


have to guess, and it will most likely give up. This code is non-synthesizable.
No double edges (posedge and negedge for the same net) in sensitivity lists:
Resets can be active-low or active-high, and the clock polarity can be pos-
itive or negative. However, mixing these up or reacting to both can have
bad consequences.
No non-blocking assigns: for sequential logic: while blocking assigns have their
use for testbenches, they should be avoided in regular sequential logic as
they complicate the timing behavior.
No initial procedures: while these will only be covered in §4, these may at first
sight look useful to put the hardware in a known initialized state. However,
many synthesis tools simply ignore these statements.
A reg can only be written to by one procedure block: to keep things clear
and uncluttered. Similarly, it is often better to have a procedure deal
with only one reg instead of many, though this sometimes has its use: for
example, in example Listing 13.12, the same procedure could assign to both
alarm and counter without making the whole a big mess, as both signals
have a shared condition on when they should get a certain value. For less
related signals, however, using a single procedure would only make things
less clear. See §3.2 for more info.
for should only be used to repeat hardware constructs: not to repeat a task
multiple times.
3. VERILOG AS A HARDWARE DESCRIPTION LANGUAGE 149

When using the approach “design first, transcribe to Verilog after”, following
these guidelines will happen quite naturally. However, treating Verilog as a software
programming language will go against a number of them.

3.2. Quick intro to hardware design.


3.2.1. Introduction. Earlier, this text instructed to “first lay out the hardware
and then transcribe that design into Verilog”. However, how does one actually do
that?
The resulting hardware design should be an FSMD: a Finite-State Machine with
a Datapath. What does this mean?
A finite state machine (FSM) is, as the name suggests, a type of automaton that
has a finite number of states, and a set of conditions that determine what the next
state should be. They are often visualized as a graph, with the states on the nodes,
and the conditions on the edges. For example, Fig. 3 shows an FSM with two states,
under E it transitions from a to b. If T, the opposite happens.

Figure 3. An example of a finite-state machine

A datapath is the digital, electronic circuit carrying out the calculations required
to manipulate the data. For example, when tasked to calculate the the largest num-
ber from a list, the datapath would be a comparator and a register when performing
the calculation one by one, or a tree of comparators when calculating the result in
a parallel way.
The combination of an FSM and a datapath is an FSMD: the FSM acts as
a controller for the datapath, instructing it when to enable certainly registers or
counters, when to start and stop certain calculations, and so on, using control signals.
The datapath meanwhile can provide some of its nets as input of the controller, to
make it able to decide on the next state.
The flow generally happens as follows:
(1) Create a rough state machine for the hardware, implementing the function-
ality
(2) Design a datapath that can perform the required calculations. Add control
signals where needed.
(3) Recreate the state machine, now using nets from the datapath to determine
the next state.
(4) Create a table that lists for every state the value of every control signal in
that state.
With this information, Verilog sources can be written rather mechanically.
3. VERILOG AS A HARDWARE DESCRIPTION LANGUAGE 150

3.2.2. Finite state machine. As an example, we’ll take the C/C++ code from
Fig. 1 at the very start of the chapter, and walk through the process of creating a
Verilog module that accomplishes the same task.
3. VERILOG AS A HARDWARE DESCRIPTION LANGUAGE 151

Listing 13.18. C code to be translated to Verilog


int␣sum(int␣max)␣{
␣␣␣␣int␣sum␣=␣0;
␣␣␣␣for␣(int␣i␣=␣0;␣i␣<␣max;␣++i)␣{
␣␣␣␣␣␣␣␣sum␣+=␣i;
␣␣␣␣}
␣␣␣␣return␣sum;
}

First of all, we need to determine what the inputs and outputs are. In this case,
they are respectively max and sum.
Then, we split the algorithm into different states, each carrying out a single task:
int␣sum(int␣max)␣{
␣␣␣␣int␣sum␣=␣0;
-------------------------------------------------
␣␣␣␣for␣(int␣i␣=␣0;␣i␣<␣max;␣++i)␣{
␣␣␣␣␣␣␣␣sum␣+=␣i;
␣␣␣␣}
-------------------------------------------------
␣␣␣␣return␣sum;
}

State 1 performs the initialization. State two increases the accumulator, and the
third state is the ‘finished’ state. This yields the finite state machine in Fig. 4.

Figure 4. Finite state machine of the example design

3.2.3. Datapath. Now we need to create a datapath. It will need at least an


adder to calculate the sums, a register to store the sum value, and a counter register
to store the current loop index i. For max, however, we are left with a choice: we
could create another register for it, and load it when starting. Alternatively, we
could simply use it directly and hope (or assume) the code using this module will
not change its value while the module is running. For now, we opt to choose the
latter option for simplicity.3
3
In real-world design, the former option will most likely be preferred, loading the register in
the start state.
3. VERILOG AS A HARDWARE DESCRIPTION LANGUAGE 152

i needs to be added to sum, and the result should be stored back into sum.
Therefore, we connect the outputs of sum and i to the inputs of the adder and the
output of the adder back into the input of sum.
We arrive at the datapath shown in Fig. 5.

Figure 5. Datapath of the example design

The inc and en (enable) signals are left unconnected. The controller is tasked
with setting them to the right value at the right time.

Warning 13.3.1

Don’t make the combinatorial logic between two registers too long and compli-
cated! The signal only propagates at a finite speed to the next register input,
and if it hasn’t arrived there in time (within a single clock cycle), the data will
turn into garbage. Synthesis tools can predict such cases and will show an error
when they occur.

3.2.4. Controller. We now need to make the state machine in §3.2.2 more con-
crete: number the states, provide correct start conditions and end signals, and
control the signals going to the datapath.
After a reset, the module would start in the start state and immediately tran-
sition to the add state. In this case, start also has no activity in the datapath, so
it can be elided. We let the module start immediately on the release of the reset
signal. Numbering the states is now very easy: add gets number 0, while end gets
number 1. The module stays in the end state until it is reset once more.
To inform the code using the module that the computation has finished, we need
to send out an extra done signal. Otherwise, it would have to rely on the knowledge
of how many cycles it takes to perform the summation, which is a very bad idea.
In order to perform the summation, both the counter i and the accumulator sum
need to be enabled in the add state, so that both get a new value on every clock
cycle.
The new FSM can be seen in Fig. 6a, while the values of the control signals for
every state are displayed in Fig. 6b.
3.2.5. Translating the design into Verilog. Now that we have a datapath and a
controller, we need to translate it into Verilog. This can be done quite mechanically.
First, we need to declare the module:
3. VERILOG AS A HARDWARE DESCRIPTION LANGUAGE 153

Signal add end


i inc 1 0
sum en 1 0
done 0 1
(b) Control signals to the datapath for
(a) Final controller FSM every FSM state

Figure 6. Controller of the design

module␣sum(
␣␣␣␣input␣wire␣clk,␣input␣wire␣nrst,
␣␣␣␣input␣␣wire␣[7:0]␣max,␣// amount of numbers to calculate the sum of
␣␣␣␣output␣wire␣[7:0]␣sum,␣// the output result
␣␣␣␣output␣reg␣done␣// output signal, 'reg': assigned to using proc,
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// using combinatorial logic
);

Let’s start with the datapath, by creating two registers, of which one is a counter:
reg␣[7:0]␣i;
reg␣i_inc;␣// assigned to using combinatorial logic!
always␣@(posedge␣clk,␣negedge␣nrst)␣begin
␣␣␣␣if␣(!nrst)␣i␣<=␣8'b0;
␣␣␣␣else␣if␣(i_inc)␣i␣<=␣i␣+␣1;
␣␣␣␣else␣i␣<=␣i;
end

reg␣[7:0]␣sum;
wire␣[7:0]␣sum_in;
reg␣sum_en;␣// assigned to using combinatorial logic!
always␣@(posedge␣clk,␣negedge␣nrst)␣begin
␣␣␣␣if␣(!nrst)␣sum␣<=␣8'b0;
␣␣␣␣else␣if␣(sum_en)␣sum␣<=␣sum_in;
␣␣␣␣else␣sum␣<=␣sum;
end

Now we need to connect the wires of the datapath:


assign␣sum_in␣=␣sum␣+␣i;

Time to move on to the controller. First, define the current state:


reg␣[0:0]␣state;
reg␣[0:0]␣nextstate;␣// assigned to using combinatorial logic!

always␣@(posedge␣clk,␣negedge␣nrst)␣begin
3. VERILOG AS A HARDWARE DESCRIPTION LANGUAGE 154

␣␣␣␣if␣(!nrst)␣state␣<=␣1'b0;
␣␣␣␣else␣state␣<=␣nextstate;
end

It needs to be able to know what the next state will be:


always␣@(*)␣begin
␣␣␣␣if␣(!nrst)␣nextstate␣<=␣1'b0;
␣␣␣␣else␣case␣(state)
␣␣␣␣1'b0:␣begin␣// 'add' state
␣␣␣␣␣␣␣␣if␣(i␣<␣max)␣nextstate␣<=␣1'b0;␣// stay in 'add' if not finished yet
␣␣␣␣␣␣␣␣else␣nextstate␣<=␣1'b1;␣// finished? transition to 'end'
␣␣␣␣end
␣␣␣␣1'b1:␣nextstate␣<=␣1'b1;␣// in 'end', always stay in this state
␣␣␣␣endcase
end

The only thing left is to define the control signals to the datapath:
always␣@(*)␣begin
␣␣␣␣if␣(!nrst)␣begin
␣␣␣␣␣␣␣␣i_inc␣<=␣1'b0;
␣␣␣␣␣␣␣␣sum_en␣<=␣1'b0;
␣␣␣␣␣␣␣␣done␣<=␣1'b0;
␣␣␣␣end␣else␣case␣(state)
␣␣␣␣␣␣␣␣1'b0:␣begin␣// 'add' state
␣␣␣␣␣␣␣␣␣␣␣␣i_inc␣<=␣1'b1;
␣␣␣␣␣␣␣␣␣␣␣␣sum_en␣<=␣1'b1;
␣␣␣␣␣␣␣␣␣␣␣␣done␣<=␣1'b0;
␣␣␣␣␣␣␣␣end
␣␣␣␣␣␣␣␣1'b1:␣begin␣// 'end' state
␣␣␣␣␣␣␣␣␣␣␣␣i_inc␣<=␣1'b0;
␣␣␣␣␣␣␣␣␣␣␣␣sum_en␣<=␣1'b0;
␣␣␣␣␣␣␣␣␣␣␣␣done␣<=␣1'b1;
␣␣␣␣␣␣␣␣end
␣␣␣␣endcase
end

And we’re done.


endmodule

While the current code is hardcoded to use an 8-bit counter and accumulator, it
is possible using module parameters to make it tweakable. This is left as an exercise
for the reader.
The code above is a bit more verbose than the one in Fig. 1, however, the former
can be transformed to the latter rather mechanically using substitutions. (The code
in Fig. 1 also had to fit in the image.)
Do note that in this case, we have simply translated the given C code into
a hardware design, without thinking much about possible optimizations. Often,
introducing a new state for every C statement is not needed, with some thinking
it is possible to optimize the design for speed by a lot. For example, in C, to find
the maximum of an array, the entire array needs to be iterated over one by one.
Meanwhile, in hardware, it is possible to perform many comparisons in parallel, and
place the comparators in a tree-like datapath, speeding up the computation by a lot.
Possible optimization strategies for hardware design and their trade-offs fall outside
the scope of this tutorial.
3. VERILOG AS A HARDWARE DESCRIPTION LANGUAGE 155

3.3. What not to do. https://danluu.com/why-hardware-development-is-hard/


has the following example to illustrate the insidious details of Verilog:
Listing 13.19. Dan Luu’s bad Verilog example
always␣@(negedge␣reset,␣posedge␣clk)␣begin
␣␣␣␣if␣(reset␣==␣0)␣begin
␣␣␣␣␣␣␣␣d_out␣<=␣16'h0000;
␣␣␣␣␣␣␣␣d_out_mem[resetcount]␣<=␣d_out;
␣␣␣␣␣␣␣␣laststoredvalue␣<=␣d_out;
␣␣␣␣end
␣␣␣␣else␣d_out␣<=␣d_out␣+␣1'b1;
end

always␣@(bufreadaddr)␣bufreadval␣=␣d_out_mem[bufreadaddr];

While it looks reasonable, it has the following problems:


✓ laststoredvalue and d out mem are only written to during a reset, and
not necessarily on a clock edge. This would necessitate the use of a D-latch,
which cannot be used on an FPGA.
✓ Due to the unconventional condition of “do something while resetting”, the
synthesis tool may not figure out a latch is required in the first place.
✓ bufreadval is only supposed to change when bufreadaddr changes, not
when d out mem changes. This cannot be meaningfully represented as hard-
ware directly4. Therefore, the synthesis tool will fail here.
Another example of what not to do, sent to the author of this tutorial for trou-
bleshooting:
Listing 13.20. Bad Verilog code sent to the author for troubleshoot-
ing
always␣@(posedge␣CLK)␣begin
␣␣␣␣if␣(clkdiv␣==␣1200000)␣begin␣// Clock divider pulse generator
␣␣␣␣␣␣␣␣clkdiv␣<=␣0;
␣␣␣␣␣␣␣␣clkdiv_pulse␣<=␣1;
␣␣␣␣end␣else␣begin
␣␣␣␣␣␣␣␣clkdiv␣<=␣clkdiv␣+␣1;
␣␣␣␣␣␣␣␣clkdiv_pulse␣<=␣0;
␣␣␣␣end
␣␣␣␣if␣(BTN1)␣running␣=␣0;
␣␣␣␣if␣(BTN2)␣begin␣// Lap time measurement
␣␣␣␣␣␣␣␣lap_timeout␣=␣20;
␣␣␣␣␣␣␣␣lap_value␣=␣display_value;
␣␣␣␣end
␣␣␣␣if␣(BTN3)␣running␣=␣1;
␣␣␣␣if␣(!BTN_N)␣begin␣// Reset button
␣␣␣␣␣␣␣␣running␣=␣0;
␣␣␣␣␣␣␣␣display_value␣<=␣0;
␣␣␣␣␣␣␣␣lap_timeout␣=␣0;
␣␣␣␣end
␣␣␣␣if␣(clkdiv_pulse␣&&␣running)␣begin␣// Timer counter
␣␣␣␣␣␣␣␣lap_timeout␣=␣lap_timeout␣?␣lap_timeout␣-␣1␣:␣lap_timeout;
␣␣␣␣␣␣␣␣display_value␣<=␣lap_timeout␣?␣lap_value␣:␣display_value␣+␣1;
␣␣␣␣end
end

4A conditional data latch could be used, but that’s not what the code declares, as for that, an
explicit ‘enable’ condition should be used, not simply “whenever bufreadaddr changes”.
4. VERILOG FOR TESTBENCHES 156

The intent here is clear: first, create a slow clock signal from the system clock.
If one button is pressed, start a counter. If another is pressed, stop it. When
a third button is pressed, display the counter value from that moment instead of
continuously updating the display. If this were a software programming language,
the above code would be a perfectly reasonable way to solve the problem.
However, as the code needed to be debugged, it obviously does not work: pressing
BTN2 will not display lap value, only continuing display value. Why does this
happen? There are several reasons:
✓ it is a bit hard to follow what exactly the code does, cycle-wise, due to its
mixing of non-blocking and blocking assigns, as well as controlling many
registers inside a single procedure.
✓ display value is assigned to in two cases, using a non-blocking assignment.
If both cases were true, it would be very hard to predict what would happen.
✓ running and clkdiv pulse are assigned to in one part of the procedure,
but then used in the final if-statement. When exactly, timing-wise, will
that if-statement happen? It is hard to say.
✓ display value uses lap value in its assignment which is non-blocking, but
the assignment of display value to lap value happens in a blocking man-
ner. What would happen if both conditins of the respective if-statements
are true?
✓ The reset mechanism is synchronous and happens in the middle of the logic.
If it weren’t for the blocking assignment, it would be partly ignored in the
next if-statement.
In conclusion, writing Verilog as if it were software will make it very hard to
reason about its function and timing, and it will very often end up not working at
all.

4. Verilog for testbenches


4.1. Introduction. While we have now covered how to write Verilog for hard-
ware synthesis, we currently have no way to test the code, aside from running it
on real hardware, which can be tedious to debug. Verilog can also be used to cre-
ate testbenches for testing the functionality of modules meant for hardware use,
inside the abstract Verilog simulator. Unlike the synthesizable hardware modules,
testbenches use a number of non-synthesizable Verilog features.
4.2. Procedural code and delays. One big difference with hardware Verilog
modules is that procedural code is used very often: “first prepare a parameter input,
and only then send a start signal, then wait until the module has finished”.
One important extra feature testbenches (and non-synthesizable Verilog in gen-
eral) can use, is delays. A delay can be inserted between two statements in a proce-
dure, by prefixing the statement by a # followed by the number of time units. The
units-per-second rate is determined by a ‘timescale directive at the very top of the
file.
With the availability of procedural code, it is now possible to use if, for and
while in a procedural way, eg. to repeat a test with different parameters, or to wait
until a component has finished computing.
A first example:
Listing 13.21. procedural code example
integer␣done;
4. VERILOG FOR TESTBENCHES 157

// ...

param␣=␣16'h1234;
#1␣start␣=␣1'b1;␣// happens one time unit after the `param' assignment

done␣=␣0;
while␣(done␣==␣0)␣begin
␣␣␣␣#1␣done␣=␣status␣&␣1'b1;␣// wait a bit
end

4.3. initial procedures. Besides always, there is another type of procedure:


initial. As the name suggests, instead of running code forever, code in such a block
will only run at startup. While this cannot be synthesized (and manual initialization
is required on hardware), this is where most test code is placed , as it only needs
to run once. Code snippet Listing 13.21, for example, would be placed in such an
initial block.

4.4. $display and $finish. Verilog also has magic non-synthesizable func-
tions for controlling the simulator. We cover two of them here: $display and
$finish. The former is used in printf-style debugging, while the latter signals the
simulator that the simulation can stop, as all tests have been performed.
For example, the following code could be appended to Listing 13.21:

Listing 13.22. example use of display and finish


$display("The␣result␣is␣0x%x",␣result);
if␣(result␣!=␣16'h5678)␣begin
␣␣␣␣$display("TEST␣FAILED!");
end
$display("===␣End␣of␣testbench␣===");
$finish();

4.5. Clock generation and reset. One common boilerplate component of


testbenches is a clock generator and reset signal control, as there is no source of
these to use. Luckily, this is fairly easy to do:

Listing 13.23. clock generation


always␣begin
␣␣␣␣#5␣clk␣=␣~clk;
end

This code will generate a clock signal with a period of 10 time units. Handling
the reset is not much more difficult:

Listing 13.24. reset logic


initial␣begin
␣␣␣␣nrst␣=␣0;␣// assert reset
␣␣␣␣clk␣=␣0;␣// reset clock (no initial value given in clkgen)

␣␣␣␣param␣=␣0;␣// reset various top−level regs

␣␣␣␣#50␣nrst␣=␣1;␣// release reset


end
4. VERILOG FOR TESTBENCHES 158

As the clock is controlled from two procedures, this is a bit cluttered. Instead,
we can also use a construct such as this one:
Listing 13.25. clock generation alt
initial␣begin
␣␣␣␣clk␣=␣0;
␣␣␣␣while␣(1)␣#5␣clk␣=␣~clk;␣// or "forever #5 clk = ~clk;"
end

4.6. Tasks. Aside from functions, Verilog also has subroutines, called tasks.
These are allowed to have side effects and contain multiple statements one after
another, which is why they are generally only used in testbenches. For example:
Listing 13.26. Example task
task␣start_module;
␣␣␣␣input␣[7:0]␣invalue;

␣␣␣␣reg␣donecheck;

␣␣␣␣begin
␣␣␣␣␣␣␣␣param␣=␣invalue;␣// prepare parameter value
␣␣␣␣␣␣␣␣#10␣start␣=␣1;␣// 'param', 'start' and 'done' are
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣// connected to the module performing the task

␣␣␣␣␣␣␣␣donecheck␣=␣0;
␣␣␣␣␣␣␣␣while␣(donecheck␣==␣0)␣begin␣// wait until the module has finished
␣␣␣␣␣␣␣␣␣␣␣␣#10␣donecheck␣=␣done;
␣␣␣␣␣␣␣␣end
␣␣␣␣end
endtask

A task can then be used as follows:


Listing 13.27. Task usage
$display("starting␣summation...");
start_module(8'd10);
$display("result:␣%d",␣module);

4.7. Example testbench. Now we can tie everything together to write a test-
bench for the code from §3.2.
First, define a time scale, and some macros for timing:
`timescale␣1ns␣/␣1ps

`define␣CLK_HALF␣␣␣␣5
`define␣CLK_PERIOD␣10
`define␣RST_PERIOD␣50

A testbench is a module with neither inputs nor outputs:


module␣sum_tb();

Now we need to define regs for a number of signals controlled by the testbench:
reg␣clk,␣nrst;

reg␣[7:0]␣max;
4. VERILOG FOR TESTBENCHES 159

Time to import the module to be tested. Such a module is often called the
Device Under Test (DUT) or Unit Under Test (UUT).
wire␣[7:0]␣sum;
wire␣done;

sum␣dut(clk,␣nrst,␣max,␣sum,␣done);

Add the clock generation logic:


initial␣begin
␣␣␣␣clk␣=␣0;
␣␣␣␣forever␣#`CLK_HALF␣clk␣=␣~clk;
end

Now we can start on the actual testing code. This code always starts with some
reset logic:
integer␣i;
integer␣donecheck;

initial␣begin
␣␣␣␣nrst␣=␣0;␣// assert reset
␣␣␣␣max␣=␣0;␣// reset various top−level regs

␣␣␣␣#`RST_PERIOD␣// give everything some time to settle

Finally, it is time to write the actual test code:


␣␣␣␣for␣(i␣=␣1;␣i␣<=␣10;␣i␣=␣i␣+␣1)␣begin␣// do a number of tests
␣␣␣␣␣␣␣␣$display("Test␣%i/10:",␣i);

␣␣␣␣␣␣␣␣#`CLK_PERIOD␣max␣=␣i;␣// prepare parameter


␣␣␣␣␣␣␣␣#`CLK_PERIOD␣nrst␣=␣1;␣// start

␣␣␣␣␣␣␣␣donecheck␣=␣0;
␣␣␣␣␣␣␣␣while␣(donecheck␣==␣0)␣begin␣// wait until the module has finished
␣␣␣␣␣␣␣␣␣␣␣␣#`CLK_PERIOD␣donecheck␣=␣done;
␣␣␣␣␣␣␣␣end

␣␣␣␣␣␣␣␣if␣(sum␣==␣(i␣+␣1)␣*␣i␣/␣2)␣begin␣// check the end result for correctness


␣␣␣␣␣␣␣␣␣␣␣␣$dislay("Expected:␣%d,␣result:␣%d.␣ERROR!",␣(i␣+␣1)␣*␣i␣/␣2,␣sum);
␣␣␣␣␣␣␣␣␣␣␣␣$finish();
␣␣␣␣␣␣␣␣end␣else␣begin
␣␣␣␣␣␣␣␣␣␣␣␣$dislay("Expected:␣%d,␣result:␣%d.␣OK",␣(i␣+␣1)␣*␣i␣/␣2,␣sum);
␣␣␣␣␣␣␣␣end
␣␣␣␣end

␣␣␣␣$display("All␣test␣succeeded.");
␣␣␣␣$finish();
end

We finish by closing the module definition.


endmodule
CHAPTER 14

Introduction to Networking

1. Networking Essentials
The idea of the COMMS module is to connect all the nodes by building an IP-
based network. Each node in this network should have an IP address. Furthermore,
each node (Raspberry Pi, Zybo) should be reachable by the ground station laptop.
The most important thing is that the two routing nodes (the Raspberry Pi’s) know
exactly how to route the data traffic to the correct destination. This section outlines
a few important concepts for this module.

1.1. IP Address. An IP(v4) address is composed of four bytes. In graphical


form, it can be written as B1.B2.B3.B4 where B1 → B4 are integers between 0 and
255 e.g, 192.168.1.1. A few IP addresses are reserved or have a special meaning.
For example, 0.0.0.0 is the full internet or all the traffic. Similarly, the IP address
127.0.0.1 is the local machine, also known as the local loopback. The network
192.168.B3.B4/16 and 10.B2.B3.B4/8 are reserved for home and private networks
respectively.
The first part of an IP address identifies a network on the internet, and the
second part addresses a machine. A network mask is defined to understand which
part of the IP is the network number and which is the machine number. For example,
if you have address 192.168.1.1 with network mask 255.255.255.0, it means that
the first 3 bytes of the IP address are the network number, and the last byte is the
machine address. On the other hand, if the network mask is 255.255.0.0, only
the first two bytes are the network number, and the last two indicate the machine
address. For more information, visit this link.

1.2. IP Assignment. A simple way of assigning IP is a static IP assignment.


Static IP addressing offers various advantages and disadvantages. For example,
it allows you to assign a preferred IP to your machine. In contrast, two devices
with the same IP cannot exist reliably on the same network. The second way is
to use DHCP (Dynamic Host Configuration Protocol), which dynamically assigns
a new IP address to each node joining the network. In general, devices such as
laptops and mobiles use DHCP by default. The machine’s network interface on
which the DHCP server is running must be configured appropriately for DHCP to
work. Having multiple machines with a DHCP server running without fixing the
configuration of at least one of them typically leads to inconsistent IP addresses and
no connectivity. The selection of static addressing or DHCP entirely depends on
your network requirements and design.

2. Sockets
Sockets aim to provide communication between two different processes on single
or multiple devices. In general, a socket identifies a specific application in a system
160
2. SOCKETS 161

Figure 1. The socket API and TCP/IP stack

and enables communication to it. In addition, sockets provide an Application Pro-


gramming Interface (API) to the network stack, usually between the transport and
application layer, that helps communicate between different processes, as shown in
Figure 1.
Figure 1 shows that the sockets API sits between the application layer and
transport layer. It allows a programmer to initiate a connection and send messages
between two processes without worrying about transmission control or framing. The
figure shows three processes/applications (P1, P2, P3) running on the application
layer. Sockets uniquely identify each application using an IP address and a port
number. The IP address uniquely identifies a machine, while the port identifies the
process on the machine. For example, the different processes running on the same
system will have similar IP addresses but different port numbers. Therefore, the
combination of IP and port uniquely identifies a process. There are different types
of sockets, however, this project’s scope is only limited to two types: stream sockets
and datagram sockets. The choice of socket type and underlying protocol depends
on the application type. See this link for an in-depth comparison of features of TCP
and UDP.
2.1. Stream sockets. Stream sockets use the Transmission Control Protocol
(TCP) for communication or sending messages. TCP requires a handshake at the
start of each transmission to establish a connection. It provides a reliable connection
and ensures that every transmitted message reaches to host. Furthermore, TCP
makes sure that all the messages are transmitted in the correct order.
2. SOCKETS 162

Figure 2. Flow diagram of TCP sockets (client/server)

2.1.1. Connection flow. The overall connection flow of stream sockets is listed
as follows and showed in Figure 2:
(1) The socket() function creates a socket for communication and returns a
descriptor used for function calls.
(2) After having a descriptor, the server uses bind() to associate a unique
name/address to each socket. In this way, client(s) can discover the server.
(3) listen() puts the socket in the listening state that shows that the server
is ready to accept connection requests from the client(s).
(4) connect() allows a client to establish a connection with a server.
(5) accept() is used by the server to accept request from the client(s).
(6) After the connection establishment, the client or server can exchange infor-
mation (send / receive).
(7) close() is called whenever a client or server wants to halt its operations.
2.2. Datagram sockets. Datagram sockets use the User Datagram Protocol
(UDP) for sending messages. In contrast to TCP, UDP is a connectionless proto-
col, where the messages are sent to other hosts without connection establishment.
Therefore, it offers faster transmissions speed as compared to TCP. However, using
UPD, messages can be lost or received out of order.
2.3. Hello World Example. A Hello World example is listed below to un-
derstand the concept of sockets.
Listing 14.1. A server that echoes back the client data
# echo server.py
import␣socket

HOST␣=␣'0.0.0.0'␣␣␣␣␣␣␣␣␣␣# Symbolic name meaning all available interfaces


PORT␣=␣12345␣␣␣␣␣␣␣␣␣␣␣␣␣␣# Arbitrary non−privileged port
s␣=␣socket.socket(socket.AF_INET,␣socket.SOCK_STREAM)
2. SOCKETS 163

s.bind((HOST,␣PORT))
s.listen(1)
conn,␣addr␣=␣s.accept()
print('Connected␣by',␣addr)
while␣True:
␣␣␣␣data␣=␣conn.recv(1024)
␣␣␣␣if␣not␣data:␣break
␣␣␣␣conn.sendall(data)
conn.close()

Listing 14.2. A client sends Hello World to the server


# echo client.py
import␣socket

HOST␣=␣socket.gethostname()␣␣# The address of the server


PORT␣=␣12345␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣# The same port as used by the server
s␣=␣socket.socket(socket.AF_INET,␣socket.SOCK_STREAM)
s.connect((HOST,␣PORT))
s.sendall(b'Hello,␣world')
data␣=␣s.recv(1024)
s.close()
print('Received',␣repr(data))

Explanation: In this example, the server echos back all the information it re-
ceives from the client. At the start, socket.socket() creates a new socket using
the given address family. You can define a custom host (IP / hostname) and port for
the server. Using 0.0.0.0 as a host allows the server to accept connections on all
available IPv4 interfaces. You can define a port in the range 1-65535, however it is
recommended to take a port higher than 1024 to not intefere with system services.
The bind() API binds the socket to a specific port and IP. Next, the listen()
allows the server to listen to client requests and accept them using accept(). When
receiving a connection, accept() creates a tuple for the client containing its host
address and port number. Finally, the while loop listens to the client’s data using
conn.recv() and echos it back using sendall(). The close() closes the socket
after receiving the data block.
The client creates a socket and connects to the server using the connect function.
After that, sendall() sends a message to the server, while recv() gets the data
from the server.
Tutorial 14.2.1 — Running the Hello World sockets example
(1) Ensure that Python 3 is installed for this tutorial (check with python
--version).
(2) Create the client.py and server.py files: either copy the code from the
above listings or use the provided files in EAGLE-Students at
Module software/COMM.
(3) Run the server by opening a terminal, navigating to the appropriate direc-
tory and executing python server.py.
(4) Run the client by opening a terminal, navigating to the appropriate direc-
tory and executing python client.py.
(5) The server terminal should output
Connected by (’B1.B2.B3.B4’, RANDOM PORT), the client terminal should
output Received b’Hello, world’.
(6) Note that you can use CTRL-C to halt the programs.
2. SOCKETS 164

2.4. Further reading. Read the following sources for an in-depth discussion
about sockets:
✓ https://docs.python.org/3/howto/sockets.html
✓ https://realpython.com/python-sockets/
Also see the asyncio streams API (Python docs) for a more modern and higher-
level API than the sockets package.
CHAPTER 15

Wireless power transfer theory

1. Introduction
A wireless power transfer (WPT) system transmits electrical energy without a
binding infrastructure. There is a transmitter device that generates an electromag-
netic field which reaches across space to a receiver device connected to a load. The
transmitter and receiver roles are played by two copper coils that transfer power by
induction. Inductive coupling is maybe the most widely used wireless technology:
its applications include, for instance, charging phones or electric toothbrushes, in-
ductive cooking... However, it is only effective if the distance between the inductors
is short enough. The efficiently exchange of power over medium distances is based
on the concept of electrical resonance, which is described in Section 2.
There are different ways to achieve resonance, depending on the relative position
of inductors and capacitors and on the number of coils used. Sections 3 and 4 provide
a description of some possibilities than can be used to design the system. Finally,
Section 5 provides some information on how to calculate and improve the efficiency
of the system.

2. Wireless magnetic resonant power transfer: some theory


Electrical resonance in a RLC-circuit is a phenomenon that occurs when the
inductive and capacitive reactance have the same magnitude, causing the mutual
cancellation of the imaginary parts of impedances (or admittances). This condition
can be written as ωL = 1/ωC and implies that the impedance perceived by the
generator is minimised and purely resistive. In practice, resonance is achieved when
the supply voltage frequency corresponds to the resonance frequency fr [Hz] (angular
resonance frequency: ωr = 2πfr [rad/s]:
ωr 1
(2.1) fr = = √ [Hz]
2π 2π LC
where C [F] is the capacitance and L [H] is the inductance.
To enable optimal WPT, the receiver and transmitter coils must work at the
same resonant frequency. This is possible by tuning their L and C values. The in-
ductance of a circular coil of radius r [m], wire bundle-thickness a [m] and N number
of turns can be calculated with the following equation:
 
2 8r a
(2.2) L = µ0 rN ln − 1.75 [H], assuming ≪ 1
a r

with µ0 = 4π10−7 [H/m] the vacuum permeability. The capacitance, on the


other hand, can be tuned in this project by ordering and adding capacitors to the
circuit.
165
3. SERIES VS PARALLEL RESONANCE 166

Another parameter required in the calculations is the mutual inductance between


two coils x and y: Mxy . Its formulation is:

µ0 1
Z Z
(2.3) Mxy = dlx dly ,
4π γx γy D

where D [m] is the distance between the two coils and γx , γy are the curves followed
by the wires. For circular coils, with average radius rx and ry and number of turns
Nx and Ny , we can simplify to the following equation:

Nx Ny
(2.4) Mxy ≈ π · µ0 · (rx ry )2 · .
2D3
The quality factor Q of a resonant circuit is typically used to assess the perfor-
mance of the power transfer. It is defined as the ratio between the power stored
Pstored in the system to the power dissipated Pdissipated in the circuit, i.e.

Pstored I 2χ χ
(2.5) Q= = = ,
Pdissipated I 2R R

with χ the capacitive or inductive reactance at resonance and R the series re-
sistance. Note that this formula is applicable to series resonant circuits and parallel
resonant circuits if the resistance is in series with the inductor/coil, which is the case
in practical applications. The expression of Q depends on the circuital configuration
of the inductance (given by the coil) and the capacitor: they can be connected ei-
ther in series or parallel both on the transmitter and on the receiver side. Section 3
discusses the differences between these two configurations. The coupling coefficient
k (or factor) quantifies the portion of the generated magnetic flux that reaches the
receiver coil. Theoretically, its value ranges from 0 to 1 (k = 0 if no magnetic flux
is transferred; k = 1 if all generated magnetic flux reaches the receiver coil). The
product of the Q factor and the mutual coupling coefficient, k, is proportional to
the efficiency of the system,
√ η ∝ kQ. If the coils have two different Q factors, the
efficiency satisfies η ∝ k Q1 Q2 . Thus, the higher Q is, the higher η becomes [Hui16].
It is important to note that the system energy efficiency ηsys is (in general)
different from the transmission efficiency ηtx . They are defined as:
PL PL
(2.6) ηsys = , ηtx = ,
PS Pin
where PS is the total power provided by the AC power source (including the power
lost in the source resistance RS . Pin is the power available from the terminals of the
AC power source (excluding the power lost in the source resistance RS ), and PL is
the power consumed by the load. Furthermore, a more practical parameter to assess
power transfer efficiency taking into account the physical features of the system is
described in Section 5.

3. Series vs parallel resonance


Choosing which configuration to adopt is far from trivial. This section describes
briefly the main differences between series and parallel configurations and provides
support to a proper design decision. Further details can be found in [JP14].
3. SERIES VS PARALLEL RESONANCE 167

3.1. Series circuit. In a series RLC circuit, the Q factor is defined as:
r
1 L ωr L 1
(3.1) Qseries = = = .
R C R ωr CR
For frequencies higher than the resonance frequency, the generator sees the circuit
as a purely ohmic-inductive load, as the inductive reactance prevails. The opposite
occurs if the frequency is lower than the resonance frequency. At resonance, the
current reaches its maximum value, as the impedance value is minimum (I = V /Z).
See Figure 1 left.

Figure 1. Current versus frequency for a series


(left) and parallel (right) resonant circuit. Source:
https://www.slideshare.net/yadavsurbhi/eee-48

3.2. Parallel circuit. In a parallel RLC circuit, the Q factor is defined as:
r
C R
(3.2) Qpar = R = = ωr CR .
L ωr L
Therefore, the lower the parallel resistance is, the lower Q is. For frequencies higher
than the resonance frequency, the generator sees the circuit as an ohmic-capacitive
load, as the capacitive reactance prevails. The opposite occurs if the frequency is
lower than the resonance frequency. At the resonance frequency, the voltage reaches
its maximum value, as the impedance is also at its maximum value (V = ZI). On
the other hand, the current is at its minimum, as displayed in Figure 1 right.
Summarizing, both series and parallel resonant circuits behave as a purely resis-
tive load if operated at the resonant frequency fr . The behaviour when the frequency
is lower/higher than fr is opposite in the two cases and it holds Qseries = Q−1 par .

3.3. Series vs parallel circuit at the transmitter side. It can be proven


that in order to maximise the efficiency of power transmission, the current flowing
through the transmitter coil should be maximised. This is intuitive as a higher
current implies a greater generated magnetic field, resulting in better coupling.
On the transmitter side, represented in Figure 2 left, the current through the
coil for the series configuration, ITs , is given by the following equation:
VG
(3.3) ITs ≈ ; if RG ≫ RT
RG
where RG is the source resistance and RT the coil resistance. If the condition
RG ≫ RT is verified, the Q factor must be high, as it refers to a high-efficient
low-loss coil.
3. SERIES VS PARALLEL RESONANCE 168

Figure 2. Transmitter coil in series resonance configuration (left),


transmitted coil in parallel resonance configuration (middle) and
equivalent transmitter coil parallel circuit (right). Source:[JP14]

To ease the study of the parallel circuit in Figure 2 middle, we analyse the
equivalent circuit in Figure 2 right. Such a transformation is possible as the circuit
is operating in resonance conditions. In resonance, the resistance seen by the source
terminal amounts to Q2T RT , and the voltage across this resistance can be expressed
as follows.
Q 2 RT
(3.4) VT = 2 T VG ≈ VG , if RG ≪ Q2T RT .
Q T RT + RG
Again, this situation occurs if the QT factor is high. Thus, the current through
the coil is:
VT VG
(3.5) ITp = ≈ .
ıωr L ıωr L

From the equations above (with ı = −1 the imaginary unit), it can be inferred
that, in high quality coils:
✓ if RG > ωr L ⇒ |ITs | < |ITp | ⇒ the parallel configuration is preferred;
✓ if RG < ωr L ⇒ |ITs | > |ITp | ⇒ the series configuration is preferred.

3.4. Series vs parallel circuit at the receiver side. Figure 3 left shows how
the series configuration at the receiver coil looks, while Figure 3 middle represents
the parallel configuration.

Figure 3. Receiver coil in series resonance configuration (left), re-


ceiver coil in parallel resonance configuration (middle) and equivalent
receiver coil parallel circuit (right). Source:[JP14]

The voltage induced at the receiver coil thanks can be modeled as the source
voltage VI . In resonance conditions and series configuration, the current flowing
through the load resistance RL is given by:
VI
(3.6) ILser = ,
RL + R R
4. TWO-COIL VERSUS FOUR-COIL SYSTEMS 169

where RR is the resistance of the receiving coil. The parallel case can be studied
with a transformation analogue to that on the transmitter side (see Figure 3 right).
Taking into account that at resonance frequency QR = ωr CR RL , the parallel current
reads
VI
(3.7) ILpar =
RL,S + RR
RL
with RL,S =
Q2R
+1
The power transmitted to the load is:
(3.8) Pser = IL2 ser RL or Ppar = IL2 par RLs
For good quality coils, RR ≪ RL and RR ≪ RL,S . Hence, we can write
VI2 VI2
(3.9) Pser = or Ppar =
RL RL,S
Since RL,S < RL by definition, the parallel configuration allows to deliver more
power.
Please note that all the previous assumptions are valid for high values of Q. If
this condition does not hold, then the optimal configuration depends on the values
of the various resistances.
Figure 4 shows the four topologies which can result as a combination of series
and parallel circuits on transmitter and receiver side. Table I in [Sal+09] reports
the equations to calculate their parameters. The total impedance of the system and
the expressions needed to calculate the efficiency are defined in [Sal+09]. They are
also reported in 5.

Figure 4. Different transmitter/receiver topologies and parame-


ters [Sal+09]

4. Two-coil versus four-coil systems


The number of coils that are used to design and build a WPT system can vary.
This section presents a two-coil system and a four-coil system, such as those repre-
sented in Figures 5 and 6, respectively.
4. TWO-COIL VERSUS FOUR-COIL SYSTEMS 170

Figure 5. Two-coil induction power transfer system.

Figure 6. Four-coil induction power transfer system.

The circuit models for these two- and four-coil systems are provided as well in
Figures 7 and 8, taken from [HB12].
Both circuit models can be represented by an input impedance Zin . Furthermore,
a good WPT system is characterised by high values of the real part of Zin .
The input impedance of the system Zin for the two-coil system (see Figure 7
reads:

ω 2 M12
2
(4.1) Zin = Z1 + − RS ,
Z2

which is actually nothing but ZT,SS in Table 1. ω is the angular frequency, RS is the
inner resistance of the source, Zx is the equivalent
√ impedance of coil x (if x = 1, the
source; if x = 2, the load), and M12 = k12 L1 L2 is the mutual inductance between
coils 1 and 2. We can write:
1
(4.2) Z1 = RS + R1 + ıωL1 + ,
ıωC1
1
(4.3) Z2 = RL + R2 + ıωL2 + .
ıωC2

A four-coil system (Figure 8) is composed of four resonant circuits corresponding


to the four coils. These coils are linked through a magnetic field characterized by
three coupling coefficients k12 , k23 , k34 . Note that the coupling between the power
source (circuit 1) and the receiving coil (circuit 3), and the coupling between the load
(circuit 4) and the transmitter coils (circuit 2) are very weak. The corresponding
coupling coefficients k13 k24 can thus be neglected. The input impedance for a
four-coil system is:

ω 2 M12
2 (Z Z + ω 2 M 2 )
3 4 34
(4.4) Zin = Z1 + 2 Z + ω 2 M 2 Z − RS
Z2 Z3 Z4 + ω 2 M34 2 23 4
4. TWO-COIL VERSUS FOUR-COIL SYSTEMS 171

with
1
(4.5) Z1 = RS + R1 + ıωL1 + ,
ıωC1
1
(4.6) Z2 = R2 + ıωL2 +
ıωC2
1
(4.7) Z3 = R3 + ıωL3 +
ıωC3
1
(4.8) Z4 = RL + R4 + ıωL4 +
ıωC4

Figure 7. Circuit model of a two-coil system [HB12].

Figure 8. Circuit model of four-coil system [HB12].

Two-coil WPT systems are appropriate for short-range applications, but the
four-coil WPT system is preferred for mid-range applications. The real part of
the input impedance ℜ(Zin ) decreases with a decreasing coupling coefficient k12 , see
Fig. 9 left. This is because the real part of the input impedance of the system ℜ(Zin )
in Figure 8) for two-coil WPT dramatically decreases if the coupling coefficient k12
decreases (see Figure 9 left). As will be discussed in Section 5, k12 is inversely
proportional to the distance between receiver and transmitter. Thus, the nearer the
two components are, the higher the efficiency is. In the four-coil scenario we must
have a look at ℜ(Zin ) as a function of the coupling coefficient k23 (see Figure 9 right).
ℜ(Zin ) is relatively small for short distances but increases with decreasing k23 until
reaching a maximum value. For larger distances, the value of ℜ(Zin ) stops having
a physical meaning (invalued area). The over coupled, critically coupled and under
coupled areas highlighted in Figure 9 right, refer to the concept of frequency splitting,
5. EFFICIENCY OF POWER TRANSFER 172

Figure 9. Variation of Zin with distance for two-coil (left) and four-
coil (right) systems.

explained in Section 5. Typically, four-coil systems present higher efficiencies, as they


are less sensitive to the load impedance. To maximize efficiency η, the minimization
of the load impedance is required. More details on transmission coefficient and
efficiency differences between two- and four-coil systems can be found in Section 5.

5. Efficiency of power transfer


Efficiency is defined as the ratio between the transferred and the input power:
Pout RL · IL2
(5.1) η= = ,
Pin V1 · I 1
where IL and VL are, respectively, the current and the voltage across the load; I1 and
V1 are, respectively, the current provided from the source and the source voltage.
Tables 1 and 2 report values of impedances and currents that allow to calculate
the efficiency for a two-coil system of any of the topologies reported in Figure 4.
Table 1. Total impedance for the four basic topologies: series-series
(SS), series-parallel (SP), parallel-series (PS) and parallel-parallel
(PP) [Sal+09].

total impedance ZT
ω2M 2
 
1
SS R1 + ı L 1 ω − +
C1 ω R2 + RL + ı(L2 ω − C12 ω )
ω2M 2
 
1
SP R1 + ı L 1 ω − +
C1 ω R2 + ıL2 ω + 1+ıRRLLC2 ω
!−1
ω2M 2
PS R1 + ıL1 ω + ıC1 ω +
R2 + ıL2 ω + 1+ıRRLLC2 ω
 −1
1
PP ıC1 ω + ω 2 M 2 (1+ıRL C2 ω)

R1 + ıL1 ω + RL +(R2 +ıL2 ω)(1+ıRL C2 ω)
5. EFFICIENCY OF POWER TRANSFER 173

Table 2. Electrical parameters for the four basic topologies: series-


series (SS), series-parallel (SP), parallel-series (PS) and parallel-
parallel (PP) [Sal+09]

SS SP PS PP

Ip I1 I1 I1 − ıωC1 V1 I1 − ıωC1 V1
ıωM Ip ıωM (1+ıRL C2 ω)Ip ıωM Ip ıωM (1+ıRL C2 ω)Ip
Is R2 +RL +ı(L2 ω− C1 ω ) RL +(R2 +ıL2 ω)(1+ıRL C2 ω) R2 +RL +ı(L2 ω− C1 ω ) RL +(R2 +ıL2 ω)(1+ıRL C2 ω)
2 2
Is Is
I2 Is Is
1 + ıRL C2 ω 1 + ıRL C2 ω
IC2 Is ıRL C2 ωI2 Is ıRL C2 ωI2
Is IC2 Is IC2
VC2
ıC2 ω ıC2 ω ıC2 ω ıC2 ω
VL RL I 2 VC2 RL I 2 VC2

As previously stated, source and load resistances cause the resonators to have a
lower quality factor Q, which implies that the efficiency of power transfer is also low.
Four-coil systems allow to separate these resistances from the resonators, providing
high-Q resonant coupling. Figure 10 compares a two-coil low-Q resonant coupling, a
four-coil high-Q resonant coupling and the classic inductive coupling by means of the
transfer coefficient S21 [dB], a parameter sometimes used instead of η to describe the
system (see references [HB12] and [Run15] for more information on this parameter).
It is clear that inductive coupling provides the lowest values of S21 for a great part
of the frequency range and in particular in resonance conditions (resonance occurs
at 8 MHz for the system considered in Figure 10). It can also be noted that in the
classic inductive case the efficiency decreases while the frequency increases, while it
presents a bell-like shape in resonant coupling.
Furthermore, the power transfer in traditional inductive coupling decreases with
the cube of the distance 1/D3 , making it unsuitable for even relative short distance
power transfer like the 20 cm that separate the drone and the load in landed position.
Finally, the high-Q resonant coupling presents the highest efficiency around the
resonance frequency. If the source operates at difference values of ω, however, it
might be outperformed by low-Q resonant coupling. Thus, attention must be paid
when tuning the system and real-life operating conditions should be taken into
account. If a nearly perfect tuning is impossible, it might be easier to achieve higher
efficiencies with a lower quality system.
Since all other parameters are fixed and can be calculated (they depend on the
geometry and physical characteristics of the coils), the efficiency of the system can
be expressed as a function of the operating frequency and the distance between the
transmitting and receiving coils. Figure 11 represents the variation of the efficiency
values (from low/blue to red/high) as a function of these two parameters.
5. EFFICIENCY OF POWER TRANSFER 174

Figure 10. Efficiency comparison of inductive power transfer sys-


tems [HB12].

5.1. Frequency splitting. Figure 11 shows that for large distances, the maxi-
mum efficiency values that can be obtained are relatively small (under-coupled zone)
but there is only one frequency value that maximises the efficiency of power trans-
mission. At a given distance, the critically coupled condition occurs: there is only
one frequency that maximises the efficiency, that can reach the highest value for the
system. This distance is the maximum distance at which power can be transferred
with the highest efficiency value.

Figure 11. Efficiency as a function of the distance (k23 ) and the


frequency f [Sam+13].

On the other hand, if the distance diminishes, the system works in over-coupled
conditions: the maximum efficiency is achieved for two different values of the fre-
quency, neither of which corresponds to the one in the critically coupled condition.
This phenomenon is known as frequency splitting and the frequency at which the
resonant efficiency peaks converge is called fundamental frequency of the system. In
over-coupled regime, the magnetic flux shared by receiver and transmitter exceeds
that required to power the load. In the under-coupled regime, the magnetic flux
from the transmitter cannot completely reach the receiver.
In a fixed distance system, the goal is to obtain the critically coupled condition.
However, if the distance between transmitter and receiver changes during the power
5. EFFICIENCY OF POWER TRANSFER 175

transfer process, the performance significantly worsens. To prevent this from hap-
pening, adaptive circuits are deployed in applications that require to transfer power
in motion. Some are described in Section 5.2.

5.2. Adaptive circuits to maximise efficiency. In the previous sections, it


was shown that the variation of the distance D between transmitting and receiving
coil compromises the transmission efficiency of the system. To overcome such prob-
lem, the designer can resort to adaptive circuits, such as adaptive frequency control
and impedance matching control, that are described hereafter.
We provide a very succinct explanation of the working principles of such meth-
ods. Plenty of practical implementations can be found in the literature. To develop
a control circuit of this type, we refer you to [HB12]; [Sam+13]; [Bar+15]; [Luo+17].
It is worth mentioning that adaptive methods can also be combined. For instance,
in [Luo+17], authors integrate frequency control with impedance matching on the
receiver circuit.
5.2.1. Maximizing efficiency with frequency control. If the operating frequency
of a system can be dynamically tuned so that it automatically adjusts to distance
variations and misalignment between transmitter and receiver coil, the maximum
possible efficiency can be achieved even while in motion, as long as the device stays
within its frequency working range. The frequency range is limited by the coil
characteristics. Besides, the frequency should not exceed the maximum switching
frequency of the electronic components of the circuit.
Different approaches can be adopted. Basically, you could measure voltage and
current at the transmitter side and develop an algorithm to adjust the frequency,
function of these measurements and the distance between the coils. To that end,
efficiency equations might need to be integrated. An example of an adaptive circuit
for frequency control is shown in Figure 12.

Figure 12. Example of circuit for adaptive frequency control


[HB12].

5.2.2. Maximizing efficiency with impedance matching control. As in the case


of frequency control, the principle is to calculate the distance variations based on
electrical measurements and adapt the impedance of the coils accordingly, to obtain
5. EFFICIENCY OF POWER TRANSFER 176

resonance conditions. Sometimes, impedance matching can be more flexible than


frequency matching, which can only operate in a limited range.
Also, one of the major advantages of impedance matching control is that it
allows to achieve reasonably high efficiencies operating at a constant frequency,
which is of great help in many real-life applications. Impedance matching can be
implemented on the receiver side, transmitter side or even both. One example of
adaptive impedance matching circuit (on both sides) is shown in Figure 13. The
impedance of the circuit can be modified by adding additional capacitors or inductors
to the system by means of controllable switches. The most important drawbacks of
integrating an impedance matching circuit on the receiver side are that it requires
several circuits and components that need more space (space is a particularly strict
requirement in many embedded system, that have to be small in size) and that it
consumes more energy, thus delivering less power to the load.

Figure 13. Example of circuit for adaptive impedance matching on


both sides [Luo+17].
CHAPTER 16

Autopilot Framework

The autopilot framework is one of the main components of the drone platform.
It contains C++ code that performs multiple important tasks:
(1) Initialize the hardware (e.g. map the FPGA memory, configure the IMU).
(2) Provides access to the hardware through user-space drivers (e.g. RC inputs,
PWM outputs, LEDs, buzzer, IMU, sonar ...).
(3) Handle input from the IMU, fuse the sensors to get a reliable orientation
estimate. Handle input from the sonar and filter it.
(4) Run all control algorithms (observers and controllers) with the latest sen-
sor measurements as inputs, and send the resulting control signals to the
motors.
(5) Handle input from the RC, keeping track of different flight modes.
(6) Provide socket connections for communication to other modules.
(7) Log flight data for debugging and visualization in the GUI.
(8) Receive position measurements from IMP and position targets from CRYPT.
You can find detailed documentation at https://eagle-gitlab.pages.esat.kuleuven.
cloud/EAGLE-students/main/Doxygen/index.html (or your team’s fork).

1. Overview
The Autopilot software runs as a high-priority process on the embedded Linux
system of the Zybo. It is written in C++, and is cross-compiled using CMake and
the provided toolchain (Tut. 26.3.2).
The main task of the Autopilot system is to send appropriate pulse-width mod-
ulation (PWM) signals to the electronic speed controllers (ESC) (cf. Ex. 25.2.2)
in order to control the drone’s attitude, altitude, and position. The Autopilot has
three flight modes manual, altitude-hold and autonomous (cf. Ch. 4).
The provided code will initialize the drone platform. It has several inputs: the
remote control (RC), the inertial measurement unit (IMU), and the sonar. Each of
these inputs is processed in its own way. We list some conventions below. The RC
signals are processed into a quaternion reference for your controller, representing the
angles of the RC’s joystick. The IMU measures the rate of rotation along the three
major axes and measures the 3D acceleration (mostly consisting of gravity). Using
these six measurements, the Madgwick algorithm in the AHRS module derives an
estimate of the current orientation of the drone, represented as a quaternion. The
sonar signal is usually very noise, so several filters are applied to eliminate outliers
and smooth the signal.
It is important to note that the IMU is the fastest updating component in the
framework: it acts as the heartbeat, and determines the rate at which the control
loop is executed.
Since usually, the IMU measurements are a little biased, a calibration phase is
implemented. Here measurements are collected and the average is stored. This value
177
2. COORDINATE SYSTEMS 178

is then subtracted from all future measurements. The calibration phase is marked
by all LEDs blinking in turn. When calibration is complete, the buzzer will sound,
and the drone is ready for take-off.

Warning 16.1.1

When the drone is in calibration mode it is important that you do not move it.
Otherwise all future measurements will be affected.
For safety reasons, two killing mechanisms are implemented. The kill switch is
a manual switch that turns all of the motors off when disabled. It is implemented
in the FPGA, independent of the Autopilot software. The arming procedure is
implemented in software and only allows the control outputs to be sent to the motor
controllers if the operator performed a specific sequence using the RC.
The framework also includes code to open network sockets for communication
with other modules (IMP, COMMS, CRYPTO). These sockets are implemented
asynchronously using Boost.Asio and run in separate threads, so there is no inter-
ference with the real-time control loop on the main thread.

2. Coordinate systems
Three coordinate systems are used in the Autopilot framework. The world coor-
dinate system (xyz) is in line with the grid. When standing in the drone room and
facing the flight area, the +x points to the right and the +y axis points away from
you. The +z axis completes the right handed coordinate system and faces upwards.
The local coordinate system (x′ y ′ z ′ ) is attached to the drone’s body and has its
origin at the center of the drone. The +x′ axis points towards the front of the drone,
as indicated by the two gold arms. The +y ′ axis points towards the left side of the
drone. The +z axis then points towards the top of the drone, completing the right
handed coordinate system. Rotation around the x′ , y ′ and z ′ axis is referred to as
roll, pitch and yaw respectively. The local coordinate system is defined such that a
positive rotation around the x′ (y ′ ) axis infers a positive translation in x (y).
The final imu coordinate system (x”y”z”) is the one used by the IMU. All coor-
dinate systems are depicted in Fig. 1.

y x’ y”
roll

pitch
x
z y’ z’ x” z”

Figure 1. Overview of the coordinate systems. Left to right: world


(xyz), local (x′ y ′ z ′ ) and imu (x”y”z”).

Warning 16.2.1
4. IMU CONFIGURATION 179

The IMU coordinate system in Fig. 1 is for the LSM9DS1. If you are using the
new LSM6DS0, this coordinate system is the same as the local one (x′ y ′ z ′ ).

3. Remote controller
There are three float valued inputs on the RC that are interpreted by the Au-
topilot framework. The raw signals are all read as PWM duty cycles between 0
and 1, but are scaled and shifted by the framework, depending on their purpose.
This is depicted in Fig. 2. The scaling factors can be modified in Autopilot/src/
students/src/main/Main-Students.cpp. The left stick determines the throttle
(τ ) and the yaw (ψ). The right stick determines the roll (θ) and the pitch (ϕ). The
tuner knob is not used by default.

Warning 16.3.1

Note that roll, pitch and yaw are not converted to a quaternion in the usual way
(cf. eagle-control-slides.pdf). First, the quaternion reference is rotated
such that the vertical axis matches the central axis of the right stick. Then the
yaw rotation around the (original) vertical axis is applied.

There are three integer valued inputs. The three-mode flight mode switch which
is interpreted as manual in its topmost position, altitude-hold in its center position
and autonomous in its bottom position. The wireless power transfer (WPT) switch
is used in both the calibration sequence (cfr. Tut. 27.2.1) and to confirm landing.
The kill switch is only implemented in hardware (see Ex. 19.6.1), such that it is
operational even if your code crashes. It turns off all of the engines.

+1 0 +10◦
ϕ
τ


−45◦ /s dt +45◦ /s −10◦ θ +10◦

0 −1 +1 −10◦

Figure 2. Overview of the RC inputs. Left to right: left stick, tuner


knob, right stick.

4. IMU configuration
There’s a variety of options that you can configure for the IMUs. You can review
or edit them in Autopilot/src/drivers/{lsm9ds1,lsm6ds0,mpu6050}/include/
drivers/IMU-Config.hpp.
Part 3

Workspace Tutorials
CHAPTER 17

Introduction

This part contains technical tutorials that teach you (i) how to use the provided
software; and (ii) how to setup and develop components in the workspaces used
throughout the project. Since the amount of tutorials is quite extensive and not all
tutorials are relevant for each module, you are strongly advised to start by checking
the reading guide of your specific module.
An overview of the workspaces is given in Section 2 of Chapter 1.

Tip 17.0.1

This part serves also as a frequently asked questions, where tutorials are added
when students repeatedly encounter the same problems. So if you would like
to see some tutorials added, feel free to contact the TA listed as editor of the
relevant chapter. Then they can consider adding the tutorial.

181
CHAPTER 18

PCB Simulations and Design


SWIPT

1. Setting up analog and digital co-simulation


1.1. Introduction. In this tutorial, the simulation part of the SWIPT assign-
ment is discussed. We will use two types of simulators for SWIPT, a pure SPICE
simulator called LTSpice and a mixed-signal simulator that can co-simulate SPICE
and digital hardware called SimVision. The tutorials below will mostly cover SimVi-
sion and how to create Spice netlists from LTSpice to use in SimVision.

LTSpice can be downloaded at https://www.analog.com/en/design-center/design-tools-and-calculator


ltspice-simulator.html, SimVision is normally available for students at ESAT (login
on your Linux account, or make one if you don’t have one yet). Under Tutorial
Files, you can find the necessary files to start the tutorials, and under Simulation
Models, you can find the files for the components you will use later. Good luck!

1.2. Analog Simulation Using LTSpice. LTSpice is a software tool that


allows to draw circuit schematics and run analog simulations on your circuits. It
supports the typical Spice-level simulation types, e.g. transient (time domain), ac
(frequency domain), and many others. In this project, you will need this tool to
design, simulate and understand the analog parts of your power/data transfer cir-
cuit. A slideshow with general information and a guide to get started with LTSpice
can be found at SWIPT LTSpiceTutorial on Gitlab, or you can look for some more
information at http://cmosedu.com/videos/ltspice/ltspice videos.htm.

Note that Spice simulations typically do not allow for complex digital control.
For instance, simulating your frequency tracking algorithm would be extremely com-
plex at this point. This is solved by using mixed-signal simulations where the digital
control is written in Verilog and the analog circuit is represented by a Spice-netlist.
The details of these mixed-signal simulations are discussed later on in this docu-
ment, but the next section already explains how you can obtain a Spice-netlist from
an LTSpice schematic.
Tutorial 18.1.1 (LTSpice to Spice-netlist) — This tutorial starts from the example
circuit shown in Figure 1. All the steps to extract the netlist of this schematic are
discussed.
(1) Create ports for any signal that you want to access in the mixed-signal
simulations, by right clicking the netname and setting the port type, as
shown in Figure 2. Pure digital signals usually have a ”strong” direction and
are thus either input or output. Analog signals are typically bi-directional,
but should not be used as a driver for a digital signal (because once in the
digital domain, this analog signal will become a ”1” or a ”0”).
182
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 183

Figure 1. Example LTSpice design of an amplifier that has a shut-


down option.

Figure 2. Adding ports to a schematic.

(2) Create a symbol for this schematic, as shown in Figure 3.

Figure 3. Creating a symbol from a schematic.

(3) The symbol should look like Figure 4. Save it.

Figure 4. The resulting symbol.


1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 184

(4) Create a new schematic and add the symbol as a component, as shown in
Figure 5. If you cannot find your symbol name, double check that the ”top
directory” matches the save location of your symbol.

(a) Add the symbol. (b) The resulting netlist

Figure 5. Creating a netlist in which the amplifier’s symbol is used.

(5) (OPTIONAL) Run the simulation to check that the symbol does indeed
work and contains the correct circuit. Figure 6 shows the transient wave-
forms. The amplification is triggered by ”SN” being high.

Figure 6. Transient waveforms of the amplifier circuit.

(6) View the Spice-netlist as shown in Figure 7 by right clicking on any open
space in the document.

Figure 7. Open the netlist.


1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 185

(7) Select and copy the SUBCKT of interest, the amplifier in this case, as
shown in Figure 8. You can now use this SUBCKT in other netlists and/or
in mixed-signal simulations. Note that, if any ”.lib” files or other source
files are included in the shown netlist (here this would be ”LTC6.lib”), you
will need to ensure that these files can be found when using the SUBCKT
outside LTSpice. A simple solution is to copy the included file to the run
directory of your simulation. Alternatively, you can update the path of the
file so it will be found correctly. The location of native LTSpice libraries
can be seen when adding the component to the schematic.

Figure 8. The final netlist. Usually you only need the subckt and
the included libraries.

1.3. Analog Simulations Using Simvision. LTSpice is not suited for com-
plex digital simulations. Simvision is a tool from Cadence that allows to do transient
simulations from both analog circuits (represented by Spice-netlists) and digital con-
trol (represented by Verilog code). This section will teach you how to do analog
simulations. Section 1.4 will then dive deeper into digital simulations, while Section
1.5 will combine analog circuits and digital control into mixed-signal simulations.
Tutorial 18.1.2 (RC-network) — The purpose of this tutorial is to implement an
RC-network in Spice and and simulate it using transient simulations.
(1) Start the computer under CentOs (Linux).
(2) Create a new directory (it it best not to use any spaces in the directory
name). Copy the content of the Analog example circuit blank directory into
your new directory.
(3) For the SWIPT-project, we will use Spice files (with a ”.spi” filename ex-
tension) to describe our analog circuits. Open ANALOG NETWORK.spi,
and add what is in the red box of Figure 9.

Tip 18.1.1

If this is the first time you encounter Spice-netlists, it might be some-


what difficult to read. But don’t worry, after a few weeks, you will see
that it is not that difficult after all. A netlist is a description of how
your electronic devices (resistors, inductors, capacitors, transistors,...)
are interconnected. The drawback of netlists is that a wrong connec-
tion (e.g. a short circuit) is easily made. In order to avoid mistakes, it
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 186

Figure 9. Adding an RC-network to the Spice file.

is important that you always draw the circuit on a piece of paper (or
import the netlist from a visual circuit simulator like LTSpice). This
will make it a lot easier to reason about your circuit.
Lets take a closer look at what we added to our ANALOG NETWORK.spi
file. The first line creates a dc voltage source of 0V between the ground and
the vss node. Hence the vss node is simply connected to the ground. The
second line creates a pulse source between nodes n1 and vss with period of
1000µs and a duty cycle of 50%. The third line creates a resistor of 1kΩ
between nodes n1 and n2. The last line creates a capacitor of 70nF be-
tween nodes n2 and vss. A schematic of this circuit is shown in Figure 10.
It it nothing more than a low-pass filter. More info on Spice-netlists can
be found at https://www.seas.upenn.edu/∼jan/spice/spice.overview.html,
or use Google to find more details.

Figure 10. Schematic of circuit described in Figure 9.

(4) Open a terminal and navigate to the directory you created at the beginning
of this tutorial. Enter the following command:
> sh run . sh
(5) The design browser window shown in Figure 11 will open. To start the
simulation press the play-button in the upper left corner.
(6) Browse to analog input inst and select the signals at the right (n1 and n2 ).
Right click on the selected signals and choose ”Send to Waveform Window”
as shown in Figure 12.
(7) When the waveform window opens, zoom out so that you can see the wave-
forms better. It is possible that even now the waveforms are barely visible.
You can solve this by clicking on the squares at the right, indicated by the
blue boxes in Figure 13. The orange waveform at the top is the input of
the RC-filter, the red waveform at the bottom is the output. It is clear that
the output is the low passed version of the input.
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 187

Figure 11. Design browser.

Figure 12. Send signals to waveform window.

Figure 13. In- and output of the RC-network.

(8) If you have adapted the Spice code and want to rerun the simulation, click
on ”Simulation” and then on ”Reinvoke Simulator...” as shown in Figure
14. You will get a message. Click ’Yes’. Press the play-button to start the
simulation again.
Tutorial 18.1.3 (Subcircuits and parameters) — The purpose of this tutorial is to
introduce subcircuits and parameters as they are used in Spice. It also explains how
to use the expression calculator of SimVision.
(1) Start the computer under CentOs (Linux).
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 188

Figure 14. Reinvoke simulator.

(2) We will work in the same directory we created at the beginning of Tutorial
18.1.2.

Figure 15. New content of ANALOG NETWORK.spi.

(3) Open ANALOG NETWORK.spi , and replace what you added in Tutorial
2 (Figure 9) with the lines in the green box in Figure 15. The first line
includes the SmallSignalDiode.spi -file that contains the Spice model of a
diode. This model is implemented as a subcircuit (more info on https:
//www.seas.upenn.edu/∼jan/spice/spice.overview.html#Subcircuits).
The circuit implemented in this file consists of a diode and a resistor of
100Ω and a sine source with a DC-value of 0V , an amplitude of 12V and a
frequency of 50kHz as shown in Figure 16.

Figure 16. Schematic of the circuit described in Figure 15.

(4) Start the simulator as explained in steps 4-6 of Tutorial 18.1.2. If you did
everything right, you should see something like in Figure 17 (after zooming
in). If you forgot to send a signal to the waveform window, you can still
add it by clicking on the tab indicated by the green box in the upper left
corner of Figure 17. Now you can browse the all the signals and plot them
by clicking on them (single click, no double click).
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 189

Figure 17. Anode (n1) and cathode (n2) voltage in waveform win-
dow.

(5) Now you will learn how to use parameters in Spice. Open ANALOG NETWORK.spi,
and add what is in the red boxes of Figure 18. Make sure that the ”.param
freq = 50k” line is stated before the definition of the subcircuit.

Figure 18. Adding parameters to the Spice file.

(6) Open amsSim.scs. This file contains the top level analog testbench. You
can find more details about the simulation file structure in later tutorials
(see Section 1.5). Add what is in the red boxes of Figure 19. The first
2000µs of the transient simulation, the frequency of the sine will be 50kHz.
From then on, the frequency will be 25kHz, until the simulation will stop
at 4000µs. If you want to simulate for a longer time, you can adapt the
value in the green box.
(7) Reinvoke the simulator and restart the simulation as explained in step 8 of
Tutorial 18.1.2. If you did everything right, you should see something like
in Figure 20.
(8) As shown in Figure 20 you can also plot the value of the parameters you
use. But be careful with this. Even though this parameter changes in time
(you can see that the frequency of n1 and n2 changes at t=2000µs), the
plotted value of the parameter freq stays constant. If you want to plot the
actual value of the freq parameter in function of time, you can apply a trick.
Add the dummy voltage source in the red box of Figure 21. Reinvoke the
simulator. Now you can plot the signal of the param freq node (as shown
in Figure 22). This is a representation of the value of the freq parameter
in function of time.
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 190

Figure 19. Adding parameters to the top level analog testbench

Figure 20. Anode (n1) and cathode (n2) voltage for a changing
frequency.

Figure 21. Adding a dummy source.

(9) The waveform window can show us the voltages on every node of our circuit.
If, for example, you want to calculate the power dissipated in the resistor of
100 Ω, you can use the expression calculator. We will do this in two different
ways. First we will use the voltage over the resistor. This is nothing more
than the n2 signal. Select the n2 signal by clicking at the location of the
red box in Figure 23. Then click on the icon of the calculator indicated by
the orange box in the upper right corner of Figure 23.
2
(10) The expression calculator should open. Now you can use the formula p = vR
to calculate the dissipated power. Add what is in the red box of Figure
24. Give your newly calculated waveform a proper name (e.g. ”Power”
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 191

Figure 22. Use a dummy voltage source to see parameter change


in time.

Figure 23. Send waveforms to calculator.

as indicated in the green box), and send it to the waveform window by


pressing the tab indicated by the orange box in the upper right corner.

Figure 24. Expression calculator.

(11) You could also use the current through the resistor to calculate the dissi-
pated power (p = i2 R). Before you can do that, you have to add the current
trough the sine voltage source (that is the same as the current through the
resistor) to the waveform window. Click on the tab indicated by the green
box of Figure 17. Add the current waveform to the waveform window by
clicking on the signal indicated in the red box of Figure 25. Note that
SimVision gives this current signals quite confusing names (like p $flow ).
Luckily, you can rename them in the waveform window by right clicking on
them.
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 192

Figure 25. Send current to waveform window.

(12) Now use this current to calculate the power dissipated in the resistor. It is
a good exercise to figure this out on your own. Of course you should get
the same results as the power you calculated in step 10. You can use this
expression calculator for a lot more than shown in this tutorial. You could
for example try to calculate the efficiency as the average power dissipated
in the resistor over the average power delivered by the source.

1.4. Digital Simulations Using SimVision. SimVision also allows you to


simulate Verilog code. The tutorial in this section will mainly focus on how to or-
ganise your Verilog files, so that they can be simulated and how to use the waveform
window for digital signals. Note that this is more a tutorial on using the simulator
than on using Verilog.

When you are writing Verilog, remember you are writing hardware instead of
software, meaning that many parts of your code can happen simultaneously (different
than e.g. C++ or python where code is executed line per line). As this might seem
difficult in the beginning, here are some good websites/tutorials to take a look at:
✓ An extensive documentation of Verilog on the syntax, different data types,
finite state machines (FSM), etc, including a lot of useful small examples:
https://verilogguide.readthedocs.io/en/latest/index.html.
✓ Some explanation on how Verilog works and more tutorials: http://www.
asic-world.com/verilog/veritut.html.
✓ A step-by-step guide how to construct an FSM: https://inst.eecs.berkeley.
edu/∼eecs151/sp22/files/verilog/verilog fsm.pdf.
✓ A tutorial on a simple FSM control different states for a robot: https:
//alchitry.com/finite-state-machines-verilog.
✓ A very short (and therefore clear) slideshow on the different parts of an
FSM: https://courses.cs.washington.edu/courses/cse370/10sp/pdfs/lectures/
15-VerilogIIPrint.pdf.
If you are not 100% sure about the correct Verilog syntax for e.g. multiplication,
if statements, etc, make sure to first take a look at these websites before you ask the
TAs for help or start to guess!

Tutorial 18.1.4 (Simple Counter) — The purpose of this tutorial is to create a


simple 8-bit counter and simulate it using SimVision.
(1) Start the computer under CentOs (Linux).
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 193

(2) Create a new directory (It it best not to use any spaces in the directory
name). Copy the content of the Digital example circuit blank directory into
a new directory.
(3) Before we can simulate our counter, we first have to implement it in Verilog
code. This is done in a Verilog file using a ”.v” filename extension. We will
implement our counter in Counter.v. Open that file in the text editor. You
will find a module where the in- and outputs are already defined, but body
is still empty as shown on Figure 26.

Figure 26. Empty counter module.

(4) Add the following code to the body of the module:


always␣@(posedge␣clk␣or␣negedge␣nrst)␣begin
if␣(~nrst)
value␣<=␣0;
else␣begin
if␣(enable)
value␣<=␣value␣+␣1;
else
value␣<=␣value;
end
end

This always-block implements the counter. If the enable signal is high, the
counter value is incremented at every rising clock edge. If the enable signal
is low the counter just remembers its current value. If the active low reset
signal nrst gets low, the counter value is set to zero.
(5) Now the counter module can be added to the testbench. Open Testbench.v
in the text editor and add what is in the red box of Figure 27.
(6) Before we start the simulation, we should tell the simulator which Verilog
files it should use. Open verilogFiles.f in the text editor. This file is empty.
Now add the following lines:
./Counter.v
./Testbench.v

(7) Open a terminal and navigate to the directory you created at the beginning
of this tutorial. Enter the following command:
> sh run . sh
(8) The design browser window shown in Figure 28 will open. Click on toplevel
and then select all the signals at the right (using the shift button). Right
click on the selected signals and choose ”Send to Waveform Window”
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 194

Figure 27. Testbench.

Figure 28. Design browser.

(9) The waveform window shown in Figure 29 will open. To start the simula-
tion, press the play-button. The counter we are simulating, is a very simple
circuit. Hence the simulation will be very fast. Press the pause-button next
to the play-button to pause the simulation. Now we can take a closer look
to the simulation results.
(10) Zoom in on the results as shown in Figure 30. Are they as you would
expect? SimVision expresses numbers by default as hexadecimal numbers.
If you want them expressed in another base right click on the number in
the red box in Figure 30. Now click on ”Radix/Mnemonic” and select the
base you want.
(11) If you have adapted the Verilog code and want to rerun the simulation, click
on ”simulation” and then on ”Reinvoke Simulator...” as shown in Figure
31. You will get a message. Click ’Yes’. Press the play-button to start the
simulation again.
1.5. Mixed Signal Simulations Using SimVision. The tutorial in this sec-
tion connects everything you have learnt in the previous tutorials. We will simulate
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 195

Figure 29. Waveform window

Figure 30. Waveform window: results

Figure 31. Waveform window: reinvoke simulator

a mixed-signal design. The main focus lies on the simulation file structure: how do
we connect the Verilog and Spice files so that they can be simulated together?
Tutorial 18.1.5 (Add Analog Circuits to Simple Counter) — The purpose of this
tutorial is to add additional analog circuitry to the simple 8-bit counter.
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 196

(1) Start the computer under CentOs (Linux).


(2) We will work in the same directory we created before.
(3) Analog circuits are not implemented in Verilog. For the SWIPT-project
we will use Spice files (with a ”.spi” filename extension) to describe our
analog circuits. Open ANALOG NETWORK.spi . You should see what is
in Figure 32.

Figure 32. ANALOG NETWORK.spi.

The line in the red box is the definition of the ANALOG NETWORK
subcircuit. It has two terminals: in analog and out analog. Note that
although these names suggest that the first terminal is an input and the
last terminal is an output, Spice does not make this distinction. For Spice
these terminals are just nodes in an electrical circuit, without any specific
in or out direction. In the blue box are two DC voltage sources defining
the VDD and VSS lines. The supply voltage of the Zybo is 3.3V . The line
in the green box just connects in analog and out analog. A schematic of
the subcircuit is shown in Figure 33. It is in fact nothing more than a wire
connecting in analog and out analog and a DC voltage source that is not
used in the rest of the circuit.

Figure 33. Schematic of ANALOG NETWORK.

(4) How do we combine the digital Verilog files and the analog Spice files such
that they can be simulated together? Figure 34 gives an overview of the
file structure. This figure will be discussed in detail in the following steps.
(5) Open Testbench.v and add the following code to the toplevel module:
// Input of analog circuit
wire␣IN_DIGITAL;
// Output of analog circuit
wire␣OUT_DIGITAL;

// Input of the analog part is the MSB of the counter value


assign␣IN_DIGITAL␣=␣value[7];
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 197

Figure 34. Simulation file structure.

// Analog circuit
ANALOG_NETWORK␣inst_ANALOG_NETWORK␣(
␣␣␣␣.IN_DIGITAL␣(IN_DIGITAL),
␣␣␣␣.OUT_DIGITAL␣(OUT_DIGITAL)
);

(6) Open analog network.pb . This file contains the portmap, which defines
the interface between the analog en digital circuitry. Portmaps are saved in
files with a ”.pb” filename extension. It connects the analog ports and the
digital ports to each other. The content of analog network.pb is shown in
Figure 35. You don’t have to change anything for now. But it is important
to understand its structure, so that you can adapt it correctly when you
change the ports of your design. In Figure 35, the analog ports are indicated
in the red boxes, the digital ports in the green boxes and the direction (in-
or output) is indicated in the purple boxes.

Figure 35. Portmap.

(7) Open amsSim.scs. This is the top level analog testbench. Uncomment the
amsd block in the red box of Figure 36. Be careful when you try to simulate
your own circuits. You should adapt the file and subcircuit names in the
blue boxes to your new Spice and portmap files. Now the simulation stops
after 2000µs . You can adapt this by changing the stop=... in the green
box.
(8) Now you are ready to start the mixed-signal (digital and analog) simulation.
Start the simulator just like in Tutorial 4: Open the terminal, navigate to
the directory you are currently working in and enter the following command:
1. SETTING UP ANALOG AND DIGITAL CO-SIMULATION 198

Figure 36. Spectre testbench.

> sh run . sh
(9) Send the signals of the toplevel module to the waveform window en start
the simulator just like you did in the previous tutorial (Figure 28 and 29).
Zoom in until you properly see the block wave of IN DIGITAL. It is possible
that the block wave of OUT DIGITAL is barely visible now. You can solve
this by clicking on the square at the right indicated by the blue box in
Figure 37.

Figure 37. Waveform window mixed-signal simulation.

(10) The IN DIGITAL signal in Figure 37 is a digital signal. It can only have
one of the logical values used by Verilog (’0’, ’1’, ’z’ , ’x’,...) while the
OUT DIGITAL signal is an analog signal that continuously varies between
0 and 3.3 V (even though it looks quite digital in this example). To plot
the analog variant of IN DIGITAL, click on the tab indicated by the orange
box at the left shown in Figure 37. Now you can browse trough the different
simulation results. Click on the in analog signal as shown in Figure 38. This
2. PCB DESIGN 199

is the analog representation of IN DIGITAL (remember that we connected


these two signals in the analog network.pb-file).

Figure 38. Browse trough the different waveforms.

(11) Reinvoke the simulator as you did in the previous tutorial (Figure 31), and
start the simulation again by pressing the play button. Now you can see
the analog input signal. You will see that the output signal is exactly the
same as the input signal. This is no surprise because our analog circuit was
nothing more than a wire connecting in- and output. Now you are ready
to implement your own (more complex) analog and digital circuitry.

Tip 18.1.2

Instead of putting your analog module directly into the testbench as shown in
step 5, you can also put it first in a separate Verilog module. In a second step,
you can add this Verilog module to the testbench, just like you did with the
counter.

Tip 18.1.3

Verilog-AMS is a variant of Verilog that can be used to model mixed-signal


circuits like ADCs and DACs. The syntax is very similar to normal Verilog.
You can also define modules with in- and outputs, and put these modules in
your testbench. If you want to do some ’processing’ of outputs of the analog
part, before these analog signals hit the digital part, you could use Verilog-
AMS. Always save your Verilog-AMS files wit a ”.ams” filename extension, and
don’t forget to add these files to the verilogFiles.f -file.

2. PCB design
2.1. Requirements. Altium is used to design and draw PCBs. The Altium
software is available under Windows on all ESAT machines. This means you can
also access Altium on your own computer, by opening a virtual machine. More
information on this can be found on the ESAT Wiki. In case of issues with the
available licences, please flag this ASAP to one of the TA’s. The design you sub-
mit must be an Altium project. A guide how to start with Altium can be found
on gitlab (AltiumStudentManual.pdf ), and there is a lot of documentation to
2. PCB DESIGN 200

be found online (e.g. https://www.youtube.com/watch?v=8FDTVCJRm-o&list=


PLxx4P PUfAcC4exFOOPOuMg1ZbL txvGi).

To reduce costs and make the exercise look like a more realistic industrial project,
the PCBs should match the following dimensions:
✓ Receiver: 5 cm×8.5 cm.
✓ Transmitter: 7.5 cm×8.5 cm.
These dimensions can only be exceeded given a good motivation (e.g. advanced
functionalities for ”exceptional” grade). Templates for the correct dimensions and
with the correct fabrication constraints (e.g. minimum linewidth) can be found un-
der Transmitter PCB, and Receiver PCB.

To make the design you will use several libraries that contain components. The
available libraries are located on gitlab. However, not all the components you may
need are there. Either you look for a ready-to-use library in e.g.Google or you create
your own component and footprint in Altium.

Finally, some physical constraints must be taken into account, to ensure fabri-
cation is possible. If you use the templates from gitlab, these rules are already
correctly implemented, so make sure not to change them.
2.1.1. About submitting your project. Before submitting your project, you should
always run the design rule check (DRC). A design that does not clear the DRC
will not be considered by the TAs. You can (only) ignore errors on silkscreen to
silkscreen clearance and silkscreen to soldermask clearance. However, unreadable
silkscreen text might make it more difficult for soldering the PCB later.
To submit your design, create a zip-file from your Altium project. This file
should include the following files:
✓ .SchDoC
✓ .PcbDoc
✓ .PrjPcb
✓ (.PrjPcbStructure)
Next, you email this zip-file to all the SWIPT TAs. They will review your
design and provide feedback if necessary. Only when they give the green light, your
submission is complete. Note that detailed feedback by email will only be given
twice, so make sure to solve all the problems from the first feedback round before
you send the PCB again. Of course, you can also ask PCB design relate questions
during the weekly sessions.

2.2. General tips on PCB design. An important issue to consider when


designing the PCB is how it will look in real life. The spatial distribution of the
components is crucial. For instance, the connection to the Zybo should be at the
side of the PCB, not in the middle. Components that are related to each other
should be close together. With that purpose, use the 3D viewing tool to visualise
your PCB. You will find it in the View tab.

Important: almost all components that you will use are available at the EAGLE
lab (current transformer, opamp,...) or at CDE (resistors, capacitors, headers,...).
For this, you can check section 7.3, and the datasheets on gitlab (there you will also
find more explanation on the LED logo and how it should be connected). CDE also
2. PCB DESIGN 201

has a Altium library for their components. Only if you have a very good reason for
using a different component, you can ask the TAs to order this component at Farnell
or RS Components. Make sure to look for a through-hole component, as this will
make the soldering easier.

Make sure you add to your PCBs all the components that are needed to enable
the integration with the drone. Some specific points of attention:
(1) You need to make sure that your Zybo can send PWM signals to the PCB,
you will need some headers for this on the side of the PCB.
(2) The coils and the power source need to be connected to the PCB. Please
select header CONCTB2P from your libraries, as this component is avail-
able at CDE and is big and steady enough to cope with the weight of the
coils and the power from the DC source.
(3) In general, foresee extra space to place headers for whatever external con-
nection you might think is useful.
(4) Decoupling capacitor for battery (Vcc): this element can either be placed
on the PCB or on the battery/cables themselves. If you are short of space,
it will be on the PCB.
2.3. Frequently committed errors. Here you can find a collection of last
year most common mistakes. Please try to avoid them:
✓ Spaghetti routing. A track should be as short as possible.
✓ Not using net labels in schematic. Nobody, including yourself, wants to
look at a bunch of messy lines.
✓ Missing connections to Zybo.
✓ Wrong and/or insufficient number of PWM pins.
✓ Missing keepout layer and/or board shape.
✓ Wrong battery and coil headers (you should use CONCTB2P from the PCB
library).
✓ Narrow power tracks. Tracks that will carry a lot of current (> 500 mA)
should be as wide as possible and preferably be replaced by power planes
or polygons.
✓ Power tracks interrupted by vias.
✓ Large number of vias. A connection should have as few vias as possible.
✓ Coil/battery headers far from the board’s edge.
✓ Interacting components placed very far from each other (hence, no interac-
tion possible).
✓ Bootstrap capacitors very far from MOSFET drivers (more important than
what you’d think).
✓ Holes for some components too small (especially those for diodes).
✓ Wrong footprint for ordered components.
✓ Galvanic isolation not connected to the right voltage domain.
✓ Incoherent naming of nets.
CHAPTER 19

Vivado Hardware Design Project


Zhenda Zhang

1. Setting up
Tutorial 19.1.1 (Starting Vivado) — Xilinx Vivado is the development environ-
ment that comes with Zybo. To start Vivado, you must first source the Xilinx
tools:
(1) Start an ESAT computer under Rocky Linux and open a terminal.
(2) Now we tell our machine where the Xilinx software is located:

source␣~micasusr/design/scripts/xilinx_vitis_2021.2.rc

(3) Now open Vivado with the following command

vivado␣&

Tip 19.1.1

The steps in this specific tutorial are specifically for the PCs at ESAT. If you
attempt to run Vivado from home, please refer to Tutorial 19.7.1 or Tutorial
19.7.2.

2. Creating and testing a hardware project


A “hardware project” is a Vivado project where the FPGA interacts only with
the hardware (buttons, switches, LEDs, Pmods). Interacting with the CPUs in the
Zynq chip will be covered in later sections.
Tutorial 19.2.1 (Creating a Vivado hardware project) —
(1) Click File → Project → New..., and click Next.
(2) Select the name of the project, and the folder in which the project files will
reside. Click Next when ready.
(3) Select an RTL project with the default options, and click Next once more.
(4) Select the hardware for the project. Here, you first have to go to the Boards
tab, not the Parts tab. Then scroll down or search until you have found
the Zybo or Zybo Z7-10, you may need to click the download icon before
continuing. See Figure 1a. When the row is highlighted, click Next and
confirm the Default Board and Default Part in the following screen are
resp. a Zybo and xc7z010clg400-1, as shown in Figure 1b.
(5) Press Finish to create the project.

202
2. CREATING AND TESTING A HARDWARE PROJECT 203

(a) (b)

Figure 1. Creating a new Vivado project

Now that we have a project ready, we should start adding some code.
There are two main types of code in a Vivado project: Verilog modules, and con-
straints. The former are modules written in Verilog, describing hardware behavior.
Modules can contain other modules, and one module is the top-level module, listed
in bold in the project sources listing. The top-level module defines which Verilog
will be synthesized and programmed to the FPGA. You can select which module
will be the top-level one, by right-clicking it in the project sources, and clicking Set
as top.
The constraints file describes the actual link between the top-level module and
the outside world. It defines the connections between the inputs and outputs of the
top-level module, and hardware on the FPGA, e.g. the leds output of the top-level
module should go to these ZYNQ outputs, which correspond to the LEDs on the
Zybo. It can also define the clock frequency.
Tutorial 19.2.2 (Adding source code to Vivado) — This tutorial will show you
how to add a Verilog module and a constraint file to your Vivado project.
(1) Click the big blue + button in the Sources window.
(2) Select Add design sources from the options, and press Next.
(3) Click Create file, and enter the name of the file you want to make. Click
Finish.
(4) Now, a popup is shown for adding input and output signals to the newly-
created module, as shown in Figure 2a. Enter what you need and click
OK.
(5) Now you can start writing Verilog.
As a very simple example, we will use the following code:
module␣switch2led(
␣␣␣␣input␣[3:0]␣sw,
␣␣␣␣output␣[3:0]␣led
);
␣␣␣␣assign␣led␣=␣sw;
endmodule

Combined with the constraint file (.xdc) of the board you use from here, (after
uncommenting the relevant lines, as shown in Figure 2b), this will simply light up
the LEDs when the corresponding switches are in their on setting.
2. CREATING AND TESTING A HARDWARE PROJECT 204

(a) (b)

Figure 2. Adding sources to the Vivado project

Tip 19.2.1

Vivado is a powerfull development environment. You can make good use of


Verilog language supports in the IDE to write your code, e.g., auto-completion,
syntax highlighting, keyboard shortcuts, language templates, etc.

Tutorial 19.2.3 (Synthesizing the Verilog design and generating a bitstream) —


The data that needs to be placed on the Zybo to perform the behavior from the
Verilog code, is called a bitstream. Such a bitstream is generated in multiple steps:
Elaboration: This step parses the Verilog code and converts it into the correspond-
ing register-transfer-level meaning.
Synthesis: Here, the RTL description of the project is converted into blocks avail-
able on the FPGA. eg. a counter register will be turned into a few LUTs
and flip-flops.
Implementation: This step performs the placement and routing, in which LUTs
and flip-flops are available somewhere on the FPGA are assigned to the
elements from the synthesized design, and are connected to one another. It
also applies some optimizations.
Bitstream generation: The final step takes the implementation output and turns
it into a file the FPGA can understand and load.
The entire process is started by clicking Program and Debug → Generate
Bitstream in the Flow manager. Running only some of these steps is also possible.
When the process is finished, the bitstream file can be exported to the file
system for processing with another application (using File → Export → Export
Bitstream File...), or loaded onto the Zybo directly from Vivado.
Tutorial 19.2.4 (Inspecting the results of a design) — This tutorial will show you
how to look into the results of the elaborated, synthesized, and implemented design.
The result of the elaboration step can be seen in Flow navigator → RTL Analysis
→ Open Elaborated Design → Schematic. As shown in Figure 3a, a symbolic
schematic of the design should be visible.
A schematic for the synthesis result is available, too. Here, the symbolic elements
(adders, registers, ...) have been replaced with LUTs and flip-flops, buffers have been
added, etc, see Figure 3b.
2. CREATING AND TESTING A HARDWARE PROJECT 205

(a) (b)

Figure 3. Elaborated and synthesized schematics of the design from


Tutorial 19.3.1

(a) (b)

Figure 4. Inspecting an implemented design

(a) I/O Ports window (b) Timing window

Figure 5

Inspecting the result of the implementation is slightly different. Instead of a


neat schematic, an overview of the internals of the FPGA is shown, with used blocks
highlighted. Nets can be selected in the sidebar to highlight the path, cf. Figure 4a
and Figure 4b.
In the I/O Ports window (Window → I/O Ports), the connections to the out-
side world can be seen, as shown in Figure 5a. The Timing window can be used
to check the amount of time it takes for a signal to travel through a path between
two registers. This can be very useful when your design does not meet the timing
constraints. An example of this is shown in Figure 5b.
3. SIMULATING AND DEBUGGING A HARDWARE PROJECT 206

Tip 19.2.2

The reports after each step are very useful for debugging. If you have any
problems with your design, check the reports first. You are also encouraged to
search for official Xilinx documents for more information.

Tutorial 19.2.5 (Testing the Verilog design on a Zybo) — This tutorial will show
you how to write the bitstream to an FPGA, like Zybo, and test it.
Once a bitstream has been generated, it can be loaded onto the Zybo when
connected using USB (connected to the PROG UART USB port). Note that its JP5
jumper, near the audio jacks and VGA (Zybo)/HDMI (Z7) port, needs to be set to
the JTAG option! Also don’t forget to set the power switch to ON.
(1) Click Open Target → Open New Target... under Flow Manager →
Program and Debug → Open Hardware Manager.
(2) Select Local server if you are using Vivado on the same computer as
you have connected the Zybo to, or are using a VM from the ESAT Virtual
Desktop Portal. When using SSH or VNC, select Remote server and enter
the connection details.
(3) Verify the correct parts are listed (arm dap 0 and xc7z010 1), and click
Next and then Finish.
(4) Now the bitstream can be loaded onto the FPGA, by clicking Program
device in the green bar.
Check if the LEDs light up when you flip a switch.
Note that, once you unplug the USB cable, the bitstream will be gone, and the
FPGA will have to be reprogrammed. In order to have the bitstream to persist
across boots, a BOOT.BIN file needs to be generated and placed on the microSD
card, which is covered in Section 4 of the Vitis chapter.

3. Simulating and debugging a hardware project


Programming hardware is much more error-prone than software and harder to
debug. Luckily, Vivado has a built-in simulator to test your Verilog code in different
abstract levels. Normally, you should test your design in behavioral simulation first,
before attempting to run it on hardware. However, some bugs may not arise in
the behavioral simulation while the simulator only performs the elaboration step.
Then, It may be required to perform extra debugging. Vivado also has involved and
restricted tools like post-synthesis functional and timing simulations, and debugging
tools on hardware.
Tutorial 19.3.1 (Simulating a Verilog module) — Let’s use the following code as
an example:
`timescale␣1ns␣/␣1ps
module␣btncount(input␣clk,␣input␣[3:0]␣btn,␣output␣[3:0]␣led);
␣␣␣␣wire␣reset;
␣␣␣␣assign␣reset␣=␣btn[3];
␣␣␣␣reg␣[3:0]␣prevbtn;
␣␣␣␣reg␣[3:0]␣counter;
␣␣␣␣always␣@(posedge␣clk,␣posedge␣reset)␣begin
␣␣␣␣␣␣␣␣if␣(reset)␣prevbtn␣<=␣4'b0;
␣␣␣␣␣␣␣␣else␣prevbtn␣<=␣btn;
␣␣␣␣end
␣␣␣␣always␣@(posedge␣clk,␣posedge␣reset)␣begin
3. SIMULATING AND DEBUGGING A HARDWARE PROJECT 207

␣␣␣␣␣␣␣␣if␣(reset)␣counter␣<=␣4'b0;
␣␣␣␣␣␣␣␣else␣if␣(btn[0]␣&&␣!prevbtn[0])␣counter␣<=␣counter␣+␣1;
␣␣␣␣␣␣␣␣else␣counter␣<=␣counter;
␣␣␣␣end
␣␣␣␣assign␣led␣=␣counter;
endmodule

This code increases a counter when button 0 is pressed, and resets it when button
3 is pressed. The counter value is displayed on the leds.
Now we need to create a testbench for this module. Create a new Verilog module
again (cf. Tutorial 19.2.2), but this time, select Add simulation sources instead
of Add design sources.
Here is a very simple testbench:
`timescale␣1ns␣/␣1ps
module␣testbench();
␣␣␣␣reg␣reset,␣clk,␣btn;
␣␣␣␣wire␣[3:0]␣btns;
␣␣␣␣assign␣btns[0]␣=␣btn;
␣␣␣␣assign␣btns[2:1]␣=␣2'b0;
␣␣␣␣assign␣btns[3]␣=␣reset;
␣␣␣␣wire␣[3:0]␣led;
␣␣␣␣btncount␣bc(clk,␣btns,␣led);

␣␣␣␣always␣#5␣clk␣<=␣~clk;␣// clock generation


␣␣␣␣initial␣begin
␣␣␣␣␣␣␣␣clk␣<=␣1'b1;
␣␣␣␣␣␣␣␣reset␣<=␣1'b1;
␣␣␣␣␣␣␣␣btn␣<=␣1'b0;
␣␣␣␣␣␣␣␣#40␣reset␣<=␣1'b0;

␣␣␣␣␣␣␣␣#40␣btn␣<=␣1'b1;␣#20␣btn␣<=␣1'b0;
␣␣␣␣␣␣␣␣#40␣btn␣<=␣1'b1;␣#20␣btn␣<=␣1'b0;
␣␣␣␣␣␣␣␣#40␣btn␣<=␣1'b1;␣#20␣btn␣<=␣1'b0;

␣␣␣␣␣␣␣␣#80␣$display("counter␣is␣at␣%h\n",␣led);
␣␣␣␣␣␣␣␣$finish;
␣␣␣␣end
endmodule

This testbench instantiates the module we want to test, generates a clock signal
with a period of 10ns, resets the module for 4 clock cycles, after which it simulates 3
button presses. The numbers after the # sign denote the amount of nanoseconds the
simulator needs to wait before performing the next statement. $display is a printf-
style function, and $finish ends the simulation. (If $finish is not reached, the
simulation continues indefinitely, or until a maximum simulation time is reached.)
Selecting the testbench as the top-level module in the Simulation Sources or
right-click Flow manager → Simulation → Run Simulation to reset or set the
simulation properties. Left-click Flow manager → Simulation → Run Simulation
and click Run Behavioral Simulation. The waveform window will pop up auto-
matically. This should display a graph as shown in Figure 6. Try running it on real
hardware to check if it works as expected.

Tip 19.3.1
4. USING IP CORES AND BLOCK DESIGNS 208

Figure 6. Wave output of a simulated testbench.

You can also run the post-synthesis simulation to check if the design is correct
after the synthesis step. It is helpful when you find the bitstream is not working
as expected. The problem can be related to timing issues or the synthesis step.
The post-synthesis simulation can help you to locate the problem. However,
running the post-implementation can be very slow.

Tip 19.3.2

In more complex designs, more signals can be added from the Scope tab/window
on the left. You can also select multiple signals, right-click, and add them to
a group (e.g. signals belonging to one module). Use these tools to make the
simulation more comprehensive and save the resulting waveform configuration
(Ctrl + S) for future use.

4. Using IP cores and block designs


A pure Verilog approach is usable for small designs, but for larger designs with
many modules, this becomes slightly impractical. Additionally, to interface with the
ZYNQ ARM processors, modules without Verilog sources need to be used, requiring
a different approach.
Vivado also has the facility to create modules from block designs. These are
collections of IP cores – other modules – connected to each other, laid out in a
graphical editor. Intefacing with the ARM processors is done by importing the ZYNQ7
Processing System IP core into a block design, and connecting other modules to
it, which will then become accessible from software running on the ARM cores.
The block design approach is a bit overkill for the example here, but it serves as
the groundwork for the following chapter, which explains how to interface with the
ARM processors.

Tutorial 19.4.1 (Creating an IP core from a Vivado Verilog project) — We now


first create an IP core from the example code in Tutorial 19.2.2, which we will then
import into a block design in the next tutorial.
(1) Click Tools → Create and Package New IP, and click Next.
(2) Select Package your current project and click Next again. This will
create an IP core from the project’s current top-level module.
4. USING IP CORES AND BLOCK DESIGNS 209

(3) Select a folder to place the IP core in. It is advised to create a new, empty
folder for this. Remember the location of this folder for later, as it will be
needed in the next tutorial. Click Next when ready, then OK and Finish.
(4) A new Vivado project opens, this time containing the files of the IP core.
Here, superfluous files (eg. simulation sources and constraints) can be re-
moved from the IP core, and modules can be parameterized when desired.
(5) Once finished with the modifications to the Verilog sources, the IP core can
be packaged. Go to the Package IP tab, and go to the File Groups, and
click Merge changes from File Groups Wizard.
(6) Similarly, continue to the Customization Parameters and Ports and
Interfaces to perform the required modifications, the Vivado UI’s guid-
ance is helpful here.
(7) When finished, go to Review and Package, click Package IP, and then
Yes.
The IP core is now packaged and can be imported into a block design.

Tutorial 19.4.2 (Creating and editing a block design) — Here, we will create a
block design version of the example in Section 2, by importing the IP core from
Tutorial 19.4.1.
(1) Create a new Vivado project, as explained in Tutorial 19.2.1. Import the
XDC file – and only the XDC file – from Tutorial 19.2.2.
(2) Create a new block design by clicking IP Integrator → Create Block
Design in the Flow Navigator. Enter its name (the default is usually ok),
and you will be shown an empty screen.
(3) You can start adding IP cores to the block design by clicking the big blue
+ button, which opens a popup of all the available IP cores. However, you
will notice the exported IP core from Tutorial 19.4.1 is not in the list: it
needs to be imported first.
(4) Open the IP Catalog by clicking Project → IP Catalog in the Flow
Navigator.
(5) Right-click the list of available IP repositories and cores, and click Add
Repository.... Navigate to the directory where the IP core from Tutorial
19.4.1 was exported to.
(6) Go back to the block design. Now you should be able to find the IP core
in the list, when adding an IP core.
(7) Now we need to add ports to interact with the real world. Right-click empty
space in the block diagram, and select Create Port.... Enter the details
of the port (as shown in Figure 7a, one should be called sw, the other led,
both a vector from 0 to 3, and resp. an input and an output). All the ports
should correspond to the options in the XDC constraints file.
(8) Connect all the elements, the end result should look like Figure 7b.
(9) In order to synthesize and generate a bitstream for this design, a Verilog
wrapper module has to be created. Right-click the block design in the souce
file listing, and select Create HDL Wrapper... as shown in Figure 7c, and
let Vivado auto-manage it.
Now you should be able to start the entire Elaboration → Synthesis → Imple-
mentation → Bitstream generation process, this time from the block design. Upload
the bitstream and verify that it still works as expected on a real Zybo.
5. HARDWARE-SOFTWARE INTERFACING 210

(a) (b) (c)

Figure 7. Creating a block design

Tutorial 19.4.3 (Editing an already exported IP core) — After having exported a


module as an IP core, and importing it into another project, you may notice small
bugs being left in the implementation. However, it is impossible to edit the Verilog
source of the IP core from the project that uses it anymore.
You may be tempted to create a new project and re-create the IP core with the
now-fixed code, but, luckily, there is a quicker way:
(1) Open the block design and right-click the IP core you want to edit, and
select Edit in IP Packager.... Press OK in the popup.
(2) Now you can edit the Verilog sources of the IP core again. As an example
change, let’s replace assign led = sw; with assign led = ~sw;, invert-
ing the signal.
(3) Once you’re finished with the modifications, go back to the Package IP
tab, perform the required checks as explained in Tutorial 19.4.1, and click
Package IP, and then Yes.
(4) Back in the main Vivado project window, the block design will have to be
updated, as Vivado will have noticed a change. Click Report IP Status1
at the top of the block design window. This opens the IP status window
at the bottom.
(5) In this bottom window, click Upgrade Selected, and then Generate in
the popup.
You can now try out the new changes, by loading the code onto the Zybo hard-
ware, and verifying the behavior of the switches is now inverted.

5. Hardware-software interfacing
Tutorial 19.5.1 (Creating a hardware-software interfacing block design) — In this
tutorial, we will apply the knowledge from the previous section to create a design
that interfaces with the ARM processors in the Zynq.

Tip 19.5.1

The communication interface within Xilinx products is the Advanced eXtensible


Interface (AXI).
Here we use an existing block that exposes the FPGA GPIO pins to the
processor through an AXI interface. Through the AXI protocol, the processor
can then toggle the GPIO pins by writing to a certain location in memory. All

1Or Show IP Status, Vivado isn’t always consistent about it, it seems.
5. HARDWARE-SOFTWARE INTERFACING 211

AXI signaling happens transparently to the user. It is called AXI GPIO. There
are also other AXI interfaces. Please refer to the follow-up tutorials for more
information, e.g., in Chapter 21 and Section 2.
✓ AXI GPIO: A GPIO interface that exposes the FPGA GPIO pins to
the processor through an AXI interface. Through the AXI protocol,
the processor can then toggle the GPIO pins by writing to a certain
location in memory. All AXI signaling happens transparently to the
user. This tutorial uses this interface.
✓ AXI4-Lite: A simple AXI interface that allows the processor to read
and write to a certain location in memory. This interface is used in
Chapter 21, Section 2.
✓ AXI4: AXI is a burst-based protocol so there may be multiple data
transfers for a single request. This makes it useful in cases where it
is necessary to transfer large amounts of data from or to a specific
pattern of addresses.
(1) Create a new Vivado project and create a new block design in it.
(2) Add one ZYNQ7 Processing System and two AXI GPIOs to the block de-
sign.
(3) Download this file, it will be needed in the following steps.
(4) Right-click the ZYNQ7 Processing System IP core and select Customize
Block.... This opens a new popup window.
(5) At the top of the popup window, click Presets → Apply
Configuration..., and select the ZYBO C.tcl file you downloaded two
steps ago.
(6) Click OK to close the popup.
(7) For the first AXI GPIO, open the block customization popup, too, go to the
IP Configuration tab, and check the Enable dual channel option, as
shown in Figure 8a.
(8) In the second AXI GPIO’s options, in the same IP Configuration tab,
set the Default Output Value to 0xF, and set the Default Tri State
Value to 0xFFFFFFF0.2
(9) Click Run Block Automation in the green bar in the block diagram win-
dow. Click OK in the popup window, as the defaults are fine.
(10) Now click Run Connection Automation. In the popup, select everything
in the tree structure on the left. For the first GPIO, select resp. sws 4bits
and btns 4bits in the Board Part Interface dropdown. For the second
GPIO, select leds 4bits. See Figure 8b.
(11) Click Regenerate Layout (blue arrow in a circle) to clean up the diagram.
The result should look like in Figure 9.
(12) Open the Address Editor. Here, the addresses at which the ARM proces-
sors can access each AXI-connected IP core are listed. These can also be
edited, but leave them as-is for now.
(13) Click Validate design (blue checkmark) to validate the design.
The entire design can again be synthesized and loaded onto the Zybo hardware
(after creating an HDL wrapper for the block design). If you try it out this time, the
four LEDs should automatically light up: this is what the Default Output Value
of the second GPIO does.
2seven Fs
5. HARDWARE-SOFTWARE INTERFACING 212

(a) (b)

Figure 8. Customizing IP blocks

Figure 9. The final block diagram of the AXI GPIO example.

However, flipping the switches or pressing the buttons causes no change. This
happens because the ARM processors and the software on them are now in charge of
reading out the switches and buttons, and controlling the LEDs. However, there is
no software running on the processors, as we didn’t write any yet! To add software,
let’s continue with the next tutorial.
Tutorial 19.5.2 (Exporting the hardware design as a hardware platform) — Em-
bedded C/C++ software projects are created in Vitis. These projects need a Vivado
platform to run on. The platform is defined by the Vivado project, which includes
the configurations of the ZYNQ processing system.
To export a platform for Vitis go through the following steps:
(1) Flow Navigator → Program and Debug → Generate Bitstream
(2) File → Export → Export Hardware... Press Next. Make sure that
include bitstream is checked and press Next again.
(3) Select the default name for the hardware platform file (XSA), and select the
default location to export to. Press Next and Finish to complete exporting
the hardware.
(4) We can now open the Vitis IDE. Within Vivado select Tools → Launch
Vitis IDE. Alternatively, you can open Vitis from the command line:
vitis␣&
6. THE EAGLE VIVADO PROJECT 213

Proceed with the Vitis tutorials in Chapter 20 to create and run a software
project. The chapter will first cover some Vitis basics, after which Tutorial 20.2.1-
Tutorial 20.2.3 will complete the example.

6. The EAGLE Vivado project


The previous tutorial defined a hardware project that only contained the ZYNQ
processing system and two AXI GPIO blocks. The EAGLE hardware project, or
“the drone project”, is much more involved and contains a lot of pre-constructed IP.
Tutorial 19.6.1 (Opening the EAGLE Vivado project) —
(1) Start Vivado and open the drone project by pressing File → Project →
Open.... You need to open the drone.xpr file, which is located in
EAGLE-students/Module software/Zybo/HW/drone/.
(2) Flow Navigator → IP Integrator → Open Block Design
The EAGLE project defines a hardware platform. Embedded C/C++ software
applications run on this platform, on the included ZYNQ processor. Before such
applications can be developed, the hardware platform must be exported from Vivado.
Refer to Tutorial 19.5.2 for how to do so.

Tip 19.6.1

Make sure to deactivate the previoulsy exixting incremental synthesis when you
synthesize the EAGLE Vivado project for the first time. This is done by pressing
Tools → Settings → Synthesis → Settings tab → Incremental Synthesis. You
can later set up a new incremental synthesis using the same menu, this is a
function that you can use to speed up the bitstream generation process.
Try to get familiar with the different components of the project. As a first step,
you can try to find and recognize the different blocks and go through the following
exercises. These list some important components and how to find them.
Exercise 19.6.1 (Kill switch) — We will take a look at the kill switch, which is
arguably one of the most critical components in the hardware design.
(1) Open the block design in Vivado and identify the kill switch block.
(2) What signals drive the motors?
(3) What is the heartbeat line? Why do we need it?
Exercise 19.6.2 (Tuning knob) — Find the tuning knob hardware address. See
where the C++ code accesses it using the search functionality of the Doxygen frame-
work (cf. Tutorial 26.5.1). Can you see from the block design which tuning knob is
actually connected, if so how?
Exercise 19.6.3 (Changing the default LED configuration) — Try changing which
LEDs light up when the bitstream gets loaded and starts working. Use the approach
from Tutorial 19.5.1.
Exercise 19.6.4 (Checking the I/O ports of the design) — After generating a
bitstream, it is possible to check which FPGA ports are connected to which physical
ports (Pmods, buttons, etc.). Try finding out how, and try making a list of what is
connected to which physical devices. Use the approach from Tutorial 19.2.4.
7. WORKING FROM HOME 214

Exercise 19.6.5 (Modifying the address space in the Address Editor) — All the
previous exercises took place in the block diagram. This is not the only part of the
block design: it also defines which memory address every AXI-connected IP core
will reside at, for CPU access. You can change these addresses, and it is important
to have them aligned to 64 kilobytes, non-overlapping, and consistent with your
software code. (Getting the last part wrong is a not too uncommon source of bugs
when making a custom AXI interface.) Try making a list of which peripherals can
be accessed by software, and try changing one to see how it breaks.
Exercise 19.6.6 (Modifying the EAGLE project) — During the project, some mod-
ifications will have to be done to the EAGLE project. For instance, the CRYPT and
SWIPT IP cores must be completed by their respective modules. You can inspect
these modules to find out what they currently implement. Follow the approach of
Tutorial 19.4.3.

Tip 19.6.2

If you want to test the hardware on the PCB while working on the EAGLE
project in Vivado in parallel, you can use an Arduino to generate some control
signals instead. To achieve an adequate speed for these signals, it may be
necessary to directly access and program the Arduino’s internal registers. A
useful script to generate a complementary PWM signal with Arduino that can
be used to control the power transfer circuit is available here.

7. Working from Home


Tutorial 19.7.1 (Local installation of Vivado) — The most straightforward way
to use Vivado remotely, is to install it on your own computer. You can download
the installation files from thegi official Xilinx website. Select the same version that
is installed on the ESAT computers and install the free (device-limited) Vivado HL
WebPACK edition. To save space on your local computer you can choose to limit
the installation to the Zynq-7000 series devices.
Tutorial 19.7.2 (Working remotely on ESAT computers) — If you are unable
to install Vivado locally, there are several options to work remotely on an ESAT
machine.
Firstly, it is possible to remotely connect to ESAT pc’s using the Secure SHell
(SSH) protocol. Instructions to do so are available here and here. This works well
for using commandline Vivado tools, but X11 forwarding is often too slow to set up
a full graphical environment.
To setup a graphical session, one option is to use the VNC protocol. Instructions
to set up this connection are available here. Alternatively, students can use the
Virtual Desktop portal. Instructions to use this portal are available here. This
portal is only accessible from the KU Leuven network. So this server is unreachable
if your client computer is e.g. on your home network. You can set up a VPN
connection to solve this problem using the KU Leuven VPN, or an SSH tunnel
using the sshuttle tool.
Using a SPICE console together with an ESAT Virtual Desktop has an USB
passthrough feature, allowing one to run and test code on a Zybo at home while using
Xilinx tools on an ESAT VM. When using VNC, this process is more complicated,
8. SWIPT IP BLOCKS 215

and involves installing the hw server tool locally and then using SSH reverse port
forwarding. For the ones seeking a challenge, An unofficial guide for the latter can
be found here, however, no tech support will be given for this.

8. SWIPT IP Blocks
This section further discusses the relevant features/parts of the Vivado project
of the EAGLE project for the SWIPT module.
Three IP blocks relevant for SWIPT are present in the Eagle project as shown
in Figure 10: a custom made SWIPT IP in the red box, the XADC Wizard IP in
the green box, and an AXI-PWM module in blue. These IPs will be discussed in
more detail in the following subsections.

Figure 10. IP blocks in Vivado used by SWIPT.

8.1. SWIPT IP. The SWIPT IP features an AXI-interface. This memory-


mapped interface enables the communication with the C-code and/or linux-OS. This
is required to start the power transfer at the right time, and to transfer data like
the coordinates of the trajectory the drone followed to the SWIPT IP. From there,
this flight data should then be send over the wireless data link towards the receiver.

Besides the AXI-interface, there are three additional inputs: ADC data, ADC ready
and swiptONHeartbeat. The first two, receive respectively the ADC data and the
ready signal from the ADC that indicates new data is available. The last acts as as a
fail-safe for when you lose control over you module through the AXI-interface. Only
when a PWM signal from the ”PWM AXI triple 4” block is active on this pin, the
SWIPT OUTX outputs can be active. This PWM signal is controlled by a switch
on the controller. During development and testing, it might be annoying to have to
use the controller to activate this signal constantly. For that reason, you are allowed
to remove the heartbeat behavior in the IP core itself.

Warning 19.8.1

However, when testing with the battery the fail-save mechanism should always
be included in the IP module!.
8. SWIPT IP BLOCKS 216

Next, there are several outputs. The SWIPT OUTX signals are the output
signals that should control the DC/AC conversion on your PCB. The other ADC xxx
signals, are used to configure the ADC. They have a default setting in the IP’s core
HDL code that can be changed if necessary.
8.1.1. AXI-interface. The SWIPT IP module interfaces with the ARM cores us-
ing the AXI-bus. This allows memory mapped access to the SWIPT module from
both bare-metal C-code or from within the linux OS environment. This is how
you can communicate the data that needs to be transmitted to the landing plat-
form from the C/Python-code to the SWIPT IP and indicate that a transmission
should start. By default the IP has 8 memory mapped registers that can be used
to write data to it. Their addresses can be found in the address-editor tab in Vivado.

Within the IP itself, the memory mapped registers can be accessed used the
”slv regx” registers. You can use these registers as any other verilog register and
they will update on each new write operation to the IP module over the AXI-bus.
If you need a notification of such an update, you should look into the specific AXI
signals that indicate a write has taken place in the IP verilog code. Further, read-
ing from the SWIPT module over the AXI-bus simply returns the values stored in
the ”slv regx” registers. Again, if desired, you can change this default read-back to
something that suits your needs by modifying the IP’s code.In general, you can make
any modification you want to the current AXI-setup of the SWIPT IP. However, if
you break it, you fix it!

One such useful modification is a debugger, by means of the memrw python


library. With the help of this library, you will be able to read out the values from
these registers and display them on your pc, which makes it possible to know the
values/states of certain variables in your code (e.g. the frequency while testing the
frequency tracking algorithm). As this debugger requires more knowledge than just
the IP blocks on the FPGA, it is best to work on this together with colleagues from
COMMS. The further explanation of this debugger is not added yet in this version
of EAGLE book, so make sure to download the most recent version once you get to
this.
8.1.2. ADC-interface. The ADC data and ADC ready inputs and the ADC address
output are used for the interface with the ADC. ADC data is 16 bit wide. Only
the 12 MSB are the converted data of the ADC. The 4 LSB are not used in this
project. With ADC ready the ADC informs the SWIPT IP that the data is ready.
There are four external analog signal channels available (ADC inn 6, ADC inp 6,
ADC inn 14 and ADC inp 14 on Figure 11) that be paired to create two differential
channels (AD14 and AD6) to obtain differential measurements. With ADC address
the SWIPT IP can select which of these four channels should be read. Note that
the ADC is capable of handling more channels than currently setup. You can used
these if you want to. More information on the ADC is given below.
8.1.3. SWIPT OUTX Outputs. There are 4 digital outputs of the SWIPT IP
(SWIPT OUT0, ..., SWIPT OUT3 ) that are already provided to control your pow-
er/data transfer PCB. Through the contraint file, these are mapped to physical pins
on the Zybo board. Figure 12 shows where you can find them on the board itself.
8.1.4. Source Files. You can edit the SWIPT IP by right clicking on the SWIPT
IP block in the block diagram and selecting ”Edit in IP Packager”. You will now
8. SWIPT IP BLOCKS 217

Figure 11. ADC channels on Zybo

Figure 12. Which pins are used for what?

be promted to create a new project that will contain the IP core. You click ”ok”
on this and Vivado will automatically start a temporary project based on the IP’s
code. The initial structure should look like in Figure 13.

Figure 13. Source files.

In this project, you can make your changes. Most of your code will go into the
swipt toplevel.v or any other submodule files you define (such as a file for the fre-
quency tracking algorithm). Initially, the swipt toplevel module just ties the output
signals to one or zero. You have to add your own code to get something useful out
8. SWIPT IP BLOCKS 218

of it, and/or you have to call your own submodule files here.

The files above this toplevel were automatically generated by Vivado when the
IP was originally created and provide the AXI interface. Again, you can also make
changes here but do not break it (only do it when you really know what you’re
doing).
Tutorial 19.8.1 (Add verilog files) — Further, you can add the Verilog files previ-
ously used in your simulations to this project as follows:
(1) Save the Verilog files in the hdl -folder of the swipt module. To make sure
that the Verilog files you use in simulation are the same as the ones you
use in the Vivado project, it could be a good idea to use symlinks.
(2) In the Flow Navigator, under project manager, click on ’Add Sources’
(3) Select ’Add or create design sources’ and click ’next’
(4) Click on ’Add Files’
(5) Select the files you want to add (they are located in the hdl -folder of the
swipt module)
(6) Click finish
Finally, once you are done changing the IP. You have to repackage it. To do so,
go to the ”Package IP” tab in the project manager as shown in Figure 14. After
some changes not all items might have a green tick. Go into these items and rerun
whatever requires rerunning. Once all items show a green tick such as in Figure
14, click ”Re-package IP”. This generated the IP again and bring you back to the
EAGLE project. Here, Vivado will normally ask to update the changed IP and you
should do so. If it does not ask to do so, you should look to do this manually.

Figure 14. IP packager window.

8.2. ADC and Constants. You can change the configuration of the ADC, by
double clicking on the ADC block. The only changes you should make are located at
the ’Channel Sequencer’ tab indicated by the green box in Figure 15. Here you can
activate the channels you want to use (only channel 6, 7, 14 and 15 are available on
the Zybo). For each channel you can indicate if you want to use it as a unipolar or a
bipolar channel. More info on the difference between unipolar and bipolar and how to
use them can be found in chapter 2 of the complete XADC User Guide: https://docs.
xilinx.com/r/en-US/ug480 7Series XADC. For more help, you can also take a look
at the following instruction document and demo: https://cdn.instructables.com/
ORIG/FRT/SYN1/IWMMH04D/FRTSYN1IWMMH04D.pdf and https://digilent.
9. PROGRAMMING THE ZYBO FOR SWIPT 219

com/reference/learn/programmable-logic/tutorials/zybo-xadc-demo/start.

Initially, only channel 14 and 6 are active. The first is used as a bipolar channels,
the second as an unipolar channel. If you want to activate an extra channel, you have
to add two extra input ports to the Block Design. These ports should be connected
to the correct physical pins. The names Vivado uses to refer to the pins can be
found at Section ”16 Pmod Ports” of the Zybo Reference Manual https://reference.
digilentinc.com/reference/programmable-logic/zybo/reference-manual. This docu-
ment can also be used if you want to use other pins or push buttons, e.g. to debug
your code.

Figure 15. Activate the channels you want to use

9. Programming the Zybo for SWIPT


Tutorial 19.9.1 (SWIPT on Zybo) — To test the SWIPT module on the Zybo,
you have to make sure that the clock is activated. This requires some more steps
than previously explained for programming the Zybo.
(1) Generate the bitstream by clicking on ’Generate Bitstream’ in the Flow
Navigator under ’Program and Debug’
(2) When the bitstream is generated click File → Export → Export Hard-
ware.
(3) Select ’Include bitstream’ and click ’OK’.
(4) Connect the Zybo via USB (PROG UART Port) to the computer
(5) Program the Zybo as described before in this chapter.
(6) Once you programmed your Zybo, you have to activate the Zynq cores.
To achieve this, carefully follow the steps from Tutorial 20.1.1, Section 2,
Tutorial 20.2.2, Tutorial 20.3.1 and Tutorial 20.3.2. The only difference is
that you can use an empty file (choose Empty Application ), so you don’t
need to add any code.
9. PROGRAMMING THE ZYBO FOR SWIPT 220

(7) Go back to Vivado and open the Hardware Manager from the Flow Navi-
gator.
(8) On the top choose Open Target → Autoconnect.
(9) Now you can program The Zybo by clicking on ’program device’
Tutorial 19.9.2 (Accessing AXI-interface registers on the Zybo) — When running
the SWIPT module on the Zybo, it can often be useful to access internal AXI-
interface registers, either for checking certain variables while your program is running
or to control the SWIPT module with signals from other modules.
(1) To write variables from Vivado to the AXI-interface registers, open AXI-
interface file in the SWIPT IP block (the file above swipt toplevel.v).
(2) Scroll down in this document to the section where the slave registers are
assigned to the reg data out variable.
(3) To store a variable in the AXI-interface registers, replace one of the slave
registers with your variable (make sure this variable is accessible in this
module file).
(4) Your variable is now being written to the AXI-interface registers. To access
these registers, first look up the adress of these registers in the Address
Editor tab of the drone project file (Note that there are 8 registers available
for SWIPT. You can access each by incrementing the base address by 100
written in hexadecimal notation).
(5) Now you need to access these registers while the Zybo is running your code.
To do that, you need to boot your Zybo using an SD card as explained in
Tutorial 22.1.2.
(6) Install the rwmem Python package on your SD card as explained in Tuto-
rial 22.2.1 and 22.2.4.
(7) By booting the Zybo with this SD card and programming it with your
Vivado code, you can read out your registers by opening a shell as explained
in Tutorial 22.1.4.
(8) Finally, display the register values by calling the rwmem function with the
register address as the argument.
You can use this method to check variables in your code, if it is not behaving as
expected. You can also use these registers to integrate data from other modules into
your SWIPT code.
CHAPTER 20

Vitis Software Design Project


Zhenda Zhang

1. Setting up
Tutorial 20.1.1 (Starting Vitis) — Analogous to Tutorial 19.1.1, we need to source
the Xilinx tools before we can launch Vitis:
(1) Start an ESAT computer under Rocky Linux and open a terminal.
(2) Now we tell our machine where the Xilinx software is located:
source␣~micasusr/design/scripts/xilinx_vitis_2021.2.rc

(3) Now open Vitis with the following command


vitis␣&

Tip 20.1.1

The steps in this specific tutorial are specifically for the PCs at ESAT. If you
attempt to run Vitis from home, please refer to Tutorial 19.7.1 or Tutorial
19.7.2.
Alternatively, when working with a hardware platform exported from Vivado, it
is possible to launch Vitis from Vivado (cf. Tutorial 19.5.2). When launching Vitis,
you will be asked to select (or create) a workspace. This is a directory in which
associated Vitis projects are collected. For example, for the CRYPT tasks, you may
want to keep the Zybo Linux project (for deployment) and a local C/C++ project
(for testing) in the same workspace. When launching Vitis from Vivado, you will be
asked to select a workspace.

2. Creating a project
In order to create a Vitis project for code that runs on the Zybo, we first need
to create a platform project: this project defines the hardware the code runs on
(including the bitstream). A platform consists of several domains: one domain is a
place where code can run, e.g. a CPU core. The Zybo has two such domains, one
for every ARM Cortex-A9 core. The platform project also defines how boot images
(that is, BOOT.BIN files) should be generated, depending on these domains.
Once such a platform project has been made, several application projects can be
made for them, associated with a specific domain, in which code can be written.
Tutorial 20.2.1 (Creating a Vitis platform project) — Embedded C/C++ software
projects in Vitis need a Vivado platform to run on. The platform is defined by a
Vivado project, which includes the configurations of the ZYNQ processing system.
221
2. CREATING A PROJECT 222

(a) (b)

Figure 1. Creating a new platform project (1)

This section continues from the hardware platform file (XSA) generated in Tuto-
rial 19.5.1. The hardware platform contains the ZYNQ processor, which has access
to the LEDs and switches over some AXI GPIO blocks. Refer to Tutorial 19.5.2 on
how to export the XSA file of a Vivado project, for use in Vitis. For your convenience,
the XSA file of the AXI GPIO example from Section 2 and of the EAGLE Vivado
project (cf. Tutorial 19.6.1) are available in the EAGLE-students git repository, in
the Module software/ZYBO/examples/ folder.
(1) Go to File → New → Platform Project (Figure 1a)
(2) Enter the name of the platform project, and press Next, as shown in Figure
1b.
(3) Specify the XSA file to be used for the platform. Click the Browse... button
to select the file exported from Vivado, and click Finish. See Figure 2a for
an illustration.
(4) Now you should see an overview of the platform project. In this screen,
you can add domains if needed. This is shown in Figure 2b.
Two domains are generated by default: the FSBL domain, and the standalone
domain. The latter is the domain in which the application code will run, while
the former is responsible for loading your code into RAM, programming the FPGA
bitstream, and so on.

Warning 20.2.1

Note that the platform project depends on the exported XSA file from the Vi-
vado project. Therefore, whenever you make changes in Vivado, it is important
that the platform project is updated accordingly. To do this, you first need to
regenerate the bitstream and export the hardware (cf. Tutorial 19.5.2). Then,
right-click the platform project and press Update Hardware Specification.
Select the new XSA file and press OK.

Tutorial 20.2.2 (Creating a C/C++ application project for bare-metal Zybo) —


Now that we have a platform project, we can make an application project:
(1) Go to File → New → Application Project.
2. CREATING A PROJECT 223

(a) (b)

Figure 2. Creating a new platform project (2)

(a) (b)

Figure 3. Creating a new Zybo baremetal project (1)

(2) Select the platform made in the previous tutorial from the list, and press
Next.
(3) In this step, the project is given a name, and a target processor can be
selected (c.f. the domains of the platform project). Make sure the operating
system is set to standalone. Generate boot components may be left checked.
When finished, press Next.
(4) Here, a template can be selected. Typical choices are either the Empty
Application of the language you want to use, or Hello World.
2. CREATING A PROJECT 224

(a) (b)

Figure 4. Creating a new Zybo baremetal project (2)


2. CREATING A PROJECT 225

Tutorial 20.2.3 (Using the AXI GPIO interface in Vitis) — Here we finish the
hardware/software codesign example from Tutorial 19.5.2. The XSA file was ex-
ported from Vivado. We now need to write software that interacts with the AXI
GPIO blocks in order to enable the LED outputs depending on the switch inputs.
the following code in helloworld.c

#include␣<stdio.h>
#include␣"platform.h"
#include␣"xil_printf.h"
#include␣"xgpio.h"
#include␣"xparameters.h"
#include␣"sleep.h"

int␣main()
{
␣␣␣␣XGpio␣input,␣output;
␣␣␣␣int␣btns␣=␣0;

␣␣␣␣// initialize AXI GPIO blocks


␣␣␣␣XGpio_Initialize(&␣input,␣XPAR_AXI_GPIO_0_DEVICE_ID);
␣␣␣␣XGpio_Initialize(&output,␣XPAR_AXI_GPIO_1_DEVICE_ID);

␣␣␣␣// set input/output directions of separate bits


␣␣␣␣XGpio_SetDataDirection(&␣input,␣1,␣0xf);
␣␣␣␣XGpio_SetDataDirection(&␣input,␣2,␣0xf);
␣␣␣␣XGpio_SetDataDirection(&output,␣1,␣0x0);

␣␣␣␣init_platform();
␣␣␣␣print("Hello␣World\n\r");

␣␣␣␣while␣(1)␣{
␣␣␣␣␣␣␣␣// read switch state and write it directly to the LED state
␣␣␣␣␣␣␣␣int␣switches␣=␣XGpio_DiscreteRead(&input,␣1);
␣␣␣␣␣␣␣␣XGpio_DiscreteWrite(&output,␣1,␣switches);

␣␣␣␣␣␣␣␣// read button state, and compare it to the previous one


␣␣␣␣␣␣␣␣int␣newbtn␣=␣XGpio_DiscreteRead(&input,␣2),
␣␣␣␣␣␣␣␣␣␣␣␣diff␣=␣btns␣^␣newbtn;

␣␣␣␣␣␣␣␣// if (newbtn & diff) xil_printf("btnpress: 0x%x\n\r", diff);


␣␣␣␣␣␣␣␣// if (btns & diff) xil_printf("btnrelease: 0x%x\n\r", diff);

␣␣␣␣␣␣␣␣btns␣=␣newbtn;
␣␣␣␣␣␣␣␣usleep(20*1000);␣// sleep 20 ms
␣␣␣␣}
␣␣␣␣cleanup_platform();
␣␣␣␣return␣0;
}

Once the project has been created, set the active build type to Hardware, by
right-clicking Hardware in the Assistant on the lower left, and clicking Set Active,
cf. Tutorial 20.2.3.
Now follow the tutorials in Section 3 and onwards, to run the code on the Zybo.
You should once again be able to control the LEDs using the switches, and see the
“button pressed/released” messages in the serial console.
A few remarks about this code:
3. COMPILING, RUNNING AND DEBUGGING CODE 226

Figure 5. Setting the active build type to Hardware

✓ Note how the input GPIO has two channels, for resp. the switches and
the buttons, just as configured in the Block Customization window in
Vivado.
✓ It is possible to control the input or output direction of individual pins, as
XGpio SetDataDirection uses bitflags to mark the direction. For switches
and LEDs this is not very useful, but this way it is possible to mark in-
dividual pins of a Pmod port as input or output, or even change it at
runtime.
✓ Note that this implementation is very much unlike a pure-FPGA one: while
an FPGA sends data through a number of blocks connected to one another,
doing computations in parallell, this code functions like a state machine
that first reads the switch state, stores the result internally, and then sends
that state to the LEDs, and then starts over again after a short pause.
This is all very sequential, and this is the big difference between hardware
(FPGA) and software designs.
✓ The code uses some bit manipulation tricks to work out the difference
between the old and new button states. Can you work out how it works?

3. Compiling, running and debugging code


Tutorial 20.3.1 (Compiling code in Vitis) — In order to run your C/C++ code,
you first need to compile it. This can be done by right-clicking the project in
the Explorer sidebar and then clicking Build Project or by clicking Project →
Build All, or by pressing Ctrl+B, as seen in Figure 6a. If there are any compiler
errors (syntax errors, typos in variable names, etc.), these will be shown on the
bottom of the screen.
3. COMPILING, RUNNING AND DEBUGGING CODE 227

(b) Running C code on the Zybo, bare-


(a) Compiling code in Vitis metal

Figure 6. Compiling and running the code.

(a) JTAG mode (b) QSPI mode (c) SD boot

Figure 7. The jumper configurations for the different boot modes


of the Zybo.

Tutorial 20.3.2 (Running your code on bare-metal Zybo) — Running a bare-metal


Zybo project should be done by right clicking on the .c file and selecting Run →
Launch Hardware (Single application debug). See Figure 6b.
Note that, for the above to work, your Zybo should have its JP5 jumper set to
JTAG, as shown in Figure 7a, its USB cable plugged in using the PROG UART port
(USB OTG will not work), and its power switch is set to ON. Powering on the Zybo
will in this case not start the code, launching it from inside Vitis will.

Tutorial 20.3.3 (Launching a serial terminal) — The output of code running on


the Zybo can be read using a serial terminal when the computer is connected
to the PROG UART port of the ZYBO. Opening a serial terminal can be done in
Vitis, by going to the Vitis Serial Terminal tab (next to the Console tab),
then clicking the Connect to serial port button, selecting the serial port
(/dev/ttyUSB<number> or CON<number> depending on your operating system, where
<number> is usually 1 or 0), and clicking OK, see Figure 8a for an illustration.
3. COMPILING, RUNNING AND DEBUGGING CODE 228

(a) (b)

Figure 8. Opening a serial console in Vitis

If the Vitis Serial Terminal cannot be found, go to Window → Show view...,


and scroll down and select Vitis Serial Terminal, as shown in Figure 8b.

Tip 20.3.1

On Linux, it is also possible to open a serial console by running the following


command in a terminal:
screen␣/dev/ttyUSB1␣115200

This method is required when using VNC to access a remote ESAT machine,
and running Vitis on that machine, as the USB serial console is only available
locally, cf. Tutorial 19.7.2.
On windows, PuTTY can also be used. Note that it is very important here
to set the speed (baudrate) to 115200.

Tip 20.3.2

When connecting a serial console, you will be prompted to provide a username


and password when Linux is running on the Zybo. The login username is root
and the password is eagle.
You may have noticed that when the code is supposed to access the FPGA (e.g.
for Crypto), it hangs. This is because Vitis may need an extra setting to program
the FPGA as well, when launching a bare-metal program on the Zybo. This can
be changed by going to Run → Run Configurations..., and then, in the Target
Setup tab, check the Program FPGA and Reset entire system checkboxes (you
may need to scroll down a bit to see them), as shown in Figure 9a and Figure 9b.
4. DEPLOYING STANDALONE CODE ON A ZYBO 229

(a) (b)

Figure 9. Configuring Vitis to program the FPGA when starting


baremetal C code on the Zybo

Tutorial 20.3.4 (Debugging code in Vitis) — Debugging code is done similarly


to running it: click Debug → Debug As → Launch Hardware, which will open the
Vitis debugger view.
Here you can pause and resume execution of your code, run code line by line,
see the contents of variables, see by which other functions the current one was
called by reading the call stack backtrace, and pause execution on specific lines using
breakpoints. As Vitis is based on the Eclipse IDE, and the debugging functionality is
taken directly from it, please refer to the Eclipse IDE about specific documentation
on using the debugging functionality.

4. Deploying standalone code on a Zybo


Deploying code on a Zybo without a link to the computer happens in two phases:
creating a BOOT.BIN file, containing all the code that gets executed on boot, and
placing it on a bootable storage medium.
Tutorial 20.4.1 (Creating a BOOT.BIN file from a Vitis hardware project) — Cre-
ating a BOOT.BIN file containing only bare-metal code, as Vitis performs most of the
heavy lifting:
(1) Compile the project (cf. Tutorial 20.3.1)
(2) A BOOT.BIN file should now have been created in either the Debug/sd card/
or the Release/sd card/ folder of the system project, depending on your
configuration, as shown in Figure 10.
(3) If the file is not created, go to the project settings of the system project,
and make sure the Generate SD card image checkbox is checked.

Tip 20.4.1

If you need to create a BOOT.BIN that includes Linux, please refer to Tutorial
22.1.6. If you have a usable BOOT.BIN file and put it on a microSD card. Follow
Tutorial 20.4.2 to boot from the microSD card.
4. DEPLOYING STANDALONE CODE ON A ZYBO 230

Figure 10. Automatic BOOT.BIN output from Vitis

Now that we have a usable BOOT.BIN file, we need to put it somewhere, e.g. on
a microSD card.
Tutorial 20.4.2 (Booting a BOOT.BIN file on a microSD card) — Booting from a
microSD card is relatively easy.
(1) Put the BOOT.BIN file on the root of the microSD card, not in a (sub)folder
of it.
(2) Set the JP5 jumper in the SD position (see Figure 7c), put the microSD
card in the Zybo, and turn the power on.
Tutorial 20.4.3 (Booting a BOOT.BIN file on QSPI flash memory) — This boot
method could for example be useful when you do not have access to a microSD
card.
(1) Set the JP5 jumper in the JTAG position, and power on the Zybo. Make
sure the Zybo is connected to your computer using the USB UART port.
(2) In Vitis, go to Xilinx → Program Flash as shown in Figure 11a.
(3) In the popup window (Figure 11b), first select the project of which you
generated a BOOT.BIN file in the previous part.
(4) As Image File, browse to the BOOT.BIN file generated in the previous part.
(5) Click Select... next to the Device field, and select the Zybo
(<number>-xc7z010). This should set the Flash Type to the correct value,
if it wasn’t already.
(6) Enable the Verify Flash checkbox to make sure flashing went correctly.
(7) Click the big Program button.
(8) Disconnect the Zybo from the computer, and put the JP5 jumper in the
QSPI position.
(9) Reconnect the Zybo and power it on. The code should now boot.
4. DEPLOYING STANDALONE CODE ON A ZYBO 231

(a) (b)

Figure 11. Programming the Zynq QSPI flash memory.


CHAPTER 21

Zybo – Crypto Project


Zhenda Zhang

1. Verilog Introductory Exercise


The goal of this warm-up exercise is to introduce essential principles of hardware
design on FPGA in Verilog by implementing a simple calculator. In the following
text, Section 1.2 describes functionalities that the FPGA calculator should have.
The calculator consists of several submodules implemented in Verilog. The descrip-
tion of each submodule, together with its interfaces and connections, is given in
Section 1.3. Section 1.4 gives tutorials on design flows on FPGA. It first starts with
a roadmap of the design entries. Secondly, it describes the behavioral simulation
step, then thirdly synthesis and programming in the FPGA.

Figure 1. FPGA calculator.

1.1.
1.2. High level description of the calculator. The FPGA calculator uses 4
switches, 4 buttons, 4 LEDs and a clock signal, as depicted in Figure 1. One button
is used as a reset signal, while the other 3 are ‘control’ buttons. The calculator works
with 4-bit data that it receives through switches and it can perform the following
6 operations: STORE, NOT, ROL (rotate input 1 position to the left), AND, OR,
and XOR. The first three operations require 1 operand and the last three require
at least 2 operands. One specific feature of this calculator is that it can accumulate
operations.
232
1. VERILOG INTRODUCTORY EXERCISE 233

The calculator works as follows. At the beginning of each calculation that re-
quires at least 2 operands (AND, OR, XOR), the user first selects operation STORE
by setting the switches in the predefined position. Then the first button is pressed to
confirm the operation. The user then chooses the first operand by again setting the
switches so that they correspond to the binary representation of the desired number.
The chosen operand is confirmed by pressing the second button. This operand is
stored in the internal accumulator register of the calculator. All subsequent calcula-
tions that require at least 2 operands will use the value in the accumulator as their
first operand. To select the next operation, the user sets the switches in one of 3
positions. Then the first button is pressed again to confirm the operation. The user
then chooses the second operand by setting the switches so that they correspond to
the binary representation of the desired number. The chosen operand is confirmed
by pressing the second button. If the user wants to use more operands, the same
procedure is followed – setting the switches to correspond to the operand’s binary
representation and then pressing the second button. Finally, to display the result
on LEDs, the user presses the third button. The calculator and the accumulator
can be reset by pressing the fourth button. However, to perform calculations that
require only one operand (NOT, ROL, STORE), the STORE operation does not
need to be performed. Therefore, the user sets the switches to choose the operation,
presses the first button, then sets the switches to choose the operand, and presses
the second button and the result can be displayed by pressing the third button.
The calculator operations are encoded as follows (1 – switch in ‘ON’ position, 0
– switch in ‘OFF’ position):
✓ 0000 – STORE
✓ 0001 – XOR
✓ 0010 – AND
✓ 0011 – OR
✓ 0100 – NOT
✓ 0101 – ROL

1.3. Description of the Submodules. The implementation of the calculator


consists of several modules and their Verilog templates can be found in the directory
Verilog exercise v1. Each submodule is located in a separate Verilog (.v) file with
the same name as the corresponding module.
Calculator top. This is the highest level module as it contains all other mod-
ules and provides an interface to the physical FPGA pins, as shown in Figure 2
and Figure 3. Inside this module, there are 3 multi-bit registers (instances of the
module Flex register) which are used to store the input signals from the switch-
es/buttons and the output signals to LEDs. Further, there are 2 more modules –
instances of Control unit and Datapath. This is where all actual operations are
performed. Your task for this module is to instantiate and correctly connect all 5 of
its submodules, as in Figure 3.
Flex register. This is the simplest module and it describes the behavior of a
multi-bit register with enable signal. Note that the size of the Flex register is
not the same for all its instances. Thus, you will have to use the Verilog construct
parameter to implement the register of the right size for each instantiation.
Control unit. The Control unit generates control signals that regulate the op-
erations of the elements of the Datapath – selecting operations to be performed and
enabling registers. It ensures that control signals are activated in the right order and
1. VERILOG INTRODUCTORY EXERCISE 234

Figure 2. Interface of the Calculator top module.

Figure 3. Submodules of the Calculator top module.

at the right times to cause the Datapath to perform the required operations on the
data flowing through it. Here, you should design a small Moore FSM (finite state
machine) which is going to issue signals to the Datapath and the output LED regis-
ter. Read carefully the functional description of the calculator from the Section 1.2
to understand which enables signals to be generated and when. Additionally, Figure
4 gives you a full set of states that should exist in your FSM and an incomplete
state transition diagram.
Datapath. This module should consist of 4 submodules – 1 instantiation of ALU
(arithmetic logic unit) module and 3 instantiations of Flex register-s – 1 for
storing input data, 1 for storing the operation and 1 for the accumulator. Note
1. VERILOG INTRODUCTORY EXERCISE 235

Figure 4. Incomplete FSM of the Control unit.

that the accumulator is used for both storing the result of the operation and as an
operand. You should only instantiate necessary submodules and correctly connect
them.
Bit slice ALU. To implement ALU module, you will first create a simpler “bit-
slice” version that implements a 1-bit wide ALU. This unit should contain com-
binational circuits that describe 6 operations of the calculator based on the user
selection. You should get an idea of how to implement the necessary logic by taking
a look at the interface in Bit slice ALU.v.
ALU. This module chains multiple instances of Bit slice ALU together. Use
Verilog generate statement to create parametrizable 4-bit wide ALU.
1.4. Tutorial to program the FPGA calculator. After following the tuto-
rial, you should be able to program the FPGA calculator on the Zybo board and
test its functionalities. The tasks in EAGLE-CRYPT project, or hardware design
in general, will be similar to this tutorial.
Tutorial 21.1.1 (Road map of hardware design in Verilog) — This tutorial is the
full roadmap for the FPGA calculator exercise. Start with the following steps:
✓ Make a new Vivado project Calculator exercise.
✓ Add all Verilog source files to your project – they are located in the directory
Verilog exercise v1).
(1) Bit slice ALU.v
(2) ALU.v
(3) Flex register.v
(4) Datapath.v
(5) Control unit.v
(6) Calculator top.v
✓ Functional simulation: To verify that your design is functioning properly
you should have a test bench and simulate it (cf. Chapter 6 of [LaM19],
Chapter 13 Section 4). You can find and import file Verilog exercise v1/tb calculator top.v
✓ Synthesis and implementation: Once you have verified that your design is
working properly, you can synthesize and implement it. you will need a
file with constraints that contains timing specifications and a description of
links between input/output ports of Calculator top module and physical
pins of the FPGA. The name of this file is top calculator zybo cstrs.xdc
2. AXI PERIPHERALS FOR CUSTOM HARDWARE-SOFTWARE INTERFACES 236

and it has already been written for you. You can add it to your project
following Tutorial 19.2.2.
✓ By following Tutorial 19.2.3, Vivado will first run synthesis, then im-
plementation and finally it will generate bitstream file .bit which you
will use to program Zybo in the next step.
✓ Once the design has been implemented, make sure that the tool does
not report any errors. If there are any errors or warnings, you should
go through them carefully and try to resolve them by fixing bugs in
your design. Watch out for synthesized latches, these will usually not
function correctly in the implemented design. Note that the warnings
about missing PS7 are not critical, since we do not need the CPU in
this design.
✓ You should pay attention to the following warnings:
✓ “MULTI-DRIVEN NET” – a wire has more than 1 input; due
to ambiguity, this is not allowed and you should find the prob-
lematic wire in your design and check if it is driven in more than
1 always block or assign statements.
✓ “DISCONNECTED PORT” – an output or input port that is
not connected.
✓ “NET WITHOUT DRIVER” – opposite of ‘multi-driven net’; a
signal that is declared but never assigned a value.
Knowing the meaning of these warnings will be useful when you im-
plement your own cryptographic hardware accelerators.
✓ Programming the FPGA: Once you have generated the bitstream file, you
can program your design on the Zybo by following Tutorial 19.2.5. Test
that your design works in hardware by changing the states of the switches
and pressing corresponding buttons as described in Section 1.2.

2. AXI peripherals for custom hardware-software interfaces


Hardware accelerators are usually used together with the CPU. A typical way
for these two to communicate is through Memory-mapped I/O (MMIO). You can
implement an ALU or a cryptographic core as a memory-mapped AXI peripheral.
The hardware accelerator defines a set of registers, which are addressable by the
CPU as a subset of its memory addresses. The conversion from memory read/writes
by the CPU to the set of signals that address a specific register, is done by the AXI
(Advanced eXtensible Interface) protocol.
Tutorial 21.2.1 (Creating a new AXI4-Lite peripheral) — This tutorial is required
for the CRYPT sub-project, both for the introductory Verilog exercise, and the
custom AXI-Lite peripheral later in the project.
To create such a peripheral, follow these steps:
(1) As with Tut. 19.4.1, start by clicking Tools → Create and Package New
IP..., and click Next.
(2) Instead of selecting Package your current project, select Create a new
AXI4 peripheral, and press Next.
(3) Enter the name of the peripheral and click Next once more.
(4) You are now shown the AXI4 peripheral wizard. Most defaults are ok here,
except you may want to change the number of registers available. For the
CRYPT Verilog introduction exercise, 4 is enough, but for the later tasks,
2. AXI PERIPHERALS FOR CUSTOM HARDWARE-SOFTWARE INTERFACES 237

the value depends on the interface with the CPU you have in mind.1 Click
Next once the options are ready.
(5) In the next screen, select Edit IP and click Finish.
(6) A new Vivado project is opened. In the Design Sources, two Verilog mod-
ules should be already created: a top-level module, and a submodule for
the AXI slave interface. The latter needs to be edited to have the IP core
react to reads and writes from the CPU.
(7) For example, under the Signals for user logic register space example
block of variable declarations, you can add extra logic, for example, import
the Verilog module you want to expose to the CPU. (These modules first
need to be imported to the IP core project.) See Fig. 5a for an example.
(8) At the Implement memory mapped register select and read logic
generation ... block, you can replace the slv regs used with outputs
from the imported module, as shown in Fig. 5b.
(9) The Implement memory mapped register select and write logic
generation ... block (Fig. 6a) is where writes happen. If your Verilog
module reacts to changing inputs properly without the need for an extra
signal, no changes are needed here. However, if you want a certain action to
happen on a register write (for example, starting a certain operation), extra
logic will have to be added here, see Fig. 6b and Fig. 7 for an example.

Warning 21.2.1

This part is where most bugs are introduced. Be sure to check your
code very thoroughly, to ensure you have not forgotten any cases. For
example, forgetting the extra else in Fig. 7 will cause the cmd reg
to not be reset to zero until another write happens, which is not the
intended behavior.
(10) Once all required code is added, you can package the IP as usual, as ex-
plained in Tut. 19.4.1.
This new IP core can now be added to an existing block design, as explained
in Tut. 19.4.2. However, it is also possible to create a new block design specifically
made to test this peripheral, and include nothing else. For this, following these steps
is recommended:
(1) The IP repository containing the newly-made AXI peripheral should have
been automatically added to the project in which the Tools → Create
and Package New IP... step has been performed. If not, import it by
following the steps explained in Tut. 19.4.2.
(2) Create a new block design by clicking Flow Navigator → IP Integrator
→ Create Block Design.
(3) As with Tut. 19.4.2, add the newly-made AXI peripheral block by clicking
the Add IP button, and searching and navigating to the right IP core.
(4) Now, add another IP core, called ZYNQ7 Processing System.
(5) In the green bar that will appear at the top of the block diagram, click Run
Block Automation, and click OK as the defaults are fine.
(6) Now click Run Connection Automation in the same green bar as in the
previous step. The defaults should once more be ok.
1If you do not yet have one, now is the time to think about one.
2. AXI PERIPHERALS FOR CUSTOM HARDWARE-SOFTWARE INTERFACES 238

(a) (b)

Figure 5. Inserting a Verilog module into an AXI peripheral (1)

(a) (b)

Figure 6. Inserting a Verilog module into an AXI peripheral (2)

(7) Now a diagram with extra Processor System Reset and AXI Interconnect
blocks should have appeared.
(8) Now go to the Platform Setup tab and open AXI Port. Enable the
M AXI GP1 interface under processing system7 0.
(9) Open Clock, enable FCLK CLK0 and set it as default.
(10) Proceed to the Address Editor tab. Here you can configure the address
at which the CPU can access the AXI peripheral.
2. AXI PERIPHERALS FOR CUSTOM HARDWARE-SOFTWARE INTERFACES 239

Figure 7. Inserting a Verilog module into an AXI peripheral: do


not forget this one.

(11) Go to the block diagram in the source file list, right-click it and click Create
HDL Wrapper..., and let Vivado auto-manage the wrapper. Set the newly-
created wrapper module as project top.
(12) You can now generate a bitstream and export the hardware platform for
use in Vitis and on real Zybo hardware, as explained in Tut. 19.5.2 and
the next chapter about Vitis.

Tutorial 21.2.2 (Debugging a custom AXI-Lite peripheral) — It often happens


that, while the module itself works in simulations, things tend to go wrong in the
hardware-software interface, for example, a signal being sent by the software side,
but seemingly not being received on the hardware end. Debugging this interfacing
code is often quite difficult.
If trial-and-error “change a small thing and see if it influences the situation in
an unexpected way”-debugging did not help and you have no idea what is going
wrong, extra tooling is available to analyze the situation. However, as these incur
extra difficulties to use, their use is discouraged as they will often make the situation
worse. We strongly suggest you contact your TAs instead.
The two main tools are the AXI Verification IP, explained in §2 of the TA-only
chapter, and real-hardware in-system debugging and waveform viewing using ILA
debug cores, cf. Tut. 31.1.1. The former requires the knowledge of SystemVerilog,
while the latter inserts extra modules into the bitstream, potentially delaying signals
that are already close to the deadline, making the system unimplementable.
3. RUNNING C CODE LOCALLY USING VITIS 240

3. Running C code locally using Vitis


Vitis has many different project templates for many different purposes. In the
previous chapter on general Vitis use, the workflow for bare-metal C or C++ code
on the Zybo has been explained. However, for the CRYPT project, we also need a
few other project types, as the code will end up running on the Linux system on the
Zybo. Therefore, we will discuss a local C/C++ project that runs on your computer
and a Zybo Linux project that will run under Linux on the Zybo. While running
CRYPT code on bare-metal Zybo is also not strictly required for the CRYPT project,
it can still be a big help for testing the FPGA interfacing code, both for the Verilog
exercise, and for the custom FPGA interface for the hardware accelerator, much
later in the project.
This first tutorial explains how to test C code for the CRYPT project locally
in Vitis. As the C code will have to run on the Zybo under Linux at the end
of the project, additional tutorials follow. However, it can still be a big help for
debugging hardware-independent “logic” code in your project, such as the CRYPT
protocol/kdf ae() implementation. Additionally, it is also required to run the code
on a regular computer when running the entire system using the ANC simulator.
The previous chapter about the general use of Vitis, Ch. 20, is required reading for
this chapter.

Tip 21.3.1

Students that are proficient in C/C++ and want to use their own IDE rather
than Vitis can do so. Refer to §4 for instructions on how to use the provided
Makefile.

Tutorial 21.3.1 (Creating a local C/C++ project) — A project can be created by


going to the menu bar at the top of the Vitis window, and selecting File → New
→ Other... as shown in Fig. 8a. A popup opens, showing the New Project wizard.
Select C/C++ project under the C/C++ folder (cf. Fig. 8b). As template, use C
Managed Build or C++ Managed Build, depending on what you need (Fig. 9a).
Then, another popup opens, with options for how the C/C++ project should
be created, see Fig. 9b for an illustration. Here you can select the project name
(which corresponds to the name of the output executable) and a template. Pick
Hello World ANSI C/C++ project, not the ARM C/C++ project, so that the code
will be compiled and will be able to run on a regular computer. Finally, press Finish
to create the project.
Tutorial 21.3.2 (Importing the CRYPT code in a local C/C++ project in Vitis) —
First of all, follow Tut. 21.3.1 from Ch. 20, after which a few extra steps need to be
taken to import the pre-existing CRYPT project structure into Vitis:
(1) Right-click the project in the Explorer sidebar on the left. Then click
Import Sources... and import the Module software/CRYPT/SW folder from
the EAGLE project.
(2) Delete the pre-generated src folder from the Vitis template from the project.
Including it would cause compilation issues.
Tutorial 21.3.3 (Running and debugging the CRYPT C code locally) — Running
and debugging code locally is done very much like running or debugging it on bare-
metal Zybo: instead of choosing Launch on Hardware (System Debugger), select
4. RUNNING C CODE ON THE ZYBO IN LINUX 241

(a) (b)

Figure 8. Creating a new C/C++ project in Vitis (1)

(a) (b)

Figure 9. Creating a new C/C++ project in Vitis (2)

Local C/C++ Application cf. Fig. 10. The output of the program will be shown on
the bottom of the screen, in the Console tab. Alternatively, you can select Debug
→ Debug As → Local C/C++ Application to open the Vitis debugging view.

Tip 21.3.2

If Vitis complains about not finding the binary, you need to create a valid run
configuration. Start as in Fig. 10, but press Run Configurations.... Right click
C/C++ Application, and select New Configuration. Set your project correctly,
and set C/C++ Application to Debug/${project name}.

However, for the CRYPT project, you will most likely encounter an error, as
shown in Fig. 11a. This can be fixed by importing the Module software/CRYPT/Tests
folder similarly to how you imported the Module software/CRYPT/SW folder. If the
link has been created correctly, you should see the console output as in Fig. 11b.

4. Running C code on the Zybo in Linux


Tutorial 21.4.1 (Creating a C/C++ project for Linux on the Zybo) — Creating
a new project for Zybo Linux code is done by clicking File → New → Other....
Select C Managed Build or C++ Managed Build again in the popup, and instead
of selecting C/C++ project under C/C++, select Xilinx ARM v7 Linux Executable
4. RUNNING C CODE ON THE ZYBO IN LINUX 242

Figure 10. Compiling and running code inside Vitis

(a) (b)

Figure 11. Malfunctioning and correct console output.

under the Other folder, as shown in Fig. 12a. Enter the name of the project, and
press Next. In the next screen you can change compiler and run/debug settings,
see below. (You can also skip this part now and modify the project settings later.)
Afterwards, you can just press Finish.

Tutorial 21.4.2 (Importing the CRYPT code in Vitis for Zybo Linux) — After
having followed Tut. 21.4.1, a few extra steps need to be taken to import the pre-
existing CRYPT project structure into Vitis:
(1) Some symbols need to be defined for the C preprocessor. In the final
configurations settings part of the wizard, click Advanced settings..., this
will open another window. Then click C/C++ Build → Settings in the tree
view on the left, and then ARM v7 Linux gcc compiler → Symbols. Add a
new symbol called ZYBO LINUX to make the code be aware that it will run
on Linux on the Zybo, you can add additional symbols here if wanted (eg.
HW 20 ROUNDS). If all this is done, click Apply and Close, and then Finish.
All this is illustrated in Fig. 12b.
(2) Then, include the pre-existing CRYPT project code into Vitis, as explained
in Tut. 21.3.2.
4. RUNNING C CODE ON THE ZYBO IN LINUX 243

(a) Creating a new C project for (b) Configuring the CRYPT project for Linux on
Linux on the Zybo the Zybo

Tip 21.4.1

There are different HW xxx preprocessor symbols used to specify which FPGA
interface should be used, and which capabilities it has. You typically progress
from one to another when moving to a different implementation phase. The full
list is:
✓ None: specifying no symbol makes the code fall back to the software
Keccak and KetjeSr implementations. This is the only available op-
tion when running code not on a Zybo. This is mainly useful when
working on other parts of the CRYPT code, such as the protocol (and
kdf ae()) implementation.
✓ HW 20 ROUNDS: The FPGA can be used, and is able to perform only a
fixed number of Keccak rounds, 20. This is probably the first setting
you want to enable when testing your FPGA implementation on real
hardware. called eaglecrypthw hash.
✓ HW ARBITRARY ROUNDS is the same as the above, except it can do a
variable number of Keccak rounds, which is required for KetjeSr
acceleration. is called eaglecrypthw aead hash.
✓ HW CUSTOM INTERFACE: this is for an FPGA implementation with a
custom interface, for when you eg. want to accelerate more operations
than just bare Keccak rounds. You will need to modify the code in
the inc/ directory to implement the CPU side of this.

Warning 21.4.1

Make sure to use the right FPGA interface (and HW xxx symbol) for the right
bitstream! A mismatch may cause hangs or other types of undefined behavior,
and will most likely break things.
4. RUNNING C CODE ON THE ZYBO IN LINUX 244

Figure 13. Executable for Linux on the Zybo, compiled by Vitis

Tutorial 21.4.3 (Running your code on Linux on the Zybo) — When you compile
your code (as described in Tut. 21.3.3), you should now have a file with the extension
.elf in the <projectname>/Debug/ folder under the Vitis workspace. Let’s presume
it is called eaglecryptsw-zybolinux.elf. See Fig. 13 for an example. The code
can be executed on the Zybo by following these steps:
(1) Place this file on the microSD card and plug it into the Zybo.
(2) Make sure the jumper is set to SD, and insert the USB cable in the Zybo
and in your computer while leaving the power switch at its OFF position.
(3) Power on the Zybo. You should see both the GOOD (red) and DONE (green)
LEDs light up, and the TX and RX (yellow-orange-ish) LEDs should start
flickering.
(4) Open a terminal, as explained in Tut. 20.3.3. This should be done relatively
quickly after plugging in the Zybo, or the login prompt may not be visible.
(You will still be able to log in.)
(5) After a while, a login prompt should be shown. The username is root, the
password is eagle.
(6) Go to the /media directory on the Zybo by running cd /media
(7) Run the file: ./eaglecryptsw-zybolinux.elf. It should now execute.

Tip 21.4.2

It is not straightforward to debug the code running under Linux on the Zybo.
In most cases, code that works locally will also work on the Zybo. If debugging
on the Zybo is required, refer to Advanced Tutorials §3 and §5.
4. RUNNING C CODE ON THE ZYBO IN LINUX 245

When integrating with other modules, the CRYPT code will receive and send
signals to other programs running on the Zybo or Raspberry Pi. These will go
through the EagleCrypt.py script, which is responsible for executing the C program.
Have a look at the provided bare-bones version of EagleCrypt.py, and see if you
can get the C program to work by only using the Python script.
CHAPTER 22

Zybo – Linux
Pieter Pas

1. Booting Linux on the Zybo


Tutorial 22.1.1 (Configure SSH to use public key authentication) — Perform the
following steps:
(1) Generate an SSH key pair as explained here. For example, save it as
~/.ssh/zybo id rsa. It is convenient to also add it to the ssh-agent.
(2) Edit your ~/.ssh/config file (on your computer) to include the following:
Host␣Zybo
␣␣␣␣HostName␣eagle.local
␣␣␣␣User␣root
␣␣␣␣PreferredAuthentications␣publickey
␣␣␣␣IdentityFile␣~/.ssh/zybo_id_rsa

See man ssh config for more information. Note that mDNS is used to
resolve the hostname.
Tutorial 22.1.2 (Booting Linux on the Zybo) — To boot Linux on the Zybo, you
need to perform the following steps:
(1) Format a microSD card as FAT32.
(2) Download sd-zybo.zip or sd-zybo-z7.zip (depending on whether you’re
using an original Zybo or a Zybo Z7) from Nextcloud and extract the
contents of the SD folder to the root of the SD card.
It is important that the files like BOOT.bin are in the root directory, not in
a subdirectory.
(3) Copy the public key you created in Tutorial 22.1.1 (e.g. ~/.ssh/zybo id rsa.pub)
to the root of the SD card.
(4) (Optional) Copy any compiled files you want to run on the Zybo to the bin
folder of the SD card (e.g. Autopilot).
(5) (Optional) Create a file with the name interfaces in the root of the SD
card to customize the network configuration.
(6) Inspect the autostart.sh file on the SD card, try to understand what it
does, and customize if necessary.
(7) Insert the SD card into the Zybo. Make sure the JP5 jumper is set to the
SD position option (cfr. Fig. 7c). Then power it up.
(8) Follow one of the follow-up tutorials below to access the Linux shell of the
Zybo.
Tutorial 22.1.3 (Connect to the Linux shell of the Zybo using USB) — After
booting Linux on the Zybo (see Tut. 22.1.2), perform the following steps:
(1) Connect to the PROG UART port of the Zybo via a USB cable.
(2) Attach to the Zybo’s login shell over the serial port using screen:
246
1. BOOTING LINUX ON THE ZYBO 247

screen␣/dev/ttyUSB1␣115200

If you have other serial ports on your system, the path might be different
from /dev/ttyUSB1. The default baud rate used by the Zybo is 115200
baud.
On Windows, you can use a tool like PuTTY instead of screen.
(3) Log in as user root using password eagle.
You should now have access to the Linux shell of the Zybo.
(4) When you’re done, cleanly shut down the Zybo as explained in Tut. 22.1.5.
Failing to do so can damage the SD card and/or the filesystem.
Some tips for using screen:
✓ To scroll back, enter “copy mode” by using Ctrl+A Esc. Navigate using
the arrow keys or h-j-k-l. You can use / and ? to search forward and
backward respectively, then use n or N to go to the next or previous match.
Esc Esc takes you out of copy mode so you can type the next command in
your Linux shell.
✓ To exit screen, use Ctrl+A \ (backslash). Confirm by typing y.

Tip 22.1.1

Connecting to the Zybo over USB is a simple way to get a quick shell, but
it is insufficient for serious development. It is highly recommended to use the
following tutorial to connect to the Zybo over SSH. This avoids issues with the
limited baud rate of the serial port, it allows you to quickly copy files to and
from the Zybo, to open and forward network sockets, enables remote debugging,
etc.

Tutorial 22.1.4 (Connect to the Linux shell of the Zybo over SSH) — After con-
figuring SSH (see Tut. 22.1.1) and booting Linux on the Zybo (see Tut. 22.1.2),
perform the following steps: (Note: if you changed the network configuration of the
Zybo, you might need to modify them slightly.)
(1) Connect to the Zybo via an Ethernet cable.
(2) Set up the Ethernet interface on your computer:
✓ Windows/WSL: Open “Network Connections” in Control Panel and
open the properties of your WiFi connection. In the “Sharing” tab,
enable “Allow other network users to connect through this computer’s
Internet connection” and select the Ethernet port that you connected
the Zybo to from the dropdown menu.
✓ Ubuntu (or another Linux distribution using NetworkManager): Open
the Network settings, and in the IPv4 and IPv6 tabs for the Ethernet
interface with the Zybo connected, select “Shared to other computers”.
(3) Connect to the Zybo over SSH, confirm with yes and enter when prompted
whether you want to connect:
ssh␣Zybo

You should now have access to the Linux shell of the Zybo.
(4) When you’re done, cleanly shut down the Zybo as explained in Tut. 22.1.5.
Failing to do so can damage the SD card and/or the filesystem.
1. BOOTING LINUX ON THE ZYBO 248

Tutorial 22.1.5 (Cleanly shutting down the Zybo) — It is important to properly


shut down the Zybo. Failing to do so will damage the SD card.
(1) Cleanly shut down Linux. In the command line of the Zybo, execute:
poweroff

If you have SSH set up (Tut. 22.1.4), you can also shut down the Zybo
directly from the command line of your computer, without first opening
the Zybo’s shell. In a terminal on your computer, execute:
ssh␣Zybo␣poweroff

(2) Wait five seconds for the system to shut down.


(3) Power off the Zybo using the switch next to the 5V jack or by disconnecting
the battery.
If you want to restart the Zybo instead of powering it off, you can use the reboot
command instead of poweroff.
Tutorial 22.1.6 (Creating a BOOT.bin file for booting a Linux system) — The
BOOT.bin file that you downloaded from Nextcloud in Tutorial 22.1.2 contains a
bitstream that only includes the IP for the Autopilot (e.g. for driving the motors
and reading the RC inputs). Eventually, you’ll have to create a new bitstream
that includes your custom CRYPTO and SWIPT IP. This tutorial explains how to
generate a BOOT.bin file that boots Linux and that includes custom IP.
(1) Make sure that you have exported the hardware with your custom IP in
Vivado.
(2) Download the bootloader-zybo.zip or bootloader-zybo-z7.zip file from
Nextcloud, depending on your Zybo version.
(3) Open Vitis, and create a new Application Project.
(4) Create a new platform from hardware (XSA).
(5) Select the XSA File, use the one generated by CRYPTO/SWIPT in the
first step. (Alternatively, if you don’t need custom IP, you can use the
default one in Module_software/ZYBO/examples/drone_wrapper.xsa.)
(6) Next.
(7) Choose an application project name.
(8) Select “Create new...” for the system project. Keep the default name,
select ps7_cortexa9_0.
(9) Next.
(10) Select “Create new...” for the domain. Keep the defaults.
(11) Next.
(12) Select “Zynq FSBL” as the software template.
(13) Finish.
(14) In the Vitis “Assistant” panel, right click the [Application] project you
just created and select “Build”.
(15) Now right click the corresponding [System] project, and select “Create
Boot Image”.
(16) Keep the default file paths, and keep the bootloader (fsbl.elf) and the bit-
stream (drone_wrapper.bit). Edit the third entry (project-name.elf)
and replace it by the u-boot_zybo.elf or u-boot_zybo-z7.elf from the
ZIP you downloaded in the second step.
(17) Click Ok and “Create Image”.
(18) The location of the generated BOOT.bin image is printed to the console.
2. INSTALLING PYTHON PACKAGES TO THE ZYBO 249

(19) Copy it to the SD card, replacing the one that came from sd-zybo.zip.

Tip 22.1.2

If your IP changes often, you can automate the generation of the BOOT.bin file
using the bootgen command-line utility directly, this is explained in §2.2.6 in
the TA-only chapter.

2. Installing Python packages to the Zybo


This section describes the process of deploying Python packages to the Zybo.
Two packages that you’re likely to need are eagle-rwmem and py-eagle-logger.
Tutorial 22.2.1 (Downloading a pre-built Python package) — The continuous inte-
gration of the EAGLE-students repository automatically builds the Python packages
for you when you push changes to them. Once the CI job is complete, you can down-
load the so-called artifact containing the compiled Python package, and deploy it
to the Zybo.
(1) Browse to https://gitlab.esat.kuleuven.be/EAGLE-gitlab/EAGLE-students/-
/jobs (or to your team’s fork of it).
(2) Locate the latest job for the Python package you want to install, e.g.
eagle-rwmem-build-job.
(3) Click the “Download artifacts” button in the right column.
(4) Open the downloaded ZIP file, go to the folder of the Python package, and
extract the .whl package from the dist folder. For packages containing
native code, you should use the linux armv7l version for the Zybo. Pure-
Python packages can run on any architecture, so you can just pick the
none-any version in that case.
Tutorial 22.2.2 (Building a pure-Python package locally) — Rather than relying
on the CI job to build the package for you, you can also build it locally. For
pure-Python packages, this can be done very easily, because no cross-compilation is
necessary. For Python packages that contain native C or C++ extensions, the process
is a bit more involved, see Tut. 22.2.3.
(1) Install PyPA build. (You only have to do this once.)
python3␣-m␣pip␣install␣build

(2) Build the package. Go to the folder containing the package’s pyproject.toml
file, e.g. Module_software/ANC/py-eagle-logger, and open a terminal.
Then execute
python3␣-m␣build␣--wheel

The .whl file can now be found in the dist folder. Every time you make a
change to one of the source files, you have to rebuild the package.
Tutorial 22.2.3 (Building a Python package with native code locally) — Building
Python packages that include native C or C++ extensions requires cross-compilation.
Before starting, ensure that you are using the same operating system and Python
version on your computer as on the Zybo (i.e. Python 3.10 on Linux or WSL), and
that you have installed the cross-compilation tools as explained in Tut. 26.3.1.
(1) Install PyPA build. (You only have to do this once.)
2. INSTALLING PYTHON PACKAGES TO THE ZYBO 250

python3.10␣-m␣pip␣install␣build

(2) Open the directory containing the Python project you want to build in a
terminal, e.g. Module_software/ANC/eagle-rwmem.
(3) Create a zybo-cross.toml file that configures the cross-compilation pro-
cess. You can use Module_software/ANC/eagle-rwmem/zybo-cross.toml
as an example.
The toolchain_file option is relative to the folder containing the cur-
rent configuration file (zybo-cross.toml). In most cases, you should use
the toolchain file that comes with the Module_software/ANC/Autopilot
project.
The copy_from_native_build option is used to copy the Python stub files
when cross-compiling, it usually contains the path
‘<package-name>/<cmake-target-name>’. You can omit this option if
you don’t need the stubs.
(4) Build the package.
Go to the folder containing the package’s pyproject.toml file, e.g.
Module_software/ANC/eagle-rwmem, and open a terminal. Then execute
python3.10␣-m␣build␣--wheel␣-C"--cross=zybo-cross.toml"

The .whl file can now be found in the dist folder. Every time you make a
change to one of the source files, you have to rebuild the package.
Tutorial 22.2.4 (Installing a Python package to the Zybo) — Make sure you have
a correctly formatted SD card with all the given files as described in Tut. 22.1.2.
(1) Copy the .whl files for py-eagle-logger and eagle-rwmem that you cre-
ated or downloaded in the previous tutorials to the wheels folder on the
SD card.
(2) Copy the py-eagle-logger/examples folder to the root of the SD card as
well.
(3) Boot the Zybo and log in over serial or SSH.
(4) Run the example for py-eagle-logger
/media/examples/Logger-example.py

(5) Run the examples for eagle-rwmem


killall␣Autopilot␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣# Make sure Autopilot isn't running
leds_cpp␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣# Run the C++ example
leds_c␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣# Run the C example
python␣-m␣eagle_rwmem.examples.leds␣# Run the Python example
while␣true;␣do␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣# Run the Bash example
␣␣␣␣rwmem␣0x41210000␣0xA
␣␣␣␣sleep␣0.2
␣␣␣␣rwmem␣0x41210000␣0x5
␣␣␣␣sleep␣0.2
done

Each of the examples will blink the LEDs in an infinite loop, you can cancel
them using Ctrl+C.
Exercise 22.2.1 (Installing a Python package locally) — When developing a pack-
age, it is much more pleasant if you can test it locally on your computer without
having to install it every time. To this end, you can use Pip’s development mode.
2. INSTALLING PYTHON PACKAGES TO THE ZYBO 251

(1) (Recommended) Create and activate a Python virtual environment.


python3␣-m␣venv␣eagle
source␣eagle/bin/activate␣␣␣# Linux/macOS (Bash/ZSH)
eagle\Scripts\Activate.ps1␣␣# Windows (Powershell)

You can pick your own name instead of eagle.


(2) Install the package in development mode. Go to the folder containing the
package’s pyproject.toml file, e.g. Module_software/ANC/py-eagle-logger,
and open a terminal. Then execute
python3␣-m␣pip␣install␣-e␣.␣-v

The -e or --editable flag tells Pip to install in development mode. The -v


or --verbose flag shows the output from the build process of the package.
See python3 -m pip install --help for more details.
(3) Run the Logger-example.py on your computer.
(4) Add a print statement in the parse() method of Module_software/ANC/
py-eagle-logger/src/eagle_logger/parser.py and run the example again.
Can you see the output from the print statement?
CHAPTER 23

Python – Image Processing


Eli Verwimp

1. Introduction
OpenCV is a software library that allows you to easily manipulate and ana-
lyze images. It has a large amount of built-in functions and is widely used for
computer vision applications. It is available in several programming languages, in-
cluding Python, which you will be using throughout the year. This chapter will give
you some instructions on how to setup a Python environment, how to install the
library and links to tutorials you should follow to familiarize yourself with OpenCV.

2. Installation
The first thing to do is to setup a Python environment, with the necessary li-
braries installed. The most common way to do this is by using pip. It is recommend
to made a virtual environment to install your python packages. This separates sys-
tem packages from self-installed ones, which can be convenient when testing version.
Or recreating environments with the same libraries. The way you do this, depends
on the operating system of your device. The tutorials below will allow you to create
such a virtual environment with the OpenCV library and other necessary packages
installed.
Tutorial 23.2.1 (Installation on your own device) — To setup a python environ-
ment with the necessary libraries, follow these steps.
✓ If you don’t have python yet, install the latest version from here. If you
want to run python from the terminal make sure to check add to path
when installing.
The RPi OS runs python 3.9, so if you already have python on your PC
check the version with python␣--version.
✓ Pip automatically comes with python. Before installing packages, you have
to create a virtual environment:
python␣-m␣venv␣eagle

✓ To activate the environment, it depends on what platform you’re working:


✓ Windows: ./eagle/Scripts/activate
✓ Linux/MacOS: source␣eagle/bin/activate
Your command prompt should now have the prefix (eagle)
More information about creating virtual environments, here.
✓ Install OpenCV in the virtual env:
pip␣install␣opencv-python==4.5.1

More information about pip, here.


✓ Install other useful packages:
252
3. THE BASICS 253

pip␣install␣zmq␣pyzbar␣matplotlib␣qrcode

✓ Done! Always execute Python files with the eagle environment activated
(./eagle/Scripts/activate). Navigate to folders using the cd <your>/<folder>
command. List files and folders using dir. Run Python files by executing
python <your file>.py.
✓ If you don’t have an IDE, make sure to install one. (e.g. PyCharm or
Visual Studio Code)
If you have problems with running Python scripts, do some online research and
contact the IMP TAs if you cannot solve it.

3. The basics
Exercise 23.3.1 (Running a Python script (Easy)) — You can run a script by open-
ing a terminal and navigating to the folder where you saved the script. Make sure
your environment is activated. Then, you simply enter: python <yourfile>.py.
Try to create a simple helloworld program and run it.
Tutorial 23.3.1 (Numpy) — OpenCV uses Numpy to handle the matrices and
vectors, so you will have to get used to working with it. Numpy is inspired by
Matlab in quite a few ways, so having experience with Matlab is useful.
✓ We recommend following this tutorial so you understand the basics:
https://cs231n.github.io/python-numpy-tutorial/#numpy
Tutorial 23.3.2 (OpenCV) — The following tutorials explain some of the most
important concepts of OpenCV. Read these and follow the instructions. We recom-
mend that you follow these tutorials in this order. Feel free to tinker around with
the code! If you find other tutorials online, be sure they are for OpenCV 3.0 or
higher. The API of OpenCV 2 is different. Let’s start with the basics of OpenCV:
✓ Basic images I/O: https://docs.opencv.org/4.5.1/db/deb/
tutorial_display_image.html
✓ Basic videos I/O: https://docs.opencv.org/4.5.1/dd/d43/
tutorial_py_video_display.html
✓ Drawing functions: https://docs.opencv.org/4.5.1/dc/da5/
tutorial_py_drawing_functions.html
✓ Basic operations: https://docs.opencv.org/4.5.1/d3/df2/
tutorial_py_basic_ops.html
✓ Operations on images: https://docs.opencv.org/4.5.1/d0/d86/
tutorial_py_image_arithmetics.html
After these tutorials, you should have a descent understanding of the basics of
OpenCV. The next tutorials will show you how to manipulate images in a meaningful
way. Understanding these is essential for completing the goals of the IMP-module.
✓ Changing colour spaces: https://docs.opencv.org/4.5.1/df/d9d/
tutorial_py_colorspaces.html
✓ Geometric Transformations:https://docs.opencv.org/4.5.1/da/d6e/
tutorial_py_geometric_transformations.html
✓ Thresholding: https://docs.opencv.org/4.5.1/d7/d4d/
tutorial_py_thresholding.html
The next tutorials are not mandatory, but they do highlight some of the more
useful things OpenCV can do.
3. THE BASICS 254

✓ Camera calibration: https://docs.opencv.org/4.5.1/dc/dbb/


tutorial_py_calibration.html
✓ Hough Line Transform: https://docs.opencv.org/4.5.1/d6/d10/
tutorial_py_houghlines.html
✓ Contour detection: https://docs.opencv.org/4.5.1/d4/d73/
tutorial_py_contours_begin.html
The official documentation page (https://docs.opencv.org/4.5.1/) contains all the
available functions for OpenCV, many of which also have examples attached. First
consult this if something does not work.
CHAPTER 24

Raspberry Pi
Simon Bos

1. Hardware
The Raspberry Pi board is a small single board computer, namely a Raspberry
Pi 4 Model B. More info can be found here. The Raspberry Pi is a very versatile
device. It can for example act as a small wireless router with an integrated wireless
card. The Gigabit ethernet port helps to utilize the full potential of 802.11n wireless
speeds. The Raspberry Pi can operate in the 5GHz spectrum as to not interfere
with the remote controller of the drone which operates at 2.4GHz frequency. Full
specifications can be found here. Figure 1 shows what the board looks like.

Figure 1. The Raspberry Pi. 1:Camera connector, 2:Ethernet port,


3:USB-C port, 4:GPIO header

There are five parts on the board of interest to you. Please refer to Figure 1 for
the corresponding parts.
(1) Camera connector : This connector is used to connect the camera. Avoid
disconnecting and reconnecting the camera as this connector is only made
for a limited amount of insertions.
255
3. CONNECTING 256

(2) Ethernet port: The ethernet interface of the routerboard. You can connect
an ethernet cable here to connect the board to another ethernet capable
device such as your laptop, ground station or the zybo.
(3) USB-C port: The USB-C port is only used for power when the Raspberry
Pi is not mounted on the drone.
(4) GPIO header : These pins will be used to communicate to the wireless
power transfer board as well as providing power to the Raspberry Pi while
mounted on the drone.
(5) SD slot: Not indicated on the figure because it is located at the bottom.
This is where the micro SD card is inserted containing all the files for
the operating system as well as your custom code. Avoid removing and
inserting the SD card as much as possible, these connectors are very fragile
and manufactured for a limited number of insertions.

2. Software
The operating system running on the Raspberry Pi’s is called Raspberry Pi OS
(formerly Raspbian). This is a Linux (Debian) based operating system specifically
designed for Raspberry Pi. On Raspberry Pi OS you will be able to run all different
programming languages; bash and Python scripts are enabled by default. You can
choose whatever software you use to achieve your goals, BUT choose wisely since
esoteric programming languages not known by expert assistants could not be sup-
ported by them. The Raspberry PI (RPi) hosts the image processing (IMP) and
Wireless Video Link (WVL) and communications (COMMS) code.
Tutorial 24.2.1 (Install OS) — Install the latest Raspberry Pi Imager from the
official Raspberry Pi website here. Then, transfer this image to the microSD card
using the instructions on the Raspberry Pi website.

3. Connecting
Tutorial 24.3.1 (Connecting to the RPi using SSH) — To connect to the RPi,
you need to connect power and link it to your laptop via an ethernet cable. After
connecting power, wait for a moment until the RPi is fully booted. Next, properly
configure the ethernet link on your laptop. If in doubt, read the IP assignment
paragraph at Chapter 14 and the RPiInternet Connectivity (section 5). You can use
SSH from your laptop to the RPi to connect if you know its IP address.
We list the default configuration below:
✓ Fallback static IP : 192.168.1.2
✓ username: pi
✓ password : raspberry
✓ hostname: raspberrypi
✓ SSH port: 22
You have multiple options to connect using SSH:
✓ Using an appropriate terminal (recommended), i.e. any Linux or Mac ter-
minal, a Windows 10 command prompt (cmd.exe) or a Windows Subsys-
tem for Linux (WSL) terminal: open the terminal and use the command
ssh␣<username>@<address>:<port>, where address is an IP address or a domain
name and port is by default set to 22. For example: ssh␣[email protected].
✓ Using Putty, see this website for more information.
4. CONFIGURING THE RPI 257

Tutorial 24.3.2 (Finding the IP address of the RPi) — If for any reason you do
not know the IP address of the RPi, you can use various options:
✓ On Linux, try the ip neighbour command (as mentioned in Chapter 9).
✓ On Windows or Mac, use the arp -a command.
✓ More advanced: use the Wireshark application to snoop on the network
traffic.

Tip 24.3.1

On Windows, your firewall can block connectivity from / to your laptop. If any
traffic gets dropped unexpectedly, disable your firewall.

4. Configuring the RPi


Tutorial 24.4.1 (Setting the hostname and password) — The first time you login
into your RPi, you will want to change the default hostname and password. The
easy way to do this is by using raspi-config:
(1) If you are not in the RPi yet, SSH into the RPi (see §3)
(2) Run raspi-config: sudo␣raspi-config
(3) Navigate to System Options > Hostname to set a new hostname. Chose
a logic name like groundstation or drone to distinguish your RPi’s.
(4) You can also change the password in System Options > Password. Make
sure you don’t forget it!
(5) If you want you can explore the other settings.
(6) Once you are finished select <Finish>, and the reboot for the changes to
take effect.
Tutorial 24.4.2 (Configure SSH to use public key authentication) — Instead of
using a username/password to authenticate, you can configure SSH to use public
key authentication. This will offer stronger security and (most importantly) greater
usability. Perform the following steps:
(1) (Optional - you can re-use your SSH key pair that you use for GitLab)
Generate an SSH key pair as explained here. For example, save it as
~/.ssh/id rsa.
(2) Edit your ~/.ssh/config file (on your laptop) to include the following:
Host␣drone
␣␣␣␣HostName␣rpi-drone
␣␣␣␣User␣pi
␣␣␣␣PreferredAuthentications␣publickey
␣␣␣␣IdentityFile␣~/.ssh/id_rsa

Change the HosName to the hostname of your raspberrypi. And Host to


whatever you want. See man ssh config for more information. Note that
mDNS is used to resolve the hostname.
(3) Install the public key on the RPi, to login without entering your password
each time. For example, if your SSH key is stored in ~/.ssh/id rsa, use:
ssh-copy-id␣-i␣~/.ssh/id_rsa␣pi@raspberrypi

If you don’t have ssh-copy-id installed, you can use (in Bash or Power-
shell):
7. PYTHON IN A VENV ENVIRONMENT 258

cat␣~/.ssh/id_rsa.pub␣|␣ssh␣pi@raspberrypi␣'cat␣>>␣~/.ssh/authorized_keys'
# Note the .pub extension, don't share your private key!

Enter the RPi’s password when prompted. If you get a No such file or
directory error, you first have to create the .ssh directory in the home
folder of the RPi.
Now you can use ssh drone (without password) to ssh into the RPi instead of ssh
pi@raspberrypi (with password raspberry).

5. Internet connectivity
We provide two main ways of connecting your Raspberry Pi to the internet.

Tutorial 24.5.1 (Connect to Eduroam via wpa supplicant) — You configure the
RPi Wi-Fi card to connect to eduroam using your student username and password.
The provided RPi image contains default wpa supplicant configuration necessary to
connect to eduroam. More info can be found at the ICTS documentation.

Tutorial 24.5.2 (Setup internet sharing from laptop) — You can share the wifi
connection from the host laptop to the RPi over ethernet. This seems quite a bit of
work, but modern operating systems provide an easy to setup configuration. Note
that if you use this option, you will be asked to explain how this technically works
in detail!
✓ MacOS: see Apple support article. Note: step 4 is not necessary.
✓ Windows 10: see this top Google search.
✓ Linux (Network Manager): either configure it via the GUI and set Connec-
tion Type to Shared to other computers or via the command line.
✓ Linux (manually): see the Arch Linux documentation.

6. Poweroff
Tutorial 24.6.1 (Powering off the Rasperry Pi) — Turn off properly using the
sudo poweroff command to prevent data loss.

7. Python in a venv environment


Tutorial 24.7.1 (Running Python with OpenCV) — When using the provided op-
erating system image processing libraries such as OpenCV packages are pre-installed
in a virtual environment. You first need to activate this virtual environment before
you can run code using OpenCV on the Raspberry Pi, which can be done by exe-
cuting the following command in the terminal:
␣␣␣␣source␣eagle/bin/activate

Note that your terminal now has the prefix (eagle). You can now execute
your python files as usual (python <file>.py). If you want to leave the virtual
environment and use the system python, run
␣␣␣␣source␣deactivate
9. TROUBLESHOOTING 259

8. Autorunning Python Scripts


Tutorial 24.8.1 (Using autostart.sh) — To run scripts at startup of the RPi, you
can edit the autostart.sh file in the home directory (/home/pi/). For example,
you could add:
cd␣/home/pi/my/folder/with/code
python␣file.py␣&

You should use absolute paths (e.g. starting with /home/pi/). The cd command
is required because python imports work relative to the current directory. The
ampersand (&) at the end of the line executes the file in the background.

9. Troubleshooting

Tip 24.9.1

✓ My Raspberry Pi is rebooting at random times: Please make sure that


the power provided to the Raspberry Pi is capable of providing 2 Am-
pere. If this is not the case the Raspberry Pi will boot, but can shut-
down at any moment.
✓ Help I cannot access the Raspberry Pi anymore, because I messed up
the networking: First of all, make sure you have backups of your SD
card files on Gitlab at all times. If this is the case, revert back to your
older SD card files and start from there. If you don’t have a backup,
connect a screen and keyboard and access the Pi this way. If you don’t
have access to those things, revert to the original SD card files provided
by us, let’s hope you remember the changes you’ve made.
CHAPTER 25

Matlab Project
Alexander Bodard

1. Setting up
In this chapter, we will discuss the provided Matlab EAGLE Simulator Frame-
work, which enables the students to use MEX to call their implemented C++ con-
trollers directly from Matlab. The EAGLE Simulator Framework has three main
dependencies: (i) a C++ compiler to produce MEX files; (ii) a Python interpreter;
and (iii) CMake.
Tutorial 25.1.1 (Installation of the C++ compiler) — To check if you already have
a C++ compiler that is usable by Matlab execute:
mex␣-setup␣-v␣cpp

Matlab will search for a suitable compiler on your operating system. If succesfull
it will display something like: 'Found␣installed␣compiler␣...'. Otherwise you will see
the following error:
Error␣using␣mex
No␣supported␣compiler␣was␣found.␣You␣can␣install␣the␣freely
available␣MinGW-w64␣C/C++␣compiler;␣see␣Install␣MinGW-w64
Compiler.␣For␣more␣options,␣visit
␣␣␣␣https://www.mathworks.com/support/compilers.

The solve this error, we need to ensure that a suitable compiler is present on your
device. Note that Matlab recommends installing the MinGW C++ compiler, but
this can be messy and the compilers are often outdated. Therefore, we provide
detailed instructions for obtaining a suitable C++ compiler on Windows, Linux and
Mac below.
✓ Linux: Most common Linux distributions have an installable package with
a suitable C++ compiler. We’ll also install the GDB debugger. In the case
of Ubuntu you can use
sudo␣apt␣update
sudo␣apt␣install␣-y␣build-essential␣gdb

This will install the GCC compilers, which Matlab can access.
Newer versions of GCC offer better diagnostics and support more of the
latest features, so you might want to install the latest version (especially
on LTS releases, the compiler might be rather old). To install GCC 11 on
Ubuntu 18.04 or 20.04, you can use:
sudo␣add-apt-repository␣ppa:ubuntu-toolchain-r/test
sudo␣apt␣update
sudo␣apt␣install␣-y␣g++-11␣gcc-11␣gdb

260
1. SETTING UP 261

✓ ESAT: On the ESAT PCs, suitable GCC and Clang compilers should al-
ready be present.
✓ WSL: Follow the instructions for Windows so you can compile code for
Matlab on Windows. Then also follow the instructions for Linux inside
of WSL so you can compile code there as well.
✓ Windows: On Windows, the default compiler is the MSVC (Microsoft Vi-
sual C++) compiler included in Microsoft’s Visual Studio IDE. We will focus
on Visual Studio 2022 (2019 works as well). You can get the installer from
here. Open it and install Visual Studio Community 2022. In Workloads,
select Desktop development with C++. You don’t need any other work-
loads.
✓ Mac: You can either (i) install XCode from the App store; or (ii) install
the XCode command-line tools from the Apple website or by using the
command xcode-select␣--install in your terminal.

Warning 25.1.1

Option (i) takes up significantly more space than option (ii). When
installing option (ii) however, Matlab might give the warning
Warning:␣Xcode␣is␣installed,␣but␣its
license␣has␣not␣been␣accepted.
Run␣XCode␣and␣accept␣its␣license␣agreement.

Accompanied by the error that no compatible compiler was found. To


fix this, use PlistBuddy to change some settings. In your terminal run
/usr/libexec/PlistBuddy␣-c␣'Add:
␣␣␣␣IDEXcodeVersionForAgreedToGMLicense␣11.4'
␣␣␣␣~/Library/Preferences/com.apple.dt.Xcode.plist

This will create the file Matlab is looking for to check that you agreed
to the XCode license. Afterwards, Matlab should successfully find the
compilers includes in the command-line tools and MEX should build
successfully.

Warning 25.1.2

There is a bug with MEX not finding the correct Xcode SDK. This is
fixed by adjusting /Applications/MATLAB R2020a.app/bin/
maci64/mexopts/clang++ maci64.xml. Alternatively use
find <Matlab root> -name ’mexopts’ in your terminal to search for
it if you have a different Matlab version. Change line 135 to read:
<cmdReturns␣name="xcrun␣-sdk␣macosx
␣␣␣␣--show-sdk-version␣|␣cut␣-c␣1-5"/>

If this does not fix the issue, copy paste the correct sdk directly. Find
the correct sdk version using /Applications/Xcode.app/Contents/
Developer/usr/bin/xcodebuild -showsdks and change line 135 to:
<cmdReturns␣name="macosx10.15"/>
1. SETTING UP 262

Tutorial 25.1.2 (Path configuration) — Users don’t have root (administrator)


privileges on ESAT machines, so we’ll install the required software in the home
folder. To make sure that these programs can be found by other software, the
installation folder should be added to the PATH.
✓ Linux/WSL: Some distributions, like Ubuntu already add ∼/.local/bin
to the PATH, verify this by checking that ∼/.profile contains something
like
# set PATH so it includes user's private bin if it exists
if␣[␣-d␣"$HOME/.local/bin"␣]␣;␣then
␣␣␣␣PATH="$HOME/.local/bin:$PATH"
fi

If this is not the case, follow the instructions for ESAT below, but use
.profile instead of .bash profile.
✓ ESAT: The default location is ∼/.local/. The bin subdirectory must be
in the path. First, make sure that this directory exists:
mkdir␣-p␣~/.local/bin

Then edit your ∼/.bash profile file such that it contains the following
line to set the $PATH environment variable:
export␣PATH="$HOME/.local/bin:$PATH"

You can edit the file by using:


gedit␣~/.bash_profile

Note the order: $PATH should be at the end, so the folders in the home di-
rectory take precedence over the system folders. We want the new versions
we install locally to be used, rather than the older system-wide versions.
After editing the .bash profile file, log out and back in again.
✓ Windows: You can add a folder to the path as follows. Search for the “Edit
environment variables for your account” setting in the start menu. Select
the Path variable and click “Edit...”. Add the directory in question to the
path by clicking “New”. Close with “OK”. You may need to reopen your
applications and terminals for the change to take effect (you might want to
log out and back in again to be sure).
Tutorial 25.1.3 (Installation of Python interpreter) — To use the Python side
of the simulator framework you will require a Python interpreter with the neces-
sary packages installed. You can download Python from https://www.python.org/
downloads, or you could use an alternative distribution such as Miniconda. Make
sure that the Python version you select is compatible with your Matlab version.
You can find all compatible versions here.
✓ ESAT: To install an arbitrary Python version on your ESAT account, you
can build Python from source. Go to EAGLE-students/Module_software/
ANC/Simulator/scripts and open a terminal there. Now execute the fol-
lowing command (change the version number as needed)
./python-esat.sh␣3.9.14

Make sure to follow Tutorial 25.1.2 next to add the Python version to your
path.
1. SETTING UP 263

Tutorial 25.1.4 (Installation of CMake) — To use the given MEX wrapper of the
simulator, you will require CMake and you need to make sure that they are added
to your path. To check if CMake is already in your path, you can simply open a
terminal and type
cmake␣--version

We recommend using CMake 3.22 or later. If your device does not have CMake yet,
follow these steps:
✓ Linux: Ninja and Make can be installed through apt:
sudo␣apt␣install␣-y␣ninja-build␣make

CMake is in the apt repositories as well, but it’s best to install the latest
version from cmake.org (requires root privileges to install to /usr/local):
cd␣~/Downloads
wget␣"https://github.com/Kitware/CMake/releases/download/v3.24.2/cmake-3.24.2-linux-x86_64.sh"
chmod␣+x␣cmake-3.24.2-linux-x86_64.sh
sudo␣./cmake-3.24.2-linux-x86_64.sh␣--skip-license␣--prefix=/usr/local

✓ ESAT: CMake and Make should be installed already, but can be outdated.
If so, you could install the latest version using:
cd␣~/Downloads
wget␣"https://github.com/Kitware/CMake/releases/download/v3.24.2/cmake-3.24.2-linux-x86_64.sh"
chmod␣+x␣cmake-3.24.2-linux-x86_64.sh
mkdir␣-p␣~/.local
./cmake-3.24.2-linux-x86_64.sh␣--skip-license␣--prefix=$HOME/.local

Again, make sure that ~/.local/bin is in the PATH as explained in Tut.


25.1.2. You could choose to install Ninja as well (it can be slightly faster
than Make), but this is optional.
✓ WSL: Follow the instructions for both Windows and Linux.
✓ Windows: You can get CMake by downloading a suitable CMake installer
from cmake.org. You probably want to select the Windows x64 Installer.
Run the installer and, when asked, make sure to select Add CMake to the
system PATH for the current user.
Additionally, you also need to install Ninja. Download the zip from the
GitHub release page https://ninja-build.org/, extract it somewhere (e.g.
create a folder C:/Program Files/ninja/) and add it to your path as
explained in Tut. 25.1.2.
✓ Mac: CMake and Ninja can be installed using Homebrew:
brew␣install␣cmake␣ninja

Tutorial 25.1.5 (Installation of Visual Studio Code) — The final thing we want
to install is Visual Studio Code. VS Code is an IDE which will be very handy when
working with MEX and the C++ Autopilot code. To get VS Code, go to the Visual
Studio Code download page and perform the following steps:
✓ Linux: Download the 64-bit .deb or .rpm version, depending on your distri-
bution. Then click on the downloaded file to open it (e.g. using the Ubuntu
Software Center) and click install.
✓ WSL: Install VSCode in Windows only. Then install the Remote – WSL
extension to access your WSL environment.
✓ Windows: Download the Windows installer and install it.
1. SETTING UP 264

✓ ESAT: Download the 64-bit .tar.gz Linux version of VS Code to your


∼/Downloads folder. Then open a terminal and run the following com-
mands:
cd
mkdir␣-p␣~/opt
tar␣xzf␣Downloads/code-stable-*.tar.gz␣-C␣~/opt
mkdir␣-p␣~/.local/bin
ln␣-s␣../../opt/VSCode-linux-x64/code␣~/.local/bin/code
xdg-icon-resource␣install␣--context␣apps␣--size␣256␣--novendor␣\
␣␣␣␣~/opt/VSCode-linux-x64/resources/app/resources/linux/code.png␣\
␣␣␣␣com.visualstudio.code
cat␣>␣/tmp/code.desktop␣<<␣EOF
[Desktop␣Entry]
Name=Visual␣Studio␣Code
Comment=Code␣Editing.␣Redefined.
GenericName=Text␣Editor
Exec=$HOME/opt/VSCode-linux-x64/code␣%F
Icon=com.visualstudio.code
Type=Application
StartupNotify=false
StartupWMClass=Code
Categories=Utility;TextEditor;Development;IDE;
MimeType=text/plain;inode/directory;
Actions=new-empty-window;
Keywords=vscode;

[Desktop␣Action␣new-empty-window]
Name=New␣Empty␣Window
Exec=$HOME/opt/VSCode-linux-x64/code␣--new-window␣%F
Icon=com.visualstudio.code
EOF
xdg-desktop-menu␣install␣--novendor␣/tmp/code.desktop

Once you have installed VS Code, open it, and go to the extensions menu using the
Ctrl+Shift+X shortcut. Then, install the following extensions:
✓ C/C++ by Microsoft
✓ CMake Tools by Microsoft
Tutorial 25.1.6 (Installation of Doxygen) — The project is documented using
automatically generated Doxygen documentation. Follow the following steps to
install Doxygen:
✓ Linux: You can use the following commands to build and install Doxygen
from source (requires root privileges to install to /usr/local).
sudo␣apt␣install␣-y␣flex␣bison␣graphviz
pushd␣/tmp
rm␣-rf␣doxygen
git␣clone␣--branch␣Release_1_9_5␣--depth␣1␣\
␣␣␣␣https://github.com/doxygen/doxygen.git
cmake␣-S␣doxygen␣-B␣doxygen/build␣\
␣␣␣␣-DCMAKE_INSTALL_PREFIX="/usr/local"␣\
␣␣␣␣-DCMAKE_BUILD_TYPE=Release
cmake␣--build␣doxygen/build␣-j$(nproc)
sudo␣cmake␣--install␣doxygen/build
popd
2. THE MEX WRAPPER 265

✓ ESAT: You can use the following commands to build and install Doxygen
from source.
pushd␣/tmp
rm␣-rf␣doxygen
git␣clone␣--branch␣Release_1_9_5␣--depth␣1␣\
␣␣␣␣https://github.com/doxygen/doxygen.git
cmake␣-S␣doxygen␣-B␣doxygen/build␣\
␣␣␣␣-DCMAKE_INSTALL_PREFIX="$HOME/.local"␣\
␣␣␣␣-DCMAKE_BUILD_TYPE=Release
cmake␣--build␣doxygen/build␣-j$(nproc)
cmake␣--install␣doxygen/build
popd

Don’t forget to add the installation folder to your path as explained in


Tutorial 25.1.2.
✓ WSL: Having Doxygen in either Windows or WSL is fine, you don’t neces-
sarily need it in both.
✓ Windows: You need both Graphviz and Doxygen. Download them from:
✓ https://graphviz.org/download/#windows
✓ https://www.doxygen.nl/download.html#srcbin
Make sure that the doxygen and dot commands are in your path (Tut.
25.1.2).
✓ Mac: You can install Doxygen and Graphiz using
brew␣install␣doxygen␣graphviz

2. The MEX Wrapper


Before we test our C++ code on the real drone, it is important that we first test
whether our implemented C++ controllers actually behave as intended. To this end,
we will use MEX, which allows us to call the C++ code directly from Matlab. For
more background information on MEX, see §3 from Ch. 11.
We have already provided a MEX wrapper for the mainControllers function
from Autopilot/src/students/src/main/Main-Controllers.cpp. In this sec-
tion, we will take a thorough look at this wrapper.
Tutorial 25.2.1 (Setting up the C++ wrapper) — We first provide instructions on
how to compile the C++ code for MEX.
(1) Open Matlab. Make sure that the current folder is in EAGLE-students/
Module_software/ANC/Simulator. You can verify if this is the case, by
either looking at the Current folder tab or by using the command:
pwd

Tip 25.2.1

If you are working on the ESAT PCs, Matlab will use the Emacs de-
fault set for keyboard shortcuts. This can be somewhat inconvenient
since ctrl+c and ctrl+v will not work. To change this, go to Home
> Preferences > Keyboard > Shortcuts > Active Settings and
change Emacs Default Set to Windows Default Set.
2. THE MEX WRAPPER 266

(2) Now, compile the C++ code. The default compilation command is specified
in the static compile method in the Control class. This method uses CMake
(cfr. §10 from Chapter 12) to easily compile the MEX interface.
Compile the MEX code with the following command
eaglesim.Control.compile()

This may take a while. Once it is finished you should have a file called
MexControllers.mexw64 or MexControllers.mexa64 should appear in the
mex-install folder.
Optionally, you can specify to compile the C++ code for debugging (see
Ex. 25.2.3) by passing ‘Debug’ as the optional build type argument:
eaglesim.Control.compile('Debug')

To clean up your build files, you can use the static clean method in the
Control class, or if you want to be really sure that all build files are gone,
you can just delete the mex-build-* folders.
Tutorial 25.2.2 (The Control class) — In this tutorial we will be using the MEX
code that you just compiled. A good first reference is the study controllers.m
script and the documentation of the Control.m class.
We will consider four major components, which come in pairs of two.
(1) +eaglesim/Control.m: The Matlab wrapper around the MexControllers
class in C++.
(2) src/mex/main/MexControllers.cpp: The C++ side of the Control.m class,
which in turn invokes the code from the Autopilot framework.
(3) +eaglesim/+memory: Folder containing classes that represent the attitude,
altitude and navigation data.
(4) src/mex/memory: Folder containing the C++ side of the different memory
classes.
Take a moment to locate all of these files in the framework. The remainder of
this tutorial reconstructs study controllers.m, which is one of the most important
entry points to the Simulator Framework for the students.

Tip 25.2.2

To navigate the MEX interface more efficiently, you might want to open the
Simulator framework in VS Code, cfr. Tut. 26.2.1. VS Code is a much more
powerful editor for C++ than Matlab as it provides IntelliSense, real-time code
review, list of errors...

(1) We first load all of the required data


% drone state estimate
q␣=␣eaglesim.utils.deg2quat(-2,␣10,␣5);␣␣␣% (roll, pitch, yaw), degrees
w␣=␣[0.02;␣0.01;␣0];␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣% angular velocity
p␣=␣[0;␣0;␣1];␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣% position
a␣=␣[0;␣0;␣-9.81];
a_␣=␣-eaglesim.utils.quatrotate(q',␣a')';␣% measured acceleration by IMU

% add drone measurements (TODO: add noise and/or AHRS)


q_meas␣=␣q;
w_meas␣=␣w;
2. THE MEX WRAPPER 267

% drone reference
q_ref␣=␣eaglesim.utils.deg2quat(0,␣5,␣0);␣% (roll, pitch, yaw), degrees

Tip 25.2.3

Note that in practice the IMU has no direct measurement of the quater-
nion q representing the orientation of the quadcopter. Instead, AHRS
is used to obtain an estimate of the orientation using a gyroscope and
an accelerometer. For more info on how to properly deal with the IMU
measurements, see slide 188 in the theory slides and Ex. 26.5.3.
(2) We then construct instances of AttitudeData, AltitudeData and Naviga-
tionData and fill it with all of the measurements/inputs.
% create memory object containing the attitude info
attData␣=␣eaglesim.memory.AttitudeData();

% assign measurements
attData.currentMeas.q␣=␣q_meas;␣␣␣% assign quaternion measurement
attData.currentMeas.w␣=␣w_meas;␣␣␣% assign gyroscope measurement

% set reference
attData.currentRef␣=␣q_ref;

% create memory object containing the altitude info


altData␣=␣eaglesim.memory.AltitudeData();

altData.sonarMeasurement.altitude␣=␣p(3);
altData.sonarMeasurement.hasNewValue␣=␣true;
altData.hoveringThrust␣=␣0.5;

% create memory object containing the navigation info


navData␣=␣eaglesim.memory.NavigationData();

navData.shared.position␣=␣p(1:2);
navData.shared.hasUpdated␣=␣true;

The currentMeas and sonarMeasurement properties are so-called structs.


Make sure that you use the correct field names.
The shared struct of the NavigationData class will become relevant in
the next semester when performing inter-process communication with the
IMP and crypto modules. Note how position measurements are already
supported.
(3) The next step is to construct the different controller instances and reset
them.
% create and reset control object
ctr␣=␣eaglesim.Control();

ctr.reset('attitude',␣attData);␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣% reset the controller


ctr.reset('altitude',␣altData);
ctr.reset('navigation',␣navData);

(4) We can now pass our measurements and inputs to the ctr instance.
2. THE MEX WRAPPER 268

output␣=␣ctr.update(attData,␣altData,␣navData);␣␣% update the controller

% obtain motor outputs and log data


u␣=␣output.u;
log␣=␣output.log;

In your simulator, you’ll need to call this method repeatedly in a for loop.
We provide some exercises, guiding you through the process of implementing
controllers. Some exercises are also intended to provide background information on
the C++ framework.
Exercise 25.2.1 (Codegeneration and template files (Beginner)) — Of course, in
its current state no control signals are produced in the Autopilot framework. In this
exercise we will change this.
(1) Where in the framework is the attitude controller implemented?
(2) How do you update the state estimate/the control signal?
(3) What are the states of your controller?
To answer these questions, you will need to dig into the Autopilot Framework C++
code. Therefore, we recommend opening the Autopilot project in VS Code, as this
allows you to easily navigate the code. Take a look at Ch. 26 and open the ANC
projects in VS Code by following Tut. 26.2.1.
As you will notice, the files where you need to calculate the observer and the
controller update are empty. They only contain lines like
(void)␣stateEstimate;

which ignores the function argument by casting it to void and


return␣{};

which calls the default constructor of the output type.


We will of course have to change these lines. To do so we first need to understand
the inputs and the outputs of the functions.
(4) Where is Attitude::State defined?
(5) Where is Attitude::ControlSignal defined?
(6) Where is Attitude::Measurement defined?
All of the types above are structs.
(7) How you can construct or initialize instances of these structs in C++? How
you can access the members?
Hint 25.2.1: Review Section 6 if necessary.
(8) Change the controller such that it returns {0.1, 0.1, 0.1} instead of {} and
run study controllers.m again. Also, run the unit tests again in VS Code
by clicking Run CTest. What do you observe? Why does this happen?
Hint 25.2.2: Use the Find All References functionality of VS Code.
Now we can start thinking about what these files should contain.
(9) Try to write down in Matlab or pseudo-code what operations should be
performed on the inputs and what the output should be.
(10) Is it easy to write this C++ code? What part will change when you re-tune?
This last question introduces the need for code generation. You can find more
information on this topic in §4 from Chapter 11.
2. THE MEX WRAPPER 269

(11) What is the difference between mutable and immutable code?


(12) Look at your pseudo-code, what part will be mutable, what part will be
immutable?
(13) What is the use of the .template files provided with the framework. Where
are they used?
Hint 25.2.3: Look at the CMakeLists.txt files that define how the project
is built.
(14) What should the arguments of eaglesim.codegen.print affine be?
Hint 25.2.4: Think about the definition of Attitude::State and others
as well as the answer to (7). Cell arrays of strings could be of use.
We now illustrate how one can implement the control law using code generation.
Look for AttitudeCodegen.cpp.template and modify it to read:
#include␣<students/control/Attitude.hpp>

auto␣Attitude::codegenNextStateEstimate(State␣stateEstimate,␣ControlSignal␣ctrl,
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣Measurement␣meas)␣->␣State␣{
␣␣␣␣// TODO
␣␣␣␣(void)stateEstimate;
␣␣␣␣(void)ctrl;
␣␣␣␣(void)meas;
␣␣␣␣return␣{};
}

auto␣Attitude::codegenControlSignal(State␣stateEstimate,␣Reference␣ref)
␣␣␣␣->␣ControlSignal␣{

␣␣␣␣// create error state (xk − xe)


␣␣␣␣State␣error{stateEstimate.q␣-␣ref,
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣stateEstimate.w,
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣stateEstimate.n};

␣␣␣␣// calculate control actions (u = K*(xk − xe))


␣␣␣␣ControlSignal␣ctrl;
␣␣␣␣//<!calc controlSignal>

␣␣␣␣return␣ctrl;
}
//<!end of file>

Notice how we added the line //<!calc controlSignal>. This is a marker that
will be replaced by the operation u = K*(xk - xe) by our code generation. It
marks the mutable part of the code. Specifically we will be using the function
process template until.
For convenience, copy the template file to the root of the Simulator folder. This
means we will not have to care about relative paths. Open Matlab in this folder
and execute the following commands:
% open template and output files
finID␣=␣fopen('AttitudeCodegen.cpp.template',␣'r');
foutID␣=␣fopen('AttitudeCodegen.cpp',␣'w');

% define the variable names in cpp


state␣=␣{
2. THE MEX WRAPPER 270

␣␣␣␣'error.q.x',␣'error.q.y',␣'error.q.z',...
␣␣␣␣'error.w.x',␣'error.w.y',␣'error.w.z',...
␣␣␣␣'error.n.x',␣'error.n.y',␣'error.n.z'
};
ctrl␣=␣{
␣␣␣␣'ctrl.x',␣'ctrl.y',␣'ctrl.z'
};

% define the gain matrix


K␣=␣ones(3,␣9);

% parse template until string


eaglesim.codegen.process_template_until(␣...
␣␣␣␣finID,␣foutID,␣'//<!calc␣controlSignal>'␣...
);

% show current state of file


type('AttitudeCodegen.cpp');

% insert K*(xk − xe)


eaglesim.codegen.print_affine(␣...
␣␣␣␣foutID,␣ctrl,␣err_state,␣K,␣zeros(3,␣1),␣'␣␣␣␣'␣...
);

% read until the end of the file


eaglesim.codegen.process_template_until(␣...
␣␣␣␣finID,␣foutID,␣'//<!end␣of␣file>'␣...
);

% close the files


fclose(finID);␣fclose(foutID);

Make sure that you understand each line of code in this problem. You can look
up the documentation of the specific methods. Once you understand this, the next
steps are:
(1) Make a Matlab script that directly uses the correct .template and .cpp
files. Use the Autopilot/src/students/src/control/codegen/scripts/
autopilot_codegen.m script as a starting point. It is already invoked au-
tomatically with the correct paths. Do not forget to declare your dependen-
cies in Autopilot/src/students/src/control/codegen/CMakeLists.txt.
(2) Apply the introduced techniques to implement codegenNextStateEstimate.

Tip 25.2.4

Take some time to think about your workflow. You’ll notice that the provided
autopilot_codegen.m script is executed automatically by eaglesim.Control.compile()
whenever the templates are updated or whenever the script itself is updated.
Make use of this automation, you don’t want to have to manually execute a
handful of scripts every time you change a tuning parameter!

Exercise 25.2.2 (Investigating the Control class (Intermediate)) — If you take a


look at the implementation of the Control.m class, you will notice that it is just a
thin wrapper. All methods directly call MexControllers. The goal of this exercise
is to more clearly understand this underlying C++ class.
3. BUILDING A SIMULATOR 271

(1) Where is the mainControllers function defined in the Autopilot frame-


work? What inputs does it require?
(2) How are the attitude, altitude and logger objects instantiated in MEX?
What arguments would you pass to MexControllers from Matlab to in-
stantiate and reset them?
(3) Where is mainControllers called in MEX? What arguments are passed?

Exercise 25.2.3 (Debugging C++ and MEX code in VSCode (Advanced)) — You
probably already have experience with debugging in Python using breakpoints. Did
you know that you can do the same for Matlab?
Debugging your C++ code is more challenging. You can try to use the MexHelper
class to implement print statements (see MexControllers for an example, you will
have to add an include to make it available, causing all kinds of issues like circular
dependencies that you’ll have to resolve in the CMake build files) or §3 in Chapter
11. You should be aware that the MexHelper will not be available when compiling
for the Zybo.
For these reasons, we recommend debugging the C++ code in isolation, without
Matlab or MEX. Use the provided test case in Autopilot/src/students/test/
test-Attitude.cpp to call your controller functions and mainControllers, and
then launch that test in the C++ debugger (e.g. using the bug icon in the bottom blue
ribbon in VSCode), where you can easily step through your code without Matlab
getting in the way.
If you really need to do some debugging within Matlab, we recommend using
the logger to send data or text from C++ back to Matlab, see Exercise 25.5.1.
A more flexible (but more complicated) way of debugging C++ in Matlab is to
attach a debugger to Matlab while running your code through MEX, as described
in this blog post.

3. Building a Simulator
We provide an overview on how to build your own simulator. This includes a
high-level description of the components that you will need.
Note that this exercise describes how to build your final simulator, including
attitude, altitude and navigation. When developing your controllers it is better
to test them seperately first, before integrating them into the suggested simulator
framework. A basic simulator example is worked out in Ch. 11.

Exercise 25.3.1 (Building the complete simulator. (Advanced)) — Since we will


be dealing with objects that have an internal state, it makes sense to implement
them as a class. You could also opt for a functional programming approach, where
you implement updates using methods that take in a struct as the state and produce
the updated state struct as well as some additional values as output.
To build your own simulator, you will be needing the following components:
(1) Controller : a wrapper for the controller. This contains both an implemen-
tation of LQR and Kalman filtering. Since it has its own state, we will be
assuming it is implemented as a class (e.g. Control.m for MEX wrapping).
We will assume it is updated as follows:
u␣=␣ctr.update(y);
3. BUILDING A SIMULATOR 272

Here u represents the control actions applied to the motors (ux , uy , uz , uc )


(or (u1 , u2 , u3 , u4 ), depending on whether you use relative motor frequen-
cies). and y represents the measurements (in Control.m these are instances
of AttitudeData, AltitudeData and NavigationData).
(2) Plant: simulates the dynamics. This will internally call a numerical inte-
grator of some dynamics (e.g. ode45). Since it has an internal state we will
assume it is implemented using some class. We assume it is updated as:
x␣=␣plant.update(u)

Here x denotes the state.


(3) Sensors: simulate the sensors. They take in the state and produce noisy
measurements. We need three sensors, all running at different rates: (i)
the IMU; (ii) the sonar; and (iii) image processing. The IMU usually runs
at Fs = 476Hz. The sonar at 20Hz and image processing runs as fast as it
can, preferably a consistent rate over 20Hz. We assume updates like:
y_imu␣=␣imu.update(x);
y_sonar␣=␣sonar.update(x);
[y_imp,␣t_imp]␣=␣imp.update(x);
y␣=␣combine(y_imu,␣y_sonar,␣y_imp);

If your measurements are just vectors, then combine is a concatenation.


Note that we assume imp.update also returns a time. This will become
relevant later.
When building your simulator, the most challenging component to model is
timing. Note how the shortest sample time in the framework is that of the IMU.
Hence we will update the simulator at a period of Ts = 1/Fs . Each update we
need to determine (i) what the measurements are; (ii) what control action is applied
based on these measurements; (iii) what the effect of the control action is.
The first step (i) is the most challenging, since measurements come in at differ-
ent rates. In this framework we only execute the hierarchical controller once, so it
makes sense to somehow pass information about which measurements are updated.
In a first prototype, you could consider updating the hierarchical controllers seper-
ately, as measurements for them are provided (i.e., update attitude each iteration,
update altitude when sonar measurements are available, update navigation when
image processing measurements are available). This framework however is no longer
applicable, when integrating with MEX.
We provide some pseudo-code to describe how one could model the sensor tim-
ings. The parameters are x0, the initial state; Ts, the sample period and N the
amount of time steps.
% initialize the state
x␣=␣x0;

% construct the controller, plant and sensors


% with suitable initial states
...

% get initial measurements and timings


t_sonar␣=␣1/20;
y_sonar␣=␣sonar.update(x);
[y_imp,␣t_imp]␣=␣imp.update(x);

% store pending sonar and imp measurement


3. BUILDING A SIMULATOR 273

y_sonar_next␣=␣y_sonar;
y_imp_next␣=␣y_imp;

% start simulation
for␣k=1:N-1
␣␣␣␣% update imu measurement
␣␣␣␣y_imu␣=␣imu.update(x);

␣␣␣␣% update sonar measurement


␣␣␣␣if␣t_sonar␣<=␣0
␣␣␣␣␣␣␣␣% reset sonar time
␣␣␣␣␣␣␣␣t_sonar␣=␣t_sonar␣+␣1/20;
␣␣␣␣␣␣␣␣y_sonar␣=␣y_sonar_next;
␣␣␣␣␣␣␣␣y_sonar_next␣=␣sonar.update(x);
␣␣␣␣end

␣␣␣␣% update imp measurement


␣␣␣␣if␣t_imp␣<=␣0
␣␣␣␣␣␣␣␣y_imp␣=␣y_imp_next;
␣␣␣␣␣␣␣␣[y_imp_next,␣t_imp_del]␣=␣imp.update(x);
␣␣␣␣␣␣␣␣t_imp␣=␣t_imp␣+␣t_imp_del;
␣␣␣␣end

␣␣␣␣% update times


␣␣␣␣t_imp␣=␣t_imp␣-␣Ts;
␣␣␣␣t_sonar␣=␣t_sonar␣-␣Ts;

␣␣␣␣% update control action


␣␣␣␣u␣=␣ctr.update(combine(y_imu,␣y_sonar,␣y_imp));

␣␣␣␣% update plant


␣␣␣␣x␣=␣plant.update(u);

␣␣␣␣% record state, measurements and control actions


␣␣␣␣...
end

We use t <sensor> next for pending measurements since we are executing the sen-
sors (especially imp) ahead of time. That is each imp update we (i) set the pending
measurement to the current measurement; (ii) get the next pending measurement
and the time it took to calculate it; (iii) wait for that time and repeat.
Note that this is pseudo code. We do not store the states over time, we do not
include construction of the components, etc. Your final code does not need to look
like this. However, the timing should be the same.

Tutorial 25.3.1 (Example script) — The Simulator framework comes with an


example script (study controllers.m), which illustrate how some of the provided
components can be used. The script illustrates the use of Control.m in combination
with the different memory classes from eaglesim.memory.

When sampling noise to generate realistic measurements it is important to con-


sider how random numbers are generated in Matlab.

Exercise 25.3.2 (Random number generation (Advanced)) — When writing code


it is important that you can consistently repeat experiments. We however also
want to sample (quasi) random numbers (e.g., for the additive measurement noise).
4. THE PYTHON WRAPPER 274

Luckily in Matlab it is possible to do so consistently by specifying a seed before


you start your simulator.
(1) Read the Matlab rng documentation here.
(2) How can you fix a seed?
(3) Create two scripts. One (i) where you fix a seed and one (ii) where you do
not and generate/display some random numbers in both. First run (i) then
run (ii). What do you notice? Explain the result.
Hint 25.3.1: There is a reason why rng shuffle is mentioned in the
rng documentation.
(4) What is the use of the seed field in the image settings?

4. The Python wrapper


Now that we have the C++ wrapper set up, we can configure the Python wrapper.
Tutorial 25.4.1 (Setting up your Matlab environment) — After completing Tut.
25.1.3 we need to configure Matlab to use the correct Python interpreter.
(1) Execute the provided setup interpreter method (do this everytime you
restart Matlab).
eaglesim.utils.setup_interpreter();

If this is the first time that you call this method then you will be prompted
with a file browser. You should select your Python executable. When using
a virtual environment, this is <path-to-venv>/bin/python. If you are
using a Miniconda environment, this is ~/miniconda3/envs/<env-name>/
bin/python. Specifically, follow the instructions below:
(a) Linux: Open the folder called bin. Select the file called python.
(b) Windows: Look for python.exe in the envs folder and select it.
(c) Mac: See Linux. In order to navigate to /opt, where Anaconda is
usually installed, use the hotkeys CMD+shift+G and type /opt.
The framework uses getpref en setpref to store the interpreter path.
This allows us to remember your choice for the next Matlab session.
If you select the wrong executable by accident, or if you want to change
to a different environment, you can force the prompt to reappear
eaglesim.utils.setup_interpreter('force');

To specify path manually, add it as a string in the second argument.


Aside from setting the interpreter, the method will also configure your
PATH environment variable. You can try to see the effect by calling
getenv('PATH')

before and after calling setup interpreter.


You can see the selected Python interpreter by calling
pyenv()

(2) Matlab runs a single instance of Python (similar to what happens when
you use the python command in a terminal), which is initialized the first
time you call a Python method. To execute Python commands, prefix them
with py., for example
py.print('hello␣world')
5. RECEIVING INFORMATION FROM OTHER WORKSPACES 275

will print using the Python interpreter. Note how ‘hello world’ is in
fact a Matlab string. It is automatically converted to a Python string
and passed to the interpreter, this works for many types, but not with all
of them!
To import a Python module or a package, you can use
pymod␣=␣py.importlib.import_module(<path-to-module-OR-name-of-package>);

This can be very useful to load the py-eagle-logger module, for example
(eagle_logger).

Tip 25.4.1

If you ever see errors like Undefined variable "py" or "py.<Some Class>"
then your Anaconda environment is not setup correctly. Either recreate it by
following Tut. 25.1.3 or try manually calling your classes (cf. Tut. 25.4.1).

Tutorial 25.4.2 (Reloading Python modules) — Whenever you change Python files
that are used by Matlab it is important to reload them. Do so by using the following
command, where pymod is a Python module loaded using py.importlib.import_module
as described in Tut. 25.4.1.
py.importlib.reload(pymod);

5. Receiving information from other workspaces


Exercise 25.5.1 (Parsing Log Data (Intermediate)) — Take a look at Ex. 26.6.1
about the logger in C++. When calling the Autopilot framework via MEX the log
will be available from the output of the update method of the Control class.
(1) How can you display the log contents in Matlab?
As you will see the log is stored in a binary format. A parser is provided in Python in
EAGLE-students/Module_software/ANC/py-eagle-logger/. Use Exercise 22.2.1
to install the package on your computer.
(2) Use Tut. 25.4.1 to import and call the parser directly from Matlab.
Hint 25.5.1: Instead of passing the uint8 MATLAB log directly to the
parser, you might need to convert it first to a Python bytes object.
(3) Log some data in the Attitude::codegenControlSignal function in C++,
and decode and print that data in Matlab.

Tip 25.5.1

Parsing the logging data will already be useful when tuning the attitude con-
troller on the actual drone. It is definitely worth looking into this sooner rather
than later!

Exercise 25.5.2 (Inter-process communication (Intermediate)) — Take a look at


Ex. 26.6.2 about inter-process communication (IPC) in C++. How do the Simulator
implementation and the Autopilot implementation of the IPC differ? How would
you add a QR reference in both?
CHAPTER 26

Zybo – Autopilot Project


Pieter Pas

1. Setting up
The Autopilot framework running on the drone gets inputs from the Inertial
Measurement Unit (IMU) and the Remote Control (RC) (and, in a later stage, data
from other modules as well) and calculates the PWM values that need to be sent
to the motors. This software keeps the drone stable and steers it towards a desired
orientation and position. For a high-level overview of the Autopilot framework,
refer to Ch. 16. This chapter explains how to compile and run the Autopilot code
on Linux on the Zybo. Hence, before reading this chapter, make sure that you are
familiar with the instructions from Ch. 22 to deploy Linux on the Zybo.
As a prerequisite for this chapter, we also expect that you have performed the
installation instructions provided in Section 1 of Ch. 25.

Tip 26.1.1

Since you will be developing software that runs on an embedded Linux system,
it is highly recommended that you use a Linux system for development as well.
You can either install a distribution of Linux on your laptop (e.g. in a dual-
boot setup alongside Windows), use a virtual machine, or use the Windows
Subsystem for Linux (WSL)a. The latest long term support (LTS) version of
Ubuntu is a good choice (20.04 at the time of writing).
If you really cannot install Linux or WSL, compiling the code using Windows
is possible, but there will be no support for remote debugging. The ESAT
machines run Linux and can be used for development as well, but they do not
allow you to connect to the Zybo over the network, again preventing remote
debugging.
If you have a Mac or don’t want to use WSL on Windows, you can access
the ESAT machines remotely (even debugging is supported in this case). See
Section 4 for more details.
ahttps://docs.microsoft.com/en-us/windows/wsl/install

2. Working with VS Code


Tutorial 26.2.1 (Opening the ANC Projects in VS Code) — For ANC, there are
two main projects, namely the Autopilot Framework and the Simulator Framework.
In this tutorial, we will focus on how to open, configure and build them.
To do this, follow the instructions in Section 2 of the Visual Studio Code page in
the Doxygen documentation. You’ll find a detailed step-by-step guide with screen-
shots and references to the code. You should follow these steps carefully.
276
3. DEBUGGING AND DEPLOYING CODE 277

Exercise 26.2.1 (Navigating the code efficiently) — To learn how to navigate the
C++ code efficiently, do the exercises in Section 3 of the Visual Studio Code page in
the Doxygen documentation.

3. Debugging and deploying code


Tutorial 26.3.1 (Installing the cross-compilation tools) — To compile code for
the Zybo, you need a cross-compilation toolchain: a set of tools, including a cross-
compiler, that run on one platform (e.g. Linux on x86-64) and generate code for a
different platform (e.g. Linux on ARMv7). An important component of the cross-
compilation toolchain is the so-called “sysroot”: this is a folder that is similar to
the actual root filesystem of the Zybo and that contains all the necessary header
files and libraries that you need for compiling and linking your code for the target
system.
✓ Linux/ESAT/WSL: Download arm-zybo-linux-gnueabihf_sdk-buildroot.tar.gz
from Nextcloud and extract it using:
cd␣EAGLE-students/Module_software/ANC
tar␣xf␣~/Downloads/arm-zybo-linux-gnueabihf_sdk-buildroot.tar.gz

Then run the relocation script to make sure all paths are configured cor-
rectly:
./arm-zybo-linux-gnueabihf_sdk-buildroot/relocate-sdk.sh

✓ Windows: Download sysroot.zip from Nextcloud and extract the con-


tents to EAGLE-students/Module_software/ANC/sysroot, such that .../
ANC/sysroot/usr/ exists.
Next, download the arm-none-linux-gnueabihf compilers from here. Ex-
tract it using 7-Zip or the tar xf command in Git Bash. Add the bin
subfolder to your path.
Tutorial 26.3.2 (Building the Autopilot project for the Zybo) — In this tutorial,
we will build the Autopilot project for running on the Zybo.
Open the Autopilot project in VSCode and follow these steps:
(1) Select the “Zybo” kit (using the button in the blue ribbon at the bottom)
(2) Build the project (using the button next to it)
You should now have successfully compiled the Autopilot project for the Zybo.
The compiled files can be found in Autopilot/build/bin/.
Tutorial 26.3.3 (Running the tests and examples on the Zybo) — To run the
compiled binaries from Tut. 26.3.2 on the Zybo, you need to perform the following
steps:
(1) Follow Tut. 22.1.2. Don’t forget to copy the compiled files from Autopilot/
build/bin/ to the bin folder on the microSD card, but don’t copy the
Autopilot program just yet.
(2) Attach to the Zybo’s shell using screen (Tut. 22.1.3) or ssh (Tut. 22.1.4).
(3) Run the logger example using:
logger.example

Make use of the tab completion.


3. DEBUGGING AND DEPLOYING CODE 278

If you’re using screen, use Ctrl+A Esc to enter “copy mode” where
you can scroll up to read the full output, and Esc Esc to exit copy mode.
See Tut. 22.1.3 for more details.
(4) You can run the logger.test, math.test and students.test tests and
the client.example and server.example in the same fashion.
Did any of the tests fail?
(5) Use the poweroff command to cleanly shut down the system as explained in
Tut. 22.1.5. Failing to do so can damage the SD card and/or the filesystem.
(6) If you’re using screen, use Ctrl+A \ to exit the screen session, type y to
confirm.
Tutorial 26.3.4 (Running the Autopilot software on the Zybo) — If available,
the given autostart.sh script automatically starts the Autopilot software in the
background on boot.
(1) Like Tutorial 26.3.3, follow Tut. 22.1.2, but this time do include the
Autopilot program. Make sure that you have the IMU and other nec-
essary drone hardware connected to the Zybo. Boot the Zybo and connect
to it over SSH.
(2) You should see the LEDs flash during IMU calibration, and then you’ll be
notified by the buzzer that the calibration was successful.
(3) To see the output of the Autopilot software, attach to it using
tmux␣attach

(4) To detach again, use Ctrl+B D.


(5) To stop the Autopilot software, use Ctrl+C (while attached) or use killall
Autopilot (while detached).

Warning 26.3.1

When booting the Autopilot project, make sure that all wires are connected to
the right PMOD pins. For a visualization of the PMOD pins, see Fig. 1.

Exercise 26.3.1 (Using SSH to upload the compiled files (Beginner)) — Instead of
having te remove the microSD card from the drone every time you want to upload
your newly compiled Autopilot code, you can upload it directly through SSH, while
the Zybo is running.
(1) Follow Tut. 22.1.1 and Tut. 22.1.4.
(2) You should now be able to connect to the Zybo as follows:
ssh␣Zybo

Disconnect again by pressing Ctrl+D or by typing logout.


(3) Stop the previous version of the Autopilot software from running. Note
that you don’t need to do this in an interactive SSH session, instead, you
could use:
ssh␣Zybo␣killall␣Autopilot

(4) Use scp to copy the Autopilot software to the Zybo:


scp␣build/bin/Autopilot␣Zybo:/media/bin

(5) To start the new version of Autopilot, either reboot:


3. DEBUGGING AND DEPLOYING CODE 279

ssh␣Zybo␣reboot

Or start the Autopilot software over SSH in the foreground:

ssh␣Zybo␣Autopilot

Or use tmux to start it in the background:

ssh␣Zybo␣tmux␣new-session␣-d␣Autopilot

(6) When you’re done, cleanly shut down the Zybo as explained in Tut. 22.1.5.
Failing to do so can damage the SD card and/or the filesystem.

ssh␣Zybo␣poweroff

Exercise 26.3.2 (Connecting a remote debug session (Beginner)) — Debugging


using print statements is cumbersome, we highly recommend using (or at least con-
sidering) to use a real debugger that allows you to set breakpoints, inspect variables,
step through the code, and so on. Currently, remote debugging is only fully sup-
ported on Linux (including WSL), but not on ESAT machines, because they don’t
allow you to modify the network settings or attach Ethernet adapters.
(1) Make sure you can connect to the Zybo over SSH, as explained in Tutorial
22.1.4. Make sure that the program you wish to debug is not already
running (you can terminate it using killall).
(2) Open the Autopilot project in VSCode.
(3) Select the “Zybo” kit (using the button in the blue ribbon at the bot-
tom).
(4) Ensure that the CMake build type is set to [Debug].
(5) Select the binary you want to debug (by clicking the target name to the right
of “Run” button, or using the “CMake: Set Debug Target” command).
As an example, select the logger.example program.
(6) Use Ctrl+Shift+D to open the debug panel.
(7) Select “Remote GDB Launch (SSH)”.
(8) Open one of the source files of the program and set a breakpoint by clicking
to the left of a line number. A red disk should appear. (Set a breakpoint
on a line that you know will actually be executed.)
(9) Use F5 to start debugging. CMake will automatically build the binary,
it will then be copied to the Zybo over SSH (see Autopilot/.vscode/
tasks.json), a GDB server will be started on the Zybo, and VSCode will
connect to it using GDB (see Autopilot/.vscode/launch.json).
(10) The execution of the program should stop at the breakpoint you set. Use
the controls at the top (or even better: use their shortcuts) to step through
the code.
(11) Inspect some variables in the left panel or hover over the variable name in
the code.
(12) Have a look at the call stack. If you’re debugging the Autopilot software:
why are there three different call stacks? Click a stack frame to go to open
the source code of that function and inspect the variables and function
arguments there.
4. REMOTE DEVELOPMENT USING VSCODE 280

4. Remote development using VSCode


Tutorial 26.4.1 (Remote development using an ESAT machine) — If you’re using
a Windows or Mac machine, you might run into issues when using the development
tools for Linux. In that case we recommend using the ESAT machines remotely:
your text editor runs on your laptop, but the compiler, debugger, etc. run on the
ESAT machine, and your files are stored there as well.
(1) Generate an SSH key pair on your laptop (see this link) if you haven’t
already. Make sure to use a passphrase, especially if your laptop is not
encrypted. It’s convenient to add it to your key agent so you don’t have to
type out the key’s passphrase every time you use it.
(2) Install the key to your ESAT account so you can login to the ESAT network
without entering your password each time. For example, if your SSH key
is stored in ~/.ssh/id rsa and if your r-number is r0123456, use
ssh-copy-id␣-i␣~/.ssh/id_rsa␣[email protected]

If you don’t have ssh-copy-id installed, you can use (in Bash or Power-
shell):
cat␣~/.ssh/id_rsa.pub␣|␣ssh␣[email protected]␣'cat␣>>␣~/.ssh/authorized_keys'
# Note the .pub extension, don't share your private key!

Enter your ESAT password when prompted.


(3) Edit your laptop’s SSH configuration. Open the file ~/.ssh/config or
create it if it doesn’t exist yet. Add two entries: one for the ESAT gateway
“Helium”, and one for the ESAT PC you want to work on.
Host␣helium
␣␣␣␣HostName␣ssh.esat.kuleuven.be
␣␣␣␣User␣r0123456
␣␣␣␣AddKeysToAgent␣yes
␣␣␣␣IdentityFile␣~/.ssh/id_esat

Host␣pc-klas1-1
␣␣␣␣HostName␣pc-klas1-1
␣␣␣␣User␣r0123456
␣␣␣␣AddKeysToAgent␣yes
␣␣␣␣IdentityFile␣~/.ssh/id_rsa
␣␣␣␣ProxyJump␣helium
␣␣␣␣ForwardAgent␣yes
␣␣␣␣ForwardX11␣yes

Edit your r-number and SSH key file. Choose a PC to connect to (having
everyone connect to the same PC is a bad idea for obvious reasons). The
machines in the ESAT PC rooms are not exposed to the internet directly,
so you have to use Helium as a proxy. Read more about the options in the
configuration file in man ssh config.
(4) Log in to the ESAT PC:
ssh␣pc-klas1-1

This should not require your ESAT password.


(5) Follow the installation instructions in Tutorial 25.1.2, 25.1.4 and 26.3.1 for
the ESAT machine.
(6) On your laptop, open VSCode and install the Remote – SSH extension.
5. GETTING TO KNOW THE FRAMEWORK 281

(7) Use Ctrl+Shift+P and execute the “Remote – SSH: Connect to Host”
command. Select the ESAT PC (e.g. pc-klas1-1). Wait while the VSCode
server is installed.
(8) In the extensions tab (while connected to the ESAT PC), install the C/C++
and CMake Tools extensions (even if you already installed them to your
laptop, you have to install them on your ESAT account as well).
(9) Clone the EAGLE-students repository on the ESAT machine if you haven’t
already.
(10) Follow Tutorial 26.2.1 and 26.3.2.

5. Getting to know the framework


Before you continue, definitely check out Exercise 26.2.1 first.
Tutorial 26.5.1 (Doxygen documentation) — The project is documented using
automatically generated Doxygen documentation, which is hosted at
https://eagle-gitlab.pages.esat.kuleuven.cloud/EAGLE-students/main/Doxygen/, (or
a similar link for your own fork of the repository, go to https://gitlab.esat.kuleuven.
be/EAGLE-gitlab/EAGLEx/EAGLE-students/pages to see the URL, replacing x
by your team number).
It is strongly recommended that you document your own code in this way as
well, following the syntax described in §11 of Chapter 12. To generate your own
documentation, open the Autopilot project in VSCode, then press Ctrl+P, type
“task doc” and hit enter to run the “Generate Documentation” task.
To view your generated documentation, start a web server using
python3␣-m␣http.server␣-b␣localhost␣8000␣--directory␣docs/Doxygen

and browse to http://localhost:8000.


See also Section 6 of the Visual Studio Code page in the Doxygen documentation.
Exercise 26.5.1 (Bypassing the controller (Beginner)) — Sometimes, one of the
drone’s commercial ESCs may lose its calibration. When this happens, its motor
may behave strangely or will simply not spin when you increase the throttle. The
calibration procedure for the ESCs will essentially tell the ESCs about the minimum
and maximum input PWM they can expect.
(1) The attitude controller should be disabled during calibration. Do you un-
derstand why?
We will bypass the controller manually here (since this gives us more flexibility).
(2) Open given/main/src/Main.cpp, the main heartbeat of the C++ frame-
work. Can you find where the PWM signals are passed to the motors?
(3) We want to send the value obtained from the RC transmitter’s throttle
stick directly to the ESC. How can we access this value of the throttle stick
within given/main/src/Main.cpp?
Hint 26.5.1: Take a look at the RCInput struct. Use the search func-
tionality from Doxygen to do so (cfr. Tut. 26.5.1).
(4) See where the output of the autopilot controller is used and change it in
such a way that the thrust is passed directly to the motors.
Hint 26.5.2: Only one line of code needs to change.
(5) Don’t forget to roll back your changes after testing, so you can test your
attitude controller without issues later.
5. GETTING TO KNOW THE FRAMEWORK 282

Figure 1. The location of the LEDs on the Zybo

Tip 26.5.1

The framework already supports a calibration mode out of the box as explained
in Tut. 27.2.1.

Exercise 26.5.2 (Controlling the LEDs (Beginner)) — The Zybo has 4 on-board
LEDs, as visualized in Fig. 1. They can be used as a quick debugging tool for various
issues. At startup, the LEDs show whether the FPGA and the C++ are booted
properly: if all the LED lights remain turned off, then the FPGA has not been
started up properly; if all the lights remain on, then the FPGA started up properly
but something went wrong in the C++ code. When everything boots correctly, the
LEDs will display the following information:
✓ LD0 indicates whether the code is throttling. This occurs when running
your controller code takes longer than the interval between two IMU mea-
surements.
✓ LD1 indicates whether the drone is armed (glowing) or not (extinguished).
✓ LD2 indicates whether the Wireless Power Transfer (WPT) switch is on or
off.
LD3 is not used in the given platform and the students are free to use it as they
desire. To learn more about the LED code, perform the following tasks:
(1) Inspect the LED code in given/main/src/Main.cpp and
given/io/src/IMU.cpp.
(2) Use LD3 to indicate something of interest, e.g. whether the drone is in
altitude mode or not.

Exercise 26.5.3 (AHRS and friends (Beginner)) — Tuning is one of the design
steps that will take a considerable amount of time in the EAGLE project. The
most obvious tuning parameters are the weight matrices in your Kalman filter and
LQR/LQI controller. However, there are some others. We study these tunable
components in this exercise.
We will give a list of these components below. Try to identify where they are
implemented in the C++ framework. How are they used?
(1) The Madgwick beta parameter. It acts similarly to the weighting matrices
in a Kalman filter, i.e., it weights the trust in the prediction of some model
and the trust in the measurements.
6. COMMUNICATING WITH OTHER MODULES 283

(2) The sonar outlier rejection settings. There are two, the max jump size and
the number of consecutive outliers that we reject.
(3) The sonar filter. Currently this is an exponential moving average, however
code for median filters is also available.
(4) The hovering and equilibrium roll-pitch bias filters.
(5) The IMU callibrator.
Use the C++ code as a reference to answer the following questions.
(1) Where is the Madgwick algorithm used? How is it initialized?
(2) How are the sonar filters initialized?
(3) What is the use of the hovering and equilibrium roll-pitch bias filters? How
are they initialized/updated?
(4) Why do we have two sample count parameters (CALIBRATION SAMPLES
and INVALID SAMPLES)? Why do we need to callibrate? Could we im-
prove the callibration procedure?
Hint 26.5.3: You should study Ex. 26.2.1 first. The workspace search function-
ality in VSCode is very powerful (Ctrl+Shift+F), especially the “Go to Symbol in
Workspace” feature (Ctrl+T). Use Ctrl+P to search and open a file by name. To
search within a file, you can use Ctrl+F for a text search or Ctrl+Shift+O to search
for the definition of a symbol. The outline panel can be useful to keep an overview
as well (hit Ctrl+Shift+P and execute the “Focus on Outline View” command).
Hint 26.5.4: The Doxygen documentation is your friend.
Hint 26.5.5: Look up IMU misalignment (Advanced).

6. Communicating with other modules


Exercise 26.6.1 (Logging Data (Beginner)) — One component that is very im-
portant for C++ development is the logger. After all, when you are testing in flight
you will be blind without it.
(1) Take a look at the logging page in the Doxygen documentation to learn
about the given logger code.
(2) Inspect the given Logger-example.cpp file and execute logger.example
on your pc (see Tut. 26.2.1 for detailed instructions).
(3) Try to figure out how you can log variables within the Autopilot framework.

Hint 26.6.1: Check Main-Students.cpp for some examples.


Exercise 26.6.2 (Inter-process communication (Intermediate)) — It is important
that the Autopilot code is able to communicate with other processes to transer data
such as the position measurements from the Vision module and the QR targets from
the Crypto module. Therefore, you will need to perform inter-process communica-
tion (IPC).
(1) Take a look at the inter-process communication page in the Doxygen doc-
umentation to learn more about the IPC code.
(2) Compare the client.example and server.example scripts with the IPC
code from the Autopilot framework.
Hint 26.6.2: Take a look at the initComms function in Main-Students.cpp.
6. COMMUNICATING WITH OTHER MODULES 284

Exercise 26.6.3 (Printing the Logging Data (Beginner)) — For the logger to be
useful, you’ll have to print or save the logging data somewhere. A simple Python
example is provided:
(1) Build the Autopilot software and put it on the SD card as explained in Tut.
26.3.4.
(2) Build and install the logger as explained in Tut. 22.2.3 and Tut. 22.2.4.
(3) Copy the EAGLE-students/Module_software/ANC/py-eagle-logger/examples
folder to the SD card as well.
(4) Boot the Zybo and log in over serial (Tut. 22.1.3) or SSH (Tut. 22.1.4).
Wait for the Autopilot software to start.
(5) Run the Python script:
/media/examples/read-and-parse-logging-data.py

(6) Can you trace the entire journey that the logging data takes from the point
where you call Logger::log() in the mainStudents() function to where it
is printed in the Python script? Why does Autopilot use a separate thread
for logging?
Exercise 26.6.4 (Sending Vision Data (Intermediate)) — Eventually, the Autopi-
lot software will need position measurements and references from other submodules,
i.e. from other processes on the Zybo. This exercise helps you send data between
them.
(1) Write a Python script that:
✓ Connects to the Unix Domain Socket at /tmp/autopilot-vision.
(Where does Autopilot open this socket?)
Hint 26.6.3: Look at the Python sockets code of Exercise 26.6.3.
✓ Encodes some dummy vision data.
Hint 26.6.4: Use Python’s struct.pack function. Find the VisionData
struct and the call to LOGGER ADD STRUCT TYPE(VisionData, ...) in
the Autopilot software to know the format of the data.
✓ Sends the encoded data over the socket.

(2) In the Autopilot code, in StudentAutopilot::mainStudents(), check if


new vision data is available, and if so, print it.
(3) Build and run your code, verify that Autopilot indeed prints the data when
you run your Python script.
(4) Can you trace the entire journey that the vision data takes from the point
where you call socket.send in the Python script to where it is printed
in the Autopilot code? Why do we need the VisionDataChannel class?
Why not just have a normal VisionData variable that is read in the
mainStudents() function?
Hint 26.6.5: Have a look at
https://en.cppreference.com/w/cpp/language/memory model.
Power ethernet HDMI SD card

project.
connec�on connec�on connec�on

JF

UART
connec�on

JA

IMU: I2C SDA IMU: I2C CLK


ADCinp_14
ADCinn_14

285
USB

Int_imu
connec�on
1. Overview
CHAPTER 27

ADCinp_6
Arne Symons

ADCinn_6
Drone Platform

JE JC
Motor FL Motor FR Motor BL Motor BR Sonar
SWIPT 0 SWIPT 1 SWIPT 2 SWIPT 3

JD JB
RC_rotz RC_roty RC_rotx RC_thro�le testpin RC_kill_test
RC_induc�ve RC_mode RC_tuner RC_killswitch sounder
it connects to everything, you understand how the drone is built.

3V3 GND pin4 pin3 pin2 pin1


3V3 GND pin10 pin9 pin8 pin7

Figure 1. The IO connections for the ZYBO used in the EAGLE


The Zybo is the heart of the drone platform. Therefore, if you understand how
2. DEBUGGING COMPONENTS 286

Exercise 27.1.1 (Understanding the drone platform (Easy)) — When you first
start testing components on the drone it is a good idea to check if all cables are still
connected. Take a look at Fig. 1 and verify whether all cables are where you expect
they are.
How is power provided to the Zybo? The options are: (i) wall socket; (ii) 5V
power; and (iii) power from USB. Look for a jumper with corresponding labels.

Warning 27.1.1

Two versions of the Zybo platform exist: the Zybo, and the Zybo-Z7. The
tutorials in this document contain files and instructions for either Zybo type.
In academic year 2021-2022, use the files for the Zybo (not the Zybo-Z7).

2. Debugging components
Tutorial 27.2.1 (Calibrating the ESCs) — The motor controllers have to know
the range of duty cycles the Zybo sends. If you notice that the four motors are not
responding equally or they don’t turn on at all, you should calibrate the ESCs. To
do this, you must send the maximum and minimum signals during power up.

Warning 27.2.1

The motor controller calibration routine involves sending 100% to all motors.
Do NOT perform calibration with the propellers attached. EVER.

(1) Disarm the drone by holding the left joystick in the bottom-left corner for
at least 2 seconds, until you hear the disarm sound.
(2) Disconnect the red power wires of all four motor controllers.
(3) Turn off the WPT switch (it should point away from you).
(4) Keep both joysticks in the top-right corner for at least 4 seconds, until the
buzzer starts sounding an alarm. The drone is now in motor calibration
mode.
(5) Turn on the WPT switch. The Zybo will now send 100% to all four motor
controllers.
(6) Plug in the red power wires of the motor controllers. The motors should
produce a series of four beeps, indicating that they are calibrating.
(7) Turn off the WPT switch again. The signal to the motors is now 0%. The
motors should beep three more times to indicate that the calibration was
successful.
(8) Disarm the drone by holding the left joystick in the bottom-left corner for
at least 2 seconds, until you hear the disarm sound.
For more information see the ”Throttle Range Calibration” section in the AfroESC
User Manual.
Exercise 27.2.1 (Visualizing the sensor measurements) — Sometimes it is unclear
whether the drone behaves weirdly due to bad controllers or due to wrong mea-
surements. Logging and visualizing those inputs and outputs can give you a better
understanding of what is happening.
3. CHANGING COMPONENTS 287

(1) Prepare an SD card for the Zybo as described in Tut. 22.1.2.


(2) Download visualizer.zip from Nextcloud and extract it.
(3) Copy the Visualizer binary for your IMU to the bin folder on the SD
card.
(4) Copy the eagle visualizer zybo-*.whl package to the wheels folder on
the SD card.
(5) Install the eagle visualizer-*.whl package on your computer:
python3␣-m␣pip␣install␣eagle_visualizer-*.whl

(6) Connect the Zybo via Ethernet with your computer and turn it on. Con-
figure your network as described in Tut. 22.1.4.
(7) Make sure that Autopilot is not running:
ssh␣Zybo␣killall␣Autopilot

(8) Start the Visualizer software on the Zybo:


echo␣'Visualizer␣&␣eagle_visualizer_zybo'␣|␣ssh␣Zybo␣bash␣-l

(9) Start the GUI on your computer:


eagle_visualizer

Tip 27.2.1

It might be useful to keep a separate SD card or folder with the files for the
Visualizer. This way you can quickly check whether all sensors still work cor-
rectly.

3. Changing components
When you notice you constantly have to calibrate the ESCs or the IMU is not
working anymore, it may be time to change them.
Tutorial 27.3.1 (Changing the ESCs) — Changing the ESCs requires no different
source code and is very straightforward. Just pay attention towards the following:
✓ Make sure you mount the ESCs on the ’arms’ of the drone. This way the
propellers provide some extra ventilation.
✓ Make sure the propellers can rotate freely and all cables are securly con-
nected.
✓ Pay extra attention when connecting the ESCs with the motors. If you
don’t reconnect them the same way, the motor may turn in the wrong
direction. After changing the four ESCs, apply low throttle to check if they
are all still turning the right way.
✓ The new ESCs have the same calibration procedure as described in Tutorial
27.2.1, but you will notice that the beeps are different.
Tutorial 27.3.2 (Changing the IMU) — Since academic year 2022-2023 the spare
IMUs are different from the previous ones. When changing them, you also need
to change a build option in the Autopilot project. This tutorial describes the
installation of the LSM6DS0 using the STEVAL-MKI197V1 adapter board:
(1) Mount the board on the drone according to Fig. 2.
3. CHANGING COMPONENTS 288

(2) Connect the GND, VDD, SDA, SCL and INT2 pin to the Zybo (see Fig. 1).
Also connect SDO to ground and VDDIO, SCx and SDx to VDD.
(3) Go to Autopilot/src/drivers/CMakeList.txt and change the default
value of the AUTOPILOT IMU variable to "lsm6ds0".
(4) In VS Code, select the Autopilot project, press Ctrl+Shift+P and execute
the CMake: Delete Cache and Reconfigure command.

Figure 2. The correct mounting direction of the LSM6DS0 on the


drone.
Part 4

Information for Teaching Assistants


CHAPTER 28

Content guidelines

Each module should keep track of several parts in the document.


✓ Part 1 contains the description of the project and the task description for
each module. This module chapter should serve as a first reading for new
students.
✓ Part 2 contains background material. This serves to bring students up-to-
date with certain requirements for the project. The most classical example
is information on programming languages they will be using.
✓ Part 3 contains project-specific tutorials. These are organized per workspace
to incentivise communication between module TAs, thereby avoiding redun-
dancies.

Tip 28.0.1

Part 3 is designed to be changed quickly during the year, thereby serv-


ing as a Frequently Asked Questions. It should also be kept minimal,
elliminating tutorials that are not used.

If you are unsure where a piece of content should be added, please consider the
following guidelines:
(1) The default format should always be a tutorial or exercise. These are easy
to reference and adjust during the year.
(2) If there is too much information to include in tutorials and the content
is not EAGLE specific (e.g. Ch. 15) or acts as an informative reference
(e.g. Ch. 16) then consider adding a new chapter in Part 2. Usually these
chapters will only be read once in the beginning of the project and used
afterwards as a quick reference.
(3) If the information is critical to one module and should be understood before
continuing with the remainder of the project then consider adding a section
to your module chapter in Part 1.
Adding new chapters to Part 2 requires due consideration. The workspace struc-
ture incentivises interaction between TAs over modules, avoiding redundancies. To
provide an example of how this manifests in practice, note that we once considered
splitting the Vivado chapter (i.e. Ch. 19) in two parts: one for SWIPT and one for
CRYPT. Both development for SWIPT and CRYPT however should occur in the
same Vivado project. This would be obfuscated by having multiple chapters. For
the two sub-Vitis chapters (i.e. Ch. 21 and Ch. 26) this is not an issue, since the
projects can be compiled seperately and run of separate cores on the Zybo.

290
CONTENT GUIDELINES 291

Warning 28.0.1

If a chapter is added to Part 2 note that Ch. 1 should be updated accordingly.


CHAPTER 29

LateX guidelines

We provide an overview of environments and tools provided by the template.

1. Tutorial and Exercise environments


These should be used in Part 3. They are the smallest unit of information
provided to the reader and provide a student with specific instructions required to
complete some task.
Tutorial 29.1.1 (Adding tutorials) — If you want to give detailed instructions
required to complete a task you add a Tutorial environment.
A␣tutorial␣environment␣is␣given␣as,
\begin{tutorial}[Example␣Tutorial]
␣␣␣␣\label{tut:example}
␣␣␣␣This␣is␣the␣content.
\end{tutorial}
We␣can␣refer␣to␣it␣as␣\cref{tut:example}.

Tutorial 29.1.2 (Adding exercises (Beginner)) — If you want to leave things open
to the student you can add an Exercise. These are most useful to direct the attention
of the students towards a specific component, without giving them the complete
procedure required to implement it. You could consider combining the exercise with
a description in Part 2 or just provide links to documentation online.
An␣exercise␣environment␣is␣given␣as,
\begin{exercise}[Example␣Exercise␣(Difficulty)]
␣␣␣␣\label{ex:example}
␣␣␣␣This␣is␣the␣content.
\end{exercise}
We␣can␣refer␣to␣it␣as␣\cref{ex:example}.

We consider four levels of difficulty for exercises and provide a loose guideline for
when to use them.
(1) Beginner: Every student should be able to complete these. They take about
10 minutes;
(2) Intermediate: Most students should be able to complete these. They take
about 30 minutes;
(3) Advanced: Most students should be able to complete these. They take
about an hour;
(4) Expert: These are only for the students that want in depth understanding
of the material. They can take a whole two hour session to complete.

2. Emphasizing
We add some boxes that allow an author to emphasize information to students.

292
3. DISPLAYING CODE SNIPPETS 293

Warning 29.2.1

Warning boxes are used for information that is absolutely essential. When
students ignore this piece of information they will either break something or
loose considerable time to fix/debug the resulting issue.

Tip 29.2.1

Tip boxes are snippets of information that can act as timesavers. Actually using
this info is optional.

\begin{warningbox}
␣␣␣␣Warning␣boxes␣are␣used␣for␣information␣that␣is
␣␣␣␣absolutely␣essential.␣When␣students␣ignore␣this
␣␣␣␣piece␣of␣information␣they␣will␣either␣break␣something
␣␣␣␣or␣loose␣considerable␣time
␣␣␣␣to␣fix/debug␣the␣resulting␣issue.
\end{warningbox}

\begin{tipbox}
␣␣␣␣Tip␣boxes␣are␣snippets␣of␣information␣that␣can␣act␣as
␣␣␣␣timesavers.␣Actually␣using␣this␣info␣is␣optional.
\end{tipbox}

3. Displaying code snippets


We can display parts of code using lstlisting. All languages are defined in
src/preambles/codex.sty. If you add your own, please be consistent with the
other pre-defined types.
We have the following pre-defined sets:
✓ matlab;
✓ json;
✓ bash;
✓ c (also for cpp);
✓ verilog;
✓ python;
✓ latex.
␣␣␣␣begin{lstlisting}[style=latex]
␣␣␣␣␣␣␣␣%% add \ before begin and end
␣␣␣␣␣␣␣␣␣␣␣␣\begin{enumerate}
␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣␣\item␣Some␣item.
␣␣␣␣␣␣␣␣␣␣␣␣\end{enumerate}
␣␣␣␣end{lstlisting}
␣␣␣␣And␣for␣some␣inline␣lst
␣␣␣␣\lstinline[style=latex]{some␣inline␣LateX}.

Warning 29.3.1

As seen above, indenting lstlisting environments should be avoided. The


indenting is included in the display for easy copy-pasting.
6. COMPILATION 294

Warning 29.3.2

Copy-pasting was tested in Firefox and Adobe Acrobat Reader and these are
the recommended viewing applications. In Chrome, indentations are copied as
a single space.

4. General typesetting
this

Below you can find some general guidelines for typesetting in tutorials.
† like

You can comment to other authors using the \todo or \*todo. These add some
red text in the margin†or inline respectively. These are defined in such a way that
they can be removed easily when compiling the document for students. These are
implemented using tikz nodes, to which optional arguments can be passed. For
example \todo[yshift=1cm]... shifts the node 1cm to the left.
In tutorials you should prefer screenshots when you have them. Whenever re-
ferring to text on the screen use texttt. This also correctly typesets symbols like <,
> and (for which you use \_). When you want to imply some nested options use >.
As an example, you can save a file in Word by using File > Save As.
When referring to sections or environments use cref. This automatically adds
Tut. or §before the reference number.
We provide the checklist environment for checklists.
You can add custom commands to src/preambles/notation.tex.

Warning 29.4.1

Try to use as little packages as possible. If you do need any, add them in
src/preables/eaglebook.cls after line 60.

5. Installation
Installation scripts for common platforms have been provided to quickly install
texlive, biber, latexmk, mdframed, thmtools, and some other tools required for the
compilation of EAGLE Book. They can be found in the install folder.

6. Compilation
The LATEX project has been setup for Visual Studio Code with the LaTeX work-
shop plugin in Linux (or WSL). If you have both installed and open the root folder
you will be able to build/visualize the project automatically.
If you wish to use another IDE, please note that compilation can happen using
the Makefile included. If your IDE does not support this (then either consider using
another IDE) or you can try to mimic the setup of the Makefile.
The important command is on line 35 and uses latexmk as a compiler. Run it
from the src folder.
max_print_line=200␣latexmk␣-pdflatex=pdflatex\
␣␣␣␣-shell-escape␣-interaction=nonstopmode\
␣␣␣␣-file-line-error␣-synctex=1\
␣␣␣␣-outdir=../output␣-pdf␣./EAGLE-book.tex
gs␣-sDEVICE=pdfwrite␣-dPDFSETTINGS=/printer␣-dPrinted=false␣-dNOPAUSE\
␣␣␣␣-dQUIET␣-dBATCH␣-sOutputFile=tmp.pdf␣./EAGLE-book.pdf
mv␣tmp.pdf␣./EAGLE-book.pdf
6. COMPILATION 295

If you run into issues, use the src folder as the root folder of the LATEX project.
Then everything should act like usual. Make sure to not push any non-source code
files. The gs command compresses figures in the pdf to reduce file size. If you use
the Makefile, this is done automatically.

Warning 29.6.1

When publishing for students remember the following:


✓ The todo flag on line 1 of EAGLE-book.tex should be set to false
\documentclass[todo=false]{eaglebook}

✓ The version on line 11 of EAGLE-book.tex should be updated


\version{x.y}

The first number x should be incremented each new academic year (0


for 2021–2022) and y should be incremented each time a new version
is send to students.
Check if Chapter 1 is up-to-date with the Workspaces defined in Part 3.
CHAPTER 30

Zybo – Linux Build

1. Introduction
This guide is meant for bootstrapping an entire Linux system, from bootloader
to userspace, for the Zybo and Zybo-Z7.
This guide is accompanied by an archive consisting of configuration files, READMEs
(containing mostly the same text as in this chapter of the book), precompiled im-
ages, source code of the components, and more. It is available at https://gitlab.esat.
kuleuven.be/EAGLE-gitlab/eagle-zybo-linux. You will need at least the configura-
tion files for successfully compiling and running the system.
Bootstrapping an embedded Linux consists mainly of three parts:
✓ The bootloader (in this case, U-Boot)
✓ The kernel (Linux)
✓ Userspace (in this case, Buildroot)
Pre-built binaries for all the above (bootloader, kernel and root filesystem) are
available in their respecive folders in the archive, however, you are encouraged to
try going through the entire bootstrapping and compilation process yourself.
The bootloader is responsible for initializing the hardware and loading the op-
erating system kernel. For this, U-Boot is used. Two differing U-Boot builds are
used, as the Zybo and Zybo-Z7 hardware differs enough to require separate builds.
See §2 for a guide on how to use, compile and install U-Boot.
Next is the Linux kernel, responsible for handling multiple user processes, man-
aging the hardware and letting the user code access it through device drivers, etc.
While it can be compiled separately (see §3), it is also possible to have it compiled
together with the rest of the Buildroot system.

Tip 30.1.1

It is still strongly advised to read the Linux compilation section, even when
using Buildroot to compile the Linux kernel, as several things explained in the
Linux kernel section are not explained again in the Buildroot section.
Last but not least is the Buildroot userspace and root filesystem, documented
in §4: it creates the standard filesystem hierarchy (with /bin, /etc and so on),
provides the standard command-line tools (such as ls, bash, etc., using Busybox),
manages the code that gets executed on system startup, and so on.
It is also responsible for the C library. For this I have opted for using glibc:
Buildroot’s default is the ill-supported uClibc, and, while musl would be a better
fit in a production embedded system, it has one downside compared to glibc that
makes its use slightly more annoying in an educational setting: most Linux distros
come with GCC ARM cross-compiler packages that can readily be installed, but
only for glibc, not for musl. In a quick, informal survey of Ubuntu, CentOS, Arch
296
2. THE U-BOOT BOOTLOADER 297

Linux and Void Linux, only the last one has a musl cross-toolchain readily available
(cross-arm-linux-musleabihf) (Arch does have one, but it must be compiled from
source, as it is only available from the AUR). The cross-toolchain packages for these
distros are:
✓ Ubuntu: {gcc,binutils}-arm-linux-gnueabi or
{gcc,binutils}-arm-linux-gnueabihf
✓ CentOS: {gcc,binutils}-arm-linux-gnu
✓ Arch Linux: arm-linux-gnueabihf-{gcc,glibc,binutils} (AUR)
✓ Void Linux: cross-arm-linux-gnueabihf
Distributions such as Debian, Linux Mint, Pop! OS, or Fedora and RHEL, have
not been checked, as these mostly draw from the same package repositories as resp.
Ubuntu and CentOS.
If no toolchain is available, one can always be created using Buildroot (see §4.2.3
for information on how this is done). Alternatively, a toolchain can be used directly
from an installed Vitis directory if you have Vitis installed,
Vitis/2021.1/gnu/aarch32/lin/gcc-arm-linux-gnueabi/bin/. On ESAT ma-
chines, the Vitis install directory is /esat/micas-data/software/xilinx vitis 2021.1/.
Programs can be compiled for this Linux installation on the Zybo by simply
using the arm-linux-gnueabihf cross toolchain instead of the default one on your
machine.
The Linux system described above, running on the first ARM core of the Cortex-
A9, needs to coexist with ANC baremetal code running on the second core. In
order to avoid them stepping on each others toes and accessing each other’s data
in memory without knowing (which would cause hard-to-debug crashes and lockups
on either side). The old Zybo has 512 MiB RAM (ranging from physical address
0 to 0x1fff’ffff), the Z7 has 1024 MiB RAM (up to 0x2fff’ffff). ANC gets
addresses 0x1000’0000 up to 0x17ff’ffff on either edition. This split is ensured
by, on the Linux side, declaring a carveout region in the device tree which Linux
cannot touch (see §3.1.7), and, on the ANC side, limiting the memory range declared
in the linker script.

2. The U-Boot bootloader


2.1. Introduction and usage notes.

This section provides an outline on how the EAGLE U-Boot build should be
used, and some information on how the boot process happens.
The Zynq boot ROM loads a file called BOOT.BIN from the microSD card, which
contains the next stages of the boot process code. This BOOT.BIN file typically
contains:
✓ A “first-stage bootloader” (FSBL), U-Boot calls this the “secondary pro-
gram loader” (SPL).1 This one is board model-specific, as it is responsible
for initializing and training the DRAM chips present, which vary per board
revision.
✓ An initial FPGA bitstream. Some of these are also board-specific. A default
one, taken from Petalinux, can be used, providing a number of generic
peripherals to the ARM Processing System complex.
1This discrepancy can be explained because U-Boot calls the boot rom, residing in mask ROM
and is the first code that gets executed on the chip, the “primary program loader”. Xilinx presum-
ably calls it the “zeroth-stage bootloader” or such.
2. THE U-BOOT BOOTLOADER 298

✓ A “second-stage bootloader” (SSBL), U-Boot calls this the “tertiary pro-


gram loader” (TPL). In our case, this is U-Boot. While the exact ELF file
used here does indeed vary depending on the board, the only difference in
configuration is which device tree is used. U-Boot will eventually read a
Linux kernel from the microSD card, and boot it.
How to generate these BOOT.BIN files is explained further below in §2.2.6, how-
ever, if a custom bitstream does not need to be loaded early at boot, one of the
default BOOT.BIN files present in the archive can be used.
The boot process to load and execute a Linux kernel performed by U-Boot is
implemented as a script called boot.scr, its source can be found in boot.scr.txt,
and can be generated from the source by the following command:2
mkimage␣-T␣script␣-c␣none␣-n␣"boot␣script"␣-d␣boot.scr.txt␣boot.scr

The script performs the following steps:


(1) It tries to look for a root filesystem image, by looking for the files /rootfs.cpio.uboot
and /boot/rootfs.cpio.uboot.
(2) By checking some U-Boot environment variables, it tries to figure out
whether it is running on a 1st edition Zybo, or on a Zybo-Z7.
(3) Depending on the result of the previous step, it tries to load either zynq-zybo.dtb
or zynq-zybo-z7.dtb as device tree, in both the root directory and the
/boot folder of the microSD card. If the file is not found, it tries to load
system.dtb (in both / and /boot) as a last resort measure.
(4) Finally, it tries to load a Linux kernel from uImage or zImage (in that
order), in both / and /boot as well.
(5) If all the previous steps succeeded, the script will now decompress the kernel
and rootfs images, and subsequently boot Linux. If not, an error message
is displayed on the UART, and the user is given a U-Boot console.

2The mkimage utility is provided by U-Boot in its tools/ directory, however, some distros have
it available from a package
2. THE U-BOOT BOOTLOADER 299

Tip 30.2.1

As the Zybo and Zybo-Z7 have some hardware differences (different DRAM
chips, different on-board oscillator frequency, different Pmod/button ↔ BGA
connection assignments , etc.), two different U-Boot builds are needed, as well
as two different BOOT.BIN files, with slightly different FSBLs and bitstreams as
well.
If the wrong U-Boot build, BOOT.BIN file, or device tree is used, the following
can happen:
✓ U-Boot does not start. Typical of a wrong BOOT.BIN binary (with
wrong FSBL), as DRAM initialization will fail.
✓ Line noise on the serial output port. Typical of a wrong device tree
or U-Boot binary used, as it declares the frequency of the on-board
oscillator which U-Boot will use as reference.
✓ Bad pinout used, buttons/switches not working, CPU freezes. Typical
of a wrong bitstream used.
✓ If the above only happens on Linux and not while U-Boot is running,
this is probably indicative of a bad device tree file being used.
✓ If ethernet does not come up, you need to modify the settings of your
FPGA block diagram to enable ethernet access from the ARM cores.
Please consult a TA.
✓ If Linux is constantly issuing messages/warnings about the CPU clock
speed, you are using a bitstream for the wrong Zybo board edition.
Recheck your Vivado project settings, and the Zynq PS7 clock settings
in your block diagram.

2.2. Compiling the pre-configured U-Boot.

2.2.1. Prerequisites and dependencies.

The following programs and packages are required to compile U-Boot success-
fully:
✓ A GCC and binutils cross-compilation toolchain, of the arm-none-eabi tar-
get (arm-linux-gnueabi or arm-linux-gnueabihf will also work). How-
ever, if a different cross-compilation toolchain is used, please do not forget
to change the CROSS COMPILE parameters used below.
✓ git
✓ dtc
✓ swig
✓ Common build tools, often packaged together by distros in a metapackage
called build-essential or base-devel or such.
More precisely, the tools make, bison, flex, rsync, libelf-dev (or
similar), ncurses-dev (or similar) are needed. (This list is hopefully ex-
haustive, but maybe not, it’s a bit hard to track down everything as it’s
not written down in U-Boot’s README.)
2.2.2. Getting the source code.

The U-Boot source code is typically acquired by using git:


2. THE U-BOOT BOOTLOADER 300

git␣clone␣--depth=1␣-b␣v2021.04␣https://source.denx.de/u-boot/u-boot.git

While version 2021.07 has been released, a quick test revealed that it does not
seem to work properly most of the time; the defconfig only seems to work on the
Z7, and even just disabling the network and USB in the config seems to upset and
break it. (This probably needs more investigation later on.)

2.2.3. Applying the configuration.

cd␣u-boot␣# cloned in the previous step


# depending on the board used (and depending on where
# the archive has been extracted):
cp␣../archive/u-boot/uboot_zybo_config␣.config
cp␣../archive/u-boot/uboot_zybo-z7_config␣.config

The only difference between these configuration files is the CONFIG DEFAULT DEVICE TREE
option. Using the wrong binary will cause line noise to show up in the console out-
put, however, it should still be able to boot.

2.2.4. Optional: modifying the configuration.

The following command should show a selection screen of which features to


enable or disable:

ARCH=arm␣CROSS_COMPILE=arm-none-eabi-␣make␣nconfig

This may be useful if you want to reenable eg. network or USB booting, which
is currently left out. Though, you’re on your own if you make these modifica-
tions, I have never tested these! Also, please do not set the CONFIG OF LIVE and
CONFIG FIT BEST MATCH options to y! For some reason, this will cause booting the
old Zybo to hang immediately, before any output is sent over the serial console.
(Maybe you can debug this issue and make a single binary for both boards, how-
ever, good luck with this.)

2.2.5. Compilation.

Compilation is started by running the make command:

ARCH=arm␣CROSS_COMPILE=arm-none-eabi-␣make␣-j`nproc`

This will compile on all available cores on the machine. Adjust the -j parameter
if needed, to eg. -j3 to compile on 3 cores. For me, on my Core 2 Duo3, this takes
only a few minutes.
If the compilation is successful, a file called u-boot.elf should have appeared.
This file will be needed later on.

3Yes.
2. THE U-BOOT BOOTLOADER 301

2.2.6. BOOT.BIN generation.

Several ways to generate a usable BOOT.BIN file exist, outlined below. These
range from more to less supported by Xilinx, but also more to less painful.

Using Xilinx Vivado and XSDK. Using these tools to generate a BOOT.BIN file
will not be explained here.

Using Xilinx Petalinux tools. If you have a Petalinux build environment, you can
use petalinux-package to generate the file:
petalinux-package␣--boot␣--force␣--fsbl␣path/to/zynq_fsbl.elf␣\
␣␣␣␣--fpga␣path/to/system_wrapper.bit␣--u-boot␣path/to/u-boot.elf

Refer to Xilinx’ and Digilent’s documentation on how to install a Petalinux build


environment, how to compile a Petalinux distribution, and when and how to invoke
the petalinux-package command.
Using the bare bootgen utility. Xilinx’ bootgen utility can be used in a stan-
dalone manner to generate BOOT.BIN files. The source code can be acquired here:
https://github.com/Xilinx/bootgen, compiling it is a simple matter of running make.
Then, run the following command (first one for 1st ed. Zybo, second for Zybo Z7):
path/to/bootgen␣-image␣./bootgen_zybo.bif␣-arch␣zynq␣-o␣./BOOT.bin␣-w
path/to/bootgen␣-image␣./bootgen_zybo-z7.bif␣-arch␣zynq␣-o␣./BOOT.bin␣-w

bootgen uses these BIF files as a declaration of what should end up in the
BOOT.BIN file. Here is an example:
EAGLE_boot_image_zybo:
{
␣␣␣␣[bootloader]␣./petalinux-binaries-zybo/zynq_fsbl.elf
␣␣␣␣./petalinux-binaries-zybo/system_wrapper.bit
␣␣␣␣./u-boot_zybo.elf
}

The supplied .bif files in the archive can be used, as well as the FSBL and
default bitstream files.

2.3. Compiling a custom U-Boot.

(1) First of all, go through steps §2.2.1 and §2.2.2. A newer version of U-Boot
can be used.
(2) Configure U-Boot to a usable default for the platform:
ARCH=arm␣CROSS_COMPILE=arm-none-eabi-␣make␣xilinx_zynq_virt_defconfig

Make sure that the default device tree used is the one for the Zybo or
Zybo-Z7, by opening the nconfig UI, going to Device Tree Control →
Default Device Tree for DT control.
Alternatively, to merely update a configuration file for a newer version,
first copy it over again as in step §2.2.3, then run one of the following
commands:
ARCH=arm␣CROSS_COMPILE=arm-none-eabi-␣make␣oldconfig
ARCH=arm␣CROSS_COMPILE=arm-none-eabi-␣make␣olddefconfig
3. THE LINUX KERNEL 302

These respectively ask what new settings should be configured to, and
use defaults for these new settings automatically.
This is not guaranteed to be error-free, make sure to thoroughly test
and tweak the configuration!
(3) Modify the configuration to your liking: see step §2.2.4. Precautions apply
doubly so here.
(4) Now start compiling the configured U-Boot as usual, see step §2.2.5 and
onward from §2.2.

3. The Linux kernel


3.1. Compiling the prepared Linux kernel for the Zybo hardware.

3.1.1. Prerequisites and dependencies.

The following programs and packages are required to compile the Linux kernel
successfully:
✓ A GCC and binutils cross-compilation toolchain, of the arm-none-eabi tar-
get (arm-linux-gnueabi or arm-linux-gnueabihf will also work). How-
ever, if a different cross-compilation toolchain is used, please do not forget
to change the CROSS COMPILE parameters used below.
✓ bc
✓ Common build tools, often packaged together by distros in a metapackage
called build-essential or base-devel or such.
More precisely, the tools make, bison, flex, rsync, libelf-dev (or
similar), ncurses-dev (or similar) are needed.

3.1.2. Getting the source code.

The latest Linux LTS (long-term support) tarball can be acquired from ker-
nel.org. Download and unpack it:
wget␣https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.63.tar.xz
tar␣xvf␣linux-5.10.63.tar.xz

We are using 5.10.63, it was the latest LTS at the time of writing. The config
file supplied in this archive is made for this kernel, refer to §3.3 if you want to use
a different Linux version, as you will most likely have to make some adaptations.
Additionally, you can check the signature and SHA256 hash of the tarball:
wget␣https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.63.tar.sign
unxz␣--keep␣linux-5.10.63.tar.xz
gpg␣--recv-keys␣647F28654894E3BD457199BE38DBBDC86092693E␣\
␣␣␣␣E27E5D8A3403A2EF66873BBCDEA66FF797772CDC␣\
␣␣␣␣AC2B29BD34A6AFDDB3F68F35E7BFC8EC95861109
gpg␣--verify␣linux-5.10.63.tar.sign
sha256sum␣-c␣--ignore-missing␣SHA256SUMS

3.1.3. Applying the configuration.

This is, just like with U-Boot, a simple matter of copying over a config file:
cd␣linux-5.10.63
3. THE LINUX KERNEL 303

cp␣../archive/linux/linux_zybos_config␣.config

3.1.4. Optional: modifying the configuration.

Again, similar to U-Boot, the make nconfig command will show a menu with
options to enable, disable or modify:
ARCH=arm␣CROSS_COMPILE=arm-none-eabi-␣make␣nconfig

PCI, UEFI, Wi-Fi, camera input, HDMI output, ... are currently disabled in
this kernel, and few USB drivers are available. With the above command, you can
reenable these. Though, you’re on your own if you make these modifications, I have
never tested these!

3.1.5. Compilation.

Once again similar to U-Boot, the make command is used to build the kernel:
KBUILD_BUILD_USER=eagle␣KBUILD_BUILD_HOST=esat␣ARCH=arm␣\
␣␣␣␣CROSS_COMPILE=arm-none-eabi-␣make␣-j`nproc`

This will compile on all available cores on your machine. Adjust the -j parameter
if needed, to eg. -j3 to compile on 3 cores. This takes a bit longer than the U-Boot
compilation process. It should not take longer than 30 minutes.
The KBUILD BUILD USER and KBUILD BUILD HOST parameters can be used to
change the the user@host display string in ther kernel build version info (cf. dmesg,
/proc/version). It defaults to the username and hostname of the system used to
compile the kernel, eg. bob@mylaptop, which may be unwanted.
If the compilation is successful, a file called arch/arm/boot/zImage should have
appeared. This file will be needed later on.

3.1.6. Installation.

Copy the following files to either the root of the microSD card, or to its /boot
folder (cf. U-Boot usage instructions):
✓ arch/arm/boot/zImage

Warning 30.3.1

Please do keep in mind that you still need a root filesystem on the microSD
at /rootfs.cpio.uboot or /boot/rootfs.cpio.uboot, and a U-Boot install
itself (BOOT.BIN and boot.scr), which this section does not cover.

3.1.7. Creating and installing the Device Tree files.

Device tree files are files needed by the kernel to know which components are
available on the hardware it is running on. To make a long story short, they basically
define a mapping between kernel drivers on the one side, and parameters (such as
MMIO addresses and clock speeds) used by the hardware on the other side.
As a base, we use the device tree from the Linux kernel or U-Boot source
tree (they are the same), and add a few things: default console parameters for
3. THE LINUX KERNEL 304

extra debugging, selecting the default output console (UART0), and some mem-
ory usage parameter tweaking. However, the most important addition here is the
reserved-memory clause: it is used to make sure the code running on core 0 will
never ever touch the memory area reserved for core 1 (running bare-metal ANC
code), from 0x1000’0000 to 0x17ff’ffff.
Some extra modifications had to be made, eg. making Linux aware of the EFUSE
and OCMC memory-mapped IO register regions, as well as making sure it keeps the
bus and PL (FPGA subsystem) configuration from what the FSBL and U-Boot set
it to, instead of reinitializing everything. It also needs a small edit to keep supplying
an extra clock to the PL. Why is this important? If this is not done, the PL will get
deinitialized or accesses to it will wait forever for a reply. This will not only halt the
core performing the access, but also all other devices attached to the bus, including
the other ARM core, and even the JTAG debug subsystem. This is bad. Therefore,
when using a new device tree from a newer kernel in a few years, please don’t forget
to readd these modifications, it will save you from week-long debugging sessions.
Device tree sources (.dts) files are compiled to their binary counterparts (.dtb)
using the dtc utility, which you need to have installed in order to compile the device
tree binaries. The Linux kernel source tree has a copy of its source code, and,
typically, when compiling Linux, it will compile its version, too, so you can use that
one if needed.
The archive comes with two device tree editions, one for each Zybo board version.
Copy the binary files to the SD card, at /zynq-zybo.dtb or /boot/zynq-zybo.dtb
for the old Zybo and /zynq-zybo-z7.dtb or /boot/zynq-zybo-z7.dtb for the Z7.
Alternatively, /system.dtb or /boot/system.dtb can be used as a fallback. U-Boot
will automatically select the right one for the board it is currently running on.

3.2. Compiling the prepared Linux kernel for a QEMU virtual ma-
chine.

You can go through the same steps as in the previous section, except this time
using linux virt config, to compile a kernel that will work on Zybo boards as
well as in a QEMU virtual machine. This can be useful for testing a root filesystem
without having to write it to an SD card and boot a physical Zybo. This is mainly
useful for testing Buildroot builds, slightly less so for testing code during the EAGLE
project itself. The resulting kernel image will also be a bit larger, as it has to include
PCI, generic terminal and peripheral drivers etc. in order to function inside QEMU.
The QEMU virtual machine can be started by using the following command:
qemu-system-arm␣-m␣512␣-M␣virt␣-kernel␣zImage-virt␣\
␣␣␣␣-initrd␣path/to/rootfs.cpio.uboot

This specifies a virtual machine as hardware model (-M virt)4, with half a
gigabyte of RAM (-m 512), using the virt kernel image (-kernel zImage-virt),
and the specified initial ramdisk (-initrd path/to/rootfs.cpio.uboot).
Optionally, you can also supply an SD card image, using the following command-
line option: -sd path/to/sd.img.

3.3. Compiling a custom Linux kernel.

4QEMU’s Zynq emulation seems to be broken currently.


4. THE BUILDROOT USERSPACE 305

(1) Perform steps §3.1.1 and §3.1.2 from section §3.1. A newer version of Linux
can be used.
(2) Configure Linux to a usable default for the platform:
ARCH=arm␣CROSS_COMPILE=arm-none-eabi-␣make␣multi_v7_defconfig

The multi v7 defconfig is a generic configuration for many ARMv7


devices (of which the Zynq, with its dualcore Cortex-A9, is one), however,
it contains a lot of unnecessary ballast that can be removed. For a list of
device drivers etc. that are needed to run Linux on the Zynq, please consult
my configuration file, together with the Xilinx Zynq technical reference
manual. Additionally, CPU frequency scaling and CPU power management
should be disabled, in order to keep CPU timings consistent.
Alternatively, if you are just updating the Linux kernel but want to
retain its configuration, you can simply copy the config file into the tree,
and run a configuration update command:
cp␣../linux_zybos_config
ARCH=arm␣CROSS_COMPILE=arm-none-eabi-␣make␣oldconfig␣# interactive
ARCH=arm␣CROSS_COMPILE=arm-none-eabi-␣make␣olddefconfig␣# automatic

This is not guaranteed to be error-free, make sure to thoroughly test


and tweak the configuration!
(3) Modify the configuration: see step §3.1.4. The defconfig used in step item 2
here adds support for a large number of boards, a number of options will
have to be disabled. Precautions apply doubly so here.

Warning 30.3.2

Some required drivers have slightly misleading names (eg. they’re


marked as ‘Cadence’). Consult the following webpage for which dri-
vers are needed for which devices: https://xilinx-wiki.atlassian.net/
wiki/spaces/A/pages/18841873/Linux+Drivers.

(4) Now start compiling the configured Linux kernel as usual, see step §3.1.5
and onward.

4. The Buildroot userspace


4.1. Introduction and usage notes.

As we were left with in the U-Boot section (and skipped over in the Linux kernel
section), the U-Boot bootloader had managed to load and start the Linux kernel.
The latter then proceeds with starting up, initializing device drivers etc., which will
not be explained in detail.
When all driver initialization has finished, the Linux kernel starts the first
userspace process: /sbin/init. This process is responsible for starting all other
userspace programs, and should keep running for as long as the machine keeps run-
ning. It loads the information of which filesystems to mount from /etc/fstab,
will (try to) bring up the network, start a login prompt, and perform other tasks
specified in /etc/inittab.
One of these tasks is “managing services”: starting and stopping programs,
as specified by scripts placed in /etc/init.d. In here we find two custom files:
4. THE BUILDROOT USERSPACE 306

S00readonlyRoot, which remounts the root filesystem as readonly, and S90customBoot,


which starts all the EAGLE-specific startup tasks, such as starting the second CPU,
and running the autostart script. The final custom file used, is /etc/default/dropbear,
which makes Dropbear, the SSH server, use a persistent location on the microSD
card to store its SSH host keys. If this would not have been done, these host keys
would change every time on a reboot. Additionally, there are some ‘standard’ ser-
vices that are also started on boot, such as the DHCP client, and haveged, which
seeds the operating system’s random number generator. The Buildroot and Busy-
box default DHCP client, udhcpc, has been replaced by dhcpcd, to speed up the
boot process. This also needs a custom, but fairly default, config file.
To compile C code for this system, a cross-compiler is needed. As the system
uses glibc, most Linux distributions already have a binary package for such a cross-
compiler (often with arm-linux-gnu or arm-linux-gnueabihf in the name). If you
however have none, please go through steps §4.2.1 to §4.2.3, and then, instead of
simply running make in step §4.2.6, run make toolchain instead. After some time
(up to a few hours), a GCC cross-compiler should have appeared in the output/host
folder, eg. output/host/arm-linux-gnueabihf-gcc.

4.2. Compiling the prepared Buildroot.

4.2.1. Prerequisites and dependencies.

✓ Common build tools, often packaged together by distros in a metapackage


called build-essential or base-devel or such.
More precisely, the tools make, bison, flex, rsync, libelf-dev (or
similar), ncurses-dev (or similar) are needed.
Buildroot can discover, download, compile and use dependencies and host tools
as it needs (eg. libtool, CMake, uboot-tools), however, having these installed already
will speed up the build process.

4.2.2. Getting the source code.

Download the latest Buildroot LTS (long-term support) tarball and unpack it:
wget␣https://buildroot.org/downloads/buildroot-2021.02.4.tar.gz
tar␣xvf␣buildroot-2021.02.4.tar.gz

We are using 2021.02.4, it is the latest LTS at the time for writing. The config
file supplied in this archive is made for this distribution, refer to the next chapter if
you want to use a different Buildroot version, as you will most likely have to make
some adaptations.
Additionally, you can check the SHA256 hash of the tarball:
sha256sum␣-c␣--ignore-missing␣SHA256SUMS

4.2.3. Applying the configuration.

cd␣buildroot-2021.02.4
cp␣../archive/buildroot/buildroot_only_config␣.config
4. THE BUILDROOT USERSPACE 307

Two configuration files are available. buildroot only config will only build a
root filesystem, without also building a kernel. If instead buildroot withkernel config
is used, a Linux kernel will be compiled at once as well, which means you do not
have to go through most of the steps from §3.1 manually.

Warning 30.4.1

The supplied Buildroot configuration uses a relative path to find the Linux
kernel and Busybox configurations. Depending on how you may have unpacked
the source tarballs and this archive, a different path may have to be used.
The path used in the kernel configuration file is ../archive/linux/linux zybos config,
the path for Busybox is ../archive/buildroot/busybox config, these paths
are designed and intended to be used with a directory structure as shown in
Fig. 1. Additionally, if you want the kernel to work in a QEMU virtual machine
as well, you need to use linux virt config instead.

4.2.4. Optional: modifying the configuration.

Here you can enable networking utilities (such as ip, ifconfig), and the Python
langauge and its libraries (eg. pyzmq). Do note that, if you have already compiled
a root filesystem, you first need to issue a make clean command before starting
a build again. The supplied configuration files already has these tools enabled.
You can also recompile a single pacakge using make <package>-rebuild, eg. make
busybox-rebuild to recompile only Busybox (which provides the shell, a number of
common command-line utilities, and many other things). See make help for more
available commands.
make␣nconfig

To modify the Busybox configuration (to eg. enable or disable some utilties pro-
vided by Busybox), after having compiled Buildroot at least once, do the following:
cd␣output/build/busybox-2021.02.4/
make␣menuconfig

Warning 30.4.2

Do not forget to copy the .config file in this directory back into
../archive/buildroot/busybox config! If this hasn’t happened, Buildroot
will overwrite the Busybox .config with the one in the archive directory, un-
doing all the changes!

4.2.5. Optional: modifying the root filesystem.

It is also possible to modify files in the root filesystem: at the end of the build
process, the supplied configuration file will take all files in the overlay directory
and overwrite any existing files in the root filesystem with these.

Warning 30.4.3
4. THE BUILDROOT USERSPACE 308

./
|---␣buildroot-2021.02.4/
|␣␣␣␣|---␣.config
|␣␣␣␣|---␣Makefile
|␣␣␣␣`---␣...
|---␣linux-5.10.61/
|␣␣␣␣|---␣.config
|␣␣␣␣|---␣Makefile
|␣␣␣␣`---␣...
|---␣u-boot/
|␣␣␣␣|---␣.config
|␣␣␣␣|---␣Makefile
|␣␣␣␣`---␣...
`---␣archive/
␣␣␣␣␣|---␣u-boot/
␣␣␣␣␣|---␣linux/
␣␣␣␣␣|␣␣␣␣|---␣linux_zybos_config
␣␣␣␣␣|␣␣␣␣`---␣...
␣␣␣␣␣`---␣buildroot/
␣␣␣␣␣␣␣␣␣␣|---␣overlay/
␣␣␣␣␣␣␣␣␣␣|␣␣␣␣|---␣inittab
␣␣␣␣␣␣␣␣␣␣|␣␣␣␣`---␣...
␣␣␣␣␣␣␣␣␣␣|---␣00-README.txt
␣␣␣␣␣␣␣␣␣␣|---␣buildroot_only_config
␣␣␣␣␣␣␣␣␣␣|---␣busybox_config
␣␣␣␣␣␣␣␣␣␣`---␣...

Figure 1. example directory structure

The supplied Buildroot configuration uses a relative path to find the overlay
directory. Depending on how you may have unpacked the source tarballs and
this archive, a different path may have to be used.
The path used in the configuration file is ../archive/buildroot/overlay,
and is designed and intended to be used with a directory structure as shown in
Fig. 1.
Additionally, if you use a Buildroot configuration that also compiles a kernel,
you should ensure the path to the kernel configuration file is also correct.
4.2.6. Compilation.

KBUILD_BUILD_USER=eagle␣KBUILD_BUILD_HOST=esat␣make␣-j`nproc`

This will compile on all available cores on your machine. Adjust the -j parameter
if needed, to eg. -j3 to compile on 3 cores. Because Buildroot also needs to compile
its own toolchain, this compilation process can take several hours.
For the meaning of the KBUILD BUILD USER and KBUILD BUILD HOST parame-
ters, see §3.1.5. These parameters only apply when also compiling a kernel using
Buildroot.
If the compilation is successful, a file called rootfs.cpio.uboot should have
appeared in the output/images folder. This is a compressed root filesystem im-
age, with extra information to make it usable by U-Boot. If you have opted to use
buildroot withkernel config, the compiled Linux kernel image will also be found
4. THE BUILDROOT USERSPACE 309

in this directory, with name zImage.

4.2.7. Installation.

Copy the output/images/rootfs.cpio.uboot file to either the root directory


of the microSD card or its /boot directory. The same should be done with the
zImage file, if the Linux kernel was not compiled and installed separately.

4.3. Compiling a Buildroot toolchain without compiling the entire


userspace.

This can be useful when a toolchain is required to be used as part of the project,
while none are available (eg. no ESAT computer can be used, and none are available
or easily installed on the user’s computer).
In order to make such a toolchain, follow the steps in §4.2, but instead of issuing
a regular make command, use make toolchain instead. The toolchain can then be
found in the output/host folder, as explained in §4.1.

4.4. Compiling a custom Buildroot userspace.

(1) 1. Perform steps §4.2.1 and §4.2.2. A newer version of Buildroot can be
used.
(2) Configure Buildroot to a usable default for the platform:
make␣zynq_zed_defconfig

The zynq zed defconfig is originally meant for the ZedBoard, not the
Zybo or Zybo-Z7, but it is close enough to use as a starting point.
Alternatively, if you are just updating Buildroot but want to retain its
configuration, you can simply copy the config file into the tree, and run a
configuration update command:
cp␣../buildroot_only_config
make␣oldconfig␣# interactive
make␣olddefconfig␣# automatic

This is not guaranteed to be error-free, make sure to thoroughly test


and tweak the configuration!
(3) Modify the configuration to your liking: see step §4.2.4 and §4.2.5. You
will have to change the libc used from uClibc to glibc (or musl, but glibc
was chosen here as many distros provide cross toolchains for only glibc
ARM as binary packages, at the time of writing). Additionally, you have
to enable all the command-line tools and daemons present on the system,
such as dropbear, ip, ifconfig, and Python and its zmq library (the latter
is listed as pyzmq in the configuration UI). Set the kernel headers version
to 4.4, which will be the minimum required Linux kernel version to run.
Secondly, you most likely want to modify Busybox as well, by eg. dis-
abling its built-in udhcpc, enabling ifup/ifdown to use external DHCP
providers, and then enabling dhcpcd in Buildroot. Together with the
/etc/dhcpcd.conf file provided in the overlay directory, this will speed
up the boot process quite a bit, as udhcpc cannot run in the background,
while dhcpcd can.
5. INSTALLATION AND FINAL NOTES 310

Finally, in the overlay directory itself, you most likely want to add
an /etc/init.d/S90customBoot script to perform the necessary steps for
starting up the second CPU core, and all the EAGLE Linux code. /etc/default/dropbear
is responsible for making Dropbear use persistent microSD storage for stor-
ing its host keys, which prevents it from regenerating these keys on each
boot.
(4) Now start compiling your configured Buildroot system as usual, see step
§4.2.6 and onward from seciton §4.2.
Do note that, if you have already compiled a root filesystem, you first
need to issue a make clean command before starting a build again. (The
supplied configuration files I have made already have these tools enabled.)
You can also recompile a single pacakge using make <package>-rebuild,
eg. make linux-rebuild to recompile only the kernel. See make help for
more available commands.

5. Installation and final notes


As a recap, here is a list of files that need to be present on the microSD card to
boot a complete Linux system:
✓ BOOT.BIN
✓ Generated from §2.2.6
✓ Contains zynq-fsbl.elf, system-wrapper.bit
✓ Contains u-boot.elf from §2.2
✓ Archive: precompiled BOOT zybo.bin or BOOT zybo-z7.bin, depend-
ing on the Zybo model.
✓ boot.scr
✓ Compiled from boot.scr.txt from §2.1
✓ Archive: archive/u-boot/boot.scr
✓ rootfs.cpio.uboot
✓ Created from §4.2 output/images/rootfs.cpio.uboot
✓ Archive: archive/buildroot/rootfs.cpio.uboot
✓ zImage
✓ Created from §3.1 arch/arm/boot/zImage
✓ Or, created from §4.2.3 output/images/zImage
✓ Archive: archive/linux/zImage
✓ QEMU VM: §3.2, archive/linux/zImage-virt
✓ uImage
✓ Older kernel used in the years before this guide was written. May
be useful when zImage or its device trees have bugs, but its use is
otherwise discouraged.
✓ Archive: archive/linux/uImage.old-4.4
✓ zynq-zybo.dtb
✓ Compiled from archive/linux/dt/zynq-zybo.dts, which is based on
the device tree from U-Boot (arch/arm/dts/zynq-zybo.dtb) or Linux
(arch/arm/boot/dts/zynq-zybo.dtb).
✓ Archive: archive/linux/dt/zynq-zybo.dtb
✓ zynq-zybo-z7.dtb
✓ Compiled from archive/linux/dt/zynq-zybo-z7.dts, which is based
on the device tree from U-Boot (arch/arm/dts/zynq-zybo-z7.dtb)
or Linux (arch/arm/boot/dts/zynq-zybo-z7.dtb).
5. INSTALLATION AND FINAL NOTES 311

✓ Archive: archive/linux/dt/zynq-zybo-z7.dtb
If it all worked out, the output on the serial port should resemble something like
this:
U-Boot:
␣0
switch␣to␣partitions␣#0, OK
mmc0␣is␣current␣device
Scanning␣mmc␣0:1...
Found␣U-Boot␣script␣/boot.scr
3071␣bytes␣read␣in␣19␣ms␣(157.2␣KiB/s)
## Executing script at 03000000
Trying␣rootfs␣from␣/rootfs.cpio.uboot
15299607␣bytes␣read␣in␣845␣ms␣(17.3␣MiB/s)
Loaded␣rootfs␣from␣/rootfs.cpio.uboot
Running␣on␣Zybo␣1st␣ed
Trying␣DTB␣at␣/zynq-zybo.dtb
11568␣bytes␣read␣in␣20␣ms␣(564.5␣KiB/s)
Loaded␣device␣tree␣from␣/zynq-zybo.dtb
Trying␣kernel␣/uImage
Failed␣to␣load␣'/uImage'
Trying␣kernel␣/zImage
4693808␣bytes␣read␣in␣274␣ms␣(16.3␣MiB/s)
Found␣kernel␣at␣/zImage␣.␣Booting!
Kernel␣image␣@␣0x4000000␣[␣0x000000␣-␣0x479f30␣]
## Loading init Ramdisk from Legacy Image at 08000000 ...
␣␣␣Image␣Name:
␣␣␣Image␣Type:␣␣␣ARM␣Linux␣RAMDisk␣Image␣(uncompressed)
␣␣␣Data␣Size:␣␣␣␣15299543␣Bytes␣=␣14.6␣MiB
␣␣␣Load␣Address:␣00000000
␣␣␣Entry␣Point:␣␣00000000
␣␣␣Verifying␣Checksum␣...␣OK
## Flattened Device Tree blob at 07ff0000
␣␣␣Booting␣using␣the␣fdt␣blob␣at␣0x7ff0000
␣␣␣Loading␣Ramdisk␣to␣1dcb3000,␣end␣1eb4a3d7␣...␣OK
␣␣␣Loading␣Device␣Tree␣to␣1dcad000,␣end␣1dcb2d2f␣...␣OK

Starting␣kernel␣...

Linux kernel:
[0.000000]␣Booting␣Linux␣on␣physical␣CPU␣0x0
[0.000000]␣Linux␣version␣5.10.63␣(eagle@esat)␣(arm-none-eabi-gcc
␣␣␣␣␣␣␣␣(GCC)␣9.3.0,␣GNU␣ld␣(GNU␣Binutils)␣2.32)␣#1 SMP
[0.000000]␣CPU:␣ARMv7␣Processor␣[413fc090]␣revision␣0
␣␣␣␣␣␣␣␣(ARMv7),␣cr=18c5387d
[0.000000]␣CPU:␣PIPT␣/␣VIPT␣nonaliasing␣data␣cache,
␣␣␣␣␣␣␣␣VIPT␣aliasing␣instruction␣cache
[0.000000]␣OF:␣fdt:␣Machine␣model:␣Digilent␣Zybo␣board
[0.000000]␣Memory␣policy:␣Data␣cache␣writealloc
[0.000000]␣cma:␣Reserved␣32␣MiB␣at␣0x1b800000
[0.000000]␣Zone␣ranges:
[0.000000]␣␣␣Normal␣␣␣[mem␣0x00000000-0x1fffffff]
[0.000000]␣␣␣HighMem␣␣empty
[0.000000]␣Movable␣zone␣start␣for␣each␣node
[0.000000]␣Early␣memory␣node␣ranges
[0.000000]␣␣␣node␣␣␣0:␣[mem␣0x00000000-0x0fffffff]
[0.000000]␣␣␣node␣␣␣0:␣[mem␣0x10000000-0x17ffffff]
[0.000000]␣␣␣node␣␣␣0:␣[mem␣0x18000000-0x1fffffff]
5. INSTALLATION AND FINAL NOTES 312

[0.000000]␣Initmem␣setup␣node␣0␣[mem␣0x00000000-0x1fffffff]
[0.000000]␣percpu:␣Embedded␣15␣pages/cpu␣s32140␣r8192
[0.000000]␣Built␣1␣zonelists,␣mobility␣grouping␣on.
␣␣␣␣␣␣␣␣Total␣pages:␣130048
[0.000000]␣Kernel␣command␣line:␣console=ttyPS0,115200
␣␣␣␣earlyprintk␣uio_pdrv_genirq.of_id=generic-uio␣cma=32M
␣␣␣␣coherent_pool=16M␣maxcpus=1
[0.000000]␣Dentry␣cache␣hash␣table␣entries:␣65536
[0.000000]␣Inode-cache␣hash␣table␣entries:␣32768
[0.000000]␣mem␣auto-init:␣stack:off,␣heap␣alloc:off,
␣␣␣␣␣␣␣␣heap␣free:off
...␣(snip)
[2.497623]␣ThumbEE␣CPU␣extension␣supported.
[2.501933]␣Registering␣SWP/SWPB␣emulation␣handler
[2.512822]␣Loading␣compiled-in␣X.509␣certificates
[2.527813]␣Freeing␣unused␣kernel␣memory:␣1024K
[2.534430]␣Run␣/init␣as␣init␣process
[2.541916]␣mmc0:␣new␣high␣speed␣SDHC␣card␣at␣address␣59b4
[2.554148]␣mmcblk0:␣mmc0:59b4␣USD00␣14.7␣GiB
[2.566417]␣␣mmcblk0:␣p1

Userspace (with some kernel log messages):


Remounting␣/␣readonly...
Saving␣random␣seed:␣SKIP␣(read-only␣file␣system␣detected)
Starting␣haveged:␣OK
haveged:␣command␣socket␣is␣listening␣at␣fd␣3
haveged:␣haveged␣starting␣up
Starting␣network:
[2.914036]␣macb␣e000b000.ethernet␣eth0:
␣␣␣␣␣␣␣␣configuring␣for␣phy/rgmii-id␣link␣mode
dhcpcd-9.4.0␣starting
Starting␣dhcpcd...
[2.981582]␣random:␣dhcpcd:␣uninitialized␣urandom
␣␣␣␣␣␣␣␣read␣(120␣bytes␣read)
dhcpcd-9.4.0␣starting
Bad␣system␣call
Starting␣dropbear␣sshd:␣OK
Starting␣EAGLE␣control␣systems␣on␣CPU1...
[5.640258]␣random:␣crng␣init␣done
Running␣/media/autostart.sh␣...

Welcome␣to␣EAGLE␣Zybo
eagle␣login:␣root
Password:

login[153]:␣pam_unix(login:session):␣session␣opened␣for
␣␣␣␣␣␣␣␣user␣root(uid=0)␣by␣LOGIN(uid=0)
login[153]:␣pam_lastlog(login:session):␣file
␣␣␣␣␣␣␣␣/var/log/lastlog␣created
login[153]:␣root␣login␣on␣'ttyPS0'
# hostname
eagle
# echo yay
yay
#

Enjoy.
CHAPTER 31

Advanced Tutorials

1. Integrated Logic Analyzer (ILA)


Tutorial 31.1.1 (Recording FPGA signals on hardware) — Vivado also comes
with an Integrated Logic Analyzer (ILA), which can record digital signals internal
to the FPGA. Note that it cannot record signals for an unlimited time, first, a
trigger (event) needs to be specified at which signal recording will start, which then
continues for a fixed amount of time.
As an example, we’ll again use the code from Tut. 19.3.1. The general flow is
as follows:

(1) In the synthesis schematic (cf. Tut. 19.2.4), mark the signals you want to
debug as debuggable by right-clicking them and selecting Mark Debug. Do
this for the four LED signals, and the button 0 signal. See Fig. 1.
(2) Click Flow navigator → Synthesis → Open Synthesized Design →
Set Up Debug.
(3) In the popup window, click Next and then select which signals are meant
as data to be recorded, which will be meant for triggers, and which are
both. It is best to specialise the purpose of each signal, as there is a fixed
amount of memory available for all data signals. More data signals means
less recording time. For this example, mark the LED signals as data, and
the button as both data and trigger. Click Next when ready.
(4) In the next screen, you can select the amount of memory available for
recording signals. Note that this will be included in your bitstream, and
thus increase delay in paths in your Verilog code, possibly making it unim-
plementable. Leave the other settings as-is. When ready, click Next, and
click Finish in the next screen.
(5) Generate a new bitstream.
(6) Open the Hardware Manager and open the Zybo target, as described in
Tut. 19.2.5.
(7) Click Program Device. The Debug probes file should be filled in this
time. Click the Program button. This should bring up the following view:
Fig. 2.
(8) In the Trigger Setup window, click the blue + to add a trigger, and select
the signal you want to trigger on, as shown in Fig. 3a.
(9) Now the trigger settings can be changed, eg. trigger on high or low levels,
or rising or falling edges. For this example, select btn IBUF to trigger on
R, a rising edge. See Fig. 3b.
(10) Arm the trigger by clicking the blue triangular “play” button. (Arming
here means: starting the system and waiting for the trigger to hit to start
recording.)
313
2. THE AXI VERIFICATION IP 314

(b) All signals to use as debug in this


(a) Marking a signal as debug example

Figure 1

Figure 2. Vivado Integrated Logic Analyzer

(a) Selecting a signal as trigger source (b) Modifying trigger settings

Figure 3

(11) Press the button on the Zybo to increase the counter. Vivado should now
start capturing the LED signal, which should appear in the Waveform win-
dow, as shown in Fig. 3b. The red vertical line indicates when the trigger
fired, which should coincede with the moment at which the LED value
changed from 0 to 1.

2. The AXI Verification IP


The AXI Verification IP — or AXI VIP for short — can be used to test AXI
peripherals in Vivado without the need of writing a software driver and deploying
everything on a physical Zybo. However, these AXI VIP testbenches are required
to be written in SystemVerilog instead of regular Verilog, and thus add some extra
complexity.
2. THE AXI VERIFICATION IP 315

Figure 4. Captured signal in ILA Waveform

Xilinx has published a tutorial on how to use the AXI VIP to debug AXI pe-
ripherals here, and can mostly be followed verbatim. However, a few things need to
be taken into account:
✓ Do not use the block and connection automation in the block design. In-
stead, make all connections by hand, directly from the VIP to the periph-
eral, with no AXI interconnect etc. used.
✓ Do not forget to assign an address to the peripheral, as this needs to be
done manually, when block and connection automation are not used.
✓ The SystemVerilog test bench has the name of the block diagram in 4
different places, and the AXI VIP block ID in 3 different places. Make sure
to keep all of these consistent with the names and numbering used in the
block diagram itself, or Vivado will yell at you when trying to start the
simulation, and an error will be showed in the Tcl console for one name at
a time. The full list is:
✓ Second import statement (BD name + VIP ID)
✓ BD module import statement (BD name)
✓ Agent declaration (BD name + VIP ID), right under the BD module
import in Xilinx’ code example.
✓ Agent instantiation (BD name + VIP ID), the first line in the initial
begin block.
✓ Additionally, make sure the names of the ports used in the BD module
import are the same as the names given to the ports in the block
diagram itself.
✓ Similarly, make sure to keep the address of the peripheral in sync with
the one used in the SystemVerilog test bench
Fig. 5 has an overview of all things that need to be kept in sync.
✓ While inspecting the output waves of the testbench, the symbols from the
top-level testbench are not very useful. Importing extra modules into the
wave window (Scope small-window, right-click a module and then Add to
Wave Window ) can be very useful to gain more insight. Two very use-
ful ones to add are the top-level Verilog module used by the AXI periph-
eral (and not the AXI peripheral module, so eg. DUT → design 1 i →
myipname 0 → inst → myipname v1 S00 AXI inst → myverilogmodule),
and the AXI VIP top-level module itself (eg. axi vip 0). The latter cleanly
3. RUNNING CRYPT ON THE ZYBO IN BAREMETAL 316

shows when reads and writes happen, and to which address and with which
value.
✓ Similarly, using groups and having different signal colors for these groups
can help making sense of the multitude of waves present. These can be
selected and changed by right-clicking a signal name in the wave window
itself, and clicking Signal Color or New Group.
✓ A simulation can be restarted with these options (extra module signals to
display, and display options) by pressing the “| →” and Start buttons in
the toolbar at the top, and not inside the wave window.

Figure 5. An overview of all the things in the SystemVerilog test-


bench that need to be kept in sync with the names used in the block
diagram and address editor.

3. Running CRYPT on the Zybo in Baremetal


Tutorial 31.3.1 (Importing the CRYPT code into a baremetal Zybo project) —
After following Tut. 20.2.2, a few extra settings need to be applied:
(1) Add a link to the SW/ folder as usual.
(2) Delete the helloworld.c file in the src/ folder, as its main() function
conflicts with the one from main.c. Make sure not to remove the entire
src/ folder! We still need its other files. See Fig. 6a
(3) Set the active build type to Hardware, by right-clicking Hardware in the
Assistant on the lower left, and clicking Set Active, cf. Tut. 20.2.3.
(4) Open the project properties by right-clicing the project in the Explorer side-
bar, and click Properties (Fig. 6b). Navigate to C/C++ Build → Settings
and then ARM v7 gcc compiler → Directories. Change the Configurations
dropdown to All configurations. Add the src/ folder as an include path,
in accordance with Fig. 7a.
(5) Optionally (but highly recommended), you can also select how the FPGA
should be used, by adding a HW xxx symbol in ARM v7 gcc compiler →
Symbols. Fig. 7b has an example. See Tut. 31.4.3 for more info on how
these symbols should be used.
3. RUNNING CRYPT ON THE ZYBO IN BAREMETAL 317

(a) (b)

Figure 6. Configuring the CRYPT project for Zybo baremetal (1)

(a) (b)

Figure 7. Configuring the CRYPT project for Zybo baremetal (2)

Warning 31.3.1

Make sure to use the right FPGA interface (and HW xxx symbol) for the right
bitstream! A mismatch may cause hangs or other types of undefined behavior,
and will most likely break things.

Tutorial 31.3.2 (Running and debugging the CRYPT bare-metal C code) — This
is covered in Tut. 20.3.2. Note that, for the CRYPT project, if you have not
4. CRYPT MAKEFILE BUILD SYSTEM 318

implemented the accelerator on the FPGA, the C code will wait for a ‘done’ signal
indefinitely and get stuck, because the FPGA will never send one.

4. CRYPT Makefile build system


As an alternative to the Vitis-based approach, it is also possible to compile the
code of the CRYPT project using a Makefile, and use a code editor of your choice.
This is slightly more complicated, though.

Tip 31.4.1

In C especially, many things that should be errors often aren’t, such as non-
void-returning functions that don’t return in all cases, or using a function that
is not declared. Many important warning messages are disabled by default,
but can be enabled by passing -Wall -Weverything to the compiler. Enabling
these options will catch many errors that would otherwise have taken a longer
time to find! Additionally, some warnings can be turned into errors, by using
-Werror=<messagename>, for example -Werror=implicit-function-declaration
-Werror=return-type -Werror=maybe-uninitialized.

Tutorial 31.4.1 (Compiling CRYPT code using the Makefile) — Compiling the
code can be done simply by running the following command:
ZYBOCC=arm-linux-gnueabi-gcc␣make

The ZYBOCC parameter tells the Makefile which cross-compiler to use for com-
piling code for Linux on the Zybo. More on this can be found in Tut. 31.4.2.
The build process automatically outputs executables both for your own com-
puter, and for Linux on the Zybo. The eaglecryptsw-host file is the former, while
all files in the zybo/ folder are for the latter, using different FPGA configurations
(see Tut. 31.4.3).
Compiling for baremetal Zybo using a Makefile is not really supported, but see
tipbox 4.
Removing all files created by the compilation process can be done using the
following command:
make␣clean

Tutorial 31.4.2 (Configuring and tweaking the Makefile-based build) — It is


possible to influence the build. Most importantly, depending on how your own
computer is set up, you may need to explicitely tell the Makefile which cross-
compilation toolchain to use to compile code for Linux on the Zybo.
The toolchain is specified by specifying the ZYBOCC variable when invoking the
make command. This is, if you have a system-wide installation of a glibc ARM
toolchain1, just the name of the toolchain, with -gcc appended. For example:
ZYBOCC=arm-linux-gnueabihf-gcc␣make

1Most Linux distros have one in their package repositories, often with arm-linux-gnueabihf
or arm-linux-gnueabi in the name.
4. CRYPT MAKEFILE BUILD SYSTEM 319

If you do not have a toolchain available, it is possible to compile one using


Buildroot, as explained in §4.3 of the Linux system installation instructions.2 Then
set ZYBOCC to the full path of the cross-compiler. Another example:
ZYBOCC=/home/namehere/buildroot-2021.02.3/output/host/arm-linux-gnueabihf-gcc␣make

If the ZYBOCC variable is not specified, the Makefile will try to find a suit-
able system-wide installation of a cross-compiler. If this fails, however, manually
specifying it is required. On an ESAT computer, it should work out of the box.
Other options are available as well, such as CFLAGS (and target-specific ones,
HOSTCFLAGS and ZYBOCFLAGS). These are standard GCC flags, for which we refer
you to the GCC documentation.
All these options can be stored in a file called config.mk, so you do not have
to specify them each time you invoke the make command. Thus, if you put the
following in config.mk:
ZYBOCC=/home/namehere/buildroot-2021.02.3/output/host/arm-linux-gnueabihf-gcc

Every subsequent invocation of make for the CRYPT project will use this com-
piler for Zybo Linux code, until config.mk is edited again.
Tutorial 31.4.3 (Selecting the FPGA interface target) — There are different HW xxx
preprocessor symbols used to specify which FPGA interface should be used, and
which capabilities it has. You typically progress from one to another when moving
to a different implementation phase. The full list is:
✓ None: specifying no symbol makes the code fall back to the software Keccak
and KetjeSr implementations. This is the only available option when run-
ning code not on a Zybo. This is mainly useful when working on other parts
of the CRYPT code, such as the protocol (and kdf ae()) implementation.
The respective binary is called eaglecryptsw.
✓ HW 20 ROUNDS: The FPGA can be used, and is able to perform only a fixed
number of Keccak rounds, 20. This is probably the first setting you want
to enable when testing your FPGA implementation on real hardware. The
respective binary is called eaglecrypthw hash.
✓ HW ARBITRARY ROUNDS is the same as the above, except it can do a variable
number of Keccak rounds, which is required for KetjeSr acceleration.
The respective binary is called eaglecrypthw aead hash.
✓ HW CUSTOM INTERFACE: this is for an FPGA implementation with a custom
interface, for when you eg. want to accelerate more operations than just
bare Keccak rounds. You will need to modify the code in the inc/ direc-
tory to implement the CPU side of this. This option is disabled by default
in the Makefile, and needs to be uncommented manually. The respective
binary is called eaglecrypthw kdf aead hash.
Compiling code for a specific target using a Makefile is done by compiling only
the respective binary, eg.
make␣zybo/eaglecrypthw_hash

For compiling code for the ţarget using the HW 20 ROUNDS preprocessor symbol.
When using Vitis, selecting the target is a simple matter of specifying the pre-
processor symbol in the Vitis gcc compiler options.
2You do not need to install a complete Linux system yourself.
5. DEBUGGING CRYPT ON THE ZYBO IN LINUX USING GDB 320

Warning 31.4.1

Make sure to use the right FPGA interface for the right bitstream! A mismatch
may cause hangs or other types of undefined behavior, and will most likely
break things.
Additionally, the ZYBO LINUX symbol will make the code assume it is running
on Linux on a Zybo. If this symbol is not present, the code will think it is running
either on your computer locally, or baremetal on the Zybo.

Warning 31.4.2

Make sure to use the right FPGA interface for the right bitstream! A mismatch
may cause hangs or other types of undefined behavior, and will most likely
break things.
Compiling code for a specific target using a Makefile is done by compiling
only the respective binary, eg.
make␣zybo/eaglecrypthw_hash

For compiling code for the ţarget using the HW 20 ROUNDS preprocessor symbol.
When using Vitis, selecting the target is a simple matter of specifying the
preprocessor symbol in the Vitis gcc compiler options.
Additionally, the ZYBO LINUX symbol will make the code assume it is run-
ning on Linux on a Zybo. If this symbol is not present, the code will think it is
running either on your computer locally, or baremetal on the Zybo.

Tip 31.4.2

A note on compiling for baremetal Zybo code:


It is possible to use the Vitis-generated Makefiles to compile C code for
baremetal Zybo. As this depends on a number of varying things (workspace
location, extra source code files, cross-compiler used, ...), this will not be ex-
plained in detail, but you are encouraged to discover the workings yourself.
Similarly, Vitis does not need to be used for running C code on the Zybo.
Programs such as OpenOCD can be used as standalone programming tools,
however, these also depends on a number of things (ELF executable and FPGA
bitstream paths, which USB device to use, configuration files specific to the
programming tool used, etc.), so this also cannot be sufficiently described in a
single section. Looking up how to use OpenOCD etc. with a Zynq can lead to
interesting results, though.

5. Debugging CRYPT on the Zybo in Linux using gdb


If you want to debug code on the Zybo running Linux, simply running gdb will
not work, as the program is not available on the Zybo. However, you can still use
gdbserver, when the Zybo has an ethernet connection.
Tutorial 31.5.1 (Using gdbserver on the Zybo) —
(1) Prerequisite: have a gdb debugger present on your computer, of the same
toolchain as the GCC compiler used, eg. arm-linux-gnueabihf-gdb when
5. DEBUGGING CRYPT ON THE ZYBO IN LINUX USING GDB 321

using arm-linux-gnueabihf-gcc. On an ESAT machine, you can use the


following file as gdb:
/esat/micas-data/software/xilinx_vitis_2021.1/Vitis/2021.1/gnu/
␣␣␣␣aarch32/lin/gcc-arm-linux-gnueabi/bin/arm-linux-gnueabihf-gdb

This one, however, has the TUI disabled, and it is thus advised to use your
own.
(2) Plug a cable into the ethernet port of the Zybo, start it up, and log in using
the serial console (screen).
(3) Get the IP address of the Zybo, by reading the output of the following
command:
ip␣a␣show␣dev␣eth0

The output should be of the following form:


2:␣eth0:␣<BROADCAST,MULTICAST,UP,LOWER_UP>␣mtu␣1500␣qdisc␣<snip>
␣␣␣␣link/ether␣42:ae:42:ed:48:cd␣brd␣ff:ff:ff:ff:ff:ff
␣␣␣␣inet␣192.168.0.157/24␣brd␣192.168.0.255␣scope␣global␣<snip>
␣␣␣␣␣␣␣valid_lft␣3599sec␣preferred_lft␣3149sec

In this case, the IP address is 192.168.0.157.


(4) Start the gdbserver: execute the following command on the Zybo:
gdbserver␣0.0.0.0:2345␣./program/i/want/to/run

(5) Start gdb on your computer, using the IP address of the Zybo:
arm-linux-gnueabihf-gdb␣-ex␣'target␣remote␣192.168.0.157:2345'␣\
␣␣␣␣path/to/program/file/on/zybo

This should show some text that starts with:


GNU␣gdb␣(GDB)␣10.2
Copyright␣(C)␣2021␣Free␣Software␣Foundation,␣Inc.
License␣GPLv3+:␣GNU␣GPL␣version␣3
or␣later␣<http://gnu.org/licenses/gpl.html>

And ends with:


0xb6f95980␣in␣_start␣()␣from␣target:/lib/ld-linux-armhf.so.3
(gdb)

(6) From here on, you can start debugging your program. However, as gdb is
mainly terminal-focussed, below we give you some tips to get started and
have it nicer to use:
✓ Enable TUI mode: layout src or lay src. This shows you a graphical-
ish overview of the source code.
✓ Run or resume code: continue or c. This starts the code until a
breakpoint is hit, or until a crash happens.
✓ Run a single line of code: step or s
✓ Run a single assembly instruction: stepi
✓ Show call stack: backtrace or bt
✓ Move up and down the call stack: up 1, down 1, or other numbers.
✓ Print a variable’s value: p value.
✓ Set a breakpoint at a function: b functionname
✓ Set a breakpoint at a specific line: b filename.c:linenumber, eg.
main.c:42
5. DEBUGGING CRYPT ON THE ZYBO IN LINUX USING GDB 322

This is only a small set of commands gdb knows of. For a full overview,
please refer to the GDB documentation. Several frontents for GDB avail-
able are, which can ease the debugging experience.
Bibliography

[LaM19] B. J. LaMeres. Quick Start Guide to Verilog. Ed. by Springer. Available at https :
//link.springer.com/book/10.1007/978-3-030-10552-5. Springer, 2019.
[06] “IEEE Standard for Verilog Hardware Description Language”. In: IEEE Std 1364-2005
(Revision of IEEE Std 1364-2001) (2006), pp. 1–590.
[Ash07] P. J. Ashenden. Digital Design (Verilog): An Embedded Systems Approach Using Ver-
ilog. Ed. by M. Kaufmann. Morgan Kaufmann, 2007.
[Hui16] S. Y. R. Hui. “Magnetic Resonance for Wireless Power Transfer [A Look Back]”. In:
IEEE Power Electronics Magazine 3.1 (2016), pp. 14–31.
[JP14] R. Jay and S. Palermo. “Resonant coupling analysis for a two-coil wireless power trans-
fer system”. In: 2014 IEEE Dallas Circuits and Systems Conference (DCAS). IEEE,
2014.
[Sal+09] J. Sallan et al. “Optimal Design of ICPT Systems Applied to Electric Vehicle Battery
Charge”. In: IEEE Transactions on Industrial Electronics 56.6 (2009), pp. 2140–2149.
[HB12] H. Hoang and F. Bie. “Maximizing Efficiency of Electromagnetic Resonance Wireless
Power Transmission Systems with Adaptive Circuits”. In: Wireless Power Transfer -
Principles and Engineering Explorations. InTech, 2012.
[Run15] B. Z. Runhong Huang. “Frequency, impedance characteristics and HF converters of two-
coil and four-coil wireless power transfer”. In: IEEE Journal of Emerging and Selected
Topics in Power Electronics 3.1 (2015), pp. 177–183.
[Sam+13] A. P. Sample et al. “Enabling Seamless Wireless Power Delivery in Dynamic Environ-
ments”. In: Proceedings of the IEEE 101.6 (2013), pp. 1343–1358.
[Bar+15] S. D. Barman et al. “Two-side Impedance Matching for Maximum Wireless Power
Transmission”. In: IETE Journal of Research 62.4 (2015), pp. 532–539.
[Luo+17] Y. Luo et al. “A Frequency-Tracking and Impedance-Matching Combined System for
Robust Wireless Power Transfer”. In: International Journal of Antennas and Propaga-
tion 2017 (2017), pp. 1–13.

323

You might also like