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 例子如下:
本博文所附代码实现了 I2C Master 以及 I2C Slave 的功能,未涉及具体设备的时序要求,但利用这两个模块可以很容易地实现更具体的功能。比如要实现上例的 I2C 设备写功能,则将 I2C_Master 模块的 data_num 设置为 3,然后在每次 data_req 时 wrdat 依次给出 {I2C_Device_Addr, 0}、Reg_Addr、Data 即可。
/* * file : I2C_Master.v* author : 今朝无言* Lab : WHU-EIS-LMSWE* date : 2023-03-18* version : v1.0* description : i2c master*/
module I2C_Master(
input clk, //clk 是 SCL 的 4 倍
input rst_n,input [7:0] data_num,
input tx_en, //上升沿有效output reg data_req,
input [7:0] wrdat,output reg tx_done,
output reg tx_err, //发送失败inout SDA, //串行数据线
inout SCL //串行时钟线
);
// START D7 D6 ... D1 D0 ACK STOPlocalparam IDLE = 8'h01;
localparam START = 8'h02; //发送起始,SCL=H期间,SDA=D
localparam SEND = 8'h04; //数据发送,注意在此期间,SDA在SCL=H时必须保持稳定,在SCL=L时才允许SDA改变
localparam WAIT = 8'h08;
localparam ACK = 8'h10; //等待Slave的ACK,期待SDA=L & 请求下一个数据
localparam STOP = 8'h20; //发送结束,SCL=H期间,SDA=Rreg [7:0] state = IDLE;
reg [7:0] next_state = IDLE;reg [1:0] clk_cnt = 2'd0;
reg send; //是否处于发送状态(SDA有效,包括START、SEND、STOP阶段,而ACK阶段应释放SDA线)reg [7:0] wrdat_buf;
reg [3:0] bit_cnt; //发送的第几位reg [7:0] data_num_buf;
reg [7:0] data_cnt; //已发送数据的个数reg SDA_buf;
reg SCL_buf;assign SDA = (send)? SDA_buf : 1'bz;
assign SCL = (~tx_done)? SCL_buf : 1'bz;always @(posedge clk) begin //SCL时钟分为4段,方便时序控制clk_cnt <= clk_cnt + 1'b1; //clk_cnt: 0 1 2 3//SCL : L H H Lcase(clk_cnt)2'd0: beginSCL_buf <= 1'b0;end2'd1: beginSCL_buf <= 1'b1;end2'd2: beginSCL_buf <= 1'b1;end2'd3: beginSCL_buf <= 1'b0;enddefault: beginSCL_buf <= 1'b0;endendcase
endwire tx_pe; //tx_en的上升沿
reg tx_en_d0;
reg tx_en_d1;assign tx_pe = tx_en_d0 & (~tx_en_d1);always @(posedge clk) begintx_en_d0 <= tx_en;tx_en_d1 <= tx_en_d0;
endreg start_flag;always @(posedge clk or negedge rst_n) beginif(~rst_n) begintx_done <= 1'b1;wrdat_buf <= 8'd0;endelse beginif(tx_pe && tx_done) beginstart_flag <= 1; //准备发送标志endelse if(clk_cnt==1) beginstart_flag <= 0;endif(start_flag && clk_cnt==1) begin //模块工作状态信号tx_done <= 1'b0;endelse if(next_state == IDLE) begintx_done <= 1'b1;endelse begintx_done <= tx_done;endif(tx_pe && tx_done) begindata_num_buf <= data_num; //data_num缓冲endelse begindata_num_buf <= data_num_buf;endif(data_req) beginwrdat_buf <= wrdat; //数据缓冲endelse beginwrdat_buf <= wrdat_buf;endend
endalways @(posedge clk) begincase(next_state)START, SEND, WAIT, STOP: beginsend <= 1'b1;enddefault: beginsend <= 1'b0;endendcase
end//--------------------------- State Machine -------------------------------
always @(posedge clk or negedge rst_n) beginif(~rst_n) beginnext_state <= IDLE;state <= IDLE;endelse begincase(next_state)IDLE, START, SEND, WAIT, ACK: beginif(clk_cnt == 3) beginstate <= next_state;endelse beginstate <= state;endendSTOP: beginif(clk_cnt == 2) beginstate <= next_state;endelse beginstate <= state;endenddefault: beginstate <= IDLE;endendcaseend
endalways @(*) begin //状态转移case(state)IDLE: beginif(~tx_done) beginnext_state <= START;endelse beginnext_state <= IDLE;endendSTART: beginnext_state <= SEND;endSEND: beginif(bit_cnt == 4'd15) beginnext_state <= WAIT;endelse beginnext_state <= SEND;endendWAIT: beginnext_state <= ACK;endACK: beginif(data_cnt <= data_num_buf) beginnext_state <= SEND;endelse beginnext_state <= STOP;endendSTOP: beginnext_state <= IDLE;enddefault: beginnext_state <= IDLE;endendcase
endalways @(posedge clk) begincase(next_state)IDLE: beginSDA_buf <= 1'b1;bit_cnt <= 4'd0;data_req <= 1'b0;data_cnt <= 8'd0;endSTART: beginbit_cnt <= 4'd7;if(clk_cnt == 2) begin //SCL=H,SDA=DSDA_buf <= 1'b0;endif(clk_cnt == 2) begindata_cnt <= data_cnt + 1'b1;data_req <= 1'b1;endelse begindata_req <= 1'b0;endendSEND: begintx_err <= 1'b0;if(clk_cnt == 0) begin //SCL=H期间SDA=wrdat,SCL=L期间改变SDASDA_buf <= wrdat_buf[bit_cnt];bit_cnt <= bit_cnt - 1'b1;endendWAIT: beginSDA_buf <= SDA_buf;endACK: beginbit_cnt <= 4'd7;SDA_buf <= 1'b0;if(clk_cnt == 1) beginif(SDA == 1'b0) begin //检查SDA线,是否有Slave的ACK(SDA=L)tx_err <= 1'b0;endelse begintx_err <= 1'b1;endendif(data_cnt < data_num_buf && clk_cnt == 2) begindata_req <= 1'b1;endelse begindata_req <= 1'b0;endif(clk_cnt == 2) begindata_cnt <= data_cnt + 1'b1;endendSTOP: beginif(clk_cnt == 2) begin //SCL=H,SDA=RSDA_buf <= 1'b1;endenddefault: beginSDA_buf <= 1'b1;endendcase
endendmodule
/* * file : I2C_Slave.v* author : 今朝无言* Lab : WHU-EIS-LMSWE* date : 2023-03-18* version : v1.0* description : i2c slave*/
module I2C_Slave(
input clk, //须远高于SCL(4倍以上)output reg [7:0] rddat,
output rx_done,
output data_valid,inout SDA, //串行数据线
input SCL //串行时钟线
);reg isACK = 1'b0; //接过SDA线控制权,给出应答信号ACK=L
reg [7:0] rddat_tmp;
reg busy;assign SDA = isACK? 1'b0 : 1'bz;
assign rx_done = ~busy;
assign data_valid = (cnt==9)? 1 : 0;always @(SDA) beginif(~SDA & SCL) begin // SCL=H,SDA=D,接收起始busy <= 1'b1;endelse if(SDA & SCL) begin // SCL=H,SDA=R,接收结束busy <= 1'b0;endelse beginbusy <= busy;end
endreg SCL_d0;
wire SCL_pe;
wire SCL_ne;reg SDA_d0;
wire SDA_pe;reg busy_d0;
wire busy_pe;assign SCL_pe = SCL & (~SCL_d0);
assign SCL_ne = (~SCL) & SCL_d0;
assign SDA_pe = SDA & (~SDA_d0);
assign busy_pe = busy & (~busy_d0);always @(posedge clk) beginSCL_d0 <= SCL;SDA_d0 <= SDA;busy_d0 <= busy;
endreg [3:0] cnt;always @(posedge clk) beginif(busy_pe) begincnt <= 4'd0;endelse if(SCL_pe) begincnt <= cnt + 1'b1;endelse if(SCL_ne && cnt == 9) begincnt <= 0;endelse begincnt <= cnt;endif(busy_pe) beginrddat_tmp <= 8'd0;endelse if(SCL_pe && cnt < 8) beginrddat_tmp[7-cnt] <= SDA;endelse beginrddat_tmp <= rddat_tmp;endif(SCL_pe && cnt == 8) beginrddat <= rddat_tmp;endelse beginrddat <= rddat;endif(busy_pe) beginisACK <= 1'b0;endelse if(SCL_ne && cnt == 8) beginisACK <= 1'b1;endelse if(SCL_ne && cnt == 9) beginisACK <= 1'b0;endelse beginisACK <= isACK;end
endendmodule
`timescale 1ns/100psmodule I2C_tb();reg clk_100M = 1'b1;
always #5 beginclk_100M <= ~clk_100M;
endreg rst_n = 1'b1;wire SDA;
wire SCL;pullup(SDA); //pullup、pulldown设置上下拉
pullup(SCL);reg [7:0] data_num;
reg tx_en;reg [7:0] wrdat = 8'd0;
wire data_req;wire tx_done;
wire tx_err;wire [7:0] rddat;
wire rx_done;
wire data_valid;I2C_Master I2C_Master(.clk (clk_100M), //clk 是 SCL 的 4 倍.rst_n (rst_n),.data_num (data_num),.tx_en (tx_en), //上升沿有效.data_req (data_req),.wrdat (wrdat),.tx_done (tx_done),.tx_err (tx_err), //发送失败.SDA (SDA), //串行数据线.SCL (SCL) //串行时钟线
);I2C_Slave I2C_Slave(.clk (clk_100M), //须远高于SCL(4倍).rddat (rddat),.rx_done (rx_done),.data_valid (data_valid),.SDA (SDA), //串行数据线.SCL (SCL) //串行时钟线
);// wrdat
always @(posedge data_req) beginwrdat <= wrdat + 1'b1;
endinitial begintx_en <= 1'b0;#100;send(8'd5);#10000;send(8'd13);wait(rx_done);#1000;$stop;
end//启动发送,发送num个数据
task send;input [7:0] num;beginwait(rx_done);data_num <= num;tx_en <= 1'b1;#100;tx_en <= 1'b0;end
endtaskendmodule