测试环境
操作系统:Windows10
综合仿真:Vivado 2018.3
芯片验证:Zynq7010
SPI模式
极性:CPOL 相位:CPHA
Mode0 CPOL=0, CPHA=0
Mode1 CPOL=0, CPHA=1
Mode2 CPOL=1, CPHA=0
Mode3 CPOL=1, CPHA=1
时钟极性CPOL: SPI空闲时,时钟信号SCLK的电平(1:空闲时高电平; 0:空闲时低电平)
时钟相位CPHA: SPI在SCLK第几个边沿采样数据(0:第一个边沿; 1:第二个边沿)
SPI时序分析
如下图,这是四种模式下的SPI时序,图中四种模式共用MISO:
从图中可以看出,MISO数据采样和MOSI数据输出都发生在同一时刻,只是它们的时钟不同。而仔细观察,4种模式都可以通过一种时钟变换得来,这里以模式3时钟为参考:
模式0时钟:对模式3时钟反相并移相半个周期
模式1时钟:对模式3时钟反相
模式2时钟:对模式3时钟移相半个周期
因此,4种模式我们可以不受SPI时钟的影响,只需要在固定的时间采样MISO数据及对MOSI时序做更新即可。
硬件描述
首先可以通过设置一个division来调节SPI时钟频率:
可以把SPI状态分为以下9种方式:
通过状态机来管理SPI的状态:
参考状态机及模式位生成SPI时钟:
参考状态机更新MOSI线:
根据参考时钟采样MISO数据:
最后检出一个finish脉冲信号:
完整代码:
/**
* division分频系数计算公式: sclock = clock/2/(division+1) => division = clock/2/sclock-1
* 例如输入时钟clock为50M, 需要得到2M的spi时钟sclock, 则 division = 50000000/2/2000000-1 大约为 12
* time: 2021-2-14
* editor: huxiang hello
*/
/**
* --------------- spi四种模式 ---------------------
* 极性:CPOL 相位:CPHA
* Mode0 CPOL=0, CPHA=0
* Mode1 CPOL=0, CPHA=1
* Mode2 CPOL=1, CPHA=0
* Mode3 CPOL=1, CPHA=1
* 时钟极性CPOL: 即SPI空闲时,时钟信号SCLK的电平(1:空闲时高电平; 0:空闲时低电平)
* 时钟相位CPHA: 即SPI在SCLK第几个边沿采样数据(0:第一个边沿开始; 1:第二个边沿开始)
*/
module spi(
input wire clock, // 时钟
input wire reset, // 复位信号
input wire [31:0] division,// 分频系数
input wire [1:0] mode, // spi模式
output wire sclock, // spi时钟引脚
input wire smiso, // spi主入从出引脚
output wire smosi, // spi主出从进引脚
input wire [7:0] odata, // 发送数据寄存器
output reg [7:0] idata, // 接收数据寄存器
input wire trigger,// 数据传输触发线 - 高电平触发
output wire finish // 数据传输完成 - 高电平触发
);
// spi时钟分频计数
reg [31:0] ccount;
// 每次clock_count计数完后发出一个高电平脉冲
wire ccount_clear = (ccount == division) ? 1'b1 : 1'b0;
always @(negedge clock)
begin
if(!reset || ccount_clear || trigger)
ccount <= 32'd0;
else
ccount = ccount + 32'd1;
end
// spi状态参数
localparam STATE_BIT0 = 4'd0;
localparam STATE_BIT1 = 4'd1;
localparam STATE_BIT2 = 4'd2;
localparam STATE_BIT3 = 4'd3;
localparam STATE_BIT4 = 4'd4;
localparam STATE_BIT5 = 4'd5;
localparam STATE_BIT6 = 4'd6;
localparam STATE_BIT7 = 4'd7;
localparam STATE_IDLE = 4'd8;
// spi状态机
reg [4:0] spi_state; // 第0位每变化一次spi时钟跳变半个周期
wire idle = spi_state[4]; // spi_state最高线就是idle线
// 产生8个时序 - 此次的时钟复位和空闲时都是高电平
always @(posedge clock)
begin
if(!reset)
spi_state <= {STATE_IDLE, 1'b0}; // 复位后为空闲态
else if(ccount_clear && !idle)
spi_state <= spi_state + 5'd1;
else if(trigger)
spi_state <= {STATE_BIT0, 1'b0}; // 收到触发发送信号进入发送状态
else;
end
// 根查表生成spi时钟(固定空闲高电平)
wire [17:0] sclock_table = 18'b111010101010101010;
wire sclock1 = sclock_table[spi_state];
// 对sclock1移相半个周期
reg sclock1_old;
always @(posedge clock) sclock1_old = ccount_clear ? sclock1 : sclock1_old;
// 根据模式设置选取时钟线 - 如果是在第一个沿采样数据,就选取慢半个周期的时钟线
wire sclock2 = mode[0] ? sclock1 : sclock1_old;
// sclock线, 根据模式设置spi输出时钟是否需要反相
assign sclock = mode[1] ? sclock2 : !sclock2;
// smosi线
wire [8:0] mosi_hub = {1'b1, odata};
assign smosi = mosi_hub[spi_state[4:1]];
// 根据参考时钟来采样数据
// smiso线
always @(posedge sclock1 or negedge reset)
begin
if(!reset)
idata <= 8'd0; // 复位后接收寄存器清0
else
idata <= {smiso, idata[7:1]}; // 综合成移位寄存器
end
// 处理finish, - 使用old_idle延一拍,检出一个上升沿
reg old_idle;
always @(posedge clock)
begin
if(!reset)
old_idle <= 1'b1;
else
old_idle <= idle;
end
assign finish = (!old_idle && idle) ? 1'b1 : 1'b0;
endmodule
如果需要通过AXI总线挂接到处理器上,可参考:使用AXI Lite总线将串口UART挂接到处理器