Own the sensor rail, and run the init chain against it
The daemon now powers the sensor and initialises the trustlet against it. On
the phone, every step of the chain returning rc=0:
gpiochip 'f100000.pinctrl' is /dev/gpiochip5 (168 lines)
sensor powered, reset released, irq=1
CMD 0x1006 INIT_SPI rc=0
CMD 0x100a PROBE_DEVICE rc=0
CMD 0x100b INIT_DEVICE rc=0
CMD 0x1004 TA_INIT rc=0
CMD 0x1020 WORK_MODE rc=0
CMD 0x100e SYNC_STATISTICS rc=0
GPIO v2 chardev ioctls directly rather than libgpiod, which is on neither the
phone nor the sysroot and would be a dependency for three lines.
The chip is found by label, and the label is not what the device tree calls it:
the node is pinctrl@f100000 so the chardev advertises "f100000.pinctrl", while
every DT reference says "tlmm". Matching on "tlmm" finds nothing, which is how
the first run failed. There is a second check on the line count, because this
SoC has another pinctrl with 23 lines and driving line 75 of the wrong
controller is not something you recover from over ssh.
The XPU guard is enforced where the line is actually opened, not only asserted
in the core. gpio8-11 are the fingerprint SPI pads and touching one is an
immediate SError with the phone rebooting where it stands, so a refusal has to
sit in front of the ioctl.
Owning the rail is what makes the session recoverable at all: one reset buys
exactly one trustlet init and a second answers -205, so a failed session needs
the rail cycled rather than the chain retried. The harness split these across
two processes and every run began by restarting the one holding the rail.
CAPTURE_IMAGE answers -201 here and that is correct, not a regression: it needs
a shared memory region whose address QTEE patches into the payload, and none is
supplied yet. That is the next piece.
This commit is contained in:
parent
2648e46d43
commit
1fb57cd1be
5 changed files with 357 additions and 3 deletions
|
|
@ -32,7 +32,9 @@ extern "C" {
|
||||||
#include <qcomtee_errno.h>
|
#include <qcomtee_errno.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include <linux/gpio.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
@ -215,6 +217,111 @@ qcomtee_object* OpenService(qcomtee_object* env, std::uint32_t uid) {
|
||||||
return p[1].object;
|
return p[1].object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- The sensor rail
|
||||||
|
//
|
||||||
|
// GPIO v2 chardev ioctls directly: libgpiod is not on the phone and this is
|
||||||
|
// three lines. The chip is found by LABEL, never by index -- /dev/gpiochipN
|
||||||
|
// ordering is not stable and driving the wrong controller's pins is the kind
|
||||||
|
// of mistake that is not recoverable over ssh.
|
||||||
|
class Sensor {
|
||||||
|
public:
|
||||||
|
~Sensor() { PowerOff(); }
|
||||||
|
|
||||||
|
bool Open() {
|
||||||
|
namespace sn = fingerprintd::sensor;
|
||||||
|
chip_ = FindChip(sn::ChipLabel);
|
||||||
|
if (chip_ < 0) {
|
||||||
|
std::println(std::cerr, "no gpiochip labelled '{}'", sn::ChipLabel);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
power_ = RequestLine(sn::PowerLine, GPIO_V2_LINE_FLAG_OUTPUT, "fpd-pwr");
|
||||||
|
reset_ = RequestLine(sn::ResetLine, GPIO_V2_LINE_FLAG_OUTPUT, "fpd-rst");
|
||||||
|
irq_ = RequestLine(sn::IrqLine, GPIO_V2_LINE_FLAG_INPUT, "fpd-irq");
|
||||||
|
return power_ >= 0 && reset_ >= 0 && irq_ >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rail up, settle, release reset, settle. Both lines are driven low first
|
||||||
|
// so a warm restart starts where a cold one does.
|
||||||
|
bool PowerOn() {
|
||||||
|
namespace sn = fingerprintd::sensor;
|
||||||
|
if (!Set(power_, 0) || !Set(reset_, 0)) return false;
|
||||||
|
if (!Set(power_, 1)) return false;
|
||||||
|
std::this_thread::sleep_for(sn::PowerSettle);
|
||||||
|
if (!Set(reset_, 1)) return false;
|
||||||
|
std::this_thread::sleep_for(sn::ResetSettle);
|
||||||
|
on_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerOff() {
|
||||||
|
if (!on_) return;
|
||||||
|
Set(reset_, 0);
|
||||||
|
Set(power_, 0);
|
||||||
|
on_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> ReadIrq() const { return Get(irq_); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int FindChip(std::string_view label) {
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
std::string path = std::format("/dev/gpiochip{}", i);
|
||||||
|
int fd = ::open(path.c_str(), O_RDWR | O_CLOEXEC);
|
||||||
|
if (fd < 0) continue;
|
||||||
|
gpiochip_info info{};
|
||||||
|
if (::ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info) == 0
|
||||||
|
&& label == info.label
|
||||||
|
&& info.lines >= fingerprintd::sensor::MinChipLines) {
|
||||||
|
std::println("gpiochip '{}' is {} ({} lines)", info.label, path,
|
||||||
|
info.lines);
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
::close(fd);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int RequestLine(unsigned line, std::uint64_t flags, const char* consumer) {
|
||||||
|
// The guard, enforced where the line is actually opened rather than
|
||||||
|
// only asserted in the core. gpio8-11 are XPU-protected and touching
|
||||||
|
// one is an immediate SError, not an error return.
|
||||||
|
if (!fingerprintd::sensor::IsSafeLine(line)) {
|
||||||
|
std::println(std::cerr,
|
||||||
|
"REFUSING to open gpio{}: XPU-protected fingerprint SPI", line);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
gpio_v2_line_request req{};
|
||||||
|
req.offsets[0] = line;
|
||||||
|
req.num_lines = 1;
|
||||||
|
req.config.flags = flags;
|
||||||
|
std::snprintf(req.consumer, sizeof(req.consumer), "%s", consumer);
|
||||||
|
if (::ioctl(chip_, GPIO_V2_GET_LINE_IOCTL, &req) < 0) {
|
||||||
|
std::println(std::cerr, "gpio{} request failed: {}", line, ::strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return req.fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Set(int fd, int v) {
|
||||||
|
if (fd < 0) return false;
|
||||||
|
gpio_v2_line_values vals{};
|
||||||
|
vals.mask = 1;
|
||||||
|
vals.bits = v ? 1 : 0;
|
||||||
|
return ::ioctl(fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::optional<int> Get(int fd) {
|
||||||
|
if (fd < 0) return std::nullopt;
|
||||||
|
gpio_v2_line_values vals{};
|
||||||
|
vals.mask = 1;
|
||||||
|
if (::ioctl(fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &vals) < 0) return std::nullopt;
|
||||||
|
return static_cast<int>(vals.bits & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int chip_ = -1, power_ = -1, reset_ = -1, irq_ = -1;
|
||||||
|
bool on_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
// ---- The trustlet
|
// ---- The trustlet
|
||||||
//
|
//
|
||||||
// The loader is IQSEEComCompatAppLoader (UID 122): op 1 loadFromBuffer, op 2
|
// The loader is IQSEEComCompatAppLoader (UID 122): op 1 loadFromBuffer, op 2
|
||||||
|
|
@ -388,11 +495,67 @@ int Probe() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// A storage read needs no sensor. It exercises the whole SFS listener path
|
// A storage read needs no sensor. It exercises the whole SFS listener path
|
||||||
// if listeners are registered, and answers -2 when they are not.
|
// if listeners are registered, and answers with no templates when they are
|
||||||
|
// not.
|
||||||
auto e = SendCommand(app, fingerprintd::ta::Cmd::Enumerate, {});
|
auto e = SendCommand(app, fingerprintd::ta::Cmd::Enumerate, {});
|
||||||
Report(fingerprintd::ta::Cmd::Enumerate, e);
|
Report(fingerprintd::ta::Cmd::Enumerate, e);
|
||||||
|
|
||||||
std::println("\ntrustlet is up and configured. Sensor not powered yet.");
|
// ---- The sensor, and the init chain that needs it powered
|
||||||
|
Sensor sensor;
|
||||||
|
if (!sensor.Open()) {
|
||||||
|
std::println(std::cerr, "sensor lines unavailable; stopping before init");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!sensor.PowerOn()) {
|
||||||
|
std::println(std::cerr, "sensor power-up failed");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
auto irq = sensor.ReadIrq();
|
||||||
|
std::println("sensor powered, reset released, irq={}",
|
||||||
|
irq ? std::to_string(*irq) : std::string("?"));
|
||||||
|
|
||||||
|
// The chain, in order. Every step answers rc=0 on a healthy sensor and the
|
||||||
|
// last one is not optional: without SYNC_STATISTICS the trustlet's
|
||||||
|
// g_statistics stays NULL and the first enrol frame that gets far enough
|
||||||
|
// writes through it.
|
||||||
|
//
|
||||||
|
// One reset buys one init. If this fails, the rail has to go down and come
|
||||||
|
// back up -- re-running the chain answers -205.
|
||||||
|
bool ok = true;
|
||||||
|
for (fingerprintd::ta::Cmd c : fingerprintd::ta::InitChain) {
|
||||||
|
std::vector<std::byte> payload;
|
||||||
|
if (c == fingerprintd::ta::Cmd::WorkMode) {
|
||||||
|
// WORK_MODE takes a u32 mode; 1 = WAIT_TOUCH.
|
||||||
|
payload.assign(0x10, std::byte{0});
|
||||||
|
payload[0] = static_cast<std::byte>(
|
||||||
|
static_cast<std::uint32_t>(fingerprintd::ta::WorkMode::WaitTouch));
|
||||||
|
} else if (c == fingerprintd::ta::Cmd::SyncStatistics) {
|
||||||
|
payload.assign(fingerprintd::ta::SyncStatisticsPayloadSize, std::byte{0});
|
||||||
|
}
|
||||||
|
auto ir = SendCommand(app, c, payload);
|
||||||
|
Report(c, ir);
|
||||||
|
if (!ir.invoked || ir.result != 0 || ir.rc != 0) {
|
||||||
|
ok = false;
|
||||||
|
if (ir.rc == fingerprintd::sensor::RcDeviceNotFound)
|
||||||
|
std::println(std::cerr,
|
||||||
|
" -205: a second init in one power cycle. "
|
||||||
|
"Power-cycle the rail, do not retry.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
std::println(std::cerr, "init chain did not complete");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the sensor initialised, a capture returns a real metric. No finger
|
||||||
|
// is needed to see the idle floor.
|
||||||
|
std::vector<std::byte> cap(fingerprintd::ta::CaptureDeclaredLen, std::byte{0});
|
||||||
|
auto c1 = SendCommand(app, fingerprintd::ta::Cmd::CaptureImage, cap);
|
||||||
|
Report(fingerprintd::ta::Cmd::CaptureImage, c1);
|
||||||
|
std::println(" idle capture metric = {}", c1.metric);
|
||||||
|
|
||||||
|
std::println("\ntrustlet initialised against a powered sensor.");
|
||||||
pthread_cancel(th);
|
pthread_cancel(th);
|
||||||
pthread_join(th, nullptr);
|
pthread_join(th, nullptr);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
106
interfaces/Fingerprintd-Sensor.cppm
Normal file
106
interfaces/Fingerprintd-Sensor.cppm
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
// 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<unsigned, 3> 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 };
|
||||||
|
}
|
||||||
|
|
@ -18,3 +18,4 @@ export import :Ta;
|
||||||
export import :Engine;
|
export import :Engine;
|
||||||
export import :Store;
|
export import :Store;
|
||||||
export import :Tee;
|
export import :Tee;
|
||||||
|
export import :Sensor;
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
ApplyStandardArgs(*Core, args);
|
ApplyStandardArgs(*Core, args);
|
||||||
Core->type = ConfigurationType::LibraryStatic;
|
Core->type = ConfigurationType::LibraryStatic;
|
||||||
{
|
{
|
||||||
std::array<fs::path, 7> ifaces = {
|
std::array<fs::path, 8> ifaces = {
|
||||||
"interfaces/Fingerprintd",
|
"interfaces/Fingerprintd",
|
||||||
"interfaces/Fingerprintd-Sfs",
|
"interfaces/Fingerprintd-Sfs",
|
||||||
"interfaces/Fingerprintd-Rpmb",
|
"interfaces/Fingerprintd-Rpmb",
|
||||||
|
|
@ -57,6 +57,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
"interfaces/Fingerprintd-Engine",
|
"interfaces/Fingerprintd-Engine",
|
||||||
"interfaces/Fingerprintd-Store",
|
"interfaces/Fingerprintd-Store",
|
||||||
"interfaces/Fingerprintd-Tee",
|
"interfaces/Fingerprintd-Tee",
|
||||||
|
"interfaces/Fingerprintd-Sensor",
|
||||||
};
|
};
|
||||||
std::array<fs::path, 0> impls = {};
|
std::array<fs::path, 0> impls = {};
|
||||||
Core->GetInterfacesAndImplementations(ifaces, impls);
|
Core->GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
|
@ -86,6 +87,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.AddTest("Engine").Dependencies({ Core.get() });
|
cfg.AddTest("Engine").Dependencies({ Core.get() });
|
||||||
cfg.AddTest("Store").Dependencies({ Core.get() });
|
cfg.AddTest("Store").Dependencies({ Core.get() });
|
||||||
cfg.AddTest("Tee").Dependencies({ Core.get() });
|
cfg.AddTest("Tee").Dependencies({ Core.get() });
|
||||||
|
cfg.AddTest("Sensor").Dependencies({ Core.get() });
|
||||||
|
|
||||||
ProjectLint::AddProjectLintRules(cfg);
|
ProjectLint::AddProjectLintRules(cfg);
|
||||||
|
|
||||||
|
|
|
||||||
82
tests/Sensor/main.cpp
Normal file
82
tests/Sensor/main.cpp
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
// lint-disable-file fixed-width-types
|
||||||
|
/*
|
||||||
|
Fingerprintd:Sensor unit tests.
|
||||||
|
|
||||||
|
Mostly one thing. gpio8-11 are the fingerprint SPI pads, they are
|
||||||
|
XPU-protected, and touching one from the normal world is not an error return --
|
||||||
|
it is an immediate SError and the phone reboots where it stands. Mainline's DTS
|
||||||
|
reserves the range but a userspace chardev request never consults that, so the
|
||||||
|
guard is ours and it is worth a test that spells out every pin.
|
||||||
|
*/
|
||||||
|
import std;
|
||||||
|
import Fingerprintd;
|
||||||
|
|
||||||
|
using namespace fingerprintd::sensor;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
int Failures = 0;
|
||||||
|
void Check(bool cond, std::string_view msg) {
|
||||||
|
if (!cond) {
|
||||||
|
std::println(std::cerr, "FAIL: {}", msg);
|
||||||
|
++Failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// ---- The XPU-protected range, pin by pin
|
||||||
|
Check(IsReserved(8) && IsReserved(9) && IsReserved(10) && IsReserved(11),
|
||||||
|
"gpio8-11 are all reserved");
|
||||||
|
Check(!IsReserved(7), "gpio7 is below the range");
|
||||||
|
Check(!IsReserved(12), "gpio12 is above it");
|
||||||
|
Check(ReservedSpiFirst == 8 && ReservedSpiCount == 4,
|
||||||
|
"the range matches the DTS gpio-reserved-ranges = <8 4>");
|
||||||
|
|
||||||
|
for (unsigned l = 8; l <= 11; l++)
|
||||||
|
Check(!IsSafeLine(l), std::format("gpio{} must never be opened", l));
|
||||||
|
|
||||||
|
// ---- Every line we own is outside it
|
||||||
|
for (unsigned l : OwnedLines)
|
||||||
|
Check(IsSafeLine(l), std::format("owned line gpio{} is safe", l));
|
||||||
|
Check(OwnedLines.size() == 3, "three lines: power, reset, irq");
|
||||||
|
|
||||||
|
// ---- The pins themselves, from the stock device tree
|
||||||
|
Check(PowerLine == 29, "vdd-gpio = <&tlmm 29>");
|
||||||
|
Check(ResetLine == 74, "reset-gpio = <&tlmm 74>");
|
||||||
|
Check(IrqLine == 75, "irq-gpio = <&tlmm 75>");
|
||||||
|
Check(PowerLine != ResetLine && ResetLine != IrqLine && PowerLine != IrqLine,
|
||||||
|
"the three are distinct");
|
||||||
|
|
||||||
|
// The vendor calls it FPS_INT_N but the DT says edge-rising: it idles low
|
||||||
|
// and pulses high. Reading it as active-low sees the finger backwards.
|
||||||
|
Check(IrqIsActiveHigh, "the interrupt is active high despite the _N");
|
||||||
|
|
||||||
|
// ---- The chip is found by label
|
||||||
|
//
|
||||||
|
// The chardev reports the DT unit address, not the binding's name. "tlmm"
|
||||||
|
// is what every DT reference calls it and matching on that finds nothing.
|
||||||
|
Check(ChipLabel == "f100000.pinctrl", "TLMM advertises its DT unit address");
|
||||||
|
Check(ChipLabel != "tlmm", "the binding name is not the chardev label");
|
||||||
|
Check(!ChipLabel.empty(), "never an index: /dev/gpiochipN is not stable");
|
||||||
|
|
||||||
|
// The line-count check has to actually exclude the other pinctrl on this
|
||||||
|
// SoC, which has 23 lines, while admitting TLMM's 168.
|
||||||
|
Check(MinChipLines > IrqLine, "a valid chip can contain every line we open");
|
||||||
|
Check(MinChipLines > 23, "excludes the 23-line 3440000.pinctrl");
|
||||||
|
Check(MinChipLines <= 168, "admits TLMM");
|
||||||
|
|
||||||
|
// ---- Sequencing
|
||||||
|
Check(PowerSettle > ResetSettle, "the rail gets the longer settle");
|
||||||
|
Check(PowerSettle.count() == 250 && ResetSettle.count() == 50, "the proven delays");
|
||||||
|
Check(AssertResetBeforePower, "a warm restart starts from the cold state");
|
||||||
|
|
||||||
|
// ---- The reason the daemon owns the rail at all
|
||||||
|
Check(RcDeviceNotFound == -205,
|
||||||
|
"a second init in one power cycle answers -205, so recovery is a power cycle");
|
||||||
|
|
||||||
|
if (Failures == 0) std::println("Sensor: all tests passed");
|
||||||
|
return Failures;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue