本文将深入介绍另一种在嵌入式系统中广泛应用的同步串行通信协议——SPI(Serial Peripheral Interface)。SPI协议通常由一个主设备和一个或多个从设备构成,主设备负责发起并控制同步通信时序,从而完成数据交换。标准SPI协议仅使用4个引脚,因其接口简单、速率较高,常被用于FPGA或微处理器与Flash存储器、实时时钟、传感器及DSP等外设器件之间的通信。
SPI接口的四根信号线详解
- SSEL/CS:从设备片选使能信号。通常为低电平有效,当主设备将该引脚拉低时,对应的从设备被选中,主从设备之间方可进行通信。
- SCK:串行时钟信号,由主设备产生并输出,用于同步数据位传输。
- MOSI:主设备输出、从设备输入数据线。主设备通过此通道向从设备发送指令或数据。
- MISO:主设备输入、从设备输出数据线。主设备通过此通道读取从设备的状态或数据。
关键概念:CPOL与CPHA
为了正确配置SPI通信,必须理解两个关键参数:
- CPOL (Clock Polarity):时钟极性。定义了SCK时钟线在空闲状态(非数据传输期间)的电平。若空闲时为高电平,则CPOL=1;若为低电平,则CPOL=0。
- CPHA (Clock Phase):时钟相位。定义了数据采样的边沿。
通过对CPOL和CPHA进行组合,SPI共有四种不同的工作模式,以适应不同外设的时序要求。
SPI的四种工作模式
模式0:CPOL = 0, CPHA = 0。
SCK空闲时为低电平。数据在SCK的上升沿被采样,在下降沿发生变化。

模式1:CPOL = 0, CPHA = 1。
SCK空闲时为低电平。数据在SCK的下降沿被采样,在上升沿发生变化。

模式2:CPOL = 1, CPHA = 0。
SCK空闲时为高电平。数据在SCK的下降沿被采样,在上升沿发生变化。

模式3:CPOL = 1, CPHA = 1。
SCK空闲时为高电平。数据在SCK的上升沿被采样,在下降沿发生变化。

时序规律总结:
- CPHA=1:数据的输出发生在SCK第一个边沿,采样发生在第二个边沿。
- CPHA=0:数据的采样发生在SCK第一个边沿,输出发生在第二个边沿。
- CPOL=1:SCK第一个边沿为下降沿。
- CPOL=0:SCK第一个边沿为上升沿。
在实际项目中,需要根据从设备的数据手册来确定应使用的SPI工作模式。理解网络协议的底层时序对于嵌入式开发和硬件设计至关重要。
FPGA实现:SPI主控制器Verilog代码示例
以下是一个SPI主控制器模块的Verilog实现代码,工作在模式0(CPOL=0, CPHA=0),系统时钟为100MHz,通过分频产生1MHz的SCK时钟。该设计采用状态机实现,是数字逻辑设计中控制时序逻辑的经典方法。
// 代码设计思路参考自《搭建你的数字积木》,已根据编码风格进行优化
module spi_master (
input mosi,
input clk, // 100MHz系统时钟
input rst,
input busy,
input spi_send, // 发送启动信号
input [7:0] spi_data_out, // 待发送的8位数据
output reg sck, // SPI时钟输出
output reg miso, // SPI数据输出(主->从)
output reg cs, // 片选信号
output reg spi_send_done // 发送完成标志
);
reg [3:0] count;
parameter IDLE = 0;
parameter CS_L = 1;
parameter DATA = 2;
parameter FINISH = 3;
reg [4:0] cur_st;
reg [4:0] nxt_st;
reg [7:0] reg_data; // 数据移位寄存器
reg sck_reg; // 1MHz时钟寄存器
reg [8:0] delay_count; // 分频计数器 (100MHz/1MHz/2 = 50)
// 分频计数器,用于产生1MHz时钟的使能信号
always @(posedge clk or negedge rst) begin
if (~rst)
delay_count <= 0;
else if (delay_count == 49)
delay_count <= 0;
else
delay_count <= delay_count + 1;
end
// 产生1MHz的sck_reg时钟信号
always @(posedge clk or negedge rst) begin
if (~rst)
sck_reg <= 0;
else if (delay_count == 49)
sck_reg <= ~sck_reg;
end
// 组合逻辑,根据状态控制最终的sck输出
always @(*) begin
if (cs)
sck = 1;
else if (cur_st == FINISH)
sck = 1;
else if (!cs)
sck = sck_reg;
else
sck = 1;
end
// 状态寄存器更新
always @(posedge sck_reg or negedge rst) begin
if (~rst)
cur_st <= IDLE;
else
cur_st <= nxt_st;
end
// 下一状态逻辑 (状态转移)
always @(*) begin
nxt_st <= cur_st;
case (cur_st)
IDLE: if (spi_send) nxt_st = CS_L;
CS_L: nxt_st = DATA;
DATA: if (count == 7) nxt_st = FINISH;
FINISH: if (busy) nxt_st = IDLE;
default: nxt_st = IDLE;
endcase
end
// 发送完成信号产生逻辑
always @(*) begin
if (~rst)
spi_send_done = 0;
else if (cur_st == FINISH)
spi_send_done = 1;
else
spi_send_done = 0;
end
// 片选信号(cs)产生逻辑
always @(posedge sck_reg or negedge rst) begin
if (~rst)
cs <= 1;
else if (cur_st == CS_L)
cs <= 0;
else if (cur_st == DATA)
cs <= 0;
else
cs <= 1;
end
// 数据位计数器
always @(posedge sck_reg or negedge rst) begin
if (~rst)
count <= 0;
else if (cur_st == DATA)
count <= count + 1;
else if (cur_st == IDLE | cur_st == FINISH)
count <= 0;
end
// 数据移位输出逻辑 (在sck下降沿操作,符合模式0时序)
always @(negedge sck_reg or negedge rst) begin
if (~rst) begin
miso <= 0;
reg_data <= 0;
end
else if (cur_st == DATA) begin
// 寄存器左移,输出最高位
reg_data[7:1] <= reg_data[6:0];
miso <= reg_data[7];
end
else if (spi_send) begin
// 发送命令到来时,锁存待发送数据
reg_data <= spi_data_out;
end
end
endmodule
|