前言

作为一个屏幕控,点点LCD屏幕之类的东西确实是这个blog经常出现的内容。这次呢还是点屏,不过玩点不一样的,这次来点VGA和CGA显示器,确实是之前没搞过的东西。

VGA和CGA都是IBM-PC上使用的显示标准,那一开始都是用于CRT显示器的,对时序都有较为严格的要求,而且必然的,需要不停刷新。这种事情让STM32来做就有点吃力了,不是说不可以,但是呢,我不是那种喜欢挑战芯片极限的人(其实就是不会),所以这次用FPGA来做。

目前FPGA还处于初学阶段(真的是点完LED就作死来搞这个了),所以代码什么的有问题记得告诉我。说起来惭愧,FPGA开发板买回来也几个月了,书看了许多可是实际上就没拿来做过什么事情,于是HDL水平接近于0,果然还是得弄点好玩的东西来练练手呢。这不最近搞了台CGA显示器,干脆试着用FPGA驱动看看。

简易VGA驱动

在搞CGA之前,还是来搞搞资料比较多的VGA。VGA的信号其实也就5个,行场同步+RGB模拟输入。以下是时序图:

从下面一张图看,这是描述了完整的一场的情况。首先有一段Vertical Blanking Interval,这段时间里是没有有效数据的,这段时间还能分3个部分,前肩、同步和后肩。其中同步也就是垂直同步信号脉冲的时候,这时垂直扫描从最下归为到最上。因此不难理解其实前肩是出现在上一场画面的最下端的,而后肩则出现在这场画面的最上端。而每一行当中同样也有Horizontal Blanking Interval,道理一致,不再赘述。

因此实际上,要产生一个VGA信号,有两个工作:1、产生垂直、水平同步信号 2、在合适的时间产生像素数据。这一切都需要一个时间基准,最简单的做法就是找一个合适频率的时钟信号作为像素时钟基准,以此进行计数。于是就有了简易的系统框图:

于是就可以开始动手了。

首先我们需要一个PLL来产生像素时钟基准,在Quartus II里选择Tools → MegaWizard Plug-in Manager,选择Create a new custom megafunction variation,之后选择PLL。Input Clock用50MHz,然后直接2分频得到25MHz(其实可以直接用板子上的27MHz时钟,我这里也是为了试试这个PLL好不好用)。之后就可以在主Verilog里面写上:

vgapll u2 (
.inclk0(CLOCK_50),
.c0(V_REFCLK)
);

这样就有了一个25MHz的V_REFCLK。

于是就可以着手产生同步信号了,也就是框图中的V-SYNC Gen.和H-SYNC Gen. 在那之前,先定义一下分辨率、前肩后肩等的参数:

//水平
parameter H_FRONT = 16; //前肩
parameter H_SYNC  = 96; //同步
parameter H_BACK  = 48; //后肩
parameter H_ACT   = 640;//有效像素
parameter H_BLANK = H_FRONT+H_SYNC+H_BACK; //总空白
parameter H_TOTAL = H_FRONT+H_SYNC+H_BACK+H_ACT; //总行长
//垂直
parameter V_FRONT = 11; //前肩 
parameter V_SYNC  = 2;  //同步
parameter V_BACK  = 31; //后肩
parameter V_ACT   = 480;//有效像素
parameter V_BLANK = V_FRONT+V_SYNC+V_BACK; //总空白
parameter V_TOTAL = V_FRONT+V_SYNC+V_BACK+V_ACT; //总场长

同步生成也就好写了,如下:

always @(posedge V_REFCLK or negedge RST_N)
begin
  if(!RST_N)
  begin
    H_Cont <= 0;
    VGA_HS <= 1;
  end
  else
    if(H_Cont<H_TOTAL)
      H_Cont <= H_Cont + 1'b1;
        else
      H_Cont <= 0;
      if(H_Cont==H_FRONT-1)
        VGA_HS <= 1'b0;
      if(H_Cont==H_FRONT+H_SYNC-1)
        VGA_HS <= 1'b1;
  end 
end

always@(posedge VGA_HS or negedge RST_N)
begin
  if(!RST_N)
  begin
    V_Cont <= 0;
    VGA_VS <= 1;
  end
  else
  begin
    if(V_Cont<V_TOTAL)
      V_Cont <= V_Cont+1'b1;
    else
      V_Cont <= 0;
    if(V_Cont==V_FRONT-1)
      VGA_VS <= 1'b0;
    if(V_Cont==V_FRONT+V_SYNC-1)
      VGA_VS <= 1'b1;
  end
end

别忘了他们还有X、Y输出:

