经过上一篇I2C协议原理的讲解,相比大家都已经了解I2C的工作原理 。首先想好设计程序代码用哪种方案。

1、使用线性序列机

根据I2C传输时序特点,是很容易分析出I2C单纯的读或者写时序就像是时间轴上的一段连续操作,我们只需要在指定的时间将SDA或者SCL信号拉高、拉低、或者设置为三态就可以了。

优点:代码编写方便,思路简单。

缺点:代码编写比较杂乱,难以调试分析,代码量比较多。

2、使用状态机

  序列机实现方便,但是编写过程中调试麻烦,而使用状态机就可以轻松解决这个问题,比如,对于整个I2C控制器从大的角度分为3个状态:空闲状态(IDLE),完整的写操作状态(WRITE),完整的读操作状态(READ)。然后再对每个单独的读和写状态进行进一步的划分,得到分别与读和写相关联的小状态,比如对于完整的写操作:

【发起起始位】>【写器件地址】>【应答位】>【写存储器地址】>【应答位】>【写数据】>【应答位】>【停止位】

对于完整的读操作,又可细分:

【发起起始位】>【写器件地址】>【应答位】>【写存储器地址】>【应答位】>【发起始位】>【写器件地址】>【应答位】>【读数据】>【应答位】>【停止位】

假设我们有以下一个模块:

先解释下模块里面的几个用到的输入输出端口信号功能:

接口名称 I/O 功能描述
Clk I 模块工作时钟,50M时钟

Rst_n

I 模块复位信号
Cmd I 控制总线实现各种传输操作的各种命令的组合(写、读、起始、停止、应答、无应答)
Go I 整个模块的启动使能信号,为了接口使用方便,使用时希望只需要对该端口产生一个单时钟周期的脉冲即可启动一个字节完整的传输(含可能的起始位、停止位、应答位)
Rx_DATA O 总线收到的8位数据,读操作时读到的数据由此端口传出
Tx_DATA I 总线要发送的8位数据,需要传输的数据经此端口传入该模块
Trans_Done O 发送或接受8位数据完成标志信号,每次传输完成都会产生一个单周期的高脉冲信号
ack_o O 从机是否应答标志,在此底层逻辑中,我们暂时不对总线上是否给出正确的应答信号做出任何处理,仅将接收到的应答信号状态存储并输出,如果没有收到正确的应答信号,该如何执行下一步,由外部其他逻辑去决定。
i2c_sclk O i2c时钟总线
i2c_sdat I/O i2c数据总线

需要注意的是,在上述端口中,Cmd端口的解释是控制总线实现各种传输操作的各种命令组合。组合二字表明,该端口每次可能不止传输一个命令。什么意思,其实很好理解,以写操作来说,要实现8位数据的写,肯定要传输写命令,不能传输读命令。但是同时在8位数据传输之前还有可能 需要产生起始位,因此还需要同时给该逻辑提供产生起始位的命令,或者写完要产生停止位,也要

给该逻辑提供写命令同时还要提供产生停止位的命令,所以说每一次传输,命令端口输入的都应该是好几个命令的组合。

对于Cmd端口来说,按照总结,可以归纳为需要传输如下若干个基本命令:

写命令:本次传输为主机向从机写一个自己的数据(数据内容可以是控制字段、地址字段或者写入数据字段)

读命令:本次传输为主机接受从从机读到的1个字节的数据内容。

产生起始位命令:本次传输需要在数据内容之前加上起始位

产生停止位命令:本次传输需要在应答位之后加上停止位

ACK命令:本次传输需要产生应答位(主要针对读数据操作)

NACK命令:本次传输需要产生无应答信号(主要针对读数据操作)

分析完端口之后,接下来就可以分析要实现的传输。

状态机设计:

