- IIC协议简介
通讯协议(Inter-Integrated Circuit)是由 Philips 公司开发的一种简单、双向二线制同步串行总线,只需要两根线即可在连接于总线上的器件之间传送信息。IIC 总线只使用两条总线:一条双向串行数据线(SDA),一条双向串行时钟线 (SCL)。数据线SDA用来表示数据,时钟线SCL用于同步数据收发。每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
总线通过上拉电阻接到电源。当 IIC 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。IIC总线是一个支持多设备的总线,所有设备都共享一根数据线SDA。所以需要每个设备都有一个唯一的地址,每个 I2C 设备都具备7位/10位器件地址。在7-bit地址格式中,在起始条件后,主机发送的第1个字节,该字节高7位为从机地址,最低位为R/W位-“0”表示发送(写),主机向从机写数据,“1”表示请求数据(读)主机读取从机数据。
主机在与从机建立通讯时,主机会将控制命令直接发送到串行数据线 SDA 上,与主机硬件相连的从机设备都会接收到主机发送的控制命令。所有从机设备在接收到主机发送的控制命令后会与自身器件地址做对比;若两者地址相同,该从机设备会回应一个应答信号告知主机设备,主机设备接收到应答信号后,主从设备建立通讯连接,两者即可开始进行数据通讯。
常见的IIC协议的传输速率有100kbps,400kbps,3.4Mbps。
- IIC协议时序图
IIC协议的写时序如下图所示。
图片来源:孤独的单刀
起始信号到达后,SDA数据线拉低,表示开始通信,然后发送7位器件地址的最高位+0(0表示写,1表示读),发送完成后释放总线,等待从机的应答信号,应答成功后开始依次发送8位寄存器地址的最高位与最低位,发送完成后再次等待从机应答,然后发送待写入的8位数据,等待应答,结束应答信后。将总线拉高,表示一次写入完成。
IIC协议的读时序如下图所示。
图片来源:孤独的单刀
读时序与写时序的不同在于发送完寄存器地址后需要再次发送7位器件地址+1(表示读操作),然后释放总线,等待从机发送数据。
- 设计思路
- 根据系统时钟频率与iic_clk驱动时钟频率计算出分频系数,以产生iic驱动时钟,通信频率就是SCL的频率,根据时序图可以看出,iic_clk的频率为SCL频率的四倍
- 利用计算出的分频系数生成计数器,从而产生iic_clk驱动时钟
- 对iic_clk驱动时钟进行计数,注意此时应该用iic_clk的上升沿,计数四次以产生SCL
- 编写状态机来实现主体功能,一定要注意状态跳转的条件还有每个状态的输出
- 设计一个8位计数器对传输数据进行计数
- 代码实现
IIC驱动的代码实现如下:
`timescale 1ns / 1ps
module IIC_drive(
input clk, //系统时钟
input rst_n, //系统复位
input iic_rw, //IIC读写控制信号
input [7:0] w_data, //写入从机的8位数据
input iic_start, //开始信号
input [7:0] iic_addr, //IIC字节地址
output reg iic_end, //结束信号
output reg[7:0] r_data, //读取8位数据
output reg scl, //IIC时钟
output reg iic_clk, //IIC驱动时钟
inout sda //IIC数据线
);
parameter SYS_CLK = 28'd100_000_000 ; //系统时钟频率
parameter IIC_FREQ = 20'd400_000 ; //IIC通信频率,即SCL频率
parameter DIV_CNT = SYS_CLK / IIC_FREQ >> 2'd3 ; //分频系数
parameter DEVICE_ADDR = 7'b1010_000;
localparam IDLE = 4'd0 , //空闲状态
IIC_START = 4'd1 , //开始状态
SEND_D_ADDR_W = 4'd2 , //发送7位器件码+写操作
ACK1 = 4'd3 , //从机响应
SEND_R_ADDR = 4'd4 , //发送8位地址码
ACK2 = 4'd5 , //从机响应
W_DATA = 4'd6 , //写入8位数据
ACK3 = 4'd7 , //从机响应
IIC_START2 = 4'd8 , //第二次开始状态
SEND_D_ADDR_R = 4'd9 , //发送7位器件码+读操作
ACK4 = 4'd10 , //从机响应
R_DATA = 4'd11 , //读取8位数据
NACK = 4'd12 , //非应答状态
STOP = 4'd13 ; //结束状态
reg [3:0] state , next_state ; //现态与次态
reg [4:0] iic_cnt; //产生IIC驱动时钟计数器
reg [1:0] iic_clk_cnt; //对驱动时钟计数以产生SCL
reg iic_clk_cnt_en; //驱动时钟计数使能信号,拉高开始计数
reg [2:0] bit_cnt; //发送或读取数据计数器
reg ack_flag; //从机响应信号
reg sda_en; //三态门使能
reg sda_out; //三态门输出
reg [7:0] r_data_temp; //暂存读取到的8位数据
wire sda_in; //三态门输入
wire [7:0] addr_w; //器件码+写操作
wire [7:0] addr_r; //器件码+读操作
assign addr_w = {DEVICE_ADDR,1'b0};
assign addr_r = {DEVICE_ADDR,1'b1};
assign sda_in = sda; //双向端口处理
assign sda = sda_en ? sda_out : 1'bz;
always @(posedge clk or negedge rst_n) begin //产生驱动时钟
if(!rst_n)begin
iic_cnt <= 5'd0;
iic_clk <= 1'b0;
end
else if(iic_cnt == DIV_CNT - 1'b1)begin //计数到分频系数,计数器清零,时钟翻转
iic_cnt <= 5'd0;
iic_clk <= ~iic_clk;
end
else begin
iic_cnt <= iic_cnt + 1'b1; //否则保持不变
iic_clk <= iic_clk;
end
end
always @(posedge iic_clk or negedge rst_n) begin
if(!rst_n)
iic_clk_cnt_en <= 1'b0;
else if((state == IDLE && !iic_start) || (state == STOP && iic_clk_cnt == 2'd3)) //没有收到开始信号或者发送完结束信号时拉低
iic_clk_cnt_en <= 1'b0;
else
iic_clk_cnt_en <= 1'b1; //否则拉高
end
always @(posedge iic_clk or negedge rst_n) begin
if(!rst_n)
iic_clk_cnt <= 2'd0;
else if(iic_clk_cnt_en)
iic_clk_cnt <= iic_clk_cnt + 1'b1; //使能信号拉高开始计数
else
iic_clk_cnt <= 2'd0;
end
always @(posedge iic_clk or negedge rst_n) begin //三段式状态机第一段
if(!rst_n)
state <= IDLE;
else
state <= next_state;
end
always@(*)begin
next_state = IDLE;
case (state)
IDLE :
if(iic_start)
next_state = IIC_START; //接收到开始信号,跳转到开始状态
else
next_state = IDLE;
IIC_START :
if(iic_clk_cnt == 2'd3)
next_state = SEND_D_ADDR_W; //iic_clk_cnt计数到3,跳转到发送器件地址和写操作
else
next_state = IIC_START;
SEND_D_ADDR_W :
if(iic_clk_cnt == 2'd3 && bit_cnt == 3'd7) //成功写入器件码和写操作,进入响应状态
next_state = ACK1;
else
next_state = SEND_D_ADDR_W;
ACK1 :
if(ack_flag && iic_clk_cnt == 2'd3) //响应有效
next_state = SEND_R_ADDR;
else if(iic_clk_cnt == 2'd3) //响应无效或者响应不及时
next_state = IDLE;
else
next_state = ACK1;
SEND_R_ADDR :
if(iic_clk_cnt == 2'd3 && bit_cnt == 3'd7) //成功写入8位地址码
next_state = ACK2;
else
next_state = SEND_R_ADDR;
ACK2 :
if(ack_flag && iic_clk_cnt == 2'd3)
if(!iic_rw) //iic_rw低电平表示写操作,跳转到写入8位数据状态
next_state = W_DATA;
else //iic_rw高电平表示读操作,跳转到重新发送7位器件码+读操作
next_state = IIC_START2;
else if(iic_clk_cnt == 2'd3) //响应不及时或响应无效
next_state = IDLE;
else
next_state = ACK2;
W_DATA :
if(bit_cnt == 3'd7 && iic_clk_cnt == 2'd3) //成功写入8位数据
next_state = ACK3;
else
next_state = W_DATA;
ACK3 :
if(ack_flag && iic_clk_cnt == 2'd3)
next_state = STOP;
else if(iic_clk_cnt == 2'd3) //响应不及时或响应无效
next_state = IDLE;
else
next_state = ACK3;
IIC_START2 :
if(iic_clk_cnt == 2'd3)
next_state <= SEND_D_ADDR_R;
else
next_state <= IIC_START2;
SEND_D_ADDR_R :
if(bit_cnt == 3'd7 && iic_clk_cnt == 2'd3)
next_state = ACK4;
else
next_state = SEND_D_ADDR_R;
ACK4 :
if(ack_flag && iic_clk_cnt == 2'd3)
next_state = R_DATA;
else if(iic_clk_cnt == 2'd3) //响应不及时或响应无效
next_state = IDLE;
else
next_state = ACK4;
R_DATA :
if(bit_cnt == 3'd7 && iic_clk_cnt == 2'd3) //成功读取8位数据
next_state = NACK;
else
next_state = R_DATA;
NACK :
if(iic_clk_cnt == 2'd3) //非响应信号
next_state = STOP;
else
next_state = NACK;
STOP :
if(iic_clk_cnt == 2'd3) //停止状态结束,跳转IDLE状态
next_state = IDLE;
else
next_state = STOP;
default: next_state = IDLE; //默认为IDLE状态
endcase
end
always @(posedge iic_clk or negedge rst_n) begin
if(!rst_n)begin
sda_en <= 1'b1;
sda_out <= 1'b1;
iic_end <= 1'b0;
r_data <= 8'd0;
r_data_temp <= 8'd0;
bit_cnt <= 3'd0;
end
else begin
iic_end = 1'b0; //iic_end信号一直保持为低电平,STOP状态才拉高
case (state)
IDLE : begin
sda_en <= 1'b1; //空闲状态时sda保持高电平
sda_out <= 1'b1;
end
IIC_START : begin
if(iic_clk_cnt == 2'd3)begin //开始信号发送完成
if(addr_w[7])begin //下一个状态表将要发送的第一个数据,提前拉高总线,方便下一个状态发送
sda_out <= 1'b1;
sda_en <= 1'b1;
end
else begin
sda_out <= 1'b0;
sda_en <= 1'b1;
end
end
else begin
sda_out <= 1'b0; //进入开始状态时sda拉低,表示发送开始
sda_en <= 1'b1;
end
end
SEND_D_ADDR_W : begin
if(bit_cnt == 3'd7)
if(iic_clk_cnt == 2'd3)begin
sda_en <= 1'b0; //8位数据发送完成,释放总线等待从机响应
bit_cnt <= 3'd0;
end
else begin
sda_en <= 1'b1;
bit_cnt <= bit_cnt;
end
else if(iic_clk_cnt == 2'd3)begin
sda_en <= 1'b1;
bit_cnt <= bit_cnt + 1'b1;
sda_out <= addr_w[6 - bit_cnt]; //将8位数据传输给sda总线
end
else begin
sda_en <= 1'b1;
bit_cnt <= bit_cnt;
sda_out <= sda_out;
end
end
ACK1 : begin
if(iic_clk_cnt == 2'd3)begin //成功接收响应信号
if(iic_addr[7])begin
sda_en <= 1'b1;
sda_out <= 1'b1;
end
else begin
sda_en <= 1'b1;
sda_out <= 1'b0;
end
end
else
sda_en <= 1'b0; //响应信号还未接收完成
end
SEND_R_ADDR :begin
if(bit_cnt == 3'd7)begin //成功发送8位寄存器码
if(iic_clk_cnt == 2'd3)begin
sda_en <= 1'b0; //发送完成释放总线,等待应答
bit_cnt <= 3'd0;
end
end
else if(iic_clk_cnt == 2'd3)begin
sda_en <= 1'b1;
sda_out <= iic_addr[6 - bit_cnt]; //发送过程
bit_cnt <= bit_cnt + 1'b1;
end
end
ACK2 : begin
bit_cnt <= 3'd0;
if(!iic_rw)begin
if(iic_clk_cnt == 2'd3)begin
if(w_data[7])begin //iic_rw为低电平,主机往从机写入数据
sda_en <= 1'b1;
sda_out <= 1'b1;
end
else begin
sda_en <= 1'b1;
sda_out <= 1'b0;
end
end
else begin //iic_rw为高电平,再次发送开始信号,拉高sda_out
sda_en <= 1'b1;
sda_out <= 1'b0;
end
end
else begin
if(iic_clk_cnt == 2'd3)begin
sda_en <= 1'b1;
sda_out <= 1'b1; //未接收到响应信号
end
else begin
sda_en <= 1'b1;
sda_out <= 1'b0;
end
end
end
W_DATA : begin
if(bit_cnt == 3'd7)begin //发送完成释放总线,等待应答
if(iic_clk_cnt == 2'd3)begin
sda_en <= 1'b0;
bit_cnt <= 3'd0;
end
end
else if(iic_clk_cnt == 2'd3)begin
sda_en <= 1'b1;
sda_out <= w_data[6 - bit_cnt];
bit_cnt <= bit_cnt + 1'b1;
end
end
ACK3 : begin
bit_cnt <= 3'd0;
if(iic_clk_cnt == 2'd3)begin //成功接收响应信号,拉低sda_out
sda_en <= 1'b1;
sda_out <= 1'b0;
end
else
sda_en <= 1'b0;
end
IIC_START2 : begin
if(iic_clk_cnt == 2'd3)begin
if(addr_r[7])begin
sda_en <= 1'b1;
sda_out <= 1'b1;
end
else begin
sda_en <= 1'b1;
sda_out <= 1'b0;
end
end
else if(iic_clk_cnt == 2'd1)begin //开始信号还未发送完成
sda_en <= 1'b1;
sda_out <= 1'b0;
end
end
SEND_D_ADDR_R : begin
if(bit_cnt == 3'd7)begin
if(iic_clk_cnt == 2'd3)begin
sda_en <= 1'b0; //8位数据发送完成,释放总线等待从机响应
bit_cnt <= 3'd0;
end
else begin
sda_en <= 1'b1;
bit_cnt <= bit_cnt;
sda_out <= 1'b1;
end
end
else if(iic_clk_cnt == 2'd3)begin
sda_en <= 1'b1;
bit_cnt <= bit_cnt + 1'b1;
sda_out <= addr_r[6 - bit_cnt]; //将8位数据传输给sda总线
end
else begin
sda_en <= 1'b1;
sda_out <= sda_out;
bit_cnt <= bit_cnt;
end
end
ACK4 : begin
sda_en <= 1'b0; //应答信号结束,释放总线,准备读取数据
end
R_DATA : begin
if(iic_clk_cnt == 2'd3)begin
if(bit_cnt == 3'd7)begin
r_data <= r_data_temp; //8位数据读取完成
sda_en <= 1'b1;
sda_out <= 1'b1; //后续为非应答信号
end
else
bit_cnt <= bit_cnt + 1'b1;
end
else if(iic_clk_cnt == 2'd1)begin
r_data_temp[7 - bit_cnt] <= sda_in;
end
end
NACK : begin
bit_cnt <= 8'd0;
r_data_temp <= 8'd0;
if(iic_clk_cnt == 2'd3)begin
sda_en <= 1'b1;
sda_out <= 1'b0; //将总线拉低,方便后续发送结束信号
end
else begin
sda_en <= 1'b1;
sda_out <= 1'b1; //总线处于高电平为非应答状态
end
end
STOP : begin
if(iic_clk_cnt == 2'd2 && bit_cnt == 3'd0)begin
sda_en <= 1'b1;
sda_out <= 1'b1; //拉高总线作为结束标志
end
else if(iic_clk_cnt == 2'd3)begin
iic_end <= 1'b1;
end
end
default:;
endcase
end
end
always @(posedge iic_clk or negedge rst_n) begin
if(!rst_n)
scl <= 1'b1;
else if(state != STOP)begin
if(iic_clk_cnt == 2'd2)
scl <= 1'b0;
else if(iic_clk_cnt == 2'd0)
scl <= 1'b1;
end
else
scl <= 1'b1;
end
always @(posedge iic_clk or negedge rst_n) begin
if(!rst_n)
ack_flag <= 1'b0;
else begin
case (state)
ACK1,ACK2,ACK3,ACK4:
if(!sda_in && iic_clk_cnt == 2'd1)
ack_flag <= 1'b1;
else if(iic_clk_cnt == 2'd3)
ack_flag <= 1'b0;
default: ack_flag <= 1'b0;
endcase
end
end
endmodule
测试代码如下:
`timescale 1ns / 1ps
module tb_IIC_drive;
// IIC_drive Parameters
parameter PERIOD = 10 ;
parameter SYS_CLK = 28'd100_000_000 ;
parameter IIC_FREQ = 20'd400_000 ;
parameter DIV_CNT = SYS_CLK / IIC_FREQ >> 2'd3;
parameter DEVICE_ADDR = 7'b1010_000 ;
// IIC_drive Inputs
reg clk ;
reg rst_n ;
reg iic_rw ;
reg [7:0] w_data ;
reg iic_start ;
reg [7:0] iic_addr ;
// IIC_drive Outputs
wire iic_end ;
wire [7:0] r_data ;
wire scl ;
wire iic_clk ;
// IIC_drive Bidirs
wire sda ;
initial
begin
forever #(PERIOD/2) clk=~clk;
end
initial
begin
#(PERIOD*2) rst_n = 1;
end
IIC_drive #(
.SYS_CLK ( SYS_CLK ),
.IIC_FREQ ( IIC_FREQ ),
.DIV_CNT ( DIV_CNT ),
.DEVICE_ADDR ( DEVICE_ADDR ))
u_IIC_drive (
.clk ( clk ),
.rst_n ( rst_n ),
.iic_rw ( iic_rw ),
.w_data ( w_data ),
.iic_start ( iic_start ),
.iic_addr ( iic_addr ),
.iic_end ( iic_end ),
.r_data (r_data ),
.scl ( scl ),
.iic_clk ( iic_clk ),
.sda ( sda )
);
EEPROM_AT24C64 u_EEPROM_AT24C64_inst(
.scl (scl),
.sda (sda)
);
initial
begin
clk = 1'b1;
rst_n = 1'b0;
iic_rw = 1'b0;
iic_start = 1'b0;
w_data = 8'd0;
iic_addr = 8'd0;
#100;
iic_start = 1'b1;
#3000;
iic_start = 1'b0;
w_data = 8'd99;
iic_addr = 8'b1010_1111;
#20000000;
iic_rw = 1'b1;
iic_start = 1'b1;
#1000;
iic_start = 1'b0;
#100000;
$stop;
end
reg [103:0] state_tb;
always@(*)begin
case (u_IIC_drive.state)
4'd0 : state_tb = "IDLE";
4'd1 : state_tb = "IIC_START";
4'd2 : state_tb = "SEND_D_ADDR_W";
4'd3 : state_tb = "ACK1";
4'd4 : state_tb = "SEND_R_ADDR";
4'd5 : state_tb = "ACK2";
4'd6 : state_tb = "W_DATA";
4'd7 : state_tb = "ACK3";
4'd8 : state_tb = "IIC_START";
4'd9 : state_tb = "SEND_D_ADDR_R";
4'd10 : state_tb = "ACK4";
4'd11 : state_tb = "R_DATA";
4'd12 : state_tb = "NACK";
4'd13 : state_tb = "STOP";
default: state_tb = "IDLE";
endcase
end
endmodule
仿真过程中遇到了各种各样的问题,虽然有刀哥的代码作参考,但还是耗费了很长时间才完成了仿真。首先是状态机的第三段复位信号为高电平复位,导致计数器也不计数,开始信号到达以后SDA也不拉低,还是我在群里寻求帮助,一位大佬帮我发现了问题。问题改正以后还是出现了各种各样的问题,比如状态跳转不对或者响应信号不对等问题,主要是状态机的第三段出了问题,对比刀哥的代码检查了好多遍终于写操作没有问题了,但是读操作中第二次发送器件地址+读操作,最后发送的一个是1,但是SDA却出现了未知态,折腾半天原来是EEPROM的寄存器地址位数不对,刀哥给的寄存器地址是16位,而我的代码设计的是只有8位的,我有参照夏宇闻老师的书把EEPROM的代码改了一遍,终于成功实现了仿真。
代码思路参考自:@孤独的单刀,也是本人强烈推荐的一位博主。本文章仅用于记录学习,如有侵权,请联系博主。