一、前置知识
1.1 EEPROM
EEPROM (Electrically Erasable Progammable Read Only Memory,E2PROM)即电可擦除可编程只读存储器,是一种常用的非易失性存储器,掉电数据不丢失。开发板上使用的是ATMEL生产的AT24C64芯片。AT24C64 存储容量为 64Kbit,内部分成 256 页,每页 32 字节, 共有 8192 个字节,且其读写操作都是以字节为基本单位。
AT24C64 采用两线串行接口的双向数据传输协议——I2C 协议实现读写操作。I2C 即 Inter-Integrated Circuit(集成电路总线),是由 Philips 半导体公司(现在的 NXP)在八十年代初设计出来的一种简单、双向、二线制总线标准。
对于 AT24C64 而言,其器件地址为 1010 加 3 位的可编程地址,3 位可编程地址由器件上的 3 个管脚A2、 A1、A0的硬件连接决定。当硬件电路上分别将这三个管脚连接到 GND 或 VCC 时,就可以设置不同的可编程地址。
1.2 IIC
1.2.1 IIC概述
I2C 总线由数据线 SDA 和时钟线 SCL 构成通信线路,既可用于发送数据,也可接收数据。在主控与被控 IC 之间可进行双向数据传送,数据的传输速率在标准模式下可达100kbit/s,在快速模式下可达 400kbit/s,在高速模式下可达 3.4Mbit/s,各种被控器件均并联在总线上,通过器件地址(SLAVE ADDR)识别。
下图是开发板上使用IIC总线设备的物理拓扑结构:
由于 I2C 器件一般采用开漏结构与总线相连,所以 I2C_SCL 和 I2C_SDA 均需接上拉电阻。
1.2.2 IIC时序
- 空闲状态:在IIC开始通信传输数据之前,时钟线SCL和数据线SDA由于上拉电阻,均处于拉高状态,此时I2C总线空闲。
- 起始信号:若主机想要传输数据,需要在SCL为高电平时,将SDA数据线拉低,产生一个起始信号。
- 数据传输:主机检测到起始信号后,开始传输数据。
- 停止信号:数据传输完成后,主机需要产生一个停止信号。停止信号是在SCL为高电平时,SDA由低电平跳变到高电平。

具体传输时序图:
- 主机产生起始信号后,在第1个周期开始传输数据。
- SCL为高电平时,SDA状态保持以便采样数据;SCL为低电平时允许SDA电平改变。
- 在第8个周期结束后,主机释放SDA以使从机产生应答信号。
- 在第9个周期,SCL高电平时,如果SDA被从机拉低,说明从机产生应答,本次通信有效;如果SDA没有被从机拉低,说明从机未产生应答,本次通信无效。
- 若从机产生有效应答信号,从机会在第9个周期末释放SDA,以使主机继续传输数据。
- 如果主机发送停止信号,则通信过程停止;若主机发送起始信号,则继续进行下一轮通信。
- 数据以8bit即1字节为单位进行传输,最先传输的为数据最高位。
1.2.3 传输过程
1. 器件地址
在进行数据传输时,主机首先向总线上发出开始信号,对应开始位 S,然后按照从高到低的位序发送器件地址,一般为 7bit,第 8bit 位为读写控制位 R/W,该位为 0 时表示主机对从机进行写操作,当该位为 1 时表示主机对从机进行读操作,然后接收从机响应。
发送完第一个字节(7位器件地址和1位读写控制位)并收到从机正确的应答后就开始发送字地址(Word Address)。2. 字地址
字地址就是IC内部寄存器的地址,由于IC存储容量的大小不同,字地址的长度也不同。比如AT24C02的存储容量是2Kbit即256Byte,寻址是以字节为单位的,所以256字节需要8位寻址的字地址就够了,即字地址为8位(1个字节);但是AT24C64存储容量为64Kb即8KB,需要13位地址来做寻址,即字地址为13位(至少2个字节)。因此就有了单字节字地址和双字节字地址。
单字节字地址:
双字节字地址:
3. 写数据
主机发送字地址,从机正确应答后,就可以根据器件地址的读写位R/W进行数据读写操作了。如果是写操作R/W=0,主机就发送写的数据。写数据分为单次写和连续写。
- 单次写是指在主机写完1字节数据后,主机发送停止信号,结束输出传输。
- 连续写是指在主机写完1字节数据后,主机继续发送下1个字节的数据,多次以1字节为单位发送,最后再发一个停止信号结束。
4. 读数据
主机发送字地址,从机正确应答后,就可以根据器件地址的读写位R/W进行数据读写操作了。如果是读操作R/W=1,从机就开始发送数据,主机读从机的数据。读数据分为当前地址读、随机读、连续读。
- 当前地址读是指在一次读写操作后进行一次读操作。由于I2C在读写操作后其内部地址指针自动加一,所以当前地址读到的数据是下一个地址的数据,即若上次读写的地址是02,则当前读所读的地址是03地址。

