blog/static/pages/Build-an-open-source-drone-with-a-Raspberry-Pi-and-Platypush.md

1577 lines
85 KiB
Markdown
Raw Normal View History

[//]: # (title: Design and build a drone from scratch)
[//]: # (description: How to use a Raspberry Pi, Platypush and some cheap electronics to build your own drone.)
[//]: # (image: /img/drone-schema.png)
[//]: # (author: Fabio Manganiello <fabio@platypush.tech>)
2021-08-29 21:04:50 +02:00
[//]: # (published: 2021-08-29)
[//]: # (latex: 1)
Drones are increasingly popular and affordable nowadays, and they have become a popular toy for kids and adults
of all ages. If all you need is a flying camera to take your holiday selfies then an off-the-shelf solution may
be good enough. If instead you want something a bit more customized, or you just enjoy the pleasure of building
things from scratch to understand how they work (and few things give more satisfaction than seeing something you have
built lift off the ground), then you are in the right place.
This article is about designing and building a drone completely from scratch and is divided in four parts.
The first part covers the physics of lift, both under an aerodynamic (how to pick the right motors and blades and
how to use them to generate lift) and an electric perspective (how lift power and time-of-flight relate to battery
capacity and discharge rate and how to optimize your design).
The second part covers the hardware side, how to get the frame in place, which electronic components to pick, how to
wire them together, how to place them on the body of the drone and how PWM modulation works.
The third part covers the software side, specifically how to code a flight controller on a Raspberry Pi as set of
Python scripts that run on top of [Platypush](https://platypush.tech).
The fourth part shows how to put all things together, calibrate and fly your new drone.
## Disclaimer
A disclaimer is owed before we start.
Drones are devices that use rigid propellers that spin hundreds or thousands of times in a minute. That's at least ten
times faster than your regular table fan. Needless to say, if those things touch your fingers you can get very, very
hurt. Moreover, these propellers are usually powered by high-power lithium batteries that spit out lots of current to
ensure that they can run so fast. If any of the connections between your propellers and the battery are loose, you may
get some bad sparks as soon as you try and fly your drone - and nothing nice can happen if those sparks touch the
battery.
So some precautions are due before you assemble and start anything:
1. **NEVER**, ever run your drone with the blades mounted unless you have completed the calibration phase, and you
are **REALLY** sure that you can quickly switch everything off in case your drone leaps towards your face.
2. As a corollary of the point above: **ALWAYS** make sure that you have a kill switch (either hardware or software)
to physically cut the power from the motors before your drone damages objects or living beings.
3. Second corollary: when you are in the initial calibration phase of your drone, or you are checking that all the
connections are fine and that the motors spin in the right direction, **ALWAYS** do it without the blades mounted.
4. **ALWAYS** make sure that the drone has enough room around before starting the motors, and that no wires nor other
mechanical obstacles are on the path of the propellers when they spin.
5. **ALWAYS** pay attention when you work with large LiPo batteries. They are amazing and can store a lot of juice, but
they are also ready to go on fire on the first mistake. Make sure that none of the moving parts is ever touching the
battery - even a small puncture can cause the battery to smoke, as the oxygen from the air comes in contact with the
electrolytes of the battery. **NEVER** overcharge them and never over-discharge them - refer to the manual for the
advised voltage range. **NEVER** short-circuit them.
6. **ALWAYS** make sure that all the connections between the battery and the motors (including those to the power board,
to the ESCs and to the motors) are stable and well soldered. When you install electronics on things that can move so
fast, even a small loose connection can be a recipe for disaster. Also, make sure that all the wires and electrolyte
capacitors are installed with the right polarity.
7. Consider installing physical protections for your propellers.
8. If you are planning to fly the drone outdoor, remember to check your local rules, and always behave responsibly:
after all, you are flying DIY vehicles in public areas.
![Drone warning](../img/drone-warning.png)
With these recommendations out of the way, let's start getting our hands dirty with a bit of theory about drones and
lift.
## The physics of a drone
### The physics of lift
This section will include some theory on how to get drones (and things in general) to lift and fly. I would advise to
do these calculations *before* you start assembling or even order components for your drone: flying objects rely on
delicate physical balances. If you get any of the variables wrong (e.g. you assemble a drone that is too heavy for what
the propellers can lift, or you install a battery that can't provide enough current for lift-off) then you'll only
notice it when the drone is completely assembled, and it'll take time, money and effort to disassemble the drone,
redesign it and reorder components in order to address these issues (I have learned this the hard way). Time to dust
off some physics books!
A quadcopter flies by displacing air through the quick rotations of its propellers. The blades of a propeller share a
similar structure with wings, and both are designed to move air at a higher speed above them than the air below. A wing
is designed to "cut" through the air at high speeds, causing the air above it to move as fast as the wing but in the
opposite direction (3rd Newton's law a.k.a. _action-and-reaction_). This movement ends up creating a pocket of air with
lower pressure and higher speed on the top of the blade. This volume of air is forced to flow downwards along the
structure, and it meets the bottom flow of air at the tip of the blade. This movement causes the air at the bottom of
the blade to apply an upward pressure towards the bottom of the propeller that is higher than the downward pressure
applied to the top. The imbalance between the pressure of the two columns of air (above and below the blade) is what
causes the attached structure to lift.
![Drone lift diagram](../img/drone-lift.gif)
The first fundamental definition when it comes to drone flight is that of **static thrust** \(T_0\). The static thrust
is informally defined as the force that the propellers need to provide to the vehicle to maintain it in _stationary_
conditions. When the drone is _hovering_, this is basically the force that the propellers need to apply to the vehicle
in order to balance its weight and keep it in position. Calculating this force is fundamental to design any vehicle that
can fly, albeit it's not a sufficient condition: to actually get a vehicle to lift off you first need to apply a force
\(T > T_0\), so you should usually design a drone in such a way that its propellers can provide a thrust at least 25%
higher than its static thrust.
![Static thrust force diagram](../img/static-thrust-1.png)
Intuitively, the thrust provided by a propeller must be somehow proportional to the velocity of the air it displaces.
The higher the speed of the air it moves, the lower its pressure, the higher the gradient of pressure between the air
above and below, the higher the lift it can provide. A formalization of this intuition is provided by the
[Bernoulli's principle](https://en.wikipedia.org/wiki/Bernoulli%27s_principle), which states that air pressure _p_
and velocity _v_ at any point in space are connected by the following relation:
$$
\frac{v^2}{2} + \frac{p}{\rho} = const
$$
where \(\rho\) is the density of the medium (in this case air).
If we now define \(p_0\) as the pressure of air below the blade, \(p_1\) the pressure above, \(v_0\) as the speed
of air before it hits the blade and \(v_1 = v_0 + \Delta v\) as the speed of air after it is accelerated by the blade,
then we must have:
$$
p_0 + \frac{1}{2} \rho v_0^2 = p_1 + \frac{1}{2} \rho v_1^2
$$
The difference in air pressure that generates the lift can therefore be written as:
$$
\Delta p = p_1 - p_0 = \frac{1}{2} \rho (v_1^2 - v_0^2)
$$
We can picture how air speed and pressure change when they are accelerated by a propeller with diameter _d_ (and
how the width of the column of air changes as well) through this diagram:
![Diagram of the air flowing through a propeller](../img/propeller-1.png)
We also know that pressure equals force divided by area, therefore the thrust must equal the difference of pressure
times the area of the propeller _A_:
$$
T = A \cdot \Delta p = \frac{1}{2} \rho A (v_1^2 - v_0^2)
$$
The area of a propeller moving at very high speed can be approximated as the area of a circular disc whose diameter
equals the diameter of the propellers:
$$
A = \pi \Big( \frac{d}{2} \Big)^2 = \frac{\pi}{4} d^2
$$
Therefore we can rewrite the thrust as:
$$
T = \frac{\pi}{8} \rho d^2 (v_1^2 - v_0^2)
$$
If we replace \(v_1\) with \(v_0 + \Delta v\) we get:
$$
T = \frac{\pi}{4} \rho d^2 \Big(v_0 + \frac{\Delta v}{2}\Big) \Delta v
$$
We can assume that \(v_0 = 0\), i.e. the velocity of air far from the propellers is zero (this may not be a good
approximation when you operate the drone in a windy or highly turbulent environment though). With this assumption, the
formula can be simplified to:
$$
T = \frac{\pi}{8} \rho d^2 \Delta v^2
$$
This equation provides two very valuable pieces of information when it comes to designing our drone:
1. *The thrust is proportional to the square of the diameter of the propellers*. If you double the diameter of the
propellers then the thrust increases by 4. If you triple it then it increases by 9, and so on. However, larger
propellers tend to have larger mass, the motors have to apply a greater force to spin them, and they generate greater
air turbulence, resulting in drones that are harder to control.
2. *The thrust is proportional to the square of the variation of speed of the air around the propellers*. Propellers
that can displace more air can generate more thrust. The more obvious way to increase the speed of air is to increase
the speed of the propellers by applying a higher current to the motors. Another way is to increase the number of
blades on the propellers (more blades will cause more air to move, which in turn causes a greater difference in
velocity), but a higher number of blades tends to also increase air turbulence. Usually a two blade configuration is
used for most of the commercial drones, while a three blade configuration is often used on racing drones or
high-power vehicles. Also keep in mind that the efficiency of a real propeller goes down when \(\Delta v\) becomes
too high (that's because a higher gradient of velocity means a higher turbulence when the air flowing on top of the
wing meets the air below at the edge of the blade), so you may want to strike a balance between thrust generated
by air displacement and propeller efficiency.
Albeit useful, this equation isn't really the most used when it comes to quadcopter design. That's because it's tricky
to build a model that takes into account the velocity of air displaced by each single propeller. Instead, most of the
drones rely on electric batteries, and we have better information about battery power, drawn current and voltage than we
have about volumes of displaced air or gradients of pressure. So it's convenient to transform velocity into power _P_,
remembering that:
$$
P = \frac{\Delta E}{\Delta t} = \frac{F \cdot \Delta s}{\Delta t} = F \cdot v
$$
A common convention is to set _v_ as the median between the velocity of air before (\(v_0 = 0\)) and after
(\(v_1 = v_0 + \Delta v\)) hitting the blade:
$$
v = \frac{v_0 + v_1}{2} = \frac{\Delta v}{2}
$$
We can then rewrite the equation of power with respect to propeller thrust as:
$$
P = \frac{1}{2} T \Delta v
$$
In a real-case scenario, however, not all the energy drawn from the battery is converted into kinetic energy that spins
the propellers. Motors have their own electric efficiency \(\eta_{el}\), which depends on the losses caused by electric
energy transformed to heat during the rotation, and propellers also have their own mechanical efficiency \(\eta_
{prop}\), mostly caused by the energy dissipated by the drag of the rotors against the air. A good brushless motor has
an electric efficiency around 90%, while a good propeller has a mechanical efficiency between 80-85%. Therefore, the
equation of the actual power that goes into lifting the drone should be rewritten as:
$$
P_{real} = \eta_{el} \cdot \eta_{prop} \cdot P = \frac{1}{2} \eta_{el} \eta_{prop} T \Delta v
$$
Solving for \(\Delta v\):
$$
\Delta v = \frac{2 \eta_{el} \eta_{prop} P}{T}
$$
Now that we have found a way to express the variation of air velocity in function of the power provided to the
propellers, we can rewrite the equation of thrust calculated previously as:
$$
T = \frac{\pi}{8} \rho d^2 \Delta v^2 \\
= \sqrt[3]{\frac{\pi}{2} \rho d^2 \eta_{el}^2 \eta_{prop}^2 P^2}
$$
Finally, remembering that \(F = ma\), let's divide both the terms in the equation above by the gravity acceleration
_g_ to get a value for thrust expressed in kg:
$$
m = \frac{1}{g} \sqrt[3]{\frac{\pi}{2} \rho d^2 \eta_{el}^2 \eta_{prop}^2 P^2}
$$
This is a very useful equation that can be used to design our drone. If we set _m_ equal to the mass of the drone,
then we can solve for _P_ and calculate how much power we need to provide to the motors in order to generate a lift that
equals the mass of the drone - in other words, this is the power required to achieve static thrust.
$$
P = \frac{1}{d \eta_{el} \eta_{prop}} \sqrt{\frac{2 m^3 g^3}{\pi \rho}}
$$
We can easily wrap the above formula into a small Python function:
```python
def balance_power(mass, propel_diam, motor_eff, propel_eff):
"""
Calculate how much power is required to provide a thrust to a
drone that matches its weight, given a certain configuration
of propellers and motors.
:param mass: Mass (in kg)
:param propel_diam: Propellers diameter (in meters)
:param motor_eff: Motor efficiency (between 0 and 1)
:param propel_eff: Propellers efficiency (between 0 and 1)
:return: The power required to generate a lift that balances the weight
of the vehicle, in Watts
"""
import math
# Air density at sea level and room temperature is about 1.225 kg/m^3
density = 1.225
# Gravity acceleration = 9.8 m/s^2
g = 9.8
return (
(1/(propel_diam * motor_eff * propel_eff)) *
math.sqrt((2 * math.pow(mass,3) * math.pow(g,3))/(math.pi * density))
)
```
For example, if you have a drone with the following characteristics:
- _mass_: 500 grams
- _propellers diameter_: 5 inches (= 0.127 meters)
- _motors efficiency_: 90%
- _propellers diameter_: 80%
We can infer that it takes about 85W of power to generate a lift that balances the weight:
```python
>>> balance_power(mass=0.5, propel_diam=0.127, motor_eff=0.9, propel_eff=0.8)
85.51256229077079
```
### The physics of electric vehicles
The last formula brings us directly to the next topic: once we have figured the dynamics and the aerodynamics, and how
much work is required to lift our object, it's time to translate those constraints into electric constraints and size
battery, speed controllers and motors accordingly. Importantly, we should also design the system to be able to provide
more power than the power required to simply balance its weight - thrust needs to be greater than weight if we want the
drone to go up.
You may probably need a powerful LiPo battery if you want to provide your motors with enough power to generate
lift for a drone that will sport a Raspberry Pi, a camera, a bunch of sensors, and who knows how much more stuff you are
planning to add :) high-power LiPo batteries usually come in packages like this:
![Picture of a LiPo battery](../img/lipo-1.jpg)
High-power LiPo batteries usually come with two types of connectors: XT60 (yellow adapter in the picture above)
or T-Plug (original dark red connector connected to the battery in the picture above). Always make sure that the
connector of your battery matches the connector of your power distribution board (adapters are available and cheap, but
they add a bit of weight and take extra space on the drone).
A LiPo battery usually comes with an indication of its total capacity _Q_, usually expressed in mAh, and its
voltage, expressed in volts (keep in mind, however, that the advertised voltage goes down as the battery discharges,
and that also impacts the output power).
You also have an indication of the **discharge rate**, usually indicated by a number suffixed by _C_. This number
is used to calculate the maximum current \(I_{max}\) that the battery can provide without resulting in damage to the
battery itself - this number is the most fundamental cap to the maximum power that a battery can provide. The maximum
current is calculated as the total capacity expressed in amperes divided by one hour and multiplied by the _C_ number.
For example, in the case of the battery pictured above, with an advertised _50C_ value and _5200 mAh_ capacity, we have:
$$
I_{max} = 5.2 \cdot 50 = 260 A
$$
Now that we have the tools to measure the capacity, voltage and maximum current supported by the battery, we can
substitute \(P = IV\) in the previous equation of power and, assuming that the voltage is about constant, we can
calculate how much current the motors will absorb to generate a certain lift:
$$
I = \frac{P}{V} = \frac{1}{Vd \eta_{el} \eta_{prop}} \sqrt{\frac{2 m^3 g^3}{\pi \rho}}
$$
For example, if the minimum power required to provide stationary thrust is 85W, and the battery works with a tension of
11.1V, then the average current that will be absorbed by the motors while hovering is about 7.6A.
It is _very_ important that this current is lower than \(I_{max}\) - ideally it should be a fraction of \(I_{max}\),
or at least half of it to give some headroom during current peaks, especially during lift off, in order to prevent
permanent damage to the battery, so choose a battery with an appropriate discharge rate for the physical characteristics
of your vehicle.
Finally, we can calculate how long the battery will be able to provide a certain current. Knowing that
\(I = \frac{\Delta Q}{\Delta t}\), we can express time as a function of battery capacity and required power as:
$$
\Delta t = \frac{Q}{I} = \frac{QV}{IV} = \frac{E}{P}
$$
Where _E_ is the total energy stored in the battery and _P_ is the power we want to provide to the load.
Suppose that a 5000 mAh, 11.V battery is used on our previous 500 grams drone. Then in an ideal case the battery can
provide a power of 85W (8A * 11.1V) for:
$$
\Delta t = \frac{5A * 3600\mbox{ sec} * 11.1V}{85W} = 39\mbox{ min}
$$
In reality this value is usually lower (usually at least a half or a third of it, depending on the minimum current
required by the motors to spin) with a non-ideal battery, because below a certain charge left the battery won't be able
to provide the same nominal values of voltage and current, while the formula above assumes that the battery can provide
the same power until the last bit of charge left. However, you can empirically estimate the amount of charge left in the
battery when it starts to struggle to provide the motors with enough torque (you can hear the motors slowing down when
this happens), replace _E_ in the equation above with \(\Delta E = E_{max} - E_ {min}\), and you can get a more
realistic estimate of the maximum flight time.
### Theory in practice
The math that governs the physics of lift and electric power may take a while to sink in, but we can briefly sum up the
key takeaways from the formulas above when it comes to designing your drone:
- The power you need to provide to lift your drone is inversely proportional to the diameter of the blades (larger
blades mean more air moved by the propellers, therefore more thrust, therefore less power required). However, larger
blades also add up to the total mass, and they also generate greater turbulence, making the vehicle harder to control.
- The power you need to provide to lift your drone is proportional to the 3/2 power of its mass (a bit more than a
linear dependency, a bit less than a squared dependency). So beware of entering the vicious cycle where greater mass
requires bigger battery and bigger propellers, which in turn add mass, which in turn requires greater power, and so
on.
- The discharge rate (_C_) of a battery is an important factor that you should take into account before selecting a
battery. Make sure that the battery can provide enough current to achieve static balance given the physical
characteristics of your drone.
- The maximum time of flight for a drone (roughly calculated as the time that a battery can provide enough lift to
counteract its weight) in an ideal scenario equals the total energy stored in the battery divided by the power
required to achieve static balance.
- In reality, this quantity assumes that the battery can provide the same power until the last drop of juice left, which
is usually not the case for real batteries. For a more accurate number, you should estimate how much charge is left in
your battery when the motors start to slow down, and calculate the total time in function of that \(\Delta E\)
difference.
- Larger batteries can provide longer flight time, but they also add up to the total mass, so you may want to find an
appropriate trade-off for your case.
Time to get our hands dirty with the real thing now!
## The hardware of a drone
### Components of a quadcopter
- **Flight controller**: This is the "brain" of the drone. It receives commands from an input source and sends signals
down the line to control the motors. This is usually the most expensive component of a drone if you want to buy an
off-the-shelf circuit. We will be using a Raspberry Pi Zero as a flight controller in this article. That makes the
final cost of the drone considerably lower, but it also requires us to write the code for receiving remote commands
and for sending the correct signals downstream to the motors.
- **Propellers**: These are technically the only moving parts of the vehicle. We have already covered previously how
their diameter affects the generated thrust, the required power and the level of air turbulence. Another important
consideration is about the number of blades. 2-blades propellers often result in vehicles that are easier to control,
even though they may not provide the same acceleration. 3-blades configuration are a bit harder to control, but they
usually provide greater acceleration and are preferred for larger drones or racing configurations. The material of
the propellers also plays a role, both in terms of robustness and added weight. Carbon fibers propellers are usually
preferred in professional applications because of their lower weight and greater robustness, but they come at a higher
cost. Plastic propellers are arguably the most common in amateur configurations. Remember: if you are still testing
and calibrating your drone, then go for cheap plastic propellers (they will get damaged for sure after a few bumps!),
and only use more expensive propellers once you have calibrated the vehicle and mastered the control.
Protective shields for propellers are usually also a *VERY* good idea.
- **Brushless motors**: Smaller drones may sometimes opt for brushed motors, but these usually have lower efficiency
(75-80% vs. 85-90% for brushless motors) and tend to have a shorter life. However, brushless motors are usually more
expensive. The principle of a brushless motor is relatively simple: it consists of two parts, a _rotor_ (the rotating
part), and a _stator_ (the stationary part at the center of the motor). Both the stator and the rotor usually include
multiple permanent or coiled magnets. An alternate electric current applied to the stator generates a magnetic field,
and such magnetic field causes a misalignment between the magnetic fields of the stator and the rotor, continuously
attracting or repelling the coils in order to adjust the misalignment, and therefore resulting in a spinning movement.
![Brushless motor structure](../img/brushless-motor-1.png)
You can find many brushless motors for quadcopters online. There are a few things to keep an eye on when you select your
motors. First, the motor size, expressed as diameter and height of the stator (e.g. 2207 means that the stator is 22mm
wide and 7mm tall): bigger motors result in higher torque but they also absorb more power. Then, the _kV_ of the motor,
which expresses the ideal number of rotations for each volt applied. The _thrust-to-weight ratio_ expresses how much
thrust the motor can generate for each unit of weight - the higher, the better, 2:1 is an acceptable minimum value, 4:1
is considered a middle sweet spot, high ratios such as 8:1 or 13:1 can theoretically be achieved by high-performance
motors, but after a certain number of rotations per second spinning a motor any faster is considered inefficient.
Finally, you may want to look at the advised number of LiPo cells for the motor. Since high-speed motors drain
more power, they usually require batteries with a higher number of cells, as a higher number of cells translates in a
higher voltage.
- **Electronic Speed Controllers** (**ESCs**), one for each motor. Brushless motors work thanks to a varying current
that generates a rotating magnetic field, but you usually need a component between your controller and the motors
to transform the desired motor speed into an alternate current configuration to be applied to the motors. ESCs are
small circuits that do exactly this job. A factor to take into account when choosing an ESC is its current
specification (in Amperes). That expresses the maximum current that the ESC can deliver to the motors. Make sure that
it is higher than the current absorption you have estimated for your drone and lower than the current defined by the
discharge rate of your battery.
- A **PWM servo controller**: Most of the ESCs out there communicate over _pulse width modulation_ (_PWM_). This is a
quite efficient way to transmit analog signals using the duration (width) of a digital signal. PWM basically allows
you to send analog signals using only one digital PIN. We will explore its internals a bit more in depth in the
coding section. Unfortunately, even though the Raspberry Pi theoretically has 4 PWM PINs, each of the pairs shares a
PWM resource (GPIO 12 and GPIO 18 share a PWM channel while GPIO 13 and GPIO 19 share the other channel). This means
that you can actually send a maximum of two distinct PWM signals at the same time, while our quadcopter obviously
needs four of them. To solve the problem, you can use a PWM servo controller to extend the PWM capabilities of the
Raspberry Pi - a popular choice is the [Adafruit 16-channel PWM driver](https://www.adafruit.com/product/815), which,
as the name suggests, can control up to 16 independent PWM channels at the same time, and you can even connect up to
62 of these boards to control up to 992 channels, just in case you are planning to design a spaceship.
- A pair of **batteries**. I said _pair_ because my advice is to use two separate power sources: a high-power LiPo
battery to power the motors, and a small LiPo battery to power the Raspberry Pi and the electronics (a Raspberry Pi
Zero that only runs the drone code shouldn't take much power). Even if a high-power LiPo can theoretically provide
enough power both for the motors and for the electronics, in reality the motors can suck up a lot of juice when they
spin fast, and if the current drops below a certain threshold the Raspberry Pi will just reboot (and you don't want
that to happen while your drone is hovering midair). You may also need a LiPo-to-USB power converter like the
[PowerBoost](https://shop.pimoroni.com/products/powerboost-1000-charger-rechargeable-5v-lipo-usb-boost-1a-1000c),
whose job is both to provide a USB output from a LiPo source and to stabilize the output voltage and current draw.
Keep in mind the previous consideration when you pick the battery to power the motors (weight, charge, discharge rate,
number of cells and output voltage).
- A **power distribution board**, like [this one](https://www.amazon.de/-/en/gp/product/B071NXZLBM/). First, LiPo
batteries for quadcopters have specific connectors (usually XT60 or T-Plug) and you need some kind of adapter to
actually connect load to them. Second, these boards are a compact solution to distribute the power of a battery to
multiple loads - the board linked above can split the power of a LiPo battery to up to 6 loads.
- A **controller**: Most of the commercial quadcopters have radio controllers, but that would add up both to the cost
and to the complexity of the circuitry used for the drone. For simplicity, in this article I'll illustrate how to set
up any device that exposes a joystick interface on Linux as your controller, with a particular focus on a Bluetooth
joystick (Bluetooth can only operate up to 10 meters, but since the Raspberry Pi has a built-in Bluetooth chip it
doesn't require USB dongles nor extra RC circuits).
- A **drone frame**: You can find many drone frames online for quite affordable prices, or you can easily download a 3D
model and print it yourself. Personally I have used
[this model](https://cults3d.com/en/3d-model/various/drone-fpv-280) for my prototype, but
[this](https://cults3d.com/en/3d-model/game/chassis-drone-quadcopter) and
[this](https://cults3d.com/en/3d-model/gadget/drone-qav-250-fpv) are also popular (and more versatile) form factors.
- A **camera** to take your amazing pictures and videos from above. We'll use a Raspberry Pi Camera in this tutorial,
since it already comes with a RPi native interface, and it doesn't require extra USB dongles and cables.
2021-08-30 00:06:35 +02:00
- (_Optional_) a **gyroscope**: if you want to bring your drone to the next level then you can also add an gyroscope to
the Raspberry Pi or a connected microcontroller. The gyroscope measures the gravity acceleration along the three axes.
You can easily add some logic to stabilize a drone if the input data from the gyroscope shows that it's leaning
too much in one direction.
- (_Optional_) a **distance sensor**: ultrasound, laser or lidar sensors are very useful to study the environment around
the drone. You can place one at the bottom of the drone to detect when the drone has touched the ground, or some
sensors around the body to detect and avoid possible obstacles.
## Putting things together
You should now have a clear idea both of how to get a vehicle to lift and which components you need in order to achieve
it. Let's put together the ingredients covered in the previous section to show a possible schema for the connections of
your components.
![Drone schema](../img/drone-schema.png)
A couple of recommendations before jumping in a step-by-step breakdown of the diagram above:
- Once you have all the components required for your drone, and before starting connecting, soldering and gluing things
together, get a scale, put all the components on it and check the total weight. This is the perfect moment to run the
calculations shown previously to estimate thrust, power and flight duration of your drone, now that the total mass is
known.
- Play a bit with the placement of your components on the drone frame before you start connecting them together. Given
the shape of your frame, study what's the best placement of all the components - Raspberry Pi, PWM driver, camera,
power board, batteries, adapters, etc. Make a drawing of their placement, if it helps you remember a particular
configuration. Also, study how each component would connect with the others given their placement - the last thing you
want is an unmanageable ball of wires going all over the place. Space is limited on the body of a drone, and an
optimal initial arrangement of all the components puts your project in a good place.
- Also, make sure that the weight is more or less balanced. It doesn't have to be 100% balanced on all the four arms, a
few grams of difference are still ok, the thrust can be adjusted accordingly during the calibration phase. However, a
too heavy imbalance can only be fixed by spinning some propellers at very high speeds, resulting in a higher current
drain, shorter duration of the flight, earlier damage to the motors and a harder to control drone.
With these recommendations out of the way, let's dive into assembling our drone.
### Frame
Start by mounting the frame. Depending on the frame you bought or printed, this can be a task with a varying level of
challenge. When you screw the parts together, remember that plastic screws are lighter than metallic ones. Avoid using
hot glue to keep things together: you can use hot glue to stick other components to the frame or hold wires together,
but make sure that the frame is sturdy and strong, and that it holds together even without glue. Your drone may fall
a lot, and the last thing you want is the drone's body coming completely apart after a small fall.
Once the frame is installed, motors are usually the next logical step. **ONLY** mount the motors right now, **NOT** the
propellers. Propellers should be installed only when you have managed to calibrate and remotely control the motors, and
you are sure that the drone won't flip backwards (or, worst, in your face) once you start giving power to the motors.
### Motors and propellers
If you have paid of attention to the previous diagram, you may have noticed that the motors spin in different
directions. More precisely, the motors on the opposite diagonals spin in the same direction, and those on the same side
spin on opposite directions. The most popular configuration is the following:
- _Front-left motor_: clockwise spin.
- _Front-right motor_: anti-clockwise spin.
- _Back-left motor_: anti-clockwise spin.
- _Back-right motor_: clockwise spin.
The reason for this configuration can be found in Newton's 3rd-law, a.k.a. "action and reaction".
If a helicopter only had the top propeller and not the smaller propeller on the back, it simply won't fly. Its body
will just spin in the opposite direction as the rotation of its propeller. It's the propeller on the back (and the
length of the arm that connects it to the body) that balances the spinning movement and provides direction to the
vehicle.
Similarly, if all the motors of a drone spun in the same direction then the drone will simply spin in the opposite
direction as their rotation. This is because the motors apply a _torque_ to the body of the drone, and the body of
the drone responds to this force with a forces that pushes it in the opposite direction. If instead you use an X-shaped
configuration, then the rotational components of the four torques cancel each other if they rotate at the same speed,
and all you have left is the lift. However, you can still adjust the torques during the flight to temporarily produce a
rotation (or _yaw_) of the drone around its _z_ axis.
Most of the sets of motors for quadcopters already come in pairs that rotate in different directions. The direction of
the rotation is usually reported as a small arrow on the side of the motor's body.
Once you have identified the correct spin direction of the motors, install the motors on the body of the drone.
### Propellers direction, wind direction and lift
Before proceeding with the other components, we should probably spend two words on the direction of the propellers
(again, **DON'T** install them yet, but just keep these considerations in mind when you are ready to install them).
Propellers for quadcopters usually come in two pairs with wings facing in slightly different directions. If you look at
the sections of a set of propellers, you'll notice that they come with two different profiles: they either have the high
edge on the left side and the bottom edge on the right side, or the other way around.
We previously mentioned that lift is generated by creating a pocket of fast-moving, low-pressure air on top of the wing
opposed to slow-moving, high-pressure air on the bottom. The high-pressure air below the wing ends up applying an
upward facing force that causes the vehicle to lift.
![Propeller rotation vs. air direction](../img/propeller-schema.png)
If you look at the section of a propeller and picture it against the diagram above, then it's quite simple to figure
out which propellers need to be attached to which motors. The propellers should "cut" through the air through the side
that faces upwards. Because of Newton's 3rd law, air will start moving in the direction opposite to the spin direction
of the motor. This causes the air above the blade to "jump" up, flow downwards along the blade, and eventually apply a
higher pressure to the air below it. Therefore, a propeller with the "high" edge on the left of its section needs to be
mounted on a clockwise spinning motor, and a propeller with the "high" edge on the right should be mounted on a
counter-clockwise spinning motor. If you invert the direction, then no matter how fast you spin the blades, your drone
won't move by an inch. That's because, under such configuration, the upper face of each blade would move the air up,
therefore the moving air would apply no lift force to the structure of the propeller, and all you have built is a
powerful fan to keep you cool in summer.
### Setting up the remaining hardware
Once the motors are set up, it's time to glue the ESCs to the frame of the drone and connect the motors to them.
An ESC usually has three wires or soldering pads on one side and five wires on the other. The side with three
wires/soldering pads should face the motor and be connected to the three wires coming out of the motor. These
wires work as a current tri-phase system with each phase shifted by 120 degrees compared to the others. The shift in the
output currents is what generates the alternate current that generates the rotating electromagnetic field that causes
the motor to spin. It shouldn't matter in which order you solder these wires to the motor.
![Example of an ESC](../img/esc-1.jpg)
On the other side you will usually find a red wire and a black wire, respectively the reference VCC and GND, which need
to be connected to the battery through the power distribution board.
Finally, you usually find three additional wires on the same side, usually bundled together in a plastic connector and
marked by black, red and white colors. These wires need to be plugged to the PWM circuit board, the red and black
cables must be respectively be plugged to the VCC and GND PINs of the board and the white one to the PWM signal PIN.
After you have installed all the four ESCs and connected them to the motors, it's time to install the power supply
board and connect the ESCs to it.
![Power supply board](../img/xt60-board.jpg)
Most of the boards for quadcopters have two pairs of power supply connectors for the ESCs respectively on the left and
right side. Solder the red wire of each ESC to a _+_ connector and each black wire to the _-_ connector.
After you have mounted the power supply board and soldered the ESC power connectors to it, it's time to move to the
PWM servo board that will control the PWM signals sent to the motors.
![Adafruit 16-channel PWM board](../img/adafruit-16-pwm.jpg)
There may be quite a bit of PINs to solder if you are using the Adafruit 16-channel board. You have to at least solder
the following:
- At least one header of PINs on either the left or right side of the board. These PINs are used to communicate with
the Raspberry Pi over I2C interface, so the only connections you really need are VCC (to the Raspberry Pi 3.3V or 5V
PIN), GND (to any of the Raspberry Pi ground PINs), SDA (serial data, to the Raspberry Pi I2C SDA PIN, usually GPIO2)
and SCL (serial clock, to the Raspberry Pi I2C SCL PIN, usually GPIO3).
- Four triples of PINs (one per ESC) on the PWM connectors on the bottom side. You will connect the wires from the
ESC to these PINs - white wire to PWM, red wire to V+ and black wire to GND.
- Two PINs or a screwed adapter on the V+/GND connectors on the top of the board. You may want to connect the power
supply board 12V/GND connectors here, since most of the ESC devices usually operate around a 9-16V voltage.
**NEVER** connect the V+ line to the Raspberry Pi in any way! 3.3V is supposed to be the maximum voltage of any
circuitry connected to the Raspberry Pi. If any higher voltage is present, then the current may start flowing on
wrong paths and you may end up damaging your Raspberry Pi for good. So connect the V+ to the power supply board,
but make sure that no other V+ PIN is connected to the Raspberry Pi - the only voltage connection to the Raspberry Pi
must be through VCC, which should always be in the 3.3-5V range.
- It's also advised to solder an electrolytic capacitor on the top-left area of the board marked by _C2_. The
electrolytic capacitor provides a voltage buffer to the connected components in case of sudden current spikes or
drops.
Once the PWM circuitry is set up, you can move to gluing or screwing the Raspberry Pi Zero. Connect the I2C interface
on the side of the Adafruit board to the Raspberry Pi. If we consider the following pinout diagram:
![Raspberry Pi pinout](../img/rpi-pinout.jpg)
Then you'll need the following connection:
- PWM board VCC PIN: connect to PIN 1, 2, 4 or 17 (the Adafruit 16-channel PWM board has an operative voltage range of
3-6V).
- PWM board GND PIN: connect to PIN 6, 9, 14, 20, 25, 30, 34 or 39.
- PWM board SDA PIN: connect to PIN 3 (GPIO 2/I2C SDA).
- PWM board SCL PIN: connect to PIN 5 (GPIO 3/I2C SCL).
If you want, you can connect a camera now - simply plug the ribbon cable into the slot on the right side of the board
if you are using a Raspberry Pi compatible camera.
Once the Raspberry Pi is mounted and connected to the rest of the circuitry, you can finally proceed with the
installation of the batteries. Mount the large LiPo battery on the frame and connect its XT60 or T-Plug cable to the
power supply board. If you got all the connections right, you should see both the PWM board and the ESCs power up.
Proceed with the installation of the smaller LiPo battery for the Raspberry Pi. You can use any available LiPo battery
shield or adapter available for the Raspberry Pi, there are many options available to power the Raspberry Pi from a LiPo
either over a USB adapter or through a shield/hat.
Once everything is connected, it's time to move to the software side of the project.
## Prepare the Raspberry Pi
First, you'll need to flash a Linux image to an SD card. Any distro should work, but this article will mostly focus on
Raspbian/Raspberry Pi OS because it makes the CircuitPython setup much easier.
[Download a Raspberry Pi OS image](https://www.raspberrypi.org/%20downloads/). I personally used a headless version
(i.e. with no X server nor desktop environment) because you don't really need to plug an HDMI cable, a mouse and a
keyboard that often into a drone and use it like a computer, but if that's one of the use cases that you have in mind
feel free to get a full desktop image. Use any of the supported methods to flash the downloaded image to the SD card,
depending on the host operating system. For instance, the quickest way from Linux would be to simply use `dd`
(**ALWAYS** make sure that the `/dev` file specified actually points to your SD card, or you may risk wiping your own
filesystem):
```shell
$ sudo dd if=./raspberry-pi-os-VERSION.img of=/dev/mmcblk0 bs=8M conv=fsync status=progress
```
Wait a bit (depending on the size of the SD card) and once the copy is done unmount the device and place it into the SD
card slot on the Raspberry Pi. Before booting the device, connect it to a monitor over an HDMI cable and connect a
wireless/wired keyboard - this may be the only time that you need to actually need to connect the Raspberry to a screen.
Boot the device and wait for the partition resize process to complete. You should eventually be prompted to a login
screen - use the default credentials, _pi - raspberry_, to log in.
We may now want to set up Wi-Fi connection, I2C and camera interfaces and SSH access for remote control. This can be
easily done on Raspberry Pi OS through the `raspi-config` utility:
- Type `sudo raspi-config`.
- Select `System Options` -> `Wireless LAN` and enter the details of your Wi-Fi network.
- Go back, select `Interface Options` -> `SSH` and enable SSH access.
- It's also a good idea to set up auto-login with the `pi` user, so a user session with all the required scripts and
services will start automatically once the device is booted. This can be done from `System Options` ->
`Boot / Auto Login` -> `Console/Desktop Autologin`.
- The Adafruit PWM servo communicates over I2C interface, therefore make sure it's enabled on the Raspberry Pi
(`Interface Options` -> `I2C`).
- If you are using a Raspberry Pi camera, you can enable it from here (`Interface Options` -> `Camera`).
- Select `Finish` and reboot.
- On reboot type `ifconfig` or `ip addr` to verify that you are connected to the Wi-Fi network and note down the
assigned IP address (you may also want to add a static MAC rule on your router to make sure that it stays the same
across reconnections).
Once your device is online, you can unplug the HDMI connection and any mouse or keyboard and restart the Raspberry Pi.
After a few seconds you should be able to ping its IP address, and you can SSH into it from your desktop/laptop through
`ssh pi@rpi-ip`.
Once you have your remote shell to the drone, update it and make sure that the basic Python dependencies are available:
```shell
$ sudo apt update
$ sudo apt upgrade
$ sudo apt install python3 python3-pip
```
You should now proceed with installing the CircuitPython environment. The
PCA9685 chip used by the Adafruit 16-channel PWM board is only compatible with CircuitPython (a minimal installation
of the Python interpreter dedicated to IoT and low-power devices), which isn't installed on the Raspberry Pi by default.
Official instructions are
[provided on the Adafruit website](https://learn.adafruit.com/16-channel-pwm-servo-driver?view=all#python-circuitpython).
The quickest way if you are running Raspbian/Raspberry Pi OS is probably to install the Adafruit Python shell, download
and run the `blinka` script:
```shell
$ sudo pip3 install --upgrade adafruit-python-shell
$ wget https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/raspi-blinka.py
$ sudo python3 raspi-blinka.py
```
Wait a bit for the script to install all the required dependencies, then reboot the device. If everything was
successful, upon reboot you should see a new direct-access block device under `/dev/i2c-1`. You can now install the
CircuitPython driver for the PCA9685 chipset:
```shell
$ sudo pip3 install --upgrade adafruit-circuitpython-pca9685
```
## Set up the controller
Time to move to the controller for our drone. As mentioned earlier, my choice for prototyping is a Bluetooth controller,
since the Raspberry Pi Zero already comes with a built-in Bluetooth adapter and connecting it doesn't require any extra
wires nor dongles. However, Bluetooth poses a limit on the maximum distance between the drone and the controller, since
Bluetooth signals quickly degrade after about 10 meters, and on even shorter distances if there are some obstacles
between the sender and the receiver. My advice is therefore to pick Bluetooth in the initial design and prototyping
phases, or if you plan to fly the drone mostly indoor or on short distances. If instead you plan to fly your drone more
than 10 meters away from you, then you may want to opt for a proper radio controller like
[this](https://www.amazon.com/Radiolink-Transmitter-Controller-Multicopters-Helicopter/dp/B07FPF2HQR/). However, a
proper radio set up is usually expensive, and you'll also have to install and configure a radio receiver on the
Raspberry Pi as well. Moreover, if you are planning to fly your drone outdoor while viewing its camera feed then you
may also want to install a 3G/4G SIM module on the drone, as well as a GPS sensor in order to locate it. These are
eventually the most expensive parts that impact the final price of high-range drones, and they are going to be expensive
even if you go for the DIY way. So my advice is to make a first prototype using the Bluetooth controller on short range,
and once you are confident about your creature then you can make the investment and buy 3G/4G, GPS and RC hardware for
a proper remote control and interface.
Sticking to the Bluetooth controller case, first turn it on and put it in pairing mode (instructions differ from device
to device, consult the manual of the controller). Once the controller is in pairing mode, start the Bluetooth
administration service on the Raspberry Pi and connect the controller:
```text
$ sudo bluetoothctl
[bluetooth]# scan on
...
[NEW] Device 00:11:22:33:44:55 My Joystick
...
[bluetooth]# scan off
[bluetooth]# pair 00:11:22:33:44:55
[bluetooth]# connect 00:11:22:33:44:55
[bluetooth]# trust 00:11:22:33:44:55
[bluetooth]# exit
```
If the connection was successful you should see a new joystick device under `/dev/input/js0`. You can test that all
the buttons are correctly detected through the `jstest` utility:
```shell
$ jstest /dev/input/js0
```
If you can't access the device through the `pi` user then check its permissions and groups through
`ls -l /dev/input/js0`. If it has no read privileges for non-group users then simply add the `pi` user to the
associated group (usually `input` on Raspberry Pi OS) and reboot the device.
## Connect the pieces together
Now that all the hardware is in place and connected, we need to program the "brain" of the drone - the software
component that emulates the flight controller, reads commands from the joystick and forwards them to the motors as
PWM signals.
We'll use [Platypush](https://platypush.tech) to connect the pieces together, since it comes both with a plugin for the
[PCA9685 chipset](https://docs.platypush.tech/platypush/plugins/pwm.pca9685.html) and
[a backend](https://docs.platypush.tech/platypush/backend/joystick.linux.html) to read events from joysticks and joypads
on Linux, as well as several plugins and backends to stream camera feeds.
Install Platypush on the Raspberry Pi Zero together with the web server, PCA9685 and PiCamera dependencies:
```shell
$ sudo pip3 install --upgrade 'platypush[http,picamera,pca9685]'
```
Then create a simple configuration file under `~/.config/platypush/config.yaml`:
```yaml
backend.http:
# Listen port
port: 8008
backend.joystick.linux:
device: /dev/input/js0
camera.pi:
horizontal_flip: False
vertical_flip: False
resolution:
- 800
- 600
pwm.pca9685:
# PWM main frequency, in Hz
frequency: 500
# Default PWM channels to control.
# In this case, we have connected the ESCs
# to the first four channels on the board.
channels:
- 0
- 1
- 2
- 3
# Start streaming the camera feed on TCP port 5000
# when the application starts
event.hook.OnApplicationStarted:
if:
type: platypush.message.event.application.ApplicationStartedEvent
then:
- action: camera.pi.start_streaming
args:
listen_port: 5000
```
All the sections of the file should be relatively self-explanatory, but the PCA9685 configuration requires a deeper dive
in how PWM modulation works under the hood to be properly grasped - we'll dive into it very soon.
In the meantime, you should already be able to start the application through the `platypush` command
(use the `pi` user). If everything was installed and configured properly, after a while you should see the camera
sensor turn on - you can view the camera feed either through the
[RPi Camera Viewer](https://play.google.com/store/apps/details?id=ca.frozen.rpicameraviewer&hl=en_US&gl=US) app for
Android, or through VLC:
```shell
$ vlc tcp/h264://rpi-ip:5000
```
If you turn on and pair your Bluetooth controller you should also be able to see some events in the application log
when you press some controls on the device:
```
...
INFO|platypush|Received event: {"type": "event", "args": {"type": "platypush.message.event.joystick.JoystickConnectedEvent", "device": "/dev/input/js0", "name": "PG-SW021", "axes": ["x", "y", "z", "rz", "gas", "brake", "hat0x", "hat0y", "x", "y", "z", "rz", "gas", "brake", "hat0x", "hat0y", "x", "y", "z", "rz", "gas", "brake", "hat0x", "hat0y", "x", "y", "z", "rz", "gas", "brake", "hat0x", "hat0y", "x", "y", "z", "rz", "gas", "brake", "hat0x", "hat0y"], "buttons": ["a", "b", "c", "x", "y", "z", "tl", "tr", "tl2", "tr2", "select", "start", "mode", "thumbl", "thumbr", "a", "b", "c", "x", "y", "z", "tl", "tr", "tl2", "tr2", "select", "start", "mode", "thumbl", "thumbr", "a", "b", "c", "x", "y", "z", "tl", "tr", "tl2", "tr2", "select", "start", "mode", "thumbl", "thumbr", "a", "b", "c", "x", "y", "z", "tl", "tr", "tl2", "tr2", "select", "start", "mode", "thumbl", "thumbr", "a", "b", "c", "x", "y", "z", "tl", "tr", "tl2", "tr2", "select", "start", "mode", "thumbl", "thumbr"]}}
...
INFO|platypush|Received event: {"type": "event", "args": {"type": "platypush.message.event.joystick.JoystickAxisEvent", "device": "/dev/input/js0", "axis": "hat0x", "value": -1.0}}
INFO|platypush|Received event: {"type": "event", "args": {"type": "platypush.message.event.joystick.JoystickAxisEvent", "device": "/dev/input/js0", "axis": "hat0x", "value": 0.0}}
...
```
If you see these events it means that the joystick was successfully detected. We'll come back to these events in a bit
to see how to connect them to drone actions. It is now a good idea to configure Platypush as a startup service for the
`pi` user, so whenever a new user session starts the application will also be started. Get its absolute path:
```
$ which platypush
```
Then create a new systemd service file as `~/.config/systemd/user/platypush.service` with the following content:
```
[Unit]
Description=Platypush service
After=network.target bluetooth.target
[Service]
ExecStart=/path/to/platypush
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
```
Then reload the systemd daemon, start and enable the service:
```
$ systemctl --user daemon-reload
$ systemctl --user start platypush.service
$ systemctl --user enable platypush.service
```
It's now time to dive into the PWM internals to understand how to _program_ the controls for the motors through the
Raspberry Pi.
## Pulse-Width Modulation (PWM) explained
PWM is one of the most common ways of driving electric motors because it makes it easy to transmit analog values using
only one digital signal that can either be high or low, and it optimizes power consumption by applying a voltage to the
load only when a change to the output power is required. The _duration_ of the high part of the signal expresses the
value that you want to transmit, therefore the longer the duration of the high part of a signal, the higher the voltage
or current that will be transmitted to the load. Conceptually, PWM is somehow similar to popular radio modulation
technologies such as AM or FM, but instead of modulating information through the amplitude or the frequency of the
signal, it modulates it through the duration of a digital high signal.
For example, the figure below shows how to transmit a sinusoidal signal _B_ through a PWM train of pulses _V_. The
duration of each pulse expresses how much the output signal should go up (if the voltage is positive) or down (if the
voltage is negative) compared to the previous output value. The duration of a high pulse is also called _duty cycle_.
![PWM signal example](../img/pwm-1.png)
The most important metric to take into account when designing PWM systems is the base frequency of the modulation
process - in other words, how often the signal should be sampled. When driving motors that usually have a minimum and
maximum power, you may also want to specify the minimum and maximum _duty cycle_, i.e. the minimum and maximum duration
of a high pulse that will be respectively be mapped to the minimum and maximum output power. It is very important to
pick these values in such a way that the minimum value is greater than the sampling period (which is simply the inverse
of the sampling frequency). To be more specific, according to
[Nyquist-Shannon's sampling theorem](https://en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem), the
minimum frequency used to deliver actual information must be at least twice the sampling frequency. If that's not the
case then some pulses will not be detected, because the duration of the pulse may be shorter than the sampling period.
Most of the ESC controllers on the market have their own supported PWM configurations, and some advanced controllers may
even allow more complex controls over PWM - for example for controlling sounds, lights or supporting multiple motor
control paradigms. Therefore, the user manual of your ESC (or the chipset used by your ESC) is usually the best place
to start before writing the code that actually controls the motors.
In my case I have used some ESC devices with a
[BLHeli_32 ARM chipset](https://github.com/bitdump/BLHeli/tree/master/BLHeli_32%20ARM), a quite popular option for
mid-to-high tier quadcopters - and also a very versatile one, since it's basically a small programmable 32-bit ARM
microcontroller. The
[user manual](https://render.githubusercontent.com/view/pdf?color_mode=light&commit=746220a3da8b36a37b9388fec624bc50299ce974&enc_url=68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f62697464756d702f424c48656c692f373436323230613364613862333661333762393338386665633632346263353032393963653937342f424c48656c695f333225323041524d2f424c48656c695f33322532306d616e75616c25323041524d25323052657633322e782e706466&nwo=bitdump%2FBLHeli&path=BLHeli_32+ARM%2FBLHeli_32+manual+ARM+Rev32.x.pdf&repository_id=3813420&repository_type=Repository#c0f6bc3e-8870-4047-8084-2b0cafc3cfbd)
mentions support for a regular pulse width input between 1-2ms. We can pick 2ms as our sampling period, and
\(1/2ms = 500 Hz\) will therefore be our sampling frequency - configured in the Platypush `pwm.pca9685` plugin through
the `frequency` attribute. The manual also mentions support for other PWM configurations (such as OneShot125, OneShot42,
Multshot and Dshot) that usually rely on shorter pulses, but for sake of simplicity (and compatibility with other ESC
devices) we can stick to the regular 2ms sampling period.
Also, the driver of the PCA9685 controller automatically takes care of picking the right duty cycle range given
the frequency, and it maps the duty cycle to 16-bit integers between 0 and 65535. A duty cycle of zero means 0% of the
maximum power, a duty cycle of 0xffff means 100% of the power, a duty cycle of 0x7fff means 50% of the power. The
Platypush wrapper for the PCA9685 uses `min_duty_cycle=0` and `max_duty_cycle=0xffff` unless configured otherwise, but
this is probably not what you want for your drone. You usually want a higher minimum duty cycle because under a certain
threshold the power will be insufficient to even get the motors to spin. And, in most of the cases, you don't want to
have a maximum duty cycle that is 100% of the maximum power. If you have 35A ESC, that will draw 35A from the battery
and, most importantly, it will probably turn your drone into an uncontrollable killer machine. In a general use case you
may want to identify the minimum duty cycle associated to a minimal spin of the motors and pick that as the minimum duty
cycle, and identify the duty cycle associated to a lift-off at the desired speed and pick that as the maximum duty
cycle. We'll see how to use Platypush to calibrate this range through the joystick in a bit.
Finally, most of the ESC devices have an **arming** sequence. Its main purpose is to prevent unexpected currents or
accidental controls from suddenly spinning the motors when it's not desired/required. Therefore, before flying the drone
you need to actually send a special PWM sequence that _arms_ the ESC controllers, and sometimes you may need to send
another sequence to disarm them (this is usually achieved by bringing the power back to zero, and most of the ESC would
automatically disarm after a while if no further pulses are sent).
You should consult the user manual of your ESC to check the supported arming and disarming sequences. For examples,
the BLHeli_32 user manual describes the following procedure:
![BLHeli_32 arming sequence](../img/esc-pwm.png)
What this diagram says is that:
- The ESC should beep three times when it's powered on.
- Once the ESC is powered on, the arming sequence can be initiated by gradually increasing the voltage on the PWM
input from 0 to approximately 50% the maximum duty cycle, and then bringing the voltage down back to zero.
- When the input voltage hits the first low voltage threshold, the ESC emits a first low beep. When it goes back to
zero, the ESC emits a high beep.
- If you hear both the beeps it means that the arming sequence was correct, and you can start stepping up the voltage
to actually send power to the motors.
- If you only hear the low beep, it means that the arming sequence hasn't been successful. It usually means that the
pulse train was either too long or too short - its duration should be a multiple of the sampling period, but if it's
too much higher then the arming sequence will time out.
- The deactivation sequence simply consists in bringing the duty cycle to zero and de-initializing the PCA9685 driver.
Now that we have learned the theory of what PWM is, how to use it for signal transmission protocols and how to arm and
deactivate an ESC, it's time to get our hands dirty with some code.
## Coding the flight controller
Also, be aware: this is the part where you actually spin and test the motors. Once again, **do it without the
propellers**, and double check again that all the connections to the motors are properly set up and that the ESCs are
powered on before you try and send any signal.
Create a new script under `~/.config/platypush/scripts/drone.py`. This script will contain the automation to control
your drone through the joypad. We first need to code a function to arm the four ESC devices. If you are using a
BLHeli_32-based ESC (or any other ESC device with a double-ramp arming sequence) then you may want to code a function
that uses the `pwm.pca9685` plugin to write a zero to the PWM channel, wait a bit, write a value about half of the
maximum duty cycle, wait a bit, and write another zero. And we also need a function that resets the motors by bringing
the voltage to zero and resetting the PCA9685 driver:
```python
import logging
import time
from platypush.context import get_plugin
logger = logging.getLogger(__name__)
def arm_motors():
logger.info('Arming drone ESCs')
pwm = get_plugin('pwm.pca9685')
pwm.write(0)
time.sleep(0.5)
pwm.write(0x7fff)
time.sleep(0.5)
pwm.write(0)
time.sleep(0.5)
logger.info('Drone ESCs ready')
def reset_motors():
logger.info('Resetting drone ESCs')
pwm = get_plugin('pwm.pca9685')
pwm.write(0)
pwm.reset()
pwm.deinit()
logger.info('Drone ESCs reset')
```
The right time between the writes largely depends on the arming sequence supported by the ESC and it may be a bit of a
trial-and-error process. Many ESCs will emit a particular sequence of beeps or flash their LED in a particular way upon
successful arming sequence. Before you add more code or hook these events to joystick events, open a Python CLI from
the `~/.config/platypush` directory and give your logic a test:
```python
>>> from scripts.drone import arm_motors, reset_motors
>>> arm_motors()
# Arming drone ESCs
# Drone ESCs ready
>>> reset_motors()
# Resetting drone ESCs
# Drone ESCs reset
```
In this case we are not specifying the list of PWM channels to write: if a single value is specified on the `.write()`
method then the same value will be written to all the channels configured on the plugin, and we previously configured
the `pwm.pca9685` plugin to use the first four channels by default.
Once you have managed to arm and disable the ESCs, let's associate these functions to joypad events. For instance, we
can prepare the motors when the joypad connects to the drone and disable them when the joypad disconnects. This can
be easily done in the `drone.py` script by leveraging the Platypush
[`JoystickConnectedEvent`](https://docs.platypush.tech/platypush/events/joystick.html#platypush.message.event.joystick.JoystickConnectedEvent)
and [`JoystickDisconnectedEvent`](https://docs.platypush.tech/platypush/events/joystick.html#platypush.message.event.joystick.JoystickDisconnectedEvent):
```python
from platypush.event.hook import hook
from platypush.message.event.joystick import JoystickConnectedEvent, JoystickDisconnectedEvent
@hook(JoystickConnectedEvent)
def on_joystick_connected(**_):
arm_motors()
@hook(JoystickDisconnectedEvent)
def on_joystick_disconnected(**_):
reset_motors()
```
Now restart the service on the drone:
```shell
$ systemctl --user restart platypush
```
Then start/pair/connect the joypad. Upon connection, you should see a `JoystickConnectedEvent` on the logs and the ESCs
should automatically get armed. Now it's a good idea to set up a joystick button to trigger the `.reset_motors()`
and `.arm_motors()` methods. This will allow us to programmatically arm/disable the motors without having to
connect/disconnect the joypad. Press the joystick keys that you want to associate to the arm and reset actions while the
application is running and check the associated key code in the logs. For example, if you pick the _Select_ and _Start_
buttons on a PlayStation or XBox-like joypad:
```text
Received event: {"type": "event", "args": {"type": "platypush.message.event.joystick.JoystickButtonPressedEvent",
"device": "/dev/input/js0", "button": "select"}}
Received event: {"type": "event", "args": {"type": "platypush.message.event.joystick.JoystickButtonPressedEvent",
"device": "/dev/input/js0", "button": "start"}}
```
So create an event hook in your `drone.py` script that associates the `.arm_motors()` and `.reset_motors()` methods to
these buttons:
```python
from platypush.event.hook import hook
from platypush.message.event.joystick import JoystickButtonPressedEvent
@hook(JoystickButtonPressedEvent, button='start')
def on_start_button_pressed(**_):
arm_motors()
@hook(JoystickButtonPressedEvent, button='select')
def on_select_button_pressed(**_):
reset_motors()
```
Now pick two joystick buttons that you want to use to bring the motor power respectively up and down and log the current
duty cycle, and add event hooks for them. For example, during calibration I usually use the L2/R2 buttons on the back of
the joystick to respectively bring the power down or up:
```python
import logging
from platypush.context import get_plugin
from platypush.event.hook import hook
from platypush.message.event.joystick import JoystickButtonPressedEvent
logger = logging.getLogger(__name__)
# This defines how much we should take the motor power up or down on each step.
# Feel free to experiment different values for different levels of control.
step = 25
def power_down():
pwm = get_plugin('pwm.pca9685')
channels = {
i: value-step
for i, value in pwm.get_channels().output.items()
}
logger.info(f'Power down. Channel values: {channels}')
pwm.write(channels=channels)
def power_up():
pwm = get_plugin('pwm.pca9685')
channels = {
i: value+step
for i, value in pwm.get_channels().output.items()
}
logger.info('Power up. Channel values: {channels}')
pwm.write(channels=channels)
@hook(JoystickButtonPressedEvent, button='tl2')
def on_power_down_button_pressed(**_):
power_down()
@hook(JoystickButtonPressedEvent, button='tr2')
def on_power_up_button_pressed(**_):
power_up()
```
Now restart the service and arm the ESCs. Once armed, press R2 repeatedly to bring up the power provided to the motors.
At the beginning the motors might not move at all, but once passed a certain current threshold they will start spinning
lightly. Take note of the power configuration required to achieve a minimum level of spin on the motors, it should now
be reported on the application logs. This is going to be the `min_duty_cycle` on the `pwm.pca9685` plugin configuration,
since any lower currents won't cause any change to the speed of the motors. Some motors may also have an upper boundary
for the maximum current that could be lower than the maximum current that can be delivered by the ESCs. If that's the
case, then the motors will usually automatically stop. If you notice such behaviour while powering up the motors, then
configure the associated duty cycle as `max_duty_cycle`. For example, this configuration seems to work quite well for my
combination of motors and ESCs:
```yaml
pwm.pca9685:
frequency: 500
min_duty_cycle: 2000
max_duty_cycle: 4000
channels:
- 0
- 1
- 2
- 3
```
Now that we have some commands configured to arm and reset the motors, and some logic to increase or decrease the power,
we can proceed to calibrating the drone.
## Drone calibration
This is the moment of truth of the project: you can now set up the propellers on the motors and verify that your
creature can move (follow the previous instructions about the direction and the order of the blades).
However, before calling your friends to show off your new drone, you may consider calibrating the motors - and this
may be a quite time-intensive process.
In theory, sending the same current to two motors should cause the same torque on the body of the drone. However, many
factors (such as small production differences between individual motors, distribution of the weight on the frame or
different level of friction between the stator and the rotor) usually cause the motors of a drone to behave slightly
differently. If the torques of the motors are not perfectly balanced, then the drone will either spin around its body or
flip in the direction opposite to the one with the strongest torque. Therefore, you may want to add to your `drone.py`
script a mapping of the current offsets for each motor. A smaller value means that less current will be provided to
that motor compared to the others, a greater value is the other way around. For now let's initialize this table with
zeros:
```python
class Channel:
"""
Motor position to PWM channel index.
"""
TOP_LEFT = 0
TOP_RIGHT = 1
BOTTOM_LEFT = 2
BOTTOM_RIGHT = 3
offsets = {
Channel.TOP_LEFT: 0,
Channel.TOP_RIGHT: 0,
Channel.BOTTOM_LEFT: 0,
Channel.BOTTOM_RIGHT: 0,
}
```
Now let's wrap the `.arm_motors()` method into a `.warmup()` method that does the following:
1. Arms the ESCs.
2. Increases the current sent to the motors by gradually increasing the duty cycle to each of the ESCs until
`min_duty_cycle + offset[i]`. This will get the motors to rotate at low speed.
3. The joystick `start` button will call this `.warmup()` method instead of `.arm_motors()`.
The logic will look like this:
```python
import logging
from platypush.context import get_plugin
from platypush.event.hook import hook
from platypush.message.event.joystick import JoystickButtonPressedEvent
logger = logging.getLogger(__name__)
def warmup():
arm_motors()
logger.info('Starting the propellers!')
pwm = get_plugin('pwm.pca9685')
# Start from 50% of min_duty_cycle
channels = {
channel: int(0.5 * pwm.min_duty_cycle) + offset
for channel, offset in offsets.items()
}
pwm.write(channels=channels)
# Bring the power up to min_duty_cycle
channels = {
channel: pwm.min_duty_cycle + offset
for channel, offset in offsets.items()
}
pwm.write(channels=channels, step=15, step_duration=0.03)
@hook(JoystickButtonPressedEvent, button='start')
def on_start_button_pressed(**_):
warmup()
```
Now it's time to remind a few precautions:
- Make sure that there's enough room around the drone and that it's sitting on a large surface.
- Make sure that no obstacles or people are present in a radius of at least 2-3 meters from the drone.
- Remember that once the motors have the propellers attached and you start loading a lot of current into them, whatever
you attached to them will start moving. But you haven't calibrated the motors yet, so you _don't know_ yet in which
direction the drone will move. Behave accordingly.
With your drone sitting in a safe spot, it's time to restart Platypush, trigger the `.warmup()` function and start
powering juice to the motors. At some point you'll hopefully start seeing the drone hover and slightly move in some
direction. This is the moment where you should pay most of the attention, for two reasons:
1. You know have an uncalibrated hovering device with blades that spin hundreds of times a second, and you
know the rules: as soon as anything goes wrong, _hit that `.reset_motors()` button_!
2. It's very likely that the drone won't simply move up straight from the floor at this stage, because some motors may
generate a lower or higher torque than others.
3. Unless you have been very lucky on the point above, the first time you try and fly the drone it'll move in some
random direction. Therefore don't provide too much power to the motors - you don't want to fly it just yet, you
just want to let it hover a bit to see how the forces are balanced.
Regarding the second point, pay attention to how the drone moves in order to understand how to calibrate it:
1. If it spins clockwise, then the torque from the anti-clockwise motors is stronger than the torque from the motors
that spin clockwise. Slightly decrease the offsets of `TOP_RIGHT` and/or `BOTTOM_LEFT`. The opposite applies if the
drone spins clockwise.
2. If it spins clockwise/anti-clockwise, but the rotation axis is closer to a motor instead of the center of the drone,
then the motor closer to the rotation axis generates a lower torque: increase its offset value.
3. If it tilts towards the left, then the torque generated by the motors on the right side is stronger: reduce the
offsets values of `TOP_RIGHT` and `BOTTOM_RIGHT`. The opposite applies if it tilts on the left side.
4. If it tilts towards the front, then the torque generated by the motors on the back is stronger: reduce the
offsets values of `BOTTOM_LEFT` and `BOTTOM_RIGHT`. The opposite applies if it tilts towards the back.
Observe how the balance of the forces affects the movement of the drone, adjust the offset values accordingly, restart
the service. Repeat this procedure until you get the drone to lift it more or less on a straight line once a sufficient
level of power has been reached.
Once you have found the combination of power offsets that causes your drone to lift along a straight line, reduce the
power pumped to the motors until the drone stabilizes into a hovering position. Take note of the values being sent to
the ESCs when static hovering is achieved, they will be reported on the application logs as `Power up/down.
Channel values: <values here>` lines. These will be the values needed to achieve static thrust. Save them in your
`drone.py` script, we'll use them later as a reference to control the movement of the drone while it's flying:
```python
static_thrusts = {
Channel.TOP_LEFT: ...,
Channel.TOP_RIGHT: ...,
Channel.BOTTOM_LEFT: ...,
Channel.BOTTOM_RIGHT: ...
}
```
Now that the most difficult part of the project (getting the drone off the ground on a straight line) is done, it's time
to move to programming the logic to actually move your drone while it's in the air.
## The mechanics of flight control
Once it's hovering above the ground, you can picture a drone as a body that can move in four possible ways. It can
go up/down depending on the power supplied to the motors, or it can rotate around its two horizontal axes (_x_ and _y_)
or around the vertical axis (_z_).
1. The movement of a drone up or down along the vertical axis is called **throttle**.
2. The rotation of a drone around the _x_ axis is called **roll**.
3. The rotation of a drone around the _y_ axis is called **pitch**.
4. The rotation of a drone around the _z_ axis is called **yaw**.
![Pitch, roll and yaw schematics (credits: https://www.researchgate.net/publication/329392693_Autonomous_Person_Detection_and_Tracking_Framework_Using_Unmanned_Aerial_Vehicles_UAVs)](../img/pitch-roll-yaw-1.png)
To regulate each of these movements from a stationary position:
1. _Throttle_: simply increase/decrease by the same offset the power provided to each of the motors. Higher offsets
result in the drone moving up, lower values result in the drone moving down.
2. _Roll_: you can use this movement to move the drone laterally without rotating its front-facing side. Slightly
increase the power supplied the motors on the opposite side of the desired direction and slightly decrease the
power supplied to the motors on the other side. So moving the drone to the left is achieved by increasing the power
to the `TOP_RIGHT` and `BOTTOM_RIGHT` motors and decreasing the power to the `TOP_LEFT` and `BOTTOM_LEFT` motors,
and the other way around if you want to move the drone to the right.
3. _Pitch_: you can use this movement to move the drone forward or backwards. Again, slightly increase the power
supplied the motors on the opposite side of the desired direction and slightly decrease the power supplied to the
motors on the other side. So moving the drone forward is achieved by increasing the power to the `BOTTOM_LEFT`
and `BOTTOM_RIGHT` motors and decreasing the power to the `TOP_LEFT` and `TOP_RIGHT` motors, and the other way around
if you want to move the drone backwards.
4. _Yaw_: you can use this movement to rotate the drone around the vertical axis without changing its inclination. This
is particularly used when you want to take 360 degrees pictures or videos of the area around the drone. This movement
is achieved by increasing the power supplied to the motors that rotate in the opposite direction as the desired
direction, and decreasing the power supplied to the other two motors. So rotating the drone clockwise is achieved by
increasing the power to the `TOP_RIGHT` and `BOTTOM_LEFT` motors and decreasing the power to the `TOP_LEFT` and
`BOTTOM_RIGHT` motors, and the other way around if you want to rotate the drone anti-clockwise.
## Controlling the movement through the joystick
Now that we have an understanding of how to get the drone to move in any direction, it's time to translate our
understanding into code by mapping these movements to joystick controls. Most of the controllers available for the
commercial drones use this configuration:
![Common mapping between joystick controls and drone movements (credits: https://smaccmpilot.org/hardware/rc-controller.html)](../img/pitch-roll-yaw-2.png)
I'll assume that your joystick has analog axis controllers, since it makes things more consistent with the common
conventions use by the drones on the market.
First, let's write a `.move()` method to move the drone in each of the desired directions. For simplicity, we'll assume
that the drone can do one movement at the time (e.g. it can't climb and yaw, or roll and pitch, at the same time), but
the code can easily be extended with more complex logic:
```python
from enum import IntEnum
from logging import getLogger
from threading import RLock
from platypush.context import get_plugin
class Movement(IntEnum):
THROTTLE = 0
PITCH = 1
ROLL = 2
YAW = 3
class Channel:
TOP_LEFT = 0
TOP_RIGHT = 1
BOTTOM_LEFT = 2
BOTTOM_RIGHT = 3
static_thrusts = {
Channel.TOP_LEFT: ...,
Channel.TOP_RIGHT: ...,
Channel.BOTTOM_LEFT: ...,
Channel.BOTTOM_RIGHT: ...,
}
movement_lock = RLock()
logger = getLogger(__name__)
# Base step offset for "smoothening" the PWM changes
step = 25
# Duration of each step pulse during the transients
step_duration = 0.03
# Maximum percentage of change. You can calibrate this
# value to achieve less abrupt rotations of the drone
# around its axes
max_percent = 0.5
def normalize_thrust(channel: int, percent: float) -> int:
"""
Converts a percent change to one of the channels into a
duty cycle integer that can be sent to the PWM controller.
The base value for all the percentage changes is always the
static thrust value for a given channel.
"""
pwm = get_plugin('pwm.pca9685')
percent *= max_percent
static_thrust = static_thrusts[channel]
percent = max(-1., min(1., percent))
thrust = static_thrust * (1 + percent)
return int(
max(pwm.min_duty_cycle, min(thrust, pwm.max_duty_cycle))
)
def move(movement: Movement, percent: float):
"""
:param movement: Direction for the movement.
:param percent: Percentage of change compared to the
static_thrusts configuration, must be between -1.0 and 1.0.
"""
channel_values = static_thrusts.copy()
if movement == Movement.THROTTLE:
channel_values = {
channel: normalize_thrust(channel, percent=percent)
for channel in channel_values.keys()
}
elif movement == Movement.ROLL:
channel_values = {
channel: normalize_thrust(
channel, percent=percent/2
if channel in [Channel.TOP_LEFT, Channel.BOTTOM_LEFT]
else -percent/2
)
for channel in channel_values.keys()
}
elif movement == Movement.PITCH:
channel_values = {
channel: normalize_thrust(
channel, percent=percent/2
if channel in [Channel.BOTTOM_LEFT, Channel.BOTTOM_RIGHT]
else -percent/2
)
for channel in channel_values.keys()
}
elif movement == Movement.YAW:
channel_values = {
channel: normalize_thrust(
channel, percent=percent/2
if channel in [Channel.TOP_RIGHT, Channel.BOTTOM_LEFT]
else -percent/2
)
for channel in channel_values.keys()
}
# Ensure that only one thread at the time can control
# the current motion of the drone
with movement_lock:
logger.info(f'Moving drone: movement={movement.name}, percent={percent}')
pwm = get_plugin('pwm.pca9685')
pwm.write(channels=channel_values, step=step, step_duration=step_duration)
```
You'll notice that in this snippet we have managed the percentage of change in a balanced way between the motors.
For instance, a +100% pitch change (meaning drone that moves forward at the maximum inclination) is achieved by
increasing the power that goes to the two motors on the back by 50% compared to their static thrust values, and the
power supplied to the motors on the front is decreased by 50% instead. This makes sure that even when the drone is
moving the power supplied to the motors remains more or less constant, because while the speed of a pair of motors
increases, the speed of the other pair always decreases. Calibrating the `max_percent` value can also help you achieve
less abrupt movements - if the drones rotates too much around a certain axis it may lose balance and start falling.
Now that we have a function that can easily translate the desired percentage of change along a certain direction into
the appropriate PWM values for the motor channels, let's connect them to our joystick controls. With Platypush running
and the controller connected, move the analog controls on your joystick and check the lines logged by the application.
You should see events like this:
```
Received event: {
"type": "event",
"args": {
"type": "platypush.message.event.joystick.JoystickAxisEvent",
"device": "/dev/input/js0",
"axis": "x",
"value": 0.5
}
}
```
A joystick's analog controls are usually mapped to four axes: the left control's horizontal and vertical axes are
respectively mapped to _x_ and _y_, and the right control's horizontal and vertical axes are respectively mapped to
_z_ and _rz_. From the previous drone controller schema, we know that the _x_ axis should be associated to the yaw
movement, the _y_ axis is associated to the throttle (up/down) movement, the _z_ axis is associated to the roll movement
and the _rz_ axis is associated to the pitch movement. Platypush already outputs events with values between -1 and 1,
where 0 represents the middle point (analog control in "rest" position), -1 represents the control being at the leftmost
position (or bottom if it's a vertical axis) and 1 represents the control being at the rightmost position (or top if
it's a vertical axis). With these considerations in mind, we can easily write an event hook that translates a joystick
event into a drone action:
```python
from platypush.event.hook import hook
from platypush.message.event.joystick import JoystickAxisEvent
@hook(JoystickAxisEvent)
def on_joystick_axis_event(event: JoystickAxisEvent, **_):
movement = None
if event.axis == 'x':
movement = Movement.YAW
elif event.axis == 'y':
movement = Movement.THROTTLE
elif event.axis == 'z':
movement = Movement.ROLL
elif event.axis == 'rz':
movement = Movement.PITCH
if movement:
move(movement, percent=event.value)
```
Now restart Platypush, make sure that the controller is paired, lift off and start moving your drone around. If you have
made it so far, congratulations on building and flying drone from scratch!
# Conclusions
By the end of this article you have learned how to design, build and fly your drone from scratch, but many improvements
can be applied to the base model we've put together here. Some ideas:
2021-08-30 00:06:35 +02:00
- Integrate a gyroscope to detect whether the drone "leans" too much in a direction other than the expected one,
and automatically adjust the thrusts to keep the drone stable.
- Move from a Bluetooth or IR joystick to an RC (radio-controlled) one, so you can control your drone over longer
ranges. If the RC controller is mapped as a standard joystick on Linux then all the code we've written for the
controller shouldn't require any changes.
- Add a GPS module (Platypush provides support for any GPS device supported by `gpsd`) and a 3G/4G module to get the
location of the drone and communicate with it even when it's not connected to your Wi-Fi network.
- Make a command-line script or a small UI application to control the drone from your computer or phone. The application
can still call the `.move()` method we have defined previously to control the vehicle.
Good hacking!