Minha Bancada

Conteúdos, Práticas e Experimentações em Eletrônica

Total de Visitas: 63
Principal Conteúdos Projetos Turmas Sobre

Temporizadores e Contadores

Temporizadores e Contadores

04/JAN/2024




Seção 1: Definição


Como se lê na Wikipedia, contadores são circuitos lógicos sequenciais, construídos para perceber a ocorrência de algum evento, e incrementar (ou decrementar) um valor armazenado. Quando implementados de forma discreta, os contadores são construídos interligando circuitos de flip-flop. Entretanto podem ser construídos de forma integrada, como um circuito exclusivo, ou como parte de um circuito de microcontrolador, provendo a função de contagem.

Do ponto de vista de classificação, os contadores podem ser diferenciados por:

Um importante tópico de discussão diz respeito ao tipo de evento a ser contado. Frequentemente vai-se utilizar o próprio sinal de "clock" como sinal "fonte" da contagem. Neste momento pode surgir uma dúvida: qual a utilidade de contar pulsos de clock? Esta pergunta pode ser respondida de muitas formas diferentes... mas, de uma forma geral, todas irão indicar que, garantida a precisão do sinal de clock, será muito simples a conversão do número de pulsos contados, em tempo transcorrido. Neste contexto, os circuitos de contador poderão ser utilizados para construir temporizadores.

Com efeito, o evento de contagem é, frequentemente, associado a um sinal de “clock”. Assim, ele vai armazenar valores referentes à contagem dos ciclos de “clock”. Os valores de contagem máximos e mínimos irão variar em função da quantidade de bits associada a esse contador. Neste cenário, a variação dos valores acumulados no contador vai refletir uma indicação de tempo decorrido. Como exemplo, se o sinal utilizado como evento de contagem do contador (o sinal de “clock”) apresentar um período de 1s (um segundo), um valor acumulado de 363 significará um tempo decorrido de 363 segundos (ou, 6 minutos e 3 segundos).

Temporizadores são circuitos eletrônicos construídos para controlar a duração de algum evento. Numa perspectiva da eletrônica digital, os temporizadores são dispositivos mono-estáveis, produzindo uma saída que apresentará um determinado estado (alto ou baixo) por um período de tempo previamente definido. Assim, considerando a disponibilidade de um sinal de clock preciso (com período bem definido e estável), os contadores podem ser usados para definir um período de temporização. Neste contexto, é bastante natural que contadores e temporizadores atuem de forma conjunta, para criar uma gama bastante ampla de aplicações distintas.

O uso de temporizadores / contadores torna-se absolutamente justificado sempre e quando é necessário garantir a acurácia e / ou a precisão de eventos de tempo. Alguns exemplos de aplicações sensíveis a tempo são:

Aplicações como essas vão requerer a implementação de um circuito preciso, que meça o tempo de forma autônoma e independente de outros eventos. É justamente nestas aplicações que se faz necessária a utilização de circuitos especializados com temporizadores / contadores.

Voltando ao tema da implementação de contadores e temporizadores, discutimos que podem ser construídos de forma discreta, através da associação de flip flops, ou podem ser implementados em circuitos integrados especializados. Neste caso, serão utilizados para implementar funcionalidades específicas de contagem, temporização ou divisão de frequências, e utilizados como parte de um projeto mais amplo, em eletrônica digital.

Por outro lado, os contadores / temporizadores também podem ser encontrados como blocos funcionais em circuitos de microcontroladores. De fato, quase todos os microcontroladores disponíveis na atualidade apresentam circuitos de contadores / temporizadores, que podem ser utilizados para as mais diversas aplicações.

Um contador / temporizador implementado em um microprocessador vai apresentar as seguintes características funcionais:



Seção 2: Temporizadores e Contadores no Atmel ATmega 328P




Seção 2.1: Introdução

O Atmel ATmega 328P possui 3 temporizadores / contadores. São eles:

Desta forma, TC0 e TC2 podem contar faixas de números representadas por 8 bits (no máximo, de 0 a 255) enquanto TC1 pode contar faixas de números de 16 bits (entre 0 e 65.535). Cada um dos 3 temporizadores / contadores possui duas unidades de comparação (A e B) e podem operar como contadores ou gerando sinais PWM, inclusive, produzindo pulsos de saída independentes. Além disso, TC2 permite operar de forma assíncrona.

O texto a seguir vai trazer mais detalhes sobre o funcionamento destes temporizadores / contadores e discutir as suas principais configurações. O texto pretende ser tão genérico quanto possível, mas, de uma forma geral, e em especial no que concerne ao detalhamento dos exemplos, frequentemente, vai se referir ao TC0. Eventualmente, deve-se destacar as diferentes características encontradas em TC1 ou TC2.

O texto elaborado foi altamente baseado no manual do microcontrolador Atmel ATmega 328P, que continua sendo uma fonte de consulta obrigatória, especialmente no que concerne a um melhor esclarecimento a respeito de tópicos específicos, ou mesmo, no esclarecimento de quaisquer discrepâncias aqui encontradas. Devido a grande quantidade de registradores e bits de controle, o texto inicia com uma seção de nomenclatura, que se propõe a explicar a forma pela qual se vai referir as funcionalidades a eles associados. Esta nomenclatura é a mesma utilizada no manual do microcontrolador e será útil entendê-la bem, caso necessário uma consulta ao manual. Descreve-se então o funcionamento genérico dos contadores e temporizadores e detalham-se os principais modos de operação. Criou-se ainda uma seção em que se explica a forma de configurar os contadores e controladores do microcontrolador a partir da IDE do Arduino, acessando-a diretamente com o C do Arduino. Ao final do texto, apresentam-se dois programas exemplos, mostrando o funcionamento destes dispositivos.

O texto vai usar bastante o termo "registrador". Este conceito foi parcialmente abordado na página Programação com Registradores. Para a compreensão do conteúdo a seguir, deve-se entender que os registradores são posições especiais da memória, as quais possuem nomes específicos de acordo com a sua funcionalidade, podem ser acessadas diretamente, e permitem acesso individual a cada um dos seus bits. Fisicamente, os registradores são "latches" cujos bits podem ser acessados (lidos ou escritos) de forma individual. Caso deseje aprofundar essa compreensão, sugiro buscar alguma literatura de bom nível, a respeito da arquitetura de microprocessadores. Entendo que, para essa finalidade, a Wikipedia e suas referências são sempre um bom ponto de partida.

Como um comentário final, gostaria de alertar para o fato de que as operações de contagem, temporização e conversão Analógica / Digital, usam amplamente o mecanismo de interrupções do processador Atmel Atmega 328P. Para essa finalidade, estou planejando a criação de uma seção explicativa sobre o uso de interrupções. Como esta seção ainda não está pronta, talvez seja uma boa idéia buscar alguma literatura de bom nível sobre esse assunto. Novamente, a Wikipedia é sempre um bom ponto de partida. Por outro lado, o texto a seguir não deve exigir nenhum conhecimento profundo, mas apenas o entendimento do que é uma interrupção, qual o seu efeito, e como ela é tratada. Neste sentido, vou discutir rapidamente esse tema, para ajudar na melhor compreensão do texto.

Existem basicamente duas formas de fazer com que um processo externo interaja com um microprocessador. A primeira técnica é chamada de "polling". A tradução de "poll" para este contexto é bastante peculiar. Numa perspectiva genérica, uma "poll" é uma consulta ampla, como uma eleição, uma pesquisa de opinião, ou ainda um formulário com perguntas que pode ser coletado por alguém que está realizando a tal "poll". No caso dos microporcessadores, o "polling" consiste em um programa que força o microprocessador a, recorrentemente, verificar o status de algum pino de I/O ("input / output", ou entrada / saída, e configurado como pino de entrada, já que o processador vai buscar uma informação externa na leitura do status deste pino). Uma vez verificado o status, o microprocessador decidirá o que fazer em função do status obtido, e em conformidade com o programa em execução. Como exemplo, uma determinada aplicação pode ser programada de tal forma que, sempre que um determinado byte esteja disponível no barramento de leitura, o microprocessador seja informado disso. Esta informação se dará pela alteração do status do tal pino, que foi separado pelo programador, para entregar esta informação ao microporcessador. Note que foi o programador quem escolheu o pino e também foi ele que tomou o cuidado de verificar, de tempos em tempos, qual o status desse pino.

A segunda técnica, muito mais eficiente, é a técnica de interrupção. Neste caso, o projetista do microcontrolador separou um ou mais pinos físicos, exclusivamente para esse fim. Sempre que um desse pinos for acionado (uma alteração de status, por exemplo), o microprocessador imediatamente interrompe a sua execução, e passa a executar um trecho de software, que está escrito numa rotina de interrupção. O uso de interrupções, desobriga o programador da preocupação de monitorar alguma mudança de status. Além disso, libera todos os pinos de I/O para uso das aplicaões. Ao programador cabe apenas escrever a rotina de interrupção, que determinará como o microprocessador deverá se comportar em caso de interrupção. De fato, a interrupção passa a ser considerada como um evento assíncrono, que pode ou não ocorrer, e que, se e quando ocorrer, será tratado imediatamente, conforme determinado na rotina de interrupção. As interrupções devem ser indicadas em pinos de interrupção, especialmente projetados para essa finalidade. Existe também o conceito de interrupção por software. Neste caso, a interrupção não se dará por alguma variação de tensão em um determinado pino mas sim por um evento de software, em algum endereço de memória, para essa finalidade designado.

