大侠好,欢迎来到FPGA技术江湖,江湖偌大,相见即是缘分。大侠可以关注FPGA技术江湖,在“闯荡江湖”、"行侠仗义"栏里获取其他感兴趣的资源,或者一起煮酒言欢。
今天给大侠带来基于FPGA的IIC设计,附源码,获取源码,请在“FPGA技术江湖”公众号内回复“IIC设计源码”,可获取源码文件。话不多说,上货。
设计原理
IIC(Inter-Integrated Circuit),其实是IIC Bus简称,中文就是集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。I²C的正确读法为“I平方C”("I-squared-C"),而“I二C”("I-two-C")则是另一种错误但被广泛使用的读法。自2006年10月1日起,使用I²C协议已经不需要支付专利费,但制造商仍然需要付费以获取I²C从属设备地址。
IIC 简单来说,就是一种串行通信协议,IIC 的通信协议和通信接口在很多工程中有广泛的应用,如数据采集领域的串行 AD,图像处理领域的摄像头配置,工业控制领域的 X 射线管配置等等。除此之外,由于 IIC协议占用的 IO 资源特别少,连接方便,所以工程中也常选用 IIC 接口做为不同芯片间的通信协议。
IIC串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL。所有接到IIC总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。
IIC电路原理图如下:
24LC64各引脚定义:
1、A0,A1,A2 为 24LC64 的片选信号,由于 IIC 总线可以挂载多个 IIC 接口器件,所以每个器件都应该有自己的“身份标识”,通过对 A0,A1,A2 输入不同的高低电平,就可以设置该 EEPROM 的片选信号。
2、WP 为读写使能信号,当 WP 悬空或者接地,EEPROM 可读可写,当 WP 接电源,EEPROM 只能读不能写。
3、SCL为 IIC 接口的时钟线。
4、SDA为 IIC 接口的数据线。
IIC 接口的读写时序:
IIC接口读写时序分为随机读写(单字节读写)和页面读写(多字节读写),先分析随机读写(Byte Write/Read)时序。
Byte Write 时序如下图:
时序分析:如果我们要向 EEPROM 写入一个字节,那么必须经过以下步骤:
1. 发送启动信号
2. 发送控制字
3. 接收并检测 EEPROM 发来的应答信号 ACK
4. 发送高字节地址位
5. 接收并检测 EEPROM 发来的应答信号 ACK
6. 发送低字节地址位
7. 接收并检测 EEPROM 发来的应答信号 ACK
8. 发送 8bit 有效数据
9. 接收并检测 EEPROM 发来的应答信号 ACK
10.发送停止信号
Byte Read 时序如下:
时序分析:如果我们要从 EEPROM 读出一个字节,那么必须经过以下步骤:
1. 发送启动信号
2. 发送控制字 1010_A2A1A0_0
3. 接收并检测 EEPROM 发来的应答信号 ACK
4. 发送高字节地址位
5. 接收并检测 EEPROM 发来的应答信号 ACK
6. 发送低字节地址位
7. 接收并检测 EEPROM 发来的应答信号 ACK
8. 发送启动信号
9. 发送控制字 1010_A2A1A0_1
10. 接收并检测 EEPROM 发来的应答信号 ACK
11. 读取一个字节数据
12. 发送 NO ACK 信号
13. 发送停止信号
接下来则需要分析各步骤具体意义:
1.启动信号
在 SCL 保持高电平期间,如果 SDA 出现由高到低的跳变沿,代表启动信号。
2. 控制字
我们的控制字为 1010_0000,其中 1010 为 EEPROM 的型号标识,为一组固定的序列,紧接着 A2,A1,A0 就是我们的片选信号,最后一位为读写控制位,低电平代表写,高电平代表读,我们这里首先需要对 EEPROM 写入地址位,所以我们最后一位为 0。
3. 高/低位地址
由于 24LC64 有 64Kbit 的存储空间,所以我们需要 13 位的地址位宽才能寻址所有的存储空间,由于 IIC 协议规定只能以字节形式写入,所以必须将 13 位的地址扩展为 16 位的地址,分为高八位和低八位,多出来的前三位填充任意数据即可,对我们的寻址地址没有影响。
4. 停止信号
5. 应答信号 ACK
应答信号是由数据接收方发出的,当 SCL 为高电平期间,如果监测到 SDA为低电平,说明有应答信号。
6. 非应答信号 NO ACK
非应答信号也是由数据接收方发出的,当 SCL 为高电平期间,如果 SDA 为高电平,说明有非应答信号。
说明:由于 IIC 总线协议启动和停止信号都是在 SCL 高电平期间发生跳变,这就决定了我们其他数据的改变只能发生在 SCL 低电平期间,在 SCL 为高电平期间,数据必须保持稳定。即在 SCL 低电平改变数据,在 SCL 高电平采集数据。
相比于单字节读写,页面读写只是增加了几个状态,具体时序如下,这里和后面的设计代码不做详细论述。
Page Write 时序如下:
Page Read 时序如下:
设计架构
本设计用两个按键控制 EEPROM 读写,当写按键按下时,向 EEPROM 某一固定地址写入一个字节数据,当读按键按下时,将该地址数据读出,并显示到数码管,LED 灯是一个标志信号,LED 亮说明数据写入完毕。设计架构如下:
设计代码
IIC顶层模块代码:
代码语言:javascript复制module IIC(
clk_sys,
rst_n,
eeprom_scl,
eeprom_sda,
key_wr,
key_rd,
oseg,
sel,
led
);
//--------输入端口--------
input clk_sys;
input rst_n;
input key_wr;
input key_rd;
//--------输出端口--------
output eeprom_scl;
output [7:0]oseg;
output [2:0]sel;
output led;
//--------双向总线--------
inout eeprom_sda;
//--------内部连线--------
wire [7:0]result;
//--------实例化eeprom控制模块--------
iic_wr iic_wr (
.clk_sys(clk_sys),
.rst_n(rst_n),
.eeprom_scl(eeprom_scl),
.eeprom_sda(eeprom_sda),
.key_wr(key_wr),
.key_rd(key_rd),
.result(result),
.led(led)
);
//--------实例化数码管显示驱动模块--------
seg7_lut seg7_lut (
.oseg(oseg),
.idig(result),
.clk_sys(clk_sys),
.sel(sel)
);
endmodule
iic_wr模块代码:负责进行IIC数据的读写
代码语言:javascript复制module iic_wr(
clk_sys, //系统时钟
rst_n, //系统复位
eeprom_scl,//eeprom 串行时钟信号
eeprom_sda,//eeprom 串行数据信号
key_wr, //外部写控制按键
key_rd, //外部读控制按键
result, //数据采集结果寄存器
led //led指示灯
);
input clk_sys;
input rst_n;
input key_wr;
input key_rd;
output reg eeprom_scl;
inout eeprom_sda;
output reg led;
reg clk;
reg [7:0]cnt; //分频计数器
reg [7:0]state; //状态寄存器
reg [3:0]counter;//数据移位计数器
reg link_sda; //总线开关
reg wr; //写标志寄存器
reg rd; //读标志寄存器
reg sda_buf; //总线数据缓存器
output [7:0]result;//最终输出结果寄存器
reg [7:0]result_1;//第一个数据寄存器
reg [7:0]result_2;//第二个数据寄存器
reg [7:0]data; //待发送控制字、地址、数据寄存器
assign eeprom_sda=(link_sda)?sda_buf:1'hz;
assign result=result_1 result_2;
//-----------------system clk----------------
//系统时钟分频
always @(posedge clk_sys or negedge rst_n)
begin
if(!rst_n)
begin
clk<=0;
cnt<=0;
end
else
begin
if(cnt<250)
cnt<=cnt 1'b1;
else
begin
clk<=~clk;
cnt<=0;
end
end
end
//-----------------eeprom scl-----------------
//产生eeprom scl信号
always @(negedge clk or negedge rst_n)
begin
if(!rst_n)
begin
eeprom_scl<=0;
end
else
eeprom_scl<=~eeprom_scl;
end
//----------------- eeprom contral-----------
always @(posedge clk or negedge rst_n)
begin
if(!rst_n) //所有寄存器复位
begin
state<=0;
link_sda<=0;
sda_buf<=0;
counter<=0;
wr<=0;
led<=1;
rd<=0;
result_1<=0;
result_2<=0;
data<=0;
end
else
begin
case(state)
//--------------send start singial-------------
0:begin
if(!key_wr) //检测外部写控制按键是否按下
wr<=1;
if(!key_rd) //检测外部读控制按键是否按下
rd<=1;
if(((rd==1)||(wr==1))&&(!eeprom_scl))
begin
link_sda<=1;
sda_buf<=1;
state<=1;
end
end
1:begin
if(eeprom_scl)//高电平期间,使sda由高变低,启动串行传输
begin
sda_buf<=0;
state<=2;
data<=8'b10100000;//写控制字准备
end
end
//--------------send countral word--------------
2:begin
if((counter<8)&&(!eeprom_scl))//在scl低电平期间,完成并串转换,发出写控制字
begin
counter<=counter 1'b1;
data<={data[6:0],data[7]};
sda_buf<=data[7];
end
else if((counter==8)&&(!eeprom_scl))
begin
counter<=0;
state<=3;
link_sda<=0;//FPGA释放总线控制权
end
end
//--------------receive ack singial--------------
3:begin
if(eeprom_scl)//在scl高电平期间,检测是否有应答信号
begin
if(!eeprom_sda)
begin
state<=4;//有应答则状态继续跳转
data<=8'b00000000;//高字节地址准备
end
end
end
//--------------send high Byte address--------------
4:begin
link_sda<=1;//FPGA控制总线
if((counter<8)&&(!eeprom_scl))//在scl低电平期间,完成并串转换,发出高字节地址
begin
counter<=counter 1'b1;
data<={data[6:0],data[7]};
sda_buf<=data[7];
end
else if((counter==8)&&(!eeprom_scl))
begin
counter<=0;
state<=5;
link_sda<=0;//FPGA释放总线控制权
end
end
//--------------receive ack singial--------------
5:begin
if(eeprom_scl)//在scl高电平期间,检测是否有应答信号
begin
if(!eeprom_sda)
begin
state<=6;//有应答则状态继续跳转
data<=8'b00000011;//低字节地址准备
end
end
end
//--------------send low Byte address--------------
6:begin
link_sda<=1;//FPGA控制总线
if((counter<8)&&(!eeprom_scl))//在scl低电平期间,完成并串转换,发出低字节地址
begin
counter<=counter 1'b1;
data<={data[6:0],data[7]};
sda_buf<=data[7];
end
else if((counter==8)&&(!eeprom_scl))
begin
counter<=0;
state<=7;
sda_buf<=1;
link_sda<=0;//FPGA释放总线控制权
end
end
//--------------receive ack singial--------------
7:begin
if(eeprom_scl)//在scl高电平期间,检测是否有应答信号
begin
if(!eeprom_sda)
begin
if(wr==1)//如果是写的话,跳到状态8,遵循页面写时序
state<=8;
if(rd==1)//如果是读的话,跳到状态11,遵循页面读时序
begin
state<=13;
sda_buf<=1;//准备再次发启动信号
end
data<=8'b00000001;//准备想要写入的第一个数据
end
end
end
//--------------send active data_1-------------
8:begin
link_sda<=1;//FPGA控制总线
if((counter<8)&&(!eeprom_scl))//在scl低电平期间,完成并串转换,发出第一个有效数据
begin
counter<=counter 1'b1;
data<={data[6:0],data[7]};
sda_buf<=data[7];
end
else if((counter==8)&&(!eeprom_scl))
begin
counter<=0;
state<=9;
link_sda<=0;//FPGA释放总线控制权
end
end
//--------------receive ack singial--------------
9:begin
if(eeprom_scl)//在scl高电平期间,检测是否有应答信号
begin
if(!eeprom_sda)//有应答则状态继续跳转
begin
state<=10;
data<=8'b00000010;//准备想要写入的第二个数据
end
end
end
//--------------send active data_2-------------
10:begin
link_sda<=1;//FPGA控制总线
if((counter<8)&&(!eeprom_scl))//在scl低电平期间,完成并串转换,发出有效数据
begin
counter<=counter 1'b1;
data<={data[6:0],data[7]};
sda_buf<=data[7];
end
else if((counter==8)&&(!eeprom_scl))
begin
counter<=0;
state<=11;
link_sda<=0;//FPGA释放总线控制权
end
end
//--------------receive ack singial--------------
11:begin
if(eeprom_scl)//在scl高电平期间,检测是否有应答信号
begin
if(!eeprom_sda)//有应答则状态继续跳转
begin
state<=12;
end
end
end
//--------------send stop singial-------------
12:begin
link_sda<=1;//FPGA控制总线
sda_buf<=0;//拉低sda,准备发出停止信号
if(eeprom_scl)//在scl高电平期间,拉高sda,终止串行传输
begin
led<=0;//点亮led,说明写操作完毕
sda_buf<=1;
if(key_wr && key_rd)//在按键放开以后才跳转回空闲状态,避免不断循环写入
state<=0;//状态跳回
wr<=0;//清除写控制标志
end
end
//------------send start singial-----------
13:begin
link_sda<=1;//FPGA控制总线
if(eeprom_scl)//scl高电平期间拉低sda,发送启动信号
begin
sda_buf<=0;
state<=14;
data<=8'b10100001;//读控制字准备
end
end
//------------send countral word------------
14:begin
if((counter<8)&&(!eeprom_scl))//在scl低电平期间,完成并串转换,发出读控制字
begin
counter<=counter 1'b1;
data<={data[6:0],data[7]};
sda_buf<=data[7];
end
else if((counter==8)&&(!eeprom_scl))
begin
counter<=0;
state<=15;
link_sda<=0;//FPGA释放总线控制权
end
end
//--------------receive ack singial--------------
15:begin
if(eeprom_scl)//在scl高电平期间,检测是否有应答信号
begin
if(!eeprom_sda)//有应答则状态继续跳转
state<=16;
end
end
//--------------receive input active data--------------
16:begin
if((counter<8)&&(eeprom_scl))//在scl高电平期间,完成串并转换,存储第一个接收数据
begin
counter<=counter 1'b1;
result_1[7-counter]<=eeprom_sda;
end
else if(counter==8)
begin
counter<=0;
state<=17;
sda_buf<=0; //拉低sda,准备发送应答信号
link_sda<=1;//接收完毕以后FPGA继续控制总线
end
end
//--------------send ACK singial--------------
17:begin
if(eeprom_scl)//在scl高电平期间,发送应答信号
begin
sda_buf<=0;
state<=18;
end
end
//------------receive input active data--------------
18:begin
link_sda<=0;
if((counter<8)&&(eeprom_scl))//在scl低电平期间,完成串并转换,存储第二个接收数据
begin
counter<=counter 1'b1;
result_2[7-counter]<=eeprom_sda;
end
else if(counter==8)
begin
counter<=0;
state<=19;
sda_buf<=1; //准备发送no ack信号
link_sda<=1;//接收完毕以后FPGA继续控制总线
end
end
//--------------send NO ACK singial--------------
19:begin
if(eeprom_scl)//在scl高电平期间,发送非应答信号
begin
sda_buf<=1;
state<=20;
end
end
//--------------send stop singial-------------
20:begin
if(!eeprom_scl)//在scl低电平期间,将sda总线拉低,准备发送停止信号
sda_buf<=0;
if(eeprom_scl)//在scl高电平期间,将sda总线拉高,发出停止信号
begin
sda_buf<=1;//拉高sda
state<=0;//状态回转
rd<=0;//清除读标志信号
end
end
default:state<=0;
endcase
end
end
endmodule
seg7_lut模块代码:负责数码管显示
代码语言:javascript复制module seg7_lut(
oseg, //数码管段选
idig, //驱动数据
clk_sys,//系统时钟
sel, //数码管位选
rst_n //系统复位
);
//--------输入信号--------
input [7:0] idig;
input clk_sys;
input rst_n;
//--------输出信号--------
output reg[7:0] oseg;
output reg[2:0] sel;
//--------中间变量--------
reg [3:0]cnt;
//-----------------系统时钟分频-----------------
always @(posedge clk_sys or negedge rst_n)
begin
if(!rst_n)
begin
sel<=3'd0;//数码管段选定位
end
end
//--------数码管显示译码--------
always@(*)
case(idig)
8'h0:oseg<=8'hC0;
8'h1:oseg<=8'hF9;
8'h2:oseg<=8'hA4;
8'h3:oseg<=8'hB0;
8'h4:oseg<=8'h99;
8'h5:oseg<=8'h92;
8'h6:oseg<=8'h82;
8'h7:oseg<=8'hF8;
8'h8:oseg<=8'h80;
8'h9:oseg<=8'h90;
8'hA:oseg<=8'h88;
8'hB:oseg<=8'h83;
8'hC:oseg<=8'hC6;
8'hD:oseg<=8'hA1;
8'hE:oseg<=8'h86;
8'hF:oseg<=8'h8E;
endcase
endmodule
仿真测试
tb顶层测试模块代码:
代码语言:javascript复制`timescale 1ns/1ns
module tb;
reg clk_sys;
reg rst_n;
reg key_wr;
reg key_rd;
wire eeprom_scl;
wire eeprom_sda;
wire [7:0]oseg;
wire [2:0]sel;
wire led;
initial
begin
clk_sys=0;
key_rd=1;
rst_n=0;
#1000 rst_n=1;
#500 key_wr=0;
#3000 key_wr=1;
# 400000 key_rd=0;
#300 key_rd=1;
end
always #10 clk_sys=~clk_sys;
IIC IIC(
.clk_sys(clk_sys),
.rst_n(rst_n),
.eeprom_scl(eeprom_scl),
.eeprom_sda(eeprom_sda),
.key_wr(key_wr),
.key_rd(key_rd),
.oseg(oseg),
.sel(sel),
.led(led)
);
endmodule
仿真图:
随机读写,仿真写时序:
随机读写,仿真读时序:
在仿真时,需要将检测应答的状态跳过,直接向下一状态跳转,观察读写时序,当读写按键按下时,都会产生对应的动作。
END
后续会持续更新,带来Vivado、 ISE、Quartus II 、candence等安装相关设计教程,学习资源、项目资源、好文推荐等,希望大侠持续关注。
大侠们,江湖偌大,继续闯荡,愿一切安好,有缘再见!