当我们理解了SPI的通讯协议后, 本文使用Verilog 语言来编写一个简单的SPI通讯模块,用这个模块展示一个完整的SPI通讯方式。
图1
SPI模块(Verilog 代码 :默认 CPOL = 0 CPHA = 0)
-
设计思路
设计思路由简单的SPI收发工程到完善的SPI工程。接下来,由下面的三个步骤进行说明。
- 设计一个SPI发送移位寄存器
- 设计完整的SPI接收,发送的工程
- 在完整的SPI收发工程上,包装了一个上层使用接口
-
SPI发送移位寄存器代码实现
实现简单的MOSI发送。
工程代码:
module test_spi( input clk, input ce, //片选 output spi_clk, //时钟 output reg mosi = 1, input miso, input reset ); reg [7:0] data = 8'hd3; //通过MOSI发送的数据,可以修改 always @ (posedge clk) if(~ce) //当片选为低时有效 data <= {data[7:0], 1'b1}; //由高位开始发送,之后每过一个时钟周期,数据移位 always @ (posedge clk) if(~ce) mosi <= data[7]; //与时钟同步发送数据 assign spi_clk = ~clk;
仿真文件:
`timescale 1ns / 1ps module test_spi_sim( ); reg reset = 0; reg clk = 0; always #5 clk = ~clk; reg ce = 1; initial begin ce = 1; #200; @(posedge clk); ce = 0; @(posedge clk); @(posedge clk); @(posedge clk); @(posedge clk); @(posedge clk); @(posedge clk); @(posedge clk); @(posedge clk); ce = 1; #400; $stop; end wire spi_clk; wire mosi; wire miso = mosi; test_spi test_spi_inst ( .clk (clk), .ce (ce), .spi_clk(spi_clk), .mosi (mosi), .miso (miso), .reset (reset) ); endmodule
仿真波形图如图2所示:
图2 仿真波形图
通过图2可以看出,数据8’hd3被正确发送。在片选ce为低的期间,在spi_clk的下降沿,mosi更换数据;同时,我们也发现, spi_clk 一直没有停止, miso 也没有接收的逻辑。
-
完整SPI收发器代码实现
在完整SPI收发器工程中,改进了上一个工程的问题。主要改进了在片选cs为高的时候,时钟不能翻转,以及增加了miso的接收逻辑。
工程代码:
module test_spi( input clk, input ce, output spi_clk, output reg mosi = 1, input miso, input reset ); reg [7:0] tx_data = 8'h6a; //通过MOSI发送的数据,可以修改 always @ (posedge clk) if(~ce) tx_data <= {tx_data[7:0], 1'b1}; always @ (posedge clk) if(~ce) mosi <= tx_data[7]; assign spi_clk = ce ? 1'b0 : ~clk; //通过片选是否有效来控制spi_clk reg [7:0] rx_data = 8'hff; //miso使用spi_clk采样数据 always @ (posedge spi_clk) rx_data <= {rx_data[6:0], miso}; endmodule
仿真结果,如图3所示:
图3 仿真波形图
根据图3可以看出,SPI基本功能已经实现(默认CPOL = 0,CPHA = 0),接下来将在此基础上增加上层模块的接口。
-
有上层使用接口的SPI收发代码实现
工程代码:
`timescale 1ns / 1ps module simple_spi ( input phy_clk, input [7:0] spi_din, // SPI需要发送的数据 input spi_data_vld, // SPI数据 有效, 一个时钟宽度(phy_clk) output reg [7:0] spi_dout = 8'hff, // SPI接收到的数据 output reg spi_data_rdy = 0, // SPI接收数据有效 , 一个时钟宽度(phy_clk) output reg spi_cs = 1, // SPI片选 pin output reg spi_clk = 0, // SPI时钟 pin output reg spi_mosi = 1, // SPI主设备输出数据(从设备接收数据) pin input spi_miso, // SPI主设备接收数据(从设备输出数据) pin input rst_n ); reg [7:0] tx_data = 8'hff; reg [3:0] tx_cnt = 0; reg [1:0] tx_st = 0; always @ (posedge phy_clk or negedge rst_n) if(!rst_n) begin spi_mosi <= 1; spi_cs <= 1; spi_clk <= 0; tx_st <= 0; end else case (tx_st) 0: begin tx_cnt <= 0; spi_mosi <= 1; spi_cs <= 1; spi_clk <= 0; if(spi_data_vld) begin tx_data <= spi_din; tx_st <= 1; end else tx_data <= 8'hff; end 1: begin spi_cs <= 0; tx_st <= 2; end 2: begin spi_clk <= 0; spi_mosi <= tx_data[7]; tx_data <= {tx_data[6:0], 1'b1}; if(tx_cnt == 8) tx_st <= 0; else tx_st <= 3; end 3: begin spi_clk <= 1; tx_cnt <= tx_cnt + 1; tx_st <= 2; end default : tx_st <= 0; endcase //======================================================================================= reg spi_clk_r = 0; always @ (posedge phy_clk ) spi_clk_r <= spi_clk; wire rx_pos_clk = ({spi_clk_r, spi_clk} == 2'b01) ? 1'b1 : 1'b0; reg [2:0] rx_cnt = 0; always @ (posedge phy_clk ) if(spi_cs) begin spi_data_rdy <= 0; rx_cnt <= 0; end else if(rx_pos_clk) begin spi_dout <= {spi_dout[6:0], spi_miso}; rx_cnt <= rx_cnt + 1; if(rx_cnt == 7) spi_data_rdy <= 1; else spi_data_rdy <= 0; end else spi_data_rdy <= 0; //======================================================================================= endmodule
详细分析上述工程代码如下:
接口信号
input phy_clk, // SPI模块 操作时钟
input [7:0] spi_din, // SPI需要发送的数据
input spi_data_vld, // SPI数据 有效, 一个时钟宽度(phy_clk)
系统上层代码 可以使用这两个信号, 将需要发送的数据传送到 当前这个模块, 用于数据发送。
output reg [7:0] spi_dout = 8’hff, // SPI接收到的数据
output reg spi_data_rdy = 0, // SPI接收数据有效 , 一个时钟宽度(phy_clk)
系统上层代码 可以使用这两个信号, 将当前模块所接收到的SPI数据发送给上层, 供上层模块使用。
output reg spi_cs = 1, // SPI片选 pin
output reg spi_clk = 0, // SPI时钟 pin
output reg spi_mosi = 1, // SPI主设备输出数据(从设备接收数据) pin
input spi_miso, // SPI主设备接收数据(从设备输出数据) pin
这 4 个信号为SPI的接口信号, 用于连接其他SPI的设备(pin )
input rst_n // 模块 reset 信号
发送状态机
复位状态: 状态机复位, spi_mosi = 1 , 保持高电平; spi_cs = 1,一般情况下SPI的从设备都是cs = 0 有效,但用户需要注意, 也有一些从设备是cs = 1 有效的。 如果遇到这样的芯片,注意修改当前缺省值。 spi_clk = 0, 对应使用的是 CPOL = 0 (标准协议), 如果用户使用 CPOL = 1 的标准, 需要修改这个值。
状态0:等待上层模块发送命令(那些数据需要SPI模块发送出去), 同时保持 cs = 1, spi_clk = 0 ; 如果得到上层的命令,将数据所存为tx_data
状态1:spi_cs = 0 , 片选使能
状态2:产生 spi_clk = 0, 在spi_clk = 0 (低电平期间) 将tx_data 的最高位 赋值给 spi_mosi , 同时 将tx_data 做 逻辑左移,最后一位补一; 如果计数器 (tx_cnt == 8) ,表示当前的数据已经发送完毕了,跳转状态机 到 0; 如果计数器 没有加到 8, 跳转到状态机 3
状态3: 产生 spi_clk = 1 ,同时 计数器加一 , 表示tx_data 已经发送 了 1 bit , 跳转状态机到2;
接收代码
reg spi_clk_r = 0; always @ (posedge phy_clk ) spi_clk_r <= spi_clk; wire rx_pos_clk = ({spi_clk_r, spi_clk} == 2'b01) ? 1'b1 : 1'b0;
得到 spi_clk 的上升沿, 因为当前代码为(CPOL = 0, CPHA = 0) 表示时钟上升沿接收数据。
reg [2:0] rx_cnt = 0; always @ (posedge phy_clk ) if(spi_cs) begin spi_data_rdy <= 0; rx_cnt <= 0; end else if(rx_pos_clk) begin spi_dout <= {spi_dout[6:0], spi_miso}; rx_cnt <= rx_cnt + 1; if(rx_cnt == 7) spi_data_rdy <= 1; else spi_data_rdy <= 0; end else spi_data_rdy <= 0;
在片选无效时, 接收计数器(rx_cnt) 清零。 每当得到一个spi_clk 的上升沿(rx_pos_clk) , spi_miso (其他设备发送, 当前模块接收)信号被存储到spi_dout 中,同时计数器(rx_cnt) 加一。当接收8-bit后, spi_data_rdy = 1 , 通知上层模块 , SPI模块得到一个byte的数据了;其他情况 spi_data_rdy = 0
仿真工程代码:
`timescale 1ns / 1ps module simple_spi_sim( ); reg clk = 0; always clk = #10 ~clk; reg rst_n = 0; reg [7:0] spi_din = 8'hff; reg spi_data_vld = 0; initial begin spi_data_vld = 0; rst_n = 0; #200; rst_n = 1; #400; @(posedge clk); spi_din = 8'h5d; spi_data_vld = 1; @(posedge clk); spi_data_vld = 0; #400; @(posedge clk); spi_din = 8'h3c; spi_data_vld = 1; @(posedge clk); spi_data_vld = 0; #400; @(posedge clk); spi_din = 8'h47; spi_data_vld = 1; @(posedge clk); spi_data_vld = 0; #200000; $stop; end wire [7:0] spi_dout; wire spi_data_rdy; wire spi_cs; wire spi_clk; wire spi_data; simple_spi simple_spi_inst ( .phy_clk (clk), .spi_din (spi_din), // SPI需要发送的数据 .spi_data_vld (spi_data_vld), // SPI数据 有效, 一个时钟宽度(phy_clk) .spi_dout (spi_dout), // SPI接收到的数据 .spi_data_rdy (spi_data_rdy), // SPI接收数据有效 , 一个时钟宽度(phy_clk) .spi_cs (spi_cs), // SPI片选 pin .spi_clk (spi_clk), // SPI时钟 pin .spi_mosi (spi_data), // SPI 主设备输出数据(从设备接收数据) pin .spi_miso (spi_data), // SPI主设备接收数据(从设备输出数据) pin .rst_n (rst_n) ); //================================================================== endmodule
仿真波形如图4所示:
图4 仿真波形
spi_din, spi_data_vld 为仿真上层模块数据信号, 当前仿真模块 仿真上层模块 发送数据 为 8’h5d, 8’h3c, 8’h47。(蓝色信号)
spi_dout, spi_data_rdy 为上层模块接收到的信号(从其他设备发送过来, 当前仿真程序 是将 spi_mosi 和 spi_miso 信号连接, loopback 回环测试) (浅粉色)
spi_cs, spi_clk, spi_data 代表SPI模块 pin , 其中spi_data 是将spi_mosi 和 spi_miso 连接。回环测试。
图中我们可以看出:
首先是cs 信号 为 0, 然后 发送spi_clk 时钟信号, 同时配合spi_clk 发送spi_data 信号,这里我们可以看出, spi_data 是在spi_clk 下降沿变化数据的。当 8bit 发送完成, cs = 1 。 表示当前操作结束。
接收模块在cs = 1 时 ,不做任何动作; 在cs = 0 时, 每当spi_clk时钟上升沿时, 采样一个bit 数据。 当 8-bit 数据接收到后, 产生spi_data_rdy 信号。
总结
由上面3个SPI实验工程,从简单到复杂的实现过程中,编写一个实现基本功能的SPI模块 还是比较简单使用的, 可以用来处理一些SPI的设备。 可以帮助大家迅速理解SPI的协议。
不足之处是spi_clk 时钟 最大只能是当前时钟(phy_clk)的 二分之一, 甚至更低, 对于高速SPI设备 不能发挥出SPI的效率。 同时 ,不是很容易添加复杂的spi 操作 向SPI FLASH 操作等等。之后的文章会针对不足进行改进。
其实当大家理解了SPI协议后, 会发现SPI协议要比 I2C, UART协议还要简单 。 SPI协议 其实就是将数据移位输出(发送),移位输入(接收),同时配合上cs, clk 信号即可。
SPI系列文章链接:
SPI 通讯协议 及 SPI 相关工程 详解