iic协议--Verilog及仿真

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

原文地址:https://www.cnblogs.com/FPGAer/p/13788439.html