domingo, 3 de setembro de 2017

"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!


terça-feira, 25 de julho de 2017

Lâmpada de LED queimou: baixa qualidade ou erro de projeto?

Último final de semana estive na casa de uma amiga e vi uma lâmpada de LED dessas comuns de 15W queimar. Fiquei intrigado, afinal, uma das maiores propagandas destas lâmpadas são, depois do seu baixo consumo, a sua vida útil. O que teria feito ela queimar tão precocemente? Curioso, peguei a lâmpada e abri. Dentro, algumas surpresas.
Vamos começar pelo básico: a foto a seguir mostra o circuito da lâmpada depois que removi tudo de dentro do invólucro e soldei alguns fios de volta.


A primeira coisa que se observa ao abrir a lâmpada é uma MCPCB com vários LEDs SMD soldados à ela. 


Dois LEDs foram substituídos por um curto de solda (bem na esquerda). Quando abri, os LEDs estavam escurecidos por dentro, e ao testar com um multímetro, verifiquei que ambos estavam abertos. Então removi eles, substituí ele por um curto para reestabelecer a continuidade do circuito de LEDs. Quando alimentei esta placa com 24V, os LEDs acenderam fracos, mas acenderam, excetos cerca de 6 ou 8, que quando testados, se mostraram em curto. Resolvi então analisar o circuito da outra placa antes de religar tudo e ver se funcionava. 


A princípio, eu acreditava que o circuito fosse uma fonte de tensão constante, semelhante aos carregadores de celular. Vindo da China, seria uma possibilidade de circuito: uma fonte de tensão constante com saída próxima da queda de tensão dos LEDs: design simples, barato e que duraria de forma aceitável, mesmo não sendo a forma ideal além de reduzir a vida útil dos LEDs. Olhando a parte de baixo da placa, achei um CI, uma ponte retificadora, um diodo retificador, um capacitor cerâmico e alguns resistores.


O CI é o BP2833A. Pesquisando na internet, foi fácil achar seu datasheet. Ele é um driver de corrente constante na topologia buck. O circuito sugerido pelo fabricante é o seguinte:


Olhando a placa, percebemos que o circuito da lâmpada segue o esquema sugerido, exceto pelo acréscimo de um resistor de 68 k em paralelo com os LEDs e pelo fato dos pinos 6 e 7 não estarem sendo utilizados.. A ponte retificadora é uma MB10F, com capacidade de 0,5 A e tensão de pico repetitiva de 1000 Volts. O diodo usado é um ES1J, também para 1000 V porém com corrente direta de até 1 A. O capacitor da entrada, responsável por retificar a tensão da rede é um AISHI de 10 uF para 400 V, com temperatura de operação de 105 °C. O de filtro para os LEDs é da mesma marca, de também 10 uF para 200 V e mesma temperatura de operação do outro. Havia ainda um resistor na entrada servindo de fusível (alternativa barata e compacta mas que funciona).
Ao juntar as duas partes novamente após ter substituído os LEDs abertos por um curto, a lâmpada funcionou. Mas aí fica a dúvida: o que causou a queima dos LEDs? O Circuito foi feito conforme sugerido pelo fabricante do CI, os componentes foram bem dimensionados. Seriam culpa dos LEDs possivelmente de baixa qualidade? 
Resolvi voltar e analisar a placa que contém os LEDs. Utilizando uma luminária de mesa, consegui ver as trilhas em relevo sob a máscara branca que cobre a placa. o que achei foram 30 LEDs dispostos em 15 arranjos em série de 2 LEDs em paralelo. Aí tudo ficou claro.



