Lezione 42 di 43

Lezione 41

🎓 PIC16 — From Zero to Hero · Lezione 41 · Modulo 7

Progetto 4: Servo Controller 8 Canali 🤖

PWM software su 8 pin PORTB · Interpolazione lineare per movimenti fluidi
Sequenze automatiche hard-coded · USART bidirezionale con parser a stati · EEPROM posizioni

🎯 Cosa costruisci in questa lezione

  • PWM software 8 canali: Timer0 interrupt ogni 50µs (TMR0 precaricato a 206). Ogni frame da 20ms ospita 8 impulsi consecutivi, uno per canale. SERVO_N = tick di durata (20=1ms=0°, 30=1.5ms=90°, 40=2ms=180°)
  • ISR a stati: decrementa PULSE_TICK; quando arriva a 0 abbassa il pin, carica il canale successivo via FSR/INDF, alza il nuovo pin
  • Interpolazione lineare: ogni frame confronta SERVO_N con TGT_N e incrementa o decrementa di 1 tick. Con STEPS_N=50 e aggiornamento ogni frame (20ms) il movimento dura 1 secondo — nessun virgola mobile, solo INCF/DECF
  • Sequenza automatica hard-coded in Flash come tabella RETLW: (canale, target, durata) × N mosse + 0xFF per loop. UPDATE_SEQUENCE avanza la lista ogni frame e imposta nuovi target
  • USART bidirezionale 9600 baud: parser a 3 stati (attendi CMD, attendi CH, attendi VAL). Comandi: S(set), G(get), R(reset), P(play), X(stop), W(write EEPROM)
  • Risposta ‘K’ per OK, ‘E’ per errore — comunicazione seriale dal PC con qualsiasi terminale (PuTTY, minicom, screen)
  • EEPROM: salva e carica 8 byte con validazione range — se i dati EEPROM sono fuori range usa default 90°

📋 Scheda progetto

Lezione: 41 / 42
Modulo: 7 — Progetti Completi
Device: PIC16F877A
Prerequisiti: tutto il corso
Difficoltà: ★★★★★
Hardware: 8× servo SG90, transistor buffer
Comandi: PC via USB-UART 9600 baud


Modulo 1 · PWM software 8 canali + parser USART base

PWM servo software — frame 20ms, 8 canali, risoluzione 50µs/tick frame 20ms = 400 tick × 50µs CH0 CH1 CH2 pausa fino a fine frame 20t=1ms 30t=1.5ms 40t=2ms ISR ogni 50µs — macchina a stati del PWM PULSE_TICK > 0 DECF PULSE_TICK pin rimane alto PULSE_TICK = 0 BCF PORTB,N (pin basso) ACTIVE_CH++ ACTIVE_CH = 8 tutti serviti aspetta fine frame FRAME_TICK=0 nuovo frame BSF PORTB,0 CH0 TMR0 precaricato a 206 ogni ISR → overflow ogni 50µs · SERVO_N: 20=1ms(0°), 30=1.5ms(90°), 40=2ms(180°)

⚠️ Corrente servo e driver: ogni servo SG90 può assorbire fino a 200mA in movimento. I pin PORTB del PIC16F877A erogano al massimo 25mA ciascuno e 200mA totali su tutto il PORTB. Con 8 servo si supera abbondantemente questo limite — è obbligatorio usare un driver tra il PIC e i servo: transistor NPN (BC547) in configurazione emitter-follower, o un buffer come ULN2003 (7 canali open-collector), oppure alimentare i servo direttamente dalla sorgente 5V e usare i pin PIC solo per il segnale PWM (che assorbe ~1mA dal servo).

; ============================================================
; Lezione 41 — Servo controller: PWM software 8 canali
; PIC16F877A @ 4 MHz
; Servo standard: impulso 1ms=0°, 1.5ms=90°, 2ms=180°
; Periodo frame: 20ms (50Hz)
; Tecnica: Timer0 interrupt ogni 50µs → risoluzione 1/400 del frame
; 8 canali su PORTB bit7:0, uno alla volta (non tutti insieme!)
; Ogni canale ha il proprio contatore impulso
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

    CBLOCK  0x20
; Posizioni servo (0-40: 0°..180°, step = 50µs)
; 1ms = 20 tick, 1.5ms = 30 tick, 2ms = 40 tick
SERVO_0         ; canale 0: tick impulso (20-40)
SERVO_1
SERVO_2
SERVO_3
SERVO_4
SERVO_5
SERVO_6
SERVO_7
; Stato PWM
FRAME_TICK      ; tick corrente nel frame (0-399)
FRAME_TICK_H    ; byte alto (frame 400 tick × 50µs = 20ms)
ACTIVE_CH       ; canale attivo in questo tick (0-7)
PULSE_TICK      ; tick rimanenti per l&#x27;impulso corrente
FLAGS           ; bit0=nuovo_frame
; Target positions (per interpolazione)
TGT_0
TGT_1
TGT_2
TGT_3
; USART
RX_BYTE
RX_CMD          ; comando ricevuto
RX_CH           ; canale dal comando
RX_VAL          ; valore dal comando
PARSE_STATE     ; 0=attendi cmd, 1=attendi ch, 2=attendi val
TMP
DLY
    ENDC

    ORG     0x0000
    GOTO    INIT

    ORG     0x0004
; ============================================================
; ISR — Timer0 ogni 50µs (TMR0 precaricato a 56)
; Prescaler 1:4: 1µs × 4 = 4µs/tick, 50µs/4 = 12.5 → uso 1:1
; @ 4MHz: 1 ciclo = 1µs, TMR0 senza prescaler overflow ogni 256µs
; Per 50µs: precarica TMR0 = 256 - 50 = 206
; ============================================================
ISR
    BTFSS   INTCON,T0IF
    RETFIE
    BCF     INTCON,T0IF
    MOVLW   D'206'             ; ricarica TMR0 per prossimo 50µs
    MOVWF   TMR0

    ; Avanza tick frame
    INCF    FRAME_TICK,F
    BTFSC   STATUS,Z
    INCF    FRAME_TICK_H,F

    ; Frame completo ogni 400 tick (400×50µs = 20ms)
    MOVF    FRAME_TICK_H,F
    BTFSS   STATUS,Z
    GOTO    ISR_CHECK_400
    MOVF    FRAME_TICK,W
    SUBLW   D'144'             ; 400 - 256 = 144 (overflow conta da 0)
    BTFSC   STATUS,Z
    GOTO    ISR_RESET_FRAME
ISR_CHECK_400
    ; Versione semplice: solo byte basso, frame = 256 tick = 12.8ms
    ; (semplificazione didattica: frame ~13ms invece di 20ms)
    MOVF    FRAME_TICK,F
    BTFSS   STATUS,Z
    GOTO    ISR_PULSE
ISR_RESET_FRAME
    CLRF    FRAME_TICK
    CLRF    FRAME_TICK_H
    BSF     FLAGS,0            ; nuovo frame
    CLRF    ACTIVE_CH          ; ricomincia dal canale 0
    ; Carica impulso canale 0
    MOVF    SERVO_0,W
    MOVWF   PULSE_TICK
    BSF     PORTB,0            ; alza pin canale 0

ISR_PULSE
    ; Gestione impulso canale attivo
    MOVF    PULSE_TICK,F
    BTFSC   STATUS,Z
    GOTO    ISR_NEXT_CH        ; impulso finito → prossimo canale

    DECF    PULSE_TICK,F       ; decrementa tick rimanenti
    RETFIE

ISR_NEXT_CH
    ; Abbassa il pin del canale corrente
    MOVF    ACTIVE_CH,W
    ADDWF   PCL,F
    GOTO    LOW_CH0
    GOTO    LOW_CH1
    GOTO    LOW_CH2
    GOTO    LOW_CH3
    GOTO    LOW_CH4
    GOTO    LOW_CH5
    GOTO    LOW_CH6
    GOTO    LOW_CH7
LOW_CH0 BCF PORTB,0 : GOTO NEXT_CH
LOW_CH1 BCF PORTB,1 : GOTO NEXT_CH
LOW_CH2 BCF PORTB,2 : GOTO NEXT_CH
LOW_CH3 BCF PORTB,3 : GOTO NEXT_CH
LOW_CH4 BCF PORTB,4 : GOTO NEXT_CH
LOW_CH5 BCF PORTB,5 : GOTO NEXT_CH
LOW_CH6 BCF PORTB,6 : GOTO NEXT_CH
LOW_CH7 BCF PORTB,7 : GOTO NEXT_CH

NEXT_CH
    ; Avanza al prossimo canale
    INCF    ACTIVE_CH,F
    MOVF    ACTIVE_CH,W
    SUBLW   D'8'
    BTFSC   STATUS,Z
    GOTO    ISR_EXIT           ; tutti 8 canali serviti

    ; Carica impulso canale successivo
    MOVLW   SERVO_0
    ADDWF   ACTIVE_CH,W
    MOVWF   TMP
    ; Leggi SERVO_N via FSR
    MOVF    TMP,W
    MOVWF   FSR
    MOVF    INDF,W
    MOVWF   PULSE_TICK
    ; Alza pin canale successivo
    MOVF    ACTIVE_CH,W
    ADDWF   PCL,F
    BSF     PORTB,0 ; ch0 (non raggiunto qui)
    BSF     PORTB,1
    BSF     PORTB,2
    BSF     PORTB,3
    BSF     PORTB,4
    BSF     PORTB,5
    BSF     PORTB,6
    BSF     PORTB,7

ISR_EXIT
    RETFIE

    ORG     0x0080
