Чужой сенсор на чужом SoC: что не работает, когда «общий API» общий не до конца. device tree.. device tree. embedded linux.. device tree. embedded linux. isp.. device tree. embedded linux. isp. linux kernel development.. device tree. embedded linux. isp. linux kernel development. mipi.. device tree. embedded linux. isp. linux kernel development. mipi. raspberry pi.. device tree. embedded linux. isp. linux kernel development. mipi. raspberry pi. RockChip.. device tree. embedded linux. isp. linux kernel development. mipi. raspberry pi. RockChip. v4l2.. device tree. embedded linux. isp. linux kernel development. mipi. raspberry pi. RockChip. v4l2. автофокус.. device tree. embedded linux. isp. linux kernel development. mipi. raspberry pi. RockChip. v4l2. автофокус. камера.. device tree. embedded linux. isp. linux kernel development. mipi. raspberry pi. RockChip. v4l2. автофокус. камера. Программирование микроконтроллеров.. device tree. embedded linux. isp. linux kernel development. mipi. raspberry pi. RockChip. v4l2. автофокус. камера. Программирование микроконтроллеров. Производство и разработка электроники.. device tree. embedded linux. isp. linux kernel development. mipi. raspberry pi. RockChip. v4l2. автофокус. камера. Программирование микроконтроллеров. Производство и разработка электроники. Системное программирование.

1. Вступление

Задача звучала просто, и мы недооценили её примерно вдвое: подключить готовый модуль камеры Raspberry Pi к камерному SoC от Rockchip. Драйвера для этого сенсора нет ни в вендорском дереве Rockchip, ни в мейнлайне. Он живёт только в дереве Raspberry Pi, под другое ядро и другой видеотракт.

Казалось бы, не проблема. Драйвер сенсора в Linux к SoC формально не привязан. Он общается с subdev-API ядра: вот формат, вот разрешение, вот экспозиция. А кто на другом конце MIPI, ему безразлично, связку делает device tree. Мейнлайн на это и рассчитан, тот же imx219.c спокойно работает и под Broadcom, и под другими SoC. То есть «взять готовый драйвер и собрать под свою платформу» это штатный сценарий, а не авантюра.

Но вот в чём проблема: мы не на мейнлайне, и RPi-драйвер тоже не на мейнлайне. Переносить пришлось не из мейнлайна в вендорское дерево, а из одного вендорского дерева в другое, где общего API ровно столько, сколько сохранили обе стороны. А сохранили не всё. Ниже первый этап одного из наших проектов: носимая камера. В ITSupportMe мы разрабатываем кастомные камеры и встроенное ПО к ним, и задачи вида «сенсор есть, платформа есть, драйвера между ними нет» встречаются в такой работе регулярно.

Этап целиком прошёл на отладочной плате. По пунктам:

  • почему драйвер IMX708 из ядра Raspberry Pi нельзя просто пересобрать под дерево Rockchip, даже когда API формально общий;

  • чем subdev-API ядра 5.10 отличается от 6.6 сильнее, чем ожидаешь;

  • почему STREAMON падал из-за одного слова, которое в двух местах значит разное;

  • как добавить второй пад под embedded-данные, чтобы получить PDAF, и откуда взялась высота в четыре строки.

Четыре места, где новый сенсор нужно прописать, чтобы он появился в системе, и почему про четвёртое не написано ни в одном руководстве. Если коротко: самым дорогим по времени оказался не сам драйвер, а расхождения, про которые не написано ни в одной документации, ни у Rockchip, ни у Raspberry Pi.

2. Задача: носимая камера, и что из неё следует

Требование было одно, зато жёсткое: максимально компактное устройство, которое носят на теле. Отсюда и остальные.

Single Chip Solution. Не набор микросхем на плате, а система на кристалле, с оперативной памятью в том же корпусе. Всё, что можно не выносить на плату отдельным компонентом, выносить не надо.

Готовый модуль камеры, в составе которого сенсор, объектив и автофокус на подвесе. Распаять сенсор и вкрутить M12 или C-mount — слишком крупно. Автофокус здесь не роскошь: камера на теле, дистанция до объекта меняется постоянно, и подкрутить фокус руками не наш случай.

