- BrainTools - https://www.braintools.ru -
Задача звучала просто, и мы недооценили её примерно вдвое: подключить готовый модуль камеры 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.
Требование было одно, зато жёсткое: максимально компактное устройство, которое носят на теле. Отсюда и остальные.
Single Chip Solution. Не набор микросхем на плате, а система на кристалле, с оперативной памятью [1] в том же корпусе. Всё, что можно не выносить на плату отдельным компонентом, выносить не надо.
Готовый модуль камеры, в составе которого сенсор, объектив и автофокус на подвесе. Распаять сенсор и вкрутить 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. Сенсор оказался мощнее тракта, к которому мы его подключили.
Ключевое решение этапа — развязать разработку ПО и разработку железа. Если ждать первую ревизию своей платы, весь софт простаивает. Поэтому взяли отладочную плату 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 и питание.
Драйвер сенсора не участвует в передаче кадров: они едут по MIPI в приёмник и ISP мимо него. Его работа — отвечать на вопросы: какой формат, какое разрешение, включись, выключись, выставь такую экспозицию и усиление. То есть драйвер сенсора — это анкета. Вся сложность порта была в том, что у Rockchip анкета своя.
Здравая первая мысль: такой драйвер уже написан, и переносить его между платформами как раз и предполагается. Драйвер жёстко привязан к своему сенсору — регистры, режимы и тайминги у каждого свои, — но не к 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 [2] отправили в январе 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 не сможет добиться никогда, причём без единой ошибки [3] в логах.
Казалось бы, портирование с 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-операций, приходится перечитывать построчно и проверять по актуальному дереву, а не по памяти.
Драйвер собрался, сенсор отозвался по 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 + этот модуль» подтверждена на живом железе — дальше можно проектировать плату, зная, что сенсор поднимется.
Ради этого всё и делалось: камера не должна требовать особого кода под себя. Любой инструмент, который умеет говорить по 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.c → rkcif → 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. Нашли только прямым замером: посчитали кадры за фиксированное время.
Сырой Байер — ещё не картинка. Сенсор отдаёт числа, и превратить их в правильные цвета должен 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 с, чтобы на движении не было смаза.
Привод объектива — 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, откуда мы подход и портировали, уложив его в один поток. В норме система живёт в замкнутом контуре по фазе: получили фазу — сдвинули линзу пропорционально — проверили. Когда фазовые данные пропадают, включается контрастный поиск: грубый проход по диапазону, при необходимости второй, затем точный вблизи найденного максимума, стабилизация — и возврат в фазовый контур.
Наружу сервис отдаёт не только положение линзы, но и вердикт: движется, остановилась, сфокусирована. Это то, что нужно прикладной логике [4] съёмки: в режиме таймлапса кадр имеет смысл делать не по таймеру, а когда линза встала и картинка резкая.
Практический вывод, который стоил нам времени: наличие фазовых данных и попадание их в окно автофокуса — разные вещи. Окно AF по умолчанию смотрит в центр кадра, и если там ровная стена, автофокус честно сообщает нулевую достоверность, хотя данные в кадре есть — просто у края. Диагностировать это по логам сервиса невозможно: нужно смотреть на сами зоны PDAF, зона за зоной.
Первый этап закончен, и главный вопрос этапа получил ответ: драйвер написан, связка «RV1106 + модуль камеры Raspberry Pi V3» работает на стенде. Сенсор стримит 2304×1296, автофокус сходится по комбинированному алгоритму, цвета откалиброваны. Проекту быть.
Дальше — своя плата: перенос device tree на кастомный дизайн, энергобюджет под конкретный аккумулятор, корпус. Из функций отложен HDR: сенсор его умеет, и регистры для него есть в открытом драйвере — отдельный режим 2304×1296, три экспозиции с фиксированным отношением 4, сведение внутри сенсора, наружу один поток. Транспорт трогать не пришлось бы, а вот калибровать этот режим надо заново, и AIQ-стек к такой схеме HDR ещё предстоит уговорить. А полное разрешение сенсора так и останется невостребованным: ISP этого чипа его не примет.
Автор: SashaTur
Источник [5]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/35297
URLs in this post:
[1] памятью: http://www.braintools.ru/article/4140
[2] патчи в linux-media: https://www.spinics.net/lists/linux-media/msg226138.html
[3] ошибки: http://www.braintools.ru/article/4192
[4] логике: http://www.braintools.ru/article/7640
[5] Источник: https://habr.com/ru/articles/1080626/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1080626
Нажмите здесь для печати.