2021"暑期一起练"项目1 - 利用ADC制作一个数字电压表
使用基于Lattice XO2-4000HC的FPGA综合训练版,实现了可调电压的数字电压表。
标签
FPGA
数字逻辑
显示
ciao
更新2021-09-15
1374

一、项目要求

利用ADC制作一个数字电压表

1.旋转电位计可以产生0-3.3V的电压

2.利用板上的串行ADC对电压进行转换

3.将电压值在板上的OLED屏幕上显示出来

二、设计思路

1.基本功能模块

ADC驱动:利用板上ADS7868芯片对电位计电压进行采集,并得到二进制电压倍数传给数据模块进行处理。

数据转换模块:将ADC传来数据先转换为二进制电压数据,再变为BCD码表示的电压值。

OLED驱动:由BCD码电压值在OLED屏幕上显示出来。

2.模块框图

FpvIocC1_b9ubSOBvwEF0BEveOEu

图1 模块框图

   如图所示,本项目的基本模块连接情况。芯片产生12MHz的晶振,经分频模块产生6MHz的时钟供ADC驱动采样使用。ADC驱动在6MHz的时钟频率下,驱动ADS7868芯片工作,以SPI通信方式对芯片输出时钟信号(adc_clk)与片选信号(adc_cs),接收到二进制形式的采样电压倍数(adc_in),并输出每次采样电压(adc_data)。此电压经数值转换模块得到BCD码电压(bcd_code),送给OLED驱动模块使用。OLED驱动模块得到要显示的BCD码后,同样以SPI的通信方式对芯片SSD1306输出指令与数据,从而实现OLED显示。详细的接口信息在下一部分说明。

3.RTL级电路图

FriwQv3CP_aVEzPAmMhWe2-58l4C

图2 RTL级电路

三、各模块代码及介绍

1 ADC驱动模块(ADC_driver.v)

本模块的编写参考了简易电压表设计项目中ADC081S101驱动的编写思路。

1.1 时序分析

Fo97GFYPsrTy1zA2nxFMsuEQiXvz

图3 芯片采样时序图

 时序图信息:①三个时钟周期的采样保持时间,输出数据无效;

                   ②时钟下降沿输出数据变化,在时钟高电平采集数据;

                   ③本芯片共输出8位有效数据;

                   ④芯片采样至少需要12个时钟周期,添加几个延时周期方便数据传输。这里选择使用14个时钟周期。

 

1.2 编程思路

FPGA芯片与ADS7868芯片采用SPI通信协议,时钟信号由FPGA设计一个计数器控制输出,按时序图实现对控制芯片的采样。

Fj4NHazCW255f8E0jG_S3YYzXoYL

图4 ADS7868采样参数

由其采样频率的参数可知,ADC的3MHz的时钟频率即可,故驱动应有6MHz的系统时钟,添加一个分频模块(clock_divide.v)。

1.3 接口

input clk,                //系统时钟
input rst_n,             //系统复位,低有效
output reg adc_cs,   //SPI总线CS
output reg adc_clk,  //SPI总线SCK
input adc_in,           //SPI总线SDA
output reg [7:0] adc_data //ADC采样数据

1.4 代码

module ADC_driver
(
input				clk,		//系统时钟
input				rst_n,  	//复位信号
output	reg			adc_cs,		//SPI总线CS
output	reg			adc_clk,	//SPI总线SCK
input				adc_in,	//SPI总线SDA
output	reg [7:0]		adc_data	//ADC采样数据
);



parameter HIGH = 1;
parameter LOW = 0;

