sdf: Add basic support for writing SDF files

Signed-off-by: David Shah <dave@ds0.me>
diff --git a/common/command.cc b/common/command.cc
index ad5b6c5..f7f804a 100644
--- a/common/command.cc
+++ b/common/command.cc
@@ -149,6 +149,8 @@
     general.add_options()("freq", po::value<double>(), "set target frequency for design in MHz");
     general.add_options()("timing-allow-fail", "allow timing to fail in design");
     general.add_options()("no-tmdriv", "disable timing-driven placement");
+    general.add_options()("sdf", po::value<std::string>(), "SDF delay back-annotation file to write");
+
     return general;
 }
 
@@ -336,6 +338,14 @@
             log_error("Saving design failed.\n");
     }
 
+    if (vm.count("sdf")) {
+        std::string filename = vm["sdf"].as<std::string>();
+        std::ofstream f(filename);
+        if (!f)
+            log_error("Failed to open SDF file '%s' for writing.\n", filename.c_str());
+        ctx->writeSDF(f);
+    }
+
 #ifndef NO_PYTHON
     deinit_python();
 #endif
diff --git a/common/nextpnr.h b/common/nextpnr.h
index bae828f..89e9c4f 100644
--- a/common/nextpnr.h
+++ b/common/nextpnr.h
@@ -808,6 +808,11 @@
 
     // --------------------------------------------------------------
 
+    // provided by sdf.cc
+    void writeSDF(std::ostream &out) const;
+
+    // --------------------------------------------------------------
+
     uint32_t checksum() const;
 
     void check() const;
diff --git a/common/sdf.cc b/common/sdf.cc
index 570b8ae..07773a6 100644
--- a/common/sdf.cc
+++ b/common/sdf.cc
@@ -18,6 +18,7 @@
  */
 
 #include "nextpnr.h"
+#include "util.h"
 
 NEXTPNR_NAMESPACE_BEGIN
 
@@ -93,6 +94,17 @@
         return fmt;
     }
 
+    std::string escape_name(const std::string &name)
+    {
+        std::string esc;
+        for (char c : name) {
+            if (c == '$' || c == '\\' || c == '[' || c == ']' || c == ':')
+                esc += '\\';
+            esc += c;
+        }
+        return esc;
+    }
+
     std::string timing_check_name(TimingCheck::CheckType type)
     {
         switch (type) {
@@ -119,11 +131,11 @@
         out << "(" << delay.min << ":" << delay.typ << ":" << delay.max << ")";
     }
 
-    void write_port(std::ostream &out, const CellPort &port) { out << format_name(port.cell + "/" + port.port); }
+    void write_port(std::ostream &out, const CellPort &port) { out << escape_name(port.cell + "/" + port.port); }
 
     void write_portedge(std::ostream &out, const PortAndEdge &pe)
     {
-        out << "(" << (pe.edge == RISING_EDGE ? "posedge" : "negedge") << " " << pe.port << ")";
+        out << "(" << (pe.edge == RISING_EDGE ? "posedge" : "negedge") << " " << escape_name(pe.port) << ")";
     }
 
     void write(std::ostream &out)
@@ -136,17 +148,35 @@
         out << "  (PROGRAM " << format_name(program) << ")" << std::endl;
         out << "  (DIVIDER /)" << std::endl;
         out << "  (TIMESCALE 1ps)" << std::endl;
+        // Write interconnect delays, with the main design begin a "cell"
+        out << "  (CELL" << std::endl;
+        out << "    (CELLTYPE " << format_name(design) << ")" << std::endl;
+        out << "    (INSTANCE )" << std::endl;
+        out << "    (DELAY" << std::endl;
+        out << "      (ABSOLUTE" << std::endl;
+        for (auto &ic : conn) {
+            out << "        (INTERCONNECT ";
+            write_port(out, ic.from);
+            out << " ";
+            write_port(out, ic.to);
+            out << " ";
+            write_delay(out, ic.delay);
+            out << ")" << std::endl;
+        }
+        out << "      )" << std::endl;
+        out << "    )" << std::endl;
+        out << "  )" << std::endl;
         // Write cells
         for (auto &cell : cells) {
             out << "  (CELL" << std::endl;
             out << "    (CELLTYPE " << format_name(cell.celltype) << ")" << std::endl;
-            out << "    (INSTANCE " << format_name(cell.instance) << ")" << std::endl;
+            out << "    (INSTANCE " << escape_name(cell.instance) << ")" << std::endl;
             // IOPATHs (combinational delay and clock-to-q)
             if (!cell.iopaths.empty()) {
                 out << "    (DELAY" << std::endl;
                 out << "      (ABSOLUTE" << std::endl;
                 for (auto &path : cell.iopaths) {
-                    out << "        (IOPATH " << path.from << " " << path.to << " ";
+                    out << "        (IOPATH " << escape_name(path.from) << " " << escape_name(path.to) << " ";
                     write_delay(out, path.delay);
                     out << ")" << std::endl;
                 }
@@ -174,27 +204,116 @@
             }
             out << "    )" << std::endl;
         }
-        // Write interconnect delays, with the main design begin a "cell"
-        out << "  (CELL" << std::endl;
-        out << "    (CELLTYPE " << format_name(design) << ")" << std::endl;
-        out << "    (DELAY" << std::endl;
-        out << "      (ABSOLUTE" << std::endl;
-        for (auto &ic : conn) {
-            out << "        (INTERCONNECT ";
-            write_port(out, ic.from);
-            out << " ";
-            write_port(out, ic.to);
-            out << " ";
-            write_delay(out, ic.delay);
-            out << std::endl;
-        }
-        out << "      )" << std::endl;
-        out << "    )" << std::endl;
-        out << "  )" << std::endl;
         out << ")" << std::endl;
     }
 };
 
 } // namespace SDF
 
