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:

  1. Pin Signatures - Define external interfaces (UART, GPIO, SPI, etc.)

  2. Annotation System - Attach metadata to designs for platform consumption

  3. Package Definitions - Map abstract ports to physical pins

  4. Platforms - Transform RTLIL to target-specific outputs

  5. Steps - Orchestrate the build process via CLI commands

  6. 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:

from chipflow_lib.platforms import UARTSignature, GPIOSignature
from amaranth import Module
from amaranth.lib.wiring import Component, Out

class MySoC(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

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:

  1. 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
    
  2. 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"},
        ...
      }
    }
    
  3. RTLIL Embedding (Conversion time)

    Amaranth calls Annotation.as_json() and embeds in RTLIL:

    (* chipflow.annotation.io-model = "{\"direction\": \"output\", \"width\": 1}" *)
    
  4. 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_lib.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_lib.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_lib.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_lib.platform import StepBase

class MyPlatformStep(StepBase):
    def build(self, m, top):
        # Extract annotations
        # Generate output for custom platform
        pass

See Also