<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>atomic14</title>
    <description>A collection of slightly mad projects, instructive/educational <a href="https://www.youtube.com/c/atomic14">videos</a>, and generally interesting stuff.
Building projects around the Arduino and ESP32 platforms - we'll be exploring AI, Computer Vision, Audio, 3D Printing - it may get a bit eclectic...
</description>
    <link>https://www.atomic14.com/</link>
    <atom:link href="https://www.atomic14.com/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Sun, 12 Jul 2026 19:31:34 +0000</pubDate>
    <lastBuildDate>Sun, 12 Jul 2026 19:31:34 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
    <item>
      <title>Any Google Font on Your ESP32 Display</title>
      <description>&lt;p&gt;Every time I put text on a TFT display with TFT_eSPI I end up staring at the same built-in fonts: chunky, jagged, and very 1980s. Which is a shame, because the library has been able to draw beautiful anti-aliased “smooth fonts” for years. The rendering was never the problem, the tooling was. The official way to make a smooth font involves installing the Processing IDE, editing a Java sketch, and running it again for every font at every size. The result is that most projects never bother.&lt;/p&gt;

&lt;p&gt;So I built something to fix that.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://fonts.atomic14.com/&quot;&gt;&lt;strong&gt;fonts.atomic14.com&lt;/strong&gt;&lt;/a&gt; is a free browser tool that turns any of 1,900+ Google Fonts (or your own TTF file) into fonts ready to use with TFT_eSPI, M5Stack/M5GFX or LVGL, with a pixel-exact preview of how they’ll look on your panel. Everything runs in your browser; no font ever gets uploaded anywhere.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://fonts.atomic14.com/&quot;&gt;&lt;img src=&quot;/assets/article_images/2026-07-12/hero.webp&quot; alt=&quot;Embedded Font Studio&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rest of this post is about what’s actually inside these font files, and the handful of things worth knowing before you use them.&lt;/p&gt;

&lt;h2 id=&quot;what-a-smooth-font-actually-is&quot;&gt;What a “smooth font” actually is&lt;/h2&gt;

&lt;p&gt;TFT_eSPI’s smooth fonts are VLW files, a format inherited from the Processing project. A VLW is gloriously simple: a small header (glyph count, size, ascent, descent), a table of per-glyph metrics, and then raw 8-bit alpha bitmaps, one byte per pixel. No vector outlines, no hinting, no kerning. The font was rasterized on a desktop, and the microcontroller just blits pixels.&lt;/p&gt;

&lt;p&gt;The “smooth” part is that alpha channel. Each pixel stores how much of the glyph covers it (0 to 255), and at draw time the library blends between your text colour and background colour in RGB565 space:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pixel = (fg × alpha + bg × (255 − alpha)) / 256
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is why &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tft.setTextColor(TFT_WHITE, TFT_BLACK)&lt;/code&gt; needs &lt;strong&gt;both&lt;/strong&gt; colours for smooth fonts: there’s no transparency, only blending toward a known background. Draw over a photo and you’ll get fringes around the letters. That’s not a bug in your font, it’s how the format works.&lt;/p&gt;

&lt;h2 id=&quot;progmem-or-littlefs&quot;&gt;PROGMEM or LittleFS?&lt;/h2&gt;

&lt;p&gt;You have two ways to get a VLW onto the chip, and the tool exports both:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.h&lt;/code&gt; header&lt;/strong&gt; with the font as a PROGMEM byte array. Easiest by far: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#include&lt;/code&gt; it, call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tft.loadFont(MyFont28);&lt;/code&gt;, done. The font lives in flash alongside your code.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vlw&lt;/code&gt; file on LittleFS or SD&lt;/strong&gt;, loaded with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tft.loadFont(&quot;MyFont28&quot;, LittleFS);&lt;/code&gt;. Better when you have several fonts or want to swap them without recompiling, at the cost of the filesystem-upload dance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, remember to add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#define SMOOTH_FONT&lt;/code&gt; to your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User_Setup.h&lt;/code&gt;. Forgetting it is the number one cause of “why doesn’t loadFont exist” compile errors.&lt;/p&gt;

&lt;h2 id=&quot;keeping-the-files-small&quot;&gt;Keeping the files small&lt;/h2&gt;

&lt;p&gt;A glyph costs 28 bytes of metadata plus width × height bytes of bitmap. At 28 px, a full Basic Latin set lands around 25-30 KB; a digits-only font for a sensor readout can be under 2 KB. Every character you leave out is flash you keep. The tool shows a live byte count as you change the character set, and you can click individual characters out of the grid. If all you need is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;°C&lt;/code&gt; and ten digits, ship &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;°C&lt;/code&gt; and ten digits.&lt;/p&gt;

&lt;h2 id=&quot;quirks-the-preview-will-save-you-from&quot;&gt;Quirks the preview will save you from&lt;/h2&gt;

&lt;p&gt;Two behaviours surprise almost everyone. I made sure the preview reproduces both, because it renders with a port of TFT_eSPI’s own drawing code rather than the browser’s font engine:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Text wraps at the right edge.&lt;/strong&gt; When a glyph won’t fit, smooth-font drawing jumps back to x=0 on the next line. It doesn’t clip.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Spaces don’t use the font’s real space width.&lt;/strong&gt; The library guesses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(ascent + descent) × 2/7&lt;/code&gt; pixels instead. Word spacing on the device will never quite match your desktop’s rendering of the same font, but the preview matches the device, which is the thing that counts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And if a character shows up blank on the display, it’s almost always because it wasn’t in your subset. The degree sign is the classic victim: it’s not in Basic Latin, so a temperature readout quietly comes out as “23.5C”. Adding it to the extra characters box fixes it, and the preview will show you the gap before your display does.&lt;/p&gt;

&lt;h2 id=&quot;try-it-without-any-hardware&quot;&gt;Try it without any hardware&lt;/h2&gt;

&lt;p&gt;Every font you make can be tested on a simulated ESP32 and ILI9341 display in &lt;a href=&quot;https://wokwi.com/&quot;&gt;Wokwi&lt;/a&gt;: one click copies a complete test sketch with the font embedded and opens a pre-wired template project. Paste it in, press play, and you’re looking at your font rendered by the real TFT_eSPI library on an emulated panel.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-07-12/wokwi.webp&quot; alt=&quot;The exported Orbitron font running on a simulated ESP32 and ILI9341 in Wokwi&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The download also includes a ready-to-flash Arduino test project if you’d rather see it on real hardware.&lt;/p&gt;

&lt;h2 id=&quot;lvgl-too&quot;&gt;LVGL too&lt;/h2&gt;

&lt;p&gt;The same subsetting and rasterization pipeline exports LVGL fonts: C arrays compatible with what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lv_font_conv&lt;/code&gt; produces, at 1, 2, 4 or 8 bits per pixel. If your project uses LVGL or SquareLine Studio, grab that file instead.&lt;/p&gt;

&lt;h2 id=&quot;under-the-hood-for-the-curious&quot;&gt;Under the hood, for the curious&lt;/h2&gt;

&lt;p&gt;The VLW writer is byte-for-byte compatible with Processing’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PFont.save&lt;/code&gt; (verified against golden files), the renderer is checked against TFT_eSPI running in an emulator, and the LVGL writer is calibrated against real &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lv_font_conv&lt;/code&gt; output. The whole thing is client-side: opentype.js parses the font, a Web Worker rasterizes the glyphs to alpha bitmaps, and TypeScript writers produce the output bytes.&lt;/p&gt;

&lt;p&gt;Go and make something less 1980s: &lt;a href=&quot;https://fonts.atomic14.com/&quot;&gt;&lt;strong&gt;fonts.atomic14.com&lt;/strong&gt;&lt;/a&gt;. And if you’re still choosing a display to pair it with, the &lt;a href=&quot;https://www.atomic14.com/esp32/boards/displays/&quot;&gt;ESP32 display boards database&lt;/a&gt; has every popular board in one filterable table. I’d love to hear how you get on.&lt;/p&gt;
</description>
      <pubDate>Sun, 12 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/07/12/anti-aliased-fonts-esp32-displays</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/07/12/anti-aliased-fonts-esp32-displays.html</guid>
        
    </item>
    
    <item>
      <title>Every Espressif Module in One Place</title>
      <description>&lt;p&gt;Every time I start a new project the first question is the same: which module should I use? Picking an Espressif chip means pulling up datasheets, hunting for pinout diagrams, checking which pins are safe, and ending up with a dozen browser tabs open.&lt;/p&gt;

&lt;p&gt;So I built something to fix that.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.atomic14.com/esp32/&quot;&gt;&lt;strong&gt;www.atomic14.com/esp32&lt;/strong&gt;&lt;/a&gt; is a free, community-maintained database of Espressif modules. Every module in one place, with the specs, pinouts and 3D models you actually need to make a decision.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.atomic14.com/esp32/&quot;&gt;&lt;img src=&quot;/assets/article_images/2026-06-09/hero.webp&quot; alt=&quot;ESP Modules&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;whats-in-there&quot;&gt;What’s in there&lt;/h2&gt;

