SPI可以实现一对多的多机通信:
本着边学习边运用的角度,想从原来的SPI库函数的使用者,实现完整的传输功能,本篇复刻了xmg的教程。有任何问题请指正!
SPI有四根标准的信号线:
- MISO(M-master I-input S-slave O-output)
- MOSI同理,主机输出,同时也是主设备向从设备输出的总线
- SCLK时钟
- SS(CS)片选信号
SPI必须由机控制从机,因为SCLK信号和SS信号是由主机设备产生的,因此根据主设备产生的时钟的相位和极性的不同,我们可主以将SPI配置成4种模式,当主机和从机配置成相同模式时,SPI的交互就可以完成传输和数据交互。
CPOL 表示时钟的极性:
CPOL=0:时钟空闲为低电平,代表高电平有效;
CPOL=1:时钟空闲为高电平,代表低电平有效;
CPHA表示时钟的采样相位:
CPHA=0:数据会从第一个边沿开始采样
CPHA=1:数据会从第二个边沿开始采样
此外我们还需要知道SPI的结构可以通过片选CS/SS来分为两种:
1.分片选在主机上(一个主机含有多个片选)
2.主机只有一个片选:这种通常适合在串行转并行的场合
我们有了以上概念就可以照着SPI的时序图 “抄” 了,这个是以CPOL=0,CPHA=0为例说明:
CPOL=0,CPHA=0的意思是什么?

