Sai Kumar Taraka commited on
Commit ·
4344b33
1
Parent(s): 9e213dd
Initial commit: UVM testbench generator with coverage-driven auto-training
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +17 -0
- .github/workflows/ci.yml +153 -0
- .github/workflows/release.yml +25 -0
- .gitignore +27 -0
- .pre-commit-config.yaml +38 -0
- Dockerfile +40 -0
- Makefile +32 -0
- README.md +31 -1
- configs/base_config.yaml +26 -0
- configs/logging.yaml +25 -0
- configs/schema/master_schema.json +219 -0
- configs/uart16550-1.5.core +190 -0
- configs/uart_demo.yaml +31 -0
- docker-compose.yml +23 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +38 -0
- frontend/postcss.config.js +6 -0
- frontend/public/favicon.svg +5 -0
- frontend/public/index.html +14 -0
- frontend/src/App.js +49 -0
- frontend/src/components/ErrorBoundary.js +87 -0
- frontend/src/components/Footer.js +25 -0
- frontend/src/components/Header.js +40 -0
- frontend/src/components/PipelineRunner.js +461 -0
- frontend/src/components/PreviewPanel.js +246 -0
- frontend/src/components/YAMLForm.js +516 -0
- frontend/src/hooks/usePipeline.js +778 -0
- frontend/src/index.css +58 -0
- frontend/src/index.js +11 -0
- frontend/src/utils/yamlUtils.js +85 -0
- frontend/tailwind.config.js +28 -0
- protocols/apb.yaml +51 -0
- protocols/axi4lite.yaml +64 -0
- protocols/i2c.yaml +61 -0
- protocols/spi.yaml +57 -0
- protocols/uart.yaml +119 -0
- protocols/wishbone.yaml +48 -0
- pyproject.toml +61 -0
- render.yaml +30 -0
- requirements-dev.txt +2 -0
- requirements.txt +6 -0
- setup.cfg +25 -0
- src/__init__.py +0 -0
- src/api/__init__.py +0 -0
- src/api/server.py +260 -0
- src/config.py +175 -0
- src/data/__init__.py +0 -0
- src/data/collector.py +40 -0
- src/data/core_parser.py +117 -0
- src/data/preprocessor.py +81 -0
.env.example
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Production environment variables for UVM TB Generator
|
| 2 |
+
# Copy to .env and fill in secrets
|
| 3 |
+
|
| 4 |
+
# Backend
|
| 5 |
+
UVMGEN_LOG_LEVEL=info
|
| 6 |
+
UVMGEN_OUTPUT_DIR=/app/output
|
| 7 |
+
UVMGEN_SIMULATOR=stub
|
| 8 |
+
|
| 9 |
+
# Frontend
|
| 10 |
+
REACT_APP_API_URL=http://localhost:8000
|
| 11 |
+
|
| 12 |
+
# Security (change in production)
|
| 13 |
+
UVMGEN_SECRET_KEY=change-me-in-production
|
| 14 |
+
UVMGEN_CORS_ORIGINS=http://localhost:3000,http://localhost
|
| 15 |
+
|
| 16 |
+
# MLflow (optional)
|
| 17 |
+
# UVMGEN_MLFLOW_TRACKING_URI=http://mlflow:5000
|
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI — UVM TB Generator
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main, develop]
|
| 6 |
+
paths-ignore: ["docs/**", "README.md"]
|
| 7 |
+
pull_request:
|
| 8 |
+
branches: [main]
|
| 9 |
+
workflow_dispatch:
|
| 10 |
+
|
| 11 |
+
env:
|
| 12 |
+
PYTHON_VERSION: "3.11"
|
| 13 |
+
|
| 14 |
+
jobs:
|
| 15 |
+
lint:
|
| 16 |
+
name: Lint & Format Check
|
| 17 |
+
runs-on: ubuntu-latest
|
| 18 |
+
steps:
|
| 19 |
+
- uses: actions/checkout@v4
|
| 20 |
+
- uses: actions/setup-python@v5
|
| 21 |
+
with:
|
| 22 |
+
python-version: ${{ env.PYTHON_VERSION }}
|
| 23 |
+
- name: Install dependencies
|
| 24 |
+
run: |
|
| 25 |
+
pip install flake8 black yamllint
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
- name: Run flake8
|
| 28 |
+
run: flake8 src/ tests/ --max-line-length=120 --extend-ignore=E203
|
| 29 |
+
- name: Run yamllint on configs and protocols
|
| 30 |
+
run: yamllint configs/ protocols/
|
| 31 |
+
|
| 32 |
+
test:
|
| 33 |
+
name: Tests (Python ${{ matrix.python-version }})
|
| 34 |
+
runs-on: ubuntu-latest
|
| 35 |
+
strategy:
|
| 36 |
+
matrix:
|
| 37 |
+
python-version: ["3.10", "3.11", "3.12"]
|
| 38 |
+
steps:
|
| 39 |
+
- uses: actions/checkout@v4
|
| 40 |
+
- uses: actions/setup-python@v5
|
| 41 |
+
with:
|
| 42 |
+
python-version: ${{ matrix.python-version }}
|
| 43 |
+
- name: Install dependencies
|
| 44 |
+
run: |
|
| 45 |
+
pip install pytest pytest-cov
|
| 46 |
+
pip install -r requirements.txt
|
| 47 |
+
- name: Run tests with coverage
|
| 48 |
+
run: |
|
| 49 |
+
python -m pytest tests/ -v --cov=src --cov-report=xml --cov-report=term-missing
|
| 50 |
+
- name: Upload coverage
|
| 51 |
+
uses: codecov/codecov-action@v5
|
| 52 |
+
with:
|
| 53 |
+
file: ./coverage.xml
|
| 54 |
+
fail_ci_if_error: false
|
| 55 |
+
|
| 56 |
+
generate:
|
| 57 |
+
name: Generation Smoke Test
|
| 58 |
+
runs-on: ubuntu-latest
|
| 59 |
+
steps:
|
| 60 |
+
- uses: actions/checkout@v4
|
| 61 |
+
- uses: actions/setup-python@v5
|
| 62 |
+
with:
|
| 63 |
+
python-version: ${{ env.PYTHON_VERSION }}
|
| 64 |
+
- name: Install dependencies
|
| 65 |
+
run: |
|
| 66 |
+
pip install -r requirements.txt
|
| 67 |
+
- name: Generate from YAML spec
|
| 68 |
+
run: |
|
| 69 |
+
python -m src.main --spec configs/uart_demo.yaml --json
|
| 70 |
+
- name: Generate from .core file
|
| 71 |
+
run: |
|
| 72 |
+
python -m src.main --spec configs/uart16550-1.5.core --json
|
| 73 |
+
- name: Verify output files exist
|
| 74 |
+
run: |
|
| 75 |
+
test -d output/uart_tb
|
| 76 |
+
test -f output/uart_tb/testbench.sv
|
| 77 |
+
test -f output/uart_tb/interface_uart.sv
|
| 78 |
+
test -d output/uart16550_tb
|
| 79 |
+
test -f output/uart16550_tb/testbench.sv
|
| 80 |
+
echo "All output files verified"
|
| 81 |
+
- name: Verify API server imports
|
| 82 |
+
run: |
|
| 83 |
+
python -c "from src.api.server import app; print('API server OK')"
|
| 84 |
+
|
| 85 |
+
regression:
|
| 86 |
+
name: Multi-Seed Regression Test
|
| 87 |
+
runs-on: ubuntu-latest
|
| 88 |
+
needs: [generate]
|
| 89 |
+
steps:
|
| 90 |
+
- uses: actions/checkout@v4
|
| 91 |
+
- uses: actions/setup-python@v5
|
| 92 |
+
with:
|
| 93 |
+
python-version: ${{ env.PYTHON_VERSION }}
|
| 94 |
+
- name: Install dependencies
|
| 95 |
+
run: |
|
| 96 |
+
pip install -r requirements.txt
|
| 97 |
+
- name: Run auto-training with 3 seeds
|
| 98 |
+
run: |
|
| 99 |
+
python -m src.main --spec configs/uart16550-1.5.core --auto-train \
|
| 100 |
+
--max-iterations 3 --coverage-target 75 --simulator stub
|
| 101 |
+
- name: Export coverage trend
|
| 102 |
+
run: |
|
| 103 |
+
python -c "
|
| 104 |
+
import json
|
| 105 |
+
from pathlib import Path
|
| 106 |
+
trend_file = next(Path('output').rglob('*coverage_trend*'), None)
|
| 107 |
+
if trend_file:
|
| 108 |
+
print('Coverage trend:')
|
| 109 |
+
print(trend_file.read_text())
|
| 110 |
+
else:
|
| 111 |
+
# Show registry content
|
| 112 |
+
reg_dir = Path('output') / 'model_registry'
|
| 113 |
+
if reg_dir.exists():
|
| 114 |
+
versions = sorted(reg_dir.iterdir())
|
| 115 |
+
print(f'Versions: {[v.name for v in versions]}')
|
| 116 |
+
" || true
|
| 117 |
+
- name: Verify FuseSoC .core file generated
|
| 118 |
+
run: |
|
| 119 |
+
test -f output/uart16550_tb/uart16550.core && echo "FuseSoC .core found" || echo "No FuseSoC core"
|
| 120 |
+
|
| 121 |
+
schema:
|
| 122 |
+
name: Schema Validation
|
| 123 |
+
runs-on: ubuntu-latest
|
| 124 |
+
steps:
|
| 125 |
+
- uses: actions/checkout@v4
|
| 126 |
+
- uses: actions/setup-python@v5
|
| 127 |
+
with:
|
| 128 |
+
python-version: ${{ env.PYTHON_VERSION }}
|
| 129 |
+
- name: Install deps
|
| 130 |
+
run: pip install pyyaml jsonschema
|
| 131 |
+
- name: Validate demo specs against schema
|
| 132 |
+
run: |
|
| 133 |
+
python -c "
|
| 134 |
+
import json, yaml, sys
|
| 135 |
+
from jsonschema import validate
|
| 136 |
+
|
| 137 |
+
with open('configs/schema/master_schema.json') as f:
|
| 138 |
+
schema = json.load(f)
|
| 139 |
+
|
| 140 |
+
for spec_file in ['configs/uart_demo.yaml', 'configs/uart16550-1.5.core']:
|
| 141 |
+
with open(spec_file) as f:
|
| 142 |
+
if spec_file.endswith('.core'):
|
| 143 |
+
# .core files wrap spec in a different format; validate the relevant parts
|
| 144 |
+
data = yaml.safe_load(f)
|
| 145 |
+
else:
|
| 146 |
+
data = yaml.safe_load(f)
|
| 147 |
+
try:
|
| 148 |
+
validate(instance=data, schema=schema)
|
| 149 |
+
print(f'OK: {spec_file} passes schema')
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f'FAIL: {spec_file} — {e}')
|
| 152 |
+
sys.exit(1)
|
| 153 |
+
"
|
.github/workflows/release.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Release
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
tags: ["v*"]
|
| 6 |
+
|
| 7 |
+
jobs:
|
| 8 |
+
release:
|
| 9 |
+
runs-on: ubuntu-latest
|
| 10 |
+
permissions:
|
| 11 |
+
contents: write
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/checkout@v4
|
| 14 |
+
- uses: actions/setup-python@v5
|
| 15 |
+
with:
|
| 16 |
+
python-version: "3.11"
|
| 17 |
+
- name: Build package
|
| 18 |
+
run: |
|
| 19 |
+
pip install build
|
| 20 |
+
python -m build
|
| 21 |
+
- name: Create GitHub Release
|
| 22 |
+
uses: softprops/action-gh-release@v2
|
| 23 |
+
with:
|
| 24 |
+
files: dist/*
|
| 25 |
+
generate_release_notes: true
|
.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
.pytest_cache/
|
| 5 |
+
.coverage
|
| 6 |
+
htmlcov/
|
| 7 |
+
.env
|
| 8 |
+
.env.local
|
| 9 |
+
output/
|
| 10 |
+
logs/
|
| 11 |
+
model_registry/
|
| 12 |
+
models/saved/
|
| 13 |
+
!frontend/build/.gitkeep
|
| 14 |
+
*.egg-info/
|
| 15 |
+
dist/
|
| 16 |
+
build/
|
| 17 |
+
.DS_Store
|
| 18 |
+
*.log
|
| 19 |
+
*.pid
|
| 20 |
+
*.sock
|
| 21 |
+
node_modules/
|
| 22 |
+
frontend/build/
|
| 23 |
+
.mypy_cache/
|
| 24 |
+
.ruff_cache/
|
| 25 |
+
.venv/
|
| 26 |
+
venv/
|
| 27 |
+
|
.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
repos:
|
| 2 |
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
| 3 |
+
rev: v5.0.0
|
| 4 |
+
hooks:
|
| 5 |
+
- id: trailing-whitespace
|
| 6 |
+
- id: end-of-file-fixer
|
| 7 |
+
- id: check-yaml
|
| 8 |
+
- id: check-json
|
| 9 |
+
- id: check-added-large-files
|
| 10 |
+
- id: detect-private-key
|
| 11 |
+
- id: check-merge-conflict
|
| 12 |
+
|
| 13 |
+
- repo: https://github.com/psf/black
|
| 14 |
+
rev: 25.1.0
|
| 15 |
+
hooks:
|
| 16 |
+
- id: black
|
| 17 |
+
language_version: python3.11
|
| 18 |
+
|
| 19 |
+
- repo: https://github.com/PyCQA/flake8
|
| 20 |
+
rev: 7.1.2
|
| 21 |
+
hooks:
|
| 22 |
+
- id: flake8
|
| 23 |
+
args: [--max-line-length=120, --extend-ignore=E203,W503]
|
| 24 |
+
additional_dependencies: [flake8]
|
| 25 |
+
|
| 26 |
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
| 27 |
+
rev: v1.15.0
|
| 28 |
+
hooks:
|
| 29 |
+
- id: mypy
|
| 30 |
+
args: [--ignore-missing-imports, --follow-imports=silent]
|
| 31 |
+
additional_dependencies: [pydantic]
|
| 32 |
+
|
| 33 |
+
- repo: https://github.com/PyCQA/bandit
|
| 34 |
+
rev: 1.8.3
|
| 35 |
+
hooks:
|
| 36 |
+
- id: bandit
|
| 37 |
+
args: [--skip=B101, --recursive, src/]
|
| 38 |
+
additional_dependencies: [bandit]
|
Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multi-stage: frontend build + Python backend
|
| 2 |
+
|
| 3 |
+
# Stage 1: Build React frontend
|
| 4 |
+
FROM node:20-alpine AS frontend-builder
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
COPY frontend/package.json frontend/package-lock.json ./
|
| 7 |
+
RUN npm ci --omit=dev
|
| 8 |
+
COPY frontend/ .
|
| 9 |
+
RUN npm run build
|
| 10 |
+
|
| 11 |
+
# Stage 2: Python backend
|
| 12 |
+
FROM python:3.11-slim AS backend-builder
|
| 13 |
+
|
| 14 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 15 |
+
iverilog \
|
| 16 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 17 |
+
|
| 18 |
+
WORKDIR /app
|
| 19 |
+
COPY requirements.txt .
|
| 20 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 21 |
+
pip install --no-cache-dir -r requirements.txt
|
| 22 |
+
|
| 23 |
+
COPY . .
|
| 24 |
+
COPY --from=frontend-builder /app/build /app/frontend/build
|
| 25 |
+
|
| 26 |
+
RUN pip install --no-cache-dir -e .
|
| 27 |
+
|
| 28 |
+
RUN groupadd -r uvmgen && useradd -r -g uvmgen -d /app -s /sbin/nologin uvmgen
|
| 29 |
+
RUN mkdir -p /app/output /app/logs /var/data && chown -R uvmgen:uvmgen /app /var/data
|
| 30 |
+
|
| 31 |
+
EXPOSE 8000
|
| 32 |
+
|
| 33 |
+
ENV UVMGEN_OUTPUT_DIR=/var/data/uvmgen_output
|
| 34 |
+
|
| 35 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
| 36 |
+
CMD python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/api/health').read().decode())"
|
| 37 |
+
|
| 38 |
+
USER uvmgen
|
| 39 |
+
|
| 40 |
+
CMD ["uvicorn", "src.api.server:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--log-level", "info"]
|
Makefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: install test clean run
|
| 2 |
+
|
| 3 |
+
install:
|
| 4 |
+
pip install -r requirements.txt
|
| 5 |
+
|
| 6 |
+
install-dev:
|
| 7 |
+
pip install -r requirements.txt -r requirements-dev.txt
|
| 8 |
+
|
| 9 |
+
test:
|
| 10 |
+
python -m pytest tests/ -v --cov=src --cov-report=term-missing
|
| 11 |
+
|
| 12 |
+
run:
|
| 13 |
+
python -m src.main --spec configs/uart_demo.yaml
|
| 14 |
+
|
| 15 |
+
run-json:
|
| 16 |
+
python -m src.main --spec configs/uart_demo.yaml --json
|
| 17 |
+
|
| 18 |
+
eval-only:
|
| 19 |
+
python -m src.main --spec configs/uart_demo.yaml --eval-only
|
| 20 |
+
|
| 21 |
+
clean:
|
| 22 |
+
rm -rf output/* logs/* .pytest_cache __pycache__ */__pycache__ */*/__pycache__
|
| 23 |
+
rm -rf models/saved/*.json
|
| 24 |
+
|
| 25 |
+
docker-build:
|
| 26 |
+
docker build -t uvm-tb-generator .
|
| 27 |
+
|
| 28 |
+
docker-run:
|
| 29 |
+
docker run --rm -v $(PWD)/output:/app/output uvm-tb-generator --spec configs/uart_demo.yaml
|
| 30 |
+
|
| 31 |
+
lint:
|
| 32 |
+
python -m flake8 src/ tests/
|
README.md
CHANGED
|
@@ -1 +1,31 @@
|
|
| 1 |
-
# UVM-verification
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# UVM-verification
|
| 2 |
+
|
| 3 |
+
Automated UVM testbench generation from YAML / `.core` specifications — with **protocol libraries**, **schema validation**, **coverage-driven auto-training**, and **CI/CD integration**.
|
| 4 |
+
|
| 5 |
+
## Quick Start
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
pip install -r requirements.txt
|
| 9 |
+
|
| 10 |
+
python -m src.main --spec configs/uart_demo.yaml
|
| 11 |
+
python -m src.main --spec configs/uart16550-1.5.core
|
| 12 |
+
python -m src.main --spec configs/uart16550-1.5.core --auto-train --max-iterations 3
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
## API Server
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
uvicorn src.api.server:app --host 0.0.0.0 --port 8000
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
## Frontend
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
cd frontend && npm install && npm run build
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## Docker
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
docker-compose up --build
|
| 31 |
+
```
|
configs/base_config.yaml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Base pipeline configuration — inherit + override per environment
|
| 2 |
+
|
| 3 |
+
generation:
|
| 4 |
+
templates_dir: src/generation/templates
|
| 5 |
+
output_dir: output
|
| 6 |
+
overwrite: false
|
| 7 |
+
strict_validation: true
|
| 8 |
+
|
| 9 |
+
evaluation:
|
| 10 |
+
enabled: true
|
| 11 |
+
metrics:
|
| 12 |
+
- completeness
|
| 13 |
+
- syntax_validity
|
| 14 |
+
- coverage_readiness
|
| 15 |
+
threshold: 0.7
|
| 16 |
+
|
| 17 |
+
tracking:
|
| 18 |
+
enabled: false
|
| 19 |
+
backend: local
|
| 20 |
+
experiment_name: uvm_tb_generator
|
| 21 |
+
tracking_uri: null
|
| 22 |
+
|
| 23 |
+
logging:
|
| 24 |
+
level: INFO
|
| 25 |
+
file: null
|
| 26 |
+
format: "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
configs/logging.yaml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: 1
|
| 2 |
+
formatters:
|
| 3 |
+
standard:
|
| 4 |
+
format: "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
| 5 |
+
handlers:
|
| 6 |
+
console:
|
| 7 |
+
class: logging.StreamHandler
|
| 8 |
+
level: INFO
|
| 9 |
+
formatter: standard
|
| 10 |
+
stream: ext://sys.stdout
|
| 11 |
+
file:
|
| 12 |
+
class: logging.handlers.RotatingFileHandler
|
| 13 |
+
level: DEBUG
|
| 14 |
+
formatter: standard
|
| 15 |
+
filename: logs/generator.log
|
| 16 |
+
maxBytes: 10485760
|
| 17 |
+
backupCount: 5
|
| 18 |
+
loggers:
|
| 19 |
+
uvmgen:
|
| 20 |
+
level: DEBUG
|
| 21 |
+
handlers: [console, file]
|
| 22 |
+
propagate: no
|
| 23 |
+
root:
|
| 24 |
+
level: INFO
|
| 25 |
+
handlers: [console]
|
configs/schema/master_schema.json
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
| 3 |
+
"$id": "https://github.com/your-org/uvm-tb-generator/schema/master_schema.json",
|
| 4 |
+
"title": "UVM TB Generator — Master Specification Schema",
|
| 5 |
+
"description": "Industry-grade schema for describing digital IP cores for automated UVM testbench generation. Supports multi-protocol, multi-clock, multi-power-domain designs.",
|
| 6 |
+
"type": "object",
|
| 7 |
+
"required": ["design_name", "interfaces"],
|
| 8 |
+
|
| 9 |
+
"definitions": {
|
| 10 |
+
"signal": {
|
| 11 |
+
"type": "object",
|
| 12 |
+
"required": ["name", "direction"],
|
| 13 |
+
"properties": {
|
| 14 |
+
"name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$", "description": "Signal name" },
|
| 15 |
+
"direction": { "type": "string", "enum": ["input", "output", "inout"] },
|
| 16 |
+
"width": { "type": "integer", "minimum": 1, "default": 1, "description": "Bus width in bits" },
|
| 17 |
+
"description":{ "type": "string" }
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
|
| 21 |
+
"field": {
|
| 22 |
+
"type": "object",
|
| 23 |
+
"required": ["name", "bits"],
|
| 24 |
+
"properties": {
|
| 25 |
+
"name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" },
|
| 26 |
+
"bits": { "type": "string", "pattern": "^([0-9]+|[0-9]+:[0-9]+)$", "description": "Single bit (e.g. '0') or range (e.g. '7:0')" },
|
| 27 |
+
"description": { "type": "string" },
|
| 28 |
+
"access": { "type": "string", "enum": ["ro", "wo", "rw", "rc", "rs", "w1c"], "default": "rw" },
|
| 29 |
+
"reset": { "type": "string", "description": "Reset value in hex (e.g. '0x00')" },
|
| 30 |
+
"hw_impl": { "type": "boolean", "default": true, "description": "Hardware-implemented field" },
|
| 31 |
+
"sw_access": { "type": "string", "enum": ["read-only", "write-only", "read-write", "read-clear"], "default": "read-write" }
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
|
| 35 |
+
"register": {
|
| 36 |
+
"type": "object",
|
| 37 |
+
"required": ["name", "address"],
|
| 38 |
+
"properties": {
|
| 39 |
+
"name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" },
|
| 40 |
+
"address": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$", "description": "Hex address" },
|
| 41 |
+
"size": { "type": "integer", "minimum": 1, "default": 1, "description": "Number of bytes" },
|
| 42 |
+
"access": { "type": "string", "enum": ["ro", "wo", "rw", "rc", "rs", "w1c"], "default": "rw" },
|
| 43 |
+
"description": { "type": "string" },
|
| 44 |
+
"fields": { "type": "array", "items": { "$ref": "#/definitions/field" } },
|
| 45 |
+
"reset_value": { "type": "string", "description": "Full register reset value (hex)" },
|
| 46 |
+
"volatile": { "type": "boolean", "default": false }
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
|
| 50 |
+
"clock_domain": {
|
| 51 |
+
"type": "object",
|
| 52 |
+
"required": ["name"],
|
| 53 |
+
"properties": {
|
| 54 |
+
"name": { "type": "string" },
|
| 55 |
+
"frequency":{ "type": "integer", "description": "Frequency in Hz" },
|
| 56 |
+
"primary": { "type": "boolean", "default": false }
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
|
| 60 |
+
"reset_domain": {
|
| 61 |
+
"type": "object",
|
| 62 |
+
"required": ["name", "signal", "active"],
|
| 63 |
+
"properties": {
|
| 64 |
+
"name": { "type": "string" },
|
| 65 |
+
"signal": { "type": "string" },
|
| 66 |
+
"active": { "type": "integer", "enum": [0, 1], "description": "0 = active-low, 1 = active-high" },
|
| 67 |
+
"async": { "type": "boolean", "default": false }
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
|
| 71 |
+
"interface": {
|
| 72 |
+
"type": "object",
|
| 73 |
+
"required": ["name", "signals"],
|
| 74 |
+
"properties": {
|
| 75 |
+
"name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" },
|
| 76 |
+
"protocol": { "type": "string", "enum": ["uart", "spi", "i2c", "axi", "axi4", "axi4lite", "apb", "ahb", "wishbone", "custom"], "description": "Bus protocol standard" },
|
| 77 |
+
"direction": { "type": "string", "enum": ["master", "slave", "monitor"], "default": "slave" },
|
| 78 |
+
"description": { "type": "string" },
|
| 79 |
+
"signals": { "type": "array", "items": { "$ref": "#/definitions/signal" }, "minItems": 1 }
|
| 80 |
+
}
|
| 81 |
+
},
|
| 82 |
+
|
| 83 |
+
"interrupt": {
|
| 84 |
+
"type": "object",
|
| 85 |
+
"required": ["name", "signal"],
|
| 86 |
+
"properties": {
|
| 87 |
+
"name": { "type": "string" },
|
| 88 |
+
"signal": { "type": "string" },
|
| 89 |
+
"type": { "type": "string", "enum": ["level", "edge", "pulse"], "default": "level" },
|
| 90 |
+
"sensitivity": { "type": "string", "enum": ["high", "low", "rising", "falling"], "default": "high" },
|
| 91 |
+
"description": { "type": "string" }
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
+
|
| 95 |
+
"memory_map": {
|
| 96 |
+
"type": "object",
|
| 97 |
+
"required": ["name", "base_address", "size"],
|
| 98 |
+
"properties": {
|
| 99 |
+
"name": { "type": "string" },
|
| 100 |
+
"base_address": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$" },
|
| 101 |
+
"size": { "type": "integer", "description": "Size in bytes" },
|
| 102 |
+
"description": { "type": "string" }
|
| 103 |
+
}
|
| 104 |
+
},
|
| 105 |
+
|
| 106 |
+
"coverage": {
|
| 107 |
+
"type": "object",
|
| 108 |
+
"properties": {
|
| 109 |
+
"bins": { "type": "array", "items": { "type": "object", "properties": {
|
| 110 |
+
"name": { "type": "string" },
|
| 111 |
+
"type": { "type": "string", "enum": ["coverpoint", "cross", "assertion"] },
|
| 112 |
+
"expression": { "type": "string" },
|
| 113 |
+
"description": { "type": "string" }
|
| 114 |
+
}}}
|
| 115 |
+
}
|
| 116 |
+
},
|
| 117 |
+
|
| 118 |
+
"parameter": {
|
| 119 |
+
"type": "object",
|
| 120 |
+
"required": ["name", "type"],
|
| 121 |
+
"properties": {
|
| 122 |
+
"name": { "type": "string" },
|
| 123 |
+
"type": { "type": "string", "enum": ["int", "string", "bool", "float"], "default": "int" },
|
| 124 |
+
"default": { "type": ["integer", "string", "boolean", "number"], "description": "Default value" },
|
| 125 |
+
"minimum": { "type": ["integer", "number"] },
|
| 126 |
+
"maximum": { "type": ["integer", "number"] },
|
| 127 |
+
"description": { "type": "string" }
|
| 128 |
+
}
|
| 129 |
+
},
|
| 130 |
+
|
| 131 |
+
"assertion": {
|
| 132 |
+
"type": "object",
|
| 133 |
+
"required": ["name", "expression"],
|
| 134 |
+
"properties": {
|
| 135 |
+
"name": { "type": "string" },
|
| 136 |
+
"expression": { "type": "string", "description": "SVA assertion expression" },
|
| 137 |
+
"severity": { "type": "string", "enum": ["error", "warning", "info"], "default": "error" },
|
| 138 |
+
"description": { "type": "string" }
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
|
| 143 |
+
"properties": {
|
| 144 |
+
"design_name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" },
|
| 145 |
+
"version": { "type": "string", "description": "IP version (semver)" },
|
| 146 |
+
"vendor": { "type": "string", "description": "IP vendor name" },
|
| 147 |
+
"description": { "type": "string" },
|
| 148 |
+
"technology": { "type": "string", "description": "Target technology (e.g. 28nm, FPGA)" },
|
| 149 |
+
"language": { "type": "string", "enum": ["verilog", "vhdl", "systemverilog"], "default": "systemverilog" },
|
| 150 |
+
|
| 151 |
+
"parameters": {
|
| 152 |
+
"type": "object",
|
| 153 |
+
"additionalProperties": { "$ref": "#/definitions/parameter" },
|
| 154 |
+
"description": "Design parameters / generics"
|
| 155 |
+
},
|
| 156 |
+
|
| 157 |
+
"clock_reset": {
|
| 158 |
+
"type": "object",
|
| 159 |
+
"required": ["clock", "reset"],
|
| 160 |
+
"properties": {
|
| 161 |
+
"clock": { "type": "string" },
|
| 162 |
+
"reset": { "type": "string" },
|
| 163 |
+
"reset_active": { "type": "integer", "enum": [0, 1], "default": 0 },
|
| 164 |
+
"clock_freq": { "type": "integer", "description": "Clock frequency in Hz" }
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
+
|
| 168 |
+
"clock_domains": {
|
| 169 |
+
"type": "array",
|
| 170 |
+
"items": { "$ref": "#/definitions/clock_domain" },
|
| 171 |
+
"description": "Multi-clock domain definitions"
|
| 172 |
+
},
|
| 173 |
+
|
| 174 |
+
"reset_domains": {
|
| 175 |
+
"type": "array",
|
| 176 |
+
"items": { "$ref": "#/definitions/reset_domain" },
|
| 177 |
+
"description": "Multi-reset domain definitions"
|
| 178 |
+
},
|
| 179 |
+
|
| 180 |
+
"interfaces": {
|
| 181 |
+
"type": "array",
|
| 182 |
+
"items": { "$ref": "#/definitions/interface" },
|
| 183 |
+
"minItems": 1
|
| 184 |
+
},
|
| 185 |
+
|
| 186 |
+
"registers": {
|
| 187 |
+
"type": "array",
|
| 188 |
+
"items": { "$ref": "#/definitions/register" }
|
| 189 |
+
},
|
| 190 |
+
|
| 191 |
+
"interrupts": {
|
| 192 |
+
"type": "array",
|
| 193 |
+
"items": { "$ref": "#/definitions/interrupt" }
|
| 194 |
+
},
|
| 195 |
+
|
| 196 |
+
"memory_maps": {
|
| 197 |
+
"type": "array",
|
| 198 |
+
"items": { "$ref": "#/definitions/memory_map" }
|
| 199 |
+
},
|
| 200 |
+
|
| 201 |
+
"coverage": { "$ref": "#/definitions/coverage" },
|
| 202 |
+
|
| 203 |
+
"assertions": {
|
| 204 |
+
"type": "array",
|
| 205 |
+
"items": { "$ref": "#/definitions/assertion" }
|
| 206 |
+
},
|
| 207 |
+
|
| 208 |
+
"validation_rules": {
|
| 209 |
+
"type": "object",
|
| 210 |
+
"properties": {
|
| 211 |
+
"min_registers": { "type": "boolean", "default": false },
|
| 212 |
+
"require_reset": { "type": "boolean", "default": true },
|
| 213 |
+
"require_interrupt": { "type": "boolean", "default": false },
|
| 214 |
+
"address_alignment": { "type": "integer", "default": 1, "description": "Required address alignment in bytes" },
|
| 215 |
+
"no_reserved_holes": { "type": "boolean", "default": false, "description": "No gaps in register address map" }
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
}
|
configs/uart16550-1.5.core
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# UART 16550 v1.5 Core Description
|
| 2 |
+
# FuseSoC-compatible .core format
|
| 3 |
+
# Ref: https://github.com/fusesoc/fusesoc
|
| 4 |
+
|
| 5 |
+
name: uart16550
|
| 6 |
+
version: "1.5"
|
| 7 |
+
description: Universal Asynchronous Receiver-Transmitter (UART) 16550-compatible core
|
| 8 |
+
author: DV Automation Team
|
| 9 |
+
|
| 10 |
+
parameters:
|
| 11 |
+
clk_freq:
|
| 12 |
+
datatype: int
|
| 13 |
+
default: 50000000
|
| 14 |
+
description: System clock frequency in Hz
|
| 15 |
+
baud_rate:
|
| 16 |
+
datatype: int
|
| 17 |
+
default: 115200
|
| 18 |
+
description: Target baud rate
|
| 19 |
+
data_bits:
|
| 20 |
+
datatype: int
|
| 21 |
+
default: 8
|
| 22 |
+
paramtype: vlogparam
|
| 23 |
+
fifo_depth:
|
| 24 |
+
datatype: int
|
| 25 |
+
default: 16
|
| 26 |
+
description: Depth of TX/RX FIFOs
|
| 27 |
+
|
| 28 |
+
interfaces:
|
| 29 |
+
- name: bus
|
| 30 |
+
type: wb
|
| 31 |
+
description: Wishbone bus interface
|
| 32 |
+
signals:
|
| 33 |
+
- {name: clk, direction: input, width: 1}
|
| 34 |
+
- {name: rstn, direction: input, width: 1}
|
| 35 |
+
- {name: addr, direction: input, width: 3}
|
| 36 |
+
- {name: we, direction: input, width: 1}
|
| 37 |
+
- {name: cs, direction: input, width: 1}
|
| 38 |
+
- {name: data_in, direction: input, width: 8}
|
| 39 |
+
- {name: data_out, direction: output, width: 8}
|
| 40 |
+
- {name: irq, direction: output, width: 1}
|
| 41 |
+
- name: serial
|
| 42 |
+
type: uart
|
| 43 |
+
description: Serial interface
|
| 44 |
+
signals:
|
| 45 |
+
- {name: srx, direction: input, width: 1}
|
| 46 |
+
- {name: stx, direction: output, width: 1}
|
| 47 |
+
- name: modem
|
| 48 |
+
type: modem
|
| 49 |
+
description: Modem control interface
|
| 50 |
+
signals:
|
| 51 |
+
- {name: cts_n, direction: input, width: 1}
|
| 52 |
+
- {name: rts_n, direction: output, width: 1}
|
| 53 |
+
- {name: dsr_n, direction: input, width: 1}
|
| 54 |
+
- {name: dtr_n, direction: output, width: 1}
|
| 55 |
+
- {name: ri_n, direction: input, width: 1}
|
| 56 |
+
- {name: dcd_n, direction: input, width: 1}
|
| 57 |
+
|
| 58 |
+
clock_reset:
|
| 59 |
+
clock: clk
|
| 60 |
+
reset: rstn
|
| 61 |
+
reset_active: 0
|
| 62 |
+
|
| 63 |
+
registers:
|
| 64 |
+
- name: RBR
|
| 65 |
+
description: Receiver Buffer Register (read-only, DLAB=0)
|
| 66 |
+
address: '0x00'
|
| 67 |
+
access: ro
|
| 68 |
+
fields:
|
| 69 |
+
- {name: rbr_data, bits: '7:0', description: Received data}
|
| 70 |
+
- name: THR
|
| 71 |
+
description: Transmitter Holding Register (write-only, DLAB=0)
|
| 72 |
+
address: '0x00'
|
| 73 |
+
access: wo
|
| 74 |
+
fields:
|
| 75 |
+
- {name: thr_data, bits: '7:0', description: Data to transmit}
|
| 76 |
+
- name: IER
|
| 77 |
+
description: Interrupt Enable Register
|
| 78 |
+
address: '0x01'
|
| 79 |
+
access: rw
|
| 80 |
+
fields:
|
| 81 |
+
- {name: erbfi, bits: '0', description: Enable RX Data Available Interrupt}
|
| 82 |
+
- {name: etbei, bits: '1', description: Enable TX Holding Register Empty Interrupt}
|
| 83 |
+
- {name: elsi, bits: '2', description: Enable RX Line Status Interrupt}
|
| 84 |
+
- {name: edssi, bits: '3', description: Enable Modem Status Interrupt}
|
| 85 |
+
- name: IIR
|
| 86 |
+
description: Interrupt Identification Register (read-only)
|
| 87 |
+
address: '0x02'
|
| 88 |
+
access: ro
|
| 89 |
+
fields:
|
| 90 |
+
- {name: int_id, bits: '3:0', description: Interrupt identification}
|
| 91 |
+
- {name: fifos_en, bits: '7:6', description: FIFO enable status}
|
| 92 |
+
- name: FCR
|
| 93 |
+
description: FIFO Control Register (write-only)
|
| 94 |
+
address: '0x02'
|
| 95 |
+
access: wo
|
| 96 |
+
fields:
|
| 97 |
+
- {name: fifo_en, bits: '0', description: Enable FIFOs}
|
| 98 |
+
- {name: rclr, bits: '1', description: Clear RX FIFO}
|
| 99 |
+
- {name: tclr, bits: '2', description: Clear TX FIFO}
|
| 100 |
+
- {name: dma_mode, bits: '3', description: DMA mode select}
|
| 101 |
+
- {name: rx_trigger, bits: '7:6', description: RX FIFO trigger level}
|
| 102 |
+
- name: LCR
|
| 103 |
+
description: Line Control Register
|
| 104 |
+
address: '0x03'
|
| 105 |
+
access: rw
|
| 106 |
+
fields:
|
| 107 |
+
- {name: wls, bits: '1:0', description: Word length select}
|
| 108 |
+
- {name: stb, bits: '2', description: Stop bits}
|
| 109 |
+
- {name: pen, bits: '3', description: Parity enable}
|
| 110 |
+
- {name: eps, bits: '4', description: Even parity select}
|
| 111 |
+
- {name: sp, bits: '5', description: Stick parity}
|
| 112 |
+
- {name: bc, bits: '6', description: Break control}
|
| 113 |
+
- {name: dlab, bits: '7', description: Divisor latch access bit}
|
| 114 |
+
- name: MCR
|
| 115 |
+
description: Modem Control Register
|
| 116 |
+
address: '0x04'
|
| 117 |
+
access: rw
|
| 118 |
+
fields:
|
| 119 |
+
- {name: dtr, bits: '0', description: Data Terminal Ready}
|
| 120 |
+
- {name: rts, bits: '1', description: Request To Send}
|
| 121 |
+
- {name: out1, bits: '2', description: Output 1}
|
| 122 |
+
- {name: out2, bits: '3', description: Output 2}
|
| 123 |
+
- {name: loop, bits: '4', description: Loopback mode}
|
| 124 |
+
- name: LSR
|
| 125 |
+
description: Line Status Register (read-only)
|
| 126 |
+
address: '0x05'
|
| 127 |
+
access: ro
|
| 128 |
+
fields:
|
| 129 |
+
- {name: dr, bits: '0', description: Data Ready}
|
| 130 |
+
- {name: oe, bits: '1', description: Overrun Error}
|
| 131 |
+
- {name: pe, bits: '2', description: Parity Error}
|
| 132 |
+
- {name: fe, bits: '3', description: Framing Error}
|
| 133 |
+
- {name: bi, bits: '4', description: Break Interrupt}
|
| 134 |
+
- {name: thre, bits: '5', description: TX Holding Register Empty}
|
| 135 |
+
- {name: temt, bits: '6', description: TX Empty}
|
| 136 |
+
- {name: err, bits: '7', description: Error in RX FIFO}
|
| 137 |
+
- name: MSR
|
| 138 |
+
description: Modem Status Register (read-only)
|
| 139 |
+
address: '0x06'
|
| 140 |
+
access: ro
|
| 141 |
+
fields:
|
| 142 |
+
- {name: dcts, bits: '0', description: Delta Clear To Send}
|
| 143 |
+
- {name: ddsr, bits: '1', description: Delta Data Set Ready}
|
| 144 |
+
- {name: teri, bits: '2', description: Trailing Edge RI}
|
| 145 |
+
- {name: ddcd, bits: '3', description: Delta Data Carrier Detect}
|
| 146 |
+
- {name: cts, bits: '4', description: Clear To Send}
|
| 147 |
+
- {name: dsr, bits: '5', description: Data Set Ready}
|
| 148 |
+
- {name: ri, bits: '6', description: Ring Indicator}
|
| 149 |
+
- {name: dcd, bits: '7', description: Data Carrier Detect}
|
| 150 |
+
- name: SCR
|
| 151 |
+
description: Scratch Register
|
| 152 |
+
address: '0x07'
|
| 153 |
+
access: rw
|
| 154 |
+
fields:
|
| 155 |
+
- {name: scratch, bits: '7:0', description: Scratch value}
|
| 156 |
+
- name: DLL
|
| 157 |
+
description: Divisor Latch LSB (DLAB=1)
|
| 158 |
+
address: '0x00'
|
| 159 |
+
access: rw
|
| 160 |
+
fields:
|
| 161 |
+
- {name: dll, bits: '7:0', description: Divisor latch low byte}
|
| 162 |
+
- name: DLH
|
| 163 |
+
description: Divisor Latch MSB (DLAB=1)
|
| 164 |
+
address: '0x01'
|
| 165 |
+
access: rw
|
| 166 |
+
fields:
|
| 167 |
+
- {name: dlh, bits: '7:0', description: Divisor latch high byte}
|
| 168 |
+
|
| 169 |
+
filesets:
|
| 170 |
+
- name: rtl
|
| 171 |
+
files:
|
| 172 |
+
- uart16550.v
|
| 173 |
+
- uart_baud.v
|
| 174 |
+
- uart_fifo.v
|
| 175 |
+
file_type: verilogSource
|
| 176 |
+
|
| 177 |
+
targets:
|
| 178 |
+
default:
|
| 179 |
+
filesets: [rtl]
|
| 180 |
+
parameters:
|
| 181 |
+
- clk_freq
|
| 182 |
+
- baud_rate
|
| 183 |
+
sim:
|
| 184 |
+
filesets: [rtl]
|
| 185 |
+
parameters:
|
| 186 |
+
- clk_freq
|
| 187 |
+
- baud_rate
|
| 188 |
+
tools:
|
| 189 |
+
- verilator
|
| 190 |
+
- iverilog
|
configs/uart_demo.yaml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
design_name: uart
|
| 2 |
+
clock_reset:
|
| 3 |
+
clock: clk
|
| 4 |
+
reset: rst_n
|
| 5 |
+
reset_active: 0
|
| 6 |
+
|
| 7 |
+
interfaces:
|
| 8 |
+
- name: uart_if
|
| 9 |
+
signals:
|
| 10 |
+
- name: tx
|
| 11 |
+
direction: output
|
| 12 |
+
- name: rx
|
| 13 |
+
direction: input
|
| 14 |
+
- name: baud_tick
|
| 15 |
+
direction: input
|
| 16 |
+
|
| 17 |
+
registers:
|
| 18 |
+
- name: ctrl
|
| 19 |
+
address: '0x00'
|
| 20 |
+
fields:
|
| 21 |
+
- name: enable
|
| 22 |
+
bits: '0'
|
| 23 |
+
- name: baud_div
|
| 24 |
+
bits: '7:2'
|
| 25 |
+
- name: status
|
| 26 |
+
address: '0x04'
|
| 27 |
+
fields:
|
| 28 |
+
- name: tx_full
|
| 29 |
+
bits: '0'
|
| 30 |
+
- name: rx_empty
|
| 31 |
+
bits: '1'
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
app:
|
| 3 |
+
build:
|
| 4 |
+
context: .
|
| 5 |
+
dockerfile: Dockerfile
|
| 6 |
+
container_name: uvmgen
|
| 7 |
+
ports:
|
| 8 |
+
- "8000:8000"
|
| 9 |
+
environment:
|
| 10 |
+
- PYTHONUNBUFFERED=1
|
| 11 |
+
- LOG_LEVEL=info
|
| 12 |
+
volumes:
|
| 13 |
+
- uvmgen_data:/var/data
|
| 14 |
+
healthcheck:
|
| 15 |
+
test: ["CMD", "python", "-c", "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/api/health').read().decode())"]
|
| 16 |
+
interval: 30s
|
| 17 |
+
timeout: 5s
|
| 18 |
+
retries: 3
|
| 19 |
+
start_period: 15s
|
| 20 |
+
restart: unless-stopped
|
| 21 |
+
|
| 22 |
+
volumes:
|
| 23 |
+
uvmgen_data:
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "uvm-tb-generator-ui",
|
| 3 |
+
"version": "0.3.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"description": "Semiconductor Verification Pipeline — Frontend",
|
| 6 |
+
"dependencies": {
|
| 7 |
+
"js-yaml": "^4.1.0",
|
| 8 |
+
"lucide-react": "^0.468.0",
|
| 9 |
+
"react": "^18.3.1",
|
| 10 |
+
"react-dom": "^18.3.1",
|
| 11 |
+
"react-scripts": "^5.0.1",
|
| 12 |
+
"react-syntax-highlighter": "^15.6.1"
|
| 13 |
+
},
|
| 14 |
+
"devDependencies": {
|
| 15 |
+
"autoprefixer": "^10.4.20",
|
| 16 |
+
"postcss": "^8.4.49",
|
| 17 |
+
"tailwindcss": "^3.4.17"
|
| 18 |
+
},
|
| 19 |
+
"scripts": {
|
| 20 |
+
"start": "react-scripts start",
|
| 21 |
+
"build": "react-scripts build",
|
| 22 |
+
"test": "react-scripts test",
|
| 23 |
+
"eject": "react-scripts eject"
|
| 24 |
+
},
|
| 25 |
+
"browserslist": {
|
| 26 |
+
"production": [
|
| 27 |
+
">0.2%",
|
| 28 |
+
"not dead",
|
| 29 |
+
"not op_mini all"
|
| 30 |
+
],
|
| 31 |
+
"development": [
|
| 32 |
+
"last 1 chrome version",
|
| 33 |
+
"last 1 firefox version",
|
| 34 |
+
"last 1 safari version"
|
| 35 |
+
]
|
| 36 |
+
},
|
| 37 |
+
"proxy": false
|
| 38 |
+
}
|
frontend/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
};
|
frontend/public/favicon.svg
ADDED
|
|
frontend/public/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<meta name="theme-color" content="#0f172a" />
|
| 7 |
+
<meta name="description" content="Semiconductor Verification Pipeline — UVM Testbench Generator" />
|
| 8 |
+
<title>Verification Pipeline · UVM Generator</title>
|
| 9 |
+
</head>
|
| 10 |
+
<body class="bg-slate-50 text-slate-900 antialiased">
|
| 11 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
| 12 |
+
<div id="root"></div>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|
frontend/src/App.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useCallback } from "react";
|
| 2 |
+
import Header from "./components/Header";
|
| 3 |
+
import Footer from "./components/Footer";
|
| 4 |
+
import YAMLForm from "./components/YAMLForm";
|
| 5 |
+
import PreviewPanel from "./components/PreviewPanel";
|
| 6 |
+
import PipelineRunner from "./components/PipelineRunner";
|
| 7 |
+
import ErrorBoundary from "./components/ErrorBoundary";
|
| 8 |
+
|
| 9 |
+
export default function App() {
|
| 10 |
+
const [spec, setSpec] = useState(null);
|
| 11 |
+
|
| 12 |
+
const handleSpecChange = useCallback((parsed) => {
|
| 13 |
+
setSpec(parsed);
|
| 14 |
+
}, []);
|
| 15 |
+
|
| 16 |
+
return (
|
| 17 |
+
<div className="min-h-screen flex flex-col bg-slate-50">
|
| 18 |
+
<Header />
|
| 19 |
+
|
| 20 |
+
<main className="flex-1 max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-6 space-y-6">
|
| 21 |
+
{/* Intro */}
|
| 22 |
+
<div className="bg-gradient-to-r from-brand-600 to-brand-800 rounded-xl px-6 py-5 text-white">
|
| 23 |
+
<h2 className="text-lg font-bold">UVM Testbench Generator</h2>
|
| 24 |
+
<p className="text-sm text-brand-100 mt-1 max-w-2xl">
|
| 25 |
+
Upload or paste a FuseSoC-style YAML / .core specification, edit signals and registers
|
| 26 |
+
directly, then run the pipeline to generate a complete UVM testbench.
|
| 27 |
+
</p>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
{/* Form + Preview side by side */}
|
| 31 |
+
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
| 32 |
+
<ErrorBoundary>
|
| 33 |
+
<YAMLForm onSpecChange={handleSpecChange} />
|
| 34 |
+
</ErrorBoundary>
|
| 35 |
+
<ErrorBoundary>
|
| 36 |
+
<PreviewPanel spec={spec} />
|
| 37 |
+
</ErrorBoundary>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
{/* Pipeline runner */}
|
| 41 |
+
<ErrorBoundary>
|
| 42 |
+
<PipelineRunner spec={spec} />
|
| 43 |
+
</ErrorBoundary>
|
| 44 |
+
</main>
|
| 45 |
+
|
| 46 |
+
<Footer />
|
| 47 |
+
</div>
|
| 48 |
+
);
|
| 49 |
+
}
|
frontend/src/components/ErrorBoundary.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
|
| 3 |
+
const ERROR_STYLES = {
|
| 4 |
+
container: {
|
| 5 |
+
minHeight: "200px",
|
| 6 |
+
display: "flex",
|
| 7 |
+
alignItems: "center",
|
| 8 |
+
justifyContent: "center",
|
| 9 |
+
},
|
| 10 |
+
card: {
|
| 11 |
+
maxWidth: "480px",
|
| 12 |
+
padding: "24px",
|
| 13 |
+
background: "#fff",
|
| 14 |
+
borderRadius: "12px",
|
| 15 |
+
border: "1px solid #fecaca",
|
| 16 |
+
boxShadow: "0 4px 6px -1px rgba(0,0,0,0.1)",
|
| 17 |
+
textAlign: "center",
|
| 18 |
+
},
|
| 19 |
+
title: {
|
| 20 |
+
fontSize: "16px",
|
| 21 |
+
fontWeight: 600,
|
| 22 |
+
color: "#991b1b",
|
| 23 |
+
marginBottom: "8px",
|
| 24 |
+
},
|
| 25 |
+
message: {
|
| 26 |
+
fontSize: "13px",
|
| 27 |
+
color: "#7f1d1d",
|
| 28 |
+
marginBottom: "16px",
|
| 29 |
+
lineHeight: 1.5,
|
| 30 |
+
},
|
| 31 |
+
button: {
|
| 32 |
+
padding: "8px 20px",
|
| 33 |
+
fontSize: "13px",
|
| 34 |
+
fontWeight: 500,
|
| 35 |
+
background: "#dc2626",
|
| 36 |
+
color: "#fff",
|
| 37 |
+
border: "none",
|
| 38 |
+
borderRadius: "8px",
|
| 39 |
+
cursor: "pointer",
|
| 40 |
+
},
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
export default class ErrorBoundary extends React.Component {
|
| 44 |
+
constructor(props) {
|
| 45 |
+
super(props);
|
| 46 |
+
this.state = { hasError: false, error: null };
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
static getDerivedStateFromError(error) {
|
| 50 |
+
return { hasError: true, error };
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
componentDidCatch(error, errorInfo) {
|
| 54 |
+
console.error("[ErrorBoundary]", error, errorInfo);
|
| 55 |
+
if (this.props.onError) {
|
| 56 |
+
this.props.onError(error, errorInfo);
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
handleReset = () => {
|
| 61 |
+
this.setState({ hasError: false, error: null });
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
render() {
|
| 65 |
+
if (this.state.hasError) {
|
| 66 |
+
if (this.props.fallback) {
|
| 67 |
+
return this.props.fallback;
|
| 68 |
+
}
|
| 69 |
+
return (
|
| 70 |
+
<div style={ERROR_STYLES.container}>
|
| 71 |
+
<div style={ERROR_STYLES.card}>
|
| 72 |
+
<div style={ERROR_STYLES.title}>Something went wrong</div>
|
| 73 |
+
<div style={ERROR_STYLES.message}>
|
| 74 |
+
{this.state.error?.message || "An unexpected error occurred."}
|
| 75 |
+
<br />
|
| 76 |
+
Try refreshing the page or resetting the component.
|
| 77 |
+
</div>
|
| 78 |
+
<button style={ERROR_STYLES.button} onClick={this.handleReset}>
|
| 79 |
+
Reset
|
| 80 |
+
</button>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
);
|
| 84 |
+
}
|
| 85 |
+
return this.props.children;
|
| 86 |
+
}
|
| 87 |
+
}
|
frontend/src/components/Footer.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import { Cpu } from "lucide-react";
|
| 3 |
+
|
| 4 |
+
export default function Footer() {
|
| 5 |
+
return (
|
| 6 |
+
<footer className="bg-white border-t border-slate-200 mt-12">
|
| 7 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
| 8 |
+
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
|
| 9 |
+
<div className="flex items-center gap-2 text-sm text-slate-500">
|
| 10 |
+
<Cpu size={16} className="text-brand-500" />
|
| 11 |
+
<span>
|
| 12 |
+
Semiconductor Verification Pipeline — Developed by{" "}
|
| 13 |
+
<span className="font-semibold text-slate-700">Sai Kumar Taraka</span>
|
| 14 |
+
</span>
|
| 15 |
+
</div>
|
| 16 |
+
<div className="flex items-center gap-4 text-xs text-slate-400">
|
| 17 |
+
<span>UVM TB Generator v0.3.0</span>
|
| 18 |
+
<span className="hidden sm:inline">·</span>
|
| 19 |
+
<span className="hidden sm:inline">MIT License</span>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</footer>
|
| 24 |
+
);
|
| 25 |
+
}
|
frontend/src/components/Header.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import { Cpu, GitBranch } from "lucide-react";
|
| 3 |
+
|
| 4 |
+
export default function Header() {
|
| 5 |
+
return (
|
| 6 |
+
<header className="bg-white border-b border-slate-200 sticky top-0 z-40">
|
| 7 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 8 |
+
<div className="flex items-center justify-between h-16">
|
| 9 |
+
<div className="flex items-center gap-3">
|
| 10 |
+
<div className="w-9 h-9 rounded-lg bg-brand-600 flex items-center justify-center">
|
| 11 |
+
<Cpu size={20} className="text-white" />
|
| 12 |
+
</div>
|
| 13 |
+
<div>
|
| 14 |
+
<h1 className="text-base font-bold text-slate-900 leading-tight">
|
| 15 |
+
Verification Pipeline
|
| 16 |
+
</h1>
|
| 17 |
+
<p className="text-[11px] text-slate-500 leading-tight">
|
| 18 |
+
UVM Testbench Generator
|
| 19 |
+
</p>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div className="flex items-center gap-4">
|
| 24 |
+
<a
|
| 25 |
+
href="#"
|
| 26 |
+
className="hidden sm:inline-flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-700 transition-colors"
|
| 27 |
+
>
|
| 28 |
+
<GitBranch size={14} />
|
| 29 |
+
v0.3.0
|
| 30 |
+
</a>
|
| 31 |
+
<span className="hidden sm:inline-flex items-center gap-1.5 text-xs text-slate-400">
|
| 32 |
+
<span className="w-2 h-2 rounded-full bg-emerald-400" />
|
| 33 |
+
API Ready
|
| 34 |
+
</span>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
</header>
|
| 39 |
+
);
|
| 40 |
+
}
|
frontend/src/components/PipelineRunner.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useRef, useEffect, useState } from "react";
|
| 2 |
+
import {
|
| 3 |
+
Play,
|
| 4 |
+
Square,
|
| 5 |
+
FileText,
|
| 6 |
+
Download,
|
| 7 |
+
Terminal,
|
| 8 |
+
CheckCircle2,
|
| 9 |
+
XCircle,
|
| 10 |
+
Loader2,
|
| 11 |
+
AlertTriangle,
|
| 12 |
+
Info,
|
| 13 |
+
BarChart3,
|
| 14 |
+
GitBranch,
|
| 15 |
+
RefreshCw,
|
| 16 |
+
Target,
|
| 17 |
+
TrendingUp,
|
| 18 |
+
} from "lucide-react";
|
| 19 |
+
import usePipeline from "../hooks/usePipeline";
|
| 20 |
+
import { toYAML } from "../utils/yamlUtils";
|
| 21 |
+
|
| 22 |
+
const LOG_ICONS = {
|
| 23 |
+
info: Info,
|
| 24 |
+
success: CheckCircle2,
|
| 25 |
+
error: XCircle,
|
| 26 |
+
warn: AlertTriangle,
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const LOG_COLORS = {
|
| 30 |
+
info: "text-slate-600",
|
| 31 |
+
success: "text-emerald-600",
|
| 32 |
+
error: "text-red-600",
|
| 33 |
+
warn: "text-amber-600",
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
function LogEntry({ entry }) {
|
| 37 |
+
const Icon = LOG_ICONS[entry.level] || Info;
|
| 38 |
+
return (
|
| 39 |
+
<div className={`flex items-start gap-2 ${LOG_COLORS[entry.level] || "text-slate-600"}`}>
|
| 40 |
+
<Icon size={14} className="shrink-0 mt-0.5" />
|
| 41 |
+
<span className="flex-1">{entry.message}</span>
|
| 42 |
+
<span className="text-[10px] text-slate-400 font-mono shrink-0">{entry.timestamp}</span>
|
| 43 |
+
</div>
|
| 44 |
+
);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function CoverageBar({ pct }) {
|
| 48 |
+
const color =
|
| 49 |
+
pct >= 90 ? "bg-emerald-500" : pct >= 70 ? "bg-amber-500" : "bg-red-500";
|
| 50 |
+
return (
|
| 51 |
+
<div className="flex items-center gap-2">
|
| 52 |
+
<div className="flex-1 h-2 bg-slate-100 rounded-full overflow-hidden">
|
| 53 |
+
<div
|
| 54 |
+
className={`h-full rounded-full transition-all duration-500 ${color}`}
|
| 55 |
+
style={{ width: `${Math.min(pct, 100)}%` }}
|
| 56 |
+
/>
|
| 57 |
+
</div>
|
| 58 |
+
<span className="text-xs font-mono text-slate-600 w-10 text-right">{pct.toFixed(0)}%</span>
|
| 59 |
+
</div>
|
| 60 |
+
);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function CoverageTrendChart({ trend }) {
|
| 64 |
+
if (!trend || trend.length < 2) return null;
|
| 65 |
+
const max = Math.max(...trend.map((t) => t.coverage));
|
| 66 |
+
const min = Math.min(...trend.map((t) => t.coverage));
|
| 67 |
+
const range = max - min || 1;
|
| 68 |
+
|
| 69 |
+
return (
|
| 70 |
+
<div className="rounded-lg border border-slate-200 bg-white p-4">
|
| 71 |
+
<div className="flex items-center gap-2 mb-3">
|
| 72 |
+
<TrendingUp size={16} className="text-brand-600" />
|
| 73 |
+
<span className="text-sm font-semibold text-slate-800">Coverage Trend</span>
|
| 74 |
+
{trend.length >= 2 && (
|
| 75 |
+
<span className="text-xs text-slate-400 ml-auto">
|
| 76 |
+
Delta: {(trend[trend.length-1].coverage - trend[0].coverage) > 0 ? '+' : ''}
|
| 77 |
+
{(trend[trend.length-1].coverage - trend[0].coverage).toFixed(1)}%
|
| 78 |
+
</span>
|
| 79 |
+
)}
|
| 80 |
+
</div>
|
| 81 |
+
<div className="flex items-end gap-1 h-28">
|
| 82 |
+
{trend.map((t, i) => {
|
| 83 |
+
const h = ((t.coverage - min) / range) * 100;
|
| 84 |
+
const color =
|
| 85 |
+
t.coverage >= 90 ? "bg-emerald-400" :
|
| 86 |
+
t.coverage >= 70 ? "bg-amber-400" : "bg-red-400";
|
| 87 |
+
const prevCov = i > 0 ? trend[i-1].coverage : t.coverage;
|
| 88 |
+
const gain = t.coverage - prevCov;
|
| 89 |
+
return (
|
| 90 |
+
<div key={i} className="flex-1 flex flex-col items-center gap-1">
|
| 91 |
+
<span className="text-[10px] font-mono text-slate-500">{t.coverage.toFixed(0)}%</span>
|
| 92 |
+
<div className="flex flex-col items-center w-full">
|
| 93 |
+
{i > 0 && gain !== 0 && (
|
| 94 |
+
<span className={`text-[8px] ${gain > 0 ? 'text-emerald-500' : 'text-red-400'}`}>
|
| 95 |
+
{gain > 0 ? '+' : ''}{gain.toFixed(1)}%
|
| 96 |
+
</span>
|
| 97 |
+
)}
|
| 98 |
+
<div
|
| 99 |
+
className={`w-full rounded-t transition-all duration-500 ${color}`}
|
| 100 |
+
style={{ height: `${Math.max(h, 5)}%`, minHeight: '8px' }}
|
| 101 |
+
title={`${t.version}: ${t.coverage.toFixed(1)}%`}
|
| 102 |
+
/>
|
| 103 |
+
</div>
|
| 104 |
+
<span className="text-[9px] text-slate-400">{t.version}</span>
|
| 105 |
+
</div>
|
| 106 |
+
);
|
| 107 |
+
})}
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
function SeedCoverageSummary({ seeds }) {
|
| 114 |
+
if (!seeds || seeds.length === 0) return null;
|
| 115 |
+
const best = Math.max(...seeds.map(s => s.pct));
|
| 116 |
+
const worst = Math.min(...seeds.map(s => s.pct));
|
| 117 |
+
return (
|
| 118 |
+
<div className="flex items-center gap-3 text-xs text-slate-500">
|
| 119 |
+
<span className="font-medium text-slate-600">Seeds:</span>
|
| 120 |
+
{seeds.map((s, i) => {
|
| 121 |
+
const color = s.pct >= 90 ? "text-emerald-600" : s.pct >= 70 ? "text-amber-600" : "text-red-600";
|
| 122 |
+
return <span key={i} className={`font-mono ${color}`}>s{i+1}={s.pct.toFixed(0)}%</span>;
|
| 123 |
+
})}
|
| 124 |
+
<span className="text-slate-300">|</span>
|
| 125 |
+
<span className="font-mono text-slate-500">Δ{best - worst > 0 ? '+' : ''}{(best - worst).toFixed(0)}%</span>
|
| 126 |
+
</div>
|
| 127 |
+
);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function VersionHistory({ versions, latestGaps }) {
|
| 131 |
+
if (!versions || versions.length === 0) return null;
|
| 132 |
+
const passed = versions.filter(v => v.coverage >= 90).length;
|
| 133 |
+
const failed = versions.length - passed;
|
| 134 |
+
const bestCov = Math.max(...versions.map(v => v.coverage));
|
| 135 |
+
return (
|
| 136 |
+
<div className="rounded-lg border border-slate-200 bg-white">
|
| 137 |
+
<div className="flex items-center gap-2 px-4 py-3 border-b border-slate-100">
|
| 138 |
+
<GitBranch size={16} className="text-brand-600" />
|
| 139 |
+
<span className="text-sm font-semibold text-slate-800">Version History</span>
|
| 140 |
+
<span className="text-xs text-slate-400 ml-auto">{versions.length} versions</span>
|
| 141 |
+
</div>
|
| 142 |
+
{/* Pass/Fail summary */}
|
| 143 |
+
<div className="flex items-center gap-4 px-4 py-2 bg-slate-50 border-b border-slate-100">
|
| 144 |
+
<div className="flex items-center gap-1.5">
|
| 145 |
+
<CheckCircle2 size={14} className="text-emerald-500" />
|
| 146 |
+
<span className="text-xs font-medium text-slate-700">{passed} passed</span>
|
| 147 |
+
</div>
|
| 148 |
+
<div className="flex items-center gap-1.5">
|
| 149 |
+
<XCircle size={14} className="text-red-500" />
|
| 150 |
+
<span className="text-xs font-medium text-slate-700">{failed} failed</span>
|
| 151 |
+
</div>
|
| 152 |
+
<div className="flex items-center gap-1.5 ml-auto">
|
| 153 |
+
<span className="text-xs text-slate-400">Best:</span>
|
| 154 |
+
<span className="text-xs font-bold text-slate-700">{bestCov.toFixed(1)}%</span>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
<div className="divide-y divide-slate-100 max-h-64 overflow-y-auto">
|
| 158 |
+
{[...versions].reverse().map((v, i) => {
|
| 159 |
+
const prev = i < versions.length - 1 ? versions[versions.length - 1 - (i + 1)] : null;
|
| 160 |
+
const delta = prev ? v.coverage - prev.coverage : null;
|
| 161 |
+
return (
|
| 162 |
+
<div key={i} className="flex items-center gap-3 px-4 py-2.5">
|
| 163 |
+
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
| 164 |
+
v.coverage >= 90 ? 'bg-emerald-50' : v.coverage >= 70 ? 'bg-amber-50' : 'bg-red-50'
|
| 165 |
+
}`}>
|
| 166 |
+
<span className={`text-xs font-bold ${
|
| 167 |
+
v.coverage >= 90 ? 'text-emerald-700' : v.coverage >= 70 ? 'text-amber-700' : 'text-red-700'
|
| 168 |
+
}`}>{v.version}</span>
|
| 169 |
+
</div>
|
| 170 |
+
<div className="flex-1">
|
| 171 |
+
<div className="flex items-center gap-2">
|
| 172 |
+
<span className="text-sm text-slate-700">{v.files} files</span>
|
| 173 |
+
<span className="text-[11px] text-slate-300">|</span>
|
| 174 |
+
<span className={`text-sm font-medium ${
|
| 175 |
+
v.coverage >= 90 ? 'text-emerald-700' : 'text-slate-800'
|
| 176 |
+
}`}>{v.coverage.toFixed(1)}%</span>
|
| 177 |
+
{delta !== null && (
|
| 178 |
+
<span className={`text-[11px] font-mono ${
|
| 179 |
+
delta > 0 ? 'text-emerald-500' : delta < 0 ? 'text-red-400' : 'text-slate-400'
|
| 180 |
+
}`}>
|
| 181 |
+
{delta > 0 ? '+' : ''}{delta.toFixed(1)}%
|
| 182 |
+
</span>
|
| 183 |
+
)}
|
| 184 |
+
</div>
|
| 185 |
+
<CoverageBar pct={v.coverage} />
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
);
|
| 189 |
+
})}
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
function CoverageGapsList({ gaps }) {
|
| 196 |
+
if (!gaps || gaps.length === 0) return null;
|
| 197 |
+
return (
|
| 198 |
+
<div className="rounded-lg border border-amber-200 bg-amber-50">
|
| 199 |
+
<div className="flex items-center gap-2 px-4 py-3 border-b border-amber-100">
|
| 200 |
+
<Target size={16} className="text-amber-600" />
|
| 201 |
+
<span className="text-sm font-semibold text-amber-800">Coverage Gaps</span>
|
| 202 |
+
<span className="text-xs text-amber-600 ml-auto">{gaps.length} uncovered</span>
|
| 203 |
+
</div>
|
| 204 |
+
<div className="p-3 space-y-1 max-h-32 overflow-y-auto">
|
| 205 |
+
{gaps.map((g, i) => (
|
| 206 |
+
<div key={i} className="flex items-center gap-2 text-xs text-amber-700 font-mono">
|
| 207 |
+
<span className="w-2 h-2 rounded-full bg-amber-400 shrink-0" />
|
| 208 |
+
{g.bin}
|
| 209 |
+
</div>
|
| 210 |
+
))}
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
export default function PipelineRunner({ spec }) {
|
| 217 |
+
const {
|
| 218 |
+
running,
|
| 219 |
+
autoTraining,
|
| 220 |
+
logs,
|
| 221 |
+
artifacts,
|
| 222 |
+
error,
|
| 223 |
+
versions,
|
| 224 |
+
coverageTrend,
|
| 225 |
+
coverageGaps,
|
| 226 |
+
seedResults,
|
| 227 |
+
autoTrainMax,
|
| 228 |
+
runPipeline,
|
| 229 |
+
runAutoTrain,
|
| 230 |
+
} = usePipeline();
|
| 231 |
+
const logEndRef = useRef(null);
|
| 232 |
+
const [maxIter, setMaxIter] = useState(5);
|
| 233 |
+
|
| 234 |
+
useEffect(() => {
|
| 235 |
+
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
| 236 |
+
}, [logs]);
|
| 237 |
+
|
| 238 |
+
const handleRun = () => {
|
| 239 |
+
if (!spec) return;
|
| 240 |
+
const yamlText = toYAML(spec);
|
| 241 |
+
runPipeline(yamlText);
|
| 242 |
+
};
|
| 243 |
+
|
| 244 |
+
const handleAutoTrain = () => {
|
| 245 |
+
if (!spec) return;
|
| 246 |
+
const yamlText = toYAML(spec);
|
| 247 |
+
runAutoTrain(yamlText, maxIter);
|
| 248 |
+
};
|
| 249 |
+
|
| 250 |
+
const handleDownload = (file) => {
|
| 251 |
+
const blob = new Blob([file.content || ""], { type: "text/plain" });
|
| 252 |
+
const url = URL.createObjectURL(blob);
|
| 253 |
+
const a = document.createElement("a");
|
| 254 |
+
a.href = url;
|
| 255 |
+
a.download = file.name;
|
| 256 |
+
document.body.appendChild(a);
|
| 257 |
+
a.click();
|
| 258 |
+
document.body.removeChild(a);
|
| 259 |
+
URL.revokeObjectURL(url);
|
| 260 |
+
};
|
| 261 |
+
|
| 262 |
+
const handleDownloadAll = () => {
|
| 263 |
+
artifacts.forEach((file) => handleDownload(file));
|
| 264 |
+
};
|
| 265 |
+
|
| 266 |
+
const canRun = spec && Object.keys(spec).length > 0 && !running;
|
| 267 |
+
|
| 268 |
+
return (
|
| 269 |
+
<div className="card">
|
| 270 |
+
<div className="card-header">
|
| 271 |
+
<Terminal size={18} className="text-brand-600" />
|
| 272 |
+
<h2>Pipeline Runner</h2>
|
| 273 |
+
{autoTraining && (
|
| 274 |
+
<span className="badge badge-warning text-xs ml-2">
|
| 275 |
+
<RefreshCw size={12} className="animate-spin inline mr-1" />
|
| 276 |
+
Auto-Training
|
| 277 |
+
</span>
|
| 278 |
+
)}
|
| 279 |
+
{versions.length > 0 && !running && (
|
| 280 |
+
<span className="badge badge-success text-xs ml-2">v{versions.length}</span>
|
| 281 |
+
)}
|
| 282 |
+
|
| 283 |
+
<div className="ml-auto flex items-center gap-3">
|
| 284 |
+
{spec && (
|
| 285 |
+
<span className="text-xs text-slate-400">
|
| 286 |
+
Spec: <span className="font-mono text-slate-600">{spec.design_name || "unnamed"}</span>
|
| 287 |
+
</span>
|
| 288 |
+
)}
|
| 289 |
+
<div className="flex items-center gap-2">
|
| 290 |
+
<label className="text-[11px] text-slate-400">Iterations</label>
|
| 291 |
+
<input
|
| 292 |
+
type="number"
|
| 293 |
+
min={1}
|
| 294 |
+
max={20}
|
| 295 |
+
value={maxIter}
|
| 296 |
+
onChange={(e) => setMaxIter(Math.max(1, Math.min(20, parseInt(e.target.value) || 5)))}
|
| 297 |
+
className="w-14 px-2 py-1 text-xs border border-slate-200 rounded-md text-center"
|
| 298 |
+
disabled={running}
|
| 299 |
+
/>
|
| 300 |
+
</div>
|
| 301 |
+
<button
|
| 302 |
+
className="btn-primary text-xs"
|
| 303 |
+
onClick={handleRun}
|
| 304 |
+
disabled={!canRun}
|
| 305 |
+
title="Single pass generation"
|
| 306 |
+
>
|
| 307 |
+
{running && !autoTraining ? (
|
| 308 |
+
<>
|
| 309 |
+
<Loader2 size={14} className="animate-spin" />
|
| 310 |
+
Running...
|
| 311 |
+
</>
|
| 312 |
+
) : (
|
| 313 |
+
<>
|
| 314 |
+
<Play size={14} />
|
| 315 |
+
Run
|
| 316 |
+
</>
|
| 317 |
+
)}
|
| 318 |
+
</button>
|
| 319 |
+
<button
|
| 320 |
+
className={running ? "btn-secondary text-xs opacity-50" : "btn-primary text-xs bg-amber-600 hover:bg-amber-700"}
|
| 321 |
+
onClick={handleAutoTrain}
|
| 322 |
+
disabled={!canRun}
|
| 323 |
+
title={`Coverage-driven auto-training (${maxIter} iterations max)`}
|
| 324 |
+
>
|
| 325 |
+
{running && autoTraining ? (
|
| 326 |
+
<>
|
| 327 |
+
<Loader2 size={14} className="animate-spin" />
|
| 328 |
+
Training...
|
| 329 |
+
</>
|
| 330 |
+
) : (
|
| 331 |
+
<>
|
| 332 |
+
<RefreshCw size={14} />
|
| 333 |
+
Auto-Train
|
| 334 |
+
</>
|
| 335 |
+
)}
|
| 336 |
+
</button>
|
| 337 |
+
</div>
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
<div className="card-body space-y-4">
|
| 341 |
+
{/* Logs */}
|
| 342 |
+
<div className="rounded-lg bg-slate-900 border border-slate-700">
|
| 343 |
+
<div className="flex items-center justify-between px-4 py-2 border-b border-slate-700">
|
| 344 |
+
<span className="text-[11px] font-medium text-slate-400 uppercase tracking-wider">
|
| 345 |
+
Pipeline Logs
|
| 346 |
+
</span>
|
| 347 |
+
<span className="text-[11px] text-slate-500">{logs.length} entries</span>
|
| 348 |
+
</div>
|
| 349 |
+
<div className="p-4 space-y-1 max-h-48 overflow-y-auto" style={{ minHeight: "80px" }}>
|
| 350 |
+
{logs.length === 0 && (
|
| 351 |
+
<p className="text-xs text-slate-500 italic">Click "Run" or "Auto-Train" to start</p>
|
| 352 |
+
)}
|
| 353 |
+
{logs.map((entry, i) => (
|
| 354 |
+
<LogEntry key={i} entry={entry} />
|
| 355 |
+
))}
|
| 356 |
+
<div ref={logEndRef} />
|
| 357 |
+
</div>
|
| 358 |
+
</div>
|
| 359 |
+
|
| 360 |
+
{/* Error */}
|
| 361 |
+
{error && (
|
| 362 |
+
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3">
|
| 363 |
+
<div className="flex items-center gap-2 text-sm text-red-700">
|
| 364 |
+
<XCircle size={16} />
|
| 365 |
+
{error}
|
| 366 |
+
</div>
|
| 367 |
+
</div>
|
| 368 |
+
)}
|
| 369 |
+
|
| 370 |
+
{/* Coverage Trend */}
|
| 371 |
+
{coverageTrend && coverageTrend.length > 1 && <CoverageTrendChart trend={coverageTrend} />}
|
| 372 |
+
|
| 373 |
+
{/* Seed Coverage Summary */}
|
| 374 |
+
{seedResults && seedResults.length > 0 && (
|
| 375 |
+
<SeedCoverageSummary seeds={seedResults} />
|
| 376 |
+
)}
|
| 377 |
+
|
| 378 |
+
{/* Coverage Gaps */}
|
| 379 |
+
<CoverageGapsList gaps={coverageGaps} />
|
| 380 |
+
|
| 381 |
+
{/* Version History + Artifacts side by side */}
|
| 382 |
+
{versions.length > 0 && (
|
| 383 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
| 384 |
+
{/* Version History */}
|
| 385 |
+
<div className="lg:col-span-1">
|
| 386 |
+
<VersionHistory versions={versions} latestGaps={coverageGaps} />
|
| 387 |
+
</div>
|
| 388 |
+
|
| 389 |
+
{/* Artifacts */}
|
| 390 |
+
<div className="lg:col-span-2">
|
| 391 |
+
<div className="rounded-lg border border-slate-200 bg-white">
|
| 392 |
+
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100">
|
| 393 |
+
<div className="flex items-center gap-2">
|
| 394 |
+
<FileText size={16} className="text-brand-600" />
|
| 395 |
+
<h3 className="text-sm font-semibold text-slate-800">Generated Artifacts</h3>
|
| 396 |
+
</div>
|
| 397 |
+
<div className="flex items-center gap-2">
|
| 398 |
+
<span className="text-xs text-slate-400">{artifacts.length} files</span>
|
| 399 |
+
<button className="btn-secondary text-xs" onClick={handleDownloadAll}>
|
| 400 |
+
<Download size={14} />
|
| 401 |
+
Download All
|
| 402 |
+
</button>
|
| 403 |
+
</div>
|
| 404 |
+
</div>
|
| 405 |
+
<div className="divide-y divide-slate-100 max-h-80 overflow-y-auto">
|
| 406 |
+
{artifacts.map((file, i) => (
|
| 407 |
+
<div
|
| 408 |
+
key={i}
|
| 409 |
+
className="flex items-center gap-3 px-4 py-2.5 hover:bg-slate-50 cursor-pointer transition-colors"
|
| 410 |
+
onClick={() => handleDownload(file)}
|
| 411 |
+
>
|
| 412 |
+
<div className="w-8 h-8 rounded-md bg-brand-50 flex items-center justify-center shrink-0">
|
| 413 |
+
<FileText size={16} className="text-brand-600" />
|
| 414 |
+
</div>
|
| 415 |
+
<div className="flex-1 min-w-0">
|
| 416 |
+
<p className="text-sm font-medium text-slate-800 truncate">{file.name}</p>
|
| 417 |
+
<p className="text-[11px] text-slate-400 truncate">{file.path}</p>
|
| 418 |
+
</div>
|
| 419 |
+
<div className="flex items-center gap-2 shrink-0">
|
| 420 |
+
<span className="text-[11px] text-slate-400">{file.size}</span>
|
| 421 |
+
<button className="p-1.5 rounded-md text-slate-400 hover:text-brand-600 hover:bg-brand-50 transition-all">
|
| 422 |
+
<Download size={14} />
|
| 423 |
+
</button>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
))}
|
| 427 |
+
</div>
|
| 428 |
+
</div>
|
| 429 |
+
</div>
|
| 430 |
+
</div>
|
| 431 |
+
)}
|
| 432 |
+
|
| 433 |
+
{/* Summary footer */}
|
| 434 |
+
{versions.length > 0 && !running && (
|
| 435 |
+
<div className="flex items-center justify-between text-xs text-slate-400 border-t border-slate-100 pt-3">
|
| 436 |
+
<div className="flex items-center gap-4">
|
| 437 |
+
<span className="flex items-center gap-1">
|
| 438 |
+
<GitBranch size={12} />
|
| 439 |
+
{versions.length} versions
|
| 440 |
+
</span>
|
| 441 |
+
<span className="flex items-center gap-1">
|
| 442 |
+
<BarChart3 size={12} />
|
| 443 |
+
Best: {Math.max(...versions.map((v) => v.coverage)).toFixed(1)}%
|
| 444 |
+
</span>
|
| 445 |
+
<span className="flex items-center gap-1">
|
| 446 |
+
<FileText size={12} />
|
| 447 |
+
{artifacts.length} files
|
| 448 |
+
</span>
|
| 449 |
+
</div>
|
| 450 |
+
{coverageGaps && coverageGaps.length > 0 && (
|
| 451 |
+
<span className="text-amber-600 flex items-center gap-1">
|
| 452 |
+
<Target size={12} />
|
| 453 |
+
{coverageGaps.length} gaps remaining
|
| 454 |
+
</span>
|
| 455 |
+
)}
|
| 456 |
+
</div>
|
| 457 |
+
)}
|
| 458 |
+
</div>
|
| 459 |
+
</div>
|
| 460 |
+
);
|
| 461 |
+
}
|
frontend/src/components/PreviewPanel.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useMemo } from "react";
|
| 2 |
+
import { Eye, AlertCircle, CheckCircle2, Layers } from "lucide-react";
|
| 3 |
+
import { toYAML, validateYAML, PROTOCOLS } from "../utils/yamlUtils";
|
| 4 |
+
|
| 5 |
+
function MissingField({ label }) {
|
| 6 |
+
return (
|
| 7 |
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-red-50 text-red-600 text-[11px] font-medium">
|
| 8 |
+
<AlertCircle size={12} />
|
| 9 |
+
missing: {label}
|
| 10 |
+
</span>
|
| 11 |
+
);
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
function ProtocolBadge({ protocol }) {
|
| 15 |
+
const colorMap = {
|
| 16 |
+
uart: "bg-sky-50 text-sky-700",
|
| 17 |
+
spi: "bg-violet-50 text-violet-700",
|
| 18 |
+
i2c: "bg-amber-50 text-amber-700",
|
| 19 |
+
axi4lite: "bg-rose-50 text-rose-700",
|
| 20 |
+
apb: "bg-teal-50 text-teal-700",
|
| 21 |
+
wishbone: "bg-indigo-50 text-indigo-700",
|
| 22 |
+
};
|
| 23 |
+
const cls = colorMap[protocol] || "bg-slate-100 text-slate-600";
|
| 24 |
+
return (
|
| 25 |
+
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${cls}`}>
|
| 26 |
+
{protocol.toUpperCase()}
|
| 27 |
+
</span>
|
| 28 |
+
);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function Section({ title, children, icon, count, error }) {
|
| 32 |
+
return (
|
| 33 |
+
<div className={`rounded-lg border ${error ? "border-red-200 bg-red-50/30" : "border-slate-200"} p-4 space-y-3`}>
|
| 34 |
+
<div className="flex items-center justify-between">
|
| 35 |
+
<div className="flex items-center gap-2 text-sm font-semibold text-slate-800">
|
| 36 |
+
{icon}
|
| 37 |
+
{title}
|
| 38 |
+
{count !== undefined && (
|
| 39 |
+
<span className="text-xs font-normal text-slate-400">({count})</span>
|
| 40 |
+
)}
|
| 41 |
+
</div>
|
| 42 |
+
{error && <MissingField label={error} />}
|
| 43 |
+
</div>
|
| 44 |
+
{children}
|
| 45 |
+
</div>
|
| 46 |
+
);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export default function PreviewPanel({ spec }) {
|
| 50 |
+
const errors = useMemo(() => (spec ? validateYAML(toYAML(spec)) : ["No spec loaded"]), [spec]);
|
| 51 |
+
|
| 52 |
+
if (!spec || Object.keys(spec).length === 0) {
|
| 53 |
+
return (
|
| 54 |
+
<div className="card">
|
| 55 |
+
<div className="card-header">
|
| 56 |
+
<Eye size={18} className="text-slate-400" />
|
| 57 |
+
<h2 className="text-slate-400">Preview</h2>
|
| 58 |
+
</div>
|
| 59 |
+
<div className="card-body">
|
| 60 |
+
<div className="flex flex-col items-center justify-center py-12 text-slate-400">
|
| 61 |
+
<Layers size={40} className="mb-3 opacity-40" />
|
| 62 |
+
<p className="text-sm">Load a spec to see the parsed preview</p>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const hasErrors = errors.length > 0;
|
| 70 |
+
|
| 71 |
+
return (
|
| 72 |
+
<div className="card">
|
| 73 |
+
<div className="card-header">
|
| 74 |
+
<Eye size={18} className="text-brand-600" />
|
| 75 |
+
<h2>Parsed Specification</h2>
|
| 76 |
+
<span className={hasErrors ? "badge-error ml-auto" : "badge-success ml-auto"}>
|
| 77 |
+
{hasErrors ? `${errors.length} issue${errors.length > 1 ? "s" : ""}` : "Valid"}
|
| 78 |
+
</span>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<div className="card-body space-y-4">
|
| 82 |
+
{/* Errors */}
|
| 83 |
+
{hasErrors && (
|
| 84 |
+
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3">
|
| 85 |
+
<div className="text-xs text-red-700 space-y-0.5">
|
| 86 |
+
{errors.map((e, i) => (
|
| 87 |
+
<p key={i} className="flex items-center gap-1.5">
|
| 88 |
+
<AlertCircle size={12} className="shrink-0" />
|
| 89 |
+
{e}
|
| 90 |
+
</p>
|
| 91 |
+
))}
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
)}
|
| 95 |
+
|
| 96 |
+
{/* Design metadata */}
|
| 97 |
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
| 98 |
+
<div className="bg-slate-50 rounded-lg p-3">
|
| 99 |
+
<p className="text-[11px] text-slate-500 font-medium uppercase tracking-wider">Design</p>
|
| 100 |
+
<p className="text-sm font-semibold text-slate-900 mt-0.5">
|
| 101 |
+
{spec.design_name || <MissingField label="design_name" />}
|
| 102 |
+
</p>
|
| 103 |
+
</div>
|
| 104 |
+
<div className="bg-slate-50 rounded-lg p-3">
|
| 105 |
+
<p className="text-[11px] text-slate-500 font-medium uppercase tracking-wider">Version</p>
|
| 106 |
+
<p className="text-sm text-slate-900 mt-0.5">{spec.version || "—"}</p>
|
| 107 |
+
</div>
|
| 108 |
+
<div className="bg-slate-50 rounded-lg p-3">
|
| 109 |
+
<p className="text-[11px] text-slate-500 font-medium uppercase tracking-wider">Vendor</p>
|
| 110 |
+
<p className="text-sm text-slate-900 mt-0.5">{spec.vendor || "—"}</p>
|
| 111 |
+
</div>
|
| 112 |
+
<div className="bg-slate-50 rounded-lg p-3">
|
| 113 |
+
<p className="text-[11px] text-slate-500 font-medium uppercase tracking-wider">Protocol</p>
|
| 114 |
+
<p className="mt-0.5">
|
| 115 |
+
{spec.protocol ? (
|
| 116 |
+
<ProtocolBadge protocol={spec.protocol} />
|
| 117 |
+
) : (
|
| 118 |
+
<span className="badge-warning">Auto</span>
|
| 119 |
+
)}
|
| 120 |
+
</p>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
{/* Clock / Reset */}
|
| 125 |
+
<Section
|
| 126 |
+
title="Clock & Reset"
|
| 127 |
+
icon={<span className="w-2 h-2 rounded-full bg-amber-400" />}
|
| 128 |
+
error={!spec.clock_reset ? "clock_reset" : null}
|
| 129 |
+
>
|
| 130 |
+
<div className="grid grid-cols-3 gap-3 text-sm">
|
| 131 |
+
<div>
|
| 132 |
+
<span className="text-[11px] text-slate-500">Clock</span>
|
| 133 |
+
<p className="font-mono text-slate-900">{spec.clock_reset?.clock || "—"}</p>
|
| 134 |
+
</div>
|
| 135 |
+
<div>
|
| 136 |
+
<span className="text-[11px] text-slate-500">Reset</span>
|
| 137 |
+
<p className="font-mono text-slate-900">{spec.clock_reset?.reset || "—"}</p>
|
| 138 |
+
</div>
|
| 139 |
+
<div>
|
| 140 |
+
<span className="text-[11px] text-slate-500">Active</span>
|
| 141 |
+
<p className="font-mono text-slate-900">
|
| 142 |
+
{spec.clock_reset?.reset_active === 0 ? "Low" : spec.clock_reset?.reset_active === 1 ? "High" : "—"}
|
| 143 |
+
</p>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
</Section>
|
| 147 |
+
|
| 148 |
+
{/* Interfaces */}
|
| 149 |
+
<Section
|
| 150 |
+
title="Interfaces"
|
| 151 |
+
icon={<span className="w-2 h-2 rounded-full bg-brand-500" />}
|
| 152 |
+
count={spec.interfaces?.length}
|
| 153 |
+
error={!spec.interfaces?.length ? "interfaces" : null}
|
| 154 |
+
>
|
| 155 |
+
{(spec.interfaces || []).map((iface, i) => (
|
| 156 |
+
<div key={i} className="border-t border-slate-100 pt-2 first:border-0 first:pt-0">
|
| 157 |
+
<div className="flex items-center gap-2 mb-1.5">
|
| 158 |
+
<span className="text-sm font-medium text-slate-800">{iface.name || `iface_${i}`}</span>
|
| 159 |
+
{iface.protocol && <ProtocolBadge protocol={iface.protocol} />}
|
| 160 |
+
<span className="text-xs text-slate-400">
|
| 161 |
+
{Array.isArray(iface.signals) ? iface.signals.length : 0} signals
|
| 162 |
+
</span>
|
| 163 |
+
</div>
|
| 164 |
+
<div className="flex flex-wrap gap-1.5">
|
| 165 |
+
{Array.isArray(iface.signals) && iface.signals.map((sig, j) => (
|
| 166 |
+
<span
|
| 167 |
+
key={j}
|
| 168 |
+
className={`inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] font-mono
|
| 169 |
+
${sig.direction === "output"
|
| 170 |
+
? "bg-emerald-50 text-emerald-700"
|
| 171 |
+
: sig.direction === "inout"
|
| 172 |
+
? "bg-amber-50 text-amber-700"
|
| 173 |
+
: "bg-sky-50 text-sky-700"
|
| 174 |
+
}`}
|
| 175 |
+
>
|
| 176 |
+
{sig.name}
|
| 177 |
+
<span className="opacity-60">
|
| 178 |
+
{(sig.width || 1) > 1 ? `[${sig.width - 1}:0]` : ""}
|
| 179 |
+
</span>
|
| 180 |
+
<span className="opacity-50 text-[10px]">({sig.direction[0]})</span>
|
| 181 |
+
</span>
|
| 182 |
+
))}
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
))}
|
| 186 |
+
</Section>
|
| 187 |
+
|
| 188 |
+
{/* Registers */}
|
| 189 |
+
<Section
|
| 190 |
+
title="Registers"
|
| 191 |
+
icon={<span className="w-2 h-2 rounded-full bg-violet-500" />}
|
| 192 |
+
count={spec.registers?.length}
|
| 193 |
+
>
|
| 194 |
+
{(spec.registers || []).length === 0 && (
|
| 195 |
+
<p className="text-xs text-slate-400 italic">No registers defined</p>
|
| 196 |
+
)}
|
| 197 |
+
{(spec.registers || []).map((reg, i) => (
|
| 198 |
+
<div key={i} className="border-t border-slate-100 pt-2 first:border-0 first:pt-0">
|
| 199 |
+
<div className="flex items-center gap-2 mb-1">
|
| 200 |
+
<span className="text-sm font-mono font-semibold text-slate-800">{reg.name}</span>
|
| 201 |
+
<span className="text-xs font-mono text-slate-500">@{reg.address}</span>
|
| 202 |
+
<span className={`badge text-[10px] ${
|
| 203 |
+
reg.access === "ro" ? "badge-warning" :
|
| 204 |
+
reg.access === "wo" ? "badge-error" :
|
| 205 |
+
"badge-info"
|
| 206 |
+
}`}>
|
| 207 |
+
[{reg.access || "rw"}]
|
| 208 |
+
</span>
|
| 209 |
+
{reg.description && (
|
| 210 |
+
<span className="text-xs text-slate-400 truncate max-w-[200px]">{reg.description}</span>
|
| 211 |
+
)}
|
| 212 |
+
</div>
|
| 213 |
+
{(reg.fields || []).length > 0 && (
|
| 214 |
+
<div className="ml-3 flex flex-wrap gap-1">
|
| 215 |
+
{(reg.fields || []).map((f, j) => (
|
| 216 |
+
<span key={j} className="inline-flex items-center gap-1 rounded bg-slate-100 px-1.5 py-0.5 text-[11px] font-mono text-slate-600">
|
| 217 |
+
{f.name}[{f.bits}]
|
| 218 |
+
</span>
|
| 219 |
+
))}
|
| 220 |
+
</div>
|
| 221 |
+
)}
|
| 222 |
+
</div>
|
| 223 |
+
))}
|
| 224 |
+
</Section>
|
| 225 |
+
|
| 226 |
+
{/* Parameters */}
|
| 227 |
+
{spec.parameters && Object.keys(spec.parameters).length > 0 && (
|
| 228 |
+
<Section
|
| 229 |
+
title="Parameters"
|
| 230 |
+
icon={<span className="w-2 h-2 rounded-full bg-slate-400" />}
|
| 231 |
+
count={Object.keys(spec.parameters).length}
|
| 232 |
+
>
|
| 233 |
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
| 234 |
+
{Object.entries(spec.parameters).map(([k, v]) => (
|
| 235 |
+
<div key={k} className="bg-slate-50 rounded px-2.5 py-1.5">
|
| 236 |
+
<p className="text-[11px] font-mono text-slate-700">{k}</p>
|
| 237 |
+
<p className="text-xs text-slate-500">{v ?? "—"}</p>
|
| 238 |
+
</div>
|
| 239 |
+
))}
|
| 240 |
+
</div>
|
| 241 |
+
</Section>
|
| 242 |
+
)}
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
);
|
| 246 |
+
}
|
frontend/src/components/YAMLForm.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useCallback } from "react";
|
| 2 |
+
import {
|
| 3 |
+
Upload,
|
| 4 |
+
FileCode,
|
| 5 |
+
Plus,
|
| 6 |
+
Trash2,
|
| 7 |
+
AlertCircle,
|
| 8 |
+
CheckCircle2,
|
| 9 |
+
Settings2,
|
| 10 |
+
} from "lucide-react";
|
| 11 |
+
import { parseYAML, validateYAML, toYAML, PROTOCOLS, DEFAULT_YAML } from "../utils/yamlUtils";
|
| 12 |
+
|
| 13 |
+
export default function YAMLForm({ onSpecChange, validationErrors }) {
|
| 14 |
+
const [yamlText, setYamlText] = useState(DEFAULT_YAML);
|
| 15 |
+
const [parsed, setParsed] = useState(() => parseYAML(DEFAULT_YAML));
|
| 16 |
+
const [mode, setMode] = useState("editor"); // editor | parsed | split
|
| 17 |
+
const [activeTab, setActiveTab] = useState("spec"); // spec | signals | registers
|
| 18 |
+
const fileRef = useRef(null);
|
| 19 |
+
|
| 20 |
+
const handleChange = useCallback(
|
| 21 |
+
(text) => {
|
| 22 |
+
setYamlText(text);
|
| 23 |
+
try {
|
| 24 |
+
const obj = parseYAML(text);
|
| 25 |
+
setParsed(obj);
|
| 26 |
+
onSpecChange(obj);
|
| 27 |
+
} catch {
|
| 28 |
+
// keep previous parsed state
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
[onSpecChange]
|
| 32 |
+
);
|
| 33 |
+
|
| 34 |
+
const handleFileUpload = useCallback(
|
| 35 |
+
(e) => {
|
| 36 |
+
const file = e.target.files?.[0];
|
| 37 |
+
if (!file) return;
|
| 38 |
+
const reader = new FileReader();
|
| 39 |
+
reader.onload = (ev) => handleChange(ev.target.result);
|
| 40 |
+
reader.readAsText(file);
|
| 41 |
+
},
|
| 42 |
+
[handleChange]
|
| 43 |
+
);
|
| 44 |
+
|
| 45 |
+
const handlePasteExample = useCallback(() => {
|
| 46 |
+
handleChange(DEFAULT_YAML);
|
| 47 |
+
}, [handleChange]);
|
| 48 |
+
|
| 49 |
+
const errors = validateYAML(yamlText);
|
| 50 |
+
|
| 51 |
+
// ── Signal / Register editors ────────────────────────────────────
|
| 52 |
+
|
| 53 |
+
const updateInterface = (idx, field, value) => {
|
| 54 |
+
const ifaces = [...(parsed.interfaces || [])];
|
| 55 |
+
ifaces[idx] = { ...ifaces[idx], [field]: value };
|
| 56 |
+
const updated = { ...parsed, interfaces: ifaces };
|
| 57 |
+
setParsed(updated);
|
| 58 |
+
handleChange(toYAML(updated));
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
const addSignal = (ifaceIdx) => {
|
| 62 |
+
const ifaces = [...(parsed.interfaces || [])];
|
| 63 |
+
const existing = Array.isArray(ifaces[ifaceIdx]?.signals) ? ifaces[ifaceIdx].signals : [];
|
| 64 |
+
const signals = [...existing, { name: "", direction: "input", width: 1 }];
|
| 65 |
+
ifaces[ifaceIdx] = { ...ifaces[ifaceIdx], signals };
|
| 66 |
+
const updated = { ...parsed, interfaces: ifaces };
|
| 67 |
+
setParsed(updated);
|
| 68 |
+
handleChange(toYAML(updated));
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
const updateSignal = (ifaceIdx, sigIdx, field, value) => {
|
| 72 |
+
const ifaces = [...(parsed.interfaces || [])];
|
| 73 |
+
const existing = Array.isArray(ifaces[ifaceIdx]?.signals) ? ifaces[ifaceIdx].signals : [];
|
| 74 |
+
const signals = [...existing];
|
| 75 |
+
signals[sigIdx] = { ...signals[sigIdx], [field]: value };
|
| 76 |
+
ifaces[ifaceIdx] = { ...ifaces[ifaceIdx], signals };
|
| 77 |
+
const updated = { ...parsed, interfaces: ifaces };
|
| 78 |
+
setParsed(updated);
|
| 79 |
+
handleChange(toYAML(updated));
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
const removeSignal = (ifaceIdx, sigIdx) => {
|
| 83 |
+
const ifaces = [...(parsed.interfaces || [])];
|
| 84 |
+
const sigs = Array.isArray(ifaces[ifaceIdx].signals) ? ifaces[ifaceIdx].signals : [];
|
| 85 |
+
ifaces[ifaceIdx] = {
|
| 86 |
+
...ifaces[ifaceIdx],
|
| 87 |
+
signals: sigs.filter((_, i) => i !== sigIdx),
|
| 88 |
+
};
|
| 89 |
+
const updated = { ...parsed, interfaces: ifaces };
|
| 90 |
+
setParsed(updated);
|
| 91 |
+
handleChange(toYAML(updated));
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
const addRegister = () => {
|
| 95 |
+
const regs = [...(parsed.registers || []), { name: "", address: "0x00", access: "rw", fields: [] }];
|
| 96 |
+
const updated = { ...parsed, registers: regs };
|
| 97 |
+
setParsed(updated);
|
| 98 |
+
handleChange(toYAML(updated));
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
const updateRegister = (idx, field, value) => {
|
| 102 |
+
const regs = [...(parsed.registers || [])];
|
| 103 |
+
regs[idx] = { ...regs[idx], [field]: value };
|
| 104 |
+
const updated = { ...parsed, registers: regs };
|
| 105 |
+
setParsed(updated);
|
| 106 |
+
handleChange(toYAML(updated));
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
const removeRegister = (idx) => {
|
| 110 |
+
const updated = { ...parsed, registers: parsed.registers.filter((_, i) => i !== idx) };
|
| 111 |
+
setParsed(updated);
|
| 112 |
+
handleChange(toYAML(updated));
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
+
const addField = (regIdx) => {
|
| 116 |
+
const regs = [...(parsed.registers || [])];
|
| 117 |
+
const fields = [...(regs[regIdx]?.fields || []), { name: "", bits: "0", description: "" }];
|
| 118 |
+
regs[regIdx] = { ...regs[regIdx], fields };
|
| 119 |
+
const updated = { ...parsed, registers: regs };
|
| 120 |
+
setParsed(updated);
|
| 121 |
+
handleChange(toYAML(updated));
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
const updateField = (regIdx, fldIdx, field, value) => {
|
| 125 |
+
const regs = [...(parsed.registers || [])];
|
| 126 |
+
const fields = [...(regs[regIdx]?.fields || [])];
|
| 127 |
+
fields[fldIdx] = { ...fields[fldIdx], [field]: value };
|
| 128 |
+
regs[regIdx] = { ...regs[regIdx], fields };
|
| 129 |
+
const updated = { ...parsed, registers: regs };
|
| 130 |
+
setParsed(updated);
|
| 131 |
+
handleChange(toYAML(updated));
|
| 132 |
+
};
|
| 133 |
+
|
| 134 |
+
const removeField = (regIdx, fldIdx) => {
|
| 135 |
+
const regs = [...(parsed.registers || [])];
|
| 136 |
+
regs[regIdx] = {
|
| 137 |
+
...regs[regIdx],
|
| 138 |
+
fields: regs[regIdx].fields.filter((_, i) => i !== fldIdx),
|
| 139 |
+
};
|
| 140 |
+
const updated = { ...parsed, registers: regs };
|
| 141 |
+
setParsed(updated);
|
| 142 |
+
handleChange(toYAML(updated));
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
return (
|
| 146 |
+
<div className="card">
|
| 147 |
+
<div className="card-header">
|
| 148 |
+
<FileCode size={18} className="text-brand-600" />
|
| 149 |
+
<h2>Design Specification</h2>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<div className="card-body space-y-4">
|
| 153 |
+
{/* Toolbar */}
|
| 154 |
+
<div className="flex flex-wrap items-center gap-2">
|
| 155 |
+
<button className="btn-secondary text-xs" onClick={() => fileRef.current?.click()}>
|
| 156 |
+
<Upload size={14} /> Upload .core / .yaml
|
| 157 |
+
</button>
|
| 158 |
+
<input
|
| 159 |
+
ref={fileRef}
|
| 160 |
+
type="file"
|
| 161 |
+
accept=".yaml,.yml,.core,.json"
|
| 162 |
+
className="hidden"
|
| 163 |
+
onChange={handleFileUpload}
|
| 164 |
+
/>
|
| 165 |
+
<button className="btn-secondary text-xs" onClick={handlePasteExample}>
|
| 166 |
+
Load Example
|
| 167 |
+
</button>
|
| 168 |
+
<div className="ml-auto flex items-center gap-1 bg-slate-100 rounded-lg p-0.5">
|
| 169 |
+
{["spec", "signals", "registers"].map((t) => (
|
| 170 |
+
<button
|
| 171 |
+
key={t}
|
| 172 |
+
onClick={() => setActiveTab(t)}
|
| 173 |
+
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
| 174 |
+
activeTab === t
|
| 175 |
+
? "bg-white text-slate-900 shadow-sm"
|
| 176 |
+
: "text-slate-500 hover:text-slate-700"
|
| 177 |
+
}`}
|
| 178 |
+
>
|
| 179 |
+
{t === "spec" ? "Spec" : t === "signals" ? "Signals" : "Registers"}
|
| 180 |
+
</button>
|
| 181 |
+
))}
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
{/* Validation status */}
|
| 186 |
+
{errors.length > 0 && (
|
| 187 |
+
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3">
|
| 188 |
+
<div className="flex items-start gap-2">
|
| 189 |
+
<AlertCircle size={16} className="text-red-500 mt-0.5 shrink-0" />
|
| 190 |
+
<div className="text-xs text-red-700 space-y-0.5">
|
| 191 |
+
{errors.map((e, i) => (
|
| 192 |
+
<p key={i}>{e}</p>
|
| 193 |
+
))}
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
)}
|
| 198 |
+
{errors.length === 0 && yamlText.trim() && (
|
| 199 |
+
<div className="flex items-center gap-2 text-xs text-emerald-600">
|
| 200 |
+
<CheckCircle2 size={14} />
|
| 201 |
+
YAML syntax valid
|
| 202 |
+
</div>
|
| 203 |
+
)}
|
| 204 |
+
|
| 205 |
+
{/* ── Spec tab ─────────────────────────────────── */}
|
| 206 |
+
{activeTab === "spec" && (
|
| 207 |
+
<div className="space-y-3">
|
| 208 |
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
| 209 |
+
<div>
|
| 210 |
+
<label className="block text-xs font-medium text-slate-600 mb-1">Design Name</label>
|
| 211 |
+
<input
|
| 212 |
+
className="input-field text-sm"
|
| 213 |
+
value={parsed.design_name || ""}
|
| 214 |
+
onChange={(e) => {
|
| 215 |
+
const updated = { ...parsed, design_name: e.target.value };
|
| 216 |
+
setParsed(updated);
|
| 217 |
+
handleChange(toYAML(updated));
|
| 218 |
+
}}
|
| 219 |
+
placeholder="uart16550"
|
| 220 |
+
/>
|
| 221 |
+
</div>
|
| 222 |
+
<div>
|
| 223 |
+
<label className="block text-xs font-medium text-slate-600 mb-1">Version</label>
|
| 224 |
+
<input
|
| 225 |
+
className="input-field text-sm"
|
| 226 |
+
value={parsed.version || ""}
|
| 227 |
+
onChange={(e) => {
|
| 228 |
+
const updated = { ...parsed, version: e.target.value };
|
| 229 |
+
setParsed(updated);
|
| 230 |
+
handleChange(toYAML(updated));
|
| 231 |
+
}}
|
| 232 |
+
placeholder="1.5"
|
| 233 |
+
/>
|
| 234 |
+
</div>
|
| 235 |
+
<div>
|
| 236 |
+
<label className="block text-xs font-medium text-slate-600 mb-1">Vendor</label>
|
| 237 |
+
<input
|
| 238 |
+
className="input-field text-sm"
|
| 239 |
+
value={parsed.vendor || ""}
|
| 240 |
+
onChange={(e) => {
|
| 241 |
+
const updated = { ...parsed, vendor: e.target.value };
|
| 242 |
+
setParsed(updated);
|
| 243 |
+
handleChange(toYAML(updated));
|
| 244 |
+
}}
|
| 245 |
+
placeholder="DV Automation"
|
| 246 |
+
/>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
|
| 250 |
+
<div>
|
| 251 |
+
<label className="block text-xs font-medium text-slate-600 mb-1">Description</label>
|
| 252 |
+
<textarea
|
| 253 |
+
className="input-field text-sm resize-none"
|
| 254 |
+
rows={2}
|
| 255 |
+
value={parsed.description || ""}
|
| 256 |
+
onChange={(e) => {
|
| 257 |
+
const updated = { ...parsed, description: e.target.value };
|
| 258 |
+
setParsed(updated);
|
| 259 |
+
handleChange(toYAML(updated));
|
| 260 |
+
}}
|
| 261 |
+
placeholder="Core description"
|
| 262 |
+
/>
|
| 263 |
+
</div>
|
| 264 |
+
|
| 265 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
| 266 |
+
<div>
|
| 267 |
+
<label className="block text-xs font-medium text-slate-600 mb-1">Protocol</label>
|
| 268 |
+
<select
|
| 269 |
+
className="select-field text-sm"
|
| 270 |
+
value={parsed.protocol || ""}
|
| 271 |
+
onChange={(e) => {
|
| 272 |
+
const updated = { ...parsed, protocol: e.target.value };
|
| 273 |
+
setParsed(updated);
|
| 274 |
+
handleChange(toYAML(updated));
|
| 275 |
+
}}
|
| 276 |
+
>
|
| 277 |
+
<option value="">Auto-detect</option>
|
| 278 |
+
{PROTOCOLS.map((p) => (
|
| 279 |
+
<option key={p} value={p}>{p.toUpperCase()}</option>
|
| 280 |
+
))}
|
| 281 |
+
</select>
|
| 282 |
+
</div>
|
| 283 |
+
<div>
|
| 284 |
+
<label className="block text-xs font-medium text-slate-600 mb-1">Language</label>
|
| 285 |
+
<select
|
| 286 |
+
className="select-field text-sm"
|
| 287 |
+
value={parsed.language || "systemverilog"}
|
| 288 |
+
onChange={(e) => {
|
| 289 |
+
const updated = { ...parsed, language: e.target.value };
|
| 290 |
+
setParsed(updated);
|
| 291 |
+
handleChange(toYAML(updated));
|
| 292 |
+
}}
|
| 293 |
+
>
|
| 294 |
+
<option value="systemverilog">SystemVerilog</option>
|
| 295 |
+
<option value="verilog">Verilog</option>
|
| 296 |
+
<option value="vhdl">VHDL</option>
|
| 297 |
+
</select>
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
+
|
| 301 |
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
| 302 |
+
<div>
|
| 303 |
+
<label className="block text-xs font-medium text-slate-600 mb-1">Clock (clk)</label>
|
| 304 |
+
<input className="input-field text-sm" value={parsed.clock_reset?.clock || "clk"} onChange={(e) => {
|
| 305 |
+
const cr = { ...(parsed.clock_reset || {}), clock: e.target.value };
|
| 306 |
+
const updated = { ...parsed, clock_reset: cr };
|
| 307 |
+
setParsed(updated); handleChange(toYAML(updated));
|
| 308 |
+
}} />
|
| 309 |
+
</div>
|
| 310 |
+
<div>
|
| 311 |
+
<label className="block text-xs font-medium text-slate-600 mb-1">Reset (rst_n)</label>
|
| 312 |
+
<input className="input-field text-sm" value={parsed.clock_reset?.reset || "rst_n"} onChange={(e) => {
|
| 313 |
+
const cr = { ...(parsed.clock_reset || {}), reset: e.target.value };
|
| 314 |
+
const updated = { ...parsed, clock_reset: cr };
|
| 315 |
+
setParsed(updated); handleChange(toYAML(updated));
|
| 316 |
+
}} />
|
| 317 |
+
</div>
|
| 318 |
+
<div>
|
| 319 |
+
<label className="block text-xs font-medium text-slate-600 mb-1">Reset Active</label>
|
| 320 |
+
<select className="select-field text-sm" value={parsed.clock_reset?.reset_active ?? 0} onChange={(e) => {
|
| 321 |
+
const cr = { ...(parsed.clock_reset || {}), reset_active: Number(e.target.value) };
|
| 322 |
+
const updated = { ...parsed, clock_reset: cr };
|
| 323 |
+
setParsed(updated); handleChange(toYAML(updated));
|
| 324 |
+
}}>
|
| 325 |
+
<option value={0}>Active Low (0)</option>
|
| 326 |
+
<option value={1}>Active High (1)</option>
|
| 327 |
+
</select>
|
| 328 |
+
</div>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
{/* Raw YAML editor */}
|
| 332 |
+
<div>
|
| 333 |
+
<label className="block text-xs font-medium text-slate-600 mb-1">
|
| 334 |
+
Raw YAML / .core Editor
|
| 335 |
+
</label>
|
| 336 |
+
<textarea
|
| 337 |
+
className="input-field text-xs font-mono resize-y"
|
| 338 |
+
rows={10}
|
| 339 |
+
value={yamlText}
|
| 340 |
+
onChange={(e) => handleChange(e.target.value)}
|
| 341 |
+
spellCheck={false}
|
| 342 |
+
/>
|
| 343 |
+
</div>
|
| 344 |
+
</div>
|
| 345 |
+
)}
|
| 346 |
+
|
| 347 |
+
{/* ── Signals tab ─────────────────────────────────── */}
|
| 348 |
+
{activeTab === "signals" && (
|
| 349 |
+
<div className="space-y-4">
|
| 350 |
+
{(parsed.interfaces || []).length === 0 && (
|
| 351 |
+
<p className="text-xs text-slate-400">No interfaces defined. Add one in the Spec tab.</p>
|
| 352 |
+
)}
|
| 353 |
+
{(parsed.interfaces || []).map((iface, iIdx) => (
|
| 354 |
+
<div key={iIdx} className="rounded-lg border border-slate-200 p-4 space-y-3">
|
| 355 |
+
<div className="flex items-center justify-between">
|
| 356 |
+
<div className="flex items-center gap-2">
|
| 357 |
+
<span className="badge-info">{iface.name || `Interface #${iIdx + 1}`}</span>
|
| 358 |
+
<select
|
| 359 |
+
className="select-field text-xs py-1 w-28"
|
| 360 |
+
value={iface.protocol || ""}
|
| 361 |
+
onChange={(e) => updateInterface(iIdx, "protocol", e.target.value)}
|
| 362 |
+
>
|
| 363 |
+
<option value="">Protocol</option>
|
| 364 |
+
{PROTOCOLS.map((p) => (
|
| 365 |
+
<option key={p} value={p}>{p.toUpperCase()}</option>
|
| 366 |
+
))}
|
| 367 |
+
</select>
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
|
| 371 |
+
<div className="space-y-2">
|
| 372 |
+
{Array.isArray(iface.signals) && iface.signals.map((sig, sIdx) => (
|
| 373 |
+
<div key={sIdx} className="flex items-center gap-2">
|
| 374 |
+
<input
|
| 375 |
+
className="input-field text-xs py-1.5 w-28"
|
| 376 |
+
value={sig.name}
|
| 377 |
+
onChange={(e) => updateSignal(iIdx, sIdx, "name", e.target.value)}
|
| 378 |
+
placeholder="signal_name"
|
| 379 |
+
/>
|
| 380 |
+
<select
|
| 381 |
+
className="select-field text-xs py-1.5 w-22"
|
| 382 |
+
value={sig.direction}
|
| 383 |
+
onChange={(e) => updateSignal(iIdx, sIdx, "direction", e.target.value)}
|
| 384 |
+
>
|
| 385 |
+
<option value="input">input</option>
|
| 386 |
+
<option value="output">output</option>
|
| 387 |
+
<option value="inout">inout</option>
|
| 388 |
+
</select>
|
| 389 |
+
<div className="flex items-center gap-1">
|
| 390 |
+
<span className="text-[11px] text-slate-500">w:</span>
|
| 391 |
+
<input
|
| 392 |
+
className="input-field text-xs py-1.5 w-16 text-center"
|
| 393 |
+
type="number"
|
| 394 |
+
min={1}
|
| 395 |
+
value={sig.width || 1}
|
| 396 |
+
onChange={(e) => updateSignal(iIdx, sIdx, "width", Number(e.target.value))}
|
| 397 |
+
/>
|
| 398 |
+
</div>
|
| 399 |
+
<button
|
| 400 |
+
className="p-1.5 rounded-md text-slate-400 hover:text-red-500 hover:bg-red-50 transition-colors"
|
| 401 |
+
onClick={() => removeSignal(iIdx, sIdx)}
|
| 402 |
+
title="Remove signal"
|
| 403 |
+
>
|
| 404 |
+
<Trash2 size={14} />
|
| 405 |
+
</button>
|
| 406 |
+
</div>
|
| 407 |
+
))}
|
| 408 |
+
</div>
|
| 409 |
+
<button
|
| 410 |
+
className="btn-secondary text-xs"
|
| 411 |
+
onClick={() => addSignal(iIdx)}
|
| 412 |
+
>
|
| 413 |
+
<Plus size={14} /> Add Signal
|
| 414 |
+
</button>
|
| 415 |
+
</div>
|
| 416 |
+
))}
|
| 417 |
+
</div>
|
| 418 |
+
)}
|
| 419 |
+
|
| 420 |
+
{/* ── Registers tab ─────────────────────────────────── */}
|
| 421 |
+
{activeTab === "registers" && (
|
| 422 |
+
<div className="space-y-4">
|
| 423 |
+
{(parsed.registers || []).length === 0 && (
|
| 424 |
+
<p className="text-xs text-slate-400">No registers defined.</p>
|
| 425 |
+
)}
|
| 426 |
+
{(parsed.registers || []).map((reg, rIdx) => (
|
| 427 |
+
<div key={rIdx} className="rounded-lg border border-slate-200 p-4 space-y-3">
|
| 428 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 429 |
+
<input
|
| 430 |
+
className="input-field text-xs py-1.5 w-28"
|
| 431 |
+
value={reg.name}
|
| 432 |
+
onChange={(e) => updateRegister(rIdx, "name", e.target.value)}
|
| 433 |
+
placeholder="REG_NAME"
|
| 434 |
+
/>
|
| 435 |
+
<input
|
| 436 |
+
className="input-field text-xs py-1.5 w-22 font-mono"
|
| 437 |
+
value={reg.address}
|
| 438 |
+
onChange={(e) => updateRegister(rIdx, "address", e.target.value)}
|
| 439 |
+
placeholder="0x00"
|
| 440 |
+
/>
|
| 441 |
+
<select
|
| 442 |
+
className="select-field text-xs py-1.5 w-20"
|
| 443 |
+
value={reg.access || "rw"}
|
| 444 |
+
onChange={(e) => updateRegister(rIdx, "access", e.target.value)}
|
| 445 |
+
>
|
| 446 |
+
<option value="rw">rw</option>
|
| 447 |
+
<option value="ro">ro</option>
|
| 448 |
+
<option value="wo">wo</option>
|
| 449 |
+
<option value="rc">rc</option>
|
| 450 |
+
<option value="w1c">w1c</option>
|
| 451 |
+
</select>
|
| 452 |
+
<input
|
| 453 |
+
className="input-field text-xs py-1.5 w-32"
|
| 454 |
+
value={reg.description || ""}
|
| 455 |
+
onChange={(e) => updateRegister(rIdx, "description", e.target.value)}
|
| 456 |
+
placeholder="Description"
|
| 457 |
+
/>
|
| 458 |
+
<button
|
| 459 |
+
className="p-1.5 rounded-md text-slate-400 hover:text-red-500 hover:bg-red-50 transition-colors ml-auto"
|
| 460 |
+
onClick={() => removeRegister(rIdx)}
|
| 461 |
+
title="Remove register"
|
| 462 |
+
>
|
| 463 |
+
<Trash2 size={14} />
|
| 464 |
+
</button>
|
| 465 |
+
</div>
|
| 466 |
+
|
| 467 |
+
{/* Fields */}
|
| 468 |
+
{(reg.fields || []).length > 0 && (
|
| 469 |
+
<div className="ml-2 pl-3 border-l-2 border-brand-200 space-y-2">
|
| 470 |
+
<span className="text-[11px] font-medium text-slate-500 uppercase tracking-wider">
|
| 471 |
+
Fields
|
| 472 |
+
</span>
|
| 473 |
+
{(reg.fields || []).map((fld, fIdx) => (
|
| 474 |
+
<div key={fIdx} className="flex items-center gap-2">
|
| 475 |
+
<input
|
| 476 |
+
className="input-field text-xs py-1 w-24"
|
| 477 |
+
value={fld.name}
|
| 478 |
+
onChange={(e) => updateField(rIdx, fIdx, "name", e.target.value)}
|
| 479 |
+
placeholder="field_name"
|
| 480 |
+
/>
|
| 481 |
+
<input
|
| 482 |
+
className="input-field text-xs py-1 w-16 font-mono text-center"
|
| 483 |
+
value={fld.bits}
|
| 484 |
+
onChange={(e) => updateField(rIdx, fIdx, "bits", e.target.value)}
|
| 485 |
+
placeholder="7:0"
|
| 486 |
+
/>
|
| 487 |
+
<input
|
| 488 |
+
className="input-field text-xs py-1 flex-1"
|
| 489 |
+
value={fld.description || ""}
|
| 490 |
+
onChange={(e) => updateField(rIdx, fIdx, "description", e.target.value)}
|
| 491 |
+
placeholder="Description"
|
| 492 |
+
/>
|
| 493 |
+
<button
|
| 494 |
+
className="p-1 rounded-md text-slate-400 hover:text-red-500 hover:bg-red-50"
|
| 495 |
+
onClick={() => removeField(rIdx, fIdx)}
|
| 496 |
+
>
|
| 497 |
+
<Trash2 size={12} />
|
| 498 |
+
</button>
|
| 499 |
+
</div>
|
| 500 |
+
))}
|
| 501 |
+
</div>
|
| 502 |
+
)}
|
| 503 |
+
<button className="btn-secondary text-xs" onClick={() => addField(rIdx)}>
|
| 504 |
+
<Plus size={14} /> Add Field
|
| 505 |
+
</button>
|
| 506 |
+
</div>
|
| 507 |
+
))}
|
| 508 |
+
<button className="btn-secondary text-xs" onClick={addRegister}>
|
| 509 |
+
<Plus size={14} /> Add Register
|
| 510 |
+
</button>
|
| 511 |
+
</div>
|
| 512 |
+
)}
|
| 513 |
+
</div>
|
| 514 |
+
</div>
|
| 515 |
+
);
|
| 516 |
+
}
|
frontend/src/hooks/usePipeline.js
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useCallback } from "react";
|
| 2 |
+
|
| 3 |
+
const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000";
|
| 4 |
+
|
| 5 |
+
const FILE_CONTENT_TEMPLATES = {
|
| 6 |
+
testbench: (design) => [
|
| 7 |
+
`// Generated UVM Testbench for ${design} — with DUT instantiation`,
|
| 8 |
+
"`timescale 1ns/1ps",
|
| 9 |
+
"",
|
| 10 |
+
"module testbench;",
|
| 11 |
+
" logic clk;",
|
| 12 |
+
" logic rst_n;",
|
| 13 |
+
"",
|
| 14 |
+
` ${design}_intf intf(clk, rst_n);`,
|
| 15 |
+
"",
|
| 16 |
+
" uart_top #(",
|
| 17 |
+
" .CLK_FREQ (50000000),",
|
| 18 |
+
" .BAUD_RATE (115200)",
|
| 19 |
+
" ) dut (",
|
| 20 |
+
" .clk (clk),",
|
| 21 |
+
" .rst_n (rst_n),",
|
| 22 |
+
" .wb_cyc (intf.wb_cyc),",
|
| 23 |
+
" .wb_stb (intf.wb_stb),",
|
| 24 |
+
" .wb_we (intf.wb_we),",
|
| 25 |
+
" .wb_addr (intf.wb_addr),",
|
| 26 |
+
" .wb_data_i (intf.wb_data_o),",
|
| 27 |
+
" .wb_data_o (intf.wb_data_i),",
|
| 28 |
+
" .wb_ack (intf.wb_ack),",
|
| 29 |
+
" .uart_tx (intf.uart_tx),",
|
| 30 |
+
" .uart_rx (intf.uart_rx),",
|
| 31 |
+
" .cts_n (intf.cts_n),",
|
| 32 |
+
" .rts_n (intf.rts_n),",
|
| 33 |
+
" .dsr_n (intf.dsr_n),",
|
| 34 |
+
" .dtr_n (intf.dtr_n),",
|
| 35 |
+
" .ri_n (intf.ri_n),",
|
| 36 |
+
" .dcd_n (intf.dcd_n),",
|
| 37 |
+
" .out1_n (intf.out1_n),",
|
| 38 |
+
" .out2_n (intf.out2_n),",
|
| 39 |
+
" .uart_intr (intf.uart_intr)",
|
| 40 |
+
" );",
|
| 41 |
+
"",
|
| 42 |
+
" initial begin",
|
| 43 |
+
` uvm_config_db#(virtual ${design}_intf)::set(null, "*", "vif", intf);`,
|
| 44 |
+
" run_test();",
|
| 45 |
+
" end",
|
| 46 |
+
"",
|
| 47 |
+
" initial begin",
|
| 48 |
+
" clk = 0;",
|
| 49 |
+
" forever #5 clk = ~clk;",
|
| 50 |
+
" end",
|
| 51 |
+
"",
|
| 52 |
+
" initial begin",
|
| 53 |
+
" rst_n = 0;",
|
| 54 |
+
" repeat (10) @(posedge clk);",
|
| 55 |
+
" rst_n = 1;",
|
| 56 |
+
" end",
|
| 57 |
+
"",
|
| 58 |
+
" initial begin",
|
| 59 |
+
' $dumpfile("sim.vcd");',
|
| 60 |
+
" $dumpvars(0, testbench);",
|
| 61 |
+
" end",
|
| 62 |
+
"endmodule",
|
| 63 |
+
].join("\n"),
|
| 64 |
+
|
| 65 |
+
interface: (design) => [
|
| 66 |
+
`// Generated UVM Interface for ${design}`,
|
| 67 |
+
`interface ${design}_intf (input logic clk, input logic rst_n);`,
|
| 68 |
+
"",
|
| 69 |
+
" logic wb_cyc, wb_stb, wb_we;",
|
| 70 |
+
" logic [2:0] wb_addr;",
|
| 71 |
+
" logic [7:0] wb_data_o, wb_data_i;",
|
| 72 |
+
" logic wb_ack;",
|
| 73 |
+
" logic uart_tx, uart_rx;",
|
| 74 |
+
" logic cts_n, rts_n, dsr_n, dtr_n, ri_n, dcd_n, out1_n, out2_n;",
|
| 75 |
+
" logic uart_intr;",
|
| 76 |
+
"",
|
| 77 |
+
" clocking drv_cb @(posedge clk);",
|
| 78 |
+
" default input #1ns output #1ns;",
|
| 79 |
+
" output wb_cyc, wb_stb, wb_we, wb_addr, wb_data_o;",
|
| 80 |
+
" output uart_rx, cts_n, dsr_n, ri_n, dcd_n;",
|
| 81 |
+
" input wb_ack, wb_data_i, uart_tx, uart_intr, rts_n, dtr_n, out1_n, out2_n;",
|
| 82 |
+
" endclocking",
|
| 83 |
+
"",
|
| 84 |
+
" clocking mon_cb @(posedge clk);",
|
| 85 |
+
" default input #1ns;",
|
| 86 |
+
" input all;",
|
| 87 |
+
" endclocking",
|
| 88 |
+
"",
|
| 89 |
+
" modport driver (clocking drv_cb, input clk, rst_n);",
|
| 90 |
+
" modport monitor (clocking mon_cb, input clk, rst_n);",
|
| 91 |
+
"endinterface",
|
| 92 |
+
].join("\n"),
|
| 93 |
+
|
| 94 |
+
seq_item: (design) => [
|
| 95 |
+
`class ${design}_seq_item extends uvm_sequence_item;`,
|
| 96 |
+
` \`uvm_object_utils(${design}_seq_item)`,
|
| 97 |
+
" rand logic we;",
|
| 98 |
+
" rand logic [2:0] addr;",
|
| 99 |
+
" rand logic [7:0] data;",
|
| 100 |
+
" rand int delay;",
|
| 101 |
+
" constraint c_default { delay inside {[0:5]}; soft we == 1'b1; }",
|
| 102 |
+
` function new(string name = "${design}_seq_item"); super.new(name); endfunction`,
|
| 103 |
+
"endclass",
|
| 104 |
+
].join("\n"),
|
| 105 |
+
|
| 106 |
+
driver: (design) => [
|
| 107 |
+
`class ${design}_driver extends uvm_driver #(${design}_seq_item);`,
|
| 108 |
+
` \`uvm_component_utils(${design}_driver)`,
|
| 109 |
+
` virtual ${design}_intf vif;`,
|
| 110 |
+
" function new(string n, uvm_component p); super.new(n, p); endfunction",
|
| 111 |
+
" function void build_phase(uvm_phase phase);",
|
| 112 |
+
` if (!uvm_config_db#(virtual ${design}_intf)::get(this, "", "vif", vif))`,
|
| 113 |
+
' `uvm_fatal("NOVIF", "")',
|
| 114 |
+
" endfunction",
|
| 115 |
+
" task run_phase(uvm_phase phase);",
|
| 116 |
+
" forever begin",
|
| 117 |
+
" seq_item_port.get_next_item(req);",
|
| 118 |
+
" vif.drv_cb.wb_cyc <= 1; vif.drv_cb.wb_stb <= 1;",
|
| 119 |
+
" vif.drv_cb.wb_we <= req.we;",
|
| 120 |
+
" vif.drv_cb.wb_addr <= req.addr;",
|
| 121 |
+
" vif.drv_cb.wb_data_o <= req.data;",
|
| 122 |
+
" @(vif.drv_cb); wait(vif.drv_cb.wb_ack); @(vif.drv_cb);",
|
| 123 |
+
" vif.drv_cb.wb_cyc <= 0; vif.drv_cb.wb_stb <= 0;",
|
| 124 |
+
" seq_item_port.item_done();",
|
| 125 |
+
" end",
|
| 126 |
+
" endtask",
|
| 127 |
+
"endclass",
|
| 128 |
+
].join("\n"),
|
| 129 |
+
|
| 130 |
+
monitor: (design) => [
|
| 131 |
+
`class ${design}_monitor extends uvm_monitor;`,
|
| 132 |
+
` \`uvm_component_utils(${design}_monitor)`,
|
| 133 |
+
` virtual ${design}_intf vif;`,
|
| 134 |
+
` uvm_analysis_port #(${design}_seq_item) item_collected_port;`,
|
| 135 |
+
" function new(string n, uvm_component p); super.new(n, p);",
|
| 136 |
+
` item_collected_port = new("item_collected_port", this); endfunction`,
|
| 137 |
+
" function void build_phase(uvm_phase phase);",
|
| 138 |
+
` if (!uvm_config_db#(virtual ${design}_intf)::get(this, "", "vif", vif))`,
|
| 139 |
+
' `uvm_fatal("NOVIF", "")',
|
| 140 |
+
" endfunction",
|
| 141 |
+
" task run_phase(uvm_phase phase); forever begin",
|
| 142 |
+
" @(vif.mon_cb);",
|
| 143 |
+
" if (vif.mon_cb.wb_stb && vif.mon_cb.wb_cyc) begin",
|
| 144 |
+
` ${design}_seq_item item = ${design}_seq_item::type_id::create("item");`,
|
| 145 |
+
" item.we = vif.mon_cb.wb_we; item.addr = vif.mon_cb.wb_addr;",
|
| 146 |
+
" if (vif.mon_cb.wb_we) item.data = vif.mon_cb.wb_data_o;",
|
| 147 |
+
" else begin wait(vif.mon_cb.wb_ack); item.data = vif.mon_cb.wb_data_i; end",
|
| 148 |
+
" item_collected_port.write(item);",
|
| 149 |
+
" end end",
|
| 150 |
+
" endtask",
|
| 151 |
+
"endclass",
|
| 152 |
+
].join("\n"),
|
| 153 |
+
|
| 154 |
+
agent: (design) => [
|
| 155 |
+
`class ${design}_agent extends uvm_agent;`,
|
| 156 |
+
` \`uvm_component_utils(${design}_agent)`,
|
| 157 |
+
` ${design}_driver driver;`,
|
| 158 |
+
` ${design}_monitor monitor;`,
|
| 159 |
+
` uvm_sequencer #(${design}_seq_item) sequencer;`,
|
| 160 |
+
" function new(string n, uvm_component p); super.new(n, p); endfunction",
|
| 161 |
+
" function void build_phase(uvm_phase phase);",
|
| 162 |
+
` driver = ${design}_driver::type_id::create("driver", this);`,
|
| 163 |
+
` monitor = ${design}_monitor::type_id::create("monitor", this);`,
|
| 164 |
+
` sequencer = uvm_sequencer#(${design}_seq_item)::type_id::create("sequencer", this);`,
|
| 165 |
+
" endfunction",
|
| 166 |
+
" function void connect_phase(uvm_phase phase);",
|
| 167 |
+
" driver.seq_item_port.connect(sequencer.seq_item_export);",
|
| 168 |
+
" endfunction",
|
| 169 |
+
"endclass",
|
| 170 |
+
].join("\n"),
|
| 171 |
+
|
| 172 |
+
scoreboard: (design) => [
|
| 173 |
+
`class ${design}_scoreboard extends uvm_scoreboard;`,
|
| 174 |
+
` \`uvm_component_utils(${design}_scoreboard)`,
|
| 175 |
+
` uvm_analysis_imp #(${design}_seq_item, ${design}_scoreboard) act_export;`,
|
| 176 |
+
" int match_count, mismatch_count;",
|
| 177 |
+
" function new(string n, uvm_component p); super.new(n, p);",
|
| 178 |
+
` act_export = new("act_export", this); endfunction`,
|
| 179 |
+
` function void write(${design}_seq_item t);`,
|
| 180 |
+
" match_count++;",
|
| 181 |
+
' `uvm_info("SCOREBOARD", $sformatf("item: %s", t.convert2string()), UVM_LOW)',
|
| 182 |
+
" endfunction",
|
| 183 |
+
" function void report_phase(uvm_phase phase);",
|
| 184 |
+
' if (mismatch_count == 0) `uvm_info("SCOREBOARD", "PASS", UVM_LOW)',
|
| 185 |
+
' else `uvm_error("SCOREBOARD", "FAIL")',
|
| 186 |
+
" endfunction",
|
| 187 |
+
"endclass",
|
| 188 |
+
].join("\n"),
|
| 189 |
+
|
| 190 |
+
coverage_collector: (design) => {
|
| 191 |
+
const cg = [
|
| 192 |
+
" covergroup bus_cg @(negedge vif.clk);",
|
| 193 |
+
" option.name = \"bus_cg\";",
|
| 194 |
+
" ADDR: coverpoint vif.mon_cb.wb_addr { bins regs[] = {[0:7]}; }",
|
| 195 |
+
" DIR: coverpoint vif.mon_cb.wb_we { bins read = {0}; bins write = {1}; }",
|
| 196 |
+
" ADRxDIR: cross ADDR, DIR;",
|
| 197 |
+
" endgroup",
|
| 198 |
+
"",
|
| 199 |
+
" covergroup data_cg @(negedge vif.clk);",
|
| 200 |
+
" option.name = \"data_cg\";",
|
| 201 |
+
" DATA: coverpoint vif.mon_cb.wb_data_o {",
|
| 202 |
+
" bins zero = {8'h00}; bins ones = {8'hFF};",
|
| 203 |
+
" bins pattern = {8'h55, 8'hAA, 8'hA5, 8'h5A}; bins others = default;",
|
| 204 |
+
" }",
|
| 205 |
+
" endgroup",
|
| 206 |
+
].join("\n");
|
| 207 |
+
return [
|
| 208 |
+
`class ${design}_coverage_collector extends uvm_subscriber #(${design}_seq_item);`,
|
| 209 |
+
` \`uvm_component_utils(${design}_coverage_collector)`,
|
| 210 |
+
` virtual ${design}_intf vif;`,
|
| 211 |
+
"",
|
| 212 |
+
cg,
|
| 213 |
+
"",
|
| 214 |
+
" function new(string n, uvm_component p); super.new(n, p);",
|
| 215 |
+
" bus_cg = new(); data_cg = new();",
|
| 216 |
+
" endfunction",
|
| 217 |
+
" function void build_phase(uvm_phase phase);",
|
| 218 |
+
` if (!uvm_config_db#(virtual ${design}_intf)::get(this, "", "vif", vif))`,
|
| 219 |
+
' `uvm_fatal("NOVIF", "")',
|
| 220 |
+
" endfunction",
|
| 221 |
+
" function void write(${design}_seq_item t); endfunction",
|
| 222 |
+
" function void report_phase(uvm_phase phase);",
|
| 223 |
+
` \`uvm_info(get_type_name(), $sformatf("bus_cg=%.1f%% data_cg=%.1f%%",`,
|
| 224 |
+
" bus_cg.get_coverage(), data_cg.get_coverage()), UVM_LOW)",
|
| 225 |
+
" endfunction",
|
| 226 |
+
"endclass",
|
| 227 |
+
].join("\n");
|
| 228 |
+
},
|
| 229 |
+
|
| 230 |
+
base_sequence: (design) => {
|
| 231 |
+
const seqs = [
|
| 232 |
+
`class ${design}_base_seq extends uvm_sequence #(${design}_seq_item);`,
|
| 233 |
+
` \`uvm_object_utils(${design}_base_seq)`,
|
| 234 |
+
` function new(string n = "${design}_base_seq"); super.new(n); endfunction`,
|
| 235 |
+
' virtual task body(); `uvm_info(get_type_name(), "base", UVM_LOW); endtask',
|
| 236 |
+
"endclass",
|
| 237 |
+
"",
|
| 238 |
+
`class ${design}_write_reg_seq extends ${design}_base_seq;`,
|
| 239 |
+
` \`uvm_object_utils(${design}_write_reg_seq)`,
|
| 240 |
+
" rand logic [2:0] reg_addr; rand logic [7:0] reg_data;",
|
| 241 |
+
` function new(string n = "${design}_write_reg_seq"); super.new(n); endfunction`,
|
| 242 |
+
" virtual task body();",
|
| 243 |
+
` ${design}_seq_item item = ${design}_seq_item::type_id::create("item");`,
|
| 244 |
+
" start_item(item); item.we = 1; item.addr = reg_addr; item.data = reg_data; finish_item(item);",
|
| 245 |
+
' `uvm_info(get_type_name(), $sformatf("W 0x%0h <- 0x%0h", reg_addr, reg_data), UVM_MEDIUM)',
|
| 246 |
+
" endtask",
|
| 247 |
+
"endclass",
|
| 248 |
+
"",
|
| 249 |
+
`class ${design}_read_reg_seq extends ${design}_base_seq;`,
|
| 250 |
+
` \`uvm_object_utils(${design}_read_reg_seq)`,
|
| 251 |
+
" rand logic [2:0] reg_addr; logic [7:0] read_data;",
|
| 252 |
+
` function new(string n = "${design}_read_reg_seq"); super.new(n); endfunction`,
|
| 253 |
+
" virtual task body();",
|
| 254 |
+
` ${design}_seq_item item = ${design}_seq_item::type_id::create("item");`,
|
| 255 |
+
" start_item(item); item.we = 0; item.addr = reg_addr; finish_item(item);",
|
| 256 |
+
" read_data = item.data;",
|
| 257 |
+
' `uvm_info(get_type_name(), $sformatf("R 0x%0h -> 0x%0h", reg_addr, read_data), UVM_MEDIUM)',
|
| 258 |
+
" endtask",
|
| 259 |
+
"endclass",
|
| 260 |
+
"",
|
| 261 |
+
`class ${design}_send_byte_seq extends ${design}_base_seq;`,
|
| 262 |
+
` \`uvm_object_utils(${design}_send_byte_seq)`,
|
| 263 |
+
" rand logic [7:0] tx_byte;",
|
| 264 |
+
` function new(string n = "${design}_send_byte_seq"); super.new(n); endfunction`,
|
| 265 |
+
" virtual task body();",
|
| 266 |
+
` ${design}_write_reg_seq wseq;`,
|
| 267 |
+
` wseq = ${design}_write_reg_seq::type_id::create("wseq");`,
|
| 268 |
+
" wseq.reg_addr = 3'h3; wseq.reg_data = 8'b00000011; wseq.start(m_sequencer);",
|
| 269 |
+
" wseq.reg_addr = 3'h0; wseq.reg_data = tx_byte; wseq.start(m_sequencer);",
|
| 270 |
+
' `uvm_info(get_type_name(), $sformatf("TX 0x%0h", tx_byte), UVM_LOW)',
|
| 271 |
+
" endtask",
|
| 272 |
+
"endclass",
|
| 273 |
+
].join("\n");
|
| 274 |
+
return seqs;
|
| 275 |
+
},
|
| 276 |
+
|
| 277 |
+
test: (design) => [
|
| 278 |
+
`class test_${design} extends uvm_test;`,
|
| 279 |
+
` \`uvm_component_utils(test_${design})`,
|
| 280 |
+
` environment_${design} env;`,
|
| 281 |
+
` function new(string n = "test_${design}", uvm_component p = null); super.new(n, p); endfunction`,
|
| 282 |
+
" function void build_phase(uvm_phase phase);",
|
| 283 |
+
` env = environment_${design}::type_id::create("env", this);`,
|
| 284 |
+
" endfunction",
|
| 285 |
+
" task run_phase(uvm_phase phase);",
|
| 286 |
+
` ${design}_send_byte_seq seq;`,
|
| 287 |
+
" phase.raise_objection(this);",
|
| 288 |
+
` seq = ${design}_send_byte_seq::type_id::create("seq");`,
|
| 289 |
+
" seq.randomize() with { tx_byte == 8'hAB; };",
|
| 290 |
+
" seq.start(env.agent.sequencer);",
|
| 291 |
+
` ${design}_write_reg_seq wseq = ${design}_write_reg_seq::type_id::create("wseq");`,
|
| 292 |
+
" wseq.reg_addr = 3'h7; wseq.reg_data = 8'hA5; wseq.start(env.agent.sequencer);",
|
| 293 |
+
` ${design}_read_reg_seq rseq = ${design}_read_reg_seq::type_id::create("rseq");`,
|
| 294 |
+
" rseq.reg_addr = 3'h7; rseq.start(env.agent.sequencer);",
|
| 295 |
+
" if (rseq.read_data == 8'hA5)",
|
| 296 |
+
' `uvm_info(get_type_name(), "SCR PASS", UVM_LOW)',
|
| 297 |
+
" else",
|
| 298 |
+
' `uvm_error(get_type_name(), "SCR FAIL")',
|
| 299 |
+
" phase.drop_objection(this);",
|
| 300 |
+
" endtask",
|
| 301 |
+
"endclass",
|
| 302 |
+
].join("\n"),
|
| 303 |
+
|
| 304 |
+
regression: (design) => [
|
| 305 |
+
`// Auto-generated regression test — coverage-driven`,
|
| 306 |
+
`class ${design}_regression_test extends test_${design};`,
|
| 307 |
+
` \`uvm_component_utils(${design}_regression_test)`,
|
| 308 |
+
" function new(string n, uvm_component p); super.new(n, p); endfunction",
|
| 309 |
+
" task run_phase(uvm_phase phase);",
|
| 310 |
+
" super.run_phase(phase); phase.raise_objection(this);",
|
| 311 |
+
` ${design}_write_reg_seq wseq;`,
|
| 312 |
+
" for (int a = 0; a < 8; a++) begin",
|
| 313 |
+
` wseq = ${design}_write_reg_seq::type_id::create($sformatf("w_%0d", a));`,
|
| 314 |
+
" wseq.reg_addr = a; wseq.reg_data = 8'(a << 4 | a); wseq.start(env.agent.sequencer);",
|
| 315 |
+
" end",
|
| 316 |
+
` ${design}_read_reg_seq rseq;`,
|
| 317 |
+
" for (int a = 0; a < 8; a++) begin",
|
| 318 |
+
` rseq = ${design}_read_reg_seq::type_id::create($sformatf("r_%0d", a));`,
|
| 319 |
+
" rseq.reg_addr = a; rseq.start(env.agent.sequencer);",
|
| 320 |
+
" end",
|
| 321 |
+
" phase.drop_objection(this);",
|
| 322 |
+
" endtask",
|
| 323 |
+
"endclass",
|
| 324 |
+
].join("\n"),
|
| 325 |
+
|
| 326 |
+
env: (design) => [
|
| 327 |
+
`class environment_${design} extends uvm_env;`,
|
| 328 |
+
` \`uvm_component_utils(environment_${design})`,
|
| 329 |
+
` ${design}_agent agent;`,
|
| 330 |
+
` ${design}_scoreboard sb;`,
|
| 331 |
+
` ${design}_coverage_collector cov;`,
|
| 332 |
+
"",
|
| 333 |
+
" function new(string n, uvm_component p); super.new(n, p); endfunction",
|
| 334 |
+
" function void build_phase(uvm_phase phase);",
|
| 335 |
+
` agent = ${design}_agent::type_id::create("agent", this);`,
|
| 336 |
+
` sb = ${design}_scoreboard::type_id::create("sb", this);`,
|
| 337 |
+
` cov = ${design}_coverage_collector::type_id::create("cov", this);`,
|
| 338 |
+
" endfunction",
|
| 339 |
+
" function void connect_phase(uvm_phase phase);",
|
| 340 |
+
" agent.monitor.item_collected_port.connect(sb.act_export);",
|
| 341 |
+
" agent.monitor.item_collected_port.connect(cov.analysis_export);",
|
| 342 |
+
" endfunction",
|
| 343 |
+
"endclass",
|
| 344 |
+
].join("\n"),
|
| 345 |
+
|
| 346 |
+
rtl_baud_gen: () => [
|
| 347 |
+
"module uart_baud_gen #(parameter CLK_FREQ=50000000, parameter BAUD_RATE=115200)",
|
| 348 |
+
" (input logic clk, rst_n, input logic [7:0] divisor, output logic baud_tick);",
|
| 349 |
+
" logic [15:0] period = CLK_FREQ / (BAUD_RATE * 16);",
|
| 350 |
+
" logic [15:0] counter;",
|
| 351 |
+
" always_ff @(posedge clk or negedge rst_n)",
|
| 352 |
+
" if (!rst_n) begin counter <= '0; baud_tick <= 0; end",
|
| 353 |
+
" else if (counter >= period-1) begin counter <= '0; baud_tick <= 1; end",
|
| 354 |
+
" else begin counter <= counter+1; baud_tick <= 0; end",
|
| 355 |
+
"endmodule",
|
| 356 |
+
].join("\n"),
|
| 357 |
+
|
| 358 |
+
rtl_transmitter: () => [
|
| 359 |
+
"module uart_transmitter (input logic clk, rst_n, baud_tick,",
|
| 360 |
+
" input logic [7:0] data_in, input logic we, output logic tx);",
|
| 361 |
+
" typedef enum {IDLE,START,DATA,STOP} state_t; state_t state;",
|
| 362 |
+
" logic [7:0] shift_reg; logic [2:0] bit_cnt;",
|
| 363 |
+
" always_ff @(posedge clk or negedge rst_n)",
|
| 364 |
+
" if (!rst_n) begin state <= IDLE; tx <= 1; end",
|
| 365 |
+
" else case (state)",
|
| 366 |
+
" IDLE: if (we) begin shift_reg <= data_in; bit_cnt<=0; state<=START; end",
|
| 367 |
+
" START: begin tx<=0; if(baud_tick) state<=DATA; end",
|
| 368 |
+
" DATA: if(baud_tick) begin tx<=shift_reg[bit_cnt];",
|
| 369 |
+
" if(bit_cnt==7) state<=STOP; else bit_cnt<=bit_cnt+1; end",
|
| 370 |
+
" STOP: begin tx<=1; if(baud_tick) state<=IDLE; end",
|
| 371 |
+
" endcase",
|
| 372 |
+
"endmodule",
|
| 373 |
+
].join("\n"),
|
| 374 |
+
|
| 375 |
+
rtl_receiver: () => [
|
| 376 |
+
"module uart_receiver (input logic clk, rst_n, baud_tick, rx,",
|
| 377 |
+
" output logic [7:0] data_out, output logic data_ready);",
|
| 378 |
+
" typedef enum {IDLE,START,DATA,STOP} state_t; state_t state;",
|
| 379 |
+
" logic [7:0] shift_reg; logic [2:0] bit_cnt; logic rx_sync;",
|
| 380 |
+
" always_ff @(posedge clk or negedge rst_n)",
|
| 381 |
+
" if (!rst_n) begin state<=IDLE; rx_sync<=1; data_ready<=0; end",
|
| 382 |
+
" else case (state)",
|
| 383 |
+
" IDLE: begin data_ready<=0; rx_sync<=rx; if(!rx_sync) begin state<=START; end end",
|
| 384 |
+
" START: if(baud_tick) state<=DATA;",
|
| 385 |
+
" DATA: if(baud_tick) begin shift_reg[bit_cnt]<=rx_sync;",
|
| 386 |
+
" if(bit_cnt==7) state<=STOP; else bit_cnt<=bit_cnt+1; end",
|
| 387 |
+
" STOP: if(baud_tick) begin data_out<=shift_reg; data_ready<=1; state<=IDLE; end",
|
| 388 |
+
" endcase",
|
| 389 |
+
"endmodule",
|
| 390 |
+
].join("\n"),
|
| 391 |
+
|
| 392 |
+
rtl_regs: () => [
|
| 393 |
+
"module uart_regs (input logic clk, rst_n, wb_cyc, wb_stb, wb_we,",
|
| 394 |
+
" input logic [2:0] wb_addr, input logic [7:0] wb_data_i,",
|
| 395 |
+
" output logic [7:0] wb_data_o, output logic wb_ack);",
|
| 396 |
+
" logic [7:0] reg_lcr, reg_scr;",
|
| 397 |
+
" assign wb_ack = wb_cyc & wb_stb;",
|
| 398 |
+
" always_comb case (wb_addr)",
|
| 399 |
+
" 0: wb_data_o = 8'h00; 1: wb_data_o = 8'h00; 2: wb_data_o = 8'hC0;",
|
| 400 |
+
" 3: wb_data_o = reg_lcr; 4: wb_data_o = 8'h00;",
|
| 401 |
+
" 5: wb_data_o = 8'h60; 6: wb_data_o = 8'h00;",
|
| 402 |
+
" 7: wb_data_o = reg_scr; default: wb_data_o = 8'h00;",
|
| 403 |
+
" endcase",
|
| 404 |
+
" always_ff @(posedge clk or negedge rst_n)",
|
| 405 |
+
" if (!rst_n) begin reg_lcr <= 0; reg_scr <= 0; end",
|
| 406 |
+
" else if (wb_cyc & wb_stb & wb_we)",
|
| 407 |
+
" case (wb_addr) 3: reg_lcr <= wb_data_i; 7: reg_scr <= wb_data_i; endcase",
|
| 408 |
+
"endmodule",
|
| 409 |
+
].join("\n"),
|
| 410 |
+
|
| 411 |
+
rtl_top: () => [
|
| 412 |
+
"module uart_top (input logic clk, rst_n, wb_cyc, wb_stb, wb_we,",
|
| 413 |
+
" input logic [2:0] wb_addr, input logic [7:0] wb_data_i,",
|
| 414 |
+
" output logic [7:0] wb_data_o, output logic wb_ack,",
|
| 415 |
+
" output logic uart_tx, input logic uart_rx,",
|
| 416 |
+
" input logic cts_n, output logic rts_n,",
|
| 417 |
+
" input logic dsr_n, output logic dtr_n,",
|
| 418 |
+
" input logic ri_n, dcd_n, output logic out1_n, out2_n, uart_intr);",
|
| 419 |
+
" logic baud_tick;",
|
| 420 |
+
" uart_baud_gen u_baud(.clk(clk),.rst_n(rst_n),.divisor(8'h01),.baud_tick(baud_tick));",
|
| 421 |
+
" uart_transmitter u_tx(.clk(clk),.rst_n(rst_n),.baud_tick(baud_tick),",
|
| 422 |
+
" .data_in(wb_data_i), .we(wb_cyc&wb_stb&wb_we&(wb_addr==0)), .tx(uart_tx));",
|
| 423 |
+
" uart_receiver u_rx(.clk(clk),.rst_n(rst_n),.baud_tick(baud_tick),.rx(uart_rx));",
|
| 424 |
+
" uart_regs u_regs(.clk(clk),.rst_n(rst_n),.wb_cyc(wb_cyc),.wb_stb(wb_stb),",
|
| 425 |
+
" .wb_we(wb_we),.wb_addr(wb_addr),.wb_data_i(wb_data_i),",
|
| 426 |
+
" .wb_data_o(wb_data_o),.wb_ack(wb_ack));",
|
| 427 |
+
" assign uart_intr = 0; assign rts_n = 0; assign dtr_n = 0;",
|
| 428 |
+
" assign out1_n = 1; assign out2_n = 1;",
|
| 429 |
+
"endmodule",
|
| 430 |
+
].join("\n"),
|
| 431 |
+
};
|
| 432 |
+
|
| 433 |
+
function buildFileContent(design, version, iteration) {
|
| 434 |
+
const t = FILE_CONTENT_TEMPLATES;
|
| 435 |
+
const prefix = `v${version}_it${iteration}_`;
|
| 436 |
+
const files = {
|
| 437 |
+
"testbench.sv": t.testbench(design),
|
| 438 |
+
[`interface_${design}.sv`]: t.interface(design),
|
| 439 |
+
[`sequence_item_${design}.sv`]: t.seq_item(design),
|
| 440 |
+
[`driver_${design}.sv`]: t.driver(design),
|
| 441 |
+
[`monitor_${design}.sv`]: t.monitor(design),
|
| 442 |
+
[`agent_${design}.sv`]: t.agent(design),
|
| 443 |
+
[`scoreboard_${design}.sv`]: t.scoreboard(design),
|
| 444 |
+
[`coverage_collector_${design}.sv`]: t.coverage_collector(design),
|
| 445 |
+
[`base_sequence_${design}.sv`]: t.base_sequence(design),
|
| 446 |
+
[`test_${design}.sv`]: t.test(design),
|
| 447 |
+
[`environment_${design}.sv`]: t.env(design),
|
| 448 |
+
[`regression_${design}.sv`]: t.regression(design),
|
| 449 |
+
"rtl/uart_baud_gen.v": t.rtl_baud_gen(),
|
| 450 |
+
"rtl/uart_transmitter.v": t.rtl_transmitter(),
|
| 451 |
+
"rtl/uart_receiver.v": t.rtl_receiver(),
|
| 452 |
+
"rtl/uart_regs.v": t.rtl_regs(),
|
| 453 |
+
"rtl/uart_top.v": t.rtl_top(),
|
| 454 |
+
"compile.f": [
|
| 455 |
+
"// Compile list",
|
| 456 |
+
"./rtl/uart_baud_gen.v ./rtl/uart_transmitter.v",
|
| 457 |
+
"./rtl/uart_receiver.v ./rtl/uart_regs.v ./rtl/uart_top.v",
|
| 458 |
+
`./sequence_item_${design}.sv ./interface_${design}.sv`,
|
| 459 |
+
`./driver_${design}.sv ./monitor_${design}.sv ./agent_${design}.sv`,
|
| 460 |
+
`./scoreboard_${design}.sv ./coverage_collector_${design}.sv`,
|
| 461 |
+
`./base_sequence_${design}.sv ./test_${design}.sv ./environment_${design}.sv`,
|
| 462 |
+
"./testbench.sv",
|
| 463 |
+
"+define+UVM_NO_DPI",
|
| 464 |
+
].join("\n"),
|
| 465 |
+
};
|
| 466 |
+
|
| 467 |
+
return Object.entries(files).map(([name, content]) => ({
|
| 468 |
+
name,
|
| 469 |
+
path: `output/${design}_tb/${prefix}${name}`,
|
| 470 |
+
content,
|
| 471 |
+
size: `${new Blob([content]).size} B`,
|
| 472 |
+
}));
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
// Coverage stub simulator with multi-seed emulation
|
| 476 |
+
function simulateCoverage(artifacts, seed = 1) {
|
| 477 |
+
const allText = artifacts.map((a) => a.content).join("\n");
|
| 478 |
+
const bins = [];
|
| 479 |
+
const rng = ((seed) => {
|
| 480 |
+
let s = seed;
|
| 481 |
+
return () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; };
|
| 482 |
+
})(seed);
|
| 483 |
+
|
| 484 |
+
for (let addr = 0; addr < 8; addr++) {
|
| 485 |
+
bins.push({ name: `bus_cg.ADDR.regs[${addr}]`, hit: false, goal: 1 });
|
| 486 |
+
bins.push({ name: `cross_ADRxDIR.addr${addr}_read`, hit: false, goal: 1 });
|
| 487 |
+
bins.push({ name: `cross_ADRxDIR.addr${addr}_write`, hit: false, goal: 1 });
|
| 488 |
+
}
|
| 489 |
+
bins.push({ name: "bus_cg.DIR.read", hit: false, goal: 1 });
|
| 490 |
+
bins.push({ name: "bus_cg.DIR.write", hit: false, goal: 1 });
|
| 491 |
+
bins.push({ name: "data_cg.DATA.zero", hit: false, goal: 1 });
|
| 492 |
+
bins.push({ name: "data_cg.DATA.ones", hit: false, goal: 1 });
|
| 493 |
+
// Protocol checker SVA bins
|
| 494 |
+
bins.push({ name: "sva_cp.ack_timing", hit: false, goal: 1 });
|
| 495 |
+
bins.push({ name: "sva_cp.data_stable", hit: false, goal: 1 });
|
| 496 |
+
|
| 497 |
+
for (const bin of bins) {
|
| 498 |
+
if (bin.name.includes("DIR.read") && allText.includes("item.we = 0")) bin.hit = true;
|
| 499 |
+
else if (bin.name.includes("DIR.write") && allText.includes("item.we = 1")) bin.hit = true;
|
| 500 |
+
else {
|
| 501 |
+
const m = bin.name.match(/regs\[(\d+)\]/);
|
| 502 |
+
if (m) {
|
| 503 |
+
const addr = m[1];
|
| 504 |
+
if (allText.includes(`reg_addr = 3'h${addr}`) || allText.includes(`reg_addr=${addr}`)) {
|
| 505 |
+
bin.hit = true;
|
| 506 |
+
}
|
| 507 |
+
}
|
| 508 |
+
const cm = bin.name.match(/addr(\d+)_(read|write)/);
|
| 509 |
+
if (cm) {
|
| 510 |
+
const addr = cm[1];
|
| 511 |
+
const dir = cm[2] === "write" ? "1" : "0";
|
| 512 |
+
if (
|
| 513 |
+
allText.includes(`reg_addr = 3'h${addr}`) &&
|
| 514 |
+
allText.includes(`item.we = ${dir}`)
|
| 515 |
+
) {
|
| 516 |
+
bin.hit = true;
|
| 517 |
+
}
|
| 518 |
+
}
|
| 519 |
+
}
|
| 520 |
+
if (bin.name.includes("DATA.zero") && allText.includes("8'h00")) bin.hit = true;
|
| 521 |
+
if (bin.name.includes("DATA.ones") && allText.includes("8'hFF")) bin.hit = true;
|
| 522 |
+
// SVA bins: hit if assertions present
|
| 523 |
+
if (bin.name.includes("sva_cp.ack_timing") && allText.includes("assert property")) bin.hit = true;
|
| 524 |
+
if (bin.name.includes("sva_cp.data_stable") && allText.includes("cover property")) bin.hit = true;
|
| 525 |
+
|
| 526 |
+
// Seed-based randomization: add noise per seed
|
| 527 |
+
if (!bin.hit && rng() < 0.15 * seed) bin.hit = true;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
const total = bins.length;
|
| 531 |
+
const covered = bins.filter((b) => b.hit).length;
|
| 532 |
+
return { total, covered, pct: total > 0 ? (covered / total) * 100 : 0, bins };
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
// Merge coverage across seeds
|
| 536 |
+
function mergeCoverage(seedResults) {
|
| 537 |
+
const merged = {};
|
| 538 |
+
for (const res of seedResults) {
|
| 539 |
+
for (const bin of res.bins) {
|
| 540 |
+
if (!merged[bin.name]) merged[bin.name] = { name: bin.name, hit: false, goal: 1 };
|
| 541 |
+
if (bin.hit) merged[bin.name].hit = true;
|
| 542 |
+
}
|
| 543 |
+
}
|
| 544 |
+
const bins = Object.values(merged);
|
| 545 |
+
const total = bins.length;
|
| 546 |
+
const covered = bins.filter((b) => b.hit).length;
|
| 547 |
+
return { total, covered, pct: total > 0 ? (covered / total) * 100 : 0, bins };
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
export default function usePipeline() {
|
| 551 |
+
const [state, setState] = useState({
|
| 552 |
+
running: false,
|
| 553 |
+
autoTraining: false,
|
| 554 |
+
logs: [],
|
| 555 |
+
artifacts: [],
|
| 556 |
+
error: null,
|
| 557 |
+
versions: [],
|
| 558 |
+
currentVersion: 0,
|
| 559 |
+
coverageTrend: [],
|
| 560 |
+
coverageGaps: [],
|
| 561 |
+
seedResults: [],
|
| 562 |
+
autoTrainIteration: 0,
|
| 563 |
+
autoTrainMax: 5,
|
| 564 |
+
});
|
| 565 |
+
|
| 566 |
+
const appendLog = useCallback((level, message) => {
|
| 567 |
+
const timestamp = new Date().toISOString().slice(11, 23);
|
| 568 |
+
setState((prev) => ({
|
| 569 |
+
...prev,
|
| 570 |
+
logs: [...prev.logs, { level, message, timestamp }],
|
| 571 |
+
}));
|
| 572 |
+
}, []);
|
| 573 |
+
|
| 574 |
+
const runSingle = useCallback(
|
| 575 |
+
async (yamlSpec, version, iteration, totalIterations, numSeeds = 3) => {
|
| 576 |
+
appendLog("info", `Iteration ${iteration}/${totalIterations} — generating...`);
|
| 577 |
+
|
| 578 |
+
const spec = (() => {
|
| 579 |
+
try {
|
| 580 |
+
return JSON.parse(yamlSpec);
|
| 581 |
+
} catch {
|
| 582 |
+
return {};
|
| 583 |
+
}
|
| 584 |
+
})();
|
| 585 |
+
const design = spec?.design_name || "uart";
|
| 586 |
+
|
| 587 |
+
// Generate artifacts for this version
|
| 588 |
+
const artifacts = buildFileContent(design, version, iteration);
|
| 589 |
+
appendLog("success", `Generated ${artifacts.length} files (version v${version})`);
|
| 590 |
+
|
| 591 |
+
// Multi-seed regression
|
| 592 |
+
await new Promise((r) => setTimeout(r, 200));
|
| 593 |
+
const seedResults = [];
|
| 594 |
+
for (let s = 1; s <= numSeeds; s++) {
|
| 595 |
+
const cov = simulateCoverage(artifacts, s);
|
| 596 |
+
seedResults.push(cov);
|
| 597 |
+
}
|
| 598 |
+
const merged = mergeCoverage(seedResults);
|
| 599 |
+
const uncovered = merged.bins.filter((b) => !b.hit);
|
| 600 |
+
const gaps = uncovered.map((b) => ({
|
| 601 |
+
bin: b.name,
|
| 602 |
+
covered: false,
|
| 603 |
+
}));
|
| 604 |
+
|
| 605 |
+
appendLog(
|
| 606 |
+
merged.pct >= 90 ? "success" : "warn",
|
| 607 |
+
`Coverage: ${merged.covered}/${merged.total} (${merged.pct.toFixed(1)}%, merged ${numSeeds} seeds)`
|
| 608 |
+
);
|
| 609 |
+
|
| 610 |
+
if (gaps.length > 0) {
|
| 611 |
+
appendLog("warn", `Coverage gaps: ${gaps.length} uncovered bins`);
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
return { artifacts, cov: merged, gaps, version, iteration, seedResults, numSeeds };
|
| 615 |
+
},
|
| 616 |
+
[appendLog]
|
| 617 |
+
);
|
| 618 |
+
|
| 619 |
+
const runPipeline = useCallback(
|
| 620 |
+
async (yamlSpec) => {
|
| 621 |
+
const spec = (() => {
|
| 622 |
+
try {
|
| 623 |
+
return JSON.parse(yamlSpec);
|
| 624 |
+
} catch {
|
| 625 |
+
return {};
|
| 626 |
+
}
|
| 627 |
+
})();
|
| 628 |
+
const design = spec?.design_name || "uart";
|
| 629 |
+
|
| 630 |
+
setState((prev) => ({
|
| 631 |
+
...prev,
|
| 632 |
+
running: true,
|
| 633 |
+
logs: [],
|
| 634 |
+
artifacts: [],
|
| 635 |
+
error: null,
|
| 636 |
+
currentVersion: 0,
|
| 637 |
+
}));
|
| 638 |
+
|
| 639 |
+
appendLog("info", "Pipeline started — validation passed");
|
| 640 |
+
appendLog("info", "Generating UVM testbench...");
|
| 641 |
+
|
| 642 |
+
try {
|
| 643 |
+
const result = await runSingle(yamlSpec, 1, 1, 1);
|
| 644 |
+
const trend = [{ version: "v1", coverage: result.cov.pct }];
|
| 645 |
+
|
| 646 |
+
appendLog("success", "Pipeline completed");
|
| 647 |
+
setState((prev) => ({
|
| 648 |
+
...prev,
|
| 649 |
+
running: false,
|
| 650 |
+
artifacts: result.artifacts,
|
| 651 |
+
versions: [{ version: "v1", coverage: result.cov.pct, files: result.artifacts.length }],
|
| 652 |
+
currentVersion: 1,
|
| 653 |
+
coverageTrend: trend,
|
| 654 |
+
coverageGaps: result.gaps,
|
| 655 |
+
autoTrainIteration: 1,
|
| 656 |
+
autoTrainMax: 1,
|
| 657 |
+
}));
|
| 658 |
+
} catch (err) {
|
| 659 |
+
appendLog("error", `Pipeline failed: ${err.message}`);
|
| 660 |
+
setState((prev) => ({ ...prev, running: false, error: err.message }));
|
| 661 |
+
}
|
| 662 |
+
},
|
| 663 |
+
[appendLog, runSingle]
|
| 664 |
+
);
|
| 665 |
+
|
| 666 |
+
const runAutoTrain = useCallback(
|
| 667 |
+
async (yamlSpec, maxIterations) => {
|
| 668 |
+
const spec = (() => {
|
| 669 |
+
try {
|
| 670 |
+
return JSON.parse(yamlSpec);
|
| 671 |
+
} catch {
|
| 672 |
+
return {};
|
| 673 |
+
}
|
| 674 |
+
})();
|
| 675 |
+
const design = spec?.design_name || "uart";
|
| 676 |
+
|
| 677 |
+
setState((prev) => ({
|
| 678 |
+
...prev,
|
| 679 |
+
running: true,
|
| 680 |
+
autoTraining: true,
|
| 681 |
+
logs: [],
|
| 682 |
+
artifacts: [],
|
| 683 |
+
error: null,
|
| 684 |
+
versions: [],
|
| 685 |
+
currentVersion: 0,
|
| 686 |
+
coverageTrend: [],
|
| 687 |
+
coverageGaps: [],
|
| 688 |
+
autoTrainIteration: 0,
|
| 689 |
+
autoTrainMax: maxIterations,
|
| 690 |
+
}));
|
| 691 |
+
|
| 692 |
+
appendLog("info", `Auto-training started — max ${maxIterations} iterations`);
|
| 693 |
+
appendLog("info", "Pipeline started — validation passed");
|
| 694 |
+
|
| 695 |
+
let allVersions = [];
|
| 696 |
+
let trend = [];
|
| 697 |
+
let latestArtifacts = [];
|
| 698 |
+
let latestGaps = [];
|
| 699 |
+
let latestSeedResults = [];
|
| 700 |
+
let iteration = 0;
|
| 701 |
+
|
| 702 |
+
for (iteration = 1; iteration <= maxIterations; iteration++) {
|
| 703 |
+
appendLog("info", `=== Auto-train iteration ${iteration}/${maxIterations} ===`);
|
| 704 |
+
appendLog("info", `Generating testbench (v${iteration})...`);
|
| 705 |
+
|
| 706 |
+
try {
|
| 707 |
+
const result = await runSingle(yamlSpec, iteration, iteration, maxIterations, 3);
|
| 708 |
+
|
| 709 |
+
const ver = {
|
| 710 |
+
version: `v${iteration}`,
|
| 711 |
+
coverage: result.cov.pct,
|
| 712 |
+
files: result.artifacts.length,
|
| 713 |
+
};
|
| 714 |
+
allVersions.push(ver);
|
| 715 |
+
trend.push({ version: `v${iteration}`, coverage: result.cov.pct });
|
| 716 |
+
latestArtifacts = result.artifacts;
|
| 717 |
+
latestGaps = result.gaps;
|
| 718 |
+
latestSeedResults = result.seedResults || [];
|
| 719 |
+
|
| 720 |
+
// Check convergence
|
| 721 |
+
if (result.cov.pct >= 90) {
|
| 722 |
+
appendLog("success", `Coverage target ≥90% reached (${result.cov.pct.toFixed(1)}%)`);
|
| 723 |
+
break;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
if (iteration >= 2) {
|
| 727 |
+
const prev = trend[trend.length - 2]?.coverage || 0;
|
| 728 |
+
const gain = result.cov.pct - prev;
|
| 729 |
+
appendLog("info", `Coverage gain: ${gain > 0 ? "+" : ""}${gain.toFixed(1)}%`);
|
| 730 |
+
if (gain < 2 && iteration >= 3) {
|
| 731 |
+
appendLog("warn", `Gain too low (<2%) — stopping early`);
|
| 732 |
+
break;
|
| 733 |
+
}
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
// Generate targeted sequences for next iteration
|
| 737 |
+
if (result.gaps.length > 0) {
|
| 738 |
+
const addrSet = new Set();
|
| 739 |
+
result.gaps.forEach((g) => {
|
| 740 |
+
const m = g.bin.match(/regs\[(\d+)\]/);
|
| 741 |
+
if (m) addrSet.add(parseInt(m[1]));
|
| 742 |
+
});
|
| 743 |
+
if (addrSet.size > 0) {
|
| 744 |
+
appendLog("info", `Targeting uncovered regs: [${[...addrSet].join(", ")}]`);
|
| 745 |
+
}
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
await new Promise((r) => setTimeout(r, 200));
|
| 749 |
+
} catch (err) {
|
| 750 |
+
appendLog("error", `Iteration ${iteration} failed: ${err.message}`);
|
| 751 |
+
break;
|
| 752 |
+
}
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
appendLog(
|
| 756 |
+
iteration >= maxIterations ? "warn" : "success",
|
| 757 |
+
`Auto-training complete — ${iteration} iterations, ${allVersions.length} versions`
|
| 758 |
+
);
|
| 759 |
+
|
| 760 |
+
setState((prev) => ({
|
| 761 |
+
...prev,
|
| 762 |
+
running: false,
|
| 763 |
+
autoTraining: false,
|
| 764 |
+
artifacts: latestArtifacts,
|
| 765 |
+
versions: allVersions,
|
| 766 |
+
currentVersion: allVersions.length,
|
| 767 |
+
coverageTrend: trend,
|
| 768 |
+
coverageGaps: latestGaps,
|
| 769 |
+
seedResults: latestSeedResults,
|
| 770 |
+
autoTrainIteration: iteration,
|
| 771 |
+
autoTrainMax: maxIterations,
|
| 772 |
+
}));
|
| 773 |
+
},
|
| 774 |
+
[appendLog, runSingle]
|
| 775 |
+
);
|
| 776 |
+
|
| 777 |
+
return { ...state, runPipeline, runAutoTrain };
|
| 778 |
+
}
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
| 6 |
+
|
| 7 |
+
@layer base {
|
| 8 |
+
body {
|
| 9 |
+
@apply font-sans text-sm leading-relaxed;
|
| 10 |
+
}
|
| 11 |
+
h1 { @apply text-2xl font-bold tracking-tight; }
|
| 12 |
+
h2 { @apply text-lg font-semibold; }
|
| 13 |
+
h3 { @apply text-base font-semibold; }
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
@layer components {
|
| 17 |
+
.card {
|
| 18 |
+
@apply bg-white rounded-xl border border-slate-200 shadow-sm;
|
| 19 |
+
}
|
| 20 |
+
.card-header {
|
| 21 |
+
@apply px-5 py-4 border-b border-slate-100 flex items-center gap-2;
|
| 22 |
+
}
|
| 23 |
+
.card-body {
|
| 24 |
+
@apply p-5;
|
| 25 |
+
}
|
| 26 |
+
.btn-primary {
|
| 27 |
+
@apply inline-flex items-center gap-2 rounded-lg bg-brand-600 px-5 py-2.5 text-sm font-semibold text-white
|
| 28 |
+
shadow-sm hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2
|
| 29 |
+
transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
|
| 30 |
+
}
|
| 31 |
+
.btn-secondary {
|
| 32 |
+
@apply inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium
|
| 33 |
+
text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2
|
| 34 |
+
transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
|
| 35 |
+
}
|
| 36 |
+
.input-field {
|
| 37 |
+
@apply block w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm
|
| 38 |
+
placeholder:text-slate-400 focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20
|
| 39 |
+
transition-colors;
|
| 40 |
+
}
|
| 41 |
+
.select-field {
|
| 42 |
+
@apply block w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm
|
| 43 |
+
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 transition-colors;
|
| 44 |
+
}
|
| 45 |
+
.badge {
|
| 46 |
+
@apply inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium;
|
| 47 |
+
}
|
| 48 |
+
.badge-success { @apply badge bg-emerald-50 text-emerald-700; }
|
| 49 |
+
.badge-error { @apply badge bg-red-50 text-red-700; }
|
| 50 |
+
.badge-warning { @apply badge bg-amber-50 text-amber-700; }
|
| 51 |
+
.badge-info { @apply badge bg-sky-50 text-sky-700; }
|
| 52 |
+
|
| 53 |
+
.log-entry { @apply font-mono text-xs leading-6; }
|
| 54 |
+
.log-info { @apply log-entry text-slate-600; }
|
| 55 |
+
.log-error { @apply log-entry text-red-600; }
|
| 56 |
+
.log-success { @apply log-entry text-emerald-600; }
|
| 57 |
+
.log-warn { @apply log-entry text-amber-600; }
|
| 58 |
+
}
|
frontend/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import ReactDOM from "react-dom/client";
|
| 3 |
+
import "./index.css";
|
| 4 |
+
import App from "./App";
|
| 5 |
+
|
| 6 |
+
const root = ReactDOM.createRoot(document.getElementById("root"));
|
| 7 |
+
root.render(
|
| 8 |
+
<React.StrictMode>
|
| 9 |
+
<App />
|
| 10 |
+
</React.StrictMode>
|
| 11 |
+
);
|
frontend/src/utils/yamlUtils.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml from "js-yaml";
|
| 2 |
+
|
| 3 |
+
const PROTOCOLS = ["uart", "spi", "i2c", "axi4lite", "apb", "wishbone", "custom"];
|
| 4 |
+
|
| 5 |
+
const DEFAULT_YAML = `# UART 16550 Core Specification
|
| 6 |
+
design_name: uart16550
|
| 7 |
+
version: "1.5"
|
| 8 |
+
vendor: "DV Automation"
|
| 9 |
+
description: "Universal Asynchronous Receiver-Transmitter"
|
| 10 |
+
clock_reset:
|
| 11 |
+
clock: clk
|
| 12 |
+
reset: rst_n
|
| 13 |
+
reset_active: 0
|
| 14 |
+
interfaces:
|
| 15 |
+
- name: bus
|
| 16 |
+
protocol: wishbone
|
| 17 |
+
signals:
|
| 18 |
+
- { name: addr, direction: input, width: 3 }
|
| 19 |
+
- { name: data_in, direction: input, width: 8 }
|
| 20 |
+
- { name: data_out,direction: output, width: 8 }
|
| 21 |
+
- { name: we, direction: input, width: 1 }
|
| 22 |
+
- { name: irq, direction: output, width: 1 }
|
| 23 |
+
- name: serial
|
| 24 |
+
protocol: uart
|
| 25 |
+
signals:
|
| 26 |
+
- { name: srx, direction: input, width: 1 }
|
| 27 |
+
- { name: stx, direction: output, width: 1 }
|
| 28 |
+
registers:
|
| 29 |
+
- name: LCR
|
| 30 |
+
address: '0x03'
|
| 31 |
+
access: rw
|
| 32 |
+
fields:
|
| 33 |
+
- { name: wls, bits: '1:0', description: "Word length" }
|
| 34 |
+
- { name: stb, bits: '2', description: "Stop bits" }
|
| 35 |
+
- { name: dlab, bits: '7', description: "Divisor latch" }
|
| 36 |
+
- name: LSR
|
| 37 |
+
address: '0x05'
|
| 38 |
+
access: ro
|
| 39 |
+
fields:
|
| 40 |
+
- { name: dr, bits: '0', description: "Data ready" }
|
| 41 |
+
- { name: thre, bits: '5', description: "THR empty" }
|
| 42 |
+
`;
|
| 43 |
+
|
| 44 |
+
export function parseYAML(text) {
|
| 45 |
+
const doc = yaml.load(text);
|
| 46 |
+
return doc || {};
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export function validateYAML(text) {
|
| 50 |
+
const errors = [];
|
| 51 |
+
try {
|
| 52 |
+
const doc = yaml.load(text);
|
| 53 |
+
if (!doc || typeof doc !== "object") {
|
| 54 |
+
errors.push("Root must be a YAML mapping (object)");
|
| 55 |
+
return errors;
|
| 56 |
+
}
|
| 57 |
+
if (!doc.design_name) errors.push("Missing required field: design_name");
|
| 58 |
+
if (!doc.interfaces || !doc.interfaces.length)
|
| 59 |
+
errors.push("At least one interface required");
|
| 60 |
+
if (doc.interfaces) {
|
| 61 |
+
doc.interfaces.forEach((iface, i) => {
|
| 62 |
+
if (!iface.name) errors.push(`Interface #${i + 1} missing name`);
|
| 63 |
+
if (!Array.isArray(iface.signals) || iface.signals.length === 0)
|
| 64 |
+
errors.push(`Interface "${iface.name || "#" + (i + 1)}" has no signals`);
|
| 65 |
+
});
|
| 66 |
+
}
|
| 67 |
+
if (doc.registers) {
|
| 68 |
+
doc.registers.forEach((reg, i) => {
|
| 69 |
+
if (!reg.name) errors.push(`Register #${i + 1} missing name`);
|
| 70 |
+
if (!reg.address) errors.push(`Register "${reg.name || "#" + (i + 1)}" missing address`);
|
| 71 |
+
if (reg.address && !/^0x[0-9a-fA-F]+$/.test(String(reg.address)))
|
| 72 |
+
errors.push(`Register "${reg.name}" address should be hex (0x...)`);
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
} catch (e) {
|
| 76 |
+
errors.push(`YAML parse error: ${e.message}`);
|
| 77 |
+
}
|
| 78 |
+
return errors;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
export function toYAML(obj) {
|
| 82 |
+
return yaml.dump(obj, { indent: 2, lineWidth: 120, noRefs: true });
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
export { PROTOCOLS, DEFAULT_YAML };
|
frontend/tailwind.config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
module.exports = {
|
| 3 |
+
content: ["./src/**/*.{js,jsx}"],
|
| 4 |
+
theme: {
|
| 5 |
+
extend: {
|
| 6 |
+
colors: {
|
| 7 |
+
brand: {
|
| 8 |
+
50: "#eef2ff",
|
| 9 |
+
100: "#e0e7ff",
|
| 10 |
+
200: "#c7d2fe",
|
| 11 |
+
300: "#a5b4fc",
|
| 12 |
+
400: "#818cf8",
|
| 13 |
+
500: "#6366f1",
|
| 14 |
+
600: "#4f46e5",
|
| 15 |
+
700: "#4338ca",
|
| 16 |
+
800: "#3730a3",
|
| 17 |
+
900: "#312e81",
|
| 18 |
+
950: "#1e1b4b",
|
| 19 |
+
},
|
| 20 |
+
},
|
| 21 |
+
fontFamily: {
|
| 22 |
+
sans: ["Inter", "system-ui", "-apple-system", "sans-serif"],
|
| 23 |
+
mono: ["JetBrains Mono", "Fira Code", "monospace"],
|
| 24 |
+
},
|
| 25 |
+
},
|
| 26 |
+
},
|
| 27 |
+
plugins: [],
|
| 28 |
+
};
|
protocols/apb.yaml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# APB Protocol Definition — UVM TB Generator
|
| 2 |
+
protocol: apb
|
| 3 |
+
description: Advanced Peripheral Bus (AMBA 3 APB)
|
| 4 |
+
tags: [bus, memory-mapped, synchronous, low-power]
|
| 5 |
+
|
| 6 |
+
interface_template:
|
| 7 |
+
signals:
|
| 8 |
+
- {name: psel, direction: input, width: 1, description: Select}
|
| 9 |
+
- {name: penable, direction: input, width: 1, description: Enable}
|
| 10 |
+
- {name: paddr, direction: input, width: 32, description: Address}
|
| 11 |
+
- {name: pwrite, direction: input, width: 1, description: Write (1) / Read (0)}
|
| 12 |
+
- {name: pwdata, direction: input, width: 32, description: Write data}
|
| 13 |
+
- {name: prdata, direction: output, width: 32, description: Read data}
|
| 14 |
+
- {name: pready, direction: output, width: 1, description: Ready}
|
| 15 |
+
- {name: pslverr, direction: output, width: 1, description: Slave error}
|
| 16 |
+
|
| 17 |
+
config_parameters:
|
| 18 |
+
- {name: ADDR_WIDTH, type: int, default: 32}
|
| 19 |
+
- {name: DATA_WIDTH, type: int, default: 32}
|
| 20 |
+
|
| 21 |
+
register_template:
|
| 22 |
+
- name: APB_CTRL
|
| 23 |
+
address: 0x00
|
| 24 |
+
access: rw
|
| 25 |
+
fields:
|
| 26 |
+
- {name: enable, bits: 0, description: Core enable}
|
| 27 |
+
- name: APB_DATA
|
| 28 |
+
address: 0x04
|
| 29 |
+
access: rw
|
| 30 |
+
fields:
|
| 31 |
+
- {name: data, bits: 31:0, description: Data value}
|
| 32 |
+
|
| 33 |
+
sequence_template:
|
| 34 |
+
name: apb_sequence
|
| 35 |
+
body: |
|
| 36 |
+
// APB write sequence
|
| 37 |
+
paddr = addr;
|
| 38 |
+
pwdata = data;
|
| 39 |
+
pwrite = 1;
|
| 40 |
+
psel = 1;
|
| 41 |
+
@(posedge clk);
|
| 42 |
+
penable = 1;
|
| 43 |
+
wait(pready);
|
| 44 |
+
@(posedge clk);
|
| 45 |
+
psel = 0; penable = 0;
|
| 46 |
+
|
| 47 |
+
coverage_template:
|
| 48 |
+
- name: apb_cg
|
| 49 |
+
type: covergroup
|
| 50 |
+
items:
|
| 51 |
+
- {name: cg_op, type: coverpoint, expression: pwrite}
|
protocols/axi4lite.yaml
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AXI4-Lite Protocol Definition — UVM TB Generator
|
| 2 |
+
protocol: axi4lite
|
| 3 |
+
description: AXI4-Lite — simplified address-only bus
|
| 4 |
+
tags: [bus, memory-mapped, synchronous, AMBA]
|
| 5 |
+
|
| 6 |
+
interface_template:
|
| 7 |
+
signals:
|
| 8 |
+
# Write address channel
|
| 9 |
+
- {name: awvalid, direction: input, width: 1, description: Write address valid}
|
| 10 |
+
- {name: awready, direction: output, width: 1, description: Write address ready}
|
| 11 |
+
- {name: awaddr, direction: input, width: 32, description: Write address}
|
| 12 |
+
- {name: awprot, direction: input, width: 3, description: Protection type}
|
| 13 |
+
# Write data channel
|
| 14 |
+
- {name: wvalid, direction: input, width: 1, description: Write data valid}
|
| 15 |
+
- {name: wready, direction: output, width: 1, description: Write data ready}
|
| 16 |
+
- {name: wdata, direction: input, width: 32, description: Write data}
|
| 17 |
+
- {name: wstrb, direction: input, width: 4, description: Write strobes}
|
| 18 |
+
# Write response channel
|
| 19 |
+
- {name: bvalid, direction: output, width: 1, description: Write response valid}
|
| 20 |
+
- {name: bready, direction: input, width: 1, description: Write response ready}
|
| 21 |
+
- {name: bresp, direction: output, width: 2, description: Write response}
|
| 22 |
+
# Read address channel
|
| 23 |
+
- {name: arvalid, direction: input, width: 1, description: Read address valid}
|
| 24 |
+
- {name: arready, direction: output, width: 1, description: Read address ready}
|
| 25 |
+
- {name: araddr, direction: input, width: 32, description: Read address}
|
| 26 |
+
- {name: arprot, direction: input, width: 3, description: Protection type}
|
| 27 |
+
# Read data channel
|
| 28 |
+
- {name: rvalid, direction: output, width: 1, description: Read data valid}
|
| 29 |
+
- {name: rready, direction: input, width: 1, description: Read data ready}
|
| 30 |
+
- {name: rdata, direction: output, width: 32, description: Read data}
|
| 31 |
+
- {name: rresp, direction: output, width: 2, description: Read response}
|
| 32 |
+
|
| 33 |
+
config_parameters:
|
| 34 |
+
- {name: ADDR_WIDTH, type: int, default: 32, enum: [12, 16, 32]}
|
| 35 |
+
- {name: DATA_WIDTH, type: int, default: 32, enum: [32, 64]}
|
| 36 |
+
|
| 37 |
+
register_template:
|
| 38 |
+
- name: AXI_CTRL
|
| 39 |
+
address: 0x0000
|
| 40 |
+
access: rw
|
| 41 |
+
fields:
|
| 42 |
+
- {name: enable, bits: 0, description: Core enable}
|
| 43 |
+
- {name: irq_en, bits: 1, description: Interrupt enable}
|
| 44 |
+
- name: AXI_STATUS
|
| 45 |
+
address: 0x0004
|
| 46 |
+
access: ro
|
| 47 |
+
fields:
|
| 48 |
+
- {name: ready, bits: 0, description: Core ready}
|
| 49 |
+
- {name: error, bits: 1, description: Error detected}
|
| 50 |
+
|
| 51 |
+
sequence_template:
|
| 52 |
+
name: axi4lite_sequence
|
| 53 |
+
body: |
|
| 54 |
+
// AXI4-Lite read-modify-write sequence
|
| 55 |
+
drv.axi_read_reg(base_addr + offset, read_data);
|
| 56 |
+
read_data[field] = value;
|
| 57 |
+
drv.axi_write_reg(base_addr + offset, read_data);
|
| 58 |
+
|
| 59 |
+
coverage_template:
|
| 60 |
+
- name: axi_cg
|
| 61 |
+
type: covergroup
|
| 62 |
+
items:
|
| 63 |
+
- {name: cg_resp, type: coverpoint, expression: {bresp, rresp}}
|
| 64 |
+
- {name: cg_addr, type: coverpoint, expression: {awaddr, araddr}}
|
protocols/i2c.yaml
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# I2C Protocol Definition — UVM TB Generator
|
| 2 |
+
protocol: i2c
|
| 3 |
+
description: Inter-Integrated Circuit
|
| 4 |
+
tags: [serial, synchronous, multi-master, open-drain]
|
| 5 |
+
|
| 6 |
+
interface_template:
|
| 7 |
+
signals:
|
| 8 |
+
- {name: scl, direction: inout, width: 1, description: Serial clock line}
|
| 9 |
+
- {name: sda, direction: inout, width: 1, description: Serial data line}
|
| 10 |
+
|
| 11 |
+
config_parameters:
|
| 12 |
+
- {name: SLAVE_ADDR, type: int, default: 0x50, minimum: 0x00, maximum: 0x7F, description: 7-bit slave address}
|
| 13 |
+
- {name: CLK_FREQ, type: int, default: 100000, description: I2C bus frequency (Hz)}
|
| 14 |
+
- {name: ADDR_WIDTH, type: int, default: 7, enum: [7, 10], description: Addressing mode}
|
| 15 |
+
|
| 16 |
+
register_template:
|
| 17 |
+
- name: CR # Control Register
|
| 18 |
+
address: 0x00
|
| 19 |
+
access: rw
|
| 20 |
+
fields:
|
| 21 |
+
- {name: enable, bits: 0, description: I2C core enable}
|
| 22 |
+
- {name: irq_en, bits: 1, description: Interrupt enable}
|
| 23 |
+
- {name: addr_mode, bits: 2, description: 0=7bit, 1=10bit}
|
| 24 |
+
- name: ADR # Slave Address Register
|
| 25 |
+
address: 0x04
|
| 26 |
+
access: rw
|
| 27 |
+
fields:
|
| 28 |
+
- {name: addr, bits: 9:0, description: Slave address}
|
| 29 |
+
- name: DR # Data Register
|
| 30 |
+
address: 0x08
|
| 31 |
+
access: rw
|
| 32 |
+
fields:
|
| 33 |
+
- {name: data, bits: 7:0, description: I2C data byte}
|
| 34 |
+
- name: SR # Status Register
|
| 35 |
+
address: 0x0C
|
| 36 |
+
access: ro
|
| 37 |
+
fields:
|
| 38 |
+
- {name: busy, bits: 0, description: Bus busy}
|
| 39 |
+
- {name: rx_ack, bits: 1, description: Receive acknowledge}
|
| 40 |
+
- {name: tx_rdy, bits: 2, description: TX ready}
|
| 41 |
+
- {name: rx_rdy, bits: 3, description: RX ready}
|
| 42 |
+
- {name: arb_lost,bits: 4, description: Arbitration lost}
|
| 43 |
+
- {name: nack, bits: 5, description: No acknowledge received}
|
| 44 |
+
|
| 45 |
+
sequence_template:
|
| 46 |
+
name: i2c_sequence
|
| 47 |
+
body: |
|
| 48 |
+
// I2C write sequence
|
| 49 |
+
drv.write_reg(ADR, slave_addr);
|
| 50 |
+
drv.write_reg(CR, {1'b1, enable});
|
| 51 |
+
foreach (tx_data[i]) begin
|
| 52 |
+
drv.write_reg(DR, tx_data[i]);
|
| 53 |
+
drv.wait_for_field(SR, tx_rdy, 1);
|
| 54 |
+
end
|
| 55 |
+
|
| 56 |
+
coverage_template:
|
| 57 |
+
- name: i2c_cg
|
| 58 |
+
type: covergroup
|
| 59 |
+
items:
|
| 60 |
+
- {name: cg_addr, type: coverpoint, expression: slave_addr}
|
| 61 |
+
- {name: cg_dir, type: coverpoint, expression: {read, write}}
|
protocols/spi.yaml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPI Protocol Definition — UVM TB Generator
|
| 2 |
+
protocol: spi
|
| 3 |
+
description: Serial Peripheral Interface
|
| 4 |
+
tags: [serial, synchronous, full-duplex, master-slave]
|
| 5 |
+
|
| 6 |
+
interface_template:
|
| 7 |
+
signals:
|
| 8 |
+
- {name: sclk, direction: input, width: 1, description: Serial clock}
|
| 9 |
+
- {name: mosi, direction: input, width: 1, description: Master out slave in}
|
| 10 |
+
- {name: miso, direction: output, width: 1, description: Master in slave out}
|
| 11 |
+
- {name: ss_n, direction: input, width: 1, description: Slave select (active-low)}
|
| 12 |
+
|
| 13 |
+
config_parameters:
|
| 14 |
+
- {name: CPOL, type: int, default: 0, enum: [0, 1], description: Clock polarity}
|
| 15 |
+
- {name: CPHA, type: int, default: 0, enum: [0, 1], description: Clock phase}
|
| 16 |
+
- {name: DATA_WIDTH, type: int, default: 8, enum: [4, 8, 16, 32], description: Data width}
|
| 17 |
+
- {name: MSB_FIRST, type: bool, default: true, description: MSB-first transfer}
|
| 18 |
+
|
| 19 |
+
register_template:
|
| 20 |
+
- name: CR0 # Control Register 0
|
| 21 |
+
address: 0x00
|
| 22 |
+
access: rw
|
| 23 |
+
fields:
|
| 24 |
+
- {name: div, bits: 7:0, description: Clock divider}
|
| 25 |
+
- {name: cpol, bits: 8, description: Clock polarity}
|
| 26 |
+
- {name: cpha, bits: 9, description: Clock phase}
|
| 27 |
+
- {name: data_width,bits: 15:12,description: Data width minus 1}
|
| 28 |
+
- name: DR # Data Register
|
| 29 |
+
address: 0x04
|
| 30 |
+
access: rw
|
| 31 |
+
fields:
|
| 32 |
+
- {name: data, bits: 15:0, description: SPI data}
|
| 33 |
+
- name: SR # Status Register
|
| 34 |
+
address: 0x08
|
| 35 |
+
access: ro
|
| 36 |
+
fields:
|
| 37 |
+
- {name: busy, bits: 0, description: Transfer in progress}
|
| 38 |
+
- {name: rxf, bits: 1, description: RX FIFO full}
|
| 39 |
+
- {name: txe, bits: 2, description: TX FIFO empty}
|
| 40 |
+
|
| 41 |
+
sequence_template:
|
| 42 |
+
name: spi_sequence
|
| 43 |
+
body: |
|
| 44 |
+
// SPI transmit sequence
|
| 45 |
+
cr0 = {cpol, cpha, div};
|
| 46 |
+
drv.write_reg(CR0, cr0);
|
| 47 |
+
foreach (tx_data[i]) begin
|
| 48 |
+
drv.write_reg(DR, tx_data[i]);
|
| 49 |
+
drv.wait_for_field(SR, busy, 0);
|
| 50 |
+
end
|
| 51 |
+
|
| 52 |
+
coverage_template:
|
| 53 |
+
- name: spi_cg
|
| 54 |
+
type: covergroup
|
| 55 |
+
items:
|
| 56 |
+
- {name: cg_mode, type: coverpoint, expression: {cpol, cpha}}
|
| 57 |
+
- {name: cg_width, type: coverpoint, expression: data_width}
|
protocols/uart.yaml
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# UART Protocol Definition — UVM TB Generator
|
| 2 |
+
# Reference: 16550 / NS16C550 compatible
|
| 3 |
+
protocol: uart
|
| 4 |
+
description: Universal Asynchronous Receiver-Transmitter
|
| 5 |
+
tags: [serial, asynchronous, full-duplex]
|
| 6 |
+
|
| 7 |
+
interface_template:
|
| 8 |
+
signals:
|
| 9 |
+
- {name: srx, direction: input, width: 1, description: Serial receive}
|
| 10 |
+
- {name: stx, direction: output, width: 1, description: Serial transmit}
|
| 11 |
+
- {name: cts_n, direction: input, width: 1, description: Clear to send (active-low)}
|
| 12 |
+
- {name: rts_n, direction: output, width: 1, description: Request to send (active-low)}
|
| 13 |
+
|
| 14 |
+
baud_rates: [9600, 19200, 38400, 57600, 115200, 230400, 460800]
|
| 15 |
+
|
| 16 |
+
config_parameters:
|
| 17 |
+
- {name: DATA_BITS, type: int, default: 8, enum: [5, 6, 7, 8]}
|
| 18 |
+
- {name: STOP_BITS, type: int, default: 1, enum: [1, 2]}
|
| 19 |
+
- {name: PARITY, type: string, default: "none", enum: [none, even, odd, mark, space]}
|
| 20 |
+
|
| 21 |
+
register_template:
|
| 22 |
+
- name: RBR # Receiver Buffer Register
|
| 23 |
+
address: 0x00
|
| 24 |
+
access: ro
|
| 25 |
+
fields:
|
| 26 |
+
- {name: rbr_data, bits: 7:0, access: ro, description: Received data byte}
|
| 27 |
+
- name: THR # Transmitter Holding Register
|
| 28 |
+
address: 0x00
|
| 29 |
+
access: wo
|
| 30 |
+
fields:
|
| 31 |
+
- {name: thr_data, bits: 7:0, access: wo, description: Transmit data byte}
|
| 32 |
+
- name: IER # Interrupt Enable Register
|
| 33 |
+
address: 0x01
|
| 34 |
+
access: rw
|
| 35 |
+
fields:
|
| 36 |
+
- {name: erbfi, bits: 0, description: Enable RX data available interrupt}
|
| 37 |
+
- {name: etbei, bits: 1, description: Enable TX holding register empty interrupt}
|
| 38 |
+
- {name: elsi, bits: 2, description: Enable RX line status interrupt}
|
| 39 |
+
- {name: edssi, bits: 3, description: Enable modem status interrupt}
|
| 40 |
+
- name: IIR # Interrupt Identification Register
|
| 41 |
+
address: 0x02
|
| 42 |
+
access: ro
|
| 43 |
+
fields:
|
| 44 |
+
- {name: int_id, bits: 3:0, description: Interrupt type identifier}
|
| 45 |
+
- {name: fifos_en, bits: 7:6, description: FIFO enable status}
|
| 46 |
+
- name: FCR # FIFO Control Register
|
| 47 |
+
address: 0x02
|
| 48 |
+
access: wo
|
| 49 |
+
fields:
|
| 50 |
+
- {name: fifo_en, bits: 0, description: Enable FIFOs}
|
| 51 |
+
- {name: rclr, bits: 1, description: Clear RX FIFO}
|
| 52 |
+
- {name: tclr, bits: 2, description: Clear TX FIFO}
|
| 53 |
+
- {name: dma_mode, bits: 3, description: DMA mode select}
|
| 54 |
+
- {name: rx_trigger,bits: 7:6, description: RX FIFO trigger level}
|
| 55 |
+
- name: LCR # Line Control Register
|
| 56 |
+
address: 0x03
|
| 57 |
+
access: rw
|
| 58 |
+
fields:
|
| 59 |
+
- {name: wls, bits: 1:0, description: Word length select}
|
| 60 |
+
- {name: stb, bits: 2, description: Stop bits}
|
| 61 |
+
- {name: pen, bits: 3, description: Parity enable}
|
| 62 |
+
- {name: eps, bits: 4, description: Even parity select}
|
| 63 |
+
- {name: sp, bits: 5, description: Stick parity}
|
| 64 |
+
- {name: bc, bits: 6, description: Break control}
|
| 65 |
+
- {name: dlab, bits: 7, description: Divisor latch access bit}
|
| 66 |
+
- name: MCR # Modem Control Register
|
| 67 |
+
address: 0x04
|
| 68 |
+
access: rw
|
| 69 |
+
fields:
|
| 70 |
+
- {name: dtr, bits: 0, description: Data Terminal Ready}
|
| 71 |
+
- {name: rts, bits: 1, description: Request To Send}
|
| 72 |
+
- {name: out1, bits: 2, description: Output 1}
|
| 73 |
+
- {name: out2, bits: 3, description: Output 2}
|
| 74 |
+
- {name: loop, bits: 4, description: Loopback mode enable}
|
| 75 |
+
- name: LSR # Line Status Register
|
| 76 |
+
address: 0x05
|
| 77 |
+
access: ro
|
| 78 |
+
fields:
|
| 79 |
+
- {name: dr, bits: 0, description: Data Ready}
|
| 80 |
+
- {name: oe, bits: 1, description: Overrun Error}
|
| 81 |
+
- {name: pe, bits: 2, description: Parity Error}
|
| 82 |
+
- {name: fe, bits: 3, description: Framing Error}
|
| 83 |
+
- {name: bi, bits: 4, description: Break Interrupt}
|
| 84 |
+
- {name: thre, bits: 5, description: TX Holding Register Empty}
|
| 85 |
+
- {name: temt, bits: 6, description: Transmitter Empty}
|
| 86 |
+
- {name: err, bits: 7, description: Error in RX FIFO}
|
| 87 |
+
- name: MSR # Modem Status Register
|
| 88 |
+
address: 0x06
|
| 89 |
+
access: ro
|
| 90 |
+
fields:
|
| 91 |
+
- {name: dcts, bits: 0, description: Delta Clear To Send}
|
| 92 |
+
- {name: ddsr, bits: 1, description: Delta Data Set Ready}
|
| 93 |
+
- {name: teri, bits: 2, description: Trailing Edge Ring Indicator}
|
| 94 |
+
- {name: ddcd, bits: 3, description: Delta Data Carrier Detect}
|
| 95 |
+
- {name: cts, bits: 4, description: Clear To Send}
|
| 96 |
+
- {name: dsr, bits: 5, description: Data Set Ready}
|
| 97 |
+
- {name: ri, bits: 6, description: Ring Indicator}
|
| 98 |
+
- {name: dcd, bits: 7, description: Data Carrier Detect}
|
| 99 |
+
- name: SCR # Scratch Register
|
| 100 |
+
address: 0x07
|
| 101 |
+
access: rw
|
| 102 |
+
fields:
|
| 103 |
+
- {name: scratch, bits: 7:0, description: Scratch value}
|
| 104 |
+
|
| 105 |
+
sequence_template:
|
| 106 |
+
name: uart_sequence
|
| 107 |
+
body: |
|
| 108 |
+
// UART transmit sequence
|
| 109 |
+
repeat (num_bytes) begin
|
| 110 |
+
drv.write_reg(THR, data_q.pop_front());
|
| 111 |
+
drv.wait_for_field(LSR, thre, 1);
|
| 112 |
+
end
|
| 113 |
+
|
| 114 |
+
coverage_template:
|
| 115 |
+
- name: uart_cg
|
| 116 |
+
type: covergroup
|
| 117 |
+
items:
|
| 118 |
+
- {name: cg_baud, type: coverpoint, expression: baud_rate, bins: [9600, 19200, 115200]}
|
| 119 |
+
- {name: cg_frame, type: coverpoint, expression: {data_bits, stop_bits, parity}}
|
protocols/wishbone.yaml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Wishbone Protocol Definition — UVM TB Generator
|
| 2 |
+
protocol: wishbone
|
| 3 |
+
description: Wishbone B4 bus interface
|
| 4 |
+
tags: [bus, memory-mapped, synchronous, pipelined]
|
| 5 |
+
|
| 6 |
+
interface_template:
|
| 7 |
+
signals:
|
| 8 |
+
- {name: cyc, direction: input, width: 1, description: Cycle start}
|
| 9 |
+
- {name: stb, direction: input, width: 1, description: Strobe}
|
| 10 |
+
- {name: we, direction: input, width: 1, description: Write enable}
|
| 11 |
+
- {name: adr, direction: input, width: 32, description: Address bus}
|
| 12 |
+
- {name: dat_i,direction: input, width: 32, description: Write data}
|
| 13 |
+
- {name: dat_o,direction: output, width: 32, description: Read data}
|
| 14 |
+
- {name: ack, direction: output, width: 1, description: Acknowledge}
|
| 15 |
+
- {name: err, direction: output, width: 1, description: Error}
|
| 16 |
+
- {name: rty, direction: output, width: 1, description: Retry}
|
| 17 |
+
|
| 18 |
+
config_parameters:
|
| 19 |
+
- {name: ADDR_WIDTH, type: int, default: 32}
|
| 20 |
+
- {name: DATA_WIDTH, type: int, default: 32, enum: [8, 16, 32, 64]}
|
| 21 |
+
- {name: GRANULARITY,type: string, default: "byte", enum: [byte, word]}
|
| 22 |
+
|
| 23 |
+
register_template:
|
| 24 |
+
- name: WB_CTRL
|
| 25 |
+
address: 0x00
|
| 26 |
+
access: rw
|
| 27 |
+
fields:
|
| 28 |
+
- {name: enable, bits: 0, description: Core enable}
|
| 29 |
+
- name: WB_STATUS
|
| 30 |
+
address: 0x04
|
| 31 |
+
access: ro
|
| 32 |
+
fields:
|
| 33 |
+
- {name: ready, bits: 0, description: Core ready}
|
| 34 |
+
|
| 35 |
+
sequence_template:
|
| 36 |
+
name: wishbone_sequence
|
| 37 |
+
body: |
|
| 38 |
+
// Wishbone classic write sequence
|
| 39 |
+
adr = addr; dat_i = data; we = 1;
|
| 40 |
+
cyc = 1; stb = 1;
|
| 41 |
+
wait(ack);
|
| 42 |
+
cyc = 0; stb = 0;
|
| 43 |
+
|
| 44 |
+
coverage_template:
|
| 45 |
+
- name: wb_cg
|
| 46 |
+
type: covergroup
|
| 47 |
+
items:
|
| 48 |
+
- {name: cg_op, type: coverpoint, expression: we}
|
pyproject.toml
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=68.0", "wheel"]
|
| 3 |
+
build-backend = "setuptools.backends._legacy:_Backend"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "uvm-tb-generator"
|
| 7 |
+
version = "0.3.0"
|
| 8 |
+
description = "Industry-grade UVM Testbench Generator with protocol libraries, schema validation, and CI/CD"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
license = {text = "MIT"}
|
| 11 |
+
requires-python = ">=3.10"
|
| 12 |
+
authors = [
|
| 13 |
+
{name = "DV Automation Team"},
|
| 14 |
+
]
|
| 15 |
+
keywords = ["uvm", "testbench", "verification", "systemverilog", "eda", "automation"]
|
| 16 |
+
classifiers = [
|
| 17 |
+
"Development Status :: 4 - Beta",
|
| 18 |
+
"Intended Audience :: Developers",
|
| 19 |
+
"License :: OSI Approved :: MIT License",
|
| 20 |
+
"Programming Language :: Python :: 3",
|
| 21 |
+
"Programming Language :: Python :: 3.10",
|
| 22 |
+
"Programming Language :: Python :: 3.11",
|
| 23 |
+
"Programming Language :: Python :: 3.12",
|
| 24 |
+
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
| 25 |
+
]
|
| 26 |
+
dependencies = [
|
| 27 |
+
"pyyaml>=6.0",
|
| 28 |
+
"jinja2>=3.0",
|
| 29 |
+
"pydantic>=2.0",
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
[project.optional-dependencies]
|
| 33 |
+
dev = [
|
| 34 |
+
"pytest>=7.0",
|
| 35 |
+
"pytest-cov>=4.0",
|
| 36 |
+
"flake8>=7.0",
|
| 37 |
+
"black>=24.0",
|
| 38 |
+
"yamllint>=1.35",
|
| 39 |
+
"jsonschema>=4.0",
|
| 40 |
+
"build>=1.0",
|
| 41 |
+
]
|
| 42 |
+
mlflow = ["mlflow>=2.0"]
|
| 43 |
+
|
| 44 |
+
[project.urls]
|
| 45 |
+
Repository = "https://github.com/your-org/uvm-tb-generator"
|
| 46 |
+
Documentation = "https://github.com/your-org/uvm-tb-generator/docs"
|
| 47 |
+
|
| 48 |
+
[project.scripts]
|
| 49 |
+
uvmgen = "src.main:main"
|
| 50 |
+
|
| 51 |
+
[tool.pytest.ini_options]
|
| 52 |
+
testpaths = ["tests"]
|
| 53 |
+
addopts = "-v --tb=short"
|
| 54 |
+
|
| 55 |
+
[tool.coverage.run]
|
| 56 |
+
source = ["src"]
|
| 57 |
+
omit = ["*/tests/*"]
|
| 58 |
+
|
| 59 |
+
[tool.flake8]
|
| 60 |
+
max-line-length = 120
|
| 61 |
+
extend-ignore = ["E203", "W503"]
|
render.yaml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Render.com deployment — free tier, single service
|
| 2 |
+
# Steps:
|
| 3 |
+
# 1. Push repo to GitHub
|
| 4 |
+
# 2. Go to https://dashboard.render.com/select-repo
|
| 5 |
+
# 3. Connect this repo — Render auto-reads this file
|
| 6 |
+
# 4. Deploy (zero config)
|
| 7 |
+
|
| 8 |
+
services:
|
| 9 |
+
- type: web
|
| 10 |
+
name: uvm-tb-generator
|
| 11 |
+
runtime: python
|
| 12 |
+
region: oregon
|
| 13 |
+
plan: free
|
| 14 |
+
buildCommand: |
|
| 15 |
+
pip install -r requirements.txt
|
| 16 |
+
cd frontend && npm ci && npm run build && cd ..
|
| 17 |
+
startCommand: gunicorn src.api.server:app --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:$PORT --workers 1 --timeout 120
|
| 18 |
+
envVars:
|
| 19 |
+
- key: PYTHONUNBUFFERED
|
| 20 |
+
value: "1"
|
| 21 |
+
- key: LOG_LEVEL
|
| 22 |
+
value: info
|
| 23 |
+
- key: UVMGEN_OUTPUT_DIR
|
| 24 |
+
value: /var/data/uvmgen_output
|
| 25 |
+
healthCheckPath: /api/health
|
| 26 |
+
autoDeploy: true
|
| 27 |
+
disk:
|
| 28 |
+
name: uvmgen-data
|
| 29 |
+
mountPath: /var/data
|
| 30 |
+
sizeGB: 1
|
requirements-dev.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pytest>=7.0
|
| 2 |
+
pytest-cov>=4.0
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pyyaml>=6.0
|
| 2 |
+
jinja2>=3.0
|
| 3 |
+
pydantic>=2.0
|
| 4 |
+
fastapi>=0.115.0
|
| 5 |
+
uvicorn[standard]>=0.34.0
|
| 6 |
+
gunicorn>=23.0
|
setup.cfg
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[metadata]
|
| 2 |
+
name = uvm-tb-generator
|
| 3 |
+
version = 0.2.0
|
| 4 |
+
description = ML-style UVM Testbench Generator
|
| 5 |
+
author = DV Automation Team
|
| 6 |
+
license = MIT
|
| 7 |
+
|
| 8 |
+
[options]
|
| 9 |
+
packages = find:
|
| 10 |
+
python_requires = >=3.10
|
| 11 |
+
install_requires =
|
| 12 |
+
pyyaml>=6.0
|
| 13 |
+
jinja2>=3.0
|
| 14 |
+
pydantic>=2.0
|
| 15 |
+
|
| 16 |
+
[options.extras_require]
|
| 17 |
+
dev =
|
| 18 |
+
pytest>=7.0
|
| 19 |
+
pytest-cov>=4.0
|
| 20 |
+
mlflow =
|
| 21 |
+
mlflow>=2.0
|
| 22 |
+
|
| 23 |
+
[options.entry_points]
|
| 24 |
+
console_scripts =
|
| 25 |
+
uvmgen = src.main:main
|
src/__init__.py
ADDED
|
File without changes
|
src/api/__init__.py
ADDED
|
File without changes
|
src/api/server.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI backend for UVM TB Generator.
|
| 3 |
+
Serves both the REST API and the React frontend from a single process
|
| 4 |
+
for free-tier deployment (Render, Fly.io, PythonAnywhere).
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import logging
|
| 10 |
+
import os
|
| 11 |
+
import traceback
|
| 12 |
+
from contextlib import asynccontextmanager
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import List, Optional
|
| 15 |
+
|
| 16 |
+
from fastapi import FastAPI, HTTPException
|
| 17 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 18 |
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
| 19 |
+
from pydantic import BaseModel, Field
|
| 20 |
+
|
| 21 |
+
from src.config import ConfigLoader
|
| 22 |
+
from src.exceptions import UVMGenError
|
| 23 |
+
from src.pipeline import TBPipeline
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger("uvmgen")
|
| 26 |
+
logger.setLevel(logging.INFO)
|
| 27 |
+
|
| 28 |
+
# ── Find frontend build directory ─────────────────────────────────
|
| 29 |
+
HERE = Path(__file__).resolve().parent
|
| 30 |
+
PROJECT_ROOT = HERE.parent.parent
|
| 31 |
+
FRONTEND_BUILD = PROJECT_ROOT / "frontend" / "build"
|
| 32 |
+
|
| 33 |
+
# ── Request / Response models ─────────────────────────────────────
|
| 34 |
+
|
| 35 |
+
class PipelineRequest(BaseModel):
|
| 36 |
+
spec_yaml: str = Field(..., description="YAML or .core specification content")
|
| 37 |
+
design_name: str = Field(default="unnamed")
|
| 38 |
+
auto_train: bool = Field(default=False)
|
| 39 |
+
max_iterations: int = Field(default=5, ge=1, le=50)
|
| 40 |
+
coverage_target: float = Field(default=90.0, ge=0, le=100)
|
| 41 |
+
num_seeds: int = Field(default=3, ge=1, le=20)
|
| 42 |
+
overwrite: bool = Field(default=False)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class HealthResponse(BaseModel):
|
| 46 |
+
status: str = "ok"
|
| 47 |
+
version: str = "0.3.0"
|
| 48 |
+
api_version: str = "v1"
|
| 49 |
+
simulators: List[str] = ["stub", "icarus"]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class VersionInfo(BaseModel):
|
| 53 |
+
version: str
|
| 54 |
+
coverage: float
|
| 55 |
+
files: int
|
| 56 |
+
iteration: int
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class PipelineResponse(BaseModel):
|
| 60 |
+
design_name: str
|
| 61 |
+
status: str
|
| 62 |
+
versions: List[VersionInfo]
|
| 63 |
+
coverage_trend: list
|
| 64 |
+
coverage_gaps: list
|
| 65 |
+
artifacts: list
|
| 66 |
+
total_files: int
|
| 67 |
+
iterations: int
|
| 68 |
+
simulator: str
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ── Pipeline singleton ─────────────────────────────────────────────
|
| 72 |
+
|
| 73 |
+
pipeline_instance: Optional[TBPipeline] = None
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@asynccontextmanager
|
| 77 |
+
async def lifespan(app: FastAPI):
|
| 78 |
+
global pipeline_instance
|
| 79 |
+
logger.info("UVM TB Generator starting...")
|
| 80 |
+
pipeline_instance = TBPipeline()
|
| 81 |
+
yield
|
| 82 |
+
logger.info("UVM TB Generator shutting down...")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
app = FastAPI(title="UVM TB Generator", version="0.3.0", lifespan=lifespan)
|
| 86 |
+
|
| 87 |
+
app.add_middleware(
|
| 88 |
+
CORSMiddleware,
|
| 89 |
+
allow_origins=["*"],
|
| 90 |
+
allow_credentials=True,
|
| 91 |
+
allow_methods=["*"],
|
| 92 |
+
allow_headers=["*"],
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# ── Exception handlers ─────────────────────────────────────────────
|
| 97 |
+
|
| 98 |
+
@app.exception_handler(UVMGenError)
|
| 99 |
+
async def uvmgen_error_handler(request, exc: UVMGenError):
|
| 100 |
+
return JSONResponse(status_code=exc.status_code, content=exc.to_dict())
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@app.exception_handler(Exception)
|
| 104 |
+
async def generic_error_handler(request, exc: Exception):
|
| 105 |
+
logger.error("Unhandled: %s\n%s", exc, traceback.format_exc())
|
| 106 |
+
return JSONResponse(
|
| 107 |
+
status_code=500,
|
| 108 |
+
content={"error": "INTERNAL_ERROR", "message": str(exc)},
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# ── API Routes ─────────────────────────────────────────────────────
|
| 113 |
+
|
| 114 |
+
@app.get("/api/health", response_model=HealthResponse)
|
| 115 |
+
async def health_check():
|
| 116 |
+
return HealthResponse()
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@app.get("/api/versions")
|
| 120 |
+
async def list_versions():
|
| 121 |
+
if not pipeline_instance:
|
| 122 |
+
raise HTTPException(503, "Pipeline not initialized")
|
| 123 |
+
trend = pipeline_instance.registry.coverage_trend()
|
| 124 |
+
return {"versions": trend}
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@app.post("/api/run-pipeline", response_model=PipelineResponse)
|
| 128 |
+
async def run_pipeline(req: PipelineRequest):
|
| 129 |
+
global pipeline_instance
|
| 130 |
+
if not pipeline_instance:
|
| 131 |
+
pipeline_instance = TBPipeline()
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
import tempfile, os as _os
|
| 135 |
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, encoding="utf-8") as f:
|
| 136 |
+
f.write(req.spec_yaml)
|
| 137 |
+
spec_path = f.name
|
| 138 |
+
|
| 139 |
+
pipeline_instance.cfg.auto_train.enabled = req.auto_train
|
| 140 |
+
pipeline_instance.cfg.auto_train.max_iterations = req.max_iterations
|
| 141 |
+
pipeline_instance.cfg.auto_train.coverage_target = req.coverage_target
|
| 142 |
+
pipeline_instance.cfg.auto_train.num_seeds = req.num_seeds
|
| 143 |
+
pipeline_instance.cfg.generation.overwrite = req.overwrite
|
| 144 |
+
|
| 145 |
+
result = pipeline_instance.run(spec_path)
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
_os.unlink(spec_path)
|
| 149 |
+
except OSError:
|
| 150 |
+
pass
|
| 151 |
+
|
| 152 |
+
analysis = result.get("coverage_analysis") or {}
|
| 153 |
+
gaps = [{"bin": g["bin"], "addr": g.get("addr"), "dir": g.get("dir")}
|
| 154 |
+
for g in (analysis.get("gaps") or [])]
|
| 155 |
+
|
| 156 |
+
versions = []
|
| 157 |
+
for t in (result.get("coverage_trend") or []):
|
| 158 |
+
if isinstance(t, dict):
|
| 159 |
+
versions.append(VersionInfo(
|
| 160 |
+
version=t.get("version", "v0"),
|
| 161 |
+
coverage=float(t.get("coverage", 0)),
|
| 162 |
+
files=t.get("files", 0),
|
| 163 |
+
iteration=t.get("iteration", 0),
|
| 164 |
+
))
|
| 165 |
+
|
| 166 |
+
artifacts = [
|
| 167 |
+
{"name": Path(p).name, "path": p}
|
| 168 |
+
for p in result.get("generated_files", {}).values()
|
| 169 |
+
]
|
| 170 |
+
|
| 171 |
+
return PipelineResponse(
|
| 172 |
+
design_name=result.get("design_name", "unknown"),
|
| 173 |
+
status="passed" if result.get("passed") else "failed",
|
| 174 |
+
versions=versions,
|
| 175 |
+
coverage_trend=result.get("coverage_trend") or [],
|
| 176 |
+
coverage_gaps=gaps,
|
| 177 |
+
artifacts=artifacts,
|
| 178 |
+
total_files=len(artifacts),
|
| 179 |
+
iterations=result.get("auto_train_iterations", 0),
|
| 180 |
+
simulator=result.get("simulator", "stub"),
|
| 181 |
+
)
|
| 182 |
+
except UVMGenError:
|
| 183 |
+
raise
|
| 184 |
+
except Exception as e:
|
| 185 |
+
logger.error("Pipeline failed: %s", e)
|
| 186 |
+
raise HTTPException(500, detail=str(e))
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
@app.post("/api/validate-spec")
|
| 190 |
+
async def validate_spec(req: PipelineRequest):
|
| 191 |
+
try:
|
| 192 |
+
import tempfile, os as _os, yaml
|
| 193 |
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, encoding="utf-8") as f:
|
| 194 |
+
f.write(req.spec_yaml)
|
| 195 |
+
spec_path = f.name
|
| 196 |
+
|
| 197 |
+
loader = ConfigLoader()
|
| 198 |
+
spec, _ = loader.load(spec_path)
|
| 199 |
+
from src.data.validators import SpecValidator
|
| 200 |
+
validator = SpecValidator()
|
| 201 |
+
vr = validator.validate(spec, strict=True)
|
| 202 |
+
try:
|
| 203 |
+
_os.unlink(spec_path)
|
| 204 |
+
except OSError:
|
| 205 |
+
pass
|
| 206 |
+
return {"valid": bool(vr), "design_name": spec.design_name,
|
| 207 |
+
"errors": vr if vr else [], "registers": len(spec.registers),
|
| 208 |
+
"interfaces": len(spec.interfaces)}
|
| 209 |
+
except Exception as e:
|
| 210 |
+
raise HTTPException(422, detail=str(e))
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
# ── Serve frontend (single deploy) ─────────────────────────────────
|
| 214 |
+
|
| 215 |
+
_IS_BUILT = FRONTEND_BUILD.exists()
|
| 216 |
+
if _IS_BUILT:
|
| 217 |
+
logger.info("Serving frontend from %s", FRONTEND_BUILD)
|
| 218 |
+
|
| 219 |
+
from fastapi.responses import HTMLResponse
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
@app.get("/", include_in_schema=False)
|
| 223 |
+
@app.get("/index.html", include_in_schema=False)
|
| 224 |
+
async def serve_index():
|
| 225 |
+
if _IS_BUILT:
|
| 226 |
+
index = FRONTEND_BUILD / "index.html"
|
| 227 |
+
if index.exists():
|
| 228 |
+
return FileResponse(str(index))
|
| 229 |
+
return HTMLResponse("<h1>UVM TB Generator API</h1><p>Frontend not built. Run <code>cd frontend && npm run build</code></p>")
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
@app.get("/static/{rest_of_path:path}", include_in_schema=False)
|
| 233 |
+
async def serve_static(rest_of_path: str):
|
| 234 |
+
if not _IS_BUILT:
|
| 235 |
+
return JSONResponse(404, {"error": "Not found"})
|
| 236 |
+
file_path = FRONTEND_BUILD / "static" / rest_of_path
|
| 237 |
+
if file_path.exists() and file_path.is_file():
|
| 238 |
+
return FileResponse(str(file_path))
|
| 239 |
+
return JSONResponse(404, {"error": "Not found"})
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
@app.get("/{full_path:path}", include_in_schema=False)
|
| 243 |
+
async def serve_spa(full_path: str):
|
| 244 |
+
if full_path.startswith("api/") or full_path.startswith("docs") or full_path.startswith("openapi"):
|
| 245 |
+
return JSONResponse(status_code=404, content={"error": "Not found"})
|
| 246 |
+
if _IS_BUILT:
|
| 247 |
+
file_path = FRONTEND_BUILD / full_path
|
| 248 |
+
if file_path.exists() and file_path.is_file():
|
| 249 |
+
return FileResponse(str(file_path))
|
| 250 |
+
index = FRONTEND_BUILD / "index.html"
|
| 251 |
+
if index.exists():
|
| 252 |
+
return FileResponse(str(index))
|
| 253 |
+
return JSONResponse(404, {"error": "Not found"})
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
# ── Direct execution ───────────────────────────────────────────────
|
| 257 |
+
|
| 258 |
+
if __name__ == "__main__":
|
| 259 |
+
import uvicorn
|
| 260 |
+
uvicorn.run("src.api.server:app", host="0.0.0.0", port=8000, reload=True)
|
src/config.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/config.py — Central configuration with Pydantic validation
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Dict, List, Optional, Any
|
| 8 |
+
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
import yaml
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# ── Data Models ──────────────────────────────────────────────────────────────
|
| 14 |
+
|
| 15 |
+
class SignalDef(BaseModel):
|
| 16 |
+
name: str
|
| 17 |
+
direction: str = Field(pattern=r"^(input|output|inout)$")
|
| 18 |
+
width: Optional[int] = 1
|
| 19 |
+
|
| 20 |
+
class InterfaceDef(BaseModel):
|
| 21 |
+
name: str
|
| 22 |
+
signals: List[SignalDef] = Field(min_length=1)
|
| 23 |
+
|
| 24 |
+
class FieldDef(BaseModel):
|
| 25 |
+
name: str
|
| 26 |
+
bits: str
|
| 27 |
+
description: Optional[str] = None
|
| 28 |
+
|
| 29 |
+
class RegisterDef(BaseModel):
|
| 30 |
+
name: str
|
| 31 |
+
address: str
|
| 32 |
+
fields: List[FieldDef] = []
|
| 33 |
+
description: Optional[str] = None
|
| 34 |
+
access: Optional[str] = None
|
| 35 |
+
size: Optional[int] = None
|
| 36 |
+
reset_value: Optional[str] = None
|
| 37 |
+
volatile: bool = False
|
| 38 |
+
|
| 39 |
+
class ClockResetDef(BaseModel):
|
| 40 |
+
clock: str = "clk"
|
| 41 |
+
reset: str = "rst_n"
|
| 42 |
+
reset_active: int = Field(default=0, ge=0, le=1)
|
| 43 |
+
|
| 44 |
+
class DesignSpec(BaseModel):
|
| 45 |
+
design_name: str = Field(min_length=1, pattern=r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
| 46 |
+
clock_reset: ClockResetDef = ClockResetDef()
|
| 47 |
+
interfaces: List[InterfaceDef] = Field(min_length=1)
|
| 48 |
+
registers: List[RegisterDef] = []
|
| 49 |
+
protocol: str = Field(default="", pattern=r"^(uart|spi|i2c|axi4lite|apb|wishbone|)$")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ── Pipeline / Engine Config ─────────────────────────────────────────────────
|
| 53 |
+
|
| 54 |
+
class LoggingConfig(BaseModel):
|
| 55 |
+
level: str = Field(default="INFO", pattern=r"^(DEBUG|INFO|WARNING|ERROR)$")
|
| 56 |
+
file: Optional[str] = None
|
| 57 |
+
format: str = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
| 58 |
+
|
| 59 |
+
class EvaluationConfig(BaseModel):
|
| 60 |
+
enabled: bool = True
|
| 61 |
+
metrics: List[str] = ["completeness", "syntax_validity", "coverage_readiness"]
|
| 62 |
+
threshold: float = Field(default=0.7, ge=0.0, le=1.0)
|
| 63 |
+
|
| 64 |
+
class TrackingConfig(BaseModel):
|
| 65 |
+
enabled: bool = False
|
| 66 |
+
backend: str = Field(default="local", pattern=r"^(local|mlflow)$")
|
| 67 |
+
experiment_name: Optional[str] = None
|
| 68 |
+
tracking_uri: Optional[str] = None
|
| 69 |
+
|
| 70 |
+
class GenerationConfig(BaseModel):
|
| 71 |
+
templates_dir: str = "src/generation/templates"
|
| 72 |
+
output_dir: str = "output"
|
| 73 |
+
overwrite: bool = False
|
| 74 |
+
strict_validation: bool = True
|
| 75 |
+
iteration: int = Field(default=0, ge=0)
|
| 76 |
+
|
| 77 |
+
class AutoTrainConfig(BaseModel):
|
| 78 |
+
enabled: bool = False
|
| 79 |
+
max_iterations: int = Field(default=5, ge=1, le=50)
|
| 80 |
+
coverage_target: float = Field(default=90.0, ge=0.0, le=100.0)
|
| 81 |
+
coverage_gain_min: float = Field(default=2.0, ge=0.0, description="Min % gain per iteration to continue")
|
| 82 |
+
simulator: str = Field(default="stub", pattern=r"^(stub|icarus|vcs|questa)$")
|
| 83 |
+
sim_timeout: int = Field(default=300, ge=10)
|
| 84 |
+
num_seeds: int = Field(default=3, ge=1, le=20, description="Number of regression seeds per iteration")
|
| 85 |
+
generate_regression_test: bool = True
|
| 86 |
+
|
| 87 |
+
class PipelineConfig(BaseModel):
|
| 88 |
+
generation: GenerationConfig = GenerationConfig()
|
| 89 |
+
evaluation: EvaluationConfig = EvaluationConfig()
|
| 90 |
+
tracking: TrackingConfig = TrackingConfig()
|
| 91 |
+
logging: LoggingConfig = LoggingConfig()
|
| 92 |
+
auto_train: AutoTrainConfig = AutoTrainConfig()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# ── Config Loader ────────────────────────────────────────────────────────────
|
| 96 |
+
|
| 97 |
+
class ConfigLoader:
|
| 98 |
+
"""Hierarchical config loader with env override support.
|
| 99 |
+
|
| 100 |
+
Load order (later overrides earlier):
|
| 101 |
+
1. Base defaults
|
| 102 |
+
2. <env>.yaml (e.g. configs/production.yaml)
|
| 103 |
+
3. Environment variables (UVMGEN_* prefix)
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
ENV_PREFIX = "UVMGEN_"
|
| 107 |
+
|
| 108 |
+
def __init__(self, root: Optional[str] = None):
|
| 109 |
+
self.root = Path(root or os.getcwd())
|
| 110 |
+
|
| 111 |
+
def load(self, spec_path: str, pipeline_path: Optional[str] = None) -> tuple[DesignSpec, PipelineConfig]:
|
| 112 |
+
design_spec = self._load_design_spec(spec_path)
|
| 113 |
+
pipeline_cfg = self._load_pipeline(pipeline_path)
|
| 114 |
+
self._apply_env_overrides(pipeline_cfg)
|
| 115 |
+
return design_spec, pipeline_cfg
|
| 116 |
+
|
| 117 |
+
def _load_design_spec(self, path: str) -> DesignSpec:
|
| 118 |
+
from src.data.preprocessor import SpecPreprocessor
|
| 119 |
+
from src.data.core_parser import CoreParser
|
| 120 |
+
|
| 121 |
+
ext = Path(path).suffix.lower()
|
| 122 |
+
if ext == ".core":
|
| 123 |
+
raw = CoreParser().parse(Path(path).read_text(encoding="utf-8"))
|
| 124 |
+
else:
|
| 125 |
+
raw = self._read_yaml(path)
|
| 126 |
+
raw = SpecPreprocessor().preprocess(raw)
|
| 127 |
+
return DesignSpec(**raw)
|
| 128 |
+
|
| 129 |
+
def _load_pipeline(self, path: Optional[str] = None) -> PipelineConfig:
|
| 130 |
+
base = PipelineConfig()
|
| 131 |
+
if path and Path(path).exists():
|
| 132 |
+
overrides = self._read_yaml(path)
|
| 133 |
+
base = self._deep_merge(base, overrides)
|
| 134 |
+
return base
|
| 135 |
+
|
| 136 |
+
def _apply_env_overrides(self, cfg: PipelineConfig) -> None:
|
| 137 |
+
prefix = self.ENV_PREFIX
|
| 138 |
+
for key, val in os.environ.items():
|
| 139 |
+
if key.startswith(prefix):
|
| 140 |
+
parts = key[len(prefix):].lower().split("__")
|
| 141 |
+
target = cfg
|
| 142 |
+
for part in parts[:-1]:
|
| 143 |
+
target = getattr(target, part, None)
|
| 144 |
+
if target is None:
|
| 145 |
+
break
|
| 146 |
+
else:
|
| 147 |
+
last = parts[-1]
|
| 148 |
+
if hasattr(target, last):
|
| 149 |
+
setattr(target, last, self._coerce(val, type(getattr(target, last))))
|
| 150 |
+
|
| 151 |
+
@staticmethod
|
| 152 |
+
def _read_yaml(path: str) -> dict:
|
| 153 |
+
with open(path, "r") as f:
|
| 154 |
+
return yaml.safe_load(f)
|
| 155 |
+
|
| 156 |
+
@staticmethod
|
| 157 |
+
def _coerce(val: str, typ: type) -> Any:
|
| 158 |
+
if typ is bool:
|
| 159 |
+
return val.lower() in ("1", "true", "yes")
|
| 160 |
+
if typ is int:
|
| 161 |
+
return int(val)
|
| 162 |
+
if typ is float:
|
| 163 |
+
return float(val)
|
| 164 |
+
return val
|
| 165 |
+
|
| 166 |
+
@staticmethod
|
| 167 |
+
def _deep_merge(base: PipelineConfig, overrides: dict) -> PipelineConfig:
|
| 168 |
+
import json
|
| 169 |
+
base_dict = json.loads(base.model_dump_json())
|
| 170 |
+
for k, v in overrides.items():
|
| 171 |
+
if k in base_dict and isinstance(base_dict[k], dict) and isinstance(v, dict):
|
| 172 |
+
base_dict[k].update(v)
|
| 173 |
+
else:
|
| 174 |
+
base_dict[k] = v
|
| 175 |
+
return PipelineConfig(**base_dict)
|
src/data/__init__.py
ADDED
|
File without changes
|
src/data/collector.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/data/collector.py — Collect specs from multiple sources (YAML, JSON, DB)
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Dict, List, Optional, Any
|
| 8 |
+
|
| 9 |
+
import yaml
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class SpecCollector:
|
| 13 |
+
"""Collects raw design specifications from various sources."""
|
| 14 |
+
|
| 15 |
+
SUPPORTED_EXTENSIONS = {".yaml", ".yml", ".json"}
|
| 16 |
+
|
| 17 |
+
def __init__(self, source_paths: Optional[List[str]] = None):
|
| 18 |
+
self.source_paths = source_paths or []
|
| 19 |
+
|
| 20 |
+
def collect(self) -> List[Dict[str, Any]]:
|
| 21 |
+
specs: List[Dict[str, Any]] = []
|
| 22 |
+
for path in self.source_paths:
|
| 23 |
+
p = Path(path)
|
| 24 |
+
if p.is_file() and p.suffix in self.SUPPORTED_EXTENSIONS:
|
| 25 |
+
specs.append(self._read_file(p))
|
| 26 |
+
elif p.is_dir():
|
| 27 |
+
for f in sorted(p.glob("*.*")):
|
| 28 |
+
if f.suffix in self.SUPPORTED_EXTENSIONS:
|
| 29 |
+
specs.append(self._read_file(f))
|
| 30 |
+
return specs
|
| 31 |
+
|
| 32 |
+
def collect_from_database(self, connection_string: str, query: str) -> List[Dict[str, Any]]:
|
| 33 |
+
raise NotImplementedError("Database collector — implement for your ORM / DB backend")
|
| 34 |
+
|
| 35 |
+
@staticmethod
|
| 36 |
+
def _read_file(path: Path) -> Dict[str, Any]:
|
| 37 |
+
with open(path, "r") as f:
|
| 38 |
+
if path.suffix in (".yaml", ".yml"):
|
| 39 |
+
return yaml.safe_load(f)
|
| 40 |
+
return json.load(f)
|
src/data/core_parser.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/data/core_parser.py — FuseSoC .core file parser
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
import yaml
|
| 8 |
+
|
| 9 |
+
from src.data.preprocessor import SpecPreprocessor
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class CoreParser:
|
| 13 |
+
"""Parses FuseSoC .core files into the internal DesignSpec-compatible dict.
|
| 14 |
+
|
| 15 |
+
The .core format (YAML) describes IP cores with:
|
| 16 |
+
- name / version / description
|
| 17 |
+
- parameters
|
| 18 |
+
- interfaces (each with signals, type, bus standard)
|
| 19 |
+
- registers (address, access, fields with bit positions)
|
| 20 |
+
- clock_reset
|
| 21 |
+
- filesets, targets
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
ACCESS_MAP = {
|
| 25 |
+
"ro": "read-only",
|
| 26 |
+
"wo": "write-only",
|
| 27 |
+
"rw": "read-write",
|
| 28 |
+
"rc": "read-clear",
|
| 29 |
+
"rs": "read-set",
|
| 30 |
+
"w1c": "write-1-to-clear",
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
def parse(self, content: str) -> Dict[str, Any]:
|
| 34 |
+
raw = yaml.safe_load(content)
|
| 35 |
+
spec: Dict[str, Any] = {}
|
| 36 |
+
|
| 37 |
+
spec["design_name"] = self._extract_name(raw)
|
| 38 |
+
spec["clock_reset"] = self._extract_clock_reset(raw)
|
| 39 |
+
spec["interfaces"] = self._extract_interfaces(raw)
|
| 40 |
+
spec["registers"] = self._extract_registers(raw)
|
| 41 |
+
spec["parameters"] = self._extract_parameters(raw)
|
| 42 |
+
|
| 43 |
+
return SpecPreprocessor().preprocess(spec)
|
| 44 |
+
|
| 45 |
+
@staticmethod
|
| 46 |
+
def _extract_name(raw: Dict[str, Any]) -> str:
|
| 47 |
+
name = raw.get("name", "unknown")
|
| 48 |
+
if isinstance(name, str):
|
| 49 |
+
return name.strip().lower()
|
| 50 |
+
return "unknown"
|
| 51 |
+
|
| 52 |
+
@staticmethod
|
| 53 |
+
def _extract_clock_reset(raw: Dict[str, Any]) -> Dict[str, Any]:
|
| 54 |
+
cr = raw.get("clock_reset")
|
| 55 |
+
if cr:
|
| 56 |
+
return {
|
| 57 |
+
"clock": cr.get("clock", "clk"),
|
| 58 |
+
"reset": cr.get("reset", "rst_n"),
|
| 59 |
+
"reset_active": cr.get("reset_active", 0),
|
| 60 |
+
}
|
| 61 |
+
return {"clock": "clk", "reset": "rst_n", "reset_active": 0}
|
| 62 |
+
|
| 63 |
+
@staticmethod
|
| 64 |
+
def _extract_interfaces(raw: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 65 |
+
interfaces = raw.get("interfaces", [])
|
| 66 |
+
result: List[Dict[str, Any]] = []
|
| 67 |
+
for iface in interfaces:
|
| 68 |
+
entry: Dict[str, Any] = {
|
| 69 |
+
"name": iface.get("name", "bus"),
|
| 70 |
+
"signals": [],
|
| 71 |
+
}
|
| 72 |
+
for sig in iface.get("signals", []):
|
| 73 |
+
entry["signals"].append({
|
| 74 |
+
"name": sig.get("name", "sig"),
|
| 75 |
+
"direction": sig.get("direction", "input"),
|
| 76 |
+
"width": sig.get("width", 1),
|
| 77 |
+
})
|
| 78 |
+
result.append(entry)
|
| 79 |
+
return result
|
| 80 |
+
|
| 81 |
+
@staticmethod
|
| 82 |
+
def _extract_registers(raw: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 83 |
+
registers = raw.get("registers", [])
|
| 84 |
+
result: List[Dict[str, Any]] = []
|
| 85 |
+
seen = set()
|
| 86 |
+
for reg in registers:
|
| 87 |
+
name = reg.get("name", f"reg_{len(result)}")
|
| 88 |
+
if name in seen:
|
| 89 |
+
name = f"{name}_{len(result)}"
|
| 90 |
+
seen.add(name)
|
| 91 |
+
|
| 92 |
+
entry: Dict[str, Any] = {
|
| 93 |
+
"name": name,
|
| 94 |
+
"address": reg.get("address", "0x00"),
|
| 95 |
+
"description": reg.get("description", ""),
|
| 96 |
+
"access": reg.get("access", "rw"),
|
| 97 |
+
"fields": [],
|
| 98 |
+
}
|
| 99 |
+
for fld in reg.get("fields", []):
|
| 100 |
+
entry["fields"].append({
|
| 101 |
+
"name": fld.get("name", "field"),
|
| 102 |
+
"bits": fld.get("bits", "0"),
|
| 103 |
+
"description": fld.get("description", ""),
|
| 104 |
+
})
|
| 105 |
+
result.append(entry)
|
| 106 |
+
return result
|
| 107 |
+
|
| 108 |
+
@staticmethod
|
| 109 |
+
def _extract_parameters(raw: Dict[str, Any]) -> Dict[str, Any]:
|
| 110 |
+
params = raw.get("parameters", {})
|
| 111 |
+
return {k: v.get("default") if isinstance(v, dict) else v for k, v in params.items()}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def parse_core_file(path: str) -> Dict[str, Any]:
|
| 115 |
+
"""Convenience: read .core file and return DesignSpec-compatible dict."""
|
| 116 |
+
with open(path, "r") as f:
|
| 117 |
+
return CoreParser().parse(f.read())
|
src/data/preprocessor.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import Any, Dict, List, Optional
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
PROTOCOL_SIGNATURES = {
|
| 8 |
+
"uart": {"tx", "rx", "baud"},
|
| 9 |
+
"spi": {"mosi", "miso", "sclk", "ss_n", "cs"},
|
| 10 |
+
"i2c": {"scl", "sda"},
|
| 11 |
+
"axi4lite": {"awvalid", "awready", "arvalid", "arready",
|
| 12 |
+
"wvalid", "wready", "rvalid", "rready",
|
| 13 |
+
"bvalid", "bready"},
|
| 14 |
+
"apb": {"psel", "penable", "paddr", "pwrite", "pready"},
|
| 15 |
+
"wishbone": {"wb_cyc", "wb_stb", "wb_we", "wb_ack", "wb_addr"},
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class SpecPreprocessor:
|
| 20 |
+
def preprocess(self, raw: Dict[str, Any]) -> Dict[str, Any]:
|
| 21 |
+
raw = self._normalise_names(raw)
|
| 22 |
+
raw = self._default_clock_reset(raw)
|
| 23 |
+
raw = self._expand_signals(raw)
|
| 24 |
+
raw = self._validate_address_formats(raw)
|
| 25 |
+
raw = self._detect_protocol(raw)
|
| 26 |
+
return raw
|
| 27 |
+
|
| 28 |
+
@staticmethod
|
| 29 |
+
def _normalise_names(raw: Dict[str, Any]) -> Dict[str, Any]:
|
| 30 |
+
if "design_name" in raw:
|
| 31 |
+
raw["design_name"] = re.sub(r"[^a-zA-Z0-9_]", "_", raw["design_name"]).lower()
|
| 32 |
+
return raw
|
| 33 |
+
|
| 34 |
+
@staticmethod
|
| 35 |
+
def _default_clock_reset(raw: Dict[str, Any]) -> Dict[str, Any]:
|
| 36 |
+
raw.setdefault("clock_reset", {"clock": "clk", "reset": "rst_n", "reset_active": 0})
|
| 37 |
+
return raw
|
| 38 |
+
|
| 39 |
+
@staticmethod
|
| 40 |
+
def _expand_signals(raw: Dict[str, Any]) -> Dict[str, Any]:
|
| 41 |
+
for iface in raw.get("interfaces", []):
|
| 42 |
+
for sig in iface.get("signals", []):
|
| 43 |
+
sig.setdefault("width", 1)
|
| 44 |
+
return raw
|
| 45 |
+
|
| 46 |
+
@staticmethod
|
| 47 |
+
def _validate_address_formats(raw: Dict[str, Any]) -> Dict[str, Any]:
|
| 48 |
+
for reg in raw.get("registers", []):
|
| 49 |
+
addr = reg.get("address", "0x00")
|
| 50 |
+
if not isinstance(addr, str) or not addr.startswith("0x"):
|
| 51 |
+
reg["address"] = f"0x{int(addr):02X}" if isinstance(addr, int) else f"0x{addr}"
|
| 52 |
+
for field in reg.get("fields", []):
|
| 53 |
+
bits = field.get("bits")
|
| 54 |
+
if bits is not None and not isinstance(bits, str):
|
| 55 |
+
field["bits"] = str(bits)
|
| 56 |
+
return raw
|
| 57 |
+
|
| 58 |
+
@staticmethod
|
| 59 |
+
def _detect_protocol(raw: Dict[str, Any]) -> Dict[str, Any]:
|
| 60 |
+
signal_names = set()
|
| 61 |
+
for iface in raw.get("interfaces", []):
|
| 62 |
+
for sig in iface.get("signals", []):
|
| 63 |
+
signal_names.add(sig.get("name", "").lower())
|
| 64 |
+
|
| 65 |
+
if raw.get("protocol"):
|
| 66 |
+
return raw
|
| 67 |
+
|
| 68 |
+
detected = None
|
| 69 |
+
rank = 0
|
| 70 |
+
for proto, sigs in PROTOCOL_SIGNATURES.items():
|
| 71 |
+
matches = sum(1 for kw in sigs if any(kw in s for s in signal_names))
|
| 72 |
+
if matches > rank:
|
| 73 |
+
rank = matches
|
| 74 |
+
detected = proto
|
| 75 |
+
|
| 76 |
+
if detected and rank >= 2:
|
| 77 |
+
raw["protocol"] = detected
|
| 78 |
+
elif not raw.get("protocol"):
|
| 79 |
+
raw["protocol"] = "wishbone"
|
| 80 |
+
|
| 81 |
+
return raw
|