从EEPROM到液晶屏:一个FPGA工程师的SPI实战踩坑记录(附Verilog代码)
从EEPROM到液晶屏一个FPGA工程师的SPI实战踩坑记录附Verilog代码当FPGA项目需要同时与多个SPI外设通信时工程师往往会面临时钟速率、数据格式和连接方式的复杂权衡。本文将分享我在驱动EEPROM、DSP协处理器和液晶屏三个典型SPI设备时遇到的真实挑战以及如何通过Verilog代码实现稳定可靠的通信。1. 多SPI设备的系统架构设计在嵌入式系统中SPI总线因其简单高效而广受欢迎。但当需要同时连接EEPROM存储配置、DSP数据处理和液晶屏显示输出时系统设计就变得复杂起来。这三种设备对SPI通信有着截然不同的需求EEPROM通常需要较低的时钟频率1-10MHz数据格式多为8位或16位DSP协处理器可能需要更高的时钟速率10-50MHz数据位宽可达32位液晶屏对时序要求严格可能需要特定的CPOL/CPHA配置1.1 连接方式的选择SPI设备有两种主要连接方式各有优缺点连接方式优点缺点适用场景多NSS独立控制每个设备需要更多GPIO引脚设备间无数据依赖菊花链节省引脚资源设备间存在串行延迟数据需要顺序处理在我的项目中EEPROM和DSP之间存在数据依赖配置需要先加载到DSP而液晶屏是独立更新的。因此采用了混合连接方式// SPI主控制器接口定义 module spi_master ( input wire clk, input wire reset, output reg [1:0] nss, // nss[0]用于EEPROMDSP链nss[1]用于液晶屏 output reg sck, output reg mosi, input wire miso );2. SPI时序参数的实战配置不同SPI设备对时序参数的要求可能大相径庭这是多设备驱动中最容易出问题的地方。2.1 时钟极性与相位配置CPOL和CPHA的组合决定了数据采样边沿常见的四种模式Mode 0(CPOL0, CPHA0)时钟空闲低电平数据在上升沿采样Mode 1(CPOL0, CPHA1)时钟空闲低电平数据在下降沿采样Mode 2(CPOL1, CPHA0)时钟空闲高电平数据在下降沿采样Mode 3(CPOL1, CPHA1)时钟空闲高电平数据在上升沿采样在我的项目中EEPROM使用Mode 0DSP使用Mode 1液晶屏使用Mode 3这要求在Verilog状态机中实现动态模式切换// 时钟生成逻辑 always (posedge clk or posedge reset) begin if (reset) begin sck (current_mode[1]) ? 1b1 : 1b0; // 根据CPOL设置初始电平 end else begin case (state) IDLE: sck (current_mode[1]) ? 1b1 : 1b0; TRANSFER: sck ~sck; // 在传输状态翻转时钟 endcase end end2.2 时钟分频策略三个设备需要不同的时钟速率EEPROM: 2MHz (主时钟100MHz的50分频)DSP: 10MHz (10分频)液晶屏: 5MHz (20分频)实现时采用了可配置的分频计数器reg [7:0] clock_divider; reg [7:0] clock_counter; always (posedge clk or posedge reset) begin if (reset) begin clock_counter 0; sck 0; end else if (clock_counter clock_divider) begin clock_counter 0; sck ~sck; end else begin clock_counter clock_counter 1; end end3. 数据传输调度与冲突解决当多个SPI设备需要同时服务时合理的调度策略至关重要。我采用了基于优先级的轮询机制高优先级液晶屏刷新需要定期更新中优先级DSP数据处理低优先级EEPROM配置读取3.1 状态机设计typedef enum { IDLE, LCD_INIT, LCD_TRANSFER, DSP_TRANSFER, EEPROM_READ, EEPROM_WRITE } spi_state_t; spi_state_t state, next_state; // 状态转移逻辑 always (*) begin case (state) IDLE: begin if (lcd_update_req) next_state LCD_INIT; else if (dsp_data_ready) next_state DSP_TRANSFER; else if (eeprom_read_req) next_state EEPROM_READ; else next_state IDLE; end // 其他状态转移... endcase end3.2 数据缓冲区管理为每个设备设置了独立的FIFO缓冲区防止数据丢失缓冲区深度位宽用途LCD_FIFO6416存储显示数据DSP_FIFO3232处理中间数据EEPROM_FIFO168配置数据缓存4. 常见问题与调试技巧在实际调试过程中遇到了几个典型问题这里分享解决方案。4.1 建立/保持时间违例当SCK频率过高时MISO数据可能无法满足从设备的建立保持时间要求。解决方法在FPGA输入端插入IDELAY原语调整数据采样点降低SCK频率在SCK边沿后延迟采样适用于CPHA1模式// 延迟采样实现示例 reg miso_delayed; always (posedge clk) begin if (sck_rising_edge) begin miso_delayed #2 miso; // 插入2ns延迟 end end4.2 多设备干扰问题当切换NSS信号时其他设备可能产生干扰。解决方案在NSS切换期间插入至少1个SCK周期的空闲时间确保MOSI在NSS无效时为高阻态为每个设备添加独立的输入滤波器// NSS切换时的保护逻辑 task change_nss; input [1:0] new_nss; begin mosi 1bz; // 置为高阻 #10; // 等待10ns nss new_nss; #10; // 等待10ns end endtask4.3 菊花链中的数据错位在EEPROM→DSP的菊花链中发现数据位错位问题。原因是EEPROM的MSB在前而DSP期望LSB在前解决方案是在FPGA中实现位序转换function [7:0] reverse_bits; input [7:0] data; begin reverse_bits {data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]}; end endfunction5. 完整SPI控制器实现要点最后分享完整的SPI控制器设计中的关键部分5.1 顶层模块接口module spi_controller ( input wire clk_100mhz, input wire reset_n, // 设备接口 output wire [1:0] nss, output wire sck, output wire mosi, input wire miso, // 配置接口 input wire [1:0] device_select, input wire [7:0] clock_div, input wire [1:0] spi_mode, // 数据接口 input wire [31:0] tx_data, output wire [31:0] rx_data, input wire tx_valid, output wire tx_ready, output wire rx_valid );5.2 核心状态机always (posedge clk_100mhz or negedge reset_n) begin if (!reset_n) begin state IDLE; bit_counter 0; end else begin case (state) IDLE: begin if (tx_valid) begin shift_reg tx_data; state START; end end START: begin nss device_select; state TRANSFER; end TRANSFER: begin if (bit_counter data_width) begin mosi shift_reg[data_width-1]; shift_reg {shift_reg[data_width-2:0], miso}; bit_counter bit_counter 1; end else begin state STOP; end end STOP: begin nss 2b11; rx_data shift_reg; rx_valid 1b1; state IDLE; end endcase end end5.3 时钟生成逻辑reg [7:0] clk_counter; reg sck_internal; always (posedge clk_100mhz or negedge reset_n) begin if (!reset_n) begin clk_counter 0; sck_internal spi_mode[1]; // CPOL end else if (state TRANSFER) begin if (clk_counter clock_div) begin clk_counter 0; sck_internal ~sck_internal; end else begin clk_counter clk_counter 1; end end else begin sck_internal spi_mode[1]; // 空闲状态 clk_counter 0; end end assign sck sck_internal;在项目最终实现中这个SPI控制器成功实现了同时驱动三个不同特性的SPI设备动态配置时钟频率1-25MHz支持四种SPI模式数据传输速率达到15Mbps资源占用不到500个LUT