第四章:量化交易数据接口——SPI协议详解与实战
各位同学,今天我们来聊聊量化交易系统里一个绕不开的话题——SPI接口。说实话,我做了十几年FPGA,从高频交易到工业控制,SPI这玩意儿几乎天天见。它不像PCIe那么复杂,也不像UART那么慢,属于那种「看着简单,用好了真香」的协议。
在量化交易系统里,SPI主要干两件事:采集ADC数据和控制DAC输出。你想想看,行情数据从模拟信号变成数字信号,靠的是ADC;交易指令从数字信号变成模拟信号去驱动硬件,靠的是DAC。而SPI就是连接FPGA和这些转换器件的桥梁。
核心知识点速览
- SPI协议的四线制原理(SCK、MOSI、MISO、CS)
- 四种工作模式(CPOL/CPHA组合)
- SPI Master的Verilog实现
- SPI Slave的Verilog实现
- ADC数据采集实战(以AD7689为例)
- DAC数据输出实战(以AD5668为例)
4.1 SPI协议基础——别被四根线吓到
SPI全称是Serial Peripheral Interface,串行外设接口。说白了就是一根时钟线、两根数据线、一根片选线。我刚开始学的时候觉得这玩意儿太简单了,结果第一次调ADC的时候就被时序坑了一把。
四根线各司其职:
- SCK(Serial Clock):时钟线,由Master产生。频率决定了通信速度。
- MOSI(Master Out Slave In):主发从收线。
- MISO(Master In Slave Out):主收从发线。
- CS(Chip Select):片选线,低电平有效。谁拉低谁说话。
个人经验:我习惯把CS叫做「话筒线」。谁拿到话筒谁发言,多个Slave设备就是轮流拿话筒。记得在切换CS的时候留几个时钟周期的间隔,不然Slave设备反应不过来。
4.2 四种工作模式——CPOL和CPHA的排列组合
SPI有四种模式,由CPOL(时钟极性)和CPHA(时钟相位)决定。很多初学者在这里栽跟头,我当年也是。
| 模式 | CPOL | CPHA | 数据采样沿 | 常用场景 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 上升沿采样 | 最常用,兼容性最好 |
| 模式1 | 0 | 1 | 下降沿采样 | 某些ADC使用 |
| 模式2 | 1 | 0 | 下降沿采样 | 部分DAC使用 |
| 模式3 | 1 | 1 | 上升沿采样 | 某些传感器使用 |
为什么会搞出四种模式?说白了就是不同厂家芯片的设计习惯不一样。我建议你拿到芯片数据手册后,第一件事就是看时序图,找到CPOL和CPHA的值。千万别想当然用模式0,我吃过这个亏。
避坑指南:我曾经在项目里用AD7689 ADC,数据手册上写的是模式1。我习惯性用了模式0,结果读出来的数据全是乱的。查了两天才发现是模式不匹配。从那以后,我每用一个新芯片,第一件事就是对着数据手册画时序图。
4.3 SPI Master的Verilog实现——状态机是王道
实现SPI Master,我推荐用状态机。别用计数器硬怼,后期维护起来想哭。下面是我常用的三段式状态机框架:
module spi_master #(
parameter CLK_DIV = 4, // 时钟分频系数
parameter DATA_WIDTH = 16 // 数据位宽
)(
input wire clk,
input wire rst_n,
input wire start,
input wire [DATA_WIDTH-1:0] tx_data,
output reg [DATA_WIDTH-1:0] rx_data,
output reg busy,
// SPI接口
output reg sck,
output reg mosi,
input wire miso,
output reg cs
);
localparam IDLE = 2'b00;
localparam TRANSFER = 2'b01;
localparam DONE = 2'b10;
reg [1:0] state, next_state;
reg [3:0] clk_cnt;
reg [4:0] bit_cnt;
reg sck_en;
// 状态机第一段:状态转移
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= IDLE;
else state <= next_state;
end
// 状态机第二段:次态逻辑
always @(*) begin
case (state)
IDLE: next_state = start ? TRANSFER : IDLE;
TRANSFER: next_state = (bit_cnt == DATA_WIDTH) ? DONE : TRANSFER;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 状态机第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cs <= 1'b1;
sck <= 1'b0;
mosi <= 1'b0;
busy <= 1'b0;
rx_data <= 0;
clk_cnt <= 0;
bit_cnt <= 0;
end else begin
case (state)
IDLE: begin
cs <= 1'b1;
busy <= 1'b0;
bit_cnt <= 0;
clk_cnt <= 0;
end
TRANSFER: begin
cs <= 1'b0;
busy <= 1'b1;
// 时钟分频逻辑
if (clk_cnt == CLK_DIV-1) begin
clk_cnt <= 0;
sck <= ~sck;
// 在SCK的上升沿发送数据
if (sck) begin
mosi <= tx_data[DATA_WIDTH-1-bit_cnt];
rx_data <= {rx_data[DATA_WIDTH-2:0], miso};
bit_cnt <= bit_cnt + 1;
end
end else begin
clk_cnt <= clk_cnt + 1;
end
end
DONE: begin
cs <= 1'b1;
busy <= 1'b0;
end
endcase
end
end
endmodule
个人习惯:我写SPI Master时,喜欢把时钟分频和位计数分开处理。这样代码可读性强,调试也方便。另外,记得在TRANSFER状态里处理SCK的边沿,别在IDLE状态里乱动时钟。
4.4 SPI Slave的Verilog实现——跟着Master的节奏走
Slave的实现比Master简单,因为时钟是Master给的。你只需要跟着SCK的节奏采样和发送数据就行。但有个坑:Slave的时钟域是外部输入的,一定要做同步处理。
module spi_slave #(
parameter DATA_WIDTH = 16
)(
input wire clk, // 系统时钟,用于同步
input wire rst_n,
// SPI接口
input wire sck,
input wire mosi,
output reg miso,
input wire cs,
// 用户接口
input wire [DATA_WIDTH-1:0] tx_data,
output reg [DATA_WIDTH-1:0] rx_data,
output reg data_valid
);
reg [2:0] sck_sync;
reg sck_rising, sck_falling;
reg [4:0] bit_cnt;
// 同步SCK到系统时钟域
always @(posedge clk or negedge rst_n) begin
if (!rst_n) sck_sync <= 0;
else sck_sync <= {sck_sync[1:0], sck};
end
assign sck_rising = (sck_sync[2:1] == 2'b01);
assign sck_falling = (sck_sync[2:1] == 2'b10);
// 数据接收与发送
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_cnt <= 0;
rx_data <= 0;
miso <= 1'b0;
data_valid <= 1'b0;
end else if (!cs) begin
if (sck_rising) begin
// 在SCK上升沿采样MOSI
rx_data <= {rx_data[DATA_WIDTH-2:0], mosi};
bit_cnt <= bit_cnt + 1;
end
if (sck_falling) begin
// 在SCK下降沿更新MISO
miso <= tx_data[DATA_WIDTH-1-bit_cnt];
end
// 数据接收完成
if (bit_cnt == DATA_WIDTH && sck_rising) begin
data_valid <= 1'b1;
bit_cnt <= 0;
end
end else begin
data_valid <= 1'b0;
bit_cnt <= 0;
end
end
endmodule
避坑指南:我曾经在Slave实现里直接用sck作为时钟驱动寄存器,结果综合出来一堆亚稳态问题。记住:外部输入的时钟一定要同步到本地时钟域。用三级触发器打拍是最稳妥的做法。
4.5 ADC数据采集实战——以AD7689为例
AD7689是一款16位、8通道的SAR ADC,在量化交易系统里很常见。它支持SPI模式1(CPOL=0, CPHA=1)。我习惯用250kHz的SCK频率,这样采样率能到15kSPS左右,够用。
AD7689的SPI时序要点:
- CS拉低后,先发送24位配置字(包含通道选择、参考电压等)
- 配置字发送完成后,等待转换完成(tconv时间)
- 再发送16个SCK脉冲,读取16位转换结果
- CS拉高,结束一次转换周期
// AD7689配置字示例
// 通道0,内部参考,正常模式
localparam [23:0] AD7689_CFG = 24'h00_00_00;
// 读取ADC数据的SPI事务
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
adc_start <= 1'b0;
adc_channel <= 0;
end else begin
case (adc_state)
IDLE: begin
if (adc_req) begin
adc_start <= 1'b1;
adc_state <= SEND_CFG;
end
end
SEND_CFG: begin
// 发送24位配置字
// ... SPI Master发送逻辑
if (cfg_done) adc_state <= WAIT_CONV;
end
WAIT_CONV: begin
// 等待转换完成,约4us
if (conv_done) adc_state <= READ_DATA;
end
READ_DATA: begin
// 读取16位数据
// ... SPI Master接收逻辑
if (read_done) begin
adc_data <= rx_data;
adc_state <= IDLE;
end
end
endcase
end
end
个人经验:AD7689的转换时间大约是4us,我建议在WAIT_CONV状态里用计数器精确等待,别用延时函数。FPGA的时序精度是纳秒级的,浪费了可惜。
4.6 DAC数据输出实战——以AD5668为例
AD5668是8通道、16位DAC,常用于生成模拟控制信号。它支持SPI模式0(CPOL=0, CPHA=0)。我一般在交易信号生成模块里用它输出模拟电压。
AD5668的SPI时序要点:
- CS拉低后,发送24位数据帧
- 前4位是命令(写、更新、复位等)
- 中间4位是地址(选择哪个通道)
- 后16位是DAC数据
- CS拉高,数据锁存到DAC寄存器
// AD5668数据帧格式
// {4'b0011, 4'b0000, 16'd_value} // 写通道0并更新
// DAC输出控制
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dac_start <= 1'b0;
dac_value <= 0;
end else begin
case (dac_state)
IDLE: begin
if (dac_req) begin
// 组装24位数据帧
dac_frame <= {4'b0011, dac_addr, dac_value};
dac_start <= 1'b1;
dac_state <= SEND_FRAME;
end
end
SEND_FRAME: begin
// 发送24位数据帧
// ... SPI Master发送逻辑
if (frame_done) begin
dac_state <= IDLE;
dac_done <= 1'b1;
end
end
endcase
end
end
关键提醒:DAC的输出更新时机很重要。AD5668有两种模式:同步更新(所有通道同时更新)和异步更新(每个通道独立更新)。在量化交易系统里,我建议用同步更新模式,保证所有通道的输出在同一时刻变化,避免时序偏差。
4.7 调试技巧与常见问题
最后分享几个我在项目中积累的调试技巧:
- 用逻辑分析仪抓波形——别光靠仿真。实际硬件上的信号质量、时序偏差,仿真看不出来。我习惯用Saleae逻辑分析仪,便宜好用。
- 先测回环——把MOSI和MISO短接,发什么收什么。如果回环测试通过,说明SPI Master基本没问题。
- 注意电平匹配——FPGA通常是3.3V或2.5V,有些ADC/DAC是5V的。电平转换芯片不能省,我见过烧芯片的案例。
- CS的毛刺——CS信号一定要干净。如果CS上有毛刺,Slave设备会误触发。加个施密特触发器或者软件去抖都行。
避坑指南:我曾经在一个高频交易项目里,SPI通信偶尔出错,查了三天发现是电源纹波太大导致SCK抖动。后来在FPGA的SPI模块输入端加了数字滤波,问题解决。记住:SPI的时钟质量直接影响通信可靠性。
好了,SPI协议和实战就讲到这里。下一章我们会深入探讨另一个重要的数据接口——I2C协议。记住,SPI的核心就是「四根线、一个主、多个从、全双工」。把这个框架记牢了,后面调任何SPI设备都不怕。
公众号:蓝海资料掘金营,微信deep3321