Root cause found + workaround
After systematic debugging, I found the root cause. It has nothing to do with the I2C peripheral itself or the power rails — it’s in the Cygnet’s SystemClock_Config inside variant_CYGNET.cpp.
What’s happening
The variant’s SystemClock_Config calls HAL_RCCEx_PeriphCLKConfig with RCC_PERIPHCLK_USB as part of its peripheral clock setup:
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USB | RCC_PERIPHCLK_SDMMC1 | RCC_PERIPHCLK_ADC;
PeriphClkInit.UsbClockSelection = RCC_USBCLKSOURCE_MSI;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) {
Error_Handler();
}
Internally, HAL_RCCEx_PeriphCLKConfig waits for the USB clock source (MSI) to be considered stable before completing. The HAL treats MSI as stable for USB purposes only after it has gone through a PLL lock cycle. The Cygnet variant runs SYSCLK directly from MSI at 48 MHz with no PLL (RCC_PLL_NONE), so that lock cycle never happens. Without USB connected there is nothing else to satisfy the condition, so the MCU hangs indefinitely inside the HAL. The watchdog then fires, resets the MCU, and on the second attempt the I2C peripheral is left in a corrupted state from the aborted init — causing the permanent hang.
When USB is connected, the USB enumeration process satisfies the HAL’s internal stability condition as a side effect, which is why everything works fine on USB power.
Why the Swan is not affected
Comparing variant_SWAN_R5.cpp, the Swan’s SystemClock_Config enables the PLL and uses it as SYSCLK source:
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_MSI;
...
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
When HAL_RCC_OscConfig activates the PLL, the HAL internally waits for PLLRDY (PLL lock). That lock process puts MSI into a state where HAL_RCCEx_PeriphCLKConfig finds the USB clock condition already satisfied — so the call returns immediately, with or without USB connected.
In short: the Swan’s PLL accidentally solves the USB clock problem as a side effect of its clock setup. The Cygnet has no PLL, so there is no side effect to save it.
Connection to the 80 MHz spec
The STM32L433 is capable of running at up to 80 MHz via its internal PLL — which is what Blues references in the datasheet and product page. However, the current variant_CYGNET.cpp does not enable the PLL; instead it runs SYSCLK directly from the MSI oscillator at 48 MHz:
RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11; // 48 MHz
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
...
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_MSI;
This is directly relevant to the bug. As explained above, the HAL only considers MSI stable enough for the USB peripheral clock after it has gone through a PLL lock cycle. Because the current variant skips the PLL entirely, that condition is never met at boot without USB connected — and the MCU hangs.
If the variant were updated to enable the PLL and run at 80 MHz, the USB clock hang would disappear as a side effect, with no need for the sketch-level workaround. That would be the cleanest long-term fix.
Additional confirming symptom
When the Cygnet is in the hung state (LED off, no USB), plugging in the USB cable causes execution to jump directly into loop() and resume normally. That is the USB enumeration satisfying the HAL’s clock condition mid-execution, unblocking the wait that started at boot.
Workaround (tested and working)
Override SystemClock_Config directly in your sketch. Because the variant defines it as WEAK, your definition takes precedence. The fix omits the USB peripheral clock block entirely and keeps only what’s needed for battery operation:
extern "C" void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {};
if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) {
Error_Handler();
}
HAL_PWR_EnableBkUpAccess();
__HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE
| RCC_OSCILLATORTYPE_MSI
| RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.LSEState = RCC_LSE_ON;
RCC_OscInitStruct.MSIState = RCC_MSI_ON;
RCC_OscInitStruct.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT;
RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11; // 48 MHz
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_MSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) {
Error_Handler();
}
// Required for correct wakeup from Stop mode (STM32LowPower)
__HAL_RCC_WAKEUPSTOP_CLK_CONFIG(RCC_STOP_WAKEUPCLOCK_MSI);
}
Place this before your setup() function. No other changes needed.
Note on USB serial: omitting RCC_PERIPHCLK_USB from SystemClock_Config does not disable USB serial. The Arduino USB stack configures the USB peripheral clock when it initializes on VBUS detection. USB serial works normally whether USB is connected at boot or hot-plugged later.
What Blues should fix
There are two separate issues worth addressing:
-
The bug: The variant’s SystemClock_Config unconditionally configures the USB peripheral clock even when USB is not present. The fix is to guard that block with a check on the USB_DETECT pin (PB3, already defined in the variant), or to move USB clock initialization out of SystemClock_Config entirely and into the USB stack init path.
-
The documentation: Blues advertises the Cygnet as running at 80 MHz, but the actual Arduino variant runs at 48 MHz with no PLL. If 80 MHz is the intended operating frequency, the variant’s SystemClock_Config should be updated to enable the PLL accordingly — which would also resolve the USB clock hang as a side effect, exactly as it works on the Swan.
At minimum, battery-powered operation should be explicitly documented as a supported and tested configuration, since the Cygnet is marketed specifically for that use case.