- 随机读是指首先主机在发送器件地址时,使用写操作,接着在发送完字地址之后再第二次发送起始信号,器件地址使用读操作,然后接着进行一次读数据。这样做是为了在第一次写操作之后,让I2C指针指向器件内部我们所需要的地址,然后在该地址进行数据读取。上面第一次的写操作成为需写操作(Dummy Write)。

- 连续读是指连续读几个字节的数据。对于当前地址读和随机读,都是只读1字节数据,在读完1字节数据后,主机发送不应答信号1,主机从送停止信号结束通信。对于连续读,在每次读完1字节数据后,主机发送应答信号0,主机继续读,直到主机发送不应答的1,主机发送停止信号结束通信。
5. 起始和停止信号
- 起始信号:SCL高电平时,SDA由高拉低
-
二、硬件设计
开发板上使用的AT24C64引脚及功能描述如下:

A0、A1、A2:器件地址选择位
- GND、VCC:接地,电源
- SDA、SCL:I2C数据线和地址线
- WP:写保护,高电平有效。高电平时只能读数据不能写入,低电平时可写入数据。
AT24C64原理图如下:
开发板上只有一个AT24C64,其A0A1A2都接地,所以器件地址为:1010_000=0x50
管脚分配如下:

inout sda;assign sda = sda_dir ? sda_out : 1'bz;assign sda_in = sda;
三、代码编写
3.1 各模块接口
3.1.1 I2C驱动模块

- clk:系统时钟
- rst_n:系统复位
- i2c_exec:I2C触发执行信号,高电平开始执行I2C读写操作。
- bit_ctr:字地址位控制(16b/8b),1为16bit字地址;0为8bit字地址
- i2c_rh_wl:I2C读写控制信号,1读0写
- i2c_addr:I2C器件内地址
- i2c_data_w:I2C要写的数据
- i2c_data_r:I2C读出的数据
- i2c_done:I2C一次操作完成,高电平写入完成
- i2c_ack:I2C应答标志 0:应答 1:未应答
- scl:I2C的SCL时钟信号
- sda:I2C的SDA信号
-
3.1.2 读写测试模块

clk:由I2C驱动模块产生的时钟信号
- rst_n:系统复位信号
- i2c_ack:I2C应答标志
- i2c_done:I2C一次操作完成标志
- i2c_exec:I2C触发执行信号
- i2c_rh_wl:I2C读写控制信号
- i2c_data_r[7:0]:I2C读出的数据
- i2c_data_w[7:0]:I2C要写的数据
- i2c_addr[15:0]:I2C器件内的地址
- rw_done:E2PROM读写测试完成标志
-
3.1.3 LED灯模块

