0 简介
IIC的物理层
IIC一共有只有两个总线: 一条是双向的串行数据线SDA,一条是串行时钟线SCL.
SDA(Serial data)是数据线,D代表Data也就是数据,Send Data 也就是用来传输数据;
SCL(Serial clock line)是时钟线,C代表Clock 也就是时钟 也就是控制数据发送的时序。
通常我们为了方便把IIC设备分为主设备和从设备,基本上谁控制时钟线(即控制SCL的电平高低变换)谁就是主设备。[1]
I2C通信方式为半双工,只有一根SDA线,同一时间只可以单向通信,485也为半双工,SPI和uart通信为全双工。
1 时序
AT24C02_IDCHIP(英锐芯)_AT24C02中文资料_PDF手册_价格-立创商城
下面的时序图均截取自AT24C02 datasheet
数据和时钟线都为高则称总线处在空闲状态。当SCL为高电平时SDA的下降沿(高到低
叫做起始条件(START,简写为S),SDA的上升沿(低到高)则叫做停止条件(STOP,简
写为P)。
IIC的起始和停止条件的定义
IIC的位传输
IIC总线的应答
应答信号
当 I2C 主机发送完 8 位数据以后会将 SDA 设置为输入状态,等待 I2C 从机应答,也就是等到 I2C 从机告诉主机它接收到数据了。应答信号是由从机发出的,主机需要提供应答信号所需的时钟,主机发送完 8 位数据以后紧跟着的一个时钟信号就是给应答信号使用的。从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。[2]
期间地址
IIC字节写
当前地址读
随机读
顺序读
本文要涉及到的单字节写时序和随机读时序。
单字节写时序
随机读时序
2完整代码
top层代码:
`timescale 1ns / 1ps
module iic_top(
clk,rst_n,
sw1,sw2,
scl,sda,
sm_cs1_n,sm_cs2_n,sm_db
);
input clk; // 50MHz
input rst_n; //复位信号,低有效
input sw1,sw2; //按键1、2,(1按下执行写入操作,2按下执行读操作)
output scl; // 24C02的时钟端口
inout sda; // 24C02的数据端口
output sm_cs1_n,sm_cs2_n; //数码管片选信号,低有效
output[6:0] sm_db; //7段数码管(不包括小数点)
wire[7:0] dis_data; //在数码管上显示的16进制数
iic_com iic_com(
.clk(clk),
.rst_n(rst_n),
.sw1(sw1),
.sw2(sw2),
.scl(scl),
.sda(sda),
.dis_data(dis_data)
);
led_seg7 led_seg7(
.clk(clk),
.rst_n(rst_n),
.dis_data(dis_data),
.sm_cs1_n(sm_cs1_n),
.sm_cs2_n(sm_cs2_n),
.sm_db(sm_db)
);
endmodule
iic_com代码
`timescale 1ns / 1ps
// Company:
// Engineer:
//
// Create Date:
// Design Name:
// Module Name: iic_top
// Project Name:
// Target Device:
// Tool versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
module iic_com(
clk,rst_n,
sw1,sw2,
scl,sda,
dis_data
);
input clk; // 50MHz
input rst_n; //复位信号,低有效
input sw1,sw2; //按键1、2,(1按下执行写入操作,2按下执行读操作)
output scl; // 24C02的时钟端口
inout sda; // 24C02的数据端口
output[7:0] dis_data; //数码管显示的数据
//--------------------------------------------
//按键检测
reg sw1_r,sw2_r; //键值锁存寄存器,每20ms检测一次键值
reg[19:0] cnt_20ms; //20ms计数寄存器
always @ (posedge clk or negedge rst_n)
if(!rst_n) cnt_20ms <= 20'd0;
else cnt_20ms <= cnt_20ms+1'b1; //不断计数
always @ (posedge clk or negedge rst_n)
if(!rst_n) begin
sw1_r <= 1'b1; //键值寄存器复位,没有键盘按下时键值都为1
sw2_r <= 1'b1;
end
else if(cnt_20ms == 20'hfffff) begin
sw1_r <= sw1; //按键1值锁存
sw2_r <= sw2; //按键2值锁存
end
//---------------------------------------------
//分频部分
reg[2:0] cnt; // cnt=0:scl上升沿,cnt=1:scl高电平中间,cnt=2:scl下降沿,cnt=3:scl低电平中间
reg[8:0] cnt_delay; //500循环计数,产生iic所需要的时钟
reg scl_r; //时钟脉冲寄存器
always @ (posedge clk or negedge rst_n)
if(!rst_n) cnt_delay <= 9'd0;
else if(cnt_delay == 9'd499) cnt_delay <= 9'd0; //计数到10us为scl的周期,即100KHz
else cnt_delay <= cnt_delay+1'b1; //时钟计数
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) cnt <= 3'd5;
else begin
case (cnt_delay)
9'd124: cnt <= 3'd1; //cnt=1:scl高电平中间,用于数据采样
9'd249: cnt <= 3'd2; //cnt=2:scl下降沿
9'd374: cnt <= 3'd3; //cnt=3:scl低电平中间,用于数据变化
9'd499: cnt <= 3'd0; //cnt=0:scl上升沿
default: cnt <= 3'd5;
endcase
end
end
`define SCL_POS (cnt==3'd0) //cnt=0:scl上升沿
`define SCL_HIG (cnt==3'd1) //cnt=1:scl高电平中间,用于数据采样
`define SCL_NEG (cnt==3'd2) //cnt=2:scl下降沿
`define SCL_LOW (cnt==3'd3) //cnt=3:scl低电平中间,用于数据变化
always @ (posedge clk or negedge rst_n)
if(!rst_n) scl_r <= 1'b0;
else if(cnt==3'd0) scl_r <= 1'b1; //scl信号上升沿
else if(cnt==3'd2) scl_r <= 1'b0; //scl信号下降沿
assign scl = scl_r; //产生iic所需要的时钟
//---------------------------------------------
//需要写入24C02的地址和数据
`define DEVICE_READ 8'b1010_0001 //被寻址器件地址(读操作)
`define DEVICE_WRITE 8'b1010_0000 //被寻址器件地址(写操作)
`define WRITE_DATA 8'b0001_0001 //写入EEPROM的数据
`define BYTE_ADDR 8'b0000_0011 //写入/读出EEPROM的地址寄存器
reg[7:0] db_r; //在IIC上传送的数据寄存器
reg[7:0] read_data; //读出EEPROM的数据寄存器
//---------------------------------------------
//读、写时序
parameter IDLE = 4'd0;
parameter START1 = 4'd1;
parameter ADD1 = 4'd2;
parameter ACK1 = 4'd3;
parameter ADD2 = 4'd4;
parameter ACK2 = 4'd5;
parameter START2 = 4'd6;
parameter ADD3 = 4'd7;
parameter ACK3 = 4'd8;
parameter DATA = 4'd9;
parameter ACK4 = 4'd10;
parameter STOP1 = 4'd11;
parameter STOP2 = 4'd12;
reg[3:0] cstate; //状态寄存器
reg sda_r; //输出数据寄存器
reg sda_link; //输出数据sda信号inout方向控制位
reg[3:0] num; //
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
cstate <= IDLE;
sda_r <= 1'b1;
sda_link <= 1'b0;
num <= 4'd0;
read_data <= 8'b0000_0000;
end
else
case (cstate)
IDLE: begin
sda_link <= 1'b1; //数据线sda为input
sda_r <= 1'b1;
if(!sw1_r || !sw2_r) begin //SW1,SW2键有一个被按下
db_r <= `DEVICE_WRITE; //送器件地址(写操作)
cstate <= START1;
end
else cstate <= IDLE; //没有任何键被按下
end
START1: begin
if(`SCL_HIG) begin //scl为高电平期间
sda_link <= 1'b1; //数据线sda为output
sda_r <= 1'b0; //拉低数据线sda,产生起始位信号
cstate <= ADD1;
num <= 4'd0; //num计数清零
end
else cstate <= START1; //等待scl高电平中间位置到来
end
ADD1: begin
if(`SCL_LOW) begin
if(num == 4'd8) begin
num <= 4'd0; //num计数清零
sda_r <= 1'b1;
sda_link <= 1'b0; //sda置为高阻态(input)
cstate <= ACK1;
end
else begin
cstate <= ADD1;
num <= num+1'b1;
case (num)
4'd0: sda_r <= db_r[7];
4'd1: sda_r <= db_r[6];
4'd2: sda_r <= db_r[5];
4'd3: sda_r <= db_r[4];
4'd4: sda_r <= db_r[3];
4'd5: sda_r <= db_r[2];
4'd6: sda_r <= db_r[1];
4'd7: sda_r <= db_r[0];
default: ;
endcase
// sda_r <= db_r[4'd7-num]; //送器件地址,从高位开始
end
end
// else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //器件地址左移1bit
else cstate <= ADD1;
end
ACK1: begin
if(/*!sda*/`SCL_NEG) begin //注:24C01/02/04/08/16器件可以不考虑应答位
cstate <= ADD2; //从机响应信号
db_r <= `BYTE_ADDR; // 1地址
end
else cstate <= ACK1; //等待从机响应
end
ADD2: begin
if(`SCL_LOW) begin
if(num==4'd8) begin
num <= 4'd0; //num计数清零
sda_r <= 1'b1;
sda_link <= 1'b0; //sda置为高阻态(input)
cstate <= ACK2;
end
else begin
sda_link <= 1'b1; //sda作为output
num <= num+1'b1;
case (num)
4'd0: sda_r <= db_r[7];
4'd1: sda_r <= db_r[6];
4'd2: sda_r <= db_r[5];
4'd3: sda_r <= db_r[4];
4'd4: sda_r <= db_r[3];
4'd5: sda_r <= db_r[2];
4'd6: sda_r <= db_r[1];
4'd7: sda_r <= db_r[0];
default: ;
endcase
// sda_r <= db_r[4'd7-num]; //送EEPROM地址(高bit开始)
cstate <= ADD2;
end
end
// else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //器件地址左移1bit
else cstate <= ADD2;
end
ACK2: begin
if(/*!sda*/`SCL_NEG) begin //从机响应信号
if(!sw1_r) begin
cstate <= DATA; //写操作
db_r <= `WRITE_DATA; //写入的数据
end
else if(!sw2_r) begin
db_r <= `DEVICE_READ; //送器件地址(读操作),特定地址读需要执行该步骤以下操作
cstate <= START2; //读操作
end
end
else cstate <= ACK2; //等待从机响应
end
START2: begin //读操作起始位
if(`SCL_LOW) begin
sda_link <= 1'b1; //sda作为output
sda_r <= 1'b1; //拉高数据线sda
cstate <= START2;
end
else if(`SCL_HIG) begin //scl为高电平中间
sda_r <= 1'b0; //拉低数据线sda,产生起始位信号
cstate <= ADD3;
end
else cstate <= START2;
end
ADD3: begin //送读操作地址
if(`SCL_LOW) begin
if(num==4'd8) begin
num <= 4'd0; //num计数清零
sda_r <= 1'b1;
sda_link <= 1'b0; //sda置为高阻态(input)
cstate <= ACK3;
end
else begin
num <= num+1'b1;
case (num)
4'd0: sda_r <= db_r[7];
4'd1: sda_r <= db_r[6];
4'd2: sda_r <= db_r[5];
4'd3: sda_r <= db_r[4];
4'd4: sda_r <= db_r[3];
4'd5: sda_r <= db_r[2];
4'd6: sda_r <= db_r[1];
4'd7: sda_r <= db_r[0];
default: ;
endcase
// sda_r <= db_r[4'd7-num]; //送EEPROM地址(高bit开始)
cstate <= ADD3;
end
end
// else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //器件地址左移1bit
else cstate <= ADD3;
end
ACK3: begin
if(/*!sda*/`SCL_NEG) begin
cstate <= DATA; //从机响应信号
sda_link <= 1'b0;
end
else cstate <= ACK3; //等待从机响应
end
DATA: begin
if(!sw2_r) begin //读操作
if(num<=4'd7) begin
cstate <= DATA;
if(`SCL_HIG) begin
num <= num+1'b1;
case (num)
4'd0: read_data[7] <= sda;
4'd1: read_data[6] <= sda;
4'd2: read_data[5] <= sda;
4'd3: read_data[4] <= sda;
4'd4: read_data[3] <= sda;
4'd5: read_data[2] <= sda;
4'd6: read_data[1] <= sda;
4'd7: read_data[0] <= sda;
default: ;
endcase
// read_data[4'd7-num] <= sda; //读数据(高bit开始)
end
// else if(`SCL_NEG) read_data <= {read_data[6:0],read_data[7]}; //数据循环右移
end
else if((`SCL_LOW) && (num==4'd8)) begin
num <= 4'd0; //num计数清零
cstate <= ACK4;
end
else cstate <= DATA;
end
else if(!sw1_r) begin //写操作
sda_link <= 1'b1;
if(num<=4'd7) begin
cstate <= DATA;
if(`SCL_LOW) begin
sda_link <= 1'b1; //数据线sda作为output
num <= num+1'b1;
case (num)
4'd0: sda_r <= db_r[7];
4'd1: sda_r <= db_r[6];
4'd2: sda_r <= db_r[5];
4'd3: sda_r <= db_r[4];
4'd4: sda_r <= db_r[3];
4'd5: sda_r <= db_r[2];
4'd6: sda_r <= db_r[1];
4'd7: sda_r <= db_r[0];
default: ;
endcase
// sda_r <= db_r[4'd7-num]; //写入数据(高bit开始)
end
// else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //写入数据左移1bit
end
else if((`SCL_LOW) && (num==4'd8)) begin
num <= 4'd0;
sda_r <= 1'b1;
sda_link <= 1'b0; //sda置为高阻态
cstate <= ACK4;
end
else cstate <= DATA;
end
end
ACK4: begin
if(/*!sda*/`SCL_NEG) begin
// sda_r <= 1'b1;
cstate <= STOP1;
end
else cstate <= ACK4;
end
STOP1: begin
if(`SCL_LOW) begin
sda_link <= 1'b1;
sda_r <= 1'b0;
cstate <= STOP1;
end
else if(`SCL_HIG) begin
sda_r <= 1'b1; //scl为高时,sda产生上升沿(结束信号)
cstate <= STOP2;
end
else cstate <= STOP1;
end
STOP2: begin
if(`SCL_LOW) sda_r <= 1'b1;
else if(cnt_20ms==20'hffff0) cstate <= IDLE;
else cstate <= STOP2;
end
default: cstate <= IDLE;
endcase
end
assign sda = sda_link ? sda_r:1'bz;
assign dis_data = read_data;
//---------------------------------------------
endmodule
3 状态机
主机要向从机写数据时:
主机首先产生START信号
然后紧跟着发送一个从机地址,这个地址共有7位,紧接着的第8位是数据方 向位(R/W),0表示主机发送数据(写),1表示主机接收数据(读)
主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,若相同,则认为自己正在被主机寻址,根据R/T位将自己确定为发送器和接收器
这时候主机等待从机的应答信号(A)
当主机收到应答信号时,发送要访问从机的那个地址, 继续等待从机的应答信号
当主机收到应答信号时,发送N个字节的数据,继续等待从机的N次应答信号,
主机产生停止信号,结束传送过程。
主机要从从机读数据时
主机首先产生START信号
然后紧跟着发送一个从机地址,注意此时该地址的第8位为0,表明是向从机写命令,
这时候主机等待从机的应答信号(ACK)
当主机收到应答信号时,发送要访问的地址,继续等待从机的应答信号,
当主机收到应答信号后,主机要改变通信模式(主机将由发送变为接收,从机将由接收变为发送)所以主机重新发送一个开始start信号,然后紧跟着发送一个从机地址,注意此时该地址的第8位为1,表明将主机设 置成接收模式开始读取数据,
这时候主机等待从机的应答信号,当主机收到应答信号时,就可以接收1个字节的数据,当接收完成后,主机发送非应答信号,表示不在接收数据
主机进而产生停止信号,结束传送过程。
4 总结
代码中已经说明了一切,这篇文章不仅仅是说IIC的时序,也想告诉大家,状态机的写法。掌握了状态机的写法,等于成功了一半。虽然本篇文章是一段式写法,但重要的是先实现功能。欢迎大家批评指正。