Jan 2, 2026

A better way to create non-blocking timed events in C++ /Arduino

I've been using the usual if / counter / compare style timers for years — increment a counter, check thresholds, reset, repeat. It works, but it's easy to get edge cases wrong, and it clutters logic. I recently learned a much cleaner approach that treats time as a phase and simply asks a question: “am I in the ON window right now?”

The result is a tiny helper that returns a boolean. No counters, no wrap bugs, no blocking. Just compute phase and decide.


// Return true during the ON window of a periodic heartbeat
bool heartbeatActive(uint32_t now_ms,
                     uint32_t period_ms,
                     uint32_t on_ms)
{
    // compute position within the cycle
    uint32_t phase = now_ms % period_ms;
    return (phase < on_ms);
}

Usage becomes very declarative. The function name explains intent, and assignment is a single line.


uint32_t now = millis();

device_state = heartbeatActive(now,
                               HB_PERIOD_MS,
                               HB_ON_MS);

digitalWrite(LED_PIN, device_state);

This replaces the usual pattern of counters, comparisons, and manual resets. There’s no state to maintain, no rollover logic to reason about, and no blocking delays. You can reuse the same helper for multiple GPIOs just by passing different parameters.


digitalWrite(LED_STATUS,
    heartbeatActive(now, 1000, 50));

digitalWrite(LED_ERROR,
    heartbeatActive(now, 250, 25));

digitalWrite(LED_ACTIVITY,
    heartbeatActive(now, 2000, 200));

What I like about this approach:

  • no counters to overflow or go negative
  • no hidden state — behavior is a pure function of time
  • easy to reason about and reuse
  • scales cleanly when multiple timers are needed

This feels closer to how I want to think about timing logic: define the period, define the active window, and let the math do the rest.

Making guard rings on PCBs for High-Z Amp Inputs

A C-shaped guard ring is acceptable and is standard practice. It's not really a "ring."

  • You do not need a fully closed 360° loop.

  • The guard ring's job is to intercept leakage paths, not form a Faraday cage.

  • Leave an intentional gap where the signal trace enters the +IN node.

  • Do not route the guard under the signal trace or jump layers just to close the loop — that usually does more harm than good.

  • Drive the guard at the same potential as +IN (voltage follower output is fine).

Dec 24, 2025

how to use the integrated TFT display on the Adafruit ESP32 board

I selected the Adafruit ESP32-S3 w/ TFT display as part of an impact measurement test fixture. By pairing this with an accelerometer, I got a portable, compact module that shows immediate results including a mini plot of the impact profile without needing a laptop to capture and convert results.

This code shows how to properly initialize the TFT on this board and a combination of libraries to get suitable text and graphics. (This info was not easy to find.) As an aside, it also shows the method to compress RGB colors to RGB565; a format for this hardware and common to displays in embedded systems.

The Critical Power-Up Sequence

  1. TFT_I2C_POWER: Master gate. Drive HIGH to provide Vcc to the display.
  2. Delay: 10ms settling time for logic stability.
  3. TFT_BACKLITE: Master backlight enable. (note spelling of this lib constant.) 
// -------------------------------------------------------------------------
// FILENAME: impact_logger_tft_init.ino
// VERSION: v1.0.5-FINAL
// DATE: 2025-DEC-26 06:25 AM PT
// -------------------------------------------------------------------------

#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include "pins_arduino.h"

// Function converts 24-bit RGB values to compressed 16-bit (565) form for TFT.
// Explicit uint16_t casting prevents bit-bleeding on 32-bit processors.
constexpr uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((uint16_t)(r & 0xF8) << 8) | ((uint16_t)(g & 0xFC) << 3) | (b >> 3);
}

// --- UI COLORS (Tuned for ST7789 Panel Saturation) ---
constexpr uint16_t CLR_BG = rgb565(0, 0, 0);
constexpr uint16_t CLR_LABEL = rgb565(120, 120, 120);
constexpr uint16_t CLR_VALUE = rgb565(255, 230, 0); // Clear Yellow
constexpr uint16_t CLR_PLOT = rgb565(255, 140, 0); // High-Vis Orange
constexpr uint16_t CLR_RISE = rgb565(0, 180, 180); // Vibrant Teal
constexpr uint16_t CLR_TAN = rgb565(210, 180, 140); // Instrumentation Tan
constexpr uint16_t CLR_FRAME = rgb565(100, 100, 100);

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
uint32_t status_counter = 0;

void initDisplay() {
pinMode(TFT_I2C_POWER, OUTPUT);
digitalWrite(TFT_I2C_POWER, HIGH);
delay(10);
pinMode(TFT_BACKLITE, OUTPUT);
digitalWrite(TFT_BACKLITE, HIGH);

tft.init(135, 240); // (width, height) ref: NATIVE portrait dimensions
tft.setRotation(3); // Landscape: USB port on the LEFT
tft.fillScreen(CLR_BG);
}

void drawStaticElements() {
tft.setTextSize(1);
tft.setTextColor(CLR_LABEL);
tft.setCursor(8, 28);
tft.print("Value 1");
tft.setCursor(8, 62);
tft.print("Value 2");
tft.setCursor(8, 96);
tft.print("Value 3");
tft.drawRect(95, 28, 138, 96, CLR_FRAME);
}

void updateStatus(uint32_t count) {
tft.setTextSize(2);
// Flicker-free refresh using overprint erasing (Text Color, Background Color)
tft.setTextColor(CLR_TAN, CLR_BG);
tft.setCursor(8, 2);
char buf[20];
sprintf(buf, "Status: %-5u", count);
tft.print(buf);
}

void drawSimpleWave() {
int x_start = 97;
int y_mid = 76;
int amp = 30; // 30px swing +/- from center
int step = 15;

for (int i = 0; i <= 110; i += (step * 4)) {
int x0 = x_start + i;
int x1 = x0 + step;
int x2 = x1 + step;
int x3 = x2 + step;
int x4 = x3 + step;

if (x1 < 232) tft.drawLine(x0, y_mid, x1, y_mid - amp, CLR_PLOT);
if (x2 < 232) tft.drawLine(x1, y_mid - amp, x2, y_mid, CLR_PLOT);
if (x3 < 232) tft.drawLine(x2, y_mid, x3, y_mid + amp, CLR_PLOT);
if (x4 < 232) tft.drawLine(x3, y_mid + amp, x4, y_mid, CLR_PLOT);
}
}

void setup() {
initDisplay();
drawStaticElements();

// Data output with 2px vertical spacing from labels
tft.setTextSize(2);
tft.setTextColor(CLR_VALUE, CLR_BG);
tft.setCursor(8, 40);
tft.print("14.8");

tft.setTextColor(CLR_PLOT, CLR_BG);
tft.setCursor(8, 74);
tft.print("18.2");

tft.setTextColor(CLR_RISE, CLR_BG);
tft.setCursor(8, 108);
tft.print("3.4");

drawSimpleWave();
}

void loop() {
updateStatus(status_counter++);
delay(800);
}

/// EOF

 


Note: This board is available from adafruit.com with the display on the top (PID 5483) or the bottom (PID 5493) to accommodate different packaging needs.

Jun 8, 2025

Getting file name and path and compile date, update

Quickly identify which firmware version is running on your Arduino or Teensy by printing the file name, path, and compile timestamp at startup. Call these functions from setup().  (This is an update to an earlier File & Path post.)

It's especially useful when you have a board with code, but you don't know what it is or where to find the source; seeing this at the start is helpful to find it. Here's a typical output:

 Path: C:\Users\you\Documents\Arduino\
 File: Example_Program.ino
 Compiled Jun 04 2025 at 22:31:48
 Version  v1.2.1
 

// Print filename and path from __FILE__
void printFilePath() {
  String fullPath = String(__FILE__);
  int split = fullPath.lastIndexOf("\\") + 1;
  String fileName = fullPath.substring(split);
  String filePath = fullPath.substring(0, split);

  snprintf(outputMsg, sizeof(outputMsg), "  Path: %s", filePath.c_str()); Serial.println(outputMsg);
  snprintf(outputMsg, sizeof(outputMsg), "  File: %s", fileName.c_str()); Serial.println(outputMsg);
}

// Print build date/time and firmware version
void printCompileInfo() {
  snprintf(outputMsg, sizeof(outputMsg), "  Compiled %s at %s", __DATE__, __TIME__); Serial.println(outputMsg);
  snprintf(outputMsg, sizeof(outputMsg), "  Version  %s", VERSION_STRING); Serial.println(outputMsg);
}

// In setup():
printFilePath();
printCompileInfo();

Jun 7, 2025

My Favorite Op Amps, Updated List

Precision Rail-to-Rail Op Amps – Selection Criteria

  • Supply Voltage: ≥ ±5 V (≥10 V total span)
  • Input Offset Voltage (Vos): < 1 mV (≤ 1.1 mV in some entries)
  • Input Bias Current (Ib): < 900 nA
  • Bandwidth (BW): 1.2 MHz to 70 MHz
  • Slew Rate (SR): ≥ 2 V/µs
  • Rail-to-Rail: Required (Input and Output)
  • Configuration: Dual amplifiers
  • Package: 8-SOIC, 8-DIP, MSOP-8

Included Parts

- AD8676ARZ
- LT1498
- AD8606ARZ
- AD823
- OPA2333PA
- LMC6482
- OPA2189
- ADA4522-2
- ADA4805-2
- ADA4082-2
- OPA2376
- AD8542ARZ
- MCP6022-I/SN

Rail-to-Rail Op Amps, Wide Supply Range ≥ 10V

Part #BW (MHz)SR (V/µs)Vos (µV)Ib (nA)Vsupply# AmpsPkg
AD8676ARZ102.5122±5 to ±1828-SOIC
LT1498106.0475650±2.5 to ±1628-SOIC/DIP
AD8231622.08000.025±1.35 to ±1828-SOIC/DIP
OPA2189145.0100.2±2.25 to ±182MSOP-8
ADA4522-2107.02.50.2±2.25 to ±222MSOP-8
LMC64821.51.33000.023 to 1528-SOIC/DIP
ADA4082-24.53.4502003 to 3028-SOIC

Rail-to-Rail Op Amps, Low-Voltage < 10V Range

Part #BW (MHz)SR (V/µs)Vos (µV)Ib (nA)Vsupply# AmpsPkg
AD8606ARZ100.6650.22.7 to 2628-SOIC
OPA2333PA3.00.3100.051.8 to 5.528-DIP/SOIC
ADA4805-21051601250.24.5 to 102MSOP-8
OPA23765.52.550.22.2 to 5.52MSOP-8
AD8542ARZ105.01000.12.7 to 5.528-SOIC
MCP6022-I/SN102.380012.5 to 5.528-SOIC/DIP

Details by Part

AD8676ARZ – 10 MHz, 2.5 V/µs
  • Vos: 12 µV
  • Ib: 2 nA
  • Vsupply: ±5 to ±18 V
  • Package: 8-SOIC
  • # Amps: 2
  • Notes: Ultra precision, fully compliant


Details by Part

AD8676ARZ – 10 MHz, 2.5 V/µs
  • Vos: 12 µV
  • Ib: 2 nA
  • Vsupply: ±5 to ±18
  • Package: 8-SOIC
  • # Amps: 2
  • Notes: Ultra precision, fully compliant
LT1498 – 10 MHz, 6.0 V/µs
  • Vos: 475 µV
  • Ib: 650 nA
  • Vsupply: ±2.5 to ±16
  • Package: 8-SOIC/DIP
  • # Amps: 2
  • Notes: C-Load™, precision, R-R I/O
