gowin: Add support for OSER primitives

* placement of OSER4, OVIDEO, OSER8 and SER10 primitives is supported;
* primitives are implemented for the GW1N-1, GW1NZ-1, GW1NSR-4C,
  GW1NR-9, GW1NR-9C chips;
* the initial support for special HCLK clock wires is implemented to the
  extent necessary for OSER primitives to function;
* output to both regular IO and TLVDS_OBUF is supported;
* tricks required for IOLOGIC to work on one side of the -9 and -9C
  chips are taken into account;
* various edits, such as using idf() instead of the local buffer.

Compatible with old apicula bases.

Signed-off-by: YRabbit <rabbit@yrabbit.cyou>
This commit is contained in:
YRabbit 2023-03-22 17:15:17 +10:00 committed by myrtle
parent b3c33bd0ab
commit 95ace0fade
6 changed files with 338 additions and 40 deletions

View File

@ -384,9 +384,6 @@ void Arch::addPip(IdString name, IdString type, IdString srcWire, IdString dstWi
pi.delay = delay;
pi.loc = loc;
// log_info("addpip %s->%s %.6f | %s name:%s\n" , srcWire.c_str(this), dstWire.c_str(this),
// getDelayNS(delay.maxDelay()), srcWire.c_str(this), name.c_str(this));
wire_info(srcWire).downhill.push_back(name);
wire_info(dstWire).uphill.push_back(name);
pip_ids.push_back(name);
@ -597,19 +594,33 @@ void Arch::addCellTimingClockToOut(IdString cell, IdString port, IdString clock,
// ---------------------------------------------------------------
IdString Arch::apply_local_aliases(int row, int col, const DatabasePOD *db, IdString &wire)
{
const TilePOD *tile = db->grid[row * db->cols + col].get();
auto local_alias = pairLookup(tile->aliases.get(), tile->num_aliases, wire.index);
IdString res_wire = IdString();
if (local_alias != nullptr) {
wire = IdString(local_alias->src_id);
res_wire = idf("R%dC%d_%s", row + 1, col + 1, wire.c_str(this));
}
return res_wire;
}
// TODO represent wires more intelligently.
IdString Arch::wireToGlobal(int &row, int &col, const DatabasePOD *db, IdString &wire)
{
const std::string &wirename = wire.str(this);
char buf[32];
if (wirename == "VCC" || wirename == "VSS") {
row = 0;
col = 0;
return wire;
}
if (!isdigit(wirename[1]) || !isdigit(wirename[2]) || !isdigit(wirename[3])) {
snprintf(buf, 32, "R%dC%d_%s", row + 1, col + 1, wirename.c_str());
return id(buf);
IdString res_wire = apply_local_aliases(row, col, db, wire);
if (res_wire == IdString()) {
return idf("R%dC%d_%s", row + 1, col + 1, wirename.c_str());
}
return res_wire;
}
char direction = wirename[0];
int num = std::stoi(wirename.substr(1, 2));
@ -628,8 +639,7 @@ IdString Arch::wireToGlobal(int &row, int &col, const DatabasePOD *db, IdString
col += segment;
break;
default:
snprintf(buf, 32, "R%dC%d_%s", row + 1, col + 1, wirename.c_str());
return id(buf);
return idf("R%dC%d_%s", row + 1, col + 1, wirename.c_str());
break;
}
// wires wrap around the edges
@ -647,18 +657,13 @@ IdString Arch::wireToGlobal(int &row, int &col, const DatabasePOD *db, IdString
col = 2 * db->cols - 1 - col;
direction = 'E';
}
snprintf(buf, 32, "%c%d0", direction, num);
wire = id(buf);
wire = idf("%c%d0", direction, num);
// local aliases
const TilePOD *tile = db->grid[row * db->cols + col].get();
auto local_alias = pairLookup(tile->aliases.get(), tile->num_aliases, wire.index);
if (local_alias != nullptr) {
wire = IdString(local_alias->src_id);
snprintf(buf, 32, "R%dC%d_%s", row + 1, col + 1, wire.c_str(this));
} else {
snprintf(buf, 32, "R%dC%d_%c%d", row + 1, col + 1, direction, num);
IdString res_wire = apply_local_aliases(row, col, db, wire);
if (res_wire == IdString()) {
res_wire = idf("R%dC%d_%c%d", row + 1, col + 1, direction, num);
}
return id(buf);
return res_wire;
}
const PairPOD *pairLookup(const PairPOD *list, const size_t len, const int dest)
@ -1581,6 +1586,10 @@ Arch::Arch(ArchArgs args) : args(args)
snprintf(buf, 32, "R%dC%d_%s", row + 1, col + 1, portname.c_str(this));
addBelInput(belname, id_XXX_VSS1, id(buf));
}
if (!z && device_id == id("GW1NR-9C")) {
addBelInput(belname, id_XXX_0, idf("R%dC%d_C6", row + 1, col + 1));
addBelInput(belname, id_XXX_1, idf("R%dC%d_D6", row + 1, col + 1));
}
} break;
// Simplified IO
case ID_IOBJS:
@ -1627,7 +1636,7 @@ Arch::Arch(ArchArgs args) : args(args)
case ID_ODDRA: {
snprintf(buf, 32, "R%dC%d_ODDR%s%c", row + 1, col + 1, oddrc ? "C" : "", 'A' + z - (oddrc ? 2 : 0));
belname = id(buf);
addBel(belname, id_ODDR, Loc(col, row, BelZ::iologic_0_z + z), false);
addBel(belname, id_ODDR, Loc(col, row, BelZ::oddr_0_z + z), false);
portname = IdString(pairLookup(bel->ports.get(), bel->num_ports, ID_D0)->src_id);
snprintf(buf, 32, "R%dC%d_%s", row + 1, col + 1, portname.c_str(this));
@ -1681,6 +1690,64 @@ Arch::Arch(ArchArgs args) : args(args)
addWire(q1_name, id_q1, row, col);
addBelOutput(belname, id_Q1, q1_name);
} break;
case ID_IOLOGICB:
z++; /* fall-through*/
case ID_IOLOGICA: {
belname = idf("R%dC%d_IOLOGIC%c", row + 1, col + 1, 'A' + z);
addBel(belname, id_IOLOGIC, Loc(col, row, BelZ::iologic_z + z), false);
for (int i = 0; i < 10; ++i) {
if (i < 4) {
// TX
IdString const tx[] = {id_TX0, id_TX1, id_TX2, id_TX3};
portname = IdString(pairLookup(bel->ports.get(), bel->num_ports, tx[i].hash())->src_id);
addBelInput(belname, tx[i], idf("R%dC%d_%s", row + 1, col + 1, portname.c_str(this)));
}
// D
IdString const d[] = {id_D0, id_D1, id_D2, id_D3, id_D4, id_D5, id_D6, id_D7, id_D8, id_D9};
portname = IdString(pairLookup(bel->ports.get(), bel->num_ports, d[i].hash())->src_id);
addBelInput(belname, d[i], idf("R%dC%d_%s", row + 1, col + 1, portname.c_str(this)));
}
portname = IdString(pairLookup(bel->ports.get(), bel->num_ports, ID_PCLK)->src_id);
addBelInput(belname, id_PCLK, idf("R%dC%d_%s", row + 1, col + 1, portname.c_str(this)));
auto fclk = pairLookup(bel->ports.get(), bel->num_ports, ID_FCLK);
// XXX as long as there is no special processing of the pins
if (fclk != nullptr) {
portname = IdString(fclk->src_id);
IdString wire = idf("R%dC%d_%s", row + 1, col + 1, portname.c_str(this));
if (wires.count(wire) == 0) {
GlobalAliasPOD alias;
alias.dest_col = col;
alias.dest_row = row;
alias.dest_id = portname.hash();
auto alias_src = genericLookup(db->aliases.get(), db->num_aliases, alias, aliasCompare);
if (alias_src != nullptr) {
int srcrow = alias_src->src_row;
int srccol = alias_src->src_col;
IdString srcid = IdString(alias_src->src_id);
wire = wireToGlobal(srcrow, srccol, db, srcid);
if (wires.count(wire) == 0) {
addWire(wire, srcid, srccol, srcrow);
}
addBelInput(belname, id_FCLK, wire);
}
// XXX here we are creating an
// IOLOGIC with a missing FCLK input. This is so
// because bels with the same type can be placed in
// on the chip where there is no pin, so no
// IOLOGIC makes sense. But since each type is
// described only once in the database we can't really
// mark these special bel somehow.
// By creating an IOLOGIC without an FCLK input we
// create a routing error later, so that "bad"
// locations are handled.
} else {
addBelInput(belname, id_FCLK, idf("R%dC%d_%s", row + 1, col + 1, portname.c_str(this)));
}
}
portname = IdString(pairLookup(bel->ports.get(), bel->num_ports, ID_RESET)->src_id);
addBelInput(belname, id_RESET, idf("R%dC%d_%s", row + 1, col + 1, portname.c_str(this)));
} break;
default:
break;
}
@ -2245,6 +2312,54 @@ void Arch::fix_pll_nets(Context *ctx)
}
}
// mark with hclk is used
void Arch::mark_used_hclk(Context *ctx)
{
for (auto &cell : ctx->cells) {
CellInfo *ci = cell.second.get();
if (ci->type != id_IOLOGIC) {
continue;
}
if (ci->attrs.count(id_IOLOGIC_FCLK)) {
continue;
}
// if it's an aux cell
if (ci->attrs.count(id_IOLOGIC_MASTER_CELL)) {
continue;
}
ci->setAttr(id_IOLOGIC_FCLK, Property("UNKNOWN"));
// *** FCLK
if (port_used(ci, id_FCLK)) {
NetInfo const *net = ci->getPort(id_FCLK);
for (auto const &user : net->users) {
if (user.cell != ci) {
continue;
}
if (user.port != id_FCLK) {
continue;
}
WireId dstWire = ctx->getNetinfoSinkWire(net, user, 0);
if (ctx->verbose) {
log_info(" Cell:%s, port:%s, wire:%s\n", user.cell->name.c_str(this), user.port.c_str(this),
dstWire.c_str(this));
}
for (PipId const &pip : getPipsUphill(dstWire)) {
if (!checkPipAvail(pip)) {
WireId src_wire = getPipSrcWire(pip);
ci->setAttr(id_IOLOGIC_FCLK, Property(wire_info(src_wire).type.str(this)));
if (ci->attrs.count(id_IOLOGIC_AUX_CELL)) {
IdString aux_cell_name = ctx->id(ci->attrs[id_IOLOGIC_AUX_CELL].as_string());
ctx->cells[aux_cell_name]->setAttr(id_IOLOGIC_FCLK,
Property(wire_info(src_wire).type.str(this)));
}
}
}
}
}
}
}
void Arch::pre_route(Context *ctx)
{
fix_pll_nets(ctx);
@ -2253,7 +2368,11 @@ void Arch::pre_route(Context *ctx)
}
}
void Arch::post_route(Context *ctx) { fix_longwire_bels(); }
void Arch::post_route(Context *ctx)
{
fix_longwire_bels();
mark_used_hclk(ctx);
}
bool Arch::route()
{

View File

@ -401,6 +401,11 @@ struct Arch : BaseArch<ArchRanges>
PortType getBelPinType(BelId bel, IdString pin) const override;
std::vector<IdString> getBelPins(BelId bel) const override;
std::array<IdString, 1> getBelPinsForCellPin(const CellInfo *cell_info, IdString pin) const override;
// Placement validity checks
virtual bool isValidBelForCellType(IdString cell_type, BelId bel) const override
{
return cell_type == id_DUMMY_CELL || cell_type == this->getBelType(bel);
}
WireId getWireByName(IdStringList name) const override;
IdStringList getWireName(WireId wire) const override;
@ -490,6 +495,9 @@ struct Arch : BaseArch<ArchRanges>
bool is_GCLKT_iob(const CellInfo *cell);
void bind_pll_to_bel(CellInfo *ci, PLL loc);
void mark_used_hclk(Context *ctx);
IdString apply_local_aliases(int row, int col, const DatabasePOD *db, IdString &wire);
GowinGlobalRouter globals_router;
void mark_gowin_globals(Context *ctx);
void route_gowin_globals(Context *ctx);
@ -525,15 +533,16 @@ namespace BelZ {
enum
{
mux_0_z = 10, // start Z for the MUX2LUT5 bels
iologic_0_z = 20, // start Z for the IOLOGIC bels
lutram_0_z = 30, // start Z for the IOLOGIC bels
oddr_0_z = 20, // XXX start Z for the ODDR bels
lutram_0_z = 30, // start Z for the LUTRAM bels
vcc_0_z = 277, // virtual VCC bel Z
gnd_0_z = 278, // virtual VSS bel Z
osc_z = 280, // Z for the oscillator bels
bufs_0_z = 281, // Z for long wire buffer bel
pll_z = 289, // PLL
pllvr_z = 290, // PLLVR
free_z = 291 // Must be the last, one can use z starting from this value, adjust accordingly.
iologic_z = 291, // IOLOGIC
free_z = 293 // Must be the last, one can use z starting from this value, adjust accordingly.
};
}

View File

@ -105,6 +105,10 @@ std::unique_ptr<CellInfo> create_generic_cell(Context *ctx, IdString type, std::
new_cell->addOutput(id_CLKOUTD);
new_cell->addOutput(id_CLKOUTD3);
new_cell->addOutput(id_LOCK);
} else if (type == id_IOLOGIC) {
new_cell->addInput(id_PCLK);
new_cell->addInput(id_RESET);
} else if (type == id_DUMMY_CELL) {
} else {
log_error("unable to create generic cell of type %s\n", type.c_str(ctx));
}

View File

@ -105,7 +105,7 @@ inline bool is_lc(const BaseCtx *ctx, const CellInfo *cell) { return cell->type
inline bool is_sram(const BaseCtx *ctx, const CellInfo *cell) { return cell->type == id_RAM16SDP4; }
inline bool is_iob(const Context *ctx, const CellInfo *cell) { return (cell->type.index == ID_IOB); }
inline bool is_iob(const Context *ctx, const CellInfo *cell) { return (cell->type == id_IOB || cell->type == id_IOBS); }
// Convert a LUT primitive to (part of) an GENERIC_SLICE, swapping ports
// as needed. Set no_dff if a DFF is not being used, so that the output

View File

@ -667,6 +667,9 @@ X(IOBH)
X(IOBI)
X(IOBJ)
// misc
X(DUMMY_CELL)
// simplified iobs
X(IOBS)
X(IOBAS)
@ -757,10 +760,18 @@ X(LWO7)
// IOLOGIC
X(TX)
X(TX0)
X(TX1)
X(TX2)
X(TX3)
X(FCLK)
X(PCLK)
X(XXX_VSS)
X(XXX_VCC)
X(XXX_VSS0)
X(XXX_VSS1)
X(XXX_0)
X(XXX_1)
X(OBUF_TYPE)
X(SBUF)
X(DBUF)
@ -770,6 +781,20 @@ X(ODDRA)
X(ODDRB)
X(ODDRCA)
X(ODDRCB)
X(OSER4)
X(OSER8)
X(OSER10)
X(OVIDEO)
X(OSER16)
X(IOLOGIC)
X(IOLOGICA)
X(IOLOGICB)
X(IOLOGIC_TYPE)
X(IOLOGIC_FCLK)
X(IOLOGIC_MASTER_CELL)
X(IOLOGIC_AUX_CELL)
X(D8)
X(D9)
// Wide LUTs
X(MUX2_LUT5)

View File

@ -813,7 +813,11 @@ static bool is_gowin_iologic(const Context *ctx, const CellInfo *cell)
{
switch (cell->type.index) {
case ID_ODDR: /* fall-through*/
case ID_ODDRC:
case ID_ODDRC: /* fall-through*/
case ID_OSER4: /* fall-through*/
case ID_OSER8: /* fall-through*/
case ID_OSER10: /* fall-through*/
case ID_OVIDEO:
return true;
default:
return false;
@ -836,7 +840,7 @@ static void pack_iologic(Context *ctx)
if (is_gowin_iologic(ctx, ci)) {
CellInfo *q0_dst = nullptr;
CellInfo *q1_dst = nullptr;
switch (ci->type.index) {
switch (ci->type.hash()) {
case ID_ODDRC: /* fall-through*/
case ID_ODDR: {
q0_dst = net_only_drives(ctx, ci->ports.at(id_Q0).net, is_iob, id_I);
@ -851,7 +855,7 @@ static void pack_iologic(Context *ctx)
if (iob_bel != q0_dst->attrs.end()) {
// already know there to place, no need of any cluster stuff
Loc loc = ctx->getBelLocation(ctx->getBelByNameStr(iob_bel->second.as_string()));
loc.z += BelZ::iologic_0_z;
loc.z += BelZ::oddr_0_z;
ci->attrs[id_BEL] = ctx->getBelName(ctx->getBelByLocation(loc)).str(ctx);
} else {
// make cluster from ODDR and OBUF
@ -864,7 +868,7 @@ static void pack_iologic(Context *ctx)
q0_dst->cluster = ci->name;
q0_dst->constr_x = 0;
q0_dst->constr_y = 0;
q0_dst->constr_z = -BelZ::iologic_0_z;
q0_dst->constr_z = -BelZ::oddr_0_z;
q0_dst->constr_abs_z = false;
}
@ -891,13 +895,142 @@ static void pack_iologic(Context *ctx)
ci->addInput(id_XXX_VCC);
ci->connectPort(id_XXX_VCC, ctx->nets[ctx->id("$PACKER_VCC_NET")].get());
}
if (ctx->gw1n9_quirk && iob_bel != q0_dst->attrs.end()) {
bool have_XXX_VSS0 =
ctx->bels[ctx->getBelByNameStr(iob_bel->second.as_string())].pins.count(id_XXX_VSS0);
if (have_XXX_VSS0) {
if (iob_bel != q0_dst->attrs.end()) {
IdString io_bel_name = ctx->getBelByNameStr(iob_bel->second.as_string());
if (ctx->gw1n9_quirk && ctx->bels[io_bel_name].pins.count(id_XXX_VSS0)) {
q0_dst->disconnectPort(id_XXX_VSS0);
q0_dst->connectPort(id_XXX_VSS0, ctx->nets[ctx->id("$PACKER_VCC_NET")].get());
}
if (ctx->bels[io_bel_name].pins.count(id_XXX_1)) {
q0_dst->disconnectPort(id_XXX_1);
q0_dst->connectPort(id_XXX_1, ctx->nets[ctx->id("$PACKER_VCC_NET")].get());
}
}
} break;
case ID_OSER4: /* fall-through */
case ID_OSER8: /* fall-through */
case ID_OSER10: /* fall-through */
case ID_OVIDEO: {
IdString output = id_Q;
IdString output_1 = IdString();
if (ci->type == id_OSER4 || ci->type == id_OSER8) {
output = id_Q0;
output_1 = id_Q1;
}
q0_dst = net_only_drives(ctx, ci->ports.at(output).net, is_iob, id_I);
NPNR_ASSERT(q0_dst != nullptr);
auto iob_bel = q0_dst->attrs.find(id_BEL);
if (iob_bel == q0_dst->attrs.end()) {
log_info("No constraints for %s\n", ctx->nameOf(q0_dst));
NPNR_ASSERT_FALSE("The pins for IDES/OSER must be specified explicitly.");
}
Loc loc = ctx->getBelLocation(ctx->getBelByNameStr(iob_bel->second.as_string()));
loc.z += BelZ::iologic_z;
ci->setAttr(id_BEL, ctx->getBelName(ctx->getBelByLocation(loc)).str(ctx));
BelId bel = ctx->getBelByLocation(loc);
if (bel == BelId()) {
log_info("No bel for %s at %s\n", ctx->nameOf(ci), iob_bel->second.as_string().c_str());
NPNR_ASSERT_FALSE("Can't place IDES/OSER here");
}
std::string out_mode;
switch (ci->type.hash()) {
case ID_OSER4:
out_mode = "ODDRX2";
break;
case ID_OSER8:
out_mode = "ODDRX4";
break;
case ID_OSER10:
out_mode = "ODDRX5";
break;
case ID_OVIDEO:
out_mode = "VIDEORX";
break;
}
ci->setParam(ctx->id("OUTMODE"), out_mode);
bool use_diff_io = false;
if (q0_dst->attrs.count(id_DIFF_TYPE)) {
ci->setAttr(id_OBUF_TYPE, std::string("DBUF"));
use_diff_io = true;
} else {
ci->setAttr(id_OBUF_TYPE, std::string("SBUF"));
}
// disconnect Q output: it is wired internally
delete_nets.insert(ci->ports.at(output).net->name);
q0_dst->disconnectPort(id_I);
ci->disconnectPort(output);
bool have_XXX = ctx->bels[ctx->getBelByNameStr(iob_bel->second.as_string())].pins.count(id_XXX_1);
if (have_XXX) {
q0_dst->disconnectPort(id_XXX_1);
q0_dst->connectPort(id_XXX_1, ctx->nets[ctx->id("$PACKER_VCC_NET")].get());
}
// if Q1 is connected then disconnet it too
if (output_1 != IdString() && port_used(ci, output_1)) {
q1_dst = net_only_drives(ctx, ci->ports.at(output_1).net, is_iob, id_OEN);
if (q1_dst != nullptr) {
delete_nets.insert(ci->ports.at(output_1).net->name);
q0_dst->disconnectPort(id_OEN);
ci->disconnectPort(output_1);
ci->setAttr(id_IOBUF, 1);
}
} else {
// force OEN = 0 in order to enable output
// XXX check for IOBUF and TBUF
if (ci->type == id_OSER4 || ci->type == id_OSER8) {
int port_num = ci->type == id_OSER4 ? 2 : 4;
for (int i = 0; i < port_num; ++i) {
IdString port = ctx->idf("TX%d", i);
ci->disconnectPort(port);
ci->connectPort(port, ctx->nets[ctx->id("$PACKER_GND_NET")].get());
}
}
}
ci->setAttr(id_IOBUF, 0);
ci->setAttr(id_IOLOGIC_TYPE, ci->type.str(ctx));
if (ci->type == id_OSER4) {
ci->type = id_IOLOGIC;
// two OSER4 share FCLK, check it
Loc other_loc = loc;
other_loc.z = 1 - loc.z + 2 * BelZ::iologic_z;
BelId other_bel = ctx->getBelByLocation(other_loc);
CellInfo *other_cell = ctx->getBoundBelCell(other_bel);
if (other_cell != nullptr) {
NPNR_ASSERT(other_cell->type == id_OSER4);
if (ci->ports.at(id_FCLK).net != other_cell->ports.at(id_FCLK).net) {
log_error("%s and %s have differnet FCLK nets\n", ctx->nameOf(ci), ctx->nameOf(other_cell));
}
}
} else {
std::unique_ptr<CellInfo> dummy =
create_generic_cell(ctx, id_DUMMY_CELL, ci->name.str(ctx) + "_DUMMY_IOLOGIC_IO");
loc.z = 1 - loc.z + BelZ::iologic_z;
if (!use_diff_io) {
dummy->setAttr(id_BEL, ctx->getBelName(ctx->getBelByLocation(loc)).str(ctx));
new_cells.push_back(std::move(dummy));
}
loc.z += BelZ::iologic_z;
std::unique_ptr<CellInfo> aux_cell =
create_generic_cell(ctx, id_IOLOGIC, ci->name.str(ctx) + "_AUX");
ci->setAttr(ctx->id("IOLOGIC_AUX_CELL"), ci->name.str(ctx) + "_AUX");
aux_cell->setParam(ctx->id("OUTMODE"), std::string("DDRENABLE"));
aux_cell->setAttr(ctx->id("IOLOGIC_MASTER_CELL"), ci->name.str(ctx));
aux_cell->setAttr(id_BEL, ctx->getBelName(ctx->getBelByLocation(loc)).str(ctx));
if (port_used(ci, id_RESET)) {
aux_cell->connectPort(id_RESET, ci->ports.at(id_RESET).net);
}
if (port_used(ci, id_PCLK)) {
aux_cell->connectPort(id_PCLK, ci->ports.at(id_PCLK).net);
}
new_cells.push_back(std::move(aux_cell));
ci->type = id_IOLOGIC;
}
} break;
default:
@ -1139,6 +1272,7 @@ static void pack_io(Context *ctx)
IdString new_cell_type = id_IOB;
std::string constr_bel_name = std::string("");
bool have_xxx_port = false;
bool have_xxx0_port = false;
// check whether the given IO is limited to simplified IO cells
auto constr_bel = ci->attrs.find(id_BEL);
if (constr_bel != ci->attrs.end()) {
@ -1157,6 +1291,7 @@ static void pack_io(Context *ctx)
if (ctx->gw1n9_quirk) {
have_xxx_port = ctx->bels[constr_bel].pins.count(id_XXX_VSS0) != 0;
}
have_xxx0_port = ctx->bels[constr_bel].pins.count(id_XXX_0) != 0;
}
}
@ -1172,6 +1307,12 @@ static void pack_io(Context *ctx)
gwiob->addInput(id_XXX_VSS1);
gwiob->connectPort(id_XXX_VSS1, ctx->nets[ctx->id("$PACKER_GND_NET")].get());
}
if (have_xxx0_port && ci->type != id_IBUF) {
gwiob->addInput(id_XXX_0);
gwiob->connectPort(id_XXX_0, ctx->nets[ctx->id("$PACKER_GND_NET")].get());
gwiob->addInput(id_XXX_1);
gwiob->connectPort(id_XXX_1, ctx->nets[ctx->id("$PACKER_GND_NET")].get());
}
packed_cells.insert(ci->name);
if (iob != nullptr) {