FPGA——IIC协议学习笔记
一、IIC读时序,时序图解
-
起始位:sclk为高电平时,SDA产生下降沿
-
停止位:sclk为高电平时,SDA产生上升沿
-
数据传输:sclk为高电平,SDA数据要保持稳定,sclk为低电平时,SDA数据可以发生变化
-
响应位:数据的接收器发送低电平的响应位(发送器在响应位传输时,应该使发送器输出为高阻态,但也可以当做无关项设置)
-
从机地址:找到指定相应从机
-
从机寄存器地址:找到指定从机的寄存器存储的数据
-
SDA输出:在接收数据时,要输出高阻态
二、IIC官方文档时序
- 标准模式下:SCL时钟频率为0-100KHz,起始位和停止位保持时间大于等于4us
- 快速模式下:SCL时钟频率为0-400KHz,起始位和停止位保持时间大于等于0.6us
三、计数器设计快速模式二字节地址的IIC的思路
-
设置scl计数器,计算SCL时钟频率
- 计数器开始条件:
读写使能
- 计数器结束条件:
系统时钟数到SCL规定时钟
计数器最大值 = 系统时钟频率/SCL时钟频率;SCL_CNT_M = SYS_CLOCK / SCL_CLOCK//sclk时钟计数器 always @(posedge clk or negedge rst_n)begin if(!rst_n)begin cnt_sclk <= 0; end else if(add_cnt_sclk)begin if(end_cnt_sclk) cnt_sclk <= 0; else cnt_sclk <= cnt_sclk + 1'b1; end end assign add_cnt_sclk = sclk_valid; assign end_cnt_sclk = add_cnt_sclk && cnt_sclk == SCL_CNT_M - 1;
-
设置传输数据(比特位)计数器
- 计数器开始条件:
scl计数器结束
- 计数器结束条件:
协议要求数据传输完成
//比特位计数器 always @(posedge clk or negedge rst_n)begin if(!rst_n)begin cnt_byte <= 0; end else if(add_cnt_byte)begin if(end_cnt_byte) cnt_byte <= 0; else cnt_byte <= cnt_byte + 1'b1; end end assign add_cnt_byte = end_cnt_sclk; assign end_cnt_byte = add_cnt_byte && cnt_byte == cnt_byte_num - 1;
-
关于起始位和停止位的问题
- 将起始位当做要传输的0 ,一个比特位
- 将停止位当做要传输的01,两个比特位
- 在起始位与停止位的时候,SCL不能拉低保持高电平
- 保持时间即为一个scl时钟周期
-
关于IIC读写标志位以及7位从机地址
- 读标志 0
- 写标志 1
- 7位从机地址不好表示,直接设置为8位,第0位为0,1-7位为寄存器地址
- 8位从机地址 或运算 8'h00 写;(CTR_BYTE | 8'h00)
- 8位从机地址 或运算 8'h01 读;(CTR_BYTE | 8'h00)
四、代码实现
module iic(
clk ,//系统时钟
rst_n ,//系统复位信号
word_addr ,//IIC器件寄存器地址
wr ,//写使能
wr_data ,//IIC器件写数据
wr_data_valid ,//IIC器件写数据有效标志位
rd ,//读使能
rd_data ,//读数据
rd_data_valid ,//IIC器件读数据有效标志位
iic_scl ,//IIC时钟线
iic_sda ,//IIC数据线
done //对IIC器件读写完成标识位
);
parameter CTR_BYTE = 8'hA0; //M24LC64器件地址
parameter SYS_CLOCK = 50_000_000; //系统时钟50MHz
parameter SCL_CLOCK = 400_000; //sclk总线时钟400KHz快速模式
parameter SCL_CNT_M = SYS_CLOCK / SCL_CLOCK; //产生时钟sclk计数器最大值
parameter W_ADDR = 16; //寄存器地址位宽
parameter WR_DATA = 8; //写数据位宽
parameter RD_DATA = 8; //读数据位宽
parameter CNT0_DATA = 7; //sclk计数器位宽
parameter CNT1_BYTE = 6; //byte计数器位宽
parameter WDATA = 50; //wdata位宽
parameter CNT_PHASE = 2; //阶段计数器位宽
input clk;
input rst_n;
input [W_ADDR-1:0] word_addr;
input wr;
input [WR_DATA-1:0] wr_data;
input rd;
output wr_data_valid;
output [RD_DATA-1:0] rd_data;
output rd_data_valid;
output iic_scl;
output done;
inout iic_sda;
reg wr_data_valid;
reg [RD_DATA-1:0] rd_data;
reg rd_data_valid;
reg iic_scl;
reg done;
reg iic_out;
wire iic_in;
reg iic_en;
//中间变量
reg [CNT0_DATA-1:0] cnt_sclk;
wire add_cnt_sclk;
wire end_cnt_sclk;
reg [CNT1_BYTE-1:0] cnt_byte;
wire add_cnt_byte;
wire end_cnt_byte;
wire scl0;
wire scl1;
wire start_flag;
wire stop_flag;
wire wr_flag;
reg sclk_valid;
reg [CNT1_BYTE-1:0] cnt_byte_num;
reg [WDATA-1:0] wdata;
wire rd_flag;
wire rd2_start;
//三态门
//防止多主机冲突
//assign iic_sda = iic_en?(iic_out?1'bz:0):1'bz;
//assign iic_sda = (iic_en && !iic_out) ? 1'b0 : 1'bz;
assign iic_sda = iic_en?iic_out:1'bz;
assign iic_in = iic_sda;
//sclk时钟计数器
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_sclk <= 0;
end
else if(add_cnt_sclk)begin
if(end_cnt_sclk)
cnt_sclk <= 0;
else
cnt_sclk <= cnt_sclk + 1'b1;
end
end
assign add_cnt_sclk = sclk_valid;
assign end_cnt_sclk = add_cnt_sclk && cnt_sclk == SCL_CNT_M - 1;
//比特位计数器
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_byte <= 0;
end
else if(add_cnt_byte)begin
if(end_cnt_byte)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1'b1;
end
end
assign add_cnt_byte = end_cnt_sclk;
assign end_cnt_byte = add_cnt_byte && cnt_byte == cnt_byte_num - 1;
//sclk起始,结束
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
sclk_valid <= 0;
end
else if(wr || rd)begin
sclk_valid <= 1'b1;
end
else if(end_cnt_byte)begin
sclk_valid <= 1'b0;
end
end
//sclk高低电平转换
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
iic_scl <= 1;
end
else if(scl0)begin
iic_scl <= 0;
end
else if(scl1)begin
iic_scl <= 1;
end
end
assign scl0 = (add_cnt_sclk && cnt_sclk == SCL_CNT_M-1) && (!start_flag) && (!stop_flag) && !end_cnt_byte && !rd2_start; //scl信号拉低条件
assign scl1 = cnt_sclk == ((SCL_CNT_M >> 1) - 1) && add_cnt_sclk; //scl信号拉高条件
assign start_flag = (add_cnt_sclk && cnt_sclk == 0) && cnt_byte == 0; //起始位
assign stop_flag = (add_cnt_sclk && cnt_sclk == SCL_CNT_M-1) && cnt_byte == cnt_byte_num - 2; //停止位
assign rd2_start = add_cnt_byte && cnt_byte == 28 && rd_data_valid; //第二阶段读开始位
//写数据
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
iic_out <= 1;
end
else if(wr_flag)begin
iic_out <= wdata[(WDATA - 1) - cnt_byte];
end
end
assign wr_flag = cnt_sclk == ((SCL_CNT_M >> 2) - 1) && add_cnt_sclk;
//读数据
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
rd_data <= 8'b0;
end
else if(rd_flag)begin
rd_data <= {rd_data[6:0],iic_in};
end
end
assign rd_flag = (cnt_sclk == ((3*(SCL_CNT_M >> 2)) - 1)) && add_cnt_sclk && (cnt_byte >= 39) && (cnt_byte < 47) && rd_data_valid;
//读有效
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
rd_data_valid <= 0;
end
else if(rd && !wr && !sclk_valid)begin
rd_data_valid <= 1;
end
else if((wr && !rd && !sclk_valid) || end_cnt_byte)begin
rd_data_valid <= 0;
end
end
//写有效
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
wr_data_valid <= 0;
end
else if(wr && !rd && !sclk_valid)begin
wr_data_valid <= 1;
end
else if((rd && !wr && !sclk_valid) || end_cnt_byte)begin
wr_data_valid <= 0;
end
end
//cnt_byte_num设置
always @(*)begin
if(wr_data_valid)begin
//2字节地址写
cnt_byte_num = 6'd39;
//写数据
wdata = {1'b0,CTR_BYTE | 8'h00,1'b0,word_addr[15:8],1'b0,word_addr[7:0],1'b0,wr_data,1'b0,1'b0,1'b1,11'b0};
end
else if(rd_data_valid)begin
//2字节地址读
cnt_byte_num = 6'd50;
//读数据
wdata = {1'b0,CTR_BYTE | 8'h00,1'b0,word_addr[15:8],1'b0,word_addr[7:0],1'b0,1'b1,1'b0,CTR_BYTE | 8'h01,1'b0,8'b0,1'b1,1'b0,1'b1};
end
else begin
cnt_byte_num = 0;
wdata = 0;
end
end
//done
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
done <= 0;
end
else if(end_cnt_byte)begin
done <= 1;
end
else
done <= 0;
end
//iic三态门使能
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
iic_en <= 0;
end
else if(wr && !rd && !sclk_valid)begin
iic_en <= 1;
end
else if((rd && !wr && !sclk_valid) || (rd_data_valid && (cnt_byte == 0 || cnt_byte == 46) && add_cnt_byte))begin
iic_en <= 1;
end
else if(rd_data_valid && cnt_byte == 38 && add_cnt_byte)begin
iic_en <= 0;
end
else if(done)begin
iic_en <= 0;
end
end
endmodule
五、仿真调试方法
使用镁光官网提供的 EEPROM M24LC64 verilog仿真模型
https://www.microchip.com/wwwproducts/en/24LC64
1、仿真模型使用方法
-
添加到工程文件综和
-
在已经添加iic仿真的文件test bench设置里,添加24LC64.文件
(选中第一个点edit,不要点new新建)
2、仿真文件代码
`timescale 1 ns/ 1 ns
module iic_vlg_tst();
// test vector input registers
reg clk;
reg rd;
reg rst_n;
reg [15:0] word_addr;
reg wr;
reg [7:0] wr_data;
// wires
wire done;
wire iic_scl;
wire iic_sda;
wire [7:0] rd_data;
wire rd_data_valid;
wire wr_data_valid;
// assign statements (if any)
//assign iic_sda = treg_iic_sda;
iic i1 (
// port map - connection between master ports and signals/registers
.clk(clk),
.done(done),
.iic_scl(iic_scl),
.iic_sda(iic_sda),
.rd(rd),
.rd_data(rd_data),
.rd_data_valid(rd_data_valid),
.rst_n(rst_n),
.word_addr(word_addr),
.wr(wr),
.wr_data(wr_data),
.wr_data_valid(wr_data_valid)
);
M24LC64 M24LC64(
.A0(1'b0),
.A1(1'b0),
.A2(1'b0),
.WP(1'b0),
.SDA(iic_sda),
.SCL(iic_scl),
.RESET(!rst_n)
);
//时钟周期,单位ns,在这里修改时钟周期
parameter CYCLE = 20;
//生成本地时钟50M
initial begin
clk = 0;
forever
#(CYCLE/2)
clk=~clk;
end
//产生复位信号
initial begin
rst_n = 0;
word_addr = 0;
wr = 0;
wr_data = 0;
rd = 0;
#(CYCLE * 200 + 1)
rst_n = 1;
#200;
word_addr = 0;
wr_data = 0;
wr = 1'b1;
#(CYCLE)
wr = 1'b0;
wr_data = 8'b1010_0011;
#200000;
word_addr = 0;
rd = 1'b1;
#(CYCLE)
rd = 1'b0;
end
endmodule
3、仿真时,如何通过24LC64的内部信号查看是否将数据写入24LC64或读出24LC64
- 点击sim
- 点击M24LC64
- 找到WrDataByte以及RdDataByte,选中,右键,添加波形,重新开始运行
- 查看WrDataByte可以看到是否将数据写进去,查看RdDataByte可以看到是否能够读取数据
写
读
4.仿真波形