文章目录
1、IIC协议
1.1、什么是iic
IC即为Inter-Inegrated Circuit(集成电路总线),在上世纪八十年代左右由Philips公司(即现做的NXP半导体公司)设计出来的一种简单、双向、二进制总线标准。其只需要两根线(SDA数据线和SCL时钟线)即可实现总线连接的器件间的通信,但同一时间只能有一个主机,即一主多从模式,通信方式为半双工通信。SDA和SCL都是双向I/O线,所以主要适用于数据量小,距离短的主从通信。
数据传输模式分为三种:
- 标准模式:100kbit/s
- 快速模式:400kbit/s
- 高速模式:3.4Mbit/s
1.2、EEPROM
1、EEPROM,全称电可擦除可编程只读存储器 (Electrically-Erasable Programmable Read-Only Memory),是一种可以通过电子方式多次复写的半导体存储设备。相比EPROM,EEPROM不需要用紫外线照射,也不需取下,就可以用特定的电压,来抹除芯片上的信息,以便写入新的数据。
2、我们使用的是C4开发板,型号为24LC04B的EEPROM存储芯片。24LC04B的存储容量为512Bytes/4Kbits,其内部有两个Block,每个Block中有256个字节(一个字节为8bit)。其读写操作都是以字节(8bit)为基本单位。
3、24LC04B EEPROM 存储芯片的器件地址包括:
- 厂商设置的高4位1010,这里表设备代码
- 用户需自主设置的低3位 x、x、B0 来选择块地址
- 字节存储地址,一共8bit
4、在IIC主从设备通讯时,主机在发送了起始信号后,接着会向从机发送控制命令。控制命令长度为1个字节,它的高7位为上文讲解的 IIC设备的器件地址,最低位为读写控制位。EEPROM储存芯片控制命令格式示意图,具体见下图。
- 读写控制位为 0 时,表示主机(FPGA)要对从机(EEPROM)进行数据写入操作:{7’b1010xxx,1’b0}
- 读写控制位为 1 时,表示主机(FPGA)要对从机(EEPROM)进行数据读出操作:{7’b1010xxx,1’b1}
1.3、iic传输特点
由于IIC是由数据线(SDA)和时钟线(SCL)所组成的,因此二者相互配合完成数据的传输。当时钟线(SCL)为高电平时要求数据线(SDA)的数据稳定,此时从机开始接收主机数据或者主机接收从机的应答;当时钟线(SCL)为低电平时数据线(SDA)的数据可变化,即更新数据以备下一次的接收。
1.4、时序特点
1.4.1、起始信号
1、IIC传输主要由开始信号、停止信号、应答/非应答、读、写信号五部分组成。
2、起始信号:
当时钟线(SCL)为高电平期间,数据线(SDA)由高电平变为低电平即为起始信号,时序图如下图所示:
1.4.2、停止信号
停止信号:
当时钟线(SCL)为高电平期间数据线(SDA)由低电平恢复到高电平即为终止信号,时序图如下图所示:
1.4.3、读操作
1、读数据:
读数据的时序与应答/非应答的相同,都是在时钟线(SCL)为高电平期间读取数据线(SDA)的值,数据线(SDA)的高低电平即代表“1”或“0”,分为当前地址读、随机读以及顺序读
2、当前地址读:当前一次读操作或者写操作完毕后,24XX04内部有一个地址计数器,会自增一,所以当前地址读是对下一个地址去读(当前地址读先起始位——然后写控制字节,从机接收到应答信号,然后读数据,无应答信号——最后结束位):
3、随机读(起始位——写控制字节,从机接收到应答信号——然后dummuy write 虚写,写数据地址,从机接收到应答信号——再次读起始位——读控制字节,从机接收到应答信号——读数据——停止位)
4、顺序读:
顺序读就在随机读之后
1.4.4、写操作
1、从机在时钟线(SCL)高电平期间读取数据线(SDA)的数据,因此写数据时需要在时钟线(SCL)低电平期间改变数据线(SDA)的值来传输数据,分为字节写和页写。
2、字节写,一次只能写入一字节(写起始位——写控制字节,从机接收到发应答信号——写数据地址,从机接收到发应答信号,如果数据地址是2位的话,就继续写数据地址,从机接收到发应答信号——写数据,从机接收到发应答信号——停止位)
3、页写,一次最多写入16个字节(每字节为8bit)数据,(写起始位——写控制字节,从机接收到应答信号——写数据地址,从机接收到应答信号,如果数据地址是2位的话,就继续写数据地址,从机接收到应答信号——写数据,从机接收到应答信号——继续写数据,直到写完全部的数据——停止位)
2、模块设计
2.1、总体设计思想
1、使用芯片外设按键模拟读请求,收到读写请求信号时,FPGA通过I2C协议向EEPROM芯片写入单字节数据或从E2PROM芯片读出单字节数据,再使用串口来接收需要写入的数据和显示被读出的数据。需要一个读写控制模块与I2C接口模块:
- 读写控制模块负责发送读写命令和地址,以及使用两个fifo分别寄存uart写入数据和EEPROM读出数据。
- 2C接口模块负责将这些指令翻译为I2C协议允许的格式再传输EEPROM中,并将接收到的信息翻译再反馈给FPGA(串并转换)。
- 串口收发模块以及收发分别对应的2个fifo。
2、读:
- 按下key,经过按键消抖模块后输出稳定的key_out信号到eeprom_rw模块中
- eeprom_rw模块接收到读请求信号后向i2c_interface模块发送读请求
- i2c_interface模块接收到读请求后发送读控制字和读地址,然后将接收到的SDA信号进行一个串并转换发送到eeprom_rw模块中
- eeprom_rw模块接收到的rd_data[7:0]缓存到rdfifo中,然后以先进先出的原则依次发送给uart_tx模块
- uart_tx模块将接收到的每一个rd_data[7:0]进行并串转换,然后发送到上位机中,在发送途中拉高busy信号,表示tx线已被占用,等发完这个字节再继续传下一个进来
3、写:
- 由上位机串行发送 x byte的数据到串口接收模块uart_rx中
- uart_rx模块将接收到的串行信号进行串并转换然后发送到eeprom_rw模块中
- eeprom_rw模块,每接收到一次din_vld(数据接收有效信号)后,开启wrfifo的写请求,写入一个字节,直到接收完为止
- wrfifo缓存完成后,eeprom_rw模块向i2c_interface模块发送写请求和需要写入的数据,每次发送1个字节的数据,在收到应答信号后再次发送1个字节
- i2c_interface模块接收到写命令后便开始发送写控制字和写地址,然后将接收到的并行数据wr_data[7:0]进行一个并串转换来写入到eeprom中
2.2、 模块状态图
1、iic模块状态图
WRITE:写完一个字节
READ:读完一个字节继续写数据发一个字节
START:发起始位
STOP:发停止位
SACK:发送应答,分为有效应答和无效应答
RACK:接收应答,分为有效应答和无效应答
2、EEPROM读写模块状态图
3、总体框架图
2.3、部分代码
1、顶层模块代码
`include "param.v"
module i2c_top (
input clk_50 , //时钟信号
input rst_n , //复位信号
input key ,
input rx , //串口数据接收总线
inout i2c_sda , //i2c的数据线
output tx , //串口数据发送总线
output i2c_scl //i2c的时钟线
);
//信号定义
wire [1:0] baud_sel ; //波特率选择
wire key_out ; //按键消抖输出
wire [7:0] rx_byte ; //串口接收到的值经过串并转换eeprom_control模块
wire rx_byte_vld ;
wire tx_busy ; //串口忙状态标志
wire [7:0] tx_data ; //EEPROM的数据输出到串口发送模块
wire tx_data_vld ;
assign baud_sel = 3; //选择波特率为115200
//例化按键消抖模块
key_debounce #(.KEY_W(1)) u_key(
/*input */.clk_50 (clk_50 ),
/*input */.rst_n (rst_n ),
/*input */.key_in (key ),
/*output */.key_out (key_out )
);
//例化串口发送模块
uart_tx u_tx(
/*input */.clk_50 (clk_50 ),
/*input */.rst_n (rst_n ),
/*input [1:0] */.baud_sel (baud_sel ), //选择波特率
/*input [7:0] */.din (tx_data ),
/*input */.din_vld (tx_data_vld ),
/*output */.busy (tx_busy ), //忙状态指示
/*output */.tx (tx ) //数据发送总线
);
//例化串口接收模块
uart_rx u_rx(
/*input */.clk_50 (clk_50 ),
/*input */.rst_n (rst_n ),
/*input */.rx (rx ), //数据接收总线
/*input [1:0] */.baud_sel (baud_sel ), //选择波特率
/*output [7:0] */.rx_byte (rx_byte ),
/*output */.rx_byte_vld (rx_byte_vld )
);
//例化EEPROM控制模块
eeprom_control#(.WR_LEN(`WR_BYTE), .RD_LEN(`RD_BYTE)) u_eeprom(
/*input */.clk_50 (clk_50 ),
/*input */.rst_n (rst_n ),
/*input [7:0] */.din (rx_byte ),
/*input */.din_vld (rx_byte_vld ),
/*input */.rd_en (key_out ),
/*input */.tx_busy (tx_busy ), //串口发送忙标志
/*inout */.i2c_sda (i2c_sda ),
/*output [7:0] */.dout (tx_data ),
/*output */.dout_vld (tx_data_vld ),
/*output */.i2c_scl (i2c_scl )
);
endmodule //i2c_top
3、IIC接口模块
`include "param.v"
module iic_interface (
input clk , //时钟信号
input rst_n , //复位信号
input req , //输入请求信号
input [3:0] cmd , //输入命令
input [7:0] din , //数据
input iic_sda_i , //输入串行数据
output iic_scl , //iic的时钟信号(200kbit/s)速率
output iic_sda_o , //输出IIC串行数据总线
output iic_sda_oe , //输出使能信号
output done , //传输完成信号
output [7:0] dout //输出数据
);
//状态机传输定义
parameter IDLE = 7'b000_0001 , //空闲状态
START = 7'b000_0010 , //发起始位状态
WRITE = 7'b000_0100 , //写状态:FPGA向EEPROM写入数据
RACK = 7'b000_1000 , //接收应答状态:FPGA接收EEPROM发送的应答
READ = 7'b001_0000 , //读状态:FPGA向EEPROM读出数据
SACK = 7'b010_0000 , //发送应答状态:FPGA发送应答给EEPROM(接收方发应答)
STOP = 7'b100_0000 ; //发停止位状态
//信号定义
reg [6:0] cstate ;
reg [6:0] nstate ;
reg iic_scl_r ;
reg iic_sda_o_r ;
reg iic_sda_oe_r ;
reg [7:0] dout_r ;
reg [7:0] cnt_scl ; //iic的时钟频率计数寄存器
wire add_cnt_scl ;
wire end_cnt_scl ;
reg [3:0] cnt_bit ; //比特计数寄存器
wire add_cnt_bit ;
wire end_cnt_bit ;
reg [7:0] tx_data ; //寄存数据输入(din)
reg [7:0] rx_data ; //寄存iic的数据总线数据
reg [3:0] bit_num ;
wire idle_to_start ;
wire idle_to_write ;
wire idle_to_read ;
wire start_to_write ;
wire start_to_read ;
wire write_to_rack ;
wire read_to_sack ;
wire rack_to_idle ;
wire rack_to_stop ;
wire sack_to_idle ;
wire sack_to_stop ;
wire stop_to_idle ;
//iic的时钟频率计数实现
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
cnt_scl <= 0;
else if (add_cnt_scl)
begin
if (end_cnt_scl)
cnt_scl <= 0;
else
cnt_scl <= cnt_scl + 1'b1;
end
end
assign add_cnt_scl = cstate != IDLE;
assign end_cnt_scl = add_cnt_scl && cnt_scl == `SCL_PERIOD - 1;
//比特计数实现
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
cnt_bit <= 0;
else if (add_cnt_bit)
begin
if (end_cnt_bit)
cnt_bit <= 0;
else
cnt_bit <= cnt_bit + 1'b1;
end
end
assign add_cnt_bit = end_cnt_scl;
assign end_cnt_bit = add_cnt_bit && cnt_bit == bit_num - 1;
//bit_num
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
bit_num <= 0;
else if (cstate == READ | cstate == WRITE)
bit_num <= 8;
else
bit_num <= 1;
end
//状态机跳转
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
cstate <= IDLE;
else
cstate <= nstate;
end
always @(*)
begin
case (cstate)
IDLE:
begin
if (idle_to_start)
nstate = START;
else if (idle_to_write)
nstate = WRITE;
else if (idle_to_read)
nstate = READ;
else
nstate = cstate;
end
START:
begin
if (start_to_read)
nstate = READ;
else if (start_to_write)
nstate = WRITE;
else
nstate = cstate;
end
WRITE:
begin
if (write_to_rack)
nstate = RACK;
else
nstate = cstate;
end
READ:
begin
if (read_to_sack)
nstate = SACK;
else
nstate = cstate;
end
RACK:
begin
if (rack_to_stop)
nstate = STOP;
else if (rack_to_idle)
nstate = IDLE;
else
nstate = cstate;
end
SACK:
begin
if (sack_to_stop)
nstate = STOP;
else if (sack_to_idle)
nstate = IDLE;
else
nstate = cstate;
end
STOP:
begin
if (stop_to_idle)
nstate = IDLE;
else
nstate = cstate;
end
default: ;
endcase
end
//状态机跳转条件判断
assign idle_to_start = nstate == IDLE & (req & (cmd&`CMD_START)) ;
assign idle_to_write = nstate == IDLE & (req & (cmd&`CMD_WRITE)) ;
assign idle_to_read = nstate == IDLE & (req & (cmd&`CMD_READ )) ;
assign start_to_write = nstate == START & (end_cnt_bit & (cmd&`CMD_WRITE)) ;
assign start_to_read = nstate == START & (end_cnt_bit & (cmd&`CMD_READ)) ;
assign write_to_rack = nstate == WRITE & (end_cnt_bit) ;
assign read_to_sack = nstate == READ & (end_cnt_bit) ;
assign rack_to_idle = nstate == RACK & (end_cnt_bit & ~(cmd&`CMD_STOP)) ;
assign rack_to_stop = nstate == RACK & (end_cnt_bit & (cmd&`CMD_STOP)) ;
assign sack_to_idle = nstate == SACK & (end_cnt_bit & ~(cmd&`CMD_STOP)) ;
assign sack_to_stop = nstate == SACK & (end_cnt_bit & (cmd&`CMD_STOP)) ;
assign stop_to_idle = nstate == STOP & end_cnt_bit ;
//tx_data
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
tx_data <= 0;
else if (req)
tx_data <= din;
end
//iic_scl_r, iic时钟总线数据寄存待输出
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
iic_scl_r <= 1'b1;
else if (idle_to_start | idle_to_write | idle_to_read) //发送数据时拉低时钟总线
iic_scl_r <= 1'b0;
else if (add_cnt_scl & cnt_scl == `SCL_HALF)
iic_scl_r <= 1'b1;
else if (add_cnt_scl && ~stop_to_idle) //停止信号是scl保持高电平时sda恢复到高电平
iic_scl_r <= 1'b0;
end
//iic_sda_o_r, iic数据总线数据寄存待输出
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
iic_sda_o_r <= 1'b1;
else if (cstate == START) //发送起始位
begin
if (add_cnt_scl & cnt_scl == `LOW_HLAF) //先在时钟低电平时拉高sda数据线,保证能够检测到起始位
iic_sda_o_r <= 1'b1;
else if (add_cnt_scl & cnt_scl == `HIGH_HALF) //检测到起始位
iic_sda_o_r <= 1'b0;
end
else if (cstate == WRITE & cnt_scl == `LOW_HLAF) //时钟低电平时进行数据发送
iic_sda_o_r <= tx_data[7-cnt_bit];
else if (cstate == RACK & cnt_scl == `LOW_HLAF)
iic_sda_o_r <= (cmd&`CMD_STOP) ? 1'b1:1'b0; //是否发送应答
else if (cstate == STOP) //发送停止位
begin
if (add_cnt_scl & cnt_scl == `LOW_HLAF) //先在时钟低电平时拉低sda数据线,保证能够检测到停止位
iic_sda_o_r <= 1'b0;
else if (add_cnt_scl & cnt_scl == `HIGH_HALF) //检测到停止位
iic_sda_o_r <= 1'b1;
end
end
//iic_sda_oe_r
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
iic_sda_oe_r <= 0;
else if (idle_to_start | idle_to_write | rack_to_stop | read_to_sack) //发送数据时使能信号拉高
iic_sda_oe_r <= 1'b1;
else if (idle_to_read | start_to_read | write_to_rack | stop_to_idle)
iic_sda_oe_r <= 1'b0;
end
//iic_sda_i 读入数据寄存
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
rx_data <= 0;
else if (cstate == READ & cnt_scl == `HIGH_HALF)
rx_data[7-cnt_bit] <= iic_sda_i;
end
//输出
assign iic_scl = iic_scl_r ;
assign iic_sda_o = iic_sda_o_r ;
assign iic_sda_oe = iic_sda_oe_r ;
assign dout = rx_data ;
assign done = rack_to_idle | sack_to_idle | stop_to_idle ;
endmodule //iic_interface
参考:
https://blog.csdn.net/xs_sd/article/details/114534036
https://blog.csdn.net/weixin_45888898/article/details/122889135