一、实验项目任务
2021年暑假硬禾学堂举办的“暑假一起练”活动包含五个项目,其中我完成了基于FPGA设计制作数字电压表,项目内容包括:
- 旋转电位计可以产生0-3.3V的电压
- 利用板上的串行ADC对电压进行转换
- 将电压值在板上的OLED屏幕上显示出来
二、实验设计思路
通过FPGA编程驱动串行ADC芯片,得到数字量化的电压信息,将量化的数字信息转换成BCD码形式,同时驱动独立数码管将电压值显示出来,并在OLED屏幕上进行显示。
由于这是我首次接触FPGA数字电路编程,我在硬禾学堂上查找了基于STEP-MAX10M08核心板 和 STEP BaseBoard V3.0底板完成的简易电压表的设计案例,还找到了基于小脚丫FPGA综合技能训练平台用SSD1306驱动的128*32分辨率的OLED图形化显示案例。
通过实际案例学习,以及自学verilog相关编程语言,我得出设计数字电压表的初步设计流程,其中采集信号模数转换,转BCD码,数码管显示,加上OLED显示模块流程框图如下:
设计部分中ADS7868为驱动SPI接口ADC芯片实现模拟信号采集,bin to bcd为将二进制数据转换为BCD码,segment led是通过驱动独立式数码管将电压数据显示出来。
三、各设计模块具体原理及代码实现
1.ADC模块
模数转换器即A/D转换器,或简称ADC,通常是指一个将模拟信号转变为数字信号的电子元件。通常的模数转换器是将一个输入电压信号转换为一个输出的数字信号。由于数字信号本身不具有实际意义,仅仅表示一个相对大小。故任何一个模数转换器都需要一个参考模拟量作为转换的标准,比较常见的参考标准为最大的可转换信号大小。而输出的数字量则表示输入信号相对于参考信号的大小。
由于我们这次所用8位串行ADC(ADS7868)与案例中的ADC081S101不同,但所用接口及程序类似,可仿照案例进行编程,结果一致。
代码如下:
module ADS7868
(
input clk, //系统时钟
input rst_n, //系统复位,低有效
output reg adc_cs, //SPI总线CS
output reg adc_clk, //SPI总线SCK
input adc_dat, //SPI总线SDA
output reg adc_done, //ADC采样完成标志
output reg [7:0] adc_data //ADC采样数据
);
localparam HIGH = 1'b1;
localparam LOW = 1'b0;
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 <= HIGH; adc_clk <= HIGH;
data <= 1'b0; adc_data <= 1'b0; adc_done <= LOW;
end else 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
endmodule
2.二进制数转换BCD码模块
在ADC模块得到的采样数据后,我们知道量化运算 N = 256 * Vin / Vref,那么逆向运算为Vin = N * Vref / 256,其中Vref = 3.3V,所以Vin = N * 0.0129,所以我们需要用FPGA计算adc_data * 0.0129的结果,然后为了使用十进制的显示,先将结果进行BCD转码,然后显示在数码管上。
将ADC采样数据按规则转换为电压数据(乘以0.0130),这里我们直接乘以129,得到的数据经过BCD转码后小数点左移4位即可,程序实现如下:
wire [15:0] bin_code = adc_data * 16'd130;
将二进制数转换成BCD码的形式,采用左移加三的算法(电子森林中案例使用): 1、左移要转换的二进制码1位 2、左移之后,BCD码分别置于百位、十位、个位 3、如果移位后所在的BCD码列大于或等于5,则对该值加3 4、继续左移的过程直至全部移位完成
二进制转BCD码代码如下:
module bin_to_bcd
(
input rst_n, //系统复位,低有效
input [15:0] bin_code, //需要进行BCD转码的二进制数据
output reg [19:0] bcd_code //转码后的BCD码型数据输出
);
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
endmodule
3.数码管显示和OLED显示模块
最后得到20位的数据输出,每4位表示一个BCD码,共有5位有效数据,同时还要将小数点左移4位。由于在核心板上只有两个数码管,所以保留一位小数后,代码如下:
module Seg_led
(
input [3:0] seg_data, //seg_data input
input seg_dot, //segment dot control
output seg_sel, //segment com port
output reg [7:0] seg_led //MSB~LSB = DP,G,F,E,D,C,B,A
);
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; //共阴极,使能
endmodule
对于OLED显示模块,通过分析电子森林上的OLED显示模块案例,知道了OLED屏幕在功能上分为驱动芯片电路和OLED点阵硬件,根据案例内容对OLED模块四行16位数据进行编程,OLED屏幕四行显示内容代码如下:
begin
if(cnt_main >= 5'd8) cnt_main <= 5'd5;
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 <= "-------zzl------";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Volt meter: ";state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " . V ";state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "-----finish-----";state <= SCAN; end
5'd5: begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd 1; char <=data[15:12]; state <= SCAN; end
5'd6: begin y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd 1; char <=data[11:8]; state <= SCAN; end
5'd7: begin y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h10; num <= 5'd 1; char <=data[7:4]; state <= SCAN; end
5'd8: begin y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <=data[3:0]; state <= SCAN; end
default: state <= IDLE;
endcase
end
4.TOP模块
该模块负责整合上述所有模块,实现功能的统一执行,代码如下:
module voltmeter
(
input clk, //系统时钟
input rst_n, //系统复位,低有效
output adc_cs, //SPI总线CS
output adc_clk, //SPI总线SCK
input adc_dat, //SPI总线SDA
output seg1_sel, //数码管位选
output [7:0] seg1_led, //数码管段选
output seg2_sel, //数码管位选
output [7:0] seg2_led, //数码管段选
output oled_csn, //OLCD液晶屏使能
output oled_rst, //OLCD液晶屏复位
output oled_dcn, //OLCD数据指令控制
output oled_clk, //OLCD时钟信号
output oled_dat //OLCD数据信号
);
wire adc_done;
wire [7:0] adc_data;
//ADC功能,例化
ADS7868 u2
(
.clk (clk ), //系统时钟
.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采样数据
);
//将ADC采样数据按规则转换为电压数据(乘以0.0130),这里我们直接乘以129,得到的数据经过BCD转码后小数点左移4位即可
wire [15:0] bin_code = adc_data * 16'd130;
wire [19:0] bcd_code;
//将处理后的ADC数据进行BCD转码,例化
bin_to_bcd u3
(
.rst_n (rst_n ), //系统复位,低有效
.bin_code (bin_code ), //需要进行BCD转码的二进制数据
.bcd_code (bcd_code ) //转码后的BCD码型数据输出
);
//Segment led display module
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 u4
(
.clk (clk ), //12MHz系统时钟
.rst_n (rst_n ), //系统复位,低有效
.data (bcd_code[19:4]),
.oled_clk (oled_clk ),
.oled_csn (oled_csn ),
.oled_dat (oled_dat ),
.oled_dcn (oled_dcn ),
.oled_rst (oled_rst )
);
endmodule
四、实验心得体会
通过本次基于FPGA设计数字电压表项目实验,这也是我第一次接触FPGA数字电路编程,在这一过程中,通过平台上相应案例的学习加上自己自学Verilog编程语言,完成了实验项目要求。在实验过程中,让我感受到时序逻辑数字电路以及模块设计的魅力,同时又让我回顾了当时数字电路课上所学的理论知识,有效地实现了知行合一。最后十分感谢电子森林给予我的这一宝贵经历,让我受益匪浅。