凔海笔记之FPGA(九):Verilog描述IIC单字节读写协议
一、简介AT24C04
AT24C04是小容量(4KB)IIC总线EPROM存储元件。这句话说得很有内容呀。首先,AT24c04是存储元件,还是EPROM即可擦写可编程只读存储器,而且断电可保存。其次,对AT24c04的控制是采用IIC协议的,也就是说,IIC≠AT24C04,曾几何时,傻傻的以为IIC=AT24CXX。
那接下来,就先说一下AT24C04吧。
A0空引脚,不是地址设置引脚,A1/A2器件地址设置引脚,是用来设置器件地址的,可以挂4个AT24c04。WP写保护,低电平可对整个AT24c04的512字节进行读写操作,高电平则会使前256个地址受保护,只读不能写,后面的则爱咋地咋地了。SDA数据总线,是一个双向口,SCL则是时钟线。
IIC总线采用两线制,由数据线SDA和时钟线SCL构成。IIC总线对数据通信时序进行了严格的定义,主要包括起始、应答、结束还有数据读写的时序。
下面,就看看IIC单字节读写操作吧。n(*≧▽≦*)n
可见,写IIC协议就是玩拼图,拼的好与坏,就看咱对时序的分析了。
但在玩拼图之前咱还是先了解一下IIC协议的一些基本知识。
1、起始信号
SCL保持高电平的状态下,SDA出现下降沿定义为I2C总线的起始信号,它是由主控器主动建立的一种电平跳变时序信号,标志着一次数据传输的开始,而在建立该信号之前IIC总线必须处于空闲状态。瞧下图
2、写地址
写地址包括写设备地址和写数据地址。就如同笔者兼职家教,得先找到人家在哪个小区哪个楼,再找到人家在哪个单元等。然后才能去输出自己的知识(数据)。
写地址有固定的格式:
硬件ID:就是知道是哪个小区,它会随着厂商设备的种类不同而改变,例如:AT24C04是4’b1010。
硬件地址:就是芯片上的A0\A1\A2,A0空引脚(why?),A1/A2器件地址设置引脚,是用来设置器件地址的,可以挂4个AT24c04。
访问方向:写为1读是0。
写数据地址:AT24C04可以对512字节进行读写操作,而数据地址只有8位!也就是说,只能读写256字节,肿么破??????????????这时候会不会想到A0为什么说是空引脚呢?没错,它就是来凑数的,实现对512字节的读写。对此,我不得不说
还得补充一句,写数据是新的下降沿跟新数据,上升沿锁存/读取数据。
3、应答信号
IIC总线上的所有数据都是以8位字节传送的,发送器每发送一个字节,就在下一个时钟脉冲释放数据线,由主机反馈一个应答信号。读数据就无视应答位。
应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示主机已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示主机接收该字节没有成功。
4、读写数据:
写数据就是写数据是新的下降沿跟新数据,上升沿锁存/读取数据,并且高位在前
读数据可以看做重复读取8次应答位,SCL的下降沿使从机跟新数据,然后主机在SCL的上升沿读取数据,此外,从机也会由高到低更新数据位。
结束位:
在时钟线SCL保持高电平期间,数据线SDA被释放,使得SDA返回高电平(即正跳变),称为I2C总线的停止信号,它标志着一次数据传输的终止。
停止信号也是一种电平跳变时序信号,而不是一个电平信号,停止信号也是由主控器主动建立的,建立该信号之后,I2C总线将返回空闲状态。
所谓的空闲状态就是I2C总线总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
(二)bala一下Verilog综合的AT24c04
上面介绍了IIC协议的一些概念,下面,就开始真二八经的用Verilog综合出来吧。首先来确定要实现的功能:向AT24C04写入123456,然后读出来送给6位数码管依次显示。
接下来,先来说说大体的框架。
smg_display_module.v数码管显示模块,这块就不多说了吧,留着以后再bala吧。
IIC_function_module.v实现对AT24c04的读写时序的综合
IIC_ctrl.v实现对写、读AT24C04的控制和对数码管显示的控制。
这三个模块也就干这点活了
iAddr、iData分别是由IIC_ctrl模块发送给IIC_function_module地址地址和数据,oData是读出来的数据,共24位,一并发给数码管显示给我们看。iCall实现读写控制,oDone是完成信号,控制IIC_ctrl的工作启停。
在这还是插句话吧,咱知道AT24c04的数据线是双向口,所以要用isQ来对数据线的输入输出状态进行选择。所以用实现,inout SDA;assign SDA = isQ ? rSDA : 1'bz;isQ=1时为输出,即将数据写入AT24C04,isQ=0时为输入,即读出AT24C04数据。那为什么在输入时要设为高阻态呢?
答:处于高阻抗状态时,输出电阻很大,相当于开路,没有任何逻辑控制功能。即可以认为是没有输出,对下级电路没有任何影响。而一个输出端口在高阻态的时候,其状态是由于其相连的其他电路决定的,可以将其看作是输入。当三态门的输出处于高阻状态的时候,取值由外部电路决定,也就是说,这一时刻是可以作为输入。
是否还记得,我们上节说了读写流程,就是下面这幅图,
仔细端详这幅图我们不难发现,我们要写的就是起始位,写数据,读数据,应答位,结束位,其他的
接下来开始苦干人生了
IIC_function_module.v该模块要完成与AT 24C04的读写通讯,也就是IIC协议。说白了就是完成写字节和读字节,所以要直勾勾的盯着下面这张图来写一写
该图给出了AT24C04对时序的要求,高电平多少,低电平多少都有规范。可见,想实现度AT24C04的控制,要求的还是蛮多的。下面我们采用400KHz的时钟周期进行读写操作。
1、起始信号,也就是通知AT24c04要干活的指令是要求在时钟线拉高的情况下数据线产生一个负跳变(高电平变为低电平)。而且要求起始信号的保持直接不少于600ns.所以我们可以这样描述
begin
isQ <= 1'b1;
rSCL <= 1'b1;
if(C1 == 8'd0)
rSDA <= 1'b1;
else if(C1 == (TR+20)) //上升沿再保持400ns
rSDA <= 1'b0;
if(C1 == TCLK-1) //一个时钟周期后进入下一步骤
begin C1 <= 8'd0; i <= i + 1'b1; end
else
C1 <= C1 + 1'b1;
end
说一下哈,else if(C1 == (TR+20))那个20不是规定,使我想让它保持20的,其实也不需要一个时钟周期,只怪我愿意
2、应答位可以看做是读取一位数据,主机写完成或读取一字节时,从机都会产生应答位,在主机拉低SCL那一刻,从机便会发送应答位,主机借上升沿读取应答,应答信号低电平有效。
5'd15:
begin
isQ <= 1'b0;
if(C1 ==TF+8'd20) isAck <= SDA;
if(C1 == 8'd0) rSCL <= 1'b0;
else if(C1 == TF+8'd20) rSCL <= 1'b1;
if(C1 == TCLK -1'b1)
begin C1 <= 8'd0; i <= i + 1'b1; end
else C1 <= C1 + 1'b1;
end
5'd16:
if(isAck != 1'b0)
i <= 5'd0;
else
i <= jump;
3、结束位
在SCL保持高电平时,SDA被释放,使得SDA返回高电平,即为停止信号,这也是一种电平跳变时序信号,而不是一个电平信号,停止信号也是由主控器主动建立的,该信号建立后,IIC返回空闲状态。
begin
isQ <= 1'b1;
if(C1 == 0) rSCL <= 1'b0;
else if(C1 == 8'd31) rSCL <= 1'b1;
if(C1==0) rSDA <= 1'b0;
else if(C1 == 8'd76) rSDA <= 1'b1;
if(C1 == 8'd155)
begin C1 <= 8'd0; i <= i + 1'b1; end
else C1 <= C1 + 1'b1;
end
4、写数据SCL下降沿更新数据,上升沿锁存数据
5'd7,5'd8,5'd9,5'd10,5'd11,5'd12,5'd13,5'd14:
begin
isQ <= 1'b1; //写入AT24c04
rSDA <= oData[5'd14 - i];
if(C1 == 0) rSCL <= 1'b0;
else if(C1 == (TF+TLOW)) rSCL <= 1'b1;
if(C1 == TCLK -1'b1)
begin C1 <= 8'b0; i <= i + 1'b1; end
else C1 <= C1 + 1'b1;
end
5、读数据,权当读取八个应答信号吧
5'd19,5'd20,5'd21,5'd22,5'd23,5'd24,5'd25,5'd26:
begin
isQ <= 1'b0;
if(C1 == 8'd62) rData[26-i] <=SDA;
if(C1 == 0) rSCL <= 1'b0;
else if(C1 == 8'd62) rSCL <= 1'b1;
if(C1 == TCLK-1’b1)
begin C1 <= 8'd0; i <= i + 1'b1;end
else C1 <= C1 + 1'b1;
end
最后呢?就是IIC_ctrl中的内容了
case(i)
3'd0:
if(DoneU1)
begin isCall <= 2'b00;i <= i + 1'b1;end
else
begin isCall <= 2'b10;rAd <= 8'd6;rDa <= 8'h21;end
3'd1:
if(DoneU1)
begin isCall<= 2'b00;i<= i + 1'b1;end
else
begin isCall <= 2'b10;rAd <= 8'd16;rDa <= 8'h43;end
3'd2:
if(DoneU1)
begin isCall <= 2'b00;i <= i + 1'b1;end
else
begin isCall <= 2'b10;rAd <= 8'd66;rDa <= 8'h65;end
3'd3:
if(DoneU1)
begin rNum[7:0] <= oData;isCall <= 2'b00;i <=i+1'b1; end
else
begin isCall <= 2'b01;rAd <= 8'd6;end
3'd4:
if(DoneU1)
begin rNum[15:8] <= oData;isCall <= 2'b00;i<=i + 1'b1;end
else
begin isCall <= 2'b01;rAd <= 8'd16;end
3'd5:
if(DoneU1)
begin rNum[23:16] <= oData;isCall <= 2'b00;i<= i + 1'b1;end
else
begin isCall <= 2'b01;rAd <= 8'd66;end
3'd6:
i <= i;
endcase
在地址6、16、66分别写入12、34、56。说实话我挺喜欢黑金这样的写法的,虽然不知道这样写算不算好的代码风格,但挺容易懂的。在完成信号来之前,重复进行一个动作,例如begin isCall <= 2'b10;rAd <= 8'd6;rDa <= 8'h21;end写操作,在地址6写入数据12,等完成信号来了,就进入下一个操作。