2021暑假一起练项目-用小脚丫FPGA平台完成一个数字电压表
本次实验使用小脚丫FPGA综合实验平台完成了一个数字电压表的设计。使用到的器件包括板上的ADS7868和OLED显示屏。结果是通过FPGA控制AD采样板上滑动变阻器分得的电压,电压值在液晶屏上显示。
标签
FPGA
测试
数字逻辑
显示
桂林小张
更新2021-09-11
1235

项目总结报告

前言:

      大学期间曾自学过FPGA,但是了解的不深。后面也有自学过常用接口的设计。最近在做项目时用到了串行AD,当时根据网上的一些教程,用线型序列机的思路完成了设计,是基于Xilinx平台和Vivado软件开发的。但是在调试的时候遇到了很多的问题,没想清楚。我在电子森林的教程中看到了关于串行ADC的教程,觉得教程中的方法更简便。加上本次小脚丫FPGA平台板上自带串行ADC,所以想再深入研究一下,为毕业后的工作做一点积累。

一 项目要求:

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

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

(2)利用板上的串行ADC(ADS7868)对电压进行转换;

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

二 设计思路:

      要完成该项目,首先要控制串行AD(ADS7868)对滑动变阻器的分压进行连续采样,这个部分需要根据AD芯片手册的时序图,在模块中写出控制AD时钟和使能的时序,并读回串行采样结果,转换成并行数据;其次,要将AD采样的数据由二进制码转换成BCD码,方便给OLED显示;最后要控制OLED显示采样结果。

      因此我把整个项目要求分为了以下几个模块进行:ADS7868时序控制模块、二进制码转BCD码模块、OLED显示模块和顶层模块。

三 代码实现:

1、ADS7868时序控制模块

      ADS7868是TI的8位串行单通道AD。查看ADS7868数据手册,其各引脚功能如下图1所示。主要控制CS(使能)、SDO(采样回传接口)和SCLK(AD时钟)。要特别注意一下SDO端口:

(1)该端口每在SCLK时钟下降沿输出数字量,从MSB至LSB,对于ADS7868其分辨率为8位(即8位数字量);

(2)当片选信号有效(低电平)后会产生连续4个0,在转换结束后SDO端口呈变为高阻态(Hi-Z)。

Fjz8FfAWDATxDtbFj4YgVEmpJrRi

图1 管脚功能图

      阅读ADS7868的时序图,时序图如图2所示:

(1)CS使能信号是低电平有效后,CS拉低后在下个SCLK下降沿前,SDO便会输出0,而后的3个SCLK下降沿各输出0;

(2)SDO是AD返回给FPGA的串行数据信号,从高位到低位依次接收;

(3)接收8位数字量后,CS拉高,一次转换完成。

FrZ7ZeDgQlE2_phBWLQxsnF1zPBt

图2 ADS7868芯片时序图

      根据手册的采样速率参数,如图3所示。我们可以看到芯片的SCLK时钟最高位3.4MHz。板上的系统时钟是12MHz,因此我们将SCLK时钟定为3MHz。为了方便时钟周期的计数,我们将计数时钟定为SCLK时钟的两倍,即6MHz,可由系统时钟分频得到。

Foh5tG5XEA68P6r2vNe9l8NbUcBo

图3 采样速率参数

      ADS7868驱动程序如下:

 

//ADC ADS7868驱动设计

