FPGA基础入门【15】开发板I2C温度传感器配置

上一篇教程挖了一个NEXYS4 开发板中MicroSD的天坑,发现难度过高,打算放到之后再完成,这一篇来电不这么重口味的温度传感器配置。在NEXYS 4开发板上的温度传感器使用的是I2C接口,这个接口很常见,可以写一个模块留着以后使用

FPGA基础入门【15】开发板I2C温度传感器配置

  • 开发板温度传感器
    • I2C接口简介
    • ADT7420简介
    • 寄存器
  • 逻辑设计
    • I2C控制模块
    • UART串口控制模块
    • 顶层逻辑
  • 模拟仿真
    • Testbench
    • 仿真脚本
    • 仿真结果
  • 编译测试
    • 结果
  • 总结

开发板温度传感器

NEXYS 4文档中写着它使用的温度传感器名字是Analog Device ADT7420,提供16位精度,一般精确到0.25摄氏度,用I2C接口和FPGA连接如下:
FPGA基础入门【15】开发板I2C温度传感器配置_第1张图片
SCL和SDA是I2C接口,用来和FPGA连接。可以看到它只有两根线,非常精简,一般用来和不太复杂的外围设备连接。并且它还有地址识别,因此1个master设备可以同时和多个slave设备相连。剩下两条线表示温度溢出(过高或者过低),以及温度高到有危险的程度。

ADT7420的文档链接:ADT7420

I2C接口简介

从ADT7420的文档中读到,它的时钟需求是400kHz以下,我们采用200kHz,需要把系统时钟100MHz减慢500倍

和这款芯片相关的I2C的时序如下:
写单字节数据
FPGA基础入门【15】开发板I2C温度传感器配置_第2张图片
写双字节数据:
FPGA基础入门【15】开发板I2C温度传感器配置_第3张图片
从配置寄存器中读取数据:
FPGA基础入门【15】开发板I2C温度传感器配置_第4张图片
读取温度数据:
FPGA基础入门【15】开发板I2C温度传感器配置_第5张图片

  • 在SCL为高电平时,拉低SDA是开始信号
  • 在SCL为高电平时,拉高SDA是结束信号
  • Master设备在读回信号的最后传出低电平表示,再来一个,高电平表示,够了够了不用再传了

从这个时序图看出,I2C读写控制参数有四个:读写控制、寄存器地址、读写数据长度、写的8位数据(每完成一次输出就更新一次)

ADT7420简介

网上很多Arduino用的温度传感器模块用的就是这款芯片,引脚如下:
FPGA基础入门【15】开发板I2C温度传感器配置_第6张图片
其中SCL和SDA是和FPGA相连的I2C接口,CT和INT是临界温度警告,VDD和GND是电源和接地,A0和A1是低地址位,在芯片较少时可以直接通过连线来同时连接几个芯片。

NEXYS 4文档中说要制定slave地址0x4B来和传感器通信,通过前面的I2C接口介绍可以看出,开发板把A0和A1两个引脚都拉高了。

ADT7420在上电后会自动进入简单温度传感器模式,不需要初始化配置。设备地址寄存器一开始被指向温度数据的高位MSB,因此不用制定地址读出来的第一个字节就是温度的MSB,第二个字节是LSB,组成需要的16位结果。因此上面的时序图中读取温度的两步,如果一开始没有做别的操作,可以把写地址省略。把16位结果右移3位,再除以16,就可以得到摄氏温度

寄存器

ADT7420的寄存器全家福及其默认值如下:
FPGA基础入门【15】开发板I2C温度传感器配置_第7张图片
这里我们只关注温度高低位、状态和配置四个寄存器

温度高低位:
FPGA基础入门【15】开发板I2C温度传感器配置_第8张图片
状态寄存器,复位后要先等这个寄存器的最高位变成0:
FPGA基础入门【15】开发板I2C温度传感器配置_第9张图片
配置寄存器:
FPGA基础入门【15】开发板I2C温度传感器配置_第10张图片

逻辑设计

首先需要一个I2C的控制逻辑模块I2C_transmitter.v,然后就可以配置一个串口控制器,随时读取寄存器与数据。这里我们把之前做过的串口模块集成一下,加个FIFO以便之后再使用

这次的代码比较长,要把之前做过的一些东西做个综合

I2C控制模块

从前面的I2C时序逻辑可以分析,定义一段I2C数据传输参数有这么几个:寄存器地址、读写选择、读写长度、需要写入的8位数据。

FPGA基础入门【15】开发板I2C温度传感器配置_第11张图片

新建一个代码文件I2C_transmitter.v,代码如下:

顶层接口配置,前面分析过I2C必要参数,还需要一些其他的控制信号

  • 设备地址dev_addr,这个应该连到固定值0x4B
  • 读写控制rdh_wrl,高电平代表读,低电平代表写
  • 寄存器地址reg_addr
  • 操作准备信号ready,它的上升沿代表数据准备完成,可以进行I2C传输
  • 输出8位数据dout
  • 传输长度dout_length
  • 输出确认dout_ack,表示一个byte已经输出完成,可以更新到下一个byte
  • 输入8位数据din
  • 输入准备信号din_valid,每当此信号升高,就有一个byte输入在din接口准备好
