verilog 实现i2c控制 本文涉及代码可以从下方链接下载:
https://gitee.com/huangzhc3/i2c_sim
i2c协议 首先要先学习一下i2c协议的基础,这里有一份官方文档(https://www.nxp.com/docs/en/user-guide/UM10204.pdf),也可以参考一些博客,总的来说i2c协议的基础原理不难,因为主要就是两根线的控制(SCL、SDA)。
i2c仿真模型(EEPROM) 当掌握了i2c协议的基础之后,建议以EEPROM为例子入手,有一篇博客介绍的很好:https://www.cnblogs.com/ninghechuan/p/9534893.html(需要有一定verilog基础),下面贴出原文中给出的eeprom仿真模型:
`timescale 1ns/1ns
`define timeslice 1250
//`define timeslice 300module EEPROM_AT24C64(
scl,
sda
);
input scl;
//串行时钟线
inout sda;
//串行数据线reg out_flag;
//SDA数据输出的控制信号reg[7:0] memory[8191:0];
//数组模拟存储器
reg[12:0]address;
//地址总线
reg[7:0]memory_buf;
//数据输入输出寄存器
reg[7:0]sda_buf;
//SDA数据输出寄存器
reg[7:0]shift;
//SDA数据输入寄存器
reg[7:0]addr_byte_h;
//EEPROM存储单元地址高字节寄存器
reg[7:0]addr_byte_l;
//EEPROM存储单元地址低字节寄存器
reg[7:0]ctrl_byte;
//控制字寄存器
reg[1:0]State;
//状态寄存器integer i;
//---------------------------
parameter
r7 = 8'b1010_1111,w7 = 8'b1010_1110,//main7
r6 = 8'b1010_1101,w6 = 8'b1010_1100,//main6
r5 = 8'b1010_1011,w5 = 8'b1010_1010,//main5
r4 = 8'b1010_1001,w4 = 8'b1010_1000,//main4
r3 = 8'b1010_0111,w3 = 8'b1010_0110,//main3
r2 = 8'b1010_0101,w2 = 8'b1010_0100,//main2
r1 = 8'b1010_0011,w1 = 8'b1010_0010,//main1
r0 = 8'b1010_0001,w0 = 8'b1010_0000;
//main0
//---------------------------assign sda = (out_flag == 1) ? sda_buf[7] : 1'bz;
//------------寄存器和存储器初始化---------------
initial
begin
addr_byte_h= 0;
addr_byte_l= 0;
ctrl_byte= 0;
out_flag= 0;
sda_buf= 0;
State= 2'b00;
memory_buf= 0;
address= 0;
shift= 0;
for(i=0;
i<=8191;
i=i+1)
memory[i] = 0;
end//启动信号
always@(negedge sda)
begin
if(scl == 1)
begin
State = State + 1;
if(State == 2'b11)
disable write_to_eeprom;
end
end//主状态机
always@(posedge sda)
begin
if(scl == 1)//停止操作
stop_W_R;
else
begin
casex(State)
2'b01:begin
read_in;
if(ctrl_byte == w7 || ctrl_byte == w6
|| ctrl_byte == w5|| ctrl_byte == w4
|| ctrl_byte == w3|| ctrl_byte == w2
|| ctrl_byte == w1|| ctrl_byte == w0)
begin
State = 2'b10;
write_to_eeprom;
//写操作
end
else
State = 2'b00;
//State = State;
end2'b11:
read_from_eeprom;
default:
State = 2'b00;
endcase
end
end//主状态机结束//操作停止
task stop_W_R;
begin
State= 2'b00;
addr_byte_h= 0;
addr_byte_l= 0;
ctrl_byte= 0;
out_flag= 0;
sda_buf= 0;
end
endtask//读进控制字和存储单元地址
task read_in;
begin
shift_in(ctrl_byte);
shift_in(addr_byte_h);
shift_in(addr_byte_l);
end
endtask//EEPROM的写操作
task write_to_eeprom;
begin
shift_in(memory_buf);
address = {addr_byte_h[4:0], addr_byte_l};
memory[address] = memory_buf;
State = 2'b00;
end
endtask//EEPROM的读操作
task read_from_eeprom;
begin
shift_in(ctrl_byte);
if(ctrl_byte == r7 || ctrl_byte == w6
|| ctrl_byte == r5|| ctrl_byte == r4
|| ctrl_byte == r3|| ctrl_byte == r2
|| ctrl_byte == r1|| ctrl_byte == r0)
begin
address = {addr_byte_h[4:0], addr_byte_l};
sda_buf = memory[address];
shift_out;
State = 2'b00;
end
end
endtask//SDA数据线上的数据存入寄存器,数据在SCL的高电平有效
task shift_in;
output[7:0]shift;
begin
@(posedge scl) shift[7] = sda;
@(posedge scl) shift[6] = sda;
@(posedge scl) shift[5] = sda;
@(posedge scl) shift[4] = sda;
@(posedge scl) shift[3] = sda;
@(posedge scl) shift[2] = sda;
@(posedge scl) shift[1] = sda;
@(posedge scl) shift[0] = sda;
@(negedge scl)
begin
#`timeslice;
out_flag = 1;
//应答信号输出
sda_buf = 0;
end@(negedge scl)
begin
#`timeslice;
out_flag = 0;
end
end
endtask//EEPROM存储器中的数据通过SDA数据线输出,数据在SCL低电平时变化
task shift_out;
begin
out_flag = 1;
for(i=6;
i>=0;
i=i-1)
begin
@(negedge scl);
#`timeslice;
sda_buf = sda_buf << 1;
end
@(negedge scl) #`timeslice sda_buf[7] = 1;
//非应答信号输出
@(negedge scl) #`timeslice out_flag = 0;
end
endtaskendmodule
//eeprom.v文件结束
这段代码主要仿真了eeprom的i2c读写模型,你可以把它当做是一个虚拟的i2c设备,通过向这个虚拟设备传输信号,观察i2c是如何实现对eeprom的控制,因此,我认为这个模型很适合i2c的入门和仿真。该模块的示意图如下图所示:
文章图片
i2c 控制逻辑 做好上面的准备工作之后,开始写i2c的控制逻辑,网上很多代码是将所有i2c的读写都放在一段代码中,用一个状态机全部做完,我觉得这种代码结构不清晰,于是打算把代码拆分成如下两个模块:
- i2c_driver:i2c的驱动模块,将一些基本指令转化为i2c控制信号
- i2c_tran:i2c_driver的上级,把一些基本指令封装为更简便的高级指令
要注意的是,这段代码仅在eeprom仿真模型中通过,目前还没上板,主要是应答信号的处理和时序方面可能会跟具体器件相关!
module i2c_driver(
inputrst_n,
inputclk_50M,
inputi2c_valid,
input[1:0]i2c_ctrl,
inputi2c_sclk,
inputtransfer_en,
inputcapture_en,
// 00: start
// 01: stop
// 10: write
// 11: read
input[7:0]i2c_d_in,output [7:0]i2c_d_out,
outputi2c_done,
output regack_r,
// i2c port
outputscl,
inoutsda,// debug
output[2:0]i2c_state
);
reg transfer_en_d1, transfer_en_d2;
always @(posedge clk_50M) begin
transfer_en_d1 <= transfer_en;
transfer_en_d2 <= transfer_en_d1;
end// ---------------------state machine------------------
reg [2:0]state,next_state;
reg [4:0]wr_bit_cnt, rd_bit_cnt;
parameteridle= 3'd0;
parameterstart= 3'd1;
parameterstop= 3'd2;
parameterwrite= 3'd3;
parameterwack= 3'd4;
parameterread= 3'd5;
parameterrack= 3'd6;
always @ (posedge clk_50M) begin
if (!rst_n)
state <= idle;
else
state <= next_state;
end
always @ (*) begin
if (transfer_en_d1)
case (state)
idle:if (i2c_valid) begin
case(i2c_ctrl)
2'b00:next_state = start;
2'b01:next_state = stop;
2'b10:next_state = write;
2'b11:next_state = read;
default:next_state = idle;
endcase
end else
next_state = idle;
start:next_state = idle;
stop:next_state = idle;
write:if (wr_bit_cnt > 5'd7)
next_state = wack;
else
next_state = write;
wack:if (ack_r == 1'b0)
next_state = idle;
else
next_state = wack;
read:if (rd_bit_cnt > 5'd7)
next_state = rack;
else
next_state = read;
rack:if (ack_r == 1'b1)
next_state = idle;
else
next_state = rack;
default:next_state = idle;
endcase
else next_state = next_state;
end
// -----------------------------------------------------------// ---------------------scl enable------------------------------
reg scl_en;
always @(posedge clk_50M) begin
if ((!rst_n) || (state == idle))
scl_en <= 1'b0;
else scl_en <= 1'b1;
end
// -------------------------------------------------------------// -------------------------sda-------------------------------
regsda_r;
regsda_link;
// inout direction ctrl: 1-out, 0-in
// sda_link should be set to 0 before read
assign sda = (sda_link) ? sda_r : 1'bz;
assign scl = scl_en && i2c_sclk;
wirecur_data_bit;
always @(posedge clk_50M) begin
if (!rst_n) begin
sda_link <= 1'b1;
sda_r <= 1'b0;
end else
case (state)
start:if (transfer_en_d2) begin sda_link <= 1'b1;
sda_r <= 1'b1;
end
else if (capture_en)begin sda_link <= 1'b1;
sda_r <= 1'b0;
end
elsebegin sda_link <= sda_link;
sda_r <= sda_r;
end
stop:if (transfer_en_d2) begin sda_link <= 1'b1;
sda_r <= 1'b0;
end
else if (capture_en)begin sda_link <= 1'b1;
sda_r <= 1'b1;
end
elsebegin sda_link <= sda_link;
sda_r <= sda_r;
end
write:if (transfer_en_d2) begin sda_link <= 1'b1;
sda_r <= cur_data_bit;
end
elsebegin sda_link <= sda_link;
sda_r <= sda_r;
endwack:if (transfer_en_d2) begin sda_link <= 1'b0;
sda_r <= sda_r;
end
elsebegin sda_link <= sda_link;
sda_r <= sda_r;
end
read:if (transfer_en_d2) begin sda_link <= 1'b0;
sda_r <= sda_r;
end
elsebegin sda_link <= sda_link;
sda_r <= sda_r;
end
rack:if (transfer_en_d2) begin sda_link <= 1'b0;
sda_r <= sda_r;
end
elsebegin sda_link <= sda_link;
sda_r <= sda_r;
end
default:begin sda_link <= sda_link;
sda_r <= sda_r;
end
endcase
end
// ------------------------------------------------------------// -----------------------write data reg-----------------------
reg [7:0]wr_data_r;
always @(posedge clk_50M) begin
if (!rst_n)
wr_data_r <= 8'd0;
else if ((i2c_valid) && (i2c_ctrl == 2'b10))
wr_data_r <= i2c_d_in;
else if (capture_en) wr_data_r <= {wr_data_r[6:0], 1'b0};
else wr_data_r <= wr_data_r;
end
assign cur_data_bit = wr_data_r[7];
// ------------------------------------------------------------// -----------------------write_bit_count----------------------
always @(posedge clk_50M) begin
if ((!rst_n) || (state == idle)) begin
wr_bit_cnt <= 5'd0;
end else if ((capture_en) && (state == write))
wr_bit_cnt <= wr_bit_cnt + 5'd1;
else wr_bit_cnt <= wr_bit_cnt;
end
// -------------------------------------------------------------// ---------------------read bit reg----------------------------
reg [7:0]rd_data_r;
always @(posedge clk_50M) begin
if ((!rst_n) || ((state == idle) && (i2c_valid)))
rd_data_r <= 8'd0;
else if ((capture_en) && (state == read))
rd_data_r <= {rd_data_r[6:0], sda};
else
rd_data_r <= rd_data_r;
end
// -------------------------------------------------------------// ---------------------i2c data out----------------------------
assign i2c_d_out = (state == idle) ? rd_data_r : 8'dx;
// -------------------------------------------------------------// ---------------------read_bit_count--------------------------
always @(posedge clk_50M) begin
if ((!rst_n) || (state == idle))
rd_bit_cnt <= 5'd0;
else if ((capture_en) && (state == read))
rd_bit_cnt <= rd_bit_cnt + 5'd1;
else rd_bit_cnt <= rd_bit_cnt;
end
// -------------------------------------------------------------// ----------------------ack reg--------------------------------
always @(posedge clk_50M) begin
if ((!rst_n) || (state == idle))
ack_r <= 1'b1;
else if (capture_en) begin
if (state == write)
ack_r <= 1'b1;
else if (state == read)
ack_r <= 1'b0;
else if ((state == wack) || (state == rack))
ack_r <= sda;
else
ack_r <= ack_r;
end else
ack_r <= ack_r;
end
// -------------------------------------------------------------// ---------------------i2c done---------------------------------
assign i2c_done = (next_state == idle) && (!i2c_valid);
// -------------------------------------------------------------// -----------------------debug---------------------------------
assign i2c_state = state;
// -------------------------------------------------------------
endmodule
上面是实现的i2c_driver的代码,i2c_tran的代码暂不给出,主要的设计思想是:首先根据系统时钟求出参考的时钟sclk_r,之后在sclk_r的高低电平的中点做标记,标记信号就是代码中的transfer_en和capture_en信号,这两个信号维持时间是一个50MHz的clock cycle,最后,再根据i2c协议的读写要求,用状态机依次实现。
为了便于使用和调试,我还加入了简单的握手信号:valid和done。这部分的逻辑如下:当发送机把指令和数据准备好后,会看当前的接收机是否空闲(done是否为1),若空闲,则发出一个持续几个周期的valid脉冲;接收机接收到valid脉冲后立刻把done拉低(忙碌),同时锁存此刻的输入指令和数据,并根据指令开始跑一个状态机周期,当接收机再次回到初始态时,再次把done信号置1,表示空闲。
下面是我的testbench:
`timescale 1ns/1ns
module test_top();
reg clk_50M, rst_n, data_valid, mode;
reg [7:0] devaddr, subaddr1, subaddr2, data_in;
wire tran_done;
wire [7:0] data_out;
wire scl, sda;
i2c_tran u1(
.clk_50M(clk_50M), .rst_n(rst_n),
.data_valid(data_valid), .mode(mode),
.devaddr(devaddr), .subaddr1(subaddr1), .subaddr2(subaddr2), .data_in(data_in),
.tran_done(tran_done), .data_out(data_out),
.scl(scl), .sda(sda),
.ack_r(ack_r));
EEPROM_AT24C64 eeprom(.scl(scl), .sda(sda));
initial clk_50M = 0;
always begin
#5 clk_50M = ~clk_50M;
endinitial begin
rst_n = 1;
data_valid = 0;
#100;
rst_n = 0;
#100;
rst_n = 1;
// prepare data
mode = 0;
devaddr = 8'hAE;
subaddr1 = 8'h00;
subaddr2 = 8'h00;
data_in = 8'h31;
#200 data_valid = 1;
// set valid to 1
#5000 data_valid = 0;
// set valid to 0#160000;
mode = 0;
devaddr = 8'hAE;
subaddr1 = 8'h00;
subaddr2 = 8'h01;
data_in = 8'h13;
#200 data_valid = 1;
#5000 data_valid = 0;
#160000;
mode = 1;
devaddr = 8'hAE;
subaddr1 = 8'h00;
subaddr2 = 8'h00;
data_in = 8'h00;
#200 data_valid = 1;
#5000 data_valid = 0;
#160000;
mode = 1;
devaddr = 8'hAE;
subaddr1 = 8'h00;
subaddr2 = 8'h01;
data_in = 8'h00;
#200 data_valid = 1;
#5000 data_valid = 0;
#200;
mode = 0;
devaddr = 8'h00;
subaddr1 = 8'h00;
subaddr2 = 8'h00;
data_in = 8'h00;
$stop;
end
endmodule
其中,这样一段代码就是一次读/写指令:
mode = 0;
devaddr = 8'hAE;
subaddr1 = 8'h00;
subaddr2 = 8'h00;
data_in = 8'h31;
#200 data_valid = 1;
// set valid to 1
#5000 data_valid = 0;
// set valid to 0
- mode 为0 代表写数据指令,为1则代表是读数据指令
- devaddr 代表i2c设备的8位地址
- subaddr1 和 subaddr2 代表子地址段
- data_in 代表要往地址写入的数据
- data_valid 信号要在数据载入完成后再置为1,并及时清零(再一次读写完成前)
下面是我的仿真波形,为了便于调试,我将ack信号也引出。
文章图片
从波形上可以看到,通过i2c_driver和i2c_tran程序搭建的模块,可以成功将数据写入和读出模拟eeprom模块,说明程序的基本时序符合i2c协议的标准。
文章图片
工程包括一个i2c_top.v,主要是用于控制button、led、pll、ila等模块,并且包含了一个小小的状态机用于发出指令(idle-load-waits),button每按一次会发出一个持续几百个周期的data_valid信号,用于启动一次操作。
并且每按一次指令内容更换一次,如下面的代码所示
3'd1:begin mode <= 1'b0;
devaddr <= 8'hA0;
subaddr<= 8'h00;
data_in<= 8'h13;
end
3'd2:begin mode <= 1'b0;
devaddr <= 8'hA0;
subaddr<= 8'h01;
data_in<= 8'h31;
end
3'd3:begin mode <= 1'b1;
devaddr <= 8'hA0;
subaddr<= 8'h00;
data_in<= 8'h00;
end
3'd4:begin mode <= 1'b1;
devaddr <= 8'hA0;
subaddr<= 8'h01;
data_in<= 8'h00;
end
4条指令分别是:
- 往0xA0的设备的0x00地址写入数据0x13
- 往0xA0的设备的0x01地址写入数据0x31
- 往0xA0的设备的0x00地址读取数据
- 往0xA0的设备的0x01地址读取数据
下图是ILA抓到的波形,可以看到成功读回0x13和0x31两个值
文章图片
文章图片
推荐阅读
- 简单接口开发|Verilog实现IIC协议读写EEPROM
- FPGA|FPGA实验记录五(I2C读取AHT10温湿度传感器)
- FPGA|FPGA实验记录四(基于FPGA的VGA协议实现)
- FPGA|FPGA基础协议二(I2C读写E2PROM)
- FPGA|FPGA图像处理(一)(边缘检测)
- FPGA|【FPGA】UART串口通信
- FPGA自学|FPGA的I2C的原理及应用(含有源码)
- FPGA|【FPGA】UART串口通信---基于FIFO
- HDLbits|Circuits--Sequential Logic--Latches and Flip-Flops--Edgecapture