&lt;p&gt;Right now it covers 24 modules across 11 SoCs, from the classic ESP32 through to the ESP32-S3, ESP32-C3 and ESP32-H2. For each one you get:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Searchable specs&lt;/strong&gt; pulled straight from the official datasheets: cores, clock speed, RAM, flash and PSRAM options, Wi-Fi and Bluetooth support.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Colour-coded pinouts&lt;/strong&gt; with the strapping pins and “don’t use these” warnings called out, so you don’t get caught by a pin that fights you at boot.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Interactive 3D models&lt;/strong&gt; so you can see the actual footprint and dimensions before you commit to a board layout.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Side-by-side comparison&lt;/strong&gt; to put two or three modules next to each other and see exactly what changes.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Getting-started pointers&lt;/strong&gt; for Arduino, ESP-IDF and MicroPython, plus links back to the source datasheets and reference designs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;update-now-with-boards-and-sensors-too&quot;&gt;Update: now with boards and sensors too&lt;/h2&gt;

&lt;p&gt;Since launch it’s grown well beyond bare modules:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.atomic14.com/esp32/boards/displays/&quot;&gt;Every popular ESP32 display board in one filterable table&lt;/a&gt;&lt;/strong&gt; — the Cheap Yellow Display and its successors, the 7” wall-panel boards, AMOLED and e-paper, filterable by size, touch type and ESPHome support. Plus guides like &lt;a href=&quot;https://www.atomic14.com/esp32/boards/for-home-assistant/&quot;&gt;which display board for Home Assistant&lt;/a&gt; and &lt;a href=&quot;https://www.atomic14.com/esp32/boards/display-interfaces/&quot;&gt;why big cheap screens feel slow&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.atomic14.com/esp32/sensors/&quot;&gt;A sensor module database&lt;/a&gt;&lt;/strong&gt; — mmWave radars, temperature, CO2, energy and more, each with its ESPHome support status and a &lt;em&gt;counterfeit-risk rating&lt;/em&gt;. If you’ve ever received a “BME280” with no humidity readout or a batch of fake DS18B20 probes, start with &lt;a href=&quot;https://www.atomic14.com/esp32/sensors/fake-sensors/&quot;&gt;the fake sensor problem&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;goodbye-to-the-old-esp32-s3-pinout&quot;&gt;Goodbye to the old ESP32-S3 pinout&lt;/h2&gt;

&lt;p&gt;This replaces my old ESP32-S3 pinout post. That started life as a single GitHub image, which was useful but hard to keep current and only covered one module. The new &lt;a href=&quot;https://www.atomic14.com/esp32/modules/esp32-s3-wroom-1/&quot;&gt;ESP32-S3-WROOM-1 page&lt;/a&gt; does everything that did and a lot more, and the same treatment is there for the rest of the family.&lt;/p&gt;

&lt;h2 id=&quot;its-a-living-document&quot;&gt;It’s a living document&lt;/h2&gt;

&lt;p&gt;Datasheets get revised, mistakes creep in, and there are always edge cases worth flagging. Every page has a built-in way to report errors, and corrections are very welcome. The more eyes on it, the more accurate it stays.&lt;/p&gt;

&lt;p&gt;Go and have a poke around: &lt;a href=&quot;https://www.atomic14.com/esp32/&quot;&gt;&lt;strong&gt;www.atomic14.com/esp32&lt;/strong&gt;&lt;/a&gt;. I’d love to hear what’s missing.&lt;/p&gt;
</description>
      <pubDate>Tue, 09 Jun 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/06/09/esp32-module-database</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/06/09/esp32-module-database.html</guid>
        
    </item>
    
    <item>
      <title>Did I Actually Make It Worse?</title>
      <description>&lt;p&gt;A little while back I &lt;a href=&quot;/2026/04/11/pir-night-light-upgrade.html&quot;&gt;ripped the PIR and mystery chip out of one of these cheap night lights and dropped a microwave radar module in its place&lt;/a&gt;. It worked beautifully: much better sensitivity, sensible timeout, the whole thing tucked back into the original case like nothing had happened.&lt;/p&gt;

&lt;p&gt;But I had a sneaking suspicion I might have improved one thing and quietly ruined another. The radar module is &lt;em&gt;always on&lt;/em&gt;. The PIR-and-mystery-chip combo, on the other hand, looked like it was doing something clever with sleep. So before I get smug about my mod, let’s actually measure what each version is pulling from the battery.&lt;/p&gt;

&lt;p&gt;Out comes the Nordic Power Profiler Kit II.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-05-17/hero.webp&quot; alt=&quot;PPK2 wired up to the original night light PCB&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;the-original-leds-on&quot;&gt;The Original: LEDs On&lt;/h1&gt;

&lt;p&gt;Power the supply on, start logging, and the LEDs immediately come up to greet me. It takes a moment to actually trigger because I’ve taped over the LDR (the night light flatly refuses to light up unless it thinks it’s dark), but once it triggers we’re sitting at about 80 mA, which matches the &lt;a href=&quot;/2026/04/11/pir-night-light-upgrade.html&quot;&gt;last time I measured this on the bench&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-05-17/leds_on.webp&quot; alt=&quot;PPK trace with the LEDs on, drawing about 80 mA&quot; /&gt;&lt;/p&gt;

&lt;p&gt;That’s the easy bit. The interesting question is what the board does when it’s &lt;em&gt;not&lt;/em&gt; lighting up.&lt;/p&gt;

&lt;h1 id=&quot;the-original-waiting-mode&quot;&gt;The Original: Waiting Mode&lt;/h1&gt;

&lt;p&gt;Wait long enough for the LEDs to time out and the trace drops off a cliff. The board settles into a “waiting for movement” mode at roughly 700-800 µA. Fine, that’s basically what a comparator and a small MCU sipping at the PIR signal ought to look like.&lt;/p&gt;

&lt;p&gt;But then something weird starts happening. Every so often the trace drops down to a tiny fraction of that. Not lower-by-a-bit. &lt;em&gt;Much&lt;/em&gt; lower, about &lt;strong&gt;30 µA&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-05-17/ultra_low_graph.webp&quot; alt=&quot;The mystery chip&apos;s pulsed ultra-low-power mode&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It sits down there for a while, then briefly pops back up to a few hundred microamps, then drops again. It’s clearly a sleep-and-poke pattern: the chip is mostly asleep and only briefly waking up to look at the PIR. As long as I don’t move, it stays in this ultra-low-power mode pretty much indefinitely.&lt;/p&gt;

&lt;p&gt;That’s actually really impressive for a chip with all its identifying marks sanded off.&lt;/p&gt;

&lt;h1 id=&quot;doing-the-maths-on-the-original&quot;&gt;Doing the Maths on the Original&lt;/h1&gt;

&lt;p&gt;Scroll back to a chunk of the ultra-low-power section, drag a selection across it, and the PPK helpfully spits out the average: &lt;strong&gt;72 µA&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-05-17/monitoring_graph.webp&quot; alt=&quot;Selecting the ultra-low-power region: 72 µA average&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And during the noisier “actively monitoring” segments it’s averaging more like 700-800 µA. So depending on what mood it’s in, the original is sipping between 72 µA and around 0.8 mA. Whichever number we pick, on its 350 mAh cell that’s a &lt;em&gt;lot&lt;/em&gt; of standby time. In theory the 72 µA figure puts it at something like 200 days. In practice it definitely doesn’t last that long, but the cell is almost certainly not really 350 mAh either.&lt;/p&gt;

&lt;p&gt;The really annoying thing is that I already know from living with these things that the original &lt;em&gt;barely triggers&lt;/em&gt;. So all that lovely low power gets wasted because the night light isn’t actually on when you want it to be. Which is, you know, the entire point of a night light.&lt;/p&gt;

&lt;h1 id=&quot;my-mod-on-the-ppk&quot;&gt;My Mod: On the PPK&lt;/h1&gt;

&lt;p&gt;Swap the leads onto the radar version. There’s a current limit resistor in series with the indicator LED here so I don’t have to worry about the LED itself drawing a lot.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-05-17/radar_setup.webp&quot; alt=&quot;Radar-modded board hooked up to the PPK&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Wait the ~40 seconds for the radar’s extended timeout to expire (that big cap on the CTM pad is doing its job) and then we get to see the real story.&lt;/p&gt;

&lt;h1 id=&quot;my-mod-idle-current&quot;&gt;My Mod: Idle Current&lt;/h1&gt;

&lt;p&gt;LED out, no movement, just sitting there waiting. The trace settles into a fairly fat band of noise centred around &lt;strong&gt;2.85-2.9 mA&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-05-17/radar_reading.webp&quot; alt=&quot;Radar version drawing 2.85 mA at idle&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I tried covering and uncovering the LDR to see whether the radar module was doing anything clever with it. Does it actually shut down in daylight, or just gate the output? Spoiler: it just gates the output. The module itself draws the same ~2.86 mA whether the LDR thinks it’s bright or dark. The LDR is purely there to stop the LEDs lighting up during the day, not to save power.&lt;/p&gt;

&lt;p&gt;Honestly, 2.8 mA isn’t &lt;em&gt;that&lt;/em&gt; bad for a radar module that’s continuously transmitting and listening. It’s just a lot more than 72 µA.&lt;/p&gt;

&lt;h1 id=&quot;so-did-i-make-it-worse&quot;&gt;So… Did I Make It Worse?&lt;/h1&gt;

&lt;p&gt;Time to be honest with the numbers.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-05-17/comparison.webp&quot; alt=&quot;Original 72 µA / 350 mAh / ~200 days versus Improved 2.8 mA / 820 mAh / ~12 days&quot; /&gt;&lt;/p&gt;

