FPGA:三大协议(IIC、UART、SPI)之IIC

摘要:1、本文讲述IIC的物理层面的结构(使用iic工作的物理层面的连接);2、本文讲解协议层面的通信交流格式(IIC时序);3、提供一个主机和从机的一个verilog代码;4、本文的主从机指的是:板子一号作为主机,发送数据给作为从机的板子二号;注意:在实际应用中,一般器件作为从机,我们写的程序作为主机通过数据线控制器件进行工作。

一、IIC物理结构

 

 二、IIC时序

1、前言:当两个器件要通过IIC协议来交流,已经在物理层面做好了准备,连接好了SDA和SCL两根线,也就是建立了一个交流通道。(比如已经拨通了电话,接下来就开始讲话了)。

2、常态:当建立好了联系,有了一个沟通的通道之后(就像拨通了电话),但是双方都没有交流(打了电话但是都不说话)。这个时候SDA和SCL的状态是高阻态,此时的高阻态不是人为设定的,是在硬件上有一个上拉电阻,让空闲时刻的SDA和SCL两根数据线为高阻态。

3、通信状态1(主机发送,从机接收):通道已经建立好了,开始交流(电话拨通开始说话了)。

(1)起始位(1bit):开始交流的时候发送一个起始位(相当于打电话说:喂,您好):

起始位构成:当SCL保持高电平期间(高阻态,主机不控制即可),SDA线拉低(主机控制SDA拉低),称为IIC的起始信号。

(2)数据位(8bit):起始位发送完毕之后开始发送数据: 数据的发送参照的是SCL时钟线(相当于给器件一个时钟),是由主机产生的(SCL产生可以是记录一段时间,SCL数据线高低电平翻转)。

数据位:变化是在SCL为低的时候SDA变化,SDA在SCL为高的时候保持不变;因为器件(从机)在SCL为高电平的时候读取SDA的数据(电平高低),来表示传输的数据。

数据线控制:SCL根据传输速率周期变化(主机控制),SDA在SCL为低的时候发送数据(比如数据是0001 1100,第一位发送0,则第一个SCL为低的时候拉低SDA,且在SCL为高的时候SDA保持不变,表示发送0),主机控制SDA。

(3)应答位(1bit):应答表示从机应答主机,给主机说一声,收到数据了。主机收到之后就知道发送的数据成功被接收到了。

数据位:SCL还是根据传输速率正常周期性变化(主机控制),SDA这个时候主机不能控制,SDA控制权交给从机;主机在SCL为高的时候接收SDA传来的数据,如果这一bit是低,那么表示正确应答(ACK,正确应答),从机正确收到数据了,如果为SDA高(NACK,错误应答)那么表示,从机收到数据错误了。

注:对于(2)、(3),可以多次进行,要发送多个Byte数据,那么可以先(1)—(2)—(3)—(2)—(3)、、、、

*(4)停止位:当发送完数据之后并且应答收到之后,主机发送一个停止位表示本次交流结束(就像打电话说拜拜)。

数据位:在SCL位高的时候,拉高SDA,表示停止位。

数据线控制:本来在SCL为高的时候SDA要保持不变表示数据发送,因此:把SDA由低拉高表示停止信号。

注:当有多Byte数据要发送的时候,那么可以先(1)—(2)—(3)—(2)—(3)、、、—(4)。

注:有一种情况:当主机收到的(3)---应答位是高电平(SDA是高,NACK),表示错位应答,那么这个时候不管发完数据没有都要发送一个停止信号来结束这次通话。(就像两个特工交流,暗号没对上,那么就要终止交易)。

时序图:

 流程图:

 4、通信状态2(从机发送,主机接收)

(1)起始位(1bit):主机发起通信,告诉可以开始了

起始位构成:当SCL保持高电平期间(高阻态,主机不控制即可),SDA线拉低(主机控制SDA拉低),称为IIC的起始信号。

