// SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // lint-disable-file fixed-width-types /* Fingerprintd:Sensor — which pins the sensor hangs off, and which must never be touched. The FT9391's SPI bus belongs to TrustZone. The normal world drives only the sideband: a load-switch enable, a reset, and an interrupt. That is the same division of labour the downstream driver uses — it owns power, reset and the IRQ while the trustlet owns SPI. Pin numbers and the pad configuration come from the stock device tree, which names this node `focalfp_ft9362` even though the part is an FT9391 (vendor copy-paste; the trustlet registers four chip drivers and selects ft9391). No I/O here: the line numbers, the timings, and the guard. The shell opens the chip. */ export module Fingerprintd:Sensor; import std; export namespace fingerprintd::sensor { // The TLMM pin controller. Resolved BY LABEL, never by /dev/gpiochipN -- // the index is not stable across kernels and getting it wrong means // driving someone else's pins. // // The label is the DT node's unit address, not the driver's name: the node // is pinctrl@f100000, so the chardev reports "f100000.pinctrl". "tlmm" is // what the binding and every DT reference call it and it is NOT what the // chip advertises -- measured on the phone, where the seven chips are five // PMIC gpio banks plus this one and the 23-line 3440000.pinctrl. inline constexpr std::string_view ChipLabel = "f100000.pinctrl"; // A second, independent check on the chip: TLMM has 168 lines, and the // other pinctrl on this SoC has 23. Matching the label alone would be // enough today, but a chip that cannot even contain our highest line is // never the right one, and driving line 75 of the wrong controller is not // recoverable over ssh. inline constexpr unsigned MinChipLines = 76; // must contain IrqLine // From the stock DT node: vdd-gpio = <&tlmm 29>, reset-gpio = <&tlmm 74>, // irq-gpio = <&tlmm 75>, interrupts = <75 1> = IRQ_TYPE_EDGE_RISING. // No clocks, no supplies, no SPI phandle -- pure GPIO plus an interrupt. inline constexpr unsigned PowerLine = 29; // FP_3P3_EN, an ETA5053 load switch inline constexpr unsigned ResetLine = 74; // FPS_RESET_N inline constexpr unsigned IrqLine = 75; // FPS_INT_N // Despite the _N in the vendor's name, the line IDLES LOW and pulses high: // the DT says edge-rising. A reader expecting active-low sees the finger // backwards. inline constexpr bool IrqIsActiveHigh = true; // ---- The hazard ------------------------------------------------------- // // gpio8-11 are the fingerprint SPI pads and they are XPU-protected. // Touching one from the normal world is not an error return: it is an // immediate SError and the phone reboots on the spot. That was paid for // once, with a module load that took the machine down mid-insmod. // // Mainline's DTS reserves them (`gpio-reserved-ranges = <8 4>`), but a // userspace chardev request does not consult that, so the guard has to be // ours and it has to be checked on every line we open. inline constexpr unsigned ReservedSpiFirst = 8; inline constexpr unsigned ReservedSpiCount = 4; inline constexpr bool IsReserved(unsigned line) { return line >= ReservedSpiFirst && line < ReservedSpiFirst + ReservedSpiCount; } inline constexpr bool IsSafeLine(unsigned line) { return !IsReserved(line); } // The only lines this daemon ever opens. inline constexpr std::array OwnedLines = { PowerLine, ResetLine, IrqLine }; static_assert(IsSafeLine(PowerLine) && IsSafeLine(ResetLine) && IsSafeLine(IrqLine), "a line this daemon opens is XPU-protected"); // ---- Power sequencing ------------------------------------------------- // // Rail up, settle, release reset, settle. The delays are the ones the // working reference used; the sensor answers its ready pulse about a // millisecond after reset is released. inline constexpr std::chrono::milliseconds PowerSettle{250}; inline constexpr std::chrono::milliseconds ResetSettle{50}; // Both lines are driven low before the rail comes up, so a warm restart // starts from the same state as a cold one. inline constexpr bool AssertResetBeforePower = true; // ---- Why the daemon owns this ---------------------------------------- // // One sensor reset buys exactly ONE trustlet init. A second init in the // same power cycle answers -205 "Device not found". So a session that // fails cannot be recovered by re-running the init chain: the rail has to // go down and come back up first, and that means the process holding the // session must also be the process holding the rail. // // The research harness split them -- a Python script held the rail for a // fixed number of seconds while a separate binary drove the trustlet -- and // every run had to restart the holder first or fail. inline constexpr int RcDeviceNotFound = -205; enum class Power { Off, On }; }