In this section, we build a graphical-schematic-defined HX711-based load cell amplifier subcircuit block and add it to our board.
This section describes how to define a block in KiCad's graphical schematic editor and import it into an HDL flow. Blocks can also be defined in HDL using the same constructs used to define a board, which is covered in the next section.
While defining blocks in HDL provides the most programming power, schematics can be a better option for blocks with complex connectivity or where the graphical arrangement is meaningful, in particular analog subcircuits. Graphical schematics may also be a more familiar interface and may be a good choice where programmatic generation is not necessary.
Schematic-defined blocks can make use of HDL library blocks (as schematic components), including all the automation (like automatic parts selection from a parts table) those libraries provide.
Start by drawing the application schematic for the HX711, as described in Figure 4 of its datasheet.
For common parts like capacitors, resistors, and transistors, we use the generic built-in KiCad symbols, which ultimately map down to HDL library blocks with parts selection automation. A full list is available in the reference section. Parts where there isn't a corresponding library block can instead be defined with a footprint and pinning (like the HX711 chip here).
Hierarchical labels are used to define the block's boundary ports (electrical interface).
Our finished schematic looks like this and is available in examples/resources/Hx711.sch:
A few notes here:
- Labels like GND and VCC work as expected within this block.
- True global labels (which would directly connect to the rest of the design, not through the block's boundary ports) are not supported.
- Components mapping down to parameterized library blocks (like resistors and capacitors) must be defined with a value. See the reference section for details on formatting.
While the schematic defines the implementation, an HDL Block wrapper class is still required to interoperate with the rest of the system.
Start by creating a empty KiCadSchematicBlock
Block class:
+ class Hx711(KiCadSchematicBlock):
+ def __init__(self) -> None:
+ super().__init__()
+ # block boundary (ports, parameters) definition here
+
+ def contents(self) -> None:
+ super().contents()
+ # block implementation (subblocks, internal connections, footprint) here
KiCadSchematicBlock
is aBlock
that is defined by a KiCad schematic.
Then, define the ports in __init__(...)
, which must have the same name as the hierarchical labels:
class Hx711(KiCadSchematicBlock):
def __init__(self) -> None:
super().__init__()
+
+ self.pwr = self.Port(VoltageSink.empty(), [Power])
+ self.gnd = self.Port(Ground.empty(), [Common])
+
+ self.dout = self.Port(DigitalSource.empty())
+ self.sck = self.Port(DigitalSink.empty())
+
+ self.ep = self.Port(Passive.empty())
+ self.en = self.Port(Passive.empty())
+ self.sp = self.Port(Passive.empty())
+ self.sn = self.Port(Passive.empty())
def contents(self) -> None:
super().contents()
# block implementation (subblocks, internal connections, footprint) here
Like the top-level board, the contents of a Block can be defined in
def contents(...)
. However, interfaces (like boundary ports and constructor parameters) must be defined indef __init__(...)
.
In the HDL model, ports must have a type.
Ground
,VoltageSink
,DigitalSource
, andDigitalSink
are typed ports that have electronics modeling (e.g. voltage limits forVoltageSink
and IO thresholds forDigitalSink
).Passive
represents a port with no electronics modeling and can be connected to any otherPassive
port. Unlike schematic ERC,Passive
cannot be directly connected to a typed port and requires an adaptor (described later).As these are intermediate ports (they connect to internal ports, here in the schematic), they must be
.empty()
to not define parameters like voltage limits which will be inferred from internal connections.
Then, import the schematic:
class Hx711(KiCadSchematicBlock):
def __init__(self) -> None:
super().__init__()
...
def contents(self) -> None:
super().contents()
+
+ self.import_kicad("path/to/your/hx711.sch", auto_adapt=True)
Many of the symbols in KiCad map to library Blocks with ports that are Passive-typed. The
auto_adapt
argument inimport_kicad
automatically adds adapters from Passive to (for example) VoltageSink and DigitalSource at these interfaces. These automatically-generated adapters produce ideal ports (for example, a VoltageSink with infinite voltage limits) and are great to get a quick-and-dirty schematic out fast, but do not enable the electronics model to check for correctness.
KiCadSchematicBlock
provides afile_path
method which allows paths to be specified relative to the Python file. This takes a variable number of arguments, each as a path component under the Python file's folder.In the core libraries and examples, we typically put imported schematics in the
resources
folder and name the schematic consistently with the class, which is then imported as:self.file_path("resources", f"{self.__class__.__name__}.kicad_sch")
Finally, while the capacitors and resistor parameters can be parsed by value, the transistor is more complex and must be instantiated separately in the HDL. Instantiate a BJT in contents(...), making sure the name matches with the schematic refdes so the importer recognizes both as the same component. Make sure the schematic symbol has no value.
class Hx711(KiCadSchematicBlock):
def __init__(self) -> None:
super().__init__()
...
def contents(self) -> None:
super().contents()
+ self.Q1 = self.Block(Bjt.Npn((0, 5)*Volt, 0*Amp(tol=0)))
self.import_kicad("path/to/your/hx711.sch", auto_adapt=True)
Overall, there are four ways to define a component in this schematic import process:
- Value parsing (like with resistors and capacitors), where the importer has an associated library Block for that particular symbol: the particular library Block is created and parses the value string to determine parameters. Typically, these are Passive-typed.
- HDL instantiation (like with the BJT), where the symbol has neither footprint or value, but a Block whose name matches the symbol refdes and with a defined symbol to port mapping: the existing Block is connected as described in the schematic.
- Inline HDL (not shown, but could be done with the BJT), where a Block's value contains HDL code prefixed with a
#
, and the resulting Block defines a symbol to port mapping. For the BJT, instead of instantiating the Block incontents(...)
, you could instead have set the value in the schematic toBjt.Npn((0, 5)*Volt, 0*Amp(tol=0))
.- Blackboxing (like with the HX711 chip), where the symbol has a footprint defined: a Block is created with all Passive ports.
See the reference section below for KiCad symbols for value parsing, HDL instantiation, or inline HDL.
With a fully defined library Block, you can now add it to your board:
class BlinkyExample(SimpleBoardTop):
def contents(self) -> None:
super().contents()
...
with self.implicit_connect(
ImplicitConnect(self.reg.pwr_out, [Power]),
ImplicitConnect(self.reg.gnd, [Common]),
) as imp:
self.mcu = imp.Block(IoController())
...
+ self.conn = self.Block(PassiveConnector(4))
+ self.sense = imp.Block(Hx711())
+ self.connect(self.mcu.gpio.request('hx711_dout'), self.sense.dout)
+ self.connect(self.mcu.gpio.request('hx711_sck'), self.sense.sck)
+ self.connect(self.conn.pins.request('1'), self.sense.ep)
+ self.connect(self.conn.pins.request('2'), self.sense.en)
+ self.connect(self.conn.pins.request('3'), self.sense.sp)
+ self.connect(self.conn.pins.request('4'), self.sense.sn)
The new Block should also show up in the library browser and can be added with graphical actions in the IDE. A recompile may be needed for the library browser to index the new Block.
While a schematic-defined Block allows subcircuit definition quickly and easily, we didn't model the device's electrical characteristics like pin voltage limitations, and as a result the automated checks would not be able to help here.
However, instead of using auto_adapt
, we can instead define conversions
on a symbol pin or boundary port basis.
Add these conversions:
class Hx711(KiCadSchematicBlock):
def __init__(self) -> None:
super().__init__()
...
def contents(self) -> None:
super().contents()
self.Q1 = self.Block(Bjt.Npn((0, 5)*Volt, 0*Amp(tol=0)))
- self.import_kicad("path/to/your/hx711.sch", auto_adapt=True)
+ self.import_kicad("path/to/your/hx711.sch",
+ conversions={
+ 'pwr': VoltageSink(
+ voltage_limits=(2.6, 5.5)*Volt,
+ current_draw=(0.3 + 0.2, 1400 + 100)*uAmp),
+ 'gnd': Ground(),
+ 'dout': DigitalSource.from_supply(self.gnd, self.pwr),
+ 'sck': DigitalSink.from_supply(self.gnd, self.pwr),
+ })
conversions
allows a port model to be specified on either a symbol pin or boundary port, and is defined as a dict from the pin name to the port model. If specified on a symbol pin, the adapter is inserted inline at the symbol pin; and if specified on a boundary port, the adapter is inserted before the boundary port, with the symbol pins internally connected as Passive.Port models can be fully or partially specified. For example, the
pwr
VoltageSink is fully specified with bothvoltage_limits
andcurrent_draw
, while thesck
DigitalSink is only partially specified and missing its digital input thresholds which are not defined in the datasheet. By convention, unspecified fields default to ideal models: for example, a missingvoltage_limits
would mean infinite voltage tolerance.
conversions
can be used in conjunction withauto_adapt
, withconversions
taking priority:self.import_kicad("path/to/your/hx711.sch", conversions={...}, auto_adapt=True)
These common symbols can be used in schematic import and map to the following passive-typed HDL blocks:
Symbol | HDL Block | Value Parsing | Notes |
---|---|---|---|
Device:C, Device:C_Polarized | Capacitor | e.g. 10uF 10V |
Voltage rating must be specified |
Device:R | Resistor | e.g. 100 |
|
Device:L | Inductor | ||
Device:Q_NPN_*, Device:Q_PNP_* | Bjt.Npn, Bjt.Pnp | ||
Device:D | Diode | ||
Device:D_Zener | ZenerDiode | ||
Device:L_Ferrite | FerriteBead | ||
Device:Q_NMOS_*, Device:Q_PMOS_* | Fet.NFet, Fet.PFet | ||
Switch:SW_SPST | Switch |
Notes:
- Blocks are Passive-typed unless otherwise noted.
- If Value Parsing is empty, the Block can only be defined by HDL instantiation or inline HDL.
- All Blocks that can be used in schematic import can be found by searching for all subclasses of
KiCadImportableBlock
. - In many cases, the
_Small
(likeDevice:C_Small
) symbol can also be used.
These higher-level symbols have typed pins (like VoltageSink, Ground, and AnalogSink) and can be used to make higher-level analog signal chains:
Symbol | HDL Block | Notes |
---|---|---|
Simulation_SPICE:OPAMP | Opamp | Supports value parsing (with empty value); is the full application circuit for an opamp (including decoupling capacitors) |
edg_importable:Amplifier | Amplifier | |
edg_importable:DifferentialAmplifier | DifferentialAmplifier | |
edg_importable:IntegratorInverting | IntegratorInverting | |
edg_importable:OpampCurrentSensor | OpampCurrentSensor |
Continue to the next part of the tutorial on defining a library Block in pure HDL.
If you want to see some more complex examples of schematic-defined Blocks, check out:
- The FET power gate schematic and FetPowerGate stub class, a power button that turns on a FET which can then be latched on by a microcontroller.
- The priority power OR schematic and PriorityPowerOr stub class, a diode-FET circuit that merges two voltage sources with priority.
- The source measure unit analog chain schematic and SourceMeasureControl stub class, a higher-level schematic that makes use of the opamp building blocks.
- The differential amplifier schematic and DifferentialAmplifier stub class, a standard opamp circuit that mixes schematic definition for the connectivity and generator code to compute resistor values.