<?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://tomverbeure.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://tomverbeure.github.io/" rel="alternate" type="text/html" /><updated>2026-03-29T05:17:07+00:00</updated><id>https://tomverbeure.github.io/feed.xml</id><title type="html">Electronics etc…</title><subtitle></subtitle><entry><title type="html">Repair of 2 Agilent 54831 Oscilloscopes</title><link href="https://tomverbeure.github.io/2026/03/28/Repair-of-Two-Agilent-54831-Oscilloscopes.html" rel="alternate" type="text/html" title="Repair of 2 Agilent 54831 Oscilloscopes" /><published>2026-03-28T10:00:00+00:00</published><updated>2026-03-28T10:00:00+00:00</updated><id>https://tomverbeure.github.io/2026/03/28/Repair-of-Two-Agilent-54831-Oscilloscopes</id><content type="html" xml:base="https://tomverbeure.github.io/2026/03/28/Repair-of-Two-Agilent-54831-Oscilloscopes.html"><![CDATA[<p><em>In the end, it comes down to fixing two early 2000s PCs…</em></p>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#the-agilent-54831" id="markdown-toc-the-agilent-54831">The Agilent 54831</a></li>
  <li><a href="#inside-the-pc-system-of-the-54831" id="markdown-toc-inside-the-pc-system-of-the-54831">Inside the PC System of the 54831</a></li>
  <li><a href="#unit-a-agilent-54831m" id="markdown-toc-unit-a-agilent-54831m">Unit A: Agilent 54831M</a></li>
  <li><a href="#first-suspect-the-ibm-travelstar-hd" id="markdown-toc-first-suspect-the-ibm-travelstar-hd">First Suspect: the IBM TravelStar HD</a></li>
  <li><a href="#getting-the-pc-to-boot" id="markdown-toc-getting-the-pc-to-boot">Getting the PC to Boot</a></li>
  <li><a href="#unit-b-agilent-54831b" id="markdown-toc-unit-b-agilent-54831b">Unit B: Agilent 54831B</a></li>
  <li><a href="#a-compactflash-adapter-without-cable" id="markdown-toc-a-compactflash-adapter-without-cable">A CompactFlash Adapter without Cable</a></li>
  <li><a href="#installing-the-software" id="markdown-toc-installing-the-software">Installing the Software</a></li>
  <li><a href="#cpu-temperature-alarm" id="markdown-toc-cpu-temperature-alarm">CPU Temperature Alarm</a></li>
  <li><a href="#upgrade-to-1-ghz-bandwidth" id="markdown-toc-upgrade-to-1-ghz-bandwidth">Upgrade to 1 GHz Bandwidth</a></li>
  <li><a href="#additional-changes-are-possible" id="markdown-toc-additional-changes-are-possible">Additional Changes are Possible</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
  <li><a href="#footnotes" id="markdown-toc-footnotes">Footnotes</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>After 6 months of deprivation, a new season of 
<a href="https://www.electronicsfleamarket.com/">Silicon Valley Electronics Flea markets</a>
is upon us! I didn’t make it there at the 6am starting time, even getting there at 6:45am was 
a struggle, but not a moment too soon because one vendor was selling not one but two broken 
Agilent<sup id="fnref:Agilent" role="doc-noteref"><a href="#fn:Agilent" class="footnote" rel="footnote">1</a></sup> 54831 oscilloscopes, $200 for both of them. While I considered the marital 
implications of having to defend 2 additional boat anchors in my garage, others were lining 
up after me, so I made the courageous decision to take the deal.</p>

<p><img src="/assets/hp54831/scopes_in_car.jpg" alt="Oscilloscopes in the trunk of a car" /></p>

<h1 id="the-agilent-54831">The Agilent 54831</h1>

<p>The specs of the 54831 are still pretty decent by today’s hobbyist standards:</p>

<ul>
  <li>4 channels</li>
  <li>600 MHz BW</li>
  <li>4 Gsps</li>
</ul>

<p>There are some limitations: channels 1 and 2 and channels 3 and 4 share the same
AD converter. To reach 4 Gsps, you can use only either channel 1 or channel 2 but not
both, and either channel 3 or channel 4 but not both, otherwise the sample rate
drops to 2 Gsps.</p>

<p>The 54832 is the slightly more potent sibling of the 54831 with an analog bandwidth 
of 1 GHz, but like the Tektronix TDS 754D and the TDS 784D, the 54831 can be
upgraded to a 54832 with a single resistor modification.</p>

<p>The HP 548xx oscilloscope series was one of the first kind of test equipment that
used Windows as their base operating system. I have an older HP 54825A, introduced in
1997, that runs Windows 95. The 54831 was introduced in 2002. Early versions ran on
Windows 98 SE Embedded, Agilent later switched to Windows XP.</p>

<p>For many years, Agilent used a Motorola VP22 motherboard to manage the test equipment 
and that’s what mine have as well.</p>

<h1 id="inside-the-pc-system-of-the-54831">Inside the PC System of the 54831</h1>

<p>Many pieces of test equipment<sup id="fnref:sleeve" role="doc-noteref"><a href="#fn:sleeve" class="footnote" rel="footnote">2</a></sup> use a sleeve-like enclosure that slides around the inner
assembly. I’m not really fond of that: it can be clumsy to remove and put back, even if you
only need to work on the top side of the scope, the bottom electronics are exposed as well.
The 54831 has separate top and bottom covers. The only disadvantage is that there are much
more screws to hold it together: you need to remove 16 of them just for the top cover.</p>

<p>Figure 6-1 of the 
<a href="https://xdevs.com/doc/HP_Agilent_Keysight/HP%2054830,%2054831,%2054832X%20Service.pdf">service guide</a>
shows that very well:</p>

<p><a href="/assets/hp54831/removing_top_cover.png"><img src="/assets/hp54831/removing_top_cover.png" alt="Removing the top cover" /></a>
<em>(Click to enlarge)</em></p>

<p>After removing the top cover, you should see something like this:</p>

<p><img src="/assets/hp54831/top_inside_view.jpg" alt="Top inside view" /></p>

<p>It’s really just a PC with some custom PCI plug-in boards. From top to bottom:</p>

<ul>
  <li>
    <p>PCI to PCI bridge board</p>

    <p>The acquisition board has its own PCI interface. The PCI signals are carried over a 
wide flat cable that’s terminated on the PC side by a 
<a href="https://www.ti.com/product/PCI2050B/part-details/PCI2050BPDV">TI PCI2050PDV</a> 
PCI-to-PCI bridge chip on this board.</p>

    <p><em>I think this is the first time I’ve seen PCI signals being carried over a flat cable.</em></p>

    <p>In addition to the PCI flat cable to the acquisition board, there’s also a narrower flat cable
on the side that goes to the front panel.</p>
  </li>
  <li>GPIB interface board</li>
  <li>
    <p>Display adapter</p>

    <p>This board, based on a 
<a href="https://www.vgamuseum.info/index.php/cpu/item/185-chips-technologies-f65550">CHIPS F65550</a>, 
is more than a regular VGA board: it has a flat panel interface to the LCD front panel and an 
LCD backlight controller. There’s also a bridge cable to the next board that carries the 
real-time waveform overlay.</p>

    <p><img src="/assets/hp54831/display_board_annotated.jpg" alt="Annotated display board" /></p>

    <p>As was the case with some 3D graphics accelerators in the nineties, the VGA card renders the
GUI, but waveforms are rendered by a separate card and merged with the GUI in hardware.<sup id="fnref:overlay" role="doc-noteref"><a href="#fn:overlay" class="footnote" rel="footnote">3</a></sup></p>
  </li>
</ul>

<ul>
  <li>
    <p>Waveform overlay rendering board</p>

    <p>The full scope of this board is not 100% clear: based on an Altera FPGA, it definitely
renders the video overlay, but I don’t know if it does anything beyond that.</p>

    <p>While it has a bunch of connectors, none of them are used in the 54831 except for the bridge
cable to the display card. This means that all data is going through the PCI bus.</p>
  </li>
</ul>

<p>The other components are standard PC stuff:</p>

<ul>
  <li><a href="https://theretroweb.com/motherboards/s/motorola-vp22">Motorola VP22 motherboard</a></li>
  <li><a href="https://theretroweb.com/chips/1482">Pentium III 1 GHz (SL52R, 370 socket)</a></li>
  <li>CDROM drive</li>
  <li><a href="https://en.wikipedia.org/wiki/SuperDisk">Superdisk LS120 floppy drive</a></li>
  <li>10 GB IBM TravelStar hard drive</li>
</ul>

<h1 id="unit-a-agilent-54831m">Unit A: Agilent 54831M</h1>

<p>The “M” stands for military version. That doesn’t mean it has different specs, it’s
just that it has been assembled in Singapore, a country that’s considered more trustworthy than
Malaysia, where HP scopes were usually assembled (or so 
<a href="https://www.eevblog.com/forum/testgear/agilent-54831d-modernising/msg4616725/#msg4616725">someone claims on The Internet</a>.)</p>

<p><img src="/assets/hp54831/unit_a_backside.jpg" alt="Unit A backside" /></p>

<p>The seller claimed that this unit didn’t work at all, and he was right.
When plugging the power cord, it immediately started to squeal long ominous beeps and that
was it.</p>

<p>This unit has VIN# M42 (Rev.A.02.30), a production date of October 31, 2003 and
according to the Microsoft license sticker it’s one of the early versions that runs Win98.</p>

<h1 id="first-suspect-the-ibm-travelstar-hd">First Suspect: the IBM TravelStar HD</h1>

<p>My <a href="/2025/04/26/RS-AMIQ-Teardown-Analog-Deep-Dive.html">Rohde &amp; Schwarz AMIQ</a> came with a
non-functional IBM TravelStar HD. They are only slightly less notorious than the 
IBM Death…DeskStar drives and known to fail with a stuck read/write head assembly, so
I assumed that this would be the case here as well.</p>

<p>The first step was to extract the drive, check if it was still working outside of the
scope, and create a backup image.</p>

<p><a href="/assets/hp54831/unit_a_hardrive_in_case.jpg"><img src="/assets/hp54831/unit_a_hardrive_in_case.jpg" alt="Unit A: hard drive mounted in case" /></a>
<em>(Click to enlarge)</em></p>

<p>HP always puts their spinning disk hard drives on a separate platform that’s mounted on the main
chassis with some rubber feet to reduce the chance of damage due to rough handling or vibrations. 
The screws that fix the drive to the platform aren’t accessible, so you need to remove the platform 
first, then remove the drive.</p>

<p><strong>Removing the hard drive</strong></p>

<p>I disassembled pretty much the whole PC section of the scope to get to the hard drive:</p>

<ul>
  <li>
    <p>Disconnect all cables</p>

    <p>Make sure to first unlock the flex cables before pulling them out!</p>

    <p><img src="/assets/hp54831/unit_a_flex_cable.jpg" alt="Flex cable lock" /></p>

    <p>The IDC flat cable connectors have a metal retaining clip around them.
  Make sure you remove those first before trying to pull the connectors out. It’s easy
  to do with a screwdriver.</p>

    <p><img src="/assets/hp54831/unit_a_idc_cable_clip.jpg" alt="IDC connector metal clip" /></p>
  </li>
  <li>Remove all the PCI boards</li>
  <li>
    <p>Remove adapter board that merges floppy drive and hard drive cables</p>

    <p><img src="/assets/hp54831/unit_a_floppy_hd_adapter_board.jpg" alt="floppy/hard drive adapter board" /></p>
  </li>
  <li>
    <p>Remove the CDROM drive</p>

    <p>This requires removing a screw on the long stick across the case and one screw on the
  back of the case. See arrows:</p>

    <p><a href="/assets/hp54831/unit_a_remove_CDROM.jpg"><img src="/assets/hp54831/unit_a_remove_CDROM.jpg" alt="Remove CDROM drive" /></a>
  <em>(Click to enlarge)</em></p>

    <p>After this, you can slide the drive back a little bit and lift it out of the case.</p>
  </li>
  <li>
    <p>Remove the DRAM stick to access the bottom screw of the hard drive platform</p>
  </li>
  <li>
    <p>Unscrew the hard drive platform</p>

    <p>You now have access to all 4 screws of the platform.</p>

    <p><img src="/assets/hp54831/unit_a_unscrew_hd_platform.jpg" alt="Screwdriver in bottom left hard drive platform screw" /></p>
  </li>
</ul>

<p><em>Only after removing the platform by removing all 4 screws did I notice that the bottom 2 rubbers 
feet were not completely enclosed by the platform. It should be possible to remove the platform with 
a bit of force, without loosening the bottom 2 of the 4 screws.</em></p>

<p>The hard drive platform is freed!</p>

<p><img src="/assets/hp54831/unit_a_hd_freed.jpg" alt="Hard drive platform freed" /></p>

<p><strong>Creating a hard drive backup image</strong></p>

<p>I use a cheap <a href="https://www.amazon.com/dp/B08KT3F998">USB to SATA IDE adapter cable</a> 
to connect the drive to a PC, and <a href="https://hddguru.com/software/HDD-Raw-Copy-Tool/">HDD Raw Copy Tool</a>
to create a backup image.</p>

<p>Contrary to my expectations, the TravelStar HD worked fine! I could copy the whole drive
without any errors:</p>

<p><img src="/assets/hp54831/unit_a_hdd_raw_success.png" alt="HDD Raw Copy Tool success" /></p>

<p>I still expect that this drive will die eventually, but with a backup on hand, I reinstalled
the drive back into the scope.</p>

<h1 id="getting-the-pc-to-boot">Getting the PC to Boot</h1>

<p>The drive was functional, but the scope didn’t boot. I decided to strip the motherboard from all 
custom cards and make it work as if it were a 23 year old PC with only DRAM and an old PCI VGA 
card that I had lying around. That didn’t work either.</p>

<p>The error signature was a repeating pattern of a long beep followed by a long pause. It didn’t
match any of the standard AMI BIOS error codes.</p>

<iframe width="640" height="360" src="https://www.youtube.com/embed/a4_unllx8ho?si=M-3o9IvE535NNHnN" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>What if the issue was the CPU itself?</p>

<p>First step is to figure out how to remove the cooler:</p>

<p><img src="/assets/hp54831/unit_a_cpu_cooler.jpg" alt="Thermaltake Golden Orb Mini CPU coolor" /></p>

<p>With help from Mastodon, I was able to figure out that the cooler is a 
<a href="https://www.electromyne.de/public/catalog_xmlxslproducts.aspx?art=viewproduct&amp;suid=11565&amp;productid=1028313753&amp;zid=210bd6ab-f876-4795-91f7-6b11a146206f&amp;ln=gb">Thermaltake Golden Orb Mini</a>. 
There’s even still a website with a 
<a href="https://www.frostytech.com/articles/256/index.html">review and installation procedure</a>!
And that’s a good thing, because I don’t think I would have figured out that you need to do a
clockwise rotation of the cooler to detach it from the CPU.</p>

<p><img src="/assets/hp54831/unit_a_cpu_cooler_removed.jpg" alt="CPU cooler removed" /></p>

<p>On a whim, I remove the CPU from the socket, plugged it back in and…</p>

<p><img src="/assets/hp54831/unit_a_boot_screen_on_monitor.jpg" alt="Boot screen on VGA monitor" /></p>

<p>The dumb thing worked! Removing and reinserting the CPU was really all it took.</p>

<p>Another 15 minutes of installing the PCI boards and cables again, and I got to see
this:</p>

<p><img src="/assets/hp54831/unit_a_works.jpg" alt="Unit A is working" /></p>

<p>Success!</p>

<h1 id="unit-b-agilent-54831b">Unit B: Agilent 54831B</h1>

<p>I immediately started on the second unit. This one a slightly younger B version, made
in Malaysia with VIN# M32 (REV.A.03.50), running Windows XP Professional instead.</p>

<p><img src="/assets/hp54831/unit_b_backside.jpg" alt="Backside of unit b" /></p>

<p>As told by the seller, this one lights up, but gets stuck at the boot screen. And indeed:</p>

<p><img src="/assets/hp54831/unit_b_boot_screen_no_drives.jpg" alt="Boot screen. No drives" /></p>

<p>We can see the same 1 GHz Pentium III, the DRAM got an upgrade from 256 to 512 MB, but
no drives of any kind are detected. Which made sense once I opened it up:</p>

<p><img src="/assets/hp54831/unit_b_no_drives_inside.jpg" alt="No drives and cables inside..." /></p>

<p>The BIOS doesn’t detect any drives, because there aren’t any… There are also no
IDE cables and the custom board with the specialty LS120 floppy drive connector is missing
as well. I can live without floppy drive, I need a hard drive and a CDROM drive is nice to
have.</p>

<h1 id="a-compactflash-adapter-without-cable">A CompactFlash Adapter without Cable</h1>

<p>I usually replace spinning disk hard drives with CompactFlash cards. In the past, I’ve used
adapter boards that accept a 40-pin IDE cable, but this time I found something better: an
adapter board that plugs straight into the PC motherboard:</p>

<p><img src="/assets/hp54831/unit_b_cf_adapter.jpg" alt="CompactFlash to IDE adapter" /></p>

<p>You can find them <a href="https://www.amazon.com/dp/B07LBLXDZM">here on Amazon</a>, only $8 for 2.</p>

<p>The adapter board requires an external 5V or 3V supply through 4-pin Molex floppy 
connector. For another $8, I bought 4 of those, again <a href="https://www.amazon.com/dp/B0CLD7YRWC">on Amazon</a>.</p>

<p>The scope has a 2-pin connector with 5V and GND, I cut that off and connected it to the Molex
connector:</p>

<p><img src="/assets/hp54831/unit_b_cf_adapter_with_power_cable.jpg" alt="CompactFlash adapter with power cable and flash card" /></p>

<p>There are 3 LEDs on the adapter with “Detect”, “Active” and “Power” next to them. None of
these worked, but when plugged into the motherboard, my 16GB CF card got detected just fine:</p>

<p><img src="/assets/hp54831/unit_b_boot_screen_with_cf_and_cdrom.jpg" alt="Boot screen with CompactFlash card and CDROM drive detected" /></p>

<p>Note that the CDROM drive is also detected, because I bought 
<a href="https://www.amazon.com/dp/B00Z5AVRDY">2 40-pin flat cables</a> 
for $9. It’s weird that 2 simple cables are more expensive than 2 adapter boards 
with active components.</p>

<p>Here are the adapter and the CDROM cables installed in the motherboard:</p>

<p><img src="/assets/hp54831/unit_b_cf_and_cdrom_connected.jpg" alt="CF adapter with card and CDROM flat cable plugged into motherboard" /></p>

<h1 id="installing-the-software">Installing the Software</h1>

<p><strong>Important: if your 54831 originally came with Windows 98 SE, a Windows XP image may or may not
work. Some scopes with Windows 98 have PCI extension boards that are not compatible with the WinXP
drivers.</strong></p>

<p>Next up: finding the software to run the scope. The hard part is not finding the software, this scope
is quite popular with hobbyists who are willing to share, it’s to figure which one to use.</p>

<p>I ended up using an image stored on the <a href="https://onedrive.live.com/?redeem=aHR0cHM6Ly8xZHJ2Lm1zL2YvcyFBbXFhcjhfWFE5VXpqNlloTjF4SGRNOHRRZEtNT0E&amp;id=33D543D7CFAF9A6A%21250657&amp;cid=33D543D7CFAF9A6A">OneDrive of Tony_G</a>. (Thanks Tony!)
It contains way more than I needed, but the golden ticket was the 6.38 GB <code class="language-plaintext highlighter-rouge">xp54831.vhdx</code> file in
the <code class="language-plaintext highlighter-rouge">54831M</code> directory. Also check out <code class="language-plaintext highlighter-rouge">Install hints.pdf</code> in the same folder with the installation
instructions.</p>

<p>It all comes down to this:</p>

<ul>
  <li>Install <a href="https://rufus.ie/en/">Rufus</a>, a utility to create bootable USB drives.</li>
  <li>Connect the CompactFlash card to your PC with a CompactFlash to USB adapter like
<a href="https://www.amazon.com/dp/B08P517NW5?th=1">this one on Amazon</a>. I used a 16 GB
CF card. I think 8 GB should work too, but I’m not 100% sure.</li>
  <li>Copy over <code class="language-plaintext highlighter-rouge">xp54831.vhdx</code> to the CF card with Rufus.</li>
</ul>

<p>Once done, install the CF card into the scope and boot. The image that you installed on the
drive contains a Symantec Ghost sub-image. When you boot the scope, you should see a Windows 98
splash screen (not WinXP!) and Symantec Ghost. Follow the instructions of the PDF file and
eventually, it will end like this:</p>

<p><img src="/assets/hp54831/unit_b_ghost_complete.jpg" alt="Ghost - Clone Complete" /></p>

<p>Reboot again, and you’ll see this, finally:</p>

<p><img src="/assets/hp54831/unit_b_win_xp_splash.jpg" alt="WinXP splash" /></p>

<p>And this:</p>

<p><img src="/assets/hp54831/unit_b_scope_waveforms_with_noise.jpg" alt="Scope showing 4 waveforms with lots of noise" /></p>

<p>The software is functional, but there’s an awful lot of noise on those signals. If you’re seeing that,
don’t worry: this happens when the scope isn’t calibrated. Go to “Calibration” in one of the menus,
let it run all the way, it takes around 1 hour, and the noise should be gone. Hurray!</p>

<h1 id="cpu-temperature-alarm">CPU Temperature Alarm</h1>

<p>After a while, the scope gave off this persistent alarm:</p>

<iframe width="640" height="360" src="https://www.youtube.com/embed/zMQvYMoHPoo?si=8P-shzt1ZYPG4ZVk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>According to the VP22 motherboard manual, the alarm can go off for 3 reasons: case open (there was no such
sensor, so that didn’t apply), CPU temperature alarm, or CPU voltage alarm.</p>

<p>I assumed a CPU voltage alarm due to old capacitors, but to be really sure, you can go into
the PC Health Status section of the BIOS menu and disable the CPU temperature alarm:</p>

<p><img src="/assets/hp54831/unit_b_temp_alarm_disabled.jpg" alt="CPU temperature alarm disabled" /></p>

<p>This made the alarm go away. That’s great because it’s much easier to fix the temperature than to replace 
the capacitors on the motherboard or the main power supply: apply thermal paste, reseat the cooler.</p>

<p>The temperature of the CPU in the BIOS screen was 64C. This is not very high by today’s
standards, but the 
<a href="https://download.intel.com/design/intarch/applnots/27332504.pdf">Pentium III thermal design guide</a>
sets a maximum junction temperature of 75C.</p>

<p>One of the benefits of running Windows XP is that many tools and USB memory
sticks just work. I installed <a href="https://www.alcpu.com/CoreTemp/">Core Temp</a> to continuously
monitor the temperatures after adding thermal paste and reseating the cooler.</p>

<p>Note that it is <em>really</em> important that you feel a subtle click when you rotate the cooler 
counter-clockwise to secure it in place. At the same time, you can’t rotate too hard because
the metal tabs on the CPU socket can shear off or you can crack the CPU die.</p>

<p>Here’s the result after:</p>

<p><img src="/assets/hp54831/core_temp_max_temp.png" alt="Core Temp - max temperature" /></p>

<p>At rest, with the scope app disabled, I saw temperatures around 40C. When the scope was
running with the CPU pegged at 100%, temperatures never exceeded 65C.</p>

<p>I still disabled the CPU temperature alarm though, because that beeping is way
too annoying. This will probably come back to bite me some time in the future…</p>

<p>The scope was really working fine now, there was just one more thing to do.</p>

<h1 id="upgrade-to-1-ghz-bandwidth">Upgrade to 1 GHz Bandwidth</h1>

<p>As mentioned earlier, the scope can be upgraded to a 54832 with 1 GHz bandwidth by
removing a single resistor on the acquisition board.</p>

<p>The resistor array can be found here:</p>

<p><a href="/assets/hp54831/unit_b_resistor_array.jpg"><img src="/assets/hp54831/unit_b_resistor_array.jpg" alt="Resistor array on acquisition board" /></a>
<em>(Click to enlarge)</em></p>

<p>Before the modification, all resistors should be present:</p>

<p><img src="/assets/hp54831/resistor_array_before_mod.jpg" alt="All resistors present" /></p>

<p>Remove this resistor for the upgrade:</p>

<p><img src="/assets/hp54831/resistor_array_after_mod.jpg" alt="One resistor removed" /></p>

<p>After rebooting, the scope identifies itself as an Agilent 54832B:</p>

<p><img src="/assets/hp54831/unit_b_about_infiniium_after_mod.jpg" alt="About Infiniium windows showing 54832B" /></p>

<p>To test the modification, I fed the AUX Out<sup id="fnref:aux_out" role="doc-noteref"><a href="#fn:aux_out" class="footnote" rel="footnote">4</a></sup> signal at the back of the scope into channel 1,
with 50 Ohm termination active.</p>

<p>Before the mod, the rise time averaged to 481 ps:</p>

<p><img src="/assets/hp54831/rise_time_before_mod.png" alt="Rise time before modification" /></p>

<p>After removing the resistor, it dropped to 331 ps:</p>

<p><img src="/assets/hp54831/rise_time_after_mod.png" alt="Rise time after modification" /></p>

<p>The scope bandwidth is calculated as <code class="language-plaintext highlighter-rouge">0.35 / t_rise</code>. Going from 481 to 
331 ps is an increase of 727 MHz to 1057 MHz. The modification worked and
the result is within spec, but others have reported an increase to 1.2 GHz. It’s
possible that my measurement is limited by the rise time of the AUX Out signal,
and that I’d see even better numbers with a real pulse generator. That’s for
another time…</p>

<h1 id="additional-changes-are-possible">Additional Changes are Possible</h1>

<p>I only did the minimum to get the scopes working, and did the resistor mod. You
can find more impressive modifications on the EEVblog forum:</p>

<ul>
  <li>Install motherboards with P4 CPUs. This requires some mechanical surgery
to the case as well, to make the connectors fit.</li>
  <li>Use faster SSDs than my compact flash cards. This can bring down the boot time
from 4 minutes to less than a minute.</li>
  <li>Replace the 640x480 LCD screen with a 1024x768 LCD screen.</li>
</ul>

<p>Some of these modifications may depend on each other. E.g. the larger resolution
LCD screen requires an integrated GPU that is not present on the VP22 motherboard.</p>

<p>I decided not to do any of them: the scope works well enough for my needs.</p>

<h1 id="conclusion">Conclusion</h1>

<p>For $200 and around $30 in additional components, I got myself 2 working
4 Gsps scopes with 600 MHz or 1 GHz bandwidth. I already sold unit A for
$200, which I think is an excellent deal for the buyer. I’m keeping the
other one.</p>

<p><em>All words in this blog post were written by a human.</em></p>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://xdevs.com/doc/HP_Agilent_Keysight/HP%2054830,%2054831,%2054832X%20Service.pdf">Agilent Model 54830 Series Oscilloscopes - Service Guide</a></li>
  <li><a href="https://download.intel.com/design/intarch/applnots/27332504.pdf">Pentium III thermal design guide</a></li>
</ul>

<p><strong>EEVblog forum</strong></p>

<ul>
  <li><a href="https://www.eevblog.com/forum/testgear/54831b-upgrade-to-54832b-possible">Agilent 600MHz 54831B hack for 1GHz 54832B possible? YES!</a></li>
  <li><a href="https://www.eevblog.com/forum/testgear/agilent-54831d-modernising/">Agilent 54831D modernising</a></li>
  <li><a href="https://www.eevblog.com/forum/testgear/agilent-54831m-upgrade-to-windows-xp-guide-and-resources/">Agilent 54831M upgrade to Windows XP, guide and resources</a></li>
</ul>

<p><strong>Other info</strong></p>
<ul>
  <li>
    <p><a href="https://wonghoi.humgar.com/blog/2016/07/">5V supply for adapter</a></p>

    <p>Instead of cutting off the existing 5V connector for the CompactFlash adapter,
  I could have gotten the 5V from somewhere else on the motherboard.</p>
  </li>
  <li>
    <p><a href="https://tolisdiy.com/2019/10/04/agilent-54831b-oscilloscope-taming/">Agilent 54831B Oscilloscope Taming</a></p>
  </li>
  <li>
    <p><a href="https://1drv.ms/f/s!Amqar8_XQ9Uzj6YhN1xHdM8tQdKMOA">Tony_G Various disk images</a></p>
  </li>
</ul>

<h1 id="footnotes">Footnotes</h1>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:Agilent" role="doc-endnote">
      <p>I’m still not used to the recent renaming of HP into Agilent and often
        use their names interchangeably. <a href="#fnref:Agilent" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:sleeve" role="doc-endnote">
      <p>The 54825A has a sleeve-like enclosure. <a href="#fnref:sleeve" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:overlay" role="doc-endnote">
      <p>You shouldn’t try this yourself because it can damage the electronics, but if you 
        unplug the bridge cable while the scope is up and running, you’ll see that the GUI 
        is still rendered but the waveforms disappear. <a href="#fnref:overlay" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:aux_out" role="doc-endnote">
      <p>Make sure to select the 10 MHz output for AUX Out in the Calibration menu. <a href="#fnref:aux_out" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[In the end, it comes down to fixing two early 2000s PCs…]]></summary></entry><entry><title type="html">Polyphase Channelizers with Frequency Offset - a Bluetooth LE Example</title><link href="https://tomverbeure.github.io/2026/03/05/Polyphase-Channelizer-with-Offset.html" rel="alternate" type="text/html" title="Polyphase Channelizers with Frequency Offset - a Bluetooth LE Example" /><published>2026-03-05T10:00:00+00:00</published><updated>2026-03-05T10:00:00+00:00</updated><id>https://tomverbeure.github.io/2026/03/05/Polyphase-Channelizer-with-Offset</id><content type="html" xml:base="https://tomverbeure.github.io/2026/03/05/Polyphase-Channelizer-with-Offset.html"><![CDATA[<script async="" src="https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=TeX-AMS_CHTML"></script>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#a-bluetooth-le-trace-as-example" id="markdown-toc-a-bluetooth-le-trace-as-example">A Bluetooth LE Trace as Example</a></li>
  <li><a href="#input-complex-heterodyne" id="markdown-toc-input-complex-heterodyne">Input Complex Heterodyne</a></li>
  <li><a href="#derivation-of-post-decimation-offset-correction" id="markdown-toc-derivation-of-post-decimation-offset-correction">Derivation of Post-Decimation Offset Correction</a></li>
  <li><a href="#simplifying-for-the-half-bin-offset-case" id="markdown-toc-simplifying-for-the-half-bin-offset-case">Simplifying for the Half-Bin Offset Case</a></li>
  <li><a href="#the-odd-case-of-an-odd-number-of-channels" id="markdown-toc-the-odd-case-of-an-odd-number-of-channels">The Odd Case of an Odd Number of Channels</a></li>
  <li><a href="#reducing-the-number-of-phase-adjustment-values" id="markdown-toc-reducing-the-number-of-phase-adjustment-values">Reducing the Number of Phase Adjustment Values</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
  <li><a href="#footnotes" id="markdown-toc-footnotes">Footnotes</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>In previous blog post, I introduced the 
<a href="/2026/02/16/Polyphase-Channelizer.html">polyphase channelizer</a>,
a DSP algorithm that is incredibly efficient at heterodyning multiple channels to baseband
in parallel. I made two major assumptions about the nature of the input signal:</p>

<ul>
  <li>The bandwidth of a channel is equal to the the input sample rate divided by the decimation factor.</li>
  <li>The center frequency of each channel is an integer multiple of the channel bandwidth</li>
</ul>

<p>If these conditions are satisfied, the channelizer reduces to a filter bank with real coefficients
and an inverse FFT on the output of the filter phases.</p>

<p>In this blog post, I’ll use a real-world Bluetooth LE recording and a polyphase channelizer to
extract all channels in parallel. There’s a twist, however, in that the center frequency of the
channels is not a multiple of the channel bandwidth. With a little bit of additional math,
we can work around that too.</p>

<p>I’m still roughly covering topics here are covered in 
<a href="https://www.youtube.com/watch?v=afU9f5MuXr8">“Recent Interesting and Useful Enhancements of Polyphase Filter Banks”</a>
by fred harris, though my approach is more mathematical and less based on intuition. Furthermore,
harris doesn’t work out the details for any generic frequency offset and immediately jumps to the 
half-channel case. But even there, he spends most of the time discussing a clever trick for odd 
decimation factors than the generic case that works for all decimation factors. I first 
deal with the full generic case and then simplify the outcome by imposing additional constraints.</p>

<h1 id="a-bluetooth-le-trace-as-example">A Bluetooth LE Trace as Example</h1>

<p><a href="https://en.wikipedia.org/wiki/Bluetooth_Low_Energy">Bluetooth Low Energy (BLE)</a> 
lives in the unlicensed 2.4 GHz radio band that’s also used by wifi and many other
protocols. It has 40 channels that are each 2 MHz wide for a total bandwidth of 80 MHz. The
center frequency of bottom physical channel is 2402 MHz. In total, BLE occupies the spectrum from
2401 MHz to 2481 MHz.</p>

<p>The 2.4 GHz radio band is often congested. To ensure that at least some packets get through, BLE
uses frequency hopping: it continuously jumps from one channel to the next in some predictable
pattern. However, to establish an initial connection, there are a number of fixed management channels.</p>

<p><a href="https://joshuawise.com">Joshua</a> used his BladeRF SDR unit to provided me with a 5 ms recording with
the following characteristics:</p>

<ul>
  <li>center frequency: 2.441 GHz</li>
  <li>sample rate: 96 MHz</li>
  <li>quadrature I/Q sampling</li>
</ul>

<p>We can create a spectral power density 
<a href="https://en.wikipedia.org/wiki/Waterfall_plot">waterfall plot</a> 
of this, where the X-axis shows the time and the Y axis the 
<a href="https://en.wikipedia.org/wiki/Short-time_Fourier_transform">short time Fourier transform (STFT)</a> of
the signal, showing the energy for the full frequency range.</p>

<p><a href="/assets/polyphase/ble/ble_input_data_waterfall.png"><img src="/assets/polyphase/ble/ble_input_data_waterfall.png" alt="BLE Waterfall Plot" /></a>
<em>(Click to enlarge)</em></p>

<p>We can see a bright line at the 2441 MHz center frequency. This is a common artifact of the 
imperfect SDR hardware. It can be caused by local oscillator leakage or an imbalance between the 
I and Q channels of the quadrature AD converters, or both.</p>

<p>In this video, harris talks about how DC is often problematic, and a reason to have channels with a
frequency offset so that none of the channel center frequencies coincide with DC. This trace shows why
this is good advice.</p>

<p>We can also see some symmetry around the 2441 MHz line. For example, there’s a short burst around
1.1 ms at 2415 Mhz and a weaker version at 2467 MHz. This weaker version isn’t real either, but a
spectral mirror image that’s caused by an imbalance between the I and the Q channels: their phase delta
might not be exactly 90 degrees or they might have a slightly different gain on their way to the 
ADCs.<sup id="fnref:gram_schmidt" role="doc-noteref"><a href="#fn:gram_schmidt" class="footnote" rel="footnote">1</a></sup>
This is another topic that harris warns about: if possible, use a single double-speed ADC and do
all the I/Q handling in the mathematically perfect digital domain.</p>

<p><em>Due to the sample rate limitations
of the BladeRF, we have to use a quadrature analog acquistion path, but this doesn’t materially impact
the techniques derived in this blog post.</em></p>

<p>A recording of 96 Msps complex samples covers 48 channels of 2 MHz. Since BLE only has 40 active channels,
we have a little bit too much data, but that’s ok. In the waterfall plot below, I’ve added separators that
the individual channels. The suprious 2441 MHz line is now obstructed, which is good because it shows that 
it falls on a transition band.</p>

<p><a href="/assets/polyphase/ble/ble_input_data_waterfall_bars.png"><img src="/assets/polyphase/ble/ble_input_data_waterfall_bars.png" alt="BLE Waterfall Plot with Channels" /></a>
<em>(Click to enlarge)</em></p>

<p>In the previous blog post, we operated under the assumption that channel center frequencies were located
at a multiple of the decimated sample rate:</p>

\[F_c = \frac{F_s}{M} c, \quad c = -\frac{M}{2}, \dots, -1, 0, 1, \dots, \frac{M}{2}-1\]

<p><img src="/assets/polyphase/ble/ble-dc_offset.svg" alt="No channel center frequency offset" /></p>

<p>That’s not the case here. Instead, we have the following situation:</p>

\[F_c = \frac{F_s}{M} c + \frac{F_s}{2M}, \quad c = -\frac{M}{2}, \dots, -1, 0, 1, \dots, \frac{M}{2}-1\]

<p><img src="/assets/polyphase/ble/ble-half_bin_offset.svg" alt="Half-bin channel center frequency offset" /></p>

<p>Concretely, instead of channel center frequencies at -2, 0, 2, 4, … MHz, the BLE channels are located at
-3, -1, 1, 3, 5, … MHz. Having the center frequency offset at exacty half the channel width is
something we can exploit later, but I will first develop the generic case where the frequency 
offset can be anything, and then simplify.</p>

<h1 id="input-complex-heterodyne">Input Complex Heterodyne</h1>

<p>The easiest way to align the channel center frequencies to an integer multiple of the output
sample rate is to remove the offset with a complex heterodyne on the input signal.</p>

<p>Like this:</p>

\[\omega_\Delta = 2 \pi \frac{F_\text{offset}}{F_s} \\
x[n] = x'[n] \, e^{j \omega_\Delta n} \\\]

<p>This works, of course, but it undoes all the effort from last blog post where we tried very
hard to not do anything at the input sample rate.</p>

<p>Still, let’s do it anyway and see what kind of result we get.</p>

<p>The code to do the input heterodyne and the polyphase channelizer is below. 
I’ve stripped some of the comments for brevity, but check out the 
<a href="https://github.com/tomverbeure/polyphase_blog_series/blob/main/ble.py">code in the GitHub repo</a>
for more details.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">n</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">ble_input</span><span class="p">),</span> <span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">)</span>

<span class="c1"># Complex 1 MHz rotator to shift the spectrum by the half-channel offset
</span><span class="n">heterodyne_1mhz</span>     <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="mf">1j</span> <span class="o">*</span> <span class="mf">2.0</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">pi</span> <span class="o">*</span> <span class="n">channel_offset_hz</span> <span class="o">/</span> <span class="n">sample_rate_hz</span> <span class="o">*</span> <span class="n">n</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">complex64</span><span class="p">)</span>
<span class="c1"># Do the heterodyne on the input signal
</span><span class="n">ble_input_pre_1mhz</span>  <span class="o">=</span> <span class="n">ble_input</span> <span class="o">*</span> <span class="n">heterodyne_1mhz</span>

<span class="c1"># Channel low-pass filter with a passband from 0 to 600 kHz
# and a stopband that starts at 800 kHz.
</span><span class="n">h_lpf</span> <span class="o">=</span> <span class="n">create_remez_lowpass_fir</span><span class="p">(</span>
    <span class="n">input_sample_rate_hz</span>     <span class="o">=</span> <span class="n">sample_rate_hz</span><span class="p">,</span>
    <span class="n">passband_hz</span>              <span class="o">=</span> <span class="mf">600e3</span><span class="p">,</span>
    <span class="n">passband_ripple_db</span>       <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span>
    <span class="n">stopband_hz</span>              <span class="o">=</span> <span class="mf">800e3</span><span class="p">,</span>
    <span class="n">stopband_attenuation_db</span>  <span class="o">=</span> <span class="mf">50.0</span>
    <span class="p">)</span>

<span class="c1"># Pad the filter with zeros so that the polyphase decomposition 
# is a clean 2D array.
</span><span class="n">h_lpf</span>   <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">pad</span><span class="p">(</span><span class="n">h_lpf</span><span class="p">,</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="nb">len</span><span class="p">(</span><span class="n">h_lpf</span><span class="p">)</span> <span class="o">%</span> <span class="n">decim_factor</span><span class="p">)</span> <span class="p">)</span>

<span class="c1"># Polyphase filter decomposition: 
# 48 rows, each row has interleaved coefficients.
</span><span class="n">h_lpf_poly</span>  <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">reshape</span><span class="p">(</span>
        <span class="n">h_lpf</span><span class="p">,</span> <span class="p">(</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">h_lpf</span><span class="p">)</span> <span class="o">//</span> <span class="n">decim_factor</span><span class="p">),</span> <span class="n">decim_factor</span><span class="p">)</span> 
    <span class="p">).</span><span class="n">T</span>

