Level
A digital spirit level on an RGB LED matrix, driven through the Display driver API and fed from an accelerometer.
Overview
This sample turns the board into a digital spirit level. The on-board accelerometer measures which way is down, and a bubble drawn on the RGB LED matrix runs towards whichever edge of the board is raised, exactly like the bubble in a vial. Hold the board flat and the bubble returns to the middle of the panel.
The panel is driven through the Display driver API and the sensor through the Sensor driver
API, so the same binary runs on panels of different geometry, wiring
order and pixel format without any board specific code. The panel is taken from
the chosen { zephyr,display = ...; }; node, which on the supported board
resolves to a led-strip-matrix device on top of a WS2812 compatible LED
strip. The sensor is taken from the accel0 alias; only the accelerometer is
used, since a level needs the drift-free reference that gravity provides and a
gyroscope does not.
Techniques worth knowing about:
The acceleration vector is normalised against its own length, which turns the in-plane components into plain sines of the tilt angle, and is smoothed by a first order low pass filter before use.
The bubble is rendered with sub-pixel accuracy: instead of snapping to the nearest pixel, its light is spread bilinearly over the up to four pixels it straddles.
Since position runs out of resolution near the centre, the bubble also changes colour over a much tighter angle than it moves over, and latches to the near colour, with hysteresis, once the board is level.
Requirements
An RGB LED matrix assigned to the zephyr,display chosen node, with a pixel
format of either RGB_888 or ARGB_8888, and a 3-axis accelerometer assigned
to the accel0 alias.
The sample supports the following platforms (located in samples/display/level/tests.yaml):
Hardware platforms |
Order number |
Board name |
Board target |
|---|---|---|---|
RP2350 |
|
Configuration options
The following sample-specific Kconfig options are used in this sample (located in samples/display/level/Kconfig):
- CONFIG_LEVEL_COLOR_RANGE_TILT
Tilt angle over which the bubble changes colour [tenths of a degree]
At this angle and beyond, the bubble is fully the far colour; at dead level it is fully the near colour; in between it is mixed. Should be far tighter than LEVEL_FULL_SCALE_TILT.
- CONFIG_LEVEL_LOCK_TILT
Tilt angle below which the board counts as level [tenths of a degree]
Below this tilt the bubble latches fully to the near colour. Keep well inside LEVEL_COLOR_RANGE_TILT. Set to 0 to disable the latch.
- CONFIG_LEVEL_LOCK_HYSTERESIS
Extra tilt needed to break the level latch [tenths of a degree]
The latch engages at LEVEL_LOCK_TILT and releases at LEVEL_LOCK_TILT plus this, so the two form a band rather than a single edge that would flicker on noise.
- CONFIG_LEVEL_FULL_SCALE_TILT
Tilt angle at which the bubble reaches the panel edge [degrees]
Tilt that moves the bubble from the centre of the panel all the way to the rim; sets the sensitivity of the instrument.
- CONFIG_LEVEL_FILTER_TIME_CONSTANT
Low pass filter time constant [milliseconds]
Time constant of the first order low pass filter on the acceleration reading. Set to 0 to disable filtering. Quoted as a time rather than a per sample weight, so the smoothing is independent of LEVEL_FRAME_INTERVAL.
- CONFIG_LEVEL_FRAME_INTERVAL
Delay between two frames [milliseconds]
Also the rate at which the inertial sensor is polled.
- CONFIG_LEVEL_SENSOR_LOG_INTERVAL
Delay between two logged sensor readings [milliseconds]
Throttles the log output only; sampling always runs at LEVEL_FRAME_INTERVAL. Set to 0 to log every sample.
- CONFIG_LEVEL_TEST_IMAGE_MS
Show the corner test image at startup [milliseconds]
Lights the four corner pixels in red, green, blue and white to show where the origin of the display physically sits. Set to 0 to skip the test image.
- CONFIG_LEVEL_AXIS_SWAP_XY
Swap the sensor X and Y axes
Feed the sensor Y axis into the panel X axis and vice versa. Applied before the inversions.
- CONFIG_LEVEL_AXIS_INVERT_X
Invert the panel X axis
Negate the horizontal component, after any swap.
- CONFIG_LEVEL_AXIS_INVERT_Y
Invert the panel Y axis
Negate the vertical component, after any swap.
- CONFIG_LEVEL_BRIGHTNESS
Brightness of the bubble and the test image
The channel level a fully lit primary colour is drawn at. This is also the number of brightness steps the sub-pixel positioning has to work with.
Important
Keep CONFIG_LEVEL_BRIGHTNESS low. A WS2812B pixel draws up to 60 ㎃ at
full white according to the WS2812B datasheet [1], so an 8×8 panel alone can
draw more than 3.5 A, far beyond what a typical board’s USB supply can
deliver.
Building and Running
On RP2350-Matrix board, on ARM Cortex-M33:
west build -b waveshare_rp2350_matrix/rp2350a/m33 -p -S "usb-console" -d build/level-waveshare_rp2350_matrix bridle/samples/display/level west flash -r uf2 -d build/level-waveshare_rp2350_matrix
On RP2350-Matrix board, on Hazard3 RISC-V (RV32IMAC+):
west build -b waveshare_rp2350_matrix/rp2350a/hazard3 -p -S "usb-console" -d build/level-waveshare_rp2350_matrix bridle/samples/display/level west flash -r uf2 -d build/level-waveshare_rp2350_matrix
Sample output
The following output is logged on the UART console, here for the 8×8 panel of the RP2350-Matrix board lying nearly flat on the table:
*** Booting Zephyr OS build v4.4.99… ***
[00:00:00.003,000] <inf> level: Level on a 8x8 RGB matrix, 4 bytes per pixel, 40 ms per frame
[00:00:00.003,000] <inf> level: Inertial sensor is qmi8658a@6b, full scale tilt is 10 deg
[00:00:00.003,000] <inf> level: Axis map: swap-xy yes, invert-x yes, invert-y no
[00:00:00.003,000] <inf> level: Filter time constant is 100 ms, giving a weight of 0.330 per sample
[00:00:00.003,000] <inf> level: Colour ramps below 1.0 deg, latches level below 0.5 deg, releases at 0.8 deg
[00:00:02.010,000] <inf> level: accel -0.569 -1.183 -9.630 m/s^2, |a| 9.719
[00:00:02.010,000] <inf> level: tilt x +0.122 y -0.059, angle 7.76 deg
[00:00:02.010,000] <inf> level: dot 5.95, 2.32, proximity 0.00
Axis mapping
Nothing in the devicetree says how the sensor is oriented relative to the
panel, and a bubble floats towards the raised edge, the opposite of
where the gravity vector points. Both are folded into the
CONFIG_LEVEL_AXIS_* options, defaulted per board:
Board |
|
|
|
|---|---|---|---|
yes |
yes |
no |
|
anything else |
no |
no |
no |
To bring up a new board:
Lay the board flat, LEDs up. Note where the corner test image puts its red pixel: that is the origin of the panel.
Build and flash with all three switches off. Raise the edge closest to you and watch the bubble:
It moves towards you: correct, leave the switches alone.
It moves away from you: set
CONFIG_LEVEL_AXIS_INVERT_Y=y.It moves left or right instead: set
CONFIG_LEVEL_AXIS_SWAP_XY=yand repeat this step.
Raise the left edge. If the bubble moves right instead of left, set
CONFIG_LEVEL_AXIS_INVERT_X=y.Record the result as a per board default in samples/display/level/Kconfig.
The console prints the mapping in use at startup. Zephyr’s
sensor-axis-align devicetree convention would be the proper home
for the hardware half of this mapping, but the qst,qmi8658a binding
does not support it yet.
Troubleshooting
- The panel stays dark
Check that the board assigns a matrix to the
zephyr,displaychosen node. The build fails when no such node exists, but a board that chooses a different kind of display reports an unsupported pixel format at run time instead.- The corner colours are wrong or the image is mirrored
The panel is not wired the way the matrix node describes. Review the
circulative,serpentineandcolor-mappingproperties.- The dot runs the wrong way
The sensor is not oriented the way the panel is. Work through Axis mapping.
- The dot never leaves the middle, or pins to the rim at the slightest tilt
CONFIG_LEVEL_FULL_SCALE_TILTis too large or too small for the way you are holding the board. The console reports the measured angle in degrees, so compare that against the configured full scale.- The bubble will not sit still, or lags behind your hand
Adjust
CONFIG_LEVEL_FILTER_TIME_CONSTANT: raise it for steadiness, lower it for responsiveness. Set it to0to see the raw, unfiltered reading.- The colour flickers while the board is nearly level
Raise
CONFIG_LEVEL_LOCK_HYSTERESIS. Set it to0once to see the flicker it is there to prevent.
Dependencies
This sample uses the following Zephyr libraries:
-
include/zephyr/drivers/display.h
-
include/zephyr/drivers/sensor.h
Light-Emitting Diode (LED), by way of the LED strip matrix display driver
-
include/zephyr/kernel.h