Низкое энергопотребление — для носимого устройства это ограничение, а не пожелание.

Под эти требования выбрали Rockchip RV1106. Это специализированный камерный SoC: на кристалле уже есть ISP, аппаратный кодер H.264/H.265, входы MIPI CSI-2 и интегрированная память — то есть большая часть камерного тракта не требует внешних компонентов. Плюс NPU на борту, что оставляет задел на обработку прямо в устройстве.

Программную платформу мы вместе с чипом и получили: Rockchip поставляет для него свою сборку Linux — ядро с драйверами видеотракта и стек обработки изображения поверх V4L2. Дальше вся работа идёт внутри этой сборки.

Модуль камеры подобрали по требованиям к габаритам и автофокусу — аналогичный Raspberry Pi Camera Module 3, с сенсором Sony IMX708. И решили попробовать подключить его к этой сборке.

Что драйвера для него в вендорском дереве нет, мы знали заранее. В этом и состоял вызов этапа: сможем ли мы написать драйвер и заставить сенсор работать на чужой для него платформе. Ответ на этот вопрос и определял судьбу проекта — получится, значит проекту быть.

Границы этапа

Этап должен был подтвердить: сенсор стабильно стримит на этом SoC, приводом автофокуса можно управлять, ISP после калибровки даёт приемлемую картинку, потребление укладывается в бюджет. В этап сознательно не входили HDR, своя плата и корпус.

Полное разрешение сенсора — 4608×2592, почти 12 Мп — в этот список не попало по другой причине: на этой платформе оно недостижимо. Предел зашит в вендорском драйвере ISP:

/* drivers/media/platform/rockchip/isp/rkisp.h */
#define CIF_ISP_INPUT_W_MAX_V32		3072
#define CIF_ISP_INPUT_H_MAX_V32		1728

В RV1106 стоит ISP версии 3.2, то есть на входе не больше 3072×1728 — около 5.3 Мп. Полный кадр IMX708 не проходит по обеим осям, а режим склейки двух проходов, который на старших чипах поднимает этот предел, для RV1106 выключен (unite = false).

Так что 2304×1296 у нас не компромисс ради сроков. Формально ISP берёт до 3072×1728, но режима такой геометрии у сенсора нет: в открытом драйвере их всего три — полный кадр, наш биннинг 2304×1296 и 1536×864 (последний кропает с матрицы как раз 3072×1728, но наружу отдаёт вдвое меньше). Угадывать такую таблицу регистров без даташита мы не взялись, так что 2304×1296 — максимум из того, что реально можно подать на этот ISP. Сенсор оказался мощнее тракта, к которому мы его подключили.

3. Стенд вместо изделия

Ключевое решение этапа — развязать разработку ПО и разработку железа. Если ждать первую ревизию своей платы, весь софт простаивает. Поэтому взяли отладочную плату Luckfox Pico Ultra W на RV1106 и саму камеру Raspberry Pi V3.

Разъёмы у них разные: у Raspberry Pi 15-контактный MIPI-шлейф, у отладки — свой, с другим шагом и большим числом контактов. Но электрически это то же самое: MIPI CSI-2 плюс I2C. Поэтому сделали простой pin-to-pin переходник и развели в нём только то, что нужно: CLK, D0, D1 — сенсор работает в режиме 2 lane, так что из пяти дифференциальных пар понадобились три, — плюс шину I2C и питание.

4. Драйвер: как подружить чужой сенсор с Rockchip

Драйвер сенсора не участвует в передаче кадров: они едут по MIPI в приёмник и ISP мимо него. Его работа — отвечать на вопросы: какой формат, какое разрешение, включись, выключись, выставь такую экспозицию и усиление. То есть драйвер сенсора — это анкета. Вся сложность порта была в том, что у Rockchip анкета своя.

Почему нельзя просто взять драйвер Raspberry Pi

Здравая первая мысль: такой драйвер уже написан, и переносить его между платформами как раз и предполагается. Драйвер жёстко привязан к своему сенсору — регистры, режимы и тайминги у каждого свои, — но не к SoC. Он общается только с subdev-API ядра, а кто на другом конце MIPI, для него деталь конфигурации: связку задаёт device tree. Поэтому в мейнлайне на каждый сенсор ровно один драйвер, и работает он с любым приёмником: тот же imx219.c живёт и под unicam на Broadcom, и под rkisp1 на других SoC.

