FPGA常见接口及逻辑实现(二)—— I2C

一、I2C协议简介

虽然上期说了接下来要更新SPI,但是由于我的板子没有SPI外设,只仿真也太不像话了,所以还是先写I2C了,等买个小SPI模块再更新SPI。

I2C(Inter-Integrated Circuit)是一种通用的总线协议。它是由Philips(飞利浦)公司,现NXP(恩智浦)半导体开发的一种简单的双向两线制总线协议标准。

对于硬件设计人员来说,只需要2个管脚,极少的连接线和面积,就可以实现芯片间的通讯,对于软件开发者来说,可以使用同一个I2C驱动库,来实现实现不同器件的驱动,大大减少了软件的开发时间。极低的工作电流,降低了系统的功耗,完善的应答机制大大增强通讯的可靠性。

I2C也是一种低速接口,常见工作速率主要有以下五种:

1.标准模式(Standard):不超过100Kbps

2.快速模式(Fast):不超过400Kbps

3.快速模式+(Fast-Plus):不超过1Mbps

4.高速模式(High-Speed):不超过3.4Mbps

5.超快模式(Ultra-Fast):不超过5Mbps(单向传输)

这里的速度单位bps是比特每秒,指数据传输的速率,但是由于I2C总线每个时钟周期只传输一比特数据,所以这个速率也等于I2C总线时钟的频率,即Hz。

由于I2C总线的时序比起串口来说较为复杂,本人对于I2C的时序有一些个人的理解,本期就粗略的介绍一下I2C的时序,在讲解时序之前先明确一些定义:

MASTER:主机,读写操作的发起方。

SLAVE:从机,读写操作的接收方。

SCL:I2C总线的时钟信号,由主机产生,接上拉电阻,空闲为高电平。

SDA:I2C总线的数据信号,接上拉电阻,空闲为高电平。

START:SCL为高时SDA的下降沿。

STOP:SCL为高时SDA的上升沿。

ACK:SDA低电平。

NO_ACK:SDA高电平。

I2C总线一次写入的时序如下:

START为起始信号,SDA下降沿以后,在SCL的低电平发送数据,图中第一个字节叫做CONTROL BYTE,是由七位从机器件ID和一位读写控制位构成的,至于图中为什么是1010xxB,主要是一般从机地址都是1010xxx;最后一位0是读写控制位,0代表写,1代表读。最后有一位响应位(ACK),代表从机接收到主机发送的内容并示意主机继续。

下一个字节开始就是数据操作了,图中把这个字节叫做字地址,但是地址归根结底也是一个八位数据,发送完一个字节的数据又是一个响应位,从机成功响应后接着发下一个数据。只要从机一直响应,主机可以一直发下去,直到从机响应失败,或者主机不想继续发送了,主动产生一个停止位。

I2C总线读时序:

读时序和写时序类似,也是先发器件ID和读写标志,然后进行数据操作,但是请注意,可以看到第一个起始信号后的读写信号是0,代表本次操作是写操作,然后下一个START之后的读写控制位才是1,代表本次操作才是读操作,因为第一次操作要先发送读取的地址,第二次操作再从刚刚发送的地址处读出数据。

很多文章将这一整个时序视为I2C的一次读操作,第二次的START叫做RESTART即重启信号,但是这其实就是一次写操作和一次读操作,事实证明,写完地址过一段时间再进行读操作,也能正确的读出该地址的数据,所以也没有什么RESTART,就是短时间内的第二次START,但是为了操作方便,还是按照这种方式来编写代码。

所以I2C的时序其实很简单,启动后第一个字节发送器件ID和读写控制位,等待响应,响应后根据读写控制位来进入写操作或读操作,每操作一个字节就等待响应,响应成功后进行下一个字节的操作,直到响应失败或主机停止。

二、I2C总线驱动verilog实现思路

上期说过verilog的编写思路主要就是计数器和状态机,根据上一部分的讲解,可以看到I2C的时序还是稍微复杂一点,只用计数器实现还是很不方便的,于是这次我将用状态机架构来实现I2C的主机和从机。

根据第一部分的时序总结,可以将一次读写操作分为以下几个状态:

平时为空闲状态(IDLE),开始信号(START/RESTART),器件ID和读写标志(DEVICE_ID),响应(ACK),写数据(WRITE),读数据(READ),结束信号(DONE)。

根据这个状态机做出波形图如下:

其中还有一个比特计数器,来计数当前操作的比特。

这个状态机已经涵盖了I2C所有可能的状态,但是响应(ACK)是双向的,当主机写的时候,是从机响应,当主机读的时候,是主机响应,所以还要将响应(ACK)分为主机响应(M_ACK)和从机响应(S_ACK),总共就是九种状态。

编写状态机的代码,从绘制状态转移图开始:

然后根据上图编写状态机即可。

编写状态机时一般都采用三段式状态机,具体什么是三段式状态机本文就不多说了,状态机编码除非状态多的离谱不然一般都采用独热码。

SDA的时序基本已解决,接下来看SCL的时序,I2C协议规定在SCL的低电平更新数据,在SCL的高电平采样数据,而且一般都是在低电平的最中心更新数据,所以很多教程都是用计数器生成SCL的过程中在中心位置产生脉冲来作为更新数据和采样数据的信号,但是实际器件很多时候并不需要那么准确的在中心位置更新和采样,比如我读写过的EEPROM都是直接取SCL下降沿作为ACK的开始输出低电平,下一个下降沿再释放总线,所以最好让数据更新点可调,可以满足各种情况,复用性高。

对此我们可以直接在SCL的变化沿进行数据操作,然后将输出的SCL进行一定周期的延时,这样我们可以自由的控制输出SCL的相位,还能简化代码编写的过程。

如上图,可以看出来只要在SCL每个周期的上升沿更新数据,下降沿采样数据即可,而起始位和结束位同样在下降沿。

上述内容主要是编写I2C主机的思路,为了将协议完整掌握,我们还要编写一个从机,从机一般都是用来和寄存器空间对接的,所以用户接口可以留ram接口,方便操作内存,时序方面类比主机,只不过要更精简一点,因为大部分时间都是主机在操作总线,所以像START和DONE这种状态从机就不需要了。

附上状态转移图:

其中中间第三行的状态是WAIT,没注意被遮住了,我的我的。

旁边是整理思路的时候写的,有些是错的,建议别看,以上就是编写思路,接下来介绍根据以上思路编写的代码。

三、I2C主机的verilog实现

