Skip to content

Calibration

Before scanning, the OEM programs the CCD (gain, offset, per-channel exposure) and computes a per-column correction from calibration passes. This is what makes the raw stream usable: it normalises the lamp profile and each sensor element's response.

From the OEM driver and Ansel pipeline reverse engineering, and the parameter table read seen in captures, March–May 2026.

The per-unit EEPROM

What earlier drafts of this page called "the parameter table read" is the OEM software reading the scanner's per-unit EEPROM at start-up: a serial I2C EEPROM at 7-bit address 0x52 on the motherboard, read through the FX2 bridge with two vendor control requests. It holds the data that makes one unit that unit: serial number, a per-resolution Offset word and motor speeds, motor-adjust words, and the two colour matrices. It does not hold the light calibration (LED currents and duty cycles); the OEM keeps those in the Windows registry, written when its light-correction routine runs. [CONFIRMED] contents by live read on an F-135+ (serial 16402), August 2026, matching the layout decoded from the OEM engine by the pakon-mac project (docs/69-calibration-auto-load.md).

The read

Two vendor requests, both with wIndex = 0x1234, repeated per chunk:

Step bmRequestType bRequest wValue wLength
select 0x40 (vendor OUT, no data) 0xA4 0x00A5 0
read 0xC0 (vendor IN) 0xA9 byte offset into the EEPROM ≤ 32

