Published 21 Jul 2026 Fairphone 6 ultra-wide working in GNOME Snapshot. Background The Fairphone 6 is an interesting target for mainline Linux and postmarketOS for a few reasons: It has relatively modern hardware compared with most other supported postmarketOS devices. Fairphone invests in mainline Linux support, with Luca Weiss driving much of the development. Those efforts have already provided promising initial support for the hardware. Fairphone deliberately allows alternative operating systems. Fairphone aims to support its devices for a long time, so the phone could provide a stable base for future work. There are still two main blockers to making the device generally usable: onboard audio and camera support. I am working on a project that needs QR scanning, so I used that as an opportunity to see whether I could get at least one camera working. Disclaimer: I heavily relied on LLM assistance to make this work. Fairphone 6 cameras The Fairphone 6 has three cameras: Rear: Sony IMX896 Front: Samsung S5KKD1 Wide: OmniVision OV13B10 The Sony and Samsung sensors have no mainline Linux drivers, but the OmniVision OV13B10 has a mainline driver. The ultra-wide camera is sufficient for QR scanning. A phone camera is not a single device. On Qualcomm SoCs, the capture path is a chain: image sensor → CSI-2 D-PHY → CSIPHY → CSID → ISP (VFE/TFE) → memory (I2C) (MIPI lanes) (decode) (demux) (write engine) (DMA) There are also several supporting components, including a camera clock controller (camcc), the CCI (Qualcomm's dedicated camera I2C controller), power rails, and a VCM (voice-coil motor) for autofocus. The downstream vendor Android kernel drives all of this with Qualcomm's substantial, proprietary CAMX/cam-kernel stack. On mainline, the equivalent is the much smaller qcom-camss driver, with libcamera providing userspace integration and image processing. To get the OmniVision camera working, I had to teach qcom-camss about the SoC's specific blocks, describe the hardware in the device tree, enable the sensor driver, and configure libcamera to turn raw Bayer frames into images that applications could use. Initial exploration Before starting the implementation, I wanted to find out how much of the work was new and how much could be ported from hardware that mainline already supported. That would show whether the project was feasible without manufacturer support. The initial research was encouraging: The FP6's SoC is Qualcomm milos (SM7635). The downstream board is codenamed volcano, and postmarketOS already boots it with a mainline-based kernel fork. The camera hardware blocks are TFE665 (a thin-front-end ISP), CSID665, and CSIPHY v2.2.1. The camcc clock controller and CCI are already present in the kernel fork. The three cameras are: Main: Sony IMX896, with no mainline driver. Ultra-wide: OmniVision OV13B10, with a mainline driver that supports ACPI/x86 only. Front: Samsung S5KKD1, with no mainline driver. That made the target obvious: the OV13B10 ultra-wide. It is a 13 MP sensor with an existing mainline driver, and its wide field of view is suitable for QR scanning. When I compared the downstream register headers (cam_tfe665.h, cam_csiphy_2_2_1_hwreg.h, and cam_tfe_bus.c) with mainline qcom-camss, I found that TFE665 is essentially the same IP as TFE530, which mainline already supports. The register layouts are identical. Only the base offsets of the internal blocks differ, because the write bus lives at a different address. Similarly, the CSID665 RDI registers match the supported CSID, and CSIPHY v2.2.1 belongs to the same three-phase PHY family. This meant that I could port existing drivers instead of writing everything from scratch. Step 1: Port the capture subsystem into the kernel qcom-camss was not yet available on milos, so I had to connect the existing pieces. TFE665 ISP driver I added a new camss-vfe-665.c, modelled directly on the mainline TFE530 driver (camss-vfe-340.c). Because the register contents are identical, the driver logic is the same. The main difference is that milos places the ISP control block and write-bus block at different offsets. The bus register base moved from 0xa00 to 0x1800, and encapsulating that offset shift accounted for most of the work. CSID665 and CSIPHY v2.2.1 CSID665 reused the existing gen-2 CSID operations because its RDI register offsets were identical. CSIPHY v2.2.1 needed a new lane-configuration table and D-PHY tuning values for this revision. I transcribed the lane register sequence and the approximately 1.1 Gbps/lane ("500 Msps") data-rate and AFE settings from the downstream cam_csiphy_2_2_1_hwreg.h. The PHY's register window was at offset 0x1000. Resources, compatible, and device tree In camss.c, I described milos's capture complex: four CSIPHYs, three CSIDs, three TFEs, their clocks, interconnects, and power domains. I also registered a new qcom,milos-camss compatible. In the device tree, I added the camss@ac13000 node with its registers, IRQs, clocks, interconnects, IOMMUs, GDSC, and CSI input ports, along with the MCLK and reset pinctrl states. Missing register-bus clock I ran into a problem at this point. The driver bound, but reading the TFE hardware-version register returned 0x0, and reset timed out as if the block were not powered. The fix was a clock that was not obviously an ISP clock: CAM_CC_SOC_AHB_CLK. It gated the AHB register bus used by the whole camera complex, including the CCI. Without it, register accesses silently returned zero. I added that clock and the CAMNOC AXI clocks, then set the CAMNOC data-path clock rate. The ISP came alive with hw_version = 0x30000000. This suggests that, on Qualcomm hardware, a block that reads as zero may be missing a clock or power domain on its access path. Step 2: Bring up the OV13B10 sensor The mainline ov13b10 driver exists, but it is written for x86/ACPI laptops and only matches through ACPI. I made changes in two places. Making the driver usable on ARM and device tree I added an OpenFirmware match table (ovti,ov13b10) so the driver could probe from the device tree. I described the sensor in milos-fairphone-fp6.dts. It lives on the CCI I2C bus at address 0x36, needs MCLK1 at 19.2 MHz, a reset GPIO, and power rails. For this experimental bring-up, I simply forced the regulators on instead of fully sequencing them. 2 small details Lane numbering. The downstream device tree used data-lanes =, while mainline's convention is zero-indexed:. With the wrong numbering, CSIPHY programmed a lane mask with lane 0 missing, and the PHY never locked. Which CSIPHY? The FP6 wiring routed the ultra-wide camera to CSIPHY1. Getting this from the downstream device tree instead of guessing saved a lot of trial and error. At this point, the sensor probed successfully, the chip ID was read correctly over I2C, CSIPHY reported lane activity, and the ISP produced frames that were completely black. Step 3: The all-zero frames mystery This was the main bug. Frames arrived at the right rate and size, and buf_done fired, but every pixel was zero. That was also true when I enabled the sensor's internal colour-bar test pattern, which was generated inside the sensor and should have appeared regardless of the scene. The data path delivered frame timing, but not pixel data. The kernel log provided a clue: VFE0: Bad config violation. The ISP's write engine reported a consumer/config violation once per frame because the write master's configuration did not match the incoming data. When I dug into the downstream cam_tfe_bus.c, the difference became clear. The RDI write-master's packer format depends on the ISP bus width: TFE530 (qcm2290, the target of the mainline driver) has a 64-bit RDI bus, so it uses packer 0xa. TFE665 (milos) has a 128-bit RDI bus, so it needs packer 0x0. The mainline driver hard-coded the 64-bit value. On milos, that was wrong, and the write engine refused to write the pixels. Changing the register value from PLAIN64 (0xa) to 0x0 turned the all-zero frames into real images: the maximum pixel value was 255, the full colour-bar pattern appeared, and the violations stopped. That was the point at which the camera physically worked: OV13B10 → CSIPHY → CSID → TFE665 → DDR, delivering genuine Bayer frames. Step 4: Raw Bayer to libcamera Raw frames were enough for a QR script, but a usable camera needs libcamera, the userspace framework used by modern Linux camera applications. libcamera 0.7.2 already recognises the milos qcom-camss graph through its generic simple pipeline handler. Its software ISP debayered the raw frames to RGB, with GPU acceleration on the Adreno through EGL, at approximately 30 fps out of the box. cam and GStreamer's libcamerasrc produced clean images immediately. Auto-exposure was the part that did not work. Images were dark or overly bright, and the log reported Failed to create camera sensor helper for ov13b10. libcamera's software auto-exposure and gain loop needed a small per-sensor helper to map the sensor's gain register value to a real gain multiplier. libcamera includes helpers for several sensors, but not this one. The fix was small. The OV13B10's analogue gain is linear, with 0x80 = 1×, or gain = code / 128, which is the same behaviour as several other OmniVision sensors. I added a class to libcamera's sensor-helper database: class CameraSensorHelperOv13b10: public CameraSensorHelper { public: CameraSensorHelperOv13b10() { gain_ = AnalogueGainLinear{ 1, 0, 0, 128 }; } }; ("ov13b10", CameraSensorHelperOv13b10) The auto-exposure loop started converging immediately. I also added an entry to libcamera's sensor-properties database and backported the sensor driver's V4L2 crop and get_selection support, which libcamera uses for pixel-array geometry. After that, GStreamer and PipeWire received a properly exposed feed. Once I installed the PipeWire libcamera plugin, the camera appeared in desktop applications as a normal video source. The images are still somewhat overexposed in some situations, so the helper might need further work. Step 5: Focus, orientation, and resolution issues I still had a few issues to fine-tune. Autofocus The ultra-wide camera has a voice-coil motor (VCM) for focus. The downstream device tree suggests that it is an Awinic AW86017. After powering the autofocus rail, which is controlled by a GPIO-driven regulator, and probing the CCI I2C bus, I found a device at address 0x0c that spoke the de facto standard DW9714 10-bit DAC protocol. The mainline dw9714 driver drives it directly. A focus sweep confirmed that the lens moved physically and that sharpness peaked clearly at a middle setting. libcamera associated the lens with the sensor through a lens-focus device-tree link. Unfortunately, libcamera's software ISP has no autofocus algorithm. Rather than leave the lens parked at infinity and produce blurry close-ups, I set a sensible fixed focus tuned for arm's-length QR scanning and documents using a small udev rule. The setting could be adjusted as needed, or the focus could be controlled from software. Orientation The sensor is mounted at an angle relative to the phone's portrait orientation, so previews initially appeared sideways. Setting the device-tree rotation and orientation properties allowed libcamera-aware applications to display the feed upright. Binned modes The OV13B10 driver advertised several resolutions, including 2×2-binned half-resolution modes. On this CAMSS path, the binned modes produced garbled, sheared output. Their readout geometry did not match the declared frame size, while full-resolution and a couple of other modes were clean. Rather than ship a broken preview, I disabled the two broken modes so libcamera would select only working ones. The trade-off is that, because the software ISP crops instead of scaling, a 1080p preview is a centre crop of a larger mode. It is slightly zoomed in rather than showing the full ultra-wide field of view. The result is a live, upright, auto-exposed, in-focus preview in GNOME Camera / Snapshot, and QR scanning works. Sources The patches and related files needed to reproduce these results are available in this repository: github.com/nondescriptpointer/fairphone6-wide-camera-linux Limitations This is an experimental start, not production camera support. Only the ultra-wide (OV13B10) works. The main (IMX896) and front (S5KKD1) sensors have no mainline drivers. There is no autofocus. libcamera's software ISP has no contrast-AF loop, so focus is a fixed compromise position. Proper autofocus would require an AF algorithm in the software ISP or an app-side loop to drive the VCM. The preview field of view is cropped at common resolutions because the 2×2-binned sensor modes are broken on this path and the software ISP crops instead of scaling. Fixing the binned-mode register sequences, or adding a scaler, would restore the full ultra-wide field of view. Power sequencing is simplified. Regulators are forced on instead of being sequenced, and the VCM draws a little idle current while held at a fixed focus position. This is fine for experimentation, but needs proper power management for daily use. The ISP is software-only. All debayering, auto-exposure, and auto-white-balance processing happens in software, with GPU assistance. It works well for QR scanning and casual use, but does not provide the image quality of a tuned hardware ISP pipeline. I did not spend time optimising image quality. Further optimisation is likely needed to get better photos from the sensor. Next steps Fix the binned modes to restore the full field of view. Write proper power sequencing. Prepare the parts that are ready for upstreaming.
Fairphone 6 wide camera experimental Linux support
Article URL: Comments URL: Points: 9 # Comments: 0
Article URL: Comments URL: Points: 9 # Comments: 0
- Background The Fairphone 6 is an interesting target for mainline Linux and postmarketOS for a few reasons: It has relatively modern hardware compared with most other supported postmarketOS devices.
- I transcribed the lane register sequence and the approximately 1.1 Gbps/lane ("500 Msps") data-rate and AFE settings from the downstream cam_csiphy_2_2_1_hwreg.h.
- The ISP's write engine reported a consumer/config violation once per frame because the write master's configuration did not match the incoming data.
- I also added an entry to libcamera's sensor-properties database and backported the sensor driver's V4L2 crop and get_selection support, which libcamera uses for pixel-array geometry.
- Orientation The sensor is mounted at an angle relative to the phone's portrait orientation, so previews initially appeared sideways.
What people are saying
Hot takes
Loading takes…
Comments
Discussion · 0
Sign in to comment, like, and save articles.
Sign inLoading comments…

