串行外围设备接口(SPI)是微控制器和外围IC(移位寄存器、SRAM等)之间广泛使用的接口。SPI是一种同步、全双工、主从式接口
。来自主机或者从机的数据在clk上升沿或下降沿同步,主机和从机可以通过MOSI、MISO线路同时传输数据
。SPI接口可以是3线式(SCLK、CS、DIO)或者4线式(SCLK、CS、MOSI、MISO)。
全双工:接口可以同时接收和发送数据(双倍速率),与iic相比,支持更高的时钟频率,SCLK范围在0.8-3.2Mhz之间。
半双工:接口任意时刻只能接收或者发送数据。
四根线的解释:
SCLK:由主机产生的时钟信号线,
.
CS:片选信号(低有效),该信号用来选择从机;
.
数据线:DIO表示主从机之间传输的数据线;MOSI表示数据从主机到从机,MISO反之。
.
当主机提供多个单独片选CS信号时,即可达到一主多从的效果,如下所示:
CPOL(时钟极性)和CPHA(时钟相位)来共同控制SPI的通信方式。其中:
CPOL决定SPI总线空闲时SCLK的电平;(0:空闲状态时SCLK低电平,1:空闲状态时SCLK高电平)
.
CPHA决定数据是上升沿还是下降沿采样;【0:第一个沿采样,1:第二个沿采样】
mode0:CPOL= 0,CPHA=0。SCLK串行时钟线空闲时为低电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
mode1:CPOL= 0,CPHA=1。SCLK串行时钟线空闲时为低电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
mode2:CPOL= 1,CPHA=0。SCLK串行时钟线空闲时为高电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
mode3:CPOL= 1,CPHA=1。SCLK串行时钟线空闲时为高电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
以Mode0为例:
当CS信号低电平时MOSI、MISO信号线有效,于是在SCK的每个时钟周期传输一位的数据。
1时刻:CS由高变低,为SPI通信的起始标志,对应从机被选中;
6时刻:CS由低变高,通信结束标志,对应从机选中取消;
2时刻(偶数时刻):数据在上升沿采样,此时位于数据中间位置,最稳定。
3时刻(奇数时刻):数据在下降沿切换,便于在下一上升沿的时候数据能正确被采集。
总的来说,这种方式能保证数据被正常采集到,也符合触发器建立和保持时间的要求,数据在上升沿之前提前到达以及在上升沿之后继续保持一段时间。
SPI每次输出传输以8或者16位为单位,每次传输的单位数不受限制。(UART为8位)
.
注意:数据传输的时候,MSB和LSB先行均可,但要保证两个SPI通信设备之间一致,一般采用MSB先行的方式。
现进行SPI与ADC进行通信,主要的接口如下:
其中Channel用来表示ADC的8个通道,这里无需过多关注,重点完成SPI驱动控制的时序设计。根据控制模块来生成对应的SPI信号,从而实现SPI与ADC的通信。
根据下面的时序图,来完成SPI控制器设计。
重点:如果理解计数器的产生?
由于数据的采样和切换是在上升沿、下降沿进行的,因此我们来产生SCLK_cnt计数器,用来计数SCLK的边沿,此外每一个边沿产生一个cnt_flag信号,简单理解就是每半个周期产生一个脉冲cnt_flag。SCLK的范围是0.8-3.2Mhz,因此我们取该范围内,若为2.5MHZ,因此一个SCLK周期是400ns,那么半个周期是200ns,由于系统时钟50Mhz,因此200ns / 20ns =10,每计数10次,表示一个cnt_flag脉冲信号。
在进行具体协议代码编写的时候,一般有状态机和线性序列机两种方式,分情况来使用。
比如IIC,有着多种开始、器件地址传输以及应答等状态,那么需要用状态机来设计,而SPI、UART即可用线性序列机,状态少且能用计数器来完成,下面代码就是用线性序列机完成SPI的方法。
module spi_drive(
input clk,
input rst_n,
input start,
input [2:0] Channel,
input DOUT,
output reg DIN,
output reg CS_N,
output reg SCLK,
output reg [11:0] data,
output reg done
);
reg en;
reg [2:0] r_channel;
reg [3:0] cnt;
reg cnt_flag;
reg [5:0] SCLK_CNT; //假设共有33个SCLK
reg [11:0] r_data;
//转换使能信号(计数器的累计使能等)
always @ (posedge clk or negedge rst_n)
if(!rst_n)
en <= 1'b0;
else if(start)
en <= 1'b1;
else if(done)
en <= 1'b0;
else
en <= en;
// r_channel
always @ (posedge clk or negedge rst_n)
if(!rst_n)
r_channel <= 1'd0;
else if(start)
r_channel <= Channel;
else
r_channel <= r_channel;
//产生时序图,SCLK 0.8 - 3.2Mhz,取SCLK=2.5Mhz
//cnt
always @ (posedge clk or negedge rst_n)
if(!rst_n)
cnt <= 4'd0;
else if(en) begin
if(cnt == 4'd9)
cnt <= 4'd0;
else
cnt <= cnt + 1'b1;
end
else
cnt <= 4'd0;
//产生cnt_flag
always @ (posedge clk or negedge rst_n)
if(!rst_n)
cnt_flag <= 0;
else if(cnt == 4'd9)
cnt_flag <= 1;
else
cnt_flag <= 0;
//产生SCLK_CNT
always @ (posedge clk or negedge rst_n)
if(!rst_n)
SCLK_CNT <= 6'd0;
else if(en) begin
if(SCLK_CNT == 6'd33)
SCLK_CNT <= 6'd0;
else if(cnt_flag)
SCLK_CNT <= SCLK_CNT + 1'b1;
else
SCLK_CNT <= SCLK_CNT ;
end
else
SCLK_CNT <= 6'd0;
//线性序列机
always @ (posedge clk or negedge rst_n)
if(!rst_n)begin
DIN <= 1'b1;
CS_N <= 1'b1; //低电平有效
SCLK <= 1'b1;
end
else if(en)begin
case(SCLK_CNT)
6'd0:begin CS_N <= 1'b0; end
6'd1:begin SCLK <= 1'b0; DIN <= 1'b0;end
6'd2:begin SCLK <= 1'b1; end
6'd3:begin SCLK <= 1'b0; end
6'd4:begin SCLK <= 1'b1; end
6'd5:begin SCLK <= 1'b0; DIN <= r_channel[2];end
6'd6:begin SCLK <= 1'b1; end
6'd7:begin SCLK <= 1'b0; DIN <= r_channel[1];end
6'd8:begin SCLK <= 1'b1; end
6'd9:begin SCLK <= 1'b0; DIN <= r_channel[0];end
6'd10,6'd12,6'd14,6'd16,6'd18,6'd20,6'd22,6'd24,6'd26,6'd28,6'd30,6'd32:
begin SCLK <= 1'b1; r_data <= {r_data[10:0],DOUT};end //上升沿采样,高电平移位操作
6'd11,6'd13,6'd15,6'd17,6'd19,6'd21,6'd23,6'd25,6'd27,6'd29,6'd31:
begin SCLK <= 1'b0;end //低电平期间数据保持
6'd33:begin CS_N <= 1'b1; end
default:begin CS_N <= 1'b1; end
endcase
end
else begin
DIN <= 1'b1;
CS_N <= 1'b1; //低电平有效
SCLK <= 1'b1;
end
//done信号
always @ (posedge clk or negedge rst_n)
if(!rst_n)
done <= 1'b0;
else if(SCLK_CNT == 6'd33)
done <= 1'b1;
else
done <= 1'b0;
//data信号
always @ (posedge clk or negedge rst_n)
if(!rst_n)
data <= 1'b0;
else if(SCLK_CNT == 6'd33)
data <= r_data;
else
data <= data;
endmodule
不是很会写tb测试文件,但也能分析分析,凑合看下吧~或者也可以用其他人写的tb文件来测试
`timescale 1ns/1ns
module spi_drive_tb;
reg clk;
reg rst_n;
reg start;
reg [2:0] Channel;
reg DOUT;
wire DIN;
wire CS_N;
wire SCLK;
wire [11:0] data;
wire done;
spi_drive spi_drive(
.clk(clk),
.rst_n(rst_n),
.start(start),
.Channel(Channel),
.DOUT(DOUT),
.DIN(DIN),
.CS_N(CS_N),
.SCLK(SCLK),
.data(data),
.done(done)
);
//产生50Mhz的时钟频率
initial clk = 1'b1;
always #10 clk = ~clk;
initial begin
rst_n = 1'b0;
Channel = 0;
start = 0;
DOUT = 0;
#100;
rst_n = 1'b1;
#10;
start = 1;
#100;
Channel = 3;
#100;
DOUT = 1;
#10;
DOUT = 0;
#50;
DOUT = 1;
#40;
DOUT = 0;
#50;
DOUT = 1;
#10;
DOUT = 1;
#50000;
$stop;
end
endmodule
重点观察所设计的几个信号是否正确。
en:当start信号为高电平的时候,en信号拉高,直到done信号出现,en才变成低电平。
cnt_flag:每cnt_flag出现一个高脉冲,则SCLK_CNT计数器加1。
SCLK:从仿真中可看到,SCLK的一个时钟周期刚好为400ns。同时其高低变化和verilog代码中描述的一致,计数为奇数的时候为低电平,计数为偶数的时候为高电平。
DIN,CS_N信号跟SCLK_CNT的计数值相关。复位的时候二者都是高电平。然后
当SCLK_CNT=0的时候,CS_N低电平,DIN高电平;
当SCLK_CNT=1的时候,CS_N低电平,DIN低电平……都是根据SPI时序或者说verilog处的描述来的。
done:由于这里提前设定传输33个数据拉高,因此计数到33的时候,done拉高.
学习博文
学习视频,讲真的很好理解