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.
; ============================================================ ; 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 "5" (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.
⚡ 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'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.
⚠️ 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
- 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.
- 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.
- 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).
- 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