<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://stpr-dev.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://stpr-dev.github.io/" rel="alternate" type="text/html" /><updated>2026-05-30T06:30:27+00:00</updated><id>https://stpr-dev.github.io/feed.xml</id><title type="html">Shri’s Technical Blog</title><subtitle>Systems engineer working across the stack: hardware-level acquisition, signal processing, and edge inference for real-time biomedical systems.
</subtitle><author><name>Shri</name></author><entry><title type="html">Teensy 4.1 USB Serial: What is the maximum throughput over USB serial? - Part 4</title><link href="https://stpr-dev.github.io/embedded/2026/05/23/teensy-usb-serial-max-speed-part-4/" rel="alternate" type="text/html" title="Teensy 4.1 USB Serial: What is the maximum throughput over USB serial? - Part 4" /><published>2026-05-23T00:00:00+00:00</published><updated>2026-05-23T00:00:00+00:00</updated><id>https://stpr-dev.github.io/embedded/2026/05/23/teensy-usb-serial-max-speed-part-4</id><content type="html" xml:base="https://stpr-dev.github.io/embedded/2026/05/23/teensy-usb-serial-max-speed-part-4/"><![CDATA[<p>This is part 4 of my series on the Teensy 4.1 USB serial throughput. Read:</p>
<ul>
  <li><a href="https://stpr-dev.github.io/embedded/2026/05/19/teensy-usb-serial-max-speed/">part 1</a> for a bit more background on what I want to achieve.</li>
  <li><a href="https://stpr-dev.github.io/embedded/2026/05/20/teensy-usb-serial-max-speed-part-2/">part 2</a> for benchmark numbers to set some context.</li>
  <li><a href="https://stpr-dev.github.io/embedded/2026/05/21/teensy-usb-serial-max-speed-part-3/">part 3</a> for more numbers.</li>
</ul>

<p>As a followup to part 3, I decided to look into latency numbers as this might provide some insights into the timing jitter. I took the results I logged across the four systems I used in the last experiment and made some plotly plots to see CDF of latency. I also wanted to see if integrating these plots into a GH blog page works seamlessly. Turns out, it is easier than I thought!</p>

<iframe src="/assets/plots/2026-05-23/index.html" width="100%" height="1000px" frameborder="0" scrolling="no">
</iframe>

<p>On Windows, zero-latency values show up in large numbers – likely an artefact of the Go reader pulling multiple buffered frames back-to-back, where the system clock doesn’t have the resolution to distinguish individual frame arrivals. These aren’t meaningful, so I trimmed them (you will see how many were trimmed by looking for the message “X% of samples at or below timing resolution”). Only above-zero values are reflected in the Windows stats. I also trimmed the first 100 samples for each run since there was an intentional delay on Teensy side for a few milliseconds before a transfer started – I didn’t want that polluting the results. Failed runs are excluded throughout; on Linux there were none.</p>

<p>The main question I had was whether tail latencies were bad enough to matter. They’re not — a few millisecond-scale spikes show up at p99.99, but p99 and the median look quite reasonable. The Linux numbers in particular are consistent enough that I’m comfortable calling this interface reliable for my use case.</p>

<p>That wraps up this miniseries. The Teensy USB serial path holds up on Linux: throughput is where it needs to be based on tests from Part 3, and the latency distribution doesn’t have anything that would destabilise a real-time pipeline.</p>]]></content><author><name>Shri</name></author><category term="embedded" /><summary type="html"><![CDATA[This is part 4 of my series on the Teensy 4.1 USB serial throughput. Read: part 1 for a bit more background on what I want to achieve. part 2 for benchmark numbers to set some context. part 3 for more numbers.]]></summary></entry><entry><title type="html">Teensy 4.1 USB Serial: What is the maximum throughput over USB serial? - Part 3</title><link href="https://stpr-dev.github.io/embedded/2026/05/21/teensy-usb-serial-max-speed-part-3/" rel="alternate" type="text/html" title="Teensy 4.1 USB Serial: What is the maximum throughput over USB serial? - Part 3" /><published>2026-05-21T00:00:00+00:00</published><updated>2026-05-21T00:00:00+00:00</updated><id>https://stpr-dev.github.io/embedded/2026/05/21/teensy-usb-serial-max-speed-part-3</id><content type="html" xml:base="https://stpr-dev.github.io/embedded/2026/05/21/teensy-usb-serial-max-speed-part-3/"><![CDATA[<p>This is part 3 of my series on the Teensy 4.1 USB serial throughput. Read:</p>
<ul>
  <li><a href="https://stpr-dev.github.io/embedded/2026/05/19/teensy-usb-serial-max-speed/">part 1</a> for a bit more background on what I want to achieve.</li>
  <li><a href="https://stpr-dev.github.io/embedded/2026/05/20/teensy-usb-serial-max-speed-part-2/">part 2</a> for benchmark numbers to set some context.</li>
</ul>

<p>In short, I am trying to benchmark the throughput and reliability of the Teensy 4.1 USB serial interface.</p>

<p>I ended part 2 wondering if there is some jank with Windows and USB serial throughput/reliability. I’ve established what I think is an upper limit on Teensy -&gt; PC USB throughput using my Go code. I was still not entirely convinced that the reliability was good, so I decided to do three things today:</p>
<ul>
  <li>Increase the number of frames sent to better assess reliability.</li>
  <li>Run the test harness on Linux to see if there is any differences in throughput/reliability.</li>
  <li>Run the test harnesses on four different computers using both Windows and Linux on each computer to see if the results I was observing last time were fluke or there is actual merit to them.</li>
</ul>

<p>My tests in the last blogs asked Teensy to send 131,072 frames, which for lower frame sizes only lasted single-digit seconds. I decided to 8x that to 1,048,576 frames, which I expected to take longer. This should be stressing the system a bit more to hopefully expose any dropped packets.</p>

<p>For Linux (I know it’s a kernel and not the entire OS, but I am using the colloquial term here), I download the lastest <a href="https://archlinux.org/download/">Arch Linux</a> (btw) ISO (2026-05-01 image) and loaded it onto a USB drive with <a href="https://rufus.ie/">Rufus</a> (v4.14). I also copied over the static Go binary from <a href="https://github.com/stpr-dev/teensy41-serial-companion-go/releases/tag/v0.1.0-alpha">this release</a> to the USB drive. I didn’t bother with the Python version because:</p>
<ul>
  <li>setting up Python for live Linux is not something I am interested in subjecting myself to when the awesome Go binary is available, and</li>
  <li>pre-allocation would not be feasible for these sizes (it will require several gigabytes of RAM for pre-allocation). 
