A Zeratul Journey: Porting Thingino to the Jooan S7-U

The Jooan S7-U ships with no public documentation. The listing does not name the SoC, the sensor, or the WiFi chip. Every fact in this port came from a serial console, a vendor firmware dump, or a live disassembly. This is what that process looked like, for anyone porting Thingino to a similarly undocumented board.

Finding the hardware

The SoC is an Ingenic T23ZN. WiFi and a secondary MCU come from a single combo chip, the ATBM6441: an NDS32 core plus a HERA radio, talking to the SoC over SDIO. None of that came from a spec sheet. It came from a serial console on the board and a vendor firmware dump pulled through it.

A shim that called itself

Before the sensor even entered the picture, the render daemon would not start at all. rvd linked fine and started, then never produced a video ring, with nothing useful in the log.

The cause sat in a compatibility shim, libuclibcshim, carried over from an older C library. The current uClibc-ng forces _FILE_OFFSET_BITS=64, which makes its own headers silently redirect fopen to fopen64. The shim’s fopen64 was written as a one-line wrapper:

fopen64() { return fopen(...); }

With the redirect in place, that line compiles into a function calling itself. Every fopen, open, and fseeko in rvd, rad, and libimp hung in an infinite tail call on first use, with no crash and no log line to point at it.

A newer, trimmed version of the shim — reduced to just mmap and a couple of ctype tables, with the rest coming from libc itself — cleared it. With the shim fixed, rvd ran far enough to hit the next problem: a wrong sensor name.

A sensor that was not what it claimed to be

The vendor app hardcodes SENSOR=sc2336p and ships ISP tuning files named for that part. A first Thingino build followed that name. It booted, then hard-reset a few seconds later, at the same line, every time. No kernel panic — a watchdog reset.

The init script normally daemonizes rvd silently. Run it in the foreground instead, with verbose logging. The silent hang turns into a clean error: sensor_detect ret=-5, v=0x10, then “chip found @ 0x30 is not an sc2336p chip.” Thingino’s own sensor-info tool settled it: the chip ID at I2C address 0x32 read 0xcb3a, the ID for a plain SC2336. An SC2336P reports 0x9b3a. The vendor’s own sensor name was wrong, and the part sat at a nonstandard address. The working defconfig needed both facts:

BR2_SENSOR_1_NAME="sc2336"
BR2_PACKAGE_THINGINO_RAPTOR_CONF_SENSOR_I2C_ADDR="0x32"

With the correct driver and address, rvd got its frame ring and video came up. Read the chip ID off the silicon. A vendor’s part name is a starting guess, not a fact.

A WiFi chip with its own boot timing

The ATBM6441’s MCU boots its own firmware from onboard NVM. It does not answer on SDIO until about five seconds after power-on. The vendor firmware loads its SDIO driver as a late kernel module, by which point the MCU is already up.

An early Thingino build made the same driver built-in instead of a module. Built-in code probes SDIO during early boot, well before five seconds, so the probe failed every time. Loading it as a module was no better: at that point in boot there was no platform data attached, and the module crashed with a null pointer dereference.

The working fix keeps the driver built-in, then re-triggers card detection from user space once the MCU has had time to boot:

echo "INSERT" > /sys/devices/platform/jzmmc_v1.2.1/present

That sysfs attribute already existed in the kernel. It calls mmc_detect_change on demand — exactly what the vendor’s own detect_on line was trying, and failing, to do.

A second SDIO issue only appeared once boot got busier. The board’s own S09mmc script loads the ATBM6441 module in the background, with a five-second wait built in. The shared, board-generic S36wireless.in script also tries to load the same module, gated only by “is it already loaded.” When that check lost the race, the second load attempt blocked forever with no timeout, and the whole boot sequence hung. The fix replaces that second, synchronous load with a bounded wait for the first one to finish, scoped to ATBM6441 boards only. No other board’s boot sequence changes.

The board also wires only two of the four SDIO data lines. Four-bit mode failed with write-CRC errors. A kernel Kconfig option, CONFIG_JZMMC_V12_MMC1_FORCE_1BIT, forces one-bit mode and fixed it. The GPIO map recovered later from the vendor’s own config file explained why: pan-tilt pins 45 and 46 are the same physical pins as SDIO DAT2/DAT3 (PB13/PB14). The board designer reused two SDIO data lines for the gimbal motor, so one-bit mode is the only mode the wiring supports.

