赞
踩
本文涉及代码可以从下方链接下载:
https://gitee.com/huangzhc3/i2c_sim
首先要先学习一下i2c协议的基础,这里有一份官方文档(https://www.nxp.com/docs/en/user-guide/UM10204.pdf),也可以参考一些博客,总的来说i2c协议的基础原理不难,因为主要就是两根线的控制(SCL、SDA)。
当掌握了i2c协议的基础之后,建议以EEPROM为例子入手,有一篇博客介绍的很好:https://www.cnblogs.com/ninghechuan/p/9534893.html(需要有一定verilog基础),下面贴出原文中给出的eeprom仿真模型:
`timescale 1ns/1ns `define timeslice 1250 //`define timeslice 300 module 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; end 2'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 endtask endmodule //eeprom.v文件结束
这段代码主要仿真了eeprom的i2c读写模型,你可以把它当做是一个虚拟的i2c设备,通过向这个虚拟设备传输信号,观察i2c是如何实现对eeprom的控制,因此,我认为这个模型很适合i2c的入门和仿真。该模块的示意图如下图所示:
做好上面的准备工作之后,开始写i2c的控制逻辑,网上很多代码是将所有i2c的读写都放在一段代码中,用一个状态机全部做完,我觉得这种代码结构不清晰,于是打算把代码拆分成如下两个模块:
下面是实现的代码,感兴趣的同学欢迎讨论。
要注意的是,这段代码仅在eeprom仿真模型中通过,目前还没上板,主要是应答信号的处理和时序方面可能会跟具体器件相关!
module i2c_driver( input rst_n, input clk_50M, input i2c_valid, input [1:0] i2c_ctrl, input i2c_sclk, input transfer_en, input capture_en, // 00: start // 01: stop // 10: write // 11: read input [7:0] i2c_d_in, output [7:0] i2c_d_out, output i2c_done, output reg ack_r, // i2c port output scl, inout sda, // 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; parameter idle = 3'd0; parameter start = 3'd1; parameter stop = 3'd2; parameter write = 3'd3; parameter wack = 3'd4; parameter read = 3'd5; parameter rack = 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------------------------------- reg sda_r; reg sda_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; wire cur_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 else begin 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 else begin 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 else begin sda_link <= sda_link; sda_r <= sda_r; end wack: if (transfer_en_d2) begin sda_link <= 1'b0; sda_r <= sda_r; end else begin sda_link <= sda_link; sda_r <= sda_r; end read: if (transfer_en_d2) begin sda_link <= 1'b0; sda_r <= sda_r; end else begin sda_link <= sda_link; sda_r <= sda_r; end rack: if (transfer_en_d2) begin sda_link <= 1'b0; sda_r <= sda_r; end else begin 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; end initial 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
经过一天的调试,终于上板成功,先附上工程目录结构:
工程包括一个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条指令分别是:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。