Но у нас этот сценарий не сработал, и причина в том, что мы не на мейнлайне — и RPi-драйвер тоже. Поддержки видеотракта RV1106 в апстриме нет, весь ISP/CIF-стек существует только в вендорском дереве Rockchip, так что выбора не было: BSP-ядро 5.10.160. А imx708.c живёт только в дереве Raspberry Pi — патчи в linux-media отправили в январе 2023-го, но апстрим до сих пор ждёт поддержки metadata/embedded-data стрима, без которой драйвер не примут с PDAF и HDR. То есть переносить пришлось не «мейнлайн → вендор», а из одного вендорского дерева в другое, где общего API ровно столько, сколько сохранили обе стороны.

А сохранили не всё. Приёмник rkcif и ISP требуют от сенсора нестандартный набор ioctl RKMODULE_* и свои заголовки: через них спрашиваются метаданные модуля — как он развёрнут на плате, какой объектив, поддерживает ли HDR. В RPi-драйвере этого нет и быть не может, а без них rkaiq с сенсором просто не свяжется.

Версии ядра тоже не совпали: самая старая ветка Raspberry Pi с этим драйвером — rpi-5.15.y, под наше 5.10 его не существует в природе. Таблицы регистров мы брали из rpi-6.6.y, а это разрыв в три минорных версии по subdev-API.

Так и получилось, что источников было два: RPi-драйвер дал всё про сам сенсор (регистры, порядок инициализации, форматы), а imx327.c из дерева Rockchip — шаблон обвязки. Итоговый файл — гибрид: начинка от Raspberry Pi, каркас от Rockchip.

Каркас

Таблицы регистров — это пары «адрес, значение». Адрес 16-битный big-endian (SMIA-подобная схема, как у большинства сенсоров Sony), а значение всегда однобайтовое, поэтому многобайтовые регистры лежат соседними записями: {0x0136, 0x18}, {0x0137, 0x00} — это внешняя частота 24 МГц. Полноценная 16-битная запись в драйвере есть, но в таблицах не участвует — ею пишутся рантайм-значения из V4L2-контролов: экспозиция (0x0202), усиление (0x0204), VTS (0x0340).

Таблиц две, и разделение не косметическое: у регистров разное время жизни. В общей (48 записей) — то, что описывает сам чип и интерфейс: внешняя частота, разрядность, число MIPI-линий и большой блок ничем не объяснённых значений вида 0x33F0, 0xF001, 0xBCF1. В мод-специфичной (93 записи) — геометрия и тайминги: кроп, биннинг, выходной размер, HTS/VTS, частота линка.

Здесь важная оговорка: даташита IMX708 у нас не было и быть не могло — Sony такие документы не публикует, они выдаются производителям модулей по NDA. В открытом доступе есть стандарт SMIA/MIPI CCS (оттуда «типовые» регистры: старт стрима 0x0100, ориентация 0x0101, экспозиция, усиление, VTS), открытый драйвер Raspberry Pi и энтузиастские аннотации регистров, собранные реверс-инжинирингом. Всё специфичное для сенсора — таблицы режимов, биннинг, настройка линка, PDAF — не описано нигде. Насколько это существенно, видно по нашим же таблицам: из 141 записи 62 адресуются в стандартное пространство CCS (ниже 0x1000), а 79 — в приватные страницы Sony вида 0x3Cxx или 0xF0xx. Больше половины инициализации сенсора выполняется значениями, смысл которых публично неизвестен. Так что RPi-драйвер был для нас не «полезным примером», а рабочей заменой документации.

Режим описали один — 2304×1296, 10 бит, 30 кадров в секунду. Это 2×2-биннинг, и для IMX708 он же и есть естественный формат: матрица у сенсора quad-Bayer, четыре соседних пикселя одного цвета, поэтому биннинг 2×2 даёт обычный байеровский узор напрямую. Полное разрешение, наоборот, требует ремозаики внутри сенсора — в RPi-драйвере у этого режима стоит флаг remosaic = true и отдельные регистры коррекции QBC.

