Pull to refresh

Захват аналогового видеосигнала при помощи STM32F4-DISCOVERY

Reading time 14 min
Views 170K
image
В этой статье я расскажу о том, как можно захватывать аналоговый черно-белый видеосигнал с помощью платы STM32F4-DISCOVERY, и об особенностях передачи его на компьютер при помощи USB.

Передача изображений на компьютер по USB


Используя плату STM32F4-DISCOVERY, можно создавать различные USB устройства — периферийный модуль USB в используемом микроконтроллере обладает большим функционалом. А вот примеров необычных конструкций с его использованием в сети мало — в большинстве случаев USB используют для реализации классов HID (эмуляция клавиатур, мышей и джойстивов) и CDC (эмуляция COM-порта). Встроенный USB-host обычно используют для подключения USB флешек.

Мне хотелось сделать какое-либо необычное USB устройство, например, web-камеру. Реализовать его можно двумя путями — написать свой собственный класс USB-устройства, и драйвер для него, либо, что значительно проще, воспользоваться стандартным для USB классом видеоустройств UVC (USB video device class). Драйверы для таких устройств встроены даже в Windows XP. Основное описание на UVC можно найти в этом документе (я использовал версию UVC 1.0, хотя есть и более новая 1.1).
Примеров реализации UVC на микроконтроллере в интернете очень мало. Достаточно большую сложность представляет правильное составление дескрипторов устройства (дескрипторы описывают весь его функционал). Даже небольшая ошибка в дескрипторе может приводить к тому, что устройство предстанет определятся, или даже к BSOD. Можно скопировать дескрипторы из имеющейся web-камеры, однако они могут быть излишне сложными — камеры часто содержат микрофон, позволяют производить захват одиночного изображения (Still image capture в терминологии UVC), позволяют изменять большое число настроек камеры. Во всем этом легко запутаться, так что мне хотелось сделать максимально простой проект.
После длительных поисков, совершенно случайно наткнулся на такой китайский проект. Это тетрис для STM32F103, причем для отображения картинки используется компьютер, который видит контроллер как UVC устройство. В проекте даже реализовано кодирование MJPEG. Проект довольно интересный, но код там невероятно запутанный, с практически полным отсутствием комментариев. Дескрипторы я взял именно из него, и немного подправил их под свои требования.

При составлении дескрипторов, среди прочего, нужно указать параметры передаваемого изображения. Я остановился на размере изображения в 320x240 пикселей и формате изображения NV12. Стандарт UVC позволяет предавать только два формата несжатых изображений: NV12 и YUY2.
Второй формат более распространен, но NV12 больше подходит для кодирования черно-белых изображений и занимает меньше места. В этом формате данные кодируются по типу YUV 4:2:0 (на четыре пикселя приходится два байта информации о цвете). Сначала идет информация о яркости всего изображения (320*240 байт в моем случае), затем информация о цвете (поочередно идут байты U и V):

image

Всего изображение будет занимать (320*240*3/2) байт. У данного формата есть недостаток — не все программы умеют с ним работать. Гарантированно с этим форматом работает бесплатная программа ContaCam, Skype тоже работал нормально.
Для того, чтобы загружать тестовые изображения в контроллер, был написан специальный конвертер, выдающий .h файлы с закодированными данными изображения. Кроме NV12, конвертер может кодировать изображения в формат YUY2.
Подробное описание того, как правильно настраивать дескрипторы и передавать поток данных в случае несжатых изображений, можно найти в отдельном документе: "Universal Serial Bus Device Class Definition for Video Devices: Uncompressed Payload"

В качестве базового проекта я взят свой проект USB-микрофона. В нем тоже была реализована передача данных на компьютер через изохронную конечную точку. Работа с USB реализована при помощи библиотеки от производителя контроллера (STSW-STM32046). После замены дескрипторов, VID/PID (как я понял, можно установить любые), контроллер обнаружился как устройство обработки изображений. Следующий этап — передача на компьютер потока видеоинформации (для начала — тестового изображения, хранящегося в памяти контроллера).