clk:时钟信号(I2C驱动时钟)
- rst_n:复位信号
- rw_done:E2PROM读写测试完成
- rw_result:E2PROM读写测试结果
-
3.2 模块代码编写
3.2.1 读写测试模块
I2C驱动代码较复杂,假设I2C驱动代码已经编写完成,先来编写I2C读写测试模块。示例代码使用了四状态的状态机来实现功能。状态0和1用来向E2PROM写入数据;状态2和3用来读取E2PROM的数据并验证数据正确性。
状态0:写数据和判断所有写数据完成
- 状态1:等待本次写数据完成,改变下一轮写数据和写地址
- 状态2:执行读数据并条状到状态3
状态3:等待本次读数据完成,验证数据正确性,向用户接口输出验证结果 ```verilog module eeprom_rw( input clk , // I2C驱动提供的时钟信号 input rst_n , // 系统复位
input i2c_ack , // I2C应答标志 input i2c_done , // I2C一次操作完成标志 output reg i2c_exec , // I2C触发执行信号 output reg i2c_rh_wl , // I2C读写控制信号 input [ 7:0] i2c_data_r , // I2C读出的数据 output reg [ 7:0] i2c_data_w , // I2C要写的数据 output reg [15:0] i2c_addr , // I2C器件内的地址
output reg rw_done , // E2PROM读写测试完成标志 output reg rw_result // E2PROM读写测试结果 );
parameter MAX_ADDR = 9’d256; // 测试地址0~255 parameter WRITE_WAIT_TIME = 13’d5000; // I2C模块输出频率4us,
reg [ 1:0] flow_cnt; // 状态流切换控制 reg [12:0] wait_cnt; // 写等待倒计时
always @(posedge clk or negedge rst_n) begin if(!rst_n) begin i2c_exec <= 1’b0; i2c_rh_wl <= 1’b0; i2c_data_w <= 8’d0; i2c_addr <= 16’d0; rw_done <= 1’b0; rw_result <= 1’b0; flow_cnt <= 2’d0; wait_cnt <= 13’d0; end else begin i2c_exec <= 1’b0; case(flow_cnt) 2’d0: begin // 写数据 wait_cnt <= wait_cnt + 1’b1; if(wait_cnt == WRITE_WAIT_TIME) begin wait_cnt <= 1’b0; if(i2c_addr == MAX_ADDR) begin flow_cnt <= 2’d2; // 跳转到读状态 i2c_addr <= 16’d0; i2c_rh_wl <= 1’b1; end else begin // 准备写入数据 i2c_exec <= 1’b1; flow_cnt <= 2’d1; end end end 2’d1: begin // 等待一轮写入数据完成,准备下一轮写入的数据 if(i2c_done) begin flow_cnt <= 2’d0; i2c_data_w <= i2c_data_w + 1’b1; i2c_addr <= i2c_addr + 1’b1; end end 2’d2: begin // 读数据 i2c_exec <= 1’b1; flow_cnt <= 2’d3; end 2’d3: begin // 等待读数据完成,验证数据正确性 if(i2c_done) begin if((i2c_data_r != i2c_addr[7:0]) || (i2c_ack == 1)) begin rw_done <= 1’b1; // 验证出错 rw_result <= 1’b0; end else if(i2c_addr == MAX_ADDR-1) begin // 全部验证完成 rw_done <= 1’b1; rw_result <= 1’b1; end else begin // 下一轮数据读取和验证 i2c_addr <= i2c_addr + 1’b1; flow_cnt <= 2’d2; end end end default :; endcase end end
endmodule
<a name="iKUSJ"></a>#### 3.2.2 LED灯控制模块上述编写的读写测试模块,当全部0~255地址中的数据全部读取并验证无误后,读写测试结束,读写测试模块在状态3不断循环,由于i2c_done是脉冲信号,而rx_done被清零,所以rx_done会输出脉冲1,rx_result一直输出1。如果存在数据验证出错,则同样在状态3循环,rx_done会输出脉冲信号1而rx_result则一直输出0。```verilogmodule led_alarm(input clk,input rst_n,input rx_done,input rx_result,output reg led);parameter BILNK_TIME = 25_000_000;reg rx_done_flag;reg [24:0] bilnk_cnt;always @(posedge clk or negedge rst_n) beginif(!rst_n)rx_done_flag <= 1'b0;else if(rx_done)rx_done_flag <= 1'b1;endalways @(posedge clk or negedge rst_n) beginif(!rst_n) beginled <= 1'b0;bilnk_cnt <= 25'd0;endelse beginif(rx_done_flag) beginif(rx_result == 1'b0) begin // 验证失败bilnk_cnt <= bilnk_cnt + 1'b1;if(bilnk_cnt == BILNK_TIME - 1) beginled <= ~led;bilnk_cnt <= 25'd0;endendelseled <= 1'b1; // 验证成功endelse // 还未验证完成led <= 1'b0;endendendmodule
3.2.3 I2C驱动模块
正点原子教程使用8状态的状态机实现了I2C驱动模块,暂时先使用正点原子提供的模板,以后再自己写。。
//****************************************Copyright (c)***********************************////原子哥在线教学平台:www.yuanzige.com//技术支持:www.openedv.com//淘宝店铺:http://openedv.taobao.com//关注微信公众平台微信号:"正点原子",免费获取ZYNQ & FPGA & STM32 & LINUX资料。//版权所有,盗版必究。//Copyright(C) 正点原子 2018-2028//All rights reserved//----------------------------------------------------------------------------------------// File name: i2c_dri// Last modified Date: 2019/05/04 9:19:08// Last Version: V1.0// Descriptions: IIC驱动////----------------------------------------------------------------------------------------// Created by: 正点原子// Created date: 2019/05/04 9:19:08// Version: V1.0// Descriptions: The original version////----------------------------------------------------------------------------------------//****************************************************************************************//module i2c_dri#(parameter SLAVE_ADDR = 7'b1010000 , //EEPROM从机地址parameter CLK_FREQ = 26'd50_000_000, //模块输入的时钟频率parameter I2C_FREQ = 18'd250_000 //IIC_SCL的时钟频率)(input clk ,input rst_n ,//i2c interfaceinput i2c_exec , //I2C触发执行信号input bit_ctrl , //字地址位控制(16b/8b)input i2c_rh_wl , //I2C读写控制信号input [15:0] i2c_addr , //I2C器件内地址input [ 7:0] i2c_data_w , //I2C要写的数据output reg [ 7:0] i2c_data_r , //I2C读出的数据output reg i2c_done , //I2C一次操作完成output reg i2c_ack , //I2C应答标志 0:应答 1:未应答output reg scl , //I2C的SCL时钟信号inout sda , //I2C的SDA信号//user interfaceoutput reg dri_clk //驱动I2C操作的驱动时钟);//localparam definelocalparam 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 definereg 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 [ 7:0] data_wr_t ; //I2C需写的数据的临时寄存reg [ 9:0] clk_cnt ; //分频时钟计数//wire definewire sda_in ; //SDA输入信号wire [8:0] clk_divide ; //模块驱动时钟的分频系数//*****************************************************//** main code//*****************************************************//SDA控制assign sda = sda_dir ? sda_out : 1'bz; //SDA数据输出或高阻assign sda_in = sda ; //SDA数据输入assign clk_divide = (CLK_FREQ/I2C_FREQ) >> 2'd2;//模块驱动时钟的分频系数//生成I2C的SCL的四倍频率的驱动时钟用于驱动i2c的操作always @(posedge clk or negedge rst_n) beginif(!rst_n) begindri_clk <= 1'b0;clk_cnt <= 10'd0;endelse if(clk_cnt == clk_divide[8:1] - 1'd1) beginclk_cnt <= 10'd0;dri_clk <= ~dri_clk;endelseclk_cnt <= clk_cnt + 1'b1;end//(三段式状态机)同步时序描述状态转移always @(posedge dri_clk or negedge rst_n) beginif(!rst_n)cur_state <= st_idle;elsecur_state <= next_state;end//组合逻辑判断状态转移条件always @(*) beginnext_state = st_idle;case(cur_state)st_idle: begin //空闲状态if(i2c_exec) beginnext_state = st_sladdr;endelsenext_state = st_idle;endst_sladdr: beginif(st_done) beginif(bit_ctrl) //判断是16位还是8位字地址next_state = st_addr16;elsenext_state = st_addr8 ;endelsenext_state = st_sladdr;endst_addr16: begin //写16位字地址if(st_done) beginnext_state = st_addr8;endelse beginnext_state = st_addr16;endendst_addr8: begin //8位字地址if(st_done) beginif(wr_flag==1'b0) //读写判断next_state = st_data_wr;elsenext_state = st_addr_rd;endelse beginnext_state = st_addr8;endendst_data_wr: begin //写数据(8 bit)if(st_done)next_state = st_stop;elsenext_state = st_data_wr;endst_addr_rd: begin //写地址以进行读数据if(st_done) beginnext_state = st_data_rd;endelse beginnext_state = st_addr_rd;endendst_data_rd: begin //读取数据(8 bit)if(st_done)next_state = st_stop;elsenext_state = st_data_rd;endst_stop: begin //结束I2C操作if(st_done)next_state = st_idle;elsenext_state = st_stop ;enddefault: next_state= st_idle;endcaseend//时序电路描述状态输出always @(posedge dri_clk or negedge rst_n) begin//复位初始化if(!rst_n) beginscl <= 1'b1;sda_out <= 1'b1;sda_dir <= 1'b1;i2c_done <= 1'b0;i2c_ack <= 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;endelse beginst_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) beginwr_flag <= i2c_rh_wl ;addr_t <= i2c_addr ;data_wr_t <= i2c_data_w;i2c_ack <= 1'b0;endendst_sladdr: begin //写地址(器件地址和字地址)case(cnt)7'd1 : sda_out <= 1'b0; //开始I2C7'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: beginsda_dir <= 1'b0;sda_out <= 1'b1;end7'd37: scl <= 1'b1;7'd38: begin //从机应答st_done <= 1'b1;if(sda_in == 1'b1) //高电平表示未应答i2c_ack <= 1'b1; //拉高应答标志位end7'd39: beginscl <= 1'b0;cnt <= 1'b0;enddefault : ;endcaseendst_addr16: begincase(cnt)7'd0 : beginsda_dir <= 1'b1 ;sda_out <= addr_t[15]; //传送字地址end7'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: beginsda_dir <= 1'b0;sda_out <= 1'b1;end7'd33: scl <= 1'b1;7'd34: begin //从机应答st_done <= 1'b1;if(sda_in == 1'b1) //高电平表示未应答i2c_ack <= 1'b1; //拉高应答标志位end7'd35: beginscl <= 1'b0;cnt <= 1'b0;enddefault : ;endcaseendst_addr8: begincase(cnt)7'd0: beginsda_dir <= 1'b1 ;sda_out <= addr_t[7]; //字地址end7'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: beginsda_dir <= 1'b0;sda_out <= 1'b1;end7'd33: scl <= 1'b1;7'd34: begin //从机应答st_done <= 1'b1;if(sda_in == 1'b1) //高电平表示未应答i2c_ack <= 1'b1; //拉高应答标志位end7'd35: beginscl <= 1'b0;cnt <= 1'b0;enddefault : ;endcaseendst_data_wr: begin //写数据(8 bit)case(cnt)7'd0: beginsda_out <= data_wr_t[7]; //I2C写8位数据sda_dir <= 1'b1;end7'd1 : scl <= 1'b1;7'd3 : scl <= 1'b0;7'd4 : sda_out <= data_wr_t[6];7'd5 : scl <= 1'b1;7'd7 : scl <= 1'b0;7'd8 : sda_out <= data_wr_t[5];7'd9 : scl <= 1'b1;7'd11: scl <= 1'b0;7'd12: sda_out <= data_wr_t[4];7'd13: scl <= 1'b1;7'd15: scl <= 1'b0;7'd16: sda_out <= data_wr_t[3];7'd17: scl <= 1'b1;7'd19: scl <= 1'b0;7'd20: sda_out <= data_wr_t[2];7'd21: scl <= 1'b1;7'd23: scl <= 1'b0;7'd24: sda_out <= data_wr_t[1];7'd25: scl <= 1'b1;7'd27: scl <= 1'b0;7'd28: sda_out <= data_wr_t[0];7'd29: scl <= 1'b1;7'd31: scl <= 1'b0;7'd32: beginsda_dir <= 1'b0;sda_out <= 1'b1;end7'd33: scl <= 1'b1;7'd34: begin //从机应答st_done <= 1'b1;if(sda_in == 1'b1) //高电平表示未应答i2c_ack <= 1'b1; //拉高应答标志位end7'd35: beginscl <= 1'b0;cnt <= 1'b0;enddefault : ;endcaseendst_addr_rd: begin //写地址以进行读数据case(cnt)7'd0 : beginsda_dir <= 1'b1;sda_out <= 1'b1;end7'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: beginsda_dir <= 1'b0;sda_out <= 1'b1;end7'd37: scl <= 1'b1;7'd38: begin //从机应答st_done <= 1'b1;if(sda_in == 1'b1) //高电平表示未应答i2c_ack <= 1'b1; //拉高应答标志位end7'd39: beginscl <= 1'b0;cnt <= 1'b0;enddefault : ;endcaseendst_data_rd: begin //读取数据(8 bit)case(cnt)7'd0: sda_dir <= 1'b0;7'd1: begindata_r[7] <= sda_in;scl <= 1'b1;end7'd3: scl <= 1'b0;7'd5: begindata_r[6] <= sda_in ;scl <= 1'b1 ;end7'd7: scl <= 1'b0;7'd9: begindata_r[5] <= sda_in;scl <= 1'b1 ;end7'd11: scl <= 1'b0;7'd13: begindata_r[4] <= sda_in;scl <= 1'b1 ;end7'd15: scl <= 1'b0;7'd17: begindata_r[3] <= sda_in;scl <= 1'b1 ;end7'd19: scl <= 1'b0;7'd21: begindata_r[2] <= sda_in;scl <= 1'b1 ;end7'd23: scl <= 1'b0;7'd25: begindata_r[1] <= sda_in;scl <= 1'b1 ;end7'd27: scl <= 1'b0;7'd29: begindata_r[0] <= sda_in;scl <= 1'b1 ;end7'd31: scl <= 1'b0;7'd32: beginsda_dir <= 1'b1;sda_out <= 1'b1;end7'd33: scl <= 1'b1;7'd34: st_done <= 1'b1; //非应答7'd35: beginscl <= 1'b0;cnt <= 1'b0;i2c_data_r <= data_r;enddefault : ;endcaseendst_stop: begin //结束I2C操作case(cnt)7'd0: beginsda_dir <= 1'b1; //结束I2Csda_out <= 1'b0;end7'd1 : scl <= 1'b1;7'd3 : sda_out <= 1'b1;7'd15: st_done <= 1'b1;7'd16: begincnt <= 1'b0;i2c_done <= 1'b1; //向上层模块传递I2C结束信号enddefault : ;endcaseendendcaseendendendmodule
3.2.4 顶层模块
顶层模块将读写测试模块、I2C驱动模块和LED控制模块连接起来即可。
module iic_eeprom(input sys_clk,input sys_rst_n,// IIC interfaceoutput iic_scl,output iic_sda,// User interfaceoutput led);parameter BIT_CTRL = 1'b1; // 字地址位控制参数parameter SLAVE_ADDR = 7'b1010000; // EEPROM从机地址parameter CLK_FREQ = 26'd50_000_000; // 模块输入的时钟频率parameter I2C_FREQ = 18'd250_000; // IIC_SCL的时钟频率parameter BILNK_TIME = 17'd125_000; // LED闪烁时间参数wire i2c_exec ;wire i2c_rh_wl ;wire [15:0] i2c_addr ;wire [ 7:0] i2c_data_w ;wire [ 7:0] i2c_data_r ;wire i2c_done ;wire i2c_ack ;wire dri_clk ;wire rw_done ;wire rw_result ;i2c_dri #(.SLAVE_ADDR (SLAVE_ADDR),.CLK_FREQ (CLK_FREQ ),.I2C_FREQ (I2C_FREQ )) u_i2c_dri(.clk (sys_clk ),.rst_n (sys_rst_n ),.i2c_exec (i2c_exec ),.bit_ctrl (BIT_CTRL ),.i2c_rh_wl (i2c_rh_wl ),.i2c_addr (i2c_addr ),.i2c_data_w (i2c_data_w),.i2c_data_r (i2c_data_r),.i2c_done (i2c_done ),.i2c_ack (i2c_ack ),.scl (iic_scl ),.sda (iic_sda ),.dri_clk (dri_clk ));eeprom_rw u_eeprom_rw(.clk (dri_clk ),.rst_n (sys_rst_n ),.i2c_ack (i2c_ack ),.i2c_done (i2c_done ),.i2c_exec (i2c_exec ),.i2c_rh_wl (i2c_rh_wl ),.i2c_data_r (i2c_data_r),.i2c_data_w (i2c_data_w),.i2c_addr (i2c_addr ),.rw_done (rw_done ),.rw_result (rw_result ));led_alarm #(.BILNK_TIME (BILNK_TIME))u_led_alarm(.clk (dri_clk ),.rst_n (sys_rst_n ),.rx_done (rw_done ),.rx_result (rw_result ),.led (led ));endmodule
3.2.5 TCL脚本
只需定义顶层模块中CLK、RST_N、SCL、SDA和LED的引脚即可。
package require ::quartus::project#system clock:50Mhzset_location_assignment PIN_M2 -to sys_clk#system resetset_location_assignment PIN_M1 -to sys_rst_n#ledset_location_assignment PIN_D11 -to led#I2C(EEPROM/ALS&PS/RTC/AUDIO)set_location_assignment PIN_D8 -to iic_sclset_location_assignment PIN_C8 -to iic_sda
程序烧录
结果出现错误,数据验证出错,LED闪烁。
另外由于其余三个LED没有约束,一直为亮的状态。在Assignments→Device→Device and Pin Options→unused pin页面,把未使用的引脚设置为As input tri-stated。重新编译之后再烧录就好了。
数据验证失败的问题还在调试中。。。


