PI Metal Detector Firmware Specification
(STM32F103C8T6 "Blue Pill" + STM32CubeIDE / HAL)
This document defines the hardware map and required functionality for a
working reference firmware. Goal: transfer the TIMING role of a classic
Delta Pulse analog circuit (LM556 + 2x CD4538 + CD4066) to the STM32.
1. MCU AND ENVIRONMENT
- MCU: STM32F103C8T6 (Blue Pill), 64KB Flash / 20KB RAM
- Clock: HSE 8MHz crystal -> PLL x9 -> SYSCLK 72MHz
- Toolchain: STM32CubeIDE + HAL library (NOT Arduino)
- Programming: ST-Link V2 (SWD)
- Display library: U8g2 (csrc, hardware I2C)
2. PIN MAP (FIXED — the hardware is already wired to these pins)
Outputs (GPIO Output, Push-Pull, Speed HIGH)
| Pin | Function | Notes |
|------|--------------|--------------------------------------------------|
| PA8 | TX_GATE | to TC4420 driver input -> IRF740 -> search coil |
| PB8 | D_SC_GATE | CD4066 pin13 (DISC sampling gate) |
| PB9 | SENS_GATE | CD4066 pin12 (SENS sampling gate) |
| PB10 | SAMPLE_GATE | CD4066 pin5,6 (AFC / ground sampling gate) |
*** GATE LOGIC — VERY IMPORTANT (there is a BC547 INVERTER!) ***
- The gate drivers have a BC547 transistor INVERTER in front of them.
- GPIO LOW (RESET) = gate OPEN (the 4066 conducts)
- GPIO HIGH (SET) = gate CLOSED
- AT STARTUP: TX_GATE = LOW (silent), the OTHER 3 GATES = HIGH (closed).
Analog Input
| Pin | Function | Notes |
|------|------------|-------------------------------------------------------|
| PA4 | ADC1_IN4 | signal meter from TL062 output (via 560R series R) |
- NOTE: PA4 is ONLY a display/monitoring meter. The actual detection +
threshold + audio are done in the ANALOG circuit.
- The TL062 stage is INVERTING (gain x10): metal -> LF357 pin6 rises ->
PA4 FALLS.
- With no metal present, PA4 is around 1.5V (set by a trimpot).
- ADC clock: APB2/6 = 12MHz (max 14MHz). Sampling time: 55.5 cycles
(the TL062 is slow / high output impedance).
Buttons (GPIO Input, PULL-UP — buttons pull to GND, active LOW)
| Pin | Function |
|------|----------|
| PB12 | UP |
| PB13 | DOWN |
| PB14 | SELECT |
OLED Display (I2C1)
| Pin | Function |
|------|-----------|
| PB6 | I2C1_SCL |
| PB7 | I2C1_SDA |
- Display: SSD1306 128x64, I2C address 0x3C (8-bit: 0x7
- U8g2 driver: u8g2_Setup_ssd1306_i2c_128x64_noname_f
- *** Use a TIMEOUT on the I2C transmit (e.g. 25ms) — do NOT use
HAL_MAX_DELAY (it will hang the MCU if the display is disconnected). ***
Timer
- TIM2: 1us resolution (PSC=71 -> 1MHz counter). For 100Hz period, ARR=9999.
- TIM2 interrupt (NVIC) must be ENABLED. The timing chain is generated
inside the period-elapsed interrupt.
3. TIMING CHAIN (the digital equivalent of the LM556 + 4538 job)
On each 100Hz period (inside the interrupt), generate PRECISE microsecond
timing by reading the TIM2 counter:
t=0 : TX_GATE = HIGH (energize the coil) <- LM556 pin9 job
t=txPulse : TX_GATE = LOW (flyback/decay begins)
+ SAMPLE_GATE(PB10) OPEN (LOW) <- LM556 pin5 / 4066 5,6
+margin : (12us main-gate settling)
+blank : (skip the flyback — blanking)
: D_SC_GATE(PB
OPEN (LOW) -> read ADC -> CLOSE (HIGH) <- 4538 #1 (DELAY/P1)
+range : (delay after DISC) <- 4538 #2 (RANGE/P2)
: SENS_GATE(PB9) OPEN (LOW) -> wait -> CLOSE (HIGH)
: SAMPLE_GATE(PB10) CLOSE (HIGH) — end of period
Adjustable parameters (defaults shown; changed via the button menu):
- txPulse (TX pulse width): 150us [50–500us]
- blank (blanking): 25us [10–300us]
- early (DISC gate width): 40us [5–200us]
- late (SENS gate width/delay):80us [10–500us]
*** CRITICAL REQUEST — FREQUENCY DITHER (the most important feature!) ***
The original Delta Pulse frequency is NOT fixed — the LM556 naturally
WANDERS continuously between 30–100Hz. This decorrelates mains/EMI noise
(which is BENEFICIAL). The firmware MUST EMULATE this:
- On each period (or every few periods), change the TIM2 ARR randomly so
the frequency wanders between 30–100Hz (e.g. ARR = 10000..33000 random).
- This dither should be switchable ON/OFF (via menu or a flag).
- PURPOSE: prevent a fixed frequency from accumulating noise; increase depth.
4. SIGNAL PROCESSING (PA4 — for the on-screen meter ONLY)
- Read the ADC (PA4) during the DISC gate window.
- At startup, average 256 samples -> baseline (calibration).
- Fast IIR filter (alpha ~ 1/
-> reduce noise.
- signalOut = |filtered − baseline| (POLARITY-INDEPENDENT: take the
ABSOLUTE deviation, because the response direction can flip depending on
the trimpot setting; metal may push PA4 up OR down).
- Slow baseline tracking (alpha ~ 1/4096), BUT FREEZE the baseline while
|signal| is above a threshold (so a target is not nulled out).
- NOTE: The detection decision / audio is in the ANALOG circuit. The
firmware only DISPLAYS signalOut on screen.
5. OLED LAYOUT (128x64)
- Top line: title + a horizontal line beneath it
- Middle (large font): "CAL %xx" during calibration, then "SIG: <value>"
- A horizontal bar graph for SIG (fills as metal approaches)
- Bottom: settings menu — PUL / BLK / ERL / LAT / (DITHER on/off) / BRIGHTNESS
- The selected item shown highlighted (inverted color)
- Refresh the screen at ~20 FPS (50ms), in the MAIN LOOP (NOT in the interrupt).
6. BUTTON BEHAVIOR
- SELECT: move to the next menu parameter (cyclic)
- UP / DOWN: increase/decrease the selected parameter (auto-repeat on hold)
- Debounce: ~25ms
7. ROBUSTNESS / ANTI-LOCKUP RULES (MANDATORY)
1. I2C transmit timeout = 25ms (HAL_MAX_DELAY FORBIDDEN).
2. Any ADC wait INSIDE the interrupt (ISR) must be BOUNDED (no infinite
loop; poll the EOC flag with a limited counter, do not rely on HAL_GetTick).
3. Start the TIM2 interrupt BEFORE initializing the OLED (so TX still runs
even if the display hangs).
4. TIM2_IRQHandler must live in stm32f1xx_it.c (do NOT define it twice in main.c).
5. At startup, gates in a safe state (TX=LOW, the others=HIGH).
8. DELIVERABLES
[ ] SOURCE CODE (main.c + required .h) — NOT a .hex, readable SOURCE
[ ] CubeIDE project (incl. .ioc) or a clear list of the pin settings
[ ] U8g2 integration (how the csrc was added)
[ ] Short explanation: how the dither works, the sampling logic
[ ] (If possible) a test note: what depth/result was achieved
9. THE 3 MOST IMPORTANT THINGS (priority order)
1. Correct TX + 3-gate TIMING (the chain above) — the foundation.
2. FREQUENCY DITHER (30–100Hz wander) — the original's secret, critical for depth.
3. No lockups (I2C timeout + safe ISR) — stable operation.
NOTE: The detection threshold and AUDIO are already handled in the analog
circuit (an NE555). The firmware does NOT generate these. The firmware only
produces TIMING and shows PA4 on the display.
(STM32F103C8T6 "Blue Pill" + STM32CubeIDE / HAL)
This document defines the hardware map and required functionality for a
working reference firmware. Goal: transfer the TIMING role of a classic
Delta Pulse analog circuit (LM556 + 2x CD4538 + CD4066) to the STM32.
1. MCU AND ENVIRONMENT
- MCU: STM32F103C8T6 (Blue Pill), 64KB Flash / 20KB RAM
- Clock: HSE 8MHz crystal -> PLL x9 -> SYSCLK 72MHz
- Toolchain: STM32CubeIDE + HAL library (NOT Arduino)
- Programming: ST-Link V2 (SWD)
- Display library: U8g2 (csrc, hardware I2C)
2. PIN MAP (FIXED — the hardware is already wired to these pins)
Outputs (GPIO Output, Push-Pull, Speed HIGH)
| Pin | Function | Notes |
|------|--------------|--------------------------------------------------|
| PA8 | TX_GATE | to TC4420 driver input -> IRF740 -> search coil |
| PB8 | D_SC_GATE | CD4066 pin13 (DISC sampling gate) |
| PB9 | SENS_GATE | CD4066 pin12 (SENS sampling gate) |
| PB10 | SAMPLE_GATE | CD4066 pin5,6 (AFC / ground sampling gate) |
*** GATE LOGIC — VERY IMPORTANT (there is a BC547 INVERTER!) ***
- The gate drivers have a BC547 transistor INVERTER in front of them.
- GPIO LOW (RESET) = gate OPEN (the 4066 conducts)
- GPIO HIGH (SET) = gate CLOSED
- AT STARTUP: TX_GATE = LOW (silent), the OTHER 3 GATES = HIGH (closed).
Analog Input
| Pin | Function | Notes |
|------|------------|-------------------------------------------------------|
| PA4 | ADC1_IN4 | signal meter from TL062 output (via 560R series R) |
- NOTE: PA4 is ONLY a display/monitoring meter. The actual detection +
threshold + audio are done in the ANALOG circuit.
- The TL062 stage is INVERTING (gain x10): metal -> LF357 pin6 rises ->
PA4 FALLS.
- With no metal present, PA4 is around 1.5V (set by a trimpot).
- ADC clock: APB2/6 = 12MHz (max 14MHz). Sampling time: 55.5 cycles
(the TL062 is slow / high output impedance).
Buttons (GPIO Input, PULL-UP — buttons pull to GND, active LOW)
| Pin | Function |
|------|----------|
| PB12 | UP |
| PB13 | DOWN |
| PB14 | SELECT |
OLED Display (I2C1)
| Pin | Function |
|------|-----------|
| PB6 | I2C1_SCL |
| PB7 | I2C1_SDA |
- Display: SSD1306 128x64, I2C address 0x3C (8-bit: 0x7

- U8g2 driver: u8g2_Setup_ssd1306_i2c_128x64_noname_f
- *** Use a TIMEOUT on the I2C transmit (e.g. 25ms) — do NOT use
HAL_MAX_DELAY (it will hang the MCU if the display is disconnected). ***
Timer
- TIM2: 1us resolution (PSC=71 -> 1MHz counter). For 100Hz period, ARR=9999.
- TIM2 interrupt (NVIC) must be ENABLED. The timing chain is generated
inside the period-elapsed interrupt.
3. TIMING CHAIN (the digital equivalent of the LM556 + 4538 job)
On each 100Hz period (inside the interrupt), generate PRECISE microsecond
timing by reading the TIM2 counter:
t=0 : TX_GATE = HIGH (energize the coil) <- LM556 pin9 job
t=txPulse : TX_GATE = LOW (flyback/decay begins)
+ SAMPLE_GATE(PB10) OPEN (LOW) <- LM556 pin5 / 4066 5,6
+margin : (12us main-gate settling)
+blank : (skip the flyback — blanking)
: D_SC_GATE(PB
OPEN (LOW) -> read ADC -> CLOSE (HIGH) <- 4538 #1 (DELAY/P1)+range : (delay after DISC) <- 4538 #2 (RANGE/P2)
: SENS_GATE(PB9) OPEN (LOW) -> wait -> CLOSE (HIGH)
: SAMPLE_GATE(PB10) CLOSE (HIGH) — end of period
Adjustable parameters (defaults shown; changed via the button menu):
- txPulse (TX pulse width): 150us [50–500us]
- blank (blanking): 25us [10–300us]
- early (DISC gate width): 40us [5–200us]
- late (SENS gate width/delay):80us [10–500us]
*** CRITICAL REQUEST — FREQUENCY DITHER (the most important feature!) ***
The original Delta Pulse frequency is NOT fixed — the LM556 naturally
WANDERS continuously between 30–100Hz. This decorrelates mains/EMI noise
(which is BENEFICIAL). The firmware MUST EMULATE this:
- On each period (or every few periods), change the TIM2 ARR randomly so
the frequency wanders between 30–100Hz (e.g. ARR = 10000..33000 random).
- This dither should be switchable ON/OFF (via menu or a flag).
- PURPOSE: prevent a fixed frequency from accumulating noise; increase depth.
4. SIGNAL PROCESSING (PA4 — for the on-screen meter ONLY)
- Read the ADC (PA4) during the DISC gate window.
- At startup, average 256 samples -> baseline (calibration).
- Fast IIR filter (alpha ~ 1/
-> reduce noise.- signalOut = |filtered − baseline| (POLARITY-INDEPENDENT: take the
ABSOLUTE deviation, because the response direction can flip depending on
the trimpot setting; metal may push PA4 up OR down).
- Slow baseline tracking (alpha ~ 1/4096), BUT FREEZE the baseline while
|signal| is above a threshold (so a target is not nulled out).
- NOTE: The detection decision / audio is in the ANALOG circuit. The
firmware only DISPLAYS signalOut on screen.
5. OLED LAYOUT (128x64)
- Top line: title + a horizontal line beneath it
- Middle (large font): "CAL %xx" during calibration, then "SIG: <value>"
- A horizontal bar graph for SIG (fills as metal approaches)
- Bottom: settings menu — PUL / BLK / ERL / LAT / (DITHER on/off) / BRIGHTNESS
- The selected item shown highlighted (inverted color)
- Refresh the screen at ~20 FPS (50ms), in the MAIN LOOP (NOT in the interrupt).
6. BUTTON BEHAVIOR
- SELECT: move to the next menu parameter (cyclic)
- UP / DOWN: increase/decrease the selected parameter (auto-repeat on hold)
- Debounce: ~25ms
7. ROBUSTNESS / ANTI-LOCKUP RULES (MANDATORY)
1. I2C transmit timeout = 25ms (HAL_MAX_DELAY FORBIDDEN).
2. Any ADC wait INSIDE the interrupt (ISR) must be BOUNDED (no infinite
loop; poll the EOC flag with a limited counter, do not rely on HAL_GetTick).
3. Start the TIM2 interrupt BEFORE initializing the OLED (so TX still runs
even if the display hangs).
4. TIM2_IRQHandler must live in stm32f1xx_it.c (do NOT define it twice in main.c).
5. At startup, gates in a safe state (TX=LOW, the others=HIGH).
8. DELIVERABLES
[ ] SOURCE CODE (main.c + required .h) — NOT a .hex, readable SOURCE
[ ] CubeIDE project (incl. .ioc) or a clear list of the pin settings
[ ] U8g2 integration (how the csrc was added)
[ ] Short explanation: how the dither works, the sampling logic
[ ] (If possible) a test note: what depth/result was achieved
9. THE 3 MOST IMPORTANT THINGS (priority order)
1. Correct TX + 3-gate TIMING (the chain above) — the foundation.
2. FREQUENCY DITHER (30–100Hz wander) — the original's secret, critical for depth.
3. No lockups (I2C timeout + safe ISR) — stable operation.
NOTE: The detection threshold and AUDIO are already handled in the analog
circuit (an NE555). The firmware does NOT generate these. The firmware only
produces TIMING and shows PA4 on the display.


Comment