Colocar diodos em paralelo é algo mais complicado do que parece. Embora na teoria eles tenham uma queda de tensão fixa, na prática ela muda com a temperatura, corrente que circula por ele e até mesmo por impurezas na hora da fabricação. Ao ter dois LEDs em paralelo com quedas de tensão diferentes, a corrente passa a circular mais por um do que pelo outro. Como eles apresentam baixa resistência interna, a corrente que circula por eles pode ser muito maior do que a nominal mesmo para pequenas diferenças de tensão. Se você pesquisar na internet, verá bastante exemplos de como colocar diodos em paralelo é algo problemático. Como temos arranjos de dois LEDs em paralelo, bastou um deles queimar por excesso de corrente para que o outro recebesse o dobro de corrente, queimando também (olhe no esquema acima para entender). 
Que conclusão tirar: embora o projeto tenha sido bem feito, com uma boa qualidade construtiva (eu diria até muito boa se tratando de um produto chinês), a decisão de colocar LEDs em paralelo foi jogar roleta russa com a física. O problema é que para contornar isto, ou deveria ser usado uma fonte de tensão constante com resistores limitadores de corrente para duas sequências em paralelo de 15 LEDs em série ou duas fontes de corrente separadas para cada sequência de 15 LEDs em série. A primeira solução derruba a eficiência do circuito, e a segunda, ocuparia bem mais espaço e aumentaria consideravelmente o custo de produção. Fica claro que, para quem projeta circuitos procurando minimizar custos, colocar os LEDs em paralelo foi a saída mais sensata. 


sábado, 1 de abril de 2017

Calculadora de Timers v2.0

Pessoal, após perceber alguns erros (obrigado César!), tive que atualizar e mudar várias coisas na calculadora de Timer que eu havia desenvolvido. Aqui está então a versão corrigida.
Tive aula de programação na faculdade, e resolvi usar isso para algo. Da necessidade surgem as ideias, não é mesmo?
Eu já falei aqui no blogger sobre os Timers do PIC (mas praticamente todo microcontrolador tem o seu: ATMEL - que é a base do arduino -, Freescale, ST e etc), e quando você vai mexer com ele, você pode acabar fazendo algumas contas, que por vezes são chatas, trabalhosas ou você precisa ficar em tentativa e erro pra achar uma solução. Desta necessidade resolvi criar uma calculadora para uso com todo e qualquer Timer de qualquer Microcontrolador. O programa é simples, funciona em modo de texto mas é super intuitivo e simples. Ocupando só 69KB, vai ser uma mão na roda toda vez que você pensar em usar Timers.

Sim, eu gosto de azul!

Como pode ver, não é nada supremo, mas é o necessário: você entra com as informações desejadas e ele faz todo o trabalho de cálculo pra você e ainda salva os resultados em um arquivo .txt pra você poder ver de forma rápida e fácil depois os resultados. Outro ponto é que ele guarda os valores que você inseriu, pra facilitar as coisas se depois de fechar o programa você precisar alterar só um parâmetro.
Você pode baixar o programa aqui. Atente que existem dois arquivos: TC_br (calculadora em português) e TC_en (calculadora em inglês). Os com UNIX são os feitos para rodarem em Linux (foi escrito e executado no Ubuntu. Se você testar em outra plataforma, por favor avisar).
Espero que seja de grande valia e utilidade pra vocês. Lembrando que se gostar peço que compartilhe com seus amigos o blog (e não apenas o programa) pra me ajudar, até porque aqui ele lerá os termos de uso, que falando nisso, quais são?

Ao baixar o programa TC_br, TC_br_UNIX, TC_en_UNIX ou TC_en você concorda publicamente em:
1. Não praticar engenharia reversa no software;
2. Redistribuir somente se citar a fonte;
3. Não comercializar, alugar ou obter qualquer tipo de lucro com ele;
4. Isentar o criador de quaisquer erros cometido pelo software ou prejuízos decorrentes da utilização deste.

Esclarecendo dois pontos alí: eu fiz esse programa com muito cuidado (afinal, eu vou usar ele também) e o testei várias vezes, mas pode acontecer de ter um erro ou outro. Se tiver, por favor não me xingue: seja educado e me comunique pelo e-mail que se encontra no programa para que eu corrija. Costumo fazer correções no mesmo dia e já atualizar o arquivo. Foram dois longos dias para criar esse programa, então espero sempre poder corrigir o mais rápido possível.
Outra que peço que não façam engenharia reversa porque eu apoio a comunidade OpenSource e a utilizo, então se quiser o código pra estudar está no SourceForge também. Só JAMAIS tire meus créditos, né! Qualquer dúvida, leia o arquivo LICENSE que está junto no diretório do projeto.

