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) |
|
Multi-page interactive docs |
Markdown |
|
Single file, GitHub-compatible |
|
Requires |
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 |
|---|---|---|
|
|
Return model for one module. Raises |
|
|
Return models for all analyzed modules. |
|
|
Generate |
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
CLI Usage - Command-line interface
Interactive GUI - Web-based interface
API Reference - Full API documentation