Lezione 36
🎓 PIC16 — From Zero to Hero · Lezione 36 · Modulo 6
Ottimizzazione e Profiling del Codice ⚡
Contare i cicli macchina con TMR0. ISR a latenza minima con il trick SWAPF.
Bank switching raggruppato. Inline vs subroutine. Loop unrolling.
🎯 Cosa impari in questa lezione
- Costo in cicli di ogni categoria di istruzione PIC16: 1 ciclo per ALU/MOV/bit, 2 per GOTO/CALL/RETURN e per ogni skip preso
- Profiling con TMR0 come cronometro: leggi TMR0 prima e dopo il codice da misurare, sottrai → cicli esatti
- ISR a latenza minima: il trick SWAPF salva STATUS senza modificare Z/C, risparmiando 1 ciclo rispetto al metodo naive con MOVF
- Bank switching raggruppato: tutti gli accessi a Bank1 in un solo blocco BSF/BCF → elimina cambi di banco ripetuti (risparmio fino al 25%)
- Inline vs subroutine: CALL+RETURN = 4 cicli overhead fisso — inline vince con corpo <3 istruzioni o <4 chiamate, subroutine vince per Flash
- Loop unrolling ×4: riduce overhead DECFSZ+GOTO da 3 cicli ogni iterazione a 0.75 cicli → 21% più veloce, 4× più Flash
- Principio di Amdahl applicato al PIC: ottimizzare il 10% del codice che gira il 90% del tempo
📋 Scheda lezione
Lezione: 36 / 42
Modulo: 6 — Tecniche avanzate
Device: PIC16F877A
Prerequisiti: Lez. 16 (TMR0), Lez. 21 (ISR)
Difficoltà: ★★★★☆
Tecnica: profiling, ottimizzazione cicli
Applicazioni: real-time, bit-banging, DSP
1 · Costo in cicli: la tabella di riferimento
Con Fosc=4MHz ogni ciclo macchina dura 1µs (4 periodi di clock ÷ 4 = 1 ciclo = 1µs). La pipeline a 2 stadi del PIC16 fa sì che quasi tutte le istruzioni completino in 1 ciclo — ma ogni trasferimento di controllo (GOTO, CALL, RETURN) e ogni skip preso richiedono 2 cicli perché invalidano la fase di fetch già avviata. Conoscere questi costi a memoria è il primo passo per scrivere codice real-time affidabile.
2 · Profiling con TMR0 — misura cicli esatti
TMR0 è un contatore a 8 bit che avanza a ogni ciclo macchina (con prescaler al WDT e clock interno). Configurando OPTION_REG con PSA=1 (prescaler assegnato al WDT, non a TMR0), TMR0 conta 1 tick = 1µs = 1 ciclo. La tecnica: salva TMR0 prima del codice da misurare, riesegui il codice, leggi TMR0 di nuovo, sottrai. Il risultato è il numero esatto di cicli macchina — inclusa la latenza di lettura stessa (2 cicli per le due MOVF TMR0).
⏱ Limite TMR0 come cronometro: TMR0 è un registro da 8 bit — overflow a 256µs. Per misurare codice che dura più di 256µs, usa TMR1 (16 bit, overflow a 65535µs = 65ms). Per codice <256 cicli TMR0 è perfetto. Attenzione: leggere TMR0 due volte costa ~2 cicli che vanno sottratti manualmente dal risultato per avere la misura pulita del codice sotto test.
; ============================================================ ; Lezione 36 — Contare i cicli macchina: profiling manuale ; PIC16F877A @ 4 MHz ; Ogni istruzione = 1 ciclo = 1µs tranne GOTO/CALL/RETURN/ ; RETLW/RETFIE/DECFSZ-skip/INCFSZ-skip = 2 cicli ; Tecnica: usa TMR0 come cronometro, leggi prima e dopo ; TMR0 conta a Fosc/4 (=1MHz), prescaler OFF → 1 tick = 1µs ; ============================================================ LIST P=16F877A #INCLUDE <p16f877a.inc> __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF CBLOCK 0x20 T_START ; valore TMR0 all'inizio T_END ; valore TMR0 alla fine T_ELAPSED ; cicli trascorsi LOOP_CNT ACC FAST_RES ; risultato versione veloce SLOW_RES ; risultato versione lenta ENDC ORG 0x0000 GOTO INIT ORG 0x0030 INIT BSF STATUS,RP0 CLRF TRISB ; TMR0: prescaler off (assegnato a WDT), clock interno MOVLW b'11001000' ; T0CS=0 (interno), PSA=1 (prescaler→WDT) MOVWF OPTION_REG BCF STATUS,RP0 ; ============================================================ ; PROFILING — misura cicli di due versioni dello stesso calcolo ; CALCOLO: somma di 10 + 20 + 30 + 40 + 50 = 150 ; Versione A: addizioni inline (veloce, usa più Flash) ; Versione B: loop con tabella (lento, usa meno Flash) ; ============================================================ ; --- Versione A: inline (no loop) --- MOVF TMR0,W MOVWF T_START ; t0 CLRF ACC MOVLW D'10' ADDWF ACC,F ; +10 MOVLW D'20' ADDWF ACC,F ; +20 MOVLW D'30' ADDWF ACC,F ; +30 MOVLW D'40' ADDWF ACC,F ; +40 MOVLW D'50' ADDWF ACC,F ; +50 ; 11 istruzioni × 1 ciclo = 11µs MOVF TMR0,W MOVWF T_END ; t1 MOVF T_START,W SUBWF T_END,W MOVWF T_ELAPSED ; dovrebbe essere ~13 (11 + 2 per le MOVFs) MOVF ACC,W MOVWF FAST_RES ; salva risultato (150) ; --- Versione B: loop (5 iterazioni) --- MOVF TMR0,W MOVWF T_START CLRF ACC MOVLW D'5' MOVWF LOOP_CNT MOVLW D'10' ; valore iniziale SLOW_LOOP ADDWF ACC,F ; accumula ADDLW D'10' ; prossimo valore (+10) DECFSZ LOOP_CNT,F ; 1 ciclo (normale) / 2 (skip) GOTO SLOW_LOOP ; 2 cicli ; Per iterazione: 1+1+1+2 = 5 cicli (senza skip) + 2 (skip finale) ; 4 iter × 5 + 1 × 6 = 26 cicli + setup ≈ 33µs totali MOVF TMR0,W MOVWF T_END MOVF T_START,W SUBWF T_END,W MOVWF T_ELAPSED ; ~35 cicli vs ~13 della versione inline MOVF ACC,W MOVWF SLOW_RES ; Versione A vince: 13µs vs ~35µs — 2.7× più veloce ; Ma usa 11 istruzioni vs 8 → tradeoff Flash/velocità ; ============================================================ ; OTTIMIZZAZIONE loop: DECFSZ vs SUBWF+BTFSS ; Misura overhead chiamata subroutine vs inline ; ============================================================ MOVF TMR0,W MOVWF T_START ; 100 NOP inline — benchmark base: 100 cicli ; (dimostrato con 10 NOP × CALL×10 = overhead call) MOVLW D'10' MOVWF LOOP_CNT NOP_LOOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP ; 10 NOP = 10 cicli per iter DECFSZ LOOP_CNT,F ; 1 ciclo GOTO NOP_LOOP ; 2 cicli → 13 cicli/iter × 10 = 130 ; + skip finale = 2 → totale ~132 cicli MOVF TMR0,W MOVWF T_END MOVF T_START,W SUBWF T_END,W MOVWF T_ELAPSED ; ~134 cicli (2 per letture TMR0) DONE GOTO DONE END
3 · ISR a bassa latenza: il trick SWAPF
Il problema classico del salvataggio STATUS nell’ISR: MOVF STATUS,W legge STATUS ma modifica il flag Z — corrompendo esattamente il registro che si vuole salvare. La soluzione è SWAPF STATUS,W: l’istruzione scambia i nibble alto e basso di STATUS e mette il risultato in W, senza toccare nessun flag. Al ripristino, un secondo SWAPF ri-scambia i nibble riportando STATUS all’originale. Per W il trucco del doppio SWAPF-in-place è ugualmente elegante: SWAPF W_SHADOW,F poi SWAPF W_SHADOW,W restituisce W originale senza passare per STATUS.
; ============================================================ ; Lezione 36 — ISR a bassa latenza: tecniche di ottimizzazione ; PIC16F877A @ 4 MHz ; Latenza ISR standard: 3+1+2+2 = 8 cicli (vettore→primo utile) ; Con salvataggio W/STATUS: +4 cicli → 12 cicli prima del codice ; Obiettivo: ridurre al minimo con shadow registers e SWAPF trick ; ============================================================ LIST P=16F877A #INCLUDE <p16f877a.inc> __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF ; Area shared: accessibile da qualsiasi banco senza cambiare RP0 #DEFINE W_SHADOW 0x70 ; salvataggio W #DEFINE ST_SHADOW 0x71 ; salvataggio STATUS CBLOCK 0x20 ISR_COUNT ; contatore interrupt EDGE_TIME_H ; timestamp interrupt alto EDGE_TIME_L ; timestamp interrupt basso PERIOD_H ; periodo misurato (alto) PERIOD_L ; periodo basso PREV_H PREV_L PULSE_COUNT FLAGS ; bit0=nuovo_dato, bit1=overflow ENDC ORG 0x0000 GOTO INIT ; ============================================================ ; ISR OTTIMIZZATA — latenza minima ; Trick SWAPF: non modifica STATUS (a differenza di MOVF) ; Ordine: salva W con MOVWF (1c), poi STATUS con SWAPF (1c) ; Nessuna PUSH/POP: tutto in locazioni fisse shared ; ============================================================ ORG 0x0004 ISR_FAST ; --- SALVATAGGIO CONTESTO: 3 istruzioni --- MOVWF W_SHADOW ; salva W (1c, non tocca STATUS) SWAPF STATUS,W ; leggi STATUS nibble-swappato (1c, non tocca Z/C!) MOVWF ST_SHADOW ; salva STATUS (1c) ; Totale salvataggio: 3 cicli (vs 4 del metodo naive) BCF STATUS,RP0 ; assicura Bank 0 ; --- IDENTIFICA SORGENTE --- BTFSS INTCON,INTF ; INT0 (RB0)? GOTO CHECK_TMR0 ; --- GESTIONE INT0: misura periodo impulso --- BCF INTCON,INTF ; azzera flag ; Leggi TMR1 come timestamp (16 bit) MOVF TMR1L,W MOVWF EDGE_TIME_L MOVF TMR1H,W MOVWF EDGE_TIME_H ; Calcola periodo = TIME_CURR - TIME_PREV MOVF PREV_L,W SUBWF EDGE_TIME_L,W MOVWF PERIOD_L MOVF PREV_H,W BTFSS STATUS,C INCFSZ PREV_H,W ; borrow dalla parte bassa SUBWF EDGE_TIME_H,W MOVWF PERIOD_H ; Aggiorna PREV MOVF EDGE_TIME_L,W MOVWF PREV_L MOVF EDGE_TIME_H,W MOVWF PREV_H ; Segnala nuovo dato BSF FLAGS,0 INCF PULSE_COUNT,F GOTO ISR_EXIT CHECK_TMR0 BTFSS INTCON,T0IF GOTO ISR_EXIT BCF INTCON,T0IF INCF ISR_COUNT,F GOTO ISR_EXIT ISR_EXIT ; --- RIPRISTINO CONTESTO: 3 istruzioni --- SWAPF ST_SHADOW,W ; ri-nibble-swap → STATUS originale MOVWF STATUS ; ripristina STATUS (1c) SWAPF W_SHADOW,F ; prepara W: swap in place (1c) SWAPF W_SHADOW,W ; ri-swap → W originale (1c, non tocca STATUS) RETFIE ; 2 cicli ; Totale ripristino: 5 cicli (stessa latenza di ritorno) ORG 0x0060 INIT ; Configura TMR1 come contatore libero (timestamp) CLRF T1CON ; TMR1 off CLRF TMR1H CLRF TMR1L MOVLW b'00000001' ; TMR1ON MOVWF T1CON ; INT0 su RB0: fronte di salita BSF STATUS,RP0 MOVLW b'11111111' MOVWF TRISB ; RB0 input BCF STATUS,RP0 CLRF FLAGS CLRF PULSE_COUNT CLRF ISR_COUNT CLRF PREV_H CLRF PREV_L ; Abilita INT0 e TMR0 MOVLW b'10110000' ; GIE=1, T0IE=1(bit5), INTE=1(bit4) MOVWF INTCON ; ============================================================ ; MAIN — polling FLAGS e output ; ============================================================ MAIN BTFSS FLAGS,0 ; nuovo dato? GOTO MAIN BCF FLAGS,0 ; Usa PERIOD_H:PERIOD_L — frequenza = 1.000.000 / PERIOD ; Esempio: PERIOD=1000 → 1kHz MOVF PERIOD_L,W MOVWF PORTB ; mostra byte basso periodo (proxy freq) GOTO MAIN END
4 · Bank switching, inline vs subroutine, loop unrolling
; ============================================================ ; Lezione 36 — Bank switching efficiente e inline vs subroutine ; PIC16F877A @ 4 MHz ; Regola d'oro: raggruppa accessi allo stesso banco per minimizzare ; il numero di BSF/BCF STATUS,RP0 (ogni cambio = 1 ciclo) ; Inline vs subroutine: CALL+RETURN = 4 cicli overhead fisso ; ============================================================ LIST P=16F877A #INCLUDE <p16f877a.inc> __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF CBLOCK 0x20 CONFIG_REG1 ; valore da scrivere in Bank1 CONFIG_REG2 RESULT1 RESULT2 RESULT3 TMP ENDC ORG 0x0000 GOTO INIT ORG 0x0040 INIT ; ============================================================ ; ESEMPIO A — bank switching INEFFICIENTE (da evitare) ; Ogni accesso a Bank1 causa un doppio cambio banco ; ============================================================ ; Cicli: 1+1+1+1+1+1+1+1+1+1 = 10 cambi banco = 10 cicli sprecati MOVLW 0x07 ; configura ADCON1 (Bank1) BSF STATUS,RP0 ; → Bank1 [1c] MOVWF ADCON1 ; scrivi [1c] BCF STATUS,RP0 ; → Bank0 [1c] MOVLW 0x00 ; configura TRISD (Bank1) BSF STATUS,RP0 ; → Bank1 [1c] ← inutile! MOVWF TRISD ; scrivi [1c] BCF STATUS,RP0 ; → Bank0 [1c] MOVLW 0xFF ; configura TRISB (Bank1) BSF STATUS,RP0 ; → Bank1 [1c] ← inutile! MOVWF TRISB ; scrivi [1c] BCF STATUS,RP0 ; → Bank0 [1c] ; Totale: 12 istruzioni, 12 cicli, 6 cambi banco ; ============================================================ ; ESEMPIO B — bank switching OTTIMIZZATO (raggruppato) ; Un solo cambio banco per tutti gli accessi a Bank1 ; ============================================================ BSF STATUS,RP0 ; → Bank1 [1c] ← UN SOLO cambio MOVLW 0x07 MOVWF ADCON1 ; [1c] MOVLW 0x00 MOVWF TRISD ; [1c] MOVLW 0xFF MOVWF TRISB ; [1c] BCF STATUS,RP0 ; → Bank0 [1c] ← UN SOLO ritorno ; Totale: 9 istruzioni, 9 cicli, 2 cambi banco ; Risparmio: 3 istruzioni e 3 cicli (il 25% in meno!) ; ============================================================ ; INLINE vs SUBROUTINE — tradeoff velocità/spazio ; Funzione: calcola val XOR 0xAA e scrivi su PORTB ; ============================================================ ; --- Inline (chiamata 3 volte) --- ; Costo: 3 × 3 istruzioni = 9 word Flash, 9 cicli MOVLW 0x55 XORLW 0xAA ; 0xFF MOVWF PORTB ; [3 istr, 3c] MOVLW 0x0F XORLW 0xAA ; 0xA5 MOVWF PORTB ; [3 istr, 3c] MOVLW 0x3C XORLW 0xAA ; 0x96 MOVWF PORTB ; [3 istr, 3c] ; Flash usata: 9 word, Tempo: 9 cicli ; --- Subroutine (chiamata 3 volte) --- ; Costo: 3 × (MOVLW + CALL) + corpo 2 istr = 3×2+2 = 8 word Flash ; Tempo: 3 × (1+2+2+2) = 21 cicli (MOVLW + CALL + corpo + RETURN) MOVLW 0x55 CALL XOR_OUT ; [1+2+2+2 = 7c] MOVLW 0x0F CALL XOR_OUT ; [7c] MOVLW 0x3C CALL XOR_OUT ; [7c] ; Flash usata: 8 word (meno!), Tempo: 21 cicli (peggio!) ; CALL vale la pena solo con corpo ≥ 3 istruzioni o ≥ 4 chiamate ; ============================================================ ; OTTIMIZZAZIONE LOOP CRITICO — riduzione istruzioni per ciclo ; Goal: output massima velocità possibile su PORTD ; Versione 1: loop normale (overhead alto) ; Versione 2: loop unrolled parzialmente ; ============================================================ ; Versione 1: loop normale, 256 iterazioni CLRF TMP BCF STATUS,RP0 BSF STATUS,RP0 CLRF TRISD BCF STATUS,RP0 LOOP_V1 MOVF TMP,W MOVWF PORTD ; output INCF TMP,F DECFSZ TMP,W ; trucco: decrementa copia in W, non TMP GOTO LOOP_V1 ; 1+1+1+1+2 = 6 cicli/iter × 256 = 1536c ; Nota: DECFSZ su W=0 è inutile, meglio usare INCF con overflow ; Versione corretta: usa il fatto che INCF su 255 torna a 0 (overflow) CLRF TMP LOOP_V2 MOVF TMP,W MOVWF PORTD INCF TMP,F ; 255+1=0, Z=1 BTFSS STATUS,Z ; se non zero, continua GOTO LOOP_V2 ; 1+1+1+1+2 = 6 cicli/iter − nessun miglioramento ; Versione 3: unroll × 4 (meno overhead, più Flash) CLRF TMP LOOP_V3 MOVF TMP,W MOVWF PORTD ; iter 0 INCF TMP,F MOVF TMP,W MOVWF PORTD ; iter 1 INCF TMP,F MOVF TMP,W MOVWF PORTD ; iter 2 INCF TMP,F MOVF TMP,W MOVWF PORTD ; iter 3 INCF TMP,F BTFSS STATUS,Z ; ogni 4 iter controlla overflow (TMP==0) GOTO LOOP_V3 ; 4+4+4+4+1+2 = 4×(1+1) + check = 4 iter × 4c + 3c overhead/4 = ~4.75c/iter ; vs 6c/iter della V1 → 21% più veloce, usa 4× Flash per il corpo MAIN GOTO MAIN XOR_OUT XORLW 0xAA MOVWF PORTB RETURN END
🧪 Prova tu
- Carica il profiling TMR0. Esegui la versione A (inline). T_ELAPSED dovrebbe valere circa 13–14 (11 istruzioni + 2 overhead letture TMR0 + 1 GOTO init). Poi esegui la versione B (loop). T_ELAPSED dovrebbe essere circa 33–36. Calcola il rapporto: quante volte è più lenta? Spiega perché con il conteggio dei cicli per iterazione.
- Modifica la ISR ottimizzata: aggiungi un secondo sorgente interrupt (TMR0). Usa la stessa struttura SWAPF per salvataggio/ripristino. Misura la latenza aggiuntiva (cicli sprecati nel check BTFSS INTCON,INTF prima di arrivare a CHECK_TMR0). Come ridurla se TMR0 è il sorgente più frequente? (Suggerimento: metti il controllo più frequente per primo.)
- Implementa un ritardo preciso di esattamente 500µs senza timer hardware, solo con NOP e loop. Calcola: quanti loop da X cicli ciascuno? Verifica con TMR0 che T_ELAPSED sia 500±2. (A 4MHz, 500µs = 500 cicli.)
- Sfida: ottimizza questo loop critico per la massima velocità su PORTD. Il loop deve scrivere i valori 0x00, 0x55, 0xAA, 0xFF in sequenza continua. Versione naive: 4 MOVLW + 4 MOVWF + GOTO = ~12 cicli/ciclo-output. Versione ottimizzata con tabella RETLW e contatore di fase: quanti cicli? Versione con scrittura diretta senza loop: quanti cicli per 4 output?
❓ Quiz rapido
1. Perché MOVF STATUS,W non va usato per salvare STATUS nell’ISR, e come fa SWAPF STATUS,W a risolvere il problema?
Mostra risposta
MOVF imposta il flag Z in STATUS in base al valore letto (Z=1 se STATUS=0x00, Z=0 altrimenti). Questo significa che il bit Z di STATUS viene modificato dall’atto stesso di leggerlo — una contraddizione: vuoi salvare STATUS, ma il salvataggio lo corrompe. SWAPF invece esegue uno scambio dei nibble (bit7:4 ↔ bit3:0) e non modifica nessun flag: C, Z, DC rimangono invariati. L’unico effetto è che il valore in W ha i nibble scambiati — ma questo non importa perché al ripristino un secondo SWAPF riscambia, restituendo il valore originale. Il doppio SWAPF è in sostanza un’operazione nulla sulla sequenza completa.
2. Un loop ha corpo da 2 istruzioni e viene chiamato 10 volte. Conviene inline o subroutine? E se il corpo fosse 8 istruzioni?
Mostra risposta
Con corpo da 2 istruzioni: inline costa 2×10=20 cicli e 20 word Flash. Subroutine costa (1+2)×10 + 2+2 = 34 cicli (MOVLW+CALL per ogni chiamata + corpo+RETURN) e 2+4=6 word Flash. Inline è più veloce (20 vs 34) ma usa più Flash (20 vs 6). Con corpo da 8 istruzioni: inline = 8×10=80 cicli, 80 word Flash. Subroutine = (1+2+8+2)×10=130 cicli, ma solo 8+2=10 word Flash. Inline è ancora più veloce (80 vs 130 cicli), ma subroutine usa 8× meno Flash. La regola “subroutine è più veloce con più chiamate” è spesso un malinteso: la subroutine risparmia sempre Flash con N≥2 chiamate, ma è sempre più lenta in cicli a causa dei 4 cicli fissi di CALL+RETURN. Se la velocità è prioritaria, inline quasi sempre vince.
3. Il loop con DECFSZ e GOTO ha overhead di 3 cicli ogni iterazione (1 per DECFSZ + 2 per GOTO). Con unroll ×4 l’overhead scende a ~0.75 cicli/iterazione. Qual è il limite dell’unrolling e quando smette di essere conveniente?
Mostra risposta
L’unrolling è conveniente finché il risparmio in cicli supera il costo in Flash. Con corpo da 2 istruzioni: unroll ×4 usa 4×2+3=11 word per 4 iter invece di 2+3=5 word per 1 iter (×4=20 word) — quindi risparmia Flash E cicli, ottimo. Ma con unroll ×256 (= nessun loop) per 256 iterazioni, si usa 256×2=512 word di Flash solo per il corpo: su un PIC16F84A con 1024 word totali è il 50% della Flash solo per un loop. Il limite pratico è che la Flash del programma deve rimanere utilizzabile, e che un loop unrolled ×N richiede che N divida esattamente il numero di iterazioni. Un ulteriore limite: con unroll molto aggressivo si perde la località di istruzioni nella cache istruzioni (pipeline), anche se su PIC16 senza cache questo conta meno.
Prossima lezione →
Lezione 37 — Bit-banging: protocolli software
Implementare 1-Wire, SPI software e I²C software senza periferiche hardware. Timing preciso con NOP. Dallas DS18B20 temperatura via 1-Wire. Tutto con le istruzioni che conosci già.
© Prof. D. Bertolino · PIC16 From Zero to Hero · WebPicSimulator · Lezione 36