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