基于Lattice XO2-4000HC FPGA核心板及电子森林综合训练底板的ADC数字电压表及OLED显示设计(Verilog)

📌 前言

本项目基于Lattice XO2-4000HC核心板,并使用基于小脚丫FPGA的综合技能训练平台,使用Verilog编程,Lattice Diamond 3.10开发环境,实现了如下功能:

  • 利用底板上的串行ADC对旋转电位计产生的0~3.3V电压进行转换;
  • 电压值显示在底板上的OLED屏幕(OLED-12832,集成SSD1306驱动芯片);
  • ADC转换的电压数据(个位+一位小数)在数码管及LED灯(8位数字量)显示。

⚠️ 注意

  1. 本文仅作为项目记录,限于所参考的资料(代码、电路图、手册等)较多且分散,难以一一标注清晰,但会大致标注引用与下载链接;
  2. 虽然Verilog的设计模块遵循自顶而下顺序,但在讲述的过程中作者认为需遵照设计时自下而上的设计思路;
  3. 各模块验证与否会标注,testbench程序也会提供,验证环境QuestaSim-64 2021.1;
  4. 本文仅限于学习用途,项目所有代码均开源,如遇CSDN动态调整下载积分等问题,请与作者联系;若转载请注意版权等问题;
  5. 由于作者水平有限,如有错误或疑问,欢迎交流。

🔑 该项目将记录在作者的GitHub上,待补充。


👾 设计模块及设计思路

综合训练底板及其与GPIO引脚的电路原理图如下图所示,详细电路图参见STEP-MXO2V2.2原理图

综合训练底板

🍤 ADC驱动及数码管显示模块:adc_driver2seg

adc_driver2seg.v 文件下的 adc_driver2seg 模块

ADC驱动模块(已验证)

adc_driver.v 文件下的 adc_driver 模块

根据基于Lattice的XO2-4000HC FPGA的核心模块的综合训练底板原理图及硬件手册的电路图,底板自带一串行ADC(ADS7868),通过可调电阻获得0~3.3V电压,其ADC输出端(ADC_SDO)、时钟(ADC_CLK)及片选(ADC_CS#,低电平有效)与FPGA的IO口连接,因此需要ADC驱动模块完成对ADC数据的采样。

参考TI:ADS7868数据手册,其各引脚功能如下图所示。
ADS7868引脚图
简要说明一下SDO端口,该端口每在SCLK时钟下降沿输出转换结果,从MSB至LSB,对于ADS7868其分辨率为8位(即8位数字量);当片选信号拉低后会产生连续4个0,在转换结束后SDO端口呈变为高阻态(Hi-Z)。

💡 着重关注ADS7868的时序图:

  1. CS片选有效后,在下个SCLK下降沿前,SDO便会输出0,而后的3个SCLK下降沿各输出0
  2. 之后每个SCLK下降沿后的小段延时后(tD),会产生新的转换数据,因此可以考虑在SCLK上升沿接收上个下降沿产生的数据
  3. 接收完8位数字量后,CS置位,一次转换完成,可以考虑产生一次EOC脉冲以控制CS。
    ADS7868采样时序图

两种驱动模块代码及时序波形的分析比较参见《基于Lattice XO2-4000HC FPGA核心板ADS7868驱动模块及波形分析(Verilog)》,在此仅给出状态机形式的ADC驱动模块代码:

module adc_driver #(
    parameter ADC_WIDTH = 8
)(
    input sys_clk,
    input rst_n,
    input sdo,
    output sclk,
    output reg adc_csn,
    output reg [ADC_WIDTH-1:0] adc_data
);
    localparam IDLE = 2'b00;
    localparam HOLD = 2'b01;
    localparam CONVERT = 2'b10;
    localparam FINISH = 2'b11;

    wire adc_eoc;

    reg [ADC_WIDTH-1:0] adc_data_reg;
    reg [1:0] state;
    reg [1:0] CNT_0;
    reg [2:0] CNT_data;

    assign sclk = sys_clk;
    assign adc_eoc = (state == FINISH);

    always @(posedge sys_clk or negedge rst_n) begin
        if(!rst_n)
            adc_csn <= 1'b1;
        else if(adc_eoc)
            adc_csn <= 1'b1;
        else
            adc_csn <= 1'b0;
    end

    always @(posedge sys_clk or negedge rst_n) begin
        if(!rst_n) begin
            state <= IDLE;
            adc_data <= 8'b0;
            adc_data_reg <= 8'b0;
            CNT_0 <= 2'b10;
            CNT_data <= 3'b111;
        end
        else
            case(state)
                IDLE: begin
                    state <= HOLD;
                end

                HOLD: begin
                    CNT_0 <= CNT_0 - 1'b1;
                    if(CNT_0 == 0) begin
                        state <= CONVERT;
                        CNT_0 <= 2'b10;
                    end         
                    else
                        state <= HOLD;
                end

                CONVERT: begin
                    CNT_data <= CNT_data - 1'b1;
                    adc_data_reg <= {adc_data_reg[ADC_WIDTH-2:0], sdo};
                    if(CNT_data == 0) begin
                        state <= FINISH;
                        CNT_data <= 3'b111;
                    end
                    else
                        state <= CONVERT;
                end

                FINISH: begin
                    adc_data <= adc_data_reg;
                    state <= IDLE;
                end

                default: state <= IDLE;
            endcase
    end

endmodule
数码管显示模块(已验证)

adc2seg.v 文件下的 adc2seg 模块

adc2seg模块由bin2bcd及两个seg_display子模块组成,顶层模块adc2seg在本章最后一小节详述。

数码管显示模块负责将ADC转换后的8位数字量以一位小数的形式(例如0.5V、3.3V等)显示在数码管上。因此,需要将ADC量化后的数字量( N N N)与模拟量( V i n V_{in} Vin)间做换算:

由于
N = 256 × V i n V r e f V i n = N × V r e f 256 , V r e f = 3.3 V N = 256 \times \frac{V_{in}}{V_{ref}}\\ V_{in}=N\times \frac{V_{ref}}{256},\quad V_{ref}=3.3V N=256×VrefVinVin=N×256Vref,Vref=3.3V

因此有
V i n = N × 0.0129 V_{in}=N\times0.0129 Vin=N×0.0129

所以需要将转换后的数字量*0.0129(此时计算得到的结果在FPGA中仍为二进制),并先将结果转换成BCD码,然后以个位+一位小数形式显示在数码管上。程序实现上,先将数字量乘以129(小数点右移4位),最后的BCD码结果舍弃后4位(小数点左移4位)即可

二进制转BCD码模块

adc2seg.v 文件下的 bin2bcd 模块

