Merge pull request #103 from YosysHQ/timingapi
Timing constraints API, multiple clock domains
This commit is contained in:
commit
9472b6d78f
@ -53,6 +53,107 @@ void IdString::initialize_add(const BaseCtx *ctx, const char *s, int idx)
|
||||
ctx->idstring_idx_to_str->push_back(&insert_rc.first->first);
|
||||
}
|
||||
|
||||
TimingConstrObjectId BaseCtx::timingWildcardObject()
|
||||
{
|
||||
TimingConstrObjectId id;
|
||||
id.index = 0;
|
||||
return id;
|
||||
}
|
||||
|
||||
TimingConstrObjectId BaseCtx::timingClockDomainObject(NetInfo *clockDomain)
|
||||
{
|
||||
NPNR_ASSERT(clockDomain->clkconstr != nullptr);
|
||||
if (clockDomain->clkconstr->domain_tmg_id != TimingConstrObjectId()) {
|
||||
return clockDomain->clkconstr->domain_tmg_id;
|
||||
} else {
|
||||
TimingConstraintObject obj;
|
||||
TimingConstrObjectId id;
|
||||
id.index = int(constraintObjects.size());
|
||||
obj.id = id;
|
||||
obj.type = TimingConstraintObject::CLOCK_DOMAIN;
|
||||
obj.entity = clockDomain->name;
|
||||
clockDomain->clkconstr->domain_tmg_id = id;
|
||||
constraintObjects.push_back(obj);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
TimingConstrObjectId BaseCtx::timingNetObject(NetInfo *net)
|
||||
{
|
||||
if (net->tmg_id != TimingConstrObjectId()) {
|
||||
return net->tmg_id;
|
||||
} else {
|
||||
TimingConstraintObject obj;
|
||||
TimingConstrObjectId id;
|
||||
id.index = int(constraintObjects.size());
|
||||
obj.id = id;
|
||||
obj.type = TimingConstraintObject::NET;
|
||||
obj.entity = net->name;
|
||||
constraintObjects.push_back(obj);
|
||||
net->tmg_id = id;
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
TimingConstrObjectId BaseCtx::timingCellObject(CellInfo *cell)
|
||||
{
|
||||
if (cell->tmg_id != TimingConstrObjectId()) {
|
||||
return cell->tmg_id;
|
||||
} else {
|
||||
TimingConstraintObject obj;
|
||||
TimingConstrObjectId id;
|
||||
id.index = int(constraintObjects.size());
|
||||
obj.id = id;
|
||||
obj.type = TimingConstraintObject::CELL;
|
||||
obj.entity = cell->name;
|
||||
constraintObjects.push_back(obj);
|
||||
cell->tmg_id = id;
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
TimingConstrObjectId BaseCtx::timingPortObject(CellInfo *cell, IdString port)
|
||||
{
|
||||
if (cell->ports.at(port).tmg_id != TimingConstrObjectId()) {
|
||||
return cell->ports.at(port).tmg_id;
|
||||
} else {
|
||||
TimingConstraintObject obj;
|
||||
TimingConstrObjectId id;
|
||||
id.index = int(constraintObjects.size());
|
||||
obj.id = id;
|
||||
obj.type = TimingConstraintObject::CELL_PORT;
|
||||
obj.entity = cell->name;
|
||||
obj.port = port;
|
||||
constraintObjects.push_back(obj);
|
||||
cell->ports.at(port).tmg_id = id;
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
void BaseCtx::addConstraint(std::unique_ptr<TimingConstraint> constr)
|
||||
{
|
||||
for (auto fromObj : constr->from)
|
||||
constrsFrom.emplace(fromObj, constr.get());
|
||||
for (auto toObj : constr->to)
|
||||
constrsTo.emplace(toObj, constr.get());
|
||||
IdString name = constr->name;
|
||||
constraints[name] = std::move(constr);
|
||||
}
|
||||
|
||||
void BaseCtx::removeConstraint(IdString constrName)
|
||||
{
|
||||
TimingConstraint *constr = constraints[constrName].get();
|
||||
for (auto fromObj : constr->from) {
|
||||
auto fromConstrs = constrsFrom.equal_range(fromObj);
|
||||
constrsFrom.erase(std::find(fromConstrs.first, fromConstrs.second, std::make_pair(fromObj, constr)));
|
||||
}
|
||||
for (auto toObj : constr->to) {
|
||||
auto toConstrs = constrsFrom.equal_range(toObj);
|
||||
constrsFrom.erase(std::find(toConstrs.first, toConstrs.second, std::make_pair(toObj, constr)));
|
||||
}
|
||||
constraints.erase(constrName);
|
||||
}
|
||||
|
||||
const char *BaseCtx::nameOfBel(BelId bel) const
|
||||
{
|
||||
const Context *ctx = getCtx();
|
||||
@ -306,4 +407,14 @@ void Context::check() const
|
||||
}
|
||||
}
|
||||
|
||||
void BaseCtx::addClock(IdString net, float freq)
|
||||
{
|
||||
log_info("constraining clock net '%s' to %.02f MHz\n", net.c_str(this), freq);
|
||||
std::unique_ptr<ClockConstraint> cc(new ClockConstraint());
|
||||
cc->period = getCtx()->getDelayFromNS(1000 / freq);
|
||||
cc->high = getCtx()->getDelayFromNS(500 / freq);
|
||||
cc->low = getCtx()->getDelayFromNS(500 / freq);
|
||||
nets.at(net)->clkconstr = std::move(cc);
|
||||
}
|
||||
|
||||
NEXTPNR_NAMESPACE_END
|
||||
|
116
common/nextpnr.h
116
common/nextpnr.h
@ -194,6 +194,14 @@ struct Loc
|
||||
bool operator!=(const Loc &other) const { return (x != other.x) || (y != other.y) || (z != other.z); }
|
||||
};
|
||||
|
||||
struct TimingConstrObjectId
|
||||
{
|
||||
int32_t index = -1;
|
||||
|
||||
bool operator==(const TimingConstrObjectId &other) const { return index == other.index; }
|
||||
bool operator!=(const TimingConstrObjectId &other) const { return index != other.index; }
|
||||
};
|
||||
|
||||
NEXTPNR_NAMESPACE_END
|
||||
|
||||
namespace std {
|
||||
@ -208,6 +216,15 @@ template <> struct hash<NEXTPNR_NAMESPACE_PREFIX Loc>
|
||||
return seed;
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<NEXTPNR_NAMESPACE_PREFIX TimingConstrObjectId>
|
||||
{
|
||||
std::size_t operator()(const NEXTPNR_NAMESPACE_PREFIX TimingConstrObjectId &obj) const noexcept
|
||||
{
|
||||
return hash<int>()(obj.index);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace std
|
||||
|
||||
#include "archdefs.h"
|
||||
@ -266,6 +283,8 @@ struct PipMap
|
||||
PlaceStrength strength = STRENGTH_NONE;
|
||||
};
|
||||
|
||||
struct ClockConstraint;
|
||||
|
||||
struct NetInfo : ArchNetInfo
|
||||
{
|
||||
IdString name;
|
||||
@ -278,6 +297,10 @@ struct NetInfo : ArchNetInfo
|
||||
// wire -> uphill_pip
|
||||
std::unordered_map<WireId, PipMap> wires;
|
||||
|
||||
std::unique_ptr<ClockConstraint> clkconstr;
|
||||
|
||||
TimingConstrObjectId tmg_id;
|
||||
|
||||
Region *region = nullptr;
|
||||
};
|
||||
|
||||
@ -293,6 +316,7 @@ struct PortInfo
|
||||
IdString name;
|
||||
NetInfo *net;
|
||||
PortType type;
|
||||
TimingConstrObjectId tmg_id;
|
||||
};
|
||||
|
||||
struct CellInfo : ArchCellInfo
|
||||
@ -320,6 +344,7 @@ struct CellInfo : ArchCellInfo
|
||||
// parent.[xyz] := 0 when (constr_parent == nullptr)
|
||||
|
||||
Region *region = nullptr;
|
||||
TimingConstrObjectId tmg_id;
|
||||
};
|
||||
|
||||
enum TimingPortClass
|
||||
@ -335,6 +360,68 @@ enum TimingPortClass
|
||||
TMG_IGNORE, // Asynchronous to all clocks, "don't care", and should be ignored (false path) for analysis
|
||||
};
|
||||
|
||||
enum ClockEdge
|
||||
{
|
||||
RISING_EDGE,
|
||||
FALLING_EDGE
|
||||
};
|
||||
|
||||
struct TimingClockingInfo
|
||||
{
|
||||
IdString clock_port; // Port name of clock domain
|
||||
ClockEdge edge;
|
||||
DelayInfo setup, hold; // Input timing checks
|
||||
DelayInfo clockToQ; // Output clock-to-Q time
|
||||
};
|
||||
|
||||
struct ClockConstraint
|
||||
{
|
||||
DelayInfo high;
|
||||
DelayInfo low;
|
||||
DelayInfo period;
|
||||
|
||||
TimingConstrObjectId domain_tmg_id;
|
||||
};
|
||||
|
||||
struct TimingConstraintObject
|
||||
{
|
||||
TimingConstrObjectId id;
|
||||
enum
|
||||
{
|
||||
ANYTHING,
|
||||
CLOCK_DOMAIN,
|
||||
NET,
|
||||
CELL,
|
||||
CELL_PORT
|
||||
} type;
|
||||
IdString entity; // Name of clock net; net or cell
|
||||
IdString port; // Name of port on a cell
|
||||
};
|
||||
|
||||
struct TimingConstraint
|
||||
{
|
||||
IdString name;
|
||||
|
||||
enum
|
||||
{
|
||||
FALSE_PATH,
|
||||
MIN_DELAY,
|
||||
MAX_DELAY,
|
||||
MULTICYCLE,
|
||||
} type;
|
||||
|
||||
delay_t value;
|
||||
|
||||
std::unordered_set<TimingConstrObjectId> from;
|
||||
std::unordered_set<TimingConstrObjectId> to;
|
||||
};
|
||||
|
||||
inline bool operator==(const std::pair<const TimingConstrObjectId, TimingConstraint *> &a,
|
||||
const std::pair<TimingConstrObjectId, TimingConstraint *> &b)
|
||||
{
|
||||
return a.first == b.first && a.second == b.second;
|
||||
}
|
||||
|
||||
struct DeterministicRNG
|
||||
{
|
||||
uint64_t rngstate;
|
||||
@ -431,6 +518,11 @@ struct BaseCtx
|
||||
idstring_idx_to_str = new std::vector<const std::string *>;
|
||||
IdString::initialize_add(this, "", 0);
|
||||
IdString::initialize_arch(this);
|
||||
|
||||
TimingConstraintObject wildcard;
|
||||
wildcard.id.index = 0;
|
||||
wildcard.type = TimingConstraintObject::ANYTHING;
|
||||
constraintObjects.push_back(wildcard);
|
||||
}
|
||||
|
||||
~BaseCtx()
|
||||
@ -524,6 +616,30 @@ struct BaseCtx
|
||||
void refreshUiPip(PipId pip) { pipUiReload.insert(pip); }
|
||||
|
||||
void refreshUiGroup(GroupId group) { groupUiReload.insert(group); }
|
||||
|
||||
// --------------------------------------------------------------
|
||||
|
||||
// Timing Constraint API
|
||||
|
||||
// constraint name -> constraint
|
||||
std::unordered_map<IdString, std::unique_ptr<TimingConstraint>> constraints;
|
||||
// object ID -> object
|
||||
std::vector<TimingConstraintObject> constraintObjects;
|
||||
// object ID -> constraint
|
||||
std::unordered_multimap<TimingConstrObjectId, TimingConstraint *> constrsFrom;
|
||||
std::unordered_multimap<TimingConstrObjectId, TimingConstraint *> constrsTo;
|
||||
|
||||
TimingConstrObjectId timingWildcardObject();
|
||||
TimingConstrObjectId timingClockDomainObject(NetInfo *clockDomain);
|
||||
TimingConstrObjectId timingNetObject(NetInfo *net);
|
||||
TimingConstrObjectId timingCellObject(CellInfo *cell);
|
||||
TimingConstrObjectId timingPortObject(CellInfo *cell, IdString port);
|
||||
|
||||
void addConstraint(std::unique_ptr<TimingConstraint> constr);
|
||||
void removeConstraint(IdString constrName);
|
||||
|
||||
// Intended to simplify Python API
|
||||
void addClock(IdString net, float freq);
|
||||
};
|
||||
|
||||
NEXTPNR_NAMESPACE_END
|
||||
|
@ -36,8 +36,8 @@ wirelen_t get_net_metric(const Context *ctx, const NetInfo *net, MetricType type
|
||||
bool driver_gb = ctx->getBelGlobalBuf(driver_cell->bel);
|
||||
if (driver_gb)
|
||||
return 0;
|
||||
IdString clock_port;
|
||||
bool timing_driven = ctx->timing_driven && type == MetricType::COST && ctx->getPortTimingClass(driver_cell, net->driver.port, clock_port) != TMG_IGNORE;
|
||||
int clock_count;
|
||||
bool timing_driven = ctx->timing_driven && type == MetricType::COST && ctx->getPortTimingClass(driver_cell, net->driver.port, clock_count) != TMG_IGNORE;
|
||||
delay_t negative_slack = 0;
|
||||
delay_t worst_slack = std::numeric_limits<delay_t>::max();
|
||||
Loc driver_loc = ctx->getBelLocation(driver_cell->bel);
|
||||
|
@ -808,7 +808,7 @@ bool router1(Context *ctx, const Router1Cfg &cfg)
|
||||
#endif
|
||||
|
||||
log_info("Checksum: 0x%08x\n", ctx->checksum());
|
||||
timing_analysis(ctx, true /* slack_histogram */, true /* print_path */);
|
||||
timing_analysis(ctx, true /* slack_histogram */, true /* print_fmax */, true /* print_path */);
|
||||
|
||||
ctx->unlock();
|
||||
return true;
|
||||
|
398
common/timing.cc
398
common/timing.cc
@ -22,6 +22,7 @@
|
||||
#include <algorithm>
|
||||
#include <boost/range/adaptor/reversed.hpp>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include "log.h"
|
||||
@ -29,17 +30,72 @@
|
||||
|
||||
NEXTPNR_NAMESPACE_BEGIN
|
||||
|
||||
namespace {
|
||||
struct ClockEvent
|
||||
{
|
||||
IdString clock;
|
||||
ClockEdge edge;
|
||||
|
||||
bool operator==(const ClockEvent &other) const { return clock == other.clock && edge == other.edge; }
|
||||
};
|
||||
|
||||
struct ClockPair
|
||||
{
|
||||
ClockEvent start, end;
|
||||
|
||||
bool operator==(const ClockPair &other) const { return start == other.start && end == other.end; }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
NEXTPNR_NAMESPACE_END
|
||||
namespace std {
|
||||
|
||||
template <> struct hash<NEXTPNR_NAMESPACE_PREFIX ClockEvent>
|
||||
{
|
||||
std::size_t operator()(const NEXTPNR_NAMESPACE_PREFIX ClockEvent &obj) const noexcept
|
||||
{
|
||||
std::size_t seed = 0;
|
||||
boost::hash_combine(seed, hash<NEXTPNR_NAMESPACE_PREFIX IdString>()(obj.clock));
|
||||
boost::hash_combine(seed, hash<int>()(int(obj.edge)));
|
||||
return seed;
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct hash<NEXTPNR_NAMESPACE_PREFIX ClockPair>
|
||||
{
|
||||
std::size_t operator()(const NEXTPNR_NAMESPACE_PREFIX ClockPair &obj) const noexcept
|
||||
{
|
||||
std::size_t seed = 0;
|
||||
boost::hash_combine(seed, hash<NEXTPNR_NAMESPACE_PREFIX ClockEvent>()(obj.start));
|
||||
boost::hash_combine(seed, hash<NEXTPNR_NAMESPACE_PREFIX ClockEvent>()(obj.start));
|
||||
return seed;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace std
|
||||
NEXTPNR_NAMESPACE_BEGIN
|
||||
|
||||
typedef std::vector<const PortRef *> PortRefVector;
|
||||
typedef std::map<int, unsigned> DelayFrequency;
|
||||
|
||||
struct CriticalPath
|
||||
{
|
||||
PortRefVector ports;
|
||||
delay_t path_delay;
|
||||
delay_t path_period;
|
||||
};
|
||||
|
||||
typedef std::unordered_map<ClockPair, CriticalPath> CriticalPathMap;
|
||||
|
||||
struct Timing
|
||||
{
|
||||
Context *ctx;
|
||||
bool net_delays;
|
||||
bool update;
|
||||
delay_t min_slack;
|
||||
PortRefVector *crit_path;
|
||||
CriticalPathMap *crit_path;
|
||||
DelayFrequency *slack_histogram;
|
||||
IdString async_clock;
|
||||
|
||||
struct TimingData
|
||||
{
|
||||
@ -49,23 +105,24 @@ struct Timing
|
||||
unsigned max_path_length = 0;
|
||||
delay_t min_remaining_budget;
|
||||
bool false_startpoint = false;
|
||||
std::unordered_map<ClockEvent, delay_t> arrival_time;
|
||||
};
|
||||
|
||||
Timing(Context *ctx, bool net_delays, bool update, PortRefVector *crit_path = nullptr,
|
||||
Timing(Context *ctx, bool net_delays, bool update, CriticalPathMap *crit_path = nullptr,
|
||||
DelayFrequency *slack_histogram = nullptr)
|
||||
: ctx(ctx), net_delays(net_delays), update(update), min_slack(1.0e12 / ctx->target_freq),
|
||||
crit_path(crit_path), slack_histogram(slack_histogram)
|
||||
crit_path(crit_path), slack_histogram(slack_histogram), async_clock(ctx->id("$async$"))
|
||||
{
|
||||
}
|
||||
|
||||
delay_t walk_paths()
|
||||
{
|
||||
const auto clk_period = delay_t(1.0e12 / ctx->target_freq);
|
||||
const auto clk_period = ctx->getDelayFromNS(1.0e9 / ctx->target_freq).maxDelay();
|
||||
|
||||
// First, compute the topographical order of nets to walk through the circuit, assuming it is a _acyclic_ graph
|
||||
// TODO(eddieh): Handle the case where it is cyclic, e.g. combinatorial loops
|
||||
std::vector<NetInfo *> topographical_order;
|
||||
std::unordered_map<const NetInfo *, TimingData> net_data;
|
||||
std::unordered_map<const NetInfo *, std::unordered_map<ClockEvent, TimingData>> net_data;
|
||||
// In lieu of deleting edges from the graph, simply count the number of fanins to each output port
|
||||
std::unordered_map<const PortInfo *, unsigned> port_fanin;
|
||||
|
||||
@ -84,22 +141,34 @@ struct Timing
|
||||
}
|
||||
|
||||
for (auto o : output_ports) {
|
||||
IdString clockPort;
|
||||
TimingPortClass portClass = ctx->getPortTimingClass(cell.second.get(), o->name, clockPort);
|
||||
int clocks = 0;
|
||||
TimingPortClass portClass = ctx->getPortTimingClass(cell.second.get(), o->name, clocks);
|
||||
// If output port is influenced by a clock (e.g. FF output) then add it to the ordering as a timing
|
||||
// start-point
|
||||
if (portClass == TMG_REGISTER_OUTPUT) {
|
||||
DelayInfo clkToQ;
|
||||
ctx->getCellDelay(cell.second.get(), clockPort, o->name, clkToQ);
|
||||
topographical_order.emplace_back(o->net);
|
||||
net_data.emplace(o->net, TimingData{clkToQ.maxDelay()});
|
||||
for (int i = 0; i < clocks; i++) {
|
||||
TimingClockingInfo clkInfo = ctx->getPortClockingInfo(cell.second.get(), o->name, i);
|
||||
const NetInfo *clknet = get_net_or_empty(cell.second.get(), clkInfo.clock_port);
|
||||
IdString clksig = clknet ? clknet->name : async_clock;
|
||||
net_data[o->net][ClockEvent{clksig, clknet ? clkInfo.edge : RISING_EDGE}] =
|
||||
TimingData{clkInfo.clockToQ.maxDelay()};
|
||||
}
|
||||
|
||||
} else {
|
||||
if (portClass == TMG_STARTPOINT || portClass == TMG_GEN_CLOCK || portClass == TMG_IGNORE) {
|
||||
topographical_order.emplace_back(o->net);
|
||||
TimingData td;
|
||||
td.false_startpoint = (portClass == TMG_GEN_CLOCK || portClass == TMG_IGNORE);
|
||||
net_data.emplace(o->net, std::move(td));
|
||||
td.max_arrival = 0;
|
||||
net_data[o->net][ClockEvent{async_clock, RISING_EDGE}] = td;
|
||||
}
|
||||
|
||||
// Don't analyse paths from a clock input to other pins - they will be considered by the
|
||||
// special-case handling register input/output class ports
|
||||
if (portClass == TMG_CLOCK_INPUT)
|
||||
continue;
|
||||
|
||||
// Otherwise, for all driven input ports on this cell, if a timing arc exists between the input and
|
||||
// the current output port, increment fanin counter
|
||||
for (auto i : input_ports) {
|
||||
@ -120,14 +189,15 @@ struct Timing
|
||||
queue.pop_front();
|
||||
|
||||
for (auto &usr : net->users) {
|
||||
IdString clockPort;
|
||||
TimingPortClass usrClass = ctx->getPortTimingClass(usr.cell, usr.port, clockPort);
|
||||
int user_clocks;
|
||||
TimingPortClass usrClass = ctx->getPortTimingClass(usr.cell, usr.port, user_clocks);
|
||||
if (usrClass == TMG_IGNORE || usrClass == TMG_CLOCK_INPUT)
|
||||
continue;
|
||||
for (auto &port : usr.cell->ports) {
|
||||
if (port.second.type != PORT_OUT || !port.second.net)
|
||||
continue;
|
||||
TimingPortClass portClass = ctx->getPortTimingClass(usr.cell, port.first, clockPort);
|
||||
int port_clocks;
|
||||
TimingPortClass portClass = ctx->getPortTimingClass(usr.cell, port.first, port_clocks);
|
||||
|
||||
// Skip if this is a clocked output (but allow non-clocked ones)
|
||||
if (portClass == TMG_REGISTER_OUTPUT || portClass == TMG_STARTPOINT || portClass == TMG_IGNORE ||
|
||||
@ -174,18 +244,28 @@ struct Timing
|
||||
|
||||
// Go forwards topographically to find the maximum arrival time and max path length for each net
|
||||
for (auto net : topographical_order) {
|
||||
auto &nd = net_data.at(net);
|
||||
if (!net_data.count(net))
|
||||
continue;
|
||||
auto &nd_map = net_data.at(net);
|
||||
for (auto &startdomain : nd_map) {
|
||||
ClockEvent start_clk = startdomain.first;
|
||||
auto &nd = startdomain.second;
|
||||
if (nd.false_startpoint)
|
||||
continue;
|
||||
const auto net_arrival = nd.max_arrival;
|
||||
const auto net_length_plus_one = nd.max_path_length + 1;
|
||||
nd.min_remaining_budget = clk_period;
|
||||
for (auto &usr : net->users) {
|
||||
IdString clockPort;
|
||||
TimingPortClass portClass = ctx->getPortTimingClass(usr.cell, usr.port, clockPort);
|
||||
if (portClass == TMG_REGISTER_INPUT || portClass == TMG_ENDPOINT || portClass == TMG_IGNORE) {
|
||||
} else {
|
||||
int port_clocks;
|
||||
TimingPortClass portClass = ctx->getPortTimingClass(usr.cell, usr.port, port_clocks);
|
||||
auto net_delay = net_delays ? ctx->getNetinfoRouteDelay(net, usr) : delay_t();
|
||||
auto budget_override = ctx->getBudgetOverride(net, usr, net_delay);
|
||||
auto usr_arrival = net_arrival + net_delay;
|
||||
|
||||
if (portClass == TMG_REGISTER_INPUT || portClass == TMG_ENDPOINT || portClass == TMG_IGNORE ||
|
||||
portClass == TMG_CLOCK_INPUT) {
|
||||
// Skip
|
||||
} else {
|
||||
auto budget_override = ctx->getBudgetOverride(net, usr, net_delay);
|
||||
// Iterate over all output ports on the same cell as the sink
|
||||
for (auto port : usr.cell->ports) {
|
||||
if (port.second.type != PORT_OUT || !port.second.net)
|
||||
@ -195,7 +275,7 @@ struct Timing
|
||||
bool is_path = ctx->getCellDelay(usr.cell, usr.port, port.first, comb_delay);
|
||||
if (!is_path)
|
||||
continue;
|
||||
auto &data = net_data[port.second.net];
|
||||
auto &data = net_data[port.second.net][start_clk];
|
||||
auto &arrival = data.max_arrival;
|
||||
arrival = std::max(arrival, usr_arrival + comb_delay.maxDelay());
|
||||
if (!budget_override) { // Do not increment path length if budget overriden since it doesn't
|
||||
@ -207,44 +287,96 @@ struct Timing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NetInfo *crit_net = nullptr;
|
||||
std::unordered_map<ClockPair, std::pair<delay_t, NetInfo *>> crit_nets;
|
||||
|
||||
// Now go backwards topographically to determine the minimum path slack, and to distribute all path slack evenly
|
||||
// between all nets on the path
|
||||
for (auto net : boost::adaptors::reverse(topographical_order)) {
|
||||
auto &nd = net_data.at(net);
|
||||
if (!net_data.count(net))
|
||||
continue;
|
||||
auto &nd_map = net_data.at(net);
|
||||
for (auto &startdomain : nd_map) {
|
||||
auto &nd = startdomain.second;
|
||||
// Ignore false startpoints
|
||||
if (nd.false_startpoint) continue;
|
||||
if (nd.false_startpoint)
|
||||
continue;
|
||||
const delay_t net_length_plus_one = nd.max_path_length + 1;
|
||||
auto &net_min_remaining_budget = nd.min_remaining_budget;
|
||||
for (auto &usr : net->users) {
|
||||
auto net_delay = net_delays ? ctx->getNetinfoRouteDelay(net, usr) : delay_t();
|
||||
auto budget_override = ctx->getBudgetOverride(net, usr, net_delay);
|
||||
IdString associatedClock;
|
||||
TimingPortClass portClass = ctx->getPortTimingClass(usr.cell, usr.port, associatedClock);
|
||||
int port_clocks;
|
||||
TimingPortClass portClass = ctx->getPortTimingClass(usr.cell, usr.port, port_clocks);
|
||||
if (portClass == TMG_REGISTER_INPUT || portClass == TMG_ENDPOINT) {
|
||||
auto process_endpoint = [&](IdString clksig, ClockEdge edge, delay_t setup) {
|
||||
const auto net_arrival = nd.max_arrival;
|
||||
auto path_budget = clk_period - (net_arrival + net_delay);
|
||||
const auto endpoint_arrival = net_arrival + net_delay + setup;
|
||||
delay_t period;
|
||||
// Set default period
|
||||
if (edge == startdomain.first.edge) {
|
||||
period = clk_period;
|
||||
} else {
|
||||
period = clk_period / 2;
|
||||
}
|
||||
if (clksig != async_clock) {
|
||||
if (ctx->nets.at(clksig)->clkconstr) {
|
||||
if (edge == startdomain.first.edge) {
|
||||
// same edge
|
||||
period = ctx->nets.at(clksig)->clkconstr->period.minDelay();
|
||||
} else if (edge == RISING_EDGE) {
|
||||
// falling -> rising
|
||||
period = ctx->nets.at(clksig)->clkconstr->low.minDelay();
|
||||
} else if (edge == FALLING_EDGE) {
|
||||
// rising -> falling
|
||||
period = ctx->nets.at(clksig)->clkconstr->high.minDelay();
|
||||
}
|
||||
}
|
||||
}
|
||||
auto path_budget = period - endpoint_arrival;
|
||||
|
||||
if (update) {
|
||||
auto budget_share = budget_override ? 0 : path_budget / net_length_plus_one;
|
||||
usr.budget = std::min(usr.budget, net_delay + budget_share);
|
||||
net_min_remaining_budget = std::min(net_min_remaining_budget, path_budget - budget_share);
|
||||
net_min_remaining_budget =
|
||||
std::min(net_min_remaining_budget, path_budget - budget_share);
|
||||
}
|
||||
|
||||
if (path_budget < min_slack) {
|
||||
if (path_budget < min_slack)
|
||||
min_slack = path_budget;
|
||||
if (crit_path) {
|
||||
crit_path->clear();
|
||||
crit_path->push_back(&usr);
|
||||
crit_net = net;
|
||||
}
|
||||
}
|
||||
|
||||
if (slack_histogram) {
|
||||
int slack_ps = ctx->getDelayNS(path_budget) * 1000;
|
||||
(*slack_histogram)[slack_ps]++;
|
||||
}
|
||||
ClockEvent dest_ev{clksig, edge};
|
||||
ClockPair clockPair{startdomain.first, dest_ev};
|
||||
nd.arrival_time[dest_ev] = std::max(nd.arrival_time[dest_ev], endpoint_arrival);
|
||||
|
||||
if (crit_path) {
|
||||
if (!crit_nets.count(clockPair) || crit_nets.at(clockPair).first < endpoint_arrival) {
|
||||
crit_nets[clockPair] = std::make_pair(endpoint_arrival, net);
|
||||
(*crit_path)[clockPair].path_delay = endpoint_arrival;
|
||||
(*crit_path)[clockPair].path_period = period;
|
||||
(*crit_path)[clockPair].ports.clear();
|
||||
(*crit_path)[clockPair].ports.push_back(&usr);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (portClass == TMG_REGISTER_INPUT) {
|
||||
for (int i = 0; i < port_clocks; i++) {
|
||||
TimingClockingInfo clkInfo = ctx->getPortClockingInfo(usr.cell, usr.port, i);
|
||||
const NetInfo *clknet = get_net_or_empty(usr.cell, clkInfo.clock_port);
|
||||
IdString clksig = clknet ? clknet->name : async_clock;
|
||||
process_endpoint(clksig, clknet ? clkInfo.edge : RISING_EDGE, clkInfo.setup.maxDelay());
|
||||
}
|
||||
} else {
|
||||
process_endpoint(async_clock, RISING_EDGE, 0);
|
||||
}
|
||||
|
||||
} else if (update) {
|
||||
|
||||
// Iterate over all output ports on the same cell as the sink
|
||||
for (const auto &port : usr.cell->ports) {
|
||||
if (port.second.type != PORT_OUT || !port.second.net)
|
||||
@ -253,10 +385,16 @@ struct Timing
|
||||
bool is_path = ctx->getCellDelay(usr.cell, usr.port, port.first, comb_delay);
|
||||
if (!is_path)
|
||||
continue;
|
||||
auto path_budget = net_data.at(port.second.net).min_remaining_budget;
|
||||
if (net_data.count(port.second.net) &&
|
||||
net_data.at(port.second.net).count(startdomain.first)) {
|
||||
auto path_budget =
|
||||
net_data.at(port.second.net).at(startdomain.first).min_remaining_budget;
|
||||
auto budget_share = budget_override ? 0 : path_budget / net_length_plus_one;
|
||||
usr.budget = std::min(usr.budget, net_delay + budget_share);
|
||||
net_min_remaining_budget = std::min(net_min_remaining_budget, path_budget - budget_share);
|
||||
net_min_remaining_budget =
|
||||
std::min(net_min_remaining_budget, path_budget - budget_share);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -264,6 +402,9 @@ struct Timing
|
||||
|
||||
if (crit_path) {
|
||||
// Walk backwards from the most critical net
|
||||
for (auto crit_pair : crit_nets) {
|
||||
NetInfo *crit_net = crit_pair.second.second;
|
||||
auto &cp_ports = (*crit_path)[crit_pair.first].ports;
|
||||
while (crit_net) {
|
||||
const PortInfo *crit_ipin = nullptr;
|
||||
delay_t max_arrival = std::numeric_limits<delay_t>::min();
|
||||
@ -278,19 +419,23 @@ struct Timing
|
||||
if (!is_path)
|
||||
continue;
|
||||
// If input port is influenced by a clock, skip
|
||||
IdString portClock;
|
||||
TimingPortClass portClass = ctx->getPortTimingClass(crit_net->driver.cell, port.first, portClock);
|
||||
if (portClass == TMG_REGISTER_INPUT || portClass == TMG_CLOCK_INPUT || portClass == TMG_ENDPOINT ||
|
||||
portClass == TMG_IGNORE)
|
||||
int port_clocks;
|
||||
TimingPortClass portClass =
|
||||
ctx->getPortTimingClass(crit_net->driver.cell, port.first, port_clocks);
|
||||
if (portClass == TMG_REGISTER_INPUT || portClass == TMG_CLOCK_INPUT ||
|
||||
portClass == TMG_ENDPOINT || portClass == TMG_IGNORE)
|
||||
continue;
|
||||
|
||||
// And find the fanin net with the latest arrival time
|
||||
const auto net_arrival = net_data.at(port.second.net).max_arrival;
|
||||
if (net_data.count(port.second.net) &&
|
||||
net_data.at(port.second.net).count(crit_pair.first.start)) {
|
||||
const auto net_arrival = net_data.at(port.second.net).at(crit_pair.first.start).max_arrival;
|
||||
if (net_arrival > max_arrival) {
|
||||
max_arrival = net_arrival;
|
||||
crit_ipin = &port.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!crit_ipin)
|
||||
break;
|
||||
@ -298,13 +443,14 @@ struct Timing
|
||||
// Now convert PortInfo* into a PortRef*
|
||||
for (auto &usr : crit_ipin->net->users) {
|
||||
if (usr.cell->name == crit_net->driver.cell->name && usr.port == crit_ipin->name) {
|
||||
crit_path->push_back(&usr);
|
||||
cp_ports.push_back(&usr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
crit_net = crit_ipin->net;
|
||||
}
|
||||
std::reverse(crit_path->begin(), crit_path->end());
|
||||
std::reverse(cp_ports.begin(), cp_ports.end());
|
||||
}
|
||||
}
|
||||
return min_slack;
|
||||
}
|
||||
@ -365,30 +511,106 @@ void assign_budget(Context *ctx, bool quiet)
|
||||
log_info("Checksum: 0x%08x\n", ctx->checksum());
|
||||
}
|
||||
|
||||
void timing_analysis(Context *ctx, bool print_histogram, bool print_path)
|
||||
void timing_analysis(Context *ctx, bool print_histogram, bool print_fmax, bool print_path)
|
||||
{
|
||||
PortRefVector crit_path;
|
||||
auto format_event = [ctx](const ClockEvent &e, int field_width = 0) {
|
||||
std::string value;
|
||||
if (e.clock == ctx->id("$async$"))
|
||||
value = std::string("<async>");
|
||||
else
|
||||
value = (e.edge == FALLING_EDGE ? std::string("negedge ") : std::string("posedge ")) + e.clock.str(ctx);
|
||||
if (int(value.length()) < field_width)
|
||||
value.insert(value.length(), field_width - int(value.length()), ' ');
|
||||
return value;
|
||||
};
|
||||
|
||||
CriticalPathMap crit_paths;
|
||||
DelayFrequency slack_histogram;
|
||||
|
||||
Timing timing(ctx, true /* net_delays */, false /* update */, print_path ? &crit_path : nullptr,
|
||||
Timing timing(ctx, true /* net_delays */, false /* update */, (print_path || print_fmax) ? &crit_paths : nullptr,
|
||||
print_histogram ? &slack_histogram : nullptr);
|
||||
auto min_slack = timing.walk_paths();
|
||||
timing.walk_paths();
|
||||
std::map<IdString, std::pair<ClockPair, CriticalPath>> clock_reports;
|
||||
std::map<IdString, double> clock_fmax;
|
||||
std::vector<ClockPair> xclock_paths;
|
||||
std::set<IdString> empty_clocks; // set of clocks with no interior paths
|
||||
if (print_path || print_fmax) {
|
||||
for (auto path : crit_paths) {
|
||||
const ClockEvent &a = path.first.start;
|
||||
const ClockEvent &b = path.first.end;
|
||||
empty_clocks.insert(a.clock);
|
||||
empty_clocks.insert(b.clock);
|
||||
}
|
||||
for (auto path : crit_paths) {
|
||||
const ClockEvent &a = path.first.start;
|
||||
const ClockEvent &b = path.first.end;
|
||||
if (a.clock != b.clock || a.clock == ctx->id("$async$"))
|
||||
continue;
|
||||
double Fmax;
|
||||
empty_clocks.erase(a.clock);
|
||||
if (a.edge == b.edge)
|
||||
Fmax = 1000 / ctx->getDelayNS(path.second.path_delay);
|
||||
else
|
||||
Fmax = 500 / ctx->getDelayNS(path.second.path_delay);
|
||||
if (!clock_fmax.count(a.clock) || Fmax < clock_fmax.at(a.clock)) {
|
||||
clock_reports[a.clock] = path;
|
||||
clock_fmax[a.clock] = Fmax;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &path : crit_paths) {
|
||||
const ClockEvent &a = path.first.start;
|
||||
const ClockEvent &b = path.first.end;
|
||||
if (a.clock == b.clock && a.clock != ctx->id("$async$"))
|
||||
continue;
|
||||
xclock_paths.push_back(path.first);
|
||||
}
|
||||
|
||||
if (clock_reports.empty()) {
|
||||
log_warning("No clocks found in design");
|
||||
}
|
||||
|
||||
std::sort(xclock_paths.begin(), xclock_paths.end(), [ctx](const ClockPair &a, const ClockPair &b) {
|
||||
if (a.start.clock.str(ctx) < b.start.clock.str(ctx))
|
||||
return true;
|
||||
if (a.start.clock.str(ctx) > b.start.clock.str(ctx))
|
||||
return false;
|
||||
if (a.start.edge < b.start.edge)
|
||||
return true;
|
||||
if (a.start.edge > b.start.edge)
|
||||
return false;
|
||||
if (a.end.clock.str(ctx) < b.end.clock.str(ctx))
|
||||
return true;
|
||||
if (a.end.clock.str(ctx) > b.end.clock.str(ctx))
|
||||
return false;
|
||||
if (a.end.edge < b.end.edge)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (print_path) {
|
||||
if (crit_path.empty()) {
|
||||
log_info("Design contains no timing paths\n");
|
||||
} else {
|
||||
auto print_path_report = [ctx](ClockPair &clocks, PortRefVector &crit_path) {
|
||||
delay_t total = 0;
|
||||
log_break();
|
||||
log_info("Critical path report:\n");
|
||||
log_info("curr total\n");
|
||||
|
||||
auto &front = crit_path.front();
|
||||
auto &front_port = front->cell->ports.at(front->port);
|
||||
auto &front_driver = front_port.net->driver;
|
||||
|
||||
IdString last_port;
|
||||
ctx->getPortTimingClass(front_driver.cell, front_driver.port, last_port);
|
||||
int port_clocks;
|
||||
auto portClass = ctx->getPortTimingClass(front_driver.cell, front_driver.port, port_clocks);
|
||||
IdString last_port = front_driver.port;
|
||||
if (portClass == TMG_REGISTER_OUTPUT) {
|
||||
for (int i = 0; i < port_clocks; i++) {
|
||||
TimingClockingInfo clockInfo = ctx->getPortClockingInfo(front_driver.cell, front_driver.port, i);
|
||||
const NetInfo *clknet = get_net_or_empty(front_driver.cell, clockInfo.clock_port);
|
||||
if (clknet != nullptr && clknet->name == clocks.start.clock &&
|
||||
clockInfo.edge == clocks.start.edge) {
|
||||
last_port = clockInfo.clock_port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log_info("curr total\n");
|
||||
for (auto sink : crit_path) {
|
||||
auto sink_cell = sink->cell;
|
||||
auto &port = sink_cell->ports.at(sink->port);
|
||||
@ -396,7 +618,12 @@ void timing_analysis(Context *ctx, bool print_histogram, bool print_path)
|
||||
auto &driver = net->driver;
|
||||
auto driver_cell = driver.cell;
|
||||
DelayInfo comb_delay;
|
||||
if (last_port == driver.port) {
|
||||
// Case where we start with a STARTPOINT etc
|
||||
comb_delay = ctx->getDelayFromNS(0);
|
||||
} else {
|
||||
ctx->getCellDelay(sink_cell, last_port, driver.port, comb_delay);
|
||||
}
|
||||
total += comb_delay.maxDelay();
|
||||
log_info("%4.1f %4.1f Source %s.%s\n", ctx->getDelayNS(comb_delay.maxDelay()), ctx->getDelayNS(total),
|
||||
driver_cell->name.c_str(ctx), driver.port.c_str(ctx));
|
||||
@ -427,12 +654,63 @@ void timing_analysis(Context *ctx, bool print_histogram, bool print_path)
|
||||
}
|
||||
last_port = sink->port;
|
||||
}
|
||||
};
|
||||
|
||||
for (auto &clock : clock_reports) {
|
||||
log_break();
|
||||
}
|
||||
std::string start = clock.second.first.start.edge == FALLING_EDGE ? std::string("negedge") : std::string("posedge");
|
||||
std::string end = clock.second.first.end.edge == FALLING_EDGE ? std::string("negedge") : std::string("posedge");
|
||||
log_info("Critical path report for clock '%s' (%s -> %s):\n", clock.first.c_str(ctx), start.c_str(), end.c_str());
|
||||
auto &crit_path = clock.second.second.ports;
|
||||
print_path_report(clock.second.first, crit_path);
|
||||
}
|
||||
|
||||
delay_t default_slack = delay_t((1.0e9 / ctx->getDelayNS(1)) / ctx->target_freq);
|
||||
log_info("estimated Fmax = %.2f MHz\n", 1e3 / ctx->getDelayNS(default_slack - min_slack));
|
||||
for (auto &xclock : xclock_paths) {
|
||||
log_break();
|
||||
std::string start = format_event(xclock.start);
|
||||
std::string end = format_event(xclock.end);
|
||||
log_info("Critical path report for cross-domain path '%s' -> '%s':\n", start.c_str(), end.c_str());
|
||||
auto &crit_path = crit_paths.at(xclock).ports;
|
||||
print_path_report(xclock, crit_path);
|
||||
}
|
||||
}
|
||||
if (print_fmax) {
|
||||
log_break();
|
||||
unsigned max_width = 0;
|
||||
for (auto &clock : clock_reports)
|
||||
max_width = std::max<unsigned>(max_width, clock.first.str(ctx).size());
|
||||
for (auto &clock : clock_reports) {
|
||||
const auto &clock_name = clock.first.str(ctx);
|
||||
const int width = max_width - clock_name.size();
|
||||
if (ctx->nets.at(clock.first)->clkconstr) {
|
||||
float target = 1000 / ctx->getDelayNS(ctx->nets.at(clock.first)->clkconstr->period.minDelay());
|
||||
log_info("Max frequency for clock %*s'%s': %.02f MHz (%s at %.02f MHz)\n", width, "", clock_name.c_str(),
|
||||
clock_fmax[clock.first], (target < clock_fmax[clock.first]) ? "PASS" : "FAIL", target);
|
||||
} else {
|
||||
log_info("Max frequency for clock %*s'%s': %.02f MHz\n", width, "", clock_name.c_str(), clock_fmax[clock.first]);
|
||||
}
|
||||
}
|
||||
for (auto &eclock : empty_clocks) {
|
||||
if (eclock != ctx->id("$async$"))
|
||||
log_info("Clock '%s' has no interior paths\n", eclock.c_str(ctx));
|
||||
}
|
||||
log_break();
|
||||
|
||||
int start_field_width = 0, end_field_width = 0;
|
||||
for (auto &xclock : xclock_paths) {
|
||||
start_field_width = std::max((int)format_event(xclock.start).length(), start_field_width);
|
||||
end_field_width = std::max((int)format_event(xclock.end).length(), end_field_width);
|
||||
}
|
||||
|
||||
for (auto &xclock : xclock_paths) {
|
||||
const ClockEvent &a = xclock.start;
|
||||
const ClockEvent &b = xclock.end;
|
||||
auto &path = crit_paths.at(xclock);
|
||||
auto ev_a = format_event(a, start_field_width), ev_b = format_event(b, end_field_width);
|
||||
log_info("Max delay %s -> %s: %0.02f ns\n", ev_a.c_str(), ev_b.c_str(), ctx->getDelayNS(path.path_delay));
|
||||
}
|
||||
log_break();
|
||||
}
|
||||
|
||||
if (print_histogram && slack_histogram.size() > 0) {
|
||||
unsigned num_bins = 20;
|
||||
|
@ -29,7 +29,7 @@ void assign_budget(Context *ctx, bool quiet = false);
|
||||
|
||||
// Perform timing analysis and print out the fmax, and optionally the
|
||||
// critical path
|
||||
void timing_analysis(Context *ctx, bool slack_histogram = true, bool print_path = false);
|
||||
void timing_analysis(Context *ctx, bool slack_histogram = true, bool print_fmax = true, bool print_path = false);
|
||||
|
||||
NEXTPNR_NAMESPACE_END
|
||||
|
||||
|
@ -404,6 +404,10 @@ actual penalty used is a multiple of this value (i.e. a weighted version of this
|
||||
|
||||
Convert an `delay_t` to an actual real-world delay in nanoseconds.
|
||||
|
||||
### DelayInfo getDelayFromNS(float v) const
|
||||
|
||||
Convert a real-world delay in nanoseconds to a DelayInfo with equal min/max rising/falling values.
|
||||
|
||||
### uint32\_t getDelayChecksum(delay\_t v) const
|
||||
|
||||
Convert a `delay_t` to an integer for checksum calculations.
|
||||
@ -461,11 +465,17 @@ Cell Delay Methods
|
||||
Returns the delay for the specified path through a cell in the `&delay` argument. The method returns
|
||||
false if there is no timing relationship from `fromPort` to `toPort`.
|
||||
|
||||
### TimingPortClass getPortTimingClass(const CellInfo *cell, IdString port, IdString &clockPort) const
|
||||
### TimingPortClass getPortTimingClass(const CellInfo *cell, IdString port, int &clockInfoCount) const
|
||||
|
||||
Return the _timing port class_ of a port. This can be a register or combinational input or output; clock input or
|
||||
output; general startpoint or endpoint; or a port ignored for timing purposes. For register ports, clockPort is set
|
||||
to the associated clock port.
|
||||
output; general startpoint or endpoint; or a port ignored for timing purposes. For register ports, clockInfoCount is set
|
||||
to the number of associated _clock edges_ that can be queried by getPortClockingInfo.
|
||||
|
||||
### TimingClockingInfo getPortClockingInfo(const CellInfo *cell, IdString port, int index) const
|
||||
|
||||
Return the _clocking info_ (including port name of clock, clock polarity and setup/hold/clock-to-out times) of a
|
||||
port. Where ports have more than one clock edge associated with them (such as DDR outputs), `index` can be used to obtain
|
||||
information for all edges. `index` must be in [0, clockInfoCount), behaviour is undefined otherwise.
|
||||
|
||||
Placer Methods
|
||||
--------------
|
||||
|
37
docs/constraints.md
Normal file
37
docs/constraints.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Constraints
|
||||
|
||||
There are three types of constraints available for end users of nextpnr.
|
||||
|
||||
## Architecture-specific IO Cconstraints
|
||||
|
||||
Architectures may provide support for their native (or any other) IO constraint format.
|
||||
The iCE40 architecture supports PCF constraints thus:
|
||||
|
||||
set_io led[0] 3
|
||||
|
||||
and the ECP5 architecture supports a subset of LPF constraints:
|
||||
|
||||
LOCATE COMP "led[0]" SITE "E16";
|
||||
IOBUF PORT "led[0]" IO_TYPE=LVCMOS25;
|
||||
|
||||
|
||||
## Absolute Placement Constraints
|
||||
|
||||
nextpnr provides generic support for placement constraints by setting the Bel attribute on the cell to the name of
|
||||
the Bel you wish it to be placed at. For example:
|
||||
|
||||
(* BEL="X2/Y5/lc0" *)
|
||||
|
||||
## Clock Constraints
|
||||
|
||||
There are two ways to apply clock constraints in nextpnr. The `--clock {freq}` command line argument is used to
|
||||
apply a default frequency (in MHz) to all clocks without a more specific constraint.
|
||||
|
||||
The Python API can apply clock constraints to specific named clocks. This is done by passing a Python file
|
||||
specifying these constraints to the `--pre-pack` command line argument. Inside the file, constraints are applied by
|
||||
calling the function `ctx.addClock` with the name of the clock and its frequency in MHz, for example:
|
||||
|
||||
ctx.addClock("csi_rx_i.dphy_clk", 96)
|
||||
ctx.addClock("video_clk", 24)
|
||||
ctx.addClock("uart_i.sys_clk_i", 12)
|
||||
|
70
ecp5/arch.cc
70
ecp5/arch.cc
@ -576,10 +576,10 @@ bool Arch::getCellDelay(const CellInfo *cell, IdString fromPort, IdString toPort
|
||||
}
|
||||
}
|
||||
|
||||
TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, IdString &clockPort) const
|
||||
TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, int &clockInfoCount) const
|
||||
{
|
||||
auto disconnected = [cell](IdString p) { return !cell->ports.count(p) || cell->ports.at(p).net == nullptr; };
|
||||
|
||||
clockInfoCount = 0;
|
||||
if (cell->type == id_TRELLIS_SLICE) {
|
||||
int sd0 = int_or_default(cell->params, id("REG0_SD"), 0), sd1 = int_or_default(cell->params, id("REG1_SD"), 0);
|
||||
if (port == id_CLK || port == id_WCK)
|
||||
@ -598,13 +598,13 @@ TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, Id
|
||||
return TMG_COMB_OUTPUT;
|
||||
if (port == id_DI0 || port == id_DI1 || port == id_CE || port == id_LSR || (sd0 == 1 && port == id_M0) ||
|
||||
(sd1 == 1 && port == id_M1)) {
|
||||
clockPort = id_CLK;
|
||||
clockInfoCount = 1;
|
||||
return TMG_REGISTER_INPUT;
|
||||
}
|
||||
if (port == id_M0 || port == id_M1)
|
||||
return TMG_COMB_INPUT;
|
||||
if (port == id_Q0 || port == id_Q1) {
|
||||
clockPort = id_CLK;
|
||||
clockInfoCount = 1;
|
||||
return TMG_REGISTER_OUTPUT;
|
||||
}
|
||||
|
||||
@ -614,7 +614,7 @@ TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, Id
|
||||
|
||||
if (port == id_WD0 || port == id_WD1 || port == id_WAD0 || port == id_WAD1 || port == id_WAD2 ||
|
||||
port == id_WAD3 || port == id_WRE) {
|
||||
clockPort = id_WCK;
|
||||
clockInfoCount = 1;
|
||||
return TMG_REGISTER_INPUT;
|
||||
}
|
||||
|
||||
@ -638,10 +638,8 @@ TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, Id
|
||||
for (auto c : boost::adaptors::reverse(port_name)) {
|
||||
if (std::isdigit(c))
|
||||
continue;
|
||||
if (c == 'A')
|
||||
clockPort = id_CLKA;
|
||||
else if (c == 'B')
|
||||
clockPort = id_CLKB;
|
||||
if (c == 'A' || c == 'B')
|
||||
clockInfoCount = 1;
|
||||
else
|
||||
NPNR_ASSERT_FALSE_STR("bad ram port");
|
||||
return (cell->ports.at(port).type == PORT_OUT) ? TMG_REGISTER_OUTPUT : TMG_REGISTER_INPUT;
|
||||
@ -658,6 +656,60 @@ TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, Id
|
||||
}
|
||||
}
|
||||
|
||||
TimingClockingInfo Arch::getPortClockingInfo(const CellInfo *cell, IdString port, int index) const
|
||||
{
|
||||
TimingClockingInfo info;
|
||||
info.setup.delay = 0;
|
||||
info.hold.delay = 0;
|
||||
info.clockToQ.delay = 0;
|
||||
if (cell->type == id_TRELLIS_SLICE) {
|
||||
int sd0 = int_or_default(cell->params, id("REG0_SD"), 0), sd1 = int_or_default(cell->params, id("REG1_SD"), 0);
|
||||
|
||||
if (port == id_WD0 || port == id_WD1 || port == id_WAD0 || port == id_WAD1 || port == id_WAD2 ||
|
||||
port == id_WAD3 || port == id_WRE) {
|
||||
info.edge = RISING_EDGE;
|
||||
info.clock_port = id_WCK;
|
||||
info.setup.delay = 100;
|
||||
info.hold.delay = 0;
|
||||
} else if (port == id_DI0 || port == id_DI1 || port == id_CE || port == id_LSR || (sd0 == 1 && port == id_M0) ||
|
||||
(sd1 == 1 && port == id_M1)) {
|
||||
info.edge = cell->sliceInfo.clkmux == id("INV") ? FALLING_EDGE : RISING_EDGE;
|
||||
info.clock_port = id_CLK;
|
||||
info.setup.delay = 100;
|
||||
info.hold.delay = 0;
|
||||
} else {
|
||||
info.edge = cell->sliceInfo.clkmux == id("INV") ? FALLING_EDGE : RISING_EDGE;
|
||||
info.clock_port = id_CLK;
|
||||
info.clockToQ.delay = 395;
|
||||
}
|
||||
} else if (cell->type == id_DP16KD) {
|
||||
std::string port_name = port.str(this);
|
||||
for (auto c : boost::adaptors::reverse(port_name)) {
|
||||
if (std::isdigit(c))
|
||||
continue;
|
||||
if (c == 'A') {
|
||||
info.clock_port = id_CLKA;
|
||||
break;
|
||||
} else if (c == 'B') {
|
||||
info.clock_port = id_CLKB;
|
||||
break;
|
||||
} else
|
||||
NPNR_ASSERT_FALSE_STR("bad ram port " + port.str(this));
|
||||
}
|
||||
info.edge = (str_or_default(cell->params, info.clock_port == id_CLKB ? id("CLKBMUX") : id("CLKAMUX"), "CLK") ==
|
||||
"INV")
|
||||
? FALLING_EDGE
|
||||
: RISING_EDGE;
|
||||
if (cell->ports.at(port).type == PORT_OUT) {
|
||||
info.clockToQ.delay = 4280;
|
||||
} else {
|
||||
info.setup.delay = 100;
|
||||
info.hold.delay = 0;
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> Arch::getTilesAtLocation(int row, int col)
|
||||
{
|
||||
std::vector<std::pair<std::string, std::string>> ret;
|
||||
|
12
ecp5/arch.h
12
ecp5/arch.h
@ -859,6 +859,12 @@ struct Arch : BaseCtx
|
||||
delay_t getDelayEpsilon() const { return 20; }
|
||||
delay_t getRipupDelayPenalty() const { return 200; }
|
||||
float getDelayNS(delay_t v) const { return v * 0.001; }
|
||||
DelayInfo getDelayFromNS(float ns) const
|
||||
{
|
||||
DelayInfo del;
|
||||
del.delay = delay_t(ns * 1000);
|
||||
return del;
|
||||
}
|
||||
uint32_t getDelayChecksum(delay_t v) const { return v; }
|
||||
bool getBudgetOverride(const NetInfo *net_info, const PortRef &sink, delay_t &budget) const;
|
||||
|
||||
@ -882,8 +888,10 @@ struct Arch : BaseCtx
|
||||
// Get the delay through a cell from one port to another, returning false
|
||||
// if no path exists
|
||||
bool getCellDelay(const CellInfo *cell, IdString fromPort, IdString toPort, DelayInfo &delay) const;
|
||||
// Get the port class, also setting clockPort if applicable
|
||||
TimingPortClass getPortTimingClass(const CellInfo *cell, IdString port, IdString &clockPort) const;
|
||||
// Get the port class, also setting clockInfoCount to the number of TimingClockingInfos associated with a port
|
||||
TimingPortClass getPortTimingClass(const CellInfo *cell, IdString port, int &clockInfoCount) const;
|
||||
// Get the TimingClockingInfo of a port
|
||||
TimingClockingInfo getPortClockingInfo(const CellInfo *cell, IdString port, int index) const;
|
||||
// Return true if a port is a net
|
||||
bool isGlobalNet(const NetInfo *net) const;
|
||||
|
||||
|
@ -130,6 +130,10 @@ void arch_wrap_python()
|
||||
"cells");
|
||||
readonly_wrapper<Context, decltype(&Context::nets), &Context::nets, wrap_context<NetMap &>>::def_wrap(ctx_cls,
|
||||
"nets");
|
||||
|
||||
fn_wrapper_2a_v<Context, decltype(&Context::addClock), &Context::addClock, conv_from_str<IdString>,
|
||||
pass_through<float>>::def_wrap(ctx_cls, "addClock");
|
||||
|
||||
WRAP_RANGE(Bel, conv_to_str<BelId>);
|
||||
WRAP_RANGE(Wire, conv_to_str<WireId>);
|
||||
WRAP_RANGE(AllPip, conv_to_str<PipId>);
|
||||
|
@ -350,6 +350,13 @@ class Ecp5GlobalRouter
|
||||
|
||||
place_dcc(dcc.get());
|
||||
|
||||
if (net->clkconstr) {
|
||||
glbnet->clkconstr = std::unique_ptr<ClockConstraint>(new ClockConstraint());
|
||||
glbnet->clkconstr->low = net->clkconstr->low;
|
||||
glbnet->clkconstr->high = net->clkconstr->high;
|
||||
glbnet->clkconstr->period = net->clkconstr->period;
|
||||
}
|
||||
|
||||
ctx->cells[dcc->name] = std::move(dcc);
|
||||
NetInfo *glbptr = glbnet.get();
|
||||
ctx->nets[glbnet->name] = std::move(glbnet);
|
||||
|
@ -463,11 +463,16 @@ bool Arch::getCellDelay(const CellInfo *cell, IdString fromPort, IdString toPort
|
||||
}
|
||||
|
||||
// Get the port class, also setting clockPort if applicable
|
||||
TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, IdString &clockPort) const
|
||||
TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, int &clockInfoCount) const
|
||||
{
|
||||
return TMG_IGNORE;
|
||||
}
|
||||
|
||||
TimingClockingInfo Arch::getPortClockingInfo(const CellInfo *cell, IdString port, int index) const
|
||||
{
|
||||
NPNR_ASSERT_FALSE("no clocking info for generic");
|
||||
}
|
||||
|
||||
bool Arch::isValidBelForCell(CellInfo *cell, BelId bel) const { return true; }
|
||||
bool Arch::isBelLocationValid(BelId bel) const { return true; }
|
||||
|
||||
|
@ -211,6 +211,14 @@ struct Arch : BaseCtx
|
||||
delay_t getDelayEpsilon() const { return 0.01; }
|
||||
delay_t getRipupDelayPenalty() const { return 1.0; }
|
||||
float getDelayNS(delay_t v) const { return v; }
|
||||
|
||||
DelayInfo getDelayFromNS(float ns) const
|
||||
{
|
||||
DelayInfo del;
|
||||
del.delay = ns;
|
||||
return del;
|
||||
}
|
||||
|
||||
uint32_t getDelayChecksum(delay_t v) const { return 0; }
|
||||
bool getBudgetOverride(const NetInfo *net_info, const PortRef &sink, delay_t &budget) const;
|
||||
|
||||
@ -225,8 +233,10 @@ struct Arch : BaseCtx
|
||||
DecalXY getGroupDecal(GroupId group) const;
|
||||
|
||||
bool getCellDelay(const CellInfo *cell, IdString fromPort, IdString toPort, DelayInfo &delay) const;
|
||||
// Get the port class, also setting clockPort if applicable
|
||||
TimingPortClass getPortTimingClass(const CellInfo *cell, IdString port, IdString &clockPort) const;
|
||||
// Get the port class, also setting clockInfoCount to the number of TimingClockingInfos associated with a port
|
||||
TimingPortClass getPortTimingClass(const CellInfo *cell, IdString port, int &clockInfoCount) const;
|
||||
// Get the TimingClockingInfo of a port
|
||||
TimingClockingInfo getPortClockingInfo(const CellInfo *cell, IdString port, int index) const;
|
||||
|
||||
bool isValidBelForCell(CellInfo *cell, BelId bel) const;
|
||||
bool isBelLocationValid(BelId bel) const;
|
||||
|
@ -856,8 +856,9 @@ bool Arch::getCellDelay(const CellInfo *cell, IdString fromPort, IdString toPort
|
||||
}
|
||||
|
||||
// Get the port class, also setting clockPort to associated clock if applicable
|
||||
TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, IdString &clockPort) const
|
||||
TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, int &clockInfoCount) const
|
||||
{
|
||||
clockInfoCount = 0;
|
||||
if (cell->type == id_ICESTORM_LC) {
|
||||
if (port == id_CLK)
|
||||
return TMG_CLOCK_INPUT;
|
||||
@ -870,18 +871,15 @@ TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, Id
|
||||
if (cell->lcInfo.inputCount == 0)
|
||||
return TMG_IGNORE;
|
||||
if (cell->lcInfo.dffEnable) {
|
||||
clockPort = id_CLK;
|
||||
clockInfoCount = 1;
|
||||
return TMG_REGISTER_OUTPUT;
|
||||
}
|
||||
else
|
||||
} else
|
||||
return TMG_COMB_OUTPUT;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (cell->lcInfo.dffEnable) {
|
||||
clockPort = id_CLK;
|
||||
clockInfoCount = 1;
|
||||
return TMG_REGISTER_INPUT;
|
||||
}
|
||||
else
|
||||
} else
|
||||
return TMG_COMB_INPUT;
|
||||
}
|
||||
} else if (cell->type == id_ICESTORM_RAM) {
|
||||
@ -889,23 +887,22 @@ TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, Id
|
||||
if (port == id_RCLK || port == id_WCLK)
|
||||
return TMG_CLOCK_INPUT;
|
||||
|
||||
if (port.str(this)[0] == 'R')
|
||||
clockPort = id_RCLK;
|
||||
else
|
||||
clockPort = id_WCLK;
|
||||
clockInfoCount = 1;
|
||||
|
||||
if (cell->ports.at(port).type == PORT_OUT)
|
||||
return TMG_REGISTER_OUTPUT;
|
||||
else
|
||||
return TMG_REGISTER_INPUT;
|
||||
} else if (cell->type == id_ICESTORM_DSP || cell->type == id_ICESTORM_SPRAM) {
|
||||
clockPort = id_CLK;
|
||||
if (port == id_CLK)
|
||||
if (port == id_CLK || port == id_CLOCK)
|
||||
return TMG_CLOCK_INPUT;
|
||||
else if (cell->ports.at(port).type == PORT_OUT)
|
||||
else {
|
||||
clockInfoCount = 1;
|
||||
if (cell->ports.at(port).type == PORT_OUT)
|
||||
return TMG_REGISTER_OUTPUT;
|
||||
else
|
||||
return TMG_REGISTER_INPUT;
|
||||
}
|
||||
} else if (cell->type == id_SB_IO) {
|
||||
if (port == id_D_IN_0 || port == id_D_IN_1)
|
||||
return TMG_STARTPOINT;
|
||||
@ -934,6 +931,51 @@ TimingPortClass Arch::getPortTimingClass(const CellInfo *cell, IdString port, Id
|
||||
log_error("no timing info for port '%s' of cell type '%s'\n", port.c_str(this), cell->type.c_str(this));
|
||||
}
|
||||
|
||||
TimingClockingInfo Arch::getPortClockingInfo(const CellInfo *cell, IdString port, int index) const
|
||||
{
|
||||
TimingClockingInfo info;
|
||||
if (cell->type == id_ICESTORM_LC) {
|
||||
info.clock_port = id_CLK;
|
||||
info.edge = cell->lcInfo.negClk ? FALLING_EDGE : RISING_EDGE;
|
||||
if (port == id_O) {
|
||||
bool has_clktoq = getCellDelay(cell, id_CLK, id_O, info.clockToQ);
|
||||
NPNR_ASSERT(has_clktoq);
|
||||
} else {
|
||||
info.setup.delay = 100;
|
||||
info.hold.delay = 0;
|
||||
}
|
||||
} else if (cell->type == id_ICESTORM_RAM) {
|
||||
if (port.str(this)[0] == 'R') {
|
||||
info.clock_port = id_RCLK;
|
||||
info.edge = bool_or_default(cell->params, id("NEG_CLK_R")) ? FALLING_EDGE : RISING_EDGE;
|
||||
} else {
|
||||
info.clock_port = id_WCLK;
|
||||
info.edge = bool_or_default(cell->params, id("NEG_CLK_W")) ? FALLING_EDGE : RISING_EDGE;
|
||||
}
|
||||
if (cell->ports.at(port).type == PORT_OUT) {
|
||||
bool has_clktoq = getCellDelay(cell, info.clock_port, port, info.clockToQ);
|
||||
NPNR_ASSERT(has_clktoq);
|
||||
} else {
|
||||
info.setup.delay = 100;
|
||||
info.hold.delay = 0;
|
||||
}
|
||||
} else if (cell->type == id_ICESTORM_DSP || cell->type == id_ICESTORM_SPRAM) {
|
||||
info.clock_port = cell->type == id_ICESTORM_SPRAM ? id_CLOCK : id_CLK;
|
||||
info.edge = RISING_EDGE;
|
||||
if (cell->ports.at(port).type == PORT_OUT) {
|
||||
bool has_clktoq = getCellDelay(cell, info.clock_port, port, info.clockToQ);
|
||||
if (!has_clktoq)
|
||||
info.clockToQ.delay = 100;
|
||||
} else {
|
||||
info.setup.delay = 100;
|
||||
info.hold.delay = 0;
|
||||
}
|
||||
} else {
|
||||
NPNR_ASSERT_FALSE("unhandled cell type in getPortClockingInfo");
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
bool Arch::isGlobalNet(const NetInfo *net) const
|
||||
{
|
||||
if (net == nullptr)
|
||||
|
12
ice40/arch.h
12
ice40/arch.h
@ -796,6 +796,12 @@ struct Arch : BaseCtx
|
||||
delay_t getDelayEpsilon() const { return 20; }
|
||||
delay_t getRipupDelayPenalty() const { return 200; }
|
||||
float getDelayNS(delay_t v) const { return v * 0.001; }
|
||||
DelayInfo getDelayFromNS(float ns) const
|
||||
{
|
||||
DelayInfo del;
|
||||
del.delay = delay_t(ns * 1000);
|
||||
return del;
|
||||
}
|
||||
uint32_t getDelayChecksum(delay_t v) const { return v; }
|
||||
bool getBudgetOverride(const NetInfo *net_info, const PortRef &sink, delay_t &budget) const;
|
||||
|
||||
@ -819,8 +825,10 @@ struct Arch : BaseCtx
|
||||
// Get the delay through a cell from one port to another, returning false
|
||||
// if no path exists
|
||||
bool getCellDelay(const CellInfo *cell, IdString fromPort, IdString toPort, DelayInfo &delay) const;
|
||||
// Get the port class, also setting clockDomain if applicable
|
||||
TimingPortClass getPortTimingClass(const CellInfo *cell, IdString port, IdString &clockDomain) const;
|
||||
// Get the port class, also setting clockInfoCount to the number of TimingClockingInfos associated with a port
|
||||
TimingPortClass getPortTimingClass(const CellInfo *cell, IdString port, int &clockInfoCount) const;
|
||||
// Get the TimingClockingInfo of a port
|
||||
TimingClockingInfo getPortClockingInfo(const CellInfo *cell, IdString port, int index) const;
|
||||
// Return true if a port is a net
|
||||
bool isGlobalNet(const NetInfo *net) const;
|
||||
|
||||
|
@ -140,6 +140,10 @@ void arch_wrap_python()
|
||||
"cells");
|
||||
readonly_wrapper<Context, decltype(&Context::nets), &Context::nets, wrap_context<NetMap &>>::def_wrap(ctx_cls,
|
||||
"nets");
|
||||
|
||||
fn_wrapper_2a_v<Context, decltype(&Context::addClock), &Context::addClock, conv_from_str<IdString>,
|
||||
pass_through<float>>::def_wrap(ctx_cls, "addClock");
|
||||
|
||||
WRAP_RANGE(Bel, conv_to_str<BelId>);
|
||||
WRAP_RANGE(Wire, conv_to_str<WireId>);
|
||||
WRAP_RANGE(AllPip, conv_to_str<PipId>);
|
||||
|
@ -462,7 +462,8 @@ static bool is_logic_port(BaseCtx *ctx, const PortRef &port)
|
||||
|
||||
static void insert_global(Context *ctx, NetInfo *net, bool is_reset, bool is_cen, bool is_logic)
|
||||
{
|
||||
log_info("promoting %s%s%s%s\n", net->name.c_str(ctx), is_reset ? " [reset]" : "", is_cen ? " [cen]" : "", is_logic ? " [logic]" : "");
|
||||
log_info("promoting %s%s%s%s\n", net->name.c_str(ctx), is_reset ? " [reset]" : "", is_cen ? " [cen]" : "",
|
||||
is_logic ? " [logic]" : "");
|
||||
|
||||
std::string glb_name = net->name.str(ctx) + std::string("_$glb_") + (is_reset ? "sr" : (is_cen ? "ce" : "clk"));
|
||||
std::unique_ptr<CellInfo> gb = create_ice_cell(ctx, ctx->id("SB_GB"), "$gbuf_" + glb_name);
|
||||
@ -489,6 +490,14 @@ static void insert_global(Context *ctx, NetInfo *net, bool is_reset, bool is_cen
|
||||
}
|
||||
}
|
||||
net->users = keep_users;
|
||||
|
||||
if (net->clkconstr) {
|
||||
glbnet->clkconstr = std::unique_ptr<ClockConstraint>(new ClockConstraint());
|
||||
glbnet->clkconstr->low = net->clkconstr->low;
|
||||
glbnet->clkconstr->high = net->clkconstr->high;
|
||||
glbnet->clkconstr->period = net->clkconstr->period;
|
||||
}
|
||||
|
||||
ctx->nets[glbnet->name] = std::move(glbnet);
|
||||
ctx->cells[gb->name] = std::move(gb);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user