(网上找的一个大佬画的图)
数据采样的意思就是输入,当然输入输出是相对的,以FPGA作为从机为例,
首先cs片选拉低后,根据数据采样的位置开始采样:
在cs拉低后的sck的初始状态是低电平,(CPOL=0)
在sck跳变的第一个时钟沿,进行数据传输,(CPHA=0)
为了追求通用一点的写法,我们需要通过两个极性创造一个自己需要的时钟:
assign clk_sel = (CPOL ^ CPHA) ? (SPI_SCK) : (~SPI_SCK);
随后我们很容易想到我们也需要一个复位信号,只要有复位按键按下,或者在cs电平被拉低就复位一次,确保单次传输所有寄存器全部清零:
assign spi_reset = (!rst_n)|spi_cs;
接下来我们就可以写接收和发送的部分了
用两个线性序列机就可以完成的,下面把代码全部附上:
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2024/09/22 22:59:45
// Design Name:
// Module Name: spi_slave
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
// SPI从机,将需要发送的数据send_data以SPI数据发送,并解析到SPI数据,
// 同时记录从cs低到高的自己输trans_cnt
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module spi_slave #
(
parameter integer CPOL = 1'b0, //空闲时SCK电平:1为高,0为低
parameter integer CPHA = 1'b0, //数据捕获边沿:0为第一个边沿,1为第2个边沿
parameter bits_order = 1'b1
)
(
input clk,
input rst_n,
input send_data_valid,
input [7:0] send_data,
input spi_cs,
input spi_sck,
input spi_mosi,
output reg recieve_data_valid,
output reg [7:0] recieve_data,
output reg [15:0] trans_cnt,//计数cs在低电平期间,接收到数据的bit
output reg trans_done,
output spi_miso,
output trans_start,
output trans_end
);
//先对CS片选信号打一拍,取下降沿
reg cs_r1,cs_r2;
wire sck_sel; //sck有效信号根据cpol和cpha来选择
wire spi_reset; //cs片选和rst复位共同决定,高电平有效
assign sck_sel = (CPHA ^ CPOL) ? (spi_sck):(~spi_sck);
assign spi_reset = (!rst_n)|spi_cs;
reg [7:0] in_cnt ;
reg [7:0] recieve;
reg miso;
//在发送MISO的数据时同样以sck_sel为时钟信号,取send_data[7] 在sck的前7个下降沿将send_data[6:0]发送
assign spi_miso = spi_cs ? 1 : ((out_cnt|CPHA) ? miso : send_data_r[7]);
reg [7:0] send_data_r ;
reg [7:0] out_cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
cs_r1 <= 8'h0;
cs_r2 <= 8'h0;
end
else begin
cs_r1 <= spi_cs;
cs_r2 <= cs_r1;
end
end
assign trans_start = (~cs_r1) & cs_r2;
assign trans_end = cs_r1 & (~cs_r2);
//将spi数据接收
always @(negedge sck_sel or posedge spi_reset) begin
if(spi_reset)begin
in_cnt <= 8'd0;
recieve <= 0;
trans_done <= 0;
trans_cnt <= 0;
end
else begin
case (in_cnt)
8'd0: begin in_cnt <= in_cnt + 1'b1; recieve[7]<=spi_mosi;trans_done<=1'b0; end
8'd1: begin in_cnt <= in_cnt + 1'b1; recieve[6]<=spi_mosi; end
8'd2: begin in_cnt <= in_cnt + 1'b1; recieve[5]<=spi_mosi; end
8'd3: begin in_cnt <= in_cnt + 1'b1; recieve[4]<=spi_mosi; end
8'd4: begin in_cnt <= in_cnt + 1'b1; recieve[3]<=spi_mosi; end
8'd5: begin in_cnt <= in_cnt + 1'b1; recieve[2]<=spi_mosi; end
8'd6: begin in_cnt <= in_cnt + 1'b1; recieve[1]<=spi_mosi; end
8'd7: begin in_cnt <= 8'd0; recieve[0]<=spi_mosi;trans_done<=1'b1; end
default: in_cnt <= 8'd0;
endcase
end
end
//在完成每一位的字节接收后,根据高位在前还是低位在前给recieve_data赋值
/*
bit_orders
由于这里sck的时钟和clk的时钟是两个时钟域,我们这里首先产生一个done_pos信号用来消除亚稳态
*/
reg done_r1,done_r2;
wire done_pos;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
done_r1 <= 0;
done_r2 <= 0;
end
else begin
done_r1 <= trans_done;
done_r2 <= done_r1;
end
end
assign done_pos = (done_r1)&(~done_r2);
//在done_pos信号为高电平期间,将接收数据有效信号拉高
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
recieve_data_valid <= 1'b0;
else if (done_pos)
recieve_data_valid <= 1'b1;
else
recieve_data_valid <= 1'b0;
end
//将接收的数据清零置位
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
recieve_data <= 8'h0;
else if(done_pos)begin
if(bits_order)
recieve_data <= recieve;
else
recieve_data <= {recieve[0],recieve[1],recieve[2],recieve[3],recieve[4],recieve[5],recieve[6],recieve[7]};
end
else
recieve_data <= recieve_data;
end
/*
我们将主机传输过来的数据进行解析,并将解析的数据recieve_data进行输出,现在我们就要将send_data
以spi的数据格式发送————MISO从机输出
这里既要考虑到发送数据的顺序问题,也要考虑到在发送的过程中,数据不要被修改
*/
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
send_data_r <= 8'h00;
else if(send_data_valid)begin
if(bits_order)
send_data_r <= send_data;
else
send_data_r <= {send_data[0],send_data[1],send_data[2],send_data[3],
send_data[4],send_data[5],send_data[6],send_data[7]};
end
else
send_data_r <= send_data_r;
end
/*
保障发送数据的数据正确性后就要考虑其时序正确性了:
同发送一样,接收部分我们也需要一个线性状态机去控制
*/
always @(negedge sck_sel or posedge spi_reset) begin
if(spi_reset)begin
out_cnt <= 8'd0;
end
else begin
case (out_cnt)
8'd0: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[6+CPHA]; end
8'd1: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[5+CPHA]; end
8'd2: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[4+CPHA]; end
8'd3: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[3+CPHA]; end
8'd4: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[2+CPHA]; end
8'd5: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[1+CPHA]; end
8'd6: begin out_cnt <= out_cnt + 1'b1; miso <= send_data_r[0+CPHA]; end
8'd0: begin out_cnt <= 8'd0; miso <= send_data_r[0]; end
default: out_cnt <= 8'd0;
endcase
end
end
endmodule
下面是仿真部分的验证:

9万+

被折叠的 条评论
为什么被折叠?



