Lezione 38 di 43

Lezione 37

🎓 PIC16 — From Zero to Hero · Lezione 37 · Modulo 6

Bit-Banging: Protocolli Software 🔌

1-Wire con DS18B20 (temperatura). SPI software Modo 0 con DAC MCP4921.
I²C software open-drain con EEPROM 24LC08. Timing preciso con NOP.

🎯 Cosa impari in questa lezione

  • Bit-banging: implementare protocolli seriali manipolando direttamente pin GPIO con NOP per il timing — nessuna periferica hardware necessaria
  • 1-Wire: reset (480µs), write-0 (60µs), write-1 (6µs), read-slot (campiona entro 15µs) — tutto con un solo pin open-drain RC0
  • Sequenza DS18B20: Skip ROM (0xCC) → Convert T (0x44) → attesa 750ms → Read Scratchpad (0xBE) → conversione raw → °C
  • SPI software Modo 0 (CPOL=0, CPHA=0): SCK idle=0, campiona MISO su rising edge, 4 pin (SCK, MOSI, MISO, CS) — genera onda seno su DAC MCP4921 12-bit
  • I²C software open-drain: START (SDA↓ con SCL=1), STOP (SDA↑ con SCL=1), 9° clock per ACK/NACK — mai usare BSF PORTC per forzare alto: solo TRIS=input
  • Scrittura e lettura EEPROM 24LC08: write cycle 5ms, repeated START per lettura random
  • Confronto hardware vs software: periferica HW = interrupt-driven, CPU libera; SW = polling, CPU bloccata ma zero costo hardware

📋 Scheda lezione

Lezione: 37 / 42
Modulo: 6 — Tecniche avanzate (ultima)
Device: PIC16F877A
Prerequisiti: Lez. 28/29 (I²C/SPI HW), Lez. 36 (timing)
Difficoltà: ★★★★★
Tecnica: GPIO bit-banging, timing NOP
Applicazioni: qualsiasi device seriale


1 · 1-Wire: Dallas DS18B20 temperatura

Il protocollo 1-Wire usa un singolo pin bidirezionale open-drain: il master può solo abbassare la linea o rilasciarla (pull-up esterno la porta a 1). Tutto il timing è basato su slot da 60µs: write-0 tiene il bus basso per 60µs, write-1 solo per 6µs poi rilascia, read abbassa per 2µs poi campiona entro 15µs mentre lo slave decide se tenere basso (bit=0) o lasciare salire (bit=1). Il reset è un impulso da 480µs a cui il DS18B20 risponde con un impulso di presenza da 60–240µs.

1-Wire — timing slot e sequenza DS18B20 RESET: WRITE 0: WRITE 1: READ: Master LOW 480µs 70µs slave ↓60-240µs LOW 60µs recovery 10µs ↑ rilascio ~6µs resto slot 54µs HIGH (slave non modifica) ~2µs slave: 1=alto 0=basso fino 15µs campiona entro 15µs → leggi PORTC,0 0 60µs 480µs +70µs
; ============================================================
; Lezione 37 — 1-Wire software: DS18B20 temperatura
; PIC16F877A @ 4 MHz (1 ciclo = 1µs)
; Protocollo 1-Wire: un solo pin bidirezionale open-drain
; Pin: RC0 (TRISC bit0 controlla direzione, PORTC bit0 = dato)
; Open-drain: per inviare 0 → output low; per inviare 1 → input (pull-up)
; Timing critico: tutto basato su slot da 60µs
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

#DEFINE OW_PIN      PORTC,0
#DEFINE OW_TRIS     TRISC,0
#DEFINE OW_INPUT    BSF TRISC,0      ; rilascia bus (input = high-Z)
#DEFINE OW_OUTPUT   BCF TRISC,0     ; prendi controllo (output)
#DEFINE OW_LOW      BCF PORTC,0     ; forza basso
#DEFINE OW_HIGH     BSF PORTC,0     ; forza alto (usato raramente: open-drain)

    CBLOCK  0x20
OW_BYTE         ; byte da inviare/ricevuto
OW_BIT_CNT      ; contatore bit (8)
TEMP_L          ; temperatura raw byte basso
TEMP_H          ; temperatura raw byte alto
TEMP_INT        ; parte intera °C
TEMP_FRAC       ; 0 o 5 (0.5°C)
DELAY_CNT       ; contatore delay
CRC_VAL         ; CRC-8 calcolato
    ENDC

    ORG     0x0000
    GOTO    INIT

; ============================================================
; DELAY_US — ritardo N µs (N in W, overhead ~2µs)
; Ogni iterazione: DECFSZ(1) + GOTO(2) = 3 cicli = 3µs
; Per N µs: passa W = N/3 (approssimato)
; Delay precisi usano NOP inline
; ============================================================
DELAY_US
    MOVWF   DELAY_CNT
