I2C总线通讯协议中主机模块的FPGA实现

来源:互联网 发布:牯岭镇 知乎 编辑:程序博客网 时间:2024/04/26 12:40

本人FPGA小白,对FPGA比较感兴趣,前段时间跟某位同学讨论I2C总线通讯协议,我以前写过关于串口和SPI的通讯协议,还没有接触过I2C总线通讯协议。这次就抱着试试看心态,去了解了下I2C总线通讯协议。
结果就是,I2C通讯的复杂程度远超串口和SPI,我查找了一些关于I2C总线通讯的资料内容,比较不开心的是,各类资料对于I2C总线通讯协议的描述不尽相同,我选了其中的两篇我能接受的资料作为这次博客内容的基础,一篇是对I2C总线通讯的基本描述,一篇是其中关于协议通讯中ACK过程的描述,在接下来的内容中,会用到两篇资料中的内容,以下是两篇资料的链接:

 1.I2C总线通讯协议及其原理

 2.对I2C总线时序的一点理解以及ACK和NACK(NAK)

I2C总线通讯协议概述:
1.信号线描述
通讯协议为串行通讯协议,总共用到两根双向信号线,为SDA与SCL,其中SDA为数据线,SCL为时钟线;总线上通过上拉电阻接正电源,当总线空闲时,两根信号线均为高电平,连接到总线上的任一设备输出低电平时都会使总线拉低,表面两根信号线均为线“与”逻辑。
SCL为时钟线,为OD门,当上升沿时将数据输入到EEPROM中,下降沿时驱动EEPROM输出数据。
SDA也为OD门,输出与其他OD门或者OC门构成线与逻辑。
2.主从设备区分
协议区分主设备与从设备,每个从设备拥有对应于自己的7位地址码,前4位为器件类型,由厂家决定,而后三位则由用户自己定义。同一时间,主设备只能与一个从设备通讯,从设备挂载在总线上的数量由地址码位数以及总线最大电容400pf限制。
主设备在通讯中主要承担提供SCL时钟,控制信息读写流向,决定通讯的开始与结束的任务。
从设备则是提供和接收信息并于主机进行交互。
3.协议的简要内容
3.1 开始和结束信号
协议有自己的通讯帧,每帧都有开始信号与结束信号
开始信号:当SCL为高电平期间,将SDA信号从高电平拉低,则构成开始信号,此时总线将由空闲状态转为被占用状态,各从机将准备好从主机接收数据。
结束信号:当SCL为高电平期间,将SDA信号从低电平拉高,则构成结束信号,此时总线将被释放,从占用状态变为空闲状态,通讯结束。
在通讯过程中,SCL为高电平时,SDA均不允许发生变化,否则将会被视作开始或者结束信号,导致通讯出错。













3.2 ACK状态
ACK状态是I2C区别串口和SPI的一个很大的地方,当发送方发送8位数据后,在第9个SCL时钟上升沿之前需要释放总线(在我写的程序中,我选择在第9个时钟的下降沿),从接收方接收一个来自SDA的信号,该信号在第9个SCL时钟的高电平期间应该保持不变,对于接收方而言,若成功接收这8位数据,在总线释放期间,将SDA线拉低,表示ACK;若无法接收这8位数据,在总线释放期间,需要将SDA线拉高,表示NACK,通知发送方结束本次发送。
如果接收方是主设备,则在接收到从设备发来的最后一位数据后,在结束信号前,需要发送一个NACK状态,以通知从设备发送方释放总线,使主设备可以发送结束信号结束本次通讯。

3.3 I2C总线读写部分

I2C总线的读写过程有相同的部分也有各自区别,对于单个8位数据读写过程而言,从主机角度看,写过程需要写入3个8位数据,分别为从设备地址与写标志(7位从地址,写标志),从设备子寄存器地址(有的设备可能不需要),写入数据;而读过程需要写3个8位数据,读一个8位数据,分别为写从设备地址与写标志,写从设备子寄存器地址,写从设备地址与读标志,读出子寄存器数据。
3.3.1主设备向从设备写
如前述,主设备发送开始信号,接下来发送7位从设备地址和写标志信号(低电平),与之匹配的从机继续通讯过程,之后主机向匹配到的从机发送要写入的子寄存器地址,随后传送写入的数据,最后发送结束信号结束本次通讯。
更加具体的过程可以参看我给的链接一中的资料,这里直接上图:













3.3.2 主设备从从设备读

读过程第一次接触的时候感觉非常的怪异,就我个人的习惯看,我觉得应该只需要改变写过程中的读写标志信号和最后一个字节的信号流向就可以改变读写过程,可事实却不是这样,读过程远比写过程复杂的多。

读过程大致过程如下:

1)发送开始信号

2)发送7位从机地址和写标志信号,接收ACK信号

3)发送8位从机子寄存器地址,接收ACK信号

4)重启开始信号

5)发送7位从机地址和读标志信号,接收ACK信号

6)读出SDA上传过来的从机子寄存器数据,发送NACK信号

7)发送结束信号



4.主机模块FPGA实现与仿真
I2C通讯协议可以有很多种实现方法,可以对硬件的I2C电路控制编程实现,也可以用模拟GPIO的时序方法实现,在这里我用的就是后者,毕竟FPGA直接写接口就是模拟GPIO时序的方式。

软件版本是ISE14.7,仿真工具用的Moesim_10.1c,主要内容包括:

I2C_Master.v-----------I2C通信协议的通用主机读写模块

SI5338_Init.v------------通过I2C通信的方式初始化时钟芯片SI5338,完成将输入25MHz时钟倍频为50MHz的任务

I2C_Master_tb.v-------I2C_Master.v的testbench


4.1. I2C通信协议通用主机读写模块
模块实例:
       

管脚说明:
I_Clk_in---------------输入时钟,默认为50MHz,其他值需要修改内部参数
I_Rst_n---------------复位端,低电平有效
I_Start-----------------使能端,置高电平则模块正常运行
I_R_W_SET---------读写控制端信号,当为1时为写过程,为0时为读过程
I_Slave_Addr[6:0]--7位从机地址
I_R_W_Data[15:0]--读写控制字,其中高八位为从机子寄存器地址,低八位为写入寄存器的数据;读过程时,低八位置入何数据,不影响运行
O_SCL-----------------协议中的SCL端,由于不考虑从机无法接收数据而主动拉低SCL的情况,这里端口定义为输出,正常考虑全部可能情况时,定义为双向端口
IO_SDA----------------协议中的SDA端,定义为双向端口,不考虑从机ACK时,可以定义为输出
O_Done----------------通讯过程结束指示端,当一次读写完成时,会在端口输出一个Clk的高脉冲
O_Error----------------通讯错误指示信号,当主从机通讯出错时,该端口电平会拉高,直到下一次通讯开始。
O_Data[7:0]-----------读过程完成后,可以从该端口得到读出的8位数据