INIT
    BSF     STATUS,RP0
    CLRF    TRISB              ; PORTB output (tutti i servo)
    ; USART: 9600 baud @ 4MHz
    ; SPBRG = FOSC/(16×baud) - 1 = 4000000/(16×9600) - 1 = 25
    MOVLW   D'25'
    MOVWF   SPBRG
    MOVLW   b'00100100'        ; TXEN=1, BRGH=1 (alta velocità)
    MOVWF   TXSTA
    ; TMR0: no prescaler (PSA=1), sorgente interna
    MOVLW   b'11001000'        ; PSA=1 (no prescaler TMR0)
    MOVWF   OPTION_REG
    BCF     STATUS,RP0

    ; RCSTA: abilita USART e ricezione continua
    MOVLW   b'10010000'        ; SPEN=1, CREN=1
    MOVWF   RCSTA

    ; Posizioni iniziali: tutti a 90° (30 tick = 1.5ms)
    MOVLW   D'30'
    MOVWF   SERVO_0
    MOVWF   SERVO_1
    MOVWF   SERVO_2
    MOVWF   SERVO_3
    MOVWF   SERVO_4
    MOVWF   SERVO_5
    MOVWF   SERVO_6
    MOVWF   SERVO_7

    CLRF    FRAME_TICK
    CLRF    FRAME_TICK_H
    CLRF    ACTIVE_CH
    CLRF    FLAGS
    CLRF    PARSE_STATE
    MOVLW   D'30'
    MOVWF   PULSE_TICK         ; primo impulso a 90°
    BSF     PORTB,0            ; alza canale 0

    ; Abilita interrupt TMR0
    MOVLW   b'10100000'        ; GIE=1, T0IE=1
    MOVWF   INTCON
    MOVLW   D'206'
    MOVWF   TMR0

; ============================================================
; MAIN — gestisce comandi USART e interpolazione
; Protocollo seriale: 'S' CH VAL → imposta servo
; CH = '0'-'7' (canale), VAL = 0x00-0x28 (0°-180°)
; Esempio: 'S' 0x02 0x1E → servo 2 a 90°
; ============================================================
MAIN
    ; Controlla USART RX
    BSF     STATUS,RP0
    BTFSS   PIR1,RCIF          ; dato disponibile?
    GOTO    MAIN_NO_RX
    BCF     STATUS,RP0

    ; Leggi byte
    MOVF    RCREG,W
    MOVWF   RX_BYTE

    ; Parsing stato: 0=attendi 'S', 1=attendi canale, 2=attendi valore
    MOVF    PARSE_STATE,W
    ADDWF   PCL,F
    GOTO    PARSE_WAIT_CMD
    GOTO    PARSE_WAIT_CH
    GOTO    PARSE_WAIT_VAL

PARSE_WAIT_CMD
    ; Attendi 'S' (0x53)
    MOVF    RX_BYTE,W
    SUBLW   'S'
    BTFSS   STATUS,Z
    GOTO    MAIN_NO_RX         ; non &#x27;S&#x27;: ignora
    INCF    PARSE_STATE,F      ; → stato 1
    GOTO    MAIN_NO_RX

PARSE_WAIT_CH
    MOVF    RX_BYTE,W
    MOVWF   RX_CH
    INCF    PARSE_STATE,F      ; → stato 2
    GOTO    MAIN_NO_RX

PARSE_WAIT_VAL
    MOVF    RX_BYTE,W
    MOVWF   RX_VAL
    CLRF    PARSE_STATE        ; → stato 0

    ; Valida canale (0-7)
    MOVF    RX_CH,W
    SUBLW   D'7'
    BTFSS   STATUS,C
    GOTO    MAIN_NO_RX         ; canale &gt; 7: ignora

    ; Valida valore (20-40: 1ms-2ms)
    MOVF    RX_VAL,W
    SUBLW   D'19'
    BTFSC   STATUS,C           ; &lt; 20?
    GOTO    MAIN_NO_RX
    MOVF    RX_VAL,W
    SUBLW   D'40'
    BTFSS   STATUS,C           ; &gt; 40?
    GOTO    MAIN_NO_RX

    ; Imposta SERVO_N = RX_VAL
    BCF     INTCON,GIE         ; disabilita interrupt durante modifica
    MOVLW   SERVO_0
    ADDWF   RX_CH,W
    MOVWF   FSR
    MOVF    RX_VAL,W
    MOVWF   INDF               ; SERVO[CH] = VAL
    BSF     INTCON,GIE

MAIN_NO_RX
    BCF     STATUS,RP0
    GOTO    MAIN

    END

Modulo 2 · Interpolazione lineare e sequenze automatiche

Interpolazione lineare servo — movimento fluido in N frame Posizione servo CH0 nel tempo 40 30 20 senza con frame → 0 50 100 150 Algoritmo ogni frame (20ms) se STEPS_N = 0: fermo (RETURN) calcola diff = TGT_N – SERVO_N diff > 0: INCF SERVO_N diff < 0: DECF SERVO_N DECF STEPS_N (conta passi) 50 passi × 20ms/frame = 1s per 10 tick

💡 Perché ±1 tick per frame funziona: la risoluzione del servo è 20 tick (1ms) per 180° → ogni tick = 9°. Con ±1 tick/frame a 50Hz (frame ogni 20ms) la velocità massima è 1 tick × 50 frame/s = 50 tick/s = 450°/s. Un servo SG90 ruota a ~500°/s — quindi l’interpolazione ±1/frame è vicina alla velocità massima del servo. Per movimenti più lenti: aggiornare solo ogni N frame (STEPS conta frame, non tick). Per movimenti più veloci con salti grandi: incremento proporzionale — ma serve la divisione, e torniamo all’algoritmo Bresenham completo.

; ============================================================
; Lezione 41 — Interpolazione lineare e sequenze
; PIC16F877A @ 4 MHz
; Interpolazione: muovi il servo da posizione A a B in N passi
; Ogni passo = (B - A) / N, aggiornato ogni frame (20ms)
; Con N=50 frame: movimento fluido in 1 secondo
; Sequenza: lista di (canale, target, durata) eseguita automaticamente
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

    CBLOCK  0x20