将二进制数转换成BCD码的流程可参考CSDN:FPGA Verilog实现二进制转BCD码,采用左移加三的算法(以243为例),计算流程如下图所示:
二进制转BCD流程
参考电子森林:简易电压表设计的代码并做了改进,将if判断操作写成function便于多次调用,改进后的代码如下:

module bin2bcd #(
    parameter ADC_WIDTH = 8
)(
    input clk,
    input rst_n,
    input [ADC_WIDTH-1:0] adc_data,
    output reg [19:0] bcd_code
);
    wire [15:0] bin_code;
    reg [35:0] shift_reg;   // 16*2+4=36
    
    assign bin_code = adc_data * 16'd129; // MAX 32895(1000_0000_0111_1111)

	// BCD码各位数据作满5加3操作
    function [3:0] fout(input [3:0] fin);
        fout = (fin > 4) ? (fin + 2'b11) : fin;
    endfunction

    always @(*) begin
        shift_reg = bin_code;
        if(!rst_n)
            bcd_code = 'b0;
        else begin
            repeat(16) begin
                shift_reg[19:16] = fout(shift_reg[19:16]);
                shift_reg[23:20] = fout(shift_reg[23:20]);
                shift_reg[27:24] = fout(shift_reg[27:24]);
                shift_reg[31:28] = fout(shift_reg[31:28]);
                shift_reg[35:32] = fout(shift_reg[35:32]);
                // 上述五句与下方注释代码等价,若采用下方代码,可注释掉function
			    // if (shift_reg[19:16] >= 5) 
                //     shift_reg[19:16] = shift_reg[19:16] + 2'b11;
			    // if (shift_reg[23:20] >= 5) 
                //     shift_reg[23:20] = shift_reg[23:20] + 2'b11;
			    // if (shift_reg[27:24] >= 5) 
                //     shift_reg[27:24] = shift_reg[27:24] + 2'b11;
			    // if (shift_reg[31:28] >= 5) 
                //     shift_reg[31:28] = shift_reg[31:28] + 2'b11;
			    // if (shift_reg[35:32] >= 5) 
                //     shift_reg[35:32] = shift_reg[35:32] + 2'b11;
			    shift_reg = shift_reg << 1; 
		    end
		    bcd_code = shift_reg[35:16];  
        end
    end

endmodule

开始的 bin_code = adc_data * 16'd129将ADC数字量与129相乘(小数点右移4位),最后转换的BCD码结果bcd_code = shift_reg[35:16]舍弃了低16位,即小数点左移4位。

数码管驱动模块

adc2seg.v 文件下的 seg_display 模块

XO2-4000HC的数码管局部电路如下图所示,控制一位数码管需要9个信号,从高至低分别为{DIG、DP、G、F、E、D、C、B、A},其中DIG控制共阴极/共阳极(DIG = 1'b0为共阴极,反之共阳极),DP控制小数点,其余信号参见下图。
七段数码管局部电路
各信号控制的数码管
参考电子森林:数码管显示代码并做了改进,为提高通用性,将共阴/阳极与小数点亮灭的控制写入模块的参数(parameter)中,本项目采用共阴极数码管。

module seg_display #(
    parameter seg_dig = 1'b0,   // 1'b0:共阴极;1'b1:共阳极
    parameter seg_dp = 1'b1
)(
    input clk,
    input rst_n,
    input [3:0] seg_data,
    output reg [8:0] seg_led    // DIG、DP、G、F、E、D、C、B、A
);
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)
            seg_led <= 9'h3f;   // 0
        else if(!seg_dig)
            case(seg_data)
                0: seg_led <= {seg_dig, seg_dp, 7'h3f}; // 0
	            1: seg_led <= {seg_dig, seg_dp, 7'h06}; // 1
	            2: seg_led <= {seg_dig, seg_dp, 7'h5b}; // 2
	            3: seg_led <= {seg_dig, seg_dp, 7'h4f}; // 3
	            4: seg_led <= {seg_dig, seg_dp, 7'h66}; // 4
	            5: seg_led <= {seg_dig, seg_dp, 7'h6d}; // 5
	            6: seg_led <= {seg_dig, seg_dp, 7'h7d}; // 6
	            7: seg_led <= {seg_dig, seg_dp, 7'h07}; // 7
	            8: seg_led <= {seg_dig, seg_dp, 7'h7f}; // 8
	            9: seg_led <= {seg_dig, seg_dp, 7'h6f}; // 9
                10:seg_led <= {seg_dig, seg_dp, 7'h77}; // A
                11:seg_led <= {seg_dig, seg_dp, 7'h7c}; // B
                12:seg_led <= {seg_dig, seg_dp, 7'h39}; // C
                13:seg_led <= {seg_dig, seg_dp, 7'h5e}; // D
                14:seg_led <= {seg_dig, seg_dp, 7'h79}; // E
                15:seg_led <= {seg_dig, seg_dp, 7'h71}; // F
                default: seg_led <= {seg_dig, seg_dp, 7'h3f};
            endcase
        else
            case(seg_data)
                0: seg_led <= {seg_dig, seg_dp, 7'hc0}; // 0
	            1: seg_led <= {seg_dig, seg_dp, 7'hf9}; // 1
	            2: seg_led <= {seg_dig, seg_dp, 7'ha4}; // 2
	            3: seg_led <= {seg_dig, seg_dp, 7'hb0}; // 3
	            4: seg_led <= {seg_dig, seg_dp, 7'h99}; // 4
	            5: seg_led <= {seg_dig, seg_dp, 7'h92}; // 5
	            6: seg_led <= {seg_dig, seg_dp, 7'h82}; // 6
	            7: seg_led <= {seg_dig, seg_dp, 7'hf8}; // 7
	            8: seg_led <= {seg_dig, seg_dp, 7'h80}; // 8
	            9: seg_led <= {seg_dig, seg_dp, 7'h90}; // 9
                10:seg_led <= {seg_dig, seg_dp, 7'h88}; // A
                11:seg_led <= {seg_dig, seg_dp, 7'h83}; // B
                12:seg_led <= {seg_dig, seg_dp, 7'hc6}; // C
                13:seg_led <= {seg_dig, seg_dp, 7'ha1}; // D
                14:seg_led <= {seg_dig, seg_dp, 7'h86}; // E
                15:seg_led <= {seg_dig, seg_dp, 7'h8e}; // F
                default: seg_led <= {seg_dig, seg_dp, 7'hc0};
            endcase
    end

endmodule
adc2seg顶层模块

将ADC转换的8位数字量(adc_data)输入bin2bcd模块,输出8位BCD码(bcd_code),高4位输入seg_display模块,并点亮小数点,低4位同理,但不点亮小数点。输出端的8位信号oled_display_digital(即转换后的BCD码,个位+一位小数)输入至OLED显示模块。

module adc2seg #(
    parameter ADC_WIDTH = 8,
    parameter seg_dig = 1'b0,
    parameter seg_dp = 2'b10
)(
    input clk,
    input rst_n,
    input [ADC_WIDTH-1:0] adc_data,
    output [7:0] oled_display_digital,
    output [8:0] digital_1,
    output [8:0] digital_2
);
    wire [19:0] bcd_code;

    bin2bcd #(
        .ADC_WIDTH(ADC_WIDTH)
    ) ins1(
       .clk(clk),
       .rst_n(rst_n),
       .adc_data(adc_data),
       .bcd_code(bcd_code) 
    );

    seg_display #(
        .seg_dig(seg_dig),
        .seg_dp(seg_dp[1])
    ) seg_1(
        .clk(clk),
        .rst_n(rst_n),
        .seg_data(bcd_code[19:16]),
        .seg_led(digital_1)
    );

    seg_display #(
        .seg_dig(seg_dig),
        .seg_dp(seg_dp[0])
    ) seg_2(
        .clk(clk),
        .rst_n(rst_n),
        .seg_data(bcd_code[15:12]),
        .seg_led(digital_2)
    );

    assign oled_display_digital = bcd_code[19:12];

endmodule
adc_driver2seg顶层模块

顶层模块将前述ADC驱动与数码管显示模块组合即可。为将8位数字量在8个LED上显示,额外增加了一个8位leds信号,对ADC转换后的数据取反后输出即可(1灭0亮)。

module adc_driver2seg #(
    parameter ADC_WIDTH = 8,
    parameter seg_dig = 1'b0,
    parameter seg_dp = 2'b10
)(
    input sys_clk,
    input rst_n,
    input sdo,
    output sclk,
    output adc_csn,
    output [ADC_WIDTH-1:0] leds,
    output [7:0] oled_display_digital,
    output [8:0] digital_1,
    output [8:0] digital_2
);
    wire [ADC_WIDTH-1:0] adc_data;

    assign leds = ~adc_data;

    adc_driver #(
        .ADC_WIDTH(ADC_WIDTH)
    ) adc_driver(
        .sys_clk(sys_clk),
        .rst_n(rst_n),
        .sdo(sdo),
        .sclk(sclk),
        .adc_csn(adc_csn),
        .adc_data(adc_data)
    );

    adc2seg #(
        .ADC_WIDTH(ADC_WIDTH),
        .seg_dig(seg_dig),
        .seg_dp(seg_dp)
    ) segment_displayer(
        .clk(sys_clk),
        .rst_n(rst_n),
        .adc_data(adc_data),
        .oled_display_digital(oled_display_digital),
        .digital_1(digital_1),
        .digital_2(digital_2)
    );

endmodule

🍯 OLED驱动模块:oled_driver_adc(实验验证)

oled_driver_adc.v 文件下的 oled_driver_adc 模块

控制OLED需要5个信号:

  1. oled_csn:使能,低有效;
  2. oled_rst:复位,低有效;
  3. oled_dcn:数据/指令控制,高为输入数据,低为输入指令;
  4. oled_clk;
  5. oled_data:数据/指令输入。

参考电子森林:OLED驱动说明及Verilog代码实例的OLED驱动模块并作出改进。首先将整个模块拆分为OLED驱动、命令RAM(oled_cmd_RAM,存放初始化命令,准确的说应该是ROM)、字库RAM(oled_char_RAM,存放显示的字符编码)。

SSD1306芯片手册参见百度云:SSD1306 英文手册,提取码csdn(由于CSDN存在相同资源但是不免费)与百度文库:SSD1306 中文手册

oled_cmd_RAM

oled_cmd_RAM.v 文件下的 oled_cmd_RAM 模块

类似一个只读ROM,根据输入的addr读取相应的指令编码,有关SSD1306指令解释参见CSDN:SSD1306 OLED驱动芯片 详细介绍及芯片手册。

此外,可参考OLED显示模块驱动原理及应用一文,该文对SS1306的工作原理和指令进行了说明。

该模块代码如下:

module oled_cmd_RAM #(
    parameter RAM_WIDTH = 8,
    parameter RAM_DEPTH = 32,
    parameter ADDR_WIDTH = 5
)(
    input clk,
    input rst_n,
    input re,
    input [ADDR_WIDTH-1:0] addr,
    output reg [RAM_WIDTH-1:0] data
);
    reg [RAM_WIDTH-1:0] Mem[RAM_DEPTH-1:0];

    always @(posedge rst_n) begin
		Mem[ 0] = 8'hae;	// 关闭屏幕
		Mem[ 1] = 8'h81;
		Mem[ 2] = 8'hff;	// 设置对比度:256级
		Mem[ 3] = 8'ha6;	// 设置显示模式为1亮0灭
		Mem[ 4] = 8'h20;
		Mem[ 5] = 8'h02;	// 设置页寻址模式
		Mem[ 6] = 8'h00; 	// 设置起始列地址低位
		Mem[ 7] = 8'h10;	// 设置起始列地址高位
		Mem[ 8] = 8'h40;	// 设置GDDRAM起始行: (01)xxxxxx -> 40为第0行
		Mem[ 9] = 8'ha1;	// 设置COL0映射到SEG0*
		Mem[10] = 8'hc8;	// 设置COM扫描方向:从COM0至COM[N-1]*
		Mem[11] = 8'ha8;
		Mem[12] = 8'h1F; 	// 设置复用率:即选通的COM行数为1F*
		Mem[13] = 8'hd3;
		Mem[14] = 8'h00;	// 设置垂直显示偏移:00~3F
		Mem[15] = 8'hd5;
		Mem[16] = 8'h80;	// 设置显示时钟分频数和fosc
		Mem[17] = 8'hd9;
		Mem[18] = 8'h1f;	// 设置预充电周期
		Mem[19] = 8'hda;
		Mem[20] = 8'h02;	// 设置COM硬件配置:禁止左右反置、使用序列COM引脚配置*
		Mem[21] = 8'hdb;
		Mem[22] = 8'h40;	// 设置VCOMH输出的高电平 
		Mem[23] = 8'h8d;
		Mem[24] = 8'ha4;	// 设置显示模式为正常模式,此时屏幕输出GDDRAM中的显示数据
		Mem[25] = 8'haf;	// 开启屏幕
    end

    always @(posedge clk) begin
        if(!re)
            data <= Mem[addr]; 
        else
            data <= 8'b0;
    end

endmodule

注:关于指令的注释,若为单条指令则直接在后注释该指令功能,若为两字指令则在第二条指令后注释。

oled_char_RAM

oled_char_RAM.v 文件下的 oled_char_RAM 模块

类似一个只读ROM,根据输入的addr(字符与地址ASCII码对应)读取相应的字符编码(5*8大小),该模块代码如下:

module oled_char_RAM #(
    parameter RAM_WIDTH = 40,
    parameter RAM_DEPTH = 256,
    parameter ADDR_WIDTH = 8
)(
    input clk,
    input rst_n,
    input re,
    input [ADDR_WIDTH-1:0] addr,
    output reg [RAM_WIDTH-1:0] data
);
    reg [RAM_WIDTH-1:0] Mem[RAM_DEPTH-1:0];

    always @(posedge rst_n) begin
        Mem[  0] = {8'h3E, 8'h51, 8'h49, 8'h45, 8'h3E};   // 48  0
		Mem[  1] = {8'h00, 8'h42, 8'h7F, 8'h40, 8'h00};   // 49  1
		Mem[  2] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46};   // 50  2
		Mem[  3] = {8'h21, 8'h41, 8'h45, 8'h4B, 8'h31};   // 51  3
		Mem[  4] = {8'h18, 8'h14, 8'h12, 8'h7F, 8'h10};   // 52  4
		Mem[  5] = {8'h27, 8'h45, 8'h45, 8'h45, 8'h39};   // 53  5
		Mem[  6] = {8'h3C, 8'h4A, 8'h49, 8'h49, 8'h30};   // 54  6
		Mem[  7] = {8'h01, 8'h71, 8'h09, 8'h05, 8'h03};   // 55  7
		Mem[  8] = {8'h36, 8'h49, 8'h49, 8'h49, 8'h36};   // 56  8
		Mem[  9] = {8'h06, 8'h49, 8'h49, 8'h29, 8'h1E};   // 57  9
		Mem[ 10] = {8'h7C, 8'h12, 8'h11, 8'h12, 8'h7C};   // 65  A
		Mem[ 11] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h36};   // 66  B
		Mem[ 12] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h22};   // 67  C
		Mem[ 13] = {8'h7F, 8'h41, 8'h41, 8'h22, 8'h1C};   // 68  D
		Mem[ 14] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h41};   // 69  E
		Mem[ 15] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h01};   // 70  F
		Mem[ 32] = {8'h00, 8'h00, 8'h00, 8'h00, 8'h00};   // 32  sp 
		Mem[ 33] = {8'h00, 8'h00, 8'h2f, 8'h00, 8'h00};   // 33  !  
		Mem[ 34] = {8'h00, 8'h07, 8'h00, 8'h07, 8'h00};   // 34  
		Mem[ 35] = {8'h14, 8'h7f, 8'h14, 8'h7f, 8'h14};   // 35  #
		Mem[ 36] = {8'h24, 8'h2a, 8'h7f, 8'h2a, 8'h12};   // 36  $
		Mem[ 37] = {8'h62, 8'h64, 8'h08, 8'h13, 8'h23};   // 37  %
		Mem[ 38] = {8'h36, 8'h49, 8'h55, 8'h22, 8'h50};   // 38  &
		Mem[ 39] = {8'h00, 8'h05, 8'h03, 8'h00, 8'h00};   // 39  '
		Mem[ 40] = {8'h00, 8'h1c, 8'h22, 8'h41, 8'h00};   // 40  (
		Mem[ 41] = {8'h00, 8'h41, 8'h22, 8'h1c, 8'h00};   // 41  )
		Mem[ 42] = {8'h14, 8'h08, 8'h3E, 8'h08, 8'h14};   // 42  *
		Mem[ 43] = {8'h08, 8'h08, 8'h3E, 8'h08, 8'h08};   // 43  +
		Mem[ 44] = {8'h00, 8'h00, 8'hA0, 8'h60, 8'h00};   // 44  ,
		Mem[ 45] = {8'h08, 8'h08, 8'h08, 8'h08, 8'h08};   // 45  -
		Mem[ 46] = {8'h00, 8'h60, 8'h60, 8'h00, 8'h00};   // 46  .
		Mem[ 47] = {8'h20, 8'h10, 8'h08, 8'h04, 8'h02};   // 47  /
		Mem[ 48] = {8'h3E, 8'h51, 8'h49, 8'h45, 8'h3E};   // 48  0
		Mem[ 49] = {8'h00, 8'h42, 8'h7F, 8'h40, 8'h00};   // 49  1
		Mem[ 50] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46};   // 50  2
		Mem[ 51] = {8'h21, 8'h41, 8'h45, 8'h4B, 8'h31};   // 51  3
		Mem[ 52] = {8'h18, 8'h14, 8'h12, 8'h7F, 8'h10};   // 52  4
		Mem[ 53] = {8'h27, 8'h45, 8'h45, 8'h45, 8'h39};   // 53  5
		Mem[ 54] = {8'h3C, 8'h4A, 8'h49, 8'h49, 8'h30};   // 54  6
		Mem[ 55] = {8'h01, 8'h71, 8'h09, 8'h05, 8'h03};   // 55  7
		Mem[ 56] = {8'h36, 8'h49, 8'h49, 8'h49, 8'h36};   // 56  8
		Mem[ 57] = {8'h06, 8'h49, 8'h49, 8'h29, 8'h1E};   // 57  9
		Mem[ 58] = {8'h00, 8'h36, 8'h36, 8'h00, 8'h00};   // 58  :
		Mem[ 59] = {8'h00, 8'h56, 8'h36, 8'h00, 8'h00};   // 59  ;
		Mem[ 60] = {8'h08, 8'h14, 8'h22, 8'h41, 8'h00};   // 60  <
		Mem[ 61] = {8'h14, 8'h14, 8'h14, 8'h14, 8'h14};   // 61  =
		Mem[ 62] = {8'h00, 8'h41, 8'h22, 8'h14, 8'h08};   // 62  >
		Mem[ 63] = {8'h02, 8'h01, 8'h51, 8'h09, 8'h06};   // 63  ?
		Mem[ 64] = {8'h32, 8'h49, 8'h59, 8'h51, 8'h3E};   // 64  @
		Mem[ 65] = {8'h7C, 8'h12, 8'h11, 8'h12, 8'h7C};   // 65  A
		Mem[ 66] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h36};   // 66  B
		Mem[ 67] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h22};   // 67  C
		Mem[ 68] = {8'h7F, 8'h41, 8'h41, 8'h22, 8'h1C};   // 68  D
		Mem[ 69] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h41};   // 69  E
		Mem[ 70] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h01};   // 70  F
		Mem[ 71] = {8'h3E, 8'h41, 8'h49, 8'h49, 8'h7A};   // 71  G
		Mem[ 72] = {8'h7F, 8'h08, 8'h08, 8'h08, 8'h7F};   // 72  H
		Mem[ 73] = {8'h00, 8'h41, 8'h7F, 8'h41, 8'h00};   // 73  I
		Mem[ 74] = {8'h20, 8'h40, 8'h41, 8'h3F, 8'h01};   // 74  J
		Mem[ 75] = {8'h7F, 8'h08, 8'h14, 8'h22, 8'h41};   // 75  K
		Mem[ 76] = {8'h7F, 8'h40, 8'h40, 8'h40, 8'h40};   // 76  L
		Mem[ 77] = {8'h7F, 8'h02, 8'h0C, 8'h02, 8'h7F};   // 77  M
		Mem[ 78] = {8'h7F, 8'h04, 8'h08, 8'h10, 8'h7F};   // 78  N
		Mem[ 79] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h3E};   // 79  O
		Mem[ 80] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h06};   // 80  P
		Mem[ 81] = {8'h3E, 8'h41, 8'h51, 8'h21, 8'h5E};   // 81  Q
		Mem[ 82] = {8'h7F, 8'h09, 8'h19, 8'h29, 8'h46};   // 82  R
		Mem[ 83] = {8'h46, 8'h49, 8'h49, 8'h49, 8'h31};   // 83  S
		Mem[ 84] = {8'h01, 8'h01, 8'h7F, 8'h01, 8'h01};   // 84  T
        Mem[ 85] = {8'h3F, 8'h40, 8'h40, 8'h40, 8'h3F};   // 85  U
        Mem[ 86] = {8'h1F, 8'h20, 8'h40, 8'h20, 8'h1F};   // 86  V
        Mem[ 87] = {8'h3F, 8'h40, 8'h38, 8'h40, 8'h3F};   // 87  W
        Mem[ 88] = {8'h63, 8'h14, 8'h08, 8'h14, 8'h63};   // 88  X
        Mem[ 89] = {8'h07, 8'h08, 8'h70, 8'h08, 8'h07};   // 89  Y
        Mem[ 90] = {8'h61, 8'h51, 8'h49, 8'h45, 8'h43};   // 90  Z
        Mem[ 91] = {8'h00, 8'h7F, 8'h41, 8'h41, 8'h00};   // 91  [
        Mem[ 92] = {8'h55, 8'h2A, 8'h55, 8'h2A, 8'h55};   // 92  .
        Mem[ 93] = {8'h00, 8'h41, 8'h41, 8'h7F, 8'h00};   // 93  ]
        Mem[ 94] = {8'h04, 8'h02, 8'h01, 8'h02, 8'h04};   // 94  ^
        Mem[ 95] = {8'h40, 8'h40, 8'h40, 8'h40, 8'h40};   // 95  _
        Mem[ 96] = {8'h00, 8'h01, 8'h02, 8'h04, 8'h00};   // 96  '
        Mem[ 97] = {8'h20, 8'h54, 8'h54, 8'h54, 8'h78};   // 97  a
        Mem[ 98] = {8'h7F, 8'h48, 8'h44, 8'h44, 8'h38};   // 98  b
        Mem[ 99] = {8'h38, 8'h44, 8'h44, 8'h44, 8'h20};   // 99  c
        Mem[100] = {8'h38, 8'h44, 8'h44, 8'h48, 8'h7F};   // 100 d
        Mem[101] = {8'h38, 8'h54, 8'h54, 8'h54, 8'h18};   // 101 e
        Mem[102] = {8'h08, 8'h7E, 8'h09, 8'h01, 8'h02};   // 102 f
        Mem[103] = {8'h18, 8'hA4, 8'hA4, 8'hA4, 8'h7C};   // 103 g
        Mem[104] = {8'h7F, 8'h08, 8'h04, 8'h04, 8'h78};   // 104 h
        Mem[105] = {8'h00, 8'h44, 8'h7D, 8'h40, 8'h00};   // 105 i
        Mem[106] = {8'h40, 8'h80, 8'h84, 8'h7D, 8'h00};   // 106 j
        Mem[107] = {8'h7F, 8'h10, 8'h28, 8'h44, 8'h00};   // 107 k
        Mem[108] = {8'h00, 8'h41, 8'h7F, 8'h40, 8'h00};   // 108 l
        Mem[109] = {8'h7C, 8'h04, 8'h18, 8'h04, 8'h78};   // 109 m
        Mem[110] = {8'h7C, 8'h08, 8'h04, 8'h04, 8'h78};   // 110 n
        Mem[111] = {8'h38, 8'h44, 8'h44, 8'h44, 8'h38};   // 111 o
        Mem[112] = {8'hFC, 8'h24, 8'h24, 8'h24, 8'h18};   // 112 p
        Mem[113] = {8'h18, 8'h24, 8'h24, 8'h18, 8'hFC};   // 113 q
        Mem[114] = {8'h7C, 8'h08, 8'h04, 8'h04, 8'h08};   // 114 r
        Mem[115] = {8'h48, 8'h54, 8'h54, 8'h54, 8'h20};   // 115 s
        Mem[116] = {8'h04, 8'h3F, 8'h44, 8'h40, 8'h20};   // 116 t
        Mem[117] = {8'h3C, 8'h40, 8'h40, 8'h20, 8'h7C};   // 117 u
        Mem[118] = {8'h1C, 8'h20, 8'h40, 8'h20, 8'h1C};   // 118 v
        Mem[119] = {8'h3C, 8'h40, 8'h30, 8'h40, 8'h3C};   // 119 w
        Mem[120] = {8'h44, 8'h28, 8'h10, 8'h28, 8'h44};   // 120 x
        Mem[121] = {8'h1C, 8'hA0, 8'hA0, 8'hA0, 8'h7C};   // 121 y
        Mem[122] = {8'h44, 8'h64, 8'h54, 8'h4C, 8'h44};   // 122 z
    end

    always @(posedge clk) begin
        if(re)
            data <= Mem[addr]; 
        else
            data <= 40'b0;
    end

endmodule

注:字符编码写入方式另做文章讨论。

oled_driver_adc顶层模块

该顶层模块的主体FSM与原参考代码相同,在调用OLED的指令编码与字符编码时存在差异。代码如下:

module oled_driver_adc #(
	parameter CMD_WIDTH = 8,   		// LCD命令宽度
    parameter CMD_DEPTH = 5'd25, 	// LCD初始化的命令的数量
    parameter CHAR_WIDTH = 40,		// 一个文字的数据宽度
    parameter CHAR_DEPTH = 7'd123	// 文字库数量
)(
	input sys_clk,
	input rst_n,
	input [7:0] oled_display_digital, 	// 两位ADC数据(一位小数)
	output reg oled_csn,	//OLCD液晶屏使能
	output reg oled_rst,	//OLCD液晶屏复位
	output reg oled_dcn,	//OLCD数据指令控制
	output reg oled_clk,	//OLCD时钟信号
	output reg oled_data	//OLCD数据信号
);
	localparam IDLE = 3'b0, MAIN = 3'b1, INIT = 3'b10;
	localparam SCAN = 3'b11, WRITE = 3'b100, DELAY = 3'b101;
	localparam HIGH	= 1'b1, LOW = 1'b0;
	localparam DATA	= 1'b1, CMD = 1'b0;

    wire [CMD_WIDTH-1:0]  cmd_out;   // cmd_RAM输出的8位命令
    wire [CHAR_WIDTH-1:0] char_out;   // data_RAM输出的40位文字

    reg [7:0] wr_reg;
	reg	[7:0] ypage, xpage_high, xpage_low;   // 位置
	reg	[(8*21-1):0] char;  // 字符串
	reg	[4:0] char_num;   // 文字个数 最多16
    reg [4:0] cmd_addr;
    reg [7:0] char_addr;
	reg	[2:0] cnt_main;
	reg [2:0] cnt_init;
	reg [3:0] cnt_scan;
	reg	[4:0] cnt_write;
	reg	[14:0]num_delay, cnt_delay;
	reg	[2:0] state, state_last;

    oled_cmd_RAM #(
        .RAM_WIDTH(CMD_WIDTH),
        .RAM_DEPTH(CMD_DEPTH),
        .ADDR_WIDTH(5)
    ) CMD_RAM(
        .clk(sys_clk),
        .rst_n(rst_n),
        .re(oled_dcn),
        .addr(cmd_addr),
        .data(cmd_out)
    );

    oled_char_RAM #(
        .RAM_WIDTH(CHAR_WIDTH),
        .RAM_DEPTH(CHAR_DEPTH),
        .ADDR_WIDTH(8)
    ) CHAR_RAM(
        .clk(sys_clk),
        .rst_n(rst_n),
        .re(oled_dcn),
        .addr(char_addr),
        .data(char_out)
    );
 
	always @(posedge sys_clk or negedge rst_n) begin
		if(!rst_n) begin
			cnt_main <= 1'b0; 
            cnt_init <= 1'b0; 
            cnt_scan <= 1'b0; 
            cnt_write <= 1'b0;
			wr_reg <= 1'b0;
			ypage <= 1'b0;
            xpage_high <= 1'b0; 
            xpage_low <= 1'b0;
			char <= 1'b0; 
			char_num <= 1'b0;
            cmd_addr <= 1'b0;
            char_addr <= 1'b0;
			num_delay <= 15'd5; 
            cnt_delay <= 1'b0; 
			oled_csn <= HIGH; 
            oled_rst <= HIGH; 
            oled_dcn <= CMD; 
            oled_clk <= HIGH; 
            oled_data <= LOW;
			state <= IDLE; 
            state_last <= IDLE;
		end 
        else begin
			case(state)
				IDLE: begin
					cnt_main <= 1'b0; 
					cnt_init <= 1'b0; 
					cnt_scan <= 1'b0; 
					cnt_write <= 1'b0;
					wr_reg <= 1'b0;
					ypage <= 1'b0;
					xpage_high <= 1'b0; 
					xpage_low <= 1'b0;
					char <= 1'b0; 
					char_num <= 1'b0; 
					cmd_addr <= 1'b0;
					char_addr <= 1'b0;
					num_delay <= 15'd5; 
					cnt_delay <= 1'b0; 
					oled_csn <= HIGH; 
					oled_rst <= HIGH; 
					oled_dcn <= CMD; 
					oled_clk <= HIGH; 
					oled_data <= LOW;
					state <= MAIN; 
					state_last <= MAIN;
				end

				MAIN: begin
					if(cnt_main >= 3'd6)
						cnt_main <= 3'd5;
					else 
						cnt_main <= cnt_main + 1'b1;
					case(cnt_main)	//MAIN状态
						3'd0: begin state <= INIT; end
						3'd1: begin ypage <= 8'hb0; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "ADC DATA DISPLAY";state <= SCAN; end
						3'd2: begin ypage <= 8'hb1; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "VOLTAGE:   . V  ";state <= SCAN; end
						3'd3: begin ypage <= 8'hb2; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "AUTHOR: ZIRU PAN";state <= SCAN; end
						3'd4: begin ypage <= 8'hb3; xpage_high <= 8'h10; xpage_low <= 8'h00; char_num <= 5'd16; char <= "                ";state <= SCAN; end
						3'd5: begin ypage <= 8'hb1; xpage_high <= 8'h15; xpage_low <= 8'h00; char_num <= 5'd1 ; char <= oled_display_digital[7:4]; state <= SCAN; end
						3'd6: begin ypage <= 8'hb1; xpage_high <= 8'h16; xpage_low <= 8'h00; char_num <= 5'd1 ; char <= oled_display_digital[3:0]; state <= SCAN; end
						default: state <= IDLE;
					endcase
				end

				INIT: begin	//初始化状态
					case(cnt_init)
						5'd0: begin 
								oled_rst <= LOW; 
								cnt_init <= cnt_init + 1'b1; 
							end	//复位有效
						5'd1: begin 
								num_delay <= 15'd25000; 
								state <= DELAY; 
								state_last <= INIT; 
								cnt_init <= cnt_init + 1'b1; 
							end	//延时大于3us
						5'd2: begin 
								oled_rst <= HIGH; 
								cnt_init <= cnt_init + 1'b1; 
							end	//复位恢复
						5'd3: begin 
								num_delay <= 15'd25000; 
								state <= DELAY; 
								state_last <= INIT; 
								cnt_init <= cnt_init + 1'b1; 
							end	//延时大于220us
						5'd4: begin 
								if(cmd_addr >= CMD_DEPTH) begin
									cmd_addr <= 1'b0;
									cnt_init <= cnt_init + 1'b1;
								end 
								else begin	
									cmd_addr <= cmd_addr + 1'b1; 
									num_delay <= 15'd5;
									oled_dcn <= CMD; 
									wr_reg <= cmd_out;
									state <= WRITE; 
									state_last <= INIT;
								end
							end
						5'd5: begin 
							cnt_init <= 1'b0; 
							state <= MAIN; 
						end
						default: state <= IDLE;
					endcase
				end
				
				SCAN: begin
					if(cnt_scan == 4'd12) begin
						if(char_num) 
							cnt_scan <= 4'd3;
						else 
							cnt_scan <= cnt_scan + 1'b1;
					end 
					else if(cnt_scan == 4'd13) 
						cnt_scan <= 4'd0;
					else 
						cnt_scan <= cnt_scan + 1'b1;
					case(cnt_scan)
						4'd0: begin oled_dcn <= CMD; wr_reg <= ypage; state <= WRITE; state_last <= SCAN; end    	//定位列页地址
						4'd1: begin oled_dcn <= CMD; wr_reg <= xpage_low; state <= WRITE; state_last <= SCAN; end	//定位行地址低位
						4'd2: begin oled_dcn <= CMD; wr_reg <= xpage_high; state <= WRITE; state_last <= SCAN; end	//定位行地址高位
						4'd3: begin char_num <= char_num - 1'b1; end
						4'd4: begin char_addr <= char[(char_num*8)+:8]; end
						4'd5: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end	//5*8点阵变成8*8
						4'd6: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end	//5*8点阵变成8*8
						4'd7: begin oled_dcn <= DATA; wr_reg <= 8'h00; state <= WRITE; state_last <= SCAN; end	//5*8点阵变成8*8
						4'd8: begin oled_dcn <= DATA; wr_reg <= char_out[39:32]; state <= WRITE; state_last <= SCAN; end
						4'd9: begin oled_dcn <= DATA; wr_reg <= char_out[31:24]; state <= WRITE; state_last <= SCAN; end
						4'd10:begin oled_dcn <= DATA; wr_reg <= char_out[23:16]; state <= WRITE; state_last <= SCAN; end
						4'd11:begin oled_dcn <= DATA; wr_reg <= char_out[15:8] ; state <= WRITE; state_last <= SCAN; end
						4'd12:begin oled_dcn <= DATA; wr_reg <= char_out[7:0]  ; state <= WRITE; state_last <= SCAN; end
						4'd13:begin state <= MAIN; end
						default: state <= IDLE;
					endcase
				end

				WRITE: begin	//WRITE状态,将数据按照SPI时序发送给屏幕
					if(cnt_write >= 5'd17) 
						cnt_write <= 5'd0;
					else 
						cnt_write <= cnt_write + 1'b1;
					case(cnt_write)
						5'd0: begin oled_csn <= LOW; end	//9位数据最高位为命令数据控制位
						5'd1: begin oled_clk <= LOW; oled_data <= wr_reg[7]; end	//先发高位数据
						5'd2: begin oled_clk <= HIGH; end
						5'd3: begin oled_clk <= LOW; oled_data <= wr_reg[6]; end
						5'd4: begin oled_clk <= HIGH; end
						5'd5: begin oled_clk <= LOW; oled_data <= wr_reg[5]; end
						5'd6: begin oled_clk <= HIGH; end
						5'd7: begin oled_clk <= LOW; oled_data <= wr_reg[4]; end
						5'd8: begin oled_clk <= HIGH; end
						5'd9: begin oled_clk <= LOW; oled_data <= wr_reg[3]; end
						5'd10:begin oled_clk <= HIGH; end
						5'd11:begin oled_clk <= LOW; oled_data <= wr_reg[2]; end
						5'd12:begin oled_clk <= HIGH; end
						5'd13:begin oled_clk <= LOW; oled_data <= wr_reg[1]; end
						5'd14:begin oled_clk <= HIGH; end
						5'd15:begin oled_clk <= LOW; oled_data <= wr_reg[0]; end	//后发低位数据
						5'd16:begin oled_clk <= HIGH; end
						5'd17:begin oled_csn <= HIGH; state <= DELAY; end
						default: state <= IDLE;
					endcase
				end

				DELAY: begin
					if(cnt_delay >= num_delay) begin
						cnt_delay <= 15'd0; 
						state <= state_last; 
					end 
					else 
						cnt_delay <= cnt_delay + 1'b1;
				end

				default: state <= IDLE;
			endcase
		end
	end
endmodule

OLED驱动状态(state)分为IDLEMAININITSCANWRITEDELAY,进入MAIN后跳转至INIT完成复位、指令加载(WRITE中完成写入)等初始化操作,根据state_last跳回MAIN,再在MAIN中获取要显示的字符串,并在SCAN中不断扫描所需要的字的编码,在WRITE中写入至OLED中。

⚠️ 注意

  1. MAIN中所要显示的字符串预留了两个数字显示的位置(“ . V”),第四行全为空格是由于作者实际调试中当未指定第四行显示内容后会出现杂点,但不排除可以通过初始化阶段指令等方式解决;
  2. INIT中5’d4时cmd_addr <= cmd_addr + 1'b1; oled_dcn <= CMD; wr_reg <= cmd_out;:由于cmd_addroled_cmd_RAM地址输入连接,cmd_outoled_cmd_RAM指令输出连接,因此当oled_dcn <= CMD时,在下个时钟上升沿时,cmd_out完成指令的输出,并赋给wr_reg,继而进入WRITE写入至OLED;
  3. SCAN负责完成对字符串中字的编码的读取:4’d3处char_num记录所需写入字的数量,为一个倒数计数器;4’d4处char_addr与字符的关系为ASCII码对应,因此倒着(字符串右侧开始)取一个八位即对应该字符在oled_char_RAM的地址,char_out与字符编码输出连接,由于每个字符由5列8位编码组成,因此将char_out(40位)分5次经wr_reg写入OLED中;
  4. 更多关于SSD1306的细节及讨论参见基于Lattice XO2-4000HC FPGA核心板的SSD1306 OLED12832驱动芯片指令及工作方式详述(Verilog)

🚀 项目顶层模块:proj_top

proj_top.v 文件下的 proj_top 模块

本项目的顶层模块将上述adc_driver2seg模块、oled_driver_adc组合,预留了7个参数。代码如下:

module proj_top #(
    parameter ADC_WIDTH = 8,
    parameter seg_dig = 1'b0,
    parameter seg_dp = 2'b10,
    parameter CMD_WIDTH = 8,   		// LCD命令宽度
    parameter CMD_DEPTH = 5'd26, 	// LCD初始化的命令的数量
    parameter CHAR_WIDTH = 40,		// 一个文字的数据宽度
    parameter CHAR_DEPTH = 7'd123	// 文字库数量
)(
    input sys_clk,
    input rst_n,
    input sdo,
    input sw,
    output sclk,
    output adc_csn,
    output [ADC_WIDTH-1:0] leds,    // LED灯
    output [8:0] digital_1,         // 数码管个位
    output [8:0] digital_2,         // 数码管小数位
    output oled_csn,	            //OLCD液晶屏使能
	output oled_rst,	            //OLCD液晶屏复位
	output oled_dcn,	            //OLCD数据指令控制
	output oled_clk,	            //OLCD时钟信号
	output oled_data,	            //OLCD数据信号
    output [2:0] RGB_led_1,         //RGB三色灯
    output [2:0] RGB_led_2          //RGB三色灯
);
    wire [7:0] oled_display_digital;

    assign RGB_led_1 = {3{~sw}};        //RGB三色灯灭
    assign RGB_led_2 = {3{~sw}};        //RGB三色灯灭

    adc_driver2seg #(
        .ADC_WIDTH(ADC_WIDTH),
        .seg_dig(seg_dig),
        .seg_dp(seg_dp)
    ) adc_driver2seg(
        .sys_clk(sys_clk),
        .rst_n(rst_n),
        .sdo(sdo),
        .sclk(sclk),
        .adc_csn(adc_csn),
        .leds(leds),
        .oled_display_digital(oled_display_digital),
        .digital_1(digital_1),
        .digital_2(digital_2)
    );

    oled_driver_adc #(
        .CMD_WIDTH(CMD_WIDTH),
        .CMD_DEPTH(CMD_DEPTH),
        .CHAR_WIDTH(CHAR_WIDTH),
        .CHAR_DEPTH(CHAR_DEPTH)
    ) oled_driver_adc(
        .sys_clk(sys_clk),
	    .rst_n(rst_n),
	    .oled_display_digital(oled_display_digital),
	    .oled_csn(oled_csn),
	    .oled_rst(oled_rst),
	    .oled_dcn(oled_dcn),
	    .oled_clk(oled_clk),
	    .oled_data(oled_data)
    );

endmodule

注:不知为何,不对三色RGB灯进行任何连接在实际调试中两个灯却常亮,因此代码中改为用一个常闭开关控制两个三色RGB灯熄灭。

整体RTL如下图所示:项目RTL
⚠️ 注意:顶层模块的输入、输出引脚与FPGA的GPIO连接需要根据小脚丫STEP FPGA Training V2.0硬件手册的管脚分配图,在Diamond软件中的“Spreadsheet View”进行分配,之后才可进行后续编译。

🌀 程序下载及调试

🔰 .JED文件

Diamond开发环境及使用参见Lattice Diamond使用案例

由于作者的STEP-MXO2小脚丫采用STM32F072作为下载器的版本,下载方式是将生成的JED文件复制到U盘里,即可完成下载。详细介绍参见STEP-MXO2-C快速上手指南STEP-MXO2-C

🌴 调试

将顶层项目的.JED文件下载至FPGA内,FPGA即刻开始运行。注意先将跳线帽连接左侧R_ADJ与J3端口。调节左侧电位器旋钮,可得到不同电压值。以下为调试效果:
3.2V
0.0V
注:显示的电压范围为0~3.2V。


📚 参考资料及下载链接

[1] 基于小脚丫FPGA的综合技能训练平台
[2] TI:ADS7868数据手册.pdf
[3] 电子森林:简易电压表设计
[4] 电子森林:数码管显示
[5] 电子森林:OLED驱动说明及Verilog代码实例
[6] CSDN:SSD1306 OLED驱动芯片 详细介绍
[7] OLED显示模块驱动原理及应用
[8] 百度云:SSD1306 英文手册,提取码csdn(由于CSDN存在相同资源但是不免费);
[9] 百度文库:SSD1306 中文手册
[10] CSDN:FPGA Verilog实现二进制转BCD码
[11] Lattice Diamond使用案例
[12] STEP-MXO2-C快速上手指南
[13] STEP-MXO2-C
[14] STEP-MXO2V2.2原理图.pdf
[15] 基于Lattice的XO2-4000HC FPGA的核心模块的综合训练底板原理图及硬件手册.pdf
[16] 小脚丫STEP FPGA Training V2.0硬件手册.pdf
[17] 基于Lattice XO2-4000HC的ADC数字电压表及OLED显示设计

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Lattice Diamond,首先需要创建一个新的工程,选择对应的FPGA器件型号,然后添加适当的约束文件和设计文件。以下是一个基于iCE40UP5K器件的频率和相位差测量的简单示例设计。 约束文件 仅需要约束输入时钟信号和输出数据信号的引脚。 ``` # Clock constraints set_location_assignment PIN_35 -to clk_i set_io clk_i !PIN_35 # Output data constraints set_location_assignment PIN_45 -to data_out set_io data_out !PIN_45 ``` 设计文件 设计包括一个简单的计数器和两个相位差测量器件来测量两个不同频率的输入信号之间的相位差。该设计在每个时钟周期内将输入信号的触发点作为相位参考,并将其与前一个周期的触发点进行比较,从而计算相位差值。 ``` module freq_phase_meter( input wire clk_i, input wire [1:0] data_i, output reg [15:0] data_out ); reg [15:0] count_reg1; reg [15:0] count_reg2; wire trigger1; wire trigger2; wire[31:0] phase_delta1; wire[31:0] phase_delta2; // Divide input clock by 2 and 3 assign trigger1 = (count_reg1 == 32767); assign trigger2 = (count_reg2 == 21845); always @(posedge clk_i) begin if (trigger1) count_reg1 <= 0; else count_reg1 <= count_reg1 + 1; if (trigger2) count_reg2 <= 0; else count_reg2 <= count_reg2 + 1; end // Phase measurement freq_phase_detector #( .WIDTH(32), .PERIOD1(32767), .PERIOD2(21845) ) phase_detector1 ( .clk_i(clk_i), .trigger_i(trigger1), .phase_delta_o(phase_delta1) ); freq_phase_detector #( .WIDTH(32), .PERIOD1(32767), .PERIOD2(14563) ) phase_detector2 ( .clk_i(clk_i), .trigger_i(trigger2), .phase_delta_o(phase_delta2) ); // Output phase difference between input signals always @(posedge clk_i) begin data_out <= phase_delta1[15:0] - phase_delta2[15:0]; end endmodule module freq_phase_detector( input wire clk_i, input wire trigger_i, output wire [$clog2(WIDTH)-1:0] phase_delta_o // $clog2(WIDTH) is the number of bits needed to represent WIDTH ); reg [31:0] phase_reg; always @(posedge clk_i) begin if (trigger_i) phase_reg <= 0; else phase_reg <= phase_reg + 1; end // Output phase difference between last two trigger points assign phase_delta_o = (phase_reg - WIDTH); endmodule ``` 以上代码可以将两个不同频率的输入信号连接到data_i[1:0] 引脚上,输出测量的相位差值将在每个时钟周期下更新到data_out[15:0] 引脚上。 注意:在实际应用,需要针对特定的输入信号频率和初始相位设置PERIOD1、PERIOD2参数,以便进行准确的相位差测量。 最后,使用Lattice Diamond的“Build”选项将设计合成并生成位文件,然后将位文件烧录到FPGA器件上即可进行测试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值