Configurazione RCC su STM32: HSE, PLL e Flash Latency dai registri

2026-05-31 · Davide Carrese
STM32 · RCC · Firmware Architecture
Commenti

Ogni progetto STM32 che opera al di sopra della frequenza HSI predefinita deve configurare il clock tree RCC. Sbagliare la sequenza — impostare i divisori del PLL prima che l'oscillatore sia pronto, commutare il clock di sistema prima di aver configurato i wait-state della flash, o dimenticare di abilitare il PLL — provoca un hard fault silenzioso o un chip che resta a 16 MHz invece di 168. Questo articolo descrive la sequenza a livello di registri per la famiglia STM32F4, con note sulle differenze per STM32G4, L4 e U5.

Perché configurare l'RCC dai registri

CubeMX e il template HAL SystemClock_Config() vanno bene per il prototipo. Ma in produzione spesso serve:

Conoscere la sequenza dei registri permette di leggere lo stato RCC e diagnosticare i guasti in minuti anziché per tentativi.

Clock tree RCC: mappa di alto livello

Tutte le famiglie STM32 condividono lo stesso albero concettuale. Al reset il chip parte da HSI (High-Speed Internal, tipicamente 8 MHz su F4/L4, 16 MHz su G4/U5). Da lì si può:

Sequenza di avvio corretta (a livello di registri)

La sequenza va seguita in quest'ordine esatto. Qualsiasi deviazione rischia un hard fault quando la CPU cerca di eseguire la prossima istruzione a una frequenza che la flash non può sostenere.

Passo 1: Abilitare HSE e attendere il ready

RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY)) { /* attendi */ }

L'oscillatore HSE impiega qualche centinaio di microsecondi a stabilizzarsi, a seconda della capacità di carico del cristallo. Il bit ready viene impostato dall'hardware. Non procedere finché non è attivo. Se la scheda non ha un cristallo esterno, HSE non diventerà mai ready e dovrai usare HSI come sorgente PLL.

Passo 2: Configurare i flash wait-state

Questo va fatto prima di aumentare la frequenza del clock di sistema. La memoria flash ha una velocità massima di accesso. Su STM32F401/411 a 3.3 V:

FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_LATENCY_5WS;

Il prefetch buffer, la cache istruzioni e la cache dati sono indipendenti dalla latenza ma vanno abilitati dopo aver impostato i wait-state. Su STM32G4 e STM32U5, l'ART accelerator sostituisce la cache legacy — stesso principio: configura prima la latenza, poi abilita gli acceleratori.

Passo 3: Configurare i divisori PLL

Su un STM32F401 con HSE da 8 MHz, target 84 MHz SYSCLK:

// PLLM = 8, PLLN = 336, PLLP = 4  →  SYSCLK = 8 / 8 * 336 / 4 = 84 MHz

RCC->PLLCFGR = (8 << RCC_PLLCFGR_PLLM_Pos)    // M = 8
              | (336 << RCC_PLLCFGR_PLLN_Pos)   // N = 336
              | (0 << RCC_PLLCFGR_PLLP_Pos)     // P = 4  (0b00 mappa a P=4)
              | RCC_PLLCFGR_PLLSRC_HSE;          // Sorgente = HSE

Il registro PLLCFGR può essere modificato solo quando il PLL è disabilitato. Scriverci mentre il PLL è attivo non ha effetto sulla maggior parte delle famiglie STM32.

Passo 4: Abilitare il PLL e attendere il lock

RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY)) { /* attendi */ }

Il tempo di lock del PLL è tipicamente 50–200 µs a seconda della frequenza d'ingresso e del range VCO. Esegui il polling del bit ready — non usare un ritardo fisso.

Passo 5: Configurare i prescaler AHB/APB

Imposta i prescaler prima di commutare SYSCLK in modo che le frequenze dei bus siano definite al momento della commutazione.

// HPRE = 1 (nessuna divisione), PPRE1 = 2 (APB1 = HCLK/2), PPRE2 = 1 (APB2 = HCLK)
RCC->CFGR = RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE1_DIV2 | RCC_CFGR_PPRE2_DIV1;

Passo 6: Commutare il clock di sistema sul PLL

RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW_Msk) | RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL) { /* attendi */ }

Controlla i bit di stato (SWS), non solo i bit di selezione (SW). L'hardware impiega alcuni cicli per migrare la sorgente del clock. Leggere SWS conferma che la commutazione è completata con successo.

