首页 > 技术文章 > FPGA——IIC协议学习笔记

cnlntr 2021-01-10 17:59 原文

一、IIC读时序,时序图解

  • 起始位:sclk为高电平时,SDA产生下降沿

  • 停止位:sclk为高电平时,SDA产生上升沿

  • 数据传输:sclk为高电平,SDA数据要保持稳定,sclk为低电平时,SDA数据可以发生变化

  • 响应位:数据的接收器发送低电平的响应位(发送器在响应位传输时,应该使发送器输出为高阻态,但也可以当做无关项设置)

  • 从机地址:找到指定相应从机

  • 从机寄存器地址:找到指定从机的寄存器存储的数据

  • SDA输出:在接收数据时,要输出高阻态

二、IIC官方文档时序


  • 标准模式下:SCL时钟频率为0-100KHz,起始位和停止位保持时间大于等于4us
  • 快速模式下:SCL时钟频率为0-400KHz,起始位和停止位保持时间大于等于0.6us

三、计数器设计快速模式二字节地址的IIC的思路


  • 设置scl计数器,计算SCL时钟频率

    • 计数器开始条件:

    读写使能

    • 计数器结束条件:

    系统时钟数到SCL规定时钟
    计数器最大值 = 系统时钟频率/SCL时钟频率;SCL_CNT_M = SYS_CLOCK / SCL_CLOCK

    //sclk时钟计数器
      always @(posedge clk or negedge rst_n)begin
          if(!rst_n)begin
              cnt_sclk <= 0;
          end
          else if(add_cnt_sclk)begin
              if(end_cnt_sclk)
                  cnt_sclk <= 0;
              else
                  cnt_sclk <= cnt_sclk + 1'b1;
          end
      end
      assign add_cnt_sclk = sclk_valid;
      assign end_cnt_sclk = add_cnt_sclk && cnt_sclk == SCL_CNT_M - 1;
    
  • 设置传输数据(比特位)计数器

    • 计数器开始条件:

    scl计数器结束

    • 计数器结束条件:

    协议要求数据传输完成

    //比特位计数器
      always @(posedge clk or negedge rst_n)begin
          if(!rst_n)begin
              cnt_byte <= 0;
          end
          else if(add_cnt_byte)begin
              if(end_cnt_byte)
                  cnt_byte <= 0;
              else
                  cnt_byte <= cnt_byte + 1'b1;
          end
      end
      assign add_cnt_byte = end_cnt_sclk;
      assign end_cnt_byte = add_cnt_byte && cnt_byte == cnt_byte_num - 1;
    
  • 关于起始位和停止位的问题

    • 将起始位当做要传输的0 ,一个比特位
    • 将停止位当做要传输的01,两个比特位
    • 在起始位与停止位的时候,SCL不能拉低保持高电平
    • 保持时间即为一个scl时钟周期
  • 关于IIC读写标志位以及7位从机地址

    • 读标志 0
    • 写标志 1
    • 7位从机地址不好表示,直接设置为8位,第0位为0,1-7位为寄存器地址
    • 8位从机地址 或运算 8'h00 写;(CTR_BYTE | 8'h00)
    • 8位从机地址 或运算 8'h01 读;(CTR_BYTE | 8'h00)

四、代码实现

module iic(
    clk             ,//系统时钟
    rst_n           ,//系统复位信号
    word_addr       ,//IIC器件寄存器地址
    wr              ,//写使能
    wr_data         ,//IIC器件写数据
    wr_data_valid   ,//IIC器件写数据有效标志位
    rd              ,//读使能
    rd_data         ,//读数据
    rd_data_valid   ,//IIC器件读数据有效标志位
    iic_scl         ,//IIC时钟线
    iic_sda         ,//IIC数据线
    done             //对IIC器件读写完成标识位
);
parameter CTR_BYTE  =   8'hA0;                  //M24LC64器件地址
parameter SYS_CLOCK =   50_000_000;             //系统时钟50MHz
parameter SCL_CLOCK =   400_000;                //sclk总线时钟400KHz快速模式
parameter SCL_CNT_M =   SYS_CLOCK / SCL_CLOCK;  //产生时钟sclk计数器最大值
parameter W_ADDR    =   16;                     //寄存器地址位宽
parameter WR_DATA   =   8;                      //写数据位宽
parameter RD_DATA   =   8;                      //读数据位宽
parameter CNT0_DATA =   7;                      //sclk计数器位宽
parameter CNT1_BYTE =   6;                      //byte计数器位宽
parameter WDATA	    =	50;						//wdata位宽		
parameter CNT_PHASE =   2;                      //阶段计数器位宽			

