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 .wbf header 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 — onyx,lfcpnx (lfcpnx@0)

bit-banged SPI, 10 MHz

spi-max-frequency = 0x989680; rails vdd1v8_uV=1.8 V / vdd1v2_uV=1.2 V; fw-product-id — two DT fragments target this node, an earlier one sets 0x9e, a later one overrides it to 0xa7 (see DTBO overlays, Display pipeline — DSI transport to the color e-ink TCON)

EPD-FPGA control — onyx,lfe5u_i2c (lfe5u_i2c@34)

I²C, addr 0x34

cfa_mode=1, epdc-a2-enhance-enable, epdc-gu-regal-enable, epdc-regal_nm-enable

Front-light (white/cold) — onyx,lm3630a_v3 (lm3630a@38)

I²C, addr 0x38

bank-a-name="white", en_gpio=73, table_ba=0x61/table_ca=0x62

Front-light (warm) — onyx,lm3630a_v3 (lm3630a@36)

I²C, addr 0x36

bank-a-name="warm", en_gpio=11, table_ba=0x63/table_ca=0x64

Front-light abstraction — onyx,backlight (onyx_bl)

bl-br-name="onyx_bl_br", bl-ct-name="onyx_bl_ct", br-max=ct-max=0x21 (33 steps)

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-gpios)

SPI CS

57 (spi-cs-gpios)

fpga_12v_core_en / fpga_30v_en

58 / 97

reset (active-low) / standby (active-low)

122 / 96

program_enable (PROGRAMN) / program_init (INITN) / program_done (DONE)

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:

  • SPIlfe5u_read_reg / lfe5u_write_reg (and …_no_cmd variants).

  • I²Clfe5u_i2c_write_reg (@0x5afb3c) takes {i2c_addr, cmd, len_operand} (it logs exactly those on error) and issues an i2c_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):

The CMD_TCON_* command set (recovered numeric ids)

#

Command

Id

Len

Function (from name)

0

CMD_TCON_VERSION

0xe0

3

read firmware version (via the _ext read path; 0xe0 handled specially)

1

CMD_TCON_GC_FULL_MODE

0x01

1

force a full GC (ghost-clearing) refresh

2

CMD_TCON_AUTO_MODE_CTRL

0x02

1

automatic-mode control

3

CMD_TCON_A2_FRAME_NUM

0x04

1

A2 mode frame count

4

CMD_TCON_CONTRAST

0x05

1

contrast

5

CMD_TCON_DISPLAY_MODE

0x06

1

select display mode

6

CMD_TCON_UI_MODE_SET

0x07

1

select UI update mode

7

CMD_TCON_VCOM

0x08

1

VCOM voltage (value − 0x32; see VCOM)

8

CMD_TCON_FILTER_P1

0x0a

1

image-filter parameter 1

9

CMD_TCON_FILTER_P2

0x0b

1

image-filter parameter 2

10

CMD_TCON_A2_THRESHOLD_WHITE

0x0c

1

A2 white binarisation threshold

11

CMD_TCON_A2_THRESHOLD_BLACK

0x0d

1

A2 black binarisation threshold

12

CMD_TCON_DEBOUNCE_ENABLE

0x0e

1

enable input/update debounce

13

CMD_TCON_DEBOUNCE_PARM

0x10

1

debounce parameter

14

CMD_TCON_DEBOUNCE_THRESHOLD

0x11

1

debounce threshold

15

CMD_TCON_FB_SYNC_AUTO

0x15

1

auto framebuffer sync

16

CMD_TCON_UPDATE_ENABLE

0x1a

1

enable updates

17

CMD_TCON_PM_STATE

0x20

1

power-management state

18

CMD_TCON_WF_SEND_STATE

0x21

1

waveform-send state

19

CMD_TCON_FLG_STATE

0x22

1

flag/status read

20

CMD_TCON_ERROR

0x23

1

error status

