A Zeratul Journey, Continued: The Vendor's Real Camera Tuning, and the Checksum Guarding It

Part 2 of the Zeratul Journey / <- Part 1: Porting Thingino to the Jooan S7-U

The vendor’s real tuning looked like a clear win for day mode. Night mode told a different story. Both A6B0 and A6B1 produced a corrupted image in night mode: clean horizontal bands, each one a flat gray tone, with a hard edge between bands. Thingino’s own generic file showed no such problem. Day mode was fine on all three files.

Fixing that corruption took three steps the vendor’s firmware never accounted for: extracting the factory tuning from a vendor partition, reverse-engineering a custom checksum the ISP driver enforces only during mode switches, and bisecting a 176,288-byte diff down to a single 1,371-byte fix.

The vendor’s real camera tuning, hidden inside a firmware partition

Thingino ships a generic ISP tuning file for the SC2336 sensor, sensor-iq/t23/sc2336.bin. This file controls color, exposure, and gain across the whole image pipeline. A generic file is a safe default. It is not tuned for one specific camera’s lens and sensor pairing.

The vendor firmware dump held two files with a promising name: etc/tag-sc2336p_A6B0.bin and etc/tag-sc2336p_A6B1.bin. A string search through the vendor’s own jooanipc binary explained the two names. The firmware reads a batch ID over I2C from the sensor’s own memory, then picks the matching file at boot. A6B0 and A6B1 are two factory calibrations for two hardware batches of the same camera, not two different features.

Each file is 360,448 bytes, far larger than Thingino’s own 176,288-byte tuning file. A first look suggested an incompatible format. The official Ingenic Zeratul SDK release explained the real structure. The vendor tool packs the sensor’s tuning data together with the boot command line, boot environment variables, and a small firmware version string, all into one flashed partition. A 12-byte header inside that partition, marked SSET, holds a size field and a checksum. Right after that header sits the exact same tuning file format Thingino already uses.

The extracted tuning data measured 176,288 bytes, the same size as Thingino’s file, byte for byte. A diff against Thingino’s generic file showed 42,266 bytes different out of 176,288, about 24 percent. The two vendor batches, A6B0 and A6B1, differed from each other by only 109 bytes, all inside small gain-table regions — the same tuning approach, recalibrated per hardware batch.

A byte edit that crashed the kernel

The vendor’s real tuning looked like a clear win for day mode. Night mode told a different story. Both A6B0 and A6B1 produced a corrupted image in night mode: clean horizontal bands, each one a flat gray tone, with a hard edge between bands. Thingino’s own generic file showed no such problem. Day mode was fine on all three files.

The next step was to copy one narrow byte range from Thingino’s working file into the vendor’s file and see whether a specific tuning table caused the corruption. The first attempt did not test that theory. It crashed the camera outright. The kernel logged a real fault inside tx_isp_t23.ko, the ISP driver, and the camera reset itself.

The kernel log named the exact cause: tiziano_load_parameters[1042]: Failed to CRC sensor setting!. The tuning file carries its own embedded checksum. The ISP driver checks that checksum, but only during a day or night mode switch, not during a normal cold boot. This is why a hand-edited file booted fine, then crashed the moment the camera tried to switch modes. Any edit to the file needed a matching, correctly computed checksum, or the camera would keep crashing on every mode switch.

Reading the checksum straight out of the kernel module

No public document describes this checksum. The only real source of truth was the running kernel module itself, tx-isp-t23.ko, copied live off the camera over SSH. The module still carried its full symbol table, unstripped, which made this possible.

The system had no MIPS disassembler installed. binutils-mipsel-linux-gnu added one. A first pass with radare2 looked promising, then led to a dead end: MIPS code often splits a 32-bit address across two separate instructions, and radare2 resolved that split incorrectly for this module, pointing at the wrong function entirely.

readelf -r gave the real relocation table, and from there each split address could be rebuilt by hand from the raw instruction bytes. That work led to a function named, plainly, crc32, inside the driver’s own code. The module even embeds the standard 256-entry CRC-32 lookup table in its read-only data. The function that actually runs, though, is not the standard byte-by-byte CRC-32 algorithm. It reads the whole file four bytes at a time, folds each 32-bit word into a running value with xor, then looks up only 8 of the table’s 256 entries:

