diff --git a/configure.ac b/configure.ac index d7888ac3a..9a2ed40ad 100644 --- a/configure.ac +++ b/configure.ac @@ -1443,6 +1443,7 @@ AC_CONFIG_FILES([Makefile src/xspice/ipc/Makefile src/xspice/idn/Makefile src/xspice/verilog/Makefile + src/xspice/vhdl/Makefile src/osdi/Makefile tests/Makefile tests/bsim1/Makefile diff --git a/examples/xspice/ghdl/555.cir b/examples/xspice/ghdl/555.cir new file mode 100644 index 000000000..9a0dbff5e --- /dev/null +++ b/examples/xspice/ghdl/555.cir @@ -0,0 +1,10 @@ +VHDL-controlled simple timer. + +* This is the model for an RS flip-flop implemented by Icarus Verilog. + +.model vlog_ff d_cosim simulation="./timer_core" sim_args=["555"] + +* The bulk of the circuit is in a shared file. + +.include ../verilator/555.shared +.end diff --git a/examples/xspice/ghdl/555.vhd b/examples/xspice/ghdl/555.vhd new file mode 100644 index 000000000..4f9f2ee1a --- /dev/null +++ b/examples/xspice/ghdl/555.vhd @@ -0,0 +1,29 @@ +-- Very simple logic for a 555 timer simulation + +library ieee; +use ieee.std_logic_1164.all; + +entity timer_core is + port ( + Trigger, Threshold, Reset : in std_logic; + Q, Qbar : out std_logic + ); +end timer_core; + +architecture rtl of timer_core is + signal result : std_logic := '0'; + signal ireset, go : std_logic; +begin + go <= Trigger and Reset; + ireset <= (Threshold and not go) or not Reset; + Q <= result; + Qbar <= not result; + + process (go, ireset) + begin + if rising_edge(go) or rising_edge(ireset) then + result <= go; + end if; + end process; +end rtl; + diff --git a/examples/xspice/ghdl/BUILD.CMD b/examples/xspice/ghdl/BUILD.CMD new file mode 100644 index 000000000..ac3815eea --- /dev/null +++ b/examples/xspice/ghdl/BUILD.CMD @@ -0,0 +1,4 @@ +ngspice -- ghnggen -top timer_core 555.vhd +ngspice ghnggen pwm.vhd +ngspice ghnggen adc.vhd +ngspice ghnggen mc.vhd diff --git a/examples/xspice/ghdl/CLEAN.CMD b/examples/xspice/ghdl/CLEAN.CMD new file mode 100644 index 000000000..b6367600e --- /dev/null +++ b/examples/xspice/ghdl/CLEAN.CMD @@ -0,0 +1 @@ +del /q *.obj *.so* *.vpi *.DLL *.cf *.raw diff --git a/examples/xspice/ghdl/README.txt b/examples/xspice/ghdl/README.txt new file mode 100644 index 000000000..8cce02f0e --- /dev/null +++ b/examples/xspice/ghdl/README.txt @@ -0,0 +1,36 @@ +The circuits in this directory illustrate the use of the d_cosim +XSPICE code model as a container for a VHDL simulation using +the GHDL compiler. Use a version of GHDL built with the LLVM back-end. + +The circuits and steps below are intended to be used from the directory +containing this file, certainly ouput files from GHDL should be in +the current directory when simulating. + +The example circuits are: + +555.cir: The probably familiar NE555 oscillator provides a minimal example +of combined simulation with SPICE and GHDL. +The digital part of the IC, a simple SR flip-flop, is expressed in VHDL. + +pwm.c: VHDL "wait" statements control a pulse-width modulated output to +generate an approximate sine wave. + +adc.cir: Slightly more complex VHDL describes the controlling part +of a switched-capacitor ADC. + +mc.cir: A motor-control simulation with the control algorithm written +in non-sythesisable VHDL. It is used as a general-purpose programming +language rather than a HDL. Apart from showing that this is easy to do, +no part should be taken seriously: the algorithm is poor (but simple), +the motor parameters are invented and the voltages and currents representing +mechanical quantities do not match standard units. + +Before a simulation can be run, the associated VHDL code must be compiled +and an additional shared library, ghdlng.vpi must be built. A library script +isavailable to simplify the steps, run like this: + + ngspice ghnggen adc.vhd + +A command interpreter script, ./build or BUILD.CMD, is provided to build +all four examples, with clean/CLEAN.CMD to remove the files created. + diff --git a/examples/xspice/ghdl/adc.cir b/examples/xspice/ghdl/adc.cir new file mode 100644 index 000000000..74cb3f42a --- /dev/null +++ b/examples/xspice/ghdl/adc.cir @@ -0,0 +1,10 @@ +Simulation of a switched-capacitor SAR ADC with GHDL and d_cosim. + +* Model line for the digital control implemented by GHDL. + +.model dut d_cosim simulation="./adc" sim_args=["./adc"] + +* The bulk of the circuit is in a shared file. + +.include ../verilator/adc.shared +.end diff --git a/examples/xspice/ghdl/adc.vhd b/examples/xspice/ghdl/adc.vhd new file mode 100644 index 000000000..2da2eb7bd --- /dev/null +++ b/examples/xspice/ghdl/adc.vhd @@ -0,0 +1,62 @@ +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity adc is + generic ( Bits : integer := 6 ); + port ( + Clk : in std_logic; + Comp : in std_logic; + Start : in std_logic; + Sample : out std_logic; + Done : out std_logic; + Result : out unsigned(0 to Bits - 1) + ); +end entity; + +architecture ghdl_adc of adc is + signal SR : unsigned(0 to Bits - 1); + signal Result_Reg : unsigned(0 to Bits - 1); + signal Sample_Reg : std_logic := '0'; + signal Running : std_logic := '0'; +begin + Result <= Result_Reg; + Sample <= Sample_Reg; + + process (Clk) + constant Zeros : unsigned(0 to Bits - 1) := (others => '0'); + variable NextSR : unsigned(0 to Bits - 1); + begin + if rising_edge(Clk) then + if Running = '1' then + if Sample_Reg = '1' then + Sample_Reg <= '0'; + SR(Bits - 1) <= '1'; + Result_Reg(Bits - 1) <= '1'; + else + if SR /= 0 then + NextSR := shift_left(SR, 1); + if Comp = '1' then + Result_Reg <= (Result_Reg and not SR) or NextSR; + else + Result_Reg <= Result_Reg or NextSR; + end if; + SR <= NextSR; + else + Running <= '0'; + Done <= '1'; + end if; + end if; + else + if Start = '1' then + Running <= '1'; + Sample_Reg <= '1'; + Done <= '0'; + SR <= Zeros; + Result_Reg <= Zeros; + end if; + end if; + end if; + end process; +end architecture; + diff --git a/examples/xspice/ghdl/build b/examples/xspice/ghdl/build new file mode 100755 index 000000000..ac3815eea --- /dev/null +++ b/examples/xspice/ghdl/build @@ -0,0 +1,4 @@ +ngspice -- ghnggen -top timer_core 555.vhd +ngspice ghnggen pwm.vhd +ngspice ghnggen adc.vhd +ngspice ghnggen mc.vhd diff --git a/examples/xspice/ghdl/clean b/examples/xspice/ghdl/clean new file mode 100755 index 000000000..d0c6cae1e --- /dev/null +++ b/examples/xspice/ghdl/clean @@ -0,0 +1,2 @@ +#!/bin/sh +rm -f *~ *.o *.so* *.vpi *.cf *.dylib *.raw diff --git a/examples/xspice/ghdl/mc.cir b/examples/xspice/ghdl/mc.cir new file mode 100644 index 000000000..dbd351017 --- /dev/null +++ b/examples/xspice/ghdl/mc.cir @@ -0,0 +1,82 @@ +Simulation of elecric motor and controller in behavioural VHDL. + +* Power from a wall socket. + +Vmains live 0 SIN 0 250 50 + +* Behavioral power-line zero-crossing detector, + +Bcross cross 0 V=(V(live) > -2 && V(live) < 2) ? 5 : 0 + +* Motor and its rotor + +Xmotor live mt1 tacho 0 motor + +* A triac to control the motor is replaced by a voltage-controlled switch +* to avoid including a manufacurer's triac library. + +*Xtriac mt1 gate 0 BTA16-600B +Striac mt1 0 drive 0 triac off +.model triac sw vt=1.5 vh=1 roff=1meg + +* The controller is implemented as VHDL code. The argument "-gTarget=5000" +* in the model line overrides the default target motor speed in the VHDL file. +* Tuning parameters Full_Band and Dead_band may also be set. + +Ademo [ cross tacho ] [ drive ] controller +.model controller d_cosim simulation="./mc" ++ sim_args=["./mc.so" "-gTarget=4500"] + + +* Motor electrical circuit - inductance, resistance and back-EMF +* Loosley based on: +* https://www.precisionmicrodrives.com/content/ab-025-using-spice-to-model-dc-motors/ +* No resemblance is claimed to any actual motor, living or dead. + +.subckt motor live neutral tacho1 tacho2 ++ Lm=0.3 Rm=30 BackC=20 // Electrical ++ TorqueC=30 Linertia=20 Rdrag=20 Load={50} // Mechanical + +* Electrical inductance and resistance of the motor. + +Lm live internal {Lm} +Rm internal back_emf {Rm} + +* Back EMF: here I(Bmech) is proportional to speed, I(Bback) is motor current. + +Bback back_emf neutral V=-I(Bmech)*I(Bback)*{BackC} + +* Mechanical simulation: torque (V), speed (I), inertia (L) and +* friction/load (R). +* Series (Universal) motor so torque proportional to current squared. + +Bmech torque 0 V=I(Bback)*I(Bback)*{TorqueC} // Motor torque +Lmech torque inertia {Linertia} +Rmech inertia load {Rdrag} + +* In addition to friction (Rdrag) there is a varying load. + +* Delay +* | Rise time +* | | Fall time +* | | | Pulse width +* | | | | Period +* | | | | | +* V V V V V +Vload load 0 PULSE 0 {Load} 1.2 10m 10m 200m 400m + +* Tachometer: integrate speed for position and pulse. + +Bpos i1 0 I=I(Bmech) +Cpos i1 0 0.01 +.ic v(i1)=0 +Btach tacho1 tacho2 V=((V(i1) - floor(V(i1)) > 0.5) ? 5 : 0) + +.ends + +.control +save drive @b.xmotor.bback[i] @b.xmotor.bmech[i] xmotor.load +tran 50u 3 +plot @b.xmotor.bback[i] @b.xmotor.bmech[i]*-10 drive xmotor.load/10 +.endc +.end diff --git a/examples/xspice/ghdl/mc.vhd b/examples/xspice/ghdl/mc.vhd new file mode 100644 index 000000000..6c323c34a --- /dev/null +++ b/examples/xspice/ghdl/mc.vhd @@ -0,0 +1,80 @@ +-- Test crude algorithm for a simple motor controller. + +library ieee; +use ieee.std_logic_1164.all; + +entity mc is + -- Control values that may be overrriden by the "sim_args" option + -- in a netlist's .model line. Intgers are used as GHDL does not + -- support overriding Real values. + generic ( + Target : Integer := 4000; -- notional RPM + Dead_Band : Integer := 200; + Full_Band : Integer := 600 + ); + port ( + Zero : in std_logic; + Tach : in std_logic; + Trigger : out std_logic + ); +end mc; + +architecture mc_arch of mc is + shared variable Speed : Real := 0.0; +begin + Tachometer : process (Tach) is + variable Time : Real := 0.0; + variable Last_pulse : Real := 0.0; + begin + if rising_edge(Tach) or falling_edge(Tach) then + Time := real(now / 1 ns) * 1.0e-9; + Speed := 30.0 / (Time - Last_pulse); + Last_pulse := Time; + end if; + end process Tachometer; + + Controller : process (Zero) is + variable Skip : Integer := 0; + variable Count : Integer := 0; + variable Even : Boolean := True; + variable Was_even : Boolean := True; + variable Error : Integer; + begin + -- Trigger triac on zero crossings, preventing DC current. + + if rising_edge(Zero) then + Even := not Even; + if (Count >= Skip) and (Even /= Was_even) then + Trigger <= '1'; + Was_even := Even; + if Count > Skip then + Count := 0; + else + Count := -1; + end if; + else + Trigger <= '0'; + end if; + Count := Count + 1; + + -- A very crude feedback mechanism. + + Error := integer(Speed) - Target; + if Error > Full_Band then + -- No drive + Skip := 1; + Count := 0; + elsif Error < -Full_Band then + -- Full Drive + Skip := 0; + elsif Error > Dead_Band then + Skip := Skip + 1; + elsif Error < -Dead_Band then + if Skip > 0 then + Skip := Skip - 1; + end if; + end if; + + end if; + end process Controller; +end mc_arch; diff --git a/examples/xspice/ghdl/pwm.cir b/examples/xspice/ghdl/pwm.cir new file mode 100644 index 000000000..fe7c42557 --- /dev/null +++ b/examples/xspice/ghdl/pwm.cir @@ -0,0 +1,12 @@ +* Waveform generation by PWM in VHDL. + +adut null [ out ] pwm_sin +.model pwm_sin d_cosim simulation="./pwm" sim_args = [ "pwm" ] + +r1 out smooth 100k +c1 smooth 0 1u +.control +tran 1m 2 +plot out-3.3 smooth +.endc +.end diff --git a/examples/xspice/ghdl/pwm.vhd b/examples/xspice/ghdl/pwm.vhd new file mode 100644 index 000000000..5df339bd7 --- /dev/null +++ b/examples/xspice/ghdl/pwm.vhd @@ -0,0 +1,37 @@ +-- Very simple logic for PWM waveform generation. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.math_real.all; + +entity pwm is + port ( output : out std_logic := '1'); +end pwm; + +architecture pwm_impl of pwm is + constant Cycles : Integer := 1000; + constant Samples : Integer := 1000; + constant uSec : Time := 1 us; +begin + process + variable j : Integer := 0; + variable width : Integer := Cycles / 2; + variable sine : Real; + begin + wait for width * uSec; + output <= '0'; + wait for (Cycles - width) * uSec; + j := j + 1; + if j = Samples then + j := 0; + end if; + sine := sin(real(j) * MATH_2_PI / real(Samples)); + width := integer(real(Samples) * (1.0 + sine) / 2.0); + if width = 0 then + output <= '0'; + else + output <= '1'; + end if; + end process; +end pwm_impl; + diff --git a/src/xspice/Makefile.am b/src/xspice/Makefile.am index cdf712bfa..6085250e9 100644 --- a/src/xspice/Makefile.am +++ b/src/xspice/Makefile.am @@ -3,13 +3,15 @@ EXTRA_DIST = README examples icm xspice.c .gitignore \ verilog/vlnggen \ verilog/verilator_shim.cpp verilog/verilator_main.cpp \ - verilog/libvvp.def verilog/MSVC.CMD verilog/README.txt + verilog/libvvp.def verilog/MSVC.CMD verilog/README.txt \ + vhdl/ghnggen \ + vhdl/ghdl_shim.h vhdl/ghdl_shim.c vhdl/ghdl_vpi.c ## This is removed because icm relies upon the existance of all other ## libs. It is currently compiled manually, last. ##SUBDIRS = mif cm enh evt ipc idn icm -SUBDIRS = mif cm enh evt ipc idn cmpp icm verilog +SUBDIRS = mif cm enh evt ipc idn cmpp icm verilog vhdl dist-hook: rm -f "$(distdir)/icm/makedefs" diff --git a/src/xspice/verilog/Makefile.am b/src/xspice/verilog/Makefile.am index 262d63384..18c03e9c0 100644 --- a/src/xspice/verilog/Makefile.am +++ b/src/xspice/verilog/Makefile.am @@ -13,7 +13,8 @@ initdata1_DATA = verilator_shim.cpp verilator_main.cpp initdata2dir = $(pkgdatadir)/scripts/src/ngspice initdata2_DATA = ../../include/ngspice/cosim.h \ ../../include/ngspice/miftypes.h \ - ../../include/ngspice/cmtypes.h + ../../include/ngspice/cmtypes.h \ + ./coroutine.h ./coroutine_cosim.h ./coroutine_shim.h # Icarus Verilog support: build two shared libraries. diff --git a/src/xspice/vhdl/Makefile.am b/src/xspice/vhdl/Makefile.am new file mode 100644 index 000000000..1686d0b6e --- /dev/null +++ b/src/xspice/vhdl/Makefile.am @@ -0,0 +1,11 @@ + ## Process this file with automake to produce Makefile.in + +MAINTAINERCLEANFILES = Makefile.in + +# GHDL support: files installed to script directory and below. + +initdatadir = $(pkgdatadir)/scripts +initdata_DATA = ghnggen + +initdata1dir = $(pkgdatadir)/scripts/src +initdata1_DATA = ghdl_shim.h ghdl_shim.c ghdl_vpi.c diff --git a/src/xspice/vhdl/ghdl_shim.c b/src/xspice/vhdl/ghdl_shim.c new file mode 100644 index 000000000..8f307f61c --- /dev/null +++ b/src/xspice/vhdl/ghdl_shim.c @@ -0,0 +1,281 @@ +/* Main file to be linked with code generated by GHDL to make a shared library + * to be loaded by ngspice's d_cosim code model to connect an instance of a + * VHDL simulation into a Ngspice netlist. + * Licensed on the same terms as Ngspice. + * + * Copyright (c) 2024 Giles Atkinson + */ + +#include +#include +#include +#include +#include +#include + +/* The GHDL code runs on its own stack, handled by cr_xxx() functions. */ + +#include "ngspice/coroutine_shim.h" + +#include "ngspice/cmtypes.h" // For Digital_t +#include "ngspice/cosim.h" + +/* This header file defines the external (d_cosim) interface. It also contains + * an initial comment that describes how this shared library is used. + */ + +#include "ghdl_shim.h" + +extern int ghdl_main(int argc, char **argv); // Not in any header. + +/* Report fatal errors. */ + +static void fail(const char *what, int why) +{ + fprintf(stderr, "Icarus shim failed in function %s: %s.\n", + what, strerror(why)); + abort(); +} + +static void input(struct co_info *pinfo, unsigned int bit, Digital_t *val) +{ + struct ng_ghdl *ctx = (struct ng_ghdl *)pinfo->handle; + struct ngvp_port *pp; + int count, a, b, dirty; + + /* Convert the value. */ + + if (val->strength <= HI_IMPEDANCE && val->state != UNKNOWN) { + a = val->state; // Normal - '0'/'1'. + b = 0; + } else if (val->strength == HI_IMPEDANCE) { + a = 0; // High impedance - 'z'. + b = 1; + } else { + a = 1; // Undefined - 'x'. + b = 1; + } + + /* Find the port. */ + + if (bit >= pinfo->in_count) { + bit -= pinfo->in_count; + if (bit >= pinfo->inout_count) + return; + pp = ctx->ports + ctx->ins + ctx->outs; // Point at inouts. + count = ctx->inouts; + } else { + pp = ctx->ports; + count = ctx->ins; + } + + while (count--) { + if (pp[count].position <= bit) + break; + } + pp = pp + count; + bit -= pp->position; + + /* Check and update. */ + + dirty = 0; + bit = pp->bits - bit - 1; // Bit position for big-endian. + a <<= bit; + if (a ^ pp->previous.aval) { + if (a) + pp->previous.aval |= a; + else + pp->previous.aval &= ~(1 << bit); + dirty = 1; + } + b <<= bit; + if (b ^ pp->previous.bval) { + if (b) + pp->previous.bval |= b; + else + pp->previous.bval &= ~(1 << bit); + dirty = 1; + } + + if (dirty && !(pp->flags & IN_PENDING)) { + pp->flags |= IN_PENDING; + ++ctx->in_pending; + } +} + +/* Move the GHDL simulation forward. */ + +static void step(struct co_info *pinfo) +{ + struct ng_ghdl *ctx = (struct ng_ghdl *)pinfo->handle; + int i; + + /* Let GHDL run. It will stop when it has caught up with SPICE time + * (pinfo->vtime) or produced output. + */ + + cr_yield_to_sim(&ctx->cr_ctx); + + /* Check for output. */ + + if (ctx->out_pending) { + struct ngvp_port *pp; + uint32_t changed, mask; + int limit, i, bit; + + limit = ctx->outs + ctx->inouts; + for (i = 0, pp = ctx->ports + ctx->ins; i < limit; ++i, ++pp) { + if (!(pp->flags & OUT_PENDING)) + continue; + + pp->flags &= ~OUT_PENDING; + changed = (pp->new.aval ^ pp->previous.aval) | + (pp->new.bval ^ pp->previous.bval); + if (changed) { + bit = pp->position; + mask = 1 << (pp->bits - 1); + while (changed) { + if (mask & changed) { + const Digital_t lv_vals[] = + { {ZERO, STRONG}, {ONE, STRONG}, + {ZERO, HI_IMPEDANCE}, {UNKNOWN, STRONG} }; + int a, b; + + a = (pp->new.aval & mask) != 0; + b = (pp->new.bval & mask) != 0; + a += (b << 1); + pinfo->out_fn(pinfo, bit, (Digital_t *)lv_vals + a); + changed &= ~mask; + } + mask >>= 1; + ++bit; + } + pp->previous.aval = pp->new.aval; + pp->previous.bval = pp->new.bval; + } + if (--ctx->out_pending == 0) + break; + } + if (ctx->out_pending) + abort(); + } +} + +static void cleanup(struct co_info *pinfo) +{ + struct ng_ghdl *ctx = (struct ng_ghdl *)pinfo->handle; + + if (!ctx) + return; + + /* Tell GHDL to exit. */ + + ctx->stop = 1; + cr_yield_to_sim(&ctx->cr_ctx); + cr_cleanup(&ctx->cr_ctx); + free(ctx->ports); + free(ctx); + pinfo->handle = NULL; +} + +/* Thread start function runs the VHDL simulation. */ + +void *run_ghdl(void *arg) +{ + struct co_info *pinfo = (struct co_info *)arg; + struct ng_ghdl *ctx, **ctx_ptr; + const char *file; + void *vpi_handle; + const char *new_argv[pinfo->sim_argc + 2]; + int new_argc; + char vpi_buf[256]; + + cr_safety(); // Make safe with signals. + + if (pinfo->sim_argc == 0) { + new_argc = 1; + new_argv[0] = "dummy_arg_0"; + } else { + /* Copy the simulation arguments to the extended array. */ + + for (new_argc = 0; new_argc < pinfo->sim_argc; new_argc++) + new_argv[new_argc] = pinfo->sim_argv[new_argc]; + } + + /* Determing the file name for our VPI module. */ + + if (pinfo->lib_argc >= 2 && pinfo->lib_argv[1][0]) // Explicit VPI file. + file = pinfo->lib_argv[1]; + else + file = "./ghdlng.vpi"; + + /* Normally, GHDL would load the VPI module. Here it is preloaded and + * an internal variable is set to point to the shared co_info struct. + */ + + ctx_ptr = NULL; + vpi_handle = dlopen(file, RTLD_GLOBAL | RTLD_NOW); + if (vpi_handle) + ctx_ptr = dlsym(vpi_handle, CTX_VAR_NAME); + if (!ctx_ptr) { + fprintf(stderr, + "The GHDL shim can not initialise VPI module %s: %s.\n", + file, dlerror()); + return NULL; + } + ctx = (struct ng_ghdl *)pinfo->handle; + *ctx_ptr = ctx; + + /* The GHDL code is passed the VPI module name in a command argument. */ + + snprintf(vpi_buf, sizeof vpi_buf, "--vpi=%s", file); + new_argv[new_argc++] = vpi_buf; + new_argv[new_argc] = NULL; + ghdl_main(new_argc, (char **)new_argv); + + /* The simulation has finished. Do nothing until destroyed. */ + + dlclose(vpi_handle); + ctx->stop = 1; + for (;;) + cr_yield_to_spice(&ctx->cr_ctx); + + return NULL; +} + +/* Entry point to this shared library. Called by d_cosim. */ + +void Cosim_setup(struct co_info *pinfo) +{ + struct ng_ghdl *context; + struct ngvp_port *last_port; + + /* It is assumed that there is no parallel access to this function + * as ngspice initialisation is single-threaded. + */ + + context = calloc(1, sizeof (struct ng_ghdl)); + if (!context) + fail("malloc", errno); + context->cosim_context = pinfo; + pinfo->handle = context; + + /* Set-up the execution stack for the GHDL-generated code and start it. */ + + cr_init(&context->cr_ctx, run_ghdl, pinfo); + + /* Return required values in *pinfo. */ + + last_port = context->ports + context->ins - 1; + pinfo->in_count = context->ins ? last_port->position + last_port->bits : 0; + last_port += context->outs; + pinfo->out_count = + context->outs ? last_port->position + last_port->bits : 0; + last_port += context->inouts; + pinfo->inout_count = + context->inouts ? last_port->position + last_port->bits : 0; + pinfo->cleanup = cleanup; + pinfo->step = step; + pinfo->in_fn = input; + pinfo->method = Normal; +} diff --git a/src/xspice/vhdl/ghdl_shim.h b/src/xspice/vhdl/ghdl_shim.h new file mode 100644 index 000000000..02fcbc6be --- /dev/null +++ b/src/xspice/vhdl/ghdl_shim.h @@ -0,0 +1,68 @@ +#ifndef _GHDL_SHIM_H_ +#define _GHDL_SHIM_H_ + +/* This is the interface definition file for the shim code (ghdl_shim.c) + * and associated Verilog VPI module (ghdl_shim.vpi). + * + * This software allows execution of VHDL code generated by the GHDL compiler + * inside a SPICE simulation performed by ngspice. + * + * Use of these components starts with a SPICE netlist containing an A-device + * (XSPICE device) whose model card specifies the d_cosim code model and + * parameter 'simulation="some_path/entity_name.so"', where "entity_name" + * represents a top-level entity defined in VHDL. The shared library + * (or DLL) named is built by the "ghdl -e" command using output from + * a VHDL compilation ("ghdl -a") and the provided ghdl_shim.c file. + * + * The VPI module ghdl_vpi.c is called on loading: its first task is to obtain + * the list of ports for the top-level VHDL entity. After that the VPI code + * controls the execution of the VHDL code by blocking execution until + * commanded to proceed by the d_cosim instance, always regaining control + * via a VPI callback before the VHDL code moves ahead of the SPICE simulation. + */ + +typedef uint32_t __vpiHandle; // Compatible with GHDL's vpi_user.h + +/* Data stored for each port. */ + +struct ngvp_port { + uint16_t bits; // How many bits? + uint16_t flags; // I/O pending. + uint32_t position; // Number of bits before this port. + struct { // Like struct t_vpi_vecval. + int32_t aval; + int32_t bval; + } previous, new; // Previous and new values. + __vpiHandle *handle; // Handle to the port's variable. + struct ng_ghdl *ctx; // Pointer back to parent. +}; + +#define IN_PENDING 1 +#define OUT_PENDING 2 + +/* Data strucure used to share context between the ngspice and GHDL threads. */ + +struct ng_ghdl { + struct cr_ctx cr_ctx; // Coroutine context. + int stop; // Indicates simulation is over. + struct co_info *cosim_context; + uint32_t ins; // Port counts by type. + uint32_t outs; + uint32_t inouts; + double base_time; // SPICE time on entry. + double tick_length; // GHDL's time unit. + __vpiHandle *stop_cb; // Handle to end-of-tick callback. + volatile uint32_t in_pending; // Counts of changed ports. + volatile uint32_t out_pending; + struct ngvp_port *ports; // Port information array. +}; + +/* The VPI module, ghdlng.vpi, contains a global variable with this name, + * used during initialisation. + */ + +#define CTX_VAR GHDLNG_VPI_context +#define STR(s) #s +#define XSTR(s) STR(s) // Puts quotes on its argument. +#define CTX_VAR_NAME XSTR(CTX_VAR) +#endif // _GHDL_SHIM_H_ diff --git a/src/xspice/vhdl/ghdl_vpi.c b/src/xspice/vhdl/ghdl_vpi.c new file mode 100644 index 000000000..5ecd1b401 --- /dev/null +++ b/src/xspice/vhdl/ghdl_vpi.c @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2002 Stephen Williams (steve@icarus.com) + * Copyright (c) 2023 Giles Atkinson + * + * This source code is free software; you can redistribute it + * and/or modify it in source code form under the terms of the GNU + * General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +#include +#include +#include +#include +#include +#include + +#if defined(__MINGW32__) || defined(_MSC_VER) +#include // Windows has a simple co-routine library - "Fibers" +#else +#include +#endif + +/* The VVP code runs on its own stack, handled by cr_xxx() functions. */ + +#include "ngspice/coroutine_cosim.h" + +#include "ngspice/cmtypes.h" // For Digital_t +#include "ngspice/cosim.h" + +/* This header file defines external interfaces. It also contains an initial + * comment that describes how this VPI module is used. + */ + +#include "ghdl_shim.h" + +/* Debugging printfs(). */ + +//#define DEBUG +#ifdef DEBUG +#define DBG(...) vpi_printf(__VA_ARGS__) +#else +#define DBG(...) +#endif + +/* This global variable is used only during initialisation and safe + * so long as XSPICE instance initialisation is single-threaded. Ugly! + */ + +__declspec(dllexport) struct ng_ghdl *CTX_VAR; // Name is a macro. + + +static PLI_INT32 next_advance_cb(struct t_cb_data *cb); + +/* Get current simulation time: no module-specific values. */ + +static double get_time(struct ng_ghdl *ctx) +{ + static struct t_vpi_time now = { .type = vpiSimTime }; + uint64_t ticks; + + vpi_get_time(NULL, &now); + ticks = ((uint64_t)now.high << 32) + now.low; + return ticks * ctx->tick_length; +} + +/* Arrange for end_advance_cb() to be called in the future. */ + +static vpiHandle set_stop(uint64_t length, struct ng_ghdl *ctx) +{ + static struct t_vpi_time now; + static struct t_cb_data cbd = { .cb_rtn = next_advance_cb, .time = &now }; + + now.type = vpiSimTime; + now.low = length; + now.high = length >> 32; + if (length == 0) + cbd.reason = cbReadWriteSynch; + else + cbd.reason = cbAfterDelay; + + /* Callback after delay. */ + + cbd.user_data = (PLI_BYTE8 *)ctx; + return vpi_register_cb(&cbd); +} + +/* Timed callback at end of simulation advance: wait for a command + * from the main thread, and schedule the next callback. + * On return, VHDL runs some more. + */ + +static PLI_INT32 next_advance_cb(struct t_cb_data *cb) +{ + struct ng_ghdl *ctx = (struct ng_ghdl *)cb->user_data; + struct t_vpi_value val; + double vl_time; + uint64_t ticks; + unsigned int i; + + for (;;) { + /* Still wanted? */ + + if (ctx->stop) { + vpi_control(vpiFinish, 0); // Returns after scheduling $finish. + return 0; + } + + /* Save base time for next slice. */ + + ctx->base_time = ctx->cosim_context->vtime; + + /* Repeatedly wait for instructions from the main thread + * until GHDL can advance at least one time unit. + */ + + cr_yield_to_spice(&ctx->cr_ctx); + + /* Check for input. */ + + val.format = vpiVectorVal; + i = ctx->ins ? 0 : ctx->outs; + while (ctx->in_pending) { + if (ctx->ports[i].flags & IN_PENDING) { + ctx->ports[i].flags ^= IN_PENDING; + val.value.vector = + (struct t_vpi_vecval *)&ctx->ports[i].previous; + vpi_put_value(ctx->ports[i].handle, &val, NULL, vpiNoDelay); + ctx->in_pending--; + DBG("VPI input %d/%d on %s\n", + val.value.vector->aval, val.value.vector->bval, + vpi_get_str(vpiName, ctx->ports[i].handle)); + } else if (++i == ctx->ins) { + i = ctx->ins + ctx->outs; // Now scan inouts + } + } + + /* How many GHDL ticks to advance? */ + + vl_time = get_time(ctx); + if (ctx->cosim_context->vtime < vl_time) { + /* This can occur legitimately as the two times need not + * align exactly. But it should be less than one SPICE timestep. + */ + + DBG("VHDL time %.16g ahead of SPICE target %.16g\n", + vl_time, ctx->cosim_context->vtime); + if (ctx->cosim_context->vtime + ctx->tick_length < vl_time) { + fprintf(stderr, + "Error: time reversal (%.10g->%.10g) in " + "GHDL shim VPI!\n", + vl_time, ctx->cosim_context->vtime); + } + continue; + } + + ticks = (ctx->cosim_context->vtime - vl_time) / ctx->tick_length; + if (ticks > 0) { + DBG("Advancing from %g (VHDL) to %g (SPICE): %lu ticks\n", + vl_time, ctx->cosim_context->vtime, ticks); + ctx->stop_cb = set_stop(ticks, ctx); + return 0; + } + } +} + +/* Callback function - new output value. */ + +static PLI_INT32 output_cb(struct t_cb_data *cb) +{ + struct ngvp_port *pp = (struct ngvp_port *)cb->user_data; + struct ng_ghdl *ctx = pp->ctx; + + DBG("Output: %s is %d now %g (VHDL) %g (SPICE)\n", + vpi_get_str(vpiName, cb->obj), + cb->value->value.vector->aval, + get_time(ctx), ctx->cosim_context->vtime); + + if (ctx->stop_cb) { + /* First call in the current VHDL cycle: cancel the current + * stop CB and request a new one at the next GHDL time point. + * That allows all output events in the current timestep + * to be gathered before stopping. + */ + + vpi_remove_cb(ctx->stop_cb); + ctx->stop_cb = NULL; + set_stop(0, ctx); + + /* Set the output time in SPICE format. + * It must not be earlier than entry time. + */ + + ctx->cosim_context->vtime = get_time(ctx); + if (ctx->cosim_context->vtime < ctx->base_time) + ctx->cosim_context->vtime = ctx->base_time; + } + + /* Record the value. */ + + pp->new.aval = cb->value->value.vector->aval; + pp->new.bval = cb->value->value.vector->bval; + if (!(pp->flags & OUT_PENDING)) { + pp->flags |= OUT_PENDING; + ++ctx->out_pending; + } + return 0; +} + +/* Utilty functions for start_cb() - initialise or set watch on a variable.*/ + +static void init(vpiHandle handle) +{ + static struct t_vpi_value val = { .format = vpiIntVal }; + + DBG("Initialising %s to 0\n", vpi_get_str(vpiName, handle)); + + vpi_put_value(handle, &val, NULL, vpiNoDelay); +} + +static void watch(vpiHandle handle, void *pp) +{ + static struct t_vpi_time time = { .type = vpiSimTime }; + static struct t_vpi_value val = { .format = vpiVectorVal }; + static struct t_cb_data cb = { + .reason = cbValueChange, .cb_rtn = output_cb, + .time = &time, .value = &val + }; + + cb.obj = handle; + cb.user_data = pp; + vpi_register_cb(&cb); +} + +/* Callback function - simulation is starting. */ + +static PLI_INT32 start_cb(struct t_cb_data *cb) +{ + struct ng_ghdl *ctx = (struct ng_ghdl *)cb->user_data; + vpiHandle iter, top, item; + PLI_INT32 direction; + char *name; + int ii, oi, ioi; + + DBG("Precision %d\n", vpi_get(vpiTimePrecision, NULL)); + ctx->tick_length = pow(10.0, vpi_get(vpiTimePrecision, NULL)); + + /* Find the (unique?) top-level module. */ + + iter = vpi_iterate(vpiModule, NULL); + top = vpi_scan(iter); + vpi_free_object(iter); + DBG("Top %s\n", vpi_get_str(vpiName, top)); + + /* Count the ports. */ + + iter = vpi_iterate(vpiPort, top); + if (!iter) + vpi_printf("Top module has no ports!\n"); // vpi_scan() aborts. + ctx->ins = ctx->outs = ctx->inouts = 0; + while ((item = vpi_scan(iter))) { + direction = vpi_get(vpiDirection, item); + switch (direction) { + case vpiInput: + ++ctx->ins; + break; + case vpiOutput: + ++ctx->outs; + break; + case vpiInout: + ++ctx->inouts; + break; + default: + break; + } + } + ctx->ports = (struct ngvp_port *)malloc( + (ctx->ins + ctx->outs + ctx->inouts) * + sizeof (struct ngvp_port)); + if (!ctx->ports) { + vpi_printf("No memory for ports at " __FILE__ ":%d\n", __LINE__); + abort(); + } + + /* Get the ports. */ + + iter = vpi_iterate(vpiPort, top); + ii = oi = ioi = 0; + while ((item = vpi_scan(iter))) { + struct ngvp_port *pp; + int first; + + direction = vpi_get(vpiDirection, item); + name = vpi_get_str(vpiName, item); + + /* It seems that in GHDL, a port and the underlying net are the + * same object. + */ + + DBG("Port %s direction %d size %d, type %d\n", + name, direction, vpi_get(vpiSize, item), + vpi_get(vpiType, item)); + + switch (direction) { + case vpiInput: + first = !ii; + pp = ctx->ports + ii++; + init(item); + break; + case vpiOutput: + first = !oi; + pp = ctx->ports + ctx->ins + oi++; + watch(item, pp); + break; + case vpiInout: + first = !ioi; + init(item); + pp = ctx->ports + ctx->ins + ctx->outs + ioi++; + watch(item, pp); + break; + default: + continue; + } + pp->bits = vpi_get(vpiSize, item); + pp->flags = 0; + pp->position = first ? 0 : pp[-1].position + pp[-1].bits; + pp->previous.aval = pp->previous.bval = 0; + pp->handle = item; + pp->ctx = ctx; + } + + /* Make a direct call to the "end-of-advance" callback to start running. */ + + cr_init(&ctx->cr_ctx); + cb->user_data = (PLI_BYTE8 *)ctx; + next_advance_cb(cb); + return 0; +} + +/* VPI initialisation. */ + +static void start(void) +{ + static struct t_vpi_time now = { .type = vpiSimTime }; + static struct t_cb_data cbd = { .reason = cbStartOfSimulation, + .time = &now, .cb_rtn = start_cb }; +#ifdef DEBUG + struct t_vpi_vlog_info info; + + /* Get the program name. */ + + if (vpi_get_vlog_info(&info)) { + vpi_printf("Starting vhdlng.vpi in %s\n", info.argv[0]); + for (int i = 0; i < info.argc; ++i) + vpi_printf("%d: %s\n", i, info.argv[i]); + vpi_printf("P: %s V: %s\n", info.product, info.version); + } else { + vpi_printf("Failed to get invocation information.\n"); + } +#endif + + /* The first step is to find the top-level module and query its ports. + * At this point they do not exist, so request a callback once they do. + */ + + cbd.user_data = (PLI_BYTE8 *)CTX_VAR; + vpi_register_cb(&cbd); +} + +/* This is a table of registration functions. It is the external symbol + * that the GHDL simulator looks for when loading this .vpi module. + */ + +void (*vlog_startup_routines[])(void) = { + start, + 0 +}; diff --git a/src/xspice/vhdl/ghnggen b/src/xspice/vhdl/ghnggen new file mode 100644 index 000000000..800340219 --- /dev/null +++ b/src/xspice/vhdl/ghnggen @@ -0,0 +1,188 @@ +*ng_script_with_params +// This Ngspice interpreter script accepts arbitrary arguments to +// the GHDL compiler (VHDL to LLVM) and builds a shared library +// or DLL that can be loaded by the d_cosim XSPICE code model. +// Instances of the model are then digital circuit elements whose +// behaviour is controlled by the Verilog source. + +set bad=0 +if $?argc = 0 + set bad=1 +end + +if $argc <= 0 + set bad=1 +end + +if $bad + echo Arguments acceptable to GHDL are required. + quit +end + +// Disable special processing of '{'. + +set noglob + +if $oscompiled = 2 | $oscompiled = 3 | $oscompiled = 8 // Windows + set windows=1 + set dirsep1="\\" + set dirsep2="/" +else + set windows=0 + set dirsep1="/" + if $oscompiled = 7 // MacOS needs an option to allow undefined symbols. + setcs ld_magic="-Wl,-undefined,dynamic_lookup" + else + set ld_magic="" + end +end + +// Is there a "-top" option first? + +strcmp top "$argv[1]" "-top" +if $top = 0 + if $argc < 3 + echo There must be at least one source file! + quit + end + set base = "$argv[2]" // The top entity name for "ghdl -e". + shift + shift +else + // Loop through the arguments to find GHDL source: some_path/xxxx.vhd + // or similar. The output file will have the same base name. + + let index=1 + set off=1 // Avoid error in dowhile + repeat $argc + set base="$argv[$&index]" + let index = index + 1 + strstr l "$base" "-" // Is it an option? + if $l = 0 + continue + end + + strstr l "$base" "" // Get string length + dowhile $off >= 0 // Strip leading directories + strstr off "$base" "$dirsep1" + if $windows + if $off < 0 + strstr off "$base" "$dirsep2" + end + end + if $off >= 0 + let off=$off+1 + strslice base "$base" $&off $l + end + end + + strstr off "$base" "." // Strip any file type suffix. + if $off >= 0 + strslice base "$base" 0 $off + end + + strstr l "$base" "" // Check for zero-length string + if $l > 0 + break + end + end + + if index - 1 > $argc + echo No file for top-level entity was found. + quit + end +end + +// Default base name of output file. + +if $windows + setcs tail=".DLL" +else + setcs tail=".so" +end +setcs soname="$base$tail" + +// The shared library/DLL contains some ngspice source code as +// well as that created by GHDL. Find it by scanning $sourcepath. + +set shimfile=ghdl_shim.c +set shimobj=ghdl_shim.o +set srcdir=src +set hfile="cmtypes.h" +set hpath="ngspice$dirsep1$hfile" +set silent_fileio // Silences fopen complaints + +let i=1 +repeat $#sourcepath + set stem="$sourcepath[$&i]" + let i = i + 1 + set fn="$stem$dirsep1$shimfile" + fopen fh "$fn" + if $fh < 0 + // Look in any "src" subdirectory (probably in installed tree). + set stem="$stem$dirsep1$srcdir" + set fn="$stem$dirsep1$shimfile" + fopen fh $fn + end + if $fh > 0 + // Found ghdl_shim.c, but it needs header files on relative path. + fclose $fh + set hn="$stem$dirsep1$hpath" + fopen fh "$hn" + if $fh > 0 + break + end + echo Ignoring source file "$fn" as "$hn" was not found. + end +end + +if $fh > 0 + fclose $fh +else + echo Can not find C source file $shimfile + quit +end + +// Some header files are with the source. + +strstr off "$stem" "." +if $off <> 0 + setcs include="-I$stem" +else + setcs include="-I..$dirsep1$stem" // Relative path +end + +// Check for ghdl_shim.o in the current directory. If not present, build it. + +set silent_fileio +fopen fh ghdl_shim.o +if $fh < 0 + shell clang -c -g -fPIC $include -o ghdl_shim.o $stem/ghdl_shim.c +else + fclose $fh +end +unset silent_fileio + +// Compile the VHDL code. + +shell ghdl -a $argv +strcmp bad "$shellstatus" "0" +if $bad = 0 + shell ghdl -e -shared "-Wl,ghdl_shim.o" $base +else + quit +end + +// Check for ghdlng.vpi in the current directory. If not present, build it. + +set silent_fileio +fopen fh ghdlng.vpi +if $fh < 0 + shell ghdl --vpi-compile clang -g -c -fdeclspec -Wno-ignored-attributes $include $stem/ghdl_vpi.c -o ghdl_vpi.o + shell ghdl --vpi-link clang $ld_magic ghdl_vpi.o -o ghdlng.vpi -lm -lpthread +else + fclose $fh +end +unset silent_fileio + +quit diff --git a/visualc/make-install-vngspice.bat b/visualc/make-install-vngspice.bat index 4f37bba02..98b22ed6f 100644 --- a/visualc/make-install-vngspice.bat +++ b/visualc/make-install-vngspice.bat @@ -79,3 +79,7 @@ copy xspice\verilog\*.cpp %dst%\share\ngspice\scripts\src copy include\ngspice\cosim.h %dst%\share\ngspice\scripts\src\ngspice copy include\ngspice\miftypes.h %dst%\share\ngspice\scripts\src\ngspice copy include\ngspice\cmtypes.h %dst%\share\ngspice\scripts\src\ngspice +copy xspice\verilog\coroutine*.h %dst%\share\ngspice\scripts\src\ngspice +copy xspice\vhdl\ghnggen %dst%\share\ngspice\scripts +copy xspice\vhdl\ghdl_shim.* %dst%\share\ngspice\scripts\src +copy xspice\vhdl\ghdl_vpi.c %dst%\share\ngspice\scripts\src diff --git a/visualc/make-install-vngspiced.bat b/visualc/make-install-vngspiced.bat index af00c4f0a..fdfa2e283 100644 --- a/visualc/make-install-vngspiced.bat +++ b/visualc/make-install-vngspiced.bat @@ -80,3 +80,7 @@ copy xspice\verilog\*.cpp %dst%\share\ngspice\scripts\src copy include\ngspice\cosim.h %dst%\share\ngspice\scripts\src\ngspice copy include\ngspice\miftypes.h %dst%\share\ngspice\scripts\src\ngspice copy include\ngspice\cmtypes.h %dst%\share\ngspice\scripts\src\ngspice +copy xspice\verilog\coroutine*.h %dst%\share\ngspice\scripts\src\ngspice +copy xspice\vhdl\ghnggen %dst%\share\ngspice\scripts +copy xspice\vhdl\ghdl_shim.* %dst%\share\ngspice\scripts\src +copy xspice\vhdl\ghdl_vpi.c %dst%\share\ngspice\scripts\src