当我们了解了spi 的通讯协议后, 我们使用verilog 语言,编写一个简单的spi 通讯模块。 这个模块将会向大家展示一个完整的spi 通讯方式。
相关参考文章:
SPI 通讯协议 及 SPI 相关工程 详解
一个简单的 spi 模块(verilog 代码 : CPOL = 0 CPHA = 0)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: Fraser Innovation Inc // Engineer: WilliamG // // Create Date: 04/23/2021 10:11:52 AM // Design Name: simple spi // Module Name: simple_spi // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// 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;
接收代码
|
1 2 3 4 5 |
reg <span style="color: #0000ff;">spi_clk_r</span> = 0; always @ (posedge phy_clk ) <span style="color: #0000ff;">spi_clk_r</span> <= spi_clk; wire <span style="color: #0000ff;">rx_pos_clk</span> = ({spi_clk_r, spi_clk} == 2'b01) ? 1'b1 : 1'b0; |
得到 spi_clk 的上升沿, 因为当前代码为(CPOL = 0, CPHA = 0) 表示时钟上升沿接收数据。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
reg [2:0] rx_cnt = 0; always @ (posedge phy_clk ) if(spi_cs) begin <span style="color: #0000ff;"> spi_data_rdy</span> <= 0; rx_cnt <= 0; end else if(rx_pos_clk) begin spi_dout <= {spi_dout[6:0], <span style="color: #0000ff;">spi_miso</span>}; rx_cnt <= rx_cnt + 1; if(rx_cnt == 7) <span style="color: #0000ff;">spi_data_rdy</span> <= 1; else <span style="color: #0000ff;">spi_data_rdy</span> <= 0; end else <span style="color: #0000ff;">spi_data_rdy</span> <= 0; |
在片选无效时, 接收计数器(rx_cnt) 清零。 每当得到一个spi_clk 的上升沿(rx_pos_clk) , spi_miso (其他设备发送, 当前模块接收)信号被存储到spi_dout 中,同时计数器(rx_cnt) 加一。当接收8bit后, spi_data_rdy = 1 , 通知上层模块 , spi 模块得到一个byte的数据了;其他情况 spi_data_rdy = 0
创建仿真工程
仿真工程代码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: Fraser Innovation Inc // Engineer: WilliamG // // Create Date: 04/23/2021 10:12:11 AM // Design Name: smple spi simulation // Module Name: simple_spi_sim // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// 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 |
仿真波形
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 数据。 当 8bit 数据接收到后, 产生spi_data_rdy 信号。
总结
一个简单的spi 模块 还是比较简单使用的, 可以用来处理一些spi 的设备。 优点是模块比较简单, 容易理解。可以帮助大家迅速理解spi 的协议。 缺点是spi_clk 时钟 最大只能是当前时钟(phy_clk)的 二分之一, 甚至更低, 对于高速spi 设备 不能发挥出spi的效率。 同时 ,不是很容易添加复杂的spi 操作 向spi flash 操作等等。
其实当大家理解了spi 协议后, 会发现spi协议要比 ⅱc , uart 协议还要简单 。 spi 协议 其实就是将数据移位输出(发送),移位输入(接收),同时配合上cs, clk 信号即可。
