(附源码)FPGA实现IIC通信,看完你就懂了!!!
1.前言
本次主要学习IIC通信协议的FPGA实现,并使用modelsim仿真,由于手中没有IIC相关的外设,所以本次暂时不进行板载验证,只进行时序逻辑仿真。
2.IIC协议简介
IIC通信协议是一种串行通信总线,但其只有一根数据线,属于半双工通信协议,适用于传输距离较短的场景下使用。IIC由数据线SDA和时钟线SCL组成,数据线既可以用来发送数据,也可以用来接收数据,数据传输速率在标准模式下可以达到100kbit/s,在快速模式下可以达到400kbit/s,在高速模式下可以达到3.4Mbit/s,IIC的数据线和时钟线可以都会接上拉电阻,保证在没有进行数据传输时引脚电平处于高电平状态。
IIC支持多主和主从两种工作方式,其通常工作在主从工作模式,在主从工作模式下,系统只有一个主机,其他期间都是IIC总线的从机,在该工作模式下,主机产生时钟信号并启动数据发送和停止。IIC工作总线时序如下图所示。空闲状态数据线和时钟线均处于高电平,当相进行数据传输时,需要先把SDA数据线拉低,产生一个起始信号,当想停止数据传输时,将SDA数据线拉高,产生一个停止信号,此时再次回到空闲状态,有没有发现看到这里数据传输格式和串口通信有点像呢?不过这个可比串口通信复杂。
IIC通信包括读和写,这两个功能都是在同一条数据线上完成的,通过不同的读写位来控制。IIC通信开始后,会首先发送一个8位数据来选择器件以及读写控制,具体格式如下:
开始位-x6-x5-x4-x3-x2-x1-x0-R/W-ACK应答位
其中x0-x6位器件地址,这跟使用的具体器件有关,R/W为读写控制位,为1时表示主机对从机进行读操作,为0时表示主机对从机进行写操作,每发送一帧8位数据都需要从机应答,相反,进行读操作时,每读完8位数据,需要主机给予相应的应答信号。
IIC单次写时序图如下图所示。首先SDA数据线拉低发送起始信号,SCL时钟开启,SDA数据线首先发送期间地址和写命令,器件地址位变化只能在SCL电平为低电平时变化,因为SCL电平为高电平时从机会从SDA读取数据,当发送完一帧数据之后,从机会发送应答信号,主机接收到应答信号之后会继续发送下一帧数据,下面就是进行“虚写”,之所以称之为“虚写”是因为当我们需要向从机某个地址写数据时,需要先发送该地址,即“虚写”,之后发送要写的地址的高8位和低8位,分贝得到从机应答之后就可以将需要写的数据发送过去,写完之后如果不需要写数据了,就将电平拉高并且停止产生时钟,即发送停止信号,一次写操作完成。
一次写操作可以在指定地址写一个8位数据,当我们需要在连续的地址写多个8位数据时,是不是每次都需要发送器件地址和写地址再发送要写的数据呢?答案是否定的,当我们需要写的地址是连续的时,可以采用连续写的方式,即只发送一次器件地址和起始写地址就可以了,时序图如下图所示。在一次写数据完成之后,不发送停止信号,继续发送数据,这是会自动向开始地址的下一个地址写数据,直到发送停止信号。
同理,IIC读操作也是相同的操作。其时序图如下图所示。读操作同样首先是发送器件地址和要读的地址(此时处于写操作模式),之后重新发送起始信号、器件地址和读信号,就可以在指定的地址进行读数据了,读完一帧8位数据之后,需要主机房应答信号,从机收到应答信号之后才会继续发送数据,结束之后主机发送停止信号结束。同理,连续读同样是主机不发送停止信号继续读下一个地址的数据,直到发送停止信号。
至此,IIC协议时序部分就介绍到这里。
3.程序设计
FPGA程序设计主要在一个iic_driver模块中实现IIC在指定地址读和写一帧8位数据。程序中需要注意一个点就是inout引脚SDA,当读数据的时候读到8位数据需要输出一个应答信号,当写数据的时候,写完8位数据需要等待接收应答信号。inout数据只能通过assign赋值,一般通过三态门实现,当需要发送数据的时候为输出状态,当需要接收数据的时候为高阻态,这具体用法在程序中有体现。仿真发送一个8位数据如下图所示。
仿真接收一个8位数据如下图所示,可以看到接收完数据之后才更新8位数据。
下面贴一下代码供大家参考一下。
iic_driver.v
/*
功能:IIC驱动模块设计
作者:王志川
时间:2024.7.5
*/
/*
进度时间戳:
2024.7.4 晚上实现发送器件地址+起始地址
2024.7.5早上实现发送1个8位数据,并成功发送停止位。
2024.7.5下午实现读取指定地址1个8位数据,并成功发送停止位。
*/
module iic_driver(
input CLK, //50MHZ时钟信号
input RST, //复位信号
inout SDA, //IIC数据引脚
input iic_rw, //IIC读写控制信号
input iic_start, //IIC起始信号,为1有效
input [15:0]iic_data_addr, //IIC要写数据的地址
input [7:0]iic_data_w, //IIC要写的数据
output reg [7:0]iic_data_r, //IIC读出的数据
output reg [3:0]read_flag, //读数据状态标志
output reg iic_stop, //IIC停止信号
output reg write_done, //IIC一次8位数据写完成标志位
output reg SCL //IIC时钟引脚
);
//IIC时钟信号产生
parameter [8:0]scl_max=9'd250;//标准模式,100kbit/s
//parameter [8:0]scl_max=9'd63;//快速模式,400kbit/s
//parameter [8:0]scl_max=9'd7;//高速模式,3.4Mbit/s
reg [8:0]scl_cnt;//时钟计数周期
reg scl_en = 1'd0;//时钟使能信号
reg [3:0]sda_state;//数据发送状态
reg [3:0]cnt;//发送数据的位数
always@(posedge CLK or negedge RST)
begin
if(!RST)begin
SCL <= 1'd1;
scl_cnt <= 9'd0;
end
else if(scl_en == 1'd1 && iic_start == 1'd1 && (iic_rw != 1'd1 || sda_state != 4'd3 || cnt != 4'd0 || scl_cnt != 9'd0))begin
if(scl_cnt == scl_max-1)begin//计数到最大值
SCL <= ~SCL;//时钟翻转
scl_cnt <= 9'd0;//计数清零
end
else
scl_cnt <= scl_cnt + 1'd1;
end
else begin
SCL = 1'd1;
scl_cnt <= 9'd0;
end
end
//数据发送计时产生
parameter [8:0]sda_max=9'd500;//标准模式,100kbit/s
//parameter [8:0]sda_max=9'd125;//快速模式,400kbit/s
//parameter [8:0]sda_max=9'd15;//高速模式,3.4Mbit/s
reg [8:0]sda_cnt;//时钟计数周期
always@(posedge CLK or negedge RST)
begin
if(!RST)begin
sda_cnt <= 9'd0;
cnt <= 4'd0;
sda_state <= 4'd0;
iic_stop <= 1'd0;
//last_cnt <= 4'd0;
end
//写命令
else if(iic_start == 1'd1 && iic_rw == 1'd0)begin
if(sda_cnt == sda_max-1)begin//,由于本次时发送最大值,不是时钟电平转换最大值,所以计数值是scl的两倍,计数到最大值
sda_cnt <= 9'd0;//计数清零
if(cnt == 4'd9 && sda_state == 4'd0 && SDA == 1'd0)begin//器件写地址完成,并且接收到应答
sda_state <= 4'd1;
cnt <= 4'd0;
//last_cnt <= 4'd0;
end
else if(cnt == 4'd8 && sda_state == 4'd1 && SDA == 1'd0)begin//要写的地址高位完成,并且接收到应答
sda_state <= 4'd2;
cnt <= 4'd0;
//last_cnt <= 4'd0;
end
else if(cnt == 4'd8 && sda_state == 4'd2 && SDA == 1'd0)begin//要写的地址低位完成,并且接收到应答
cnt <= 4'd0;
sda_state <= 4'd3;
//last_cnt <= 4'd0;
end//虚写操作完成
else if(cnt == 4'd9 && sda_state == 4'd3 && SDA == 1'd0)begin//该地址8位数据发送完毕,需要发送停止信号
cnt <= 4'd0;
iic_stop <= 1'd1;
//last_cnt <= 4'd0;
end
else
cnt <= cnt + 1'd1;
end
// else if(sda_cnt == sda_max-2)begin
// last_cnt <= cnt + 1'd1;
// sda_cnt <= sda_cnt + 1'd1;
// end
else
sda_cnt <= sda_cnt + 1'd1;
end
//读命令
else if(iic_start == 1'd1 && iic_rw == 1'd1)begin
if(sda_cnt == sda_max-1)begin//,由于本次时发送最大值,不是时钟电平转换最大值,所以计数值是scl的两倍,计数到最大值
sda_cnt <= 9'd0;//计数清零
if(cnt == 4'd9 && sda_state == 4'd0 && SDA == 1'd0)begin//器件写地址完成,并且接收到应答
sda_state <= 4'd1;
cnt <= 4'd0;
//last_cnt <= 4'd0;
end
else if(cnt == 4'd8 && sda_state == 4'd1 && SDA == 1'd0)begin//写地址高位完成,并且接收到应答
sda_state <= 4'd2;
cnt <= 4'd0;
//last_cnt <= 4'd0;
end
else if(cnt == 4'd8 && sda_state == 4'd2 && SDA == 1'd0)begin//写地址低位完成,并且接收到应答
cnt <= 4'd0;
sda_state <= 4'd3;
//last_cnt <= 4'd0;
end//虚写操作完成
else if(cnt == 4'd10 && sda_state == 4'd3 && SDA == 1'd0)begin//器件写地址完成,并且接收到应答
sda_state <= 4'd4;
cnt <= 4'd0;
//last_cnt <= 4'd0;
end
else if(cnt == 4'd9 && sda_state == 4'd4)begin//读取八位数据完成,发送停止信号
cnt <= 4'd0;
iic_stop <= 1'd1;
//last_cnt <= 4'd0;
end
else
cnt <= cnt + 1'd1;
end
// else if(sda_cnt == sda_max-2)begin
// last_cnt <= cnt + 1'd1;
// sda_cnt <= sda_cnt + 1'd1;
// end
else
sda_cnt <= sda_cnt + 1'd1;
end
end
always@(*)begin
//接收写的应答
if(iic_rw == 1'd0)begin
if(cnt == 4'd9 && sda_state == 4'd0)//器件写地址完成
write_done <= 1'd1;
else if(cnt == 4'd8 && sda_state == 4'd1)//写地址高位完成
write_done <= 1'd1;
else if(cnt == 4'd8 && sda_state == 4'd2)//写地址低位完成
write_done <= 1'd1;//虚写操作完成
else if(cnt == 4'd8 && sda_state == 4'd3)//发送八位数据完成
write_done <= 1'd1;//发送八位数据完成
else
write_done <= 1'd0;
end
//接收写的应答和发送读的应答
else if(iic_rw == 1'd1)begin
if(cnt == 4'd9 && sda_state == 4'd0)//器件写地址完成
write_done <= 1'd1;
else if(cnt == 4'd8 && sda_state == 4'd1)//写地址高位完成
write_done <= 1'd1;
else if(cnt == 4'd8 && sda_state == 4'd2)//写地址低位完成
write_done <= 1'd1;//虚写操作完成
else if(cnt == 4'd10 && sda_state == 4'd3)//发送其实信号和器件地址
write_done <= 1'd1;
else if(cnt != 4'd8 && cnt != 4'd9 && sda_state == 4'd4)
write_done <= 1'd1;
else
write_done <= 1'd0;
end
end
always@(posedge CLK or negedge RST)
begin
if(!RST)
read_flag <= 4'd0;
else if(iic_rw == 1'd1 && cnt != 4'd8 && cnt != 4'd9 && sda_state == 4'd4)
read_flag <= cnt + 1'd1;
else
read_flag <= 4'd0;
end
//由于发送数据只能是在scl低电平的时候发送,所以这两个信号的时钟需要进行错位
//即起始信号开始,当起始信号的低电平计数到一半时,开始计数scl时钟。
always@(posedge CLK or negedge RST)
begin
if(!RST)
scl_en <= 1'd0;
else if(sda_cnt == sda_max/2 - scl_max/2)
scl_en <= 1'd1;
end
//数据发送状态机
reg sda_data;//发送数据暂存
reg [7:0]r_iic_data_r;//接收数据暂存
reg dif_sda;//每一位数据暂存
always@(posedge CLK or negedge RST)
begin
if(!RST)begin
sda_data <= 1'd1;//默认时刻为高电平
r_iic_data_r <= 8'd0;
end
else if(iic_stop == 1'd0 && iic_rw == 1'd0)begin//写命令
case(sda_state)//根据不同的状态发送不同的数据
4'd0:begin//发送起始信号和器件地址
case(cnt)
4'd0:sda_data <= 1'd0;//起始位
4'd1:sda_data <= 1'd1;
4'd2:sda_data <= 1'd0;
4'd3:sda_data <= 1'd1;
4'd4:sda_data <= 1'd0;
4'd5:sda_data <= 1'd0;
4'd6:sda_data <= 1'd0;
4'd7:sda_data <= 1'd0;
4'd8:sda_data <= iic_rw;//0表示写
4'd9:sda_data <= iic_data_addr[15];//下一位数据提前写进去
endcase
end
4'd1:begin//发送地址高位
case(cnt)
4'd0:sda_data <= iic_data_addr[15];
4'd1:sda_data <= iic_data_addr[14];
4'd2:sda_data <= iic_data_addr[13];
4'd3:sda_data <= iic_data_addr[12];
4'd4:sda_data <= iic_data_addr[11];
4'd5:sda_data <= iic_data_addr[10];
4'd6:sda_data <= iic_data_addr[9];
4'd7:sda_data <= iic_data_addr[8];
4'd8:sda_data <= iic_data_addr[7];//下一位数据提前写进去
endcase
end
4'd2:begin//发送地址低位
case(cnt)
4'd0:sda_data <= iic_data_addr[7];
4'd1:sda_data <= iic_data_addr[6];
4'd2:sda_data <= iic_data_addr[5];
4'd3:sda_data <= iic_data_addr[4];
4'd4:sda_data <= iic_data_addr[3];
4'd5:sda_data <= iic_data_addr[2];
4'd6:sda_data <= iic_data_addr[1];
4'd7:sda_data <= iic_data_addr[0];
4'd8:sda_data <= iic_data_w[7];//下一位数据提前写进去
endcase
end
4'd3:begin//发送八位数据
case(cnt)
4'd0:sda_data <= iic_data_w[7];
4'd1:sda_data <= iic_data_w[6];
4'd2:sda_data <= iic_data_w[5];
4'd3:sda_data <= iic_data_w[4];
4'd4:sda_data <= iic_data_w[3];
4'd5:sda_data <= iic_data_w[2];
4'd6:sda_data <= iic_data_w[1];
4'd7:sda_data <= iic_data_w[0];
4'd8:sda_data <= 1'd0;//下一位提前写进去
4'd9:sda_data <= 1'd0;//停止信号
endcase
end
endcase
end
else if(iic_stop == 1'd0 && iic_rw == 1'd1)begin//读命令
case(sda_state)//根据不同的状态发送不同的数据
4'd0:begin//发送起始信号和器件地址
case(cnt)
4'd0:sda_data <= 1'd0;//起始位
4'd1:sda_data <= 1'd1;
4'd2:sda_data <= 1'd0;
4'd3:sda_data <= 1'd1;
4'd4:sda_data <= 1'd0;
4'd5:sda_data <= 1'd0;
4'd6:sda_data <= 1'd0;
4'd7:sda_data <= 1'd0;
4'd8:sda_data <= 1'd0;//0表示写
4'd9:sda_data <= iic_data_addr[15];//下一位数据提前写进去
endcase
end
4'd1:begin//发送地址高位
case(cnt)
4'd0:sda_data <= iic_data_addr[15];
4'd1:sda_data <= iic_data_addr[14];
4'd2:sda_data <= iic_data_addr[13];
4'd3:sda_data <= iic_data_addr[12];
4'd4:sda_data <= iic_data_addr[11];
4'd5:sda_data <= iic_data_addr[10];
4'd6:sda_data <= iic_data_addr[9];
4'd7:sda_data <= iic_data_addr[8];
4'd8:sda_data <= iic_data_addr[7];//下一位数据提前写进去
endcase
end
4'd2:begin//发送地址低位
case(cnt)
4'd0:sda_data <= iic_data_addr[7];
4'd1:sda_data <= iic_data_addr[6];
4'd2:sda_data <= iic_data_addr[5];
4'd3:sda_data <= iic_data_addr[4];
4'd4:sda_data <= iic_data_addr[3];
4'd5:sda_data <= iic_data_addr[2];
4'd6:sda_data <= iic_data_addr[1];
4'd7:sda_data <= iic_data_addr[0];
4'd8:sda_data <= 1'd0;//下一位数据提前写进去
endcase
end
4'd3:begin//发送起始信号和器件地址
case(cnt)
4'd0:sda_data <= 1'd1;//先拉高
4'd1:sda_data <= 1'd0;//起始位
4'd2:sda_data <= 1'd0;
4'd3:sda_data <= 1'd1;
4'd4:sda_data <= 1'd0;
4'd5:sda_data <= 1'd1;
4'd6:sda_data <= 1'd0;
4'd7:sda_data <= 1'd0;
4'd8:sda_data <= 1'd0;
4'd9:sda_data <= iic_rw;
endcase
end
4'd4:begin//接收数据
case(cnt)
4'd0:
if(sda_cnt == sda_max/2)begin
r_iic_data_r[7] <= dif_sda;
end
4'd1:
if(sda_cnt == sda_max/2)begin
r_iic_data_r[6] <= dif_sda;
end
4'd2:
if(sda_cnt == sda_max/2)begin
r_iic_data_r[5] <= dif_sda;
end
4'd3:
if(sda_cnt == sda_max/2)begin
r_iic_data_r[4] <= dif_sda;
end
4'd4:
if(sda_cnt == sda_max/2)begin
r_iic_data_r[3] <= dif_sda;
end
4'd5:
if(sda_cnt == sda_max/2)begin
r_iic_data_r[2] <= dif_sda;
end
4'd6:
if(sda_cnt == sda_max/2)begin
r_iic_data_r[1] <= dif_sda;
end
4'd7:
if(sda_cnt == sda_max/2)begin
r_iic_data_r[0] <= dif_sda;
end
4'd8:sda_data <= 1'd1;//发送应答信号
4'd9:sda_data <= 1'd0;//发送停止信号
endcase
end
endcase
end
else if(iic_stop == 1'd1)
sda_data <= 1'd1;//保持高电平
end
always@(*)begin
dif_sda <= SDA;
end
always@(*)begin
if(cnt == 4'd8 && sda_state == 4'd4)
iic_data_r <= r_iic_data_r;
end
//inout类型的数据使用三态门来实现,1'bz为高阻态
//inout只能使用assign赋值
assign SDA = write_done ? 1'bz : sda_data;
endmodule
iic_driver_tb.v
/*
功能:IIC驱动模块设计
作者:王志川
时间:2024.7.5
*/
`timescale 1ns/1ps
module iic_driver_tb();
reg clk;
reg rst;
reg iic_rw;
reg iic_start;
reg [15:0]iic_data_addr;
reg [7:0]iic_data_w;
wire [7:0]iic_data_r;
wire [3:0]read_flag;
wire iic_stop;
wire write_done;
wire sda;
wire scl;
iic_driver iic_driver(
.CLK(clk), //50MHZ时钟信号
.RST(rst), //复位信号
.SDA(sda), //IIC数据引脚
.iic_rw(iic_rw), //IIC读写控制信号,1为读,0为写
.iic_start(iic_start), //IIC起始信号,为1有效
.iic_data_addr(iic_data_addr), //IIC要写数据的地址
.iic_data_w(iic_data_w), //IIC要写的数据
.iic_data_r(iic_data_r), //IIC读出的数据
.read_flag(read_flag), //读数据状态标志
.iic_stop(iic_stop), //IIC停止信号
.write_done(write_done), //IIC一次8位数据写完成标志位
.SCL(scl) //IIC时钟引脚
);
initial clk=1;
always #10 clk=!clk;
always@(*)begin
if(iic_stop == 1'd1)
iic_start <= 1'd0;
end
reg data;//应答数据
reg [7:0]r_data = 8'b1111_0000;//要发送的数据
always@(*)begin
if(iic_rw == 1'd1 && read_flag != 4'd0)
data = r_data[4'd8 - read_flag];
else
data = 1'b0;
end
//inout类型的数据使用三态门来实现
assign sda = write_done? data : 1'bz;
initial begin
rst=0;
iic_start=0;
iic_rw=0;//读写切换
iic_data_addr = 16'b0110_0001_0110_0001;
iic_data_w = 8'b0101_1001;
#201;
rst=1;
iic_start=1;
#5000_000;
$stop;
end
endmodule