通信协议篇——I2C通信
1.简介
I2C(Inter-Integrated Circuit)是一种串行通信总线,总线上可以挂多个设备,可实现同步半双工通信。
2.原理
通信方式
I2C通信属于串行通信,使用串行数据线SDA和串行时钟线SCL两线实现同步半双工通信。
同步 | 接收端时钟频率和发送端时钟频率一致 |
---|---|
异步 | 接收端时钟频率和发送端时钟频率不一致 |
同步通信和异步通信的区别:
- 异步通信中的接收方并不知道数据什么时候会到达,收发双方可以有各自自己的时钟。发送方发送的时间间隔可以不均,接收方是在数据的起始位和停止位的帮助下实现信息同步的。这种传输通常是很小的分组,比如一个字符为一组,为这个组配备起始位和结束位。所以这种传输方式的效率是比较低的,毕竟额外加入了很多的辅助位作为负载,常用在低速的传输中。
- 同步通信中双方使用频率一致的时钟 ,它的分组相比异步通信则大得多,称为一个数据帧,通过独特的bit串作为启停标识。发送方要以固定的节奏去发送数据,而接收方要时刻做好接收数据的准备,识别到前导码后马上要开始接收数据了。同步这种方式中因为分组很大,很长一段数据才会有额外的辅助位负载,所以效率更高,更加适合对速度要求高的传输,当然这种通信对时序的要求也更高。
单工 | 在任何时间,数据只能单向传输 |
---|---|
半双工 | 能够双向通信,但通信双方不能同时进行数据收发,在同一时刻只有一方发送另一方接收 |
全双工 | 能够双向通信,且通信双方能够同时进行数据收发,两者同步进行 |
I2C通信中,主机通过时钟线SCL发送时钟信号,通过数据线SDA发送数据(包括从机地址、指令、数据包等),在发送完一帧数据后,需要等待从机的响应,才能继续发送下一帧数据,因此I2C属于同步通信。
I2C通信中,数据在一根数据线SDA上传输,同一时刻数据传输的方向只能是单向的,从A到B或者从B到A;通过切换传输方向从而实现双向通信,因此I2C属于半双工通信。
数据格式
I2C通信的数据包大小为8bit,主要有三类——指令、字节地址、数据。数据传输时,按照高位在前,低位在后的顺序(即MSB First,LSB Last)。
类型 | 格式 |
---|---|
指令 | 7位从机地址+1位读写命令(写0,读1) |
字节地址 | 8位字节地址,从这个地址开始读写数据 |
数据 | 8位数据 |
I2C通信通过时钟线SCL和数据线SDA确定几种通信状态——空闲状态、启动信号、停止信号、数据位传输、应答信号。
空闲状态
当I2C总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
启动信号
在时钟线SCL保持高电平期间,数据线SDA上的下降沿,定义为I2C总线的启动信号,它标志着一次数据传输的开始。启动信号是由主机建立的,在建立该信号之前,I2C总线必须处于空闲状态。
停止信号
在时钟线SCL保持高电平期间,数据线SDA上的上升沿,定义为I2C总线的停止信号,它标志着一次数据传输的终止。停止信号是由主机建立的,建立该信号之后,I2C总线将返回空闲状态。
数据位传输
在I2C通信中,时钟线SCL上的每一个时钟,同步对应着数据线SDA上的一位数据。即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。进行数据传送时,在SCL是高电平期间,SDA上的电平必须保持稳定,低电平为数据0,高电平为数据1。只有在SCL为低电平期间,才允许SDA上的电平改变状态。
应答信号
I2C总线上的所有数据都是以8bit字节传输的,发送器每发送一个字节,就在第9个时钟开始时释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。
如果接收器是主控器,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号。
操作时序
I2C设备的操作时序有四种,分别为写单个存储字节,写多个存储字节,读单个存储字节和读多个存储字节。操作时序如下图:
具体通信过程
以写单个存储字节这一操作为例,介绍I2C通信的具体流程:
初始状态:SCL、SDA都为高电平,总线处于空闲状态;
→启动信号:在SCL为高电平时,SDA由高变低,产生下降沿,此时I2C通信开始启动;
→发送7位从机地址和1位读写指令:按位传输,按照高位在前、低位在后的顺序,且遵循SCL高电平时SDA上的数据保持不变,SCL低电平时SDA上的数据发生改变的原则,每个时钟脉冲发送一位地址数据;
→接收响应:I2C通信中,每发送完8bit数据,会接收1bit响应;此时,主机先把数据线SDA释放,然后在第9个时钟脉冲的高电平期间读取SDA上的应答信号,0代表ACK信号,1代表NACK信号;只有接收到ACK信号,才继续之后的操作,否则重新开始通信过程;
→发送8位字节地址:同上;
→接收响应:同上;
→写入8位数据:同上;
→接收响应:同上;
→停止信号:在SCL为高电平时,SDA由低变高,产生上升沿,此时I2C通信结束。
其他三种操作的具体流程是类似的。
标准接口
name | description | direction | length |
---|---|---|---|
clk | 系统时钟 | input | 1 |
rst | 复位信号 | input | 1 |
scl | I2C串行时钟线 | output | 1 |
sda | I2C串行数据线 | inout | 1 |
rd_sig | I2C读命令 | input | 1 |
wr_sig | I2C写命令 | input | 1 |
rd_data | I2C读取的数据 | output | 8 |
wr_data | I2C写入的数据 | input | 8 |
addr | I2C读/写的开始字节地址 | input | 8 |
done_sig | I2C读/写操作完成信号 | output | 1 |
3.程序实现
通过EEPROM的读写操作,验证I2C通信的程序实现。
RTL视图
I2C控制模块
`timescale 1ns/1ps
//Module Name : iic_control
//Description : read and write eeprom using iic bus
//Editor : Yongxiang
//Time : 2019-11-25
module iic_control
(
input wire clk_50M,
input wire rst_n,
output reg wr_sig,
output reg rd_sig,
output reg[7:0] addr_sig,
output reg[7:0] wr_data,
input wire done_sig
);
reg[1:0] state;
//eeprom先写后读
always @(posedge clk_50M)
begin
if(!rst_n)begin
state <= 2'd0;
addr_sig <= 8'd0;
wr_data <= 8'd0;
rd_sig <= 1'b0;
wr_sig <= 1'b0;
end
else begin
case(state)
2'd0:begin
if(done_sig)begin
wr_sig <= 1'b0;
rd_sig <= 1'b0;
state <= 2'd1;
end
else begin
wr_sig <= 1'b1;
rd_sig <= 1'b0;
wr_data <= 8'hff; //写入数据0Xff
addr_sig <= 8'd0; //在eeprom的0X00地址写入数据
end
end
2'd1:begin
if(done_sig)begin
wr_sig <= 1'b0;
rd_sig <= 1'b0;
state <= 2'd2;
end
else begin
wr_sig <= 1'b0;
rd_sig <= 1'b1;
addr_sig <= 8'd0; //在eeprom的0X00地址写入数据
end
end
2'd2:begin
state <= 2'd2;
end
endcase
end
end
endmodule
I2C通信模块
`timescale 1ns/1ps
//module name : iic
//description : iic communication module
//Editor : Yongxiang
//Time : 2019-11-25
module iic
(
input wire clk_50M,
input wire rst_n,
input wire wr_sig, //写命令,1有效
input wire rd_sig, //读命令,1有效
input wire[7:0] addr_sig, //数据地址
input wire[7:0] wr_data, //写数据
output reg[7:0] rd_data, //读数据
output reg done_sig, //读写完成标志,1有效
output reg scl,
inout wire sda
);
reg[4:0] state;
reg[4:0] state_save;
reg[8:0] cnt;
reg[7:0] data_reg;
reg is_out;
reg sda_reg;
reg is_ask_n; //应答信号,0有效
assign sda = is_out ? sda_reg : 1'bz; //SDA输入输出方向控制
//IIC读写数据
always @(posedge clk_50M)
begin
if(!rst_n)begin //系统复位
state <= 5'd0;
cnt <= 9'd0;
sda_reg <= 1'b1; //SDA置高
scl <= 1'b1; //SCL置高
is_out <= 1'b1;
is_ask_n <= 1'b1;
rd_data <= 8'd0;
done_sig <= 1'b0;
end
else if(wr_sig)begin //iic数据写
case(state)
5'd0:begin //iic启动
is_out <= 1'b1; //SDA输出
if(cnt == 9'd0)begin
scl <= 1'b1;
sda_reg <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd100)begin
sda_reg <= 1'b0; //启动信号:在SCL为1时,SDA的下降沿
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd200)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd249)begin
cnt <= 9'd0;
state <= 5'd1;
end
else begin
cnt <= cnt + 9'd1;
end
end
5'd1:begin //发送7位从机地址、1位写命令
data_reg <= 8'hA0;
state <= 5'd7;
state_save <= 5'd2;
end
5'd2:begin //发送数据写入地址
data_reg <= addr_sig;
state <= 5'd7;
state_save <= 5'd3;
end
5'd3:begin //写入数据
data_reg <= wr_data;
state <= 5'd7;
state_save <= 5'd4;
end
5'd4:begin //iic停止
is_out <= 1'b1; //SDA输出
if(cnt == 9'd0)begin
scl <= 1'b0;
sda_reg <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd50)begin
scl <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd150)begin
sda_reg <= 1'b1; //停止信号:在SCL为1时,SDA的上升沿
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd249)begin
cnt <= 9'd0;
state <= 5'd5;
end
else begin
cnt <= cnt + 9'd1;
end
end
5'd5:begin //写iic结束
done_sig <= 1'b1;
state <= 5'd6;
end
5'd6:begin
done_sig <= 1'b0;
state <= 5'd0;
end
5'd7,5'd8,5'd9,5'd10,5'd11,5'd12,5'd13,5'd14:begin //发送一个字节
is_out <= 1'b1;
sda_reg <= data_reg[14-state]; //高位先发送
if(cnt == 9'd0)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd50)begin
scl <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd150)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd199)begin
cnt <= 9'd0;
state <= state + 5'd1;
end
else begin
cnt <= cnt + 9'd1;
end
end
5'd15:begin //等待应答
is_out <= 1'b0; //SDA输入
if(cnt == 9'd0)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd50)begin
scl <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd100)begin
is_ask_n <= sda;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd150)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd199)begin
cnt <= 9'd0;
state <= state + 5'd1;
end
else begin
cnt <= cnt + 9'd1;
end
end
5'd16:begin
if(!is_ask_n)begin //接收到应答信号
state <= state_save;
end
else begin
state <= 5'd0;
end
end
endcase
end
else if(rd_sig)begin //iic数据读
case(state)
5'd0:begin //iic启动
is_out <= 1'b1; //SDA输出
if(cnt == 9'd0)begin
scl <= 1'b1;
sda_reg <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd100)begin
sda_reg <= 1'b0; //启动信号:在SCL为1时,SDA的下降沿
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd200)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd249)begin
cnt <= 9'd0;
state <= 5'd1;
end
else begin
cnt <= cnt + 9'd1;
end
end
5'd1:begin //发送7位从机地址、1位写命令
data_reg <= 8'hA0;
state <= 5'd9;
state_save <= 5'd2;
end
5'd2:begin //发送读取数据地址
data_reg <= addr_sig;
state <= 5'd9;
state_save <= 5'd3;
end
5'd3:begin //iic再次启动
is_out <= 1'b1; //SDA输出
if(cnt == 9'd0)begin
scl <= 1'b1;
sda_reg <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd100)begin
sda_reg <= 1'b0; //启动信号:在SCL为1时,SDA的下降沿
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd200)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd249)begin
cnt <= 9'd0;
state <= 5'd4;
end
else begin
cnt <= cnt + 9'd1;
end
end
5'd4:begin //发送7位从机地址、1位读命令
data_reg <= 8'hA1;
state <= 5'd9;
state_save <= 5'd5;
end
5'd5:begin //读数据
data_reg <= 8'd0;
state <= 5'd19;
state_save <= 5'd6;
end
5'd6:begin //iic停止
is_out <= 1'b1; //SDA输出
if(cnt == 9'd0)begin
scl <= 1'b0;
sda_reg <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd50)begin
scl <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd150)begin
sda_reg <= 1'b1; //停止信号:在SCL为1时,SDA的上升沿
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd249)begin
cnt <= 9'd0;
state <= 5'd7;
end
else begin
cnt <= cnt + 9'd1;
end
end
5'd7:begin //读iic结束
done_sig <= 1'b1;
state <= 5'd8;
end
5'd8:begin
done_sig <= 1'b0;
state <= 5'd0;
end
5'd9,5'd10,5'd11,5'd12,5'd13,5'd14,5'd15,5'd16:begin //发送一个字节
is_out <= 1'b1;
sda_reg <= data_reg[16-state]; //高位先发送
if(cnt == 9'd0)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd50)begin
scl <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd150)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd199)begin
cnt <= 9'd0;
state <= state + 5'd1;
end
else begin
cnt <= cnt + 9'd1;
end
end
5'd17:begin //等待应答
is_out <= 1'b0; //SDA输入
if(cnt == 9'd0)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd50)begin
scl <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd100)begin
is_ask_n <= sda;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd150)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd199)begin
cnt <= 9'd0;
state <= state + 5'd1;
end
else begin
cnt <= cnt + 9'd1;
end
end
5'd18:begin
if(!is_ask_n)begin //接收到应答信号
state <= state_save;
end
else begin
state <= 5'd0;
end
end
5'd19,5'd20,5'd21,5'd22,5'd23,5'd24,5'd25,5'd26:begin //接收一个字节
is_out <= 1'b0;
if(cnt == 9'd0)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd50)begin
scl <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd100)begin
data_reg[26-state] <= sda; //高位先接收
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd150)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd199)begin
cnt <= 9'd0;
state <= state + 5'd1;
end
else begin
cnt <= cnt + 9'd1;
end
end
5'd27:begin //无应答信号
is_out <= 1'b1; //SDA输入
rd_data <= data_reg; //接收完一个字节数据
if(cnt == 9'd0)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd50)begin
scl <= 1'b1;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd150)begin
scl <= 1'b0;
cnt <= cnt + 9'd1;
end
else if(cnt == 9'd199)begin
cnt <= 9'd0;
state <= state_save;
end
else begin
cnt <= cnt + 9'd1;
end
end
endcase
end
end
endmodule
数码管显示模块
——部分具体实现代码省略(比较简单)
`timescale 1ns/1ps
//module name: smg_demo
module smg_demo
(
input clk_50MHz,
input rst,
input[7:0] data,
output[5:0] smg_sig,
output[7:0] smg_data
//output rdsig_nextdata
);
wire clk_1khz, clk_1hz;
//clkdiv
smg_clkdiv smg_clkdiv_inst
(
.clk_50MHz(clk_50MHz),
.rst(rst),
.clk_1khz(clk_1khz),
.clk_1hz(clk_1hz)
//.rdsig_nextdata(rdsig_nextdata)
);
//display
smg_display smg_display_inst
(
.clk_1khz(clk_1khz),
.clk_1hz(clk_1hz),
.rst(rst),
.data(data),
.smg_sig(smg_sig),
.smg_data(smg_data)
);
endmodule
顶层模块
`timescale 1ns/1ps
//Module Name : eeprom
//Description : top_file
//Editor : Yongxiang
//Time : 2019-11-25
module eeprom
(
input wire clk_50M,
input wire rst_n,
output wire scl,
inout wire sda,
output wire[5:0] smg_sig,
output wire[7:0] smg_data
);
wire wr_sig;
wire rd_sig;
wire[7:0] addr_sig;
wire[7:0] wr_data;
wire[7:0] rd_data;
wire done_sig;
//iic_control
iic_control iic_control_inst
(
.clk_50M(clk_50M),
.rst_n(rst_n),
.wr_sig(wr_sig),
.rd_sig(rd_sig),
.addr_sig(addr_sig),
.wr_data(wr_data),
.done_sig(done_sig)
);
//iic
iic iic_inst
(
.clk_50M(clk_50M),
.rst_n(rst_n),
.wr_sig(wr_sig), //写命令,1有效
.rd_sig(rd_sig), //读命令,1有效
.addr_sig(addr_sig), //数据地址
.wr_data(wr_data), //写数据
.rd_data(rd_data), //读数据
.done_sig(done_sig), //读写完成标志,1有效
.scl(scl),
.sda(sda)
);
//smg
smg_demo smg_demo_inst
(
.clk_50MHz(clk_50M),
.rst(rst_n),
.data(rd_data),
.smg_sig(smg_sig),
.smg_data(smg_data)
);
endmodule