FPGA学习历程(九):基于串口蓝牙的图像无线传输与VGA显示


以下内容并非学习自 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:显示屏中间那条意义不明的横线是屏幕自身的问题,与图像数据流无关。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值