1、实验目的
使用逻辑派Z1开发板(基于紫光同创 compa 系列 PGC4KD-6ILPG144 芯片)编写spi屏幕的驱动代码,最终在屏幕上实现红绿蓝交错的九宫格图案。
2、实验原理
2.1 SPI协议介绍
SPI协议(Serial Peripheral Interface,串行外围接口)是一种同步串行通信协议,通常用于短距离设备间的高速数据传输。它采用主从架构,由主设备(如微控制器)和一个或多个从设备(如传感器、显示屏等)组成。SPI通过4条主要的信号线进行数据传输:MOSI(主设备输出,从设备输入)、MISO(主设备输入,从设备输出)、SCLK(串行时钟,由主设备提供)和CS(片选信号,控制与哪个从设备通信)。其中,MOSI和MISO实现双向通信,而SCLK负责同步时钟信号。
SPI的通信特点是全双工,即主设备和从设备可以同时发送和接收数据。通信过程由主设备控制,通过时钟信号SCLK同步数据传输。每次传输的数据按位发送,通常一字节一字节地进行,数据传输的顺序可以配置为从最高有效位(MSB)或最低有效位(LSB)开始。片选信号CS用于选择具体的从设备,低电平激活。当有多个从设备时,主设备可以通过多个CS引脚分别控制它们。SPI协议广泛应用于短距离、高速通信的场景,如显示屏、存储设备、传感器、音频设备等。
SPI有四种通讯模式,由时钟极性和时钟相位共同决定。时钟极性全称 ClockPolarity,通常简写成CPOL。它是指时钟信号在空闲状态下是高电平还是低电平,当时钟空闲时为低电平即CPOL==0,反之则CPOL==1;时钟相位全称 ClockPhase,通常简写成CPHA。它是指时钟信号开始有效的第一个边沿和数据的关系,当CPHA==0时,数据采样发生在时钟信号有效的第一个边沿(也叫奇边沿),当CPHA==1时,数据采样发生在时钟信号有效的第二个边沿(也叫偶边沿);我们以CPOL=1,CPHA=1的模式进行分析:
上图分析了时钟相位与时钟极性均设置为1时的SPI通讯时序。根据时序图可以发现,由于CPOL设置为1,时钟SCK空闲状态为高电平,有效状态为低电平。由于CPHA设置为1,数据采样将发生在时钟有效的第二个边沿,也就是图中时钟为低电平后的第二个边沿。根据Sitronix公司的ST7789VM芯片数据手册,我们本次实验所使用的SPI通讯模式即为此模式。
2.2 SPI屏幕介绍
SPI屏幕是一种通过串行外围接口(SPI, Serial Peripheral Interface)与微控制器或处理器通信的显示设备。其与微控制器的通讯通常包含四条主要的信号线:MOSI(主设备输出,从设备输入)、MISO(主设备输入,从设备输出)、SCLK(串行时钟)和CS(片选/从设备选择)。有时也会简化为三线结构,不使用MISO(本次实验使用的通讯方式就是三线结构)。
相比于并行通信方式(直接使用rgb格式输入像素数据),SPI使用的引脚数量较少,通常只需要4-5根信号线就可以完成数据传输,因此硬件设计更为简洁,尤其适合引脚资源有限的嵌入式系统。
SPI屏幕的控制流程一般是由主控制器通过SPI发送显示数据到屏幕控制器,屏幕控制器再将这些数据处理后驱动液晶或OLED屏幕显示图像。由于SPI是一种全双工通信协议,主设备和从设备可以在同一时钟周期内同时发送和接收数据,不过在大多数SPI屏幕应用中,通常只有主设备(控制器)负责发送显示数据,从设备(屏幕)接收数据。
虽然SPI接口相对于并行接口来说在引脚数和硬件复杂性上有优势,但其缺点是数据传输的带宽相对较小,因此在显示高分辨率图像时速度可能较慢,适合在较小分辨率的屏幕或更新频率要求不高的场景中使用。常见的SPI屏幕有TFT和OLED两种类型,广泛应用于智能手表、物联网设备、传感器终端等对显示要求不高的小型设备上。
其引脚说明如下:
3、代码设计
3.1、总体介绍
我们将SPI屏幕的驱动功能分为以下模块:
test:产生测试数据,例化整个spi屏幕驱动模块,用于板级测试产生图像数据。
spi_screen_top:对屏幕驱动模块进行封装,使之便于用户操作。
spi_tft_screen_driver:spi屏幕驱动模块,例化屏幕初始化模块,屏幕刷新显示模块,spi发送模块。
spi_tft_screen_flush:对用户输入的图像数据进行刷新显示。
spi_tft_screen_init:spi屏幕初始化模块,对ST7789VM进行相关配置。
spi_master_driver:接收各个模块传入的命令与数据并通过spi协议发送给ST7789VM。
模块层次框图如下:
3.2、模块介绍
test模块:
`timescale 1ns / 1ps
module test(
input sys_clk ,// 系统时钟
input sys_rst_n ,// 复位
//spi tft screen 屏幕接口
output lcd_spi_sclk ,// 屏幕spi时钟接口
output lcd_spi_mosi ,// 屏幕spi数据接口
output lcd_spi_cs ,// 屏幕spi使能接口
output lcd_dc ,// 屏幕 数据/命令 接口
output lcd_reset ,// 屏幕复位接口
output lcd_blk // 屏幕背光接口
);
wire flush_data_update ;//更新当前坐标点显示数据使能
reg [ 15: 0] flush_data ;//当前坐标点显示的数据
wire [ 15: 0] flush_addr_width ;//当前刷新的x坐标
wire [ 15: 0] flush_addr_height ;//当前刷新的y坐标
always @(posedge sys_clk or negedge sys_rst_n) begin // 显示九宫格测试图片 240*320
if (sys_rst_n == 1'b0)
flush_data <= 16'd0;
else if ((flush_addr_width >= 'd0 && flush_addr_width < 'd106) && (flush_addr_height >= 'd0 && flush_addr_height < 'd80))
flush_data <= 16'hF800;//红 1111 1000 0000 0000
else if ((flush_addr_width >= 'd106 && flush_addr_width < 'd212) && (flush_addr_height >= 'd0 && flush_addr_height < 'd80))
flush_data <= 16'h07E0;//绿 0000 0111 1110 0000
else if ((flush_addr_width >= 'd212 && flush_addr_width < 'd320) && (flush_addr_height >= 'd0 && flush_addr_height < 'd80))
flush_data <= 16'h001F;//蓝 0000 0000 0001 1111
else if ((flush_addr_width >= 'd0 && flush_addr_width < 'd106) && (flush_addr_height >= 'd80 && flush_addr_height < 'd160))
flush_data <= 16'h07E0;//绿
else if ((flush_addr_width >= 'd106 && flush_addr_width < 'd212) && (flush_addr_height >= 'd80 && flush_addr_height < 'd160))
flush_data <= 16'h001F;//蓝
else if ((flush_addr_width >= 'd212 && flush_addr_width < 'd320) && (flush_addr_height >= 'd80 && flush_addr_height < 'd160))
flush_data <= 16'hF800;//红
else if ((flush_addr_width >= 'd0 && flush_addr_width < 'd106) && (flush_addr_height >= 'd160 && flush_addr_height < 'd240))
flush_data <= 16'h001F;//蓝
else if ((flush_addr_width >= 'd106 && flush_addr_width < 'd212) && (flush_addr_height >= 'd160 && flush_addr_height < 'd240))
flush_data <= 16'hF800;//红
else if ((flush_addr_width >= 'd212 && flush_addr_width < 'd320) && (flush_addr_height >= 'd160 && flush_addr_height < 'd240))
flush_data <= 16'h07E0;//绿
else
flush_data <= 16'h0000;
end
//整个spi屏幕控制顶层,用户只需要提供显示数据就可以使用这个顶层进行spi屏幕显示
spi_screen_top spi_screen_top_inst(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
//用户信号
.flush_data_update_o (flush_data_update ),//更新当前坐标点显示数据使能
.flush_data_i (flush_data ),//当前坐标点显示的数据
.flush_addr_width_o (flush_addr_width ),//当前刷新的x坐标
.flush_addr_height_o (flush_addr_height ),//当前刷新的y坐标
//spi tft screen 屏幕接口
.lcd_spi_sclk (lcd_spi_sclk ),// 屏幕spi时钟接口
.lcd_spi_mosi (lcd_spi_mosi ),// 屏幕spi数据接口
.lcd_spi_cs (lcd_spi_cs ),// 屏幕spi使能接口
.lcd_dc (lcd_dc ),// 屏幕 数据/命令 接口
.lcd_reset (lcd_reset ),// 屏幕复位接口
.lcd_blk (lcd_blk ) // 屏幕背光接口
);
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
这是整个实验工程的顶层模块,用于产生测试数据进行板级验证,模块中例化了spi_screen_top模块,根据spi_screen_top输出的flush_addr_width与flush_addr_height信号分配不同的显示数据,产生红绿蓝交错的九宫格测试图案。以下是对代码具体的解释说明。
代码第6-13行定义了与TFT屏幕通信的SPI接口信号,包括时钟、数据、片选、数据/命令控制、复位和背光控制这些信号都由spi_screen_top模块传出。
代码第15行定义了信号flush_data_update,用于标志当前坐标点显示数据更新状态,若为高电平则代表一个16bit的rgb565图像数据通过spi协议成功发送给屏幕。
代码第16行定义了寄存器flush_data,用于存储当前坐标点的显示数据,同时将数据传入spi_screen_top模块。
代码第17-18行定义了flush_addr_width和flush_addr_height,表示当前刷新的X和Y坐标。
代码第21-46行根据flush_addr_width和flush_addr_height的坐标值,将flush_data赋值为不同的16位颜色数据,形成九宫格显示效果。
代码第48-66行实例化了spi_screen_top模块,负责SPI屏幕通信,并根据spi_screen_top模块输出的坐标将图像数据flush_data传入用于驱动TFT屏幕显示。
总体代码比较简单,主要是功能是生成图像数据。值得注意的是flush_addr_width和flush_addr_height由spi_screen_top模块输出,他们的值会自动递增,我们只需要使用类似vga接口的操作方式在刷新的过程中给出图像数据即可。
spi_screen_top模块:
`timescale 1ns / 1ps
//整个spi屏幕控制顶层,用户只需要提供显示数据就可以使用这个顶层进行spi屏幕显示
module spi_screen_top(
input sys_clk ,
input sys_rst_n ,
//用户信号
output flush_data_update_o ,//更新当前坐标点显示数据使能
input [ 15: 0] flush_data_i ,//当前坐标点显示的数据
output [ 15: 0] flush_addr_width_o ,//当前刷新的x坐标
output [ 15: 0] flush_addr_height_o ,//当前刷新的y坐标
//spi tft screen 屏幕接口
output lcd_spi_sclk ,// 屏幕spi时钟接口
output lcd_spi_mosi ,// 屏幕spi数据接口
output lcd_spi_cs ,// 屏幕spi使能接口
output lcd_dc ,// 屏幕 数据/命令 接口
output lcd_reset ,// 屏幕复位接口
output lcd_blk // 屏幕背光接口
);
//屏幕尺寸
parameter SCREEN_WIDTH = 32'd320;
parameter SCREEN_HEIGHT = 32'd240;
//屏幕用户接口
wire [ 7: 0] spi_screen_flush_data ;//屏幕显示数据
wire spi_screen_flush_updte ;//像素点数据刷新
wire spi_screen_flush_fsync ;//屏幕帧同步
//长宽计数器
reg [ 15: 0] width_cnt ;
reg [ 15: 0] height_cnt ;
//数据更新
reg data_update_cnt ;
//更新数据寄存器
reg [ 15: 0] flush_data_reg ;
//更新数据使能寄存器
reg flush_updte_en;
assign spi_screen_flush_data = flush_data_reg[15:8];//高位发送
//当发送完16bit的图像数据时,坐标点显示数据使能拉高
assign flush_data_update_o = (spi_screen_flush_updte == 1'b1 && data_update_cnt == 1'b0 && flush_updte_en == 1'b1) ? 1'b1 : 1'b0;
//将长宽计数器连接到输出
assign flush_addr_width_o = width_cnt;
assign flush_addr_height_o = height_cnt;
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
flush_updte_en <= 'd0;
else if( spi_screen_flush_fsync == 1'b1 ) //刷新模块发送完一帧数据,清零
flush_updte_en <= 'd0;
else if( spi_screen_flush_updte == 1'b1) //发送完8位数据后,flush_updte_en拉高
flush_updte_en <= 'd1;
else
flush_updte_en <= flush_updte_en;
end
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
data_update_cnt <= 'd0;
else if( spi_screen_flush_fsync == 1'b1 ) //刷新模块发送完一帧数据
data_update_cnt <= 'd0;
else if( spi_screen_flush_updte == 1'b1) //刷新模块通过spi发送完一个8bit数据,data_update_cnt加一
data_update_cnt <= data_update_cnt + 1'b1; //发送高八位时data_update_cnt=1,发送低八位时data_update_cnt=0,
else //所以发送完16bit数据时data_update_cnt=0
data_update_cnt <= data_update_cnt;
end
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0 )
width_cnt <= 'd0;
else if( spi_screen_flush_fsync == 1'b1 ) //一帧图像数据发送完,计数器清零
width_cnt <= 'd0;
else if( flush_data_update_o)//发送完当前像素点16bit图像数据时
if( width_cnt == (SCREEN_WIDTH-1)) //计数到计数器最大值时清零
width_cnt <= 'd0;
else
width_cnt <= width_cnt + 1'b1; //width_cnt计数器加1
else
width_cnt <= width_cnt;
end
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0 )
height_cnt <= 'd0;
else if( spi_screen_flush_fsync == 1'b1) //一帧图像数据发送完,计数器清零
height_cnt <= 'd0;
else if( width_cnt == (SCREEN_WIDTH-1) && flush_data_update_o)//当图像绘制完一行时
if( height_cnt == (SCREEN_HEIGHT-1)) //计数到计数器最大值时清零
height_cnt <= 'd0;
else
height_cnt <= height_cnt + 1'b1; //height_cnt计数器加一
else
height_cnt <= height_cnt;
end
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
flush_data_reg <= 'd0;
else if( spi_screen_flush_updte == 1'b1) //刷新模块发送完一个图像数据
if( data_update_cnt == 1'b0 ) //data_update_cnt=0时,发送完了低八位,寄存新数据
flush_data_reg <= flush_data_i;
else
flush_data_reg <= flush_data_reg << 8; //data_update_cnt=1时,发送完了高八位,将数据向左移动8位,发送低八位数据
else
flush_data_reg <= flush_data_reg;
end
spi_tft_screen_driver spi_tft_screen_driver_inst(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
//用户接口
.spi_screen_flush_data_i (spi_screen_flush_data ),//屏幕显示数据
.spi_screen_flush_updte_o (spi_screen_flush_updte ),//像素点数据刷新//在进行数据到tft屏幕的显示
.spi_screen_flush_fsync_o (spi_screen_flush_fsync ),//屏幕帧同步
//spi tft screen 屏幕接口
.lcd_spi_sclk (lcd_spi_sclk ),// 屏幕spi时钟接口
.lcd_spi_mosi (lcd_spi_mosi ),// 屏幕spi数据接口
.lcd_spi_cs (lcd_spi_cs ),// 屏幕spi使能接口
.lcd_dc (lcd_dc ),// 屏幕 数据/命令 接口
.lcd_reset (lcd_reset ),// 屏幕复位接口
.lcd_blk (lcd_blk ) // 屏幕背光接口
);
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
spi_screen_top模块是对spi_tft_screen_driver模块的封装,简化了spi驱动模块(spi_tft_screen_driver模块)的操作方式,使得用户端只需要使用类似的vga接口的方式给在行列坐标刷新的过程中给出数据即可。以下是对spi_screen_top模块的具体介绍:
代码第3行定义了模块spi_screen_top,这是SPI屏幕控制的顶层模块,用户只需提供显示数据即可使用。
代码第8-11行定义了用户接口信号,包括数据刷新使能、当前显示数据、刷新X坐标和Y坐标输出。
代码第14-21行定义了SPI屏幕的接口信号,用于与TFT屏幕通信。
代码第23-24行定义了屏幕宽度和高度的参数,分别为320和240。
代码第26-30行定义了内部信号,包括用于刷新屏幕的像素数据spi_screen_flush_data、数据更新spi_screen_flush_updte和帧同步信号spi_screen_flush_fsync。spi_screen_flush_updte信号由屏幕显示刷新模块(spi_tft_screen_flush)传出,其每拉高一个高电平代表图像数据发送完成8bit。spi_screen_flush_fsync信号也由屏幕显示刷新模块(spi_tft_screen_flush)传出,其每拉高一个高电平代表发送完成一帧数据。
代码第36行定义了data_update_cnt 信号,由于用户给出的数据是16bit的rgb565数据,而spi发送数据只能是8bit,所以使用这个信号对用户的16bit数据进行高位与低位的区分,当data_update_cnt为0时发送用户高8位数据,当data_update_cnt为1时发送用户低8位数据。
代码第74-86行驱动width_cnt,当帧同步信号(spi_screen_flush_fsync)有效时width_cnt置零,当数据更新信号spi_screen_flush_updte为高电平并且data_update_cnt也为高电平时(已经发送完成用户16bit图像数据的高位和低位)时,width_cnt根据屏幕显示坐标自增和复位(自增到320然后清零)。
代码第89-101行驱动height_cnt,更新逻辑与width_cnt信号相同,不同的是height_cnt自增到240然后清零。
代码第105-115行通过时钟驱动flush_data_reg,根据数据更新信号data_update_cnt将输入的显示数据逐字节移位(0时发送高8位,1时发送低8位),并输出给屏幕。
代码第81-96行实例化了子模块spi_tft_screen_driver,用于将显示数据通过SPI接口传输到TFT屏幕,完成显示刷新。
通过这样的机制用户只需要在行列坐标刷新时给模块提供图像数据即可完成图片的显示,操作十分方便。
spi_tft_screen_driver模块:
`timescale 1ns / 1ps
module spi_tft_screen_driver(
input sys_clk ,
input sys_rst_n ,
//用户接口
input [ 7: 0] spi_screen_flush_data_i ,//屏幕显示数据
output spi_screen_flush_updte_o ,//像素点数据刷新
output spi_screen_flush_fsync_o ,//屏幕帧同步
//------
//spi tft screen 屏幕接口
output lcd_spi_sclk ,// 屏幕spi时钟接口
output lcd_spi_mosi ,// 屏幕spi数据接口
output lcd_spi_cs ,// 屏幕spi使能接口
output lcd_dc ,// 屏幕 数据/命令 接口
output lcd_reset ,// 屏幕复位接口
output lcd_blk // 屏幕背光接口
);
//屏幕尺寸
parameter SCREEN_WIDTH = 32'd320;
parameter SCREEN_HEIGHT = 32'd240;
//总模块信号
reg lcd_init_done ;//初始化完成标志信号
wire spi_start ;//spi开始信号
wire spi_end ;//spi结束信号
wire [ 7: 0] spi_send_data ;//spi发送数据
wire spi_send_ack ;//spi数据发送完成响应
wire lcd_dc_i ;//lcd数据/命令信号
// 初始化模块信号
wire tft_screen_init_req ;//初始化模块请求
wire tft_screen_init_ack ;//初始化模块完成
wire [ 7: 0] tft_screen_init_data ;//初始化模块数据
wire tft_screen_init_dc ;//初始化模块dc
wire spi_send_init_req ;//spi发送数据请求
wire spi_send_init_end ;//结束spi发送
wire spi_send_init_ack ;//spi数据发送完成响应
//刷新模块
wire tft_screen_flush_req ;//刷新模块请求
wire [ 7: 0] tft_screen_flush_data ;//刷新模块数据
wire tft_screen_flush_dc ;//刷新模块数据/命令信号
wire spi_send_flush_req ;//刷新模块发送请求
wire spi_send_flush_end ;//刷新模块发送结束
wire spi_send_flush_ack ;//刷新模块数据发送完成响应
//屏幕驱动信号,默认
assign lcd_reset = 1'b1;
assign lcd_blk = 1'b1;
assign tft_screen_flush_req = ( lcd_init_done == 1'b1) ? 1'b1 : 1'b0;
assign spi_send_flush_ack = ( lcd_init_done == 1'b1) ? spi_send_ack : 1'b0;
assign tft_screen_init_req = ~lcd_init_done;
assign spi_send_init_ack = ( lcd_init_done == 1'b0) ? spi_send_ack : 1'b0;
//像素初始化完成之后进入显示数据刷新模式
assign spi_start = ( lcd_init_done == 1'b0) ? spi_send_init_req : spi_send_flush_req;
assign spi_end = ( lcd_init_done == 1'b0) ? spi_send_init_end : spi_send_flush_end;
assign spi_send_data = ( lcd_init_done == 1'b0) ? tft_screen_init_data : tft_screen_flush_data;
assign lcd_dc_i = ( lcd_init_done == 1'b0) ? tft_screen_init_dc : tft_screen_flush_dc;
//初始化是否完成
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
lcd_init_done <= 1'b0;
else if( tft_screen_init_ack == 1'b1) //初始化模块完成 ,lcd_init_done拉高
lcd_init_done <= 1'b1;
else
lcd_init_done <= lcd_init_done;
end
//刷新模块
spi_tft_screen_flush #(
.SCREEN_WIDTH (SCREEN_WIDTH ),
.SCREEN_HEIGHT (SCREEN_HEIGHT ),
.Number_Of_Pixels (SCREEN_WIDTH*SCREEN_HEIGHT*'d2)
)spi_tft_screen_flush_inst(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
//用户接口
.spi_screen_flush_data_i (spi_screen_flush_data_i ),//屏幕显示数据
.spi_screen_flush_updte_o (spi_screen_flush_updte_o ),//像素点数据刷新
.spi_screen_flush_fsync_o (spi_screen_flush_fsync_o ),//屏幕帧同步
.tft_screen_flush_req_i (tft_screen_flush_req ),//刷新请求
.tft_screen_flush_data_o (tft_screen_flush_data ),//刷新数据
.tft_screen_flush_dc_o (tft_screen_flush_dc ),//刷新dc
.spi_send_flush_req_o (spi_send_flush_req ),//spi发送数据请求
.spi_send_flush_end_o (spi_send_flush_end ),//结束spi发送
.spi_send_flush_ack_i (spi_send_flush_ack ) //spi一个数据发送完成
);
//初始化模块
spi_tft_screen_init #(
.SCREEN_WIDTH (SCREEN_WIDTH ),
.SCREEN_HEIGHT (SCREEN_HEIGHT )
) spi_tft_screen_init_inst(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.tft_screen_init_req_i (tft_screen_init_req ),//初始化请求
.tft_screen_init_ack_o (tft_screen_init_ack ),//初始化完成
.tft_screen_init_data_o (tft_screen_init_data ),//初始化数据
.tft_screen_init_dc_o (tft_screen_init_dc ),//初始化dc
.spi_send_init_req_o (spi_send_init_req ),//spi发送数据请求
.spi_send_init_end_o (spi_send_init_end ),//结束spi发送
.spi_send_init_ack_i (spi_send_init_ack ) //spi一个数据发送完成
);
//spi主机驱动模块
spi_master_driver spi_master_driver_inst(
//系统接口
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
//用户接口
.spi_start_i (spi_start ),// spi开始信号
.spi_end_i (spi_end ),// spi结束信号
.spi_send_data_i (spi_send_data ),// spi发送数据
.spi_send_ack_o (spi_send_ack ),// spi发送8bit数据完成信号
.lcd_dc_i (lcd_dc_i ),//数据还是命令信号输入
.lcd_dc (lcd_dc ),//数据还是命令信号输出
//spi 端口
.spi_sclk (lcd_spi_sclk ),
.spi_mosi (lcd_spi_mosi ),
.spi_cs (lcd_spi_cs )
);
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
此模块是屏幕数据刷新模块(spi_tft_screen_flush)、屏幕初始化模块(spi_tft_screen_init)、spi发送模块(spi_master_driver)的顶层代码。其主要功能是连接三个模块,配置并刷新屏幕,同时提供给用户接口信号,发出spi屏幕控制信号。代码简单,我们只对其做简单的介绍。
第7-10行定义了用户接口信号,包括显示数据、像素点数据刷新信号和屏幕帧同步信号。
第13-20行定义了SPI屏幕的接口信号,用于与TFT屏幕通信。
第23-24行定义了屏幕尺寸的参数,分别为宽度320和高度240。
第27-32行定义了驱动屏幕显示的相关信号,包括SPI开始/结束信号、发送的数据和DC(数据/命令)信号(高电平数据,低电平命令)。
第55-56行默认将lcd_reset和lcd_blk信号设置为高电平,控制屏幕复位和背光。
第74-81行通过lcd_init_done信号控制初始化完成状态,决定系统进入刷新模式。
第86-108行实例化了spi_tft_screen_flush模块,负责屏幕显示数据的刷新控制。
第113-130行实例化了spi_tft_screen_init模块,负责给出初始化屏幕命令与数据。
第135-153行实例化了spi_master_driver模块,负责通过SPI接口将数据发送到TFT屏幕,并输出SPI时钟、数据和片选信号。
spi_tft_screen_flush模块:
`timescale 1ns / 1ps
//对spi tft屏幕进行刷新
module spi_tft_screen_flush(
input sys_clk ,
input sys_rst_n ,
//用户接口
input [ 7: 0] spi_screen_flush_data_i ,//屏幕显示数据
output spi_screen_flush_updte_o ,//像素点数据刷新
output spi_screen_flush_fsync_o ,//屏幕帧同步
//驱动模块
input tft_screen_flush_req_i ,//刷新请求//初始化完成之后此信号拉高模块进入刷新模式
output reg [ 7: 0] tft_screen_flush_data_o ,//刷新数据//刷新的图像数据和刷新命令通过spi模块发送
output reg tft_screen_flush_dc_o ,//刷新dc//区分命令与数据
//SPI主模块
output spi_send_flush_req_o ,//spi发送数据请求
output spi_send_flush_end_o ,//结束spi发送
input spi_send_flush_ack_i //spi一个数据发送完成
);
parameter SCREEN_WIDTH = 16'd320;
parameter SCREEN_HEIGHT = 16'd240;
parameter Number_Of_Pixels = 32'd240*32'd320*32'd2; // 像素点个数
localparam S_IDLE = 4'b0001;
localparam S_DATA = 4'b0010; //发送数据
localparam S_DELAY = 4'b0100; //延时
localparam S_FRAME_SYNC = 4'b1000; // 帧同步
localparam DELAY_5clk = 'd5 ; //命令与数据之间切换等待5个时钟周期
reg [ 31: 0] flush_cnt ; //刷新模块写命令/数据计数器
reg [ 12: 0] delay_cnt ; //延迟计数
reg [ 3: 0] state ,next_state; //状态寄存器
//为写数据状态时,请求拉高
assign spi_send_flush_req_o = (state == S_DATA) ? 1'b1 : 1'b0;
//为延迟状态或帧同步状态时结束信号拉高
assign spi_send_flush_end_o = (state == S_DELAY || state == S_FRAME_SYNC) ? 1'b1 : 1'b0;
//当发送完数据时,且写命令/数据计数器 计数到10时,像素点数据刷新拉高
assign spi_screen_flush_updte_o = ( spi_send_flush_ack_i == 1'b1 && flush_cnt >= 'd10 ) ? 1'b1 : 1'b0;
//为帧同步状态时,屏幕帧同步拉高,产生帧同步信号
assign spi_screen_flush_fsync_o = ( state == S_FRAME_SYNC ) ? 1'b1 : 1'b0;
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0 )
state <= S_IDLE;
else
state <= next_state; //将状态赋值为下一状态
end
always@(*) begin
case(state)
S_IDLE:
if( tft_screen_flush_req_i == 1'b1 ) //初始化完成之后此信号拉高模块进入刷新模式
next_state = S_DATA;
else
next_state = S_IDLE;
S_DATA:
if( spi_send_flush_ack_i == 1'b1 && flush_cnt <= 'd10 ) //地址设置需要发送命令和写入四个参数,设置XY地址共需要发送10个数据
next_state = S_DELAY; //加上发送内存写入命令共需要11个数据,所以flush_cnt计数到10
else if( spi_send_flush_ack_i == 1'b1 && flush_cnt == (Number_Of_Pixels + 'd10))//一帧图像数据发送完成,跳转到帧同步状态
next_state = S_FRAME_SYNC;
else
next_state = S_DATA;
S_DELAY:
if( delay_cnt == DELAY_5clk) //延迟5给时钟周期后,跳转到S_DATA状态
next_state = S_DATA;
else
next_state = S_DELAY;
S_FRAME_SYNC:
next_state = S_IDLE;
default: next_state = S_IDLE;
endcase
end
//发送数据计数
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0 )
flush_cnt <= 'd0;
else if( spi_send_flush_ack_i == 1'b1 && flush_cnt == (Number_Of_Pixels + 'd10))//一帧图像数据发送完成,flush_cnt清零
flush_cnt <= 'd0;
else if( spi_send_flush_ack_i == 1'b1 )//发送完一个数据,flush_cnt加1
flush_cnt <= flush_cnt + 1'b1;
else
flush_cnt <= flush_cnt;
end
//延时计数
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
delay_cnt <= 'd0;
else if( state == S_DELAY)//为延迟状态时,delay_cnt加1
delay_cnt <= delay_cnt + 1'b1;
else
delay_cnt <= 'd0;
end
//tft_screen_flush_dc_o=0时写命令
//tft_screen_flush_dc_o=1时写数据
always @(*) begin
case(flush_cnt)
'd0: begin
tft_screen_flush_data_o = 8'h2A; //设置列地址
tft_screen_flush_dc_o = 1'b0;
end
//写X
'd1: begin
tft_screen_flush_data_o = 8'h00; //列地址开始的高8位
tft_screen_flush_dc_o = 1'b1;
end
'd2: begin
tft_screen_flush_data_o = 8'h00; //列地址开始的低8位
tft_screen_flush_dc_o = 1'b1;
end
'd3: begin
tft_screen_flush_data_o = SCREEN_WIDTH[15:8]; //列地址结束的高8位
tft_screen_flush_dc_o = 1'b1;
end
'd4: begin
tft_screen_flush_data_o = SCREEN_WIDTH[7:0] - 1'b1; //列地址结束的低8位
tft_screen_flush_dc_o = 1'b1;
end
//写Y
'd5: begin
tft_screen_flush_data_o = 8'h2B; //设置行地址
tft_screen_flush_dc_o = 1'b0;
end
'd6: begin
tft_screen_flush_data_o = 8'h00; //行地址开始的高8位
tft_screen_flush_dc_o = 1'b1;
end
'd7: begin
tft_screen_flush_data_o = 8'h00; //行地址开始的低8位
tft_screen_flush_dc_o = 1'b1;
end
'd8: begin
tft_screen_flush_data_o = SCREEN_HEIGHT[15:8]; //行地址结束的高8位
tft_screen_flush_dc_o = 1'b1;
end
'd9: begin
tft_screen_flush_data_o = SCREEN_HEIGHT[7:0] - 1'b1; //行地址结束的低8位
tft_screen_flush_dc_o = 1'b1;
end
//写数据
'd10: begin
tft_screen_flush_data_o = 8'h2C; //发送图像数据
tft_screen_flush_dc_o = 1'b0;
end
default: begin
tft_screen_flush_data_o = spi_screen_flush_data_i; //图像显示数据
tft_screen_flush_dc_o = 1'b1;
end
endcase
end
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
此模块接收经过spi_screen_top模块转换的8bit数据(用户给的是16bit的rgb565格式的),将其不断发送至屏幕进行显示,以下是对代码的分析:
第3-23行:模块的输入输出接口定义。
定义了系统时钟sys_clk和复位信号sys_rst_n。
用户接口包括spi_screen_flush_data_i(输入的屏幕显示数据,8bit)、spi_screen_flush_updte_o(像素刷新信号)和spi_screen_flush_fsync_o(屏幕帧同步信号)。
驱动模块接口定义了tft_screen_flush_req_i(刷新请求输入),tft_screen_flush_data_o(输出的刷新数据),tft_screen_flush_dc_o(DC控制位,高电平代表本次发送的是数据,低电平代表本次发送的是命令),spi_send_flush_req_o(SPI发送请求),spi_send_flush_end_o(SPI发送结束信号),以及spi_send_flush_ack_i(SPI发送成功反馈,每拉高一次代表成功发送了一个8bit数据)。
第25-27行:定义屏幕的参数。 SCREEN_WIDTH和SCREEN_HEIGHT分别表示屏幕宽度320和高度240。 Number_Of_Pixels表示屏幕像素总数3202402=153600(用户顶层用户接口提供的数据格式是16bit的,在这里每8bit就发送一次,发送两次才相当于刷新了一个像素数据。像素总数是320240但是总共要发送3202402次8bit数据才能完成一帧图像的刷新,所以这里需要2)。
第30-33行:状态机的状态定义。 S_IDLE表示空闲状态。 S_DATA表示发送数据状态。 S_DELAY表示延时状态(根据芯片数据手册,发送的命令之间需要适当延时)。 S_FRAME_SYNC表示帧同步状态(发送完成一帧数据)。 第36行:DELAY_5clk='d5。 定义了在命令和数据之间切换时需要延迟的时钟周期数,5个时钟周期。
第38-40行:寄存器定义。 flush_cnt用于记录当前发送数据的进度(其中每一帧的前10个数据为芯片配置数据,设置当前帧图像的大小)。 delay_cnt用于在S_DELAY状态下实现延时。 state和next_state用于描述三段式状态机的当前状态和下一个状态。
第43-45行:控制SPI发送请求和结束信号。 当状态为S_DATA时,spi_send_flush_req_o置为高,发出SPI发送请求。 当状态为S_DELAY或S_FRAME_SYNC时,spi_send_flush_end_o置为高,表示此时不进行SPI数据的传输。
第47-48行:像素点刷新和帧同步信号的输出。 当flush_cnt>=11为1并且spi_send_flush_ack_i同时也为1,spi_screen_flush_updte_o置为高,已经通过SPI发送完一个8bit数据。 当状态为S_FRAME_SYNC时,spi_screen_flush_fsync_o置为高,产生帧同步信号。
第50-57行:三段式状态机的时序逻辑状态更新。
第60-83行:状态机的状态转移逻辑。 在S_IDLE状态,如果收到tft_screen_flush_req_i(刷新请求)信号,进入 S_DATA状态。 在S_DATA状态,首次进入S_DATA状态代码43行会将spi_send_flush_req_o发送请求信号拉高,发送第一个命令。接下来如果spi_send_flush_ack_i为1且flush_cnt<=10,进入S_DELAY状态;如果flush_cnt达到最大像素数,进入S_FRAME_SYNC。 在S_DELAY状态,如果延迟计数器delay_cnt达到DELAY_5clk,回到S_DATA。 在S_FRAME_SYNC状态,状态返回S_IDLE。
第87-96行:数据发送计数器flush_cnt的更新逻辑。 如果复位信号为0,flush_cnt复位为0。 如果spi_send_flush_ack_i为1且flush_cnt达到最大像素数+10(这10个数据是配置的命令和数据),flush_cnt复位为0。 否则flush_cnt每次spi_send_flush_ack_i(spi每发送成功8bit拉高一次) 为1时自增1。
第99-107行:延迟计数器delay_cnt的更新逻辑。 如果复位信号为0,delay_cnt复位为0。 如果状态为S_DELAY,delay_cnt自增。 否则delay_cnt复位为0。
第111-165行:根据flush_cnt输出相应的SPI数据和DC控制信号。 flush_cnt==0时发送命令8'h2A,表示设置列地址。 flush_cnt==1-4时依次发送X轴的起始和结束地址(每次发送8bit,总共发送了四次)。 flush_cnt==5时发送命令8'h2B,表示设置行地址。 flush_cnt==6-9时依次发送Y轴的起始和结束地址(每次发送8bit,总共发送了四次)。 flush_cnt==10时发送命令8'h2C,表示开始发送图像数据。 其余情况下,发送用户输入的图像数据spi_screen_flush_data_i。
屏幕数据刷新模块在外部信号tft_screen_flush_req_i(刷新请求)的触发下开始工作,用一个状态机产生需要写入芯片的数据,并向spi发送模块发送请求信号,将命令与数据不断的产生并发送给芯片。在芯片初始化完成之后,这个模块是主要的驱动逻辑。
spi_tft_screen_init模块
`timescale 1ns / 1ps
//对spi tft屏幕进行初始化
module spi_tft_screen_init(
input sys_clk ,
input sys_rst_n ,
input tft_screen_init_req_i ,//初始化请求
output tft_screen_init_ack_o ,//初始化完成
output reg [ 7: 0] tft_screen_init_data_o ,//初始化数据
output reg tft_screen_init_dc_o ,//初始化dc
output spi_send_init_req_o ,//spi发送数据请求
output spi_send_init_end_o ,//结束spi发送
input spi_send_init_ack_i //spi一个数据发送完成
);
parameter SCREEN_WIDTH = 16'd320;
parameter SCREEN_HEIGHT = 16'd240;
localparam DELAY_255ms = 32'd12_750_000;//255ms 255_000_000 /20 =12_750_000
localparam DELAY_200us = 32'd10_000; //200us 200_000/20=10_000
localparam S_IDLE = 4'b0001;//初始状态
localparam S_SEND_DATA = 4'b0010;//发送数据状态
localparam S_DELAY = 4'b0100;//延迟状态
localparam S_ACK = 4'b1000;//响应状态
reg [ 4: 0] init_cnt ;//初始化命令/数据计数
reg [ 31: 0] delay_cnt ;//延时计数
reg [ 3: 0] state ;//状态寄存器
reg [ 3: 0] next_state ;//下一状态寄存器
//为响应状态时,初始化完成信号拉高
assign tft_screen_init_ack_o = (state == S_ACK) ? 1'b1 : 1'b0;
//为发送数据状态时,spi发送数据请求信号拉高
assign spi_send_init_req_o = (state == S_SEND_DATA) ? 1'b1 : 1'b0;
//为延迟状态时,结束spi发送
assign spi_send_init_end_o = (state == S_DELAY) ? 1'b1 : 1'b0;
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
state <= S_IDLE;
else
state <= next_state;
end
always@(*) begin
case(state)
S_IDLE:
if( tft_screen_init_req_i == 1'b1)//初始化请求有效时,跳转到发送数据状态
next_state <= S_SEND_DATA;
else
next_state <= S_IDLE;
S_SEND_DATA:
if( spi_send_init_ack_i == 1'b1)//spi一个数据发送完成,跳转到延迟状态
next_state <= S_DELAY;
else
next_state <= S_SEND_DATA;
S_DELAY:
if( init_cnt == 'd18)//初始化命令和数据发送完成跳转到响应状态
if( delay_cnt == DELAY_255ms)
next_state <= S_ACK;
else
next_state <= S_DELAY;
else if(init_cnt == 'd1 )//延迟结束后,跳转到发送数据状态
if(delay_cnt == DELAY_255ms)
next_state <= S_SEND_DATA;
else
next_state <= S_DELAY;
else if(init_cnt == 'd2 )
if(delay_cnt == DELAY_255ms)
next_state <= S_SEND_DATA;
else
next_state <= S_DELAY;
else if(init_cnt == 'd4 )
if(delay_cnt == DELAY_255ms)
next_state <= S_SEND_DATA;
else
next_state <= S_DELAY;
else if(init_cnt == 'd17 )
if(delay_cnt == DELAY_255ms)
next_state <= S_SEND_DATA;
else
next_state <= S_DELAY;
else if( delay_cnt == DELAY_200us)
next_state <= S_SEND_DATA;
else
next_state <= S_DELAY;
S_ACK:
next_state <= S_IDLE;
default: next_state <= S_IDLE;
endcase
end
//初始化数据计数//
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
init_cnt <= 'd0;
else if( spi_send_init_ack_i == 1'b1)//spi一个数据发送完成,init_cnt加1
init_cnt <= init_cnt + 1'b1;
else
init_cnt <= init_cnt;
end
//延时计数//写命令之间需要间隔的时间
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
delay_cnt <= 'd0;
else if( state == S_DELAY) //为延迟状态时,delay_cnt加1
delay_cnt <= delay_cnt + 1'b1;
else
delay_cnt <= 'd0;
end
//命令数据输出
//0命令,1数据
always@(*)begin
case (init_cnt)
'd0: begin
tft_screen_init_data_o = 8'h01; //SWRESET//软复位
tft_screen_init_dc_o = 1'b0;
end
'd1: begin
tft_screen_init_data_o = 8'h11; //SLPOUT//唤醒
tft_screen_init_dc_o = 1'b0;
end
'd2: begin
tft_screen_init_data_o = 8'h3A; //COLMOD//像素设置
tft_screen_init_dc_o = 1'b0;
end
'd3: begin
tft_screen_init_data_o = 8'h55; //数据//0_101_0_101//0_65k像素_0_rgb565(16bit)
tft_screen_init_dc_o = 1'b1;
end
'd4: begin
tft_screen_init_data_o = 8'h36; //MADCTL//帧内存数据的读写扫描方向
tft_screen_init_dc_o = 1'b0;
end
'd5: begin
tft_screen_init_data_o = 8'h70; //数据//01110000
tft_screen_init_dc_o = 1'b1;
end
'd6: begin
tft_screen_init_data_o = 8'h2A; //CASET列地址设置
tft_screen_init_dc_o = 1'b0;
end
'd7: begin
tft_screen_init_data_o = 8'h00; //列地址开始的高8位
tft_screen_init_dc_o = 1'b1;
end
'd8: begin
tft_screen_init_data_o = 8'h00; //列地址开始的低8位
tft_screen_init_dc_o = 1'b1;
end
'd9: begin
tft_screen_init_data_o = SCREEN_WIDTH[15:8]; //列地址结束的高8位
tft_screen_init_dc_o = 1'b1;
end
'd10: begin
tft_screen_init_data_o = SCREEN_WIDTH[7:0] - 1'b1; //列地址结束的低8位
tft_screen_init_dc_o = 1'b1;
end
'd11: begin
tft_screen_init_data_o = 8'h2B; //RASET//行地址设置
tft_screen_init_dc_o = 1'b0;
end
'd12: begin
tft_screen_init_data_o = 8'h00; //与列地址设置相似
tft_screen_init_dc_o = 1'b1;
end
'd13: begin
tft_screen_init_data_o = 8'h00; //数据
tft_screen_init_dc_o = 1'b1;
end
'd14: begin
tft_screen_init_data_o = SCREEN_HEIGHT[15:8]; //数据
tft_screen_init_dc_o = 1'b1;
end
'd15: begin
tft_screen_init_data_o = SCREEN_HEIGHT[7:0] - 1'b1; //数据
tft_screen_init_dc_o = 1'b1;
end
'd16: begin
tft_screen_init_data_o = 8'h13; //NORON//转换为正常显示模式
tft_screen_init_dc_o = 1'b0;
end
'd17: begin
tft_screen_init_data_o = 8'h29; //DISPON//开启显示
tft_screen_init_dc_o = 1'b0;
end
default: begin
tft_screen_init_data_o = 8'h01; //SWRESET//软复位
tft_screen_init_dc_o = 1'b0;
end
endcase
end
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
屏幕初始化模块是本实验最先执行的逻辑,系统上电阶段spi_tft_screen_driver模块会将本模块输入信号tft_screen_init_req_i(初始化请求信号)拉高,开始对ST7789VM芯片进行初始化配置,当配置完成后拉高tft_screen_init_ack(初始化完成)信号,同时spi_tft_screen_driver模块拉低tft_screen_init_req_i(初始化请求信号)。代表ST7789VM芯片初始化配置完成。以下是本模块代码分析:
第3-16行:模块接口定义 这个模块名为spi_tft_screen_init,主要负责通过SPI协议对TFT屏幕进行初始化。 sys_clk是系统时钟输入,sys_rst_n是系统复位输入。 tft_screen_init_req_i是初始化请求输入信号,tft_screen_init_ack_o是初始化完成输出信号。 tft_screen_init_data_o是初始化数据输出信号,tft_screen_init_dc_o是命令/数据选择信号(高电平输出数据,低电平输出命令)。 spi_send_init_req_o是SPI发送数据请求信号,spi_send_init_end_o表示SPI发送完成,spi_send_init_ack_i是SPI发送反馈,代表成功发送一个8bit配置数据。
第18-20行:参数定义 SCREEN_WIDTH定义屏幕宽度为320像素。 SCREEN_HEIGHT定义屏幕高度为240像素。
第22-23行*:延时常量定义 DELAY_255ms定义了255毫秒的延时时间。 DELAY_200us定义了200微秒的延时时间。
第24-27行*:状态机状态定义 S_IDLE表示空闲状态。 S_SEND_DATA表示正在发送数据状态。 S_DELAY表示延时状态。 S_ACK表示初始化完成的状态。
第30-33行:寄存器定义 init_cnt是用于计数初始化过程中发送命令和数据的计数器。 delay_cnt是延时计数器。 state和next_state是状态机的当前状态和下一个状态。
第37-41行:输出信号的定义 tft_screen_init_ack_o表示当状态机进入S_ACK状态时,初始化完成。 spi_send_init_req_o表示在S_SEND_DATA状态时发出SPI数据发送请求(当该信号拉高时请求spi数据发送模块发送当前配置数据)。 spi_send_init_end_o表示在S_DELAY状态暂时停止发送配置数据(根据芯片数据手册要求命令与命令之间需要延时255ms)。
第43-48行:状态机状态更新逻辑 如果复位信号sys_rst_n为0,状态机回到S_IDLE状态。 否则,状态机的当前状态被更新为next_state。
第51-98行:状态机的状态转移逻辑 在S_IDLE状态,当tft_screen_init_req_i(初始化请求信号)为高时,进入S_SEND_DATA状态。 在S_SEND_DATA状态,当SPI发送完成(spi_send_init_ack_i为高,spi发送模块发送完成8bit配置数据)时,进入S_DELAY状态。 在S_DELAY状态,根据不同的初始化命令和数据的计数(init_cnt)来决定是否进行延时或进入下一个状态: 如果init_cnt=='d18且延时结束,进入S_ACK状态。 否则,针对特定的init_cnt值,如d1,d2,d4,d17,延时结束后进入S_SEND_DATA状态。 对于其他情况,当延时结束(delay_cnt==DELAY_200us),进入S_SEND_DATA状态。 在S_ACK状态,表示初始化完成,状态回到S_IDLE。
第103-111行:初始化计数器init_cnt更新逻辑 如果复位信号sys_rst_n为0,init_cnt清零。 如果spi_send_init_ack_i(代表spi发送模块发送完成8bit配置数据)为高,init_cnt自增。
第114-122行:延时计数器delay_cnt更新逻辑 如果复位信号为0,delay_cnt清零。 如果当前状态是S_DELAY,则delay_cnt自增,否则清零。
第125-209行:根据init_cnt输出相应的初始化命令和数据 init_cnt==0时,发送复位命令8'h01(软件复位)。 init_cnt==1时,发送命令8'h11(唤醒命令)。 init_cnt==2时,发送命令8'h3A(设置像素格式),紧接着是8'h55,表示使用RGB565格式。 init_cnt==4时,发送命令8'h36(芯片内存数据的读写扫描方向),紧接着是8'h78,设置扫描方式。 其他init_cnt对应列地址设置、行地址设置、显示模式为正常、以及开启显示命令,依次为8'h2A,8'h2B,8'h138'h29
spi_master_driver模块:
`timescale 1ns / 1ps
// //模式0:CPOL= 0,CPHA=0。SCK串行时钟线空闲是为低电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
// //模式1:CPOL= 0,CPHA=1。SCK串行时钟线空闲是为低电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
// //模式2:CPOL= 1,CPHA=0。SCK串行时钟线空闲是为高电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
// //模式3:CPOL= 1,CPHA=1。SCK串行时钟线空闲是为高电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
//模式3:CPOL= 1,CPHA=1。SCK串行时钟线空闲是为高电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
module spi_master_driver(
//系统接口
input sys_clk ,
input sys_rst_n ,
//用户接口
input spi_start_i ,// spi开始信号
input spi_end_i ,// spi结束信号
input [ 7: 0] spi_send_data_i ,// spi发送数据
output reg spi_send_ack_o ,// spi发送8bit数据完成信号
input lcd_dc_i ,//数据还是命令信号输入
output reg lcd_dc ,//数据还是命令信号输出
//spi 端口
output reg spi_sclk ,//spi时钟
output reg spi_mosi ,//spi数据
output spi_cs //spi使能
);
localparam IDLE = 3'b001, //空闲状态
DATA = 3'b010, //发送数据状态
STOP = 3'b100; //停止状态
reg [2:0] cure_state;//状态寄存器
reg [2:0] next_state;//下一状态寄存器
reg [7:0] spi_send_data_reg ;//数据寄存器
reg [3:0] spi_send_data_bit_cnt ;//数据发送bit计数器
assign spi_cs = (cure_state == IDLE) ?1:0; //片选信号,低电平有效,为空闲状态时拉高
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0 )
cure_state <= IDLE;
else
cure_state <= next_state;
always @(*)
case(cure_state)
IDLE:begin //空闲
if(spi_start_i) //spi_start 开始信号来临时,开始进行数据发送
next_state = DATA;
else
next_state = IDLE;
end
DATA:begin //发送数据
if(spi_send_data_bit_cnt == 7 && spi_sclk == 1'b0) //字节发送完毕
next_state = STOP;
else
next_state = DATA;
end
STOP:next_state = IDLE; //停止
default:next_state = IDLE;
endcase
//发送数据缓存
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0 )
spi_send_data_reg <= 'd0;
else if(spi_send_data_bit_cnt == 'd0) //8bit数据发送完成后,缓存新的数据
spi_send_data_reg <= spi_send_data_i;
else
spi_send_data_reg <= spi_send_data_reg;
end
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0 )
spi_send_ack_o <= 'd0;
else if(spi_send_data_bit_cnt == 'd7 && spi_sclk == 1'b0 && spi_cs == 1'b0) //发送完8位数据后,spi发送8bit数据完成信号拉高
spi_send_ack_o <= 'd1;
else
spi_send_ack_o <= 'd0;
end
//数据是命令还是数据
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
lcd_dc <= 1'b1;
else if( spi_start_i == 1'b1) //接收到开始信号时,lcd_dc赋值为lcd_dc_i
lcd_dc <= lcd_dc_i;
else
lcd_dc <= lcd_dc;
end
//产生spi时钟
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
spi_sclk <= 1'b1;
else if(cure_state != DATA )
spi_sclk <= 1'b1;
else if( spi_cs == 1'b0 )
spi_sclk <= ~spi_sclk; //当spi_cs低电平时,翻转spi_sclk,生成40ns周期的sclk时钟
else
spi_sclk <= 1'b1;
end
//数据发送bit数寄存器
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
spi_send_data_bit_cnt <= 'd0;
else if( spi_cs == 1'b0 && spi_sclk == 1'b0) //使用SPI模式三,所以数据在下降沿切换,所以当时钟为低电平时,数据计数器加1
if( spi_send_data_bit_cnt == 'd7)//发送完8位数据,清零
spi_send_data_bit_cnt <= 'd0;
else
spi_send_data_bit_cnt <= spi_send_data_bit_cnt + 1'b1;
else if( spi_cs == 1'b0)
spi_send_data_bit_cnt <= spi_send_data_bit_cnt;//在时钟上升沿时,保持不变
else
spi_send_data_bit_cnt <= 'd0;
end
//spi数据发送
always@(posedge sys_clk or negedge sys_rst_n) begin
if( sys_rst_n == 1'b0)
spi_mosi <= 1'b1;
else if( spi_cs == 1'b0)
spi_mosi <= spi_send_data_reg['d7 - spi_send_data_bit_cnt];//将数据从高位到低位逐位发送
else
spi_mosi <= spi_mosi;
end
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
本模块为SPI发送模块,使用时钟相位时钟极性均设置为1。也就是时钟低电平有效,数据采样边沿为偶边沿。本模块为其他模块发送配置信息,也负责图像数据的发送。以下是具体分析: 模块使用一个状态机来控制来SPI 通信的过程,其包括三个状态:
IDLE(空闲)状态:在空闲状态下,模块等待 spi_start_i 信号的触发。当上层模块触发这个开始信号时,状态机会切换到数据发送状态,处理发送逻辑。
DATA(数据发送)状态:当进入数据发送状态时,模块通过 SPI 总线逐位发送数据。每次发送一个字节(8 位),在数据传输过程中,信号spi_send_data_bit_cnt 用于记录当前发送的位数。每当一个字节数据发送完毕时,状态机会进入停止状态。
STOP(停止)状态:数据发送完成后,状态机会进入停止状态,SPI 片选信号被拉高,表示传输结束。之后,状态机重新回到空闲状态,等待上层模块触发下一次数据传输请求。 模块的核心功能是实现数据的逐位传输。
我们首先来看SPI 时钟的生成逻辑:在代码第90~99行,SPI 时钟(spi_sclk)的生成由状态机控制。在数据发送状态下,当spi_cs低电平时,将时钟信号周期性地反转,生成一个周期为40ns的时钟,用于同步 MOSI 数据线的变化。模块使用的是 SPI 模式3,因此时钟在空闲时为高电平,数据在时钟的上升沿被采样,数据在下降沿切换。
SPI 片选信号(spi_cs)也是模块中的关键信号,代码第39行对该信号进行了赋值,在 IDLE 状态下,spi_cs 被拉高,表示 SPI 总线处于空闲状态,主设备不与外设通信。而在 DATA 状态下,spi_cs 被拉低,表示 SPI 通信已经启动。
接下来观察spi_mosi 信号:在代码第101~113行:每当 SPI 时钟为低电平并且spi_cs低电平时,待发送的数据将按照spi_send_data_bit_cnt索引从高位到低位将数据从数据寄存器spi_send_data_reg中取出,并通过 spi_mosi 信号发送。当发送完一个字节的数据后,模块通过 spi_send_ack_o 信号通知上层控制逻辑,表示数据发送已完成。该信号将在每次发送完 8 位数据时拉高,作为上层逻辑知晓一字节数据发送完成的反馈。
此外,本次实验所用到的SPI显示屏型号GMT024-8pinSPI_LCM使用Sitronix公司的ST7789VM芯片来控制tft液晶显示屏幕的显示,其还使用DC信号用于指示当前传输的数据是命令数据还是实际数据,在本模块中DC信号由上层逻辑传入,当 spi_start_i 信号触发时,lcd_dc 会被更新为输入信号 lcd_dc_i 的值,配合spi的发送过程,指示发送的每个字节数据是图像数据还是控制命令。
4、实验现象
新建工程,添加rtl代码,添加fdc文件;本实验管脚约束如下:(示例工程中也有仿真代码,读者若不清楚时序也可自行仿真分析)
综合生成比特流,将程序下载进入板卡,由此可见实验设计成功,如图所示: