gowin: Himbaechel. Add OSER16 and IDES16

Information about what function (main or auxiliary) the cell performs in
these primitives is transmitted through the tile's extra data. And this
also allows us to remove the calculation of the coordinates of the
auxiliary cell on the go.

Signed-off-by: YRabbit <rabbit@yrabbit.cyou>
This commit is contained in:
YRabbit 2023-08-13 22:05:18 +10:00 committed by myrtle
parent a823543932
commit 0994e11b73
6 changed files with 300 additions and 32 deletions

View File

@ -175,7 +175,7 @@ void GowinImpl::postRoute()
for (auto &cell : ctx->cells) {
auto ci = cell.second.get();
if (is_iologic(ci) && !ci->type.in(id_ODDR, id_ODDRC, id_IDDR, id_IDDRC)) {
if (ci->type == id_IOLOGIC || (is_iologic(ci) && !ci->type.in(id_ODDR, id_ODDRC, id_IDDR, id_IDDRC))) {
if (visited_hclk_users.find(ci->name) == visited_hclk_users.end()) {
// mark FCLK<-HCLK connections
const NetInfo *h_net = ci->getPort(id_FCLK);
@ -192,6 +192,9 @@ void GowinImpl::postRoute()
IdString up_wire_name = ctx->getWireName(ctx->getPipSrcWire(up_pip))[1];
if (up_wire_name.in(id_HCLK_OUT0, id_HCLK_OUT1, id_HCLK_OUT2, id_HCLK_OUT3)) {
user.cell->setAttr(id_IOLOGIC_FCLK, Property(up_wire_name.str(ctx)));
if (ctx->debug) {
log_info("set IOLOGIC_FCLK to %s\n", up_wire_name.c_str(ctx));
}
}
if (ctx->debug) {
log_info("HCLK user cell:%s, port:%s, wire:%s, pip:%s, up wire:%s\n",

View File

@ -39,7 +39,15 @@ inline bool is_iologic(const CellInfo *cell) { return type_is_iologic(cell->type
inline bool type_is_ssram(IdString cell_type) { return cell_type.in(id_RAM16SDP1, id_RAM16SDP2, id_RAM16SDP4); }
inline bool is_ssram(const CellInfo *cell) { return type_is_ssram(cell->type); }
// ==========================================
// extra data in the chip db
// ==========================================
NPNR_PACKED_STRUCT(struct Tile_extra_data_POD {
int32_t class_id;
int16_t io16_x_off;
int16_t io16_y_off;
});
NPNR_PACKED_STRUCT(struct Bottom_io_cnd_POD {
int32_t wire_a_net;
int32_t wire_b_net;
@ -77,6 +85,8 @@ enum
IOBB_Z = 51, // +IOBC...IOBL
IOLOGICA_Z = 70,
IDES16_Z = 72,
OSER16_Z = 73,
OSC_Z = 274,
PLL_Z = 275,

View File

@ -29,6 +29,8 @@ IOBA_Z = 50
IOBB_Z = 51
IOLOGICA_Z = 70
IDES16_Z = 72
OSER16_Z = 73
OSC_Z = 274
PLL_Z = 275
@ -45,10 +47,15 @@ class TileExtraData(BBAStruct):
# let's say the behavior of LUT+DFF in the tiles are completely identical,
# but one of them also contains clock-wire switches,
# then we assign them to the same LOGIC class.
io16_x_off: int = 0 # OSER16/IDES16 offsets to the aux cell
io16_y_off: int = 0
def serialise_lists(self, context: str, bba: BBAWriter):
pass
def serialise(self, context: str, bba: BBAWriter):
bba.u32(self.tile_class.index)
bba.u16(self.io16_x_off)
bba.u16(self.io16_y_off)
@dataclass
class BottomIOCnd(BBAStruct):
@ -262,6 +269,28 @@ def create_extra_funcs(tt: TileType, db: chipdb, x: int, y: int):
tt.create_wire(wire, "GSRI")
bel = tt.create_bel("GSR", "GSR", z = GSR_Z)
tt.add_bel_pin(bel, "GSRI", wire, PinType.INPUT)
if func == 'io16':
role = desc['role']
if role == 'MAIN':
y_off, x_off = desc['pair']
tt.extra_data.io16_x_off = x_off
tt.extra_data.io16_y_off = y_off
for io_type, z in {('IDES16', IDES16_Z), ('OSER16', OSER16_Z)}:
bel = tt.create_bel(io_type, io_type, z = z)
portmap = db.grid[y][x].bels[io_type].portmap
for port, wire in portmap.items():
if port == 'FCLK': # XXX compatibility
wire = 'FCLKA'
if not tt.has_wire(wire):
if port in {'CLK', 'PCLK'}:
tt.create_wire(wire, "TILE_CLK")
else:
tt.create_wire(wire, "IOL_PORT")
if 'OUT' in port:
tt.add_bel_pin(bel, port, wire, PinType.OUTPUT)
else:
tt.add_bel_pin(bel, port, wire, PinType.INPUT)
def create_tiletype(create_func, chip: Chip, db: chipdb, x: int, y: int, ttyp: int):
has_extra_func = (y, x) in db.extra_func

View File

@ -11,6 +11,24 @@
NEXTPNR_NAMESPACE_BEGIN
// tile extra data
IdString GowinUtils::get_tile_class(int x, int y)
{
int tile = tile_by_xy(ctx->chip_info, x, y);
const Tile_extra_data_POD *extra =
reinterpret_cast<const Tile_extra_data_POD *>(chip_tile_info(ctx->chip_info, tile).extra_data.get());
return IdString(extra->class_id);
}
// oser16/ides16 aux cell offsets
Loc GowinUtils::get_tile_io16_offs(int x, int y)
{
int tile = tile_by_xy(ctx->chip_info, x, y);
const Tile_extra_data_POD *extra =
reinterpret_cast<const Tile_extra_data_POD *>(chip_tile_info(ctx->chip_info, tile).extra_data.get());
return Loc(extra->io16_x_off, extra->io16_y_off, 0);
}
// pin functions: GCLKT_4, SSPI_CS, READY etc
IdStringList GowinUtils::get_pin_funcs(BelId bel)
{

View File

@ -19,6 +19,10 @@ struct GowinUtils
void init(Context *ctx) { this->ctx = ctx; }
// tile
IdString get_tile_class(int x, int y);
Loc get_tile_io16_offs(int x, int y);
// pin functions: GCLKT_4, SSPI_CS, READY etc
IdStringList get_pin_funcs(BelId bel);

View File

@ -332,7 +332,19 @@ struct GowinPacker
return ctx->getBelByLocation(loc);
}
void pack_bi_output_iol(CellInfo &ci, std::vector<IdString> &cells_to_remove, std::vector<IdString> &nets_to_remove)
void check_iologic_placement(CellInfo &ci, Loc iob_loc, int diff /* 1 - diff */)
{
if (ci.type.in(id_ODDR, id_ODDRC, id_IDDR, id_IDDRC) || diff) {
return;
}
BelId l_bel = ctx->getBelByLocation(Loc(iob_loc.x, iob_loc.y, BelZ::IOBA_Z + 1 - (iob_loc.z - BelZ::IOBA_Z)));
if (!ctx->checkBelAvail(l_bel)) {
log_error("Can't place %s at %s because it's already taken by %s\n", ctx->nameOf(&ci),
ctx->nameOfBel(l_bel), ctx->nameOf(ctx->getBoundBelCell(l_bel)));
}
}
void pack_bi_output_iol(CellInfo &ci, std::vector<IdString> &nets_to_remove)
{
// These primitives have an additional pin to control the tri-state iob - Q1.
IdString out_port = id_Q0;
@ -346,6 +358,9 @@ struct GowinPacker
if (l_bel == BelId()) {
log_error("Can't place IOLOGIC %s at %s\n", ctx->nameOf(&ci), ctx->nameOfBel(iob_bel));
}
// mark IOB as used by IOLOGIC
out_iob->setParam(id_IOLOGIC_IOB, 1);
check_iologic_placement(ci, ctx->getBelLocation(iob_bel), out_iob->attrs.count(id_DIFF_TYPE));
if (!ctx->checkBelAvail(l_bel)) {
log_error("Can't place %s at %s because it's already taken by %s\n", ctx->nameOf(&ci),
@ -367,8 +382,6 @@ struct GowinPacker
}
ci.setParam(ctx->id("OUTMODE"), out_mode);
// mark IOB as used by IOLOGIC
out_iob->setParam(id_IOLOGIC_IOB, 1);
// disconnect Q output: it is wired internally
nets_to_remove.push_back(ci.getPort(out_port)->name);
out_iob->disconnectPort(id_I);
@ -390,8 +403,7 @@ struct GowinPacker
make_iob_nets(*out_iob);
}
void pack_single_output_iol(CellInfo &ci, std::vector<IdString> &cells_to_remove,
std::vector<IdString> &nets_to_remove)
void pack_single_output_iol(CellInfo &ci, std::vector<IdString> &nets_to_remove)
{
IdString out_port = id_Q;
@ -403,6 +415,9 @@ struct GowinPacker
if (l_bel == BelId()) {
log_error("Can't place IOLOGIC %s at %s\n", ctx->nameOf(&ci), ctx->nameOfBel(iob_bel));
}
// mark IOB as used by IOLOGIC
out_iob->setParam(id_IOLOGIC_IOB, 1);
check_iologic_placement(ci, ctx->getBelLocation(iob_bel), out_iob->attrs.count(id_DIFF_TYPE));
if (!ctx->checkBelAvail(l_bel)) {
log_error("Can't place %s at %s because it's already taken by %s\n", ctx->nameOf(&ci),
@ -420,8 +435,6 @@ struct GowinPacker
}
ci.setParam(ctx->id("OUTMODE"), out_mode);
// mark IOB as used by IOLOGIC
out_iob->setParam(id_IOLOGIC_IOB, 1);
// disconnect Q output: it is wired internally
nets_to_remove.push_back(ci.getPort(out_port)->name);
out_iob->disconnectPort(id_I);
@ -451,12 +464,12 @@ struct GowinPacker
bool is_diff_io(BelId bel) { return ctx->getBoundBelCell(bel)->attrs.count(id_DIFF_TYPE) != 0; }
void create_aux_iologic_cells(CellInfo &ci, IdString mode)
CellInfo *create_aux_iologic_cell(CellInfo &ci, IdString mode, bool io16 = false, int idx = 0)
{
if (ci.type.in(id_ODDR, id_ODDRC, id_OSER4, id_IDDR, id_IDDRC, id_IDES4)) {
return;
return nullptr;
}
IdString aux_name = create_aux_iologic_name(ci.name);
IdString aux_name = create_aux_iologic_name(ci.name, idx);
BelId bel = get_aux_iologic_bel(ci);
BelId io_bel = gwu.get_io_bel_from_iologic(bel);
if (!ctx->checkBelAvail(io_bel)) {
@ -467,15 +480,18 @@ struct GowinPacker
}
ctx->createCell(aux_name, id_IOLOGIC_DUMMY);
CellInfo *aux = ctx->cells[aux_name].get();
aux->addInput(id_PCLK);
ci.copyPortTo(id_PCLK, ctx->cells.at(aux_name).get(), id_PCLK);
aux->addInput(id_RESET);
ci.copyPortTo(id_RESET, ctx->cells.at(aux_name).get(), id_RESET);
ctx->cells.at(aux_name)->setParam(mode, Property("DDRENABLE"));
ctx->cells.at(aux_name)->setAttr(ctx->id("IOLOGIC_TYPE"), Property("DUMMY"));
ctx->cells.at(aux_name)->setAttr(ctx->id("MAIN_CELL"), Property(ci.name.str(ctx)));
CellInfo *aux = ctx->cells.at(aux_name).get();
ci.copyPortTo(id_PCLK, aux, id_PCLK);
ci.copyPortTo(id_RESET, aux, id_RESET);
if (io16) {
aux->setParam(mode, Property("DDRENABLE16"));
} else {
aux->setParam(mode, Property("DDRENABLE"));
}
aux->setAttr(ctx->id("IOLOGIC_TYPE"), Property("DUMMY"));
aux->setAttr(ctx->id("MAIN_CELL"), Property(ci.name.str(ctx)));
ctx->bindBel(bel, aux, PlaceStrength::STRENGTH_LOCKED);
return aux;
}
void reconnect_ides_outs(CellInfo *ci)
@ -516,7 +532,7 @@ struct GowinPacker
}
}
void pack_ides_iol(CellInfo &ci, std::vector<IdString> &cells_to_remove, std::vector<IdString> &nets_to_remove)
void pack_ides_iol(CellInfo &ci, std::vector<IdString> &nets_to_remove)
{
IdString in_port = id_D;
@ -528,6 +544,9 @@ struct GowinPacker
if (l_bel == BelId()) {
log_error("Can't place IOLOGIC %s at %s\n", ctx->nameOf(&ci), ctx->nameOfBel(iob_bel));
}
// mark IOB as used by IOLOGIC
in_iob->setParam(id_IOLOGIC_IOB, 1);
check_iologic_placement(ci, ctx->getBelLocation(iob_bel), in_iob->attrs.count(id_DIFF_TYPE));
if (!ctx->checkBelAvail(l_bel)) {
log_error("Can't place %s at %s because it's already taken by %s\n", ctx->nameOf(&ci),
@ -555,9 +574,7 @@ struct GowinPacker
}
ci.setParam(ctx->id("INMODE"), in_mode);
// mark IOB as used by IOLOGIC
in_iob->setParam(id_IOLOGIC_IOB, 1);
// disconnect Q output: it is wired internally
// disconnect D input: it is wired internally
nets_to_remove.push_back(ci.getPort(in_port)->name);
in_iob->disconnectPort(id_O);
ci.disconnectPort(in_port);
@ -570,7 +587,7 @@ struct GowinPacker
void pack_iologic()
{
log_info("Pack IO logic...\n");
std::vector<IdString> cells_to_remove, nets_to_remove;
std::vector<IdString> nets_to_remove;
for (auto &cell : ctx->cells) {
CellInfo &ci = *cell.second;
@ -581,29 +598,213 @@ struct GowinPacker
log_info("pack %s of type %s.\n", ctx->nameOf(&ci), ci.type.c_str(ctx));
}
if (ci.type.in(id_ODDR, id_ODDRC, id_OSER4, id_OSER8)) {
pack_bi_output_iol(ci, cells_to_remove, nets_to_remove);
create_aux_iologic_cells(ci, ctx->id("OUTMODE"));
pack_bi_output_iol(ci, nets_to_remove);
create_aux_iologic_cell(ci, ctx->id("OUTMODE"));
continue;
}
if (ci.type.in(id_OVIDEO, id_OSER10)) {
pack_single_output_iol(ci, cells_to_remove, nets_to_remove);
create_aux_iologic_cells(ci, ctx->id("OUTMODE"));
pack_single_output_iol(ci, nets_to_remove);
create_aux_iologic_cell(ci, ctx->id("OUTMODE"));
continue;
}
if (ci.type.in(id_IDDR, id_IDDRC, id_IDES4, id_IDES8, id_IDES10, id_IVIDEO)) {
pack_ides_iol(ci, cells_to_remove, nets_to_remove);
create_aux_iologic_cells(ci, ctx->id("INMODE"));
pack_ides_iol(ci, nets_to_remove);
create_aux_iologic_cell(ci, ctx->id("INMODE"));
continue;
}
}
for (auto cell : cells_to_remove) {
ctx->cells.erase(cell);
for (auto net : nets_to_remove) {
ctx->nets.erase(net);
}
}
// ===================================
// IDES16 / OSER16
// ===================================
void check_io16_placement(CellInfo &ci, Loc main_loc, Loc aux_off, int diff /* 1 - diff */)
{
int mod[][3] = {{0, 0, 1}, {1, 1, 0}, {1, 1, 1}};
for (int i = diff; i < 3; ++i) {
Loc aux_loc(main_loc.x + mod[i][0] * aux_off.x, main_loc.y + mod[i][1] * aux_off.y, main_loc.z + mod[i][2]);
BelId l_bel = ctx->getBelByLocation(aux_loc);
if (!ctx->checkBelAvail(l_bel)) {
log_error("Can't place %s at %s because it's already taken by %s\n", ctx->nameOf(&ci),
ctx->nameOfBel(l_bel), ctx->nameOf(ctx->getBoundBelCell(l_bel)));
}
}
}
void pack_oser16(CellInfo &ci, std::vector<IdString> &nets_to_remove)
{
IdString out_port = id_Q;
CellInfo *out_iob = net_only_drives(ctx, ci.ports.at(out_port).net, is_iob, id_I, true);
NPNR_ASSERT(out_iob != nullptr && out_iob->bel != BelId());
// mark IOB as used by IOLOGIC
out_iob->setParam(id_IOLOGIC_IOB, 1);
BelId iob_bel = out_iob->bel;
Loc iob_loc = ctx->getBelLocation(iob_bel);
Loc aux_offset = gwu.get_tile_io16_offs(iob_loc.x, iob_loc.y);
if (aux_offset.x == 0 && aux_offset.y == 0) {
log_error("OSER16 %s can not be placed here\n", ctx->nameOf(&ci));
}
check_io16_placement(ci, iob_loc, aux_offset, out_iob->attrs.count(id_DIFF_TYPE));
BelId main_bel = ctx->getBelByLocation(Loc(iob_loc.x, iob_loc.y, BelZ::OSER16_Z));
ctx->bindBel(main_bel, &ci, PlaceStrength::STRENGTH_LOCKED);
// disconnect Q output: it is wired internally
nets_to_remove.push_back(ci.getPort(out_port)->name);
out_iob->disconnectPort(id_I);
ci.disconnectPort(out_port);
// to simplify packaging, the parts of the OSER16 are presented as IOLOGIC cells
// and one of these aux cells is declared as main
IdString main_name = create_aux_iologic_name(ci.name);
IdString aux_name = create_aux_iologic_name(ci.name, 1);
ctx->createCell(aux_name, id_IOLOGIC_DUMMY);
CellInfo *aux = ctx->cells.at(aux_name).get();
aux->setAttr(ctx->id("MAIN_CELL"), Property(main_name.str(ctx)));
aux->setParam(ctx->id("OUTMODE"), Property("ODDRX8"));
aux->setParam(ctx->id("UPDATE"), Property("SAME"));
aux->setAttr(ctx->id("IOLOGIC_TYPE"), Property("DUMMY"));
ci.copyPortTo(id_PCLK, aux, id_PCLK);
ci.copyPortTo(id_RESET, aux, id_RESET);
ctx->bindBel(ctx->getBelByLocation(Loc(iob_loc.x, iob_loc.y, BelZ::IOLOGICA_Z)), aux,
PlaceStrength::STRENGTH_LOCKED);
// make aux cell in the first cell
aux = create_aux_iologic_cell(*aux, ctx->id("OUTMODE"), true, 2);
aux->setAttr(ctx->id("MAIN_CELL"), Property(main_name.str(ctx)));
aux->setParam(ctx->id("UPDATE"), Property("SAME"));
// make cell in the next location
ctx->createCell(main_name, id_IOLOGIC);
aux = ctx->cells.at(main_name).get();
aux->setAttr(ctx->id("MAIN_CELL"), Property(main_name.str(ctx)));
aux->setParam(ctx->id("OUTMODE"), Property("DDRENABLE16"));
aux->setParam(ctx->id("UPDATE"), Property("SAME"));
aux->setAttr(ctx->id("IOLOGIC_TYPE"), Property("DUMMY"));
ci.copyPortTo(id_PCLK, aux, id_PCLK);
ci.copyPortTo(id_RESET, aux, id_RESET);
ci.movePortTo(id_FCLK, aux, id_FCLK);
ci.movePortTo(id_D12, aux, id_D0);
ci.movePortTo(id_D13, aux, id_D1);
ci.movePortTo(id_D14, aux, id_D2);
ci.movePortTo(id_D15, aux, id_D3);
Loc next_io16(iob_loc.x + aux_offset.x, iob_loc.y + aux_offset.y, BelZ::IOLOGICA_Z);
ctx->bindBel(ctx->getBelByLocation(next_io16), aux, PlaceStrength::STRENGTH_LOCKED);
Loc io_loc = ctx->getBelLocation(iob_bel);
if (io_loc.y == ctx->getGridDimY() - 1) {
config_bottom_row(*out_iob, io_loc, Bottom_io_POD::DDR);
}
make_iob_nets(*out_iob);
}
void pack_ides16(CellInfo &ci, std::vector<IdString> &nets_to_remove)
{
IdString in_port = id_D;
CellInfo *in_iob = net_driven_by(ctx, ci.ports.at(in_port).net, is_iob, id_O);
NPNR_ASSERT(in_iob != nullptr && in_iob->bel != BelId());
// mark IOB as used by IOLOGIC
in_iob->setParam(id_IOLOGIC_IOB, 1);
BelId iob_bel = in_iob->bel;
Loc iob_loc = ctx->getBelLocation(iob_bel);
Loc aux_offset = gwu.get_tile_io16_offs(iob_loc.x, iob_loc.y);
if (aux_offset.x == 0 && aux_offset.y == 0) {
log_error("IDES16 %s can not be placed here\n", ctx->nameOf(&ci));
}
check_io16_placement(ci, iob_loc, aux_offset, in_iob->attrs.count(id_DIFF_TYPE));
BelId main_bel = ctx->getBelByLocation(Loc(iob_loc.x, iob_loc.y, BelZ::IDES16_Z));
ctx->bindBel(main_bel, &ci, PlaceStrength::STRENGTH_LOCKED);
// disconnect Q output: it is wired internally
nets_to_remove.push_back(ci.getPort(in_port)->name);
in_iob->disconnectPort(id_O);
ci.disconnectPort(in_port);
// to simplify packaging, the parts of the IDES16 are presented as IOLOGIC cells
// and one of these aux cells is declared as main
IdString main_name = create_aux_iologic_name(ci.name);
IdString aux_name = create_aux_iologic_name(ci.name, 1);
ctx->createCell(aux_name, id_IOLOGIC_DUMMY);
CellInfo *aux = ctx->cells.at(aux_name).get();
aux->setAttr(ctx->id("MAIN_CELL"), Property(main_name.str(ctx)));
aux->setParam(ctx->id("INMODE"), Property("IDDRX8"));
aux->setAttr(ctx->id("IOLOGIC_TYPE"), Property("DUMMY"));
ci.copyPortTo(id_PCLK, aux, id_PCLK);
ci.copyPortTo(id_RESET, aux, id_RESET);
ctx->bindBel(ctx->getBelByLocation(Loc(iob_loc.x, iob_loc.y, BelZ::IOLOGICA_Z)), aux,
PlaceStrength::STRENGTH_LOCKED);
// make aux cell in the first cell
aux = create_aux_iologic_cell(*aux, ctx->id("INMODE"), true, 2);
aux->setAttr(ctx->id("MAIN_CELL"), Property(main_name.str(ctx)));
ci.copyPortTo(id_CALIB, aux, id_CALIB);
// make cell in the next location
ctx->createCell(main_name, id_IOLOGIC);
aux = ctx->cells.at(main_name).get();
aux->setAttr(ctx->id("MAIN_CELL"), Property(main_name.str(ctx)));
aux->setParam(ctx->id("INMODE"), Property("DDRENABLE16"));
aux->setAttr(ctx->id("IOLOGIC_TYPE"), Property("DUMMY"));
ci.copyPortTo(id_PCLK, aux, id_PCLK);
ci.copyPortTo(id_RESET, aux, id_RESET);
ci.copyPortTo(id_CALIB, aux, id_CALIB);
ci.movePortTo(id_FCLK, aux, id_FCLK);
ci.movePortTo(id_Q0, aux, id_Q6);
ci.movePortTo(id_Q1, aux, id_Q7);
ci.movePortTo(id_Q2, aux, id_Q8);
ci.movePortTo(id_Q3, aux, id_Q9);
Loc next_io16(iob_loc.x + aux_offset.x, iob_loc.y + aux_offset.y, BelZ::IOLOGICA_Z);
ctx->bindBel(ctx->getBelByLocation(next_io16), aux, PlaceStrength::STRENGTH_LOCKED);
make_iob_nets(*in_iob);
}
void pack_io16(void)
{
std::vector<IdString> nets_to_remove;
log_info("Pack DESER16 logic...\n");
for (auto &cell : ctx->cells) {
CellInfo &ci = *cell.second;
if (ci.type == id_OSER16) {
if (ctx->debug) {
log_info("pack %s of type %s.\n", ctx->nameOf(&ci), ci.type.c_str(ctx));
}
pack_oser16(ci, nets_to_remove);
continue;
}
if (ci.type == id_IDES16) {
if (ctx->debug) {
log_info("pack %s of type %s.\n", ctx->nameOf(&ci), ci.type.c_str(ctx));
}
pack_ides16(ci, nets_to_remove);
continue;
}
}
for (auto net : nets_to_remove) {
ctx->nets.erase(net);
}
}
// ===================================
// Constant nets
// ===================================
@ -1123,6 +1324,9 @@ struct GowinPacker
pack_iologic();
ctx->check();
pack_io16();
ctx->check();
pack_gsr();
ctx->check();