基于FPGA的串口接收lcd1602显示

简介

DE2开发板上使用uart接收来自pc串口发送的字符,通过lcd1602液晶屏显示。

开发板:DE2
型号:EP2C35F672C6
开发工具:Quartus II 13.0 + Modelsim 10.5 SE
全局时钟:50M

串口接收模块

波特率:115200
数据位:8
校验:
停止位:1
不使用状态机,只有接收模块,代码:

module uart_rx(
    // clk, rst_n
    input               clk,
    input               rst_n,
    
    // uart rx
    input               rx,
    
    // valid data output
    output reg          valid,
    output reg [7:0]    data
);
    parameter CLK_FRE      = 50_000_000;
    parameter BAUD_RATE    = 115200;       // 默认115200波特率
    parameter cnt_baud_max = CLK_FRE / BAUD_RATE - 1;  // 115200波特率下是434时钟周期一波特

    // 打两拍防止亚稳态,rx_3用于检测rx下降沿
    reg         rx_1;
    reg         rx_2;
    reg         rx_3;
    // 工作使能
    reg         work_ena;
    // 波特计数器 比特计数器
    reg [8:0]   cnt_baud;
    reg [4:0]   cnt_bit;
    // 计数器使能、清零
    wire        cnt_baud_ena;
    wire        cnt_baud_end;
    wire        cnt_bit_ena;
    wire        cnt_bit_end;
    // data移位寄存器
    reg [7:0]   data_shift;
    // valid打一拍再输出,和data同步输出
    reg         valid_reg;

    // 打两拍防止亚稳态
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n) begin
            rx_1 <= 0;
            rx_2 <= 0;
            rx_3 <= 0;
        end else begin
            rx_1 <= rx;
            rx_2 <= rx_1;
            rx_3 <= rx_2; 
        end
    end

    // rx下降沿检测,出现下降沿开始工作,接收完数据结束工作
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n) begin
            work_ena <= 0;
        end else begin
            if(~rx_2 & rx_3)
                work_ena <= 1;
            else if(cnt_bit_ena && cnt_bit_end)
                work_ena <= 0;
        end
    end
    
    // 波特计数器
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            cnt_baud <= 0;
        else if(cnt_baud_ena) begin
            if(cnt_baud_end)
                cnt_baud <= 0;
            else
                cnt_baud <= cnt_baud + 1;
        end else
            cnt_baud <= 0;
    end

    assign cnt_baud_ena = work_ena;
    assign cnt_baud_end = (cnt_baud == cnt_baud_max);

    // 比特计数器
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            cnt_bit <= 0;
        else if(cnt_bit_ena) begin
            if(cnt_bit_end)
                cnt_bit <= 0;
            else
                cnt_bit <= cnt_bit + 1;
        end
    end

    assign cnt_bit_ena = (cnt_baud == cnt_baud_max / 2);
    assign cnt_bit_end = (cnt_bit == 8);

    // data移位寄存器
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            data_shift <= 0;
        else if(cnt_bit_ena)
            data_shift <= {rx_2, data_shift[7:1]}; 
    end

    // 内部valid_reg有效信号
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n) begin
            valid_reg <= 0;
        end 
        else if(cnt_bit_ena && cnt_bit_end)
            valid_reg <= 1;
        else
            valid_reg <= 0;
    end

    // valid_reg 打一拍再输出,和data同步
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            valid <= 0;
        else
            valid <= valid_reg;
    end

    // 输出data
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            data <= 0;
        else if(valid_reg)
            data <= data_shift;
    end

endmodule

tb测试文件和波形图就不放了

lcd1602驱动模块

根据datasheet,先进行初始化,再进入工作模式
每次写入使能大概16个时钟周期(50M时钟下20ns一时钟周期)
状态机三个初始化状态 + IDLE + WRITE共5个状态
三个初始化状态:

  1. 设定8位宽、两行显示、5*7点阵
  2. 开显示、开光标显示、开光标闪烁
  3. 清屏