+void Context::writeSDF(std::ostream &out) const
+{
+    using namespace SDF;
+    SDFWriter wr;
+    wr.design = str_or_default(attrs, id("module"), "top");
+    wr.sdfversion = "3.0";
+    wr.vendor = "nextpnr";
+    wr.program = "nextpnr";
+
+    const double delay_scale = 1000;
+    // Convert from DelayInfo to SDF-friendly RiseFallDelay
+    auto convert_delay = [&](const DelayInfo &dly) {
+        RiseFallDelay rf;
+        rf.rise.min = getDelayNS(dly.minRaiseDelay()) * delay_scale;
+        rf.rise.typ = getDelayNS((dly.minRaiseDelay() + dly.maxRaiseDelay()) / 2) * delay_scale; // fixme: typ delays?
+        rf.rise.max = getDelayNS(dly.maxRaiseDelay()) * delay_scale;
+        rf.fall.min = getDelayNS(dly.minFallDelay()) * delay_scale;
+        rf.fall.typ = getDelayNS((dly.minFallDelay() + dly.maxFallDelay()) / 2) * delay_scale; // fixme: typ delays?
+        rf.fall.max = getDelayNS(dly.maxFallDelay()) * delay_scale;
+        return rf;
+    };
+
+    auto convert_setuphold = [&](const DelayInfo &setup, const DelayInfo &hold) {
+        RiseFallDelay rf;
+        rf.rise.min = getDelayNS(setup.minDelay()) * delay_scale;
+        rf.rise.typ = getDelayNS((setup.minDelay() + setup.maxDelay()) / 2) * delay_scale; // fixme: typ delays?
+        rf.rise.max = getDelayNS(setup.maxDelay()) * delay_scale;
+        rf.fall.min = getDelayNS(hold.minDelay()) * delay_scale;
+        rf.fall.typ = getDelayNS((hold.minDelay() + hold.maxDelay()) / 2) * delay_scale; // fixme: typ delays?
+        rf.fall.max = getDelayNS(hold.maxDelay()) * delay_scale;
+        return rf;
+    };
+
+    for (auto cell : sorted(cells)) {
+        Cell sc;
+        const CellInfo *ci = cell.second;
+        sc.instance = ci->name.str(this);
+        sc.celltype = ci->type.str(this);
+        for (auto port : ci->ports) {
+            int clockCount = 0;
+            TimingPortClass cls = getPortTimingClass(ci, port.first, clockCount);
+            if (cls == TMG_IGNORE)
+                continue;
+            if (port.second.type != PORT_IN) {
+                // Add combinational paths to this output (or inout)
+                for (auto other : ci->ports) {
+                    if (other.second.type == PORT_OUT)
+                        continue;
+                    DelayInfo dly;
+                    if (!getCellDelay(ci, other.first, port.first, dly))
+                        continue;
+                    IOPath iop;
+                    iop.from = other.first.str(this);
+                    iop.to = port.first.str(this);
+                    iop.delay = convert_delay(dly);
+                    sc.iopaths.push_back(iop);
+                }
+                // Add clock-to-output delays, also as IOPaths
+                if (cls == TMG_REGISTER_OUTPUT)
+                    for (int i = 0; i < clockCount; i++) {
+                        auto clkInfo = getPortClockingInfo(ci, port.first, i);
+                        IOPath cqp;
+                        cqp.from = clkInfo.clock_port.str(this);
+                        cqp.to = port.first.str(this);
+                        cqp.delay = convert_delay(clkInfo.clockToQ);
+                        sc.iopaths.push_back(cqp);
+                    }
+            }
+            if (port.second.type != PORT_OUT && cls == TMG_REGISTER_INPUT) {
+                // Add setup/hold checks
+                for (int i = 0; i < clockCount; i++) {
+                    auto clkInfo = getPortClockingInfo(ci, port.first, i);
+                    TimingCheck chk;
+                    chk.from.edge = RISING_EDGE; // Add setup/hold checks equally for rising and falling edges
+                    chk.from.port = port.first.str(this);
+                    chk.to.edge = clkInfo.edge;
+                    chk.to.port = clkInfo.clock_port.str(this);
+                    chk.type = TimingCheck::SETUPHOLD;
+                    chk.delay = convert_setuphold(clkInfo.setup, clkInfo.hold);
+                    sc.checks.push_back(chk);
+                    chk.from.edge = FALLING_EDGE;
+                    sc.checks.push_back(chk);
+                }
+            }
+        }
+        wr.cells.push_back(sc);
+    }
+
+    for (auto net : sorted(nets)) {
+        NetInfo *ni = net.second;
+        if (ni->driver.cell == nullptr)
+            continue;
+        for (auto &usr : ni->users) {
+            Interconnect ic;
+            ic.from.cell = ni->driver.cell->name.str(this);
+            ic.from.port = ni->driver.port.str(this);
+            ic.to.cell = usr.cell->name.str(this);
+            ic.to.port = usr.port.str(this);
+            // FIXME: min/max routing delay - or at least constructing DelayInfo here
+            ic.delay = convert_delay(getDelayFromNS(getDelayNS(getNetinfoRouteDelay(ni, usr))));
+            wr.conn.push_back(ic);
+        }
+    }
+    wr.write(out);
+}
+
 NEXTPNR_NAMESPACE_END
\ No newline at end of file