Fix Sleeping, voltage and temperature readings (#1591)

This commit is contained in:
Olivier
2026-02-14 09:38:22 +01:00
committed by GitHub
parent d6525d4c2e
commit dd12875b37
3 changed files with 387 additions and 300 deletions

View File

@@ -22,10 +22,10 @@
* @brief Hardware abstraction layer for STM32 microcontrollers using STM32duino core
*
* This implementation uses the official STM32duino Arduino core which provides
* STM32Cube HAL underneath. It supports a wide range of STM32 families including
* F0, F1, F4, L0, L4, G0, G4, H7, and more.
* STM32Cube HAL underneath. It supports a wide range of STM32 families.
*
* Tested on:
* - STM32F103C8 Blue Pill
* - STM32F401CC/CE Black Pill
* - STM32F411CE Black Pill
*
@@ -66,6 +66,18 @@ static void hwSleepRestoreSystemClock(void);
static void wakeUp1ISR(void);
static void wakeUp2ISR(void);
// Forward declarations for RTC counter helpers (portable across F1 and modern)
static uint32_t hwRtcGetCounter(void);
/**
* @brief Prevent HAL_RTC_Init from re-running clock setup
* All clock and backup domain configuration is handled in hwSleepInit()
*/
extern "C" void HAL_RTC_MspInit(RTC_HandleTypeDef *hrtc_param)
{
(void)hrtc_param;
}
bool hwInit(void)
{
#if !defined(MY_DISABLED_SERIAL)
@@ -86,7 +98,7 @@ bool hwInit(void)
void hwReadConfigBlock(void *buf, void *addr, size_t length)
{
uint8_t *dst = static_cast<uint8_t *>(buf);
int pos = reinterpret_cast<int>(addr);
const intptr_t pos = reinterpret_cast<intptr_t>(addr);
for (size_t i = 0; i < length; i++) {
dst[i] = EEPROM.read(pos + i);
@@ -96,7 +108,7 @@ void hwReadConfigBlock(void *buf, void *addr, size_t length)
void hwWriteConfigBlock(void *buf, void *addr, size_t length)
{
uint8_t *src = static_cast<uint8_t *>(buf);
int pos = reinterpret_cast<int>(addr);
const intptr_t pos = reinterpret_cast<intptr_t>(addr);
for (size_t i = 0; i < length; i++) {
EEPROM.update(pos + i, src[i]);
@@ -117,8 +129,6 @@ void hwWatchdogReset(void)
{
#if defined(HAL_IWDG_MODULE_ENABLED) && defined(IWDG)
// Reset independent watchdog if enabled
// Use direct register write to reload watchdog counter
// This works whether IWDG was initialized by HAL or LL drivers
IWDG->KR = IWDG_KEY_RELOAD;
#endif
}
@@ -128,145 +138,234 @@ void hwReboot(void)
NVIC_SystemReset();
}
int32_t hwReadInternalTemp(void)
{
#if defined(ATEMP)
// STM32duino defines ATEMP — use the easy path
analogReadResolution(12);
return (int32_t)analogRead(ATEMP);
#elif defined(STM32F1xx) && defined(ADC1)
// STM32F1: ATEMP not defined in STM32duino, read ADC channel 16 directly
// Temperature sensor is on ADC1_IN16, requires TSVREFE bit enabled
// Enable ADC1 clock
__HAL_RCC_ADC1_CLK_ENABLE();
// Enable temperature sensor and VREFINT (TSVREFE bit in CR2)
ADC1->CR2 |= ADC_CR2_TSVREFE;
// Wait for sensor startup (~10 µs per datasheet)
delayMicroseconds(20);
// Configure channel 16, sample time = 239.5 cycles (longest, for accuracy)
// SMP16 is bits [23:21] in SMPR1, value 0b111 = 239.5 cycles
ADC1->SMPR1 |= (0x7UL << 18); // SMP16 = 239.5 cycles
// Set sequence: 1 conversion, channel 16
ADC1->SQR3 = 16; // First conversion = channel 16
ADC1->SQR1 = 0; // 1 conversion in sequence
// Ensure ADC is on
ADC1->CR2 |= ADC_CR2_ADON;
delayMicroseconds(2);
// Start conversion
ADC1->CR2 |= ADC_CR2_ADON; // Second write starts conversion on F1
// Wait for conversion complete (EOC flag)
uint32_t timeout = 100000;
while (((ADC1->SR & ADC_SR_EOC) == 0) && (timeout > 0)) {
timeout--;
}
if (timeout == 0) {
return -1;
}
int32_t raw = (int32_t)(ADC1->DR & 0x0FFF);
// Disable temperature sensor to save power
ADC1->CR2 &= ~ADC_CR2_TSVREFE;
return raw;
#else
return -1;
#endif
}
void hwRandomNumberInit(void)
{
// Use internal temperature sensor and ADC noise as entropy source
// This provides reasonably good random seed values
#if defined(HAL_RNG_MODULE_ENABLED) && defined(RNG)
// ============================================================
// Hardware RNG available (F2, F4x5/F4x7+, F7, L4, G0, G4, H7)
// ============================================================
__HAL_RCC_RNG_CLK_ENABLE();
#ifdef ADC1
RNG_HandleTypeDef hrng = {0};
hrng.Instance = RNG;
if (HAL_RNG_Init(&hrng) == HAL_OK) {
uint32_t seed = 0;
if (HAL_RNG_GenerateRandomNumber(&hrng, &seed) == HAL_OK) {
randomSeed(seed);
// Disable RNG clock to save power after seeding
HAL_RNG_DeInit(&hrng);
__HAL_RCC_RNG_CLK_DISABLE();
return;
}
HAL_RNG_DeInit(&hrng);
}
__HAL_RCC_RNG_CLK_DISABLE();
// Fall through to software entropy if hardware RNG failed
#endif // HAL_RNG_MODULE_ENABLED && RNG
// ============================================================
// Software entropy fallback (F0, F1, F3, and chips without RNG)
// Uses ADC noise from temp sensor, VREFINT, and timing jitter
// ============================================================
#if defined(ADC1)
uint32_t seed = 0;
// Read multiple samples from different sources for entropy
for (uint8_t i = 0; i < 32; i++) {
uint32_t value = 0;
uint32_t value = hwReadInternalTemp();
#ifdef TEMP_SENSOR_AVAILABLE
// Try to read internal temperature sensor if available
value ^= analogRead(ATEMP);
#endif
#ifdef VREF_AVAILABLE
// Mix in internal voltage reference reading
#if defined(AVREF)
value ^= analogRead(AVREF);
#endif
// Mix in current time
value ^= hwMillis();
// Mix in system tick
value ^= micros();
// Accumulate into seed
seed ^= (value & 0x7) << (i % 29);
// Rotate and XOR for better bit distribution
seed ^= (i == 0) ? value : ((value << i) | (value >> (32 - i)));
// Small delay to ensure values change
delayMicroseconds(100);
}
randomSeed(seed);
#else
// Fallback: use millis as weak entropy source
randomSeed(hwMillis());
// Last resort: weak entropy from timing only
randomSeed(hwMillis() ^ (micros() << 16));
#endif // ADC1
}
bool hwUniqueID(unique_id_t *uniqueID)
{
#ifdef UID_BASE
// padding
#if defined(UID_BASE)
(void)memset(reinterpret_cast<uint8_t *>(uniqueID), MY_HWID_PADDING_BYTE, sizeof(unique_id_t));
(void)memcpy(reinterpret_cast<uint8_t *>(uniqueID), (uint32_t *)UID_BASE, 12);
(void)memcpy(reinterpret_cast<uint8_t *>(uniqueID), (const uint8_t *)UID_BASE, 12);
return true;
#else
// Unique ID not available on this variant
return false;
#endif
}
uint16_t hwCPUVoltage(void)
{
#if defined(VREF_AVAILABLE) && defined(AVREF) && defined(__HAL_RCC_ADC1_CLK_ENABLE)
// Read internal voltage reference to calculate VDD
// VREFINT is typically 1.2V (varies by STM32 family)
#if defined(AVREF) && defined(__HAL_RCC_ADC1_CLK_ENABLE)
// Force 12-bit resolution for predictable raw values
analogReadResolution(12);
uint32_t vrefint = analogRead(AVREF);
if (vrefint > 0) {
// Calculate VDD in millivolts
// Formula: VDD = 3.3V * 4096 / ADC_reading
// Adjusted: VDD = 1200mV * 4096 / vrefint_reading
return (uint16_t)((1200UL * 4096UL) / vrefint);
#if defined(VREFINT_CAL_ADDR) && defined(VREFINT_CAL_VREF)
// Use factory calibration for accurate measurement
// VREFINT_CAL was measured at VREFINT_CAL_VREF mV with 12-bit ADC
uint16_t vrefint_cal = *((uint16_t *)VREFINT_CAL_ADDR);
return (uint16_t)((uint32_t)VREFINT_CAL_VREF * vrefint_cal / vrefint);
#else
// No factory calibration (e.g., STM32F103)
// Assume typical VREFINT = 1.2V, 12-bit ADC
return (uint16_t)((1200UL * 4095UL) / vrefint);
#endif
}
#endif
// Return typical 3.3V if measurement not available
return 3300;
}
uint16_t hwCPUFrequency(void)
{
// Return CPU frequency in 0.1 MHz units
// F_CPU is defined by the build system (e.g., 84000000 for 84 MHz)
return F_CPU / 100000UL;
// HAL_RCC_GetSysClockFreq() reads the actual clock configuration registers
// and returns the current SYSCLK frequency in Hz
return (uint16_t)(HAL_RCC_GetSysClockFreq() / 100000UL);
}
int8_t hwCPUTemperature(void)
{
#if defined(TEMP_SENSOR_AVAILABLE) && defined(ATEMP) && defined(__HAL_RCC_ADC1_CLK_ENABLE)
// Read internal temperature sensor
// Note: Requires calibration values for accurate results
// cppcheck-suppress knownConditionTrueFalse
int32_t temp_raw = hwReadInternalTemp();
int32_t temp_raw = analogRead(ATEMP);
if (temp_raw < 0) { // cppcheck-suppress knownConditionTrueFalse
return FUNCTION_NOT_SUPPORTED;
}
#ifdef TEMP110_CAL_ADDR
// Use factory calibration if available (STM32F4, L4, etc.)
uint16_t *temp30_cal = (uint16_t *)TEMP30_CAL_ADDR;
uint16_t *temp110_cal = (uint16_t *)TEMP110_CAL_ADDR;
#if defined(TEMP110_CAL_ADDR) && defined(TEMP30_CAL_ADDR)
// Use factory calibration if available (STM32F4, L4, G4, etc.)
uint16_t cal30 = *((uint16_t *)TEMP30_CAL_ADDR);
uint16_t cal110 = *((uint16_t *)TEMP110_CAL_ADDR);
if (temp30_cal && temp110_cal && *temp110_cal != *temp30_cal) {
// Calculate temperature using two-point calibration
// Formula: T = ((110-30) / (CAL_110 - CAL_30)) * (raw - CAL_30) + 30
int32_t temp = 30 + ((110 - 30) * (temp_raw - *temp30_cal)) /
(*temp110_cal - *temp30_cal);
// Apply user calibration
temp = (temp - MY_STM32_TEMPERATURE_OFFSET) / MY_STM32_TEMPERATURE_GAIN;
if (cal110 != cal30) {
int32_t temp = 30 + ((110 - 30) * (temp_raw - (int32_t)cal30)) /
((int32_t)cal110 - (int32_t)cal30);
temp = ((temp - MY_STM32_TEMPERATURE_OFFSET) * 100) / MY_STM32_TEMPERATURE_GAIN;
return (int8_t)temp;
}
#endif // TEMP110_CAL_ADDR
#endif
// Fallback: use typical values (less accurate)
// Typical slope: 2.5 mV/°C, V25 = 0.76V for STM32F4
// This is a rough approximation
// Fallback: use typical datasheet values (no factory calibration)
#if defined(STM32F1xx)
// STM32F1: V25 = 1.43V, slope = 4.3 mV/°C, slope is NEGATIVE (higher T = lower V)
float voltage = (temp_raw * 3.3f) / 4096.0f;
int32_t temp = 25 + (int32_t)((1.43f - voltage) / 0.0043f);
#else
// STM32F4 and others without calibration: V25 = 0.76V, slope = 2.5 mV/°C
float voltage = (temp_raw * 3.3f) / 4096.0f;
int32_t temp = 25 + (int32_t)((voltage - 0.76f) / 0.0025f);
return (int8_t)((temp - MY_STM32_TEMPERATURE_OFFSET) / MY_STM32_TEMPERATURE_GAIN);
#else
// Temperature sensor not available
return FUNCTION_NOT_SUPPORTED;
#endif
return (int8_t)(((temp - MY_STM32_TEMPERATURE_OFFSET) * 100) / MY_STM32_TEMPERATURE_GAIN);
}
extern "C" caddr_t _sbrk(int incr);
uint16_t hwFreeMem(void)
{
// Calculate free heap memory
// This uses newlib's mallinfo if available
// Use ARM semihosting / newlib approach
// _sbrk(0) returns current program break (top of heap)
#ifdef STACK_TOP
extern char *__brkval;
extern char __heap_start;
char *heap_end = __brkval ? __brkval : &__heap_start;
char stack_var;
char *heap_end = (char *)_sbrk(0);
if (heap_end != (char *)-1) {
return (uint16_t)(&stack_var - heap_end);
}
// Calculate space between heap and stack
return (uint16_t)(&stack_var - heap_end);
#else
// Alternative method: try to allocate and measure
// Not implemented to avoid fragmentation
return FUNCTION_NOT_SUPPORTED;
}
// ======================== RTC Counter Helpers ========================
/**
* @brief Read current RTC counter value (portable)
* @return 32-bit counter on F1, subsecond-approximated tick on modern STM32
*/
static uint32_t hwRtcGetCounter(void)
{
#if defined(STM32F1xx)
return (uint32_t)(RTC->CNTL | (RTC->CNTH << 16));
#else
// Modern STM32: use HAL to get time and compute seconds since epoch
// For sleep remaining calculation, we only need relative elapsed seconds
RTC_TimeTypeDef sTime = {0};
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
// Must read date after time to unlock shadow registers
RTC_DateTypeDef sDate = {0};
HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
return (uint32_t)(sTime.Hours * 3600 + sTime.Minutes * 60 + sTime.Seconds);
#endif
}
@@ -282,103 +381,107 @@ static bool hwSleepInit(void)
return true;
}
// Enable PWR clock
// ---- Enable PWR and backup domain access ----
#if defined(STM32F1xx)
// STM32F1 requires BKP clock enabled BEFORE PWR clock
__HAL_RCC_BKP_CLK_ENABLE();
__DSB();
__HAL_RCC_PWR_CLK_ENABLE();
__DSB();
// Enable backup domain access
// Dummy read to ensure clocks are active before accessing PWR registers
volatile uint32_t dummy = RCC->APB1ENR;
(void)dummy;
// Direct register write for backup domain access
PWR->CR |= PWR_CR_DBP;
__DSB();
#else
// Modern STM32 (F2/F3/F4/F7/L0/L1/L4/L5/G0/G4/H7)
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
#endif
// Only reset backup domain if RTC is not already configured
// This prevents disrupting other peripherals when MySensors radio is initialized first
if ((RCC->BDCR & RCC_BDCR_RTCEN) != 0) {
// RTC already enabled - check if it's the right clock source
// If already configured, skip reset to avoid disrupting existing setup
} else {
// RTC not enabled - safe to reset backup domain for clean slate
// ---- Reset backup domain if RTC not already configured ----
if ((RCC->BDCR & RCC_BDCR_RTCEN) == 0) {
__HAL_RCC_BACKUPRESET_FORCE();
HAL_Delay(10);
__HAL_RCC_BACKUPRESET_RELEASE();
HAL_Delay(10);
}
// Try LSE first (32.768 kHz external crystal - more accurate)
// Fall back to LSI if LSE is not available
// ---- Select RTC clock source: try LSE, fall back to LSI ----
bool useLSE = false;
// Check if LSE is already running
if ((RCC->BDCR & RCC_BDCR_LSERDY) != 0) {
// LSE already ready - use it
useLSE = true;
} else {
// Attempt to start LSE
RCC->BDCR |= RCC_BDCR_LSEON;
uint32_t timeout = 2000000; // LSE takes longer to start
uint32_t timeout = 2000000;
while (((RCC->BDCR & RCC_BDCR_LSERDY) == 0) && (timeout > 0)) {
timeout--;
}
if (timeout > 0) {
// LSE started successfully
useLSE = true;
} else {
// LSE failed - fall back to LSI
if ((RCC->CSR & RCC_CSR_LSIRDY) == 0) {
// LSI not ready, try to start it
RCC->BDCR &= ~RCC_BDCR_LSEON; // Disable failed LSE
RCC->BDCR &= ~RCC_BDCR_LSEON; // Disable failed LSE
// Enable LSI (internal ~32 kHz oscillator)
if ((RCC->CSR & RCC_CSR_LSIRDY) == 0) {
RCC->CSR |= RCC_CSR_LSION;
timeout = 1000000;
while (((RCC->CSR & RCC_CSR_LSIRDY) == 0) && (timeout > 0)) {
timeout--;
}
if (timeout == 0) {
return false; // Both LSE and LSI failed
}
}
// LSI ready (either was already running or just started)
}
}
// Configure RTC clock source (only if not already configured correctly)
// ---- Configure RTC clock source ----
uint32_t currentRtcSel = (RCC->BDCR & RCC_BDCR_RTCSEL);
uint32_t desiredRtcSel = useLSE ? RCC_BDCR_RTCSEL_0 : RCC_BDCR_RTCSEL_1;
if (currentRtcSel != desiredRtcSel) {
// Need to change clock source - clear and set
RCC->BDCR &= ~RCC_BDCR_RTCSEL; // Clear selection
RCC->BDCR |= desiredRtcSel; // Set new selection
// Changing RTCSEL requires backup domain reset on most STM32 families
__HAL_RCC_BACKUPRESET_FORCE();
__HAL_RCC_BACKUPRESET_RELEASE();
RCC->BDCR |= desiredRtcSel;
}
RCC->BDCR |= RCC_BDCR_RTCEN; // Ensure RTC clock is enabled
RCC->BDCR |= RCC_BDCR_RTCEN;
// Initialize RTC peripheral
// ---- Initialize RTC peripheral ----
hrtc.Instance = RTC;
#if defined(STM32F1xx)
// ============================================================
// STM32F1: Legacy RTC with counter-based architecture
// ============================================================
// F1 RTC uses simple 32-bit counter with prescaler
// No calendar, no wake-up timer - use RTC Alarm instead
if (useLSE) {
// LSE: 32.768 kHz exact - set prescaler for 1 Hz tick
hrtc.Init.AsynchPrediv = 32767; // (32767+1) = 32768 = 1 Hz
hrtc.Init.AsynchPrediv = 32767; // 32768 Hz / 32768 = 1 Hz
} else {
// LSI: ~40 kHz (STM32F1 LSI is typically 40kHz, not 32kHz)
hrtc.Init.AsynchPrediv = 39999; // (39999+1) = 40000 = 1 Hz (approximate)
hrtc.Init.AsynchPrediv = 39999; // ~40 kHz / 40000 = 1 Hz (F1 LSI is ~40 kHz)
}
hrtc.Init.OutPut = RTC_OUTPUTSOURCE_NONE;
// F1 RTC initialization is simpler
if (HAL_RTC_Init(&hrtc) != HAL_OK) {
return false;
}
// CRITICAL: Enable RTC Alarm interrupt in NVIC (F1 uses Alarm, not WKUP)
// Without this, the MCU cannot wake from STOP mode via RTC
// Clear any stale alarm flags before enabling interrupts
RTC->CRL &= ~RTC_CRL_ALRF;
EXTI->PR = EXTI_PR_PR17;
NVIC_ClearPendingIRQ(RTC_Alarm_IRQn);
// Configure EXTI line 17 for RTC Alarm wake-up from STOP mode
// Without EXTI, the alarm flag is set but the CPU does not wake
EXTI->IMR |= EXTI_IMR_MR17; // Unmask EXTI line 17
EXTI->RTSR |= EXTI_RTSR_TR17; // Rising edge trigger
HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
@@ -386,40 +489,32 @@ static bool hwSleepInit(void)
// ============================================================
// STM32F2/F3/F4/F7/L1/L4/L5/G0/G4/H7: Modern RTC
// ============================================================
// Modern RTC with BCD calendar and dedicated wake-up timer
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
if (useLSE) {
// LSE: 32.768 kHz exact - perfect 1 Hz with these prescalers
hrtc.Init.AsynchPrediv = 127; // (127+1) = 128
hrtc.Init.SynchPrediv = 255; // (255+1) = 256, total = 32768
hrtc.Init.AsynchPrediv = 127; // 128
hrtc.Init.SynchPrediv = 255; // 256, total = 32768
} else {
// LSI: ~32 kHz (variable) - approximate 1 Hz
hrtc.Init.AsynchPrediv = 127;
hrtc.Init.SynchPrediv = 249; // Adjusted for typical LSI
hrtc.Init.SynchPrediv = 249; // Adjusted for typical ~32 kHz LSI
}
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
// Check if RTC is already initialized (INITS bit in ISR register)
// If already initialized, we can skip HAL_RTC_Init which may fail
// when called after other peripherals (like SPI) are already running
// Check if RTC is already initialized (avoid reinit conflicts with SPI etc.)
if ((RTC->ISR & RTC_ISR_INITS) == 0) {
// RTC not yet initialized - call HAL_RTC_Init
if (HAL_RTC_Init(&hrtc) != HAL_OK) {
return false;
}
} else {
// RTC already initialized - just update the handle
// This allows us to use it for sleep even if something else initialized it
hrtc.State = HAL_RTC_STATE_READY;
}
// CRITICAL: Enable RTC wakeup interrupt in NVIC
// Without this, the MCU cannot wake from STOP mode via RTC
// Configure EXTI line 22 for RTC wake-up timer (required for STOP mode wake)
// HAL_RTCEx_SetWakeUpTimer_IT handles this on most families, but be explicit
HAL_NVIC_SetPriority(RTC_WKUP_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(RTC_WKUP_IRQn);
@@ -437,17 +532,13 @@ static bool hwSleepInit(void)
static bool hwSleepConfigureTimer(uint32_t ms)
{
if (!rtcInitialized) {
if (!hwSleepInit()) {
return false;
}
return false;
}
if (ms == 0) {
#if defined(STM32F1xx)
// F1: Disable RTC Alarm
HAL_RTC_DeactivateAlarm(&hrtc, RTC_ALARM_A);
#else
// Modern STM32: Disable wake-up timer
HAL_RTCEx_DeactivateWakeUpTimer(&hrtc);
#endif
return true;
@@ -455,35 +546,33 @@ static bool hwSleepConfigureTimer(uint32_t ms)
#if defined(STM32F1xx)
// ============================================================
// STM32F1: Use RTC Alarm for wake-up
// STM32F1: Use RTC Alarm (counter-based)
// ============================================================
// F1 doesn't have wake-up timer, use alarm instead
// RTC counter runs at 1 Hz (configured in hwSleepInit)
// Read current counter value
// Note: On F1, read CNT register directly (HAL doesn't provide a clean way)
uint32_t currentCounter = RTC->CNTL | (RTC->CNTH << 16);
uint32_t currentCounter = hwRtcGetCounter();
// Calculate alarm value (counter + seconds)
// Convert ms to seconds (RTC runs at 1 Hz)
uint32_t seconds = ms / 1000;
if (seconds == 0) {
seconds = 1; // Minimum 1 second
}
if (seconds > 0xFFFFFFFF - currentCounter) {
// Overflow protection
seconds = 0xFFFFFFFF - currentCounter;
}
uint32_t alarmValue = currentCounter + seconds;
// Configure alarm
RTC_AlarmTypeDef sAlarm = {0};
sAlarm.Alarm = alarmValue;
// Write alarm registers directly via configuration mode
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) { } // Wait for last write
RTC->CRL |= RTC_CRL_CNF; // Enter config mode
if (HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN) != HAL_OK) {
return false;
}
RTC->ALRH = (alarmValue >> 16) & 0xFFFF;
RTC->ALRL = alarmValue & 0xFFFF;
RTC->CRL &= ~RTC_CRL_CNF; // Exit config mode
while ((RTC->CRL & RTC_CRL_RTOFF) == 0) { } // Wait for write complete
// Enable alarm interrupt in RTC
RTC->CRH |= RTC_CRH_ALRIE;
#else
// ============================================================
@@ -493,32 +582,28 @@ static bool hwSleepConfigureTimer(uint32_t ms)
uint32_t wakeUpCounter;
uint32_t wakeUpClock;
// Choose appropriate clock and counter value based on sleep duration
if (ms <= 32000) {
// Up to 32 seconds: use RTCCLK/16 (2048 Hz, 0.488 ms resolution)
// Up to 32 seconds: use RTCCLK/16 (2048 Hz, ~0.49 ms resolution)
wakeUpClock = RTC_WAKEUPCLOCK_RTCCLK_DIV16;
// Counter = ms * 2048 / 1000 = ms * 2.048
// Use bit shift for efficiency: ms * 2048 = ms << 11
wakeUpCounter = (ms << 11) / 1000;
wakeUpCounter = (ms << 11) / 1000; // ms * 2048 / 1000
if (wakeUpCounter < 2) {
wakeUpCounter = 2; // Minimum 2 ticks
wakeUpCounter = 2;
}
if (wakeUpCounter > 0xFFFF) {
wakeUpCounter = 0xFFFF;
}
} else {
// More than 32 seconds: use CK_SPRE (1 Hz, 1 second resolution)
// Over 32 seconds: use CK_SPRE (1 Hz, 1 second resolution)
wakeUpClock = RTC_WAKEUPCLOCK_CK_SPRE_16BITS;
wakeUpCounter = ms / 1000; // Convert to seconds
wakeUpCounter = ms / 1000;
if (wakeUpCounter == 0) {
wakeUpCounter = 1; // Minimum 1 second
wakeUpCounter = 1;
}
if (wakeUpCounter > 0xFFFF) {
wakeUpCounter = 0xFFFF; // Max ~18 hours
}
}
// Configure wake-up timer with interrupt
if (HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, wakeUpCounter, wakeUpClock) != HAL_OK) {
return false;
}
@@ -530,14 +615,11 @@ static bool hwSleepConfigureTimer(uint32_t ms)
/**
* @brief Restore system clock after wake-up from STOP mode
* @note After STOP mode, system clock defaults to HSI (16 MHz). We always call
* SystemClock_Config() to restore the full clock configuration as the
* Arduino core and peripherals expect it.
* @note After STOP mode, system clock defaults to HSI (8 MHz on F1, 16 MHz on F4).
* SystemClock_Config() restores the full PLL configuration.
*/
static void hwSleepRestoreSystemClock(void)
{
// After STOP mode, system runs on HSI (16 MHz)
// Always restore the system clock configuration to what the Arduino core expects
SystemClock_Config();
}
@@ -558,16 +640,16 @@ static void wakeUp2ISR(void)
}
/**
* @brief RTC Wake-up Timer interrupt handler
* @brief RTC interrupt handlers
*/
#if defined(STM32F1xx)
// F1: Use RTC Alarm interrupt
extern "C" void RTC_Alarm_IRQHandler(void)
{
HAL_RTC_AlarmIRQHandler(&hrtc);
// Lean handler: clear flags directly instead of going through HAL
EXTI->PR = EXTI_PR_PR17; // Clear EXTI line 17 pending
RTC->CRL &= ~RTC_CRL_ALRF; // Clear RTC alarm flag
}
#else
// Modern STM32: Use dedicated wake-up timer interrupt
extern "C" void RTC_WKUP_IRQHandler(void)
{
HAL_RTCEx_WakeUpTimerIRQHandler(&hrtc);
@@ -581,87 +663,27 @@ uint32_t hwGetSleepRemaining(void)
return sleepRemainingMs;
}
int8_t hwSleep(uint32_t ms)
/**
* @brief Common sleep implementation
* @param interrupt1 First interrupt pin (INVALID_INTERRUPT_NUM = none)
* @param mode1 First interrupt mode
* @param interrupt2 Second interrupt pin (INVALID_INTERRUPT_NUM = none)
* @param mode2 Second interrupt mode
* @param ms Sleep duration in ms (0 = interrupt-only, no timer)
* @return Wake-up source or error code
*/
static int8_t hwSleepInternal(const uint8_t interrupt1, const uint8_t mode1,
const uint8_t interrupt2, const uint8_t mode2,
uint32_t ms)
{
// Initialize RTC if needed
if (!rtcInitialized) {
if (!hwSleepInit()) {
return MY_SLEEP_NOT_POSSIBLE;
}
bool hasInterrupt = (interrupt1 != INVALID_INTERRUPT_NUM) ||
(interrupt2 != INVALID_INTERRUPT_NUM);
// Reject timer-only sleep with ms=0 (would sleep forever with no wake source)
if (ms == 0 && !hasInterrupt) {
return MY_SLEEP_NOT_POSSIBLE;
}
// Configure RTC wake-up timer
if (ms > 0) {
if (!hwSleepConfigureTimer(ms)) {
return MY_SLEEP_NOT_POSSIBLE;
}
}
// Reset sleep remaining
sleepRemainingMs = 0ul;
// CRITICAL: Clear wakeup flags before entering sleep
// This prevents spurious wakeups from previous events
#if defined(STM32F1xx)
__HAL_RTC_ALARM_CLEAR_FLAG(&hrtc, RTC_FLAG_ALRAF);
#else
__HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&hrtc, RTC_FLAG_WUTF);
#endif
__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
// Suspend SysTick to prevent 1ms interrupts during sleep
HAL_SuspendTick();
// NOTE: USB CDC will disconnect during STOP mode (expected behavior)
// USB peripheral requires system clock which is stopped in STOP mode
// After wake-up, the host will detect USB disconnect/reconnect
// This is normal and unavoidable when using STOP mode sleep
// Enter STOP mode with low-power regulator
// This achieves 10-50 µA sleep current on STM32F4
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// ====================================================================
// === MCU is in STOP mode here (10-50 µA), waiting for wake-up ===
// ====================================================================
// After wake-up: restore system clock (defaults to HSI)
hwSleepRestoreSystemClock();
// Resume SysTick
HAL_ResumeTick();
// CRITICAL: Clear wakeup flags after wake-up
// This ensures clean state for next sleep cycle
#if defined(STM32F1xx)
__HAL_RTC_ALARM_CLEAR_FLAG(&hrtc, RTC_FLAG_ALRAF);
#else
__HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&hrtc, RTC_FLAG_WUTF);
#endif
__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
// Disable wake-up timer
if (ms > 0) {
#if defined(STM32F1xx)
HAL_RTC_DeactivateAlarm(&hrtc, RTC_ALARM_A);
#else
HAL_RTCEx_DeactivateWakeUpTimer(&hrtc);
#endif
}
// Always timer wake-up for this variant
return MY_WAKE_UP_BY_TIMER;
}
int8_t hwSleep(const uint8_t interrupt, const uint8_t mode, uint32_t ms)
{
// Delegate to dual-interrupt variant with INVALID second interrupt
return hwSleep(interrupt, mode, INVALID_INTERRUPT_NUM, 0, ms);
}
int8_t hwSleep(const uint8_t interrupt1, const uint8_t mode1,
const uint8_t interrupt2, const uint8_t mode2, uint32_t ms)
{
// Initialize RTC if needed
if (!rtcInitialized) {
if (!hwSleepInit()) {
@@ -676,61 +698,95 @@ int8_t hwSleep(const uint8_t interrupt1, const uint8_t mode1,
}
}
// Reset sleep remaining
// Reset sleep state
sleepRemainingMs = 0ul;
// Configure interrupt wake-up sources
_wakeUp1Interrupt = interrupt1;
_wakeUp2Interrupt = interrupt2;
_wokeUpByInterrupt = INVALID_INTERRUPT_NUM;
// Attach interrupts in critical section (prevent premature wake-up)
MY_CRITICAL_SECTION {
if (interrupt1 != INVALID_INTERRUPT_NUM)
{
attachInterrupt(digitalPinToInterrupt(interrupt1), wakeUp1ISR, mode1);
// Attach interrupt wake-up sources
if (hasInterrupt) {
_wakeUp1Interrupt = interrupt1;
_wakeUp2Interrupt = interrupt2;
MY_CRITICAL_SECTION {
if (interrupt1 != INVALID_INTERRUPT_NUM)
{
attachInterrupt(digitalPinToInterrupt(interrupt1), wakeUp1ISR, mode1);
}
if (interrupt2 != INVALID_INTERRUPT_NUM)
{
attachInterrupt(digitalPinToInterrupt(interrupt2), wakeUp2ISR, mode2);
}
}
if (interrupt2 != INVALID_INTERRUPT_NUM)
{
attachInterrupt(digitalPinToInterrupt(interrupt2), wakeUp2ISR, mode2);
// Check if an interrupt already fired between attach and here
if (_wokeUpByInterrupt != INVALID_INTERRUPT_NUM) {
// Already woken — skip sleep, go to cleanup
goto cleanup;
}
}
// CRITICAL: Clear wakeup flags before entering sleep
// Record RTC counter before sleep (for sleepRemaining calculation)
{
const uint32_t sleepStartCounter = hwRtcGetCounter();
// Clear all wake-up flags before entering STOP mode
#if defined(STM32F1xx)
__HAL_RTC_ALARM_CLEAR_FLAG(&hrtc, RTC_FLAG_ALRAF);
RTC->CRL &= ~RTC_CRL_ALRF;
EXTI->PR = EXTI_PR_PR17;
NVIC_ClearPendingIRQ(RTC_Alarm_IRQn);
#else
__HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&hrtc, RTC_FLAG_WUTF);
__HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&hrtc, RTC_FLAG_WUTF);
#endif
__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
// Suspend SysTick
HAL_SuspendTick();
// Suspend SysTick to prevent 1ms interrupts during sleep
HAL_SuspendTick();
// NOTE: USB CDC will disconnect during STOP mode (expected behavior)
// See note in timer-only hwSleep() variant above
// Enter STOP mode with low-power regulator
// NOTE: USB CDC will disconnect (expected; USB needs system clock)
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// Enter STOP mode with low-power regulator
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// ================================================================
// === MCU is in STOP mode here, waiting for wake-up event ===
// ================================================================
// ====================================================================
// === MCU is in STOP mode here (10-50 µA), waiting for wake-up ===
// ====================================================================
// Restore system clock (HSI -> PLL)
hwSleepRestoreSystemClock();
// After wake-up: restore system clock
hwSleepRestoreSystemClock();
// Resume SysTick
HAL_ResumeTick();
// Resume SysTick
HAL_ResumeTick();
// CRITICAL: Clear wakeup flags after wake-up
// Clear wake-up flags after wake
#if defined(STM32F1xx)
__HAL_RTC_ALARM_CLEAR_FLAG(&hrtc, RTC_FLAG_ALRAF);
RTC->CRL &= ~RTC_CRL_ALRF;
EXTI->PR = EXTI_PR_PR17;
#else
__HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&hrtc, RTC_FLAG_WUTF);
__HAL_RTC_WAKEUPTIMER_CLEAR_FLAG(&hrtc, RTC_FLAG_WUTF);
#endif
__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
// Calculate sleep remaining
if (ms > 0) {
const uint32_t sleepEndCounter = hwRtcGetCounter();
uint32_t elapsedSeconds;
#if defined(STM32F1xx)
elapsedSeconds = sleepEndCounter - sleepStartCounter;
#else
// Handle midnight rollover (86400 seconds in a day)
if (sleepEndCounter >= sleepStartCounter) {
elapsedSeconds = sleepEndCounter - sleepStartCounter;
} else {
elapsedSeconds = (86400 - sleepStartCounter) + sleepEndCounter;
}
#endif
const uint32_t elapsedMs = elapsedSeconds * 1000;
sleepRemainingMs = (elapsedMs < ms) ? (ms - elapsedMs) : 0;
}
}
cleanup:
// Detach interrupts
if (interrupt1 != INVALID_INTERRUPT_NUM) {
detachInterrupt(digitalPinToInterrupt(interrupt1));
@@ -749,9 +805,9 @@ int8_t hwSleep(const uint8_t interrupt1, const uint8_t mode1,
}
// Determine wake-up source
int8_t ret = MY_WAKE_UP_BY_TIMER; // Default: timer wake-up
int8_t ret = MY_WAKE_UP_BY_TIMER;
if (_wokeUpByInterrupt != INVALID_INTERRUPT_NUM) {
ret = (int8_t)_wokeUpByInterrupt; // Interrupt wake-up
ret = (int8_t)_wokeUpByInterrupt;
}
// Reset interrupt tracking
@@ -761,3 +817,19 @@ int8_t hwSleep(const uint8_t interrupt1, const uint8_t mode1,
return ret;
}
int8_t hwSleep(uint32_t ms)
{
return hwSleepInternal(INVALID_INTERRUPT_NUM, 0, INVALID_INTERRUPT_NUM, 0, ms);
}
int8_t hwSleep(const uint8_t interrupt, const uint8_t mode, uint32_t ms)
{
return hwSleepInternal(interrupt, mode, INVALID_INTERRUPT_NUM, 0, ms);
}
int8_t hwSleep(const uint8_t interrupt1, const uint8_t mode1,
const uint8_t interrupt2, const uint8_t mode2, uint32_t ms)
{
return hwSleepInternal(interrupt1, mode1, interrupt2, mode2, ms);
}

View File

@@ -20,13 +20,13 @@
#ifndef MyHwSTM32_h
#define MyHwSTM32_h
#include <SPI.h>
#include <EEPROM.h>
#ifdef __cplusplus
#include <Arduino.h>
#endif
#include <SPI.h>
#include <EEPROM.h>
// Crypto endianness
#define CRYPTO_LITTLE_ENDIAN
@@ -46,18 +46,20 @@
#endif
/**
* @brief Temperature sensor offset calibration
* @brief Temperature sensor offset calibration (integer)
* @note Adjust based on your specific STM32 chip calibration
* @note Calibration model: T_corrected = (T_raw - offset) / gain
*/
#ifndef MY_STM32_TEMPERATURE_OFFSET
#define MY_STM32_TEMPERATURE_OFFSET (0.0f)
#define MY_STM32_TEMPERATURE_OFFSET (0)
#endif
/**
* @brief Temperature sensor gain calibration
* @brief Temperature sensor gain calibration (integer, scaled x100)
* @note Default 100 means gain = 1.0. Use 110 for gain = 1.1, etc.
*/
#ifndef MY_STM32_TEMPERATURE_GAIN
#define MY_STM32_TEMPERATURE_GAIN (1.0f)
#define MY_STM32_TEMPERATURE_GAIN (100)
#endif
// Printf format string compatibility
@@ -113,6 +115,15 @@ void hwWatchdogReset(void);
*/
void hwReboot(void);
/**
* @brief Read internal temperature sensor via HAL ADC
* @return Raw 12-bit ADC value, or -1 on error
* @note Works on all STM32 families. On F1, the temperature sensor is on
* ADC1 channel 16 and requires TSVREFE bit to be set in ADC_CR2.
* On F4/L4/G4 etc., analogRead(ATEMP) works when ATEMP is defined.
*/
int32_t hwReadInternalTemp(void);
/**
* @brief Initialize random number generator
* @note Uses internal temperature sensor as entropy source
@@ -181,11 +192,12 @@ int8_t hwCPUTemperature(void);
uint16_t hwFreeMem(void);
/**
* @brief Sleep for specified milliseconds
* @param ms Milliseconds to sleep (0 = sleep until interrupt)
* @brief Sleep for specified milliseconds (timer wake-up only)
* @param ms Milliseconds to sleep (must be > 0)
* @return MY_WAKE_UP_BY_TIMER (-1) if woken by timer, MY_SLEEP_NOT_POSSIBLE (-2) on error
* @note Uses STOP mode with low-power regulator (10-50 µA sleep current)
* @note Maximum sleep time depends on RTC configuration (~18 hours)
* @note Uses STOP mode with low-power regulator
* @note Maximum sleep time: ~65535 seconds on F1, ~18 hours on modern STM32
* @note ms=0 returns MY_SLEEP_NOT_POSSIBLE (use interrupt variant for indefinite sleep)
*/
int8_t hwSleep(uint32_t ms);
@@ -193,7 +205,7 @@ int8_t hwSleep(uint32_t ms);
* @brief Sleep with interrupt wake
* @param interrupt Arduino pin number for interrupt wake-up
* @param mode Interrupt mode (RISING, FALLING, CHANGE)
* @param ms Maximum sleep time in milliseconds (0 = no timeout)
* @param ms Maximum sleep time in milliseconds (0 = no timeout, interrupt only)
* @return Interrupt number (0-255) if woken by interrupt, MY_WAKE_UP_BY_TIMER (-1) if timeout,
* MY_SLEEP_NOT_POSSIBLE (-2) on error
* @note Supports wake-up on any GPIO pin via EXTI (critical for radio IRQ)
@@ -206,7 +218,7 @@ int8_t hwSleep(const uint8_t interrupt, const uint8_t mode, uint32_t ms);
* @param mode1 First interrupt mode (RISING, FALLING, CHANGE)
* @param interrupt2 Second Arduino pin number for interrupt wake-up
* @param mode2 Second interrupt mode (RISING, FALLING, CHANGE)
* @param ms Maximum sleep time in milliseconds (0 = no timeout)
* @param ms Maximum sleep time in milliseconds (0 = no timeout, interrupt only)
* @return Interrupt number that caused wake-up, MY_WAKE_UP_BY_TIMER (-1) if timeout,
* MY_SLEEP_NOT_POSSIBLE (-2) on error
* @note Useful for hybrid sensors (e.g., button press OR periodic wake-up)

View File

@@ -6,9 +6,11 @@ This directory contains the Hardware Abstraction Layer (HAL) implementation for
The STM32 HAL enables MySensors to run on a wide range of STM32 microcontrollers, including:
- **STM32F0** series (Cortex-M0)
- **STM32F1** series (Cortex-M3)
- **STM32F4** series (Cortex-M4 with FPU)
Not tested / implemented:
- **STM32F0** series (Cortex-M0)
- **STM32L0/L4** series (Low-power Cortex-M0+/M4)
- **STM32G0/G4** series (Cortex-M0+/M4)
- **STM32H7** series (Cortex-M7)
@@ -16,6 +18,7 @@ The STM32 HAL enables MySensors to run on a wide range of STM32 microcontrollers
## Supported Boards
Tested on:
- **STM32F103C8 Blue Pill** (72 MHz, 64KB Flash, 20KB RAM)
- **STM32F401CC Black Pill** (84 MHz, 256KB Flash, 64KB RAM)
- **STM32F411CE Black Pill** (100 MHz, 512KB Flash, 128KB RAM)