<span class="c1"># Polyphase decomposition/decimation of the input signal
</span><span class="n">ble_decim_pre_1mhz</span>  <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">flipud</span><span class="p">(</span>
    <span class="n">np</span><span class="p">.</span><span class="n">reshape</span><span class="p">(</span>
        <span class="n">ble_input_pre_1mhz</span><span class="p">,</span>
        <span class="p">((</span><span class="nb">len</span><span class="p">(</span><span class="n">ble_input_pre_1mhz</span><span class="p">)</span> <span class="o">//</span> <span class="n">decim_factor</span><span class="p">),</span> <span class="n">decim_factor</span><span class="p">),</span>
    <span class="p">).</span><span class="n">T</span>
<span class="p">)</span>

<span class="c1"># Calculate the output of all polyphase filters
</span><span class="n">h_poly_out_pre_1mhz</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span>
        <span class="p">[</span><span class="n">np</span><span class="p">.</span><span class="n">convolve</span><span class="p">(</span><span class="n">ble_decim_pre_1mhz</span><span class="p">[</span><span class="n">_</span><span class="p">],</span> <span class="n">h_lpf_poly</span><span class="p">[</span><span class="n">_</span><span class="p">])</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">decim_factor</span><span class="p">)])</span>

<span class="c1"># Vectorized IFFT to calculate the output of all channels
</span><span class="n">channel_data_pre_1mhz</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">fft</span><span class="p">.</span><span class="n">ifft</span><span class="p">(</span><span class="n">h_poly_out_pre_1mhz</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">complex64</span><span class="p">)</span>
</code></pre></div></div>

<p>After extracting the data from channel 33<sup id="fnref:33" role="doc-noteref"><a href="#fn:33" class="footnote" rel="footnote">2</a></sup> between 1.14 ms and 1.24 ms, we get the following:</p>

<p><a href="/assets/polyphase/ble/chan_33_time_plot_het_pre_iq.svg"><img src="/assets/polyphase/ble/chan_33_time_plot_het_pre_iq.svg" alt="Channel 33 I/Q time plot" /></a>
<em>(Click to enlarge)</em></p>

<p>The active period of a packet can be derived from the amplitude of the I/Q vector (green).
And the I/Q data clearly has some structure in it.</p>

<p>BLE uses 
<a href="https://en.wikipedia.org/wiki/Frequency-shift_keying#Gaussian_frequency-shift_keying">Gaussian frequency shift keying (GFSK)</a>.
Like ordinary <a href="https://en.wikipedia.org/wiki/Frequency-shift_keying">frequency shift keying (FSK)</a>, 
a 0 and a 1 are coded with slightly different frequencies, but the transistion between them is just
a bit smoother for GFSK.</p>

<p>Frequency is the derivative of the phase. Since I and Q are available, you can calculate the
phase as follows<sup id="fnref:atan2" role="doc-noteref"><a href="#fn:atan2" class="footnote" rel="footnote">3</a></sup>:</p>

\[\phi[n] = \text{atan2}(q[n],i[n])\]

<p>The derivative is simply the delta between consecutive phase samples.</p>

<p>In Python, we can demodulate a GFSK signal like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">angle</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">unwrap</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">angle</span><span class="p">(</span><span class="n">iq_data</span><span class="p">))</span>
<span class="n">d_angle</span> <span class="o">=</span> <span class="n">angle</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">angle</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
</code></pre></div></div>

<p>Here’s the result:</p>

<p><a href="/assets/polyphase/ble/chan_33_time_plot_het_pre_gfsk.svg"><img src="/assets/polyphase/ble/chan_33_time_plot_het_pre_gfsk.svg" alt="Channel 33 GFSK time plot" /></a>
<em>(Click to enlarge)</em></p>

<p>A BLE packet starts with a 16-symbol 1010101010101010 sync word, followed by data. This definitely looks
like a valid packet.</p>

<p>Cool! But it costs us a table with 48 rotator values that are fed into a complex multiplier, at the input sample rate. 
In this example, the input samples are already complex, but if they were real, the input heterodyne also
forces all filter bank calculations to become complex.</p>

<p>Can we do better?</p>

<h1 id="derivation-of-post-decimation-offset-correction">Derivation of Post-Decimation Offset Correction</h1>

<p>Here’s the standard polyphase channelizer pipeline from last blog post:</p>

<p><a href="/assets/polyphase/ble/ble-polyphase_ifft.svg"><img src="/assets/polyphase/ble/ble-polyphase_ifft.svg" alt="Standard polyphase channelizer" /></a>
<em>(Click to enlarge)</em></p>

<p>And here’s the mathematical description of the pipeline, for 3 channels and a filter with 9 coefficients:</p>

\[\begin{alignedat}{0}
y_c[n]    &amp; = &amp; e^{j \frac{2 \pi}{3} c \, 0} &amp; ( &amp; h[0] &amp; x[3n]   &amp; + &amp;  h[3] &amp; x[3n-3] &amp; + &amp; h[6] &amp; x[3n-6] &amp; ) \\
          &amp; + &amp; e^{j \frac{2 \pi}{3} c \, 1} &amp; ( &amp; h[1] &amp; x[3n-1] &amp; + &amp;  h[4] &amp; x[3n-4] &amp; + &amp; h[7] &amp; x[3n-7] &amp; ) \\
          &amp; + &amp; e^{j \frac{2 \pi}{3} c \, 2} &amp; ( &amp; h[2] &amp; x[3n-2] &amp; + &amp;  h[5] &amp; x[3n-5] &amp; + &amp; h[8] &amp; x[3n-8] &amp; ) \\
\\
y_c[n+1]  &amp; = &amp; e^{j \frac{2 \pi}{3} c \, 0} &amp; ( &amp; h[0] &amp; x[3n+3] &amp; + &amp;  h[3] &amp; x[3n]   &amp; + &amp; h[6] &amp; x[3n-3] &amp; ) \\
          &amp; + &amp; e^{j \frac{2 \pi}{3} c \, 1} &amp; ( &amp; h[1] &amp; x[3n+2] &amp; + &amp;  h[4] &amp; x[3n-1] &amp; + &amp; h[7] &amp; x[3n-4] &amp; ) \\
          &amp; + &amp; e^{j \frac{2 \pi}{3} c \, 2} &amp; ( &amp; h[2] &amp; x[3n+1] &amp; + &amp;  h[5] &amp; x[3n-2] &amp; + &amp; h[8] &amp; x[3n-5] &amp; ) \\
\end{alignedat}\]

<p>Let’s generalize this formula to \(M\) channels and \(N\) filter taps per phase:</p>

\[y_c[n] = \sum_{m=0}^{M-1}  
         \underbrace{ e^{j \frac{2 \pi}{M} c \, m} }_\text{IFFT}
         \sum_{k=0}^{N-1} h[kM + m] \; x[(n - k)M - m] \\\]

<p>Now substitute input \(x[n]\) with an input signal to which a complex heterodyne has been applied:</p>

\[x[n] = x'[n] \; e^{j \omega_{\Delta} n}\]

<p><a href="/assets/polyphase/ble/ble-pre_polyphase_ifft.svg"><img src="/assets/polyphase/ble/ble-pre_polyphase_ifft.svg" alt="Input heterodyne + polyphase channelizer" /></a>
<em>(Click to enlarge)</em></p>

\[y_c[n] = \sum_{m=0}^{M-1}  e^{j \frac{2 \pi}{M} c \, m}  \sum_{k=0}^{N-1} h[kM + m] \; x'[(n - k)M - m] \; 
         \underbrace{e^{j \omega_{\Delta} ((n - k)M - m)}}_\text{offset adjust rotator}\]

<p>A frequency offset adjustment rotator has been introduced.</p>

<p>We can split it up this exponential, extract a free-running output rotator that only depends on decimated 
sample number \(nM\), and move it all the way to the front:</p>

\[y_c[n] = \underbrace{e^{j \omega_{\Delta} Mn} }_\text{output rotator}
         \sum_{m=0}^{M-1}  e^{j \frac{2 \pi}{M} c \, m}  \sum_{k=0}^{N-1} h[kM + m] \; x'[(n - k)M - m] \; e^{j \omega_{\Delta} (- kM - m)}\]

<p>Now extract a term that only depends on polyphase variable \(m\):</p>

\[y_c[n] = e^{j \omega_{\Delta} Mn} \sum_{m=0}^{M-1}  
         \underbrace{e^{-j \omega_{\Delta} m} }_\text{phase adjustment}
         e^{j \frac{2 \pi}{M} c \, m}  \sum_{k=0}^{N-1} h[kM + m] \; x'[(n - k)M - m] \; e^{j \omega_{\Delta} (- kM)}\]

<p>Finally, rearrange the remaining exponential that is different for each filter coefficient index \(k\):</p>

\[y_c[n] = \underbrace{e^{j \omega_{\Delta} Mn}}_{\text{output rotator}} 
         \sum_{m=0}^{M-1}  
         \underbrace{e^{-j \omega_{\Delta} m}}_{\text{phase adjustment}} 
         \underbrace{e^{j \frac{2 \pi}{M} c \, m}}_{\text{IFFT}}  
         \sum_{k=0}^{N-1} h[kM + m]  
         \underbrace{e^{-j \omega_{\Delta} (kM)}}_{\text{filter adjustment}}  \; x'[(n - k)M - m]\]

<p>There are 3 additional terms now:</p>

<ul>
  <li>all the filter coefficients are modified by a filter adjustment term \(e^{-j \omega_{\Delta} (kM)}\).</li>
  <li>the output of each phase sub-filter is multiplied by a phase adjustment term \(e^{-j \omega_{\Delta} m}\).</li>
  <li>all outputs of the IFFT are subjected to complex heterodyne \(e^{j \omega_{\Delta} Mn}\).</li>
</ul>

<p>None of this is ideal, but the first 2 terms are not dependent on the sample number and can be baked 
into the design. Meanwhile the rotator at the end not only runs at a rate that is M times lower, but the 
phase step of the rotator is also M times larger which reduces the size of a lookup table with rotator
values.</p>

<p>The diagram looks like this:</p>

<p><a href="/assets/polyphase/ble/ble-polyphase_ifft_post.svg"><img src="/assets/polyphase/ble/ble-polyphase_ifft_post.svg" alt="Polyphase channelizer with decimated offset adjustment" /></a>
<em>(Click to enlarge)</em></p>

<p>In Python, we can use this code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># No more input heterodyne. Immediately decimate the input signal
</span><span class="n">ble_decim</span>   <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">flipud</span><span class="p">(</span>
    <span class="n">np</span><span class="p">.</span><span class="n">reshape</span><span class="p">(</span>
        <span class="n">ble_input</span><span class="p">,</span>
        <span class="p">((</span><span class="nb">len</span><span class="p">(</span><span class="n">ble_input</span><span class="p">)</span> <span class="o">//</span> <span class="n">decim_factor</span><span class="p">),</span> <span class="n">decim_factor</span><span class="p">),</span>
    <span class="p">).</span><span class="n">T</span>
<span class="p">)</span>

<span class="c1"># Calculate frequency offset
</span><span class="n">freq_offset</span>           <span class="o">=</span> <span class="n">channel_offset_hz</span> <span class="o">/</span> <span class="p">(</span><span class="n">sample_rate_hz</span> <span class="o">/</span> <span class="n">decim_factor</span><span class="p">)</span>
<span class="n">omega_delta</span>           <span class="o">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">pi</span> <span class="o">*</span> <span class="n">freq_offset</span> <span class="o">/</span> <span class="n">decim_factor</span>

<span class="c1"># Modify the low pass filter coefficients
</span><span class="n">h_n</span>                   <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">h_lpf_poly</span><span class="p">[</span><span class="mi">0</span><span class="p">]),</span> <span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">)</span>
<span class="n">h_lpf_poly_adj</span>        <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="mf">1j</span> <span class="o">*</span> <span class="n">omega_delta</span> <span class="o">*</span> <span class="n">decim_factor</span> <span class="o">*</span> <span class="n">h_n</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">complex64</span><span class="p">)</span>
<span class="n">h_lpf_poly_het</span>        <span class="o">=</span> <span class="n">h_lpf_poly</span> <span class="o">*</span> <span class="n">h_lpf_poly_adj</span>

<span class="c1"># Output of the polyphase filter
</span><span class="n">h_poly_out</span>            <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">np</span><span class="p">.</span><span class="n">convolve</span><span class="p">(</span><span class="n">ble_decim</span><span class="p">[</span><span class="n">_</span><span class="p">],</span> <span class="n">h_lpf_poly_het</span><span class="p">[</span><span class="n">_</span><span class="p">])</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">decim_factor</span><span class="p">)])</span>

<span class="c1"># Apply a phase rotation to the output of each phase
</span><span class="n">phase_nr</span>              <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="n">decim_factor</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">)</span>
<span class="n">h_phase_adj</span>           <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="mf">1j</span> <span class="o">*</span> <span class="n">omega_delta</span> <span class="o">*</span> <span class="n">phase_nr</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">complex64</span><span class="p">)</span>
<span class="n">h_poly_out_phase_adj</span>  <span class="o">=</span> <span class="n">h_poly_out</span> <span class="o">*</span> <span class="n">h_phase_adj</span><span class="p">[:,</span> <span class="bp">None</span><span class="p">]</span>

<span class="c1"># IFFT...
</span><span class="n">channel_data</span>          <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">fft</span><span class="p">.</span><span class="n">ifft</span><span class="p">(</span><span class="n">h_poly_out_phase_adj</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">complex64</span><span class="p">)</span>

<span class="c1"># Output rotator
</span><span class="n">sample_nr</span>             <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="n">channel_data</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">)</span>
<span class="n">heterodyne_1mhz_decim</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="mf">1j</span> <span class="o">*</span> <span class="n">omega_delta</span> <span class="o">*</span> <span class="n">decim_factor</span> <span class="o">*</span> <span class="n">sample_nr</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">complex64</span><span class="p">)</span>

<span class="c1"># Heterodyne all channels
</span><span class="n">channel_data_1mhz_post</span>  <span class="o">=</span> <span class="n">channel_data</span> <span class="o">*</span> <span class="n">heterodyne_1mhz_decim</span><span class="p">[</span><span class="bp">None</span><span class="p">,</span> <span class="p">:]</span>
</code></pre></div></div>

<p>While the channel I/Q output samples are not identical to the previous case due to a phase shift, 
the result after GFSK modulation is the same:</p>

<p><a href="/assets/polyphase/ble/chan_33_time_plot_het_post.svg"><img src="/assets/polyphase/ble/chan_33_time_plot_het_post.svg" alt="BLE Channel 33 decoding with 1 MHz heterodyne after decimation " /></a>
<em>(Click to enlarge)</em></p>

<p>This seems like a whole lot of effort for little benefit. Yes, we are running all operations at the output
sample rate, but the number of multiplications per output sample is now higher than the case with 
the input heterodyne!</p>

<p>But remember: this is for the generic case, with a random frequency offset. Let’s fix that.</p>

<h1 id="simplifying-for-the-half-bin-offset-case">Simplifying for the Half-Bin Offset Case</h1>

<p>As mentioned at the start of this blog post, it’s common to have a frequency offset that
is equal to half the channel width:</p>

\[F_\text{offset} = \frac{F_s}{2 M} \\
\omega_\Delta = 2 \pi \frac{F_\text{offset}}{F_s} = \frac{2 \pi}{2 M} = \frac{\pi}{M}\]

<p>A crucial observation is that 2 of our adjustment exponentionals feature a 
multiplication by \(M\).</p>

<p>The filter coefficients adjustment:</p>

\[e^{-j \omega_\Delta (kM)} = e^{-j \frac{\pi}{M} (kM)} = e^{-j \pi k } = (-1)^k\]

<p>The output rotator:</p>

\[e^{j \omega_\Delta (Mn)} = e^{j \frac{\pi}{M} (Mn)} = e^{j \pi n } = (-1)^n\]

<p>Awesome!  The general equation has been simplified to this:</p>

\[y_c[n] = (-1)^n
         \sum_{m=0}^{M-1}  
         \underbrace{e^{-j \omega_{\Delta} m}}_{\text{phase adjustment}} 
         \underbrace{e^{j \frac{2 \pi}{M} c \, m}}_{\text{IFFT}}  
         \sum_{k=0}^{N-1} h[kM + m]  
         (-1)^k
         \; x'[(n - k)M - m]\]

<p>The filter coefficients are real again and the complex multiplier for the output rotator
can be replaced by logic that just inverts the sign of the output samples for each time tick.</p>

<p><a href="/assets/polyphase/ble/ble-polyphase_ifft_post_half_bin.svg"><img src="/assets/polyphase/ble/ble-polyphase_ifft_post_half_bin.svg" alt="Post-decimation frequency adjust for half-bin offset" /></a>
<em>(Click to enlarge)</em></p>

<p>This is so much better! But it’s <em>still</em> possible to do better, though the requirements
become even stricter.</p>

<h1 id="the-odd-case-of-an-odd-number-of-channels">The Odd Case of an Odd Number of Channels</h1>

<p>We are currently still stuck with the per-phase complex rotator:</p>

\[e^{-j \omega_\Delta m}\]

<p>When the channel center frequencies are offset by half the channel width, we’ve so far
only considered an adjustment where the correction offset is half the channel bandwidth:</p>

\[\omega_\Delta = \frac{\pi}{M}\]

<p>Relative to the full channel bandwidth of \(\frac{2 \pi}{M}\), this offset is \(r=0.5\).</p>

\[\omega_\Delta = \frac{ 2 \pi }{M} r\]

<p>But \(r\) doesn’t have to be 0.5: we can use any kind of offset, as long as the fractional
part of the value is 0.5.</p>

<p>For example, when \(r = 2.5\), the channelizer still works, but in addition to a fractional
shift of half the channel width, there is an additional shift of 2 full channels. An output
sample that would go to channel \(k\) for an offset of 0.5 now goes to channel \(k+2\) instead.
Not the exactly the same result, but this reassigned output channel is just a minor bookkeeping
issue.</p>

<p><img src="/assets/polyphase/ble/ble-offset_of_2.5.svg" alt="Spectrum with integer channel offset of 2" /></p>

<p>Let’s see what happens when \(r=M/2\).</p>

<p>For even values of M, \(r\) is an integer value, without the fractional 0.5 half-bin
offset that we need:</p>

<p><img src="/assets/polyphase/ble/ble-offsest_of_m_div2_even.svg" alt="Spectrum with even channels moved by M/2" /></p>

<p>For odd values of M, we get the half-bin offset and all channels are moved by \(\frac{M-1}{2}\) at the output.</p>

<p><img src="/assets/polyphase/ble/ble-offsest_of_m_div2_odd.svg" alt="Spectrum with integer channel offset of M/2 = 3.5" /></p>

<p><em>harris shows this graphically with phase adjust values on a unity circle, but the principle is the same.</em></p>

<p>Let’s see what \(r=M/2\) does to the phase adjust term:</p>

\[r = M/2   \\
\omega_\Delta = \frac{ 2 \pi }{M} \frac{M}{2} \\
\omega_\Delta = \pi \\
e^{-j \omega_\Delta m} = e^{-j \pi m} = (-1)^m\]

<p>Nothing changes for the 2 other terms: for odd values of M, they still reduce to \((-1)^k\) and \((-1)^n\).</p>

<p>Conclusion: for odd values of M, we can do a half-bin frequency offset without an additional complex
multiplier! Flipping the sign of some sub-filter output values and reassigning the output channel
numbers is all that it takes.</p>

<p><a href="/assets/polyphase/ble/ble-polyphase_ifft_odd_m.svg"><img src="/assets/polyphase/ble/ble-polyphase_ifft_odd_m.svg" alt="DSP pipeline for odd M and half-bin offset" /></a>
<em>(Click to enlarge)</em></p>

<h1 id="reducing-the-number-of-phase-adjustment-values">Reducing the Number of Phase Adjustment Values</h1>

<p>We can expand this trick for cases where M is even but its number of prime factors 2 is low.
Let’s do the exercise for \(M = 18\) and select \(r = \frac{M}{4} = \frac{18}{4} = 4.5\).</p>

\[r = M/4   \\
\omega_\Delta = \frac{ 2 \pi }{M} \frac{M}{4} \\
\omega_\Delta = \frac{\pi}{2}  \\
e^{-j \omega_\Delta m} = e^{-j \frac{\pi}{2} m} = 1, -j, -1, j, 1, \dots\]

<p>We didn’t get rid of the complex term, but we can implement these factors with a sign flip
and/or swapping the real and imaginary part of the sub-filter outputs.</p>

<p>In general, if the following it true:</p>

\[M = 2^p K, \quad K &gt; 2\]

<p>Then you should choose \(r\) as follows:</p>

\[r = \frac{M}{2^{p+1}} = \frac{K}{2}\]

<p>When \(p=0\), you get the case where M is odd, and adjustment factors of \({-1,1}\).
When \(p=1\), the adjustment factors are \({-1,1, j, -j}\).
For larger values of \(p\), you can’t avoid a complex multiplier, but at least you will
limit the number adjustment values, which can be useful if you have 1 complex multiplier
that serially processes all the sub-filter outputs before sending them to the IFFT.</p>

<p>For the BLE example:</p>

\[M = 48 = 16 \cdot 3 = 2^4 \cdot 3 \\
r =  \frac{48}{2^5} = 1.5\]

<p>With this configuration, the phase adjustment term wraps around at phase 32, so we only need a
lookup table of 32 instead of 48 if we choose \(r=0.5\).<sup id="fnref:lut" role="doc-noteref"><a href="#fn:lut" class="footnote" rel="footnote">4</a></sup></p>

<h1 id="conclusion">Conclusion</h1>

<p>Just like in previous blog post, we started with a straightfoward solution to a problem
that worked, but that required significant mathematical resources. We then threw
some math at it and added constraints to simplify the math even more.</p>

<p>The outcome is once again appealing: for all decimation factors, the common case of
shifting the spectrum by half the width of a channel requires at most one additional
complex multiplication at the output of each sub-filter of the polyphase bank. And even
this multiplication can be removed entirely if we can choose a decimation factor that
is odd or if it only has one prime factor of 2.</p>

<p><em>All words in this blog post were written by a human.</em></p>

<h1 id="references">References</h1>

<ul>
  <li>
    <p><a href="https://www.youtube.com/watch?v=afU9f5MuXr8">Youtube - Recent Interesting and Useful Enhancements of Polyphase Filter Banks: fred harris</a></p>
  </li>
  <li>
    <p><a href="https://dsp.stackexchange.com/questions/96042/understanding-polyphase-filter-banks">Stackexchange - Understanding Polyphase Filter Banks</a></p>
  </li>
  <li>
    <p><a href="https://www.dsponlineconference.com/WPMC_2020_Even_and_Odd_Bin%20Centers_5.pdf">Analysis Channelizers with Even and Odd Indexed Bin Centers - fred harris</a></p>
  </li>
  <li>
    <p><a href="https://ieeexplore.ieee.org/document/1193158">IEEE - Digital Receivers and Transmitters Using Polyphase Filter Banks for Wireless Communications</a></p>
  </li>
</ul>

<p><strong>Other blog posts in this series</strong></p>

<ul>
  <li><a href="/2026/01/25/Notes-on-Basic-Polyphase-Decimation.html">Notes about Basic Polyphase Decimation Filters</a></li>
  <li><a href="/2026/02/07/Complex-Heterodyne.html">Complex Heterodynes Explained</a></li>
  <li><a href="/2026/02/16/Polyphase-Channelizer.html">The Stunning Efficiency and Beauty of the Polyphase Channelizer</a></li>
</ul>

<p><strong>Source code</strong></p>

<ul>
  <li><a href="https://github.com/tomverbeure/polyphase_blog_series">GitHub - Polyphase Filtering Blog Series</a></li>
</ul>

<h1 id="footnotes">Footnotes</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:gram_schmidt" role="doc-endnote">
      <p>You can use <a href="https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process">Gram-Schmidt decorrelation</a>
             to fix the I/Q vectors, supposedly, but I haven’t explored that yet. <a href="#fnref:gram_schmidt" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:33" role="doc-endnote">
      <p>Channel zero is located at 2441 MHz. Channel numbers increment up to 24 the top frequency
   is reached, after which the frequency rolls over to the bottom and channel numbers continue
   to increment. That’s how you end up with 33. <a href="#fnref:33" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:atan2" role="doc-endnote">
      <p>The \(\text{atan2}(q,i)\) function differs from \(\arctan(\frac{q}{i})\) function in the
      sense that the former works in all 4 quadrants whereas the latter only works in 1 quadrant.
      For DSP, you almost always need the 4 quadrant version. <a href="#fnref:atan2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:lut" role="doc-endnote">
      <p>This lookup table can be reduced further by exploiting symmetry along the circle. <a href="#fnref:lut" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">The Stunning Efficiency and Beauty of the Polyphase Channelizer</title><link href="https://tomverbeure.github.io/2026/02/16/Polyphase-Channelizer.html" rel="alternate" type="text/html" title="The Stunning Efficiency and Beauty of the Polyphase Channelizer" /><published>2026-02-16T10:00:00+00:00</published><updated>2026-02-16T10:00:00+00:00</updated><id>https://tomverbeure.github.io/2026/02/16/Polyphase-Channelizer</id><content type="html" xml:base="https://tomverbeure.github.io/2026/02/16/Polyphase-Channelizer.html"><![CDATA[<p><em>All words in this blog post were written by a human being.</em></p>

<script async="" src="https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=TeX-AMS_CHTML"></script>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#where-we-left-things-last-time" id="markdown-toc-where-we-left-things-last-time">Where We Left Things Last Time</a></li>
  <li><a href="#sidestep-ignoring-linear-phase-fir-coefficient-symmetry" id="markdown-toc-sidestep-ignoring-linear-phase-fir-coefficient-symmetry">Sidestep: Ignoring Linear Phase FIR Coefficient Symmetry</a></li>
  <li><a href="#naive-performance-baseline" id="markdown-toc-naive-performance-baseline">Naive Performance Baseline</a></li>
  <li><a href="#straightforward-polyphase-filtering-and-decimation" id="markdown-toc-straightforward-polyphase-filtering-and-decimation">Straightforward Polyphase Filtering and Decimation</a></li>
  <li><a href="#a-free-running-rotator" id="markdown-toc-a-free-running-rotator">A Free-Running Rotator</a></li>
  <li><a href="#from-low-pass-to-band-pass-filter" id="markdown-toc-from-low-pass-to-band-pass-filter">From Low Pass to Band Pass Filter</a></li>
  <li><a href="#disappearing-the-complex-rotator" id="markdown-toc-disappearing-the-complex-rotator">Disappearing the Complex Rotator</a></li>
  <li><a href="#moving-another-rotator-behind-the-filter-and-more-again" id="markdown-toc-moving-another-rotator-behind-the-filter-and-more-again">Moving Another Rotator behind the Filter and More… Again</a></li>
  <li><a href="#the-polyphase-channelizer" id="markdown-toc-the-polyphase-channelizer">The Polyphase Channelizer</a></li>
  <li><a href="#from-theory-to-practice" id="markdown-toc-from-theory-to-practice">From Theory to Practice</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
  <li><a href="#footnotes" id="markdown-toc-footnotes">Footnotes</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>In the past 2 blog posts, I wrote about 
<a href="/2026/01/25/Notes-on-Basic-Polyphase-Decimation.html">polyphase decimation filters</a>
and <a href="/2026/02/07/Complex-Heterodyne.html">complex heterodynes</a>, the latter with
some decimation thrown in for good measure.</p>

<p>It’s now time to put everything together, and more. First, I’ll look at
the complex heterodyne/decimation combo and see how it can be implemented as
efficiently as possible. There’s already some surprises in there, but to
top it off, I’ll expand the solution to do the operation for multiple
channels in parallel.</p>

<p>The result is amazing.</p>

<p>I’m still roughly following the flow of 
<a href="https://www.youtube.com/watch?v=afU9f5MuXr8">fred harris’ video about polyphase filter banks</a><sup id="fnref:harris" role="doc-noteref"><a href="#fn:harris" class="footnote" rel="footnote">1</a></sup>,
but I’ll be making some detours along the way because they helped me to put things
better in context and help me with understanding the topic.</p>

<p>There’s a lot more math<sup id="fnref:math" role="doc-noteref"><a href="#fn:math" class="footnote" rel="footnote">2</a></sup> this time around, out of necessity: some of the optimizations
can’t be figured out with intuition alone. But the math consist almost exclusively of
shuffling around sums and products of scalar values and complex exponentials, with a
convolution here and there.</p>

<p>For those who don’t want to read previous installments of this series, check out the section with
<a href="/2026/02/07/Complex-Heterodyne.html#some-common-dsp-notations">Some Common DSP Notations</a>
if you need a quick refresher about the meaning of some of the symbols.</p>

<p>The NumPy code that was used to create the plots in this series can be found
<a href="https://github.com/tomverbeure/polyphase_blog_series">here</a>.</p>

<h1 id="where-we-left-things-last-time">Where We Left Things Last Time</h1>

<p>I ended my <a href="/2026/02/07/Complex-Heterodyne.html">blog post about complex heterodynes</a>
with a question about the efficiency of implementing them as a low pass filter that is 
followed by a decimation. In <a href="https://youtu.be/afU9f5MuXr8?t=552">the video</a>, harris
calls this the Armstrong<sup id="fnref:armstrong" role="doc-noteref"><a href="#fn:armstrong" class="footnote" rel="footnote">3</a></sup> heterodyne.</p>

<p>Here’s a quick recap of that pipeline:</p>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-rot_lpf_decim.svg" alt="Rotator, LPF, decimator" /></p>

<ul>
  <li>\(f_c\) is the normalized center frequency of the channel that we’re interested in. 
In our example, the sample rate \(F_s = 100 \text{MHz}\) and the channel center frequency
\(F_c = 20 \text{MHz}\) so \(f_c = 0.2\). Further down, I’ll often use \(\theta_c = 2 \pi f_c\)
because that makes equations less cluttered.</li>
  <li>\(e^{-j 2 \pi f_c n}\) is a rotator. When multiplied with the input signal, it shifts down a 
channel with center frequency \(F_c\) down to 0 Hz. That’s the complex heterodyne.</li>
  <li>\(H_\text{lpf}(z)\) is a low-pass FIR filter with 201 real taps and a linear phase<sup id="fnref:linear_phase" role="doc-noteref"><a href="#fn:linear_phase" class="footnote" rel="footnote">4</a></sup>. It
removes all the frequencies outside the -5 MHz to 5 MHz range.</li>
  <li>Each channel has a 10 MHz bandwidth. Since there is no mirror spectrum due to the complex
heterodyne, once the channel has been moved to 0 Hz, we can decimate by a factor 10 so that
the range from -5 MHz to 5 MHz is all that’s left.</li>
</ul>

<p>Check out my <a href="/2026/02/07/Complex-Heterodyne.html#some-common-dsp-notations">section with common DSP notations</a>
for a general overview of symbols used in DSP math formulas.</p>

<h1 id="sidestep-ignoring-linear-phase-fir-coefficient-symmetry">Sidestep: Ignoring Linear Phase FIR Coefficient Symmetry</h1>

<p>Linear phase FIR filters have the desirable property that their coefficients are symmetric
around the center tap. Here’s a random example:</p>

\[H(z) = -1 + 3 z^{-1} - 6 z^{-2} + 10 z^{-3} - 6 z^{-4} + 3 z^{-5} - z^{-6}\]

<p>This filter has 7 coefficients, the center coefficient is 10, the ones to the left and right of 
it are both -6 and so forth.</p>

<p>When you convert a DSP algorithm to hardware that needs to consume an input sample and produce
and output for every clock tick, the straightforward implementation is to have one multiplier per
coefficient<sup id="fnref:multiplier" role="doc-noteref"><a href="#fn:multiplier" class="footnote" rel="footnote">5</a></sup>.</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-fir_no_optimized_muls.svg" alt="FIR without optimized multipliers" /></p>

<p>Since multipliers are often a scarce resource, you can reduce their number by
almost half by rearranging the equation as follows:</p>

\[H(z) = -1 (1 + z^{-6})  + 3 (z^{-1} + z^{-5} ) - 6 (z^{-2} + z^{-4})  + 10 z^{-3}\]

<p>We’ve removed 3 out of 7 multipliers.</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-fir_optimized_muls.svg" alt="FIR with optimized multipliers" /></p>

<p>This works, but you need to trade off the reduction in multipliers against an increase in wiring
to get the 2 operands to the addition that feeds the multiplier. On FPGAs, wiring congestion is
a real concern so it’s not always a slam dunk.</p>

<p>If you have a hardware architecture where delayed inputs are stored in a RAM instead of individual
registers and you use an FSM to execute the filter over multiple clock cycles, trying to do this
trick can make scheduling transactions more complicated too.</p>

<p>And when converting the FIR filter into its polyphase form, the simple symmetry breaks entirely. Here’s
an example of a symmetrical 19-tap filter. In its original form, coefficients are symmetric, but
when split up into 10 phases, the symmetry inside each phase is gone.</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-tap_symmetry_19.svg" alt="19-tap filter split up into 10 phases" /></p>

<p>It’s still possible to share multiplications if you merge multiple phases, note how phase 2 has
coefficients 6 and 2 and phase 7 has coefficients 2 and 6, but that again makes data organization
and movement more difficult.</p>

<p>For the remainder of this blog post, I will ignore symmetry related optimizations when calculating
the number of multiplications.</p>

<h1 id="naive-performance-baseline">Naive Performance Baseline</h1>

<p>I will use multiplication as the main indicator by which to judge the efficiency of a DSP algorithm.</p>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-rot_lpf_decim.svg" alt="Rotator, LPF, decimator" /></p>

<p>Let’s evaluate the number of multiplications for the naive architecture:</p>

<ul>
  <li>The complex rotator multiplies a real sample with a complex number or 2 multiplications per operation
and 200M per second.</li>
  <li>The low pass filter has 201 real taps, for a total of 201 x 2 x 100M = 
40.2B operations per second.</li>
</ul>

<p>Total: 40.4B multiplications per second!</p>

<p>This is our baseline, and it’s a lot. Let’s see what we can do about this…</p>

<h1 id="straightforward-polyphase-filtering-and-decimation">Straightforward Polyphase Filtering and Decimation</h1>

<p>There’s a reason why I also wrote 
<a href="/2026/01/25/Notes-on-Basic-Polyphase-Decimation.html">Notes about Basic Polyphase Decimation Filters</a>:
it discusses exactly this kind of scenario, the combo of an FIR filter followed by a decimation. Yes,
there’s a complex rotator in front of the FIR filter, but for now we can keep it there 
while we transform the FIR/decimator to its polyphase form.</p>

<p>harris mentions this case only tangentially, but it’s useful to compare how well the straightforward
polyphase filter bank performs compared to the naive solution.</p>

<p>First split the FIR filter into its polyphase form with 10 sub-filters, the decimation factor:</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-complex_het_polyphase_decim.svg" alt="Complex heterodyne - Polyphase - Decimation" /></p>

<p>Apply the <a href="/2026/01/25/Notes-on-Basic-Polyphase-Decimation.html#the-noble-identity-for-decimation">noble identity for decimation</a>:</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-complex_het_decim_polyphase.svg" alt="Complex heterodyne - Decimation - Polyphase" /></p>

<p>Moving the FIR filter operation behind the decimator is a huge savings. The complex rotator still counts
for 200M multiplications per second, but the combined 201 taps now need to deliver samples at a 10 times
lower rate, 201 x 2 x 10M = 4.02B operations per second, for a total of 4.22B operations per second. If
it weren’t for the complex rotator, the savings ratio is exactly the decimation factor.</p>

<p>The biggest problem with this arrangement is that the rotator is in front of the decimator and there is
no obvious way to move it behind the decimator. If the DSP pipeline is implemented in an FPGA and the
input sample clock is very high, the multiplier hardware may simply not be fast enough.</p>

<h1 id="a-free-running-rotator">A Free-Running Rotator</h1>

<p>One minor thing to note is that the rotator consists of the input signal being multiplied by
the output of a free-running oscillator. Free-running implies that there are no restrictions on 
the starting phase of the oscillator.</p>

<p>In the previous diagram, sample \(x[n]\) is multiplied by \(e^{-j \theta_c n}\), sample \(x[n+1]\) by 
\(e^{-j \theta_c (n+1)}\), and so forth, but that’s really arbitrary. We could multiply \(x[n]\) by \(e^{-j \theta_c (n+1)}\) 
and \(x[n+1]\) by \(e^{-j \theta_c (n +2)}\) and the outcome in terms of frequency characteristics 
wouldn’t be materially different (though there would be constant phase shift.)</p>

<p>What is true is that you have to continuously loop through all the values of the rotator, irrespective
of the length of the number of filter taps: if the rotator completes a full rotation in 128<sup id="fnref:steps" role="doc-noteref"><a href="#fn:steps" class="footnote" rel="footnote">6</a></sup> steps, then
you’ll need a table or a calculation<sup id="fnref:unity_point_calculation" role="doc-noteref"><a href="#fn:unity_point_calculation" class="footnote" rel="footnote">7</a></sup> to produce 128 points around the unity circle.</p>

<p>We’ll soon see that this isn’t the case in other schemes.</p>

<h1 id="from-low-pass-to-band-pass-filter">From Low Pass to Band Pass Filter</h1>

<p>Let’s undo the previous polyphase optimization, start again from the naive solution, and try 
something different.</p>

<p>So far, we have been heterodyning the channel of interest to the baseband and then sent it through a low-pass filter, 
as seen in the plot from previous blog post:</p>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-low_pass_filter.svg" alt="Complex heterodyne followed by low pass filter spectrum" /></p>

<p>Can we turn the order around, first send the channel of interest through a band-pass filter and then heterodyne
the result down to baseband? As <a href="https://youtu.be/afU9f5MuXr8?t=597">harris points out</a>, the Armstrong heterodyne
was created to avoid that, because a movable band-pass filter requires mechanically tuned capacitors and inductors.
In the DSP world, however, it’s just numbers and calculations.</p>

<p>So, yes, we can do the filtering first and then do the heterodyne, and it’s relatively easy to show that mathematically.</p>

<p><em>In what follows, I will deviate from the harris’s notation in 2 ways. He uses \(a[n] * b[n]\) for convolution. \((a * b)[n]\)
is the more common way. He also overloads the meaning of \(n\) in the same equation, in a way that I found utterly
confusing. Instead, I will use the \([\cdot]\) and \((\cdot)\) notation, where \(\cdot\) is essentially a
temporary local loop variable. If you see a \(\cdot\) in the equations below, assume that harris had a \(n\) there.</em></p>

<p>Starting with this:</p>

\[y[n] = \big( \underbrace{ \underbrace{(x[\cdot] e^{-j \theta_c (\cdot)})}_{\text{heterodyne}} * h\big)[n]}_{\text{low-pass filter}}\]

<p>\(*\) is the convolution operator, in this case, a 
<a href="https://en.wikipedia.org/wiki/Convolution#Discrete_convolution">discrete convolution</a>.
Let’s expand the equation by applying the definition of the convolution:</p>

\[y[n] = \sum_{k=0}^{N-1} (x[n-k] e^{-j \theta_c (n-k)}) \; h[k]\]

<p>\(N\) is the number of coefficients of the filter.</p>

<p>Extract the common exponential term that doesn’t depend on \(k\):</p>

\[y[n] = e^{-j \theta_c n} \sum_k x[n-k] \; ( e^{j \theta_c k} h[k] )\]

<p>Reduce back to a convolution operator:</p>

\[\begin{alignedat}{0}
y[n] &amp; = &amp; e^{-j \theta_c n} \; \big( x * (h[\cdot] e^{j \theta_c (\cdot)} ) \big)[n] \\ 
     &amp; = &amp; \big( x * (h[\cdot] e^{j \theta_c (\cdot)} ) \big)[n] \; e^{-j \theta_c n}
\end{alignedat}\]

<p>We’ve just proven what, <a href="https://youtu.be/afU9f5MuXr8?t=985">in the video</a>, harris calls the 
<em>Equivalency Theorem</em>:</p>

\[\big(( x[\cdot] e^{-j \theta_c (\cdot)} ) * h\big)[n] = e^{-j \theta_c n} \; \big( x * (h[\cdot] e^{j \theta_c (\cdot)} ) \big)[n]\]

<p>There’s one minor comment about this: while Google turns up plenty of equivalency
theorems, none of them deal with the swapping around a heterodyne and convolution. The only reference<sup id="fnref:equivalency" role="doc-noteref"><a href="#fn:equivalency" class="footnote" rel="footnote">8</a></sup> 
that I found was in section 6.1 of his own book, 
<a href="https://www.amazon.com/Multirate-Processing-Communication-Systems-Publishers-dp-877022210X/dp/877022210X/">Multirate Signal Processing for Communication Systems</a><sup id="fnref:book" role="doc-noteref"><a href="#fn:book" class="footnote" rel="footnote">9</a></sup>,
which has the same formulas and figures as the one of the video. It says:</p>

<blockquote>
  <p>The equivalency theorem states that the operations of a down-conversion followed by a low-pass 
filter are totally equivalent to the operations of a band-pass filter followed by a down-conversion.</p>
</blockquote>

<p>Anyway, this transformation doesn’t look like an improvement, and it will take a while before we 
can see how this helps us.  For now, let’s break the equation into pieces and look at them step by step.</p>

\[h[\cdot] e^{j \theta_c (\cdot)}\]

<p>The coefficients of the low-pass filter with transfer function  \(H_\text{lpf}(z)\) are each multiplied by 
a value of a rotator. Notice how the \(-\) sign in front of the \(j\) exponent of the rotator has
disappeared: when we were heterodyning the channel, we were bringing the spectrum <em>down</em> to baseband.
Now, we’re doing the opposite and heterodyning the low-pass filter <em>up</em> to channel band!</p>

<p>Let’s apply the equation above to an example. If the transfer function of the original filter is this:</p>

\[H_\text{lpf}(z) = h_0 z^{0} + h_1 z^{-1} + h_2 z^{-2} + h_3 z^{-3} + h_4 z^{-4}\]

<p>Then the new filter is this:</p>

\[\begin{alignedat}{0}
H_\text{bpf}(z) &amp; = &amp; h_0 e^{j \theta_c 0} z^{0} &amp;+&amp; h_1 e^{j \theta_c 1} z^{-1} &amp;+&amp; h_2 e^{j \theta_c 2} z^{-2} &amp;+&amp; h_3 e^{j \theta_c 3} z^{-3} &amp;+&amp; h_4 e^{j \theta_c 4} z^{-4} \\
                &amp; = &amp; h_0 (e^{-j \theta_c} z)^{0} &amp;+&amp; h_1 (e^{-j \theta_c} z)^{-1} &amp;+&amp; h_2 (e^{-j \theta_c} z)^{-2} &amp;+&amp; h_3 (e^{-j \theta_c} z)^{-3} &amp;+&amp; h_4 (e^{-j \theta_c} z)^{-4} \\
\end{alignedat}\]

<p>This can be written much shorter, useful for drawings, like this:</p>

\[H_\text{bpf}(z) = H_\text{lpf}(e^{-j \theta_c} z)\]

<p>It is important to note that the coefficients of \(H_\text{bpf}(z)\) are constants: for a given center frequency, we can
pre-calculate the coefficients and never change them again. And contrary to the free-running rotator that
shifted down the spectrum of the input signal, the number of rotator values to shift up the filter is fixed to the
number of filter taps. However, compared to the original filter \(H_\text{lpf}(z)\), the coefficients are now complex 
instead of real.</p>

<p>To simulate the behavior of this band-pass filter, we create an array with as many complex rotator values
as there are filter taps and multiply them with the low-pass filter coefficients from previous blog:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tap_idx</span>       <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">arange</span><span class="p">(</span><span class="n">LPF_FIR_TAPS</span><span class="p">)</span>
<span class="n">complex_lo</span>    <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="mf">1j</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">pi</span> <span class="o">*</span> <span class="n">lo_freq_hz</span> <span class="o">*</span> <span class="n">tap_idx</span> <span class="o">/</span> <span class="n">sample_clock_hz</span><span class="p">)</span>
<span class="n">h_bpf_complex</span> <span class="o">=</span> <span class="n">h_lpf</span> <span class="o">*</span> <span class="n">complex_lo</span>
</code></pre></div></div>

<p>Looking at the spectrum of this filter, there are no surprises: the filter has been transformed from a
low-pass filter to a band-pass filter with \(F_c = 20 \text{MHz}\) as center frequency:</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het_sim-bpf_complex_filtered.svg" alt="Bandpass filter spectrum and filtered input signal" /></p>

<p>The second plot of the figure above shows the input signal after applying the band-pass filter.</p>

\[x[n] * h_\text{bpf}[n]\]

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">signal_bpf_complex</span>  <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">convolve</span><span class="p">(</span><span class="n">signal</span><span class="p">,</span> <span class="n">h_bpf_complex</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"same"</span><span class="p">)</span>
</code></pre></div></div>

<p>The final step shifts the filtered signal back to baseband:</p>

\[y[n] = ( x[n] * h_\text{bpf}[n] ) \; e^{-j \theta_c n}\]

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-bpf_het_decim.svg" alt="Pipeline with band-pass filter, heterodyne and decimation" /></p>

<p>After decimation, we end up with <a href="/2026/02/07/Complex-Heterodyne.html#decimation">the same result in the previous blog post</a>:</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het_sim-signal_bfp_filtered_decim_complex.svg" alt="Bandpass filter, followed by heterodyne and decimation" /></p>

<p>Cool! But what did we gain?</p>

<p>The input to the filter is now real instead of complex, but the coefficents are now complex instead of 
real. So the number of multiplications in the filter remains the same. And the heterodyne now multiplies 2 
complex numbers instead of multiplying a real input with a complex. We’ve regressed!</p>

<p>But that’s something that will be fixed in the next section…</p>

<h1 id="disappearing-the-complex-rotator">Disappearing the Complex Rotator</h1>

<p>In the straightforward case, we had to switch a polyphase decomposition to move the decimator from behind
the filter to in front of the filter. But that decomposition introduces single timestep delays which
prevents moving the decimator even further to the front of the rotator.</p>

<p>This is not the case anymore: the rotator is behind the filter and there are no delay elements. This 
allows us to move the decimator before the rotator.</p>

<p>Here’s the rotator before decimation:</p>

\[e^{-j \theta_c n}\]

<p>When we decimate by a factor of M, the rotator completes a circle by a factor M less steps than before
the decimation. Or the angle by which the rotator moves forward each step is now M times larger.</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-decimated_phasor.svg" alt="Original vs decimated rotator" /></p>

<p>After decimation, the exponent of the rotator now has factor \(M\) add to it:</p>

\[e^{-j \theta_c M m}, m = \lfloor \frac{n}{M} \rfloor\]

<p>where \(\lfloor x \rfloor\) means “\(x\) rounded down to the closest integer number”.</p>

<p>Since the decimator and the rotator have swapped positions, the earlier problem of having to run the
rotator at the input sample rate has been solved!</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-bpf_decim_het.svg" alt="Pipeline with band-pass filter, decimation, and heterodyne" /></p>

<p>But we can do better! The rotator can disappear entirely if its value is equal to one at all times.</p>

\[e^{-j \theta_c M m} \stackrel{?}{=} 1 + 0j\]

<p>We don’t want this to be dependent on \(m\), so we’re really finding a solution for this equation:</p>

\[e^{-j \theta_c M } \stackrel{?}{=} 1 + 0j\]

<p>The rotator is one whenever it makes a full circle or whenever the exponent is an integer multiple of \(2 \pi\). 
\(m\) is an integer. If the equation above is satisfied without \(m\), then it will also be satisfied after 
adding \(m\) to the exponent.</p>

\[\theta_c M = k \; 2 \pi\]

<p>Replace \(\theta_c\) with its definition:</p>

\[2 \pi \frac{F_c}{F_s} M = k \; 2 \pi\]

<p>Simplify and rearrange:</p>

\[F_c = k \frac{F_s}{M} \\
\theta_c = \frac{2 \pi k}{M}\]

<p>It doesn’t seem like it, but this is a crucial result:</p>

<p><strong>If the center frequency of your channel is a multiple of the sample rate divided by the decimation 
factor, the decimated rotator will always evaluate to 1 and thus the multiplication disappears entirely.</strong></p>

<p>In our example with \(F_s=100 \text{MHz}\), \(M=10\), \(F_c=20 \text{MHz}\), this
equation is satisfied for \(k=2\), and we end up with this:</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-bpf_decim.svg" alt="Band-pass filter and decimation" /></p>

<p>If all of this feels a bit familiar, it’s probably because you’ve heard about 
<a href="https://en.wikipedia.org/wiki/Undersampling">undersampling or band-pass sampling</a>. 
It’s what happens when you deliberately violate the 
<a href="https://en.wikipedia.org/wiki/Nyquist–Shannon_sampling_theorem">Nyquist theorem</a>,
sample at a rate that is much lower than twice the bandwidth of a signal, but do it in such
a way that the spectrum of the signal aliases exactly where you want it to be: at baseband.</p>

<p><img src="/assets/polyphase/polyphase_het/sampling-undersampling.svg" alt="Undersampling" /></p>

<p>Band-pass sampling only works if there are no stray frequency components outside the channel,
which is why preprocessing the input with a band-pass filter is essential.</p>

<p>Even with complex filter coefficients, we can still do the polyphase decomposition and 
move the decimator before the set of filters:</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-decim_poly_bpf.svg" alt="Decimator, polyphase band-pass filter" /></p>

<p>Tadaa! All elements of the pipeline can now run at the decimated output sample rate.</p>

<p>Last time we checked, we needed 4.22B multiplications per second. With the complex rotator gone,
we’re now at 4.02B: just a filter with 201 complex taps, fed with a real value, executed 10M 
times per second.</p>

<p>A pitiful 5% savings is not worth writing home about, but we can do even better.</p>

<p><em>Note: even if we don’t satisfy the \(F_c = k \frac{F_s}{M}\) condition, we’re still better off
than before, because the rotator still runs at the output instead of the input sample rate:</em></p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-decim_poly_bpf_het.svg" alt="Pipeline with decimation, polyphase band-pass filters, and heterodyne" /></p>

<p><em>This blog post is already long as it is, so for this one, I’m focussing only on the case
where the center frequency condition is satisfied.</em></p>

<h1 id="moving-another-rotator-behind-the-filter-and-more-again">Moving Another Rotator behind the Filter and More… Again</h1>

<p>Let’s play another game of shuffling around sums and terms. So far, we’ve only engaged
the polyphase decomposition after the fact, to lower the number of filter calculations. 
This time we’re adding the polyphase decomposition explicitly to the mathematical mix
for additional benefits.</p>

<p>Here’s where we left it last time:</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-bpf_decim.svg" alt="Band-pass filter and decimation" /></p>

<p>Let’s move our attention to the transfer function of filter:</p>

\[\begin{alignedat}{0}
H_\text{bpf}(z) &amp; = &amp; H_\text{lpf}(e^{-j \theta_c} z) \\
           &amp; = &amp; \sum_{n=0}^{N-1} h[n] (e^{-j \theta_c } z)^{-n}  \\
           &amp; = &amp; h_0 e^{j \theta_c 0} z^{ 0} &amp;+&amp; h_1 e^{j \theta_c 1} z^{-1} &amp;+&amp; h_2 e^{j \theta_c 2} z^{-2} &amp;+&amp; 3 e^{j \theta_c 3}  z^{-3} &amp;+&amp; ... 
\end{alignedat}\]

<p>Do the polyphase decomposition. Instead of summing all the terms the full \(h[n]\) polynomial in one go,
we sum the terms of \(M\) different polyphase polynomials separately, and then add them together:</p>

\[= \sum_{m=0}^{M-1} \sum_{n=0}^{N-1} h[m + Mn] e^{j \theta_c (m + Mn)} z^{-(m+Mn)} \\\]

<p>When studying this step <a href="https://youtu.be/afU9f5MuXr8?t=1480">in the video</a>, it took
me a minute to understand what happened with \(h[n]\). In the first equation,
\(n = 0 ... N-1\), where \(N\) is the number of coefficients. In the equation above, 
the range of \(n\) doesn’t change, but now it’s used like this: \(h[m + Mn]\). The
maximum index of \(h\) now goes beyond the number of coefficients. This isn’t a problem,
though, as long as you keep in mind that \(h[n]\) is \(\color{red}{0}\) when \(n\) is smaller than 0
or larger than \(N-1\).</p>

<p>To make things really clear, let’s expand all these sums and products for a 9-tap
filter with decimation factor \(M=3\):</p>

\[\begin{alignedat}{0}
H_\text{bpf}(z) &amp; = &amp;  h_0 e^{j \theta_c 0} z^{ 0} &amp;+&amp; h_3 e^{j \theta_c 3} z^{-3} &amp;+&amp; h_6 e^{j \theta_c 6} z^{-6} &amp;+&amp; \color{red}{0} \, e^{j \theta_c 9}  z^{-9}  &amp;+&amp; ... &amp;&amp; \qquad (m = 0) \\
           &amp; + &amp;  h_1 e^{j \theta_c 1} z^{-1} &amp;+&amp; h_4 e^{j \theta_c 4} z^{-4} &amp;+&amp; h_7 e^{j \theta_c 7} z^{-7} &amp;+&amp; \color{red}{0} \, e^{j \theta_c 10} z^{-10} &amp;+&amp; ... &amp;&amp; \qquad (m = 1) \\
           &amp; + &amp;  h_2 e^{j \theta_c 2} z^{-2} &amp;+&amp; h_5 e^{j \theta_c 5} z^{-5} &amp;+&amp; h_8 e^{j \theta_c 8} z^{-8} &amp;+&amp; \color{red}{0} \, e^{j \theta_c 11} z^{-11} &amp;+&amp; ... &amp;&amp; \qquad (m = 2) \\
\end{alignedat}\]

<p>In each of the polyphase sub-filters, the factor \(e^{j \theta_c m} z^{-m}\) is not dependent on \(n\)
and can be moved ahead of the inner sum:</p>

\[=  \sum_{m=0}^{M-1} e^{j \theta_c m} z^{-m} \sum_{n=0}^{N-1} h[m + Mn] e^{j \theta_c Mn} z^{-Mn} \\\]

\[\begin{alignedat}{0}
H_\text{bpf}(z) &amp; = &amp; e^{j \theta_c 0} z^{ 0} &amp; \big(  h_0 e^{j \theta_c 0} z^{0} &amp;+&amp; h_3 e^{j \theta_c 3} z^{-3} &amp;+&amp; h_6 e^{j \theta_c 6} z^{-6} \big)  &amp;&amp; \qquad (m = 0) \\
           &amp; + &amp; e^{j \theta_c 1} z^{-1} &amp; \big(  h_1 e^{j \theta_c 0} z^{0} &amp;+&amp; h_4 e^{j \theta_c 3} z^{-4} &amp;+&amp; h_7 e^{j \theta_c 6} z^{-7} \big)  &amp;&amp; \qquad (m = 1) \\
           &amp; + &amp; e^{j \theta_c 2} z^{-2} &amp; \big(  h_2 e^{j \theta_c 0} z^{0} &amp;+&amp; h_5 e^{j \theta_c 3} z^{-5} &amp;+&amp; h_8 e^{j \theta_c 6} z^{-8} \big)  &amp;&amp; \qquad (m = 2) \\
\end{alignedat}\]

<p>Now look back at the previous section where we figured out the condition to eliminate the rotator. In the equation
above, we see \(e^{j \theta_c Mn }\), which contains \(e^{j \theta_c M }\). This is exactly the same rotator
that we eliminated before.</p>

<p>In other words, when using the same restriction \(F_c = k \frac{F_s}{M}\), the rotator in the products of the
inner sum simply disappears and we end up with this:</p>

\[=  \sum_{m=0}^{M-1} e^{j \theta_c m} z^{-m} \sum_{n=0}^{N-1} h[m + Mn] z^{-Mn} \\\]

\[\begin{alignedat}{0}
H_\text{bpf}(z) &amp; = &amp; e^{j \theta_c 0} z^{ 0} &amp; \big(  h_0 z^{0} &amp;+&amp; h_3 z^{-3} &amp;+&amp; h_6 z^{-6} \big)  &amp;&amp; \qquad (m = 0) \\
           &amp; + &amp; e^{j \theta_c 1} z^{-1} &amp; \big(  h_1 z^{0} &amp;+&amp; h_4 z^{-4} &amp;+&amp; h_7 z^{-7} \big)  &amp;&amp; \qquad (m = 1) \\
           &amp; + &amp; e^{j \theta_c 2} z^{-2} &amp; \big(  h_2 z^{0} &amp;+&amp; h_5 z^{-5} &amp;+&amp; h_8 z^{-8} \big)  &amp;&amp; \qquad (m = 2) \\
\end{alignedat}\]

<p>Or abbreviated:</p>

\[H_\text{bpf}(z) = \sum_{m=0}^{M-1} e^{j \theta_c m} z^{-m} H_m(z^M)\]

<p>Furthermore:</p>

\[\theta_c = k \frac{2 \pi}{M}\]

<p>So we end up with this:</p>

\[H_\text{bpf}(z) = \sum_{m=0}^{M-1} e^{j \frac{2 \pi}{M} k m} z^{-m} H_m(z^M)\]

<p>\(e^{j \frac{2 \pi}{M} k m}\) is a scalar value, so we can move the multiplication 
to the back of the filter:</p>

\[H_\text{bpf}(z) = \sum_{m=0}^{M-1} z^{-m} H_m(z^M) e^{j \frac{2 \pi}{M} k m}\]

\[\begin{alignedat}{0}
H_\text{bpf}(z) &amp; = &amp; z^{ 0} &amp; \big(  h_0 z^{0} &amp;+&amp; h_3 z^{-3} &amp;+&amp; h_6 z^{-6} \big) e^{j \frac{2 \pi}{M} k 0} \\
           &amp; + &amp; z^{-1} &amp; \big(  h_1 z^{0} &amp;+&amp; h_4 z^{-4} &amp;+&amp; h_7 z^{-7} \big) e^{j \frac{2 \pi}{M} k 1} \\
           &amp; + &amp; z^{-2} &amp; \big(  h_2 z^{0} &amp;+&amp; h_5 z^{-5} &amp;+&amp; h_8 z^{-8} \big) e^{j \frac{2 \pi}{M} k 2} \\
\end{alignedat}\]

<p>Here’s how that looks as a diagram:</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-poly_real_bpf_het_decim.svg" alt="Polyphase with real band-pass filter, heterodyne, decimator" /></p>

<p>As a final step, we can move the decimator back to the front by applying the noble identity on the 
polyphase sub-filters. Note that this time, the rotator exponent is not multiplied by \(M\), because
the exponent is a fixed value, not a changing rotator.</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-decim_poly_real_bpf_het.svg" alt="Polyphase with decimator, real band-pass filter, heterodyne" /></p>

<p>This is a truly remarkable outcome:</p>

<ul>
  <li>
    <p><strong>All math operations happen at a slow rate behind the decimators.</strong></p>

    <p>We can do this because of the noble identies that give us the polyphase
transformations and because the rotators are located after the filters.</p>
  </li>
  <li>
    <p><strong>The inputs to the filters are real.</strong></p>

    <p>We achieved this by applying the equivalency theorem.</p>
  </li>
  <li>
    <p><strong>The coefficients of the polyphase filters are real again.</strong></p>

    <p>We did this by extracting the rotators from the filters, and removing the 
frequency-dependent component.</p>
  </li>
  <li>
    <p><strong>The coefficients don’t depend on the targeted channel frequency.</strong></p>

    <p>We did this also by extracting the rotators from the filters (as long as the channel $k$ 
meets our criterion above of being an integer multiple of the decimation rate).</p>
  </li>
  <li>
    <p><strong>The rotators are located behind the filters.</strong></p>

    <p>This will be very important in the next section.</p>
  </li>
</ul>

<p>The importance of the last 2 points can’t be overstated: if you want to change the channel \(k\) that needs to
be brought to base band from one to another, all you need to change are the rotators.</p>

<p>Compared the last checkpoint, the resource requirements have also been reduced roughly by half:</p>

<ul>
  <li>the 201 filter taps are multiplied by a real input at a rate of 10M per second = 2.01B multiplications.</li>
  <li>10 rotators multiply the real output of the filters by a complex number at 10M per second = 200M multiplications.</li>
</ul>

<p>Total: 2.21B multiplications.</p>

<p>Our naive initial baseline was 40.4B multiplications per second, we’ve reduced that number by a factor of 20.</p>

<p>And still we are not done…</p>

<h1 id="the-polyphase-channelizer">The Polyphase Channelizer</h1>

<p>So far, we’ve focused on finding an optimal solution to extracting the signal of one channel out of a possible many 
to baseband. We’re now expanding our scope: what if we want to extract the signal of all channels in parallel?</p>

<p>This is where the conclusion of previous section pays off ever more: since only the final rotators are channel
dependent, all we need is an additional set of rotators for each channel. The filters remain untouched. That’s
a huge win: from the resource calculation, we can already conclude that the filters tend to be require the
large majority of multipliers. And that’s for a filter with 201 taps, which is relatively modest. In today’s
world, channels are often stacked one next to the other with a very narrow transistion band and narrow
transistion bands require very steep filters to separate one from the other.</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-polyphase_multi_chan.svg" alt="Polyphase channelizer with 2 channels" /></p>

<p>In the DSP pipeline above, 2 channels are brought to baseband resulting in 2 time domain signals \(s_k[n]\)
and \(s_l[n]\), but the number of channels that can be extracted efficiently is really only limited by the
decimation factor (due to the \(F_c = k F_s / M\) requirement.)</p>

<p>If we have a decimation factor of \(M\) and we want to extract \(M\) channels in parallel, then we’re looking at
\(M (M-1)\)<sup id="fnref:nr_rotators" role="doc-noteref"><a href="#fn:nr_rotators" class="footnote" rel="footnote">10</a></sup> rotators or \(2 M (M-1)\) multipliers. In his video, harris talks about 
<a href="https://youtu.be/afU9f5MuXr8?t=2075">a polyphase channelizer with 65536 channels</a>. There are many cases where
\(O(n^2)\) is good enough and suddenly it’s not. 4,294,901,760 complex multiplications to calculate 65536 output samples
is not good enough.</p>

<p>Let’s look at the rotator section for a single channel:</p>

\[s_k[n] = \sum_{m=0}^{M-1} y_m[n] e^{j \frac{2 \pi}{M} k \, m}\]

<p>This calculation must be done for each output time step \(n\), so let’s drop that index. And while we’re at it,
let’s group the outputs \(y_m\) of the filters into their own array, so that we can reference them, for each
time step \(n\), as \(y[m]\) instead of \(y_m\):</p>

\[s_k = \sum_{m=0}^{M-1} y[m] e^{j \frac{2 \pi}{M} k \, m}\]

<p>Does this equation ring a bell? Compare against this:</p>

\[x[n] = \frac{1}{N} \sum_{k=0}^{N-1}{X[k] e^{j \frac{2 \pi}{N} n \, k } }\]

<p><em>Don’t confuse the meanings of \(k\), \(m\), and \(n\). The \(k\) in the first equation
matches the purpose of \(n\) in the second one!</em></p>

<p>This is the definition of inverse<sup id="fnref:inverse" role="doc-noteref"><a href="#fn:inverse" class="footnote" rel="footnote">11</a></sup> 
<a href="https://en.wikipedia.org/wiki/Discrete_Fourier_transform">discrete Fourier transform (DFT)</a>!
Except for the front scaling factor, our equation has the same form.</p>

<p>If, for each time step \(n\), we want the output samples of all channels \(0..M-1\), the DFT will give us
exactly that. That in itself doesn’t solve our problem: it is well known that a naive DFT
implementation has \(O(n^2)\) behavior. But in DSP land, it’s impossible to mention the discrete
Fourier transform without immediately bringing up the 
<a href="https://en.wikipedia.org/wiki/Fast_Fourier_transform">Fast Fourier transform (FFT)</a>,
which has \(O(n \log n)\) behavior.</p>

<p>For a 65536 channel polyphase channelizer, the FFT brings down the number of complex multiplications
from 4,294,901,760 down to 1,048,576. We’re received a second boost-of-efficiency miracle.</p>

<p>Finally, we’re at the end of a journey that gives us this wonderful result:</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het-polyphase_ifft.svg" alt="Polyphase filters, IFFT" /></p>

<p>The Fourier transform is known primarily for converting signals from the time domain to the
frequency domain and back, but you don’t have to use it for frequency stuff, as is the case here.
The output of the IFFT in the polyphase channelizer is an array with the samples of all
channels of a given time tick. The IFFT is simply used as an algorithmic accelerator.</p>

<p>This is as good a time as any to link to my favorite Youtube video of all time:
“The Fast Fourier Transform (FFT): Most Ingenious Algorithm Ever?”</p>

<iframe width="640" height="360" src="https://www.youtube.com/embed/h7apO7q16V0?si=8zM2mOaMBD0byhyb" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>It develops the FFT algorithm, and like the polyphase channelizer, it doesn’t use the FFT
for time domain/frequency conversion, but to accelerate the multiplication of polynomials.</p>

<h1 id="from-theory-to-practice">From Theory to Practice</h1>

<p>Let’s put everything together in a simulated example. I’ve created a new signal
that has 2 active channels, with center frequency at 20 MHz and 30 MHz. The 20 MHz channel
has the same peaks as before, the 30 MHz one has 2 different peaks. As before, the
inactive channels have a large noise component.</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het_sim-signal_multi_spectrum.svg" alt="Spectrum of signal with 2 active channels" /></p>

<p>NumPy has all kinds of nice operators to manipulate multi-dimensional arrays, but my
knowledge about them is thin. So the code below won’t be the most canonical way
of doing things!</p>

<p>Split up the taps of low-pass filter <code class="language-plaintext highlighter-rouge">h_lpf</code> into <code class="language-plaintext highlighter-rouge">h_poly</code>, its polyphase decomposition:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">h_poly</span>              <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">((</span><span class="n">DECIM_FACTOR</span><span class="p">,</span> <span class="nb">int</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">ceil</span><span class="p">(</span><span class="n">LPF_FIR_TAPS</span> <span class="o">/</span> <span class="n">DECIM_FACTOR</span><span class="p">))))</span>
<span class="k">for</span> <span class="n">phase</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">DECIM_FACTOR</span><span class="p">):</span>
    <span class="n">phase_taps</span>      <span class="o">=</span> <span class="n">h_lpf</span><span class="p">[</span><span class="n">phase</span><span class="p">::</span><span class="n">DECIM_FACTOR</span><span class="p">]</span>
    <span class="n">h_poly</span><span class="p">[</span><span class="n">phase</span><span class="p">,</span> <span class="p">:</span><span class="nb">len</span><span class="p">(</span><span class="n">phase_taps</span><span class="p">)]</span> <span class="o">=</span> <span class="n">phase_taps</span>
</code></pre></div></div>

<p>Decimate the input signal into 10 different signals, each with a different phase:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">signal_multi_decim</span>  <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">((</span><span class="n">DECIM_FACTOR</span><span class="p">,</span> <span class="nb">int</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">ceil</span><span class="p">(</span><span class="n">NR_SAMPLES</span><span class="o">/</span><span class="n">DECIM_FACTOR</span><span class="p">))))</span>
<span class="k">for</span> <span class="n">phase</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">DECIM_FACTOR</span><span class="p">):</span>
    <span class="n">phase_decim</span>     <span class="o">=</span> <span class="n">signal_multi</span><span class="p">[</span><span class="n">DECIM_FACTOR</span><span class="o">-</span><span class="mi">1</span><span class="o">-</span><span class="n">phase</span><span class="p">::</span><span class="n">DECIM_FACTOR</span><span class="p">]</span>
    <span class="n">signal_multi_decim</span><span class="p">[</span><span class="n">phase</span><span class="p">,</span> <span class="p">:</span><span class="nb">len</span><span class="p">(</span><span class="n">phase_decim</span><span class="p">)]</span> <span class="o">=</span> <span class="n">phase_decim</span>
</code></pre></div></div>

<p>Note the <code class="language-plaintext highlighter-rouge">DECIM_FACTOR-1-phase</code> part. It’s tempting to write <code class="language-plaintext highlighter-rouge">phase</code> there, but that
won’t work. Ask me how I know…</p>

<p>For each phase, apply the decimated input signal to the corresponding polyphase
sub-filter:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">h_poly_out</span>          <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">((</span><span class="n">DECIM_FACTOR</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">signal_multi_decim</span><span class="p">[</span><span class="mi">0</span><span class="p">])))</span>
<span class="k">for</span> <span class="n">phase</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">DECIM_FACTOR</span><span class="p">):</span>
    <span class="n">phase_h_out</span>     <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">convolve</span><span class="p">(</span><span class="n">signal_multi_decim</span><span class="p">[</span><span class="n">phase</span><span class="p">],</span> <span class="n">h_poly</span><span class="p">[</span><span class="n">phase</span><span class="p">],</span> <span class="n">mode</span><span class="o">=</span><span class="s">"same"</span><span class="p">)</span>
    <span class="n">h_poly_out</span><span class="p">[</span><span class="n">phase</span><span class="p">,</span> <span class="p">:</span><span class="nb">len</span><span class="p">(</span><span class="n">phase_h_out</span><span class="p">)]</span> <span class="o">=</span> <span class="n">phase_h_out</span>
</code></pre></div></div>

<p>For each timestep, take the 10 samples from output values of the filters, perform an IFFT,
and store it as the output of 10 channels:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>signal_poly_out    = np.zeros((DECIM_FACTOR, int(np.ceil(NR_SAMPLES/DECIM_FACTOR))), dtype=complex)
for m in range(len(h_poly_out[0])):
    ifft_input  = h_poly_out[:, m]
    ifft_out    = np.fft.ifft(ifft_input)
    signal_poly_out[:, m] = ifft_out
</code></pre></div></div>

<p>Here’s a plot witih the spectra for channels 1 (noise), 2 and 3:</p>

<p><img src="/assets/polyphase/polyphase_het/polyphase_het_sim-signal_poly_out_ch2_ch3.svg" alt="Plot with the spectrum for channels 1, 2 and 3" /></p>

<p>Success!</p>

<h1 id="conclusion">Conclusion</h1>

<p>This was a long story, but I felt that it had to be told in one go to keep all the context
together.</p>

<p>Let’s do a step-by-step recap:</p>

<ul>
  <li>We started with a very naive implementation of a single channel downconverter.</li>
  <li>Using a straightforward polyphase decomposition, we came up with a much more efficient design but
with one major flaw: it required a rotator that runs at the input sample rate.</li>
  <li>With a bit of algebra, we were able to move that rotator to the back of the pipeline,
after the decimator. No more units running at input sample rate!</li>
  <li>A smart choice of the sample rate allowed us to get rid of the rotator altogether.</li>
  <li>Some more algebra allowed us to cut the number of multiplications by half and
isolate all channel specific calculations to the very end of the pipeline.</li>
  <li>With only 1 non-channel specific polyphase filter and different rotators at the back, 
we could expand the pipeline to support multiple channels at low extra cost.</li>
  <li>That cost became even lower by recognizing the presence of an inverse discrete Fourier
transform and using an IFFT to accelerate the calculations.</li>
</ul>

<p>I just love how everything, like a plan, comes beautifully together.</p>

<p>I deliberately left out the parts of the video where harris discusses cases where channel centers
have a fixed offset from where they should be. It would make this blog post even longer, but these
cases are also not fully worked-out in the video. I’ll need more time to digest those parts.</p>

<p>The topics that have been covered in these last 3 blog posts only take around 40 min of a video
that’s 90 min long. The remainder contains a bunch of interesting examples and applications for
polyphase filter banks and polyphase channelizers. I want to dive into those as well.</p>

<p>One thing that I didn’t cover is the intuitive explanation about how polyphase channelizers
work. harris talks about rotating spectra, aliased to the same baseband, that cancel each other
out for different rotators. While I kind of get what he’s trying to say, the truth is that I
currently don’t have the kind of intuition that harris has, so I’ll 
defer <a href="https://youtu.be/afU9f5MuXr8?t=2151">to the video</a> for that.</p>

<p>Many thanks to <a href="https://joshuawise.com">Joshua</a> for reviewing!</p>

<h1 id="references">References</h1>

<ul>
  <li>
    <p><a href="https://www.youtube.com/watch?v=afU9f5MuXr8">Youtube - Recent Interesting and Useful Enhancements of Polyphase Filter Banks: fred harris</a></p>
  </li>
  <li>
    <p><a href="https://dsp.stackexchange.com/questions/96042/understanding-polyphase-filter-banks">Stackexchange - Understanding Polyphase Filter Banks</a></p>
  </li>
  <li>
    <p><a href="https://ieeexplore.ieee.org/document/1193158">IEEE - Digital Receivers and Transmitters Using Polyphase Filter Banks for Wireless Communications</a></p>
  </li>
</ul>

<p><strong>Other blog posts in this series</strong></p>

<ul>
  <li><a href="/2026/01/25/Notes-on-Basic-Polyphase-Decimation.html">Notes about Basic Polyphase Decimation Filters</a></li>
  <li><a href="/2026/02/07/Complex-Heterodyne.html">Complex Heterodynes Explained</a></li>
  <li><a href="/2026/03/05/Polyphase-Channelizer-with-Offset.html">Polyphase Channelizers with Frequency Offset - a Bluetooth LE Example</a></li>
</ul>

<p><strong>Source code</strong></p>

<ul>
  <li><a href="https://github.com/tomverbeure/polyphase_blog_series">GitHub - Polyphase Filtering Blog Series</a></li>
</ul>

<h1 id="footnotes">Footnotes</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:harris" role="doc-endnote">
      <p>fred harris insists on writing his name entirely in lower case. But according to
       <a href="https://www.reddit.com/r/DSP/comments/1cyrh9/comment/c9lwtot">this reddit comment</a>
       that’s only true in the time domain. <a href="#fnref:harris" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:math" role="doc-endnote">
      <p>I’ve been spending weeks on this subject now: watching videos, reading books,
     and writing the blog posts. Doing so, I’ve become much more comfortable with the
     math. That’s good for me personally, but it’s ironic that this might make the
     blog posts less accessible for others! <a href="#fnref:math" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:armstrong" role="doc-endnote">
      <p>Edwin Armstrong was the inventor of the superheterodyne receiver that 
          I mentioned in the previous blog post. <a href="#fnref:armstrong" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:linear_phase" role="doc-endnote">
      <p>Whether or not an FIR is linear phase depends on its coefficients, but most
             common methods to determine those result in a linear phase filter. <a href="#fnref:linear_phase" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:multiplier" role="doc-endnote">
      <p>For the sake of argument, I’m assuming the coefficients are programmable so that
           a full-fledged multiplier is needed. If the coefficients are constant, you can
           almost always replace a multiplier by a much cheaper combination of add and shift
           operations. <a href="#fnref:multiplier" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:steps" role="doc-endnote">
      <p>In theory, the number of steps to complete a rotation could be a fractional number. <a href="#fnref:steps" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:unity_point_calculation" role="doc-endnote">
      <p>There are multiple techniques to calculate the next point on a unity circle.
                        The most straightforward one is to do a rotation with a fixed 
                        <a href="https://en.wikipedia.org/wiki/Rotation_matrix">rotation matrix</a>, you that
                        will cost up to 4 multipliers, and you need to watch out for accumulating
                        errors over time. The <a href="https://en.wikipedia.org/wiki/CORDIC">CORDIC</a>
                        algorithm is very popular, requires no multiplication, but requires much
                        more steps per result to achieve the desired precision. <a href="#fnref:unity_point_calculation" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:equivalency" role="doc-endnote">
      <p>There are other references, but all of those are either papers written by harris
            or papers that reference one of his papers or books. <a href="#fnref:equivalency" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:book" role="doc-endnote">
      <p>I’ve only just started reading the book, but so far I really like what I see. <a href="#fnref:book" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:nr_rotators" role="doc-endnote">
      <p>\(M(M-1)\) instead of \(M^2\) because the rotator at the output of \(H_0(z)\) has an exponent of
            0 and thus reduces to 1. <a href="#fnref:nr_rotators" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:inverse" role="doc-endnote">
      <p>It’s inverse because there’s no \(-\) sign in front of \(j\). <a href="#fnref:inverse" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[All words in this blog post were written by a human being.]]></summary></entry><entry><title type="html">Complex Heterodynes Explained</title><link href="https://tomverbeure.github.io/2026/02/07/Complex-Heterodyne.html" rel="alternate" type="text/html" title="Complex Heterodynes Explained" /><published>2026-02-07T10:00:00+00:00</published><updated>2026-02-07T10:00:00+00:00</updated><id>https://tomverbeure.github.io/2026/02/07/Complex-Heterodyne</id><content type="html" xml:base="https://tomverbeure.github.io/2026/02/07/Complex-Heterodyne.html"><![CDATA[<script async="" src="https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=TeX-AMS_CHTML"></script>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#some-common-dsp-notations" id="markdown-toc-some-common-dsp-notations">Some Common DSP Notations</a></li>
  <li><a href="#sampling-with-1-adc-creates-a-real-signal" id="markdown-toc-sampling-with-1-adc-creates-a-real-signal">Sampling with 1 ADC Creates a Real Signal</a></li>
  <li><a href="#heterodyning-the-signal-to-baseband-the-wrong-way" id="markdown-toc-heterodyning-the-signal-to-baseband-the-wrong-way">Heterodyning the Signal to Baseband the Wrong Way</a></li>
  <li><a href="#complex-heterodyne-to-the-rescue" id="markdown-toc-complex-heterodyne-to-the-rescue">Complex Heterodyne to the Rescue</a></li>
  <li><a href="#filtering-away-the-old-negative-image" id="markdown-toc-filtering-away-the-old-negative-image">Filtering Away the Old Negative Image</a></li>
  <li><a href="#decimation" id="markdown-toc-decimation">Decimation</a></li>
  <li><a href="#final-block-diagram" id="markdown-toc-final-block-diagram">Final Block Diagram</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#afterthought-the-fourier-transform-is-a-bunch-of-averaged-complex-heterodynes" id="markdown-toc-afterthought-the-fourier-transform-is-a-bunch-of-averaged-complex-heterodynes">Afterthought: the Fourier Transform is a Bunch of Averaged Complex Heterodynes</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
  <li><a href="#footnotes" id="markdown-toc-footnotes">Footnotes</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>In my previous blog post about <a href="/2026/01/25/Notes-on-Basic-Polyphase-Decimation.html">polyphase decimation</a>, 
my reason for looking at that topic was “reading up on polyphase filters and multi-rate digital signal 
processing”, but to be more specific, it all started by watching 
<a href="https://www.youtube.com/watch?v=afU9f5MuXr8">“Recent Interesting and Useful Enchancements of Polyphase Filter Banks”</a>,
a fantastic tutorial by <a href="https://en.wikipedia.org/wiki/Fredric_J._Harris">Fred Harris</a>.
The video is more than 90 min long and is a lot to process when your DSP knowledge is lacking.</p>

<p>I’ve watched the video a few times now, and while I kind of get what he’s doing, it made me realize even 
more how skin-deep my DSP knowledge really is.</p>

<p>For example, the video talks about <em>complex heterodynes</em> all over the place, but I couldn’t really 
explain how the outcome of that operation is different from mixing an input signal with a regular, real sinusoid.</p>

<p>To fix this, I’m going through video sections step-by-step and blog post by blog post. The general
approach is to demonstrate concepts (to myself) by implementing them in 
<a href="httpsP//numpy.org">NumPy</a> 
and plotting the results while limiting the number of mathematical formulas. In the process of peeling 
that onion, new knowledge gaps will be exposed that might not be directly relevant to the video, but if 
interesting enough, I’ll check those out just the same.</p>

<p>But that’s for the future. Let’s talk about the why and how of the complex heterodyne.</p>

<p>The scripts that were used to create the figures in this blog post series can be found in
my <a href="https://github.com/tomverbeure/polyphase_blog_series"><code class="language-plaintext highlighter-rouge">polyphase_blog_series</code></a> on GitHub.</p>

<h1 id="some-common-dsp-notations">Some Common DSP Notations</h1>

<p>There are some conventions that are useful to know about. They aren’t a hard
and fast rules, but I’ll try to stick to them as well as I can.</p>

<ul>
  <li>\(N\): the number of samples in the time domain buffer over which a certain
block operation is performed.</li>
  <li>\(n\): the current time in a discrete time system. For example, \(s[n]\) could be
an array or sequence of input samples that come out of an ADC.</li>
  <li>\(k\): an index in a size limited set of numbers. \(k\) could be used to indicate 
one of many channels, it could be one bin out of all the bins of 
a discrete time Fourier transform, etc.</li>
  <li>\(H(z)\): a discrete transfer function, usually of a filter. The fact that it’s
an uppercase \(H\) indicates that the function is in the z-domain, the discrete
version of \(H(s)\) which is in the Laplace domain, but don’t worry about those
terms, it’s the last time they’ll be mentioned.</li>
  <li>\(h[n]\): the impulse response of the \(H(z)\) transfer function. This is the
time domain sequence that you get if you apply a 1 and then nothing but zeros
to \(H(z)\). Since I’ll only be discussing finite impulse response filters (FIR),
\(h[n]\) will be the same as the coefficients of the polynomial that describes
\(H(z)\).</li>
  <li>\(h[k]\): one of the polynomial coefficients of \(H(z)\). For all coeffients of
\(H(z)\), \(h[k]\) will be identical to \(h[n]\). For all other values, \(h[n]\)
will be zero, while \(h[k]\) won’t really exist. This is a pretty subtle difference
and often \(h[k]\) and \(h[n]\) will be used interchangeably (I’ve definitely done so!), 
but the notation can help to make clear the intent of a formula.</li>
  <li>\(F_x\): a real world analog frequency, measured in Hz. \(F_s\) is often used for
the sample rate. \(F_c\) could be the center frequency of a channel.</li>
  <li>\(f_x\): a normalized frequency, usually relative to the sample frequency. 
\(f_c\) would be the ratio of \(F_c / F_s\).</li>
  <li>\(\omega\) and \(\theta\): both are used to indicate the rate
of change of a periodic signal. But \(\omega\) tends to be used more when the intent
is an angular frequency, e.g. in the context of shifting the spectrum of a signal,
while \(\theta\) puts more emphasis on the change of an angle on the unit circle.
From a pure mathematical point of view, they’re the same: \(\sin(\omega n)\) is
no different than \(\sin(\theta n)\).
One reason to use \(\omega\) instead of \(2 \pi n/N\) is because it reduces the visual 
clutter when used as an argument of trigonometry functions. Compare \(\sin(2 \pi n /N)\) 
with \(\sin(\omega n)\).</li>
</ul>

<p>I’ll try to stick to these conventions as much as possible. Feel free to reach out
if you think I’m doing it wrong somewhere.</p>

<h1 id="sampling-with-1-adc-creates-a-real-signal">Sampling with 1 ADC Creates a Real Signal</h1>

<p>Let’s create an input signal that’s interesting enough to demonstrate DSP theory in practice and
that will trip us up if we’re doing something wrong. It’s a signal that you could get out of
a real-world analog front-end with a single AD converter (ADC) that has a sampling clock
of 100 MHz.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">signal_pure</span> <span class="o">=</span> <span class="p">(</span> <span class="n">signal1_amplitude</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">sin</span><span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">pi</span> <span class="o">*</span> <span class="n">signal1_freq_hz</span> <span class="o">*</span> <span class="n">t</span><span class="p">)</span>
              <span class="o">+</span> <span class="n">signal2_amplitude</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">cos</span><span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">pi</span> <span class="o">*</span> <span class="n">signal2_freq_hz</span> <span class="o">*</span> <span class="n">t</span><span class="p">)</span> <span class="p">)</span>

<span class="n">noise_floor</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">normal</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">noise_rms</span><span class="p">,</span> <span class="n">NR_SAMPLES</span><span class="p">)</span>

<span class="n">oob_noise</span>           <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">normal</span><span class="p">(</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">oob_noise_rms</span><span class="p">,</span> <span class="n">NR_SAMPLES</span><span class="p">)</span>

<span class="n">oob_noise_cutoffs</span>   <span class="o">=</span> <span class="p">[</span> <span class="n">OOB_NOISE_SBF_LOW_MHZ</span> <span class="o">/</span> <span class="p">(</span><span class="n">SAMPLE_CLOCK_MHZ</span> <span class="o">/</span> <span class="mf">2.0</span><span class="p">),</span>
                        <span class="n">OOB_NOISE_SBF_HIGH_MHZ</span> <span class="o">/</span> <span class="p">(</span><span class="n">SAMPLE_CLOCK_MHZ</span> <span class="o">/</span> <span class="mf">2.0</span><span class="p">)</span> <span class="p">]</span>

<span class="n">oob_noise_h</span>         <span class="o">=</span> <span class="n">firwin</span><span class="p">(</span> <span class="n">OOB_NOISE_FIR_TAPS</span><span class="p">,</span>
                              <span class="n">oob_noise_cutoffs</span><span class="p">,</span>
                              <span class="n">window</span><span class="o">=</span><span class="p">(</span><span class="s">"kaiser"</span><span class="p">,</span> <span class="n">OOB_NOISE_KAISER_BETA</span><span class="p">),</span>
                              <span class="n">pass_zero</span><span class="o">=</span><span class="s">"bandstop"</span> <span class="p">)</span>

<span class="n">oob_noise_filtered</span>  <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">convolve</span><span class="p">(</span><span class="n">oob_noise</span><span class="p">,</span> <span class="n">oob_noise_h</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"same"</span><span class="p">)</span>

<span class="n">signal</span>      <span class="o">=</span> <span class="n">signal_pure</span> <span class="o">+</span> <span class="n">noise_floor</span> <span class="o">+</span> <span class="n">oob_noise_filtered</span>
</code></pre></div></div>

<p>The signal has the following components:</p>

<ul>
  <li>
    <p>2 sinusoids, one at 22 MHz and one at 17 MHz. The second one has an amplitude that
is 10 dB lower.</p>

    <p>This is the signal that we’re interested in.</p>
  </li>
  <li>
    <p>A tiny bit of noise across the whole spectrum</p>

    <p>This adds a noise floor to the overall spectrum which makes it more like 
the real world and also makes the frequency plots more pleasing go the eye.</p>
  </li>
  <li>
    <p>Out-of-band noise that is everywhere expect in the frequency band where our
signal lives.</p>

    <p>This is useful to verify that we’re processing the signal the right way. If we
don’t then this noise will overlap the spectrum of the signal of interest and
we’d notice that right away in the spectrum plot.</p>
  </li>
</ul>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-input_signal.svg" alt="Input signal time and spectrum plot" /></p>

<p>In a time domain plot, we see a typical case of sinusoids interacting with each other,
resulting in some kind of beat envelope frequency. The noise is too low to be noticable
in a non-logarithmic plot.</p>

<p>The frequency domain amplitude plot is more interesting. There are the 2 peaks
of different amplitude, a noise floor in the frequency band where our signal lives,
and the more prominent out-of-band noise everywhere else.</p>

<p>We can also see that the negative frequency side of the spectrum is a mirror of 
the positive side. This is as it should be: to display the spectrum, we performed a 
<a href="https://en.wikipedia.org/wiki/Discrete_Fourier_transform">Discrete Fourier Transformation (DFT)</a>,
which I’ll sometimes, incorrectly, call the Fourier transform for brevity.</p>

<p>The definition of the DFT is as follows:</p>

\[X[k] = \sum_{n=0}^{N-1}{x[n] e^{-j {2 \pi k n}/{N} } }\]

<p>That looks intimidating, but if we use
<a href="https://en.wikipedia.org/wiki/Euler%27s_formula">Euler’s formula</a>, 
we can rewrite this as:</p>

\[X[k] = \sum_{n=0}^{N-1}{x[n] \cos( \frac{2 \pi k n}{N} ) } - j \sum_{n=0}^{N-1}{x[n] \sin( \frac{2 \pi k n}{N} ) }\]

<p>For a given frequency bucket \(k\), we are multiplying the input signal by cosine and by a sine.
This is essentially a correlation function that calculates the extent by which sine and cosine are part
of the input signal. Since the cosine and sine have a 90 degree phase difference between them,
we’re using complex notation for the final number:</p>

\[X[k] = R + j I\]

<p>The magnitude of the frequency of each frequench components is:</p>

\[\left| X[k] \right| = \sqrt{R^2 + I^2}\]

<p>The phase is the angle between R and I is:</p>

\[\angle{X[k]} = \arctan(\frac{I}{R})\]

<p>If the Fourier transform is applied to signal that doesn’t have complex samples, 
as is the case when there is only 1 ADC, then the Fourier transform has
<a href="https://www.dsprelated.com/freebooks/sasp/Symmetry_DTFT_Real_Signals.html">Hermitian symmetry</a>:
for every complex value on the positive frequency side, the corresponding negative frequency value
will have the same real value \(R_k\) and an inverted imaginary value \(I_k\). Because of this,
the amplitude is the same but the phase is inverted.</p>

<p>In the frequency plot above, only the amplitude is shown, hence the mirror image with
identical amplitudes left and right.</p>

<p>In DSP land, a signal that doesn’t have imaginary component values is called a real
signal. A signal that is complex and that doesn’t have a negative frequency 
components is an <a href="https://en.wikipedia.org/wiki/Analytic_signal">analytic signal</a>.</p>

<p>A common way of saying that the sine and cosine have a 90 degree phase difference, is that
they are in quadrature. It’s an extremely powerful concept that makes many DSP operations
a whole lot easier, as we’ll see below.</p>

<h1 id="heterodyning-the-signal-to-baseband-the-wrong-way">Heterodyning the Signal to Baseband the Wrong Way</h1>

<p>Imagine that we have multiple frequency bands or channels, that each channel has 
a bandwidth of 10 MHz and a center frequencies at 0, 10, 20, 30 and 40 MHz. The signal that we
created above would then be part of the 20 MHz channel that ranges from 15 to 25 MHz.</p>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-channels.svg" alt="Different channels" /></p>

<p>To process the channel, we’d like to move it from 15 MHz to 25 MHz to the baseband 
range of -5 MHz to 5 MHz. For our case, this means that we want the 17 MHz and 22 MHz 
components to end up at -3 MHz and +2 MHz resp.</p>

<p>Moving a channel to baseband before doing further processing allows us to use the same DSP 
operations no matter which channel we’ve selected. It also allows us to reduce the sample rate 
from 100 MHz to something much lower, thus reducing DSP resource requirements.</p>

<p>You can shift the spectrum of a signal by multiplying it with a sine wave. The
multiplication of 2 signals is also called <a href="https://en.wikipedia.org/wiki/Frequency_mixer">mixing</a>. 
And mixing with the purpose of moving the spectrum of a signal is called 
<a href="https://en.wikipedia.org/wiki/Heterodyne">heterodyning</a>. In the analog world,
the signal is multiplied with the sinusoidal output of a local oscillator (LO). We still
need this in the virtual work of DSP math in the form a simulated
<a href="https://en.wikipedia.org/wiki/Numerically_controlled_oscillator">numerically controlled oscillator</a>
so I will keep on using the name of local oscillator.</p>

<p>The math of heterodyning a sine wave is straightforward. Here I show how it works in the
continuous time domain, but it works the same after sampling. Let’s start with signal
\(s(t)\) and local oscillator \(l(t)\):</p>

\[s(t)= A \cos(2 \pi f_0 t) \\
l(t)= \cos(2 \pi f_c t) \\\]

<p>Multiply the 2 signals to get heterodyne signal \(y(t)\):</p>

\[y(t) = s(t) l(t) = A \cos(2 \pi f_0 t) \cos(2 \pi f_c t) \\\]

<p>Use the textbook trigonometry identity:</p>

\[\cos \alpha \cos \beta = \frac{1}{2} \big[ \cos(\alpha + \beta) + \cos(\alpha - \beta)  \big]\]

<p>We get:</p>

\[y(t) = \frac{1}{2} A \big[ \cos(2 \pi (f_0 + f_c) t) + \cos( 2 \pi (f_0 - f_c) t) \big]\]

<p>This tells us is that multiplying a signal with frequency component \(f_0\)
with sine wave with frequency \(f_c\) creates a new signal with 2 frequency components
\(f_0 + f_c\) and \(f_0 - f_c\).</p>

<p>If we want to shift the center frequency of our channel from 20 MHz to 0 MHz, we need to
multiply with a 20 MHz sine wave. Let’s simulate that:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lo_signal</span>       <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">sin</span><span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">pi</span> <span class="o">*</span> <span class="n">lo_freq_hz</span> <span class="o">*</span> <span class="n">t</span><span class="p">)</span>
<span class="n">signal_real_het</span> <span class="o">=</span> <span class="n">signal</span> <span class="o">*</span> <span class="n">lo_signal</span>
</code></pre></div></div>

<p>This is the resulting spectrum:</p>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-real_het.svg" alt="Spectrum after real heterodyn" /></p>

<p>That… didn’t go as we hoped.</p>

<p>The spectrum got shifted down by 20 MHz to 0 MHz and to -40 MHz,
giving us peaks at -3 MHz and +2MHz and -37 MHz and -42 MHz. That’s what we wanted!
But since <code class="language-plaintext highlighter-rouge">lo_signal</code> is a real signal, it has a mirror image at -20 MHz. This made
the spectrum of the signal shift up to +3 MHz and -2 MHz and 37 MHz and 42 MHz.</p>

<p>Instead of the desired 2 peaks in the baseband, there are now 4 peaks, at -3, -2, 2 and 3 MHz. 
We’ve destroyed the original signal.</p>

<p>Heterodyning with a real local oscillator is a common operation in the analog world, but
when this is done, the heterodyne doesn’t happen to baseband but a non-zero intermediate
frequency. That is the idea of the 
<a href="https://en.wikipedia.org/wiki/Superheterodyne_receiver">superheterodyne receiver</a><sup id="fnref:super" role="doc-noteref"><a href="#fn:super" class="footnote" rel="footnote">1</a></sup>, a huge
breakthrough in 1918 in the development of radio technology: it mixes the desired signal
to a fixed intermediate frequency (IF),  not the baseband, and does further demodulation such
AM or FM on that IF signal.</p>

<p><img src="https://upload.wikimedia.org/wikipedia/commons/3/3f/Superheterodyne_receiver_block_diagram_2.svg" alt="Superheterodyne example block diagram" />
<em>(Source: Wikipedia)</em></p>

<h1 id="complex-heterodyne-to-the-rescue">Complex Heterodyne to the Rescue</h1>

<p>We could definitely do a superheterodyne in the digital world, but many modern modulation
schemes such as 
<a href="https://en.wikipedia.org/wiki/Quadrature_amplitude_modulation">QAM</a> or 
<a href="https://en.wikipedia.org/wiki/Orthogonal_frequency-division_multiplexing">OFDM</a>
rely on the ability to process the signal in the baseband.</p>

<p>Luckily, there is a solution. The root of our troubles is the presence of a 
mirror frequency image for the local oscillator. If we can get rid of one of those orange LO peaks, 
only one spectrum image of the signal will get heterodyned into the baseband.</p>

<p>This is surprisingly simple: instead of a real sinusoid, we use a complex one as local oscillator:</p>

\[l(t) = e^{-j 2 \pi f_c t}\]

<p>This signal only has a peak in the spectrum at \(-F_c\). We’re using a negative LO frequency
because we want to shift the spectrum down so that positive image of the channel spectrum ends
up at baseband. If we use \(F_c\), the whole spectrum shifts up instead and the negative
channel lands on baseband.</p>

<p>Let’s create the complex local oscillator signal and multiply it by
the input signal:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">complex_lo_signal</span>   <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="mf">1j</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">pi</span> <span class="o">*</span> <span class="n">lo_freq_hz</span> <span class="o">*</span> <span class="n">t</span><span class="p">)</span>
<span class="n">signal_complex_het</span>  <span class="o">=</span> <span class="n">signal</span> <span class="o">*</span> <span class="n">complex_lo_signal</span>
</code></pre></div></div>

<p>And voila:</p>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-complex_lo.svg" alt="Complex LO and heterodyne" /></p>

<p>We had to introduce complex numbers, but the result is worth it: the baseband has exactly 
what we want.</p>

<h1 id="filtering-away-the-old-negative-image">Filtering Away the Old Negative Image</h1>

<p>The only thing that’s still bothering us are the 2 peaks around -40 MHz, the negative image
of the channel that used to be at -20 MHz. This needs to go if we want to lower the sample 
rate by decimation.</p>

<p>We can easily do this with a low pass FIR filter. There are multiple ways to design those,
I even wrote <a href="/2020/10/11/Designing-Generic-FIR-Filters-with-pyFDA-and-Numpy.html">a blog post</a> 
about it.</p>

<p>Here, I chose the <a href="https://www.dsprelated.com/freebooks/sasp/Window_Method_FIR_Filter.html">windowing method</a>
to create a steep 201 taps FIR filter with a passband of 5 MHz.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fir_cutoff</span>  <span class="o">=</span> <span class="n">FIR_PASSBAND_MHZ</span> <span class="o">/</span> <span class="p">(</span><span class="n">SAMPLE_CLOCK_MHZ</span> <span class="o">/</span> <span class="mf">2.0</span><span class="p">)</span>
<span class="n">h_lpf</span>       <span class="o">=</span> <span class="n">firwin</span><span class="p">(</span><span class="n">FIR_TAPS</span><span class="p">,</span> <span class="n">fir_cutoff</span><span class="p">,</span> <span class="n">window</span><span class="o">=</span><span class="p">(</span><span class="s">"kaiser"</span><span class="p">,</span> <span class="n">FIR_KAISER_BETA</span><span class="p">),</span> <span class="n">pass_zero</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<p>The filter is applied by doing a convolution between the heterodyned signal and the filter taps in <code class="language-plaintext highlighter-rouge">h_lpf</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">signal_het_lpf</span>      <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">convolve</span><span class="p">(</span><span class="n">signal_complex_het</span><span class="p">,</span> <span class="n">h_lpf</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">"same"</span><span class="p">)</span>
</code></pre></div></div>

<p>Note that the samples of <code class="language-plaintext highlighter-rouge">signal_complex_het</code> are complex, but the filter coefficients are real.</p>

<p>Here’s the result:</p>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-low_pass_filter.svg" alt="Complex heterodyned signal with low pass filter" /></p>

<h1 id="decimation">Decimation</h1>

<p>The spectrum has now been reduced to -5 MHz and 5 MHz. Since there is no mirror image, we can safely
do a decimation without having to worry about aliasing as long as we obey 
<a href="https://en.wikipedia.org/wiki/Nyquist–Shannon_sampling_theorem">Nyquist</a> 
by keeping the width of the spectrum is equal or larger than the 2-sided width of channel, which is
10 MHz. With a sample rate of 100 MHz, we can decimate by a factor of 10.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">signal_decim</span>    <span class="o">=</span> <span class="n">signal_het_lpf</span><span class="p">[::</span><span class="n">DECIM_FACTOR</span><span class="p">]</span>
</code></pre></div></div>

<p>We now have 10 times less data to deal with, but the spectrum looks just the same
as before:</p>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-decim_fft.svg" alt="Spectrum after decimation" /></p>

<p>Success!</p>

<h1 id="final-block-diagram">Final Block Diagram</h1>

<p>Wrapping up, we arrived at the following block diagram of operations and transformations:</p>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-block_diagram.svg" alt="Block diagram with all operations" /></p>

<ul>
  <li>The analog signal is converted to a real digital with a single channel, 100 Msps ADC.</li>
  <li>A mixer and a complex local oscillator heterodynes the signal to baseband. The
signal is now complex.</li>
  <li>A low pass filter removes all frequencies that don’t reside in the baseband.</li>
  <li>A decimator brings down the sample rate from 100 MHz to 10 MHz</li>
  <li>The output is a complex 10 MHz sample stream.</li>
</ul>

<p><img src="/assets/polyphase/complex_heterodyne/complex_heterodyne-rot_lpf_decim.svg" alt="Mathematical block diagram" /></p>

<p>Expressed mathematically:</p>

\[y[m] = \big[(x[n] e^{-j 2 \pi f_c n}) * h_{\text{lpf}}[n]\big] \downarrow M \\
f_c = \frac{F_c}{F_s}, m = n M\]

<p>The thing works, but is the optimal of doing things? My 
<a href="/2026/01/25/Notes-on-Basic-Polyphase-Decimation.html">previous blog post about polyphase decimation filtering</a>
should be a hint that the answer is: definitely not.</p>

<p>Dealing with a complex instead of real signal doubles the number of math operations
and performing the decimation at the end of the pipeline means that we’re doing a lot 
of math that gets thrown away.</p>

<p>But I have a much better understanding of complex heterodyning now, so that’s a definite
win!</p>

<p>In a next installment, I’ll explore how this can be optimized.</p>

<h1 id="conclusion">Conclusion</h1>

<p>In the Fred Harris video that started this all, complex heterodynes are everywhere and
treated as a known quantity. And they’re straightforward once you get to know them better.</p>

<p>I used to think that dealing with signals in quadrature, representing them with complex numbers, 
was dOne primarily as a way to reduce the sample rate by half. There are certain potential
cost savings to that.</p>

<p>But the benefits are more fundamental: they eliminate the issue of having to deal with mirror images
in the spectrum.</p>

<h1 id="afterthought-the-fourier-transform-is-a-bunch-of-averaged-complex-heterodynes">Afterthought: the Fourier Transform is a Bunch of Averaged Complex Heterodynes</h1>

<p>While writing this blog post, I suddenly struck me: the discrete time Fourier
transform is the same as doing a complex heterodyne to 0 Hz and then calculating 
the DC value by summing the samples, for all frequencies of interest.</p>

<p>Complex heterodyne:</p>

\[y[n] = x[n] e^{-j 2 \pi f_k n}\]

<p>DFT:</p>

\[X[k] = \sum_{n=0}^{N-1}{x[n] e^{-j {2 \pi k n}/{N} } } \\
f_k = k / N \\
X[k] = \sum_{n=0}^{N-1}{x[n] e^{-j {2 \pi f_k n } } } \\\]

<p>This is kind of obvious when you think about it, but I had never dealt with
complex heterodynes so it’s something new for me.</p>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://www.youtube.com/watch?v=afU9f5MuXr8">Youtube - Recent Interesting and Useful Enhancements of Polyphase Filter Banks: Fred Harris</a></li>
  <li><a href="https://github.com/tomverbeure/polyphase_blog_series">polyphase blog series scripts</a></li>
</ul>

<p>Other blog posts in this series:</p>

<ul>
  <li><a href="/2026/01/25/Notes-on-Basic-Polyphase-Decimation.html">Notes about Basic Polyphase Decimation Filters</a></li>
  <li><a href="/2026/02/16/Polyphase-Channelizer.html">The Stunning Efficiency and Beauty of the Polyphase Channelizer</a></li>
  <li><a href="/2026/03/05/Polyphase-Channelizer-with-Offset.html">Polyphase Channelizers with Frequency Offset - a Bluetooth LE Example</a></li>
</ul>

<h1 id="footnotes">Footnotes</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:super" role="doc-endnote">
      <p>If you’re wondering why it’s called ‘super’: it’s because the result of the
      heterodyne is a signal that is still in the supersonic frequency range, as in,
      above the audible frequency range. Before superheterodyne receivers, the radio signal
      of interested was heterodyned straight to the audio range. <a href="#fnref:super" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Notes about Basic Polyphase Decimation Filters</title><link href="https://tomverbeure.github.io/2026/01/25/Notes-on-Basic-Polyphase-Decimation.html" rel="alternate" type="text/html" title="Notes about Basic Polyphase Decimation Filters" /><published>2026-01-25T10:00:00+00:00</published><updated>2026-01-25T10:00:00+00:00</updated><id>https://tomverbeure.github.io/2026/01/25/Notes-on-Basic-Polyphase-Decimation</id><content type="html" xml:base="https://tomverbeure.github.io/2026/01/25/Notes-on-Basic-Polyphase-Decimation.html"><![CDATA[<script async="" src="https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=TeX-AMS_CHTML"></script>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#the-decimation-and-anti-aliasing-fir-filter-combo" id="markdown-toc-the-decimation-and-anti-aliasing-fir-filter-combo">The Decimation and Anti-Aliasing FIR Filter Combo</a></li>
  <li><a href="#naive-hardware-implementation" id="markdown-toc-naive-hardware-implementation">Naive Hardware Implementation</a></li>
  <li><a href="#reduce-number-of-calculations---move-decimator-before-multiplier" id="markdown-toc-reduce-number-of-calculations---move-decimator-before-multiplier">Reduce number of calculations - Move decimator before multiplier</a></li>
  <li><a href="#polyphase-decomposition-of-the-original-filter" id="markdown-toc-polyphase-decomposition-of-the-original-filter">Polyphase decomposition of the original filter</a></li>
  <li><a href="#the-noble-identity-for-decimation" id="markdown-toc-the-noble-identity-for-decimation">The Noble Identity for Decimation</a></li>
  <li><a href="#reusing-common-hardware-in-the-fast-clock-domain" id="markdown-toc-reusing-common-hardware-in-the-fast-clock-domain">Reusing Common Hardware in the Fast Clock Domain</a></li>
  <li><a href="#delayed-multiplications-instead-of-delayed-inputs" id="markdown-toc-delayed-multiplications-instead-of-delayed-inputs">Delayed multiplications instead of delayed inputs</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
  <li><a href="#footnotes" id="markdown-toc-footnotes">Footnotes</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>I’ve been reading up on polyphase filters and multi-rate digital signal processing.
It’s a broad topic, but as a beginner I need to start with the basics. And to better 
internalize those, I like to expand the generic math into concretely worked-out, 
smaller examples.</p>

<p>And if I’m going to write things down anyway, I might as well put them in a blog
post, this way I know where to look if I want to review things later.</p>

<p>I hope the content here is useful to someone, but don’t assume that I know what
I’m doing. There are hundreds of articles on the web on the same topic, so make sure to
sample a bunch of them to get different perspectives.</p>

<p>One of the things that clicked with me while writing this, is the benefit of rearranging
the mathematical equations so that they reflect the hardware implementation. In the
past, I’ve seen a different of architectures for polyphase filters. I was able
to understand them intuitively but linking them to math adds an additional layer of
confidence.</p>

<p>So that’s one of the things I’m doing here: switch back and forth between math
and hardware architecture.</p>

<p><em>Update: in a later blog post, I added a section about 
<a href="/2026/02/07/Complex-Heterodyne.html#some-common-dsp-notations">common DSP notations</a>. 
Check it out first!</em></p>

<h1 id="the-decimation-and-anti-aliasing-fir-filter-combo">The Decimation and Anti-Aliasing FIR Filter Combo</h1>

<p>In digital signal processing (DSP), decimation is an operation in which you retain
1 out of every M samples and throw away the rest. It has the benefit of bringing the 
sample rate down, and thus the amount of data that flows through the system, the clock 
speed, the number of calculations etc. Decimation is a very common operation.</p>

<p>When following DSP theory, if you want to decimate a signal from a sample
rate \(f_s\) to a sample rate \(f_{s/M}\), you first need to apply an anti-aliasing 
filter that removes all the frequeny components above \(f_s/(2 \cdot M)\) to make sure
that the Nyquist criterium remains valid after the sample frequency has
been reduced.<sup id="fnref:Nyqist" role="doc-noteref"><a href="#fn:Nyqist" class="footnote" rel="footnote">1</a></sup></p>

<p>When using an FIR filter, the conceptual block diagram looks like this:</p>

<p><img src="/assets/polyphase/basic_polyphase/polyphase-naive_decimation_filter_basic_block_diagram.svg" alt="Filter then decimate basic block diagram" /></p>

<p>Let’s do this with 7-tap FIR filter that has transfer function \(H(z)\) and
a decimation factor M of 3.</p>

\[H(z) = h_0 + h_1 z^{-1} + h_2 z^{-2} + h_3 z^{-3} + h_4 z^{-4} + h_5 z^{-5} + h_6 z^{-6}\]

<p>In this kind of notation, \(z^{-2}\) means the input value that was delayed by 2 discrete
steps. In electronics, the equation above has a delay line of 6 elements, each
element is multiplied by a different value, and the result of those multiplication is
added together.</p>

<p>Mathematically, the combiation of a filter followed by a decimator is often expressed
like this:</p>

\[H(z) \; \downarrow M\]

<p>Let’s now take the following stream of input samples</p>

\[\cdots, x[-3], x[-2], x[-1], x[0], x[1], x[2], x[3], x[4], \cdots\]

<p>… and apply this stream to the filter equation for multiple time steps:</p>

\[\begin{alignedat}{0}
f[0] &amp; = h_0 x[0] &amp;+&amp; h_1 x[-1] &amp;+&amp; h_2 x[-2] &amp;+&amp; h_3 x[-3] &amp;+&amp; h_4 x[-4] &amp;+&amp; h_5 x[-5] &amp;+&amp; h_6 x[-6] \\
f[1] &amp; = h_0 x[1] &amp;+&amp; h_1 x[0]  &amp;+&amp; h_2 x[-1] &amp;+&amp; h_3 x[-2] &amp;+&amp; h_4 x[-3] &amp;+&amp; h_5 x[-4] &amp;+&amp; h_6 x[-5] \\
f[2] &amp; = h_0 x[2] &amp;+&amp; h_1 x[1]  &amp;+&amp; h_2 x[0]  &amp;+&amp; h_3 x[-1] &amp;+&amp; h_4 x[-2] &amp;+&amp; h_5 x[-3] &amp;+&amp; h_6 x[-4] \\
f[3] &amp; = h_0 x[3] &amp;+&amp; h_1 x[2]  &amp;+&amp; h_2 x[1]  &amp;+&amp; h_3 x[0]  &amp;+&amp; h_4 x[-1] &amp;+&amp; h_5 x[-2] &amp;+&amp; h_6 x[-3] \\
f[4] &amp; = h_0 x[4] &amp;+&amp; h_1 x[3]  &amp;+&amp; h_2 x[2]  &amp;+&amp; h_3 x[1]  &amp;+&amp; h_4 x[0]  &amp;+&amp; h_5 x[-1] &amp;+&amp; h_6 x[-2] \\
f[5] &amp; = h_0 x[5] &amp;+&amp; h_1 x[4]  &amp;+&amp; h_2 x[3]  &amp;+&amp; h_3 x[2]  &amp;+&amp; h_4 x[1]  &amp;+&amp; h_5 x[0]  &amp;+&amp; h_6 x[-1] \\
f[6] &amp; = h_0 x[6] &amp;+&amp; h_1 x[5]  &amp;+&amp; h_2 x[4]  &amp;+&amp; h_3 x[3]  &amp;+&amp; h_4 x[2]  &amp;+&amp; h_5 x[1]  &amp;+&amp; h_6 x[0]  \\
f[7] &amp; = h_0 x[7] &amp;+&amp; h_1 x[6]  &amp;+&amp; h_2 x[5]  &amp;+&amp; h_3 x[4]  &amp;+&amp; h_4 x[3]  &amp;+&amp; h_5 x[2]  &amp;+&amp; h_6 x[1]  \\
f[8] &amp; = h_0 x[8] &amp;+&amp; h_1 x[7]  &amp;+&amp; h_2 x[6]  &amp;+&amp; h_3 x[5]  &amp;+&amp; h_4 x[4]  &amp;+&amp; h_5 x[3]  &amp;+&amp; h_6 x[2]  \\
f[9] &amp; = h_0 x[9] &amp;+&amp; h_1 x[8]  &amp;+&amp; h_2 x[7]  &amp;+&amp; h_3 x[6]  &amp;+&amp; h_4 x[5]  &amp;+&amp; h_5 x[4]  &amp;+&amp; h_6 x[3]  \\
\end{alignedat}\]

<p>Decimate by selecting 1 out of every 3 filtered sample:</p>

\[\begin{alignedat}{0}
y[0] &amp; = f[0] \\
y[1] &amp; = f[3] \\
y[2] &amp; = f[6] \\
y[3] &amp; = f[9] \\
\end{alignedat}\]

<p>Or:</p>

\[\begin{alignedat}{0}
y[0] &amp; = h_0 x[0] &amp;+&amp; h_1 x[-1] &amp;+&amp; h_2 x[-2] &amp;+&amp; h_3 x[-3] &amp;+&amp; h_4 x[-4] &amp;+&amp; h_5 x[-5] &amp;+&amp; h_6 x[-6] \\
y[1] &amp; = h_0 x[3] &amp;+&amp; h_1 x[2]  &amp;+&amp; h_2 x[1]  &amp;+&amp; h_3 x[0]  &amp;+&amp; h_4 x[-1] &amp;+&amp; h_5 x[-2] &amp;+&amp; h_6 x[-3] \\
y[2] &amp; = h_0 x[6] &amp;+&amp; h_1 x[5]  &amp;+&amp; h_2 x[4]  &amp;+&amp; h_3 x[3]  &amp;+&amp; h_4 x[2]  &amp;+&amp; h_5 x[1]  &amp;+&amp; h_6 x[0]  \\
y[3] &amp; = h_0 x[9] &amp;+&amp; h_1 x[8]  &amp;+&amp; h_2 x[7]  &amp;+&amp; h_3 x[6]  &amp;+&amp; h_4 x[5]  &amp;+&amp; h_5 x[4]  &amp;+&amp; h_6 x[3]  \\
\end{alignedat}\]

<h1 id="naive-hardware-implementation">Naive Hardware Implementation</h1>

<p>A straight up hardware implementation looks like this:</p>

<p><img src="/assets/polyphase/basic_polyphase/polyphase-naive_decimation_filter.svg" alt="Naive decimation filter" /></p>

<p>As mentioned before, we have 6 delay elements and 7 multipliers that operate on the each stage
of the delay line.</p>

<p>This solution is dumb: we calculate a filter output for every input clock cycle only to throw away 2 
out of 3 results. Let’s do better.</p>

<h1 id="reduce-number-of-calculations---move-decimator-before-multiplier">Reduce number of calculations - Move decimator before multiplier</h1>

<p>We can reduce the number of calculations by moving the decimator before the filter.</p>

<p>All multiplications still happen at the same time but they can now be performed in a clock domain that is
3 times slower. This definitely reduces power and also reduces the multiplication area in an ASIC process, 
because timing paths won’t be as strict.</p>

<p><img src="/assets/polyphase/basic_polyphase/polyphase-delay_input_multiply_in_slow_domain.svg" alt="Decimate input values, multiply in slow domain" /></p>

<p>While the number of multiplications per unit of time has been reduced by 3, the number of multipliers is
still the same.</p>

<p>The data flowing through this architecture looks like this:</p>

<p><img src="/assets/polyphase/basic_polyphase/polyphase-delay_input_multiply_in_slow_domain_annotated.svg" alt="Decimate input value, multiply in slow domain, annotated" /></p>

<p>When you look at bit closer, you can see that pipes of input samples with the same color
have the same data flowing through them: the input feed of the \(h_0\) multiplier sees the
same \(x[3i]\) samples as the \(h_3\) and the \(h_6\) multipliers, it’s just that there is 
a delay of 1 clock cycle in the slow clock domain for each term.
Similarly, \(h_1\) and \(h_5\) multipliers see samples \(x[3i+1]\), and the \(h_2\) and \(h_6\) 
multipliers see samples \(x[3i+2]\).</p>

<h1 id="polyphase-decomposition-of-the-original-filter">Polyphase decomposition of the original filter</h1>

<p>Let’s take the earlier equation for value \(y[3]\) and decorate the 7 terms with the colors
of the diagram:</p>

\[y[3] = \color{red}{h_0 x[9]} + \color{green}{h_1 x[8]}  + \color{blue}{h_2 x[7]}  + \color{red}{h_3 x[6]}  + \color{green}{h_4 x[5]}  + \color{blue}{h_5 x[4]}  + \color{red}{h_6 x[3]}\]

<p>Now split the equation into 3 steps so that each step uses input values \(x[i]\) with the same color:</p>

\[\begin{alignedat}{0}
\mathrm{tmp} &amp;=&amp; \color{red}  {h_0 x[9]} &amp;\;+\;&amp; \color{red}  {h_3 x[6]} &amp;\;+\;&amp; \color{red}  {h_6 x[3]} \\
\mathrm{tmp} &amp;=&amp; \color{green}{h_1 x[8]} &amp;\;+\;&amp; \color{green}{h_5 x[5]} &amp;&amp; &amp;\;+\;&amp; \mathrm{tmp} \\
y[2]         &amp;=&amp; \color{blue} {h_2 x[7]} &amp;\;+\;&amp; \color{blue} {h_4 x[4]} &amp;&amp; &amp;\;+\;&amp; \mathrm{tmp} \\
\end{alignedat}\]

<p>What we’ve done here is split 7-tap filter \(H(z)\) into 3 separate sub-filters:</p>

\[H_0(z) = h_0 + h_3 z^{-1} + h_6 z^{-2} \\
H_1(z) = h_1 + h_4 z^{-1} + h_7 z^{-2} \\
H_2(z) = h_2 + h_5 z^{-1} + h_8 z^{-2} \\\]

<p><em>(In our example, \(h_7\) and \(h_8\) are zero.)</em></p>

<p>The equation of the original filter is now this:</p>

\[H(z) = H_0(z^3) + z^{-1} H_1(z^3) + z^{-2} H_2(z^3)\]

<p><strong>This is the polyphase decomposition of the original filter.</strong></p>

<p>The exponent of 3 in \(z^3\) tells us that input to each sub-filter is a decimated version, because if
we substitute \(z^{3}\) into the set of equations \(H_i(z)\), we get:</p>

\[H_0(z^3) = h_0 + h_3 {z^3}^{-1} + h_6 {z^3}^{-2} \\
H_1(z^3) = h_1 + h_4 {z^3}^{-1} + h_7 {z^3}^{-2} \\
H_2(z^3) = h_2 + h_5 {z^3}^{-1} + h_8 {z^3}^{-2} \\\]

<p>Or:</p>

\[H_0(z^3) = h_0 + h_3 z^{-3} + h_6 z^{-6} \\
H_1(z^3) = h_1 + h_4 z^{-3} + h_7 z^{-6} \\
H_2(z^3) = h_2 + h_5 z^{-3} + h_8 z^{-6} \\\]

<p>The polyphase equation of \(H(z)\) is now:</p>

\[H(z) = 
(h_0 + h_3 z^{-3} + h_6 z^{-6}) 
+ z^{-1} (h_1 + h_4 z^{-3} + h_7 z^{-6}) 
+ z^{-2} (h_2 + h_5 z^{-3} + h_8 z^{-6})\]

<p>Which becomes this:</p>

\[H(z) = 
  (h_0 + h_3 z^{-3} + h_6 z^{-6}) 
+ (h_1 + h_4 z^{-4} + h_7 z^{-7}) 
+ (h_2 + h_5 z^{-5} + h_8 z^{-8})\]

<p>And after reordering the terms and setting \(h_7\) and \(h_8\) to zero, we’re back
to the definition of \(H(z)\) at the start of this blog post:</p>

\[H(z) = h_0 + h_1 z^{-1} + h_2 z^{-2} + h_3 z^{-3} + h_4 z^{-4} + h_5 z^{-5} + h_6 z^{-6}\]

<h1 id="the-noble-identity-for-decimation">The Noble Identity for Decimation</h1>

<p>Those who are studying multi-rate digital signal processing will almost certainly
be confronted with the noble identities.</p>

<p>For decimation, the noble identity is formulated as follows<sup id="fnref:notation" role="doc-noteref"><a href="#fn:notation" class="footnote" rel="footnote">2</a></sup>:</p>

\[\downarrow M \: H(z) \equiv H(z^M) \: \downarrow M\]

<p>When I first got exposed to that, I thought it was confusing, but after
going through the motions of the math equations above, it started to make sense.</p>

<p>What it says is:</p>

<p><em>Performing a decimation and applying those samples to filter \(H(z)\) is equivalent
to applying the same filter to every M-th sample and then doing the decimation.</em></p>

<p>Let’s look back at the polyphase decomposition of our original \(H(z)\):</p>

\[H(z) = H_0(z^3) + z^{-1} H_1(z^3) + z^{-2} H_2(z^3)\]

<p>It important to note that we can’t apply the noble identity to our \(H(z)\) directly,
because its coefficients \(h_1\), \(h_2\), \(h_4\) and \(h_5\) are non-zero. But we <em>can</em> apply 
it to the 3 individual phases.</p>

<p>Like this:</p>

\[H(z) \downarrow 3 = (\downarrow 3 \: H_0(z)) + z^{-1} (\downarrow 3 \: H_1(z)) + z^{-2} (\downarrow 3\: H_2(z))\]

<p>Converted to a hardware diagram:</p>

<p><img src="/assets/polyphase/basic_polyphase/polyphase-noble_identity.svg" alt="Polyphase decimation hardware diagram after applying noble identity" /></p>

<p>It’s not immediately obvious, but this last diagram is similar to the previous one after we’ve
rearranged some items:</p>

<ul>
  <li>there’s now 1 decimator per phase instead of one per coefficient.</li>
  <li>a single bank of 7 multipliers and one addition has been refactored into
3 banks of multipliers with addition, and then one final addition.</li>
  <li>each multiplier-addition bank has its own delay elements.</li>
</ul>

<h1 id="reusing-common-hardware-in-the-fast-clock-domain">Reusing Common Hardware in the Fast Clock Domain</h1>

<p>In the previous diagram, it’s clear that there’s a lot of common hardware between
the different phases. We can exploit that by doing everything in the fast clock domain
and reuse the hardware that’s used for one phase for the other phases.</p>

<p>Recall the previous equation where the result was calculated in 3 steps:</p>

\[\begin{alignedat}{0}
\mathrm{tmp} &amp;=&amp; \color{red}  {h_0 x[9]} &amp;\;+\;&amp; \color{red}  {h_3 x[6]} &amp;\;+\;&amp; \color{red}  {h_6 x[3]} \\
\mathrm{tmp} &amp;=&amp; \color{green}{h_1 x[8]} &amp;\;+\;&amp; \color{green}{h_5 x[5]} &amp;&amp; &amp;\;+\;&amp; \mathrm{tmp} \\
y[2]         &amp;=&amp; \color{blue} {h_2 x[7]} &amp;\;+\;&amp; \color{blue} {h_4 x[4]} &amp;&amp; &amp;\;+\;&amp; \mathrm{tmp} \\
\end{alignedat}\]

<p>Now check out this diagram:</p>

<p><img src="/assets/polyphase/basic_polyphase/polyphase-delay_input_multiply_in_fast_domain.svg" alt="Delay input - multiply in fast domain" /></p>

<p>Everything happens in the fast clock domain, but there are only 3 multipliers instead of 7 and
we’re only adding 4 numbers together at any time. The only extra cost is a register
to store the <em>tmp</em> value, and each of the multipliers has a multiplexer to rotate between
different coefficients.</p>

<p>There is only one \(y[m]\) output every 3 clock cycles.</p>

<p>Here’s the same diagram annotated with intermediates values for different time steps:</p>

<p><img src="/assets/polyphase/basic_polyphase/polyphase-delay_input_multiply_in_fast_domain_annotated.svg" alt="Delay input - multiply in fast domain, internal values" /></p>

<h1 id="delayed-multiplications-instead-of-delayed-inputs">Delayed multiplications instead of delayed inputs</h1>

<p>In the previous diagram, the inputs are delayed and the multiplications summed together. But that’s
not the only way to implement this.</p>

<p>Let’s start again from the original equation:</p>

\[H(z) = \color{red}{h_0 z^0} + \color{green}{h_1 z^{-1}}  + \color{blue}{h_2 z^{-2}}  + \color{red}{h_3 z^{-3}}  + \color{green}{h_4 z^{-4}}  + \color{blue}{h_5 z^{-5}}  + \color{red}{h_6 z^{-6}}\]

<p>Reformat:</p>

\[\begin{alignedat}{0}
H(z) = &amp;&amp; ( \color{red}{h_0 z^0} + \color{green}{h_1 z^{-1}}  + \color{blue}{h_2 z^{-2}} )   \\
     + &amp;&amp; ( \color{red}{h_3 z^{-3}}  + \color{green}{h_4 z^{-4}}  + \color{blue}{h_5 z^{-5}} ) \\ 
     + &amp;&amp; ( \color{red}{h_6 z^{-6}} ) \\
\end{alignedat}\]

<p>Extract common \(z^{-3}\) and \(z^{-6}\):</p>

\[\begin{alignedat}{0}
H(z) = &amp;&amp; ( \color{red}{h_0 z^0} + \color{green}{h_1 z^{-1}}  + \color{blue}{h_2 z^{-2}} )   \\
     + &amp;&amp; z^{-3} ( \color{red}{h_3 z^{0}}  + \color{green}{h_4 z^{-1}}  + \color{blue}{h_5 z^{-2}} ) \\ 
     + &amp;&amp; z^{-6} ( \color{red}{h_6 z^{0}} ) \\
\end{alignedat}\]

<p>Extract common \(z^{-3}\):</p>

\[\begin{aligned}
H(z) = \; &amp; ( \color{red}{h_0 z^{0}} + \color{green}{h_1 z^{-1}} + \color{blue}{h_2 z^{-2}}) \\
          &amp;  + z^{-3} \Big[ ( \color{red}{h_3 z^{0}} + \color{green}{h_4 z^{-1}} + \color{blue}{h_5 z^{-2}} ) \\
          &amp;  \qquad\qquad\;\; + z^{-3}( \color{red}{h_6 z^{0}} ) \Big]
\end{aligned}\]

<p>We now have a nested structure, with a delay of 3 for each nesting level.</p>

<p>In hardware that looks like this:</p>

<p><img src="/assets/polyphase/basic_polyphase/polyphase-delayed_multiplications.svg" alt="Delayed multiplication results" /></p>

<p>This structure is not intrinsically worse or better than the previous one, the architecture
to use will depend on the technology that you’re mapping it to. On FPGAs, for example, you should
choose something that makes efficient use of the built-in pipelining registers inside their
DSP blocks.</p>

<h1 id="conclusion">Conclusion</h1>

<p>This only scratches the surface of polyphase filters. I didn’t even mention interpolation, which
does the opposite of decimation but has very similar computational characteristics. I plan
to cover addition of topics in the future, especially the expantion of a polyphase filter into
a polyphase filter bank. That said, I can’t promise a timeline.</p>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://dsp.stackexchange.com/questions/43344/how-to-implement-polyphase-filter">Stackexchange - How to implement Polyphase filter?</a></li>
</ul>

<blockquote>
  <p>Making a polyphase filter implementation is quite easy; given the desired coefficients 
for a simple FIR filter, you distribute those same coefficients in “row to column” format 
into the separate polyphase FIR components</p>
</blockquote>

<p>Other blog posts in this series:</p>

<ul>
  <li><a href="/2026/02/07/Complex-Heterodyne.html">Complex Heterodynes Explained</a></li>
  <li><a href="/2026/02/16/Polyphase-Channelizer.html">The Stunning Efficiency and Beauty of the Polyphase Channelizer</a></li>
  <li><a href="/2026/03/05/Polyphase-Channelizer-with-Offset.html">Polyphase Channelizers with Frequency Offset - a Bluetooth LE Example</a></li>
</ul>

<h1 id="footnotes">Footnotes</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:Nyqist" role="doc-endnote">
      <p>This is not entirely true. You can also apply a bandpass anti-aliasing filter 
       that only retains a part of the spectrum above the new sample rate, and use
       decimation to bring that section down to the baseband. But that’s a
       topic for a future blog post. <a href="#fnref:Nyqist" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:notation" role="doc-endnote">
      <p>\(\equiv\) means “is equivalent to.” <a href="#fnref:notation" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">The Scenic Route to Repairing a Self-Destructing SRS DG535 Digital Delay Generator</title><link href="https://tomverbeure.github.io/2025/12/24/Repair-of-SRS-DG535.html" rel="alternate" type="text/html" title="The Scenic Route to Repairing a Self-Destructing SRS DG535 Digital Delay Generator" /><published>2025-12-24T10:00:00+00:00</published><updated>2025-12-24T10:00:00+00:00</updated><id>https://tomverbeure.github.io/2025/12/24/Repair-of-SRS-DG535</id><content type="html" xml:base="https://tomverbeure.github.io/2025/12/24/Repair-of-SRS-DG535.html"><![CDATA[<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#the-stanford-research-systems-dg535" id="markdown-toc-the-stanford-research-systems-dg535">The Stanford Research Systems DG535</a></li>
  <li><a href="#who-uses-a-pulse-delay-generator" id="markdown-toc-who-uses-a-pulse-delay-generator">Who Uses a Pulse Delay Generator?</a></li>
  <li><a href="#inside-the-dg535" id="markdown-toc-inside-the-dg535">Inside the DG535</a></li>
  <li><a href="#the-annoying-mechanical-design-of-the-dg535" id="markdown-toc-the-annoying-mechanical-design-of-the-dg535">The Annoying Mechanical Design of the DG535</a></li>
  <li><a href="#its-always-the-power-supply" id="markdown-toc-its-always-the-power-supply">It’s Always the Power Supply</a></li>
  <li><a href="#power-architecture-of-the-dg535" id="markdown-toc-power-architecture-of-the-dg535">Power Architecture of the DG535</a></li>
  <li><a href="#the-how-why-and-please-dont-of-current-boost-resistor-circuits" id="markdown-toc-the-how-why-and-please-dont-of-current-boost-resistor-circuits">The How, Why, and Please Don’t of Current Boost Resistor Circuits</a></li>
  <li><a href="#root-causing-the-dg535-issue" id="markdown-toc-root-causing-the-dg535-issue">Root Causing the DG535 Issue</a></li>
  <li><a href="#debugging-the-7v-issue" id="markdown-toc-debugging-the-7v-issue">Debugging the +7V Issue</a></li>
  <li><a href="#side-quest-debugging-the-cpu-system---connector-stupidity" id="markdown-toc-side-quest-debugging-the-cpu-system---connector-stupidity">Side Quest: Debugging the CPU System - Connector Stupidity</a></li>
  <li><a href="#fixing-the-burnt-pcb-trace" id="markdown-toc-fixing-the-burnt-pcb-trace">Fixing the Burnt PCB Trace</a></li>
  <li><a href="#endless-boot-loop-after-reassembly" id="markdown-toc-endless-boot-loop-after-reassembly">Endless Boot Loop after Reassembly</a></li>
  <li><a href="#dg535-up-and-running-with-a-variac" id="markdown-toc-dg535-up-and-running-with-a-variac">DG535 Up and Running with a Variac</a></li>
  <li><a href="#tracking-down-the-12-12v-on-the-9-9v-rails" id="markdown-toc-tracking-down-the-12-12v-on-the-9-9v-rails">Tracking down the +12/-12V on the +9/-9V Rails</a></li>
  <li><a href="#lcd-replacement" id="markdown-toc-lcd-replacement">LCD Replacement</a></li>
  <li><a href="#post-mortem" id="markdown-toc-post-mortem">Post Mortem</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
  <li><a href="#footnotes" id="markdown-toc-footnotes">Footnotes</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>I got my hands on a <a href="https://www.thinksrs.com/products/dg535.html">Stanford Research Systems DG535</a>
at the <a href="https://www.thinksrs.com/products/dg535.html">Silicon Valley Electronics Flea Market</a>, 
$40 for a device that was marked “X Dead”.</p>

<p><img src="/assets/dg535/DG535_at_flea_market.jpg" alt="DG535 at flea market" /></p>

<p>That’s a really good deal: SRS products are pricey and even 
the cheapest <em>Parts-Only</em> listings on eBay are $750 and up. Worst case, I’d get a few weekends
of unsuccessful repair entertainment out of it, but even then I’d probably be able to recoup my money
by selling pieces for parts.<sup id="fnref:parts" role="doc-noteref"><a href="#fn:parts" class="footnote" rel="footnote">1</a></sup> Just the keyboard PCB is currently selling for $150<sup id="fnref:keyboard" role="doc-noteref"><a href="#fn:keyboard" class="footnote" rel="footnote">2</a></sup>.</p>

<p>It doesn’t matter how broken they are, the first step after acquiring a new toy is cleaning up
years of accumulated asset tracking labels, coffee stains, finger grime and glue residue. This one 
cleaned up nicely; the front panel is pretty much flawless:</p>

<p><img src="/assets/dg535/DG535_frontview.jpg" alt="DG535 front view" /></p>

<p>After an initial failed magic smoke repair attempt, the unit went back to the garage for
18 months, but last week I finally got around to giving it the attention it deserves.</p>

<p>The repair was successful, and when you only look at the end result, it was a straightforward 
replacement of a diode bridge and LCD panel. However, the road to get there was long and winding. 
The broken power architecture and awkward mechanical design of the SRS DG535 made it way too easy 
to damage the device <em>because</em> I was trying to repair it.</p>

<p>So let’s get this advise out of the way first:</p>

<p><strong>Do NOT power on the device with the analog PCB disconnected. It will almost certainly self-destruct
with burnt PCB traces.</strong></p>

<p>The details will be explained further below.</p>

<h1 id="the-stanford-research-systems-dg535">The Stanford Research Systems DG535</h1>

<p>Conceptually, the purpose of the DG535 is straightforward: it’s a tool that takes in an input
trigger pulse and generates 4 output pulses after some programmable delay. What makes things interesting 
is that these delays can be specified with a 5 ps precision, though the jitter on the outputs far
exceed that number.</p>

<p>The DG535 has 9 outputs on the front panel:</p>

<ul>
  <li>T0 marks the start of a timing interval. You’ll most likely use it when you use the device with 
an internal trigger to know when a timing sequence has started. There is delay of around 85ns 
between the external trigger and T0.</li>
  <li>4 channels A, B, C and D can independently be configured to change a programmable time after T0
or after some of the other channels.</li>
  <li>Output AB is a pulse for the interval between the time set for A and B. It’s an XNOR between those
2 channels. -AB is the inverse of output AB. CD and -CD are the same for channels C and D.</li>
</ul>

<p><a href="/assets/dg535/DG535_timing_diagram.jpg"><img src="/assets/dg535/DG535_timing_diagram.jpg" alt="DG535 timing diagram" /></a>
<em>(Click to enlarge)</em></p>

<p>All outputs support a number of logic standards: TTL, ECL, NIM<sup id="fnref:NIM" role="doc-noteref"><a href="#fn:NIM" class="footnote" rel="footnote">3</a></sup>, or fully programmable
voltage amplitude and offset.</p>

<p>Settings can be entered through the front panel or through a GPIB interface that is
available at the back of the device.</p>

<p><img src="/assets/dg535/DG535_rearview.jpg" alt="DG535 rear view" /></p>

<p>In addition to the GPIB interface, the back has another set of T0/A/B/C/D outputs because my unit
is equiped option 02. These outputs are not an identical copy of the ones in the front: their amplitudes
can go from -32V to 32V when terminated by a 50 Ohm impedance and each output has pulse width of roughly 
1 us.</p>

<p>There is also a connector and a switch to select either the internal or an external
10 MHz timebase. Missing screws around the transformer housing are an indication that I’m not the 
first one who has been inside to repair the unit.</p>

<p>This <a href="/assets/dg535/SRS_ad_1993.pdf">1993 ad</a> lists the DG535 for $3500. It is currently still for sale 
on the SRS website for $4495, remarkable for an instrument that dates from the mid 1980s. I assume 
that today’s buyers are primarily those who need an exact replacement for an existing, certified setup, 
because the <a href="https://www.thinksrs.com/products/dg645.html">DG645</a>, 
SRS’s more modern successor with better features and specs, costs only $500 more.</p>

<h1 id="who-uses-a-pulse-delay-generator">Who Uses a Pulse Delay Generator?</h1>

<p>Anyone who has a setup where multiple pieces of test or lab equipment need to work together with a
strictly timed sequence.</p>

<p>When you google for applications where the DG535 is used, you get a long list of PhD theses, national
or military laboratory documents, optical setups with lasers and so on. Look closer at the first 
picture of this blog post and you can see that mine was used by <a href="https://www.chemicaldynamics.com">Chemical Dynamics</a> 
in a molecular beam setup… whatever that is.</p>

<p>Here are just a few examples:</p>

<ul>
  <li>
    <p><a href="https://www.sciencedirect.com/science/article/pii/S0010218025001142">Combustion and Flame - A comprehensive study on dynamics of flames in a nanosecond pulsed discharge. Part II: Plasma-assisted ammonia and methane combustion</a></p>

    <blockquote>
      <p>We employed a delay generator (SRS system, DG535) to control the timing of the plasma 
and measurement systems. The DG535 generator was externally triggered by the pre-triggering 
signal from the laser system and then sent sequential TTL signals to trigger the ns 
pulse generator and camera.</p>
    </blockquote>
  </li>
  <li>
    <p><a href="https://arxiv.org/html/2510.11451v1">Astigmatism-free 3D Optical Tweezer Control for Rapid Atom Rearrangement</a></p>

    <blockquote>
      <p>Images were taken at delayed time steps (250-ns shutter, SRS DG535) as the translation 
stage was stepped from Z min = − 24.5 mm to Z max = 24.5 mm.</p>
    </blockquote>
  </li>
  <li>
    <p><a href="https://www.sciencedirect.com/science/article/pii/S0039914025009531">Rapid elemental imaging of copper-bearing critical ores using laser-induced breakdown spectroscopy coupled with PCA and PLS-DA</a></p>

    <blockquote>
      <p>A delay generator (SRS DG-535) synchronized the laser and detection systems to capture 
time-integrated spectra at each point.</p>
    </blockquote>
  </li>
  <li>
    <p><a href="https://www.researchgate.net/publication/231085177_High-precision_Gravity_Measurements_Using_Atom-Interferometry">High-precision Gravity Measurements Using Atom-Interferometry</a></p>

    <blockquote>
      <p>The timing of the pulsesis controlled by a set of synchronized pulse generators
(SRS DG535), one of which also triggers all the hardware involved in generating the 
Raman frequencies.</p>
    </blockquote>
  </li>
  <li>
    <p><a href="https://pubs.aip.org/aip/pop/article/32/12/122109/3374743/Laboratory-generation-of-multiple-periodic-arrays">Physics of Plasmas - Laboratory generation of multiple periodic arrays of Alfvénic vortices</a></p>

    <blockquote>
      <p>Each antenna was switched on with a pulse generator (Stanford SRS-DG535), which then activated 
two arbitrary waveform generators (Agilent 3322A).</p>
    </blockquote>
  </li>
  <li>
    <p><a href="https://www.researchgate.net/publication/382346349_Liquid-to-gas_transfer_of_sodium_in_a_liquid_cathode_glow_discharge">Liquid-to-gas transfer of sodium in a liquid cathode glow discharge</a></p>

    <blockquote>
      <p>The laser system, operating at a 200 Hz repetition rate, was synchronized with the 
plasma discharge using a SRS DG535 delay generator, allowing time-resolved measurements 
of Na fluorescence during and after the discharge pulse.</p>
    </blockquote>
  </li>
</ul>

<p>At this time, I don’t have a use for a pulse delay generator, but as a hobbyist it’s important 
to keep the following in mind:</p>

<p><strong>We buy test equipment NOT because we need it, but because one day we might need it.</strong></p>

<p>I don’t see a future where I’ll be doing high-precision gravity measurements in my garage, but
a DG535 could be useful to precisely time a voltage glitching pulse when trying to break the
security of a microcontroller, for example.</p>

<h1 id="inside-the-dg535">Inside the DG535</h1>

<p>It’s not complicated to create pulse delay generator as long as delay precision and jitter
requirements are larger than the clock period of the internal digital logic: a simple digital
counter will do. But when the timing precision is smaller than the clock period, you need 
some analog wizardry to make it happen.</p>

<p>SRS includes detailed schematics and theory of operation for many of their products and the
DG535 is no exception. It’s a great way to study and learn how non-trivial problems were solved 
40 years ago.</p>

<p><img src="/assets/dg535/DG535_block_diagram.jpg" alt="DG535 block diagram" /></p>

<p>The DG535 takes a combined digital/analog approach to create delays of up to 1000 s.
With an 80 MHz internal clock, the digital delay can be specified with 12.5 ns of precision.
The remainder is handled by two analog circuits: the jitter circuit measures the delay between 
the start of the external trigger and the next rising edge of the 80 MHz clock. The analog delay
circuit creates a delay between 0 and 12.5 ns after digital delay has expired. Channels A/B/C/D 
each have their own instance of the analog delay circuit.</p>

<p><a href="/assets/dg535/dg535-waveform.svg"><img src="/assets/dg535/dg535-waveform.svg" alt="Jitter/Digital Delay/Analog Delay waveform" /></a>
<em>(Click to enlarge)</em></p>

<p>I will leave the low level details to a future blog post, but at their core, both the jitter and 
analog delay circuit work by precharging and discharging a capacitor with a constant current
source for a time that varies between 0 and 12.5 ns. Precharging the analog delay capacitor is 
controlled by a 12-bit DAC. If you were wondering where the 5 ps of precision limit is coming from: 
12.5 ns / (2^12) = 3 ps. Close enough!</p>

<p>Using a capacitor to measure time with higher precision that the digital clock is called “analog 
interpolation”. It’s often used by time interval and frequency counters such as the SRS SR620. 
I briefly touch this in 
<a href="/2023/06/16/Frequency-Counting-with-Linear-Regression.html#frequency-counter-basics">my blog post about linear regression in frequency counters</a>.</p>

<h1 id="the-annoying-mechanical-design-of-the-dg535">The Annoying Mechanical Design of the DG535</h1>

<p>In <a href="/2025/08/19/SRS-SR620-Frequency-Counter-Power-Switch-Battery-Replacment.html#repairing-the-sr620">my blog post about the SR620</a>, 
I comment on a mechanical design that gives full access to all components by just removing 
the top and bottom cover. The DG535 is a different story.</p>

<p>While the covers are just as easy to remove, the functionality is spread over 2 large 
PCBs, mounted with components facing inwards, and connected with a bunch of cables 
that are too short to allow separating the PCBs.</p>

<p><a href="/assets/dg535/DG535_open_sideview.jpg"><img src="/assets/dg535/DG535_open_sideview.jpg" alt="DG535 open side view" /></a>
<em>(Click to enlarge)</em></p>

<p><a href="/assets/dg535/DG535_open_bottomview.jpg"><img src="/assets/dg535/DG535_open_bottomview.jpg" alt="DG535 open bottom view" /></a>
<em>(Click to enlarge)</em></p>

<p>SRS was clearly aware that this PCB arrangement makes the unit harder to repair,
because they helpfully added component designators and even component name annotations on 
the solder side of the PCB, though, sadly, there are no dots to mark pin 1 of an IC.<sup id="fnref:pin_dot" role="doc-noteref"><a href="#fn:pin_dot" class="footnote" rel="footnote">4</a></sup></p>

<p><img src="/assets/dg535/PCB_solderside_annotated.jpg" alt="PCB solder-side annotated" /></p>

<p>Most cables have connectors and can easily unplugged, but not all of them.</p>

<p><img src="/assets/dg535/power_supply_wires_to_opt02.jpg" alt="Power supply wires for OPT02 board" /></p>

<p>The red and orange wires in the picture above deliver +20 and -20V from the top PCB
to the OPT02 PCB that is mounted below the bottom PCB. They are just long enough. If you want to
take the unit apart, your only choice is desoldering these wires. It’s not rocket science,
but… really? You also need to desolder the wires that power the cooling fan.</p>

<p>Enough whining… for now. When all wires are desoldered, connectors disconnected and screws
removed, you can fold open the top PCB from the rest of the unit and get a full view of the inner
components:</p>

<p><a href="/assets/dg535/DG535_folded_open.jpg"><img src="/assets/dg535/DG535_folded_open.jpg" alt="DG535 folded open" /></a>
<em>(Click to enlarge)</em></p>

<p>We can see:</p>

<ul>
  <li>a top PCB that contains a Z80-based controller and the counters that are used for the digital
delay generation</li>
  <li>a bottom PCB with the rest of the delay and output driver circuitry</li>
  <li>the front has a generic LCD panel and a keyboard and LED PCB</li>
</ul>

<h1 id="its-always-the-power-supply">It’s Always the Power Supply</h1>

<p>Before taking it apart, I had already powered up the device and nothing happened: the LEDs 
and the LCD screen were dead, only the fan spun up. No matter what state a device is in,
you always have to make sure first that power rails are functional.</p>

<p>The power architecture is split between the top and bottom PCB, but the two secondary windings of 
the power transformer first go to the top PCB. <em>Since the transformer is located at the bottom,
you always need to keep top and bottom PCBs closely together if you want to make live measurements.</em></p>

<p><a href="/assets/dg535/power_supply_top.jpg"><img src="/assets/dg535/power_supply_top.jpg" alt="Power supply schematic top" /></a>
<em>(Click to enlarge)</em></p>

<p>On the schematic, we can see the output of integrated full-bridge rectifier BR601 go to linear regulators 
U601 and U503 to create +15V and -15V and then immediately to the connector on the right which goes to the 
bottom PCB. These voltage rails are not used by the top PCB.</p>

<p>A discrete diode bridge and some capacitors create an unregulated +/-9V that goes to the same connector
and to U501 / LM340-5, a linear +5V regulator that is functionally equivalent to a 7805. The 5V is used
to power pretty much the entire top PCB as well as some ICs on the bottom.</p>

<p>I measured the following voltages on the top-to-bottom power connector:</p>

<ul>
  <li>0V - instead of 10V</li>
  <li>+15V - good!</li>
  <li>+12V - instead of +9V</li>
  <li>+7V - instead of +5V. Horrible!</li>
  <li>GND</li>
  <li>0V - instead of -9V</li>
  <li>-15V - good!</li>
</ul>

<p>The lack of 10V is easy to explain: it’s an input, generated by a high precision voltage reference
on the bottom PCB out of the +15V. On the top PCB, it’s only used for dying gasp<sup id="fnref:gasp" role="doc-noteref"><a href="#fn:gasp" class="footnote" rel="footnote">5</a></sup> and power-on/off 
reset generation.</p>

<p>+12V instead of +9V was only a little bit concerning, at the time. The lack of -9V was clearly a problem.
And applying +7V instead of +5V to all digital logic is a great way to destroy all digital logic ICs.</p>

<p>Here’s the part of the PCB with the 9V diode bridge:</p>

<p><img src="/assets/dg535/diode_bridge.jpg" alt="9V diode bridge" /></p>

<p>Observations:</p>

<ul>
  <li>the discrete diodes look like a bodge</li>
  <li>marked in red, there is a blackened spot above-right of the diodes</li>
  <li>there is a green patch wire. There are quite a bit of those on the top PCB and they turned out to be 
harmless; they work around bugs in the PCB itself.</li>
</ul>

<p>2 discrete diodes were on the other side of the PCB to complete the discrete full bridge, though one soon
fell off. Underneath the discrete diodes is a footprint
for a BR501 full bridge rectifier <em>that is not in the schematic</em><sup id="fnref:schematic" role="doc-noteref"><a href="#fn:schematic" class="footnote" rel="footnote">6</a></sup>!</p>

<p><img src="/assets/dg535/br501_and_jumpers.jpg" alt="BR501 and jumpers" /></p>

<p>While doing these measurements, magic smoke appeared at the same location as 
blackened spot in the picture. At that point, I called it quits and left the unit sit for 18 months.</p>

<p>The schematic shows jumpers on the +/-15V and the +5V rail, see the orange rectangle
in the previous picture. These are intended for power measurements, but when removed
they also disconnect the not-at-all-5V rail from the digital logic and thus
protect it from further damage until I had sorted out the issue.</p>

<h1 id="power-architecture-of-the-dg535">Power Architecture of the DG535</h1>

<p>Since I suspected a problem with the discrete diode bridge bodge on the top PCB, the plan
was to repopulate the PCB with an integrated full-bridge rectifier. Turns out: even though
the schematic in the manual shows a discrete bridge, the schematic description in the same
manual indeed talks about an integrated full bridge. Instead of buying one at Digikey and pay 
$7 for shipping a $1 component, I found a suitable 100V/2A alternative, a 2KBP01M, at 
<a href="https://anchor-electronics.com">Anchor Electronics</a>, the last remaining Silicon Valley 
retail components supplier, conveniently located across the street from work.</p>

<p><img src="/assets/dg535/br501_replacement.jpg" alt="BR501 replacement" /></p>

<p>The footprint of the new diode bridge wasn’t quite the same, but you can easily nudge the
pins a bit to make it work.</p>

<p>I then had a look at the schematic of the bottom PCB power supply:</p>

<p><a href="/assets/dg535/power_supply_bottom.jpg"><img src="/assets/dg535/power_supply_bottom.jpg" alt="Bottom PCB power supply" /></a>
<em>(Click to enlarge)</em></p>

<p>More linear regulators, 2 on the unregulated +8V (?) rail to create +6V and +5.2V, and 3 on
the unregulated -8V rail to create -2V, -5.2V, and -6V (“actually -5.6V”).</p>

<p>Now here’s the interesting part: the -2V and -5.2V rails have heavy duty 5W 18 Ohm and 10 Ohm resistors 
between the input and the output of their linear regulator.</p>

<p><img src="/assets/dg535/current_boost_resistors.jpg" alt="Current boost resistors" /></p>

<p>These are called <em>current boost</em> resistors and while they are useful in the right
conditions, they are bad news. And when we go back to the top PCB, here’s what we see:</p>

<p><img src="/assets/dg535/5v_current_boost_resistor.jpg" alt="5V current boost resistor" /></p>

<p>It may not be in the schematic, but located below right next to the 5V regulator is
another 10 Ohm 5W current boost resistor.</p>

<h1 id="the-how-why-and-please-dont-of-current-boost-resistor-circuits">The How, Why, and Please Don’t of Current Boost Resistor Circuits</h1>

<p>The purpose of a current boost resistor is to partially offload a linear regulator.</p>

<p><img src="/assets/dg535/without_current_boost.png" alt="Power supply without current boost" /></p>

<p>Imagine we have design with a 5V rail and a load with an equivalent resistance of 3 Ohm,
good for a constant current draw of 1.67A. When we only use a linear regulator with 9V on the
input side, the current through the regulator will be 1.67A as well and the regulator 
needs to dissipate (9-5) * 1.67 = 6.7 W. That is too much for a 7805 in TO-220 package 
to handle: with the right heatsink, 1.5A is about the limit.</p>

<p><img src="/assets/dg535/current_boost_circuit.png" alt="Power supply with current boost resistor" /></p>

<p>With a 10 Ohm current boost resistor, the 7805 still supplies current to keep the voltage 
across the load at 5V, but the current boost resistor injects a constant current of (9-5)/10 = 0.4A. 
This reduces the current through the regulator from 1.67 A to 1.27 A and its power dissipation 
from 6.7W to 5.1W. The dissipation in the resistor is (9-5)^2 / 10 = 1.6 W. The total
power consumption remains the same: 5.1 W + 1.6 W = 6.7 W.</p>

<p>What have we gained? For the price of adding a beefy 10 Ohm resistor, we’re now staying 
within the current and power limits of the 7805 in TO-220 package. There is no need to
upgrade the 7805 to a much larger TO-3 package and the changes to the PCB are minimal.</p>

<p>But there is a price to pay! In fact, there’s more than one.</p>

<p><strong>Overvoltage risk when system load goes down</strong></p>

<p>A linear regulator can only supply current from input to output; it can’t sink current from 
output to input. If the system load drops below the 0.4A that’s supposed to be supplied by 
the current boost resistor, that 0.4A has nowhere to go and the voltage at the output
of the regulator has to go up.</p>

<p>We can see that here:</p>

<p><img src="/assets/dg535/current_boost_load_too_low.png" alt="Power supply with system load 90 Ohm instead of 3 Ohm" /></p>

<p>Assume that the system load has reduced and the equivalent system resistance is now 
90 Ohm instead of 3 Ohm. The current through the 2 resistors is just 0.09A. The voltage at the 7805 output
node is 8.1V and there is nothing the 7805 can do to bring the voltage down.</p>

<p><strong>No safeguards when input voltage goes up</strong></p>

<p>Another issue is when the input voltage increases. In the example below, it goes from
+9V to +12V. The power dissipation in the 7805 goes up a little bit, but the one in
the current boost resistor increases from 1.6W to 4.9W.</p>

<p><img src="/assets/dg535/current_boost_with_input_voltage_higher.png" alt="Power supply with input voltage 12V instead of 9V" /></p>

<p>The +12V that I measured on one of the connector is more than just a little bit concerning
after all.</p>

<p>Even without the current boost resistor, +12V at the input would be a real problem, since all 
the power of the resistor would have to be dissipated by the regulator. But with only a regulator, 
there is at least the possibility of including safeguards: there could be a current limiter, 
a temperature monitor, worst case, the regulator burns out and disconnects the output
from the input. With a dumb resistor you have none of that.</p>

<p>In 
<a href="/2025/08/10/HP-5370A-Repair.html#power-suppy-architecture">my 5370A repair blog post</a>,
I describe the current limiters that are part of its discrete linear voltage regulators: when the
current is too high, the output voltage is reduced. The DG535 has no such safety mechanism.</p>

<h1 id="root-causing-the-dg535-issue">Root Causing the DG535 Issue</h1>

<p>Let’s recap the issues that I had to deal with:</p>

<ul>
  <li>+7V on the +5V rail</li>
  <li>+12V instead of +9V at the input of the 7805 regulator</li>
  <li>A blackened PCB</li>
</ul>

<p>These issues were all related.</p>

<h1 id="debugging-the-7v-issue">Debugging the +7V Issue</h1>

<p>The +7V could be explained by the current boost resistor and a load that was too low. If the load is 
too low anyway, why not temporarily desolder the current boost resistor and check what happens? I did that 
and the voltage on the +5V rail predicably dropped down to +5V. The temperature on the 7805 remained 
in check. Good!</p>

<p>But why was the load too low?</p>

<p>A quick probe on the pins of the Z80 CPU showed no activity. Better yet: there was no clock!</p>

<p><a href="/assets/dg535/z80_clock_generator.jpg"><img src="/assets/dg535/z80_clock_generator.jpg" alt="Z80 clock generator" /></a>
<em>(Click to enlarge)</em></p>

<p>The 5 MHz CPU clock is derived from the 10 MHz clock, which comes from connector J40: the
cable that connects the top and bottom PCB. In other words: if you run the top PCB by itself, there is
no clock. And without a clock, the power consumption of the CPU system will be much lower… and
with a current boost resistor, the voltage will rise to +7V.</p>

<p>To run the CPU board stand-alone with an active clock, I configured 
<a href="/2023/01/02/HP33120A-Repair-Shutting-Down-the-Eye-of-Sauron.html">my HP 33120A signal generator</a> 
to generate a 10 MHz signal and routed its SYNC output to connector J40.</p>

<p><a href="/assets/dg535/10MHz_from_function_generator.jpg"><img src="/assets/dg535/10MHz_from_function_generator.jpg" alt="10MHz from function generator" /></a>
<em>(Click to enlarge)</em></p>

<p>In the picture above, in addition to the signal generator, you can also see an HP 3631A power
supply that outputs 10V: this is a replacement of the reference voltage that’s needed for the dying
gasp and reset generator that I mentioned earlier. These are the 2 external signals that are needed
to run the CPU top PCB without the analog bottom PCB, though only for a short time: without
current boost resistor and cooling fan, the 7805 was now taking on all the current and warming up
quickly.</p>

<p><strong>Important: The +12V issue was still there! As soon as the current boost resistor was placed
back, it was dissipating 5W and its temperature rose to 130C almost instantly!!!</strong></p>

<h1 id="side-quest-debugging-the-cpu-system---connector-stupidity">Side Quest: Debugging the CPU System - Connector Stupidity</h1>

<p>With the CPU clock running, I expected some activity on the keyboard/LED and LCD boards, but
the CPU seemed stuck.</p>

<p>It took a lot of effort to root cause this. I dumped the 
<a href="/assets/dg535/DG535_ROM_v2.0_SN4633.bin">ROM contents</a><sup id="fnref:ROM" role="doc-noteref"><a href="#fn:ROM" class="footnote" rel="footnote">7</a></sup>, used Ghidra to disassemble
the code. I also used a logic analyzer to trace the Z80 address bus to get a better insight into
what was happening, resulting in this pretty picture:</p>

<p><img src="/assets/dg535/logic_analyzer.jpg" alt="Logic analyzer on Z80" /></p>

<p>After many hours, the simple conclusion was this: the connector of the LCD panel cable was plugged
in incorrectly. This pulled high a crucial status bit on the data bus which made the Z80 go into
an endless loop.</p>

<p>I partially blame SRS for this: the way they deal with connector-related documentation is horrible,
unconventional, and inconsistent. Just look at this beauty:</p>

<p><img src="/assets/dg535/connector_docs.jpg" alt="Connector documentation" /></p>

<p>At the bottom right (red), they lay out a pinout convention. The keyboard/LED PCB (green) doesn’t
follow that convention. The LCD panel display does follow it, but this is a standard 14-pin
interface that’s used by an HD44780-based LCD controller which uses an entirely different convention.
They also don’t consistently mark pin 1 on the PCB.</p>

<p>Still, even after fixing that, the LCD didn’t come up. This turned out to be due to another
signal that came from the bottom PCB, the analog voltage that sets the LCD contrast. It was
sufficient to connect that to ground. That’s the blue wire that the red arrow is pointing to:</p>

<p><img src="/assets/dg535/LCD_up_and_running.jpg" alt="LCD up and running" /></p>

<p>The LCD was working now, but without backlight. The backlight of the original LCD panel
requires 120V AC with a 50 kOhm resistor in series. This voltage is coming straight from a
primary winding of the transformer. I measured 120V just fine, so the backlight was broken.
It doesn’t make the display entirely unreadable, but it’s definitely annoying.</p>

<h1 id="fixing-the-burnt-pcb-trace">Fixing the Burnt PCB Trace</h1>

<p>When I measured the voltages at the start of this journey, I noticed that the -9V was missing
on the power connector towards the analog PCB. The trace to this connector is running below
the overheating current boost resistor. All I needed to do was install a replacement
wire.</p>

<p><img src="/assets/dg535/burnt_pcb_trace_fix.jpg" alt="Bodge wire to fix the PCB trace" /></p>

<h1 id="endless-boot-loop-after-reassembly">Endless Boot Loop after Reassembly</h1>

<p>After going through the pain of reassembling the whole unit, I has hopeful that I’d be able
to at least operate the keyboard and see things happening on the LCD. That, of course,
didn’t happen. Instead, the unit got into an endless boot loop, showing the splash
screen, then going blank, repeat.</p>

<iframe width="680" height="400" src="https://www.youtube.com/embed/DJEbIihfNMo?si=c1rQnyQcja4wqpwB" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>This cost me another couple of hours to root cause. I disconnected all wires to the top PCB to
revert back to the condition where things worked before, but no luck. Eventually, I
stumbled onto the “Cold Boot” section in the user manual:</p>

<blockquote>
  <p>If the instrument turns on, but is completely unresponsive to the keyboard, then the RAM
contents may have been corrupted causing the instrument to “hang”. To remedy this situation,
turn the unit off, then hold down the BSP (backspace) key down and turn the unit back on
again.</p>
</blockquote>

<p>Like many old pieces of test equipment, the DG535 uses a 
<a href="https://www.digikey.com/en/products/detail/panasonic-energy/BR-2-3AE2SP/64350">BR-2/3A</a>
3V lithium battery to retain settings and calibration values while the unit is powered down.
The battery was still good when I measured its voltage, but maybe there was a 
short circuit due reassembly that made the SRAM loose its contents.</p>

<p>Either way, after following the power-up procedure from the manual the unit worked again.</p>

<p>Note that it’s not necessary to go through a full recalibration after losing the SRAM
contents: the EPROM that holds the firmware also contains calibration constants and the
serial number that are unique to each unit. That’s pretty cool! The calibration
constants are guarded by a checksum to ensure their correctness. What’s puzzling is that
the firmware checks the correctness, but when it detects an error, instead of reporting
a meaningful error, it does a system reset and retries again, leaving the operator
to guess what went wrong.</p>

<h1 id="dg535-up-and-running-with-a-variac">DG535 Up and Running with a Variac</h1>

<p>I still hadn’t tracked down the +12V/-12V on the +9V/-9V rails, but with everything else
fixed, I wondered if I could get the full unit to work. Just a few flea markets ago,
I had picked up a variac for $15. I always wondered why people need such a thing, and
wouldn’t you know it, this was the perfect use case: reduce the mains voltage from 120V AC
to ~100V AC to bring down the voltage on the secondary windings of the transformer.</p>

<p><img src="/assets/dg535/variac.jpg" alt="Variac on my bench" /></p>

<p>And just like that, the DG535 was working!</p>

<p><img src="/assets/dg535/dg535_and_oscilloscope_working.jpg" alt="DG535 with oscilloscope showing pulses" /></p>

<p>With my SR620 time interval counter and averaging a lot of measurements, I was even able 
to show that delays could be changed with 5 ps precision.</p>

<p>I measured a power consumption of 62W, not too far away from the 70W that’s specified in the
manual, which is just a case of being conservative. Right?</p>

<h1 id="tracking-down-the-12-12v-on-the-9-9v-rails">Tracking down the +12/-12V on the +9/-9V Rails</h1>

<p>I once again spent a long time trying to track down the 12V vs 9V issue. My only theories
were a short somewhere in the transformer, or some wires misconnected during an earlier repair,
or the original transformer being replaced by an incorrect one, but extensive and sometimes
questionable measurement practises didn’t turn up anything.</p>

<p><img src="/assets/dg535/questionable_measurement.jpg" alt="A very questionable measurement setup" /></p>

<p>Other than secondary winding voltages being too high, the transformer behaved fine.</p>

<p>I started a 
<a href="https://www.eevblog.com/forum/repair/rewinding-a-power-transformer/">thread on the EEVblog forum about rewinding a transformer</a>
where someone suggested that the output voltage of a transformer can be… load dependent. 
When only the CPU board was connected, I had measured an overall power consumption of 10W,
60W below specification.</p>

<p>I removed the variac from the setup and measured a power consumption of 72W. The
measured voltage on the +9V rail was +10.2V. Enough to raise the power consumption
in the 5V current boost resistor from 1.6W to 2.7W, but well within spec of its
5W rating.</p>

<p>The +12V issue was another manifestation of the lack of load resulting in a
self-distructing unit! And I had been chasing another ghost.</p>

<h1 id="lcd-replacement">LCD Replacement</h1>

<p>With the unit now fully working, all that remained was fixing the LCD backlight.
SRS sells a replacement LCD panel for a ridiculous $200. This must be old stock
because you can’t find ones anymore with a 120VAC backlight power supply.</p>

<p>Instead, I bought a <a href="https://www.crystalfontz.com/product/cfah2001btmiet-20x1-character-display-module">CFAH2001B-TMI-ET panel</a>
from crystalfontz.com.</p>

<p>It has a 16 pin instead of 14 pin interface, but the 2 additional pins are for
the backlight. The original LCD has separate pins for that.</p>

<p>The backlight has an LED with threshold voltage of 3.5v. The typical current is
48mA. The LED connector has a 5V pin already, but the top PCB creates this voltage
rail with a 5.1V zener diode and a series resistor from the +15V rail.<sup id="fnref:lcd_supply" role="doc-noteref"><a href="#fn:lcd_supply" class="footnote" rel="footnote">8</a></sup>
This rail can’t supply 48mA. Instead, I used the +5V pin of the keyboard/LCD
PCB nearby, with a 30 Ohm resistor in series, good for a current of (5-3.5)/30 = 50 mA.</p>

<p><img src="/assets/dg535/LCD_panel_working.jpg" alt="New LCD panel working" /></p>

<p>The new LCD panel is considerably thicker than the old one, so you can’t reuse the old
screws.</p>

<p><img src="/assets/dg535/LCD_panel_is_thicker.jpg" alt="LCD thickness comparison" /></p>

<p>I used Everbilt #4-40 3/8” machine screws from Home Depot instead. Be carefull
when tightening those new screws: it’s now possible to overdo things and
bend the LCD PCB.</p>

<p><img src="/assets/dg535/no_spacers_for_screws.jpg" alt="No spacers for the new screws" /></p>

<p>My unit had only 2 out of 4 transformer mounting screws in place. Home Depot didn’t 
have the #10-32 1 5/8” screws, but slightly shorter #10-32 1 1/2” screws worked fine.</p>

<p>After one more round of carefully connecting all connectors back in place, the DG535
was finally back to where it needed to be:</p>

<p><img src="/assets/dg535/DG535_with_new_LCD.jpg" alt="DG535 with new LCD" /></p>

<h1 id="post-mortem">Post Mortem</h1>

<p>A bunch of things went wrong during the design and repair of this DG535.</p>

<p>Design weaknesses:</p>

<ul>
  <li>Current boost resistors make a design prone to self-destruction
due to overvoltage when the system load is too low due to some internal
failure.</li>
  <li>Current boost resistors also result in burning out a PCB when
the voltage difference between input and output of a voltage regulator
becomes too high. This can again happen when the system load is lower
than designed for.</li>
  <li>The schematic in the manual shows a discrete diode full bridge for the
unregulated +/-9V rail, instead of an integrated one, and no current
boost resistor.</li>
  <li>The mechanical design and short cables make it tempting to power
the top PCB without connecting the bottom PCB… which cuts down the
system load dramatically.</li>
  <li>The power consumption of the top PCB is very low when the bottom
PCB is disconnected, due to the lack of 10 MHz clock.</li>
  <li>the pinout of the connectors of the DG535 doesn’t follow standard convention,
and the convention that is documented in the manual is violated on the
same page.</li>
  <li>The schematic of the top PCB shows a +/-9V rail. The bottom PCB schematic
shows +/-8V rails on the same connector pins. In reality, the measured voltage
is 10.2V. Confusing.</li>
</ul>

<p>Repair mistakes:</p>

<ul>
  <li>A previous attempt at repairing saw the replacement of an integrated
diode bridge by a discrete one. To make things worse, they used 1N5822 
Schottky diodes, as shown in the incorrect schematic. Schottky diodes
have a threshold voltage of 0.4V instead of a 0.7V threshold for the integrated 
diode bridge. Because of this, the unregulated DC output was 2 x (0.7 - 0.4V) = 0.6V 
higher, which increased the power consumption in the current boost resistors 
even more!</li>
  <li>PCBs were powered on without full load. This resulted in PCB traces burning up.</li>
  <li>Connectors were incorrectly plugged in. I should have taken pictures before
disconnecting anything.</li>
  <li>I knew not enough about transformers and wasted way too much time chasing
a ghost because of it!</li>
</ul>

<p>In the end, I only made 3 real fixes:</p>

<ul>
  <li>removed the discrete diode bridge and replaced it by an integrated one</li>
  <li>installed a bodge wire to bring the -9V to the top-to-bottom PCB power
connector</li>
  <li>replaced the LCD panel with broken backlight by a new one with diode 
backlight</li>
</ul>

<p>I got lucky that the 5V digital components survived being exposed to 7V. One
thing that I’ve learned over the years is that old ICs are pretty good at
surviving that kind of abuse.</p>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://www.thinksrs.com/products/dg535.html">Stanford Research - DG535 Digital Delay Generator</a></li>
  <li><a href="https://www.analog.com/en/resources/design-notes/single-resistor-provides-extra-current-from-a-linear-regulator.html">Analog Devices - Single Resistor Provides Extra Current from a Linear Regulator</a></li>
  <li><a href="https://www.eevblog.com/forum/repair/srs-stanford-research-dg535/">EEVblog forum - SRS Stanford Research DG535</a></li>
  <li><a href="https://www.eevblog.com/forum/repair/rewinding-a-power-transformer/">EEVblog forum - Rewinding a power transformer?</a></li>
</ul>

<h1 id="footnotes">Footnotes</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:parts" role="doc-endnote">
      <p>Not that I’ve ever done that, but it’s what I tell my wife. <a href="#fnref:parts" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:keyboard" role="doc-endnote">
      <p>Whether or not it will ever sell for that asking price is a different story. <a href="#fnref:keyboard" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:NIM" role="doc-endnote">
      <p>NIM stands for <a href="https://en.wikipedia.org/wiki/Nuclear_Instrumentation_Module">Nuclear Instrumentation Model</a>. 
    It’s a voltage and current standard for fast digital pulses for physics and nuclear experiments. <a href="#fnref:NIM" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:pin_dot" role="doc-endnote">
      <p>When dealing with mirror image of an IC footprint, I’m constantly
        second guessing myself about whether or not I’m probing the right pin. <a href="#fnref:pin_dot" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:gasp" role="doc-endnote">
      <p>When the +9V voltage rail drops below +7.5V, the dying gasp circuit creates a non-maskable
     interrupt to the CPU, allowing to quickly store data in non-volatile RAM before the power
     is completely gone. <a href="#fnref:gasp" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:schematic" role="doc-endnote">
      <p>I emailed SRS to ask if they had an updated schematic, but they told me
          to send in the unit for repair. <a href="#fnref:schematic" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:ROM" role="doc-endnote">
      <p>The ROM contents of each DG535 are unique for that particular unit, since they contain
    the serial number and calibration data that were determined in the factory. If you
    program the EPROM with my ROM file in your unit, expect delay specification to be
    significantly worse. <a href="#fnref:ROM" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:lcd_supply" role="doc-endnote">
      <p>I have no idea why SRS didn’t use the regular +5V rail to power the
           LCD panel. <a href="#fnref:lcd_supply" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Fixing LCD Screen Corruption of a Tektronix TDS220 Oscilloscope</title><link href="https://tomverbeure.github.io/2025/11/03/TDS220-LCD-Corruption-Fix.html" rel="alternate" type="text/html" title="Fixing LCD Screen Corruption of a Tektronix TDS220 Oscilloscope" /><published>2025-11-03T10:00:00+00:00</published><updated>2025-11-03T10:00:00+00:00</updated><id>https://tomverbeure.github.io/2025/11/03/TDS220-LCD-Corruption-Fix</id><content type="html" xml:base="https://tomverbeure.github.io/2025/11/03/TDS220-LCD-Corruption-Fix.html"><![CDATA[<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#the-tds220-oscilloscope" id="markdown-toc-the-tds220-oscilloscope">The TDS220 Oscilloscope</a></li>
  <li><a href="#opening-up-the-tds220" id="markdown-toc-opening-up-the-tds220">Opening Up the TDS220</a></li>
  <li><a href="#common-tds220-issues" id="markdown-toc-common-tds220-issues">Common TDS220 issues</a></li>
  <li><a href="#replacing-the-power-supply-capacitors" id="markdown-toc-replacing-the-power-supply-capacitors">Replacing the power supply capacitors</a></li>
  <li><a href="#lcd-panel-corruption" id="markdown-toc-lcd-panel-corruption">LCD Panel Corruption</a></li>
  <li><a href="#extracting-the-lcd-panel" id="markdown-toc-extracting-the-lcd-panel">Extracting the LCD Panel</a></li>
  <li><a href="#lcd-panel-capacitor-replacement" id="markdown-toc-lcd-panel-capacitor-replacement">LCD Panel Capacitor Replacement</a></li>
  <li><a href="#lcd-panel-backlight-replacement" id="markdown-toc-lcd-panel-backlight-replacement">LCD Panel Backlight Replacement</a></li>
  <li><a href="#fixing-the-square-wave-issue" id="markdown-toc-fixing-the-square-wave-issue">Fixing the Square Wave Issue</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
  <li><a href="#footnotes" id="markdown-toc-footnotes">Footnotes</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>I found a <a href="https://w140.com/tekwiki/wiki/TDS220">Tektronix TDS220 oscilloscope</a> at the
<a href="https://www.electronicsfleamarket.com">Silicon Valley Electronics Flea Market</a>. 
The seller told me that it worked but that the screen flickered a bit and that
this model is known to have issues with leaking capacitors. He asked $25 which 
would be a great price for any evening of entertainment even if an oscilloscope
wasn’t part of the deal, so I bought it.</p>

<p>Wise men claim that you should not power up an old device that with leaking capacitors, 
but I obviously did that anyway. The scope booted up nicely with some occasional screen 
corruption, as promised.</p>

<p><img src="/assets/tds220/screen_corruption.jpg" alt="Screen corruption" /></p>

<p><a href="https://youtu.be/Np21eQKw6sw?si=13aY1BOcO3j50V-m">This video</a> gives a better idea about the 
corruption. It’s intermittent and depends on the kind of content that is shown on the
screen. It also less prevalent when the scope has warmed up. All in all, it’s not a deal
breaker, the scope is perfectly usable as is, but it would nice to fix it.<sup id="fnref:flicker" role="doc-noteref"><a href="#fn:flicker" class="footnote" rel="footnote">1</a></sup></p>

<p>When connected to a signal generator it showed 2 sine waves:</p>

<p><img src="/assets/tds220/tds220_2_sine_waves.jpg" alt="TDS220 showing 2 sine waves" /></p>

<p>But when I connected the probe to the probe compensation pin, 
I got the signal below instead of a square wave:</p>

<p><img src="/assets/tds220/tds220_probe_compensation_waveform.jpg" alt="TDS220 definitely not showing a square waveform" /></p>

<p>The scope had this issue for both channels.</p>

<p>Alright, maybe I’d get more than just an evening of fun out of it.</p>

<h1 id="the-tds220-oscilloscope">The TDS220 Oscilloscope</h1>

<p>The TDS220 was introduced in 1997. It was a low cost oscilloscope with a limited
number of features, but with a weight of just 1.5kg/3.25lb and a small size, it was
great for technicians and for educational use. I’m not sure if it was Tektronix’ first 
oscilloscope with an LCD but, if not, it was definitely one of the early ones.</p>

<p>Some key characteristics:</p>

<ul>
  <li>2 channels</li>
  <li>100 MHz/1 Gsps</li>
  <li>2500 sample points per channel</li>
  <li>Only a few measurements: period, frequency, cycle RMS, mean and peak-to-peak voltage</li>
</ul>

<p>With a plug-in extension board, you can add a parallel, serial and GPIB port and FFT 
functionality, but even with those, it’s a really bare bones scope. And yet, I expect 
that I’ll be using it quite a bit: it’s so portable and the footprint is so small that 
it’s perfect for a quick measurement on a busy workbench.</p>

<p>Let’s take it apart!</p>

<h1 id="opening-up-the-tds220">Opening Up the TDS220</h1>

<p>Opening up the TDS220 isn’t hard, but you need to do the steps in the right order and there’s
a bit of bending-the-plastic involved.</p>

<p><strong>Remove handle and power button</strong></p>

<p><img src="/assets/tds220/tds220_handle_and_power_button.jpg" alt="TDS220 handle and power button" /></p>

<p>The handle must lay flat against the case to widen it and remove it. Also pull off the white
knob.</p>

<p><strong>Remove the 2 screws</strong></p>

<p>Once the handle it removed, you get access to 2 screws, one on each side. Remove
them with a Torx 15 screwdriver.</p>

<p><strong>Remove expansion module</strong></p>

<p>If you have a TDS2CM or TDS2MM expansion module, you need to remove it because
otherwise will block the case from coming off.</p>

<p>It took me longer than I care to admit to figure out how to do this. 
There is no need to play with the tab at the top of the module, just forcefully slide 
the thing upwards until it disconnects from the connector at the bottom.</p>

<p><img src="/assets/tds220/tds220_remove_expansion.jpg" alt="Sliding up the expansion module" /></p>

<p><strong>Pry off the back case</strong></p>

<p>This is the part that I always hate, because you need to figure which location is the
best to jam a screwdriver between 2 pieces of plastic. And based on the scuff marks in 
the picture below, others have struggled with it as well.</p>

<p><img src="/assets/tds220/tds220_remove_back_cover.jpg" alt="Remove back cover" /></p>

<p>But I think I found the best way to go about it now. At the right side, insert the
screwdriver horizontally between the blue and the white plastic and then lift the blue 
part. Insert a smaller screwdriver in the gap that you just made to prevent it from closing again
and repeat the same operation in the middle and the left.</p>

<p><strong>Inside exposed</strong></p>

<p>You can now take off the blue back cover and have a look at the inside of the scope.</p>

<p><a href="/assets/tds220/tds220_inside_exposed.jpg"><img src="/assets/tds220/tds220_inside_exposed.jpg" alt="TDS220 inside exposed" /></a>
<em>(Click to enlarge)</em></p>

<p>There are 2 PCBs: the left horizontal PCB contains all the acquistion and processing logic.
The one on the right is the power supply.</p>

<p><strong>Extract the power supply PCB</strong></p>

<p>To remove the power supply, unplug the orange bundle with 7 wires from the main PCB as 
well as the fat ground wire. The PCB is held in place by 2 plastic tabs at the bottom.</p>

<h1 id="common-tds220-issues">Common TDS220 issues</h1>

<p>Here are the most common TDS220 issues:</p>

<ul>
  <li>leaking capacitors in the power supply</li>
  <li>mechanical stress around the BNC connectors</li>
  <li>LCD backlight too weak or not working</li>
  <li>
    <p>Weak ground connection from BNC connector to power supply</p>

    <p>Tektronix issued a <a href="https://www.tek.com/en/services/safety/tds200/originalletterhtml">product recall</a> 
for this. My unit has components with dates that come after the product recall.
Check out <a href="https://www.youtube.com/watch?v=9N8UKwn4okM">this video</a> for a fix.</p>
  </li>
</ul>

<p>Not so common issue:</p>

<ul>
  <li>LCD screen corruption</li>
</ul>

<p>While I’ll document 3 of the 4 common repairs, you can find plenty of other
source on the web that do the same thing. That’s not the case for the LCD screen
corruption.</p>

<h1 id="replacing-the-power-supply-capacitors">Replacing the power supply capacitors</h1>

<p>I didn’t take pictures of it, but the solder side of the power supply PCB was drenched
in a light-brown/yellow-ish fluid. Some of that made it to the front side of the PCB
as can be seen here:</p>

<p><a href="/assets/tds220/fluid_marks.jpg"><img src="/assets/tds220/fluid_marks.jpg" alt="Fluid on front of the PCB" /></a>
<em>(Click to enlarge)</em></p>

<p>I’m not 100% sure about the source of this fluid because I was never able to pinpoint exactly 
which of the capacitors started leaking, but it’s fair to assume that this fluid was capacitor 
electrolyte. I decided to remove all electrolytic capacitors with new ones. There are 11 of them, 
listed in the table below:</p>

<p><strong>I used these components for my TDS220 recapping, but there is absolutely no guarantee
that these are the right ones. You need to double check everything yourself! Recapping
the scope is done at your own risk!</strong></p>

<table>
  <thead>
    <tr>
      <th><strong>#</strong></th>
      <th><strong>Indicator</strong></th>
      <th><strong>Capacitance</strong></th>
      <th><strong>Voltage</strong></th>
      <th><strong>Location</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1a</td>
      <td>C3</td>
      <td>47 uF</td>
      <td>450V</td>
      <td>Largest on the PCB</td>
    </tr>
    <tr>
      <td>1b</td>
      <td>C3</td>
      <td>68 uF</td>
      <td>450V</td>
      <td>Largest on the PCB</td>
    </tr>
    <tr>
      <td>2</td>
      <td>C13</td>
      <td>2200 uF</td>
      <td>6.3V</td>
      <td>Next to connector CN2</td>
    </tr>
    <tr>
      <td>3</td>
      <td>C12</td>
      <td>2200 uF</td>
      <td>6.3V</td>
      <td>Next to C13</td>
    </tr>
    <tr>
      <td>4</td>
      <td>C11</td>
      <td>2200 uF</td>
      <td>6.3V</td>
      <td>Next to C12</td>
    </tr>
    <tr>
      <td>5</td>
      <td>C14</td>
      <td>1000 uF</td>
      <td>6.3V</td>
      <td>Next to C11</td>
    </tr>
    <tr>
      <td>6</td>
      <td>C15</td>
      <td>470 uF</td>
      <td>6.3V</td>
      <td>Between C13 and C12</td>
    </tr>
    <tr>
      <td>7</td>
      <td>C21</td>
      <td>47 uF</td>
      <td>16V</td>
      <td>Close to “AULT KOREA”</td>
    </tr>
    <tr>
      <td>8</td>
      <td>C18</td>
      <td>22 uF</td>
      <td>35V</td>
      <td>Next to CN2</td>
    </tr>
    <tr>
      <td>9</td>
      <td>C6</td>
      <td>22 uF</td>
      <td>35V</td>
      <td>Next to IC1</td>
    </tr>
    <tr>
      <td>10</td>
      <td>C17</td>
      <td>4.7 uF</td>
      <td>50V</td>
      <td>Next to C16</td>
    </tr>
    <tr>
      <td>11</td>
      <td>C10</td>
      <td>2.2 uF</td>
      <td>50 V</td>
      <td>Next to CN2</td>
    </tr>
  </tbody>
</table>

<p>Pay attention to 1a and 1b: some TDS220 power supplies have a 47 uF, other have a 68 uF capacitor.
Mine had a 47 uF one. You don’t need to buy both of them.</p>

<p>I created <a href="https://www.digikey.com/en/mylists/list/NISC68K89D">this Digikey list</a> with
all these capacitors. At the time of writing this, the cost was $8.31, tax and shipping 
not included.</p>

<p><img src="/assets/tds220/pcb_glue.jpg" alt="PCB glue-like substance" /></p>

<p>On my unit, most capacitors were fixed to the PCB with a soft, glue-like substance.
Use an Exacto knife to cut it loose before desoldering a capacitor.</p>

<p>The PCB has markers for capacitor polarity. For smaller ones, it uses regular + and -
notation. For larger ones, a black circle indicates negative polarity.</p>

<p><img src="/assets/tds220/capacitor_polarity.jpg" alt="Capacitor polarity" /></p>

<p>All in all, the PSU recapping process is pretty straightforward and took around
1 hour to complete.</p>

<p>However, after power the scope back on, the screen corruption was still there!</p>

<h1 id="lcd-panel-corruption">LCD Panel Corruption</h1>

<p>The LCD screen corruption is content specific and it happens for a whole pixel row
at a time. I thought that it was caused by some signal corruption on the flat cable
between the main PCB and the LCD panel, but this was not case. I googled around
a bit, but couldn’t find any references to the issue that I was seeing, so I asked
on the EEVblog Repair forum. A few hours later, I got 
<a href="https://www.eevblog.com/forum/repair/tds220-lcd-content-dependent-screen-corruption/msg6027427/#msg6027427">the following reply</a>:</p>

<blockquote>
  <p>Please refer to the link above, the problem that occurs is very similar to your problem</p>
</blockquote>

<p>It included a link a Chinese forum that requires an account to get access to
photos and any pages beyond the first one, but <em>daisizhou</em> helpfully
posted those pictures in the EEVblog forum thread:</p>

<p><strong>You need to replace some capacitors that are inside the LCD panel!</strong></p>

<h1 id="extracting-the-lcd-panel">Extracting the LCD Panel</h1>

<p>Replacing the LCD panel capacitors is not complicated, but since an LCD panel assembly
is a bit fragile, you need to be careful to not destroy anything. Let’s first extract
the panel from the case.</p>

<p><strong>Remove the front panel knobs</strong></p>

<p>The panel knobs are the main components that are still keeping the front enclosure
attached to the main body. You can just pull them off.</p>

<p><img src="/assets/tds220/tds220_remove_front_buttons.jpg" alt="Remove front panel knobs" /></p>

<p><strong>Remove buttons PCB</strong></p>

<p>Unplug 2 flat cable connectors that links the main PCB to the buttons PCB and to
the LCD panel. 
<strong>Not shown: also unplug the power connector of the LCD panel.</strong> It’s right next
to the mains receptacle on the power PCB.</p>

<p><img src="/assets/tds220/tds220_unplug_button_connector.jpg" alt="Unplug buttons PCB connector" /></p>

<p>You can now remove the buttons PCB by pushing down 2 plastic tabs near the BNC
connectors.</p>

<p><img src="/assets/tds220/tds220_front_panel_removed.jpg" alt="Front panel removed" /></p>

<p>If the LCD panel was never removed before, chances are that the LCD front protector
sticks to the LCD panel itself. That is the case in the picture above. The protector
has a dark gray foam around a transparant piece of plastic.</p>

<p><strong>Remove LCD protector</strong></p>

<p>To get to the PCB inside the LCD panel, you need access to a screw that is covered
by the LCD protector. A weak adhesive keeps the protector in place. You can gently pull 
it from the LCD screen.</p>

<p><img src="/assets/tds220/LCD_protector.jpg" alt="LCD protector" /></p>

<p>There are 2 plastic tabs on the left of the LCD panel that keep it locked in place. Push
those up and down to unlock the the panel. You can now lift that left size away from the main
chassis and then slide the panel to the left to get the metal tab on the right out as well.</p>

<p><img src="/assets/tds220/tds220_front_with_only_LCD_panel.jpg" alt="Front with only LCD panel left" /></p>

<p>The panel is now loose. Remove the screw on the center left.</p>

<p><strong>LCD frame clips</strong></p>

<p>Turn the LCD panel around so that plastic back is towards you. Put something on the table
to protect the LCD front screen. I used the LCD protector for that.</p>

<p>We can now see the 3 capacitors that need to be replaced:</p>

<p><img src="/assets/tds220/LCD_frame_clips.jpg" alt="LCD frame clips" /></p>

<p>In addition to the screw from the previous step, the metal frame at the front of the
LCD panel is held in place by 8 metal tabs the bend into gaps of the plastic back. Use
nose pliers to straighten those tabs.</p>

<p>You can now remove the plastic back. Finally, you have access to the LCD PCB!</p>

<p><img src="/assets/tds220/LCD_PCB_exposed.jpg" alt="LCD PCB exposed" /></p>

<h1 id="lcd-panel-capacitor-replacement">LCD Panel Capacitor Replacement</h1>

<p>Here are the 3 capacitors in close-up. They’re 3.3 uF 35V polarized capacitors.</p>

<p><img src="/assets/tds220/LCD_PCB_zoom.jpg" alt="LCD PCB zoom" /></p>

<p>However, they are not your garden variety SMD tantalum capacitors! Notice how both
leads are on the same side of the capacitor. When we look at the other side, we can
see how the capacitor has a cylindrical core with a box plastic enclosure around it.</p>

<p><img src="/assets/tds220/LCD_capacitor_closeup.jpg" alt="LCD capacitor closeup" /></p>

<p>I couldn’t find any exact replacement. On that Chinese forum, they used regular
electrolytic caps instead, so that’s what I did as well.</p>

<p>The plastic back has cut-outs for the 3 capacitors. On the Chinese forum, they
made those cut-outs a bit larger to make the new capacitors fit, but that was not
necessary in my case: the holes were large enough as-is, as long as you took care to
solder them as close to the inside of the PCB as possible.</p>

<p>You can see this here:</p>

<p><img src="/assets/tds220/LCD_capacitors_soldered.jpg" alt="Replacement LCD capacitors soldered" /></p>

<p>The top capacitor is soldered too far to the left, the bottom one is fine.  I had to resolder 
the top capacitor one to make it fit in the cut-out.</p>

<p>One of the old LCD capacitors came in at 2.5 uF and a 75 Ohm ESR. The replacement ones
have an ESR of 6 Ohm…</p>

<p><img src="/assets/tds220/LCR_meter_result.jpg" alt="LCD meter result" /></p>

<p>With the capacitors replace, you can now put the LCD panel back together in reverse order.
But don’t mount it back into the chasses just yet!</p>

<h1 id="lcd-panel-backlight-replacement">LCD Panel Backlight Replacement</h1>

<p>The LCD panel uses a small CCFL tube as backlight. Over time, these CCFLs lose their
intensity which makes the screen less bright.</p>

<p>You access the CCFL tube by removing an easy to remove cover on the left of the panel:</p>

<p><img src="/assets/tds220/LCD_backlight.jpg" alt="LCD panel CCFL backlight blackened" /></p>

<p>Notice how some parts of the tube are black.</p>

<p>Replacement tubes can be found on eBay. Sellers vary, but just search for “CCFL lamp tds220”
and you’ll find what you need. Prices have gone up due to tariffs, I paid $17.46 including 
shipping.</p>

<p>The new lamp doesn’t come with the right connector, so some soldering is required to transfer
the connector from the old lamp to the new one. I first placed new tube in the LCD panel and
the LCD panel in the chassis before soldering the connector. That made it easier to get
the length of the wires correct.</p>

<p><img src="/assets/tds220/LCD_backlight_soldered_wires.jpg" alt="LCD backlight with connector replaced" /></p>

<p>After the replacement, the screen brightness was noticable… dimmer, but that’s normal:
new CCFL lamps needs a few minutes to reach their full brightness.</p>

<p><img src="/assets/tds220/screen_after_backlight_swap.jpg" alt="Screen after backlight swap" /></p>

<p>You can find plenty of videos on Youtube of this backlight swap and authors always claim
to see a significant improvement. I’m not so sure for my case: it’s not dimmer, but I can’t
honestly say that it’s much brighter. In one case, someone replaced the CCFL lamp 
<a href="http://hxc2001.free.fr/tektronix_tds220/index.html">with an LED PCB</a>. I looked around
for suitable LED PCBs to do that as well but didn’t find anything that worked. If you want to
try that, understand that the voltage of the CCFL lamp is much higher than the 5V or so you’d
need for LEDs!</p>

<h1 id="fixing-the-square-wave-issue">Fixing the Square Wave Issue</h1>

<p>The corrupted signal compensation square wave issue was solved by reheating the solder
of the BNC connector pins on the main PCB.</p>

<p><img src="/assets/tds220/BNC_connectors.jpg" alt="BNC connectors" /></p>

<p>To do this right, you need to remove the RF shielding at the bottom of the main PCB, but
I just squeezed my soldering iron into an open space and hope for the best. It worked:</p>

<p><img src="/assets/tds220/tds220_with_square_wave.jpg" alt="TDS220 with square wave" /></p>

<h1 id="conclusion">Conclusion</h1>

<p>The TDS220 is working perfect fine again. It measures signals correctly, there is no LCD screen 
corruption, and the brightness is fine. It’s still sitting on the bench, connected to a
logic analyzer, but that’s still a work in progress and may be a topic for a future blog post.</p>

<p><img src="/assets/tds220/tds220_connected_to_logic_analyzer.jpg" alt="TDS220 connected to logic analyzer" /></p>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://www.youtube.com/watch?v=weUSGjzEoVM">Tony Albus - Tektronix TDS220 Backlight Replace and Restore</a></li>
  <li><a href="https://www.youtube.com/watch?v=YF-kBXBnxzw">Max’s Garage - Repairing Two Digital Oscilloscopes from the 90s! Tektronix TDS210 and TDS220 Restoration</a></li>
  <li><a href="http://hxc2001.free.fr/tektronix_tds220/index.html">Tektronix TDS200 CCFL to LED backlight replacement</a></li>
  <li><a href="https://www.eevblog.com/forum/testgear/tektronix-tds210-teardown-and-bnc-replacement/msg1722653/#msg1722653">EEVblog forum - Recap list</a></li>
  <li><a href="https://www.eevblog.com/forum/repair/tektronix-tds-220-repair/">EEVblog forum - TDS 2002B repair</a></li>
  <li><a href="https://www.youtube.com/watch?v=9N8UKwn4okM">NFM - Tektronix TDS210 TDS220 Oscilloscope Recall and Loose BNC Fix</a></li>
</ul>

<h1 id="footnotes">Footnotes</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:flicker" role="doc-endnote">
      <p>In addition to the corruption, there’s also quite a bit of full-screen flicker.
        This is only a video recording artifact. There is no visible flicker. <a href="#fnref:flicker" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Inside an Isotemp OCXO107-10 Oven Controlled Crystal Oscillator</title><link href="https://tomverbeure.github.io/2025/10/26/Inside-an-Isotemp-OCXO107-10.html" rel="alternate" type="text/html" title="Inside an Isotemp OCXO107-10 Oven Controlled Crystal Oscillator" /><published>2025-10-26T10:00:00+00:00</published><updated>2025-10-26T10:00:00+00:00</updated><id>https://tomverbeure.github.io/2025/10/26/Inside-an-Isotemp-OCXO107-10</id><content type="html" xml:base="https://tomverbeure.github.io/2025/10/26/Inside-an-Isotemp-OCXO107-10.html"><![CDATA[<ul id="markdown-toc">
  <li><a href="#the-isotemp-ocxo107-10" id="markdown-toc-the-isotemp-ocxo107-10">The Isotemp OCXO107-10</a></li>
  <li><a href="#gathering-information-from-time-nuts" id="markdown-toc-gathering-information-from-time-nuts">Gathering Information from time-nuts</a></li>
  <li><a href="#getting-it-to-run" id="markdown-toc-getting-it-to-run">Getting It to Run</a></li>
  <li><a href="#on-the-bench" id="markdown-toc-on-the-bench">On the Bench</a></li>
  <li><a href="#inside-the-ocxo107-10" id="markdown-toc-inside-the-ocxo107-10">Inside the OCXO107-10</a></li>
  <li><a href="#looking-forward" id="markdown-toc-looking-forward">Looking Forward</a></li>
</ul>

<h1 id="the-isotemp-ocxo107-10">The Isotemp OCXO107-10</h1>

<p>I spent $5 at the <a href="https://www.electronicsfleamarket.com/">Silicon Valley Electronics Flea Market</a>
on an Isotemp OCXO107-10 oscillator.</p>

<p><img src="/assets/ocxo107-10/isoterm_ocxo107-10.jpg" alt="Isotemp OCXO107-10" /></p>

<p>Compared to my other OCXOs, this one is a real chonker, which is often correlates with 
its ability to keep the output frequency stable during changing environmental conditions: 
a large volume gives you more real estate for tricks to keep the internal temperature constant.</p>

<p>Despite the -10 suffix of the product name, it has an output frequency of 5 MHz, not
the 10 MHz that can be found on most equipment these days. 5 MHz used to be more 
popular; HP’s famous 5061A and 5071A Cesium atomic clocks have a 5 MHz output, for example,
and my <a href="/2025/08/10/HP-5370A-Repair.html">HP 5370A</a> 
and <a href="/2025/08/19/SRS-SR620-Frequency-Counter-Power-Switch-Battery-Replacment.html">SRS SR620</a>
time interval counters accept both 5 MHz and 10 MHz clocks on their external reference clock
input.</p>

<h1 id="gathering-information-from-time-nuts">Gathering Information from time-nuts</h1>

<p>I did some Google research and, to the surprise of no one, found a few scraps of information on the
<a href="http://leapsecond.com/time-nuts.htm">time-nuts email list</a>:</p>

<ul>
  <li>These oscillators used to cost more than <a href="https://www.febo.com/pipermail/time-nuts/2014-March/083620.html">$1000 a piece</a>.</li>
  <li>In addition to Isotemp, <a href="https://www.ctscorp.com/Products/Passive-Components/Frequency-Control-Products">CTS</a> 
Knights made a product with the <a href="https://www.febo.com/pipermail/time-nuts/2014-March/083623.html">same 0410-2450 SKU number</a>.</li>
  <li>These oscillators were used by Lucent. The CTS Knights unit has a 
<a href="https://www.febo.com/pipermail/time-nuts/2014-March/083625.html">date code of 1989</a>, 
well before AT&amp;T spun off its AT&amp;T Technologies business unit into Lucent in 1996.
My unit has a scribble of 1986.</li>
  <li>There’s an <a href="https://www.febo.com/pipermail/time-nuts/2014-March/083583.html">OCXO107-16 version</a>
which is also a 5 MHz option.</li>
  <li>Someone opened up his unit, did 
<a href="https://www.febo.com/pipermail/time-nuts/2014-March/083920.html">a bunch of stability measurements, and posted pictures</a>.
Those pictures have since disappeared, but I contacted the author, Ed Palmer, who graciously sent them
to me.</li>
  <li>One of the pins of the 9-pin connector of the OCXO107 is a reference voltage that
can be used to construct an EFC (electronic frequency control) input voltage to tune
the output frequency. There’s apparently quite a bit of 
<a href="https://febo.com/pipermail/time-nuts_lists.febo.com/2013-April/058247.html">noise on this Vref output</a>.</li>
  <li>There’s a <a href="/assets/ocxo107-10/ISOTEMP OCXO107 Series.pdf">datasheet</a> 
for an Isotemp OCXO107-3. It’s not identical to the OCXO107-10:
it has a different connector, uses more power, and there’s also mention of a 16-bit
D/A converter to discipline the output frequency. But chances are that some of the
characteristics are similar?</li>
  <li>Photo with <a href="https://www.febo.com/pipermail/time-nuts/2014-March/083616.html">pinout of the DE-9 connector</a>.</li>
</ul>

<p>That’s all I could find, but it’s more than enough to get started.</p>

<h1 id="getting-it-to-run">Getting It to Run</h1>

<p>The 107-10 has DE-9 connector for power and control and an SMA connector for the
clock output.</p>

<p><img src="/assets/ocxo107-10/connectors.jpg" alt="Isotemp OCXO107-10 connectors" /></p>

<p>The DE-9 pinout:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 - 5MHz TTL Out
2 - Ground
3 - +5V
4 - Ground
5 - +12V (Oven)
6 - Ground
7 - Ground
8 - EFC
9 - VREF 7.0V
</code></pre></div></div>

<p>The 5 V power rail is only used for the 5 MHz digital output. The OCXO will work fine and
output a sine wave on the SMA port when you leave this 5 V rail unconnected.</p>

<p><img src="/assets/ocxo107-10/pinout.jpg" alt="Isotemp OCXO107-10 pinout" /></p>

<h1 id="on-the-bench">On the Bench</h1>

<p>I don’t have a setup to make long-term measurements, but I just wanted to see if I could
get the thing to work. Here’s my earthquake-hardened bench setup:</p>

<p><a href="/assets/ocxo107-10/on_the_bench.jpg"><img src="/assets/ocxo107-10/on_the_bench.jpg" alt="Isotemp OCXO107-10 on the bench" /></a></p>

<p>One output of an HP E3631A power supply creates the 12 V rail, the other an EFC voltage that
is tuned to match 5 MHz output against the 10 MHz of my 
<a href="https://tomverbeure.github.io/2023/07/09/TM4313-GPSDO-Teardown.html">TM4313 GPSDO</a>.</p>

<p>When I power up the unit, the 12 V rail initially pulls around 320 mA (3.8W) to
heat up the internal oven. The current quickly drops below 100 mA and eventually settles
to 69 mA (0.83 mW.)</p>

<p><img src="/assets/ocxo107-10/spectrum.jpg" alt="Spectrum and harmonics of output signal " /></p>

<p>When fed into a 50 Ohm termination, my uncalibrated spectrum analyzer measures a power level 
of -1.80 dBm and a second harmonic of -55.04 dBm or -53.23 dBc. The output level is different than 
the &gt;+3 dBm that is listed in the datasheet for the OCXO107-3, but it is similar to what 
others on the time-nuts list have measured.</p>

<p>My unit has a tag to it that says:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1/8 2.47V
1/30 2.44V
4/2/86 2.54V
</code></pre></div></div>

<p>This must be the voltage level that’s required on the EFC input to tune the output frequency at 5 MHz.
In my current setup, that voltage level is roughly 2.228 V though that’s only 2 days after powering
it up. An OCXO107-10 needs about a week to truly stablize.</p>

<p>The Vref output measures 6.78 V, not too far off the expected 7 V.</p>

<h1 id="inside-the-ocxo107-10">Inside the OCXO107-10</h1>

<p>The OCXO has 4 solder points to weld the outside case to inside sliding assembly. I tried to 
get it open with a soldering iron, but the metal enclosure immediately dissipated the heat
away. I wasn’t able to open my unit, but luckily Ed gave permission to use his pictures. Let’s
have a look:</p>

<p><a href="/assets/ocxo107-10/Dewar &amp; Assembly.jpg"><img src="/assets/ocxo107-10/Dewar &amp; Assembly.jpg" alt="Dewar flask with electronis" /></a>
<em>(Click to enlarge)</em></p>

<p>All the components of the OCXO107 reside inside a <a href="https://en.wikipedia.org/wiki/Vacuum_flask">Dewar flask</a>.
Think coffee thermos with double sided wall with near-vacuum to reduce the heat transfer between the
center cavity and the outside world.</p>

<p>In the picture above, you see the Dewar flask on the right, the electronics slided-out on the left,
and an insulating foam on the far left to plug off the open side of the Dewar cylinder.</p>

<p>The Dewar flask makes the OCXO more resistant against varying outside temperatures, but it also makes the 
unit very expensive and fragile. Ed’s first unit wasn’t packaged correctly and arrived with a broken flask,
which makes the OCXO useless. These days, high stability OCXOs have one or two ovens and insulating material 
around it, though the website of Quantic Wenzel, producer of very high performance oscillators, says that
<a href="https://www.quanticwenzel.com/library/crystal-oscillator-tutorials/ocxos-oven-controlled-crystal-oscillators/">“units with Dewar flasks are still available for superior temperature performance and lower power consumption”</a>.</p>

<p>I’m too much of a beginner to compare the specifications of different OCXOs but I’ll give it a try anyway, 
so caveat emptor. The OCXO107-3 datasheet mentions a temperature stability of &lt; +/- 0.06 ppb for an 
ambient temperature between 0 C and 60 C.</p>

<p><img src="/assets/ocxo107-10/hp10811_specs.jpg" alt="HP 10811 specifications" /></p>

<p>The <a href="https://hparchive.com/Manuals/HP-10811AB-Manual.pdf">datasheet of the HP 10811 OCXO</a>
lists a frequency vs temperature sensitivity of &lt; 2.5 10^-9 between 0 C and 71 C. If that’s
apples to apples that would make the OCXO107-3 41 times more resistant against temperature variations.</p>

<p><img src="/assets/ocxo107-10/Rakon_ROX5242T1.png" alt="Rakon ROX5242T1 specs" /></p>

<p>I randomly searched for specs of contemporary double-oven OCXOs and found numbers from 0.1 ppb for a
<a href="https://www.rakon.com/products/ocxo-ocso/high-end-telecom-discrete-ocxo">Rakon ROX5242T1</a> 
and even 0.05 ppb, for units that are smaller and definitely less fragile. 
Just a case of old fashioned technological progress?</p>

<p>Note that temperature sensitivity is just one of many OXCO metrics. You also need to compare again 
voltage stability, phase noise and a whole bunch of other parameters, and select the one that
matches your needs. For example, the temperature sensitivity of a 10 MHz lab reference clock may be 
more important than phase noise, while the opposite can be true for an oscillator that’s used for multi-GHz
communication links.</p>

<p>After removing the copper heatsink, you can see the oscillator control board on top of a large crystal:</p>

<p><a href="/assets/ocxo107-10/Xtal &amp; Heatsink.jpg"><img src="/assets/ocxo107-10/Xtal &amp; Heatsink.jpg" alt="Xtal and heatsink" /></a>
<em>(Click to enlarge)</em></p>

<p>Here’s another view of this side of the assembly:</p>

<p><a href="/assets/ocxo107-10/Oscillator, Cover, and Unknown.jpg"><img src="/assets/ocxo107-10/Oscillator, Cover, and Unknown.jpg" alt="Oscillator and other stuff" /></a>
<em>(Click to enlarge)</em></p>

<p>If you turn around the assembly, you see this:</p>

<p><a href="/assets/ocxo107-10/Oven Controller.jpg"><img src="/assets/ocxo107-10/Oven Controller.jpg" alt="Oven heater" /></a>
<em>(Click to enlarge)</em></p>

<p>The blue component at the bottom is a Motorola JE800 Darlington transistor that is used as heating
element. Closeby, to the right of the orange capacitor, is an IC with 431 marking. It’s tempting
at first to speculate that this is a 
<a href="https://www.ti.com/product/TMP431">TMP431 temperature sensor</a>,
, but since those require a microcontroller to configure that’s unlikely. Maybe it’s 
<a href="https://www.ti.com/lit/ds/symlink/tl431.pdf">TL431 voltage reference</a> instead? Either way, 
there must be something on the PCB to measure voltage and feed that back to the heating
transistor to keep temperature stable.</p>

<h1 id="looking-forward">Looking Forward</h1>

<p>My home lab currently has 2 clock references: the TM4313 GPSDO and the free-running 
<a href="/2024/04/06/Guide-Tech-GT300-Frequency-Reference-Teardown.html">GT300 frequency standard</a>
that I tore down last year. I’ve been wanting to do a bunch of long-term comparative measurements
on a bunch of OCXOs, just for the fun of it. However, since crystal oscillators need a long
time to truly stabilize, think a week for the OCXO107, this is not something I want to do
with a power guzzling and noisy E3631A bench supply. The first step is to build a custom smaller scale
linear power supply just for this purpose. In other words: yet another project to put on the
stack!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Power Switch and Battery Replacement of an SR620 Universal Time Interval Counter</title><link href="https://tomverbeure.github.io/2025/08/19/SRS-SR620-Frequency-Counter-Power-Switch-Battery-Replacment.html" rel="alternate" type="text/html" title="Power Switch and Battery Replacement of an SR620 Universal Time Interval Counter" /><published>2025-08-19T10:00:00+00:00</published><updated>2025-08-19T10:00:00+00:00</updated><id>https://tomverbeure.github.io/2025/08/19/SRS-SR620-Frequency-Counter-Power-Switch-Battery-Replacment</id><content type="html" xml:base="https://tomverbeure.github.io/2025/08/19/SRS-SR620-Frequency-Counter-Power-Switch-Battery-Replacment.html"><![CDATA[<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#the-sr620" id="markdown-toc-the-sr620">The SR620</a></li>
  <li><a href="#repairing-the-sr620" id="markdown-toc-repairing-the-sr620">Repairing the SR620</a></li>
  <li><a href="#replacing-the-backup-3v-lithium-battery" id="markdown-toc-replacing-the-backup-3v-lithium-battery">Replacing the Backup 3V Lithium Battery</a></li>
  <li><a href="#switching-to-an-external-reference-clock" id="markdown-toc-switching-to-an-external-reference-clock">Switching to an External Reference Clock</a></li>
  <li><a href="#running-auto-calibration" id="markdown-toc-running-auto-calibration">Running Auto-Calibration</a></li>
  <li><a href="#oscilloscope-display-mode" id="markdown-toc-oscilloscope-display-mode">Oscilloscope Display Mode</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
  <li><a href="#footnotes" id="markdown-toc-footnotes">Footnotes</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>A little over a year ago, I found 
<a href="https://tomverbeure.github.io/2024/07/14/Symmetricom-S200-NTP-Server-Setup.html#introduction">a Stanford Research Systems SR620 universal time interval counter</a>
at the 
<a href="https://www.electronicsfleamarket.com">Silicon Valley Electronics Flea Market</a>. 
It had a big sticker “Passes Self-Test” 
and “Tested 3/9/24” (the day before the flea market) on it so I took the gamble and spent
an ungodly $400<sup id="fnref:cost" role="doc-noteref"><a href="#fn:cost" class="footnote" rel="footnote">1</a></sup> on it.</p>

<p><a href="/assets/s200/fleamarket_haul.jpg"><img src="/assets/s200/fleamarket_haul.jpg" alt="Flea Market Haul" /></a></p>

<p>Luckily, it <em>did</em> work fine, initially at least, but I soon discovered that it sometimes
got into some weird behavior after pressing the power-on switch.</p>

<h1 id="the-sr620">The SR620</h1>

<p>The SR620 was designed sometime in the mid-1980s. Mine has a rev C PCB with a date of July 1988,
37 year old! The manual lists 1989, 2006, 2019 and 2025 revisions. I don’t know if there were any
major changes along the way, but I doubt it. It’s still for sale on the 
<a href="https://www.thinksrs.com/products/sr620.html">SRS website</a>, starting at $5150.</p>

<p>The specifications are still pretty decent, especially for a hobbyist:</p>

<ul>
  <li>25 ps single shot time resolution</li>
  <li>1.3 GHz frequency range</li>
  <li>11-digit resolution over a 1 s measurement interval</li>
</ul>

<p>The SR620 is not perfect, one notable issue is its thermal design. It simply doesn’t have enough
ventilation holes, the heat-generating power regulators are located close to the high precision
time-to-analog converters, and the temperature sensor for the fan is inexplicably placed right
next to the fan, which is not close at all to the power regulators. The Signal Path has 
<a href="https://youtu.be/sDecJDgStcI?t=416">an SR620 repair video</a> that talks about this.</p>

<h1 id="repairing-the-sr620">Repairing the SR620</h1>

<p>You can see the power-on behavior in the video below:</p>

<iframe width="680" height="480" src="https://www.youtube.com/embed/pgqye6YGhBY?si=J_VlCNSirvTlY0vj" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>Of note is that lightly touching the power button changes the behavior and sometimes makes it get
all the way through the power-on sequence. This made me hopeful that the switch itself was bad,
something that should be easy to fix.</p>

<p>Unlike my still broken <a href="https://www.thinksrs.com/products/dg535.html">SRS DG535</a>, 
another flea market buy with the most cursed assembly, the SR620 is a dream to work on: 
4 side screws is all it takes to remove the top of the case and have access to all the components from 
the top. Another 4 screws to remove the bottom panel and you have access to the solder side of the PCB.
You can desolder components without lifting the PCB out of the enclosure.</p>

<p>Like <a href="/2025/08/10/HP-5370A-Repair.html">my HP 5370A</a>, the power switch of the SR620
selects between power on and standby mode. The SR620 enables the 15V rail at all times to 
keep a local TCXO or OCXO warmed up.</p>

<p>The power switch is located at the right of the front panel. It has 2 black and 2 red wires. When
the unit is powered on, the 2 black wires and the 2 red wires are connected to each other.</p>

<p><img src="/assets/sr620/switch_with_wires.jpg" alt="Switch with wires" /></p>

<p><img src="/assets/sr620/sr620_power_switch_schematic.jpg" alt="SR620 power switch schematic" /></p>

<p>To make sure that the switch itself was the problem, I soldered the wires together to create a
permanent connection:</p>

<p><img src="/assets/sr620/wires_soldered_together.jpg" alt="Wires soldered together" /></p>

<p>After this, the SR620 worked totall fine! Let’s replace the switch.</p>

<p>Unscrew 4 more screws and pull the knobs of the 3 front potentiometers and
power switch to get rid of the front panel:</p>

<p><img src="/assets/sr620/front_panel_removed.jpg" alt="Front panel removed" /></p>

<p>A handful of additional screws to remove the front PCB from the chassis, and you have access to the
switch:</p>

<p><img src="/assets/sr620/power_switch_exposed.jpg" alt="Power switch exposed" /></p>

<p>The switch is an ITT Schadow NE15 T70. Unsurprisingly, these are not produced anymore, but
you can still find them on eBay. I paid $7.5 + shipping, the price increased to $9.5 immediately
after that. According to 
<a href="https://www.eevblog.com/forum/repair/schadow-ne15-(hp)-power-switch-replacement-or-alternative/msg4342096/#msg4342096">this EEVblog forum post</a>,
<a href="https://www.digikey.com/en/products/detail/c-k/NE18-2U-EE-6AMP/7056067">this switch on Digikey</a>
is a suitable replacement, but I didn’t try it.</p>

<p>The old switch (bottom) has 6 contact points vs only 4 of the new one (top), but that wasn’t an
issue since only 4 were used. Both switches also have a metal screw plate, but they were oriented
differently. However, you can easily reconfigure the screw plate by straightening 4 metal prongs.</p>

<p><img src="/assets/sr620/old_and_new_switch.jpg" alt="Old and new switch" /></p>

<p>If you buy the new switch from Digikey and it doesn’t come with the metal screw plate, you should
be able to transplant the plate from the broken switch to the new one just the same.</p>

<p>To get the switch through the narrow hole of the case, you need to cut off the pins on the one
side of the switch and you need to bend the contact points a bit. After soldering the wires back 
in place, the SR620 powered on reliably.</p>

<p><img src="/assets/sr620/sr620_without_front_cover_powered_up.jpg" alt="SR620 powered up without front panel cover" /></p>

<p>Switch replacement completed!</p>

<h1 id="replacing-the-backup-3v-lithium-battery">Replacing the Backup 3V Lithium Battery</h1>

<p>The SR620 has a simple microcontroller system consists of a Z8800 CPU, 64 KB of EPROM and a
32 KB SRAM. In addition to program data, the SRAM also contains calibration and settings.<br />
To retain its contents when the AC power plug is removed, a non-recharchable 3V Panasonic BR-2/3A 
lithium battery keeps this SRAM powered at all times. You can find this battery in many old pieces 
of test equipment, I already
<a href="/2022/12/02/HP3478A-Multimeter-Calibration-Data-Backup-and-Battery-Replacement.html#replacement-3v-battery">replaced one such battery in my HP 3478A multimeter</a>.</p>

<p><img src="/assets/sr620/sr620_3v_battery.jpg" alt="SR620 schematic of 3V battery that power SRAM" /></p>

<p>These batteries last almost forever, but mine had a 1987 date code and 38 years is really pushing things,
so I replaced it with <a href="https://www.digikey.com/en/products/detail/panasonic-energy/BR-2-3AE2SP/64350">this new one from Digikey</a>.</p>

<p>The 1987 version of this battery had 1 pin on each side, on the new ones, the + side has 2 pins, so 
you need to cut one of those pins and install the battery slightly crooked back onto the PCB.</p>

<p><img src="/assets/sr620/battery_replacement.jpg" alt="Replacement battery installed" /></p>

<p>When you first power up the SR620 after replacing the battery, you might see “Test Error 3”
on the display. According to the manual:</p>

<blockquote>
  <p>Test error 3 is usually “self-healing”. The instrument settings will be returned to their 
default values and factory calibration data will be recalled from ROM. Test Error 3 will 
recur if the Lithium battery or RAM is defective.</p>
</blockquote>

<p>After power cycling the device again, the test error was gone and everything worked, but with
a precision that was slightly lower than before: before the battery replacement, when feeding the 
10 MHz output reference clock into channel A and measuring frequency with a 1s gate time, I’d get a 
read-out of 10,000,000.000<em>N</em> MHz. In other words: around a milli-Hz accuracy. After the replacment, 
the accuracy was about an order of magnitude worse. That’s just not acceptable!</p>

<p>The reason for this loss in accuracy is because the auto-calibration parameters were lost. 
Luckily, this is easy to fix.</p>

<h1 id="switching-to-an-external-reference-clock">Switching to an External Reference Clock</h1>

<p>My SR620 has the cheaper TCXO option which gives frequency measurement results that are about
one order of magnitude less accurate than using an external OCXO based reference clock. So
I always switch to an external reference clock. The SR620 doesn’t do that automatically, 
you need to manually change it in the settings, as follows:</p>

<ul>
  <li>SET -&gt; “<strong>ctrl</strong> cal out scn”</li>
  <li>SEL -&gt; “ctrl <strong>cal</strong> out scn”</li>
  <li>SET -&gt; “auto cal”</li>
  <li>SET -&gt; “cloc source int”</li>
  <li>Scale Down arrow -&gt; “cloc source rear”</li>
  <li>SET -&gt; “cloc Fr 10000000”</li>
  <li>SET</li>
</ul>

<p>If you have a 5 MHz reference clock, use the down or up arrow to switch between 1000000
and 5000000.</p>

<h1 id="running-auto-calibration">Running Auto-Calibration</h1>

<p>You can rerun auto-calibration manually from the front panel without opening up the device
with this sequence:</p>

<ul>
  <li>SET -&gt; “<strong>ctrl</strong> cal out scn”</li>
  <li>SEL -&gt; “ctrl <strong>cal</strong> out scn”</li>
  <li>SET -&gt; “auto cal”</li>
  <li>START</li>
</ul>

<p>The auto-calibration will take around 2 minutes. Only run it once the device has been running
for a while to make sure all components have warmed up and are at stable temperature. The manual 
recommends a 30 minute warmup time.</p>

<p>After doing auto-calibration, feeding back the reference clock into channel A and measuring
frequency with a 1 s gate time gave me a result that oscillated around 10 MHz, with the
mHz digits always 000 or 999.<sup id="fnref:precision" role="doc-noteref"><a href="#fn:precision" class="footnote" rel="footnote">2</a></sup></p>

<iframe width="640" height="480" src="https://www.youtube.com/embed/s0IHz3gXhrE?si=1_HjJGFwSBJaZ5iH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>It’s possible to fine-tune the SR620 beyond the auto-calibration settings. One reason 
why one might want to do this is to correct for drift of the internal oscillator 
To enable this kind of tuning, you need to move a jumper inside the case.
The time-nuts email list has a couple of discussions about this, here is
<a href="https://febo.com/pipermail/time-nuts_lists.febo.com/2011-February/037194.html">one such post</a>.
Page 69 of the <a href="https://www.thinksrs.com/downloads/pdfs/manuals/SR620m.pdf">SR620 manual</a> 
has detailed calibration instructions.</p>

<h1 id="oscilloscope-display-mode">Oscilloscope Display Mode</h1>

<p>When the 16 7-segment LEDs on the front panel are just not enough, the SR620 has this interesting
way of (ab)using an oscilloscope as general display: it uses XY mode to paint the data.</p>

<p>I had tried this mode in the past with my Sigilent digital oscilloscope, but the result was
unreadable: for this kind of rendering, having a CRT beam that lights up all the phosphor
from one point to the next is a feature, not a bug. This time, I tried it with an old
school analog oscilloscope<sup id="fnref:calibration" role="doc-noteref"><a href="#fn:calibration" class="footnote" rel="footnote">3</a></sup>:</p>

<p><a href="/assets/sr620/sr620_oscilloscope_mode.jpg"><img src="/assets/sr620/sr620_oscilloscope_mode.jpg" alt="oscilloscope on top of an SR620 showing a rendered image" /></a>
<em>(Click to enlarge)</em></p>

<p>The result is much better on the analog scope, but still very hard to read. When you really 
need all the data you can get from the SR620, just use the GPIB or RS232 interface.</p>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://www.youtube.com/watch?v=sDecJDgStcI">The Signal Path - TNP #41 - Stanford Research SR620 Universal Time Interval Counter Teardown, Repair &amp; Experiments</a></li>
  <li><a href="https://www.prc68.com/I/TandFTE.shtml#SR620">Some calibration info about the SR620</a></li>
  <li>
    <p><a href="https://www.prc68.com/I/FTS4060.shtml#SR620Fast">Fast High Precision Set-up of SR 620 Counter</a></p>

    <p>The rest of this page has a bunch of other interesting SR620 related comments.</p>
  </li>
</ul>

<p><strong>Time-Nuts topics</strong></p>

<p>The SR620 is mentioned in tons of threads on the time-nuts emaiml list. Here are just a few interesting posts:</p>

<ul>
  <li>
    <p><a href="https://www.febo.com/pipermail/time-nuts/2012-December/072305.html">This post</a> talks about some thermal
design mistakes in the SR620. E.g. the linear regulators and heat sink are placed right next to the
the TCXO. It also talks about the location of the thermistor inside the fan path, resulting in unstable
behavior. This is something Shrirar of The Signal Path fixed by moving the thermistor.</p>
  </li>
  <li>
    <p><a href="https://www.febo.com/pipermail/time-nuts/2012-December/072302.html">This comment</a> mentions that while the
TXCO stays powered on in standby, the DAC that sets the control voltage does not, which results in an additional
settling time after powering up. General recommendation is to use an external 10 MHz clock reference.</p>
  </li>
  <li>
    <p><a href="https://www.febo.com/pipermail/time-nuts/2012-December/072369.html">This comment</a> talks about warm-up
time needed depending on the desired accuracy. It also has some graphs.</p>
  </li>
</ul>

<h1 id="footnotes">Footnotes</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:cost" role="doc-endnote">
      <p>This time, the gamble paid off, and the going rate of a good second hand SR620
     is quite a bit higher. But I don’t think I’ll ever do this again! <a href="#fnref:cost" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:precision" role="doc-endnote">
      <p>In other words, when fed with the same 10 MHz as the reference clock, the
          display always shows a number that is either 10,000,000,000x or 9,999,999,xx. <a href="#fnref:precision" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:calibration" role="doc-endnote">
      <p>I find it amazing that this scope was calibrated as recently as April 2023. <a href="#fnref:calibration" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Repairing an HP 5370A Time Interval Counter</title><link href="https://tomverbeure.github.io/2025/08/10/HP-5370A-Repair.html" rel="alternate" type="text/html" title="Repairing an HP 5370A Time Interval Counter" /><published>2025-08-10T10:00:00+00:00</published><updated>2025-08-10T10:00:00+00:00</updated><id>https://tomverbeure.github.io/2025/08/10/HP-5370A-Repair</id><content type="html" xml:base="https://tomverbeure.github.io/2025/08/10/HP-5370A-Repair.html"><![CDATA[<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
    jax: ["input/TeX", "output/HTML-CSS"],
    tex2jax: {
      inlineMath: [ ['$', '$'], ["\\(", "\\)"] ],
      displayMath: [ ['$$', '$$'], ["\\[", "\\]"] ],
      processEscapes: true,
      skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
    }
    //,
    //displayAlign: "left",
    //displayIndent: "2em"
  });
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS_HTML" type="text/javascript"></script>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#inside-the-hp-5370a" id="markdown-toc-inside-the-hp-5370a">Inside the HP 5370A</a></li>
  <li><a href="#high-stability-reference-clock-with-an-hp-10811-60111-ocxo" id="markdown-toc-high-stability-reference-clock-with-an-hp-10811-60111-ocxo">High Stability Reference Clock with an HP 10811-60111 OCXO</a></li>
  <li><a href="#rifa-capacitors-in-the-corcom-f2058-power-entry-module" id="markdown-toc-rifa-capacitors-in-the-corcom-f2058-power-entry-module">RIFA Capacitors in the Corcom F2058 Power Entry Module?</a></li>
  <li><a href="#15v-rail-issues" id="markdown-toc-15v-rail-issues">15V Rail Issues</a></li>
  <li><a href="#power-suppy-architecture" id="markdown-toc-power-suppy-architecture">Power Suppy Architecture</a></li>
  <li><a href="#fault-isolation---its-the-reference-frequency-buffer-pcb" id="markdown-toc-fault-isolation---its-the-reference-frequency-buffer-pcb">Fault Isolation - It’s the Reference Frequency Buffer PCB!</a></li>
  <li><a href="#the-reference-frequency-buffer-board" id="markdown-toc-the-reference-frequency-buffer-board">The Reference Frequency Buffer Board</a></li>
  <li><a href="#fixing-the-internal-reference-clock" id="markdown-toc-fixing-the-internal-reference-clock">Fixing the Internal Reference Clock</a></li>
  <li><a href="#fixing-the-external-reference-clock" id="markdown-toc-fixing-the-external-reference-clock">Fixing the External Reference Clock</a></li>
  <li><a href="#future-work" id="markdown-toc-future-work">Future work</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
  <li><a href="#footnotes" id="markdown-toc-footnotes">Footnotes</a></li>
</ul>

<h1 id="introduction">Introduction</h1>

<p>I bought an HP 5370A time interval counter at the 
<a href="https://www.electronicsfleamarket.com">Silicon Valley Electronics Flea Market</a> for
a cheap $40.</p>

<p><img src="/assets/hp5370a/hp5370a_on_desk.jpg" alt="HP 5370A sitting on a desk" /></p>

<p>The 5370A is a pretty popular device among <a href="http://leapsecond.com/time-nuts.htm">time nuts</a>: 
it has a precision of 20ps for single-shot time interval measurements, amazing for a device that was 
released in 1978, and even compared to contemporary time interval counters it’s still a decent performance. 
The 74LS chips in mine have a 1981 time code which makes the unit a whopping 44 years old.</p>

<p>But after I plugged it in and pressed the power button, smoke and a horrible smell came out after a 
few minutes.</p>

<p>I had just purchased myself hours of entertainment!</p>

<h1 id="inside-the-hp-5370a">Inside the HP 5370A</h1>

<p>It’s trivial to open the 5370A:</p>

<ul>
  <li>remove the 4 feet in the back by removing the Philips screws inside them.</li>
  <li>remove a screw to release the top or bottom cover</li>
</ul>

<p><a href="/assets/hp5370a/hp5370a_top_cover_removed.jpg"><img src="/assets/hp5370a/hp5370a_top_cover_removed.jpg" alt="HP 5370A top cover removed" /></a>
<em>(Click to enlarge)</em></p>

<p>Once inside, you can see an extremely modular build: the center consists of a motherboard 
with 10 plug-in PCBs, 4 on the left for an embedded computer that’s based on an MC6800 CPU,
6 on the right for the time acquisition.  The top has plug-in PCBs as well, with the power supply on the left
and reference clock circuitry on the right. My unit uses the well known 
<a href="https://hparchive.com/Manuals/HP-10811AB-Manual.pdf">HP 10811-60111</a>
high-stability <a href="https://en.wikipedia.org/wiki/Crystal_oven">OCXO</a> as 10MHz clock reference. 
The bottom doesn’t have plug-in PCBs. It has PCBs for trigger logic and the front panel.</p>

<p>This kind of modular build probably adds significant cost, but it’s a dream for servicing 
and tracking down faults. To make things even easier, the vertical PCBs have a plastic ring 
or levers to pull them out of their slot! There are also plenty of generously sized test pins 
and some status LEDs.</p>

<p><img src="/assets/hp5370a/plastic_pull_ring.jpg" alt="Plastic pull-ring on PCB" /></p>

<h1 id="high-stability-reference-clock-with-an-hp-10811-60111-ocxo">High Stability Reference Clock with an HP 10811-60111 OCXO</h1>

<p>Since the unit has the high stability option, I have now yet another piece of test equipment
with an HP 10811-60111. OCXOs are supposed to be powered on at all time: environmental 
changes tend to stress them out and result in a deviation of their clock speed, which is why 
there’s a “24 hour warm-up” sticker on top of the case. It can indeed take a while for 
an OCXO to relax and settle back into its normal behavior, though 24 hours seems a bit excessive.</p>

<p>The 5370A has a separate always-on power supply just for the oven of the OCXO to keeps the crystal 
at constant temperature even when the power switch on the front is not in the ON position. Luckily, 
the fan is powered off when the front switch is set to stand-by.<sup id="fnref:hp8656a" role="doc-noteref"><a href="#fn:hp8656a" class="footnote" rel="footnote">1</a></sup></p>

<p><img src="/assets/hp5370a/power_controller_and_ref_clock.jpg" alt="Main power regulator, OCXO, OCXO power regulator and clock circuit" /></p>

<p>In the image above, from top to bottom, you can see:</p>

<ul>
  <li>the main power supply control PCB</li>
  <li>the HP 10811-60111 OCXO. To the right of it is the main power relay.</li>
  <li>the OCXO oven power supply</li>
  <li>the reference frequency buffer PCB</li>
</ul>

<p>These are the items that will play the biggest role during the repair.</p>

<h1 id="rifa-capacitors-in-the-corcom-f2058-power-entry-module">RIFA Capacitors in the Corcom F2058 Power Entry Module?</h1>

<p><em>Spoiler: probably not…</em></p>

<p>After plugging in the 5370A the first time, magic smoke came out of it along with a pretty
disgusting chemical smell, one that I already knew from some work that I did on my
<a href="/2024/02/22/HP-8656A-Schematics.html">HP 8656A RF signal generator</a>.</p>

<p>I unplugged the power, opened up the case, and looked for burnt components but couldn’t
find any. After a while, I decided to power the unit back on and… nothing. No smoke,
no additional foul smell, but also no display.</p>

<p>One of common failure mode of test equipment from way back when are RIFA capacitors that sit 
right next to the mains power input, <em>before</em> any kind of power switch. Their primary function
is to filter out high frequency noise that’s coming from the device and reduce EMI.
RIFAs have a well known tendency to crack over time and eventually catch fire. A couple of years ago, I
<a href="/2022/12/02/HP3478A-Multimeter-Calibration-Data-Backup-and-Battery-Replacement.html#replacing-the-capacitors">replaced the RIFA capacitors of my HP 3457A</a>,
but a general advice is to inspect all old equipment for these gold colored capacitors. However,
no such discrete capacitors could be found.</p>

<p><img src="/assets/hp5370a/Corcom_F2058.jpg" alt="Corcom F2058 line power module" /></p>

<p>But that doesn’t mean they are not there: like a lot of older HP test equipment, the 5370A uses 
a Corcom F2058 line power module that has capacitors embedded inside.</p>

<p>Below is the schematic of the Corcom F2058 (HP part number 0960-0443). The capacitors are marked
in red. You can also see a fuse F1, a transformer and, on the right, a selector that can be used
to configure the device for 100V, 115V/120V, 220V and 230V/240V operation.</p>

<p><a href="/assets/hp5370a/Corcom_F2058_schematic.jpg"><img src="/assets/hp5370a/Corcom_F2058_schematic.jpg" alt="Corcom F0258 schematic" /></a>
<em>(Click to enlarge)</em></p>

<p>There was a bad smell lingering around the Corcom module, so I removed it to check it out. There
are metal clips on the left and right side that you need to push in to get the module out. It
takes a bit of wiggling, but it works out eventually. Once removed, however, the Corcom didn’t
really have a strong smell at all. I couldn’t find any strong evidence online that these modules
have RIFAs inside them, so for now, my conclusion is that they don’t have them and that there’s
no need to replace them.</p>

<p><strong>Module replacement</strong></p>

<p>In the unlikely case that you want to replace the Corcom module, you can use this
$20
<a href="https://www.mouser.com/ProductDetail/Astrodyne-TDI/082SM.00300.00?qs=eP2BKZSCXI7mqcKDec2Dzg%3D%3D">AC Power Entry Module</a>
from Mouser. One reason why you might want to do this is because the new module has a built-in
power switch. If you use an external 10 MHz clock reference instead of the 10811 OCXO, then 
there’s really no need to keep the 5370A connected to the mains all the time.</p>

<p>There are two caveats, however:</p>

<ul>
  <li>
    <p>while it has the same dimensions as the Corcom F2058, the power terminals are located at the very back,
not in an indented space. This is not a problem for the 5370A, which still has enough room for both, but 
it doesn’t work for most other HP devices that don’t have an oversized case. You can see that in the
picture below:</p>

    <p><img src="/assets/hp5370a/corcom_mains_input_blocks.jpg" alt="Corcom and replacement AC Power Entry Module" /></p>
  </li>
  <li>
    <p>Unlike the Corcom F2058, the replacement only feeds through the line, neutral and ground that’s fed into it. 
You’d have to choose one configuration, 120V in my case, and wire a bunch of wires together to drive the 
transformer correctly. If you do this wrong, the input voltage to the power regulator will either be too low, 
and it wont work, or too high, and you might blow up the power regulation transistors. It’s not super 
complicated, but you need to know what you’re doing.</p>
  </li>
</ul>

<h1 id="15v-rail-issues">15V Rail Issues</h1>

<p>After powering the unit back up, it still didn’t work, but thanks to the 4 power rail status LEDs, it 
was immediately obvious that +15V power rail had issues.</p>

<p><img src="/assets/hp5370a/status_leds.jpg" alt="4 status LEDs on the power controller PCB. The left one is off" /></p>

<p>A close-by PCB is the reference frequency buffer PCB. It has a “10 MHz present” status
LED that didn’t light up either, suggesting an issue with the 10811 OCXO, but I soon figured
out that this status LED relies on the presence of the 15V rail.</p>

<h1 id="power-suppy-architecture">Power Suppy Architecture</h1>

<p>The 5370A was first released in 1978, decades before HP decided to stop including detailed
schematics in their service manuals. Until Keysight, the Company Formerly Known as HP, decides
to change its name again, you can download the operating and service manual
<a href="https://www.keysight.com/us/en/assets/9018-06037/user-manuals/9018-06037.pdf">here</a>. If you
need a higher quality scan, you can also purchase the manual for $10 from 
<a href="https://artekmanuals.com/">ArtekManuals</a><sup id="fnref:artek" role="doc-noteref"><a href="#fn:artek" class="footnote" rel="footnote">2</a></sup>. The diagrams below were copied from
the Keysight version.</p>

<p><img src="/assets/hp5370a/power_supply_block_diagram.jpg" alt="Power supply block diagram" /></p>

<p>The power supply architecture is straightforward: the line transformer has 5 separate windings,
4 for the main power supply and 1 for the always-on OCXO power supply. A relay is used to
disconnect the 4 unregulated DC rails from the power regulators when the front power button is in stand-by 
position, but the diode rectification bridge and the <em>gigantic</em> smoothing capacitors are located before
the relay.<sup id="fnref:bleeding_resistor" role="doc-noteref"><a href="#fn:bleeding_resistor" class="footnote" rel="footnote">3</a></sup></p>

<p>For each of the 4 main power rails, a discrete linear voltage regulator is built
around a power transistor, an LM307AN opamp and a smaller transistor for over-current protection,
and a fuse.  The 4 regulators share a 10V voltage reference.</p>

<p>The opamps and the voltage reference are powered by a simple +16.2V power rail built out of
a resistor and a Zener diode.</p>

<p><a href="/assets/hp5370a/power_supply_schematic.jpg"><img src="/assets/hp5370a/power_supply_schematic.jpg" alt="Power supply block schematic" /></a>
<em>(Click to enlarge)</em></p>

<p>The power regulators for the +5V and -5.2V rails have a current sense resistor of 0.07 Ohm. The
sense resistors for the +15V and -15V rails have a value of 0.4 Ohm. When the voltage across
these resistors exceeds the 0.7V base-emitter potential of the bipolar transistors across them,
the transistors start to conduct and pull down the base-emitter voltage of the power transistor,
thus shutting them off. <em>In the red rectangle of the schematic above, the +15V power transistor 
is on the right, the current control transistor on the left, and current sense resistor R4 is right 
next to the +15V label.</em></p>

<p>Using the values of 0.4 Ohm, 0.07 Ohm and 0.7V, we can estimate that the power regulators
enter current control (and reduce the output voltage) when the current exceeds 10A for the 
+5/-5.2V rails and 1.5A for the +15/-15V rails. This more or less matches the value of the fuses, 
which are rated at 7A and 1.5A respectively.</p>

<p>Power loss in these high current linear regulators is signficant and the heat sink in the back
become pretty hot. Some people have installed an external fan to cool it down a bit.</p>

<h1 id="fault-isolation---its-the-reference-frequency-buffer-pcb">Fault Isolation - It’s the Reference Frequency Buffer PCB!</h1>

<p>I measured a voltage of 8V instead of 15V. I would have prefered if I had measured no voltage at all, 
because a lower than expected voltage suggests that the power regulator is in current control instead 
of voltage control mode. In other words: there’s a short somewhere which results in a current that exceeds 
what’s expected under normal working conditions. Such a short can be located anywhere.</p>

<p>But this is where the modular design of the 5370A shines: you can unplug all the PCBs, check the
15V rail, and if it’s fine, add back PCBs until it’s dead again. And, indeed, with all the
PCBs removed, the 15V rail worked fine. I first added the CPU related PCBs, then the time acquisition
PCBs, and the 15V stayed healthy. But after plugging in the reference frequency buffer PCB, the
15V LED went off and I measured 8V again. Of all the PCBs, this one is the easiest one to understand.</p>

<h1 id="the-reference-frequency-buffer-board">The Reference Frequency Buffer Board</h1>

<p><img src="/assets/hp5370a/ref_freq_buffer_block_diagram.jpg" alt="Reference frequency buffer board block diagram" /></p>

<p>The reference frequency buffer board has the following functionality:</p>

<ul>
  <li>
    <p>Convert the internally generated 10MHz frequency to 
<a href="https://en.wikipedia.org/wiki/Emitter-coupled_logic">emitter-coupled logic (ECL)</a> signaling.</p>

    <p>The 5370A came either with the OCXO or with a lower performance crystal oscillator. These cheaper units 
were usually deployed in labs that already had an external reference clock network.</p>
  </li>
  <li>
    <p>Receive an external reference clock of 5 MHz or 10 MHz, multiply by 2 in the case of
5 MHz, and apply a 10 MHz filter. Convert to ECL as well.</p>
  </li>
  <li>
    <p>Select between internal and external clock to create the final reference clock.</p>
  </li>
  <li>
    <p>Send final reference clock out as ECL (time measurement logic), TTL (CPU) and 
sine wave (reference-out connector on the back panel).</p>
  </li>
</ul>

<p>During PCB swapping, the front-panel display had remained off when all CPU boards were plugged in. 
Unlike later HP test equipment like the HP 5334A universal counter, the CPU clock of the 5370A is derived 
from the 10 MHz clock that comes out of this reference frequency buffer PCB<sup id="fnref:div8" role="doc-noteref"><a href="#fn:div8" class="footnote" rel="footnote">4</a></sup>, so if this board is 
broken, nothing works.</p>

<p>When we zoom down from block diagram to the schematic, we get this:</p>

<p><a href="/assets/hp5370a/ref_freq_buffer_schematic.jpg"><img src="/assets/hp5370a/ref_freq_buffer_schematic.jpg" alt="Reference frequency buffer board schematic" /></a>
<em>(Click to enlarge)</em></p>

<p>Leaving aside the debug process for a moment, I thought the 5 MHz/10 MHz to 10 MHz circuit 
was intriguing. I assumed that it worked by creating some second harmonic and filter
out the base frequency, and that’s kind of how it works.</p>

<p><img src="/assets/hp5370a/ref_freq_buffer_class_c_amplifiers.jpg" alt="Cascade class C amplifiers and 3 10MHz LC tanks" /></p>

<p>There are 3 LC tanks with an inductance of 1 uH and a capacitance of 250pF, good for a natural
resonance frequency of \(f = \frac{1}{2 \pi \sqrt{ L C }}\) = 10.066 MHz. The first 2 LC tanks
are each part of a class C amplifier. The 3rd LC tank is an additional filter.</p>

<p>The incoming 5 MHz or 10 MHz signal periodically inserts a bit of energy into the LC tank and nudges it 
to be in sync with it. This circuit deserves a blog post on its own.</p>

<h1 id="fixing-the-internal-reference-clock">Fixing the Internal Reference Clock</h1>

<p>When you take a closer look at the schematic, there are 2 points that you can take advantage of:</p>

<ul>
  <li>The only part on the path from the internal clock input to the various <em>internal</em> outputs that depends
on the 15V rail is the ECL to TTL conversion circuit. And that part of the 15V rail is only connected
to 3k Ohm resistor R4.</li>
  <li>Immediately after the connector, 15V first goes through an L/C/R/C circuit.</li>
</ul>

<p><img src="/assets/hp5370a/ref_freq_buffer_input_rails.png" alt="Ref frequency board input rails" /></p>

<p>In the process of debugging, I noticed the following:</p>

<p><img src="/assets/hp5370a/ref_frequency_buffer_pcb.jpg" alt="Black capacitor on reference frequency buffer PCB" /></p>

<p>The arrow points to capacitor C17, which looks suspiciously black. I found the magic smoke
generator.</p>

<p>This was the plan of attack:</p>

<ul>
  <li>Replace C17 with a new 10uF capacitor.</li>
  <li>Remove resistor R16 to decouple the internal 15V rail from the external one.</li>
  <li>Disconnect the top side of R4 from the internal 15V and wire it up straight to the
connector 15V rail so that only the ECL to TTL conversion circuit has 15V power.</li>
</ul>

<p><img src="/assets/hp5370a/bodge_wire.jpg" alt="PCB with bodge wire, annotated with 3 fixes" /></p>

<p>It’s an ugly bodge, but after these 3 fixes, I had a nice 10MHz ECL clock signal on the
output clock test pin.</p>

<p><img src="/assets/hp5370a/10MHz_scope_shot.jpg" alt="10MHz signal on Yokogawa oscillscope screen" /></p>

<p>The 5370A was alive and working fine!</p>

<p><img src="/assets/hp5370a/hp5370a_showing_a_result.jpg" alt="HP 5370A measuring a frequency of 9,999,999.999,25 MHz" /></p>

<h1 id="fixing-the-external-reference-clock">Fixing the External Reference Clock</h1>

<p>I usually connect my test equipment to my 
<a href="/2024/04/06/Guide-Tech-GT300-Frequency-Reference-Teardown.html">GT300 frequency standard</a>,
so I really wanted to fix that part of the board as well.</p>

<p>This took way longer than it could have been…</p>

<p>I started by replacing the burnt capacitor with a 10uF electrolytic capacitor and reinstalling
R16. That didn’t go well: this time, the resistor went up in smoke. My theory is that, with
shorted capacitor C17 removed, there was still another short and now the current path had
to go through this resistor. Before burning up, this 10 Ohm resistor measured only 4 Ohms.</p>

<p><img src="/assets/hp5370a/standalone_setup.jpg" alt="Stand-alone bench setup" /></p>

<p>I then removed the board and created a stand-alone setup to debug the board in isolation.
With that burnt up R16 removed again, 15V applied to the internal 15V and  a 10 MHz signal at
the external input, the full circuit was working fine.</p>

<p>I removed capacitor C16, checked it with an LCR tester and the values nicely in spec.</p>

<p>Unable to find any real issues, I finally put in a new 10 Ohm resistor, put a new 10uF capacitor
for C16 as well, plugged in the board and… now the external clock input was working fine too?!</p>

<p>So the board is fixed now and I can use both the internal and external clock, but I still don’t understand
why R16 burnt up after the first capacitor was replaced.</p>

<h1 id="future-work">Future work</h1>

<p>The HP 5370A is working very well now. Once I have another Digikey order going out, I want to add 
2 tantalum capacitors to list and install those  instead of the electrolytics that I used to repair.</p>

<p>I can’t find it back, but 2 easy modifications were suggested on the
time-nuts email list:</p>

<ul>
  <li>
    <p>Drill a hole through the case right above the HP 10811-60111 to have access to the frequency
adjust screw.</p>

    <p>An OCXO is supposed to be immune to external temperature variations, but when you’re measuring picoseconds, 
a difference in ambient temperature can still have a minor impact. With this hole, you can keep
the case closed while calibrating the internal oscillator.</p>
  </li>
  <li>
    <p>Disconnect the “10 MHz present” status LED on the reference clock buffer PCB. Apparently, this
circuit creates some frequency spurs that can introduce some additional jitter on the reference
clock.</p>
  </li>
</ul>

<p>If you’re really hard core:</p>

<ul>
  <li>
    <p>Replace the entire CPU system by a modern CPU board</p>

    <p>More than 10 years ago, the <a href="http://www.jks.com/5370/5370.html">HP5370 Processor Replacement Project</a> 
reverse engineered the entire embedded software stack, created a PCB based on a Beagle board
with new firmware. PCBs are not available anymore, but one could easily have a new one made for
much cheaper than what it would have cost back then.</p>
  </li>
</ul>

<h1 id="references">References</h1>

<ul>
  <li>
    <p><a href="https://www.keysight.com/us/en/assets/9018-06037/user-manuals/9018-06037.pdf">Keysight - 5370A Universal Time Interval Counter Operating and Service Manual</a></p>
  </li>
  <li>
    <p><a href="http://www.hparchive.com/Journals/Low-Resolution/HPJ-1978-08-Low-Resolution.pdf">HP Journal - August 1978</a></p>

    <p>Introduction of the HP 5370A with extensive explanations of how it works under the hood.</p>
  </li>
  <li>
    <p><a href="https://ilrs.gsfc.nasa.gov/docs/time_interval_measurements.pdf">HP Application Note 200-3 - Fundamentals of Time Interval Measurements</a></p>
  </li>
</ul>

<h1 id="footnotes">Footnotes</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:hp8656a" role="doc-endnote">
      <p>My HP 8656A RF signal generator has an OCXO as well. But the fan keeps running
  even when it’s in stand-by mode, and the default fan is very loud too! <a href="#fnref:hp8656a" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:artek" role="doc-endnote">
      <p>Don’t expect to be able to cut-and-paste text from the ArtekManuals scans, because they have
  some obnoxious rights managment that prevents this. <a href="#fnref:artek" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:bleeding_resistor" role="doc-endnote">
      <p>Each smoothing capacitor has a bleeding resistor in parallel to discharge the capacitors
  when the power cable is unplugged. But these resistors will leak power even when the
  unit is switched off. Energy Star regulations clearly weren’t a thing back in 1978. <a href="#fnref:bleeding_resistor" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:div8" role="doc-endnote">
      <p>The CPU runs at 1.25 MHz, the 10 MHz divided by 8. <a href="#fnref:div8" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry></feed>