&lt;p&gt;On paper, the original on its sad little 350 mAh cell at 72 µA average should run for around 200 days. My “improved” version at 2.8 mA on the 820 mAh cell I fitted comes out at… about &lt;strong&gt;12 days&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ouch.&lt;/p&gt;

&lt;p&gt;In fairness, the original definitely doesn’t get anywhere near 200 days in real life, partly because the cell is optimistic and partly because every actual trigger spends time at 80 mA. And my mod &lt;em&gt;actually triggers reliably&lt;/em&gt;, which the original conspicuously does not. But still, twelve days between charges is rubbish for something that’s supposed to live on a wall.&lt;/p&gt;

&lt;h1 id=&quot;salvaging-it&quot;&gt;Salvaging It&lt;/h1&gt;

&lt;p&gt;All is not lost. I’ve got a stash of much beefier 2000 mAh cells that physically fit inside the case if I’m a bit careful about packaging.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-05-17/battery_upgrade.webp&quot; alt=&quot;2000 mAh Liter Energy Battery dropped into the case&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Swapping that in gets the modded version to roughly &lt;strong&gt;30 days between charges&lt;/strong&gt;, which is much more like it. Not as flashy as “charge it four times a year”, but I’ll actually believe 30 days from a thing that genuinely turns on when I walk past.&lt;/p&gt;

&lt;p&gt;So yes: strictly on standby current, I made it considerably worse. But factor in “does it actually do its job”, and a bigger cell, and I think I’m still ahead. Just.&lt;/p&gt;

&lt;p&gt;The bit that’s still nagging at me is the original. Whatever that mystery chip is doing in its sleep mode is really nicely done; 72 µA average is properly low. If I could find a way to make the &lt;em&gt;original&lt;/em&gt; trigger reliably, I’d have basically the perfect night light. That’s probably a project for another day.&lt;/p&gt;

&lt;p&gt;If you want to watch the meters in motion (including the satisfying moment when the trace drops to a pancake-flat 30 µA), &lt;a href=&quot;https://www.youtube.com/watch?v=3vL5iFn4Rjk&quot;&gt;it’s all on YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;3vL5iFn4Rjk&quot; playlabel=&quot;Did I actually make it worse?&quot;&gt;&lt;/lite-youtube&gt;
</description>
      <pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/05/17/did-i-make-it-worse</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/05/17/did-i-make-it-worse.html</guid>
        
    </item>
    
    <item>
      <title>ESP-IDF v6 in VS Code: Blink (and debug) on the ESP32-S3</title>
      <description>&lt;p&gt;ESP-IDF &lt;strong&gt;v6&lt;/strong&gt; dropped back in March and the &lt;a href=&quot;https://github.com/espressif/esp-idf/releases&quot;&gt;GitHub releases page&lt;/a&gt; is already showing &lt;strong&gt;v6.0.1&lt;/strong&gt; - so I figured it was time to stop hiding in the v5.x cave and actually try it out.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-release.webp&quot; alt=&quot;ESP-IDF v6.0.1 release on GitHub&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is a nice gentle introduction. We’ll install the IDF, wire it up to VS Code, build the classic Blink example for an ESP32-S3, then start poking at menuconfig to do the things you can’t easily do from the Arduino IDE.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;y2pIqbZa-iA&quot; playlabel=&quot;ESP-IDF v6 in VS Code&quot;&gt;&lt;/lite-youtube&gt;

&lt;h1 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h1&gt;

&lt;p&gt;If you’re new to the ESP-IDF there are a couple of prerequisites to install before anything else. I’m on a Mac so for me it’s a quick &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;brew install ...&lt;/code&gt; from the &lt;a href=&quot;https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/get-started/index.html#installation&quot;&gt;official setup page&lt;/a&gt;. Windows and Linux instructions are on the same page - just pick your platform.&lt;/p&gt;

&lt;p&gt;If you don’t already have &lt;a href=&quot;https://brew.sh/&quot;&gt;Homebrew&lt;/a&gt;, grab that first.&lt;/p&gt;

&lt;h1 id=&quot;vs-code-and-the-esp-idf-extension&quot;&gt;VS Code and the ESP-IDF extension&lt;/h1&gt;

&lt;p&gt;You don’t &lt;em&gt;have&lt;/em&gt; to use &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VS Code&lt;/a&gt;, but the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=espressif.esp-idf-extension&quot;&gt;ESP-IDF extension&lt;/a&gt; is genuinely nice and makes everything much less painful, so that’s what I’m going to use.&lt;/p&gt;

&lt;p&gt;In VS Code, open the Extensions panel, search for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ESP-IDF&lt;/code&gt; and install the official Espressif one.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-vscode-extension.webp&quot; alt=&quot;ESP-IDF extension in the VS Code marketplace&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Once it’s installed you’ll get a little ESP-IDF Explorer icon in the side bar.&lt;/p&gt;

&lt;h1 id=&quot;running-the-installation-manager&quot;&gt;Running the Installation Manager&lt;/h1&gt;

&lt;p&gt;Click the ESP-IDF icon, go to &lt;strong&gt;Advanced&lt;/strong&gt; and pick &lt;strong&gt;Open ESP-IDF Installation Manager&lt;/strong&gt;. The first time round it’ll tell you the EIM executable can’t be found and ask you to choose a download mirror. Unless you’re in China, &lt;strong&gt;GitHub&lt;/strong&gt; is the right answer (if you are in China, the Espressif mirror is likely faster).&lt;/p&gt;

&lt;p&gt;That fires up the actual installation manager:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-install-methods.webp&quot; alt=&quot;ESP-IDF Installation Manager - install methods&quot; /&gt;&lt;/p&gt;

&lt;p&gt;There are four options here but really only two you’re likely to care about:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Easy Installation&lt;/strong&gt; - sensible defaults, downloads everything, just works.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Offline Installation&lt;/strong&gt; - you download a 3-4 GB archive from the Espressif site separately and point the installer at it. Worth doing if your internet is flaky - you can put the archive on an SD card and avoid endless mid-install retries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I went with &lt;strong&gt;Easy Installation&lt;/strong&gt;. Espressif say it can take 10-45 minutes depending on your connection, which feels about right - it has to clone the whole IDF from GitHub, then download 10 toolchain bundles, then set up Python.&lt;/p&gt;

&lt;p&gt;The progress bar is… let’s say &lt;em&gt;creative&lt;/em&gt;. It cheerfully ticked back up to 39 seconds at one point.&lt;/p&gt;

&lt;p&gt;After a quick “12 seconds left” stand-off, the dashboard finally shows &lt;strong&gt;ESP-IDF v6.0.1&lt;/strong&gt; as an installed version:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-installed.webp&quot; alt=&quot;ESP-IDF Installation Manager dashboard with v6.0.1 installed&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;creating-a-project&quot;&gt;Creating a project&lt;/h1&gt;

&lt;p&gt;Back in VS Code, the ESP-IDF Explorer has a little &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+&lt;/code&gt; icon for the &lt;strong&gt;New Project Wizard&lt;/strong&gt;. It asks which IDF version to use (we just installed 6.0.1, easy) and then offers a list of templates - I’m going for the classic Blink.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-new-project.webp&quot; alt=&quot;New Project Wizard with the Blink template&quot; /&gt;&lt;/p&gt;

&lt;p&gt;A few things to be careful with on the wizard:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Path case matters.&lt;/strong&gt; macOS file systems aren’t case-sensitive by default but the IDF tools &lt;em&gt;are&lt;/em&gt;. My &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Work&lt;/code&gt; folder is capital-W, so if I type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;work&lt;/code&gt; here I’ll get a mountain of weird errors later. Match the actual case.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;IDF target&lt;/strong&gt; - I’m using an ESP32-S3 so I picked that.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Serial port&lt;/strong&gt; - leave it on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;detect&lt;/code&gt; and it usually works out which port the board is on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;looking-at-the-code&quot;&gt;Looking at the code&lt;/h1&gt;

&lt;p&gt;If you’re coming from the Arduino IDE, the layout is going to look intimidating - lots of folders, CMake files, build directories. It’s not actually that bad once you know where to look. The interesting bit lives in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main/blink_example_main.c&lt;/code&gt; and the entry point is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app_main()&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-blink-code.webp&quot; alt=&quot;main.c with the ESP-IDF Explorer side panel&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The other thing that’s very different from Arduino is &lt;strong&gt;menuconfig&lt;/strong&gt;. Most of the build is configured through it rather than by editing files. From the ESP-IDF Explorer side panel pick &lt;strong&gt;SDK Configuration Editor (menuconfig)&lt;/strong&gt;.&lt;/p&gt;

&lt;h1 id=&quot;configuring-the-led&quot;&gt;Configuring the LED&lt;/h1&gt;

&lt;p&gt;The Blink example has its own little &lt;strong&gt;Example Configuration&lt;/strong&gt; section in menuconfig. By default it’s set up for an addressable LED strip - I just have a regular LED on a GPIO pin, so I need to switch the type to &lt;strong&gt;GPIO&lt;/strong&gt; and tell it which pin to use.&lt;/p&gt;

&lt;p&gt;A quick peek at the KiCad schematic for my dev board:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-kicad-gpio21.webp&quot; alt=&quot;KiCad schematic showing the LED on GPIO21&quot; /&gt;&lt;/p&gt;

&lt;p&gt;GPIO 21 is driving the LED (it’s actually green on this board because I just grabbed random LEDs out of the bin). So in menuconfig I set:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Blink LED type:&lt;/strong&gt; GPIO&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Blink GPIO number:&lt;/strong&gt; 21&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Blink period in ms:&lt;/strong&gt; 500&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-menuconfig-gpio.webp&quot; alt=&quot;menuconfig: GPIO 21, 500 ms&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Save and we’re good to go.&lt;/p&gt;

&lt;h1 id=&quot;build-and-flash-the-bit-that-goes-wrong&quot;&gt;Build and flash (the bit that goes wrong)&lt;/h1&gt;

&lt;p&gt;The ESP-IDF Explorer has buttons for &lt;strong&gt;Build Project&lt;/strong&gt; and &lt;strong&gt;Flash Device&lt;/strong&gt; (and the spanner / lightning icons in the bottom status bar do the same). Build was clean, but flashing failed.&lt;/p&gt;

&lt;p&gt;I tried it again with the board held in programming mode (boot + reset) - still no good. The default flash method on the dev board is USB JTAG and it just wasn’t co-operating.&lt;/p&gt;

&lt;p&gt;Easy fix: change the flash method.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-flash-method.webp&quot; alt=&quot;Flash method menu in the ESP-IDF Explorer&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Pick &lt;strong&gt;Select Flash Method&lt;/strong&gt;, switch from JTAG to &lt;strong&gt;UART&lt;/strong&gt;, and try again. Flash succeeds, and the LED starts cheerfully blinking on the bench.&lt;/p&gt;

&lt;h1 id=&quot;reading-the-monitor&quot;&gt;Reading the monitor&lt;/h1&gt;

&lt;p&gt;With the board running, &lt;strong&gt;Monitor Device&lt;/strong&gt; opens a serial monitor and prints the boot log. Two things jumped out:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cpu_start: Pro cpu start user code
cpu_start: cpu freq: 160000000 Hz
...
spi_flash: Detected size (16384k) larger than the size in the binary image header (2048k).
Using the size in the binary image header.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-first-boot.webp&quot; alt=&quot;First boot - 160 MHz CPU and a flash size mismatch warning&quot; /&gt;&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The CPU is running at &lt;strong&gt;160 MHz&lt;/strong&gt; but the ESP32-S3 will happily do &lt;strong&gt;240 MHz&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;The chip has &lt;strong&gt;16 MB&lt;/strong&gt; of flash but the binary image header says &lt;strong&gt;2 MB&lt;/strong&gt;, so the IDF is using the smaller of the two and politely warning us about it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both of these are easy menuconfig fixes.&lt;/p&gt;

&lt;h1 id=&quot;squeezing-more-out-of-the-chip&quot;&gt;Squeezing more out of the chip&lt;/h1&gt;

&lt;p&gt;Back into the SDK config editor:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Search for &lt;strong&gt;CPU&lt;/strong&gt; and change the CPU frequency to &lt;strong&gt;240 MHz&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;Search for &lt;strong&gt;flash&lt;/strong&gt; and change the flash size to &lt;strong&gt;16 MB&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While we’re in there, this board also has &lt;strong&gt;PSRAM&lt;/strong&gt;, so might as well turn that on:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Search for &lt;strong&gt;PSRAM&lt;/strong&gt;, enable &lt;strong&gt;Support for external SPI-connected RAM&lt;/strong&gt;, set the mode to &lt;strong&gt;octal&lt;/strong&gt; (mine is octal PSRAM), and bump the speed to &lt;strong&gt;80 MHz&lt;/strong&gt; for good measure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save, build, flash, monitor. Now the boot log looks rather more pleased with itself:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-psram-240mhz.webp&quot; alt=&quot;Boot with PSRAM, 8 MB octal at 80 MHz, 240 MHz CPU&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;MSPI Timing: Enter psram timing tuning
esp_psram: Found 8MB PSRAM device
esp_psram: Speed: 80MHz
esp_psram: SPI SRAM memory test OK
...
cpu_start: cpu freq: 240000000 Hz
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;8 MB of PSRAM, the full 16 MB of flash, and 240 MHz of CPU. Lovely.&lt;/p&gt;

&lt;h1 id=&quot;bonus-round-jtag-debugging&quot;&gt;Bonus round: JTAG debugging&lt;/h1&gt;

&lt;p&gt;The other thing the ESP-IDF brings to the party that you don’t really get with the Arduino IDE is proper on-chip debugging. The S3 has a built-in USB JTAG, OpenOCD comes with the toolchain, and VS Code already speaks the GDB protocol - so it should “just work”.&lt;/p&gt;

&lt;p&gt;Should.&lt;/p&gt;

&lt;p&gt;When I tried to start the OpenOCD server it complained that something was already listening - I had a stale OpenOCD from a previous session hanging around. A quick &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ps&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kill&lt;/code&gt; in the terminal and the server started cleanly. With the flash method back on &lt;strong&gt;JTAG&lt;/strong&gt;, the board flashed without complaint.&lt;/p&gt;

&lt;p&gt;Now the fun bit - drop a breakpoint in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blink_led()&lt;/code&gt; and hit &lt;strong&gt;Launch Debug&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/esp-idf6-debug-breakpoint.webp&quot; alt=&quot;Stopped on a breakpoint inside blink_led()&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The IDF stops you at the start of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app_main&lt;/code&gt; first; hit continue and you sail straight into the breakpoint. Step over the line, watch the GPIO state flip, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turning the LED off&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turning the LED on&lt;/code&gt; appear in the serial monitor. Step again, the LED physically turns off on the bench. That’s &lt;em&gt;very&lt;/em&gt; satisfying compared to sprinkling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Serial.println&lt;/code&gt; everywhere.&lt;/p&gt;

&lt;p&gt;(Tiny gotcha: the serial monitor latched onto the wrong port the first time - just pick the right one and you’re back in business.)&lt;/p&gt;

&lt;h1 id=&quot;so-esp-idf-or-arduino&quot;&gt;So… ESP-IDF or Arduino?&lt;/h1&gt;

&lt;p&gt;The blink code itself isn’t hugely different from the Arduino version - it’s still mostly &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpio_set_level(BLINK_GPIO, s_led_state)&lt;/code&gt; - but you get the menuconfig system, proper logging, a real debugger, and access to all the bits of the chip that the Arduino layer hides from you.&lt;/p&gt;

&lt;p&gt;Honestly, it’s a lot less scary than it looks. If you’ve been putting off trying the IDF, &lt;strong&gt;v6 in VS Code is a really nice place to start&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let me know in the comments - &lt;strong&gt;ESP-IDF or Arduino&lt;/strong&gt;, which do you prefer?&lt;/p&gt;

&lt;h1 id=&quot;useful-links&quot;&gt;Useful links&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/get-started/index.html#installation&quot;&gt;ESP-IDF setup docs (v6)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=espressif.esp-idf-extension&quot;&gt;ESP-IDF VS Code extension&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/espressif/esp-idf/releases&quot;&gt;ESP-IDF releases on GitHub&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;Visual Studio Code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/05/10/esp-idf6-vscode-blink</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/05/10/esp-idf6-vscode-blink.html</guid>
        
    </item>
    
    <item>
      <title>Hacking a Cheap PIR Night Light Into Something Actually Useful</title>
      <description>&lt;p&gt;I’ve been picking up a whole bunch of these motion sensitive night lights and, let’s just say, they haven’t been great. The range is poor, the battery life is nowhere near the advertised “charge it four times a year”, and the timeout is so short it’s not even enough time to get to the toilet.&lt;/p&gt;

&lt;p&gt;I thought it might be interesting to crack one open and see if I could improve it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/pile.webp&quot; alt=&quot;A pile of cheap motion sensor night lights&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I’ve got both flavours — &lt;a href=&quot;https://s.click.aliexpress.com/e/_c2JvLwPT&quot;&gt;round ones&lt;/a&gt; and &lt;a href=&quot;https://s.click.aliexpress.com/e/_c4KK6t4t&quot;&gt;oval ones&lt;/a&gt; — and internally they’re basically identical, so whatever I do to one should work for both. (Those are affiliate links — if you pick a pair up through them, I get a few pennies at no extra cost to you.)&lt;/p&gt;

&lt;h1 id=&quot;the-plan&quot;&gt;The Plan&lt;/h1&gt;

&lt;p&gt;I’ve got a drawer full of these nice little RC-WL0516 microwave radar sensors. They’re much more sensitive than a PIR, they see through plastic, and you can tweak the sensitivity and timeout with a couple of passives. The idea is to rip the original motion sensing front end out of one of these night lights and drop the radar module in instead.&lt;/p&gt;

&lt;div class=&quot;esp32-cta&quot;&gt;
  &lt;p&gt;&lt;strong&gt;Going down the radar rabbit hole?&lt;/strong&gt; The modern mmWave modules (HLK-LD2410 and friends) can detect you &lt;em&gt;sitting still&lt;/em&gt;, not just moving — I&apos;ve compared the whole family plus PIRs in my &lt;a href=&quot;https://www.atomic14.com/esp32/sensors/presence-sensors/&quot;&gt;presence sensor guide&lt;/a&gt;, and there&apos;s a &lt;a href=&quot;https://www.atomic14.com/esp32/sensors/&quot;&gt;filterable table of all the sensors people wire to ESP32s&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/radar.webp&quot; alt=&quot;RC-WL0516 microwave radar modules&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;cracking-one-open&quot;&gt;Cracking One Open&lt;/h1&gt;