DLY DECFSZ  DELAY_CNT,F
    GOTO    DLY
    RETURN

; ============================================================
; OW_RESET — impulso reset e attesa presenza (480µs low)
; Ritorna Z=1 se nessun device, Z=0 se device presente
; ============================================================
OW_RESET
    BSF     STATUS,RP0
    OW_OUTPUT                  ; TRISC,0 = output
    BCF     STATUS,RP0
    OW_LOW                     ; trascina basso
    MOVLW   D'160'             ; 480µs / 3 = 160
    CALL    DELAY_US
    BSF     STATUS,RP0
    OW_INPUT                   ; rilascia bus
    BCF     STATUS,RP0
    ; Attendi 70µs poi campiona presenza (device abbassa per 60-240µs)
    MOVLW   D'23'              ; ~70µs
    CALL    DELAY_US
    ; Leggi pin
    BCF     STATUS,Z
    BTFSC   OW_PIN             ; pin ancora alto? → nessun device
    BSF     STATUS,Z           ; Z=1 → nessun device
    ; Attendi fine slot presenza (~410µs rimanenti)
    MOVLW   D'137'             ; ~410µs
    CALL    DELAY_US
    RETURN

; ============================================================
; OW_WRITE_BIT — invia 1 bit (in bit0 di W)
; Write-1: abbassa 6µs, rilascia 64µs
; Write-0: abbassa 60µs, rilascia 10µs
; ============================================================
OW_WRITE_BIT
    BSF     STATUS,RP0
    OW_OUTPUT
    BCF     STATUS,RP0
    OW_LOW                     ; sempre: abbassa per iniziare slot
    BTFSS   OW_BYTE,0          ; controlla bit LSB
    GOTO    WRITE_ZERO
    ; Write-1: basso 6µs
    NOP                        ; +1µs
    NOP
    NOP
    NOP
    NOP
    NOP                        ; totale ~8µs (incluso overhead)
    BSF     STATUS,RP0
    OW_INPUT
    BCF     STATUS,RP0         ; rilascia presto
    MOVLW   D'19'              ; ~57µs resto slot
    CALL    DELAY_US
    RETURN
WRITE_ZERO
    ; Write-0: basso 60µs
    MOVLW   D'17'              ; ~51µs (già 8µs trascorsi)
    CALL    DELAY_US
    BSF     STATUS,RP0
    OW_INPUT
    BCF     STATUS,RP0
    MOVLW   D'3'               ; ~9µs recovery
    CALL    DELAY_US
    RETURN

; ============================================================
; OW_WRITE_BYTE — invia byte in OW_BYTE, LSB first
; ============================================================
OW_WRITE_BYTE
    MOVLW   D'8'
    MOVWF   OW_BIT_CNT
OWW_LOOP
    CALL    OW_WRITE_BIT
    BCF     STATUS,C
    RRF     OW_BYTE,F          ; shift right: prossimo bit in LSB
    DECFSZ  OW_BIT_CNT,F
    GOTO    OWW_LOOP
    RETURN

; ============================================================
; OW_READ_BIT — leggi 1 bit nel LSB di OW_BYTE
; Master abbassa 1-2µs, poi campiona entro 15µs
; ============================================================
OW_READ_BIT
    BSF     STATUS,RP0
    OW_OUTPUT
    BCF     STATUS,RP0
    OW_LOW
    NOP
    NOP                        ; ~2µs low
    BSF     STATUS,RP0
    OW_INPUT                   ; rilascia
    BCF     STATUS,RP0
    NOP
    NOP
    NOP
    NOP
    NOP                        ; attendi ~5µs poi campiona
    ; Leggi bit e metti in C
    BCF     STATUS,C
    BTFSC   OW_PIN
    BSF     STATUS,C           ; C = valore pin
    ; Shift C in MSB di OW_BYTE (LSB first → accumula da MSB)
    RRF     OW_BYTE,F          ; C → MSB, resto shifta
    ; Attendi fine slot (~50µs)
    MOVLW   D'16'
    CALL    DELAY_US
    RETURN

; ============================================================
; OW_READ_BYTE — leggi 8 bit in OW_BYTE, LSB first
; ============================================================
OW_READ_BYTE
    MOVLW   D'8'
    MOVWF   OW_BIT_CNT
OWR_LOOP
    CALL    OW_READ_BIT
    DECFSZ  OW_BIT_CNT,F
    GOTO    OWR_LOOP
    RETURN

    ORG     0x0100
INIT
    BSF     STATUS,RP0
    MOVLW   b'11111110'        ; RC0=output, resto input
    MOVWF   TRISC
    CLRF    TRISB              ; PORTB output (display)
    BCF     STATUS,RP0
    BCF     PORTC,0            ; prepara livello basso