; Posizioni correnti (come modulo 1)
SERVO_0
SERVO_1
SERVO_2
SERVO_3
; Posizioni target per interpolazione
TGT_0
TGT_1
TGT_2
TGT_3
; Passi rimanenti per ogni canale
STEPS_0         ; 0 = fermo
STEPS_1
STEPS_2
STEPS_3
; Direzione: 0=avanza, 1=indietro (per calcolo)
; (usiamo il segno della differenza target-corrente)
; Errore accumulato (fixed point 4.4)
ERR_0
ERR_1
ERR_2
ERR_3
; Sequenza automatica
SEQ_PTR         ; puntatore alla sequenza corrente (indice)
SEQ_PAUSE       ; frame rimanenti di pausa tra mosse
SEQ_ACTIVE      ; 1=sequenza in esecuzione
FLAGS
; Temporanei
TMP
TMP2
    ENDC

    ORG     0x0000
    GOTO    INIT

; ============================================================
; Sequenza hard-coded: 8 mosse
; Formato: CH, TARGET (tick 20-40), DURATA (frame)
; 0xFF = fine sequenza (loop)
; ============================================================
    ORG     0x0020
SEQUENCE
    ; ch0 → 20 (0°), 50 frame (1s)
    RETLW   0x00
    RETLW   D'20'
    RETLW   D'50'
    ; ch1 → 40 (180°), 50 frame
    RETLW   0x01
    RETLW   D'40'
    RETLW   D'50'
    ; ch0 → 40 (180°), 100 frame (2s)
    RETLW   0x00
    RETLW   D'40'
    RETLW   D'100'
    ; ch1 → 20 (0°), 100 frame
    RETLW   0x01
    RETLW   D'20'
    RETLW   D'100'
    ; ch2 → 30 (90°), 30 frame
    RETLW   0x02
    RETLW   D'30'
    RETLW   D'30'
    ; ch3 → 25 (45°), 30 frame
    RETLW   0x03
    RETLW   D'25'
    RETLW   D'30'
    ; ch2 → 30 (90°), 50 frame (torna centro)
    RETLW   0x02
    RETLW   D'30'
    RETLW   D'50'
    ; fine: loop
    RETLW   0xFF

; ============================================================
; READ_SEQ — legge byte N dalla sequenza in W
; ============================================================
READ_SEQ
    ADDWF   PCL,F              ; salta nella tabella SEQUENCE
    ; (non funziona così direttamente, usiamo call)
    RETURN

; ============================================================
; GET_SEQ_BYTE — legge SEQUENCE[SEQ_PTR] in W
; ============================================================
GET_SEQ_BYTE
    MOVF    SEQ_PTR,W
    CALL    SEQUENCE
    RETURN

; ============================================================
; UPDATE_SEQUENCE — avanza sequenza se attiva
; Chiamato ogni frame (FLAGS,0=1)
; ============================================================
UPDATE_SEQUENCE
    BTFSS   SEQ_ACTIVE,0
    RETURN
    ; Controlla pausa
    MOVF    SEQ_PAUSE,F
    BTFSC   STATUS,Z
    GOTO    SEQ_NEXT_MOVE
    DECF    SEQ_PAUSE,F
    RETURN
SEQ_NEXT_MOVE
    ; Leggi prossima mossa dalla sequenza
    CALL    GET_SEQ_BYTE
    MOVWF   TMP                ; canale
    ADDLW   0                  ; check 0xFF
    SUBLW   0xFF
    BTFSC   STATUS,Z
    GOTO    SEQ_LOOP           ; fine: ricomincia
    ; Avanza puntatore e leggi target
    INCF    SEQ_PTR,F
    CALL    GET_SEQ_BYTE
    MOVWF   TMP2               ; target
    INCF    SEQ_PTR,F
    CALL    GET_SEQ_BYTE
    MOVWF   SEQ_PAUSE          ; durata come pausa
    INCF    SEQ_PTR,F
    ; Imposta interpolazione per il canale TMP
    MOVF    TMP,W
    ADDWF   PCL,F
    GOTO    SET_CH0
    GOTO    SET_CH1
    GOTO    SET_CH2
    GOTO    SET_CH3
    GOTO    SEQ_DONE           ; canali &gt; 3: ignora
    GOTO    SEQ_DONE
    GOTO    SEQ_DONE
    GOTO    SEQ_DONE
SET_CH0
    MOVF    TMP2,W
    MOVWF   TGT_0
    MOVLW   D'50'
    MOVWF   STEPS_0
    CLRF    ERR_0
    GOTO    SEQ_DONE
SET_CH1
    MOVF    TMP2,W
    MOVWF   TGT_1
    MOVLW   D'50'
    MOVWF   STEPS_1
    CLRF    ERR_1
    GOTO    SEQ_DONE
SET_CH2
    MOVF    TMP2,W
    MOVWF   TGT_2
    MOVLW   D'50'
    MOVWF   STEPS_2
    CLRF    ERR_2
    GOTO    SEQ_DONE
SET_CH3
    MOVF    TMP2,W
    MOVWF   TGT_3
    MOVLW   D'50'
    MOVWF   STEPS_3
    CLRF    ERR_3
SEQ_DONE
    RETURN
