Сегодня мы продолжим тему SDR-приема и обработки сигналов. Приемом аналогового ТВ я заинтересовался совершенно случайно, после вопроса одного из читателей. Однако это оказалось не так просто, из-за банального отсутствия образцов сигнала — во многих местах аналоговое ТВ уже отключено. Читатель даже прислал запись с RTL-SDR, однако ширина записи у RTL порядка 2МГц, в то время как полоса ТВ-сигнала занимает около 8МГц, и на записи было ничего не понятно. В итоге, тема была надолго заброшена, и наконец, только сейчас, в очередную поездку к родственникам я взял с собой SDRPlay, и настроившись на частоты ТВ-каналов, увидел на экране искомый сигнал.
Небольшая программа на Python, и все работает:
Для тех, кому интересны подробности, продолжение под катом.
Теория
В те давние послевоенные годы, когда о цифровой передаче сигналов знали только в секретных лабораториях, но люди уже хотели смотреть ТВ, существовали три конкурирующих аналоговых стандарта. Первым был американский NTSC (National Television System Committee), который разрабатывался еще с 40х годов, был «заточен» под американскую частоту сети 60Гц и имел вертикальное разрешение всего лишь в 486 строк. Чуть позже в Германии стал разрабатываться стандарт PAL (Phase Alternating Line), который был немного лучше американского (разрешение «целых» 576 строк и ориентированность на европейскую частоту сети 50Гц), и еще чуть позже появился французский SECAM (Séquentiel couleur à mémoire). В нем были устранены некоторые недостатки PAL, которые касались передачи цвета, ну и есть версия, что принятие двух стандартов также было политическим решением, чтобы жители одних стран не могли смотреть передачи из стран других (до единого Евросоюза и Шенгена было еще около 50 лет). Так или иначе, но весь мир оказался разделен примерно так:
Т.к. Хабр все-таки сайт русскоязычный, то в дальнейшем мы будем рассматривать именно SECAM, хотя если кто-то пришлет образец сигнала PAL, тоже было бы интересно.
Спектр SECAM, если верить старинным свиткам, выглядит следующим образом:
Слева, на частоте F0, находится амплитудно-модулированный яркостный (L) сигнал. Это фактически черно-белое изображение, которое до сих пор может быть показано на старом теплом и ламповом черно-белом ТВ. Проблема Legacy и наличия у пользователей старых девайсов существовала уже тогда, так что цветовой канал был добавлен отдельно, без потери совместимости со старыми телеприемниками. Два канала цвета передавались поочередно в частотной модуляции на частотах 4.25 и 4.406МГц. И наконец, еще выше по частоте отдельно передавался звук, также в частотной модуляции.
Кстати, с приемом ТВ в Петербурге есть забавный момент. Как отрапортовали российские СМИ, аналоговое ТВ было отключено еще в октябре:
Однако это касается лишь государственных каналов, коммерческие никто не принуждает отключать свое вещание. По крайней мере, на момент написания статьи (декабрь 2019) примерно 5-6 каналов еще доступны в «аналоге» прямо в центре Питера. Но сколько это продлится, неизвестно, так что желающим записать «для истории» образцы сигналов наверное все-таки стоит поторопиться.
Наконец, настала пора включить SDR и посмотреть, что мы имеем в реале:
Звуковой канал сложности не представляет, на него можно просто навестись «мышкой» в HDSDR, выбрать FM с шириной полосы порядка 50КГц и послушать. Мы начнем декодирование с канала яркостного, это позволит нам получить готовую «картинку».
Декодирование
Как было написано выше, сигналы яркости передаются в АМ. Чтобы не писать декодер самостоятельно, воспользуемся GNU Radio — перенесем спектр на нулевую частоту, запустим АМ-декодер и сохраним результат в файл.
Теперь мы можем открыть сохраненный файл в Python:
import numpy as np
import matplotlib.pyplot as plt
lum_data = np.fromfile("pal_lum.raw", dtype='int32')
lum_data = -lum_data - 4700
fs = 9000000//2
x_time = np.linspace(0, len(lum_data)/fs, num=len(lum_data))
plt.plot(x_time, lum_data)
Мы видим на экране последовательность из 4х кадров.
Длина одного кадра 0.02с — это как раз 1/50 — кратно частоте сети 50Гц, сигналы которой служат в качестве «тактового генератора» (не забываем, что сигнал аналоговый). За каждый кадр передается 320 строк — развертка у нас черезстрочная, так что итоговая частота кадров составляет 25Гц.
Посмотрим отдельные строки подробнее:
Как можно видеть, началу каждой строки соответствует «синхроимпульс», затем размах сигнала соответствует текущим значениям яркости в данной строке. Все довольно просто, и вероятно, практически без изменений такой сигнал и подавался на электронно-лучевую трубку ТВ.
Остальное дело техники. Создаем в памяти изображение и копируем в него два кадра, т.к. развертка у нас черезстрочная. Размах сигнала не превышает +200, что позволяет нам записать эти значения напрямую как цвета RGB.
# Output image
frame_size = fs*1//50
img_x, img_y = 320, 650
img_size = (img_y, img_x, 3)
img_data = np.zeros(img_size, dtype=np.uint8)
img_data.fill(255)
frame_num = 0
# Frame #1
pos_x, pos_y = 0, 0
for px in range(frame_num*frame_size, (frame_num+1)*frame_size):
val = lum_data[px]
if val < 0: val = 0
if val > 255: val = 255
img_data[pos_y][pos_x] = (0, val, 0)
pos_x += 1
if lum_data[px] <= 0 and lum_data[px+1] > 0:
pos_x = 0
pos_y += 2
print("Scan lines 1:", pos_y)
# Frame #2
pos_x, pos_y = 0, 0
for px in range((frame_num+1)*frame_size, (frame_num+2)*frame_size):
val = lum_data[px]
if val < 0: val = 0
if val > 255: val = 255
img_data[pos_y+1][pos_x] = (0, val, 0)
pos_x += 1
if lum_data[px] <= 0 and lum_data[px+1] > 0:
pos_x = 0
pos_y += 2
img_resized = cv2.resize(img_data, dsize=(3*img_x, img_y), interpolation=cv2.INTER_CUBIC)
plt.imshow(img_resized, interpolation='nearest')
Как можно видеть, я использую переход через ноль для детектирования начала новой строки. Картинка оказалась сжатой по вертикали, это зависит в данном случае от частоты дискретизации SDR, в итоге я просто сделал ресайз в ширину.
Окончательный результат на анимации из 10 кадров (больше не принимает файловый архив хабра):
Заключение
Подобные стандарты интересно анализировать, т.к. во-первых, они довольно-таки простые для реализации, во-вторых, их изучение представляет еще и отчасти исторический интерес. Разумеется, цели сделать полноценный софтовый ТВ-тюнер у меня не было, так что код приведен в минимально-работоспособном виде.
Если оценки статьи будут положительны, во второй части можно будет рассмотреть работу с цветом, и вывести полноценную цветную картинку.
Для желающих поэкспериментировать самостоятельно, IQ-файл можно скачать по ссылке.
Всем удачных экспериментов.