基于FPGA的MCP4725驱动程序
- 芯片资料
MCP4725是低功耗、高精度、单通道的12位缓冲电压输出数模转换器(Digital-to-Analog Convertor,DAC),具有非易失性存储器(EEPROM)。用户可以使用I2C接口命令将DAC输入和配置数据烧写到非易失性存储器(EEPROM)。非易失性存储器功能使得DAC器件在断电期间仍能保持DAC输入代码,且DAC输出在上电后立即可用。
图1.MCP4725功能框图
MCP4725具有外部A0地址位选择引脚。此A0引脚可连接用户应用电路板的VDD或VSS。MCP4725具有2线型IIC兼容串行接口,可用于标准(100 kHz)、快速(400 kHz)或高速(3.4 MHz)模式。
Vout:模拟输出电压;
Vss:参考地;
VDD:电源电压;3.7~5.5V
SDA:IIC串行数据;
SCL:IIC串行时钟输入
A0:地址位选择引脚;该引脚可连接到VSS或VDD ,或由数字逻辑电平有效驱动。该引脚的逻辑状态决定了I2 C地址位的A0位。
2. 输出电压计算
例如当我们输入0x400,即十进制数1024,电源电压接入为5V,那么输出电压Vout=5*1024/4096=1.25V。
3. 工作原理
当器件连接到I2C总线时,器件作为从器件工作。使用I2C接口命令,主器件可以读/写DAC输入寄存器或EEPROM。MCP4725器件地址包含4个固定位(1100 =器件代码)和3个地址位(A2、A1和A0)。A2和A1位是在出厂前硬连线的,而A0位由A0引脚的逻辑状态决定。A0引脚可连接到VDD或VSS,或由数字逻辑电平有效驱动。写命令用于将配置位和DAC输入代码装载到DAC寄存器,或写入器件的EEPROM。通过使用3个写命令类型位(C2、C1和C0)定义写命令类型。
当C2=0,C1=0 时,为快速模式,此命令用于更改DAC寄存器,EEPROM不受影响;当C2=0,C1=1,C0=0 时,为写DAC寄存器模式,即将配置位和数据代码装载到DAC寄存器;当C2=0,C1=1,C0=1 时,为写DAC寄存器和更新EEPROM模式,将配置位和数据代码装载到DAC寄存器并且写入EEPROM中。本次主要使用写DAC寄存器模式和写DAC寄存器和更新EEPROM模式,如下图所示。
第一个字节为器件寻址,A2和A1已经被厂家设置为0,A0由自己控制(默认为0,即接地),因此第一个字节为0x60;第二个字节为写数据地址,PD0和PD1都为0时为正常模式,因此第二个字节为0x60;第三个字节和第四个字节的高4位组成12位数据输入,由我们自己定义输入。
4. IIC串行通信
MCP4725器件使用2线IIC串行接口,该接口可在标准、快速或高速模式下工作。在总线上发送数据的器件定义为发送器,而接收数据的器件定义为接收器。总线必须由主器件控制,主器件产生串行时(SCL)信号、控制总线访问权并产生启动条件和停止条件。MCP4725器件作为从器件工作。主器件和从器件都可以作为发送器或接收器工作,但是由主器件决定激活哪种模式。通信由主器件(单片机)发起,它发送启动位,随后是从地址字节。发送的第一个字节始终为从地址字节,它包含器件代码、地址位和R/W位。MCP4725器件的器件代码为1100。当器件接收到读命令(R/W = 1)时,发送DAC输入寄存器和EEPROM的内容。下图给出了IIC通信时序要求。
在本次设计中,SCL时钟输入频率选择为250KHz,FPGA工作时钟为50MHz,上电等待20ms后开始IIC数据写入。采用模块化设计,分为IIC驱动设计,MCP4725初始化设计,顶层模块。
5. 代码模块
5.1 IIC驱动模块
module i2c_dri
#(// slave address(器件地址)
parameter SLAVE_ADDR = 7'b1100000 ,
parameter CLK_FREQ = 26'd50_000_000, // 时钟频率(CLK_FREQ)
parameter I2C_FREQ = 18'd250_000 // I2C的SCL时钟频率
)(
//global clock
input clk , // 时钟
input rst_n , // 复位信号
//i2c interface
input i2c_exec , // I2C触发执行信号
input bit_ctrl , // 字地址位控制(16b/8b)
input i2c_rh_wl , // I2C读写控制信号
input [15:0] i2c_addr , // I2C器件内地址
input [15:0] i2c_data_w , // I2C要写的数据
output reg [ 7:0] i2c_data_r , // I2C读出的数据
output reg i2c_done , // I2C一次操作完成
output reg scl , // I2C的SCL时钟信号
inout sda , // I2C的SDA信号
//user interface
output reg dri_clk // 驱动I2C操作的驱动时钟
);
//localparam define
localparam st_idle = 8'b0000_0001; // 空闲状态
localparam st_sladdr = 8'b0000_0010; // 发送器件地址(slave address)
localparam st_addr16 = 8'b0000_0100; // 发送16位字地址
localparam st_addr8 = 8'b0000_1000; // 发送8位字地址
localparam st_data_wr = 8'b0001_0000; // 写数据(8 bit)
localparam st_addr_rd = 8'b0010_0000; // 发送器件地址读
localparam st_data_rd = 8'b0100_0000; // 读数据(8 bit)
localparam st_stop = 8'b1000_0000; // 结束I2C操作
//reg define
reg sda_dir ; // I2C数据(SDA)方向控制
reg sda_out ; // SDA输出信号
reg st_done ; // 状态结束
reg wr_flag ; // 写标志
reg [ 6:0] cnt ; // 计数
reg [ 7:0] cur_state ; // 状态机当前状态
reg [ 7:0] next_state ; // 状态机下一状态
reg [15:0] addr_t ; // 地址
reg [ 7:0] data_r ; // 读取的数据
reg [15:0] data_wr_t ; // I2C需写的数据的临时寄存
reg [ 9:0] clk_cnt ; // 分频时钟计数
//wire define
wire sda_in ; // SDA输入信号
wire [8:0] clk_divide ; // 模块驱动时钟的分频系数
//SDA控制
assign sda = sda_dir ? sda_out : 1'bz; // SDA数据输出或高阻
assign sda_in = sda ; // SDA数据输入
assign clk_divide = (CLK_FREQ/I2C_FREQ) >> 3; // 模块驱动时钟的分频系数
//生成I2C的SCL的四倍频率的驱动时钟用于驱动i2c的操作
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
dri_clk <= 1'b1;
clk_cnt <= 10'd0;
end
else if(clk_cnt == clk_divide - 1'd1) begin
clk_cnt <= 10'd0;
dri_clk <= ~dri_clk;
end
else
clk_cnt <= clk_cnt + 1'b1;
end
//(三段式状态机)同步时序描述状态转移
always @(posedge dri_clk or negedge rst_n) begin
if(!rst_n)
cur_state <= st_idle;
else
cur_state <= next_state;
end
//组合逻辑判断状态转移条件
always @( * ) begin
// next_state = st_idle;
case(cur_state)
st_idle: begin // 空闲状态
if(i2c_exec) begin
next_state = st_sladdr;
end
else
next_state = st_idle;
end
st_sladdr: begin
if(st_done) begin
if(bit_ctrl) // 判断是16位还是8位字地址
next_state = st_addr16;
else
next_state = st_addr8 ;
end
else
next_state = st_sladdr;
end
st_addr16: begin // 写16位字地址
if(st_done) begin
next_state = st_addr8;
end
else begin
next_state = st_addr16;
end
end
st_addr8: begin // 8位字地址
if(st_done) begin
if(wr_flag==1'b0) // 读写判断
next_state = st_data_wr;
else
next_state = st_addr_rd;
end
else begin
next_state = st_addr8;
end
end
st_data_wr: begin // 写数据(8 bit)
if(st_done)
next_state = st_stop;
else
next_state = st_data_wr;
end
st_addr_rd: begin // 写地址以进行读数据
if(st_done) begin
next_state = st_data_rd;
end
else begin
next_state = st_addr_rd;
end
end
st_data_rd: begin // 读取数据(8 bit)
if(st_done)
next_state = st_stop;
else
next_state = st_data_rd;
end
st_stop: begin // 结束I2C操作
if(st_done)
next_state = st_idle;
else
next_state = st_stop ;
end
default: next_state= st_idle;
endcase
end
//时序电路描述状态输出
always @(posedge dri_clk or negedge rst_n) begin
//复位初始化
if(!rst_n) begin
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b1;
i2c_done <= 1'b0;
cnt <= 1'b0;
st_done <= 1'b0;
data_r <= 1'b0;
i2c_data_r <= 1'b0;
wr_flag <= 1'b0;
addr_t <= 1'b0;
data_wr_t <= 1'b0;
end
else begin
st_done <= 1'b0 ;
cnt <= cnt +1'b1 ;
case(cur_state)
st_idle: begin // 空闲状态
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b1;
i2c_done<= 1'b0;
cnt <= 7'b0;
if(i2c_exec) begin
wr_flag <= i2c_rh_wl ;
addr_t <= i2c_addr ;
data_wr_t <= i2c_data_w;
end
end
st_sladdr: begin // 写地址(器件地址和字地址)
case(cnt)
7'd1 : sda_out <= 1'b0; // 开始I2C
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= SLAVE_ADDR[6]; // 传送器件地址
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= SLAVE_ADDR[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= SLAVE_ADDR[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= SLAVE_ADDR[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= SLAVE_ADDR[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= SLAVE_ADDR[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= SLAVE_ADDR[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: sda_out <= 1'b0; // 0:写
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: begin
sda_dir <= 1'b0; // 从机应答
sda_out <= 1'b1;
end
7'd37: scl <= 1'b1;
7'd38: st_done <= 1'b1;
7'd39: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_addr16: begin
case(cnt)
7'd0 : begin
sda_dir <= 1'b1 ;
sda_out <= addr_t[15]; // 传送字地址
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= addr_t[14];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= addr_t[13];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= addr_t[12];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= addr_t[11];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= addr_t[10];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= addr_t[9];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= addr_t[8];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0; // 从机应答
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: st_done <= 1'b1;
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_addr8: begin
case(cnt)
7'd0: begin
sda_dir <= 1'b1 ;
sda_out <= addr_t[7]; // 字地址
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= addr_t[6];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= addr_t[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= addr_t[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= addr_t[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= addr_t[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= addr_t[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= addr_t[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0; // 从机应答
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: st_done <= 1'b1;
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_data_wr: begin // 写数据(12 bit)
case(cnt)
7'd0: begin
sda_out <= data_wr_t[15]; // I2C写2次8位数据
sda_dir <= 1'b1;
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= data_wr_t[14];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= data_wr_t[13];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= data_wr_t[12];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= data_wr_t[11];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= data_wr_t[10];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= data_wr_t[9];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= data_wr_t[8];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0; // 从机应答
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: sda_out <= data_wr_t[7];
7'd37: scl <= 1'b1;
7'd39: scl <= 1'b0;
7'd40: sda_out <= data_wr_t[6];
7'd41: scl <= 1'b1;
7'd43: scl <= 1'b0;
7'd44: sda_out <= data_wr_t[5];
7'd45: scl <= 1'b1;
7'd47: scl <= 1'b0;
7'd48: sda_out <= data_wr_t[4];
7'd49: scl <= 1'b1;
7'd51: scl <= 1'b0;
7'd52: sda_out <= data_wr_t[3];
7'd53: scl <= 1'b1;
7'd55: scl <= 1'b0;
7'd56: sda_out <= data_wr_t[2];
7'd57: scl <= 1'b1;
7'd59: scl <= 1'b0;
7'd60: sda_out <= data_wr_t[1];
7'd61: scl <= 1'b1;
7'd63: scl <= 1'b0;
7'd64: sda_out <= data_wr_t[0];
7'd65: scl <= 1'b1;
7'd67: scl <= 1'b0;
7'd68: begin
sda_dir <= 1'b0; // 从机应答
sda_out <= 1'b1;
end
7'd69: scl <= 1'b1;
7'd70: st_done <= 1'b1;
7'd71: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_addr_rd: begin // 写地址以进行读数据
case(cnt)
7'd0 : begin
sda_dir <= 1'b1;
sda_out <= 1'b1;
end
7'd1 : scl <= 1'b1;
7'd2 : sda_out <= 1'b0; // 重新开始
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= SLAVE_ADDR[6]; // 传送器件地址
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= SLAVE_ADDR[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= SLAVE_ADDR[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= SLAVE_ADDR[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= SLAVE_ADDR[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= SLAVE_ADDR[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= SLAVE_ADDR[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: sda_out <= 1'b1; // 1:读
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: begin
sda_dir <= 1'b0; // 从机应答
sda_out <= 1'b1;
end
7'd37: scl <= 1'b1;
7'd38: st_done <= 1'b1;
7'd39: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_data_rd: begin // 读取数据(8 bit)
case(cnt)
7'd0: sda_dir <= 1'b0;
7'd1: begin
data_r[7] <= sda_in;
scl <= 1'b1;
end
7'd3: scl <= 1'b0;
7'd5: begin
data_r[6] <= sda_in ;
scl <= 1'b1 ;
end
7'd7: scl <= 1'b0;
7'd9: begin
data_r[5] <= sda_in;
scl <= 1'b1 ;
end
7'd11: scl <= 1'b0;
7'd13: begin
data_r[4] <= sda_in;
scl <= 1'b1 ;
end
7'd15: scl <= 1'b0;
7'd17: begin
data_r[3] <= sda_in;
scl <= 1'b1 ;
end
7'd19: scl <= 1'b0;
7'd21: begin
data_r[2] <= sda_in;
scl <= 1'b1 ;
end
7'd23: scl <= 1'b0;
7'd25: begin
data_r[1] <= sda_in;
scl <= 1'b1 ;
end
7'd27: scl <= 1'b0;
7'd29: begin
data_r[0] <= sda_in;
scl <= 1'b1 ;
end
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b1; // 非应答
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: st_done <= 1'b1;
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
i2c_data_r <= data_r;
end
default : ;
endcase
end
st_stop: begin // 结束I2C操作
case(cnt)
7'd0: begin
sda_dir <= 1'b1; // 结束I2C
sda_out <= 1'b0;
end
7'd1 : scl <= 1'b1;
7'd3 : sda_out <= 1'b1;
7'd15: st_done <= 1'b1;
7'd16: begin
cnt <= 1'b0;
i2c_done <= 1'b1; // 向上层模块传递I2C结束信号
end
default : ;
endcase
end
endcase
end
end
endmodule
5.2 MCP4725初始化模块
module MCP4725_init(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
input i2c_done , //I2C寄存器配置完成信号
output reg i2c_exec , //I2C触发执行信号
output reg [23:0] i2c_data //I2C要配置的地址与数据(高16位地址,低8位数据)
);
//reg define
reg [14:0] start_init_cnt; //等待延时计数器
//scl配置成250khz,输入的clk为1Mhz,周期为1us,20000*1us = 20ms
//上电到开始配置IIC至少等待20ms
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
start_init_cnt <= 15'd0;
else if(start_init_cnt < 15'd20000)
start_init_cnt <= start_init_cnt + 1'b1;
end
//i2c触发执行信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
i2c_exec <= 1'b0;
else if(start_init_cnt == 15'd19999)
i2c_exec <= 1'b1;
else
i2c_exec <= 1'b0;
end
//配置寄存器地址与数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
i2c_data <= 16'd0;
else
i2c_data <= { 8'h60,8'h40,8'h00} ;//第一个字节为模式控制(60:同步更新到EEPROM;40:只更新DAC寄存器),后面两个字节为输入数据,取高12位
end
endmodule
5.3 顶层模块
module MCP4725_CTRL(
input clk,
input rst_n,
output scl,
inout sda
);
wire i2c_exec ; //I2C触发执行信号
wire [23:0] i2c_data ; //I2C要配置的地址与数据(高8位地址,低16位数据)
wire i2c_done ; //I2C寄存器配置完成信号
wire i2c_dri_clk ; //I2C操作时钟
parameter SLAVE_ADDR = 7'h60 ; //MCP4725的器件地址7'h60
parameter BIT_CTRL = 1'b0 ; //字节地址为8位 0:8位 1:16位
parameter CLK_FREQ = 26'd50_000_000; //时钟频率 50MHz
parameter I2C_FREQ = 18'd250_000 ; //I2C的SCL时钟频率,250KHz
i2c_dri
#(
.SLAVE_ADDR (SLAVE_ADDR), //参数传递
.CLK_FREQ (CLK_FREQ ),
.I2C_FREQ (I2C_FREQ )
)
u_i2c_dri(
.clk (clk ),
.rst_n (rst_n ),
.i2c_exec (i2c_exec ),
.bit_ctrl (BIT_CTRL ),
.i2c_rh_wl (1'b0), //固定为0,只用到了IIC驱动的写操作
.i2c_addr (i2c_data[23:16]),
.i2c_data_w (i2c_data[15:0]),
.i2c_data_r (),
.i2c_done (i2c_done ),
.scl (scl ),
.sda (sda ),
.dri_clk (i2c_dri_clk) //I2C操作时钟
);
MCP4725_init u_MCP4725_init(
.clk (i2c_dri_clk), //时钟信号
.rst_n (rst_n), //复位信号,低电平有效
.i2c_done(i2c_done), //I2C寄存器配置完成信号
.i2c_exec(i2c_exec), //I2C触发执行信号
.i2c_data(i2c_data) //I2C要配置的地址与数据(高8位地址,低16位数据)
);
endmodule
PS:IIC驱动程序是我在以前写的IIC单字节读写程序的基础上修改而来,从而实现两个字节的写入(这里可以再增加实现多个字节的写入,但要注意cnt的位宽)。MCP4725初始化程序中最后的 i2c_data <= {8’h60,8’h40,8’h00} ; 这一行大家可以根据自己的实际需求做更改,详细更改的内容自行参考芯片的数据手册。
6.验证
将工程编译好后的sof文件下载到EP4CE6F17C8器件中,连接好MCP4725各个引脚,电源接5V,输入的数据为1024,为4096的四分之一,因此输出电压应该为1.25V,利用万用表测量数据为1.25V,将模块断电再重新上电,再次测量也为1.25V,证明数据成功写入EEPROM中,验证成功。