本节将和大家一起使用FPGA驱动底板上的PCF8591的ADC采样(I2C)功能。


硬件说明

PCF8591是集成了4路ADC和1路DAC的芯片,使用I2C总线通信。
I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件。如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件,然后主机接收从器件发送的数据,最后由主机终止接收过程。这里不做过多的讲解,硬件连接如下:
本设计的硬件连接如下
本设计中FPGA作为I2C主设备,PCF8591作为I2C从设备,从设备的地址由固定地址和可编程地址组成,我们的外设底板已将可编程地址A0、A1、A2接地,所以7位地址为7'h48,加上最低位的读写控制,所以给PCF8591写数据时的寻址地址为8'h90,对PCF8591读数据时的寻址地址为8'h91。如下
PCF8591集成了很多功能,当需要不同的功能时要对PCF8591做相应的配置,配置数据存储在名为CONTROL BYTE的寄存器中,下图展示了寄存器中部分bit的功能,详细请参考PCF8591的datasheet,本设计中我们只使用通道1的ADC功能,配置数据为8'h01。
本设计中我们需要两次通信,

  • 第一次为配置数据,具体为:开始–写寻址–读响应–写配置数据–读响应–结束
  • 第二次为读ADC数据,具体为:开始–读寻址–读响应–[读ADC数据–写响应–]循环读

第二次的时序如下图:
通过上面的介绍大家应该对如何驱动PCF8591进行ADC采样有了整体的概念,还有一些细节就是I2C通信的时序明细,如下图


Verilog代码

