1.实验说明
1.1 管脚说明
set_pin_assignment { clk_ref } { LOCATION = P11; IOSTANDARD = LVCMOS33; }
set_pin_assignment { lcd_bl_out } { LOCATION = P4; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment { lcd_cs_out } { LOCATION = P10; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment { lcd_dc_out } { LOCATION = P13; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment { lcd_rst_n_out } { LOCATION = P16; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment { lcd_data_out } { LOCATION = P18; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment { lcd_clk_out } { LOCATION = P23; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
- clk_ref,系统输入时钟,对应于开发板上的10M时钟,即P128
- clk_osc, 输出系统的时钟用于测试,本工程中无用
- lcd_bl_out, LCD 背光, 对应于模块的BLK,连接到开发板的P111
- lcd_cs_out, LCD片选,对应于模块的CS,连P106
- lcd_dc_out, LCD 用于指示当前传送的是命令还是数据, 对于应于模块上的CS,连P104
- lcd_rst_n_out, LCD复位,对应于模块上的RES
- lcd_data_out, LCD串行数据,对应模块上的SDA
- lcd_clk_out, LCD串行时钟,对应模块上的SCL
可以看到模块的PCB丝印上有明确标识管脚信号。
电源正接开发板的3V3,GND接开发板的GND。
1.2 实验现象
下载程序之后可以看到LCD上循环往复地刷着红、绿、蓝屏。
2.原理说明
2.1 TFT LCD原理
TFT-LCD(Thin Film Tube-Liquid Crystal Display,薄膜晶体管–液晶显示器)最初作为笔
记本电脑、文字处理器的显示装置发展起来,现已扩展应用于计算机、游戏机、弹子机、摄
像机、车载电视等,从小型的到大型的机器,在各个领域都得到广泛应用。
液晶同固态晶体一样,具有特异的光学各向异性。而且这种光学各向异性伴随分子排列
结构的不同将呈现不同的光学形态。分子取向一旦发生变化,这些光学特性将随之变化,于
是在液晶中传输的光就受到调制。由此可见,变更分子的排列状态即可实现光调制。利用外
加电场即可改变液晶分子的取向,产生调制。这种由电场产生的光调制现象叫作液晶的“电光
效应”,它是液晶显示的基础。液晶不但具有固态晶体的光学特性,还具有液态的流动特性。
它的物理特性包括:黏性、弹性和极化性。黏性和弹性,使其对于方向不同的作用力具有不
同的效果,可产生自然偏转现象;极化性使液晶在受到外加电场作用时,很容易产生感应偶
极性,形成光电效应。
液晶显示器根据它的成像原理不同分为TN-LCD(Twist Nematic,扭曲向列)液晶显示
器、STN-LCD(Super Twist Nematic,超扭曲向列)液晶显示器、DSTN-LCD(Double Super
Twist Nematic,双层超扭曲向列)液晶显示器和 TFT-LCD 液晶显示器。目前,计算机所使用
的液晶显示器多使用 TFT-LCD 薄膜晶体管型。
液晶显示器显像原理
液晶显示器的显像原理,是将液晶置于两片导电玻璃之间,靠两个电极间电场的驱动,
引起液晶分子扭曲向列的电场效应,以控制光源透射或遮蔽功能,在电源关开之间产生明暗
而将图像显示出来。
TFT-LCD显示器的核心是液晶屏,其结构是在两块玻璃基板中间充斥着运动的液晶分
子。信号电压直接控制薄膜晶体管的开关状态,再利用晶体管控制液晶分子,来实现图像的
显示。LCD屏的结构如同“三明治”,即在两片玻璃基板内夹着彩色滤光片、偏光板、配向膜
等材料,灌入液晶材料,最后封装成一个液晶盒。LCD 屏的结构如图所示。
如果给液晶显示器的 LCD 面板中加上彩色滤光片,则可显示彩色影像。在彩色 LCD 面
板中,每一个像素都是由 3 个液晶单元格构成,其中每一个单元格前面都分别有红色、绿色
或蓝色的过滤片。光线经过过滤片处理后照射到每个像素中不同色彩的液晶单元格上,利用
三基色原理组合出不同的色彩。彩色液晶显示器面板结构如图所示。
因液晶屏本身没有发光功能,这就需要在液晶屏后加一个照明系统,该背光照明系统
能使光线均匀照射在液晶表面。现在发光部件的主流为被称作冷阴极管的荧光灯管。其发
光原理与室内照明用的热阴管类似,但不需像热阴管那样先预热灯丝,它在较低温状态就
能点亮,因此叫冷阴极管。但要驱动这种冷阴极管需要能输出1000~1500V交流电压的
特殊电源。逆变器单元就是驱动这种冷阴极管用的小型电源,是液晶显示装置中最重要的
功能部件之一。
液晶面板的结构及工作原理
液晶面板由上下两块玻璃基板组成,中间由液晶材料均匀间隔隔开。因为液晶材料本身
并不发光,所以必须依靠两边的背光灯管作为光源,而在液晶显示屏背面上的导光板让光线
往液晶方向前进,并通过反光膜和散射板将光线均匀地分布到各个区域去,给液晶材料一个
均匀明亮的光源。在玻璃基板与液晶材料之间安装行、列电极,并在行与列的交叉点上,通
过控制电压来达到液晶的旋光状态。LCD 是由两个互相垂直的极化滤光器构成,中间充满了
扭曲的液晶材料,光线穿出第一个滤光器后会使液晶扭转 90,最后从第二个过滤器中穿出。
另外,在液晶材料的周围设置控制电路和驱动电路,在电路的控制下使行、列电极产生电场,
并在不同的电场作用下,液晶分子会作规则的 90旋转排列,这样,在电源接通与断开的转
换过程中产生明暗的差别,依此原理控制每个像素的明暗变化,便可构成所需显示的图像。
液晶面板的结构如图所示。
液晶屏驱动电路
液晶屏驱动电路由图像处理器、时序转换电路、行驱动电路、列驱动电路、直流电压转
换电路等组成。液晶屏驱动电路组成框图如图所示。
本模块中的ST7735 IC就实现了上面框图中除开图像处理器的部分,FPGA在本实验中就相当于是图像处理器,只不过本模块的驱动接不是图上的RGB行场(HS VS DE)接口,而是SPI口。
2.2 ST7735
你会发现在模块的板子上根本找不着ST7735,因为该模块的ST7735 IC其实是在TFT屏上的,它是TFT屏的驱动IC,驱动IC一般绑定在TFT的玻璃上,这也是行业趋势。
先看下模块提供的资料。我们主要关心下面这两个文档,
- ST7735.pdf
- ST7735S_V1.1_20111121.pdf
ST7735.pdf是7735的驱动IC规格书,ST7735S是本模块的驱动IC规格书,二者之间在寄存器上没有啥区别,对于我们编程而言更容易找到ST7735的驱动及编程资料,所以主要看ST7735的寄存器。
ST7735的支持多种接口,包括:
- -Parallel 8080-series MCU Interface
(8-bit, 9-bit, 16-bit & 18-bit)
-3-line serial interface
-4-line serial interface
而我们的模块只接出来了SPI模式,而我们的工程用的是4线SPI模式,从规书上我们可以看到它的接口时序,
比较简单,进行通讯前先CS(图中CSX)拉低选中片选,然后SCL发时钟,SDA发像素数据或者命令,DC(图中D/CX)用于驱动SDA传的是像素数据还是命令。
从下图可以看到SDA是先传高位再传低位。
4线串行模式的时序图如下:
4线模式执行RDID的命令时序如下:
更多时序图参考规格书的第9.4章。
上面只是讲了接口,但具体怎么驱动呢?简单讲这么两个步骤:
- 发送初始化命令初始化寄存器,由于寄存器很多具体怎么设我们不必细究,一般参考厂家提供的初始化列表,或者是C代码的初始化。
- 发送RAM写入指令,进行色彩数据的写入。
由于ST7735本身带SRAM显存,因此我们不需要像裸屏一样进行TFT的周期刷新,这个过程由ST7735完成了。
3.代码分析
工程与本实验相关的代码在LCD_ST7735S.v中
module LCD_st7735s #
(
parameter LCD_W = 8'd132, //液晶屏像素宽度
parameter LCD_H = 8'd162 //液晶屏像素高度
)
(
input clk_in, //12MHz系统时钟
input rst_n_in, //系统复位,低有效
output reg ram_lcd_clk_en, //RAM时钟使能
output reg [7:0] ram_lcd_addr, //RAM地址信号
input [LCD_W-1:0] ram_lcd_data, //RAM数据信号, 一次写一行,每个像素只有两种色彩
output reg lcd_rst_n_out, //LCD液晶屏复位
output reg lcd_bl_out, //LCD背光控制
output reg lcd_dc_out, //LCD数据指令控制
output reg lcd_clk_out, //LCD时钟信号
output reg lcd_data_out //LCD数据信号
);
需要注意由于PotatoPie本身内部RAM比较小,所以这个例程中对像素数据做了简化处理,每个像素精简为1位,默认情况下该位为1表示红色,该位为0表示黑色,但程序中为了演示对色彩进行了变化处理。
这段代码定义了色彩常量,以及初始化数据的长度
localparam INIT_DEPTH = 16'd75; //LCD初始化的命令及数据的数量
localparam RED = 16'hf800; //红色
localparam GREEN = 16'h07e0; //绿色
localparam BLUE = 16'h001f; //蓝色
localparam BLACK = 16'h0000; //黑色
localparam WHITE = 16'hffff; //白色
localparam YELLOW = 16'hffe0; //黄色
这估定义了状态机的状态
localparam IDLE = 3'd0; // 空闲态
localparam MAIN = 3'd1; // 主态
localparam INIT = 3'd2; // 初始态
localparam SCAN = 3'd3; // 写色彩数据
localparam WRITE = 3'd4; // 写操作
localparam DELAY = 3'd5; // 延时
接下来的代码在复位时为初始化寄存器RAM reg_init 进行赋值,每一个寄存器的值的含义可以对照ST7735规格书中的寄存器表进行含义解读,注释中也作了说明。
reg_setxy[0] <= {1'b0,8'h2a}; // 显示区域列地址
reg_setxy[1] <= {1'b1,8'h01}; // 显示区域区域起始X
reg_setxy[2] <= {1'b1, 8'd0};
reg_setxy[3] <= {1'b1,8'h00}; // 显示区域区域结束X
reg_setxy[4] <= {1'b1,8'd160};
reg_setxy[5] <= {1'b0,8'h2b}; // 显示区域行地址
reg_setxy[6] <= {1'b1,8'h00}; // 显示区域区域起始Y
reg_setxy[7] <= {1'b1, 8'd26 /*8'h1a*/};
reg_setxy[8] <= {1'b1,8'h00}; // 显示区域结束Y
reg_setxy[9] <= {1'b1, 8'd105 };
reg_setxy[10] <= {1'b0,8'h2c}; // mem wirte
reg_init[0] <= {1'b0,8'h11}; //Sleep out
//delay 120ms
reg_init[1] <= {1'b0,8'hb1}; //Normal mode
reg_init[2] <= {1'b1,8'h05};
reg_init[3] <= {1'b1,8'h3a};
reg_init[4] <= {1'b1,8'h3a};
reg_init[5] <= {1'b0,8'hb2}; //Idle mode
reg_init[6] <= {1'b1,8'h05};
reg_init[7] <= {1'b1,8'h3a};
reg_init[8] <= {1'b1,8'h3a};
reg_init[9] <= {1'b0,8'hb3}; //Partial mode
reg_init[10] <= {1'b1,8'h05};
reg_init[11] <= {1'b1,8'h3a};
reg_init[12] <= {1'b1,8'h3a};
reg_init[13] <= {1'b1,8'h05};
reg_init[14] <= {1'b1,8'h3a};
reg_init[15] <= {1'b1,8'h3a};
reg_init[16] <= {1'b0,8'hb4}; //Dot inversion
reg_init[17] <= {1'b1,8'h03};
reg_init[18] <= {1'b0,8'hc0}; //AVDD GVDD
reg_init[19] <= {1'b1,8'h62};
reg_init[20] <= {1'b1,8'h02};
reg_init[21] <= {1'b1,8'h04};
reg_init[22] <= {1'b0,8'hc1}; //VGH VGL
reg_init[23] <= {1'b1,8'hc0};
reg_init[24] <= {1'b0,8'hc2}; //Normal Mode
reg_init[25] <= {1'b1,8'h0d};
reg_init[26] <= {1'b1,8'h00};
reg_init[27] <= {1'b0,8'hc3}; //Idle
reg_init[28] <= {1'b1,8'h8d};
reg_init[29] <= {1'b1,8'h6a};
reg_init[30] <= {1'b0,8'hc4}; //Partial+Full
reg_init[31] <= {1'b1,8'h8d};
reg_init[32] <= {1'b1,8'hee};
reg_init[32] <= {1'b0,8'hc5}; //VCOM
reg_init[33] <= {1'b1,8'h0e};
reg_init[34] <= {1'b0,8'h36}; // 改变显示方向
reg_init[35] <= {1'b1,8'ha8/*3'b101,1'b0,4'h8*/};
reg_init[36] <= {1'b0,8'he0}; //positive gamma
reg_init[37] <= {1'b1,8'h10};
reg_init[38] <= {1'b1,8'h0E};
reg_init[39] <= {1'b1,8'h02};
reg_init[40] <= {1'b1,8'h03};
reg_init[41] <= {1'b1,8'h0E};
reg_init[42] <= {1'b1,8'h07};
reg_init[43] <= {1'b1,8'h02};
reg_init[44] <= {1'b1,8'h07};
reg_init[45] <= {1'b1,8'h0A};
reg_init[46] <= {1'b1,8'h12};
reg_init[47] <= {1'b1,8'h27};
reg_init[48] <= {1'b1,8'h37};
reg_init[49] <= {1'b1,8'h00};
reg_init[50] <= {1'b1,8'h0D};
reg_init[51] <= {1'b1,8'h0E};
reg_init[52] <= {1'b1,8'h10};
reg_init[53] <= {1'b0,8'he1}; //negative gamma
reg_init[54] <= {1'b1,8'h10};
reg_init[55] <= {1'b1,8'h0E};
reg_init[56] <= {1'b1,8'h03};
reg_init[57] <= {1'b1,8'h03};
reg_init[58] <= {1'b1,8'h0F};
reg_init[59] <= {1'b1,8'h06};
reg_init[60] <= {1'b1,8'h02};
reg_init[61] <= {1'b1,8'h08};
reg_init[62] <= {1'b1,8'h0A};
reg_init[63] <= {1'b1,8'h13};
reg_init[64] <= {1'b1,8'h26};
reg_init[65] <= {1'b1,8'h36};
reg_init[66] <= {1'b1,8'h00};
reg_init[67] <= {1'b1,8'h0D};
reg_init[68] <= {1'b1,8'h0E};
reg_init[69] <= {1'b1,8'h10};
INIT状态
代码段就是反复进行初始化命令的写入操作,直到写入计数达到INIT_DEPTH
的定义就结束初化操作,并进入一个较长的等待,等待ST7735完成初始化。
INIT:begin //初始化状态
....
end
SCAN刷屏状态
从RAM中读取数据刷屏,简单来看就是这么几个步骤:
- 确定刷屏的区域坐标
- RAM时钟使能
- 延时一个时钟
- 读取RAM数据,同时关闭RAM时钟使能
- 传输像素。
其中第5步里又细分为:
- 每个像素点需要16bit的数据,SPI每次传8bit,两次分别传送高8位和低8位
- 第一行写完之后进行y计算加1换行写入
- 直到写满
SCAN:begin //刷屏状态,从RAM中读取数据刷屏
case(cnt_scan)
3'd0: begin //确定刷屏的区域坐标,这里为全屏
end
3'd1: begin
end //RAM时钟使能
3'd2: begin cnt_scan <= cnt_scan + 1'b1; end //延时一个时钟
3'd3: begin //读取RAM数据,同时关闭RAM时钟使能
end
3'd4: begin //每个像素点需要16bit的数据,SPI每次传8bit,两次分别传送高8位和低8位
if(x_cnt==LCD_W+1) begin //当一个数据(一行屏幕)写完后,
x_cnt <= 8'd0;
if(y_cnt==LCD_H) begin //如果是最后一行就跳出循环
end //否则跳转至RAM时钟使能,循环刷屏
else begin
y_cnt <= y_cnt + 1'b1;
cnt_scan <= 3'd1;
end
end else begin
if(high_word) begin //根据相应bit的状态判定显示顶层色或背景色,根据high_word的状态判定写高8位或低8位
data_reg <= {1'b1,(ram_data_r[x_cnt] ? color_t[15:8]:color_b[15:8])};
// num_delay <= 16'h50; //设定延时时间
end
else begin //根据相应bit的状态判定显示顶层色或背景色,根据high_word的状态判定写高8位或低8位,同时指向下一个bit
data_reg <= {1'b1,(ram_data_r[x_cnt] ? color_t[7:0]:color_b[7:0])};
x_cnt <= x_cnt + 1'b1;
// num_delay <= 16'h50; //设定延时时间
end
num_delay <= 'h50; //设定延时时间
high_word <= ~high_word; //high_word的状态翻转
state <= WRITE; //跳转至WRITE状态
state_back <= SCAN; //执行完WRITE及DELAY操作后返回SCAN状态
end
end
3'd5: begin
cnt_scan <= 'd0;
lcd_bl_out <= HIGH;
state <= MAIN;
end
default: state <= IDLE;
endcase
end
色彩变行就是在这里做的
color_index <= color_index + 1'b1;
case (color_index)
0: color_t <= GREEN;
1: color_t <= YELLOW;
2: color_t <= BLUE;
3: color_t <= RED;
default : /* default */;
endcase
WRITE状态
比较简单,就是移位输出16比特的数据
WRITE:begin //WRITE状态,将数据按照SPI时序发送给屏幕
if(cnt_write >= 6'd17) cnt_write <= 1'b0;
else cnt_write <= cnt_write + 1'b1;
case(cnt_write)
6'd0: begin lcd_dc_out <= data_reg[8]; end //9位数据最高位为命令数据控制位
6'd1: begin lcd_clk_out <= LOW; lcd_data_out <= data_reg[7]; end //先发高位数据
6'd2: begin lcd_clk_out <= HIGH; end
6'd3: begin lcd_clk_out <= LOW; lcd_data_out <= data_reg[6]; end
6'd4: begin lcd_clk_out <= HIGH; end
6'd5: begin lcd_clk_out <= LOW; lcd_data_out <= data_reg[5]; end
6'd6: begin lcd_clk_out <= HIGH; end
6'd7: begin lcd_clk_out <= LOW; lcd_data_out <= data_reg[4]; end
6'd8: begin lcd_clk_out <= HIGH; end
6'd9: begin lcd_clk_out <= LOW; lcd_data_out <= data_reg[3]; end
6'd10: begin lcd_clk_out <= HIGH; end
6'd11: begin lcd_clk_out <= LOW; lcd_data_out <= data_reg[2]; end
6'd12: begin lcd_clk_out <= HIGH; end
6'd13: begin lcd_clk_out <= LOW; lcd_data_out <= data_reg[1]; end
6'd14: begin lcd_clk_out <= HIGH; end
6'd15: begin lcd_clk_out <= LOW; lcd_data_out <= data_reg[0]; end //后发低位数据
6'd16: begin lcd_clk_out <= HIGH; end
6'd17: begin lcd_clk_out <= LOW; state <= DELAY; end //
default: state <= IDLE;
endcase
end
DELAY延时状态
延时状诚也比较简单通过一个计数器cnt_delay计数,然后与要delay的时钟数num_delay进行比较以确认延时是否已到。
if(cnt_delay >= num_delay) begin
cnt_delay <= 'd0;
state <= state_back;
end else cnt_delay <= cnt_delay + 1'b1;
没有回复内容