input                   clk;
input                   rst_n;
input   [W_ADDR-1:0]    word_addr;
input                   wr;
input   [WR_DATA-1:0]   wr_data;
input                   rd;
output                  wr_data_valid;
output  [RD_DATA-1:0]   rd_data;
output                  rd_data_valid;
output                  iic_scl;
output                  done;
inout                   iic_sda;

reg                     wr_data_valid;
reg     [RD_DATA-1:0]   rd_data;
reg                     rd_data_valid;
reg                     iic_scl;
reg                     done;

reg                     iic_out;
wire                    iic_in;
reg                     iic_en;

//中间变量
reg     [CNT0_DATA-1:0] cnt_sclk;
wire                    add_cnt_sclk;
wire                    end_cnt_sclk;

reg     [CNT1_BYTE-1:0] cnt_byte;
wire                    add_cnt_byte;
wire                    end_cnt_byte;

wire                    scl0;
wire                    scl1;
wire                    start_flag;
wire                    stop_flag;
wire                    wr_flag;
reg					    	sclk_valid;
reg	  [CNT1_BYTE-1:0]	cnt_byte_num;
reg	  [WDATA-1:0]		wdata;
wire							rd_flag;

wire                    rd2_start;

//三态门
//防止多主机冲突
//assign iic_sda = iic_en?(iic_out?1'bz:0):1'bz;
//assign iic_sda = (iic_en && !iic_out) ? 1'b0 : 1'bz;
assign iic_sda = iic_en?iic_out:1'bz;
assign iic_in = iic_sda;

//sclk时钟计数器
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        cnt_sclk <= 0;
    end
    else if(add_cnt_sclk)begin
        if(end_cnt_sclk)
            cnt_sclk <= 0;
        else
            cnt_sclk <= cnt_sclk + 1'b1;
    end
end
assign add_cnt_sclk = sclk_valid;
assign end_cnt_sclk = add_cnt_sclk && cnt_sclk == SCL_CNT_M - 1;

//比特位计数器
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        cnt_byte <= 0;
    end
    else if(add_cnt_byte)begin
        if(end_cnt_byte)
            cnt_byte <= 0;
        else
            cnt_byte <= cnt_byte + 1'b1;
    end
end
assign add_cnt_byte = end_cnt_sclk;
assign end_cnt_byte = add_cnt_byte && cnt_byte == cnt_byte_num - 1;

//sclk起始,结束
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        sclk_valid <= 0;
    end
    else if(wr || rd)begin
        sclk_valid <= 1'b1;
    end
    else if(end_cnt_byte)begin
        sclk_valid <= 1'b0;
    end
end

//sclk高低电平转换
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        iic_scl <= 1;
    end
    else if(scl0)begin
        iic_scl <= 0;
    end
    else if(scl1)begin
        iic_scl <= 1;
    end
end
assign scl0         =   (add_cnt_sclk && cnt_sclk == SCL_CNT_M-1) && (!start_flag) && (!stop_flag) && !end_cnt_byte && !rd2_start;  //scl信号拉低条件
assign scl1         =   cnt_sclk == ((SCL_CNT_M >> 1) - 1) && add_cnt_sclk;                                                         //scl信号拉高条件
assign start_flag   =   (add_cnt_sclk && cnt_sclk == 0) && cnt_byte == 0;                                                           //起始位
assign stop_flag    =   (add_cnt_sclk && cnt_sclk == SCL_CNT_M-1) && cnt_byte == cnt_byte_num - 2;                                  //停止位
assign rd2_start    =   add_cnt_byte && cnt_byte == 28 && rd_data_valid;                                                            //第二阶段读开始位

//写数据
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        iic_out <= 1;
    end
    else if(wr_flag)begin
        iic_out <= wdata[(WDATA - 1) - cnt_byte];
    end
end
assign wr_flag = cnt_sclk == ((SCL_CNT_M >> 2) - 1) && add_cnt_sclk;

//读数据
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        rd_data <= 8'b0;
    end
    else if(rd_flag)begin
        rd_data <= {rd_data[6:0],iic_in};
    end
end
assign rd_flag = (cnt_sclk == ((3*(SCL_CNT_M >> 2)) - 1)) && add_cnt_sclk && (cnt_byte >= 39) && (cnt_byte < 47) && rd_data_valid;

//读有效
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        rd_data_valid <= 0;
    end
    else if(rd && !wr && !sclk_valid)begin
        rd_data_valid <= 1;
    end
    else if((wr && !rd && !sclk_valid) || end_cnt_byte)begin
        rd_data_valid <= 0;
    end
