Up to now, SKiDL supported hierarchy by applying the @subcircuit
decorator to a Python function:
@subcircuit
def analog_average(in1, in2, avg):
"""Output the average of the two inputs."""
# Create two 1K resistors.
r1, r2 = 2 * Part('Device', 'R', value='1K', dest=TEMPLATE)
# Each input connects thru a resistor to the avg output.
r1[1,2] += in1, avg
r2[1,2] += in2, avg
Then this subcircuit is instantiated by calling the function with nets passed as arguments:
in1, in2, in3, in4, out1, out2 = Net()*6 # Make some I/O nets.
analog_average(in1, in2, out1) # One instantiation of the averager.
... Some more code ...
analog_average(in3, in4, out2) # A second instantiation.
It was pointed out that this method of instantiating subcircuits is quite different from what is used for parts. Unlike the connections to a subcircuit that are all made at a single place when the function is called, the connections to a part can be placed at various, non-contiguous locations in the code:
q_npn = Part("Device", "Q_NPN_BCE") # Instantiate a transistor.
...
q_npn['E'] += Net('GND') # Connect the emitter to ground.
...
q_npn['B'] += in1 # Connect an input to the base.
...
q_npn['C'] += out1 # Connect the output to the collector.
Another issue is that you can pass a part instance as an argument to a subcircuit that connects it to the internal circuitry. But you can't do the same thing with a subcircuit function without making changes to the code because of the syntactic differences in how connections are made:
@subcircuit
def analog_average(in1, in2, avg, r):
"""Output the average of the two inputs."""
r1, r2 = r(num_copies=2) # Create two copies of the resistor part.
r1[1,2] += in1, avg
r2[1,2] += in2, avg
# If r was a subcircuit function, this would have to be written as:
# r(in1, avg)
# r(in2, avg)
To make subcircuits act more like parts, the @package
decorator has been introduced.
Just replace the @subcircuit
decorator while keeping everything else the same:
@package
def analog_average(in1, in2, avg):
r1, r2 = 2 * Part('Device', 'R', value='1K', dest=TEMPLATE)
r1[1,2] += in1, avg
r2[1,2] += in2, avg
Instantiating the subcircuit now occurs in two phases. First, create instances of the subcircuit wherever they are needed:
avg1 = analog_average()
...
avg2 = analog_average()
In the second phase, make connections to these subcircuits as if they were parts with the names of the function parameters serving as pin names:
in1, in2, in3, in4, out1, out2 = Net()*6
# Make connections. You can use either [] or . to reference the I/O.
avg1['in1'] += in1
avg1.in2 += in2
avg1['avg'] += out1
...
avg2['in1'] += in3
avg2['in2'] += in4
avg2.avg += out2
In addition to nets, pins, and buses, you can pass any other type of
parameter to subcircuits.
For example, analog_average
could take a float as a ratio
parameter to
set the amount each input contributes to the output:
@package
def analog_average(in1, in2, avg, ratio):
r = Part('Device', 'R', dest=TEMPLATE)
r1 = r(value=2000 * ratio)
r2 = r(value=2000 * (1-ratio))
r1[1,2] += in1, avg
r2[1,2] += in2, avg
Then the subcircuit can either be instantiated with a given ratio:
avg1 = analog_average(ratio=0.25)
avg1.in1 += in1
avg1.in2 += in2
avg1.avg += out1
or you can set the ratio outside the function call:
avg2 = analog_average()
avg2.ratio = 0.25 # Use a normal assignment (=) since this is not a circuit connection.
avg2.in1 += in3
avg2.in2 += in4
avg2.avg += out2
A subcircuit function instantiates its circuitry when it is called.
But this doesn't happen when using a package.
Instead, the subcircuit is placed in a list and executed after the
complete circuit is finalized
(i.e., whenever ERC()
or generate_netlist()
is called).
The arguments passed to the function consist of the connections
and other values that were assigned to the package parameters in the preceding code.
That's about it for the @package
decorator.
Since it's new, it hasn't seen a lot of use and there could be unknown bugs
lying in wait.
If you have questions or problems, please ask on the
SKiDL forum or
raise an issue.