AddingPeripherals.mdwn
[pinmux.git] / docs / AddingPeripherals.mdwn
index 964bc8ca299a14314189e9278452e69a1d3a3b2e..f2a405c0a58eabd694d323d9159349d289874b41 100644 (file)
@@ -29,6 +29,31 @@ for the SDRAM peripheral, we find its interface is defined as follows:
         interface Clock sdram_clk;
     endinterface
 
+Also note further down that the code to map, for example, the 8 actual dqm
+pins into a single 8-bit interface has also been auto-generated.  Generally
+it is a good idea to verify that correspondingly the three data in/out/outen
+interfaces have also been correctly generated.
+
+    interface sdr = interface PeripheralSideSDR
+
+          ...
+          ...
+
+          interface dqm = interface Put#(8)
+             method Action put(Bit#(8) in);
+               wrsdr_sdrdqm0 <= in[0];
+               wrsdr_sdrdqm1 <= in[1];
+               wrsdr_sdrdqm2 <= in[2];
+               wrsdr_sdrdqm3 <= in[3];
+               wrsdr_sdrdqm4 <= in[4];
+               wrsdr_sdrdqm5 <= in[5];
+               wrsdr_sdrdqm6 <= in[6];
+               wrsdr_sdrdqm7 <= in[7];
+             endmethod
+           endinterface;
+
+    endinterface;
+
 So now we go to src/spec/pinfunctions.py and add a corresponding function
 that returns a list of all of the required pin signals.  However, we note
 that it is a huge number of pins so a decision is made to split it into
@@ -75,7 +100,7 @@ the address lines to 13-bit wide:
 
     def sdram3(suffix, bank):
         buspins = []
-        inout = [] 
+        inout = []
         for i in range(12, 13):
             buspins.append("SDRAD%d+" % i)
         for i in range(8, 64):
@@ -215,7 +240,7 @@ The first requirement is that the pins from the peripheral side be connected
 through to IO cells.  This can be verified by running the pinmux code
 generator (to activate "default" behaviour), just to see what happens:
 
-    $ python src/pinmux_generator.py -o i_class 
+    $ python src/pinmux_generator.py -o i_class
 
 Files are auto-generated in ./i\_class/bsv\_src and it is recommended
 to examine the pinmux.bsv file in an editor, and search for occurrences
@@ -271,6 +296,84 @@ straight-muxing for outputs, however in this instance, a deliberate
 pragmatic decision is being taken not to put 92 pins of 133mhz+ signalling
 through muxing.
 
+## Making the peripheral a "MultiBus" peripheral
+
+The sheer number of signals coming out of PeripheralSideSDR is so unwieldy
+that something has to be done.  We therefore create a "MultiBus" interface
+such that the pinmux knows which pins are grouped together by name.
+This is done in src/bsv/interface\_decl.py.
+
+The MultiBus code is quite sophisticated, in that buses can be identified
+by pattern, and removed one by one.  The *remaining* pins are left behind
+as individual single-bit pins.  Starting from a copy of InterfaceFlexBus
+as the most similar code, a cut/paste copy is taken and the new class
+InterfaceSDRAM created:
+
+    class InterfaceSDRAM(InterfaceMultiBus, Interface):
+
+        def __init__(self, ifacename, pinspecs, ganged=None, single=False):
+            Interface.__init__(self, ifacename, pinspecs, ganged, single)
+            InterfaceMultiBus.__init__(self, self.pins)
+            self.add_bus(False, ['dqm', None, None],
+                         "Bit#({0})", "sdrdqm")
+            self.add_bus(True, ['d_out', 'd_out_en', 'd_in'],
+                         "Bit#({0})", "sdrd")
+            self.add_bus(False, ['ad', None, None],
+                         "Bit#({0})", "sdrad")
+            self.add_bus(False, ['ba', None, None],
+                         "Bit#({0})", "sdrba")
+
+        def ifacedef2(self, *args):
+            return InterfaceMultiBus.ifacedef2(self, *args)
+
+Here, annoyingly, the data bus is a mess, requiring identification of
+the three separate names for in, out and outen.  The prefix "sdrd" is however
+unique and obvious in its purpose: anything beginning with "sdrd" is
+treated as a multi-bit bus, and a template for declaring a BSV type is
+given that is automatically passed the numerical quantity of pins detected
+that start with the word "sdrd".
+
+Note that it is critical to lexicographically identify pins correctly,
+so sdrdqm is done **before** sdrd.
+
+Once the buses have been identified the peripheral can be added into
+class Interfaces:
+
+    class Interfaces(InterfacesBase, PeripheralInterfaces):
+        """ contains a list of interface definitions
+        """
+
+        def __init__(self, pth=None):
+            InterfacesBase.__init__(self, Interface, pth,
+                                    {'gpio': InterfaceGPIO,
+                                     'fb': InterfaceFlexBus,
+                                     'sdr': InterfaceSDRAM,  <--
+
+Running the tool again results in a much smaller, tidier output
+that will be a lot less work, later.  Note the automatic inclusion of
+the correct length multi-bit interfaces.  d-out/in/out-en is identified
+as 64-bit, ad is identified as 13-bit, ba as 2 and dqm as 8.
+
+      // interface declaration between SDR and pinmux
+      (*always_ready,always_enabled*)
+      interface PeripheralSideSDR;
+          interface Put#(Bit#(1)) sdrcke;
+          interface Put#(Bit#(1)) sdrrasn;
+          interface Put#(Bit#(1)) sdrcasn;
+          interface Put#(Bit#(1)) sdrwen;
+          interface Put#(Bit#(1)) sdrcsn0;
+
+          interface Put#(Bit#(8)) dqm;
+          interface Put#(Bit#(64)) d_out;
+          interface Put#(Bit#(64)) d_out_en;
+          interface Get#(Bit#(64)) d_in;
+          interface Put#(Bit#(13)) ad;
+          interface Put#(Bit#(2)) ba;
+
+      endinterface
+
+## Adding the peripheral
+
 In examining the slow\_peripherals.bsv file, there should at this stage
 be no sign of an SDRAM peripheral having been added, at all.  This is
 because it is missing from the peripheral\_gen side of the tool.
@@ -326,6 +429,8 @@ is directly connected to the relevant IO pad cells, so that the *actual*
 peripheral may be declared in the "fast" fabric and connected up to the
 relevant and required "fast" bus.
 
+## Connecting in the fabric
+
 Now we can begin the process of systematically inserting the correct
 "voodoo magic" incantations that, as far as this auto-generator tool is
 concerned, are just bits of ASCII text.  In this particular instance, an
@@ -415,3 +520,379 @@ Re-run the tool and check that the correct-looking code has been created:
             fb0 <- mkAXI4_Slave_to_FlexBus_Master_Xactor;
     Ifc_rgbttl_dummy lcd0 <-  mkrgbttl_dummy();
 
+The next thing to do: searching for the string "sdram\_out" shows that the
+original hand-generated code contains (contained) a declaration of the
+SDRAM Interface, presumably to which, when compiling to run on an FPGA,
+the SDRAM interface would be connected at the top level.  Through this
+interface, connections would be done *by hand* to the IO pads, whereas
+now they are to be connected *automatically* (on the peripheral side)
+to the IO pads in the pinmux.  However, at the time of writing this is
+not fully understood by the author, so the fastifdecl and extfastifinstance
+functions are modified to generate the correct output but the code is
+*commented out*
+
+    def extfastifinstance(self, name, count):
+        return "// TODO" + self._extifinstance(name, count, "_out", "", True,
+                                   ".if_sdram_out")
+
+    def fastifdecl(self, name, count):
+        return "// (*always_ready*) interface " + \
+                "Ifc_sdram_out sdr{0}_out;".format(count)
+
+Also the corresponding (old) manual declarations of sdram\_out
+removed from the template:
+
+    `ifdef SDRAM                                            <-- xxxx
+        (*always_ready*) interface Ifc_sdram_out sdram_out; <-- xxxx
+    `endif                                                  <-- xxxx
+    ...
+    ...
+    `ifdef SDRAM                                  <--- xxxx
+        interface sdram_out=sdram.ifc_sdram_out;  <--- xxxx
+    `endif                                        <--- xxxx
+
+Next, again searching for signs of the "hand-written" code, we encounter
+the fabric connectivity, which wires the SDRAM to the AXI4.  We note however
+that there is not just one AXI slave device but *two*: one for the SDRAM
+itself and one for *configuring* the SDRAM.  We therefore need to be
+quite careful about assigning these, as will be subsequently explained.
+First however, the two AXI4 slave interfaces of this peripheral are
+declared:
+
+    class sdram(PBase):
+
+        ...
+        ...
+        def _mk_connection(self, name=None, count=0):
+            return ["sdr{0}.axi4_slave_sdram",
+                    "sdr{0}.axi4_slave_cntrl_reg"]
+
+Note that, again, in case multiple instances are ever to be added, the
+python "format" string "{0}" is inserted so that it can be substituted
+with the numerical identifier suffix.  Also note that the order
+of declaration of these two AXI4 slave is **important**.
+
+Re-running the auto-generator tool, we note the following output has
+been created, and match it against the corresponding hand-generated (old)
+code:
+
+    `ifdef SDRAM
+            mkConnection (fabric.v_to_slaves
+                              [fromInteger(valueOf(Sdram_slave_num))],
+                              sdram.axi4_slave_sdram); // 
+            mkConnection (fabric.v_to_slaves
+                              [fromInteger(valueOf(Sdram_cfg_slave_num))],
+                              sdram.axi4_slave_cntrl_reg); // 
+    `endif
+
+    // fabric connections
+    mkConnection (fabric.v_to_slaves
+                [fromInteger(valueOf(SDR0_fastslave_num))],
+                sdr0.axi4_slave_sdram);
+    mkConnection (fabric.v_to_slaves
+                [fromInteger(valueOf(SDR0_fastslave_num))],
+                sdr0.axi4_slave_cntrl_reg);
+
+Immediately we can spot an issue: whilst the correctly-named slave(s) have
+been added, they have been added with the *same* fabric slave index.  This
+is unsatisfactory and needs resolving.
+
+Here we need to explain a bit more about what is going on.  The fabric
+on an AXI4 Bus is allocated numerical slave numbers, and each slave is
+also allocated a memory-mapped region that must be resolved in a bi-directional
+fashion.  i.e whenever a particular memory region is accessed, the AXI
+slave peripheral responsible for dealing with it **must** be correctly
+identified.  So this requires some further crucial information, which is
+the size of the region that is to be allocated to each slave device.  Later
+this will be extended to being part of the specification, but for now
+it is auto-allocated based on the size.  As a huge hack, it is allocated
+in 32-bit chunks, as follows:
+
+class sdram(PBase):
+
+    def num_axi_regs32(self):
+        return [0x400000,  # defines an entire memory range (hack...)
+                12]        # defines the number of configuration regs
+
+So after running the autogenerator again, to confirm that this has
+generated the correct code, we examine several files, starting with
+fast\+memory\_map.bsv:
+
+    /*====== Fast peripherals Memory Map ======= */
+    `define SDR0_0_Base 'h50000000
+    `define SDR0_0_End  'h5FFFFFFF // 4194304 32-bit regs
+    `define SDR0_1_Base 'h60000000
+    `define SDR0_1_End  'h600002FF // 12 32-bit regs
+
+This looks slightly awkward (and in need of an external specification
+section for addresses) but is fine: the range is 1GB for the main
+map and covers 12 32-bit registers for the SDR Config map.
+Next we note the slave numbering:
+
+    typedef 0 SDR0_0__fastslave_num;
+    typedef 1 SDR0_1__fastslave_num;
+    typedef 2 FB0_fastslave_num;
+    typedef 3 LCD0_fastslave_num;
+    typedef 3 LastGen_fastslave_num;
+    typedef  TAdd#(LastGen_fastslave_num,1)      Sdram_slave_num;
+    typedef  TAdd#(Sdram_slave_num   ,`ifdef SDRAM      1 `else 0 `endif )
+                          Sdram_cfg_slave_num;
+
+Again this looks reasonable and we may subsequently (carefully! noting
+the use of the TAdd# chain!) remove the #define for Sdram\_cfg\_slave\_num.
+The next phase is to examine the fn\_addr\_to\_fastslave\_num function,
+where we note that there were *two* hand-created sections previously,
+now joined by two *auto-generated* sections:
+
+    function Tuple2 #(Bool, Bit#(TLog#(Num_Fast_Slaves)))
+                    fn_addr_to_fastslave_num  (Bit#(`PADDR) addr);
+
+      if(addr>=`SDRAMMemBase && addr<=`SDRAMMemEnd)
+          return tuple2(True,fromInteger(valueOf(Sdram_slave_num)));  <--
+      else if(addr>=`DebugBase && addr<=`DebugEnd)
+          return tuple2(True,fromInteger(valueOf(Debug_slave_num)));  <--
+      `ifdef SDRAM
+          else if(addr>=`SDRAMCfgBase && addr<=`SDRAMCfgEnd )
+              return tuple2(True,fromInteger(valueOf(Sdram_cfg_slave_num)));
+      `endif
+
+      ...
+      ...
+      if(addr>=`SDR0_0_Base && addr<=`SDR0_0_End)                         <--
+          return tuple2(True,fromInteger(valueOf(SDR0_0__fastslave_num)));
+      else
+      if(addr>=`SDR0_1_Base && addr<=`SDR0_1_End)                         <--
+          return tuple2(True,fromInteger(valueOf(SDR0_1__fastslave_num)));
+      else
+      if(addr>=`FB0Base && addr<=`FB0End)
+
+Now, here is where, in a slightly unusual unique set of circumstances, we
+cannot just remove all instances of this address / typedef from the template
+code.  Looking in the shakti-core repository's src/lib/MemoryMap.bsv file,
+the SDRAMMemBase macro is utilise in the is\_IO\_Addr function.  So as a
+really bad hack, which will need to be properly resolved, whilst the
+hand-generated sections from fast\_tuple2\_template.bsv are removed,
+and the corresponding (now redundant) defines in src/core/core\_parameters.bsv
+are commented out, some temporary typedefs to deal with the name change are
+also added:
+
+    `define SDRAMMemBase SDR0_0_Base
+    `define SDRAMMemEnd SDR0_0_End
+
+This needs to be addressed (pun intended) including being able to specify
+the name(s) of the configuration parameters, as well as specifying which
+memory map range they must be added to.
+
+Now however finally, after carefully comparing the hard-coded fabric
+connections to what was formerly named sdram, we may remove the mkConnections
+that drop sdram.axi4\_slave\_sdram and its associated cntrl reg from
+the soc\_template.bsv file.
+
+## Connecting up the pins
+
+We are still not done!  It is however worth pointing out that if this peripheral
+were not wired into the pinmux, we would in fact be finished.  However there
+is a task that (previously having been left to outside tools) now needs to
+be specified, which is to connect the sdram's pins, declared in this
+instance in Ifc\_sdram\_out, and the PeripheralSideSDR instance that
+was kindly / strategically / thoughtfully / absolutely-necessarily exported
+from slow\_peripherals for exactly this purpose.
+
+Recall earlier that we took a cut/paste copy of the flexbus.py code.  If
+we now examine socgen.bsv we find that it contains connections to pins
+that match the FlexBus specification, not SDRAM.  So, returning to the
+declaration of the Ifc\_sdram\_out interface, we first identify the
+single-bit output-only pins, and add a mapping table between them:
+
+    class sdram(PBase):
+
+        def pinname_out(self, pname):
+            return {'sdrwen': 'ifc_sdram_out.osdr_we_n',
+                    'sdrcsn0': 'ifc_sdram_out.osdr_cs_n',
+                    'sdrcke': 'ifc_sdram_out.osdr_cke',
+                    'sdrrasn': 'ifc_sdram_out.osdr_ras_n',
+                    'sdrcasn': 'ifc_sdram_out.osdr_cas_n',
+                    }.get(pname, '')
+
+Re-running the tool confirms that the relevant mkConnections are generated:
+
+    //sdr {'action': True, 'type': 'out', 'name': 'sdrcke'}
+    mkConnection(slow_peripherals.sdr0.sdrcke,
+                sdr0_sdrcke_sync.get);
+    mkConnection(sdr0_sdrcke_sync.put,
+                sdr0.ifc_sdram_out.osdr_cke);
+    //sdr {'action': True, 'type': 'out', 'name': 'sdrrasn'}
+    mkConnection(slow_peripherals.sdr0.sdrrasn,
+                sdr0_sdrrasn_sync.get);
+    mkConnection(sdr0_sdrrasn_sync.put,
+                sdr0.ifc_sdram_out.osdr_ras_n);
+
+Next, the multi-value entries are tackled (both in and out).  At present
+the code is messy, as it does not automatically detect the multiple numerical
+declarations, nor that the entries are sometimes inout (in, out, outen),
+so it is *presently* done by hand:
+
+    class sdram(PBase):
+
+        def _mk_pincon(self, name, count, typ):
+            ret = [PBase._mk_pincon(self, name, count, typ)]
+            assert typ == 'fast' # TODO slow?
+            for pname, stype, ptype in [
+                ('dqm', 'osdr_dqm', 'out'),
+                ('ba', 'osdr_ba', 'out'),
+                ('ad', 'osdr_addr', 'out'),
+                ('d_out', 'osdr_dout', 'out'),
+                ('d_in', 'ipad_sdr_din', 'in'),
+                ('d_out_en', 'osdr_den_n', 'out'),
+            ]:
+                ret.append(self._mk_vpincon(name, count, typ, ptype, pname,
+                                            "ifc_sdram_out.{0}".format(stype)))
+
+This generates *one* mkConnection for each multi-entry pintype, and here we
+match up with the "InterfaceMultiBus" class from the specification side,
+where pin entries with numerically matching names were "grouped" into single
+multi-bit declarations.
+
+## Adjusting the BSV Interface to a get/put style
+
+For various reasons, related to BSV not permitting wires to be connected
+back-to-back inside the pinmux code, a get/put style of interface had to
+be done.  This requirement has a knock-on effect up the chain into the
+actual peripheral code.  So now the actual interface (Ifc\_sdram\_out)
+has to be converted.  All straight methods (outputs) are converted to Get,
+and Action methods (inputs) converted to Put.  Also, it is just plain
+sensible not to use Bool but to use Bit#, and for the pack / unpack to
+be carried out in the interface.  After conversion, the code looks like this:
+
+    interface Ifc_sdram_out;
+        (*always_enabled, always_ready*)
+        interface Put#(Bit#(64)) ipad_sdr_din;
+        interface Get#(Bit#(64)) osdr_dout;
+        interface Get#(Bit#(64)) osdr_den_n;
+        interface Get#(Bit#(1))  osdr_cke;
+        interface Get#(Bit#(1))  osdr_cs_n;
+        interface Get#(Bit#(1))  osdr_ras_n;
+        interface Get#(Bit#(1))  osdr_cas_n;
+        interface Get#(Bit#(1))  osdr_we_n;
+        interface Get#(Bit#(8))  osdr_dqm;
+        interface Get#(Bit#(2))  osdr_ba;
+        interface Get#(Bit#(13)) osdr_addr;
+
+        method Bit#(9) sdram_sdio_ctrl;
+        interface Clock sdram_clk;
+    endinterface
+
+Note that osdr\_den\_n is now **64** bit **not** 8, as discussed above.
+After conversion, the code looks like this:
+
+    interface Ifc_sdram_out ifc_sdram_out;
+
+        interface ipad_sdr_din = interface Put
+            method Action put(Bit#(64) in)
+                sdr_cntrl.ipad_sdr_din <= in;
+            endmethod
+        endinterface;
+
+        interface osdr_dout = interface Get
+          method ActionValue#(Bit#(64)) get;
+            return sdr_cntrl.osdr_dout();
+          endmethod
+        endinterface;
+
+        interface osdr_den_n = interface Get
+            method ActionValue#(Bit#(64)) get;
+                Bit#(64) temp;
+                for (int i=0; i<8; i=i+1) begin
+                  temp[i*8] = sdr_cntrl.osdr_den_n[i];
+                end
+                return temp;
+            endmethod
+        endinterface;
+
+        interface osdr_cke = interface Get
+          method ActionValue#(Bit#(1)) get;
+            return pack(sdr_cntrl.osdr_cke());
+          endmethod
+        endinterface;
+
+        ...
+        ...
+
+    endinterface;
+
+Note that the data input is quite straightforward, as is data out,
+and cke: whether 8-bit, 13-bit or 64-bit, the conversion process is
+mundane, with only Bool having to be converted to Bit#(1) with a call
+to pack.  The data-enable however is a massive hack: whilst 64 enable
+lines come in, only every 8th bit is actually utilised and passed
+through.  Whether this should be changed is a matter for debate that
+is outside of the scope of this document.
+
+Lastly for this phase we have two anomalous interfaces, exposing
+the control registers and the clock, that have been moved out of
+Ifc\_sdram\_out, to the level above (Ifc\_sdr\_slave) so that the sole
+set of interfaces exposed for inter-connection by the auto-generator is
+related exclusively to the actual pins.  Resolution of these two issues
+is currently outside of the scope of this document.
+
+## Clock synchronisation
+
+Astute readers, if their heads have not exploded by this point, will
+have noticed earlier that the SDRAM instance was declared with an entirely
+different clock domain from slow peripherals.  Whilst the pinmux is
+completely combinatorial logic that is entirely unclocked, BSV has no
+means of informing a *module* of this fact, and consequently a weird
+null-clock splicing trick is required.
+
+This code is again auto-generated.  However, it is slightly tricky to
+describe and there are several edge-cases, including ones where the
+peripheral is a slow peripheral (UART is an example) that is driven
+from a UART clock but it is connected up inside the slow peripherals
+code; FlexBus is a Fast Bus peripheral that needs syncing up; RGB/TTL
+likewise, but JTAG is specifically declared inside the SoC and passed
+through, but its instantiation requires a separate incoming clock.
+
+Examining the similarity between the creation of an SDRAM instance
+and the JTAG instance, the jtag code therefore looks like it is the
+best candidate fit.  However, it passes through a reset signal as well.
+Instead, we modify this to create clk0 but use the slow\_reset
+signal:
+
+    class sdram(PBase):
+
+        def get_clk_spc(self, typ):
+            return "tck, slow_reset"
+
+        def get_clock_reset(self, name, count):
+            return "slow_clock,  slow_reset"
+
+The resultant synchronisation code, that accepts pairs of clock/reset
+tuples in order to take care of the prerequisite null reset and clock
+"synchronisation", looks like this:
+
+        Ifc_sync#(Bit#(1)) sdr0_sdrcke_sync <-mksyncconnection(
+                    clk0, slow_reset, slow_clock, slow_reset);
+        Ifc_sync#(Bit#(1)) sdr0_sdrrasn_sync <-mksyncconnection(
+                    clk0, slow_reset, slow_clock, slow_reset);
+        ...
+        ...
+        Ifc_sync#(Bit#(64)) sdr0_d_in_sync <-mksyncconnection(
+            slow_clock, slow_reset, clk0, slow_reset);
+
+Note that inputs are in reverse order from outputs.  With the inclusion of
+clock synchronisation, automatic chains of mkConnections are set up between
+the actual peripheral and the peripheral side of the pinmux:
+
+    mkConnection(slow_peripherals.sdr0.sdrcke,
+                sdr0_sdrcke_sync.get);
+    mkConnection(sdr0_sdrcke_sync.put,
+                sdr0.ifc_sdram_out.osdr_cke);
+
+Interestingly, if *actual* clock synchronisation were ever to be needed,
+it could easily be taken care of with relatively little extra work.  However,
+it is worth emphasising that the pinmux is *entirely* unclocked zero-reset
+combinatorial logic.
+
+# Conclusion
+
+This is not a small project, by any means.