module ADS7868(
			Clk,//假设是12MHz
			Rst_n,//复位输入,低电平复位

			Data,//ADC_SDO合并成的并行数据
			bin_code,
			
			ADC_SCLK,
			ADC_SDO,//数据输出,从ADC输入到FPGA中,即是读回来的串行数据
			ADC_CS	
		);

	input Clk;	    //输入时钟
	input Rst_n;    //复位输入,低电平复位
   
    input ADC_SDO;		//ADC转换结果,由ADC发给FPGA
	
    output reg [7:0]Data;	//ADC转换结果8位
	output reg ADC_SCLK;	//ADC 串行数据接口时钟信号
	output reg ADC_CS;    //ADC 串行数据接口使能信号
	
	output wire [21:0] bin_code;

	reg [7:0]r_data;	//转换结果读取内部寄存器
	
	//AD时钟是3MHz,2倍的情况是6MHz,即先对12M的时钟进行2分频
	reg [1:0]DIV_CNT;//分频计数器
	reg SCLK2X;     //2倍SCLK的采样时钟,即6MHz,需要对12MHz输入时钟进行二分频
	
	reg [4:0]SCLK_GEN_CNT;//SCLK生成暨序列机计数器

   parameter DIV_PARAM = 2;//时钟分频设置,实际SCLK时钟 频率 = fclk / (DIV_PARAM * 2)//值为2

	//生成2倍SCLK使能时钟计数器
	always@(posedge Clk or negedge Rst_n)
	if(!Rst_n)
		DIV_CNT  <= 2'd0;
	else
		if(DIV_CNT == ( DIV_PARAM - 1'b1))//2'd2 - 1'b1
			DIV_CNT  <= 2'd0;
		else 
			DIV_CNT  <= DIV_CNT + 1'b1;

	//生成2倍SCLK使能时钟(6MHz)
	always@(posedge Clk or negedge Rst_n)
	if(!Rst_n)
		SCLK2X  <= 1'b0;
	else if((DIV_CNT == (DIV_PARAM - 1'b1)))//2'd2 - 1'b1//en && 
		SCLK2X  <= 1'b1;
	else
		SCLK2X  <= 1'b0;
		
	//生成序列计数器//ADC完整采样是一个序列周期,一共要个周期
	always@(posedge SCLK2X or negedge Rst_n)
	if(!Rst_n)
		SCLK_GEN_CNT  <= 5'd0;
	else
			if(SCLK_GEN_CNT == 5'd26)//
				SCLK_GEN_CNT  <= 5'd0;
			else
				SCLK_GEN_CNT  <= SCLK_GEN_CNT + 1'd1;
	
	//序列机实现ADC串行数据接口的数据发送和接收
	always@(posedge SCLK2X or negedge Rst_n)//Clk
	if(!Rst_n)begin
		ADC_SCLK <= 1'b1;
		ADC_CS   <= 1'b1;	
		r_data   <= 8'd0;
		Data     <= 8'd0;
	end 
	else 
			case(SCLK_GEN_CNT)//每个上升沿,寄存ADC串行数据输出线上的转换结果
				5'd0 :begin ADC_CS <= 1'b1; ADC_SCLK <= 1'b1; end
				5'd1 :begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; end
				5'd2 :begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
				5'd3 :begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; end//1
				5'd4 :begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
				5'd5 :begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; end//2
				5'd6 :begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
				5'd7 :begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; end//3	
				5'd8 :begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
				5'd9 :begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; r_data[7] <= ADC_SDO; end//4	
				5'd10:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
				5'd11:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; r_data[6] <= ADC_SDO; end//5	
                5'd12:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
                5'd13:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; r_data[5] <= ADC_SDO; end//6
                5'd14:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
                5'd15:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; r_data[4] <= ADC_SDO; end//7
                5'd16:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
                5'd17:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; r_data[3] <= ADC_SDO; end//8
                5'd18:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
                5'd19:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; r_data[2] <= ADC_SDO; end//9
                5'd20:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
                5'd21:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; r_data[1] <= ADC_SDO; end//10
                5'd22:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end
                5'd23:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; r_data[0] <= ADC_SDO; end//11
                5'd24:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b0; end			
                5'd25:begin ADC_CS <= 1'b0; ADC_SCLK <= 1'b1; Data <= r_data;       end//12
                5'd26:begin ADC_CS <= 1'b1; ADC_SCLK <= 1'b1; end
                
				default:begin ADC_CS <= 1'b1; ADC_SCLK <= 1'b1;end //将转换结果输出
			endcase

	assign bin_code   = (Data*14'd12942);//
	
endmodule

      在别的教程中,我使用线性序列机实现过ADC128S22芯片的控制和采样。但是那种方法需要给一次使能En才完成一个采样周期,即采样一个数据。要连续采样就要不停地给使能信号,这样有些麻烦。在学习了电子森林的教程之后,我将系统时钟进行二分频得到一个SCLK2X信号(6MHz),对这个时钟信号进行计数。一个周期需要27个SCLK2X时钟周期,则计数器每计数27次则从新计数,循环往复。在27个周期对应的时刻,按照时序图对CS和SCLK进行拉高和拉低的操作,同时读入SDO的串行数据,SCLK的时钟是3MHz。这样就可以控制AD不停地进行采样。若需要外部使能控制(比如按键),则在SCLK2X时钟计数器上加个使能信号(如Start)即可。

      在程序中,Data是SDO经过串并转换后的采样数据(8位二进制数),为了方便取整数和后期二进制转BCD码,在这里乘12942.根据计算公式:

FteT62-HJL_JyiLJFK7FCXgBR1HM

      Data的范围是0-255,当Data=255时,表示输入的电压是3.3V,但是带入上述公式,V,与3.3V的实际电压相差较大,因此为了修正误差,采用一下公式:

FsbVE1tNINpyj0Sb_a6mFRvZhA7e

      我们将0.0129412换成0.012942,当Data=255时,,我们取小数点后三位,即3.300V,符合设计的要求。在程序中为避免使用小数,将0.012942放大为12942,当Data=255时,bin_code的十进制结果是3300210,在二进制转十进制模块中用取高位的3300的部分就可以将要显示的电压值取出来。

      下面是在Quartus Prime中实现的仿真程序:

`timescale 1ns/1ns
`define clock_period 83.3
/*注意,由于使用联合仿真的时候,modelsim的默认目录是当前Quartus工
程下的simulation目录下的modelsim文件夹,所以,需要在执行仿真前手
动将sin_8bit.txt文件拷贝到simulation/modelsim下。修改了
sin_8bit.txt内容后也请记得重新覆盖modelsim下的sin_8bit.txt文件
*/
`define sin_data_file "./sin_8bit.txt"

module ADS7868_tb;

	reg Clk;
	reg Rst_n;
	wire [7:0]Data;
	wire [21:0] bin_code;
	//reg En_Conv;
	//wire Conv_Done;
	//wire ADC_State;
	//wire [7:0]DIV_PARAM;
	
	wire ADC_SCLK;
	wire ADC_CS;
	reg  ADC_SDO;
	
	
	reg[7:0]  memory[255:0];//测试波形数据存储空间//256个8位数据
	
	reg[7:0] address;//存储器地址 

	ADS7868 ADS7868(
		.Clk(Clk),
		.Rst_n(Rst_n),
		.Data(Data),
		.bin_code(bin_code),
		//.En_Conv(En_Conv),
		//.Conv_Done(Conv_Done),
		.ADC_SCLK(ADC_SCLK),
		.ADC_SDO(ADC_SDO),
		.ADC_CS(ADC_CS)
	);

	initial Clk = 1'b1;
	always #(`clock_period/2) Clk = ~Clk;//时钟的问题···············
	
	//将原始波形数据从文件读取到定义的存储器中
	initial $readmemh(`sin_data_file,memory);//读取原始波形数据读到memory中

	integer i;
	
	initial begin
		Rst_n = 0;
		//En_Conv = 0;
		ADC_SDO = 0;
		address = 0;
		#(`clock_period*2);//#101;
		Rst_n = 1;
		#(`clock_period*2);//#100;
		for(i=0;i<1;i=i+1)begin
			for(address=0;address<255;address=address+1)begin
				//En_Conv = 1;
				#(`clock_period);//#20;
				//En_Conv = 0;
				gene_DOUT(memory[address]);	//依次将存储器中存储的波形读出,按照ADC的转换结果输出方式送到DOUT信号线上
				//@(posedge Conv_Done);	//等待转换完成信号
				#(`clock_period*2);//#200;
			end
		end
		#(`clock_period*2);//#200;
		$stop;
	end	
	
	//将并行数据按照ADC的数据输出格式,送到DOUT信号线上,供控制模块采集读取
	task gene_DOUT;
		input [11:0]vdata;//原来是16位的[15:0],现在设置成12位[11:0]
		reg [4:0]cnt;
		begin
			cnt = 0;
			wait(!ADC_CS);
			while(cnt<12)begin//<16
				@(negedge ADC_SCLK) ADC_SDO = vdata[11-cnt];//15-
				cnt = cnt + 1'b1;
			end
		end
	endtask
	
endmodule

      在仿真文件中,我们使用Guagle_wave软件生成一个正弦波文件,里面是256个8位16进制正弦波数据,并以sin_8bit.txt保存到simulation/modelsim文件夹下。这样就需要将产生的数据发送到AD驱动模块的输入线SDO上,共采集模块采集,以此验证模块的功能是否正确。仿真结果如图4所示:

FsH6o9Vl_c174kJSF3_Z8gnkZme1

图4 ADS7868模块仿真图

      如图4所示,连续采样时SCLK、CS和SDO的时序和对应的数据都正确。由于文件的数据是256个8位正弦波数据,因此将数据格式变成模拟的,可以看到Data的数据呈一个周期的正弦波。说明AD模块的设计是正确的。

  • 二进制码转BCD码模块

      该模块的功能是将AD采样的二进制码结果转换成BCD码的结果,转格式方便OLED模块显示。

      AD模块输出的二进制码是22位的,每4位二进制码组成一个BCD码,因此22位二进制码需要7个BCD码来表示。这里采用加3移位法进行码制转换。

      此处以 8 位二进制转换为 3 位 BCD 码为例,转换步骤是:将待转换的二进制码从最高位开始左移 BCD 的寄存器(从高位到低位排列),每移一次,检查每一位 BCD 码是否大于 4,是则加上 3,否则不变。左移 8 次后,即完成了转换。需要注意的是第八次移位后不需要检查是否大于 4。 这里之所以检查每一个 BCD 码是否大于 4,因为如果大于 4(比如 5、 6),下一步左移就要溢出了,所以加 3 等于左移后的加 6,起到十进制调节的作用。

      本模块中注意要左移22次,程序如下:

/*
//二进制转BCD码
*/
module bin_to_bcd #
(
parameter B_SIZE = 22//22位二进制数转BCD码
)
(
input				Rst_n,			// system reset, active low
input		[B_SIZE-1:0]	bin_code,		// binary code
output	reg	[27:0]	bcd_code		// bcd code//26位B_SIZE+3
);

reg		[49:0]		shift_reg; //48 2*B_SIZE+3  
always@(bin_code or Rst_n)begin      
	shift_reg= {28'h0,bin_code};          
	if(!Rst_n) bcd_code <= 0;      
	else begin         
	repeat(B_SIZE)//repeat B_SIZE times 22次向左移位
		begin                                
		if (shift_reg[25:22] >= 5) shift_reg[25:22] = shift_reg[25:22] + 2'd3;
		if (shift_reg[29:26] >= 5) shift_reg[29:26] = shift_reg[29:26] + 2'd3;
		if (shift_reg[33:30] >= 5) shift_reg[33:30] = shift_reg[33:30] + 2'd3;
		if (shift_reg[37:34] >= 5) shift_reg[37:34] = shift_reg[37:34] + 2'd3;
		if (shift_reg[41:38] >= 5) shift_reg[41:38] = shift_reg[41:38] + 2'd3;	
		if (shift_reg[45:42] >= 5) shift_reg[45:42] = shift_reg[45:42] + 2'd3;
		if (shift_reg[49:46] >= 5) shift_reg[49:46] = shift_reg[49:46] + 2'd3;
			
		shift_reg = shift_reg << 1; 
		end         
		bcd_code<=shift_reg[49:22]; //28位BCD码  
	end  
end

endmodule

      在BCD码转换完成后,将移位寄存器shift_reg中前28位作为结果的BCD码,此时数据的BCD码实际有7个(每4位组成一个BCD码),将这个数据传输给OLED显示模块即可。

下面是在Quartus Prime中实现的仿真程序:

`timescale 1ns/1ns
`define clock_period 20 //50MHz

module bin_to_bcd_tb;

	reg Rst_n;
	reg [21:0]bin_code;
	wire [27:0] bcd_code;

	bin_to_bcd bin_to_bcd(

		.Rst_n(Rst_n),
		.bin_code(bin_code),
		.bcd_code(bcd_code)
	);

	//initial Clk = 1'b1;
	//always #(`clock_period/2) Clk = ~Clk;//时钟的问题···············
	
	initial begin
		Rst_n = 0;
		bin_code = 0;
		//bcd_code = 0;
		#100;
		Rst_n = 1;//复位
		#100;
		bin_code = 2006010;//155-2006 约等于2V
		#200;
		bin_code = 3300210;//255-3200 约等于3.3V
		#200;
		$stop;
	end	
endmodule

      注意转码模块中不需要系统时钟。仿真结果如图5所示:

Fji4Yc1QU_OxoUiGZyxBJ9cXXXa7

图5 BCD码转码仿真结果

      如图5所示,当bin_code = 2006010时,表示实际输入电压是2.006V,bcd_code从高位到低位每4位一组,分别是0010 0000 0000 0110 0000 0001 0000,这7组数是2006010的BCD码;

当bin_code = 3300210时,表示实际输入电压是3.300V,bcd_code从高位到低位每4位一组,分别是0011 0011 0000 0000 0010 0001 0000,这7组数是3300210的BCD码;仿真结果显示BCD转码程序功能正确。

  • OLED显示模块

      OLED显示模块的功能是将采样电压数值显示在液晶屏上。

      由于数据的BCD码是28位的,因此我们从高位到低位,每4位为一组,取4组BCD码。例如bcd_code=28’b0011_0011_0000_0000_0010_0001_0000,我们从高位到低位取16位二进制数,即16’b0011_0011_0000_0000,这个数即是3300的BCD码,我们将BCD码0011、0011、0000、0000写入寄存器,分别显示在液晶屏上,为3 3 0 0,加上小数点和“V”符号,即3 . 3 0 0 V

程序如下:

// Module: OLED12832
// 
// Author: Step
// 
// Description: OLED12832_Driver,使用8*8点阵字库,每行显示128/8=16个字符
// 
// Web: www.stepfpga.com
// 
// --------------------------------------------------------------------
// Code Revision History :
// --------------------------------------------------------------------
// Version: |Mod. Date:   |Changes Made:
// V1.0     |2015/11/11   |Initial ver
// --------------------------------------------------------------------
module OLED12832
(
	input				Clk,		//12MHz系统时钟
	input				Rst_n,		//系统复位,低有效
 
	//input		[3:0]	sw,		//
	input		[27:0]  bcd_code,	//给OLED模块的BCD码数据
 
	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数据信号
);
 
	localparam INIT_DEPTH = 16'd25; //LCD初始化的命令的数量
	localparam IDLE = 6'h1, MAIN = 6'h2, INIT = 6'h4, SCAN = 6'h8, WRITE = 6'h10, DELAY = 6'h20;
	localparam HIGH	= 1'b1, LOW = 1'b0;
	localparam DATA	= 1'b1, CMD = 1'b0;
 
	reg [7:0] cmd [24:0];
	reg [39:0] mem [122:0];
	reg	[7:0]	y_p, x_ph, x_pl;
	reg	[(8*21-1):0] char;
	reg	[7:0]	num, char_reg;				//
	reg	[4:0]	cnt_main, cnt_init, cnt_scan, cnt_write;
	reg	[15:0]	num_delay, cnt_delay, cnt;
	reg	[5:0] 	state, state_back;
 
	always@(posedge Clk or negedge Rst_n) begin
		if(!Rst_n) begin
			cnt_main <= 1'b0; cnt_init <= 1'b0; cnt_scan <= 1'b0; cnt_write <= 1'b0;
			y_p <= 1'b0; x_ph <= 1'b0; x_pl <= 1'b0;
			num <= 1'b0; char <= 1'b0; char_reg <= 1'b0;
			num_delay <= 16'd5; cnt_delay <= 1'b0; cnt <= 1'b0;
			oled_csn <= HIGH; oled_rst <= HIGH; oled_dcn <= CMD; oled_clk <= HIGH; oled_dat <= LOW;
			state <= IDLE; state_back <= IDLE;
		end else begin
			case(state)
				IDLE:begin
						cnt_main <= 1'b0; cnt_init <= 1'b0; cnt_scan <= 1'b0; cnt_write <= 1'b0;
						y_p <= 1'b0; x_ph <= 1'b0; x_pl <= 1'b0;
						num <= 1'b0; char <= 1'b0; char_reg <= 1'b0;
						num_delay <= 16'd5; cnt_delay <= 1'b0; cnt <= 1'b0;
						oled_csn <= HIGH; oled_rst <= HIGH; oled_dcn <= CMD; oled_clk <= HIGH; oled_dat <= LOW;
						state <= MAIN; state_back <= MAIN;
					end
				MAIN:begin
						if(cnt_main >= 5'd10) 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 <= "ADS7868_OLED    ";state <= SCAN; end
							5'd2:	begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Voltage_TEST    ";state <= SCAN; end
							5'd3:	begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Vout =          ";state <= SCAN; end
							5'd4:	begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
 
							5'd5:	begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd 1; char <= bcd_code[27:24]; state <= SCAN; end
							5'd6:	begin y_p <= 8'hb3; x_ph <= 8'h11; x_pl <= 8'h00; num <= 5'd 1; char <= ".";             state <= SCAN; end
							5'd7:	begin y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd 1; char <= bcd_code[23:20]; state <= SCAN; end
							5'd8:	begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd 1; char <= bcd_code[19:16];   state <= SCAN; end
							5'd9:	begin y_p <= 8'hb3; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd 1; char <= bcd_code[15:12];   state <= SCAN; end
							5'd10:	begin y_p <= 8'hb3; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <= "V";             state <= SCAN; end
							
							default: state <= IDLE;
						endcase
					end
				INIT:begin	//初始化状态
						case(cnt_init)
							5'd0:	begin oled_rst <= 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 <= 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_dcn <= 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	//刷屏状态,从RAM中读取数据刷屏
						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_dcn <= CMD; char_reg <= y_p; state <= WRITE; state_back <= SCAN; end		//定位列页地址
							5'd 1:	begin oled_dcn <= CMD; char_reg <= x_pl; state <= WRITE; state_back <= SCAN; end	//定位行地址低位
							5'd 2:	begin oled_dcn <= 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_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	//将5*8点阵编程8*8
							5'd 5:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	//将5*8点阵编程8*8
							5'd 6:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	//将5*8点阵编程8*8
							5'd 7:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][39:32]; state <= WRITE; state_back <= SCAN; end
							5'd 8:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][31:24]; state <= WRITE; state_back <= SCAN; end
							5'd 9:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][23:16]; state <= WRITE; state_back <= SCAN; end
							5'd10:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][15: 8]; state <= WRITE; state_back <= SCAN; end
							5'd11:	begin oled_dcn <= 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	//WRITE状态,将数据按照SPI时序发送给屏幕
						if(cnt_write >= 5'd17) cnt_write <= 1'b0;
						else cnt_write <= cnt_write + 1'b1;
						case(cnt_write)
							5'd 0:	begin oled_csn <= LOW; end	//9位数据最高位为命令数据控制位
							5'd 1:	begin oled_clk <= LOW; oled_dat <= char_reg[7]; end	//先发高位数据
							5'd 2:	begin oled_clk <= HIGH; end
							5'd 3:	begin oled_clk <= LOW; oled_dat <= char_reg[6]; end
							5'd 4:	begin oled_clk <= HIGH; end
							5'd 5:	begin oled_clk <= LOW; oled_dat <= char_reg[5]; end
							5'd 6:	begin oled_clk <= HIGH; end
							5'd 7:	begin oled_clk <= LOW; oled_dat <= char_reg[4]; end
							5'd 8:	begin oled_clk <= HIGH; end
							5'd 9:	begin oled_clk <= LOW; oled_dat <= char_reg[3]; end
							5'd10:	begin oled_clk <= HIGH; end
							5'd11:	begin oled_clk <= LOW; oled_dat <= char_reg[2]; end
							5'd12:	begin oled_clk <= HIGH; end
							5'd13:	begin oled_clk <= LOW; oled_dat <= char_reg[1]; end
							5'd14:	begin oled_clk <= HIGH; end
							5'd15:	begin oled_clk <= LOW; oled_dat <= char_reg[0]; end	//后发低位数据
							5'd16:	begin oled_clk <= HIGH; end
							5'd17:	begin oled_csn <= 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配置指令数据
	always@(posedge Rst_n)
		begin
			cmd[ 0] = {8'hae}; 
			cmd[ 1] = {8'h00}; 
			cmd[ 2] = {8'h10}; 
			cmd[ 3] = {8'h00}; 
			cmd[ 4] = {8'hb0}; 
			cmd[ 5] = {8'h81}; 
			cmd[ 6] = {8'hff}; 
			cmd[ 7] = {8'ha1}; 
			cmd[ 8] = {8'ha6}; 
			cmd[ 9] = {8'ha8}; 
			cmd[10] = {8'h1f}; 
			cmd[11] = {8'hc8};
			cmd[12] = {8'hd3};
			cmd[13] = {8'h00};
			cmd[14] = {8'hd5};
			cmd[15] = {8'h80};
			cmd[16] = {8'hd9};
			cmd[17] = {8'h1f};
			cmd[18] = {8'hda};
			cmd[19] = {8'h00};
			cmd[20] = {8'hdb};
			cmd[21] = {8'h40};
			cmd[22] = {8'h8d};
			cmd[23] = {8'h14};
			cmd[24] = {8'haf};
		end 
 
	//5*8点阵字库数据
	always@(posedge Rst_n)
		begin
			mem[  0] = {8'h3E, 8'h51, 8'h49, 8'h45, 8'h3E};   // 48  0
			mem[  1] = {8'h00, 8'h42, 8'h7F, 8'h40, 8'h00};   // 49  1
			mem[  2] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46};   // 50  2
			mem[  3] = {8'h21, 8'h41, 8'h45, 8'h4B, 8'h31};   // 51  3
			mem[  4] = {8'h18, 8'h14, 8'h12, 8'h7F, 8'h10};   // 52  4
			mem[  5] = {8'h27, 8'h45, 8'h45, 8'h45, 8'h39};   // 53  5
			mem[  6] = {8'h3C, 8'h4A, 8'h49, 8'h49, 8'h30};   // 54  6
			mem[  7] = {8'h01, 8'h71, 8'h09, 8'h05, 8'h03};   // 55  7
			mem[  8] = {8'h36, 8'h49, 8'h49, 8'h49, 8'h36};   // 56  8
			mem[  9] = {8'h06, 8'h49, 8'h49, 8'h29, 8'h1E};   // 57  9
			mem[ 10] = {8'h7C, 8'h12, 8'h11, 8'h12, 8'h7C};   // 65  A
			mem[ 11] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h36};   // 66  B
			mem[ 12] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h22};   // 67  C
			mem[ 13] = {8'h7F, 8'h41, 8'h41, 8'h22, 8'h1C};   // 68  D
			mem[ 14] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h41};   // 69  E
			mem[ 15] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h01};   // 70  F
 
			mem[ 32] = {8'h00, 8'h00, 8'h00, 8'h00, 8'h00};   // 32  sp 
			mem[ 33] = {8'h00, 8'h00, 8'h2f, 8'h00, 8'h00};   // 33  !  
			mem[ 34] = {8'h00, 8'h07, 8'h00, 8'h07, 8'h00};   // 34  
			mem[ 35] = {8'h14, 8'h7f, 8'h14, 8'h7f, 8'h14};   // 35  #
			mem[ 36] = {8'h24, 8'h2a, 8'h7f, 8'h2a, 8'h12};   // 36  $
			mem[ 37] = {8'h62, 8'h64, 8'h08, 8'h13, 8'h23};   // 37  %
			mem[ 38] = {8'h36, 8'h49, 8'h55, 8'h22, 8'h50};   // 38  &
			mem[ 39] = {8'h00, 8'h05, 8'h03, 8'h00, 8'h00};   // 39  '
			mem[ 40] = {8'h00, 8'h1c, 8'h22, 8'h41, 8'h00};   // 40  (
			mem[ 41] = {8'h00, 8'h41, 8'h22, 8'h1c, 8'h00};   // 41  )
			mem[ 42] = {8'h14, 8'h08, 8'h3E, 8'h08, 8'h14};   // 42  *
			mem[ 43] = {8'h08, 8'h08, 8'h3E, 8'h08, 8'h08};   // 43  +
			mem[ 44] = {8'h00, 8'h00, 8'hA0, 8'h60, 8'h00};   // 44  ,
			mem[ 45] = {8'h08, 8'h08, 8'h08, 8'h08, 8'h08};   // 45  -
			mem[ 46] = {8'h00, 8'h60, 8'h60, 8'h00, 8'h00};   // 46  .
			mem[ 47] = {8'h20, 8'h10, 8'h08, 8'h04, 8'h02};   // 47  /
			mem[ 48] = {8'h3E, 8'h51, 8'h49, 8'h45, 8'h3E};   // 48  0
			mem[ 49] = {8'h00, 8'h42, 8'h7F, 8'h40, 8'h00};   // 49  1
			mem[ 50] = {8'h42, 8'h61, 8'h51, 8'h49, 8'h46};   // 50  2
			mem[ 51] = {8'h21, 8'h41, 8'h45, 8'h4B, 8'h31};   // 51  3
			mem[ 52] = {8'h18, 8'h14, 8'h12, 8'h7F, 8'h10};   // 52  4
			mem[ 53] = {8'h27, 8'h45, 8'h45, 8'h45, 8'h39};   // 53  5
			mem[ 54] = {8'h3C, 8'h4A, 8'h49, 8'h49, 8'h30};   // 54  6
			mem[ 55] = {8'h01, 8'h71, 8'h09, 8'h05, 8'h03};   // 55  7
			mem[ 56] = {8'h36, 8'h49, 8'h49, 8'h49, 8'h36};   // 56  8
			mem[ 57] = {8'h06, 8'h49, 8'h49, 8'h29, 8'h1E};   // 57  9
			mem[ 58] = {8'h00, 8'h36, 8'h36, 8'h00, 8'h00};   // 58  :
			mem[ 59] = {8'h00, 8'h56, 8'h36, 8'h00, 8'h00};   // 59  ;
			mem[ 60] = {8'h08, 8'h14, 8'h22, 8'h41, 8'h00};   // 60  <
			mem[ 61] = {8'h14, 8'h14, 8'h14, 8'h14, 8'h14};   // 61  =
			mem[ 62] = {8'h00, 8'h41, 8'h22, 8'h14, 8'h08};   // 62  >
			mem[ 63] = {8'h02, 8'h01, 8'h51, 8'h09, 8'h06};   // 63  ?
			mem[ 64] = {8'h32, 8'h49, 8'h59, 8'h51, 8'h3E};   // 64  @
			mem[ 65] = {8'h7C, 8'h12, 8'h11, 8'h12, 8'h7C};   // 65  A
			mem[ 66] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h36};   // 66  B
			mem[ 67] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h22};   // 67  C
			mem[ 68] = {8'h7F, 8'h41, 8'h41, 8'h22, 8'h1C};   // 68  D
			mem[ 69] = {8'h7F, 8'h49, 8'h49, 8'h49, 8'h41};   // 69  E
			mem[ 70] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h01};   // 70  F
			mem[ 71] = {8'h3E, 8'h41, 8'h49, 8'h49, 8'h7A};   // 71  G
			mem[ 72] = {8'h7F, 8'h08, 8'h08, 8'h08, 8'h7F};   // 72  H
			mem[ 73] = {8'h00, 8'h41, 8'h7F, 8'h41, 8'h00};   // 73  I
			mem[ 74] = {8'h20, 8'h40, 8'h41, 8'h3F, 8'h01};   // 74  J
			mem[ 75] = {8'h7F, 8'h08, 8'h14, 8'h22, 8'h41};   // 75  K
			mem[ 76] = {8'h7F, 8'h40, 8'h40, 8'h40, 8'h40};   // 76  L
			mem[ 77] = {8'h7F, 8'h02, 8'h0C, 8'h02, 8'h7F};   // 77  M
			mem[ 78] = {8'h7F, 8'h04, 8'h08, 8'h10, 8'h7F};   // 78  N
			mem[ 79] = {8'h3E, 8'h41, 8'h41, 8'h41, 8'h3E};   // 79  O
			mem[ 80] = {8'h7F, 8'h09, 8'h09, 8'h09, 8'h06};   // 80  P
			mem[ 81] = {8'h3E, 8'h41, 8'h51, 8'h21, 8'h5E};   // 81  Q
			mem[ 82] = {8'h7F, 8'h09, 8'h19, 8'h29, 8'h46};   // 82  R
			mem[ 83] = {8'h46, 8'h49, 8'h49, 8'h49, 8'h31};   // 83  S
			mem[ 84] = {8'h01, 8'h01, 8'h7F, 8'h01, 8'h01};   // 84  T
			mem[ 85] = {8'h3F, 8'h40, 8'h40, 8'h40, 8'h3F};   // 85  U
			mem[ 86] = {8'h1F, 8'h20, 8'h40, 8'h20, 8'h1F};   // 86  V
			mem[ 87] = {8'h3F, 8'h40, 8'h38, 8'h40, 8'h3F};   // 87  W
			mem[ 88] = {8'h63, 8'h14, 8'h08, 8'h14, 8'h63};   // 88  X
			mem[ 89] = {8'h07, 8'h08, 8'h70, 8'h08, 8'h07};   // 89  Y
			mem[ 90] = {8'h61, 8'h51, 8'h49, 8'h45, 8'h43};   // 90  Z
			mem[ 91] = {8'h00, 8'h7F, 8'h41, 8'h41, 8'h00};   // 91  [
			mem[ 92] = {8'h55, 8'h2A, 8'h55, 8'h2A, 8'h55};   // 92  .
			mem[ 93] = {8'h00, 8'h41, 8'h41, 8'h7F, 8'h00};   // 93  ]
			mem[ 94] = {8'h04, 8'h02, 8'h01, 8'h02, 8'h04};   // 94  ^
			mem[ 95] = {8'h40, 8'h40, 8'h40, 8'h40, 8'h40};   // 95  _
			mem[ 96] = {8'h00, 8'h01, 8'h02, 8'h04, 8'h00};   // 96  '
			mem[ 97] = {8'h20, 8'h54, 8'h54, 8'h54, 8'h78};   // 97  a
			mem[ 98] = {8'h7F, 8'h48, 8'h44, 8'h44, 8'h38};   // 98  b
			mem[ 99] = {8'h38, 8'h44, 8'h44, 8'h44, 8'h20};   // 99  c
			mem[100] = {8'h38, 8'h44, 8'h44, 8'h48, 8'h7F};   // 100 d
			mem[101] = {8'h38, 8'h54, 8'h54, 8'h54, 8'h18};   // 101 e
			mem[102] = {8'h08, 8'h7E, 8'h09, 8'h01, 8'h02};   // 102 f
			mem[103] = {8'h18, 8'hA4, 8'hA4, 8'hA4, 8'h7C};   // 103 g
			mem[104] = {8'h7F, 8'h08, 8'h04, 8'h04, 8'h78};   // 104 h
			mem[105] = {8'h00, 8'h44, 8'h7D, 8'h40, 8'h00};   // 105 i
			mem[106] = {8'h40, 8'h80, 8'h84, 8'h7D, 8'h00};   // 106 j
			mem[107] = {8'h7F, 8'h10, 8'h28, 8'h44, 8'h00};   // 107 k
			mem[108] = {8'h00, 8'h41, 8'h7F, 8'h40, 8'h00};   // 108 l
			mem[109] = {8'h7C, 8'h04, 8'h18, 8'h04, 8'h78};   // 109 m
			mem[110] = {8'h7C, 8'h08, 8'h04, 8'h04, 8'h78};   // 110 n
			mem[111] = {8'h38, 8'h44, 8'h44, 8'h44, 8'h38};   // 111 o
			mem[112] = {8'hFC, 8'h24, 8'h24, 8'h24, 8'h18};   // 112 p
			mem[113] = {8'h18, 8'h24, 8'h24, 8'h18, 8'hFC};   // 113 q
			mem[114] = {8'h7C, 8'h08, 8'h04, 8'h04, 8'h08};   // 114 r
			mem[115] = {8'h48, 8'h54, 8'h54, 8'h54, 8'h20};   // 115 s
			mem[116] = {8'h04, 8'h3F, 8'h44, 8'h40, 8'h20};   // 116 t
			mem[117] = {8'h3C, 8'h40, 8'h40, 8'h20, 8'h7C};   // 117 u
			mem[118] = {8'h1C, 8'h20, 8'h40, 8'h20, 8'h1C};   // 118 v
			mem[119] = {8'h3C, 8'h40, 8'h30, 8'h40, 8'h3C};   // 119 w
			mem[120] = {8'h44, 8'h28, 8'h10, 8'h28, 8'h44};   // 120 x
			mem[121] = {8'h1C, 8'hA0, 8'hA0, 8'hA0, 8'h7C};   // 121 y
			mem[122] = {8'h44, 8'h64, 8'h54, 8'h4C, 8'h44};   // 122 z
		end
 
endmodule

      OLED部分的程序是根据电子森林的例程修改得来的,修改过程不难,且能在板上显示结果,因此这里没有使用testbench文件进行仿真。实物图如图6所示:

Fhlr-p-FI3NLYlgJmSLqDy06-DtO

图6 实物图

  • 顶层模块

      顶层模块的作用是在顶层将三个底层模块的端口相连,并确定整个程序的输入输出端口。顶层模块一般不写逻辑。程序如下:

/***************************************************
*	Module Name		:	ADS7868_OLED_top		   
*	Engineer		   :	zcy
*	Target Device	:	
*	Tool versions	:	
*	Create Date		:	2021-08-11
*	Revision		   :	v1.0
*	Description		:  ADS7868采样并在OLED上显示的顶层文件
**************************************************/
module ADS7868_OLED_top(
			Clk,//系统时钟12MHz,ADC控制时钟SCLK=3MHz
			Rst_n,//采用低电平复位
			
			ADC_SCLK,//AD时钟 3MHz
			ADC_CS,//AD工作使能
			ADC_SDO,//数据输出,从ADC输入到FPGA中,即是读回来的串行数据
			
			oled_csn,	//OLCD液晶屏使能
			oled_rst,	//OLCD液晶屏复位
			oled_dcn,	//OLCD数据指令控制
			oled_clk,	//OLCD时钟信号
			oled_dat	//OLCD数据信号	
			
				);

	input Clk,Rst_n;
	
	//ADC模块接口
	output ADC_SCLK;//AD时钟 3MHz
	output ADC_CS;  //AD工作使能
	input  ADC_SDO; //数据输出,从ADC输入到FPGA中,即是读回来的串行数据

	//OLED模块接口
	output oled_csn;	//OLCD液晶屏使能
	output oled_rst;	//OLCD液晶屏复位
	output oled_dcn;	//OLCD数据指令控制
	output oled_clk;	//OLCD时钟信号
	output oled_dat;    //OLCD数据信号	

	//顶层到ADS7868模块的连线
	
	//ADS7868模块到二进制转BCD码模块(bin_to_bcd)的连线
	wire [21:0] bin_code;
	
	//二进制转BCD码模块(bin_to_bcd)到OLED显示模块(OLED12832)的连线
	wire [28:0] bcd_code;

	//AD7868时序控制模块;负责控制AD,读回采样数据,转并行,得二进制码转换结果
	ADS7868 ADS7868(
	.Clk(Clk),
	.Rst_n(Rst_n),
	
	.Data(),//ADC_SDO合并成的并行数据,这里不连接,预留
	.bin_code(bin_code),//给二进制转BCD码模块的数据,二进制数据
	
	.ADC_SCLK(ADC_SCLK),
	.ADC_CS(ADC_CS),
	.ADC_SDO(ADC_SDO)//
		
	);
	
	//二进制转BCD码模块;负责将AD采样的二进制数据转换成BCD码,发送给OLED显示
	bin_to_bcd bin_to_bcd(
	.Rst_n(Rst_n),
	
	.bin_code(bin_code),//给二进制转BCD码模块的数据,二进制数据
	.bcd_code(bcd_code)//给OLED模块的BCD码数据,用于显示
		
	);
	
	//OLED显示模块;控制OLED显示电压数据
	OLED12832 OLED12832(
		.Clk(Clk),
		.Rst_n(Rst_n),
		
		.bcd_code(bcd_code),	//给OLED模块的BCD码数据

		.oled_csn(oled_csn),	//OLCD液晶屏使能
		.oled_rst(oled_rst),	//OLCD液晶屏复位
		.oled_dcn(oled_dcn),	//OLCD数据指令控制
		.oled_clk(oled_clk),	//OLCD时钟信号
		.oled_dat(oled_dat)	    //OLCD数据信号
				
);		

endmodule

顶层模块编译后,无错误,RTL视图如图7所示:

FiUFUJTbfATTCmbNQK-E-t0OavaL

图7 整个工程的RTL视图

      程序仿真,RTL视图都确认无误,根据电路图配置管脚,功能达成。

四 总结

      经过本次学习,我掌握了串行AD时序逻辑控制原理,二进制转BCD码的转码原理和OLED显示。对线性序列机和连续采样的程序写法有了新的体会。

五 注意事项

      原理图上是没有跳线帽的,但是板上有啊!!!跳线帽要插到R_ADJ才能采到电压!(别问我为什么知道)

FvBDfwtZPSldaoYn8Zz3Y9M1x09q

FlDL8l5PF9XFcYiT83y9QCcLReWC

 

 

 

 

附件下载
源代码.rar
数字电压表的源代码。
团队介绍
本次项目是个人项目,因此是一个人完成的。
团队成员
桂林小张
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号