Предварительно стоит упомянуть о различных USB запросах (Request), которые нужно обрабатывать. При получении контроллером запроса от компьютера (хоста) некоторых видов запросов, библиотека USB вызывает функцию usbd_video_Setup, которая должна обработать запрос.
Большая часть этой функции взята из кода микрофона — это обработка Standard Device Requests. Здесь можно обратить внимание на переключение между альтернативными интерфейсами, которое происходит при получении запроса SET_INTERFACE. UVC устройство должно предоставлять как минимум два альтернативных интерфейса, на один из которых (Zero Bandwidth, идет под 0 номером) компьютер переключает USB устройство, когда оно не нужно, тем самым ограничивая поток данных по шине. Когда какая-либо программа на компьютере готова принимать данные от устройства, она передает на него запрос на переключение на другой альтернативный интерфейс, после чего устройство начинает получать от хоста IN Token Packets, сигнализирующие о том, что хост ожидает передачи данных.
Есть и другой тип запросов — Class Device Requests, специфичные для данного класса — UVC. Используются они для получения с камеры данных о ее состоянии и управления ее работой. Но даже в простейшей реализации, когда никакие параметры камеры изменить нельзя, программа должна обрабатывать запросы: GET_CUR, GET_DEF, GET_MIN, GET_MAX, SET_CUR. Все они передаются перед включением камеры с компьютера. Согласно спецификации UVC, компьютер запрашивает с камеры режимы, в которых она умеет работать, а потом передает указание, в каком режиме камера должна работать. Причем есть два типа таких запросов: Probe и Commit. В моем случае, эти данные никак не используются, но если запрос не обработать (не забрать отправленные данные или не ответить), то программа на компьютере «зависнет», и контроллеру потребуется перезагрузка.

В процессе создания проекта обнаружилось, что библиотека USB иногда некорректно обрабатывает запросы передачи данных на хост — после передачи некоторого небольшого объема данных передача данных прекращается, и возобновить ее можно только перезагрузить компьютер. Это касается как передачи видеоинформации (через 1 конечную точку), так и управляющей информации (через 0 конечную точку). Исправляется это предварительной очисткой FIFO нужной конечной точки перед началом записи в нее.

После того, как все нужные запросы переданы, и компьютер отправил запрос на переключение альтернативного интерфейса в основной режим, можно начинать передавать видеоданные. Компьютер начинает выдавать на шину IN Token Packet каждую миллисекунду, при получении которых контроллер вызывает функцию usbd_video_DataIn, из которой нужно вызвать библиотечную функцию передачи данных DCD_EP_Tx.
Видеоданные передаются пакетами, в начале каждого пакета должен находится заголовок длиной 2 байта (спецификация UVC поддерживает использование и более длинных заголовков с дополнительной информацией). Первый байт заголовка всегда равен 2 — это общая длина заголовка. Второй байт позволяет хосту обнаруживать начало кадра и их смену — первый бит этого байта нужно переключать в первом пакете нового кадра. В последующих пакетах этого кадра значение этого бита должно оставаться таким же. Остальные биты можно оставить равными нулю. Оставшуюся часть пакета занимают видеоданные. Их длина в пакете может быть произвольной (но не больше определенного размера).
Я специально подбирал длину видеоданных в пакете такой, чтобы размер изображения в байтах делился на нее без остатка — так все пакеты получаются одинаковой длины.

Получается вот такой результат:

image

