diff --git a/XSPICE_CODE_MODELS.md b/XSPICE_CODE_MODELS.md new file mode 100644 index 000000000..087b6a712 --- /dev/null +++ b/XSPICE_CODE_MODELS.md @@ -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///`: + +### 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 + +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.) diff --git a/build_cmpp.py b/build_cmpp.py new file mode 100644 index 000000000..bd31d65b0 --- /dev/null +++ b/build_cmpp.py @@ -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} ===") diff --git a/pyngspice/codemodels/analog.cm b/pyngspice/codemodels/analog.cm new file mode 100644 index 000000000..d2c7c3229 Binary files /dev/null and b/pyngspice/codemodels/analog.cm differ diff --git a/pyngspice/codemodels/digital.cm b/pyngspice/codemodels/digital.cm new file mode 100644 index 000000000..bd0d7de7b Binary files /dev/null and b/pyngspice/codemodels/digital.cm differ diff --git a/pyngspice/codemodels/spice2poly.cm b/pyngspice/codemodels/spice2poly.cm new file mode 100644 index 000000000..6372a2097 Binary files /dev/null and b/pyngspice/codemodels/spice2poly.cm differ diff --git a/pyngspice/codemodels/table.cm b/pyngspice/codemodels/table.cm new file mode 100644 index 000000000..2c4a5c652 Binary files /dev/null and b/pyngspice/codemodels/table.cm differ diff --git a/pyngspice/codemodels/tlines.cm b/pyngspice/codemodels/tlines.cm new file mode 100644 index 000000000..be71d84d4 Binary files /dev/null and b/pyngspice/codemodels/tlines.cm differ diff --git a/pyngspice/codemodels/xtradev.cm b/pyngspice/codemodels/xtradev.cm new file mode 100644 index 000000000..af4f1e2b6 Binary files /dev/null and b/pyngspice/codemodels/xtradev.cm differ diff --git a/pyngspice/codemodels/xtraevt.cm b/pyngspice/codemodels/xtraevt.cm new file mode 100644 index 000000000..c829625d2 Binary files /dev/null and b/pyngspice/codemodels/xtraevt.cm differ