Ve günlükerin sonuna geldik. PAL yada NTSC sinyali FPGA ile scandoubling yaparak HDMI formatına çevirmek...
PAL yada NTSC, analog sinyali FPGA ile işleyebilmemiz için öncelikle dijital bir hale sokmamız gerekiyor. Bu noktada Video Decoder entegrelerinden yararlanıyoruz. Gelen analog sinyali, dijital ortamda bir çok farklı şekilde ifade etmek mümkün. Biz sektörde çok kabul görmüş bir format olan BT.601 formatını kullanacağız. Esasen bu formatı seçme sebebimiz, ulaşılabilir birçok video decoder chipinin bu formatta çıkış veriyor olması. Bu format hakkında detaylı bilgiye aşağıdaki linkten ulaşabilirsiniz.
https://en.wikipedia.org/wiki/Rec._601Özetlemek gerekirse, bu format ile gelen sinyal ayrıştırıldıktan sonra Luma(Işık/Parlaklık) ve Chroma(Renk) bilgisi haline dönüştürülüyor ve dijital ortama atılıyor. PAL yada NTSC, her bir TV satırı (yada scanline) 720 adet ışık örneğinden ve 360 adet renk örneğinden oluşuyor. İnsan gözü ışık farklılıklarına daha duyarlı olduğu için, renk örneklemesinin az olması gözle görülür bir fark yaratmıyor.
Renk örneklemeleri tek bir byte halinde değil, Cb ve Cr olarak iki byte halinde aktarılıyor. Luma, ışık örnekleri ise Y olarak tek byte olarak aktarılıyor.
Video decoder entegremizden ard arda dijitie edilmiş olarak Cb, Y, Cr, Y şekline devam eden 4 bytelık paketler gelmekte. Bir satırda 720 Y bilgisi, 360 Cr Bilgisi, 360 Cb bilgisi geliyor. Ancak ekranda görünür 720 nokta olsa bile, bir alt satıra geçmeden önce, horizontal blanking adı verilen boşluklar bırakılıyor. Yani eski tip bir CRT monitor bir satırı çizmeyi bitirdikten sonra, bir alt satırın görünür başlangıç noktasına geçmesi için süre tanınıyor. Bu sebeple her bir satır için aslında 720 değil, 858 nokta gönderiliyor. Bu 858 değeri nerden geldi diyecek oluranız, google da 'EIA-CEA-861-B.pdf' dökümanını arayabilirsiniz. Döküman ücretli olduğundan direk link paylaşmıyorum ama kolaylıkla kopyası bulunabilir. Bu dökümanda her bir format için timing ve blanking değerleri detalı olarak açıklanmış durumda.
Bu dijitize edilmiş olan bytelar 27MHz lik bir clock eşliğinde geliyor. Yani decoderdan gelen veriyi 27 mega sample hızında işleyebilmemiz gerekiyor. Decoder chipinin bir özelliği ise, bu 27 MHz sinyali gelen video sinyalinden PLL ile elde ediyor olması. Yani eğer kaynak biraz yavaş yada hızlı bile olsa, gene de bir satırda gelen sample sayısı değişmiyor. Chipin üretmiş olduğu 27MHz clock biraz yavaşlamış yada hızlanmış oluyor sadece.
Artık FPGA kısmına geçebiliriz. FPGA'de hazırladığım kodları olabildiğince modüller halinde yazdım. Daha önce belirtmiştim, verilog tercih ediyorum. İlk modülümüz gelen dijital video sinyalinin, gene FPGA içerisinde oluşturduğumuz bir RAM'e atılmasını sağlıyor. Buna neden ihtiyacımız var? PAL yada NTSC gelen sinyalin her bir satırı toplam 64uS de geliyor. Ancak biz scandoubling yaptığımız için, kaynaktan 1 satır geldiğinde, HDMI'a iki satır göndermemiz gerekiyor. Bu sayede aynı zamanda interlaced gelen sinyali de progressive hale sokmuş olacağız. İhtiyacımız olan RAMin sadece 1 satırlık veri barındırması yeterli. Ben uygulamamda FPGA içerisinde iki adet RAM tanımladım. Bunlardan bri bir satıra ait olan Luma yani ışık değrlerini aklıyor. İkincisi ise Renk değerlerini saklıyor. Her bir RAM toplam 720 sample saklayacak.
Ancak burada bir sıkıntı çıkıyor karşımıza. Gelen stream içerisinde satır başı olduğunu nereden anlayacağız. Bu dijital formatı hazırlayanlar bu detaya şu şekilde bir çözüm getirmişler. Normalde görüntü taranırken kesinlikle 0 veya 255 değerleri üretilmiyor. Aktif video başlangıç ve bitiş anlarında, arka arkaya $FF, $00, $00 ve Senkron byteları gönderiliyor. Normalde bu değerler sinyal içerisinde bulunmadığından, bu bytelar arka arkaya görüldüğünde bir senkron işareti olduğu anlaşılıyor.
Senkron byte'inin içeriği değerlendirilerek, ekran başı, satır başı yada satır sonu işareti mi olduğu belirleniyor.
Bunu FPGA ile nasıl tespit ederiz?
Öncelikle son gelen 3 byte ve o anki byte'ın değerine ihtiyacımız var. Bu sebeple aşağıdaki gibi bir verilog kodu yazıyoruz.
always @(posedge videoInClock)
begin
// HSync Detector
sync1<=sync2;
sync2<=sync3;
sync3<=videoInData;
// sync4 0b1FVH1234 (1234Protection)
// H=0 Begin of Video
// H=1 End Of Video
if ( (sync1==8'b11111111) && (sync2==8'b00000000) && (sync3==8'b00000000) && ((videoInData&8'b10010000)==8'b10000000) )
begin
// Bu clock ile adigimiz son data icerisinde HSYNC isareti var
// Yani bir sonraki data Cb ile baslayacak, adres 0 olacak ...
regAddress<=10'b0;
regChromaSample<=1'b1;
regHSync<=1'b1;
// Vertical Sync Durumunu Kontrol Et ...
if ((videoInData&8'b00100000)==8'b00100000)
begin
regVSync<=1'b1;
end else
begin
regVSync<=1'b0;
end
end else
begin
if (regChromaSample==0) regAddress<=regAddress+1;
regChromaSample<=!regChromaSample;
regHSync<=1'b0;
end
end
Burada yaptığımız işlem, 3 bytelık bir shift register yaratmak ve son gelen 4 byte'ın senkron işareti taşıyıp taşımadığına bakmak. Eğer senkron işareti varsa, RAM için oluşturduğumuz adres değerini 0 yapıyoruz. Bu arada diğer modüllerde kullanılmak üzere bir senkron sinyali üretiyoruz. Eğer clock anında senkron işareti yakalanmamışsa, RAM için oluşturduğumuz adres değerini bir arttırıyoruz.
Ayrıca burada bir detay daha mevcut, regChromaSample<=!regChromaSample; satırı ile, gelen byte'ın Luma mı Chroma mı olduğuna karar veriyoruz. Sıralama Cb, Y, Cr, Y şeklinde olduğundan, bir Luma bir Chroma değeri geliyor olmalı...
İlk modülümüz bu kadar, yani gelen stream'e senkronize olup, Luma bytelarını Luma RAM'ine, Chroma bytelarını Chroma'a RAM'ine atıyor sadece.
İkinci modülümüz, RAM'lerden adığı veriyi HDMI formatın çeviren kısım. Daha önce yazdığım üzere, HDMI'da her bir pixel 10 bit olarak gönderiliyor. Demekki gelen 27MHz clock'un 10 katı hızında bir başka clock'a daha ihtiyacımız olacak. Bunu kullandığımız FPGA içerisinde 2 adet mevcut olan PLL'lerden birini kullanarak elde ediyoruz. Kabacak 27MHZ giriş clock'unu 270MHz e çeviriyor.
Ayrıca burada bir başka detay ise, elimizdeki dijital verinin Y,Cb,Cr şeklinde olması. Habuki HDMI'a RGB formatında göndereceğiz. Bunun için internette kolayca bulabileceğiniz katsayıları kullanarak bir YCbCr2RGB modülü yazıyoruz. Önce bu modülün koduna bakalım, sonra HDMI encoding işine geri dönelim.
module YCbCr2RGB
(
output reg [7:0]oRed,
output reg [7:0]oGreen,
output reg [7:0]oBlue,
input [7:0]iY,
input [7:0]iCb,
input [7:0]iCr,
input iCLK
);
// TEMP RGB 21Bit+Sign
wire signed [22:0] mR;
wire signed [22:0] mG;
wire signed [22:0] mB;
// -----------------------------------------------------------
// Bu magnitude toplamları maximum 21Bit+Sign olabilir !!!
// -----------------------------------------------------------
// Y*(4096) 8Bit+12Bit=20Bit+Sign
// 1.772*Cb*(4096) 8Bit+12Bit=20Bit+Sign
// -----------------------------------------------------------
// R = 1*Y + 1.402*(Cr-128)
// G = 1*Y - 0.344*(cb-128) - 0.714*(cR-128)
// B = 1*Y + 1.772*(Cb-128)
// -----------------------------------------------------------
assign mR = (iY*4096 + 4096*(iCr-128) + 1647*(iCr-128))>>12;
assign mG = (iY*4096 - 1409*(iCb-128) - 2925*(iCr-128))>>12;
assign mB = (iY*4096 + 4096*(iCb-128) + 3162*(iCb-128))>>12;
always @(posedge iCLK)
begin
oRed [7:0]<=(mR[10])?0:((mR>8'hff)?8'hFF:mR[7:0]);
oGreen[7:0]<=(mG[10])?0:((mG>8'hff)?8'hFF:mG[7:0]);
oBlue [7:0]<=(mB[10])?0:((mB>8'hff)?8'hFF:mB[7:0]);
end
endmodule
HDMI encoder kısmına geri dönecek olursak, verilog kodumuz aşağdaki gibi.
// ----------------------------------------------------------------------------
// HDMI64 - C64 Video HDMI Converter (with Scanline Effect)
// [url]www.commodore.gen.tr[/url] - delimawi'2016
// ----------------------------------------------------------------------------
// Project based on HDMI Sample Code from fpga4fun.com
// ----------------------------------------------------------------------------
module HDMI_Encoder
(
input pixelClock, // 27MHz Incomming Video Pixel Clock
input pixelClockX10, // 270MHz HDMI Bit Clock (pixelClockx10)
input extHSync,
input extVSync,
output [2:0] TMDSp, TMDSn, // HDMI Output Pins
output TMDSp_clock, TMDSn_clock,
// FIFO Output
output [9:0] PixelAddress,
input [7:0] PixelLuma,
input [7:0] PixelChroma,
);
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
`define DISPLAY_WIDTH 720
`define DISPLAY_HEIGHT 480
`define FULL_WIDTH 858
`define FULL_HEIGHT 525 //525
`define H_FRONT_PORCH 16
`define H_SYNC 62
`define V_FRONT_PORCH 9
`define V_SYNC 6
`define H_SYNC_BEGIN `DISPLAY_WIDTH+`H_FRONT_PORCH
`define H_SYNC_END `H_SYNC_BEGIN+`H_SYNC
`define V_SYNC_BEGIN `DISPLAY_HEIGHT+`V_FRONT_PORCH
`define V_SYNC_END `V_SYNC_BEGIN+`V_SYNC
reg [9:0] regXPos, regYPos; // 10 Bit X,Y Counters (0-1023)
reg regHSync,regVSync,regDrawing; // HDMI Synch Pulses
reg [7:0] regRed, regBlue, regGreen;
assign PixelAddress=regXPos;
always @(posedge pixelClock)
begin
regDrawing<=(regXPos<`DISPLAY_WIDTH)&&(regYPos<`DISPLAY_HEIGHT);
regHSync <= (regXPos>=`H_SYNC_BEGIN)&&(regXPos<`H_SYNC_END);
regVSync <= (regYPos>=`V_SYNC_BEGIN) && (regYPos<`V_SYNC_END);
end
reg regExtHSync, regExtVSync;
always @(posedge pixelClock)
begin
regExtHSync<=extHSync;
regExtVSync<=extVSync;
// Dusey Senkronu yakaladığın anda degerleri sıfırla
if ((extVSync==0) && (regExtVSync==1))
begin
regXPos<=0;
regYPos<=0;
end else
begin
if (regXPos==(`FULL_WIDTH-1))
begin
if (regYPos==(`FULL_HEIGHT-1))
begin
regYPos<=0;
end else
begin
regYPos<=regYPos+1'b1;
end
regXPos<=0;
end else
begin
regXPos<=regXPos+1'b1;
end
end
end
// ----------------------------------------------------------------------------
// Scanline Effect & YCbCr > RGB
// ----------------------------------------------------------------------------
reg [7:0] regCr;
reg [7:0] regCb;
wire [7:0] regInpRed, regInpBlue, regInpGreen;
YCbCr2RGB Convert( .oRed(regInpRed), .oGreen(regInpBlue), .oBlue(regInpGreen), .iY(PixelLuma[7:0]), .iCb(regCb), .iCr(regCr), .iCLK(pixelClock));
always @(posedge pixelClock)
begin
if (regXPos[0]==1)
begin
regCr<=PixelChroma;
end else
begin
regCb<=PixelChroma;
end
if (regYPos[0]==1)
begin
regRed <= (regInpRed>>1);
regGreen <= (regInpGreen>>1);
regBlue <= (regInpBlue>>1);
end
else
begin
regRed <= regInpRed;
regGreen <= regInpGreen;
regBlue <= regInpBlue;
end
end
// ----------------------------------------------------------------------------
// Generate TDMS Encoded 10 Bit Signals
// ----------------------------------------------------------------------------
wire [9:0] TMDS_red;
wire [9:0] TMDS_green;
wire [9:0] TMDS_blue;
TMDS_encoder encode_R( .clk(pixelClock), .VD(regRed ), .CD(2'b00) , .VDE(regDrawing), .TMDS(TMDS_red) );
TMDS_encoder encode_G( .clk(pixelClock), .VD(regGreen), .CD(2'b00) , .VDE(regDrawing), .TMDS(TMDS_green) );
TMDS_encoder encode_B( .clk(pixelClock), .VD(regBlue ), .CD({regVSync,regHSync}), .VDE(regDrawing), .TMDS(TMDS_blue) );
// ----------------------------------------------------------------------------
// Generate TDMS Clock
// ----------------------------------------------------------------------------
wire clk_TMDS=pixelClockX10;
// ----------------------------------------------------------------------------
// Generate TDMS Signals (Serialise)
// ----------------------------------------------------------------------------
reg [3:0] TMDS_mod10=0; // modulus 10 counter
reg [9:0] TMDS_shift_red=0; // 10 Bit Shift Registers
reg [9:0] TMDS_shift_green=0; // 10 Bit Shift Registers
reg [9:0] TMDS_shift_blue=0; // 10 Bit Shift Registers
reg TMDS_shift_load=0;
always @(posedge clk_TMDS) TMDS_shift_load <= (TMDS_mod10==4'd9);
always @(posedge clk_TMDS)
begin
TMDS_shift_red <= TMDS_shift_load ? TMDS_red : TMDS_shift_red [9:1];
TMDS_shift_green <= TMDS_shift_load ? TMDS_green : TMDS_shift_green[9:1];
TMDS_shift_blue <= TMDS_shift_load ? TMDS_blue : TMDS_shift_blue [9:1];
TMDS_mod10 <= (TMDS_mod10==4'd9) ? 4'd0 : TMDS_mod10+4'd1;
end
assign TMDSp[2] = TMDS_shift_red[0] ;
assign TMDSp[1] = TMDS_shift_green[0];
assign TMDSp[0] = TMDS_shift_blue[0] ;
assign TMDSp_clock = pixelClock;
assign TMDSn[2] = !TMDSp[2];
assign TMDSn[1] = !TMDSp[1];
assign TMDSn[0] = !TMDSp[0];
assign TMDSn_clock = !TMDSp_clock;
endmodule
Buradaki timing değerleri en son NES üzerinde çalıştığımdan NTSC için. PAL için olması gereken değerler aşağıda:
// ----------------------------------------------
// Timing 720x576 @50 Hz
// pixelClock 30MHz
// ----------------------------------------------
`define DISPLAY_WIDTH 720
`define DISPLAY_HEIGHT 576
`define FULL_WIDTH 864
`define FULL_HEIGHT 624
`define H_FRONT_PORCH 12
`define H_SYNC 64
`define V_FRONT_PORCH 5
`define V_SYNC 5
Son olarak FPGA top Level Design Şemamız aşağıda. (Bu modüllerin birbirlerine nasıl bağlandığı kısmı)

Büyük hali:
http://i67.tinypic.com/34g4pb4.jpgProjenin şu anki kaynak kodlarını da üzerinde çalışmak isteyenler için olduğu gibi ekliyorum. Kaynak kodlar ile ilgili olarak hiçbir garanti vermiyorum. Düzenleme yapmadım. Üzerinde çalıştığım son kodu olduğu gibi paylaşıyorum. Kodlar daha önce de belirttiğim gibi başka sitelerden alınmış parçalar bulundurmaktadır. (fgpa4fun)
Bundan sonra yapılacaklar:
- HDMI Ses encoding
- FPGA tarafındaki parametrelerin PIC'ile kontrol edilebilmesi için, FPGA I2C Slave fonksiyonu
- FPGA tarafında HDMI OSD çalışması
- FPGA 4-Bit çalışması
Projenin devamı için ilerleyen günlerde teknik olmayan ayrı bir proje başlığı açacağım.
Herkese iyi akşamlar...