; ============================================================
; SEQUENZA LETTURA DS18B20
; 1. Reset + presenza
; 2. Skip ROM (0xCC): ignora indirizzo, parla con tutti
; 3. Convert T (0x44): avvia conversione (750ms)
; 4. Reset + Skip ROM
; 5. Read Scratchpad (0xBE): leggi 9 byte
; 6. Byte 0 = TEMP_L, Byte 1 = TEMP_H
; ============================================================
READ_TEMP
    ; Step 1: Reset
    CALL    OW_RESET
    BTFSC   STATUS,Z
    GOTO    NO_DEVICE

    ; Step 2: Skip ROM
    MOVLW   0xCC
    MOVWF   OW_BYTE
    CALL    OW_WRITE_BYTE

    ; Step 3: Convert T
    MOVLW   0x44
    MOVWF   OW_BYTE
    CALL    OW_WRITE_BYTE

    ; Attendi conversione 750ms (parasitic power)
    ; 750ms / 3µs = 250 × ~1000 → loop annidato
    MOVLW   D'250'
    MOVWF   DELAY_CNT
WAIT_CONV
    MOVLW   D'250'             ; ~750µs interni
    CALL    DELAY_US
    DECFSZ  DELAY_CNT,F
    GOTO    WAIT_CONV          ; ~187ms totali (sufficiente per 9-bit)

    ; Step 4: Reset + Skip ROM
    CALL    OW_RESET
    MOVLW   0xCC
    MOVWF   OW_BYTE
    CALL    OW_WRITE_BYTE

    ; Step 5: Read Scratchpad
    MOVLW   0xBE
    MOVWF   OW_BYTE
    CALL    OW_WRITE_BYTE

    ; Leggi byte 0 (TEMP_L) e byte 1 (TEMP_H)
    CALL    OW_READ_BYTE
    MOVF    OW_BYTE,W
    MOVWF   TEMP_L
    CALL    OW_READ_BYTE
    MOVF    OW_BYTE,W
    MOVWF   TEMP_H

    ; Converti raw in °C (risoluzione 9-bit = 0.5°C)
    ; Temperatura = TEMP_H:TEMP_L × 0.0625
    ; Per 9-bit (bit0 di TEMP_L = 0.5°C):
    BCF     STATUS,C
    RRF     TEMP_L,W           ; W = TEMP_L / 2
    MOVWF   TEMP_INT           ; parte intera in °C
    ; parte frazionaria: bit0 originale = 0.5°C
    BTFSC   TEMP_L,0
    MOVLW   D'5'               ; 0.5 → mostra come &quot;5&quot; (decimo)
    BTFSS   TEMP_L,0
    CLRF    TEMP_FRAC
    MOVWF   TEMP_FRAC

    MOVF    TEMP_INT,W
    MOVWF   PORTB              ; es: 36 per 36.x°C

NO_DEVICE
MAIN
    GOTO    READ_TEMP          ; loop continuo

    END

2 · SPI software Modo 0 — DAC MCP4921

SPI software usa 4 pin GPIO e richiede di replicare manualmente il protocollo: abbassa CS, poi per ogni bit imposta MOSI, pulsa SCK alto (campiona MISO sul rising edge), abbassa SCK, avanza al prossimo bit. In Modo 0 SCK parte da 0 e i dati sono validi sul fronte di salita. Il MCP4921 è un DAC SPI a 12 bit con formato comando 16-bit: 4 bit di configurazione seguiti da 12 bit di dato. La tabella seno a 32 entry (vs 256 della Lez.34) occupa solo 33 word Flash ma produce un’onda riconoscibile.

SPI Modo 0 (CPOL=0, CPHA=0) — trasmissione byte 0xA5 = 10100101 CS SCK MOSI MISO b7=1 b6=0 b5=1 b4=0 b3=0 b2=1 b1=0 b0=1 MISO: risposta slave (campionata su rising SCK) — 0x3C in questo esempio ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ = campionamento MISO su rising edge SCK

⚡ HW vs SW SPI: con la periferica MSSP (Lez.29) il trasferimento di 1 byte richiede ~8µs e la CPU è libera di fare altro. Con SPI software ci vogliono ~64µs per byte (8 bit × ~8µs) e la CPU è bloccata nell’intero loop. Per applicazioni non real-time (configurazione dispositivi all’avvio, DAC a bassa frequenza) il bit-banging è perfetto. Per velocità >100kbps o per liberare la CPU usa sempre la periferica hardware.