具体的实现代码如下:
`timescale 1ns / 1ps//////////////////////////////////////////////////////////////////////////////////// Company: // Engineer: // // Create Date:    22:40:45 11/20/2017 // Design Name: // Module Name:    I2C_Master // Project Name: // Target Devices: // Tool versions: // Description:                /*       I2C总线通信协议通用模块:SCL SDA       开始信号:SCL高时,SDA拉低       结束信号:SCL高时,SDA拉高       SDA数据在SCL低电平时置位       模块中实际默认开始信号与结束信号在SCL高电平中间产生       SDA数据位改变在SCL低电平的中间产生       SCL时钟频率为200kHz       从机地址可调,模块既支持读也支持写,通过输入管脚控制*/// // Dependencies: //// Revision: // Revision 0.01 - File Created// Additional Comments: ////////////////////////////////////////////////////////////////////////////////////module I2C_Master(
//I2C
I_Clk_in,
I_Rst_n,O_SCL,IO_SDA,//control_sigI_Start,   //一次读/写操作开始信号O_Done,    //一次读/写操作结束信号I_R_W_SET, //读写控制信号,写为1,读为0I_Slave_Addr,//从机地址I_R_W_Data,//读写控制字16位I_R_W_Data[15:8]->reg_addr,I_R_W_Data[7:0]->W_data,读状态则可默认为7'b0            O_Data,    //读到的数据,当O_Done拉高时数据有效O_Error  //检测传输错误信号,当出现从机未响应,从机不能接收数据等情况时,拉高电平 );//I/OinputI_Clk_in;inputI_Rst_n;outputO_SCL;inoutIO_SDA;inputI_Start;outputO_Done;input  [6:0] I_Slave_Addr;inputI_R_W_SET;input  [15:0]I_R_W_Data;output [7:0] O_Data;output      O_Error;/******时钟定位模块(测试时时钟为50MHz),定位SCL的高电平中心,与SCL的低电平中心,产生100kHz的SCL*******/parameter   Start_Delay=9'd60;//开始时SDA拉低电平持续的时间,共用计数器下应小于SCL_HIGH2LOW-1parameter   Stop_Delay=9'd150;//一次读/写结束后SDA拉高电平的时间,共用计数器下应小于SCL_HIGH2LOW-1parameter   SCL_Period=9'd499;//测试板时钟为50MHz,100KHz为500个Clkparameter   SCL_LOW_Dest=9'd374;//时钟判定高电平在前,低电平在后,低电平中央为3/4个周期,375个Clkparameter   SCL_HIGH2LOW=9'd249;//电平翻转位置,1/2个SCL周期,250个Clkparameter   ACK_Dect=9'd124;     //SCL高电平中间位置,用于检测ACK信号reg    [8:0]R_SCL_Cnt;reg         R_SCL_En;assign      O_SCL=(R_SCL_Cnt<=SCL_HIGH2LOW)?1'b1:1'b0;//SCL 时钟输出always @ (posedge I_Clk_in or negedge I_Rst_n)beginif (~I_Rst_n) begin  R_SCL_Cnt<=9'b0; endelse begin  if (R_SCL_En)   if (R_SCL_Cnt==SCL_Period)    R_SCL_Cnt<=9'b0;   else    R_SCL_Cnt<=R_SCL_Cnt+9'b1;  else   R_SCL_Cnt<=9'b0; endend/******SDA读写控制模块******/reg [5:0]    R_State;reg          R_SDA_I_O_SET;//SDA双向选择I/O口 1为输出,0为输入reg          R_SDA_t;      //SDA的输出端口reg          O_Done;       //结束信号reg [7:0]    O_Data;       //读到的数据reg          O_Error;//传输错误指示信号/****状态定义*****/parameter    Start=6'd0;  //一次读写开始的状态parameter    ReStart=6'd34; //读操作入口状态parameter    Stop=6'd56;    //发送停止位状态always @ (posedge I_Clk_in or negedge I_Rst_n)beginif (~I_Rst_n) begin R_SCL_En<=1'b0;     //计数时钟停止 R_State<=6'd0; R_SDA_I_O_SET<=1'b1;//默认设置为输出管脚 R_SDA_t<=1'b1;      //SDA输出默认拉高 O_Data<=8'b0; O_Done<=1'b0; O_Error<=1'b0; endelse begin  if (I_Start) //当开始信号置高时表示I2C通信开始   begincase(R_State) Start:   //启动位   begin   R_SCL_En<=1'b1;   O_Error<=1'b0;//每次重新下一次传输时,清除错误标志位   if (R_SCL_Cnt==Start_Delay)     begin      R_SDA_t<=1'b0; //SCL高电平时拉低      R_State<=R_State+6'd1;     end   else     begin      R_SDA_t<=1'b1;      R_State<=R_State;     end    end  6'd1,6'd2,6'd3,6'd4,6'd5,6'd6,6'd7:  //写入7位从机地址    begin      if (R_SCL_Cnt==SCL_LOW_Dest)begin         R_SDA_t<=I_Slave_Addr[6'd7-R_State];//从MSB-LSB写入输入端从机地址 R_State<=R_State+6'd1;end      else                                 R_State<=R_State;     end  6'd8: //写入写标志(0)    begin     if (R_SCL_Cnt==SCL_LOW_Dest)      beginR_SDA_t<=1'b0;R_State<=R_State+6'd1;      end    else                               R_State<=R_State;     end  6'd9: //ACK状态     begin     if (R_SCL_Cnt==SCL_HIGH2LOW) //在第8个时钟的下降沿释放SDA       beginR_SDA_I_O_SET<=1'b0;R_State<=R_State+6'd1;       end     elseR_State<=R_State;    end  6'd10: //在第9个时钟高电平中心检测ACK信号是否为0,如果为1,则表示从机未应答,进入结束位     begin       if (R_SCL_Cnt==ACK_Dect) begin  O_Error<=IO_SDA;  //检测从机是否响应  R_State<=R_State+6'd1; end       else  R_State<=R_State;      end  6'd11:     begin       if (R_SCL_Cnt==SCL_HIGH2LOW) //在第9个时钟的下降沿重新占用SDA,准备发送从机子寄存器地址 begin   R_SDA_I_O_SET<=1'b1;   R_State<=(O_Error)?Stop:(R_State+6'd1);   R_SDA_t<=1'b0; endelse   R_State<=R_State;        end  6'd12,6'd13,6'd14,6'd15,6'd16,6'd17,6'd18,6'd19:  //写入8位寄存器地址     begin      if (R_SCL_Cnt==SCL_LOW_Dest)begin R_SDA_t<=I_R_W_Data[6'd27-R_State];//从MSB-LSB写入寄存器地址 I_R_W_Data[15:8] R_State<=R_State+6'd1;end      else                                 R_State<=R_State;       end    6'd20: //ACK状态       begin       if (R_SCL_Cnt==SCL_HIGH2LOW)//在第8个时钟的下降沿释放SDA begin  R_SDA_I_O_SET<=1'b0;  R_State<=R_State+6'd1; end       else R_State<=R_State;     end   6'd21: //检测ACK     beginif (R_SCL_Cnt==ACK_Dect)  begin   O_Error<=IO_SDA;//检测从机是否响应   R_State<=R_State+6'd1;  endelse  R_State<=R_State;      end   6'd22:       begin       if (R_SCL_Cnt==SCL_HIGH2LOW) //在第9个时钟的下降沿重新占用SDA,区分接下来该发送数据还是读数据         begin  R_SDA_I_O_SET<=1'b1;  R_State<=(O_Error)?Stop:((I_R_W_SET)?(R_State+6'd1):ReStart); //从机状态  R_SDA_t<=(O_Error|I_R_W_SET)?1'b0:1'b1; //此处拉高SDA信号是为读状态重启开始信号做准备 end       else  R_State<=R_State; end    6'd23,6'd24,6'd25,6'd26,6'd27,6'd28,6'd29,6'd30://写入8位数据地址        beginif (R_SCL_Cnt==SCL_LOW_Dest)  begin   R_SDA_t<=I_R_W_Data[6'd30-R_State];//从MSB-LSB写入8位数据地址   R_State<=R_State+6'd1;  endelse                                   R_State<=R_State;                                end     6'd31: //ACK状态        begin if (R_SCL_Cnt==SCL_HIGH2LOW)//在第8个时钟的下降沿释放SDA   begin    R_SDA_I_O_SET<=1'b0;    R_State<=R_State+6'd1;   end  else            R_State<=R_State; end      6'd32://检测ACK begin   if (R_SCL_Cnt==ACK_Dect)    begin     O_Error<=IO_SDA;//检测从机是否响应     R_State<=R_State+6'd1;    end    else     R_State<=R_State;   end        6'd33: begin   if (R_SCL_Cnt==SCL_HIGH2LOW)//在第9个时钟的下降沿重新占用SDA,准备发送停止位     begin      R_SDA_I_O_SET<=1'b1;      R_SDA_t<=1'b0;//先拉低SDA信号      R_State<=Stop;//跳转到结束位发送状态     end    else      R_State<=R_State;   end       ReStart://主机读状态入口 初始时需要重启开始状态 begin  if (R_SCL_Cnt==Start_Delay)   begin    R_SDA_t<=1'b0; //SCL高电平时拉低    R_State<=R_State+6'd1;   end  else   begin    R_SDA_t<=1'b1;            R_State<=R_State;           end   end                               6'd35,6'd36,6'd37,6'd38,6'd39,6'd40,6'd41://发送从机7位地址                                 begin          if (R_SCL_Cnt==SCL_LOW_Dest)    begin     R_SDA_t<=I_Slave_Addr[6'd41-R_State];//从MSB-LSB写入输入端从机地址     R_State<=R_State+6'd1;    end   else                                     R_State<=R_State; end        6'd42://写入读标志(1) begin   if (R_SCL_Cnt==SCL_LOW_Dest)     begin      R_SDA_t<=1'b1;//写入读地址标志      R_State<=R_State+6'd1;     end   else                                      R_State<=R_State;   end6'd43: //ACK状态   begin     if (R_SCL_Cnt==SCL_HIGH2LOW)//在第8个时钟的下降沿释放SDA       begin        R_SDA_I_O_SET<=1'b0;R_State<=R_State+6'd1;       end     else        R_State<=R_State;             end        6'd44://ACK检测   begin     if (R_SCL_Cnt==ACK_Dect)        begin O_Error<=IO_SDA; R_State<=R_State+6'd1;        end      else R_State<=R_State;    end6'd45://之后需要一直读取数据,所以SDA总线这里需要保持输入状态    begin      if (R_SCL_Cnt==SCL_HIGH2LOW)//在第9个时钟下降沿保持SDA总线的释放状态begin R_SDA_I_O_SET<=(O_Error)?1'b1:1'b0;//若前次ACK检测通过,则保持SDA总线释放状态,不                                                                                通过则占用SDA总线用来发送停止位 R_State<=(O_Error)?Stop:(R_State+6'd1); R_SDA_t<=1'b0; end       else R_State<=R_State;     end6'd46,6'd47,6'd48,6'd49,6'd50,6'd51,6'd52,6'd53://8个时钟信号高电平中间依次从SDA上读取数据     begin       if (R_SCL_Cnt==ACK_Dect) begin  O_Data<={O_Data[6:0],IO_SDA};//从MSB开始读入数据  R_State<=R_State+6'd1; end       else  R_State<=R_State;      end6'd54://读入8位数据后,主机需要向外发送一个NACK信号    begin       if (R_SCL_Cnt==SCL_HIGH2LOW) begin  R_SDA_I_O_SET<=1'b1;//主机重新占用SDA  R_SDA_t<=1'b1;  R_State<=R_State+6'd1; end       else  R_State<=R_State;     end6'd55://在第9个时钟下降沿持续占用总线,拉低SDA,开始发送结束位    begin       if (R_SCL_Cnt==SCL_HIGH2LOW) begin  R_SDA_t<=1'b0;  R_State<=R_State+6'd1; end       else R_State<=R_State;    endStop: //发送停止位    begin       if (R_SCL_Cnt==Stop_Delay) begin  R_SDA_t<=1'b1;  R_State<=R_State+6'd1; end       else  R_State<=R_State;    end6'd57: //停止时钟,同时输出Done信号,表示一次读写操作完成    begin     R_SCL_En<=1'b0;     O_Done<=1'b1;//拉高Done信号     R_State<=R_State+6'd1;    end6'd58:    begin     O_Done<=1'b0;//拉低Done信号     R_State<=Start;    enddefault:   begin             R_SCL_En<=1'b0;//计数时钟停止             R_State<=6'd0;                     R_SDA_I_O_SET<=1'b1;//默认设置为输出管脚                     R_SDA_t<=1'b1;//SDA输出默认拉高                     O_Done<=1'b0;    endendcase   end    else         //开始信号无效时,回到初始设置   begin    R_SCL_En<=1'b0;     //计数时钟停止    R_State<=6'd0;    R_SDA_I_O_SET<=1'b1;//默认设置为输出管脚    R_SDA_t<=1'b1;      //SDA输出默认拉高    O_Done<=1'b0;   end        endend/*******配置三态门信号******/assign  IO_SDA=(R_SDA_I_O_SET)?R_SDA_t:1'bz;endmodule

采用了一个仿顺序操作的写法来完成了本次的主机模块的编写,代码段注释比较详细,就不在过多解读代码,至于什么是仿顺序写法与本文无关,感兴趣的可以去看《FPGA那些事--建模篇》,本人受这本书“荼毒甚深”。
为了能对该模块进行测试,编写了相应的TestBench程序,具体的代码段如下:
`timescale 1ns / 1ps////////////////////////////////////////////////////////////////////////////////// Company: // Engineer://// Create Date:   19:10:07 11/21/2017// Design Name:   I2C_Master// Module Name:   F:/verilog_demo/I2C_Bus/I2C_Master_tb.v// Project Name:  I2C_Bus// Target Device:  // Tool versions:  // Description: //// Verilog Test Fixture created by ISE for module: I2C_Master//// Dependencies:// // Revision:// Revision 0.01 - File Created// Additional Comments:// ////////////////////////////////////////////////////////////////////////////////module I2C_Master_tb;// Inputsreg I_Clk_in;reg I_Rst_n;reg I_Start;reg I_R_W_SET;reg [6:0] I_Slave_Addr;reg [15:0] I_R_W_Data;// Outputswire O_SCL;wire O_Done;wire [7:0] O_Data;        wire O_Error;// Bidirswire IO_SDA;// Instantiate the Unit Under Test (UUT)I2C_Master uut (.I_Clk_in(I_Clk_in), .I_Rst_n(I_Rst_n), .O_SCL(O_SCL), .IO_SDA(IO_SDA), .I_Start(I_Start), .O_Done(O_Done), .I_R_W_SET(I_R_W_SET), .I_Slave_Addr(I_Slave_Addr), .I_R_W_Data(I_R_W_Data), .O_Data(O_Data),.O_Error(O_Error));//重定义参数大小,减少仿真时间defparam uut.Start_Delay=8'd4;defparam uut.Stop_Delay=8'd4;defparam uut.SCL_Period=8'd19;defparam uut.SCL_LOW_Dest=8'd14;defparam uut.SCL_HIGH2LOW=8'd9;defparam uut.ACK_Dect=8'd5;initial begin// Initialize InputsI_Clk_in = 0;I_Rst_n = 0;I_Start = 0;I_R_W_SET=1'b1;I_Slave_Addr = 0;#3 I_Rst_n=1'b1;   I_Start=1'b1;                I_Slave_Addr=7'b010_1100;//从机地址                I_R_W_Data=16'b0010_0001_0010_0011;//I_R_W_Data[15:8]->reg addr I_R_W_Data[7:0]-> data    end /***CLK***/always #1  I_Clk_in=~I_Clk_in;/****Test_Vector****/reg  [6:0]  R_State;    reg  [7:0]  WR_Data; reg  [7:0]  RD_Data;//建立新的三态门reg         SDA_t;reg         SDA_SET;wire        IO_SDA_S;assign      IO_SDA_S=(SDA_SET)?SDA_t:1'bz;//两个三态门相连assign      IO_SDA=IO_SDA_S;always @ (posedge O_SCL or negedge I_Rst_n)begin      if (~I_Rst_n)   begin  R_State<=7'd0;  WR_Data<=8'b0;  RD_Data<=8'b0101_0110;  SDA_SET<=1'b0; //默认为输入  SDA_t<=1'b0; endelse                  begin   case(R_State) 7'b0,7'd1,7'd2,7'd3,7'd4,7'd5,7'd6: //接收从机地址和读写标志    R_State<=R_State+7'd1; 7'd7: //第8个脉冲周期    begin      #20 SDA_SET<=1'b1;//三态转输出  SDA_t<=1'b0;  R_State<=R_State+7'd1;    end 7'd8://返回ACK信号    begin      #20 SDA_SET<=1'b0; //三态门转输入  R_State<=R_State+7'd1;    end 7'd9,7'd10,7'd11,7'd12,7'd13,7'd14,7'd15://接收寄存器地址     R_State<=R_State+7'd1; 7'd16:    begin      #20 SDA_SET<=1'b1;//三态转输出  SDA_t<=1'b0;  R_State<=R_State+7'd1;     end     7'd17: //返回ACK信号    begin       #20 SDA_SET<=1'b0; //三态门转输入                                   R_State<=R_State+7'd1;      end 7'd18,7'd19,7'd20,7'd21,7'd22,7'd23,7'd24://接收数据    begin       WR_Data<={WR_Data[6:0],IO_SDA};       R_State<=R_State+7'd1;    end 7'd25:    begin       WR_Data<={WR_Data[6:0],IO_SDA};       #20 SDA_SET<=1'b1;//三态转输出   SDA_t<=1'b0; //返回ACK信号   R_State<=R_State+7'd1;      end 7'd26: //ACK    begin      #20 SDA_SET<=1'b0; //三态门转输入                                  R_State<=R_State+7'd1;    end 7'd27: //终止位    begin       #18  I_R_W_SET<=1'b0; //运行模式转为读,同时Start关掉    I_Start=1'b0;    //关闭模块       #100 I_Start=1'b1; //Start开启  进行一次完整读过程    R_State<=R_State+7'd1;         end 7'd28,7'd29,7'd30,7'd31,7'd32,7'd33,7'd34://接收从机地址      R_State<=R_State+7'd1;                         7'd35:                             begin      #20 SDA_SET<=1'b1;//三态转输出  SDA_t<=1'b0;  R_State<=R_State+7'd1;                                 end   7'd36: //返回ACK信号     begin       #20 SDA_SET<=1'b0; //三态门转输入                               R_State<=R_State+7'd1;       end 7'd37,7'd38,7'd39,7'd40,7'd41,7'd42,7'd43: //接收读寄存器地址     R_State<=R_State+6'd1; 7'd44: //第8个脉冲     begin       #20 SDA_SET<=1'b1;//三态转输出   SDA_t<=1'b0;   R_State<=R_State+7'd1;        end 7'd45://返回ACK信号     begin      #20 SDA_SET<=1'b0;//三态转输入                                  R_State<=R_State+7'd1;       end 7'd46:       R_State<=R_State+7'd1;                          7'd47,7'd48,7'd49,7'd50,7'd51,7'd52,7'd53://接收从机地址       R_State<=R_State+7'd1;                         7'd54://第8个脉冲                              begin#20 SDA_SET<=1'b1;//三态转输出            SDA_t<=1'b0;//返回ACK信号            R_State<=R_State+7'd1;                              end                          7'd55://第9个脉冲                              begin#30 SDA_t<=RD_Data[7];    R_State<=R_State+7'd1;                              end 7'd56,7'd57,7'd58,7'd59,7'd60,7'd61,7'd62://输出寄存器内数据       begin #30 SDA_t<=RD_Data[7'd62-R_State];     R_State<=R_State+7'd1;         end 7'd63://第8个脉冲       begin#20 SDA_SET<=1'b0;//三态转输入    R_State<=R_State+7'd1;         end 7'd64: //第9个脉冲        begin#20 R_State<=R_State+7'd1;  end                         7'd65://STOP                                begin #18  I_R_W_SET<=1'b1; //运行模式转为写,同时Start关掉      I_Start=1'b0; #100 I_Start=1'b1; //Start开启  进行一次NACK的写过程      R_State<=R_State+7'd1;                                                    end                         7'd66,7'd67,7'd68,7'd69,7'd70,7'd71,7'd72://写入7位从机地址                                 R_State<=R_State+7'd1;                         7'd73://第8个脉冲                                begin#20  SDA_SET<=1'b1;     SDA_t<=1'b1;//发送NACK信号             R_State<=R_State+7'd1;                                 end                         7'd74://第9个脉冲                                 begin #20 SDA_SET<=1'b0;     R_State<=R_State+7'd1;                                  end                         7'd75://Stop                                 begin   #10  I_R_W_SET<=1'b1; //运行模式为写,同时Start关掉I_Start=1'b0;   #100 R_State<=R_State;//结束仿真                                       enddefault: R_State<=6'b0;endcase    endendendmodule
TestBench使主机模块先写后读一组数据,两次过程间存在一定延迟,最后模拟了从机地址未成功写入下的主机应答,本次写TestBench让我收获最大的是完成了三态门仿真,以前如何对三态门进行仿真是我很头疼的问题,这次得到了解决,具体的仿真方法,在TestBench里有实例可供参考。
以下为仿真结果:
主机进行一次写时序:
主机进行一次读时序:
主机写入从机地址时从机未响应:

4.2 SI5338时钟芯片初始化程序
5338时钟芯片是一类可实现时钟分频与倍频的芯片,通过配置其寄存器参数,可以完成对输入时钟的分频与倍频操作,配置寄存器参数则需要用到I2C通讯协议。芯片资料上关于5338上I2C接口的说明如下:
从上述说明中得到如下重要的信息:其一,5338从机地址为70H,当然这是在不使用I2C_LSB_PIN的情况下。其二,5338支持连续写功能,主机在第三个字节写入后不发送结束信号,之后写入的一字节数据将会进入比当前子寄存器地址高一位的子寄存器中;其三,可以发现5338的读过程比先前资料给出的多了一个P过程,也就是结束信号过程,这再次充分证明了,这个协议具体怎么使还得看资料手册怎么标注的,不过还好,本次配置5338只需要用写过程就可以。
5338的子寄存器不是多而是很多。。。,完成一次赋值需要使用252次的子寄存器赋值,为了方便赋值和模块移植,把需要赋值的子寄存器的地址保存在一个深度为252,位宽为8的ROM内,而相应的数据则存放在同样大小ROM内,此后要改变参数只要修改ROM值就可以了。
在5338完成时钟分频后,把输入信号和输出时钟分频成1Hz分别驱动两个LED灯,可以观察到两个LED闪烁的频率一致,从而验证赋值的准确,在这里只给出配置寄存器的模块代码,顶层代码由于只具备验证性,不再给出。