每个初始化流程占大概1.53ms
没深入研究lcd1602,所以没写其他功能,比如清屏,换行等
所以只能复位清屏,第二行没法使用
代码:

module lcd1602_ctrl(
    // clk, rst_n
    input               clk,
    input               rst_n,
    
    // valid data input
    input               valid,
    input [7:0]         data,
    
    // lcd1602 side
    output              LCD_ON,
    output              LCD_BLON,
    output              LCD_RW,
    output              LCD_EN,
    output              LCD_RS,
    output reg [7:0]    LCD_DATA
);
    parameter MODE_SET0 = 0, MODE_SET1 = 1, CLEAR = 2, IDLE = 3, WRITE = 4;
    reg [2:0]   state, next;
    
    // 数据寄存器
    reg [7:0]   data_reg;

    // 初始化计数器,初始化三个状态各保持1.53ms
    reg [16:0]  cnt_init;
    wire        cnt_init_ena;
    wire        cnt_init_end;

    // EN计数器(写入命令和数据需要EN拉高16个时钟周期)
    reg [3:0]   cnt_en;
    reg         cnt_en_ena;
    wire        cnt_en_end;
    
    // 这两个信号不能忘了
    assign LCD_ON = 1;
    assign LCD_BLON = 1;

    // 只写不读
    assign LCD_RW = 0;
    // WRITE状态下是data,其余状态是cmd
    assign LCD_RS = (state == WRITE);

    // 数据寄存器
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            data_reg <= 0;
        else if(valid)
            data_reg <= data;
    end

    // 初始化计数器
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            cnt_init <= 0;
        else if(cnt_init_ena) begin
            if(cnt_init_end)
                cnt_init <= 0;
            else
                cnt_init <= cnt_init + 1;
        end
    end

    assign cnt_init_ena = (state == MODE_SET0 || state == MODE_SET1 || state == CLEAR);
    assign cnt_init_end = (cnt_init == 80_000);

    // EN计数器使能
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            cnt_en_ena <= 0;
        else if(cnt_en_end)
            cnt_en_ena <= 0;
        else if(cnt_init == 100 || state == WRITE)
            cnt_en_ena <= 1;
    end

    // EN 计数器
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            cnt_en <= 0;
        else if(cnt_en_ena) begin
            if(cnt_en_end)
                cnt_en <= 0;
            else
                cnt_en <= cnt_en + 1;
        end
    end

    assign cnt_en_end = (cnt_en == 15);

    assign LCD_EN = cnt_en_ena;

    // 状态机
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            state <= MODE_SET0;
        else
            state <= next;
    end

    always @(*) begin
        case(state)
            MODE_SET0: next = cnt_init_end ? MODE_SET1 : MODE_SET0;
            MODE_SET1: next = cnt_init_end ? CLEAR : MODE_SET1;
            CLEAR    : next = cnt_init_end ? IDLE : CLEAR;
            IDLE     : next = valid ? WRITE : IDLE;
            WRITE    : next = cnt_en_end ? IDLE : WRITE;
            default  : next = MODE_SET0;
        endcase
    end

    // LCD_DATA
    always @(*) begin
        case(state)
            MODE_SET0: LCD_DATA = 8'h38; // 8位宽,两行显示,5*7点阵
            MODE_SET1: LCD_DATA = 8'h0f; // 开显示,开光标显示、闪烁
            CLEAR    : LCD_DATA = 8'h01; // 清屏
            IDLE     : LCD_DATA = 8'h00;
            WRITE    : LCD_DATA = data_reg;
            default  : LCD_DATA = 8'h00;
        endcase
    end

endmodule

tb测试文件和波形图就不放了
顶层模块只需例化这两个模块,将uart_rx模块输出端口validdata接入到lcd1602模块的validdata输入端口即可。

结果

代码经过modelsim仿真和上板验证。
实验结果:串口发送hello world!FPGA上的lcd1602就显示hello world!
本次实验是串口传图实验的第一步,使用串口接收数据。

你可能感兴趣的:(FPGA,Quartus,Modelsim,fpga开发)