А что же с производительностью?
Контроллер поддерживает стандарт USB Full Speed, что дает теоретическую скорость 12 Мбит/с. Таким образом, максимум, на что можно рассчитывать — время передачи кадра будет (320*240*3/2) / (12*10^6 / 8) = 76 мс, что дает 13 FPS. Однако, USB — полудуплексный протокол, а микроконтроллер имеет свои ограничения. Данные по USB контроллер передает с использованием FIFO, причем этой памяти у контроллера — 1250 байт, и ее нужно разделить между всеми контрольными точками. Распределение памяти указывается в файле «usb_conf.h», причем размеры указываются в 32-битных словах.

 #define RX_FIFO_FS_SIZE                          64
 #define TX0_FIFO_FS_SIZE                         16
 #define TX1_FIFO_FS_SIZE                         232
 #define TX2_FIFO_FS_SIZE                         0
 #define TX3_FIFO_FS_SIZE                         0

Для FIFO приема команд от компьютера нужно выделить не менее 64 слов, на FIFO предачи управляющей информации на компьютер через 0 конечную точку нужно еще 16 слов. Все остальное можно выделить на первую конечную точку для передачи видеоданных. Суммарно получается (64 + 16 + 232)*4 = 1248 байта. Так как имеется ограничение на 232 слово (928 байт), то размер пакета (VIDEO_PACKET_SIZE) был установлен равным (768+2) байт. Таким образом, один кадр состоит из (320*240*3/2) / (768) = 150 пакетов, которые будут предаваться 150*1мс, что дает 6,6 FPS.
Реальный результат совпадает с рассчитанным:

image

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

Переход к MJPEG


Стандарт UVC поддерживает разные виды сжатия, один из которых — MJPEG. В данном виде сжатия каждый исходный кадр изображения сжимается по стандарту JPEG. Полученный сжатый кадр можно отправить на компьютер так, как описано выше. Особенности дескрипторов и передачи данных для MJPEG описаны в документе "Universal Serial Bus Device Class Definition for Video Devices: Motion-JPEG Payload".

Передача статического изображения, подготовленного на компьютере, оказалось довольно простой — преобразуем обычный JPEG файл в .h файл, добавляем его к проекту, передаем его по пакетам, также как и раньше. Так как размер сжатого изображения может быть произвольным, то длина последнего пакета данных тоже получается переменной, так что ее нужно вычислять.
При размере сжатого изображения 30000 байт, он будет состоять из (30000/768) => 40 пакетов, которые будут передаваться 40мс, что соответствует 25 FPS.
Для сжатия в JPEG я решил использовать кодировщик, взятый здесь. Он адаптирован для ARM, и рассчитан только на черно-белое изображение, что меня устраивало, так так я собирался брать данные с черно-белой камеры.
На STM32F4 этот кодировщик заработал сразу же, никакой адаптации под Cortrx-M4 я не делал. Тестовый bmp файл сжимался за 25мс, что соответствует 40 FPS. Для того, чтобы считать сжатое изображение из контроллера, я использовал программу «STM32 ST-LINK Utility». Предварительно во время отладки программы нужно узнать начальный адрес массива, в который будет помещаться сжатое изображение, и затем указать его в этой программе. Считанный дамп можно сохранить сразу как .jpg.
Далее я добавил в кодировщик возможность работы с двумя выходными массивами — для двойной буферизации, и объединил его с проектом вывода данных по USB.
Особенность использования памяти CCM
В используемом контроллере RAM разделена на несколько блоков. Один из них — (64 Кбайта) называется CCM, и к нему нельзя получить доступ через DMA. Я решил поместить сюда два массива для хранения сжатого изображения.
Для того, чтобы использовать эту память, в IAR нужно отредактировать используемый .icf файл, добавив в него строки:
define symbol __ICFEDIT_region_RAMCCM_start__ = 0x10000000;
define symbol __ICFEDIT_region_RAMCCM_end__   = 0x1000FFFF;
.......
define region CCMRAM_region   = mem:[from __ICFEDIT_region_RAMCCM_start__   to __ICFEDIT_region_RAMCCM_end__];
.......
place in CCMRAM_region {section .ccmram};


Массивы в коде нужно объявлять так:
#pragma location = ".ccmram"
uint8_t outbytes0[32000];
#pragma location = ".ccmram"
uint8_t outbytes1[32000];