Espero que gostem.

sexta-feira, 31 de março de 2017

Algorítimos de ordenação - considerações importantes

Fala pessoal. A pouco tempo eu fiz uma postagem sobre algorítimos de ordenação, mas senti que faltaram algumas considerações a serem feitas, então resolvi fazer um novo post ao invés de atualizar o anterior.
Antes de tudo, vamos lembrar de uma coisa: memórias (seja RAM, ROM, EEPROM, FLASH ou outra) não duram para sempre! São componentes eletrônicos, possuem vida útil limitada, e a maioria delas tem sua vida definida por ciclos de leitura/escrita, ou seja, quanto mais você escrever e acessar sua memória, menos tempo ela durará! Dito isto, observe a imagem abaixo e creio que ficará bem claro onde quero chegar:


Esta é a saída de um programa que escrevi em C para ordenar o mesmo vetor usando Selection Sorting e Bubble Sorting. Antes de ficar pasmo com a diferença de tempo, vamos olhar para o número de operações no vetor. O segundo método "só" realizou 24.958 vezes mais operações do que o primeiro. Quase 2 bilhões e meio de operações. Isso para um vetor de 100k Bytes. Fica bem claro qual algorítimo irá garantir maior vida útil para a memória. A diferença de tempo (3,5x) é só uma decorrência do número de operações maior.
Agora imagine que isto foi feito em um computador, com uma CPU operando a 3,2GHz e memória RAM (que é onde ficam as variáveis do C) a 1333MHz. Imagine no meu PIC que opera a 48MHz e a memória I2C, por exemplo, a 400kHz no máximo e levando 5ms pra cada operação de escrita. Pois então. Com Bubble Sorting o PIC levou mais 3 horas pra organizar 8K Bytes de uma 24C64, enquanto o Selection, cerca de 45 minutos a uma hora (eu realmente me perdi na contagem do tempo de tanto que demorou, mas foi algo próximo disto). Vamos simular no PC pra ver a diferença no número de operações:


A diferença de tempo foi minúscula se pensarmos em segundos, e não em razão entre eles, mas o número de operações na memória continua bem significativo (2022 vezes mais operações no Bubble). Não acho que a memória I2C duraria muito tempo.
E se está achando que as memórias duram bastante, que isto é conversa fiada, veja o EEPROM Destroyer.
A intenção aqui foi chamar a atenção para o fato de que a simples escolha de um algorítimo pode não só impactar na performance de um equipamento, mas também na sua vida útil, mesmo sendo difícil de acreditar que um firmaware pode encurtar a vida de um componente eletrônico.

Por fim, resolvi testar o limite máximo do meu PC, o que me deu um vetor de 259625 Bytes e uam longa espera.


sábado, 25 de fevereiro de 2017

Acelerômetro com PIC (medidor de força G)

Depois de algumas postagens falando sobre os módulos do PIC e alguns truques, chegou a hora de juntar quase tudo. Este será o post mais complexo e rico da história do blog. Varredura, WatchDog Timer, Timers, Interrupções, Estados de gerenciamento de energia e ADC. Tudo em um único projeto. Trago a vocês um circuito (e programa) que mede os valores de aceleração em 3 eixos (x, y e z) passados por um acelerômetro (MMA7361) e exibe em dois displays de LED de 7 segmentos a resultante vetorial da aceleração. Pegue uma boa xícara de café e se prepare para revisar algumas coisas e aprender outras. Vamos lá!

Vetores, resultante vetorial (comprimento de um vetor) e aceleração.

 

Podemos de forma simples definir vetor como uma reta que interliga dois pontos, dotada de comprimento, sentido e direção. Aqui vamos tratar todos os vetores saindo da origem até um ponto, tendo como comprimento final a aceleração em um dos eixos cartesianos (x, y ou z) e a resultante dos três. 
Os três eixos cartesianos

Vamos agora pensar em 4 vetores: a aceleração no eixo X, no eixo Y, no eixo Z (todos em verde) e a resultante destas três (em vermelho).

Três vetores e a resultante deles.

O comprimento deste vetor resultante é dado por , sendo l seu comprimento total.
"Tá, mas por que precisamos disso?" Bem, nenhum corpo está sujeito a apenas uma força em um único sentido, mas sim à combinação de todas as forças nele, ou seja, à resultante. Imagine um carro acelerando e virando ao mesmo tempo (acelerando dentro de uma curva): você tem uma força te puxando para dentro da curva (eixo X), uma força para frente acelerando o carro (eixo Y) e uma para baixo, para que o carro não voe (eixo Z). Vetorialmente, temos algo assim:

Mas no final das contas, todo o carro (e você dentro dele) está submetido à resultante de todas estas forças atuando juntas. E é isto que nosso projeto medirá!

O circuito

 

O circuito fica conforme abaixo:
Resistores sem valor devem ser calculados.


Temos como "cérebro" nosso PIC18F2550, com um cristal de 20MHz alimentando o PLL interno, gerando um clock final de 48MHZ. Temos dois displays de ânodo comum, sendo controlados por varredura, que consiste em alternar os displays de forma rápida, sem que nossos olhos percebam. Isto nos economizou 6 pinos do microcontrolador. Os displays são selecionados pelos transistores Q1 e Q2, e os segmentos controlados pelo PORTB.
Temos dois botões: S1, que alternará os modos de visualização (valor atual ou maior valor) e S2, responsável por colocar nosso circuito no modo de gerenciamento de energia (SLEEP).
R12 e R13 formam um divisor de tensão para abaixar a tensão do pino do PIC - que são 5V em estado alto - para 3.3V, o aceitado pelo acelerômetro, sendo por meio deste divisor de tensão, controlado pelo pino RC6, que selecionamos o acelerômetro no modo sleep ou normal. Perceba que liguei dois pinos do módulo acelerômetro: o 3.3V e o GS, sendo este pino responsável por selecionar o máximo valor lido e a resolução, que no nosso caso serão 6g com resolução de 206mV/g (lembrando que este g aqui não é de gramas!). O circuito deve ser todo alimentado com 5V providos por uma fonte bem estável e com baixo ruído, para evitar erros de leitura por parte do ADC.
Para calcular os resistores do display, use a fórmula R = (Vcc - Vf)/If, onde R é o valor do resistor em ohms, Vcc a tensão de alimentação (no nosso caso, 5 V), Vf a tensão direta dos LEDs do display e If a corrente direta daqueles.

Código

 

Primeiro vou colocar os código e depois explico.

main.c

delays.h

ADC.h


No código principal, começamos com os #pragmas responsáveis por configurar os bits de configuração do PIC. Veja que estamos usando como oscilador o modo HSPLL_HS, que significa que estamos usando um cristal de alta velocidade para alimentar o PLL que por fim gerará o clock final do microcontrolador, que no caso será de 48MHz. Desabilitamos o bit do WatchDog Timer (WDT) para que possamos controlar ele pelo software e configuramos o prescaler dele para 64. O WDT tem como base um período de 4 ms, mas como colocamos o prescaler dele para 64, 64x4 = 256 ms, que é o tempo que levará para o WDT estourar e resetar o PIC.
Em seguida declaramos os headers necessários e o #define para a velocidade do clock.
Logo se notam 3 funções: mask, ISRHP e GO_SLEEP.
1) A função mask é responsável por converter o valor numérico em um valor no PORTB para que sejam corretamente exibidos os números no display. Os valores retornados pelo switch case estão no formato binário.
2) A função ISRHP (Interrupt Service Routine of High Priority) é um pouco mais complexa. Primeiro ela verifica se a causa da interrupção foi o Timer 1: se foi, ele limpa o registrador do WDT, recarrega os registradores do Timer 1 para que a interrupção ocorra a 200 Hz (o que permite limpar o registrador do WDT antes do estouro), trata de desligar o display ligado anteriormente, atualizar o valor do PORTB pro do outro display e o liga. Por fim, incrementa a variável atualiza, que será responsável por fixar a taxa de atualização do display em até 5Hz e limpa o bit da interrupção. Se a causa da interrupção não foi o Timer 1, ela foi gerada pela INT0, então a função verifica se foi ela a causa. Se sim, ela troca o modo do display entre os já citados e limpa o bit da respectiva interrupção.
3) A função GO_SLEEP põe o PORTB em estado baixo, corta os transistores de acionamento do display, põe o acelerômetro em sleep mode e desativa o WDT através do bit SWTDEN, isto pois mesmo nos modos de gerenciamento de energia o clock do WDT continua ativo. Importante observar isto, caso contrário, o WDT irá resetar o PIC, uma vez que o Timer 1 irá parar e consequentemente a interrupção que chama a rotina que limpa o registrador daquele. A função continua reativando tudo que fora desativado após acordar do modo sleep. Explicarei mais sobre os modos de gerenciamento de energia no final do post.