(2)数据位(8bit):起始位发送完毕之后开始接收数据: 数据的接收参照的是SCL时钟线(相当于给器件一个时钟),是由主机产生的(SCL产生可以是记录一段时间,SCL数据线高低电平翻转)。

数据位:主机在SCL为高的时候识别SDA,SDA的高低认定是数据这一bit的高低,最后接收8位表示这一Byte数据。

数据线控制:SCL根据传输速率周期变化(主机是控制SCL线的),主机放开SDA线给从机操作,并且主机在SCL线为高的时候识别SDA。

(3)应答位(1bit):应答表示主机应答从机,给从机说一声,表示主机接受到从机的数据了。

数据线控制:SCL照旧周期变化(主机控制),在SCL为低的时候SDA变化,应答从机。这个时候有2种情况的应答:第一种:数据接收正确,且后面主机还要接收数据,回复正确应答:SDA拉低(ACK),从机接收到之后继续发送数据;第二种:数据接收正确,但是后面主机不再接收数据了,回复错误应答,SDA拉高(NACK)。表示从机不再发送数据了。这个时候主机再发送停止位表示交流结束。

*(4)停止位当发送完数据之后并且应答收到之后,主机发送一个停止位表示本次交流结束(就像打电话说拜拜)。

数据位:在SCL位高的时候,拉高SDA,表示停止位。

数据线控制:本来在SCL为高的时候SDA要保持不变表示数据发送,因此:把SDA由低拉高表示停止信号。

时序图:

流程图:

思路总结:

(1)我们可以使用一个计数器来控制输出SCL,计满一定数额就让SCL实现翻转,这样就产生了SCL时钟。

(2)可以用一个计数器来记录发送或者接收的数据位数,来确定我们接收或者发送的数据位数是否正确。

 主机verilog代码:

module iic_master(
    input		clk,
    input		rst_n,

    input		start,           //发起数据请求
/*     input[7:0]	data_tx,         //要发送的数据 ,同TX_DATA*/

    output	reg	iic_scl,          //发送的时钟总线scl,根据选择的传输速率实现不同的scl的输出。

    //inout     iic_sda,          
    // assign iic_sda = sda_oe ? sda_out : 1'bz;
    //assign sda_in = iic_sda;                              
    input		sda_in,          //发送的总线sda
    output reg  sda_oe,          //
    output reg  sda_out
);

//parameter declarations
parameter SCL_MAX = 9'd500; // 50M/10k
localparam TX_DATA = 24'h112345;//发送三个字节

//状态
localparam 
    IDEL  = 4'b0001,
    START = 4'b0010,
    SEND_DA = 4'b0100,
    STOP = 4'b1000;

reg             work_en;        //数据开始传输使能位

reg [7:0]       data_tmp;
reg             ack;


reg     [8:0]   cnt_scl;        //生成scl,250次翻转一下
wire			add_cnt_scl ;       //Counter Enable
wire			end_cnt_scl ;       //Counter Reset 

reg     [3:0]   cnt_bit;        //表明哪一个位发送
wire			add_cnt_bit ;       //Counter Enable
wire			end_cnt_bit ;       //Counter Reset 

reg		[1:00]	cnt_byte		; //Counter   
wire			add_cnt_byte ; //Counter Enable
wire			end_cnt_byte ; //Counter Reset 

reg     [3:0]   state_c,state_n;//状态
reg    [8:0]    max_scl_cnt;

//状态跳转条件定义
wire        idle2start;
wire        start2send_da;
wire        send_da2stop;
wire        stop2idle;

//使能位赋值操作
    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin  
            work_en <= 1'b0; 
        end  
        else if(start)begin
            work_en <= 1'b1;
        end
        else if(stop2idle)begin
            work_en <= 1'b0;
        end
        else begin
            work_en <= work_en;
        end
    end

//计数器1  cnt_scl
    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin  
            cnt_scl <= 9'd0; 
        end  
        else if(add_cnt_scl)begin  
            if(end_cnt_scl)begin  
                cnt_scl <= 9'd0; 
            end  
            else begin  
                cnt_scl <= cnt_scl + 1; 
            end  
        end  
        else begin  
            cnt_scl <= cnt_scl;  
        end  
    end 