reg [7:0] cnt; //计数器
always @(posedge clk or negedge rst_n)
	if(!rst_n) cnt <= 1'b0;
	else if(cnt >= 8'd28) 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;
	end else case(cnt)
		8'd0 :  begin adc_cs <= HIGH; adc_clk <= HIGH; end
		8'd1 :  begin adc_cs <= LOW;  adc_clk <= HIGH; end  //CS信号片选 进入采样周期
		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:	
				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_in; end //3 按MSB顺序采集8个有效数据
		8'd11 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[6] <= adc_in; end //4
		8'd13 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[5] <= adc_in; end //5
		8'd15 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[4] <= adc_in; end //6
		8'd17 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[3] <= adc_in; end //7
		8'd19 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[2] <= adc_in; end //8
		8'd21 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[1] <= adc_in; end //9
		8'd23 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[0] <= adc_in; end //10
		8'd25 : begin adc_cs <= LOW;  adc_clk <= HIGH; adc_data <= data; end //11 结束采样 拉高CS信号
		8'd27 : begin adc_cs <= LOW;  adc_clk <= HIGH; end //12
		8'd28 : begin adc_cs <= HIGH;  adc_clk <= HIGH; end
		default : begin adc_cs <= HIGH;  adc_clk <= HIGH;  end
	endcase

endmodule

 

2.十进制码转换模块(bin2num.v)

将二进制数转换成BCD码的形式,采用左移加三的算法。

2.1 接口

input [15:0]bin_code,            //二进制电压
input rst_n,                           //复位信号
output reg[19:0] bcd_code     //BCD码电压

2.2 代码

module bin2num(
input [15:0]bin_code,
input rst_n,
output reg[19:0] bcd_code
);
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驱动模块(OLED_SSD1306.v)

OLED驱动即是与OLED屏控制芯片SSD1306进行通信,此部分的完整编写难度较大,并未改变寻址方式,这里使用了电子森林OLED驱动说明及Verilog代码实例项目源码进行修改得到了输出。

3.1 编程思路:

①读取cmd_RAM中存储的初始化命令进行寻址方式等相关设置;

②利用状态机讲显示分为五个部分:待机(IDLE)、主要状态(MAIN)、初始化状态(INIT)、刷屏状态(SCAN)、数据发送状态(WRITE)、延时状态(DELAY)。

③MAIN状态:设置每一行输入的字符存储到char寄存器中。在MAIN状态中完成对工作状态的切换,以实现OLED驱动的运转。这也是在相同格式下输出不同内容要修改的地方。

   INIT状态:执行预存储的命令完成OLED初始化;

   SCAN状态:读取每个字符8*8矩阵中对应的LED显示数组(前3*8矩阵起分割作用),存储到char_reg中;

   WRITE状态:SPI时序将数据发送给屏幕点亮对应点位。

④配置命令CMD与字库数据在RAM中以便使用。

3.2 接口

output reg oled_csn, //OLCD液晶屏使能
output reg oled_rst, //OLCD液晶屏复位
output reg oled_dcn, //OLCD数据指令控制 控制写入数据为数据还是指令
output reg oled_clk, //OLCD时钟信号
output reg oled_dat //OLCD数据信号

3.3 修改部分代码

MAIN:begin
						if(cnt_main >= 5'd5) 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 <= "Project:        ";state <= SCAN; end
							5'd2:	begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "ADC_voltmeter   ";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 <= "V:              ";state <= SCAN; end
							
							//一位数字对应4位BCD码,一个char的输出对应8位,故此处用4'b0+bcd_code[]补足一个char型结构
							5'd5:	begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd 5; char <= {bcd_code[19:16],".",4'b0,bcd_code[15:12],4'b0,bcd_code[11:8],"V"}; state <= SCAN; end
 
							default: state <= IDLE;
						endcase
					end

4.顶层模块(top.v)

按设计思路对各个基本模块进行连接。

module top
(
	input clk,
	input rst_n,
	//ADC管脚
	input adc_in,
	output adc_clk,
	output adc_cs,
	//OLED驱动管脚
	output	oled_csn,
    output	oled_rst,
    output	oled_dcn,
    output	oled_clk,
    output	oled_dat
);

wire clk_6M;

//时钟分频
clock_divide u4
(
.clk(clk),
.rst_n(rst_n),
.clk_6M(clk_6M)
);

//ADC采样
wire[7:0] adc_data;
ADC_driver u1
(
	.clk(clk_6M),		//系统时钟
	.rst_n(rst_n),  	//系统复位,低有效
	.adc_cs(adc_cs),		//SPI总线CS
	.adc_clk(adc_clk),	//SPI总线SCK
	.adc_in(adc_in),	//SPI总线SDA
	.adc_data(adc_data)	//ADC采样数据
);

//ADC采样二进制码转换电压BCD码
wire [15:0] bin_code = adc_data * 16'd129;
wire[19:0] bcd_code;
bin2num u2(
.bin_code(bin_code),
.rst_n(rst_n),
.bcd_code(bcd_code)
);

//BCD码显示
OLED_SSD1306 u3
(
.clk(clk),
.rst_n(rst_n),
.bcd_code(bcd_code),
.oled_csn(oled_csn),
.oled_rst(oled_rst),
.oled_dcn(oled_dcn),
.oled_clk(oled_clk),
.oled_dat(oled_dat)
);

endmodule

四、项目总结与心得

本次项目是我第一次完整的接触到FPGA项目的实战,小脚丫开发板虽然体积不大,但功能对我这样的初学者也十分足够,小小一块板子也有足够的东西去学习。

在本次项目中,让我学到了许多。我第一次明白了如何用Verilog描述芯片的时序图,在以前总是对时序的学习是纸上谈兵的感觉,这次终于理解了它工作起来的面貌;其次,芯片通信协议其实就藏在芯片使用之中,之前学习通信时序时,总觉得时序十分玄妙,这次在编写两个驱动时真切感受到了了SPI时序并不如我所想的一样;最后则是各种硬件电路与芯片功能上知识的积累。

同样本次实习中也还存在一些遗憾,未能自己改变OLED的驱动方式,自己写出一套命令对OLED进行配置,希望以后能有所长进。

总之,在本次“暑期一起练”活动中,FPGA的学习让我乐在其中,受益匪浅!

五、成果演示

FsBAWfurwnmuFedhJMldcclVmTt5

本项目完整工程文件附在附件中。

附件下载
ADC_voltmeter.rar
完整工程文件
团队介绍
北京理工大学
团队成员
段松
北京理工大学电子信息工程专业
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号