别再只会用UART了!用Verilog手撸一个PISO移位寄存器,搞定SPI主设备数据发送

张开发
2026/4/14 12:30:51 15 分钟阅读

分享文章

别再只会用UART了!用Verilog手撸一个PISO移位寄存器,搞定SPI主设备数据发送
用Verilog实现SPI主设备PISO移位寄存器实战指南在嵌入式开发中SPISerial Peripheral Interface总线因其简单高效而广受欢迎。但当你遇到MCU硬件SPI外设资源不足或者需要完全自定义时序的场景时用Verilog手写一个并行转串行PISO移位寄存器就成了解决问题的利器。本文将带你从零开始实现一个可即插即用的SPI主设备发送模块避开枯燥的理论直击实战要点。1. 为什么需要PISO移位寄存器SPI通信的核心在于主设备能够准确地将并行数据转换为串行比特流。虽然大多数MCU都内置了硬件SPI模块但在以下场景中自己实现PISO显得尤为必要资源受限当项目需要多个SPI主设备而硬件外设不足时时序定制需要非标准时钟极性CPOL或相位CPHA配置教学目的深入理解SPI底层工作原理FPGA开发在可编程逻辑中构建轻量级SPI控制器与硬件SPI相比Verilog实现的PISO模块具有以下优势特性硬件SPIVerilog PISO灵活性固定配置完全可定制资源占用专用硬件可优化逻辑时钟控制受限自由定义调试难度低中等适用场景标准通信特殊需求2. PISO移位寄存器工作原理PISOParallel-In Serial-Out移位寄存器的核心功能是将N位并行数据转换为1位串行输出。让我们分解其工作流程加载阶段当load信号有效时并行数据被锁存到内部寄存器移位阶段每个时钟周期输出一位数据寄存器内容左移或右移输出阶段最高位或最低位取决于设计被送到串行输出端口一个典型的4位PISO操作时序如下时钟周期: 0 1 2 3 4 Load: H L L L L 输出: X D3 D2 D1 D0注意实际设计中需要考虑时钟极性、数据对齐和同步问题3. Verilog实现细节下面是一个完整的SPI主设备发送模块实现包含PISO核心功能module SPI_Master_TX #( parameter DATA_WIDTH 8 )( input wire clk, input wire reset_n, input wire load, input wire [DATA_WIDTH-1:0] parallel_data, output reg serial_out, output reg busy ); reg [DATA_WIDTH-1:0] shift_reg; reg [3:0] bit_counter; always (posedge clk or negedge reset_n) begin if (!reset_n) begin shift_reg 0; bit_counter 0; serial_out 0; busy 0; end else if (load !busy) begin shift_reg parallel_data; bit_counter DATA_WIDTH; busy 1; end else if (busy) begin serial_out shift_reg[DATA_WIDTH-1]; shift_reg shift_reg 1; bit_counter bit_counter - 1; if (bit_counter 1) begin busy 0; end end end endmodule关键设计要点参数化设计DATA_WIDTH参数允许灵活配置数据位宽状态管理busy信号指示模块是否正在发送数据移位控制每个时钟周期输出最高位并左移寄存器自动停止bit_counter计数确保完整发送所有位4. 仿真测试与验证为确保模块可靠性我们需要构建测试平台进行验证。以下是一个简单的测试用例module SPI_Master_TX_tb; reg clk 0; reg reset_n 0; reg load 0; reg [7:0] parallel_data 0; wire serial_out; wire busy; SPI_Master_TX uut ( .clk(clk), .reset_n(reset_n), .load(load), .parallel_data(parallel_data), .serial_out(serial_out), .busy(busy) ); always #5 clk ~clk; initial begin // 初始化 #10 reset_n 1; // 测试用例1正常发送 #10 parallel_data 8b1010_1101; load 1; #10 load 0; // 等待发送完成 #160; // 测试用例2连续发送 parallel_data 8b1100_0011; load 1; #10 load 0; #80 parallel_data 8b1111_0000; load 1; #10 load 0; #160 $finish; end endmodule仿真中需要检查的关键点复位后所有信号是否处于初始状态load信号触发后busy信号是否立即变高每个时钟周期输出是否正确发送完成后busy信号是否及时变低连续发送时数据是否无冲突5. 实际应用优化技巧在真实项目中我们还需要考虑以下优化点5.1 时钟域交叉处理当SPI时钟与系统时钟不同源时需要添加同步逻辑// 双触发器同步器 reg spi_clk_sync1, spi_clk_sync2; always (posedge clk or negedge reset_n) begin if (!reset_n) begin spi_clk_sync1 0; spi_clk_sync2 0; end else begin spi_clk_sync1 spi_clk; spi_clk_sync2 spi_clk_sync1; end end5.2 可配置时钟极性通过参数支持不同SPI模式module SPI_Master_TX #( parameter DATA_WIDTH 8, parameter CPOL 0, // 时钟极性 parameter CPHA 0 // 时钟相位 )( // ...其他端口... output reg spi_clk ); // 根据CPOL/CPHA生成时钟 always (*) begin if (busy) begin if (CPHA) spi_clk CPOL ^ clk; else spi_clk CPOL; end else begin spi_clk CPOL; end end5.3 性能优化技巧流水线设计在高速应用中可将移位操作分为两级流水状态机优化使用独热码编码状态机提高时序性能资源共享多个SPI设备可共享移位逻辑时分复用6. 常见问题排查在实际部署中可能会遇到以下问题问题1数据错位检查时钟极性和相位配置确认主从设备使用相同的位序MSB/LSB first问题2时序违例添加适当的时钟约束在关键路径插入寄存器问题3毛刺干扰在输出端口添加消抖逻辑使用同步复位避免亚稳态// 输出消抖示例 reg clean_serial_out; always (posedge clk) begin if (serial_out 1b0) clean_serial_out 0; else clean_serial_out 1; end在最近的一个物联网设备项目中我们使用这种PISO设计实现了与多个传感器的通信。最初遇到数据错位问题通过添加详细的仿真测试和实际示波器测量最终定位到是时钟相位配置不一致导致的。修正后系统稳定运行证明了这种方法的可靠性。

更多文章