Про 30 fps стоит сказать отдельно. Сам режим умеет до 56 fps, если сжать вертикальное гашение до минимума, который разрешает RPi-драйвер, — но у нас диапазон V4L2_CID_VBLANK начинается с дефолтного значения, так что гашение может только расти. 30 fps, соответственно, жёсткий потолок. Поднимать частоту незачем: кадры снимаем раз в несколько секунд, повода трогать гашение просто не было.

Границы этого диапазона, кстати, ограничивают не только нас. Частоту кадров крутит и вендорский стек — rkaiq пишет ровно в этот контрол, — а значения за объявленными границами V4L2 молча подрежет по краю. То есть тем, что драйвер объявил в одной строке, определяется и то, чего вендорский AE не сможет добиться никогда, причём без единой ошибки в логах.

Чего мы не ожидали: API ядра 5.10 отличается сильнее, чем кажется

Казалось бы, портирование с 6.6 на 5.10 — это «поправить сигнатуры». На деле subdev-API за эти версии переехал заметно:

  • Тип состояния pad-операций. Upstream 6.x: struct v4l2_subdev_state *. Наш 5.10: struct v4l2_subdev_pad_config *cfg.

  • Получение try-формата. Upstream 6.x: v4l2_subdev_get_try_format(sd, sd_state, pad). Наш 5.10: v4l2_subdev_get_try_format(sd, cfg, pad).

  • Регистрация async subdev. Upstream 6.x: v4l2_async_register_subdev_sensor(). Наш 5.10: v4l2_async_register_subdev_sensor_common().

  • Сигнатура probe. Upstream 6.x: probe(struct i2c_client *). Наш 5.10: probe(struct i2c_client *, const struct i2c_device_id *).

  • Опциональные регуляторы. Upstream 6.x: devm_regulator_bulk_get_optional(). Наш 5.10: не существует.

Имя v4l2_subdev_get_try_format в 6.6 то же, что и в 5.10 — разница в том, что передаётся вторым аргументом.

Последняя строка — отдельная история. Регуляторы питания сенсора мы просто выкинули из драйвера: на этой плате рельсы фиксированные и управляются аппаратно, так что бороться за отсутствующую функцию смысла не было. Из управления питанием остались xvclk, GPIO reset и pinctrl.

Каждое отличие по отдельности — мелочь на пять минут. Вместе они означают, что каждую функцию, которая касается pad-операций, приходится перечитывать построчно и проверять по актуальному дереву, а не по памяти.

Один таргет, два смысла: почему STREAMON падал

Драйвер собрался, сенсор отозвался по I2C и вернул свой chip ID. А STREAMON падал: rkcif ругался «crop size is bigger than input», хотя формат и разрешение драйвер отдавал верные.

Дело оказалось в get_selection — точнее, в том, что у одного и того же таргета в двух деревьях разный смысл. В мейнлайне V4L2_SEL_TGT_CROP_BOUNDS — это границы аналогового кропа в координатах сенсора, для IMX708 честные 4608×2592, а биннинг выражается разницей между кропом и форматом. В дереве Rockchip конвенция другая: тот же таргет задаёт окно внутри передаваемого кадра и обязан в него влезать. Это видно по соседнему imx327.c: он передаёт по MIPI 1948×1110, а полезным окном объявляет 1920×1080. Проверка сходится, 1920 ≤ 1948. А мы принесли мейнлайновую трактовку, и получилось 4608 против передаваемых 2304 — отказ был справедливым.

Так что метод мы переписали в конвенции того дерева, в котором работаем:

	/*
	 * Rockchip convention: bounds live inside the transmitted frame,
	 * not in sensor array coordinates. Never report the 4608x2592
	 * pixel array here - rkcif rejects STREAMON.
	 */
	if (sel->target == V4L2_SEL_TGT_CROP_BOUNDS) {
		sel->r.left   = 0;
		sel->r.top    = 0;
		sel->r.width  = imx708->cur_mode->width;
		sel->r.height = imx708->cur_mode->height;
		return 0;
	}

У биннингованного режима полезного подокна внутри кадра нет — сенсор передаёт кадр целиком, поэтому границы совпадают с ним самим.

