How to write a driver
We are using the SparkFun Qwiic Joystick as an example of how to write a driver for a sensor. The SparkFun Qwiic Joystick is a 2-axis joystick with a single button. Using a Qwiic connector, it's very simple to get the joystick connected.
This guide will walk you through the steps of identifying how the sensor communicates and how to write a fully working driver for it.
The completed package can be found in the toit-qwiic-joystick repository.
Approach
The Joystick features an ATtiny85 microcontroller with a custom firmware. As described in the Hookup Guide, the firmware exposes several registers. With that in mind, we're going to do the following steps:
Connect the Joystick to an I2C bus, configuring the I2C device.
Use the
Registersabstraction to communicate with the chip.Validate connectivity to the device.
Read out Axis and Button values.
I2C setup
We use a simple I2C setup, with pin 21 for SDA (blue) and pin 22 for SCL (yellow).
Driver skeleton
As all I2C/SPI drivers that work using registers, the driver starts with the following skeleton.
import serial
class Joystick:
static I2C-ADDRESS ::= 0x20
registers_/serial.Registers
constructor device/serial.Device:
registers_ = device.registersMost drivers turn on their devices in the constructor, and shut them down in a close method.
Drivers that can be turned on or off repeatedly should instead have on and off methods.
The hookup guide has a table of I2C registers available in the custom firmware. At address 0x00 is the slave address assigned to the device (hard-coded to 0x20).
For simplicity we didn't add a close method, but it's OK to have an empty one.
Validate connectivity
By reading the REG-DEFAULT-ADDRESS_ register, we can confirm the connectivity to the device is functional.
class Joystick:
static REG-DEFAULT-ADDRESS_ ::= 0x00
// ...
constructor device/serial.Device:
registers_ = device.registers
reg := registers_.read-u8 REG-DEFAULT-ADDRESS_
if reg != I2C-ADDRESS: throw "INVALID_CHIP"With this added, we can validate the setup:
main: // ... device := bus.device Joystick.I2C-ADDRESS joystick := Joystick device print "SparkFunJoystick"
Running the code should print out the string SparkFunJoystick to the terminal. If not, the I2C bus is not configured to match the wiring.
If the Joystick is connected without the full breakout board from SparkFun, I2C pull-up resistors may be needed.
Read out data
We're going to expand the driver with 3 new methods:
class Joystick:
// ...
/**
Returns the horizontal value in the range [-1..1].
*/
horizontal -> float:
// ...
/**
Returns the vertical value in the range [-1..1].
*/
vertical -> float:
// ...
/**
Returns true if the button is pressed.
*/
pressed -> bool:
// ...Both the horizontal and vertical values are formatted the same way; 2 bytes in big endian order (MSB first). We want to transform this value to a float in the range [-1..1]. Let's create a helper function to perform this step:
The last 6 bits of the result are unused, but to keep the code simple we treat the result as an 16-bit unsigned integer.
import serial
class Joystick:
// ...
read-position_ reg/int -> float:
value := registers_.read-i16-be reg
// Perform floating-point division to get to [-1..1] range.
return value.to-float / int.MAX-16With that in place, we can now finish the horizontal and vertical methods:
// Continuing class Joystick:
static REG-HORIZONTAL-POSITION_ ::= 0x03 // (to 0x04)
static REG-VERTICAL-POSITION_ ::= 0x05 // (to 0x06)
// ...
horizontal -> float:
return read-position_ REG-HORIZONTAL-POSITION_
vertical -> float:
return read-position_ REG-VERTICAL-POSITION_Lastly, we need to implement the pressed method. We're simply going to read out the 1-byte register value and check for 0, with 0 meaning pressed.
// Continuing class Joystick:
static REG-BUTTON-POSITION_ ::= 0x07
// ...
pressed -> bool:
return (registers_.read-u8 REG-BUTTON-POSITION_) == 0Let's try it out:
main:
// ...
while true:
print "$joystick.horizontal - $joystick.vertical (pressed: $joystick.pressed)"
sleep --ms=250As the joystick is moved around, it's possible to get an I2C error if the I2C bus is accidentally short-circuited by the fingers.
To improve responsibility, the sensor should be read at a higher frequency.
Full code
The driver
qwiic-joystick.toit
import serial
class Joystick:
static I2C-ADDRESS ::= 0x20
static REG-DEFAULT-ADDRESS_::= 0x00
static REG-HORIZONTAL-POSITION_ ::= 0x03 // (to 0x04)
static REG-VERTICAL-POSITION_::= 0x05 // (to 0x06)
static REG-BUTTON-POSITION_ ::= 0x07
registers_/serial.Registers
constructor device/serial.Device:
registers_ = device.registers
reg := registers_.read-u8 REG-DEFAULT-ADDRESS_
if reg != I2C-ADDRESS: throw "INVALID_CHIP"
/**
Returns the horizontal value in the range [-1..1].
*/
horizontal -> float:
return read-position_ REG-HORIZONTAL-POSITION_
/**
Returns the vertical value in the range [-1..1].
*/
vertical -> float:
return read-position_ REG-VERTICAL-POSITION_
/**
Returns true if the button is pressed.
*/
pressed -> bool:
return (registers_.read-u8 REG-BUTTON-POSITION_) == 0
read-position_ reg/int -> float:
value := registers_.read-u16-be reg
// Perform floating-point division to get to [-1..1] range.
return value.to-float / int.MAX-16The Toit application running on your device
main.toit