&lt;p&gt;The top just pops off with a bit of a squeeze. Inside, there’s a PIR sensor, an eight pin mystery chip with all the identifying marks sanded off, a light dependent resistor, and some charging circuitry.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/opened.webp&quot; alt=&quot;Inside the night light, with U2 mystery chip close-up&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Popping the single screw out releases the board. Underneath is a tiny 350 mAh lithium cell — and no sign of a protection board on it, which is always reassuring.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/battery.webp&quot; alt=&quot;The 350 mAh lithium cell&quot; /&gt;&lt;/p&gt;

&lt;p&gt;With the board out, the layout is pretty clear. PIR sensor dead centre, ring of LEDs around it, mystery chip off to one side next to the LDR, and the charging circuitry tucked down by the micro USB port.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/board_hero.webp&quot; alt=&quot;The stripped PCB showing PIR sensor, U2 chip, LDR and LEDs&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;doing-a-big-clive&quot;&gt;Doing a Big Clive&lt;/h1&gt;

&lt;p&gt;Time to do my best &lt;a href=&quot;https://www.youtube.com/@bigclivedotcom&quot;&gt;Big Clive&lt;/a&gt; impression and reverse engineer the circuit. It’s a single layer board, so it’s all quite tractable — the positive side of every LED is tied straight to battery plus, and the negative side goes through the mystery chip down to battery minus.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/trace.webp&quot; alt=&quot;Board with tracks traced out over the top&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The PIR plus and PIR minus pins go straight into the mystery chip. The LDR is part of a potential divider with R5 feeding another pin on the chip, with a decoupling cap on the rail. Over on the charging side there’s a TP4054 with a 3.3k programming resistor — that’s about 300 mA of charge current — and an LED to show the state. That’s it. Highly optimised BOM, one big mystery IC doing all the work.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/schematic.webp&quot; alt=&quot;Reverse engineered schematic&quot; /&gt;&lt;/p&gt;

&lt;p&gt;What’s interesting is there’s no current limiting on the LEDs. Nothing at all. The chip is clearly driving them directly, and presumably there’s some limit built in to the chip itself.&lt;/p&gt;

&lt;h1 id=&quot;how-much-current&quot;&gt;How Much Current?&lt;/h1&gt;

&lt;p&gt;Before doing anything else, I wanted to know how hard this thing is actually driving the LEDs. Desolder the battery, hook the pads up to a bench supply with the current limit set to 100 mA, and trigger the PIR.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/bench.webp&quot; alt=&quot;Board running on the bench supply with the LEDs lit&quot; /&gt;&lt;/p&gt;

&lt;p&gt;68 mA. So there is some built-in limiting on the mystery chip after all. This mystery IC is doing a lot of heavy lifting.&lt;/p&gt;

&lt;h1 id=&quot;playing-with-the-radar-module&quot;&gt;Playing with the Radar Module&lt;/h1&gt;

&lt;p&gt;These RC-WL0516 modules are lovely. They have pads for a light dependent resistor so they only trigger in the dark, and a space to solder on a capacitor to extend the timeout.&lt;/p&gt;

&lt;p&gt;I wired one up on a breadboard, hung an LED off the output through a current limit resistor, and fed it 5 V.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/breadboard_led.webp&quot; alt=&quot;Radar module on a breadboard driving a red LED&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Stand still, the LED goes off. Move, it comes back on. The default timeout is about three seconds. As I mentioned, three seconds is nowhere near enough time to get to the toilet, so I soldered a 0.22 µF capacitor to the CTM pad to extend it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/capacitor.webp&quot; alt=&quot;Soldering a 0.22 µF capacitor onto the CTM pad&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Much better. I also wired up an LDR so it only triggers in the dark, and confirmed the motion detection works really nicely — much more sensitive than the original PIR ever was.&lt;/p&gt;

&lt;h1 id=&quot;the-modification&quot;&gt;The Modification&lt;/h1&gt;

&lt;p&gt;Here’s the plan. Strip the PIR sensor, the mystery chip, and its supporting passives off the original board. Sit the radar module on top where there’s a nice flat empty space. Drive the LEDs through a current limiting resistor and a small MOSFET controlled by the radar module’s output pin. Power everything from the existing battery pads.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/plan.webp&quot; alt=&quot;The radar module test fitted on the stripped PCB&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Microscope out, and with a bit of soldering the whole thing came together more neatly than I expected. I took the opportunity to upgrade the battery too — swapping the tiny 350 mAh cell for a much more generous 820 mAh one.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/modded.webp&quot; alt=&quot;The modified board with the 820 mAh battery, LEDs lit&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The board slides back into its case, the battery tucks in alongside, the USB port still lines up with the hole in the shell, the screw goes back in, the diffuser clips in, and… good as new. You’d never know anything had happened.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/final_diffuser.webp&quot; alt=&quot;Finished, diffuser on, LEDs glowing&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;does-it-actually-work&quot;&gt;Does It Actually Work?&lt;/h1&gt;

&lt;p&gt;Stuck it on the stairs, turned the lights off, and waited. Walk past — it lights up. Stand still — it goes out after a sensible amount of time. Sensitivity is miles better than it was, the battery should last a lot longer, and the timeout is long enough to be useful.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-11/hero_stairs.webp&quot; alt=&quot;The modified night light installed on the stairs, glowing&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It works. Now I just need to do the rest of them…&lt;/p&gt;

&lt;p&gt;If you want to see the whole thing in motion — the teardown, the reverse engineering, the soldering, and the moment it finally lights up on the stairs — &lt;a href=&quot;https://www.youtube.com/watch?v=eVac2GMyddk&quot;&gt;go and watch it on YouTube&lt;/a&gt;. Likes and subscribes are always appreciated.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;eVac2GMyddk&quot; playlabel=&quot;Hacking a Cheap PIR Night Light&quot;&gt;&lt;/lite-youtube&gt;
</description>
      <pubDate>Sat, 11 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/04/11/pir-night-light-upgrade</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/04/11/pir-night-light-upgrade.html</guid>
        
    </item>
    
    <item>
      <title>I learned to solve a Rubik&apos;s cube and it was incredibly disappointing</title>
      <description>&lt;p&gt;I recently decided to learn how to solve the Rubik’s cube. As a self-confessed geek, it felt like something I should be able to do.&lt;/p&gt;

&lt;p&gt;I found a pretty good &lt;a href=&quot;https://easiestsolve.com/step-1-the-daisy/&quot;&gt;website&lt;/a&gt; that breaks it down into eight steps, from “the Daisy” all the way through to “Finish Him!”. I memorised the moves. Pretty soon I could solve it without looking at the website.&lt;/p&gt;

&lt;p&gt;It was incredibly disappointing.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-05/rubiks.webp&quot; alt=&quot;Rubiks be gone!&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I was just mechanically following “algorithms” I’d memorised. No skill, no insight. Pattern match, apply algorithm, repeat.&lt;/p&gt;

&lt;p&gt;So I thought, this can’t be how speed cubers do it. Steps 1-3 are painfully tedious. There must be a faster way.&lt;/p&gt;

&lt;p&gt;This led me to F2L, solving the first two layers simultaneously. The first resource I found suggested memorising 42 algorithms - my response - yeah, I’m not doign that…&lt;/p&gt;

&lt;p&gt;Then I found a few suggestions on solving F2L “intuitively”, which sounded more my style. Maybe this approach would actually involve some thinking.&lt;/p&gt;

&lt;p&gt;It’s definitely faster and requires a bit more brainpower, but ultimately? Still pattern matching. I wanted to understand &lt;em&gt;why&lt;/em&gt; the algorithms work, not just memorise better ones.&lt;/p&gt;

&lt;p&gt;Frustrated, I did what everyone does now. I asked ChatGPT.&lt;/p&gt;

&lt;p&gt;After the obligatory “That’s a brilliant insight!” it gave me this analogy:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Beginner: following GPS directions&lt;/li&gt;
  &lt;li&gt;Intermediate: memorised routes&lt;/li&gt;
  &lt;li&gt;Advanced: understand the map&lt;/li&gt;
  &lt;li&gt;Expert: improvise new routes instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep coming back to this because it maps so well onto the junior-to-senior engineer journey.&lt;/p&gt;

&lt;p&gt;You start out following instructions. At some point you start pattern matching. This problem looks like that problem I solved last year. Eventually the memorised playbook isn’t enough and you start synthesising new solutions from first principles. You develop taste. Judgment. The ability to look at a system you’ve never seen and know where the problems are going to be.&lt;/p&gt;

&lt;p&gt;That last part is what I find interesting about coding agents right now. They’re very good at levels one and two. Following instructions, applying known patterns at scale, faster and more consistently (well, most of the time…) than any person could.&lt;/p&gt;

&lt;p&gt;And sometimes watching an agent churn out code gives me that same Rubik’s cube feeling. It works. It passes the tests. But I’m just sitting here approving it. What am I even doing here?&lt;/p&gt;

&lt;p&gt;But then you hit a problem where the agent can’t see what you see. It solves the ticket but puts the logic in the wrong place. It writes code that works today but will be a nightmare in six months. And you remember what you’re doing here. That’s the difference between mechanically producing a solution and deeply understanding the solution.&lt;/p&gt;

&lt;p&gt;For now, experienced engineers are the ones providing the taste and judgment that makes the difference between code that works and code that works well.&lt;/p&gt;