Урок здесь не про конкретный метод, а про то, что при переносе между деревьями совпадение имён и констант не гарантирует совпадения смысла. Причём документация тут не помогает, а мешает: в дереве лежит штатный мейнлайновый Documentation/userspace-api/media/v4l/selection-api-configuration.rst, где CROP_BOUNDS описан именно как координаты сенсора — ровно та трактовка, которая приводит к отказу. Вендорского отступления от неё не описано нигде, так что практическая спецификация для такого дерева одна: драйвер, который в нём уже работает.

Что пришлось добавить от себя: второй канал под данные автофокуса

До сих пор речь шла о переносе существующего. Но одну вещь мы дописали сами, потому что её не было ни в одном из источников.

IMX708 передаёт по MIPI не только кадр. Параллельно, отдельным типом данных (0x12, embedded data), идут служебные строки, и в них — данные фазового автофокуса. У Raspberry Pi этот поток разбирает libcamera в юзерспейсе, и драйверу про него знать незачем. У Rockchip механизм другой: rkcif умеет выложить embedded-поток в отдельное видеоустройство, но узнать о его существовании ему неоткуда — он не догадывается, что в потоке есть второй тип данных, и не знает его геометрии. Рассказать может только сенсорный драйвер, одним ответом на RKMODULE_GET_CHANNEL_INFO:

if (imx708->ebd_id < PAD_MAX &&
		ch_info->index == imx708->ebd_id) {
	ch_info->vc        = V4L2_MBUS_CSI2_CHANNEL_0;
	ch_info->width     = imx708->cur_mode->width * 10 / 8;
	ch_info->height    = 4; /* 2 SMIA + 1 PDAF + 1 AE-hist */
	ch_info->bus_fmt   = MEDIA_BUS_FMT_EBD_1X8;
	ch_info->data_type = 0x12; /* MIPI embedded data */
	ch_info->data_bit  = 8;
}

Ни rockchip,ebd-id, ни сам RKMODULE_GET_CHANNEL_INFO в биндингах и Documentation/ не описаны — что именно приёмник хочет услышать, выясняется чтением его кода. Разберём поля, потому что каждое означает решение.

index сравнивается с ebd_id из device tree (rockchip,ebd-id = <1>). Если свойства в DTS нет, ebd_id получает PAD_MAX — индекс, который никогда ни с чем не совпадёт, то есть захват embedded-данных выключается строкой в DTS, а не пересборкой драйвера.

vc — тот же виртуальный канал, что у кадра: IMX708 различает потоки типом данных (0x12 против 0x2b у RAW10), отдельный канал заводить не нужно.

width — не ширина кадра, а его размер в байтах: 2304 * 10 / 8 = 2880. Embedded data едет потоком 8-битных байт, поэтому упакованный RAW10 нужно пересчитать в байты, иначе приёмник обрежет строку.

height = 4 — единственное поле, которое нельзя было вывести из кода. В RPi-драйвере буфер метаданных описан как 5 * 5760 байт — пять строк по длине строки полной матрицы (4608 пикселей в упакованном RAW10), и раскладка в комментарии рядом: две строки SMIA-регистров, строка PDAF, две строки AE-hist. Мы поставили пять и получили csi_size_err на каждом кадре — данные не доходили вообще. Пришлось снять дамп живого потока (v4l2-ctl умеет это сам, свой код не нужен) и разобрать его руками. Сходится на четырёх: две строки регистров, PDAF и одна AE-hist. В каждой строке 2880 байт, а занято в них от нескольких сотен до тысячи — остальное набивка, поэтому дамп на первый взгляд выглядит подозрительно пустым. Пятой строки в нашем режиме мы не увидели.

Четвёртую строку при этом выбросить нельзя — она приезжает всегда, хотя содержимого в ней нет: байты одинаковы от кадра к кадру и не меняются между тёмной и светлой сценой. Это сходится с комментарием в RPi-драйвере, где сказано, что гистограммы валидны только в HDR-режиме, а в обычном пусты. То есть строка есть, места требует, а данных не несёт.