Na main, começamos com a configuração dos módulos. Não explicarei a configuração dos módulos: deixarei a cargo de vocês buscarem no datasheet cada registrador para entenderem o que cada configuração fez. Caso surjam dúvidas estas serão bem vindas no campo para comentários lá em baixo. O que vou comentar são apenas três coisas:
1) Começo ela limpando o registrador do WDT e o desativando, pois como o Timer 1 não foi ativado ainda, não quero que ele estoure por acidente. Veja que o ideal é ativá-lo somente após configurar o Timer 1 e sua interrupção. 
2) O bit IPEN (Interrupt Priority Enable) ativa o modo de prioridades das interrupções. Isto nos permite escolher qual interrupção terá prioridade caso duas ocorram ao mesmo tempo. Juntamente à ativação deste bit, uma coisa muda na programação: agora a função que tratará as interrupções deve ter outro formato! A função que for tratar das de alta prioridade deverá ser da forma tipo_de_retorno interrupt high_priority nome_da_função, e a que tratar as de baixa prioridade, tipo_de_retorno interrupt low_priority nome_da_função. E para escolher qual interrupção terá prioridade alta ou baixa não basta apenas tratar ela dentro da função, mas configurar seus respectivos bits, que serão aqueles terminados em IP, como por exemplo a interrupção do Timer 1, que tem seu bit de prioridade TMR1IP, sendo atribuído o valor 1 para alta prioridade e 0 para baixa. Veja que neste código, coloquei o Timer 1 como prioritário, afinal, se outra função segurar o processador, o WDT pode não ser resetado, vindo a estourar e resetar o PIC. 
3) os bits USBEN e SSPEN foram limpos para liberar os respectivos pinos do PORTC ao uso, evitando conflito.

