Python API

The Python API provides programmatic access to Axion-HDL, allowing integration with custom workflows, build systems, and other tools.

Installation

pip install axion-hdl

Quick Start

from axion_hdl import AxionHDL

# Create instance
axion = AxionHDL()

# Add source files
axion.add_source("controller.yaml")
axion.add_source("./rtl")

# Analyze all sources
modules = axion.analyze()

# Generate outputs
axion.set_output_dir("./output")
axion.generate_all()

Core Class: AxionHDL

Creating an Instance

from axion_hdl import AxionHDL

# Basic usage
axion = AxionHDL()

# With initial source
axion = AxionHDL()
axion.add_source("registers.yaml")

Adding Sources

# Add single file (auto-detects type by extension)
axion.add_source("registers.yaml")
axion.add_source("controller.xml")
axion.add_source("gpio.json")
axion.add_source("spi.toml")
axion.add_source("spi_master.vhd")
axion.add_source("design.sv")

# Add directory (recursive scan)
axion.add_source("./rtl")

# Add specific format
axion.add_yaml_src("registers.yaml")
axion.add_xml_src("controller.xml")
axion.add_json_src("gpio.json")
axion.add_toml_src("spi.toml")
axion.add_sv_src("design.sv")      # SystemVerilog source file
axion.add_sv_src("./rtl/sv")       # Directory of .sv/.svh files

# List current sources
sources = axion.list_src()
print(f"Loaded sources: {sources}")

Excluding Files

# Exclude by pattern
axion.exclude("*_tb.vhd")
axion.exclude("test_*")
axion.exclude("deprecated")

# List excludes
excludes = axion.list_excludes()

# Clear all excludes
axion.clear_excludes()

Analysis

# Analyze all sources
modules = axion.analyze()

# Check if analyzed
if axion.is_analyzed:
    print(f"Found {len(modules)} modules")

# Get parsed modules
modules = axion.get_modules()

for module in modules:
    print(f"Module: {module['name']}")
    print(f"  Source: {module['source_file']}")
    print(f"  Base Address: {module['base_addr']}")
    print(f"  Registers: {len(module['registers'])}")
    
    for reg in module['registers']:
        print(f"    - {reg['name']}: {reg['access']} @ {reg['addr']}")

Generation

# Set output directory
axion.set_output_dir("./output")

# Generate all outputs (default: HTML documentation)
axion.generate_all()

# Generate all outputs with specific documentation format
axion.generate_all(doc_format="md")   # Use Markdown instead of HTML
axion.generate_all(doc_format="pdf")  # Use PDF (requires weasyprint)

# Generate specific formats only
axion.generate_vhdl()
axion.generate_systemverilog()
axion.generate_c_header()
axion.generate_documentation(format="html")  # HTML (default)
axion.generate_documentation(format="md")    # Markdown
axion.generate_documentation(format="pdf")   # PDF
axion.generate_xml()
axion.generate_yaml()
axion.generate_json()

Documentation Output Formats:

Format

Output Files

Notes

HTML (default)

