Lezione 27
🎓 PIC16 — From Zero to Hero · Lezione 27 · Modulo 5
PWM hardware con CCP ⚙️
Il modulo CCP1 del PIC16F877A in modalità PWM. Duty cycle a 10 bit,
dimmer LED controllato da ADC e controllo servo con segnale RC a 50Hz.
🎯 Cosa impari in questa lezione
- Principio del PWM: frequenza fissa, duty cycle variabile → tensione media variabile
- Registri del modulo CCP1:
CCP1CON,CCPR1L,T2CON,PR2 - Formula frequenza:
Fpwm = Fosc / (4 × (PR2+1) × prescaler) - Duty cycle a 10 bit spalmato su CCPR1L (bit9:2) e CCP1CON bit5:4 (bit1:0)
- Applicazione: dimmer LED con potenziometro su AN0 che controlla duty CCP1
- Segnale servo RC: periodo 20ms (50Hz), impulso 1–2ms per posizione 0°–180°
- PWM software con Timer1 + interrupt per frequenze non raggiungibili dall’hardware
📋 Scheda lezione
Lezione: 27 / 42
Modulo: 5 — Periferiche avanzate
Device: PIC16F877A
Prerequisiti: Lez. 16 (Timer0), Lez. 26 (ADC)
Difficoltà: ★★★★☆
Periferica: CCP1 (Compare/Capture/PWM)
Applicazioni: LED dimmer, servo motore
1 · Il segnale PWM — frequenza e duty cycle
Il PWM (Pulse Width Modulation) genera un segnale digitale on/off a frequenza fissa, variando la percentuale di tempo in cui il segnale è alto (duty cycle). Un motore DC connesso a un PWM all’80% riceve in media l’80% della tensione di alimentazione e gira all’80% della velocità massima. Sul PIC16F877A il modulo CCP1 genera il PWM in hardware autonomamente — la CPU non interviene durante la generazione, il segnale è perfettamente preciso.
2 · Registri CCP1 e duty cycle a 10 bit
Perché 10 bit per il duty cycle? Con PR2=124 il periodo è 500 “step” (4×125). Per poter impostare qualsiasi valore da 0 a 500 serve almeno ceil(log2(500)) = 9 bit. Microchip ha scelto 10 bit per avere margine — il valore massimo utile con PR2=124 è 500 (duty 100%), non 1023. Valori oltre 500 saturano al 100%. Con PR2=255 il periodo sale a 1024 step e si sfruttano tutti i 10 bit.
3 · Codice — PWM fisso 8kHz 50% duty
; ============================================================ ; Lezione 27 — PWM hardware con CCP1 ; PIC16F877A @ 4 MHz Timer2 come base tempi PWM ; Frequenza PWM: Fpwm = Fosc / (4 × (PR2+1) × TMR2 prescaler) ; Con PR2=124, prescaler=1: Fpwm = 4MHz / (4×125×1) = 8 kHz ; Duty cycle: CCPR1L:CCP1CON<5:4> = valore 10 bit ; ============================================================ LIST P=16F877A #INCLUDE <p16f877a.inc> __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF ; Registri CCP/PWM: ; CCP1CON (0x17): DCB1 DCB0 CCP1M3 CCP1M2 CCP1M1 CCP1M0 ; CCP1M=1100 → PWM mode ; DCB1:DCB0 = bit1:0 del duty cycle (10 bit totali) ; CCPR1L (0x15): bit9:2 del duty cycle ; T2CON (0x12): TOUTPS3-0 TMR2ON T2CKPS1 T2CKPS0 ; PR2 (0x92 Bank1): periodo PWM ORG 0x0000 GOTO INIT ORG 0x0005 INIT ; Configura RC2/CCP1 come output (pin PWM) BSF STATUS,RP0 BCF TRISC,2 ; RC2 = output ; PR2 = 124 → Fpwm = 8kHz con prescaler=1 MOVLW D'124' MOVWF PR2 BCF STATUS,RP0 ; CCP1CON: mode PWM (CCP1M=1100), DCB=00 MOVLW b'00001100' ; PWM mode, LSB duty=00 MOVWF CCP1CON ; Duty cycle 50%: valore = (PR2+1) × 2 = 250 → CCPR1L=62, DCB=10 ; Formula: DC_10bit = duty% × 4 × (PR2+1) / 100 ; 50% → 0.5 × 4 × 125 = 250 → CCPR1L=62 (250>>2), DCB=10 (250&3=2) MOVLW D'62' MOVWF CCPR1L MOVLW b'00101100' ; DCB=10 (bit5:4), PWM mode MOVWF CCP1CON ; T2CON: TMR2ON=1, prescaler=1 (T2CKPS=00) MOVLW b'00000100' ; TMR2ON=1, prescaler 1:1 MOVWF T2CON MAIN GOTO MAIN ; PWM gira in hardware autonomamente END
4 · Codice — dimmer LED con ADC → PWM
Il potenziometro su AN0 genera un valore ADC 0–255 (usiamo gli 8 bit bassi). Questo valore viene scritto direttamente in CCPR1L come duty cycle. Il range 0–255 copre lo 0–51% di duty effettivo (255/500 ≈ 51%) — per il range completo 0–100% si userebbe il valore × 2. Il risultato percettivo è un LED che varia da spento a massima luminosità con il potenziometro.
; ============================================================ ; Lezione 27 — PWM dimmer LED con ADC ; AN0 (potenziometro) controlla il duty cycle CCP1 ; Mapping: ADC 0-1023 → duty 0-100% ; Fpwm = 8kHz, aggiornamento ogni ~1ms ; PIC16F877A @ 4 MHz ; ============================================================ LIST P=16F877A #INCLUDE <p16f877a.inc> __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF CBLOCK 0x20 ADC_HI ; bit9:8 ADC_LO ; bit7:0 DC_HI ; duty cycle high (bit9:2 → CCPR1L) DC_LO ; duty cycle low (bit1:0 → CCP1CON<5:4>) ENDC ORG 0x0000 GOTO INIT ORG 0x0005 INIT BSF STATUS,RP0 BCF TRISC,2 ; RC2 output (PWM) BSF TRISA,0 ; RA0 input (ADC) MOVLW b'10001110' ; ADFM=1, PCFG=1110 (solo AN0) MOVWF ADCON1 MOVLW D'124' MOVWF PR2 BCF STATUS,RP0 ; Init ADC MOVLW b'01000001' ; ADCS=01, AN0, ADON=1 MOVWF ADCON0 ; Init CCP1 in PWM mode, duty=0 all'inizio MOVLW b'00001100' MOVWF CCP1CON CLRF CCPR1L ; Timer2 ON, prescaler 1:1 MOVLW b'00000100' MOVWF T2CON ; Attendi stabilizzazione ADC MOVLW D'10' MOVWF 0x70 WAIT DECFSZ 0x70,F GOTO WAIT ; ============================================================ ; MAIN: leggi ADC → calcola duty → aggiorna PWM ; ============================================================ MAIN ; 1. Avvia conversione ADC su AN0 BSF ADCON0,GO ADC_W BTFSC ADCON0,GO GOTO ADC_W ; 2. Leggi risultato 10 bit MOVF ADRESH,W MOVWF ADC_HI ; bit9:8 (solo bit1:0 usati) BSF STATUS,RP0 MOVF ADRESL,W BCF STATUS,RP0 MOVWF ADC_LO ; bit7:0 ; 3. Mappa ADC (0-1023) → duty cycle 10 bit (0-500) ; DC = ADC × (PR2+1) × 4 / 1023 ; Approssimazione: DC = ADC_LO × 125 / 255 × 4 ; Semplificato: DC_HI = ADC_LO >> 1 (duty 0-127, range 0-50%) ; Per range completo 0-100%: DC_10bit = ADC_LO × 2 ; (ADC_LO × 2 → 0-510 ≈ 0-500 = range PWM con PR2=124) BCF STATUS,C RLF ADC_LO,W ; ADC_LO × 2 (shift left 1) MOVWF DC_HI ; bit1:0 del duty (DC_LO per CCP1CON<5:4>) ; Con questa approssimazione usiamo solo DC_HI come CCPR1L ; DC_LO = bit1:0 di ADC_LO (prima dello shift) MOVF ADC_LO,W ANDLW 0x03 ; prendi bit1:0 ; shift in posizione DCB (bit5:4 di CCP1CON) ; DCB1=bit5, DCB0=bit4 → moltiplica per 16 BCF STATUS,C RLF DC_HI,W ; NOTA: qui usiamo W come temp MOVWF DC_LO BCF STATUS,C RLF DC_LO,W BCF STATUS,C RLF DC_LO,W BCF STATUS,C RLF DC_LO,W ANDLW b'00110000' ; maschera bit5:4 IORLW b'00001100' ; aggiungi CCP1M=1100 (PWM mode) ; Semplificazione finale: usiamo solo ADC_LO come CCPR1L ; e DCB=00 → duty 8 bit effettivo (risoluzione sufficiente) MOVF ADC_LO,W MOVWF CCPR1L ; duty = ADC_LO (0-255 su range 0-500) ; CCP1CON con DCB=00 MOVLW b'00001100' MOVWF CCP1CON GOTO MAIN END
5 · Codice — controllo servo con PWM software
I servo RC richiedono un segnale a 50Hz con impulso 1–2ms. Il modulo PWM hardware non arriva facilmente a 50Hz con un cristallo da 4MHz (il prescaler massimo di Timer2 è 16, il postscaler 16, ma servirebbero PR2=312 — oltre il massimo di 255). La soluzione è il PWM software con Timer1 e interrupt: la ISR gestisce la durata dell’impulso e della pausa, il main loop aggiorna solo la posizione desiderata modificando il valore di ricarica di TMR1.
; ============================================================ ; Lezione 27 — PWM servo motore ; Segnale servo: periodo 20ms (50Hz), impulso 1-2ms ; 1ms = 0°, 1.5ms = 90°, 2ms = 180° ; PIC16F877A @ 4 MHz ; Fpwm = Fosc / (4 × (PR2+1) × prescaler) ; Per 50Hz: (PR2+1) × prescaler = 4MHz / (4×50) = 20000 ; prescaler=16 → PR2+1 = 1250 → impossibile (max 256) ; prescaler=64 → PR2+1 = 312.5 → non intero ; SOLUZIONE: Timer2 con prescaler=16, postscaler=5 ; PR2+1 = 20000/16/5 = 250 → PR2=249 ; Oppure: PWM software con Timer1 e interrupt ; Qui usiamo PWM software per controllo preciso ; ============================================================ LIST P=16F877A #INCLUDE <p16f877a.inc> __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF ; Segnale servo su RC1 (output digitale) ; Timer1 gestisce i tempi con interrupt CBLOCK 0x20 W_SAVE STATUS_SAVE PCLATH_SAVE SERVO_POS ; posizione 0-180 (gradi) PULSE_TICKS_HI ; TMR1 reload per durata impulso (high) PULSE_TICKS_LO ; TMR1 reload per durata impulso (low) STATE ; 0=impulso attivo, 1=pausa (fino a 20ms) TICK_COUNT ; contatore ms per la pausa ENDC ; Calcoli tempi @ 4MHz, Timer1 prescaler=8 ; 1 tick Timer1 = 8/4MHz = 2µs ; 1ms = 500 tick, 1.5ms = 750 tick, 2ms = 1000 tick ; Reload TMR1 per 1ms: 65536 - 500 = 65036 = 0xFE0C ; Reload TMR1 per 2ms: 65536 - 1000 = 64536 = 0xFC18 ; Pausa: 20ms - impulso ≈ 18-19ms → gestita con contatore #DEFINE SERVO_MIN_HI 0xFE ; reload 1ms high #DEFINE SERVO_MIN_LO 0x0C ; reload 1ms low #DEFINE SERVO_MAX_HI 0xFC ; reload 2ms high #DEFINE SERVO_MAX_LO 0x18 ; reload 2ms low ORG 0x0000 GOTO INIT ORG 0x0004 ISR MOVWF W_SAVE SWAPF STATUS,W MOVWF STATUS_SAVE ; Timer1 overflow? BTFSS PIR1,TMR1IF GOTO ISR_EXIT BCF PIR1,TMR1IF BTFSC STATE,0 GOTO ISR_PAUSE ; siamo nella pausa ; --- Fine impulso: abbassa RC1, avvia pausa --- BCF PORTC,1 ; impulso terminato BSF STATE,0 ; Pausa: ~18ms → contatore 18 × (500µs/tick × overflow ogni 500ticks) ; Semplificazione: usiamo delay fisso 18ms ; Timer1 per 1ms: reload = 65036 MOVLW 0xFE MOVWF TMR1H MOVLW 0x0C MOVWF TMR1L MOVLW D'18' MOVWF TICK_COUNT GOTO ISR_EXIT ISR_PAUSE ; Conta i tick da 1ms della pausa DECFSZ TICK_COUNT,F GOTO ISR_EXIT_RELOAD ; pausa non finita ; --- Fine pausa: alza RC1, avvia nuovo impulso --- BCF STATE,0 BSF PORTC,1 ; inizio impulso ; Ricarica TMR1 con la durata dell'impulso corrente MOVF PULSE_TICKS_HI,W MOVWF TMR1H MOVF PULSE_TICKS_LO,W MOVWF TMR1L GOTO ISR_EXIT ISR_EXIT_RELOAD ; Ricarica TMR1 per 1ms MOVLW 0xFE MOVWF TMR1H MOVLW 0x0C MOVWF TMR1L ISR_EXIT SWAPF STATUS_SAVE,W MOVWF STATUS SWAPF W_SAVE,F SWAPF W_SAVE,W RETFIE ORG 0x0050 INIT BSF STATUS,RP0 BCF TRISC,1 ; RC1 output (servo) ; Abilita interrupt Timer1 MOVLW b'00000001' ; TMR1IE=1 in PIE1 MOVWF PIE1 BCF STATUS,RP0 ; Configura Timer1: prescaler 1:8, clock interno MOVLW b'00110001' ; T1CKPS=11 (1:8), TMR1ON=1 MOVWF T1CON ; Posizione iniziale: centro (90°, 1.5ms) MOVLW 0xFD ; 65036 - 250 = 64786 = 0xFD12 MOVWF PULSE_TICKS_HI ; ~1.5ms MOVLW 0x12 MOVWF PULSE_TICKS_LO MOVLW D'90' MOVWF SERVO_POS CLRF STATE MOVLW D'18' MOVWF TICK_COUNT ; Carica TMR1 e alza RC1 per primo impulso MOVF PULSE_TICKS_HI,W MOVWF TMR1H MOVF PULSE_TICKS_LO,W MOVWF TMR1L BSF PORTC,1 ; inizio impulso MOVLW b'11000000' ; GIE=1, PEIE=1 MOVWF INTCON ; ============================================================ ; MAIN: spazza il servo 0°→180°→0° lentamente ; ============================================================ MAIN ; Muovi verso 0° (1ms) MOVLW SERVO_MIN_HI MOVWF PULSE_TICKS_HI MOVLW SERVO_MIN_LO MOVWF PULSE_TICKS_LO CALL DELAY_500MS ; Muovi verso 90° (1.5ms) MOVLW 0xFD MOVWF PULSE_TICKS_HI MOVLW 0x12 MOVWF PULSE_TICKS_LO CALL DELAY_500MS ; Muovi verso 180° (2ms) MOVLW SERVO_MAX_HI MOVWF PULSE_TICKS_HI MOVLW SERVO_MAX_LO MOVWF PULSE_TICKS_LO CALL DELAY_500MS GOTO MAIN DELAY_500MS MOVLW D'200' MOVWF 0x70 DA MOVLW D'250' MOVWF 0x71 DB MOVLW D'10' MOVWF 0x72 DC DECFSZ 0x72,F GOTO DC DECFSZ 0x71,F GOTO DB DECFSZ 0x70,F GOTO DA RETURN END
🧪 Prova tu
- Carica il PWM fisso. Osserva nel pannello PWM che RC2 alterna high/low con duty 50%. Modifica CCPR1L a 0 (duty 0%) poi a 124 (duty 99% → CCPR1L=124, DCB=11 → 4×125-1=499/500). Nota la differenza visiva sul simulatore.
- Calcola il valore CCPR1L per un duty del 75% con PR2=124. Formula: DC_10bit = 0.75 × 4 × 125 = 375 → CCPR1L = 375 >> 2 = 93, DCB = 375 & 3 = 3. Imposta CCPR1L=93 e CCP1CON=0b00111100. Verifica.
- Carica il dimmer. Imposta AN0 a 128 nel pannello ADC. Verifica CCPR1L=128 e RC2 al ~25% di duty (128/500≈25.6%).
- Sfida: modifica il programma servo per accettare la posizione da AN0 (ADC): ADC=0 → 0° (1ms), ADC=255 → 180° (2ms). Formula: PULSE_TICKS = 500 + (ADC_LO × 500/255). Implementa il calcolo e aggiorna PULSE_TICKS_HI:LO nel main loop.
❓ Quiz rapido
1. Perché si deve impostare RC2 come output prima di configurare il modulo CCP1 in modalità PWM?
Mostra risposta
Se RC2 rimane configurato come input (TRISC,2=1) mentre il modulo CCP1 genera il segnale PWM, il pin è in alta impedenza e il segnale PWM non appare fisicamente sul pin — il modulo genera internamente la forma d’onda ma il buffer di output è disabilitato. In realtà sul PIC16F877A il datasheet specifica che RC2/CCP1 deve essere configurato come output prima di abilitare il PWM, altrimenti la prima transizione potrebbe essere anomala. La sequenza corretta è: imposta TRISC,2=0, configura PR2 e T2CON, poi imposta CCP1CON in PWM mode.
2. Con PR2=255 e prescaler=1, qual è la frequenza PWM a 4MHz? E con prescaler=4?
Mostra risposta
Prescaler=1: Fpwm = 4MHz / (4 × 256 × 1) = 3906 Hz ≈ 3.9kHz. Prescaler=4: Fpwm = 4MHz / (4 × 256 × 4) = 976 Hz ≈ 1kHz. Con PR2=255 e prescaler=1 si sfruttano tutti i 10 bit di risoluzione del duty cycle (4×256=1024 step). Con prescaler=4 si scende a ~1kHz, utile per motori che richiedono frequenze più basse, ma la risoluzione rimane 10 bit.
3. Nel servo, perché si usa il Timer1 con prescaler 1:8 invece del prescaler 1:1?
Mostra risposta
Timer1 è a 16 bit e conta fino a 65536. Con prescaler 1:1 ogni tick è 1µs (@4MHz) → overflow ogni 65536µs ≈ 65ms. Per generare un impulso da 1ms bisognerebbe ricaricare TMR1 a 65036 — fattibile ma il margine per errori è piccolo. Con prescaler 1:8 ogni tick è 2µs → 1ms = 500 tick, ricarica a 65036 (identico per coincidenza, ma la granularità è 2µs invece di 1µs). La ragione principale è che prescaler più alto permette di misurare eventi più lunghi senza overflow intermedi, utile quando si vuole estendere la pausa da 18ms senza ulteriori contatori.
Prossima lezione →
Lezione 28 — I²C: comunicazione con sensori e display
Il protocollo I²C con il modulo MSSP del PIC16F877A. Master mode, lettura da sensore di temperatura DS1621, scrittura su display LCD I²C.
© Prof. D. Bertolino · PIC16 From Zero to Hero · WebPicSimulator · Lezione 27