FPGA中级项目8---------UART-RAM-TFT
UART串口我们学过,RAM IP核学过,TFT同样也学过。那如何将它们联合起来呢?
言简意赅:实现从串口写入图像到RAM并且由TFT显示屏输出!
首先第一步,便是要将UART_RX与RAM之间架起桥梁,也就是我们要写一个控制器,其需要的接口如下
需要串口端的数据导入,一个字节的数据导入完成标志;RAM端的写使能信号,写入地址,写入数据。
问题一
特别需要注意的是,在本节内容中,选用的RAM核为16位宽的,同时有16根地址线。
RAM 存储容量 :一个 16 位宽的 RAM,意味着它每次读写操作的数据宽度是 16 位(也就是 2 字节)。如果这个 RAM 的地址线有 16 根(因为 (2^16=65536) ),那么它的存储深度就是 65536 个存储单元。每个存储单元可以存放 16 位(2 字节)的数据。所以这个 RAM 的总存储容量是131072 字节 = 128KB 。
像素点与数据关系 :一个像素点用 16 位(2 字节)来表示,那么这个 128KB 容量的 RAM 正好可以存放 65536 个像素点的数据。
所以需要设立rx_down信号,来进行计数直到一幅图像数据 65536 完成。
问题二
同时定义rx_data_tmp寄存器是很有必要的,不能直接用ram_wrdata,原因有下:
1. 数据组装需求
UART 串口每次传输一个字节(8 位)的数据,而 RAM 需要以 16 位(两个字节)为单位写入数据,也就是一个像素点的数据。这就需要将两个连续接收到的字节组合成一个 16 位的数据。
rx_data_tmp寄存器起到了临时存储和组装数据的作用。每次接收到一个新的字节rx_data时,它会把之前存储的低 8 位数据和新的rx_data组合起来,形成一个完整的 16 位数据。
2. 信号类型和赋值规则
ram_wrdata是一个wire类型的信号,它是通过assign语句进行赋值的,不能在always块中直接修改其值。assign语句通常用于组合逻辑的赋值,而rx_data_tmp的更新是时序逻辑,需要在always块中完成。如果直接使用ram_wrdata,就无法在always块中对其进行更新,也就无法实现数据的正确组装。
3. 写使能和数据同步
写使能信号ram_wren是在接收到第二个字节时才有效,这意味着只有在两个字节都接收完成后,才会将组合好的 16 位数据写入 RAM。rx_data_tmp在接收到每个字节时都会更新,但只有在写使能有效时,ram_wrdata才会将组合好的数据输出到 RAM。
控制器代码如下所示:
代码展示
//输入输出模块
module img_rx_wr(
clk,
reset_n,
rx_data,//串口的导入数据
rx_down,//串口接受到一个字节的标志信号
ram_wren,//写使能
ram_wraddr,//地址
ram_wrdata,//输出给RAM的数据
led
);
input clk;
input reset_n;
input [7:0]rx_data;
input rx_down;
output reg ram_wren;
output reg [15:0]ram_wraddr;
output wire [15:0]ram_wrdata;
output reg led;
//统计串口接受的计数器计数,看是否达到了128k个数据传输
reg [16:0]data_cnt ;
always@(posedge clk or negedge reset_n)
if(!reset_n)
data_cnt <= 0;
else if(rx_down)
data_cnt <= data_cnt +1'd1;
//定义16位寄存器,用于存放一个像素点也就是两个字节
reg [15:0]rx_data_tmp;
always@(posedge clk or negedge reset_n)
if(!reset_n)
rx_data_tmp <= 0;
else if(rx_down)
rx_data_tmp <= {rx_data_tmp[7:0],rx_data};
//写使能逻辑,利用相与
always@(posedge clk or negedge reset_n)
if(!reset_n)
ram_wren <= 0;
else if(rx_down && data_cnt[0])//利用奇数末尾为1,相与为1,表明写使能有效
ram_wren <= 1'd1;
else
ram_wren <= 0;
//将数据位置写入地址,2个数据一个地址,采用舍弃低位方式
always@(posedge clk or negedge reset_n)
if(!reset_n)
ram_wraddr <= 0;
else if(rx_down && data_cnt[0])//也就是写使能
ram_wraddr <= data_cnt[16:1];
assign ram_wrdata = rx_data_tmp;
//led灯翻转逻辑,也就是串口传输完完整数据后翻转
always@(posedge clk or negedge reset_n)
if(!reset_n)
led <= 0;
else if(data_cnt == 131071 && rx_down)
led <= ~led;
endmodule
实现了以上控制器逻辑之后,我们便需要来编写顶层模块了,也就是将之前学过的相关模块例化并连接。
问题一
要创建一个MMCN IP核创建33M频率的时钟clk_TFT,来实现各模块的时钟同步。
问题二
要创建RAM IP核实现上述要求
问题三
要对原先的TFT模块进行微微修改,也就是要创建一个TFT数据请求输出data_req,用来输出TFT的数据时间段,也就是将原先的TFT_DE的打拍操作,同时将TFT-TS和TFT-VS打两拍,从而消除亚稳态。
TFT代码展示
//定义输入输出端口
module TFT(
clk,
reset_n,
data_in,//用户输入数据
data_req,
TFT_HS,//行同步信号
TFT_VS,//场同步信号
hcount,//行扫描位置
vcount,//场扫描位置
TFT_DE,//数据输出时间段
TFT_CLK,
TFT_DATA,
TFT_BL
);
input clk;
input reset_n;
input [15:0]data_in;
output reg data_req;
output TFT_HS;
output TFT_VS;
output reg [11:0]hcount;//行同步的信号最大值为1056
output reg [11:0]vcount;
output TFT_DE;
output TFT_CLK;
output reg [15:0]TFT_DATA;
output TFT_BL;
// 定义不同的分辨率
//`define resolution_480x272 1 // 时钟为9MHz
//`define resolution_640x480 1 // 时钟为25MHz
`define resolution_800x480 1 // 时钟为33MHz
//`define resolution_800x600 1 // 时钟为40MHz
//`define resolution_1024x600 1 // 时钟为51MHz
//`define resolution_1024x768 1 // 时钟为65MHz
//`define resolution_1280x720 1 // 时钟为74.25MHz
//`define resolution_1920x1080 1 // 时钟为148.5MHz
`ifdef resolution_480x272
`define h_right_border 0
`define h_front_porch 2
`define h_sync_time 41
`define h_back_porch 2
`define h_left_border 0
`define h_data_time 480
`define h_total_time 525
`define v_bottom_border 0
`define v_front_porch 2
`define v_sync_time 10
`define v_back_porch 2
`define v_top_border 0
`define v_data_time 272
`define v_total_time 286
`elsif resolution_640x480
`define h_right_border 0
`define h_front_porch 16
`define h_sync_time 96
`define h_back_porch 48
`define h_left_border 0
`define h_data_time 640
`define h_total_time 800
`define v_bottom_border 0
`define v_front_porch 10
`define v_sync_time 2
`define v_back_porch 33
`define v_top_border 0
`define v_data_time 480
`define v_total_time 525
`elsif resolution_800x480
`define h_right_border 0
`define h_front_porch 40
`define h_sync_time 128
`define h_back_porch 88
`define h_left_border 0
`define h_data_time 800
`define h_total_time 1056
`define v_bottom_border 8
`define v_front_porch 2
`define v_sync_time 2
`define v_back_porch 25
`define v_top_border 8
`define v_data_time 480
`define v_total_time 525
`elsif resolution_800x600
`define h_right_border 0
`define h_front_porch 40
`define h_sync_time 128
`define h_back_porch 88
`define h_left_border 0
`define h_data_time 800
`define h_total_time 1056
`define v_bottom_border 0
`define v_front_porch 1
`define v_sync_time 4
`define v_back_porch 23
`define v_top_border 0
`define v_data_time 600
`define v_total_time 628
`elsif resolution_1024x600
`define h_right_border 0
`define h_front_porch 24
`define h_sync_time 136
`define h_back_porch 160
`define h_left_border 0
`define h_data_time 1024
`define h_total_time 1344
`define v_bottom_border 0
`define v_front_porch 1
`define v_sync_time 3
`define v_back_porch 28
`define v_top_border 0
`define v_data_time 600
`define v_total_time 632
`elsif resolution_1024x768
`define h_right_border 0
`define h_front_porch 24
`define h_sync_time 136
`define h_back_porch 160
`define h_left_border 0
`define h_data_time 1024
`define h_total_time 1344
`define v_bottom_border 0
`define v_front_porch 3
`define v_sync_time 6
`define v_back_porch 29
`define v_top_border 0
`define v_data_time 768
`define v_total_time 806
`elsif resolution_1280x720
`define h_right_border 0
`define h_front_porch 110
`define h_sync_time 40
`define h_back_porch 220
`define h_left_border 0
`define h_data_time 1280
`define h_total_time 1650
`define v_bottom_border 0
`define v_front_porch 5
`define v_sync_time 5
`define v_back_porch 36
`define v_top_border 0
`define v_data_time 720
`define v_total_time 750
`elsif resolution_1920x1080
`define h_right_border 0
`define h_front_porch 88
`define h_sync_time 44
`define h_back_porch 148
`define h_left_border 0
`define h_data_time 1920
`define h_total_time 2200
`define v_bottom_border 0
`define v_front_porch 4
`define v_sync_time 5
`define v_back_porch 36
`define v_top_border 0
`define v_data_time 1080
`define v_total_time 1125
`endif
//定义时序中相关信号
//parameter VGA_HS_end = 11'd127;
//parameter hdat_begin = 11'd216;//行数据开始输出位置
//parameter hdat_end = 11'd1016;//行数据停止输出位置
//parameter hpixel_end = 11'd1055;//行扫描的最大位置处
//parameter VGA_VS_end = 11'd1;
//parameter vdat_begin = 11'd35;
//parameter vdat_end = 11'd515;
//parameter vpixel_end = 11'd524;
//将上述的parameter定义改为参数定义,便于适配
parameter TFT_HS_end = `h_sync_time - 1,
hdat_begin = `h_sync_time + `h_back_porch + `h_left_border,
hdat_end = `h_sync_time + `h_back_porch + `h_left_border + `h_data_time,
hpixel_end = `h_total_time - 1,
TFT_VS_end = `v_sync_time - 1,
vdat_begin = `v_sync_time + `v_back_porch + `v_top_border,
vdat_end = `v_sync_time + `v_back_porch + `v_top_border + `v_data_time,
vpixel_end = `v_total_time - 1;
//定义计数器,开始行扫描信号,场扫描信号计数
reg [11:0]hcount_r;
reg [11:0]vcount_r;
always@(posedge clk or negedge reset_n)
if(!reset_n)
hcount_r <= 11'd0;
else if(hcount_r == hpixel_end)
hcount_r <= 11'd0;
else
hcount_r <= hcount_r + 1'd1;
always@(posedge clk or negedge reset_n)
if(!reset_n)
vcount_r <= 11'd0;
else if(hcount_r == hpixel_end) begin
if(vcount_r == vpixel_end)
vcount_r <= 11'd0;
else
vcount_r <= vcount_r + 1'd1;
end
else
vcount_r <= vcount_r;
assign TFT_CLK = ~clk;
always@(posedge clk)
data_req <= ((hcount_r >= hdat_begin) &&(hcount_r < hdat_end ) &&(vcount_r >= vdat_begin )&&(vcount_r < vdat_end))?1'b1:1'b0;
reg [3:0]TFT_DE_r;//将data_req打两拍
always@(posedge clk)begin
TFT_DE_r[0] <= data_req;
TFT_DE_r[3:1] <= TFT_DE_r[2:0];
end
assign TFT_DE = TFT_DE_r[2];
always@(posedge clk)begin
hcount <= data_req ? (hcount_r - hdat_begin) :10'd0;
vcount <= data_req ? (vcount_r - vdat_begin) :10'd0;
end
always@(posedge clk)begin
TFT_DATA <= (data_req)? data_in: 16'h0000;
end
reg [3:0]TFT_HS_r;//同样打两拍
always@(posedge clk)begin
TFT_HS_r[0] = (hcount_r > TFT_HS_end) ?1'b1:1'b0;
TFT_HS_r[3:1] <= TFT_HS_r[2:0];
end
assign TFT_HS = TFT_HS_r[2];
reg [3:0]TFT_VS_r;//同样打两拍
always@(posedge clk)begin
TFT_VS_r[0] = (vcount_r > TFT_VS_end) ?1'b1:1'b0;
TFT_VS_r[3:1] <= TFT_VS_r[2:0];
end
assign TFT_VS = TFT_VS_r[2];
//定义相关信号
//assign TFT_HS = (hcount_r > TFT_HS_end) ?1'b1:1'b0;
//assign TFT_VS = (vcount_r > TFT_VS_end) ?1'b1:1'b0;
//assign TFT_DE =((hcount_r >= hdat_begin) &&(hcount_r < hdat_end ) &&(vcount_r >= vdat_begin )&&(vcount_r < vdat_end))?1'b1:1'b0;
//assign hcount = TFT_DE ? (hcount_r - hdat_begin) :10'd0;
//assign vcount = TFT_DE ? (vcount_r - vdat_begin) :10'd0;
//assign data_out = (TFT_DE) ? data_in : 24'h000000;
//assign TFT_CLK = ~clk;
assign TFT_BL = 1;
endmodule
顶层模块代码展示
//输入输出模块
module UART_RAM_TFT(
clk,
reset_n,
uart_rx,
TFT_RGB,//tft数据输出
TFT_HS,//TFT行同步信号
TFT_VS,//TFT场同步信号
TFT_DE,//TFT数据有效信号
TFT_CLK,
TFT_BL,//TFT背光
led
);
input clk;
input reset_n;
input uart_rx;
output [15:0]TFT_RGB;
output TFT_HS;
output TFT_VS;
output TFT_DE;
output TFT_CLK;
output TFT_BL;
output led;
//定义相关变量
wire [7:0]rx_data;
wire rx_down;
wire ram_wren;//RAM 的写使能信号,高电平有效时允许向 RAM 写入数据。
wire [15:0]ram_wraddr;//RAM 的写地址信号,用于指定写入数据的存储位置。
wire [15:0]ram_wrdata;//要写入 RAM 的数据
reg [15:0]ram_rdaddr;//用于存储从 RAM 中读取数据的地址,是一个寄存器类型,因为它需要在always块中被赋值和更新。
wire clk_TFT;//TFT 显示屏的时钟信号,由 MMCM 模块生成。
wire [15:0]ram_rddata;//从 RAM 中读取出来的 16 位数据。
wire [11:0]hcount,vcount;
wire ram_data_en;//用于控制是否从 RAM 中读取数据的使能信号
wire [15:0]disp_data;//最终要显示在 TFT 显示屏上的数据,根据ram_data_en信号从 RAM 中读取或赋值为 0。
wire locked;
//例化相关模块
MMCM MMCM
(
// Clock out ports
.clk_out1(clk_TFT), // output clk_out1
// Status and control signals
.reset(!reset_n), // input reset
.locked(locked), // output locked
// Clock in ports
.clk_in1(clk));
uart_rx1 uart_rx1(
.clk(clk),
.reset_n(reset_n),
.uart_rx(uart_rx),
.rx_data(rx_data),
.rx_down(rx_down)
);
img_rx_wr img_rx_wr(
.clk(clk_TFT),
.reset_n(reset_n),
.rx_data(rx_data),//串口的导入数据
.rx_down(rx_down),//串口接受到一个字节的标准信号
.ram_wren(ram_wren),//写使能
.ram_wraddr(ram_wraddr),//地址
.ram_wrdata(ram_wrdata),//输出给RAM的数据
.led(led)
);
RAM RAM (
.clka(clk), // input wire clka
.ena(1), // input wire ena
.wea(ram_wren), // input wire [0 : 0] wea
.addra(ram_wraddr), // input wire [15 : 0] addra
.dina(ram_wrdata), // input wire [15 : 0] dina
.clkb(clk_TFT), // input wire clkb
.enb(1), // input wire enb
.addrb(ram_rdaddr), // input wire [15 : 0] addrb
.doutb(ram_rddata) // output wire [15 : 0] doutb
);
//RAM中存储的图像是256*256像素矩阵,取完一个数据地址加一
wire data_req;
always@(posedge clk_TFT or negedge reset_n)
if(!reset_n)
ram_rdaddr <= 0;
else if(ram_data_en)
ram_rdaddr <= ram_rdaddr + 1'd1;
assign ram_data_en = data_req && (hcount >= 272 && hcount < 528) &&(vcount >= 112 && vcount <= 368);
assign disp_data = ram_data_en ? ram_rddata : 0;
TFT TFT(
.clk(clk_TFT),
.reset_n(reset_n),
.data_in(disp_data),//用户输入数据
.data_req(data_req),
.TFT_HS(TFT_HS),//行同步信号
.TFT_VS(TFT_VS),//场同步信号
.hcount(hcount),//行扫描位置
.vcount(vcount),//场扫描位置
.TFT_DE(TFT_DE),//数据输出时间段
.TFT_CLK(TFT_CLK),
.TFT_DATA(TFT_RGB),
.TFT_BL(TFT_BL)
);
endmodule
同时需要必须的一些小文件来进行图像输入操作,例如网友创建的COE文件程序"BMP2MIF"可将图像BMP文件转换为COE格式输入到RAM中,从而在TFT屏幕显示。

最后利用UART串口发送工具将图片文件发送即可使用!!!