文章摘要:
本文实现了类似于单片机的外设的I2C读写模块,将I2C各步骤独立出来,可以更清晰的描述I2C时序及工作原理,也使代码具有更发了好的通用性;

重点内容: 三段式状态机,双向IO,I2C时序;


硬件平台:EP4CE6F17C8
开发环境:Quartus II 13.1


时序图:

请输入图片描述

起始位:SCL高电平时,SDA下跳;
停止位:SCL高电平时,SDA上跳;
数据位:SCL低电平时,SDA可改变状态,SCL上跳时传输;
应答位: 0 - ACK, 1 - NAK(只有主机才允许发送NAK)


I2C模块

/*
 *  功能描述:I2C接口模块
 */
module i2c_comm(
    clk,        // 模块时钟
    rst_n,      // 模块复位(低电平有效)
    scl,        // 时钟引脚
    sda,        // 数据引脚(双向)
    
    start_req,  // 发送起始位
    wr_req,     // 发送请求
    rd_req,     // 接收请求
    stop_req,   // 发送停止位
    
    wdata,      // 待发数据
    rdata,      // 接收数据
    busy,       // 空闲(0)/繁忙(1)
    done        // 操作完成(脉冲信号)
);

input  clk;     // 模块时钟
input  rst_n;   // 模块复位(低电平有效)
output scl;     // 时钟引脚
inout  sda;     // 数据引脚(双向)

input start_req;    // 发送直始位
input wr_req;       // 发送数据
input rd_req;       // 读取数据
input stop_req;     // 发送停止位

input [7:0]wdata;   // 待发数据
output[7:0]rdata;   // 待发数据
output busy;        // 操作完成(空闲)
output reg done;
//-----------------------------------------
parameter I2C_COUNT = 20'd500;  // 100KHz = 10us

parameter S_IDLE       = 4'd0;
parameter S_START      = 4'd1;
parameter S_SEND_DATA  = 4'd2;
parameter S_RECV_DATA  = 4'd3;
parameter S_WAIT_ACK   = 4'd4;
parameter S_SEND_ACK   = 4'd5;
parameter S_STOP       = 4'd6;
//-----------------------------------------
reg [3:0] nstate;   // 后续状态
reg [3:0] cstate;   // 当前状态
//-----------------------------------------
reg isout;
reg scl_r;
reg sda_r;
reg [7:0]rdata_r;
reg [3:0] bits;
reg[19:0] i2c_cnt;
//-----------------------------------------
assign scl  = scl_r;
assign sda  = isout?sda_r:1'bz;  
assign busy = (cstate == S_IDLE)?1'b0:1'b1;
assign rdata = rdata_r;
//-----------------------------------------
// 传输计时逻辑
always @(posedge clk or negedge rst_n) begin
    if(~rst_n) begin
        i2c_cnt <= 0;
    end
    else if(cstate == S_IDLE) begin
        i2c_cnt <= 0;
    end
    else if(i2c_cnt == I2C_COUNT) begin
        i2c_cnt <= 0;
    end    
    else begin
        i2c_cnt <= i2c_cnt + 1'b1;
    end 
end

//-----------------------------------------
// 三段状态机第一段(标准代码)
always @(posedge clk or negedge rst_n) begin
    if(~rst_n) begin
        cstate <= S_IDLE;
    end
    else begin
        cstate <= nstate;
    end 
