一、项目介绍:
2021年硬禾学堂举办的“暑假一起练”活动包含五个项目,本项目是基于小脚丫FPGA综合技能训练平台类别中的项目一:利用ADC制作一个数字电压表。具体功能如下:
- 旋转电位计可以产生0-3.3V的电压
- 利用板上的串行ADC对电压进行转换
- 将电压值在板上的OLED屏幕上显示出来
二、设计思路:
由于是第一次接触FPGA,所以在决定参加这次的暑假一起练活动的时候便向周围参与过此类活动的同学详细请教了相关的基础操作知识。然后在老师的指导下也知晓了在电子森林和硬禾学堂的网站上有着很多的参考资料,于是就找到了基于STEP-MAX10M08核心板 和 STEP BaseBoard V3.0底板完成的简易电压表的设计案例,还找到了基于小脚丫FPGA综合技能训练平台用SSD1306驱动的128*32分辨率的OLED屏幕的案例。
通过研究这些案例和代码,以及自学了一些verilog知识,初步确定了自己完成项目一的初步思路:由控制ADC转换电位计电压的ADC转换模块、二进制转换成可显示在OLED屏幕上供人查看的bin_to_bcd模块、OLED屏显模块以及顶层模块,但在实现的过程中遇到了很多困难,于是在将电压值显示在OLED屏幕上前,先利用数码管显示数据以验证ADC转换模块的结果。
具体模块见下图:
三、具体原理及代码:
模数转换器即A/D转换器,或简称ADC,通常是指一个将模拟信号转变为数字信号的电子元件。通常的模数转换器是将一个输入电压信号转换为一个输出的数字信号。由于数字信号本身不具有实际意义,仅仅表示一个相对大小。故任何一个模数转换器都需要一个参考模拟量作为转换的标准,比较常见的参考标准为最大的可转换信号大小。而输出的数字量则表示输入信号相对于参考信号的大小。
为了驱动ADC进行电位计数据的采样,我们需要写相应的驱动程序,而这些我们都可以在电子森林的网站上检索到,而且此次配套的的训练底板的数据手册都能直接查阅或下载后查看。由于案例中使用的是ADC081S101芯片,而此次所用的Training V2.1训练底板上的ADC电路包含可调电位计(3386P-1-103T)和8位串⾏ADC(ADS7868),ADC采集可调电位计的模拟输出,通过三线SPI总线被FPGA核⼼板读取。电路连接如下:
虽然两者是不一样的芯片,但是端口类似、且均是串行ADC,所以完全可以借鉴案例中使用的ADC驱动模块的代码。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
到这我们就完成了串行ADC芯片ADC081S101的驱动设计,整个采样周期用了35个系统时钟,如果我们采用12MHz时钟作为该模块系统时钟,采样率Fs = 12M/35 = 343Ksps,ADC主频Fsclk = 12 MHz /2 = 6MHz。
项目一要求设计0-3.3V量程的数字电压表,理论上我们得到的采样数据adc_data应该为8’hff,而电压表最终显示在数码管上的数据应该为3.3,这里我们利用ADC量化数据的逆向运算将8’hff转换成可以显示的3.3数据。量化运算 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;
wire [19:0] bcd_code;
但是adc_data最大为8’hff,即N最大为255,所以最大显示电压值是3.28V。所以实际过程中应该Vin = N * Vref / 255,即Vin = N * 0.01295,但碍于水平有限无法,选择了电子森林提供的左移加三的算法: 1、左移要转换的二进制码1位 2、左移之后,BCD码分别置于百位、十位、个位 3、如果移位后所在的BCD码列大于或等于5,则对该值加3 4、继续左移的过程直至全部移位完成。所以最后的电压显示最大值是3.28V。
其中,二进制码转换成BCD码的代码如下:
module bin_to_bcd //此模块为了将ADC采样的数据转换为我们常用的十进制显示而存在
(
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
接下来就是将得到的电压数值的BCD数据显示在OLED屏幕上的模块,参考电子森林的OLED显示案例,知道OLED屏幕是SSD1306驱动的128*32分辨率的OLED屏幕,从功能上可以划分成两部分,驱动芯片电路 和 OLED点阵硬件,着手这部分时,我也去百度和CSDN上详细检索浏览了相关知识,但也只是停留在了解这个程度,这里也不过多赘述,详情见OLED屏幕的案例。案例中是用了5*8的点阵显示一个字符,OLED屏幕上共有四行,每行都显示了数据,案例中先显示不变化的屏幕内容(四行"OLED TEST ")然后将得到的拨码开关数据显示在四行中空白的地方。案例main状态代码如下:
5'd1: begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "OLED TEST ";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "OLED TEST ";state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "OLED TEST ";state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "OLED TEST ";state <= SCAN; end
5'd5: begin y_p <= 8'hb0; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <= sw; state <= SCAN; end
5'd6: begin y_p <= 8'hb1; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <= sw; state <= SCAN; end
5'd7: begin y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <= sw; state <= SCAN; end
5'd8: begin y_p <= 8'hb3; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <= sw; state <= SCAN; end
而本项目只需要显示一次数据,所以我就集中显示了四行的屏显内容,具体代码如下:
MAIN:begin
if(cnt_main >= 5'd4) cnt_main <= 5'd3;//四行,第3行显示电压数据
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 <= " STEP-MXO2 ";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Voltmeter ";state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <={" voltage",": ",oled_din[23:16],8'd46,oled_din[15:8],oled_din[7:0]," V "};
state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " Lyc";state <= SCAN; end
default: state <= IDLE;
endcase
end
OLED屏幕具体图像如下:
最后附上TOP模块代码:
module TOP
(
input clk, //系统时钟
input rst_n, //系统复位,低有效
//ADC总线
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, //数码管段选
//oled输出
output oled_csn, //OLCD液晶屏使能
output oled_rst, //OLCD液晶屏复位
output oled_dcn, //OLCD数据指令控制
output oled_clk, //OLCD时钟信号
output oled_dat //OLCD数据信号
);
wire [19:0] bcd_code;
wire adc_done;
wire [7:0] adc_data;
//ADC功能,例化
ADS7868 u1
(
.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.0129),这里我们直接乘以129,得到的数据经过BCD转码后小数点左移4位即可
wire [15:0] bin_code = adc_data * 16'd129;
wire [19:0] bcd_code;
//将处理后的ADC数据进行BCD转码,例化
bin_to_bcd u2
(
.rst_n (rst_n ), //系统复位,低有效
.bin_code (bin_code ), //需要进行BCD转码的二进制数据
.bcd_code (bcd_code ) //转码后的BCD码型数据输出
);
//将电压值在OLED屏幕上输出
OLED12832 u3
(
.clk (clk ), //系统时钟
.rst_n (rst_n ), //系统复位,低有效
.oled_csn (oled_csn ), //OLED屏CS
.oled_rst (oled_rst ), //OLED_RST
.oled_dcn (oled_dcn ), //OLED_D/C
.oled_clk (oled_clk ), //OLED时钟
.oled_dat (oled_dat ), //OLED数据
.bcd_code (bcd_code ) //输入到OLED的BCD数据
);
//电压值在数码管输出模块
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
);
endmodule
四、心得体会
第一次接触FPGA,知道了硬件编程和软件编程有很大的不同。用Verilog编程体现了不一样的编程思维,需要先自己设计大概的模块,然后根据模块的内容反映到代码当中,还需要考虑一系列的时序问题。
这一次的活动,不仅仅是一次锻炼,更是一次从理论到实践的过度,是对于FPGA工程师这类职业一次亲切的接触。最后,要感谢电子森林和硬核学堂给我提供的这次机会,也要感谢在此过程中帮助过我的人。