A warm reboot exposed a second timing gap. The MCU sends its SDIO startup signal, HI_SDIO_StartUp_Indication, exactly once, at its own power-on. A reboot on the SoC does not reset the MCU, so the MCU’s session stays live across the reboot while the SoC’s driver starts fresh expecting a new startup signal that never comes. The result: stale sequence numbers and SDIO hardware errors on every warm reboot, cold boot unaffected. The vendor SDK already shipped the fix, unused: a WSM command, atbm_wsm_force_reboot, that tells the MCU to reboot in step with the host. Calling it from a kernel reboot notifier, on SYS_RESTART only — power-off keeps the original vendor sleep path — fixed every warm reboot.

A motion sensor with no visible transport

The camera’s PIR sensor lives inside the ATBM6441 MCU, not on the main SoC. PIR events now reach the SoC and show on the video overlay; they are not yet wired into full recording and alerting. Even getting the overlay working took three wrong turns and a dead end that was never fully root-caused.

The MCU logs a PIR alarm on its own console the moment it fires. The first guess at getting that alarm to the SoC was the SDIO command channel already used for WiFi control. Disassembling the vendor’s jooanipc binary turned up a plausible command pair — cmd_id 64 to enable PIR, cmd_id 65 for sensitivity — sent as a customer command over the existing SDIO ioctl. That channel armed the PIR logic on the MCU side, visible in the MCU’s own log, but nothing ever reached the SoC. Wrong channel.

The real path came from probing both UARTs on the board at once: one to the MCU’s AT command console, one to the SoC’s own shell. The vendor’s host-side process, contractMCU, opens a second UART, /dev/ttyS0, and reads PIR alarms from it as a binary framed protocol. That second UART is the T23ZN SoC’s UART0, wired straight to the ATBM6441. Thingino’s kernel registers UART0 but never enables its pin mux, so /dev/ttyS0 never appeared. A new kernel patch fixed that:

package/all-patches/linux/3.10.14/0093-enable-uart0-portb-default.patch

With UART0 enabled, /dev/ttyS0 started receiving the MCU’s binary frames. A small standalone tool confirmed a clean, repeatable, checksum-verified frame on every trigger.

Wiring that into the full motion pipeline is where this hit a wall. Running the render daemon and the full motion daemon together triggered a silent hard lockup, with zero kernel output before a hardware watchdog reset. That lockup was never root-caused past “interrupts are very likely disabled system-wide” — going further would need JTAG-level access.

A few sharp edges

SD card autoupdate failed only on large images. U-Boot’s jz_mmc driver bounded an entire PIO transfer with a single one-second timeout, instead of timing out only on an actual stall. A large file needs more than one second to copy, so every big fatload failed regardless of card health. The fix changes the timeout to reset on forward progress, so it only fires on a genuine stall. This is a generic U-Boot bug, not specific to this board, and a fair candidate for upstreaming on its own.

Two smaller bugs cost real time for reasons specific to this board. The MCU control CLI failed every call with a plain “connect err,” because its socket path was relative and the process’s working directory landed on the read-only squashfs — fixed by hardcoding an absolute path under /tmp. WiFi provisioning had one landmine: never let the placeholder SSID (THINGINO-) reach the MCU. It commits to trying that network and drops into a scan-and-sleep loop hunting for one that does not exist — a loop that survives into the next boot’s SDIO probe.

From a working port to review-ready pull requests

Every fix above was tested on real hardware before it went near a pull request, including a negative-control test: rebuilding with the 1-bit SDIO patch removed, to confirm the board really does hang without it, not just in theory. The maintainer asked for the work split by concern instead of one large patch:

Repository Change
gtxaspec/thingino-linux PR #16 Kernel patch forcing 1-bit SDIO mode on the ATBM6441’s MMC controller
themactep/thingino-firmware PR #1362 Generic ATBM6441 support: WiFi driver package, MCU control tools, WiFi framework wiring
themactep/thingino-firmware PR #1361 The Jooan S7-U board config, on top of #1362

PR #1362 carries five commits, each with its own justification, so a reviewer can accept or question one piece at a time. Rebuilding that branch by cherry-picking individual files, instead of replaying the full merge history, dropped two patches the board still needed — the MMC1 pin-mux fix and the one-bit-force option — and the board hung exactly as it had before either fix existed.

The fix: check the full numbered patch directory for the board, not just the one patch already in mind.

What you learned

  • A serial console turns silent failures into readable ones.
  • A vendor’s part name is a starting guess, not a fact. Read the chip ID off the silicon.
  • A combo WiFi/MCU chip can hide a second data path behind a pin mux the kernel never enabled.
  • Run a daemon in the foreground while debugging. It turns a boot-time wedge into a readable error.
  • When a root cause is out of reach, ship the smaller, provably safe scope, and say plainly what is still missing.