从上面的状态转移图可以看出,这里总共分成了7个状态,刚开始复位(RST)后的默认状态(IDLE)、产生起始信号状态(GEN_STA)、写数据状态(WR_DATA)、读数据状态(RD_DATA)、检测从机是否应答状态(CHECK_ACK)、给从机应答状态(GEN_ACK)、产生停止位状态(GEN_STO),通过这些状态我们就能组合成基本的I2C读写时序。

于是就可以定义如下几个状态:

localparamIDLE = 7'b0000001,//空闲状态GEN_STA = 7'b0000001,//产生起始信号WR_DATA = 7'b0000001,//写数据状态RD_DATA = 7'b0000001,//读数据状态CHECK_ACK = 7'b0000001,//检测应答状态GEN_ACK = 7'b0000001,//产生应答状态GEN_STO = 7'b0000001;//产生停止信号

上文已经提到,一次传输中包含多种可能的命令要求,为了方便组合成对应传输情况,我们在这里将传输时需要执行的可能的6个命令先定义出来,(写请求<WR>、起始位请求<STA>、读请求<RD>、停止位请求<STO>、应答位请求<ACK>、无应答请求<NACK>)

​
localparamWR = 6'b000001,//写请求STA = 6'b000001,//起始位请求RD = 6'b000001,//读请求STO = 6'b000001,//停止位请求ACK = 6'b000001,//应答位请求NACK = 6'b000001;//无应答位请求​

有了上述命令,我们就可以通过给模块的Cmd端口传输多个命令的组合来唯一限定一次特定传输需求。例如I2C传输中写器件地址操作,需要在传输之前加上起始位。那么我们根据写操作时序命令就是,起始位、器件地址、最低位为零,如果我们简化写一下就是Cmd=WR|STA。

了解了端口定义,在端口脉冲控制信号的控制下,使能en_div_cnt来让序列机计数器模块运行,同时将Cmd命令和定义的几个命令按位与运算,根据运算结果来确定下一步的操作。当然这个是有优先级的,因为只有产生起始信号(STA),才能进行写操作(WR)和读操作(RD)。所以空闲状态代码里面可以按照这个优先级来将Cmd和上面的几种命令进行按位与运算,如果结果为1,就可以跳转下面进行的对应状态,如果运算结果为0,接着往下面进行判断,代码如下:

IDLE(空闲状态):

IDLE:beginTrans_Done <= 1'b0;i2c_sdat_oe <= 1'd1;if(Go)beginen_div_cnt <= 1'b1;if(Cmd & STA)state <= GEN_STA;else if(Cmd & WR)state <= WR_DATA;else if(Cmd & RD)state <= RD_DATA;elsestate <= IDLE;end else beginen_div_cnt <= 1'b0;state <= IDLE;endend

根据上面代码,很显然写器件地址操作的Cmd命令里面的条件首先会跳转到GEN_STA这个状态,根据I2C总线协议规定,在时钟(SCL)为高电平的时候,数据总线(SDA)由高变低的跳变为总线其实信号,所以这里该开始第一步将i2c_sdat_o设置为1,i2c_sdat_oe使能,第二步将总线时钟(SCL,代码里面i2c_sclk)拉高,第三步将第一步已经被上拉电阻拉高的i2c_sdat再拉低(代码里面是通过拉低i2c_sdat_o来间接拉低i2c_sdat),此时的i2c应该还是维持高电平,第四步将时钟总线i2c_sclk拉为低电平。这里将一个操作分成4步来完成,这也是为什么SCL_CNT_M的时候除以4的原因,代码如下:

GEN_STA:

GEN_STA:beginif(sclk_plus)beginif(cnt == 3)cnt <= 0;elsecnt <= cnt +1'b1;case(cnt)0:begin i2c_sdat_0 <=1; i2c_sdat_oe <= 1'd1;end1:begin i2c_sclk <= 1'd1;end2:begin i2c_sdat_0 <=0; i2c_sclk <= 1;end3:begin i2c_sclk <= 1'd1;enddefault:begin i2c_sdat_o <= 1; i2c_sclk <= 1;endendcaseif(cnt == 3)beginif(Cmd & WR)state <= WR_DATA;else if(Cmd & RD)state <= RD_DATA;endendend