Получившаяся конструкция заработала, однако только в программе ContaCam и в браузере (проверялось здесь). На статическом изображении удалось получить 35 FPS.
Пример сжатого изображения (размер изображения 17 Кбайт):

image

Изображение перевернуто, так как информация в bmp файлах хранится именно так.

А вот другие программы или не работали вовсе, или давали вот такое изображение:

image

Это связано с тем, что стандарт UVC не поддерживает передачу черно-белых изображений при помощи MJPEG.
Требования в JPEG к изображению такие:
• Color encoding — YCbCr
• Bits per pixel — 8 per color component (before filtering/subsampling)
• Subsampling — 422

Таким образом, требовалось переделать имеющийся кодировщик для формирования псевдоцветных изображений — по настоящему в таком изображении кодируются только данные о яркости (Y), а вместо данных о цвете (Cb и Cr) передаются нули. Пришлось ознакомится со структурой формата JPEG глубже.

Переход от черно-белого изображения к псевдоцветному


Как работал кодировщик раньше:
1. Формируется заголовок JPEG файла.
2. Поблочная (8x8 пикселей) обработка исходного изображения.
2.1 Каждый блок считывается из памяти, производится его дискретное косинусное преобразование (DCT)
2.2 Получившиеся 64 значения квантуются и результат пакуется с использованием кодов Хаффмана.
3. Формируется маркер конца данных и подсчитывается размер сжатого изображения.
Более подробно про JPEG можно почитать здесь и здесь.
Информация о наличии цвета в сжатом изображении хранится в заголовке JPEG, так что его нужно изменить. Менять надо секции SOF0 и SOS, указав в них использование трех компонентов, для яркостного компонента прореживание 22, для цветовых 11. Везде в качестве идентификатора таблиц квантования я указывал 0.
Теперь можно изменить методику кодирования информации. Так как цветовая информация кодируется с прореживанием, то двум цветовым блокам информации должны соответствовать четыре блока яркостной информации. Таким образом, сначала последовательно кодируются четыре блока яркостной информации, после чего нужно произвести кодирование еще двух блоков цветовой информации (пример из вышеуказанной статьи):

image

В использованной библиотеке квантование, окончательное сжатие обработанных данных, и запись их в память выполняются отдельной функцией, так что для формирования цветовой информации достаточно обнулить массив DCT коэффициентов и вызвать эту функцию дважды.
Однако в JPEG кодировании есть важная особенность — кодируются не DC-коэффициенты, идущие в начале каждого блока, а разность текущего DC-коэффициента, и DC-коэффициента предыдущего блока соответствующего компонента. В библиотеке эта разность изначально вычислялась перед квантованием, так что пришлось модифицировать вышеуказанную функцию, так, чтобы во время обработки каналов Cr и Cb разность не вычислялась — в этих компонентах и так идут нули.
В результате картинка начала отображаться корректно во всех используемых программах видеозахвата. Недостаток такого псевдоцветного кодирования — несколько упала его скорость. Сжатие тестового изображение стало занимать 35 мс, что дает 28 FPS.

Захват аналогового видеосигнала


Теперь, когда появился способ передавать видеоданные на компьютер с приемлемой скоростью, можно заняться и захватом видеосигнала. С самого начала экспериментов с USB я предполагал реализовать захват видеосигнала от аналоговой видеокамеры средствами самой отладочной платы.
Так как раньше я уже делал самодельный телевизор на микроконтроллере, то методика захвата черно-белого видеосигнала не была для меня чем-то новым. Конечно, контроллер STM32F4 сильно отличается от ATxmega, так что и подход к захвату видео пришлось поменять.