end
//-----------------------------------------
// 三段状态机第二段(状态转移)
always @(*) begin
    case(cstate)
        S_IDLE: begin
            if(start_req) begin
                nstate = S_START;       // 启动总线
            end    
            else if(stop_req) begin
                nstate = S_STOP;        // 停止总线
            end 
            else if(wr_req) begin
                nstate = S_SEND_DATA;   // 发送数据
            end            
            else if(rd_req) begin
                nstate = S_RECV_DATA;   // 发送数据
            end
            else begin
                nstate = S_IDLE;
            end
        end   
        
        // 发送起始位
        S_START: begin            
            if(i2c_cnt == I2C_COUNT) begin
                nstate  = S_SEND_DATA;  // 发送完起始位,发送设备地址
            end            
            else begin
                nstate  = S_START; 
            end
        end          
        
        // 发送数据
        S_SEND_DATA: begin
            if(bits == 4'd8) begin
                nstate  = S_WAIT_ACK;  // 发送完数据,接收ACK
            end            
            else begin
                nstate  = S_SEND_DATA; 
            end
        end
        // 等待设备响应
        S_WAIT_ACK: begin
            if(i2c_cnt == I2C_COUNT) begin
                nstate  = S_IDLE;      // 接收完ACK,转向空闲
            end            
            else begin
                nstate  = S_WAIT_ACK; 
            end
        end
        
        // 接收数据
        S_RECV_DATA: begin
            if(bits == 4'd8) begin
                nstate  = S_SEND_ACK;  // 接收完数据,发送ACK/NAK
            end            
            else begin
                nstate  = S_RECV_DATA; 
            end
        end 
        // 主机响应
        S_SEND_ACK: begin
            if(i2c_cnt == I2C_COUNT) begin
                nstate  = S_IDLE;      // 发送完ACK/NAK,转向空闲
            end            
            else begin
                nstate  = S_SEND_ACK; 
            end
        end
        
        // 发送停止位
        S_STOP: begin
            if(i2c_cnt == I2C_COUNT) begin
                nstate  = S_IDLE;      // 发送完停止位,转向空闲
            end            
            else begin
                nstate  = S_STOP; 
            end
        end 
        
        default: begin
            nstate = S_IDLE;
        end    
    endcase
end

//-----------------------------------------
// 三段状态机第三段(输出)
always @(posedge clk or negedge rst_n) begin

    if(~rst_n) begin
        isout <= 0;     // 设置双向端口为输入
        sda_r <= 1;     // 复位时,设置为高电平
        scl_r <= 1;     // 复位时,设置为高电平
    end 
    
    else begin
        case(cstate)
        S_IDLE: begin
            done <= 0; 
            bits <= 0; 
        end
        
        // 发送起始位:SCL为高时,SDA下跳
        // 前置状态:SCL为高,SDL为高(S_IDLE);
        // 后续状态:SCL为低,SDA为低(S_SEND_DATA);
        S_START: begin   
            if(i2c_cnt == 20'd100) begin
                scl_r <= 1'b1;      // SCL拉高
            end            
            else if(i2c_cnt == 20'd200) begin
                sda_r <= 1'b0;      // SDA下跳(产生起始位)
            end            
            else if(i2c_cnt == 20'd400) begin
                scl_r <= 1'b0;      // SCL拉低为次数据做准备     
            end                            
            else begin         
                isout <= 1'b1;      // 设置为输出   
            end   
        end        

        // 发送数据:SCL低电平准备,SCL上跳送出
        // 前置状态:SCL为低(重要)(S_START/S_WAIT_ACK);
        // 后续状态:SCL为低,SDAl输入(S_WAIT_ACK)
        S_SEND_DATA: begin           
            if(bits == 4'd8) begin
                isout <= 0;    // 设置为输入准备接收ACK
                sda_r <= 1;                        
            end            
            else if(i2c_cnt == 20'd100) begin
                isout <= 1'b1;              // 设置为输出  
                sda_r <= wdata[7 - bits];   // 低电平时准备SDA数据
            end        
            else if(i2c_cnt == 20'd200) begin
                scl_r <= 1'b1;              // SCL上跳发送数据 
            end            
            else if(i2c_cnt == 20'd400) begin
                scl_r <= 1'b0;              // SCL拉低准备下次发送   
            end   
            else if(i2c_cnt == I2C_COUNT) begin                 
                bits <= bits + 1'b1;        // 时序结束时才计数
            end 
        end 
        
        // 等待ACK: 由设备回应,设备在SCL拉低之后一段时间内已经准备好了数据;
        // 前置状态:SCL为低,SDA输入(S_SEND_DATA)
        // 后续状态:SCL为低,SDA输入
        S_WAIT_ACK: begin        
            if(i2c_cnt == 20'd200) begin
                isout <= 0;    // 设置为输入准备接收ACK
                scl_r <= 1'b1; // SCL上跳以接收ACK                
            end             
            if(i2c_cnt == 20'd400) begin
                scl_r <= 1'b0; // SCL拉低以准备下次发送 
                // 在此处理ACK
            end    
            else if(i2c_cnt == I2C_COUNT) begin 
                bits <= 0;
                done <= 1;
            end   
            else begin
                isout <= 1'b0;     // 设置为输入 
            end
        end       
        
        // 接收数据:SCL低电平准备 
        // 前置状态:SCL为低,SDA输入(S_WAIT_ACK)
        // 后续状态:SCL为低,SDA输入(S_SEND_ACK)
        S_RECV_DATA: begin           
            if(bits == 4'd8) begin 
                bits  <= 0;
            end        
            else if(i2c_cnt == 20'd100) begin
                scl_r <= 1'b0;                  // SCL先拉低,因为不确定之前的电平
            end
            else if(i2c_cnt == 20'd200) begin
                scl_r <= 1'b1;                  // SCL上跳接收数据 
            end            
            else if(i2c_cnt == 20'd300) begin
                rdata_r[7 - bits] <= sda;       // 数据稳定后读取数据
            end               
            else if(i2c_cnt == 20'd400) begin
                scl_r <= 1'b0;                  // SCL拉低准备下次发送  
            end  
            else if(i2c_cnt == I2C_COUNT) begin
                bits <= bits + 1'b1;
            end           
            else begin         
                isout <= 1'b0;                  // 设置为输入   
            end   
        end
        
        // 发送ACK: ACK(0)/NAK(1)
        // 前置状态:SCL为低,SDA输入,
        // 后续状态:SCL为高,SDA为低(STOP)
        S_SEND_ACK: begin        
            if(i2c_cnt == 20'd100) begin
                isout <= 1;                     // 设置为输出
                sda_r <= 1'b1;                  // SCL低电平设置ACK(0)/NAK(1)数据
            end 
            else if(i2c_cnt == 20'd200) begin                
                scl_r <= 1'b1;                  // SCL上跳发送
            end 
            else if(i2c_cnt == 20'd400) begin
                scl_r <= 1'b0;                 // SCL拉低以准备修改SDA    
            end         
            else if(i2c_cnt == I2C_COUNT) begin
                sda_r <= 0;                    // 恢复为低电平(准备上跳停止)
                done  <= 1;
            end 
            else begin
            
            end
        end         
            
        // 发送停止位: SCL为高电平时,SDA上跳
        // 前置状态:SCL为低,SDA不确定
        // 后续状态:SCL为高,SDA为高(准备下次起始位)
        S_STOP: begin          
            if(i2c_cnt == 20'd200) begin
                isout <= 1'b1;      // 输出
                sda_r <= 1'b0;      // 设置SDA输出低电平
            end
            else if(i2c_cnt == 20'd300) begin
                scl_r   <= 1'b1;    // SCL设置为高电平 
            end
            else if(i2c_cnt == 20'd400) begin
                sda_r   <= 1'b1;    // SDA上跳  
            end
            else if(i2c_cnt == I2C_COUNT) begin        
                done <= 1;
            end
            else begin         
                isout <= 1'b1; 
            end  
        end    
        default:  begin
            done <= 0;
        end                
        endcase            
    end        
end

endmodule

I2C模块接口图