一、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 / 1psmodule 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)beginif(clkdiv < SCL_DIV) clkdiv <= clkdiv + 1'b1;else beginclkdiv <= 16'd0; scl_clk <= !scl_clk;end
endassign scl_offset = (clkdiv == OFFSET) ; //设置scl_offset的时间参数always @(posedge I_clk)beginO_iic_scl <= scl_offset ? scl_r : O_iic_scl; //O_iic_scl延迟scl_offset时间的scl_r
end//当IIC_S状态机处于,同时空闲状态,设置SCL为高电平,同时也是空闲,停止状态,用于产生起始位和停止位时序,否则寄存scl_clk时钟
always @(*) beginif(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 @(*) beginif(IIC_S == START || IIC_S == STOP1 || (IIC_S == R_ACK && (rcnt != I_rd_cnt)))beginsda_o <= 1'b0;endelse if(IIC_S == W_WAIT)beginsda_o <= sda_r[7]; endelse beginsda_o <= 1'b1; //否则其他状态都为1,当(IIC_S == R_ACK && (rcnt == I_rd_cnt) 产生一个NACK end
end//I2C数据发送模块,所有的写数据都通过此模块发送
always @(posedge scl_clk) beginif(IIC_S == W_ACK || IIC_S == START)begin //IIC_S=START和W_ACK,把需要发送的数据,寄存到sda_rsda_r <= I_wr_data[(wcnt*8) +: 8]; //寄存需要发发送的数据到sda_rif( rd_req ) beginsda_r <= {I_wr_data[7:1],1'b1}; //对于读操作,rd_req由内部代码产生,当写完第一个数据(器件地址),后通过判断I_rd_cnt,确认是否数据需要读endendelse if(IIC_S == W_WAIT)begin //当W_WAT状态,通过移位操作,把数据发送到数据总线sda_r <= {sda_r[6:0],1'b1}; //移位操作endelse beginsda_r <= sda_r;end
end//I2C数据接收模块,I2C读期间,把数据通过移位操作,移入O_rd_data
always @(negedge scl_clk)beginif(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};endelse if(IIC_S == R_ACK) begin //当IIC_S == R_ACK,完成一个BYTE读,把数据保存到O_rd_dataO_rd_data[((rcnt-1'b1)*8) +: 8] <= sda_i_r[7:0];endelse if(IIC_S == IDLE)begin //空闲状态,重置sda_i_rsda_i_r <= 8'd0;end
end//总线忙状态
always @(posedge scl_clk or negedge I_rstn )beginif(I_rstn == 1'b0)beginO_iic_busy <= 1'b0; endelse beginif((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; endelse if(IIC_S == IDLE)beginO_iic_busy <= 1'b0;endend
end//总线错误状态
always @(negedge scl_clk or negedge I_rstn )beginif(I_rstn == 1'b0)beginO_iic_bus_error <= 1'b0;end else beginif(IIC_S == W_ACK && sda_i == 1'b1)begin//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态O_iic_bus_error <= 1'b1; endelse if(I_iic_req == 0)beginO_iic_bus_error <= 1'b0;endend
end//状态机
always @(posedge scl_clk or negedge I_rstn) beginif (!I_rstn) beginwcnt <= 8'd0 ;rcnt <= 8'd0 ;rd_req <= 1'd0 ;IIC_S <= IDLE ;end else begincase (IIC_S) //sda = 1 scl = 1IDLE:beginif ((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 beginwcnt <= 8'd0;rcnt <= 8'd0;endend 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减1end else begin //8bits发送完毕wcnt <= wcnt + 1'b1; //wcnt计数器,用于记录已经写了多少字节IIC_S <= W_ACK; //进入W_ACK状态endend W_ACK:begin //等待WACK,此阶段,也判断是否有读操作if (wcnt < I_wr_cnt) begin //判断是否所有数据发送(写)完成bcnt <= 3'd7; //如果没有写完,重置bcntIIC_S <= W_WAIT; //继续回到W_WAIT等待数据发送(写)完成end else if(I_rd_cnt > 3'd0)beginif ((rd_req == 1'b0) && (I_iic_mode == 1'b1)) beginrd_req <= 1'b1 ; //请求读操作IIC_S <= IDLE ; //设置状态进入IDLE,根据rd_req的值会重新产生一次为读操作进行的repeated重复startend else beginIIC_S <= R_WAIT ; //进入读等待bcnt <= 3'd7 ; endendelse beginIIC_S <= STOP1; //如果所有的发送完成,也没数据需要读,进入停止状态endend R_WAIT:beginrd_req <= 1'b0 ; //重置读请求 bcnt <= bcnt - 1'b1 ;if (bcnt == 3'd0) beginrcnt <= ( rcnt < I_rd_cnt ) ? ( rcnt + 1'b1 ) : rcnt ; //判断是否还有数据需要读IIC_S <= R_ACK ;endend R_ACK:beginbcnt <= 3'd7 ; //重置读请求bcnt计数器IIC_S <= ( rcnt < I_rd_cnt ) ? R_WAIT : STOP1 ; //如果所有数据读完,进入停止状态 end STOP1:begin //产生停止位 sda = 0 scl = 1rd_req <= 1'b0 ; IIC_S <= STOP2 ; end STOP2:begin //产生停止位 sda = 1 scl = 1IIC_S <= IDLE;end default:begin IIC_S <= IDLE;endendcaseend
endendmodule
3、
状态机:
代码
`timescale 1ns / 1psmodule 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 , //测试LEDoutput 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 ) beginif (!rst_cnt[8]) beginrst_cnt <= rst_cnt + 1'b1;end
end//I2C总线延迟间隔操作,该时间约不能低于500us,否则会导致EEPROM操作失败
always@(posedge I_sysclk) beginif(!rst_cnt[8])begindelay_cnt <= 0;endelse if((TS_S == 3'd0 || TS_S == 3'd2 )) begindelay_cnt <= delay_cnt + 1'b1;endelse begin delay_cnt <= 0;end
end//每间隔500ms状态机运行一次
always@(posedge I_sysclk) beginif(!rst_cnt[8])begint500ms_cnt <= 0;endelse if(t500ms_cnt == T500MS_CNT) begint500ms_cnt <= 0;endelse begin t500ms_cnt <= t500ms_cnt + 1'b1;end
end//状态机实现每次写1字节到EEPROM然后再读1字节
always @(posedge I_sysclk ) beginif (!rst_cnt[8]) beginiic_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 begincase (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 ; endend1:beginif (iic_busy == 1'b1) beginiic_req <= 1'b0 ;TS_S <= 3'd2 ;end end2:beginif ((!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 end3:beginif (iic_busy == 1'b1) beginiic_req <= 1'b0 ;TS_S <= 3'd4 ;endend4:beginif (!iic_busy) begin //当总线非忙,代表前面读数据完成 if (wr_data[23:16] != rd_data[7:0]) begin //对比数据是否正确iic_error <= 1'b1 ;end else beginiic_error <= 1'b0 ;endwr_data[15:8] <= wr_data[15:8] + 1'b1 ; //地址和数据都加1TS_S <= 3'd5 ;endend5:beginif(t500ms_en == 1'b1)beginTS_S <= 3'd0;endenddefault: TS_S <= 3'd0;endcaseend
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) begindg_clk_cnt <= dg_clk_cnt+ 1'b1;
endila_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