0x00A5 is the EEPROM's 8-bit I2C address with the read bit set ((0x52 << 1) | 1); the select is re-issued before every read. The OEM reads 32 bytes at a time at increasing offsets (0x008, 0x028, 0x048, …), which is the "fixed number of pairs, resolution independent" pattern the captures showed. [CONFIRMED] on hardware; the same two requests, and no others, are all that is needed to read the chip. The reads here were made with the pakon-tlx-macos project's tools/eedump.py, which issues exactly this sequence (decoded from a live capture of the OEM engine's own transfers), extended to read the backup copies and check the CRCs.

The select's wValue is general: ((0x50 | n) << 1) | direction, so the same pair reaches any I2C EEPROM at 0x50–0x57 (the boot-personality chip at 0x51 included), and bit 0 is the direction. A write is the same select with bit 0 clear (0x00A4) followed by 0xA2 in place of 0xA9, with wValue again a byte offset. This is from the engine's own read routine (FN_bEEPromRead, fcn.100160a0 in TLB.dll 3.1.0.28: or eax, 0x50; shl eax, 1, then or [arg], 1 and 0xA9 for a read, 0xA2 otherwise), read out by the pakon-mac project while building its transport allow-list (tools/pakon_usb_guard.py). [CONFIRMED] for the read direction on hardware, and on both chips: 0x00A5 reaches 0x52, and 0x00A3 reaches the boot EEPROM at 0x51, the latter tested in August 2026 on serial 16402 against the application firmware. The generality is therefore a demonstrated property of the firmware rather than an inference from the disassembly, and it is what makes the boot chip archivable at all (see per-unit-data-and-safety.md). The write direction is from the disassembly only, and the OEM's normal scanning path never issues it (only its Calibration Wizard writes the EEPROM). The practical corollary for anyone writing a read tool: an allow-list that admits only the odd-wValue select and 0xA9 cannot be turned into a write by any caller.

Note that 0xA9 with wIndex = 0 is a different operation: the 8-byte personality read served by the stage-1 loader (see usb-identity-and-firmware.md). The wIndex value selects which.

Layout

The data is stored twice. Two sections, each a header {u32 length; u32 crc32} followed by the payload, with the CRC-32 (the standard reflected zlib/PKZIP polynomial) taken over the payload only. Each section has a backup copy:

Section Primary Backup Length
A 0x000 0x400 398 bytes (8 header + 390)
B 0x800 0xA00 36 bytes (8 header + 28)

Section A payload, absolute byte offsets, little-endian:

Offset Type Field
0x008 u32 hardware version (ScannerVersionHw; 400 on every unit read)
0x00C u32 scanner type (ScannerType): 1350 = F-135, 1351 = F-135 Plus
0x010 u32 scanner serial number (ScannerSerialNumber)
0x014 / 0x016 / 0x018 u16 ×3 resolution base 4: Offset, MotorSpeed, MotorSpeed (IR)
0x01A / 0x01C / 0x01E u16 ×3 resolution base 8: same three
0x020 / 0x022 / 0x024 u16 ×3 resolution base 16: same three
0x026 – 0x09D f32 ×30 NegMatrix0–29: the colour-negative correction, 3 rows × 10
0x09E – 0x115 f32 ×30 PosMatrix0–29: the positive (slide) correction, 3 rows × 10
0x116 – 0x18D 120 bytes unused (zero on every unit seen)

The word at 0x00C is the scanner type, and it is a reliable model discriminator: 1350 on a base F-135, 1351 on an F-135 Plus. [CONFIRMED] across five units (see the EEPROM dump comparison) and two ways on the OEM software: its error log prints "Scanner Type 1351" for a unit whose EEPROM decodes to 1351, and the engine mirrors the word into the registry as ScannerType (see below). Identified by Mats Fagerberg (thetalkingdrum); the field was decoded but left unnamed by pakon-mac. So model and serial can both be read straight off a raw dump without running the OEM software.

The three consecutive words at 0x008, 0x00C and 0x010 are named by the OEM engine itself, which mirrors them into the registry in that order as ScannerVersionHw, ScannerType and ScannerSerialNumber under HKLM\Software\Pakon\TLB\Scan. On serial 16402 those keys read dword:00000190, dword:00000547 and dword:00004012 (400, 1351 and 16402), matching the EEPROM words exactly. [CONFIRMED] against that unit's registry export.

So 0x008 is the hardware version, and it reads 400 on every unit read so far. Because no unit has yet shown a different value, what a change in it would signify is untested.

Offset is the OEM's own name for the first word of each per-base triple (27 to 34 at base 4 across the units read; see the dump comparison). Its meaning is not stated by the OEM. It sits with the motor speeds, scales with the resolution base, and matches the CCD pixel-window start that implementations write to the FPGA geometry register before a scan (31 for base 4, 62 for bases 8 and 16 in one implementation; the OEM writes 62 for the non-IR case), so it is most likely the per-unit start of the imaged region along the CCD line for that base, i.e. this unit's optical alignment to the film gate. [INFERRED from the value correspondence; not confirmed.]

Section B payload: twelve u16 motor-adjust words (0x808–0x81E, values near 1000 and clamped by the OEM to 900–1100), then one unnamed u32. Section B appears to be a factory default scoped per model, not per-unit and not universal: it is byte-identical (CRC 0x873e6ed3) on all four F-135 Plus units read, while the one base F-135 read differs in exactly one payload byte: the third motor-adjust word is 0x03F0 where the Plus has 0x03E8, giving CRC 0x2a582d50. Either the OEM's motor-speed calibration has never been run on any unit read so far, or it does not write here. [CONFIRMED] on five units; the base-model reading rests on a single unit.

Each 3×10 matrix row is R, G, B, R², G², B², RG, GB, BR, constant, i.e. a second-order colour matrix; on the reference unit the quadratic columns are all ≈ 0 and NegMatrix is in effect a 3×4 affine (diagonal ≈ 0.29 / 0.29 / 0.32, constants ≈ 166 / 430 / 638), and PosMatrix is the plain 0.25 diagonal with no cross terms or offsets. The OEM also mirrors both into the registry.

The two matrices behave differently across units: NegMatrix is per-unit (diagonals range 0.26–0.34 and constants 145–166 / 386–445 / 602–651 over the units read), while PosMatrix is bit-identical on every unit read, across both models: the plain 0.25 diagonal, so a shared constant rather than a factory measurement. [CONFIRMED] on five units; see the dump comparison. Layout first established by pakon-mac from the OEM engine.

Reference-unit motor constants, for scale (per-unit; do not copy):

Base Offset MotorSpeed MotorSpeed (IR)
4 30 25726 19278
8 58 11434 7557
16 60 5900 4836

Values from every unit read are collected in the EEPROM dump comparison, which is the practical way to see which fields are per-unit, which are per-model, and which are constant.

Verifying a read

Read both copies of both sections, check all four CRCs, and compare primary with backup. On the reference unit the backup of section A and both copies of section B validate; the primary of section A fails its CRC because of a single byte, 0x0A5, which reads 0x48 where the backup has 0x00 (the high byte of PosMatrix1, turning a 0.0 into 131072.0). Stable across four reads and two power cycles, so a stored-data fault, not a read artifact; it touches only the slide matrix.

How the OEM uses the two copies: at start-up it reads the primary of each section, checks the length and CRC, and if either is bad reads the backup and uses that instead. The first good copy wins; the primary is not repaired. Only if both copies of a section fail does it keep what it read and raise the SDK's INITIALIZEW_EEPROM_BLANK / INITIALIZEW_EEPROM_CHECKSUM_BAD warning (a warning, not an error). [CONFIRMED] two ways on the reference unit: the retry loop in the engine's EEPROM-to-registry routine, and the registry it populates, which holds the backup's PosMatrix1 = 0.0, not the primary's corrupted value, with no warning shown. So a unit can carry a bad primary copy for years without anyone noticing. Anyone archiving their own unit's EEPROM should read both copies; a dump of the primaries alone cannot show whether it is good.

The pakon-mac project reports that on its unit these EEPROMs returned good data only on the first transaction after a power cycle and degraded on later reads while still reporting success (backups/eeprom-i2c/README.md). That is not how these parts normally behave and may be specific to that unit or its tooling; the reference unit shows no such behaviour (four consistent reads). Until it is understood: read once per power cycle, and confirm by comparing across power cycles rather than by re-reading within one.

CCD register banks

The CCD is configured through register writes on the command channel (light controller). The reverse engineering identified banked registers for scan geometry and exposure/gain:

  • a scan-window/geometry bank (start, end, and timing/span of the active CCD window)
  • an exposure/gain bank (per-channel R/G/B exposure, and per-channel dark trim with a sign bit)
  • a profile-mode selector (visible / IR / complete) that gates whether the IR channel is acquired

[DOCUMENTED]/[INFERRED] from the register-write sequences; the profile-mode selector corresponds to the IR/Digital-ICE fourth channel appearing in the image stream (see image-stream.md).

Dark / bright calibration and fixed-pattern correction

The OEM computes a per-column fixed-pattern correction from calibration lines (a set of dark reference lines and a set of bright, open-gate lines) so that each CCD column's gain and offset are individually normalised. [DOCUMENTED] the per-column dark-offset subtraction and per-column gain multiplication are visible in the OEM line processor; the exact gain-computation formula was reverse-engineered later (documented below).

A practical constraint worth recording: on this hardware the lamp only illuminates while the film transport is running, so the bright calibration pass must be taken with the motor moving: the calibration is driven, not a motor-off measurement. [INFERRED] from the lamp/transport behaviour.

The per-column gain formula (partial)

The per-column gain computation was reverse-engineered from the OEM CiConfigFixedPatternCorrection routine. The known form is:

gain[column] = (125 · 2^32) / (bright[column] − dark[column] − <prefix terms>)

with a dark target near 300 and a bright target near 64000, and the result clamped to a maximum. This is a partial formula: the <prefix terms> in the denominator and the exact clamp value are not independently established here, so it is not yet implementable as written. What is solid is the constant 125 · 2^32 (= 0x7d00000000), which was independently cross-confirmed against the same constant in Stefan Dierauf's libpakon, and the dark/bright targets. [DOCUMENTED, partial; June 2026]

Colour vs. calibration

This CCD-level calibration (gain/offset/exposure/fixed-pattern) is distinct from the colour negative inversion. Calibration makes the raw linear CCD values correct and uniform; the C-41 inversion (LUT + matrix) is a separate, later stage documented in color-pipeline.md.

Open questions

  • The fixed-pattern-correction formula is partial (prefix terms and clamp).
  • The two unnamed u32 words at the top of section A and the one at the end of section B, and the 120 unused bytes.
  • The register banks are only partially mapped.