Qualcuno potrebbe spiegare questo strano codice, usato per impostare i timer?


10

Mentre guardo gli schizzi che altre persone hanno scritto, di tanto in tanto mi imbatto in un codice che assomiglia in qualche modo a questo:

TCCR1A = 0;
TCCR1B = 0;

TCNT1 = 34286;
TCCR1B |= (1 << CS12);
TIMSK1 |= (1 << TOIE1);

Tutto quello che so è che ha qualcosa a che fare con i tempi / i timer (penso). Come posso decifrare e creare codice come questo? Quali sono TCCR1A, TCCR1B, TCNT1, CS12, TIMSK1, e TOIE1?



1
Scarica il foglio dati "Completo" per il tuo dispositivo dal sito Web Atmel e leggi i capitoli sui timer. A mio avviso, la scheda tecnica è sorprendentemente buona da leggere.
jippie,

Risposte:


15

Questo non è strano. È come appare il normale codice MCU.

Quello che hai qui è un esempio del concetto di periferiche mappate in memoria . Fondamentalmente, l'hardware MCU ha posizioni speciali nello spazio degli indirizzi SRAM dell'MCU assegnato ad esso. Se si scrive a questi indirizzi, i bit del byte scritti sull'indirizzo n controllano il comportamento della periferica m .

Fondamentalmente, alcuni banchi di memoria hanno letteralmente dei piccoli fili che vanno dalla cella SRAM all'hardware. Se si scrive un "1" su questo bit in quel byte, imposta questa cella SRAM su un valore logico alto, che quindi attiva una parte dell'hardware.

Se osservi le intestazioni dell'MCU, ci sono grandi tabelle di mappature di indirizzi di parole chiave <->. Ecco come cose come TCCR1Betc ... vengono risolte in fase di compilazione.

Questo meccanismo di mappatura della memoria è ampiamente utilizzato nelle MCU. L'ATMega MCU nell'Arduino lo usa, così come le serie MCU PIC, ARM, MSP430, STM32 e STM8, così come molte MCU che non conosco immediatamente.


Il codice Arduino è roba strana, con funzioni che accedono indirettamente al controllo MCU. Anche se è un po 'più "bello", è anche molto più lento e utilizza molto più spazio del programma.

Le costanti misteriose sono tutte descritte in dettaglio nel foglio dati ATmega328P , che dovresti davvero leggere se sei interessato a fare qualcosa di più che attivare e disattivare i pin su un arduino.

Seleziona estratti dal foglio dati collegato sopra:

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

Quindi, ad esempio, TIMSK1 |= (1 << TOIE1);imposta il bit TOIE1in TIMSK1. Ciò si ottiene spostando i binari 1 ( 0b00000001) a sinistra di TOIE1bit, TOIE1definendoli in un file di intestazione come 0. Questo viene quindi OR bit a bit nel valore corrente di TIMSK1, che di fatto imposta questo bit alto.

Guardando la documentazione per il bit 0 di TIMSK1, possiamo vedere che è descritta come

Quando questo bit viene scritto su uno e viene impostato il flag I nel registro di stato (interrupt abilitati a livello globale), viene attivato l'interrupt di overflow Timer / Counter1. Il vettore di interrupt corrispondente (vedere "Interrupt" a pagina 57) viene eseguito quando viene impostato il flag TOV1, situato in TIFR1.

Tutte le altre righe dovrebbero essere interpretate allo stesso modo.


Alcune note:

Potresti anche vedere cose come TIMSK1 |= _BV(TOIE1);. _BV()è una macro comunemente usata originariamente dall'implementazione di libc AVR . _BV(TOIE1)è funzionalmente identico a (1 << TOIE1), con il vantaggio di una migliore leggibilità.

Inoltre, potresti anche vedere righe come: TIMSK1 &= ~(1 << TOIE1);o TIMSK1 &= ~_BV(TOIE1);. Questo ha la funzione opposta di TIMSK1 |= _BV(TOIE1);, in quanto disinserisce il bit TOIE1in TIMSK1. Questo si ottiene prendendo la maschera di bit prodotta da _BV(TOIE1), eseguendo un'operazione NOT bit a bit su di essa ( ~), e quindi ANDing TIMSK1con questo valore NOTed (che è 0b11111110).

Si noti che in tutti questi casi, il valore di cose come (1 << TOIE1)o _BV(TOIE1)sono completamente risolte al momento della compilazione , quindi si riducono funzionalmente a una costante semplice e quindi non richiedono tempi di esecuzione per il calcolo in fase di esecuzione.


Il codice scritto correttamente avrà generalmente dei commenti in linea con il codice che descrivono in dettaglio cosa devono fare i registri. Ecco una routine soft-SPI abbastanza semplice che ho scritto di recente:

uint8_t transactByteADC(uint8_t outByte)
{
    // Transfers one byte to the ADC, and receives one byte at the same time
    // does nothing with the chip-select
    // MSB first, data clocked on the rising edge

    uint8_t loopCnt;
    uint8_t retDat = 0;

    for (loopCnt = 0; loopCnt < 8; loopCnt++)
    {
        if (outByte & 0x80)         // if current bit is high
            PORTC |= _BV(ADC_MOSI);     // set data line
        else
            PORTC &= ~(_BV(ADC_MOSI));  // else unset it

        outByte <<= 1;              // and shift the output data over for the next iteration
        retDat <<= 1;               // shift over the data read back

        PORTC |= _BV(ADC_SCK);          // Set the clock high

        if (PINC & _BV(ADC_MISO))       // sample the input line
            retDat |= 0x01;         // and set the bit in the retval if the input is high

        PORTC &= ~(_BV(ADC_SCK));       // set clock low
    }
    return retDat;
}

