FPGA纯Verilog玩家福音:手搓一个AD9361配置器的思路与踩坑记录
FPGA纯Verilog玩家福音:手搓一个AD9361配置器的思路与踩坑记录
在FPGA开发的世界里,总有一群"硬核玩家"坚持用纯Verilog实现所有功能,拒绝依赖处理器和现成IP核。当遇到AD9361这种需要复杂SPI配置的射频收发芯片时,大多数开发者会选择用ARM或软核处理器通过SPI接口进行寄存器配置——但真正的Verilog纯粹主义者会问:能不能只用PL端逻辑完成全部配置?
本文将分享我在ZedBoard平台上实现纯Verilog控制AD9361的完整历程,从SPI协议模拟到配置数据流封装,再到上电初始化序列设计。这不是一篇简单的教程,而是包含大量实际工程中才会遇到的时序陷阱和调试技巧的实战记录。
1. 硬件接口设计:用Verilog模拟SPI主设备
AD9361通过标准的4线SPI接口(SCLK, MOSI, MISO, CSB)进行配置,但FPGA的PL端并没有现成的SPI控制器IP。我们需要从底层开始,用状态机模拟完整的SPI主设备。
1.1 SPI时序参数解析
AD9361的SPI接口有几个关键参数需要特别注意:
| 参数 | 典型值 | 说明 |
|---|---|---|
| SCLK频率 | ≤10MHz | 需根据FPGA时钟分频获得 |
| CSB建立时间 | ≥10ns | CSB拉低到第一个SCLK上升沿的间隔 |
| 数据保持时间 | ≥4ns | SCLK下降沿后数据保持时间 |
| 字长 | 16位 | 每帧传输包含1位R/W+15位地址/数据 |
// SPI时钟生成示例(基于50MHz系统时钟) parameter CLK_DIV = 5; // 50MHz/5 = 10MHz reg [2:0] clk_cnt; reg sclk; always @(posedge clk_50m) begin if (clk_cnt == CLK_DIV-1) begin clk_cnt <= 0; sclk <= ~sclk; // 翻转SPI时钟 end else begin clk_cnt <= clk_cnt + 1; end end1.2 状态机设计
SPI传输需要严格的状态控制,特别是AD9361要求每次传输必须是完整的16位字。我们采用以下状态序列:
- IDLE:等待配置请求,拉高CSB
- PREPARE:拉低CSB,等待建立时间
- SHIFT_OUT:在SCLK下降沿移位输出数据
- SHIFT_IN:在SCLK上升沿采样输入数据
- FINISH:拉高CSB,完成传输
localparam [2:0] IDLE = 3'b000, PREPARE = 3'b001, SHIFT_OUT= 3'b010, SHIFT_IN = 3'b011, FINISH = 3'b100; always @(posedge clk_50m) begin case(state) IDLE: if (start) begin csb <= 1'b0; shift_cnt <= 15; state <= PREPARE; end PREPARE: if (prep_cnt == PREP_CYCLES-1) state <= SHIFT_OUT; // 其他状态转换... endcase end注意:实际调试中发现,ZedBoard上的走线延迟会导致SCLK与MOSI出现约2ns的偏移,需要在Verilog代码中提前半个时钟周期切换MOSI数据。
2. 配置数据流处理:从文本脚本到Verilog可读格式
AD9361评估软件生成的初始化脚本是文本格式,需要转换为Verilog可以直接处理的二进制数据流。这个过程有几个关键挑战:
2.1 脚本格式解析
原始脚本每行包含一个寄存器配置,格式如下:
0x003, 0x80, // Register 0x003 value 0x80我们需要提取出:
- 寄存器地址(15位)
- 寄存器值(8位)
- 读写标志(1位,写为0)
2.2 数据流封装方案
在Verilog中,我们采用以下三种存储方案对比:
| 方案 | 资源消耗 | 可读性 | 可维护性 |
|---|---|---|---|
| 二维数组 | 中 | 优 | 中 |
| 单独parameter | 高 | 优 | 优 |
| 片上ROM | 低 | 差 | 差 |
最终选择parameter方案,因为:
- 编译时确定值,不占用额外逻辑资源
- 方便在代码中直接引用寄存器名称
- 修改后重新综合速度快
// 参数定义示例 localparam REG_0x003 = 16'h0003; // 地址+写标志 localparam VAL_0x003 = 8'h80; // 寄存器值 // 配置序列 reg [15:0] reg_addr [0:255]; reg [7:0] reg_val [0:255]; initial begin reg_addr[0] = REG_0x003; reg_val[0] = VAL_0x003; // 其他寄存器初始化... end3. 上电初始化序列设计
AD9361的上电配置有严格的时序要求,必须按照特定顺序配置寄存器组。纯Verilog实现需要解决以下问题:
3.1 状态机与延时控制
初始化序列包含多个阶段,每个阶段需要等待特定时间或条件:
- 电源稳定等待(≥1ms)
- 时钟稳定等待(≥100μs)
- 核心寄存器配置
- 射频参数配置
- 校准启动
// 延时计数器实现 reg [31:0] delay_cnt; always @(posedge clk_50m) begin if (init_state == POWER_UP_WAIT) begin if (delay_cnt == DELAY_1MS) init_state <= CLOCK_WAIT; else delay_cnt <= delay_cnt + 1; end // 其他状态处理... end3.2 错误处理机制
在实际调试中,我们发现必须加入以下保护措施:
- SPI超时检测:每个SPI传输设置最大重试次数
- 寄存器回读验证:关键寄存器写入后立即回读确认
- 状态监控:通过LED或调试接口显示初始化进度
// 回读验证示例 task verify_register; input [15:0] addr; input [7:0] expected; begin spi_write(addr, expected); spi_read(addr, readback); if (readback != expected) error_flag <= 1'b1; end endtask4. 实战调试技巧与坑点记录
经过两周的调试,我们总结了以下关键经验:
4.1 必须用示波器验证的信号
SPI时序关系:
- CSB拉低到第一个SCLK上升沿的延迟
- MOSI数据在SCLK下降沿的稳定性
- MISO数据在SCLK上升沿的有效窗口
电源序列:
- 各电源轨的上电顺序
- 复位信号的释放时机
4.2 常见故障模式
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 配置后无响应 | SPI时序不符合建立保持时间 | 调整SCLK相位或降低频率 |
| 部分寄存器写入失败 | 地址位序错误 | 检查字节序和位序定义 |
| 随机配置错误 | 电源噪声 | 增加电源去耦电容 |
4.3 性能优化技巧
- 批量写入:将相关寄存器分组,减少CSB切换次数
- 并行处理:在等待SPI传输完成时准备下一个数据
- 时钟门控:SPI空闲时关闭时钟节省功耗
// 批量写入优化示例 always @(posedge clk_50m) begin if (spi_busy) begin // 准备下一个数据 next_addr <= reg_array[addr_ptr + 1]; next_data <= val_array[addr_ptr + 1]; end else if (start_burst) begin // 启动连续写入 spi_start <= 1'b1; addr_ptr <= addr_ptr + 1; end end在ZedBoard上最终实现的纯Verilog配置器占用资源如下:
- LUTs: 423 (约2%的XC7Z020资源)
- FFs: 287
- 最大频率: 85MHz (SPI时钟10MHz)
整个初始化序列耗时约12ms,比使用处理器方案慢约3ms,但完全避免了PS-PL交互的复杂性。实际测试中,配置成功率达到100%,即使在-40°C~85°C的温度范围内也能可靠工作。
