Lavorare con i registri.

Che cos'è un registro? Qui è tutto molto semplice: si tratta di blocchi di RAM ultra veloci con un volume di 1 Byte, situati accanto al core MCU e alle periferiche. Dal lato del programma, si tratta di normali variabili globali che possono essere lette e modificate. I registri del microcontrollore memorizzano le "impostazioni" per le sue varie periferiche: timer-contatori, porte con pin, ADC, bus UART, I2C, SPI e altro hardware integrato nel MCU. Modificando il registro, diamo un comando quasi diretto al microcontrollore cosa e come fare. La scrittura nel registro richiede circa un ciclo MCU (0,0625 µs a una frequenza di clock di 16 MHz). I nomi dei registri sono fissi, un elenco completo con una descrizione dettagliata si trova nel datasheet del microcontrollore (il datasheet ufficiale dell'ATmega328 è Arduino Nano/UNO/Mini). 
Lavorare con i registri è molto difficile se non li hai imparati tutti a memoria, perché i loro nomi di solito sono illeggibili, abbreviazioni. Le funzioni Arduino funzionano effettivamente con i registri, lasciandoci con una funzione comoda, comprensibile e leggibile, non c'è nulla di soprannaturale in questo. Perché lavorare direttamente con i registri? Ci sono diversi grandi vantaggi:

  • Velocità di lavoro: la lettura/scrittura dei registri viene eseguita il più rapidamente possibile, il che consente di velocizzare il lavoro con MCU (ad esempio, tirare i pin manualmente invece di digitalWrite()). La velocità massima di lavoro con le periferiche MCU è tutt'altro che sempre necessaria, quindi se non hai bisogno di risparmiare qualche microsecondo, non hai nemmeno bisogno di pasticciare con i registri.
  • Dimensioni della memoria: il lavoro diretto con i registri ti consente di lavorare con le periferiche MCU nel modo più compatto possibile, leggendo e scrivendo solo i bit necessari, quindi il codice specifico scritto per le tue attività occuperà meno spazio rispetto alle funzioni e alle librerie universali già pronte di qualcun altro ( ad esempio, lo stesso digitalWrite o lavorare con Serial). Quando lavori con Arduino, non puoi quasi mai preoccuparti di questo, ma se fai un progetto su ATTiny, dovrai comprimere il codice fino al byte.
  • Flessibilità delle impostazioni: lavorare con il microcontrollore direttamente utilizzando i registri consente di configurare in modo molto flessibile le periferiche per le proprie attività. Il fatto è che tutte le librerie Arduino esistenti coprono poco più della metà di tutte le capacità del microcontrollore! Un numero enorme di impostazioni e trucchi utili non sono descritti da nessuna parte tranne che per il datasheet e per usarli è necessario essere in grado di leggere questo stesso datasheet e lavorare con i registri.
Nei microcontrollori della serie ATmega / ATtiny, i registri sono a 8 bit, ovvero un registro è una variabile del tipo byte. Per quanto ne sappiamo, un byte è un numero da 0 a 255, quindi ogni registro ha 255 impostazioni? No, la logica qui è completamente diversa: un byte è 8 bit, ovvero un registro memorizza 8 impostazioni che possono essere attivate/disattivate. Prendiamo come esempio uno dei registri del Timer 1, chiamato TCCR1B. Immagine dal datasheet su ATmega328p:

Il registro TCCR1B è composto da 8 bit. Quasi ogni bit ha un nome (tranne il 5° bit, che non è usato in questo MCU). Ciò che fa ogni bit e registro è descritto nel datasheet nel modo più dettagliato. I nomi di tutti i bit e dei registri sono impostati nel compilatore, ovvero non è possibile creare variabili con lo stesso nome. Anche i valori dei bit non possono essere modificati, sono costanti:

int WGM12; // risulterà in un errore
CS11 = 5; // risulterà in un errore

