diff --git a/himbaechel/arch.cc b/himbaechel/arch.cc
index 9ae9225e00..4590c66ac2 100644
--- a/himbaechel/arch.cc
+++ b/himbaechel/arch.cc
@@ -307,6 +307,24 @@ IdStringList Arch::getPipName(PipId pip) const
 
 IdString Arch::getPipType(PipId pip) const { return IdString(); }
 
+GroupId Arch::getGroupByName(IdStringList name) const
+{
+    NPNR_ASSERT(name.size() == 2);
+    int tile = tile_name2idx.at(name[0]);
+    const auto &tdata = chip_tile_info(chip_info, tile);
+    for (int group = 0; group < tdata.groups.ssize(); group++) {
+        if (IdString(tdata.groups[group].name) == name[1])
+            return GroupId(tile, group);
+    }
+    return GroupId();
+
+}
+
+IdStringList Arch::getGroupName(GroupId group) const
+{
+    return IdStringList::concat(tile_name.at(group.tile), IdString(chip_group_info(chip_info, group).name));
+}
+
 std::string Arch::getChipName() const { return chip_info->name.get(); }
 
 IdString Arch::archArgsToId(ArchArgs args) const
@@ -504,7 +522,12 @@ IdString Arch::get_tile_type(int tile) const
 std::vector<GraphicElement> Arch::getDecalGraphics(DecalId decal) const
 {
     std::vector<GraphicElement> ret;
-    if (decal.type == DecalId::TYPE_BEL) {
+    if (decal.type == DecalId::TYPE_GROUP) {
+        GroupId group(decal.tile, decal.index);
+        Loc loc;
+        tile_xy(chip_info, decal.tile, loc.x, loc.y);
+        uarch->drawGroup(ret, getGroupType(group), loc);
+    } else if (decal.type == DecalId::TYPE_BEL) {
         BelId bel(decal.tile, decal.index);
         GraphicElement::style_t style = decal.active ? GraphicElement::STYLE_ACTIVE : GraphicElement::STYLE_INACTIVE;
         uarch->drawBel(ret, style, getBelType(bel), getBelLocation(bel));
@@ -557,7 +580,10 @@ DecalXY Arch::getPipDecal(PipId pip) const
 
 DecalXY Arch::getGroupDecal(GroupId group) const
 {
-    return DecalXY();
+    DecalXY decalxy;
+    decalxy.decal = DecalId(group.tile, group.index, DecalId::TYPE_GROUP);
+    decalxy.decal.active = true;
+    return decalxy;
 }
 
 NEXTPNR_NAMESPACE_END
diff --git a/himbaechel/arch.h b/himbaechel/arch.h
index ca965c8625..b419db55eb 100644
--- a/himbaechel/arch.h
+++ b/himbaechel/arch.h
@@ -49,6 +49,10 @@ inline const PipDataPOD &chip_pip_info(const ChipInfoPOD *chip, PipId pip)
 {
     return chip_tile_info(chip, pip.tile).pips[pip.index];
 }
+inline const GroupDataPOD &chip_group_info(const ChipInfoPOD *chip, GroupId group)
+{
+    return chip_tile_info(chip, group.tile).groups[group.index];
+}
 inline const TileRoutingShapePOD &chip_tile_shape(const ChipInfoPOD *chip, int tile)
 {
     return chip->tile_shapes[chip->tile_insts[tile].shape];
@@ -332,6 +336,48 @@ template <RelSlice<int32_t> TileWireDataPOD::*ptr> struct UpDownhillPipRange
 
 // -----------------------------------------------------------------------
 
+template <typename Tid, RelSlice<int32_t> GroupDataPOD::*ptr> struct GroupObjIterator
+{
+    const GroupDataPOD &group;
+    int tile = -1;
+    int cursor = -1;
+
+    GroupObjIterator(const GroupDataPOD &group, int tile, int cursor)
+            : group(group), tile(tile), cursor(cursor) {};
+
+    void operator++() { cursor++; }
+
+    bool operator!=(const GroupObjIterator<Tid, ptr> &other) const
+    {
+        return tile != other.tile || cursor != other.cursor;
+    }
+
+    bool operator==(const GroupObjIterator<Tid, ptr> &other) const
+    {
+        return tile == other.tile && cursor == other.cursor;
+    }
+
+    Tid operator*() const
+    {
+        return Tid(tile, (group.*ptr)[cursor]);
+    }
+};
+
+template <typename Tid, RelSlice<int32_t> GroupDataPOD::*ptr> struct GroupObjRange
+{
+    using iterator = GroupObjIterator<Tid, ptr>;
+    GroupObjRange(const GroupDataPOD &group, int tile)
+            : b(group, tile, 0), e(group, tile, (group.*ptr).ssize())
+    {
+    }
+
+    iterator b, e;
+    iterator begin() const { return b; }
+    iterator end() const { return e; }
+};
+
+// -----------------------------------------------------------------------
+
 struct BelPinIterator
 {
     const ChipInfoPOD *chip;
@@ -393,10 +439,16 @@ struct ArchArgs
 
 typedef TileObjRange<BelId, BelDataPOD, &TileTypePOD::bels> BelRange;
 typedef TileObjRange<PipId, PipDataPOD, &TileTypePOD::pips> AllPipRange;
+typedef TileObjRange<GroupId, GroupDataPOD, &TileTypePOD::groups> GroupRange;
 
 typedef UpDownhillPipRange<&TileWireDataPOD::pips_uphill> UphillPipRange;
 typedef UpDownhillPipRange<&TileWireDataPOD::pips_downhill> DownhillPipRange;
 
+typedef GroupObjRange<BelId, &GroupDataPOD::group_bels> GroupBelRange;
+typedef GroupObjRange<WireId, &GroupDataPOD::group_wires> GroupWireRange;
+typedef GroupObjRange<PipId, &GroupDataPOD::group_pips> GroupPipRange;
+typedef GroupObjRange<GroupId, &GroupDataPOD::group_groups> GroupGroupRange;
+
 struct ArchRanges : BaseArchRanges
 {
     using ArchArgsT = ArchArgs;
@@ -412,6 +464,12 @@ struct ArchRanges : BaseArchRanges
     using WireBelPinRangeT = BelPinRange;
     // Pips
     using AllPipsRangeT = AllPipRange;
+    // Groups
+    using AllGroupsRangeT = GroupRange;
+    using GroupBelsRangeT = GroupBelRange;
+    using GroupWiresRangeT = GroupWireRange;
+    using GroupPipsRangeT = GroupPipRange;
+    using GroupGroupsRangeT = GroupGroupRange;
 };
 
 struct Arch : BaseArch<ArchRanges>
@@ -699,6 +757,17 @@ struct Arch : BaseArch<ArchRanges>
         return uarch->getClusterPlacement(cluster, root_bel, placement);
     }
 
+    // -------------------------------------------------
+    // Group methods    
+    GroupId getGroupByName(IdStringList name) const override;
+    IdStringList getGroupName(GroupId group) const override;
+    GroupRange getGroups() const override { return GroupRange(chip_info); }
+    IdString getGroupType(GroupId group) const { return IdString(chip_group_info(chip_info, group).group_type); }
+    GroupBelRange getGroupBels(GroupId group) const override { return GroupBelRange(chip_group_info(chip_info, group), group.tile); }
+    GroupWireRange getGroupWires(GroupId group) const override { return GroupWireRange(chip_group_info(chip_info, group), group.tile); }
+    GroupPipRange getGroupPips(GroupId group) const override { return GroupPipRange(chip_group_info(chip_info, group), group.tile); }
+    GroupGroupRange getGroupGroups(GroupId group) const override { return GroupGroupRange(chip_group_info(chip_info, group), group.tile); }
+
     // -------------------------------------------------
     // Decal methods
     std::vector<GraphicElement> getDecalGraphics(DecalId decal) const override;
diff --git a/himbaechel/archdefs.h b/himbaechel/archdefs.h
index 589dea9926..be2fb5ebde 100644
--- a/himbaechel/archdefs.h
+++ b/himbaechel/archdefs.h
@@ -107,7 +107,23 @@ struct DecalId
     unsigned int hash() const { return mkhash(tile, mkhash(index, type)); }
 };
 
-typedef IdString GroupId;
+struct GroupId
+{
+    int32_t tile = -1;
+    int32_t index = -1;
+
+    GroupId() = default;
+    GroupId(int32_t tile, int32_t index) : tile(tile), index(index) {};
+
+    bool operator==(const GroupId &other) const { return tile == other.tile && index == other.index; }
+    bool operator!=(const GroupId &other) const { return tile != other.tile || index != other.index; }
+    bool operator<(const GroupId &other) const
+    {
+        return tile < other.tile || (tile == other.tile && index < other.index);
+    }
+    unsigned int hash() const { return mkhash(tile, index); }
+};
+
 typedef IdString BelBucketId;
 typedef IdString ClusterId;
 
diff --git a/himbaechel/chipdb.h b/himbaechel/chipdb.h
index 4f3241c781..099a1112c7 100644
--- a/himbaechel/chipdb.h
+++ b/himbaechel/chipdb.h
@@ -93,11 +93,23 @@ NPNR_PACKED_STRUCT(struct NodeShapePOD {
     int32_t timing_idx;
 });
 
+NPNR_PACKED_STRUCT(struct GroupDataPOD {
+    int32_t name;
+    int32_t group_type;
+    RelSlice<int32_t> group_bels;
+    RelSlice<int32_t> group_wires;
+    RelSlice<int32_t> group_pips;
+    RelSlice<int32_t> group_groups;
+
+    RelPtr<uint8_t> extra_data;
+});
+
 NPNR_PACKED_STRUCT(struct TileTypePOD {
     int32_t type_name;
     RelSlice<BelDataPOD> bels;
     RelSlice<TileWireDataPOD> wires;
     RelSlice<PipDataPOD> pips;
+    RelSlice<GroupDataPOD> groups;
     RelPtr<uint8_t> extra_data;
 });
 
diff --git a/himbaechel/himbaechel_api.h b/himbaechel/himbaechel_api.h
index fa75740139..8aa5ebd40c 100644
--- a/himbaechel/himbaechel_api.h
+++ b/himbaechel/himbaechel_api.h
@@ -114,6 +114,8 @@ struct HimbaechelAPI
     virtual void drawPip(std::vector<GraphicElement> &g,GraphicElement::style_t style, Loc loc,
                 WireId src, IdString src_type, int32_t src_id, WireId dst, IdString dst_type, int32_t dst_id) {};
 
+    virtual void drawGroup(std::vector<GraphicElement> &g, IdString group_type, Loc loc) {};
+
     // Routing methods
     virtual void expandBoundingBox(BoundingBox &bb) const;
     // --- Flow hooks ---
diff --git a/himbaechel/himbaechel_dbgen/chip.py b/himbaechel/himbaechel_dbgen/chip.py
index bedf0f28d4..b82c26174e 100644
--- a/himbaechel/himbaechel_dbgen/chip.py
+++ b/himbaechel/himbaechel_dbgen/chip.py
@@ -202,6 +202,47 @@ def serialise(self, context: str, bba: BBAWriter):
         else:
             bba.u32(0)
 @dataclass
+class GroupData(BBAStruct):
+    index: int
+    name: IdString
+    group_type: IdString = field(default_factory=IdString)
+    group_bels: list[int] = field(default_factory=list)
+    group_wires: list[int] = field(default_factory=list)
+    group_pips: list[int] = field(default_factory=list)
+    group_groups: list[int] = field(default_factory=list)
+    extra_data: object = None
+
+    def serialise_lists(self, context: str, bba: BBAWriter):
+        bba.label(f"{context}_group_bels")
+        for idx in self.group_bels:
+            bba.u32(idx)
+        bba.label(f"{context}_group_wires")
+        for idx in self.group_wires:
+            bba.u32(idx)
+        bba.label(f"{context}_group_pips")
+        for idx in self.group_pips:
+            bba.u32(idx)
+        bba.label(f"{context}_group_groups")
+        for idx in self.group_groups:
+            bba.u32(idx)
+        # extra data (optional)
+        if self.extra_data is not None:
+            self.extra_data.serialise_lists(f"{context}_extra_data", bba)
+            bba.label(f"{context}_extra_data")
+            self.extra_data.serialise(f"{context}_extra_data", bba)
+    def serialise(self, context: str, bba: BBAWriter):
+        bba.u32(self.name.index)
+        bba.u32(self.group_type.index)
+        bba.slice(f"{context}_group_bels", len(self.group_bels))
+        bba.slice(f"{context}_group_wires", len(self.group_wires))
+        bba.slice(f"{context}_group_pips", len(self.group_pips))
+        bba.slice(f"{context}_group_groups", len(self.group_groups))
+        if self.extra_data is not None:
+            bba.ref(f"{context}_extra_data")
+        else:
+            bba.u32(0)
+
+@dataclass
 class TileType(BBAStruct):
     strs: StringPool
     gfx_wire_ids: dict()
@@ -210,8 +251,10 @@ class TileType(BBAStruct):
     bels: list[BelData] = field(default_factory=list)
     pips: list[PipData] = field(default_factory=list)
     wires: list[TileWireData] = field(default_factory=list)
+    groups: list[GroupData] = field(default_factory=list)
 
     _wire2idx: dict[IdString, int] = field(default_factory=dict)
+    _group2idx: dict[IdString, int] = field(default_factory=dict)
 
     extra_data: object = None
 
@@ -253,6 +296,26 @@ def create_pip(self, src: str, dst: str, timing_class: str=""):
         self.wires[dst_idx].pips_uphill.append(pip.index)
         self.pips.append(pip)
         return pip
+    def create_group(self, name: str, type: str):
+        # Create a new group of a given name and type in the tile type
+        group = GroupData(index=len(self.groups),
+            name=self.strs.id(name),
+            group_type=self.strs.id(type))
+        self._group2idx[group.name] = group.index
+        self.groups.append(group)
+        return group
+    def add_bel_to_group(self, bel: BelData, group: str):
+        group_idx = self._group2idx[self.strs.id(group)]
+        self.groups[group_idx].group_bels.append(bel.index)
+    def add_wire_to_group(self, wire: TileWireData, group: str):
+        group_idx = self._group2idx[self.strs.id(group)]
+        self.groups[group_idx].group_wires.append(wire.index)
+    def add_pip_to_group(self, pip: PipData, group: str):
+        group_idx = self._group2idx[self.strs.id(group)]
+        self.groups[group_idx].group_pips.append(pip.index)
+    def add_group_to_group(self, sub_group: GroupData, group: str):
+        group_idx = self._group2idx[self.strs.id(group)]
+        self.groups[group_idx].group_groups.append(sub_group.index)
     def has_wire(self, wire: str):
         # Check if a wire has already been created
         return self.strs.id(wire) in self._wire2idx
@@ -267,6 +330,8 @@ def serialise_lists(self, context: str, bba: BBAWriter):
             wire.serialise_lists(f"{context}_wire{i}", bba)
         for i, pip in enumerate(self.pips):
             pip.serialise_lists(f"{context}_pip{i}", bba)
+        for i, group in enumerate(self.groups):
+            group.serialise_lists(f"{context}_group{i}", bba)
         # lists of members
         bba.label(f"{context}_bels")
         for i, bel in enumerate(self.bels):
@@ -277,6 +342,9 @@ def serialise_lists(self, context: str, bba: BBAWriter):
         bba.label(f"{context}_pips")
         for i, pip in enumerate(self.pips):
             pip.serialise(f"{context}_pip{i}", bba)
+        bba.label(f"{context}_groups")
+        for i, group in enumerate(self.groups):
+            group.serialise(f"{context}_group{i}", bba)
         # extra data (optional)
         if self.extra_data is not None:
             self.extra_data.serialise_lists(f"{context}_extra_data", bba)
@@ -287,6 +355,7 @@ def serialise(self, context: str, bba: BBAWriter):
         bba.slice(f"{context}_bels", len(self.bels))
         bba.slice(f"{context}_wires", len(self.wires))
         bba.slice(f"{context}_pips", len(self.pips))
+        bba.slice(f"{context}_groups", len(self.groups))
         if self.extra_data is not None:
             bba.ref(f"{context}_extra_data")
         else: