In this tutorial we'll use Ada language to write an accelerometer driver for LSM303AGR and then we'll make a small nerve game.
We'll begin by knowing how the accelerometer works and after that we will write the driver for the accelerometer on BBC:MicroBit v1.5 then use that driver to make a small game.
Note it's very important to check your version number as MicroBits other than v1.5 have different accelerometer modules and won't work.
Accelerometers are intricate devices that can compute the acceleration in the 3 axis; X, Y, Z.
If you hang an egg using a spring and let it settle the spring will store an amount of energy that's needed to restore it to its original state as illustrated by the image from hyperphysics.
Repeat this image for the remaining 2 axis, Y and Z, then scale that down into the micrometer level and you'll almost get the same accelerometer that we'll be using.
The only difference is that instead of measuring the force stored in the spring, the accelerometers we use utilize measuring capacitance (en electrical measure) instead of the stored force (mechanical measure) and they look somewhat like this, more details are siliconsensing.com.
It's important to know how your accelerometer is pointing because for example the X and Y axes might be flipped, so it's always a good idea to lightly consult the data sheet, the BBC:MicroBit (v1.5) uses LSM303AGR sensor from ST and the directions look like the following, note that those are the positive directions.
other versions of MicroBit can have different sensors, but fret not! those details are just to let you know what happens under the hood, you shouldn't worry about them too much unless you're dealing with the underlying hardware (which is not our case).
CircuitWe'll just use a little shiny MicroBit.
Ada's main premise is reliability when it comes to writing software, it's a "very" strongly typed language, everything started in the 70s in the department of defense in the USA where they needed a single language to program reliable systems, you can find more about its history here
Ada has features that are not in other any languages like specifying a range of values to make a new type of integer. For example; you can have a type called Length that must be a positive integer and a type called Area that's a result of multiplying 2 Lengths, in regular languages you can just multiply both and assign the result to the variable of type Area but in Ada you have to explicitly write that in the code; hidden intentions aren't taken into consideration to reduce the amount of possible bugs.
Moreover, you can specify the range of a value for example if you want to apply discount to a price the discount ranges only from 0..100, you can declare a Discount type that accepts only values in that range, you can read more here
LSM303AGR + BBC:MicroBit 🤖To interface with the MicroBit, Ada drivers (software that knows how to deal with hardware) must be used in order to make programming easier.
Drivers are provided by AdaCore here, you should reference the drivers library when making your own project.
After you download the drivers library, make sure you install its dependencies by running scripts/install_dependencies.py
then things should work well, you'll also need to install GNAT IDE from this link. You can try the examples provided, if you want to just go; after an example works just modify its code as the examples already reference the drivers package correctly, otherwise you'll need to change the include path to point to the drivers.
To write a proper driver we have to know how the module works from the datasheet LSM303AGR, which can be found here
We'll notice that the module has both an accelerometer and a magnetometer, for this tutorial we'll focus on the accelerometer and supports both I2C
and SPI
interfaces, we'll be using I2C
interface here.
After getting an idea about the features in the accelerometer, we now want to make it useful to us and get some data from it.
I2C Sanity Check ✅Since we'll be using I2C interface, we have to know the address of the device we want to communicate with, this can be found in section 6.1 I2C operation
in the datasheet, we'll find that the address we should use to communicate with the accelerometer is 0x32
(and 0x33
for reading).
The existing I2C library that's available just cares about the writing address which is 0x32
and computes the reading address on its own, a good resource to understand more about I2C can be found here.
The tiniest step to take to validate that things are working well is to check the device ID of the installed accelerometer, which is a constant value for all modules out there stored in the WHO_AM_I_A
register, in our case the value is 0x33
(as we're checking the value of the accelerometer).
It's worth noting that we need to know the address of the register also (besides the device address), you can find that address in section 7 Register mapping in the datasheet.
Calling the Read_Register
function on the I2C port with the device [0x32
] and register's address [0x0F
] should return 0x33
, if that's the case with you, you're on the right track. Be extra careful with the values here as messing them up will cause hard to find bugs, I included a comment to elaborate the correct values.
Read_Register (I2c_Port, Device_Address, WHO_AM_I_Register_Address)
-- Read_Register (<object>, 16#32#, 16#0F#)
-- Should return 0x33 (2#00110011#)
Setting Up 🏗Setting up the accelerometer happens when we write values to control registers, mainly CTRL_REG1 which has the main configuration of powering up the sensor and selecting a power mode. By default the sensor is shutdown, so we need to configure it.
We'll discuss the process of setting up and select our values for CTRL_REG1 after then we'll just write those values through the I2C port and voila! your sensor is up and running.
Setting Up (Power Modes) ⚡️Now that we know we can communicate with our device, it's time to initialize it to a working mode, according to the datasheet this small device has XX working modes, choose each over with a certain frequency and resolution choose whatever you like or try them all, details are in section 4.2
We can ignore the BW, Turn-on time for our application. [ ODR is part of CTRL_REG1_A register ]
Coming to the So @ +-2g column, this column represents the sensitivity of the accelerometer for a change of 1 in the reading. For example; if we are using the 10-bit data output and we have a readings of 236 and 237 this means that the second reading is higher by 4mg (milli G :: (m/s2)*10^-3) than the first one. You can find more detailed explanation here.
Setting Up (Power up & Data Rate) 🏁Now we need to set up the data rates, by default all the 3 axes values are enabled for the sensor to read, but the sensor itself is put in power-down mode.
Available data rates depend on the current power modes, (HR: High resolution).
Select whichever sampling frequency you like and set the values of the ODR accordingly. You can make yourself a favour by writing down the values of CTRL_REG1 register that you changed so you can easily reach them when writing code.
we're just one step of turning on our sensor, but before that we need to make a little pause, if this is your first time writing a driver (like me (: ) then wait a second.
To represent a register in code, we'll need something like a C-struct with bitfields which is luckily available in Ada, you can define a new record and set the field positions for each bit, more information can be found here
One mistake that I did was that I flipped the bit order, this device is little-endian meaning that the LSB bit has the lowest location, which essentially makes the Xen bit the #0 bit in the struct as illustrated in the code.
type CTRL_REG1_A_Register is record
Xen : Bit := 0;
Yen : Bit := 0;
Zen : Bit := 0;
LPen : Bit := 0;
ODR : UInt4 := 0;
end record;
for CTRL_REG1_A_Register use record
Xen at 0 range 0 .. 0;
Yen at 0 range 1 .. 1;
Zen at 0 range 2 .. 2;
LPen at 0 range 3 .. 3;
ODR at 0 range 4 .. 7;
end record;
Just a quick note, the at
statement represents the index of the "byte" while the range
statement represents the "bits" of that byte. So for example ODR is at byte #0 and bits 4-7 (4-bit wide). The code above matches the layout in the datasheet.
Defining similar structs for the registers that we're using will make programming way easier and less error prone, as now we're simply dealing with the register as an object with named fields.
Setting Up (Ignition!) 🚀Now we're ready to go, what we need to do to start the sensor is to write the value of CTRL_REG1 object to the I2C address, the one thing left to do so is to cast this object to a UInt8 so the I2C port can understand it.
This.Write_Register
(Accelerometer_Address, CTRL_REG1_A, To_UInt8 (CTRLA));
Reading Sensor Values 🔎The sensor represents the reading as "two’s complement left-justified " values for each of the following register pairs (one pair for each axis)
- X-axis OUT_X_L_A (28h), OUT_X_H_A (29h)
- Y-axis OUT_Y_L_A (2Ah), OUT_Y_H_A (2Bh)
- Z-axis OUT_Z_L_A (2Ch), OUT_Z_H_A (2Dh)
So to get the (10-bit) reading for the X-axis you need to read both OUT_X_L_A
and OUT_X_H_A
on addresses 0x28,0x29
respectively. H stands for high byte and L stands for low byte, a 10-bit reading would look like the following
╔═════════╦═══════════╦═══════════╗
║ ║ OUT_X_H ║ OUT_X_L ║
╠═════════╬═══════════╬═══════════╣
║ Content ║ xxxx xxxx ║ xx00 0000 ║
╚═════════╩═══════════╩═══════════╝
So, we'll need all OUT_X_H and the highest two bits in OUT_X_L (given 10-bit mode) more details can be found here.
Now that we got the part of left-justification we move to the meaning of two's complement, which is basically a way to represent positive and negative digits in binary, since all we get from the sensor is raw bytes; we'll have to perform this conversion too (from 2 raw bytes to a single positive/negative integer) by casting the reading to an Integer in Ada.
Here is the code that adjusts the reading:
function Convert (Low, High : UInt8) return Axis_Data is
Tmp : UInt10;
begin
Tmp := UInt10 (Shift_Right (Low, 6));
Tmp := Tmp or UInt10 (High) * 2**2;
return To_Axis_Data (Tmp);
end Convert;
function To_Axis_Data is new Ada.Unchecked_Conversion (UInt10, Axis_Data);
The first thing is that we define an unsigned int of 10 bits (UInt10), then give it an initial value of the low byte (e.g. OUT_X_L) shifted 6 bits to the right.
-- Tmp := UInt10 (Shift_Right (Low, 6));
╔═════════╦══════════════╗
║ ║ Tmp ║
╠═════════╬══════════════╣
║ Content ║ 00 0000 00LL ║
╚═════════╩══════════════╝
Then we cast the High byte to a UInt10
(effectively adding two zeros on the left) to get space for the low 2-bits,
-- UInt10 (High) * 2**2;
╔═════════╦══════════════╗ ╔════════════════════════╗
║ ║ Unt10(High) ║ ║ Unt10(High) * 2**2 ║
╠═════════╬══════════════╣ ═══>> ╠════════════════════════╣
║ Content ║ 00 HHHH HHHH ║ ║ Content ║ HH HHHH HH00 ║
╚═════════╩══════════════╝ ╚═════════╩══════════════╝
after that we shift the UInt(High) two bits to the left, then we OR them together to get
-- Tmp := Tmp or UInt10 (High) * 2**2;
╔═════════╦══════════════╗
║ ║ Tmp ║
╠═════════╬══════════════╣
║ Content ║ HH HHHH HHLL ║
╚═════════╩══════════════╝
After that we perform an unchecked cast to Axis_Data which is basically an int to perform the two's complement conversion to -/+ve number.
function To_Axis_Data is new Ada.Unchecked_Conversion (UInt10, Axis_Data);
Then we repeat this process for each of the axes of interest and that's it.
This.Port.Mem_Read
(Addr => Accelerometer_Address,
Mem_Addr => To_Multi_Byte_Read_Address (OUT_X_L_A),
Mem_Addr_Size => Memory_Size_8b, Data => Data, Status => Status);
-- LSM303AGR has its X-axis in the opposite direction
-- of the MMA8653FCR1 sensor. (reference implementation)
AxisData.X := Convert (Data (1), Data (2)) * Axis_Data (-1);
AxisData.Y := Convert (Data (3), Data (4));
AxisData.Z := Convert (Data (5), Data (6));
⚠️ A note about getting the readings ⚠️
One pitfall I came across was trying to read both registers together, which is already supported by the Read_Register
code in the provided framework, the readings were wrong but after a fair amount of debugging I figured out that I was getting back only 1 byte (the second being undefined) although I requested two bytes to read.
Reading our for the AdaCore community on Telegram, they pointed that some devices require some tweaking for reading multi-bytes which was the case for me.
This is pointed out in section 6.11, so this basically says that if I want to read multiple bytes I have to set the most significant bit (MSB) of the address to 1.
This can be achieved by OR
ing the address value with a 1 followed by 7 zeros.
MULTI_BYTE_READ : constant := 2#1000_0000#;
-- Constant to read multi-byte
function To_Multi_Byte_Read_Address
(Register_Addr : Register_Address) return UInt16
is
begin
return UInt16 (Register_Addr) or MULTI_BYTE_READ;
end To_Multi_Byte_Read_Address;
The Register_Addr
is converted to UInt16
as this is the type required by the framework when specifying a Mem_Addr
to be read.
To make use of the sensor we'll make a small nerve game, the idea is to keep the MicroBit in an almost flat position where acceleration along X and Y axes is close to zero.
We won't make the game so hard, so we'll let the MicroBit assist the player by displaying an arrow to indicate the direction of fixing their position.
To guide the user, we'll first make sure they adjust their Y-axis then we'll proceed to displayed information about the X-axis. So, the Y-Axis will always have a higher priority.
The first step is to initialize I2C communication on the MicroBit in order to use it with the sensor and then setup the sensor operating mode to 10-bit resolution with 10Hz sampling rate.
if not MicroBit.I2C.Initialized then
MicroBit.I2C.Initialize;
end if;
Acc.Configure (LSM303AGR.Freq_10); -- We don't need more than 10Hz
In our main loop, we start by acquiring readings from the sensor
Acc_Data := LSM303AGR.Read_Accelerometer (Acc);
A note about threshold
Then we compare the absolute value to a threshold, you can start with the value of threshold as 15 (which is 15 * 4 = 60 mG) that's you allow 60mG acceleration on the X-axis, this happens because normally if the MicroBit is in a flat plane all the gravity will be acting on the Z-axis, but when the MicroBit is a bit tilted the gravity component is broken down to act on the titled axes.
Then we continue with our logic to display arrows for the user to correct their tilt
if Below_Threshold (Abs_Axes_Data.Y) then
Y_Good := True; -- We flag that Y-axis is ok, to continue with X-axis
else
Good_Count := 0;
if Acc_Data.Y < 0 then
Display.Clear;
Display.Symbols.Up_Arrow;
elsif Acc_Data.Y > 0 then
Display.Clear;
Display.Symbols.Down_Arrow;
end if;
end if;
and repeat the same for the X-axis readings.
After verifying the X-Axis we can congratulate a user that maintains a high record of stability by keeping their hand stable for an extended period of time, we keep track of the number of good readings the user made then after they score 50 good readings we show that he's a good player that deserves a heart ♥️.
if Good_Count > 50 then
Display.Clear;
Display.Symbols.Heart; -- Great job!
elsif Good_Count >= 10 then
Display.Clear;
Display.Symbols.Smile; -- Keep being good
end if;
Different levels 💪You can make the game harder by reducing the threshold. Happy time stabilizing your hand!
if you like this project please share it with your friends, also don't forget to check my other projects and hit respect 👍
Comments