Ma puoi leggere il valore di un bit dal suo nome. Inoltre, il valore sarà uguale al suo numero nel registro, contando da destra. CS11 è 1, WGM13 è 4, ICNC1 è 7 (vedi tabella sopra).
Penso che qui sia tutto chiaro: c'è un registro (byte) che ha un nome univoco ed è composto da 8 bit, ogni bit ha anche un nome univoco con il quale puoi ottenere il numero di questo bit nel byte del suo registro. Resta da capire come utilizzarlo tutto.

Scrivi/leggi registro.
Esistono diversi modi per impostare i bit nei registri. Li esamineremo tutti in modo che quando ne incontri uno, saprai di cosa si tratta in generale e come funziona questa riga di codice. Nell'argomento precedente sulle operazioni sui bit, abbiamo discusso in dettaglio tutto ciò che riguarda la manipolazione dei bit, quindi se l'hai letto e compreso, le seguenti informazioni non saranno nuove per te.
Torniamo al registro timer che ho mostrato sopra e proviamo a configurarlo. Il primo modo consiste nell'impostare in modo esplicito l'intero byte in una volta, con tutti uno e zero. Puoi farlo in questo modo:
TCCR1B = 0b01010101;
Pertanto, abbiamo attivato e disattivato i bit necessari contemporaneamente, in un colpo solo. Come ricordi dall'argomento sui tipi di dati e sui numeri, al microcontrollore non importa in quale sistema di calcolo si lavora, cioè il numero 0b01010101 che abbiamo in binario, in decimale sarà 85 e in esadecimale sarà 0x55. E queste tre opzioni sono esattamente le stesse in termini di risultato:
TCCR1B = 0b01010101;
TCCR1B = 85;
TCCR1B = 0x55;
Puoi solo guardare il primo e capire subito di cosa si tratta, cosa che non si può dire degli altri due. Molto spesso negli schizzi di altre persone c'è una tale registrazione, e questo non è molto comodo.
Molto più spesso capita di dover "mirare" un bit in un byte, e qui le funzioni logiche (operazioni bit) e macro vengono in soccorso. Considera tutte le opzioni, in tutte BYTE è un registro di byte e BIT è un numero di bit, contando da destra. Cioè, BIT è un numero da 0 a 7, o il nome di un bit dal foglio dati.
Impostare il bit su 1   Impostare il bit su 0    Descrizione
BYTE |= (1 << BIT);      BYTE &= ~(1 << BIT);  Spostamento bit <<.
BYTE |= (2^BIT);          BYTE &= ~(2^BIT);       2 alla potenza del numero di bit.
BYTE |= bit(BIT);         BYTE &= ~bit(bit);        Macro di Arduino bit() che sostituisce spostamento.
BYTE |= _BV(BIT);       BYTE &= ~_BV(BIT);     Funzione incorporata _BV(), di nuovo spostamento.
sbi(BYTE, BIT);           cbi(BYTE, BIT);                Usiamo le macro assembler sbi e cbi.
bitSet(BYTE, BIT);      bitClear(BYTE, BIT);      Utilizzo delle funzioni di Arduino bitSet() e bitClear().

Quello che voglio dire sulle opzioni elencate: sono essenzialmente tutte uguali, ovvero la prima, sono semplicemente racchiuse in altre funzioni e macro. Il tempo di esecuzione di tutte le opzioni è lo stesso, perché le macro-funzioni non eseguono azioni non necessarie, ma portano tutti i metodi al primo, con uno spostamento |= e &=. Puoi trovare tutti questi metodi negli schizzi da Internet, questo è un dato di fatto. Personalmente, mi piacciono di più Arduino bitSet e bitClear perché hanno un nome leggibile e si trovano nella libreria.
Per quanto riguarda sbi() e cbi(), le macro devono essere create da qualche parte per usarle:
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) 
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
E dopo puoi usare sbi() e cbi().
Considera un esempio in cui cambiamo semplicemente lo stato di ogni bit in un byte nel registro TCCR1B in modi diversi:
void setup() 
     TCCR1B = 0; // ripristina il registro 
     bitSet(TCCR1B, CS11); // abilita il bit 1 
     TCCR1B |= _BV(4); // abilita il bit 4 
     TCCR1B |= (1 << WGM12); // abilita il bit 3 
     TCCR1B &= ~_BV(WGM13); // Disattiva il bit 4
     bitClear(TCCR1B, 3); // Disattiva il bit 3
}
Puoi anche aggiungere un'opzione in cui puoi "mirare" diversi bit in un byte:
void setup() 
{ 
     TCCR1B = 0; // ripristina il registro 
      // imposta i bit 1, 3 e 4(WGM13) 
     TCCR1B |= _BV(1) | _BV(3) | _BV(WGM13);
}
Penso che qui sia tutto chiaro, ora proviamo a "mirare" leggendo dal registro:
Leggi bit                            Descrizione
(BYTE >> BIT) &1          manualmente tramite spostamento.
bitRead(BYTE, BIT)      Funzione macro Arduino
I metodi precedenti restituiscono 0 o 1 a seconda dello stato del bit. Esempio:
void setup() 
     TCCR1B = 0; // ripristina il registro 
     bitSet(TCCR1B, CS12); // abilita il bit 2 
     Serial.begin(9600); // apre la porta 
     Serial.println(bitRead(TCCR1B, 2)); // visualizza 1
}

Registri a 16 bit.
Gli Arduino (AVR) hanno anche registri doppi a 16 bit, costituiti da due registri a 8 bit, ad esempio ADC "doppio registro" è composto da ADCH e ADCL, oppure il registro timer ICR1 è costituito da ICR1H e ICR1L. Il nostro ADC è a 10 bit, ma i registri sono a 8 bit, quindi una parte (8 bit) viene archiviata in un registro (ADCL) e il resto (2 bit) viene archiviato in un altro (ADCH). Guarda come appare sotto forma di tabella:

Come accettare o modificare un numero a 10 bit se è diviso in due registri diversi? Molto semplice: lavorare con tali doppi registri è integrato nel compilatore e puoi semplicemente lavorare con loro direttamente come con una normale variabile, ad esempio:

int val = ADC; // legge il valore 
ICR1 = 1234; // scrivi il valore

Per qualche ragione, questa notazione è usata raramente, forse per compatibilità con altri compilatori. Molto spesso vedrai questa opzione, in cui i valori sono "incollati" attraverso uno spostamento:

int val = ADCL + (ADCH << 8);
// possibile variante ADCL | (ADCH << 8)

Devi leggere dal registro basso. Non appena leggiamo il registro basso (il primo), MCU è completamente bloccato dall'accesso all'intero registro fino a quando non viene letto il registro alto. Se si legge prima il registro alto, il valore del registro basso potrebbe andare perso.
Problema inverso: c'è ancora un registro immaginario a 16 bit (costituito da due a 8 bit) in cui dobbiamo scrivere un valore. Ad esempio dual register ICR1H e ICR1L, ecco la tabella:

Il microcontrollore può funzionare solo con un byte, ma come possiamo scrivere un numero a due byte? E in questo modo: dividi il numero in due byte usando le funzioni di Arduino highByte() e lowByte() e scrivi questi byte nei registri corrispondenti. Esempio:

uint16_t val = 1500; // solo un numero di tipo int
ICR1H = highByte(val); // scrivi un byte alto
ICR1L = byte basso(val); // scrivi un byte basso
// rileggi i byte e "incolla" in int
byte val_1 = ICR1L;
byte val_2 = ICR1H;
uint16_t value = val_1 + (val_2 << 8); 

Devi scrivere dal byte alto. Non appena scriviamo il byte basso (l'ultimo), l'MK "aggancia" entrambi i registri in memoria, rispettivamente, se scrivi prima il byte basso, il byte alto sarà 0 e la successiva immissione dell'alto sarà ignorato.
IMPORTANTE: la scrittura nei registri a 16 bit viene eseguita partendo dal byte alto, leggendo dal byte basso.
Crea il tuo sito web gratis! Questo sito è stato creato con Webnode. Crea il tuo sito gratuito oggi stesso! Inizia