绪论:
IIC总线是一种常用的片级总线,它集成于很多器件当中。在构造一个FPGA系统框架中,往往会使用IIC总线对存储器等一些重要的外围器件进行读写。而在这种应用场景之下,IIC主机数据接收发送控制器就成为FPGA工程师所必须掌握的模块。本文设计了一种可进行读写双向操作的IIC主机控制器,但由于未根据传输数据量大小设立足够的缓冲区,因此通过了指示信号来进行信号传输沟通。当实际应用时,使用者可根据实际数据大小标定缓冲区,从而减少控制信号标定带来的复杂性。另外,IIC总线控制器实际上是一个低速设备,由于当中必须使用到计数器,因此会造成关键路径上的大延时。如果需要使整个系统运行在更高的时钟频率下,请使用者使用锁相环为该模块分频出一个小频率时钟,并通过FIFO连接到FPGA内的其它模块。
一、IIC总线
1.概论
IIC总线介绍:https://blog.csdn.net/qq_38410730/article/details/80312357
I²C(Inter-Integrated Circuit)即集成电路总线,它是一种串行通信总线,使用多主从架构,实现白双工通信。IIC总线最主要的优点是其简单性和有效性。由于接口直接在组件之上,因此IIC总线占用的空间非常小,减少了电路板的空间和芯片管脚的数量,降低了互联成本。总线的长度可高达25英尺,并且能够以10Kbps的最大传输速率支持40个组件。IIC总线支持多主控, 其中任何能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。
IIC串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL,其时钟信号是由主控器件产生。所有接到IIC总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。对于并联在一条总线上的每个IC都有唯一的地址。
通常,数据线SDA和时钟线SCL都是处于上拉电阻状态。因为:在总线空闲状态时,这两根线一般被上面所接的上拉电阻拉高,保持着高电平。IIC上,SDA传输的有效数据只能在SCL低电平时改变。在SCL为高时,SDA上的电平代表着数据的高低。
2.起始信号与终止信号时序图
- 起始信号:当时钟线SCL为高期间,数据线SDA由高到低的跳变;启动信号是一种电平跳变时序信号,而不是一个电平信号;
- 停止信号:当时钟线SCL为高期间,数据线SDA由低到高的跳变;停止信号也是一种电平跳变时序信号,而不是一个电平信号。
3.主机读写时序图
如图所示,IIC数据传输为大端模式。其中DEVICE ADDRESS为所要交互的从机地址,地址的最低位表示着读写操作(0写1读)。图上所示的即为主机写从机操作,主机从START开始状态出发,逐位传送一字节数据,等待从机应答信号ACK(ACK为0则应答,为1则一边表示接收有误)。之后,WORD ADDRESS可以为数据,也可以为地址。图上标注为地址是因为它是用于写存储器,故而将第一字节数据作为地址,第二字节作为实际需写入的数据。
同理可得,读操作即为DEVICE ADDRESS传输完毕后,从机应答。主机将SDA总线交给从机控制,主机自己控制SCL时钟信号,并在接收到从机一字节数据后向其提供ACK应答。当主机不想继续从从机读取数据(突发),主机在ACK应答1,然后发出STOP信号。当为正常约定时,主机ACK回复0,然后主机拉STOP信号。
二、FPGA程序设计
IIC总线主机控制器是以状态机的形式进行数据读写。由于采用了简化性操作,状态机的状态分量比较少,在这里就不予给出。读者可自行阅读注释完备的代码,自己绘制出状态转移图。读者有兴趣可以对下列代码进行仿真,并注意inout口的问题。
1.程序代码
module IIC(
clk,rst_n,sda,scl,data_rd,data_in,data_v,wr,rd
);
///
//定位:主机控制模块
//发送设计目标:可整体发送一帧数据码,数据帧由地址信息作为起始,每节数据依赖于data_v进行识别采样;
//接收设计目标:可连续接收数据,并利用rd脉冲标记接收缓冲数据可被读出
//硬件时钟为50MHz,SCL通讯时钟100KHz,可通过宏定义修改
//不对传输结果有效性进行判断、不对总线冲突进行仲裁
//IIC总线通信应当提前约定好接收/发送数据每帧的数据长度
//本设计通过宏定义规定:
// 发送数据:操作地址(非设备地址)为8位,数据量为16bit分两次发送
// 接受数据:操作地址为8位,数据量为16bit分两次接收
//由于IIC为低速总线,建议使用PLL分频较低时钟控制,并在末端配置FIFO以降低其对整体设计性能的影响
//IIC总线数据传递形式为大端,请注意读写顺序
///
input clk,rst_n;
inout sda; //数据接收发送口
output scl; //IIC数据时钟线
output[7:0]data_rd; //数据接收后并行输出
output rd; //接收数据可读出脉冲
input[7:0]data_in; //发送数据输入
input data_v; //发送数据输入指示脉冲
output wr; //发送数据缓冲区可写入数据
///
//IIC模块输出信号定义
reg scl;
reg rd;
reg[7:0]data_rd;
reg wr;
wire sda;
//
reg SDA; //sda信号的中介信号,inout综合为三台门,不应直接用寄存器存z值
reg link; //sda传输方向控制信号
//0:写
//1:读
reg[7:0]data_t; //寄存需要向外发送的数据
reg[7:0]data_r; //接收缓冲寄存器
///
parameter END_FRAME = 8'hff; //发送数据停止帧,IIC接收到该帧后停止数据发送
//也可以提前约定好数据帧的长度,利用计数器判定。
//在一般使用中应当提前约定好一次发送的数据长度
reg SDA_link; //SDA读/写方向控制
reg[3:0]cnt; //发送/接收一次的8位数据计数
reg[3:0]cnt_frame; //发送/接收数据帧数计数,如提前约定好可以使用状态机分量实现,避免计数器带来的时钟瓶颈
parameter FRAME_R = 4'd1; //主机一次接收一帧数据,第一帧为从机地址,第二帧为从机发送的数据
parameter FRAME_T = 4'd2; //主机一次发送两帧数据,包含第一帧的设备地址
//reg[7:0]add_dev; //本次发送的从机地址:使用发送数据寄存器替代
///
//状态机参数定义
//如需操作存储器,添加已注释的状态可使整个过程更清晰
reg[3:0]STATE; //状态机
parameter IDLE = 4'b0000; //空闲状态
parameter START = 4'b0001; //开始信号生成状态
parameter ADD_DEV = 4'b0010; //从机地址数据发送状态:一次发送8位地址
parameter ACK_DEV_ADD = 4'b0011; //从机设备地址应答识别状态
//parameter ADD_WORD = 4'b0100; //操作字地址发送状态:可以根据实际地址长度进行修改,本设计以8位地址为例
//parameter ACK_WORD_ADD = 4'b0101; //操作字地址应答与读写识别状态
parameter DATA_T = 4'b0110; //数据发送状态
parameter ACK_DATA_T = 4'b0111; //数据发送应答接收状态
//parameter DEV_ADD_R = 4'b1000; //从机设备地址接收状态
//parameter ACK_DEV_R = 4'b1001; //应答从机设备地址状态
parameter DATA_R = 4'b1010; //数据接收状态
parameter ACK_DATA_R = 4'b1011; //数据接收应答状态
parameter STOP = 4'b1100; //发送停止信号生成状态
///
///
//SCL时钟线状态定义与延时控制模块
//由于设定中时钟频率为50MHz,通信频率100KHz,因此分频计数500,4个阶段各分配125
`define SCL_POS (cnt_delay == 9'd499) //延时计数器计数值为499时,SCL拉高
`define SCL_HIG (cnt_delay == 9'd124) //延时计数器计数值为124时,SCL为稳定高电平,此时可以读取SDA上的数据
`define SCL_NEG (cnt_delay == 9'd249) //延时计数器计数值为249时,SCL为拉低
`define SCL_LOW (cnt_delay == 9'd374) //延时计数器计数值为374时,SCL为稳定低电平,此时可以改变SDA上发送的数据
reg[8:0]cnt_delay; //延时计数器
//延时计数器模块
always@(posedge clk or negedge rst_n)
if(!rst_n) cnt_delay <= 9'd0;
else if(cnt_delay == 9'd499) cnt_delay <= 9'd0;
else cnt_delay <= cnt_delay + 9'd1;
//scl电平控制
always@(posedge clk or negedge rst_n)
if(!rst_n) scl <= 1'b1;
else
case(STATE)
IDLE : begin
scl <= 1'b1; //空闲状态,scl拉高
end
START : begin
if(`SCL_HIG) scl <= 1'b1; //开始状态,scl拉高,等待计数器计数到sdr拉低,通讯开始
end
STOP : begin
if(`SCL_POS) scl <= 1'b1; //通讯结束状态,scl拉高
end
default : begin
if(`SCL_POS) scl <= 1'b1; //其余状态遵循计数器宏定义状态改变
else if(`SCL_NEG) scl <= 1'b0;
else scl <= scl;
end
endcase
///
//输入数据监测模块:生成数据有效标志,通过检测外部输入数据有效标志上升沿
reg flag_data_in; //并行输入数据有效标志
reg data_v_1; //外部输入数据有效标志寄存
always@(posedge clk or negedge rst_n)
if(!rst_n)
begin
flag_data_in <= 1'b0;
data_v_1 <= 1'b0;
end
else
begin
data_v_1 <= data_v;
flag_data_in <= ~data_v_1 & data_v;
end
///
//模块控制状态机转移控制模块
always@(posedge clk or negedge rst_n)
if(!rst_n) STATE <= 4'b0000;
else if(STATE == IDLE) //空闲状态
begin
if(flag_data_in) STATE <= START; //如果检测到有数据需要通过IIC模块发出,状态转移为START
else STATE <= IDLE; //否则继续保持空闲状态
end
else if(STATE == START) //开始信号生成状态
begin
if(`SCL_HIG) STATE <= ADD_DEV; //如果scl下降沿已生成,状态转移至ADD_DEV
else STATE <= START;
end
else if(STATE == ADD_DEV) //设备地址发送状态
begin
if(cnt == 4'd8&&`SCL_LOW) STATE <= ACK_DEV_ADD; //8位地址(7位设备地址加一位读写标志)发送完毕,转入ACK_DEV_ADD状态
else STATE <= ADD_DEV; //未发送完毕,继续保持
end
else if(STATE == ACK_DEV_ADD) //从机设备地址接收应当状态
begin
if(`SCL_NEG) //在SCL下降沿改变状态
begin
if(data_t[0]) //设备地址最低位位读写标志,1位读,0为写
STATE <= DATA_R; //转移至DATA_R
else
STATE <= DATA_T; //转移至DATA_T
end
else
STATE <= ACK_DEV_ADD;
end
else if(STATE == DATA_T) //主机数据发送状态
begin
if(cnt == 4'd8 && `SCL_LOW) //8位数据发送完毕且处于SCL低电平
STATE <= ACK_DATA_T; //状态转移至ACK_DATA_T
else STATE <= DATA_T;
end
else if(STATE == ACK_DATA_T) //主机数据发送应答状态
begin
if(`SCL_NEG) //在SCL下降沿时检测
begin
if(cnt_frame == (FRAME_T-1)) //当已发送指定帧数的数据
STATE <= STOP; //状态转移至STOP
else STATE <= DATA_T; //否则,状态转移至DATA_T,继续进行数据发送
end
else STATE <= ACK_DATA_T;
end
else if(STATE == DATA_R) //主机数据接收状态
begin
if(cnt == 4'd8 && `SCL_LOW) //8位数据接收完毕且处于SCL低电平
STATE <= ACK_DATA_R; //状态转移至ACK_DATA_R
else STATE <= DATA_R;
end
else if(STATE == ACK_DATA_R) //主机接收数据应答状态
begin
if(`SCL_NEG) //SCL下降沿检测
begin
if(cnt_frame == (FRAME_R-1)) //当已接收指定帧数数据
STATE <= STOP; //状态转移至STOP
else STATE <= DATA_R; //否则继续接收下一阵数据
end
else STATE <= ACK_DATA_R;
end
else if(STATE == STOP) //数据发送/接收终止状态
begin
if(`SCL_HIG) //当SCL抵达高电平时
STATE <= IDLE; //状态转移至IDLE
else STATE <= STOP;
end
else STATE <= IDLE; //默认状态为IDLE
//状态机状态操作模块
always@(posedge clk or negedge rst_n)
if(!rst_n)
begin
cnt <= 4'd0; //发送/接收一次的8位数据计数
cnt_frame <= 4'd0; //发送/接收数据帧数计数
SDA <= 1'b1; //写加载串行数据
link <= 1'b0; //sda传输方向控制信号,0写1读
wr <= 1'b1; //发送数据缓冲区可写入数据
rd <= 1'b0; //接收数据可读出脉冲
data_t <= 8'd0; //寄存需要向外部发送的数据
data_r <= 8'd0; //读取接收的数据
data_rd <= 8'd0; //接收的数据寄存
end
else
case(STATE)
IDLE : begin //空闲状态,SDA拉高,IIC等待内部数据到来
cnt <= 4'd0;
cnt_frame <= 4'd0;
SDA <= 1'b1;
link <= 1'b0;
rd <= 1'b0;
data_t <= 8'd0;
if(flag_data_in) wr <= 1'b0;
end
START : begin //开始状态:向外发送起始信息
if(`SCL_HIG) //当计数器计数到scl为高时,sdr上的数据拉低,通讯开始
begin
link <= 1'b0; //sda传输向外写
SDA <= 1'b0; //写数据为0
data_t <= data_in; //发送数据寄存
wr <= 1'b0; //不接受新的数据进入缓冲
//在约定数据深度情况下,可以一步到位锁存,降低对FPGA内部其它模块的干扰
rd <= 1'b0; //不接受数据被读取
end
end
ADD_DEV : begin //设备地址发送状态:IIC通讯必须以此开始确定通讯对象及读写操作
if(`SCL_LOW) //SCL低电平时改变数据
begin
if(cnt == 4'd8) //8bit发送完毕后
begin
cnt <= 4'd0; //8位数据计数清零
SDA <= 1'b1; //SDR输出数据赋1,但此时为输入高阻状态
link <= 1'b1; //SDR输入状态
if(data_t[0]) wr <= 1'b0;
else wr <= 1'b1; //发送缓冲区可写入数据
rd <= 1'b0; //接收缓冲区不可被读
end
else
begin
cnt <= cnt + 4'd1; //位计数器计数
link <= 1'b0; //SDR输出状态
case(cnt)
4'd0 : SDA <= data_t[7];
4'd1 : SDA <= data_t[6];
4'd2 : SDA <= data_t[5];
4'd3 : SDA <= data_t[4];
4'd4 : SDA <= data_t[3];
4'd5 : SDA <= data_t[2];
4'd6 : SDA <= data_t[1];
4'd7 : SDA <= data_t[0];
default : SDA <= 1'b1;
endcase
end
end
end
ACK_DEV_ADD : begin //设备地址接收应答状态
cnt <= 4'd0; //位发送接收计数器清零
if(flag_data_in) //有数据需要发送,则赋值到寄存器内
begin
data_t <= data_in; //由于下一个状态也可能是数据接收,因此本步不一定运行
cnt_frame <= 4'd1; //接收到一帧发送数据,计数器赋1
wr <= 1'b0; //数据不可写入发送缓冲寄存器
rd <= 1'b0; //接收缓冲寄存器不可读
end
end
DATA_T : begin //数据发送状态
if(`SCL_LOW) //SCL低电平时改变数据
begin
if(cnt == 4'd8) //8bit发送完毕后
begin
cnt <= 4'd0; //8位数据计数清零
SDA <= 1'b1; //SDR输出数据赋1,但此时为输入高阻状态
link <= 1'b1; //SDR输入状态
wr <= 1'b1; //发送缓冲区可写入数据
rd <= 1'b0; //接收缓冲区不可被读
end
else
begin
cnt <= cnt + 4'd1; //位计数器计数
link <= 1'b0; //SDR输出状态
case(cnt)
4'd0 : SDA <= data_t[7];
4'd1 : SDA <= data_t[6];
4'd2 : SDA <= data_t[5];
4'd3 : SDA <= data_t[4];
4'd4 : SDA <= data_t[3];
4'd5 : SDA <= data_t[2];
4'd6 : SDA <= data_t[1];
4'd7 : SDA <= data_t[0];
default : SDA <= 1'b1;
endcase
end
end
end
ACK_DATA_T : begin //数据发送应答状态
cnt <= 4'd0; //位发送接收计数器清零
if(flag_data_in) //有数据需要发送,则赋值到寄存器内
begin
data_t <= data_in; //由于下一个状态也可能是数据接收,因此本步不一定运行
cnt_frame <= cnt_frame + 4'd1; //接收到一帧发送数据,计数器加1
wr <= 1'b0; //数据发送缓冲区不可写入
rd <= 1'b0; //数据接收缓冲
end
else
begin
cnt_frame <= cnt_frame; //接收到一帧发送数据,计数器加1
wr <= wr; //数据发送缓冲区不可写入
rd <= rd;
end
end
DATA_R : begin //数据接收状态
if(`SCL_HIG) //SCL高电平读取数据
begin
if(cnt == 4'd8)
begin
cnt <= 4'd0; //8位数据计数清零
SDA <= 1'b1; //SDR输出数据赋1,但此时为输入高阻状态
link <= 1'b1; //SDR接收状态,下一步需要应答,此时不可在SCL高电平输出应答信号
wr <= 1'b0; //发送缓冲区可写入数据
rd <= 1'b1; //接收缓冲区不可被读
data_rd <= data_r; //接受数据放入可读接口中
end
else
begin
wr <= 1'b0; //数据发送缓冲区不可写
rd <= 1'b0; //数据接收缓冲区不可读
cnt <= cnt + 1'b1; //位计数加1
case(cnt)
4'd0 : data_r[0] <= sda;
4'd1 : data_r[1] <= sda;
4'd2 : data_r[2] <= sda;
4'd3 : data_r[3] <= sda;
4'd4 : data_r[4] <= sda;
4'd5 : data_r[5] <= sda;
4'd6 : data_r[6] <= sda;
4'd7 : data_r[7] <= sda;
default : data_r <= 4'd0;
endcase
end
end
end
ACK_DATA_R : begin //数据接收应答状态
cnt <= 4'd0; //8位数据计数器清零
link <= 1'b0; //SDA向外写
SDA <= 1'b0; //SDA拉低
wr <= 1'b0; //数据发送缓冲区不可写
rd <= 1'b1; //数据接收缓冲区可读
if(`SCL_NEG) //SCL低电平
begin
if(cnt_frame == FRAME_R) //SCL下降沿
begin
cnt_frame <= 4'd0; //帧计数器清零
end
else
begin
cnt_frame <= cnt_frame + 4'd1; //帧计数器加1
end
end
end
STOP : begin //停止发送/接收状态
wr <= 1'b0; //不可写入数据
rd <= 1'b0; //不可读出数据
if(`SCL_LOW)
begin
link <= 0; //向外写数据
SDA <= 1'b0; //拉高SDA
end
else if(`SCL_HIG) //SCL高电平状态
begin
link <= 0; //向外写数据
SDA <= 1'b1; //拉高SDA
end
end
default : begin
cnt <= 4'd0; //发送/接收一次的8位数据计数
cnt_frame <= 4'd0; //发送/接收数据帧数计数
SDA <= 1'b0; //写加载串行数据
link <= 1'b0; //sda传输方向控制信号,0写1读
wr <= 1'b1; //发送数据缓冲区可写入数据
rd <= 1'b0; //接收数据可读出脉冲
data_t <= 8'd0; //待发送数据缓冲区
end
endcase
///
assign sda = link ? 1'bz : SDA; //读时sda高阻,写时为SDA加载数据
///
endmodule
2.主机写仿真波形
3.主机读仿真波形