i2c|用verilog 实现的 i2c控制模块

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|用verilog 实现的 i2c控制模块
文章图片

i2c 控制逻辑 做好上面的准备工作之后,开始写i2c的控制逻辑,网上很多代码是将所有i2c的读写都放在一段代码中,用一个状态机全部做完,我觉得这种代码结构不清晰,于是打算把代码拆分成如下两个模块:
  1. i2c_driver:i2c的驱动模块,将一些基本指令转化为i2c控制信号
  2. i2c_tran:i2c_driver的上级,把一些基本指令封装为更简便的高级指令
【i2c|用verilog 实现的 i2c控制模块】下面是实现的代码,感兴趣的同学欢迎讨论。
要注意的是,这段代码仅在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

  1. mode 为0 代表写数据指令,为1则代表是读数据指令
  2. devaddr 代表i2c设备的8位地址
  3. subaddr1 和 subaddr2 代表子地址段
  4. data_in 代表要往地址写入的数据
  5. data_valid 信号要在数据载入完成后再置为1,并及时清零(再一次读写完成前)
    下面是我的仿真波形,为了便于调试,我将ack信号也引出。
    i2c|用verilog 实现的 i2c控制模块
    文章图片

    从波形上可以看到,通过i2c_driver和i2c_tran程序搭建的模块,可以成功将数据写入和读出模拟eeprom模块,说明程序的基本时序符合i2c协议的标准。
上板!! 经过一天的调试,终于上板成功,先附上工程目录结构:
i2c|用verilog 实现的 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条指令分别是:
  1. 往0xA0的设备的0x00地址写入数据0x13
  2. 往0xA0的设备的0x01地址写入数据0x31
  3. 往0xA0的设备的0x00地址读取数据
  4. 往0xA0的设备的0x01地址读取数据
    下图是ILA抓到的波形,可以看到成功读回0x13和0x31两个值
    i2c|用verilog 实现的 i2c控制模块
    文章图片

    i2c|用verilog 实现的 i2c控制模块
    文章图片

    推荐阅读