Отдельно стоит сказать, чего мы не делали: второй pad в медиаграфе не регистрируется, media_entity_pads_init по-прежнему объявляет один. Индекс из ebd-id работает как номер пада только в запросах формата и описания канала — rkcif спрашивает драйвер напрямую, а не ходит по графу. Полноценный второй pad понадобился бы для маршрутизации внутри графа, а нам нужно ровно одно: чтобы приёмник знал геометрию второго потока.

Четыре места, где новый сенсор нужно прописать

Три — в дереве исходников, четвёртое уже на плате. Общее у всех одно: при пропуске ни одно не даёт внятной диагностики.

Первые два рутинные: объявить опцию VIDEO_IMX708 в drivers/media/i2c/Kconfig и связать её с объектником в соседнем Makefile. Третье интереснее — device tree. Вот ключевая часть ноды:

imx708: imx708@1a {
	compatible = "sony,imx708";
	reg = <0x1a>;
	clocks = <&cru MCLK_REF_MIPI0>;
	clock-names = "xvclk";
	reset-gpios = <&gpio3 RK_PC5 GPIO_ACTIVE_HIGH>;
	rockchip,camera-module-name = "rpi-camv3";
	rockchip,camera-module-lens-name = "default";
	rockchip,ebd-id = <1>;
	lens-focus = <&dw9807>;
	port {
		imx708_out: endpoint {
			remote-endpoint = <&csi_dphy_input2>;
			data-lanes = <1 2>;
			link-frequencies = /bits/ 64 <450000000>;
		};
	};
};

clocks и reset-gpios — тот самый xvclk и XCLR вместо отсутствующих регуляторов. У XCLR сброс активен низким уровнем, но в DTS стоит GPIO_ACTIVE_HIGH: так принято в вендорском дереве — полярность не описывается в DTS, драйвер сам трактует логическую единицу как «вывести из сброса». ebd-id включает второй канал, data-lanes и link-frequencies описывают режим 2 lane × 450 МГц, lens-focus связывает привод фокуса с камерой.

Отдельно стоит сказать про camera-module-name и lens-name. Это не подписи для человека, а часть контракта с ISP-стеком: из них rkaiq собирает имя своего файла настроек — imx708_rpi-camv3_default.json. Библиотеке передают каталог с профилями, а не путь к файлу, и нужный она выбирает сама по этим строкам, так что совпадать они обязаны.

Чего в ноде не видно, но что ломает всё: на этой же шине в DTS описаны другие сенсоры, и они должны быть явно status = "disabled". Одновременно на MIPI может работать только один — если оставить два «okay», async-notifier попытается собрать граф с обоими и не соберёт ни один.

И четвёртое — на плате. В загрузочном скрипте модулей imx708.ko должен грузиться до video_rkcif.ko. Формально это не «прописать сенсор»: все предыдущие правки на месте, драйвер собран и корректен. Но если сенсор появляется позже приёмника, async-notifier успевает завершиться без него — граф собран, камеры в нём нет, ошибок в логе никаких. Просто нет устройства, и искать причину в драйвере можно долго.

Что получилось

IMX708 detected @ 0x1a (chip_id=0x0708)

В медиаграфе появилась сущность m00_b_imx708 4-001a, DPHY отрапортовал data_rate_mbps 900. Итог: 2304×1296, SRGGB10, до 30 fps. Две линии по 450 МГц, а поскольку D-PHY передаёт по двум фронтам, это 900 Мбит/с на линию и 1.8 Гбит/с суммарно.

Полезная нагрузка при этом примерно вдвое меньше канала: 2304 × 1296 × 10 бит × 30 fps ≈ 0.9 Гбит/с. Разница — не потери и не запас «на всякий случай», а вертикальное гашение: в кадре 2494 строки, активных из них 1296, и по MIPI уезжают только активные. Отношение 2494/1296 ≈ 1.9 — это и есть вся разница. Внутри активной строки канал почти забит: 2880 байт полезных данных — это 23 040 бит, при 1.8 Гбит/с они уходят за ~12.8 мкс из 13.36 мкс, отведённых на строку.

Плюс второе устройство с embedded-потоком. Для этапа это и был главный результат: связка «этот SoC + этот модуль» подтверждена на живом железе — дальше можно проектировать плату, зная, что сенсор поднимется.

5. Результат: обычное V4L2-устройство

