赞
踩
前面《详解UART通信协议以及FPGA实现》《UART自适应任意(典型)波特率原理以及FPGA实现》我们实现了UART通信以及自适应任意波特率UART的通信,常见的通信协议还有SPI和IIC。其中SPI 的接口速度可以最高到上百兆,采用 SPI 接口的设备一般兼顾低速通信和上百兆的高速通信。
SPI(Serial Peripheral Interface)是一种同步,串行,全双工的通信接口,常用于在微控制器、传感器、存储器和其他外部设备之间进行数据交换。例如: EEPROM、RTC(实时时钟)、ADC(模数转换器)、DAC(数模转换器)、LCD、音频 IC、温度和压力等传感器,MMC 或 SD 卡等存储卡等等
SPI是一个同步的数据总线,由上文《》可以知道,同步通信需要随路时钟,也就是说SPI是用单独的数据线和一个单独的时钟信号来保证发送端和接收端的完美同步。通信结构如下图所示:
在SPI中,主机需要配置时钟极性和时钟相位。
SPI模块 | CPOL | CPHA | 空闲状态时钟极性 | 采样和发送数据的时钟相位 |
0 | 0 | 0 | 低电平 | 数据在第1个沿采样,在第2个沿发送 |
1 | 0 | 1 | 低电平 | 数据在第2个沿采样,在第1个沿发送 |
2 | 1 | 0 | 高电平 | 数据在第1个沿采样,在第2个沿发送 |
3 | 1 | 1 | 高电平 | 数据在第2个沿采样,在第1个沿发送 |
2. SPI模式1,CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在上升沿发送,并在下降沿采样。
`timescale 1ns/1ns
module spi_master_tx#
(
parameter SYS_CLK_FREQ = 50000000, //输入时钟频率50M
parameter SPI_CLK_FREQ = 500000, //输出spi时钟500K
parameter CPOL = 1'b0, //时钟极性设置为0
parameter CPHA = 1'b0 //时钟相位设置为0
)
(
input sys_clk , //输入系统时钟
input rst_n , //系统复位
input spi_tx_req , //待发送数据请求
input [7:0] spi_tx_data , //待发送数据
output reg spi_cs , //cs片选信号
output spi_clk , //主机发送的spi时钟
output spi_busy, //spi繁忙信号
output spi_mosi //mosi
);
localparam [9:0] spi_clk_cnt_max = SYS_CLK_FREQ / SPI_CLK_FREQ; //一个spi时钟需要计数的最大值
localparam [9:0] spi_clk_cnt_max_div2 = spi_clk_cnt_max/2; //半个时钟周期需要计数的最大值
reg [9:0] clk_div_cnt ; //spi时钟计数器
reg spi_en ; //spi发送使能
reg [7:0] spi_tx_data_reg ; //缓存待发送的数据
reg clk1_en ; //第一时钟边沿
reg clk2_en ; //第二时钟边沿
reg spi_clk_temp ; //spi时钟
reg [3:0] tx_cnt ; //发送的bit计数器
reg spi_strobe_en ; //spi发送数据使能有效范围
wire strobe ; //spi发送数据使能信号
//如果时钟极性=1,则取反时钟信号
assign spi_clk = (CPOL == 1'b1)? ~spi_clk_temp : spi_clk_temp;
//先发送高位数据
assign spi_mosi = spi_tx_data_reg[7];
//如果时钟相位=1,则采样数据才第二时钟沿,发送数据在第一时钟沿
assign strobe = (CPHA == 1'b1)? clk1_en&spi_strobe_en : clk2_en&spi_strobe_en;
//spi繁忙信号就=spi发送使能信号
assign spi_busy = spi_en;
//等待上游给出发送请求和发送数据。发送完成8个bit,则本次发送完成。
always @(posedge sys_clk or negedge rst_n) begin
if((rst_n == 1'b0) || (clk1_en == 1'b1 && tx_cnt == 4'd8)) begin
spi_en <= 1'b0;
spi_tx_data_reg <= 'd0;
end
else if((spi_tx_req == 1'b1) && (spi_en == 1'b0))begin
spi_en <= 1'b1;
spi_tx_data_reg <= spi_tx_data;
end
else if(spi_en == 1'b1 && strobe)
spi_tx_data_reg <= {spi_tx_data_reg[6:0],1'b0};
else
spi_tx_data_reg <= spi_tx_data_reg;
end
//发送开始前,拉低cs信号
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
spi_cs <= 1'b1;
else if(spi_en == 1'b1)
spi_cs <= 1'b0;
else
spi_cs <= 1'b1;
end
//当时钟计数器计数到一半时,拉高第一时钟沿信号,计数完成一个时钟周期时,拉高第二时钟信号
always @(posedge sys_clk or negedge rst_n) begin
if((rst_n == 1'b0) || (spi_cs == 1'b1))begin
clk_div_cnt <= 0;
clk1_en <= 1'b0;
clk2_en <= 1'b0;
end
else if(clk_div_cnt == spi_clk_cnt_max - 1)begin
clk_div_cnt <= 0;
clk2_en <= 1'b1;
end
else if(clk_div_cnt == spi_clk_cnt_max_div2 - 1)begin
clk1_en <= 1'b1;
clk_div_cnt <= clk_div_cnt + 1'b1;
end
else begin
clk_div_cnt <= clk_div_cnt + 1'b1;
clk1_en <= 1'b0;
clk2_en <= 1'b0;
end
end
//每次第一时钟沿上来,则表示发送完一个数
always @(posedge sys_clk or negedge rst_n) begin
if((rst_n == 1'b0)||(spi_en == 1'b0))
tx_cnt <= 'd0;
else if(clk1_en == 1'b1)
tx_cnt <= tx_cnt + 1'b1;
else
tx_cnt <= tx_cnt;
end
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
spi_strobe_en <= 1'b0;
else if(tx_cnt < 4'd8)
if(clk1_en == 1'b1 )
spi_strobe_en <= 1'b1;
else
spi_strobe_en <= spi_strobe_en;
else
spi_strobe_en <= 1'b0;
end
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
spi_clk_temp <= 1'b0;
else if(clk2_en == 1'b1)
spi_clk_temp <= 1'b0;
else if((clk1_en == 1'b1)&&(tx_cnt <4'd8))
spi_clk_temp <= 1'b1;
else
spi_clk_temp <= spi_clk_temp;
end
endmodule
简单设置发送数据控制模块,发送数据从1开始累加,代码如下:
`timescale 1ns / 1ps
module spi_top
(
input sys_clk , //输入时钟
input rst_n , //系统复位
output spi_clk , //SPI发送时钟
output spi_mosi //SPI发送数据
);
wire spi_cs ;
wire spi_busy ; //SPI忙信号
reg spi_tx_req ; //SPI发送req信号,有发送需求时拉高
reg [7:0] spi_tx_data ; //待发送数据存储
reg [1:0] state ; //状态机
//spi send state machine
always @(posedge sys_clk) begin
if(!rst_n) begin
spi_tx_req <= 1'b0;
spi_tx_data <= 8'd0;
state <= 2'd0;
end
else begin
case(state)
0:if(!spi_busy)begin //总线不忙启动传输
spi_tx_req <= 1'b1; //req信号拉高,开始传输
spi_tx_data <= spi_tx_data + 1'b1; //测试数据
state <= 2'd1;
end
1:if(spi_busy)begin //如果spi总线忙,清除spi_tx_req
spi_tx_req <= 1'b0;
state <= 2'd0;
end
default:state <= 2'd0;
endcase
end
end
//例化SPI Master发送驱动器
spi_master_tx#(
.SYS_CLK_FREQ ( 50000000 ),
.SPI_CLK_FREQ ( 500000 ),
.CPOL ( 1'b0 ),
.CPHA ( 1'b0 )
)u_spi_master_tx(
.sys_clk ( sys_clk ),
.rst_n ( rst_n ),
.spi_tx_req ( spi_tx_req ),
.spi_tx_data ( spi_tx_data ),
.spi_cs ( spi_cs ),
.spi_clk ( spi_clk ),
.spi_busy ( spi_busy ),
.spi_mosi ( spi_mosi )
);
endmodule
tb代码如下:
`timescale 1ns / 1ps
module master_spi_tb;
reg sys_clk; //系统时钟
reg rst_n;
wire spi_clk;
wire spi_mosi;
spi_top u_spi_top(
.sys_clk(sys_clk),
.rst_n(rst_n),
.spi_clk(spi_clk),
.spi_mosi(spi_mosi)
);
initial begin
sys_clk = 1'b0; //设置时钟基础值
rst_n = 1'b0; //低电平复位
#100;
rst_n = 1'b1; //复位释放
#2000000 $finish;
end
always #10 sys_clk = ~sys_clk; //产生主时钟
endmodule
设置CPOL=0,CPHA=0观看仿真结果,我们随便选取一个数据,例如发送8‘d37,结果如下:
8’d37的二进制为00100101,所以采集数据在时钟上升沿,采集到的数据也为00100101,结果正确。
现在修改CPOL=0,CPHA=1观看同样的结果,如下:
采集数据在时钟下升沿,采集到的数据也为00100101,结果正确。
`timescale 1ns / 1ns
module spi_slave_rx#
(
parameter BITS_LEN = 8, //设置接收bit长度
parameter CPOL = 1'b0, //时钟极性
parameter CPHA = 1'b1 //时钟相位
)
(
input sys_clk ,
input rst_n ,
input spi_cs , //spi片选信号
input spi_clk , //spi_clk
input spi_mosi , //spi_mosi
output reg rx_data_valid , //接收到的数据有效信号
output reg [BITS_LEN - 1:0] rx_data //接收到的信号
);
reg [3:0] spi_cs_reg ; //打四拍
reg [3:0] spi_clk_reg ; //打四拍
reg [3:0] spi_mosi_reg ; //打四拍
reg cap ; //采集时刻信号
reg spi_clk_pos ; //上升沿
reg spi_clk_neg ; //下降沿
wire rx_en ; //接收使能信号
reg [4:0] rx_bit_cnt ; //接收bit计数器
assign rx_en = (~spi_cs_reg[3]);
//将 cs,spi_clk,mosi信号都打三拍消除亚稳态
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
spi_cs_reg <= 3'd0;
else
spi_cs_reg <= {spi_cs_reg[2:0],spi_cs};
end
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
spi_clk_reg <= 3'd0;
else
spi_clk_reg <= {spi_clk_reg[2:0],spi_clk};
end
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
spi_mosi_reg <= 3'd0;
else
spi_mosi_reg <= {spi_mosi_reg[2:0],spi_mosi};
end
//spi_clk上升沿
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
spi_clk_pos <= 1'b0;
else if (spi_clk_reg[2] == 1'b0 && spi_clk_reg[1] == 1'b1)
spi_clk_pos <= 1'b1;
else
spi_clk_pos <= 1'b0;
end
//spi_clk下降沿
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
spi_clk_neg <= 1'b0;
else if (spi_clk_reg[2] == 1'b1 && spi_clk_reg[1] == 1'b0)
spi_clk_neg <= 1'b1;
else
spi_clk_neg <= 1'b0;
end
//根据CPOL CPHA来确定采集信号位置
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
cap <= 1'b0;
else if(CPOL == 1'b0 && CPHA == 1'b0)
cap <= spi_clk_pos;
else if(CPOL == 1'b0 && CPHA == 1'b1)
cap <= spi_clk_neg;
else if(CPOL == 1'b1 && CPHA == 1'b0)
cap <= spi_clk_neg;
else if(CPOL == 1'b1 && CPHA == 1'b1)
cap <= spi_clk_pos;
else
cap <= 1'b0;
end
//每次采集一次,接收bit计数器累加一次
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)begin
rx_bit_cnt <= 'd0;
rx_data_valid <= 1'b0;
end
else if((rx_en == 1'b1) && (cap == 1'b1) && (rx_bit_cnt < BITS_LEN))begin
rx_bit_cnt <= rx_bit_cnt +1'b1;
rx_data_valid <= 1'b0;
end
else if((rx_en == 1'b0) || (rx_bit_cnt == BITS_LEN))begin
rx_bit_cnt <= 'd0;
rx_data_valid <= 1'b1;
end
else begin
rx_bit_cnt <= rx_bit_cnt;
rx_data_valid <= 1'b0;
end
end
//每次采集时刻到来,进行串并转换
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
rx_data <= 'd0;
else if(rx_en == 1'b1 && cap == 1'b1)
rx_data <= {rx_data[BITS_LEN -2 : 0],spi_mosi_reg[3]};
else if(rx_en == 1'b0)
rx_data <= 'd0;
else
rx_data <= rx_data;
end
endmodule
编写tb文件如下:
`timescale 1ns / 1ps
module tb_spi_slave_rtx();
localparam BYTES = 8;
localparam TCNT = BYTES*8*2-1;
localparam CPOL = 1;
localparam CPHA = 1;
reg I_clk; //系统时钟
reg [7:0] i;//计数器,用于产生SPI时钟数量
reg I_rstn; //系统复位
reg I_spi_clk;//SPI时钟
reg I_spi_ss; //SPI的Slave选通信号
reg [3:0]bit_cnt; //bit计数器
reg [7:0]spi_tx_buf; //发送缓存(移位寄存器)
reg [7:0]spi_tx_buf_r; //发送化缓存,用于产生测试数据
reg first_data_flag; //是否一个时钟改变数据
wire O_spi_rvalid; //SPI 数据接收有效,当该信号有效代表接收到一个有效数据
wire [7:0]O_spi_rdata; //SPI读数据
wire I_spi_rx;//SPI数据总线
//tb模拟的SPI测试数据接到I_spi_rx
assign I_spi_rx = spi_tx_buf[7];
//例化SPI 接收模块
spi_slave_rx#(
.BITS_LEN ( 8 ),
.CPOL ( CPOL ),
.CPHA ( CPHA )
)u_spi_slave_rx(
.sys_clk ( I_clk ),
.rst_n ( I_rstn ),
.spi_cs ( I_spi_ss ),
.spi_clk ( I_spi_clk ),
.spi_mosi ( I_spi_rx ),
.rx_data_valid ( O_spi_rvalid ),
.rx_data ( O_spi_rdata )
);
initial begin
I_clk = 1'b0;
I_rstn = 1'b0;
#100;
I_rstn = 1'b1;
end
always #10 I_clk = ~I_clk; //时钟信号翻转,产生系统时钟
initial begin
#100;
i = 0;
forever begin
I_spi_clk = CPOL; //设置时钟极性
I_spi_ss = 1; // 设置SPI的SS控制信号
#2000;
I_spi_ss = 0;
for(i=0;i<TCNT;i=i+1) #1000 I_spi_clk = ~ I_spi_clk; //产生SPI时钟
#2000;
I_spi_ss = 1;
end
end
initial begin
#100;
bit_cnt = 0;
first_data_flag =0;
spi_tx_buf[7:0] = 8'ha0;
spi_tx_buf_r[7:0] = 8'ha0;
forever begin
//spi ss 控件用于启用传输
wait(I_spi_ss);//spi ss
bit_cnt = 0;
spi_tx_buf[7:0] = 8'ha0;
spi_tx_buf_r[7:0] = 8'ha0;
if((CPHA == 1 && CPOL ==0)||(CPHA == 1 && CPOL ==1))//第一个时钟沿改变数据的情况
first_data_flag = 1; //设置first_data_flag=1 下面的发送时序对应情况跳过第一个沿
//ss低时开始数据传输
wait(!I_spi_ss);
while(!I_spi_ss)begin
//COPL=0 CPHA=0默认SCLK为低电平,对于发送方,在对于第1个bit数据提前放到总线
if(CPHA == 0 && CPOL ==0)begin
@(negedge I_spi_clk) begin //每个时钟的下降沿更新需要发送的BIT
if(bit_cnt == 7)begin//连续发送过程中,8bits 发送完毕后更新数据
bit_cnt = 0;
spi_tx_buf_r = spi_tx_buf_r + 1'b1;//产生新的测试数据
spi_tx_buf = spi_tx_buf_r;//重新跟新发送寄存器
end
else begin
spi_tx_buf = {spi_tx_buf[6:0],1'b0};//数据移位,更新数据
bit_cnt = bit_cnt + 1'b1;//SPI发送位数计数器
end
end
end
//CPHA=0 COPL=1 默认SCLK为高电平,对于发送方,在对于第1个bit数据提前放到总线
if(CPHA == 0 && CPOL ==1)begin
@(posedge I_spi_clk) begin //每个时钟的上升沿更新需要发送的BIT
if(bit_cnt == 7)begin //连续发送过程中,8bits 发送完毕后更新数据
bit_cnt = 0;
spi_tx_buf_r = spi_tx_buf_r + 1'b1;//产生新的测试数据
spi_tx_buf = spi_tx_buf_r; //重新跟新发送寄存器
end
else begin
spi_tx_buf = {spi_tx_buf[6:0],1'b0};//数据移位,更新数据
bit_cnt = bit_cnt + 1'b1;//SPI发送位数计数器
end
end
end
//CPHA=1 COPL=0 默认SCLK为低电平,对于发送方,在第1个SCLK的跳变沿更新
if(CPHA == 1 && CPOL ==0)begin
@(posedge I_spi_clk) begin
if(first_data_flag == 1'b1)begin //第一个时钟沿,由于前面已经提前初始化第一个需要发送的数据,因此,这里跳过第一个跳变沿沿
first_data_flag = 0;
//spi_tx_buf[7:0] = 8'ha0;//也可以在第一个跳变沿初始化第一个发送的数据
end
else begin
if(bit_cnt == 7)begin
bit_cnt = 0;
spi_tx_buf_r = spi_tx_buf_r + 1'b1;//产生新的测试数据
spi_tx_buf = spi_tx_buf_r;//重新跟新发送寄存器
end
else begin
spi_tx_buf = {spi_tx_buf[6:0],1'b0}; //数据移位,更新数据
bit_cnt = bit_cnt + 1'b1;//SPI发送位数计数器
end
end
end
end
//CPHA=1 COPL=1 默认SCLK为高电平,对于发送方,在第1个SCLK的跳变沿更新
if(CPHA == 1 && CPOL ==1)begin
@(negedge I_spi_clk) begin
if(first_data_flag == 1'b1)begin //第一个时钟沿,由于前面已经提前初始化第一个需要发送的数据,因此,这里跳过第一个跳变沿沿
first_data_flag = 0;
//spi_tx_buf[7:0] = 8'ha0;//也可以在第一个跳变沿初始化第一个发送的数据
end
else begin
if(bit_cnt == 7)begin
bit_cnt = 0;
spi_tx_buf_r = spi_tx_buf_r + 1'b1;//产生新的测试数据
spi_tx_buf = spi_tx_buf_r;//重新跟新发送寄存器
end
else begin
spi_tx_buf = {spi_tx_buf[6:0],1'b0};//数据移位,更新数据
bit_cnt = bit_cnt + 1'b1;//SPI发送位数计数器
end
end
end
end
end
end
end
endmodule
测试结果如下图所示,上游发送数据a0,接收模块接收到的也是a0 。
将spi发送端和spi接收端接起来,通过顶层模拟发送数据。
顶层代码:
`timescale 1ns / 1ps
module spi_top
(
input sys_clk , //输入时钟
input rst_n , //系统复位
input spi_clk_in ,
input spi_miso ,
output spi_clk , //SPI发送时钟
output spi_mosi //SPI发送数据
);
reg spi_cs ;
wire spi_busy ; //SPI忙信号
reg spi_tx_req ; //SPI发送req信号,有发送需求时拉高
reg [7:0] spi_tx_data ; //待发送数据存储
reg [1:0] state ; //状态机
reg [10:0] delay_cnt ;
wire rx_data_valid ;
wire [7:0] rx_data ;
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)
delay_cnt <= 'd0;
else if(delay_cnt[10] == 1'b1)
delay_cnt <= 'd0;
else
delay_cnt <= delay_cnt +1'b1;
end
//spi send state machine
always @(posedge sys_clk) begin
if(!rst_n) begin
spi_tx_req <= 1'b0;
spi_tx_data <= 8'd0;
state <= 2'd0;
spi_cs <= 1'b1;
end
else begin
case(state)
0:if((delay_cnt[10]==1'b1) &&(spi_busy == 1'b0))begin
spi_cs <= 1'b1;
state <= 2'd1;
end
1:if((delay_cnt[10]==1'b1) &&(spi_busy == 1'b0))begin
spi_cs <= 1'b0;
state <= 2'd2;
end
2:if((delay_cnt[10]==1'b1) &&(spi_busy == 1'b0))begin //总线不忙启动传输
spi_tx_req <= 1'b1; //req信号拉高,开始传输
spi_tx_data <= spi_tx_data + 1'b1; //测试数据
state <= 2'd3;
end
3:if(spi_busy)begin //如果spi总线忙,清除spi_tx_req
spi_tx_req <= 1'b0;
state <= 2'd0;
end
default:state <= 2'd0;
endcase
end
end
//例化SPI Master发送驱动器
spi_master_tx#(
.SYS_CLK_FREQ ( 50000000 ),
.SPI_CLK_FREQ ( 500000 ),
.CPOL ( 1'b0 ),
.CPHA ( 1'b1 )
)u_spi_master_tx(
.sys_clk ( sys_clk ),
.rst_n ( rst_n ),
.spi_tx_req ( spi_tx_req ),
.spi_tx_data ( spi_tx_data ),
.spi_cs ( ),
.spi_clk ( spi_clk ),
.spi_busy ( spi_busy ),
.spi_mosi ( spi_mosi )
);
spi_slave_rx#(
.BITS_LEN ( 8 ),
.CPOL ( 1'b0 ),
.CPHA ( 1'b1 )
)u_spi_slave_rx(
.sys_clk ( sys_clk ),
.rst_n ( rst_n ),
.spi_cs ( spi_cs ),
.spi_clk ( spi_clk_in ),
.spi_mosi ( spi_miso ),
.rx_data_valid ( rx_data_valid ),
.rx_data ( rx_data )
);
ila_0 u_ila (
.clk(sys_clk), // input wire clk
.probe0(rx_data_valid), // input wire [0:0] probe0
.probe1(rx_data) // input wire [7:0] probe1
);
endmodule
`timescale 1ns / 1ps
module master_spi_tb;
reg sys_clk; //系统时钟
reg rst_n;
wire spi_clk;
wire spi_mosi;
spi_top u_spi_top(
.sys_clk(sys_clk),
.rst_n(rst_n),
.spi_clk(spi_clk),
.spi_mosi(spi_mosi),
.spi_clk_in(spi_clk),
.spi_miso(spi_mosi)
);
initial begin
sys_clk = 1'b0; //设置时钟基础值
rst_n = 1'b0; //低电平复位
#100;
rst_n = 1'b1; //复位释放
#2000000 $finish;
end
always #10 sys_clk = ~sys_clk; //产生主时钟
endmodule
仿真结果如下:
顶层模拟发送累加数字给发送端,发送端再连接到接收端,数据接收一致。
我们例化一个ILA,抓取spi_rx模块里的rx_data和rx_data_valid信号,并且打开捕获模式。具体捕获模式原理以及怎么使用,请看Vivado ILA Capture Control 模式与 Advanced Trigger的功能使用以及TSM(触发状态机)的编写,配置图如下:
下载后,我们捕获模式设置BASIC,触发条件选择rx_data_valid。观察波形,发现是连续累加的数据,表面SPI回环成功。
《introduction-to-spi-interface》
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。