Lezione 28 di 43

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.

Segnale PWM — periodo, duty cycle e formula 25% 50% 75% T = 1/Fpwm Fpwm = Fosc / (4 × (PR2+1) × prescaler) Duty = CCPR1L:DCB / (4 × (PR2+1)) PR2=124, prescaler=1 → Fpwm=8kHz CCPR1L=62, DCB=10 → duty=250/500=50%

2 · Registri CCP1 e duty cycle a 10 bit

Registri CCP1 PWM — duty cycle a 10 bit spalmato su due registri CCPR1L (0x15) — bit9:2 del duty DC9 DC8 DC7 DC6 DC5 DC4 DC3 DC2 bit9 (MSB) ←————————————————————————→ bit2 CCP1CON (0x17) — bit1:0 del duty — — DCB1 DCB0 CCP1M3 CCP1M2 CCP1M1 CCP1M0 DCB=bit1:0 duty CCP1M=1100 → PWM mode T2CON + PR2 T2CON: TOUTPS TMR2ON T2CKPS PR2=124 → periodo = 125 cicli TMR2 Duty cycle a 10 bit: CCPR1L[7:0] = bit9:2 CCP1CON[5:4] = bit1:0 valore 0-1023 → duty 0-100%

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&lt;5:4&gt;)
    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.

Segnale servo RC — periodo 20ms, impulso 1–2ms 1ms 0° 1.5ms 90° 2ms 180° T = 20ms (50Hz) Posizione = (impulso_ms − 1.0) × 180 | Timer1 prescaler=8 → 1 tick=2µs | 1ms=500tick, 2ms=1000tick
; ============================================================
; 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

  1. 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.
  2. 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.
  3. Carica il dimmer. Imposta AN0 a 128 nel pannello ADC. Verifica CCPR1L=128 e RC2 al ~25% di duty (128/500≈25.6%).
  4. 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