Помните как некто 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


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

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

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




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

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


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

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

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

Настройки таймера




В ходе эксперимента выяснилось что к регистрам 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 «пикселей» на строку.

Значит фреймбуфер получится размером 312x410.

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

Перед запуском передачи происходит заполнение фреймбуфера синхроимпульсами, а уровень черного обеспечивается заполнением ШИМ в 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 никак не отличается для телевизора.

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


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

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


Также пытался использовать I2S, но безуспешно.

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

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


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


В STM32 в регистре RCC_CR есть биты HSITRIM, отвечающие за подстройку частоты HSI генератора.

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

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


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