Our Expected Result in KiCad
If we pull this off with Zener, it should end up looking like the KiCad schematic below! You can follow along with the example GitHub directory.
The first thing to do is import our dependencies. Diode kindly provides a few generics in the standard library that are very helpful for each of our physical components. Notably, our L Network uses an SMA connector, resistor, capacitor, inductor, ground, and antenna schematic symbol. We can easily import each of these schematics using the built-in Module()
or load()
function, coupled with the default package aliases, if the module is already available in the standard library.
However, for both our SMA connector and the antenna, the standard library does not provide a module, so we will need to create a module for each component ourselves.
Component or Module?
If you check the Zener module specification, you’ll see the following:
Each .zen
file is a python module. It can be used in two ways:
-
Its exported symbols can be
load()
ed into other modules. For example, load("./MyFile.zen", "MyFunction", "MyType")
will load the MyFunction
and MyType
symbols from the MyFile.zen
module.
-
It can be loaded as a schematic module using the
Module()
helper. For example, MyFile = Module("./MyFile.zen")
will import MyFile.zen
as a schematic module, which you can instantiate like so:
MyFile = Module("./MyFile.zen")
MyFile(
name = "MyFile",
...
)
In short, a module is defined by a .zen
file that declares its inputs and creates components. So, with this knowledge, let’s define a component for our SMA connector and antenna.
The .zen
file in which we write our component is technically a module. However, since a component must be defined within a .zen
file, we will consider any file that specifies a component using the Component()
syntax to be a component itself.
I’ll first make a components
directory in the root directory, where we can put both of our custom components.
It is convention to put the component’s .zen
file inside of another directory with the same component’s name
Components
Now, if we think logically about a connector, we need one input and one output. What better construct than an io()
declaration! Before we define our io()
s, let’s first import a ground signal, which is require for the SMA connector to work. Then, let’s define our two io
s for the connector, which by convention will be called In
and Ext
with a type of net:
load("@stdlib/interfaces.zen", "Ground")
# Declare io
In = io("In", Net) # Convention to name the variable and net the same
Ext = io("Ext", Net)
Finally, we’ll define a component for our SMA Connector. A component requires a name, footprint, symbol, and pins (which are defined by the symbol). Let’s get our schematic symbol from KiCad. If we double-click on the schematic symbol in KiCad, we can see a Library link: Connector:Conn_Coaxial_Small
in the bottom left. Then, we can import the symbol with Zener (notice how we append .kicad_sym
). We can do something very similarly for the footprint by looking around in ~/Library/Caches/pcb/gitlab/kicad/libraries/kicad-footprints/...
. Last but not least, the standard library provides some layout functionality, so we can see a preview of the layout.
load("@stdlib/properties.zen", "Layout") # Import layout functions from the standard library
Component(
name = "SMA_Connector",
footprint = File("@kicad-footprints/Connector_Coaxial.pretty/SMA_Amphenol_132134_Vertical.kicad_mod"),
symbol = Symbol("@kicad-symbols/Connector.kicad_sym:Conn_Coaxial_Small"),
pins = {
"In": In,
"Ext": Ext
}
)
Layout(name = "SMA_Connector", path = "build/preview") # Don't use spaces in the name
Hovering over the pins in a component definition will tell you all the pins Zener expects you to define.
Let’s quickly take a look at the whole file before we move on.
load("@stdlib/interfaces.zen", "Ground")
load("@stdlib/properties.zen", "Layout")
In = io("In", Net)
Ext = io("Ext", Net)
Component(
name = "SMA_Connector",
footprint = File("@kicad-footprints/Connector_Coaxial.pretty/SMA_Amphenol_132134_Vertical.kicad_mod"),
symbol = Symbol("@kicad-symbols/Connector.kicad_sym:Conn_Coaxial_Small"),
pins = {
"In": In,
"Ext": Ext
}
)
Layout(name = "SMA_Connector", path = "build/preview")
And the generated output, when clicking on the pcb: View Schematic
button in VSCode:
Pretty close! We might also want to change the prefix from U1 to J1 when we initialize the component. We can improve our SMAConnector.zen
file by adding a prefix field to the Component()
function and using a config
.
load("@stdlib/interfaces.zen", "Ground")
load("@stdlib/properties.zen", "Layout")
In = io("In", Net)
Ext = io("Ext", Net)
prefix = config("prefix", str, default="J") # Add prefix from config
Component(
name = "SMA_Connector",
footprint = File("@kicad-footprints/Connector_Coaxial.pretty/SMA_Amphenol_132134_Vertical.kicad_mod"),
symbol = Symbol("@kicad-symbols/Connector.kicad_sym:Conn_Coaxial_Small"),
prefix = prefix, # Initialize component with prefix
pins = {
"In": In,
"Ext": Ext
}
)
Layout(name = "SMA_Connector", path = "build/preview")
Even better!
Similarly, for the antenna:
load("@stdlib/properties.zen", "Layout")
A = io("A", Net)
prefix = config("prefix", str, default="AE")
Component(
name = "Antenna",
footprint = File("@kicad-footprints/RF_Antenna.pretty/Texas_SWRA416_868MHz_915MHz.kicad_mod"),
prefix = prefix,
symbol = Symbol("@kicad-symbols/Device.kicad_sym:Antenna"),
pins = {
"A": A,
}
)
Layout(name = "Antenna", path = "build/preview")
Modules
While we could just instantiate a resistor, capacitor, inductor, and ground together on our PCB, it would make more sense for our matching network to be a module we can instantiate as a single unit. So, let’s make a seperate module for our matching network!
As per usual, let’s import the necessary modules we need from the standard library:
load("@stdlib/interfaces.zen", "Ground")
load("@stdlib/properties.zen", "Layout")
Then, we can import our specific components for the matching network:
Capacitor = Module("@stdlib/generics/Capacitor.zen")
Inductor = Module("@stdlib/generics/Inductor.zen")
Resistor = Module("@stdlib/generics/Resistor.zen")
Let’s also define our io()
s:
io_IN = io("io_IN", Net) # Using "io_" is convention for io()s in modules
io_OUT = io("io_OUT", Net)
io_GND = io("io_GND", Ground)
Finally, we will use Net()
s for internal wires/signals:
L_IN = Net("L_IN")
T = Net("T")
io()
s are for signals/wires that need to be accessed by other components or modules, while Net()
s should be used for internal signals/wires that should not be exposed to external components or modules
Now, we can define the connections between our different components for the module by using our io()
s and Net()
s we just defined:
Inductor(name = "L1", value = "10nH", package = "0603", P1 = io_IN, P2 = T) # Using placeholder values for value and package
Capacitor(name = "C1", value = "10pF", package = "0603", P1 = T, P2 = io_GND)
Resistor(name = "R1", value = "100ohm", package = "0603", P1 = T, P2 = io_OUT)
Finally, we’ll add a Layout()
function, so the user can view the layout of the module in KiCad:
Layout(name = "MatchingNetwork", path = "layout/MatchingNetwork")
Let’s take a look at the whole module before making our PCB:
load("@stdlib/interfaces.zen", "Ground")
load("@stdlib/properties.zen", "Layout")
Capacitor = Module("@stdlib/generics/Capacitor.zen")
Inductor = Module("@stdlib/generics/Inductor.zen")
Resistor = Module("@stdlib/generics/Resistor.zen")
io_IN = io("io_IN", Net)
io_OUT = io("io_OUT", Net)
io_GND = io("io_GND", Ground)
L_IN = Net("L_IN")
T = Net("T")
Inductor(name = "L1", value = "10nH", package = "0603", P1 = io_IN, P2 = T)
Capacitor(name = "C1", value = "10pF", package = "0603", P1 = T, P2 = io_GND)
Resistor(name = "R1", value = "100ohm", package = "0603", P1 = T, P2 = io_OUT)
Layout(name = "MatchingNetwork", path = "layout/MatchingNetwork")
PCB
As we’ve done for both our custom components and modules, let’s import the necessary standard library functions into EX0001.zen
:
load("@stdlib/interfaces.zen", "Ground")
load("@stdlib/properties.zen", "Layout")
Let’s import our custom modules and components:
MatchingNetwork = Module("//modules/MatchingNetwork.zen")
Antenna = Module("//components/Antenna/Antenna.zen")
SMA_Connector = Module("//components/SMAConnector/SMAConnector.zen")
We should also define our Net()
s:
IN = Net("IN")
OUT = Net("OUT")
GND = Ground("GND") # Ground is a special type of Net
Finally, our connections as well as the layout:
Antenna(name = "Antenna", A = OUT)
MatchingNetwork(name = "MatchingNetwork", io_IN = IN, io_OUT = OUT, io_GND = GND)
SMA_Connector(name = "SMA_Connector", In = IN, Ext = GND)
Layout(name = "Antenna_LRC_Matcher", path = "build/preview")
After rotating and moving the rendered schematic components, just as you would with KiCad, here is the result:
You’ve created a functional schematic with Zener!
Bonus
We can also programmatically add mounting holes with Zener:
load("@stdlib/interfaces.zen", "Ground")
load("@stdlib/properties.zen", "Layout")
MatchingNetwork = Module("//modules/MatchingNetwork.zen")
Antenna = Module("//components/Antenna/Antenna.zen")
SMA_Connector = Module("//components/SMAConnector/SMAConnector.zen")
IN = Net("IN")
OUT = Net("OUT")
GND = Ground("GND")
Antenna(name = "Antenna", A = OUT)
MatchingNetwork(name = "MatchingNetwork", io_IN = IN, io_OUT = OUT, io_GND = GND)
SMA_Connector(name = "SMA_Connector", In = IN, Ext = GND)
MountingHole = Module("@stdlib/generics/MountingHole.zen") # Import mounting holes from the standard library
for i in range(4): # Generate four mounting holes
MountingHole(name = "H" + str(i), diameter = "M2")
Layout(name = "Antenna_LRC_Matcher", path = "build/preview")
We can then finish up the layout and routing inside of KiCad:
