Conteúdos, Práticas e Experimentações em Eletrônica
04/JAN/2024
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:
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.
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!
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).
Figura 1 - Diagrama em blocos do temporizador / contador de 8 bits
De uma forma geral:
Figura 2 - Diagrama em blocos da unidade de contagem
Figura 3 - Diagrama em blocos da unidade de comparação de saída (Output Compare Unit)
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.
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:
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.
Os temporizadores podem operar em 4 modos distintos:
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.
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.
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:
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.
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.
Figura 7 - Diagrama de Tempos do Modo PWM Rápido
Como indicado na figura:
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.
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.
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.
Figura 8 - Diagrama de Tempos do Modo PWM com Correção de Fase
Como mostrado:
A frequência da saída do sinal PWM com correção de fase pode ser calculada pela seguinte equação:
A variável N representa o fator pré-escala (1, 8, 64, 256 ou 1024).
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”.
Figura 9 - Configuração dos fatores de “Prescaler”
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.
Figura 10 - Diagrama em Blocos do Tratamento de “Clock” Externo
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.
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:
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:
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:
Vamos executá-la passo a passo:
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.
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.
Este programa vai ser executado com o circuito a seguir:
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;
}
}
Este programa vai ser executado com o circuito a seguir:
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);
}