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