前言
今天学习正点原子领航者开发板RGB LCD屏幕模块,以下是记录的笔记和重写的代码。
1: LCD屏幕
基本原理:在两块平行玻璃板中填充液晶材料,通过电场控制液晶分子旋转从而达到透光和遮光的目的。
分辨率、像素格式、驱动时序是lcd屏幕最重要三个要素。
1.1 分辨率
1.2 像素格式
RGB:由红 绿 蓝三种颜色通道构成,这三种颜色的分量叠加决定实际颜色;通常,会给RGB图像加一个通道alpha,即透明度,这样就会有四个分量共同控制颜色。
YUV:YUV是编译true-color颜色空间(color space)的种类,Y'UV, YUV, YCbCr,YPbPr等专有名词都可以称为YUV,彼此有重叠。“Y”表示明亮度(Luminance或Luma),也就是灰阶值,“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。
RGB更加细腻准确,YUV有颜色损失。RGB888,一个像素点,红色8bit,绿色8bit,蓝色8bit,24bit数值。YUV444或者YUV422数值占的少,但YUV的带宽要求低,适合数字传输。
R = Y + 1.403V
G = Y - 0.344U - 0.714V
B = Y + 1.770U
1.3 驱动时序
VSYNC控制图片切换,HSYNC信号控制像素点换行(行同步信号),行同步前后沿HFP,HBP表示电子枪等待它移动换行的时间。场同步前后沿VFP,VBP表示电子枪等待从最后一个像素点到第一个像素点的转换。消隐时间不显示任何的像素点,然后逐渐从一个像素点到一个像素点,一行像素点到一行像素点,一页像素点到一页像素点的转换,然后经过等待时间,出现画面。
LCD屏幕时序参数表
显示分辨率 | 时钟(Mhz) | 行时序(像素数) | 帧时序(行数) | ||||||||
行同步 | 显示后沿 | 显示区域 | 显示前沿 | 显示周期 | 场同步 | 显示后沿 | 显示区域 | 显示前沿 | 显示 周期 | ||
HSPW | HBP | HOZVAL | HFP | HSYNC | VSPW | VBP | LINE | VEP | VSYNC | ||
480*272 | 9 | 41 | 2 | 480 | 2 | 525 | 10 | 2 | 272 | 2 | 286 |
800*480 | 33.3 | 128 | 88 | 800 | 40 | 1056 | 2 | 33 | 480 | 10 | 525 |
1024*600 | 50 | 20 | 140 | 1024 | 160 | 1344 | 3 | 20 | 600 | 12 | 635 |
1280*800 | 70 | 10 | 80 | 1280 | 70 | 1440 | 3 | 10 | 800 | 10 | 823 |
信号线 | 描述 |
R[7:0] | 8根红色数据线 |
G[7:0] | 8根绿色数据线 |
B[7:0] | 8根蓝色数据线 |
DE | 数据使能线 |
VSYNC | 垂直同步信号线 |
HSYNC | 水平同步信号线 |
PCLK | 像素时钟信号线 |
2: 看波形——写代码
本节的实验任务是使用正点原子开发板上的RGB TFT-LCD接口,驱动RGB LCD液晶屏(支持目前推出的所有RGB LCD屏),并显示出彩条。
2.1 rd_id模块
代码分析:
4.3' RGB LCD RES:480x272
7' RGB LCD RES:800x480
7' RGB LCD RES:1024x600
4.3' RGB LCD RES:800x480
10' RGB LCD RES:1280x800
获取rd_flag和lcd_id的数据:复位信号开始后,rd_flag变为1,同时获取LCD ID M2:B7 (lcd_rgb[7]) M1:G7 (lcd_rgb[15]) M0:R7 (lcd_rgb[23])相关的数值,根据以上数值,分别对应写出其lcd_id对应的识别号。
module rd_id(
input sys_clk ,
input sys_rst_n ,
input [23:0] lcd_rgb ,
output reg [15:0] lcd_id
);
reg lcd_flag;
//获取rd_flag和lcd_id的数据
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
lcd_flag <= 1'b0 ;
lcd_id <= 16'b0 ;
end
else if(lcd_flag == 0) begin
lcd_flag <= 1'b1 ;
case({lcd_rgb[7],lcd_rgb[15],lcd_rgb[23]})
3'b000 : lcd_id <= 16'h4342; //4.3' RGB LCD RES:480x272
3'b001 : lcd_id <= 16'h7084; //7' RGB LCD RES:800x480
3'b010 : lcd_id <= 16'h7016; //7' RGB LCD RES:1024x600
3'b100 : lcd_id <= 16'h4384; //4.3' RGB LCD RES:800x480
3'b101 : lcd_id <= 16'h1018; //10' RGB LCD RES:1280x800
default: lcd_id <= 16'b0 ;
endcase
end
end
endmodule
2.2 clk_div模块
代码分析:
1:时钟2分频 输出25MHz时钟:复位后,clk_25m初始值为0,遇到上升沿,那么clk_25m就翻转一次。
2:时钟4分频 输出12.5MHz时钟(由div_4_cnt推出):复位后,clk_12_5m初始值为0,一遇到上升沿div_4_cnt就翻转一次。当div_4_cnt为1的时候,clk_12_5m翻转一次,其他时刻保持不变。
3:组合逻辑使不同lcd_id赋值不同的时钟周期。
module clk_div(
input sys_clk ,
input sys_rst_n ,
input [15:0] lcd_id ,
output reg lcd_pclk
);
reg div_4_cnt ;
reg clk_25m ;
reg clk_12_5m ;
//时钟2分频 输出25MHz时钟
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
clk_25m <= 1'b0 ;
else
clk_25m <= ~clk_25m ;
end
//时钟4分频 输出12.5MHz时钟
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
clk_12_5m <= 1'b0 ;
div_4_cnt <= 1'b0 ;
end
else begin
div_4_cnt <= ~div_4_cnt;
if(div_4_cnt)
clk_12_5m <= ~clk_12_5m;
else
clk_12_5m <= clk_12_5m ;
end
end
always @(*) begin
case(lcd_id)
16'h4342 : lcd_pclk = clk_12_5m ;
16'h7084 : lcd_pclk = clk_25m ;
16'h7016 : lcd_pclk = sys_clk ;
16'h4384 : lcd_pclk = clk_25m ;
16'h1018 : lcd_pclk = sys_clk ;
default : lcd_pclk = 1'b0 ;
endcase
end
endmodule
2.3 lcd_driver模块
pixel_data像素数据本节未使用,所以用省略号替代。如下图,为了产生行同步时序de信号,其是在行同步信号(128),行同步后延(88)之后的有效显示区域(800)拉高,行同步前沿(40)拉低。具体拉高拉低,得与场同步信号配合来定。以800*480的屏幕为例,h_vnt是行同步计数器,行同步信号+行同步后延为(0~215),有效显示区域(216~1015)拉高,行同步前沿(1016~1055)。
v_cnt是场同步计数器,每次行同步完成一次(0~1055),就记一次。场同步计数器是在场同步信号(2),显示后延(33)之后拉高同步时序de信号,因此是在同步信号+显示后延(0~34)之后,每次行同步信号的有效显示区域(216~1015)拉高使能信号。
data_req是数据准备信号,在使能信号拉高之前一个周期信号就拉高。pixel_xpos和pixel_ypos简单理解为点亮像素点和点亮行数,每次使能信号点亮一行里面的800个像素点,每次点亮1行。
代码分析:
代码里面没有行显示前沿,场显示前沿的原因在于,根本没用到,没必要设置。计数器已经把行的总周期数和场总周期数设置好了,只要在有效显示区域拉高en信号就行。
1:行场时序参数:写出不同屏幕,不同模块对应的行场同步数据(easy)。根据lcd_id,用组合逻辑case的形式写出对应不同的数据。
2:RGB LCD 采用DE模式:行场同步信号lcd_hs和lcd_vs需要拉高。背光控制信号lcd_b1和复位信号lcd_rst也拉高,不让复位,lcd_clk与lcd_pclk一致。
3:行计数器h_cnt对像素时钟计数:复位后,h_cnt为0,当行计数器h_cnt到达h_total-1的时候,计数器清零,否则加1。
4:场计数器v_cnt对行计数:复位后,场计数器v_cnt为0,当行计数器h_cnt到达h_total-1且场计数器v_cnt到达v_total-1的时候,计数器清零,否则加1。
5:请求像素点颜色数据输入data_req:代码中h_sync + h_back - 2'd2,行时序减2的原因是从零开始需要减一,又因为提前一个周期数据请求,所以再次减一。h_cnt < h_sync + h_back + h_disp - 2'd2,行时序减2的原因是从零开始需要减一,又因为提前一个周期数据请求,所以再次减一,而且这里必须用小于号。场时序不减的原因是,他会持续很长一段时间,35行的时间足够大,所以第35行的时候有足够的时间让他去拉高(具体看波形图就理解了)。
always@ (posedge lcd_pclk or negedge rst_n) begin
if(!rst_n)
data_req<=1'b0;
else if((h_cnt >= h_sync + h_back - 2'd2) && (h_cnt < h_sync + h_back + h_disp - 2'd2)
&& (v_cnt >= v_sync + v_back) && (v_cnt < v_sync + v_back + v_disp))
data_req <= 1'b1;
else
data_req <= 1'b0;
end
6:数据使能信号:和data_req保持一致,用时序语句赋值,晚一个周期刚刚好。
7:像素点x坐标和像素点y坐标(由数据请求信号data_req推出):
这里有两种不同的写法一个是用时序if else,一个是用assign三目运算符。
时序if else:
always@ (posedge lcd_pclk or negedge rst_n) begin
if(!rst_n)
pixel_xpos <= 11'd0;
else if(data_req)
pixel_xpos <= h_cnt + 2'd2 - h_sync - h_back ;
else
pixel_xpos <= 11'd0;
end
data_req拉高的时候,pixel_xpos <= h_cnt + 2'd2 - h_sync - h_back,加2的原因是pixel_xpos是从1开始的,而计数器是从0开始计时,所以得先加个1。其次是因为data_req信号比使能信号早一个周期,时序赋值也得加个1。
assign三目运算符:
assign pixel_xpos = lcd_de ? (h_cnt + 1'b1 - h_sync - h_back) : 11'd0;
这里用assign语句,没有时序的问题,h_cnt + 1'b1 - h_sync - h_back,加1的原因是pixel_xpos是从1开始的,而计数器是从0开始计时,所以得先加个1。
module lcd_driver(
input lcd_pclk , //时钟
input sys_rst_n , //复位,低电平有效
input [15:0] lcd_id , //LCD屏ID
input [23:0] pixel_data , //像素数据
output [10:0] pixel_xpos , //当前像素点横坐标
output [10:0] pixel_ypos , //当前像素点纵坐标
output reg [10:0] h_disp , //LCD屏水平分辨率
output reg [10:0] v_disp , //LCD屏垂直分辨率
output reg data_req , //数据请求信号
//RGB LCD接口
output reg lcd_de , //LCD 数据使能信号
output lcd_hs , //LCD 行同步信号
output lcd_vs , //LCD 场同步信号
output lcd_bl , //LCD 背光控制信号
output lcd_clk , //LCD 像素时钟
output lcd_rst , //LCD复位
output [23:0] lcd_rgb //LCD RGB888颜色数据
);
//parameter define
// 4.3' 480*272
parameter H_SYNC_4342 = 11'd41; //行同步
parameter H_BACK_4342 = 11'd2; //行显示后沿
parameter H_DISP_4342 = 11'd480; //行有效数据
parameter H_FRONT_4342 = 11'd2; //行显示前沿
parameter H_TOTAL_4342 = 11'd525; //行扫描周期
parameter V_SYNC_4342 = 11'd10; //场同步
parameter V_BACK_4342 = 11'd2; //场显示后沿
parameter V_DISP_4342 = 11'd272; //场有效数据
parameter V_FRONT_4342 = 11'd2; //场显示前沿
parameter V_TOTAL_4342 = 11'd286; //场扫描周期
// 7' 800*480
parameter H_SYNC_7084 = 11'd128; //行同步
parameter H_BACK_7084 = 11'd88; //行显示后沿
parameter H_DISP_7084 = 11'd800; //行有效数据
parameter H_FRONT_7084 = 11'd40; //行显示前沿
parameter H_TOTAL_7084 = 11'd1056; //行扫描周期
parameter V_SYNC_7084 = 11'd2; //场同步
parameter V_BACK_7084 = 11'd33; //场显示后沿
parameter V_DISP_7084 = 11'd480; //场有效数据
parameter V_FRONT_7084 = 11'd10; //场显示前沿
parameter V_TOTAL_7084 = 11'd525; //场扫描周期
// 7' 1024*600
parameter H_SYNC_7016 = 11'd20; //行同步
parameter H_BACK_7016 = 11'd140; //行显示后沿
parameter H_DISP_7016 = 11'd1024; //行有效数据
parameter H_FRONT_7016 = 11'd160; //行显示前沿
parameter H_TOTAL_7016 = 11'd1344; //行扫描周期
parameter V_SYNC_7016 = 11'd3; //场同步
parameter V_BACK_7016 = 11'd20; //场显示后沿
parameter V_DISP_7016 = 11'd600; //场有效数据
parameter V_FRONT_7016 = 11'd12; //场显示前沿
parameter V_TOTAL_7016 = 11'd635; //场扫描周期
// 10.1' 1280*800
parameter H_SYNC_1018 = 11'd10; //行同步
parameter H_BACK_1018 = 11'd80; //行显示后沿
parameter H_DISP_1018 = 11'd1280; //行有效数据
parameter H_FRONT_1018 = 11'd70; //行显示前沿
parameter H_TOTAL_1018 = 11'd1440; //行扫描周期
parameter V_SYNC_1018 = 11'd3; //场同步
parameter V_BACK_1018 = 11'd10; //场显示后沿
parameter V_DISP_1018 = 11'd800; //场有效数据
parameter V_FRONT_1018 = 11'd10; //场显示前沿
parameter V_TOTAL_1018 = 11'd823; //场扫描周期
// 4.3' 800*480
parameter H_SYNC_4384 = 11'd128; //行同步
parameter H_BACK_4384 = 11'd88; //行显示后沿
parameter H_DISP_4384 = 11'd800; //行有效数据
parameter H_FRONT_4384 = 11'd40; //行显示前沿
parameter H_TOTAL_4384 = 11'd1056; //行扫描周期
parameter V_SYNC_4384 = 11'd2; //场同步
parameter V_BACK_4384 = 11'd33; //场显示后沿
parameter V_DISP_4384 = 11'd480; //场有效数据
parameter V_FRONT_4384 = 11'd10; //场显示前沿
parameter V_TOTAL_4384 = 11'd525; //场扫描周期
//reg define
reg [10:0] h_sync ;
reg [10:0] h_back ;
reg [10:0] h_total;
reg [10:0] v_sync ;
reg [10:0] v_back ;
reg [10:0] v_total;
reg [10:0] h_cnt ;
reg [10:0] v_cnt ;
//RGB888数据输出
assign lcd_rgb = lcd_de ? pixel_data : 24'd0;
//1:行场时序参数
always @(*) begin
case(lcd_id)
16'h4342 : begin
h_sync = H_SYNC_4342;
h_back = H_BACK_4342;
h_disp = H_DISP_4342;
h_total = H_TOTAL_4342;
v_sync = V_SYNC_4342;
v_back = V_BACK_4342;
v_disp = V_DISP_4342;
v_total = V_TOTAL_4342;
end
16'h7084 : begin
h_sync = H_SYNC_7084;
h_back = H_BACK_7084;
h_disp = H_DISP_7084;
h_total = H_TOTAL_7084;
v_sync = V_SYNC_7084;
v_back = V_BACK_7084;
v_disp = V_DISP_7084;
v_total = V_TOTAL_7084;
end
16'h7016 : begin
h_sync = H_SYNC_7016;
h_back = H_BACK_7016;
h_disp = H_DISP_7016;
h_total = H_TOTAL_7016;
v_sync = V_SYNC_7016;
v_back = V_BACK_7016;
v_disp = V_DISP_7016;
v_total = V_TOTAL_7016;
end
16'h4384 : begin
h_sync = H_SYNC_4384;
h_back = H_BACK_4384;
h_disp = H_DISP_4384;
h_total = H_TOTAL_4384;
v_sync = V_SYNC_4384;
v_back = V_BACK_4384;
v_disp = V_DISP_4384;
v_total = V_TOTAL_4384;
end
16'h1018 : begin
h_sync = H_SYNC_1018;
h_back = H_BACK_1018;
h_disp = H_DISP_1018;
h_total = H_TOTAL_1018;
v_sync = V_SYNC_1018;
v_back = V_BACK_1018;
v_disp = V_DISP_1018;
v_total = V_TOTAL_1018;
end
default : begin
h_sync = H_SYNC_4342;
h_back = H_BACK_4342;
h_disp = H_DISP_4342;
h_total = H_TOTAL_4342;
v_sync = V_SYNC_4342;
v_back = V_BACK_4342;
v_disp = V_DISP_4342;
v_total = V_TOTAL_4342;
end
endcase
end
//2:RGB LCD 采用DE模式
assign lcd_hs = 1'b1 ;
assign lcd_vs = 1'b1 ;
assign lcd_bl = 1'b1 ;
assign lcd_rst = 1'b1 ;
assign lcd_clk = lcd_pclk;
//3:行计数器h_cnt对像素时钟计数
always @(posedge lcd_pclk or negedge sys_rst_n) begin
if(!sys_rst_n)
h_cnt <= 11'b0;
else if(h_cnt == h_total - 1'b1)
h_cnt <= 11'b0;
else
h_cnt <= h_cnt + 1'b1;
end
//4:场计数器v_cnt对行计数
always @(posedge lcd_pclk or negedge sys_rst_n) begin
if(!sys_rst_n)
v_cnt <= 11'b0;
else if(h_cnt == h_total - 1'b1)
v_cnt <= v_cnt + 1'b1;
else
v_cnt <= v_cnt;
end
//5:请求像素点颜色数据输入data_req
always @(posedge lcd_pclk or negedge sys_rst_n) begin
if(!sys_rst_n)
data_req <= 1'b0;
else if((h_cnt >= h_sync + h_back - 2'd2) &&(h_cnt < h_disp + h_sync + h_back - 2'd2)
&& (v_cnt >= v_sync + v_back) && (v_cnt < v_disp + v_sync + v_back))
data_req <= 1'b1;
else
data_req <= 1'b0;
end
//6:数据使能信号
always @(posedge lcd_pclk or negedge sys_rst_n) begin
if(!sys_rst_n)
lcd_de <= 1'b0 ;
else
lcd_de <= data_req ;
end
//7:像素点x坐标和像素点y坐标
assign pixel_xpos = data_req ? h_cnt + 1'b1 - h_sync - h_back : 11'b0;
assign pixel_ypos = data_req ? v_cnt + 1'b1 - v_sync - v_back : 11'b0;
endmodule
2.4 lcd_display模块
代码分析:
复位前,初始色为黑色。
白色(FFFFFF)—— pixel_xpos:1~160,黑色(000000)—— pixel_xpos:161~320
红色(FF0000)—— pixel_xpos:320~480,绿色(00FF00)—— pixel_xpos:480~640
蓝色(0000FF)—— pixel_xpos:640~800
module lcd_display(
input lcd_pclk , //时钟
input sys_rst_n , //复位,低电平有效
input [10:0] pixel_xpos , //当前像素点横坐标
input [10:0] pixel_ypos , //当前像素点纵坐标
input [10:0] h_disp , //LCD屏水平分辨率
input [10:0] v_disp , //LCD屏垂直分辨率
output reg [23:0] pixel_data //像素数据
);
//定义颜色
parameter WHITE = 24'hFFFFFF; //白色
parameter BLACK = 24'h000000; //黑色
parameter RED = 24'hFF0000; //红色
parameter GREEN = 24'h00FF00; //绿色
parameter BLUE = 24'h0000FF; //蓝色
always @(posedge lcd_pclk or negedge sys_rst_n) begin
if(!sys_rst_n)
pixel_data <= BLACK;
else if((pixel_xpos >= 24'b0) && (pixel_xpos < h_disp*1/5))
pixel_data <= WHITE;
else if((pixel_xpos >= h_disp*1/5) && (pixel_xpos < h_disp*2/5))
pixel_data <= BLACK;
else if((pixel_xpos >= h_disp*2/5) && (pixel_xpos < h_disp*3/5))
pixel_data <= RED ;
else if((pixel_xpos >= h_disp*3/5) && (pixel_xpos < h_disp*4/5))
pixel_data <= GREEN;
else
pixel_data <= BLUE ;
end
endmodule
2.5 顶层模块
三态门电路:
数据双向传输三态门:具体内容原理看数电课程,王文俊老师的数模电讲的都非常好,特别喜欢他的课。参考资料:数字电子技术基础 3.11 CMOS传输门及三态输出的门电路_哔哩哔哩_bilibili
inout_en拉高的时候,代表这个三态门当做输出来使用,通过out模块传输给外设模块,作为输出使用。当 inout_en拉低的时候,代表这个三态门作为输入使用,数据从总线传输到进内部,作为输入使用。
代码分析:
调用一个三态门,调用四个模块,很简单。
module lcd_rgb_colorbar(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位
//RGB LCD接口
output lcd_de, //LCD 数据使能信号
output lcd_hs, //LCD 行同步信号
output lcd_vs, //LCD 场同步信号
output lcd_bl, //LCD 背光控制信号
output lcd_clk, //LCD 像素时钟
output lcd_rst, //LCD 复位
inout [23:0] lcd_rgb //LCD RGB888颜色数据
);
wire [15:0] lcd_id ; //LCD屏ID
wire lcd_pclk ; //LCD像素时钟
wire [10:0] pixel_xpos; //当前像素点横坐标
wire [10:0] pixel_ypos; //当前像素点纵坐标
wire [10:0] h_disp ; //LCD屏水平分辨率
wire [10:0] v_disp ; //LCD屏垂直分辨率
wire [23:0] pixel_data; //像素数据
wire [23:0] lcd_rgb_o ; //输出的像素数据
wire [23:0] lcd_rgb_i ; //输入的像素数据
assign lcd_rgb = lcd_de ? lcd_rgb_o : {24{1'bz}};
assign lcd_rgb_i = lcd_rgb;
//读LCD ID模块
rd_id u_rd_id(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n),
.lcd_rgb (lcd_rgb_i),
.lcd_id (lcd_id )
);
//时钟分频模块
clk_div u_clk_div(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n),
.lcd_id (lcd_id ),
.lcd_pclk (lcd_pclk )
);
//LCD显示模块
lcd_display u_lcd_display(
.lcd_pclk (lcd_pclk ),
.sys_rst_n (sys_rst_n ),
.pixel_xpos (pixel_xpos),
.pixel_ypos (pixel_ypos),
.h_disp (h_disp ),
.v_disp (v_disp ),
.pixel_data (pixel_data)
);
//LCD驱动模块
lcd_driver u_lcd_driver(
.lcd_pclk (lcd_pclk ),
.sys_rst_n (sys_rst_n ),
.lcd_id (lcd_id ),
.pixel_data (pixel_data),
.pixel_xpos (pixel_xpos),
.pixel_ypos (pixel_ypos),
.h_disp (h_disp ),
.v_disp (v_disp ),
.data_req ( ),
.lcd_de (lcd_de ),
.lcd_hs (lcd_hs ),
.lcd_vs (lcd_vs ),
.lcd_bl (lcd_bl ),
.lcd_clk (lcd_clk ),
.lcd_rst (lcd_rst ),
.lcd_rgb (lcd_rgb_o )
);
endmodule
参考内容
- 正点原子 领航者ZYNQ开发板资料盘(http://www.openedv.com/docs/)
- ZYNQ领航者V2开发板