No caso dos temporizadores e contadores, o fim da contagem ou temporização, PODE gerar uma interrupção, que será tratada de acordo com o requerido pelo programador do microprocessador. Cabe ao programador definir se o evento vai ou não gerar uma interrupção, e o que deverá acontecer neste caso. O Atmel ATmega 328P oferece diversas interrupções já pré-configuradas, as quais podem ser associadas aos eventos de contagem e temporização.

A partir daqui, deixo o texto a seguir, que vai focar na configuração dos temporizadores e contadores do Atmel ATmega 328P.



Seção 2.2: Nomenclatura

Os três temporizadores / contadores possuem muitas características semelhantes e algumas poucas diferenciações entre eles. Por simplicidade, e em conformidade com as definições do próprio manual do processador, eventualmente, vai-se referir a eles, de forma genérica, como TCn, onde n vai se referir ao temporizador / contador 0, 1 ou 2. Vai-se utilizar também a letra x, que vai se referir às unidades de comparação de saída (em inglês, "Output Compare Unit" com sigla OCU) A, B ou C (que existe apenas em TC1). Neste caso, a unidade de controle de saída será referida como OCUx, indicando OCUA, OCUB ou OCUC. Por simplicidade, na nomenclatura dos registradores, frequentemente vai-se omitir a referência ao termo "unidade" e a sua sigla vai omitir a letra U. Assim, OCnx pode indicar a unidade de comparação de saída x do temporizador / contador n.

Os valores de contagem utilizados na configuração serão carregados nos diversos registradores alocados para essa finalidade. Nesse caso, deve-se adotar a seguinte convenção:

Os três temporizadores / contadores são configurados por bits de controle (“flags”) em registradores especiais, voltados para este fim. Da mesma forma, o texto pode se referir, tanto aos registradores quantos aos seus bits, usando a mesma convenção mencionada. Como exemplo, o registrador de controle do contador / temporizador (em inglês, "Timer Counter Control Register, com sigla TCCR) pode ser referido como TCCRnx. Nesse caso, a referência se aplica a TCCR0A, TCCR0B, TCCR1A, TCCR1B, TCCR1C, TCCR2A e TCCR2B. Eventualmente, a referência pode ser mais específica, como, por exemplo, OCRnA. Neste caso, a referência vale para OCR0A, OCR1A e OCR2A.

Finalmente, os bits do registrador também podem ser referidos usando a mesma nomenclatura. No caso dos bits, a nomenclatura deve ainda indicar as posições relativas dos bits, dentro de cada registrador. Como exemplo, os 3 bits reservados para a seleção do modo de relógio (em inglês, "Clock Select" e sigla CS), indicando a fonte de "clock" e eventual prescaler, estão posicionados no registrador TCCRnB e são chamados pela referência CSn[2:0]. Neste caso, pode indicar os bits dedicados a seleção de "clock" (CS), nos registradores TC0, TC1 e TC2. Os colchetes são usados para indicar a posição em conjunto dos bits. Assim, CS0[2:0] vai indicar os 3 bits CS do registrador TC0. Individualmente, serão referidos de forma direta, sem o uso de colchetes: CS02, CS01 e CS00. Nesse caso, deve-se notar que CS02 é o bit CS mais significativo. No caso destes bits, CS02, de fato, está na posição 2 do registrador TCCR0A.

Entretanto, isso nem sempre será verdade. Como exemplo, a configuração do modo de geração de forma de onda (em inglês, "Waveform Generation Mode", com sigla WGM) vai requerer 3 bits (nos TC0 e TC2) ou 4 bits (no TC1). Estes bits estão divididos entre os registradores TCCRnA e TCCRnB. Tomando como exemplo o TC1, os 4 bits dedicados a essa finalidade estão posicionados nos registradores TCCR1A e TCCR1B, e serão referenciados por WGM1[3:0]. Neste caso, indica os 4 bits dedicados a geração de forma de onda no TC1. Ainda assumindo como exemplo o TC1 (e, consequentemente, o TCCR1A, TCCR1B e WGM1), deve-se notar que, os 4 bits WGM1 estão alocados da seguinte formas: WGM13 e WGM12 estão no registrador TCCR1B enquanto que WGM11 e WGNM10 estão no registrador TCCR1B. Destes, WGM13 é, de fato, o bit WGM1 mais significativo. Entretanto, ele não ocupa a posição 3 do registrador TCCR1B, mas sim a posição 4. É importante entender que, ainda que fisicamente dispostos em registradores distintos, os (3, no caso de TC0 1 TC2 ou) 4 bits (no caso de TC1) serão sempre configurados em conjunto, a despeito de sua disposição física.

Como comentário adicional, cabe ao programador conhecer a disposição física de cada conjunto de bits de controle, e configurar o registrador específico de acordo com a sua necessidade. Infelizmente, este tópico é bastante confuso. Ainda que os desenvolvedores de circuitos integrados tenham posto muita energia para, da melhor forma pssível, organizar os bits de controle nos registradores disponíveis no circuito integrado, o resultado não foi muito didático... Entretanto, antes de desitir deste estudo, é sempre importante lembrar que a implementação física de um microntrolador (estudada em uma cadeira de microeletrônica avançada), é muito mais complexa que a eventual dificuldade de entender como e onde os bits de controle foram dispostos. Com isso em mente, recomendo uma pausa restauratória, piscar os olhos, suspirar fundo e seguir em frente!



Seção 2.3: Funcionamento Básico

A Figura 1, a seguir, extraída do manual do Atmel 328P, mostra o diagrama em blocos genérico de um temporizador / contador de 8 bits (TC0 ou TC2).

Diagrama em blocos do temporizador / contador de 8 bits

Figura 1 - Diagrama em blocos do temporizador / contador de 8 bits




De uma forma geral:

  1. Haverá um evento de disparo do dispositivo TCn (TC0 ou TC2, e mesmo TC1, ainda que o diagrama se refira a um contador de 8 bits);
  2. Este evento pode ser um sinal externo (indicado como Tn) ou um sinal de clock interno, derivado do próprio clock da placa do Arduino (ou da placa na qual se utiliza o Atmel 328P – caso em que o circuito microprocessado não seria o Arduino). O "clock" interno é gerado através de um circuito de “prescaler” (este circuito combina diversos ciclos de “clock” da placa do Arduino em um novo “clock” cujo período é maior que o original – como exemplo, um “prescaler” de 8 vai gerar um clock com período 8 vezes maior e, por conseguinte, com frequência 8 vezes menor). Este comportamento será definido pela configuração dos “flags” CSn2 a CSn0, no registrador TCCRnB;
  3. Cada novo evento vai incrementar (ou decrementar) o contador. A sequência de contagem é definida pelos modos de operação do contador / temporizador, configurado através dos “flags” WGMn2 a WGMn0 (no caso de TC0 ou TC2), ou WGM13 a WGM10 (no caso de TC1). Os "flags" WGM são configurados nos registradores TCCRnA e TCCRnB;
  4. O valor da contagem é armazenado no registrador TCNTn;
  5. A todo instante de tempo o valor armazenado em TCNTn é comparado com os valores armazenados em OCRnA e, simultaneamente, em OCRnB;
  6. De acordo com o modo de operação configurado, o resultado da análise do valor de contagem (armazenado em TCNTn) vai interferir na forma como a contagem procede. A figura a seguir apresenta o diagrama de blocos da unidade de contagem:
  7. Diagrama em blocos da unidade de Contagem

    Figura 2 - Diagrama em blocos da unidade de contagem




  8. Como mostrado, a lógica de controle pode determinar: (a) uma contagem (que significa incrementar ou decrementar o valor acumulado em TCNTn de 1 unidade); (b) a re-inicialização do valor acumaulado em TCNTn (de fato, armazenar o valor 0 - BOTTOM - nos bits do registrador); (c) determinar a direção de contagem (se haverá incremento ou decremento do valor em TCNTn); e (d) sinalizar se o valor de contagem chegou aos valores TOP, MAX ou BOTTOM. Além disso, como mostrado, a cadência de contagem é determinada pelo circuito de "prescaler", que efetivamente determina qual será a frequência do "clock" de contagem, dividindo a frequência do "clock" externo, por um fator de "prescaler" configurado;
  9. O resultado positivo da comparação de TCNTn com os valores armazenados em OCRnA e OCRnB (os valores TOP, MAX, BOTTOM, ou quaisquer outros lá configurados) pode ser usado para gerar uma forma de onda, como, por exemplo, um sinal PWM, usando as saídas OCnA e OCnB, acessíveis fisicamente, por pinos, no processador Atmel 328P. Isto é mostrado na Figura 2 e, com mais detalhes, na Figura 3;
  10. Diagrama em blocos da unidade de comparação de saída (Output Compare Unit)

    Figura 3 - Diagrama em blocos da unidade de comparação de saída (Output Compare Unit)




  11. Alternativamente, o resultado das comparações pode ser utilizado para gerar interrupções no processador. Estas interrupções forçam o processador a interromper o seu ciclo normal de processamento e executar uma rotina específica, associada à interrupção em questão;
  12. O cenário em que a comparação resultou em correspondência (dos registradores TCNTn e OCRnx) será sinalizado nos “flags” OCFnA (correspondência de TCNTn e OCRnA) e OCFnB (correspondência de TCNTn e OCRnB), do registrador TIFRn.
  13. Os “flags” de resultado de comparação (OCFnA ou OCFnB) serão habilitados no ciclo de “clock” seguinte aquele em que a correspondência foi verificada. Se o processador estiver habilitado para interrupções, os “flags” OCFnx vão sinalizar uma requisição de interrupção no processador. Uma vez executada a interrupção, os bits correspondentes a OCFnx serão re-inicializados;
  14. O gerador de forma de ondas vai usar o evento de correspondência de comparação para determinar uma saída, que pode variar em função da configuração dos “flags” WGMn2, no registrador TCCR0B, e WGMn1 e WGMn0, no registrador TCCR0A;
  15. Da mesma forma, o comportamento do gerador de forma de onda também será influenciado pelos “flags” COMnx1 e COMnx0, no registrador TCCRnA;
  16. Como se vê, a comparação simultânea de TCNTn com TCCRnA e TCCRnB implementa uma característica de "buffer" duplo. Esta característica pode ser explorada para a implantação de modos de contagem diversos. Os bits WGMn3:0 serão utilizados para definir quais serão esses modos. Tal característica é utilizada nos modos PWM, em que a CPU acessa aos registradores de "buffer", e inibida nos modos Normal e CTC, fazendo com que a CPU acesse diretamente a OCRnx. Ela garante o sincronismo de OCRnx com os valores de TOP ou BOTTOM na sequência de contagem, evitando o efeito de "glitch" nas formas de onda de saída.


Seção 2.4: Descrição dos Registradores de Controle

O comportamento dos temporizadores / contadores é controlado por um conjunto de registradores. Cada um dos registradores tem uma função específica e os seus bits podem ser usados em conjunto,ou de forma individual, para determinar algum comportamento esperado. A Figura 4, a seguir, traz uma tabela que mostra os registradores utilizados no controle dos temporizadores / contadores do Atmel 328P. Para cada registrador, destaca quais bits são usados como “flags”, para sinalizar algum evento ou para disparar um determinado comportamento.

Registradores de Controle dos Temporizadores / Contadores

Figura 4 - Registradores de Controle dos Temporizadores / Contadores

Você deve ter notado que, apesar de muitos registradores comuns, os 3 temporizadores / contadores apresentam importantes diferenças entre eles, seja na quantidade de registradores, ou na disposição de bits. Em especial, o TC1, que é um contador de 16 bits, organiza seus bits de forma distinta de TC0 e TC2. Numa primeira olhada, isso pode parecer confuso (na verdade, acho que a sensação vai ser a mesma, seja qual for a quantdade de olhadas que você der :-) ). Por outro lado, essa complexidade se mostra muito menor quando se usa a técnica de configuração proposta na Seção 3.1, que nos permite configurar diretamente o bit em questão, a despeito de onde ele se posicione.

