## 1. 从脚本到代码:为什么我们需要自动化转换?
上一回我们聊了怎么用ADI官方的评估软件,点点鼠标,生成一个能让AD9361“醒过来”的初始化脚本。那个脚本文件,本质上是一长串的寄存器地址和要写入的值。如果你是手动操作,接下来就得对着这份“天书”,在Verilog里写几百甚至上千行`spi_write`任务,把每个值一个个灌进AD9361的SPI接口里。这事儿我干过,非常痛苦,不仅耗时,还特别容易出错。寄存器地址抄错一位,或者数值写反一个比特,AD9361就可能“装死”不干活,排查起来简直是大海捞针。
所以,我们这第二回的核心目标就非常明确了:**写一个Python脚本,自动把那个文本格式的初始化脚本,转换成可以直接在FPGA里用的、规整的Verilog代码**。这不仅仅是省时间,更是为了保证代码的准确性和可维护性。想象一下,当你需要调整AD9361的工作频率或者滤波器参数时,你只需要在图形化软件里重新配置,点一下“生成脚本”,然后跑一遍我们的Python转换工具,一份全新的、准确的Verilog初始化代码就生成了。这比手动修改要可靠得多,也符合现代开发的自动化流程。
这个转换过程,听起来好像就是文本处理,但里面有几个关键点需要处理好。第一,原始脚本的格式解析,怎么准确地提取出地址和数据。第二,转换后的Verilog代码结构怎么设计才既清晰又高效。第三,如何让生成的代码能轻松地集成到你现有的FPGA工程里,而不是一个孤立的、难以使用的模块。接下来,我们就一步步拆解,看看这个“翻译官”脚本到底该怎么写。
## 2. 解析原始脚本:读懂AD9361的“配置清单”
拿到那个从评估软件导出的`.txt`文件(比如上一回生成的`fdd_600m.txt`),我们首先得读懂它。用文本编辑器打开,你会看到类似下面的内容:
```
// 这是一些注释
0x000, 0x01
0x001, 0x80
0x003, 0x3C
...
0x3FF, 0xAA
```
它的格式非常规律:每一行通常代表一个寄存器操作。开头可能有一些以`//`开头的注释行,我们需要忽略它们。有效行一般包含两个部分,用逗号分隔。第一部分是寄存器的地址(比如`0x000`),第二部分是要写入该寄存器的数据值(比如`0x01`)。有些行可能包含多个数据,对应连续地址的批量写入,但评估软件默认生成的通常是单地址写入格式,我们按这个来处理就足够了。
我们的Python脚本首先要做的,就是像一个小秘书一样,逐行阅读这份文件,把里面的“干货”——地址和数据——准确地挑出来。这里有一个细节需要注意:地址和数据通常是以十六进制(`0x`开头)表示的,但在Python里处理时,我们可能需要将它们转换成整数,方便后续的格式化和计算。同时,我们必须保留原始的十六进制表示,因为最终在Verilog代码里,我们也更习惯用`8‘h01`这样的格式来书写常量。
我写了一个简单的函数来处理这个解析过程。它会打开文件,跳过空行和注释行,然后用正则表达式或者简单的字符串分割来提取每一行的地址和数据。这里我倾向于使用字符串分割,因为格式固定,更简单直接。我会把解析出来的每一个(地址, 数据)对存到一个列表里,这个列表就是我们整个转换过程的“原材料”。
```python
def parse_init_file(file_path):
"""
解析AD9361初始化脚本文件
:param file_path: 脚本文件路径
:return: 包含(地址, 数据)元组的列表
"""
config_list = []
try:
with open(file_path, 'r') as f:
for line in f:
line = line.strip()
# 跳过空行和注释行
if not line or line.startswith('//'):
continue
# 分割地址和数据
parts = line.split(',')
if len(parts) >= 2:
addr_str = parts[0].strip()
data_str = parts[1].strip()
# 这里可以添加校验,确保是十六进制格式
# 简单处理:直接去除可能的‘0x’前缀,转换为整数,再转回十六进制字符串用于存储
# 我们最终在Verilog中需要的是带位宽的十六进制数,所以这里先存字符串
config_list.append((addr_str, data_str))
except FileNotFoundError:
print(f"错误:找不到文件 {file_path}")
return []
return config_list
```
这个函数跑完,我们就得到了一个干净的配置列表。有了这个列表,我们就可以开始构思怎么把它“编织”成Verilog代码了。
## 3. 设计Verilog代码模板:骨架决定姿态
直接生成一堆散乱的`spi_write`调用虽然也能用,但代码会很难看,也不利于维护和集成。一个好的做法是,把AD9361的初始化过程封装成一个独立的Verilog模块。这个模块应该有一个清晰的状态机,按顺序执行所有寄存器的写入操作,并在完成后给出一个完成信号。这样,在顶层模块中,我们只需要实例化这个初始化模块,然后在收到完成信号后,就知道AD9361已经准备好,可以开始正常的收发工作了。
我设计的模块接口大概是这样的:
```verilog
module ad9361_init (
input wire clk, // 系统时钟
input wire rst_n, // 异步低电平复位
input wire init_start, // 初始化启动信号
output reg init_done, // 初始化完成标志
// SPI Master接口 - 这里假设你有一个SPI Master模块,本模块通过此接口发起写操作
output reg spi_write_start,
output reg [15:0] spi_write_addr,
output reg [7:0] spi_write_data,
input wire spi_write_done
);
```
模块内部,我会用一个状态机来控制初始化流程。状态机很简单,主要就是`IDLE`(空闲)、`WRITE_REG`(写寄存器)、`WAIT_SPI`(等待SPI操作完成)这几个状态。在`WRITE_REG`状态,模块从我们生成的配置列表中,按顺序取出一个地址和数据,赋值给`spi_write_addr`和`spi_write_data`,然后拉高`spi_write_start`,启动一次SPI写操作。随后进入`WAIT_SPI`状态,等待外部的SPI Master模块完成操作(`spi_write_done`拉高)。完成后,再取下一个配置,直到所有寄存器都写完,就拉高`init_done`并回到`IDLE`状态。
那么,Python脚本的任务,就是根据我们解析出来的配置列表,生成这个状态机里“按顺序写入”的具体逻辑。通常,我会在Verilog模块内部定义一个常量数组(比如用`parameter`或`localparam`定义二维数组),或者用一个`case`语句来存储所有的地址-数据对。考虑到FPGA综合工具对大型数组的支持可能因厂商而异,我更喜欢使用一个简单的计数器+大型`case`语句的方式。计数器作为索引,`case`语句根据索引值输出对应的地址和数据。
我们的Python脚本,就需要生成这个庞大的`case`语句块,以及配套的计数器比较逻辑。下面我们就来看看生成逻辑的具体实现。
## 4. 实现Python转换核心逻辑
现在进入最核心的部分:编写Python函数,将配置列表“渲染”成我们设计好的Verilog代码模板。这个过程本质上是字符串拼接,但要注意代码的可读性和格式。
首先,我们需要生成`case`语句的各个分支。每个分支对应一个配置项。假设我们的状态机里有一个寄存器`config_index`作为索引,那么生成的部分看起来应该是这样:
```python
def generate_case_statements(config_list):
"""生成Verilog case语句块"""
case_lines = []
for idx, (addr_hex, data_hex) in enumerate(config_list):
# 将十六进制字符串转换为Verilog格式,例如 16'h0001
# 注意地址是16位,数据是8位
addr_verilog = f"16'h{addr_hex[2:].zfill(4)}" # 去除‘0x’,补零至4位十六进制数
data_verilog = f"8'h{data_hex[2:].zfill(2)}" # 去除‘0x’,补零至2位十六进制数
case_line = f" {idx}: begin\n"
case_line += f" spi_write_addr <= {addr_verilog};\n"
case_line += f" spi_write_data <= {data_verilog};\n"
case_line += f" end"
case_lines.append(case_line)
# 添加一个default分支,防止锁存器生成
case_lines.append(f" default: begin\n")
case_lines.append(f" spi_write_addr <= 16'h0000;\n")
case_lines.append(f" spi_write_data <= 8'h00;\n")
case_lines.append(f" end")
return '\n'.join(case_lines)
```
接下来,我们需要生成整个Verilog模块的完整代码。这包括模块声明、端口定义、内部寄存器、状态机定义,以及把上面生成的`case`语句嵌入到合适的位置。我会把整个模板写成一个多行字符串,然后在其中留出一些“占位符”,比如`{CASE_STATEMENTS}`、`{CONFIG_COUNT}`,再用Python的字符串格式化方法将这些占位符替换成我们动态生成的内容。
```python
def generate_verilog_module(config_list, module_name="ad9361_init"):
"""生成完整的Verilog初始化模块"""
case_block = generate_case_statements(config_list)
config_count = len(config_list)
verilog_template = f"""
`timescale 1ns / 1ps
// ============================================================================
// 自动生成的AD9361初始化模块
// 配置项总数:{config_count}
// 生成工具:Python转换脚本
// ============================================================================
module {module_name} (
input wire clk,
input wire rst_n,
input wire init_start,
output reg init_done,
// SPI Master接口
output reg spi_write_start,
output reg [15:0] spi_write_addr,
output reg [7:0] spi_write_data,
input wire spi_write_done
);
// 状态定义
localparam S_IDLE = 3'd0;
localparam S_WRITE_REG = 3'd1;
localparam S_WAIT_SPI = 3'd2;
reg [2:0] current_state, next_state;
reg [{max(1, (config_count-1).bit_length())-1}:0] config_index; // 索引位宽根据配置数动态计算
// 状态机第一段:同步时序逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
current_state <= S_IDLE;
config_index <= 0;
init_done <= 1'b0;
spi_write_start <= 1'b0;
spi_write_addr <= 16'h0000;
spi_write_data <= 8'h00;
end else begin
current_state <= next_state;
// 在WRITE_REG状态启动SPI写操作
if (next_state == S_WRITE_REG) begin
spi_write_start <= 1'b1;
end else begin
spi_write_start <= 1'b0;
end
// 索引递增逻辑
if (current_state == S_WAIT_SPI && spi_write_done) begin
if (config_index == {config_count-1}) begin
config_index <= 0;
end else begin
config_index <= config_index + 1;
end
end
// 完成标志置位逻辑
if (current_state == S_WAIT_SPI && spi_write_done && config_index == {config_count-1}) begin
init_done <= 1'b1;
end else if (current_state == S_IDLE) begin
init_done <= 1'b0;
end
end
end
// 状态机第二段:组合逻辑,决定下一个状态和输出(case语句部分嵌入此处)
always @(*) begin
next_state = current_state;
case (current_state)
S_IDLE: begin
if (init_start) begin
next_state = S_WRITE_REG;
end
end
S_WRITE_REG: begin
next_state = S_WAIT_SPI;
end
S_WAIT_SPI: begin
if (spi_write_done) begin
if (config_index == {config_count-1}) begin
next_state = S_IDLE;
end else begin
next_state = S_WRITE_REG;
end
end
end
default: next_state = S_IDLE;
endcase
end
// 地址和数据选择逻辑(根据config_index选择)
always @(*) begin
case (config_index)
{CASE_BLOCK_PLACEHOLDER} // 这里将被替换成实际的case语句块
endcase
end
endmodule
"""
# 替换占位符
verilog_code = verilog_template.replace("{CASE_BLOCK_PLACEHOLDER}", case_block)
# 注意:上面的模板中{CASE_BLOCK_PLACEHOLDER}是一个标记,需要被替换。
# 在实际代码中,我通常会用更清晰的占位符,比如 `{case_statements}`,并在生成时用 `format` 方法。
# 这里为了演示,在字符串中直接留了标记。更健壮的做法是:
# template = \"\"\" ... {case_statements} ... \"\"\"
# final_code = template.format(case_statements=case_block, config_count=config_count, ...)
return verilog_code
```
注意,上面的模板字符串和替换过程是一个简化的示意。在实际编写时,你需要构建一个完整的、可综合的Verilog代码字符串,并确保所有信号(如`config_index`的位宽)都能根据配置列表的长度正确计算。`config_index`的位宽需要用`math.ceil(math.log2(config_count))`来计算,以确保能索引所有配置项。
## 5. 集成与使用:让脚本跑起来
有了解析函数和生成函数,我们只需要一个主函数把它们串起来,并处理一些命令行参数,比如输入文件路径、输出文件路径、模块名等。这样,我们就可以在终端里像下面这样运行脚本了:
```bash
python ad9361_config_converter.py -i fdd_600m.txt -o ad9361_init.v -m ad9361_init
```
脚本会读取`fdd_600m.txt`,解析它,生成Verilog代码,并保存到`ad9361_init.v`文件中。生成的这个`.v`文件就是你FPGA工程的一部分了。
在你顶层的FPGA设计中,你需要实例化这个`ad9361_init`模块,并把它和你自己的SPI Master模块连接起来。连接关系很简单:`ad9361_init`的`spi_write_start`、`addr`、`data`连接到SPI Master模块的对应控制端口;SPI Master模块的`done`信号连回`ad9361_init`的`spi_write_done`。上电后,在合适的时机(比如FPGA配置完成后)给`init_start`一个脉冲,模块就会自动完成所有初始化工作,然后拉高`init_done`。之后,你的AD9361就可以正常进行射频收发了。
这里有一个非常重要的**实测经验**分享:第一次用自动生成的代码初始化AD9361,未必能一次成功。最常见的问题出在**时序**上,尤其是前面提到的`Delay Cell Control`相关设置。我们的脚本忠实地转换了寄存器值,但如果这个值不适合你的硬件板卡,数据通信就会失败。这时候,不要怀疑脚本,而是应该回到评估软件里,调整“数字接口设置”中的`Delay Cell Control`参数,重新生成脚本,再用我们的工具转换,更新FPGA代码。通常需要迭代测试几次,才能找到最优值。这个过程也体现了自动化工具的优势:调整参数、重新生成代码非常迅速,让你能专注于硬件调试本身。
## 6. 扩展思路:让工具更加强大
我们目前实现的是一个基础版本,已经能解决从脚本到代码的核心转换问题。但你可以根据实际项目需求,让它变得更强大。
**1. 支持批量写入优化:**
评估软件生成的脚本是单地址写入,但AD9361的SPI接口支持多字节连续写入。如果连续地址的寄存器需要配置,我们可以让脚本智能地合并这些操作,生成一条SPI写命令,从而减少初始化时间。这需要在解析时判断地址是否连续,并进行合并处理。
**2. 添加配置校验与报告:**
脚本可以在解析后,打印出一份摘要报告,比如总共解析了多少个有效配置项,地址范围是什么,有没有发现明显可疑的配置值(比如保留位被写入)。这能帮助你在早期发现配置问题。
**3. 生成可读性更强的注释:**
在生成的Verilog代码中,每个`case`分支旁边,可以添加原始脚本中的注释(如果原始脚本有的话),或者根据地址映射表,自动标注这个寄存器的大致功能(例如 `// Register 0x000: Chip ID`)。这需要你维护一个AD9361寄存器的描述数据库。
**4. 支持多种输出格式:**
除了生成状态机形式的Verilog模块,也可以选择输出为简单的任务(task)列表,或者直接生成用于仿真测试的测试向量文件,方便在仿真环境中验证初始化序列。
把这些功能一点点加进去,这个Python脚本就会从一个简单的转换器,进化成你AD9361开发工作流中的一个强大助力。它节省的不仅仅是时间,更是减少了人为失误,让整个开发过程更加稳健和可重复。