网上很多教程都是怎么做SPI主机,怎么控制外部的东西,但是有时候FPGA是作为接受上游信息的存在的,有必要实现一个从机协议.我这里就一步一步的描述我是怎么思考然后最终实现一个从机的.
首先我这里确定一下我需要的信号.
module top(
input wire clk_i,
input wire rst_i,
input wire sdi_csn_i,
input wire sdi_clk_i,
input wire sdi_dat_i,
output wire sdi_dat_o,
output wire [5:0] led
);
因为我的开发板只有6个LED,所以就暂时让收到的8B数据中的6B显示在LED,其余的都是SPI本身的信号,以及系统主时钟,也可以理解成采样时钟,并且采样时钟是快于SPI时钟的.
然后跨时钟域采集他们,并且要捕获SCK的上升沿.
reg [2:0] sync_sck_ff;
reg [1:0] sync_csn_ff;
reg [1:0] sync_sdi_ff;
// SCK 边缘采集
assign sync_sck = sync_sck_ff[1] ^ sync_sck_ff[2];
assign sync_csn = sync_csn_ff[1];
assign sync_sdi = sync_sdi_ff[1];
// 跨时钟域处理
always @(posedge clk_i or negedge rstn_i) begin
if(!rstn_i) begin
sync_sck_ff <= 1'b0;
sync_csn_ff <= 1'b0;
sync_sdi_ff <= 1'b0;
end else begin
sync_sck_ff <= {sync_sck_ff[1:0],sdi_clk_i};
sync_csn_ff <= {sync_csn_ff[0],sdi_csn_i};
sync_sdi_ff <= {sync_sdi_ff[0],sdi_dat_i};
end
end
其中sync_sck指示目前时钟跳变了,sync_csn是传递到clk_i时钟域的CS,sync_sdi是传递到clk_i时钟域的DAT,sdi_dat_o为什么没在这里呢,因为我们clk_i是快时钟,我们只要保证快时钟有足够的周期数,慢时钟的sdi_clk_i就能采集,而因为我们一定会保持到下一个sdi_clk_i来到,所以肯定是足够的.
接下来到最难的状态机部分了,我这里定义了三个状态,用serial_state来记录.
- serial_state = 00,属于IDLE状态,等CSN有效后转到10状态.
- serial_state = 10,属于等待SCK状态,当SCK发生上升沿,转到11状态.
- serial_state = 11,锁存数据,如果计算到8位数据了,说明传输完成.
- 传输完成或者CSN中途拉高会使得serial_state = 00.
- 传输完成会触发serial_done上升沿,传输开始会触发serial_start上升沿.
- 数据应存在serial_sreg,当serial_done上升沿时,sreg有效,应立即取走.
- sdi_dat_o数据也是在serial_sreg,当然这个可以另外实现.
reg [1:0] serial_state;
reg [3:0] serial_cnt;
reg [7:0] serial_sreg;
reg serial_sdi_ff;
reg serial_start;
reg serial_done;
assign sdi_dat_o = serial_sreg[7];
always @(posedge clk_i or negedge rstn_i) begin
if(!rstn_i) begin
serial_start <= 1'b0;
serial_done <= 1'b0;
serial_state <= 3'b0;
serial_cnt <= 4'b0;
serial_sreg <= 8'b0;
serial_sdi_ff <= 1'b0;
end
else begin
serial_start <= 1'b0;
serial_done <= 1'b0;
case (serial_state)
2'b00:begin
serial_cnt <= 4'b0;
serial_sreg <= 8'h00;
if(!sync_csn) begin
serial_start <= 1'b1;
serial_state[1:0] <= 2'b10;
end
end
2'b10:begin
serial_sdi_ff <= sync_sdi;
if(sync_csn) begin
serial_state[1:0] <= 2'b00;
end
else if (sync_sck) begin
serial_cnt <= serial_cnt + 1'b1;
serial_state[1:0] <= 2'b11;
end
end
2'b11:begin
if(sync_csn) begin
serial_state[1:0] <= 2'b00;
end
else if(sync_sck) begin
serial_sreg <= {serial_sreg[6:0],serial_sdi_ff};
if(serial_cnt[3] == 1'b1) begin
serial_done <= 1'b1;
serial_state[1:0] <= 2'b00;
end
else begin
serial_state[1:0] <= 2'b10;
end
end
end
default begin
serial_state[1:0] <= 2'b00;
end
endcase
end
end
把数值附给LED.
reg [7:0] led_reg;
assign led = ~led_reg[5:0];
always @(posedge clk_i or negedge rstn_i) begin
if(!rstn_i) begin
led_reg[7:0] = 8'b0;
end
else if(serial_done)
begin
led_reg[7:0] = serial_sreg[7:0];
end
end
但是在实测中serial_sreg仍有低概率是错误的,这时候可以再前置一个滤波采集,完整代码如下.
https://gist.github.com/nickfox-taterli/fe3713455b0ba55c73b63d45512f2bd9