# How to add a new peripheral This document describes the process of adding a new peripheral to the pinmux and auto-generator, through a worked example, adding SDRAM. # Creating the specifications The tool is split into two halves that are separated by tab-separated files. The first step is therefore to add a function that defines the peripheral as a python function. That implies in turn that the pinouts of the peripheral must be known. Looking at the BSV code for the SDRAM peripheral, we find its interface is defined as follows: interface Ifc_sdram_out; (*always_enabled,always_ready*) method Action ipad_sdr_din(Bit#(64) pad_sdr_din); method Bit#(9) sdram_sdio_ctrl(); method Bit#(64) osdr_dout(); method Bit#(8) osdr_den_n(); method Bool osdr_cke(); method Bool osdr_cs_n(); method Bool osdr_ras_n (); method Bool osdr_cas_n (); method Bool osdr_we_n (); method Bit#(8) osdr_dqm (); method Bit#(2) osdr_ba (); method Bit#(13) osdr_addr (); interface Clock sdram_clk; 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 groups: sdram1, sdram2 and sdram3. Firstly, sdram1, covering the base functionality: def sdram1(suffix, bank): buspins = [] inout = [] for i in range(8): pname = "SDRDQM%d*" % i buspins.append(pname) for i in range(8): pname = "SDRD%d*" % i buspins.append(pname) inout.append(pname) for i in range(12): buspins.append("SDRAD%d+" % i) for i in range(2): buspins.append("SDRBA%d+" % i) buspins += ['SDRCKE+', 'SDRRASn+', 'SDRCASn+', 'SDRWEn+', 'SDRCSn0++'] return (buspins, inout) This function, if used on its own, would define an 8-bit SDRAM bus with 12-bit addressing. Checking off the names against the corresponding BSV definition we find that most of them are straightforward. Outputs must have a "+" after the name (in the python representation), inputs must have a "-". However we run smack into an interesting brick-wall with the in/out pins. In/out pins which are routed through the same IO pad need a *triplet* of signals: one input wire, one output wire and *one direction control wire*. Here however we find that the SDRAM controller, which is a wrapper around the opencores SDRAM controller, has a *banked* approach to direction-control that will need to be dealt with, later. So we do *not* make the mistake of adding 8 SDRDENx pins: the BSV code will need to be modified to add 64 one-for-one enabling pins. We do not also make the mistake of adding separate unidirectional "in" and separate unidirectional "out" signals under different names, as the pinmux code is a *PAD* centric tool. The second function extends the 8-bit data bus to 64-bits, and extends the address lines to 13-bit wide: def sdram3(suffix, bank): buspins = [] inout = [] for i in range(12, 13): buspins.append("SDRAD%d+" % i) for i in range(8, 64): pname = "SDRD%d*" % i buspins.append(pname) inout.append(pname) return (buspins, inout) In this way, alternative SDRAM controller implementations can use sdram1 on its own; implementors may add "extenders" (named sdram2, sdram4) that cover extra functionality, and, interestingly, in a pinbank scenario, the number of pins on any given GPIO bank may be kept to a sane level. The next phase is to add the (now supported) peripheral to the list of pinspecs at the bottom of the file, so that it can actually be used: pinspec = (('IIS', i2s), ('MMC', emmc), ('FB', flexbus1), ('FB', flexbus2), ('SDR', sdram1), ('SDR', sdram2), ('SDR', sdram3), <--- ('EINT', eint), ('PWM', pwm), ('GPIO', gpio), ) This gives a declaration that any time the function(s) starting with "sdram" are used to add pins to a pinmux, it will be part of the "SDR" peripheral. Note that flexbus is similarly subdivided into two groups. Note however that due to a naming convention issue, interfaces must be declared with names that are lexicographically unique even in subsets of their names. i.e two interfaces, one named "SD" which is shorthand for SDMMC and another named "SDRAM" may *not* be added: the first has to be the full "SDMMC" or renamed to "MMC". # Adding the peripheral to a chip's pinmux specification Next, we add the peripheral to an actual chip's specification. In this case it is to be added to i\_class, so we open src/spec/i\_class.py. The first thing to do is to add a single-mux (dedicated) bank of 92 pins (!) which covers all of the 64-bit Data lines, 13 addresses and supporting bank-selects and control lines. It is added as Bank "D", the next contiguous bank: def pinspec(): pinbanks = { 'A': (28, 4), 'B': (18, 4), 'C': (24, 1), 'D': (92, 1), <--- } fixedpins = { 'CTRL_SYS': [ This declares the width of the pinmux to one (a dedicated peripheral bank). Note in passing that A and B are both 4-entry. Next, an SDRAM interface is conveniently added to the chip's pinmux with two simple lines of code: ps.gpio("", ('B', 0), 0, 0, 18) ps.flexbus1("", ('B', 0), 1, spec=flexspec) ps.flexbus2("", ('C', 0), 0) ps.sdram1("", ('D', 0), 0) <-- ps.sdram3("", ('D', 35), 0) <-- Note that the first argument is blank, indicating that this is the only SDRAM interface to be added. If more than one SDRAM interface is desired they would be numbered from 0 and identified by their suffix. The second argument is a tuple of (Bank Name, Bank Row Number), and the third argument is the pinmux column (which in this case must be zero). At the top level the following command is then run: $ python src/pinmux_generator.py -o i_class -s i_class The output may be found in the ./i\_class subdirectory, and it is worth examining the i\_class.mdwn file. A table named "Bank D" will have been created and it is worth just showing the first few entries here: | Pin | Mux0 | Mux1 | Mux2 | Mux3 | | --- | ----------- | ----------- | ----------- | ----------- | | 70 | D SDR_SDRDQM0 | | 71 | D SDR_SDRDQM1 | | 72 | D SDR_SDRDQM2 | | 73 | D SDR_SDRDQM3 | | 74 | D SDR_SDRDQM4 | | 75 | D SDR_SDRDQM5 | | 76 | D SDR_SDRDQM6 | | 77 | D SDR_SDRDQM7 | | 78 | D SDR_SDRD0 | | 79 | D SDR_SDRD1 | | 80 | D SDR_SDRD2 | | 81 | D SDR_SDRD3 | | 82 | D SDR_SDRD4 | | 83 | D SDR_SDRD5 | | 84 | D SDR_SDRD6 | | 85 | D SDR_SDRD7 | | 86 | D SDR_SDRAD0 | | 87 | D SDR_SDRAD1 | Returning to the definition of sdram1 and sdram3, this table clearly corresponds to the functions in src/spec/pinfunctions.py which is exactly what we want. It is however extremely important to verify. Lastly, the peripheral is a "fast" peripheral, i.e. it must not be added to the "slow" peripherals AXI4-Lite Bus, so must be added to the list of "fast" peripherals, here: ps = PinSpec(pinbanks, fixedpins, function_names, ['lcd', 'jtag', 'fb', 'sdr']) <-- # Bank A, 0-27 ps.gpio("", ('A', 0), 0, 0, 28) This basically concludes the first stage of adding a peripheral to the pinmux / autogenerator tool. It allows peripherals to be assessed for viability prior to actually committing the engineering resources to their deployment. # Adding the code auto-generators. With the specification now created and well-defined (and now including the SDRAM interface), the next completely separate phase is to auto-generate the code that will drop an SDRAM instance onto the fabric of the SoC. This particular peripheral is classified as a "Fast Bus" peripheral. "Slow" peripherals will need to be the specific topic of an alternative document, however the principles are the same. 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 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 of the string "sdrd63". It can clearly be seen that an interface named "PeripheralSideSDR" has been auto-generated: // interface declaration between SDR and pinmux (*always_ready,always_enabled*) interface PeripheralSideSDR; interface Put#(Bit#(1)) sdrdqm0; interface Put#(Bit#(1)) sdrdqm1; interface Put#(Bit#(1)) sdrdqm2; interface Put#(Bit#(1)) sdrdqm3; interface Put#(Bit#(1)) sdrdqm4; interface Put#(Bit#(1)) sdrdqm5; interface Put#(Bit#(1)) sdrdqm6; interface Put#(Bit#(1)) sdrdqm7; interface Put#(Bit#(1)) sdrd0_out; interface Put#(Bit#(1)) sdrd0_outen; interface Get#(Bit#(1)) sdrd0_in; .... .... endinterface Note that for the data lines, that where in the sdram1 specification function the signals were named "SDRDn+, out, out-enable *and* in interfaces/methods have been created, as these will be *directly* connected to the I/O pads. Further down the file we see the *actual* connection to the I/O pad (cell). An example: // -------------------- // ----- cell 161 ----- // output muxer for cell idx 161 cell161_mux_out= wrsdr_sdrd63_out; // outen muxer for cell idx 161 cell161_mux_outen= wrsdr_sdrd63_outen; // bi-directional // priority-in-muxer for cell idx 161 rule assign_wrsdr_sdrd63_in_on_cell161; wrsdr_sdrd63_in<=cell161_mux_in; endrule Here, given that this is a "dedicated" cell (with no muxing), we have *direct* assignment of all three signals (in, out, outen). 2-way, 3-way and 4-way muxing creates the required priority-muxing for inputs and 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. 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. However, as the slow\_peripherals module takes care of the IO cells (because it contains a declared and configured instance of the pinmux package), signals from the pinmux PeripheralSideSDR instance need to be passed *through* the slow peripherals module as an external interface. This will happen automatically once a code-generator class is added. So first, we must identify the nearest similar class. FlexBus looks like a good candidate, so we take a copy of src/bsv/peripheral\_gen/flexbus.py called sdram.py. The simplest next step is to global/search/replace "flexbus" with "sdram", and for peripheral instance declaration replace "fb" with "sdr". At this phase, despite knowing that it will auto-generate the wrong code, we add it as a "supported" peripheral at the bottom of src/bsv/peripheral\_gen/base.py, in the "PFactory" (Peripheral Factory) class: from gpio import gpio from rgbttl import rgbttl from flexbus import flexbus from sdram import sdram <-- for k, v in {'uart': uart, 'rs232': rs232, 'sdr': sdram, 'twi': twi, 'quart': quart, Note that the name "SDR" matches with the prefix used in the pinspec declaration, back in src/spec/pinfunctions.py, except lower-cased. Once this is done, and the auto-generation tool re-run, examining the slow\_peripherals.bsv file again shows the following (correct) and only the following (correct) additions: method Bit#(1) quart0_intr; method Bit#(1) quart1_intr; interface GPIO_config#(28) pad_configa; interface PeripheralSideSDR sdr0; <-- interface PeripheralSideFB fb0; .... .... interface iocell_side=pinmux.iocell_side; interface sdr0 = pinmux.peripheral_side.sdr; <-- interface fb0 = pinmux.peripheral_side.fb; These automatically-generated declarations are sufficient to "pass through" the SDRAM "Peripheral Side", which as we know from examination of the code 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.