"cadê meu relógio?" (Timers no PIC)

Movido pela dúvida de uma amiga neste assunto e posteriormente de outros amigos, resolvi fazer este post específico sobre Timers no PIC
Basicamente, um Timer é um módulo do microcontrolador responsável por contar pulsos, sejam eles gerados externamente ou internamente. A contagem destes pulsos é acessível (você pode ler o valor ou fazer ele contar com um "preset". Isso será melhor explicado a frente).
Peço que leia até o fim. Essa aula será longa porque, de fato, usar Timers não é algo a ser explicado rápido, mas garanto que apesar do tamanho é fácil. Os exemplos explicarão bem, e será melhor ainda se ler as partes do datasheet que eu mencionar.

Configurando o Timer

Primeiro de tudo, vamos ver como configurar o Timer de acordo com o que precisamos. Para este exemplo, usarei o PIC16F628A. Deixarei a família 18F de lado para simplificar as coisas.
Olhando no datasheet dele, veremos 3 Timers (do 0 ao 2). Usaremos aqui o Timer1 por ser o mais completo. Vamos dar uma olhada no seu registrador de controle:

imagem extraída do datasheet fornecido pela Microchip

Começando pelo prescaler, ele é um divisor do clock de entrada. Outros Timers, como o 2, possuem ainda um postscaler, que dividirá a saída do Timer. Útil para alcançar frequências mais baixas. No caso deste exemplo, podemos apenas dividir a entrada em até 8 vezes.
O bit T1OSCEN habilita ou desabilita o circuito oscilador do Timer. Será usado quando você quiser um cristal oscilador dedicado pro Timer1, separado do que fornece sinal para o Oscilador geral do PIC. Se não usar, deixá-lo desabilitado é mandatório para evitar conflitos e economizar energia. Aqui não usaremos ele, mas caso queira saber como usar, o próprio datasheet fornece o circuito para utilizá-lo.
T1SYNC raramente é utilizado. Ele será responsável por sincronizar o clock externo com o interno. Ainda não achei aplicação prática para esta função. Quando utilizar o clock interno como fonte para o Timer1, este bit será totalmente ignorado.
TMR1CS selecionará de onde virá o clock. Você pode contar pulsos gerados por algo externo ao MCU (como um sensor que conte quantas pessoas já passaram por uma porta, por exemplo) ou interno. Neste caso o Timer será alimentado com o Clock da CPU (clock interno).
TMR1ON, por fim, habilita ou não o Timer, ou seja, faz ele contar ou não os pulsos recebidos.

Contas e mais contas

"OK. Você só praticamente traduziu o que o datasheet já explica. Como usar?" Bem, aqui entrará uma matemática (a mesma que a calculadora que programei realiza). Vou me ater a explicar um modo de operação do Timer apenas, pois intuitivamente vocês entenderão como usar os demais.
O modo que veremos é o Timer operando como um contador de tempo, baseado no clock interno.
Vamos considerar o seguinte cenário: PIC16F628A, Clock interno de 4 MHz. O datasheet nos diz que o clock que entra no Timer1, quando usando o clock interno, será este dividido por 4 (Fosc/4, onde Fosc é a frequência na qual o microcontrolador está operando), ou seja, temos 1 MHz chegando no Timer. Usaremos isso na seguinte equação:



