1. LCD显示原理
液晶屏显示画面时,屏幕从上到下逐行扫描,扫描完成后液晶屏就呈现一帧画面。然后屏幕回到初始位置进行下一次扫描。为了同步液晶屏的显示过程和液晶控制器,控制器会产生一系列的定时信号。当电子枪换行进行扫描时,控制器会发出一个水平同步信号,简称HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,控制器会发出一个垂直同步信号,简称 VSync。控制器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。
- 水平同步信号 HSYNC(行同步信号)
水平同步信号用于表示液晶屏一行像素数据的传输结束,每传输完成液晶屏的一行像素数据时,HSYNC 会发生电平跳变,如分辨率为 800x480 的显示屏,传输一帧图像 HSYNC 的电平会跳变 480 次
- 垂直同步信号 VSYNC(场同步信号)
垂直同步信号 VSYNC 用于表示液晶屏一帧像素数据的传输结束,每次传输完成一帧像素数据时,VSYNC 会发生电平跳变。其中 “帧” 是图像的单位,一副图像成为一帧,在液晶中,一帧指一个完整屏幕液晶像素点。常常用 “帧/秒” 来表示液晶屏的刷新特性,即液晶屏每秒钟可以显示多少帧图像,也通常成为 “fps”,如液晶屏以 60 帧每秒的速率运行时,VSYNC 每秒电平跳变 60 次,也成为 60 fps
- 数据使能信号 DE
数据使能信号 DE 用于表示数据的有效性,当 DE 信号线为高电平时,RGB 信号线表示数据有效
RGB LCD时间参数
RGB LCD行显示时序
- HSYNC信号有效时,表示一行数据的开始。
- HSPW表示HSYNC信号的脉冲宽度为(HSPW+1)个VCLK信号周期,即(HSPW+1)个像素,这(HSPW+1)个像素的数据无效。
- HSYNC信号脉冲之后,还要经过(HBP+1)个VCLK信号周期,有效的像素数据才出现。所以,在HSYNC有效之后,总共要经过(HSPW+1+HBP+1)个无效的像素,它对应左边框,第一个有效的像素才出现。
- 随后即连续发出(HOZVAL+1)个像素的有效数据。
- 最后是(HFP+1)个无效的像素,它对应右边框,完整的一行结束,紧接着就是下一行的数据了。
RGB LCD帧显示时序
- VSYNC信号有效时,表示一帧数据的开始。
- VSPW表示VSYNC信号的脉冲宽度为(VSPW+1)个HSYNC信号周期,即(VSPW+1)行,这(VSPW+1)行的数据无效。
- VSYNC信号脉冲之后,还要经过(VBP+1)个HSYNC信号周期,有效的行数据才出现。所以,在VSYNC信号有效后要经过(VSPW+1+VBP+1)个无效的行,第一个有效行才出现,对应上边框。
- 随后即连续发出(LINEVAL+1)行的有效数据。
- 最后是(VFPD+1)个无效的行,它对应下边框,完整的一帧结束,紧接着就是下一帧数据了。
RGB LCD屏幕时序参数
2. 字符图像
2.1 字符矩阵
我将四个字视为一个整体来取字模,需要注意的是图左下角“每行显示数据”是以字节(Byte)为单位的,而一个字节的数据为 8 个 bit,即可以表示一行点阵中的 8 个像素点。由于图 中的点阵每行为 128 个像素点,所以需要 16 个 Byte 的数据来表示一行,因此将“每行显示数据—点阵”处设置为 16。 数据以十六进制显示,每行有 16 个 Byte,对应每行四个汉字共 128 个像素点;共有32 行,对应每个汉字的高度为 32。
##天道酬勤字模
00000000000000000000000000000000;
00000000000000000000000000000000;
00000000000001000000001002080400;
000000800C0301C00002301C030C0700;
000001C0060181807FFF3218020C8600;
07FFFFE00301C300036031983FFFC600;
000300000300C20003603118020C0600;
000300000300841803603118020C0600;
00030000003FFFFC03603118020C0600;
00030000000030001FFE311803FC0608;
00030000000030001364311802687FFC;
00030000010420401364B19800610608;
000300207F87FFE01364B9580FFF8608;
00030070030600C01364BD780C610608;
3FFFFFF8030600C01264B5780C610608;
000340000307FFC01265B5380C610608;
00024000030600C01265A1180C610618;
00064000030600C0143C21180FFF0618;
00062000030600C0140421180C610418;
000620000307FFC01804211800610418;
000C1000030600C0100421181FFF8C18;
000C1800030600C01FFC611800600C18;
00180800030600C01004611800610818;
00300C000307FFC0100441180FFF9818;
00300600048600C01004411800601818;
00600300184600801004C11800603018;
00C003C0302000001FFC81180061A030;
010001F0301C00001007811801FE43F0;
0600007E000FFFFC100100183F0080F0;
0800003000007FF00002001810010060;
30000000000000000004001000060000;
00000000000000000000000000000000;
2.2 图像矩阵
在 Vivado 软件中,RAM 和 ROM 都是由 BMG IP 核(Block Memory Generator)配置生成的,ROM的配置过程和 RAM 类似在“Port A Options”选项卡中,设置 ROM 读端口的位宽和深度,因为我们的像素数据是“RGB888” 格式,所以端口位宽要设置成 24 位;使用“Notepad++”编辑器打开.coe 初始化文件,可以看到存储的数 据共有 10000 个数据,所以端口深度设置成 10000。与 RAM IP 核一样,我们同样不使用流水线寄存器。 “Port A Options”选项卡的设置如下图所示:
最重要的一部就是添加我们的.coe文件
最后点击 OK,完成对 ROM 的配置
3. 代码实现
3.1 IOBuffer
在vivado中,连接的管脚的信号一般都会自动添加OBUF或IBUF。但是对于inout类型的接口,不会主动添加IOBUF,因为in/out切换需要控制信号,需要用户自己分配好。IOBUF这个原语在Xilinx的原语手册有说明,主要作为三态端口使用,作用是把FPGA内部三态信号与外部的双向信号连接。
当信号T为1时,IOBUF作为输入,即外部信号输入到FPGA;T为0时IOBUF作为输出。实际使用时,方向引脚对应T,代码模块输出对应IOBUF的I,代码块输入对应IOBUF的O。需要注意代码块的引脚方向信号必须是1为输入,0为输出,IOBUF的IO脚直接连FPGA引脚。
assign语句实现
module PIOBUF
2 (
3 inout IO ,
4 input T ,
5 input I ,
6 output O
7 );
8
9 assign O = IO;
10 assign IO = ~T ? I : 1'bz;
11
12 endmodule
module PIOBUF
(
inout IO ,
input T ,
input I ,
output O
);
assign O = T ? IO : I;
assign IO = ~T ? I : 1'bz;
endmodule
以上便是最常见的两种三态门写法,很多双向口都是使用该方法。实际上这两种方式是一样的,assign语句是线型,所以当T为0时,直接把I的值赋给O与先把值赋给IO,IO再赋给O,结果是一样的。
在该LCD显示中,由于 lcd_rgb 是 24 位的双向引脚,所以这里对双向引脚的方向做一个切换。当 lcd_de 信号为高电平时,此时输出的像素数据有效,将 lcd_rgb 的引脚方向切换成输出,并将 LCD 驱动模块输出的 lcd_rgb_o(像素数据)连接至 lcd_rgb 引脚; 当 lcd_de 信号为低电平时,此时输出的像素数据无效,将 lcd_rgb 的引脚方向切换成输入。代码中将高阻状态“Z”赋值给 lcd_rgb 的引脚,表示此时 lcd_rgb 的引脚电平由外围电路决定,此时可以读取 lcd_rgb 的引脚电平,从而获取到 LCD 屏的 ID,获得了LCD的ID之后就便于LCD的时钟分频模块计算LCD的驱动时钟
//像素数据方向切换
32 assign lcd_rgb = lcd_de ? lcd_rgb_o : {24{1'bz}};
33 assign lcd_rgb_i = lcd_rgb;
原语实现
// IOBUF: Single-ended Bi-directional Buffer
2 // All devices
3 // Xilinx HDL Language Template, version 2020.1
4 IOBUF #(
5 .DRIVE(12), // Specify the output drive strength
6 .IBUF_LOW_PWR("TRUE"), // Low Power - "TRUE", High Performance = "FALSE"
7 .IOSTANDARD("DEFAULT"), // Specify the I/O standard
8 .SLEW("SLOW") // Specify the output slew rate
9 ) IOBUF_inst (
10 .O(O), // Buffer output
11 .IO(IO), // Buffer inout port (connect directly to top-level port)
12 .I(I), // Buffer input
13 .T(T) // 3-state enable input, high=input, low=output
14 );
15 // End of IOBUF_inst instantiation
根据Xilinx的IOBUF原语T=0时,I与O信号是连接的特性,可以实现一些接口的小妙用。比如对于音频接口I2S,如果RX与TX作为两个模块,在FPGA作为时钟WCLK,BCLK输出,同时使用数据输出输入时,那么WCLK,BCLK分别使用IOBUF连接可以简单的解决既能发送数据,也能采样接收的数据。也可以在GPIO使用,三态口接IOBUF,输出高低电平时可以通过读取输入得到实际输出状态。
3.2 字符图片显示
首先需要定义寄存器将我们的128*32位的字符矩阵寄存,即一行共128bit,共32行
//给字符数组赋值,显示汉字“天道酬勤”,每个汉字大小为 32*32
43 always @(posedge lcd_pclk) begin
44 char[0 ] <= 128'h00000000000000000000000000000000;
45 char[1 ] <= 128'h00000000000000000000000000000000;
46 char[2 ] <= 128'h00000000000100000000002000000000;
47 char[3 ] <= 128'h000000100001800002000070000000C0;
48 char[4 ] <= 128'h000000380001800003FFFFF803FFFFE0;
49 char[5 ] <= 128'h07FFFFFC0001800003006000000001E0;
50 char[6 ] <= 128'h0000C000000180600300600000000300;
51 char[7 ] <= 128'h0000C0000001FFF00300C00000000600;
52 char[8 ] <= 128'h0000C000000180000310804000001800;
53 char[9 ] <= 128'h0000C00000018000031FFFE000003000;
54 char[10] <= 128'h0000C00000018000031800400001C000;
55 char[11] <= 128'h0000C00000018000031800400001C000;
char[12] <= 128'h00C0C000018181800318004000018000;
57 char[13] <= 128'h00C0C00001FFFFC0031FFFC000018010;
58 char[14] <= 128'h00C0C060018001800318004000018038;
59 char[15] <= 128'h00C0FFF001800180031800403FFFFFFC;
60 char[16] <= 128'h00C0C000018001800318004000018000;
61 char[17] <= 128'h00C0C000018001800218004000018000;
62 char[18] <= 128'h00C0C00001800180021FFFC000018000;
63 char[19] <= 128'h00C0C000018001800210304000018000;
64 char[20] <= 128'h00C0C00001FFFF800200300000018000;
65 char[21] <= 128'h00C0C000018001800606300000018000;
66 char[22] <= 128'h00C0C000018001000607370000018000;
67 char[23] <= 128'h00C0C00000000000060E31C000018000;
68 char[24] <= 128'h00C0C000001000400418307000018000;
69 char[25] <= 128'h00C0C000020830600430303800018000;
70 char[26] <= 128'h00C0C010020C18300860301800018000;
71 char[27] <= 128'h00C0C038060E18180883700800018000;
72 char[28] <= 128'h3FFFFFFC0C0618181100F008003F8000;
73 char[29] <= 128'h000000001C0408182000600000070000;
74 char[30] <= 128'h00000000000000000000000000020000;
75 char[31] <= 128'h00000000000000000000000000000000;
76 end
然后通过计数器来显示对应点的LCD点亮或熄灭状态
//为 LCD 不同显示区域绘制图片、字符和背景色
79 always @(posedge lcd_pclk or negedge rst_n) begin
80 if (!rst_n)
81 pixel_data <= BACK_COLOR;
82 else if( (pixel_xpos >= PIC_X_START) && (pixel_xpos < PIC_X_START + PIC_WIDTH)
83 && (pixel_ypos >= PIC_Y_START) && (pixel_ypos < PIC_Y_START + PIC_HEIGHT) )
84 pixel_data <= rom_rd_data ; //显示图片
85 else if((pixel_xpos >= CHAR_X_START) && (pixel_xpos < CHAR_X_START + CHAR_WIDTH)
86 && (pixel_ypos >= CHAR_Y_START) && (pixel_ypos < CHAR_Y_START + CHAR_HEIGHT))
begin
87 if(char[y_cnt][CHAR_WIDTH -1'b1 - x_cnt])
88 pixel_data <= CHAR_COLOR; //显示字符
89 else
90 pixel_data <= BACK_COLOR; //显示字符区域的背景色
91 end
92 else
93 pixel_data <= BACK_COLOR; //屏幕背景色
94 end
其中代码第82~83行:利用行场坐标来限制图像的输出范围。