FPGA/数字IC求职笔试面试(2)之IIC协议的FPGA实现
文章目录
前言
书接上文,本文使用FPGA来实现IIC协议!该文章首发于公众号“FPGA学习者”,更多精彩内容敬请关注。
一、前情回顾
上期说到IIC协议的概述以及具体时序要求等等,本期将基于FPGA作为主控制器,EEPROM作为从机,实现FPGA与EEPROM之间的IIC通信协议。
二、编程思路
先来看两个时序图,从图中可以看到,无论是写入数据还是从EEPROM中读出数据,都需要一系列的较多重复的步骤,而每个步骤的核心部分就是8位数据的发送,相伴随着的是是否有起始位、是否有结束位、是否有应答位等等。
既然如此,那我们就可以先抽象出一个单独的关于8bit数据传输的模块,上层再去调用这个模块完成读写任务;
这里使用状态机的编写方式先去编写一个底层的i2c_bit_shift模块
该状态机有七个状态,分别为:
IDLE = 7'b0000001, //空闲状态
GEN_STA = 7'b0000010, //产生起始位状态
WR_DATA = 7'b0000100, //写数据状态
RD_DATA = 7'b0001000, //读数据状态
CHECK_ACK = 7'b0010000, //检测应答信号状态
GEN_ACK = 7'b0100000, //产生应答信号(主要用于读数据时FPGA给出反馈)
GEN_STO = 7'b1000000; //产生停止位状态
除此之外,该怎么确定是否经过其中的某些状态呢?可以通过定义不同的命令来指导状态的跳转方向,命令定义如下图所示:
localparam
WR = 6'b000001, //写请求
STA = 6'b000010, //起始位请求
RD = 6'b000100, //读请求
STO = 6'b001000, //停止位请求
ACK = 6'b010000, //应答位请求
NACK = 6'b100000; //无应答请求
例如在下图所示的步骤中,需要经历IDLE、GEN_STA、WR_DATA、CHECK_ACK、IDLE状态,则命令可以定义为:(STA | WR) ;
根据不同的部分需要的功能,命令设置不同,状态跳转路径也有所差异,最终得到的状态转换图如下:
还有一些注意事项:
1️⃣SCL时钟信号要满足400kHz左右,则需要通过系统时钟进行计数分频得到;
2️⃣SDA数据的变化,必须在SCL时钟为低电平的时候改变,SCL为高电平时必须保持稳定。
3️⃣SDA为双向数据线,在FPGA中需要使用三态门进行控制双向数据线,具体编程如下:
inout i2c_sdat;
//******************************//
reg i2c_sdat_o;
reg i2c_sdat_oe;
assign i2c_sdat = i2c_sdat_oe?i2c_sdat_o:1'bz;
//在上述程序中,当i2c_sdat_oe为高电平时,SDA信号线作为输出(对于FPGA来讲)。
//当i2c_sdat_oe为低电平时,SDA信号线为高阻态,其状态受从机影响,可以作为数据输入线。
经过上述处理方法,当SDA需要作为输出信号时,只需将i2c_sdat_oe置为高电平,然后操作i2c_sdat_o即可。
综上所述,该模块可以综合成如下图所示的模块框图:
上层模块需要调用上述模块去完成具体的读写任务,故编写i2c_control模块。
具体来说,上层模块需要完成什么任务呢?
①向0XA0这个器件的0Xxxxx地址单元写入数据0Xxx;步骤如下:
1️⃣CMD = STA | WR,Tx_data = dev_id;
2️⃣CMD = WR,Tx_data = addrH,
3️⃣CMD = WR,Tx_data = addrL,
4️⃣CMD = WR | STO, Tx_data = 0Xxx;
②从0XA0这个器件的0Xxxxx地址单元读取数据;步骤如下:
1️⃣CMD = STA | WR,Tx_data = dev_id;
2️⃣CMD = WR,Tx_data = addrH,
3️⃣CMD = WR,Tx_data = addrL,
4️⃣CMD = STA | WR,Tx_data = dev_id | 0X01;(确保最后一位为1)
5️⃣CMD = RD | NACK | STO,Rx_data = 0Xxx;
对于上层模块来说,同样使用状态机的方式去编程,状态定义如下:
localparam
IDLE = 7'b0000001, //空闲状态
WR_REG = 7'b0000010, //写请求状态
WAIT_WR_DONE = 7'b0000100, //等待写完成状态
WR_REG_DONE = 7'b0001000, //写请求完成状态
RD_REG = 7'b0010000, //读请求状态
WAIT_RD_DONE = 7'b0100000, //等待读完成状态
RD_REG_DONE = 7'b1000000; //读完成状态
电路综合后的状态转换图如上图所示。该模块的框图如下图所示:
三、仿真验证
1 i2c_bit_shift仿真验证
在验证过程中,加载了一个M24LC048文件,该模块可以模拟EEPROM模块产生应答信号等需求。从而简化我们的testbench程序编写。
testbench程序中只需模拟产生命令信号、器件地址信号、写入/读出的地址单元信号即可,可以使用task任务来讲写入过程或者读取过程编写为单独一个任务。具体程序如下:
task write_one_byte;
input [7:0]device_id;
input [7:0]mem_address;
input [7:0]data;
begin
Cmd = WR | STA;
Go = 1;
Tx_DATA = device_id;
#20;
Go = 0;
#200;
@(posedge Trans_Done);
#20;
Cmd = WR;
Go = 1;
Tx_DATA = mem_address;
#20;
Go = 0;
#200;
@(posedge Trans_Done);
#20
Cmd = WR | STO;
Go = 1;
Tx_DATA = data;
#20;
Go = 0;
#200;
@(posedge Trans_Done);
end
endtask
task read_one_byte;
input [7:0]device_id;
input [7:0]mem_address;
begin
Cmd = WR | STA;
Go = 1;
Tx_DATA = device_id;
#20;
Go = 0;
#200;
@(posedge Trans_Done);
#20000;
Cmd = WR | STO;
Go = 1;
Tx_DATA = mem_address;
#20;
Go = 0;
#200;
@(posedge Trans_Done);
#20;
Cmd = WR | STA;
Go = 1;
Tx_DATA = device_id | 8'd1;
#20;
Go = 0;
#200;
@(posedge Trans_Done);
#20;
Cmd = RD | NACK | STO;
Go = 1;
#20;
Go = 0;
#200;
@(posedge Trans_Done);
#20;
end
endtask
仿真图如下图所示:
(文末提供所有源代码供学习使用)
2 i2c_control模块仿真验证
同样的,该testbench文件中例化M24LC048文件,以便简化我们的仿真代码。同样使用task任务编写写入过程和读取过程,具体详见代码分享。不再赘述。
仿真图如下图所示:
四、板级验证
在板级验证中,编写顶层i2c_eeprom模块,在该模块中例化i2c_control模块、按键消抖模块、ISSP工具;
其中按键消抖模块主要用于在按键按下时产生写入/读取数据的请求信号,请求信号为一个时钟周期的高电平信号。
i2c_control模块主要用于将数据通过iic协议的方式传输到EEPROM芯片中。
ISSP工具主要用来写入地址信号、数据信号,以及显示从EEPROM中读取到的数据。
该模块中例化结构如下图所示:
使用的硬件平台为:小梅哥FPGA_AC620开发板。
板级验证成功效果图:
五、写在后面
本编程思想例程参考小梅哥FPGA,当然了,我自己也写了IIC模块的驱动程序,也进行了仿真验证,只不过出现了小小的问题,目前还没有较好的解决方法,那就只好学习学习小梅哥的FPGA例程了,在此表示感谢。
关注微信公众号“FPGA学习者”后台回复【IIC例程】获取本期内容所有例程。