Esto no tiene un aspecto extraño. Así es como se ve el código MCU normal.
Lo que tienes aquí es un ejemplo del concepto de periféricos mapeados en memoria . Básicamente, el hardware de MCU tiene ubicaciones especiales en el espacio de direcciones SRAM de la MCU asignada. Si escribe en estas direcciones, los bits del byte escrito en la dirección n controlan el comportamiento del periférico m .
Básicamente, ciertos bancos de memoria tienen literalmente pequeños cables que van desde la celda SRAM al hardware. Si escribe un "1" en este bit en ese byte, establece esta celda SRAM en un valor lógico alto, que luego activa alguna parte del hardware.
Si observa los encabezados de la MCU, existen grandes tablas de asignaciones de direcciones de palabras clave <->. Así es como TCCR1B
se resuelven cosas como etc. en tiempo de compilación.
Este mecanismo de mapeo de memoria es extremadamente utilizado en MCU. La MCU ATmega en el arduino lo usa, al igual que las series MCU PIC, ARM, MSP430, STM32 y STM8, así como muchas MCU con las que no estoy familiarizado de inmediato.
El código Arduino es algo extraño, con funciones que acceden a los registros de control de MCU indirectamente. Si bien esto tiene un aspecto algo "más agradable", también es mucho más lento y utiliza mucho más espacio en el programa.
Todas las constantes misteriosas se describen con gran detalle en la hoja de datos ATmega328P , que realmente debería leer si está interesado en hacer algo más que pasadores de palanca en un arduino.
Seleccione extractos de la hoja de datos vinculada anteriormente:
Así, por ejemplo, TIMSK1 |= (1 << TOIE1);
establece el bit TOIE1
en TIMSK1
. Esto se logra desplazando el binario 1 ( 0b00000001
) a la izquierda por TOIE1
bits, y TOIE1
se define en un archivo de encabezado como 0. Esto luego se OR en bits en el valor actual de TIMSK1
, que efectivamente establece este bit alto.
Mirando la documentación para el bit 0 de TIMSK1
, podemos ver que se describe como
Cuando este bit se escribe en uno, y se establece el indicador I en el Registro de estado (interrupciones habilitadas globalmente), se habilita la interrupción de desbordamiento del temporizador / contador1. El vector de interrupción correspondiente (consulte "Interrupciones" en la página 57) se ejecuta cuando se establece el indicador TOV1, ubicado en TIFR1.
Todas las otras líneas deben interpretarse de la misma manera.
Algunas notas:
También puede ver cosas como TIMSK1 |= _BV(TOIE1);
. _BV()
es una macro comúnmente utilizada originalmente de la implementación AVR libc . _BV(TOIE1)
es funcionalmente idéntico a (1 << TOIE1)
, con el beneficio de una mejor legibilidad.
Además, también puede ver líneas como: TIMSK1 &= ~(1 << TOIE1);
o TIMSK1 &= ~_BV(TOIE1);
. Esto tiene la función opuesta de TIMSK1 |= _BV(TOIE1);
, en el que se desarma el bit TOIE1
en TIMSK1
. Esto se logra tomando la máscara de bits producida por _BV(TOIE1)
, realizando una operación NOT bit a bit ( ~
), y luego AND TIMSK1
por este valor NOTed (que es 0b11111110).
Tenga en cuenta que en todos estos casos, el valor de cosas como (1 << TOIE1)
o _BV(TOIE1)
se resuelven por completo en tiempo de compilación , por lo que se reducen funcionalmente a una constante simple y, por lo tanto, no requieren tiempo de ejecución para calcular en tiempo de ejecución.
El código escrito correctamente generalmente tendrá comentarios en línea con el código que detalla lo que los registros asignados deben hacer. Aquí hay una rutina de SPI suave bastante simple que escribí recientemente:
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
es el registro que controla el valor de los pines de salida dentro PORTC
del ATmega328P. PINC
es el registro donde están disponibles los valores de entrada de PORTC
. Básicamente, cosas como esta son las que ocurren internamente cuando usas las funciones digitalWrite
o digitalRead
. Sin embargo, hay una operación de búsqueda que convierte los "números de pin" de arduino en números de pin de hardware reales, lo que lleva a algún lugar en el ámbito de los 50 ciclos de reloj. Como probablemente pueda adivinar, si está tratando de ir rápido, desperdiciar 50 ciclos de reloj en una operación que solo debería requerir 1 es un poco ridículo.
La función anterior probablemente tome algún lugar en el ámbito de 100-200 ciclos de reloj para transferir 8 bits. Esto implica 24 escrituras pin y 8 lecturas. Esto es mucho, muchas veces más rápido que usar las digital{stuff}
funciones.