摘要: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代码:文章来源:https://www.toymoban.com/news/detail-736975.html
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代码:文章来源地址https://www.toymoban.com/news/detail-736975.html
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
到了这里,关于FPGA:三大协议(IIC、UART、SPI)之IIC的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!