I2C总线通讯协议中主机模块的FPGA实现
2017-11-29 22:37
507 查看
本人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位数据
具体的实现代码如下:
采用了一个仿顺序操作的写法来完成了本次的主机模块的编写,代码段注释比较详细,就不在过多
dd7b
解读代码,至于什么是仿顺序写法与本文无关,感兴趣的可以去看《FPGA那些事--建模篇》,本人受这本书“荼毒甚深”。
为了能对该模块进行测试,编写了相应的TestBench程序,具体的代码段如下:
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闪烁的频率一致,从而验证赋值的准确,在这里只给出配置寄存器的模块代码,顶层代码由于只具备验证性,不再给出。
结果就是,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_sig I_Start, //一次读/写操作开始信号 O_Done, //一次读/写操作结束信号 I_R_W_SET, //读写控制信号,写为1,读为0 I_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/O input I_Clk_in; input I_Rst_n; output O_SCL; inout IO_SDA; input I_Start; output O_Done; input [6:0] I_Slave_Addr; input I_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-1 parameter Stop_Delay=9'd150;//一次读/写结束后SDA拉高电平的时间,共用计数器下应小于SCL_HIGH2LOW-1 parameter SCL_Period=9'd499;//测试板时钟为50MHz,100KHz为500个Clk parameter SCL_LOW_Dest=9'd374;//时钟判定高电平在前,低电平在后,低电平中央为3/4个周期,375个Clk parameter SCL_HIGH2LOW=9'd249;//电平翻转位置,1/2个SCL周期,250个Clk parameter 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) begin if (~I_Rst_n) begin R_SCL_Cnt<=9'b0; end else 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; end end /******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) begin if (~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; end else begin if (I_Start) //当开始信号置高时表示I2C通信开始 begin case(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) begin R_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 begin R_SDA_I_O_SET<=1'b0; R_State<=R_State+6'd1; end else R_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; end else 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 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'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位数据地址 begin if (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; end else 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; end 6'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; end 6'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; end 6'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; end 6'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; end 6'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; end Stop: //发送停止位 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; end 6'd57: //停止时钟,同时输出Done信号,表示一次读写操作完成 begin R_SCL_En<=1'b0; O_Done<=1'b1;//拉高Done信号 R_State<=R_State+6'd1; end 6'd58: begin O_Done<=1'b0;//拉低Done信号 R_State<=Start; end default: 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 endcase 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 end end /*******配置三态门信号******/ assign IO_SDA=(R_SDA_I_O_SET)?R_SDA_t:1'bz; endmodule
采用了一个仿顺序操作的写法来完成了本次的主机模块的编写,代码段注释比较详细,就不在过多
dd7b
解读代码,至于什么是仿顺序写法与本文无关,感兴趣的可以去看《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; // Inputs reg 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; // Outputs wire O_SCL; wire O_Done; wire [7:0] O_Data; wire O_Error; // Bidirs wire 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 Inputs I_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; end else 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;//结束仿真 end default: R_State<=6'b0; endcase end end endmodule
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/O input 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-1 parameter Stop_Delay=8'd99;//一次读/写结束后SDA拉高电平的时间,共用计数器下应小于SCL_HIGH2LOW-1 parameter SCL_Period=8'd249;//测试板时钟为25MHz,100KHz为250个Clk parameter SCL_LOW_Dest=8'd187;//时钟判定高电平在前,低电平在后,低电平中央为3/4个周期,187.5个Clk parameter SCL_HIGH2LOW=8'd124;//电平翻转位置,1/2个SCL周期,125个Clk reg [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) begin if (~I_Rst_n) begin R_SCL_Cnt<=8'b0; end else 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; end end /***写入延迟计数器***/ 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; else R_Delay_Cnt<=8'd0; end end /***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) begin R_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; end else 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个时钟下降沿开始(跳过)检测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'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 else R_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 else R_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 end end /***三态门定义***/ 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
相关文章推荐
- 1.1 SATA主机协议的FPGA实现之准备工作
- 1.2 SATA主机协议的FPGA实现之物理层设计
- I2C协议master设备的FPGA实现
- 1.1 SATA主机协议的FPGA实现之准备工作
- 1.2 SATA主机协议的FPGA实现之物理层设计
- apache中使用mod_gnutls模块实现多个SSL站点配置(多个HTTPS协议的虚拟主机)
- 1.1 SATA主机协议的FPGA实现之准备工作
- 1.2 SATA主机协议的FPGA实现之物理层设计
- 1.1 SATA主机协议的FPGA实现之准备工作
- 1.2 SATA主机协议的FPGA实现之物理层设计
- 1.1 SATA主机协议的FPGA实现之准备工作
- 1.2 SATA主机协议的FPGA实现之物理层设计
- SATA主机协议的FPGA实现之物理层设计
- 1.1 SATA主机协议的FPGA实现之准备工作
- 1.2 SATA主机协议的FPGA实现之物理层设计
- 1.1 SATA主机协议的FPGA实现之准备工作
- 1.2 SATA主机协议的FPGA实现之物理层设计
- I2C总线协议的verilog实现
- 1.1 SATA主机协议的FPGA实现之准备工作
- 1.2 SATA主机协议的FPGA实现之物理层设计