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

Python调用Vivado自动化:subprocess+Tcl脚本,一键综合实现生成比特流

2 分钟
468 words

Python调用Vivado自动化:subprocess+Tcl脚本,一键综合实现生成比特流

Vivado是Xilinx FPGA的主要开发工具。通过Python调用Vivado的Tcl接口,可以实现工程创建、综合、实现、生成比特流等操作的自动化,提高开发效率。


1. Vivado命令行基础

# Vivado命令行模式
vivado -mode batch -source script.tcl      # 批处理模式
vivado -mode tcl                            # Tcl交互模式
vivado -mode gui                            # GUI模式

# 常用参数
vivado -mode batch -source script.tcl -log run.log -journal run.jou
vivado -mode batch -source script.tcl -notrace  # 不显示Tcl命令

# 环境变量
# Windows: 添加 Vivado安装路径\bin 到PATH
# 例如: C:\Xilinx\Vivado\2023.1\bin
# 基本Tcl命令示例 (script.tcl)

# 创建工程
create_project my_project ./my_project -part xc7a35tcpg236-1

# 添加源文件
add_files -norecurse {./src/top.v ./src/module1.v}
add_files -fileset constrs_1 -norecurse ./constraints/pins.xdc

# 设置顶层模块
set_property top top_module [current_fileset]

# 运行综合
launch_runs synth_1 -jobs 4
wait_on_run synth_1

# 运行实现
launch_runs impl_1 -jobs 4
wait_on_run impl_1

# 生成比特流
launch_runs impl_1 -to_step write_bitstream -jobs 4
wait_on_run impl_1

# 关闭工程
close_project

2. Python调用Vivado

import subprocess
import os
from pathlib import Path

class VivadoRunner:
    """Vivado运行器"""
    
    def __init__(self, vivado_path: str = None):
        """
        初始化
        
        Args:
            vivado_path: Vivado可执行文件路径,如果为None则从PATH查找
        """
        if vivado_path:
            self.vivado_path = vivado_path
        else:
            # 尝试从PATH查找
            self.vivado_path = 'vivado'
        
        self.check_vivado()
    
    def check_vivado(self):
        """检查Vivado是否可用"""
        try:
            result = subprocess.run(
                [self.vivado_path, '-version'],
                capture_output=True,
                text=True,
                timeout=30
            )
            if result.returncode == 0:
                version = result.stdout.strip().split('\n')[0]
                print(f"Vivado版本:{version}")
            else:
                raise RuntimeError("Vivado返回错误")
        except FileNotFoundError:
            raise RuntimeError(f"找不到Vivado:{self.vivado_path}")
        except subprocess.TimeoutExpired:
            raise RuntimeError("Vivado响应超时")
    
    def run_tcl(self, tcl_script: str, log_file: str = None, 
                working_dir: str = None) -> subprocess.CompletedProcess:
        """
        运行Tcl脚本
        
        Args:
            tcl_script: Tcl脚本路径
            log_file: 日志文件路径
            working_dir: 工作目录
        """
        cmd = [
            self.vivado_path,
            '-mode', 'batch',
            '-source', tcl_script,
            '-notrace'
        ]
        
        if log_file:
            cmd.extend(['-log', log_file])
        
        print(f"运行Vivado:{' '.join(cmd)}")
        
        result = subprocess.run(
            cmd,
            cwd=working_dir,
            capture_output=True,
            text=True
        )
        
        if result.returncode != 0:
            print(f"Vivado错误:\n{result.stderr}")
        
        return result
    
    def run_tcl_commands(self, commands: list, working_dir: str = None) -> subprocess.CompletedProcess:
        """
        运行Tcl命令列表
        
        Args:
            commands: Tcl命令列表
            working_dir: 工作目录
        """
        # 创建临时Tcl脚本
        tcl_content = '\n'.join(commands)
        tcl_file = Path(working_dir or '.') / '_temp_script.tcl'
        
        tcl_file.write_text(tcl_content, encoding='utf-8')
        
        try:
            result = self.run_tcl(str(tcl_file), working_dir=working_dir)
        finally:
            tcl_file.unlink()  # 删除临时文件
        
        return result

# 使用示例
def example():
    vivado = VivadoRunner()
    
    # 运行Tcl脚本
    result = vivado.run_tcl('build.tcl', log_file='build.log')
    
    # 运行Tcl命令
    commands = [
        'puts "Hello from Vivado"',
        'puts [version]',
    ]
    result = vivado.run_tcl_commands(commands)
    print(result.stdout)

