eeprom
出于积累Verilog编程和调试经验的需求,用这个东西练手
使用的是正点原子领航者zynq的板子 实验环境过于优越了 导致进度慢就会很自责 ,程序一天出来了 。。 写个总结算是对得起我这两天的debug,
eeprom 用的是板子上自带的AT24C64
iic时序
先分析下这货的iic时序,后面再说这货的iic读写时序
这里提前说一下,虽然很多设备都叫iic但是iic和iic之间也是有区别的,所以在写时序的时候一般最好还是看一眼手册和你之前用的iic是不是一样的
简单说两句这个图,你生成的SCL时钟高电平的时候,SDA给一个下降沿,就会生成一个起始信号,起始信号之后进入输出传输bit,在你生成的时钟(SCL)下降沿之后,SDA的输出可以变化,也就是可以更改输出的数据,而时钟(SCL)上升沿之后,数据不论是高电平还是低电平,都应该是稳定的,在数据传输结束时,最后一个时钟的上升沿之后(SCL为高电平时)一个SDA的上升沿代表通讯结束
关于我这部分的描述,原手册都有对应英文,只不过分成了好几段,我截图在本节的最后了
至于这个图中的一堆带角标的t,就是一些通讯细节,规定得是设备能反应的一些极限速度,如果你操作的比它的快就会导致一些问题,但是一般低速操作不会有影响,这里附赠一个基础知识点
IIC通用频率也就400K ,不要上M ,否则有可能会出问题,当然 不同设备 ,极限情况就不好说了
原手册对这部分有更清晰的描述和附图,这里我就不一一翻译了,应该说明白了
AT24C64 通讯时序
写入时序
下图中Figure1 展示了设备的地址定义,Figure2 展示的是主机发送单byte写的时候的时序
英文描述我就不截图了,简单说下这个图的意思,你要往eeprom的一个地址写一byte的东西,那么就需要把以下的步骤重复一遍:
起始位==>发1byte(8bit)的设备地址==>再发2byte(16bit)的写入地址==>再发1byte的数据,结束位 ==>写入成功
这里有一个ACK ,在发每个byte结束的时候都会收到一个设备发回来的低电平ACK表示它收到了这个byte的数据
设备地址:高四位是1010 固定的 ,中间三位是用户可以根据设置芯片三个管脚电平控制的,这里原子都给接地了 所以我也只能给000,最后一位是读写位 如果是写就置0,读就置1
写地址:可以看到只有13位是有效的,带 的是任意的,因为AT24c64是64K的eeprom,存储空间只有64 1024个bit,也就是64 * 1024/8=8192个byte,写作二进制就是0010_0000_0000_0000,由于0位的存在,所以表示这么多的数据位的地址最多也就到第13位,所以最高的三位是无效的
MSB:数据的高位
LSB:数据的低位
了解到MSB和LSB可以确定数据时序先发高位,这个在不同的设备用有时候也会有些微的不同,有的手册不说明白,可能就一句话告诉你MSB First ,也就是先发高位
因为不想连片写,所以也没分析连续写入时序,也没差太多
这里涉及到了引脚SDA既有输入,又有输出,也就是说它是个inout 的管脚,当场查了一下这样的管教在Verilog里面怎么描述,因为你输出的话还好,输入的话怎么能让自己不占用,然后发现高阻态是可以直接赋值的,这样通过下面的一个操作,就可以比较方便的操作sda
inout引脚(sda)描述
因为in 的时候需要把引脚置为高阻态,out 的时候要assign到寄存器
所以有了如下的方式,设置一个使能输出位 sda_en
inout iic_sda//定义一个inout
r_sdi_shift<={r_sdi_shift[:],io_sdio}; //先接收到的为高位读取数据拼合成一个data
assign iic_sda = sda_en ? 1'bz : iic_sdo_reg; //输出
读取时序
读取的时序如下,解读方式和写入相似,简单说一下
它一共支持三种读法,和连片写一样,我也没做解析连片读,只说下面两个Fig
Fig4 读取当前地址,这种应该是上次写入时候写的那个地址,直接就可以返回当前的地址数据,这种时候只需要写入设备地址2'b10100001,然后收到一个低电平ACK ,之后收到1Byte的数据。发送结束后有一位的缓冲但是它不会给你ACK,然后等你发Stop。
Fig5 说的是读一个随机地址,我只实现了这个
在这样的读取的时候顺序如下:
起始位==>1Byte地址(1010_0000)==> ACK==>2Byte地址 都有ACK==> 起始位==>1Byte地址(1010_0001)==>ACK ==>读取1Byte数据,没有ACK==>STOP 停止位
其中
起始位==>1Byte地址(1010_0000)==> ACK==>2Byte地址 都有ACK
这几个步骤叫做虚写 (dummy write) 最开始看漏了这个 主要看漏了中间的一次写设备地址,导致一开始通讯不正常。
这样就读到了1byte的数据
实现思路
还没有学习状态机的N种写法,所以写的可能稚嫩一些,简单说下思路,如果有感兴趣的可以移步下载,让我赚个零花钱
工程文件有三个:
tb_iic.v (仿真文件)
rw_test.v (定层文件)
e2prom.v (底层实现文件)
一共分了5个状态,IDLE,START,DWR,ACK,STOP
这里读写放到了一个状态下,所以导致有的读时序很混乱,主要是一开始读手册没看懂读时序是先进行虚写,后面又重发了一次device 设备地址,如果看懂了把读写分开应该逻辑会更清晰
下面是状态机,具体的思路就是状态之间的转换,先说写时序的生成状态:
在clk检测,当SCL 的上升沿触发状态修改操作。
首先进入IDLE待机状态,当en使能时候从IDLE出去进入START时序,在其他always块中实现sda的从低到高
当进入start后,en如果还没有置低,进入下一状态 DWR,在此生成8byte的读写时序,这里只考虑了写任意地址,也就是发送4bytes。此时在sda生成块中对对应的计数情况进行反应,发送指定的bit位
每次发送完8bit之后进入1bit的ACK位,这时在对应位置将sda的状态置为高阻,让他能发出来ack。
在ACK状态下对不同位数的写进行反应,当写了4bytes时进入下一状态,STOP,然后在上升沿之后生成一个scl高电平下的sda的从低到高,至此,一次写操作结束
读操作也差不多,区别在于进入了两次start ,所以多了一个read_cnt用于记录是否进行dummy write
/*状态机 状态从0-4 暂时不支持连续写 只支持按照地址写 一次发
每次写传输都有:
起始位
一个字节的设备地址 Ack 等待 1010000 0
两个字节的byte地址 每个字节都有Ack 等待 写入地址 0000 0000 0000 0010
一个字节的写入数据 Ack 等待 写入数据 0000 1000
停止位
*/
always @ (posedge clk or negedge rst) begin
if (!rst||!en) begin
next_state <= IDLE;
byte_cnt <=5'd0;
bit_cnt <=5'd0;
read_cnt<=3'd0;
end
else if(next_state ==IDLE && en && iic_posedge )
next_state <= START ;
else if(next_state ==START && iic_nedge ) begin
next_state <= DWR ;
byte_cnt <=8'd0;
bit_cnt <=5'd0;
end
else if(next_state ==DWR && iic_nedge && bit_cnt < 7 ) begin
next_state <= DWR;
bit_cnt <= bit_cnt + 8'd1;
end
else if(next_state ==DWR && iic_nedge && bit_cnt == 7 ) begin
next_state <= ACK ;
bit_cnt <= 8'd0;
end
else if(next_state == ACK && iic_nedge && byte_cnt == 1 && ren&&read_cnt ==3'd1) begin
next_state <= STOP;
byte_cnt <= 8'd0000;
read_cnt <= 3'd0;
end
else if(next_state == ACK && iic_nedge && byte_cnt < 3&& wen) begin
next_state <= DWR;
byte_cnt <= byte_cnt+8'd1;
end
else if(next_state == ACK && iic_nedge && byte_cnt < 2&& ren) begin
next_state <= DWR;
byte_cnt <= byte_cnt+8'd1;
end
else if(next_state == ACK && iic_nedge && byte_cnt == 2 && ren&&read_cnt ==3'd0) begin
next_state <= IDLE;
byte_cnt <= 8'd0000;
read_cnt <= 3'd1;
end
else if(next_state == ACK && iic_nedge && byte_cnt == 3)
next_state <= STOP;
else if(next_state == STOP && iic_nedge)
next_state <= IDLE;
end
iic时钟沿的检测时序
// en=0 没有使能或者复位时,不操作 否则缓存2位iic时钟,用于之后做 起始位 数据发送的判定信号
assign iic_po = iic_scl_d0 & (~iic_scl_d1);
assign iic_ne = iic_scl_d1 & (~iic_scl_d0);
always @ (posedge clk or negedge rst) begin
if (!rst||!en) begin
iic_scl_d0 <= 1'b0;
iic_scl_d1 <= 1'b0;
end
else begin
iic_scl_d0 <= iic_scl;
iic_scl_d1 <= iic_scl_d0;
end
end
为了防止我对下降沿或者上升沿反应过快 ,还给他们做了个延时。。 应该0可以写的好一点,当时脑子不大好,就用了很多寄存器
//给 iic_posedge iic_nedge 写一个延长的时钟防止操作过快,垃圾设备反应不过来
//因为周期是500拍 所以不可能同时发生上下沿
//所以写一起了
always @ (posedge clk or negedge rst) begin
if(!rst||!en)
delay_cnt <= 11'd0;
else if(po_delay||ne_delay&&delay_cnt<200)
delay_cnt <= delay_cnt + 11'd1;
else
delay_cnt <= 11'd0;
end
//给 iic_posedge iic_nedge 写一个延长的时钟防止操作过快,垃圾设备反应不过来
always @ (posedge clk or negedge rst) begin
if(!rst||!en) begin
po_delay<=0;
ne_delay<=0;
end
else if(iic_po) begin
po_delay<=1;
end
else if(iic_ne) begin
ne_delay<=1;
end
else if(po_delay == 1 && delay_cnt ==200)
po_delay <=0;
else if(ne_delay == 1 && delay_cnt ==200)
ne_delay <=0;
end
always @ (posedge clk or negedge rst) begin
if(!rst||!en) begin
iic_posedge<=0;
iic_nedge<=0;
end
else if(po_delay == 1&& delay_cnt ==198) begin
iic_posedge<=1;
end
else if(po_delay == 1&& delay_cnt ==199) begin
iic_posedge<=0;
end
else if(ne_delay == 1&& delay_cnt ==198) begin
iic_nedge<=1;
end
else if(ne_delay == 1&& delay_cnt ==199) begin
iic_nedge<=0;
end
end
另外在调试的时候发现,使用ila读取iic_sda数据时候会在我给他置位为高阻时读到FF,也就是全高,这是因为什么我不大确定,但是解决方法是给iic_sda 来一个读取缓存,如下,然后读取这个sda_in ,在子模块下例化ila,就可以看到正确的输入数据
另外,在写下面这个assign之后,ila直接读取sda的操作会报错,说是缺buf还是什么,解决方案同上 ,不要看sda,看sda_in.
assign sda_in = iic_sda;
工程文档上传到了csdn,如果有需要自取
https://download.csdn.net/download/qq_31764341/85425598