在时钟(SCL)为高电平的时候,数据总线(SDA)必须保持稳定,所以数据总线(SDA)在时钟(SCL)为低电平的时候才能改变。
在时钟(SCL)为高电平的时候,数据总线(SDA)由高到低的跳变为总线起始信号,在时钟(SCL)为高电平的时候,数据总线(SDA)由低到高的跳变为总线停止信号。
应答,当 IIC 主机(不一定是发送端还是接受端)将 8 位数据或命令传出后,会将数据总线(SDA)释放,即设置为输入,然后等待从机应答(低电平 0 表示应答,1 表示非应答),此时的时钟仍然是主机提供的。
数据帧格式,I2C 器件通讯的时候首先是要发送“起始信号”,紧跟着就是七位器件地址,第八位是数据传送方向位(0:代表写,1:代表读),再后面就是等待从机的应答。当然传送结束后,“终止信号”也是由主机来产生的。发送数据的时候是高位先发送。
module iic #(
parameter DIV_CLK = 100,
parameter WR_MAX = 8'd1,
parameter RD_MAX = 8'd1
) (
input clk,
input rst_n,
input enable,
output reg busy,
input [7:0] rdlen,
input [7:0] wrlen,
output reg [RD_MAX*8-1:0] rddata,
input [WR_MAX*8-1:0] wrdata,
output reg isack,
output scl,
inout sda
);
// -------------------------------------------------------------------------
// 信号分频
// -------------------------------------------------------------------------
reg [$clog2(DIV_CLK):0] clk_cnt;
reg scl_clk;
always @(posedge clk or negedge rst_n) begin
if (~rst_n) begin
clk_cnt <= 16'd0;
scl_clk <= 1'd0;
end else begin
if (clk_cnt == (DIV_CLK >> 1) - 1) begin
clk_cnt <= 16'd0;
scl_clk = ~scl_clk;
end else begin
clk_cnt <= clk_cnt + 16'd1;
end
end
end
localparam ST_IDLE = 0,
ST_START = 1,
ST_WR_DATA = 2,
ST_WR_READ_ACK = 3,
ST_WR_CHECK_ACK = 4,
ST_RD_START = 5,
ST_RD_DATA = 6,
ST_RD_ACK_REPLY = 7,
ST_RD_ACK_DONE = 8,
ST_STOP = 9;
reg [3:0] state = ST_IDLE;
assign scl = (state == ST_IDLE || state == ST_START) ? 1 : scl_clk;
// SDA 输入输出切换
reg sda_r = 1;
reg sda_oe = 1;
assign sda = sda_oe ? sda_r : 1'bz;
// 写入和读取数据时使用的中间变量
reg [7:0] byte_no = 0;
reg [3:0] bit_no = 0;
always @(posedge clk or negedge rst_n) begin
if (~rst_n) begin
isack <= 0;
busy <= 0;
rddata <= 0;
end else begin
case (state)
ST_IDLE: begin
sda_r <= 1;
if (enable) begin
busy <= 1;
end
if (busy && clk_cnt == DIV_CLK >> 2 && scl_clk == 1) begin
state <= ST_START;
end
end
// 产生开始信号, 即在 scl_clk 为高电平时 sda 由高变低
ST_START: begin
if (clk_cnt == DIV_CLK >> 2 && scl_clk == 1) begin
sda_r <= 0;
state <= ST_WR_DATA;
byte_no <= wrlen;
bit_no <= 4'd0;
isack <= 0;
end
end
// 写入数据, wrdata 的最高位字节是器件的 i2c 地址以及读写标志
ST_WR_DATA: begin
if (clk_cnt == DIV_CLK >> 2 && scl_clk == 0) begin
if (bit_no == 4'd8) begin
state <= ST_WR_READ_ACK;
byte_no <= byte_no - 1'b1;
bit_no <= 4'd0;
// 这里切换 sda 为读模式以便在下一个 scl_clk 高电平时接收应答
sda_oe <= 0;
end else begin
bit_no <= bit_no + 1'b1;
sda_r <= wrdata[byte_no*8-1-bit_no-:1];
sda_oe <= 1;
end
end
end
// scl_clk 为高时读取应答信号
ST_WR_READ_ACK: begin
if (clk_cnt == DIV_CLK >> 2 && scl_clk == 1) begin
if (sda == 0) isack <= 1;
state <= ST_WR_CHECK_ACK;
end
end
// scl_clk 为低时检测应答信号并做出状态转移
ST_WR_CHECK_ACK: begin
if (clk_cnt == DIV_CLK >> 2 && scl_clk == 0) begin
// 数据还没写完则继续进入写数据状态
if (isack && byte_no != 0) begin
state <= ST_WR_DATA;
bit_no <= bit_no + 1'b1;
sda_r <= wrdata[byte_no*8-1-bit_no-:1];
sda_oe <= 1;
isack <= 0;
// 否则进入读状态
end else if (isack && rdlen > 0) begin
state <= ST_RD_START;
sda_oe <= 1;
sda_r <= 1;
// 没有应答或数据写完了且不需要读模式则停止传输
end else begin
state <= ST_STOP;
sda_r <= 0;
sda_oe <= 0;
end
end
end
// 在 scl_clk 为高时将 sda 输出为低进入读状态
ST_RD_START: begin
if (clk_cnt == DIV_CLK >> 2 && scl_clk == 1) begin
sda_r <= 0;
// 同时在 scl_clk 为低时将 sda 切到读模式, 以便下一个 scl_clk 为高时将 sda 读取
end else if (clk_cnt == DIV_CLK >> 2 && scl_clk == 0) begin
sda_oe <= 0;
state <= ST_RD_DATA;
bit_no <= 0;
byte_no <= rdlen;
end
end
// 在 scl_clk 为高时读取 sda 的数据
ST_RD_DATA: begin
if (clk_cnt == DIV_CLK >> 2 && scl_clk == 1) begin
bit_no <= bit_no + 1'b1;
rddata[byte_no*8-1-bit_no-:1] <= sda;
if (bit_no == 4'd7) begin
state <= ST_RD_ACK_REPLY;
byte_no <= byte_no - 1'b1;
bit_no <= 4'd0;
end
end
end
// 在 scl_clk 为低时读取产生应答信号, 以便在下一个高电平被对方采集到
ST_RD_ACK_REPLY: begin
if (clk_cnt == DIV_CLK >> 2 && scl_clk == 0) begin
sda_oe = 1;
sda_r <= 0;
state <= ST_RD_ACK_DONE;
end
end
// 经过了一个高电平, 在该低电平处判断数据有没有传完以进行状态转移
ST_RD_ACK_DONE: begin
if (clk_cnt == DIV_CLK >> 2 && scl_clk == 0) begin
if (byte_no == 0) begin
sda_oe = 1;
state <= ST_STOP;
end else begin
sda_oe = 0;
state <= ST_RD_DATA;
end
end
end
// 产生停止信号, 即在 scl_clk 为高电平时将 sda 由低变高
ST_STOP: begin
if (clk_cnt == DIV_CLK >> 2 && scl_clk == 1) begin
sda_oe <= 1;
sda_r <= 1;
state <= ST_IDLE;
busy <= 0;
end
end
endcase
end
end
endmodule
仿真文件 tb.v
`timescale 1ns / 1ns
module tb;
// -------------------------------------------------------------------------
// clk and reset signal
// -------------------------------------------------------------------------
reg clk;
reg rst_n;
initial clk = 0;
always #10 clk = ~clk;
initial begin
rst_n = 0;
#100;
rst_n = 1;
end
initial begin
$dumpvars;
#5000000;
$finish;
end
reg enable;
wire busy;
initial begin
enable = 0;
@(posedge rst_n);
enable = 1;
@(negedge busy);
enable = 0;
#10000;
enable = 1;
@(negedge busy);
enable = 0;
#10000;
end
wire [15:0] rddata;
wire isack;
iic #(
.DIV_CLK(100),
.WR_MAX (8'd2),
.RD_MAX (8'd2)
) u_iic (
.clk (clk),
.rst_n (rst_n),
.enable(enable),
.busy (busy),
.rdlen (8'd2),
.wrlen (8'd2),
.rddata(rddata),
.wrdata(16'hd552),
.isack (isack),
.scl (scl),
.sda (sda)
);
endmodule
通常直接使用上面的 testbench 由于没有接实际的设备,没有收到应答后 iic 模块会自动停止传输,并拉低 busy 标志, 同时 isack 也为 0
为了方便查看波形,以下是将 iic.v 的 if (sda == 0) isack <= 1;
改为 if (1) isack <= 1;
即在 读取 ack 信号时, 总是假设能读到
并且 将读信号 rddata[byte_no*8-1-bit_no-:1] <= sda;
总是写读到 1,rddata[byte_no*8-1-bit_no-:1] <= 1;
于是有如下波形