For each computer I tested, I used the default Windows 11 install that was on it, and then once it was done, rebooted to the live USB and ran the test from the terminal.</li>
</ul>

<p>The four computers I used for the test consist of:</p>
<ul>
  <li>Two high-performance desktops: one with AMD 7900X and the other with AMD 9800X3D. One thing to note is that both use different motherboards/chipsets - I wanted to mention this since I know that they are both AM5-based CPUs so they can hypothetically use the same motherboard/chipset. Two different chipsets would at least give some variability to isolate any weird chipset-specific IO quirks.</li>
  <li>Two laptops: one Lenovo Thinkpad and one Lenovo Yoga Book, both using Intel CPUs.</li>
</ul>

<p>This gives me a good variety of relatively high-end desktops to an arguably low-powered Yoga Book, and CPUs/chipsets from both major vendors. I am going to focus on the no-ACK version (why I chose to only focus on this will become clear very soon).</p>

<p>Now let’s take a look at the results. First, let’s look at the 7900X desktop:</p>

<table>
  <thead>
    <tr>
      <th>Frame Size (bytes)</th>
      <th>Throughput Windows (MB/s)</th>
      <th>Throughput Linux (MB/s)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>128</td>
      <td>5.1</td>
      <td>19.3</td>
    </tr>
    <tr>
      <td>256</td>
      <td>10.2</td>
      <td>28.6</td>
    </tr>
    <tr>
      <td>512</td>
      <td>18.3*</td>
      <td>28.8</td>
    </tr>
    <tr>
      <td>1024</td>
      <td>24.6*</td>
      <td>27.9</td>
    </tr>
    <tr>
      <td>2048</td>
      <td>32.7*</td>
      <td>36.9</td>
    </tr>
    <tr>
      <td>4096</td>
      <td>32.7*</td>
      <td>36.9</td>
    </tr>
  </tbody>
</table>

<p>Few things to note here:</p>
<ul>
  <li>I used the same machine and USB ports for this, and the same Go code cross-compiled to both Windows and Linux.
    <ul>
      <li>Now, of course, just because I cross-compiled it doesn’t mean that the low-level behaviour is guaranteed to be the same. The syscalls and other OS primitives around data transfer will be different across OSes. It just means that the overall logic is consistent.</li>
    </ul>
  </li>
  <li>The Linux results are remarkably faster than windows across all frame sizes.</li>
  <li>Unlike Windows, Linux doesn’t appear to heavily penalise smaller frame sizes. On Linux, the min throughput for the smallest was 19.3 MB/s compared to a max of 36.9 MB/s. That is only 50% slower. On Windows, the min is 5.1 MB/s, which is about 6x slower than the max of 32.7 MB/s.</li>
  <li>The peak throughput was also higher for Linux than for Windows, though not substantially (32.7 MB/s vs 36.9 MB/s).</li>
</ul>

<p>Now notice the (*) on Windows’ numbers? That is because I saw failures. That’s right; unlike last time, I saw a failure on frames as small as 512:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sent handshake data:
512 frame size, 1048576 frames, and using ack: false. Current time is 2026-05-21T12:33:09-06:00. Waiting for data...
2026/05/21 12:33:09 Running simple loop-based acquisition.
2026/05/21 12:33:19 [Sample 369082] Failed to read full frame: read returned 0 bytes after 0/512
</code></pre></div></div>

<p>That’s disappointing. It appears to have timed out for the read. You know what is even more surprising? The Linux tests <strong>never</strong> saw any failures, even for the smallest frame sizes. Not once. I ran the tests multiple times and still didn’t see any failures.</p>

<p>OK, this was maybe a fluke. Let’s look at a different machine. Here is the 9800X3D one:</p>

<table>
  <thead>
    <tr>
      <th>Frame Size (bytes)</th>
      <th>Throughput Windows (MB/s)</th>
      <th>Throughput Linux (MB/s)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>128</td>
      <td>2.5</td>
      <td>18.3</td>
    </tr>
    <tr>
      <td>256</td>
      <td>5.1</td>
      <td>27.5</td>
    </tr>
    <tr>
      <td>512</td>
      <td>10.2*</td>
      <td>26.2</td>
    </tr>
    <tr>
      <td>1024</td>
      <td>19.1*</td>
      <td>30.7</td>
    </tr>
    <tr>
      <td>2048</td>
      <td>32.4*</td>
      <td>36.0</td>
    </tr>
    <tr>
      <td>4096</td>
      <td>32.4*</td>
      <td>36.0</td>
    </tr>
  </tbody>
</table>

<p>These results are worse on Windows compared to the last time. The smaller frame sizes are even slower here (2.5MB/s vs 5.1 MB/s on the last PC). On Linux, the pattern is more or less the same as the first machine, and I saw no errors across multiple runs.</p>

<p>I even encountered my first weird error on Windows:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2026/05/21 13:37:10 Retrying read after error: read returned 0 bytes after 13/1024
</code></pre></div></div>

<p>What? <strong>13 bytes</strong>? That’s a really odd number (both linguistically and mathematically). I am used to seeing multiples of 512, or at least powers of 2, which would make sense. 13 is really odd. But let’s keep going.</p>

<p>Next up, the Thinkpad:</p>

<table>
  <thead>
    <tr>
      <th>Frame Size (bytes)</th>
      <th>Throughput Windows (MB/s)</th>
      <th>Throughput Linux (MB/s)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>128</td>
      <td>2.5</td>
      <td>11.3</td>
    </tr>
    <tr>
      <td>256</td>
      <td>5.1</td>
      <td>8.6</td>
    </tr>
    <tr>
      <td>512</td>
      <td>10.2*</td>
      <td>14.7</td>
    </tr>
    <tr>
      <td>1024</td>
      <td>18.9*</td>
      <td>22.2</td>
    </tr>
    <tr>
      <td>2048</td>
      <td>-</td>
      <td>31.2</td>
    </tr>
    <tr>
      <td>4096</td>
      <td>-</td>
      <td>31.2</td>
    </tr>
  </tbody>