assign add_cnt_scl = work_en;                                  //随时开始计数
assign end_cnt_scl = add_cnt_scl && cnt_scl >= max_scl_cnt;     //一个周期结束一次计数,当进入发送停止位的时候也要清零

always@(*)begin//变换是组合逻辑,方便试试更新,不会出现说延时错误
    case(state_c)
        IDEL,START,STOP :   max_scl_cnt = SCL_MAX >> 1;
        SEND_DA         :   max_scl_cnt = SCL_MAX;
        default         :   max_scl_cnt = SCL_MAX;
    endcase
end
//计数器2,cnt_bit      
    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin  
            cnt_bit <= 4'd0; 
        end  
        else if(add_cnt_bit)begin  
            if(end_cnt_bit)begin  
                cnt_bit <= 4'd0; 
            end  
            else begin  
                cnt_bit <= cnt_bit + 1'b1; 
            end  
        end  
        else begin  
            cnt_bit <= cnt_bit;  
        end  
    end 
            
    assign add_cnt_bit = (state_c == SEND_DA) && end_cnt_scl; //开始位计数是在进入数据发送的时候,并且是在结束计数一个周期的时候进行计数位加一
    assign end_cnt_bit = add_cnt_bit && cnt_bit >= 4'd8 ; 

//状态机第一段,描述状态输出
    always @(posedge clk or negedge rst_n)begin//要和第三段相同,不然的话flag_cak不能归零,cnt_us也不能
        if(!rst_n)begin
            state_c <= IDEL;
        end
        else begin
            state_c <= state_n;
        end
    end
//状态机第二段,描述状态转换,对n_state操作
    always @(*)begin
        case(state_c)
            IDEL: begin
                if(idle2start)begin
                    state_n = START;
                end
                else begin
                    state_n = IDEL;
                end
            end
            START :begin
                if(start2send_da)begin
                    state_n = SEND_DA;
                end
                else begin
                    state_n = START;
                end
            end
            SEND_DA :begin
                if(send_da2stop)begin
                    state_n = STOP;
                end
                else begin
                    state_n = SEND_DA;
                end
            end
            STOP :begin
                if(stop2idle)begin
                    state_n = IDEL;
                end
                else begin
                    state_n = STOP;
                end
            end
            default :state_n = IDEL;
        endcase
    end

    assign idle2start       =       state_c == IDEL     &&   start; //跳出IDLE状态条件:在start来临
    assign start2send_da    =       state_c == START    &&   end_cnt_scl;//跳出START状态条件:起始位发送完成
    assign send_da2stop     =       state_c == SEND_DA  &&   (end_cnt_byte || (ack && end_cnt_bit));//跳出发数据状态的条件:发送完三个字节之后,或者是异常之后,回复一个高电平
    assign stop2idle        =       state_c == STOP     &&   end_cnt_scl;
    
//第三段,定义状态机输出情况,可以时序逻辑,也可以组合逻辑
	always @(posedge clk or negedge rst_n)begin  
		if(!rst_n)begin  
            data_tmp <= 8'd0;
            iic_scl <= 1'b1;
			sda_oe <= 1'b0;
            sda_out <= 1'b1;
            ack <= 1'b0;
		end 
		else begin  
			case(state_c)
                IDEL : begin
                    iic_scl <= 1'b1;
                    sda_oe <= 1'b1;//空闲状态                 //在等待状态要释放总线,但是此时时钟总线是一直跑的
                    sda_out <= 1'b1;
                    ack <= 1'b0;
                end 

                START :begin//半个周期
                    sda_oe <= 1'b1;
                    sda_out <= 1'b0;//进来就拉低
                    if(cnt_scl >= (max_scl_cnt >> 1))begin
                        iic_scl <= 1'b0;
                    end
                    else begin
                        iic_scl <= 1'b1;
                    end
                end

                SEND_DA : begin
                    case(cnt_byte)
                        0:data_tmp <= TX_DATA[7:0];
                        1:data_tmp <= TX_DATA[15:8];
                        2:data_tmp <= TX_DATA[23:16];
                        default: data_tmp <= 8'd0;
                    endcase
                    
                    if(cnt_scl > (max_scl_cnt >> 2 ) && (cnt_scl < ((max_scl_cnt >>2 ) + (max_scl_cnt >>1 ))))begin//高电平持续时间
                            iic_scl <= 1'b1;
                    end
                    else begin
                            iic_scl <= 1'b0;
                    end

                    if(cnt_bit != 4'd8)begin
                        sda_oe <= 1'b1;
                        sda_out <= data_tmp[cnt_bit];
                    end
                    else begin//接收应答位
                        sda_oe <= 1'b0;
                        if(cnt_bit == 4'd8 && cnt_scl == max_scl_cnt >> 1)begin
                            if(sda_in)begin//当异常的时候ack保存当前输入的值
                                ack <= sda_in;
                            end
                            else begin
                                ack <= ack;
                            end
                        end
                        else ;
                    end

                end
                STOP : begin
                    sda_oe <= 1'b1;//进入就是总线控制
                    if(end_cnt_scl)begin//结束可以修改结束时间
                        sda_out <= 1'b1;
                    end
                    else begin
                        sda_out <= 1'b0;
                    end

                    if(cnt_scl >= (max_scl_cnt >> 1))begin
                        iic_scl <= 1'b1;
                    end
                    else begin
                        iic_scl <= 1'b0;
                    end
                end
                default :;
            endcase

		end  
	end //always end

//字节计数器
    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin  
            cnt_byte <= 2'd0; 
        end  
        else if(add_cnt_byte)begin  
            if(end_cnt_byte)begin  
                cnt_byte <= 2'd0; 
            end  
            else begin  
                cnt_byte <= cnt_byte + 1'b1; 
            end  
        end  
        else begin  
            cnt_byte <= cnt_byte;  
        end  
    end    

    assign add_cnt_byte = end_cnt_bit; //开始计数是在发送数据状态,实现加一是位记完9位
    assign end_cnt_byte = add_cnt_byte && (cnt_byte >= 2'd2 || send_da2stop) ; //当异常输入的时候也要对计数位清零


endmodule

从机verilog代码:

module iic_slave(
    input		clk,
    input		rst_n,
    input		iic_scl,

    input		sda_in,         //sda总线信号输入
    output  reg  sda_oe,
    output  reg  sda_out,		    //sda接收端发送
    output  reg [23:0]   data_out        //接收到的数据全部整合
);

parameter SCL_MAX = 9'd500; // 50M/10k
localparam	IDLE	=	4'b0001, 
            SLAVE	=	4'b0010,
            ACK		=	4'b0100,
            JUDG	=	4'b1000;
            
reg		[3:00]	state_c, state_n; //
reg             flag_judg2idle;

//表示最大计数
reg    [8:0]    max_scl_cnt;
reg             work_en;//开始使能

//接收到的信号
reg [7:0]       data_tmp;

/* //计数器
reg     [8:0]   cnt_scl;        //生成scl,250次翻转一下
wire			add_cnt_scl ;       //Counter Enable
wire			end_cnt_scl ;       //Counter Reset  */

reg     [3:0]   cnt_bit;        //表明哪一个位发送
wire			add_cnt_bit ;       //Counter Enable
wire			end_cnt_bit ;       //Counter Reset 

reg		[1:00]	cnt_byte		; //Counter   
wire			add_cnt_byte ; //Counter Enable
wire			end_cnt_byte ; //Counter Reset 
        
//跳转条件定义
wire			idle2slave;
wire			slave2ack;
wire			ack2judg;
wire			judj2idle;
wire            judj2slave;

//检测开始标志
//对信号打拍
reg             sda_start_r1;
reg             sda_start_r2;
wire            flag_start;

always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        sda_start_r1 <= 1'b0;
        sda_start_r2 <= 1'b0;
    end
    else begin
        sda_start_r1 <= sda_in;
        sda_start_r2 <= sda_start_r1;
    end
end
assign flag_start = ((~sda_start_r1 && sda_start_r2) && iic_scl) ? 1'b1 : 1'b0;//检测开始信号,检测下降沿

//检测结束标志
reg             sda_stop_r1;
reg             sda_stop_r2;
wire            flag_stop;
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        sda_stop_r1 <= 1'b0;
        sda_stop_r2 <= 1'b0;
    end
    else begin
        sda_stop_r1 <= sda_in;
        sda_stop_r2 <= sda_stop_r1;
    end
end
assign flag_stop = ((sda_stop_r1 && ~sda_stop_r2) && iic_scl) ? 1'b1 : 1'b0;//检测停止信号,检测上升沿

//检测iic_scl的上升沿
reg             scl_r1;
reg             scl_r2;
wire            flag_scl_pos;
wire            flag_scl_neg;
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        scl_r1 <= 1'b0;
        scl_r2 <= 1'b0;
    end
    else begin
        scl_r1 <= iic_scl;
        scl_r2 <= scl_r1;
    end
end
assign flag_scl_pos = ((scl_r1 && ~scl_r2) && iic_scl) ? 1'b1 : 1'b0;//检测停止信号,检测上升沿
assign flag_scl_neg = ((~scl_r1 && scl_r2) && (iic_scl == 1'b0)) ? 1'b1 : 1'b0;//检测停止信号,检测下降沿


//使能赋值
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin  
        work_en <= 1'b0; 
    end  
    else if(flag_start)begin
        work_en <= 1'b1;
    end
    else if(judj2idle)begin//当判断是结束之后回到idle状态,表示使能结束
        work_en <= 1'b0;
    end
    else begin
        work_en <= work_en;
    end
end

//计数器
always@(*)begin//变换是组合逻辑,方便试试更新,不会出现说延时错误
    case(state_c)
        IDLE                 :   max_scl_cnt = SCL_MAX >> 1;
        SLAVE,ACK,JUDG      :   max_scl_cnt = SCL_MAX;
        default             :   max_scl_cnt = SCL_MAX;
    endcase
end
/* //计数器1  cnt_scl
always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin  
            cnt_scl <= 9'd0; 
        end  
        else if(add_cnt_scl)begin  
            if(end_cnt_scl)begin  
                cnt_scl <= 9'd0; 
            end  
            else begin  
                cnt_scl <= cnt_scl + 1; 
            end  
        end  
        else begin  
            cnt_scl <= 9'd0;  //回到idle自动归零
        end  
    end 
assign add_cnt_scl = work_en;                                  //当开始位来临的时候开始计数
assign end_cnt_scl = (add_cnt_scl && cnt_scl >= max_scl_cnt) || judj2idle;     //一个周期结束一次计数,当进入发送停止位的时候也要清零 */

//计数器2,cnt_bit      
    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin  
            cnt_bit <= 4'd0; 
        end  
        else if(add_cnt_bit)begin  
            if(end_cnt_bit)begin  
                cnt_bit <= 4'd0; 
            end  
            else begin  
                cnt_bit <= cnt_bit + 1'b1; 
            end  
        end  
        else begin  
            cnt_bit <= cnt_bit;  
        end  
    end 
            
    assign add_cnt_bit = (state_c == SLAVE || state_c == ACK || state_c == JUDG) && flag_scl_neg; //开始位计数是在进入数据
    assign end_cnt_bit = (add_cnt_bit && cnt_bit >= 4'd8) || judj2idle  ; //停止位来领直接归零

//第一段设置状态转移空间
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        state_c <= IDLE;
    end
    else begin
        state_c <= state_n;
    end
end //always end

//第二段、组合逻辑定义状态转移
always@(*)begin
    case(state_c)
        IDLE:begin
            if(idle2slave)begin
                state_n = SLAVE;
            end
            else begin
                state_n = state_c;
            end
        end
        SLAVE:begin
            if(slave2ack)begin
                state_n = ACK;
            end 
            else begin
                state_n = state_c;	
            end 
        end
        ACK:begin
            if(ack2judg)begin
                state_n = JUDG;
            end 	
            else begin
                state_n = state_c;	
            end 
        end 
        JUDG:begin
            if(judj2idle)begin
                state_n = IDLE;
            end 
            else if(judj2slave)begin
                state_n = SLAVE;
            end
            else begin
                state_n = state_c;	
            end 
        end 
        default: begin
            state_n = IDLE;
        end
    endcase
end //always end
        
assign	idle2slave	=	state_c ==  IDLE	&& 	(flag_scl_neg && iic_scl == 1'b0);
assign	slave2ack   =	state_c	==	SLAVE	&&	(cnt_bit == 4'd7 && flag_scl_neg);//8位数据位接收完毕,但是还是要留一位给应答位
assign	ack2judg	=	state_c	==	ACK 	&&	(cnt_bit == 4'd8 && flag_scl_neg);
assign	judj2idle	=	state_c	==	JUDG	&&	flag_judg2idle;
assign	judj2slave	=	state_c	==	JUDG	&&	(cnt_bit == 4'd0 && flag_scl_neg) ;//接收完一个字节数据回到salve状态
        
//第三段,定义状态机输出情况,可以时序逻辑,也可以组合逻辑
always @(posedge clk or negedge rst_n)begin  
    if(!rst_n)begin  
        sda_out <= 1'b0;
        sda_oe <= 1'd0;
        data_tmp <= 8'd0;
        data_out <= 24'd0;
        flag_judg2idle <= 1'b0;
    end  
    else begin  
        case (state_c)
            IDLE:begin
                flag_judg2idle <= 1'b0;
            end
            SLAVE :begin
                sda_oe <= 1'd0;
                if(flag_scl_pos && iic_scl)begin
                    data_tmp[cnt_bit] <= sda_in;
                end
                else ;
            end
            ACK :begin//持续一个bit的低电平输出应答
                case(cnt_byte)
                    2'b00:begin
                            data_out[7 : 0]   <= data_tmp;
                            sda_out <= 1'd0;
                            sda_oe <= 1'd1;
                        end
                    2'b01:begin
                            data_out[15:8]    <= data_tmp;
                            sda_out <= 1'd0;
                            sda_oe <= 1'd1;
                        end
                    2'b10:begin
                            data_out[23:16]   <= data_tmp;
                            sda_out <= 1'd1;
                            sda_oe <= 1'd1;
                        end
                    default :data_tmp <= 24'd0;
                endcase
            end
            JUDG:begin//检测下一个位是不是停止位
                sda_oe <= 1'd0;
                if(iic_scl == 1'b1 && flag_stop)begin//如果是停止位就结束,回到idle状态
                    flag_judg2idle <= 1'b1;
                end
                else if(flag_scl_pos && iic_scl)begin//在下一个iic_scl周期进行检测,看是否是停止位,先保存当前数据
                    data_tmp[cnt_bit] <= sda_in;
                end
                else ;
            end
            default :begin
                sda_out <= 1'd1;
                sda_oe <= 1'd0;
            end
        endcase
    end
end //always end

//字节计数器
    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin  
            cnt_byte <= 2'd0; 
        end  
        else if(add_cnt_byte)begin  
            if(end_cnt_byte)begin  
                cnt_byte <= 2'd0; 
            end  
            else begin  
                cnt_byte <= cnt_byte + 1'b1; 
            end  
        end  
        else begin  
            cnt_byte <= cnt_byte;  
        end  
    end    

    assign add_cnt_byte = end_cnt_bit; //开始计数是在发送数据状态,实现加一是位记完9位
    assign end_cnt_byte = (add_cnt_byte && cnt_byte >= 2'd2) ||  judj2idle ; //停止位来领直接归零


endmodule

  • 18
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值