AD8606ARZ – 10 MHz, 0.6 ✖ V/µs
  • Vos: 65 µV
  • Ib: 0.2 nA
  • Vsupply: 2.7 to 26
  • Package: 8-SOIC
  • # Amps: 2
  • Notes: Low noise, SR limited
AD823 – 16 MHz, 22.0 V/µs
  • Vos: 800 ✖ µV
  • Ib: 0.025 nA
  • Vsupply: ±1.35 to ±18
  • Package: 8-SOIC/DIP
  • # Amps: 2
  • Notes: High SR, offset exceeds spec
OPA2333PA – 3.0 MHz, 0.3 ✖ V/µs
  • Vos: 10 µV
  • Ib: 0.05 nA
  • Vsupply: 1.8 to 5.5
  • Package: 8-DIP/SOIC
  • # Amps: 2
  • Notes: Zero-drift, SR too low
LMC6482 – 1.5 ✖ MHz, 1.3 ✖ V/µs
  • Vos: 300 µV
  • Ib: 0.02 nA
  • Vsupply: 3 to 15
  • Package: 8-SOIC/DIP
  • # Amps: 2
  • Notes: DC-accurate, SR/BW below spec
OPA2189 – 14 MHz, 5.0 V/µs
  • Vos: 10 µV
  • Ib: 0.2 nA
  • Vsupply: ±2.25 to ±18
  • Package: MSOP-8
  • # Amps: 2
  • Notes: Zero-drift, low noise
ADA4522-2 – 10 MHz, 7.0 V/µs
  • Vos: 2.5 µV
  • Ib: 0.2 nA
  • Vsupply: ±2.25 to ±22
  • Package: MSOP-8
  • # Amps: 2
  • Notes: High precision, zero-drift
ADA4805-2 – 105 ✖ MHz, 160 V/µs
  • Vos: 125 µV
  • Ib: 0.2 nA
  • Vsupply: 4.5 to 10
  • Package: MSOP-8
  • # Amps: 2
  • Notes: High-speed, BW exceeds spec
ADA4082-2 – 4.5 MHz, 3.4 V/µs
  • Vos: 50 µV
  • Ib: 200 nA
  • Vsupply: 3 to 30
  • Package: 8-SOIC
  • # Amps: 2
  • Notes: Low-voltage R-R I/O
OPA2376 – 5.5 MHz, 2.5 V/µs
  • Vos: 5 µV
  • Ib: 0.2 nA
  • Vsupply: 2.2 to 5.5
  • Package: MSOP-8
  • # Amps: 2
  • Notes: Zero-drift, audio-grade
AD8542ARZ – 10 MHz, 5.0 V/µs
  • Vos: 100 µV
  • Ib: 0.1 nA
  • Vsupply: 2.7 to 5.5
  • Package: 8-SOIC
  • # Amps: 2
  • Notes: CMOS R-R I/O, low noise
MCP6022-I/SN – 10 MHz, 2.3 V/µs
  • Vos: 800 µV
  • Ib: 1 nA
  • Vsupply: 2.5 to 5.5
  • Package: 8-SOIC/DIP
  • # Amps: 2
  • Notes: Entry-level, meets core criteria

Apr 19, 2023

Eliminate audio pops when bypass-switching

What really causes switch pop

Why a pair of switches is not enough for a clean solution. 
There are two separate problems to be addressed.

Mr. Black explains...
https://www.mrblackpedals.com/blogs/straight-jive/6629778-what-really-causes-switch-pop 

Feb 18, 2023

Add Arm Cortex Boards to Arduino 1.8.x

For boards not in the Boards Manager  

1) go to Preferences to add path of specialty boards (Arduino Feather)

https://learn.adafruit.com/add-boards-arduino-v164/installing-boards

2) Then go to boards manager and search Adafruit, Install SAMxx 32-bit boards (It will take several minutes to load)

instructions:  https://learn.adafruit.com/adafruit-feather-m0-express-designed-for-circuit-python-circuitpython/using-with-arduino-ide