- 项目背景
旨在提高同学们在暑假对于数字电路和FPGA的理解,培养学生掌握核心技术的创新意识,硬禾学堂开展了“暑假一起练”活动。通过免费获得板卡、提升技能的方式,立足于让更多的同学通过一系列有趣的项目挑战提升自己的综合技能。而FPGA综合技能训练板基于STEP-MXO2核心板。STEP-MXO2第二代是小脚丫团队推出的最新一款FPGA开发板,选用了Lattice公司的MXO2系列的4000HC,逻辑资源较第一代产品提升了近4倍。同时,在板卡的背面集成了编程器,通过USB数据线即可完成FPGA的编程和下载,使用硬禾学堂的小脚丫训练板,可以实现多种不同方式的输入输出功能。其平台架构如下图所示。
- 项目需求
利用基于STEP-MXO2核心板的综合技能训练平台,完成一个数字电压表的设计。其应具备如下功能:
- 设计一个模拟电路,可产生0-3.3V的电压,通过旋转电位计可以调整产生的电压。
- 驱动SPI接口串行ADC芯片实现模拟电压信号采集。
- 驱动平台搭载的0.91寸OLED屏幕将经过处理后的电压值实时显示。
- 设计思路
该项目使用的模块包括:板载的12MHz时钟、PLL分频后的24MHz时钟、0.91寸OLED显示屏、模拟电压产生模块、ADC芯片,大体思路如下:
- 通过模块组件例化来实现OLED、ADC与FPGA之间的连接。
- ADC采样所需的24MHz时钟直接采用PLL分频的方式,节省代码编写时间。
- 使用case语句完成SPI总线的时序操作。
项目的模块结构如下图所示:
项目的模块层次设计如下图所示:
- 功能实现和代码展示
1、Voltage.v——顶层模块
顶层模块中包含了所有对外的输入、输出声明以及其他子模块的例化,可以看到模块例化过程中在同名例化方式上增加的模块数,这里可以按照个人习惯例化,作者在此这样例化的目的是方便后续仿真调试过程中能够迅速找到出错的模块和清晰地了解例化了多少模块。
module voltage(
input clk, //系统时钟12MHz
input rst_n, //系统复位,低有效
input adc_dat, //SPI总线SDA
output adc_cs, //SPI总线CS
output adc_clk, //SPI总线SCK
output seg1_sel, //数码管位选
output [7:0] seg1_led, //数码管段选
output seg2_sel, //数码管位选
output [7:0] seg2_led, //数码管段选
output oled_cs, //OLCD液晶屏使能
output oled_rst_n, //OLCD液晶屏复位
output oled_dc, //OLCD数据指令控制
output oled_clk, //OLCD时钟信号
output oled_data //OLCD数据信号
);
//PLL时钟例化
wire clk_24mhz, clk_12mhz, clk_120mhz, clk_6mhz;
ip_pll u1_ip_pll
(
.CLKI (clk), //12MHz系统时钟输入
.CLKOP (clk_24mhz), //24MHz时钟输出
.CLKOS (clk_12mhz), //12MHz时钟输出
.CLKOS2 (clk_120mhz), //120MHz时钟输出
.CLKOS3 (clk_6mhz) //6MHz时钟输出
);
//ADC功能例化
wire adc_done; //ADC采样完成标志
wire [7:0] adc_data; //ADC采样数据
ADC081S101_driver u2_adc
(
.clk (clk_24mhz), //系统时钟
.rst_n (rst_n), //系统复位,低有效
.adc_cs (adc_cs), //SPI总线CS
.adc_clk (adc_clk), //SPI总线SCK
.adc_dat (adc_dat), //SPI总线SDA
.adc_done (adc_done), //ADC采样完成标志
.adc_data (adc_data) //ADC采样数据
);
//二进制转BCD码模块例化
wire [15:0] bin_code = adc_data * 16'd129;
wire [19:0] bcd_code;
bin_to_bcd u3_bin_to_bcd
(
.rst_n (rst_n), //系统复位,低有效
.bin_code (bin_code), //需要进行BCD转码的二进制数据
.bcd_code (bcd_code) //转码后的BCD码型数据输出
);
//个位数码管模块例化
Segment_led u4_Segment_led
(
.seg_dot (1'b1), //seg_dot input
.seg_data (bcd_code[19:16]), //seg_data input
.seg_sel (seg1_sel),
.seg_led (seg1_led) //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
);
//分数位数码管模块例化
Segment_led u5_Segment_led
(
.seg_dot (1'b0), //seg_dot input
.seg_data (bcd_code[15:12]), //seg_data input
.seg_sel (seg2_sel),
.seg_led (seg2_led) //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
);
//OLED模块例化
oled12832 u6_oled12832(
.clk (clk), //12MHz系统时钟
.rst_n (rst_n), //时钟复位信号
.vol (bcd_code), //ADC采样电压数据
.oled_rst_n (oled_rst_n), //OLED复位信号
.oled_cs (oled_cs), //OLED片选使能信号
.oled_dc (oled_dc), //OLED数据或指令控制信号
.oled_clk (oled_clk), //OLED时钟信号
.oled_data (oled_data) //OLED数据信号
);
endmodule
通过顶层模块将各个子模块例化,就完成了各个模块的调用,将各个独立的模块同FPGA连接了起来。
2、ip_PLL.v——锁相环分频
由于整个采样周期用了35个系统时钟,如果我们采用12MHz时钟作为该模块系统时钟,采样率Fs = 12M/35 = 343Ksps,ADC主频Fsclk = 12 MHz /2 = 6MHz。而ADC7868主频及采样率要求如下图所示,按照要求我们当前的主频和采样率不足,所以在使用该模块时,可以使用更高的时钟(比如24MHz)以达到芯片的要求。因此我们可以通过FPGA内部锁相环PLL倍频来获得更高频率的时钟信号。
查阅手册可知板上芯片是LCMXO2-4000HC,内部含有两路PLL,最高倍频可达400MHz。PLL就是锁相环,这里我们用它来进行频率合成。PLL是由鉴相器PD、压控振荡器VCO、低通滤波器LPF三个基本电路组成。仔细阅读手册可知MXO2 4000HC上的PLL时钟输出不止一个,可以同时配置四组CLKOP、CLKOS、CLKOS1、CLKOS2时钟信号,满足多时钟需求,因此我将12MHz的时钟作为锁相环的输入信号,输出信号分别为24MHz、12MHz、120MHz、6MHz,配置四个时钟输出信号以备不时之需。
我们可以通过Diamond自带的IP核配置来配置PLL。
打开Lattice Diamond软件后新建工程,点击Tools——IPexpress即可创建用户所需的各种IP核,这里我们选择PLL配置。如果是新建工程在Diamond里打开IPexpress的话,那么器件选型已经自动匹配无需用户自行填写。需要注意的是IP核的芯片选型必须和板子上的芯片匹配,不然无法下载运行。输出代码选择本次设计所需的Verilog,文件路径设定完成之后我们点击customize,即出现下图所示场景。
其中CLKI的位置输入我们的板子晶振的频率12MHz,CLKOP可以选择24MHz,但是这里输入必须是CLKI的整数倍或者是0.1-0.9倍。否则后面的计算会提示错误。同样我们可以设置CLKOS、CLKOS1,设置频率之前要先使能,勾上Enable。设置好之后就可以下拉滑动条,点击Calculate。同时点击左下角导入工程。最后点击Generate生成。生成成功后我们就可以看见大致如下列代码的一个.v文件。
`timescale 1 ns / 1 ps
module ip_pll (CLKI, CLKOP, CLKOS, CLKOS2, CLKOS3)/* synthesis NGD_DRC_MASK=1 */;
input wire CLKI;
output wire CLKOP;
output wire CLKOS;
output wire CLKOS2;
output wire CLKOS3;
wire LOCK;
wire CLKOS3_t;
wire CLKOS2_t;
wire CLKOS_t;
wire CLKOP_t;
wire scuba_vlo;
VLO scuba_vlo_inst (.Z(scuba_vlo));
defparam PLLInst_0.DDRST_ENA = "DISABLED" ;
defparam PLLInst_0.DCRST_ENA = "DISABLED" ;
defparam PLLInst_0.MRST_ENA = "DISABLED" ;
defparam PLLInst_0.PLLRST_ENA = "DISABLED" ;
defparam PLLInst_0.INTFB_WAKE = "DISABLED" ;
defparam PLLInst_0.STDBY_ENABLE = "DISABLED" ;
defparam PLLInst_0.DPHASE_SOURCE = "DISABLED" ;
defparam PLLInst_0.PLL_USE_WB = "DISABLED" ;
defparam PLLInst_0.CLKOS3_FPHASE = 0 ;
defparam PLLInst_0.CLKOS3_CPHASE = 79 ;
defparam PLLInst_0.CLKOS2_FPHASE = 0 ;
defparam PLLInst_0.CLKOS2_CPHASE = 3 ;
defparam PLLInst_0.CLKOS_FPHASE = 0 ;
defparam PLLInst_0.CLKOS_CPHASE = 39 ;
defparam PLLInst_0.CLKOP_FPHASE = 0 ;
defparam PLLInst_0.CLKOP_CPHASE = 19 ;
defparam PLLInst_0.PLL_LOCK_MODE = 0 ;
defparam PLLInst_0.CLKOS_TRIM_DELAY = 0 ;
defparam PLLInst_0.CLKOS_TRIM_POL = "RISING" ;
defparam PLLInst_0.CLKOP_TRIM_DELAY = 0 ;
defparam PLLInst_0.CLKOP_TRIM_POL = "RISING" ;
defparam PLLInst_0.FRACN_DIV = 0 ;
defparam PLLInst_0.FRACN_ENABLE = "DISABLED" ;
defparam PLLInst_0.OUTDIVIDER_MUXD2 = "DIVD" ;
defparam PLLInst_0.PREDIVIDER_MUXD1 = 0 ;
defparam PLLInst_0.VCO_BYPASS_D0 = "DISABLED" ;
defparam PLLInst_0.CLKOS3_ENABLE = "ENABLED" ;
defparam PLLInst_0.OUTDIVIDER_MUXC2 = "DIVC" ;
defparam PLLInst_0.PREDIVIDER_MUXC1 = 0 ;
defparam PLLInst_0.VCO_BYPASS_C0 = "DISABLED" ;
defparam PLLInst_0.CLKOS2_ENABLE = "ENABLED" ;
defparam PLLInst_0.OUTDIVIDER_MUXB2 = "DIVB" ;
defparam PLLInst_0.PREDIVIDER_MUXB1 = 0 ;
defparam PLLInst_0.VCO_BYPASS_B0 = "DISABLED" ;
defparam PLLInst_0.CLKOS_ENABLE = "ENABLED" ;
defparam PLLInst_0.OUTDIVIDER_MUXA2 = "DIVA" ;
defparam PLLInst_0.PREDIVIDER_MUXA1 = 0 ;
defparam PLLInst_0.VCO_BYPASS_A0 = "DISABLED" ;
defparam PLLInst_0.CLKOP_ENABLE = "ENABLED" ;
defparam PLLInst_0.CLKOS3_DIV = 80 ;
defparam PLLInst_0.CLKOS2_DIV = 4 ;
defparam PLLInst_0.CLKOS_DIV = 40 ;
defparam PLLInst_0.CLKOP_DIV = 20 ;
defparam PLLInst_0.CLKFB_DIV = 2 ;
defparam PLLInst_0.CLKI_DIV = 1 ;
defparam PLLInst_0.FEEDBK_PATH = "CLKOP" ;
EHXPLLJ PLLInst_0 (.CLKI(CLKI), .CLKFB(CLKOP_t), .PHASESEL1(scuba_vlo),
.PHASESEL0(scuba_vlo), .PHASEDIR(scuba_vlo), .PHASESTEP(scuba_vlo),
.LOADREG(scuba_vlo), .STDBY(scuba_vlo), .PLLWAKESYNC(scuba_vlo),
.RST(scuba_vlo), .RESETM(scuba_vlo), .RESETC(scuba_vlo), .RESETD(scuba_vlo),
.ENCLKOP(scuba_vlo), .ENCLKOS(scuba_vlo), .ENCLKOS2(scuba_vlo),
.ENCLKOS3(scuba_vlo), .PLLCLK(scuba_vlo), .PLLRST(scuba_vlo), .PLLSTB(scuba_vlo),
.PLLWE(scuba_vlo), .PLLADDR4(scuba_vlo), .PLLADDR3(scuba_vlo), .PLLADDR2(scuba_vlo),
.PLLADDR1(scuba_vlo), .PLLADDR0(scuba_vlo), .PLLDATI7(scuba_vlo),
.PLLDATI6(scuba_vlo), .PLLDATI5(scuba_vlo), .PLLDATI4(scuba_vlo),
.PLLDATI3(scuba_vlo), .PLLDATI2(scuba_vlo), .PLLDATI1(scuba_vlo),
.PLLDATI0(scuba_vlo), .CLKOP(CLKOP_t), .CLKOS(CLKOS_t), .CLKOS2(CLKOS2_t),
.CLKOS3(CLKOS3_t), .LOCK(LOCK), .INTLOCK(), .REFCLK(), .CLKINTFB(),
.DPHSRC(), .PLLACK(), .PLLDATO7(), .PLLDATO6(), .PLLDATO5(), .PLLDATO4(),
.PLLDATO3(), .PLLDATO2(), .PLLDATO1(), .PLLDATO0())
/* synthesis FREQUENCY_PIN_CLKOS3="6.000000" */
/* synthesis FREQUENCY_PIN_CLKOS2="120.000000" */
/* synthesis FREQUENCY_PIN_CLKOS="12.000000" */
/* synthesis FREQUENCY_PIN_CLKOP="24.000000" */
/* synthesis FREQUENCY_PIN_CLKI="12.000000" */
/* synthesis ICP_CURRENT="7" */
/* synthesis LPF_RESISTOR="8" */;
assign CLKOS3 = CLKOS3_t;
assign CLKOS2 = CLKOS2_t;
assign CLKOS = CLKOS_t;
assign CLKOP = CLKOP_t;
endmodule
至此通过PLL倍频产生的时钟就配置完成了,只需在顶层模块中对其例化就可以。
3、ADC.v——数据采集
本次综合训练板上FPGA直接连接ADC芯片的控制端,ADC有6个管脚,3脚Vin为VCC和Vref功能复用,即Vin = VCC = Vref。ADC前端是一个3Pin排针,用来选择ADC采样信号的来源(类似单刀双掷开关),当我们用跳线帽将1、2脚短路时(默认状态下跳线帽2、3脚短接),ADC芯片就会采集电位计电压。ADC芯片管脚说明如下图所示:
了解ADC芯片外部电路后我们需要对ADC的通信时序电路做一个了解。
SDATA信号在SCLK的节拍下传输数据,当SCLK下降沿时SDATA更新数据输出,当驱动程序编程时我们要在上升沿采样数据可以得到稳定的输出。CS信号拉低有效,经过16个时钟完成一次ADC转换并采样,采样回来的数据前3位无效,接下来为DB7~DB0(有效数据),再接下来为无效数据。
针对ADC芯片的通信时序,我们可以用Verilog设计一个计数器,当计数器值不同时完成不同操作,实现一次ADC采样,程序实现如下:
reg [7:0] cnt; //计数器
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 1'b0;
else if(cnt >= 8'd34)
cnt <= 1'b0;
else
cnt <= cnt + 1'b1;
end
//总线操作
reg [7:0] data;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
adc_cs <= HIGH; adc_clk <= HIGH;
end
else begin
case(cnt)
8'd0 : begin adc_cs <= HIGH; adc_clk <= HIGH; end
8'd1 : begin adc_cs <= LOW; adc_clk <= HIGH; end
8'd2,8'd4,8'd6,8'd8,8'd10,8'd12,8'd14,8'd16,
8'd18,8'd20,8'd22,8'd24,8'd26,8'd28,8'd30,8'd32:
begin adc_cs <= LOW; adc_clk <= LOW; end
8'd3 : begin adc_cs <= LOW; adc_clk <= HIGH; end //0
8'd5 : begin adc_cs <= LOW; adc_clk <= HIGH; end //1
8'd7 : begin adc_cs <= LOW; adc_clk <= HIGH; end //2
8'd9 : begin adc_cs <= LOW; adc_clk <= HIGH; data[7] <= adc_dat; end //3
8'd11 : begin adc_cs <= LOW; adc_clk <= HIGH; data[6] <= adc_dat; end //4
8'd13 : begin adc_cs <= LOW; adc_clk <= HIGH; data[5] <= adc_dat; end //5
8'd15 : begin adc_cs <= LOW; adc_clk <= HIGH; data[4] <= adc_dat; end //6
8'd17 : begin adc_cs <= LOW; adc_clk <= HIGH; data[3] <= adc_dat; end //7
8'd19 : begin adc_cs <= LOW; adc_clk <= HIGH; data[2] <= adc_dat; end //8
8'd21 : begin adc_cs <= LOW; adc_clk <= HIGH; data[1] <= adc_dat; end //9
8'd23 : begin adc_cs <= LOW; adc_clk <= HIGH; data[0] <= adc_dat; end //10
8'd25 : begin adc_cs <= LOW; adc_clk <= HIGH; adc_data <= data; end //11
8'd27 : begin adc_cs <= LOW; adc_clk <= HIGH; adc_done <= HIGH; end //12
8'd29 : begin adc_cs <= LOW; adc_clk <= HIGH; adc_done <= LOW; end //13
8'd31 : begin adc_cs <= LOW; adc_clk <= HIGH; end //14
8'd33 : begin adc_cs <= LOW; adc_clk <= HIGH; end //15
8'd34 : begin adc_cs <= HIGH; adc_clk <= HIGH; end
default : begin adc_cs <= HIGH; adc_clk <= HIGH; end
endcase
end
end
对于ADC模块的接口如下所示,至此我们完成了ADC模块的设计。
module ADC
(
input clk, //系统时钟
input rst_n, //系统复位,低有效
input adc_dat, //SPI总线SDA
output reg adc_cs, //SPI总线CS
output reg adc_clk, //SPI总线SCK
output reg adc_done, //ADC采样完成标志
output reg [7:0] adc_data //ADC采样数据
);
4、BIN_to_BCD.v——数据转码
通过ADC芯片进行数据采集后,我们就得到了原始数据,但是这个数据是以Vref=3.3V为参考电压的,该ADC芯片为8位的ADC芯片,理论上我们得到的采样数据adc_data应该为8’hFF,而我们所设计的数字电压表最终显示在OLED显示屏上的数据应该为3.3V,如何将8’hFF转换成3.3就是BIN_to_BCD模块所需要执行的任务了。
我们由模数转换的量化运算中知道 N = 256 * Vin / Vref,那么逆向运算为Vin = N * Vref / 256,其中Vref = 3.3V,所以Vin = N * 0.0129。所以我们需要用FPGA计算adc_data * 0.0129的结果,然后为了使用十进制的显示,先将结果进行BCD转码,然后显示在OLED显示屏上。所以将ADC采样数据按规则转换为电压数据(乘以0.0129),这里我们可以将数据乘以129,再将得到的数据经过BCD转码后,小数点左移4位即可,程序实现如下:
wire [15:0] bin_code = adc_data * 16'd129;
二进制转BCD码程序实现如下图所示,最后BCD_code得到20位的数据输出,每4位表示一个BCD码,所以有5位有效数据,这里我们还需要将小数点左移4位才能获得最后结果。由于数据精确到小数点后4位,用户可以根据自己的需求选择展示到小数点后几位。
reg [35:0] shift_reg;
always@(bin_code or rst_n)begin
shift_reg = {20'h0,bin_code};
if(!rst_n) bcd_code = 0;
else begin
repeat(16) begin //循环16次
//BCD码各位数据作满5加3操作,
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
5、OLED12832.v——数据显示
在前面几个步骤以后,数据已经经过一系列处理并获得了3.3V的数据,接下来只需把数据在OLED显示屏上显示出来即可。从使用的角度上,我们驱动该OLED屏幕显示,实际是与驱动芯片SSD1306通信,让SSD1306控制OLED点阵显示。SSD1306相当于一个中介,我们只需要了解SSD1306的功能、寄存器、总线、驱动流程等参数或工作方式,根据SSD1306的工作方式通信即可。本次OLED显示设计作者参考了硬禾学堂的代码示例,在其基础上进行了数据的修改,大家根据自己的需求进行修改即可。对于OLED的显示其主要需要掌握SPI总线下FPGA芯片与SSD1306的通信和内存写入等难点。本次部分代码如下:
//main code
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
cnt <= 1'b0; cnt_init <= 1'b0;
cnt_main <= 1'b0; cnt_scan <= 1'b0;
cnt_write <= 1'b0; cnt_delay <= 1'b0;
num <= 1'b0; num_delay <= 16'd5;
char <= 1'b0; char_reg <= 1'b0;
x_ph <= 1'b0; x_pl <= 1'b0;
y_p <= 1'b0; oled_cs <= HIGH;
oled_clk <= HIGH; oled_rst_n <= HIGH;
oled_dc <= CMD; oled_data <= LOW;
state <= IDLE; state_back <= IDLE;
end
else
begin
case(state)
IDLE: begin
cnt <= 1'b0; cnt_init <= 1'b0;
cnt_main <= 1'b0; cnt_scan <= 1'b0;
cnt_write <= 1'b0; cnt_delay <= 1'b0;
num <= 1'b0; num_delay <= 16'd5;
char <= 1'b0; char_reg <= 1'b0;
x_ph <= 1'b0; x_pl <= 1'b0;
y_p <= 1'b0; oled_cs <= HIGH;
oled_clk <= HIGH; oled_rst_n <= HIGH;
oled_dc <= CMD; oled_data <= LOW;
state <= MAIN; state_back <= MAIN;
end
MAIN: begin //MAIN状态机
if(cnt_main >= 5'd8) cnt_main <=5'd5;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main)
5'd0: begin state <= INIT; end
5'd1: begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " STEP FPGA "; state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Voltage: "; state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " "; state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " Jinzj "; state <= SCAN; end
5'd5: begin y_p <= 8'hb2; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd 1; char <= vol[19:16]; state <= SCAN; end
5'd6: begin y_p <= 8'hb2; x_ph <= 8'h12; x_pl <= 8'h10; num <= 5'd 1; char <= 8'd46; state <= SCAN; end
5'd7: begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd 1; char <= vol[15:12]; state <= SCAN; end
5'd8: begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h10; num <= 5'd 1; char <= 8'd86; state <= SCAN; end
default: state <= IDLE;
endcase
end
INIT: begin //初始化状态
case(cnt_init)
5'd0: begin
oled_rst_n <= LOW;
cnt_init <= cnt_init + 1'b1;
end //复位有效
5'd1: begin
num_delay <= 16'd25000;
state <= DELAY;
state_back <= INIT;
cnt_init <= cnt_init + 1'b1;
end //延时大于3us
5'd2: begin
oled_rst_n <= HIGH;
cnt_init <= cnt_init + 1'b1;
end //复位恢复
5'd3: begin
num_delay <= 16'd25000;
state <= DELAY;
state_back <= INIT;
cnt_init <= cnt_init + 1'b1;
end //延时大于220us
5'd4: begin
if(cnt >= INIT_DEPTH) begin //当25条指令及数据发出后,配置完成
cnt <= 1'b0;
cnt_init <= cnt_init + 1'b1;
end
else begin
cnt <= cnt + 1'b1; num_delay <= 16'd5;
oled_dc <= CMD;
char_reg <= cmd[cnt];
state <= WRITE;
state_back <= INIT;
end
end
5'd5: begin
cnt_init <= 1'b0;
state <= MAIN;
end //初始化完成,返回MAIN状态
default: state <= IDLE;
endcase
end
SCAN: begin //屏幕显示状态
if(cnt_scan == 5'd11) begin
if(num)
cnt_scan <= 5'd3;
else
cnt_scan <= cnt_scan + 1'b1;
end
else if(cnt_scan == 5'd12)
cnt_scan <= 1'b0;
else
cnt_scan <= cnt_scan + 1'b1;
case(cnt_scan)
5'd 0: begin
oled_dc <= CMD;
char_reg <= y_p;
state <= WRITE;
state_back <= SCAN;
end //定位列页地址
5'd 1: begin
oled_dc <= CMD;
char_reg <= x_pl;
state <= WRITE;
state_back <= SCAN;
end //定位行地址低位
5'd 2: begin
oled_dc <= CMD;
char_reg <= x_ph;
state <= WRITE;
state_back <= SCAN;
end //定位行地址高位
5'd 3: begin
num <= num - 1'b1;
end
5'd 4: begin
oled_dc <= DATA;
char_reg <= 8'h00;
state <= WRITE;
state_back <= SCAN;
end //将5*8点阵编程8*8
5'd 5: begin
oled_dc <= DATA;
char_reg <= 8'h00;
state <= WRITE;
state_back <= SCAN;
end //将5*8点阵编程8*8
5'd 6: begin
oled_dc <= DATA;
char_reg <= 8'h00;
state <= WRITE;
state_back <= SCAN;
end //将5*8点阵编程8*8
5'd 7: begin
oled_dc <= DATA;
char_reg <= mem[char[(num*8)+:8]][39:32];
state <= WRITE;
state_back <= SCAN;
end
5'd 8: begin
oled_dc <= DATA;
char_reg <= mem[char[(num*8)+:8]][31:24];
state <= WRITE;
state_back <= SCAN;
end
5'd 9: begin
oled_dc <= DATA;
char_reg <= mem[char[(num*8)+:8]][23:16];
state <= WRITE;
state_back <= SCAN;
end
5'd10: begin
oled_dc <= DATA;
char_reg <= mem[char[(num*8)+:8]][15: 8];
state <= WRITE;
state_back <= SCAN;
end
5'd11: begin
oled_dc <= DATA;
char_reg <= mem[char[(num*8)+:8]][ 7: 0];
state <= WRITE;
state_back <= SCAN;
end
5'd12: begin
state <= MAIN;
end
default: state <= IDLE;
endcase
end
WRITE: begin //写状态
if(cnt_write >= 5'd17)
cnt_write <= 1'b0;
else
cnt_write <= cnt_write + 1'b1;
case(cnt_write)
5'd 0: begin oled_cs <= LOW; end //9位数据最高位为命令数据控制位
5'd 1: begin oled_clk <= LOW; oled_data <= char_reg[7]; end //先发高位数据
5'd 2: begin oled_clk <= HIGH; end
5'd 3: begin oled_clk <= LOW; oled_data <= char_reg[6]; end
5'd 4: begin oled_clk <= HIGH; end
5'd 5: begin oled_clk <= LOW; oled_data <= char_reg[5]; end
5'd 6: begin oled_clk <= HIGH; end
5'd 7: begin oled_clk <= LOW; oled_data <= char_reg[4]; end
5'd 8: begin oled_clk <= HIGH; end
5'd 9: begin oled_clk <= LOW; oled_data <= char_reg[3]; end
5'd10: begin oled_clk <= HIGH; end
5'd11: begin oled_clk <= LOW; oled_data <= char_reg[2]; end
5'd12: begin oled_clk <= HIGH; end
5'd13: begin oled_clk <= LOW; oled_data <= char_reg[1]; end
5'd14: begin oled_clk <= HIGH; end
5'd15: begin oled_clk <= LOW; oled_data <= char_reg[0]; end //后发低位数据
5'd16: begin oled_clk <= HIGH; end
5'd17: begin oled_cs <= HIGH; state <= DELAY; end
default: state <= IDLE;
endcase
end
DELAY: begin //延时状态
if(cnt_delay >= num_delay) begin
cnt_delay <= 16'd0;
state <= state_back;
end
else
cnt_delay <= cnt_delay + 1'b1;
end
default: state <= IDLE;
endcase
end
end
//OLED配置指令数据,SSD1306初始化流程
always@(posedge rst_n)
begin
cmd[ 0] = {8'hae}; //0xAE 关闭显示
cmd[ 1] = {8'h00}; //0x00-0x0F 页地址模式下设置列地址起始低四位
cmd[ 2] = {8'h10}; //0x10-0x1F 页地址模式下设置列地址起始高四位
cmd[ 3] = {8'h00};
cmd[ 4] = {8'hb0}; //0xB0-0xB7 设置页地址
cmd[ 5] = {8'h81}; //0x81 设置对比度(双字节指令 第一个字节指令)
cmd[ 6] = {8'hff}; //0xFF 设置对比度亮度最大(双字节指令 第二个字节指令)
cmd[ 7] = {8'ha1}; //0xA1 设置段重映射,左右方向正常显示
cmd[ 8] = {8'ha6}; //0xA6 设置正常显示模式,此模式下数据1表示点亮
cmd[ 9] = {8'ha8}; //0xA8 设置复用率(双字节指令 第一个字节指令)
cmd[10] = {8'h1f}; //0x1F 设置复用率为63(双字节指令 第二个字节指令)
cmd[11] = {8'hc8}; //0xC8 设置重映射模式,上下方向正常显示
cmd[12] = {8'hd3}; //0xD3 设置显示偏移位置(双字节指令 第一个字节指令)
cmd[13] = {8'h00}; //0x00 设置偏移量为0(双字节指令 第二个字节指令)
cmd[14] = {8'hd5}; //0xD5 设置显示时钟分比率(双字节指令 第一个字节指令)
cmd[15] = {8'h80}; //0x80 高4位晶振频率,低4位分频因子(双字节指令 第二个字节指令)
cmd[16] = {8'hd9}; //0xD9 设置预充电周期
cmd[17] = {8'h1f}; //Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
cmd[18] = {8'hda}; //0xDA 设置列引脚硬件配置
cmd[19] = {8'h00}; //
cmd[20] = {8'hdb}; //0xDB 设置VCOMH反压值,调整VCOMH调节器的输出
cmd[21] = {8'h40}; //0x40 设置屏幕(GDDRAM)起始行 -- Set Display Start Line (0x40~0x7F)
cmd[22] = {8'h8d}; //0x8D 设置电荷泵
cmd[23] = {8'h14}; //0x14 开启电荷泵
cmd[24] = {8'haf}; //0xAF 开启显示
end
参考文档也通过链接的形式贴在此处。
- 项目资源情况
项目RTL图:
项目管脚分配情况:
项目资源使用报告:
- 项目成果展示
由于电源最高为3.3V,所以随着电位器的旋转,示数随之变化最低显示为0V,最高显示为3.2V,由此我们可以看出该数字电压表的精确度也得到一定的保证。
- 遇到的困难及解决办法
1、困难:ADC模块的SPI总线的代码编写。
解决办法:由STM32转战FPGA后,面临的问题就是原本的函数体在FPGA中不能实现,串行结构也变成了并行结构。我选择使用case选择语句顺序执行的特点编写SPI总线的时序操作。
2、困难:板子执行逻辑顺序混乱,各部分执行先后顺序、因果关系错误。
解决办法:在各个模块中设置了状态变量,从状态机的角度去解决问题,并且配套使用许多if-else if-else语句用来做条件选择。
- 总结与感悟
本次项目还是比较简单的,所需要的模块也不是多。数字电压表其实可以作为一个示波器项目的一个小部分,在后续开发中我将通过数字电压表逐步完善基于FPGA的数字示波器的开发。
在此之前本科阶段一直是做的单片机和STM32开发,并没有涉足过FPGA的开发,通过暑假这个时间短期突击了一下FPGA的知识,基于自己的理解开发了这个项目。项目过程中发生了许多问题,好在通过多方资源能够解决。在后期的学习过程中仍要加深对时序的理解,提高自己的FPGA技术。
最后,非常感谢硬禾学堂带给我这次机会,让我对数字逻辑电路、FPGA有了更深入的认识,对Verilog语言的使用也有了不少的提升,同时也给我了一个更加充实的假期。