Isso não é estranho. É como o código MCU normal realmente se parece.
O que você tem aqui é um exemplo do conceito de periféricos mapeados na memória . Basicamente, o hardware do MCU possui locais especiais no espaço de endereço SRAM do MCU atribuído a ele. Se você escreve nesses endereços, os bits do byte gravados no endereço n controlam o comportamento do m periférico .
Basicamente, certos bancos de memória literalmente têm poucos fios que vão da célula SRAM para o hardware. Se você escrever um "1" nesse bit nesse byte, ele definirá essa célula SRAM para um nível lógico alto, que ativará uma parte do hardware.
Se você procurar nos cabeçalhos do MCU, existem grandes tabelas de mapeamentos de endereços de palavras-chave <->. É assim que coisas como TCCR1B
etc ... são resolvidas em tempo de compilação.
Esse mecanismo de mapeamento de memória é extremamente amplamente usado em MCUs. O ATmega MCU do arduino o usa, assim como as séries PIC, ARM, MSP430, STM32 e STM8 MCU, bem como muitas MCUs com as quais não estou familiarizado imediatamente.
O código do Arduino é estranho, com funções que acessam os registros de controle do MCU indiretamente. Embora isso pareça um pouco "mais agradável", também é muito mais lento e usa muito mais espaço do programa.
As constantes misteriosas são todas descritas detalhadamente na folha de dados do ATmega328P , que você realmente deve ler se estiver interessado em fazer algo mais do que alternar pinos localmente em um arduino.
Selecione trechos da folha de dados vinculada acima:
Assim, por exemplo, TIMSK1 |= (1 << TOIE1);
define o bit TOIE1
no TIMSK1
. Isso é obtido deslocando o binário 1 ( 0b00000001
) para a esquerda pelos TOIE1
bits, TOIE1
sendo definido em um arquivo de cabeçalho como 0. Esse valor é OR bit a bit no valor atual de TIMSK1
, que efetivamente define esse bit um pouco alto.
Olhando para a documentação do bit 0 de TIMSK1
, podemos ver que é descrito como
Quando esse bit é gravado em um e o sinalizador I no registro de status é definido (interrupções ativadas globalmente), a interrupção do temporizador / estouro do contador1 é ativada. O vetor de interrupção correspondente (consulte ”Interrupções” na página 57) é executado quando o sinalizador TOV1, localizado em TIFR1, é definido.
Todas as outras linhas devem ser interpretadas da mesma maneira.
Algumas notas:
Você também pode ver coisas como TIMSK1 |= _BV(TOIE1);
. _BV()
é uma macro comumente usada originalmente na implementação libc do AVR . _BV(TOIE1)
é funcionalmente idêntico a (1 << TOIE1)
, com o benefício de melhor legibilidade.
Além disso, você também pode ver linhas como: TIMSK1 &= ~(1 << TOIE1);
ou TIMSK1 &= ~_BV(TOIE1);
. Isso tem a função oposta de TIMSK1 |= _BV(TOIE1);
, na medida em que desativa o bit TOIE1
in TIMSK1
. Isso é obtido usando a máscara de bit produzida por _BV(TOIE1)
, executando uma operação NOT bit a bit nela ( ~
) e depois TIMSK1
ANDing por esse valor NOTed (que é 0b11111110).
Observe que em todos esses casos, o valor de coisas como (1 << TOIE1)
ou _BV(TOIE1)
são totalmente resolvidas no tempo de compilação , portanto, elas se reduzem funcionalmente a uma constante simples e, portanto, não levam tempo de execução para computar no tempo de execução.
O código corretamente escrito geralmente possui comentários alinhados com o código que detalha o que os registros que estão sendo atribuídos devem fazer. Aqui está uma rotina bastante simples de SPI que escrevi recentemente:
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
é o registro que controla o valor dos pinos de saída no PORTC
ATmega328P. PINC
é o registro no qual os valores de entrada de PORTC
estão disponíveis. Fundamentalmente, coisas assim são o que acontece internamente quando você usa as funções digitalWrite
ou digitalRead
. No entanto, há uma operação de pesquisa que converte os "números de pinos" do arduino em números de pinos de hardware reais, o que leva a algo em torno de 50 ciclos de clock. Como você provavelmente pode adivinhar, se você está tentando ir rápido, desperdiçar 50 ciclos de relógio em uma operação que deve exigir apenas 1 é um pouco ridículo.
A função acima provavelmente leva algum lugar no domínio de 100-200 ciclos de clock para transferir 8 bits. Isso implica 24 gravações de pinos e 8 leituras. Isso é muitas, muitas vezes mais rápido que o uso das digital{stuff}
funções.