Сам формат PAL уже многократно описан на различных ресурсах, так что остановлюсь на его основных положениях, и только для черно-белого варианта.
Кадровая частота этого формата — 25 Гц, но при этом используется чересстрочная развертка — то есть при передаче кадра сначала передаются сначала четные, а затем нечетные строки. Каждый такой набор строк называется полем. Поля в данном формате идут с частотой 50 Гц (20 мс). В одном поле передается 312,5 строк (из них видеоинформацию содержат только 288,5). Все строки разделяются синхроимпульсами, которые следуют с периодом 64 мкс. Сам видеосигнал в строке при этом занимает 52 мкс.
Поля разделяются кадровыми и уравнивающими синхроимпульсами. Важная особенность уравнивающих синхроимпульсов — их период в два раза меньше периода строк — 32 мкс, так что их легко отличить от синхроимпульсов.

image

Таким образом, для того, чтобы захватывать изображение в память контроллера, нужно написать программу, способную обнаруживать синхросигналы, выделять из них уравнивающие импульсы, и запускать преобразование АЦП в перед началом передачи видеоданных каждой строки.

Теперь следует подробнее остановится на методике оцифровки видеосигнала.
В контроллере STM32F4 имеются три отдельных АЦП, каждый из которых может работать со скоростью 2.4 MSPS при разрядности 12 бит. При уменьшении разрядности скорость работы увеличивается, но и этого не хватит для получения разрешения 320*240. Однако контроллер позволяет объединять несколько АЦП — можно настроить захват всеми АЦП одновременно, а можно установить задержку захвата между АЦП, в результате чего общая скорость захвата возрастает.

Какая же скорость захвата будет при использовании сразу двух АЦП (Interleaved dual mode)?
Для тактирования АЦП используется шина APB2, тактовая частота которой при инициализации контроллера устанавливается раной половине системной частоты (168 MHz / 2) = 84 MHz. Для АЦП это слишком много, так что при настройке АЦП приходится устанавливать предделитель на 2. Получившаяся частота 42 MHz все равно больше максимальной допустимой по даташиту (36 MHz), но у меня АЦП хорошо работает и при такой частоте.
В случае, если бы каждый АЦП при установленной разрядности 8 бит работал бы отдельно, то максимальная скорость преобразования была бы (42 MHz / (3+8)) = 3.81 MSPS. Установив задержку между временем захвата данных в 6 циклов, можно получить скорость 7 MSPS, а при 7 циклах — 6 MSPS.
Я выбрал последний вариант. При этом получается, что вся строка (64 мкс) займет 384 байта, а активная часть строки с видеосигналом (52 мкс) займет 312 байт (пикселей).
АЦП передает результаты преобразования в память при помощи DMA. При использовании двух 8-битных АЦП, данные передаются в память в виде 16-битных слов в момент завершения преобразования второго АЦП. В принципе, можно было бы захватывать в память содержимое практически всего кадра целиком — для этого нужно (384*240) = 92,16 Кбайт. Но я пошел по другому пути — захват данных начинается после обнаружения контроллером импульса синхронизации, и останавливается после захвата 366 байт (183 передачи DMA). Почему выбрано такое число — расскажу далее. В результате видеоданные занимают (366*240) = 87,84 Кбайт ОЗУ.

Рассмотрим методику обнаружения синхросигнала. В идеале, его лучше обнаруживать специальной микросхемой, или хотя бы компаратором, но это усложняет конструкцию. Так как у меня остался один не использованный АЦП, то я решил применить его для обнаружения синхросигналов. В состав каждого АЦП входит специальный модуль «Analog watchdog». Он может формировать прерывание в случае, если оцифрованное значение выходит за заданные пределы. Однако этот модуль не способен реагировать на изменение фронта оцифровываемого сигнала — он будет формировать прерывание до тех пор, пока не изменится входной сигнал или настройки модуля. Так как мне нужно было обнаруживать фронты сигнала, то пришлось сделать перенастройку этого модуля при каждом его прерывании. Я не стал реализовывать в программе автоматическое определение порогов срабатывания Analog watchdog, так что они указываются вручную для используемой камеры.

