181 lines
5.8 KiB
Python
181 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate MCU database structure against the expected schema.
|
|
|
|
Usage:
|
|
python validate_mcu_db.py path/to/mcu_db.py
|
|
|
|
Checks:
|
|
- All required keys present at each level
|
|
- Pin numbers unique within each package
|
|
- Every package has at least one power and one ground pin
|
|
- Capability counts are non-negative integers
|
|
- Voltage profiles within valid ranges
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
REQUIRED_PACKAGE_KEYS = {"description", "pin_count", "pinmap", "capabilities"}
|
|
REQUIRED_CAPABILITY_KEYS = {"uart_count", "i2c_count", "spi_count"}
|
|
VALID_ROLES = {"power", "ground", "reset", "boot", "gpio", "analog", "nc"}
|
|
VOLTAGE_MIN = 0.5
|
|
VOLTAGE_MAX = 5.5
|
|
|
|
|
|
def load_mcu_db(path: Path) -> dict[str, Any]:
|
|
"""Dynamically import mcu_db.py and return MCU_DB dict."""
|
|
spec = importlib.util.spec_from_file_location("mcu_db", path)
|
|
if spec is None or spec.loader is None:
|
|
raise FileNotFoundError(f"Cannot load module from {path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module) # type: ignore[union-attr]
|
|
|
|
db = getattr(module, "MCU_DB", None)
|
|
if db is None:
|
|
raise ValueError(f"MCU_DB not found in {path}")
|
|
return db
|
|
|
|
|
|
def validate_voltage_profile(profile: dict, path_prefix: str, errors: list[str]) -> None:
|
|
for field in ("id", "vcore", "vio"):
|
|
if field not in profile:
|
|
errors.append(f"{path_prefix}: missing required field '{field}'")
|
|
|
|
for vfield in ("vcore", "vio"):
|
|
v = profile.get(vfield)
|
|
if isinstance(v, (int, float)) and not (VOLTAGE_MIN <= v <= VOLTAGE_MAX):
|
|
errors.append(f"{path_prefix}.{vfield}={v} outside valid range [{VOLTAGE_MIN}, {VOLTAGE_MAX}]")
|
|
|
|
|
|
def validate_pinmap(pinmap: dict, path_prefix: str, errors: list[str], warnings: list[str]) -> None:
|
|
has_power = False
|
|
has_ground = False
|
|
seen_pins: set[Any] = set()
|
|
|
|
for pin_num, pin_def in pinmap.items():
|
|
if pin_num in seen_pins:
|
|
errors.append(f"{path_prefix}: duplicate pin number {pin_num}")
|
|
seen_pins.add(pin_num)
|
|
|
|
if "signal" not in pin_def:
|
|
errors.append(f"{path_prefix}.pin[{pin_num}]: missing 'signal'")
|
|
|
|
roles = pin_def.get("roles", [])
|
|
if not roles:
|
|
warnings.append(f"{path_prefix}.pin[{pin_num}] ({pin_def.get('signal', '?')}): no roles defined")
|
|
|
|
for role in roles:
|
|
if role not in VALID_ROLES:
|
|
warnings.append(f"{path_prefix}.pin[{pin_num}]: unknown role '{role}'")
|
|
|
|
if "power" in roles:
|
|
has_power = True
|
|
if "ground" in roles:
|
|
has_ground = True
|
|
|
|
if not has_power:
|
|
errors.append(f"{path_prefix}: no pin with role 'power' found")
|
|
if not has_ground:
|
|
errors.append(f"{path_prefix}: no pin with role 'ground' found")
|
|
|
|
|
|
def validate_capabilities(caps: dict, path_prefix: str, errors: list[str]) -> None:
|
|
for key in REQUIRED_CAPABILITY_KEYS:
|
|
if key not in caps:
|
|
errors.append(f"{path_prefix}: missing required capability '{key}'")
|
|
|
|
for key, value in caps.items():
|
|
if key.endswith("_count") or key.endswith("_channels"):
|
|
if not isinstance(value, int) or value < 0:
|
|
errors.append(f"{path_prefix}.{key}={value!r}: must be non-negative int")
|
|
|
|
|
|
def validate_db(db: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
errors: list[str] = []
|
|
warnings: list[str] = []
|
|
|
|
if not isinstance(db, dict):
|
|
errors.append("MCU_DB must be a dict")
|
|
return errors, warnings
|
|
|
|
for vendor, families in db.items():
|
|
if not isinstance(families, dict):
|
|
errors.append(f"MCU_DB['{vendor}'] must be a dict of families")
|
|
continue
|
|
|
|
for family_id, family in families.items():
|
|
fpath = f"{vendor}/{family_id}"
|
|
|
|
if "description" not in family:
|
|
warnings.append(f"{fpath}: missing 'description'")
|
|
|
|
for vp in family.get("voltage_profiles", []):
|
|
validate_voltage_profile(vp, f"{fpath}/voltage_profiles", errors)
|
|
|
|
packages = family.get("packages", {})
|
|
if not packages:
|
|
errors.append(f"{fpath}: no packages defined")
|
|
|
|
for pkg_id, pkg in packages.items():
|
|
ppath = f"{fpath}/{pkg_id}"
|
|
|
|
missing_keys = REQUIRED_PACKAGE_KEYS - set(pkg.keys())
|
|
if missing_keys:
|
|
errors.append(f"{ppath}: missing keys {missing_keys}")
|
|
|
|
pinmap = pkg.get("pinmap", {})
|
|
if not pinmap:
|
|
warnings.append(f"{ppath}: pinmap is empty (needs datasheet data)")
|
|
else:
|
|
validate_pinmap(pinmap, ppath, errors, warnings)
|
|
|
|
caps = pkg.get("capabilities", {})
|
|
validate_capabilities(caps, ppath, errors)
|
|
|
|
return errors, warnings
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) < 2:
|
|
print(f"Usage: {sys.argv[0]} <path/to/mcu_db.py>")
|
|
return 1
|
|
|
|
path = Path(sys.argv[1])
|
|
if not path.exists():
|
|
print(f"File not found: {path}")
|
|
return 1
|
|
|
|
try:
|
|
db = load_mcu_db(path)
|
|
except Exception as exc:
|
|
print(f"Failed to load MCU_DB: {exc}")
|
|
return 1
|
|
|
|
errors, warnings = validate_db(db)
|
|
|
|
for w in warnings:
|
|
print(f" WARN: {w}")
|
|
for e in errors:
|
|
print(f" ERROR: {e}")
|
|
|
|
total_vendors = len(db)
|
|
total_families = sum(len(v) for v in db.values())
|
|
total_packages = sum(
|
|
len(f.get("packages", {}))
|
|
for v in db.values()
|
|
for f in v.values()
|
|
)
|
|
|
|
print(f"\nSummary: {total_vendors} vendors, {total_families} families, {total_packages} packages")
|
|
print(f" {len(errors)} errors, {len(warnings)} warnings")
|
|
|
|
return 1 if errors else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|