assign N_X = (H_Cont>=H_BLANK) ? H_Cont-H_BLANK : 11'h0;
assign N_Y = (V_Cont>=V_BLANK) ? V_Cont-V_BLANK : 11'h0;
assign N_Address = N_Y*H_ACT+N_X;
assign N_Enable = ((H_Cont>=H_BLANK && H_Cont<H_TOTAL)&&
                   (V_Cont>=V_BLANK && V_Cont<V_TOTAL));

其中X、Y指示的是目前扫描的位置,而Address则是对于VRAM的地址,Enable是数据有效信号,因为可能当前正在Blanking。

看上面框图,还差Pattern Gen.和一个Data Control,我们这里简化下,先随便产生一点数据来看看效果。

简单一点先让VGA工作在RGB332模式下好了,于是定义一个reg [7:0] N_Color,然后把VGA的色彩和这个寄存器关联起来:

assign VGA_R[9:7] = N_Color[7:5];
assign VGA_R[6:4] = N_Color[7:5];
assign VGA_R[3:1] = N_Color[7:5];

assign VGA_G[9:7] = N_Color[4:2];
assign VGA_G[6:4] = N_Color[4:2];
assign VGA_G[3:1] = N_Color[4:2];

assign VGA_B[9:8] = N_Color[1:0];
assign VGA_B[7:6] = N_Color[1:0];
assign VGA_B[5:4] = N_Color[1:0];
assign VGA_B[3:2] = N_Color[1:0];
assign VGA_B[1:0] = N_Color[1:0];

之后简单地让N_Color = N_Address,就可以看见效果了:

(当然中间的是显示器菜单,显示当前的显示模式为640*480 66Hz,这张图PLL应该是产生28MHz的时钟,后来改25MHz了)

然而这显然没啥意思,至少也得能显示个图片吧(惯例)。首先我试了把图片放在Flash里面,用Img2Lcd产生图片,然后烧录到Flash里面,然后我就遇到了一个问题:Flash的延迟是70ns,也就是14MHz左右的读取速度,可是现在的像素时钟高达25MHz,Flash根本没有办法在这个速度下工作。于是解决方法也很粗暴,把分辨率降低到320*480,也就是每两个像素读取一次Flash。做法不复杂,首先生成一个和之前那个同步的12.5MHz时钟,然后写上

always @(posedge V_SLCLK)
begin
  N_Color <= FL_DQ;
  FL_ADDR <= N_Address;
end

完工,综合下载,能看见图片了吧

(其实看小图似乎看不出什么问题)

从256色到64K色

然而这只是320*480*256色的图片,效果还是差了点。为了实现全分辨率+16bit的色彩,我们需要用到更快的存储器,比如SRAM和SDRAM。由于我是第一次玩Verilog,觉得还是不要去折腾SDRAM这么复杂的东西了,搞个SRAM好了。SRAM因为是断电掉数据的,因此需要在上电时先把数据从Flash里面复制到SRAM才行。为此定义一个信号叫DATA_RDY用来表示复制是否完成,一个寄存器DATA_SEEK用来指示当前复制的位置。另外SRAM的读写口要抽象开来,不然会出问题的。于是代码如下:

wire DATA_RDY;
reg [18:0] DATA_SEEK = 0;
assign DATA_RDY = (DATA_SEEK == DATA_COUNT) ? 1 : 0;
parameter DATA_COUNT = 460800;
assign FL_CE_N = 0;
assign FL_OE_N = 0;
assign FL_RST_N = 1;
assign FL_WE_N = 1;	
assign SRAM_CE_N = 0; 
reg [7:0] SRAM_IN;
reg [17:0] SRAM_AIN;
reg [17:0] SRAM_AOUT;
reg SRAM_BIN; //1:UB Activate 0:LB Activate
assign SRAM_ADDR = DATA_RDY ? SRAM_AOUT : SRAM_AIN;
assign SRAM_DQ[7:0] = SRAM_WE_N ? 8'hzz : SRAM_IN;
assign SRAM_DQ[15:8] = SRAM_WE_N ? 8'hzz : SRAM_IN;
assign SRAM_WE_N = DATA_RDY;
assign SRAM_OE_N = !DATA_RDY;
assign SRAM_CE_N = 0;
assign SRAM_LB_N = DATA_RDY ? 0 : (!SRAM_BIN);
assign SRAM_UB_N = DATA_RDY ? 0 : SRAM_BIN;
always @(posedge V_SLCLK or negedge RST_N)
begin
  if(!RST_N)
  begin
    DATA_SEEK <= 0;
    FL_ADDR <= 0;
  end
  else
  if (!DATA_RDY)
  begin
    SRAM_AIN[17:0] <= DATA_SEEK[18:1];
    SRAM_IN <= FL_DQ;
    SRAM_BIN <= DATA_SEEK[0];
    DATA_SEEK <= DATA_SEEK + 1;
    FL_ADDR <= DATA_SEEK;
  end
end

大致思路就是上电后DATA_RDY为0,于是SRAM为写入模式,然后用V_SLCLK为时钟,每次从Flash读取1个字节然后写入SRAM。注意SRAM是16bit的,所以需要控制UB和LB来让数据写入到正确的位置。我也不知道这么写对不对,反正能用……

于是现在的效果是这样了,16bit色彩:

从VGA到CGA

CGA和VGA有以下几个不同:

  1. CGA的HSYNC、VSYNC信号极性和VGA相反
  2. CGA的行频率为固定的15.75kHz
  3. CGA使用RGBI的4位数字颜色输入,也就是最大发色为16色

因此也就别指望CGA的图像能有多好看了……不过呢,Verilog改起来还是方便的,首先改PLL。我的设定是108分频+31倍频,可以达到14.352MHz的像素时钟,和CGA的标准14.32MHz像素时钟偏差不大。随后设置参数以及输出:

//水平
parameter H_FRONT = 27; //前肩
parameter H_SYNC  = 161; //同步
parameter H_BACK  = 81; //后肩
parameter H_ACT   = 640;//有效像素
parameter H_BLANK = H_FRONT+H_SYNC+H_BACK; //总空白
parameter H_TOTAL = H_FRONT+H_SYNC+H_BACK+H_ACT; //总行长
//垂直
parameter V_FRONT = 20; //前肩 
parameter V_SYNC  = 3;  //同步
parameter V_BACK  = 40; //后肩
parameter V_ACT   = 200;//有效像素
parameter V_BLANK = V_FRONT+V_SYNC+V_BACK; //总空白
parameter V_TOTAL = V_FRONT+V_SYNC+V_BACK+V_ACT; //总场长
assign GPIO_0[1] = !VGA_VS;
assign GPIO_0[3] = !VGA_HS;
assign GPIO_0[5] = N_Enable ? N_Color[3] : 0; //I
assign GPIO_0[0] = N_Enable ? N_Color[0] : 1; //B
assign GPIO_0[2] = N_Enable ? N_Color[1] : 0; //G
assign GPIO_0[4] = N_Enable ? N_Color[2] : 0; //R

值得注意的是,我上面没有把数据无效时输出的颜色设置成0,而是深蓝色,下面可以看到效果。然后呢,只要准备显示图像就可以了。打开PS,找一张图片,调节到16:10的比例,然后分辨率设置成640*200(去掉约束比例,因为CGA的像素不是正方形),接着设置模式为索引颜色,在这里选择自定义调色板,设置如下(也就是CGA的颜色,注意要按顺序设置,Wikipedia上有各个颜色的值):

随后你会发现,啊,这图片真丑……都不想做下去了……

保存图片为bmp,色深为4bpp,这样就是完全按照调色板的索引来记录图像了。接着写个小工具,把一个像素里面的两个颜色分开来(也就是浪费点空间,每个字节只存一个颜色),顺便把BMP从下到上的记录方式改回从上到下。用C语言不难实现,如下所示:

#define BMP_OFFSET 0x76
#define BMP_WIDTH 640
#define BMP_HEIGHT 200
fseek(fpo,0,SEEK_SET);
for (i = 0;i < BMP_HEIGHT; i++)
{
  fseek(fpi,BMP_OFFSET+(BMP_HEIGHT-i-1)*(BMP_WIDTH/2),SEEK_SET);
  fread(linebuf,(BMP_WIDTH/2),1,fpi);
  for (j = 0;j < (BMP_WIDTH/2);j++)
  {
    outputbuf[j*2] = linebuf[j] >> 4;
    outputbuf[j*2+1] = linebuf[j] & 0x0F;
  }
  fwrite(outputbuf, BMP_WIDTH, 1, fpo);
}

于是拿着转换好的bin烧进Flash,开FPGA接上显示器看看吧。

所有的16种颜色(记得之前讲的深蓝色底色吧):

以及显示图片的效果(CGA数字显示器还是好好用来显示文字吧)

闲聊

这次玩Verilog意外地顺利,可能是因为确实很简单吧……说起来我这台CGA显示器有点奇怪,我知道的IBM的CGA显示器只有IBM 5153一款,或者是IBM 5154可以向下兼容CGA。我这台确实是纯CGA,但是外观上明显和5153不同,比如铭牌上的文字、旋钮的图案等等,包括我这台是220V输入的,5153应该是110V的才对。然而显示器背面的铭文被撕掉了,也就无从得知它的型号。不过总体来说应该就是和5153差不多的东西。哦对了,我之前翻译过The 8bit Guy的一个关于CGA显卡的视频,b站地址:http://www.bilibili.com/video/av4667283/,有兴趣可以去看看。闲着研究研究这种古董也是挺有趣的hh。就这样,下次见。

参考资料