Onde:
-Foverflow é a frequência com que ocorrerá o overflow (já vou explicar o que é isso. Sem pânico);
-Prescaler é o valor do Prescaler que você configurou no registrador de controle do Timer (no nosso caso, o T1CON);
-Preset é o valor inicial do Timer (calma! já explicarei sobre);
-n é o tamanho em bits do seu Timer. Isto é dito de forma bem clara no datasheet. No nosso caso, o Timer1 tem 16 bits, ou seja, conta de 0 a 65535. Quando ele estiver em 65535 e receber mais um pulso, ocorrerá o overflow. Quando isso ocorre, o  contador volta pra zero e é gerada uma interrupção (já falarei mais sobre as interrupções).
"Tá. Essa equação me dá frequência. Como vou usar isso como referência de tempo?" Antes de mais nada, você precisa saber onde fica guardada esta contagem de pulsos feito pelo Timer. Existe um registrador próprio para isso. O Timer1 usa dois registradores de 8 bits pra guardar sua contagem: o TMR1H, que guarda os 8 bits mais significativos, e o TMR1L, que guarda os 8 menos significativos. No exemplo dado mais para a frente mostrarei como usar isso.
Voltando a questão do tempo, temos dois valores pra ajustar: o prescaler e o preset. SEMPRE ajustaremos o prescaler de forma que o preset seja mínimo. Presets muito próximos do valor máximo do timer podem gerar problemas no código (a interrupção mal se resolve e já acontece outra, ou seja, o programa não sai do lugar ou funciona bem lerdo).
Exemplo: quero uma interrupção a cada 1 ms (overflow ocorrendo a 1 kHz, ou 1/1 ms). Vamos começar com o prescaler igual a 1. Jogando na equação:


O que nos dá um preset de 64536. Veja que aumentar o prescaler só provocaria um preset maior, o que é indesejável. Então usaremos este valor.
Já sabemos de quanto em quanto tempo ocorrerá o overflow. Agora vamos ver como usar isto de alguma forma útil. Como eu já disse, usaremos interrupções pra saber quando o overflow ocorreu. Interrupções nada mais são do que sinais que fazem seu código parar o que estava fazendo, realizar outros comandos, e assim que terminar, voltar a fazer o que estava fazendo antes da interrupção. Para configurar as interrupções, existem registradores dedicados. Geralmente são chamados de INTCON, PIR e PIE. Vamos dar uma olhada neles:


GIE: habilita ou desabilita todas as interrupções;
PEIE: habilita ou desabilita as interrupções causadas por periféricos (já falaremos sobre isso);
Os demais bits controlam as interrupções que não são de periféricos. Não falaremos delas agora. "Mas como sei quem é considerado periférico ou não?" Bem, novamente, na página 109 do datasheet temos um diagrama das interrupções:


Veja que o Timer1 (TMR1), Timer2 (TMR2), CCP1, Comparadores, TX e RC do módulo USART e a leitura/escrita da EEPROM estão ligadas a uma porta AND junto com o valor do bit PEIE, que habilita ou não os periféricos, ou seja, todos esses são considerados periféricos. Portanto, suas interrupções só serão consideradas se o bit PEIE estiver habilitado.
Os bits de controle da interrupção do Timer1 são o TMR1IF, que indica quando a interrupção ocorreu, e o TMR1IE, que habilita ou não esta interrupção em específico. Eles fazem parte, respectivamente, dos registradores PIR1 e PIE1, que não trarei a imagem completa para não sobrecarregar este post. Vamos juntar tudo logo em um código de exemplo para explicar melhor:


Eu disse que seria simples!


Vamos analisar este código:
Primeiro criei uma variável chamada contagem, suficientemente grande para contar o número de interrupções que ocorrerão.
Em seguida vem a função da interrupção. Ela SEMPRE é declara como void. O modificador interrupt avisa para o compilador que esta função deve ir para o espaço da memória reservado para as interrupções. Isto porque você nunca chamará esta função. Ela será invocada automaticamente. ISR (interrupt service routine) é apenas o nome da função. Você pode dar até seu nome se for uma pessoas narcisista.
Dentro desta função está o código do que será feito caso uma interrupção aconteça. NUNCA coloque funções com delay aqui dentro. Códigos de interrupção devem sempre ser mínimos, curtos e breves. Se precisar fazer algo mais demorado, crie uma variável que dirá ao seu programa principal o que fazer (algo parecido com o que faremos aqui). Temos então um if que verifica se o bit TMR1IF é verdadeiro. Quando ocorrer o overflow, o microcontrolador automaticamente atribui "1" para este bit. É ele quem nos diz que ocorreu o overflow. Caso seja verdade, somamos um na variável contagem (incrementamos). O propósito disto será explicado mais para frente. 
Lembra do preset e dos registradores que guardam o valor do Timer? Aqui estão eles. Precisamos recarregar o Timer com o valor do preset, pra que seja feito o número de contagem que determinamos com a equação lá em cima. Isto deve ser feito pois, sempre no overflow, o registrador do Timer volta para o valor 0, e se deixarmos ele contar a partir do zero, a contagem será bem mais longa do que a necessária. Mas há uma coisa aqui: lembra de quando falei dos 8 bits mais significativos e menos significativos? Teremos que fazer esta divisão aqui, que é simples. Ela é feita nesta ordem:
 -Pegue o valor do preset (no nosso caso, 64536) e divida por 256, valor máximo de cada registrador (TRM1H e TMR1L);
