一、EEPROM简介
Electrically Erasable Progammable Read Only Memory:是指带电可擦可编程只读存储器,是一种常用的非易失性存储器(掉电数据不丢失)
EEPROM发展历史
我们这次实验所用的AT24C64存储容量为64Kbit,内部分成256页,每页32字节,共有8192个字节,且其读写操作都是以字节为基本单位。可以把AT24C64看作一本书,那么这本书有256页,每页有32行,每行有8个字,总共有256*32*8=65536个字,对应着AT24C64的64*1024=65536个bit。
读是从起始读到最大,回到起始; 写是写到一页的结束;
24C02:
片内地址寻址:
读写操作:
二、IIC简介
1、简介
IIC即Inter-Integrated Circuit(集成电路总线),是由Philips半导体公司(现在的NXP半导体公司)在八十年代初设计出来的一种简单、双向、二线制总线标准。
2、 IIC结构示意
3、IIC协议时序
时钟线为低电平可以变化数据,时钟线为高电平不允许变化数据,只能保持。
4、器件地址
5、字寄存器地址 (24C64)
6、单次写(字节写)
7、连续写(页写)
8、读当前地址:
9、任意地址单次读(随机读)
10、从xxx地址开始连续读
三、试验任务
本节的实验任务是先向E2PROM(AT24C64)的存储器地址0至255分别写入数据0~255;写完之后再读取存储器地址0~255中的数据,若读取的值全部正确则LED灯常亮,否则LED灯闪烁。
四、程序设计
1、整体框图
2、IIC_my
状态机:
代码:
`timescale 1ns / 1ps
module iic_my#(
parameter integer WMEN_LEN = 8'd1 , //写长度,以字节为单位,包含器件地址
parameter integer RMEN_LEN = 8'd0 , //读长度,以字节为单位,不包含器件地址
parameter integer CLK_DIV = 16'd499 //IIC时钟分频系数
)
(
input wire I_clk , //系统时钟输入
input wire I_rstn , //系统复位,低电平有效
output reg O_iic_scl = 1'b0 , //I2C时钟SCL
inout wire IO_iic_sda , //I2C 数据总线
input wire [WMEN_LEN*8-1'b1:0] I_wr_data , //写数据寄存器,其中WMEN_LEN设置了最大支持的数据字节数,越大占用的FPGA资源越多
input wire [7:0] I_wr_cnt , //写数据计数器,代表写了多少个字节
output reg [RMEN_LEN*8-1'b1:0] O_rd_data = 0 , //读数据寄存器,其中RMEN_LEN设置了最大支持的数据字节数,越大占用的FPGA资源越多
input wire [7:0] I_rd_cnt , //读数据计数器
input wire I_iic_req , //I_iic_req == 1 使能I2C传输
input wire I_iic_mode , //I_iic_mode = 1 随机读 I_iic_mode = 0 读当前寄存器或者页读
output reg O_iic_busy = 1'b0 , //I2C控制器忙
output reg O_iic_bus_error , //I2C总线,无法读到正确ACK出错
output reg IO_iic_sda_dg //数据总线上的数据
);
localparam IDLE = 4'd0 ; //I2C 总线空闲状态
localparam START = 4'd1 ; //I2C 总线启动
localparam W_WAIT = 4'd2 ; //I2C 总线等待写完成
localparam W_ACK = 4'd3 ; //I2C 总线等待写WACK
localparam R_WAIT = 4'd4 ; //I2C 总线等待读完成
localparam R_ACK = 4'd5 ; //I2C 总线等待读RACK
localparam STOP1 = 4'd6 ; //I2C 总线产生停止位
localparam STOP2 = 4'd7 ; //I2C 总线产生停止位
localparam SCL_DIV = CLK_DIV/2 ;
localparam OFFSET = SCL_DIV - SCL_DIV/4 ; //设置I2C总线的SCL时钟的偏移,以满足SCL和SDA的时序要求,外部的SCL延迟内部的半周期的四分之三
reg [2:0 ] IIC_S = 4'd0 ; //I2C 状态机
//generate scl
reg [15:0] clkdiv = 16'd0 ; //I2C 时钟分频寄存器
reg scl_r = 1'b1 ; //I2C控制器的SCL内部时钟
reg sda_o = 1'b0 ; //I2C控制器的SDA
reg scl_clk = 1'b0 ; //I2C控制器内部SCL时钟,与外部时钟存在OFFSET参数设置的相位偏移
reg [7:0] sda_r = 8'd0 ; //发送寄存器
reg [7:0] sda_i_r = 8'd0 ; //接收寄存器
reg [7:0] wcnt = 8'd0 ; //发送数据计数器,以byte为单位
reg [7:0] rcnt = 8'd0 ; //接收数据计数器,以byte为单位
reg [2:0] bcnt = 3'd0 ; //bit计数器
reg rd_req = 1'b0 ; //读请求,当判断到需要读数据,内部状态机中设置1
wire sda_i ; //sda 输入
wire scl_offset ; //scl 时钟偏移控制
assign sda_i = (IO_iic_sda == 1'b0) ? 1'b0 : 1'b1 ; //读总线
assign IO_iic_sda = (sda_o == 1'b0) ? 1'b0 : 1'bz ; //写总线,1'bz代表高阻,I2C外部通过上拉电阻,实现总线的高电平
PULLUP PULLUP_inst (.O(iic_sda));//仅对仿真有效,仿真设计iic_sda状态为高阻,模拟上拉电阻
//scl 时钟分频器
always@(posedge I_clk)begin
if(clkdiv < SCL_DIV)
clkdiv <= clkdiv + 1'b1;
else begin
clkdiv <= 16'd0;
scl_clk <= !scl_clk;
end
end
assign scl_offset = (clkdiv == OFFSET) ; //设置scl_offset的时间参数
always @(posedge I_clk)begin
O_iic_scl <= scl_offset ? scl_r : O_iic_scl; //O_iic_scl延迟scl_offset时间的scl_r
end
//当IIC_S状态机处于,同时空闲状态,设置SCL为高电平,同时也是空闲,停止状态,用于产生起始位和停止位时序,否则寄存scl_clk时钟
always @(*) begin
if(IIC_S == IDLE || IIC_S == STOP1 || IIC_S == STOP2)
scl_r <= 1'b1;
else
scl_r <= scl_clk;
end
//当进入IIC_S状态为启动、停止设置sda=0,结合scl产生起始位,或者(IIC_S == R_ACK && (rcnt != I_rd_cnt) sda=0,用于产生读操作的ACK
always @(*) begin
if(IIC_S == START || IIC_S == STOP1 || (IIC_S == R_ACK && (rcnt != I_rd_cnt)))begin
sda_o <= 1'b0;
end
else if(IIC_S == W_WAIT)begin
sda_o <= sda_r[7];
end
else begin
sda_o <= 1'b1; //否则其他状态都为1,当(IIC_S == R_ACK && (rcnt == I_rd_cnt) 产生一个NACK
end
end
//I2C数据发送模块,所有的写数据都通过此模块发送
always @(posedge scl_clk) begin
if(IIC_S == W_ACK || IIC_S == START)begin //IIC_S=START和W_ACK,把需要发送的数据,寄存到sda_r
sda_r <= I_wr_data[(wcnt*8) +: 8]; //寄存需要发发送的数据到sda_r
if( rd_req ) begin
sda_r <= {I_wr_data[7:1],1'b1}; //对于读操作,rd_req由内部代码产生,当写完第一个数据(器件地址),后通过判断I_rd_cnt,确认是否数据需要读
end
end
else if(IIC_S == W_WAIT)begin //当W_WAT状态,通过移位操作,把数据发送到数据总线
sda_r <= {sda_r[6:0],1'b1}; //移位操作
end
else begin
sda_r <= sda_r;
end
end
//I2C数据接收模块,I2C读期间,把数据通过移位操作,移入O_rd_data
always @(negedge scl_clk)begin
if(IIC_S == R_WAIT ) begin //当IIC_S == R_WAIT ||IIC_S == W_ACK(如果读操作,第1个BIT是W_ACK这个状态读)启动移位操作
sda_i_r <= {sda_i_r[6:0],sda_i};
end
else if(IIC_S == R_ACK) begin //当IIC_S == R_ACK,完成一个BYTE读,把数据保存到O_rd_data
O_rd_data[((rcnt-1'b1)*8) +: 8] <= sda_i_r[7:0];
end
else if(IIC_S == IDLE)begin //空闲状态,重置sda_i_r
sda_i_r <= 8'd0;
end
end
//总线忙状态
always @(posedge scl_clk or negedge I_rstn )begin
if(I_rstn == 1'b0)begin
O_iic_busy <= 1'b0;
end
else begin
if((I_iic_req == 1'b1 || rd_req == 1'b1 || O_iic_bus_error))begin//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态
O_iic_busy <= 1'b1;
end
else if(IIC_S == IDLE)begin
O_iic_busy <= 1'b0;
end
end
end
//总线错误状态
always @(negedge scl_clk or negedge I_rstn )begin
if(I_rstn == 1'b0)begin
O_iic_bus_error <= 1'b0;
end
else begin
if(IIC_S == W_ACK && sda_i == 1'b1)begin//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态
O_iic_bus_error <= 1'b1;
end
else if(I_iic_req == 0)begin
O_iic_bus_error <= 1'b0;
end
end
end
//状态机
always @(posedge scl_clk or negedge I_rstn) begin
if (!I_rstn) begin
wcnt <= 8'd0 ;
rcnt <= 8'd0 ;
rd_req <= 1'd0 ;
IIC_S <= IDLE ;
end
else begin
case (IIC_S) //sda = 1 scl = 1
IDLE:begin
if ((I_iic_req == 1'b1)||(rd_req == 1'b1) )begin //当I_iic_req == 1'b1代表启动传输 当 rd_req == 1'b1 代表读操作需要产生repeated start 重复启动
IIC_S <= START; //进入总线启动
end
else begin
wcnt <= 8'd0;
rcnt <= 8'd0;
end
end
START:begin //这个状态,前面的代码,先设置sda = 0,scl_offset参数设置了scl_clk时钟的偏移,之后 scl_clk =0 即scl =0 产生起始位或者重复起始位
bcnt <= 3'd7; //设置bcnt的初值
IIC_S <= W_WAIT; //进入发送等待
end
W_WAIT:begin //等待发送完成,这里发送8bits 数据,写器件地址,写寄存器地址,写数据,都在这个状态完成
if (bcnt > 3'd0) begin //如果8bits没发送完,直到发送完
bcnt <= bcnt - 1'b1; //bcnt计数器,每发送1bit减1
end
else begin //8bits发送完毕
wcnt <= wcnt + 1'b1; //wcnt计数器,用于记录已经写了多少字节
IIC_S <= W_ACK; //进入W_ACK状态
end
end
W_ACK:begin //等待WACK,此阶段,也判断是否有读操作
if (wcnt < I_wr_cnt) begin //判断是否所有数据发送(写)完成
bcnt <= 3'd7; //如果没有写完,重置bcnt
IIC_S <= W_WAIT; //继续回到W_WAIT等待数据发送(写)完成
end
else if(I_rd_cnt > 3'd0)begin
if ((rd_req == 1'b0) && (I_iic_mode == 1'b1)) begin
rd_req <= 1'b1 ; //请求读操作
IIC_S <= IDLE ; //设置状态进入IDLE,根据rd_req的值会重新产生一次为读操作进行的repeated重复start
end
else begin
IIC_S <= R_WAIT ; //进入读等待
bcnt <= 3'd7 ;
end
end
else begin
IIC_S <= STOP1; //如果所有的发送完成,也没数据需要读,进入停止状态
end
end
R_WAIT:begin
rd_req <= 1'b0 ; //重置读请求
bcnt <= bcnt - 1'b1 ;
if (bcnt == 3'd0) begin
rcnt <= ( rcnt < I_rd_cnt ) ? ( rcnt + 1'b1 ) : rcnt ; //判断是否还有数据需要读
IIC_S <= R_ACK ;
end
end
R_ACK:begin
bcnt <= 3'd7 ; //重置读请求bcnt计数器
IIC_S <= ( rcnt < I_rd_cnt ) ? R_WAIT : STOP1 ; //如果所有数据读完,进入停止状态
end
STOP1:begin //产生停止位 sda = 0 scl = 1
rd_req <= 1'b0 ;
IIC_S <= STOP2 ;
end
STOP2:begin //产生停止位 sda = 1 scl = 1
IIC_S <= IDLE;
end
default:begin
IIC_S <= IDLE;
end
endcase
end
end
endmodule
3、
状态机:
代码
`timescale 1ns / 1ps
module eeprom_rt
(
input wire I_sysclk , //系统时钟输入
output wire O_iic_scl , // I2C SCL时钟
inout wire IO_iic_sda , //I2C SDA数据总线
output wire [3:0] test_led , //测试LED
output wire led //error LED
);
localparam SYSCLKHZ = 100_000_000;
localparam T500MS_CNT = (SYSCLKHZ / 2 - 1) ;
reg [8 :0] rst_cnt = 9'd0 ; //延迟复位计数器
reg [25:0] t500ms_cnt = 26'd0 ; //500ms计数器
reg [19:0] delay_cnt = 20'd0 ; //eeprom每次读写完后,延迟操作计数器
reg [2 :0] TS_S = 2'd0 ; // 读写EEPROM状态机
reg iic_req = 1'b0 ; //i2c总线,读/写请求信号
reg [31:0] wr_data = 32'd0 ; //写数据寄存器
reg [7 :0] wr_cnt = 8'd0 ; //写数据计数器
reg [7 :0] rd_cnt = 8'd0 ; //读数据计数器
wire iic_busy ; // i2c总线忙信号标志
wire [31:0] rd_data ; // i2c读数据
wire t500ms_en ; // 500ms延迟到使能
wire IO_iic_sda_dg ;
wire iic_bus_error ; //i2c总线错误
reg iic_error = 1'b0 ; //i2c 读出数据有错误
assign test_led = rd_data[3:0] ;//测试LED输出
assign led = iic_error ;//通过LED显示错误标志
assign t500ms_en = (t500ms_cnt==T500MS_CNT);//500ms 使能信号
//系统复位计数器
always @(posedge I_sysclk ) begin
if (!rst_cnt[8]) begin
rst_cnt <= rst_cnt + 1'b1;
end
end
//I2C总线延迟间隔操作,该时间约不能低于500us,否则会导致EEPROM操作失败
always@(posedge I_sysclk) begin
if(!rst_cnt[8])begin
delay_cnt <= 0;
end
else if((TS_S == 3'd0 || TS_S == 3'd2 )) begin
delay_cnt <= delay_cnt + 1'b1;
end
else begin
delay_cnt <= 0;
end
end
//每间隔500ms状态机运行一次
always@(posedge I_sysclk) begin
if(!rst_cnt[8])begin
t500ms_cnt <= 0;
end
else if(t500ms_cnt == T500MS_CNT) begin
t500ms_cnt <= 0;
end
else begin
t500ms_cnt <= t500ms_cnt + 1'b1;
end
end
//状态机实现每次写1字节到EEPROM然后再读1字节
always @(posedge I_sysclk ) begin
if (!rst_cnt[8]) begin
iic_req <= 1'b0 ;
wr_data <= 32'd0 ;
rd_cnt <= 8'd0 ;
wr_cnt <= 8'd0 ;
iic_error <= 1'b0 ;
TS_S <= 3'd0 ;
end
else begin
case (TS_S)
0:begin
if (!iic_busy) begin //当总线不忙,可以开启一次IIC数据操作
iic_req <= 1'b1 ; //请求发送数据
//数据寄存器中8'b10100000代表需要写的器件地址,第一个wr_data[15:8]代表了EEPROM内存地址,第二个wr_data[15:8]代表了写入数据
wr_data <= {8'hfe,wr_data[15:8],wr_data[15:8],8'b10100000};
rd_cnt <= 8'd0 ; //不需要读数据
wr_cnt <= 8'd3 ; //需要写入3个BYTES数据,包含一个器件地址,1个EEPROM寄存器地址,一个数据
TS_S <= 3'd1 ;
end
end
1:begin
if (iic_busy == 1'b1) begin
iic_req <= 1'b0 ;
TS_S <= 3'd2 ;
end
end
2:begin
if ((!iic_busy) && (delay_cnt[19] == 1'b1)) begin//当总线非忙,可以开始一次I2C数据操作,该时间约不能低于500us,否则会导致EEPROM操作失败
iic_req <= 1'b1 ; //请求接收数据
rd_cnt <= 8'd1 ; //需要读取一个字节
wr_cnt <= 8'd2 ; //需要写两个byte 一个器件地址8’d1010_0000 一个寄存器地址 wr_data[15:8] (I2C控制器会自定设置读写标志位)
TS_S <= 3'd3 ;
end
end
3:begin
if (iic_busy == 1'b1) begin
iic_req <= 1'b0 ;
TS_S <= 3'd4 ;
end
end
4:begin
if (!iic_busy) begin //当总线非忙,代表前面读数据完成
if (wr_data[23:16] != rd_data[7:0]) begin //对比数据是否正确
iic_error <= 1'b1 ;
end
else begin
iic_error <= 1'b0 ;
end
wr_data[15:8] <= wr_data[15:8] + 1'b1 ; //地址和数据都加1
TS_S <= 3'd5 ;
end
end
5:begin
if(t500ms_en == 1'b1)begin
TS_S <= 3'd0;
end
end
default: TS_S <= 3'd0;
endcase
end
end
// 以下代码为在线逻辑分析仪观察调试部分
reg scl_r = 1'b0;
always @(posedge I_sysclk)begin //对O_iic_scl寄存1次
scl_r <= O_iic_scl;
end
//产生一个触发时钟,这个时钟是系统时钟的512倍分频,这样抓取总线的时候,可以看到更多I2C的有效信号
reg [8:0] dg_clk_cnt;
wire dg_clk = (dg_clk_cnt==0);//用scl_dg即O_iic_scl的跳变沿作为触发信号
always@(posedge I_sysclk) begin
dg_clk_cnt <= dg_clk_cnt+ 1'b1;
end
ila_0 ila_debug (
.clk(I_sysclk),//在线逻辑分析仪的时钟
.probe0({rd_data[7:0],wr_data[23:0],TS_S,iic_error,iic_req,scl_r,IO_iic_sda_dg,iic_bus_error,dg_clk,t500ms_en}) // 需要观察的调试信号
);
//例化I2C控制模块
iic_my#
(
.WMEN_LEN (4 ) , //最大支持一次写入4BYTE(包含器件地址)
.RMEN_LEN (4 ) , //最大支持一次读出4BYTE(包含器件地址)
.CLK_DIV (SYSCLKHZ/100000) //100KHZ I2C总线时钟
)
u_iic_my
(
.I_clk (I_sysclk ) , //系统时钟
.I_rstn (rst_cnt[8] ) , //系统复位
.O_iic_scl (O_iic_scl ) , //I2C SCL总线时钟
.IO_iic_sda (IO_iic_sda ) , //I2C SDA数据总线
.I_wr_data (wr_data ) , //写数据寄存器
.I_wr_cnt (wr_cnt ) , //需要写的数据BYTES
.O_rd_data (rd_data ) , //读数据寄存器
.I_rd_cnt (rd_cnt ) , //需要读的数据BYTES
.I_iic_req (iic_req ) , //I2C控制器请求
.I_iic_mode (1'b1 ) , //读模式
.O_iic_busy (iic_busy ) , //I2C控制器忙
.O_iic_bus_error (iic_bus_error ) , //总线错误信号标志
.IO_iic_sda_dg (IO_iic_sda_dg ) //debug IO_iic_sda
);
endmodule