A seguir, vai-se apresentar um resumo das funcionalidades associadas a cada um desses registradores e do seus bits. As tabelas apresentadas a seguir devem ser relacionadas com as tabelas dos registradores mostrada antes. Recomenda-se fortemente a leitura do manual para esclarecimento de dúvidas ou entendimento de comportamentos inesperados.

De uma forma geral, temos as seguintes funções configuráveis:

Funções Configuráveis

Funções Configuráveis

Funções Configuráveis

Funções Configuráveis

Funções Configuráveis

Figura 5 - Funções Configuráveis

A seguir, vai-se apresentar um resumo das principais configurações. Recomenda-se fortemente a leitura do manual para maiores detalhes ou para configurações distintas dos modos Normal ou CTC.



Seção 2.5: Modos de Operação

Os temporizadores podem operar em 4 modos distintos:



Seção 2.5.1: Modo Normal

O modo Normal é o modo de operação mais simples e é definido, em TC0 e TC2, pelos “flags” WGMn2, no registrador TCCRnB e WGMn1 e WGMn0, no registrador TCCRnA. No caso de TC1, deve-se considerar também WGM13 em TCCR1B. Vamos usar como exemplo o temporizador / contador TC0. Neste caso, faz-se WGM0[2:0] igual a 0x0 (ou seja, faz-se com que WGM02, WGM01 e WGM00 sejam todos iguais a 0). Aqui, vale a pena uma explicação mais em detalhes sobre como é feita esta configuração. Note que, em TC0, WGM0[2:0] é um número de 3 bits (bit 2, bit 1 e bit 0) e acomoda um número decimal entre 0 e 7. Escrever 0 em WGM0[2:0] significa escrever 0 simultanemanete em WGM02, WGM01 e WGM00. A complicação aqui é que WGM02 está em um registrador (TCCR0B) e WGM0[1:0] está em outro (TCCR0A). A Seção 3.1 explica a melhor forma de fazer tais configurações usando o C do Arduino e as Seções 3.2 e 3.3 mostram exemplos de como essas configurações aparecem nos códigos fonte. Para os demais temporizadores / contadores (TC1 e TC2) bastaria ajustar o nome dos registradores e dos bits. A Seção 2.6 discute com mais detalhes a configuração de contagem.

No modo normal, o contador será sempre incrementado a cada novo evento de disparo. Além disso, o valor de contagem vai variar do valor mínimo, chamado de BOTTOM no manual do processador, e com valor de 0x00, ao valor de contagem chamado de TOP, no manual. Neste modo, o valor TOP equivale ao máximo valor de contagem (chamado de MAX, no manual) e vai depender de qual dispositivo está sendo usado (255 no caso do TC0 ou TC2, com 8 bits ou 65.535, no caso do TC1, com 16 bits). Uma vez que o contador alcance o valor TOP, o valor de contagem volta ao valor BOTTOM, e a contagem se reinicia.

É importante notar que, alcançado o valor TOP, o próximo evento de contagem vai levar o valor de contagem ao valor BOTTOM. Se as interrupções forem habilitadas, no mesmo ciclo de “clock” em que o valor do contador é reinicializado, o “flag” TOV no registrador TIFR0 será “ligado” (nível alto), assim permanecendo. Desta forma, este “flag” pode funcionar como se fosse um nono bit de contagem. Assim, um programador poderá usar uma interrupção por “timer overflow” (o evento que leva o contador a se reinicializar) para expandir, por “software”, o valor de contagem, além da limitação dos 8 bits do registrador TC0 (ou TC2). Neste cenário, a interrupção por “timer overflow” vai “desligar” o “flag” TOV. O manual do microcontrolador recomenda que, neste modeo, não se habilite a geração física de formas de onda.



Seção 2.5.2: Modo CTC (Clear Timer on Compare)

Este modo apresenta mais flexibilidade no controle do contador / temporizador. De fato, é possível definir um valor TOP diferente de MAX. Neste cenário, o contador vai se comportar como se houvesse atingido o valor MAX no modo Normal. Tal como no modo Normal, o modo CTC WGMn2, no registrador TCCRnB e WGMn1 e WGMn0, no registrador TCCRnA. No caso de TC1, deve-se considerar também WGM13 em TCCR1B. Vamos usar como exemplo o temporizador / contador TC0. Neste caso, faz-se WGM0[2:0]=0x2. O registrador OCR0A é usado armazenar o valor TOP e, assim, manipular a resolução do contador. Assim que o valor de contagem, no registrador TCNT0, corresponda ao valor TOP, no registrador OCR0A, o contador será reinicializado. Desta forma, o registrador OCR0A define o valor máximo para o contador (TOP) e, daí, também sua resolução. Este modo permite maior controle da frequência de saída do sinal gerado em OCnx e também simplifica a contagem de eventos externos. Para os demais temporizadores / contadores (TC1 e TC2) bastaria ajustar o nome dos registradores e dos bits. A Seção 2.6 discute com mais detalhes a configuração de contagem.

O diagrama de tempo para o modo CTC é mostrado a seguir. Novamente, tomando TC0 como exemplo, o valor do contador (TCNT0) é incrementado até que ocorra uma correspondência entre TCNT0 e OCR0A. Quando isto acontece, o valor em TCNT0 é reinicializado.

Diagrama de Tempos do Modo CTC

Figura 6 - Diagrama de Tempos do Modo CTC




Como mostrado na Figura 3, pode-se gerar uma interrupção sempre que o valor TOP for alcançado. Para isso, deve-se “ligar” o “flag” OCF0A, no registrador TIFR0. Se a interrupção estiver habilitada, a rotina de tratamento de interrupção pode ser usada para atualizar o valor TOP.

Para gerar uma saída de forma de onda, no modo CTC, a saída OC0A pode ser configurada para alternar seu nível lógico a cada correspondência de comparação. Este comportamento foi ilustrado na Figura 3, onde, a cada reinicialização de TCNTn, a forma de onda em OCnx alterna o seu estado. Vamos focar o exemplo na configuração de TC0. Nesse caso, este comportamento é configurado através dos “flags” COM0A1 e COM0A0, no registrador TCCR0A. Para isso, deve-se “escrever” o valor ‘0’ em COM0A1 e ‘1’ em COM0A0 (indicado como TCCR0A.COM0A[1:0]=0x1).

O valor OC0A só será visível no pino de saída do processador se a porta correspondente a esse pino for definida como saída. Aqui, vale a pena lembrar que a função OC0A está disponível no pino 10 do processador 328P, nos "form factors" TQFP e 32 MLF. Este mesmo pino é compartilhado com as funções PCINT22 e AIN0, e se apresenta a nós como a entrada / saída digital 6 (bit 6 da PORTD), na placa do Arduino.

Como exemplo, a máxima frequência de saída do pino OC0A será obtida se não for configurado nenhum "prescaler" e se OCR0A for definido como 0x00. Neste cenário, o contador será iniciado e, no ciclo de "clock" seguinte, será re-inicializado. Desta forma, a forma de onda gerada terá uma frequência máxima de metade da frequência de "clock". Em um cenário mais genérico, pode-se considerar a fórmula a seguir, aplicável às duas saídas OCnx, nos três temporizadores / contadores: Frequência de Saída

Nesta expressão, N representa o fator de “prescaler” (1, 8, 64, 256 ou 1024).

Da mesma forma que no modo Normal, o “flag” TOVn, no registrador TIFRn, é “ligado” no mesmo ciclo de “clock” em que o contador é reinicializado, logo após haver alcançado o valor TOP.



Seção 2.5.3: Modo PWM Rápido ("Fast PWM")

Ainda estou planejando acrescentar ao portal uma página sobre o PWM, tal é a sua importância. De fato, a sigla PWM refere-se a "Pulse Width Modulation", ou, em português, modulação por largura de pulsos. Este conteúdo é típico de uma cadeira introdutória às modulações digitais, em um curso de telecomunicações, em que o tema seria apresentado junto com as demais modulções de pulso (PAM, referindo-se a "Pulse Amplitude Modulation" ou modulação por amplitude de pulso; e PPM, referindo-se a "Pulse Position Modulation", ou modulação por posição de pulso). Entretanto, o PWM acabou se "descolando" da sua natureza de telecomunicações e ganhou uma grande importância com a sua aplicação em Eletrônica de Potência, em que é utilizado na conversão e controle de potência entregue a um determinado sistema elétrico. Na prática, um sinal PWM consiste em uma onda quadrada de período e frequência constantes, mas permitindo variar o tempo em que o pulso está no nível "alto", em relação ao período total do pulso. Esta razão (tempo em que o pulso está alto em relação ao período total) é chamada de "duty cycle". É justamente a variação do "duty cycle" que permite controlar a potência entregue ao sistema elétrico em questão. Na falta do texto, recomendo uma pesquisa em boa bibliografia e, como ponto de partida, (sempre) sugiro a Wikipedia e suas referências.

Este modo (PWM Rápido) permite configurar o temporizador / contador para a geração de um sinal PWM de alta frequência, utilizando o método de rampa simples. O termo "rápido" no modo PWM Rápido se deve ao fato de que a frequência do sinal PWM obtido é o dobro da que se pode obter com o modo "PWM com Correção de Fase", baseado em rampa dupla. Essa frequência mais alta é bastante conveniente em sistemas de voltados para a regulação de potência elétrica, uma vez que permite o uso de componentes (indutores e capacitores) fisicamente menores, reduzindo o custo total do sistema.

Efetivamente, o temporizador / contador é disparado a partir do valor mínimo (BOTTOM) e conta até um número (que será configurado como TOP), calculado para gerar um tempo durante o qual a saída de pulso tem o nível "alto". Uma vez alcançado o valor de contagem, a saída volta ao nível "baixo", e o tempo continua a transcorrer até que se complete o período total do sinal PWM. O "duty cycle" é calculado pela razão entre o tempo máximo de contagem (o tempo para contar até TOP) e o tempo total do ciclo (o tempo até MAX). O termo "rampa simples" vem da alusão ao fato de que o valor da variável de contagem cresce linearmente com o avanço do tempo, formando uma espécie de "rampa". Ao fim da contagem (até TOP), a "rampa" volta a zero, e a saída muda de estado. O processo recomeça no próximo ciclo, após a conclusão do tempo total até MAX. A variação do "duty cycle" é feita com a alteração do valor de TOP, sempre que isso se fizer necessário.

Tomando como exemplo o temporizador / contador TC0, este modo é configurado com os bits WGM (WGM0[2:0] = 0x3 ou 0x7). No primeiro caso (WGM0[2:0] = 0x3), o valor de TOP é ajustado com o valor de MAX (ou seja, é definido como 0xFF). No segundo caso (WGM0[2:0] = 0x7) o valor de TOP é definido em OCR0A. A saída (OC0x) pode se apresentar em dois modos distintos: inversora e não inversora. No modo de saída não inversora, a saída OC0x assume o nível "alto" desde a contagem a partir de BOTTOM, e assim permanece até que TCNT0 se iguale a TOP. No ciclo de "clock" seguinte, OC0x volta ao nível "baixo", e assim permanece durante o tempo de contagem transcorrido entre os valores TOP e MAX. O modo de saída inversora é complementar (oposto, ou barrado, em relação) a esse.

O diagrama de temporização para o modo PWM Rápido é mostrado na Figura 7.

Diagrama de Tempos do Modo PWM Rápido

Figura 7 - Diagrama de Tempos do Modo PWM Rápido




Como indicado na figura:

  1. O valor de contagem está armazenado em TCNT0. Com o avanço da contagem, este valor é incrementado a cada ciclo de "clock". Isto é indicado pela rampa mostrada na figura.
  2. A rampa cresce até que o valor armazenado em TCNT0 se iguale ao valor de TOP.
  3. Nos períodos 1 e 2 indicados, o valor de TOP coincide com MAX. Assim, a contagem (e a rampa) toma toda a duração do período.
  4. O efeito disso se reflete no sinal de saída OCnx e seu complemento. Como se vê, não há varação de nível neste sinal, uma vez que um período se "emenda" ao período seguinte.
  5. A cada vez que TCNT0 alcança o valor de TOP, a situação é indicada no bit TOV0 (bit de "Timer Overflow"). Se as interrupções estiverem habilitadas, pode-se configurar uma rotina de interrupção para atualizar o valor de comparação.
  6. No período 3, o valor de TOP é configurado com um valor inferior a MAX. Neste caso, a comparação positiva entre TCNT0 e OCR0x ocorrerá antes do fim do período do PWM. Este efeito será refletido nas saídas OC0x e seu complemento.

No modo PWM Rápido, a unidade de comparação permite a geração de formas de onda PWM nos pinos OC0x. Configurando os bits COM0x[1:0] para 0x2 vai-se gerar uma saída em modo não invertido. A saída PWM invertida pode ser gerada configurando COM0x[1:0] no valor 0x3.

Se os bits COM0A1:0 forem definidos como 0x1, o pino OC0A vai alternar o seu estado a cada correspondência de comparação. Esta situação vai requerer também que WGM02 tenha sido definido como '1' (Na verdade, WGM0[2:0] deverá ter sido definido como 0x7). Esta opção não está disponível para o pino OC0B. O valor real de OC0x só será percebido no pino físico do microcontrolador (como discutido antes, no pino 10 dos "form factors" discutidos, e apresentados como entrada / saída digital 6, da placa do Arduino), se este pino for configurado como pino de saída. A forma de onda PWM é gerada ao "ligar" (ou "apagar") o "flag" OC0x quando houver uma comparação positiva entre OCR0x e TCNT0, e ao "apagar" (ou "ligar") o "flag" OC0x quando o o contador é zerado (passando de TOP para BOTTOM).

A frequência de saída do PWM pode ser calculada pela equação a seguir. O Valor de N indica o fator de "prescaler" configurado.

Frequência de Saída no Modo PWM Rápido

A configuração do registror OCR0A com valores extremos representa casos especiais do modo PWM Rápido. Se o OCR0A for definido como BOTTOM, a saída consistirá em um pico estreito a cada ciclo de "clock" na contagem MAX + 1. Por outro lado, definir OCR0A igual a MAX resultará em uma saída constantemente alta ou baixa (dependendo da polaridade da saída - os modos invertido e não invertido - definida pelos bits COM0A[1:0]).

