# GL128: what was learned at the chip level This document holds the findings that are properties of the **ASIC**, not of the A320E as a product. They are the part most likely to be useful to someone adding another device from the same family. Everything here was measured, either from USB captures of the vendor's Windows and macOS drivers or from instrumented runs against the device. Where a claim rests on a single observation, it says so. ## 1. Identifying the chip The scanner reports `bcdDevice = 0x702`, and `sane-find-scanner` prints `chip=GL128`. That string is not a probe result. The logic behind it (at `0x2cb9` in the binary) asks the chip nothing; it maps byte 12 of the USB device descriptor to a name. The table reconstructed from it: | `bcdDevice` | name | | `bcdDevice` | name | |---|---|---|---|---| | `0x702` | **GL128** | | `0x500`–`0x5ff` | GL843 | | `0x701` | GL124 | | `0x300`–`0x4ff` | GL842 | | `0x700`, `≥0x703` | GL848+ | | `0x102`–`0x2ff` | GL841 | | `0x605`–`0x6ff` | GL845 | | `≤0x101` | GL646_HP | | `0x603`–`0x604` | GL847 | | | | | `0x600`–`0x602` | GL846 | | | | An earlier working assumption in this project was GL846. **It was wrong**, and the way it was wrong is instructive: it rested on symbols found in the vendor's `ScanApi` binary, but that framework covers 27 models, so the presence of a symbol says nothing about which chip *this* device uses. The stronger evidence is independent of the descriptor: **The register space is 8 bits wider than GL846's.** The vendor's own device description contains `AfeReadReg = 0x0050,0x0107,0x0108` and keys `114` / `115`. In SANE, `gl124_registers.h:255-256` defines `REG_0x114` and `REG_0x115`; `gl846_registers.h` defines **no** register at or above `0x100`. A device addressing `0x107`, `0x108`, `0x114` and `0x115` is not a GL846. `bcdDevice` is firmware-settable and a vendor could in principle put anything there. The register map is silicon. The two agree. ### Which devices this transfers to The vendor's plist collection covers 27 models, and they split cleanly into two chip families by exactly this criterion — whether registers at or above `0x100` exist: | family | marker | Plustek PIDs (`07b3`) | |---|---|---| | GL124/GL128 | keys `114`, `115` + `AfeReadReg 0x107,0x108` | `1406 140E 141F 1422 1801 1802 181B 1823 1826` | | older | no registers ≥ `0x100` | `1301 130C 130D 131C 131E 1320 1436 143D 181F 1820 1824 1825` | **This table describes the plists, and the plists are not the last word.** Two of the PIDs in the older row are worth flagging, because your own tree says something different about them: - `doc/descriptions/genesys.desc:74` lists the OpticFilm 8200i (`07b3:1825`) as `:status :unsupported` with the comment **"GL128 based"** — while the vendor plist for that same PID shows no registers at or above `0x100`. Note that the tree also carries an OpticFilm 8200i at `07b3:130d` with `:status :complete`, and `130d` is one of the two devices the register-space test cross-validates as the older family. - In April 2026 Alessandro Coan reported on sane-devel a **new revision** of the OpticFilm 8100 (`07b3:1824`) that genesys does not detect. The straightforward reading is that these product IDs cover more than one revision, and the plists in this collection describe the older silicon — which would also explain why a new-revision 8100 goes undetected by a driver written against the old one. **I cannot settle it from here**: it needs `bcdDevice` read off an actual unit of the revision in question, which is one line of `lsusb -v` for anyone who owns one. So: treat the table as a statement about the plists, not about every unit ever sold under those IDs. If the 8200i at `1825` really is GL128, the material in this document should apply to it — with the caveat that both neighbours are film scanners with different optics and mechanics, so only the silicon-level findings could transfer, not the tables. `181B` is the same physical scanner as `1826`: the A320E ships as internal model 2D1 or 2D3, and the only configuration difference is `StaggeredLine = 4` versus `-4`. The heuristic is cross-validated where it can be: `130C` and `130D` are the only two devices for which both a vendor plist and a SANE verdict exist. SANE calls them `AsicType::GL845` — the older family — and the register-space test puts them there too. There is, unfortunately, **no chip-level Rosetta stone**: the intersection of "has a vendor plist" and "is supported by SANE" is exactly `{130C, 130D}`, both of the older family. SANE's four GL124 devices are all Canon LiDE models, for which no plist exists. ## 2. The analog front end is an AD9822 The device does not use the "ti" front end that the LiDE GL124 models use (`SIFSEL = 3`). It sets `SIFSEL = 1` and drives an **Analog Devices AD9822**. The chip was identified by reading the write sequence out of a USB capture and matching it against the datasheet. The register map: | address | contents | |---|---| | `0x00` | Configuration (VREF, 3-channel, CDS mode) | | `0x01` | MUX — channel order R-G-B | | `0x02`–`0x04` | PGA gain, 6 bits D5..D0, 1.0x–5.7x | | `0x05`–`0x07` | Offset, 9 bits, **sign and magnitude**, D8 = sign | This is exactly SANE's existing `analog_devices` layout. The AD9822 differs from the AD9826 that layout was written for only in scale (gain 1–5.7x rather than 1–6x, offset ±350 mV rather than ±300 mV), so the AD9826 formula in `compute_frontend_gain` is close enough — calibration iterates the difference away. ### Configuration register `0x00` is not a constant This is the finding most likely to bite someone else. The vendor changes `0x00` **with the speed block**: | speed block | resolutions | `0x00` | |---|---|---| | Speed2 ("48M+12CLK") | 100–400 dpi | `0x70` | | Speed1 ("60M+12CLK") | 600/800 dpi | `0xf8` | The differing bits are D7 (ADC full scale, 2 V → 4 V) and D3 (input clamp bias, 3 V → 4 V; at 3 V the input's peak range shrinks to 3 volts). The vendor doubles the ADC's full scale at exactly the point where it doubles the exposure (LPERIOD 6256 → 12000). Hardcoding Speed2's `0x70` at every resolution — the natural mistake, since a 200 dpi capture is a Speed2 run — makes 600 and 800 dpi run on half the ADC scale. The symptom is a flat field: gain calibration drives the PGA to its ceiling of 63 and the white reference saturates. Two further points, both learned the hard way: - **The value must be taken from the session's requested resolution, not from the sensor passed to `set_fe`.** Calibration calls `set_fe` with a low-resolution `calib_sensor` — in an 800 dpi scan, `set_fe` runs 14 times and in six of those the sensor is the 100 dpi row. Taken per sensor, `0x00` swings mid-session and calibration and the image scan end up measuring on different ADC scales, which ruins the shading coefficients. - **The vendor writes `0x00` exactly once**, at the start of the session, and holds it. Verified across four captures. ### The offset search only reaches half the range `scanner_offset_calibration` sets `bottom[ch] = 10` and searches upward, so it never sets D8 — it can only ever produce a positive offset. The vendor searches both sides and ends up **negative** for R at 800 dpi (`0x134` = −52). The trap for anyone implementing this: the value is sign-and-magnitude, not two's complement. A search variable `s ∈ [−255, 255]` maps to the register as `s < 0 ? 0x100 | (−s) : s`. **Measured, since the obvious inference from the paragraph above is wrong.** Writing the vendor's own negative value (`0x134`) does not improve anything (runs 320, 321). Moving the offset does not move the operating point either: the gain calibration that follows compensates for it and the white level comes back to where it was. Whatever the vendor gains at 800 dpi, it is not gained by being on the negative side of the offset. A related trap: the vendor's capture writes AFE register `0x26`, which looks like a register of its own. It is an **alias for register 6** (the green offset) — writing `0x26 = 0` and `0x06 = 0` produce bit-identical output on the device (runs 317–319). ## 3. Registers whose meaning differs from upstream's assumption | register | upstream's handling | what it actually is | |---|---|---| | `0x31` bit `0x80` | written once from the GPIO table | **speed block select.** `0` = Speed1 (60 MHz, ≥600 dpi), `1` = Speed2 (48 MHz, ≤400 dpi). Getting it wrong at 800 dpi makes every other pixel exactly zero — 50.0 % of the data. It must be adjusted per scan. | | `0x32` bit `0x02` | cleared as an output (`GPIO10`) | **the home switch input.** The vendor's plist states it: `HomeSwitch = '0x32,0x02,0x02'` (register, mask, home value). Clearing it pins `HOMESNR` high, so `scanner_move_back_home` concludes "already at home" every time and the carriage is never returned. | | `0x06` SCANMOD | never touched | must be cleared before every scan. The device's init table sets `SCANMOD = 6`; the vendor writes `0` immediately after initialisation and never goes back. | | `0x22` | never written; unnamed in `gl124_registers.h` | `FWDSTEP` (`min_restep`), as on every other genesys ASIC. Both vendor drivers write `0x03` before the first scan. | | `0x0b` | hardcoded `0x2a` (30 MHz, 64 Mbit) | the device's init table sets `0x6c` = 48 MHz, 256 Mbit DRAM. The hardcoded value makes the memory map the wrong size and image data is read from an area the scan never filled. | | `0x08` | `CIS_LINE` (`0x10`) | on a CCD device the init table sets `0x20` (`MPENB`). | Reading a GPIO register back does **not** return the last value written — it returns the state of the pins. Bit-level changes to `0x31` therefore have to be made against a shadow copy of the last write, not against a read. ## 4. The shading RAM is paged and interleaved Two independent properties, both of which have to be right or the image breaks in a characteristic way. **Interleaved, not planar.** The coefficient table is 4 bytes per channel per pixel with the channels consecutive *within* a pixel (dark 16-bit, coefficient 16-bit). The dark values in the vendor's upload repeat with a period of 3. The whole window goes up as a **single** AHB write; `0xd0` = `0xd1` = `0xd2` in every mode, so the generic code's three separate writes land on the same address and overwrite each other. The symptom is that data the size of one channel covers exactly the first third of the pixels. **Paged.** 252 words of payload, then a 4-word gap, page 256 words — the same layout `gl843.cpp:1741-1776` implements. At 12 bytes per pixel that is 504/12 = **42 pixels** per page, which is exactly the period of the phase slip seen in broken images. Without honouring the gap, the ASIC reads pixel *p*'s coefficients from word address `6p + 4*floor(p/42)`. Measured at 200 dpi over 20 mm: paging on gives 100.0 % data and an intact image; off gives 30.8 % and 42-pixel stripes. **Overflow margin.** The ASIC computes `out = (in - dark) * coeff / 0x4000`. The coefficient is by definition `0x4000*target/(white-dark)`, so `out = target` when `in = white`. If image data exceeds the white level measured at calibration, the product exceeds 65535 and **wraps**, giving zeros in the brightest places. A target of `0x8000` rather than `0xdc00` leaves the input room to exceed the local white level by 2x before wrapping; in practice `in/white` stays below about 1.3. The vendor uses coefficients in the range 19301–65535, so values above `0x4000` are permitted — it is only the product's overflow that is not. The cost is a nominal white level at roughly half scale, and it must be paid back somewhere other than here. The ASIC applies the gamma table **after** this division, and there overflow clips instead of wrapping, so that is where this driver compensates: see `hardware-a320e.md` §3, "The white level is compensated in the gamma table". Raising `target_code` instead would buy brightness by spending the wrap margin, which is the one thing this margin exists for. ## 5. Image buffer addresses depend on the mode Registers `0xe0`–`0xf7` are the ASIC's image buffer addresses: twelve 16-bit values, i.e. six (begin, end) pairs. The vendor uses two layouts and switches between them just before the image scan: | mode | layout | `0xf8` | |---|---|---| | colour | six separate buffers of 2714 words | `0x05` | | gray/lineart | two buffers of 8142 words, repeated across the three channel slots | `0x23` | The available space is `[0x0064, 0x3fff]` = 16284 words, divided into 2 × channels parts. A memory-layout table read from the *initialisation* phase of a capture is the colour layout; used unchanged in gray, it gives the single active channel a third of the buffer it needs. These registers are not members of any `Genesys_Register_Set` — the generic code sends them with `apply_reg_settings_to_device_write_only` — so they have to be written directly to the device, one at a time, after calibration and before the image scan. That is also what the vendor does. ## 6. The motor is not stopped at home Homing is an open loop: at EOF the driver commands roughly 1100 steps backwards and nothing stops the motor when `HOMESNR` rises. Upstream does that for GL846/GL847 only. On this device the leftover steps grind audibly against the end stop for several seconds after the carriage has already arrived. `scanner_stop_action` is idempotent, so calling it on arrival is safe even when the return did complete. Upstream's timeout path is worth a look too: when the carriage fails to reach the park position it throws without stopping the motor at all.