Previously in Part 4, we gave our STM32F429ZI the ability to communicate through UART. Now our microcontroller can speak, and the first thing it does is announce itself to the world.
Where We Are in the Code
UART_Init() has just returned to main(), and the very next lines are:
UART_SendString("\r\n=== Task Scheduler and Power Management Demo ===\r\n");
UART_SendString("Using correct LED pins:\r\n");
UART_SendString(" LD1 (Green) - PB0\r\n");
UART_SendString(" LD2 (Blue) - PB7\r\n");
UART_SendString(" LD3 (Red) - PB14\r\n\r\n");
These five UART_SendString() calls are our microcontroller’s first words. Each call blocks until all characters are transmitted (taking about 4-5 milliseconds per line). After about 25 milliseconds total, these messages have been sent to your terminal, and execution continues to:
/* Initialize the power management subsystem */
PowerMgmt_Init();
Why Power Management Matters
Before diving into the code, let’s understand why power management is crucial for embedded systems, especially IoT devices:
Battery Life: Many embedded devices run on batteries. The difference between good and bad power management can mean the difference between a device lasting days versus years on the same battery.
Heat Dissipation: Running at full power generates heat. Power management helps keep devices cool.
Energy Efficiency: Even wall-powered devices benefit from using less energy.
The STM32F429ZI offers seven different power modes, from full-speed operation down to standby mode that consumes just a few microamps. Our power management system will configure and control these modes.
The Journey Begins: Jumping to PowerMgmt_Init
When the processor encounters PowerMgmt_Init(), it jumps from main.c to the PowerMgmt_Init function in power_management.c:
void PowerMgmt_Init(void) {
/* Enable power interface clock */
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
We’re now inside the power management initialization function.
Step 1: Enabling the Power Interface
The first line of actual code is:
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
This enables the clock to the PWR (Power) peripheral. Just like we enabled clocks for GPIOD and USART3 in the previous part, we must enable the clock for the power control interface.
What is the PWR peripheral? The PWR peripheral is a specialized hardware block that controls:
- Voltage regulators
- Power supply monitoring
- Low-power mode entry/exit
- Backup domain access
- Wake-up sources
Without enabling its clock, we can’t access any power management features.
Step 2: Setting Default Configuration
Next, the code initializes a default configuration structure:
/* Set default configuration */
memset(¤tConfig, 0, sizeof(PowerConfig_t));
Wait, where does currentConfig come from?
The currentConfig variable is declared at the top of power_management.c as a static global variable:
/* Current power configuration */
static PowerConfig_t currentConfig;
Let’s break this down:
- static: This keyword means the variable is only visible within power_management.c (file scope) and persists for the entire program lifetime
- PowerConfig_t: This is the type – a structure that holds all power management settings
- currentConfig: The variable name – it stores the current power configuration
Memory layout visualization:
RAM Memory Map:
├── Stack (grows down)
├── Heap (grows up)
├── .bss section
│ ├── currentConfig <-- Lives here (static variables)
│ ├── lastWakeupSource
│ └── currentPowerMode
├── .data section
└── ...
Why use & (address-of operator)? The memset() function needs to know WHERE in memory to write zeros. The & operator gives the memory address of currentConfig. Without &, we’d be trying to pass the structure’s value, which memset() can’t use.
memset(¤tConfig, 0, sizeof(PowerConfig_t));
// ^
// | This gives the address where currentConfig lives in memory
// Without &, we'd get a compile error
What is sizeof(PowerConfig_t)? This calculates how many bytes the PowerConfig_t structure occupies in memory. On the STM32F429ZI, it’s typically around 12-16 bytes (depending on alignment).
Where in memory does currentConfig live? As a static global variable, currentConfig is stored in the .bss section of RAM, which is automatically cleared to zero at startup. However, we explicitly clear it again with memset() to be absolutely certain it starts in a known state.
The memset() function fills the entire currentConfig structure with zeros. This structure is defined in power_management.h as:
typedef struct {
PowerMode_t mode; /* Power mode selection */
VoltageScale_t voltageScale; /* Voltage scaling selection */
bool flashPowerDown; /* Whether to power down flash in Stop mode */
bool enableWakeupPin; /* Enable PA0 as wake-up pin */
bool enableBackupRegulator; /* Enable backup regulator for SRAM in standby */
bool enablePVD; /* Enable Programmable Voltage Detector */
PVDLevel_t pvdLevel; /* PVD threshold level */
WakeupSource_t wakeupSources; /* Sources that can wake the MCU from low-power modes */
} PowerConfig_t;
Important distinction:
- The above is the TYPE DEFINITION (what a PowerConfig_t looks like)
- The actual VARIABLE is declared in power_management.c as
static PowerConfig_t currentConfig; - We have one instance of this structure that persists throughout program execution
Why clear it with memset() if static variables start at zero? While the C standard says static variables are initialized to zero, explicitly clearing ensures:
- We don’t rely on startup code behavior
- The code is self-documenting – it’s clear we want zeros
- If PowerMgmt_Init() is ever called again, it starts fresh
After memset(), all fields are zero/false. Now we set specific defaults:
currentConfig.mode = POWER_MODE_RUN;
currentConfig.voltageScale = VOLTAGE_SCALE_1; /* Highest performance by default */
currentConfig.flashPowerDown = false;
currentConfig.enableWakeupPin = false;
currentConfig.enableBackupRegulator = false;
currentConfig.enablePVD = false;
currentConfig.pvdLevel = PVD_LEVEL_2V7;
currentConfig.wakeupSources = WAKEUP_SOURCE_NONE;
The lifecycle of currentConfig:
- Program start: Memory allocated in .bss section (already zero)
- PowerMgmt_Init() called: Explicitly cleared with memset, then initialized with defaults
- Throughout program: Updated when power settings change
- Program lifetime: Persists until system reset or power loss
Why use a static variable? The power management system needs to remember its configuration across function calls. Using a static variable means:
- The configuration persists without using the stack
- Only power_management.c functions can access it (encapsulation)
- No need to pass configuration pointers around
- Always know the current power state
These defaults configure the system for:
- RUN mode: Full power, maximum performance
- Voltage Scale 1: Highest voltage for maximum clock speeds
- No power-saving features: Everything disabled initially
Step 3: Applying Voltage Scaling
/* Apply default voltage scaling */
ConfigureVoltageScaling(currentConfig.voltageScale);
This calls an internal function to set the voltage scaling. Let’s trace into ConfigureVoltageScaling():
static bool ConfigureVoltageScaling(VoltageScale_t scale) {
uint32_t vos_bits;
/* Determine VOS bits based on scale */
switch (scale) {
case VOLTAGE_SCALE_1:
vos_bits = PWR_CR_VOS; /* Both bits set for Scale 1 (11) */
break;
// ... other cases
}
/* Update voltage scaling bits */
PWR->CR &= ~PWR_CR_VOS; /* Clear VOS bits */
PWR->CR |= vos_bits; /* Set new VOS bits */
/* Wait for voltage scaling to complete */
return IsVoltageScalingReady();
}
What is Voltage Scaling? The STM32F429ZI can operate at different voltage levels:
- Scale 1 (1.26V): Allows maximum clock frequency (180 MHz)
- Scale 2 (1.20V): Medium performance, lower power
- Scale 3 (1.14V): Lowest power, limited to 120 MHz
Higher voltage allows faster operation but consumes more power. We start with Scale 1 for maximum performance.
The IsVoltageScalingReady() function waits for the VOSRDY flag, indicating the voltage regulator has stabilized at the new level:
static bool IsVoltageScalingReady(void) {
uint32_t timeout = 10000;
while (!(PWR->CSR & PWR_CSR_VOSRDY)) {
if (timeout-- == 0) {
return false;
}
}
return true;
}
Step 4: Debug Message
After voltage scaling completes, PowerMgmt_Init() sends a debug message:
/* Debug message */
char debug_msg[100];
snprintf(debug_msg, sizeof(debug_msg),
"[POWER] Power management initialized, voltage scale: %d\r\n",
(int)currentConfig.voltageScale);
UART_SendString(debug_msg);
This constructs a message in a buffer using snprintf() (safe string formatting that prevents buffer overflows), then sends it via UART. The message will be:
[POWER] Power management initialized, voltage scale: 1
This takes about 4.5 milliseconds to transmit.
Step 5: Returning to main()
}
PowerMgmt_Init() completes and returns to main(). At this point:
- PWR peripheral has power
- Voltage scaling is set to maximum performance
- Default configuration is stored in memory
- Power management system is ready for use
What Just Happened Under the Hood
While PowerMgmt_Init() seems simple, it has set up critical infrastructure:
1. Power Control Register Access
By enabling the PWR clock, we can now access registers that control:
- PWR->CR (Control Register): Controls power modes, voltage scaling, and various enables
- PWR->CSR (Control/Status Register): Shows power status and wake-up flags
2. Voltage Regulator Configuration
The voltage scaling we configured affects the entire system:
- CPU core voltage
- Maximum achievable clock frequency
- Power consumption
- Heat generation
3. Foundation for Power Modes
Although we haven’t entered any low-power modes yet, the initialization prepares us to use:
Available Power Modes (from highest to lowest power):
- RUN: Normal operation (what we’re in now)
- RUN_LP: Reduced clock speed for power saving
- SLEEP: CPU stopped, peripherals running
- STOP: Most clocks stopped, RAM retained
- STANDBY: Lowest power, only backup domain active
4. Wake-up Infrastructure
The system is now ready to configure wake-up sources:
- Wake-up pin (PA0)
- RTC alarms and periodic wake-up
- External interrupts
Back in main() – Configuring Power Management
Right after PowerMgmt_Init() returns, main() continues with:
/* Configure power management */
PowerConfig_t powerConfig;
powerConfig.mode = POWER_MODE_RUN;
powerConfig.voltageScale = VOLTAGE_SCALE_1;
powerConfig.flashPowerDown = true;
powerConfig.enableWakeupPin = true;
powerConfig.enableBackupRegulator = false;
powerConfig.enablePVD = false;
powerConfig.pvdLevel = PVD_LEVEL_2V7;
powerConfig.wakeupSources = WAKEUP_SOURCE_RTC_WAKEUP | WAKEUP_SOURCE_PIN;
PowerMgmt_Configure(&powerConfig);
Let’s understand each parameter:
mode = POWER_MODE_RUN
This sets the initial power mode. The STM32F429ZI supports seven modes:
- POWER_MODE_RUN: Normal operation, all clocks running
- POWER_MODE_RUN_LP: CPU runs at reduced frequency to save power
- POWER_MODE_SLEEP: CPU stops, but peripherals keep running
- POWER_MODE_STOP_MR: Most clocks stop, main regulator stays on for faster wake-up
- POWER_MODE_STOP_LP: Low-power regulator, slower wake-up but less power
- POWER_MODE_STOP_LP_FPD: Same as above but also powers down flash memory
- POWER_MODE_STANDBY: Lowest power (3μA), but loses all RAM contents
voltageScale = VOLTAGE_SCALE_1
Sets the core voltage. Higher voltage allows higher clock speeds but uses more power.
flashPowerDown = true
When entering Stop mode, should we power down the flash memory?
- true: Saves more power but takes longer to wake up
- false: Uses more power but wakes up faster
enableWakeupPin = true
Enables PA0 as a wake-up pin. When the system is in a low-power mode, a rising edge on PA0 will wake it up. This is typically connected to a button or external interrupt source.
enableBackupRegulator = false
The backup regulator maintains a small portion of SRAM during Standby mode. We don’t need this for our application.
enablePVD = false
PVD (Programmable Voltage Detector) monitors the supply voltage and can trigger an interrupt if it drops below a threshold. Useful for battery-powered systems.
pvdLevel = PVD_LEVEL_2V7
If PVD were enabled, this would set the voltage threshold (2.7V in this case).
wakeupSources = WAKEUP_SOURCE_RTC_WAKEUP | WAKEUP_SOURCE_PIN
This is a bitmask specifying what can wake the system from low-power modes:
- WAKEUP_SOURCE_RTC_WAKEUP: The RTC (Real-Time Clock) can wake the system after a programmed delay
- WAKEUP_SOURCE_PIN: The wake-up pin (PA0) can wake the system
- The
|operator combines both sources
Important: Don’t confuse these two different variables!
- currentConfig (in power_management.c): Static variable that persists, holds the actual configuration
- powerConfig (in main.c): Temporary local variable used to pass configuration to PowerMgmt_Configure()
Here’s what happens:
- main() creates a temporary PowerConfig_t structure on its stack
- main() fills it with desired settings
- main() passes its address to PowerMgmt_Configure()
- PowerMgmt_Configure() copies these settings to the static currentConfig
- The temporary powerConfig in main() is discarded when main() continues
Inside PowerMgmt_Configure():
bool PowerMgmt_Configure(const PowerConfig_t* config) {
if (config == NULL) {
return false;
}
/* Update configuration */
memcpy(¤tConfig, config, sizeof(PowerConfig_t));
// ^ ^
// | |
// | +-- From the temporary config in main()
// +-- To the persistent static variable
// Apply the configuration...
}
How Wake-up Sources Work
RTC Wake-up Mechanism
The RTC runs from a separate 32.768 kHz crystal that continues running even in low-power modes. Here’s how it works:
- Before sleeping, you program the RTC wake-up timer:
PowerMgmt_SetWakeupTimer(5); // Wake up after 5 seconds - During sleep, the RTC counts down using its low-power oscillator
- Wake-up occurs when the counter reaches zero:
- RTC generates an interrupt on EXTI Line 22
- The interrupt wakes the CPU from Stop mode
- System resumes from where it left off
Wake-up Pin Mechanism
The wake-up pin (PA0) is specially designed to work in low-power modes:
- Hardware monitoring: Even in Stop/Standby mode, minimal circuitry monitors PA0
- Edge detection: A rising edge (LOW to HIGH transition) triggers wake-up
- Immediate response: Wake-up is nearly instantaneous (microseconds)
Why Multiple Wake-up Sources?
Having both RTC and pin wake-up gives flexibility:
- RTC wake-up: For periodic tasks (e.g., read sensor every 10 minutes)
- Pin wake-up: For immediate response to external events (e.g., button press)
When and Where Power Modes Are Actually Used
Looking at the complete codebase, here’s when different power modes are used:
1. In the Power Saving Task (main.c)
void Task_PowerSaving(void) {
static uint32_t counter = 0;
counter++;
/* Every 10 executions (50 seconds), enter Stop mode */
if (counter % 10 == 0) {
UART_SendString("[POWER] Automatic power saving - Entering Stop mode for 2s\r\n");
SysTick_Delay(10); // Let UART finish
PowerMgmt_ConfigureWakeupSources(WAKEUP_SOURCE_RTC_WAKEUP);
PowerMgmt_SetWakeupTimer(2); // Wake after 2 seconds
PowerMgmt_EnterLowPowerMode(POWER_MODE_STOP_LP);
// System wakes up here after 2 seconds
UART_SendString("[POWER] Woke up from Stop mode\r\n");
}
}
Why enter Stop mode here?
- Demonstrates power saving in a real application
- Shows the system can sleep and wake periodically
- Saves significant power (50μA vs 120mA)
2. User-Triggered Power Modes (main.c command interface)
case '1': // User pressed '1'
UART_SendString("\r\nEntering Sleep mode - press any key to wake up...\r\n");
SysTick_Delay(10);
PowerMgmt_EnterLowPowerMode(POWER_MODE_SLEEP);
break;
case '2': // User pressed '2'
UART_SendString("\r\nEntering Stop mode - will wake up after 5s...\r\n");
SysTick_Delay(10);
PowerMgmt_SetWakeupTimer(5);
PowerMgmt_EnterLowPowerMode(POWER_MODE_STOP_MR);
break;
Why these modes?
- Sleep mode: CPU stops but peripherals run – good for waiting for UART input
- Stop mode: Much lower power, good for battery applications
3. The Power Mode Decision Process
When PowerMgmt_EnterLowPowerMode() is called, here’s what happens:
void PowerMgmt_EnterLowPowerMode(PowerMode_t mode) {
switch (mode) {
case POWER_MODE_SLEEP:
// CPU stops, peripherals continue
// Wake on any interrupt (UART, timer, etc.)
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk; // Shallow sleep
__WFI(); // Wait For Interrupt
break;
case POWER_MODE_STOP_LP:
// Most clocks stop, RAM retained
// Wake only on configured sources
PWR->CR |= PWR_CR_LPDS; // Low-power regulator
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; // Deep sleep
__WFI(); // Wait For Interrupt
// After wake-up, clocks need reconfiguration
break;
}
}
Real-World Usage Patterns
Typical IoT Device Power Profile:
- Active period (RUN mode): Read sensors, process data, transmit – 100ms
- Sleep period (STOP mode): Wait for next measurement – 9.9 seconds
- Average power: (100mA × 0.1s + 0.05mA × 9.9s) / 10s = 1.05mA
Without power management: 100mA continuous = 95x more power!
When to use each mode:
- RUN: Active processing, communication
- SLEEP: Waiting for fast events (UART, quick timers)
- STOP: Long idle periods with periodic wake-ups
- STANDBY: Ultra-long sleep (hours/days), can afford to lose RAM
The PowerMgmt_Configure() function applies these settings, involving:
- Configuring the wake-up pin
- Setting up RTC for wake-up capability
- Storing the configuration for later use
Let’s trace what happens inside PowerMgmt_Configure():
bool PowerMgmt_Configure(const PowerConfig_t* config) {
/* Copy configuration to static variable */
memcpy(¤tConfig, config, sizeof(PowerConfig_t));
/* Configure voltage scaling */
if (!ConfigureVoltageScaling(config->voltageScale)) {
UART_SendString("[POWER] Failed to set voltage scaling\r\n");
return false;
}
/* Configure wake-up pin */
ConfigureWakeupPinPA0(config->enableWakeupPin);
/* Configure wake-up sources */
if (!PowerMgmt_ConfigureWakeupSources(config->wakeupSources)) {
UART_SendString("[POWER] Failed to configure wake-up sources\r\n");
return false;
}
/* Configure PVD if enabled */
PowerMgmt_ConfigurePVD(config->enablePVD, config->pvdLevel);
/* Configure backup regulator if needed */
if (config->enableBackupRegulator) {
PowerMgmt_EnableBackupAccess(true);
PWR->CSR |= PWR_CSR_BRE; // Enable backup regulator
while (!(PWR->CSR & PWR_CSR_BRR)); // Wait until ready
}
UART_SendString("[POWER] Configuration updated\r\n");
return true;
}
What each configuration step does:
- Wake-up Pin Configuration:
if (enable) { PWR->CSR |= PWR_CSR_EWUP; // Enable wake-up pin functionality }This connects PA0 to the wake-up circuitry that remains powered in low-power modes. - RTC Wake-up Configuration:
- Enables RTC clock (using 32.768 kHz LSI oscillator)
- Configures EXTI Line 22 for RTC wake-up interrupt
- Sets up NVIC to handle the interrupt
- EXTI Configuration for Wake-up:
EXTI->IMR |= (1 << 22); // Unmask interrupt EXTI->RTSR |= (1 << 22); // Rising edge triggerThis ensures the RTC can generate an interrupt that wakes the system.
The Power Management State Machine
After initialization, the power management system maintains several important states:
/* Current power configuration */
static PowerConfig_t currentConfig;
/* Last detected wake-up source */
static WakeupSource_t lastWakeupSource = WAKEUP_SOURCE_NONE;
/* Current power mode */
static PowerMode_t currentPowerMode = POWER_MODE_RUN;
These static variables persist throughout program execution, tracking:
- What power mode we’re in
- How the system was configured
- What woke us up (after sleeping)
How currentConfig is used throughout power_management.c:
When other functions need to check the configuration:
// In PowerMgmt_GetPowerConsumptionEstimate()
switch (currentConfig.voltageScale) {
case VOLTAGE_SCALE_1:
// No adjustment, highest power
break;
case VOLTAGE_SCALE_2:
estimate = (estimate * 80) / 100; // 20% reduction
break;
}
When entering low-power modes:
// In EnterStopMode()
if (currentConfig.flashPowerDown) {
PWR->CR |= PWR_CR_FPDS; // Power down flash
}
The static currentConfig acts as the single source of truth for all power settings, accessible by all functions in power_management.c but hidden from the rest of the system.
Summary: The Journey of currentConfig
- Declaration: Static variable in power_management.c (allocated at program start)
- Initialization: Cleared and set to defaults in PowerMgmt_Init()
- Configuration: Updated with user settings via PowerMgmt_Configure()
- Usage: Read by all power management functions to make decisions
- Lifetime: Exists until system reset
This design pattern (static configuration variable with init and configure functions) is common in embedded systems because it:
- Encapsulates state within a module
- Provides controlled access through functions
- Minimizes memory usage (no dynamic allocation)
- Ensures configuration persistence
Integration with System Features
Power management doesn’t operate in isolation. It integrates with:
SysTick Integration
When entering low-power modes, SysTick may stop. The system must account for “lost” time during sleep.
UART Integration
Before entering low-power modes, we must ensure UART transmissions complete:
UART_SendString("[POWER] Entering Stop mode...\r\n");
SysTick_Delay(10); // Let UART finish
PowerMgmt_EnterLowPowerMode(POWER_MODE_STOP_MR);
RTC Integration
The Real-Time Clock (RTC) runs from a separate low-power oscillator, allowing it to wake the system after a programmed delay.
Power Consumption Impact
The initialization we just completed enables dramatic power savings:
| Mode | Typical Current | Use Case |
|---|---|---|
| RUN @ 180MHz | 120mA | Active processing |
| RUN @ 16MHz | 15mA | Low-speed operation |
| SLEEP | 15mA | Waiting for interrupts |
| STOP | 50µA | Deep sleep, quick wake |
| STANDBY | 3µA | Deepest sleep |
The difference between RUN and STANDBY is a factor of 40,000! This is why power management is crucial for battery-powered devices.
What’s Next in main()
After power management configuration, the next major initialization will be the Task Scheduler. But power management will be actively used throughout the program:
- Tasks will periodically enter Stop mode to save power
- The system will wake on RTC timer or external events
- Debug messages will report each power state transition
The Complete Initialization Picture So Far
We now have four major subsystems initialized:
- Vector Table Verification: Ensures interrupts work correctly
- SysTick: Provides 1ms timing heartbeat
- UART: Enables debug communication
- Power Management: Controls system power states
Each builds on the previous:
- Power management uses UART for status messages
- Power management uses SysTick for delays before sleeping
- Power management relies on properly configured interrupts for wake-up
The system is becoming more sophisticated with each initialization, preparing for the task scheduler that will orchestrate everything.
Next: Part 6 – Task Scheduler Initialization →
This is Part 5 of the STM32 IoT Framework series.
Pingback: Part 6: The Ultimate Guide to Effortless MCU Scheduling - Learn By Building