Tecnicas de pooling

Post date: 14/02/2012 14:08:58

Na maioria dos projetos é necessário realizar leituras de valores/sinais externos. A internalização destes sinais é feita através de circuitos dedicados, dependendo, principalmente, do tipo de sinal.

Para que o programa receba estes sinais existem basicamente duas técnicas: recenseamento (pooling) e interrupuções.

A primeira (pooling) permite ler estes sinais de maneira relativamente simples, do ponto de vista de complexidade e tamanho do código. Já a segunda diminui a latência entre o evento e a percepção deste pelo programa mas exige um hardware ($$$) dedicado.

Neste artigo vou abordar um pouco sobre o pooling e as técnicas de programação por trás dele.

Acesso ao Hardware

Primeiramente é necessário que tenhamos acesso ao sinal externo. Em geral estes sinais são mapeados para uma região de memória pré-definida. O modo de operação depende muito do hardware. Neste primeiro exemplo vou tomar a porta D de um microcontrolador da microchip (PIC18F4550).

Esta porta é composta por 8 terminais físicos no microcontrolador que são mapeados no endereço 0xF83. Para realizar a leitura basta ler o valor deste endereço. A única maneira de acessar um endereço físico na memória é através de ponteiros:

unsigned char * portaD; portaD = 0xF83; if((*portaD) == 0xFF) { //se todos os pinos estiverem em nível alto (5v). }

No exemplo estamos criando um ponteiro para char, pois a porta D possui apenas 8 bits, que será inicializado com o endereço da porta. Depois de inicializado o valor pode ser lido realizando-se a derreferência do ponteiro, pois estamos interessados não no endereço 0xF83 mas no valor armazenado no endereço 0xF83.

Uma maneira mais simples nesse caso é criar um define que 1) cria o ponteiro e 2) dereferência o mesmo:

#define PORTD (*((*unsigned char) (0xF83)))

No define o valor 0xF83 recebe um cast para um ponteiro para um unsigned char, do mesmo jeito que no primeiro exemplo. Em seguida esse novo ponteiro é derreferenciado e retorna o valor desejado. O novo código se torna bem mais simples e o uso torna-se bem parecido com uma variável qualquer.

if(PORTD == 0xFF) { //se todos os pinos estiverem em nível alto (5v). }

Pooling

Agora que já temos acesso ao hardware queremos saber o momento exato que a variavel mudou de valor. É neste instante que utilizamos o pooling. Esta técnica consiste em ficar perguntando constantemente para a variável se o valor dela já chegou no valor desejado.

Burro: - Já chegou?

Shrek - Não.

Burro: - Já chegou?

Shrek - Não.

Burro: - Já chegou?

Shrek - Não.

Burro: - Já chegou?

Shrek - Não.

Burro: - Já chegou?

Shrek - Não.

Burro: - Já chegou?

Shrek - Não.

Burro: - Já chegou?

Shrek - Não.

Burro: - Já chegou?

Shrek - Não.

Burro: - Já chegou?

Shrek - Não.

Tradicionalmente é utilizado um loop fazendo essa verificação. Se o valor desejado for 0x01 (apenas o primeiro bit ligado) é necessário ficar no loop enquanto o valor for DIFERENTE de 0x01.

while(PORTD != 0x01); //enquanto for diferente de 0x01 fique perguntando de novo

//Neste ponto do programa a porta D acabou de "virar" 0x01.

O código acima tem apenas um problema: as vezes funciona, as vezes não. =) (1)

Volatile e os perigos da otimização

Por padrão os compiladores são feitos para otimizar os códigos preocupados com a velocidade primeiro e depois para o tamanho. Otimizar, neste caso, é uma péssima ideia. Vejam o que o compilador entende:

while(PORTD != 0x01);

- Deixa eu ver isso aqui... O programador queria que eu verificasse se PORTD é diferente de 0x01. Se for verdade, eu pergunto de novo, se for mentira posso continuar pra sempre perguntando.