Ради этого всё и делалось: камера не должна требовать особого кода под себя. Любой инструмент, который умеет говорить по V4L2, должен взять с неё кадр — без единой строчки, которая знает слово «IMX708».

Проверять надо не «модуль загрузился без ошибок», а снятый кадр. Subdev — это только анкета; кадры отдаёт /dev/video0, узел приёмника. Пока драйвер отвечал на ioctl’ы неправильно, граф не собирался, и в логе при этом было пусто. Самый примитивный тест — снять сырой Байер средствами голого V4L2:

v4l2-ctl -d /dev/video0 --set-fmt-video=width=2304,height=1296,pixelformat=RG10 
  --stream-mmap --stream-count=1 --stream-to=/userdata/frame.raw

Работает.

Более показательная проверка — чужой потребитель. Фирменный ISP-стек Rockchip написан не под наш сенсор, он универсальный для любого V4L2-совместимого источника. Он подхватил IMX708 и не упал: на выходе JPEG с автоэкспозицией и дебайеризацией, плюс работающий RTSP-стрим. Полный путь — imx708.crkcif → ISP → rkaiq крутит экспозицию и усиление через тот же subdev-API → кодер. Точка стыка ровно одна — наш imx708.c, и рядом с ним IQ-профиль, тоже привязанный к конкретному сенсору. Дальше по тракту про Sony не знает никто: rkcif, ISP и кодер видят абстрактный V4L2-источник. Ради этой развязки V4L2 и существует, и цена ей — один файл драйвера.

Обратная сторона в том, что развязка работает, только пока драйвер отвечает правду. В pixel_rate мы записали 180 МГц, посчитав их из скорости линка, — а это битовый клок MIPI, тогда как hts_def/vts_def считаются в пиксельных тактах; настоящее значение 585.6 МГц. Заодно в таблице режима стояли 56 fps вместо 30. Не сломалось ничего: кадры шли, ISP работал, ни ошибки, ни варнинга нигде. Но время строки из этих чисел выходило 7.16 мкс вместо 13.36, и потолок автоэкспозиции, заданный в IQ-файле как 10 мс, физически был 18.7 мс — 1/53 секунды вместо 1/100. Нашли только прямым замером: посчитали кадры за фиксированное время.

6. Калибровка цвета

Сырой Байер — ещё не картинка. Сенсор отдаёт числа, и превратить их в правильные цвета должен ISP: вычесть чёрный уровень, домножить каналы на коэффициенты баланса белого, применить цветокоррекционную матрицу. Все эти коэффициенты зависят от конкретной связки сенсора и объектива, и берутся они из файла настроек — у Rockchip это JSON, который движок rkaiq подхватывает по имени, собранному из свойств device tree.

Штатный путь — включить автоматический баланс белого и не думать. У нас он не сработал: картинка уезжала в зелень, и никакие правки коэффициентов R и B это не лечили. Причина в том, что профиль мы начинали с файла для другого сенсора, а цветокоррекционные матрицы в нём посчитаны под его спектральные характеристики — автоматике просто нечем было прийти в правильную точку.

Поэтому баланс белого перевели в ручной режим (CALIB_WB_MODE_MANUAL) и откалибровали под нашу связку: снимок эталонной поверхности при известном освещении, расчёт коэффициентов по каналам, проверка на реальных кадрах. Итоговые множители для комнатного LED-освещения:

"mwbGain": [ 2.52, 1.0, 1.0, 1.721 ]

Четыре числа — усиления каналов R, Gr, Gb, B. Зелёные оставлены за единицу как опорные, красный поднят в 2.52 раза, синий — в 1.72: столько не хватает этим каналам при этом свете. Рядом в файле — чёрный уровень 256 (сенсор 10-битный, а ISP работает в 12-битном пространстве, отсюда сдвиг) и потолок автоэкспозиции 1/100 с, чтобы на движении не было смаза.

7. Автофокус

Привод объектива — VCM DW9807 на той же шине I2C, адрес 0x0c. Управление примитивное: включить, записать положение в два регистра.

i2cset -f -y 4 0x0c 0x02 0x00           # выход из power-down
i2cset -f -y 4 0x0c 0x03 $(( POS >> 8 ))  # старшие биты
i2cset -f -y 4 0x0c 0x04 $(( POS & 0xFF )) # младшие

