Python生成测试向量:自动生成Verilog仿真激励,告别手写testbench数据
测试向量是FPGA验证的重要组成部分。使用Python可以方便地生成各种测试数据,包括随机数据、特定模式、边界条件等,并导出为Verilog/VHDL仿真可用的格式。
1. 测试向量基础
import numpy as np
from typing import List, Tuple
from dataclasses import dataclass
@dataclass
class TestVector:
"""测试向量"""
inputs: dict # 输入信号
outputs: dict # 期望输出
description: str = ""
class TestVectorGenerator:
"""测试向量生成器基类"""
def __init__(self, seed: int = None):
if seed is not None:
np.random.seed(seed)
self.vectors = []
def add_vector(self, inputs: dict, outputs: dict, desc: str = ""):
"""添加测试向量"""
self.vectors.append(TestVector(inputs, outputs, desc))
def generate_random(self, count: int, input_widths: dict,
output_func=None) -> List[TestVector]:
"""生成随机测试向量"""
vectors = []
for i in range(count):
inputs = {}
for name, width in input_widths.items():
max_val = (1 << width) - 1
inputs[name] = np.random.randint(0, max_val + 1)
outputs = output_func(inputs) if output_func else {}
vectors.append(TestVector(inputs, outputs, f"Random #{i}"))
self.vectors.extend(vectors)
return vectors
def generate_exhaustive(self, input_widths: dict,
output_func=None) -> List[TestVector]:
"""生成穷举测试向量(适用于小位宽)"""
from itertools import product
ranges = []
names = list(input_widths.keys())
for name in names:
width = input_widths[name]
ranges.append(range(1 << width))
vectors = []
for values in product(*ranges):
inputs = dict(zip(names, values))
outputs = output_func(inputs) if output_func else {}
vectors.append(TestVector(inputs, outputs))
self.vectors.extend(vectors)
return vectors
def generate_boundary(self, input_widths: dict,
output_func=None) -> List[TestVector]:
"""生成边界测试向量"""
vectors = []
for name, width in input_widths.items():
max_val = (1 << width) - 1
# 边界值
boundary_values = [0, 1, max_val - 1, max_val, max_val // 2]
for val in boundary_values:
inputs = {n: 0 for n in input_widths.keys()}
inputs[name] = val
outputs = output_func(inputs) if output_func else {}
vectors.append(TestVector(inputs, outputs, f"{name}={val}"))
self.vectors.extend(vectors)
return vectors
# 使用示例
def adder_output(inputs):
"""加法器期望输出"""
result = inputs['a'] + inputs['b']
return {'sum': result & 0xFF, 'carry': (result >> 8) & 1}
gen = TestVectorGenerator(seed=42)
gen.generate_random(10, {'a': 8, 'b': 8}, adder_output)
gen.generate_boundary({'a': 8, 'b': 8}, adder_output)
for v in gen.vectors[:5]:
print(f"a={v.inputs['a']:3d}, b={v.inputs['b']:3d} -> "
f"sum={v.outputs['sum']:3d}, carry={v.outputs['carry']}")
2. 数据格式转换
import numpy as np
class DataFormatter:
"""数据格式转换器"""
@staticmethod
def int_to_bin(value: int, width: int) -> str:
"""整数转二进制字符串"""
if value < 0:
# 补码表示
value = (1 << width) + value
return format(value & ((1 << width) - 1), f'0{width}b')
@staticmethod
def int_to_hex(value: int, width: int) -> str:
"""整数转十六进制字符串"""
hex_width = (width + 3) // 4
if value < 0:
value = (1 << width) + value
return format(value & ((1 << width) - 1), f'0{hex_width}X')
@staticmethod
def bin_to_int(bin_str: str, signed: bool = False) -> int:
"""二进制字符串转整数"""
value = int(bin_str, 2)
if signed and bin_str[0] == '1':
value -= (1 << len(bin_str))
return value
@staticmethod
def hex_to_int(hex_str: str, width: int = None, signed: bool = False) -> int:
"""十六进制字符串转整数"""
value = int(hex_str, 16)
if signed and width:
if value >= (1 << (width - 1)):
value -= (1 << width)
return value
@staticmethod
def float_to_fixed(value: float, int_bits: int, frac_bits: int) -> int:
"""浮点数转定点数"""
scale = 1 << frac_bits
fixed = int(round(value * scale))
# 限制范围
max_val = (1 << (int_bits + frac_bits - 1)) - 1
min_val = -(1 << (int_bits + frac_bits - 1))
fixed = max(min_val, min(max_val, fixed))
# 转为无符号表示
if fixed < 0:
fixed = (1 << (int_bits + frac_bits)) + fixed
return fixed
@staticmethod
def fixed_to_float(value: int, int_bits: int, frac_bits: int) -> float:
"""定点数转浮点数"""
total_bits = int_bits + frac_bits
# 检查符号位
if value >= (1 << (total_bits - 1)):
value -= (1 << total_bits)
return value / (1 << frac_bits)
@staticmethod
def array_to_mem(data: np.ndarray, width: int,
format: str = 'hex') -> List[str]:
"""数组转存储器初始化格式"""
lines = []
for value in data:
if format == 'hex':
lines.append(DataFormatter.int_to_hex(int(value), width))
else:
lines.append(DataFormatter.int_to_bin(int(value), width))
return lines
# 测试
fmt = DataFormatter()
print(f"255 -> bin: {fmt.int_to_bin(255, 8)}")
print(f"255 -> hex: {fmt.int_to_hex(255, 8)}")
print(f"-1 (8bit) -> bin: {fmt.int_to_bin(-1, 8)}")
print(f"0.75 -> Q4.4: {fmt.int_to_hex(fmt.float_to_fixed(0.75, 4, 4), 8)}")
print(f"Q4.4 0x0C -> float: {fmt.fixed_to_float(0x0C, 4, 4)}")
3. 常用测试模式
import numpy as np
from typing import List
class TestPatternGenerator:
"""测试模式生成器"""
@staticmethod
def walking_ones(width: int) -> List[int]:
"""行走的1"""
return [1 << i for i in range(width)]
@staticmethod
def walking_zeros(width: int) -> List[int]:
"""行走的0"""
mask = (1 << width) - 1
return [mask ^ (1 << i) for i in range(width)]
@staticmethod
def all_ones(width: int) -> int:
"""全1"""
return (1 << width) - 1
@staticmethod
def all_zeros(width: int) -> int:
"""全0"""
return 0
@staticmethod
def alternating(width: int) -> List[int]:
"""交替模式"""
pattern1 = int('10' * (width // 2) + '1' * (width % 2), 2)
pattern2 = int('01' * (width // 2) + '0' * (width % 2), 2)
return [pattern1, pattern2]
@staticmethod
def counting(width: int, start: int = 0, step: int = 1) -> List[int]:
"""计数模式"""
max_val = 1 << width
values = []
val = start
while val < max_val:
values.append(val)
val += step
return values
@staticmethod
def prbs(width: int, length: int, taps: List[int] = None) -> List[int]:
"""伪随机二进制序列"""
if taps is None:
# 默认抽头(适用于常见位宽)
default_taps = {
7: [7, 6],
8: [8, 6, 5, 4],
15: [15, 14],
16: [16, 15, 13, 4],
23: [23, 18],
31: [31, 28],
}
taps = default_taps.get(width, [width, width-1])
state = 1
values = []
for _ in range(length):
values.append(state)
# 计算反馈
feedback = 0
for tap in taps:
feedback ^= (state >> (tap - 1)) & 1
state = ((state << 1) | feedback) & ((1 << width) - 1)
return values
@staticmethod
def gray_code(width: int) -> List[int]:
"""格雷码序列"""
values = []
for i in range(1 << width):
gray = i ^ (i >> 1)
values.append(gray)
return values
@staticmethod
def ramp(width: int, start: int = 0, end: int = None) -> List[int]:
"""斜坡"""
if end is None:
end = (1 << width) - 1
return list(range(start, end + 1))
@staticmethod
def sine_samples(width: int, num_samples: int,
amplitude: float = 1.0) -> List[int]:
"""正弦波采样"""
max_val = (1 << (width - 1)) - 1
t = np.linspace(0, 2 * np.pi, num_samples, endpoint=False)
samples = np.round(amplitude * max_val * np.sin(t)).astype(int)
# 转为无符号
samples = np.where(samples < 0, samples + (1 << width), samples)
return samples.tolist()
# 测试
pat = TestPatternGenerator()
print(f"Walking ones (8-bit): {[bin(x) for x in pat.walking_ones(8)]}")
print(f"Alternating (8-bit): {[bin(x) for x in pat.alternating(8)]}")
print(f"Gray code (4-bit): {pat.gray_code(4)}")
print(f"PRBS-7 (first 10): {pat.prbs(7, 10)}")
4. 导出格式
from pathlib import Path
from typing import List, Dict
import numpy as np
class TestVectorExporter:
"""测试向量导出器"""
@staticmethod
def to_mem_file(data: List[int], width: int, filename: str,
format: str = 'hex', radix: int = None):
"""
导出为.mem文件(Vivado可读)
Args:
data: 数据列表
width: 数据位宽
filename: 输出文件名
format: 'hex' 或 'bin'
radix: 基数注释(可选)
"""
lines = []
if radix:
lines.append(f'// Radix = {radix}')
for i, value in enumerate(data):
if format == 'hex':
hex_width = (width + 3) // 4
lines.append(f'{value:0{hex_width}X}')
else:
lines.append(f'{value:0{width}b}')
Path(filename).write_text('\n'.join(lines))
print(f"已导出到 {filename},共 {len(data)} 条数据")
@staticmethod
def to_coe_file(data: List[int], width: int, filename: str,
radix: int = 16):
"""
导出为.coe文件(Xilinx IP初始化)
Args:
data: 数据列表
width: 数据位宽
filename: 输出文件名
radix: 基数(2, 10, 16)
"""
lines = [
f'; COE file generated by Python',
f'; Data width: {width} bits',
f'; Data depth: {len(data)}',
f'',
f'memory_initialization_radix={radix};',
f'memory_initialization_vector='
]
hex_width = (width + 3) // 4
for i, value in enumerate(data):
if radix == 16:
val_str = f'{value:0{hex_width}X}'
elif radix == 2:
val_str = f'{value:0{width}b}'
else:
val_str = str(value)
if i < len(data) - 1:
lines.append(f'{val_str},')
else:
lines.append(f'{val_str};')
Path(filename).write_text('\n'.join(lines))
print(f"已导出到 {filename}")
@staticmethod
def to_verilog_array(data: List[int], width: int,
array_name: str) -> str:
"""
生成Verilog数组初始化代码
"""
lines = [
f'// Test vector array: {array_name}',
f'// Width: {width} bits, Depth: {len(data)}',
f'reg [{width-1}:0] {array_name} [0:{len(data)-1}];',
f'',
f'initial begin'
]
for i, value in enumerate(data):
lines.append(f" {array_name}[{i}] = {width}'h{value:0{(width+3)//4}X};")
lines.append('end')
return '\n'.join(lines)
@staticmethod
def to_verilog_testbench(vectors: List['TestVector'],
module_name: str,
input_widths: Dict[str, int],
output_widths: Dict[str, int]) -> str:
"""
生成Verilog测试平台
"""
lines = [
f'`timescale 1ns / 1ps',
f'',
f'module {module_name}_tb;',
f''
]
# 声明信号
lines.append('// 输入信号')
for name, width in input_widths.items():
lines.append(f'reg [{width-1}:0] {name};')
lines.append('')
lines.append('// 输出信号')
for name, width in output_widths.items():
lines.append(f'wire [{width-1}:0] {name};')
# 实例化DUT
lines.append('')
lines.append('// 实例化被测模块')
lines.append(f'{module_name} dut (')
all_ports = list(input_widths.keys()) + list(output_widths.keys())
for i, port in enumerate(all_ports):
comma = ',' if i < len(all_ports) - 1 else ''
lines.append(f' .{port}({port}){comma}')
lines.append(');')
# 测试过程
lines.append('')
lines.append('// 测试过程')
lines.append('initial begin')
lines.append(' $display("开始测试...");')
lines.append('')
for i, vec in enumerate(vectors):
lines.append(f' // 测试向量 #{i}: {vec.description}')
for name, value in vec.inputs.items():
width = input_widths[name]
lines.append(f" {name} = {width}'h{value:0{(width+3)//4}X};")
lines.append(' #10;')
# 检查输出
for name, expected in vec.outputs.items():
width = output_widths[name]
lines.append(f' if ({name} !== {width}\'h{expected:0{(width+3)//4}X}) begin')
lines.append(f' $display("ERROR: {name} mismatch at vector {i}");')
lines.append(f' $display(" Expected: %h, Got: %h", {width}\'h{expected:0{(width+3)//4}X}, {name});')
lines.append(f' end')
lines.append('')
lines.append(' $display("测试完成");')
lines.append(' $finish;')
lines.append('end')
lines.append('')
lines.append('endmodule')
return '\n'.join(lines)
@staticmethod
def to_csv(vectors: List['TestVector'], filename: str):
"""导出为CSV文件"""
if not vectors:
return
# 获取所有字段名
input_names = list(vectors[0].inputs.keys())
output_names = list(vectors[0].outputs.keys())
lines = [','.join(input_names + output_names)]
for vec in vectors:
values = [str(vec.inputs[n]) for n in input_names]
values += [str(vec.outputs[n]) for n in output_names]
lines.append(','.join(values))
Path(filename).write_text('\n'.join(lines))
print(f"已导出到 {filename}")
5. 协议测试数据
import numpy as np
from typing import List
class ProtocolTestGenerator:
"""协议测试数据生成器"""
@staticmethod
def uart_frame(data: int, parity: str = 'N') -> List[int]:
"""
生成UART帧
Args:
data: 8位数据
parity: 'N'无校验, 'E'偶校验, 'O'奇校验
"""
bits = [0] # 起始位
# 数据位(LSB first)
for i in range(8):
bits.append((data >> i) & 1)
# 校验位
if parity != 'N':
parity_bit = bin(data).count('1') % 2
if parity == 'E':
bits.append(parity_bit)
else:
bits.append(1 - parity_bit)
bits.append(1) # 停止位
return bits
@staticmethod
def spi_transaction(mosi_data: List[int], width: int = 8) -> dict:
"""
生成SPI事务
Args:
mosi_data: MOSI数据列表
width: 数据位宽
"""
# 生成时钟和片选
total_bits = len(mosi_data) * width
sck = []
cs_n = []
mosi = []
# 片选拉低
cs_n.extend([1, 1, 0])
sck.extend([0, 0, 0])
mosi.extend([0, 0, 0])
# 数据传输
for byte in mosi_data:
for i in range(width - 1, -1, -1):
bit = (byte >> i) & 1
# SCK上升沿
sck.extend([0, 1])
cs_n.extend([0, 0])
mosi.extend([bit, bit])
# 片选拉高
cs_n.extend([0, 1, 1])
sck.extend([0, 0, 0])
mosi.extend([0, 0, 0])
return {'sck': sck, 'cs_n': cs_n, 'mosi': mosi}
@staticmethod
def i2c_write(device_addr: int, reg_addr: int, data: List[int]) -> dict:
"""
生成I2C写事务
"""
bits = {
'sda': [],
'scl': [],
'description': []
}
def add_byte(byte, desc):
# 发送8位数据
for i in range(7, -1, -1):
bit = (byte >> i) & 1
bits['sda'].extend([bit, bit])
bits['scl'].extend([0, 1])
# ACK
bits['sda'].extend([0, 0]) # 从机ACK
bits['scl'].extend([0, 1])
bits['description'].append(desc)
# 起始条件
bits['sda'].extend([1, 0])
bits['scl'].extend([1, 1])
# 设备地址 + 写
add_byte((device_addr << 1) | 0, f'Addr: 0x{device_addr:02X} + W')
# 寄存器地址
add_byte(reg_addr, f'Reg: 0x{reg_addr:02X}')
# 数据
for i, d in enumerate(data):
add_byte(d, f'Data[{i}]: 0x{d:02X}')
# 停止条件
bits['sda'].extend([0, 1])
bits['scl'].extend([1, 1])
return bits
@staticmethod
def axi_write(addr: int, data: int, strb: int = 0xF) -> dict:
"""生成AXI写事务"""
return {
'awaddr': addr,
'awvalid': 1,
'wdata': data,
'wstrb': strb,
'wvalid': 1,
'bready': 1
}
@staticmethod
def axi_read(addr: int) -> dict:
"""生成AXI读事务"""
return {
'araddr': addr,
'arvalid': 1,
'rready': 1
}
# 测试
proto = ProtocolTestGenerator()
# UART帧
uart = proto.uart_frame(0x55, 'E')
print(f"UART帧 (0x55, 偶校验): {uart}")
# SPI事务
spi = proto.spi_transaction([0xAA, 0x55])
print(f"SPI MOSI: {spi['mosi'][:20]}...")
# I2C写
i2c = proto.i2c_write(0x50, 0x00, [0x12, 0x34])
print(f"I2C SDA bits: {len(i2c['sda'])}")
6. 信号生成
import numpy as np
from typing import Tuple
class SignalGenerator:
"""信号生成器"""
@staticmethod
def sine(freq: float, fs: float, duration: float,
amplitude: float = 1.0, phase: float = 0,
bits: int = 16) -> np.ndarray:
"""
生成正弦波
Args:
freq: 信号频率
fs: 采样率
duration: 持续时间
amplitude: 幅度 (0-1)
phase: 初始相位
bits: 量化位数
"""
t = np.arange(0, duration, 1/fs)
signal = amplitude * np.sin(2 * np.pi * freq * t + phase)
# 量化
max_val = (1 << (bits - 1)) - 1
quantized = np.round(signal * max_val).astype(np.int32)
# 转为无符号
quantized = np.where(quantized < 0, quantized + (1 << bits), quantized)
return quantized.astype(np.uint32)
@staticmethod
def chirp(f0: float, f1: float, fs: float, duration: float,
bits: int = 16) -> np.ndarray:
"""
生成扫频信号
Args:
f0: 起始频率
f1: 结束频率
fs: 采样率
duration: 持续时间
bits: 量化位数
"""
t = np.arange(0, duration, 1/fs)
k = (f1 - f0) / duration
signal = np.sin(2 * np.pi * (f0 * t + 0.5 * k * t**2))
max_val = (1 << (bits - 1)) - 1
quantized = np.round(signal * max_val).astype(np.int32)
quantized = np.where(quantized < 0, quantized + (1 << bits), quantized)
return quantized.astype(np.uint32)
@staticmethod
def noise(length: int, bits: int = 16,
distribution: str = 'uniform') -> np.ndarray:
"""
生成噪声
Args:
length: 采样点数
bits: 量化位数
distribution: 'uniform' 或 'gaussian'
"""
max_val = (1 << bits) - 1
if distribution == 'uniform':
return np.random.randint(0, max_val + 1, length, dtype=np.uint32)
else:
# 高斯噪声,截断到有效范围
signal = np.random.randn(length)
signal = np.clip(signal, -3, 3) / 3 # 归一化到[-1, 1]
half_max = (1 << (bits - 1)) - 1
quantized = np.round(signal * half_max).astype(np.int32)
quantized = np.where(quantized < 0, quantized + (1 << bits), quantized)
return quantized.astype(np.uint32)
@staticmethod
def pulse(width: int, period: int, count: int,
high: int = 1, low: int = 0) -> np.ndarray:
"""
生成脉冲信号
Args:
width: 脉冲宽度
period: 周期
count: 周期数
high: 高电平值
low: 低电平值
"""
one_period = [high] * width + [low] * (period - width)
return np.array(one_period * count, dtype=np.uint32)
@staticmethod
def pwm(duty_cycles: List[float], period: int,
bits: int = 1) -> np.ndarray:
"""
生成PWM信号
Args:
duty_cycles: 占空比列表 (0-1)
period: PWM周期
bits: 输出位宽
"""
high = (1 << bits) - 1
signal = []
for duty in duty_cycles:
on_time = int(period * duty)
off_time = period - on_time
signal.extend([high] * on_time + [0] * off_time)
return np.array(signal, dtype=np.uint32)
# 测试
sig = SignalGenerator()
# 生成1kHz正弦波
sine_data = sig.sine(freq=1000, fs=100000, duration=0.01, bits=12)
print(f"正弦波采样点数: {len(sine_data)}")
# 生成扫频信号
chirp_data = sig.chirp(f0=100, f1=10000, fs=100000, duration=0.1, bits=12)
print(f"扫频信号采样点数: {len(chirp_data)}")
# 生成PWM
pwm_data = sig.pwm([0.25, 0.5, 0.75], period=100)
print(f"PWM采样点数: {len(pwm_data)}")
7. 结果比对
import numpy as np
from pathlib import Path
from typing import Tuple, List, Optional
from dataclasses import dataclass
@dataclass
class CompareResult:
"""比对结果"""
match: bool
total_count: int
mismatch_count: int
first_mismatch_idx: Optional[int]
mismatch_indices: List[int]
max_diff: float
mean_diff: float
class ResultComparator:
"""结果比对器"""
@staticmethod
def compare_exact(expected: np.ndarray, actual: np.ndarray) -> CompareResult:
"""精确比对"""
if expected.shape != actual.shape:
return CompareResult(
match=False,
total_count=len(expected),
mismatch_count=len(expected),
first_mismatch_idx=0,
mismatch_indices=list(range(len(expected))),
max_diff=float('inf'),
mean_diff=float('inf')
)
diff = expected != actual
mismatch_indices = np.where(diff)[0].tolist()
return CompareResult(
match=len(mismatch_indices) == 0,
total_count=len(expected),
mismatch_count=len(mismatch_indices),
first_mismatch_idx=mismatch_indices[0] if mismatch_indices else None,
mismatch_indices=mismatch_indices,
max_diff=0 if len(mismatch_indices) == 0 else float('inf'),
mean_diff=0 if len(mismatch_indices) == 0 else float('inf')
)
@staticmethod
def compare_tolerance(expected: np.ndarray, actual: np.ndarray,
tolerance: float = 0) -> CompareResult:
"""容差比对"""
diff = np.abs(expected.astype(float) - actual.astype(float))
mismatch_mask = diff > tolerance
mismatch_indices = np.where(mismatch_mask)[0].tolist()
return CompareResult(
match=len(mismatch_indices) == 0,
total_count=len(expected),
mismatch_count=len(mismatch_indices),
first_mismatch_idx=mismatch_indices[0] if mismatch_indices else None,
mismatch_indices=mismatch_indices,
max_diff=float(np.max(diff)),
mean_diff=float(np.mean(diff))
)
@staticmethod
def load_simulation_output(filename: str, width: int,
format: str = 'hex') -> np.ndarray:
"""加载仿真输出文件"""
lines = Path(filename).read_text().strip().split('\n')
values = []
for line in lines:
line = line.strip()
if not line or line.startswith('//') or line.startswith('#'):
continue
if format == 'hex':
values.append(int(line, 16))
else:
values.append(int(line, 2))
return np.array(values, dtype=np.uint32)
@staticmethod
def generate_report(result: CompareResult,
expected: np.ndarray = None,
actual: np.ndarray = None) -> str:
"""生成比对报告"""
lines = [
"="*50,
"比对报告",
"="*50,
f"结果: {'通过' if result.match else '失败'}",
f"总数: {result.total_count}",
f"不匹配数: {result.mismatch_count}",
f"最大差异: {result.max_diff}",
f"平均差异: {result.mean_diff:.4f}",
]
if not result.match and expected is not None and actual is not None:
lines.append("")
lines.append("前10个不匹配项:")
for idx in result.mismatch_indices[:10]:
lines.append(f" [{idx}] 期望: {expected[idx]}, 实际: {actual[idx]}")
lines.append("="*50)
return '\n'.join(lines)
# 测试
comp = ResultComparator()
expected = np.array([1, 2, 3, 4, 5, 6, 7, 8])
actual = np.array([1, 2, 3, 4, 5, 7, 7, 8])
result = comp.compare_exact(expected, actual)
print(comp.generate_report(result, expected, actual))
result2 = comp.compare_tolerance(expected, actual, tolerance=1)
print(f"\n容差比对 (tolerance=1): {'通过' if result2.match else '失败'}")
8. 实战案例
案例:FIR滤波器测试向量生成
"""
实战案例:FIR滤波器测试向量生成
"""
import numpy as np
from pathlib import Path
from scipy import signal as scipy_signal
class FIRTestGenerator:
"""FIR滤波器测试向量生成器"""
def __init__(self, num_taps: int, cutoff: float, fs: float,
data_width: int = 16, coef_width: int = 16):
self.num_taps = num_taps
self.cutoff = cutoff
self.fs = fs
self.data_width = data_width
self.coef_width = coef_width
# 生成滤波器系数
self.coefficients = self._generate_coefficients()
self.coef_quantized = self._quantize_coefficients()
def _generate_coefficients(self) -> np.ndarray:
"""生成滤波器系数"""
normalized_cutoff = self.cutoff / (self.fs / 2)
return scipy_signal.firwin(self.num_taps, normalized_cutoff)
def _quantize_coefficients(self) -> np.ndarray:
"""量化系数"""
scale = (1 << (self.coef_width - 1)) - 1
quantized = np.round(self.coefficients * scale).astype(np.int32)
# 转为无符号
quantized = np.where(quantized < 0,
quantized + (1 << self.coef_width),
quantized)
return quantized.astype(np.uint32)
def generate_input(self, signal_type: str, length: int) -> np.ndarray:
"""生成输入信号"""
max_val = (1 << (self.data_width - 1)) - 1
if signal_type == 'impulse':
signal = np.zeros(length)
signal[0] = 1.0
elif signal_type == 'step':
signal = np.ones(length)
elif signal_type == 'sine':
t = np.arange(length) / self.fs
signal = np.sin(2 * np.pi * self.cutoff * 0.5 * t)
elif signal_type == 'noise':
signal = np.random.randn(length)
signal = signal / np.max(np.abs(signal))
elif signal_type == 'mixed':
t = np.arange(length) / self.fs
# 低频 + 高频
signal = (np.sin(2 * np.pi * self.cutoff * 0.3 * t) +
0.5 * np.sin(2 * np.pi * self.cutoff * 3 * t))
signal = signal / np.max(np.abs(signal))
else:
signal = np.zeros(length)
# 量化
quantized = np.round(signal * max_val).astype(np.int32)
quantized = np.where(quantized < 0,
quantized + (1 << self.data_width),
quantized)
return quantized.astype(np.uint32)
def calculate_expected_output(self, input_data: np.ndarray) -> np.ndarray:
"""计算期望输出(使用量化系数)"""
# 转换为有符号
input_signed = input_data.astype(np.int64)
input_signed = np.where(input_signed >= (1 << (self.data_width - 1)),
input_signed - (1 << self.data_width),
input_signed)
coef_signed = self.coef_quantized.astype(np.int64)
coef_signed = np.where(coef_signed >= (1 << (self.coef_width - 1)),
coef_signed - (1 << self.coef_width),
coef_signed)
# 卷积
output = np.convolve(input_signed, coef_signed, mode='full')
# 截断到输出位宽
output_width = self.data_width + self.coef_width
output = output >> (self.coef_width - 1) # 缩放
output = np.clip(output,
-(1 << (self.data_width - 1)),
(1 << (self.data_width - 1)) - 1)
# 转为无符号
output = np.where(output < 0,
output + (1 << self.data_width),
output)
return output[:len(input_data)].astype(np.uint32)
def export_all(self, output_dir: str, input_length: int = 1000):
"""导出所有测试文件"""
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
# 导出系数
exporter = TestVectorExporter()
exporter.to_coe_file(
self.coef_quantized.tolist(),
self.coef_width,
str(output_path / 'fir_coefficients.coe')
)
exporter.to_mem_file(
self.coef_quantized.tolist(),
self.coef_width,
str(output_path / 'fir_coefficients.mem')
)
# 生成各种测试信号
test_signals = ['impulse', 'step', 'sine', 'noise', 'mixed']
for sig_type in test_signals:
input_data = self.generate_input(sig_type, input_length)
output_data = self.calculate_expected_output(input_data)
exporter.to_mem_file(
input_data.tolist(),
self.data_width,
str(output_path / f'input_{sig_type}.mem')
)
exporter.to_mem_file(
output_data.tolist(),
self.data_width,
str(output_path / f'expected_{sig_type}.mem')
)
# 生成参数文件
params = {
'NUM_TAPS': self.num_taps,
'DATA_WIDTH': self.data_width,
'COEF_WIDTH': self.coef_width,
'CUTOFF_FREQ': self.cutoff,
'SAMPLE_RATE': self.fs
}
param_lines = [f'// FIR Filter Parameters']
for name, value in params.items():
param_lines.append(f'parameter {name} = {value};')
(output_path / 'fir_params.vh').write_text('\n'.join(param_lines))
print(f"所有文件已导出到 {output_dir}")
# 使用示例
def main():
# 创建16阶低通滤波器,截止频率1kHz,采样率10kHz
fir_gen = FIRTestGenerator(
num_taps=16,
cutoff=1000,
fs=10000,
data_width=16,
coef_width=16
)
# 导出测试文件
fir_gen.export_all('fir_test_vectors', input_length=500)
# 显示系数
print("\n滤波器系数(量化后):")
for i, coef in enumerate(fir_gen.coef_quantized):
print(f" h[{i:2d}] = 0x{coef:04X}")
main()
9. 总结
🔑 核心要点
| 知识点 | 要点 |
|---|---|
| 测试模式 | Walking 1/0, PRBS, Gray码, 边界值 |
| 数据格式 | 二进制、十六进制、定点数转换 |
| 导出格式 | .mem, .coe, Verilog数组 |
| 协议数据 | UART, SPI, I2C帧生成 |
| 信号生成 | 正弦波、扫频、噪声、PWM |
| 结果比对 | 精确比对、容差比对 |
✅ 学习检查清单
- 能生成常用测试模式
- 能进行数据格式转换
- 能导出为仿真可用格式
- 能生成协议测试数据
- 能比对仿真结果
📖 下一步学习
掌握了测试向量生成后,让我们学习日志解析与报告生成:
常见问题 FAQ
💬 $readmemh和$readmemb有什么区别?
$readmemh读十六进制文件,$readmemb读二进制文件。Python生成hex文件用format(val, '04x'),生成bin文件用format(val, '016b')。推荐用hex格式,更紧凑。
💬 怎么生成定点数测试向量?
浮点数转定点:乘以2^小数位数再取整。比如Q8.8格式:int(round(float_val * 256))。注意处理负数(补码)和溢出。
� 系列导航
- 上一篇:26 - Python调用Vivado自动化
- 当前:27 - Python生成测试向量
- 下一篇:28 - Python日志解析与报告生成