; ============================================================
; Lezione 37 — SPI software (bit-banging)
; PIC16F877A @ 4 MHz
; Pin: RB3=SCK, RB4=MOSI, RB5=MISO, RB6=CS (active low)
; Modo 0 (CPOL=0, CPHA=0): clock idle=0, campiona su rising edge
; Velocità: ~8µs per bit = ~125kbps (limitato da NOP)
; Applicazione: MCP4921 DAC 12-bit (stesso della Lez.29,
;               qui in versione software senza MSSP)
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

#DEFINE SPI_SCK     PORTB,3    ; Clock
#DEFINE SPI_MOSI    PORTB,4    ; Master Out Slave In
#DEFINE SPI_MISO    PORTB,5    ; Master In Slave Out
#DEFINE SPI_CS      PORTB,6    ; Chip Select (active low)

    CBLOCK  0x20
SPI_TX          ; byte da trasmettere
SPI_RX          ; byte ricevuto
SPI_CNT         ; contatore bit
DAC_HI          ; byte alto comando DAC (config + D11..D8)
DAC_LO          ; byte basso comando DAC (D7..D0)
PHASE           ; fase generatore d&#x27;onda (0-255)
TMP
    ENDC

    ORG     0x0000
    GOTO    INIT

; ============================================================
; SPI_INIT — configura pin
; ============================================================
SPI_INIT
    BSF     STATUS,RP0
    ; RB3=SCK out, RB4=MOSI out, RB5=MISO in, RB6=CS out
    MOVLW   b'00100000'        ; RB5=input, resto output
    MOVWF   TRISB
    BCF     STATUS,RP0
    BCF     SPI_SCK            ; SCK idle = 0 (CPOL=0)
    BSF     SPI_CS             ; CS idle = high (deselect)
    RETURN

; ============================================================
; SPI_BYTE — trasferisci SPI_TX, ricevi in SPI_RX
; Modo 0: campiona MISO su rising edge di SCK
; MSB first (standard)
; ============================================================
SPI_BYTE
    MOVLW   D'8'
    MOVWF   SPI_CNT
    CLRF    SPI_RX
SPI_LOOP
    ; Setup MOSI (MSB di SPI_TX)
    BCF     SPI_MOSI
    BTFSC   SPI_TX,7
    BSF     SPI_MOSI           ; bit da trasmettere

    ; Rising edge SCK
    BSF     SPI_SCK
    NOP
    NOP                        ; setup time ~2µs

    ; Campiona MISO
    BCF     STATUS,C
    BTFSC   SPI_MISO
    BSF     STATUS,C           ; C = MISO
    RLF     SPI_RX,F           ; shift in SPI_RX (MSB first)

    ; Falling edge SCK
    BCF     SPI_SCK
    NOP                        ; hold time

    ; Shift TX per prossimo bit
    RLF     SPI_TX,F           ; MSB uscente, prossimo bit sale

    DECFSZ  SPI_CNT,F
    GOTO    SPI_LOOP
    RETURN

; ============================================================
; DAC_WRITE — scrive valore 12-bit su MCP4921
; Input: DAC_HI (bit11..8 + config), DAC_LO (bit7..0)
; Formato comando MCP4921:
;   bit15: A/B (canale, sempre 0 per canale A)
;   bit14: BUF (buffer Vref, 0=unbuffered)
;   bit13: GA  (gain, 1=1×, 0=2×)
;   bit12: SHDN (1=active)
;   bit11..0: dato 12-bit
; Per output 0-Vref: 0b0x110000_xxxxxxxx
; ============================================================
DAC_WRITE
    BCF     SPI_CS             ; seleziona DAC
    NOP
    NOP                        ; CS setup time

    MOVF    DAC_HI,W
    MOVWF   SPI_TX
    CALL    SPI_BYTE           ; invia byte alto

    MOVF    DAC_LO,W
    MOVWF   SPI_TX
    CALL    SPI_BYTE           ; invia byte basso

    BSF     SPI_CS             ; deseleziona (latch DAC)
    RETURN

; ============================================================
; Tabella seno 32 valori (8-bit, scalata 0-255)
; Meno precisa di 256, ma molto più compatta (32 word)
; ============================================================
    ORG     0x0080
SIN32
    ADDWF   PCL,F
    RETLW   D'128' ; 0
    RETLW   D'153' ; 1
    RETLW   D'176' ; 2
    RETLW   D'196' ; 3
    RETLW   D'212' ; 4
    RETLW   D'222' ; 5
    RETLW   D'228' ; 6
    RETLW   D'227' ; 7
    RETLW   D'221' ; 8
    RETLW   D'210' ; 9
    RETLW   D'194' ; 10
    RETLW   D'175' ; 11
    RETLW   D'154' ; 12
    RETLW   D'132' ; 13
    RETLW   D'111' ; 14
    RETLW   D'91'  ; 15
    RETLW   D'74'  ; 16
    RETLW   D'59'  ; 17
    RETLW   D'47'  ; 18
    RETLW   D'39'  ; 19
    RETLW   D'35'  ; 20
    RETLW   D'35'  ; 21
    RETLW   D'39'  ; 22
    RETLW   D'47'  ; 23
    RETLW   D'59'  ; 24
    RETLW   D'74'  ; 25
    RETLW   D'91'  ; 26
    RETLW   D'111' ; 27
    RETLW   D'132' ; 28
    RETLW   D'153' ; 29
    RETLW   D'176' ; 30
    RETLW   D'196' ; 31

    ORG     0x0120
