一、前置知识
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
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25873077/1651824690105-a5b433bf-39ec-45ae-80eb-70431d065869.png#clientId=ud4d9d2c3-1449-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=600&id=u2486bafe&margin=%5Bobject%20Object%5D&name=image.png&originHeight=600&originWidth=771&originalType=binary&ratio=1&rotation=0&showTitle=false&size=35572&status=done&style=none&taskId=u95e81df1-f5b5-4947-b8f7-d7bbfdcf385&title=&width=771)
<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。
```verilog
module 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) begin
if(!rst_n)
rx_done_flag <= 1'b0;
else if(rx_done)
rx_done_flag <= 1'b1;
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
led <= 1'b0;
bilnk_cnt <= 25'd0;
end
else begin
if(rx_done_flag) begin
if(rx_result == 1'b0) begin // 验证失败
bilnk_cnt <= bilnk_cnt + 1'b1;
if(bilnk_cnt == BILNK_TIME - 1) begin
led <= ~led;
bilnk_cnt <= 25'd0;
end
end
else
led <= 1'b1; // 验证成功
end
else // 还未验证完成
led <= 1'b0;
end
end
endmodule
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 interface
input 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 interface
output reg dri_clk //驱动I2C操作的驱动时钟
);
//localparam define
localparam 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 define
reg 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 define
wire 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) begin
if(!rst_n) begin
dri_clk <= 1'b0;
clk_cnt <= 10'd0;
end
else if(clk_cnt == clk_divide[8:1] - 1'd1) begin
clk_cnt <= 10'd0;
dri_clk <= ~dri_clk;
end
else
clk_cnt <= clk_cnt + 1'b1;
end
//(三段式状态机)同步时序描述状态转移
always @(posedge dri_clk or negedge rst_n) begin
if(!rst_n)
cur_state <= st_idle;
else
cur_state <= next_state;
end
//组合逻辑判断状态转移条件
always @(*) begin
next_state = st_idle;
case(cur_state)
st_idle: begin //空闲状态
if(i2c_exec) begin
next_state = st_sladdr;
end
else
next_state = st_idle;
end
st_sladdr: begin
if(st_done) begin
if(bit_ctrl) //判断是16位还是8位字地址
next_state = st_addr16;
else
next_state = st_addr8 ;
end
else
next_state = st_sladdr;
end
st_addr16: begin //写16位字地址
if(st_done) begin
next_state = st_addr8;
end
else begin
next_state = st_addr16;
end
end
st_addr8: begin //8位字地址
if(st_done) begin
if(wr_flag==1'b0) //读写判断
next_state = st_data_wr;
else
next_state = st_addr_rd;
end
else begin
next_state = st_addr8;
end
end
st_data_wr: begin //写数据(8 bit)
if(st_done)
next_state = st_stop;
else
next_state = st_data_wr;
end
st_addr_rd: begin //写地址以进行读数据
if(st_done) begin
next_state = st_data_rd;
end
else begin
next_state = st_addr_rd;
end
end
st_data_rd: begin //读取数据(8 bit)
if(st_done)
next_state = st_stop;
else
next_state = st_data_rd;
end
st_stop: begin //结束I2C操作
if(st_done)
next_state = st_idle;
else
next_state = st_stop ;
end
default: next_state= st_idle;
endcase
end
//时序电路描述状态输出
always @(posedge dri_clk or negedge rst_n) begin
//复位初始化
if(!rst_n) begin
scl <= 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;
end
else begin
st_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) begin
wr_flag <= i2c_rh_wl ;
addr_t <= i2c_addr ;
data_wr_t <= i2c_data_w;
i2c_ack <= 1'b0;
end
end
st_sladdr: begin //写地址(器件地址和字地址)
case(cnt)
7'd1 : sda_out <= 1'b0; //开始I2C
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'b0; //0:写
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd37: scl <= 1'b1;
7'd38: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd39: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_addr16: begin
case(cnt)
7'd0 : begin
sda_dir <= 1'b1 ;
sda_out <= addr_t[15]; //传送字地址
end
7'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: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_addr8: begin
case(cnt)
7'd0: begin
sda_dir <= 1'b1 ;
sda_out <= addr_t[7]; //字地址
end
7'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: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_data_wr: begin //写数据(8 bit)
case(cnt)
7'd0: begin
sda_out <= data_wr_t[7]; //I2C写8位数据
sda_dir <= 1'b1;
end
7'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: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_addr_rd: begin //写地址以进行读数据
case(cnt)
7'd0 : begin
sda_dir <= 1'b1;
sda_out <= 1'b1;
end
7'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: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd37: scl <= 1'b1;
7'd38: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd39: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_data_rd: begin //读取数据(8 bit)
case(cnt)
7'd0: sda_dir <= 1'b0;
7'd1: begin
data_r[7] <= sda_in;
scl <= 1'b1;
end
7'd3: scl <= 1'b0;
7'd5: begin
data_r[6] <= sda_in ;
scl <= 1'b1 ;
end
7'd7: scl <= 1'b0;
7'd9: begin
data_r[5] <= sda_in;
scl <= 1'b1 ;
end
7'd11: scl <= 1'b0;
7'd13: begin
data_r[4] <= sda_in;
scl <= 1'b1 ;
end
7'd15: scl <= 1'b0;
7'd17: begin
data_r[3] <= sda_in;
scl <= 1'b1 ;
end
7'd19: scl <= 1'b0;
7'd21: begin
data_r[2] <= sda_in;
scl <= 1'b1 ;
end
7'd23: scl <= 1'b0;
7'd25: begin
data_r[1] <= sda_in;
scl <= 1'b1 ;
end
7'd27: scl <= 1'b0;
7'd29: begin
data_r[0] <= sda_in;
scl <= 1'b1 ;
end
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b1;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: st_done <= 1'b1; //非应答
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
i2c_data_r <= data_r;
end
default : ;
endcase
end
st_stop: begin //结束I2C操作
case(cnt)
7'd0: begin
sda_dir <= 1'b1; //结束I2C
sda_out <= 1'b0;
end
7'd1 : scl <= 1'b1;
7'd3 : sda_out <= 1'b1;
7'd15: st_done <= 1'b1;
7'd16: begin
cnt <= 1'b0;
i2c_done <= 1'b1; //向上层模块传递I2C结束信号
end
default : ;
endcase
end
endcase
end
end
endmodule
3.2.4 顶层模块
顶层模块将读写测试模块、I2C驱动模块和LED控制模块连接起来即可。
module iic_eeprom(
input sys_clk,
input sys_rst_n,
// IIC interface
output iic_scl,
output iic_sda,
// User interface
output 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:50Mhz
set_location_assignment PIN_M2 -to sys_clk
#system reset
set_location_assignment PIN_M1 -to sys_rst_n
#led
set_location_assignment PIN_D11 -to led
#I2C(EEPROM/ALS&PS/RTC/AUDIO)
set_location_assignment PIN_D8 -to iic_scl
set_location_assignment PIN_C8 -to iic_sda
程序烧录
结果出现错误,数据验证出错,LED闪烁。
另外由于其余三个LED没有约束,一直为亮的状态。在Assignments
→Device
→Device and Pin Options
→unused pin
页面,把未使用的引脚设置为As input tri-stated
。重新编译之后再烧录就好了。
数据验证失败的问题还在调试中。。。