文章目录
前言
一、I2C简介
二、I2C原理
2.1、I2C物理层
2.2、I2C协议层
2.2.1、I2C协议
2.2.2、I2C数据传输格式
2.2.3、I2C写操作
2.2.4、I2C读操作
三、项目设计
3.1、任务需求
3.2、状态机设计
3.3、程序代码
3.4、仿真验证
总结
前言
在前面的文章内容中我们提到常用的三个低速串行通信总线,即uart、I2C和SPI,uart串口协议前面我们已经对它做了一个详细的说明了,相信大家也都理解了它的原理,还是比较简单的。今天我们就来对I2C协议作一些简单的说明与介绍,并采用I2C协议实现通信回环功能,深入理解I2C主机与从机的时序以及其中的判断逻辑。
一、I2C简介
I2C的英文全称为Inter Integrated Circuit,即集成电路总线,是由Philips半导体公司在八十年代初设计出来的一种简单、双向、二线制总线标准,多用于主机和从机在数据量不大且传输距离短的情况下的主从通信。
I2C总线是一种两线式串行总线,由时钟线SCL与数据线SDA构成通信线路,即可用来发送数据,也可接收数据, 是一种半双工通信协议,主要用于连接微控制器及其外围设备。主机启动总线,并产生时钟用于传输数据,此时任何接收数据的器件均被认为是从机。I2C器件一般采用开漏结构与总线相连,所以SCL和SDA均需要上拉电阻,也正因如此,当总线空闲时,这两条线都处于高电平状态,当连接到总线上的任何器件输出低电平,都会使总线拉低,表明占用总线进行通信。
二、I2C原理
2.1、I2C物理层
如上图所示,I2C物理层有两条总线,一条是串行时钟线IIC_SCL,一条是双向传输的串行数据线IIC_SDA。所有连接到I2C总线设备上的串行数据SDA都连接到IIC_SDA上,各个设备的时钟线SCL都连接到总线的IIC_SCL上,I2C总线上的每个设备都有自己唯一的设备地址,来确保不同设备之间访问的准确性。
I2C总线支持多主机和注册两种工作方式,通信工作在主从工作方式。在主从工作方式下,系统中只有一个主机,其他器件都是具有I2C总线的外围从机。在主场工作方式中,书记启动数据的发送(发送起始信号)并产生时钟信号,数据发送完成后,发出停止信号结束通信。
2.2、I2C协议层
2.2.1、I2C协议
I2C总线在传输过程中也是需要遵循一定的协议进行通信的,在I2C协议数据的传输工程中有三个类型的信号:开始信号、结束信号与应答信号。
① 开始信号:当I2C总线处于空闲状态时,串行时钟线SCL和串行数据线SDA由于上拉电阻的原因都处于高电平状态,如果此时主机想要发起一次通信,就需要在SCL为高电平时将SDA数据线拉低,产生一个起始信号,表明通信的开始。
② 结束信号:当主机的一次写完成或者一次读完成后,主机在SCL为高电平时,将SDA数据线由低电平跳变为高电平,产生一个停止信号,表明一次通信的结束。
③ 应答信号:接收数据的I2C从机在接收到8bit数据后,向发送数据的I2C主机发送特定的低电平脉冲,表示已经收到数据并且数据正确。如果收到的数据不正确,那么就不会发送应答信号给主机,表示通信故障。
上图为I2C协议整体时序图,在起始信号之前为空闲状态,起始信号到停止信号之间为数据传输状态,主机可以向从机写数据,也可以读取从机输出的数据,数据的传输是由双向数据线SDA完成,当停止信号产生以后,总线再次回到空闲状态。
在I2C总线上进行数据传输时,数据的传输也是按照一定的规律进行的,否则就会导致数据混乱,数据错误等现象发生。数据的传输规律为:我们在起始信号之后,主机开始发生传输的数据,在串行时钟线SCL为低电平状态时,SDA允许改变传输的数据位(1为高电平,0为低电平),在SCL为高电平状态时,SDA要求保持稳定,相当与一个时钟周期传输1bit数据,经过8个时钟周期以后,传输8bit数据,即一个字节。如果第9个时钟周期SCL为高电平时,SDA未被检测到低电平,视为非应答,表明此次数据传输失败。第9个时钟周期末,从机释放SDA以使主机继续传输数据,如果主机发送停止信号,表明此次传输结束。这里我们需要注意的是,数据是以一个字节为单位进行传输的,其最先发送的是最高位。
2.2.2、I2C数据传输格式
当使用I2C协议进行数据传输时,首先需要在SCL时钟线为高电平时将SDA数据线拉低,来产生一个起始信号,然后按照从高到低的位序发送器件地址,一般为7bit,第8bit位为读写控制位R/W,该位为0表示主机对从机进行写操作,当该位为1时表示主机对从机进行读操作,然后接收从机响应。
当发送完第一个字节(7位器件地址+读/写控制位)并收到从机正确的应答信号后就开始发送字地址。一般而言,每个兼容I2C协议的器件,内部总会有可供读写的寄存器或者存储器,当我们对一器件中的存储单元进行读写时,首先要指定存储单元的地址(字地址),然后再向该地址写入数据或者读取数据。字地址的长度一般为1个或者2个字节,这主要取决于器件内部存储单元的数量,在本次实验中我们使用的字地址是1个字节。
主机发送完字地址,从机正确应答后就把内部的存储单元地址指针指向该单元。如果读写控制位为0,则表明主机要向从机写入数据,从机此时就处于接收数据状态,等待主机的数据写入。主机写数据也分为单字节写和页写。顾名思义,单字节写就是一次通信只向从机写入一个字节数据,页写就是一次通信向从机写入多个字节的数据。如果一个字节写入完成后发送停止命令,就是单字节写,如果继续发送下一字节数据,就是页写。
如果读写控制位为1,表明主机要向从机读取数据,此时主机作为接收方,从机作为发送方发送数据给主机。主机读取数据也有三种情况:当前地址读、随机地址读和连续地址读。当前地址读是指在一次读或写操作后发起读操作,由于I2C器件在读写操作后,其内部的地址指针自动加一,因此当前地址读可以读取下一字地址的数据。由于当前地址读极不方便读取任意地址单元的数据,所以后面也就有了随机地址读(本次实验所采用的就是随机地址读,时序见后文)。至于连续地址读,也就是在随机地址读的基础上继续读取数据罢了。
最后就是主机来结束本次通信了,由主机发送停止位(在SCL时钟线高电平时,将SDA数据线拉高),从机检测到停止位后释放总线,使之处于空闲状态,这一次的I2C通信也就结束了。
2.2.3、I2C写操作
上图为I2C的单字节写操作,具体的时序为:起始信号→从机设备地址+写控制字→应答信号→字地址→应答信号→数据→应答信号→停止信号。
2.2.4、I2C读操作
上图为I2C随机地址读操作,具体的时序为:起始信号→从机设备地址+写控制字→应答信号→字地址→应答信号→开始信号→从机设备地址+读控制字→应答信号→数据→应答信号→停止信号。
注:I2C随机地址读操作中有两次起始信号,两次设备地址,第一次设备地址+写控制字叫做虚写,这是因为我们并不是要真的写数据,而是通过这种续写操作使地址指针指向虚写操作中字地址的位置,等待从机应答后,我们就可以从中读取数据了。
三、项目设计
3.1、任务需求
通过FPGA开发板,模拟I2C回环时序传输数据,写操作采用单字节写模式,读操作采用随机地址读模式,主机发送数给从机,然后再从从机读取数据回来,实现主从机回环的功能。
在这里我将工程分为了两个模块,一个是I2C主机模块,用来产生I2C时序进行控制I2C总线,一个是I2C从机模块,用来检测I2C总线变化以及接收或者发送数据给主机进行通信。
3.2、状态机设计
这里我采用的是三段式状态机,这样理解起来容易一点,后期也方便维护和修改。I2C主机和I2C从机模块都采用了状态机的编写方法,当然这种方法也不是唯一的,状态机的划分也不是唯一的,根据自己的理解来就可以了。
① I2C_master状态机:
IDLE:空闲状态
START:开始状态,产生开始信号
WRITE:写状态,写控制字、字地址、数据等
READ:读状态,读应答信号,数据
RACK:接收应答状态,接收从机返回的应答信号
SACK:发送应答状态,读数据完成后发送应答给从机
STOP:停止状态,产生停止位
② I2C_slave状态机:
IDLE:空闲状态
START:开始状态,检测到开始信号,进入开始状态
CTRL_BYTE:控制字节状态,接收从机设备地址+读/写控制字,并发送应答信号给主机
WORD_ADDR:字节地址状态,接收主机发送的字节地址,并发送应答信号给主机
RECE_DATA:接收数据状态,接收主机发送过来的数据,并发送应答信号给主机
SEND_DATA:发送数据状态,当主机读数据时,发送数据给主机
STOP:停止状态,检测到停止信号,进入停止状态
3.3、程序代码
① I2C_master程序代码:
/*========================================*\
filename : i2c_master.v
description : i2c主机模块
time : 2022-12-05
author : 卡夫卡与海
\*========================================*/
module i2c_master(
input clk ,
input rst_n ,
input req ,//请求
input [3:0] cmd ,//命令
input [7:0] din ,//数据(给从机)
output [7:0] dout ,//数据(读从机的数据)
output done ,
output slave_ack ,//从机应答
output i2c_scl ,//i2c时钟线
input i2c_sda_i ,//i2c数据线(输入)
output i2c_sda_o ,//i2c数据线(输出)
output i2c_sda_oe //i2c输出使能
);
//参数定义
//i2c时钟参数,速率200Kbit/s
parameter SCL_PERIOD = 250,//i2c时钟周期
SCL_HALF = 125,//时钟周期的一半
LOW_HLAF = 65 ,//低电平中间点
HIGH_HALF = 190;//高电平中间点
//i2c命令参数
parameter CMD_START = 4'b0001,//开始命令
CMD_WRITE = 4'b0010,//写命令
CMD_READ = 4'b0100,//读命令
CMD_STOP = 4'b1000;//停止命令
//状态机参数
localparam IDLE = 7'b000_0001,//空闲
START = 7'b000_0010,//开始(产生起始位)
WRITE = 7'b000_0100,//写(写控制字节,字节地址,数据)
RACK = 7'b000_1000,//接收应答
READ = 7'b001_0000,//读(读从机数据)
SACK = 7'b010_0000,//发送应答
STOP = 7'b100_0000;//停止(产生停止位)
//信号定义
reg [6:0] state_c ;
reg [6:0] state_n ;
reg [8:0] cnt_scl ;//产生i2c时钟
wire add_cnt_scl ;
wire end_cnt_scl ;
reg [3:0] cnt_bit ;//传输数据 bit计数器
wire add_cnt_bit ;
wire end_cnt_bit ;
reg [3:0] bit_num ;
reg scl ;//输出寄存器
reg sda_out ;
reg sda_out_en ;
reg [7:0] rx_data ;//接收数据
reg [7:0] m_dout ;//数据
reg rx_ack ;//接收应答
reg [3:0] command ;
reg [7:0] tx_data ;//发送数据
wire idle2start ;//状态跳转规律
wire idle2write ;
wire idle2read ;
wire start2write ;
wire start2read ;
wire write2rack ;
wire read2sack ;
wire rack2stop ;
wire sack2stop ;
wire rack2idle ;
wire sack2idle ;
wire stop2idle ;
//状态机
always @(posedge clk or negedge rst_n) begin
if (rst_n==0) begin
state_c <= IDLE ;
end
else begin
state_c <= state_n;
end
end
always @(*) begin
case(state_c)
IDLE :begin
if(idle2start)
state_n = START ;
else if(idle2write)
state_n = WRITE ;
else if(idle2read)
state_n = READ ;
else
state_n = state_c ;
end
START :begin
if(start2write)
state_n = WRITE ;
else if(start2read)
state_n = READ ;
else
state_n = state_c ;
end
WRITE :begin
if(write2rack)
state_n = RACK ;
else
state_n = state_c ;
end
RACK :begin
if(rack2stop)
state_n = STOP ;
else if(rack2idle)
state_n = IDLE ;
else
state_n = state_c ;
end
READ :begin
if(read2sack)
state_n = SACK ;
else
state_n = state_c ;
end
SACK :begin
if(sack2stop)
state_n = STOP ;
else if(sack2idle)
state_n = IDLE ;
else
state_n = state_c ;
end
STOP :begin
if(stop2idle)
state_n = IDLE ;
else
state_n = state_c ;
end
default : state_n = IDLE ;
endcase
end
assign idle2start = state_c==IDLE && (req && (cmd&CMD_START));
assign idle2write = state_c==IDLE && (req && (cmd&CMD_WRITE));
assign idle2read = state_c==IDLE && (req && (cmd&CMD_READ ));
assign start2write = state_c==START && (end_cnt_bit && (command&CMD_WRITE));
assign start2read = state_c==START && (end_cnt_bit && (command&CMD_READ ));
assign write2rack = state_c==WRITE && (end_cnt_bit);
assign read2sack = state_c==READ && (end_cnt_bit);
assign rack2stop = state_c==RACK && (end_cnt_bit && (command&CMD_STOP ));
assign sack2stop = state_c==SACK && (end_cnt_bit && (command&CMD_STOP ));
assign rack2idle = state_c==RACK && (end_cnt_bit && (command&CMD_STOP ) == 0);
assign sack2idle = state_c==SACK && (end_cnt_bit && (command&CMD_STOP ) == 0);
assign stop2idle = state_c==STOP && (end_cnt_bit);
//计数器
always @(posedge clk or negedge rst_n) begin
if (rst_n==0) begin
cnt_scl <= 0;
end
else if(add_cnt_scl) begin
if(end_cnt_scl)
cnt_scl <= 0;
else
cnt_scl <= cnt_scl+1 ;
end
end
assign add_cnt_scl = (state_c != IDLE);
assign end_cnt_scl = add_cnt_scl && cnt_scl == (SCL_PERIOD)-1 ;
always @(posedge clk or negedge rst_n) begin
if (rst_n==0) begin
cnt_bit <= 0;
end
else if(add_cnt_bit) begin
if(end_cnt_bit)
cnt_bit <= 0;
else
cnt_bit <= cnt_bit+1 ;
end
end
assign add_cnt_bit = (end_cnt_scl);
assign end_cnt_bit = add_cnt_bit && cnt_bit == (bit_num)-1 ;
always @(*)begin
if(state_c == WRITE | state_c == READ) begin
bit_num = 8;
end
else begin
bit_num = 1;
end
end
//command
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
command <= 0;
end
else if(req)begin
command <= cmd;
end
end
//tx_data
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
tx_data <= 0;
end
else if(req)begin
tx_data <= din;
end
end
//scl
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
scl <= 1'b1;
end
else if(idle2start | idle2write | idle2read)begin//开始发送时,拉低
scl <= 1'b0;
end
else if(add_cnt_scl && cnt_scl == SCL_HALF-1)begin
scl <= 1'b1;
end
else if(end_cnt_scl && ~stop2idle)begin
scl <= 1'b0;
end
end
//sda_out
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
sda_out <= 1'b1;
end
else if(state_c == START)begin //发起始位
if(cnt_scl == LOW_HLAF)begin //时钟低电平时拉高sda总线
sda_out <= 1'b1;
end
else if(cnt_scl == HIGH_HALF)begin //时钟高电平时拉低sda总线
sda_out <= 1'b0; //保证从机能检测到起始位
end
end
else if(state_c == WRITE && cnt_scl == LOW_HLAF)begin //scl低电平时发送数据 并串转换
sda_out <= tx_data[7-cnt_bit];
end
else if(state_c == SACK && cnt_scl == LOW_HLAF)begin //发应答位
sda_out <= (command&CMD_STOP)?1'b1:1'b0;
end
else if(state_c == STOP)begin //发停止位
if(cnt_scl == LOW_HLAF)begin //时钟低电平时拉低sda总线
sda_out <= 1'b0;
end
else if(cnt_scl == HIGH_HALF)begin //时钟高电平时拉高sda总线
sda_out <= 1'b1; //保证从机能检测到停止位
end
end
end
//sda_out_en 总线输出数据使能
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
sda_out_en <= 1'b0;
end
else if(idle2start | idle2write | read2sack | rack2stop)begin
sda_out_en <= 1'b1;
end
else if(idle2read | start2read | write2rack | stop2idle)begin
sda_out_en <= 1'b0;
end
end
//rx_data 接收读入的数据
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
rx_data <= 0;
end
else if(state_c == READ && cnt_scl == HIGH_HALF)begin
rx_data[7-cnt_bit] <= i2c_sda_i; //串并转换
end
else begin
rx_data <= rx_data;
end
end
//m_dout
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
m_dout <= 0;
end
else if(state_c == READ && end_cnt_bit)begin
m_dout <= rx_data;
end
else begin
m_dout <= m_dout;
end
end
//rx_ack
always @(posedge clk or negedge rst_n)begin
if(~rst_n)begin
rx_ack <= 1'b1;
end
else if(state_c == RACK && cnt_scl == HIGH_HALF)begin
rx_ack <= i2c_sda_i;
end
else begin
rx_ack <= 1'b1;
end
end
//输出信号
assign i2c_scl = scl ;
assign i2c_sda_o = sda_out ;
assign i2c_sda_oe = sda_out_en ;
assign dout = m_dout;
assign done = rack2idle | sack2idle | stop2idle;
assign slave_ack = rx_ack;
endmodule
② I2C_slave程序代码:
/*========================================*\
filename : i2c_slave.v
description : i2c从机模块
time : 2023-01-02
author : 卡夫卡与海
\*========================================*/
module i2c_slave(
input clk ,//系统时钟 50MHZ
input rst_n ,//系统复位
input [7:0] data_in ,//数据(发送给主机)
input i2c_scl ,//i2c时钟
input sda_in ,//数据输入
output reg sda_out ,//数据输出
output reg sda_oen ,//数据输出使能
output reg [7:0] word_addr ,//寄存器地址
output reg [7:0] data_out //数据(从机接收的数据)
);
//参数定义
parameter SLAVE_ADDR = 7'h3C;//从机地址
parameter CLOCK = 20;//20ns 50MHZ
parameter SCL_TIME = 2500/CLOCK,//接收速率400Kbit/s
SCL_HIGH_HOUD = 1200/CLOCK,//高电平保持时间
SCL_LOW_HOUD = 1300/CLOCK,//低电平保持时间
SAMPLE_TIME = 800/CLOCK ,//采样时间
CHANGE_TIME = 200/CLOCK ;//跳变时间
//状态机
parameter IDLE = 7'b000_0001,//空闲
START = 7'b000_0010,//开始
CTRL_BYTE = 7'b000_0100,//控制字节
WORD_BYTE = 7'b000_1000,//字节地址
SEND_DATA = 7'b001_0000,//发送数据
RECE_DATA = 7'b010_0000,//接收数据
STOP = 7'b100_0000;//停止
//信号定义
reg [6:0] state_c ;//现态
reg [6:0] state_n ;//次态
reg scl_0 ;//对i2c_scl打拍
reg scl_1 ;
wire scl_podge ;//scl上升沿
wire scl_nedge ;//scl下降沿
reg sda_in_0 ;//sda_in打拍
reg sda_in_1 ;
wire sda_in_podge ;//sda_in上升沿
wire sda_in_nedge ;//sda_in下降沿
wire i2c_start ;//起始信号
wire i2c_stop ;//停止信号
reg [6:0] cnt_scl_low ;//计数scl低电平
wire add_cnt_scl_low ;
wire end_cnt_scl_low ;
reg [3:0] cnt_bit ;//bit计数器
wire add_cnt_bit ;
wire end_cnt_bit ;
reg wr_or_rd ;//判断当前读写操作 (0:写 1:读)
reg [7:0] receive_buff ;//数据缓存
reg [6:0] chir_addr ;//缓存从机地址
reg [7:0] tx_data ;//发送给主机的数据缓存
wire idle2start ;//状态跳转规律
wire start2ctrl ;
wire ctrl2word ;
wire ctrl2send ;
wire ctrl2idle ;
wire word2rece ;
wire send2stop ;
wire rece2start ;
wire rece2stop ;
wire stop2idle ;
//对SCL打拍,检测上升沿、下降沿
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
scl_0 <= 1'b1;
scl_1 <= 1'b1;
end
else begin
scl_0 <= i2c_scl;
scl_1 <= scl_0;
end
end
assign scl_podge = ~scl_1 && scl_0;//SCL上升沿
assign scl_nedge = scl_1 && ~scl_0;//SCL下降沿
//对SDA_in打拍,检测上升沿、下降沿
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
sda_in_0 <= 1'b1;
sda_in_1 <= 1'b1;
end
else begin
sda_in_0 <= sda_in;
sda_in_1 <= sda_in_0;
end
end
assign sda_in_podge = sda_in_0 && ~sda_in_1;//sda_in上升沿
assign sda_in_nedge = ~sda_in_0 && sda_in_1;//sda_in下降沿
assign i2c_start = (i2c_scl && sda_in_nedge) ? 1'b1 : 1'b0;//起始信号
assign i2c_stop = (i2c_scl && sda_in_podge) ? 1'b1 : 1'b0;//停止信号
//FSM
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 @(*)begin
case(state_c)
IDLE : begin
if(idle2start)begin
state_n = START;
end
else begin
state_n = state_c;
end
end
START : begin
if(start2ctrl)begin
state_n = CTRL_BYTE;
end
else begin
state_n = state_c;
end
end
CTRL_BYTE : begin
if(ctrl2word)begin
state_n = WORD_BYTE;
end
else if(ctrl2send)begin
state_n = SEND_DATA;
end
else if(ctrl2idle)begin
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
WORD_BYTE : begin
if(word2rece)begin
state_n = RECE_DATA;
end
else begin
state_n = state_c;
end
end
SEND_DATA : begin
if(send2stop)begin
state_n = STOP;
end
else begin
state_n = state_c;
end
end
RECE_DATA : begin
if(rece2start)begin
state_n = START;
end
else if(rece2stop)begin
state_n = STOP;
end
else begin
state_n = state_c;
end
end
STOP : begin
if(stop2idle)begin
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
default: begin
state_n = IDLE;
end
endcase
end
assign idle2start = (state_c ==IDLE ) && (i2c_start);
assign start2ctrl = (state_c ==START ) && (scl_nedge);
assign ctrl2word = (state_c ==CTRL_BYTE) && (end_cnt_bit && chir_addr==SLAVE_ADDR && ~wr_or_rd);
assign ctrl2send = (state_c ==CTRL_BYTE) && (end_cnt_bit && chir_addr==SLAVE_ADDR && wr_or_rd);
assign ctrl2idle = (state_c ==CTRL_BYTE) && (end_cnt_bit && chir_addr!=SLAVE_ADDR && ~wr_or_rd);
assign word2rece = (state_c ==WORD_BYTE) && (end_cnt_bit);
assign send2stop = (state_c ==SEND_DATA) && (cnt_bit=='d8 && scl_0 && sda_in_0);
assign rece2start = (state_c ==RECE_DATA) && (i2c_start);
assign rece2stop = (state_c ==RECE_DATA) && (i2c_stop);
assign stop2idle = (state_c ==STOP ) && ((~wr_or_rd) || (wr_or_rd && i2c_stop));
//cnt_scl_low 计数SCL低电平数
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_scl_low <= 0;
end
else if(add_cnt_scl_low)begin
if(end_cnt_scl_low)begin
cnt_scl_low <= 0;
end
else begin
cnt_scl_low <= cnt_scl_low + 1'b1;
end
end
end
assign add_cnt_scl_low = state_c >= CTRL_BYTE && ~scl_0;
assign end_cnt_scl_low = add_cnt_scl_low && cnt_scl_low == (SCL_LOW_HOUD - 1);
//cnt_bit
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_bit <= 0;
end
else if(add_cnt_bit)begin
if(end_cnt_bit)begin
cnt_bit <= 0;
end
else begin
cnt_bit <= cnt_bit + 1'b1;
end
end
end
assign add_cnt_bit = state_c >= CTRL_BYTE && state_c < STOP && (scl_nedge || send2stop);
assign end_cnt_bit = add_cnt_bit && cnt_bit == 'd8;
//wr_or_rd 判断当前是读写状态
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
wr_or_rd <= 1'b0;
end
else if(stop2idle)begin
wr_or_rd <= 1'b0;
end
else if(state_c==CTRL_BYTE && cnt_bit == 7 && scl_0)begin
wr_or_rd <= sda_in_0;
end
else begin
wr_or_rd <= wr_or_rd;
end
end
//receive_buff 数据缓存
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
receive_buff <= 0;
end
else if(state_c == CTRL_BYTE && scl_0)begin
receive_buff[7 - cnt_bit] <= sda_in_0; //缓存控制字节
end
else if(state_c == WORD_BYTE && scl_0)begin
receive_buff[7 - cnt_bit] <= sda_in_0; //缓存寄存器地址
end
else if(state_c == RECE_DATA && scl_0)begin
receive_buff[7 - cnt_bit] <= sda_in_0; //缓存数据
end
else begin
receive_buff <= receive_buff;
end
end
//chir_addr 从机地址缓存
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
chir_addr <= 0;
end
else if(state_c == CTRL_BYTE && cnt_bit=='d8)begin
chir_addr <= receive_buff[7:1];//高7位是从机地址,最后一位是读写位
end
else begin
chir_addr <= chir_addr;
end
end
//word_addr 寄存器地址缓存
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
word_addr <= 0;
end
else if(state_c == WORD_BYTE && cnt_bit=='d8)begin
word_addr <= receive_buff;
end
else begin
word_addr <= word_addr;
end
end
//data_out 数据缓存
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
data_out <= 0;
end
else if(state_c == RECE_DATA && cnt_bit=='d8)begin
data_out <= receive_buff;
end
else begin
data_out <= data_out;
end
end
//tx_data 发送给主机的数据缓存
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
tx_data <= 0;
end
else if(state_c == SEND_DATA)begin
tx_data <= data_in;
end
end
//sda_out sda_oen
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
sda_out <= 1'bz;
sda_oen <= 1'b0;
end
else if(state_c==IDLE && ~i2c_scl)begin
sda_out <= 1'bz;
sda_oen <= 1'b0;
end
else if(state_c == SEND_DATA)begin
if(cnt_scl_low == CHANGE_TIME)begin
if(cnt_bit <= 'd7)begin
sda_out <= tx_data[7 - cnt_bit];
sda_oen <= 1'b1;
end
else begin
sda_out <= 1'bz;
sda_oen <= 1'b0;
end
end
else begin
sda_out <= sda_out;
sda_oen <= sda_oen;
end
end
else begin
if((cnt_scl_low==CHANGE_TIME) && (chir_addr==SLAVE_ADDR))begin
case(cnt_bit)
'd0 : begin
sda_out <= 1'bz;
sda_oen <= 1'b0;
end
'd8 : begin
sda_out <= 1'b0;
sda_oen <= 1'b1;
end
default : begin
sda_out <= 1'bz;
sda_oen <= 1'b0;
end
endcase
end
end
end
endmodule
③ 仿真代码:
`timescale 1ns/1ns
/*========================================*\
filename : i2c_master_tb.v
description : i2c仿真模块
time : 2023-01-02
author : 卡夫卡与海
\*========================================*/
module i2c_master_tb();
//时钟复位输入
reg clk ;
reg rst_n ;
//激励输入
reg req ;
reg [3:0] cmd ;
reg [7:0] din ;
//输出
wire [7:0] dout ;
wire done ;
wire ack ;
wire scl ;
wire sda_i ;
wire sda_o ;
wire sda_oe ;
wire sda_oen ;
wire [7:0] word_addr ;
wire [7:0] data_out ;
//时钟周期定义
parameter CYCLE = 20;
//复位时间定义
parameter RST_TIME = 3 ;
//i2c命令参数
parameter CMD_START = 4'b0001,//开始命令
CMD_WRITE = 4'b0010,//写命令
CMD_READ = 4'b0100,//读命令
CMD_STOP = 4'b1000;//停止命令
//模块例化
i2c_master u_i2c_master(
/*input */.clk (clk ),
/*input */.rst_n (rst_n ),
/*input */.req (req ),
/*input [3:0] */.cmd (cmd ),
/*input [7:0] */.din (din ),
/*output [7:0] */.dout (dout ),
/*output */.done (done ),
/*output */.slave_ack (ack ),
/*output */.i2c_scl (scl ),
/*input */.i2c_sda_i (sda_i ),
/*output */.i2c_sda_o (sda_o ),
/*output */.i2c_sda_oe (sda_oe )
);
i2c_slave u_i2c_slave(
/*input */.clk (clk),//系统时钟
/*input */.rst_n (rst_n),//系统复位
/*input [7:0] */.data_in (data_out),//外部数据输入(给主机读取的数据)
/*input */.i2c_scl (scl),//I2C时钟输入
/*input */.sda_in (sda_o),//I2C数据线(输入)
/*output */.sda_out (sda_i),//I2C数据线(输出)
/*output */.sda_oen (sda_oen),//使能
/*output [7:0] */.word_addr (word_addr),//寄存器地址
/*output [7:0] */.data_out (data_out)//主机发送的数据
);
task traffic_gen;
input [7:0] data ;
input [3:0] command ;
begin
#2;
req = 1'b1;
din = data;
cmd = command;
#(CYCLE*1);
req = 1'b0;
@(negedge done);
#(CYCLE*1);
end
endtask
//产生时钟
initial begin
clk = 1;
forever
#(CYCLE/2)
clk=~clk;
end
//产生复位
initial begin
rst_n = 0;
#(CYCLE*RST_TIME);
rst_n = 1;
end
//激励
initial begin
#1;
req = 0 ;
cmd = 0 ;
din = 0 ;
#(10*CYCLE);
/****************************
字节写
****************************/
traffic_gen(8'b1100_0110,{CMD_START | CMD_WRITE});//发起始位 + 写控制字
traffic_gen(8'hB2,CMD_WRITE); //写字地址
traffic_gen(8'hb2,{CMD_WRITE |CMD_STOP}); //发数据 + 停止位
#(50*CYCLE);
traffic_gen(8'b0111_1000,{CMD_START | CMD_WRITE});//发起始位 + 写控制字
traffic_gen(8'hB3,CMD_WRITE); //写字地址
traffic_gen(8'hc9,{CMD_WRITE |CMD_STOP}); //发数据 + 停止位
#(50*CYCLE);
/****************************
随机地址读
****************************/
traffic_gen(8'b0111_1000,{CMD_START | CMD_WRITE});//发起始位 + 写控制字
traffic_gen(8'hB3,CMD_WRITE); //写字地址
traffic_gen(8'b0111_1001,{CMD_START | CMD_WRITE});//发起始位 + 发读控制字
traffic_gen(8'h00,{CMD_READ | CMD_STOP}); //读数据 + 发停止位
#(100*CYCLE);
$stop;
end
endmodule
这里为了使I2C从机模块更加符合I2C总线协议,我在仿真的时候首先发送了一个错误的设备地址,此时I2C从机模块不能够作出任何相应,不能够对总线做任何操作,不然就会占用总线,导致挂载在该总线上的其他设备不工作,这也是我在工作当中发现的问题,希望对大家有帮助。
3.4、仿真验证
① I2C_master仿真波形:
② I2C_slave仿真波形:
从仿真波形来看,I2C主机向从机发起通信时,首先发送的设备地址是63,但是我这里定义的从机设备地址是3C,所以从机并未做出任何响应,也没有对I2C总线做出任何操作,当后面检测到主机发送的设备地址是3C时,从机拉低SDA数据线,发送一个应答信号给主机,表示与I2C主机建立了通信,紧接着主机发送字节地址B3,然后发送数据C9给从机,从机收到寄存器地址与数据后将其缓存下来并输出出去,然后主机再读取字节地址B3中的数据,最后主机收到的数据是C9,表示此次I2C主从机回环实验成功,数据收发正确。
这里还要注意主从机的采样速率,如果主机发送数据的速率比从机采样数据的速率快,那么很可能采样到的数据就会丢帧,我前面也实验过,会导致采样的数据不正确。我这里从机从采样速率在400Kbit/s左右,主机的发送速率在200Kbit/s左右,采样的数据是正确的,如果将主机的速率降低到100Kbit/s,仍然能够采样到正确的数据。
总结
I2C协议虽然是两线协议,其实它的时序还是有点复杂的,主机的逻辑还好,特别是从机,千万不能一直占用总线不释放,这样的话就导致总线上的其他从设备无法工作,这个原因我调试了好几天,希望各位小伙伴们别踩这个坑。
我这里的从机模块的设备地址是直接定义在模块里面的,这就有一点限制的模块的移植性,感兴趣的小伙伴也可以改写由外部输入,这样就可以随时进行更改了,但是原理都是一样的,理解才是最重要的。文章来源:https://www.toymoban.com/news/detail-596450.html
详细的协议还是得看手册,我这里是根据EEPROM的手册写的,只不过没有做读写EEPROM的程序项目,搞这个回环的目的就是很多时候我们FPGA是要做I2C从机的,I2C从机的资料并不是很多,很多都是主机的,索性我就手撕一个回环,主从机都有,以后可以拿来直接用,哈哈!!!文章来源地址https://www.toymoban.com/news/detail-596450.html
到了这里,关于【FPGA】十一、I2C通信回环的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!