Fork me on GitHub

FPGA的I2C协议实现(FPGA学习笔记)

参考资料:正点原子E2PROM读写测试

I2C总线结构

 

I2C单次写


SCL为高电平时,SDA产生下降沿表示I2C传输开始,在传输完7位器件地址及一位写命令(0)后,主机释放SDA总线,从机拉低SDA应答。对于16位器件存储字地址而言,需要分两次8位器件存储字地址传输,等待从机应答后再传输8位数据,最后SDA在SCL为高电平时产生上升沿表示一次I2C单次传输结束。

 

I2C随机读


I2C随机读操作与前面传输器件地址和传输16位器件存储字地址相同,在传输完器件存储字地址后SDA会在次在SCL为高电平时产生下降沿(既起始信号)和再次传输器件地址,只不过这次7位器件地址后面是读命令(1),这种操作称为虚写,主要是为了使从机内部指针指向需要读取得存储单元地址。

 

I2C驱动模块状态机


I2C驱动时钟频率为scl频率的4倍(便于SDA实现在SCL低电平中间(此时为dri_clk上升沿)改变数据,并在SCL为高电平时保持数据),dri_clk由输入的50MHZ晶振分频得到1000KHZ。初始状态SCL与SDA保持高电平,当收到外部I2C触发信号i2c_exec高电平时,SCL拉低启动I2C,接着后面状态转移分为8个部分。

8个状态

 

I2C驱动模块
由于SDA为双向接口,所以要避免同时作为输入与输出

 

三态门解决sda双向传输问题
当i2c_exec拉高时,外部传入I2C的控制信号通过寄存器临时保存

 

寄存外部数据
后面由三段式状态机实现I2C的状态转移

第一段时序电路描述状态转移

第二段逻辑电路描述状态转移条件

第三段时序电路描述状态输入与输出


`timescale 1ns / 1ps


//I2C驱动,实现单次写与随机读
module i2c_dri #(
parameter SLAVE_ADDR = 7'b1_010_000 , //EEPROM从机地址
parameter CLK_FREQ = 26'd50_000_000, //模块输入的时钟频率
parameter I2C_FREQ = 18'd250_000 //IIC_SCL的时钟频率
)
(
//global signal
input clk, //50MHZ系统时钟
input rst_n,


//input interface
input bit_ctrl, //地址位宽控制,1:16位 0:8位
input i2c_exec, //I2C触发开始信号
input i2c_rh_wl, //I2C读写控制信号
input [15:0]i2c_addr, //I2C读写地址
input [7:0] i2c_data_w, //I2C写入E2PROM数据


//output interface
output reg dri_clk, //I2C驱动时钟
output reg i2c_ack, //E2PROM应答信号 0:从机应答 1:从机未应答
output reg [7:0] i2c_data_r, //I2C读取数据
output reg i2c_done, //I2C完成一次操作
output reg scl,
inout sda
);


reg sda_dir; //设置sda方向,当sda_out输出信息时,sda_dir拉高
reg sda_out; //sda输出信号
wire sda_in; //输入信号
reg [7:0] cur_state; //当前状态
reg [7:0] next_state; //下个状态
reg st_done; //状态完成标志


reg wr_flag; //临时寄存输入控制I2C的读写信号
reg [15:0] addr_t; //临时寄存输入I2C的地址信号
reg [7:0] data_wr_t; //临时寄存输入I2C的数据信息
reg [7:0] data_r; //临时存储E2PROM读入的数据


reg [6:0] cnt; //状态内部计数器


//时钟分频
wire [8:0] CLK_DIV_MUL; //输入时钟分频系数
reg [8:0] clk_cnt;
assign CLK_DIV_MUL = (CLK_FREQ / I2C_FREQ ) >> 2'd2;


always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
dri_clk <= 1'b0;
clk_cnt <= 9'd0;
end
else if(clk_cnt == CLK_DIV_MUL[8:1] - 1)begin
dri_clk <= ~dri_clk;
clk_cnt <= 9'd0;
end
else
clk_cnt <= clk_cnt + 1'b1;
end


//sda三态门处理
assign sda = sda_dir ? sda_out : 1'bz; //sda_dir为高时,sda作为sda_out输出 sda_dir为低时,sda输出高阻
assign sda_in = sda;


//8个状态机状态
localparam st_idle = 8'b00_000_001; //起始状态
localparam st_sladdr = 8'b00_000_010; //发送器件地址及写控制命令
localparam st_addr16 = 8'b00_000_100; //发送16位地址中的高8位地址
localparam st_addr8 = 8'b00_001_000; //发送8位地址或16位低8位地址
localparam st_data_wr = 8'b00_010_000; //写数据
localparam st_addr_rd = 8'b00_100_000; //发送器件地址及读控制命令
localparam st_data_rd = 8'b01_000_000; //读数据
localparam st_stop = 8'b10_000_000; //发送结束信号


//====================================================
// 三段式状态机
//====================================================
//时序逻辑描述状态转移
always @(posedge dri_clk or negedge rst_n)begin
if(!rst_n)
cur_state <= st_idle;
else
cur_state <= next_state;
end


//组合逻辑描述状态转移条件
always @(*)begin
next_state = st_idle;
case(cur_state)
st_idle:begin
if(i2c_exec == 1'b1)
next_state = st_sladdr;
else
next_state = st_idle;
end
st_sladdr:begin
if(st_done == 1'b1)begin
if(bit_ctrl == 1'b1)
next_state = st_addr16;
else
next_state = st_addr8;
end
else
next_state = st_sladdr;
end
st_addr16:begin
if(st_done == 1'b1)
next_state = st_addr8;
else
next_state = st_addr16;
end
st_addr8:begin
if(st_done == 1'b1)begin
if(wr_flag == 1'b0)
next_state = st_data_wr;
else
next_state = st_addr_rd;
end
else
next_state = st_addr8;
end
st_data_wr:begin
if(st_done == 1'b1)
next_state = st_stop;
else
next_state = st_data_wr;
end
st_addr_rd:begin
if(st_done == 1'b1)
next_state = st_data_rd;
else
next_state = st_addr_rd;
end
st_data_rd:begin
if(st_done == 1'b1)
next_state = st_stop;
else
next_state = st_data_rd;
end
default:next_state = st_idle;
endcase
end


//时序逻辑描述状态输出
always @(posedge dri_clk or negedge rst_n)begin
if(!rst_n)begin
//输入接口信号
wr_flag <= 1'b0;
addr_t <= 16'b0;
data_wr_t <= 8'b0;
//输出接口信号
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b1;
i2c_ack <= 1'b0;
i2c_data_r <= 8'b0;
i2c_done <= 1'b0;
//内部寄存器信号
cnt <= 7'd0;
st_done <= 1'b0;
data_r <= 8'b0; //E2PROM输入数据临时存储
end
else begin
st_done <= 1'b0;
cnt <= cnt + 1'b1;
case(cur_state)
st_idle:begin
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b1;
i2c_done <= 1'b0;
cnt <= 7'd0;
if(i2c_exec == 1'b1)begin //触发I2C信号拉高,寄存输入的信号
wr_flag <= i2c_rh_wl;
addr_t <= i2c_addr;
data_wr_t <= i2c_data_w;
end
end
st_sladdr:begin
case(cnt)
7'd1 : sda_out <= 1'b0;
7'd2 : scl <= 1'b0;
7'd3 : sda_out <= SLAVE_ADDR[6];
7'd4 : scl <= 1'b1;
7'd6 : scl <= 1'b0;
7'd7 : sda_out <= SLAVE_ADDR[5];
7'd8 : scl <= 1'b1;
7'd10: scl <= 1'b0;
7'd11: sda_out <= SLAVE_ADDR[4];
7'd12: scl <= 1'b1;
7'd14: scl <= 1'b0;
7'd15: sda_out <= SLAVE_ADDR[3];
7'd16: scl <= 1'b1;
7'd18: scl <= 1'b0;
7'd19: sda_out <= SLAVE_ADDR[2];
7'd20: scl <= 1'b1;
7'd22: scl <= 1'b0;
7'd23: sda_out <= SLAVE_ADDR[1];
7'd24: scl <= 1'b1;
7'd26: scl <= 1'b0;
7'd27: sda_out <= SLAVE_ADDR[0];
7'd28: scl <= 1'b1;
7'd30: scl <= 1'b0;
7'd31: sda_out <= 1'b0; //7位器件地址后接一位写符号
7'd32: scl <= 1'b1;
7'd34: scl <= 1'b0;
7'd35: begin
sda_dir <= 1'b0; //主机释放sda,sda作为输入接口
sda_out <= 1'b1; //此时sda_out赋值无意义,只是防止出现未知X
end
7'd36: scl <= 1'b1;
7'd37: begin
st_done <= 1'b1; //一次传输完成
if(sda_in == 1'b1) //如果从机未应答,则拉高i2c_ack
i2c_ack <= 1'b1;
end
7'd38: begin
scl <= 1'b0;
cnt <= 7'd0;
end
default: ;
endcase
end
st_addr16:begin
case(cnt) //cnt <= 7'd0 与 cur_state <= next_state 同时发送
7'd0: begin
sda_dir <= 1'b1;
sda_out <= addr_t[15]; //sda_dir拉高的同时发送一位地址数据
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= addr_t[14];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= addr_t[13];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= addr_t[12];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= addr_t[11];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= addr_t[10];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= addr_t[9];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= addr_t[8];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin
st_done <= 1'b1;
if(sda_in == 1'b1)
i2c_ack <= 1'b1;
end
7'd35: begin
scl <= 1'b0;
cnt <= 7'd0;
end
default: ;
endcase
end
st_addr8:begin //输出16位地址的低8位或者8位地址
case(cnt)
7'd0: begin
sda_dir <= 1'b1;
sda_out <= addr_t[7];
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= addr_t[6];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= addr_t[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= addr_t[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= addr_t[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= addr_t[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= addr_t[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= addr_t[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin
st_done <= 1'b1;
if(sda_in == 1'b1)
i2c_ack <= 1'b1;
end
7'd35: begin
scl <= 1'b0;
cnt <= 7'd0;
end
default: ;
endcase
end
st_data_wr:begin
case(cnt)
7'd0: begin
sda_dir <= 1'b1;
sda_out <= data_wr_t[7];
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= data_wr_t[6];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= data_wr_t[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= data_wr_t[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= data_wr_t[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= data_wr_t[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= data_wr_t[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= data_wr_t[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin
st_done <= 1'b1;
if(sda_in == 1'b1)
i2c_ack <= 1'b1;
end
7'd35: begin
scl <= 1'b0;
cnt <= 7'd0;
end
default: ;
endcase
end
st_addr_rd:begin
case(cnt)
7'd0 :begin
sda_dir <= 1'b1;
sda_out <= 1'b1; //sda_out保持高电平
end
7'd1 : scl <= 1'b1;
7'd2 : sda_out <= 1'b0; //scl为高时,sda_out 产生下降沿,传输重新开始
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= SLAVE_ADDR[6];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= SLAVE_ADDR[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= SLAVE_ADDR[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= SLAVE_ADDR[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= SLAVE_ADDR[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= SLAVE_ADDR[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= SLAVE_ADDR[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: sda_out <= 1'b1; //7位器件地址后接一位读符号
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: begin
sda_dir <= 1'b0; //主机释放sda,sda作为输入接口
sda_out <= 1'b1; //此时sda_out赋值无意义,只是防止出现未知X
end
7'd37: scl <= 1'b1;
7'd38: begin
st_done <= 1'b1; //一次传输完成
if(sda_in == 1'b1) //如果从机未应答,则拉高i2c_ack
i2c_ack <= 1'b1;
end
7'd39: begin
scl <= 1'b0;
cnt <= 7'd0;
end
default: ;
endcase
end
st_data_rd:begin
case(cnt)
7'd0: sda_dir <= 1'b0; //sda_in输入数据保持在scl为高电平时刻
7'd1:begin
scl <= 1'b1;
data_r[7] <= sda_in;
end
7'd3: scl <= 1'b0;
7'd5: begin
scl <= 1'b1;
data_r[6] <= sda_in;
end
7'd7: scl <= 1'b0;
7'd9: begin
scl <= 1'b1;
data_r[5] <= sda_in;
end
7'd11: scl <= 1'b0;
7'd13: begin
scl <= 1'b1;
data_r[4] <= sda_in;
end
7'd15: scl <= 1'b0;
7'd17: begin
scl <= 1'b1;
data_r[3] <= sda_in;
end
7'd19: scl <= 1'b0;
7'd21: begin
scl <= 1'b1;
data_r[2] <= sda_in;
end
7'd23: scl <= 1'b0;
7'd25: begin
scl <= 1'b1;
data_r[1] <= sda_in;
end
7'd27: scl <= 1'b0;
7'd29: begin
scl <= 1'b1;
data_r[0] <= sda_in;
end
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b1;
sda_out <= 1'b1; //主机响应
end
7'd33: scl <= 1'b1;
7'd34: st_done <= 1'b1;
7'd35:begin
scl <= 1'b0;
cnt <= 7'd0;
i2c_data_r <= data_r;
end
default: ;
endcase
end
st_stop:begin
case(cnt)
7'd0:begin
sda_dir <= 1'b1;
sda_out <= 1'b0; //拉低sda_out,下一时钟产生上升沿结束I2C
end
7'd1: scl <= 1'b1;
7'd2: sda_out <= 1'b1; //scl为高电平时,sda_out产生下降沿结束I2C
7'd15: st_done <= 1'b1;
7'd16: begin
cnt <= 7'd0;
i2c_done <= 1'b1; //向上层模块传递I2C结束信号
end
default: ;
endcase
end
endcase
end
end


endmodule 作者:xil_maker https://www.bilibili.com/read/cv22640885/ 出处:bilibili


 

I2C写操作 

 写入7位器件地址及写命令

写入16位字地址的高8位

 向从机写入8位数据10101010


I2C读操作

重新写入器件地址及读命令

读取8位数据10101010

作者:xil_maker https://www.bilibili.com/read/cv22640885/ 出处:bilibili

posted @ 2024-01-23 10:54  _浮尘  阅读(938)  评论(0)    收藏  举报