EAGLE Book
EAGLE Book
EAGLE TA Team
Chapter 8. Introduction 52
Project Overview
CHAPTER 1
Introduction
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
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.
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
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.
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_rotx
connec�on
JC
HDMI
RC_rotz RC_roty
connec�on
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
GND
GND
3V3
3V3
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
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
✓ 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.
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
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.
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.
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.
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
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.
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
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.
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
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
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
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.
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
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
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
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.
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.
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
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).
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
}
Nonce
Plaintext
Auth. Data
Plaintext’
Plaintext
Decryptk
Tag’
Encryptk
Ciphertext
Tag =
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).
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
✓ 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
✓ 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.
✓ 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).
✓ 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
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.
47
1. INTRODUCTION 48
✓ 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
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
52
CHAPTER 9
Linux Commandline
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
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'
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
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
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.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
Tip 9.5.1
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
Tip 9.5.4
You can use the ~/.bashrc file to configure the bash terminal.
CHAPTER 10
Programming Languages – C
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
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
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();
}
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;
␣␣␣␣...
};
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);
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
};
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.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
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;
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
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.
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);
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 symbols `x` and `u` for the state and input
% variables
x␣=␣sym('x',␣[nx,␣1],␣'real');
u␣=␣sym('u',␣'real');
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 .
% Input signal
u␣=␣@(t,x)␣sin(t);
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');
% System dimensions
nx␣=␣length(A);␣␣␣% number of states
nu␣=␣size(B,2);␣␣␣% number of inputs
ny␣=␣size(C,1);␣␣␣% number of outputs
end
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
% Symbolic Jacobians:
Jfx␣=␣jacobian(f,␣xSymb);
Jfu␣=␣jacobian(f,␣uSymb);
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);
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 .
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');.
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.
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
␣␣␣␣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
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
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
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.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;
␣␣␣␣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)␣})
␣␣␣␣␣␣␣␣␣␣␣␣);
␣␣␣␣␣␣␣␣}
};
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.");
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
% Built−in function
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
Dependency-free
C++ implementation
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
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;
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␣=␣{};
Vec3f␣evaluate(Vec2f␣x)␣{
␣␣␣␣// initialize y
␣␣␣␣Vec3f␣y␣=␣{};
% open files
finID␣=␣fopen('matvec.cpp.template',␣'r');
foutID␣=␣fopen('matvec.cpp',␣'w');
% add y = A*x + b
print_affine(filep,␣y,␣x,␣A,␣b);
We also added Ex. 25.2.1 for some specific instructions related to codegeneration
for the provided framework.
CHAPTER 12
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
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
float␣a␣{1.2};
std::complex<float>␣c␣{0.5,␣-0.5};␣// 0.5 − 0.5i
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'};
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"
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.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
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
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'
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:
✓ 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
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>'
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.
// 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
float␣get_pi()␣{
␣␣␣␣return␣3.14159265;
}
float␣get_pi();␣// declaration
float␣get_tau()␣{
3. FUNCTIONS 104
float␣get_pi()␣{␣// definition
␣␣␣␣return␣3.14159265;
}
This example relies on the C++ standard library function std::log2 that is included
with your compiler.
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.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);
}
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 + 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 : 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 ? 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
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>
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"
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>
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
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"
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");
␣␣␣␣}
}
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;
␣␣␣␣}
}
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
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()␣{␣/* ... */␣}
};
␣␣␣␣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;
␣␣␣␣}
};
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;
␣␣␣␣}
};
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);
␣␣␣␣}
␣␣␣␣// [...]
};
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.
␣␣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
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
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,
};
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:
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!";
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
#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
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:
Warning 12.9.1
You should never compile a header file directly, and you should never #include
an implementation file.
✓ 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.
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
.
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>
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.
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:
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()
}
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
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
␣␣␣␣}
}
Warning 12.12.1
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;
}
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:
// 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
// 4.
// Arrays cannot be copied using the assignment operator:
int␣a[]␣=␣{10,␣20,␣30};
int␣b[]␣=␣a;␣// error
// 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')
✓ 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;
}
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"
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"
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
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.
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.
1https://en.wikipedia.org/wiki/Four-valued logic
2. VERILOG, THE ABSTRACT SIMULATION LANGUAGE 142
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.
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
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
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
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
reg␣enable;
wire␣[23:0]␣ncyc;
wire␣alarm;
assign␣ncyc␣=␣24'd424242;
timer␣#(CWIDTH=24)␣mytimer(clk,␣nrst,␣enable,␣ncyc,␣alarm);
always␣@(*)␣enable␣<=␣!alarm;
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;
endgenerate
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.
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
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.
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.
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
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
always␣@(posedge␣clk,␣negedge␣nrst)␣begin
3. VERILOG AS A HARDWARE DESCRIPTION LANGUAGE 154
␣␣␣␣if␣(!nrst)␣state␣<=␣1'b0;
␣␣␣␣else␣state␣<=␣nextstate;
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
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
always␣@(bufreadaddr)␣bufreadval␣=␣d_out_mem[bufreadaddr];
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.
// ...
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.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:
This code will generate a clock signal with a period of 10 time units. Handling
the reset is not much more difficult:
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
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
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);
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
␣␣␣␣␣␣␣␣donecheck␣=␣0;
␣␣␣␣␣␣␣␣while␣(donecheck␣==␣0)␣begin␣// wait until the module has finished
␣␣␣␣␣␣␣␣␣␣␣␣#`CLK_PERIOD␣donecheck␣=␣done;
␣␣␣␣␣␣␣␣end
␣␣␣␣$display("All␣test␣succeeded.");
␣␣␣␣$finish();
end
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.
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
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
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()
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
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.
µ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.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.
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 .
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.
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.
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
ω 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
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.
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
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
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.
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.
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”
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◦
ϕ
τ
dψ
−45◦ /s dt +45◦ /s −10◦ θ +10◦
0 −1 +1 −10◦
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
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
(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.
(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.
(6) View the Spice-netlist as shown in Figure 7 by right clicking on any open
space in the document.
(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
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.
(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
(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
(2) We will work in the same directory we created at the beginning of Tutorial
18.1.2.
(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.
(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.
(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 20. Anode (n1) and cathode (n2) voltage for a changing
frequency.
(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
(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
(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.
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!
(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.
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
(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
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
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.
(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;
// 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.
(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
> 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.
(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
(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
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
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.
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
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
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.
202
2. CREATING AND TESTING A HARDWARE PROJECT 203
(a) (b)
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)
Tip 19.2.1
(a) (b)
(a) (b)
Figure 5
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.
␣␣␣␣␣␣␣␣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);
␣␣␣␣␣␣␣␣#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
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.
(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
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
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)
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.
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.
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.
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!
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.
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.
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.
(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
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
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)
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.
(a) (b)
(a) (b)
(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)
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;
␣␣␣␣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);
␣␣␣␣␣␣␣␣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
✓ 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?
(a) (b)
Tip 20.3.1
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
(a) (b)
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
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)
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
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
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.
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)
(a) (b)
(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
(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.
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.
(a) (b)
(a) (b)
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.
(a) (b)
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
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
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
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
(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) 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
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. 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
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
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.
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.
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.
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
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
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.
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
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"
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
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
[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
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...
% 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;
altData.sonarMeasurement.altitude␣=␣p(3);
altData.sonarMeasurement.hasNewValue␣=␣true;
altData.hoveringThrust␣=␣0.5;
navData.shared.position␣=␣p(1:2);
navData.shared.hasUpdated␣=␣true;
(4) We can now pass our measurements and inputs to the ctr instance.
2. THE MEX WRAPPER 268
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;
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␣{
␣␣␣␣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');
␣␣␣␣'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'
};
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.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.
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);
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.
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');
(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);
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!
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
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.
Then run the relocation script to make sure all paths are configured cor-
rectly:
./arm-zybo-linux-gnueabihf_sdk-buildroot/relocate-sdk.sh
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
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
ssh␣Zybo␣reboot
ssh␣Zybo␣Autopilot
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
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!
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
(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.
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).
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.
project.
connec�on connec�on connec�on
JF
UART
connec�on
JA
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.
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
(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
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.
Content guidelines
Tip 28.0.1
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
LateX guidelines
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}
Warning 29.3.1
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
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.
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
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.
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.
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.)
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.
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.
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
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
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.
(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.
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.
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
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
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.
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.
(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
Warning 30.3.2
(4) Now start compiling the configured Linux kernel as usual, see step §3.1.5
and onward.
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
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
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.
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!
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
␣␣␣␣␣␣␣␣␣␣`---␣...
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
4.2.7. Installation.
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.
(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
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.
✓ 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
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) 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
Figure 1
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.
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.
(a) (b)
(a) (b)
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.
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
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 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
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
(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
(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