2025-10-09 9:55 PM
Context
Custom 3D-printer controller that I'm developing from scratch, currently on rev7. Rev6 (very similar) runs the same blinky fine. On rev7 I can connect/erase/flash/verify over SWD, but code doesn’t run—GPIOs stay at ~0 V.
MCU / Tooling
MCU: STM32F446ZET6 (no bootloader)
Programmer: ST-Link V2, STM32CubeProgrammer (SWD)
Build: STM32Duino / Generic F446ZETx (144-pin), USB support = None, Clock = HAL, Debug = Serial Wire
Test firmware: blinker for the LEDs on PA8 and PA15
Symptoms
Flash + verify at 0x08000000 succeed.
Vector table @0x08000000 looks sane (SP=0x200xxxxx, PC=0x0800xxxx|1).
After reset, PA8 and PA15 don’t toggle (measured at MCU pins). PA15 blips only when debugger attaches.
Same binary works on rev6.
Measured the following:
VDD = 3.3 V on all banks (decoupled).
VDDA = 3.3 V, local 100 n + 1 µF.
VCAP1/VCAP2 ≈ 1.3 V each (2.2 µF caps fitted).
NRST ≈ 3.3 V idle, pulses on connect.
BOOT0 pulled low (confirmed low during reset).
What I tried
Full erase, re-flash minimal register blinker
Rebuilt with USB=None, HSI only.
Tried lower BOR level.
JTAG vs SWD: using Serial Wire to free PA15 (still no PA8 toggle).
Attachments
Schematic fragment: MCU core
Board photo around MCU
Would anyone know what could be wrong here? I've not encountered such an issue before where I could connect, interact, and flash the chip, but not be able to execute the sketch I flashed.
Any help would be greatly appreciated.
Thanks!
#include <Arduino.h>
#include <Servo.h>
// Pins
const uint32_t LED1 = PA8, LED2 = PA15; // PA15 PA12
const uint32_t BED = PD7, E0 = PD8, E1 = PD9;
const uint32_t BLT_SERVO_PIN = PD6;    // orange
const uint32_t BLT_PROBE_PIN = PC12;   // white (open-drain)
HardwareSerial Serial2(PA9, PA10);  // (rx, tx)
#define LED_ACTIVE_HIGH 1
#define HEATER_ACTIVE_HIGH 1
const bool ENABLE_HEATER_DEMO = false;
const unsigned long LED_PERIOD_MS    = 500;
const unsigned long HEATER_PERIOD_MS = 5000;
const unsigned long PROBE_CONFIRM_MS = 2;    // LOW must be stable this long
const unsigned long SERVO_MASK_MS    = 300;  // ignore triggers after servo move
unsigned long t_led=0, t_heater=0;
bool led_state1=false, led_state2=true;
bool bed_state=false, e0_state=false, e1_state=false;
Servo blt;
volatile bool edgeSeen=false;
volatile unsigned long edgeAt=0;
unsigned long mask_until=0;
bool last_reported_low=false;
inline void setPin(uint32_t pin, bool on, bool ah){ digitalWrite(pin,(on ^ !ah)?HIGH:LOW); }
void mask_after_servo(){ mask_until = millis() + SERVO_MASK_MS; }
void blt_deploy(){ blt.write(10);  mask_after_servo(); }
void blt_stow(){   blt.write(90);  mask_after_servo(); }
void blt_self(){   blt.write(160); mask_after_servo(); }
void blt_reset(){  blt.write(120); mask_after_servo(); }
void blt_cycle(uint16_t dep=800,uint16_t stw=800){ blt_deploy(); delay(dep); blt_stow(); delay(stw); }
// ISR (plain STM32)
void probeISR(){ edgeSeen=true; edgeAt=millis(); }
void setup() {
  pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT);
  pinMode(BED, OUTPUT);  pinMode(E0, OUTPUT); pinMode(E1, OUTPUT);
  setPin(LED1,false,LED_ACTIVE_HIGH); setPin(LED2,false,LED_ACTIVE_HIGH);
  setPin(BED,false,HEATER_ACTIVE_HIGH); setPin(E0,false,HEATER_ACTIVE_HIGH); setPin(E1,false,HEATER_ACTIVE_HIGH);
  // Prefer external pull-up 4.7k–10k to 3.3V + 0.01–0.1uF to GND on PC12
  pinMode(BLT_PROBE_PIN, INPUT); // if you lack external PU, use INPUT_PULLUP and FALLING
  attachInterrupt(digitalPinToInterrupt(BLT_PROBE_PIN), probeISR, CHANGE);
  blt.attach(BLT_SERVO_PIN);
  blt_stow();
  Serial2.begin(115200);
  delay(50);
  Serial2.println("\n[BLTouch Test + Debounce @ USART2]");
  Serial2.println("d=deploy s=stow t=self r=reset c=cycle p=probe h=help");
  t_led = t_heater = millis();
}
void loop() {
  const unsigned long now = millis();
  if (now - t_led >= LED_PERIOD_MS) {
    t_led += LED_PERIOD_MS;
    led_state1=!led_state1; led_state2=!led_state2;
    setPin(LED1, led_state1, LED_ACTIVE_HIGH);
    setPin(LED2, led_state2, LED_ACTIVE_HIGH);
  }
  if (ENABLE_HEATER_DEMO && (now - t_heater >= HEATER_PERIOD_MS)) {
    t_heater += HEATER_PERIOD_MS;
    bed_state=!bed_state; e0_state=!e0_state; e1_state=!e1_state;
    setPin(BED, bed_state, HEATER_ACTIVE_HIGH);
    setPin(E0,  e0_state,  HEATER_ACTIVE_HIGH);
    setPin(E1,  e1_state,  HEATER_ACTIVE_HIGH);
  }
  // Debounced probe read
  bool raw = digitalRead(BLT_PROBE_PIN);           // HIGH idle, LOW trigger
  static unsigned long low_since = 0;
  if (!raw) { if (!low_since) low_since = now; }
  else low_since = 0;
  bool debounced_low = (low_since && (now - low_since >= PROBE_CONFIRM_MS));
  setPin(LED1, debounced_low, LED_ACTIVE_HIGH);
  if (edgeSeen) edgeSeen=false;
  if (now >= mask_until) {
    if (debounced_low && !last_reported_low) {
      last_reported_low = true;
      Serial2.println("[BLT] TRIGGER (debounced)");
    } else if (!debounced_low && last_reported_low) {
      last_reported_low = false;
      Serial2.println("[BLT] RELEASE");
    }
  }
  if (Serial2.available()) {
    switch ((char)Serial2.read()) {
      case 'd': blt_deploy();      Serial2.println("[BLT] Deploy"); break;
      case 's': blt_stow();        Serial2.println("[BLT] Stow"); break;
      case 't': blt_self();        Serial2.println("[BLT] Self-test"); break;
      case 'r': blt_reset();       Serial2.println("[BLT] Reset"); break;
      case 'c': Serial2.println("[BLT] Cycle"); blt_cycle(); break;
      case 'p': Serial2.println(raw ? "[BLT] HIGH (idle)" : "[BLT] LOW (TRIGGER)"); break;
      case 'h': default: Serial2.println("d s t r c p h"); break;
    }
  }
}
2025-10-10 10:13 AM
So to reiterate and to give further progress, my code never runs (pins stay low). PA15 toggles when CubeProgrammer connects (so GPIO/clock isn’t totally dead).
Initially I hit UsageFault, now I consistently land in WWDG_IRQHandler very early.
What I verified:
Vector table @ 0x08000000 looks sane:
SP = 0x20020000, Reset = 0x08007305 (Thumb bit set).
VTOR reads 0x08000000 when halted.
CFSR/HFSR = 0 when halted, DFSR = 0xE000EDF8.
Fault PC (from stacked frame) previously pointed into HAL_RCC_GetSysClockFreq; after fixing duplicate-symbol link error and reflashing, the PC now goes to WWDG_IRQHandler() while HAL_InitTick() calls HAL_NVIC_SetPriority().
What I tried:
Connect under reset, halt, then try to mask interrupts and stop SysTick.
Writes to NVIC ICER sometimes don’t “stick” unless I halt under reset immediately before editing:
NVIC_ICER0 @ 0xE000E180 = 0xFFFFFFFF
NVIC_ICER1 @ 0xE000E184 = 0xFFFFFFFF
SysTick->CTRL @ 0xE000E010 = 0x00000000
If I manage to apply those, the core can step a bit longer; otherwise it immediately re-enters WWDG_IRQHandler.
Option bytes are default (RDP AA, BOR 3, WDG_SW checked).
Hypotheses I’d love feedback on:
Spurious WWDG IRQ at startup: IRQ0 is firing before SystemClock_Config/HAL_Init finish. Why would WWDG be enabled? (I didn’t enable it.) Is something in my startup enabling APB1/WWDG or setting its IRQ pending?
Vector/VTOR glitch: could VTOR be wrong at runtime (HotPlug reads look fine but code sees a different VTOR)? If VTOR were off by 0x100 or similar, an unrelated interrupt could vector to WWDG.
Clock bring-up (I use 8 MHz HSE): if HSE/PLL isn’t ready and HAL_InitTick/clock code goes sideways, could that leave WWDG pending? (Note: even on HSI I see the same WWDG hit.)
Toolchain/Arduino core: I did hit a duplicate-definition of HAL_RCC_GetSysClockFreq (fixed). Could there still be a startup mismatch (wrong variant, wrong vectors)?
Specific asks:
On F446, besides clearing NVIC->ICER[0] bit 0, is there any other place WWDG can be enabled at reset (RCC APB1 ENR, EWI legacy, etc.) that would explain an immediate IRQ?
Any known issues with Arduino STM32 core 2.11.0 on F446ZEx where vectors/VTOR don’t point to flash by default?
Recommended minimal no-HAL startup/blink that masks interrupts and blinks PC13, just to prove the board is OK?
Why I think it’s not hardware: ST-LINK can read/write flash/RAM, VCAPs and VDDA are correct, and an older board rev with the same MCU and almost identical wiring runs the same sketch. This smells like startup/interrupt/vector configuration, not power or pin wiring.
Any pointers appreciated! Happy to post full map/ELF, option bytes, and register dumps.
2025-10-10 10:27 AM
Probably not the watchdog, but multiple vectors folded on to that one handler
Check VECTACTIVE field in SCB->ICSR, to see which IRQ Handler fired.
Or use bisection in startup.s to sieve out which source fired.
Probably a TIM or SYSTICK
2025-10-10 10:53 AM
As Tesla writes, how you get info, that is WWDG fired ??? I mean you selfreply, new rev have hw issue = your code result in hardfault (masked as while ISR used many time.)
2025-10-10 11:19 AM - edited 2025-10-10 11:21 AM
You were right, it wasn’t the watchdog firing. I checked SCB->ICSR as suggested:
ICSR = 0x00000803 → VECTACTIVE = 3, so the core was actually in HardFault.
HFSR = 0x40000000 (FORCED),
CFSR = 0x00000400 → BusFault, IMPRECISERR (imprecise bus fault).
Dumping the stacked frame put the faulted PC inside HAL RCC code:
HAL_RCCEx_PeriphCLKConfig and earlier I’d also seen HAL_RCC_GetSysClockFreq.
So “multiple vectors folded onto WWDG” was not the root-cause, the live vector was HardFault.
What changed things:
I halted the core, disabled SysTick/IRQs, and manually poked RCC/GPIO:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER   = (GPIOA->MODER & ~(3u<<(8*2))) | (1u<<(8*2));
GPIOA->ODR    ^= (1u<<8);LED came on immediately. Then I ran a minimal bare-metal HSI blink on PA8/PA15 (no HAL, no USB, no PLL) and it works. That proves power, SWD, and GPIO wiring are fine. the crash is higher up in clock/USB init.
Likely root cause: my Arduino/ST core build was pulling in -DUSBCON -DUSE_USB_HS -DUSE_USB_HS_IN_FS. HAL’s RCC/USB init assumes a valid HSE/PLL path. On this new rev the HSE likely isn’t starting , so HAL touches clocks and trips an imprecise bus fault? → FORCED HardFault.
Appreciate the nudge to look at ICSR, that made it clear it wasn’t WWDG but a FORCED HardFault from clock init. If you see anything I’m missing on the RCC side (F446Z, USB HS-in-FS flags), I’m all ears. Because I'm not sure where to go from there, and what to check. I doubt Marlin would work given the hardfault
#include "stm32f4xx.h"
static void delay_cycles(volatile uint32_t n) { while (n--) __NOP(); }
int main(void)
{
    /* Enable GPIOA clock */
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    (void)RCC->AHB1ENR; // ensure clock is live before GPIO write
    /* PA8, PA15 as push-pull outputs, high speed, no pull */
    // MODER: set to 01 (output)
    GPIOA->MODER   &= ~((3u << (8*2)) | (3u << (15*2)));
    GPIOA->MODER   |=  ((1u << (8*2)) | (1u << (15*2)));
    // OTYPER: 0 = push-pull
    GPIOA->OTYPER  &= ~((1u << 8) | (1u << 15));
    // OSPEEDR: 11 = high speed
    GPIOA->OSPEEDR |=  ((3u << (8*2)) | (3u << (15*2)));
    // PUPDR: 00 = no pull
    GPIOA->PUPDR   &= ~((3u << (8*2)) | (3u << (15*2)));
    // Optional: start states (PA8 off, PA15 on)
    GPIOA->BSRR = (1u << (15)) | (1u << (8+16)); // set PA15, reset PA8
    for (;;)
    {
        // Toggle PA8 and PA15 opposite
        GPIOA->ODR ^= (1u << 8) | (1u << 15);
        delay_cycles(800000);  // ~50 ms @ HSI ~16 MHz (tune as you like)
    }
}
2025-10-10 12:23 PM
Imprecise just means its a write to an undecoded address, the write buffers defer the action, so the code has moved on and the fault points to some subsequent code.
Look at the write instruction immediately preceding the fault. Show a disassembly of that code and the MCU registers.
The code shown looks innoxious enough, and the VCAP caps/voltage look ok.
Perhaps look at the FLASH wait states if the MCU is running faster than 27 MHz. Prefetch or ART settings.
