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.