Диапазон 0–1023, где 0 — бесконечность, 1023 — макро; рабочая часть 100–900, метровая дистанция приходится примерно на 499.

Эти три команды — инструмент bring-up: так мы убедились, что привод жив и как он отзывается. В DTS он привязан к сенсору свойством lens-focus, и ядро поднимает его отдельным subdev’ом типа Lens — для AIQ-стека это признак того, что фокусировкой есть чем управлять.

Роли в автофокусе при этом разделены так: фазовые данные разбирает наш код, метрику резкости считает ISP, а позицию привода выставляет наш же контур. То есть «куда двигать линзу» — вопрос простой. Сложный вопрос — «куда именно», и на него отвечают два независимых источника данных.

Фазовый автофокус (PDAF) приходит от сенсора — тот самый embedded-поток, ради которого мы заводили второй канал в драйвере. Это сетка 12×16, в каждой зоне пара «достоверность, фаза». Фаза в формате Q4: делим на 16 и получаем расфокус в пикселях, причём со знаком — сразу известно, в какую сторону двигать. Это огромное преимущество перед контрастным методом, которому направление приходится угадывать.

Контрастный автофокус считает сам ISP Rockchip: он выдаёт метрику резкости по зонам, и задача сводится к поиску максимума перебором положений линзы. Работает по любой сцене, но медленнее и требует движения «на пробу». Эта часть включается одной строкой в том же IQ-файле, что и баланс белого (af_v31.TuningPara.contrast_af.enable = 1) — без неё аппаратный блок метрику не считает, и это стоило нам отдельного расследования.

Для корректной работы автофокуса используются оба источника, и у фазы в этой паре своё слепое место: она требует текстуры. На однородной поверхности достоверность не просто падает — данные исчезают целиком. Мы проверяли дампами живого потока: на ровной стене нулевую достоверность отдают все 192 зоны сетки, порог не проходит ни одна, и решать по фазе становится нечем. Причём дело именно в текстуре, а не в освещении: сцена при этом может быть нормально проэкспонирована.

Поэтому алгоритм комбинированный, с PDAF как основным контуром и контрастом как резервным — ровно так же это устроено в libcamera Raspberry Pi, откуда мы подход и портировали, уложив его в один поток. В норме система живёт в замкнутом контуре по фазе: получили фазу — сдвинули линзу пропорционально — проверили. Когда фазовые данные пропадают, включается контрастный поиск: грубый проход по диапазону, при необходимости второй, затем точный вблизи найденного максимума, стабилизация — и возврат в фазовый контур.

Наружу сервис отдаёт не только положение линзы, но и вердикт: движется, остановилась, сфокусирована. Это то, что нужно прикладной логике съёмки: в режиме таймлапса кадр имеет смысл делать не по таймеру, а когда линза встала и картинка резкая.

Практический вывод, который стоил нам времени: наличие фазовых данных и попадание их в окно автофокуса — разные вещи. Окно AF по умолчанию смотрит в центр кадра, и если там ровная стена, автофокус честно сообщает нулевую достоверность, хотя данные в кадре есть — просто у края. Диагностировать это по логам сервиса невозможно: нужно смотреть на сами зоны PDAF, зона за зоной.

8. Итог

Первый этап закончен, и главный вопрос этапа получил ответ: драйвер написан, связка «RV1106 + модуль камеры Raspberry Pi V3» работает на стенде. Сенсор стримит 2304×1296, автофокус сходится по комбинированному алгоритму, цвета откалиброваны. Проекту быть.

Дальше — своя плата: перенос device tree на кастомный дизайн, энергобюджет под конкретный аккумулятор, корпус. Из функций отложен HDR: сенсор его умеет, и регистры для него есть в открытом драйвере — отдельный режим 2304×1296, три экспозиции с фиксированным отношением 4, сведение внутри сенсора, наружу один поток. Транспорт трогать не пришлось бы, а вот калибровать этот режим надо заново, и AIQ-стек к такой схеме HDR ещё предстоит уговорить. А полное разрешение сенсора так и останется невостребованным: ISP этого чипа его не примет.


Автор: SashaTur

Источник