-Esta divisão retorna o valor 252,09375. O registrador TMR1H deve guardar a parte inteira deste valor, no caso, 252;
-Para o TMR1L, multiplique o valor do TMR1H por 256 (252*256) e subtraia do valor do preset. Esta conta fica TMR1L = preset - 256 * TMR1H = 64536 - 256 * 252 = 24.
"Nossa! Por que não deixa essas contas todas pro microcontrolador fazer?" Lembra que eu disse que a ISR deve ser o mais curta possível? Então! Se quiser, pode criar um #define para fazer estas contas, algo assim:



 "Ah, mas eu uso o MikroC como compilador!" Não tem problema. Apenas insira estes #defines antes do teu código começar de fato, assim como fiz aí com o MPLAB rodando o XC8. Lembrando que expressões nos #define serão calculadas pelo compilador, ou seja, teu PIC será programado já com o valor resultante desta equação.
A função floor alí é para arredondar a divisão para baixo, e não para o valor mais próximo como ocorre por default.

"Você é um inútil! Nem pra colocar estas contas já na calculadora que publicou a um tempo atrás no blog!" Não coloquei mesmo nem vou colocar, pois tem muitos microcontroladores (família 18F, por exemplo) que tem Timers que guardam seu valor em um registrador de 16 bits, ou seja, o valor 64536 caberia dentro de um registrador só, sem ter que fazer nenhuma divisão como fizemos.
Voltando ao código, temos um TMR1IF = 0. Isto é necessário pois o microcontrolador só modifica o bit TMR1IF para o valor 1, para nos avisar que algo aconteceu. Nós é que temos que zerar ele de novo, fazendo o MCU entender que já processamos aquela interrupção. Se não fizermos isso, a interrupção ocorrerá apenas uma vez, mesmo com o overflow acontecendo várias vezes. Estes bits terminados em IF (interrupt flag) NUNCA voltarão a 0 sozinhos. 
Na nossa main, configuramos o registrador INTCON de forma que as interrupções gerais e a dos periféricos sejam habilitadas (suba o post e veja de novo a explicação do registrador se estiver com dúvidas), configuro o registrador T1CON para habilitar o Timer1 com clock interno e prescaler 1:1, configuro o PORTB pra ser saída e carrego a variável contagem com o valor 0 (lembrando que variáveis, quando você liga o circuito, começam com valores aleatórios). Em seguida temos um loop infinito e é aqui que a mágica acontece.
Aquele if verifica se a variável contagem tem um valor maior ou igual a 1000. De onde surgiu esse valor? Se você lembrar, calculamos o preset do timer para ter uma interrupção a cada 1 milissegundo. Mas quero que meu LED ligado ao PORTB mude de estado a cada 1 segundo, então preciso de mil interrupções. Se eu quisesse que meu LED mudasse de estado a cada 250 milissegundos, seria só verificar quando a variável contagem chegasse ao valor 250. Aí o resto é simples: se o if for verdadeiro, o código inverte o estado do PORTB e zera a variável contagem pra contar o tempo a partir do zero.
Uma observação: uso o maior ou igual pois pode acontecer do programa principal perder a contagem e a variável passar a ser 1001 ou 1002, por exemplo. Isso pode ser causado por outro código maior estar sendo executado antes ou outra interrupção ter interrompido o programa antes. Isso deixa bem claro que, para intervalos de tempo bem pequenos, esta não é uma aproximação super precisa!
E é isso! Como eu disse, o post seria bem longo e cheio de coisas, mas se você tentar escrever o programa do zero, e fizer cada passo do que expliquei, em pouquíssimo tempo entenderá a lógica de uso dos Timers para o que precisar!

"Mas pra quê tanto trabalho se existe a função delay_ms()?" O problema com a função delay_ms() é que ela literalmente para o programa. Ela funciona fazendo o microcontrolador executar milhares de instruções NOP, que é literalmente dizer pro microcontrolador não fazer nada, ou seja, a CPU fica fazendo "vários nada", literalmente. Com isso, todo o resto do código só vai continuar depois deste tempo da função passar. Usar Timers permite contar tempo com o programa correndo normalmente, uma vez que ele só vai parar o código principal para processar a interrupção e já volta a fazer o que estava fazendo. Uma abordagem muito mais inteligente e eficiente. 


Usando a calculadora

Usando o mesmo exemplo nosso aqui, vou mostrar como usar a calculadora que postei anteriormente aqui no blog. Leia aqui como baixar.
Abrindo a calculadora você verá a seguinte tela:


Não se assuste: o programa grava suas preferências (clock, prescaler e etc) para você não ter que ficar sempre digitando de novo quando abrir ele. Como você acabou de baixar ele, obviamente não tem nada gravado ainda. Só apertar qualquer tecla que tudo ficará bem e você verá a tela principal do programa como esta:


Para o nosso exemplo siga os seguintes passos:
-Digite 1 e em seguida pressione Enter. Ela pedirá o valor do clock do microcontrolador em kHz. No nosso caso, consideramos o clock interno do PIC de 4 MHz, ou 4000 kHz. digite 4000 e pressione Enter. O programa voltará à tela principal com o clock correto agora;
-Deixe a segunda opção com "s" (sim). Esta opção divide ou não o clock por 4. Como já disse, quando o Timer é alimentado pelo clock interno, este é divido por 4. Nenhuma divisão é feita (senão a do pre/postscaler) para o clock externo (informações tiradas do datasheet);
-Digite 3 e pressione enter. O programa pedirá o prescaler, que no nosso caso, foi 1:1. Digite 1 e pressione enter;
-Como não usaremos postscaler, deixe a opção 4 como está (1:1);
-A resolução do Timer1 é de 16 bits mesmo, então não alteraremos a opção 5;
-Não usaremos a opção 6, pois como está explicado na tela, queremos descobrir o valor dele, portanto deixaremos ele com 0;
-digite 7 e enter. O programa pedirá a frequência de overflow desejada, no nosso caso, 1 kHz para ter uma interrupção a cada 1 milissegundo. Digite 1 e pressione enter;
-Agora é só digitar 8 e pressionar enter pra mágica acontecer! Veja que ele dá várias informações acerca dos tempos e frequências envolvendo o Timer com os parâmetros informados, e o principal, o valor inicial (preset) do Timer necessário. Caso não seja possível obter a frequência exata, o programa ainda mostrará o erro relativo estimado. Se algo estiver fora dos limites, ele ainda vai lhe recomendar aumentar ou reduzir o prescaler (se sair um valor maluco de preset, desconfie também haha). Legal, não?
Aí é só pressionar enter, alterar os parâmetros se necessário e ser feliz. Não se esqueça de fechar digitando 9 e enter na tela principal para que o programa crie um arquivo resultados.txt, que conterá os resultados que você obteve, pra não precisar abrir o programa de novo caso tenha esquecido os valores.
É isso, galera. Se surgirem dúvidas, se perceberem erros no post utilize o campo dos comentários aqui em baixo pra avisar! Até o próximo, flws!

Comentários