10.4. E-ink driver internals (register-level, from the kernel)¶
The e-ink software stack (Onyx EPDC) documents the e-ink stack from userspace down to the interface
of the built-in kernel driver (the /dev/ebc ioctls, the waveform, the
front-light class devices). Display pipeline — DSI transport to the color e-ink TCON documents the hardware
(the DSI transport, the panel geometry, EPD power in XBL). This page fills the
gap between them: the register-level internals of the in-kernel e-ink drivers
— the FPGA timing-controller command/flash protocol, the EPDC update engine and
its .wbf header parser, the EPD-PMIC register map, and the front-light
controller — recovered by static disassembly of artifacts/kernel.
It also serves as the complete function catalogue of the e-ink subsystem: the
Onyx EPDC is a large, heavily-instrumented built-in driver, and every function
named here was recovered from the driver’s own __func__ log strings.
Note
This is descriptive of this device’s kernel, not a build guide. Where a fact
was confirmed by reading the instructions of a specific function, the file
offset into artifacts/kernel is given (e.g. @0x592fb4); where it is
inferred from a function/DT name or a log string, that is stated. See
Method and confidence for the limits.
10.4.1. Method and confidence¶
The kernel is a raw, decompressed ARM64 Image with no ELF symbol table and no
usable kallsyms name table. Three independent evidence sources were combined:
Device tree (
artifacts/dtbo_a/overlay_0.dts) — authoritative for the hardware map: bus type, I²C/SPI addresses, GPIO numbers, regulators, panel timing. These are exact.Driver log strings (
strings artifacts/kernel) — the Onyx drivers log"%s(): ..."with__func__everywhere, so the string table yields the complete function inventory, the ioctl and command names, the.wbfheader field names, and the firmware filenames.Targeted disassembly — a base-free ADRP/ADD cross-referencer (the kernel virtual base cancels out of the page arithmetic, so string references resolve from file offsets alone) locates the function that uses a given log string; that function is then disassembled with
capstone. This recovered the register numbers, the VCOM formula, the SPI framing and the waveform-header offsets below.
Confidence caveats. Function start is taken as the first instruction after
the previous ret; for a few small or inlined functions two __func__
strings resolve to one start, so a bare address may name the enclosing function
rather than the exact callee — addresses are therefore cited only where the body
was read and confirmed. No code was executed and nothing was run on a device.
10.4.2. The hardware map (device tree)¶
Everything the drivers bind to, from overlay_0.dts (GPIOs are Qualcomm TLMM
numbers; see Pin control — TLMM and the PMIC GPIOs):
Device (compatible) |
Bus / addr |
Key properties |
|---|---|---|
EPD-FPGA TCON — |
bit-banged SPI, 10 MHz |
|
EPD-FPGA control — |
I²C, addr 0x34 |
|
Front-light (white/cold) — |
I²C, addr 0x38 |
|
Front-light (warm) — |
I²C, addr 0x36 |
|
Front-light abstraction — |
— |
|
The FPGA SPI bus (the parent of lfcpnx@0) is a GPIO-driven spi-gpio:
Signal |
TLMM GPIO |
|---|---|
SPI SCLK / MOSI / MISO |
56 / 45 / 46 ( |
SPI CS |
57 ( |
|
58 / 97 |
|
122 / 96 |
|
38 / 9 / 8 |
The program_* triple is the Lattice slave-SPI configuration handshake
(PROGRAMN/INITN/DONE), confirming the FPGA is field-configured over the same
bit-banged SPI (see The FPGA config flash / bitstream).
10.4.3. The Lattice ECP5 EPD-FPGA / TCON (lfe5u)¶
The timing controller is a Lattice ECP5-UM FPGA, part LFE5UM-85F
(lfe5um85f; the driver logs "chip_id[0x%x], force set LFE5UM_85F chip id").
It is driven by the onyx,lfcpnx (SPI) and onyx,lfe5u_i2c (I²C) drivers —
ONYX_TCON_DRIVER_VERSION_1.00. The FPGA is what actually generates the EPD
source/gate waveforms; the SoC only streams pixels to it (The e-ink software stack (Onyx EPDC)).
10.4.3.1. Register / command access¶
The FPGA exposes a register space reached two ways:
SPI —
lfe5u_read_reg/lfe5u_write_reg(and…_no_cmdvariants).I²C —
lfe5u_i2c_write_reg(@0x5afb3c) takes{i2c_addr, cmd, len_operand}(it logs exactly those on error) and issues ani2c_transfer; this is the addr-0x34 control channel.
The low-level SPI transfer — the flash routines and the …_no_cmd register
variants funnel through one helper at @0x5a9720 (a second entry @0x5a8bf8
adds a read phase). Decoded, its signature is xfer(dev, cmd_byte=w1, tx=x2,
tx_len=w3, rx=x4, rx_len=w5): it lays out a stack buffer
``[cmd_byte][tx…][rx region]``, sets speed_hz = 0x989680 (10 MHz), and
issues a single spi_sync. So each such operation is one SPI transaction whose
first byte is the command and whose remaining bytes are operands then read-back.
lfe5u_write_reg_no_cmd (@0x5a852c) uses this to send a 3-byte (24-bit)
big-endian register address.
The FPGA functional-command channel is I²C, not SPI. SPI (the @0x5a9720
helper and the 24-bit-address lfe5u_write_reg_no_cmd @0x5a852c) is used for
the config flash and low-level register-address access; the enumerated
CMD_TCON_* commands instead go over the I²C link to the FPGA (the
lfe5u_i2c DT node, client address ``0x34``). lfe5u_ctrl_send_cmd
(@0x5af9c8) looks the command up in a descriptor table and calls the sender at
@0x5afa80, which builds the packet ``[id_byte][data … len]`` and issues a
single i2c_transfer (@0xa71540). Register reads (e.g. VCOM) use an I²C read
(@0xa73708). lfe5u_ctrl_send_cmd_ext (@0x5afbe0) maps a raw command
byte to a table index through a jump table at file 0x1924558 before sending.
Note
The numeric ``CMD_TCON_*`` wire ids are recovered. lfe5u_ctrl_send_cmd
indexes two parallel tables by command index: a char * name table at file
0x1924710 and a 12-byte descriptor table at file 0x1924610, whose
layout is {u32 id; u32 payload_len; u32 cap}. The name pointers read as zero
in the static image (the kernel is CONFIG_RELOCATABLE; the pointers are filled
by RELA at boot), but the name strings sit in a contiguous pool at file
0x2f35689 in table order, so the pairing is exact. Three-way cross-check:
(a) id byte in the descriptor, (b) the ext jump table maps cmd byte →
index with cmd byte == id, and (c) lfe5u_ctrl_set_vcom logs
[CMD_TCON_VCOM] cmd[0x8] and lfe5u_ctrl_get_vcom I²C-reads register 8 —
so CMD_TCON_VCOM = id 0x08. Extraction script and raw table:
artifacts/re_static/cmd_tcon_ids.txt.
The full command set (index = table slot; id = first I²C byte):
# |
Command |
Id |
Len |
Function (from name) |
|---|---|---|---|---|
0 |
|
|
3 |
read firmware version (via the |
1 |
|
|
1 |
force a full GC (ghost-clearing) refresh |
2 |
|
|
1 |
automatic-mode control |
3 |
|
|
1 |
A2 mode frame count |
4 |
|
|
1 |
contrast |
5 |
|
|
1 |
select display mode |
6 |
|
|
1 |
select UI update mode |
7 |
|
|
1 |
VCOM voltage (value − |
8 |
|
|
1 |
image-filter parameter 1 |
9 |
|
|
1 |
image-filter parameter 2 |
10 |
|
|
1 |
A2 white binarisation threshold |
11 |
|
|
1 |
A2 black binarisation threshold |
12 |
|
|
1 |
enable input/update debounce |
13 |
|
|
1 |
debounce parameter |
14 |
|
|
1 |
debounce threshold |
15 |
|
|
1 |
auto framebuffer sync |
16 |
|
|
1 |
enable updates |
17 |
|
|
1 |
power-management state |
18 |
|
|
1 |
waveform-send state |
19 |
|
|
1 |
flag/status read |
20 |
|
|
1 |
error status |
Id bytes 0x03, 0x09, 0x0f, 0x12–0x14, 0x16–0x19 and
0x1b–0x1f are unassigned (reserved) in this firmware.
10.4.3.2. The FPGA config flash / bitstream¶
The FPGA has an attached SPI configuration flash the driver can read, erase, program and verify — i.e. the bitstream is field-upgradable:
lfe5u_flash_detect,lfe5u_flash_is_busy/…_check,lfe5u_flash_qpi_enable,lfe5u_flash_erase_chip/…_erase_cfg,lfe5u_program_cfg,lfe5u_erase_flash_config,lfe5u_upgrade_flash_config,lfe5u_verity_flash_config/lfe5u_verity_cfg;the bitstreams themselves are the six
lfcpnx/lfcpnx100_tcon_fw_{99,9e,9f,a2, a5,a7}.binblobs built into the kernel (The e-ink software stack (Onyx EPDC), Display pipeline — DSI transport to the color e-ink TCON).lfe5u_generate_firmware_filenamebuilds the name from an id; this PCB’s DT carries two fragments targeting this node (DTBO overlays), an earlier one setting ``fw-product-id = 0x9e`` and a later one overriding it to ``0xa7`` — under DT-overlay apply semantics the later fragment wins, so the running bitstream is ``lfcpnx100_tcon_fw_a7.bin``, not…_9e.bin(see Display pipeline — DSI transport to the color e-ink TCON for the fragment-level detail);lfe5u_load_fw/lfe5u_upgrade_firmware_from_builtin/lfe5u_parse_firmware_builtin/lfe5u_firmware_cont_builtin/lfe5u_check_firmware_version_builtinhandle loading and version-gating a bitstream from the built-in firmware against what is in flash.
The six bitstreams are embedded in artifacts/kernel (via
CONFIG_EXTRA_FIRMWARE) back-to-back in the CONFIG-listed order 9f, 9e, 99,
a2, a5, a7, each occupying a fixed 0x300010-byte slot (the build’s
firmware-embedding stride/padding — not the true payload size, see below).
Each carries a Lattice metadata block containing the signature "LSCC" and a
product-id/USERCODE word whose second byte is the product id: d3 9e 9a 09
for the 0x9e variant (at file 0x1d7b4c0), and likewise for
9f/99/a2/a5/a7. This is how the id in fw-product-id
selects the matching bitstream.
The bitstream container was decoded against the public ECP5 sysCONFIG /
Project Trellis bitstream-format spec (fetched from
YosysHQ/prjtrellis — not present in this dump, cited for the opcode table
only). Starting from the 9f
slot and scanning for the documented preamble FF FF BD B3, the region
0x1a0f562–0x1a7b4b0 (before the 9f USERCODE anchor) contains
eight separate, byte-exact command bursts, each of the form:
FF FF BD B3 preamble
FF [FF ...] dummy padding (Lattice "Dummy" opcode, 1 byte each)
3B 00 00 02 LSC_RESET_CRC (opcode 0x3B)
<12 bytes, unresolved — most likely LSC_WRITE_COMP_DIC (0x02) + 8 dictionary
pattern bytes for the compressor, per the spec's byte
count, but not confirmed byte-for-byte>
46 00 00 00 LSC_INIT_ADDRESS (opcode 0x46, info all-zero —
an exact match to the spec)
B8 <3-byte setup> LSC_PROG_INCR_CMP (opcode 0xB8 — **compressed**
frame programming, not the uncompressed 0x82 opcode)
The 3-byte setup word after B8 decodes cleanly per spec (1 CRC-compare bit +
1 CRC-at-end bit + 1 dummy-bits bit + 1 dummy-byte bit + 4-bit dummy-byte count +
16-bit frame count). All eight bursts set the “CRC-at-end” flag (one CRC-16
after the whole burst, not per frame) and carry a frame count of either
333 or 623: six bursts of 333 and two of 623, summing to 3244
frames sent — versus 13294 total configuration frames the LFE5UM-85F
actually has (per the Trellis device table). That gap is expected, not a
parsing error: LSC_INIT_ADDRESS/LSC_WRITE_ADDRESS let the bitstream
skip whole frame ranges that stay at their default (unconfigured) value, and a
DSI-to-TCON glue design plausibly uses only a small fraction of an 85K-LUT
fabric. The two device IDs for this family — 0x41113043 (LFE5U-85, no
SERDES) and 0x01113043 (LFE5UM-85, with SERDES) — both appear verbatim
or near 0x1924498/0x1924498+0x40, alongside the MXO chip-signature
table (E-ink driver internals (register-level, from the kernel)’s MXO section) rather than inside a live
VERIFY_ID (0xE2) command — i.e. that is the kernel driver’s own
device-recognition table, not proof of which of the two the bitstream’s own
VERIFY_ID targets (not located).
What this does and doesn’t settle: the container format — preamble, command opcodes, the compressed/multi-burst structure, per-burst frame counts — is now read directly off this device’s own bitstream bytes against the public spec, not assumed. What remains out of reach by any amount of further static effort is the compressed payload’s silicon meaning — which decompressed bit drives which physical LUT input, routing mux, or I/O buffer. That mapping is not part of the bitstream format; Project Trellis recovered it by differentially reverse-engineering the physical die against hundreds of known tool-generated designs, tile by tile, over several years — a different, much larger undertaking than decoding the bitstream container format; redoing that work from scratch is out of scope here. The FPGA is, regardless, fully driven and controllable through the I²C/SPI interfaces already documented above (flash programming) and in The e-ink software stack (Onyx EPDC) (the runtime TCON command set), which is what a replacement bootloader/driver actually needs.
The flash uses standard SPI-NOR opcodes, decoded from the cmd_byte passed
to the transfer helper:
Opcode |
Operation |
Framing / evidence |
|---|---|---|
|
Write Enable (WREN) |
sent before every erase/program ( |
|
Read Status Register (RDSR) |
polled for the WIP (busy) bit0 in a ~10001-iteration wait loop
( |
|
Read Data |
3-byte address ( |
|
Block Erase (64 KB) |
erase before rewriting a config/VCOM block ( |
|
Chip Erase |
full-flash erase ( |
|
config-access “enter” |
a descriptor at data |
Page-program and further flash commands are issued through a command-descriptor
table populated at probe time from the built-in firmware (the driver reads the
opcode as ldrb [desc, #off] at offsets 0x18/0x24/0x30/0xfc/0x108/0x114/
0x210/0x21c/0x234/0x240), so those specific bytes are firmware data rather than
code constants.
10.4.3.3. VCOM¶
VCOM (the common-plane bias, panel-specific) is stored in the FPGA config flash / OTP and mirrored to the filesystem, so it survives without a separate EEPROM. The sourcing priority, from the log strings, is:
kernel command line —
"vcom[%d uV] from cmdline"wins if present;FPGA flash —
lfe5u_flash_read_vcom/…_write_vcom/lfe5u_flash_vcom_sync("flash vcom and fs vcom is [%d]");filesystem —
lfe5u_fs_write_vcom("flash vcom[%d] sync to fs");default —
"Never set vcom. default vcom[%d]".
It is programmed with lfe5u_ctrl_set_vcom / …_set_vcom_workaround (there
is a documented vcom_workaround) and read with lfe5u_ctrl_get_vcom
(@0x5afd6c: reads FPGA I²C register 8, returns raw + 0x32);
lfe5u_key_write / lfe5u_otp_write write the protected/OTP region. The
flash side stores a 7-byte VCOM block (read flash [%x ×7] vcom[%d]) and the
driver keeps flash and fs in sync newest-wins (vcom_flash[%d] is older, so fs
vcom[%d] sync to flash / vcom_fs[%d], so flash vcom[%d] sync to fs).
A per-panel VCOM value can be recovered from a device backup, not only
from the FPGA config flash. The fs mirror is /onyxconfig/com.android.vcom
(the onyxconfig partition, captured as _READONLY/lun0/onyxconfig.bin),
and it contains an ASCII string — [REDACTED — unique per physical unit] —
that is the per-panel VCOM the driver loads (units are the driver’s internal
scale; conventionally the VCOM magnitude is expressed in centivolts). So the
per-panel calibration is not device-only after all: a backup that captures
the onyxconfig (or the FPGA config flash) partition carries it. A sibling
file in the same backup, /onyxconfig/test_result_info, carries a
fingerprint field (a build fingerprint string, not a per-unit value)
and three factory test-station timestamps ([REDACTED — narrows production
timeframe]) alongside the model string NoteAir5C — full breakdown in
The Onyx/Boox platform layer.
10.4.3.4. The onyx_tcon transport layer¶
Sitting on top of lfe5u_* is onyx_tcon_*, which the EPDC calls to move
pixels and issue commands:
Function |
Role |
|---|---|
|
queue and fire |
|
the pixel path — push an external framebuffer to the FPGA
( |
|
the low-latency pen path: a dedicated handwriting buffer streamed straight to the FPGA |
|
map an EPDC update to the FPGA’s mode/scheme/enable commands |
|
the DRM/SDE plane hook that feeds the transport |
|
bring-up, DT parse, command parse, resume re-sync |
Full lfe5u_* function list: probe/bring-up (lfe5u_probe,
lfe5u_i2c_probe, lfe5u_parse_dt, lfe5u_power_ctrl,
lfe5u_driver_init, lfe5u_i2c_driver_init); config
(lfe5u_config_done_wait / …_for_resume, lfe5u_program_cfg); registers
(lfe5u_read_reg / lfe5u_write_reg / …_no_cmd, lfe5u_i2c_write_reg,
lfe5u_ctrl_send_cmd / …_ext / lfe5u_ctrl_read_cmd_reg,
lfe5u_ctrl_dump); PM (lfe5u_suspend / lfe5u_resume /
lfe5u_sleep_in / lfe5u_sleep_out / lfe5u_pm_event_notify /
lfe5u_ctrl_reg_backup / …_restore / lfe5u_ctrl_wait_tcon_sleep /
…_wait_panel_power_down / lfe5u_wait_resume_late_comelete /
_lfe5u_shutdown); flash/VCOM/firmware as above.
10.4.4. The EPDC update engine (onyx_epdc / ebc)¶
The EPD controller is the built-in framebuffer driver onyx_epdc
(ONYX_EBC_DRIVER_VERSION_2.00), exposing /dev/ebc. It composes update
regions, looks up per-temperature waveform LUTs, and drives the transport above.
10.4.4.1. The /dev/ebc ioctl surface¶
The handler (epdc_ioctl, with a legacy ebc_ioctl alongside), its
dispatch mechanism, and the full ioctl command set / update-request struct
are documented once, in The /dev/ebc interface: ioctls, update modes, and the waveform format.
10.4.4.2. The LUT pipeline¶
Each update becomes a waveform look-up table appended to a bounded lut_list,
so several updates can be in flight and are checked for spatial overlap:
4 LUT slots — the driver traces
epdc_free_luts[0x%x][0x%x][0x%x][0x%x]andepdc_colliding_luts[…](collision detection between overlapping regions), plusepdc_active_luts;_onyx_epdc_produce_wf_sgbuilds the per-frame waveform scatter-gather;__onyx_refresh_waveform_one_frame/epdc_refresh_waveform_task/…_threaddrive frame emission;onyx_epdc_resize_lut_producehandles scaled regions;_onyx_epdc_extbuf_convert_grayconverts the RGBA source to the panel’s gray/CFA representation (4-bit / 16-level,wb_4bit);writeback merge —
mergeWbBySgPartUpdate/…FullUpdate/onyx_epdc_update_wb_sg/onyx_epdc_wb_restore_dither.
The pipeline is spliced into the MSM SDE atomic-commit path via _epdc
variants of the SDE functions — __msm_atomic_commit_epdc,
__sde_crtc_atomic_flush_epdc, __sde_plane_atomic_update_epdc,
__sde_plane_prepare_fb_epdc, _sde_plane_set_epdc_upd /
…_upd_cnt — i.e. the EPDC rides the normal DPU commit to get pixels to the DSI
intf, then the FPGA takes over.
10.4.4.3. Update modes and schemes¶
The named, special-cased paths — A2 (fast), GU/REGAL, CFA (colour), handwrite
and merge/snapshot — are documented once, in The /dev/ebc interface: ioctls, update modes, and the waveform format, alongside
the DT capability flags each one corresponds to. The pen fast path
specifically routes into onyx_tcon_handwrite_* (above).
Threading: the driver runs waveform/message worker threads
(epdc_msg_thread, epdc_wf_msg_thread, epdc_refresh_waveform_thread,
_onyx_epdc_submit_upd_work_func, onyx_epdc_done_work_func) with
power-down scheduling (onyx_epdc_set_pwrdown_delay, epdc_power_timeout).
10.4.4.4. Temperature compensation¶
Every update is temperature-compensated. onyx_epdc_read_temperature /
onyx_epdc_set_temp obtain the panel temperature (the log notes
temp_deteck_from_pmic — read from the EPD PMIC’s sensor) and select the
matching per-temperature LUT band from the waveform; userspace may also pass
temp in the update struct.
10.4.4.5. The .wbf waveform format¶
The waveform blob’s header fields, the mode/temperature/LUT tree, the temperature-band table, the LUT payload encoding, and the parse/expansion functions that implement them are documented once, in The /dev/ebc interface: ioctls, update modes, and the waveform format.
10.4.4.6. Panel timing knobs¶
onyx_epdc_set_timing / ebc_set_timing_buf build the panel timing buffer;
the driver reads these panel descriptors (DT / waveform): color_panel,
timing_version, waveform_file_format, half_empty, sdshr (source
shift-register direction), gdlr (gate-driver L/R, with a gdlr_gpio),
density (default 300 dpi), and xres_lcd_data / yres_lcd_data. The
SoC→TCON link timing has selectable forms — ebc_set_timing_buf_16bits_lvds /
…_v1 / …_v2 — matching the "panel_width %d not support" guard.
Selected onyx_epdc_* functions: bring-up (onyx_epdc_fb_probe,
onyx_epdc_fb_load_driver, onyx_epdc_mfd_probe, onyx_epdc_parse_dt,
onyx_epdc_LCDIF_init / …_isr_callback); power
(onyx_epdc_power_init / …_powerup / onyx_epdc_regulator_init /
onyx_epdc_reset); geometry (onyx_epdc_set_screen_info /
…_transform_rotation / …_resize_and_rotation_to_wfdfb); memory
(onyx_epdc_mempool_cteate [sic] / onyx_epdc_mdp_buffer_malloc /
…_free); waveform fw (onyx_epdc_init_wavefw / onyx_epdc_fb_fw_handler);
capture (ebc_* buffer/mmap/open path). epdc_gdlr_gpio toggles the
gate-driver direction pin.
10.4.5. The MXO source-driver and its NVCM firmware¶
A second EPD-controller driver, ``onyx,mxo`` (mxo_probe / mxo_parse_dt),
is compiled in. MXO is an EPD source-driver IC reached over I²C (its command
sender at @0x5a56a0 builds a [cmd][data…] packet and issues a two-message
i2c_transfer @0xa71540), with a 30 V source-rail enable (mxo_30v_en_gpio)
and a mxo-reset line.
Note
Like the EPD-PMICs below, onyx,mxo is compiled in but not device-tree
bound on this unit (the string appears in the driver but in no
board.dts/overlay node) — the Lattice ECP5 FPGA TCON drives the panel’s
source/gate lines directly here. The MXO NVCM images still ship as built-in
firmware, and the loader is fully recovered.
The MXO controller takes firmware in two forms, chosen by a device flag
([dev+0x114]): a volatile SRAM image (.bit) or a non-volatile
NVCM config image (.ied). mxo_generate_firmware_filename (@0x5a5580)
builds the name from the template ``mxo/%s_%s_%x%s`` →
mxo/<chip>_<nvcm|sram>_<id><.ied|.bit>, where:
<chip>ismxo1300ormxo4300, selected from a chip table (file0x1924168) by a 4-byte chip signature read from the part —"A+ C"(0x43202b41) →mxo1300,"A+@C"(0x43402b41) →mxo4300(else"unknow");<id>is the product-id byte (hex):81–87.
That yields exactly the eight built-in blobs
(mxo1300_nvcm_{81,82,83,84,86,87}.ied + mxo4300_nvcm_{81,82}.ied —
Display pipeline — DSI transport to the color e-ink TCON). mxo_upgrade_firmware_from_builtin loads the image
and downloads it verbatim to the source driver over I²C. The .ied payload is
the source driver’s raw register/config stream; its internal register semantics
need the vendor datasheet (not in the dump). Details:
artifacts/re_static/item5_mxo_nvcm.txt.
Note
Byte-exact carve. All eight .ied blobs were carved out of
artifacts/kernel byte-exactly. Method: the kernel’s
built-in-firmware table’s size fields survive even though the adjacent
name/data pointers are RELA-zeroed (the same “read the literal integers,
not the zeroed pointers” trick used for the CMD_TCON table above) —
a 15-entry, 24-byte-stride array at file
0x31452c0 gives the exact size of eink_waveform.wbf plus all 8
.ied blobs plus all 6 lfcpnx bitstreams (the latter independently
cross-checking at exactly 0x300010 bytes each against the FPGA
section’s own finding above). A literal firmware-filename string pool at
file 0x307a5e0 gives the build order: the interleaved sequence
(mxo1300_81, mxo4300_81, mxo1300_82, mxo4300_82, mxo1300_83,
mxo1300_84, mxo1300_86, mxo1300_87). Concatenating the 9 sizes
(waveform + 8 .ied) with zero padding and working backward from
this section’s lfcpnx100_tcon_fw_9f.bin anchor (0x1a0f562) lands
the 8 .ied boundaries exactly on visibly structured, non-degenerate
data at every single one — carved copies are at
artifacts/re_static/ied_carved/*.ied. (The waveform’s own leading
bytes are less certain — its built-in copy is a different revision from
the extracted eink_waveform.wbf, sharing no string or checksum bytes
with it anywhere in the kernel — but its end boundary is pinned exactly
by the same arithmetic.)
The payload is empirically structured, not opaque — two distinct
byte-level families appear across the 8 blobs: a low-entropy family
(mxo1300_nvcm_{81,82}, mxo4300_nvcm_81) built from fixed 8- or
24-byte records (one clearly holds a monotonically-changing 16-bit ramp
column next to a linearly-incrementing index column — the same shape as
the FP9931 VPOS/VNEG threshold table above, just a different IC’s
version), and a higher-entropy family (mxo1300_nvcm_{83,84,86,87},
mxo4300_nvcm_82) built from densely-repeated small multi-level byte
motifs (0x55/0xaa-style alternating fills). This confirms real
internal framing (record boundaries, address/index vs. value columns) —
what remains unrecovered is purely semantic (which register a byte
addresses, what physical quantity a ramp value represents), which
does need the MXO/Ilitek datasheet. Full byte evidence:
artifacts/re_static/item_mxo_wacom_recheck.txt (§1).
``libonyx_dsl.so`` checked and ruled out. This library was suspected
of being a vendor DSL interpreter that might drive the MXO register
stream. Its actual exports (DslDictionaryProvider_getEntry,
DslContext::addEntry etc.) show it is Onyx’s parser for the ABBYY
Lingvo ``.dsl`` dictionary file format, used by the on-device
dictionary lookup feature — “DSL” here is the dictionary format’s own
name, unrelated to hardware. No mxo/tcon/i2c/register string appears in
it. This avenue is closed.
10.4.6. The EPD PMIC drivers (FP9931 / MAX17135)¶
Two EPD-PMIC drivers are compiled in: FP9931 (Fitipower, fitipower,fp9931)
and MAX17135 (Maxim, Maxim,max17135, “PMIC MAX17135 for eInk display”).
Note
Neither EPD-PMIC appears as a device node in this unit’s overlay_0.dts —
on the Note Air5 C the Lattice FPGA is the runtime EPD power/timing
controller. The FP9931/MAX17135 drivers are present for sibling
panels/PCB variants and mirror the EPD-power code XBL runs for the boot splash
(fp9931_power_on / max17135_power_on in XBL DXE,
Display pipeline — DSI transport to the color e-ink TCON). The register map below is real and useful (it is
the same silicon), but is not the bind path on this device.
10.4.6.1. FP9931 register map (disassembly-confirmed)¶
Register writes go through fp9931_reg_write (@0x7d6b70; signature
(client, reg=w1, val=w2) → I²C write at @0xa737c0), reads through
fp9931_reg_read. From fp9931_vpos_vneg_set_voltage /
_fp9931_vcom_set_voltage (@0x64a658):
Reg |
Meaning |
|---|---|
|
VCOM setting (selector). The stored VCOM value is scaled (× 1000) and
converted to an 8-bit selector by a reciprocal-multiply ( |
|
VPOS/VNEG (source-driver rail). The target × 1000 is threshold-matched
against a 256-entry table (at file |
|
control / enable registers (written by |
After setting the rails the driver polls power-good
(fp9931_wait_power_good: up to 50 retries with a delay, checking a status
register). Other setters cover panel timing: fp9931_set_vgh_extension_time /
…_vgl_extension_time / …_vghnm_extension_time /
…_vghnm_voltage_percent / …_xon_delay_time / …_xon_len_time /
…_disable_delay_time, plus night-mode (fp9931_config_night_mode_set /
…_unset) and the fp9931-hwmon temperature sensor
(drivers/hwmon/fp9931-hwmon.c). fp9931_setup / fp9931_display_enable /
…_disable / …_is_enabled gate the high-voltage output.
10.4.6.2. MAX17135 (alternative)¶
max17135_probe / …_detect ("Max17135 PMIC not found!"),
max17135_regulator_probe / …_shutdown, DT parse
(max17135_i2c_parse_dt_pdata / max17135_pmic_dt_parse_pdata), power
sequencing (__max17135_display_powerup, __max17135_v3p3_powerup /
…_powerdown, __max17135_suspend / …_resume), VCOM
(max17135_vcom_is_enabled), power-good (max17135_wait_power_good), and a
max17135_sensor hwmon ("program timing1-timing8", HVINP range check). All
I²C access is logged as "Unable to {read,write} MAX17135 register[0x%x] …".
10.4.7. The front-light (LM3630A ×2)¶
Two LM3630A LED drivers, on I²C 0x38 (white/cold) and 0x36 (warm),
driven by Onyx’s own onyx,lm3630a_v3 driver
(CONFIG_ONYX_BACKLIGHT_LM3630A=y; mainline BACKLIGHT_LM3630A is off).
Each chip is dual-bank (dual-ctrl); lm3630a_set_br_ct /
lm3630a_max_ma_set / lm3630a_i2c_read / …_write do the register I/O;
lm3630a_match_chip / lm3630a_v2 / …_v3 select the register variant;
lm3630a_chip_init / …_hw_init / …_power_enable bring it up. The
per-step current is taken from the DT brightness/colour-temperature tables
(table_ba/table_ca phandles → lm3630_table_03..06 curves,
table-a-ma = 14 mA).
Above the two chips sits the onyx,backlight abstraction (onyx_bl_*:
onyx_bl_probe / …_chip_register / onyx_bl_br_update /
onyx_bl_ct_update / onyx_bl_set), which presents the user-facing
brightness (onyx_bl_br) and colour-temperature (onyx_bl_ct)
sliders (33 steps each, br-max=ct-max=0x21) and decomposes them into the warm
and cold channels. The four class devices and their sysfs nodes are in
The e-ink software stack (Onyx EPDC).
10.4.8. What a from-scratch driver still needs¶
Combined with The e-ink software stack (Onyx EPDC) and Display pipeline — DSI transport to the color e-ink TCON, the
SPI framing (10 MHz, [cmd][tx][rx] spi_sync), the
flash command set (WREN/RDSR/read/erase + the config-access descriptor), the
I²C CMD_TCON command channel ([id][data] to FPGA 0x34) with every
numeric command id recovered — cross-checked three ways: the kernel’s
descriptor table (@0x1924610), the ext jump table, and the VCOM
register-8 anchor — the EPD-PMIC register map and VCOM formula, the full
.wbf mode/temperature/LUT layout, and how a panel’s actual VCOM value
can be recovered from onyxconfig.bin are all decoded above. The
remaining caveats are of a different kind:
The FPGA fabric logic itself. The six
lfcpnx100_tcon_fw_*.binblobs are ECP5 configuration bitstreams (see The FPGA config flash / bitstream); their internal gate-level behaviour is opaque without full Project-Trellis reconstruction. This is not needed to write a driver: the FPGA’s interface (the I²CCMD_TCON_*command set and the SPI flash/register access) is what a host talks to, and that is fully documented here.On-device validation. Every value above is read statically from the binaries and the backup (
persist.binandonyxconfig.binsupply the device-resident state; the FPGA’s own config flash is the only store not present, and its load-bearing content, VCOM, is mirrored intoonyxconfig); none has been exercised on hardware, so a from-scratch driver should still confirm timing and power sequencing on a live panel.
10.4.9. Provenance¶
- Source:
artifacts/kernel(built-inonyx_epdc/lfe5u/onyx_tcon/fp9931/max17135/lm3630adrivers — disassembled with the base-free ADRP/ADD cross-referencer +capstone; anchors cited by file offset),artifacts/dtbo_a/overlay_0.dts(the hardware map: FPGA SPI/I²C, GPIOs, rails,fw-product-id, LM3630A addresses/tables), the driver’s__func__/ log string table (the function inventory,CMD_TCON_*andSET/GET_EBC_*sets, the.wbfheader field names), andsystem_a.img’swaveform/eink_waveform.wbf(parsed against the recovered header map — full walk inartifacts/re_eink/wbf_parse.txt), and the device backup’s_READONLY/lun0/onyxconfig.bin(com.android.vcom= the panel’s VCOM value;test_result_info) andpersist.bin; the built-in-firmware size table (file0x31452c0) and filename string pool (file0x307a5e0) used to byte-exact-carve the 8 MXO.iedblobs, andartifacts/onyx_libs/ libonyx_dsl.so(checked and ruled out as an MXO interpreter) — full evidence inartifacts/re_static/item_mxo_wacom_recheck.txt.- Method:
strings + device-tree reading; targeted static disassembly (no ELF symbols / no
kallsyms— function starts located via string cross-references, then read withcapstone). Scripts and full annotated listings are underartifacts/re_eink/. No code executed; nothing run on a device.- Cross-refs:
The /dev/ebc interface: ioctls, update modes, and the waveform format (the ioctl set, update modes and
.wbfwaveform format this driver implements), The e-ink software stack (Onyx EPDC) (userspace glue, front-light class devices, userspace libraries), Display pipeline — DSI transport to the color e-ink TCON (DSI transport, panel geometry, EPD power in XBL), Pin control — TLMM and the PMIC GPIOs (the FPGA control GPIOs and front-light enables), Firmware-blob inventory (the payloads) (the built-in TCON/waveform/NVCM firmware), Display drivers and HAL (where the e-ink driver sits among the kernel modules).