1、協議原理:
IIC(Inter-Integrated Circuit),i2c總線由數據線sda和時鍾線scl這兩條構成的串行總線,主機和從機可以在i2c總線上發送和接收數據。scl時鍾線作為控制,sda則包含有ack、nack、設備地址、字節地址、8bits數據。
起始信號(scl為高電平時,sda變成低電平)與結束信號(scl為高電平時,sda變成高電平)的狀態:
IIC單字節寫時序有兩種:1字節地址段器件單字節寫時序、2字節地址段器件單字節寫時序。
IIC單字節讀時序有兩種:1字節地址段器件單節讀時序、2字節地址段器件單節讀時序
字節地址高三位xxx:這里使用的EEPROM的存儲容量只有8192bits(1024bits*8)=210*23=213,所以16位的字節地址就多余了三位。
2、協議代碼:
學習文章:https://www.cnblogs.com/xiaomeige/p/6509414.html、https://www.cnblogs.com/liujinggang/p/9656358.html、正點原子教程
1、這里實現的是2字節單次讀寫。
2、開始和結束時,雖然scl為高電平,sda仍要變化;接下來傳輸字節,scl為低電平,sda才能變化。這里采取在scl高電平和低電平中線產生標志。
3、通過狀態機來實現讀寫。
綜合代碼:
module IIC_AT24C64( input sys_clk, input sys_rst_n, input iic_en, input [2:0]cs_bit,//可編程地址 input [12:0]byte_address,//字節地址 input write, input read, input [7:0]write_data, output reg[7:0]read_data, output reg scl, inout sda, output reg done ); parameter SYS_CLK=50_000_000,//系統時鍾50MHz SCL_CLK=200_000;//scl時鍾200KHz reg [7:0]scl_cnt;//時鍾計數 parameter div_cnt=SYS_CLK/SCL_CLK; always @(posedge sys_clk or negedge sys_rst_n)begin if(!sys_rst_n) scl_cnt<=8'd0; else if(scl_cnt == div_cnt-1'b1) scl_cnt<=8'd0; else scl_cnt<=scl_cnt+1'b1; end //生成scl時鍾線 always @(posedge sys_clk or negedge sys_rst_n)begin if(!sys_rst_n) scl<=1'b1; else if(scl_cnt == (div_cnt>>1)-1'b1) scl<=1'b0; else if(scl_cnt == div_cnt-1'b1) scl<=1'b1; else scl<=scl; end //scl電平中線 reg scl_high_middle;//scl高電平中線 reg scl_low_middle;//scl低電平中線 always @(posedge sys_clk or negedge sys_rst_n)begin if(!sys_rst_n)begin scl_high_middle<=1'b0; scl_low_middle<=1'b0; end else if(scl_cnt == (div_cnt>>2)) scl_high_middle<=1'b1; else if(scl_cnt == (div_cnt>>1)+(div_cnt>>2)) scl_low_middle<=1'b1; else begin scl_high_middle<=1'b0; scl_low_middle<=1'b0; end end reg [15:0]state; parameter idle=16'd1,//空閑狀態 w_or_r_start=16'd2,//設備地址 device_ADDR=16'd3,//發送 ACK1=16'd4, byte_ADDR_high=16'd5,//字節地址高8位 ACK2=16'd6, byte_ADDR_low=16'd7,//字節地址低8位 ACK3=16'd8, w_data=16'd9,//寫數據 ACK4=16'd10, r_start=16'd11,//讀開始 device_ADDR_r=16'd12,//設備地址讀 ACK5=16'd13, r_data=16'd14,//讀數據 NACK=16'd15,//非應答位 stop=16'd16; reg sda_en;//sda數據線使能 reg sda_reg;//sda數據暫存位 reg [7:0]sda_data_out;//sda數據發給從機暫存 reg [7:0]sda_data_in;//sda數據取之從機暫存 reg [3:0]bit_cnt;//每一bit assign sda=sda_en?sda_reg:1'bz; //讀寫標志位 reg w_flag; reg r_flag; always @(posedge sys_clk or negedge sys_rst_n)begin if(!sys_rst_n)begin state<=idle; w_flag<=1'b0; r_flag<=1'b0; sda_reg<=1'b1; done<=1'b0; sda_en<=1'b0; end else begin case(state) idle:begin sda_reg<=1'b1; w_flag<=1'b0; r_flag<=1'b0; sda_en<=1'b0; sda_reg<=1'b1; done<=1'b0; if(iic_en && write)begin w_flag<=1'b1; sda_en<=1'b1; sda_reg<=1'b1; state<=w_or_r_start; end else if(iic_en && read)begin r_flag<=1'b1; sda_en<=1'b1; sda_reg<=1'b1; state<=w_or_r_start; end else state<=idle; end w_or_r_start:begin if(scl_high_middle)begin sda_reg<=1'b0; sda_data_out<={4'b1010,cs_bit,1'b0};//在這里裝好設備地址 bit_cnt<=4'd8; state<=device_ADDR; end else begin sda_reg<=1'b1; state<=w_or_r_start; end end device_ADDR:begin if(scl_low_middle)begin bit_cnt<=bit_cnt-1'b1; sda_reg<=sda_data_out[7]; sda_data_out<={sda_data_out[6:0],1'b0};//在這里發出設備地址。其他的也是在上一狀態裝好值,下一個狀態發出 if(bit_cnt==0)begin state<=ACK1; sda_en<=1'b0; end else state<=device_ADDR; end else state<=device_ADDR; end ACK1:begin if(scl_high_middle)begin if(sda==1'b0)begin state<=byte_ADDR_high; sda_data_out<={3'bxxx,byte_address[12:8]}; bit_cnt<=4'd8; end else state<=idle; end else state<=ACK1; end byte_ADDR_high:begin if(scl_low_middle)begin sda_en<=1'b1; bit_cnt<=bit_cnt+1'b1; sda_reg<=sda_data_out[7]; sda_data_out<={sda_data_out[6:0],1'b0}; if(bit_cnt==0)begin state<=ACK2; sda_en<=1'b0; end else state<=byte_ADDR_high; end else state<=byte_ADDR_high; end ACK2:begin if(scl_high_middle)begin if(sda==1'b0)begin state<=byte_ADDR_low; sda_data_out<=byte_address[7:0]; bit_cnt<=4'd8; end else state<=idle; end else state<=ACK2; end byte_ADDR_low:begin if(scl_low_middle)begin sda_en<=1'b1; bit_cnt<=bit_cnt-1'b1; sda_reg<=sda_data_out[7]; sda_data_out<={sda_data_out[6:0],1'b0}; if(bit_cnt==0)begin state<=ACK3; sda_en<=1'b0; end else state<=byte_ADDR_low; end else state<=byte_ADDR_low; end ACK3:begin if(scl_high_middle)begin if(sda==1'b0)begin if(w_flag)begin sda_data_out<=write_data; bit_cnt<=4'd8; state<=w_data; end else if(r_flag)begin sda_reg<=1'b1; state<=r_start; end end else state<=ACK3; end end w_data:begin if(scl_low_middle)begin sda_en<=1'b1; bit_cnt<=bit_cnt-1'b1; sda_reg<=sda_data_out[7]; sda_data_out<={sda_data_out[6:0],1'b0}; if(bit_cnt==0)begin state<=ACK4; sda_en<=1'b0; end else state<=w_data; end else state<=w_data; end ACK4:begin if(scl_high_middle)begin if(sda==1'b0) state<=stop; else state<=idle; end else state<=ACK4; end r_start:begin if(scl_low_middle)begin sda_en<=1'b1; end else if(scl_high_middle)begin sda_reg<=1'b0; state<=device_ADDR_r; sda_data_out<={4'b1010,cs_bit,1'b1}; bit_cnt<=4'd8; end else begin sda_reg<=1'b1; state<=r_start; end end device_ADDR_r:begin if(scl_low_middle)begin bit_cnt<=bit_cnt-1'b1; sda_reg<=sda_data_out[7]; sda_data_out<={sda_data_out[6:0],1'b0}; if(bit_cnt==0)begin state<=ACK5; sda_en<=1'b0; end else state<=device_ADDR_r; end else state<=device_ADDR_r; end ACK5:begin if(scl_high_middle)begin if(sda==1'b0)begin state<=r_data; sda_en<=1'b0; bit_cnt<=4'd8; end else state<=idle; end else state<=ACK5; end r_data:begin if(scl_high_middle)begin sda_data_in<={sda_data_in[6:0],sda}; bit_cnt<=bit_cnt-1'b1; state<=r_data; end else if(scl_low_middle && bit_cnt==0) state<=NACK; else state<=r_data; end NACK:begin read_data<=sda_data_in; if(scl_high_middle)begin state<=stop; sda_reg<=1'b0; //為什么這里要為0?因為你發現其他的應答位都是判斷if(sda==1'b0),而這里由於最后一個是主機發出的應答,而不是像之前那些是從機發出的。 end //也可以看出主機應答的是直接讓output sda_reg為0,而前面的應答是等待從機input sda的值來判斷應答位 else state<=NACK; end stop:begin if(scl_low_middle)begin sda_en<=1'b1; sda_reg<=1'b1; state<=idle; done<=1'b1; end else state<=stop; end default:begin state<=idle; sda_en<=1'b0; sda_reg<=1'b1; w_flag<=1'b0; r_flag<=1'b0; done<=1'b0; end endcase end end endmodule
仿真代碼:直接用了別人的代碼。
仿真部分結果:設備地址:1010 001 0 字節地址高8位:xxx 00000 字節地址低8位:1100 1000 數據:1100 1000
主機是fpga,從機是EEPROM。仿真一個EEPROM存儲器:
`timescale 1ns/1ns `define timeslice 1250 module EEPROM_AT24C64( scl, sda ); input scl; //串行時鍾線 inout sda; //串行數據線 reg out_flag; //SDA數據輸出的控制信號 reg[7:0] memory[8191:0]; //數組模擬存儲器 reg[12:0]address; //地址總線 reg[7:0]memory_buf; //數據輸入輸出寄存器 reg[7:0]sda_buf; //SDA數據輸出寄存器 reg[7:0]shift; //SDA數據輸入寄存器 reg[7:0]addr_byte_h; //EEPROM存儲單元地址高字節寄存器 reg[7:0]addr_byte_l; //EEPROM存儲單元地址低字節寄存器 reg[7:0]ctrl_byte; //控制字寄存器 reg[1:0]State; //狀態寄存器 integer i; //--------------------------- parameter r7 = 8'b1010_1111, w7 = 8'b1010_1110, //main7 r6 = 8'b1010_1101, w6 = 8'b1010_1100, //main6 r5 = 8'b1010_1011, w5 = 8'b1010_1010, //main5 r4 = 8'b1010_1001, w4 = 8'b1010_1000, //main4 r3 = 8'b1010_0111, w3 = 8'b1010_0110, //main3 r2 = 8'b1010_0101, w2 = 8'b1010_0100, //main2 r1 = 8'b1010_0011, w1 = 8'b1010_0010, //main1 r0 = 8'b1010_0001, w0 = 8'b1010_0000; //main0 //--------------------------- assign sda = (out_flag == 1) ? sda_buf[7] : 1'bz; //------------寄存器和存儲器初始化--------------- initial begin addr_byte_h = 0; addr_byte_l = 0; ctrl_byte = 0; out_flag = 0; sda_buf = 0; State = 2'b00; memory_buf = 0; address = 0; shift = 0; for(i=0;i<=8191;i=i+1) memory[i] = 0; end //啟動信號 always@(negedge sda) begin if(scl == 1) begin State = State + 1; if(State == 2'b11) disable write_to_eeprom; end end //主狀態機 always@(posedge sda) begin if(scl == 1) //停止操作 stop_W_R; else begin casex(State) 2'b01:begin read_in; if(ctrl_byte == w7 || ctrl_byte == w6 || ctrl_byte == w5 || ctrl_byte == w4 || ctrl_byte == w3 || ctrl_byte == w2 || ctrl_byte == w1 || ctrl_byte == w0) begin State = 2'b10; write_to_eeprom; //寫操作 end else State = 2'b00; end 2'b11: read_from_eeprom; default: State = 2'b00; endcase end end //主狀態機結束 //操作停止 task stop_W_R; begin State = 2'b00; addr_byte_h = 0; addr_byte_l = 0; ctrl_byte = 0; out_flag = 0; sda_buf = 0; end endtask //讀進控制字和存儲單元地址 task read_in; begin shift_in(ctrl_byte); shift_in(addr_byte_h); shift_in(addr_byte_l); end endtask //EEPROM的寫操作 task write_to_eeprom; begin shift_in(memory_buf); address = {addr_byte_h[4:0], addr_byte_l}; memory[address] = memory_buf; State = 2'b00; end endtask //EEPROM的讀操作 task read_from_eeprom; begin shift_in(ctrl_byte); if(ctrl_byte == r7 || ctrl_byte == w6 || ctrl_byte == r5 || ctrl_byte == r4 || ctrl_byte == r3 || ctrl_byte == r2 || ctrl_byte == r1 || ctrl_byte == r0) begin address = {addr_byte_h[4:0], addr_byte_l}; sda_buf = memory[address]; shift_out; State = 2'b00; end end endtask //SDA數據線上的數據存入寄存器,數據在SCL的高電平有效 task shift_in; output[7:0]shift; begin @(posedge scl) shift[7] = sda; @(posedge scl) shift[6] = sda; @(posedge scl) shift[5] = sda; @(posedge scl) shift[4] = sda; @(posedge scl) shift[3] = sda; @(posedge scl) shift[2] = sda; @(posedge scl) shift[1] = sda; @(posedge scl) shift[0] = sda; @(negedge scl) begin #`timeslice; out_flag = 1; //應答信號輸出 sda_buf = 0; end @(negedge scl) begin #`timeslice; out_flag = 0; end end endtask //EEPROM存儲器中的數據通過SDA數據線輸出,數據在SCL低電平時變化 task shift_out; begin out_flag = 1; for(i=6; i>=0; i=i-1) begin @(negedge scl); #`timeslice; sda_buf = sda_buf << 1; end @(negedge scl) #`timeslice sda_buf[7] = 1; //非應答信號輸出 @(negedge scl) #`timeslice out_flag = 0; end endtask endmodule
仿真iic:
`timescale 1ns/1ns
`define clk_period 20
module iic_tb;
reg clk50M;
reg reset;
reg iic_en;
reg [12:0]address;
reg write;
reg [7:0]write_data;
reg read;
wire [7:0]read_data;
wire scl;
wire sda;
wire done;
integer i;
IIC_AT24C64 u_IIC_AT24C64(
.sys_clk(clk50M),
.sys_rst_n(reset),
.iic_en(iic_en),
.cs_bit(3'b001),
.byte_address(address),
.write(write),
.write_data(write_data),
.read(read),
.read_data(read_data),
.scl(scl),
.sda(sda),
.done(done)
);
EEPROM_AT24C64 EEPROM(
.scl(scl),
.sda(sda)
);
initial clk50M = 1'b1;
always #(`clk_period/2)clk50M = ~clk50M;
initial
begin
reset = 1'b0;
iic_en = 1'b0;
address = 13'h0;
write = 1'b0;
write_data = 1'b0;
read = 1'b0;
#(`clk_period*200 + 1)
reset = 1'b1;
#200;
write = 1'b1;
address = 200;
write_data = 200;
iic_en = 1'b1;
#(`clk_period)
iic_en = 1'b0;
for(i=199;i>0;i=i-1)
begin
@(posedge done);
#2000;
address = i;
write_data = i;
iic_en = 1'b1;
#(`clk_period)
iic_en = 1'b0;
end
@(posedge done);
#2000;
write = 1'b0;
#5000;
//?????????200???
read = 1'b1;
iic_en = 1'b1;
#(`clk_period)
iic_en = 1'b0;
for(i=200;i>0;i=i-1)
begin
@(posedge done);
#2000;
address = i;
iic_en = 1'b1;
#(`clk_period)
iic_en = 1'b0;
end
@(posedge done);
#20000;
read = 1'b0;
#5000;
$stop;
end
endmodule