SEQ_LOOP
    CLRF    SEQ_PTR            ; ricomincia sequenza
    CLRF    SEQ_PAUSE
    GOTO    SEQ_NEXT_MOVE

; ============================================================
; INTERPOLATE_CH — avvicina SERVO_N di un passo verso TGT_N
; Algoritmo Bresenham lineare: evita divisione floating point
; Ogni frame: SERVO += (TGT - SERVO) > 0 ? +1 : -1
; Fine quando SERVO == TGT
; ============================================================
INTERP_CH0
    MOVF    STEPS_0,F
    BTFSC   STATUS,Z
    RETURN                     ; fermo
    MOVF    TGT_0,W
    SUBWF   SERVO_0,W          ; W = SERVO_0 - TGT_0
    BTFSC   STATUS,Z
    GOTO    IC0_DONE           ; uguali: ferma
    BTFSS   STATUS,C           ; C=0 → SERVO &lt; TGT
    GOTO    IC0_UP
    ; SERVO > TGT: decrementa
    DECF    SERVO_0,F
    GOTO    IC0_STEP
IC0_UP
    INCF    SERVO_0,F
IC0_STEP
    DECF    STEPS_0,F
    RETURN
IC0_DONE
    CLRF    STEPS_0
    RETURN

INTERP_CH1
    MOVF    STEPS_1,F
    BTFSC   STATUS,Z
    RETURN
    MOVF    TGT_1,W
    SUBWF   SERVO_1,W
    BTFSC   STATUS,Z
    GOTO    IC1_DONE
    BTFSS   STATUS,C
    GOTO    IC1_UP
    DECF    SERVO_1,F
    DECF    STEPS_1,F
    RETURN
IC1_UP
    INCF    SERVO_1,F
    DECF    STEPS_1,F
    RETURN
IC1_DONE
    CLRF    STEPS_1
    RETURN

INTERP_CH2
    MOVF    STEPS_2,F
    BTFSC   STATUS,Z
    RETURN
    MOVF    TGT_2,W
    SUBWF   SERVO_2,W
    BTFSC   STATUS,Z
    GOTO    IC2_DONE
    BTFSS   STATUS,C
    GOTO    IC2_UP
    DECF    SERVO_2,F
    DECF    STEPS_2,F
    RETURN
IC2_UP
    INCF    SERVO_2,F
    DECF    STEPS_2,F
    RETURN
IC2_DONE
    CLRF    STEPS_2
    RETURN

INTERP_CH3
    MOVF    STEPS_3,F
    BTFSC   STATUS,Z
    RETURN
    MOVF    TGT_3,W
    SUBWF   SERVO_3,W
    BTFSC   STATUS,Z
    GOTO    IC3_DONE
    BTFSS   STATUS,C
    GOTO    IC3_UP
    DECF    SERVO_3,F
    DECF    STEPS_3,F
    RETURN
IC3_UP
    INCF    SERVO_3,F
    DECF    STEPS_3,F
    RETURN
IC3_DONE
    CLRF    STEPS_3
    RETURN

    ORG     0x0100
INIT
    BSF     STATUS,RP0
    CLRF    TRISB
    BCF     STATUS,RP0
    ; Posizioni iniziali 90°
    MOVLW   D'30'
    MOVWF   SERVO_0
    MOVWF   SERVO_1
    MOVWF   SERVO_2
    MOVWF   SERVO_3
    ; Target iniziali = posizione attuale (fermi)
    MOVLW   D'30'
    MOVWF   TGT_0
    MOVWF   TGT_1
    MOVWF   TGT_2
    MOVWF   TGT_3
    CLRF    STEPS_0
    CLRF    STEPS_1
    CLRF    STEPS_2
    CLRF    STEPS_3
    CLRF    SEQ_PTR
    CLRF    SEQ_PAUSE
    MOVLW   1
    MOVWF   SEQ_ACTIVE         ; avvia sequenza automatica
    CLRF    FLAGS

MAIN
    ; Attendi nuovo frame (segnalato dalla ISR - non inclusa qui
    ; per brevità, vedi Modulo 1)
    BTFSS   FLAGS,0
    GOTO    MAIN
    BCF     FLAGS,0

    ; Interpolazione ogni frame
    CALL    INTERP_CH0
    CALL    INTERP_CH1
    CALL    INTERP_CH2
    CALL    INTERP_CH3

    ; Avanza sequenza
    CALL    UPDATE_SEQUENCE

    GOTO    MAIN

    END

Modulo 3 · USART completo e salvataggio EEPROM

Protocollo USART bidirezionale — macchina a stati parser STATO 0 attendi CMD STATO 1 attendi CH STATO 2 attendi VAL ‘S’ o ‘G’ byte CH ‘G’: esegui DO_GET, torna a 0 ‘S’: DO_SET, torna a 0 Cmds diretti ‘R’ → reset 90° ‘P’ → play seq ‘X’ → stop seq ‘W’ → EEPROM Reply: ‘K’=ok Reply: ‘E’=err avvio: “SVC8”
; ============================================================
; Lezione 41 — USART completo + salvataggio EEPROM
; PIC16F877A @ 4 MHz
; Protocollo bidirezionale:
;   RX: 'S' CH VAL  → muovi servo
;       'G' CH      → leggi posizione (risponde con 1 byte)
;       'R'         → reset tutti a 90°
;       'P'         → play sequenza
;       'X'         → stop sequenza
;       'W'         → write to EEPROM (salva tutte le posizioni)
;   TX: risponde con 'K' (ok) o 'E' (errore)
; EEPROM: 8 byte da addr 0x00 (un byte per servo)
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

    CBLOCK  0x20
