目录
一、EEPROM介绍
二、I2C协议
2.1 简介
2.2 I2C总线协议
2.2.1 开始与停止条件
2.2.2 地址帧
2.2.3 数据帧
三、EEPROM 24C02芯片
3.1 写时序
3.2 读时序
四、核心代码
4.1 I2C协议
4.1.1 逻辑图
4.1.2 相关代码
4.2 EEPROM读写控制模块
4.2.1 原理框图
4.2.2 相关代码
五、总结
EEPROM(Electrically Erasable Programmable Read-Only Memory,电可擦除可编程只读存储器)是一种非易失性存储器,它可以在不需要额外的设备支持的情况下被电子设备重新编程和擦除。
EEPROM最明显的特点是它可以被电子设备内部的电路电子擦除和编程,而不需要使用外部设备或特殊的擦除/编程方法。这使得EEPROM非常方便和灵活,适用于各种应用,例如存储系统配置数据、校准值、设备参数等。
EEPROM的存储单元由晶体管和电容器组成,每个存储单元可以存储一个比特(1或0)。为了编程和擦除EEPROM,一个存储单元的电荷状态会被改变,进而改变其保存的信息。编程通常是将存储单元的电荷增加到一个特定的电压,而擦除则是将存储单元的电荷完全清除。
由于EEPROM是非易失性存储器,意味着它的数据在断电后仍然保持。这使得EEPROM非常适用于存储需要长期保存的重要信息,例如系统配置和设备的唯一识别码。
EEPROM在计算机、消费电子产品和嵌入式系统中被广泛使用。它提供了一种可编程的存储解决方案,为电子设备提供了灵活性和可靠性。此外,EEPROM还具有较快的读取速度和较高的擦写/编程寿命,使得它成为许多应用中的理想选择。
IIC(Inter-Integrated Circuit),也被称为I2C(Inter-Integrated Circuit),是一种串行通信协议,用于在芯片和芯片之间进行数据传输。该协议由飞利浦半导体(现在是恩智浦半导体)在上世纪80年代开发,目的是在芯片之间实现简单、高效的通信。
I2C协议使用两根线作为物理通信通道:Serial Data Line(SDA)和Serial Clock Line(SCL)。SDA线用于传输数据,而SCL线用于传输时钟信号。这两根线共享与多个设备连接的总线,因此I2C支持多主机和多从机的通信。
I2C协议中的一个重要概念是主机和从机。主机是启动和控制通信的设备,而从机则被动地响应主机的命令并传输数据。通信过程中,主机向从机发送地址和命令,然后接收或发送数据。
I2C协议支持多种数据传输模式,包括标准模式(最高传输速率为100 kbit/s)、快速模式(最高传输速率为400 kbit/s)和高速模式(最高传输速率为3.4 Mbit/s)。传输速率通常是由设备的能力和总线负载决定的。
由于其简单灵活的设计和广泛的设备支持,I2C已被广泛应用于各种领域,包括电子设备、嵌入式系统、传感器、存储器和显示器等。它为芯片和模块之间的通信提供了一种方便可靠的解决方案,并简化了多设备系统的设计和开发过程。
I2C 协议把传输的消息分为两种类型的帧:
一个地址帧 :用于 master 指明消息发往哪个 slave;
一个或多个数据帧: 由 master 发往 slave 的数据(或由 slave 发往 master),每一帧是 8-bit 的数据。
I2C 数据传输的时序图如下:
地址帧总是在一次通信的最开始出现。一个 7-bit 的地址是从最高位(MSB)开始发送的,这个地址后面会紧跟 1-bit 的操作符,1 表示读操作,0 表示写操作。接下来的一个 bit 是 NACK/ACK,当这个帧中前面 8bits 发送完后,接收端的设备获得 SDA 控制权,此时接收设备应该在第 9 个时钟脉冲之前回复一个 ACK(将 SDA 拉低)以表示接收正常,如果接收设备没有将 SDA 拉低,则说明接收设备可能没有收到数据(如寻址的设备不存在或设备忙)或无法解析收到的消息,如果是这样,则由 master来决定如何处理(stop 或 repeated start condition)。
在地址帧发送之后,就可以开始传输数据了。Master 继续产生时钟脉冲,而数据则由 master(写操作)或 slave(读操作)放到 SDA 上。每个数据帧 8bits,数据帧的数量可以是任意的,直到产生停止条件。每一帧数据传输(即每 8-bit)之后,接收方就需要回复一个 ACK 或 NACK(写数据时由 slave 发送 ACK,读数据时由 master 发送 ACK。当 master 知道自己读完最后一个 byte 数据时,可发送 NACK 然后接 stop condition)。
24C02 的容量位 2Kbit=256Bytes,每 1 页 16Bytes 因此又 16 页。对于写操作一次最多写 16Bytes,对于读操作可以一次全部读完 256Bytes.
如下图,A0-A2 是 EEPROM I2C 器件地址,SDA 和 SCL 是 EEPROM I2C 总线 SLAVE 接口,WP 是保护脚,一般接 VCC。
器件地址:
支持单个字节的写,以及多个字节的写。首先发送器件的地址,然后发送需要写 EEPROM 存储空间的地址,之后就是数据,对于读操作一次可以写 1 个字节或者多个字节。
我们看下 24C02 的读时序,可以看到,支持单个字节的读,以及多个字节的读。以下支持 3 种读的方式:
第一种:CURRENT ADDRESS READ 只要发送器件地址就能读数据。
第二种:RANDOM READ 需要发送器件地址,然后发送内存地址,之后再发送器件地址并且读取到数据,支持连续读取。
第三种:SEQUENTIAL CURRENT READ 只要发送器件地址,就能连续读取当前地址的数据,支持连续读取。
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 ; //模块驱动时钟的分频系数
//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
module e2prom_rw(
input clk , //时钟信号
input rst_n , //复位信号
//i2c interface
output reg i2c_rh_wl , //I2C读写控制信号
output reg i2c_exec , //I2C触发执行信号
output reg [15:0] i2c_addr , //I2C器件内地址
output reg [ 7:0] i2c_data_w , //I2C要写的数据
input [ 7:0] i2c_data_r , //I2C读出的数据
input i2c_done , //I2C一次操作完成
input i2c_ack , //I2C应答标志
//user interface
output reg rw_done , //E2PROM读写测试完成
output reg rw_result //E2PROM读写测试结果 0:失败 1:成功
);
parameter WR_WAIT_TIME = 14'd5000;
parameter MAX_BYTE = 16'd256;
reg [1 :0] flow_cnt;
reg [13:0] wait_cnt;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
i2c_rh_wl <= 1'b0;
i2c_addr <= 16'd0;
i2c_data_w <= 8'd0;
i2c_exec <= 1'b0;
flow_cnt <= 2'd0;
wait_cnt <= 14'd0;
rw_done <= 1'b0;
rw_result <= 1'b0;
end
else begin
i2c_exec <= 1'b0;
rw_done <= 1'b0;
case(flow_cnt)
2'd0:begin
wait_cnt <= wait_cnt + 1'b1;
if(wait_cnt == WR_WAIT_TIME - 1'b1) begin
if(i2c_addr == MAX_BYTE - 1'b1) begin
i2c_addr <= 16'd0;
i2c_rh_wl <= 1'b1;
flow_cnt <= 2'd2;
end
else begin
flow_cnt <= flow_cnt + 1'b1;
i2c_exec <= 1'b1;
end
end
end
2'd1:begin
if(i2c_done == 1'b1) begin
i2c_addr <= i2c_addr + 1'b1;
flow_cnt <= 2'd0;
i2c_data_w <= i2c_data_w + 1'b1;
end
end
2'd2:begin
flow_cnt <= flow_cnt + 1'b1;
i2c_exec <= 1'b1;
end
2'd3:begin
if((i2c_addr[7:0] != i2c_data_r) || (i2c_ack == 1'b1)) begin
rw_done <= 1'b1;
rw_result <= 1'b0;
end
else if(i2c_addr == MAX_BYTE - 1'b1) begin
rw_done <= 1'b1;
rw_result <= 1'b1;
end
else begin
flow_cnt <= 2'd2;
i2c_addr <= i2c_addr + 1'b1;
end
end
endcase
end
end
endmodule
关于EEPROM功能的实现的关键在于I2C协议,因此只需要熟悉了解与掌握关于I2C协议的相关驱动模块状态跳转图,按照状态图进行相关的代码编写即可。当然关于I2C协议的时序图,同样重要,需要了解与掌握开始、停止、地址位、数据位的发送与SClK时钟的同步。
本次代码中具有一个非常重要的因素。就是关于系统时钟的分频。
在生成dri_clk时钟时,本代码选择了与SCLK时钟4倍的关系,即可更好的生成SCLK与数据位、地址位的传输。