3. Tcl脚本生成

from pathlib import Path
from typing import List, Dict, Optional
from dataclasses import dataclass, field

@dataclass
class VivadoProject:
    """Vivado工程配置"""
    name: str
    part: str
    top_module: str
    project_dir: str = '.'
    source_files: List[str] = field(default_factory=list)
    constraint_files: List[str] = field(default_factory=list)
    ip_files: List[str] = field(default_factory=list)
    generics: Dict[str, str] = field(default_factory=dict)
    jobs: int = 4

class TclGenerator:
    """Tcl脚本生成器"""
    
    def __init__(self, project: VivadoProject):
        self.project = project
        self.commands = []
    
    def add_command(self, cmd: str):
        """添加命令"""
        self.commands.append(cmd)
    
    def generate_create_project(self):
        """生成创建工程命令"""
        proj_path = Path(self.project.project_dir) / self.project.name
        self.add_command(f'# 创建工程')
        self.add_command(f'create_project {self.project.name} {proj_path} -part {self.project.part} -force')
    
    def generate_add_sources(self):
        """生成添加源文件命令"""
        if self.project.source_files:
            self.add_command(f'\n# 添加源文件')
            files = ' '.join(f'{{{f}}}' for f in self.project.source_files)
            self.add_command(f'add_files -norecurse {files}')
        
        if self.project.constraint_files:
            self.add_command(f'\n# 添加约束文件')
            files = ' '.join(f'{{{f}}}' for f in self.project.constraint_files)
            self.add_command(f'add_files -fileset constrs_1 -norecurse {files}')
        
        if self.project.ip_files:
            self.add_command(f'\n# 添加IP文件')
            for ip_file in self.project.ip_files:
                self.add_command(f'import_ip -srcset sources_1 {{{ip_file}}}')
    
    def generate_set_top(self):
        """生成设置顶层模块命令"""
        self.add_command(f'\n# 设置顶层模块')
        self.add_command(f'set_property top {self.project.top_module} [current_fileset]')
    
    def generate_set_generics(self):
        """生成设置泛型参数命令"""
        if self.project.generics:
            self.add_command(f'\n# 设置泛型参数')
            generics_str = ' '.join(f'{k}={v}' for k, v in self.project.generics.items())
            self.add_command(f'set_property generic {{{generics_str}}} [current_fileset]')
    
    def generate_synthesis(self):
        """生成综合命令"""
        self.add_command(f'\n# 运行综合')
        self.add_command(f'launch_runs synth_1 -jobs {self.project.jobs}')
        self.add_command(f'wait_on_run synth_1')
        self.add_command(f'')
        self.add_command(f'# 检查综合结果')
        self.add_command(f'if {{[get_property PROGRESS [get_runs synth_1]] != "100%"}} {{')
        self.add_command(f'    puts "ERROR: 综合失败"')
        self.add_command(f'    exit 1')
        self.add_command(f'}}')
    
    def generate_implementation(self):
        """生成实现命令"""
        self.add_command(f'\n# 运行实现')
        self.add_command(f'launch_runs impl_1 -jobs {self.project.jobs}')
        self.add_command(f'wait_on_run impl_1')
        self.add_command(f'')
        self.add_command(f'# 检查实现结果')
        self.add_command(f'if {{[get_property PROGRESS [get_runs impl_1]] != "100%"}} {{')
        self.add_command(f'    puts "ERROR: 实现失败"')
        self.add_command(f'    exit 1')
        self.add_command(f'}}')
    
    def generate_bitstream(self):
        """生成比特流命令"""
        self.add_command(f'\n# 生成比特流')
        self.add_command(f'launch_runs impl_1 -to_step write_bitstream -jobs {self.project.jobs}')
        self.add_command(f'wait_on_run impl_1')
    
    def generate_reports(self):
        """生成报告命令"""
        self.add_command(f'\n# 打开实现结果')
        self.add_command(f'open_run impl_1')
        self.add_command(f'')
        self.add_command(f'# 生成报告')
        self.add_command(f'report_timing_summary -file timing_summary.rpt')
        self.add_command(f'report_utilization -file utilization.rpt')
        self.add_command(f'report_power -file power.rpt')
    
    def generate_close(self):
        """生成关闭工程命令"""
        self.add_command(f'\n# 关闭工程')
        self.add_command(f'close_project')
        self.add_command(f'puts "构建完成"')
    
    def generate_full_build(self) -> str:
        """生成完整构建脚本"""
        self.commands = []
        
        self.add_command('# Vivado自动构建脚本')
        self.add_command(f'# 生成时间:{__import__("datetime").datetime.now()}')
        self.add_command('')
        
        self.generate_create_project()
        self.generate_add_sources()
        self.generate_set_top()
        self.generate_set_generics()
        self.generate_synthesis()
        self.generate_implementation()
        self.generate_bitstream()
        self.generate_reports()
        self.generate_close()
        
        return '\n'.join(self.commands)
    
    def save(self, filename: str):
        """保存Tcl脚本"""
        script = self.generate_full_build()
        Path(filename).write_text(script, encoding='utf-8')
        print(f"Tcl脚本已保存到:{filename}")

