上一篇教程介绍的是NEXYS4开发板上的温度传感器,用上了串口通信和I2C接口,这次使用的加速度传感器使用的是SPI接口,是除了I2C之外另一种常用的接口,实用性很高
NEXYS 4文档中写着它使用的加速度传感器是Analog Device的ADXL362,它和FPGA的连接如下
它使用的接口是SPI,在系列教程8的SPI Flash中使用过这种接口,我们可以从那份代码中提取相应的控制逻辑
ADXL362的文档
ADXL362文档中提到时钟SCK的频率范围是2.4kHz到8MHz,NEXYS4开发板文档中推荐频率在1MHz到5MHz。芯片文档中的应该是上下极限,我们这里采用5MHz作为SPI借口频率,只需要将100MHz的系统时钟减慢20倍。
SPI接口中写寄存器的时序如下
读寄存器的时序如下
由于SPI没有I2C接口中那样输入输出共用的引脚,不需要频繁切换方向,因此逻辑相对简单,不过比起I2C的两根线多出了两根。
还有很多传感器,可以在芯片文档中一一阅读,这里给出一个文档中的初始化流程example:
这一通操作之后,每当开发板被至少0.25个重力加速度移动的时候,INT2中断引脚就会变成高电平,板子不动5秒左右会恢复低电平,引到LED上即可观察到。
这篇教程的计划是利用串口,输送控制命令给SPI接口控制模块,读写相应寄存器。
和前面的教程不同的是,前面的教程用单一数字代表预先配置好的指令,这篇教程的计划是打算通过串口输送指令,而不是一个单一数字。为此我们需要改进收到串口指令后的控制逻辑
首先我们需要一个SPI控制模块SPI_transmitter.v,这部分代码由之前的SPI flash的代码修改得来,不过做了很大的改动:
顶层接口定义,除了SPI接口外,控制接口有:
module SPI_transmitter(
input clk,
input rst,
// SPI port
output reg CSN,
output reg SCLK,
output reg MOSI,
input MISO,
// Control port
input ready,
input [7:0] inst,
input rdh_wrl,
input [7:0] reg_addr,
input [7:0] dout,
output reg [7:0] din,
output reg din_valid
);
SPI时钟SCLK生成器,减缓20倍成5MHz
// SCK generator, 5MHz output
reg SCLK_en;
reg SCLK_d;
reg [7:0] SCLK_count;
wire SCLK_posedge;
wire SCLK_negedge;
always @(posedge clk or posedge rst) begin
if(rst || ~SCLK_en) begin
SCLK <= 1'b0;
SCLK_count <= 8'd0;
end
else if(SCLK_en && (SCLK_count<8'd10)) begin
SCLK_count <= SCLK_count + 8'd1;
end
else begin
SCLK <= ~SCLK;
SCLK_count <= 8'd0;
end
end
监测SCLK的上升沿和下降沿
always @(posedge clk) begin
SCLK_d <= SCLK;
end
assign SCLK_posedge = ({SCLK_d, SCLK}==2'b01) ? 1'b1 : 1'b0;
assign SCLK_negedge = ({SCLK_d, SCLK}==2'b10) ? 1'b1 : 1'b0;
监测ready的上升沿
// 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
reg [3:0] state;
reg [3:0] next_state;
parameter IDLE = 4'd0;
parameter START = 4'd1;
parameter INST_OUT = 4'd2;
parameter ADDR_OUT = 4'd3;
parameter WRITE_DATA = 4'd4;
parameter READ_DATA = 4'd5;
parameter ENDING = 4'd6;
reg [6:0] MISO_buf;
reg [7:0] MOSI_buf;
reg [3:0] MOSI_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 // IDLE state
next_state <= START;
MOSI <= 1'b0;
CSN <= 1'b1;
SCLK_en <= 1'b0;
MOSI_buf <= inst;
MOSI_count <= 4'd0;
din <= 8'h00;
din_valid <= 1'b0;
end
当ready上升沿时,拉低CS,开启SCLK生成器,进入读写流程
START:
begin // enable SCK and CS
// start the process when ready rise, load instruction
if(ready_posedge) begin
next_state <= INST_OUT;
CSN <= 1'b0;
SCLK_en <= 1'b1;
MOSI_buf <= {inst[6:0], 1'b0};
MOSI <= inst[7];
end
end
输出8位指令
INST_OUT:
begin // send out instruction
if(SCLK_negedge && (MOSI_count < 4'd7)) begin
{MOSI, MOSI_buf} <= {MOSI_buf, 1'b0};
MOSI_count <= MOSI_count + 4'd1;
end
else if(SCLK_negedge) begin
{MOSI, MOSI_buf} <= {reg_addr, 1'b0};
MOSI_count <= 4'd0;
next_state <= ADDR_OUT;
end
end
输出8位地址,根据读写控制进入读数据流程或者写数据流程
ADDR_OUT:
begin // send out register address
if(SCLK_negedge && (MOSI_count < 4'd7)) begin
{MOSI, MOSI_buf} <= {MOSI_buf, 1'b0};
MOSI_count <= MOSI_count + 4'd1;
end
else if(SCLK_negedge) begin
{MOSI, MOSI_buf} <= {dout, 1'b0};
MOSI_count <= 4'd0;
next_state <= (rdh_wrl) ? READ_DATA : WRITE_DATA;
end
end
读写数据流程,写完进入结尾。将来可能会加入多字节读写控制
WRITE_DATA:
begin // send testing data out to flash
if(SCLK_negedge && (MOSI_count < 4'd7)) begin
{MOSI, MOSI_buf} <= {MOSI_buf, 1'b0};
MOSI_count <= MOSI_count + 4'd1;
end
else if(SCLK_negedge) begin
{MOSI, MOSI_buf} <= 9'h0;
MOSI_count <= 4'd0;
next_state <= ENDING;
end
end
READ_DATA:
begin // get a byte
if(SCLK_posedge && (MOSI_count < 4'd7)) begin
MISO_buf <= {MISO_buf[5:0], MISO};
MOSI_count <= MOSI_count + 4'd1;
end
else if(SCLK_posedge) begin
MOSI_count <= 4'd0;
next_state <= ENDING;
din <= {MISO_buf, MISO};
din_valid <= 1'b1;
end
else begin
din_valid <= 1'b0;
end
end
结尾流程,一段时间后拉高CSN
ENDING:
begin //disable SCK and CS
if(SCLK_negedge) begin
CSN <= 1'b1;
next_state <= IDLE;
SCLK_en <= 1'b0;
end
end
endcase
end
endmodule
就像前面说的,这次要改进顶层逻辑,使得指令不再是预设好的。代码accel.v如下:
顶层引脚配置,时钟复位一如既往,两个LED分别和加速度传感器的中断引脚相连,再加上串口接口,以及加速度传感器的SPI接口
module accel(
input clk,
input rst,
output reg LED_INT1,
output reg LED_INT2,
// UART port
output TXD,
input RXD,
output CTS,
input RTS,
// SPI port
output ACL_CSN,
output ACL_MOSI,
input ACL_MISO,
output ACL_SCLK,
input ACL_INT1,
input ACL_INT2
);
把LED接到两个中断引脚上
// Direct connect LED to interrupt pins
always @(posedge clk or posedge rst) begin
if(rst) begin
LED_INT1 <= 1'b0;
LED_INT2 <= 1'b0;
end
else begin
LED_INT1 <= ACL_INT1;
LED_INT2 <= ACL_INT2;
end
end
调用前面写好的SPI控制模块
// SPI controller
reg SPI_ready;
reg [7:0] SPI_inst;
reg SPI_rdh_wrl;
reg [7:0] SPI_reg_addr;
reg [7:0] SPI_dout;
wire [7:0] SPI_din;
wire SPI_din_valid;
SPI_transmitter SPI_transmitter(
.clk (clk),
.rst (rst),
// SPI port
.CSN (ACL_CSN),
.SCLK (ACL_SCLK),
.MOSI (ACL_MOSI),
.MISO (ACL_MISO),
// Control port
.ready (SPI_ready),
.inst (SPI_inst),
.rdh_wrl (SPI_rdh_wrl),
.reg_addr (SPI_reg_addr),
.dout (SPI_dout),
.din (SPI_din),
.din_valid (SPI_din_valid)
);
调用上一个教程写好的syn_fifo.v和UART_transmitter.v:
// Data IO with UART
wire [3:0] uart_din;
reg [3:0] uart_din_d;
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)
);
新的串口控制逻辑,加入了一个8byte的buffer,从高位到低位,分别表示读写控制、8位指令、寄存器地址、需要写入的8位数据。
同时从串口接收到的信号经过一级buffer后,以8位送回串口模块,因此这次打一个字符看不到,要打第二个才能看到
从SPI收到信号后会立刻输出
// Command control
// [27:24] rdh_wrl
// [23:16] inst
// [15: 8] reg_addr
// [ 7: 0] din
reg [27:0] UART_cmd_buf;
reg [3:0] din_count;
always @(posedge clk) begin
if(rst) begin
UART_cmd_buf <= 28'h0000000;
uart_dout_ready <= 1'b0;
uart_dout <= 8'h00;
din_count <= 4'd0;
SPI_ready <= 1'b0;
SPI_inst <= 8'h00;
SPI_rdh_wrl <= 1'b0;
SPI_reg_addr <= 8'h00;
SPI_dout <= 8'h00;
end
else if(uart_din_valid && din_count < 4'd7) begin
UART_cmd_buf <= {UART_cmd_buf[23:0], uart_din};
if(din_count[0]) begin
uart_dout <= {uart_din_d, uart_din};
uart_dout_ready <= 1'b1;
end
else begin
uart_din_d <= uart_din;
end
din_count <= din_count + 4'd1;
end
else if(uart_din_valid && din_count == 4'd7) begin
uart_dout <= {uart_din_d, uart_din};
uart_dout_ready <= 1'b1;
din_count <= 4'd0;
SPI_ready <= 1'b1;
SPI_inst <= UART_cmd_buf[19:12];
if(UART_cmd_buf[23:20] == 4'b0000) begin
SPI_rdh_wrl <= 1'b0;
end
else if(UART_cmd_buf[23:20] == 4'b0001) begin
SPI_rdh_wrl <= 1'b1;
end
SPI_reg_addr <= UART_cmd_buf[11:4];
SPI_dout <= {UART_cmd_buf[3:0], uart_din};
end
else if(SPI_din_valid) begin
uart_dout <= SPI_din;
uart_dout_ready <= 1'b1;
end
else begin
uart_dout_ready <= 1'b0;
SPI_ready <= 1'b0;
end
end
endmodule
和之前一样,要写一个Testbench和一个仿真脚本来仿真
代码tb_accel如下:
`timescale 1ns/1ns
module tb_accel;
reg clock;
reg reset;
reg RXD;
wire TXD;
wire CTS;
reg RTS;
reg [9:0] RXD_buf;
wire ACL_CSN;
wire ACL_MOSI;
reg ACL_MISO;
wire ACL_SCLK;
reg [15:0] ACL_MOSI_buf;
复位之后,送一个8byte的指令到串口,如果是读操作,则在收到SPI指令后传回任意数据。
这里在注释中的一段是写数据0x57到寄存器0x20的操作,注释外的一段是从寄存器0x20读数据的操作,可以根据需要使用某一段
initial begin
clock = 1'b0;
reset = 1'b0;
RXD = 1'b1;
RTS = 1'b1;
RXD_buf = 8'h00;
ACL_MISO = 1'b0;
// Reset for 1us
#100
reset = 1'b1;
#1000
reset = 1'b0;
/*
// Testing command 0x000A2057
// 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 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 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 a number A into uart
RXD_buf = 10'b0100000101;
RTS = 1'b0;
repeat(10) begin
repeat(867) @(posedge clock);
{RXD, RXD_buf} = {RXD_buf, 1'b1};
end
// Send a number 2 into uart
RXD_buf = 10'b0010011001;
RTS = 1'b0;
repeat(10) begin
repeat(867) @(posedge clock);
{RXD, RXD_buf} = {RXD_buf, 1'b1};
end
// 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 a number 5 into uart
RXD_buf = 10'b0101011001;
RTS = 1'b0;
repeat(10) begin
repeat(867) @(posedge clock);
{RXD, RXD_buf} = {RXD_buf, 1'b1};
end
// Send a number 7 into uart
RXD_buf = 10'b0111011001;
RTS = 1'b0;
repeat(10) begin
repeat(867) @(posedge clock);
{RXD, RXD_buf} = {RXD_buf, 1'b1};
end
*/
// Testing command 0x010B2000, return 0xAA in SPI
// 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 a number 1 into uart
RXD_buf = 10'b0100011001;
RTS = 1'b0;
repeat(10) begin
repeat(867) @(posedge clock);
{RXD, RXD_buf} = {RXD_buf, 1'b1};
end
// 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 a number B into uart
RXD_buf = 10'b0010000101;
RTS = 1'b0;
repeat(10) begin
repeat(867) @(posedge clock);
{RXD, RXD_buf} = {RXD_buf, 1'b1};
end
// Send a number 2 into uart
RXD_buf = 10'b0010011001;
RTS = 1'b0;
repeat(10) begin
repeat(867) @(posedge clock);
{RXD, RXD_buf} = {RXD_buf, 1'b1};
end
// 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 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 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
// Take 16 bits from MOSI, and send 8 bits to MISO
repeat(16) begin
@(posedge ACL_SCLK)
ACL_MOSI_buf = {ACL_MOSI_buf[14:0], ACL_MOSI};
end
repeat(8) begin
@(negedge ACL_SCLK)
ACL_MISO = ~ACL_MISO;
end
end
// Generate 100MHz clock signal
always #5 clock <= ~clock;
accel accel(
.clk (clock),
.rst (reset),
// SPI port
.ACL_CSN (ACL_CSN),
.ACL_MOSI (ACL_MOSI),
.ACL_MISO (ACL_MISO),
.ACL_SCLK (ACL_SCLK),
// UART port
.RXD (RXD),
.TXD (TXD),
.CTS (CTS),
.RTS (RTS)
);
endmodule
写脚本sim.do如下:
vlib work
vlog ../src/accel.v ../src/SPI_transmitter.v ../src/UART_transmitter.v ../src/syn_fifo.v ./tb_accel.v
vsim work.tb_accel -voptargs=+acc +notimingchecks
log -depth 7 /tb_accel/*
#do wave.do
run 2ms
调用前面全部的代码,打开ModelSim后转到脚本在的路径,使用命令do sim.do即可开始仿真。
仿真时可以添加想要的信号到waveform窗口中观察,然后可以保存为wave.do,这样下次可以通过调用它来加入一样的信号,节省一个一个加入的时间,这时你可以把sim.do中被#注释掉的那行去注释
调用仿真脚本得到的结果如下,读取到0xAA:
将Testbench中的注释部分修改后,变成SPI写操作,结果如下:
这和SPI在文档中的时序一样
新建一个叫accel的project,配置为开发板NEXYS4。添加代码文件accel.v、SPI_transmitter.v、UART_transmitter.v和syn_fifo.v
下一步加入约束constraint文件accel.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]
##Switches
set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst]
## LEDs
set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports LED_INT1]
set_property -dict {PACKAGE_PIN K15 IOSTANDARD LVCMOS33} [get_ports LED_INT2]
##Accelerometer
set_property -dict {PACKAGE_PIN E15 IOSTANDARD LVCMOS33} [get_ports ACL_MISO]
set_property -dict {PACKAGE_PIN F14 IOSTANDARD LVCMOS33} [get_ports ACL_MOSI]
set_property -dict {PACKAGE_PIN F15 IOSTANDARD LVCMOS33} [get_ports ACL_SCLK]
set_property -dict {PACKAGE_PIN D15 IOSTANDARD LVCMOS33} [get_ports ACL_CSN]
set_property -dict {PACKAGE_PIN B13 IOSTANDARD LVCMOS33} [get_ports ACL_INT1]
set_property -dict {PACKAGE_PIN C16 IOSTANDARD LVCMOS33} [get_ports ACL_INT2]
##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,加入和SPI有关的接口,并设置长度为8192,因为SPI接口的5MHz比其他的接口时钟速度要快不少:
下面就可以Run Implementation和Generate Bitstream生成bitstream了。
和前面的教程一样,USB线连接NEXYS4板子,开启Hardware Manager,然后auto连接上板子,Program Device烧写进程序,注意Debug probes file有对应的ltx文件。
打开Putty串口接口,具体配置可以参考教程系列11,下面根据前面example里的流程,分别打入如下指令(注意大写锁定,没有做大小写识别):
在Putty窗口中看到结果如下:
看到在读取寄存器0x20时,返回的是之前写入的0xFA
读取Z方向的加速度传感器时,返回的是0xC5,根据example中的描述,数据范围是正负2g,因此剔除符号位后除以64。0xC5取反加一得到其绝对值0x3B,也就是十进制59,除以64再考虑符号位,得到的结果是Z方向是-0.921875个重力加速度。
为了验证没读错,我把开发板反过来,再读取,得到0x4A,换算出来是1.15625个重力加速度,大概是正确的。
再打开ChipScope,设置trigger为CSN的下降沿,开启后输入000A2396,看到的SPI写操作如下:
再尝试010B0A00,看到的SPI读操作如下:
可以看到仿真看到的时序
如果你做完了这一系列操作,再看开发板,在不动时候,从右到左第二个LED是暗的,把开发板快速提起来就亮起,放下等待5秒之后,就自动暗下,直到下一次开发板被动
板载的加速度传感器accelerometer ADXL362就到这里了,下一篇要介绍开发板上的音频控制