
Здравствуйте меня зовут Дмитрий сегодня мы продолжим исследование FPGA плат. Мы напишем контроллер HDMI интерфейса для Altera Ciclone.
Итак давайте начнем.
HDMI интерфейс
Работа HDMI интерфейса очень похожа на работу VGA интерфейса. Нам также необходимо сформировать 2 сигнала, сигнал синхронизации и сигнал гашения. Тайминги этих сигналов можно узнать на сайте tinyvga.com. Мы как и в случае с VGA выберем 800x600@60 Hz.
always @(posedge clk_40)
begin
if (rst)
begin
Hblank <= 1'b0;
Hsync <= 1'b0;
end
else
begin
case (H_count)
799: Hblank <= 1'b1;
839: Hsync <= 1'b1;
967: Hsync <= 1'b0;
ENDSTRING: Hblank <= 1'b0;
endcase
case (V_count)
599: Vblank <= 1'b1;
600: Vsync <= 1'b1;
604: Vsync <= 1'b0;
ENDFRAME: Vblank <= 1'b0;
endcase
end
end
Но в отличии от VGA используется принципиально иной способ доставки этих сигналов. Сигналы в HDMI интерфейсе передаются последовательно через 4 витых пары. Одна пара это CK через неё передается тактовый сигнал пикселей, одному пикселю соответствует один тик.
И три информационных пары D0, D1, D2. Через D0 передается синий цвет, D1 - зеленый, D2 - красный. Также через D0 передаются синхросигналы. Если рассматривать каждую витую пару отдельно. То данные в них передаются по интерфейсу TMDS.
TMDS
Интерфейс TMDS это дифференциальный интерфейс то есть сигнал в нем фиксируется как разность напряжений двух линий, если на одной линии напряжение больше чем на другой то ноль если меньше то единица. Причем типичная разница напряжений составляет 500 мВ. Да благодаря такому небольшому перепаду напряжений удается достигать очень высоких частот при передачи данных, порядка 10Гбит/с (цифра приблизительная я в справочнике не смотрел).
Ну и как вы понимаете такие высокие частоты ставят нас перед фактом. Наша линия это не просто линия, это длинная линия (то есть четверть длинны волны нашего сигнала меньше чем длинна линии).
Конечно про длинные линии можно читать целые лекции. Но я здесь этого делать не буду. Я лишь замечу что для длинной линии важно что-бы либо ближний, либо дальний её конец был согласован по сопротивлению. Причем не важно в какую сторону происходит рассогласование, это может быть нулевое сопротивление или сопротивление в несколько мегаом. Если конец линии не согласован то сигнал будет от него отражается, и постоянно бегая туда сюда как неприкаянный, он будет создавать помехи и ложные срабатывания.

Чтобы согласовать длинную линию нужно на её конце установить сопротивление равное удельному сопротивлению самой длинной линии, в данном случае это 50 Ом. Как видно по схеме у нас согласован дальний конец длинной линии. Причем резистор 50 Ом установлен не между линией и землей, а между линией и шиной питания. Таким образом наша линия мало того что длинная так она ещё и смещена по постоянному напряжению вверх на 3.3 вольта. Для чего это делают? Ну существуют логические вентили которые могут выдерживать намного больший входящий ток, чем исходящий (причем разница может достигать нескольких раз). Но для нас это не имеет никакого значения поскольку пины Cyclone 4 выдерживают только 8 мА как входящего так и выходящего тока.
Кстати резисторы согласующие дальний конец линии имеют такое интересное название. Но я что-то его забыл, там было что-то вроде резистор "Т-1000", но не так, если вы знаете напишите в комментариях.
LVDS

Другой высокоскоростной дифференциальный интерфейс это LVDS. Он очень похож на интерфейс TMDS но имеет несколько отличий. Первое что бросается в глаза это способ согласования на конце длинной линии. Вместо двух резисторов по 50 Ом здесь используется один на 100 Ом но он установлен между дифференциальными линиями. Также отличается уровень питания не 3,3В а 2,5В, и как будто этого мало линия смещена вверх не на весь уровень питания а только на половину то есть 1,25В. Поэтому уровень сигнала в линиях колеблется от 1В до 1,5В.
И вы наверно спросите. А чего это вдруг, я начал рассказывать про какой-то LVDS если я сказал что HDMI использует TMDS? Дак вот плата Cyclone 4 не поддерживает интерфейс TMDS, а поддерживает только LVDS. Поэтому придется подружить эти два интерфейса. Главная проблема это разный уровень смещения по постоянному току у этих интерфейсов. Решить это можно покупкой на Али вот такой вот платы.

В ней для развязки по постоянному напряжению используется конденсатор.

Также пины по которым будет передаваться сигнал нужно перевести в режим LVDS.