Esempio pratico: STM32F411 da HSI a 100 MHz

Alcune schede non hanno un cristallo HSE (es. WeAct STM32F411CEU6 "Black Pill"). Bisogna usare il PLL alimentato da HSI (16 MHz su F411):

void SystemClock_HSI_100MHz(void) {
    // 1. Flash: 3 wait-state per 100 MHz
    FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN
               | FLASH_ACR_LATENCY_3WS;

    // 2. PLL: HSI16 / 8 * 100 / 2 = 100 MHz
    // Su STM32F411 PLLP=0b00 mappa a P=2
    RCC->PLLCFGR = (8 << RCC_PLLCFGR_PLLM_Pos)
                 | (100 << RCC_PLLCFGR_PLLN_Pos)
                 | (0 << RCC_PLLCFGR_PLLP_Pos)
                 | RCC_PLLCFGR_PLLSRC_HSI;

    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY));

    // 3. Prescaler AHB/APB
    RCC->CFGR = RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE1_DIV2 | RCC_CFGR_PPRE2_DIV1;

    // 4. Commutazione
    RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW_Msk) | RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL);
}

Importante: La codifica di PLLP cambia tra famiglie. Su F4, PLLP=0b00 → P=4 (minimo), mentre su F411 la stessa codifica dà P=2. Verifica sempre la mappa dei divisori PLLP nel reference manual del tuo target. Un valore PLLP sbagliato produce la frequenza errata ma il PLL si aggancia lo stesso — il bug si nasconde finché non misuri l'uscita.

Flash latency per famiglia: cosa cambia

Le tabelle di latenza flash differiscono tra famiglie STM32. Ecco le sezioni chiave dei reference manual:

Modalità di guasto comuni

Hard fault immediatamente dopo la commutazione

Quasi sempre causato da wait-state flash insufficienti. La CPU esegue un fetch a 168 MHz, la flash consegna dati a 30 MHz, e il bus riceve dati errati — tipicamente un HardFault alla prima istruzione dopo il SWS check. Soluzione: aumentare LATENCY prima di commutare.

Il PLL non si aggancia mai

Verifica che la frequenza VCO del PLL sia nel range valido (tipicamente 100–432 MHz per F4, 64–344 MHz per G4). Se N × HSE / M è fuori dal range VCO, il PLL non attiverà mai RDY. Leggi RCC->PLLCFGR e calcola la frequenza VCO manualmente.

Periferica APB1 alla frequenza sbagliata

I timer alimentati da APB1 (TIM2–TIM7 su F4) contano al doppio della frequenza del bus APB1 quando il prescaler APB1 non è 1. Se PPRE1 = 2, il clock del timer è 2 × APB1. Un errore comune con CubeMX: l'utente imposta il prescaler del TIM assumendo APB1 = 42 MHz, ma PPRE1 = 4 dimezza il bus a 21 MHz e il timer conta comunque a 42 MHz. Calcola sempre: APB1_timer_clock = HCLK / ppres1 * (ppres1 == 1 ? 1 : 2).

Checklist pratica

Come lo affronterei su un progetto cliente

Su un progetto firmware in produzione, non incorporo mai un singolo SystemClock_Config() hardcodato. Scrivo invece una struttura di configurazione che trasporta frequenza target, WS flash e divisori PLL come macro compile-time:

typedef struct {
    uint32_t pll_m;
    uint32_t pll_n;
    uint32_t pll_p;
    uint32_t pll_q;
    uint8_t  flash_latency;
    uint8_t  hpre, ppre1, ppre2;
} rcc_config_t;

static const rcc_config_t RCC_CFG_84MHZ = {
    .pll_m = 8, .pll_n = 336, .pll_p = 4, .pll_q = 7,
    .flash_latency = FLASH_ACR_LATENCY_3WS,
    .hpre = RCC_CFGR_HPRE_DIV1,
    .ppre1 = RCC_CFGR_PPRE1_DIV2,
    .ppre2 = RCC_CFGR_PPRE2_DIV1,
};

int rcc_apply(const rcc_config_t *cfg);  // restituisce 0 in caso di successo

Questa struttura vive in un modulo rcc.c dedicato con i suoi test unitari (verificati contro la tabella del reference manual). Quando il cliente cambia il cristallo o modifica la frequenza target, modifica un header, ricompila e verifica con un analizzatore logico su MCO — niente CubeMX, niente errori di copia-incolla.

Fonti e approfondimenti

Commenti

Hai commenti? Scrivimi un'email.