Veloris.
返回索引
设计实战 2026-02-14

Python生成测试向量:自动生成Verilog仿真激励,告别手写testbench数据

2 分钟
475 words

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))。注意处理负数(补码)和溢出。


系列导航

End of file.