end

//写有效
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        wr_data_valid <= 0;
    end
    else if(wr && !rd && !sclk_valid)begin
        wr_data_valid <= 1;
    end
    else if((rd && !wr && !sclk_valid) || end_cnt_byte)begin
        wr_data_valid <= 0;
    end
end

//cnt_byte_num设置
always @(*)begin
    if(wr_data_valid)begin
        //2字节地址写
        cnt_byte_num = 6'd39;
        //写数据
        wdata = {1'b0,CTR_BYTE | 8'h00,1'b0,word_addr[15:8],1'b0,word_addr[7:0],1'b0,wr_data,1'b0,1'b0,1'b1,11'b0};
    end
    else if(rd_data_valid)begin
        //2字节地址读
        cnt_byte_num = 6'd50;
        //读数据
        wdata = {1'b0,CTR_BYTE | 8'h00,1'b0,word_addr[15:8],1'b0,word_addr[7:0],1'b0,1'b1,1'b0,CTR_BYTE | 8'h01,1'b0,8'b0,1'b1,1'b0,1'b1};
    end
	else begin
		  cnt_byte_num = 0;
        wdata = 0;
	end
end

//done
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        done <= 0;
    end
    else if(end_cnt_byte)begin
        done <= 1;
    end
    else
        done <= 0;
end

//iic三态门使能
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        iic_en <= 0;
    end
    else if(wr && !rd && !sclk_valid)begin
        iic_en <= 1;
    end
    else if((rd && !wr && !sclk_valid) || (rd_data_valid && (cnt_byte == 0 || cnt_byte == 46) && add_cnt_byte))begin
        iic_en <= 1;
    end
    else if(rd_data_valid && cnt_byte == 38 && add_cnt_byte)begin
        iic_en <= 0;
    end
	else if(done)begin
		iic_en <= 0;
	end
end


endmodule

五、仿真调试方法

使用镁光官网提供的 EEPROM M24LC64 verilog仿真模型

https://www.microchip.com/wwwproducts/en/24LC64

1、仿真模型使用方法

  • 添加到工程文件综和

  • 在已经添加iic仿真的文件test bench设置里,添加24LC64.文件
    (选中第一个点edit,不要点new新建)

2、仿真文件代码


`timescale 1 ns/ 1 ns
module iic_vlg_tst();
// test vector input registers
reg clk;
reg rd;
reg rst_n;
reg [15:0] word_addr;
reg wr;
reg [7:0] wr_data;
// wires                                               
wire done;
wire iic_scl;
wire iic_sda;
wire [7:0]  rd_data;
wire rd_data_valid;
wire wr_data_valid;

// assign statements (if any)                          
//assign iic_sda = treg_iic_sda;
iic i1 (
// port map - connection between master ports and signals/registers   
	.clk(clk),
	.done(done),
	.iic_scl(iic_scl),
	.iic_sda(iic_sda),
	.rd(rd),
	.rd_data(rd_data),
	.rd_data_valid(rd_data_valid),
	.rst_n(rst_n),
	.word_addr(word_addr),
	.wr(wr),
	.wr_data(wr_data),
	.wr_data_valid(wr_data_valid)
);

M24LC64 M24LC64(
	.A0(1'b0),
	.A1(1'b0),
	.A2(1'b0),
	.WP(1'b0),
	.SDA(iic_sda),
	.SCL(iic_scl),
	.RESET(!rst_n)
);

//时钟周期,单位ns,在这里修改时钟周期
parameter CYCLE = 20;

//生成本地时钟50M
initial begin
	clk = 0;
	forever
	#(CYCLE/2)
	clk=~clk;
end

//产生复位信号
initial begin
	rst_n = 0;
	word_addr = 0;
	wr = 0;
	wr_data = 0;
	rd = 0;
	#(CYCLE * 200 + 1)
	rst_n = 1;
	#200;

	word_addr = 0;
	wr_data = 0;
	wr = 1'b1;
	#(CYCLE)
	wr = 1'b0;
	wr_data = 8'b1010_0011;
	#200000;

	word_addr = 0;
	rd = 1'b1;
	#(CYCLE)
	rd = 1'b0;
end

endmodule

3、仿真时,如何通过24LC64的内部信号查看是否将数据写入24LC64或读出24LC64

  • 点击sim
  • 点击M24LC64
  • 找到WrDataByte以及RdDataByte,选中,右键,添加波形,重新开始运行
  • 查看WrDataByte可以看到是否将数据写进去,查看RdDataByte可以看到是否能够读取数据



4.仿真波形


全部工程

推荐阅读