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

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

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

开发板温度传感器

NEXYS 4文档中写着它使用的温度传感器名字是Analog Device ADT7420,提供16位精度,一般精确到0.25摄氏度,用I2C接口和FPGA连接如下:

SCL和SDA是I2C接口,用来和FPGA连接。可以看到它只有两根线,非常精简,一般用来和不太复杂的外围设备连接。并且它还有地址识别,因此1个master设备可以同时和多个slave设备相连。剩下两条线表示温度溢出(过高或者过低),以及温度高到有危险的程度。

ADT7420的文档链接:ADT7420

I2C接口简介

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

和这款芯片相关的I2C的时序如下:
写单字节数据

写双字节数据:

从配置寄存器中读取数据:

读取温度数据:

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

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

ADT7420简介

网上很多Arduino用的温度传感器模块用的就是这款芯片,引脚如下:

其中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的寄存器全家福及其默认值如下:

这里我们只关注温度高低位、状态和配置四个寄存器

温度高低位:

状态寄存器,复位后要先等这个寄存器的最高位变成0:

配置寄存器:

逻辑设计

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

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

I2C控制模块

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

新建一个代码文件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 addressinput      [7:0]  reg_addr,     // register addressinput             rdh_wrl,      // 1 is read, 0 is writeinput             ready,        // write and read readyinput      [7:0]  dout,         // write dataoutput reg        dout_ack,     // write data acknowledge by slave deviceinput      [3:0]  dout_length,  // the number of bytes of write and read dataoutput 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) beginif(rst | ~(I2C_SCL_en)) beginI2C_SCL_counter <= 8'd0;I2C_SCL <= 1'b1;endelse if(I2C_SCL_counter < 8'd250) beginI2C_SCL_counter <= I2C_SCL_counter + 8'd1;endelse beginI2C_SCL_counter <= 8'd0;I2C_SCL <= ~I2C_SCL;end
end

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

