logo资料库

I2C接口学习心得.pdf

第1页 / 共82页
第2页 / 共82页
第3页 / 共82页
第4页 / 共82页
第5页 / 共82页
第6页 / 共82页
第7页 / 共82页
第8页 / 共82页
资料共82页,剩余部分请下载后查看
北京至芯科技 FPGA 创新中心 01062670708 http://www.zxopen.com I2C 学习心得 我最近刚做完 I2C 通信协议的编写与调试,下面介绍一下我从一开始理解夏老师的程序, 修改程序,直到下板调试整个的学习过程,希望对大家学习 I2C 有一定的帮助。 一、 分析源代码 学习 I2C,首先我们要知道 I2C 是一种串行总线协议。目前几种常用的串行总线有 UART、 SPI 和 I2C,先来简单了解一下这三种串行总线。 图 1. UART、SPI、I2C 总线对比 如图 1 所示,UART 的总线数只有两条,分别是 TX(发送)和 RX(接收),没有时钟信 号,所以 UART 需要固定的波特率,也就是两位数据的间隔要相等。而 SPI 的四条总线分别 为 SCLK(时钟)、MISO(主器件数据输入,从器件数据输出 )、MOSI(主器件数据输出, 从器件数据输入)、SS(从器件使能信号),SPI 总线由主机提供时钟,为同步通信,并且 SPI 总线有两条总线进行数据传输,可以同时进行收发数据,为双工模式。SPI 有 3 线、4 线 两种模式,3 线模式一般代表没有 SS 信号,具体情况还要视器件手册而定。I2C 有两条总线 SCL(时钟)和 SDA(数据),I2C 用的线更少,因为 I2C 需要有双向 IO 的支持,而且使用上 拉电阻,抗干扰能力没有其他两种强,一般用于同一板卡上芯片之间的通信,较少用于远距 离通信。 具体到 FPGA 设计这一环节,我们需要搞清这三种总线上的信号是怎么走的,UART 很简 单,使用发送数据线 TXD 和接收数据线 RXD 来传送数据,接收和发送可以单独进行也可以 同时进行。它传送数据的格式有严格的规定,每个数据以相同的位串形式传送,每个串行数 据由起始位,数据位,奇偶校验位和停止位组成。从起始位到停止位为一个字符的完整通信 格式。SPI 情况就相对多一些,根据时钟极性(CPOL)和时钟相位(CPHA)两个参数的不同有四 种基本情况,传送数据的格式与 UART 差不多。而 I2C 总线的协议要比 UART 和 SPI 复杂,能 掌握 I2C,也就能掌握 UART 和 SPI。言归正传,回到我们的 I2C 设计实例。 第一步首先了解,这个 I2C 实例的功能。 这个实例实现了通过 I2C 总线对 EEPROM 写入数据,再将写入 EEPROM 中的数据读取出 来的一个过程。实例的重点在于对 I2C 总线协议时序的掌握,即用 I2C 总线要求的格式将数 据写入到 EEPROM 中,再读取出来。 什么是 EEPROM?EEPROM (Electrically Erasable Programmable Read-Only Memory),电可 擦可编程只读存储器,一种掉电后数据不丢失的存储芯片。EEPROM 可以在电脑上或专用设 备上擦除已有信息,重新编程。所以,EEPROM 是可以写入数据也可以读取数据的,并且 EEPROM 掉电数据并不会丢失,在后面将程序烧写到开发板的过程中可以验证这一点。 第二步,简单地了解一下这个实例的各个模块。 1.5@128kbps<3<3总线类型线数通信类型多主支持数据率总线上器件的数量线缆长度(米)UARTSPII2C242异步同步同步不支持不支持支持3Kbps到4Mbps>1Mbps<3.4Mbps2<10<10
北京至芯科技 FPGA 创新中心 01062670708 http://www.zxopen.com 在这个 I2C 实例中,为了实现两个器件的通信,我们需要一个主机和一个从机,如下图 2 所示,它们之间采用 I2C 协议进行串行通信,设计实例中的 signal 模块和 EEPROM_WR 模 块模拟主机,EEPROM 模块模拟从机。当写入数据时,EEPROM_WR 模块从 signal 模块中调 取数据,然后通过 I2C 的两条总线 SDA(数据总线)和 SCL(时钟总线)向 EEPROM 器件写 入数据。当读出数据时,EEPROM_WR 仍然通过 SDA 和 SCL 总线读出 EEPROM 中的数据,然 后输入给 signal 模块,比较读出的数据与当时写入的数据是否相同。验证读写的正确性。 在至芯科技 EP2C8-2010 开发板上,是有 EEPROM 器件的,而 signal 模块我们可以通 过创建一个 RAM 的 IP 核来实现其功能,所以图 2 中唯有 EEPROM_WR 模块是要写入 FPGA 图 2. EEPROM 读写电路和它的测试电路 中的可综合模块,所谓可综合模块就是可以转化为实际电路的模块,而 signal 和 EEPROM 模块则是为了仿真而存在的,所以其代码不必可综合,只编写其行为级模型即可。 读者 在后面会发现这三个模块的源代码编写风格不一样,这就是因为 EEPROM_WR 模块是可 综合的,要求比较严格,不能用一些 initial、#50 之类的语句,而 signal 与 EEPROM 模块 则没有这个限制。 第三步,了解 I2C 总线特征。 (A) (B) SCL 在 I2C 总线中,只有在总线处于“非忙”状态时,才能开始数据传输。在数据传输期间, 只要时钟线为高电平,数据线都必须保持稳定,否则数据线上的任何变化都被当作“启动” 或“停止”信号。图 3 是总线状态的定义。 图 3. I2C 双向二线制串行总线 数据在此 期间变化 数据在此 期间变化 数据的电平稳定 数据才有效 数据在此 期间变化 (D) 1/0 SDA (D) (C) (A) 启动信号 停止信号 eeprom_wrSignaleepromDATA[7:0]DATA[7:0]ADDR[10:0]ADDR[10:0]CLKCLKRDRDWRWRRESETRESETACKACKSDASCLSCLSDA
北京至芯科技 FPGA 创新中心 01062670708 http://www.zxopen.com (1)总线非忙状态(A 段):数据线 SDA 和 时钟线 SCL 都保持高电平。 (2)启动数据传输(B 段):当时钟线(SCL)为高电平状态时,数据线(SDA)由 高电平变为低电平的下降沿被认为是“启动”信号。只有出现“启动”信号后,其它的命令 才有效。 (3)停止数据传输(C 段):当时钟线(SCL)为高电平状态时,数据线(SDA)由 低电平变为高电平的上升沿被认为是“停止”信号。随着“停在”信号出现,所有的外部操 作都结束。 (4)数据有效(D 段):在出现“启动”信号以后,在时钟线(SCL)为高电平状态时 数据线是稳定的,这时数据线的状态就要传送的数据。数据线(SDA)上的数据的改变 必须在时钟线为低电平期间完成,每位数据占用一个时钟脉冲。每个数传输都是由“启动” 信号开始,结束于“停止”信号。 (5)应答信号:每个正在接收数据的从机 EEPROM 在接到一个字节的数据后,通 常需要发出一个应答信号。而每个正在发送数据的 EEPROM 在发出一个字节的数据后, 通常需要接收一个应答信号。EEPROM 读写控制器必须产生一个与这个应答位相联系的 额外的时钟脉冲。在 EEPROM 的读操作中,EEPROM 读写控制器对 EEPROM 完成的最 后一个字节产生一个高的应答位,这叫做非应答信号,随后给 EEPROM 一个结束信号。 第四步,理解各个模块的代码。 首先介绍核心模块——EEPROM_WR 模块,这个模块就干一件事,严格控制 SDA 与 SCL 总线上的信号,使其满足 I2C 总线时序要求。这里要用到状态机来控制 SDA 与 SCL 上的信号, 所以难点就在于对状态机的编写。因为 EP2C8-2010 开发板采用的 EEPROM 是 AT24C02, 所以该状态机控制的 SCL 与 SDA 时序就要满足 AT24C02 的写入和读取格式。图 4、5 分别 是 AT24C02/4/8/16 字节写入帧格式和读指定地址存储单元的数据帧格式,简单分析一下字 节写入格式,如图 4 所示:第 1 位启动信号,接下来的第 2-9 位是控制字节写入,其中 2-5 位是固定的机器码 1010,6-8 位是页地址,第 10 位是 EEPROM 给出的应答信号 0,第 11-18 位是存储单元地址,19 位是 EEPROM 给出的应答信号 0,第 20-27 位是写入的数据,28 位 应答信号,29 位停止信号。而 AT24C02/4/8/16 的字节读取格式也是大同小异,先写入控制 字和存储地址,然后是启动信号与控制字节信号,这时,控制字节的第 8 位变为了 1(读取), 最后读取数据,并且在读取完毕后,主机将 SDA 拉高作为非应答信号。最后是停止位。 可能有人还是对这种字节写入与读取格式不明白,其实这种格式是别人定好的,我们所 需要的就是要让 SDA 与 SCL 的信号满足这种格式要求即可,下面我们来看夏老师的 EEPROM_WR 程序。 图 4. AT24C02/4/8/16 字节写入帧格式 图 5. AT24C02/4/8/16 读指定地址存储单元的数据帧格式 启动控制字节EEPROM存储单元地址数据停止写SDA线S 1 0 1 0 X X X应答应答应答启动控制字节EEPROM存储单元指定地址(n)写SDA线S 1 0 1 0 X X X应答应答启动S 1 0 1 0 X X X停止读控制字节数据(n)应答非应答P
北京至芯科技 FPGA 创新中心 01062670708 http://www.zxopen.com 首先是一堆输入输出、寄存器的定义,我们读程序的时候大可先不看这些,等到后面有 需要的时候再回过头来看这些定义,这里需要注意的是 SDA 与 DATA 的类型,都是 inout 型, SDA 我们很容易理解,因为主机和从机都会给 SDA 线上发信号,比如字节写入格式时,主机 先给 SDA 发了 1 个 8 位数据,然后作为应答位,从机要把 SDA 拉低,表示我已经接受到你 的信号了,在应答位时主机是不能操作 SDA 线的。然而 DATA 设定为 inout 型,我们可以看 到图 2,其实在字节写入格式时,DATA 是 signal 模块传输给 EEPROM_WR 模块,作为要写入 的数据。而在字节读取格式时, EEPROM_WR 通过 SDA 从 EEPROM 中读取数据,其实跟 DATA 是没有关系的,这里可以只将 DATA 设定为 input 型,设定为 inout 型是因为后续的程序会将 EEPROM_WR 读取到的数据发给 signal,与 signal 当初发送的数据进行比较,检验通信是否 正确。如果将比较数据这程序操作放在 EEPROM_WR 模块中,就可以将 DATA 设为 input。 提到 inout 类型,还得再多补充两句,inout,顾名思义,双向口既能作为输入又能作为 输出,可以节省管脚,在具体实现上一般是用三态门来实现,图 6 就是用三态门实现的 sda 总线的示意图。 图 6. sda 总线示意图 link_sda 和 out_flag 分别为 EEPROM_WR 和 EEPROM 控制三态门输出的开关,当 link_sda 打开,out_flag 关闭时,EEPROM_WR 向 SDA 传输数据,EEPROM 接收 SDA 的数据,此时, SDA 对于 EEPROM_WR 来说就是 output,对于 EEPROM 来说就是 input。反之,当 link_sda 关闭,out_flag 打开时,EEPROM_WR 通过 SDA 从从 EEPROM 中读取数据。当 link_sda 与 out_flag 都关闭时,SDA 就被置为高阻状态。 : 1'b0; : 1'b0; : 1'b0; ? head_buf[1] ? sh8out_buf[7] ? stop_buf[1] = (link_head) = (link_write) = (link_stop) = (sda1 | sda2 | sda3); = (link_sda) = (link_read) 下面来看程序中对于 SDA 和 DATA 三态门的描述。 assign sda1 assign sda2 assign sda3 assign sda4 assign SDA assign DATA 这是程序中应用三态门对双向口的实现,我们可以看到引入了多个开关,link_head、 link_write、link_stop、link_sda、link_read,其中 link_sda 和 link_read 这两个开关是最主要的, 当 link_sda =1,即 link_sda 打开时,将 sda4 输出,SDA=sda4。当 link_sda =0 时,将 SDA 置 为高阻。同时 sda4 也是由三个开关控制的,为 sda1、sda2、sda3 三个信号相或的结果,这 里是将 EEPROM_WR 的状态分为了三个基本的状态,分别是启动状态、写入状态以及停止状 ? sda4 ? data_from_rm : 1'bz; : 8'hzz; EEPROM_WRlink_sdaout_flagSDAEEPROM
北京至芯科技 FPGA 创新中心 01062670708 http://www.zxopen.com 态,这是根据 EEPROM 对 SDA 总线输出的值区分的。对照图 3、图 4,其实意思就是在启动 的时候给 SDA 一个下降沿,停止时给 SDA 一个上升沿,传输数据时给 SDA 一个有效数据。 引入这些开关是为了看得更加直观。当然,我们也可以不引入这么多开关,只需要 link_sda 和 link_read,在每次 link_sda 打开的时候我们都给 sda4 赋新值即可。 下面我们来看产生串行时钟 SCL 的程序: always @(negedge CLK) if(RESET) SCL <= 0; else SCL <= ~SCL; 这条产生 SCL 的程序也是十分巧妙的,本文中的程序是用 CLK 的下降沿来触发 SCL 时钟, 用 CLK 的上升沿来触发 SDA,这样 SDA 与 SCL 就相差了半个 SCL 周期,目的是为了满足 I2C 总线上的传输协议,具体见图 7. 图 7. SCL 与 SDA 基本时序关系 从图 7 中,我们可以看出,启动位占了一个 SCL 时钟周期,1 时刻拉高,2 时刻拉低, 保证在 SCL 高电平时 SDA 有下降沿,这样就是启动信号,同理,如果我们要传输数据时,3 时刻将传输的数据赋给 SDA,比如要传输的数是 1010,3 时刻将 SDA 拉高,直到完成 1 个 SCL 周期时再把下一个值 0 赋给 SDA。这样就是一个 SCL 与 SDA 基本的时序关系,我个人的理解 是这样的:首先,要保证图 3、4 中字节读写格式的每一位都要占一个 SCL 时钟周期。其次, 每个 CLK 上升沿时将数据传给 SDA,比如 1:SDA<=1;2:SDA<=0。这样才能保证时序的一 致性。 接下来我们看到的就是主状态机程序,由图 3、4 列出的字节读写格式,我们可以将读 写操作分为一共 11 个状态,状态图如下。 CLKSCLSDA启动位数据1数据0数据1数据0停止位123456798
北京至芯科技 FPGA 创新中心 01062670708 http://www.zxopen.com 图 8. 读写操作状态转移 我们依次来看到各个状态。 Idle: begin link_read <= NO; link_write <= NO; link_head <= NO; link_stop <= NO; link_sda <= NO; //空闲状态时 SDA 总线关闭 //如果有写信号 //写标志位置 1,主状态跳到准备状态 //如果有读信号 //读标志位置 1,主状态跳到准备状态 if(WR) begin WF <= 1; main_state <= Ready ; end else if(RD) begin else begin WF <= 0; RF <= 0; main_state <= Idle; end RF <= 1; main_state <= Ready ; end end Idle:空闲状态,主要就是说当我们的读写控制器收到读/写指令时,要跳到 Ready 状态, 准备往 SDA 上进行操作。(很多时候如果下板调试的时候读出没有信号的情况,我们可以考 虑在 signaltap 中检查 WR 和 RD 是否改变了,很有可能这两个输入信号输入错误导致进入不 空闲准备写开始写控制字写数据操作停止应答读开始读控制字读数据写入地址读或写信号输入启动读操作写操作停止输出应答信号
北京至芯科技 FPGA 创新中心 01062670708 http://www.zxopen.com 了状态机了) Ready: begin link_read link_write link_stop <= NO; <= NO; <= NO; //准备状态时 <= YES; <= YES; //link_head 打开,sda1=1 //link_sda 打开,sda=sda4=1 //link_head 打开时 sda1=head_buf[1] //给 stop_buf 赋值,操作停止时用 <= 2'b10; <= 2'b01; link_head link_sda head_buf[1:0] stop_buf[1:0] head_state FF ACK main_state end Ready:一些初始化赋值语句,此时 SDA<=head_buf[1],也就是将 SDA 拉高,准备启动。 这里的 ACK 应答信号并不是我们所说的 EEPROM 与 EEPROM_WR 传输数据中的应答信号, 如图 2 所示,这里的 ACK 是 EEPROM_WR 与 signal 模块传输数据的应答信号。 Write_start: <= head_begin; //给 shift_head 任务中的状态 //head_state 赋值,准备启动 //标志位 FF 置 0 <= 0; //应答信号 ACK 置 0 <= 0; <= Write_start;//主状态跳至开始 shift_head; else //此时 FF=0,进入 shift_head 任务 //跳转到 task shift_head 中执行 //shift_head 中将 FF 置 1,此时进入 else 语句 begin sh8out_buf[7:0] <= {1'b1,1'b0,1'b1,1'b0,ADDR[10:8],1'b0}; if(FF == 0) //给 sh8out_buf 赋值,1010 为机器码,ADDR[10:8]是页地址,0 代表写操作,sh8out_buf 就 //是要给 sda 赋的值,sda 取 sh8out_buf 的最高位。 link_head <= NO; link_write <= YES; FF <= 0; sh8out_state <= sh8out_bit6; //启动信号开关断开 //EEPROM 写操作开关打开 //将标志位 FF 置 0 //shift_head 任务中的状态从 sh8out_bit6 //开始,而不是从 sh8out_bit7 开始 main_state <= Ctrl_write; //主状态跳到 end //--------------------------- 输出启动信号任务 --------------------------------- task shift_head; begin begin casex(head_state) head_begin: if(!SCL) //shift_head 任务,输出启动信号 //即 scl 为高电平时,sda 由高电平变为低电平 //之前给 head_state 赋过值 head_begin,所以直接从 //head_begin 开始 //当 scl 为低电平时 //sda 置 1,同时状态 head_state 跳到 head_bit; link_write <= NO;
北京至芯科技 FPGA 创新中心 01062670708 http://www.zxopen.com link_sda head_state <= head_bit; <= YES; <= YES; link_head end else head_state <= head_begin; head_bit: //当 scl 不为低电平时,继续执行 head_begin if(SCL) begin //当 scl 为高电平时 <= 1; <= head_buf<<1; //head_buf 左移一位,sda 置 0 //标志位 FF 置 1 head_buf FF head_state <= head_end; else head_state <= head_bit; end //此时 FF=1,下一次 clk 上升沿来的时候,会跳 //出这个任务,执行 Write_start 中 else 的内容, //所以这个状态不曾进入过,可以删去 <= NO; link_write <= YES; head_end: if(!SCL) begin link_head end endcase end else head_state <= head_end; endtask Write_start:首先一开始 FF=0,就进入任务 shift_head,shift_head 任务就相当于图 7 的 时刻 1 与时刻 2 的操作,先将 sda 拉高,再拉低,并且通过 if(scl)来保证在 scl 高电平期 间,sda 有下降沿。而在 shift_head 这个任务里,我们可以发现除了时刻 1、2 外,还有一个 状态 head_end,其实我认为 head_end 是可以省去的,启动时我们要做的就是两件事:1 时 刻 sda 拉高,2 时刻 sda 拉低,进入控制字节写入环节。并且在由于时刻 2 中 FF 置 1,所以 从来没有进入过 head_end 这个状态,读者在 modelsim 仿真中也可以进行验证。 这里还要多说一句的是那个 else 语句,此时应该图 7 中的时刻 3 了,此时 sh8out_buf[7]=1, 同时 link_sda 打开,意味着我们已经将 sda 置 1 了,这与 sh8out_state<= sh8out_bit6 这条语 句要结合起来看,要传输的 8 个数据我们已经给发送了一个了,下面就要从第二个数据开始 发,所以下一步要做的是在图 7 中的时刻 4 发送第二个数。这也就是为什么 sh8out_state 不 从 sh8out_bit7 开始读第一个数了。在这里读者可以按照时序要求画出 clk、scl、sda 的时序 图,就像图 7 所示的那样,用时序图作参考来理解代码,这样有助于掌握。 //这个状态是要写入 8 位的控制字 //shift8_out 实现了并行转串行的功能 //将 sh8out_buf 的 8 位数据一位一位地通过 sda 输出 Ctrl_write: if(FF ==0) shift8_out; else
分享到:
收藏