uint32_t crc = 0;
for (i = 0; i < word_count; i++) {
    crc ^= ((uint32_t*)buf)[i];
    crc ^= table[crc & 0x7];
}

A Python copy of this exact algorithm reproduced the stored checksum for all three known files: Thingino’s default, A6B0, and A6B1.

With a working checksum function, the tuning file’s layout became clear. The first 16 bytes hold a version tag. The next 4 bytes hold the length of the data that follows. The next 4 bytes hold the checksum itself, covering everything from byte 24 to the end of the file. One checksum covers the whole tuning payload, including the day, night, and other sub-tables packed inside it, not a separate checksum per table.

Twenty-two thousand bytes down to thirteen hundred seventy-one

A working checksum function turned a guessing game into a real, testable search. The process for each test: pick a byte range, copy Thingino’s values into that range inside a copy of the vendor’s file, recompute the checksum, copy the file onto the camera, restart the ISP daemon, and check both day and night mode.

The first real test copied half of the tuning payload, about 22,000 bytes, from Thingino’s file into the vendor’s file. Night mode came back working. Day mode lost real detail: still tuned, but visibly flatter than the vendor’s own calibration.

That result set the actual question: how much of that 22,000-byte range actually mattered for night mode, and how much only cost day-mode quality for no reason. Each round below tested one half of the previous range, alone:

Range tested alone Night mode Day mode
First half, ~22,000 bytes Fixed Flatter, but usable
First quarter Still broken New defect: black band across top of frame
Second quarter Fixed Flatter
…first half of that Still broken Better
…second half of that, 5,484 bytes Fixed Flatter
…first half of that Still broken Good
…second half of that, 2,742 bytes Fixed Flatter
…first half of that Still broken Flattest yet
…second half of that, 1,371 bytes Fixed Good

The first quarter tested here turned up an unrelated problem: copying it alone caused a solid black band across the top of the frame, in day mode and night mode alike. That range plays no part in the night-mode fix. It stayed untouched in every later test.

The last row found the actual answer: a single 1,371-byte range. Copying only that range from Thingino’s file into the vendor’s file fixed night mode completely, with no visible cost to day mode. The other roughly 40,000 bytes that differ between Thingino’s file and the vendor’s file can stay exactly as the vendor tuned them.

The result is a hybrid tuning file: about 97 percent real factory calibration from the camera’s own vendor firmware, plus one small, precisely located range pulled from Thingino’s known-working default. Day mode looks better than Thingino’s generic file. Night mode works. The same 1,371-byte fix, applied the same way, also cleared up night mode in the second vendor calibration file, A6B1, confirming the fix is not specific to one hardware batch.

A reusable tool, not just a one-off fix

The container format work above applies to any camera built on Ingenic’s Zeratul SDK, not just this one Jooan model. That format knowledge is now a small, dependency-free command-line tool: zrt-tag-tool.

Point it at a vendor’s tag.bin and it extracts a standalone tuning file in the same format Thingino already uses, along with the other blocks packed into the same partition: the boot command line, boot environment variables, and firmware version string, each in plain text. Point it at a directory of those parts and it rebuilds a valid tag.bin, recomputing both checksums correctly, including the driver-checked one described above. The tool round-trips two real factory firmware dumps byte for byte, and its test suite runs those exact files through both directions.

What you learned

  • A vendor’s own firmware often holds better tuning data than a generic default. Extracting it is worth the archaeology.
  • A file format that looks foreign at first glance may just be a known format wrapped in an unfamiliar container. Check for a matching header a few hundred bytes in before assuming the format itself is different.
  • A checksum failure that only appears on a specific code path, not on cold boot, will not show up in a quick smoke test. Exercise every mode a driver supports, not just the default one.
  • When a checksum algorithm is not documented, the running binary is the real specification. Pull it, disassemble it, and verify a rebuilt copy against known-good data before trusting it.
  • A binary search over a byte range, with a real pass/fail test at each step, finds a small root cause inside a large diff far faster than guessing by table shape or offset alone. Keep bisecting past the first working answer — a smaller fix is usually a better fix.