PotatoPie 2.1/3.0 教程(13) —— 实验12 FPGA驱动ST7735 SPI LCD显示模块-Anlogic-安路社区-FPGA CPLD-ChipDebug

PotatoPie 2.1/3.0 教程(13) —— 实验12 FPGA驱动ST7735 SPI LCD显示模块

1.实验说明

1.1 管脚说明

set_pin_assignment	{ clk_ref }	{ LOCATION = P128; IOSTANDARD = LVCMOS33; }
set_pin_assignment	{ clk_osc }	{ LOCATION = P120; IOSTANDARD = LVCMOS33; }
set_pin_assignment	{ lcd_bl_out }	{ LOCATION = P111; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment	{ lcd_cs_out }	{ LOCATION = P106; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment	{ lcd_dc_out }	{ LOCATION = P104; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment	{ lcd_rst_n_out }	{ LOCATION = P100; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment	{ lcd_data_out }	{ LOCATION = P98; IOSTANDARD = LVCMOS33; PULLTYPE = PULLDOWN; }
set_pin_assignment	{ lcd_clk_out }	{ LOCATION = P96; 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丝印上有明确标识管脚信号。

20231220125051966-image

20231220125720790-image

电源正接开发板的3V3,GND接开发板的GND。

1.2 实验现象

下载程序之后可以看到LCD上循环往复地刷着红、绿、蓝屏。

 

2.原理说明

2.1 TFT LCD原理

TFT-LCDThin Film Tube-Liquid Crystal Display,薄膜晶体管液晶显示器)最初作为笔
记本电脑、文字处理器的显示装置发展起来,现已扩展应用于计算机、游戏机、弹子机、摄
像机、车载电视等,从小型的到大型的机器,在各个领域都得到广泛应用。

液晶同固态晶体一样,具有特异的光学各向异性。而且这种光学各向异性伴随分子排列
结构的不同将呈现不同的光学形态。分子取向一旦发生变化,这些光学特性将随之变化,于
是在液晶中传输的光就受到调制。由此可见,变更分子的排列状态即可实现光调制。利用外
加电场即可改变液晶分子的取向,产生调制。这种由电场产生的光调制现象叫作液晶的“电光
效应”,它是液晶显示的基础。液晶不但具有固态晶体的光学特性,还具有液态的流动特性。
它的物理特性包括:黏性、弹性和极化性。黏性和弹性,使其对于方向不同的作用力具有不
同的效果,可产生自然偏转现象;极化性使液晶在受到外加电场作用时,很容易产生感应偶
极性,形成光电效应。
液晶显示器根据它的成像原理不同分为
TN-LCDTwist Nematic,扭曲向列)液晶显示
器、
STN-LCDSuper Twist Nematic,超扭曲向列)液晶显示器、DSTN-LCDDouble Super

Twist Nematic,双层超扭曲向列)液晶显示器和 TFT-LCD 液晶显示器。目前,计算机所使用
的液晶显示器多使用
TFT-LCD 薄膜晶体管型。

液晶显示器显像原理

液晶显示器的显像原理,是将液晶置于两片导电玻璃之间,靠两个电极间电场的驱动,
引起液晶分子扭曲向列的电场效应,以控制光源透射或遮蔽功能,在电源关开之间产生明暗
而将图像显示出来。
TFT-LCD显示器的核心是液晶屏,其结构是在两块玻璃基板中间充斥着运动的液晶分
子。信号电压直接控制薄膜晶体管的开关状态,再利用晶体管控制液晶分子,来实现图像的
显示。
LCD屏的结构如同“三明治”,即在两片玻璃基板内夹着彩色滤光片、偏光板、配向膜
等材料,灌入液晶材料,最后封装成一个液晶盒。
LCD 屏的结构如图所示。

20231220130828950-image

如果给液晶显示器的 LCD 面板中加上彩色滤光片,则可显示彩色影像。在彩色 LCD
板中,每一个像素都是由
3 个液晶单元格构成,其中每一个单元格前面都分别有红色、绿色
或蓝色的过滤片。光线经过过滤片处理后照射到每个像素中不同色彩的液晶单元格上,利用
三基色原理组合出不同的色彩。彩色液晶显示器面板结构如图
所示。

20231220130906456-image

因液晶屏本身没有发光功能,这就需要在液晶屏后加一个照明系统,该背光照明系统
能使光线均匀照射在液晶表面。现在发光部件的主流为被称作冷阴极管的荧光灯管。其发
光原理与室内照明用的热阴管类似,但不需像热阴管那样先预热灯丝,它在较低温状态就
能点亮,因此叫冷阴极管。但要驱动这种冷阴极管需要能输出
10001500V交流电压的
特殊电源。逆变器单元就是驱动这种冷阴极管用的小型电源,是液晶显示装置中最重要的
功能部件之一。

液晶面板的结构及工作原理 

液晶面板由上下两块玻璃基板组成,中间由液晶材料均匀间隔隔开。因为液晶材料本身
并不发光,所以必须依靠两边的背光灯管作为光源,而在液晶显示屏背面上的导光板让光线
往液晶方向前进,并通过反光膜和散射板将光线均匀地分布到各个区域去,给液晶材料一个
均匀明亮的光源。在玻璃基板与液晶材料之间安装行、列电极,并在行与列的交叉点上,通
过控制电压来达到液晶的旋光状态。
LCD 是由两个互相垂直的极化滤光器构成,中间充满了
扭曲的液晶材料,光线穿出第一个滤光器后会使液晶扭转
90,最后从第二个过滤器中穿出。
另外,在液晶材料的周围设置控制电路和驱动电路,在电路的控制下使行、列电极产生电场,
并在不同的电场作用下,液晶分子会作规则的
90旋转排列,这样,在电源接通与断开的转
换过程中产生明暗的差别,依此原理控制每个像素的明暗变化,便可构成所需显示的图像。
液晶面板的结构如图
所示。

20231220131004256-image

液晶屏驱动电路

液晶屏驱动电路由图像处理器、时序转换电路、行驱动电路、列驱动电路、直流电压转
换电路等组成。液晶屏驱动电路组成框图如图
所示。

20231220131039944-image

本模块中的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

    20240329212338785-image

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模式,从规书上我们可以看到它的接口时序,

20231220132507869-image

比较简单,进行通讯前先CS(图中CSX)拉低选中片选,然后SCL发时钟,SDA发像素数据或者命令,DC(图中D/CX)用于驱动SDA传的是像素数据还是命令。

从下图可以看到SDA是先传高位再传低位。

20231220133102984-image

4线串行模式的时序图如下:

20231220133253393-image

4线模式执行RDID的命令时序如下:

20231220133348929-image

更多时序图参考规格书的第9.4章。

上面只是讲了接口,但具体怎么驱动呢?简单讲这么两个步骤:

  1. 发送初始化命令初始化寄存器,由于寄存器很多具体怎么设我们不必细究,一般参考厂家提供的初始化列表,或者是C代码的初始化。
  2. 发送RAM写入指令,进行色彩数据的写入。

由于ST7735本身带SRAM显存,因此我们不需要像裸屏一样进行TFT的周期刷新,这个过程由ST7735完成了。

3.代码分析

工程与本实验相关的代码在LCD_ST7735S.v中

20231220133909140-image

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中读取数据刷屏,简单来看就是这么几个步骤:

  1. 确定刷屏的区域坐标
  2. RAM时钟使能
  3. 延时一个时钟
  4. 读取RAM数据,同时关闭RAM时钟使能
  5. 传输像素。

其中第5步里又细分为:

  1. 每个像素点需要16bit的数据,SPI每次传8bit,两次分别传送高8位和低8位
  2. 第一行写完之后进行y计算加1换行写入
  3. 直到写满
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;
请登录后发表评论

    没有回复内容