上面的代码中产生了起始信号,接着就是发送7位器件地址和1位方向位(0:写,1:读),这里举例的是写器件地址操作,所以当然最低位为0,那么起始信号发送完后同时就要在里面判断这个Cmd中是否含有写数据(WR)命令,再次判断如果Cmd和写数据命令(WR)命令进行按位与运算,如果运算结果为1,说明命令里面有写数据请求,此时就可以跳转到写数据状态,如果运算结果为0就接着往下面判断,代码如下:

WR_DATA:

WR_DATA:if(sclk_plus)beginif(cnt == 31)cnt <= 0;elsecnt <= cnt + 1'b1;case(cnt)0,4,8,12,16,20,24,28:begini2c_sdat_o <= Tx_DATA[7-cnt[4:2]];i2c_sdat_oe <= 1'd1;end1,5,9,13,17,21,25,29:begin i2c_sclk <= 1;end 2,6,10,14,18,22,26,30:begin i2c_sclk <= 1;end 3,7,11,15,19,23,27,31:begin i2c_sclk <= 0;end default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;endendcaseif(cnt == 31)beginstate <= CHECK_ACK;endend

为了让数据这个状态写的数据更加灵活,这里我们把要写入的8位数据定义成输入端口Tx_DATA,那么我们把7位器件地址和最低位(方向位)的0直接赋值给Tx_DATA,之后就可以直接统一发送给Tx_DATA里面的数据就行了,同样我们还是按照产生起始位一样的规律,写数据的时候每一个bit也分成4步来完成,第一步将要发送的数据追备好(i2c_sdat_o <=Tx_DATA[7]),i2c_sdat_oe使能,第二步将总线时钟(SCL,代码里i2c_sclk)拉高,第三步将总线时钟继续保持为高电平,第四步将数据总线i2c_sclk拉为低电平。这样就将最高位的数据通过总线发出,接着还是一样分为四步发送Tx_DATA[6].....一直到最低位Tx_DATA[0]发送完成。

写完数据后我们就要判断是否真的写成功,判断依据就是对方有没有产生应答,所以写完数据后就要跳转到应答状态(CHECK_ACK)。

检测应答状态一样分为4步,第一步将总线设置为输入,即i2c_sdat_oe设置为0,i2c_sclk拉为低电平,第二步将时钟总线i2c_sclk拉高,第三步总线时钟继续保持为高电平,读取数据总线i2c_sdat的值到ack_o,此判断对方产生是否产生应答只需要判断ack_o的值是0(应答)还是1(无应答),第四步将时钟总线i2c_sclk拉为低电平。检测应答状态代码如下:

CHECK_ACK:

CHECK_ACK:beginif(sclk_plus)beginif(cnt == 3)cnt <= 0;else         cnt <= cnt + 1'b1;case(cnt)0:begin i2c_sdat_oe <= 1'd0; i2c_sclk <= 0;end1:begin i2c_sclk <= 1;end2:begin ack_o <= i2c_sdat; i2c_sclk <= 0;end3:begin i2c_sclk <= 0;enddefault: begin i2c_sdat_o <= 1; i2c_sclk <= 1;endendcaseif(cnt == 3)beginif(Cmd & STO)state <= GEN_STO;else beginstate <= IDLE;Trans_DONE <= 1'b1;endendendend

检测完应答信号后要判断Cmd里面是否含有要产生停止信号的命令,如果有就跳转到产生停止信号(CEN_STO)状态,没有就跳到空闲状态(IDLE)。

