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 确认信号,也会自行终止读取进程,结果如下

精彩内容

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: