# 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.)