Why This Post Exists
I wanted to learn practical fault injection on low-cost MCUs like WCH CH32V003 and Puya PY32.

The usual recommendation is ChipWhisperer, and yes, it is excellent. But in my reality, it is also expensive and often hard to source (sometimes effectively unobtanium in my region).
Pico Glitcher V2 (now V3) was the lifesaver.

Affordable, available, hackable, and excellent enough to do real work.
Scope and Ethics
This work is done only on my own hardware, my own firmware, and my own test boards.
Goal: Understand attack surfaces and improve embedded security.
Setup
My baseline lab stack:
Pico Glitcherfor fault injection timing and pulse generation- Targets: Bare CH32V003 SOP-8 and Puya PY32 SOP-8 chips for easy glitching!
- GPIO trigger from firmware checkpoint
I started with a boring, reproducible target firmware:
- Boot
- Password check
- Branch to
successorfail
#include <stdint.h>
#include "ch32fun.h"
/*
* CH32V003 (SOP-8) glitch-target demo for ch32fun.
*
* Default pins below are examples; change to pins actually broken out
* on your SOP-8 board/wiring.
*
* PIN_TRIGGER: pulse before sensitive compare window
* PIN_SUCCESS: high when glitch forces success path
* PIN_FAIL: high on normal fail path
*/
#define PIN_TRIGGER PC1
#define PIN_SUCCESS PC2
#define PIN_FAIL PC4
static inline void delay_cycles(volatile uint32_t n) {
while (n--) {
__asm__ volatile("nop");
}
}
static void gpio_init_simple(void) {
funGpioInitAll();
funPinMode(PIN_TRIGGER, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP);
funPinMode(PIN_SUCCESS, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP);
funPinMode(PIN_FAIL, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP);
funDigitalWrite(PIN_TRIGGER, FUN_LOW);
funDigitalWrite(PIN_SUCCESS, FUN_LOW);
funDigitalWrite(PIN_FAIL, FUN_LOW);
}
/*
* Intentionally glitchable control-flow check (demo only).
* Keep this in lab firmware only.
*/
static int auth_check(uint32_t input) {
volatile uint32_t secret = 0xA55A1234u;
volatile uint32_t s1 = 0;
volatile uint32_t s2 = 0;
/* Trigger marker for PicoGlitcher/scope timing anchor */
funDigitalWrite(PIN_TRIGGER, FUN_HIGH);
delay_cycles(200);
funDigitalWrite(PIN_TRIGGER, FUN_LOW);
if ((input ^ 0x11111111u) == (secret ^ 0x11111111u)) {
s1 = 1;
}
delay_cycles(700);
if ((input + 7u) == (secret + 7u)) {
s2 = 1;
}
return (s1 && s2) ? 1 : 0;
}
int main(void) {
SystemInit();
gpio_init_simple();
/* Wrong on purpose; glitches try to flip branch outcome. */
volatile uint32_t guess = 0xDEADBEEFu;
while (1) {
int ok = auth_check(guess);
if (ok) {
funDigitalWrite(PIN_SUCCESS, FUN_HIGH);
funDigitalWrite(PIN_FAIL, FUN_LOW);
delay_cycles(220000);
funDigitalWrite(PIN_SUCCESS, FUN_LOW);
} else {
funDigitalWrite(PIN_FAIL, FUN_HIGH);
funDigitalWrite(PIN_SUCCESS, FUN_LOW);
delay_cycles(60000);
funDigitalWrite(PIN_FAIL, FUN_LOW);
}
/* Inter-attempt gap for clean captures */
delay_cycles(250000);
}
}
$ git diff
diff --git a/examples/blink/Makefile b/examples/blink/Makefile
index 7107cf3..ecb622e 100644
--- a/examples/blink/Makefile
+++ b/examples/blink/Makefile
@@ -3,9 +3,10 @@ all : flash
TARGET:=blink
TARGET_MCU?=CH32V003
+# Bare CH32V003 without external crystal: force internal HSI clock source.
+EXTRA_CFLAGS += -DFUNCONF_USE_HSI=1 -DFUNCONF_USE_HSE=0 -DFUNCONF_HSE_BYPASS=0
...
$ pwd
~/repos/ch32fun/examples/blink
This made it easy to detect whether a glitch changed control flow.
Methodology (What Actually Matters)
Voltage glitching is mostly about discipline, not magic.
I swept three core parameters:
- Glitch offset (when to inject)
- Glitch width (how long)
- Glitch amplitude/depth (how hard)
For each parameter set, I ran repeated trials and labeled outcomes:
- Normal boot
- Reset/hang
- Faulted behavior (interesting)
- False positive
Then I plotted success-rate heatmaps. Without data, glitching turns into superstition.
$ python3 glitch_sweep_ch32_gpio.py --trials 3200000 --status-every-s 1 --status-every-trials 100
[+] Starting sweep script...
[+] Connecting to Pico Glitcher on /dev/ttyACM0...[+] Version of Pico Glitcher: [1, 13, 1]
[+] Version of findus: [1, 13, 1]
Done.
[+] Initializing database (resume=False)... Done. (Database: glitch_sweep_ch32_gpio.py_20260309_123807.sqlite [Fast Mode])
[i] Approx upper-bound per trial: 0.140s (one coordinate worst-case: 124h26m40s)
[+] Opening CSV for results: ch32v003_glitch_sweep.csv
[*] Testing offset= 1 width= 20 (3200000 trials)
[.] 10/3200000 ( 0.00%) rate= 9.3/s eta=95h40m53s ok=0 fail=10 reset=0 timeout=0 noTrig=0 commErr=0
[.] 19/3200000 ( 0.00%) rate= 8.9/s eta=99h42m30s ok=0 fail=19 reset=0 timeout=0 noTrig=0 commErr=0
[.] 28/3200000 ( 0.00%) rate= 8.8/s eta=101h08m25s ok=0 fail=28 reset=0 timeout=0 noTrig=0 commErr=0
[.] 37/3200000 ( 0.00%) rate= 8.7/s eta=101h52m44s ok=0 fail=37 reset=0 timeout=0 noTrig=0 commErr=0
[.] 46/3200000 ( 0.00%) rate= 8.7/s eta=102h19m28s ok=0 fail=46 reset=0 timeout=0 noTrig=0 commErr=0
[.] 55/3200000 ( 0.00%) rate= 8.7/s eta=102h37m43s ok=0 fail=55 reset=0 timeout=0 noTrig=0 commErr=0
[.] 64/3200000 ( 0.00%) rate= 8.6/s eta=102h50m42s ok=0 fail=64 reset=0 timeout=0 noTrig=0 commErr=0
[.] 73/3200000 ( 0.00%) rate= 8.6/s eta=103h00m27s ok=0 fail=73 reset=0 timeout=0 noTrig=0 commErr=0
[.] 82/3200000 ( 0.00%) rate= 8.6/s eta=103h08m03s ok=0 fail=82 reset=0 timeout=0 noTrig=0 commErr=0
[.] 91/3200000 ( 0.00%) rate= 8.6/s eta=103h14m26s ok=0 fail=91 reset=0 timeout=0 noTrig=0 commErr=0
[.] 100/3200000 ( 0.00%) rate= 8.6/s eta=103h19m06s ok=0 fail=100 reset=0 timeout=0 noTrig=0 commErr=0
[.] 109/3200000 ( 0.00%) rate= 8.6/s eta=103h23m19s ok=0 fail=109 reset=0 timeout=0 noTrig=0 commErr=0
[.] 118/3200000 ( 0.00%) rate= 8.6/s eta=103h26m54s ok=0 fail=118 reset=0 timeout=0 noTrig=0 commErr=0
[.] 127/3200000 ( 0.00%) rate= 8.6/s eta=103h29m58s ok=0 fail=127 reset=0 timeout=0 noTrig=0 commErr=0
[.] 136/3200000 ( 0.00%) rate= 8.6/s eta=103h32m35s ok=0 fail=136 reset=0 timeout=0 noTrig=0 commErr=0
[.] 145/3200000 ( 0.00%) rate= 8.6/s eta=103h35m50s ok=0 fail=145 reset=0 timeout=0 noTrig=0 commErr=0
[.] 154/3200000 ( 0.00%) rate= 8.6/s eta=103h36m59s ok=0 fail=154 reset=0 timeout=0 noTrig=0 commErr=0
...
Why Pico Glitcher Helped So Much
What made Pico Glitcher practical for me:
- Low cost, so I could actually buy and use it
- Simple scripting and rapid iteration
- Easy integration with a budget bench setup
- "Good enough" timing control to find real fault windows
For learning + meaningful MCU fault research, it absolutely delivers.
Early Results
On both CH32V003 and Puya PY32, I observed narrow timing windows where behavior changed under controlled glitches. Most attempts still fail (as expected), but that is normal for real glitch work.
The key lesson: repeatability > lucky hits.
Cost/Access Reality Check
Security tooling should not be gated behind high prices and supply-chain luck. Open and affordable tools are critical for independent researchers.
For me, Pico Glitcher converted "I should learn this someday" into "I can test this today."
What's Next
On to "bigger" targets next ;)
Breadboard Glitching HW
References
https://fault-injection-library.readthedocs.io/en/latest/getting_started/
https://github.com/cnlohr/ch32fun (awesome stack for 'ch32')
https://github.com/IOsetting/py32f0-template (the best 'sdk' for PY32)