The Fairphone 6 is an interesting target for mainline Linux and postmarketOS for a few reasons:
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.
The Fairphone 6 has three cameras:
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:
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.
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:
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.
qcom-camss was not yet available on milos, so I had to connect the existing pieces.
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.
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.
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.
The mainline ov13b10 driver exists, but it is written for x86/ACPI laptops and only matches through ACPI. I made changes in two places.
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.
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:
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.
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:
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.
I still had a few issues to fine-tune.
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.
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.
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.
The patches and related files needed to reproduce these results are available in this repository:
github.com/nondescriptpointer/fairphone6-wide-camera-linux
This is an experimental start, not production camera support.
Source: Hacker News — This article was automatically imported from the source. Read full article at original source →