Driving the display
The device's screen is a Sharp LS021B7DD02 — a 2.13″, 240 × 320 reflective memory-in-pixel (MIP) LCD. "Memory-in-pixel" is the whole story: every subpixel has a 1-bit latch on the glass. Write a bit and the panel holds that image with zero bus traffic — you only spend power when the picture actually changes, which is exactly what an always-on, battery-bound device wants.
Colour is RGB222 — 2 bits per channel → 64 colours — and the panel is normally black (an unwritten, all-zero panel is dark). Two facts about how you talk to it shape everything below:
- The interface is parallel, not SPI. Pixel data goes out on a 6-bit parallel source bus plus a handful of control lines.
- The stored bits don't drive the liquid crystal directly. A separate, continuously running AC waveform does; the stored bit only chooses which way each subpixel leans.
This page is the protocol — conceptual model first, then the mechanical detail and the signal reference. The firmware that implements it is split three ways: the M33 COM-waveform driver, the FLPR scan blob that clocks the gate and source buses, and the host-tested wire-format packer.
Two signal paths that never meet
The single most useful way to think about this panel is that two unrelated jobs run on two different sets of pins, and they don't talk to each other.
VCOM/VA/VB) is a free-running ~60 Hz square wave that physically drives the liquid crystal; the stored bit just routes each subpixel to follow the VA rail (→ white) or the VB rail (→ black).The consequence for firmware is that COM is its own concern. The liquid crystal must never see a DC bias — a steady field degrades the cells — so VCOM/VA/VB have to keep alternating the entire time the panel is powered, even on a perfectly static image. Generate them on a hardware timer or dedicated task that runs autonomously and never stalls behind the image write. The write path may be slow and bursty; the polarity path cannot pause.
Pixels: RGB222 by area gradation
Each channel carries 2 bits, but the panel has no analog grey — it fakes four levels per channel with area gradation. Every subpixel is physically split into two 1-bit on/off blocks of different size:
A channel's 2-bit level therefore maps to a (MSB, LSB) pair and a lit area:
Level l | (MSB, LSB) | Lit area | Appears as |
|---|---|---|---|
| 0 | (0, 0) | 0 | black |
| 1 | (0, 1) | 1/3 | LSB / middle band |
| 2 | (1, 0) | 2/3 | MSB / top + bottom |
| 3 | (1, 1) | 3/3 | full (channel on) |
Bit extraction from a level is just msb = (l >> 1) & 1, lsb = l & 1 — so an RGB222 framebuffer maps onto the wire with no lookup.
The source bus is odd/even interleaved
The 6 data lines carry two adjacent pixels at once — one bit each for the odd and the even column:
| Lines | Pixel | Channel |
|---|---|---|
R[0] G[0] B[0] | odd column | R / G / B |
R[1] G[1] B[1] | even column | R / G / B |
So each BCK edge clocks one pixel pair (two columns) — the panel latches the source bus on both edges of BCK (DDR). Drive a distinct pair on the rising edge and the next pair on the falling edge, and the 240 columns ship in 60 BCK cycles of data per sub-line.
Both edges, learned the hard way. It's natural to assume one pair per
BCKcycle (120 cycles for 240 columns) and hold the data steady across the whole period. Do that and the panel captures each pair twice — every pair lands in four columns, so the left half of the frame stretches to fill the screen, the right half drops, and the 64-colour gamut collapses to 32. It's invisible on solid fills (uniform either way), which is why it can hide for a long time; fine vertical detail (1-px lines, text) is what exposes it. The fix is to feed a new pair on each edge (DDR), which also clocks the line out ~2× faster — and is why the panel's ~53 ms full-frame spec is actually achievable at the ratedBCK.
One gate line, two area blocks
Here is the key to the whole protocol — easy to miss in the datasheet's timing charts:
A pixel row is ONE gate line. The display has exactly 320 gate lines (
L1…L320), one per pixel row. The "three bands" above are the area layout inside a single cell — not three separate gate lines.
To write a row you transfer the MSB data and the LSB data into that same gate line. The only thing that routes a latch to the 2/3 (MSB) block versus the 1/3 (LSB) block is the level of GCK:
GCKHIGH → MSB phase → the latch writes the 2/3-area cells.GCKLOW → LSB phase → the latch writes the 1/3-area cells.
There is no separate MSB/LSB select pin — GCK level is the area-block selector. So one pixel row is one GCK period: a rising edge that both advances the gate and opens the MSB phase, then a falling edge that holds the same row for the LSB phase.
GCK is high (latched into the 2/3 block by a GEN pulse), then the LSB plane while GCK is low (latched into the 1/3 block). The gate advances on the GCK rising edge, so a single gate line stays selected across both phases of its period.Writing a frame
A frame is bracketed by INTB and is a gate scan over the 320 rows. Two non-obvious rules are called out in the pseudocode:
# RULE 1: INTB must be HIGH for the whole frame, or nothing latches.
INTB = HIGH
GSP = pulse # frame start; loads the first gate
leading dummy gate advances # pipeline fill (a few GCK periods, no GEN/BCK);
# release GSP on the very first GCK edge
for each of 320 pixel rows: # one GCK PERIOD per row — RULE 2
GCK = HIGH # rising edge advances the gate to this row
shift MSB plane (BSP + 60 DDR BCK) # 2/3-area data, a pair per BCK edge
GEN = pulse # latch the 2/3 (MSB) block [GCK still HIGH]
GCK = LOW # same gate line, NOT an advance
shift LSB plane (BSP + 60 DDR BCK) # 1/3-area data
GEN = pulse # latch the 1/3 (LSB) block [GCK LOW]
trailing dummy gate advances # flush / "necessary signal" blank
INTB = LOW # end of frame; panel now holds the image
What the counts mean:
INTBHIGH = "write enabled" for the whole frame.INTBLOW is the inter-frame Hold state: the panel ignores the gate/source scan and keeps its current memory.- One
GCKperiod per pixel row → 320 gate advances, not 640. The vertical-timing chart shows ~640GCKmarks because those are edges (320 periods × 2 phases); with the bracketing dummies it totals ~648 edges. - Two
GENpulses per row — one in the MSB (high) phase, one in the LSB (low) phase. - 120 pixel-pair columns per sub-line + a few dummy/flush columns that push the last columns through the source shift register. Because the panel is DDR (a pair per
BCKedge, see above), those 120 columns are clocked in ~60BCKcycles, not 120. GSPis pulsed once at frame start and released on the firstGCKedge so its high overlapsGCK(1).
Partial update — a span-masked gate scan
Because pixel memory is retained you do not have to rewrite the whole frame, and the firmware doesn't. The FLPR backend drives a span-masked scan: given the row-spans that changed, it fast-forwards GCK over every clean row — GEN inactive, so nothing latches — does the shift-and-latch work only on the dirty rows, and stops early after the last one (drop INTB; the panel holds everything below). A skipped row costs one fast GCK advance instead of two full sub-line writes, so a frame scales with the number of changed rows, not a flat 320.
The grain is a whole row, though: touching any row re-latches all 240 of its columns — the source shift register feeds the entire line — so there is no cheap "just these few columns." A partial update is a set of full-width row-spans; a 16-px-wide right-edge overlay still rewrites its rows full-width, and is cheap only because it touches few rows, not few columns.
The hold-progress bulge rides exactly this: as it animates, only its rows re-push — a fraction of a full-frame scan — while the rest of the map stays untouched and asleep. And the map present rides it too: it keeps a per-row hash of the last-pushed frame and feeds the masked scan only the rows that actually changed, so an idle Home clock ticking a minute repaints its clock band and nothing else — no per-screen code, the change detected automatically in the present layer. It's the renderer's redraw-only-what-changed design carried onto the glass, and for a UI that changes a few fields per second, a large power win.
Power-on, power-off, and retention
The never-DC rule (Path B) dictates a strict order:
- Power-on: write an all-black frame first (Path A), then start the
VCOM/VA/VBwaveform (Path B), with a small settle (datasheet ≥ 30 µs) before COM begins. The panel is now in a known, bias-free state before any AC drive. - Power-off: reverse it — bring the content to the safe state before stopping COM. Never cut COM while a biased image is held.
- Retention: the image holds indefinitely for practical purposes, but refresh it at least every ~2 days rather than holding a single static frame longer than that.
Signal & timing reference
Pins
| Signal | Group | Rail | Role |
|---|---|---|---|
GSP | Gate | VDD2 (~5.0 V) | Gate start pulse — once per frame |
GCK | Gate | VDD2 | Gate clock and MSB/LSB phase select (HIGH = MSB/⅔, LOW = LSB/⅓); one period per row |
GEN | Gate | VDD2 | Gate output enable — pulsed once per phase to latch the selected block |
INTB | Gate | VDD2 | Frame envelope — HIGH for the whole frame; LOW = Hold (no write) |
BSP | Source | VDD1 (~3.2 V) | Sub-line (start-of-line) pulse |
BCK | Source | VDD1 | Source shift clock — 124 per sub-line (120 data + 4 dummy) |
R[0] G[0] B[0] | Source data | VDD1 | Odd-column R/G/B bit |
R[1] G[1] B[1] | Source data | VDD1 | Even-column R/G/B bit |
VCOM | COM | — | Common AC reference (free-running ~60 Hz) |
VB | COM | — | In phase with VCOM — the "black" rail |
VA | COM | — | Inverse of VCOM — the "white" rail |
Timing (datasheet values are the envelope, not a target)
| Parameter | Value | Notes |
|---|---|---|
BCK max frequency | 0.758 MHz | Source/shift clock ceiling |
BCK min high / low | 660 ns each | Honour as minimum widths |
| COM frequency | 54–66 Hz (60 typ) | 48–52 % duty |
| COM edge (rise/fall) | ≤ 100 µs | Into ~56–77 nF per line; high-drive GPIO / buffer as needed |
GCK ↔ GEN setup & hold | ≥ 16.37 µs | Keep GEN clear of GCK edges |
GEN high width | ≥ 24.56 µs | Valid-output / latch window |
| Power-on settle (black → COM) | ≥ 30 µs | See power-on order above |
| Full-frame update | ~18 Hz (~55.6 ms) | At datasheet-nominal clocking |
A bring-up driver may deliberately clock far slower than these maxima (e.g. BCK in the low-hundreds-of-kHz) so a logic analyzer resolves every edge — that's fine; the table is the envelope. The minimum widths and the GCK↔GEN setup/hold are the relationships that actually matter for correct latching.
Power — the figures
This is why the panel suits an always-on device. Typical module power (datasheet, excludes the COM capacitive-load current):
| State | Power |
|---|---|
| Hold / static | ≈ 32 µW |
| 1 Hz update | ≈ 45 µW |
| 30 Hz update | ≈ 290 µW |
The panel itself is nearly free at rest; cost scales with how often you rewrite — which is exactly why the partial-update strategy above matters.
Where this lives
- The COM polarity waveform on the M33 — and its zero-CPU
TIMER→DPPI→GPIOTEvariant:obc-fw-nrf54l/src/com.rs - The gate/source scan that writes a frame — the
FLPRcoprocessor blob:flpr/flpr_scan.c - The host-tested wire-format pack (the area-gradation bit packing this page describes):
obc-display/src/ls021/wire.rs - How a rendered frame reaches the panel — the banded push and the RGB222 framebuffer: rendering pipeline
- The hardware overview: Hardware