Передача аналогового тв сигнала с помощью STM32

Помните как некто cnlohr запустил передачу ТВ сигнала на ESP8266?

Недавно мне попалось к просмотру это видео, стало интересно как это возможно и выяснил что автор видео разогнал частоту I2S до телевизионного диапазона, а затем с помощью DMA генерировал AM сигнал. Мне захотелось повторить это, но или прошивка криво собирается, или ESP модуль оказался неподходящий. Запустить передачу телесигнала не получалось.

Затем я вспомнил что STM32 умеет выводить свой тактовый сигнал на один из пинов.

Введение


Современные микроконтроллеры могут работать на частотах в сотни МГц, они попадают в диапазон работы FM приемников и аналоговых телевизоров. Практически все они имеют возможность вывода своей тактовой частоты на один из пинов. В микроконтроллерах STM32 эта функция называется Master Clock Output.

Если выбрать источником тактирования PLL, частоту на выходе можно менять в широких пределах. Так-же выход MCO можно включить и выключить простым переключением режима пина в регистре GPIO. Это побудило меня к экспериментам над возможностями формирования радиосигналов с помощью микроконтроллера.

В наличии была отладочная плата с микроконтроллером STM32F407. Его максимальная частота ядра равна 168МГц, а максимальная частота переключения GPIO равна 84 мгц.

Для начала нужно было понять на какую частоту настраивать MCO чтобы его мог поймать телевизор. Поиск привел меня на страницу с таблицей частот всех тв каналов. Наибольшую частоту ядра без превышения максимальной частоты переключения GPIO можно достичь выбрав 3 канал.
Частота ядра при этом будет равна 154.5МГц, а тактовый выход необходимо поделить на 2, чтобы получить искомые 77.25МГц.

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


Чтобы долго не изучать мануалы, для генерации инициализационного кода был использован cubeMX. В нем настроил PLL на частоту 154.5МГц, вывод MCO1 сделал с источником от PLL предделителем на 2. К выводу PA8 подсоединил кусок провода.

Настройки тактирования в CubeMX
8tvb_5piedtgh9xupcnxfam3sew.png


Скомпилировал проект, залил прошивку в плату и экран на телевизоре стал темным. Это означает что телевизор понимает импровизированную несущую как сигнал.

Дальше было определение насколько быстро можно включать/выключать тактовый сигнал чтобы создать подобие амплитудной модуляции. В бесконечном цикле включая и выключая тактирование с плавным уменьшением задержек удалось получить сначала горизонтальные, а затем и вертикальные полосы на кинескопе.

Результат программного управления MCO
2hv50xyy4hsplooylkopztcehde.jpeg

ayrdthq4eoxgdx6urtszcxsf_3u.jpeg

Но как вывести на него изображение?

Использование DMA

Если осуществлять управление с помощью ядра, практически всё его время будет использовано только для переключения тактового сигнала и любое прерывание будет сбивать тайминги. Поэтому единственным способом управления осталось использовние DMA с буфером.

Чтобы не тратить ресурсы ядра на запуск DMA каждый кадр, последнее можно настроить на работу в кольцевом режиме. Для того чтобы данные переносились со строго заданной скоростью, запуск передачи нужно осуществлять по событию от таймера.

В cubeMX это выглядит так:

Настройки таймера
9tp2tjbchfzlogr1kpngdgfdiwa.png
qitn111jpvzodahcybls2jnclzk.png


В ходе эксперимента выяснилось что к регистрам GPIO возможен и побайтовый доступ как самим ядром, так и через DMA, что позволило тратить всего 1 байт на пиксель:

#define GPIOA_MODER_8_11 (((uint8_t*)(&(GPIOA->MODER)))[2])
#define MCO_ON()  (GPIOA_MODER_8_11) = 2
#define MCO_OFF() (GPIOA_MODER_8_11) = 0


В начальной поставке библиотеки HAL, адресом назначения DMA является регистр таймера ARR. Пришлось написать функцию, позволяющую задать произвольный адрес назначения. Этим адресом является биты [16:23] регистра GPIOA→MODER.

Так-как DMA имеет 16 битный счетчик элементов, размер буфера ограничен в 64 кб. Но можно настроить DMA на работу с двойным буфером, что позволяет увеличить количество элементов в 2 раза.


// Изначальная функция, которая принимает в качестве аргумента лишь источник данных, а назначением является регистр TIM->ARR (регистр предзагрузки)
// HAL_StatusTypeDef HAL_TIM_Base_Start_DMA(TIM_HandleTypeDef *htim, uint32_t *pData, uint16_t Length);
// Добавлен аргумент - адрес назначения данных
HAL_StatusTypeDef HAL_TIM_Base_Start_DMA(TIM_HandleTypeDef *htim, uint32_t *pData, uint32_t *pDest, uint16_t Length);
// Запуск в режиме двойного буфера
HAL_StatusTypeDef HAL_TIM_Base_Start_DMA_DoubleBuffer(TIM_HandleTypeDef *htim, uint32_t *pData, uint32_t *pData2, uint32_t *pDest, uint16_t Length);