</table>

<p>For the Thinkpad, I was unable to get the test to pass at frame sizes larger than 1024 bytes on Windows. Same as before, I saw errors from 512 frame size onwards on Windows, but on reruns, I was able to get past it. On Linux, the tests chugged along with no errors. The Thinkpad was actually approaching desktop-level performance on USB with Linux, something I didn’t see on Windows. I even made sure to turn on “ultra performance mode” on the laptop to disable any energy-saving behaviour on Windows.</p>

<p>Finally, the Yoga Book. This one was more of a curiosity for me as the device is not as high-performance as the other devices, but I wanted to see how it would perform to provide recommendations for the project.</p>

<table>
  <thead>
    <tr>
      <th>Frame Size (bytes)</th>
      <th>Throughput Windows (MB/s)</th>
      <th>Throughput Linux (MB/s)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>128</td>
      <td>-</td>
      <td>13.5</td>
    </tr>
    <tr>
      <td>256</td>
      <td>-</td>
      <td>8.4</td>
    </tr>
    <tr>
      <td>512</td>
      <td>-</td>
      <td>14.3</td>
    </tr>
    <tr>
      <td>1024</td>
      <td>-</td>
      <td>21.0</td>
    </tr>
    <tr>
      <td>2048</td>
      <td>-</td>
      <td>30.8</td>
    </tr>
    <tr>
      <td>4096</td>
      <td>-</td>
      <td>30.8</td>
    </tr>
  </tbody>
</table>

<p>Oooh boy. So <strong>none</strong> of the Windows tests passed. I tried running my PowerShell script multiple times, but it always failed right at the beginning at 128 bytes of frame length. I get two errors on Windows:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2026/05/21 15:25:33 Retrying read after error: read returned 0 bytes after 13/128
</code></pre></div></div>

<p>Once again, our friend 13-byte error. Why 13?!!!</p>

<p>Or the more classic error, which indicates full frames being dropped:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2026/05/21 15:26:34 [Sample 448065] Failed to read full frame: read returned 0 bytes after 0/128
</code></pre></div></div>

<p>Linux once again passed with flying colours, no errors reported and really good throughput.</p>

<h1 id="conclusion">Conclusion</h1>
<p>So what have I learnt so far?</p>

<p>First, let me answer the question that I used to title this miniseries: What is the maximum USB serial throughput for a Teensy 4.1 with a USB? Or more precisely:</p>
<ul>
  <li>If you are continuously dumping data from Teensy with the intent of reading that data from a PC connected to the Teensy, and</li>
  <li>Without sending an ACK back</li>
</ul>

<p>what is the throughput you expect to see? Based on measurements across four machines, it is somewhere between <strong>30MB/s - 36MB/s</strong>. Some nuance to this figure, as we’ve seen in these posts, mainly around <em>reliability</em>.</p>

<p>The number above is assuming <strong>ideal conditions</strong> where you don’t lose any data. I’ve learnt that Windows is absolutely terrible in this regard. I was able to get significantly better performance on every machine with Linux. Heck, laptops running Linux could achieve almost desktop-like throughputs. You could absolutely critique my method of using an Arch Live USB commandline-only environment vs a full desktop Windows, which I grant is absolutely fair. For these tests, the machines I was working with were work machines, so I couldn’t install Linux with a full Desktop Environment like KDE. At some point, perhaps I will try the tests out on a machine that I own with and without a DE running to see how much impact that has. I suspect that I will be seeing little of a difference in the results. So <strong>if you want to achieve max performance and no dropped data, Linux appears to be mandatory</strong>.</p>

<p>For Windows, I suspect that we will end up needing to design a protocol at the application layer to detect data corruption and implement retransmission logic. It is an open question what this looks like or how best to implement it. There are also alternative avenues to explore here: custom USB drivers, Ethernet adapters, using <em>actual</em> serial instead of USB serial, etc. I will keep them in mind as I settle on a solution that works for the project I am working on.</p>

<p>The 13-byte partial reads on Windows are interesting – I hypothesise that the Windows USB CDC drivers are terminating the reads early. I will put this on my list of things to investigate further.</p>

<p>Overall, I am happy with this investigation. The next step for me is to evaluate other parts of this acquisition system, including DMA and SD writes. These will be part of my upcoming posts!</p>

<h1 id="footnotes">Footnotes</h1>
<ul>
  <li>Side rant: I am always annoyed by secure boot and having to disable it to boot from the live USB. I wish it were plug-and-play, but this isn’t Arch Linux’s fault – it is just the way secure boot works.</li>
</ul>]]></content><author><name>Shri</name></author><category term="embedded" /><summary type="html"><![CDATA[This is part 3 of my series on the Teensy 4.1 USB serial throughput. Read: part 1 for a bit more background on what I want to achieve. part 2 for benchmark numbers to set some context.]]></summary></entry><entry><title type="html">Teensy 4.1 USB Serial: What is the maximum throughput over USB serial? - Part 2</title><link href="https://stpr-dev.github.io/embedded/2026/05/20/teensy-usb-serial-max-speed-part-2/" rel="alternate" type="text/html" title="Teensy 4.1 USB Serial: What is the maximum throughput over USB serial? - Part 2" /><published>2026-05-20T00:00:00+00:00</published><updated>2026-05-20T00:00:00+00:00</updated><id>https://stpr-dev.github.io/embedded/2026/05/20/teensy-usb-serial-max-speed-part-2</id><content type="html" xml:base="https://stpr-dev.github.io/embedded/2026/05/20/teensy-usb-serial-max-speed-part-2/"><![CDATA[<p>This is part 2 of my series on the Teensy 4.1 USB serial throughput. Read <a href="https://stpr-dev.github.io/embedded/2026/05/19/teensy-usb-serial-max-speed/">part 1</a> for a bit more background on what I want to achieve. In short, I am trying to benchmark the throughput and reliability of the Teensy 4.1 USB serial interface.</p>

<p>After my recent success, I decided to run a few more frame sizes to see if throughput still goes up, or if we’ve peaked. I reran the optimised Python code (the one which precomputes the RNG values). The results are as follows:</p>

