项目描述及要求:
基于包含Lattice版本的小脚丫FPGA综合训练板完成以下基本功能:
旋转电位计可以产生0-3.3V的电压利用板上的串行ADC对电压进行转换将电压值在板上的LED、OLED屏幕上显示出来。
总体设计思路介绍:
由小脚丫FPGA的综合技能训练板上带有的一颗SPI接口的串行ADC,采集电位计上的电压数据,然后通过数据转换模块,将采集到的二进制电压数据转换成可显示的BCD码,最终将其显示在LED以及OLED屏幕上。设计思路图如下:
综合电路图:
模块实现:
ADC驱动模块:
ADC,即模数转换器,通常是指一个将模拟信号转变为数字信号的电子元件。通常的模数转换器是将一个输入电压信号转换为一个输出的数字信号。数字信号本身不具有实际意义,仅表示一个相对大小,故需要一个参考模拟量作为转换的标准。本模块将最大的可转换电压3.3v,数字量输出dac_data则表示输入信号相对于参考信号的大小。如输入信号为3.3v,则dac_data为8'hff。本模块代码基本参考电子森林简易电压表设计的例程。该模块系统时钟clk选择12Mhz时钟,rstn为系统复位。依据ADS7868时序,用Verilog设计一个计数器,当计数器值不同时完成不同操作,实现一次ADC采样。整个采样周期用了35个系统时钟,采样率Fs = 12M/35 = 343Ksps,ADC主频Fsclk = 12 MHz /2 = 6MHz,其中adc_cs,adc_clk和adc_dat为ADC控制管脚,adcdata为ADC采样数据,adc_done产生一个脉冲对应adc_data得到一个有效数据。
reg [7:0] cnt; //计数器
always @(posedge clk or negedge rst_n)
if(!rst_n) cnt <= 1'b0;
else if(cnt >= 8'd34) cnt <= 1'b0;
else cnt <= cnt + 1'b1;
reg [7:0] data;
always @(posedge clk or negedge rst_n)
if(!rst_n) begin
adc_cs <= 1'b1; adc_clk <= 1'b1;
end else case(cnt)
8'd0 : begin adc_cs <= 1'b1; adc_clk <= 1'b1; end
8'd1 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; 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 <= 1'b0; adc_clk <= 1'b0; end
8'd3 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; end //0
8'd5 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; end //1
8'd7 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; end //2
8'd9 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; data[7] <= adc_dat; end //3
8'd11 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; data[6] <= adc_dat; end //4
8'd13 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; data[5] <= adc_dat; end //5
8'd15 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; data[4] <= adc_dat; end //6
8'd17 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; data[3] <= adc_dat; end //7
8'd19 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; data[2] <= adc_dat; end //8
8'd21 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; data[1] <= adc_dat; end //9
8'd23 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; data[0] <= adc_dat; end //10
8'd25 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; adc_data <= data; end //11
8'd27 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; adc_done <= 1'b1; end //12
8'd29 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; adc_done <= 1'b0; end //13
8'd31 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; end //14
8'd33 : begin adc_cs <= 1'b0; adc_clk <= 1'b1; end //15
8'd34 : begin adc_cs <= 1'b1; adc_clk <= 1'b1; end
default :
begin adc_cs <= 1'b1; adc_clk <= 1'b1; end
endcase
转换BCD码模块:
若ADC模拟输入电压为3.3V,理论上得到的采样数据adc_data应该为8’hff,电压表最终应在数码管和oled屏幕上显示3.3。这一模块的功能就是将上一个模块的输出转换成可以显示的数据。量化运算 N = 256 * Vin / Vref,逆向运算为Vin = N * Vref / 256,其中Vref = 3.3V,所以Vin = N * 0.0129。实际应用中,在这一模块里先用FPGA计算adc_data * 129的结果,然后为了使用十进制的显示,将结果进行BCD转码。BCD转换主要采用电子森林的例程:运用左移加三的算法,左移要转换的二进制码1位,左移之后,BCD码分别置于百位、十位、个位。如果移位后所在的BCD码列大于或等于5,则对该值加3,继续左移的过程直至全部移位完成。由于bcd_code位宽为16,所以循环操作16次。具体代码如下:
wire [7:0] adc_data;
wire [15:0] bin_code = adc_data * 16'd129;
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
LED显示模块:
本部分在这一项目中并没有要求,但是由于自己原本一开始写完代码,验证功能自己失败了,为了排查问题是出在ADC模块,还是OLED显示模块,所以加入了较为简单的led显示功能,最终排查出是自己oled模块的驱动问题。此部分直接套用电子森林的代码,代码主要部分如下:
always@(seg_data)
case(seg_data)
4'h0: seg_led = {seg_dot,7'h3f}; // 0
4'h1: seg_led = {seg_dot,7'h06}; // 1
4'h2: seg_led = {seg_dot,7'h5b}; // 2
4'h3: seg_led = {seg_dot,7'h4f}; // 3
4'h4: seg_led = {seg_dot,7'h66}; // 4
4'h5: seg_led = {seg_dot,7'h6d}; // 5
4'h6: seg_led = {seg_dot,7'h7d}; // 6
4'h7: seg_led = {seg_dot,7'h07}; // 7
4'h8: seg_led = {seg_dot,7'h7f}; // 8
4'h9: seg_led = {seg_dot,7'h6f}; // 9
4'ha: seg_led = {seg_dot,7'h77}; // A
4'hb: seg_led = {seg_dot,7'h7C}; // b
4'hc: seg_led = {seg_dot,7'h39}; // C
4'hd: seg_led = {seg_dot,7'h5e}; // d
4'he: seg_led = {seg_dot,7'h79}; // E
4'hf: seg_led = {seg_dot,7'h71}; // F
default: seg_led = {seg_dot,7'h00};
endcase
assign seg_sel = 1'b0;
OLED显示模块:
由于没有注意cnt_main的状态,对循环的判断有误,所以导致OLED屏幕会被空白频繁覆盖。OLED显示四行,第二行显示不断刷新的数据,所以cnt_main大于4时要为其赋值2。本模块主要参考于电子森林例程,仅修改了状态MAIN使得OLED显示自己想要显示的内容。本模块输入为bin_code,用以接受上一个模块输出的bcd码电压数据,同时将其前三位及小数点赋值给自定义的信号adc_dout,最终将其在OLED屏幕上显示出来。仅展示修改部分的代码:
wire [19:0] bcd_code;
wire [31:0] adc_dout;
assign adc_dout[31:24] ={4'b0,bcd_code[19:16]}; //电压数据的个位
assign adc_dout[23:16] =8'd46; //小数点
assign adc_dout[15:8] ={4'b0,bcd_code[15:12]}; //电压数据的第一位小数
assign adc_dout[7:0] ={4'b0,bcd_code[11:8]}; //电压数据的第二位小数
........................................................................................
MAIN:begin
if(cnt_main >= 5'd4) cnt_main <= 5'd2;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main) //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 <= " VOLTMETER ";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= {" voltage",": ",adc_dout," V "};state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " by yecong ";state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " YingHeXueTang ";state <= SCAN; end
default: state <= IDLE;
endcase
end
顶层模块:
通过这次verilog语言的编程训练,我深刻体会到模块化编程的方便,一个模块能在另一个模块中被引用,建立起描述的层次。在顶层模块里,通过实例化各个模块,实现了各自相对独立的功能。顶层模块的输入,是所有底层模块的输入的总和。顶层模块的输出,是所有底层模块的总和。具体代码如下:
input clk; //系统时钟
input rst_n; //系统复位,低有效
input adc_dat; //SPI总线SDA
output adc_cs; //SPI总线CS
output adc_clk; //SPI总线SCK
output oled_csn; //OLCD液晶屏使能
output oled_rst; //OLCD液晶屏复位
output oled_dcn; //OLCD数据指令控制
output oled_clk; //OLCD时钟信号
output oled_dat; //OLCD数据信号
output seg1_sel; //数码管位选
output [7:0] seg1_led; //数码管段选
output seg2_sel; //数码管位选
output [7:0] seg2_led; //数码管段选
wire [19:0] bcd_code;
wire adc_done;
wire [7:0] adc_data;
ADC_DRIVER ADC_DRIVER_v(
.clk (clk ),
.rst_n (rst_n ),
.adc_cs (adc_cs ),
.adc_clk (adc_clk ),
.adc_dat (adc_dat ),
.adc_done (adc_done ),
.adc_data (adc_data )
);
VOLTMETER VOLTMETER_v(
.rst_n (rst_n ),
.bcd_code (bcd_code ),
.adc_data (adc_data )
);
Seg_led seg[1:0] (
.seg_data (bcd_code[19:12]), //seg_data input
.seg_dot ({1'b1,1'b0} ), //segment dot control
.seg_sel ({seg1_sel,seg2_sel}), //segment com port
.seg_led ({seg1_led,seg2_led}) //MSB~LSB = DP,G,F,E,D,C,B,A
);
OLED12832 OLED12832_v(
.clk (clk ),
.rst_n (rst_n ),
.oled_csn (oled_csn ),
.oled_rst (oled_rst ),
.oled_dcn (oled_dcn ),
.oled_clk (oled_clk ),
.oled_dat (oled_dat ),
.bcd_code (bcd_code )
);
结语:
本次项目中硬禾学堂已大大降低了项目难度,但由于我是初次接触FPGA的实际设计与功能仿真,仅仅学过一些Verilog的基本语法,所以在这个项目中我不可避免遇到了一些问题。比如ADC驱动的输入输出分配,自定义信号的定义,OLED显示模块main状态的选取等等,所幸电子森林提供了很多与项目密切相关的例程,也有一起报名项目的优秀的大佬们在我遇到问题时为我答疑解惑,在此我表示由衷的感谢!在完成项目的过程中,我学会了如何去写好可综合的Verilog代码、协调各个模块之间的输入输出,体会了不断试错、不断debug、不断完善的过程。在做项目的过程中,虽然遇到的问题可能越来越多,越来越棘手,但同时我的信心也不断在增强,对成功的渴望也越来越大。在未来我将继续积累FPGA综合设计的经验,增强自己的代码编写能力,多与同学们交流技术问题。