Add bidirectional digital/analog bridge.

This commit is contained in:
Giles Atkinson 2022-09-15 17:12:08 +01:00 committed by Holger Vogt
parent 118b997642
commit af09a06cdb
4 changed files with 667 additions and 16 deletions

View File

@ -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;

View File

@ -0,0 +1,446 @@
/* -*- mode: C;-*- (emacs magic)
*
* Simple bidirectional digital/analog bridge.
* Giles Atkinson 2022
*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <math.h>
#include <string.h>
/* 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;
}
}
}
}
}

View File

@ -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

View File

@ -1,5 +1,6 @@
adc_bridge
dac_bridge
bidi_bridge
d_and
d_buffer
d_dff