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.