I2C协议
IIC 协议是三种最常用的串行通信协议(I2C,SPI,UART)之一,接口包含 SDA(串行数据线)和 SCL(串行时钟线),均为双向端口。I2C 仅使用两根信号线,极大地减少了连接线的数量,支持多主多从,且具有应答机制,因此在片间通信有较多的应用。
I2C 主要包括四个状态:起始 START,数据传送 SEND,应答 ACK,停止 STOP。
- 传输起始
当 SCL 为高电平,SDA 出现下跳变时,标志着传输的起始。
- 数据传输
在传输数据位时,采用大端传输(即先传最高位 MSB),SDA 在SCL 低电平时改变,在 SCL=H 时,必须保持 SDA 稳定。
- 应答
在传输完 8bit 数据后,Master 须释放 SDA ,Slave 接过 SDA 的控制权,给出应答信号 ACK,当 ACK=L 时,表示本字节数据传输有效。
- 停止
当 SCL 为高,SDA 出现上跳变时,标志着传输的结束。
一次 I2C 传输可以传输多个字节,通常第一个字节为 I2C 设备地址 ADDR(7bit)和读写标志 R / W ‾ \rm{R/\overline W} R/W(1bit)。一个可能的 I2C 例子如下:
Verilog实现
I2C的时序相对而言较复杂,因此实现方法自然是万能的三段式状态机(状态机大法好,状态机大法万岁!)
SCL/SDA 状态机输出控制
不同的 I2C 设备可能具有不同的读写序列,因此这里首先实现 Master 与 Slave 的状态机输出的子模块(即三段式状态机的第三段),分别为 I2C_Master_sub、I2C_Slave_sub,顶层模块只需要合理安排状态转移,即可实现各种 I2C 读写时序!
为了方便地控制 SDA 和 SCL ,Master 将一个 SCL 周期划分为 4 段;Slave 为了检测 SDA 和 SCL 的边沿并及时做出响应,须采用 8 倍以上的时钟。
- I2C_Master_sub.v
/*
* file : I2C_Master_sub.v
* author : 今朝无言
* Lab : WHU-EIS-LMSWE
* date : 2023-03-19
* version : v1.0
* description : I2C master 的 SDA/SCL 控制模块(通过 state)
*/
module I2C_Master_sub(
input clk, //4倍SCL
input [7:0] wrdat_buf,
output reg [7:0] rddat_tmp,
output reg check_ack, //检查Slave给出的ACK信号,若为NACK,输出一个高电平脉冲
inout SCL,
inout SDA,
output reg change_state, //上升沿时 top 模块应执行 state <- next_state
input [7:0] state
);
localparam IDLE = 8'h01; //空闲,释放SCL/SDA
localparam START = 8'h02; //起始,SCL=H,SDA=D
localparam SEND_DATA = 8'h04; //发送数据
localparam GET_DATA = 8'h08; //读取数据,释放SDA
localparam CHECK_ACK = 8'h10; //检查SDA的ACK/NACK,释放SDA
localparam ACK = 8'h20; //发出ACK,SDA=L
localparam NACK = 8'h40; //发出NACK,SDA=H
localparam STOP = 8'h80; //停止,SCL=H,SDA=R
reg SCL_link = 1'b0;
reg SDA_link = 1'b0;
reg SCL_buf = 1'b1; //o_buf
reg SDA_buf = 1'b1;
wire SCL_ibuf; //i_buf
wire SDA_ibuf;
reg [3:0] bit_cnt = 4'd15;
//----------------------IO_BUF-----------------------------
//IOBUF fo SCL
IOBUF IOBUF_SCL(
.O (SCL_ibuf), // Buffer output Buffer的输出,接采集信号
.IO (SCL), // Buffer inout port (connect directly to top-level port)
.I (SCL_buf), // Buffer input Buffer的输入,接要输出到FPGA外的信号
.T (~SCL_link) // 3-state enable input, high=input, low=output =1时,O <- IO;=0时,IO <- I
);
//IOBUF fo SDA
IOBUF IOBUF_SDA(
.O (SDA_ibuf),
.IO (SDA),
.I (SDA_buf),
.T (~SDA_link)
);
//---------------------clk div-----------------------------
//将一个SCL周期划分为4份,便于逻辑实现
reg [1:0] clk_cnt = 2'd0;
always @(posedge clk) begin
clk_cnt <= clk_cnt + 1'b1;
end
//---------------------SCL_link-----------------------------
always @(posedge clk) begin
case(state)
IDLE: begin
SCL_link <= 1'b0;
end
START, SEND_DATA, GET_DATA, CHECK_ACK, ACK, NACK, STOP: begin
SCL_link <= 1'b1;
end
default: begin
SCL_link <= 1'b0;
end
endcase
end
//---------------------SDA_link-----------------------------
always @(posedge clk) begin
case(state)
IDLE, GET_DATA, CHECK_ACK: begin
SDA_link <= 1'b0;
end
START, SEND_DATA, ACK, NACK, STOP: begin
SDA_link <= 1'b1;
end
default: begin
SDA_link <= 1'b0;
end
endcase
end
//---------------------SCL_buf-----------------------------
always @(posedge clk) begin
case(state)
IDLE: begin //1111
SCL_buf <= 1'b1;
end
START: begin //1110
case(clk_cnt)
2'd0, 2'd1, 2'd2: begin
SCL_buf <= 1'b1;
end
2'd3: begin
SCL_buf <= 1'b0;
end
default: ;
endcase
end
STOP: begin //0111
case(clk_cnt)
2'd1, 2'd2, 2'd3: begin
SCL_buf <= 1'b1;
end
2'd0: begin
SCL_buf <= 1'b0;
end
default: ;
endcase
end
SEND_DATA, GET_DATA, CHECK_ACK, ACK, NACK: begin //0110
case(clk_cnt)
2'd1, 2'd2: begin
SCL_buf <= 1'b1;
end
2'd0, 2'd3: begin
SCL_buf <= 1'b0;
end
default: ;
endcase
end
default: begin //1111
SCL_buf <= 1'b1;
end
endcase
end
//---------------------bit_cnt-----------------------------
always @(posedge clk) begin
case(state)
SEND_DATA, GET_DATA: begin
case(clk_cnt)
2'd2: begin
bit_cnt <= bit_cnt - 1'b1;
end
default: ;
endcase
end
START, ACK, NACK, CHECK_ACK: begin
bit_cnt <= 4'd7;
end
default: begin
bit_cnt <= 4'd15;
end
endcase
end
//--------------------rddat_tmp----------------------------
always @(posedge clk) begin
case(state)
GET_DATA: begin
case(clk_cnt)
2'd1: begin
rddat_tmp[bit_cnt] <= SDA_ibuf;
end
default: ;
endcase
end
default: begin
rddat_tmp <= rddat_tmp;
end
endcase
end
//--------------------check_ack----------------------------
always @(posedge clk) begin
case(state)
CHECK_ACK: begin
case(clk_cnt)
2'd1: begin
check_ack <= SDA_ibuf;
end
default: begin
check_ack <= check_ack;
end
endcase
end
default: begin
check_ack <= 0;
end
endcase
end
//---------------------SDA_buf-----------------------------
always @(posedge clk) begin
case(state)
IDLE: begin
SDA_buf <= 1'b1;
end
START: begin //1100,从而在SCL=H时,产生SDA=D
case(clk_cnt)
2'd0, 2'd1: begin
SDA_buf <= 1'b1;
end
2'd2, 2'd3: begin
SDA_buf <= 1'b0;
end
default: ;
endcase
end
SEND_DATA: begin //在clk_cnt=0给出数据,从而在clk_cnt=1,2时(SCL=H)保持SDA的稳定
case(clk_cnt)
2'd0: begin
SDA_buf <= wrdat_buf[bit_cnt];
end
default: ;
endcase
end
GET_DATA: begin
SDA_buf <= 1'b1;
end
CHECK_ACK: begin
SDA_buf <= 1'b0;
end
ACK: begin
SDA_buf <= 1'b0;
end
NACK: begin
SDA_buf <= 1'b1;
end
STOP: begin //0011,从而在SCL=H时,产生SDA=R
case(clk_cnt)
2'd0, 2'd1: begin
SDA_buf <= 1'b0;
end
2'd2, 2'd3: begin
SDA_buf <= 1'b1;
end
default: ;
endcase
end
default: begin
SDA_buf <= 1'b1;
end
endcase
end
//-------------------change_state---------------------------
always @(posedge clk) begin
case(state)
IDLE, ACK, NACK, CHECK_ACK, STOP: begin
case(clk_cnt)
2'd3: begin
change_state <= 1'b1;
end
default: begin
change_state <= 1'b0;
end
endcase
end
SEND_DATA, GET_DATA: begin
case(bit_cnt)
4'd15: begin
case(clk_cnt)
2'd3: begin
change_state <= 1'b1;
end
default: begin
change_state <= 1'b0;
end
endcase
end
default: begin
change_state <= 1'b0;
end
endcase
end
default: begin
case(clk_cnt)
2'd3: begin
change_state <= 1'b1;
end
default: begin
change_state <= 1'b0;
end
endcase
end
endcase
end
endmodule
- I2C_Slave_sub.v
/*
* file : I2C_Slave_sub.v
* author : 今朝无言
* Lab : WHU-EIS-LMSWE
* date : 2023-03-19
* version : v1.0
* description : I2C Slave 的 SDA/SCL 控制模块(通过 state)
*/
module I2C_Slave_sub(
input clk, //SCL的8倍以上
input [7:0] wrdat_buf,
output reg [7:0] rddat_tmp,
output reg check_ack, //检查Master给出的ACK信号,若为NACK,输出一个高电平脉冲
inout SCL,
inout SDA,
output reg change_state, //上升沿时 top 模块应执行 state <- next_state
input [7:0] state,
output reg busy
);
localparam IDLE = 8'h01; //空闲
localparam START = 8'h02; //起始,检测到SCL=H,SDA=D,
localparam SEND_DATA = 8'h04; //Slave发送数据,接管SDA控制权
localparam GET_DATA = 8'h08; //Slave读取数据
localparam CHECK_ACK = 8'h10; //检查SDA的ACK/NACK
localparam ACK = 8'h20; //发出ACK,SDA=L,接管SDA控制权
localparam NACK = 8'h40; //发出NACK,SDA=H,接管SDA控制权
localparam STOP = 8'h80; //停止,检测到SCL=H,SDA=R
//不实现Clock Stretching功能,因此Slave从不试图接管SCL
//除注释注明的状态外,不获取SDA控制权
wire SCL_link;
reg SDA_link = 1'b0;
reg SCL_buf = 1'b1; //o_buf
reg SDA_buf = 1'b1;
wire SCL_ibuf; //i_buf
wire SDA_ibuf;
assign SCL_link = 1'b0;
//----------------------IO_BUF-----------------------------
//IOBUF fo SCL
IOBUF IOBUF_SCL(
.O (SCL_ibuf), //Buffer的输出,接采集信号
.IO (SCL),
.I (SCL_buf), //Buffer的输入,接要输出到FPGA外的信号
.T (~SCL_link) //=1时,O <- IO;=0时,IO <- I
);
//IOBUF fo SDA
IOBUF IOBUF_SDA(
.O (SDA_ibuf),
.IO (SDA),
.I (SDA_buf),
.T (~SDA_link)
);
//--------------------------busy----------------------------
reg busy_d0;
reg busy_d1;
wire busy_pe;
wire busy_ne;
always @(SDA_ibuf) begin
if(~SDA_ibuf & SCL_ibuf) begin // SCL=H,SDA=D,接收起始
busy <= 1'b1;
end
else if(SDA_ibuf & SCL_ibuf) begin // SCL=H,SDA=R,接收结束
busy <= 1'b0;
end
else begin
busy <= busy;
end
end
always @(posedge clk) begin
busy_d0 <= busy;
busy_d1 <= busy_d0;
end
assign busy_pe = busy_d0 & (~busy_d1);
assign busy_ne = (~busy_d0) & busy_d1;
//--------------------------edge----------------------------
reg SDA_d0;
reg SDA_d1;
wire SDA_pe;
wire SDA_ne;
reg SCL_d0;
reg SCL_d1;
wire SCL_pe;
wire SCL_ne;
always @(posedge clk) begin
SDA_d0 <= SDA_ibuf;
SDA_d1 <= SDA_d0;
SCL_d0 <= SCL_ibuf;
SCL_d1 <= SCL_d0;
end
assign SDA_pe = SDA_d0 & (~SDA_d1);
assign SDA_ne = (~SDA_d0) & SDA_d1;
assign SCL_pe = SCL_d0 & (~SCL_d1);
assign SCL_ne = (~SCL_d0) & SCL_d1;
//-----------------------SCL_cnt----------------------------
reg [3:0] SCL_cnt; //计算当前是第几个SCL_pe
always @(posedge clk) begin
if(busy_pe) begin
SCL_cnt <= 4'd0;
end
else if(SCL_ne && SCL_cnt==4'd9) begin
SCL_cnt <= 4'd0;
end
else if(SCL_pe) begin
SCL_cnt <= SCL_cnt + 1'b1;
end
else begin
SCL_cnt <= SCL_cnt;
end
end
//---------------------change_state--------------------------
always @(posedge clk) begin
case(state)
IDLE: begin
if(busy_pe) begin
change_state <= 1'b1;
end
else begin
change_state <= 1'b0;
end
end
START: begin
if(SCL_ne) begin
change_state <= 1'b1;
end
else begin
change_state <= 1'b0;
end
end
SEND_DATA, GET_DATA: begin
if(SCL_ne && SCL_cnt==4'd8) begin
change_state <= 1'b1;
end
else begin
change_state <= 1'b0;
end
end
ACK, NACK, CHECK_ACK: begin
if(SCL_ne) begin
change_state <= 1'b1;
end
else begin
change_state <= 1'b0;
end
end
STOP: begin
if(busy_ne) begin
change_state <= 1'b1;
end
else begin
change_state <= 1'b0;
end
end
default: begin
change_state <= 1'b0;
end
endcase
end
//-----------------------SDA_link----------------------------
always @(posedge clk) begin
case(state)
SEND_DATA, ACK, NACK: begin
SDA_link <= 1'b1;
end
default: begin
SDA_link <= 1'b0;
end
endcase
end
//----------------------check_ack----------------------------
always @(posedge clk) begin
case(state)
CHECK_ACK: begin
if(SCL_pe) begin
check_ack <= SDA_ibuf;
end
else begin
check_ack <= 1'b0;
end
end
default: begin
check_ack <= 1'b0;
end
endcase
end
//----------------------rddat_tmp----------------------------
always @(posedge clk) begin
case(state)
GET_DATA: begin
if(SCL_pe) begin
rddat_tmp[7 - SCL_cnt] <= SDA_ibuf;
end
else ;
end
default: ;
endcase
end
//-----------------------SDA_buf-----------------------------
always @(posedge clk) begin
case(state)
SEND_DATA: begin
if(SCL_ne || change_state) begin
SDA_buf <= wrdat_buf[7 - SCL_cnt];
end
else begin
SDA_buf <= SDA_buf;
end
end
ACK: begin
SDA_buf <= 1'b0;
end
NACK: begin
SDA_buf <= 1'b1;
end
default: begin
SDA_buf <= 1'b1;
end
endcase
end
endmodule
Master 读/写子模块
基于 Master_sub 状态机输出控制子模块,分别搭建 Master 读/写控制子模块例程如下(这里实现的是比较常规的 I2C 读写时序,要实现更加具体的读写时序可参考该例程自行实现)
- I2C_Master_Write.v
/*
* file : I2C_Master_Write.v
* author : 今朝无言
* Lab : WHU-EIS-LMSWE
* date : 2023-03-20
* version : v1.0
* description : I2C写功能
* Copyright © 2023 WHU-EIS-LMSWE, All Rights Reserved.
*/
module I2C_Master_Write(
input clk, //4倍SCL
input wr_en, //上升沿有效
output reg wrdat_req, //上升沿驱动上层模块给出wrdat
input [7:0] wrdat,
output busy,
output check_ack, //检查Slave给出的ACK信号,若为NACK,输出一个高电平脉冲
inout SCL,
inout SDA
);
// S {ADDR,RW_W} A DATA A ... DATA A P
parameter ADDR = 7'h11; //I2C设备地址
parameter WR_DATA_LEN = 16'd1; //写的数据个数
localparam RW_W = 1'b0;
localparam RW_R = 1'b1;
//---------------------I2C Master State Define----------------------
localparam IDLE = 8'h01; //空闲,释放SCL/SDA
localparam START = 8'h02; //起始,SCL=H,SDA=D
localparam SEND_DATA = 8'h04; //发送数据
localparam GET_DATA = 8'h08; //读取数据,释放SDA
localparam CHECK_ACK = 8'h10; //检查SDA的ACK/NACK,释放SDA
localparam ACK = 8'h20; //发出ACK,SDA=L
localparam NACK = 8'h40; //发出NACK,SDA=H
localparam STOP = 8'h80; //停止,SCL=H,SDA=R
//------------------------------------------------------------------
reg [7:0] state = IDLE;
reg [7:0] next_state = IDLE;
reg start_flag = 1'b0;
wire change_state;
reg [7:0] wrdat_buf = 8'd0;
reg [15:0] data_cnt = 16'd0;
//------------------------start_flag--------------------------------
reg wr_en_d0;
reg wr_en_d1;
wire wr_en_pe;
always @(posedge clk) begin
wr_en_d0 <= wr_en;
wr_en_d1 <= wr_en_d0;
end
assign wr_en_pe = wr_en_d0 & (~wr_en_d1);
assign busy = (state == IDLE)? 1'b0 : 1'b1;
always @(posedge clk) begin
if(wr_en_pe && ~busy) begin
start_flag <= 1'b1;
end
else if(state == START) begin
start_flag <= 1'b0;
end
else begin
start_flag <= start_flag;
end
end
//-------------------------State Machine----------------------------
always @(posedge change_state) begin
state <= next_state;
end
//状态转移
always @(*) begin
case(state)
IDLE: begin
if(start_flag) begin
next_state <= START;
end
else begin
next_state <= IDLE;
end
end
START: begin
next_state <= SEND_DATA;
end
SEND_DATA: begin
next_state <= CHECK_ACK;
end
CHECK_ACK: begin
if(data_cnt == 1 && check_ack) begin //第一个CHECK检测到NACK,STOP
next_state <= STOP;
end
else begin
if(data_cnt > WR_DATA_LEN) begin
next_state <= STOP;
end
else begin
next_state <= SEND_DATA;
end
end
end
STOP: begin
next_state <= IDLE;
end
default: begin
next_state <= IDLE;
end
endcase
end
//三段式状态机第三段,I2C Master sub
I2C_Master_sub I2C_Master_sub_inst(
.clk (clk),
.wrdat_buf (wrdat_buf),
.rddat_tmp (),
.check_ack (check_ack),
.SCL (SCL),
.SDA (SDA),
.change_state (change_state),
.state (state)
);
// -----data_req-----
always @(*) begin
case(state)
CHECK_ACK: begin
if(data_cnt <= WR_DATA_LEN) begin
wrdat_req <= 1'b1;
end
else begin
wrdat_req <= 1'b0;
end
end
default: begin
wrdat_req <= 1'b0;
end
endcase
end
// -----data_cnt-----
always @(posedge change_state) begin
case(state)
IDLE: begin
data_cnt <= 16'd0;
end
SEND_DATA: begin
data_cnt <= data_cnt + 1'b1;
end
default: begin
data_cnt <= data_cnt;
end
endcase
end
// -----wrdat_buf-----
always @(posedge change_state) begin
case(state)
IDLE: begin
wrdat_buf <= 8'd0;
end
START: begin
wrdat_buf <= {ADDR, RW_W};
end
CHECK_ACK: begin
wrdat_buf <= wrdat;
end
default: begin
wrdat_buf <= wrdat_buf;
end
endcase
end
endmodule
- I2C_Master_Read.v
/*
* file : I2C_Master_Read.v
* author : 今朝无言
* Lab : WHU-EIS-LMSWE
* date : 2023-03-20
* version : v1.0
* description : I2C读功能
* Copyright © 2023 WHU-EIS-LMSWE, All Rights Reserved.
*/
module I2C_Master_Read(
input clk, //4倍SCL
input rd_en, //上升沿有效
output reg rddat_vaild,
output reg [7:0] rddat,
output busy,
output check_ack,
inout SCL,
inout SDA
);
// S {ADDR,RW_R} A DATA A ... DATA A P
parameter ADDR = 7'h11; //I2C设备地址
parameter RD_DATA_LEN = 16'd1; //读的数据个数
localparam RW_W = 1'b0;
localparam RW_R = 1'b1;
//---------------------I2C Master State Define----------------------
localparam IDLE = 8'h01; //空闲,释放SCL/SDA
localparam START = 8'h02; //起始,SCL=H,SDA=D
localparam SEND_DATA = 8'h04; //发送数据
localparam GET_DATA = 8'h08; //读取数据,释放SDA
localparam CHECK_ACK = 8'h10; //检查SDA的ACK/NACK,释放SDA
localparam ACK = 8'h20; //发出ACK,SDA=L
localparam NACK = 8'h40; //发出NACK,SDA=H
localparam STOP = 8'h80; //停止,SCL=H,SDA=R
//------------------------------------------------------------------
reg [7:0] state = IDLE;
reg [7:0] next_state = IDLE;
reg start_flag = 1'b0;
wire change_state;
reg [7:0] wrdat_buf = 8'd0;
wire [7:0] rddat_tmp;
reg [15:0] data_cnt = 16'd0;
//------------------------start_flag--------------------------------
reg rd_en_d0;
reg rd_en_d1;
wire rd_en_pe;
always @(posedge clk) begin
rd_en_d0 <= rd_en;
rd_en_d1 <= rd_en_d0;
end
assign rd_en_pe = rd_en_d0 & (~rd_en_d1);
assign busy = (state == IDLE)? 1'b0 : 1'b1;
always @(posedge clk) begin
if(rd_en_pe && ~busy) begin
start_flag <= 1'b1;
end
else if(state == START) begin
start_flag <= 1'b0;
end
else begin
start_flag <= start_flag;
end
end
//-------------------------State Machine----------------------------
always @(posedge change_state) begin
state <= next_state;
end
//状态转移
always @(*) begin
case(state)
IDLE: begin
if(start_flag) begin
next_state <= START;
end
else begin
next_state <= IDLE;
end
end
START: begin
next_state <= SEND_DATA;
end
SEND_DATA: begin
next_state <= CHECK_ACK;
end
CHECK_ACK: begin
if(check_ack) begin //检测到NACK,STOP
next_state <= STOP;
end
else begin
next_state <= GET_DATA;
end
end
GET_DATA: begin
next_state <= ACK;
end
ACK: begin
if(data_cnt >= RD_DATA_LEN) begin
next_state <= STOP;
end
else begin
next_state <= GET_DATA;
end
end
STOP: begin
next_state <= IDLE;
end
default: begin
next_state <= IDLE;
end
endcase
end
//三段式状态机第三段,I2C Master sub
I2C_Master_sub I2C_Master_sub_inst(
.clk (clk),
.wrdat_buf (wrdat_buf),
.rddat_tmp (rddat_tmp),
.check_ack (check_ack),
.SCL (SCL),
.SDA (SDA),
.change_state (change_state),
.state (state)
);
// -----data_valid-----
always @(*) begin
case(state)
ACK: begin
rddat_vaild <= 1'b1;
end
default: begin
rddat_vaild <= 1'b0;
end
endcase
end
// -----data_cnt-----
always @(posedge change_state) begin
case(state)
IDLE: begin
data_cnt <= 16'd0;
end
GET_DATA: begin
data_cnt <= data_cnt + 1'b1;
end
default: begin
data_cnt <= data_cnt;
end
endcase
end
// -----rddat-----
always @(posedge change_state) begin
case(state)
IDLE: begin
rddat <= rddat;
end
GET_DATA: begin
rddat <= rddat_tmp;
end
default: begin
rddat <= rddat;
end
endcase
end
// ---wrdat_buf---
always @(posedge change_state) begin
case(state)
IDLE: begin
wrdat_buf <= 8'd0;
end
START: begin
wrdat_buf <= {ADDR, RW_R};
end
CHECK_ACK: begin
wrdat_buf <= 8'd0;
end
default: begin
wrdat_buf <= wrdat_buf;
end
endcase
end
endmodule
Slave 读/写子模块
同样,基于 Slave_sub 设计 Slave 的读/写控制子模块例程如下
- I2C_Slave_Receive.v
/*
* file : I2C_Slave_Receive.v
* author : 今朝无言
* Lab : WHU-EIS-LMSWE
* date : 2023-03-21
* version : v1.0
* description : 作为Slave<接收>数据
* Copyright © 2023 WHU-EIS-LMSWE, All Rights Reserved.
*/
module I2C_Slave_Receive(
input clk, //SCL的8倍以上
output reg rddat_vaild, //下降沿有效
output reg [7:0] rddat,
output busy,
inout SCL,
inout SDA
);
// S {ADDR,RW_W} A DATA A ... DATA A P -- 本机地址
// S {ADDR,RW_W} NA P -- 非本机地址
parameter ADDR = 7'h11; //I2C设备地址
parameter RECEIVE_DATA_LEN = 16'd1; //接收的数据个数
localparam RW_W = 1'b0;
localparam RW_R = 1'b1;
//---------------------I2C Slave State Define----------------------
localparam IDLE = 8'h01; //空闲
localparam START = 8'h02; //起始,检测到SCL=H,SDA=D,
localparam SEND_DATA = 8'h04; //Slave发送数据,接管SDA控制权
localparam GET_DATA = 8'h08; //Slave读取数据
localparam CHECK_ACK = 8'h10; //检查SDA的ACK/NACK
localparam ACK = 8'h20; //发出ACK,SDA=L,接管SDA控制权
localparam NACK = 8'h40; //发出NACK,SDA=H,接管SDA控制权
localparam STOP = 8'h80; //停止,检测到SCL=H,SDA=R
//------------------------------------------------------------------
reg [7:0] state = IDLE;
reg [7:0] next_state = IDLE;
wire change_state;
wire [7:0] rddat_tmp;
reg [15:0] data_cnt = 16'd0;
reg isMe = 1'b0; //是否为本机地址
//-------------------------State Machine----------------------------
always @(posedge change_state or negedge busy) begin
if(~busy) begin
state <= IDLE;
end
else begin
state <= next_state;
end
end
//状态转移
always @(*) begin
if(busy) begin
case(state)
IDLE: begin
next_state <= START;
end
START: begin
next_state <= GET_DATA;
end
GET_DATA: begin
if(isMe) begin
next_state <= ACK;
end
else begin
next_state <= NACK;
end
end
ACK: begin
if(data_cnt > RECEIVE_DATA_LEN) begin
next_state <= STOP;
end
else begin
next_state <= GET_DATA;
end
end
NACK: begin
next_state <= STOP;
end
STOP: begin
next_state <= IDLE;
end
default: begin
next_state <= IDLE;
end
endcase
end
else begin
next_state <= START;
end
end
//三段式状态机第三段,I2C Slave sub
I2C_Slave_sub I2C_Slave_sub_inst(
.clk (clk),
.wrdat_buf (),
.rddat_tmp (rddat_tmp),
.check_ack (),
.SCL (SCL),
.SDA (SDA),
.change_state (change_state),
.state (state),
.busy (busy)
);
// ---rddat_vaild---
always @(*) begin
case(state)
IDLE: begin
rddat_vaild <= 1'b0;
end
ACK: begin
if(data_cnt>1) begin
rddat_vaild <= 1'b1;
end
else begin
rddat_vaild <= 1'b0;
end
end
default: begin
rddat_vaild <= 1'b0;
end
endcase
end
// ---rddat---
always @(posedge change_state) begin
case(state)
GET_DATA: begin
if(data_cnt>0) begin
rddat <= rddat_tmp;
end
else begin
rddat <= 8'd0;
end
end
default: begin
rddat <= rddat;
end
endcase
end
// ---data_cnt---
always @(posedge change_state) begin
case(state)
IDLE: begin
data_cnt <= 16'd0;
end
GET_DATA: begin
data_cnt <= data_cnt + 1'b1;
end
default: begin
data_cnt <= data_cnt;
end
endcase
end
// ---isMe---
always @(*) begin
case(state)
IDLE: begin
isMe <= 1'b0;
end
GET_DATA: begin
if(data_cnt==0) begin
if(rddat_tmp=={ADDR, RW_W}) begin //地址=本机,且RW=W,启动Slave接收进程
isMe <= 1'b1;
end
else begin
isMe <= isMe;
end
end
else begin
isMe <= isMe;
end
end
default: begin
isMe <= isMe;
end
endcase
end
endmodule
- I2C_Slave_Send.v
/*
* file : I2C_Slave_Send.v
* author : 今朝无言
* Lab : WHU-EIS-LMSWE
* date : 2023-03-21
* version : v1.0
* description : 作为Slave<发送>数据
* Copyright © 2023 WHU-EIS-LMSWE, All Rights Reserved.
*/
module I2C_Slave_Send(
input clk, //SCL的8倍以上
output reg wrdat_req,
input [7:0] wrdat,
output busy,
inout SCL,
inout SDA
);
// S {ADDR,RW_R} A DATA A ... DATA A P -- 本机地址
// S {ADDR,RW_R} NA P -- 非本机地址
parameter ADDR = 7'h11; //I2C设备地址
parameter SEND_DATA_LEN = 16'd1; //发送的数据个数
localparam RW_W = 1'b0;
localparam RW_R = 1'b1;
//---------------------I2C Slave State Define----------------------
localparam IDLE = 8'h01; //空闲
localparam START = 8'h02; //起始,检测到SCL=H,SDA=D,
localparam SEND_DATA = 8'h04; //Slave发送数据,接管SDA控制权
localparam GET_DATA = 8'h08; //Slave读取数据
localparam CHECK_ACK = 8'h10; //检查SDA的ACK/NACK
localparam ACK = 8'h20; //发出ACK,SDA=L,接管SDA控制权
localparam NACK = 8'h40; //发出NACK,SDA=H,接管SDA控制权
localparam STOP = 8'h80; //停止,检测到SCL=H,SDA=R
//------------------------------------------------------------------
reg [7:0] state = IDLE;
reg [7:0] next_state = IDLE;
wire change_state;
wire [7:0] rddat_tmp;
reg [15:0] data_cnt = 16'd0;
reg isMe = 1'b0; //是否为本机地址
//-------------------------State Machine----------------------------
always @(posedge change_state or negedge busy) begin
if(~busy) begin
state <= IDLE;
end
else begin
state <= next_state;
end
end
//状态转移
always @(*) begin
if(busy) begin
case(state)
IDLE: begin
next_state <= START;
end
START: begin
next_state <= GET_DATA;
end
GET_DATA: begin
if(isMe) begin
next_state <= ACK;
end
else begin
next_state <= NACK;
end
end
ACK: begin
next_state <= SEND_DATA;
end
NACK: begin
next_state <= STOP;
end
SEND_DATA: begin
next_state <= CHECK_ACK;
end
CHECK_ACK: begin
if(data_cnt > SEND_DATA_LEN) begin
next_state <= STOP;
end
else begin
next_state <= SEND_DATA;
end
end
STOP: begin
next_state <= IDLE;
end
default: begin
next_state <= IDLE;
end
endcase
end
else begin
next_state <= START;
end
end
//三段式状态机第三段,I2C Slave sub
I2C_Slave_sub I2C_Slave_sub_inst(
.clk (clk),
.wrdat_buf (wrdat),
.rddat_tmp (rddat_tmp),
.check_ack (check_ack),
.SCL (SCL),
.SDA (SDA),
.change_state (change_state),
.state (state),
.busy (busy)
);
// ---wrdat_req---
always @(*) begin
case(state)
ACK, CHECK_ACK: begin
if(data_cnt <= SEND_DATA_LEN) begin
wrdat_req <= 1'b1;
end
else begin
wrdat_req <= 1'b0;
end
end
default: begin
wrdat_req <= 1'b0;
end
endcase
end
// ---data_cnt---
always @(posedge change_state) begin
case(state)
IDLE: begin
data_cnt <= 16'd0;
end
GET_DATA, SEND_DATA: begin
data_cnt <= data_cnt + 1'b1;
end
default: begin
data_cnt <= data_cnt;
end
endcase
end
// ---isMe---
always @(*) begin
case(state)
IDLE: begin
isMe <= 1'b0;
end
GET_DATA: begin
if(data_cnt==0) begin
if(rddat_tmp=={ADDR, RW_R}) begin //地址=本机,且RW=R,启动Slave发送进程
isMe <= 1'b1;
end
else begin
isMe <= isMe;
end
end
else begin
isMe <= isMe;
end
end
default: begin
isMe <= isMe;
end
endcase
end
endmodule
Test Bench & 测试结果
Master写 & Slave接收
- I2C_Master_w_Slave_r_tb.v
`timescale 1ns/100ps
module I2C_Master_w_Slave_r_tb();
//测试Master写、Slave接收
reg clk_100M = 1'b1;
always #5 begin
clk_100M <= ~clk_100M;
end
reg clk_50M = 1'b1;
always #10 begin
clk_50M <= ~clk_50M;
end
wire SCL;
wire SDA;
pullup(SCL);
pullup(SDA);
//-------------------Master-----------------------
reg wr_en;
wire wrdat_req;
reg [7:0] wrdat = 8'd0;
wire busy;
wire check_ack;
I2C_Master_Write #(
.ADDR (7'h44),
.WR_DATA_LEN (16'd4))
I2C_Master_Write_inst(
.clk (clk_50M),
.wr_en (wr_en),
.wrdat_req (wrdat_req),
.wrdat (wrdat),
.busy (busy),
.check_ack (check_ack),
.SCL (SCL),
.SDA (SDA)
);
always @(posedge wrdat_req) begin
wrdat <= wrdat + 1'b1;
end
//-------------------Slave-----------------------
wire rddat_vaild;
wire [7:0] rddat;
wire S_busy;
I2C_Slave_Receive #(
.ADDR (7'h44),
.RECEIVE_DATA_LEN (16'd4))
I2C_Slave_Receive_inst(
.clk (clk_100M),
.rddat_vaild (rddat_vaild),
.rddat (rddat),
.busy (S_busy),
.SCL (SCL),
.SDA (SDA)
);
//---------------------test-------------------------
initial begin
wr_en <= 1'b0;
#100;
wr_en <= 1'b1;
#100;
wr_en <= 1'b0;
wait(busy);
wait(~busy);
#100;
wr_en <= 1'b1;
#100;
wr_en <= 1'b0;
wait(busy);
wait(~busy);
#200;
$stop;
end
endmodule
设置 Master 写设备地址与 Slave 设备地址相同,单次 I2C 通信发送/接收 4 个数据,结果如下
若设置两者地址不同,Master 会检测到 NACK 信号,从而直接终止通信,结果如下
由于例程编写考虑并不全面,因此这里检查到 NACK 时仍进行了数据请求(但没进行数据发送),在实际系统设计中读者应自行修正。
Master读 & Slave发送
- I2C_Master_r_Slave_s_tb.v
`timescale 1ns/100ps
module I2C_Master_r_Slave_s_tb();
//测试Maste读、Slave发送
reg clk_100M = 1'b1;
always #5 begin
clk_100M <= ~clk_100M;
end
reg clk_50M = 1'b1;
always #10 begin
clk_50M <= ~clk_50M;
end
wire SCL;
wire SDA;
pullup(SCL);
pullup(SDA);
//-------------------Master-----------------------
reg rd_en = 1'b0;
wire rddat_vaild;
wire [7:0] rddat;
wire busy;
wire check_ack;
I2C_Master_Read #(
.ADDR (7'h44),
.RD_DATA_LEN (16'd4))
I2C_Master_Read_inst(
.clk (clk_50M),
.rd_en (rd_en),
.rddat_vaild (rddat_vaild),
.rddat (rddat),
.busy (busy),
.check_ack (check_ack),
.SCL (SCL),
.SDA (SDA)
);
//-------------------Slave-----------------------
wire wrdat_req;
reg [7:0] wrdat = 8'd0;
wire S_busy;
I2C_Slave_Send #(
.ADDR (7'h44),
.SEND_DATA_LEN (16'd4))
I2C_Slave_Send_inst(
.clk (clk_100M),
.wrdat_req (wrdat_req),
.wrdat (wrdat),
.busy (S_busy),
.SCL (SCL),
.SDA (SDA)
);
always @(posedge wrdat_req) begin
wrdat <= wrdat + 1'b1;
end
//---------------------test-------------------------
initial begin
rd_en <= 1'b0;
#100;
rd_en <= 1'b1;
#100;
rd_en <= 1'b0;
wait(busy);
wait(~busy);
#100;
rd_en <= 1'b1;
#100;
rd_en <= 1'b0;
wait(busy);
wait(~busy);
#200;
$stop;
end
endmodule
设置 Master 读设备地址与 Slave 设备地址相同,单次 I2C 通信读取 4 个数据,结果如下
若两者地址不同,Slave 会自行挂起,直到 I2C 总线释放后自动回到 IDLE 状态,而 Master 由于没有收到指定设备的 ACK 确认信号,也会自行终止读取进程,结果如下