我编写接口的理念是结构简单,复用性高,对于I2C主机,首先想到需要兼容的就是总线速度,所以要将I2C总线的速率参数化,除了速度,I2C还有个7位地址和10位地址的区别,不过这个问题不是问题,不管是7位地址还是10位地址,I2C总线的操作时序都是没有变化的,只是前两个字节都需要发送器件ID,这种事交给发送控制端去考虑就好,接口只需要负责兼容。

除此以外还有前文提到的SCL延时功能,也可以通过参数配置。

端口部分留下和控制器交互的操作启动输入,操作结束输出,操作长度,I2C必须的读写标志,器件ID,还有和fifo交互的数据接口即可。

综上所述,I2C主机的端口如下:

module i2c_master #(
    parameter SYS_CLK = 50_000_000,     // 输入时钟周期,单位为 Hz
    parameter IIC_FREQ = 100_000,       // 总线速度,单位为 Hz
    parameter SCL_DELAY_OW = 0,         // 时钟线延延时重载使能
    parameter SCL_DELAY_USR = 0         // 时钟线用户输入延时,单位为 周期(输入时钟)
)(
    input               clk,            // 输入时钟
    input               rst_n,          // 同步复位

    input               op_start,       // 启动信号
    output              op_done,        // 结束信号
    input   [3:0]       wr_len,         // 写操作长度
    input   [3:0]       rd_len,         // 读操作长度
    input   [1:0]       rw_flag,        // 读写标志 1 : read, 0 : write.
    input   [6:0]       device_id,      // 器件ID
    output              dreq,           // 数据请求
    input   [7:0]       din,            // 数据输入
    output              dvld,           // 数据有效
    output  [7:0]       dout,           // 数据输出

    output  wire        scl_out,        // IIC时钟输出
    output  wire        scl_ctrl,       // IIC时钟线控制
    input   wire        sda_in,         // IIC数据输入
    output  wire        sda_out,        // IIC数据输出
    output  wire        sda_ctrl        // IIC数据线控制
    );

可以看到端口声明中I2C的接口并不是scl和sda,而是分成了输入输出和控制,这是因为I2C总线是多主多从的协议,总线在不用的时候就要释放掉以便其他器件操作总线,所以scl和sda都是以inout的形式接入FPGA的,对于inout port,规范的操作是在top module把它分成in,out和tri三个信号接入其他模块,具体操作的代码是:

assign scl = scl_ctrl ? scl_out : 1'bz;
assign sda = sda_ctrl ? sda_out : 1'bz;
assign scl_in = scl;
assign sda_in = sda;

然后是变量声明和组合逻辑,代码如下:

// -------------------- Declaration --------------------

    // SCL相对于输入时钟的计数
    localparam SCL_CYCLE = SYS_CLK / IIC_FREQ;
    // 实际SCL延迟,当重载参数为1时选用用户定义的延迟,否则使用默认的1/4计数延迟
    localparam SCL_DELAY = SCL_DELAY_OW ? SCL_DELAY_USR : SCL_CYCLE/4 - 1;

    // 状态机编码
    localparam IDLE  = 8'b0000_0000;
    localparam START = 8'b0000_0001;
    localparam RE_ST = 8'b0000_0010;
    localparam DEVID = 8'b0000_0100;
    localparam WRITE = 8'b0000_1000;
    localparam READ  = 8'b0001_0000;
    localparam M_ACK = 8'b0010_0000;
    localparam S_ACK = 8'b0100_0000;
    localparam DONE  = 8'b1000_0000;

    // 寄存器声明
    reg                 start_ff1;
    reg                 start_ff2;
    reg                 start_reg;
    reg                 read_flag;
    reg                 op_type;

    reg                 iic_clk;
    reg     [9:0]       clk_cnt;

    reg     [3:0]       bit_cnt;
    reg     [3:0]       wr_byte_cnt;
    reg     [3:0]       rd_byte_cnt;
    reg                 iic_busy;
    reg     [7:0]       iic_dout;
    reg     [7:0]       iic_din;

    reg     [7:0]       cur_state;
    reg     [7:0]       nxt_state;
    reg     [127:0]     state_ascii;

    reg                 slave_ack;
    reg                 delay_array [0:SCL_DELAY-1];

    // 线网声明
    wire    scl = delay_array[SCL_DELAY-1];
    wire    update_edge = clk_cnt == SCL_CYCLE/2 - 1;
    wire    latch_edge = clk_cnt == SCL_CYCLE - 1;
    wire    one_byte = bit_cnt == 7;

    // 输出信号连接
    assign scl_out = scl;
    assign scl_ctrl = iic_busy;
    assign sda_out = iic_dout[7];
    assign sda_ctrl = iic_busy & (cur_state != S_ACK) & (cur_state != READ);
    assign dout = iic_din;
    assign dreq = cur_state == WRITE & one_byte & update_edge;
    assign dvld = cur_state == READ & one_byte & latch_edge;
    assign op_done = ((wr_byte_cnt == wr_len & op_type == 0)|(rd_byte_cnt == rd_len & op_type == 1)) & iic_busy;

时序逻辑代码如下:

// -------------------- I2C start --------------------

    // 接收开始信号并寄存
    always @(negedge clk) begin
        if(!rst_n) begin
            start_ff1 <= 0;
            start_ff2 <= 0;
        end else begin
            start_ff1 <= op_start;
            start_ff2 <= start_ff1;
        end
    end

    always @(negedge clk) begin
        if(!rst_n)
            start_reg <= 1'b0;
        else if(start_ff1 & ~start_ff2)
            start_reg <= 1'b1;
        else if(cur_state == START)
            start_reg <= 1'b0;
    end

    always @(negedge clk) begin
        if(!rst_n)
            read_flag <= 1'b0;
        else if(cur_state == RE_ST)
            read_flag <= 1'b0;
        else if(cur_state == START)
            read_flag <= rw_flag[1];
    end

// -------------------- I2C busy status --------------------

    // 根据状态产生总线忙碌信号
    always @(negedge clk) begin
        if(!rst_n)
            iic_busy <= 0;
        else if((cur_state == START | cur_state == RE_ST) & latch_edge)
            iic_busy <= 1;
        else if(cur_state == DONE & latch_edge)
            iic_busy <= 0;
    end

