SkillAgentSearch skills...

Hdl21

Hardware Description Library

Install / Use

/learn @dan-fritchman/Hdl21
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

HDL21

Analog Hardware Description Library in Python

pypi python-versions test codecov

Hdl21 is a hardware description library embedded in Python. It is targeted for analog and custom integrated circuits, and for maximum productivity with minimum fancy-programming skill.

Contents

Installation

pip install hdl21

That's it. No crazy build step, no crazy dependencies, no crazy EDA stuff, no "clone and just modify these 300 things", no sourceing, none of that. Hdl21 is pure Python, and is designed to be as easy to install as any other Python package.

Modules

Hdl21's primary unit of hardware reuse is the Module. Think of it as Verilog's module, or VHDL's entity, or SPICE's subckt. Better yet if you are used to graphical schematics, think of it as the content of a schematic. Hdl21 Modules are containers of a handful of hdl21 types. Think of them as including:

  • Instances of other Modules
  • Connections between them, defined by Signals and Ports
  • Fancy combinations thereof, covered later

An example Module:

import hdl21 as h

m = h.Module(name="MyModule")
m.i = h.Input()
m.o = h.Output(width=8)
m.s = h.Signal()
m.a = AnotherModule()

In addition to the procedural-syntax shown above, Modules can also be defined through a class-based syntax by applying the hdl21.module decorator to a class-definition.

import hdl21 as h

@h.module
class MyModule:
    i = h.Input()
    o = h.Output(width=8)
    s = h.Signal()
    a = AnotherModule()

This class-based syntax produces identical results to the procedural code-block above. Its declarative style can be much more natural and expressive in many contexts, especially for designers familiar with popular HDLs.

Creation of Module signal-attributes is generally performed by the built-in Signal, Port, Input, and Output constructors. Each comes with a "plural version" (Input*s* etc.) which creates several identical objects at once:

import hdl21 as h

@h.module
class MyModule:
    a, b = h.Inputs(2)
    c, d, e = h.Outputs(3, width=16)
    z, y, x, w = h.Signals(4)

Signals

Hdl21's primary connection type is Signal. Think of it as Verilog's wire, or a node in that schematic. Each Signal has an integer-valued bus width field, and can be connected to any other equal-width Port.

A subset of Signals are exposed outside their parent Module. These externally-connectable signals are referred to as Ports. Hdl21 provides four port constructors: Input, Output, Inout, and Port. The last creates a directionless (or direction unspecified) port akin to those of common spice-level languages.

Connections

Popular HDLs generally feature one of two forms of connection semantics. Verilog, VHDL, and most dedicated HDLs use "connect by call" semantics, in which signal-objects are first declared, then passed as function-call-style arguments to instances of other modules.

module my_module();
  logic a, b, c;                              // Declare signals
  another_module i1 (a, b, c);                // Create an instance
  another_module i2 (.a(a), .b(b), .c(c));    // Another instance, connected by-name
endmodule

Chisel, in contrast, uses "connection by assignment" - more literally using the walrus := operator. Instances of child modules are created first, and their ports are directly walrus-connected to one another. No local-signal objects ever need be declared in the instantiating parent module.

class MyModule extends Module {
  // Create Module Instances
  val i1 = Module(new AnotherModule)
  val i2 = Module(new AnotherModule)
  // Wire them directly to one another
  i1.io.a := i2.io.a
  i1.io.b := i2.io.b
  i1.io.c := i2.io.c
}

Each can be more concise and expressive depending on context. Hdl21 Modules support both connect-by-call and connect-by-assignment forms.

Connections by assignment are performed by assigning either a Signal or another instance's Port to an attribute of a Module-Instance.

# Create a module
m = h.Module()
# Create its internal Signals
m.a, m.b, m.c = h.Signals(3)
# Create an Instance
m.i1 = AnotherModule()
# And wire them up
m.i1.a = m.a
m.i1.b = m.b
m.i1.c = m.c

This also works without the parent-module Signals:

# Create a module
m = h.Module()
# Create the Instances
m.i1 = AnotherModule()
m.i2 = AnotherModule()
# And wire them up
m.i1.a = m.i2.a
m.i1.b = m.i2.b
m.i1.c = m.i2.c

Instances can instead be connected by call:

# Create a module
m = h.Module()
# Create the Instances
m.i1 = AnotherModule()
m.i2 = AnotherModule()
# Call one to connect them
m.i1(a=m.i2.a, b=m.i2.b, c=m.i2.c)

These connection-calls can also be performed inline, as the instances are being created.

# Create a module
m = h.Module()
# Create the Instance `i1`
m.i1 = AnotherModule()
# Create another Instance `i2`, and connect to `i1`
m.i2 = AnotherModule(a=m.i1.a, b=m.i1.b, c=m.i1.c)

These methods hides some of what happens under the hood of HDL21 for ease-of-use. A more thorough method of defining objects, especially in Generators seen below, leverage endpoints in the Module and Instance APIs:

h.Module.add is used to add either Signal or Instances instantiated in the usual way and also allows the use of an optional name keyword argument which names the newly added object so it can be accessed using the methods we've already described above.

h.Module.get is used to get the Signal or Instance with a given name from a module via a single argument in string form.

h.Instance.connect takes two arguments, the first a string referring to an Instance's available ports and the second refers to any "connectable" object which can be of the type Signal, PortRef, Slice or Concat.

Slicing

Signal objects are equipped with a width keyword argument, which determines the width of a signal bus. This creates a 1D array that can accessed using Python's usual slicing syntax used with lists:

sig1 = h.Signal(width=12)
sig2 = h.Input(width=6)
# Map sig2 signals to even numbered sig1 signals
sig2 = sig1[::2]

NOTE: the slicing provided works by creating a reference to the underlying signals to be mapped, so at this time can't be used to set connections but only get connections. That is, the following will raise an error:

sig1 = h.Signal(width=12)
sig2 = h.Input(width=6)
# Map sig2 signals to even numbered sig1 signals
sig1[::2] = sig2

Concatenation

Signal's can be concatenated to make wider signal buses that you can use to interface with between buses of variable width. This is done using the Concat command:

a = h.Signal()
b = h.Signal(width=2)

# This is a Concat with two parts
# that is resolved into signal bus
# with a width of 3.
c = h.Concat(a,b)

The Concat command can be used with an arbitrary number of Signals, as well as recursively to create heirarchical Concat structures:

a = h.Signal()
b = h.Signal(width=2)
c = h.Signal(width=3)

# This is a Concat with three parts
# it is resolved to a width-6 bus
d = h.Concat(a,b,c)

# This is a Concat with two parts
# with objects 2-part Concat and c
# it is flattened to the same width-6 bus
d = h.Concat(h.Concat(a,b),c)

Debugging Tips

Each Module has an attribute called ports and signals which store what they are labelled respectively. Taking either of these, you can examine individual Signals to see if they've been correctly connected by checking their individual _slices, _concats and _connected_ports attributes.

Whereas, Instances contain attributes conns which list what objects an Instance's ports are connected to and _refs which keeps track of where PortRefs for a given Instance are being distributed to other Modules and Instances in your program.

Generators

Hdl21 Modules are "plain old data". They require no runtime or execution environment. They can be (and are!) fully represented in markup languages such as ProtoBuf, JSON, and YAML. The power of embedding Modules in a general-purpose programming language lies in allowing code to create and manipulate them. Hdl21's Generators are functions which produce Modules, and have a number of built-in features to aid embedding in a hierarchical hardware tree.

In other words:

  • Modules are "structs". Generators are functions which return Modules.
  • Generators are code. Modules are data.
  • Generators require a runtime environment. Modules do not.

Creating a generator just requires applying the @hdl21.generator decorator to a Python function:

import hdl21 as h

@h.generator
def MyFirstGenerator(params: MyParams) -> h.Module:
    # A very exciting first generator function
    m = h.Module()
    m.i = h.Input(width=params.w)
    return m

The generator-function body can define a Module however it like

Related Skills

View on GitHub
GitHub Stars90
CategoryDevelopment
Updated19d ago
Forks22

Languages

Python

Security Score

95/100

Audited on Mar 16, 2026

No findings