Alguém poderia explicar esse código de aparência estranha, usado para configurar temporizadores?

10

Ao olhar para esboços que outras pessoas escreveram, ocasionalmente encontro um código parecido com o seguinte:

TCCR1A = 0;
TCCR1B = 0;

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

Tudo o que sei é que isso tem algo a ver com o tempo / temporizadores (eu acho). Como posso decifrar - e criar - códigos como este? Quais são TCCR1A, TCCR1B, TCNT1, CS12, TIMSK1, e TOIE1?

O cara com o chapéu
fonte
Não sei o suficiente para responder, mas: electronics.stackexchange.com/questions/92350/… , forum.arduino.cc/index.php?topic=134602.0 e stackoverflow.com/questions/9475482/… . Não sei se você já viu isso.
Anonymous Penguin
11
Faça o download da folha de dados "Completa" para o seu dispositivo no site da Atmel e leia os capítulos sobre temporizadores. A folha de dados é surpreendentemente boa de ler na minha opinião.
jippie

Respostas:

15

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 TCCR1Betc ... 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:

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

Assim, por exemplo, TIMSK1 |= (1 << TOIE1);define o bit TOIE1no TIMSK1. Isso é obtido deslocando o binário 1 ( 0b00000001) para a esquerda pelos TOIE1bits, TOIE1sendo 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 TOIE1in TIMSK1. Isso é obtido usando a máscara de bit produzida por _BV(TOIE1), executando uma operação NOT bit a bit nela ( ~) e depois TIMSK1ANDing 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 PORTCATmega328P. PINCé o registro no qual os valores de entrada de PORTCestão disponíveis. Fundamentalmente, coisas assim são o que acontece internamente quando você usa as funções digitalWriteou 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.

Connor Wolf
fonte
Observe que esse código também deve funcionar com o Atmega32u4 (usado no Leonardo), pois contém mais timers que o ATmega328P.
Jfpoilpret
11
@ Ricardo - Provavelmente mais de 90% do código incorporado de pequenas MCU usa manipulação direta de registro. Fazer coisas com funções de utilidade indireta não é o modo comum de manipulação de E / S / periféricos. Existem alguns kits de ferramentas para abstrair o controle de hardware (o Atmel ASF, por exemplo), mas isso geralmente é escrito para compilar o máximo possível para reduzir a sobrecarga do tempo de execução, e quase invariavelmente requer realmente entender os periféricos lendo as folhas de dados.
Connor Lobo
11
Basicamente, as coisas do arduino, dizendo "aqui estão as funções que executam o X", sem realmente se preocupar em fazer referência à documentação real ou como o hardware está fazendo as coisas que faz, não é muito normal. Entendo que seu valor é uma ferramenta introdutória, mas, com exceção da prototipagem rápida, ela nunca é realmente realizada em ambientes profissionais reais.
Connor Wolf
11
Para deixar claro, o que torna o código do arduino incomum para o firmware MCU incorporado não é exclusivo do código do arduino, é uma função da abordagem geral. Basicamente, uma vez que você tenha uma compreensão decente do MCU real , fazer as coisas corretamente (por exemplo, usando registros de hardware diretamente) leva pouco ou nenhum tempo adicional. Como tal, se você quiser aprender o verdadeiro desenvolvimento do MCU, é muito melhor sentar e entender o que o MCU está realmente fazendo, em vez de confiar na abstração de outra pessoa , que tende a vazar.
Connor Wolf
11
Observe que posso ser um pouco cínico aqui, mas muitos comportamentos que vejo na comunidade do arduino estão programando antipadrões. Eu vejo muita programação de "copiar e colar", tratar as bibliotecas como caixas-pretas e apenas práticas gerais de design ruins na comunidade em geral. Obviamente, sou bastante ativo no EE.stackexchange, portanto, posso ter uma visão um pouco inclinada, pois tenho algumas ferramentas de moderador e, como tal, vê muitas perguntas fechadas. Definitivamente, existe um viés nas perguntas do arduino que eu já vi sobre "me diga o que a C&P consertar", em vez de "por que isso não está funcionando".
Connor Lobo
3

TCCR1A é timer / contador 1 controle de registro A

TCCR1B é o temporizador / contador 1 registo de controlo B

TCNT1 é o valor do contador / contador 1

CS12 é o terceiro bit de seleção do relógio para o temporizador / contador 1

TIMSK1 é o registro da máscara de interrupção do contador / contador 1

TOIE1 é a interrupção de estouro do timer / contador 1 habilitada

Portanto, o código habilita o timer / contador 1 em 62,5 kHz e define o valor para 34286. Em seguida, habilita a interrupção de estouro para que, quando atingir 65535, ative a função de interrupção, provavelmente rotulada como ISR(timer0_overflow_vect)

O médico
fonte
1

CS12 tem um valor 2, pois representa o bit 2 do registro TCCR1B.

(1 << CS12) pega o valor 1 (0b00000001) e o desloca para a esquerda 2 vezes para obter (0b00000100). A ordem das operações determina que as coisas em () ocorram primeiro, portanto, isso é feito antes da avaliação "| =".

(1 << CS10) pega o valor 1 (0b00000001) e o desloca para a esquerda 0 vezes para obter (0b00000001). A ordem das operações determina que as coisas em () ocorram primeiro, portanto, isso é feito antes da avaliação "| =".

Então agora temos TCCR1B | = 0b00000101, que é o mesmo que TCCR1B = TCCR1B | 0b00000101.

Desde "|" é "OR", todos os bits que não sejam CS12 no TCCR1B não são afetados.

VENKATESAN
fonte