// detection of falling edge of SCL
always @(posedge clk) beginI2C_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) beginready_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) beginif(rst) beginstate <= IDLE;endelse beginstate <= next_state;end
endalways @(posedge clk) begincase(state)IDLE: begindout_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: beginif(ready_posedge) beginnext_state <= ADDR_DEV_WRITE;dout_buf <= {dev_addr, 1'b0};    // the first step is always write register addressI2C_SDA_out <= 1'b0;I2C_SDA_oe <= 1'b1;I2C_SCL_en <= 1'b1;endend

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

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

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

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

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

    REPEAT_START: begin//  not stopped by masterif(I2C_SCL_negedge) beginI2C_SDA_oe <= 1'b1;I2C_SDA_out <= 1'b1;endelse if(I2C_SCL_posedge) beginI2C_SCL_en <= 1'b0;end// delay a while and pull down the SDA, indicating repeat startelse if(~I2C_SCL_en && (end_count < 8'd250)) beginend_count <= end_count + 8'd1;endelse if(~I2C_SCL_en) beginend_count <= 8'd0;I2C_SDA_out <= 1'b0;I2C_SCL_en <= 1'b1;next_state <= ADDR_DEV_READ;endend

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

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

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

    WRITE: begin        if(I2C_SCL_negedge && (I2C_SCL_count < 4'd8) ) beginI2C_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;endelse if(I2C_SCL_negedge && (I2C_SCL_count == 4'd8)) beginI2C_SCL_count <= I2C_SCL_count + 4'd1;dout_ack <= 1'b1; // indicate that ready to take next output dataI2C_SDA_oe <= 1'b0;endelse if(I2C_SCL_posedge && (I2C_SCL_count == 4'd9)) begindout_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 ENDINGnext_state <= ENDING;dout_count <= 4'd0;endelse if(~I2C_SDA_in) begin  // acknowledged by device, not write enough, keep in WRITEnext_state <= WRITE;dout_count <= dout_count + 4'd1;endelse begin // not acknowledged by device, go to ENDINGnext_state <= ENDING;dout_count <= 4'd0;endendelse begindout_ack <= 1'b0;endend

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

    READ: beginif(I2C_SCL_posedge && (I2C_SCL_count < 4'd8) ) beginI2C_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;endelse if(I2C_SCL_negedge && (I2C_SCL_count == 4'd8)) beginI2C_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 ackI2C_SDA_out <= 1'b1;endelse begin  // need more data, send ack to deviceI2C_SDA_out <= 1'b0;endendelse if(I2C_SCL_negedge && (I2C_SCL_count == 4'd9)) beginI2C_SCL_count <= 4'd0;I2C_SDA_oe <= 1'b0;if(dout_count == (dout_length - 4'd1) ) begin // already read enough, go to ENDINGnext_state <= ENDING;dout_count <= 4'd0;endelse begin  // need more data, continue in READ statenext_state <= READ;dout_count <= dout_count + 4'd1;endendelse begindin_valid <= 1'b0;endend

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

    ENDING: beginif(I2C_SCL_posedge) beginI2C_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 endif(~I2C_SCL_en && (end_count < 8'd250)) beginend_count <= end_count + 8'd1;endelse if(~I2C_SCL_en) beginI2C_SDA_out <= 1'b1;next_state <= IDLE;endendendcase
endendmodule

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) beginif(rst) beginwr_pointer <= 0;end else if(wr_en) beginwr_pointer <= wr_pointer + 1;end
end// READ_POINTER
always @(posedge clk or posedge rst) beginif(rst) beginrd_pointer <= 0;end else if(rd_en) beginrd_pointer <= rd_pointer + 1;end
end

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

// READ DATA
always  @(posedge clk or posedge rst) beginif(rst) begindata_out <= 0;end else if(rd_en) begindata_out <= data_ram[rd_pointer];end
end// WRITE DATA
always  @(posedge clk) beginif(wr_en) begindata_ram[wr_pointer] <= data_in;end
end// STATUS COUNTER
always @(posedge clk or posedge rst) beginif(rst) beginstatus_cnt <= 0;// Read but no write.end else if(rd_en &&  !wr_en && (status_cnt != 0)) beginstatus_cnt <= status_cnt - 1;// Write but no read.end else if(wr_en &&  !rd_en && (status_cnt != RAM_DEPTH)) beginstatus_cnt <= status_cnt + 1;end
endendmodule

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

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

module UART_transmitter(input      clk,input      rst,// UART portoutput reg TXD,input      RXD,output reg CTS,input      RTS,// Control portinput      [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) beginfifo_rd_en_d1 <= fifo_rd_en;fifo_rd_en_d2 <= fifo_rd_en_d1;
endalways @(posedge clk or posedge rst) beginif(rst) begintx_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 UARTelse if(~tx_start && ~fifo_empty) beginfifo_rd_en <= 1'b1;tx_start <= 1'b1;end// FIFO ready complete, get the data, transfer and buffer it into registerelse if(fifo_rd_en_d2) beginfifo_rd_en <= 1'b0;case(fifo_data_out[3:0])4'h0: begin tx_shift[9:0] <= 10'b0000011001; end4'h1: begin tx_shift[9:0] <= 10'b0100011001; end4'h2: begin tx_shift[9:0] <= 10'b0010011001; end4'h3: begin tx_shift[9:0] <= 10'b0110011001; end4'h4: begin tx_shift[9:0] <= 10'b0001011001; end4'h5: begin tx_shift[9:0] <= 10'b0101011001; end4'h6: begin tx_shift[9:0] <= 10'b0011011001; end4'h7: begin tx_shift[9:0] <= 10'b0111011001; end4'h8: begin tx_shift[9:0] <= 10'b0000111001; end4'h9: begin tx_shift[9:0] <= 10'b0100111001; end4'hA: begin tx_shift[9:0] <= 10'b0100000101; end4'hB: begin tx_shift[9:0] <= 10'b0010000101; end4'hC: begin tx_shift[9:0] <= 10'b0110000101; end4'hD: begin tx_shift[9:0] <= 10'b0001000101; end4'hE: begin tx_shift[9:0] <= 10'b0101000101; end4'hF: begin tx_shift[9:0] <= 10'b0011000101; endendcasecase(fifo_data_out[7:4])4'h0: begin tx_shift[19:10] <= 10'b0000011001; end4'h1: begin tx_shift[19:10] <= 10'b0100011001; end4'h2: begin tx_shift[19:10] <= 10'b0010011001; end4'h3: begin tx_shift[19:10] <= 10'b0110011001; end4'h4: begin tx_shift[19:10] <= 10'b0001011001; end4'h5: begin tx_shift[19:10] <= 10'b0101011001; end4'h6: begin tx_shift[19:10] <= 10'b0011011001; end4'h7: begin tx_shift[19:10] <= 10'b0111011001; end4'h8: begin tx_shift[19:10] <= 10'b0000111001; end4'h9: begin tx_shift[19:10] <= 10'b0100111001; end4'hA: begin tx_shift[19:10] <= 10'b0100000101; end4'hB: begin tx_shift[19:10] <= 10'b0010000101; end4'hC: begin tx_shift[19:10] <= 10'b0110000101; end4'hD: begin tx_shift[19:10] <= 10'b0001000101; end4'hE: begin tx_shift[19:10] <= 10'b0101000101; end4'hF: begin tx_shift[19:10] <= 10'b0011000101; endendcaseCTS_delay <= 20'h00000;end// Shift out the received dataelse beginfifo_rd_en <= 1'b0;if(tx_count < 16'd867) begintx_count <= tx_count + 16'd1;endelse begintx_count <= 16'd0;endif(tx_count == 16'd0) beginTXD <= 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];endend
end

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

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

顶层逻辑

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

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

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

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

// LED on when the temperature over limit
always @(posedge clk or posedge rst) beginif(rst) beginled <= 2'b00;endelse beginled <= {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) beginif(rst) beginuart_dout <= 8'h00;uart_dout_ready <= 1'b0;endelse if(uart_din_valid) begin// return it back to UART, to show up in consoleuart_dout <= {4'h0,uart_din}; uart_dout_ready <= 1'b1;// send command to I2C according to the numberI2C_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 device4'h1: begin I2C_dout <= 8'h00; I2C_reg_addr <= 8'h02; I2C_rdh_wrl <= 1'b1; I2C_dout_length <= 4'd1; end // Read Status4'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 MSB4'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 Celsius4'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 againdefault: begin I2C_dout <= 8'h00; I2C_reg_addr <= 8'h00; I2C_rdh_wrl <= 1'b1; I2C_dout_length <= 4'd2; endendcaseendelse if(I2C_din_valid) begin// receive data from I2C and transfer it to UARTuart_dout <= I2C_din; uart_dout_ready <= 1'b1;endelse begin// in case some signal is two clock cycles wideuart_dout_ready <= 1'b0;I2C_ready <= 1'b0;end
endendmodule

模拟仿真

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

Testbench

代码tb_temperature如下:

`timescale 1ns/1nsmodule 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 beginclock = 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;#1000reset = 1'b0;// Send a number 0 into uartRXD_buf = 10'b0000011001;RTS = 1'b0;repeat(10) beginrepeat(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中被#注释掉的那行去注释

仿真结果

调用仿真脚本得到的结果如下:

和前面介绍的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]## LEDsset_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports {led[0]}]
set_property -dict {PACKAGE_PIN K15 IOSTANDARD LVCMOS33} [get_ports {led[1]}]##Switchesset_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst]##Temperature Sensorset_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 Interfaceset_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:


下面就可以Run Implementation和Generate Bitstream生成bitstream了。

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

结果

打开Putty串口接口,具体配置可以参考教程系列11,分别打入几个指令后收到结果如下:

  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显示如下:


这个波形图基本展示了I2C读和写的时序图,就算不是使用FPGA,应该也可以参考这两张图。

总结

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

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

  1. FPGA基础入门【12】开发板USB鼠标控制

    上一篇教程介绍了NEXYS4 开发板中UART串口通信的使用方式,这一篇介绍USB接口接收鼠标和键盘信号 FPGA基础入门[12]开发板USB鼠标控制 开发板USB芯片 信号时序图 鼠标初始化 逻辑设 ...

  2. FPGA基础入门【8】开发板外部存储器SPI flash访问

    前两篇教程利用数码管project介绍了chipscope和各种烧写开发板的方式,这篇开始继续探索开发板,这次关注外置存储器的控制,外置指的是芯片外部,不是开发板外部.板子上的外置存储器有DDR2和S ...

  3. FPGA基础入门【1】Vivado官方免费版安装

    本人自本科大二开始接触FPGA相关知识,现已将近五年,从这篇开始将从比较基础的角度讲述如何一步步了解FPGA.我相信动手一步步做下去是从零开始学习知识的最快方法,因此不会从最基础开始讲,而是在碰到相应 ...

  4. FPGA基础入门【3】Blink逻辑及仿真

    从这一篇开始正式介绍FPGA中的硬件逻辑,第一个目标就是从零开始在NEXYS 4开发板上实现闪烁LED. 软件编程中hello world是初学语言中实现的第一个功能,而硬件编程中blink是同等的地 ...

  5. FPGA基础入门【6】ChipScope的使用

    当FPGA设计中复杂度慢慢变高的时候,仿真的手段也要增加,目前我们仿真的手段都是在ModelSim中配置相应的testbench,给模块发送需要的信号.这种软件仿真的方式有几个缺点: 一个是软件仿真速 ...

  6. FPGA基础入门篇(四) 边沿检测电路

    FPGA基础入门篇(四)--边沿检测电路 一.边沿检测 边沿检测,就是检测输入信号,或者FPGA内部逻辑信号的跳变,即上升沿或者下降沿的检测.在检测到所需要的边沿后产生一个高电平的脉冲.这在FPGA电 ...

  7. Linux基础入门--驱动开发--USB

    Linux基础入门--驱动开发--USB 1.基本概念 2.组成结构 2.1 设备描述符 2.2 配置描述符 2.3 接口描述符 2.4 端点描述符 2.5 字符串描述符 3.管道 4.端点分类 4. ...

  8. 一期完结《一篇文章让你从HTML零基础入门前端开发》12.28

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VRcXH9FM-1672214813897)(./assets/%E7%8E%84%E5%AD%90Shaer-%E4% ...

  9. 【视频回放与课件】零基础入门AI开发

    今天上午,受广州图书馆邀请,在第一讲<零代码上手人工智能>的基础上,以<零基础入门AI开发>为主题,分四步解锁人工智能学习的概念与开发工具,让您在一小时内轻松掌握人工智能开发要 ...

  10. 零基础学习MSP430F552LP开发板,学习前期准备,Code Composer Studio(CCS)软件的安装

    零基础学习MSP430F552LP开发板 一.前言 零基础学习MSP430F552LP开发板,为电子设计竞赛做准备以及学好这一款芯片. 在选择比赛题目时,发现有的题目时规定使用ti的芯片作为控制MCU ...

最新文章

  1. 清华大学朱文武团队夺冠AAAI 2021国际深度元学习挑战赛
  2. git多人协作冲突解决方法
  3. 后台开发经典书籍--unix网络编程
  4. mysql安装和服务相关问题总结
  5. reactjs组件实例三大属性之refs使用示例:字符串形式的ref、回调函数形式的ref、回调ref中回调执行次数的问题
  6. .NET 6新特性试用 | 可写JSON DOM API
  7. 前端学习(2306):react之组件使用
  8. Docker安装(安装docker)
  9. Consecutive Factors (20)
  10. ubuntu下安装英汉词典——stardict
  11. Kendo UI 模板概述
  12. 向量表示 运动抛物线_黄老师讲数学(460)向量、数列、双变量、坐标计算、极限等结合的一道题...
  13. 苹果计算机远程控制软件,向日葵远程控制软件iPhone手机远程控制电脑
  14. 人人网市值缩水近80%,究竟发生了什么?
  15. Bootstrap——制作个人简历网页、工具类【边框(添加、删除、颜色、圆角)、清除浮动、颜色(文本、链接、背景)、display属性、浮动、定位、文本对齐】
  16. Parse error in application web.xml file at jndi:/localhost/ipws/WEB-INF/web.xml
  17. pdf文件转换ppt可编辑_创建,转换和编辑PDF文件的免费工具
  18. 云服务器可以修改ip,云服务器的ip可以更换吗
  19. “沃尔沃环球帆船赛挑战极限—S40征服之旅”冠军竞猜活动启动
  20. Windows 10: The Next Chapter

热门文章

  1. 深度学习落地项目 呼叫中心系统
  2. 转:二阶有源低通滤波器设计
  3. 老男孩python全栈视频教程_老男孩Python全栈7期,Flask全套组件及原理剖析视频教程下载...
  4. 将url编码数据转换为简单字符串
  5. oracle的dbv命令,DBV(DBVERIFY)工具
  6. 微信公众号php支付设置回调,php微信公众号支付讲解(JSAPI)
  7. 项目管理软件: 禅道、JIRA
  8. matlab 二维矩形函数,rect矩形函数 matlab中怎样编写矩形函数
  9. idea热部署插件JRebel激活方法 (免费)
  10. html九宫格实现人像拼图游戏,实例分享jQuery+vue.js实现的九宫格拼图游戏