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

Waveshare RP2350-Matrix

  • waveshare_rp2350_matrix/rp2350a/m33

  • waveshare_rp2350_matrix/rp2350a/hazard3

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

SWAP_XY

INVERT_X

INVERT_Y

RP2350-Matrix

yes

yes

no

anything else

no

no

no

To bring up a new board:

  1. Lay the board flat, LEDs up. Note where the corner test image puts its red pixel: that is the origin of the panel.

  2. 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=y and repeat this step.

  3. Raise the left edge. If the bubble moves right instead of left, set CONFIG_LEVEL_AXIS_INVERT_X=y.

  4. 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,display chosen 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, serpentine and color-mapping properties.

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_TILT is 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 to 0 to see the raw, unfiltered reading.

The colour flickers while the board is nearly level

Raise CONFIG_LEVEL_LOCK_HYSTERESIS. Set it to 0 once to see the flicker it is there to prevent.

Dependencies

This sample uses the following Zephyr libraries:

References