如果Cmd命令中定义了需要产生的停止位,则跳转到产生停止位状态(GEN_STO),根据i2c总线协议停止位的定义,在时钟(SCL,代码里面i2c_sclk)为高电平的时候,数据总线(SDA)由低电平到高电平的跳变就是一个停止信号,所以这里刚开始第一步就是讲i2c_sdat_o设置为0,i2c_sdat_oe使能,第二步将总线时钟i2c_sclk拉高,第三步将i2c_sdat_o设置为1,让外部上拉电阻将数据总线i2c_sclk拉成高电平,此时的i2c_sclk应该还是维持高电平,第四步将时钟总线i2c_sclk拉为低电平。产生停止信号代码如下:

GEN_STO:

GEN_STO:beginif(sclk_plus)beginif(cnt ==3)cnt <=0;elsecnt <= cnt + 1'b1;case(cnt)0:begin i2c_sdat_o <=0; i2c_sdat_oe <= 1'd1;end1:begin i2c_sclk <=1;end2:begin i2c_sdat_o <=1; i2c_sclk <= 1;end3:begin i2c_sclk <=1;enddefault:begin i2c_sdat_o <=1; i2c_sclk <= 1;endendcaseif(cnt == 3)beginTrans_Done <= 1'b1;state <= IDLE;endendend

到这里一个基本写器件地址操作就完成了。

当然作为一个基本的i2c操作,读操作肯定是少不了的,我们可以按照写操作和应答检测的思路来实现,带一步将总线设置为输入,即i2c_sdat_oe设置为0以使SDA信号线为输入高组态,i2c_sclk拉为低电平,第二步将总线时钟i2c_sclk拉高,第三步将总线时钟继续保持为高电平,读取数据总线i2c_sdat_oe的值到Rx_DATA[7],第四步将时钟总线i2c_sclk拉为低电平。这样就将最高位的数据总线读取出来了,接着还是一样的分成4不来读取Rx_DATA[6].....直到最低位Rx_DATA[0]读取完成。这里用到了一个小技巧,每次将读取到的数据放到Rx_DATA的最低位,读取一次,左移一次,读取完8次数据后,第一次读取的最高位数据也就通过移位的方式放到最高位。读数据状态代码如下:

RD_DATA:

RD_DATA:if(sclk_plus)beginif(cnt ==31)cnt <= 0;elsecnt <= cnt +1'b1;case(cnt)0,4,8,12,16,20,24,28:begini2c_sdat_oe <= 1'd0;i2c_sclk <= 0;end1,5,9,13,17,21,25,29:begin i2c_sclk <= 1;end 2,6,10,14,18,22,26,30:begin i2c_sclk <= 1;Rx_DATA <= {Rx_DATA[6:0],i2c_sdat};end 3,7,11,15,19,23,27,31:begin i2c_sclk <= 0;end default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;endendcaseif(cnt == 31)state <= GEN_ACKendend

读完数据后跳转到产生应答状态(GEN_ACK),这个状态里面会根据Cmd里面是否含有应答位的请求(ACK)或者无应答请求(NACK)来给对方做出一个应答,产生应答信号跟写数据状态一个思路,分为4步,第一步将Cmd与ACK进行与运算,如果结果为1说明需要产生应答,此时将i2c_sdat_o赋值为0,如果不满足,接着往下判断,将Cmd与NACK进行与运算,如果结果为1说明需要产生非应答信号,此时将i2c_sdat_o赋值为1,让外部电阻将数据总线拉成高电平。将i2c_sdat_oe使能,将时钟总线i2c_sclk拉为低电平,第二步将总线时钟i2c_sclk拉高,第三步将总线时钟继续保持为高电平,第四步将时钟总线i2c_sclk拉为低电平。这样就会根据Cmd给对方做出了应答,此时还要判断Cmd里面是否含有跳转到产生停止信号(STO)的命令(通过判断Cmd&STO是否为1就能判断),如果有就要跳转到产生停止信号(GEN_STO)。

GEN_ACK:

GEN_ACK:beginif(sclk_plus)beginif(cnt ==3)cnt <= 0;elsecnt <= cnt +1'b1;case(cnt)0:begin i2c_sdat_oe <= 1'd1;i2c_sclk <= 0;if(Cmd & ACK)i2c_sdat_o <= 1'b0;else if(Cmd & NACK)i2c_sdat_o <= 1'b1;end1:begin i2c_sclk <= 1;end2:begin i2c_sclk <= 1;end3:begin i2c_sclk <= 0;enddefault:begin i2c_sdat_o <= 1; i2c_sclk <=1;endendcaseif(cnt == 3)beginif(Cmd & STO)state <= GEN_STO;else beginstate <= IDLE;Trans_Done <= 1'b1;endendend

我们根据I2C的协议标准,这里采用的是快速模式(400kb/s)来作为总线的工作时钟,当然为了让这个模块更具有通用性和灵活性,这里就将系统时钟和产生的总线工作时钟频率都写成参数化,这里系统采用50MHZ的时钟,SYS_CLOCK设置成50000000,实际也可以根据输入的时钟来更改这个值,SCL_CLOCK是用来配置时钟(SCL)总线的频率,通过这两个参数就可以计算出参数SCL_CNT_M要计数到多少就能产生期望的总线工作时钟(SCL)频率,为什么要计算除以4,在产生起始信号状态就已说明。

//系统时钟采用50MHZ
paratmeter SYS_CLOCK=50_000_000;
//SCL总线时钟采用400kHZ
paratmeter SCLK_CLOCK = 40_000;
//产生时钟SCL计数器最大值
localparam SC_CNT_M = SYS_CLOCK/SCL_CLOCK/4-1;

有了上面的SCL_CNT_M ,接下来就可以根据这个值来产生状态机部分的工作时钟sclk_plus。这个部分的逻辑运行是通过en_div_cnt的有效控制的,当然这个信号的有效与否是受脉冲信号Go来控制的,在状态机部分可以提现出来。

reg [19:0]dic_cnt;
reg en_div_cnt;
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)div_cnt <= 20'd0;
else if(en_div_cnt)beginif(div_cnt < SCL_CNT_M)div_cnt <= div_cnt + 1'b1;elsediv_cnt <= 0;
end elsediv_cnt <= 0;wire sclk_plus = div_cnt == SCL_CNT_M;

对于i2c总线,要求连接到总线上的输出端必须是开漏输出结构,给不了高电平,所以总线上所有的高电平应该是有上拉电阻上拉达到的效果,而不是由主机直接给总线赋值为1就能实现,所以我们在写这个逻辑的时候也应该遵循这个标准,当总线上要输出低电平的时候,我们就直接给总线赋值为0,要输出为高电平时,只能将总线设置成高组态,这样再由外部上拉电阻上来成高电平,这是为了方便理解,就把我们给赋值给总线的值先复制给i2c_sdat_o,然后在控制使能信号i2c_sdat_oe,通过这两个信号间接的来给数据总线SDA赋值,代码是通过下面实现的

assign i2c_sdat =!i2c_sdat_o && i2c_sdat_oe ?1'b0:1'bz

未完续(请看仿真验证) 。。。。。。