# 使用示例
def generate_build_script():
    project = VivadoProject(
        name='my_fpga_project',
        part='xc7a35tcpg236-1',
        top_module='top',
        project_dir='./build',
        source_files=[
            './src/top.v',
            './src/uart.v',
            './src/spi.v'
        ],
        constraint_files=[
            './constraints/pins.xdc',
            './constraints/timing.xdc'
        ],
        generics={
            'CLK_FREQ': '100000000',
            'BAUD_RATE': '115200'
        },
        jobs=8
    )
    
    generator = TclGenerator(project)
    generator.save('build.tcl')

# generate_build_script()

4. 工程自动化

import subprocess
import os
import re
from pathlib import Path
from datetime import datetime
from dataclasses import dataclass
from typing import Optional, Dict

@dataclass
class BuildResult:
    """构建结果"""
    success: bool
    duration: float
    synth_status: str
    impl_status: str
    timing_met: bool
    wns: float  # Worst Negative Slack
    tns: float  # Total Negative Slack
    utilization: Dict[str, float]
    bitstream_path: Optional[str]
    log_path: str

class VivadoBuilder:
    """Vivado自动构建器"""
    
    def __init__(self, vivado_path: str = 'vivado'):
        self.vivado_path = vivado_path
        self.build_dir = Path('build')
        self.log_dir = Path('logs')
    
    def setup_directories(self):
        """创建目录"""
        self.build_dir.mkdir(exist_ok=True)
        self.log_dir.mkdir(exist_ok=True)
    
    def build(self, project: 'VivadoProject') -> BuildResult:
        """执行构建"""
        self.setup_directories()
        
        # 生成Tcl脚本
        generator = TclGenerator(project)
        tcl_file = self.build_dir / 'build.tcl'
        generator.save(str(tcl_file))
        
        # 运行Vivado
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        log_file = self.log_dir / f'build_{timestamp}.log'
        
        start_time = datetime.now()
        
        result = subprocess.run(
            [self.vivado_path, '-mode', 'batch', '-source', str(tcl_file)],
            cwd=str(self.build_dir),
            capture_output=True,
            text=True
        )
        
        duration = (datetime.now() - start_time).total_seconds()
        
        # 保存日志
        log_file.write_text(result.stdout + '\n' + result.stderr, encoding='utf-8')
        
        # 解析结果
        build_result = self._parse_result(result, duration, str(log_file), project)
        
        return build_result
    
    def _parse_result(self, result: subprocess.CompletedProcess, 
                      duration: float, log_path: str,
                      project: 'VivadoProject') -> BuildResult:
        """解析构建结果"""
        success = result.returncode == 0
        
        # 解析时序
        timing_met = True
        wns = 0.0
        tns = 0.0
        
        timing_file = self.build_dir / project.name / f'{project.name}.runs' / 'impl_1' / 'timing_summary.rpt'
        if timing_file.exists():
            timing_content = timing_file.read_text()
            wns_match = re.search(r'WNS\(ns\)\s*:\s*([-\d.]+)', timing_content)
            tns_match = re.search(r'TNS\(ns\)\s*:\s*([-\d.]+)', timing_content)
            if wns_match:
                wns = float(wns_match.group(1))
            if tns_match:
                tns = float(tns_match.group(1))
            timing_met = wns >= 0
        
        # 解析资源利用率
        utilization = {}
        util_file = self.build_dir / project.name / f'{project.name}.runs' / 'impl_1' / 'utilization.rpt'
        if util_file.exists():
            utilization = self._parse_utilization(util_file.read_text())
        
        # 查找比特流
        bitstream_path = None
        bit_file = self.build_dir / project.name / f'{project.name}.runs' / 'impl_1' / f'{project.top_module}.bit'
        if bit_file.exists():
            bitstream_path = str(bit_file)
        
        return BuildResult(
            success=success,
            duration=duration,
            synth_status='完成' if success else '失败',
            impl_status='完成' if success else '失败',
            timing_met=timing_met,
            wns=wns,
            tns=tns,
            utilization=utilization,
            bitstream_path=bitstream_path,
            log_path=log_path
        )
    
    def _parse_utilization(self, content: str) -> Dict[str, float]:
        """解析资源利用率"""
        utilization = {}
        
        patterns = {
            'LUT': r'Slice LUTs\s*\|\s*(\d+)\s*\|\s*\d+\s*\|\s*([\d.]+)',
            'FF': r'Slice Registers\s*\|\s*(\d+)\s*\|\s*\d+\s*\|\s*([\d.]+)',
            'BRAM': r'Block RAM Tile\s*\|\s*(\d+)\s*\|\s*\d+\s*\|\s*([\d.]+)',
            'DSP': r'DSPs\s*\|\s*(\d+)\s*\|\s*\d+\s*\|\s*([\d.]+)',
        }
        
        for name, pattern in patterns.items():
            match = re.search(pattern, content)
            if match:
                utilization[name] = float(match.group(2))
        
        return utilization
    
    def print_result(self, result: BuildResult):
        """打印构建结果"""
        print("\n" + "="*50)
        print("构建结果")
        print("="*50)
        print(f"状态:{'成功' if result.success else '失败'}")
        print(f"耗时:{result.duration:.1f} 秒")
        print(f"时序:{'满足' if result.timing_met else '不满足'}")
        print(f"WNS:{result.wns:.3f} ns")
        print(f"TNS:{result.tns:.3f} ns")
        print(f"\n资源利用率:")
        for name, value in result.utilization.items():
            print(f"  {name}: {value:.1f}%")
        if result.bitstream_path:
            print(f"\n比特流:{result.bitstream_path}")
        print(f"日志:{result.log_path}")
        print("="*50)

