520 lines
19 KiB
Markdown
520 lines
19 KiB
Markdown
# RK3588 DDR Blob Reverse Engineering — Project Diary
|
|
|
|
*A chronicle of decompiling, patching, bricking, and recovering a closed-source DDR initialization binary.*
|
|
|
|
---
|
|
|
|
## Day 1: 2026-04-02 — The Idea
|
|
|
|
It started with a simple question: "Can you decompile the RK3588 DDR init blob?"
|
|
|
|
The RK3588 ships with a closed-source binary blob that initializes LPDDR5
|
|
memory during early boot. Rockchip provides no source code — it's a black
|
|
box. The user (a kernel developer working on the CoolPi GenBook and Radxa
|
|
Rock 5 ITX+) wanted to understand what it does, find bugs, and potentially
|
|
fix the cold boot failures the community reports.
|
|
|
|
### First Attempt: Ghidra on Tesla (aarch64)
|
|
|
|
We installed Ghidra on tesla (an aarch64 LXD container on hertz, a Pi 5).
|
|
Analysis worked — 118 functions found. But when we tried to decompile:
|
|
|
|
> `ERROR os/linux_arm_64/decompile does not exist`
|
|
|
|
Ghidra's decompiler backend is a **native x86 binary**. No ARM64 build exists.
|
|
The analysis (disassembly) works on any platform, but decompilation requires x86.
|
|
|
|
### Moving to Oppenheimer (x86)
|
|
|
|
We created a new Proxmox container (CT131, "oppenheimer") on data — the
|
|
Ryzen 7 1700 server. Debian 12, x86_64. Installed JDK 21, Ghidra 11.3.
|
|
|
|
**First surprise:** The blob is **AArch64 (64-bit ARM)**, not Cortex-M0 as
|
|
initially assumed. The first instruction `01 00 00 14` is an AArch64 branch.
|
|
It runs on the main A76/A55 cores during boot, not on the PMU's M0 core.
|
|
|
|
**Result:** 118 functions decompiled, 11,923 lines of C. The Ghidra headless
|
|
analyzer + a custom Java export script did the heavy lifting:
|
|
|
|
```java
|
|
DecompInterface decompiler = new DecompInterface();
|
|
decompiler.openProgram(currentProgram);
|
|
// ... iterate all functions, export decompiled C
|
|
```
|
|
|
|
## Day 1: The Annotated Source
|
|
|
|
We transformed Ghidra's raw output into human-readable C:
|
|
- 53 functions renamed based on behavior (sgrf_wait_ready, ddr_pll_configure, etc.)
|
|
- 79 MMIO registers mapped to hardware blocks using the RK3588 TRM Part 2
|
|
- Register addresses cross-referenced with kernel device tree sources
|
|
|
|
Key discovery: the `0xFF00AA` value appearing 28 times is Rockchip's GRF
|
|
**write-enable mask pattern** — upper 16 bits mask which bits are writable.
|
|
|
|
## Day 1: The Version Comparison
|
|
|
|
We extracted all DDR blob versions from the rkbin git history (v1.02 through
|
|
v1.19) and compared them:
|
|
|
|
**Shocking finding:** Every version has **major code changes** — not just
|
|
timing parameter tweaks. The blob grew from 42KB to 77KB over its lifetime.
|
|
|
|
But the fast vs conservative blobs of the **same version** differ by only
|
|
**6 bytes** — the LP4 and LP5 frequency parameters in the data section.
|
|
|
|
## Day 1: 45 Bugs Found
|
|
|
|
The most critical finding: **45 hardware poll loops without timeouts**.
|
|
|
|
```c
|
|
// This loops FOREVER if SGRF doesn't respond:
|
|
do {
|
|
} while (SGRF_DDR_STATUS != 0);
|
|
```
|
|
|
|
These explain the cold boot failures the RK3588 community reports. At low
|
|
temperatures, the PHY takes longer to respond, and without a timeout, the
|
|
system hangs permanently during boot.
|
|
|
|
**Categories:**
|
|
- 16 B.cond backward branches
|
|
- 26 TBZ/TBNZ backward branches
|
|
- 3 CBZ/CBNZ backward branches
|
|
|
|
## Day 1: Community Research
|
|
|
|
A background research agent spent 15 minutes collecting 40+ sources about:
|
|
- DDR training (ZQ cal → write leveling → gate → DQ → eye → VREF → CA)
|
|
- Why Rockchip dropped 2736 MHz LP5 in v1.16 (PHY eye margin failures)
|
|
- The Synopsys DWC LPDDR5/4X PHY used in the RK3588
|
|
- The rkddr tool for frequency overclocking
|
|
- Community-achieved 3200 MHz (6400 MT/s) on SK Hynix modules
|
|
|
|
## Day 2: 2026-04-03 — The NOP Patcher
|
|
|
|
### The Idea
|
|
|
|
Simple: replace each tight poll loop's backward branch with a NOP.
|
|
The register is read once — if ready, great. If not, fall through.
|
|
|
|
### QEMU Testing
|
|
|
|
We set up Unicorn (CPU emulator) on oppenheimer to test:
|
|
- Map all MMIO regions as RAM with pre-seeded "ready" values
|
|
- Skip MSR/MRS instructions via exception hooks
|
|
- Count instructions, compare original vs patched
|
|
|
|
**Result:** Original stuck at 0x10350 (TBZ loop). Patched progressed to 0x9124
|
|
(deep into PHY training). The NOP approach worked... in emulation.
|
|
|
|
### The Production Patcher
|
|
|
|
We classified the 45 polls:
|
|
- 40 non-critical (SGRF, firewall, PLL) → NOP
|
|
- 5 training-critical (DfiStatus, CalBusy, etc.) → KEEP
|
|
|
|
Built U-Boot on ampere (the GenBook itself) with the patched blob.
|
|
|
|
### 💀 The Bricking
|
|
|
|
```
|
|
$ sudo flashcp -v u-boot-rockchip-8mb.bin /dev/mtd0
|
|
```
|
|
|
|
Reboot. Black screen. Maskrom mode.
|
|
|
|
**What went wrong:** The NOP approach was too aggressive. The PHY genuinely
|
|
needs wait time for operations to complete. Converting polls to single checks
|
|
meant the code proceeded before the hardware was ready, corrupting the DDR
|
|
controller state.
|
|
|
|
### The Recovery Odyssey
|
|
|
|
**Problem 1:** rkdeveloptool hanging on "Downloading bootloader..."
|
|
|
|
Turns out the Debian-packaged rkdeveloptool is the **Pine64 fork** which
|
|
doesn't have the `cs` (chip select) command needed for SPI flash. The
|
|
Rockchip original does:
|
|
|
|
```bash
|
|
git clone https://github.com/rockchip-linux/rkdeveloptool.git
|
|
# This one has: cs [storage: 1=EMMC, 2=SD, 9=SPINOR]
|
|
```
|
|
|
|
**Problem 2:** ModemManager grabbing the USB device
|
|
|
|
```
|
|
EBUSY: Device or resource busy
|
|
```
|
|
|
|
ModemManager probes every new USB device. `sudo systemctl stop ModemManager`
|
|
fixed the USB claim issue.
|
|
|
|
**Problem 3:** USB signal integrity
|
|
|
|
The first recovery host (ohm) had flaky USB — `error -71` (protocol error).
|
|
Moved to higgs (Pi 5). Still had issues on bus 001. Switching to bus 004
|
|
(different USB port) got it working.
|
|
|
|
**Problem 4:** rkdeveloptool wrote to eMMC, not SPI
|
|
|
|
The `cs 9` command to select SPI was crucial. Without it, the 8MB image
|
|
overwrote the eMMC boot partition instead of SPI. We recovered the eMMC
|
|
file system using **testdisk** (restored FAT directory entries) but the
|
|
file **contents** were zeroed.
|
|
|
|
**The Save:** A March 24 SPI backup on the data partition:
|
|
```
|
|
/mnt/sda3/spi-flash-backup-20260324.bin
|
|
```
|
|
|
|
This backup was our mainline U-Boot. Flash it back to SPI, boot from the
|
|
USB stick (stock CoolPi kernel), mount the NVMe (which has the arch rootfs
|
|
and kernel source), rebuild the boot files, copy to eMMC. **Ampere lives.**
|
|
|
|
### Lessons Learned (The Hard Way)
|
|
|
|
1. **NEVER NOP hardware polls on production hardware.** Counted loops or nothing.
|
|
2. **ALWAYS backup SPI before flashing:** `dd if=/dev/mtdblock0 of=backup.bin`
|
|
3. **Use the Rockchip rkdeveloptool**, not the Pine64 fork, when `cs` is needed.
|
|
4. **Stop ModemManager** before using rkdeveloptool.
|
|
5. **Battery disconnect** isn't needed — maskrom button held during power-on works.
|
|
6. **Test with QEMU first.** It caught the TBZ poll type we initially missed.
|
|
|
|
## Day 2: The Trampoline Patcher
|
|
|
|
After the bricking, we built a proper fix: **assembly trampolines with counted loops.**
|
|
|
|
Each poll loop's backward branch is replaced with a `B trampoline_N`, where
|
|
the trampoline section is **appended** after the blob (no code shifting):
|
|
|
|
```
|
|
Trampoline for each poll:
|
|
MOV w18, #0x20000 ; 128K iteration timeout
|
|
MOVK w18, #0x2, LSL #16 ; (if needed for large count)
|
|
LDR w0, [xN, #offset] ; copy of original register load
|
|
<condition check> ; inverted: exit on success
|
|
SUBS w18, w18, #1 ; decrement counter
|
|
B.NE .-16 ; retry if counter > 0
|
|
B return_addr ; timeout: fall through
|
|
```
|
|
|
|
**QEMU Result:** Original stuck at 0x10350. Trampoline blob: X18=0x17ED
|
|
(counter counting down on the last poll). **All 45 polls have timeouts now.**
|
|
|
|
The blob grows from 76,704 to 78,068 bytes (+1,364 bytes). Whether BL2
|
|
accepts the larger blob is the open question for real hardware testing.
|
|
|
|
## Day 2: The Recompilation Attempt
|
|
|
|
We tried to make Ghidra's decompiled C actually compile. Starting from
|
|
11,976 lines and 4,184 errors:
|
|
|
|
- Added type definitions, register headers, forward declarations
|
|
- Fixed Ghidra artifacts (switchD_, stack0x, register0x, ._0_1_ sub-fields)
|
|
- Renamed 41 duplicate functions to unique names
|
|
- Fixed asm string literals, system register access
|
|
|
|
Got down to ~270 errors but hit a wall: Ghidra's C output is fundamentally
|
|
a **reading aid**, not compilable source. Array assignments, unresolved call
|
|
targets (same name for different functions), and struct sub-field access
|
|
patterns can't be mechanically fixed.
|
|
|
|
**Verdict:** Binary patching (trampolines) is the right approach. Recompilation
|
|
from decompiled output would require rewriting every function by hand.
|
|
|
|
## Current State
|
|
|
|
### What Exists
|
|
- Full decompilation of all blob versions (v1.02-v1.19)
|
|
- 53 named functions, 79 mapped MMIO registers
|
|
- Trampoline patcher (QEMU-verified, not yet hardware-tested)
|
|
- Frequency table (2112-3200 MHz LP5)
|
|
- Community research (40+ sources)
|
|
- DokuWiki article and Gitea repo
|
|
|
|
### What's Next
|
|
1. **Instrumented QEMU trace** — log every MMIO access with register state
|
|
to build a complete execution flow map
|
|
2. **Hardware test** of trampoline blob (with iFixit kit ready)
|
|
3. **UART capture** of DDR training output for comparison
|
|
4. **Frequency patching** — try 2736 MHz on boltzmann's Rock 5 ITX+
|
|
|
|
### Infrastructure
|
|
| Host | Role |
|
|
|------|------|
|
|
| oppenheimer (CT131 on data) | Ghidra, QEMU, cross-compile |
|
|
| boltzmann (Rock 5 ITX+) | Source repo, DDR test target |
|
|
| ampere (GenBook) | The patient that survived surgery |
|
|
| tesla (hertz LXD) | Initial Ghidra attempt (failed) |
|
|
|
|
### Repository
|
|
- Private Gitea: `git.reauktion.de/marfrit/rk3588-ddr-analysis`
|
|
- DokuWiki: `kelvin.reauktion.de/doku.php?id=rk3588_ddr_analysis`
|
|
|
|
---
|
|
|
|
*"We saved Private Ampere."* — 2026-04-03, after 4 hours of recovery work.
|
|
|
|
---
|
|
|
|
*Diary maintained by Claude Code (Opus 4.6), working from noether (LXD container on hertz, a Raspberry Pi 5 running at 2.8 GHz because we overclocked that too).*
|
|
|
|
## Day 2 Late Night: The Deep Dive
|
|
|
|
### The Instrumented Tracer
|
|
|
|
We built a Unicorn-based tracer that logs every MMIO read/write with PC
|
|
context. Running the original blob:
|
|
|
|
**19 MMIO accesses in 200K instructions** — the blob barely touches hardware
|
|
before hitting the first poll loop. The boot sequence:
|
|
|
|
1. Read PMU1_GRF (DDR status)
|
|
2. Read SRAM boot flag
|
|
3. Write blob header to SRAM (BL31 mailbox registration)
|
|
4. Configure BUS_GRF (DDR QoS, routing — the 0xFF00AA write-mask pattern)
|
|
5. Zero DDRC CH0 registers
|
|
6. Configure SCRU (DDR PLL: gate → set → release → enable)
|
|
7. Configure BUS_GRF (base + route)
|
|
8. ... stuck at poll
|
|
|
|
### The Smart Injection Approach
|
|
|
|
Made the tracer inject "ready" values after 5 repeated reads from the same PC.
|
|
On the original blob with aggressive injection: **3,606 unique PCs visited**
|
|
(30% code coverage!) before jumping to unmapped memory at 0x100000FFF.
|
|
|
|
**Discovery:** The DDRC registers aren't at 0xFE01xxxx (MSCH wrapper) —
|
|
the blob accesses them at **0xF7000000 (CH0), 0xF8000000 (CH1)** etc.
|
|
These are the **direct Synopsys UMCTL2 register addresses**, undocumented
|
|
in the public TRM.
|
|
|
|
### The Outer Retry Loop
|
|
|
|
Running the trampoline blob for 10M instructions revealed the architecture:
|
|
the blob has an **outer retry loop** that repeatedly calls the training
|
|
function. Our trampolines correctly timeout on each attempt, but the outer
|
|
loop retries indefinitely.
|
|
|
|
On real hardware: inner poll passes → training succeeds → outer loop exits.
|
|
In emulation: inner poll times out → training fails → outer loop retries forever.
|
|
|
|
**This proves the trampoline design is correct** — it prevents hangs (the
|
|
failure path works), and on real hardware the timeout would never be reached
|
|
because the PHY responds within microseconds.
|
|
|
|
### The Complete DDR Init Flow (as revealed by tracing)
|
|
|
|
```
|
|
Entry (0x10978):
|
|
├─ Read PMU1_GRF status
|
|
├─ Read SRAM boot flag
|
|
├─ Register with BL31 via SRAM mailbox
|
|
├─ Configure DDRC MSCH (reset controller regs)
|
|
├─ Configure SCRU (DDR PLL setup)
|
|
│ ├─ Gate clock
|
|
│ ├─ Set DPLL params
|
|
│ ├─ Release reset
|
|
│ └─ Enable clock
|
|
├─ Configure BUS_GRF (27 registers)
|
|
│ ├─ QoS configuration
|
|
│ ├─ DDR routing
|
|
│ └─ Address mapping
|
|
└─ Enter training loop
|
|
├─ Configure DDRC channels (0xF7-FA000000)
|
|
├─ Start PHY training
|
|
├─ Poll for completion ← trampoline timeout
|
|
├─ Check result
|
|
└─ Retry if failed
|
|
```
|
|
|
|
## Day 2 Late Night Bonus: U-Boot eDP Analysis
|
|
|
|
The user's GenBook eDP patches for U-Boot cause a boot hang. Without UART
|
|
serial debug, the exact failure point was unknown. Here's the analysis from
|
|
reading the patches:
|
|
|
|
### Likely Failure Points (ordered by probability)
|
|
|
|
**1. Missing Power Domain Enable (MOST LIKELY)**
|
|
|
|
The RK3588 VOP2 and HDPTX PHY sit in the `pd_vo1` power domain. The VOP2
|
|
driver (`rk3588_vop2.c`) **does not enable the power domain** — there's no
|
|
`power_domain_on()` call in the probe function. If the VO1 power domain is
|
|
off (which it is by default at boot), all register accesses to VOP2 and
|
|
HDPTX PHY will **bus fault or return garbage**, hanging the SoC.
|
|
|
|
The kernel driver handles this via the device tree `power-domains` property
|
|
and the PM framework. U-Boot needs explicit power domain management.
|
|
|
|
**Fix:** Add to VOP2 probe:
|
|
```c
|
|
struct power_domain pd;
|
|
ret = power_domain_get(dev, &pd);
|
|
if (!ret)
|
|
power_domain_on(&pd);
|
|
```
|
|
|
|
And ensure the DTS has:
|
|
```dts
|
|
&vop {
|
|
power-domains = <&power RK3588_PD_VOP>;
|
|
};
|
|
```
|
|
|
|
**2. HDPTX PHY Poll Timeout Without Error Recovery**
|
|
|
|
The PHY driver has three `regmap_read_poll_timeout` calls:
|
|
- `PHY_RDY` — 5ms timeout
|
|
- `PLL_LOCK_DONE` — 1ms timeout
|
|
- `SB_RDY` — 1ms timeout
|
|
|
|
If any of these times out (because the power domain is off or clocks aren't
|
|
enabled), the driver prints an error but **continues execution**. Subsequent
|
|
register writes to a non-responsive PHY could hang the bus.
|
|
|
|
**Fix:** Return `-ETIMEDOUT` and abort initialization on poll failure.
|
|
|
|
**3. Missing Clock Enable for HDPTX PHY**
|
|
|
|
The HDPTX PHY probe function gets clocks and resets via DT, but the patch
|
|
doesn't show explicit `clk_enable()` calls for the PHY reference clock.
|
|
The kernel driver (`phy-rockchip-samsung-hdptx.c`) calls `clk_prepare_enable()`
|
|
for `ref` and `apb` clocks. If these aren't enabled in U-Boot, the PHY
|
|
PLL will never lock.
|
|
|
|
**4. VOP2 DCLK Not Configured**
|
|
|
|
The VOP2 driver gets `dclk` (display clock) but the pixel clock calculation
|
|
and parent mux selection is complex on RK3588 (VPLL/CPLL/GPLL sources).
|
|
If the clock tree isn't set up correctly, the VOP2 outputs nothing and the
|
|
eDP link training fails.
|
|
|
|
**5. DTS Overlay Issues**
|
|
|
|
The U-Boot DTS overlay enables `edp1`, `hdptxphy1`, and `vop` but:
|
|
- Doesn't set `power-domains` on any of them
|
|
- Doesn't set clock assignments (`assigned-clocks`, `assigned-clock-rates`)
|
|
- Uses `&vop` not `&vop2` (might not match the U-Boot DT node name)
|
|
- Missing `edp1` status = "okay" (only sets panel, not status)
|
|
|
|
### Debugging Strategy
|
|
|
|
With the Tigard UART adapter (1.5Mbaud on UART2 debug pads):
|
|
1. Enable `CONFIG_DEBUG_UART=y` and `CONFIG_LOG_MAX_LEVEL=9`
|
|
2. Add `printf()` calls at VOP2 probe entry, PHY probe entry, and before
|
|
each poll timeout
|
|
3. The hang point will be immediately visible in the serial output
|
|
|
|
### Without UART (QEMU approach)
|
|
|
|
Unlike the DDR blob, U-Boot is too complex for Unicorn emulation. But we
|
|
can build U-Boot with `CONFIG_SANDBOX=y` on x86 and test the driver probe
|
|
logic in the sandbox — this would catch null pointer dereferences and logic
|
|
errors, though not hardware register issues.
|
|
|
|
## Day 2 Final: OEM eDP Analysis — The Missing Pieces
|
|
|
|
Analyzing the OEM CoolPi SPI image (genbook_spi.img) from /rpool/nas revealed
|
|
exactly what the working eDP init looks like vs our broken patches:
|
|
|
|
### OEM DTS vs Our Patches — The Differences
|
|
|
|
| Property | OEM (working) | Our patch (broken) |
|
|
|----------|--------------|-------------------|
|
|
| VOP power-domain | `<&power 0x18>` (PD_VOP) | **MISSING** |
|
|
| eDP power-domain | `<&power 0x1A>` (PD_VO1) | **MISSING** |
|
|
| eDP `force-hpd` | **present** | **MISSING** |
|
|
| eDP clocks | `dp`, `pclk` explicit | inherited from kernel DTS |
|
|
| VOP `assigned-clocks` | CPLL parent for ACLK | **MISSING** |
|
|
| VOP `rockchip,pmu` | present | **MISSING** |
|
|
| HDPTX PHY resets | 4 resets explicit | inherited |
|
|
| edp1 (0xfded0000) | `status = "okay"` | Was missing (fixed in 0009) |
|
|
|
|
### Key Findings
|
|
|
|
1. **`force-hpd` is critical** — eDP panels don't have hot-plug detect.
|
|
Without `force-hpd`, the eDP driver waits for HPD assertion which never
|
|
comes on an internal laptop panel. This alone could cause a hang.
|
|
|
|
2. **VOP power domain is 0x18 (PD_VOP)**, eDP power domain is 0x1A (PD_VO1).
|
|
These are DIFFERENT power domains. Our fix patch only added power domain
|
|
to VOP2, not to the eDP controller node.
|
|
|
|
3. **`assigned-clocks` sets CPLL as ACLK parent** — this is the VOP AXI clock
|
|
parent mux. Without this, the VOP2 might run off the wrong clock source.
|
|
|
|
4. **OEM uses edp@fded0000 (eDP1)** not edp@fdec0000 (eDP0). Our DTS
|
|
correctly targets `&edp1`. Good.
|
|
|
|
### Updated Fix Required
|
|
|
|
The 0009 patch needs to add:
|
|
- `force-hpd` to edp1 node
|
|
- `power-domains` to edp1, vop, and hdptxphy1
|
|
- `assigned-clocks`/`assigned-clock-parents` to vop
|
|
- `rockchip,pmu` phandle to vop
|
|
|
|
## Day 2 Very Late Night: Keyboard Investigation
|
|
|
|
### pct13x2 Identified from Vendor Kernel
|
|
|
|
The CoolPi vendor kernel (coolpi-george/coolpi-kernel, branch linux-6.1-stan-rkr5.1)
|
|
has the full GenBook DTS at \`rk3588-cpcm5-genbook.dts\`:
|
|
|
|
**Keyboard (pct13x2):**
|
|
- Bus: I2C5 (0xFEAD0000), pinctrl i2c5m3_xfer
|
|
- Address: 0x2C
|
|
- Compatible: \"hid-over-i2c\" (generic driver)
|
|
- HID descriptor register: 0x0020
|
|
- Interrupt: GPIO1_D6, active low
|
|
- Power: dedicated 5V regulator via GPIO1_A7 (vcc5v0_keyboard)
|
|
|
|
**Touchpad (g7500):**
|
|
- Bus: I2C4 (0xFEAC0000), pinctrl i2c4m3_xfer
|
|
- Address: 0x10
|
|
- HID descriptor: 0x0001
|
|
- Interrupt: GPIO1_A1, active low
|
|
|
|
**Battery gauge (CW2015):**
|
|
- Bus: I2C4, address 0x62
|
|
|
|
**Key insight:** The keyboard needs vcc5v0_keyboard regulator enabled
|
|
(GPIO1_A7 high) before it responds on I2C.
|
|
|
|
### Sandbox Testing Attempt
|
|
|
|
Built U-Boot sandbox with the HID-over-I2C driver (patch 0006) and a
|
|
custom I2C emulator. The emulator responds to HID descriptor reads
|
|
with a standard boot keyboard descriptor.
|
|
|
|
The sandbox binary builds and runs, but integrating the emulator into
|
|
the sandbox DTS requires more plumbing work. Saved for a future session.
|
|
|
|
The driver code itself looks correct — the I2C HID protocol implementation
|
|
follows the Microsoft HID-over-I2C v1.0 spec. The real risk is:
|
|
1. Keyboard power regulator not enabled (GPIO1_A7)
|
|
2. Interrupt GPIO not configured correctly
|
|
3. pct13x2 might have non-standard HID quirks
|
|
|
|
## Day 2 3AM: Sandbox Keyboard Progress
|
|
|
|
Got the U-Boot sandbox running with:
|
|
- HID-over-I2C keyboard driver (patch 0006) compiled in
|
|
- I2C HID emulator (custom sandbox driver) bound
|
|
- i2c@5 bus appearing in `i2c bus` output (Bus 1)
|
|
- Emulator visible in `dm tree` as `hid_i2c_emul emul-hid-kbd`
|
|
|
|
The KEY insight from the research agent: sandbox uses `test.dts` (via -T flag),
|
|
NOT `sandbox.dts`. We spent an hour modifying the wrong file.
|
|
|
|
Remaining issue: the emulator `xfer` function isnt being called during
|
|
`i2c probe` or `i2c md`. The emulator is bound but not probed (lazy binding).
|
|
The sandbox I2C bus auto-probe mechanism isnt matching our device to its
|
|
emulator. Likely a small detail in the phandle dispatch or ops registration.
|
|
|
|
Also discussed: piclaudeio protocol using ASCII record/field separators
|
|
(0x1E/0x1F) for escaping, netstrings for binary. Would solve the heredoc hell.
|