Sweetening SKiDL

I've added a bit of syntactic sugar to SKiDL over the past few months:

It doesn't change what SKiDL does, but does make it easier to do it.

Series, Parallel, and Tee Network Constructors

Last year, I had a discussion with kasbah on the SKiDL Github about his suggestion to overload the >> operator to wire two-pin parts in series. A lot of good ideas came out of that, but in the end we thought it had limited use and dropped it.

Fast forward to a few months ago when I was working on a project to use genetic algorithms for optimizing a power supply. One of the tasks was to programmatically create series and parallel combinations of two-pin components. By looking at the connection of two-pin components in serial or parallel as the creation of a two-pin network, a concise syntax arose to create circuits much more complicated than simple series connections.

To support this idea in SKiDL, I overloaded the bitwise AND (&) and OR (|) operators to connect two-pin components in series or parallel, respectively. For example, here is a network of four series resistors using the standard SKiDL syntax:

r1, r2, r3, r4 = Part('Device', 'R', dest=TEMPLATE) * 4
r1[2] += r2[1]
r2[2] += r3[1]
r3[2] += r4[1]

This is the same thing using the new & operator:

r1, r2, r3, r4 = Part('Device', 'R', dest=TEMPLATE) * 4
ser_ntwk = r1 & r2 & r3 & r4

Serial Network

Here are four resistors wired in parallel:

par_ntwk = r1 | r2 | r3 | r4

Parallel Network

Or you can do something like placing pairs of resistors in series and then paralleling those combinations like this:

combo_ntwk = (r1 & r2) | (r3 & r4)

Parallel+Serial Network

In addition to connecting parts, the & and | operators also work with nets. This lets you apply inputs and extract outputs by attaching nets to nodes in the network. To illustrate, here's a simple resistor network that divides an input voltage to generate an output voltage:

vin, vout, gnd = Net('VI'), Net('VO'), Net('GND')
vdiv_ntwk = vin & r1 & vout & r2 & gnd

Voltage-Divider Network

You could do the same thing using a single pin instead of a net. Here's the voltage divider attached directly to a pin of a microcontroller:

pic10 = Part('MCU_Microchip_PIC10', 'PIC10F320-IP')
pin_ntwk = vin & r1 & pic10['RA3'] & r2 & gnd

Voltage-Divider Network for Microcontroller

The examples above work with non-polarized components, but what about polarized parts like diodes and electrolytic capacitors? For those, you have to specify the pins explicitly with the first pin connected to the preceding part and the second pin to the following part like so:

vcc = Net('VCC')
d1 = Part('Device', 'D')
polar_ntwk = vcc & r1 & d1['A,K'] & gnd  # Diode anode connected to resistor and cathode to ground.

Polar network

Explicitly listing the pins also lets you use multi-pin parts with networks. For example, here's an NPN-transistor amplifier built using two networks: one for the carrying the amplified current through the load resistor and the transistor's collector and emitter, and another for applying the input to the base.

inp, outp = Net('INPUT'), Net('OUTPUT')
q1 = Part('Device', 'Q_NPN_ECB')
ntwk_ce = vcc & r1 & outp & q1['C,E'] & gnd  # Connect net outp to the junction of the resistor and transistor collector.
ntwk_b = inp & r2 & q1['B']  # Connect net inp to the resistor driving the transistor base.

Transistor Amplifier

Not all networks are composed of parts in series or parallel. For example, here's a Pi matching network:

Pi Matching Network

This could be described using the tee() function like so:

inp, outp, gnd = Net('INPUT'), Net('OUTPUT'), Net('GND')
l1 = Part('Device', 'L')
c1, c2 = Part('Device', 'C', dest=TEMPLATE) * 2
pi_ntwk = inp & tee(c1 & gnd) & l1 & tee(c2 & gnd) & outp

The tee function takes any network as its argument and returns the first node of that network to be connected into the higher-level network. The network passed to tee can be arbitrarily complex, including any combination of parts, &'s, |'s, and tee's.

Bussed Part Pins

Some parts have sequentially-numbered sets of pins, such as a RAM's address and data buses. Previously, you had to explicitly list these pins to make connections like this:

>>> databus = Bus('DATA', 8)
>>> ram = Part('Memory_RAM','AS6C1616')
>>> ram['DQ7,DQ6,DQ5,DQ4,DQ3,DQ2,DQ1,DQ0'] += databus[7:0]
>>> databus
DATA:
        DATA0: Pin U1/29/DQ0/BIDIRECTIONAL
        DATA1: Pin U1/31/DQ1/BIDIRECTIONAL
        DATA2: Pin U1/33/DQ2/BIDIRECTIONAL
        DATA3: Pin U1/35/DQ3/BIDIRECTIONAL
        DATA4: Pin U1/38/DQ4/BIDIRECTIONAL
        DATA5: Pin U1/40/DQ5/BIDIRECTIONAL
        DATA6: Pin U1/42/DQ6/BIDIRECTIONAL
        DATA7: Pin U1/44/DQ7/BIDIRECTIONAL

This is functional but tedious for large buses, so I introduced a more compact notation to do the same thing:

>>> ram['DQ[7:0]'] += databus[7:0]
>>> databus
DATA:
        DATA0: Pin U1/29/DQ0/BIDIRECTIONAL
        DATA1: Pin U1/31/DQ1/BIDIRECTIONAL
        DATA2: Pin U1/33/DQ2/BIDIRECTIONAL
        DATA3: Pin U1/35/DQ3/BIDIRECTIONAL
        DATA4: Pin U1/38/DQ4/BIDIRECTIONAL
        DATA5: Pin U1/40/DQ5/BIDIRECTIONAL
        DATA6: Pin U1/42/DQ6/BIDIRECTIONAL
        DATA7: Pin U1/44/DQ7/BIDIRECTIONAL

Accessing Part Pins as Attributes

The standard syntax for accessing a part pin uses array index notation like this:

>>> ram['DQ0'] += databus[0]

In order to slim this down, part pins can now also be referenced using their names as attributes:

>>> ram.DQ0 += databus[0]

Note that this works as long as the pin name is a legal attribute name (i.e., it begins with an alpha character and contains only alphanumeric characters and the underscore). If it's not, you'll have to use array indexing.

You can also use attribute references with pin numbers by prefixing the number with p:

>>> r = Part("Device", 'R')
>>> r

 R (): Resistor
    Pin R1/1/~/PASSIVE
    Pin R1/2/~/PASSIVE

>>> vcc = Net('VCC')
>>> r.p1 += vcc
>>> vcc
VCC: Pin R1/1/~/PASSIVE