5. 报告解析

import re
from pathlib import Path
from dataclasses import dataclass
from typing import List, Dict, Optional

@dataclass
class TimingPath:
    """时序路径"""
    slack: float
    source: str
    destination: str
    requirement: float
    data_path_delay: float

@dataclass
class TimingReport:
    """时序报告"""
    wns: float
    tns: float
    whs: float  # Worst Hold Slack
    ths: float  # Total Hold Slack
    timing_met: bool
    critical_paths: List[TimingPath]

class VivadoReportParser:
    """Vivado报告解析器"""
    
    def parse_timing_summary(self, filepath: str) -> TimingReport:
        """解析时序摘要报告"""
        content = Path(filepath).read_text()
        
        # 解析WNS/TNS
        wns = self._extract_value(content, r'WNS\(ns\)\s*:\s*([-\d.]+)')
        tns = self._extract_value(content, r'TNS\(ns\)\s*:\s*([-\d.]+)')
        whs = self._extract_value(content, r'WHS\(ns\)\s*:\s*([-\d.]+)')
        ths = self._extract_value(content, r'THS\(ns\)\s*:\s*([-\d.]+)')
        
        timing_met = wns >= 0 and whs >= 0
        
        # 解析关键路径
        critical_paths = self._parse_critical_paths(content)
        
        return TimingReport(
            wns=wns,
            tns=tns,
            whs=whs,
            ths=ths,
            timing_met=timing_met,
            critical_paths=critical_paths
        )
    
    def parse_utilization(self, filepath: str) -> Dict[str, Dict]:
        """解析资源利用率报告"""
        content = Path(filepath).read_text()
        
        utilization = {}
        
        # 解析各类资源
        patterns = {
            'LUT': r'Slice LUTs\*?\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*([\d.]+)',
            'LUTRAM': r'LUT as Memory\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*([\d.]+)',
            'FF': r'Slice Registers\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*([\d.]+)',
            'BRAM': r'Block RAM Tile\s*\|\s*([\d.]+)\s*\|\s*(\d+)\s*\|\s*([\d.]+)',
            'DSP': r'DSPs\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*([\d.]+)',
            'IO': r'Bonded IOB\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*([\d.]+)',
        }
        
        for name, pattern in patterns.items():
            match = re.search(pattern, content)
            if match:
                utilization[name] = {
                    'used': float(match.group(1)),
                    'available': int(match.group(2)),
                    'percentage': float(match.group(3))
                }
        
        return utilization
    
    def parse_power(self, filepath: str) -> Dict[str, float]:
        """解析功耗报告"""
        content = Path(filepath).read_text()
        
        power = {}
        
        # 总功耗
        total_match = re.search(r'Total On-Chip Power \(W\)\s*\|\s*([\d.]+)', content)
        if total_match:
            power['total'] = float(total_match.group(1))
        
        # 动态功耗
        dynamic_match = re.search(r'Dynamic \(W\)\s*\|\s*([\d.]+)', content)
        if dynamic_match:
            power['dynamic'] = float(dynamic_match.group(1))
        
        # 静态功耗
        static_match = re.search(r'Device Static \(W\)\s*\|\s*([\d.]+)', content)
        if static_match:
            power['static'] = float(static_match.group(1))
        
        return power
    
    def _extract_value(self, content: str, pattern: str) -> float:
        """提取数值"""
        match = re.search(pattern, content)
        return float(match.group(1)) if match else 0.0
    
    def _parse_critical_paths(self, content: str) -> List[TimingPath]:
        """解析关键路径"""
        paths = []
        
        # 简化的路径解析
        path_pattern = r'Slack\s*:\s*([-\d.]+)ns.*?Source:\s*(\S+).*?Destination:\s*(\S+)'
        matches = re.finditer(path_pattern, content, re.DOTALL)
        
        for match in matches:
            paths.append(TimingPath(
                slack=float(match.group(1)),
                source=match.group(2),
                destination=match.group(3),
                requirement=0,
                data_path_delay=0
            ))
            if len(paths) >= 10:  # 只取前10条
                break
        
        return paths
    
    def generate_summary(self, project_dir: str) -> str:
        """生成综合报告摘要"""
        report = []
        report.append("="*60)
        report.append("Vivado构建报告摘要")
        report.append("="*60)
        
        # 时序报告
        timing_file = Path(project_dir) / 'timing_summary.rpt'
        if timing_file.exists():
            timing = self.parse_timing_summary(str(timing_file))
            report.append("\n时序分析:")
            report.append(f"  WNS: {timing.wns:.3f} ns")
            report.append(f"  TNS: {timing.tns:.3f} ns")
            report.append(f"  WHS: {timing.whs:.3f} ns")
            report.append(f"  状态: {'满足' if timing.timing_met else '不满足'}")
        
        # 资源利用率
        util_file = Path(project_dir) / 'utilization.rpt'
        if util_file.exists():
            util = self.parse_utilization(str(util_file))
            report.append("\n资源利用率:")
            for name, data in util.items():
                report.append(f"  {name}: {data['used']:.0f}/{data['available']} ({data['percentage']:.1f}%)")
        
        # 功耗
        power_file = Path(project_dir) / 'power.rpt'
        if power_file.exists():
            power = self.parse_power(str(power_file))
            report.append("\n功耗估计:")
            for name, value in power.items():
                report.append(f"  {name}: {value:.3f} W")
        
        report.append("="*60)
        return '\n'.join(report)