; Servo
SERVO_0
SERVO_1
SERVO_2
SERVO_3
SERVO_4
SERVO_5
SERVO_6
SERVO_7
; USART parser
RX_BYTE
CMD_BYTE        ; comando corrente
ARG_CH          ; argomento canale
ARG_VAL         ; argomento valore
PARSE_STATE     ; 0=cmd, 1=ch, 2=val
; EEPROM
EEP_ADDR
EEP_DATA
; Stato
SEQ_ACTIVE
FLAGS
TMP
    ENDC

    ORG     0x0000
    GOTO    INIT

; ============================================================
; USART_TX — trasmetti W via USART, attendi TXIF
; ============================================================
USART_TX
    BSF     STATUS,RP0
UTXW BTFSS   TXSTA,TRMT         ; TX shift register vuoto?
    GOTO    UTXW
    BCF     STATUS,RP0
    MOVWF   TXREG
    RETURN

; ============================================================
; USART_RX_READY — ritorna Z=0 se byte disponibile
; ============================================================
USART_RX_READY
    BSF     STATUS,RP0
    BTFSS   PIR1,RCIF
    BCF     STATUS,Z
    BTFSC   PIR1,RCIF
    BSF     STATUS,Z
    BCF     STATUS,RP0
    RETURN

; ============================================================
; EEPROM_WRITE / READ (come Lez.38)
; ============================================================
EEP_WR
    BSF     STATUS,RP0
    MOVF    EEP_ADDR,W
    MOVWF   EEADR
    MOVF    EEP_DATA,W
    MOVWF   EEDATA
    BSF     EECON1,WREN
    BCF     INTCON,GIE
    MOVLW   0x55
    MOVWF   EECON2
    MOVLW   0xAA
    MOVWF   EECON2
    BSF     EECON1,WR
    BSF     INTCON,GIE
EWAIT BTFSC  EECON1,WR
    GOTO    EWAIT
    BCF     EECON1,WREN
    BCF     STATUS,RP0
    RETURN

EEP_RD
    BSF     STATUS,RP0
    MOVF    EEP_ADDR,W
    MOVWF   EEADR
    BSF     EECON1,RD
    MOVF    EEDATA,W
    BCF     STATUS,RP0
    MOVWF   EEP_DATA
    RETURN

; ============================================================
; SAVE_ALL — salva tutte le 8 posizioni in EEPROM
; ============================================================
SAVE_ALL
    MOVLW   SERVO_0
    MOVWF   FSR
    CLRF    EEP_ADDR
SAV_LOOP
    MOVF    INDF,W
    MOVWF   EEP_DATA
    CALL    EEP_WR
    INCF    FSR,F
    INCF    EEP_ADDR,F
    MOVF    EEP_ADDR,W
    SUBLW   D'8'
    BTFSS   STATUS,Z
    GOTO    SAV_LOOP
    RETURN

; ============================================================
; LOAD_ALL — carica posizioni da EEPROM
; ============================================================
LOAD_ALL
    MOVLW   SERVO_0
    MOVWF   FSR
    CLRF    EEP_ADDR
LOD_LOOP
    CALL    EEP_RD
    MOVF    EEP_DATA,W
    ; Valida range (20-40)
    SUBLW   D'19'
    BTFSC   STATUS,C           ; &lt; 20?
    GOTO    LOD_DEFAULT
    MOVF    EEP_DATA,W
    SUBLW   D'40'
    BTFSS   STATUS,C           ; &gt; 40?
    GOTO    LOD_DEFAULT
    MOVF    EEP_DATA,W
    GOTO    LOD_STORE
LOD_DEFAULT
    MOVLW   D'30'              ; default 90°
LOD_STORE
    MOVWF   INDF
    INCF    FSR,F
    INCF    EEP_ADDR,F
    MOVF    EEP_ADDR,W
    SUBLW   D'8'
    BTFSS   STATUS,Z
    GOTO    LOD_LOOP
    RETURN

; ============================================================
; DO_COMMAND — esegui comando CMD_BYTE
; ============================================================
DO_COMMAND
    ; Dispatch su CMD_BYTE
    MOVF    CMD_BYTE,W
    SUBLW   'R'
    BTFSC   STATUS,Z
    GOTO    CMD_RESET
    MOVF    CMD_BYTE,W
    SUBLW   'P'
    BTFSC   STATUS,Z
    GOTO    CMD_PLAY
    MOVF    CMD_BYTE,W
    SUBLW   'X'
    BTFSC   STATUS,Z
    GOTO    CMD_STOP
    MOVF    CMD_BYTE,W
    SUBLW   'W'
    BTFSC   STATUS,Z
    GOTO    CMD_WRITE
    ; Comando sconosciuto
    MOVLW   'E'
    CALL    USART_TX
    RETURN

CMD_RESET
    ; Reset tutti i servo a 90°
    MOVLW   SERVO_0
    MOVWF   FSR
    MOVLW   D'8'
    MOVWF   TMP
    MOVLW   D'30'
RST_LOOP
    MOVWF   INDF
    INCF    FSR,F
    DECFSZ  TMP,F
    GOTO    RST_LOOP
    MOVLW   'K'
    CALL    USART_TX
    RETURN

CMD_PLAY
    MOVLW   1
    MOVWF   SEQ_ACTIVE
    MOVLW   'K'
    CALL    USART_TX
    RETURN

CMD_STOP
    CLRF    SEQ_ACTIVE
    MOVLW   'K'
    CALL    USART_TX
    RETURN

CMD_WRITE
    CALL    SAVE_ALL
    MOVLW   'K'
    CALL    USART_TX
    RETURN

; ============================================================
; DO_SET — imposta servo ARG_CH a ARG_VAL
; ============================================================
DO_SET
    ; Valida canale
    MOVF    ARG_CH,W
    SUBLW   D'7'
    BTFSS   STATUS,C
    GOTO    SET_ERR
    ; Valida valore
    MOVF    ARG_VAL,W
    SUBLW   D'19'
    BTFSC   STATUS,C
    GOTO    SET_ERR
    MOVF    ARG_VAL,W
    SUBLW   D'40'
    BTFSS   STATUS,C
    GOTO    SET_ERR
    ; Scrivi SERVO[CH]
    BCF     INTCON,GIE
    MOVLW   SERVO_0
    ADDWF   ARG_CH,W
    MOVWF   FSR
    MOVF    ARG_VAL,W
    MOVWF   INDF
    BSF     INTCON,GIE
    MOVLW   'K'
    CALL    USART_TX
    RETURN
SET_ERR
    MOVLW   'E'
    CALL    USART_TX
    RETURN

; ============================================================
; DO_GET — risponde con posizione servo ARG_CH
; ============================================================
DO_GET
    MOVF    ARG_CH,W
    SUBLW   D'7'
    BTFSS   STATUS,C
    GOTO    GET_ERR
    MOVLW   SERVO_0
    ADDWF   ARG_CH,W
    MOVWF   FSR
    MOVF    INDF,W
    CALL    USART_TX           ; invia posizione
    RETURN
GET_ERR
    MOVLW   'E'
    CALL    USART_TX
    RETURN

    ORG     0x0140
INIT
    BSF     STATUS,RP0
    CLRF    TRISB
    MOVLW   D'25'              ; SPBRG per 9600 baud
    MOVWF   SPBRG
    MOVLW   b'00100100'        ; TXEN=1, BRGH=1
    MOVWF   TXSTA
    BCF     STATUS,RP0
    MOVLW   b'10010000'        ; SPEN=1, CREN=1
    MOVWF   RCSTA
    CLRF    PARSE_STATE
    CLRF    SEQ_ACTIVE
    CLRF    FLAGS
    CALL    LOAD_ALL           ; carica posizioni da EEPROM

    ; Messaggio di avvio
    MOVLW   'S'
    CALL    USART_TX
    MOVLW   'V'
    CALL    USART_TX
    MOVLW   'C'
    CALL    USART_TX
    MOVLW   '8'
    CALL    USART_TX
    MOVLW   0x0D
    CALL    USART_TX           ; CR

; ============================================================
; MAIN — polling USART e parser
; ============================================================
MAIN
    BSF     STATUS,RP0
    BTFSS   PIR1,RCIF
    BCF     STATUS,RP0
    BTFSS   PIR1,RCIF
    GOTO    MAIN
    BCF     STATUS,RP0
    MOVF    RCREG,W
    MOVWF   RX_BYTE

    ; State machine parser
    MOVF    PARSE_STATE,F
    BTFSS   STATUS,Z
    GOTO    PARSE_NOT0

    ; Stato 0: attendi comando
    MOVWF   CMD_BYTE
    MOVF    RX_BYTE,W
    SUBLW   'S'
    BTFSC   STATUS,Z
    GOTO    EXPECT_CH          ; &#x27;S&#x27; vuole canale
    MOVF    RX_BYTE,W
    SUBLW   'G'
    BTFSC   STATUS,Z
    GOTO    EXPECT_CH          ; &#x27;G&#x27; vuole canale
    ; Comando senza argomenti: esegui direttamente
    CALL    DO_COMMAND
    GOTO    MAIN

EXPECT_CH
    MOVLW   1
    MOVWF   PARSE_STATE
    GOTO    MAIN

PARSE_NOT0
    MOVF    PARSE_STATE,W
    SUBLW   1
    BTFSC   STATUS,Z
    GOTO    PARSE_STATE1
    ; Stato 2: aspetta valore
    MOVF    RX_BYTE,W
    MOVWF   ARG_VAL
    CLRF    PARSE_STATE
    ; Esegui 'S' o 'G'
    MOVF    CMD_BYTE,W
    SUBLW   'S'
    BTFSC   STATUS,Z
    CALL    DO_SET
    MOVF    CMD_BYTE,W
    SUBLW   'G'
    BTFSC   STATUS,Z
    CALL    DO_GET
    GOTO    MAIN

