FPAG学习笔记——I2C接口实现
Posted 星河带悦流
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FPAG学习笔记——I2C接口实现相关的知识,希望对你有一定的参考价值。
一、I2C总线介绍
I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。
主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件.在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件.然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下.主机负责产生定时时钟和终止数据传送。
I2C总线是公认的世界标准,由50多家公司生产超过1000个不同的地方实施的集成电路。此外,通用的i2c总线用于各种控制体系结构,如系统管理总线(SMBus),电源管理总线(PMBus),智能平台管理接口(IPMI),显示器数据通道(DDC)和高级电信计算架构(ATCA)。
I2C总线的原理和细节参考这篇文章,感谢作者的整理,受益匪浅。
二、I2C总线协议
-
串行数据线(SDA)和串行时钟线(SCL)
SDA和SCL都是双向的线路,通过电流源或上拉电阻连接到一个正的供应电压。连接到总线的设备的输出级必须有一个开路漏极或开路集电极来执行线和功能。I2C-bus上的数据可以在标准模式下以高达100kbit /s的速率传输,在快速模式下以高达400kbit /s的速率传输,在快速模式+下为1 Mbit/s,在高速模式下最高为3.4 Mbit/s。总线电容限制连接到总线的接口数量。 -
数据有效性
SDA线上的数据必须在SCL时钟线时钟高电平期间保持稳定,在时钟低电平期间改变(见下图)。因此我们在设计信号时,最佳情况就是在时钟线SCL为低电平中间时SDA数据线上的数据改变,在时钟高电平中间时获取SDA数据线上的数据。
-
起始和终止位
所有数据的传输都以START ( S )开始,以STOP ( P )结束(见下图)。
I2C总线空闲:SDA和SCL均为高电平;
I2C协议起始位:SCL为高电平时,SDA出现下降沿;
I2C协议终止位:SCL为高电平时,SDA出现上升沿。
启动和停止条件总是由主设备生成。在启动条件后,总线被认为是忙碌的。该总线在停止条件后的某一段时间内再次空闲。如果生成了重复启动(Sr)而不是停止条件,则总线将保持忙碌状态。在这方面,启动(S)和重复启动(Sr)条件在功能上是相同的。 -
传输1字节格式
在SDA数据线上传输的每个字节长度必须是8位。每次传输可以传输的字节数是不受限制的。每个字节后面必须跟着一个应答位(ACK)。数据从字节最高位(MSB)开始传输(见下图)。如果一个从设备无法接收或发送另一个完整的字节的数据,直到执行一些其他功能,例如服务内部中断,它可以维持时钟线scl为低强迫主设备进入等待状态。当从设备准备好另一个字节的数据后继续传输数据并释放时钟线SCL。
-
应答(ACK)与非应答(NACK)
应答发生在每个字节之后。应答位响应,表明该字节已成功接收,并且可以发送另一个字节。主设备产生所有时钟脉冲,包括确认位第九个时钟脉冲。应答信号定义如下:在响应时钟脉冲期间,发射机释放SDA线,接收机可以把SDA线拉低,,并且在时钟高电平期间稳定保持低电平。
当SDA在第9个时钟脉冲期间保持高时,这被定义为非应答信号。然后,主设备可以生成终止传输 的停止条件,或者生成启动新传输的重复启动条件。NACK的产生有五个条件:
- 总线上没有带有所传输地址的接收者,因此没有设备以应答。
- 接收器无法接收或发送,因为它正在执行一些实时功能,并没有准备好开始与主控通信。
- 在传输过程中,接收方获取它不理解的数据或命令。
- 在传输过程中,接收器无法接收到更多的数据字节。
- 主控接收机必须将传输结束的信号发送给从发射机。
- 从设备器件地址和读写位
从设备地址是I2C协议在传输数据时对总线上设备寻址的依据。数据传输遵循下图所示的格式。在启动条件之后,发送一个从设备器件地址。这个地址有7位长,后面跟着一个数据方向位(R/W)——“0”表示传输(写),“1”表示对数据的请求(读)(参见下2图)。数据传输总是以主设备产生的停止条件§结束。然而,如果一个主人仍然希望在总线上通信,它可以产生一个重复的开始条件(Sr)和地址而不用再产生一个停止条件。
从设备器件地址通常是由固定为和可变位组合而成的。所谓固定位就是器件本就确定无法更改的,认为不能让控制的的,而可变位通常是器件的硬件,可供用户进行硬件连接,按照用户的硬件连接确定,例如下图中7位器件地址中,前四位1010是出厂时就已经固定了的,而后三位是器件硬件引脚可供用户改变的。
对不同的器件,I2C传输格式略有不同,对于存储设备,还具有存储器的地址号,在主设备发送器件地址,从设备存储器响应后,主设备要再发8或16位存储器地址数据,来选择存储器的地址,等待从设备响应后,主设备在发送数据到存储器地址或从存储器地址读取数据。
三、代码实现
I2C接口具有严格的读写时序,包括写数据时序、读数据时序、连续写数据以及连续读时序。本次实验仅实现写数据时序和读数据时序。I2C数据传输都是高位优先。
根据I2C的读写时序图,我们可以发现,读写时序传输顺序:
- 首先,主设备在SCL为高电平时拉低SDA,产生一个起始信号;
- 主设备发送从设备地址位(R/W位为0,写入),在之后的一个时钟周期,主设备释放SDA总线的控制,从设备将SDA拉低,产生一个应答位(ACK);
- 主设备发送要写入或读取的寄存器地址位,在之后的一个时钟周期,主设备释放SDA总线的控制,从设备将SDA拉低,产生一个应答位(ACK);
- (写入数据时) 主设备发送要写入的数据,在之后的一个时钟周期,主设备释放SDA总线的控制,从设备将SDA拉低,产生一个应答位(ACK);
- (读取数据时) 主设备再次产生一个起始信号(这里非常重要!),然后再次发送从设备地址位(R/W位为1,读取),主设备释放SDA总线控制,从设备产生应答位,然后从设备发送数据,主设备不产生应答位;
- 最后,主设备在SCL高电平期间,拉高SDA,产生一个结束信号。
所以可以据此画出状态机图:
废话不多说,上代码:
module IICModule(
input CLK, // 50MHz时钟频率
input reset,
input start, // 启动信号,注意!!!启动信号高电平持续时间最好在一个scl高/低周期左右,不可以很长
input WR_OR_RE, // 读/写控制,0=写,1=读
input [6:0]DeviecAddr, // 从设备器件地址,7bit
input [7:0] RegisterAddr, // 寄存器地址
input [7:0] WriteData, // 要写入的数据
output reg scl,
inout sda,
output reg done,
output reg [7:0] readData // 从从设备读取的8bit数据
);
parameter CLK_FREQ = 50_000_000 , SCL_FREQ = 100_000; // SCL的频率设置为100kHz
localparam SCL_CNT = CLK_FREQ/(2*SCL_FREQ); // 为使signalTap能够观察足够长的窗口时间,我提高了频率,实际不需要除2
// 状态机的状态标识
parameter IDLE =4'd0 ,START =4'd1 ,WR_ADDR =4'd2 ,WR_REGISTER =4'd3 , WR_DATA =4'd4 ,RE_START =4'd5 ,RE_ADDR =4'd6 ,RE_DATA =4'd7 ,STOP =4'd8 ;
reg [7:0] state = IDLE , next_state = IDLE;
// 设备地址
// parameter DeviecAddr = 7'b0011101;
reg [7:0] DeviecAddr_wr;
reg [7:0] DeviecAddr_re;
always@(*)begin
DeviecAddr_wr = {DeviecAddr , 1'b0};
DeviecAddr_re = {DeviecAddr , 1'b1};
end
// scl & scl_cnt ---- 50MHz时钟生成100kHzscl时钟
reg [7:0] scl_cnt;
always@(posedge CLK)begin
if(reset)begin
scl <= 1'b1;
scl_cnt <= 8'd0;
end
else begin
if(scl_cnt == (SCL_CNT/2-1))begin
scl <= ~scl;
scl_cnt <= 8'd0;
end
else begin
scl_cnt <= scl_cnt + 1'b1;
end
end
end
// scl_midHigh ---- 当处于scl高电平段的中间时,置1
reg scl_midHigh;
always@(posedge CLK)begin
if(reset)begin
scl_midHigh <= 1'b0;
end
else begin
if((scl_cnt == (SCL_CNT/4-1)) & (scl == 1'b1))begin
scl_midHigh <= 1'b1;
end
else begin
scl_midHigh <= 1'b0;
end
end
end
// scl_midLow ---- 当处于scl低电平段的中间时,置1
reg scl_midLow;
always@(posedge CLK)begin
if(reset)begin
scl_midLow <= 1'b0;
end
else begin
if((scl_cnt == (SCL_CNT/4-1)) & (scl == 1'b0))begin
scl_midLow <= 1'b1;
end
else begin
scl_midLow <= 1'b0;
end
end
end
// bit_cnt ---- 写器件地址、寄存器、数据等都是一字节(8bit)
// 在scl_midLow和scl_midHigh时均计数(8bit数据位 + 1bit应答位,所以计数范围为0~17)
reg [4:0] bit_cnt;
always@(posedge CLK)begin
if(reset)begin
bit_cnt <= 5'd0;
end
else begin
case(state)
IDLE: bit_cnt <= 5'd0;
START: bit_cnt <= 5'd0;
WR_ADDR: begin
if(scl_midHigh | scl_midLow)begin
bit_cnt <= (bit_cnt == 5'd17 & scl_midLow)?5'd0:(bit_cnt + 1'b1);
end
else begin
bit_cnt <= bit_cnt + 1'b0;
end
end
WR_REGISTER: begin
if(scl_midHigh | scl_midLow)begin
bit_cnt <= (bit_cnt == 5'd17 & scl_midLow)?5'd0:(bit_cnt + 1'b1);
end
else begin
bit_cnt <= bit_cnt + 1'b0;
end
end
WR_DATA: begin
if(scl_midHigh | scl_midLow)begin
bit_cnt <= (bit_cnt == 5'd17 & scl_midLow)?5'd0:(bit_cnt + 1'b1);
end
else begin
bit_cnt <= bit_cnt + 1'b0;
end
end
RE_START: bit_cnt <= 5'd0;
RE_ADDR: begin
if(scl_midHigh | scl_midLow)begin
bit_cnt <= (bit_cnt == 5'd17 & scl_midLow)?5'd0:(bit_cnt + 1'b1);
end
else begin
bit_cnt <= bit_cnt + 1'b0;
end
end
RE_DATA: begin
if(scl_midHigh | scl_midLow)begin
bit_cnt <= (bit_cnt == 5'd17 & scl_midLow)?5'd0:(bit_cnt + 1'b1);
end
else begin
bit_cnt <= bit_cnt + 1'b0;
end
end
STOP: bit_cnt <= 5'd0;
default: bit_cnt <= 5'd0;
endcase
end
end
// link ---- 控制sda(inout信号)的控制权,link == 0 时从设备控制, link == 1 时主设备控制
reg link;
assign sda = link?sda_reg:1'bz; // 1'bz表示由从设备控制数据总线
always@(posedge CLK)begin
if(reset)begin
link <= 1'b1;
end
else begin
case(state)
IDLE: link <= 1'b1;
START: link <= 1'b1;
WR_ADDR: begin
if(bit_cnt < 5'd16)begin
link <= 1'b1;
end
else begin
link <= 1'b0;
end
end
WR_REGISTER: begin
if(bit_cnt < 5'd16)begin
link <= 1'b1;
end
else begin
link <= 1'b0;
end
end
WR_DATA: begin
if(bit_cnt < 5'd16)begin
link <= 1'b1;
end
else begin
link <= 1'b0;
end
end
RE_START: link <= 1'b1;
RE_ADDR: begin
if(bit_cnt < 5'd16)begin
link <= 1'b1;
end
else begin
link <= 1'b0;
end
end
RE_DATA: begin
if(bit_cnt < 5'd16)begin
link <= 1'b0;
end
else begin
link <= 1'b1; // 此时主设备不会产生应答信号,但是也要将控制权交给主设备
end
end
STOP: link <= 1'b1;
default: link <= 1'b1;
endcase
end
end
// ack ---- 当主设备发送8bit数据后,将sda控制权释放,从设备将sda拉低产生应答位
reg ack;
always@(posedge CLK)begin
if(reset)begin
ack <= 1'd0;
end
else begin
case(state)
IDLE: ack <= 1'd0;
START: ack <= 1'd0;
WR_ADDR: begin
if(bit_cnt < 5'd16)begin
ack <= 1'b0;
end
else if(bit_cnt == 5'd16 & scl_midHigh & ~sda)begin
ack <= 1'b1;
end
else
ack <= ack + 1'b0;
end
WR_REGISTER: begin
if(bit_cnt < 5'd16)begin
ack <= 1'b0;
end
else if(bit_cnt == 5'd16 & scl_midHigh & ~sda)begin
ack <= 1'b1;
end
else
ack <= ack + 1'b0;
end
WR_DATA: begin
if(bit_cnt < 5'd16)begin
ack <= 1'b0;
end
else if(bit_cnt == 5'd16 & scl_midHigh & ~sda)begin
ack <= 1'b1;
end
else
ack <= ack + 1'b0;
end
RE_START: ack <= 1'b0;
RE_ADDR: begin
if(bit_cnt < 5'd16)begin
ack <= 1'b0;
end
else if(bit_cnt == 5'd16 & scl_midHigh & ~sda)begin
ack <= 1'b1;
end
else
ack <= ack + 1'b0;
end
RE_DATA: begin
if(bit_cnt < 5'd16)begin
ack <= 1'b0;
end
else if(bit_cnt == 5'd16 & scl_midHigh & ~sda)begin
ack <= 1'b1;
end
else
ack <= ack + 1'b0;
end
STOP: ack <= 1'b0;
default: ack <= 1'b0;
endcase
end
end
// sda_reg_cnt ---- 8bit数据计数,用作数据数组索引
reg [3:0] sda_reg_cnt;
always@(posedge CLK)begin
if(reset)begin
sda_reg_cnt <= 4'd7;
end
else begin
case(state)
IDLE: sda_reg_cnt <= 4'd7;
START: sda_reg_cnt <= 4'd7;
WR_ADDR: begin
if(bit_cnt < 5'd16 & scl_midLow )begin
sda_reg_cnt <= (sda_reg_cnt == 4'd0)?4'd7:(sda_reg_cnt - 1'b1);
end
else begin
sda_reg_cnt <= sda_reg_cnt + 1'b0;
end
end
WR_REGISTER: begin
if(bit_cnt < 5'd16 & scl_midLow )begin
sda_reg_cnt <= (sda_reg_cnt == 4'd0)?4'd7:(sda_reg_cnt - 1'b1);
end
else begin
sda_reg_cnt <= sda_reg_cnt + 1'b0;
end
end
WR_DATA: begin
if(bit_cnt < 5'd16 & scl_midLow )begin
sda_reg_cnt <= (sda_reg_cnt == 4'd0)?4'd7:(sda_reg_cnt - 1'b1);
end
else begin
sda_reg_cnt <= sda_reg_cnt + 1'b0;
end
end
RE_START: sda_reg_cnt <= 4'd7;
RE_ADDR: begin
if(bit_cnt < 5'd16 & scl_midLow )begin
sda_reg_cnt <= (sda_reg_cnt == 4'd0)?4'd7:(sda_reg_cnt - 1'b1);
end
else begin
sda_reg_cnt <= sda_reg_cnt + 1'b0;
end
end
RE_DATA: sda_reg_cnt <= 4'd7;
STOP: sda_reg_cnt <= 4'd7;
default: sda_reg_cnt <= 4'd7;
endcase
end
end
// sda_reg ---- 在不同state时sda输出不同的值,使用sda_reg来控制
reg sda_reg;
always@(posedge CLK)begin
if(reset)begin
sda_reg <= 1'b1;
end
else begin
case(state)
IDLE: sda_reg <= 1'b1;
START: sda_reg <= 1'b0;
WR_ADDR: begin
if(bit_cnt < 5'd16)begin
sda_reg <= DeviecAddr_wr[sda_reg_cnt];
end
else begin
sda_reg <= 1'b1;
end
end
WR_REGISTER: begin
if(bit_cnt < 5'd16)begin
sda_reg <= RegisterAddr[sda_reg_cnt];
end
else begin
sda_reg <= 1'b1; // 这里sda_reg <= 1'b1是为了state==RE_START时方便产生start信号
end
end
WR_DATA: begin
if(bit_cnt < 5'd16)begin
sda_reg <= WriteData[sda_reg_cnt];
end
else begin
sda_reg <= 1'b0; // 这里sda_reg <= 1'b0是为了state==STOP时方便产生stop信号
end
end
RE_START: begin
if(scl_midHigh & scl)begin
sda_reg <= 1'b0;
end
else begin
sda_reg <= sda_reg;
end
end
RE_ADDR: sda_reg <= DeviecAddr_re[sda_reg_cnt];
RE_DATA: sda_reg <= 1'b0; // 这里sda_reg <= 1'b0是为了state==STOP时方便产生stop信号
STOP: begin // 产生stop信号
if(scl_midHigh & scl)
sda_reg <= 1'b1;
else
sda_reg <= sda_reg + 1'b0;
end
default: sda_reg <= 1'b1;
endcase
end
end
// readData ---- 存储从设备发回的数据
always@(posedge CLK)begin
if(reset)begin
readData <= 8'd0;
end
else begin
case(state)
RE_DATA: begin
if(bit_cnt < 5'd16 & scl_midHigh)begin
readData <= {readData[6:0],sda};
//readData[sda_reg_cnt] <= sda;
end
else begin
readData <= readData + 1'b0;
end
end
default: readData <= readData + 1'b0;
endcase
end
end
// done ---- 一次读/写数据完成后置1
always@(posedge CLK)begin
if(reset)begin
done <= 1'b0;
end
else begin
if(state == STOP)
done <= 1'b1;
else
done <= 1'b0;
end
end
// 状态机第一段
always@(posedge CLK)begin
if(reset)begin
state <= IDLE;
end
else begin
state <= next_state;
end
end
// 状态机第二段
always@(*)begin
case(state)
IDLE: next_state = (start&scl)?START:IDLE;
START: next_state = (scl_midLow&~scl)?WR_ADDR:START;
WR_ADDR: begin
if(bit_cnt == 5'd17 && scl_midLow && ack)
以上是关于FPAG学习笔记——I2C接口实现的主要内容,如果未能解决你的问题,请参考以下文章
stm32103C8T6通过I2C接口实现温湿度(AHT20)的采集与OLED显示
紫光同创PGL22G学习四以24LC04为例,上手I2C接口使用(附加学习Opencores开源网站使用 和 Fabric Debugger在线调试)