Формирование кадра

В стандарте PAL/SECAM видеосигнал имеет частоту кадров равную 25 гц и 625 строк на кадр. В случае отказа от черезстрочной развертки остается 312 строк изображения с частотой полей 50 гц. При максимальном размере буфера в 128 кб получится: 131072/312 = 420 «пикселей» на строку.
Значит фреймбуфер получится размером 312×410.

Чтобы телевизор понял где находится начало кадра и начало каждой строки, необходимо формировать синхроимпульсы. Поскольку в эфире сигнал передается с негативной полярностью, эти импульсы будут соответствовать максимальной амплитуде сигнала. Поскольку управление сигнала возможно лишь дискретно, для создания уровня черного используется ШИМ.

Перед запуском передачи происходит заполнение фреймбуфера синхроимпульсами, а уровень черного обеспечивается заполнением ШИМ в 25%.

Заполнение буфера кадра

// заполнение синхроимпульсами
void init_fb()
{
  // кадровый синхроимпульс находится в начале буфера,
  // чтобы прерывание конца передачи DMA (Vsync) приходилось
  // на начало обратного хода луча
  for (int i=0;i<24;i++)
    for (int j=0;j<(WIDTH);j++)
      frameBuf[i][j] = 2;
  
  // строчный синхроимпульс
  for (int i=0;i<312;i++)
    for (int j=(WIDTH - 30);j<(WIDTH);j++)
      frameBuf[i][j] = 2;
}

// приведение буфера кадра к уровню черного
void clear_fb()
{
    for (int i=24;i<312;i++)
    {
      // смещение начала строки
      int offset = (i * WIDTH);
      // ядро cortex-m4 позволяет работать с невыровненными данными,
      // 32 битный доступ позволяет ускорить работу
      uint32_t* data_ptr = (uint32_t*)(&((uint8_t*)(frameBuf))[offset]);
      // диагональные линии менее заметны на экране чем вертикальные
      uint32_t pattern = 0x02020202;
      pattern &= ~( 2 << (((i)%4)*8) );
      
      for (int j=0;j<(390);j+=4)
      {
        *(data_ptr++) = pattern;
      }
    }
}


На этом этапе записью значений в frameBuf можно формировать изображение на экране телевизора.
Графическая библиотека была портирована из демо проекта от другой платы discovery. С ее помощью можно генерировать различные графические примитивы и символы. Так-же был портирован когда-то мной написанный клон игры Space Invaders и 3D шары из проекта от ESP8266.

Результат получился следующий:

Полученный результат

По фотографиям экрана видны диагональные полосы, полученные в результате формирования «уровня черного».

Эксперименты с созданием промежуточных уровней сигнала

Что если управлять тактовым выходом не просто включая и выключая его, а менять значение регистра OSPEEDR? Этим регистром управляется крутизна фронта переключения вывода. Было интересно, возможно ли меняя его значение создать на экране телевизора больше чем 2 градации яркости.

Написал код, который в бесконечном цикле перебирает 5 вариантов:
MCO выключен через MODER
OSPEEDR = 0
OSPEEDR = 1
OSPEEDR = 2
OSPEEDR = 3

На экране появилось 4 полосы с разной яркостью. Состояние с минимальной крутизной фронтов и выключенным совсем MCO никак не отличается для телевизора.

Полосы с градациями яркости
_iu82zyl0a4fx9toxjvvcu2owqa.jpeg

Использование регистра OSPEED вместо MODER позволило увеличить четкость изображения.

Изображение при использовании модуляции с помощью OSPEEDR
cbclrabplriodhfvb0mkfuiaq70.jpeg

Так-же пытался использовать I2S, но безуспешно.
Выше примерно скорости 20 Мбит/с при дальнейшем увеличении частоты тактирования интерфейса, появляется нестабильность в работе. А на «стабильных» частотах если принимать гармоники сигнала, изображение едва отличимо от шума. SPI1 может работать на частотах выше, но качество сигнала тоже остается плохим.

Видео демонстрации работы прилагаю, код на гитхабе

github.com/rus084/F4Discovery-tv-transmitter

Как насчет частотной модуляции?

В STM32 в регистре RCC_CR есть биты HSITRIM, отвечающие за подстройку частоты HSI генератора.
Если настроить PLL со входом от HSI и выходной частотой, попадающей в FM диапазон, можно получить радиосигнал, который будет приниматься FM приемником. Модуляция осуществляется изменением значения битов HSITRIM.

Доказательство работоспособности показано в этом видео. Исходники тоже прилагаются.

github.com/rus084/F4Discovery-fm-transmitter

p.s. Да, код ужасный, но как proof of concept сгодится

© Habrahabr.ru