目录
以下内容并非学习自 SDRAM 那些事儿第二季。个人对于 SDRAM 那些事儿第二季的理解止步于 VGA 驱动。后面的 SDRAM 控制器参数完善与 IP 核化跟着调好了仿真,但是代码上板就炸,而且教程中设计的 SDRAM 控制器在仲裁读写操作上有 bug 而使得 VGA 显示存在问题。由于第一季的学习所以对 SDRAM 有了基础的理解,且对 FPGA 的学习需求更多还是来自课程设计要求,而非个人意向的深耕方向,因此接下来并未再沿用开源骚客教程中的 SDRAM 控制器,转而上手正点原子的 IP 核式 SDRAM 驱动模块(甚至用在自己板子上时序参数都不用改直接就能用,正点原子yyds)完成后续工作。
一、发送图像数据到FPGA
目的在于完成任务,一切从简,就使用串口蓝牙将图像数据发送到 FPGA 上。VGA 显示的图像数据格式是 RGB565,也就是 16bit 的像素值,而串口收发的数据位宽一般是 8bit,因此需要发送 RGB565 格式的图像数据,同时对串口接收到的数据做一次拼接才能获取到正确的像素值。
1.1 图像数据的生成
这里使用工具软件 Image2Lcd 将目标图片转化为 bin 文件。因为使用串口发送数据的缘故,图像大一些就会非常非常慢,为了让图像尽可能显示得快一些,这里使用的是 320x240 分辨率的图片:
设置完毕以后点击保存即可将 bin 文件输出到指定存储位置:
1.2 FPGA端串口接收模块
这里的接收模块是以 SDRAM 那些事儿第一季中设计的串口接收模块为基础修改的,原谅咱比较懒,第一季学习过程中敲出来的代码不用白不用:
1.2.1 uart_rx.v
总体上来说改动其实不大,也就是增加了 16bit 的数据输出以及对应的 po_flag 而已,原本 8bit 的数据以及 po_flag 则被设置为内置的信号了。
//`define SIM // Modelsim 仿真宏定义
module uart_rx
(
// system signals
input sclk , // 系统时钟 50MHz
input s_rst_n , // 系统复位信号,低电平有效
// UART Interface
input rs232_rx , // rx 信号
// others
output reg [15:0] pixel_data , // rx 数据
output reg po_flag // 数据可用标识
);
/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
// 内部参数定义
`ifndef SIM
localparam BAUD_END = 433 ; // 真实参数
`else
localparam BAUD_END = 56 ; // 仿真参数,改小的目的仅为方便仿真
`endif
localparam BAUD_M = BAUD_END/2 - 1 ;
localparam BIT_END = 8 ;
// 以下全是模块设计时序图中出现的非输出信号
reg rx_r1 ; // rx 信号延一拍
reg rx_r2 ;
reg rx_r3 ;
reg rx_flag ;
reg [12:0] baud_cnt ;
reg bit_flag ;
reg [3:0] bit_cnt ;
// 模块内部信号
reg [7:0] rx_data ; // 一次串口接收的 8bit 数据
reg rx_done ; // 一次串口接收完成信号
reg [1:0] cnt_pixel ; // 8bit 数据计数,两个拼接一次
wire rx_neg ; // rx 信号下降沿
/**************************************************************************/
/******************************* Main Code ********************************/
/**************************************************************************/
// 捕获下降沿:rx_r2 为低电平且 rx_r3 为高电平时
assign rx_neg = ~rx_r2 & rx_r3;
// 延三拍处理
always @(posedge sclk)
begin
rx_r1 <= rs232_rx;
rx_r2 <= rx_r1;
rx_r3 <= rx_r2;
end
// rx_flag
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
rx_flag <= 1'b0;
else if(rx_neg == 1'b1) // 在起始位就置位了,所以即便在数据传输过程中出现了下降沿,rx_flag 也还是为高,并无影响
rx_flag <= 1'b1;
else if(bit_cnt == 'd0 && baud_cnt == BAUD_END)
rx_flag <= 1'b0;
end
// baud_cnt
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
baud_cnt <= 'd0;
else if(baud_cnt == BAUD_END) // 计满清零
baud_cnt <= 'd0;
else if(rx_flag == 1'b1)
baud_cnt <= baud_cnt + 1'b1;
else // rx_flag 为低时当然要清零
baud_cnt <= 'd0;
end
// bit_flag
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
bit_flag <= 1'b0;
else if(baud_cnt == BAUD_M)
bit_flag <= 1'b1;
else
bit_flag <= 1'b0;
end
// bit_cnt
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
bit_cnt <= 'd0;
else if(bit_flag == 1'b1 && bit_cnt == BIT_END) // 这里和下面的顺序不能颠倒,因为两者的适用范围的大小关系
bit_cnt <= 'd0;
else if(bit_flag == 1'b1)
bit_cnt <= bit_cnt + 1'b1;
end
// rx_data
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
rx_data <= 'd0;
else if(bit_flag == 1'b1 && bit_cnt >= 'd1) // bit_cnt 设置为大于 1 只是为了滤除起始位,实际上不加这个条件也可以,起始位也会被移出去
rx_data <= {rx_r2,rx_data[7:1]};
end
// rx_done
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
rx_done <= 1'b0;
else if(bit_cnt == BIT_END && bit_flag == 1'b1)
rx_done <= 1'b1;
else
rx_done <= 1'b0;
end
// cnt_pixel
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
cnt_pixel <= 'd0;
else if(cnt_pixel == 'd2)
cnt_pixel <= 'd0;
else if(rx_done == 1'b1)
cnt_pixel <= cnt_pixel + 1'b1;
else
cnt_pixel <= cnt_pixel;
end
// pixel_data
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
pixel_data <= 'd0;
else if(rx_done == 1'b1 && cnt_pixel == 'd0) // 先接收高位还是先接收低位
pixel_data[15:8] <= rx_data;
else if(rx_done == 1'b1 && cnt_pixel == 'd1)
pixel_data[7:0] <= rx_data;
else
pixel_data <= pixel_data;
end
// po_flag
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
po_flag <= 'd0;
else if(bit_cnt == 'd0 && cnt_pixel == 'd2)
po_flag <= 1'b1;
else
po_flag <= 'd0;
end
endmodule
1.2.2 tb_uart_rx.v
这里的激励文件还是使用 SDRAM 那些事儿第一季的串口接收模块里写的,完全无需修改:
`timescale 1ns/1ns
module tb_uart_rx;
reg sclk;
reg s_rst_n;
reg rs232_tx;
wire po_flag;
wire [15:0] pixel_data;
reg [7:0] mem_a[3:0]; // 定义一个存储器
/************************************ define system signals ************************************/
initial begin
sclk = 1;
s_rst_n <= 0;
rs232_tx <= 1; // 为高表示处于空闲状态
#100 // 延时 100ns
s_rst_n <= 1; // 释放复位信号
#100
tx_byte();
end
always #5 sclk = ~sclk; // 模拟系统时钟,延时 5ns 翻转一次,10ns 一个周期
/***********************************************************************************************/
initial $readmemh("./tx_data.txt",mem_a); // 从当前目录下读取 txt 文本内容并存放到存储器中
// 发送串口数据
task tx_byte();
integer i;
for(i=0; i<4; i=i+1)
begin
tx_bit(mem_a[i]);
end
endtask
// 模拟串口发送时序
task tx_bit
(
input [7:0] data
);
integer i; // 整型变量
for(i=0; i<10; i=i+1) // 仿真脚本里用用就行,正式模块里不建议使用 for
begin
case(i)
0: rs232_tx <= 1'b0; // 发送起始位
1: rs232_tx <= data[0];
2: rs232_tx <= data[1];
3: rs232_tx <= data[2];
4: rs232_tx <= data[3];
5: rs232_tx <= data[4];
6: rs232_tx <= data[5];
7: rs232_tx <= data[6];
8: rs232_tx <= data[7];
9: rs232_tx <= 1'b1; // 发送停止位
endcase
#560; // 延时,防止瞬时发送
end
endtask
// uart_rx 模块例化
uart_rx uart_rx_inst
(
// system signals
.sclk (sclk), // 系统时钟 50MHz
.s_rst_n (s_rst_n), // 系统复位信号,低电平有效
// UART Interface
.rs232_rx (rs232_tx), // rx 信号
// others
.pixel_data (pixel_data), // rx 数据
.po_flag (po_flag) // 数据可用标识
);
endmodule
1.3 Modelsim仿真
调试重心就在第二个 8bit rx_data 拼接那里。实际上模块代码里修改的条件基本都是跑仿真硬凑出来的波形。
二、SDRAM缓存图像数据
IP 核式 SDRAM 控制器:取自正点原子开拓者 FPGA 教程第 34 个例程:34_top_sd_photo_vga。这个 SDRAM 控制器已经把用户接口都留出来了,咱需要做的只是用两根线把用户写端口的 wr_en 和 wr_data 连到 uart_rx 模块的 po_flag 和 pixel_data 上就完了。
本节没有分析,因为不知道为什么正点原子写的 SDRAM 控制器无论是否按照自己板子上 SDRAM 的手册设置时序参数都可以在自己板子上正常工作。不过既然都能直接用了那管他里面怎么跑的呢(又不是专门研究这个,理不直气也壮.jpg)。
三、VGA显示图像
VGA 驱动把前一篇博客里的拿出来改一改就可以了。一开始还想着用 1024x768 的分辨率,但是实在没什么精力去找和测验更快捷的数据传输方法了,既然决定用串口传输图像数据,那 VGA 这边肯定要配合一下改成支持的最小分辨率(640x480):
module vga_driver(
// system signals
input sclk , // 像素时钟
input s_rst_n , // 复位信号,低电平有效
// VGA
output wire vga_clk , // 驱动时钟
output wire vga_hsync , // 行同步信号
output wire vga_vsync , // 场同步信号
output wire vga_blank ,
output wire [15:0] vga_rgb , // RGB565
// User Interface
input [15:0] img_data , // 要显示的图像数据
output wire data_req // 请求像素点颜色数据输入
);
/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
// 不需要定义 H_FRONT_PORCH 和 V_FRONT_PORCH 是因为直接使用 H_TOTAL_TIME 与 V_TOTAL_TIME 作为计数周期
// 640x480 25MHz
localparam H_TOTAL_TIME = 800;
localparam H_ADD_TIME = 640;
localparam H_SYNC_TIME = 96;
localparam H_BACK_PORCH = 48;
localparam V_TOTAL_TIME = 525;
localparam V_ADD_TIME = 480;
localparam V_SYNC_TIME = 2;
localparam V_BACK_PORCH = 33;
// 1024x768 65MHz
//localparam H_TOTAL_TIME = 1344;
//localparam H_ADD_TIME = 1024;
//localparam H_SYNC_TIME = 136;
//localparam H_BACK_PORCH = 160;
//localparam V_TOTAL_TIME = 806;
//localparam V_ADD_TIME = 768;
//localparam V_SYNC_TIME = 6;
//localparam V_BACK_PORCH = 29;
wire vga_en; // VGA 显示使能信号
reg [10:0] cnt_h; // 从行同步信号拉高开始作为计数周期的起点
reg [9:0] cnt_v; // 场同步计数同理
/**************************************************************************/
/******************************* Main Code ********************************/
/**************************************************************************/
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
cnt_h <= 'd0;
else if(cnt_h >= H_TOTAL_TIME - 1'b1)
cnt_h <= 'd0;
else
cnt_h <= cnt_h + 1'b1;
end
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
cnt_v <= 'd0;
else if((cnt_v >= V_TOTAL_TIME - 1'b1) && cnt_h >= H_TOTAL_TIME - 1'b1) // 最后一行也扫描完了才算完整的一场
cnt_v <= 'd0;
else if(cnt_h >= H_TOTAL_TIME - 1'b1)
cnt_v <= cnt_v + 1'b1;
end
彩条测试
//always @(posedge sclk or negedge s_rst_n)
//begin
// if(s_rst_n == 1'b0)
// vga_rgb <= 'd0;
// else if(cnt_v >= (V_SYNC_TIME + V_BACK_PORCH) && cnt_v < (V_SYNC_TIME + V_BACK_PORCH + V_ADD_TIME)) // 场有效区域
// begin
// if(cnt_h >= (H_SYNC_TIME + H_BACK_PORCH - 1) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH - 1 + 10))
// vga_rgb <= 16'hffff;
// else if(cnt_h >= (H_SYNC_TIME + H_BACK_PORCH - 1) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH - 1 + 256))
// vga_rgb <= 16'hff00;
// else if(cnt_h >= (H_SYNC_TIME + H_BACK_PORCH - 1) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH - 1 + 512))
// vga_rgb <= 16'h00ff;
// else if(cnt_h >= (H_SYNC_TIME + H_BACK_PORCH- 1) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH - 1 + 768))
// vga_rgb <= 16'hf0f0;
// else if(cnt_h >= (H_SYNC_TIME + H_BACK_PORCH- 1) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH - 1 + 1014))
// vga_rgb <= 16'h0f0f;
// else if(cnt_h >= (H_SYNC_TIME + H_BACK_PORCH- 1) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH - 1 + 1024))
// vga_rgb <= 16'hffff;
// else
// vga_rgb <= 16'h0000;
// end
// else
// vga_rgb <= 16'h0000;
//end
assign vga_en = (cnt_h >= (H_SYNC_TIME + H_BACK_PORCH + 8'd160) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH + H_ADD_TIME - 8'd160) &&
cnt_v >= (V_SYNC_TIME + V_BACK_PORCH + 7'd120) && cnt_v < (V_SYNC_TIME + V_BACK_PORCH + V_ADD_TIME - 7'd120)) ? 1'b1 : 1'b0;
assign vga_hsync = (cnt_h <= H_SYNC_TIME - 1'b1) ? 1'b0 : 1'b1;
assign vga_vsync = (cnt_v <= V_SYNC_TIME - 1'b1) ? 1'b0 : 1'b1;
assign vga_blank = vga_hsync & vga_vsync;
assign vga_clk = ~sclk;
assign vga_rgb = (vga_en == 1'b1) ? img_data : 16'h0000;
assign data_req = (cnt_h >= (H_SYNC_TIME + H_BACK_PORCH - 1'b1 + 8'd160) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH + H_ADD_TIME - 1'b1 - 8'd160) &&
cnt_v >= (V_SYNC_TIME + V_BACK_PORCH + 7'd120) && cnt_v < (V_SYNC_TIME + V_BACK_PORCH + V_ADD_TIME - 7'd120)) ? 1'b1 : 1'b0;
endmodule
PS:vga_en 和 data_req 中另加的偏移仅仅是为了让图像显示在屏幕的正中间。
四、整体功能验证
将 sof 下载到板卡中,打开串口调试助手,配置好波特率,勾选十六进制发送,然后选择发送文件(.bin 文件),点击发送,就可以看到图片慢慢出现在屏幕中心了(把 SDRAM 控制器的乒乓操作使能关了,不然在图像数据全部发送完成之前看不到图像)。没错,即便把图片分辨率改到 320x240 也还是很慢 Orz…
320 x 240 x 2 = 76800 x 2 = 153600,一个像素点两个字节:
PS:显示屏中间那条意义不明的横线是屏幕自身的问题,与图像数据流无关。