INIT
    CALL    SPI_INIT
    CLRF    PHASE

; ============================================================
; MAIN — genera onda sinusoidale sul DAC via SPI software
; Frequenza ≈ 4MHz / (32 campioni × ~50 cicli/campione) ≈ 2.5kHz
; ============================================================
MAIN
    ; Leggi campione seno
    MOVF    PHASE,W
    ANDLW   0x1F               ; modulo 32
    CALL    SIN32              ; W = campione 0-255

    ; Scala a 12 bit: moltiplica per 16 (shift left 4)
    ; Valore 8-bit: 0-255 → 12-bit: 0-4080
    ; Strategia: high nibble del byte alto = valore alto 8-bit >> 4
    ; DAC_HI = 0x30 | (valore >> 4)   (config + bit11..8)
    ; DAC_LO = (valore << 4) & 0xF0   (bit7..4 = bit3..0 del valore)
    MOVWF   TMP                ; salva campione (0-255)

    ; Calcola DAC_HI
    SWAPF   TMP,W              ; nibble swap: bit7..4 → bit3..0
    ANDLW   0x0F               ; isola nibble basso (bit11..8 del DAC)
    IORLW   b'00110000'        ; config: BUF=0, GA=1×, SHDN=active
    MOVWF   DAC_HI

    ; Calcola DAC_LO
    SWAPF   TMP,W              ; nibble swap ancora
    ANDLW   0xF0               ; nibble alto (bit7..4 del DAC)
    MOVWF   DAC_LO

    CALL    DAC_WRITE

    INCF    PHASE,F            ; avanza fase
    GOTO    MAIN

    END

3 · I²C software open-drain — EEPROM 24LC08

I²C software è il più delicato del trio perché l’open-drain impone una regola ferrea: non si scrive mai 1 su PORTC per forzare la linea alta. Il modo corretto è configurare il pin come input (BSF TRISC,3) e lasciare che il pull-up esterno (tipicamente 4.7kΩ) porti la linea a Vcc. Per portare la linea bassa si configura come output (BCF TRISC,3) con PORTC già a 0. Le condizioni START e STOP sfruttano questo: START è l’unico momento in cui SDA scende mentre SCL è alta, STOP è l’unico in cui SDA sale con SCL alta — qualsiasi altra transizione di SDA avviene solo quando SCL è bassa.

I²C — condizioni START/STOP e trasmissione byte con ACK SCL SDA START SDA↓ SCL=1 b7 b6 b5 b4 b3 b2 b1 b0 ACK slave↓ STOP SDA↑ SCL=1 Open-drain: per HIGH → TRISC=input (pull-up esterno tira SDA/SCL a Vcc) · per LOW → TRISC=output + PORTC=0 Mai scrivere 1 su PORTC — si colegano due driver e si brucia il pin o il dispositivo

⚠️ Pull-up obbligatori: senza resistori pull-up su SCL e SDA il bus I²C non funziona — le linee restano basse. Valori tipici: 4.7kΩ per 100kHz (standard mode), 2.2kΩ per 400kHz (fast mode). Un pull-up troppo alto (>10kΩ) causa fronti di salita lenti e errori di lettura. Un pull-up troppo basso (<1kΩ) aumenta il consumo corrente e può non soddisfare le specifiche elettriche degli slave. Con alimentazione 3.3V usa 2.2kΩ–4.7kΩ, con 5V preferisci 4.7kΩ–10kΩ.

; ============================================================
; Lezione 37 — I²C software (bit-banging)
; PIC16F877A @ 4 MHz
; Pin: RC3=SCL (clock), RC4=SDA (dati)
; Open-drain: non portare mai alto in output — solo input o low!
; Velocità standard: 100kHz → periodo 10µs, half-period 5µs
; Applicazione: scrive su EEPROM 24LC08 (8Kbit, 8 pagine × 128)
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

; Open-drain: per HIGH → configura come input (pull-up esterno)
;             per LOW  → configura come output + scrivi 0
#DEFINE SCL_HIGH    BSF TRISC,3    ; SCL = input (high-Z) → pull-up→1
#DEFINE SCL_LOW     BCF TRISC,3    ; SCL = output low
#DEFINE SDA_HIGH    BSF TRISC,4    ; SDA = input (high-Z)
#DEFINE SDA_LOW     BCF TRISC,4    ; SDA = output low
#DEFINE SDA_READ    PORTC,4        ; leggi valore SDA

    CBLOCK  0x20
I2C_ADDR        ; indirizzo slave (7 bit + R/W in LSB)
I2C_DATA        ; byte da inviare/ricevuto
I2C_CNT         ; contatore bit
I2C_ACK         ; 0=ACK, 1=NACK (ricevuto o da inviare)
MEM_ADDR        ; indirizzo memoria EEPROM (byte)
WRITE_VAL       ; valore da scrivere
DLY             ; delay
    ENDC

    ORG     0x0000
    GOTO    INIT

; ============================================================
; I2C_DELAY — mezzo periodo I²C = ~5µs (20 cicli)
; ============================================================
I2C_HALF_BIT
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP                        ; ~18 NOP + overhead = ~20 cicli = 5µs
    RETURN

; ============================================================
; I2C_START — condizione di START: SDA↓ mentre SCL=1
; SCL e SDA devono essere 1 (idle) prima di chiamare
; ============================================================
I2C_START
    BSF     STATUS,RP0
    SDA_HIGH
    SCL_HIGH                   ; assicura idle
    BCF     STATUS,RP0
    CALL    I2C_HALF_BIT
    BSF     STATUS,RP0
    SDA_LOW                    ; SDA abbassa (START)
    BCF     STATUS,RP0
    BCF     PORTC,4            ; forza output 0
    CALL    I2C_HALF_BIT
    BSF     STATUS,RP0
    SCL_LOW                    ; SCL abbassa → fine START
    BCF     STATUS,RP0
    RETURN

; ============================================================
; I2C_STOP — condizione di STOP: SDA↑ mentre SCL=1
; ============================================================
I2C_STOP
    BSF     STATUS,RP0
    SDA_LOW                    ; assicura SDA bassa
    BCF     STATUS,RP0
    BCF     PORTC,4
    CALL    I2C_HALF_BIT
    BSF     STATUS,RP0
    SCL_HIGH                   ; SCL sale
    BCF     STATUS,RP0
    CALL    I2C_HALF_BIT
    BSF     STATUS,RP0
    SDA_HIGH                   ; SDA sale mentre SCL=1 → STOP
    BCF     STATUS,RP0
    CALL    I2C_HALF_BIT
    RETURN

; ============================================================
; I2C_WRITE_BYTE — invia I2C_DATA, riceve ACK in I2C_ACK
; I2C_ACK = 0 → ACK (slave pronto), 1 → NACK (errore/fine)
; ============================================================
I2C_WRITE_BYTE
    MOVLW   D'8'
    MOVWF   I2C_CNT
I2C_WB_LOOP
    BSF     STATUS,RP0
    ; Imposta SDA in base a MSB di I2C_DATA
    BTFSS   I2C_DATA,7
    SDA_LOW                    ; bit = 0 → output low
    BTFSC   I2C_DATA,7
    SDA_HIGH                   ; bit = 1 → input (high-Z)
    BCF     STATUS,RP0
    ; Se SDA_LOW, scrivi 0 esplicitamente
    BTFSS   I2C_DATA,7
    BCF     PORTC,4

    CALL    I2C_HALF_BIT       ; setup

    BSF     STATUS,RP0
    SCL_HIGH                   ; clock pulse alto
    BCF     STATUS,RP0
    CALL    I2C_HALF_BIT

    BSF     STATUS,RP0
    SCL_LOW
    BCF     STATUS,RP0

    RLF     I2C_DATA,F         ; prossimo bit in MSB
    DECFSZ  I2C_CNT,F
    GOTO    I2C_WB_LOOP

    ; Leggi ACK: 9° clock pulse, SDA controllata dallo slave
    BSF     STATUS,RP0
    SDA_HIGH                   ; rilascia SDA
    BCF     STATUS,RP0
    CALL    I2C_HALF_BIT

    BSF     STATUS,RP0
    SCL_HIGH
    BCF     STATUS,RP0
    CALL    I2C_HALF_BIT

    ; Campiona ACK
    CLRF    I2C_ACK
    BTFSC   SDA_READ           ; SDA=1 → NACK
    BSF     I2C_ACK,0

    BSF     STATUS,RP0
    SCL_LOW
    BCF     STATUS,RP0
    RETURN

; ============================================================
; I2C_READ_BYTE — ricevi byte in I2C_DATA, invia ACK/NACK
; I2C_ACK = 0 → invia ACK (voglio altro), 1 → invia NACK (ultimo)
; ============================================================
I2C_READ_BYTE
    MOVLW   D'8'
    MOVWF   I2C_CNT
    CLRF    I2C_DATA
    BSF     STATUS,RP0
    SDA_HIGH                   ; rilascia SDA
    BCF     STATUS,RP0
I2C_RB_LOOP
    CALL    I2C_HALF_BIT
    BSF     STATUS,RP0
    SCL_HIGH
    BCF     STATUS,RP0
    CALL    I2C_HALF_BIT

    ; Campiona bit
    BCF     STATUS,C
    BTFSC   SDA_READ
    BSF     STATUS,C
    RLF     I2C_DATA,F         ; shift in bit (MSB first)

    BSF     STATUS,RP0
    SCL_LOW
    BCF     STATUS,RP0
    DECFSZ  I2C_CNT,F
    GOTO    I2C_RB_LOOP

    ; Invia ACK/NACK
    BSF     STATUS,RP0
    BTFSS   I2C_ACK,0
    SDA_LOW                    ; ACK: SDA bassa
    BTFSC   I2C_ACK,0
    SDA_HIGH                   ; NACK: SDA alta
    BCF     STATUS,RP0
    BTFSS   I2C_ACK,0
    BCF     PORTC,4
    CALL    I2C_HALF_BIT

    BSF     STATUS,RP0
    SCL_HIGH
    BCF     STATUS,RP0
    CALL    I2C_HALF_BIT
    BSF     STATUS,RP0
    SCL_LOW
    BCF     STATUS,RP0
    RETURN

    ORG     0x0100
INIT
    ; Configura RC3=SCL, RC4=SDA come open-drain (start input)
    BSF     STATUS,RP0
    BSF     TRISC,3            ; SCL input (pull-up → 1)
    BSF     TRISC,4            ; SDA input (pull-up → 1)
    CLRF    TRISB
    BCF     STATUS,RP0
    BCF     PORTC,3            ; prepara 0 (usato quando TRIS=output)
    BCF     PORTC,4

; ============================================================
; SCRITTURA EEPROM 24LC08: indirizzo 0x50, byte reg 0x10 = 0xAB
; Sequenza: START | ADDR(W) | MEM_ADDR | DATA | STOP
; Poi attesa write cycle 5ms
; ============================================================
    CALL    I2C_START
    ; Indirizzo 24LC08 (Write): 1010_000_0 = 0xA0
    MOVLW   0xA0
    MOVWF   I2C_DATA
    CALL    I2C_WRITE_BYTE     ; → ACK da slave

    ; Indirizzo memoria
    MOVLW   0x10
    MOVWF   I2C_DATA
    CALL    I2C_WRITE_BYTE

    ; Dato
    MOVLW   0xAB
    MOVWF   I2C_DATA
    CALL    I2C_WRITE_BYTE

    CALL    I2C_STOP

    ; Attesa write cycle (~5ms)
    MOVLW   D'200'
    MOVWF   DLY
WAIT_WR
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP                        ; ~20µs per loop
    DECFSZ  DLY,F
    GOTO    WAIT_WR            ; 200 × 20µs = 4ms ≈ 5ms

; ============================================================
; LETTURA EEPROM: leggi byte da indirizzo 0x10
; Sequenza: START|ADDR(W)|MEM_ADDR|ReSTART|ADDR(R)|DATA(NACK)|STOP
; ============================================================
    CALL    I2C_START
    MOVLW   0xA0               ; Write per puntare indirizzo
    MOVWF   I2C_DATA
    CALL    I2C_WRITE_BYTE

    MOVLW   0x10
    MOVWF   I2C_DATA
    CALL    I2C_WRITE_BYTE

    CALL    I2C_START          ; Repeated START
    MOVLW   0xA1               ; Read: bit0=1
    MOVWF   I2C_DATA
    CALL    I2C_WRITE_BYTE

    MOVLW   1                  ; NACK dopo (ultimo byte)
    MOVWF   I2C_ACK
    CALL    I2C_READ_BYTE      ; I2C_DATA = 0xAB

    CALL    I2C_STOP

    MOVF    I2C_DATA,W
    MOVWF   PORTB              ; mostra 0xAB = 171

MAIN
    GOTO    MAIN

    END

🧪 Prova tu

  1. Carica il codice 1-Wire. Simula il reset: dopo OW_RESET, verifica che STATUS,Z sia 0 (device presente). Segui la sequenza a passo singolo: osserva TRISC bit0 alternare tra 0 (output/low) e 1 (input/rilasciato) durante i timing slot. Conta i cicli tra un’operazione e l’altra: OW_WRITE_BYTE per 0xCC deve durare 8 slot × 70µs ≈ 560µs.
  2. Carica il SPI software. Osserva PORTB bit per bit durante SPI_BYTE: RB3=SCK sale e scende 8 volte, RB4=MOSI cambia prima di ogni rising SCK, RB6=CS basso durante il trasferimento. Verifica che per 0xA5 (10100101) MOSI segua la sequenza 1-0-1-0-0-1-0-1 (MSB first). Calcola la frequenza: 8 bit × ~8µs/bit = 64µs per byte → ~15.6kbps.
  3. Carica l’I²C software. Singolo-step su I2C_START: verifica che SCL e SDA siano entrambi input (1) prima, poi SDA diventi output-low mentre SCL rimane alto → condizione START. Poi scrivi 0xA0 su I2C_DATA e chiama I2C_WRITE_BYTE: osserva le 8+1 pulsazioni SCK. Dopo il 9° clock, I2C_ACK deve essere 0 (ACK ricevuto).
  4. Sfida: implementa un protocollo di comunicazione custom su 1 pin (half-duplex). Inventa un protocollo semplice: 1ms basso = START, poi bit a 200µs/slot (basso=0, alto=1), 1ms basso = STOP. Scrivi TX_CUSTOM_BYTE e RX_CUSTOM_BYTE. Collega due PIC16 e fai comunicare il sensore di temperatura DS18B20 del PIC1 con il display del PIC2 — tutto su 1 filo dati + massa comune.

❓ Quiz rapido

1. Nel protocollo 1-Wire, perché il master abbassa la linea per 60µs per write-0 ma solo 6µs per write-1, e perché il slave deve campionare entro 15µs per read?

Mostra risposta

Il 1-Wire distingue i bit in base alla durata dell’abbassamento: write-0 tiene il bus basso per l’intero slot (60µs), write-1 lo abbassa brevemente (6µs) poi rilascia. Lo slave campiona il bus a circa 30µs dall’inizio dello slot: a 30µs dopo il write-0 la linea è ancora bassa → riceve 0; dopo il write-1 la linea è già risalita → riceve 1. Per la lettura il master abbassa per 2µs poi rilascia, e lo slave ha una finestra di 15µs per rispondere: se vuole inviare 0 mantiene la linea bassa fino a 60µs, se vuole inviare 1 lascia risalire il pull-up. Il master campiona a ~10µs dove la distinzione è netta. Dopo i 15µs di campionamento il master attende il completamento del 60µs slot prima del prossimo bit.


2. Nel SPI software il loop di trasmissione usa RLF SPI_TX,F per estrarre il bit MSB. Come funziona esattamente, e cosa succede al valore di SPI_TX dopo 8 iterazioni?

Mostra risposta

Ogni iterazione: (1) si legge il bit7 di SPI_TX per impostare MOSI; (2) RLF SPI_TX,F ruota a sinistra SPI_TX attraverso il carry — il bit7 (MSB appena trasmesso) esce nel carry, tutti gli altri bit si spostano di una posizione verso sinistra, il carry precedente entra nel bit0. Dopo 8 iterazioni SPI_TX è stato ruotato di 8 posizioni — se il carry era 0 all’inizio, SPI_TX torna al valore originale (rotazione completa di 8 bit = identità). L’effetto collaterale è che dopo la trasmissione SPI_TX non è più il valore originale se il carry non era 0 — per usare SPI_TX dopo la chiamata bisogna salvarlo prima.


3. Nel I²C software, cosa succede se si omette il repeated START nella sequenza di lettura e si manda direttamente l’indirizzo di Read dopo l’indirizzo di memoria?

Mostra risposta

Il repeated START è necessario perché dopo aver inviato l’indirizzo di memoria (operazione di Write) il master deve cambiare direzione e rileggere. La sequenza corretta è: START → ADDR_W → MEM_ADDR → rSTART → ADDR_R → DATA → STOP. Se si omette il rSTART e si invia direttamente ADDR_R, lo slave lo interpreta come un byte di dati da scrivere nella memoria — non come un nuovo ciclo. Il risultato è corruzione della EEPROM (si scrive 0xA1 nell’indirizzo successivo) e nessun dato viene letto. Il repeated START è l’unico modo per cambiare direzione senza rilasciare il bus, impedendo ad altri master di “rubare” la transazione in un sistema multi-master.

🏁 Fine Modulo 6 — Tecniche Avanzate

Hai completato il Modulo 6! Conosci ora FSM, lookup table, aritmetica fixed-point, gestione RAM multi-banco, ottimizzazione cicli e tre protocolli seriali implementati da zero. Sei pronto per i Progetti Completi del Modulo 7.

Prossima lezione →

Lezione 38 — Progetto 1: Termostato digitale completo
DS18B20 + display 7-segmenti multiplexato + relè di controllo + isteresi + EEPROM per setpoint persistente. Un progetto reale da cima a fondo.

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