Id bytes 0x03, 0x09, 0x0f, 0x120x14, 0x160x19 and 0x1b0x1f 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}.bin blobs built into the kernel (The e-ink software stack (Onyx EPDC), Display pipeline — DSI transport to the color e-ink TCON). lfe5u_generate_firmware_filename builds 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_builtin handle 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 0x1a0f5620x1a7b4b0 (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

0x06

Write Enable (WREN)

sent before every erase/program (@0x5ae034 etc.)

0x05

Read Status Register (RDSR)

polled for the WIP (busy) bit0 in a ~10001-iteration wait loop (lfe5u_flash_is_busy_check); rx_len=1

0x03

Read Data

3-byte address (tx_len=3), e.g. VCOM read pulls rx_len=0x20 (32 B)

0xD8

Block Erase (64 KB)

erase before rewriting a config/VCOM block (@0x5a82c8)

0xC7

Chip Erase

full-flash erase (lfe5u_flash_erase_chip @0x5ae198)

0x3a (+ operands)

config-access “enter”

a descriptor at data @0x380d870+0x240 (opcode 0x3a, len 3), sent with CS/reset held, to open the FPGA’s background/flash path

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:

  1. kernel command line"vcom[%d uV] from cmdline" wins if present;

  2. FPGA flashlfe5u_flash_read_vcom / …_write_vcom / lfe5u_flash_vcom_sync ("flash vcom and fs vcom is [%d]");

  3. filesystemlfe5u_fs_write_vcom ("flash vcom[%d] sync to fs");

  4. 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

onyx_tcon_send_cmd / onyx_tcon_cmd_queue / onyx_tcon_cmd_triger [sic]

queue and fire CMD_TCON_* commands

onyx_tcon_update_image / onyx_tcon_display_extbuf (+ …_for_sync / …_backup_from_fb / …_sync_work_commit)

the pixel path — push an external framebuffer to the FPGA (MDP_FB_FLAG_SYNC_TCON); onyx_tcon_sync_hw_buf_with_fb / …_sync_wb_with_fb keep hardware/writeback buffers coherent

onyx_tcon_handwrite_buf_malloc / onyx_tcon_handwrite_create_cmd_packet

the low-latency pen path: a dedicated handwriting buffer streamed straight to the FPGA

onyx_tcon_set_display_mode / onyx_tcon_set_a2_mode_type / onyx_tcon_set_upd_scheme / onyx_tcon_set_update_enable

map an EPDC update to the FPGA’s mode/scheme/enable commands

onyx_tcon_drm_mode_setplane / onyx_tcon_ext_buf_sync_with_fb

the DRM/SDE plane hook that feeds the transport

onyx_tcon_init / onyx_tcon_parse_dt / onyx_tcon_parse_cmd / onyx_tcon_resume_sync_fb

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] and epdc_colliding_luts[…] (collision detection between overlapping regions), plus epdc_active_luts;

  • _onyx_epdc_produce_wf_sg builds the per-frame waveform scatter-gather; __onyx_refresh_waveform_one_frame / epdc_refresh_waveform_task / …_thread drive frame emission; onyx_epdc_resize_lut_produce handles scaled regions;

  • _onyx_epdc_extbuf_convert_gray converts 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> is mxo1300 or mxo4300, selected from a chip table (file 0x1924168) 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): 8187.

That yields exactly the eight built-in blobs (mxo1300_nvcm_{81,82,83,84,86,87}.ied + mxo4300_nvcm_{81,82}.iedDisplay 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

0x01

VCOM setting (selector). The stored VCOM value is scaled (× 1000) and converted to an 8-bit selector by a reciprocal-multiply (× 0x357a1947, asr #44 — a fixed-point divide, ≈ 19.6 mV per selector step) then written to reg 1.

0x02

VPOS/VNEG (source-driver rail). The target × 1000 is threshold-matched against a 256-entry table (at file @0x193d338 — first entry greater than the target wins); the resulting index (valid range 0–0x3f) is the selector written to reg 2.

0x0a / 0x0b

control / enable registers (written by fp9931_set_nigth_mode [sic], fp9931_set_v3p3, fp9931_v3p3_disable, fp9931_regulator_probe)

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_*.bin blobs 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²C CMD_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.bin and onyxconfig.bin supply the device-resident state; the FPGA’s own config flash is the only store not present, and its load-bearing content, VCOM, is mirrored into onyxconfig); 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-in onyx_epdc / lfe5u / onyx_tcon / fp9931 / max17135 / lm3630a drivers — 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_* and SET/GET_EBC_* sets, the .wbf header field names), and system_a.img’s waveform/eink_waveform.wbf (parsed against the recovered header map — full walk in artifacts/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) and persist.bin; the built-in-firmware size table (file 0x31452c0) and filename string pool (file 0x307a5e0) used to byte-exact-carve the 8 MXO .ied blobs, and artifacts/onyx_libs/ libonyx_dsl.so (checked and ruled out as an MXO interpreter) — full evidence in artifacts/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 with capstone). Scripts and full annotated listings are under artifacts/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 .wbf waveform 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).