A Linux driver for the Himax HM1092, the monochrome near-infrared sensor
behind the Windows Hello face-unlock camera on Intel IPU6/IPU7 laptops. There
is no support for this part anywhere upstream, in intel/ipu6-drivers, or in
intel/ipu7-drivers — the sensor enumerates as an ACPI device, sits there
unclaimed, and produces nothing.
Developed and tested on a Lenovo ThinkPad X9-14 Gen 1 (Lunar Lake, IPU7), kernel 7.1.
hm1092 |
v4l2 subdev driver: 1296x736 and 648x368, RAW10 mono, exposure / analogue + digital gain / vblank / test pattern |
ipu-bridge |
rebuilt with HM1092 in its sensor table, plus a DMI quirk for the X9-14's inverted module |
hm1092-capture |
one command from cold hardware to a PNG, illuminator included |
hm1092-relay |
publishes the sensor as an 8-bit grayscale v4l2loopback node so OpenCV consumers (howdy, face unlock) can read it, lighting the emitter only while something is attached |
| NixOS module | hardware.hm1092.enable = true; |
| DKMS | for everyone else |
Not done yet: no in-kernel driver for the LM3643 IR emitter, so nothing
strobes it per-frame. hm1092-capture and hm1092-relay drive it in torch
mode over i2c while they are capturing. See Illuminator.
hardware.hm1092 = {
enable = true;
relay = {
enable = true;
sink = "/dev/video45"; # a v4l2loopback node you allocate
group = "howdy"; # not "video" — see below
rotate180 = true; # if the module is mounted inverted
};
};Point howdy's device_path at the sink and enrol. Two things to get right:
- Re-enrol. Face models are wavelength-specific; anything trained against a visible-light camera will not match an IR frame.
- Group. Whoever can open the sink's OUTPUT side can inject frames, and an
injected frame is a face-unlock bypass needing neither a photo nor physical
access. Give the node a group holding only the accounts allowed to
authenticate, not
video.
dlib's detector is not rotation invariant, which is why rotate180 matters:
upside down it finds no face at all rather than a worse one.
{
inputs.hm1092.url = "github:meros/hm1092-linux";
outputs = { nixpkgs, hm1092, ... }: {
nixosConfigurations.laptop = nixpkgs.lib.nixosSystem {
modules = [
hm1092.nixosModules.default
{ hardware.hm1092.enable = true; }
];
};
};
}Rebuild and reboot — see ipu-bridge is boot-only. Then:
$ hm1092-capture --list
media device : /dev/media0
sensor : hm1092 2-0024 (/dev/v4l-subdev5)
receiver : Intel IPU7 CSI2 1
capture : Intel IPU7 ISYS Capture 8 (/dev/video8)
i2c bus : 2
$ sudo hm1092-capture -o face.png --exposure 796 --gain 240
captured 14 frames, 10-bit levels 8..771 -> face.pngOptions worth knowing:
| flag | |
|---|---|
--no-torch |
leave the illuminator off (expect a near-black frame) |
--torch-brightness N |
LM3643 torch level, 0-127, default 40 |
--no-rotate |
skip the 180 degree flip |
--raw |
no autoscaling, straight 10-to-8 bit truncation |
--keep-raw PATH |
also keep the undecoded capture |
Build and install the sensor driver:
$ make -C src
$ sudo make -C src installOr via DKMS, so it survives kernel updates:
$ sudo cp -r . /usr/src/hm1092-0.1.0
$ sudo dkms add -m hm1092 -v 0.1.0
$ sudo dkms install -m hm1092 -v 0.1.0That alone is not enough: ipu-bridge still has to learn the sensor.
Either patch and rebuild your kernel with patches/ipu-bridge-hm1092.patch, or
build just that one module out of tree against your kernel source and drop it
into updates/, where depmod prefers it over the in-tree copy:
$ cp /usr/src/linux/drivers/media/pci/intel/ipu-bridge.c build/
$ patch -d build -p4 < patches/ipu-bridge-hm1092.patch
$ echo 'obj-m += ipu-bridge.o' > build/Makefile
$ make -C /lib/modules/$(uname -r)/build M=$PWD/build modules
$ sudo install -D build/ipu-bridge.ko /lib/modules/$(uname -r)/updates/ipu-bridge.ko
$ sudo depmod -a && sudo rebootThe capture tool needs python3 (standard library only), v4l-utils and
i2c-tools.
Everything below was measured on the machine or recovered from Intel's Windows driver; nothing is guessed.
| Sensor | Himax HM1092, 1/9" 720p mono/NIR |
| ACPI HID | HM1092 (HIMX1092 on some Dell machines) |
| i2c | bus 2, address 0x24, 16-bit register addresses, 8-bit data |
| Chip ID | reads 0x1091 from 0x0000/0x0001 |
| CSI-2 | link 1, 1 lane, RAW10 |
| Link frequency | 180 480 000 Hz (360.96 Mbit/s), pixel rate 36 096 000 |
| Modes | 1296x736 @ 30, 648x368 @ 30 and @ 60 (2x2 binned) |
| Timing | full mode LLP 1508, FLL 798 |
| MCLK | 19.2 MHz, from INT3472:01-clk |
| Power | avdd regulator plus a privacy LED from INT3472:01; no reset or powerdown GPIO |
| Illuminator | TI LM3643 at 0x63 on the same bus |
The link frequency is not documented anywhere; it falls out of the mode timing and cross-checks exactly:
1508 px * 798 lines * 30 fps * 10 bits = 360.96 Mbit/s over 1 lane
= 180.48 MHz DDR clock
and the ACPI SSDB independently reports link 1, lanes 1, mclk 19200000 Hz.
These cost real time, so they are worth stating plainly.
In either direction. A 16-bit read of the chip ID at 0x0000 returns 0x10
followed by a bus-idle 0xff; a 16-bit write to 0x0340 is NAKed with
-EREMOTEIO. Every 16-bit field — exposure, VTS, digital gain, the chip ID —
is therefore two separate 8-bit transactions. This is why the driver uses
CCI_REG8 throughout and never CCI_REG16.
ipu_bridge_init() leaks its struct ipu_bridge on the success path and never
unregisters the software nodes it registers — the comment in-tree says as much,
"so that it survives if the module is unloaded". Those nodes' property arrays
point into the module's .rodata. rmmod ipu_bridge therefore leaves dangling
swnodes behind, and the next sensor probe faults in
fwnode_property_read_int_array():
BUG: unable to handle page fault for address: ffffffffc0db7530
software_node_read_int_array+0x83/0xe0
fwnode_property_read_int_array+0x55/0xe0
v4l2_fwnode_endpoint_alloc_parse+0x106/0x1c0 [v4l2_fwnode]
There is no way to hot-swap it. Reboot after installing.
The module carries an IR bandpass filter, and modern LED room lighting emits almost no NIR, so ambient frames are close to black — mean level 33 out of 1023 in a lit room here. With the LM3643 in torch mode that goes to 141 and the image is perfectly usable.
Nothing in the kernel claims the LM3643, so hm1092-capture pokes it directly.
Its register map is shifted one from the LM3644 many datasheets lead you to
expect: 0x00 is a read-only ID and 0x01 is Enable, with mode in bits
[3:2] (10 = torch) and LED1/LED2 in the low two bits. Torch brightness is
0x05 and 0x06. Writes to 0x00 are silently dropped.
Doing this properly means a v4l2-flash driver strobed from the sensor, which is how Windows drives it. Patches welcome.
On the X9-14, both the IR and RGB modules are physically inverted, and the ACPI
SSDB still reports degree 0. The bridge patch adds a DMI quirk so the
rotation property comes out as 180 and userspace can do the right thing. If
your machine is affected, add it to upside_down_sensor_dmi_ids — note that
Lenovo puts the useful name in DMI_PRODUCT_VERSION, not DMI_PRODUCT_NAME.
| Machine | Status |
|---|---|
| ThinkPad X9-14 Gen 1 (21QA/21QB) | tested |
Anything else with ACPI HM1092 behind an IPU6/IPU7 |
should work; the sensor config is not board-specific |
Intel's driver package ships tuning for three HM1092 modules — CJFLE26IR,
BBG803N3 and CJFOE83 — all sharing one register set, which is a good sign
the init sequence generalises. If it works on your machine, or doesn't, please
open an issue with dmesg | grep -i hm1092 and the output of
hm1092-capture --list.
The register sequences in src/hm1092_regs.h and the mode geometry were
recovered from hm1092.sys, Intel's Windows driver, shipped in Lenovo package
n4dim10w.exe. There is no public datasheet for this part. The method, and how
the table format was validated against a known-good driver, is written up in
docs/REVERSE-ENGINEERING.md.
Be aware of what that means: these tables are extracted from a proprietary binary, not independently derived. That is fine for running your own hardware, and it is the same position every "recovered from the vendor blob" sensor driver starts from, but it is a real obstacle to merging this upstream as-is. Anyone wanting to take it to linux-media should know that going in.
GPL-2.0-only, matching the kernel.