index.html + html/*.html

Multi-page interactive docs

Markdown

register_map.md

Single file, GitHub-compatible

PDF

register_map.pdf

Requires weasyprint package


Complete Examples

Example 1: Basic Generation

from axion_hdl import AxionHDL

def generate_from_yaml():
    axion = AxionHDL()
    
    # Add source
    axion.add_source("registers.yaml")
    
    # Analyze
    modules = axion.analyze()
    print(f"Found {len(modules)} modules")
    
    # Generate all outputs
    axion.set_output_dir("./generated")
    axion.generate_all()
    
    print("Generation complete!")

if __name__ == "__main__":
    generate_from_yaml()

Example 2: Multi-Source Project

from axion_hdl import AxionHDL

def generate_project():
    axion = AxionHDL()
    
    # Add multiple sources
    axion.add_source("./rtl")          # VHDL files
    axion.add_source("extra_regs.yaml") # Additional YAML
    axion.add_source("config.json")     # JSON config
    
    # Exclude testbenches
    axion.exclude("*_tb.vhd")
    axion.exclude("*_test.vhd")
    
    # Analyze
    modules = axion.analyze()
    
    # Print summary
    for m in modules:
        print(f"  {m['name']}: {len(m['registers'])} registers")
    
    # Generate
    axion.set_output_dir("./output")
    axion.generate_all()

if __name__ == "__main__":
    generate_project()

Example 3: Custom Workflow with Validation

from axion_hdl import AxionHDL
from axion_hdl.rule_checker import RuleChecker

def validated_generation():
    axion = AxionHDL()
    
    # Add sources
    axion.add_source("./rtl")
    
    # Analyze
    modules = axion.analyze()
    
    # Run validation
    checker = RuleChecker()
    results = checker.run_all_checks(modules)
    
    # Check for errors
    if results["errors"]:
        print("❌ Validation failed!")
        for error in results["errors"]:
            print(f"  ERROR: {error['message']}")
        return False
    
    # Show warnings
    if results["warnings"]:
        print("⚠️ Warnings:")
        for warning in results["warnings"]:
            print(f"  WARNING: {warning['message']}")
    
    # Generate only if valid
    print("✓ Validation passed, generating...")
    axion.set_output_dir("./output")
    axion.generate_all()
    
    return True

if __name__ == "__main__":
    success = validated_generation()
    exit(0 if success else 1)

Example 4: Address Overlap Detection

from axion_hdl import AxionHDL

def check_overlaps():
    axion = AxionHDL()
    
    # Add all modules
    axion.add_source("./rtl")
    axion.analyze()
    
    # Check for address overlaps between modules
    overlaps = axion.check_address_overlaps()
    
    if overlaps:
        print("⚠️ Address overlaps detected:")
        for overlap in overlaps:
            print(f"  {overlap['module1']} ({overlap['range1']}) "
                  f"overlaps with {overlap['module2']} ({overlap['range2']})")
    else:
        print("✓ No address overlaps")

if __name__ == "__main__":
    check_overlaps()

Example 5: Selective Generation

from axion_hdl import AxionHDL

def generate_specific_outputs():
    axion = AxionHDL()
    
    axion.add_source("registers.yaml")
    axion.analyze()
    axion.set_output_dir("./output")
    
    # Generate only VHDL and C header
    axion.generate_vhdl()
    axion.generate_c_header()
    
    print("Generated VHDL and C header only")

if __name__ == "__main__":
    generate_specific_outputs()

Example 6: Build System Integration

#!/usr/bin/env python3
"""
build_regs.py - Integrate with build system
"""

import sys
import os
from axion_hdl import AxionHDL

def build_registers(source_dir: str, output_dir: str) -> bool:
    """Generate register files for build."""
    axion = AxionHDL()
    
    # Add source directory
    axion.add_source(source_dir)
    axion.exclude("*_tb.vhd")
    
    # Analyze
    try:
        modules = axion.analyze()
    except Exception as e:
        print(f"Analysis error: {e}", file=sys.stderr)
        return False
    
    if not modules:
        print("No modules found!", file=sys.stderr)
        return False
    
    # Create output directory
    os.makedirs(output_dir, exist_ok=True)
    
    # Generate
    axion.set_output_dir(output_dir)
    axion.generate_all()
    
    # Report
    print(f"Generated files for {len(modules)} modules:")
    for m in modules:
        print(f"  - {m['name']}_axion_reg.vhd")
        print(f"  - {m['name']}_regs.h")
    
    return True

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print(f"Usage: {sys.argv[0]} <source_dir> <output_dir>")
        sys.exit(1)
    
    success = build_registers(sys.argv[1], sys.argv[2])
    sys.exit(0 if success else 1)

RuleChecker API

The RuleChecker validates register definitions before generation.

from axion_hdl.rule_checker import RuleChecker
from axion_hdl import AxionHDL

# Setup
axion = AxionHDL()
axion.add_source("registers.yaml")
modules = axion.analyze()

# Create checker
checker = RuleChecker()

# Run all checks
results = checker.run_all_checks(modules)

# Access results
print(f"Errors: {len(results['errors'])}")
print(f"Warnings: {len(results['warnings'])}")

# Get detailed report
report = checker.generate_report()
print(report)

# Individual check result
for error in results["errors"]:
    print(f"[{error['rule']}] {error['message']}")
    print(f"  Module: {error['module']}")
    if 'register' in error:
        print(f"  Register: {error['register']}")

Check Categories

Category

Checks

Address

Overlap, alignment, conflicts

Naming

Duplicates, conventions

Values

Default value validity

Structure

Missing fields, invalid types


Data Structures

Module Structure

module = {
    "name": "spi_master",
    "base_addr": "0x1000",
    "source_file": "spi_master.yaml",
    "config": {
        "cdc_en": True,
        "cdc_stage": 2
    },
    "registers": [
        # List of register dicts
    ]
}

Register Structure

register = {
    "name": "control",
    "addr": "0x00",
    "access": "RW",          # RO, WO, RW
    "width": 32,
    "default": 0,
    "description": "Control register",
    "r_strobe": False,
    "w_strobe": True,
    "fields": []             # Optional subregisters
}

Error Handling

from axion_hdl import AxionHDL

axion = AxionHDL()

try:
    axion.add_source("nonexistent.yaml")
except FileNotFoundError:
    print("Source file not found")

try:
    axion.add_source("invalid.yaml")
    axion.analyze()
except ValueError as e:
    print(f"Parse error: {e}")

try:
    axion.generate_all()
except RuntimeError:
    print("Must analyze before generating")

CI/CD Integration

Use the Python API for advanced CI/CD pipelines with validation, conditional generation, and reporting.

GitHub Actions with Python

Create .github/workflows/validate-regs.yml:

name: Validate & Generate Registers

on:
  push:
    paths:
      - 'regs/**'
  pull_request:
    paths:
      - 'regs/**'

jobs:
  validate:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      
      - name: Install Dependencies
        run: pip install axion-hdl
      
      - name: Validate Register Definitions
        run: |
          python3 << 'EOF'
          from axion_hdl import AxionHDL
          from axion_hdl.rule_checker import RuleChecker
          import sys
          
          axion = AxionHDL()
          axion.add_source("./regs")
          axion.exclude("*_tb.vhd")
          
          try:
              modules = axion.analyze()
          except Exception as e:
              print(f"❌ Analysis failed: {e}")
              sys.exit(1)
          
          print(f"📦 Found {len(modules)} modules")
          
          checker = RuleChecker()
          results = checker.run_all_checks(modules)
          
          # Report warnings
          for w in results["warnings"]:
              print(f"⚠️  {w['message']}")
          
          # Report errors
          for e in results["errors"]:
              print(f"❌ {e['message']}")
          
          if results["errors"]:
              print(f"\n❌ Validation failed with {len(results['errors'])} errors")
              sys.exit(1)
          else:
              print(f"\n✅ Validation passed!")
          EOF
      
      - name: Generate Files
        if: success()
        run: |
          python3 << 'EOF'
          from axion_hdl import AxionHDL
          
          axion = AxionHDL()
          axion.add_source("./regs")
          axion.exclude("*_tb.vhd")
          axion.analyze()
          axion.set_output_dir("./generated")
          axion.generate_all()
          print("✅ Generation complete")
          EOF
      
      - name: Upload Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: generated-registers
          path: generated/

GitLab CI with Python

stages:
  - validate
  - generate

variables:
  REGS_DIR: "./regs"
  OUTPUT_DIR: "./generated"

validate-registers:
  stage: validate
  image: python:3.11
  script:
    - pip install axion-hdl
    - python3 scripts/validate_regs.py
  only:
    - merge_requests
    - main

generate-registers:
  stage: generate
  image: python:3.11
  script:
    - pip install axion-hdl
    - python3 scripts/generate_regs.py
  artifacts:
    paths:
      - generated/
    expire_in: 1 week
  only:
    - main

Create scripts/validate_regs.py:

#!/usr/bin/env python3
"""Validate register definitions for CI."""

import sys
from axion_hdl import AxionHDL
from axion_hdl.rule_checker import RuleChecker

def main():
    axion = AxionHDL()
    axion.add_source("./regs")
    axion.exclude("*_tb.vhd")
    
    print("🔍 Analyzing register definitions...")
    modules = axion.analyze()
    
    print(f"📦 Found {len(modules)} modules:")
    for m in modules:
        print(f"   - {m['name']}: {len(m['registers'])} registers")
    
    print("\n🔎 Running validation checks...")
    checker = RuleChecker()
    results = checker.run_all_checks(modules)
    
    # Print results
    for w in results["warnings"]:
        print(f"⚠️  WARNING: {w['message']}")
    
    for e in results["errors"]:
        print(f"❌ ERROR: {e['message']}")
    
    # Summary
    print(f"\n📊 Summary:")
    print(f"   Modules:  {len(modules)}")
    print(f"   Warnings: {len(results['warnings'])}")
    print(f"   Errors:   {len(results['errors'])}")
    
    if results["errors"]:
        print("\n❌ Validation FAILED")
        return 1
    else:
        print("\n✅ Validation PASSED")
        return 0

if __name__ == "__main__":
    sys.exit(main())

Pytest Integration

Create tests/test_registers.py:

"""Test register definitions with pytest."""

import pytest
from axion_hdl import AxionHDL
from axion_hdl.rule_checker import RuleChecker

@pytest.fixture
def axion():
    """Create AxionHDL instance with sources."""
    a = AxionHDL()
    a.add_source("./regs")
    a.exclude("*_tb.vhd")
    return a

@pytest.fixture
def modules(axion):
    """Analyze and return modules."""
    return axion.analyze()

def test_modules_found(modules):
    """Verify at least one module is defined."""
    assert len(modules) > 0, "No modules found in ./regs"

def test_no_validation_errors(modules):
    """Verify no validation errors exist."""
    checker = RuleChecker()
    results = checker.run_all_checks(modules)
    
    error_messages = [e["message"] for e in results["errors"]]
    assert len(results["errors"]) == 0, f"Validation errors: {error_messages}"

def test_no_address_overlaps(axion, modules):
    """Verify no address overlaps between modules."""
    overlaps = axion.check_address_overlaps()
    assert len(overlaps) == 0, f"Address overlaps detected: {overlaps}"

def test_all_modules_have_registers(modules):
    """Verify each module has at least one register."""
    for m in modules:
        assert len(m["registers"]) > 0, f"Module {m['name']} has no registers"

def test_all_registers_have_descriptions(modules):
    """Verify all registers have descriptions."""
    missing = []
    for m in modules:
        for r in m["registers"]:
            if not r.get("description"):
                missing.append(f"{m['name']}.{r['name']}")
    
    assert len(missing) == 0, f"Registers missing descriptions: {missing}"

Run with:

pytest tests/test_registers.py -v

Pre-commit Hook

Create .pre-commit-config.yaml:

repos:
  - repo: local
    hooks:
      - id: validate-registers
        name: Validate Register Definitions
        entry: python scripts/validate_regs.py
        language: python
        additional_dependencies: ['axion-hdl']
        files: ^regs/.*\.(yaml|yml|json|xml|vhd)$
        pass_filenames: false

Install and run:

pip install pre-commit
pre-commit install
pre-commit run --all-files

Custom Build Script

Create scripts/build_all.py:

#!/usr/bin/env python3
"""
Complete build script with validation, generation, and reporting.
"""

import sys
import os
import json
from datetime import datetime
from axion_hdl import AxionHDL
from axion_hdl.rule_checker import RuleChecker

def main():
    # Configuration
    source_dir = os.environ.get("REGS_DIR", "./regs")
    output_dir = os.environ.get("OUTPUT_DIR", "./generated")
    
    print("=" * 60)
    print("Axion-HDL Build Script")
    print(f"Time: {datetime.now().isoformat()}")
    print("=" * 60)
    
    # Initialize
    axion = AxionHDL()
    axion.add_source(source_dir)
    axion.exclude("*_tb.vhd")
    axion.exclude("deprecated")
    
    # Analyze
    print(f"\n📂 Source: {source_dir}")
    try:
        modules = axion.analyze()
    except Exception as e:
        print(f"❌ Analysis failed: {e}")
        return 1
    
    print(f"📦 Modules: {len(modules)}")
    total_regs = sum(len(m["registers"]) for m in modules)
    print(f"📝 Registers: {total_regs}")
    
    # Validate
    print("\n🔎 Validating...")
    checker = RuleChecker()
    results = checker.run_all_checks(modules)
    
    if results["errors"]:
        print(f"❌ {len(results['errors'])} errors found:")
        for e in results["errors"]:
            print(f"   - {e['message']}")
        return 1
    
    if results["warnings"]:
        print(f"⚠️  {len(results['warnings'])} warnings:")
        for w in results["warnings"]:
            print(f"   - {w['message']}")
    
    print("✅ Validation passed")
    
    # Generate
    print(f"\n📁 Output: {output_dir}")
    os.makedirs(output_dir, exist_ok=True)
    axion.set_output_dir(output_dir)
    axion.generate_all()
    
    # Report
    print("\n📋 Generated files:")
    for m in modules:
        print(f"   - {m['name']}_axion_reg.vhd")
        print(f"   - {m['name']}_regs.h")
    
    # Write build info
    build_info = {
        "timestamp": datetime.now().isoformat(),
        "modules": len(modules),
        "registers": total_regs,
        "warnings": len(results["warnings"]),
        "source": source_dir
    }
    
    with open(f"{output_dir}/build_info.json", "w") as f:
        json.dump(build_info, f, indent=2)
    
    print("\n" + "=" * 60)
    print("✅ Build completed successfully!")
    print("=" * 60)
    
    return 0

if __name__ == "__main__":
    sys.exit(main())

Example 7: SystemVerilog Project

from axion_hdl import AxionHDL

axion = AxionHDL(output_dir="./output")

# Add SystemVerilog source files
axion.add_sv_src("./rtl/sensor_ctrl.sv")
axion.add_sv_src("./rtl/dma_engine.sv")

# Analyze
modules = axion.analyze()
print(f"Found {len(modules)} modules")

# Generate SystemVerilog register modules + C headers
axion.generate_systemverilog()
axion.generate_c_header()
axion.generate_documentation()

You can also mix VHDL and SystemVerilog sources in a single run:

axion.add_src("./rtl/legacy_block.vhd")   # VHDL source
axion.add_sv_src("./rtl/new_block.sv")    # SystemVerilog source
axion.add_yaml_src("./regs/shared.yaml")  # YAML definitions

axion.analyze()
axion.generate_vhdl()            # VHDL output for legacy_block
axion.generate_systemverilog()   # SV output for new_block
axion.generate_c_header()        # C headers for all modules

Python Register Model API

axion-hdl provides a Python simulation model of each register space for use in golden models. The model enforces hardware access semantics (RO/WO/RW), supports bit-field masking, enum lookups, and strobe callbacks.

Getting a model from AxionHDL

from axion_hdl import AxionHDL

axion = AxionHDL(output_dir="./output")
axion.add_source("my_module.yaml")
axion.analyze()

# Get a single module
space = axion.get_model("my_module")

# Get all modules
models = axion.get_models()   # Dict[str, RegisterSpaceModel]

AxionHDL model methods

Method

Returns

Description

get_model(module_name)

RegisterSpaceModel

Return model for one module. Raises RuntimeError before analyze(), KeyError if not found.

get_models()

Dict[str, RegisterSpaceModel]

Return models for all analyzed modules.

generate_python(output_dir=None)

List[str] | False

Generate *_regs.py files for all modules. Returns list of created paths, or False on error.

RegisterSpaceModel

from axion_hdl import RegisterSpaceModel

space = RegisterSpaceModel.from_module_dict(module_dict)

space.name            # str — module name
space.base_address    # int — base address
space.registers       # Dict[str, RegisterModel]

# Bus-level access (absolute addresses)
space.read(0x1000)           # int
space.write(0x1000, 0x42)    # None; raises ReadOnlyError for RO

# Named access
space.control            # RegisterModel (via __getattr__)
space.control.value      # int

# Strobe callbacks
space.on_write("control", lambda name, val: ...)
space.on_read("irq_status", lambda name, val: ...)

# Utilities
space.reset()            # Restore all registers to defaults
space.dump()             # str — human-readable dump of all registers
space.get_register("control")           # RegisterModel
space.get_register_at(0x1000)           # RegisterModel

# Iteration in address order
for reg in space:
    print(reg.name, hex(reg.address))

RegisterModel

reg = space.get_register("status")

reg.name             # str
reg.address          # int — absolute address
reg.relative_address # int — offset from base
reg.access_mode      # str — 'RO', 'RW', or 'WO'
reg.width            # int — bit width (usually 32)
reg.default_value    # int
reg.is_packed        # bool — True if has bit fields
reg.fields           # Dict[str, FieldModel]

reg.value            # int — WO returns 0 (bus semantics)
reg.raw_value        # int — always actual value (golden model inspection)
reg.read()           # int — fires read_strobe callback if applicable
reg.write(0x42)      # None — fires write_strobe callback if applicable; raises ReadOnlyError for RO
reg.reset()          # Restore to default_value
reg.dump()           # str

# Attribute access to fields (packed registers only)
reg.ready            # FieldModel  (same as reg.fields['ready'])

FieldModel

field = space.status.fields["ready"]

field.name           # str
field.bit_low        # int
field.bit_high       # int
field.width          # int
field.access_mode    # str
field.mask           # int — pre-computed bit mask
field.default_value  # int

field.value          # int — extracted from parent raw_value
field.value = 1      # Write (raises ReadOnlyError if RO)
field.raw_value      # int — always actual bits (WO inspection)
field.enum_name      # Optional[str] — current value as enum string
field.reset()        # Restore to default_value without RO check

Exceptions

from axion_hdl import ReadOnlyError, AddressError

# ReadOnlyError — write attempted on RO register or field
try:
    space.write(0x1008, 0xFF)   # status is RO
except ReadOnlyError as e:
    print(e)   # "Register 'status' is read-only"

# AddressError — bus access at unregistered address
try:
    space.read(0xDEAD)
except AddressError as e:
    print(e)   # "No register at address 0xDEAD in space 'my_module'"

Generating *_regs.py files

Use --python on the CLI or generate_python() in code to produce stand-alone model files:

axion-hdl -s my_module.yaml -o ./output --python
# Produces: ./output/my_module_regs.py
# In your golden model (axion_hdl must be installed):
from my_module_regs import MY_MODULE

base = MY_MODULE.base_address
MY_MODULE.write(base + 0x0000, 0x1)
print(MY_MODULE.dump())

See Also