This is only a preview of the July 2026 issue of Practical Electronics. You can view 0 of the 84 pages in the full issue. Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
Articles in this series:
|
Max’s Cool Beans
By Max the Magnificent
Weird & Wonderful
Arduino Projects
Part 19: Marble mayhem and tilting tomfoolery
I
t never ceases to amaze me how
often I fall into the trap of thinking something is going to be easypeasy lemon squeezy to explain, only
to discover it quickly transmogrifies
itself into a case of stressed, depressed
lemon zest.
You only discover what you don’t
know when you try to explain something you think you know to someone else; several well-known quotes
capture this concept. For example, “If
you can’t explain it simply, you don’t
understand it” is commonly attributed
to Albert Einstein. Likewise, Richard
Feynman once observed, “You don’t
really understand something until you
try to teach it”.
Sad to relate, this is the position I
currently find myself in. As you may
recall, last month we said we would
add an inertial measurement unit
(IMU) to our retro games console.
Although we’re exploring all this
in the context of our little console,
don’t be fooled into thinking it’s a
one-trick pony.
The ability to detect tilt, orientation, and motion turns up in all sorts
of applications, from balancing robots
and camera stabilisers to gesture-
controlled gadgets and industrial
monitoring systems. So, while our
immediate goals may be modest, the
underlying techniques are anything
but frivolous.
We can use our IMU to detect whether we are tilting the console left or
right, and/or tilting it forward or backward. We will then be perfectly poised
to employ this information to control whatever game we happen to be
playing at the time (assuming such
control is appropriate for the game
in question).
This all hinges on what we mean
by “forward”, “backward”, “left” and
“right.” It would be nice if we were all
singing from the same hymn sheet. It
also depends on how the IMU interprets those directions.
front and a back. In this case, terms
like bow (front), stern (back), port
(left when looking toward the front),
and starboard (right when looking
toward the front) make perfect sense
because they are defined relative to
the vessel itself.
If we are talking about something
else, like a landscape, there is no inherent front or back. Those notions
depend entirely on the observer. In
such cases, we must impose an external frame of reference.
Once we’ve defined our up–down
and front–back axes, we can establish our left–right axis. In this case,
“left” lies to the left of the up–down
axis when we are looking toward the
No, your left, stupid!
front (the assignment of “right” is left
Our first task is to define three as an exercise for the reader).
axes that are perpendicular (at right
It’s common to associate the letangles) to each other, as depicted in ters X, Y and Z with these axes, but
Fig.1(a). We start by assigning names which letter should go with which
to them.
axis? Sadly, there’s no universally
Our lives are made easier if we agreed answer. Different disciplines
assume we are working in the pres- have evolved their own conventions
ence of a gravitational field, because over time. In mathematics, X is typithis allows us to immediately define cally left–right, Y is up–down, and Z
an up–down axis, with the “down” represents depth (into and out of the
direction pointing toward the gravi- page or screen, for example).
tational source and the “up” direction
In robotics, X often points forward, Y
pointing away from it.
to the left, and Z upward. Meanwhile,
If we are talking about an object the practitioners of computer graphlike a boat, we can also say it has a ics seem to delight in variety, with
some systems using Y as the vertical
axis and others preferring Z—just to
Yaw
Yaw
Axis
Yaw
Axis
Axis
Up
Up (+Z)
Up(+Z)
(+Z)
Up
Up Up
Up
Up(+Z)
(+Z)
Up (+Z)
keep everyone on their toes.
For reasons that will become apRoll
RollAxis
Axis
Roll Axis
Pitch
Axis
PitchPitch
Axis Axis
Left
Left Left
Front
(+Y)
FrontFront
(+Y) (+Y)
Front
FrontFront Left
Left (-X)
Left(-X)
(-X)
Front
FrontFront
(+Y)
(+Y) (+Y) parent in a moment, we will associate
Left
Left(-X)
(-X)
Left (-X)
X with left–right, Y with front–back
and Z with up–down, as depicted in
Fig.1(b).
Back
BackBack
Right
RightRight Back
(-Y)
Right
(+X)
BackBack
(-Y) (-Y)
RightRight
(+X) (+X)
We have associated increasing (+ve)
Right
(+X)
RightRight
(+X) (+X)
Back
(-Y)
BackBack
(-Y) (-Y)
Z values with up and decreasing (−ve)
Down
(-Z)
Down
Down
(-Z) (-Z)
Down
Down
Down
values with down. Similarly, increasDown
Down
Down
(-Z)
(-Z) (-Z)
(a)
(a)Define
Define
(a) Define
world
worldworld
spatial
spatial
spatial
axes
axesaxes
(b)Assign
Assign
(b) Assign
X,Y,
Y,Z
X,
Zaxes
Y,
axes
Z and
axes
and and (c)
(b)
X,
(c) Define
rotation
directions
(c)Define
Define
rotation
rotation
directions
directions
and
and and ing (+ve) X values correspond to the
(up-down,
(up-down,
(up-down,
front-back,
front-back,
front-back,
left-right)
left-right)
left-right)
define
define
signconventions
sign
conventions
conventions
define
sign
sign
conventions
sign
signconventions
conventions
right and decreasing (−ve) values to the
left, while increasing (+ve) Y values
Fig.1: definitions and conventions for our IMU’s axes and rotations.
48
Practical Electronics | July | 2026
Left (-X)
+
Roll Axis
Front (+Y)
-
Back (-Y)
correspond to the front and decreasing (−v) values to the back.
These assignments are not universal;
different domains have adopted different conventions. In many cases, they
have simply chosen the convention that
works best for the task at hand, which
is exactly what we are doing here.
The terms ‘roll’, ‘pitch’ and ‘yaw’
refer to rotation around the front-back,
left-right, and up-down axes, respectively, as illustrated in Fig.1(c). Once
again, the signs of these rotations (is
clockwise +ve or -ve?) depend on the
domain in which we are working. Just
to increase the fun and frivolity, we
also need to define what we mean by
‘clockwise’ (ie, what frame of reference we’re using).
And then things get complicated
To cut a long story short (which is
the opposite of how I usually like to
do things), the conventions we will
use for our retro games console are
shown in Fig.2.
Previously, we established the use of
the X and Y axes in the context of our
14 × 10 tricolor light-emitting diode
(LED) array. In this case, X spans the
width of the array, while Y represents
its height. This is why we have chosen
to associate X and Y with the left–right
and front–back axes, respectively, in
the context of our console.
Similarly, the way we reference the
array, with the origin at (0, 0) in the
lower left-hand corner and (13, 9) in
the upper right-hand corner, explains
why we have decided to associate
positive (+ve) values with front and
right, and negative (−ve) values with
back and left.
This leaves the Z-axis, which points
out of the console and into the third
dimension (always a handy place to
hide things). We won’t be worrying
about rotations about the Z-axis in
the context of our console for the foreseeable future, so we’ll largely omit
Practical Electronics | July | 2026
+
-
Fig.2: how these
conventions will be
employed with the
Games Console.
Pitch Axis
Right (+X)
it from our discussions.
When it comes to tilting the console,
a tilt to the right (left side up, right
side down) corresponds to a roll (rotation about the Y-axis). In our case, we
shall define clockwise rotation (when
viewed from the back looking toward
the front along this axis) as positive
because we have to pick something,
and this makes sense to me.
Similarly, a tilt forward (back end
up, front end down) corresponds to a
pitch; rotation about the X-axis. In this
case, we shall again define clockwise
rotation as positive, but now we are
viewing things from the right looking
toward the left along this axis.
I know this is probably making your
head hurt (it’s certainly not doing
mine any good), but wrapping our
brains around this now will make
things much easier in the not-toodistant future.
One question you may have is why
we say that clockwise is defined when
looking from back to front (−ve to +ve)
along the Y-axis, while also saying it
is defined when looking from right
to left (+ve to −ve) along the X-axis.
I’m glad to see that you’re paying attention. This has to do with how I’m
visualising things in my head (which
is the best place to visualise things,
in my experience).
Suppose we were to place a physical glass marble on top of our console
and tilt the console to the right (right
side down). In this case, our marble
will roll toward the right. Since I’m sitting at the back of the console looking
toward the front, I naturally perceive
this as a clockwise rotation.
If we tilt the console to the right,
we will create our code such that a
virtual marble ‘rolls’ to the right by
keeping the value of Y constant and
increasing the value of X. Since we’re
making X more positive, it intuitively
makes sense (at least, to me) that we
should consider this clockwise rota-
tion to be positive.
If we say that a clockwise rotation
is positive when we perform a roll
about the Y-axis, it makes sense to
adopt a consistent convention when
we perform a pitch about the X-axis.
If we tilt the console forward (front
end down), we will make our virtual
marble roll toward the front by keeping the value of X constant and increasing the value of Y.
Since we are increasing the value
of Y, and since we are associating
this motion with a positive rotation,
then it follows that this motion must
appear as a positive (clockwise) rotation when viewed along the X-axis.
For this to appear clockwise, we
must be looking along the X-axis from
right to left (that is, from +X toward
−X). If we were to look in the opposite direction, the same motion would
appear anti-clockwise and our sign
convention would be broken.
And people wonder why I drink.
Reconciling multiple realities
Let’s assume we are going to use an
IMU that boasts a 3-axis gyroscope (to
measure rotation), flaunts a 3-axis accelerometer (to sense gravity and acceleration) and sports a 3-axis magnetometer (to determine orientation).
Each type of sensor has its own set
of concerns. Gyroscopes drift over
time (bias integration error), accelerometers are noisy and get confused by
motion vs gravity, and magnetometers
are easily disturbed by nearby magnets, metal and electronics.
As a result, we are faced with multiple ‘truths’ (“The truth is rarely pure
and never simple” — Oscar Wilde). The
gyroscope says, “I’m rotating smoothly” (while quietly drifting over time),
the accelerometer says, “gravity is over
here” (except when you’re accelerating), while the magnetometer insists
that “north is… well… somewhere in
that general direction”.
By combining data from the gyroscopes, accelerometers, and magnetometers, and continuously crosschecking them against one another,
advanced sensor fusion algorithms
can compensate for the weaknesses
of each sensor and provide a much
more stable and reliable estimate of
orientation and motion than any one
sensor could achieve on its own.
You can perform sensor fusion on
your own, assuming you’re happy to
juggle Euler angles, wrestle with quaternions, wrangle rotation matrices,
normalise vectors, transform coordinates and face the ever-present spectre of numerical drift. Alternatively—
and this is the option I prefer—you
can get someone else to do it for you.
49
7
~6
~5
4
~3
2
TX-1
RX-0
AREF
GND
13
12
~11
~10
~9
8
DIGITAL IN/OUT (PWM ~)
USB
Orange LED
Green LED
Setting up the hardware
Before we integrate the BNO055
breakout board into our console, we’ll
begin by experimenting with it in isolation. This will let us get up to speed
with its capabilities, understand its
quirks, and iron out any problems in
a simple setup before introducing any
50
Fig.3:
attaching
the IMU to
an Arduino
using a
breadboard
for testing.
added complexity to the full system.
Since my IMU already had its header
pins soldered in, I plugged it into a
breadboard and then connected the
breadboard to a spare Arduino Uno I
had lying around, as in Fig.3.
The BNO055 sensor communicates
with the outside world using a simple
two-wire serial bus interface called
I²C. One wire carries the clock signal
(called SCL), which keeps everything
in step, while the other carries the
data (called SDA).
One device—usually a microcontroller like an Arduino Uno—acts as
the boss and controls the conversation with any other devices (“targets”)
on the bus. These additional devices
can include things like sensors, displays and memory chips. On both the
Uno and Nano, analog pins A4 and A5
serve as the I²C SDA and SCL signals,
respectively.
Each device has its own address,
rather like a house number. When the
Arduino wants to talk to a particular device, it first sends that device’s
ANALOG IN
3.3V
5V
GND
GND
Vin
A0
A1
A2
A3
A4
A5
POWER
GND
SDA
SCL
Power
Jack
Vin
As we noted last month, there are
various cheap-and-cheerful IMUs
on the market. Although it’s not the
bargain-basement option, I do have a
soft spot for the 9DOF Absolute Orientation IMU Fusion Breakout board
from Adafruit (www.amazon.co.uk/
dp/B017PEIGIG) – see Photo 1.
There are three reasons why I like
this little beauty, which is based on
the Bosch BNO055 micro-electromechanical system (MEMS) sensor:
1. The BNO055 includes a 32-bit microcontroller unit (MCU), which the
nice guys and gals at Bosch have
already programmed to perform
sensor fusion.
2. The chaps and chapesses at Adafruit
have done their usual outstanding job
creating and providing wiring information, example code, and luscious
libraries to get us up and running
(www.adafruit.com/product/2472).
3. I already have one of these little
scamps lurking in the depths of my
treasure chest of “Bits and pieces
that will undoubtedly come in useful
one day if I only wait long enough”.
Can you guess how the BNO055’s
creators decided to handle all the
X-Y-Z and pitch-roll-yaw stuff? I don’t
have a clue. We could look at the data
sheet, but what would be the fun in
that? (I also have a sneaking suspicion that attempting to work things
out from the datasheet would cause
my brain to leak out of my ears.)
After all, how the IMU’s X-Y-Z axes
ultimately map onto the console’s
X-Y-Z axes will depend on how we
end up physically mounting the IMU
in (or on) the console. So, on this
basis, I think our best bet is to adopt
the time-honoured engineering approach of “suck it and see”.
IOREF
Meet our IMU
RESET
ATmega328P
Photo 1:
Adafruit’s BNO055 breakout board.
address. All the devices listen, but
only the one with the matching address responds. Thus, many devices
can share the same two wires, which
saves a lot of pins and wiring. That’s
why I²C is so popular in small electronic systems.
It’s important to note that the I²C
bus requires a pull-up resistor on each
of its wires, but these are included
on the BNO055 breakout board, so
that’s one less thing for us to worry
about (phew!).
Setting up the software
On the software side, the first thing
we need to do is install the libraries
required to run our BNO055 sensor (if
you use a different IMU, you’ll need
to load its associated libraries).
Launch the Arduino IDE, then use
the Tools → Manage Libraries menu
commands to access the Library Manager. Search for “Adafruit BusIO” and
install it. This is a low-level helper
library that many Adafruit sensor libraries depend on.
Practical Electronics | July | 2026
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>
#include <utility/imumaths.h>
Adafruit_BNO055 IMU = Adafruit_BNO055(55);
void setup()
{
Serial.begin(115200);
while (!Serial) {}
Serial.println("BNO055 orientation test");
Serial.println("Move the breadboard, watch which values change.");
Serial.println();
if (!IMU.begin()) {
Serial.println("Ooops, no BNO055 detected ... check wiring!");
while (1) {}
}
delay(1000);
IMU.setExtCrystalUse(true);
}
void loop()
{
sensors_event_t event;
IMU.getEvent(&event);
Serial.print("IMU.x = ");
Serial.print(event.orientation.x, 2);
Serial.print("\tIMU.y = ");
Serial.print(event.orientation.y, 2);
Serial.print("\tIMU.z = ");
Serial.println(event.orientation.z, 2);
delay(100);
}
Listing 1(a): our first IMU test program.
Next, search for “Adafruit Unified
Sensor” and install that. This provides
the common sensor framework that
the BNO055 library will use.
Finally, search for “Adafruit
BNO055” and install that library last.
This is the actual sensor driver for
the BNO055.
At this point, I suggest you exit and
relaunch the IDE because the Arduino IDE doesn’t always immediately
recognise newly installed libraries.
Restarting saves you from mysterious “file not found” errors later on.
You’re welcome.
Coding our first test
The code for our first test is essentially a copy of the example code on
Adafruit’s website. This is a nice,
simple “Is the sensor alive and what
happens when I tilt it?” test program.
You can find the version we’re using
in the file named CB-jul26-code-01.txt
(as usual, all files mentioned in this
column are in the July 2026 download package from the PE website at
https://pemag.au/link/acbx).
This code is shown in Listing 1(a).
We’re using the convention that the
listing number (1 in this case) corresponds to the numerical part of the
matching code file name (“01” here).
Since much of this is new to us, it’s
Practical Electronics | July | 2026
worth looking at it in detail. We start
by including several libraries. On line
1, we include <Wire.h>, which is the
Arduino’s built-in I²C library.
On line 2, we include <Adafruit_
Sensor.h>, which is Adafruit’s common
sensor framework. This provides standard data structures and helper functions used by many sensors.
On line 3, we include <Adafruit_
BNO055.h>, which is the actual library for the BNO055. This provides
us with the sensor-specific functions
we’ll use later, like IMU.begin() and
IMU.getEvent().
On line 4, we include <utility/
imumaths.h>, which provides support
for the vector and math types used by
the BNO055 library.
On line 6, we create our IMU object,
which we’ve cleverly called “IMU”
(don’t worry about the number 55,
which is just an identifier used internally by Adafruit’s framework).
On line 10, in the setup() function, we set the serial port running
at 115,200 baud (think “bits per
second”). This is much higher than
the value of 9600 we usually use because we’re going to stream multiple
sensor readings several times per
second. The higher data rate keeps
the Serial Monitor updated smoothly and helps avoid bottlenecks, such
as data queuing faster than it can be
displayed.
The statement on line 11 waits until
the serial connection is ready for use.
This matters more on some boards
than on an Uno, but it’s harmless and
good practice to include it.
Lines 13, 14 and 15 are used to print
a heading to the Serial Monitor. This
is the message we’ll see when we open
the monitor.
Line 17 attempts to start the BNO055.
The library checks whether the sensor
is connected and responds over the
I²C bus. If the initialisation fails, we
print an error message (line 18) and
then pause forever (line 19). That
way, we won’t confuse ourselves by
displaying nonsense readings.
Assuming everything has passed
thus far, we pause for a second (line
22) to give the sensor time to settle.
Next, we tell the sensor to switch over
to using the crystal oscillator on the
breakout board as its clock (line 23).
This improves timing stability and
makes the sensor’s readings more reliable.
Now let’s turn our attention to the
loop() function. This runs repeatedly
forever. Each time around the loop,
we ask the sensor for its orientation,
print its responses, and wait for 1/10th
of a second (100ms).
On line 28, we create a variable
called event. This is of type sensors_event_t, which is defined by
the BNO055 library. Think of this as
a little container that will hold data
from the sensor. On line 29, we use
IMU.getEvent() to fetch the current
orientation readings from the sensor
and store them in our event variable.
On lines 31-36, we print the
orientation.x, .y, and .z values stored
in our event variable. These are floating-point values, meaning they can
include both whole-number and fractional parts. The \t portions of these
statements insert tabs to make the
columns line up nicely, while the 2
values tell the Arduino to print the
floating-point numbers with two digits
after the decimal point.
Running our first test
Load this program into your Arduino. It will start running automatically. Open the Serial Monitor window
and make sure it’s set to 115200 baud,
matching the value in our code.
In my case, I had everything arranged as shown in Fig.3. I was sitting at my desk with the breadboard
directly in front of me. Once the program was up and running, I tilted my
breadboard about 45° to the left, then
45° to the right, then 45° forward, and
finally 45° backward. The first few
51
lines from my run look like the following (note that I’ve chopped a lot
of stuff out).
BNO055 orientation test
Move the breadboard, watch
which values change.
IMU.x = 359.25 IMU.y = -4.25 IMU.z = 1.44
IMU.x = 358.94 IMU.y = -10.13 IMU.z = 0.69
These two lines correspond to tilting
the breadboard to the left. From this,
you might assume that working with
these values would be a piece of cake.
You’d be mistaken. If you look at all
the results from my first run (see the
file named CB-jul26-results-01.txt), it
quickly becomes apparent that nothing makes sense (at least, not to the
untrained eye).
Try experimenting with tilting your
own breadboard and see if you can
work out what’s going on.
Part of the problem stems from the
fact that the orientation.x, .y, and .z
values are presented as Euler angles.
These are named after the 18th-century
Swiss polymath (and mathematic fanatic) Leonhard Euler. Unfortunately,
these values are interrelated in nonobvious ways.
You know how, with ordinary numbers, you can swap the order of the
operands in a multiplication and the
result remains the same (for example, 6 × 3 = 3 × 6)? This is known as
the commutative property. Well, that
doesn’t apply to matrices.
Although we want to think of Euler
angles as three separate rotations,
what’s really going on is a sequence of
matrix multiplications. Since matrix
multiplication is not commutative,
the order of these rotations matters.
As a result, first tilting to the left
and then tilting forward will give a
different set of Euler angles than first
tilting forward and then tilting to the
left. And from here on, things start to
get… interesting.
Adding gravity vectors
Although Euler orientation values
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
imu::Vector<3> grav = IMU.getVector(Adafruit_BNO055::VECTOR_GRAVITY);
Serial.print(event.orientation.x, 2);
Serial.print(event.orientation.y, 2);
Serial.print(event.orientation.z, 2);
Serial.print(grav.x(), 2);
Serial.print(grav.y(), 2);
Serial.println(grav.z(), 2);
delay(100);
}
BNO055 tilt test
Tilt left-right and front-back
and watch what changes.
EulerX EulerY EulerZ GravX GravY GravZ
1.44 -43.06 3.63 -6.69 -0.45 7.14
1.44 -43.06 3.63 -6.69 -0.45 7.14
These two lines correspond to tilting
the breadboard to the left. As before,
I’ve chopped a lot of stuff out.
Although this may look a little
messy, if you examine the results
from the full run (see the file named
CB-jul26-results-02.txt), it becomes
much easier to see what’s going on.
Tilting the board about 45° to the left
yields GravX values of around -6.9,
while tilting it about 45° to the right
produces GravX values of around
+6.4. In both cases, GravY remains
close to zero.
By comparison, tilting the board
about 45° forward returns GravY values
of around +7.0, while tilting it about
45° backward produces GravY values
of around -7.4. In both these cases,
GravX remains close to zero.
All this tells us that the GravX and
GravY values from our sensor correspond to the X and Y axes we’ve
adopted for our console, respectively. Even better, the signs of the GravX
and GravY values map directly onto
those we need for our console. What
are the odds? (We could, of course,
have corrected any mismatches under
program control, but it’s nice not to
have to do so.)
Meanwhile, the GravZ values remain
positive and relatively large throughout the entire run. From this, we can
deduce that GravZ represents the vertical component of gravity passing
through the board. When the breadboard is lying flat and face-up, GravZ
is close to +9.8. As we tilt the board
by about 45°, some of that gravity is
diverted into the X or Y direction, so
GravZ falls to around +7.
If we were to hold the board vertically, GravZ would move toward zero,
while flipping the board upside down
would cause GravZ to go negative.
Converting to angles
void loop()
{
sensors_event_t event;
IMU.getEvent(&event);
Listing 2(a): our second IMU test program.
52
are useful for all sorts of things, they
are not ideally suited for our purposes here—that is, detecting the tilt
angles of our console. Fortunately,
the BNO055 can also produce a gravity vector with .x, .y, and .z components. We can think of this vector as
the ‘down’ direction expressed in
the sensor’s coordinate frame, which
makes it ideal for ‘marble rolling’ style
behaviour.
Let’s keep our current Euler values
for curiosity, whilst also adding grav.x,
.y, and .z values to the output. The
bulk of our program remains the
same. All we need to do is modify
the loop() function, as illustrated
in Listing 2(a) from the file named
CB-jul26-code-02.txt.
On line 30, we declare a variable
called event as before. On line 31, we
use the getEvent() function to query
the sensor for its orientation and store
the results in our event variable.
On line 33, we declare a three-
element vector variable called grav.
Later in this line, we use the getVector()
function to ask the sensor ‘which way
is down?’ and store the results in our
grav variable.
I know this statement looks like
punctuation gone wild; I’m sorry, it’s
because we’ve wandered beyond traditional C into the more exotic realms
of C++, with its templates, namespaces and object-oriented syntax. All that
is beyond the scope of our discussion
here, so let’s just accept that it works
and move on with our lives.
On lines 35-37, we print the
orientation.x, .y, and .z values stored
in our event variable, each with two
digits after the decimal point. On lines
38-40, we print the grav.x(), .y(), and
.z() values stored in our grav variable,
again with two digits after the decimal point.
Once again, as soon as my program
was up and running, I tilted my breadboard about 45° to the left, then 45°
to the right, then 45° forward, and finally 45° backward. The first few lines
from this run look like the following:
Serial.print('\t');
Serial.print('\t');
Serial.print('\t');
Serial.print('\t');
Serial.print('\t');
We’re in a pretty good place at this
point. What we’re going to do now is
use only the X and Y components of
the gravity vector from our sensor.
We will also limit the effective tilt to
±60° in any direction by clipping the
calculated angles in software.
There are two reasons for doing
this. First, in practice, we’re unlikely to tilt the console much beyond
60° during normal use, so allowing for larger angles doesn’t buy us
anything. Second, as we approach
steeper angles, the response becomes
Practical Electronics | July | 2026
W
void loop()
{
imu::Vector<3> grav = IMU.getVector(Adafruit_BNO055::VECTOR_GRAVITY);
G
B
S
// Normalize and protect against tiny over/under-shoots
float gx = clampFloat(grav.x() / G, -1.0, 1.0);
float gy = clampFloat(grav.y() / G, -1.0, 1.0);
float leftRightDegF = asin(gx) * 180.0 / PI;
float frontBackDegF = asin(gy) * 180.0 / PI;
int leftRightDeg = (int)round(leftRightDegF);
int frontBackDeg = (int)round(frontBackDegF);
Serial.print("LR = ");
Serial.print (leftRightDeg);
Serial.print(" FB = "); Serial.println(frontBackDeg);
GND
SDA
SCL
// Optional clipping for display / gameplay
leftRightDeg = constrain(leftRightDeg, -MAX_TILT, MAX_TILT);
frontBackDeg = constrain(frontBackDeg, -MAX_TILT, MAX_TILT);
Vin
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
delay(100);
}
Fig.4: connecting the IMU to the console via
its 15-way D-sub connector.
Listing 3(a): our third IMU test program.
increasingly sensitive and harder to
control, which can make the behaviour feel a bit ‘twitchy’.
By limiting the range, we keep things
well-behaved and easier to use.
Once again, the bulk of our program remains the same (see the file
named CB-jul26-code-03.txt). One
change is that we declare two global
variables: a floating-point G (the gravitational constant), which we assign
the value 9.81, and an integer MAX_
TILT, which we assign the value 60
(degrees).
Also, we create a simple helper
function called ClampFloat() that constrains (or ‘clamps’) a floating-point
value to lie within a specified range.
This function accepts three floatingpoint arguments: the value x, a lower
limit lo, and an upper limit hi. If x is
less than lo, the function returns lo; if
x is greater than hi, it returns hi; otherwise, it returns x unchanged.
The main changes occur in our loop()
function, as illustrated in Listing 3(a).
On line 39, we ask the sensor for
the current gravity vector, as before.
On lines 42 and 43, we divide the
X and Y values by G (the strength of
gravity, 9.81) to get numbers between
about -1 and +1. We then clamp them
just in case they stray slightly outside
that range due to noise.
On lines 45 and 46, we use the inverse sine (arcsine) function, asin(), to
convert our gx and gy values to angles
in radians, then multiply by 180/π to
convert them into degrees.
On lines 48 and 49, we round the
angles to the nearest integer to make
them easier to use.
On lines 52 and 53, we limit the
values to ±60° to ensure things stay
well-behaved and easy to control.
On lines 55 and 56, we print the
results.
As usual, as soon as my program
Practical Electronics | July | 2026
was up and running, I tilted my breadboard about 45° to the left, then 45°
to the right, then 45° forward, and finally 45° backward. If you examine
the results from the full run (in the
file named CB-jul26-results-03.txt),
you’ll see that things are now very
easy to understand. A drastically
cut-down version of these results is
shown below:
BNO055 tilt angles
LR: left = -ve, right = +ve
FB: back = -ve, front = +ve
LR = -45 FR = -4 (tilting left)
LR = 45 FR = -2 (tilting right)
LR = -1 FR = 45 (tilting forward)
LR = -1 FR = -45 (tilting backward)
Translating to delta values
Having access to specific tilt angles
is all well and good, and I’m sure this
data will come in handy in the future.
For the moment, however, I want to
keep things as simple as possible.
Let’s say that we are going to use
the tilting of our console to ‘roll’ a
virtual marble around our array. Let’s
also set a tilt threshold of 15°. Any56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
thing more than this and the virtual
marble will roll; anything less and it
will stay where it is.
Since our ‘marble’ can move by only
one pixel at a time, we will make use
of integer deltaX and deltaY values,
each of which can be -1, 0, or +1.
As before, the bulk of our program
remains unchanged (see the file named
CB-jul26-code-04.txt). The only modification we make to the program’s
front end is to add a new global integer variable named TILT_THRESHOLD,
which we set to 15.
The remaining changes occur in
our loop() function, as illustrated in
Fig. 4(a). On lines 56 and 57, we declare two 8-bit integer variables, deltaX
and deltaY, and set them to 0.
On lines 59-62, we check whether
the console is tilted more than 15°
to the left or right; if so, we set the
deltaX value accordingly. Similarly,
on lines 64-67, we check whether the
console is tilted more than 15° forward
or backward; if so, we set the deltaY
value accordingly.
On lines 69 to 72, we print the actual
left-right (LR) tilt angle, followed by
int8_t deltaX = 0;
int8_t deltaY = 0;
if (leftRightDeg <= -TILT_THRESHOLD)
deltaX = -1;
else if (leftRightDeg >= TILT_THRESHOLD)
deltaX = +1;
if (frontBackDeg <= -TILT_THRESHOLD)
deltaY = -1;
else if (frontBackDeg >= TILT_THRESHOLD)
deltaY = +1;
Serial.print("LR = ");
Serial.print
(leftRightDeg);
Serial.print(" dX = "); Serial.print
(deltaX);
Serial.print(" FB = "); Serial.print
(frontBackDeg);
Serial.print(" dY = "); Serial.println (deltaY);
delay(100);
}
Listing 4(a): adding tilt threshold and delta values to our game.
53
Topside up
Topside of breakout board facing down
GND
SDA
SCL
Vin
Tag for illustration only
(a) Original
(b) Flipped
horizontally
(c) Flipped and
rotated 90°
(d) Flipped and
rotated 180°
(5) Flipped and
rotated 270°
Fig.5: reflections and rotations depending on our IMU’s mounting arrangement.
the corresponding deltaX value, then
the actual forward-backward (FB) tilt
angle, followed by the corresponding
deltaY value.
Some sample (drastically cut down)
results are shown below:
BNO055 tilt angles
LR: left = -ve, right = +ve
FB: back = -ve, front = +ve
LR = 0 dX = 0 FB = 0 dY = 0
LR = -18 dX = -1 FB = -1 dY = 0
LR = 21 dX = 1 FB = 0 dY = 0
LR = 9 dX = 0 FB = 18 dY = 1
LR = 2 dX = 0 FB = -17 dY = -1
In this case, the board started out
flat, then I tilted it more than 15° left,
then right, then forward, then back.
Run this yourself and verify that the
results are as you expect.
Adding the IMU to the console
This is the exciting part where we
get to add the IMU to the console. In
the fullness of time, I plan to mount
my IMU inside my console. In the
short term, however, I’ve placed it on
a small circuit board and connected
it via the 15-way D-sub connector, as
shown in Fig.4.
Before you do anything else, rerun
our latest and greatest “Delta Values”
program and verify that everything still
works as expected. It will be best to
detect and resolve any problems now,
before we start adding more bells and
whistles.
The main thing to note here is
that the IMU is oriented exactly as it
was during my earlier experiments.
When I eventually mount it inside
the console, however, I may flip it
upside-down. It could also be rotated
by 90°, 180°, or 270°, as illustrated
in Fig.5.
The configurations shown in Fig.5(b)
and Fig.5(d) will probably require us
to invert (change the sign of) one or
both of the X and Y values reported
by the IMU. The options in Fig.5(c)
and Fig.5(e) will also require us to
54
swap the X and Y axes between the
IMU and the console (map X to Y and
Y to X), in addition to applying any
necessary sign changes.
As a rule of thumb, flipping the IMU
reverses the direction (sign change),
while rotating the IMU reassigns the
directions (axis swap).
Rolling a virtual marble
Do you recall the flashing pixel program we created in the March 2026
issue? I’m referring to the one that
started with a green pixel flashing
in the middle of our array. Pressing
any of the red, yellow, green, or blue
action buttons changed the pixel’s
colour, while the north, south, east,
and west direction buttons moved it
one pixel in that direction.
If the pixel reached the edge of the
display, continuing in the same direction caused it to ‘wrap around’ to
the opposite side. You may wish to
refresh your memory before we proceed by perusing the program (in the
file named CB-jul26-code-05.txt) and
rerunning it on your console.
How about we keep this functionality while adding the ability to treat
the flashing pixel as a virtual marble,
tilting the console to ‘roll it’ around
the display? Happily, the way we’ve
designed things means that doing this
is going to be quite easy.
We start by copying the four “include library” statements from our
latest and greatest ‘delta values program’ (DVP) (lines 1-4) into our new
‘flashing pixel program’ (FPP) (lines
5-8 in the file named CB-jul26-code-06.
txt).
Next, we copy the IMU-related definitions from DVP (lines 6-10) into
FPP (lines 10-16). We also copy the
ClampFloat() function from DVP (lines
12-17) into the ‘game generic routines’ section of FPP (lines 439-445).
In the ‘game related stuff’ section
of FPP, just under the 1ms TICK definition (line 151), we add a 200ms
IMU_SAMPLE_TICKS definition (line
152). We also add a new MarbleThen
variable (line 182). We’ll use these
to periodically sample the IMU and
update the marble’s position.
To initialise the IMU in the setup()
function, we copy the key statements
from DVP (lines 29, 34, and 35) into
FPP (lines 213-216). In the original FPP
loop() function, we repeatedly called
our UpdateFlash(), UpdateSwitches()
and ProcessSwitches() functions each
millisecond. The only change we’re
making here is to follow these with a
call to a new UpdateMarbleFromIMU()
function.
Essentially, we are going to take
the entire loop() function from our
DVP, chop out anything we don’t
need, augment it with just a soupçon of additional logic, and add
it into FPP as the aforementioned
UpdateMarbleFromIMU() function
(lines 499-537). Let’s take a moment
to touch on some of the highlights, as
illustrated in Listing 6(a).
On line 501, we check whether
sufficient time has passed since we
last sampled the IMU and moved
the marble (this delay, as specified
by IMU_SAMPLE_TICKS, is currently set to 200ms, but you can increase
or decrease it as you wish). If not, we
simply skip the rest of this function
and try again on the next iteration of
the main loop.
The bulk of this function (lines 503530) is lifted from the DVP, ultimately
yielding deltaX and deltaY values of
-1, 0, or +1.
On lines 532–533, we perform a
quick sanity check: if both deltaX
and deltaY are zero, the board is essentially level (or close enough), and
there’s nothing to do. However, if
either value is non-zero—meaning
we’ve tilted beyond our threshold in
at least one direction—we call our
trusty UpdateLocation() function to
move the marble by one step in the
appropriate direction.
Finally, on line 535, we store the
current time in MarbleThen, thereby
Practical Electronics | July | 2026
marking the time of this marble update.
The next update won’t occur until another IMU_SAMPLE_TICKS milliseconds have elapsed.
As you’ll see when you run this
new version of our program, we can
still change the flashing pixel’s colour
using the action buttons and move it
with the direction buttons, as before.
However, we can also ‘roll the marble’
by tilting the console.
You’ll observe that, in addition to
rolling the marble north, south, east
or west, we can move it along the diagonals—northeast, southeast, northwest or southwest—a trick we never
bothered to implement under button
control.
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
Returning to the Countdown Timer
Ironically and paradoxically, it
seems we’ve run out of time, so we’ll
have to regroup on this next month.
As always, if you have any thoughts
on anything you’ve read here, please
feel free to drop me an email at max<at>
PE
clivemaxfield.com
void UpdateMarbleFromIMU()
{
if ( (TimeNow - MarbleThen) >= IMU_SAMPLE_TICKS)
{
imu::Vector<3> grav = IMU.getVector(Adafruit_BNO055::VECTOR_GRAVITY);
// Normalize and protect against tiny over/under-shoots
float gx = ClampFloat(grav.x() / G, -1.0, 1.0);
float gy = ClampFloat(grav.y() / G, -1.0, 1.0);
float leftRightDegF = asin(gx) * 180.0 / PI;
float frontBackDegF = asin(gy) * 180.0 / PI;
int leftRightDeg = (int)round(leftRightDegF);
int frontBackDeg = (int)round(frontBackDegF);
// Optional clipping for display / gameplay
leftRightDeg = constrain(leftRightDeg, -MAX_TILT, MAX_TILT);
frontBackDeg = constrain(frontBackDeg, -MAX_TILT, MAX_TILT);
int8_t deltaX = 0;
int8_t deltaY = 0;
if (leftRightDeg <= -TILT_THRESHOLD)
deltaX = -1;
else if (leftRightDeg >= TILT_THRESHOLD)
deltaX = +1;
if (frontBackDeg <= -TILT_THRESHOLD)
deltaY = -1;
else if (frontBackDeg >= TILT_THRESHOLD)
deltaY = +1;
if ((deltaX != 0) || (deltaY != 0))
UpdateLocation(deltaX, deltaY);
MarbleThen = millis();
}
}
Listing 6(a): our new virtual marble function.
Devices and components used in this issue
9DOF Adafruit IMU
www.amazon.co.uk/dp/B017PEIGIG
ETI BUNDLE (1) Teach-In 3, 4 and 5 – all on CD-ROM – only £18.95
ELECTRONICS TEACH-IN 3 – CD-ROM
ELECTRONICS
TEACH-IN 3
EE M
FR -RO
CD
Mike & Richard Tooley
The three sections of the Teach-In 3 CD-ROM cover a
huge range of subjects that will interest everyone involved
in electronics – from newcomers to the hobby and
students to experienced constructors and professionals.
£7.99
FROM THE PUBLISHERS OF
i The how and why of circuit design
PRACTICALLY SPEAKING
i The techniques of electronic
project construction
INGENUITY UNLIMITED
The first section (80 pages) is dedicated to Circuit
Surgery, EPE/PE’s regular clinic dealing with readers’
TeachFREE
In
nics
queries on circuit design problems – from voltage
TEACH-IN 1 CD-ROM
ro
TWO TEACH-INs FOR
regulation to using SPICE circuit simulation software.
THE PRICE OF ONE!
The second section – Practically Speaking –
covers hands-on aspects of electronics construction.
Again, a whole range of subjects, from soldering to
avoiding problems with static electricity and identifying
components is covered. Finally, our collection of
Ingenuity Unlimited circuits provides over 40 circuit designs submitted by readers.
The CD-ROM also contains the complete Electronics Teach-In 1 book, which
provides a broad-based introduction to electronics in PDF form, plus interactive
quizzes to test your knowledge and TINA circuit simulation software (a limited
version – plus a specially written TINA Tutorial). Teach-In 1 covers everything
from electric current through to microprocessors and microcontrollers, and each part
includes demonstration circuits to build on breadboards or to simulate on your PC.
©
blishing Ltd 20
10
1
Elec
t
i Over 40 different circuit ideas
W
i The free CD-ROM provides
a broad-based introduction
to electronics
n Program
Simulatio
Circuit d version)
(Limite
ramming
PIC Prog n)
CODE V3
d versio
8 FLOW
(Limite
Software Quizzes to Test
active
ledge
8 Inter
Your Know
8 TINA
i A complete stand-alone
tutorial in 11 parts plus free
software
Teach In 3 Cover.indd 1
10 13:21:42
29/04/20
dd 1
Mike & Richard Tooley
The Teach-In 4 CD-ROM covers three of the most
important electronics units that are currently studied in
many schools and colleges. These include, Edexcel
BTEC level 2 awards and the electronics units of the
Diploma in Engineering, Level 2.
The CD-ROM also contains the full Modern
Electronics Manual, worth £29.95. The Manual
contains over 800 pages of electronics theory,
projects, data, assembly instructions and web links.
It's an ideal way to learn about electronics.
ELECTRONICS
TEACH-IN 4
EE M
FR -RO
CD
£8.99
FROM THE PUBLISHERS OF
A BROAD-BASED
INTRODuCTION TO ELECTRONICS
FREE
CD-RO
M
WORT
H
£29.9
5
i An eleven part tutorial
i uses inexpensive circuit
simulation software
FREE M
CD-RO
£8.99
JUMP START
15 design and build circuit projects
dedicated to newcomers or those
following courses in schools and
colleges
PRACTICALLY SPEAKING
The techniques of project construction
PIC ‘N MIx
Starting out with PIC microcontrollers
FREE M
CD-RO
TEACH-IN 2
cal
a practi
Provides
to PIC
introduction llers
microcontro
ws
for Windo
CD ROM
start
should
This CD
y, if not
automaticallindex.html
double-click
TWO TEACH-INs FOR
THE PRICE OF ONE !
Plus:
nika,
MikroElektro
Microchip pe
L-Tek PoSco
software
The free CD-ROM provides a
practical introduction to PIC
microcontrollers
Plus MikroElektronika,
Microchip and L-Tek PoScope
software
In 2
TeachLtd
nicsorne
dsPIC are
Publishing
PIC and
rated
Ele©ctro
MPLAB, logy Incorpo ogy
2013 Wimb
and logo,
Techno
Technol
ip
ip name of Microchip
Microch
.09
arks
016-02
The Microch
s. © 2013
ed tradem
countrie
1. MCCD1
register
d. Issue
and other
in the USAAll rights reserve
Inc.
PLUS
3 09:59:25
29/07/201
d 1
Teach
In 2 - JUL13.ind
Teach In 5 Cover.indd 1
29/07/2013 10:00:29
PIC’n’ Mix – starting out with the popular range of
PIC microcontrollers and Practically Speaking – tips and techniques for project construction.
The CD-ROM also contains:
n Complete Teach-In 2 book, a practical introduction to PIC microprocessors
n MikroElektronika, Microchip and L-Tek PoScope software.
Three Teach-ins for the great price of
£18.95
PLUS you also get the contents of the
free CD-ROM from each issue...
THE MODERN ELECTRONICS MANuAL
The essential reference
work for everyone
studying electronics
i Over 800 PDF pages
i In-depth theory
autorun,
should
This software
in Windows
if not, open double-click
and
f
Explorer
index.pd
requires
This CD-ROM
Reader™
from
Adobe®
dable Free
Downloa
be.com
www.ado
UAL
MAN.co.u
k
BA SE .epe
mag
www
© Wimbor
Teach In 4 Cover.indd 1
Practical Electronics | July | 2026
The projects are: n Moisture Detector n Quiz
Machine n Battery Voltage Checker n SolarPowered Charger n Versatile Theft Alarm
n Spooky Circuits n Frost Alarm n Mini Christmas
Lights n iPod Speaker n Logic Probe n DC Motor
Controller n Egg Timer n Signal Injector Probe
n Simple Radio Receiver n Temperature Alarm.
06/05/2010 16:22:29
CD Cover.in
ELECTRONICS TEACH-IN 4 – CD-ROM, A
BROAD INTRODUCTION TO ELECTRONICS
Mike & Richard Tooley
ELECTRONICS
TEACH-IN 5
EE M
FR -RO
CD
FROM THE PUBLISHERS OF
15 design and build circuit projects for newcomers
or those following courses in school and colleges.
CIRCUIT SURGERY
Pu
rne
bo
im
ELECTRONICS TEACH-IN 5 – CD-ROM
JUMP START
ne Publishi
ng Ltd.
i Extensive data tables
and web links
... so that’s another TWO Teach-Ins and
The Full Modern Electronics Manual!
2011
14/11/2011 20:33:21
55
|