6. 批量处理

import json
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, asdict
from typing import List, Dict
from datetime import datetime

@dataclass
class BuildConfig:
    """构建配置"""
    name: str
    part: str
    top_module: str
    sources: List[str]
    constraints: List[str]
    generics: Dict[str, str]

class BatchBuilder:
    """批量构建器"""
    
    def __init__(self, vivado_path: str = 'vivado', max_workers: int = 2):
        self.vivado_path = vivado_path
        self.max_workers = max_workers
        self.results = []
    
    def load_configs(self, config_file: str) -> List[BuildConfig]:
        """从JSON文件加载配置"""
        with open(config_file, 'r', encoding='utf-8') as f:
            data = json.load(f)
        
        configs = []
        for item in data['builds']:
            configs.append(BuildConfig(**item))
        
        return configs
    
    def build_single(self, config: BuildConfig) -> Dict:
        """构建单个配置"""
        print(f"开始构建:{config.name}")
        start_time = datetime.now()
        
        try:
            project = VivadoProject(
                name=config.name,
                part=config.part,
                top_module=config.top_module,
                source_files=config.sources,
                constraint_files=config.constraints,
                generics=config.generics
            )
            
            builder = VivadoBuilder(self.vivado_path)
            result = builder.build(project)
            
            return {
                'name': config.name,
                'success': result.success,
                'duration': result.duration,
                'timing_met': result.timing_met,
                'wns': result.wns,
                'utilization': result.utilization,
                'error': None
            }
        except Exception as e:
            return {
                'name': config.name,
                'success': False,
                'duration': (datetime.now() - start_time).total_seconds(),
                'timing_met': False,
                'wns': 0,
                'utilization': {},
                'error': str(e)
            }
    
    def build_all(self, configs: List[BuildConfig]) -> List[Dict]:
        """并行构建所有配置"""
        results = []
        
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            futures = {executor.submit(self.build_single, config): config 
                      for config in configs}
            
            for future in as_completed(futures):
                config = futures[future]
                try:
                    result = future.result()
                    results.append(result)
                    status = '成功' if result['success'] else '失败'
                    print(f"完成:{config.name} - {status}")
                except Exception as e:
                    results.append({
                        'name': config.name,
                        'success': False,
                        'error': str(e)
                    })
        
        self.results = results
        return results
    
    def generate_report(self) -> str:
        """生成批量构建报告"""
        report = []
        report.append("="*70)
        report.append("批量构建报告")
        report.append(f"时间:{datetime.now()}")
        report.append("="*70)
        
        success_count = sum(1 for r in self.results if r['success'])
        report.append(f"\n总计:{len(self.results)} 个配置")
        report.append(f"成功:{success_count}")
        report.append(f"失败:{len(self.results) - success_count}")
        
        report.append("\n详细结果:")
        report.append("-"*70)
        
        for result in self.results:
            status = '✓' if result['success'] else '✗'
            timing = '满足' if result.get('timing_met') else '不满足'
            report.append(f"{status} {result['name']}")
            report.append(f"   耗时:{result.get('duration', 0):.1f}s")
            report.append(f"   时序:{timing}, WNS={result.get('wns', 0):.3f}ns")
            if result.get('error'):
                report.append(f"   错误:{result['error']}")
        
        report.append("="*70)
        return '\n'.join(report)
    
    def save_results(self, filename: str):
        """保存结果到JSON"""
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(self.results, f, indent=2, ensure_ascii=False)

