在I2C发起读/写过程之前,主设备会先发起I2C的启动(START), 从设备的地址匹配(从设备地址写),I2C的读/写控制以及从设备应答检测的等一系列过程。由于无论是I2C读还是I2C写,该部分的操作过程都是必须的,因此把该部分内容单独作为一个模块进行设计,便于代码的重用。时序如图1:

图1
1.I2C启动部分主设备程序设计
-
接口要求:
- 复位信号,系统时钟,系统时钟分频(由于I2C不同速度匹配)
- input rst,
- input sys_clk,
- input clk_en, clk_en_half,
- 从设备地址、启动信号、读/写标志(即该段程序之后的I2C 总线行为
- input [7:0] data_in, // {dev_addr[6:0], 1’b0/1’b1}
- input [1:0] op_cmd, // 2’b00 means START + dev_addr + R/W
- input op_cmd_en, // op_cmd enable , sys_clk single pulse
- I2C 总线的内部信号:由于inout信号一般只在端口使用,因此在FPGA的内部逻辑(内部模块)将会把inout(双向口)变换成input, output类型进行传递
- input sda_i,
- output sda_o,
- output sda_t,
- input scl_i,
- output scl_o,
- output scl_t,
- 握手信号:op_cmd_done 操作完成、从设备应答。当op_cmd_done产生,从设备应答后(dev_ack)同时送给上层模块。op_cmd_done 保持一个sys_clk 时钟宽度。
- output op_cmd_done, // operation finished
- output reg dev_ack//从设备是否有应答
- 复位信号,系统时钟,系统时钟分频(由于I2C不同速度匹配)
-
总体接口如下:
module i2c_addr(
input sys_clk,
input [1:0] op_cmd,
input op_cmd_en,
output reg op_cmd_done,
output reg dev_ack,
input [7:0] data_in,
input clk_en,
input clk_en_half,
output reg sda_t,
output sda_o,
input sda_i,
output reg scl_t,
output scl_o,
input scl_i,
input rst
);
-
主体程序设计
该部分程序实现的主体内容如下:初始化,在IDLE状态下检测启动请求(op_cmd = 2’b00),对从设备的地址与读写控制信号移位进行串行化,等待从设备应答。串行化期间对SCL,SDA的要求是,在SCL为高电平期间SDA保持不变(start除外),SDA的任何变化都在SCL为低电平的时段发生。程序的流程图如下:
图2
-
程序代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
`timescale 1ns / 1ps module i2c_addr( input sys_clk, input [1:0] op_cmd, input op_cmd_en, output reg op_cmd_done, output reg dev_ack, input [7:0] data_in, input clk_en, input clk_en_half, output reg sda_t, output sda_o, input sda_i, output reg scl_t, output scl_o, input scl_i, input rst ); /* op_cmd[1:0] : 00 send start + address + wr/rd (data_in) 01 send data 10 receive data (data_out) 11 send stop flag */ assign scl_o = 1'b0; assign sda_o = 1'b0; reg [7:0] i2c_st; reg [2:0] sft_count; reg [7:0] i2c_wr_data; //将需要写的数据存入本地寄存器中 always@(posedge sys_clk or posedge rst) if(rst) begin op_cmd_done <= 0; dev_ack <= 0; scl_t <= 1'b1; sda_t <= 1'b1; sft_count <= 7; i2c_wr_data <= 0; i2c_st <= 0; end else case(i2c_st) 0: begin sft_count <= 7; op_cmd_done <= 1'b0; dev_ack <= 0; scl_t <= scl_i; sda_t <= sda_i; i2c_wr_data <= data_in; if({op_cmd_en,op_cmd[1:0]} == 3'b100) i2c_st <= 1; // start flag end 1: //start flag begin if(clk_en) begin scl_t <= 1'b1; sda_t <= 1'b1; i2c_st <= 2; end end 2: begin scl_t <= 1'b1; if(clk_en) begin sda_t <= 1'b0; i2c_st <= 3; end end 3: begin if(clk_en) begin scl_t <= 1'b0; i2c_st <= 4; end end 4: //write begin if(clk_en) begin scl_t <= 1'b0; i2c_st <= 5; end end 5: begin if(clk_en_half) sda_t <= i2c_wr_data[sft_count]; if(clk_en) begin scl_t <= 1'b1; sft_count <= sft_count - 1; i2c_st <= 4; if( sft_count == 0 ) i2c_st <= 6; end end 6: begin if(clk_en) begin scl_t <= 1'b0; i2c_st <= 7; end end 7: //read ack/nack from device begin if( clk_en_half ) sda_t <= 1'b1; if(clk_en) begin scl_t <= 1'b1; dev_ack <= sda_i; i2c_st <= 8; end end 8: begin if(clk_en) begin scl_t <= 1'b0; op_cmd_done <= 1'b1; i2c_st <= 9; end end 9: begin op_cmd_done <= 1'b0; if(clk_en) begin scl_t <= 1'b1; i2c_st <= 0; end end default:i2c_st <= 0; endcase endmodulefsfsf |
- iic_phy 模块代码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
`timescale 1ns / 1ps module iic_phy( input sys_clk, input [1:0] op_cmd, input op_cmd_en, output reg op_cmd_done, output reg dev_ack, input master_ack, input [7:0] data_in, output [7:0] data_out, input clk_en, input clk_en_half, output reg sda_t, output sda_o, input sda_i, output reg scl_t, output scl_o, input scl_i, input rst ); /* op_cmd[1:0] : 00 send start + address + wr/rd (data_in) 01 send data 10 receive data (data_out) 11 send stop flag */ wire dev_ack_addr; wire dev_ack_wr; always @(*) begin case(op_cmd) 2'b00: dev_ack = dev_ack_addr; 2'b01: dev_ack = dev_ack_wr; default: ; endcase end wire op_cmd_done_addr; wire op_cmd_done_wr; wire op_cmd_done_read; wire op_cmd_done_stop; always @(*) begin case(op_cmd) 2'b00: op_cmd_done = op_cmd_done_addr; 2'b01: op_cmd_done = op_cmd_done_wr; 2'b10: op_cmd_done = op_cmd_done_read; 2'b11: op_cmd_done = op_cmd_done_stop; endcase end wire sda_t_addr; wire sda_t_wr; wire sda_t_read; wire sda_t_stop; wire scl_t_addr; wire scl_t_wr; wire scl_t_read; wire scl_t_stop; always @(*) begin case(op_cmd) 2'b00: begin sda_t = sda_t_addr; scl_t = scl_t_addr; end 2'b01: begin sda_t = sda_t_wr; scl_t = scl_t_wr; end 2'b10: begin sda_t = sda_t_read; scl_t = scl_t_read; end 2'b11: begin sda_t = sda_t_stop; scl_t = scl_t_stop; end endcase end i2c_addr i2c_addr_inst ( .sys_clk (sys_clk), .op_cmd (op_cmd), .op_cmd_en (op_cmd_en), .op_cmd_done (op_cmd_done_addr), .dev_ack (dev_ack_addr), .data_in (data_in), .clk_en (clk_en), .clk_en_half (clk_en_half), .sda_t (sda_t_addr), .sda_i (sda_i), .scl_t (scl_t_addr), .scl_i (scl_i), .rst (rst) ); assign sda_o = 1'b0; assign scl_o = 1'b0; endmodule |
- 将设计的模块例化,并组装到 iic_engine的模块中,这样就可以使用SCl_en模块,充分利用以设计的成果,逐步组装成程序代码及接口如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
`timescale 1ns / 1ps module iic_engine # ( parameter DIV_ADDR = 32'hFFFF_0001, parameter DEV_ADDR = 7'b101_0000, parameter CLK_F = 100_000_000, parameter SPEED = 100_000 ) ( input sys_clk, inout SDA, inout SCL, input [31:0] div_addr, input [31:0] div_in, input div_en, input div_rd, output [31:0] div_dout, input [1:0] op_cmd, input op_cmd_en, output op_cmd_done, output dev_ack, input master_ack, input [7:0] data_in, output [7:0] data_out, output i2c_error, input rst ); wire sda_i; wire sda_o; wire sda_t; wire scl_o; wire scl_t; wire scl_i; assign SDA = sda_t ? 1'bz: sda_o; assign sda_i = SDA; assign SCL = scl_t ? 1'bz: scl_o; assign scl_i = SCL; wire clk_en; wire clk_en_half; //=========================== i2c clock baudtick scl_en # ( .DIV_ADDR ( DIV_ADDR ), .CLK_F ( CLK_F ), .SPEED ( SPEED ) ) scl_en_inst ( .sys_clk ( sys_clk ), .div_addr ( div_addr ), .div_in ( div_in ), .div_en ( div_en ), .clk_en ( clk_en ), .div_rd ( div_rd ), .div_dout ( div_dout ), .clk_en_half ( clk_en_half ), .rst ( rst ) ); //===============================iic phy iic_phy iic_phy_inst( .sys_clk (sys_clk), .op_cmd ( op_cmd ), .op_cmd_en ( op_cmd_en ), .op_cmd_done ( op_cmd_done ), .dev_ack ( dev_ack ), .master_ack ( master_ack ), .data_in ( data_in ), .data_out ( data_out ), .clk_en ( clk_en ), .clk_en_half ( clk_en_half ), .sda_t ( sda_t ), .sda_o ( sda_o ), .sda_i ( sda_i ), .scl_t ( scl_t ), .scl_o ( scl_o ), .scl_i ( scl_i ), .rst ( rst ) ); endmodule |
-
仿真程序设计:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
`timescale 1ns / 1ps module tb_dev_addr(); parameter PERIOD = 10 ; parameter FREQUANCY = 100000000; //100M parameter SPEED = 100000; // 100K parameter SPEED1 = 400000; //400K parameter DEV_ADDR = 7'b101_0001; reg inclk; always #( PERIOD / 2 ) inclk = ~inclk; //133M wire sys_clk = inclk; reg rst; // reg [ 31: 0 ] div_in; //for baudrate register wire [ 31: 0 ] div_addr = 32'hFFFF_0001; reg div_en = 0; reg [1:0] op_cmd = 0; reg op_cmd_en = 0; wire op_cmd_done; wire dev_ack; reg master_ack = 0; reg [7:0] data_in = 0; wire [7:0] data_out; reg i2c_error = 0; initial begin rst = 1'b1; inclk = 0; #50 rst = 1'b0; end reg [3:0] st = 0; reg [31:0] cnt = 0; always@( posedge sys_clk or posedge rst ) if ( rst ) begin i2c_error <= 0; cnt <= 0; st <= 0; op_cmd_en <= 1'b0; op_cmd <= 0; end else begin case ( st ) 0: begin i2c_error <= 0; op_cmd <= 2'b00; op_cmd_en <= 0; if(cnt == 5000) begin cnt <= 0; st <= 1; end else cnt <= cnt + 1; end 1: begin data_in <= {DEV_ADDR, 1'b0}; op_cmd <= 2'b00; op_cmd_en <= 1; st <= 2; end 2: begin op_cmd_en <= 0; if(op_cmd_done) begin if(dev_ack) i2c_error <= 1; st <= 3; end end 3: begin st <= 3; end default: st <= 0; endcase end wire SDA; wire SCL; iic_engine #( .CLK_F ( FREQUANCY ) , //default with 100Mhz .SPEED ( SPEED ) //default speed 100k ) iic_engine_inst ( .sys_clk ( sys_clk ), .div_addr ( div_addr ), .div_in ( div_in ), .div_en ( div_en ), .op_cmd ( op_cmd ), .op_cmd_en ( op_cmd_en ), .op_cmd_done( op_cmd_done ), .dev_ack ( dev_ack ), .master_ack ( master_ack ), .data_in ( data_in ), .data_out ( data_out ), .SDA ( SDA ), .SCL ( SCL ), .rst ( rst ) ); PULLUP SDA_inst ( .O(SDA) ); PULLUP SCL_inst ( .O(SCL) ); AT24C02D # (.SLAVE_ADDRESS(DEV_ADDR)) AT24C02D_inst ( .SDA (SDA), .SCL (SCL), .WP (0) ); endmodule |
仿真程序中使用at24c02D IP ,这个IP 是由厂家提供的,这里我们只是使用这个I2C EEPROM ip 核。 IP核具体信息,可以参考文章https://www.microchip.com/en-us/product/AT24C02D
图3
注意,厂家提供的verilog IP ,目前只能在vivado下或者是questaSim 下仿真。 在quartus下或者在modelsim,questaSim_starter下不能仿真。(有些仿真语法不支持)。
在仿真工程中,我们主要学习三态门如何在仿真程序中仿真。
-
仿真结果
仿真波形如图4
图4
从图4可以看出在写阶段串行了1010_001(地址)及0(写控制)。24c02 iic设备应答。在I2C PHY层 Verilog程序结构化设计中介绍过op_cmd 定义:op_cmd[1:0] = 2’b00;代表start + write + dev_ack。 op_cmd[1:0] 和op_cmd_en 同时由仿真程序发出,当iic_phy完成相关操作后,返回op_cmd_done 信号, 代表当前操作命令(op_cmd)完成。