PARSE_STATE1
    MOVF    RX_BYTE,W
    MOVWF   ARG_CH
    MOVF    CMD_BYTE,W
    SUBLW   'G'
    BTFSC   STATUS,Z
    GOTO    EXEC_GET_NOW       ; &#x27;G&#x27; non ha valore: esegui subito
    MOVLW   2
    MOVWF   PARSE_STATE
    GOTO    MAIN
EXEC_GET_NOW
    CLRF    PARSE_STATE
    CALL    DO_GET
    GOTO    MAIN

    END

🧪 Prova tu

  1. Carica Modulo 1. Imposta SERVO_0=30 (90°). Verifica la ISR: con TMR0=206 e T0IE=1, conta manualmente i cicli fino al primo interrupt. PULSE_TICK deve partire da 30 e decrementare ogni overflow. Dopo 30 overflow (~1.5ms) PORTB bit0 deve abbassarsi e ACTIVE_CH diventare 1.
  2. Testa l’interpolazione (Modulo 2): imposta SERVO_0=20, TGT_0=40, STEPS_0=20. Chiama INTERP_CH0 venti volte (una per frame simulato). Dopo ogni chiamata traccia SERVO_0: deve aumentare di 1 ogni volta fino a raggiungere 40. Poi chiama un’altra volta: SERVO_0 deve restare 40 (IC0_DONE).
  3. Testa USART (Modulo 3): simula la ricezione di ‘S’, 0x02, 0x1E (servo 2, 90°). Dopo i tre byte PARSE_STATE deve tornare 0 e SERVO_2 deve valere 30 (0x1E = 30 decimale). Poi invia ‘G’, 0x02: deve rispondere con 30. Invia ‘R’: tutti i servo tornano a 30, risponde ‘K’.
  4. Sfida: aggiungi il comando ‘I’ (info): il PIC risponde con tutti e 8 i valori SERVO_0..7 in sequenza (8 byte). Modifica DO_COMMAND per gestire ‘I’ con un loop FSR che trasmette SERVO_0..7 via USART_TX. Quante istruzioni aggiuntive servono?

❓ Quiz rapido

1. Perché la ISR del PWM disabilita gli interrupt durante la scrittura di SERVO[CH] nel main (BCF INTCON,GIE / BSF INTCON,GIE), e cosa succederebbe senza questa protezione?

Mostra risposta

La sequenza di scrittura su SERVO[CH] nel main usa FSR: prima si calcola l’indirizzo (MOVLW SERVO_0 + ADDWF ARG_CH,W + MOVWF FSR) poi si scrive (MOVF ARG_VAL,W + MOVWF INDF). Se la ISR del Timer0 interviene tra MOVWF FSR e MOVWF INDF, potrebbe modificare FSR per accedere a SERVO_N nel ciclo PWM, cambiando il puntatore nel mezzo della scrittura del main. Risultato: il valore verrebbe scritto in un indirizzo sbagliato (un altro servo o RAM casuale). Disabilitando GIE per la coppia FSR+INDF si crea una sezione critica atomica — la ISR può aspettare i pochi cicli necessari senza conseguenze sul timing PWM.


2. Il SPBRG per 9600 baud a 4MHz con BRGH=1 è calcolato come 25. Quale baud rate si ottiene esattamente, e qual è l’errore percentuale?

Mostra risposta

Formula con BRGH=1: baud = FOSC / (16 × (SPBRG+1)) = 4000000 / (16 × 26) = 4000000 / 416 = 9615.38 baud. L’errore rispetto a 9600 baud è (9615.38 − 9600) / 9600 × 100 = +0.16%. L’USART tollera errori fino a ±3% — 0.16% è ampiamente dentro le specifiche. Per riferimento, con BRGH=0: baud = FOSC / (64 × (SPBRG+1)); per 9600: SPBRG = 4000000/(64×9600) − 1 = 5.51 → arrotondato a 6, errore = 4000000/(64×7) − 9600 = 8929 − 9600 = −7.4% — troppo alto, per questo si preferisce BRGH=1.


3. Il Modulo 2 usa una tabella RETLW come lista di mosse in Flash. Qual è il limite pratico alla lunghezza della sequenza, e come si potrebbe estendere oltre questo limite?

Mostra risposta

La tabella usa ADDWF PCL,F per il salto indicizzato — questo funziona solo entro 256 word dalla base (una pagina). Con 3 byte per mossa (canale, target, durata), una pagina ospita al massimo 85 mosse (255/3). Per sequenze più lunghe: 1) usare PCLATH per accedere a pagine oltre la prima, impostando il byte alto del PC prima del salto; 2) memorizzare la sequenza in EEPROM interna (256 byte = 85 mosse) e leggerla con EEP_RD — più lenta ma illimitata al costo della Flash; 3) usare una struttura dati in RAM con FSR al posto del PCL jump — perdendo la compattezza Flash ma guadagnando flessibilità. Il PIC16F877A ha 1024 word di Flash suddivise in 4 pagine da 256 word — con PCLATH si possono raggiungere tutte e 4.

Ultima lezione del corso →

⭐ Lezione 42 — Progetto 5: Sistema integrato completo

Retrocomputer didattico con PIC16F877A: mini-interprete di comandi seriali, gestione task cooperativa (round-robin), watchdog software, auto-test RAM al boot. Il progetto finale che unisce tutto ciò che hai imparato.

© Prof. D. Bertolino · PIC16 From Zero to Hero · WebPicSimulator · Lezione 41