From af09a06cdb8c61a2dea6d14cdc1f2fc3b059b5c1 Mon Sep 17 00:00:00 2001 From: Giles Atkinson <“gatk555@gmail.com”> Date: Thu, 15 Sep 2022 17:12:08 +0100 Subject: [PATCH] Add bidirectional digital/analog bridge. --- src/xspice/evt/evtcheck_nodes.c | 40 +- src/xspice/icm/digital/bidi_bridge/cfunc.mod | 446 ++++++++++++++++++ src/xspice/icm/digital/bidi_bridge/ifspec.ifs | 196 ++++++++ src/xspice/icm/digital/modpath.lst | 1 + 4 files changed, 667 insertions(+), 16 deletions(-) create mode 100644 src/xspice/icm/digital/bidi_bridge/cfunc.mod create mode 100644 src/xspice/icm/digital/bidi_bridge/ifspec.ifs diff --git a/src/xspice/evt/evtcheck_nodes.c b/src/xspice/evt/evtcheck_nodes.c index 2983d3b1a..7a26b3623 100644 --- a/src/xspice/evt/evtcheck_nodes.c +++ b/src/xspice/evt/evtcheck_nodes.c @@ -123,15 +123,23 @@ for device libraries whose devices are defined by subcircuits. digital nodes where defaults are supplied. For digital the defaults are: - ( ".model auto_adc adc_bridge(in_low = {%g/2} in_high = {%g/2})" + ( ".model auto_adc adc_bridge(in_low = '%g/2' in_high = '%g/2')" "auto_adc%d [ %s ] [ %s ] auto_adc" ) - for a digital input and + for a digital input with ( ".model auto_dac dac_bridge(out_low = 0 out_high = %g)" "auto_dac%d [ %s ] [ %s ] auto_dac" ) - for digital output. + for digital output and + + ( ".model auto_bidi bidi_bridge(out_high=%g in_low='%g/2' in_high='%g/2')" + "auto_bidi%d [ %s ] [ %s ] null auto_bidi" ) + + for a node with a digital inout port or with both inputs and outputs. + Note that single quotes surround expressions to be evaluated during + netlist parsing. They are preferred to braces because braces are stripped + by the "set" command. A non-digital example (real to analogue) is: @@ -589,21 +597,21 @@ static struct bridge *find_bridge(Evt_Node_Info_t *event_node, /* Last and probably most common case, default digital bridges. */ if (!format && event_node->udn_index == 0) { - if (direction == MIF_INOUT) { - return NULL; // Abandon hope, for now. + if (direction == MIF_IN) { + setup = ".model auto_adc adc_bridge(" + "in_low = '%g/2' in_high = '%g/2')"; + format = copy("auto_adc%d [ %s ] [ %s ] auto_adc"); + } else if (direction == MIF_OUT) { // MIF_OUT + setup = ".model auto_dac dac_bridge(" + "out_low = 0 out_high = %g)"; + format = "auto_dac%d [ %s ] [ %s ] auto_dac"; } else { - if (direction == MIF_IN) { - setup = ".model auto_adc adc_bridge(" - "in_low = {%g/2} in_high = {%g/2})", - format = copy("auto_adc%d [ %s ] [ %s ] auto_adc"); - } else { // MIF_OUT - setup = ".model auto_dac dac_bridge(" - "out_low = 0 out_high = %g)"; - format = "auto_dac%d [ %s ] [ %s ] auto_dac"; - } - setup = copy(setup); - format = copy(format); + setup = ".model auto_bidi bidi_bridge(" + "out_high = %g in_low = '%g/2' in_high = '%g/2')"; + format = "auto_bidi%d [ %s ] [ %s ] null auto_bidi"; } + setup = copy(setup); + format = copy(format); } if (!format) return NULL; diff --git a/src/xspice/icm/digital/bidi_bridge/cfunc.mod b/src/xspice/icm/digital/bidi_bridge/cfunc.mod new file mode 100644 index 000000000..6f9371b4a --- /dev/null +++ b/src/xspice/icm/digital/bidi_bridge/cfunc.mod @@ -0,0 +1,446 @@ +/* -*- mode: C;-*- (emacs magic) + * + * Simple bidirectional digital/analog bridge. + * Giles Atkinson 2022 + */ + +#include +#include +#include +#include +#include + +/* These define driver cut-off: use PARAMs? */ + +#define OFF_LOW 0.7 +#define OFF_HIGH 0.3 + +/* Structure for analogue state: input and output. */ + +struct a_data { + Digital_t i; // Previous output drive + double svoc; // Previous scaled open-circuit estimate + double o; // Previous output +}; + +/* Structure for digital state: input and output. */ + +struct d_data { + Digital_t i, n, o; // Input, new output, previous output + double i_changed; // Time of first input change. +}; + +/* Called at end to free memory. */ + +void cm_bidi_bridge(ARGS) +{ + Digital_State_t atod; + struct d_data *d; + struct a_data *a, *olda; + int i, size, dsize, strength, dir_param; + double delta, rise_delay, fall_delay, t_rise, t_fall; + + + size = PORT_SIZE(a); + dsize = PORT_NULL(dir) ? 0 : PORT_SIZE(dir); + + if (INIT) { + /* Allocate storage for previous values on both sides */ + + cm_analog_alloc(0, size * (int) sizeof (struct a_data)); + a = (struct a_data *)cm_analog_get_ptr(0, 1); + cm_event_alloc(0, size * (int) sizeof (struct d_data)); + d = (struct d_data *)cm_event_get_ptr(0, 0); + + for (i = 0; i < size; ++i) { + LOAD(d[i]) = PARAM(input_load); + a[i].o = 0.0; + a[i].i.state = UNKNOWN; + a[i].i.strength = HI_IMPEDANCE; + + /* Force port initialisation on first event call. */ + + d[i].i.state = UNKNOWN; + d[i].i.strength = UNDETERMINED; + d[i].o.state = (Digital_State_t)(UNKNOWN + 1); // Force update + d[i].n.state = UNKNOWN; + d[i].i_changed = -1.0; + OUTPUT_STATE(d[i]) = UNKNOWN; + OUTPUT_STRENGTH(d[i]) = d[i].o.strength = HI_IMPEDANCE; + } + for (i = 0; i < dsize; ++i) + LOAD(dir[i]) = PARAM(input_load); + } + + if (dsize > size) + dsize = size; + strength = PARAM(strength); + dir_param = PARAM(direction); + t_rise = PARAM(t_rise); + t_fall = PARAM(t_fall); + d = (struct d_data *)cm_event_get_ptr(0,0); + + if (CALL_TYPE == ANALOG) { + double in_high, in_low; + double out_high, out_low, drive_high, drive_low, r_high, r_low; + double r_stl, r_sth; + int smooth; // Smoothing level 0-2 (default 0). + + a = (struct a_data *)cm_analog_get_ptr(0,0); + olda = (struct a_data *)cm_analog_get_ptr(0, 1); + in_high = PARAM(in_high); + in_low = PARAM(in_low); + out_high = PARAM(out_high); + out_low = PARAM(out_low); + drive_low = PARAM(drive_low); + r_stl = PARAM(r_stl); + drive_high = PARAM(drive_high); + r_sth = PARAM(r_sth); + r_low = PARAM(r_low); + r_high = PARAM(r_high); + smooth = PARAM(smooth); + + for (i = 0; i < size; ++i) { + double svoc, voc; // Notional open-circuit voltage scaled/actual. + double in, out; + + /* Determine direction. */ + + in = INPUT(a[i]); + atod = (Digital_State_t)dir_param; + if (atod == UNKNOWN) + atod = (i < dsize) ? INPUT_STATE(dir[i]) : UNKNOWN; + + /* Analog to digital. */ + + if (atod != ZERO) { + Digital_State_t new; + + /* Determine digital output state. */ + + if (in_high < in_low) { // Hysteresis + if (in > in_low) + new = ONE; + else if (in < in_high) + new = ZERO; + else + new = d[i].o.state; // Same as before + } else { + if (in < in_low) + new = ZERO; + else if (in > in_high) + new = ONE; + else + new = UNKNOWN; + } + if (new != d[i].n.state) { + /* Output change, schedule EVENT call. */ + + d[i].n.state = new; + cm_event_queue(TIME); + } + } + + /* Digital to analog. */ + + if (atod == ONE) { + out = 0.0; /* AtoD, no analogue output. */ + svoc = 0.5; + } else { + double target, iota, interval[2], range, partial; + int step, step_count; + Digital_t drive, *dp; + + /* Find analogue output current. */ + + if (atod == UNKNOWN) { + /* What is the external drive on the digital node? */ + + drive.state = UNKNOWN; drive.strength = HI_IMPEDANCE; + if (!cm_probe_node(1, (unsigned int)i, &drive)) + cm_message_printf("Can not probe port d[%d].", i); + } else { + drive = *(Digital_t *)INPUT(d[i]); + } + a[i].i = drive; + + /* Has the input changed during this timestep? */ + + iota = (T(0) - T(1)) * 1e-6; // Ignorable + if (T(0) - d[i].i_changed < iota) { + /* Previous input value in force for whole step. */ + + step_count = 1; + step = 0; + interval[0] = T(0) - T(1); + } else if (d[i].i_changed - T(1) < iota) { + /* New input value in force for whole step. + * Includes common no-change case where new == old. + */ + + step_count = 2; + step = 1; + interval[1] = T(0) - T(1); + } else { + /* Calculate both sides of change. */ + + step_count = 2; + step = 0; + interval[0] = d[i].i_changed - T(1); + interval[1] = T(0) - d[i].i_changed; + } + + out = olda[i].o; + svoc = olda[i].svoc; + for (; step < step_count; ++step) { + double change, tv; + double max_high, max_low; + + if (step == 0) { + dp = &olda[i].i; + } else { + dp = &drive; + } + + /* Calculate new value for open-circuit output voltage. */ + + change = T(0) - T(1); + switch (dp->state) { + case ZERO: + svoc -= change / t_fall; + if (svoc < 0.0) + svoc = 0.0; + break; + case ONE: + svoc += change / t_rise; + if (svoc > 1.0) + svoc = 1.0; + break; + default: + if (dp->strength == HI_IMPEDANCE) { + svoc = 0.5; // Keeps compiler happy + } else { + /* Assume both drivers are on. */ + + if (dp->strength == STRONG) { + if (drive_high > drive_low) + tv = 1.0; + else if (drive_high > drive_low) + tv = 0.0; + else + tv = 0.5; + } else { + tv = r_low / (r_low + r_high); + } + if (svoc < tv) { + svoc += change / t_rise; + if (svoc > tv) + svoc = tv; + } else { + svoc -= change / t_fall; + if (svoc < tv) + svoc = tv; + } + } + } + if (smooth > 0) { + cm_smooth_discontinuity(svoc, 0.0, 0.0, 1.0, 1.0, + &voc, &tv); // tv is dummy. + } else { + voc = svoc; + } + + /* Available current depends on svoc (driver cut-off). */ + + max_high = drive_high; + max_high *= (voc - OFF_HIGH) / (1 - OFF_HIGH); + if (max_high < 0.0) + max_high = 0.0; + if (smooth > 1) { + cm_smooth_discontinuity(max_high, 0.0, 0.0, + drive_high, drive_high, + &max_high, &tv); + } + max_low = drive_low * (OFF_LOW - voc) / OFF_LOW; + if (max_low < 0.0) + max_low = 0.0; + if (smooth > 1) { + cm_smooth_discontinuity(max_low, 0.0, 0.0, + drive_low, drive_low, + &max_low, &tv); + } + + /* Convert to voltage. */ + + voc = out_low + (out_high - out_low) * voc; + target = 0.0; + partial = 0.0; + + /* Calculate new value for output current. */ + + switch (dp->strength) { + case STRONG: + range = drive_high + drive_low; + switch (dp->state) { + case ZERO: + target = (in - voc) / r_stl; + partial = 1.0 / r_stl; + break; + case ONE: + target = (in - voc) / r_sth; + partial = 1.0 / r_sth; + break; + case UNKNOWN: + /* Assume both drivers are on. */ + + target = (in - voc) * + ((r_stl + r_sth) / (r_stl * r_sth)); + partial = (r_stl + r_sth) / (r_stl * r_sth); + break; + } + if (target > max_low) { + target = max_low; + partial = 0.0; + } else if (target < -max_high) { + target = -max_high; + partial = 0.0; + } + if (smooth > 2) { + cm_smooth_discontinuity(target, + -max_high, -max_high, + max_low, max_low, + &target, &tv); + } + break; + case RESISTIVE: + case UNDETERMINED: // Who knows? + range = out_high / r_high + out_low / r_low; + switch (dp->state) { + case ZERO: + if (in < voc) { + target = 0.0; + } else { + target = (in - voc) / r_low; + partial = 1.0 / r_low; + } + break; + case ONE: + if (in > voc) { + target = 0.0; + } else { + target = (in - voc) / r_high; + partial = 1.0 / r_high; + } + break; + case UNKNOWN: + if (in < out_low) { + target = (in - out_high) / r_high; + partial = 1.0 / r_high; + } else if (in >= out_high) { + target = (in - out_low) / r_low; + partial = 1.0 / r_low; + } else { + /* Both drivers on. */ + + partial = 1.0 / r_low + 1.0 / r_high; + target = (in - voc ) * partial; + } + break; + } + break; + case HI_IMPEDANCE: + default: + range = fmax(drive_high, drive_low); + target = 0.0; + break; + } + + /* Can the transition complete in available time? */ + + delta = target - out; // Current + delta *= + ((delta > 0) ? t_rise : t_fall) / range; // Time + if (delta < 0) + delta = -delta; + if (delta > interval[step]) { + out += (target - out) * interval[step] / delta; + } else { + /* Transition complete. */ + + out = target; + } + } + if (partial != 0.0) + PARTIAL(a[i], a[i]) = partial; + } + OUTPUT(a[i]) = a[i].o = out; + a[i].svoc = svoc; + } + } else { + /* Digital. */ + + rise_delay = PARAM(rise_delay); + fall_delay = PARAM(fall_delay); + + for (i = 0; i < size; ++i) { + /* Determine direction. */ + + atod = (Digital_State_t)dir_param; + if (atod == UNKNOWN) + atod = (i < dsize) ? INPUT_STATE(dir[i]) : UNKNOWN; + + if (atod != ONE && (INPUT_STATE(d[i]) != d[i].i.state || + INPUT_STRENGTH(d[i]) != d[i].i.strength)) { + double transition; + + /* Digital input changed, request break. */ + + d[i].i = *(Digital_t *)INPUT(d[i]); + cm_analog_set_temp_bkpt(TIME); + d[i].i_changed = TIME; + + /* Do not let big timesteps smear the transition (it could be + * much faster with current output into high impedance. + */ + + transition = (d[i].i.state == ZERO) ? t_fall : + (d[i].i.state == ONE) ? t_rise : fmin(t_rise, t_fall); + cm_analog_set_perm_bkpt(TIME + transition * 1.0001); + } + + /* Check for output change from analogue. */ + + if (atod == ZERO) { + if (d[i].o.strength != HI_IMPEDANCE) { + OUTPUT_STATE(d[i]) = d[i].o.state = UNKNOWN; + OUTPUT_STRENGTH(d[i]) = d[i].o.strength = HI_IMPEDANCE; + OUTPUT_DELAY(d[i]) = fmax(rise_delay, fall_delay); + } else { + OUTPUT_CHANGED(d[i]) = FALSE; + } + } else { + if (d[i].o.state != d[i].n.state) { + double delay; + + OUTPUT_STATE(d[i]) = d[i].o.state = d[i].n.state; + OUTPUT_STRENGTH(d[i]) = d[i].o.strength = strength; + switch (d[i].o.state) { + case ZERO: + delay = fall_delay; + break; + case ONE: + delay = rise_delay; + break; + default: + case UNKNOWN: + delay = fmin(rise_delay, fall_delay); + break; + } + OUTPUT_DELAY(d[i]) = delay; + } else { + OUTPUT_CHANGED(d[i]) = FALSE; + } + } + } + } +} diff --git a/src/xspice/icm/digital/bidi_bridge/ifspec.ifs b/src/xspice/icm/digital/bidi_bridge/ifspec.ifs new file mode 100644 index 000000000..3994104d4 --- /dev/null +++ b/src/xspice/icm/digital/bidi_bridge/ifspec.ifs @@ -0,0 +1,196 @@ +/* +AUTHORS + + 24 Apr 2022 Giles Atkinson + +SUMMARY + + This file contains the interface specification file for the + hybrid bidi_bridge code model, a (potentially) bidirectional + analogue/digital bridge. The device has direction inputs + but if they are not driven it tries to behave like a piece of wire. + A parameter may override any direction inputs. + The analogue ports are type "g" as that is bidirectional. +*/ + +NAME_TABLE: + + +C_Function_Name: cm_bidi_bridge +Spice_Model_Name: bidi_bridge +Description: "bidirectional digital/analog node bridge" + + +PORT_TABLE: + +Port_Name: a d +Description: "analog" "digital in/out" +Direction: inout inout +Default_Type: g d +Allowed_Types: [g, gd] [d] +Vector: yes yes +Vector_Bounds: [1 -] [1 -] +Null_Allowed: no no + +/* The direction of the bridge ports may be controlled by digital inputs. + * If null, or the value is UNKNOWN the bridge will be truly bi-directional. + */ + +PORT_TABLE: + +Port_Name: dir +Description: "direction" +Direction: in +Default_Type: d +Allowed_Types: [d] +Vector: yes +Vector_Bounds: - +Null_Allowed: yes + +/* Alternatively, this parameter sets direction: 0-2 for DAC, ADC, ignore. + * Values 0/1 override the direction port. + */ + +PARAMETER_TABLE: + +Parameter_Name: direction input_load +Description: "force direction" "capacitive input load (F)" +Data_Type: int real +Default_Value: 2 1.0e-12 +Limits: [0 2] - +Vector: no no +Vector_Bounds: - - +Null_Allowed: yes yes + +/* Output strength is 0 (strong, default) or 1 (resistive). + * Smooth controls use of smoothing functions, default is 0 (no smoothing). + */ + +PARAMETER_TABLE: + +Parameter_Name: strength smooth +Description: "output strength" "smoothing level" +Data_Type: int int +Default_Value: 0 0 +Limits: [0 2] [0 2] +Vector: no no +Vector_Bounds: - - +Null_Allowed: yes yes + +/* Analog thresholds, in_low may be greater than in-high, enabling hysteresis. +*/ + +PARAMETER_TABLE: + +Parameter_Name: in_low +Description: "maximum 0-valued analog input" +Data_Type: real +Default_Value: 0.1 +Limits: - +Vector: no +Vector_Bounds: - +Null_Allowed: yes + +PARAMETER_TABLE: + +Parameter_Name: in_high +Description: "minimum 1-valued analog input" +Data_Type: real +Default_Value: 0.9 +Limits: - +Vector: no +Vector_Bounds: - +Null_Allowed: yes + +PARAMETER_TABLE: + +/* Analog maximum and minimum output voltages. */ + +Parameter_Name: out_low +Description: "minimum analog output voltage for 'ZERO' digital input" +Data_Type: real +Default_Value: 0.0 +Limits: - +Vector: no +Vector_Bounds: - +Null_Allowed: yes + +PARAMETER_TABLE: + +Parameter_Name: out_high +Description: "maximum analog output voltage for 'ONE' digital input" +Data_Type: real +Default_Value: 3.3 +Limits: - +Vector: no +Vector_Bounds: - +Null_Allowed: yes + +/* Analog maximum current. */ + +PARAMETER_TABLE: + +Parameter_Name: drive_low drive_high +Description: "max current to ground" "max current to ground" +Data_Type: real real +Default_Value: 0.02 0.02 +Limits: - - +Vector: no no +Vector_Bounds: - - +Null_Allowed: yes yes + +/* Strong analog output cuts off smoothly at the voltage limits. + * Let vth = out_high - r_sth * drive_high. + * Then for input voltage v, with drive_high > v > vth, + * the maximum output current is (drive_high - v) / r_sth + */ + +PARAMETER_TABLE: + +Parameter_Name: r_stl r_sth +Description: "low taper resistance" "high taper resistance" +Data_Type: real real +Default_Value: 20 20 +Limits: [1e-6 -] [1e-6 -] +Vector: no no +Vector_Bounds: - - +Null_Allowed: yes yes + +/* Resistive analog drive. */ + +PARAMETER_TABLE: + +Parameter_Name: r_low r_high +Description: "drive resistor to ground" "drive resistor to out_high" +Data_Type: real real +Default_Value: 10000 10000 +Limits: [1e-6 -] [1e-6 -] +Vector: no no +Vector_Bounds: - - +Null_Allowed: yes yes + +/* Analog rise and fall times. */ + +PARAMETER_TABLE: + +Parameter_Name: t_rise t_fall +Description: "rise time 0 -> 1" "fall time 1 -> 0" +Data_Type: real real +Default_Value: 1.0e-9 1.0e-9 +Limits: [1e-12 -] [1e-12 -] +Vector: no no +Vector_Bounds: - - +Null_Allowed: yes yes + +/* Digital rise and fall delays. */ + +PARAMETER_TABLE: + +Parameter_Name: rise_delay fall_delay +Description: "rise delay 0 -> 1" "fall delay 1 -> 0" +Data_Type: real real +Default_Value: 1.0e-9 1.0e-9 +Limits: [1e-12 -] [1e-12 -] +Vector: no no +Vector_Bounds: - - +Null_Allowed: yes yes diff --git a/src/xspice/icm/digital/modpath.lst b/src/xspice/icm/digital/modpath.lst index ab1fa4b6c..3b5ee09f5 100644 --- a/src/xspice/icm/digital/modpath.lst +++ b/src/xspice/icm/digital/modpath.lst @@ -1,5 +1,6 @@ adc_bridge dac_bridge +bidi_bridge d_and d_buffer d_dff