Uma saída PWM com "duty cycle" de 50% pode ser alcançada ao se configurar OC0x para alternar o seu valor a cada correspondência de comparação (COM0x[1:0] = 0x1). A forma de onda gerada terá uma frequência máxima igual a metade da que se obtém com OCR0A definido como BOTTOM. Este recurso é semelhante à alternância de OC0A no modo CTC, exceto pelo recurso de "buffer" disponível no modo PWM rápido.



Seção 2.5.4: Modo PWM com correção de Fase

Este modo permite configurar o temporizador / contador para a geração de um sinal PWM de alta frequência, utilizando o método de "rampa dupla".

O modo PWM com correção de fase vai gerar um sinal PWM centrado no meio do período da forma de onda. Desta forma, o pulso de saída mantém a sua fase "cosntante", já que o seu centro sempre coincidirá com o centro do período. Assim, um sinal modulante de valor pequeno será representado por um pulso "fino", que ocorre, ao centro do período. A medida em que o valor do sinal modulante cresce, o pulso se tornará mais "largo", mantendo o seu centro coincidente com o centro do período. Se visualizado num osciloscópio, o sinal PWM vai variar simetricamente para os lados, mantendo o seu centro sempre coincidente com o centro do período.

Esta característica é obtida utilizando o conceito de "rampa dupla", pelo qual o contador do Atmel ATmega 328P inicialmente é incrementado e, ao atingir o valor TOP, passa a ser decrementado até o valor inicial. O período total é formado pelo tempo em que o contador é incrementado, somado ao tempo em que ele é decrementado. Neste sentido, é fácil entender que a fequência máxima do sinal PWM no modo com correção de fase, em que se utiliza a "rampa dupla", é exatamente a metade da frequência do sinal PWM no modo anterior, com a "rampa simples". É por isso que o modo anterior é chamadado de PWM Rápido ("Fast PWM").

O manual do Atmel ATmega 328P afirma que este modo, com correção de fase, baseado em rampa dupla, é mais adequado ao controle de motores. Não encontrei outras referências que embasem essa afirmação e pretendo aprofundar esse tópico no futuro. Por ora, vamos simplesmente "acreditar" na informação do manual e, dependendo da aplicação desejada, preferir o modo com correção de fase.

Tomando como exemplo o temporizador / contador TC0, o modo com correção de fase é configurado pelos bits WGM, fazendo-os iguais a 1 ou 5 (WGM02:0 = 1 ou 5). No modo PWM de correção de fase, o contador é incrementado até que o valor do contador corresponda a TOP. Quando o contador chegar a TOP, muda-se a direção da contagem. O contador conta repetidamente de BOTTOM a TOP e, ao alcançar o valor de TOP, de TOP a BOTTOM. TOP será definido como 0xFF se os bits WGM forem configurados como 1 (WGM2:0 = 1). Alternativamente, TOP pode ser definido com o valor armazenado em OCR0A, fazendo os bits WGM iguais a 5 (WGM2:0 = 5). O valor de TOP pode ser definido a cada ciclo de clock e permanece o mesmo ao longo do ciclo.

Tal como no modo "Fast PWM", o sinal PWM pode se apresentar com uma saída inversora ou não inversora. No modo de comparação de saída não inversora, a comparação de saída (OC0x) apresenta nível "baixo" até que haja uma correspondência de comparação entre TCNT0 e OCR0x, durante a contagem crescente. A partir deste momento, OC0x comuta para o nível "alto" e assim permanece até que haja uma nova correspondência de comparação, durante a contagem decrescente. No modo de saída invertida, a operação é exatamente simétrica.

O diagrama de tempo para o modo PWM com correção de fase é mostrado na Figura 8.

Diagrama de Tempos do Modo PWM com Correção de Fase

Figura 8 - Diagrama de Tempos do Modo PWM com Correção de Fase




Como mostrado:

  1. O ciclo do PWM inclui a contagem de BOTTOM a TOP e de TOP a BOOTOM;
  2. A "rampa dupla" é inferida a partir da evolução do valor de contagem, indicado pelo valor do registrador TCNT0;
  3. As marcas horizontais indicam a correspondência de comparação entre OCR0x e TCNT0;
  4. No início do período 2, OCnx sofre uma transição de alto para baixo. O objetivo dessa transição é garantir a simetria em torno do BOTTOM. Existem dois cenários em que ocorre uma transição sem uma correspondência de comparação:
    • OCRnx alcança MAX. Quando o valor OCR0A chega a MAX, o pino OCn tem seu valor igual ao resultado de uma correspondência de comparação durante a contagem descrescente. Para garantir a simetria de OCnx em torno do BOTTOM, o seu valor ao chegar em MAX deve corresponder ao resultado de uma correspondência de comparação durante a contagem crescente;
    • O temporizador / contador inicia a contagem a partir de um valor superior ao de OCRnx e, por esse motivo, perde a correspondência de comparação, sendo necessária a mudança de OCnx, que teria acontecido durante a contagem crescente;
  5. Os valores extremos para o registrador OCR0A representam casos especiais na geração de uma saída PWM no mode de correção de fase. Se OCR0A for igual a BOTTOM, a saída será continuamente baixa e se for igual a MAX, a saída será continuamente alta no modo não invertido. No modo invertido, a saída terá lógica oposta;
  6. O "flag" de "overflow" (TOV0) do temporizador / contador TC0 é habilitado cada vez que o contador atinge BOTTOM. O "flag" de interrupção pode ser usado para gerar uma interrupção cada vez que o contador atingir o valor BOTTOM;
  7. O diagrama inclui saídas PWM não invertidas e invertidas. A definição a respeito de qual modo será implementado ocorre com os bits COM. Definindo COM0x1:0 em 2 vai produzir uma saída PWM não invertida. A saída PWM invertida é gerada configurando COM0x1:0 em 3. Se os bits COM0A0 forem definidos como 1, o pino OC0A vai alternar a cada correspondência de comparação, desde que o bit WGM02 tenha sido habilitado. Esta opção não está disponível para o pino OC0B. O valor real de OC0x só será visível no respectivo pino físico da porta se a direção de dados para este pino porta estiver definida como saída. A forma de onda PWM é gerada "limpando" (ou "configurando") o registrador OC0x na comparação entre OCR0x e TCNT0 quando o contador cresce e "configura" (ou "limpa") o registrador OC0x na comparação entre OCR0x e TCNT0 quando o contador descresce.

A frequência da saída do sinal PWM com correção de fase pode ser calculada pela seguinte equação:

Frequência de Saída no Modo PWM com Correção de Fase

A variável N representa o fator pré-escala (1, 8, 64, 256 ou 1024).



Seção 2.6: Configuração do sinal de contagem



Seção 2.6.1: Configuração de “Clock” Interno e Uso de “Prescalers”

O conceito de “prescaler” diz respeito a possibilidade de alterar a frequência do sinal de “clock” utilizado como fonte de contagem. Isto é feito através de um divisor de frequências (o “prescaler”) configurado por “software” (através dos registradores de controle do processador).

No Atmel ATmega 328P, os temporizadores / contadores TC0 e TC1 compartilham o mesmo módulo de “prescaler”, embora possam ser configurados com diferentes parâmetros. Já o temporizador / contador TC2 utiliza um módulo distinto. A configuração do “prescaler” se aplica apenas ao modo em que se utiliza o próprio sinal de “clock” do processador (“clock” interno”) como fonte de contagem.

O modo de “clock” interno é definido fazendo CSn[2:0]=0x1 (ou seja, configurando, no registrador TCCRnB, os “flags” CSn2 com o valor 0, CSn1 com o valor 0 e CSn0 com o valor 1). Esta configuração fornece a operação mais rápida, já que utiliza, como fonte de contagem, uma frequência igual à do próprio “clock” do sistema. Alternativamente, nos temporizadores / contadores TC0 e TC1, a frequência de contagem pode admitir uma redução por um dos 4 fatores de “prescaler” possíveis: 8, 64, 256 ou 1024. Já em TC2, pode-se usar um dos sete fatores: 1, 8, 32, 64, 128, 256 e 1024.

A Figura 9 a seguir mostra a configuração dos fatores de “prescaler”.

Configuração dos fatores de “Prescaler”

Figura 9 - Configuração dos fatores de “Prescaler”



Seção 2.6.2: Configuração de “Clock” Externo

O Atmel ATmega328P permite ainda a configuração de uma fonte de contagem externa. Neste caso, o sinal externo será aplicado a um dos pinos T0 ou T1. Neste caso, o sinal elétrico no pino será amostrado uma vez a cada ciclo de “clock” do sistema e finalmente aplicado ao detector de borda, que vai gerar o sinal de contagem. A Figura 10 a seguir mostra o diagrama em blocos do sistema de tratamento de “clock” externo.

Diagrama em Blocos do Tratamento de “Clock” Externo

Figura 10 - Diagrama em Blocos do Tratamento de “Clock” Externo



Seção 3: Programação com Temporizadores / Contadores


A placa do Arduino, bem como a sua IDE, permitem a utilização dos temporizadores / contadores internos do microcontrolador Atmel ATmega328P. Esses recursos podem ser programados e utilizados tanto de forma direta, configurando diretamente os registradores do Atmel ATmega328P, ou através de bibliotecas a serem adicionadas ao código do programa.

Esta seção visa exemplificar a configuração e o uso de temporizadores / contadores do Atmel ATmega328P na placa do Arduino Uno, através da IDE do Arduino.



Seção 3.1: Referenciamento de Temporizadores / Contadores em C do Arduino

Seção 3.1.1: Acesso direto aos Registradores do Atmel ATmega328P

A IDE do Arduino, na sua configuração mais básica, “conhece” os nomes dos registradores internos do Atmel ATmega328P e os seus diversos bits, utilizados para configuração. De fato, estes nomes foram definidos e se constituem em palavras reservadas, que podem ser usadas por qualquer programa. Desta forma, “ligar” um determinado “flag”, acaba por se transformar em uma operação absolutamente usual, em um programa C.

Por outro lado, a forma de codificar tais instruções é frequentemente efetuada de uma forma muito peculiar, tal como vamos discutir. Como exemplo, vamos configurar o TC0 com “prescaler” de 1024. Para isso, sabe-se que deve-se configurar o registrador TCCR0B com um valor ‘1’ no bit CS02, um valor ‘0’ no bit CS01 e um valor ‘1’ no bit CS00.

Idealmente, poderíamos pensar em uma operação de atribuição direta, simplesmente atribuindo um valor ‘1’ ou ‘0’ ao bit desejado. Infelizmente isto não é possível, pois se trata de um registrador. Assim, não existe um comando ou função (tal como o digitalWrite() ) que acesse diretamente um bit dos registradores internos em questão.

Por outro lado, atribuir um número a um registrador vai alterar todos os seus bits, e não apenas o bit desejado. Considerando que um mesmo registrador pode ter os seus bits associados a diversos comportamentos distintos, pode ser bastante “perigosa” uma operação de atribuição direta de valor a um determinado registrador. Isto porque, ao modificar um determinado bit, pode-se equivocadamente, alterar os demais.

A maneira utilizada para configuração dos bits de um registrador é mostrada a seguir, implementando o exemplo proposto (configurar TC0 com “prescaler” de 1024).

				
					TCCR0B |= 1 << CS02;
					TCCR0B |= 0 << CS01;
					TCCR0B |= 1 << CS00;
				
				

Esta notação curiosa é na verdade bastante concisa e amigável. Vamos analisá-la por partes.

O primeiro aspecto a notar é o operador “|=”. Este operador é uma “abreviação” do C, para minimizar a digitação. Na prática, a linha de comando pretende realizar uma operação do tipo “|” (um OR bit a bit) entre o registrador TCCR0B (ou qualquer outro argumento variável indicado a esquerda do sinal de “|=”) e os demais operandos (à direita de “|=”). Assim:

				
					TCCR0B |= 1 << CS02;
				
				

É equivalente a:

				
					TCCR0B = TCCR0B | (1 << CS02);
				
				

Ou seja, basicamente, a linha de comando lê o valor armazenado em TCCR0B, faz alterações neste valor, e atribui o resultado ao mesmo registrador.

Agora temos o operador “<<”. Este operador desloca um número binário para a esquerda. O número binário que vai ser deslocado aparece a esquerda do operador. À direita do operador, indica-se a quantidade de bits que serão deslocados. Como exemplo, note a linha de código a seguir:

				
					X = 7 << 1;
				
				

Neste exemplo, o número 7 será deslocado para a esquerda por uma posição e o resultado deste deslocamento será atribuído a variável X. Supondo um número inteiro armazenado com 16 bits, tem-se o seguinte:

Deslocamento de bit

Como indicado, o número 7 foi deslocado para a esquerda por um bit. Esta operação “eliminou” o bit mais significativo (já que o deslocamento foi para a esquerda) e “acrescentou” um bit ‘0’ na posição menos significativa (já que ela estava vazia após o deslocamento). Assim:

				
					1 << CS02;
				
				

Implica em que o número 1 (em binário, B0000000000000001) será deslocado pela esquerda do valor de CS02. Ocorre que a IDE do Arduino “sabe” que a palavra reservada CS02 vale 2, já que essa é a posição do bit CS02. Assim: Deslocamento de bit

Como se nota, o número 1 apresentava um bit ‘1’ na posição 0 e, após a operação, resultou em um bit ‘1’ na posição 2. Agora, vamos combinar os conceitos todos para entender a codificação utilizada:

				
					TCCR0B |= 1 << CS02;
				
				

Esta linha de comando:

  1. Lê o valor de TCCR0B;
  2. Desloca o número decimal 1 de CS02 (2) posições para a esquerda;
  3. Realiza uma operação OR bit a bit entre TCCR0B e o valor 1 deslocado de CS02 posições;
  4. Armazena o resultado no registrador TCCR0B.

Vamos executá-la passo a passo:

Deslocamento de bit

Assim, a configuração desejada, como mostrado anteriormente, seria:

				
					TCCR0B |= 1 << CS02;
					TCCR0B |= 0 << CS01;
					TCCR0B |= 1 << CS00;
				
				

Alternativamente, e já sabendo como seriam configurados os bits CS02, CS01 e CS00, pode-se adotar uma codificação mais concisa ainda:

				
					TCCR0B |= B00000101; /*o B indica ao compilador que número é binário*/
				
				

Ou ainda:

				
					TCCR0B |= 5; //Note que 5 é 101
				
				

Note que, a despeito da sua forma concisa, as duas últimas codificações mostradas são bem menos legíveis, e não fazem referência aos bits (“flags”) que estão sendo configurados. Assim, recomenda-se fortemente a notação mais clara, ainda que obrigando-se à digitação de mais linhas de código.

A codificação direta, tal como discutida, permite configurar cada um dos registradores de interesse utilizando a granularidade máxima disponível, e com total flexibilidade para configurar os temporizadores / contadores internos do Arduino (na verdade, do Atmel ATmega 328P) da forma mais adequada ao objetivo pretendido.

Seção 3.1.2: Acesso aos temporizadores / contadores com o uso de bibliotecas

Uma alternativa à configuração direta (como explicada neste texto) é a utilização de diversas bibliotecas, as quais podem ser encontradas de forma relativamente fácil. Tais bibliotecas são desenvolvidas usualmente de forma comunitária e, normalmente, disponibilizadas para uso “as is”. Frequentemente, estão sujeitas ao licenciamento GPL (ou algum outro indicado) e sua utilização para fins comerciais pode ser vedada ou limitada. Se esse for o objetivo, é conveniente avaliar com cuidado as restrições de cada tipo de licença em questão. As bibliotecas devem ser instaladas na IDE do Arduino e incluídas no seu código principal.

De uma forma geral, as bibliotecas disponibilizadas publicamente possuem funções previamente construídas, e que podem ser usadas como linhas de comando ou como funções básicas do Arduino (como digitalWrite(), por exemplo). A grande vantagem no uso destas bibliotecas é a simplificação da configuração dos diversos registradores necessários, já que uma única função pode configurar completamente o modo de operação desejado. Por outro lado, é necessário estudar as funções disponíveis em cada biblioteca e, eventualmente, testar o seu comportamento, para ter certeza de que funciona da forma requerida pelo seu programa.

Algumas bibliotecas disponíveis são:

A página de Bibliotecas do sítio do Arduino lista diversas outras bibliotecas que podem ser instaladas.



Seção 3.2: Exemplo de um Programa “Blink” Preciso

Este programa vai ser executado com o circuito a seguir:

“Blink” Preciso