Для того, чтобы обнаруживать уравнивающие импульсы, используется один из таймеров контроллера, работающий с частотой 1 МГц. Таймер работает постоянно, а в обработчике прерывания Analog watchdog (при обнаружении переднего фронта синхроимпульса) считывается его текущее значение, и сравнивается с предыдущим. Таким образом можно отличать строчные синхроимпульсы от уравнивающих. После того, как уравнивающие синхроимпульсы закончились, контроллер пропускает 17 строчных синхроимпульсов, и при обнаружении переднего фронта синхроимпульса начинает захват видеоданных текущей строки. Так как вход в обработчик прерывания в данном контроллере может может происходить за переменное время, а так же из-за того, что АЦП 3 работает медленнее, чем первые два вместе, то время между фронтом синхроимпульса и началом захвата может отличатся, что приводит к «дрожанию» строк. Именно поэтому захват видеоданных начинается еще с переднего фронта синхроимпульса, а строка занимает 366 байт — таким образом часть синхроимпульса попадает в кадр, и ее можно убрать программно для каждой строки.
На осциллограмме видно, как идет захват видеосигнала (во время работы DMA «желтый» канал устанавливается в 1):

image

Захват начинается только после появления видеоданных:

image

Не все строки в одном поле захватываются, так как установлено ограничение в 240 строк.

image

В результате получается вот такое необработанное изображение (получено с использованием ST-Link Utility):

image

После того, как изображение захвачено в память контроллера, его нужно обработать — для каждой строки убрать смещение, связанное с захватом синхросигнала, и вычесть из значений яркости пикселей величину уровня черного. Я не старался оптимизировать этот участок кода, так что его выполнение занимает 5 мс.

После того, как изображение обработано, его можно начинать кодировать в JPEG, при этом одновременно начинается передача по USB уже закодированных данных предыдущего изображения.
Таким образом, кадр захватывается 20 мс, обработка идет 5 мс, и кодирование вместе с передачей данных идут 35 мс, что суммарно дает 60 мс, или частоту кадров 16.6 FPS. В результате получается, что один кадр (на самом деле поле) захватывается, а два пропускаются. Так как развертка в формате PAL чересстрочная, то выходит, что поочередно захватываются то четное, то нечетное поле, что приводит к дрожанию изображения на один пиксель. Избавится от этого можно, добавив дополнительную задержку между захватом кадров — тогда будет пропускаться еще одно поле, и частота кадров на выходе упадет до (50 / 4) = 12.5 FPS.

Немного про источник видеосигнала


Изначально я планировал использовать в качестве источника сигнала видеокамеру видеонаблюдения KPC-190S (этой камере уже практически 15 лет). Нельзя сказать, что она обеспечивает хорошее качество изображения — оно достаточно шумное, с не очень высоким контрастом, и маленькой амплитудой (из осциллограммы видно, что она близка к 1 В). Для небольшой подстройки выходного сигнала камера подключается к контроллеру через резисторный делитель на переменном резисторе. Единственный сигнальный вывод платы, к которому подключена камера — это PC2 (к нему подключены все АЦП).
Внешний вид конструкции:



Так как камера не обеспечивала хорошего качества изображения, я решил попробовать брать сигнал с фотоаппарата Canon A710. У него имеется аналоговый выход для подключения к телевизору, на который выводится все, что отображается на экране фотоаппарата. Качество сигнала у фотоаппарата получше, однако видеосигнал у него цветной. Для того, чтобы убрать из сигнала цветовую составляющую, я использовал вот такой фильтр:

image

Кроме того, синхроимпульсы на выходе фотоаппарата имеют отрицательную полярность, так что для того, чтобы они обнаруживались АЦП контроллера, пришлось добавить к сигналу дополнительное напряжение смещения при помощи регулируемого источника питания. Так же пришлось немного изменить пороги срабатывания Analog watchdog.
Внешний вид конструкции с подключенным фотоаппаратом:



Пример изображения, получаемого от фотоаппарата:

image

Видео работы устройства:



Исходные коды проектов: github.com/iliasam/STM32F4_UVC_Camera
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+68
Comments 8
Comments Comments 8

Articles