- Acontece que o valor dessa variável é sempre o mesmo! Afinal de contas ninguem está mudando o valor dessa variável dentro do loop. Tive uma idéia, (danger mode on) vou fazer apenas um if (que é mais rápido) e se passar na primeira vez coloco um loop infinito:

if(PORTD == 0x01){ for(;;); }

Pronto. O programa não funciona. O problema é que o compilador não percebeu que a variável PORTD pode sim mudar o seu valor mesmo que isto não esteja implicito no loop. Esta mudança só é possível pois PORTD é uma variável que reflete uma entrada EXTERNA. O compilador tentando otimizar a quantidade de vezes que a variável é lida otimizou o código extraindo a comparação para fora do loop. Quando tratamos com variáveis externas devemos indicar isto de modo explícito para o compilador. Para estes casos utilizamos o qualitativo "volatile".

#define PORTD (*((*volatile unsigned char) (0xF83)))

while(PORTD != 0x01); //enquanto for diferente de 0x01 fique perguntando de novo

//Neste ponto do programa a porta D acabou de "virar" 0x01.

Modificando a variável com o qualitativo volatile ordenamos ao compilador que NÃO otimize nenhum código relacionado àquela variável, mas realize a leitura cada vez que a variável for solicitada, sem recorrer ao valor antigo, mesmo que este já esteja na memória.

Detectando a mudança de valor

O mais comum é que uma mesma variável represente diversas chaves/sinais. No nosso exemplo podemos pensar que a porta D possui 8 chaves ligadas, cada uma representada por um bit.

Nesta situação é mais interessados monitorar quando existe a mudança no valor e apenas depois tomar uma decisão. Para isto basta realizar a leitura dos valores em dois momentos e comparar se há mudança no mesmo.

while (PORTD == PORTD);

Notar que o código acima NÃO é um loop infinito. E isso é verdadade porque a variável PORTD está declarada como "volatile", mas principalmente porque é uma variável que pode ser alterada externamente. Por causa do qualitativo "volatile" garantimos que a leitura será realizada DUAS vezes e apenas depois realizada a comparação.

Novamente existe um pequento problema no código acima, algumas vezes ele funciona, algumas vezes não funciona e algumas vezes é intermitente. =) (2)

O problema desta vez é o tempo. Podemos perceber o fluxo da seguinte maneira:

1-> Leitura de PORTD(1)

2-> Armazenamento de PORTD(1) na memória

3-> Leitura de PORTD(2)

4-> Armazenamento de PORTD(2) na memória

5-> Comparação dos valores

6-> Tomada da decisão de repetir ou não.

O algoritmo funciona apenas se a mudança no valor acontecer no momento 2. Em qualquer outro momento que ela acontecer as duas leituras serão do mesmo valor. A solução é bastante simples: criar uma variável temporária com o valor inicial:

#define PORTD (*((*volatile unsigned char) (0xF83)))

char temp = PORTD; while (temp == PORTD);

O código acima garante que o valor da porta D será lido uma vez na inicialização da variável temp e uma vez a cada comparação feita dentro do loop. Deste modo o programa percebe quando a variável mudar de valor externamente e com um atraso máximo de um ciclo de comparação.

Torrando processamento e "soluções"

Pooling é uma técnica bastante simples de ser feita mas toma 100% do tempo do processador por causa do loop de comparações. Sempre que possível evite seu uso. Caso seu projeto possua um sistema operacional com preempção é possível realizar o uso do pooling sem travar o processador. Em todos os outros casos procure alternativas, principalmente as interrupções.

Caso seu uso seja necessário (falta de interrupções por hardware, custo, tempo de projeto) considere em trocar o tipo de loop para um "for". Deste modo é possível implementar uma espécie de timeout para evitar o travamento do sistema.

Velocidade de detecção

É possível quem em alguns sistemas o pooling tenha uma velocidade de detecção superior às interrupções por causa da latência entre o evento da interrupção e o início da rotina de serviço da mesma.

Mas cuidado, se você chegou no nível de contar ciclos de instrução da interrupção ta na hora de arrumar uma CPU mais rápida!