Figura 11 - “Blink” Preciso

				

				/*
				O objetivo deste programa-exemplo é ilustrar
				o uso dos temporizadores e contadores internos 
				do Atmel ATmega328P, através da placa do Arduino
				Uno Rev 3, usando um programa em C, elaborado com 
				a IDE do Arduino.

				Este exemplo vai implementar uma forma de piscar um 
				LED com frequência exata de 1 Hz. Como se sabe, o 
				programa "Blink" utiliza a função delay() para definir
				a frequência com que o LED pisca. Entretanto, naquele 
				programa, a frequência, de fato, depende da duração
				de execução da função loop(). Quanto mais instruções
				forem codificadas, mais tempo a função loop() leva 
				para ser executada. 

				Neste programa, vamos controlar a frequência de 1Hz
				usando os temporizadores / contadores internos do 
				Arduino e usar a função loop apenas para acender e 
				apagar um LED no pino 5. Ainda vamos usar a função 
				delay() para manter o LED aceso mas, o momento de 
				acender vai ser definido pelo temporizador / contador.

				Como se sabe, a placa do Arduino Uno Rev 3 tem um
				"clocK" interno de 16 MHz. É exatamente a partir 
				desse "clock" que deve-se gerar a frequência 
				desejada de 1 Hz. A estratégia a ser usada é a
				de utilizar um "prescaler" para reduzir ao máximo 
				a frequência de contagem e então ajustar o tempo
				pelo valor de contagem dos ciclos obtidos. Assim, 
				tendo alcançado o valor contagem calculado, quando 
				vier o próximo ciclo de contagem, o contador será 
				reinicializado e vai gerar uma informação de "overflow", 
				que poderá ser utilizada para interromper o processador 
				e informá-lo da necessidade de avançar o tempo.

				Os temporizadores / contadores internos do Arduino 
				têm um "prescaler" máximo de 1024. A frequência de 
				"clock" da placa é a máxima (e única) frequência 
				disponível:
			  	  fcm = 16 MHz = 16.000.000 Hz

				Com um "prescaler" de 1.024 têm-se que:
			  	  fc = fcm / prescaler
					 = 16 MHz / 1.204
				 	 = 15.625 Hz
				     = 15,625 KHz

				Ora, esta frequência nos dá um período de:
				  Tc = 1 / fc
					 = 0,0064 ms

				Com estes valores ainda estamos longo do período de 1s 
				desejado. Por outro lado, se usarmos um contador para 
				contar os ciclos deste "clock" pré-escalado, deve-se notar 
				que a cada 15.625 ciclos, teremos um tempo exato de 1s.

				Considerando que o contador inicia em zero, para obter os
				15.625 ciclos desejados, deve-se contar de 0 até 15.624. 

				Para quem gosta de fórmulas, o valor de contagem pode ser 
				obtido assim:
				  fd = fc / [vc + 1]
				  vc = [fc / fd] - 1
					 = [fcm / (prescaler * fd) ] -1

				Onde: 
				  fd - frequência desejada, em Hz. No nosso caso, 1 Hz.
				  fcm - frequência máxima de "clock", em Hz. No nosso 
						caso, 16.000.000 Hz.
				  prescaler - o valor a ser utilizado. No nosso caso, 1.024.
				  vc - valor de contagem que se deseja obter.

				Assim: 
				  vc = [fcm / (prescaler * fd) ] -1
					 = [16.000.000 Hz / (1.024 * 1 Hz)] - 1
					 = 15.625 - 1
					 = 15.624

				Considerando que TC0 e TC2 são contadores de 8 bits, não
				seria possível uma contagem até 15.624. Desta forma, a
				única opção plausível é usar o TC1, que tem 16 bits, podendo 
				contar de 0 até 65.535. Ou seja, a nossa contagem até 15.624
				é perfeitamente factível.

				Agora vamos aos detalhes de configuração. O modo de contagem
				mais adequado ao nosso problema é o modo CTC. Neste modo, com
				um "prescaler" de 1.024, o contador iniciaria a contagem em 0 
				e seria incrementado até 15.624. No ciclo seguinte deverá ser 
				reinicializado e gerar uma interrupção para tratamento pelo 
				programa principal. Finalmente, não vai ser necessário gerar 
				qualquer sinal de saída, já que toda a temporização vai 
				simplesmente gerar uma interrupção.

				Para isso, vai ser necessário:
				- Desconectar as saídas OC1A e OC1B. Para isto, faz-se 
				COM1A[1:0] e COM1B[1:0], em TCCR1A, iguais a zero;
				- Configurar o modo CTC. Para isto faz-se WGM1[3:2], em TCCR1B, 
				iguais a B01 (WGM13 = 0 e WGM12 = 1). Além disso, 
				WGM1[1:0], em TCCR1A, devem ser iguais a B00 (WGM11 = 0 e 
				WGM10 = 0);
				- Como a contagem será interna, os bits de entrada devem 
				ser desativados. Isto é feito fazendo ICNC1 e ICES1, ambos em 
				TCCR1B, iguais a zero;
				- Configurar o "prescaler" em 1.024. Isto é feito fazendo 
				CS1[2:0], em TCCR1B, iguais a B101;
				- Inicializar a contagem em zero. Isto é feito armazenando
				zero em TCNT1;
				- Configurar o valor de contagem em 15.624. Isto é feito 
				armazenando esse valor em OCR1A. Neste exemplo, não 
				vamos usar uma segunda comparação, que poderia ser
				configurada em OCR1B;
				- Habilitar as interrupções em caso de correspondência de
				contagem. Isto é feito no bit OCIE1A, no registrador, 
				TIMSK1. Todos os demais bits deste registrador devem ser zero.

				Vamos ao programa então!
				*/

				volatile boolean acendeLed = LOW;
				/* Vamos precisar de uma variável global, que seja conhecida 
				tanto no programa principal quanto na rotina de interrupção. 
				O tipo "volatile" serve para indicar ao compilador que esta
				variável é alterada por algum evento independente. */

				void setup(){

					/* Durante essa fase de configuração dos registradores
					é importante inibir as interrupções. Ao fim desta etapa
					vamos habilitá-las novamente. */
					cli(); // Inibe interrupções
				
					/* Como discutimos, COM1A[1:0], COM1B[1:0] e WGM1[1:0] devem 
					ser todos iguais a zero. Assim, considerando que os demais 
					bits de TCCR1A não são utilizados, podemos simplesmente
					carregar o valor zero - B00000000 - neste registrador*/
					TCCR1A = 0; // Configura TCCR1A em zero
			  
					/* Como discutimos, ICNC1, ICES1 devem ser zero. Entretanto, 
					WGM12 deve ser igual 1, para configurar o modo CTC (isto 
					porque, no modo CTC, WGM1[3:0] deve valer B0100, sendo que 
					WGM1[3:2] está em TCCR1B e WGM1[1:0] já foi configurado, pois
					estão em TCCR1A). Além disso, CS1[2:0] deve valer B101 para
					configurar o "prescaler" em 1.024. Desta forma, fazemos a
					configuração nas linhas a seguir: */
					TCCR1B = 0; // Configura TCCR1B em zero
					TCCR1B |= (1 << WGM12); // "Liga" o bit WGM12. Assim: WGM1[3:0]=B0100
					TCCR1B |= (1 << CS12) | (1 << CS10); // Faz CS1[2:0]=B101
			  
					/* Vamos inicializar o contador para começar a contar a partir 
					de zero */
					TCNT1  = 0; // Configura TCNT1 em zero
				
					/* O registrador de comparação deve ser configurado com o valor 
					15.624, como discutimos antes */
					OCR1A = 15624;
			  
					/* Agora vamos fazer com que a cada correspondência entre TCNT1 e 
					OCR1A (ou seja, sempre que o contador chegue no valor desejado,
					produzinfo o tempo total esperado), o sistema gere uma interrupção
					no vetor TIMER1_COMPA_vect */
					TIMSK1 |= (1 << OCIE1A);
			  
					/* Terminadas as configurações, rehabilitamos as interrupções */
					sei(); // Habilita interrupções
				}

				ISR(TIMER1_COMPA_vect) {
					/* Esta é a rotina de interrupção. Sempre que houver uma 
					correspondência entre TCNT1 e OCR1A, na ocorrência do próximo
					ciclo, o valor de TCNT1 voltará a zero e o vetor TIMER1_COMPA_vect
					será sinalizado. Assim, essa função ISR será executada.
				
					As rotinas de interrupção devem ser curtas e rápidas, não se 
					propondo a "fazer" nada, mas, apenas, sinalizar que alguma coisa
					aconteceu. O que tiver de ser feito, será feito no programa 
					principal */
			   
					acendeLed = HIGH;
				}


				void loop() {
					if(acendeLed) {
						digitalWrite(5, HIGH);
						delay(200); // Acende por 200ms - uma piscada...
						digitalWrite(5, LOW);
						acendeLed = LOW;
					}
				}

				
				


Seção 3.3: Exemplo de um Programa de Relógio

Este programa vai ser executado com o circuito a seguir:

Circuito do Relógio: Dígito de Segundo

Figura 12 - Circuito do Relógio: Dígito de Segundo

				

				/*
				Este exemplo vai implementar um mostrador de segundos,
				de um único dígito, de um relógio digital. Para isso, 
				vai ser necessário gerar um sinal com período de 1s
				(ou de frequência de 1Hz). Valem todos os comentários do 
				programa anterior.

				Considerando as explicações do programa anterior, vamos
				a este programa então!
				*/

				volatile boolean acendeLed = LOW;
				/* Vamos precisar de uma variável global, que seja conhecida 
				tanto no programa principal quanto na rotina de interrupção. 
				O tipo "volatile" serve para indicar ao compilador que esta
				variável é alterada por algum evento independente. */

				int segundo = 0;
				/* Essa variável global vai guardar o valor do segundo atual. É ela
				quem vai definir o dígito a ser mostrado */

				void setup(){

					/* Durante essa fase de configuração dos registradores
					é importante inibir as interrupções. Ao fim desta etapa
					vamos habilitá-las novamente. */
					cli(); // Inibe interrupções
				
					/* Como discutimos, COM1A[1:0], COM1B[1:0] e WGM1[1:0] devem 
					ser todos iguais a zero. Assim, considerando que os demais 
					bits de TCCR1A não são utilizados, podemos simplesmente
					carregar o valor zero - B00000000 - neste registrador*/
					TCCR1A = 0; // Configura TCCR1A em zero
			  
					/* Como discutimos, ICNC1, ICES1 devem ser zero. Entretanto, 
					WGM12 deve ser igual 1, para configurar o modo CTC (isto 
					porque, no modo CTC, WGM1[3:0] deve valer B0100, sendo que 
					WGM1[3:2] está em TCCR1B e WGM1[1:0] já foi configurado, pois
					estão em TCCR1A). Além disso, CS1[2:0] deve valer B101 para
					configurar o "prescaler" em 1.024. Desta forma, fazemos a
					configuração nas linhas a seguir: */
					TCCR1B = 0; // Configura TCCR1B em zero
					TCCR1B |= (1 << WGM12); // "Liga" o bit WGM12. Assim: WGM1[3:0]=B0100
					TCCR1B |= (1 << CS12) | (1 << CS10); // Faz CS1[2:0]=B101
			  
					/* Vamos inicializar o contador para começar a contar a partir 
					de zero */
					TCNT1  = 0; // Configura TCNT1 em zero
				
					/* Para uma frquência de 1Hz, o registrador de comparação deve ser 
					configurado com o valor de 15.624, como discutimos antes. Para outras
					frequeências, vale a fórmula:
					vc = [fcm / (prescaler * fd) ] -1 */
					OCR1A = 15624;
			  
					/* Agora vamos fazer com que a cada correspondência entre TCNT1 e 
					OCR1A (ou seja, sempre que o contador chegue no valor desejado,
					produzinfo o tempo total esperado), o sistema gere uma interrupção
					no vetor TIMER1_COMPA_vect */
					TIMSK1 |= (1 << OCIE1A);
			  
					/* Terminadas as configurações, rehabilitamos as interrupções */
					sei(); // Habilita interrupções
			  
					DDRD = 255; // Configura os 8 bits da porta D como saídas
			  
			  

				}

				ISR(TIMER1_COMPA_vect) {
					/* Esta é a rotina de interrupção. Sempre que houver uma 
					correspondência entre TCNT1 e OCR1A, na ocorrência do próximo
					ciclo, o valor de TCNT1 voltará a zero e o vetor TIMER1_COMPA_vect
					será sinalizado. Assim, essa função ISR será executada.
				
					As rotinas de interrupção devem ser curtas e rápidas, não se 
					propondo a "fazer" nada, mas, apenas, sinalizar que alguma coisa
					aconteceu. O que tiver de ser feito, será feito no programa 
					principal */
			   
					segundo++;
					if(segundo > 9) segundo = 0;
				}

				void mostraDigito (int digito) {
					int a = 0;  //Segmento "a" - Conectar ao pino  7 do display
					int b = 1;  //Segmento "b" - Conectar ao pino  6 do display
					int c = 2;  //Segmento "c" - Conectar ao pino  4 do display
					int d = 3;  //Segmento "d" - Conectar ao pino  2 do display
					int e = 4;  //Segmento "e" - Conectar ao pino  1 do display
					int f = 5;  //Segmento "f" - Conectar ao pino  9 do display
					int g = 6;  //Segmento "g" - Conectar ao pino 10 do display
					int p = 7;  //Segmento "dp" - Conectar ao pino 5 do display
			  
					switch (digito) {
					  case 0: // Acende segmentos a, b, c, d, e, f. Forma o digito '0'
						digitalWrite(a,HIGH);
						digitalWrite(b,HIGH);
						digitalWrite(c,HIGH);
						digitalWrite(d,HIGH);
						digitalWrite(e,HIGH);
						digitalWrite(f,HIGH);
						digitalWrite(g,LOW);
						digitalWrite(p,LOW);
						break;
				  
					  case 1: // Acende segmentos b, c. Forma o digito '1'
						digitalWrite(a,LOW);
						digitalWrite(b,HIGH);
						digitalWrite(c,HIGH);
						digitalWrite(d,LOW);
						digitalWrite(e,LOW);
						digitalWrite(f,LOW);
						digitalWrite(g,LOW);
						digitalWrite(p,LOW);
						break;
				  
					  case 2: // Acende segmentos a, b, d, e, g. Forma o digito '2'
						digitalWrite(a,HIGH);
						digitalWrite(b,HIGH);
						digitalWrite(c,LOW);
						digitalWrite(d,HIGH);
						digitalWrite(e,HIGH);
						digitalWrite(f,LOW);
						digitalWrite(g,HIGH);
						digitalWrite(p,LOW);
						break;
				  
				 	 case 3: // Acende segmentos a, b, c, d, g. Forma o digito '3'
						digitalWrite(a,HIGH);
						digitalWrite(b,HIGH);
						digitalWrite(c,HIGH);
						digitalWrite(d,HIGH);
						digitalWrite(e,LOW);
						digitalWrite(f,LOW);
						digitalWrite(g,HIGH);
						digitalWrite(p,LOW);
						break;
				  
					  case 4: // Acende segmentos b, c, f, g. Forma o digito '4'
						digitalWrite(a,LOW);
						digitalWrite(b,HIGH);
						digitalWrite(c,HIGH);
						digitalWrite(d,LOW);
						digitalWrite(e,LOW);
						digitalWrite(f,HIGH);
						digitalWrite(g,HIGH);
						digitalWrite(p,LOW);
						break;
				  
					  case 5: // Acende segmentos a, c, d, f, g. Forma o digito '5'
						digitalWrite(a,HIGH);
						digitalWrite(b,LOW);
						digitalWrite(c,HIGH);
						digitalWrite(d,HIGH);
						digitalWrite(e,LOW);
						digitalWrite(f,HIGH);
						digitalWrite(g,HIGH);
						digitalWrite(p,LOW);
						break;
				  
					  case 6: // Acende segmentos a, c, d, e, f, g. Forma o digito '6'
						digitalWrite(a,HIGH);
						digitalWrite(b,LOW);
						digitalWrite(c,HIGH);
						digitalWrite(d,HIGH);
						digitalWrite(e,HIGH);
						digitalWrite(f,HIGH);
						digitalWrite(g,HIGH);
						digitalWrite(p,LOW);
						break;
				  
				 	 case 7: // Acende segmentos a, b, c. Forma o digito '7'
						digitalWrite(a,HIGH);
						digitalWrite(b,HIGH);
						digitalWrite(c,HIGH);
						digitalWrite(d,LOW);
						digitalWrite(e,LOW);
						digitalWrite(f,LOW);
						digitalWrite(g,LOW);
						digitalWrite(p,LOW);
						break;
				  
					  case 8: // Acende segmentos a, b, c, d, e, f, g. Forma o digito '8'
						digitalWrite(a,HIGH);
						digitalWrite(b,HIGH);
						digitalWrite(c,HIGH);
						digitalWrite(d,HIGH);
						digitalWrite(e,HIGH);
						digitalWrite(f,HIGH);
						digitalWrite(g,HIGH);
						digitalWrite(p,LOW);
						break;
				  
					  case 9: // Acende segmentos a, b, c, f, g. Forma o digito '9'
						digitalWrite(a,HIGH);
						digitalWrite(b,HIGH);
						digitalWrite(c,HIGH);
						digitalWrite(d,LOW);
						digitalWrite(e,LOW);
						digitalWrite(f,HIGH);
						digitalWrite(g,HIGH);
						digitalWrite(p,LOW);
						break;
					}
				}

				void loop() {
					mostraDigito(segundo);
				}


				
				


Seção 4: Referências


  1. Wikipedia, visitado 10/FEV/2022
  2. Contador binário síncrono crescente Mod 10 - Embarcados, visitado 10/FEV/2022
  3. Atmel-42735-8-bit-AVR-Microcontroller-ATmega328-328P_Datasheet, visitado 10/FEV/2022
  4. Saiba como usar Timers do ATmega328 no Arduino – Embarcados, visitado 10/FEV/2022
  5. Arduino Timer Interrupts, visitado 10/FEV/2022
  6. TimerInterrupt - Arduino Reference, visitado 10/FEV/2022
  7. Internal Timers of Arduino - Arduino Project Hub, visitado 10/FEV/2022
  8. Tutorial: Executando funções em intervalos de tempo fixos (timers) com Arduino – Laboratorio de Garagem (arduino, eletrônica, robotica, hacking) (labdegaragem.com), visitado 10/FEV/2022
  9. Arduino Playground – LibraryList, visitado 10/FEV/2022
  10. Arduino Playground - Timer1, visitado 10/FEV/2022