# 配置文件示例 (builds.json)
example_config = """
{
    "builds": [
        {
            "name": "design_100mhz",
            "part": "xc7a35tcpg236-1",
            "top_module": "top",
            "sources": ["src/top.v", "src/core.v"],
            "constraints": ["constraints/pins.xdc"],
            "generics": {"CLK_FREQ": "100000000"}
        },
        {
            "name": "design_150mhz",
            "part": "xc7a35tcpg236-1",
            "top_module": "top",
            "sources": ["src/top.v", "src/core.v"],
            "constraints": ["constraints/pins.xdc"],
            "generics": {"CLK_FREQ": "150000000"}
        }
    ]
}
"""

7. 实战案例

案例:完整的FPGA构建系统

"""
实战案例:FPGA自动构建系统
"""
import argparse
import json
from pathlib import Path
from datetime import datetime

class FPGABuildSystem:
    """FPGA构建系统"""
    
    def __init__(self, config_file: str = 'fpga_config.json'):
        self.config = self._load_config(config_file)
        self.vivado_path = self.config.get('vivado_path', 'vivado')
        self.output_dir = Path(self.config.get('output_dir', 'output'))
        self.output_dir.mkdir(exist_ok=True)
    
    def _load_config(self, config_file: str) -> dict:
        """加载配置"""
        if Path(config_file).exists():
            with open(config_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        return {}
    
    def build(self, target: str = 'all'):
        """执行构建"""
        print(f"开始构建:{target}")
        print(f"时间:{datetime.now()}")
        
        project = VivadoProject(
            name=self.config['project_name'],
            part=self.config['part'],
            top_module=self.config['top_module'],
            source_files=self.config['sources'],
            constraint_files=self.config['constraints'],
            generics=self.config.get('generics', {})
        )
        
        builder = VivadoBuilder(self.vivado_path)
        result = builder.build(project)
        builder.print_result(result)
        
        # 保存结果
        self._save_result(result)
        
        return result.success
    
    def clean(self):
        """清理构建文件"""
        import shutil
        
        dirs_to_clean = ['build', '.Xil']
        for dir_name in dirs_to_clean:
            dir_path = Path(dir_name)
            if dir_path.exists():
                shutil.rmtree(dir_path)
                print(f"已删除:{dir_path}")
    
    def program(self, bitstream: str = None):
        """下载比特流"""
        if bitstream is None:
            # 查找最新的比特流
            bit_files = list(Path('build').rglob('*.bit'))
            if not bit_files:
                print("找不到比特流文件")
                return False
            bitstream = str(bit_files[0])
        
        tcl_commands = [
            'open_hw_manager',
            'connect_hw_server',
            'open_hw_target',
            'set_property PROGRAM.FILE {%s} [current_hw_device]' % bitstream,
            'program_hw_devices [current_hw_device]',
            'close_hw_manager'
        ]
        
        runner = VivadoRunner(self.vivado_path)
        result = runner.run_tcl_commands(tcl_commands)
        
        return result.returncode == 0
    
    def _save_result(self, result: BuildResult):
        """保存构建结果"""
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        result_file = self.output_dir / f'build_result_{timestamp}.json'
        
        result_dict = {
            'timestamp': timestamp,
            'success': result.success,
            'duration': result.duration,
            'timing_met': result.timing_met,
            'wns': result.wns,
            'tns': result.tns,
            'utilization': result.utilization,
            'bitstream': result.bitstream_path
        }
        
        with open(result_file, 'w', encoding='utf-8') as f:
            json.dump(result_dict, f, indent=2)

def main():
    parser = argparse.ArgumentParser(description='FPGA构建系统')
    parser.add_argument('command', choices=['build', 'clean', 'program'],
                       help='执行的命令')
    parser.add_argument('-c', '--config', default='fpga_config.json',
                       help='配置文件路径')
    parser.add_argument('-b', '--bitstream', help='比特流文件路径')
    
    args = parser.parse_args()
    
    system = FPGABuildSystem(args.config)
    
    if args.command == 'build':
        success = system.build()
        exit(0 if success else 1)
    elif args.command == 'clean':
        system.clean()
    elif args.command == 'program':
        success = system.program(args.bitstream)
        exit(0 if success else 1)

if __name__ == '__main__':
    main()

8. 常见问题

❌ 问题1:找不到Vivado

# 解决:设置完整路径
vivado_path = r'C:\Xilinx\Vivado\2023.1\bin\vivado.bat'

# 或添加到环境变量
import os
os.environ['PATH'] = r'C:\Xilinx\Vivado\2023.1\bin;' + os.environ['PATH']

❌ 问题2:Tcl脚本编码问题

# 解决:使用UTF-8编码保存Tcl脚本
Path('script.tcl').write_text(content, encoding='utf-8')

# 路径使用正斜杠
path = path.replace('\\', '/')

❌ 问题3:License问题

# 在Tcl脚本中检查License
if {[catch {check_license -quiet} result]} {
    puts "License错误:$result"
    exit 1
}

9. 总结

🔑 核心要点

知识点要点
命令行模式vivado -mode batch -source script.tcl
Python调用subprocess.run()
Tcl脚本生成自动生成构建脚本
报告解析正则表达式解析时序、资源报告
批量处理并行构建多个配置

✅ 学习检查清单

  • 能使用命令行运行Vivado
  • 能用Python调用Vivado
  • 能生成Tcl构建脚本
  • 能解析Vivado报告
  • 能实现批量构建

📖 下一步学习

掌握了Vivado自动化后,让我们学习测试向量生成:


常见问题 FAQ

💬 Vivado的Tcl和Python怎么配合?

Python负责生成Tcl脚本和调用Vivado,Vivado内部执行Tcl。两者通过文件和subprocess交互。不要试图在Python里直接调用Vivado的API,走Tcl是官方推荐方式。

💬 怎么判断综合/实现是否成功?

检查Vivado的返回码(0=成功),同时解析日志文件中的ERRORCRITICAL WARNING。建议把关键指标(时序裕量、资源利用率)也自动提取出来。


系列导航

End of file.