Prosseguindo, temos um for simples, responsável por fazer os displays exibirem os números de 0 a 9 (para verificar se todos os segmentos estão bons).
Em seguida vem o while(1), que criará o loop responsável pela execução do programa em sí.
O if é responsável por verificar a variável atualiza, responsável por fazer que os valores do display sejam atualizados em até 5Hz. Isto evita que os valores fiquem trocando muito rápido, e ajuda a exibir menos ruídos, deixando uma leitura mais consistente. Dentro do if temos a leitura dos valores retornados por cada canal do ADC, armazenando o valor em cada variável do respectivo eixo, sendo este valor convertido para a unidade de gravidades (g) logo em seguida (variáveis Xg, Yg e Zg). A conversão é feita da seguinte forma: o ADC retorna um valor de 0 a 1023 para a tensão presente no canal. Para saber a tensão lida, precisamos multiplicar o valor lido pela tensão de referência e dividir pelo maior valor que pode ser retornado, ou seja, multiplicamos por 5000 mV e dividimos por 1023. Isto nos dá o valor lido em mV. Em seguida convertemos a tensão em g. Sabendo que o MMA7361 tem uma sáida de 1650 mV para 0 g, precisamos subtrair isto do valor lido. O datasheet ainda informa que, quando a escala selecionada for de 6g (nosso caso), temos na saída 206 mV para cada g, então precisamos dividir o valor lido por 206. Pronto, temos o valor lido de cada canal em g. Para saber o valor resultante dos três vetores, usamos a fórmula citada lá em cima e armazenamos este valor na variável Gt. O programa então verifica se o valor lido é maior do que o maior lido anteriormente e caso seja, atualiza o valor máximo. Ele segue verificando o modo selecionado de exibição e joga nas variáveis digit1 e 2 os valores numéricos das unidades e dos décimos do devido valor, ou seja, nosso circuito nos dá valores de 0,0 a 6,0, em intervalos de 0,1. A variável atualiza é zerada para que a contagem de tempo reinicie. Por último, mas não menos importante, o programa verifica se a chave ligada no pino RC2 está pressionada (0 para pressionada e 1 para solta), e caso esteja pressionada, chama a função GO_SLEEP.
Perceba a quantidade de coisas que foram abordadas neste programa. Pode parecer até cansativo, mas se estudado com calma será bastante enriquecedor para seu conhecimento tanto de hardware quanto de software.

Power-managed modes

 

Os estados de gerenciamento de energia na família 18F são dois: sleep e idle. O modo pode ser escolhido pelo bit IDLEN do registrador OSCCON. Para entrar em qualquer um dos modos basta dar a instrução assembly SLEEP ou, no compilador XC8, chamar a função nativa SLEEP.
A diferença entre os dois é que no modo idle, o clock do processador é cortado, porém os periféricos continuam executando suas funções. Em sleep, o processador e todos os periféricos sem clock próprio param, ou seja, apenas o WDT e o Timer 1 (quando sua fonte de clock for o oscilador de baixa potência) continuam ativos. Ambos os modos visam reduzir o consumo de energia, aumentando a durabilidade das baterias que alimentam o circuito, por exemplo. Para se obter o mínimo consumo possível, todos os pinos devem ser colocados em estado baixo, por isso que na função GO_SLEEP desliguei todo o PORTB. "Ué, mas você deixou o PORTC com o valor 0x03!" Sim, pois os transistores que controlam os displays - que neste caso são de ânodo comum - são do tipo PNP. Caso eu ponha o PORTC todo em estado baixo, os transistores saturariam e os displays ficariam ligados exibindo "88", que é justamente onde haveria o maior consumo de energia, já que todos os segmentos estariam acesos. Novamente, para mais informações, leiam o datasheet (para este recurso, seção 3.0). Lá vocês terão todas as informações, como as formas de sair destes estados, como o clock reage a eles e etc.
Vale lembrar que quando o microcontrolador sair de qualquer um dos modos de gerenciamento de energia por meio de uma interrupção, o programa não voltará ao início, mas continuará de onde parou (agora a função GO_SLEEP deve fazer mais sentido, não?). Caso ele saia por meio de um reset, seja no pino MCLR, seja pela instrução RESET ou pelo estouro do WDT, o programa voltará ao início.

Para fechar, fica uma foto do circuito montado na protoboard:

Tenho inveja de quem consegue deixar os circuitos organizados e apresentáveis nas protoboards ;-;
 E é "só" isso por hoje. Estou aberto a dúvidas e sugestões. Use o campo dos comentários aqui em baixo para que possamos evoluir juntos. Um abraço e até a próxima!

quarta-feira, 15 de fevereiro de 2017

Recursos especiais do PIC (parte 2)

Como prometido, continuo aqui com o último post sobre alguns módulos dos PICs. Lembrando novamente que estamos falando sobre o PIC18F2550/4550 mas isto se aplica a todos que tiverem. Confiram sempre o datasheet do seu microcontrolador para conferir o nome e função dos registradores.

Fail-Safe Clock Monitor