PORTCè il registro che controlla il valore dei pin di uscita all'interno PORTCdi ATmega328P. PINCè il registro in cui sono disponibili i valori di input di PORTC. Fondamentalmente, cose del genere sono ciò che accade internamente quando si usano le funzioni digitalWriteo digitalRead. Tuttavia, esiste un'operazione di ricerca che converte i "numeri di pin" arduino in numeri di pin hardware effettivi, che porta da qualche parte nel regno di 50 cicli di clock. Come probabilmente puoi immaginare, se stai cercando di andare veloce, sprecare 50 cicli di clock in un'operazione che dovrebbe richiedere solo 1 è un po 'ridicolo.

La funzione sopra probabilmente porta da qualche parte nel regno di 100-200 cicli di clock per trasferire 8 bit. Ciò comporta 24 pin-write e 8 letture. Questo è molte, molte volte più veloce dell'uso delle digital{stuff}funzioni.


Si noti che questo codice dovrebbe funzionare anche con Atmega32u4 (utilizzato in Leonardo) in quanto contiene più timer di ATmega328P.
jfpoilpret,

1
@Ricardo - Probabilmente il 90% + del codice incorporato per piccole MCU usa la manipolazione diretta del registro. Fare cose con funzioni di utilità indiretta non è molto la modalità comune di manipolazione di IO / periferiche. Ci sono alcuni toolkit per sottrarre il controllo hardware (The Atmel ASF, per esempio), ma generalmente è scritto per compilare il più possibile per ridurre il sovraccarico di runtime e quasi invariabilmente richiede effettivamente la comprensione delle periferiche leggendo i fogli dati.
Connor Wolf,

1
Fondamentalmente, roba di arduino, dicendo "qui ci sono funzioni che fanno X", senza preoccuparsi davvero di fare riferimento alla documentazione effettiva o al modo in cui l'hardware sta facendo le cose che fa, non è molto normale. Capisco che sia un valore come strumento introduttivo, ma a parte la prototipazione rapida, non viene mai realmente realizzato in ambienti professionali reali.
Connor Wolf,

1
Per essere chiari, la cosa che rende insolito il codice Arduino per il firmware MCU incorporato non è unica per il codice Arduino, è una funzione dell'approccio globale. Fondamentalmente, una volta che hai una buona comprensione dell'MCU reale , fare le cose correttamente (ad esempio usando direttamente i registri hardware) richiede poco o nessun tempo aggiuntivo. Come tale, se vuoi imparare il vero sviluppatore MCU, è molto meglio semplicemente sedersi e capire cosa sta effettivamente facendo il tuo MCU , piuttosto che fare affidamento sull'astrazione di qualcun altro , che tende a perdere.
Connor Wolf,

1
Nota che qui posso essere un po 'cinico, ma i comportamenti che vedo nella comunità di Arduino stanno programmando anti-schemi. Vedo molta programmazione "copia-incolla", il trattamento delle biblioteche come black-box e solo pratiche di progettazione generali di scarsa qualità nella comunità in generale. Certo, sono abbastanza attivo su EE.stackexchange, quindi potrei avere una visione un po 'inclinata, dato che ho alcuni strumenti di moderazione e come tale vedo molte domande chiuse. C'è sicuramente un pregiudizio nelle domande arduino che ho visto lì per "dimmi cosa C&P per risolvere", piuttosto che "perché questo non funziona".
Connor Wolf,

3

TCCR1A è il registro di controllo timer / contatore 1 A

TCCR1B è il registro di controllo timer / contatore 1 B

TCNT1 è il valore del contatore del timer / contatore 1

CS12 è il 3o bit di selezione dell'orologio per timer / contatore 1

TIMSK1 è il registro della maschera di interruzione del timer / contatore 1

TOIE1 è l'abilitazione dell'interrupt di overflow timer / contatore 1

Pertanto, il codice abilita il timer / contatore 1 a 62,5 kHz e imposta il valore su 34286. Quindi abilita l'interruzione di overflow, quindi quando raggiunge 65535, attiverà la funzione di interruzione, molto probabilmente etichettata come ISR(timer0_overflow_vect)


1

CS12 ha un valore di 2 poiché rappresenta il bit 2 del registro TCCR1B.

(1 << CS12) prende il valore 1 (0b00000001) e lo sposta a sinistra 2 volte per ottenere (0b00000100). L'ordine delle operazioni impone che le cose in () accadano per prime, quindi questo viene fatto prima che venga valutato "| =".

(1 << CS10) prende il valore 1 (0b00000001) e lo sposta a sinistra 0 volte per ottenere (0b00000001). L'ordine delle operazioni impone che le cose in () accadano per prime, quindi questo viene fatto prima che venga valutato "| =".

Quindi ora otteniamo TCCR1B | = 0b00000101, che è lo stesso di TCCR1B = TCCR1B | 0b00000101.

Dal "|" è "OR", tutti i bit diversi da CS12 in TCCR1B non sono interessati.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.