Browse Source
Add XSPICE code model build system and pre-built .cm plugins
Add XSPICE code model build system and pre-built .cm plugins
- Add build_cmpp.py: builds cmpp preprocessor, preprocesses .ifs/.mod files, compiles .cm DLL plugins for runtime loading via codemodel command - Ship 7 pre-built .cm plugins in pyngspice/codemodels/: analog (23 models), digital (32 models incl. d_cosim for Verilog co-simulation), spice2poly, table, tlines (5 transmission line models), xtradev (13 models incl. magnetic core, zener), xtraevt (4 models + UDNs) - Add XSPICE_CODE_MODELS.md developer guide for writing custom components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>pre-master-46
9 changed files with 435 additions and 0 deletions
-
193XSPICE_CODE_MODELS.md
-
242build_cmpp.py
-
BINpyngspice/codemodels/analog.cm
-
BINpyngspice/codemodels/digital.cm
-
BINpyngspice/codemodels/spice2poly.cm
-
BINpyngspice/codemodels/table.cm
-
BINpyngspice/codemodels/tlines.cm
-
BINpyngspice/codemodels/xtradev.cm
-
BINpyngspice/codemodels/xtraevt.cm
@ -0,0 +1,193 @@ |
|||||
|
# Writing XSPICE Code Models for pyTesla |
||||
|
|
||||
|
## What This Is |
||||
|
|
||||
|
XSPICE code models let you write circuit components in C. During simulation, |
||||
|
ngspice calls your C function at each timestep with port voltages/states and |
||||
|
you compute the outputs. This is how we'll add custom parts (gate drivers, |
||||
|
controllers, digital logic) to the pyTesla part library. |
||||
|
|
||||
|
## How a Code Model Looks in a Netlist |
||||
|
|
||||
|
```spice |
||||
|
* Analog component using a code model |
||||
|
A1 in out mymodel |
||||
|
.model mymodel custom_model(gain=2.0 offset=0.1) |
||||
|
|
||||
|
* Digital component with bridge to analog world |
||||
|
A2 [digital_in] [digital_out] my_gate |
||||
|
.model my_gate d_and(rise_delay=1n fall_delay=1n) |
||||
|
|
||||
|
* Analog-to-digital bridge (connects analog node to digital code model) |
||||
|
A3 [analog_node] [digital_node] adc1 |
||||
|
.model adc1 adc_bridge(in_low=0.8 in_high=2.0) |
||||
|
``` |
||||
|
|
||||
|
The `A` device letter tells ngspice this is an XSPICE code model instance. |
||||
|
|
||||
|
## File Structure for a New Code Model |
||||
|
|
||||
|
Each code model needs two files in `src/xspice/icm/<category>/<model_name>/`: |
||||
|
|
||||
|
### 1. `ifspec.ifs` — Interface Specification |
||||
|
|
||||
|
Declares the model's name, ports, and parameters. Not C — it's a simple |
||||
|
declarative format that the `cmpp` preprocessor converts to C. |
||||
|
|
||||
|
``` |
||||
|
NAME_TABLE: |
||||
|
C_Function_Name: cm_my_driver |
||||
|
Spice_Model_Name: my_driver |
||||
|
Description: "Half-bridge gate driver model" |
||||
|
|
||||
|
PORT_TABLE: |
||||
|
Port_Name: hin lin ho lo |
||||
|
Description: "high in" "low in" "high out" "low out" |
||||
|
Direction: in in out out |
||||
|
Default_Type: d d v v |
||||
|
Allowed_Types: [d] [d] [v] [v] |
||||
|
Vector: no no no no |
||||
|
Vector_Bounds: - - - - |
||||
|
Null_Allowed: no no no no |
||||
|
|
||||
|
PARAMETER_TABLE: |
||||
|
Parameter_Name: vcc dead_time |
||||
|
Description: "supply volts" "dead time seconds" |
||||
|
Data_Type: real real |
||||
|
Default_Value: 12.0 100e-9 |
||||
|
Limits: [0.1 1000] [0 -] |
||||
|
Vector: no no |
||||
|
Vector_Bounds: - - |
||||
|
Null_Allowed: yes yes |
||||
|
``` |
||||
|
|
||||
|
### 2. `cfunc.mod` — C Implementation |
||||
|
|
||||
|
Plain C with XSPICE macros for accessing ports/parameters. This is where |
||||
|
your component's behavior goes. |
||||
|
|
||||
|
```c |
||||
|
#include <math.h> |
||||
|
|
||||
|
void cm_my_driver(ARGS) |
||||
|
{ |
||||
|
/* Read parameters */ |
||||
|
double vcc = PARAM(vcc); |
||||
|
double dead_time = PARAM(dead_time); |
||||
|
|
||||
|
/* Read digital input states */ |
||||
|
Digital_State_t hin = INPUT_STATE(hin); |
||||
|
Digital_State_t lin = INPUT_STATE(lin); |
||||
|
|
||||
|
/* Compute outputs */ |
||||
|
if (INIT) { |
||||
|
/* First call — initialize state */ |
||||
|
OUTPUT(ho) = 0.0; |
||||
|
OUTPUT(lo) = 0.0; |
||||
|
} else { |
||||
|
/* Normal operation */ |
||||
|
if (hin == ONE) |
||||
|
OUTPUT(ho) = vcc; |
||||
|
else |
||||
|
OUTPUT(ho) = 0.0; |
||||
|
|
||||
|
if (lin == ONE) |
||||
|
OUTPUT(lo) = vcc; |
||||
|
else |
||||
|
OUTPUT(lo) = 0.0; |
||||
|
} |
||||
|
|
||||
|
/* Set output delay */ |
||||
|
OUTPUT_DELAY(ho) = dead_time; |
||||
|
OUTPUT_DELAY(lo) = dead_time; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Key XSPICE Macros |
||||
|
|
||||
|
| Macro | Purpose | |
||||
|
|-------|---------| |
||||
|
| `ARGS` | Function signature placeholder (always use in the function declaration) | |
||||
|
| `INIT` | True on first call — use for initialization | |
||||
|
| `PARAM(name)` | Read a model parameter value | |
||||
|
| `INPUT(name)` | Read analog input voltage/current | |
||||
|
| `INPUT_STATE(name)` | Read digital input (ONE, ZERO, UNKNOWN) | |
||||
|
| `OUTPUT(name)` | Set analog output value | |
||||
|
| `OUTPUT_STATE(name)` | Set digital output state | |
||||
|
| `OUTPUT_DELAY(name)` | Set propagation delay for output | |
||||
|
| `OUTPUT_STRENGTH(name)` | Set digital output drive strength | |
||||
|
| `PORT_SIZE(name)` | Number of elements in a vector port | |
||||
|
| `PARTIAL(out, in)` | Set partial derivative (for analog convergence) | |
||||
|
| `STATIC_VAR(name)` | Access persistent state between timesteps | |
||||
|
| `T(n)` | Current time at call (n=0) or previous calls (n=1,2) | |
||||
|
| `cm_event_alloc(tag, size)` | Allocate event-driven state memory | |
||||
|
| `cm_event_get_ptr(tag, n)` | Get state pointer (n=0 current, n=1 previous) | |
||||
|
|
||||
|
## Port Types |
||||
|
|
||||
|
- **v** — analog voltage |
||||
|
- **i** — analog current |
||||
|
- **vd** — analog voltage differential |
||||
|
- **id** — analog current differential |
||||
|
- **d** — digital (ONE, ZERO, UNKNOWN) |
||||
|
|
||||
|
## Analog-Digital Bridges |
||||
|
|
||||
|
To connect analog and digital ports, use bridge models in the netlist: |
||||
|
|
||||
|
```spice |
||||
|
* Analog voltage -> Digital state |
||||
|
A_bridge1 [v_node] [d_node] adc1 |
||||
|
.model adc1 adc_bridge(in_low=0.8 in_high=2.0) |
||||
|
|
||||
|
* Digital state -> Analog voltage |
||||
|
A_bridge2 [d_node] [v_node] dac1 |
||||
|
.model dac1 dac_bridge(out_low=0.0 out_high=3.3) |
||||
|
``` |
||||
|
|
||||
|
## Verilog Co-Simulation (Future) |
||||
|
|
||||
|
Once code models work, Verilog support follows: |
||||
|
|
||||
|
1. Write your FPGA design in Verilog |
||||
|
2. Compile with Verilator: `verilator --cc design.v --exe` |
||||
|
3. Build the output into a shared library (.dll) |
||||
|
4. Use `d_cosim` in the netlist: |
||||
|
|
||||
|
```spice |
||||
|
A1 [clk din] [dout pwm] cosim1 |
||||
|
.model cosim1 d_cosim(simulation="my_fpga_design.dll") |
||||
|
``` |
||||
|
|
||||
|
ngspice loads the DLL and runs the compiled Verilog alongside the analog sim. |
||||
|
|
||||
|
## Directory Layout for pyTesla Parts |
||||
|
|
||||
|
``` |
||||
|
src/xspice/icm/ |
||||
|
├── analog/ # Built-in analog models (gain, limit, etc.) |
||||
|
├── digital/ # Built-in digital models (d_and, d_cosim, etc.) |
||||
|
├── pytesla/ # Our custom part library (new category) |
||||
|
│ ├── modpath.lst # List of model directories |
||||
|
│ ├── gate_driver/ |
||||
|
│ │ ├── cfunc.mod |
||||
|
│ │ └── ifspec.ifs |
||||
|
│ ├── spark_gap/ |
||||
|
│ │ ├── cfunc.mod |
||||
|
│ │ └── ifspec.ifs |
||||
|
│ └── ... |
||||
|
``` |
||||
|
|
||||
|
## Build Process |
||||
|
|
||||
|
After adding a new code model: |
||||
|
|
||||
|
1. Place `cfunc.mod` and `ifspec.ifs` in the appropriate directory |
||||
|
2. Add the model name to `modpath.lst` |
||||
|
3. Rebuild: `python build_mingw.py` |
||||
|
|
||||
|
The CMake build will run `cmpp` to preprocess the files, compile |
||||
|
the generated C, and register the model with ngspice automatically. |
||||
|
|
||||
|
(Note: the ICM build step in CMakeLists.txt is not yet implemented — |
||||
|
this document describes the target workflow once it is.) |
||||
@ -0,0 +1,242 @@ |
|||||
|
"""Build the cmpp preprocessor and compile XSPICE .cm code model libraries.""" |
||||
|
import subprocess |
||||
|
import os |
||||
|
import sys |
||||
|
import shutil |
||||
|
|
||||
|
MINGW_BIN = r"C:\mingw64\bin" |
||||
|
NGSPICE_ROOT = r"C:\git\ngspice" |
||||
|
CMPP_SRC = os.path.join(NGSPICE_ROOT, "src", "xspice", "cmpp") |
||||
|
ICM_DIR = os.path.join(NGSPICE_ROOT, "src", "xspice", "icm") |
||||
|
INCLUDE_DIR = os.path.join(NGSPICE_ROOT, "src", "include") |
||||
|
CONFIG_H_DIR = os.path.join(NGSPICE_ROOT, "visualc", "src", "include") |
||||
|
BUILD_DIR = os.path.join(NGSPICE_ROOT, "build", "cmpp_build") |
||||
|
CM_OUTPUT_DIR = os.path.join(NGSPICE_ROOT, "pyngspice", "codemodels") |
||||
|
CMPP_EXE = os.path.join(BUILD_DIR, "cmpp.exe") |
||||
|
|
||||
|
def get_env(): |
||||
|
env = os.environ.copy() |
||||
|
env["PATH"] = MINGW_BIN + ";" + env["PATH"] |
||||
|
return env |
||||
|
|
||||
|
def run(cmd, cwd=None): |
||||
|
print(f" > {' '.join(cmd)}") |
||||
|
r = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd, env=get_env()) |
||||
|
if r.stdout.strip(): |
||||
|
for line in r.stdout.strip().split('\n')[:20]: |
||||
|
print(f" {line}") |
||||
|
if r.stderr.strip(): |
||||
|
lines = r.stderr.strip().split('\n') |
||||
|
# Show first 10 and last 30 lines of errors |
||||
|
if len(lines) > 40: |
||||
|
for line in lines[:5]: |
||||
|
print(f" {line}") |
||||
|
print(f" ... ({len(lines) - 35} lines omitted) ...") |
||||
|
for line in lines[-30:]: |
||||
|
print(f" {line}") |
||||
|
else: |
||||
|
for line in lines: |
||||
|
print(f" {line}") |
||||
|
if r.returncode != 0: |
||||
|
print(f" FAILED (rc={r.returncode})") |
||||
|
return False |
||||
|
return True |
||||
|
|
||||
|
def build_cmpp(): |
||||
|
"""Build the cmpp preprocessor tool.""" |
||||
|
print("=== Step 1: Building cmpp preprocessor ===") |
||||
|
os.makedirs(BUILD_DIR, exist_ok=True) |
||||
|
|
||||
|
if os.path.exists(CMPP_EXE): |
||||
|
print(f" cmpp.exe already exists, skipping") |
||||
|
return True |
||||
|
|
||||
|
cmpp_sources = [ |
||||
|
"main.c", "file_buffer.c", "pp_ifs.c", "pp_lst.c", "pp_mod.c", |
||||
|
"read_ifs.c", "util.c", "writ_ifs.c", "ifs_yacc.c", "mod_yacc.c", |
||||
|
"ifs_lex.c", "mod_lex.c" |
||||
|
] |
||||
|
|
||||
|
cmd = [ |
||||
|
"gcc", "-o", CMPP_EXE, |
||||
|
] + [os.path.join(CMPP_SRC, f) for f in cmpp_sources] + [ |
||||
|
f"-I{CMPP_SRC}", f"-I{INCLUDE_DIR}", |
||||
|
"-lshlwapi" |
||||
|
] |
||||
|
|
||||
|
if not run(cmd): |
||||
|
return False |
||||
|
|
||||
|
print(f" OK: {CMPP_EXE}") |
||||
|
return True |
||||
|
|
||||
|
def preprocess_category(category): |
||||
|
"""Run cmpp on a code model category (analog, digital, etc.).""" |
||||
|
print(f"\n=== Step 2: Preprocessing '{category}' code models ===") |
||||
|
src_dir = os.path.join(ICM_DIR, category) |
||||
|
out_dir = os.path.join(BUILD_DIR, category) |
||||
|
|
||||
|
# Copy the category directory to build dir for cmpp to work in |
||||
|
if os.path.exists(out_dir): |
||||
|
shutil.rmtree(out_dir) |
||||
|
shutil.copytree(src_dir, out_dir) |
||||
|
|
||||
|
# Step 2a: Generate registration headers from modpath.lst |
||||
|
print(f" Generating registration headers...") |
||||
|
env = get_env() |
||||
|
env["CMPP_IDIR"] = src_dir |
||||
|
env["CMPP_ODIR"] = out_dir |
||||
|
r = subprocess.run([CMPP_EXE, "-lst"], capture_output=True, text=True, |
||||
|
cwd=out_dir, env=env) |
||||
|
if r.returncode != 0: |
||||
|
print(f" cmpp -lst FAILED: {r.stderr}") |
||||
|
return None |
||||
|
print(f" OK: cminfo.h, cmextrn.h generated") |
||||
|
|
||||
|
# Step 2b: Read modpath.lst to get model names |
||||
|
modpath = os.path.join(src_dir, "modpath.lst") |
||||
|
with open(modpath) as f: |
||||
|
models = [line.strip() for line in f if line.strip()] |
||||
|
|
||||
|
# Step 2c: Preprocess each model's .ifs and .mod files |
||||
|
for model in models: |
||||
|
model_src = os.path.join(src_dir, model) |
||||
|
model_out = os.path.join(out_dir, model) |
||||
|
|
||||
|
if not os.path.isdir(model_src): |
||||
|
print(f" SKIP: {model} (directory not found)") |
||||
|
continue |
||||
|
|
||||
|
# Preprocess ifspec.ifs -> ifspec.c |
||||
|
env_mod = get_env() |
||||
|
env_mod["CMPP_IDIR"] = model_src |
||||
|
env_mod["CMPP_ODIR"] = model_out |
||||
|
r = subprocess.run([CMPP_EXE, "-ifs"], capture_output=True, text=True, |
||||
|
cwd=model_out, env=env_mod) |
||||
|
if r.returncode != 0: |
||||
|
print(f" FAIL: {model}/ifspec.ifs: {r.stderr}") |
||||
|
return None |
||||
|
|
||||
|
# Preprocess cfunc.mod -> cfunc.c |
||||
|
r = subprocess.run([CMPP_EXE, "-mod"], capture_output=True, text=True, |
||||
|
cwd=model_out, env=env_mod) |
||||
|
if r.returncode != 0: |
||||
|
print(f" FAIL: {model}/cfunc.mod: {r.stderr}") |
||||
|
return None |
||||
|
|
||||
|
print(f" OK: {len(models)} models preprocessed") |
||||
|
return models |
||||
|
|
||||
|
# Category-specific extra source files and include paths |
||||
|
CATEGORY_EXTRAS = { |
||||
|
"tlines": { |
||||
|
"extra_sources": [ |
||||
|
os.path.join(NGSPICE_ROOT, "src", "xspice", "tlines", "tline_common.c"), |
||||
|
os.path.join(NGSPICE_ROOT, "src", "xspice", "tlines", "msline_common.c"), |
||||
|
], |
||||
|
"extra_includes": [ |
||||
|
os.path.join(NGSPICE_ROOT, "src", "xspice", "tlines"), |
||||
|
], |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
def compile_cm(category, models): |
||||
|
"""Compile preprocessed code models into a .cm DLL.""" |
||||
|
print(f"\n=== Step 3: Compiling '{category}.cm' DLL ===") |
||||
|
src_dir = os.path.join(ICM_DIR, category) |
||||
|
out_dir = os.path.join(BUILD_DIR, category) |
||||
|
os.makedirs(CM_OUTPUT_DIR, exist_ok=True) |
||||
|
|
||||
|
# Collect all .c files to compile |
||||
|
c_files = [] |
||||
|
|
||||
|
# dlmain.c (the DLL entry point with exports) |
||||
|
dlmain = os.path.join(ICM_DIR, "dlmain.c") |
||||
|
c_files.append(dlmain) |
||||
|
|
||||
|
# Each model's cfunc.c and ifspec.c |
||||
|
for model in models: |
||||
|
model_dir = os.path.join(out_dir, model) |
||||
|
cfunc_c = os.path.join(model_dir, "cfunc.c") |
||||
|
ifspec_c = os.path.join(model_dir, "ifspec.c") |
||||
|
if os.path.exists(cfunc_c) and os.path.exists(ifspec_c): |
||||
|
c_files.append(cfunc_c) |
||||
|
c_files.append(ifspec_c) |
||||
|
else: |
||||
|
print(f" SKIP: {model} (generated .c files missing)") |
||||
|
|
||||
|
# UDN (user-defined node) types — udnfunc.c files from udnpath.lst |
||||
|
udnpath = os.path.join(src_dir, "udnpath.lst") |
||||
|
if os.path.exists(udnpath): |
||||
|
with open(udnpath) as f: |
||||
|
udns = [line.strip() for line in f if line.strip()] |
||||
|
for udn in udns: |
||||
|
udnfunc = os.path.join(src_dir, udn, "udnfunc.c") |
||||
|
if os.path.exists(udnfunc): |
||||
|
c_files.append(udnfunc) |
||||
|
print(f" UDN: {udn}/udnfunc.c") |
||||
|
|
||||
|
# Also need dstring.c for some models that use DS_CREATE |
||||
|
dstring_c = os.path.join(NGSPICE_ROOT, "src", "misc", "dstring.c") |
||||
|
if os.path.exists(dstring_c): |
||||
|
c_files.append(dstring_c) |
||||
|
|
||||
|
# Category-specific extra sources |
||||
|
extras = CATEGORY_EXTRAS.get(category, {}) |
||||
|
for src in extras.get("extra_sources", []): |
||||
|
if os.path.exists(src): |
||||
|
c_files.append(src) |
||||
|
print(f" EXTRA: {os.path.basename(src)}") |
||||
|
|
||||
|
cm_file = os.path.join(CM_OUTPUT_DIR, f"{category}.cm") |
||||
|
|
||||
|
cmd = [ |
||||
|
"gcc", "-shared", "-o", cm_file, |
||||
|
] + c_files + [ |
||||
|
f"-I{INCLUDE_DIR}", |
||||
|
f"-I{CONFIG_H_DIR}", |
||||
|
f"-I{out_dir}", # For cminfo.h, cmextrn.h |
||||
|
f"-I{ICM_DIR}", # For dlmain.c includes |
||||
|
] |
||||
|
|
||||
|
# Category-specific extra include paths |
||||
|
for inc in extras.get("extra_includes", []): |
||||
|
cmd.append(f"-I{inc}") |
||||
|
|
||||
|
cmd += [ |
||||
|
"-DHAS_PROGREP", |
||||
|
"-DXSPICE", |
||||
|
"-DSIMULATOR", |
||||
|
"-DNG_SHARED_BUILD", |
||||
|
"-DNGSPICEDLL", |
||||
|
"-Wall", "-Wno-unused-variable", "-Wno-unused-function", |
||||
|
"-Wno-sign-compare", "-Wno-maybe-uninitialized", |
||||
|
"-Wno-implicit-function-declaration", # GCC 15 makes this an error; some models missing stdlib.h |
||||
|
] |
||||
|
|
||||
|
if not run(cmd): |
||||
|
return False |
||||
|
|
||||
|
size_kb = os.path.getsize(cm_file) // 1024 |
||||
|
print(f" OK: {cm_file} ({size_kb} KB)") |
||||
|
return True |
||||
|
|
||||
|
def build_category(category): |
||||
|
"""Full pipeline: preprocess + compile a code model category.""" |
||||
|
models = preprocess_category(category) |
||||
|
if models is None: |
||||
|
return False |
||||
|
return compile_cm(category, models) |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
categories = sys.argv[1:] if len(sys.argv) > 1 else ["analog"] |
||||
|
|
||||
|
if not build_cmpp(): |
||||
|
sys.exit(1) |
||||
|
|
||||
|
for cat in categories: |
||||
|
if not build_category(cat): |
||||
|
print(f"\nFailed to build {cat}.cm") |
||||
|
sys.exit(1) |
||||
|
|
||||
|
print(f"\n=== Done! Code models in: {CM_OUTPUT_DIR} ===") |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue