上一篇教程挖了一个NEXYS4 开发板中MicroSD的天坑,发现难度过高,打算放到之后再完成,这一篇来电不这么重口味的温度传感器配置。在NEXYS 4开发板上的温度传感器使用的是I2C接口,这个接口很常见,可以写一个模块留着以后使用
NEXYS 4文档中写着它使用的温度传感器名字是Analog Device ADT7420,提供16位精度,一般精确到0.25摄氏度,用I2C接口和FPGA连接如下:
SCL和SDA是I2C接口,用来和FPGA连接。可以看到它只有两根线,非常精简,一般用来和不太复杂的外围设备连接。并且它还有地址识别,因此1个master设备可以同时和多个slave设备相连。剩下两条线表示温度溢出(过高或者过低),以及温度高到有危险的程度。
ADT7420的文档链接:ADT7420
从ADT7420的文档中读到,它的时钟需求是400kHz以下,我们采用200kHz,需要把系统时钟100MHz减慢500倍
和这款芯片相关的I2C的时序如下:
写单字节数据
写双字节数据:
从配置寄存器中读取数据:
读取温度数据:
从这个时序图看出,I2C读写控制参数有四个:读写控制、寄存器地址、读写数据长度、写的8位数据(每完成一次输出就更新一次)
网上很多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数据传输参数有这么几个:寄存器地址、读写选择、读写长度、需要写入的8位数据。
新建一个代码文件I2C_transmitter.v,代码如下:
顶层接口配置,前面分析过I2C必要参数,还需要一些其他的控制信号
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串口控制的模块,明明已经有它的逻辑代码了。问题是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)
);
根据串口接收到的指令,进行不同的读写操作
另外把串口接收到的数据重传回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和一个仿真脚本来仿真
代码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中被#注释掉的那行去注释
调用仿真脚本得到的结果如下:
和前面介绍的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:
下面就可以Run Implementation和Generate Bitstream生成bitstream了。
和前面的教程一样,USB线连接NEXYS4板子,开启Hardware Manager,然后auto连接上板子,Program Device烧写进程序,注意Debug probes file有对应的ltx文件。
打开Putty串口接口,具体配置可以参考教程系列11,分别打入几个指令后收到结果如下:
调用ChipScope,设置trigger为SCL的下降沿,分别在putty打入指令0和3显示如下:
这个波形图基本展示了I2C读和写的时序图,就算不是使用FPGA,应该也可以参考这两张图。
没能填上上期说的SD卡的坑,那部分要放到最后。下一篇要介绍板载的加速度传感器accelerometer ADXL362