<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://rohith-04.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://rohith-04.github.io//" rel="alternate" type="text/html" /><updated>2026-10-04T16:01:33+00:00</updated><id>https://rohith-04.github.io//feed.xml</id><title type="html">trace</title><subtitle>final-year ECE in hyderabad. pivoting from embedded systems into RTL design and computer architecture. this is where i keep notes on things i build and stuff i&apos;m trying to understand.</subtitle><author><name>rohith</name></author><entry><title type="html">That Time I Decided to Use a Dimension-Sliced Crossbar but Crossed the Idea Because Synthesis Said No</title><link href="https://rohith-04.github.io//2026/10/04/that-time-i-decided-to-use-a-dimension-sliced-crossbar-but-crossed-the-idea-because-synthesis-said-no.html" rel="alternate" type="text/html" title="That Time I Decided to Use a Dimension-Sliced Crossbar but Crossed the Idea Because Synthesis Said No" /><published>2026-10-04T00:00:00+00:00</published><updated>2026-10-04T00:00:00+00:00</updated><id>https://rohith-04.github.io//2026/10/04/that-time-i-decided-to-use-a-dimension-sliced-crossbar-but-crossed-the-idea-because-synthesis-said-no</id><content type="html" xml:base="https://rohith-04.github.io//2026/10/04/that-time-i-decided-to-use-a-dimension-sliced-crossbar-but-crossed-the-idea-because-synthesis-said-no.html"><![CDATA[<style>
table.xb {
  border-collapse: collapse;
  display: block;
  max-width: 100%;
  overflow-x: auto;
  margin: 0 0 1.5rem;
  line-height: 1.5;
}
table.xb th, table.xb td {
  padding: 0.5rem 1rem;
  border: 1px solid var(--border, #282c33);
  text-align: left;
  white-space: nowrap;
  font-family: var(--mono, monospace);
  font-size: 0.78rem;
}
table.xb th {
  background: var(--surface, #1a1d22);
  color: var(--muted, #8a929f);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
table.xb tbody tr:nth-child(even) { background: var(--bg-subtle, #141619); }
table.xb .r { text-align: right; }
</style>

<p>I know what I’m doing with that title. If you got the reference, welcome cultured people.</p>

<h2 id="the-idea">The idea</h2>

<p>I was reading textbooks for the NoC when I came across an interesting combination of words: <strong>dimension-sliced xbar</strong> (xbar is slang for crossbar).</p>

<p>The more I thought about it, the more I realised that with XY routing, some connections in the crossbar are never touched. A packet finishes its X movement before it starts moving in Y, so some input-to-output combinations can never happen.</p>

<p>A 5×5 crossbar has 25 possible connections, but only 16 of them are legal under XY routing. The other 9 are things like U-turns and reverse-dimension turns that a correctly routed packet should never request.</p>

<p>So naturally I thought: why am I building hardware for connections that can never happen?</p>

<p>That was the idea behind the dimension-sliced xbar. Instead of one full 5×5 crossbar, split the switch fabric into two smaller ones, one for each routing dimension, with a bridge between them. Roughly:</p>

<ul>
  <li><strong>X crossbar:</strong> East, West and Local-side inputs, with outputs for continuing in X, moving toward the Y side, and local ejection.</li>
  <li><strong>Y crossbar:</strong> North, South, Local-side input and the bridge input, with outputs for North, South and local ejection.</li>
  <li>The Local input can participate in both dimensions, so the final local ejection paths have to be handled carefully.</li>
</ul>

<p>The X side handles X-phase traffic, the Y side handles Y-phase traffic, and the bridge lets a packet move from one to the other.</p>

<p>It looked reasonable. The crossbar takes up a substantial amount of space in a NoC, so removing hardware I already knew would never be used felt like a pretty good optimisation.</p>

<p>Then I decided to actually measure how much I was gaining. Well, let’s just say it was not good.</p>

<h2 id="so-i-decided-to-measure-it">So I decided to measure it</h2>

<p>This was also a good chance to understand Yosys and ABC properly, instead of treating synthesis as a command that gives me a number while I pretend I know what happened.</p>

<p>I wrote the logic for both the full 5×5 xbar and the dimension-sliced version (xbar is slang for crossbar), and ran the experiments.</p>

<table class="xb">
  <thead>
    <tr>
      <th>Tool</th>
      <th>Version</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Yosys</td>
      <td>0.33</td>
    </tr>
    <tr>
      <td>ABC</td>
      <td>1.01</td>
    </tr>
    <tr>
      <td>Vivado</td>
      <td>2025.2</td>
    </tr>
    <tr>
      <td>OS</td>
      <td>Kubuntu 24.04.5 LTS</td>
    </tr>
  </tbody>
</table>

<p>I started with the generic LUT mapping flow in Yosys, using 4-input LUTs:</p>

<table class="xb">
  <thead>
    <tr>
      <th>Design</th>
      <th class="r">LUT4</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full 5×5</td>
      <td class="r">1125</td>
    </tr>
    <tr>
      <td>Dimension-sliced</td>
      <td class="r"><strong>711</strong></td>
    </tr>
  </tbody>
</table>

<p>I looked at that and thought:</p>

<blockquote>
  <p>I am intelligent.</p>
</blockquote>

<p>The dimension-sliced version was significantly smaller. This was going exactly how I imagined it would.</p>

<p>So instead of stopping there like a responsible engineer, I took both designs to Vivado.</p>

<h2 id="then-vivado-happened">Then Vivado happened</h2>

<p>For the FPGA experiment I targeted <code class="language-plaintext highlighter-rouge">xc7a200t-fbg676-2</code> with Vivado 2025.2. The result:</p>

<table class="xb">
  <thead>
    <tr>
      <th>Design</th>
      <th class="r">LUT5</th>
      <th class="r">LUT6</th>
      <th class="r">Total LUTs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full 5×5</td>
      <td class="r">160</td>
      <td class="r">160</td>
      <td class="r"><strong>320</strong></td>
    </tr>
    <tr>
      <td>Dimension-sliced</td>
      <td class="r">160</td>
      <td class="r">160</td>
      <td class="r"><strong>320</strong></td>
    </tr>
  </tbody>
</table>

<p>Wait. 320? Both?</p>

<p>I had just seen 1125 versus 711 in Yosys, and now Vivado was telling me the two designs were identical in size. I had no idea what was happening. This was the point where I stopped pretending I understood synthesis.</p>

<h2 id="so-i-went-back-to-yosys">So I went back to Yosys</h2>

<p>This time I didn’t only run the 4-LUT experiment. I swept the LUT input width:</p>

<table class="xb">
  <thead>
    <tr>
      <th class="r">LUT width</th>
      <th class="r">Full 5×5</th>
      <th class="r">Dimension-sliced</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="r">4</td>
      <td class="r">1125</td>
      <td class="r"><strong>711</strong></td>
    </tr>
    <tr>
      <td class="r">5</td>
      <td class="r">645</td>
      <td class="r"><strong>480</strong></td>
    </tr>
    <tr>
      <td class="r">6</td>
      <td class="r">490</td>
      <td class="r"><strong>352</strong></td>
    </tr>
    <tr>
      <td class="r">7</td>
      <td class="r">320</td>
      <td class="r"><strong>256</strong></td>
    </tr>
    <tr>
      <td class="r">8</td>
      <td class="r">320</td>
      <td class="r"><strong>256</strong></td>
    </tr>
    <tr>
      <td class="r">9</td>
      <td class="r"><strong>160</strong></td>
      <td class="r">256</td>
    </tr>
    <tr>
      <td class="r">10</td>
      <td class="r"><strong>160</strong></td>
      <td class="r">256</td>
    </tr>
    <tr>
      <td class="r">11</td>
      <td class="r"><strong>160</strong></td>
      <td class="r">256</td>
    </tr>
    <tr>
      <td class="r">12</td>
      <td class="r"><strong>160</strong></td>
      <td class="r">256</td>
    </tr>
  </tbody>
</table>

<p>And I sat there for a while.</p>

<p>The LUT input count itself changes the result. It isn’t just <em>design → number of LUTs</em>. It’s <em>design + target LUT architecture → number of LUTs</em>. The dimension-sliced design wins at the smaller LUT widths in this generic experiment, but once the LUTs get wide enough the full crossbar becomes smaller.</p>

<p>That explained why my first LUT4 number wasn’t some universal truth about the two architectures. It was a result for one particular LUT model, and it was far more useful than the number that made me feel clever.</p>

<h2 id="and-then-i-looked-at-timing">And then I looked at timing</h2>

<p>I ran both designs through Vivado timing analysis with a virtual 10 ns clock. Placement was not constrained and neither design met the target, so these are not final NoC timing numbers. I mainly wanted the relative comparison.</p>

<table class="xb">
  <thead>
    <tr>
      <th>Design</th>
      <th class="r">WNS</th>
      <th class="r">TNS</th>
      <th class="r">Failing endpoints</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full 5×5</td>
      <td class="r"><strong>-1.153 ns</strong></td>
      <td class="r"><strong>-99.740 ns</strong></td>
      <td class="r">158 / 160</td>
    </tr>
    <tr>
      <td>Dimension-sliced</td>
      <td class="r">-1.426 ns</td>
      <td class="r">-132.273 ns</td>
      <td class="r">151 / 160</td>
    </tr>
  </tbody>
</table>

<p>The full crossbar had the better WNS and TNS.</p>

<p>My best guess is the bridge. I added it to make the architecture smarter, but a path from an X-direction input to a Y-direction output now has to go through both sides of the split, which adds a level of logic to the common turning case. So I removed some impossible connections and then added a physical bridge that made the turns that actually happen slower.</p>

<p>That hurt.</p>

<p>At this point I was ready to blame FPGAs. Vendor locked. Corporate. Weird architecture. Yosys, on the other hand, was sitting there being open source and letting me poke around inside the flow, and I love open source.</p>

<p>Maybe the FPGA world just wasn’t ready for my intelligence yet. Maybe I was destined for the ASIC world.</p>

<p>ASIC world, here I come.</p>

<h2 id="then-the-asic-experiment-put-the-final-nail-in-the-coffin">Then the ASIC experiment put the final nail in the coffin</h2>

<p>I took both designs and mapped them to the Nangate45 standard-cell library. Now I wasn’t thinking in LUTs anymore. The design gets mapped into actual standard cells, and I was suddenly looking at things like <code class="language-plaintext highlighter-rouge">AOI21_X1</code>, <code class="language-plaintext highlighter-rouge">AOI222_X1</code>, <code class="language-plaintext highlighter-rouge">AOI22_X1</code> and <code class="language-plaintext highlighter-rouge">OAI33_X1</code>.</p>

<p>What are these?</p>

<p>AOI and OAI cells combine several Boolean operations into a single cell. Instead of building everything from tiny primitive gates, the mapper can recognise a Boolean structure and map it onto a cell that already implements it. I had spent a lot of time thinking about muxes and LUTs, and this was a completely different way of looking at the same RTL.</p>

<p>Then I got the area report.</p>

<table class="xb">
  <thead>
    <tr>
      <th>Design</th>
      <th class="r">Area</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full 5×5</td>
      <td class="r"><strong>759.430 µm²</strong></td>
    </tr>
    <tr>
      <td>Dimension-sliced, X crossbar</td>
      <td class="r">389.424 µm²</td>
    </tr>
    <tr>
      <td>Dimension-sliced, Y crossbar</td>
      <td class="r">397.404 µm²</td>
    </tr>
    <tr>
      <td>Dimension-sliced, total</td>
      <td class="r">820.876 µm²</td>
    </tr>
  </tbody>
</table>

<p>The full crossbar was <strong>61.446 µm² smaller</strong>, about <strong>7.49%</strong>, than the version I had specifically designed to remove unnecessary hardware.</p>

<p>The full crossbar mapped to:</p>

<table class="xb">
  <thead>
    <tr>
      <th>Cell</th>
      <th class="r">Count</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AND3_X1</td>
      <td class="r">5</td>
    </tr>
    <tr>
      <td>AOI21_X1</td>
      <td class="r">160</td>
    </tr>
    <tr>
      <td>AOI222_X1</td>
      <td class="r">160</td>
    </tr>
    <tr>
      <td>AOI22_X1</td>
      <td class="r">160</td>
    </tr>
    <tr>
      <td>INV_X1</td>
      <td class="r">15</td>
    </tr>
    <tr>
      <td>NOR3_X1</td>
      <td class="r">20</td>
    </tr>
  </tbody>
</table>

<p>The full crossbar still had all those theoretically illegal connections. I had deliberately removed them in the other design. And the full crossbar still came out smaller.</p>

<p>I was dumbstruck.</p>

<p>The worst part wasn’t that the full crossbar won. It was that I didn’t understand why. I could explain the FPGA result, because I could look at the LUT architecture and see that the target primitive changed the answer. Here I had a standard-cell library, no LUT-width ceiling to blame, and I had still made my supposedly optimised design bigger.</p>

<p>That was honestly more painful than a bad number. It was also the part that made this experiment worth doing.</p>

<h2 id="the-thing-i-realised-about-asic-and-fpga-synthesis">The thing I realised about ASIC and FPGA synthesis</h2>

<p>This slowly made me realise that FPGA and ASIC synthesis really are two different ball games.</p>

<p>In college, we learn an ASIC-esque design flow: write RTL, synthesise, talk about timing and area, and then very often the story ends with <strong>run it on an FPGA and call it a day.</strong> I don’t mean that as an attack on what we learn. It’s a useful flow and it gives you something physical to work with.</p>

<p>But there is a whole other world behind it. The FPGA flow asks you to map your logic onto a specific set of programmable resources. The ASIC flow hands the tool a standard-cell library and lets it choose from a completely different collection of building blocks, with different drive strengths, cell areas, and timing characterised for that library.</p>

<p>That was probably the most valuable part of the whole experiment.</p>

<h2 id="so-was-the-dimension-sliced-crossbar-actually-bad">So was the dimension-sliced crossbar actually bad?</h2>

<table class="xb">
  <thead>
    <tr>
      <th></th>
      <th class="r">Generic Yosys</th>
      <th class="r">Xilinx Vivado</th>
      <th class="r">Nangate45</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full 5×5</td>
      <td class="r">1125 LUT4</td>
      <td class="r">320 LUTs</td>
      <td class="r"><strong>759.430 µm²</strong></td>
    </tr>
    <tr>
      <td>Dimension-sliced</td>
      <td class="r"><strong>711 LUT4</strong></td>
      <td class="r">320 LUTs</td>
      <td class="r">820.876 µm²</td>
    </tr>
  </tbody>
</table>

<p>For the Xilinx target I tested, the dimension-sliced design gave no area advantage and made timing worse. For Nangate45, the full crossbar was about 7.5% smaller. At first glance the conclusion is obvious: dimension slicing was a bad idea.</p>

<p>Except I don’t think that’s quite right.</p>

<p>While writing this up I went back through the results again, and the conclusion I was originally writing towards was slightly wrong. At 4 LUT inputs, the dimension-sliced design was 711 LUTs against 1125 for the full crossbar. That’s not a small gap. If I were targeting a technology where 4-input LUTs are the real primitive, the idea suddenly becomes worth discussing.</p>

<p>That doesn’t mean every 4-LUT FPGA will love my architecture. The Yosys result is still a generic mapping experiment. But it does mean the idea wasn’t stupid. I was asking a technology-dependent question and expecting a universal answer, and synthesis doesn’t work like that.</p>

<h2 id="what-i-actually-learned">What I actually learned</h2>

<p>I started this because I thought I had found an obvious optimisation. Nine connections that XY routing never uses. Remove them, smaller hardware, done.</p>

<p>Instead I learned that:</p>

<ul>
  <li>a generic LUT experiment is not the same as mapping onto a real FPGA,</li>
  <li>LUT input width can completely change the result,</li>
  <li>FPGA and ASIC synthesis optimise for very different target architectures,</li>
  <li>an architectural reduction does not automatically mean a physical area reduction,</li>
  <li>and the synthesis result can be more interesting than the architecture idea that started it.</li>
</ul>

<h2 id="what-i-havent-tested-yet">What I haven’t tested yet</h2>

<p>Looking back at it, I actually tested two ideas at once and only noticed afterwards.</p>

<p>The first was pruning: remove the 9 connections XY routing never uses. The second was the structure I chose to implement it, which was two smaller crossbars with a bridge between them. Every result I got is for the combination. The bridge is my best explanation for the timing loss, and the full crossbar collapsing to exactly 160 LUTs at wide LUT widths suggests it maps very cleanly, while the two-stage version never gets there.</p>

<p>So I can’t say pruning is a bad idea. I can only say pruning <em>with a bridge</em> lost on the targets I tried.</p>

<p>The experiment I haven’t run yet is the obvious one: keep a single-level crossbar and just delete the 9 illegal inputs from each output mux. No slicing, no bridge. If that one wins, the idea was fine and my implementation of it was the problem. If it loses too, then I’ll finally have a result that points at the idea itself.</p>

<p>And underneath that is the bigger thing this left me with. I want to put FPGA and ASIC synthesis side by side and understand why the same RTL changes shape when the target changes, not just read off a LUT count.</p>

<p>I’ll write it up when I get to it.</p>

<p><strong>Pick your poison.</strong></p>]]></content><author><name>rohith</name></author><category term="synthesis" /><category term="yosys" /><category term="abc" /><category term="fpga" /><category term="asic" /><category term="crossbar" /><category term="NoC" /><summary type="html"><![CDATA[I know what I'm doing with that title. If you got the reference, welcome cultured people.]]></summary></entry><entry><title type="html">How Many Body Flits Is Too Many?</title><link href="https://rohith-04.github.io//2026/08/24/how-many-body-flits-is-too-many.html" rel="alternate" type="text/html" title="How Many Body Flits Is Too Many?" /><published>2026-08-24T00:00:00+00:00</published><updated>2026-08-24T00:00:00+00:00</updated><id>https://rohith-04.github.io//2026/08/24/how-many-body-flits-is-too-many</id><content type="html" xml:base="https://rohith-04.github.io//2026/08/24/how-many-body-flits-is-too-many.html"><![CDATA[<p><em>A naive man’s attempt to understand the efficiency of a NoC.</em></p>

<p>It all started with a simple question:</p>

<p><strong>How many body flits is too many body flits?</strong></p>

<p>Is having 100 body flits in a packet okay? Sure, but then the resources associated with that flow remain occupied for a brutal amount of time.</p>

<p>It’s crazy to get all that transmission efficiency from sending just one head flit and one tail flit, only for a small packet to be trapped behind it waiting for its turn.</p>

<p>This is called <strong>serialization delay</strong>.</p>

<p>So the naive man thought, <em>let’s improve fairness in the arbiter.</em></p>

<p>But alas! Fairness doesn’t make the packet shorter. The arbiter can be perfectly fair, and the Virtual Channel (VC) is still held hostage while all those body flits slowly make their way through.</p>

<p>The naive man was naive again.</p>

<p>A 100-flit packet simply consumes more cycles, and in a wormhole-style design, the VC allocation remains locked until the tail flit finally passes. At first glance, this looks like a purely <strong>temporal</strong> problem: the packet simply exists for too long.</p>

<p>The obvious solution is to give the router more choices. If one VC is occupied, let another packet use a different VC, assuming the required downstream buffers are available.</p>

<blockquote>
  <p>“Then increase the VCs,” he said.<br />
“But SRAM is costly,” they said.</p>

  <p><em>This is capitalism baring its fangs.</em></p>
</blockquote>

<p>And then the arbitration logic bites back, too. Every additional VC adds another possible request that the allocator has to consider. More VCs mean more state, and wider, far more complex allocation logic. The exact cost depends on the router architecture, but eventually, the hardware needed just to <em>decide</em> who gets the resource starts becoming part of the bottleneck itself.</p>

<p>Which makes this feel like a temporal problem disguising a spatial one.</p>

<p>We started by worrying about how much time a packet spends holding a resource. But the only way to escape that temporal delay is by spending physical hardware: more buffers, more VCs, more arbitration gates. The delay is temporal, but the way we buy concurrency is spatial. And the cost of that solution is spatial, too.</p>

<p>So perhaps the real problem was never just that the packet takes too many cycles. It is that we have a finite amount of hardware trying to force temporal contention into spatial concurrency.</p>

<p>We have gone in a full circle, and the original question returns:</p>

<p><strong>How many body flits is too many?</strong></p>

<p>Apparently, there is no magic number.</p>

<p>Just a collection of tradeoffs waiting to bite you.</p>]]></content><author><name>rohith</name></author><category term="architecture" /><category term="rtl" /><category term="tradeoffs" /><summary type="html"><![CDATA[A naive man's attempt to understand the efficiency of a NoC, serialization delay, and the brutal reality of hardware design.]]></summary></entry><entry><title type="html">First In, Still Waiting</title><link href="https://rohith-04.github.io//2026/08/13/first-in-still-waiting.html" rel="alternate" type="text/html" title="First In, Still Waiting" /><published>2026-08-13T00:00:00+00:00</published><updated>2026-08-13T00:00:00+00:00</updated><id>https://rohith-04.github.io//2026/08/13/first-in-still-waiting</id><content type="html" xml:base="https://rohith-04.github.io//2026/08/13/first-in-still-waiting.html"><![CDATA[<p><em>Designing the input FIFO for the router.</em></p>

<p><img src="/assets/images/noc/fifo/fifo.png" alt="Five-port router in the 2D mesh" style="max-width: 650px; width: 100%; height: auto;" /></p>

<p>I wanted to tackle the FIFO first for two reasons. First, I was confident I could write one. Second, it is the first block the data hits when it enters the router, so it made sense to start there.</p>

<p>I was thinking I didn’t want this to be a simple FIFO. Everything from virtual channels to contention eventually rides on these buffers, and these little simple shits are expensive. SRAM is expensive, so I wanted to make sure I was getting as much use out of it as I could.</p>

<p>A simple FIFO is pretty straightforward. You have a memory, a read pointer, a write pointer, and some logic to tell you when the FIFO is full or empty. That would have worked, but I wanted to see if there was anything useful I could squeeze out of it before calling it done.</p>

<p>I already knew about FWFT FIFOs, and I liked the idea behind them. If the data is already available, why make the consumer wait another cycle just because the FIFO wants to be formal about the procedure?</p>

<p>I didn’t need a full FWFT implementation for the router, but I could take the idea and apply it where it actually helped. So I added simultaneous read/write handling and an empty-FIFO bypass.</p>

<p>If the FIFO is empty and a read and write happen in the same cycle, the incoming data can go straight to the output instead of taking a trip through the memory just to immediately come back out.</p>

<p>That felt like a useful little optimisation rather than adding complexity for the sake of it.</p>

<p>Then there was the other side of the FIFO, the part that deals with the router’s flow control.</p>

<p>The router uses a credit-based system, so a write should only happen when there is space available in the buffer. I kept the credit logic outside the FIFO itself, but added a protocol error at the FIFO level when an illegal operation is attempted. A write while full and a read while empty both raise <code class="language-plaintext highlighter-rouge">err</code>. It’s mostly there as a debugging signal, but I like having the FIFO complain when something that should never happen actually happens.</p>

<p>There is another case I wanted to make explicit too. If the FIFO is full and both read and write are asserted, the read still happens but the write is rejected. The FIFO was full when the write was presented, so I don’t want the behavior to depend on treating the simultaneous read as some kind of retroactive permission.</p>

<p>I liked the design I ended up with. I think I hit a decent sweet spot where I didn’t over-engineer the FIFO, but I also didn’t just grab an off-the-shelf FIFO and call it a day.</p>

<p>Then, while writing this post, I came across something.</p>

<p><strong>Wait. How do I handle credits for the bypass condition?</strong></p>

<p>The flit never actually enters the FIFO in that case. And there is the full-FIFO case too, where the read happens but the write is rejected. The FIFO’s occupancy and the interface signals aren’t quite telling the same story anymore, so the credit controller needs to account for what actually happened.</p>

<p>I think I can handle both cases using the full/empty conditions, the read and write pointers, and some combinational logic to track these states.</p>

<p>Hmm.</p>

<p><strong>The blog is useful after all.</strong></p>

<p>Now there is another voice calling me, and this one is asking for <strong>numbers</strong>.</p>

<p>The RTL tells me what the FIFO does. Simulation can tell me what it actually does under traffic.</p>

<p>I can put some instrumentation behind an <code class="language-plaintext highlighter-rouge">`ifdef SIM</code> and start collecting things like buffer occupancy, how often the bypass is actually hit, and how long a flit spends waiting in the buffer.</p>

<p>Now that is gold.</p>

<p>I want to see those numbers.</p>]]></content><author><name>rohith</name></author><category term="FIFO" /><category term="Buffer" /><category term="SRAM" /></entry><entry><title type="html">Router Microarchitecture</title><link href="https://rohith-04.github.io//2026/08/11/router-microarchitecture.html" rel="alternate" type="text/html" title="Router Microarchitecture" /><published>2026-08-11T00:00:00+00:00</published><updated>2026-08-11T00:00:00+00:00</updated><id>https://rohith-04.github.io//2026/08/11/router-microarchitecture</id><content type="html" xml:base="https://rohith-04.github.io//2026/08/11/router-microarchitecture.html"><![CDATA[<p>The router is where the NoC architecture stops being a collection of ideas and starts becoming actual hardware.</p>

<p><img src="/assets/images/noc/router_microarch/router_block_diagram.png" alt="Five-port router in the 2D mesh" style="max-width: 650px; width: 100%; height: auto;" /></p>

<p><em>Five-port router in the 2D mesh.</em>
.</p>

<p><img class="architecture-diagram" src="/assets/images/noc/router_microarch/Router.png" alt="Router microarchitecture" /></p>

<p><em>Router microarchitecture.</em></p>

<p>Its job is deceptively simple: accept a flit, figure out where it needs to go, acquire the resources required to get there, resolve contention, and move it to the next router.</p>

<p>The difficulty is that all of those things are competing for the same few cycles.</p>

<p>The current router uses a <strong>5-port input-buffered architecture</strong> with two virtual channels per port, credit-based flow control, deterministic XY routing, and a pipelined datapath:</p>

<p><strong>BW → VA → SA → ST → LT</strong></p>

<p>The stages are arranged so that work which can be done early is done early, while the stages responsible for actually moving data are kept as clean as possible.</p>

<hr />

<h2 id="starting-with-the-buffer">Starting with the Buffer</h2>

<p>The first architectural question was where to put the buffers.</p>

<p>Input buffering is the obvious answer only until you stop and ask why.</p>

<p>A flit arrives at an input port, but it may not be able to move immediately. The router may be busy, the required downstream VC may not be available, or the flit may lose switch arbitration. Something has to hold it while that gets sorted out.</p>

<p>With input buffering, that waiting happens exactly where the flit arrived.</p>

<p>The flit sits in the input FIFO until it has everything it needs to move. The input side does not need to know whether the crossbar is available or whether the flit will win arbitration. It just needs to accept the transfer when it has buffer space.</p>

<p>That makes the input buffer a useful boundary between <strong>receiving traffic</strong> and <strong>deciding what to do with it</strong>.</p>

<h3 id="why-not-output-buffering">Why not output buffering?</h3>

<p>The alternative initially seems reasonable.</p>

<p>Suppose North and West both want East in the same cycle. Only one flit can physically leave through East, so one of them has to wait somewhere. An output buffer seems like a natural place to absorb that contention. It appears to free up the input side and let incoming traffic continue while the output handles the waiting.</p>

<p>My first assumption was that this would then require another arbitration stage at the output to decide which waiting flit should be served next.</p>

<p>Then I looked at the router pipeline again.</p>

<p><strong>SA has already made that decision.</strong></p>

<p>Switch allocation has already decided which input gets access to a given output for the current cycle. The crossbar is not making another scheduling decision. It is simply implementing the connection selected by the allocator.</p>

<p>So an additional output arbiter would just be making the same decision twice.</p>

<p>That still leaves the more fundamental question: what happens if the input buffers disappear entirely and only the output side is buffered?</p>

<p>Now the crossbar has to accept traffic from every input as it arrives, with nothing upstream holding a flit while the router decides whether it can proceed.</p>

<p>There are two obvious ways to make that work.</p>

<p>The first is to run the crossbar faster than the rest of the router so it can drain all possible incoming transfers within a single router cycle.</p>

<p>No.</p>

<p>The second is to buffer at the crosspoints of the crossbar. For an (N)-port router, that potentially means (N^2) buffering locations.</p>

<p>Five ports is not catastrophic. The problem is the direction of the scaling. The buffering requirement grows quadratically with the number of ports.</p>

<p>Multi-write-port FIFOs can change the implementation tradeoff, but introducing more complicated memory structures simply to avoid buffering at the inputs is difficult to justify.</p>

<p>Input buffering therefore remains the architectural choice.</p>

<p>Not because it was the first option considered, but because the alternatives were followed far enough to expose their actual costs.</p>

<hr />

<h2 id="the-flit-lifecycle">The Flit Lifecycle</h2>

<p>With the buffering decision made, the next question is what actually happens to a flit once it enters the router.</p>

<p>At a high level:</p>

<p><strong>Buffer → Allocate VC → Allocate Switch → Traverse Crossbar → Traverse Link</strong></p>

<p>The actual pipeline is:</p>

<p><strong>BW → VA → SA → ST → LT</strong></p>

<p>Each stage has a fairly specific job.</p>

<hr />

<h2 id="bw-buffer-write">BW: Buffer Write</h2>

<p>A flit arrives at an input port.</p>

<p>The first thing that happens is, thankfully, not routing.</p>

<p>The incoming transfer is sampled on the clock edge and the flit is written into the input FIFO. This stage is deliberately simple. No routing, no arbitration, no credit management.</p>

<p>Just:</p>

<p><strong>Is the transfer valid? If it is, write the flit.</strong></p>

<p>I want the rest of the router to work with registered state rather than whatever happens to be sitting on the input wires. There is no point doing interesting work before the data has even been captured.</p>

<p>Once the flit is in the input buffer, the router can start working on it.</p>

<hr />

<h2 id="route-computation">Route Computation</h2>

<p>If the flit is a head flit, route computation begins.</p>

<p>The slightly strange part is that the router is calculating the route for the <strong>next router</strong>, not this one.</p>

<p>This router already knows where the flit is. What matters is where it needs to go after it leaves.</p>

<p>The destination information is extracted from the header and used to determine the next direction. Since the network uses deterministic XY routing, the decision is straightforward, but the important part is <strong>when</strong> it happens.</p>

<p>The route is computed early rather than becoming another piece of work on the critical path when the flit is finally ready to cross the switch.</p>

<p>The result can sit there while the flit waits for the resources it needs.</p>

<p>Might as well make the waiting time useful.</p>

<hr />

<h2 id="va-virtual-channel-allocation">VA: Virtual Channel Allocation</h2>

<p>The head flit now needs a VC in the next router.</p>

<p>Body and tail flits do not need to repeat this process. They follow the VC established by their head flit.</p>

<p>The mesh topology gives us a useful shortcut here.</p>

<p>If a flit needs to enter the West port of the next router, it has to leave the current router through its East output. The same relationship exists for every direction. The current output directly corresponds to the input port that the flit will use at the next router.</p>

<p>That means the VC request can be handled using the VC arbiter associated with the <strong>current output port</strong>.</p>

<p>If multiple input ports want VCs at the same downstream port, they are already competing at the same output here. The VC arbiter resolves that contention and grants one of the available downstream VCs.</p>

<p>This is where VA and SA need to be kept separate.</p>

<p>VA is asking:</p>

<p><strong>“Can I reserve a VC at the next router?”</strong></p>

<p>SA is asking:</p>

<p><strong>“Can I use this router’s crossbar?”</strong></p>

<p>Two different resources, two different decisions.</p>

<p>Winning VC allocation means the flit has secured a place in the next router.</p>

<p>It still hasn’t earned the right to leave this one.</p>

<hr />

<h2 id="sa-switch-allocation">SA: Switch Allocation</h2>

<p>Now the flit reaches switch allocation.</p>

<p>There are two rounds because there are two different contention problems.</p>

<h3 id="first-stage">First stage</h3>

<p>An input port can have multiple VCs with eligible flits.</p>

<p>Only one of them can be presented to the crossbar in a cycle, so the first stage chooses which VC gets to compete.</p>

<p>Now every input has at most one contender.</p>

<h3 id="second-stage">Second stage</h3>

<p>The remaining contenders compete for the output ports they want.</p>

<p>North and West might both want East this cycle.</p>

<p>East is, unfortunately, still only one East.</p>

<p>The second stage resolves that contention and selects which input gets the output.</p>

<p>Nothing has moved yet.</p>

<p>VA and SA are making decisions. The flit is still sitting in the input FIFO.</p>

<p>If it loses arbitration, it stays there and tries again on the next cycle.</p>

<p>There is no partial credit for almost winning.</p>

<hr />

<h2 id="st-switch-traversal">ST: Switch Traversal</h2>

<p>Once the flit wins switch allocation, the router finally gets to move some data.</p>

<p>The crossbar establishes the connection selected by the allocator, and the flit traverses the crossbar to its granted output.</p>

<p>This is where all the earlier decisions become an actual data movement.</p>

<p>The route information computed earlier is also inserted into the <strong>flit’s payload</strong> at this point.</p>

<p>Until now, the route computation was local control information. Now it becomes part of the flit itself and travels with it to the next router.</p>

<p>The router has done its thinking.</p>

<p>The flit gets to leave.</p>

<hr />

<h2 id="lt-link-traversal">LT: Link Traversal</h2>

<p>The flit then traverses the physical link to the next router.</p>

<p>This is probably the least intellectually exciting stage. After all the allocation and arbitration, it is essentially:</p>

<p><strong>send the flit.</strong></p>

<p>But the work done in the previous stages matters here.</p>

<p>When the flit arrives at the next router, the route information carried in its payload is already available. The next router does not need to spend another cycle figuring out the direction from scratch.</p>

<p>It can use the information computed by the previous router and continue the same process.</p>

<p>Then the cycle repeats.</p>

<hr />

<h2 id="virtual-channels-and-flow-control">Virtual Channels and Flow Control</h2>

<p>The router has <strong>two virtual channels per port</strong>, with credit-based flow control between neighboring routers.</p>

<p>The important distinction is that a virtual channel is not another physical link. Multiple logical channels share the same physical resources while maintaining separate flow-control state.</p>

<p>The credits provide the information needed to determine whether the downstream buffer has space before sending a flit.</p>

<p>This fits naturally with the input-buffered architecture. The downstream router owns the storage, while the upstream router keeps track of whether that storage can accept another flit.</p>

<p>The resulting relationship is simple:</p>

<p><strong>Downstream buffer space → credit → upstream transmission permission</strong></p>

<p>The details of the credit state and per-VC control form part of the input-port architecture rather than being mixed into the data path itself.</p>

<hr />

<h2 id="the-crossbar">The Crossbar</h2>

<p>The crossbar is the physical structure that connects the router’s inputs to its outputs.</p>

<p>It does not decide which input should win.</p>

<p>That decision has already been made by switch allocation.</p>

<p>This distinction matters because it keeps the control and datapath separate. SA produces the connections that are allowed for the current cycle, and the crossbar implements those connections.</p>

<p>The result is a clean division:</p>

<p><strong>Allocator:</strong> decide.</p>

<p><strong>Crossbar:</strong> connect.</p>

<p>The crossbar therefore does not need to understand packet destinations, VC ownership, or arbitration policy. It only needs to establish the selected input-to-output paths and move the corresponding flits.</p>

<hr />

<h2 id="putting-the-router-together">Putting the Router Together</h2>

<p>At this point the router can be viewed as a sequence of increasingly constrained decisions.</p>

<p>A flit first becomes local state.</p>

<p><strong>BW</strong></p>

<p>Then its destination is determined and, for a head flit, a downstream VC is requested.</p>

<p><strong>VA</strong></p>

<p>Once the required resources are available, it competes for the crossbar.</p>

<p><strong>SA</strong></p>

<p>If it wins, the datapath finally moves it.</p>

<p><strong>ST</strong></p>

<p>Then the physical link carries it to the next router.</p>

<p><strong>LT</strong></p>

<p>The entire path is therefore:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>              ┌───────────────┐
              │  Input FIFO   │
              └───────┬───────┘
                      │
                      ▼
                     BW
                      │
                      ▼
                     VA
                      │
                      ▼
                     SA
                      │
                      ▼
                  Crossbar
                      │
                      ▼
                     ST
                      │
                      ▼
                     LT
                      │
                      ▼
                Next Router
</code></pre></div></div>

<p>The architecture is intentionally structured so that <strong>eligibility, allocation, and movement are separate problems</strong>.</p>

<p>A flit can be present without being eligible.</p>

<p>It can be eligible without winning arbitration.</p>

<p>It can win arbitration without having physically moved yet.</p>

<p>Only after all of those conditions are satisfied does the crossbar actually carry the data.</p>

<p>That separation is what allows the router to pipeline the work rather than making every stage wait for the entire decision process to finish.</p>

<hr />

<h2 id="what-comes-next">What Comes Next</h2>

<p>The microarchitecture is now defined well enough that the next problem is no longer deciding what the router should be.</p>

<p>It is building it.</p>

<p>The immediate work is around the individual structures that make up this pipeline: the input-port cluster, VC control, arbitration, crossbar, and the ST/LT datapath.</p>

<p>Once those pieces are implemented and verified independently, they can be brought together into the complete router.</p>

<p>At that point the interesting question changes from:</p>

<p><strong>“Can this router move a flit?”</strong></p>

<p>to:</p>

<p><strong>“What happens when an entire network of these routers starts moving them at the same time?”</strong></p>

<p>That is where contention stops being a local issue and becomes a network-wide behavior.</p>]]></content><author><name>rohith</name></author><category term="NoC" /><category term="Network-on-Chip" /><category term="Router Microarchitecture" /><category term="Computer Architecture" /><category term="RTL Design" /><category term="SystemVerilog" /><category term="Virtual Channels" /><category term="Flow Control" /></entry><entry><title type="html">Why this blog exists</title><link href="https://rohith-04.github.io//2026/07/20/why-this-blog-exists.html" rel="alternate" type="text/html" title="Why this blog exists" /><published>2026-07-20T00:00:00+00:00</published><updated>2026-07-20T00:00:00+00:00</updated><id>https://rohith-04.github.io//2026/07/20/why-this-blog-exists</id><content type="html" xml:base="https://rohith-04.github.io//2026/07/20/why-this-blog-exists.html"><![CDATA[<p>If this blog survives longer than three posts, I’ll consider it a success.</p>

<p>If Project Linnea eventually becomes a working multicore system, that’ll be nice too.</p>

<p>I’ve started and abandoned enough half-finished write-ups to know how this usually goes. A burst of motivation, a post explaining what I’m about to build, and then silence for six months while the actual building happens somewhere no one can see it. The blog dies quietly. The project, if I’m lucky, doesn’t.</p>

<p>I’d like to try doing both at once this time.</p>

<p>Not because I think anyone is waiting for progress updates on a NoC from a final-year student in Hyderabad. I don’t think that. I’m writing this mostly for the version of me six months from now, who will have forgotten why a particular arbitration scheme seemed like a good idea at 2 AM, or what exactly broke during verification and how I fixed it. Notes fade. This is meant to be a slightly more permanent notebook than the ones I keep losing.</p>

<p>If someone else finds these notes useful along the way, that’s a nice bonus.</p>

<p>There’s also a simpler reason. I think better when I have to explain something, and I explain things better when I know someone else might actually read it. Most of what happens while building Linnea stays in my head, half-formed, until I’m forced to put it into words. This blog is where I force myself to finish those thoughts instead of letting them remain vague ideas that never quite become anything.</p>

<p>So this is what the blog is for: the good, the bad, and the ugly. Design decisions I’m proud of and others I’ll almost certainly regret. Things that work, things that don’t, and the occasional rant when something takes four days longer than it should have. If you’re here for polished updates only, you’re in the wrong place.</p>

<p>No promises on a schedule. Updated when I have something to say, same as the rest of the site.</p>

<p>Let’s see how far past three posts this gets.</p>]]></content><author><name>rohith</name></author></entry></feed>