ChipFlow Architecture Overview¶
This guide explains the overall architecture of ChipFlow and how different components work together to transform your Python hardware design into manufacturable silicon.
High-Level Overview¶
ChipFlow follows a multi-stage flow from Python design to silicon:
┌─────────────────┐
│ Python Design │ Your Amaranth HDL design with ChipFlow signatures
│ (design.py) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Elaboration │ Amaranth converts to Fragment tree
│ │ ChipFlow annotations attached
└────────┬────────┘
│
▼
┌─────────────────┐
│ RTLIL │ Intermediate representation with annotations
│ (design.rtlil) │ JSON schemas embedded as attributes
└────────┬────────┘
│
├─────────────┬────────────────┬──────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Silicon │ │ Simulation │ │ Software │ │ Board │
│ Platform │ │ Platform │ │ Platform │ │ Platform │
└────────────┘ └──────────────┘ └──────────┘ └──────────┘
│ │ │ │
▼ ▼ ▼ ▼
GDS-II CXXRTL C++ soc.h + .elf Bitstream
Core Components¶
ChipFlow consists of several key subsystems that work together:
Pin Signatures - Define external interfaces (UART, GPIO, SPI, etc.)
Annotation System - Attach metadata to designs for platform consumption
Package Definitions - Map abstract ports to physical pins
Platforms - Transform RTLIL to target-specific outputs
Steps - Orchestrate the build process via CLI commands
Configuration - TOML-based project configuration
Design Flow in Detail¶
1. User Defines Design¶
You write your design in Python using Amaranth HDL and ChipFlow signatures:
class MySoC(wiring.Component):
def __init__(self):
super().__init__({
"uart": Out(UARTSignature()),
"gpio": Out(GPIOSignature(pin_count=8)),
})
def elaborate(self, platform):
m = Module()
# Your design logic here
return m
# Verify the design can be instantiated
design = MySoC()
assert hasattr(design, 'uart')
assert hasattr(design, 'gpio')
2. Signatures Add Metadata¶
ChipFlow signatures are decorated with @amaranth_annotate which adds JSON schema metadata:
IOModel: I/O configuration for external interfaces of the IC (direction, width, drive modes, trip points)
SimInterface: Interface type identification for matching simulation models (UID, parameters)
DriverModel: Software drivers for the IP block (C/H files, register structures)
Data: Software binaries to load into memory (flash images, bootloaders)
This metadata is preserved through the entire flow.
3. Pin Allocation¶
When you run chipflow pin lock:
Top-level Interface
(MySoC.uart, MySoC.gpio)
│
▼
Extract IOSignatures
(UARTSignature, GPIOSignature)
│
▼
Calculate Pin Requirements
(UART: 2 pins, GPIO: 8 pins)
│
▼
Package Allocator
(Selects pins from package definition)
│
▼
pins.lock File
(Persists allocation)
The pins.lock file maps abstract interface names to concrete package pin locations:
{
"uart.tx": {"pin": "42", "loc": "A12"},
"uart.rx": {"pin": "43", "loc": "A13"},
"gpio.gpio[0]": {"pin": "44", "loc": "B12"},
...
}
4. Elaboration & RTLIL Generation¶
Amaranth elaborates your design into a Fragment tree, then converts to RTLIL:
Fragment Tree RTLIL
┌──────────┐ ┌────────────────────────┐
│ Top │ │ module \MySoC │
│ │ │ (* chipflow.io = ... │
├──────────┤ ────────> │ wire \uart$tx$o │
│ MySoC │ │ ... │
│ - uart │ │ endmodule │
│ - gpio │ │ │
└──────────┘ └────────────────────────┘
Annotations from signatures are embedded in RTLIL as attributes:
(* chipflow.annotation.io-model = "{\"direction\": \"output\", \"width\": 1}" *)
wire \uart$tx$o;
5. Platform Consumption¶
Different platforms consume the RTLIL + annotations:
Silicon Platform¶
RTLIL + pins.lock
│
▼
Read IOModel annotations
(drive mode, trip point, etc.)
│
▼
Create SiliconPlatformPort
(Sky130Port, etc.)
│
▼
Generate I/O cell configuration
(PAD instances with controls)
│
▼
Synthesis → Place & Route → GDS-II
Simulation Platform¶
RTLIL
│
▼
Read SimInterface annotations
(UID, parameters)
│
▼
Match to C++ models
(UART model, SPI flash model)
│
▼
Generate CXXRTL C++
│
▼
Compile with models → Executable simulator
Software Platform¶
Design Fragment
│
▼
Read DriverModel annotations
(C/H files, regs_struct)
│
▼
Extract memory map from Wishbone decoder
│
▼
Generate soc.h with peripheral pointers
│
▼
Compile user code + drivers → ELF binary
6. Step Orchestration¶
The chipflow CLI uses “Steps” to orchestrate the flow:
$ chipflow silicon prepare
│
▼
┌─────────────┐
│ SiliconStep │
│ .prepare() │
└─────────────┘
│
├─> Load config (chipflow.toml)
├─> Instantiate top components
├─> Load pins.lock
├─> Create SiliconPlatform
├─> Elaborate design
└─> Convert to RTLIL → build/silicon/design.rtlil
$ chipflow silicon submit
│
▼
┌─────────────┐
│ SiliconStep │
│ .submit() │
└─────────────┘
│
├─> Package RTLIL + pins.lock
├─> Authenticate with API
└─> Upload to ChipFlow cloud
Annotation System Architecture¶
The annotation system is central to how ChipFlow propagates metadata:
Decorator Application (Design time)
@amaranth_annotate(IOModel, "https://chipflow.com/schemas/io-model/v0", "_model") class IOSignature(wiring.Signature): def __init__(self, width, direction, **kwargs): self._model = IOModel(width=width, direction=direction, **kwargs) # Decorator will extract self._model when serializing
JSON Schema Generation (Elaboration time)
Pydantic TypeAdapter generates JSON schema from TypedDict:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://chipflow.com/schemas/io-model/v0", "type": "object", "properties": { "direction": {"type": "string", "enum": ["input", "output", "bidir"]}, "width": {"type": "integer"}, "invert": {"type": "boolean"}, ... } }
RTLIL Embedding (Conversion time)
Amaranth calls
Annotation.as_json()and embeds in RTLIL:(* chipflow.annotation.io-model = "{\"direction\": \"output\", \"width\": 1}" *)
Platform Extraction (Build time)
Platform uses
submodule_metadata()to walk Fragment and extract:for component, name, meta in submodule_metadata(frag, "top"): annotations = meta['annotations'] if IO_ANNOTATION_SCHEMA in annotations: io_model = TypeAdapter(IOModel).validate_python(annotations[IO_ANNOTATION_SCHEMA]) # Use io_model to configure platform
Package System Architecture¶
Packages define the physical constraints of your chip:
BasePackageDef
├── bringup_pins() → PowerPins, JTAGPins, etc.
├── allocate() → Assigns ports to pins
└── instantiate() → Creates PortDesc for each allocation
LinearAllocPackageDef (extends BasePackageDef)
└── Sequential allocation strategy
QuadPackageDef (extends LinearAllocPackageDef)
└── PGA-style packages (pga144)
GAPackageDef (extends BasePackageDef)
└── Grid array packages with row/col addressing
OpenframePackageDef (extends BasePackageDef)
└── Open-frame packages with custom layouts
Allocation Flow:
User runs: chipflow pin lock
│
▼
┌──────────────────────┐
│ Load chipflow.toml │
│ - process: sky130 │
│ - package: pga144 │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Instantiate package │
│ PACKAGE_DEFS[pkg] │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Elaborate top design │
│ Extract interfaces │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ For each interface: │
│ - Get IOModel │
│ - Create PortDesc │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ package.allocate() │
│ - Assign pins │
│ - Check constraints │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Write pins.lock │
│ - Persist mapping │
└──────────────────────┘
Configuration System¶
ChipFlow uses Pydantic models for configuration:
chipflow.toml
│ (parsed by tomllib)
▼
dict[str, Any]
│ (validated by Pydantic)
▼
Config dataclass
├── chipflow: ChipFlowConfig
│ ├── project_name: str
│ ├── top: dict[str, str]
│ ├── clock_domains: list[str]
│ ├── silicon: SiliconConfig
│ │ ├── process: Process
│ │ └── package: str
│ ├── software: SoftwareConfig
│ │ └── riscv: CompilerConfig
│ └── simulation: SimulationConfig
└── tool: dict[str, Any]
Steps access config during execution:
class SiliconStep(StepBase):
def prepare(self):
process = self.config.chipflow.silicon.process
package = PACKAGE_DEFS[self.config.chipflow.silicon.package]
# Use process and package to build...
Extending ChipFlow¶
ChipFlow is designed to be extensible at multiple levels:
Custom Pin Signatures¶
Create new interface types:
@simulatable_interface()
class MyCustomSignature(wiring.Signature):
def __init__(self, **kwargs):
super().__init__({
"custom": Out(BidirIOSignature(4, **kwargs))
})
To attach a simulation model to your custom signature:
from chipflow.platform import SimModel, BasicCxxBuilder
# Define the C++ model
MY_BUILDER = BasicCxxBuilder(
models=[
SimModel('my_custom', 'my_namespace', MyCustomSignature),
],
hpp_files=[Path('design/sim/my_custom_model.h')],
)
# In your custom SimStep
class MySimPlatform(SimPlatform):
def __init__(self, config):
super().__init__(config)
self._builders.append(MY_BUILDER)
See Simulation Guide for complete examples of creating custom simulation models.
Custom Steps¶
Override default behavior:
from chipflow.platform import SiliconStep
class MySiliconStep(SiliconStep):
def prepare(self):
# Custom pre-processing
result = super().prepare()
# Custom post-processing
return result
Reference in chipflow.toml:
[chipflow.steps]
silicon = "my_project.steps:MySiliconStep"
Custom Packages¶
Define new package types:
from chipflow.packaging import BasePackageDef
class MyPackageDef(BasePackageDef):
def __init__(self):
# Define pin layout
pass
def allocate(self, ports):
# Custom allocation algorithm
pass
Custom Platforms¶
Add new target platforms:
from chipflow.platform import StepBase
class MyPlatformStep(StepBase):
def build(self, m, top):
# Extract annotations
# Generate output for custom platform
pass
See Also¶
Using Pin Signatures and Software Drivers - User guide for pin signatures
Pin Signature Architecture (Contributor Guide) - Deep dive into annotation system
Intro to chipflow.toml - Configuration reference
The chipflow command - CLI command reference