No último post da série, falamos sobre o WDT e como ele pode nos ajudar quando o PIC "trava", mas e a causa deste travamento? Um estouro de pilha? Uma operação ilegal? Uma flutuação de tensão? Ruído na alimentação? Bem, em todos estes casos com certeza o WDT irá atuar e se for algo transiente, o PIC retoma à operação normal. Mas e se ele travou porque houve uma falha no seu Oscilador externo? Se deu defeito no capacitor ou no cristal do circuito de clock? Aí entra o FSCM pra salvar o dia.
 Segundo o datasheet, "O Fail-Safe Clock Monitor (FSCM) permite ao microcontrolador continuar a operar na eventual falha do oscilador externo ao mudar automaticamente o clock do dispositivo para o bloco oscilador interno". Ao detectar uma falha no oscilador externo, o PIC gera uma interrupção (OSCFIF), muda o clock para o interno e limpa o WDT. Com a interrupção você pode optar por dois caminhos: entrar em um dos estados de gerenciamento de energia do PIC ou escolher a próxima fonte de clock.
No programa, tudo que envolver tempo terá que ser ajustado para o novo clock interno, ou você pode usar um cristal sempre da mesma frequência do clock interno escolhida (eu trabalharia com 8MHz).
Para ativar este recurso, primeiro configure o registrador OSCCON selecionando o clock desejado em caso de falha e as interrupções (INTCON). Ative a interrupção pelo bit OSCFIE e a monitore pelo bit OSCFIF. Lembre-se de ativar o bit de configuração FCMEN (CONFIG1H).

Two-Speed Start-up

 Este é o mais simples de todo: ative o bit de configuração IESO (CONFIG1H). Em modos de oscilador que usem um cristal (XT, HS, XTPLL e HSPLL), este recurso permite o microcontrolador rodar no clock interno até que o clock do cristal esteja estável. O sugerido é apenas que a primeira coisa que seu programa deve fazer é configurar o clock interno (escolher a velocidade) através do OSCCON. Assim que o clock do cristal estiver pronto, automaticamente o PIC muda sua fonte de clock para ele. É uma boa alternativa para o Power-Up Timer.

High/Low Voltage Detector

 Este é o tipo de recurso que você quer ter ativado caso esteja alimentando seu circuito com bateria ou fontes que estejam sujeitas a flutuações de tensão. Este módulo é basicamente um comparador de tensão com sua referência ajustável. Quando a tensão no pino RA5 (HLVDIN) ficar acima ou abaixo do nível configurado, é gerada uma interrupção (HLVDIF). Este módulo é configurado pelo registrador HLVDCON. Importante ressaltar que cada vez que você ativa este módulo, ele leva um tempo para estabilizar, e para isto, há um bit que informa quando ele está pronto (IRVST). Segundo o datasheet, para usar ele você deve prosseguir da seguinte forma:
1. Desabilite o módulo limpando o bit HLVDEN (HLVDCON<4>).
2. Escreva o valor aos bits HLVDL3:HLVDL0 que selecionam a tensão de disparo do HLVD desejada.
3. Arme o bit VDIRMAG para detectar uma tensão acima (VDIRMAG=1) ou abaixo (VDIRMAG=0) da tensão de disparo.
4. Habilite o módulo HLVD armando o bit HLVDEN.
5. Limpe a bandeira de interrupção do HLVD, HLVDIF
(PIR2<2>), que pode ter sido armada por uma interrupção anterior.
6. Habilite a interrupção do HLVD, se desejado, armando os bits HLVDIE e GIE/GIEH (PIE2<2> and INTCON<7>). Uma interrução não será gerada até o bit IRVST estar armado.
E pronto! O HLVD estará aí pra te ajudar a saber se uma tensão está abaixo ou acima de um valor desejado. Isto é muito útil pra monitorar a tensão da bateria que alimenta seu circuito, por exemplo.


Estes eram os últimos módulos que faltavam eu comentar. Sei que ficou parecendo estar tudo por cima mas a ideia era apresentar os módulos pra que vocês saibam da existência deles. Recomendo e muito lerem o datasheet para entenderem como eles funcionam e para como usá-los da melhor forma nos seus projetos. Tire sempre o máximo proveito do microcontrolador que você comprou!

As aparências enganam (algorítimos de ordenação)

Ordenar de forma crescente ou decrescente um sequência de dados na memória de um microcontrolador ou em uma memória externa pode ser necessário uma hora, ainda mais se for usar a busca binária, ou pelo simples fato de você querer dados ordenados. Mas como fazer isto?
Procurando no Google por "sorting algorithms", achei uma página bem interessante que permite visualizar de forma gráfica o que acontece em cada algorítimo. Fui pesquisar como cada um funciona e, buscando o mais simples de programar, acabei no Bubble sorting. Após programar ele com sucesso no PC, resolvi escrever ele e rodar no PIC18F2550. Pra minha surpresa, quando fui organizar uma memória de 512 bytes (4Kbits), esta tarefa foi executada de forma absurdamente lenta (cerca de 10 minutos pra finalizar). Resolvi escrever outro, mas não fui procurar na internet (até procurei o Quick sort mas fiquei com preguiça por dar muito trabalho haha): resolvi pensar. Desenvolvi um algorítimo, mas depois descobri que era exatamente o Selection Sort. Desta vez, o mesmo trabalho que o Bubble levou 10 minutos, em cerca de 3 o Selection fez. "Ora, mas na página lá em cima ele se mostrava bem mais lento. O que houve?" Bem, vou deixar vcs curiosos pela resposta por enquanto. Agora vou mostrar como cada algorítimo realmente trabalha e mostrar os códigos pro PIC que fiz.


Bubble Sorting

Bem simples de programar, o Bubble Sorting consiste em trocar duplas de dados se n+1 > n (sendo n o índice do vetor). Vamos usar de exemplo um vetor de cinco posições com os números 1, 7, 3, 5 e 2. Observe a imagem abaixo que mostra uma iteração apenas (leia como um gibi):


Na próxima iteração, o algorítimo veria que 1 < 3, e portanto não os trocaria de lugar; 3 < 5 mesma coisa, até que por fim chega em 5 > 2, ou seja, trocaria ambos de lugar. Se os números forem iguais, ele não faz nada. O programa precisa repetir isto até todos estarem ordenados.
Abaixo segue um pseudocódigo como exemplo:




Selection Sorting

Tão simples de programar quanto o Bubble, este método surpreendeu ao se mostrar significantemente mais ágil com memórias externas. Este consiste em buscar e "pegar" os menores valores encontrados no vetor e os colocar na ordem crescente, fazendo isso cada vez avançando a partir do último número ordenado. A imagem abaixo exemplifica como isto deve ser feito:


Veja que quando os números forem iguais nada é feito. Creio que este método seja o mais intuitivo (mais próximo do que faríamos como pessoas). Tudo é feito em uma única "passada" pelo vetor.
Aqui segue o pseudocódigo:




Mas e a velocidade?

OK, matando sua curiosidade vamos lá: e a velocidade? A página e vídeos que demonstravam o funcionamento dos algorítimos estavam todos errados? Bem, sim e não.
O problema destes exemplos em animação ou vídeo é considerar que o tempo de leitura e escrita da memória e de comparação sejam todos iguais, mas sabemos que não é verdade. O PIC leva muito mais tempo lendo ou escrevendo uma memória do que simplesmente comparando dois valores da sua RAM. Em uma memória 24C64, por exemplo, a escrita leva cerca de 5 ms a mais do que a leitura, portanto, o algorítimo que menos escrever ganhará vantagem, e em um vetor enorme isto se torna muito relevante, como no exemplo que dei em que o Selection levou cerca de um terço do tempo do Bubble. Ainda mais, o algorítimo que menos escrever aumentará a vida útil da memória. Por isso devemos tomar cuidado com comparações fora do contexto, não só nestes algorítimos mas em qualquer outro. Precisamos pensar muito maior do que as coisas que nos são mostradas. Trago este caso pra que vocês reflitam como é importante considerar os diversos fatores em um programa e escolher métodos conforme a realidade com a qual está trabalhando.