// --------------------------------------------------------------------
// >>>>>>>>>>>>>>>>>>>>>>>>> COPYRIGHT NOTICE <<<<<<<<<<<<<<<<<<<<<<<<<
// --------------------------------------------------------------------
// Module: ADC_I2C
// 
// Author: Step
// 
// Description: ADC_I2C
// 
// Web: www.stepfpga.com
//
// --------------------------------------------------------------------
// Code Revision History :
// --------------------------------------------------------------------
// Version: |Mod. Date:   |Changes Made:
// V1.1     |2016/10/30   |Initial ver
// --------------------------------------------------------------------
module ADC_I2C
(
	input				clk_in,		//系统时钟
	input				rst_n_in,	//系统复位,低有效
	output				scl_out,	//I2C总线SCL
	inout				sda_out,	//I2C总线SDA
	output	reg			adc_done,	//ADC采样完成标志
	output	reg	[7:0]	adc_data	//ADC采样数据
);
 
	parameter	CNT_NUM	=	15;
 
	localparam	IDLE	=	3'd0;
	localparam	MAIN	=	3'd1;
	localparam	START	=	3'd2;
	localparam	WRITE	=	3'd3;
	localparam	READ	=	3'd4;
	localparam	STOP	=	3'd5;
 
	//根据PCF8591的datasheet,I2C的频率最高为100KHz,
	//我们准备使用4个节拍完成1bit数据的传输,所以需要400KHz的时钟触发完成该设计
	//使用计数器分频产生400KHz时钟信号clk_400khz
	reg					clk_400khz;
	reg		[9:0]		cnt_400khz;
	always@(posedge clk_in or negedge rst_n_in) begin
		if(!rst_n_in) begin
			cnt_400khz <= 10'd0;
			clk_400khz <= 1'b0;
		end else if(cnt_400khz >= CNT_NUM-1) begin
			cnt_400khz <= 10'd0;
			clk_400khz <= ~clk_400khz;
		end else begin
			cnt_400khz <= cnt_400khz + 1'b1;
		end
	end
 
	reg		[7:0]		adc_data_r;
	reg					scl_out_r;
	reg					sda_out_r;
	reg		[2:0]		cnt;
	reg		[3:0]		cnt_main;
	reg		[7:0]		data_wr;
	reg		[2:0]		cnt_start;
	reg		[2:0]		cnt_write;
	reg		[4:0]		cnt_read;
	reg		[2:0]		cnt_stop;
	reg		[2:0] 		state;
 
	always@(posedge clk_400khz or negedge rst_n_in) begin
		if(!rst_n_in) begin	//如果按键复位,将相关数据初始化
			scl_out_r <= 1'd1;
			sda_out_r <= 1'd1;
			cnt <= 1'b0;
			cnt_main <= 4'd0;
			cnt_start <= 3'd0;
			cnt_write <= 3'd0;
			cnt_read <= 5'd0;
			cnt_stop <= 1'd0;
			adc_done <= 1'b0;
			adc_data <= 1'b0;
			state <= IDLE;
		end else begin
			case(state)
				IDLE:begin	//软件自复位,主要用于程序跑飞后的处理
						scl_out_r <= 1'd1;
						sda_out_r <= 1'd1;
						cnt <= 1'b0;
						cnt_main <= 4'd0;
						cnt_start <= 3'd0;
						cnt_write <= 3'd0;
						cnt_read <= 5'd0;
						cnt_stop <= 1'd0;
						adc_done <= 1'b0;
						state <= MAIN;
					end
				MAIN:begin
						if(cnt_main >= 4'd6) cnt_main <= 4'd6;  //对MAIN中的子状态执行控制cnt_main
						else cnt_main <= cnt_main + 1'b1;
						case(cnt_main)
							4'd0:	begin state <= START; end	//I2C通信时序中的START
							4'd1:	begin data_wr <= 8'h90; state <= WRITE; end	//A0,A1,A2都接了GND,写地址为8'h90
							4'd2:	begin data_wr <= 8'h00; state <= WRITE; end	//control byte为8'h00,采用4通道ADC中的通道0
							4'd3:	begin state <= STOP; end	//I2C通信时序中的START
							4'd4:	begin state <= START; end	//I2C通信时序中的STOP
							4'd5:	begin data_wr <= 8'h91; state <= WRITE; end	//A0 A1 A2都接了GND,读地址为8'h91
							4'd6:	begin state <= READ; adc_done <= 1'b0; end	//读取ADC的采样数据
							4'd7:	begin state <= STOP; adc_done <= 1'b1; end	//I2C通信时序中的STOP,读取完成标志
							4'd8:	begin state <= MAIN; end	//预留状态,不执行
							default: state <= IDLE;	//如果程序失控,进入IDLE自复位状态
						endcase
					end
				START:begin	//I2C通信时序中的起始START
						if(cnt_start >= 3'd5) cnt_start <= 1'b0;	//对START中的子状态执行控制cnt_start
						else cnt_start <= cnt_start + 1'b1;
						case(cnt_start)
							3'd0:	begin sda_out_r <= 1'b1; scl_out_r <= 1'b1; end	//将SCL和SDA拉高,保持4.7us以上
							3'd1:	begin sda_out_r <= 1'b1; scl_out_r <= 1'b1; end	//clk_400khz每个周期2.5us,需要两个周期
							3'd2:	begin sda_out_r <= 1'b0; end	//SDA拉低到SCL拉低,保持4.0us以上
							3'd3:	begin sda_out_r <= 1'b0; end	//clk_400khz每个周期2.5us,需要两个周期
							3'd4:	begin scl_out_r <= 1'b0; end	//SCL拉低,保持4.7us以上
							3'd5:	begin scl_out_r <= 1'b0; state <= MAIN; end	//clk_400khz每个周期2.5us,需要两个周期,返回MAIN
							default: state <= IDLE;	//如果程序失控,进入IDLE自复位状态
						endcase
					end
				WRITE:begin	//I2C通信时序中的写操作WRITE和相应判断操作ACK
						if(cnt <= 3'd6) begin	//共需要发送8bit的数据,这里控制循环的次数
							if(cnt_write >= 3'd3) begin cnt_write <= 1'b0; cnt <= cnt + 1'b1; end
							else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
						end else begin
							if(cnt_write >= 3'd7) begin cnt_write <= 1'b0; cnt <= 1'b0; end	//两个变量都恢复初值
							else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
						end
						case(cnt_write)
							//按照I2C的时序传输数据
							3'd0:	begin scl_out_r <= 1'b0; sda_out_r <= data_wr[7-cnt]; end	//SCL拉低,并控制SDA输出对应的位
							3'd1:	begin scl_out_r <= 1'b1; end	//SCL拉高,保持4.0us以上
							3'd2:	begin scl_out_r <= 1'b1; end	//clk_400khz每个周期2.5us,需要两个周期
							3'd3:	begin scl_out_r <= 1'b0; end	//SCL拉低,准备发送下1bit的数据
							//获取从设备的响应信号并判断
							3'd4:	begin sda_out_r <= 1'bz; end	//释放SDA线,准备接收从设备的响应信号
							3'd5:	begin scl_out_r <= 1'b1; end	//SCL拉高,保持4.0us以上
							3'd6:	begin if(sda_out) state <= IDLE; else state <= state; end	//获取从设备的响应信号并判断
							3'd7:	begin scl_out_r <= 1'b0; state <= MAIN; end	//SCL拉低,返回MAIN状态
							default: state <= IDLE;	//如果程序失控,进入IDLE自复位状态
						endcase
					end
				READ:begin	//I2C通信时序中的读操作READ和返回ACK的操作
						if(cnt <= 3'd6) begin	//共需要接收8bit的数据,这里控制循环的次数
							if(cnt_read >= 3'd3) begin cnt_read <= 1'b0; cnt <= cnt + 1'b1; end
							else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
						end else begin
							if(cnt_read >= 3'd7) begin cnt_read <= 1'b0; cnt <= 1'b0; end	//两个变量都恢复初值
							else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
						end
						case(cnt_read)
							//按照I2C的时序接收数据
							3'd0:	begin scl_out_r <= 1'b0; sda_out_r <= 1'bz; end	//SCL拉低,释放SDA线,准备接收从设备数据
							3'd1:	begin scl_out_r <= 1'b1; end	//SCL拉高,保持4.0us以上
							3'd2:	begin adc_data_r[7-cnt] <= sda_out; end	//读取从设备返回的数据
							3'd3:	begin scl_out_r <= 1'b0; end	//SCL拉低,准备接收下1bit的数据
							//向从设备发送响应信号
							3'd4:	begin sda_out_r <= 1'b0; adc_done <= 1'b1; adc_data <= adc_data_r; end	//发送响应信号,将前面接收的数据锁存
							3'd5:	begin scl_out_r <= 1'b1; end	//SCL拉高,保持4.0us以上
							3'd6:	begin scl_out_r <= 1'b1; adc_done <= 1'b0; end	//SCL拉高,保持4.0us以上
							3'd7:	begin scl_out_r <= 1'b0; state <= MAIN; end	//SCL拉低,返回MAIN状态
							default: state <= IDLE;	//如果程序失控,进入IDLE自复位状态
						endcase
					end
				STOP:begin	//I2C通信时序中的结束STOP
						if(cnt_stop >= 3'd5) cnt_stop <= 1'b0;	//对STOP中的子状态执行控制cnt_stop
						else cnt_stop <= cnt_stop + 1'b1;
						case(cnt_stop)
							3'd0:	begin sda_out_r <= 1'b0; end	//SDA拉低,准备STOP
							3'd1:	begin sda_out_r <= 1'b0; end	//SDA拉低,准备STOP
							3'd2:	begin scl_out_r <= 1'b1; end	//SCL提前SDA拉高4.0us
							3'd3:	begin scl_out_r <= 1'b1; end	//SCL提前SDA拉高4.0us
							3'd4:	begin sda_out_r <= 1'b1; end	//SDA拉高
							3'd5:	begin sda_out_r <= 1'b1; state <= MAIN; end	//完成STOP操作,返回MAIN状态
							default: state <= IDLE;	//如果程序失控,进入IDLE自复位状态
						endcase
					end
				default:;
			endcase
		end
	end
 
	assign	scl_out = scl_out_r;	//对SCL端口赋值
	assign	sda_out = sda_out_r;	//对SDA端口赋值
 
endmodule

小结

本节主要为大家讲解了使用I2C驱动PCF8591的ADC功能的原理及软件设计,需要大家掌握的同时自己创建工程,通过整个设计流程,生成FPGA配置文件加载测试。
如果你对Diamond软件的使用不了解,请参考这里:Diamond的使用


相关资料

使用STEP-MXO2第二代的PCF8591的ADC驱动程序: 后续会有下载连接 待更新
使用STEP-MAX10的PCF8591的ADC驱动程序: 后续会有下载连接 待更新