● SPI传输协议
● SPI配置方式
● SPI代码实现
● SPI的优缺点
SPI 主要应用在 EEPROM, Flash, 实时时钟(RTC), 数模转换器(ADC), 数字信号处理器(DSP) 以及数字信号解码器之间。它在芯片中只占用四根管脚 (Pin) 用来控制以及数据传输, 节约了芯片的 pin 数目, 同时为 PCB 在布局上节省了空间。正是出于这种简单易用的特性, 现在越来越多的芯片上都集成了 SPI技术。
在数据传输的过程中, 每次接收到的数据必须在下一次数据传输之前被采样. 如果之前接收到的数据没有被读取, 那么这些已经接收完成的数据将有可能会被丢弃, 导致 SPI 物理模块最终失效. 因此, 在程序中一般都会在 SPI 传输完数据后, 去读取 SPI 设备里的数据, 即使这些数据(Dummy Data)在我们的程序里是无用的.
CPOL(clock polarity): 时钟极性, 表示 SPI 在空闲时, 时钟信号是高电平还是低电平. 若 CPOL 被设为 1, 那么该设备在空闲时 SCK 管脚下的时钟信号为高电平. 当 CPOL 被设为 0 时则正好相反;
CPHA(clock phase): 时钟相位, 表示 SPI 设备是在 SCK 管脚上的时钟信号变为上升沿时触发数据采样, 还是在时钟信号变为下降沿时触发数据采样. 若 CPHA 被设置为 1, 则 SPI 设备在时钟信号变为下降沿时触发数据采样, 在上升沿时发送数据. 当 CPHA 被设为 0 时也正好相反.
SSPSR(Synchronous Serial Port Register): 泛指 SPI 设备里面的移位寄存器(Shift Regitser), 它的作用是根据设置好的数据位宽(bit-width) 把数据移入或者移出 SSPBUF;
Controller: 泛指 SPI 设备里面的控制寄存器, 可以通过配置它们来设置 SPI 总线的传输模式.
通常情况下, 我们只需要对上图所描述的四个管脚(pin) 进行控制即可实现整个 SPI 设备之间的数据通信:
SCK(Serial Clock): 主要的作用是 Master 设备往 Slave 设备传输时钟信号, 控制数据交换的时机以及速率;
SS/CS(Slave Select/Chip Select): 用于 Master 设备片选 Slave 设备, 使被选中的 Slave 设备能够被 Master 设备所访问;
SDO/MOSI(Serial Data Output/Master Out Slave In): 在 Master 上面也被称为 Tx-Channel, 作为数据的出口, 主要用于 SPI 设备发送数据;
SDI/MISO(Serial Data Input/Master In Slave Out): 在 Master 上面也被称为 Rx-Channel, 作为数据的入口, 主要用于SPI 设备接收数据。
首先, 在这里解释一下两个概念:
CPOL: 时钟极性, 表示 SPI 在空闲时, 时钟信号是高电平还是低电平. 若 CPOL 被设为 1, 那么该设备在空闲时 SCK 管脚下的时钟信号为高电平. 当 CPOL 被设为 0 时则正好相反.
CPHA: 时钟相位, 表示 SPI 设备是在 SCK 管脚上的时钟信号变为上升沿时触发数据采样, 还是在时钟信号变为下降沿时触发数据采样. 若 CPHA 被设置为 1, 则 SPI 设备在时钟信号变为下降沿时触发数据采样, 在上升沿时发送数据. 当 CPHA 被设为 0 时也正好相反.
例如,SPI 数据传输模式被设置成 CPOL = 1, CPHA = 1. 这样, 在一个 Clock 周期内, 每个单独的 SPI 设备都能以全双工(Full-Duplex) 的方式, 同时发送和接收 1 bit 数据, 即相当于交换了 1 bit 大小的数据. 如果 SPI 总线的 Channel-Width 被设置成 Byte, 表示 SPI 总线上每次数据传输的最小单位为 Byte, 那么挂载在该 SPI 总线的设备每次数据传输的过程至少需要 8 个 Clock 周期(忽略设备的物理延迟). 因此, SPI 总线的频率越快, Clock 周期越短, 则 SPI 设备间数据交换的速率就越快.
2. SSPSR
SSPSR 是 SPI 设备内部的移位寄存器(Shift Register). 它的主要作用是根据 SPI 时钟信号状态, 往 SSPBUF 里移入或者移出数据, 每次移动的数据大小由 Bus-Width 以及 Channel-Width 所决定.
Bus-Width 的作用是指定地址总线到 Master 设备之间数据传输的单位.
例如, 我们想要往 Master 设备里面的 SSPBUF 写入 16 Byte 大小的数据: 首先, 给 Master 设备的配置寄存器设置 Bus-Width 为 Byte; 然后往 Master 设备的 Tx-Data 移位寄存器在地址总线的入口写入数据, 每次写入 1 Byte 大小的数据(使用 writeb 函数); 写完 1 Byte 数据之后, Master 设备里面的 Tx-Data 移位寄存器会自动把从地址总线传来的1 Byte 数据移入 SSPBUF 里; 上述动作一共需要重复执行 16 次.
Channel-Width 的作用是指定 Master 设备与 Slave 设备之间数据传输的单位. 与 Bus-Width 相似, Master 设备内部的移位寄存器会依据 Channel-Width 自动地把数据从 Master-SSPBUF 里通过 Master-SDO 管脚搬运到 Slave 设备里的 Slave-SDI 引脚, Slave-SSPSR 再把每次接收的数据移入 Slave-SSPBUF里.
通常情况下, Bus-Width 总是会大于或等于 Channel-Width, 这样能保证不会出现因 Master 与 Slave 之间数据交换的频率比地址总线与 Master 之间的数据交换频率要快, 导致 SSPBUF 里面存放的数据为无效数据这样的情况.
3. SSPBUF
我们知道, 在每个时钟周期内, Master 与 Slave 之间交换的数据其实都是 SPI 内部移位寄存器从 SSPBUF 里面拷贝的. 我们可以通过往 SSPBUF 对应的寄存器 (Tx-Data / Rx-Data register) 里读写数据, 间接地操控 SPI 设备内部的 SSPBUF.
例如, 在发送数据之前, 我们应该先往 Master 的 Tx-Data 寄存器写入将要发送出去的数据, 这些数据会被 Master-SSPSR 移位寄存器根据 Bus-Width 自动移入 Master-SSPBUF 里, 然后这些数据又会被 Master-SSPSR 根据 Channel-Width 从 Master-SSPBUF 中移出, 通过 Master-SDO 管脚传给 Slave-SDI 管脚, Slave-SSPSR 则把从 Slave-SDI 接收到的数据移入 Slave-SSPBUF 里. 与此同时, Slave-SSPBUF 里面的数据根据每次接收数据的大小(Channel-Width), 通过 Slave-SDO 发往 Master-SDI, Master-SSPSR 再把从 Master-SDI 接收的数据移入 Master-SSPBUF.在单次数据传输完成之后, 用户程序可以通过从 Master 设备的 Rx-Data 寄存器读取 Master 设备数据交换得到的数据.
4. Controller
Master 设备里面的 Controller 主要通过时钟信号(Clock Signal)以及片选信号(Slave Select Signal)来控制 Slave 设备. Slave 设备会一直等待, 直到接收到 Master 设备发过来的片选信号, 然后根据时钟信号来工作.
Master 设备的片选操作必须由程序所实现. 例如: 由程序把 SS/CS 管脚的时钟信号拉低电平, 完成 SPI 设备数据通信的前期工作; 当程序想让 SPI 设备结束数据通信时, 再把 SS/CS 管脚上的时钟信号拉高电平.
但是,随着从机数量的增加,来自主机的片选线的数量也增加。这会快速增加主机需要提供的输入和输出数量,并限制可以使用的从机数量。可以使用其他技术来增加常规模式下的从机数量,例如使用多路复用器产生片选信号。
使用该方法时,由于数据是从一个从机传播到下一个从机,所以传输数据所需的时钟周期数与菊花链中的从机位置成比例。例如,在上图所示的8位系统中,为使第3个从机能够获得数据,需要24个时钟脉冲,而常规SPI模式下只需8个时钟脉冲。
时钟周期和通过菊花链的数据传播如下图所示:
1. 主机模块(master)接口定义:
module spi_master(
input clk_40k, //时钟信号,40kHz
input rst_n, //复位信号,低有效
input [7:0] data_in, //主机准备要输出给从机的数据,8位宽
input send_start, //通信使能信号,高有效,宽度为1个时钟周期(40kHz),收到该信号后开始一次主从设备通信
output [7:0] data_out, //主机从从机接收到的数据,8位宽
output data_out_vld, //输出数据有效标志,高电平有效,宽度为1个时钟周期(40kHz)
output cs_n, //从设备片选使能信号,低有效,低电平时选中从设备与主设备进行通信,处于通信状态时维持低电平
output sclk, //同步时钟,1kHz,空闲时置低电平
input miso, //主机当前从从机收到的串行数据
output mosi //主机当前发送给从机的串行数据
);
module spi_slave(
input rst_n, //复位信号,低有效
input cs_n, //从设备片选使能信号
input sclk, //SPI时钟,1kHz空闲时置低电平
input mosi, //从机从主机接收到的串行数据
output miso, //从机要发送给主机的串行数据
output [7:0] reg0_out, //内部寄存器0的值
output [7:0] reg1_out, //内部寄存器1的值
output [7:0] reg2_out, //内部寄存器2的值
output [7:0] reg3_out //内部寄存器3的值
);
3. 电路功能描述:
4. SPI传输格式:
SPI每帧数据包含16位,最先发送的第0位为读写控制位,该位为0代表master向slave写数据,为1则代表master从slave读数据;随后发送的第1-7位为地址位,先发高位地址再发低位地址,9-16位为数据位,高位数据先发。所有数据均在sclk的上升沿产生,下降沿采样。
module spi_master(
input clk_40k,
input rst_n,
input [7:0] data_in,
input send_start,
output [7:0] data_out,
output data_out_vld,
output cs_n,
output sclk,
input miso,
output mosi
);
reg cs_n_r;
reg sclk_r;
reg mosi_r;
reg data_out_vld_r;
reg [7:0] data_out_r;
reg wr_rd;
reg [1:0] reg_num;
reg [6:0] clk_cnt;
reg [3:0] bit_cnt;
reg [7:0] rx_data;
reg [15:0] tx_data;
parameter reg0 = 2'd0;
parameter reg1 = 2'd1;
parameter reg2 = 2'd2;
parameter reg3 = 2'd3;
parameter reg0_address = 7'b0000000; //address of reg0
parameter reg1_address = 7'b0000001; //address of reg1
parameter reg2_address = 7'b0000010; //address of reg2
parameter reg3_address = 7'b0000011; //address of reg3
//cs_n
assign cs_n = cs_n_r;
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
cs_n_r <= 1'b1;
else if(send_start)
cs_n_r <= 1'b0;
else if(data_out_vld_r)
cs_n_r <= 1'b1;
end
//clk_cnt
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
clk_cnt <= 7'b0;
else if(cs_n_r)
clk_cnt <= 7'b0;
else if(clk_cnt == 7'd39)
clk_cnt <= 7'b0;
else
clk_cnt <= clk_cnt + 1'b1;
end
//bit_cnt
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
bit_cnt <= 4'b0;
else if(clk_cnt == 7'd39)
bit_cnt <= bit_cnt + 1'b1;
end
//sclk
assign sclk = sclk_r;
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
sclk_r <= 1'b0;
else if(clk_cnt == 7'd19 || clk_cnt == 7'd39)
sclk_r <= ~sclk_r;
end
//wr_rd
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
wr_rd <= 1'b0;
else if(cs_n_r)
wr_rd <= 1'b0;
else if(reg_num == reg3 && bit_cnt == 4'd15 && clk_cnt == 7'd39)
wr_rd <= 1'b1;
end
//send
//reg_num
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
reg_num <= reg0;
else if(cs_n_r)
reg_num <= reg0;
else if(bit_cnt == 4'd15 && clk_cnt == 7'd39)
case(reg_num)
reg0:reg_num <= reg1;
reg1:reg_num <= reg2;
reg2:reg_num <= reg3;
reg3:reg_num <= reg0;
endcase
end
//tx_data
always @ (posedge sclk_r or negedge rst_n)
begin
if(~rst_n)
tx_data <= 16'b0;
else if(wr_rd && bit_cnt == 4'd0)
tx_data <= {1'b1,reg2_address,8'b0};
else if(~wr_rd && bit_cnt == 4'd0)
case(reg_num)
reg0:tx_data <= {1'b0,reg0_address,data_in};
reg1:tx_data <= {1'b0,reg1_address,data_in[1:0],data_in[7:2]};
reg2:tx_data <= {1'b0,reg2_address,data_in[3:0],data_in[7:4]};
reg3:tx_data <= {1'b0,reg3_address,data_in[5:0],data_in[7:6]};
endcase
else
tx_data <= {tx_data[14:0],tx_data[15]};
end
//mosi
assign mosi = tx_data[15];
//recieve
//rx_data
always @ (posedge sclk_r or negedge rst_n)
begin
if(~rst_n)
rx_data <= 8'b0;
else if(wr_rd)
rx_data <= {rx_data[6:0],miso};
end
//data_out_vld
assign data_out_vld = data_out_vld_r;
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
data_out_vld_r <= 1'b0;
else if(wr_rd && bit_cnt == 4'd15 && clk_cnt == 7'd39)
data_out_vld_r <= 1'b1;
else
data_out_vld_r <= 1'b0;
end
//data_out
assign data_out = data_out_r;
always @ (posedge clk_40k or negedge rst_n)
begin
if(~rst_n)
data_out_r <= 8'b0;
else if(wr_rd && bit_cnt == 4'd15 && clk_cnt == 7'd39)
data_out_r <= rx_data;
end
endmodule
module spi_slave(
input rst_n,
input cs_n,
input sclk,
input mosi,
output miso,
output [7:0] reg0_out,
output [7:0] reg1_out,
output [7:0] reg2_out,
output [7:0] reg3_out
);
reg miso_r;
reg [7:0] reg1_out_r;
reg [7:0] reg2_out_r;
reg [7:0] reg3_out_r;
reg [7:0] reg0_out_r;
reg start;
reg wr_rd;
reg [3:0] bit_cnt;
reg [6:0] reg_addr;
reg [7:0] reg_data;
parameter reg0_address = 7'b0000000; //address of reg0
parameter reg1_address = 7'b0000001; //address of reg1
parameter reg2_address = 7'b0000010; //address of reg2
parameter reg3_address = 7'b0000011; //address of reg3
//start
always @ (posedge sclk or negedge rst_n)
begin
if(~rst_n)
start <= 1'b0;
else
start <= 1'b1;
end
//bit_cnt
always @ (posedge sclk or negedge rst_n)
begin
if(~rst_n)
bit_cnt <= 4'b0;
else if(start)
bit_cnt <= bit_cnt + 1'b1;
end
//wr_rd
always @ (posedge sclk or negedge rst_n)
begin
if(~rst_n)
wr_rd <= 1'b0;
else if(bit_cnt == 4'b0 && mosi == 1'b0)
wr_rd <= 1'b0;
else if(bit_cnt == 4'b0 && mosi == 1'b1)
wr_rd <= 1'b1;
end
//reg_addr
always @ (negedge sclk or negedge rst_n)
begin
if(~rst_n)
reg_addr <= 7'b0;
else if(bit_cnt >= 4'd1 && bit_cnt <= 4'd7)
reg_addr <= {reg_addr[5:0],mosi};
end
//reg_data
always @ (posedge sclk or negedge rst_n)
begin
if(~rst_n)
reg_data <= 8'b0;
else if(!wr_rd && bit_cnt >= 4'd7)
reg_data <= {reg_data[6:0],mosi};
else if(wr_rd && bit_cnt == 4'd6)
reg_data <= reg2_out_r;
else if(wr_rd && bit_cnt >= 4'd7)
reg_data <= {reg_data[6:0],reg_data[7]};
end
//reg_out
assign reg0_out = reg0_out_r;
assign reg1_out = reg1_out_r;
assign reg2_out = reg2_out_r;
assign reg3_out = reg3_out_r;
always @ (negedge sclk or negedge rst_n)
begin
if(~rst_n)
begin
reg0_out_r <= 8'b0;
reg1_out_r <= 8'b0;
reg2_out_r <= 8'b0;
reg3_out_r <= 8'b0;
end
else if(!wr_rd && bit_cnt == 4'd0)
case(reg_addr)
reg0_address: reg0_out_r <= reg_data;
reg1_address: reg1_out_r <= reg_data;
reg2_address: reg2_out_r <= reg_data;
reg3_address: reg3_out_r <= reg_data;
endcase
end
//miso
assign miso = miso_r;
always @ (negedge sclk or posedge rst_n)
begin
if(~rst_n)
miso_r <= 1'b0;
else if(wr_rd && bit_cnt >= 4'd7)
miso_r <= reg_data[7];
end
endmodule
3. Testbench(tb):
`timescale 1us/1us
module tb();
regclk_40k;
regrst_n;
reg [7:0] data_in;
regsend_start;
wiresclk;
wirecs_n;
wiremosi;
wiremiso;
wire [7:0] data_out;
wire data_out_vld;
wire [7:0] reg0_out;
wire [7:0] reg1_out;
wire [7:0] reg2_out;
wire [7:0] reg3_out;
spi_master i_spi_master(
.clk_40k (clk_40k),
.rst_n (rst_n),
.data_in (data_in),
.send_start (send_start),
.sclk (sclk),
.cs_n (cs_n),
.mosi (mosi),
.miso (miso),
.data_out (data_out),
.data_out_vld (data_out_vld)
);
spi_slave i_spi_slave(
.rst_n (rst_n),
.cs_n (cs_n),
.sclk (sclk),
.mosi (mosi),
.miso (miso),
.reg0_out (reg0_out),
.reg1_out (reg1_out),
.reg2_out (reg2_out),
.reg3_out (reg3_out)
);
initial
begin
rst_n = 1'b0;
#10rst_n = 1'b1;
end
initial
begin
clk_40k = 1'b0;
forever
#1clk_40k = ~clk_40k;
end
initial
begin
send_start = 1'b0;
data_in = 8'd0;
forever
begin
#200;
data_in = $random()%256;
send_start = 1'b1;
#2
send_start = 1'b0;
#8000;
end
end
endmodule
2. 复位信号rst_n低电平有效,正常传输时始终处于高电平;
3. 开始传输时send_start信号拉高,传输结束时data_out_vld信号拉高;
4. SPI主设备将输入数据data_in并行转mosi串行输出,SPI从设备将接收到的串行存入数据,将移位后的数据data_out并行转miso串行输出。
2. 允许数据逐位传递;
3. 允许数据传输暂停;
4. 硬件结构简单,不需要精密时钟;
5. 从机不需要唯一地址,也不需要收发器。
2. 支持传输距离较短;
3. 硬件层面没有定义校错协议和从机应答信号。
没有回复内容