FPGA——I2C代码篇相关推荐

  1. FPGA基础入门篇(四) 边沿检测电路

    FPGA基础入门篇(四)--边沿检测电路 一.边沿检测 边沿检测,就是检测输入信号,或者FPGA内部逻辑信号的跳变,即上升沿或者下降沿的检测.在检测到所需要的边沿后产生一个高电平的脉冲.这在FPGA电 ...

  2. FPGA通信第一篇--USB2.0

    FPGA通信第一篇–USB2.0 1 初识USB 1.1 简介 USB(UniversalSerialBus)是一种支持热插拔的高速串行传输总线,它使用差分信号来传输数据.在USB1.0和USB1.1 ...

  3. FPGA通信第二篇--UDP

    FPGA通信第二篇–UDP 本文通过对以太网通信中的UDP传输协议的理论学习,针对UDP实际应用中的丢包问题,提出一种人为的重发机制完成UDP稳定可靠的传输,并通过实验进行了验证. 1 以太网简介 以 ...

  4. 2 FPGA时序约束理论篇之时序路径与时序模型

    时序路径   典型的时序路径有4类,如下图所示,这4类路径可分为片间路径(标记①和标记③)和片内路径(标记②和标记④).   对于所有的时序路径,我们都要明确其起点和终点,这4类时序路径的起点和终点分 ...

  5. 爬虫python代码广告_零基础掌握百度地图兴趣点获取POI爬虫(python语言爬取)(代码篇)...

    我是怎么想的,在新浪博客里写代码教程. 这篇博客的内容同步到了CSND博客中,那里不限制外链,也可以复制代码. http://blog.csdn.net/sinat_41310868/article/ ...

  6. js字符串(String)转多维数组(Array) - 代码篇

    js字符串(String)转多维数组(Array) - 代码篇 Demo代码: <!DOCTYPE html> <html> <head> <meta cha ...

  7. <img src=“图片引用失败“ onerror=“自动替换默认图片“> - 代码篇

    img图片引用失败,自动替换默认图片(半句代码搞定) 图片引用失败,显示默认图片: <!--代码定义如下:--> <img src="图片引用失败" onerro ...

  8. 判断域名来源的操作【window.location.host.indexOf(‘域名关键词‘)】 - 代码篇

    文章目录 判断域名来源的操作 - 代码篇 代码如下: 判断域名来源的操作 - 代码篇 代码如下: // 判断域名的操作:如果域名是qblog 或qdownload,则为true: var flagBo ...

  9. bootstrapV4.6.0实现标签页(改造v3.3.7)- 代码篇

    文章目录 疑问 · 注意事项: 效果图: 全部代码示下: 疑问 · 注意事项: 本案例中bootstrap.css.js使用的是4.6.0版本: 网上说4.0+版本的没有"标签页" ...

最新文章

  1. 俄罗斯的顶级数学家,到底有多恐怖?
  2. 简述原型链是什么,有什么用处?
  3. 非交互模式修改Ubuntu密码的命令
  4. 【微信开发】微信开发 之 开启开发模式
  5. 重装系统后需要安装的软件
  6. 三十七、Redis和MongoDB基本语法
  7. 牛客网【每日一题】4月2日 月月查华华的手机
  8. pwm控制的基本原理_单片机PWM控制基本原理详解~
  9. Java笔记-使用RestTemplate发送http数据包(get与post)
  10. C++之关键字(63个)
  11. java 数据结构之堆排序
  12. 『真实故事』我经历了坏人变老了
  13. HTTP Header 详解 Requests 与 Responses 头信息
  14. Windows编译OpenCV
  15. C# 在线PDF阅读
  16. 服务器被攻击ip显示国外,服务器被不同的IP攻击怎么破?
  17. 基于永洪BI部署的自助分析平台(一)
  18. [UML] 如何找参与者、找用例
  19. PID调节经验知识梳理
  20. OpenBSD 6.8 切换到国内镜像源的一种临时方法:声明PKG_PATH

热门文章

  1. Codeforces 4D Mysterious (DP)
  2. SSH框架各自优缺点总结
  3. 自己写strcpy函数
  4. java-php-python-ssm-SpringMVC的时鲜蔬菜配送系统-计算机毕业设计
  5. axios请求mysql_接收post请求(vue+axios)解决跨域问题(三)
  6. Word处理控件Aspose.Words功能演示:在 C# .NET 中将 Word 转换为 PDF - 完整指南
  7. Linux下Tars框架服务更新(含自动更新脚本)
  8. 论药品包装机械的概念设计 Comment on medicines and chemical reagents package machinery conceptual design
  9. 以图搜图 – 3大相似图片搜索引擎
  10. vsb asc_vsb电力线故障检测kaggle竞争