// -------------------- SCL generator --------------------

    // 产生SCL
    always @(posedge clk) begin
        if (!rst_n)
            iic_clk <= 1'b0;
        else if (update_edge)
            iic_clk <= 1'b1;
        else if (latch_edge)
            iic_clk <= 1'b0;
    end

    // SCL计数器
    always @(posedge clk) begin
        if (!rst_n) begin
            clk_cnt <= 10'd0;
        end
        else begin
            if (latch_edge) 
                clk_cnt <= 10'd0;
            else
                clk_cnt <= clk_cnt + 1'd1;
        end
    end

    // SCL输出延时
    always @(posedge clk)
        delay_array[0] <= iic_clk;

    genvar i;
    generate
        for (i = 0;i < SCL_DELAY-1;i = i + 1) begin
            always @(posedge clk)
                delay_array[i+1] <= delay_array[i];
        end
    endgenerate


// -------------------- FSM --------------------

    // 状态机第一段,时序逻辑切换状态
    always @(posedge clk) begin
        if(!rst_n)
            cur_state <= IDLE;
        else if(update_edge)
            cur_state <= nxt_state;
    end

    // 状态机第二段,组合逻辑产生次态
    always @(*) begin
        case (cur_state)

            IDLE:begin
                if(start_reg)        // 开始信号为高进入开始状态
                    nxt_state <= START;
                else if(read_flag)
                    nxt_state <= RE_ST;
                else
                    nxt_state <= IDLE;
            end

            START:begin
                if(iic_busy)        // 总线忙碌进入发送ID状态
                    nxt_state <= DEVID;
                else
                    nxt_state <= START;
            end

            RE_ST:begin
                if(iic_busy)        // 总线忙碌进入发送ID状态
                    nxt_state <= DEVID;
                else
                    nxt_state <= RE_ST;
            end

            DEVID:begin
                if(one_byte)        // 发送一个字节进入等待从机响应状态
                    nxt_state <= S_ACK;
                else
                    nxt_state <= DEVID;
            end

            WRITE:begin
                if(one_byte)        // 写一个字节进入等待从机响应状态
                    nxt_state <= S_ACK;
                else
                    nxt_state <= WRITE;
            end

            READ:begin
                if(one_byte)        // 读一个字节进入主机响应状态
                    nxt_state <= M_ACK;
                else
                    nxt_state <= READ;
            end

            M_ACK:begin
                if(op_done)         // 主机不响应进入结束状态
                    nxt_state <= DONE;
                else                // 主机响应进入读状态
                    nxt_state <= READ;
            end

            S_ACK:begin
                if(!slave_ack)      // 从机不响应进入结束状态
                    nxt_state <= DONE;
                else if(op_done)    // 主机操作完成进入结束状态
                    nxt_state <= DONE;
                else if(op_type)    // 从机响应且为读操作进入读状态
                    nxt_state <= READ;
                else                // 从机响应且为写操作进入写状态
                    nxt_state <= WRITE;
            end

            DONE:begin
                if(!iic_busy)  // 总线空闲则进入空闲状态
                    nxt_state <= IDLE;
                else
                    nxt_state <= DONE;
            end

            default: nxt_state <= IDLE;
        endcase
    end

    // 状态机第三段,各个状态输出信号
    // SCL上升沿更新数据
    always @(posedge clk) begin
        if(!rst_n)
            bit_cnt <= 0;
        else if(update_edge) begin
            if(cur_state == DEVID | cur_state == WRITE | cur_state == READ)
                bit_cnt <= bit_cnt + 1;
            else 
                bit_cnt <= 0;
        end
    end

    always @(posedge clk) begin
        if(!rst_n)
            iic_dout <= 0;
        else if(update_edge) begin
            if(cur_state == START)
                iic_dout <= {device_id,rw_flag[0]};
            else if(cur_state == RE_ST)
                iic_dout <= {device_id,rw_flag[1]};
            else if(cur_state == WRITE | cur_state == DEVID)
                iic_dout <= {iic_dout[6:0],1'b0};
            else if(cur_state == READ)
                iic_dout <= (rd_byte_cnt == rd_len - 1) ? 8'h80 : 8'h00;
            else if(cur_state == S_ACK)
                iic_dout <= slave_ack ? din : 8'h00;
            else if(cur_state == M_ACK)
                iic_dout <= 8'h00;
            else if(cur_state == IDLE)
                iic_dout <= 8'h00;
        end
    end

    always @(posedge clk) begin
        if(!rst_n)
            wr_byte_cnt <= 0;
        else if(cur_state == WRITE & one_byte & update_edge)
            wr_byte_cnt <= wr_byte_cnt + 1;
        else if(cur_state == DONE)
            wr_byte_cnt <= 0;
    end

    always @(posedge clk) begin
        if(!rst_n)
            rd_byte_cnt <= 0;
        else if(cur_state == READ & one_byte & update_edge) 
            rd_byte_cnt <= rd_byte_cnt + 1;
        else if(cur_state == DONE)
            rd_byte_cnt <= 0;
    end

    // SCL下降沿采样数据
    always @(posedge clk) begin
        if(!rst_n)
            iic_din <= 0;
        else if(latch_edge) begin
            if(cur_state == READ)
                iic_din[7 - bit_cnt] <= sda_in;
            else
                iic_din <= 0;
        end
    end

    always @(posedge clk) begin
        if(!rst_n)
            op_type <= 0;
        else if(cur_state == DEVID & one_byte & latch_edge)
            op_type <= sda_in;
        else if(cur_state == IDLE)
            op_type <= 0;
    end

    always @(posedge clk) begin
        if(!rst_n)
            slave_ack <= 0;
        else if(latch_edge) begin
            if(cur_state == S_ACK)
                slave_ack <= ~sda_in;
            else
                slave_ack <= 0;
        end
    end

    // simulation only
    always @(*) begin
        case(cur_state)
            IDLE:state_ascii <= "IDLE";
            START:state_ascii <= "START";
            RE_ST:state_ascii <= "RESTART";
            DEVID:state_ascii <= "DEVID";
            WRITE:state_ascii <= "WRITE";
            READ:state_ascii <= "READ";
            M_ACK:state_ascii <= "M_ACK";
            S_ACK:state_ascii <= "S_ACK";
            DONE:state_ascii <= "DONE";
            default:state_ascii <= "UNKNOWN";
        endcase
    end

endmodule

本次编写的I2C模块和串口模块的发送方式不同,选用了移位输出。

此次I2C的代码中没有sda和scl的同步链模块,这是因为sda和scl都是inout类型,由于inout是在顶层操作的,所以把同步工作也留给顶层去处理,而且I2C读写过程中由于总线不停的被占用和释放,经常产生毛刺,还需要对SDA做一个简单的滤波工作再接入I2C模块。

虽然代码看上去还是有点多,但是这已经是我能想到的兼顾复用性和稳定性的最简单的I2C主机了,代码大部分都在编写状态机,组合逻辑的部分也很简单,都是基于状态机和SCL变化沿的输出,很容易理解。

读写标志信号rw_flag有三种情况,为0时,写操作,写wr_len个字节的数据后停止;为1时是读操作,直接开始读rd_len个字节的数据后停止;为2时是先写后读的操作,也就是大部分器件的读指定寄存器的操作,先写wr_len个字节的数据后停止,再重新开始然后读rd_len个字节的数据后停止。

还有最后的代码,备注了simulation only的那部分,是一个仿真小技巧,因为vivado在仿真过程中,状态机的值没有办法显示为定义的状态名,仿真的时候就会很不方便,状态机全是0001,0010这种,很难分的清这个值是哪个状态,因此我们声明一个变量,每当状态切换时,同步把变量名作为字符串赋给这个变量,然后在仿真界面拖入这个变量,选择Radix -> ASCII,这样就能在仿真过程中显示状态名啦,而且由于这个变量完全没有使用,所以在综合的过程中就会优化掉,没有任何影响。

最后,附上顶层sda滤波模块的代码:

滤波模块:

module filter(
    input   wire    clk,
    input   wire    sin,
    output  wire    sout
    );

    (* ASYNC_REG = "true" *)reg     in_ff1;
    (* ASYNC_REG = "true" *)reg     in_ff2;
    (* ASYNC_REG = "true" *)reg     in_ff3;
    (* ASYNC_REG = "true" *)reg     out_ff;

    always @(posedge clk) begin
        in_ff1 <= ~sin;
        in_ff2 <= in_ff1;
        in_ff3 <= in_ff2;
    end

    always @(posedge clk) begin
        if(in_ff2 == in_ff1)
            out_ff <= in_ff2;
    end

    assign sout = ~out_ff;

endmodule

由于I2C没有从机无法仿真,没有响应直接就停止操作了,所以本文的仿真环节留到从机编写完成之后再进行。

四、I2C从机的verilog实现

I2C从机比起主机要简单很多,只用给出响应和返回数据即可,数据接收和串口类似,不过I2C是MSB在前LSB在后。从机可以直接在SCL的变化沿进行数据操作,所以不需要延时。

I2C从机不需要什么参数,只需要设定从机ID即可,和其他模块的接口就按照上文所说的,用ram接口,ram接口一般包含en,we,addr,din,dout。en使能时读取addr处的数据到dout,we和en同时使能时将din写入addr处,为了减少接口数量,就不要en信号了,让en总是处于使能状态即可,综上所述,I2C从机模块的代码如下:

`timescale 1ns / 1ps

module i2c_slave #(
    parameter DEVICE_ID = 7'b1010010    // 7'h52 从机ID
)(
    input               clk,        // 输入时钟
    input               rst_n,      // 同步复位

    input               SCL_in,     // I2C时钟输入
    input               SDA_in,     // I2C数据输入
    output              SDA_out,    // I2C数据输出
    output              sda_ctrl,   // I2C数据控制

    input   [7:0]       rd_data,    // 读ram数据
    output  [7:0]       wr_data,    // 写ram数据
    output  [7:0]       op_addr,    // 操作ram地址
    output  reg         wr_en       // 写ram使能
    );

//变量声明

    localparam IDLE  = 6'b000001;
    localparam DEVID = 6'b000010;
    localparam WAIT  = 6'b000100;
    localparam WBACK = 6'b001000;
    localparam M_ACK = 6'b010000;
    localparam S_ACK = 6'b100000;

    reg                 scl_ff1;
    reg                 scl_ff2;
    reg                 scl_ff3;
    reg                 scl_sync;
    reg                 sda_ff1;
    reg                 sda_ff2;
    reg                 sda_ff3;
    reg                 sda_sync;

    reg     [5:0]       cur_state;
    reg     [5:0]       nxt_state;
    reg     [63:0]      state_ascii;

    reg                 rw_flag;
    reg                 ad_flag;
    reg                 start_flag;
    reg                 done_flag;
    reg                 slave_ack;
    reg                 master_ack;
    reg     [6:0]       deviceid;
    reg     [7:0]       iic_dout;
    reg     [7:0]       iic_din;
    reg     [3:0]       bit_cnt;
    reg     [7:0]       r_addr;
    reg     [7:0]       r_data;

    wire    scl_rise = scl_ff3 & ~scl_sync;
    wire    scl_fall = ~scl_ff3 & scl_sync;
    wire    sda_rise = sda_ff3 & ~sda_sync;
    wire    sda_fall = ~sda_ff3 & sda_sync;
    wire    one_byte = bit_cnt == 7;

    assign SDA_out = iic_dout[7];
    assign sda_ctrl = cur_state == WBACK | cur_state == S_ACK;
    assign op_addr = r_addr;
    assign wr_data = r_data;

// 输入信号同步链滤波

    always @(posedge clk) begin
        scl_ff1 <= SCL_in;
        scl_ff2 <= scl_ff1;
        scl_ff3 <= (scl_ff2 == scl_ff1) ? scl_ff2 : scl_ff3;
        scl_sync <= scl_ff2;
    end

    always @(posedge clk) begin
        sda_ff1 <= SDA_in;
        sda_ff2 <= sda_ff1;
        sda_ff3 <= (sda_ff2 == sda_ff1) ? sda_ff2 : sda_ff3;
        sda_sync <= sda_ff2;
    end

// 状态机

    always @(posedge clk) begin
        if(!rst_n)
            cur_state <= IDLE;
        else if(scl_fall)
            cur_state <= nxt_state;
    end

    always @(*) begin
        case (cur_state)

            IDLE:begin
                if(start_flag)
                    nxt_state <= DEVID;
                else
                    nxt_state <= IDLE;
            end

            DEVID:begin
                if(one_byte)
                    nxt_state <= S_ACK;
                else
                    nxt_state <= DEVID;
            end

            WAIT:begin
                if(done_flag)
                    nxt_state <= IDLE;
                else if(one_byte)
                    nxt_state <= S_ACK;
                else
                    nxt_state <= WAIT;
            end

            WBACK:begin
                if(one_byte)
                    nxt_state <= M_ACK;
                else
                    nxt_state <= WBACK;
            end

            M_ACK:begin
                if(master_ack)
                    nxt_state <= WBACK;
                if(!master_ack)
                    nxt_state <= IDLE;
                else
                    nxt_state <= M_ACK;
            end

            S_ACK:begin
                if(rw_flag & slave_ack)
                    nxt_state <= WBACK;
                else
                    nxt_state <= WAIT;
            end

            default:nxt_state <= IDLE;
        endcase
    end

    always @(posedge clk) begin
        if(!rst_n) begin
            iic_dout <= 0;
            bit_cnt <= 0;
        end else if(scl_fall) begin
            case(cur_state)

                DEVID:begin
                    bit_cnt <= bit_cnt + 1;
                    iic_dout <= (deviceid == DEVICE_ID) ? 8'h00 : 8'hff;
                end

                WAIT:begin
                    bit_cnt <= bit_cnt + 1;
                    iic_dout <= (deviceid == DEVICE_ID) ? 8'h00 : 8'hff;
                end

                WBACK:begin
                    bit_cnt <= bit_cnt + 1;
                    iic_dout <= {iic_dout[6:0],1'b0};
                end

                M_ACK:begin
                    bit_cnt <= 0;
                end

                S_ACK:begin
                    bit_cnt <= 0;
                    iic_dout <= rd_data;
                end

                default:begin
                    iic_dout <= 0;
                    bit_cnt <= 0;
                end
            endcase
        end
    end

    always @(posedge clk) begin
        if(!rst_n) begin
            r_addr <= 0;
            r_data <= 0;
        end else if(cur_state == WAIT & scl_fall & one_byte) begin
            if(ad_flag) begin
                r_addr <= iic_din;
            end else begin
                r_data <= iic_din;
            end
        end else if(wr_en)
            r_addr <= r_addr + 1;
    end

    always @(posedge clk) begin
        if(!rst_n) begin
            deviceid <= 0;
            iic_din <= 0;
            master_ack <= 0;
            slave_ack <= 0;
        end else if(scl_rise) begin
            case(cur_state)
                DEVID:deviceid[6 - bit_cnt] <= sda_sync;
                WAIT:iic_din[7 - bit_cnt] <= sda_sync;
                M_ACK:master_ack <= ~sda_sync;
                S_ACK:slave_ack <= (deviceid == DEVICE_ID);
                IDLE:deviceid <= 0;
                default:begin
                    master_ack <= 0;
                    slave_ack <= 0;
                end
            endcase
        end
    end

    always @(posedge clk) begin
        if(!rst_n)
            wr_en <= 0;
        else if(cur_state == WAIT & scl_fall & one_byte & !ad_flag)
            wr_en <= 1;
        else
            wr_en <= 0;
    end

    always @(posedge clk) begin
        if(!rst_n)
            ad_flag <= 1;
        else if(cur_state == WAIT & scl_fall & one_byte & ad_flag)
            ad_flag <= 0;
        else if(cur_state == IDLE)
            ad_flag <= 1;
    end

    always @(posedge clk) begin
        if(!rst_n)
            rw_flag <= 0;
        else if(cur_state == DEVID & scl_rise & one_byte)
            rw_flag <= sda_sync;
        else if(cur_state == IDLE)
            rw_flag <= 0;
    end

    always @(posedge clk) begin
        if(!rst_n)
            start_flag <= 0;
        else if(scl_sync & sda_fall)
            start_flag <= 1;
        else if(cur_state == DEVID)
            start_flag <= 0;
    end

    always @(posedge clk) begin
        if(!rst_n)
            done_flag <= 0;
        else if(scl_sync & sda_rise)
            done_flag <= 1;
        else if(cur_state == IDLE)
            done_flag <= 0;
    end

    // simulation only
    always @(*) begin
        case (cur_state)
            IDLE:state_ascii <= "IDLE";
            DEVID:state_ascii <= "DEVID";
            WAIT:state_ascii <= "WAIT";
            WBACK:state_ascii <= "WBACK";
            M_ACK:state_ascii <= "M_ACK";
            S_ACK:state_ascii <= "S_ACK"; 
            default:state_ascii <= "IDLE";
        endcase
    end

endmodule

一般来说,Verilog代码中应该尽量减少信号的耦合,不同信号应该尽量分开单独编写一个always块,我这里就是懒了,感觉不冲突的信号都写到一起去了,这样并不好,除非是高度关联的信号,不然一般都分开写比较好。

五、I2C主从仿真

现在I2C的主机从机都已经编写完成,可以对其进行仿真了。

I2C的仿真需要一点平时不怎么用的小技巧,众所周知,I2C协议规定时钟线和数据线需要上拉,在空闲时都为高电平,而在仿真里,一条wire不被占用的时候,就成了高阻态z,这样就没法进行仿真了。所以我们需要用pullup语句模拟上拉电阻,这样这条wire在空闲的时候值就是1了。

    wire                scl;
    wire                sda;
    pullup(sda);
    assign sda = 
    sda_ctrl ? sda_out :
    sda_slv ? SDA_out : 1'bz;
    assign sda_in = sda;
    assign SDA_in = sda;

例化主机和从机,主机使用标准模式速度,使用默认延时,设定从机地址,主机操作地址设定为相同地址,开始仿真:

    i2c_master #(
        .SYS_CLK        (50_000_000),   // unit "hz"
        .IIC_FREQ       (100_000),      // unit "hz"
        .SCL_DELAY_OW   (0),            // delay overwrite enable
        .SCL_DELAY_USR  (20)            // delay input by user
    ) i2c_master_inst(
        .clk        (clk),
        .rst_n      (rst_n),

        .op_start   (op_start),
        .op_done    (op_done),
        .wr_len     (wr_len),
        .rd_len     (rd_len),
        .rw_flag    (rw_flag),    // 1 : read, 0 : write.
        .device_id  (device_id),
        .dreq       (txq),
        .din        (txd),
        .dvld       (rxv),
        .dout       (rxd),
        
        .scl_out    (scl),
        .sda_in     (sda_in),
        .sda_out    (sda_out),
        .sda_ctrl   (sda_ctrl));

    i2c_slave #(
        .DEVICE_ID (7'b1010010)    // 'h52
    ) i2c_slave_inst(
        .clk(clk),
        .rst_n(rst_n),
        .SCL_in(scl),
        .SDA_in(SDA_in),
        .SDA_out(SDA_out),
        .sda_ctrl(sda_slv),
        .rd_data(8'h9a),
        .wr_data(),
        .op_addr(),
        .wr_en());

先看主机时序,START状态SDA产生下降沿,进入写器件ID状态,写完等待从机响应,从机响应后根据读写标志进行读写操作,移位计数器循环计数移位输出。

可以看到在响应位后会有毛刺,这是因为主机是延时了四分之一周期的,而从机是直接在SCL变化沿操作的,所以在下降沿后从机已经完成响应操作并释放了总线,但是主机还没有进行下一步操作,所以总线空闲,呈现高电平,这种情况在实际应用中也不会影响读写结果的,只要在SCL高电平期间,数据保持稳定即可。

读写完成后在一个SCL的高电平器件拉高SDA作为一次操作的结束信号。

然后是从机时序,接收到主机发出的开始信号后,直接进入接收ID的状态,接收到的ID与自身ID相同时做出响应,否则不响应。

因为在响应之后,从机不知道主机是要继续写还是要结束操作,所以称该状态为等待状态,假如主机继续发送数据,从机就继续接收缓存并响应,假如在等待状态下收到主机发出的结束信号,就结束接收进入空闲状态。

六、I2C主机板级验证

终于来到了上板的环节,无论仿真的时序有多完美,没有上板就都是虚的。因为I2C是主从结构,没法环回验证,只能主机从机分开验证,先通过读写板上的EEPROM来验证主机的正确性,再通过主机读写从机来验证从机的正确性。

首先遇到的第一个问题就是如何与I2C主机交互,看代码可以知道我们I2C主机的用户接口比较多,没有匹配的通用接口,即使可以通过VIO控制所有的用户接口,但是这样的模块终究是复用性很差的,那么为了提高模块的复用性,对于这种用户接口很多的模块,可以用寄存器去控制,把所有用户接口和寄存器连接,然后用配置接口去控制寄存器,这样就可以通过配置接口去控制该模块了,常用的配置接口有native ram,axi lite还有Xilinx自己的drp等,为了简便这里就用native ram接口去编写一个寄存器控制模块。

`timescale 1ns / 1ps

module i2c_ctrl(
    input               clk,

    input               en,
    input               we,
    input       [7:0]   din,
    output  reg [7:0]   dout,
    input       [7:0]   addr,

    output              op_start,
    input               op_done,
    output      [3:0]   wr_len,
    output      [3:0]   rd_len,
    output      [1:0]   rw_flag,
    output      [6:0]   device_id,
    input               txq,
    output      [7:0]   txd,
    input               rxv,
    input       [7:0]   rxd
    );

    wire [3:0]  tx_ptr = reg_tx_buffer_ctrl[3:0];
    wire [3:0]  rx_ptr = reg_rx_buffer_ctrl[3:0];

// write only
    reg     [7:0]       reg_tx_buffer_0 = 0;    // 0x00
    reg     [7:0]       reg_tx_buffer_1 = 0;    // 0x01
    reg     [7:0]       reg_tx_buffer_2 = 0;    // 0x02
    reg     [7:0]       reg_tx_buffer_3 = 0;    // 0x03
    reg     [7:0]       reg_tx_buffer_4 = 0;    // 0x04
    reg     [7:0]       reg_tx_buffer_5 = 0;    // 0x05
    reg     [7:0]       reg_tx_buffer_6 = 0;    // 0x06
    reg     [7:0]       reg_tx_buffer_7 = 0;    // 0x07

// read only
    reg     [7:0]       reg_rx_buffer_0 = 0;    // 0x08
    reg     [7:0]       reg_rx_buffer_1 = 0;    // 0x09
    reg     [7:0]       reg_rx_buffer_2 = 0;    // 0x0a
    reg     [7:0]       reg_rx_buffer_3 = 0;    // 0x0b
    reg     [7:0]       reg_rx_buffer_4 = 0;    // 0x0c
    reg     [7:0]       reg_rx_buffer_5 = 0;    // 0x0d
    reg     [7:0]       reg_rx_buffer_6 = 0;    // 0x0e
    reg     [7:0]       reg_rx_buffer_7 = 0;    // 0x0f

// read - write
    reg     [7:0]       reg_tx_buffer_ctrl = 0; // 0x10
    // bit 7: clear buffer, self reset
    // bit 3-0: tx_ptr 
    reg     [7:0]       reg_rx_buffer_ctrl = 0; // 0x11
    // bit 7: clear buffer, self reset
    // bit 3-0: rx_ptr 

    reg     [7:0]       reg_op_status = 0;      // 0x12
    // bit 0: op start, self reset
    // bit 7: op done
    reg     [7:0]       reg_op_deviceid = 0;    // 0x13 8'b01010000 = 8'h50
    // bit 6-0: device id
    reg     [7:0]       reg_op_length = 0;      // 0x14
    // bit 7-0: operate length
    reg     [7:0]       reg_op_rwctrl = 0;      // 0x15
    // bit 0: read-write flag

    reg     [7:0]       reg_reserve_0 = 0;      // 0x16
    reg     [7:0]       reg_reserve_1 = 0;      // 0x17
    reg     [7:0]       reg_reserve_2 = 0;      // 0x18
    reg     [7:0]       reg_reserve_3 = 0;      // 0x19
    reg     [7:0]       reg_reserve_4 = 0;      // 0x1a
    reg     [7:0]       reg_reserve_5 = 0;      // 0x1b
    reg     [7:0]       reg_reserve_6 = 0;      // 0x1c
    reg     [7:0]       reg_reserve_7 = 0;      // 0x1d
    reg     [7:0]       reg_reserve_8 = 0;      // 0x1e
    reg     [7:0]       reg_reserve_9 = 0;      // 0x1f

    assign  op_start = reg_op_status[0];
    assign  wr_len = reg_op_length[3:0];
    assign  rd_len = reg_op_length[7:4];
    assign  rw_flag = reg_op_rwctrl[1:0];
    assign  device_id = reg_op_deviceid[6:0];
    assign  txd = 
    tx_ptr == 0 ? reg_tx_buffer_0 :
    tx_ptr == 1 ? reg_tx_buffer_1 :
    tx_ptr == 2 ? reg_tx_buffer_2 :
    tx_ptr == 3 ? reg_tx_buffer_3 :
    tx_ptr == 4 ? reg_tx_buffer_4 :
    tx_ptr == 5 ? reg_tx_buffer_5 :
    tx_ptr == 6 ? reg_tx_buffer_6 :
    tx_ptr == 7 ? reg_tx_buffer_7 : 8'h00;

    always @(posedge clk) begin
        if(en) begin
            case (addr)
                8'h08:dout <= reg_rx_buffer_0;
                8'h09:dout <= reg_rx_buffer_1;
                8'h0a:dout <= reg_rx_buffer_2;
                8'h0b:dout <= reg_rx_buffer_3;
                8'h0c:dout <= reg_rx_buffer_4;
                8'h0d:dout <= reg_rx_buffer_5;
                8'h0e:dout <= reg_rx_buffer_6;
                8'h0f:dout <= reg_rx_buffer_7;
                8'h10:dout <= reg_tx_buffer_ctrl;
                8'h11:dout <= reg_rx_buffer_ctrl;
                8'h12:dout <= reg_op_status;
                8'h13:dout <= reg_op_deviceid;
                8'h14:dout <= reg_op_length;
                8'h15:dout <= reg_op_rwctrl;
                8'h16:dout <= reg_reserve_0;
                8'h17:dout <= reg_reserve_1;
                8'h18:dout <= reg_reserve_2;
                8'h19:dout <= reg_reserve_3;
                8'h1a:dout <= reg_reserve_4;
                8'h1b:dout <= reg_reserve_5;
                8'h1c:dout <= reg_reserve_6;
                8'h1d:dout <= reg_reserve_7;
                8'h1e:dout <= reg_reserve_8;
                8'h1f:dout <= reg_reserve_9;
                default:dout <= dout;
            endcase
            if(we) begin
                case (addr)
                    8'h13:reg_op_deviceid <= din;
                    8'h14:reg_op_length <= din;
                    8'h15:reg_op_rwctrl <= din;
                    8'h16:reg_reserve_0 <= din;
                    8'h17:reg_reserve_1 <= din;
                    8'h18:reg_reserve_2 <= din;
                    8'h19:reg_reserve_3 <= din;
                    8'h1a:reg_reserve_4 <= din;
                    8'h1b:reg_reserve_5 <= din;
                    8'h1c:reg_reserve_6 <= din;
                    default:;
                endcase
            end
        end
    end

// -------------------- Tx buffer ctrl --------------------

    always @(posedge clk) begin
        if(reg_tx_buffer_ctrl[7]) begin
            reg_tx_buffer_0 <= 0;
            reg_tx_buffer_1 <= 0;
            reg_tx_buffer_2 <= 0;
            reg_tx_buffer_3 <= 0;
            reg_tx_buffer_4 <= 0;
            reg_tx_buffer_5 <= 0;
            reg_tx_buffer_6 <= 0;
            reg_tx_buffer_7 <= 0;
        end else if(we) begin
            case (addr)
                8'h00:reg_tx_buffer_0 <= din;
                8'h01:reg_tx_buffer_1 <= din;
                8'h02:reg_tx_buffer_2 <= din;
                8'h03:reg_tx_buffer_3 <= din;
                8'h04:reg_tx_buffer_4 <= din;
                8'h05:reg_tx_buffer_5 <= din;
                8'h06:reg_tx_buffer_6 <= din;
                8'h07:reg_tx_buffer_7 <= din;
                default:;
            endcase
        end
    end

    always @(posedge clk) begin
        if(we) begin
            if(addr == 8'h10)
                reg_tx_buffer_ctrl <= din;
        end else if(&reg_reserve_7) begin
            reg_tx_buffer_ctrl <= reg_tx_buffer_ctrl & 8'b0111_1111;
        end else if(txq) begin
            reg_tx_buffer_ctrl[3:0] <= reg_tx_buffer_ctrl[3:0] + 1;
        end
    end

// -------------------- Tx buffer ctrl --------------------

    always @(posedge clk) begin
        if(reg_tx_buffer_ctrl[7]) begin
            reg_rx_buffer_0 <= 0;
            reg_rx_buffer_1 <= 0;
            reg_rx_buffer_2 <= 0;
            reg_rx_buffer_3 <= 0;
            reg_rx_buffer_4 <= 0;
            reg_rx_buffer_5 <= 0;
            reg_rx_buffer_6 <= 0;
            reg_rx_buffer_7 <= 0;
        end else if(rxv) begin
            case (rx_ptr)
                8'h00:reg_rx_buffer_0 <= rxd;
                8'h01:reg_rx_buffer_1 <= rxd;
                8'h02:reg_rx_buffer_2 <= rxd;
                8'h03:reg_rx_buffer_3 <= rxd;
                8'h04:reg_rx_buffer_4 <= rxd;
                8'h05:reg_rx_buffer_5 <= rxd;
                8'h06:reg_rx_buffer_6 <= rxd;
                8'h07:reg_rx_buffer_7 <= rxd;
                default:;
            endcase
        end
    end

    always @(posedge clk) begin
        if(we) begin
            if(addr == 8'h11)
                reg_rx_buffer_ctrl <= din;
        end else if(&reg_reserve_8) begin
            reg_rx_buffer_ctrl <= reg_rx_buffer_ctrl & 8'b0111_1111;
        end else if(rxv) begin
            reg_rx_buffer_ctrl[3:0] <= reg_rx_buffer_ctrl[3:0] + 1;
        end
    end

    always @(posedge clk) begin
        if(we) begin
            if(addr == 8'h12)
                reg_op_status <= din;
        end else if(&reg_reserve_9) begin
            reg_op_status <= reg_op_status & 8'b1111_1110;
        end else begin
            reg_op_status <= reg_op_status | {op_done,7'b0000_000};
        end
    end

    always @(posedge clk) begin
        if(we) begin
            if(addr == 8'h1d)
                reg_reserve_7 <= din;
        end else if(!reg_tx_buffer_ctrl[7]) begin
            reg_reserve_7 <= 0;
        end else begin
            reg_reserve_7 <= {reg_reserve_7[6:0],1'b1};
        end
    end

    always @(posedge clk) begin
        if(we) begin
            if(addr == 8'h1e)
                reg_reserve_8 <= din;
        end else if(!reg_rx_buffer_ctrl[7]) begin
            reg_reserve_8 <= 0;
        end else begin
            reg_reserve_8 <= {reg_reserve_8[6:0],1'b1};
        end
    end

    always @(posedge clk) begin
        if(we) begin
            if(addr == 8'h1e)
                reg_reserve_9 <= din;
        end else if(!reg_op_status[0]) begin
            reg_reserve_9 <= 0;
        end else begin
            reg_reserve_9 <= reg_reserve_9 + 1;
        end
    end
    
endmodule

最后用VIO去控制这个ram接口,对寄存器进行读写,就可以轻松控制I2C主机了。再用ILA抓取I2C模块内部的SCL和SDA,方便观察总线波形。编写好的顶层模块如下:

`timescale 1ns / 1ps

module top(
    input   wire        sys_clk,
    input   wire        sys_rst_n,

    inout   wire        scl,
    inout   wire        sda
    );

    wire                clk_50M;
    wire                sync_rst_n;

    wire                write;
    wire                read;
    wire    [7:0]       wr_data;
    wire    [7:0]       rd_data;
    wire    [7:0]       rw_addr;

    wire                en;
    wire                we;
    wire    [7:0]       din;
    wire    [7:0]       dout;
    wire    [7:0]       addr;

    wire                scl_in;
    wire                scl_out;
    wire                scl_ctrl;
    wire                sda_in;
    wire                sda_out;
    wire                sda_ctrl;

    assign scl = scl_ctrl ? scl_out : 1'bz;
    assign sda = sda_ctrl ? sda_out : 1'bz;
    assign scl_in = scl;
    assign sda_in = sda;

    sys_pll pll_inst(
        .clk_in     (sys_clk),
        .clk_out    (clk_50M)
    );

    filter filter_rst(
        .clk        (clk_50M),
        .sin        (sys_rst_n),
        .sout       (sync_rst_n));

    ram_ctrl ram_ctrl_inst(
        .clk        (clk_50M),

        .write      (write),
        .read       (read),
        .wr_data    (wr_data),
        .rd_data    (rd_data),
        .rw_addr    (rw_addr),

        .en         (en),
        .we         (we),
        .din        (din),
        .dout       (dout),
        .addr       (addr));

    i2c_master_top i2c_master_top_inst(
        .clk        (clk_50M),
        .rst_n      (sync_rst_n),

        .scl_out    (scl_out),
        .scl_ctrl   (scl_ctrl),
        .sda_in     (sda_in),
        .sda_out    (sda_out),
        .sda_ctrl   (sda_ctrl),

        .en         (en),
        .we         (we),
        .din        (din),
        .dout       (dout),
        .addr       (addr));

    vio_0 vio_inst(
        .clk        (clk_50M),
        .probe_out0 (write),
        .probe_out1 (read),
        .probe_out2 (wr_data),
        .probe_out3 (rw_addr),
        .probe_in0  (rd_data),
        .probe_in1  (rw_flag),
        .probe_in2  (device_id),
        .probe_in3  ({wr_len,rd_len})
    );

    ila_i2c ila_inst(
        .clk        (clk_50M),
        .probe0     (scl_in),
        .probe1     (sda_in)
    );

endmodule

程序上板后,先对寄存器进行配置,向对应地址写入值:

读写标志设置为0,写操作,写操作长度3,写入三字节数据,器件ID设为EEPROM的ID,在TX buffer的前两字节里写入了00和10,第三个字节处写入8D,即往0x0010地址处写入0x8D。往启动寄存器中写入01开始发送,然后抓取总线的波形:

可以看到和预期的波形一致,EEPROM也正确地产生了响应位。

再次对寄存器进行配置:

读写标志设置为10,先写后读操作,写操作长度为2,读操作长度为1,写两字节数据,读一字节数据,器件ID不变。对于我写的寄存器模块,还需要清除发送buffer的指针。因为要读同一地址,发送buffer中的内容也不用变,即读出EEPROM在0x0010处的数据。往启动寄存器中写入01开始发送:

成功地读出了0x8D!证明该主机模块是可以使用的。

七、I2C从机板级验证

有了验证通过的主机,就可以使用主机来验证从机了,本次验证在两块FPGA开发板上进行,在另一块板上例化从机后,把I2C接口约束在任意两个引出的IO,修改此开发板上主机的引脚约束,也约束在两个引出的IO上,这样通过杜邦线连接就可以实现I2C通信了。

module top(
    input               sys_clk,
    input               sys_rst_n,

    input               scl,
    inout               sda,

    output  [3:0]       led_out
);

    wire                clk_100M;
    wire                pll_locked;

    wire    [7:0]       wr_data;
    wire    [7:0]       rd_data;
    wire    [7:0]       op_addr;
    wire                wr_en;

    wire                scl_in;
    wire                sda_in;
    wire                sda_out;
    wire                sda_ctrl;

    wire    [3:0]       flow;

    assign led_out = flow;
    assign scl_in = scl;
    assign sda_in = sda;
    assign sda = sda_ctrl ? sda_out : 1'bz;

    sys_pll pll_inst(
        .CLK    (sys_clk), 
        .CLKOP  (clk_100M), 
        .LOCK   (pll_locked));

    timer #(
        .SYS_CLK (100_000_000)
    ) timer_inst(
        .clk(clk_100M),
        .rst_n(sys_rst_n),
        .intr_a(intr_a),
        .intr_b(intr_b),
        .flow(flow));

    spram #(
        .DATA_WIDTH(8),
        .RAM_DEPTH(256)
    ) spram(
        .clk(clk_100M),
        .en(1'b1),
        .we(wr_en),
        .addr(op_addr),
        .din(wr_data),
        .dout(rd_data));

    i2c_slave #(
        .DEVICE_ID (7'b1010010) // 7'h52 从机ID
    ) i2c_slave_inst(
        .clk(clk_100M),         // 输入时钟
        .rst_n(sys_rst_n),      // 同步复位

        .SCL_in(scl_in),        // I2C时钟输入
        .SDA_in(sda_in),        // I2C数据输入
        .SDA_out(sda_out),      // I2C数据输出
        .sda_ctrl(sda_ctrl),    // I2C数据控制

        .rd_data(rd_data),      // 读ram数据
        .wr_data(wr_data),      // 写ram数据
        .op_addr(op_addr),      // 操作ram地址
        .wr_en(wr_en)           // 写ram使能
    );

endmodule

用杜邦线连接的I2C需要注意一个问题,I2C协议中SCL和SDA都是上拉的,一般开发板上的电路都是在外部做了上拉的,所以不需要进行额外的处理,但是杜邦线肯定是没有上拉的,所以需要在引脚配置界面将SCL和SDA设置为上拉模式,主机和从机的四个引脚都需要上拉。

因为从机是一字节地址,所以写长度设为2,写入一字节地址一字节数据,寄存器配置为在0x00处写入0x53。

可以看出从机是正常响应的,接下来再读0x00处的数据,读出0x53,从机验证完成。

这一篇内容拖了时间太长,中间还经历了换不同板子,换不同厂家的芯片,刚开始写的一些内容到后来已经不一样了,毕竟用公司的板子,还是限制比较多,有些接口还得自己买外界模块才能做,SPI flash模块已经到了,下一篇内容就是SPI接口了,欢迎持续关注!

  • 31
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值