========================================================== E-ink driver internals (register-level, from the kernel) ========================================================== :doc:`stack` 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). :doc:`/display/panel` 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. 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. The hardware map (device tree) ============================== Everything the drivers bind to, from ``overlay_0.dts`` (GPIOs are Qualcomm TLMM numbers; see :doc:`/soc/pinctrl`): .. list-table:: :header-rows: 1 :widths: 30 22 48 * - 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 :doc:`/boot/dtbo`, :doc:`/display/panel`) * - 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``: .. list-table:: :header-rows: 1 :widths: 44 56 * - 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`_). 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 (:doc:`stack`). Register / command access ------------------------- The FPGA exposes a register space reached two ways: - **SPI** — ``lfe5u_read_reg`` / ``lfe5u_write_reg`` (and ``…_no_cmd`` variants). - **I²C** — ``lfe5u_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): .. list-table:: The ``CMD_TCON_*`` command set (recovered numeric ids) :header-rows: 1 :widths: 6 40 10 10 34 * - # - 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``, ``0x12``–``0x14``, ``0x16``–``0x19`` and ``0x1b``–``0x1f`` are unassigned (reserved) in this firmware. 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 (:doc:`stack`, :doc:`/display/panel`). ``lfe5u_generate_firmware_filename`` builds the name from an id; this PCB's DT carries two fragments targeting this node (:doc:`/boot/dtbo`), 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 :doc:`/display/panel` 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 ``0x1a0f562``–``0x1a7b4b0`` (before the ``9f`` USERCODE anchor) contains **eight** separate, byte-exact command bursts, each of the form: .. code-block:: text 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 (:doc:`tcon`'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 :doc:`stack` (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: .. list-table:: :header-rows: 1 :widths: 20 22 58 * - 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. 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 flash** — ``lfe5u_flash_read_vcom`` / ``…_write_vcom`` / ``lfe5u_flash_vcom_sync`` (``"flash vcom and fs vcom is [%d]"``); 3. **filesystem** — ``lfe5u_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 :doc:`/userspace/onyx-platform`. The onyx_tcon transport layer ----------------------------- Sitting on top of ``lfe5u_*`` is ``onyx_tcon_*``, which the EPDC calls to move pixels and issue commands: .. list-table:: :header-rows: 1 :widths: 42 58 * - 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. 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. 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 :doc:`ebc-interface`. 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. Update modes and schemes ------------------------ The named, special-cased paths — A2 (fast), GU/REGAL, CFA (colour), handwrite and merge/snapshot — are documented once, in :doc:`ebc-interface`, 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``). 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. 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 :doc:`ebc-interface`. 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. 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/__<.ied|.bit>``, where: - ```` 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"``); - ```` 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`` — :doc:`/display/panel`). ``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. 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, :doc:`/display/panel`). The register map below is real and useful (it is the same silicon), but is not the bind path on this device. 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``): .. list-table:: :header-rows: 1 :widths: 18 82 * - 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. 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] …"``. 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 :doc:`stack`. What a from-scratch driver still needs ====================================== Combined with :doc:`stack` and :doc:`/display/panel`, 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. 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: :doc:`ebc-interface` (the ioctl set, update modes and ``.wbf`` waveform format this driver implements), :doc:`stack` (userspace glue, front-light class devices, userspace libraries), :doc:`/display/panel` (DSI transport, panel geometry, EPD power in XBL), :doc:`/soc/pinctrl` (the FPGA control GPIOs and front-light enables), :doc:`/userspace/firmware-blobs` (the built-in TCON/waveform/NVCM firmware), :doc:`/display/hal` (where the e-ink driver sits among the kernel modules).