Кстати когда я перевел пины в этот режим компилятор начал ругаться на меня из-за того что в одном банке все пины должны иметь одинаковое напряжение питания у LVDS это 2,5В а у обычных цифровых пинов это 3,3В. Так что будьте готовы к тому что придется пожертвовать целым банком пинов ради нескольких LVDS контактов.
Шифрование
Как я уже сказал сигнал синхронизации передается по линии D0 причем они передается специальными кодами.
wire [9:0] HDMI_code_R = 10'b1101010100;
wire [9:0] HDMI_code_G = 10'b1101010100;
wire [9:0] HDMI_code_B = Vsync ? (Hsync ? 10'b1010101011 : 10'b0101010100) : (Hsync ? 10'b0010101011 : 10'b1101010100);
Сначала нужно сказать что пиксели передаются в инвертированном виде то есть 00000000 это самый яркий пиксель, а 11111111, это черный цвет.
Данные о цвете пикселей также должны быть закодированы особым образом. Это кодирование нужно для балансировки сигнала. Если сигнал будет не сбалансирован. Например в нем будут одни единицы (скажем мы захотели посмотреть по телевизору черный квадрат Малевича) тогда одна из линий будет постоянно нагружена и она может сгореть. Чтобы этого не случилось мы должны стремится к тому чтобы количество нулей в сигнале равнялось количеству единиц.
module DVI_encoder
(
input wire clk,
input wire [7:0] VD,
output reg [9:0] Out_Data = 0
);
// Stage 1: 8 bit -> 9 bit Make "0" more than "1"
wire [3:0] N1_VD = VD[0] + VD[1] + VD[2] + VD[3] + VD[4] + VD[5] + VD[6] + VD[7];
wire flag = (N1_VD > 4'h4) | ((N1_VD == 4'h4) & (VD[0] == 1'b0));
wire [8:0] q_m = flag ? {1'b0, ~VD[7:0]} : {1'b1, VD[7:0]};
// Stage 2: 9 bit -> 10 bit
//Find parity, if number "1" more then "0" then positive parity, else negotyve
wire [3:0] N1_q_m = q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7];
wire [3:0] N0_q_m = 4'h8 - (q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7]);
reg [4:0] Unbulance_count = 'd0;
always @(posedge clk)
begin
Out_Data[8] <= q_m[8];
if((N1_q_m == 4'h4) | (Unbulance_count == 5'h0)) // If we get parity or disbalance counter is zero
begin
Out_Data[9] <= ~q_m[8];
Out_Data[7:0] <= (q_m[8]) ? q_m[7:0] : ~q_m[7:0];
Unbulance_count <= (~q_m[8]) ? (Unbulance_count + N0_q_m - N1_q_m) : (Unbulance_count + N1_q_m - N0_q_m);
end
else
begin
if(Unbulance_count[4]) // If disbalance counter negotive
begin
Out_Data[9] <= 1'b1;
Out_Data[7:0] <= ~q_m[7:0];
Unbulance_count <= Unbulance_count + {q_m[8], 1'b0} + (N0_q_m - N1_q_m);
end
else
begin
Out_Data[9] <=1'b0;
Out_Data[7:0] <=q_m[7:0];
Unbulance_count <= Unbulance_count - {~q_m[8], 1'b0} + (N1_q_m - N0_q_m);
end
end
end
endmodule
На первом этапе мы делаем так чтобы нулей было больше чем единиц, и если оказывается единиц больше то мы инвертируем байт. После чего дописываем девятый бит который говорит был байт инвертирован или нет.
На втором этапе в дело вступает счетчик, который содержит информацию о том насколько сигнал не сбалансирован вообще. И если он отрицательный то это значит что мы отправили слишком много нулей (а как мы помним у нас сейчас нулей больше чем единиц) и нам нужно инвертировать сигнал чтобы единиц было больше. А если положительный то нечего инвертировать не надо и дописываем ещё один бит который говорит инвертировали мы сигнал ещё один раз или нет (всего получается 10 бит). И таким образом мы добиваемся чтобы счетчик стремился к нулю, а сигнал был сбалансирован.
Пересылка сигнала
Как мы узнали из предыдущего раздела для отправки одного пикселя нам нужно последовательно отправить 10 бит. Это значит что биты нужно отправлять на частоте в 10 большей чем частота пикселей. Поскольку у нас частота пикселей 40 мегагерц то для пересылки бит нам нужна частота 400 мегагерц. И тут мы вплотную подошли к пределу частоты PLL Циклона 4, максимальная частота которую он может выдавать это 460 мегагерц.
Но просто так послать сигнал через LVDS пин не получится, для этого в библиотеки Quartus существует специальный модуль LVDS_TX.
Мне повезло я смог уложится в лимит частоты Циклона 4. Но если вы захотите выводить изображение с разрешением больше чем 800x600, тогда можно использовать модуль ALTDDIO. Этот модуль отправляет сигнал через LVDS пин не только по фронту тактового сигнала но ещё и по спаду. Вы наверно слышали про DDR (Double Data Rate) память, она использует такой-же принцип передачи данных. Таким образом частота пересылки бит можно увеличить в два раза до 1020 мегагерц.
Вывод
Наверно самым интересным отличием HDMI от VGA, является то что мы "бесплатно" получаем 8-битный цвет. В то время как VGA требует что-бы для каждого бита мы руками припаяли дополнительный резистор к ЦАПу, поэтому редко когда на VGA встречается 8 бит, обычно все ограничиваются четырьмя, что-бы меньше паять.
Для демонстрации я использовал множество Мандельброта из моей предыдущей статьи.
Если вам понравилась эта статья, может вам понравятся и другие мои статьи по FPGA:
Создание видеокарты Бена Итера на FPGA чипе
Доступ к SDRAM памяти на FPGA и «множество Мандельброта»
Написание i2c контроллера для FPGA и подключение камер ov7670 и ov2640