Verilog:I2C控制器

目录

一、I2C 通信协议

        (1)简介

        (2)时序

二、Verilog 实现

        (1)设计要求

        (2)设计要点

        (3)模块完整代码

三、功能验证

        (1)写数据

        (2)读数据


一、I2C 通信协议

        (1)简介

        I2C IIC(Inter-Integrated Circuit)是一种串行通信协议,通常用于微控制器、传感器和其他集成电路之间的短距离通信。主要用于在主设备(Master)和从设备(Slave)间传输数据。通常支持多种速率,通常标准模式100 kbps 和快速模式 400 kbps,高速模式3.4 mbps。

        I2C 使用两根线进行通信:SDA(Serial Data Line) 数据线、SCL(Serial Clock Line)时钟线,由主设备生成,用于同步数据传输。I2C 地址寻址:I2C设备由唯一的地址标识,通常为7位。主设备通过地址选择特定的从设备进行通信。(注意:总线有上拉电阻,这里示意图省略了)

        (2)时序

       本文简单介绍 I2C 单字节读写时序。起始位、停止位、读写数据、应答的时序规范如下图所示,同时需要注意以下几点:

        1.SCL和SDA 空闲时均为高电平(高阻态+上拉)

        2.读命令为1、写命令为0 

        3. ACK应答由从设备发送,0表示从设备接收成功,主设备可继续发送数据。

        4.NACK无应答由主设备发送,1表示数据读取结束。

(单字节)写时序 :Byte Write

        主设备向从设备特定地址写入数据,sda数据线上依次为:起始位、7bit从地址+写命令0、应答1、8bit字节地址、应答2、8bit写入数据、应答3、停止位 

AT24C02官方时序图

(随机)读时序 :Random Read

        主设备读取从设备特定地址数据,sda数据线上依次为:起始位、7bit从设备地址+写命令0、应答1、8bit字节地址、应答2、起始位2、7bit从设备地址+读命令1、应答3、8bit读取数据、无应答、停止位                                                                        

AT24C02官方时序图

(蓝色表示读写操作都有的公共状态,红色表示各自的特殊状态,如此区分方便后面状态机的设计)

       

二、Verilog 实现

        (1)设计要求

                1. 实现对 EEPROM(AT24C02)的单字节读写操作

                2. I2C速度采用快速模式(400KHz,即AT24C02支持最大频率)

        (2)设计要点

                1.sda双向端口

        sda是双向信号,既可以做输入也可以做输出。因此在 Verilog 中应声明为 “inout ” 类型。对这类信号需要一个控制信号进行输入输出控制,便定义 sda_oe(sda output enable)当 sda_oe 为1时表示sda作输出 ,反之作输入

        sda作输出:根据I2C协议规范,当sda要输出高电平时只需要保持高阻态1’bz即可,因为总线有上拉电阻,高阻态+上拉电阻即为高电平。而低电平则正常输出1’b0。但是不能直接对sda进行赋值,需要一个寄存器 sda_out 来间接控制,sda_out 为1表示输出z,为0表示输出0。

        sda作输入:此时sda需要保持为高阻态以确保主设备不会影响从设备对于sda信号的控制作,即对外呈高阻态确保总线信号互不干扰。同样由于inout类型信号的特殊性,不可以直接读取sda,可以定义一个 wire 类型的 sda_in 来传输sda的值,需要读取sda时可以通过sda_in获取。

        综上所述,用Verilog语言进行描述:

module i2c_ctrl(
    inout  wire sda,    //双向数据线(inout)
    ...
    ...
);
reg  sda_oe;            //sda输出使能,为1表示sda作输出
reg  sda_out;           //sda输出信号线
wire sda_in;            //sda输入寄存器
assign sda_in = sda;    //sda作输入直接读
assign sda = sda_oe ? (sda_out ? 1'bz : 1'b0) : 1'bz; 
//作输入需确保总线信号互不干扰对外呈高阻态,空闲和输出1时输出高阻态,因为sda线有上拉电阻

                2.scl时钟信号

        scl信号在空闲时同样保持高电平,只有在i2c工作时产生工作时钟信号,其频率决定了即为数据传输速度,本次设计速度采用400KHz,因此设计一个分频计数器对100MHz系统时钟clk进行分频,且该分频器只在i2c工作时(work_en=1)产生分频时钟信号 clk_div 。

        此外还定义了三个信号线 wire scl_half_1、wire scl_half_0、wire scl_ack_jump,他们分别对应 scl 时钟的高电平中点时刻、低电平中点时刻、高电平中点前5个clk时刻。这些时刻为数据读取、数据输出、应答位状态跳转时刻。

//---------------------------------FPGA系统时钟clk频率为100MHz
wire scl_half_1;
wire scl_half_0;
wire scl_ack_jump;
assign scl_half_1  = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b1);     //scl高电平中点(起始位、ACK接收、读数据、停止位时刻)
assign scl_half_0  = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b0);     //scl低电平中点(写数据时刻)
assign scl_ack_jump=((cnt_clk ==(cnt_max_400khz >> 1)-5) && clk_div==1'b0); //scl低电平中点前5clk周期---
//---(ACK状态的下一状态跳转时刻,因为跳转都是由输入转输出状态,快一周期让输出状态赶上紧跟着的第一个scl_half_0,避免错过第1位数据)

//分频计数器(400khz时钟scl)
always @(posedge clk or negedge rst_n) begin
    if (!work_en || !rst_n) begin
        cnt_clk <= 8'd1;
        clk_div <= 1'b1;
	end else if (cnt_clk == cnt_max_400khz) begin
        cnt_clk <= 8'd1;
        clk_div <= ~clk_div;
    end else 
        cnt_clk <= cnt_clk + 8'd1;
end
assign scl = clk_div;

                3.状态机

        在时序部分介绍了读/写操作的各个状态,不难发现从第一个起始位到应答位2均为公共状态(起始位、7bit从设备地址+写命令0、应答1、8bit字节地址、应答2),且特殊状态都是在 应答2这一状态进行区分的,因此可以设计一个rw_ctrl信号来区分读写操作,rw_ctrl = 0表示写操作,1表示读操作,使状态机进入不同的状态分支,最后都回到停止位状态。(此外若从设备应答失败即ack=1会跳转至空闲状态停止工作)

状态转移图
//状态机参数
reg [3:0] state;        //当前状态
reg [3:0] next_state;   //下一状态
localparam  //--------------------------------------------公共状态
            IDLE          = 4'd0,  //空闲
            START         = 4'd1,  //起始位
            W_SLAVE_ADDR  = 4'd2,  //写7位从设备地址+写命令0
            ACK1          = 4'd3,  //应答1
            W_BYTE_ADDR   = 4'd4,  //写8位字地址
            ACK2          = 4'd5,  //应答2(状态转移时进行读写判断)
            STOP          = 4'd6,  //停止位
            //--------------------------------------------写操作特殊状态
            W_DATA        = 4'd7,  //写8位数据位
            W_ACK3        = 4'd8,  //写应答3           
            //--------------------------------------------读操作特殊状态
            START2        = 4'd9,  //读操作第2次起始位                         
            R_SLAVE_ADDR  = 4'd10, //写7位从设备地址+读命令1 
            R_ACK3        = 4'd11, //读应答3 
            R_DATA        = 4'd12, //读8位数据位        
            N_ACK         = 4'd13; //无应答

(3)模块完整代码

        结合以上要点,最终的i2c_ctrl模块代码设计如下:(work_start为启动信号,i2c模块接收到一个高电平脉冲开始工作,work_done为工作结束信号,读/写完成产生一个高电平脉冲(持续两个clk周期,因为是两段式状态机,若只要持续一个clk周期,改为单端式状态机即可))

`timescale 1ns / 1ps
module i2c_ctrl(
    input  wire clk,                //系统时钟100MHz
    input  wire rst_n,              //复位
    inout  wire sda,                //双向数据线(inout)
    output wire scl,                //输出时钟线
    input  wire rw_ctrl,            //读写使能信号(0写1读)
    input  wire work_start,         //i2c启动信号
    input  wire [6:0] slave_addr,   //7bit从设备地址
    input  wire [7:0] byte_addr,    //8bit字地址 
    input  wire [7:0] w_data,       //8bit待写数据
    output reg  [7:0] r_data,       //8bit读取数据
    output reg  work_done           //i2c读写完成信号
);
//sda传输方向控制
reg  sda_oe;            //sda输出使能,为1表示sda作输出
reg  sda_out;           //sda输出信号线
wire sda_in;            //sda输入寄存器
assign sda_in = sda;    //sda作输入直接读
assign sda = sda_oe ? (sda_out ? 1'bz : 1'b0) : 1'bz; //作输入需确保总线信号互不干扰对外呈高阻态,空闲和输出1时输出高阻态,因为sda线有上拉电阻

//状态机参数
reg [3:0] state;        //当前状态
reg [3:0] next_state;   //下一状态
localparam  //--------------------------------------------公共状态
            IDLE          = 4'd0,  //空闲
            START         = 4'd1,  //起始位
            W_SLAVE_ADDR  = 4'd2,  //写7位从设备地址+写命令0
            ACK1          = 4'd3,  //应答1
            W_BYTE_ADDR   = 4'd4,  //写8位字地址
            ACK2          = 4'd5,  //应答2(状态转移时进行读写判断)
            STOP          = 4'd6,  //停止位
            //--------------------------------------------写操作特殊状态
            W_DATA        = 4'd7,  //写8位数据位
            W_ACK3        = 4'd8,  //写应答3           
            //--------------------------------------------读操作特殊状态
            START2        = 4'd9,  //读操作第2次起始位                         
            R_SLAVE_ADDR  = 4'd10, //写7位从设备地址+读命令1 
            R_ACK3        = 4'd11, //读应答3 
            R_DATA        = 4'd12, //读8位数据位        
            N_ACK         = 4'd13; //无应答

//计数器及参数
reg clk_div;
reg [7:0] cnt_clk; //分频计数
reg [3:0] cnt_bit; //位计数器
localparam  cnt_max_400khz = 8'd125; //400khz分频翻转计算值
wire scl_half_1;
wire scl_half_0;
wire scl_ack_jump;
assign scl_half_1  = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b1);     //scl高电平中点(起始位、ACK接收、读数据、停止位时刻)
assign scl_half_0  = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b0);     //scl低电平中点(写数据时刻)
assign scl_ack_jump=((cnt_clk ==(cnt_max_400khz >> 1)-5) && clk_div==1'b0); //scl低电平中点前5clk周期---
//---(ACK状态的下一状态跳转时刻,因为跳转都是由输入转输出状态,快一周期让输出状态赶上紧跟着的第一个scl_half_0,避免错过第1位数据)

//数据寄存器
reg [7:0] w_data_buf;       //写入数据寄存器
reg [7:0] r_data_buf;       //读出数据寄存器
reg [7:0] w_slave_addr_buf; //从设备地址寄存器(地址存高7位,0位为写命令0)
reg [7:0] r_slave_addr_buf; //从设备地址寄存器(地址存高7位,0位为读命令1)
reg [7:0] byte_addr_buf;    //字地址寄存器
reg ack_buf;                //接收应答位寄存器
reg work_en;                //工作使能信号

//*************************************** MAIN CODE ***************************************//
//数据复位、开始工作时寄存数据(避免传输中途数据不稳定)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        w_slave_addr_buf <= 8'b0000_0000;//0位为写命令0
        r_slave_addr_buf <= 8'b0000_0001;//0位为读命令1
        byte_addr_buf    <= 8'b0;
        w_data_buf       <= 8'b0;
    end else if (work_start) begin
        w_slave_addr_buf [7:1] <= slave_addr; //地址存高7位
        r_slave_addr_buf [7:1] <= slave_addr; //地址存高7位
        w_data_buf       <= w_data;
        byte_addr_buf    <= byte_addr;
    end
end

//分频计数器(400khz时钟scl)
always @(posedge clk or negedge rst_n) begin
    if (!work_en || !rst_n) begin
        cnt_clk <= 8'd1;
        clk_div <= 1'b1;
	end else if (cnt_clk == cnt_max_400khz) begin
        cnt_clk <= 8'd1;
        clk_div <= ~clk_div;
    end else 
        cnt_clk <= cnt_clk + 8'd1;
end
assign scl = clk_div;

//两段式状态机
always @(posedge clk or negedge rst_n) begin //state状态转移
    if (!rst_n) 
        state <= IDLE;
    else 
        state <= next_state;
end
always @(posedge clk or negedge rst_n) begin //state状态执行操作
    if (!rst_n) begin
        sda_oe     <= 1'b0;//sda默认不使能输出(高阻态经上拉为1)
        sda_out    <= 1'b1;//sda默认输出1避免输出0
        work_en    <= 1'b0;
        work_done  <= 1'b0;
        cnt_bit    <= 4'd0;
        next_state <= IDLE;
    end else
        case(state)
            //---------------------空闲----------------------//
            IDLE: begin
                sda_oe    <= 1'b0;
                sda_out   <= 1'b1; 
                work_done <= 1'b0; 
                if (work_start) begin //开始工作
                  work_en    <= 1'b1; //工作使能信号work_en(工作时持续为1)
                  next_state <= START;
                end
            end 
            //--------------------起始位1--------------------//
            START: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_1) begin
                    sda_out <= 1'b0;//sda输出起始位0
                    next_state <= W_SLAVE_ADDR;
                end
            end
            //--------------7bit从地址+写命令0---------------//
            W_SLAVE_ADDR: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= w_slave_addr_buf[7-cnt_bit];//sda输出设备地址(从高到低)
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        next_state <= ACK1;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            //--------------------应答1---------------------//
            ACK1: begin 
                sda_oe <= 1'b0;//sda输出失能作输入
                if (scl_half_1) 
                    ack_buf <= sda_in;
                if (scl_ack_jump) 
					if (ack_buf==1'b0) //应答位判断
                        next_state <= W_BYTE_ADDR;
                    else begin
                        work_en    <= 1'b0;
                        next_state <= IDLE;
                    end 
			end
            //-----------------8bit字节地址-----------------//
            W_BYTE_ADDR: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= byte_addr_buf[7-cnt_bit];//sda输出字节地址(从高到低)
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        next_state <= ACK2;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            //--------------------应答2---------------------//
            ACK2: begin
                sda_oe <= 1'b0;//sda输出失能作输入
                if (scl_half_1) 
                    ack_buf <= sda_in; 
                if (scl_ack_jump) 
					if (ack_buf==1'b0) //应答位判断
                        next_state = rw_ctrl ? START2 : W_DATA; //读写操作判断(0写1读)
                    else begin
                        work_en    <= 1'b0;
                        next_state <= IDLE;
                    end 
            end 
            //--------------------停止位--------------------//
            STOP: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_1) begin
                    sda_out    <= 1'b1;
                    work_done  <= 1'b1;//工作结束信号置1(在STOP转IDLE时清0)
                    work_en    <= 1'b0;//工作使能信号置0
                    next_state <= IDLE;
                end
            end
            //----------------写操作特殊状态-----------------//
            //-----------------8bit写入数据-----------------//
            W_DATA: begin               
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= w_data_buf[7-cnt_bit];//sda输出写入数据(从高到低)
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        next_state <= W_ACK3;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            //-------------------写应答3--------------------//
            W_ACK3: begin 
                sda_oe <= 1'b0;//sda输出失能作输入
                if (scl_half_1) 
                    ack_buf <= sda_in; 
                if (scl_ack_jump) 
					if (ack_buf==1'b0) begin //应答位判断
                        sda_out    <= 1'b0;  //停止位要输出0跳1,转状态时提前置0
                        next_state <= STOP;
                    end else begin
                        work_en    <= 1'b0;
                        next_state <= IDLE;
                    end 
            end 
            //----------------读操作特殊状态-----------------//
            //-------------------起始位2--------------------//
            START2: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_1) begin
                    sda_out    <= 1'b0;//sda输出起始位0
                    next_state <= R_SLAVE_ADDR;
                end
            end
            //--------------7bit从地址+读命令1---------------//
            R_SLAVE_ADDR: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= r_slave_addr_buf[7-cnt_bit];//sda输出设备地址(从高到低)
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        next_state <= R_ACK3;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            //-------------------读应答3--------------------//
            R_ACK3: begin 
                sda_oe <= 1'b0;//sda输出失能作输入
                if (scl_half_1) 
                    ack_buf <= sda_in; 
                if (scl_ack_jump) 
					if (ack_buf==1'b0) //应答位判断
                        next_state <= R_DATA;
                    else begin
                        work_en    <= 1'b0;
                        next_state <= IDLE;
                    end 
            end 
            //-----------------8bit读取数据-----------------//
            R_DATA: begin
                sda_oe <= 1'b0;//sda输出失能作输入
                if (scl_half_1 && cnt_bit!=4'd8) begin      
                    r_data_buf[7-cnt_bit] <= sda_in;//sda在scl高电平中点读取数据(从高到低)
                    cnt_bit <= cnt_bit + 4'd1;
                end 
                if (scl_ack_jump && cnt_bit==4'd8) begin //提前转状态,因为无应答会在scl_half_0输出          
                    next_state <= N_ACK;
                    cnt_bit <= 4'd0;
                    r_data <= r_data_buf;//从寄存器取出读取的数据
                end
            end
            //--------------------无应答--------------------//  
            N_ACK: begin 
                sda_oe  <= 1'b1;//sda输出使能
                if (scl_half_0)
                    sda_out <= 1'b1;
                if (scl_ack_jump) begin
                    sda_out <= 1'b0;//停止位要输出0跳1,转状态时提前置0
                    next_state <= STOP;
                end 
			end
            default: next_state <= IDLE;
        endcase 
end
endmodule

三、功能验证

        验证方法:

        i2c读写EEPROM的仿真有点麻烦,还需一个从设备模块以模拟EEPROM行为。因此我选择直接上板调试:用拨码开关进行读写控制、写入数据设置,按钮发送工作启动信号 (按钮经过软件消抖处理),进行一次写数据操作后再将进行读数据操作,并将数据返回到LED灯上进行验证,同时调用Vivado的ILA进行在线调试,抓取波形检查时序。(关于ILA的使用可以参考我的另一篇笔记:Vivado:使用 ILA 进行在线调试

        FPGA开发板上的操作及现象为:

        拨码开关设置好待写入数据 F0 (1111_0000),rw_ctrl的拨码开关打向0,按下按钮发送启动信号,向从设备写入数据。接着 rw_ctrl 的拨码开关打向1,再次按下按钮发送启动信号,从从设备读取数,此时LED被立刻点亮,点亮情况为 F0 (1111_0000),说明模块功能成功实现了对EEPROM的读写操作。

        下面对ILA抓取到的波形进行分析:

        (1)写数据

                向 AT24C02(设备地址7‘b1010000)的地址0F处 写入数据 F0。可以看到sda数据正确传输,同时从设备也正常进行接收应答。(应答位的毛刺个人认为是从设备的应答信号持续时间不够长导致,从设备发送应答后开始接收主设备发送的数据,sda保持高阻态,因此主设备这边接收到了高电平。但并不影响i2c功能,因为sda数据是在scl高电平中点进行接收)

        (2)读数据

                从 AT24C02(设备地址7‘b1010000)的地址0F处 读取数据。发现主设备成功读取到了之前向从设备写入的数据F0。

                到此,所设计i2c模块功能验证结束,成功实现对EEPROM进行读写操作的功能。

                

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值