1. 项目背景与核心价值
在嵌入式系统开发中,Zynq系列SoC因其独特的ARM+FPGA架构而广受欢迎。这个项目聚焦于Zynq平台上三种最常用的串行通信协议(UART、IIC、SPI)的实践应用,特别是第24课专门探讨了如何通过PL(Programmable Logic)端模拟SPI从设备与W25Q16 Flash芯片交互的技术实现。
SPI(Serial Peripheral Interface)作为一种高速全双工同步串行总线,在存储器件、传感器、显示模块等领域应用广泛。W25Q16是Winbond公司推出的16M-bit串行Flash存储器,采用标准SPI接口,读写速度可达104MHz。传统做法通常用PS(Processing System)端的SPI控制器直接操作,但通过PL端逻辑模拟SPI从设备有三大独特优势:
- 时序精度控制:PL端可精确到纳秒级的时序调整能力,适合对时序敏感的定制协议
- 资源利用率:减轻PS端负担,特别在需要并行处理多个SPI设备时优势明显
- 协议扩展性:便于实现非标准SPI变种(如Dual/Quad SPI模式)
2. 硬件架构设计解析
2.1 系统整体连接方案
本实验的硬件连接涉及三个关键部分:
- Zynq PS端:运行Linux系统,通过AXI总线与PL交互
- PL端逻辑:实现SPI从设备控制器
- W25Q16芯片:作为目标存储设备
具体信号连接如下表所示:
| Zynq PL端信号 | W25Q16引脚 | 功能说明 |
|---|---|---|
| spi_clk | CLK | 时钟信号(由PL生成) |
| spi_mosi | DI | 主出从入数据线 |
| spi_miso | DO | 主入从出数据线 |
| spi_cs | CS# | 片选信号(低有效) |
| - | WP# | 写保护(实验时可接高电平) |
| - | HOLD# | 保持(实验时可接高电平) |
关键提示:PL端需要特别注意信号电平匹配。W25Q16工作电压为3.3V,需确保Zynq Bank电压设置正确,必要时加入电平转换电路。
2.2 PL逻辑模块划分
在Vivado中设计的PL侧逻辑包含以下核心模块:
-
SPI从机状态机:
- 实现SPI模式3(CPOL=1, CPHA=1)的时序控制
- 处理CS#下降沿触发、时钟同步等关键事件
- 包含8位移位寄存器用于数据收发
-
AXI-Lite接口:
- 32位数据总线宽度
- 4KB地址空间映射
- 实现以下寄存器:
- CTRL_REG(控制寄存器)
- STATUS_REG(状态寄存器)
- TX_DATA_REG(发送数据)
- RX_DATA_REG(接收数据)
-
W25Q16指令解码器:
- 识别0x03(READ)、0x02(PP)、0x05(RDSR)等常用指令
- 自动处理地址相位和数据相位转换
- 实现写使能(WREN)的安全机制
3. Verilog实现关键代码分析
3.1 SPI时序生成模块
verilog复制module spi_slave (
input wire axi_clk,
input wire axi_resetn,
// AXI-Lite接口信号
input wire [31:0] axi_awaddr,
// ...其他AXI信号省略...
// SPI物理接口
output reg spi_clk,
output reg spi_cs,
output reg spi_mosi,
input wire spi_miso
);
// 状态机定义
localparam [2:0]
IDLE = 3'b000,
CMD_PH = 3'b001,
ADDR_PH = 3'b010,
DATA_PH = 3'b011,
WAIT_CS = 3'b100;
reg [2:0] current_state, next_state;
reg [7:0] shift_reg;
reg [3:0] bit_cnt;
reg [23:0] mem_addr;
reg [7:0] cmd_reg;
always @(posedge axi_clk or negedge axi_resetn) begin
if (!axi_resetn) begin
current_state <= IDLE;
spi_clk <= 1'b1; // 模式3要求空闲时时钟为高
end else begin
current_state <= next_state;
// 时钟生成逻辑
if (current_state != IDLE && !spi_cs) begin
spi_clk <= ~spi_clk;
end
end
end
这段代码实现了SPI从设备的核心状态机,关键点包括:
- 采用三段式状态机设计(状态声明、状态转移、输出逻辑)
- 严格遵循模式3的时钟极性要求(空闲时SCK=1)
- 使用bit_cnt计数器精确控制每个相位的数据位
3.2 W25Q16指令处理逻辑
verilog复制// 指令解码处理
always @(*) begin
case (cmd_reg)
8'h03: begin // READ
if (bit_cnt == 8'd31) begin
next_data = flash_mem[mem_addr];
next_state = DATA_PH;
end
end
8'h02: begin // PAGE PROGRAM
if (bit_cnt == 8'd31) begin
if (wren_active) begin
flash_mem[mem_addr] = shift_reg;
end
end
end
8'h05: begin // READ STATUS REG
next_data = {6'b0, wip_bit, wel_bit};
end
default: next_state = IDLE;
endcase
end
经验之谈:实际调试中发现W25Q16对指令时序极其敏感,建议:
- 在CS#拉低后等待至少50ns再发送第一个时钟边沿
- 连续两次操作之间保持CS#高电平至少500ns
- 写操作后必须检查状态寄存器的WIP位
4. PS端Linux驱动集成
4.1 字符设备驱动实现
在PS端运行的Linux系统中,需要通过字符设备驱动与PL模块交互:
c复制static int spi_slave_open(struct inode *inode, struct file *filp) {
struct spi_slave_dev *dev;
dev = container_of(inode->i_cdev, struct spi_slave_dev, cdev);
filp->private_data = dev;
return 0;
}
static ssize_t spi_slave_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
uint32_t val;
// 读取AXI寄存器
val = ioread32(dev->base_addr + RX_DATA_OFFSET);
if (copy_to_user(buf, &val, sizeof(val)))
return -EFAULT;
return sizeof(val);
}
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = spi_slave_open,
.read = spi_slave_read,
.write = spi_slave_write,
.unlocked_ioctl = spi_slave_ioctl
};
4.2 用户空间测试程序
编写简单的测试程序验证读写功能:
c复制int main() {
int fd = open("/dev/spi_slave", O_RDWR);
// 发送READ指令读取Flash ID
uint8_t cmd[] = {0x9F, 0, 0, 0}; // RDID指令
write(fd, cmd, sizeof(cmd));
uint8_t id[3];
read(fd, id, sizeof(id));
printf("Flash ID: %02X %02X %02X\n", id[0], id[1], id[2]);
close(fd);
return 0;
}
5. 调试技巧与性能优化
5.1 在线调试方法
-
ILA核实时监测:
- 在Vivado中添加ILA(Integrated Logic Analyzer)核
- 监控关键信号:spi_clk、spi_cs、spi_mosi、spi_miso
- 设置触发条件:CS下降沿触发
-
Linux端调试信息:
bash复制# 查看驱动打印信息 dmesg | grep spi_slave # 实时监控AXI寄存器 devmem2 0x43C00000 # 控制寄存器地址
5.2 时序优化策略
通过实测发现原始设计在100MHz时钟下SPI速率只能达到约10MHz,通过以下优化提升到25MHz:
-
流水线设计:
verilog复制// 原顺序执行方式 always @(posedge spi_clk) begin shift_reg <= {shift_reg[6:0], spi_miso}; bit_cnt <= bit_cnt + 1; end // 优化为并行处理 always @(posedge spi_clk) begin shift_reg[7:1] <= shift_reg[6:0]; shift_reg[0] <= spi_miso; end always @(negedge spi_clk) begin bit_cnt <= bit_cnt + 1; end -
跨时钟域处理:
- 使用双缓冲技术处理AXI总线与SPI时钟域的数据交换
- 添加同步触发器链避免亚稳态
-
资源复用:
- 将地址寄存器和数据寄存器合并
- 使用LUTRAM替代Block RAM存储W25Q16模拟数据
6. 扩展应用与进阶方向
6.1 Quad SPI模式实现
W25Q16支持Quad SPI模式,通过PL实现可大幅提升吞吐量:
-
硬件修改:
- 连接IO2(WP#)和IO3(HOLD#)作为数据线
- 在PL端增加4位宽度的移位寄存器
-
指令扩展:
verilog复制8'hEB: begin // Fast Read Quad Output if (bit_cnt == 8'd7) begin quad_mode <= 1'b1; next_state = ADDR_PH; end end
6.2 与DMA控制器集成
通过AXI DMA实现PS与PL间的高速数据传输:
- Vivado中添加DMA IP核
- 配置SG(Scatter-Gather)模式描述符
- Linux驱动中实现mmap映射:
c复制fd = open("/dev/mem", O_RDWR); dma_buf = mmap(NULL, BUF_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, DMA_ADDR);
实测DMA传输相比IOCTL方式性能提升8-10倍,特别适合大块数据读写场景。
7. 常见问题解决方案
7.1 数据错位问题
现象:接收到的数据总是偏移1位
排查步骤:
- 用ILA捕获原始波形,确认时钟极性设置
- 检查第一个数据位采样点(应在第一个时钟边沿之后)
- 验证bit_cnt复位逻辑(CS#变高时是否清零)
解决方案:
verilog复制// 修正采样时机
always @(negedge spi_clk) begin // 改为下降沿采样
if (!spi_cs) begin
shift_reg <= {shift_reg[6:0], spi_miso};
end
end
7.2 写操作失败
典型错误流程:
- 未发送WREN指令直接写
- 写操作后未等待WIP清除
- 页编程跨越256字节边界
正确操作序列:
c复制// 1. 发送WREN
write(fd, "\x06", 1);
// 2. 发送PP指令+地址+数据
uint8_t cmd[4] = {0x02, 0x00, 0x01, 0x00};
write(fd, cmd, 4);
write(fd, data_buf, 256);
// 3. 轮询状态寄存器
do {
read_status(fd, &status);
} while (status & 0x01);
8. 实测性能数据对比
通过不同实现方式的对比测试得到以下数据:
| 实现方式 | 最大时钟频率 | 读取1KB耗时 | CPU占用率 |
|---|---|---|---|
| PS端SPI控制器 | 50 MHz | 220 μs | 85% |
| PL基础实现 | 25 MHz | 420 μs | 15% |
| PL+DMA优化 | 25 MHz | 380 μs | <5% |
| Quad SPI模式 | 50 MHz | 105 μs | 10% |
从数据可以看出,虽然纯PL实现时钟频率较低,但CPU占用率大幅下降。通过Quad SPI+DMA的组合方案,既能保证高吞吐量,又能最大限度释放CPU资源。