`timescale 1ns / 1ps//////////////////////////////////////////////////////////////////////////////////// Company: // Engineer: // // Create Date:    22:28:47 11/21/2017 // Design Name: // Module Name:    SI5338_Init // Project Name: // Target Devices: // Tool versions: // Description: //              使用I2C总线协议控制SI5338时钟芯片的寄存器初始化,由于只需要写,不使用I2C_Master通用读写模块,太笨重了。// Dependencies: //// Revision: // Revision 0.01 - File Created// Additional Comments: ////////////////////////////////////////////////////////////////////////////////////module SI5338_Init(          //I2C  I_Clk_in,  I_Rst_n,  O_SCL,   //SCL时钟线  IO_SDA,  //SDA双向数据总线          //  O_Init_Done //初始化完成指示位.初始化后输出一个高电平    );//I/Oinput I_Clk_in;input I_Rst_n;output         O_SCL;inout IO_SDA;output         O_Init_Done;/******时钟定位模块(测试时钟25MHz),定位SCL的高电平中心,与SCL的低电平中心,产生100kHz的SCL*******/parameter   Start_Delay=8'd24;//开始时SDA拉低电平持续的时间,共用计数器下应小于SCL_HIGH2LOW-1parameter   Stop_Delay=8'd99;//一次读/写结束后SDA拉高电平的时间,共用计数器下应小于SCL_HIGH2LOW-1parameter   SCL_Period=8'd249;//测试板时钟为25MHz,100KHz为250个Clkparameter   SCL_LOW_Dest=8'd187;//时钟判定高电平在前,低电平在后,低电平中央为3/4个周期,187.5个Clkparameter   SCL_HIGH2LOW=8'd124;//电平翻转位置,1/2个SCL周期,125个Clkreg [7:0]   R_SCL_Cnt;reg         R_SCL_En;assign      O_SCL=(R_SCL_Cnt<=SCL_HIGH2LOW)?1'b1:1'b0;//SCL 时钟输出always @ (posedge I_Clk_in or negedge I_Rst_n)beginif (~I_Rst_n) begin  R_SCL_Cnt<=8'b0; endelse begin  if (R_SCL_En)   if (R_SCL_Cnt==SCL_Period)    R_SCL_Cnt<=8'b0;   else    R_SCL_Cnt<=R_SCL_Cnt+8'b1;  else   R_SCL_Cnt<=8'b0; endend/***写入延迟计数器***/parameter   T_Delay=8'd249;reg         R_Delay_En;reg [7:0]   R_Delay_Cnt;always @ (posedge I_Clk_in or negedge I_Rst_n)begin     if (~I_Rst_n)   begin    R_Delay_Cnt<=8'd0;   end  else   begin     if (R_Delay_En)                R_Delay_Cnt<=R_Delay_Cnt+8'd1;     elseR_Delay_Cnt<=8'd0;   endend/***I2C写模块***/parameter    Slave_Addr=7'b111_0000; //从机地址reg [8:0]    R_Word_Cnt;             //保存写入的子寄存器个数reg [5:0]    R_State;reg [7:0]    R_Rom_Addr_Addr;        //写入寄存器地址的地址reg [7:0]    R_Rom_Data_Addr;        //写入数据的地址reg R_I_O_SET; //SDA管脚输入输出选择线reg          R_SDA_t;                //三态门的输出管教reg          O_Init_Done;            //指示芯片内部寄存器是否初始化完成wire[6:0]    W_Slave_Addr;           //从机地址wire[7:0]    W_Reg_Addr;             //从机子寄存器地址wire[7:0]    W_Reg_Data; //子寄存器数据assign       W_Slave_Addr=Slave_Addr;always @ (posedge I_Clk_in or negedge I_Rst_n)begin    if (~I_Rst_n)       beginR_State<=6'b0;R_SCL_En<=1'b0;R_Delay_En<=1'b0;R_Word_Cnt<=5'b0;R_Rom_Addr_Addr<=8'b0;R_Rom_Data_Addr<=8'b0;R_SDA_t<=1'b1;   //SDA默认拉高R_I_O_SET<=1'b1; //默认SDA为输出O_Init_Done<=1'b0;       end    else     begin        case (R_State) 6'd0:  //启动位   begin    R_SCL_En<=1'b1;  //SCL时钟启动    R_Delay_En<=1'b0; //延迟时钟关闭      if (R_SCL_Cnt==Start_Delay)begin R_SDA_t<=1'b0; //启动位时,在SCL高电平拉低SDA R_State<=R_State+6'd1;        end      else                 R_State<=R_State;    end 6'd1,6'd2,6'd3,6'd4,6'd5,6'd6,6'd7: //写入从机7位地址    begin     if (R_SCL_Cnt==SCL_LOW_Dest)       begin R_SDA_t<=W_Slave_Addr[6'd7-R_State]; //从MSB-LSB顺序写入 R_State<=R_State+6'd1;       end     else R_State<=R_State;      end  6'd8: //写入写标志0    begin      if (R_SCL_Cnt==SCL_LOW_Dest) begin  R_SDA_t<=1'b0; //从MSB-LSB顺序写入
                  R_State<=R_State+6'd1;         end      else          R_State<=R_State;             end   6'd9: //第8个时钟下降沿处开始(跳过)检测ACK     begin      if (R_SCL_Cnt==SCL_HIGH2LOW)   begin  R_I_O_SET<=1'b0; //SDA输出转输入  R_State<=R_State+6'd1; end       else     R_State<=R_State;        end    6'd10: //第9个时钟下降沿处停止检测ACK      begin        if (R_SCL_Cnt==SCL_HIGH2LOW)    begin    R_I_O_SET<=1'b1; //占用总线用于下次发送    R_SDA_t<=1'b0;                    R_State<=R_State+6'd1;       endelse    R_State<=R_State;          end     6'd11,6'd12,6'd13,6'd14,6'd15,6'd16,6'd17,6'd18://写入从机子寄存器8位地址       begin if (R_SCL_Cnt==SCL_LOW_Dest)      begin     R_SDA_t<=W_Reg_Addr[6'd18-R_State]; //从MSB-LSB写入子寄存器地址        R_State<=R_State+6'd1;    end else     R_State<=R_State;       end     6'd19: //第8个时钟下降沿开始(跳过)检测ACKbegin          if (R_SCL_Cnt==SCL_HIGH2LOW)     begin         R_I_O_SET<=1'b0; //SDA输出转输入      R_State<=R_State+6'd1;     end  else     R_State<=R_State;end     6'd20: begin   if (R_SCL_Cnt==SCL_HIGH2LOW)      begin       R_I_O_SET<=1'b1; //占用总线用于下次发送       R_SDA_t<=1'b0;                       R_State<=R_State+6'd1;        end   else       R_State<=R_State;  end             6'd21,6'd22,6'd23,6'd24,6'd25,6'd26,6'd27,6'd28: //写入子寄存器数据                 begin    if (R_SCL_Cnt==SCL_LOW_Dest)       begin R_SDA_t<=W_Reg_Data[6'd28-R_State]; //从MSB-LSB顺序写入子寄存器中数值 R_State<=R_State+6'd1;       end    elseR_State<=R_State;                 end                       6'd29://第8个时钟下降沿开始(跳过)检测ACK  begin     if (R_SCL_Cnt==SCL_HIGH2LOW)        begin           R_I_O_SET<=1'b0; //SDA输出转输入     R_State<=R_State+6'd1;end      else    R_State<=R_State;   end             6'd30:    begin      if (R_SCL_Cnt==SCL_HIGH2LOW) begin  R_I_O_SET<=1'b1; //占用总线用于下次发送  R_SDA_t<=1'b0;                          R_State<=R_State+6'd1;           end      else  R_State<=R_State;    end            6'd31: //停止位                    begin      if (R_SCL_Cnt==Stop_Delay)          begin   R_SDA_t<=1'b1; //SCL高电平期间拉高SDA总线表示停止信号   R_SCL_En<=1'b0;//SCL时钟使能关闭   R_Delay_En<=1'b1;//由于数据写入从机寄存器需要时间,在结束位发送后,延迟一段时间再次开始下一次传输                           R_State<=R_State+6'd1;//开始进入等待数据写入寄存器状态   end       else   R_State<=R_State;                     end             6'd32:                    begin     if (R_Delay_Cnt==T_Delay) begin   R_Word_Cnt<=R_Word_Cnt+8'd1;      R_Delay_En<=1'b0;       //定时时间到,则定时时钟停止   if (R_Word_Cnt==8'd252) //当全部寄存器写入时,不在返回初始状态,初始化完成标志拉高     begin       R_State<=R_State;       O_Init_Done<=1'b1;     end    else      begin       R_State<=6'd0;       R_Rom_Addr_Addr<=R_Rom_Addr_Addr+8'd1;               R_Rom_Data_Addr<=R_Rom_Data_Addr+8'd1;       end   end      elseR_State<=R_State;                     end        default:   begin     R_State<=6'b0;     R_SCL_En<=1'b0;     R_Delay_En<=1'b0;             R_Word_Cnt<=5'b0;             R_SDA_t<=1'b1;   //SDA默认拉高             R_I_O_SET<=1'b1; //默认SDA为输出             O_Init_Done<=1'b0;     end endcase        endend/***三态门定义***/assign  IO_SDA=(R_I_O_SET)?R_SDA_t:1'bz;/***SI5338数据模块***/ROM_Si5338_addr_252x8bit  ROM_Reg_Addr (  .clka(I_Clk_in), // input clka  .addra(R_Rom_Addr_Addr), // input [7 : 0] addra  .douta(W_Reg_Addr) // output [7 : 0] douta);ROM_Si5338_data_252x8bit ROM_Reg_Data (  .clka(I_Clk_in), // input clka  .addra(R_Rom_Data_Addr), // input [7 : 0] addra  .douta(W_Reg_Data) // output [7 : 0] douta);endmodule













原创粉丝点击