IIC(I2C)协议是一种串行通信协议,用于连接微控制器和外围设备。IIC协议只需要两根信号线(时钟线SCL和数据线SDA)就能完成设备之间的通信;支持多主机和多从机通信,通过设备地址区分不同的设备;标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s;具有应答机制,可以检测数据的正确性和设备的存在性。
在闲置状态时,时钟线和数据线都会保持高电平。IIC协议的具体传输过程如下:
1. 主机发送起始信号,即在时钟线SCL保持高电平的情况下,数据线SDA由高电平向低电平跳变。
2. 主机发送从机设备地址和读写控制位,从机的设备地址一般为7位,读写控制位为1位,0表示写操作,1表示读操作。主机在发送完8位数据后,释放数据线SDA,等待从机的应答信号。
3. 从机接收到地址和控制位后,进行地址识别,如果匹配,则在下一个时钟周期内,将数据线SDA拉低,表示应答信号。如果不匹配,则保持数据线SDA为高电平,等待主机发送停止信号或新的起始信号。
4. 如果主机发送的是写操作,那么主机继续发送数据字节,每发送一个字节后,释放数据线SDA,等待从机的应答信号。如果从机接收到数据字节后,将数据线SDA拉低,表示应答信号。如果从机无法接收数据字节或者发生错误,那么保持数据线SDA为高电平,等待主机发送停止信号或新的起始信号。
5. 如果主机发送的是读操作,那么主机释放数据线SDA,由从机发送数据字节。每接收一个字节后,主机根据需要发送应答信号或非应答信号。应答信号表示主机需要继续接收数据字节,非应答信号表示主机已经接收完毕或者发生错误。非应答信号由主机在最后一个时钟周期内将数据线SDA拉高实现。
6. 主机发送停止信号,即在时钟线SCL保持高电平的情况下,数据线SDA由低电平向高电平跳变。停止信号表示一次IIC通信的结束。
重要的起始位和结束位如下图所示,且在IIC协议中,控制数据线的设备会在SCL为低电平时控制SDA数据线,在SCL为高电平时对SDA数据线进行采样:
具体读写时序图类似下图,这是我从一个支持IIC协议的设备的手册上截取下来的,值得一提的是,在下图中,灰色部分表示的是数据总线SDA由主机操纵,白色部分表示数据总线SDA由从机操纵。
写数据时(如下图的I2C Register Wr):首先是主机发送一个起始位,接着是七位宽的设备地址加上一位宽的写控制位,从机响应一次;然后主机发送一字节的寄存器地址,从机响应一次;然后由主机发送一字节以上的数据,从机响应;最后发送一个停止位。
读数据时(如下图的I2C Register Rd):首先是主机发送一个起始位,接着是七位宽的设备地址加上一位宽的写控制位;然后是主机发送寄存器地址,从机应答;再重新发送一次起始位,设备地址和读控制位,用于表示接下来是要进行读操作,从机应答;从机发送数据,主机应答,在得到最后一字节数据后,由主机发送一个非应答信号;最后发送一个停止位。
网站上大家对于iic的设计已经有非常详细的讲解了,这里就不再做太简单的说明,以下主要是我对于IIC的个人方面的一些理解和设计。
作为被广泛使用的协议,我们在设计时应该尽可能的设计成通用模块,以便下一次能够直接使用。因此,模块的接口定义应为如图所示:
clk 为输入时钟;
rst 为复位信号(高有效);
txdat 表示需要通过IIC输出的数据;
txget 表示已输出该字节的信号;
rxdat 表示从sda获得的数据;
rxvld 表示数据有效;
dev_addr 表示设备地址;
reg_addr 表示读写地址(寄存器地址);
cmd_op 表示读写指令;
op_byte_num 表示读写字节数;
iic_work_en 表示模块开始工作信号;
busy 表示工作状态信号;
scl 表示IIC时钟;
sda 表示IIC数据;
因为要做一个IIC协议会比较复杂,所以说这里采用状态机的形式,以理清每一个操作。但是一般我们看到的IIC状态设计、包括在之前我设计IIC时,都会将每一个操作都分为一个状态,例如,起始位是一个状态,发送设备地址是一个状态,读写控制位是一个状态,接收应答和发送非应答分别为一个状态等等,这样的划分十分的细致,每一个操作都能一目了然,但我认为这样做的同时会使得状态的跳转更加的复杂,使得整个状态机变得十分的臃肿,如下图。
于是,我便重新构思了一下设计方案:以9位(9bit)为一个进行周期设计,即每9位或1位为一个状态。具体状态如下:
状态一:IDLE 表示闲置状态;
状态二:START 表示起始位;
状态三:DEV_CMD 表示设备地址位、读写控制位宽和从机的应答位;
状态四:ADDR 表示发送寄存器地址位和接收从机的应答位;
状态五:WDATA 表示写数据部分;
状态六:RDATA 表示读数据部分;
状态七:STOP 表示停止位;
写数据时序:
读数据时序:
其中,我使用的时钟为100Mhz,复位信号为高电平有效:
- module i2c_master_interface #(
- parameter ADDR_LEN = 1,
- parameter IIC_CLOCK = 400000
- ) (
- input i_clk ,
- input i_rst ,
-
- input [7:0] i_txdat ,
- output o_txget ,
- output [7:0] o_rxdat ,
- output o_rxvld ,
- input [6:0] i_dev_addr ,
- input [ADDR_LEN*8-1:0] i_reg_addr,
- input i_cmd_op ,
- input [3:0] i_op_byte_num ,
- input i_iic_work_en ,
- output o_busy ,
-
- //output o_output_en,
- inout io_scl ,
- inout io_sda
- );
-
-
-
- endmodule
- localparam SCL_PERIOD = 1000/(IIC_CLOCK/100000) , // in_clk = 100MHz
- SCL_HALF = SCL_PERIOD >> 1 ,
- LOW_HALF = SCL_HALF >> 1 ,
- HIGH_HALF = (SCL_PERIOD+SCL_HALF)>>1;
-
- localparam IDLE = 7'b000_0001,
- START = 7'b000_0010,
- DEV_CMD = 7'b000_0100,
- ADDR = 7'b000_1000,
- WDATA = 7'b001_0000,
- RDATA = 7'b010_0000,
- STOP = 7'b100_0000;
-
- localparam WRITE_BIT = 1'b0,
- READ_BIT = 1'b1,
- OP_WRITE = 1'b0,
- OP_READ = 1'b1;
- reg [6:0] state_c ;
- reg [6:0] state_n ;
-
- reg [7:0] cnt_scl ;
- reg reg_scl ;
- reg [3:0] cnt_bit ;
- wire end_cnt_bit;
- reg [3:0] bit_num ;
- reg rw_bit ;
-
- reg [3:0] cnt_byte;
- reg [3:0] op_byte_num ;
-
- reg [ADDR_LEN*8-1:0] reg_addr_1;
- reg [7:0] reg_addr_2 ;
-
- reg [7:0] reg_txdat ;
- reg txget ;
-
- reg reg_sda ;
- reg reg_i_sda ;
- reg [7:0] rx_data ;
- reg reg_ack ;
- reg rxvld ;
- reg busy ;
-
- wire o_sda;
- wire i_sda;
- wire o_scl;
- wire i_scl;
- wire output_en;
依照逻辑,状态机的跳转条件如下:
- // FMS
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- state_c <= IDLE;
- end
- else begin
- state_c <= state_n;
- end
- end
-
- always @(*) begin
- case (state_c)
- IDLE : begin
- if (i_iic_work_en) begin
- state_n = START;
- end
- else begin
- state_n = state_c;
- end
- end
- START : begin
- if (end_cnt_bit) begin
- state_n = DEV_CMD;
- end
- else begin
- state_n = state_c;
- end
- end
- DEV_CMD : begin
- if (end_cnt_bit && reg_ack) begin // NO ACK
- state_n = IDLE;
- end
- else if ((rw_bit == WRITE_BIT) && end_cnt_bit && (reg_ack == 0)) begin //
- state_n = ADDR;
- end
- else if ((rw_bit == READ_BIT) && end_cnt_bit && (reg_ack == 0)) begin //
- state_n = RDATA;
- end
- else begin
- state_n = state_c;
- end
- end
- ADDR : begin
- if (end_cnt_bit && reg_ack) begin
- state_n = IDLE;
- end
- else if ((i_cmd_op == OP_WRITE) && (cnt_byte == (ADDR_LEN - 1)) && end_cnt_bit && (reg_ack == 0)) begin //
- state_n = WDATA;
- end
- else if ((i_cmd_op == OP_READ) && (cnt_byte == (ADDR_LEN - 1)) && end_cnt_bit && (reg_ack == 0)) begin //
- state_n = START;
- end
- else begin
- state_n = state_c;
- end
- end
- WDATA : begin
- if (end_cnt_bit && reg_ack) begin
- state_n = IDLE;
- end
- else if ((cnt_byte == (op_byte_num - 1)) && end_cnt_bit && (reg_ack == 0)) begin //
- state_n = STOP;
- end
- else begin
- state_n = state_c;
- end
- end
- RDATA : begin
- if ((cnt_byte == (op_byte_num - 1)) && end_cnt_bit) begin //
- state_n = STOP;
- end
- else begin
- state_n = state_c;
- end
- end
- STOP : begin
- if (end_cnt_bit) begin
- state_n = IDLE;
- end
- else begin
- state_n = state_c;
- end
- end
- default: state_n = IDLE;
- endcase
- end
- // cnt_bit bit_num
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- cnt_bit <= 0;
- end
- else if ((state_c != IDLE) && (cnt_scl == (SCL_PERIOD - 1))) begin
- if (end_cnt_bit) begin
- cnt_bit <= 0;
- end
- else begin
- cnt_bit <= cnt_bit + 1;
- end
- end
- end
- assign end_cnt_bit = (state_c != IDLE) && (cnt_scl == (SCL_PERIOD - 1)) && (cnt_bit == (bit_num - 1));
-
- always @(*) begin
- case (state_c)
- IDLE : bit_num = 0;
- START : bit_num = 1;
- DEV_CMD : bit_num = 9;
- ADDR : bit_num = 9;
- WDATA : bit_num = 9;
- RDATA : bit_num = 9;
- STOP : bit_num = 1;
- default : bit_num = 0;
- endcase
- end
-
- // rw_bit
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- rw_bit <= WRITE_BIT;
- end
- else begin
- case (i_cmd_op)
- OP_WRITE : rw_bit <= WRITE_BIT;
- OP_READ : begin
- if ((state_c == STOP) && end_cnt_bit) begin
- rw_bit <= WRITE_BIT;
- end
- else if ((state_c == DEV_CMD) && (rw_bit == WRITE_BIT) && end_cnt_bit && (reg_ack == 0)) begin //
- rw_bit <= READ_BIT;
- end
- end
- default : rw_bit <= rw_bit;
- endcase
- end
- end
-
- // cnt_byte op_byte_num
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- cnt_byte <= 0;
- end
- else if (state_c == IDLE) begin
- cnt_byte <= 0;
- end
- else if ((state_c == ADDR) && end_cnt_bit && (reg_ack == 0)) begin
- if (cnt_byte == (ADDR_LEN - 1)) begin
- cnt_byte <= 0;
- end
- else begin
- cnt_byte <= cnt_byte + 1;
- end
- end
- else if ((state_c == RDATA || state_c == WDATA) && end_cnt_bit && (reg_ack == 0)) begin
- if (cnt_byte == (op_byte_num - 1)) begin
- cnt_byte <= 0;
- end
- else begin
- cnt_byte <= cnt_byte + 1;
- end
- end
- end
-
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- op_byte_num <= 1;
- end
- else if ((state_c == START) && end_cnt_bit) begin
- op_byte_num <= i_op_byte_num;
- end
- end
-
-
- // reg_addr_1 reg_addr_2
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- reg_addr_1 <= 0;
- end
- else if ((state_c == START) && end_cnt_bit) begin
- reg_addr_1 <= i_reg_addr;
- end
- end
-
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- reg_addr_2 <= 0;
- end
- else if (state_c == ADDR) begin
- reg_addr_2 <= reg_addr_1[((ADDR_LEN - cnt_byte)*8-1) -: 8]; //
- end
- end
-
- // reg_txdat txget
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- reg_txdat <= 8'hff;
- end
- else if (state_c == WDATA) begin
- reg_txdat <= i_txdat;
- end
- end
-
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- txget <= 1'b0;
- end
- else if ((state_c == WDATA) && end_cnt_bit) begin
- txget <= 1'b1;
- end
- else begin
- txget <= 1'b0;
- end
- end
- // cnt_scl reg_scl
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- cnt_scl <= 0;
- end
- else if (state_c != IDLE) begin
- if (cnt_scl == (SCL_PERIOD - 1)) begin
- cnt_scl <= 0;
- end
- else begin
- cnt_scl <= cnt_scl + 1;
- end
- end
- end
-
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- reg_scl <= 0;
- end
- else if (cnt_scl == SCL_HALF - 1) begin
- reg_scl <= 1;
- end
- else if (cnt_scl == SCL_PERIOD - 1) begin
- reg_scl <= 0;
- end
- else begin
- reg_scl <= reg_scl;
- end
- end
- // reg_sda
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- reg_sda <= 1'b1;
- end
- else begin
- case (state_c)
- START : begin
- if (cnt_scl == LOW_HALF - 1) begin
- reg_sda <= 1'b1;
- end
- else if (cnt_scl == HIGH_HALF -1) begin
- reg_sda <= 1'b0;
- end
- end
- DEV_CMD : begin
- if (cnt_scl == LOW_HALF - 1) begin
- case (cnt_bit)
- 0 : reg_sda <= i_dev_addr[6];
- 1 : reg_sda <= i_dev_addr[5];
- 2 : reg_sda <= i_dev_addr[4];
- 3 : reg_sda <= i_dev_addr[3];
- 4 : reg_sda <= i_dev_addr[2];
- 5 : reg_sda <= i_dev_addr[1];
- 6 : reg_sda <= i_dev_addr[0];
- 7 : reg_sda <= rw_bit;
- default: reg_sda <= reg_sda;
- endcase
- end
- end
- ADDR : begin
- if (cnt_scl == LOW_HALF - 1) begin
- case (cnt_bit)
- 0 : reg_sda <= reg_addr_2[7];
- 1 : reg_sda <= reg_addr_2[6];
- 2 : reg_sda <= reg_addr_2[5];
- 3 : reg_sda <= reg_addr_2[4];
- 4 : reg_sda <= reg_addr_2[3];
- 5 : reg_sda <= reg_addr_2[2];
- 6 : reg_sda <= reg_addr_2[1];
- 7 : reg_sda <= reg_addr_2[0];
- default: reg_sda <= reg_sda;
- endcase
- end
- end
- WDATA : begin
- if (cnt_scl == LOW_HALF - 1) begin
- case (cnt_bit)
- 0 : reg_sda <= reg_txdat[7];
- 1 : reg_sda <= reg_txdat[6];
- 2 : reg_sda <= reg_txdat[5];
- 3 : reg_sda <= reg_txdat[4];
- 4 : reg_sda <= reg_txdat[3];
- 5 : reg_sda <= reg_txdat[2];
- 6 : reg_sda <= reg_txdat[1];
- 7 : reg_sda <= reg_txdat[0];
- default: reg_sda <= reg_sda;
- endcase
- end
- end
- RDATA : begin // SACK
- if ((cnt_scl == LOW_HALF - 1) && (cnt_byte == op_byte_num - 1) && (cnt_bit == 8)) begin //
- reg_sda <= 1'b1; // NACK
- end
- else if ((cnt_scl == LOW_HALF - 1) && (cnt_bit == 8)) begin
- reg_sda <= 1'b0; // ACK
- end
- end
- STOP : begin
- if (cnt_scl == LOW_HALF - 1) begin
- reg_sda <= 1'b0;
- end
- else if (cnt_scl == HIGH_HALF -1) begin
- reg_sda <= 1'b1;
- end
- end
- default : reg_sda <= reg_sda;
- endcase
- end
- end
-
- // reg_i_sda rx_data reg_ack rxvld
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- reg_i_sda <= 1'b1;
- end
- else begin
- reg_i_sda <= i_sda;
- end
- end
-
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- rx_data <= 0;
- end
- else if ((state_c == RDATA) && (cnt_scl == HIGH_HALF - 1)) begin
- case (cnt_bit)
- 0 : rx_data[7] <= reg_i_sda;
- 1 : rx_data[6] <= reg_i_sda;
- 2 : rx_data[5] <= reg_i_sda;
- 3 : rx_data[4] <= reg_i_sda;
- 4 : rx_data[3] <= reg_i_sda;
- 5 : rx_data[2] <= reg_i_sda;
- 6 : rx_data[1] <= reg_i_sda;
- 7 : rx_data[0] <= reg_i_sda;
- default: rx_data <= rx_data;
- endcase
- end
- end
-
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- reg_ack <= 1'b1;
- end
- else if ((state_c == DEV_CMD || state_c == ADDR || state_c == WDATA) && (cnt_bit == 8) && (cnt_scl == HIGH_HALF -1)) begin
- reg_ack <= reg_i_sda;
- end
- end
-
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- rxvld <= 1'b0;
- end
- else if ((state_c == RDATA) && end_cnt_bit) begin
- rxvld <= 1'b1;
- end
- else begin
- rxvld <= 1'b0;
- end
- end
- // busy
- always @(posedge i_clk or posedge i_rst) begin
- if (i_rst) begin
- busy <= 1'b0;
- end
- else if (i_iic_work_en) begin
- busy <= 1'b1;
- end
- // else if ((state_c == STOP) && end_cnt_bit) begin
- // busy <= 1'b0;
- // end
- else if (state_c == IDLE) begin
- busy <= 1'b0;
- end
- else begin
- busy <= 1'b1;
- end
- end
- // output
- assign o_scl = (state_c == IDLE)? 1 : reg_scl;
- assign o_txget = txget;
- assign o_rxdat = rx_data;
- assign o_rxvld = rxvld;
- assign o_busy = busy;
- assign o_sda = reg_sda;
- assign output_en = (state_c == RDATA)? ((cnt_bit == 8)? 1:0) : ((cnt_bit == 8)? 0:1);
-
- //assign o_output_en = output_en;
这里使用vivado中的原语IO_BUF来构建:
- IOBUF #(
- .DRIVE(12), // Specify the output drive strength
- .IBUF_LOW_PWR("TRUE"), // Low Power - "TRUE", High Performance = "FALSE"
- .IOSTANDARD("DEFAULT"), // Specify the I/O standard
- .SLEW("SLOW") // Specify the output slew rate
- ) iobuf_inst_sda (
- .O ( i_sda ), // Buffer output
- .IO ( io_sda ), // Buffer inout port (connect directly to top-level port)
- .I ( o_sda ), // Buffer input
- .T (~output_en ) // 3-state enable input, high=input, low=output
- );
-
- IOBUF #(
- .DRIVE(12), // Specify the output drive strength
- .IBUF_LOW_PWR("TRUE"), // Low Power - "TRUE", High Performance = "FALSE"
- .IOSTANDARD("DEFAULT"), // Specify the I/O standard
- .SLEW("SLOW") // Specify the output slew rate
- ) iobuf_inst_scl (
- .O ( i_scl ), // Buffer output
- .IO ( io_scl ), // Buffer inout port (connect directly to top-level port)
- .I ( o_scl ), // Buffer input
- .T ( 0 ) // 3-state enable input, high=input, low=output
- );
-
写时序
读时序:
其中,我们可以清楚的看到以下关键信息:
起始位:
状态二,设备地址与写控制位:
状态三,发送寄存器地址:
写数据操作,写了5个字节:
读操作时的RESTART状态,设备地址、读控制位以及读出的1字节数据:
停止位:
由以上设计可以看出,我设计的SCL是由低到高的,同时,在起始位的状态时,SCL也同样会由低到高,这与通常我们所理解的SCL在起始位保持高电平不符,如下图所示:
但经过我的上板实测,该方案可行。
第二点, 由以上设计可以看出,由于时序逻辑的影响,导致busy信号的输出会比state信号延迟一个周期,这意味这最好使用检测busy下降沿的方法来判断IIC接口是否还在工作,或者更改busy信号的赋值逻辑。