<table>
  <thead>
    <tr>
      <th>Frame Size (bytes)</th>
      <th>Total Time No-ACK (s)</th>
      <th>Total Time ACK (s)</th>
      <th>Throughput No-ACK (MB/s)</th>
      <th>Throughput ACK (MB/s)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>128</td>
      <td>2.9</td>
      <td>13.5</td>
      <td>5.6</td>
      <td>1.2</td>
    </tr>
    <tr>
      <td>256</td>
      <td>2.9</td>
      <td>13.6</td>
      <td>11.2</td>
      <td>2.4</td>
    </tr>
    <tr>
      <td>512</td>
      <td>3.5</td>
      <td>13.9</td>
      <td>18.6</td>
      <td>4.8</td>
    </tr>
    <tr>
      <td>1024</td>
      <td>4.8</td>
      <td>14.4</td>
      <td>27.4</td>
      <td>9.2</td>
    </tr>
    <tr>
      <td>2048*</td>
      <td>8.3</td>
      <td>16.0</td>
      <td>32.3</td>
      <td>16.6</td>
    </tr>
    <tr>
      <td>4096*</td>
      <td>16.6</td>
      <td>30.4</td>
      <td>32.3</td>
      <td>17.6</td>
    </tr>
    <tr>
      <td>8192*</td>
      <td>32.0</td>
      <td>64.0</td>
      <td>32.2</td>
      <td>22.2</td>
    </tr>
  </tbody>
</table>

<p>A few points to note:</p>
<ul>
  <li>I added (*) to indicate that these runs are flaky. They seem susceptible to frame loss, especially more so with the no-ACK mode. It got worse around 4096, and I ran it and higher settings multiple times to get a single successful run.</li>
  <li>The peak throughput for no-ACK mode is around 32.3 MB/s around 2048 bytes per frame. For ACK mode, the throughput is still improving with increasing frame sizes, though this is entirely unsurprising.
    <ul>
      <li>If we consider a big frame, like 4096, we can consider that to be 8x512 byte mini-frames. We are essentially getting to a point where we are sending ACK every nth mini-frame, so we are sort of amortising the cost of sending ACK. At the limit where the frame size is extremely large, it is essentially no-ACK in disguise.</li>
    </ul>
  </li>
</ul>

<p>Now I recognise that my desktop is a bit on the higher-end in terms of specs. I wanted to ensure that this will also apply to slower systems. I found a Thinkpad P16 Gen 1 system and decided to run the tests there.</p>

<table>
  <thead>
    <tr>
      <th>Frame Size (bytes)</th>
      <th>Total Time No-ACK (s)</th>
      <th>Total Time ACK (s)</th>
      <th>Throughput No-ACK (MB/s)</th>
      <th>Throughput ACK (MB/s)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>128</td>
      <td>6.6</td>
      <td>16.0</td>
      <td>2.5</td>
      <td>1.0</td>
    </tr>
    <tr>
      <td>256</td>
      <td>6.7</td>
      <td>16.8</td>
      <td>4.9</td>
      <td>2.0</td>
    </tr>
    <tr>
      <td>512</td>
      <td>6.7</td>
      <td>17.2</td>
      <td>9.9</td>
      <td>3.9</td>
    </tr>
    <tr>
      <td>1024*</td>
      <td>6.9</td>
      <td>20.5</td>
      <td>19.3</td>
      <td>6.6</td>
    </tr>
    <tr>
      <td>2048*</td>
      <td>14.1</td>
      <td>25.4</td>
      <td>19.0</td>
      <td>10.5</td>
    </tr>
  </tbody>
</table>

<p>With the Thinkpad, the peak throughput at a given frame size is significantly lower than on my desktop. Nevertheless, it is still quite respectable. The interesting thing I noted here is that up to 1024 bytes, the total time stays quite constant. This seems to indicate that the overhead for syscall is much higher here than on the desktop case. I did ensure that it was in ultra-performance mode. Another thing to note is that the failures are much more frequent in no-ACK mode for sizes &gt;= 1024 bytes. I had to rerun 2048 bytes at least 10 times to get one successful run, which was quite a lot. The higher sizes - forget about it, I tried my best, but I just couldn’t get the no-ACK mode to work. Perhaps the ACK mode would work, but I think I got my answer with this experiment, so decided to skip it.</p>

<p>The conclusion so far is that the frame size and throughput depend on the system being used. On my desktop, it appears that I can comfortably set 1024 and be fine most of the time. On the Thinkpad, 1024 seems a bit questionable. In either case, some sort of ACK mechanism could significantly improve reliability at the cost of throughput.</p>

<p>So now that I know generally what performance to expect, there are still two questions in my mind:</p>
<ol>
  <li>How much performance is left on the table from Python being slow?</li>
  <li>Does Windows itself set an upper limit on USB serial throughput or reliability?</li>
</ol>

<p>While 1. is more important for the project, 2. is also personally interesting to me as it gives me an excuse to restart my Linux adventures. But that will have to wait for a bit. In the last post, I mentioned using Go instead of Python. In the past couple of weeks, I had been working on the Go code so now I can share some results!</p>

<h1 id="go-rewrite">Go Rewrite</h1>
<p>The Go code is located <a href="https://github.com/stpr-dev/teensy41-serial-companion-go">in this repository</a>. It is more or less identical to the Python code behaviour from my earlier blog.</p>

<p>Since we are here, I wanted to note my development experience with Go as someone who mostly used Python so far. Now full disclaimer, I used Claude a good amount to help me learn the ropes quickly. Nevertheless, I learnt quite a lot of things:</p>
<ul>
  <li>Go dependency management is <em>such a great experience</em> compared to Python. Things seem to just work. It’s easy to point to a GH repo and ask Go to take care of things for you.</li>
  <li>It was surprisingly easy to pick up Go. There is little in terms of “fanciness” to the language. I know it’s a deliberate design choice by the Go team, and it has people both for and against it. However, personally for me, I found it to be a huge positive – at no point did I feel intimidated by either the build system or the syntax. I could instead focus on the implementation details.
    <ul>
      <li>Now of course, I do see what people are saying about the whole <code class="language-plaintext highlighter-rouge">if (err != nil) {}</code> soup. It’s a bit verbose, for sure, but it’s not something that’s a dealbreaker for me. I can live with it.</li>
    </ul>
  </li>
  <li>stdlib features seem to rival Python’s. I see <code class="language-plaintext highlighter-rouge">flags</code> which is the <code class="language-plaintext highlighter-rouge">argparse</code> equivalent, which immediately made it useful for building this tool. Of course, it also has basic stuff like TCP/UDP functionality, but I also got a taste of the HTTP handlers, which were really nice. I didn’t use them for this project but have ideas for them. JSON serialization was really interesting and nice.</li>
  <li>Type system is really nice to have – less footguns in general.</li>
  <li>Single static <code class="language-plaintext highlighter-rouge">.exe</code> that you can ship to someone and ask them to double-click to run? FINALLY! It is such a relief to not have to ask someone for which version of Python they have installed, and the source (official Python vs conda vs WinPython). Or tell them how to create venv or use <code class="language-plaintext highlighter-rouge">uv</code> and please not mix dependencies across projects. Or…. you get the idea.
    <ul>
      <li>I know <code class="language-plaintext highlighter-rouge">pyinstaller</code> exists but it’s not always great. Static binaries are just sooooo good.</li>
      <li>I also got a taste for the build flags, which are really useful for controlling compilation.</li>
    </ul>
  </li>
  <li>Of course, I cannot gloss over Go’s most notable feature – concurrency and supporting channels. Coming from Python land, just the thought of <strong>fast</strong> concurrency makes me think, “Oh good, lots of careful design required, probably might involve shared memory and mutexes.” I can only describe Go’s concurrency model as - a revelation. Just <code class="language-plaintext highlighter-rouge">go myfunc()</code>? And <em>directional</em> channels that are really fast and I don’t need to be clever about doing mutexes unless I need to? OH. MY. GOD. Why doesn’t every language have this???!
    <ul>
      <li>Python does have <a href="https://docs.python.org/3/library/multiprocessing.html#managers">Managers</a> but my experience with them is that they aren’t particularly useful for real-time systems. The abstraction overhead is too high (in terms of latency), and I found that just using numpy with SharedMemory and mutexes is much faster.</li>
    </ul>
  </li>
</ul>

<p>Ok, enough with my rants, let’s take a look at the results when running the Go binary:</p>

<table>
  <thead>
    <tr>
      <th>Frame Size (bytes)</th>
      <th>Total Time No-ACK (s)</th>
      <th>Total Time ACK (s)</th>
      <th>Throughput No-ACK (MB/s)</th>
      <th>Throughput ACK (MB/s)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>128</td>
      <td>2.8</td>
      <td>13.5</td>
      <td>5.8</td>
      <td>1.2</td>
    </tr>
    <tr>
      <td>256</td>
      <td>3.3</td>
      <td>13.6</td>
      <td>10.0</td>
      <td>2.4</td>
    </tr>
    <tr>
      <td>512</td>
      <td>3.7</td>
      <td>13.9</td>
      <td>18.0</td>
      <td>4.8</td>
    </tr>
    <tr>
      <td>1024</td>
      <td>5.5</td>
      <td>14.4</td>
      <td>24.5</td>
      <td>9.3</td>
    </tr>
    <tr>
      <td>2048</td>
      <td>8.3</td>
      <td>15.5</td>
      <td>32.3</td>
      <td>17.2</td>
    </tr>
    <tr>
      <td>4096</td>
      <td>16.6</td>
      <td>30.2</td>
      <td>32.3</td>
      <td>17.7</td>
    </tr>
  </tbody>
</table>

<p>This more or less matches the optimised Python results. Something to note here is that I didn’t need to precompute the RNG values like I had to do with Python to achieve these results. I also didn’t note as many failed passes with the Go binary as I did with Python, though I feel like I need to do a lot more testing to quantify if this was a fluke. However, the processing headroom is definitely noticeable, and I would be far more comfortable using Go rather than Python for this case.</p>

<p>So this tells me that faster language is beneficial – you have headroom to do stuff like error-checking. But the throughput didn’t significantly improve, so we are reaching some limits here. Is it Windows or is it the hardware? Given Paul’s (Paul Stoffregen, Teensy’s creator) numbers from the last blog post, I feel like we are hitting USB serial limits, though I have to question if the reliability issue is more a Windows thing rather than a USB serial thing. Claude did <a href="https://kb.segger.com/CDC">find this web page</a>, which states:</p>
<blockquote>
  <p>Windows 10 comes with a re-designed driver for CDC-ACM. At the time of writing (June 2019) Windows 10 has an issue with large IN CDC transfers. Sometimes packets seems to disappear inside the Windows 10 USB stack.</p>
</blockquote>

<p>Hmm… interesting. This sounds very much like what I am observing. Now I am even more curious to do testing on Linux to validate this theory.</p>

<p>That’s the next experiment. Time to grab my live USB!</p>]]></content><author><name>Shri</name></author><category term="embedded" /><summary type="html"><![CDATA[This is part 2 of my series on the Teensy 4.1 USB serial throughput. Read part 1 for a bit more background on what I want to achieve. In short, I am trying to benchmark the throughput and reliability of the Teensy 4.1 USB serial interface.]]></summary></entry><entry><title type="html">Teensy 4.1 USB Serial: What is the maximum throughput over USB serial?</title><link href="https://stpr-dev.github.io/embedded/2026/05/19/teensy-usb-serial-max-speed/" rel="alternate" type="text/html" title="Teensy 4.1 USB Serial: What is the maximum throughput over USB serial?" /><published>2026-05-19T00:00:00+00:00</published><updated>2026-05-19T00:00:00+00:00</updated><id>https://stpr-dev.github.io/embedded/2026/05/19/teensy-usb-serial-max-speed</id><content type="html" xml:base="https://stpr-dev.github.io/embedded/2026/05/19/teensy-usb-serial-max-speed/"><![CDATA[<p>As part of a research project, I’m in charge of developing software/firmware for a real-time data acquisition system using <a href="https://www.pjrc.com/store/teensy41.html">Teensy 4.1</a>. This is the first time I got to work with a Teensy, and I have to say, I am pretty impressed with the capabilities of the tiny, yet fierce, device. I am also really glad that Teensyduino supports C++17 so I can work on a slightly more familiar (and sometimes better IMO) C++ standards rather than plain C code (though sometimes looking at the maze of weird C++ quirks makes me appreciate C a lot).</p>

<p>Anyway, the data acquisition system is interesting in that it has to sample data at several hundred kilohertz, much higher than we typically handle in most of our systems. This comes with its own set of challenges, and I’ll detail a few of those across a few posts on this blog. The core part of the data acquisition system waits for an external trigger (a button press, for instance), and when the trigger condition is satisfied, the system samples data for T seconds at the given sampling rate (&gt;=100 kHz). Per trigger, we can expect anywhere between 1x10^(6) to 10x10^(6) samples (the exact numbers depend on the settings and are being finalised, but this is a good ballpark for me to target). So the workload is bursty on a <em>session</em> level (since it’s mostly idle between triggers), but sustained on a <em>trigger</em> level when we’re actively acquiring data. So the USB link needs to handle both: long quiet periods and then sudden sustained demand at full rate. Of course, this isn’t <em>entirely</em> accurate, as there are things like DMA that come into the picture, but we can ignore that for now as it doesn’t meaningfully change the math at the moment.</p>

<p>For this post, I want to focus specifically on the maximum throughput that one can 
achieve <strong>reliably</strong> using an out-of-the-box Teensy 4.1 and serial port communication. For now, I am discounting any custom USB stuff and focusing on getting data out of the default COM port. This is important for me to know so that I can ensure that the data is drained from the Teensy quickly, preventing any data loss due to buffer overruns. I can use this to establish guidelines like max sample rate, max duration, analogue-to-digital converter resolution, etc. I can also provide a basic test harness for the project so that people can test reliability of the transfer.</p>

<p>Seems like a simple answer, right? After all, Teensy 4.1 <a href="https://www.pjrc.com/store/teensy41.html#specs">advertises 480 Mbits/s connection</a>, and <a href="https://forum.pjrc.com/index.php?threads/recommended-baud-rate-on-teensy-4-1.69839/post-302810">Paul and other well-known contributors</a> (Paul Stoffregen, Teensy’s creator) occasionally have explicitly mentioned the same fact:</p>
<blockquote>
  <p>Teensy transmits at USB Speed. The baud rate is ignored. And USB Serial on t4.x is very fast (T3.x connects a USB full speed 12MBS, where the T4.x connects at USB high-speed 480mbs)</p>
</blockquote>

<p>So the baud rate is ignored (good to know that you can put any number in there, and it doesn’t matter), and Teensy 4.1 always connects at 480 Mbits/s. So 480 Mbits /8 = ~60 MB/s, this is what we should be able to achieve (or at least, somewhere in the ballpark) for sustained transfers from Teensy to PC right?</p>

<p>Well, the answer is surprisingly not so clear. In fact, there’s very little information on what’s <strong>practically</strong> achievable. To be clear, I fully expected going into this that I won’t achieve the maximum speed of 60 MB/s since this is how usually marketing works. The numbers cited are typically <em>raw</em> throughput values (which is <a href="https://www.youtube.com/watch?v=aIzMuPMicGc">technically correct</a>) and don’t take into account protocol/OS overheads, so what we <em>actually</em> see will be lower than what the marketing numbers are. And of course, it also depends on system load, USB controller quirks, etc. Still, there has to be some value that I should be able to target, give or take. What’s that value?</p>

<p>Two threads I found particularly relevant here are:</p>
<ol>
  <li><a href="https://forum.pjrc.com/index.php?threads/teensy-4-1-coding-guidance-for-usb-serial-communication.76589/">This one that explicitly shows some benchmark numbers</a>
    <blockquote>
      <p>The Teensy 4.1 supports USB 2.0 High Speed (480 Mbps = 60 MB/s), but I never reach anzywhere close to that.</p>

      <p>My best observed speeds:
PC → Teensy: 7.38 MB/s (using PySerial)
Teensy → PC: 20.97 MB/s (without send_now())</p>
    </blockquote>
  </li>
  <li><a href="https://forum.pjrc.com/index.php?threads/can-teensy-4-1-bit-stream-at-480mbps-with-usb-2.74782/post-341468">And this comment from Paul</a>
    <blockquote>
      <p>The maximum theoretical USB 480 Mbit / second speed with protocol overhead is 53,248,000 bytes/second. See page 55 (83rd page in the PDF) of the USB 2.0 spec for details. But that doesn’t include data-dependent bitstuffing overhead or trade-offs all USB host controllers make for bandwidth planning to ensure SOF packets transmit precisely on schedule. So in practice you’ll see even the best USB hardware achieve only some fraction of this theoretical maximum. We’ve often seen about 50% with this benchmark which includes binary to ascii conversion overhead, though results vary quite substantially depending on which software on the PC side receives the data.</p>
    </blockquote>
  </li>
</ol>

<p>(Also, shoutout to this <a href="https://forum.pjrc.com/index.php?threads/high-speed-usb-data-custom-isochronous-usb-descriptor.64481/">interesting thread</a> that seems very similar to what I am doing. Some interesting ideas for custom USB descriptors and settings, but that is something I will look at perhaps in the future).</p>

<p>OK, so these two do have some numbers to go off of: ~20 MB/s from the first one and ~25 MB/s (from the 50% number Paul mentioned if we take his throughput with overhead included) from the second one (both are surprisingly lower than I expected, which I found interesting, but they still satisfy the project requirements). The first one is that one I’ll start with since the stack that I inherited uses <code class="language-plaintext highlighter-rouge">PySerial</code>. So let’s see if I can replicate these numbers.</p>

<h1 id="testing">Testing</h1>
<p>To test this out, I started sketching out a simple test harness that mirrors the project’s expected workflow. I iterated over the test harness design for a few days and came up with a simple, but effective, scheme. The full harness is in <a href="https://github.com/stpr-dev/teensy41-serial-test-firmware">this repository</a>. It is relatively simple — the Teensy waits for a handshake from the PC (more specifically, a Python script). The handshake is expected to be nine bytes, with the following format:</p>
<ul>
  <li>Bytes 0–3: LE 32-bit integer frame size (M)</li>
  <li>Bytes 4–7: LE 32-bit integer number of frames to send (N)</li>
  <li>Byte 8: Whether to wait for ACK from host per frame sent before sending the next one (0 for no, non-zero for yes)</li>
</ul>

<p>This handshake itself serves as our trigger. This doesn’t include the notion of sampling rate since I was hoping to actually measure the upper bound of the max sampling rate that can be achieved. Wait, how would that work? Well, since we’re measuring the USB transfer rate, it also indirectly tells us the maximum sampling rate that can be used reliably. If I can send N frames of size M bytes each for a total of T seconds, then the max sampling rate is: (M x N) / T Hz. This of course assumes <code class="language-plaintext highlighter-rouge">uint8_t</code> samples, adjust accordingly for different data widths.</p>

<p>Now on the <em>data</em> side, I needed a datastream that’ll allow me to test that the data I’m receiving is correct: on a byte level per frame, as well as ordering and validity across frames. Now I could have done a repeated pattern of bytes, but that’d be boring. Instead, I decided to do something fun and explore pseudorandom data. I found <a href="https://www.pcg-random.org/posts/some-prng-implementations.html">this website</a> which has a lot of information on PRNGs in general, but I decided to go with what the website called <a href="https://burtleburtle.net/bob/rand/smallprng.html">“JSF: Bob Jenkins’s Small/Fast Chaotic PRNG”</a>. It seemed simple enough to implement in any language as it mostly appears to contain few bitshift and logical operations, which should also be fast on both the PC and Teensy. I also took the opportunity to try out a couple of things I’ve been wanting to do:</p>
<ul>
  <li>Setup <a href="https://docs.platformio.org/en/latest/platforms/teensy.html">Platform IO for Teensy</a>  for <a href="https://www.jetbrains.com/clion/">CLion</a>. I am a massive fan of JetBrains IDEs (PyCharm/CLion/Rider, et al.) and have been using them for a while now.</li>
  <li>Use Claude Code for the first time to do some basic automated coding for simple tasks.</li>
  <li>Fully integrate <a href="https://docs.astral.sh/uv/">uv</a> on the Python side for package management and dependency resolution. Big fan of <code class="language-plaintext highlighter-rouge">uv</code>, it really solved a major annoyance I have with <code class="language-plaintext highlighter-rouge">pip</code> and <code class="language-plaintext highlighter-rouge">requirements.txt</code> files. Most of the stuff I did with it were on existing projects, so I figured it would be a good time to try it out on a new project.</li>
</ul>

<p>I asked CC to implement <code class="language-plaintext highlighter-rouge">JSF32</code> in C++ and Python. I tested both of them quickly to ensure that they provided the same outputs. I included the C++ version with the Teensy firmware. As long as I use the same seed on the client side, it should generate the same outputs, and I can use this to validate samples I am receiving from Teensy on-the-fly.</p>

<p>After receiving the handshake, the Teensy now sends a sequence of frames, each containing pseudorandom data generated by the JSF32 PRNG. Each frame (expected to be at least four bytes and the frame size is expected to be a multiple of 4) contains consecutive 32-bit integers (LE) generated by the JSF32 PRNG. Fairly straightforward.</p>

<p>Now that the firmware side is done, I implemented a simple Python client that can send the handshake and receive the data. The client is responsible for a) validating the received data against the pseudorandom sequence generated by the JSF32 PRNG, ensuring data integrity, and b) timing the process to calculate throughput. The Python script is in <a href="https://github.com/stpr-dev/teensy41-serial-companion-python">this repository</a>.</p>

<h1 id="first-pass">First Pass</h1>
<p>I used the basic example for this. No real optimisations were done as I felt that the code was relatively straightforward (spoiler alert: probably not).</p>

<h2 id="results">Results</h2>
<p>I did the testing on my desktop with an AMD 7900X PC with 64GB RAM and the USB3 port. The PC should be plenty fast. I initially set a frame size of 2048 bytes (value picked from <a href="https://forum.pjrc.com/index.php?threads/increase-the-usb-buffer-size-in-teensy-4-1.73656/post-332404">this post</a> and asked for 2^(17) = 131,072 frames to be transmitted.</p>

<p>The results were … <em>drumroll</em>… erm, underwhelming? Weird? My script indicates that it fails validation:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run .\pyserial_example.py --port "COM3" --frame-size 2048 --num-frames 131072
Port COM3 opened.
Sent handshake data:
2048 frame size, 131072 frames, and using ack: false ...
...
Traceback (most recent call last):
  ...
ValueError: [Frame (8), Value (0)] Write failed: Received 1663108126 value instead of expected 261443033.
</code></pre></div></div>

<p>One thing I noticed that was it didn’t fail right away. That is a good indication that it wasn’t likely that the random numbers are wrong, but more likely that data is being corrupted during transmission.</p>

<p>I ran it again, and it failed again, though at a different spot:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ValueError: [Frame (119034), Value (0)] Write failed: Received 2472662199 value instead of expected 195615389
</code></pre></div></div>

<p>Now I suspect one of two things: 
a) Python is too slow so there is a buffer overrun, or
b) The cable or transmission is somehow faulty</p>

<p>I suspect a) is likely true. One easy fix is to turn on ACK per frame. I fully expect it to kill throughput, <a href="https://forum.pjrc.com/index.php?threads/transfer-to-pc.73502/post-331440">as Paul mentioned</a>, but I just wanted to validate that there was no other issue to blame.</p>

<p>Aha! So this passed:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
Min delays: 448300.00 ns
Max delays: 1294100.00 ns
Average delays: 522467.98 ns
</code></pre></div></div>
<p>It is indeed pointing to a buffer overrun. I ran this multiple times, and it passed every time.</p>

<h2 id="reruns">Reruns</h2>
<p>The ACK method indeed felt slow, it took a long time. But it bugged me that the no-ACK version didn’t pass. So I backed out a little and decided that I would try different frame sizes. Maybe it could reveal something?</p>

<p>I wrote a PowerShell script to automate it. Surprisingly, a few of them ran with no issues. The results were:</p>

<table>
  <thead>
    <tr>
      <th>Frame Size (bytes)</th>
      <th>Total Time No-ACK (s)</th>
      <th>Total Time ACK (s)</th>
      <th>Throughput No-ACK (MB/s)</th>
      <th>Throughput ACK (MB/s)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>128</td>
      <td>4.4</td>
      <td>13.9</td>
      <td>3.7</td>
      <td>1.2</td>
    </tr>
    <tr>
      <td>256</td>
      <td>7.2</td>
      <td>15.5</td>
      <td>4.6</td>
      <td>2.1</td>
    </tr>
    <tr>
      <td>512</td>
      <td>13.0</td>
      <td>28.4</td>
      <td>5.1</td>
      <td>2.3</td>
    </tr>
    <tr>
      <td>1024</td>
      <td>24.8</td>
      <td>40.9</td>
      <td>5.4</td>
      <td>3.2</td>
    </tr>
    <tr>
      <td>2048</td>
      <td>—</td>
      <td>68.4</td>
      <td>—</td>
      <td>3.9</td>
    </tr>
  </tbody>
</table>

<p>Oh. <strong>Very interesting</strong>. The no-ACK version does indeed pass, and it seems to consistently pass at <em>smaller</em> frame sizes. This was very surprising to me: I thought that lower frame sizes would be worse due to syscall overhead. Seems like I was wrong here. However, two things stood out to me:</p>
<ol>
  <li>The ACK version’s throughput is half as fast as the no-ACK version.</li>
  <li>Even the no-ACK version’s throughput is substantially lower than what I was targetting.</li>
</ol>

<p>Now technically, per project requirements, 5MB/s would work, so the 512 no-ACK version is sufficient for our needs. However, I was still not entirely happy with the throughput of the no-ACK version, as it was significantly lower than the target. I decided to investigate further to see if there were any optimisations that could be made to improve the performance.</p>

<h1 id="second-pass">Second Pass</h1>
<p>When rerunning the tests, I noticed something interesting. I used the LED indicator on the Teensy to tell me when it started/stopped sending the data. While this was happening, I was observing the console output and noted that the Teensy finished significantly before the console output stopped when using the no-ACK version. This got worse with increasing frame sizes, suggesting that the Teensy was able to send the data faster than Python could keep up with.</p>

<p>So I started digging through the Python code. The only bottleneck I could think of was the RNG. Though it was extremely simple, I decided to benchmark it. Here’s what I found:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Average time per sample: 664.8639 ns
</code></pre></div></div>

<p>Doesn’t seem like a lot, but if we were to multiply that by 512, we are looking at 338 microseconds per frame. This doesn’t sound like much but it is in the same order of magnitude as the processing time per frame. So this could potentially be a bottleneck.</p>

<p>I copied the script and made one modification: I pre-generated the expected RNG values I was expecting ahead of time and stored them in an array. I then reran the tests. Now while running it, I did notice that it took a significant amount of time to generate the values. This is an indication that the RNG is indeed the bottleneck.</p>

<p>Regardless, here are the results of the tests with pre-generated values:</p>

<table>
  <thead>
    <tr>
      <th>Frame Size (bytes)</th>
      <th>Total Time No-ACK (s)</th>
      <th>Total Time ACK (s)</th>
      <th>Throughput No-ACK (MB/s)</th>
      <th>Throughput ACK (MB/s)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>128</td>
      <td>2.9</td>
      <td>13.5</td>
      <td>5.6</td>
      <td>1.2</td>
    </tr>
    <tr>
      <td>256</td>
      <td>2.9</td>
      <td>13.6</td>
      <td>11.2</td>
      <td>2.4</td>
    </tr>
    <tr>
      <td>512</td>
      <td>3.5</td>
      <td>13.9</td>
      <td>18.6</td>
      <td>4.8</td>
    </tr>
    <tr>
      <td>1024</td>
      <td>4.8</td>
      <td>14.4</td>
      <td>27.4</td>
      <td>9.2</td>
    </tr>
    <tr>
      <td>2048</td>
      <td>8.3</td>
      <td>16.0</td>
      <td>32.3</td>
      <td>16.6</td>
    </tr>
  </tbody>
</table>

<p>And there it is! A few interesting points to note:</p>
<ul>
  <li>With frame sizes &gt;=1024 and no-ACK, we were able to achieve &gt;25MB/s. 2048 seems to have the max throughput, but the tests were flaky – sometimes they failed. Of the 10 runs I ran, around 30% randomly failed. Not entirely happy with the results, but it’s a start.</li>
  <li>Sizes &lt;=512 seem to take about the same time, indicating potential syscall overhead.</li>
  <li>The ACK version still has quite poor throughput, anywhere from 3-5x worse.</li>
</ul>

<h1 id="conclusion">Conclusion</h1>
<p>Technically speaking, it is indeed possible to achieve around 30MB/s with no-ACK and frame sizes &gt;=1024. At least on my PC. On a laptop or slower systems, it will likely be lower. I confirmed this myself by running this on a slower laptop, and I found lower throughput values.</p>

<p>However, there is a glaring flaw here: while I could pre-compute the values for this case, in real production cases, this isn’t possible. Given the flakiness of the protocol, I think that some minimal framing is necessary to ensure reliable communication. At minimum, something like sequence number and CRC of the frame should be included to ensure data integrity and reliability. This poses a problem with Python: if JSF is too slow, CRC or other error detection methods will almost definitely be much slower. Additionally, we didn’t even do any of the other stuff that comes with the acquisition: UI, API calling, IO, etc. All of these are things you would see in the full application.</p>

<p>The results don’t fill me with confidence that the Python approach will work in a real-world setting. Not in the very least without significant architecting around using multiprocessing to decouple the reading loop from the processing loop. And boy,isn’t multiprocessing IPC fun to work with in Python (being sarcastic BTW).</p>

<p>This is one of those cases where I think that a faster language would be the better solution. I’ve been meaning to learn and use Go for something real-world. Guess this is my calling? Time to learn Go!</p>]]></content><author><name>Shri</name></author><category term="embedded" /><summary type="html"><![CDATA[As part of a research project, I’m in charge of developing software/firmware for a real-time data acquisition system using Teensy 4.1. This is the first time I got to work with a Teensy, and I have to say, I am pretty impressed with the capabilities of the tiny, yet fierce, device. I am also really glad that Teensyduino supports C++17 so I can work on a slightly more familiar (and sometimes better IMO) C++ standards rather than plain C code (though sometimes looking at the maze of weird C++ quirks makes me appreciate C a lot).]]></summary></entry></feed>