gowin: Himbaechel. Add constraint file processing.
- minor fixes for pinout saving; - CST parser taken from generic-based apicula; - $nextpnr IOB detachment is copied here because it is necessary to copy attributes from deleted bels. Signed-off-by: YRabbit <rabbit@yrabbit.cyou>
This commit is contained in:
parent
3d3039e25c
commit
6cac19c055
@ -388,7 +388,10 @@ class PadInfo(BBAStruct):
|
|||||||
extra_data: object = None
|
extra_data: object = None
|
||||||
|
|
||||||
def serialise_lists(self, context: str, bba: BBAWriter):
|
def serialise_lists(self, context: str, bba: BBAWriter):
|
||||||
pass
|
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):
|
def serialise(self, context: str, bba: BBAWriter):
|
||||||
bba.u32(self.package_pin.index)
|
bba.u32(self.package_pin.index)
|
||||||
bba.u32(self.tile.index)
|
bba.u32(self.tile.index)
|
||||||
@ -414,8 +417,11 @@ class PackageInfo(BBAStruct):
|
|||||||
return pad
|
return pad
|
||||||
|
|
||||||
def serialise_lists(self, context: str, bba: BBAWriter):
|
def serialise_lists(self, context: str, bba: BBAWriter):
|
||||||
for i, pad in enumerate(self.pad):
|
for i, pad in enumerate(self.pads):
|
||||||
bel.serialise_lists(f"{context}_pad{i}", pad)
|
pad.serialise_lists(f"{context}_pad{i}", bba)
|
||||||
|
bba.label(f"{context}_pads")
|
||||||
|
for i, pad in enumerate(self.pads):
|
||||||
|
pad.serialise(f"{context}_pad{i}", bba)
|
||||||
|
|
||||||
def serialise(self, context: str, bba: BBAWriter):
|
def serialise(self, context: str, bba: BBAWriter):
|
||||||
bba.u32(self.name.index)
|
bba.u32(self.name.index)
|
||||||
@ -515,6 +521,8 @@ class Chip:
|
|||||||
shp.serialise_lists(f"nshp{i}", bba)
|
shp.serialise_lists(f"nshp{i}", bba)
|
||||||
for i, tsh in enumerate(self.tile_shapes):
|
for i, tsh in enumerate(self.tile_shapes):
|
||||||
tsh.serialise_lists(f"tshp{i}", bba)
|
tsh.serialise_lists(f"tshp{i}", bba)
|
||||||
|
for i, pkg in enumerate(self.packages):
|
||||||
|
pkg.serialise_lists(f"pkg{i}", bba)
|
||||||
for y, row in enumerate(self.tiles):
|
for y, row in enumerate(self.tiles):
|
||||||
for x, tinst in enumerate(row):
|
for x, tinst in enumerate(row):
|
||||||
tinst.serialise_lists(f"tinst_{x}_{y}", bba)
|
tinst.serialise_lists(f"tinst_{x}_{y}", bba)
|
||||||
@ -530,6 +538,9 @@ class Chip:
|
|||||||
bba.label(f"tile_shapes")
|
bba.label(f"tile_shapes")
|
||||||
for i, tsh in enumerate(self.tile_shapes):
|
for i, tsh in enumerate(self.tile_shapes):
|
||||||
tsh.serialise(f"tshp{i}", bba)
|
tsh.serialise(f"tshp{i}", bba)
|
||||||
|
bba.label(f"packages")
|
||||||
|
for i, pkg in enumerate(self.packages):
|
||||||
|
pkg.serialise(f"pkg{i}", bba)
|
||||||
bba.label(f"tile_insts")
|
bba.label(f"tile_insts")
|
||||||
for y, row in enumerate(self.tiles):
|
for y, row in enumerate(self.tiles):
|
||||||
for x, tinst in enumerate(row):
|
for x, tinst in enumerate(row):
|
||||||
|
191
himbaechel/uarch/gowin/cst.cc
Normal file
191
himbaechel/uarch/gowin/cst.cc
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
#include <boost/algorithm/string.hpp>
|
||||||
|
#include <regex>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
#include "nextpnr.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
#define HIMBAECHEL_CONSTIDS "uarch/gowin/constids.inc"
|
||||||
|
#include "himbaechel_constids.h"
|
||||||
|
#include "himbaechel_helpers.h"
|
||||||
|
|
||||||
|
#include "cst.h"
|
||||||
|
#include "gowin.h"
|
||||||
|
|
||||||
|
NEXTPNR_NAMESPACE_BEGIN
|
||||||
|
|
||||||
|
struct GowinCstReader
|
||||||
|
{
|
||||||
|
Context *ctx;
|
||||||
|
std::istream ∈
|
||||||
|
|
||||||
|
GowinCstReader(Context *ctx, std::istream &in) : ctx(ctx), in(in){};
|
||||||
|
|
||||||
|
const PadInfoPOD *pinLookup(const PadInfoPOD *list, const size_t len, const IdString idx)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
const PadInfoPOD *pin = &list[i];
|
||||||
|
if (IdString(pin->package_pin) == idx) {
|
||||||
|
return pin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Loc getLoc(std::smatch match, int maxX, int maxY)
|
||||||
|
{
|
||||||
|
int col = std::stoi(match[2]);
|
||||||
|
int row = 1; // Top
|
||||||
|
std::string side = match[1].str();
|
||||||
|
if (side == "R") {
|
||||||
|
row = col;
|
||||||
|
col = maxX;
|
||||||
|
} else if (side == "B") {
|
||||||
|
row = maxY;
|
||||||
|
} else if (side == "L") {
|
||||||
|
row = col;
|
||||||
|
col = 1;
|
||||||
|
}
|
||||||
|
int z = match[3].str()[0] - 'A';
|
||||||
|
return Loc(col - 1, row - 1, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool run(void)
|
||||||
|
{
|
||||||
|
pool<std::pair<IdString, IdStringList>> constrained_cells;
|
||||||
|
auto debug_cell = [this, &constrained_cells](IdString cellId, IdStringList belId) {
|
||||||
|
if (ctx->debug) {
|
||||||
|
constrained_cells.insert(std::make_pair(cellId, belId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log_info("Reading constraints...\n");
|
||||||
|
try {
|
||||||
|
// If two locations are specified separated by commas (for differential I/O buffers),
|
||||||
|
// only the first location is actually recognized and used.
|
||||||
|
// And pin A will be Positive and pin B will be Negative in any case.
|
||||||
|
std::regex iobre = std::regex("IO_LOC +\"([^\"]+)\" +([^ ,;]+)(, *[^ ;]+)? *;.*[\\s\\S]*");
|
||||||
|
std::regex portre = std::regex("IO_PORT +\"([^\"]+)\" +([^;]+;).*[\\s\\S]*");
|
||||||
|
std::regex port_attrre = std::regex("([^ =;]+=[^ =;]+) *([^;]*;)");
|
||||||
|
std::regex iobelre = std::regex("IO([TRBL])([0-9]+)\\[?([A-Z])\\]?");
|
||||||
|
std::regex inslocre =
|
||||||
|
std::regex("INS_LOC +\"([^\"]+)\" +R([0-9]+)C([0-9]+)\\[([0-9])\\]\\[([AB])\\] *;.*[\\s\\S]*");
|
||||||
|
std::regex clockre = std::regex("CLOCK_LOC +\"([^\"]+)\" +BUF([GS])(\\[([0-7])\\])?[^;]*;.*[\\s\\S]*");
|
||||||
|
std::smatch match, match_attr, match_pinloc;
|
||||||
|
std::string line, pinline;
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
ioloc,
|
||||||
|
ioport,
|
||||||
|
insloc,
|
||||||
|
clock
|
||||||
|
} cst_type;
|
||||||
|
|
||||||
|
while (!in.eof()) {
|
||||||
|
std::getline(in, line);
|
||||||
|
cst_type = ioloc;
|
||||||
|
if (!std::regex_match(line, match, iobre)) {
|
||||||
|
if (std::regex_match(line, match, portre)) {
|
||||||
|
cst_type = ioport;
|
||||||
|
} else {
|
||||||
|
if (std::regex_match(line, match, clockre)) {
|
||||||
|
cst_type = clock;
|
||||||
|
} else {
|
||||||
|
if (std::regex_match(line, match, inslocre)) {
|
||||||
|
cst_type = insloc;
|
||||||
|
} else {
|
||||||
|
if ((!line.empty()) && (line.rfind("//", 0) == std::string::npos)) {
|
||||||
|
log_warning("Invalid constraint: %s\n", line.c_str());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IdString net = ctx->id(match[1]);
|
||||||
|
auto it = ctx->cells.find(net);
|
||||||
|
if (cst_type != clock && it == ctx->cells.end()) {
|
||||||
|
log_info("Cell %s not found\n", net.c_str(ctx));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (cst_type) {
|
||||||
|
case clock: { // CLOCK name BUFG|S=#
|
||||||
|
std::string which_clock = match[2];
|
||||||
|
std::string lw = match[4];
|
||||||
|
int lw_idx = -1;
|
||||||
|
if (lw.length() > 0) {
|
||||||
|
lw_idx = atoi(lw.c_str());
|
||||||
|
log_info("lw_idx:%d\n", lw_idx);
|
||||||
|
}
|
||||||
|
if (which_clock.at(0) == 'S') {
|
||||||
|
auto ni = ctx->nets.find(net);
|
||||||
|
if (ni == ctx->nets.end()) {
|
||||||
|
log_info("Net %s not found\n", net.c_str(ctx));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// if (!allocate_longwire(ni->second.get(), lw_idx)) {
|
||||||
|
log_info("Can't use the long wires. The %s network will use normal routing.\n", net.c_str(ctx));
|
||||||
|
//}
|
||||||
|
} else {
|
||||||
|
log_info("BUFG isn't supported\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case ioloc: { // IO_LOC name pin
|
||||||
|
IdString pinname = ctx->id(match[2]);
|
||||||
|
pinline = match[2];
|
||||||
|
|
||||||
|
const PadInfoPOD *belname =
|
||||||
|
pinLookup(ctx->package_info->pads.get(), ctx->package_info->pads.ssize(), pinname);
|
||||||
|
if (belname != nullptr) {
|
||||||
|
IdStringList bel = IdStringList::concat(IdString(belname->tile), IdString(belname->bel));
|
||||||
|
it->second->setAttr(IdString(ID_BEL), bel.str(ctx));
|
||||||
|
debug_cell(it->second->name, bel);
|
||||||
|
} else {
|
||||||
|
if (std::regex_match(pinline, match_pinloc, iobelre)) {
|
||||||
|
// may be it's IOx#[AB] style?
|
||||||
|
Loc loc = getLoc(match_pinloc, ctx->getGridDimX(), ctx->getGridDimY());
|
||||||
|
BelId bel = ctx->getBelByLocation(loc);
|
||||||
|
if (bel == BelId()) {
|
||||||
|
log_error("Pin %s not found (TRBL style). \n", pinline.c_str());
|
||||||
|
}
|
||||||
|
it->second->setAttr(IdString(ID_BEL), std::string(ctx->nameOfBel(bel)));
|
||||||
|
debug_cell(it->second->name, ctx->getBelName(bel));
|
||||||
|
} else {
|
||||||
|
log_error("Pin %s not found (pin# style)\n", pinname.c_str(ctx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
default: { // IO_PORT attr=value
|
||||||
|
std::string attr_val = match[2];
|
||||||
|
while (std::regex_match(attr_val, match_attr, port_attrre)) {
|
||||||
|
std::string attr = "&";
|
||||||
|
attr += match_attr[1];
|
||||||
|
boost::algorithm::to_upper(attr);
|
||||||
|
it->second->setAttr(ctx->id(attr), 1);
|
||||||
|
attr_val = match_attr[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx->debug) {
|
||||||
|
for (auto &cell : constrained_cells) {
|
||||||
|
log_info("Cell %s is constrained to %s\n", cell.first.c_str(ctx), cell.second.str(ctx).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (log_execution_error_exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool gowin_apply_constraints(Context *ctx, std::istream &in)
|
||||||
|
{
|
||||||
|
GowinCstReader reader(ctx, in);
|
||||||
|
return reader.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
NEXTPNR_NAMESPACE_END
|
13
himbaechel/uarch/gowin/cst.h
Normal file
13
himbaechel/uarch/gowin/cst.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#ifndef GOWIN_CST_H
|
||||||
|
#define GOWIN_CST_H
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include "nextpnr.h"
|
||||||
|
|
||||||
|
NEXTPNR_NAMESPACE_BEGIN
|
||||||
|
|
||||||
|
bool gowin_apply_constraints(Context *ctx, std::istream &in);
|
||||||
|
|
||||||
|
NEXTPNR_NAMESPACE_END
|
||||||
|
|
||||||
|
#endif
|
@ -1,3 +1,5 @@
|
|||||||
|
#include <regex>
|
||||||
|
|
||||||
#include "himbaechel_api.h"
|
#include "himbaechel_api.h"
|
||||||
#include "himbaechel_helpers.h"
|
#include "himbaechel_helpers.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
@ -7,6 +9,7 @@
|
|||||||
#define HIMBAECHEL_CONSTIDS "uarch/gowin/constids.inc"
|
#define HIMBAECHEL_CONSTIDS "uarch/gowin/constids.inc"
|
||||||
#include "himbaechel_constids.h"
|
#include "himbaechel_constids.h"
|
||||||
|
|
||||||
|
#include "cst.h"
|
||||||
#include "globals.h"
|
#include "globals.h"
|
||||||
#include "gowin.h"
|
#include "gowin.h"
|
||||||
#include "pack.h"
|
#include "pack.h"
|
||||||
@ -76,8 +79,6 @@ void GowinImpl::init(Context *ctx)
|
|||||||
if (!args.options.count("partno")) {
|
if (!args.options.count("partno")) {
|
||||||
log_error("Partnumber (like --vopt partno=GW1NR-LV9QN88PC6/I5) must be specified.\n");
|
log_error("Partnumber (like --vopt partno=GW1NR-LV9QN88PC6/I5) must be specified.\n");
|
||||||
}
|
}
|
||||||
ctx->settings[ctx->id("packer.partno")] = args.options.at("partno");
|
|
||||||
|
|
||||||
// GW1N-9C.xxx -> GW1N-9C
|
// GW1N-9C.xxx -> GW1N-9C
|
||||||
std::string chipdb = args.chipdb;
|
std::string chipdb = args.chipdb;
|
||||||
auto dot_pos = chipdb.find(".");
|
auto dot_pos = chipdb.find(".");
|
||||||
@ -85,10 +86,60 @@ void GowinImpl::init(Context *ctx)
|
|||||||
chipdb.resize(dot_pos);
|
chipdb.resize(dot_pos);
|
||||||
}
|
}
|
||||||
chip = ctx->id(chipdb);
|
chip = ctx->id(chipdb);
|
||||||
partno = ctx->id(args.options.at("partno"));
|
std::string pn = args.options.at("partno");
|
||||||
|
partno = ctx->id(pn);
|
||||||
|
ctx->settings[ctx->id("packer.partno")] = pn;
|
||||||
|
|
||||||
|
std::regex speedre = std::regex("(.*)(C[0-9]/I[0-9])$");
|
||||||
|
std::smatch match;
|
||||||
|
|
||||||
|
IdString spd;
|
||||||
|
IdString package_idx;
|
||||||
|
if (std::regex_match(pn, match, speedre)) {
|
||||||
|
package_idx = ctx->id(match[1]);
|
||||||
|
spd = ctx->id(match[2]);
|
||||||
|
} else {
|
||||||
|
if (pn.length() > 2 && pn.compare(pn.length() - 2, 2, "ES")) {
|
||||||
|
package_idx = ctx->id(pn.substr(pn.length() - 2));
|
||||||
|
spd = ctx->id("ES");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->debug) {
|
||||||
|
log_info("packages:%d\n", ctx->chip_info->packages.ssize());
|
||||||
|
}
|
||||||
|
for (int i = 0; i < ctx->chip_info->packages.ssize(); ++i) {
|
||||||
|
if (IdString(ctx->chip_info->packages[i].name) == package_idx) {
|
||||||
|
if (ctx->debug) {
|
||||||
|
log_info("i:%d %s\n", i, package_idx.c_str(ctx));
|
||||||
|
}
|
||||||
|
ctx->package_info = &ctx->chip_info->packages[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx->package_info == nullptr) {
|
||||||
|
log_error("No package for partnumber %s\n", partno.c_str(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.options.count("cst")) {
|
||||||
|
ctx->settings[ctx->id("cst.filename")] = args.options.at("cst");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GowinImpl::pack() { gowin_pack(ctx); }
|
void GowinImpl::pack()
|
||||||
|
{
|
||||||
|
if (ctx->settings.count(ctx->id("cst.filename"))) {
|
||||||
|
std::string filename = ctx->settings[ctx->id("cst.filename")].as_string();
|
||||||
|
std::ifstream in(filename);
|
||||||
|
if (!in) {
|
||||||
|
log_error("failed to open CST file '%s'\n", filename.c_str());
|
||||||
|
}
|
||||||
|
if (!gowin_apply_constraints(ctx, in)) {
|
||||||
|
log_error("failed to parse CST file '%s'\n", filename.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gowin_pack(ctx);
|
||||||
|
}
|
||||||
void GowinImpl::prePlace() { assign_cell_info(); }
|
void GowinImpl::prePlace() { assign_cell_info(); }
|
||||||
void GowinImpl::postPlace()
|
void GowinImpl::postPlace()
|
||||||
{
|
{
|
||||||
@ -99,7 +150,7 @@ void GowinImpl::postPlace()
|
|||||||
IdStringList bel = ctx->getBelName(ci->bel);
|
IdStringList bel = ctx->getBelName(ci->bel);
|
||||||
log_info("%s -> %s\n", ctx->nameOf(ci), bel.str(ctx).c_str());
|
log_info("%s -> %s\n", ctx->nameOf(ci), bel.str(ctx).c_str());
|
||||||
}
|
}
|
||||||
log_info("======================================================\n");
|
log_break();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,8 +353,8 @@ def create_packages(chip: Chip, db: chipdb):
|
|||||||
partno = partno_spd.removesuffix(spd) # drop SPEED like 'C7/I6'
|
partno = partno_spd.removesuffix(spd) # drop SPEED like 'C7/I6'
|
||||||
if partno in created_pkgs:
|
if partno in created_pkgs:
|
||||||
continue
|
continue
|
||||||
|
created_pkgs.add(partno)
|
||||||
pkg = chip.create_package(partno)
|
pkg = chip.create_package(partno)
|
||||||
print(partno)
|
|
||||||
for pinno, pininfo in db.pinout[variant][pkgname].items():
|
for pinno, pininfo in db.pinout[variant][pkgname].items():
|
||||||
io_loc, cfgs = pininfo
|
io_loc, cfgs = pininfo
|
||||||
tile, bel = ioloc_to_tile_bel(io_loc)
|
tile, bel = ioloc_to_tile_bel(io_loc)
|
||||||
@ -391,7 +391,7 @@ def main():
|
|||||||
# these differences (in case it turns out later that there is a slightly
|
# these differences (in case it turns out later that there is a slightly
|
||||||
# different routing or something like that).
|
# different routing or something like that).
|
||||||
logic_tiletypes = {12, 13, 14, 15, 16}
|
logic_tiletypes = {12, 13, 14, 15, 16}
|
||||||
io_tiletypes = {53, 55, 58, 59, 64, 65, 66}
|
io_tiletypes = {52, 53, 55, 58, 59, 64, 65, 66}
|
||||||
ssram_tiletypes = {17, 18, 19}
|
ssram_tiletypes = {17, 18, 19}
|
||||||
# Setup tile grid
|
# Setup tile grid
|
||||||
for x in range(X):
|
for x in range(X):
|
||||||
|
@ -29,7 +29,37 @@ struct GowinPacker
|
|||||||
CellTypePort(id_IBUF, id_I),
|
CellTypePort(id_IBUF, id_I),
|
||||||
CellTypePort(id_OBUF, id_O),
|
CellTypePort(id_OBUF, id_O),
|
||||||
};
|
};
|
||||||
h.remove_nextpnr_iobs(top_ports);
|
std::vector<IdString> to_remove;
|
||||||
|
for (auto &cell : ctx->cells) {
|
||||||
|
auto &ci = *cell.second;
|
||||||
|
if (!ci.type.in(ctx->id("$nextpnr_ibuf"), ctx->id("$nextpnr_obuf"), ctx->id("$nextpnr_iobuf")))
|
||||||
|
continue;
|
||||||
|
NetInfo *i = ci.getPort(ctx->id("I"));
|
||||||
|
if (i && i->driver.cell) {
|
||||||
|
if (!top_ports.count(CellTypePort(i->driver)))
|
||||||
|
log_error("Top-level port '%s' driven by illegal port %s.%s\n", ctx->nameOf(&ci),
|
||||||
|
ctx->nameOf(i->driver.cell), ctx->nameOf(i->driver.port));
|
||||||
|
for (const auto &attr : ci.attrs) {
|
||||||
|
i->driver.cell->attrs[attr.first] = attr.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NetInfo *o = ci.getPort(ctx->id("O"));
|
||||||
|
if (o) {
|
||||||
|
for (auto &usr : o->users) {
|
||||||
|
if (!top_ports.count(CellTypePort(usr)))
|
||||||
|
log_error("Top-level port '%s' driving illegal port %s.%s\n", ctx->nameOf(&ci),
|
||||||
|
ctx->nameOf(usr.cell), ctx->nameOf(usr.port));
|
||||||
|
for (const auto &attr : ci.attrs) {
|
||||||
|
usr.cell->attrs[attr.first] = attr.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ci.disconnectPort(ctx->id("I"));
|
||||||
|
ci.disconnectPort(ctx->id("O"));
|
||||||
|
to_remove.push_back(ci.name);
|
||||||
|
}
|
||||||
|
for (IdString cell_name : to_remove)
|
||||||
|
ctx->cells.erase(cell_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================================
|
// ===================================
|
||||||
|
Loading…
Reference in New Issue
Block a user