&lt;p&gt;I’m never going to be a speed cuber. But maybe, with the help of agents, I might end up being a speed coder…&lt;/p&gt;
</description>
      <pubDate>Sun, 05 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/04/05/rubiks-cube</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/04/05/rubiks-cube.html</guid>
        
    </item>
    
    <item>
      <title>Why Won&apos;t My Board Reset?</title>
      <description>&lt;p&gt;I’ve got a slightly annoying problem with one of my dev boards. The reset button does absolutely nothing.&lt;/p&gt;

&lt;p&gt;These are the boards I got made by PCBWay — they did the PCBs and they came out really nice. I did the assembly myself. But somewhere along the way, something went wrong on one of them.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;VdJXJQXXZoo&quot; playlabel=&quot;Why won&apos;t my board reset?&quot;&gt;&lt;/lite-youtube&gt;

&lt;h1 id=&quot;a-tale-of-two-boards&quot;&gt;A Tale of Two Boards&lt;/h1&gt;

&lt;p&gt;I loaded both boards with a fast blink sketch to make the problem obvious. On the working board, hitting the reset button stops the flashing immediately — exactly what you’d expect. On the broken board? Nothing. The LED just keeps happily blinking away.&lt;/p&gt;

&lt;p&gt;This is more than just a minor annoyance. To get an ESP32 into programming mode, you hold down the boot button and then press reset. Without a working reset button, I’m constantly having to unplug and re-plug the USB cable. Not ideal.&lt;/p&gt;

&lt;h1 id=&quot;checking-the-schematic-and-pcb&quot;&gt;Checking the Schematic and PCB&lt;/h1&gt;

&lt;p&gt;First things first — let’s make sure the design is actually correct.&lt;/p&gt;

&lt;p&gt;The schematic looks fine: the reset switch is connected to the EN (enable) pin through the standard RC circuit — a resistor and a capacitor. Nothing unusual there.&lt;/p&gt;

&lt;p&gt;The PCB layout checks out too. Tracing the path from the reset switch down to ground, up through the RC network, and all the way to the EN pin — everything is routed correctly.&lt;/p&gt;

&lt;p&gt;So the design isn’t the problem. Time to get the multimeter out.&lt;/p&gt;

&lt;h1 id=&quot;buzzing-out-the-circuit&quot;&gt;Buzzing Out the Circuit&lt;/h1&gt;

&lt;p&gt;I started probing the connections on the board. The EN pin to the resistor — good. EN pin to the capacitor — good. The trace from the RC network to the switch — also good.&lt;/p&gt;

&lt;p&gt;Then I put the probes across the switch itself and pressed the button.&lt;/p&gt;

&lt;p&gt;Nothing. No continuity at all.&lt;/p&gt;

&lt;p&gt;The boot button on the same board? Works perfectly. But the reset button is completely dead.&lt;/p&gt;

&lt;h1 id=&quot;under-the-microscope&quot;&gt;Under the Microscope&lt;/h1&gt;

&lt;p&gt;Time for a closer look. Under the microscope, the soldering on the ESP32’s EN pin looks fine — which the continuity test already confirmed. The RC components look good too.&lt;/p&gt;

&lt;p&gt;The button itself? It looks solidly soldered. Visually, I couldn’t see any obvious problems. Comparing it with the boot button, the boot button’s joints were a bit shinier, but nothing that would explain a total failure.&lt;/p&gt;

&lt;p&gt;It’s funny how boards look under the microscope — from a distance, this board looks perfectly clean, but zoom in and there’s all sorts of hairs and disgusting stuff.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-04-04/yuck.webp&quot; alt=&quot;Yuck&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;reflow-and-test&quot;&gt;Reflow and Test&lt;/h1&gt;

&lt;p&gt;With no obvious visual fault, I decided to reflow the solder joints on the reset button. Applied some flux, heated up each joint, and could feel solid connections on both sides of the switch.&lt;/p&gt;

&lt;p&gt;Buzzed it out again. Still nothing. The button clicks mechanically, but there’s no electrical continuity when pressed.&lt;/p&gt;

&lt;p&gt;At this point, it’s clear: &lt;strong&gt;the button itself is the problem.&lt;/strong&gt;&lt;/p&gt;

&lt;h1 id=&quot;the-fix-just-add-alcohol&quot;&gt;The Fix: Just Add Alcohol&lt;/h1&gt;

&lt;p&gt;Looking more closely, the inside of the button seemed to be full of flux residue. On a whim, I soaked the board in IPA (isopropyl alcohol) to try and clean it out.&lt;/p&gt;

&lt;p&gt;After a good soak, I probed the button again — and it worked perfectly. Every press gave clean, reliable continuity.&lt;/p&gt;

&lt;p&gt;Plugging the board back in, the blink sketch was running. Hit the reset button and… the flashing stopped. Success!&lt;/p&gt;

&lt;h1 id=&quot;lesson-learned&quot;&gt;Lesson Learned&lt;/h1&gt;

&lt;p&gt;The whole problem was flux residue inside the tactile switch, preventing the internal contacts from making a clean connection. The solder joints were fine, the traces were fine, the schematic was fine — it was just a dirty button.&lt;/p&gt;

&lt;p&gt;A remarkably simple fix in the end. A bit of cleaning was all it needed.&lt;/p&gt;

&lt;p&gt;The lesson? Use more alcohol — that’s the conclusion I’m taking from this…&lt;/p&gt;

</description>
      <pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/04/04/why-no-reset</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/04/04/why-no-reset.html</guid>
        
    </item>
    
    <item>
      <title>Is SOT666 Really Standard?</title>
      <description>&lt;h1 id=&quot;is-sot666-really-standard-spoiler-not-really&quot;&gt;Is SOT666 Really Standard? Spoiler: Not Really&lt;/h1&gt;

&lt;p&gt;I’ve been investigating some quality assurance failures on my ESP32 rainbow boards and tracked a few issues down to a USBLC6 ESD protection IC in a SOT666 package. Under the microscope, it looked like the footprint didn’t quite match the package — like it had missed alignment and hadn’t soldered on properly.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;z3s550opeZ0&quot; playlabel=&quot;Is SOT666 Really Standard?&quot;&gt;&lt;/lite-youtube&gt;

&lt;p&gt;I fixed the immediate problem by reflowing the solder and pushing the device into place. But this should be automatic — the pick and place machine puts the component down and solder tension should pull it into alignment. So I started wondering: is the footprint actually correct?&lt;/p&gt;

&lt;h1 id=&quot;the-problem-footprint-vs-datasheet&quot;&gt;The Problem: Footprint vs Datasheet&lt;/h1&gt;

&lt;p&gt;Opening up the schematic in KiCad, the USBLC6 is assigned a SOT666 footprint. That’s the correct package — so far so good.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-22/sot-666-kicad.webp&quot; alt=&quot;KiCad schematic showing the USBLC6 with SOT666 footprint&quot; /&gt;&lt;/p&gt;

&lt;p&gt;But when I compared the KiCad footprint against the recommended footprint in the USBLC6 datasheet, things looked pretty different.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-22/footprint-comparison.webp&quot; alt=&quot;KiCad footprint vs datasheet recommended footprint&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The KiCad footprint for SOT666 is completely different from the footprint in the datasheet — even though they’re both supposed to be SOT666.&lt;/p&gt;

&lt;h1 id=&quot;so-is-kicad-wrong&quot;&gt;So Is KiCad Wrong?&lt;/h1&gt;

&lt;p&gt;Maybe. But it’s not that simple.&lt;/p&gt;

&lt;p&gt;If you Google “SOT666” you’ll find all sorts of different footprints from different manufacturers. Here’s how they stack up:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Nexperia’s SOT666 definition&lt;/strong&gt; — looks very similar to KiCad’s version. In fact, I think KiCad’s footprint is based on this one.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ST’s USBLC6 datasheet&lt;/strong&gt; — has its own recommended footprint with quite different dimensions.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Diodes Incorporated’s SOT666&lt;/strong&gt; — yet another completely different suggested pad layout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-22/different-footprints.webp&quot; alt=&quot;Different SOT666 footprints from different manufacturers&quot; /&gt;&lt;/p&gt;

&lt;p&gt;So you’ve got a “standard” package name — SOT666 — but every manufacturer seems to have their own opinion about what the footprint should look like.&lt;/p&gt;

&lt;h1 id=&quot;checking-easyeda-for-comparison&quot;&gt;Checking EasyEDA for Comparison&lt;/h1&gt;

&lt;p&gt;I thought it might be instructive to check another EDA package. In EasyEDA, the SOT666 footprint for the USBLC6-2P6 is different again:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Pad width (PW1):&lt;/strong&gt; 0.3 mm — this actually matches the ST datasheet&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pad length (PL1):&lt;/strong&gt; 0.85 mm — the ST datasheet says 0.99 mm, so still different&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-22/easy-eda-footprint.webp&quot; alt=&quot;What does EasyEDA think?&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Three different tools, three different footprints, all for the same “standard” package. Not exactly confidence-inspiring.&lt;/p&gt;

&lt;h1 id=&quot;the-lesson-always-follow-the-datasheet&quot;&gt;The Lesson: Always Follow the Datasheet&lt;/h1&gt;

&lt;p&gt;The basic conclusion is that although these packages share the same name, the footprints are not really standardised. Everything is randomly different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The advice: look in the datasheet and use what’s in the datasheet.&lt;/strong&gt; Don’t rely on the standard package names because they might not quite match up with your specific component.&lt;/p&gt;

&lt;h1 id=&quot;creating-a-custom-footprint-in-kicad&quot;&gt;Creating a Custom Footprint in KiCad&lt;/h1&gt;

&lt;p&gt;Fortunately, it’s really easy to create footprints in KiCad. Using the footprint wizard with the SOIC template, I just needed to plug in the values from the ST datasheet:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Pad count:&lt;/strong&gt; 6 (two rows of 3)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pitch:&lt;/strong&gt; 0.5 mm&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pad width:&lt;/strong&gt; 0.3 mm&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pad length:&lt;/strong&gt; 0.99 mm&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Row spacing:&lt;/strong&gt; 1.61 mm (0.62 mm gap + 0.99 mm pad length)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-22/kicad-wizard.webp&quot; alt=&quot;KiCad footprint wizard with datasheet values&quot; /&gt;&lt;/p&gt;

&lt;p&gt;A quick sanity check with the ruler confirmed everything matched the datasheet — the 0.62 mm gap between rows and 2.6 mm overall dimensions were spot on.&lt;/p&gt;

&lt;h1 id=&quot;updating-the-pcb&quot;&gt;Updating the PCB&lt;/h1&gt;

&lt;p&gt;It took a bit of finagling — I had to make some traces slightly thinner so they didn’t collide — but the new footprint dropped into the design nicely.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-22/updated-pcb.webp&quot; alt=&quot;Updated PCB with the new footprint&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The 3D preview looks good. Obviously the proof of the pudding will be the next production run, but it matches the datasheet now — so fingers crossed it will eliminate this class of QA errors.&lt;/p&gt;

&lt;h1 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h1&gt;

&lt;p&gt;If you’re designing PCBs, don’t assume that a standard package name means a standard footprint. Always cross-reference with your specific component’s datasheet. It only takes a few minutes to create a custom footprint in KiCad, and it could save you from chasing down mysterious assembly failures.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;z3s550opeZ0&quot; playlabel=&quot;Is SOT666 Really Standard?&quot;&gt;&lt;/lite-youtube&gt;
</description>
      <pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/03/15/is-sot666-standard</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/03/15/is-sot666-standard.html</guid>
        
    </item>
    
    <item>
      <title>MAX30102 $3 Heart Rate Monitor</title>
      <description>&lt;h1 id=&quot;building-a-heart-rate-monitor-with-an-esp32-c3-and-max30102&quot;&gt;Building a Heart Rate Monitor with an ESP32-C3 and MAX30102&lt;/h1&gt;

&lt;p&gt;You know how it is. You buy one cute little module from AliExpress and then think “it looks lonely, I’ll get another”. Before you know it, you’ve got a drawer full of cute little modules. So it’s time to put one of them to work.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;0OV5aCOnXBA&quot; playlabel=&quot;Cheap Heart Rate Monitor&quot;&gt;&lt;/lite-youtube&gt;

&lt;h1 id=&quot;the-esp32-c3-board&quot;&gt;The ESP32-C3 Board&lt;/h1&gt;

&lt;p&gt;I’ve got a bunch of these tiny ESP32-C3 boards kicking around. Let’s take a look at one under the microscope.&lt;/p&gt;

&lt;p&gt;The brains of the operation is an ESP32-C3 with built-in flash — four megabytes of it, which is pretty decent.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-15/back.webp&quot; alt=&quot;ESP32-C3 board under the microscope&quot; /&gt;&lt;/p&gt;

&lt;p&gt;There’s a 3.3 volt regulator on board, and the soldering looks nicely done. It’s a pretty clean, well-designed little board. The antenna is a ceramic one on the side — though I’ve read a few people saying that on these tiny compact boards you don’t get much Wi-Fi signal due to the antenna placement being a bit too close to the buttons and other components. So we’ll avoid Wi-Fi for this project.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-15/front.webp&quot; alt=&quot;ESP32-C3 board under the microscope&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;the-max30102-heart-rate-and-blood-oxygen-sensor&quot;&gt;The MAX30102 Heart Rate and Blood Oxygen Sensor&lt;/h1&gt;

&lt;p&gt;For the actual sensor, I’m using a MAX30102 module — a heart rate and blood oxygen monitoring board that I picked up in a recent AliExpress order.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-15/max30102.webp&quot; alt=&quot;MAX30102 sensor module&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The board has a 3.3 volt boost regulator and a 1.8 volt regulator. Some people have reported finding a 2.8 volt regulator instead of 1.8 on some boards — it’s worth checking yours is correct. Mine was fine.&lt;/p&gt;

&lt;h1 id=&quot;how-the-sensor-works&quot;&gt;How the Sensor Works&lt;/h1&gt;

&lt;p&gt;The really interesting part is the sensor itself. Under the microscope you can see:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-15/max30102-closeup.webp&quot; alt=&quot;Close-up of the MAX30102 sensor&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Two LEDs&lt;/strong&gt; behind a window — a red LED and an infrared LED&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;A photodetector&lt;/strong&gt; with a bunch of DSP processing built in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The outputs from this device are just the value of the red channel and the value of the infrared channel. You stick your finger on top and it detects pulse rate using the red LED and blood oxygen levels using the infrared LED.&lt;/p&gt;

&lt;p&gt;When you power it up, you can easily see the red LED. If you cover it up and move the lights away, you can even see a slight glow from the infrared LED — the camera on the microscope picks it up nicely.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-15/leds.webp&quot; alt=&quot;MAX30102 LEDs&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-15/infra-red.webp&quot; alt=&quot;MAX30102 Infra Red LED&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;wiring-it-up&quot;&gt;Wiring It Up&lt;/h1&gt;

&lt;p&gt;The wiring is pretty simple — it’s just I2C plus power:&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;byte&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oxiInt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// pin connected to MAX30102 INT&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SENSOR_I2C_SDA_PIN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SENSOR_I2C_SCL_PIN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Pin 6&lt;/strong&gt; → SCL&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pin 5&lt;/strong&gt; → SDA&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pin 2&lt;/strong&gt; → Interrupt (tells us when data is ready)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;5V&lt;/strong&gt; → Vin (the board has its own 3.3V regulator)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;GND&lt;/strong&gt; → GND&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-15/wiring.webp&quot; alt=&quot;Wiring diagram&quot; /&gt;&lt;/p&gt;

&lt;p&gt;A quick I2C scan confirms everything is connected properly — two devices show up: the I2C display and the heart rate monitor.&lt;/p&gt;

&lt;h1 id=&quot;the-code&quot;&gt;The Code&lt;/h1&gt;

&lt;p&gt;The main loop reads samples from the MAX30102 into a ring buffer, then runs the heart rate and SpO2 algorithms once the buffer is full:&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n_spo2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ratio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;correl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int8_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ch_spo2_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n_heart_rate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int8_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ch_hr_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Read UPDATE_STEP new samples into the ring buffer&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UPDATE_STEP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;readNextSampleIntoRing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data_ready_timeout_us&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Need a full buffer before we can calculate&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sample_count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BUFFER_SIZE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;buildOrderedWindow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Calculate heart rate and SpO2&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;rf_heart_rate_and_oxygen_saturation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ordered_ir_buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BUFFER_SIZE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ordered_red_buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_spo2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch_spo2_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_heart_rate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch_hr_valid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ratio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;correl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Median filter smooths out noisy readings&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;hr_filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch_hr_valid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n_heart_rate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;INVALID_HR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;spo2_filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch_spo2_valid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n_spo2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;INVALID_SPO2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;updateDisplay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filtered_heart_rate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filtered_spo2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The display code is pretty straightforward — just show the heart rate and oxygen percentage on the tiny OLED, or dashes if we don’t have a valid reading:&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;updateDisplay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spo2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;u8g2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;clearBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;u8g2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setFont&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;u8g2_font_10x20_mr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;u8g2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;19&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;250&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;u8g2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;HR: %d&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;u8g2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;HR: --&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;u8g2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;39&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spo2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spo2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;u8g2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;O2: %d%%&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spo2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;u8g2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;O2: --&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;u8g2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sendBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There’s also a nice touch — the on-board LED flashes in time with your heartbeat.&lt;/p&gt;

&lt;h1 id=&quot;getting-readings&quot;&gt;Getting Readings&lt;/h1&gt;

&lt;p&gt;With the sketch uploaded and the OLED display showing results, it’s time to test.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-15/heart-rate.webp&quot; alt=&quot;Heart rate readings on the display&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It can be a bit finicky about finger placement — you need to keep it very still. But once it settles, it starts producing results: heart rate and SpO2 percentage.&lt;/p&gt;

&lt;h1 id=&quot;comparing-to-a-commercial-device&quot;&gt;Comparing to a Commercial Device&lt;/h1&gt;

&lt;p&gt;The real test — how does it compare to a proper pulse oximeter?&lt;/p&gt;

&lt;p&gt;With both running side by side:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Heart rate: &lt;strong&gt;around 70 BPM&lt;/strong&gt; on the ESP32-C3, matching the commercial monitor&lt;/li&gt;
  &lt;li&gt;Blood oxygen: &lt;strong&gt;99%&lt;/strong&gt; on both devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-03-15/compare.webp&quot; alt=&quot;Side-by-side comparison with commercial pulse oximeter&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Not bad at all. The main challenge is keeping your finger positioned accurately without squishing it on — it’s quite easy to lose the reading. But when you get a good contact, the results match up well.&lt;/p&gt;

&lt;h1 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h1&gt;

&lt;p&gt;The full code is on &lt;a href=&quot;https://github.com/atomic14/max30102-esp32c3-oled-oximeter&quot;&gt;GitHub&lt;/a&gt;. Grab yourself one of these ESP32-C3 modules, a MAX30102 sensor, and a little OLED display, and you can build something equivalent to a commercial pulse oximeter for very little money. It’s a fun project — why not try it out?&lt;/p&gt;

&lt;lite-youtube videoid=&quot;0OV5aCOnXBA&quot; playlabel=&quot;Cheap Heart Rate Monitor&quot;&gt;&lt;/lite-youtube&gt;
</description>
      <pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/03/15/cheap-heart-rate-monitor</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/03/15/cheap-heart-rate-monitor.html</guid>
        
    </item>
    
    <item>
      <title>I Made My ESP32-S3 Faster by Optimizing for SIZE — Not Speed</title>
      <description>&lt;h1 id=&quot;optimizing-esp32-s3-performance-why-smaller-code-can-ran-faster&quot;&gt;Optimizing ESP32-S3 Performance: Why Smaller Code Can Ran Faster&lt;/h1&gt;

&lt;p&gt;I’ve been tuning the performance of some code recently on an ESP32-S3 project,&lt;/p&gt;

&lt;p&gt;There was a slightly counter-intuitive result that might surprise some people: optimizing for code size produced faster runtime performance than optimizing for performance.&lt;/p&gt;

&lt;p&gt;There’s a full video here - but you can skim this post for most of the information.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;cqHH2NXcf5E&quot; playlabel=&quot;Size is better than speed&quot;&gt;&lt;/lite-youtube&gt;

&lt;h1 id=&quot;the-benchmark&quot;&gt;The Benchmark&lt;/h1&gt;

&lt;p&gt;This is just some code that I pulled out of my latest project. It’s not an official piece of benchmarking code. So your mileage will definitely vary and you shohld do your own tests on your own code.&lt;/p&gt;

&lt;p&gt;I kept my test case was deliberately simple and pulled out the most performance critical part of my code.&lt;/p&gt;

&lt;p&gt;The code, input data, and hardware stayed constant throughout. Only ESP-IDF configuration options were changed between runs.&lt;/p&gt;

&lt;h1 id=&quot;baseline-results&quot;&gt;Baseline Results&lt;/h1&gt;

&lt;p&gt;Starting from a stock ESP-IDF configuration (I used their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hello_world&lt;/code&gt; starter app), the decode took roughly &lt;strong&gt;1.4 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From there, I explored a few of the usual performance levers:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;CPU frequency (an obvious one!)&lt;/li&gt;
  &lt;li&gt;Compiler optimization level&lt;/li&gt;
  &lt;li&gt;Flash SPI mode&lt;/li&gt;
  &lt;li&gt;Cache configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;cpu-frequency-the-obvious-win&quot;&gt;CPU Frequency: The Obvious Win&lt;/h1&gt;

&lt;p&gt;Unsurprisingly, increasing the CPU frequency from 160 MHz to 240 MHz produced an immediate improvement.&lt;/p&gt;

&lt;p&gt;The result was roughly the 1.5× speed-up thst you woulld hope for (it was actually around 1.46 - but close enough).&lt;/p&gt;

&lt;h1 id=&quot;compiler-optimizations-size-vs-speed&quot;&gt;Compiler Optimizations: Size vs Speed&lt;/h1&gt;

&lt;p&gt;ESP-IDF menu config offers two commonly used optimization modes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Optimize for size (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Os&lt;/code&gt;)&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Optimize for performance (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-O2&lt;/code&gt;)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Weirdly, in my first job (many years ago!), this was discussed in quite detail.&lt;/p&gt;

&lt;p&gt;You naively expect that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-O2&lt;/code&gt; should be faster than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Os&lt;/code&gt;. In this case, it wasn’t.&lt;/p&gt;

&lt;p&gt;With this workload, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Os&lt;/code&gt; outperformed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-O2&lt;/code&gt; quite significantly. They both beat the debug build - but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Os&lt;/code&gt; improved it the most.&lt;/p&gt;

&lt;h1 id=&quot;why-would-smaller-code-be-faster&quot;&gt;Why Would Smaller Code Be Faster?&lt;/h1&gt;

&lt;p&gt;On the ESP32-S3, application code normally runs from flash memory with an instruction cache (you can actually copy code into PSRAM if it’s available which can be faster than flash).&lt;/p&gt;

&lt;p&gt;Flash is pretty slow which means the CPU caches can have a significant impact.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;instruction cache size&lt;/li&gt;
  &lt;li&gt;cache line fetches&lt;/li&gt;
  &lt;li&gt;cache miss penalties&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optimizing for performance often:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;increases inlining&lt;/li&gt;
  &lt;li&gt;unrolls loops&lt;/li&gt;
  &lt;li&gt;duplicates code paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this increases code size, which can lead to more instruction cache misses and more reads from the flash.&lt;/p&gt;

&lt;p&gt;Optimizing for size does the opposite: it produces tighter code that is more likely to stay resident in the cache, reducing stalls caused by flash fetches.&lt;/p&gt;

&lt;p&gt;In this workload, that effect outweighed the benefits of more aggressive instruction-level optimizations.&lt;/p&gt;

&lt;h1 id=&quot;flash-mode-and-cache-configuration&quot;&gt;Flash Mode and Cache Configuration&lt;/h1&gt;

&lt;p&gt;Because the GIF was embedded in flash and accessed frequently, flash bandwidth also mattered.&lt;/p&gt;

&lt;p&gt;Two changes helped further:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Switching the flash SPI mode from DIO to QIO (chack that your module supports this!)&lt;/li&gt;
  &lt;li&gt;Increasing instruction and data cache sizes to their maximum values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These changes reduced the cost of cache refills and improved overall throughput.&lt;/p&gt;

&lt;p&gt;Interestingly, once the caches were enlarged, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-O2&lt;/code&gt; did improve — but it still didn’t beat the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Os&lt;/code&gt; configuration with the same cache settings.&lt;/p&gt;

&lt;h1 id=&quot;the-winning-configuration&quot;&gt;The Winning Configuration&lt;/h1&gt;

&lt;p&gt;For this benchmark, the fastest setup was:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;CPU frequency: &lt;strong&gt;240 MHz&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;Compiler optimization: &lt;strong&gt;Optimize for size (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Os&lt;/code&gt;)&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;Instruction &amp;amp; data cache: &lt;strong&gt;maximum&lt;/strong&gt; (turn it up to 11!)&lt;/li&gt;
  &lt;li&gt;Flash SPI mode: &lt;strong&gt;QIO&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This combination produced the lowest overall decode time.&lt;/p&gt;

&lt;h1 id=&quot;this-is-the-case-for-my-specific-workload&quot;&gt;This is the case for my specific workload!&lt;/h1&gt;

&lt;p&gt;It’s important to be clear about the limits of this result.&lt;/p&gt;

&lt;p&gt;You may not see the same behaviour if:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Your hot code runs entirely from the instruction cache or IRAM&lt;/li&gt;
  &lt;li&gt;Your workload is dominated by tight math loops on small datasets&lt;/li&gt;
  &lt;li&gt;You’re memory-bandwidth bound on internal RAM rather than flash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In those cases, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-O2&lt;/code&gt; or even more aggressive optimization may still win.&lt;/p&gt;

&lt;h1 id=&quot;full-results-table&quot;&gt;Full Results Table&lt;/h1&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Build type&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;usecs&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;ms&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;% of baseline&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Baseline&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1425476&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1425.48&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;100&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;240MHz&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;965408&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;965.4&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;67.72&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;240MHz + Os&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;872495&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;872.5&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;61.21&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;240MHz + O2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;962329&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;962.3&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;67.50&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;240MHz + Os + QIO&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;861859&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;861.9&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;60.46&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;240MHz + Os + QIO + caches&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;843286&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;843.3&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;59.16&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;240MHz + O2 + QIO + caches&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;933093&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;933.1&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;65.46&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;img src=&quot;/assets/article_images/2026-02-02/chart.webp&quot; alt=&quot;Chart&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h1&gt;

&lt;p&gt;Performance is often limited by memory and caching, not raw CPU execution.&lt;/p&gt;

&lt;p&gt;Smaller code can mean fewer cache misses - and fewer cache misses can mean faster code.&lt;/p&gt;

&lt;p&gt;If you’re working on performance-critical ESP-IDF projects, it’s worth benchmarking &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Os&lt;/code&gt; alongside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-O2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you’ve tried similar experiments on other ESP32 variants, I’d love to hear what you found.&lt;/p&gt;

&lt;lite-youtube videoid=&quot;cqHH2NXcf5E&quot; playlabel=&quot;Size is better than speed&quot;&gt;&lt;/lite-youtube&gt;
</description>
      <pubDate>Fri, 06 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://www.atomic14.com/2026/03/06/size-better-than-speed</link>
      <!-- guid kept as the original .html permalink so existing items are not re-flagged as new in feed readers -->
      <guid isPermaLink="true">https://www.atomic14.com/2026/03/06/size-better-than-speed.html</guid>
        
    </item>
    
  </channel>
</rss>