module I2C_transmitter(
    input             clk,
    input             rst,
    
    output reg        I2C_SCL,
    output reg        I2C_SDA_out,
    input             I2C_SDA_in,
    output reg        I2C_SDA_oe,
    
    input      [6:0]  dev_addr,     // device address
    input      [7:0]  reg_addr,     // register address
    input             rdh_wrl,      // 1 is read, 0 is write
    input             ready,        // write and read ready
    input      [7:0]  dout,         // write data
    output reg        dout_ack,     // write data acknowledge by slave device
    input      [3:0]  dout_length,  // the number of bytes of write and read data
    output reg [7:0]  din,
    output reg        din_valid
);

生成I2C接口需要的100kHz时钟,用I2C_SCL_en来使能控制

// SCL clock generator, 100MHz => 200kHz
reg [7:0] I2C_SCL_counter;
reg       I2C_SCL_en;   // enable signal, SCL only driven when this one asserted
reg       I2C_SCL_d;
wire      I2C_SCL_posedge;
wire      I2C_SCL_negedge;

always @(posedge clk) begin
    if(rst | ~(I2C_SCL_en)) begin
        I2C_SCL_counter <= 8'd0;
        I2C_SCL <= 1'b1;
    end
    else if(I2C_SCL_counter < 8'd250) begin
        I2C_SCL_counter <= I2C_SCL_counter + 8'd1;
    end
    else begin
        I2C_SCL_counter <= 8'd0;
        I2C_SCL <= ~I2C_SCL;
    end
end

实时监测SCL的上升沿和下降沿,由于SDA是双向的信号,读的时候用上升沿,写的时候用下降沿,经常需要切换

// detection of falling edge of SCL
always @(posedge clk) begin
    I2C_SCL_d <= I2C_SCL;
end
assign I2C_SCL_negedge = ({I2C_SCL_d,I2C_SCL}==2'b10) ? 1'b1 : 1'b0;
assign I2C_SCL_posedge = ({I2C_SCL_d,I2C_SCL}==2'b01) ? 1'b1 : 1'b0;

探测ready信号的上升沿以开始I2C传输

// ready rising edge detection
reg  ready_d;
wire ready_posedge;
always @(posedge clk) begin
    ready_d <= ready;
end
assign ready_posedge = ({ready_d, ready}==2'b01) ? 1'b1 : 1'b0;

状态机配置

// state machine
parameter [3:0] IDLE = 0;
parameter [3:0] WAIT = 1;
parameter [3:0] ADDR_DEV_WRITE = 2;
parameter [3:0] ADDR_REG = 3;
parameter [3:0] REPEAT_START = 4;
parameter [3:0] ADDR_DEV_READ = 5;
parameter [3:0] WRITE = 6;
parameter [3:0] READ = 7;
parameter [3:0] ENDING = 8;

reg  [3:0] state;
reg  [3:0] next_state;
reg  [3:0] I2C_SCL_count;
reg  [7:0] dout_buf;
reg  [3:0] dout_count;
reg  [7:0] din_buf;
reg  [7:0] end_count;

always @(posedge clk or posedge rst) begin
    if(rst) begin
        state <= IDLE;
    end
    else begin
        state <= next_state;
    end
end

always @(posedge clk) begin
    case(state)
    IDLE: begin
        dout_ack <= 1'b0;
        I2C_SCL_count <= 4'd0;
        din <= 8'h00;
        din_valid <= 1'b0;
        I2C_SDA_out <= 1'b1;
        I2C_SDA_oe <= 1'b1;
        next_state <= WAIT;
        dout_buf <= 8'h00;
        I2C_SCL_en <= 1'b0;
        dout_count <= 4'd0;
        end_count <= 8'd0;
    end

侦测到ready上升沿,在SCL为高的情况下拉低SDA表示开始(Start by master),这也就是SCL生成器在复位情况下为高的原因

    WAIT: begin
        if(ready_posedge) begin
            next_state <= ADDR_DEV_WRITE;
            dout_buf <= {dev_addr, 1'b0};    // the first step is always write register address
            I2C_SDA_out <= 1'b0;
            I2C_SDA_oe <= 1'b1;
            I2C_SCL_en <= 1'b1;
        end
    end

输出设备地址0x4B,带上最低位为1表示写入,经历9个时钟周期,最后一个上升沿观察设备是否传回一个ack信号

    ADDR_DEV_WRITE: begin
        if(I2C_SCL_negedge && (I2C_SCL_count < 4'd8) ) begin
            I2C_SCL_count <= I2C_SCL_count + 4'd1;
            {I2C_SDA_out, dout_buf} <= {dout_buf, 1'b0};
            I2C_SDA_oe <= 1'b1;
        end
        else if(I2C_SCL_negedge && (I2C_SCL_count == 4'd8)) begin
            I2C_SCL_count <= I2C_SCL_count + 4'd1;
            I2C_SDA_oe <= 1'b0;
        end
        else if(I2C_SCL_posedge && (I2C_SCL_count == 4'd9)) begin
            I2C_SCL_count <= 4'd0;
            dout_buf <= reg_addr;
            if(~I2C_SDA_in) begin   // acknowledged by device and turn to ADDR_REG state
                next_state <= ADDR_REG;
            end
            else begin  // not acknowledged, go to ENDING
                next_state <= ENDING;
            end
        end
    end

写入寄存器地址,根据读写控制信号进入读流程或者写流程

    ADDR_REG: begin
        if(I2C_SCL_negedge && (I2C_SCL_count < 4'd8) ) begin
            I2C_SCL_count <= I2C_SCL_count + 4'd1;
            {I2C_SDA_out, dout_buf} <= {dout_buf, 1'b0};
            I2C_SDA_oe <= 1'b1;
        end
        else if(I2C_SCL_negedge && (I2C_SCL_count == 4'd8)) begin
            I2C_SCL_count <= I2C_SCL_count + 4'd1;
            I2C_SDA_oe <= 1'b0;
        end
        else if(I2C_SCL_posedge && (I2C_SCL_count == 4'd9)) begin
            I2C_SCL_count <= 4'd0;
            if(rdh_wrl && ~I2C_SDA_in) begin   // acknowledged by device and turn to read state
                next_state <= REPEAT_START;
                dout_buf <= {dev_addr, 1'b1};
            end
            else if(~rdh_wrl && ~I2C_SDA_in) begin  // acknowledged by device and turn to write state
                next_state <= WRITE;
                dout_buf <= dout;
            end
            else begin  // not acknowledged, go to ENDING
                next_state <= ENDING;
            end
        end
    end

当需要读数据时,需要再次进行Start by master,拉高SDA后,在SCL的高电平时拉低SDA

    REPEAT_START: begin
        //  not stopped by master
        if(I2C_SCL_negedge) begin
            I2C_SDA_oe <= 1'b1;
            I2C_SDA_out <= 1'b1;
        end
        else if(I2C_SCL_posedge) begin
            I2C_SCL_en <= 1'b0;
        end
        // delay a while and pull down the SDA, indicating repeat start
        else if(~I2C_SCL_en && (end_count < 8'd250)) begin
            end_count <= end_count + 8'd1;
        end
        else if(~I2C_SCL_en) begin
            end_count <= 8'd0;
            I2C_SDA_out <= 1'b0;
            I2C_SCL_en <= 1'b1;
            next_state <= ADDR_DEV_READ;
        end
    end

读流程还需要再写一次设备地址,并把最后一位改成高电平以表示读取

    ADDR_DEV_READ: begin
        if(I2C_SCL_negedge && (I2C_SCL_count < 4'd8) ) begin
            I2C_SCL_count <= I2C_SCL_count + 4'd1;
            {I2C_SDA_out, dout_buf} <= {dout_buf, 1'b0};
            I2C_SDA_oe <= 1'b1;
        end
        else if(I2C_SCL_negedge && (I2C_SCL_count == 4'd8)) begin
            I2C_SCL_count <= I2C_SCL_count + 4'd1;
            I2C_SDA_oe <= 1'b0;
        end
        else if(I2C_SCL_posedge && (I2C_SCL_count == 4'd9)) begin
            I2C_SCL_count <= 4'd0;
            if(~I2C_SDA_in) begin   // acknowledged by device and turn to read state
                next_state <= READ;
            end
            else begin  // not acknowledged, go to ENDING
                next_state <= ENDING;
            end
        end
    end

写操作,将一个byte输出给设备,获得ack信号后计算是否已经输出到指定长度,如果已经达到则进入结束流程

    WRITE: begin        
        if(I2C_SCL_negedge && (I2C_SCL_count < 4'd8) ) begin
            I2C_SCL_count <= I2C_SCL_count + 4'd1;
            {I2C_SDA_out, dout_buf} <= {dout_buf, 1'b0};
            dout_ack <= 1'b0;
            I2C_SDA_oe <= 1'b1;
        end
        else if(I2C_SCL_negedge && (I2C_SCL_count == 4'd8)) begin
            I2C_SCL_count <= I2C_SCL_count + 4'd1;
            dout_ack <= 1'b1; // indicate that ready to take next output data
            I2C_SDA_oe <= 1'b0;
        end
        else if(I2C_SCL_posedge && (I2C_SCL_count == 4'd9)) begin
            dout_buf <= dout;
            I2C_SCL_count <= 4'd0;
            if(~I2C_SDA_in && (dout_count == (dout_length - 4'd1)) ) begin   // acknowledged by device, write enough, go to ENDING
                next_state <= ENDING;
                dout_count <= 4'd0;
            end
            else if(~I2C_SDA_in) begin  // acknowledged by device, not write enough, keep in WRITE
                next_state <= WRITE;
                dout_count <= dout_count + 4'd1;
            end
            else begin // not acknowledged by device, go to ENDING
                next_state <= ENDING;
                dout_count <= 4'd0;
            end
        end
        else begin
            dout_ack <= 1'b0;
        end
    end

读操作,从设备读取一个byte后,如果还没有读取足够数据,则在第九个时钟周期输出一个低电平作为ack信号,否则输出一个高电平作为no ack信号,表示master已经读取了足够的数据

    READ: begin
        if(I2C_SCL_posedge && (I2C_SCL_count < 4'd8) ) begin
            I2C_SCL_count <= I2C_SCL_count + 4'd1;
            din_buf <= {din_buf[6:0], I2C_SDA_in};
            din_valid <= 1'b0;
            I2C_SDA_oe <= 1'b0;
        end
        else if(I2C_SCL_negedge && (I2C_SCL_count == 4'd8)) begin
            I2C_SCL_count <= I2C_SCL_count + 4'd1;
            din <= din_buf;
            din_valid <= 1'b1;
            I2C_SDA_oe <= 1'b1;
            if(dout_count == (dout_length - 4'd1) ) begin // already read enough, send no ack
                I2C_SDA_out <= 1'b1;
            end
            else begin  // need more data, send ack to device
                I2C_SDA_out <= 1'b0;
            end
        end
        else if(I2C_SCL_negedge && (I2C_SCL_count == 4'd9)) begin
            I2C_SCL_count <= 4'd0;
            I2C_SDA_oe <= 1'b0;
            if(dout_count == (dout_length - 4'd1) ) begin // already read enough, go to ENDING
                next_state <= ENDING;
                dout_count <= 4'd0;
            end
            else begin  // need more data, continue in READ state
                next_state <= READ;
                dout_count <= dout_count + 4'd1;
            end
        end
        else begin
            din_valid <= 1'b0;
        end
    end

结尾操作,关闭SCL生成器,在SCL的高电平拉高SDA(Stop by master)

    ENDING: begin
        if(I2C_SCL_posedge) begin
            I2C_SCL_en <= 1'b0;
            I2C_SDA_oe <= 1'b1;
            I2C_SDA_out <= 1'b0;
        end
        
        // delay a while and pull up the SDA, indicating the end
        if(~I2C_SCL_en && (end_count < 8'd250)) begin
            end_count <= end_count + 8'd1;
        end
        else if(~I2C_SCL_en) begin
            I2C_SDA_out <= 1'b1;
            next_state <= IDLE;
        end
    end
    endcase
end

endmodule

UART串口控制模块

看过前面教程的人可能会觉得奇怪,为什么还需要写UART串口控制的模块,明明已经有它的逻辑代码了。问题是ADT7420的I2C接口(包括不少其他芯片的I2C)使用的时钟是400kHz以下,一般比UART串口用的波特率115200要高很多,如果继续用之前的简单逻辑会出现串口数据还没送完,新的数据就已经进来的情况。

这里我们改进一下串口逻辑,添加一个同步先入先出队列(First in first out, FIFO)。FIFO在FPGA设计中非常常见,通过调用少量存储器平衡写入和读出两端的速度差。同步是指读写用的是同一个时钟,读写使能可以在不同的时间段激活,对于高速时钟控制低速接口很有用。异步FIFO是用在读写用的是不同时钟的情况下(不是读写使能信号),比起同步FIFO,它的难点在于跨越了时钟域,这部分有很多细节,以后再写。

同步FIFO的代码syn_fifo.v如下:

顶层接口定义,这里我们用了模块定义参数,在名称后面加上井号#与一对括号,在其中定义一些与该模块有关的参数,比如FIFO的数据宽度和地址长度,这样在调用相似模块时不用写多个模块,只需要在调用时候配置不同参数即可

接口比较简单,rd_en读使能的高电平时读取一个FIFO数据到data_out,wr_en写使能的高电平时写一个data_in到FIFO中,empty和full分表代表FIFO空了或者满了,避免出现错误

module syn_fifo #
(
// FIFO constants
parameter DATA_WIDTH = 8,
parameter ADDR_WIDTH = 8
)
(
input                       clk      , // Clock input
input                       rst      , // Active high reset
input      [DATA_WIDTH-1:0] data_in  , // Data input
input                       rd_en    , // Read enable
input                       wr_en    , // Write Enable
output reg [DATA_WIDTH-1:0] data_out , // Data Output
output                      empty    , // FIFO empty
output                      full       // FIFO full
);    

最大深度由地址宽度决定,定义RAM的读指针和写指针(可循环),加上一个FIFO有效计数器,用来观察FIFO是空还是满。

这里data_ram是RAM的核心,定义比较特殊,前面的宽度代表每个地址对应的数据宽度,后面的是定义它的深度

// RAM definition
parameter RAM_DEPTH = (1 << ADDR_WIDTH);
reg [DATA_WIDTH-1:0] data_ram[ADDR_WIDTH-1:0];

// Pointers and counters
reg [ADDR_WIDTH-1:0] wr_pointer;
reg [ADDR_WIDTH-1:0] rd_pointer;
reg [ADDR_WIDTH :0] status_cnt;

assign full = (status_cnt == (RAM_DEPTH-1));
assign empty = (status_cnt == 0);

// WRITE_POINTER
always @(posedge clk or posedge rst) begin
    if(rst) begin
        wr_pointer <= 0;
    end else if(wr_en) begin
        wr_pointer <= wr_pointer + 1;
    end
end

// READ_POINTER
always @(posedge clk or posedge rst) begin
    if(rst) begin
        rd_pointer <= 0;
    end 
    else if(rd_en) begin
        rd_pointer <= rd_pointer + 1;
    end
end

定义一个RAM,通过操控读写的地址来读写数据

// READ DATA
always  @(posedge clk or posedge rst) begin
    if(rst) begin
        data_out <= 0;
    end 
    else if(rd_en) begin
        data_out <= data_ram[rd_pointer];
    end
end

// WRITE DATA
always  @(posedge clk) begin
    if(wr_en) begin
        data_ram[wr_pointer] <= data_in;
    end
end

// STATUS COUNTER
always @(posedge clk or posedge rst) begin
    if(rst) begin
        status_cnt <= 0;
    // Read but no write.
    end 
    else if(rd_en &&  !wr_en && (status_cnt != 0)) begin
        status_cnt <= status_cnt - 1;
    // Write but no read.
    end 
    else if(wr_en &&  !rd_en && (status_cnt != RAM_DEPTH)) begin
        status_cnt <= status_cnt + 1;
    end
end

endmodule

有了FIFO的代码,我们可以改进之前的串口控制代码为UART_transmitter.v:

顶层定义,除了UART必要的接口外,加上dout、din以及他们的准备信号。这些信号都是十六进制数,每4位都是一位数

module UART_transmitter(
    input      clk,
    input      rst,
    
    // UART port
    output reg TXD,
    input      RXD,
    output reg CTS,
    input      RTS,
    
    // Control port
    input      [7:0] dout,
    input            dout_ready,
    output reg [3:0] din,
    output reg       din_valid
);

加入前面写好的FIFO代码,调用时参数的配置可以参考下面的代码

// FIFO definition
wire [7:0] fifo_data_in;
wire [7:0] fifo_data_out;
reg        fifo_rd_en;
reg        fifo_rd_en_d1;
reg        fifo_rd_en_d2;
wire       fifo_wr_en;
wire       fifo_empty;
wire       fifo_full;

assign fifo_data_in = dout;
assign fifo_wr_en = dout_ready;

syn_fifo #
(
    // FIFO constants
    .DATA_WIDTH(8),
    .ADDR_WIDTH(4)
) syn_fifo
(
    .clk      (clk), // Clock input
    .rst      (rst), // Active high reset
    .data_in  (fifo_data_in), // Data input
    .rd_en    (fifo_rd_en), // Read enable
    .wr_en    (fifo_wr_en), // Write Enable
    .data_out (fifo_data_out), // Data Output
    .empty    (fifo_empty), // FIFO empty
    .full     (fifo_full)   // FIFO full
);    

和之前差不多的串口发送端代码,去除了接收回传,这部分由调用串口模块的顶层做

// resource definition
reg [15:0] tx_count;
reg [19:0] tx_shift;
reg        tx_start;
reg [19:0] CTS_delay;
reg [7:0]  RXD_delay;
reg [15:0] rx_count;
reg [3:0]  rx_bit_count;
reg        rx_start;

always @(posedge clk) begin
    fifo_rd_en_d1 <= fifo_rd_en;
    fifo_rd_en_d2 <= fifo_rd_en_d1;
end

always @(posedge clk or posedge rst) begin
    if(rst) begin
        tx_count <= 16'd0;
        TXD <= 1'b1;
		tx_shift <= 20'hFFFFF;
        CTS <= 1'b1;
        CTS_delay <= 20'hFFFFF;
        fifo_rd_en <= 1'b0;
        tx_start <= 1'b0;
    end
    // When FIFO is not empty, and last sending completed, read the next data, and send through UART
    else if(~tx_start && ~fifo_empty) begin
        fifo_rd_en <= 1'b1;
        tx_start <= 1'b1;
    end
    // FIFO ready complete, get the data, transfer and buffer it into register
    else if(fifo_rd_en_d2) begin
        fifo_rd_en <= 1'b0;
        
        case(fifo_data_out[3:0])
        4'h0: begin tx_shift[9:0] <= 10'b0000011001; end
        4'h1: begin tx_shift[9:0] <= 10'b0100011001; end
        4'h2: begin tx_shift[9:0] <= 10'b0010011001; end
        4'h3: begin tx_shift[9:0] <= 10'b0110011001; end
        4'h4: begin tx_shift[9:0] <= 10'b0001011001; end
        4'h5: begin tx_shift[9:0] <= 10'b0101011001; end
        4'h6: begin tx_shift[9:0] <= 10'b0011011001; end
        4'h7: begin tx_shift[9:0] <= 10'b0111011001; end
        4'h8: begin tx_shift[9:0] <= 10'b0000111001; end
        4'h9: begin tx_shift[9:0] <= 10'b0100111001; end
        4'hA: begin tx_shift[9:0] <= 10'b0100000101; end
        4'hB: begin tx_shift[9:0] <= 10'b0010000101; end
        4'hC: begin tx_shift[9:0] <= 10'b0110000101; end
        4'hD: begin tx_shift[9:0] <= 10'b0001000101; end
        4'hE: begin tx_shift[9:0] <= 10'b0101000101; end
        4'hF: begin tx_shift[9:0] <= 10'b0011000101; end
        endcase
        
        case(fifo_data_out[7:4])
        4'h0: begin tx_shift[19:10] <= 10'b0000011001; end
        4'h1: begin tx_shift[19:10] <= 10'b0100011001; end
        4'h2: begin tx_shift[19:10] <= 10'b0010011001; end
        4'h3: begin tx_shift[19:10] <= 10'b0110011001; end
        4'h4: begin tx_shift[19:10] <= 10'b0001011001; end
        4'h5: begin tx_shift[19:10] <= 10'b0101011001; end
        4'h6: begin tx_shift[19:10] <= 10'b0011011001; end
        4'h7: begin tx_shift[19:10] <= 10'b0111011001; end
        4'h8: begin tx_shift[19:10] <= 10'b0000111001; end
        4'h9: begin tx_shift[19:10] <= 10'b0100111001; end
        4'hA: begin tx_shift[19:10] <= 10'b0100000101; end
        4'hB: begin tx_shift[19:10] <= 10'b0010000101; end
        4'hC: begin tx_shift[19:10] <= 10'b0110000101; end
        4'hD: begin tx_shift[19:10] <= 10'b0001000101; end
        4'hE: begin tx_shift[19:10] <= 10'b0101000101; end
        4'hF: begin tx_shift[19:10] <= 10'b0011000101; end
        endcase
        
        CTS_delay <= 20'h00000;
    end
    // Shift out the received data
    else begin
        fifo_rd_en <= 1'b0;
    
		if(tx_count < 16'd867) begin
			tx_count <= tx_count + 16'd1;
		end
		else begin
			tx_count <= 16'd0;
		end
		
		if(tx_count == 16'd0) begin
			TXD <= tx_shift[19];
			tx_shift <= {tx_shift[18:0], 1'b1};
            CTS <= CTS_delay[19];
            CTS_delay <= {CTS_delay[18:0], 1'b1};
            tx_start <= ~CTS_delay[19];
		end
    end
end

和之前差不多的串口接收逻辑,加入了转换成16进制数的逻辑,每次输出一个4位的十六进制数

// Input from uart
always @(posedge clk or posedge rst) begin
    if(rst) begin
        RXD_delay <= 8'h00;
        rx_count <= 16'd0;
        rx_bit_count <= 4'd0;
        din_valid <= 1'b0;
        rx_start <= 1'b0;
    end
    else if(~RTS) begin
        if(rx_count < 16'd867) begin
			rx_count <= rx_count + 16'd1;
		end
		else begin
			rx_count <= 16'd0;
		end
        
        if( (rx_count == 16'd0) && (~RXD) && (~rx_start) ) begin
            RXD_delay <= 8'h00;
            rx_bit_count <= 4'd0;
            rx_start <= 1'b1;
        end
        else if( (rx_count == 16'd0) && rx_start && (rx_bit_count != 4'd8)) begin
            rx_bit_count <= rx_bit_count + 4'd1;
            RXD_delay <= {RXD_delay[6:0], RXD};
        end
        else if( (rx_count == 16'd0) && rx_start) begin
            rx_start <= 1'b0;
            rx_bit_count <= 4'd0;
            din_valid <= 1'b1;
            // Need to transfer the received data into hex data
            case(RXD_delay[7:0])
            8'b00001100 : begin din <= 4'h0; end
            8'b10001100 : begin din <= 4'h1; end
            8'b01001100 : begin din <= 4'h2; end
            8'b11001100 : begin din <= 4'h3; end
            8'b00101100 : begin din <= 4'h4; end
            8'b10101100 : begin din <= 4'h5; end
            8'b01101100 : begin din <= 4'h6; end
            8'b11101100 : begin din <= 4'h7; end
            8'b00011100 : begin din <= 4'h8; end
            8'b10011100 : begin din <= 4'h9; end
            8'b10000010 : begin din <= 4'hA; end
            8'b01000010 : begin din <= 4'hB; end
            8'b11000010 : begin din <= 4'hC; end
            8'b00100010 : begin din <= 4'hD; end
            8'b10100010 : begin din <= 4'hE; end
            8'b01100010 : begin din <= 4'hF; end
            endcase
        end
        else begin
            din_valid <= 1'b0;
        end
    end
end

endmodule

顶层逻辑

做好两个接口的准备工作,开始写顶层的temperature.v:

顶层定义,时钟复位和LED,I2C接口以及UART串口接口

module temperature(
    input  clk,
    input  rst,
    output reg [1:0] led,
    
    // I2C port
    output SCL,
    inout  SDA,
    input  TMP_INT,
    input  TMP_CT,
    
    // UART port
    input  RXD,
    output TXD,
    output CTS,
    input  RTS
);

将ADT7420另外两个引脚直接连接到LED上

// LED on when the temperature over limit
always @(posedge clk or posedge rst) begin
    if(rst) begin
        led <= 2'b00;
    end
    else begin
        led <= {TMP_CT, TMP_INT};
    end
end

// output control of pin SDA
wire SDA_in, SDA_out, SDA_oe;
assign SDA = (SDA_oe) ? SDA_out : 1'bz;
assign SDA_in = SDA;

// I2C interface controller
reg  [7:0] I2C_reg_addr;
reg        I2C_rdh_wrl;
reg        I2C_ready;
reg  [7:0] I2C_dout;
wire       I2C_dout_ack;
reg  [3:0] I2C_dout_length;
wire [7:0] I2C_din;
wire       I2C_din_valid;

调用前面写好的I2C控制器

I2C_transmitter I2C_transmitter(
    .clk         (clk),
    .rst         (rst),
    
    .I2C_SCL     (SCL),
    .I2C_SDA_out (SDA_out),
    .I2C_SDA_in  (SDA_in),
    .I2C_SDA_oe  (SDA_oe),
    
    .dev_addr    (7'h4B),        // device address
    .reg_addr    (I2C_reg_addr),     // register address
    .rdh_wrl     (I2C_rdh_wrl),      // 1 is read, 0 is write
    .ready       (I2C_ready),        // write and read ready
    .dout        (I2C_dout),         // write data
    .dout_ack    (I2C_dout_ack),     // write data acknowledge by slave device
    .dout_length (I2C_dout_length),  // the number of bytes of write and read data
    .din         (I2C_din),
    .din_valid   (I2C_din_valid)
);

调用前面写好的UART串口控制模块

// Data IO with UART
wire [3:0] uart_din;
wire       uart_din_valid;
reg  [7:0] uart_dout;
reg        uart_dout_ready;
UART_transmitter UART_transmitter(
    .clk         (clk),
    .rst         (rst),
    
    // UART port
    .TXD         (TXD),
    .RXD         (RXD),
    .CTS         (CTS),
    .RTS         (RTS),
    
    // Control port
    .dout        (uart_dout),
    .dout_ready  (uart_dout_ready),
    .din         (uart_din),
    .din_valid   (uart_din_valid)
);

根据串口接收到的指令,进行不同的读写操作

  • 0读取温度数据,传回四个byte
  • 1读取ADT7420的状态寄存器,期望是0x00
  • 2读取温度上限高位寄存器
  • 3把温度上限设置为28摄氏度
  • 4把温度上限改回默认的64摄氏度

另外把串口接收到的数据重传回PC,用来显示自己打入的命令,由于收到的是4位,而输出是8位,在高位加4位0

// Interface between UART and I2C
always @(posedge clk or posedge rst) begin
    if(rst) begin
        uart_dout <= 8'h00;
        uart_dout_ready <= 1'b0;
    end
    else if(uart_din_valid) begin
        // return it back to UART, to show up in console
        uart_dout <= {4'h0,uart_din}; 
        uart_dout_ready <= 1'b1;
        
        // send command to I2C according to the number
        I2C_ready <= 1'b1;
        case(uart_din)
        4'h0: begin 
            I2C_dout <= 8'h00; I2C_reg_addr <= 8'h00; I2C_rdh_wrl <= 1'b1; I2C_dout_length <= 4'd2; 
        end // Read two bytes from device
        4'h1: begin 
            I2C_dout <= 8'h00; I2C_reg_addr <= 8'h02; I2C_rdh_wrl <= 1'b1; I2C_dout_length <= 4'd1; 
        end // Read Status
        4'h2: begin 
            I2C_dout <= 8'h00; I2C_reg_addr <= 8'h04; I2C_rdh_wrl <= 1'b1; I2C_dout_length <= 4'd1; 
        end // Read back from T_high MSB
        4'h3: begin 
            I2C_dout <= 8'h0E; I2C_reg_addr <= 8'h04; I2C_rdh_wrl <= 1'b0; I2C_dout_length <= 4'd1; 
        end // Set T_high to 28 Celsius
        4'h4: begin 
            I2C_dout <= 8'h20; I2C_reg_addr <= 8'h04; I2C_rdh_wrl <= 1'b0; I2C_dout_length <= 4'd1; 
        end // Set T_high to 64 Celsius again
        default: begin 
            I2C_dout <= 8'h00; I2C_reg_addr <= 8'h00; I2C_rdh_wrl <= 1'b1; I2C_dout_length <= 4'd2; 
        end
        endcase
    end
    else if(I2C_din_valid) begin
        // receive data from I2C and transfer it to UART
        uart_dout <= I2C_din; 
        uart_dout_ready <= 1'b1;
    end
    else begin
        // in case some signal is two clock cycles wide
        uart_dout_ready <= 1'b0;
        I2C_ready <= 1'b0;
    end
end

endmodule

模拟仿真

和之前一样,要写一个Testbench和一个仿真脚本来仿真

Testbench

代码tb_temperature如下:

`timescale 1ns/1ns

module tb_temperature;

reg         clock;
reg         reset;

wire        SCL;
wire        SDA;
reg         SDA_oe;
reg         SDA_in;
wire        SDA_out;
wire        TMP_INT;
wire        TMP_CT;
assign      SDA = (SDA_oe) ? 1'bz : SDA_in;
assign      SDA_out = SDA;

reg         RXD;
wire        TXD;
wire        CTS;
reg         RTS;

reg  [9:0]  RXD_buf;
reg  [7:0]  SDA_out_buf;

复位以后,参考I2C的时序,接收或者传回生成的数据,使用指令0,读取温度数据

initial begin
    clock = 1'b0;
    reset = 1'b0;
    
    RXD = 1'b1;
    RTS = 1'b1;
    
    SDA_oe = 1'b1;
    SDA_in = 1'b0;
    
    SDA_out_buf = 8'h00;

    // Reset for 1us
    #100 
    reset = 1'b1;
    #1000
    reset = 1'b0;
    
    // Send a number 0 into uart
    RXD_buf = 10'b0000011001;
    RTS = 1'b0;
    repeat(10) begin
        repeat(867) @(posedge clock);
        {RXD, RXD_buf} = {RXD_buf, 1'b1};
    end
    
    // Send signal to I2C port to read
    @(negedge SCL)
    SDA_oe = 1'b1;
    repeat(8) begin
        @(posedge SCL) SDA_out_buf = {SDA_out_buf[6:0], SDA_out};
    end
    // Ack from device to FPGA
    @(posedge SCL)
    SDA_oe = 1'b0;
    SDA_in = 1'b0;
    
    @(negedge SCL)
    SDA_oe = 1'b1;
    repeat(8) begin
        @(posedge SCL) SDA_out_buf = {SDA_out_buf[6:0], SDA_out};
    end
    // Ack from device to FPGA
    @(posedge SCL)
    SDA_oe = 1'b0;
    SDA_in = 1'b0;
    
    @(posedge SCL); // waiting for the repeat start
    
    @(negedge SCL)
    SDA_oe = 1'b1;
    repeat(8) begin
        @(posedge SCL) SDA_out_buf = {SDA_out_buf[6:0], SDA_out};
    end
    // Ack from device to FPGA
    @(posedge SCL)
    SDA_oe = 1'b0;
    SDA_in = 1'b0;

    repeat(8) begin
        @(posedge SCL) SDA_in <= ~SDA_in;
    end
    // Ack from FPGA to device
    @(negedge SCL)
    SDA_oe = 1'b1;
    @(posedge SCL)
    SDA_oe = 1'b0;
    SDA_in = 1'b0;
    
    repeat(8) begin
        @(posedge SCL) SDA_in <= ~SDA_in;
    end
    // Ack from FPGA to device
    @(negedge SCL)
    SDA_oe = 1'b1;
end

// Generate 100MHz clock signal
always #5 clock <= ~clock;

temperature temperature(
    .clk      (clock),
    .rst      (reset),
    
    // I2C port
    .SCL      (SCL),
    .SDA      (SDA),
    .TMP_INT  (TMP_INT),
    .TMP_CT   (TMP_CT),
    
    // UART port
    .RXD      (RXD),
    .TXD      (TXD),
    .CTS      (TXD),
    .RTS      (RTS)
);

endmodule

仿真脚本

写脚本sim.do如下:

vlib work
vlog ../src/temperature.v ../src/I2C_transmitter.v ../src/UART_transmitter.v ../src/syn_fifo.v ./tb_temperature.v
vsim work.tb_temperature -voptargs=+acc +notimingchecks
log -depth 7 /tb_temperature/*
#do wave.do
run 1ms

调用前面全部的代码,打开ModelSim后转到脚本在的路径,使用命令do sim.do即可开始仿真。

仿真时可以添加想要的信号到waveform窗口中观察,然后可以保存为wave.do,这样下次可以通过调用它来加入一样的信号,节省一个一个加入的时间,这时你可以把sim.do中被#注释掉的那行去注释

仿真结果

调用仿真脚本得到的结果如下:
waveform
和前面介绍的I2C时序比较可以看出是符合预期的,当中的一些蓝色和红色是由于Testbench毕竟不是真实芯片,无法返回完美的确认信号ack,之后可以用ChipScope来观察I2C信号

编译测试

新建一个叫temperature的project,配置为开发板NEXYS4。添加代码文件temperature.v、I2C_transmitter.v、UART_transmitter.v和syn_fifo.v

下一步加入约束constraint文件temperature.xdc,同样这是用标准模板取自己需要部分修改出来的(NEXYS 4 DDR Master XDC):

## This file is a general .xdc for the Nexys4 DDR Rev. C
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project

## Clock signal
set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports clk]
create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} -add [get_ports clk]


## LEDs

set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports {led[0]}]
set_property -dict {PACKAGE_PIN K15 IOSTANDARD LVCMOS33} [get_ports {led[1]}]


##Switches

set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst]


##Temperature Sensor

set_property -dict {PACKAGE_PIN C14 IOSTANDARD LVCMOS33} [get_ports SCL]
set_property -dict {PACKAGE_PIN C15 IOSTANDARD LVCMOS33} [get_ports SDA]
set_property -dict {PACKAGE_PIN D13 IOSTANDARD LVCMOS33} [get_ports TMP_INT]
set_property -dict {PACKAGE_PIN B14 IOSTANDARD LVCMOS33} [get_ports TMP_CT]


##USB-RS232 Interface

set_property -dict {PACKAGE_PIN C4 IOSTANDARD LVCMOS33} [get_ports RXD]
set_property -dict {PACKAGE_PIN D4 IOSTANDARD LVCMOS33} [get_ports TXD]
set_property -dict {PACKAGE_PIN D3 IOSTANDARD LVCMOS33} [get_ports CTS]
set_property -dict {PACKAGE_PIN E5 IOSTANDARD LVCMOS33} [get_ports RTS]

到这里可以点击 Run Synthesis做综合,几分钟完成后用Set Up Debug配置ChipScope,加入和I2C有关的接口SCL和SDA(进出两个口),并设置长度为65536:
FPGA基础入门【15】开发板I2C温度传感器配置_第12张图片
FPGA基础入门【15】开发板I2C温度传感器配置_第13张图片
下面就可以Run Implementation和Generate Bitstream生成bitstream了。

和前面的教程一样,USB线连接NEXYS4板子,开启Hardware Manager,然后auto连接上板子,Program Device烧写进程序,注意Debug probes file有对应的ltx文件。

结果

打开Putty串口接口,具体配置可以参考教程系列11,分别打入几个指令后收到结果如下:
FPGA基础入门【15】开发板I2C温度传感器配置_第14张图片

  1. 指令00读取温度,返回0x0DC8,根据温度的计算方式,右移3位后除以16,得到温度27.5625摄氏度,当然精度没有这么高,只是计算的结果
  2. 指令01读取状态寄存器,如同预料的返回0x00
  3. 指令02读取温度上限的高位,返回了默认值0x20,表示64摄氏度
  4. 指令03写温度上限的高位为0x0E,表示28摄氏度,不会返回数据
  5. 再用指令02读取温度上限高位,返回的是刚刚写入的0x0E
  6. 指令04把温度上限的高位写回0x20
  7. 再用指令02,读回的是0x20

调用ChipScope,设置trigger为SCL的下降沿,分别在putty打入指令0和3显示如下:
cmd0
FPGA基础入门【15】开发板I2C温度传感器配置_第15张图片
这个波形图基本展示了I2C读和写的时序图,就算不是使用FPGA,应该也可以参考这两张图。

总结

没能填上上期说的SD卡的坑,那部分要放到最后。下一篇要介绍板载的加速度传感器accelerometer ADXL362

你可能感兴趣的:(FPGA)