August 06, 2026
I'll say it again, I love the Commodore community. There are so many awesome people putting out great stuff that pushes these old machines beyond where many thought possible. There are also projects such as bwack's C64 Saver whose purpose is simply to keep these wonderful computers alive. The C64 Saver is a small device that sits inline between the Commodore 64 and the power supply, protecting the 5V rail feeding the Commodore from dangerous overvoltage. The idea is not new, it's been produced in many iterations through the years, but the fact remains that the design of the original Commodore power supplies left them prone to failure and these devices are a nice safety net.
Bwack's solution is modular so you can build a barebones main board that has a set overvoltage cutoff and if you want to extend the functionality you can build the optional addon board that provides screen output via an OLED, current sensing and firmware to support configuration. One thing I really like about bwack's solution is that it uses a MOSFET for switching rather than a slower mechanical relay so a cut can act faster once triggered.
After building and using a C64 Saver with an addon board, I felt there were a few changes I would appreciate. Since bwack was generous enough to make his project open source on GitHub I decided to fork the project and see what I could build into the firmware. This series focuses mainly on that new firmware. The hardware is bwack's design, apart from a capacitor on the addon board and two small changes on the main board that I describe in Part 2.
Heads-up for builders: on my build of the through-hole 2.6 main board, the addon stopped the C64 from reliably powering up until I changed Q1 (BC547A to BC547C) and R5 (3.3k to 100k). My testing suggested this is a hardware issue, not a firmware one. If you are building this, read "Hardware changes: getting the C64 to start with the addon fitted" in Part 2 before building.
Before we go any further: I'm not a programmer. While I have some programming knowledge, it's been decades since I've programmed in C, and I've done even less embedded programming. The closest I've come lately is a few small Arduino projects.
I want to be transparent that the refactoring and new features in my fork were generated with AI assistance. A human programmer may look at the code and see issues, slop or other undesirables, but the beauty of open source is that anyone can take the good parts they want and throw out the rest. To keep myself honest, the behaviors I report as tested were tested on real hardware (a bench supply, an electronic load, a scope and a real C64) and anything else was checked only by simulation and code review (the list is under "How it was tested" in Part 3). What follows is an outline of the changes in the firmware version I've posted to GitHub (my fork). A complete itemized list is in CHANGES.md in the repository.
The primary job of the device is simple: detect a fault and cut the power as quickly as possible. Any delay is time your vintage ICs are absorbing punishing overvoltage.
In bwack's original firmware, protection and the user interface shared one loop. Each cycle read the sensor, drew to the OLED (slow, because it's bit-banged over I2C), and only then updated the output control pin (HOLD_N) at the very end which in turn drives an optocoupler that switches the Q2 MOSFET as needed. A fault spotted in a cycle wasn't acted on until the display had finished drawing. Further, the AC readings were also taken with busy-waits: the CPU sat polling the converter until each conversion finished.
So what did this restructuring include?
Each main-loop tick (about 11 ms) reads the INA219, evaluates the limits and sets the output pin before any display work happens. The display code now lives in ui.c. With the new code, the worst case before an output change should be one tick plus whatever OLED redraw was already in progress. I measured it on the bench with the trip set to 5.1 V: with the input stepped from 5.0 V to 5.3 V, the Q2 gate rose 31 to 36 ms after the input crossed the trip point (eight runs, mean about 34 ms). That is the three-reading confirmation described below, and the output was below 0.4 V within one 100 microsecond sample of the gate moving.
The AC voltage and current conversions now run from an interrupt service routine (ISR(ADC_vect)). The ADC works away in the background, alternating between the two channels, and the main loop never waits on it. The voltage window is 200 samples (about 42 ms), which still covers more than two full 60 Hz cycles. The current window is 256 samples (about 53 ms), long enough for the true RMS calculation described in Part 2. The original gathered 3,000 samples per update.
The push button shares the AC voltage sense pin (a reading below 100 counts means pressed). The original firmware only looked at the button once every four cycles. The button is now checked on every tick, with a three-sample debounce.
The ATtiny85 is an 8-bit chip, so reading a 16-bit value takes more than one instruction. If the ISR updates a value halfway through a read, you get half old and half new data. The UI now copies the shared values inside an ATOMIC_BLOCK, with interrupts briefly masked, so it always sees a consistent set.
A 250 ms hardware watchdog resets the chip if the main loop ever stops running. After a reset, the firmware turns the output off first and keeps it off until the INA219 answers.
With the basic restructuring done, we can improve the robustness against a few wrenches that can get thrown into the mix. An embedded device protecting sensitive retro hardware needs to be paranoid: if something fails internally, the default state should always be "fail-safe." The table compares bwack's original firmware and how the fork attempts to handle the issue.
INA219 disconnected, or the I2C bus is dead
Reads come back as 0xFFFF. The protection logic sees about 32.8 V and 65 A, so it tripped, but by accident rather than design. The screen showed nonsense such as -3.276V and 0.0-1A. Sensor ACKs were never checked.
ACKs are checked. After about 0.5 s of bad reads a SENSOR fault latches and the output is cut. It clears itself once readings are good again.
INA219 loses its calibration (static, brown-out)
Config and calibration were rewritten on every voltage read. That hid the problem, but restarted the sensor's conversion each cycle.
The calibration register is read back every tick and rewritten only if it doesn't match. A mismatch counts as a bad reading.
Slow-starting power at boot (sensor powers up later than the MCU)
Output switched on straight away at start-up, and protection began a couple of hundred milliseconds later. A dead sensor read as a fault, latched in HOLD mode.
The output stays off until the sensor has answered once, and SENSOR faults clear themselves.
MCU hang or glitch
No watchdog.
250 ms watchdog. During a reset, the output driver floats for a few milliseconds then the firmware cuts the output and holds it off until the sensor answers.
Wrong clock fuse (CKDIV8 programmed)
Every timing constant silently 8 times too slow.
The build refuses to compile unless F_CPU is 8 MHz. At start-up the chip reads its own fuse and, if CKDIV8 is set, halts with the output off and shows CLOCK ERROR.
Corrupt or blank EEPROM
Any stored value below 255 was accepted.
Every setting is range-checked on load, and out-of-range values fall back to the default.
Overvoltage
Trips on the first reading at or above the trip point (default 5.5 V).
Trips after three consecutive readings at or above the trip point (about 31 to 36 ms measured; default 5.4 V, adjustable 5.1 to 5.5 V). The board's hardware cut-off remains the fast backstop. The reason for three readings is explained under "Start-up and fault recovery".
Overcurrent
One reading over the limit trips. The INA219 was set to a 160 mV range (clips near 1.6 A) with 128-sample averaging (about 68 ms). Overcurrent always auto-retried every 5s, forever, even in HOLD mode.
Must stay over the limit for about 340 ms (a leaky counter). The INA219 uses a 320 mV range (3.2 A) with single 12-bit conversions. HOLD latches the fault; AUTO retries a limited number of times, then latches.
A fault cleared by accident (an AC sag looks like a button press, because the button shares the AC sense pin)
Any long press cleared faults instantly.
Clearing a latched fault needs a deliberate, checked hold of the button, and the screen says why if it refuses (details under "Start-up and fault recovery").
Voltage sag
Nothing.
LOW V flashes when the 5V rail drops below an adjustable threshold (4.0 to 4.9 V, default 4.6 V). It's a warning only.
Once the microcontroller is running, the output now starts off and only switches on once the INA219 has answered a read.
IMPORTANT: When the microcontroller isn't running, for example with no 9 V AC or for a few milliseconds around a reset, the board passes 5 V straight through and only the hardware cut-off protects it. This comes from the hardware, so it is just as true of the original firmware. See the known limitations in Part 3. In the original, the output was switched on during initialization, so the C64 was powered for a couple of hundred milliseconds before anything was being checked.
Fault handling also changed, because the original's recovery was easy to trigger by accident and had no limits.
HOLD or AUTO now applies to over-voltage and over-current. In the original, the setting only governed overvoltage, and overcurrent always retried itself every few seconds. In HOLD mode a fault now stays latched until you clear it. In AUTO mode an overvoltage clears once the voltage is 0.1 V below the trip point and the fault has been latched for at least 2 seconds. Overcurrent retries up to three times, about 5 to 6 seconds apart, then latches until you clear it by hand. The retry allowance refills after about a minute of healthy running.
SENSOR faults always clear themselves, in both modes, after about a second of good sensor readings. The INA219 is powered from the DC rail, so switching the supply off looks exactly like a dead sensor. Having to press a button every time you power-cycle the supply would be a nuisance, and it is safe because the output stays off for the whole outage.
Clearing a fault by hand is guarded. Hold the button for about a second on the main screen. The firmware refuses unless the fault has been latched for at least 2 seconds, the sensor is reading properly and the voltage is below the trip point. If it refuses, the screen tells you why for two seconds: "Wait 2 seconds", "Sensor not OK" or "Voltage too high".
That last guard exists because the push button shares the AC voltage sense pin, which is how bwack's board is wired. A deep enough AC sag looks to the firmware like a button press, so a single press is never enough to release a latched fault.
Why the trip delays are what they are. Over-voltage trips only after three consecutive readings at or above the trip point (31 to 36 ms measured). Requiring three readings stops the sub-millisecond spike when the C64 is switched off from latching a false fault; anything faster is the job of the hardware cut-off. Over-current needs about 340 ms of sustained overload, using a leaky counter so brief spikes are forgiven.
The INA219 is a good sensor, but the original code assumed every read succeeded. Three changes are worth explaining.
The original called initINA() inside every bus-voltage read, so the chip was being reconfigured over I2C up to about a hundred times a second. The fork configures it once, then reads, and re-initializes it only after a bad read. It also checks the link by reading the calibration register back (masked with 0xFFFE, since the lowest bit is not stored). If the read-back is wrong, the sensor is treated as missing or confused.
An I2C bus with nothing answering reads back as all ones. In the original, the protection logic sees that as about 32.8 V and 65 A, and the screen shows nonsense such as -3.276V and 0.0-1A (the display code prints the values as signed numbers), but it is still "a reading". In the fork a bad read-back counts against the sensor. After 50 bad ticks (about half a second) a SENSOR fault latches and the output is cut. It clears on its own after 100 good ticks, in either recovery mode, because a dead sensor is not something a person needs to acknowledge.
The original configuration (0x31FF, PGA /4, 160 mV full scale) clips at roughly 1.6 A, with 128-sample averaging that takes about 68ms per reading. The averaging is useful for a steady display but slow for protection, so the over-current debounce in the fork is built on ticks, not on sensor averaging. See "Why the trip delays are what they are" under "Start-up and fault recovery" for how it behaves. The fork uses 0x399F, which is also the INA219's power-on default: PGA /8 (320 mV full scale, about 3.2 A on the 0.1 ohm shunt) and a single 12-bit conversion with no averaging, so a reading is never stale.
The INA219 runs from the 5 V rail, but the microcontroller runs from the 9 V AC rail. A vintage linear supply brings both up within a few milliseconds. A modern switching supply soft-starts, and on mine the 5 V rail came up about 120 ms after the 9 V rail, which I measured with a two-channel oscilloscope. Firmware that configures the sensor once at start-up can therefore write its calibration while the sensor has no power and misses the message. The sensor then wakes at its reset default of calibration 0 and reports zero current, while the voltage reading, which doesn't depend on calibration, still looks fine. The display shows 0.00A on the 5V line with a real load attached, and it stays that way until the next power cycle. The original firmware avoided this by accident, because it reconfigured the sensor on every read. The fork configures it once, but checks the calibration by read-back and re-initializes after a bad read.
The lesson I took from it is that a current reading of exactly zero from a load that should be drawing power is a symptom worth distrusting. If you see 0.00A on the 5V line with a board attached, suspect the sensor's configuration or its power sequencing before blaming the load.
Next, in Part 2: the hardware change that lets the C64 start with the addon fitted, making the AC readings make sense, and fitting it all into 8 KB.