i2c通信过程
sda和scl
i2c使用scl、sda这两条线进行通信,通信方向是双向的,通信过程有主从之分,是串行、同步的。可以进行多对多的通信,i2c有5种通信速率:标准模式(100 kbps)、快速模式(400 kbps)和高速模式(3.4 Mbps),另外一些变种实现了低速模式(10 kbps)和快速+模式(1 Mbps)。
i2c为了进行多对多的通信引入了时钟同步+总线仲裁的机制:
SCL同步是由于总线具有线“与”的逻辑功能,即只要有一个节点发送低电平时,总线上就表现为低电平。当所有的节点都发送高电平时,总线才能表现为高电平。正是由于线“与”逻辑功能的原理,当多个节点同时发送时钟信号时,在总线上表现的是统一的时钟信号。
各I2C设备的数据串行线SDA也是线“与”的关系。SDA线的仲裁也是建立在总线具有线“与”逻辑功能的原理上的。节点在发送1位数据后,比较总线上所呈现的数据与自己发送的是否一致。是,继续发送;否则,退出竞争。SDA线的仲裁可以保证I2C总线系统在多个主节点同时企图控制总线时通信正常进行并且数据不丢失。总线系统通过仲裁只允许一个主节点可以继续占据总线
sda可进行传输或者解收数据,因此采用inout端口。主机通过地址找到具体的从机设备并实现数据传输或者接收,任何被寻址的器件都被认为是从机。
器件
在执行数据传输时,器件也可以被看作主器件(主机)或从器件(从机)。主器件是用于启动总线传送数据,并产生时钟的器件,此时任何被寻址的器件均被认为是从器件(一主多从)。在总线上主和从、发和收的关系取决于此时的数据传送方向,而不是恒定的。如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件,然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下,主机负责产生定时时钟和终止数据传送。
硬件上都需要接一个上拉电阻到VCC。各种被控制电路均并联在这条总线上,但就像电话机一样只有拨通各自的号码才能工作,所以每个电路和模块都有唯一的地址,这样,各控制电路虽然挂在同一条总线上,却彼此独立,互不相关.
i2c接口设备有很多,包括温度传感器、模数转换芯片以及eeprom等
sda和scl时序图:
读写过程
开始和停止条件:
非传输状态下拉高SCL和SDA,SDA处于下降沿SCL为高电平的时候,表示启动信号。SDA处于上升沿SCL为高电平的时候,表示结束信号。注意,在传输时按照数据位从高到低传输,且数据在scl低电平时改变,数据传输时SDA必须在SCL高电平时保持不变。
对一个器件中的存储单元(包括寄存器)进行读写时,首先要指定存储单元的地址即字地址,然后再向该地址写入内容。该地址为一个或两个字节长度,具体长度由器件内部的存储单元的数量决定,当存储单元数量不超过一个字节所能表示的最大数量(2^8=256)时,用一个字节表示,超过一个字节所能表示的最大数量时,就需要用两个字节来表示
下面给出读写的基本流程:
写数据 |
---|
确认器件地址,写入字地址,将sda设置为out,发送写命令等待从机应答。收到应答后开始写入数据,直到写完数据后主机发送停止数据结束发送过程。在写过程中,只有发送ack或者nack是不受主机控制的 |
1、Master在SCL为高电平期间,拉低SDA,发起START。 |
2、Master发送设备地址(7bit)和写操作0(1bit),等待ACK。 |
3、对应的Slave回应ACK。 |
4、Master发送寄存器字地址(8bit),等待ACK。 |
5、对应的Slave回应ACK。 |
6、Master发送数据(8bit),也就是要写入Slave寄存器中的数据,等待ACK。 |
7、对应的Slave回应ACK。 |
8、其中的6,7步可重复执行多次,即按顺序对多个寄存器进行写操作。如果写入的地址是连续的,那么在得到初始的写入地址后继续向下一个地址写入,不必重新传入地址。连续写又被称为页写 |
9、Master发起STOP。 |
读数据: |
---|
将sda设置为in,发送读命令等待主机应答。收到应答后开始读入数据,直到写完数据后主机停止读取数据并发送停止信号 。在读过程中,发送ack或者nack以及接收数据是不受主机控制的 |
1、Master在SCL为高电平期间,拉低SDA,发起START。 |
2、Master发送设备地址(7bit)和写操作0(1bit),等待ACK。 |
3、Slave发送ACK。 |
4、Master发送寄存器字地址(8bit)并释放SDA信号线,slave控制sda低电平并发送ACK。如果是nack则slave控制sda高电平 |
5、Slave发ACK。 |
6、Master发起START。 |
7、Master发送I2C设备地址(7bit)和读操作1(1bit),等待ACK。 |
8、Slave发送ACK。 |
9、Slave发送data(以字节为单位),即对应寄存器中的值。 |
10、Master发送ACK。 |
11、第9步和第10步可重复进行多次,即按顺序读多个寄存器。 |
在通信中还可能需要知道从机内部的寄存器位置(字地址),这样才能把数据成功传输到我们想要的位置,在传输时每一个byte后面附带一个ack或者nack信号
发送模块应该具有以下接口:clk、rtsn、send_en(用来提示开始发送)、device_addr(设备地址,不同设备位宽不同)、word_addr(字节地址,一般是8位)、data_w(发送有效数据,8位)、done(传输完成信号)、scl串行时钟线与sda串行信号线
lk、rtsn、send_en(用来提示开始发送)、device_addr(设备地址,不同设备位宽不同)、word_addr(字节地址,一般是8位)、data_r(接收有效数据,8位)、done(传输完成信号)、scl串行时钟线与sda串行信号线
接收模块的定义类似,
eeprom
eeprom支持断电后保持数据并可以修改,实验的芯片型号是at24c64,容量为65536bit
器件地址位宽8位, 前4位固定等于1010,5-7位可自行配置所以最大可以支持8个设备,第8位是读写控制位,写为0读为1。sda的控制在fpga和eeprom之间切换
实验任务与代码设计
实验任务是先向EEPROM(AT24C64)的存储器地址0至255分别写入数据0~255; 写完之后再读取存储器地址0-255中的数据,若读取的值全部正确则LED灯常亮,否则LED灯闪烁。
状态机设计:
我们把发送接收模块合在一起编写
module i2c_driver(
input clk,input rstn,
inout sda,output reg scl,
input en,input[15:0] device_addr,
input wl_rh,input bit_ctrl,
output reg ack, input [7:0]data_w,
output reg[7:0]data_r,
output reg done,output reg dri_clk //driver clock
);
parameter clk_freq=50_000_000, i2c_freq= 250_000,
bps_cnt=(clk_freq/i2c_freq)>>2'd3,slav_addr=7'b1_010_000;
localparam
st_idle =8'b0000_0001,st_slv_addr =8'b0000_0010,
st_addr16_wr=8'b0000_0100,st_addr8_wr =8'b0000_1000,
st_data_wr =8'b0001_0000,st_addr_rd =8'b0010_0000,
st_data_rd =8'b0100_0000,st_stop =8'b1000_0000;
wire sda_in;reg sda_out;reg sda_dir;
reg[7:0]cstate,nstate;
reg[7:0]data_rd,data_wr_d0;
reg[15:0]addr_t;
reg done_flag,wr_flag ; //写标志
reg[6:0]cnt;reg[9:0]clk_cnt;
assign sda = sda_dir ? sda_out : 1'bz,
sda_in = sda ;
always @(posedge dri_clk or negedge rstn) begin
if(!rstn)
cstate<=st_idle;
else cstate<=nstate;
end
always @(posedge clk or negedge rstn) begin
if(!rstn)
begin
dri_clk<='d0;
clk_cnt<='d0;
end
else if(clk_cnt==bps_cnt-'d1)
begin
clk_cnt<='d0;
dri_clk<=~dri_clk;
end
else clk_cnt<=clk_cnt+'d1;
end
always @(*) begin
if(!rstn)
nstate=st_idle;
else begin
case(cstate)
st_idle:
if(en)
nstate=st_slv_addr;
else ;
st_slv_addr:
if(done_flag)begin
if(bit_ctrl)// bit_ctrl=1,addr width=16 else addr width=8
nstate=st_addr16_wr;
else nstate=st_addr8_wr;
end
else ;
st_addr16_wr:begin
if(done_flag)nstate=st_addr8_wr;
else ;
end
st_addr8_wr:begin
if(done_flag)begin
if(wr_flag==1'b0) //读写判断
nstate = st_data_wr;
else
nstate = st_addr_rd;
end
else ;
end
st_data_wr :begin
if(done_flag)
nstate=st_stop;
else ;
end
st_addr_rd:begin
if(done_flag)
nstate=st_data_rd;
else ;
end
st_data_rd :begin
if(done_flag)
nstate=st_stop;
else ;
end
st_stop :begin
if(done_flag)
nstate=st_idle;
else ;
end
default : nstate=st_idle;
endcase
end
end
always @(posedge dri_clk or negedge rstn) begin
if(!rstn)
scl<='d1;
else case(cstate)
st_idle: scl<='d1;
st_slv_addr :begin
case(cnt)
7'd3,7'd7,7'd11,7'd15,7'd19,7'd23,
7'd27,7'd31,7'd35,7'd39:scl<='d0;
7'd5,7'd9,7'd13,7'd17,7'd21,
7'd25,7'd29,7'd33,7'd37:scl<='d1;
default :;
endcase
end
st_addr_rd:begin
case(cnt)
7'd1,7'd5,7'd9,7'd13,7'd17,7'd21,
7'd25,7'd29,7'd33,7'd37:scl<='d1;
7'd3,7'd7,7'd11,7'd15,7'd19,7'd23,
7'd27,7'd31,7'd35,7'd39:scl<='d0;
default :;endcase
end
st_addr16_wr, st_addr8_wr,
st_data_wr,st_data_rd: begin
case(cnt)
7'd1,7'd5,7'd9,7'd13,7'd17,7'd21,
7'd25,7'd29,7'd33:scl<='d1;
7'd3,7'd7,7'd11,7'd15,7'd19,7'd23,
7'd27,7'd31,7'd35:scl<='d0;
default:;
endcase
end
st_stop:if(cnt==7'd1)scl<='d1; else ;
default :;
endcase
end
always @(posedge dri_clk or negedge rstn) begin
if(!rstn)
begin
sda_out<='d1;
sda_dir<='d1;
ack<='d0;
cnt<='d0;
done<='d0;
done_flag<='d0;
data_r<='d0;
data_rd<='d0;
data_wr_d0<='d0;
addr_t<='d0;
wr_flag<='d0;
end
else begin
done_flag<='d0;
cnt<=cnt+1'b1;
case(cstate)
st_idle:begin
sda_out<='d1;
sda_dir<='d1;
done<='d0;
cnt<='d0;
if(en)begin// store data when en
addr_t<=device_addr;
data_wr_d0<=data_w;
wr_flag<=wl_rh;
ack<='d0;
end
else ;
end
st_slv_addr :begin
case(cnt)
7'd1:sda_out<='d0;7'd4 : sda_out <= slav_addr[6];
7'd8 : sda_out <= slav_addr[5];7'd12:sda_out <= slav_addr[4];
7'd16 : sda_out <= slav_addr[3];7'd20 : sda_out <= slav_addr[2];
7'd24 : sda_out <= slav_addr[1];7'd28 : sda_out <= slav_addr[0];
7'd32:sda_out<='d0;
7'd36: begin
sda_dir <= 1'b0;sda_out <= 1'b1;
end
7'd38: begin //从机应答
done_flag <= 1'b1;
if(sda_in ) //高电平表示nack
ack <= 1'b1; //拉高应答标志位
end
7'd39:cnt<='d0;
default :;
endcase
end
st_addr16_wr:begin
case(cnt)
7'd0:begin
sda_dir<='d1;
sda_out<=addr_t[15];
end
7'd4:sda_out<=addr_t[14]; 7'd8:sda_out<=addr_t[13];
7'd12:sda_out<=addr_t[12];7'd16:sda_out<=addr_t[11];
7'd20:sda_out<=addr_t[10];7'd24:sda_out<=addr_t[9];
7'd28:sda_out<=addr_t[8];
7'd32: begin
sda_dir <= 1'b0;sda_out <= 1'b1; end
7'd34: begin
done_flag<='d1;if(sda_in) ack<='d1;end
7'd35:cnt<='d0;
default:;
endcase
end
st_addr8_wr :begin
case(cnt)
7'd0:begin
sda_dir<='d1;
sda_out<=addr_t[7];
end
7'd4:sda_out<=addr_t[6]; 7'd8:sda_out<=addr_t[5];
7'd12:sda_out<=addr_t[4];7'd16:sda_out<=addr_t[3];
7'd20:sda_out<=addr_t[2];7'd24:sda_out<=addr_t[1];
7'd28:sda_out<=addr_t[0];
7'd32: begin
sda_dir <= 1'b0;sda_out <= 1'b1; end
7'd34: begin
done_flag<='d1;if(sda_in) ack<='d1;end
7'd35:cnt<='d0;
default:;
endcase
end
st_data_wr :begin
case(cnt)
7'd0:begin
sda_dir<='d1;
sda_out<=data_wr_d0[7];
end
7'd4:sda_out<=data_wr_d0[6]; 7'd8:sda_out<=data_wr_d0[5];
7'd12:sda_out<=data_wr_d0[4];7'd16:sda_out<=data_wr_d0[3];
7'd20:sda_out<=data_wr_d0[2];7'd24:sda_out<=data_wr_d0[1];
7'd28:sda_out<=data_wr_d0[0];
7'd32: begin
sda_dir <= 1'b0;sda_out <= 1'b1; end
7'd34: begin
done_flag<='d1;if(sda_in) ack<='d1;end
7'd35:cnt<='d0;
default:;
endcase
end
st_addr_rd:begin
case(cnt)
7'd0:begin
sda_dir<='d1;sda_out<='d1;end
7'd2:sda_out<='d0;7'd4:sda_out <= slav_addr[6];
7'd8:sda_out <= slav_addr[5];7'd12:sda_out <= slav_addr[4];
7'd16:sda_out <= slav_addr[3];7'd20:sda_out <= slav_addr[2];
7'd24:sda_out <= slav_addr[1];7'd28:sda_out <= slav_addr[0];
7'd32:sda_out<='d1;
7'd36: begin
sda_dir <= 1'b0;sda_out <= 1'b1;
end
7'd38: begin //从机应答
done_flag <= 1'b1;
if(sda_in ) //高电平表示未应答
ack <= 1'b1; //拉高应答标志位
end
7'd39:cnt<='d0;
default :;
endcase
end
st_data_rd :begin
case(cnt)
7'd0:sda_dir<='d0;7'd1:data_rd[7]<=sda_in;
7'd5:data_rd[6]<=sda_in;7'd9:data_rd[5]<=sda_in;
7'd13:data_rd[4]<=sda_in;7'd17:data_rd[3]<=sda_in;
7'd21:data_rd[2]<=sda_in;7'd25:data_rd[1]<=sda_in;
7'd29:data_rd[0]<=sda_in;
7'd32: begin
sda_dir <= 1'b1;
sda_out <= 1'b1;
end
7'd34:done_flag<='d1;
7'd35: begin data_r<=data_rd; cnt<='d0; end
default:;
endcase
end
st_stop :begin
case(cnt)
7'd0:begin sda_dir<='d1 ; sda_out<='d0;end
7'd3:sda_out<='d1;
7'd15:done_flag<='d1;
7'd16: begin done<='d1;cnt<='d0; end
endcase
end
default:; endcase
end
end
endmodule
tesdtbench文件:
`timescale 1ns/1ns
module i2c_driver_tb();
parameter T=20,wr_cycle = 10_000;
reg clk, rstn,en,wl_rh,bit_ctrl;
reg[15:0]device_addr;
reg[7:0]data_w;
reg [3:0] flow_cnt ;
reg [13:0] delay_cnt ;
wire scl,ack,done, dri_clk,sda;
wire [7:0]data_r;
always #(T/2)clk=~clk;
initial begin
clk=0;
rstn=0;
#(T+1) rstn=1;
end
always @(posedge dri_clk or negedge rstn) begin
if(!rstn)
begin
en<='d0;bit_ctrl<='d0;
wl_rh<='d0;device_addr<='d0;
data_w<='d0;delay_cnt<='d0;
flow_cnt<='d0;
end
else begin
case(flow_cnt)
'd0:flow_cnt<=flow_cnt+1'b1;
'd1:begin
en<='d1;
bit_ctrl<='d1;
wl_rh<='d0;
device_addr<='h0555;
data_w<=8'h9c;
flow_cnt<=flow_cnt+1'b1;
end
'd2:begin
en<='d0;
flow_cnt<=flow_cnt+'d1;
end
'd3:begin
if(done)flow_cnt<=flow_cnt+'d1;
end
'd4:begin
delay_cnt<=delay_cnt+'d1;
if(delay_cnt==wr_cycle-'d1)// add delay_cnt until finish write cycle
flow_cnt<=flow_cnt+'d1;
end
'd5:begin
en<='d1;
bit_ctrl<='d1;
wl_rh<='d1;
device_addr<='h0555;
data_w<=8'h3a;
flow_cnt<=flow_cnt+1'b1;
end
'd6:begin
en<='d0;
flow_cnt<=flow_cnt+1'b1;
end
'd7:if(done) flow_cnt<=flow_cnt+1'b1;
default :;
endcase
end
end
pullup(sda);
i2c_driver i2c_driver_u(
.clk(clk),
.rstn(rstn),
.sda(sda),
.scl(scl),
.en(en),
.device_addr(device_addr),
.wl_rh(wl_rh),
.bit_ctrl(bit_ctrl),
.ack(ack),
.data_r(data_r),
.data_w(data_w),
.done(done),
.dri_clk(dri_clk)
);
EEPROM_AT24C64 eeprom_u(
.scl(scl),
.sda(sda)
);
endmodule
仿真结果:
eeprom模块:
module eeprom_driver(
input clk,rstn,
output reg wl_rh,en,
output reg[15:0]device_addr,
output reg[7:0]data_w,
input [7:0]data_r,
input done, ack,
output reg rw_done,output reg suce
);
parameter max_byte=256,//max data unm=256
wait_time= 5_000;//delay 5 ms
reg [1:0] flow_cnt ;
reg [13:0] delay_cnt ;
always @(posedge clk or negedge rstn) begin
if(!rstn)
delay_cnt<='d0;
else begin
case(flow_cnt)
2'd0:begin
if(delay_cnt==wait_time-'d1)
delay_cnt<='d0;
else delay_cnt<=delay_cnt+'d1;
end
default:;
endcase
end
end
always @(posedge clk or negedge rstn) begin
if(!rstn)begin
flow_cnt<='d0;
delay_cnt<='d0;
wl_rh<='d0;
en<='d0;
device_addr<='d0;
data_w<='d0;
rw_done<='d0;
suce<='d0;
end
else begin
en<='d0;
rw_done<='d0;
case(flow_cnt)
2'd0:begin
if(delay_cnt==wait_time-'d1)
begin
if(device_addr==max_byte)
begin
device_addr<='d0;
wl_rh<='d1;
flow_cnt<=2'd2;
end
else begin flow_cnt<=flow_cnt+'d1;
en<='d1; end//start wr or rd
end
end
2'd1: begin
if(done)begin
flow_cnt<=2'd0;
device_addr<=device_addr+'d1;
data_w<=data_w+'d1;
end
end
2'd2:begin//when write dfata fulliy, jump to step 2
flow_cnt<=flow_cnt+'d1;
en<='d1;
end
2'd3:begin//read written data
if(done)
if(device_addr[7:0]!=data_r||ack)begin//write data failed
rw_done<='d1;
suce<='d0;
end
else if(device_addr==max_byte-'d1) begin
rw_done<='d1;
suce<='d1;//write all data,finish process
end
else begin
flow_cnt<='d2;
device_addr<=device_addr+'d1;
end
end
default:;
endcase
end
end
endmodule
led模块
module led_light(
input clk,rstn,
input rw_done,input suce,output reg led
);
parameter clk_freq=50_000_000, i2c_freq= 250_000,
bps_cnt=(clk_freq/i2c_freq)>>2'd3,
slav_addr=7'b1_010_000,bit_ctrl='d1,
ltime=18'd225_000;
reg rw_done_flag;
reg[24:0]led_cnt;
always @(posedge clk or negedge rstn) begin
if(!rstn)
begin
rw_done_flag<='d0;
end
else if(rw_done)
begin
rw_done_flag<='d1;
end
else ;
end
always @(posedge clk or negedge rstn) begin
if(!rstn)
begin
led_cnt<='d0;
led<='d0;
end
else begin
if(rw_done_flag)
begin
if(suce)
led<='d1;
else begin
led_cnt<=led_cnt+25'd1;
if(led_cnt==ltime-'d1)
begin
led_cnt<='d0;
led<=~led;
end
end
end
else led<='d0;
end
end
endmodule
顶层模块:
module e2prom_top(
input clk , //系统时钟
input rstn , //系统复位
//eeprom interface
output scl , //eeprom的时钟线scl
inout sda , //eeprom的数据线sda
//user interface
output led //led显示
);
//parameter define
parameter SLAVE_ADDR = 7'b1010000 ; //器件地址(SLAVE_ADDR)
parameter BIT_CTRL = 1'b1 ; //字地址位控制参数(16b/8b)
parameter CLK_FREQ = 26'd50_000_000 ; //i2c_dri模块的驱动时钟频率(CLK_FREQ)
parameter I2C_FREQ = 18'd250_000 ; //I2C的SCL时钟频率
parameter L_TIME = 17'd125_000 ; //led闪烁时间参数
//wire define
wire dri_clk ; //I2C操作时钟
wire en ; //I2C触发控制
wire [15:0] device_addr ; //I2C操作地址
wire [ 7:0] data_w; //I2C写入的数据
wire [ 7:0] data_r; //I2C读出的数据
wire done ; //I2C操作结束标志
wire ack ; //I2C应答标志 0:应答 1:未应答
wire wl_rh ; //I2C读写控制
wire rw_done ; //E2PROM读写测试完成
wire suce ; //E2PROM读写测试结果 0:失败 1:成功
//*****************************************************
//** main code
//*****************************************************
//e2prom读写测试模块
eeprom_driver u_e2prom_rw(
.clk (dri_clk ), //时钟信号
.rstn (rstn), //复位信号
//i2c interface
.en (en ), //I2C触发执行信号
.wl_rh (wl_rh ), //I2C读写控制信号
.device_addr (device_addr ), //I2C器件内地址
.data_w (data_w), //I2C要写的数据
.data_r (data_r), //I2C读出的数据
.done (done ), //I2C一次操作完成
.ack (ack ), //I2C应答标志
//user interface
.rw_done (rw_done ), //E2PROM读写测试完成
.suce (suce ) //E2PROM读写测试结果 0:失败 1:成功
);
//i2c驱动模块
i2c_driver #(
.slav_addr (SLAVE_ADDR), //EEPROM从机地址
.clk_freq (CLK_FREQ ), //模块输入的时钟频率
.i2c_freq (I2C_FREQ ) //IIC_SCL的时钟频率
) u_i2c_dri(
.clk (clk ),
.rstn (rstn ),
//i2c interface
.en (en ), //I2C触发执行信号
.bit_ctrl (BIT_CTRL ), //器件地址位控制(16b/8b)
.wl_rh (wl_rh ), //I2C读写控制信号
.device_addr (device_addr ), //I2C器件内地址
.data_w (data_w), //I2C要写的数据
.data_r (data_r), //I2C读出的数据
.done (done ), //I2C一次操作完成
.ack (ack ), //I2C应答标志
.scl (scl ), //I2C的SCL时钟信号
.sda (sda ), //I2C的SDA信号
.dri_clk (dri_clk ) //I2C操作时钟
);
//led指示模块
led_light u_led_alarm(
.clk (dri_clk ),
.rstn (rstn),
.rw_done (rw_done ),
.suce (suce ),
.led (led )
);
endmodule
当然,也可以在顶层添加ila查看分析结果,不过个人试过后发现效果不太好,应该是深度不够导致后面的数据还没有出现
参考博客
协议基本知识:
https://www.cnblogs.com/yjw951012/p/11594694.html
verilog代码实现:
https://www.cnblogs.com/liujinggang/p/9656358.html
eeprom实验:
https://blog.csdn.net/mikusic/article/details/114936770
相关硬件介绍和eeprom实验:
https://www.amobbs.com/thread-5758040-1-5.html