quarta-feira, junho 17, 2009

Sopke-o-dometer - Parte 11

Neste final da série vamos ver a lógica da aplicação em si. Temos três grandes partes: medir o tempo de uma volta da roda, calcular a velocidade correspondente e apresentar nos LEDs este resultado.

Medindo o Tempo de Uma Volta

A medição em si é simples: um contador (tickVolta) é zerado a cada começo de volta e incrementado pela interrupção periódica do timer. A parte mais complicada está em detectar o começo da volta, o que é feito pelo sensor de efeito Hall conectado ao ADC.

Como detalhei anteriormente, o ADC nos fornecerá um valor proporcional à proximidade do imã fixo ao quadro da bicicleta. Nos meus testes verifiquei um valor de 88 quando o sensor está longe do imã. Quando o sensor passa na frente do imã este valor aumenta ou diminui conforme o polo do imã que fica próximo do sensor.

Uma vez que o valor varia ligeiramente durante a passada, uso uma histerese (como detalhei aqui). O código abaixo faz a detecção:
    if (!bDetectou && ((medida > VPOS_1) || (medida < VNEG_1)))
{
// fim de uma volta
bDetectou = TRUE;

// pulando um trecho que explico depois...

tickVolta = 0;
P1OUT |= 1;
}
else
{
if (bDetectou && (medida < VPOS_2) && (medida > VNEG_2))
{
// pode procurar imã de novo
bDetectou = FALSE;
P1OUT &= 0xFE;
}
}
O teste (!bDetectou && ((medida > VPOS_1) || (medida < VNEG_1))) verifica se nos afastamos da referência num momento em que ainda não detectamos o imã, indicando o início da passagem.

Após a detecção do imã é preciso esperar o valor retornar para perto da referência antes de detectar uma outra passagem do imã: (bDetectou && (medida < VPOS_2) && (medida > VNEG_2)).

Cálculo da Velocidade

Esta parte é trivial, desde que se preste atenção às unidades de medida. A velociadade é distância dividida por tempo e queremos exibir o resultado final em Km/h, com uma casa decimal.

A constante CIRCUNF é a circunferência da roda (quanto ela anda em uma volta), medida em milímetros. A variável tickVolta indica quantos ticks de 100 microsegundos demorou a volta.

A velocidade em Km/s corresponde a (CIRCUNF/1000000)/(tickVolta/10000). Para obtermos em Km/h precisamos multiplicar por 3600 (número de segundos por hora). Por último vamos multiplicar por 10 para termos um casa decimal (mais precisamente, estamos medindo a velocidade em unidades de 0,1Km/h). Simplificando as potências de dez, ficamos com:
      // Calcula a velocidade em 0,1 Km/h
// veloc = distância / tempo
// CIRCUNF em mm, ticVolta em 100 us
// veloc = 3600 * (CIRCUNF / 100000) / (ticVolta / 10000)
veloc = (unsigned int) ((CIRCUNF*360L)/tickVolta);
Apresentação do Resultado

Para a apresentação do resultado, dividimos o tempo de uma volta em frames. Cada coluna de cada caracter do resultado será apresentada por um frame. Entre um caracter e outro teremos SPACE frames. A variável tickFrame contem quantos ticks dura cada frame; contFrame conta os ticks até o final do frame atual. Inicialmente contFrame contém a quantidade de ticks que precisam ser aguardados até a posição inicial da mensagem (escolhida para garantir a legibilidade da mensagem).

Uma dificuldade é que a apresentação é feita na volta seguinte a cada medição de velocidade. Se a bicicleta estiver acelerando ou freando a apresentação fica muito ruim. Após algumas tentativas mal sucedidas de compensar isto, acabei optando por não apresentar a mensagem quando ocorre uma mudança grande de velocidade entre duas voltas.

Dada a posição onde eu montei o Spoke-o-dometer e o sentido de rotação da roda, é necessário apresentar a mensagem do fim para o começo. Ou seja da última coluna do último caracter para a primeira coluna do primeiro caracter. Por este motivo a mensagem é montada em ordem contrária e a variável col é iniciada com 5.

Segue o resto do código que é executado quando a roda completa uma volta:
        // Calcula variação em relação à volta anterior
if (tickVolta > tickVoltaAnt)
delta = tickVolta - tickVoltaAnt;
else
delta = tickVoltaAnt - tickVolta;

// Só mostra velocidade se maior que 7.0 Km/h
// e não variou mais que 25%
if ((veloc > 70) && (delta < (tickVolta >> 2)))
tmsg = escreve_kmh(veloc, msg);
else
{
msg[0] = ' ';
msg[1] = 0;
tmsg = 1;
}
pMsg = &msg[0];

// Calcula o tempo por frame e a contagem para a posição inicial
tickFrame = tickVolta / NFRAMES;
contFrame = (tickVolta *(NFRAMES - POS_MSG - tmsg*(5+SPACE))) / NFRAMES;
tickVoltaAnt = tickVolta;
col = 5;
A formatação da mensagem é feita por uma conversão "caseira" do número binário em caracteres:
// Formata a velocidade para apresentação
// Mensagem é montada em ordem reversa
// "h/mk#,###"
int escreve_kmh (unsigned int vel, char *pv)
{
int tam = 7;

*pv++ = 'h';
*pv++ = '/';
*pv++ = 'm';
*pv++ = 'k';
*pv++ = '0' + (vel % 10); vel /= 10;
*pv++ = ',';
*pv++ = '0' + (vel % 10); vel /= 10;
if (vel != 0)
{
*pv++ = '0' + (vel % 10); vel /= 10;
tam++;
if (vel != 0)
{
*pv++ = '0' + (vel % 10);
tam++;
}
}
*pv = 0;
return tam;
}
O gerador de caracteres, que indica qual LED deve estar aceso em cada coluna de cada caracter está em uma matriz de bytes:
// Gerador de Caracteres
// Cada caracter corresponde a 5 bytes
// Cada byte corresponde a uma coluna
static const byte gc[16][5] =
{
0x7C, 0x82, 0x82, 0x82, 0x7C, // 0
0x02, 0x42, 0xFE, 0x02, 0x02, // 1
0x46, 0x8A, 0x92, 0xA2, 0x42, // 2
0x44, 0x82, 0x92, 0x92, 0x6C, // 3
0xF0, 0x10, 0x10, 0x10, 0xFE, // 4
0xF2, 0x92, 0x92, 0x92, 0x8C, // 5
0x6C, 0x92, 0x92, 0x92, 0x0C, // 6
0x80, 0x86, 0x98, 0xA0, 0xC0, // 7
0x6C, 0x92, 0x92, 0x92, 0x6C, // 8
0x60, 0x92, 0x92, 0x92, 0x6C, // 9
0x00, 0x00, 0x00, 0x00, 0x00, // SPACE (10)
0x00, 0x04, 0x06, 0x00, 0x00, // , (11)
0x7E, 0x08, 0x14, 0x22, 0x00, // k (12)
0x1E, 0x10, 0x0C, 0x10, 0x1E, // m (13)
0x03, 0x0C, 0x18, 0x30, 0x60, // / (14)
0x7E, 0x08, 0x08, 0x06, 0x00 // h (15)
};
Sobra apenas o trabalho de mudar os LEDs ao final de cada frame, na interrupção do timer:
                if (contFrame == 0)         // fim do frame atual
{
if (col > 0)
{
// vamos para a coluna anterior
col--;
valor = gc [char_index(*pMsg)][col];
P1OUT &= 0xC1;
P1OUT |= (valor & 0x3E);
P2OUT &= 0x3F;
P2OUT |= (valor & 0xC0);
contFrame = tickFrame;
}
else
{
// passar ao caracter seguinte
P1OUT = P1OUT & 0xC1;
P2OUT = P2OUT & 0x3F;
col = 5;
pMsg++;
if (*pMsg == 0)
tickFrame = 0; // fim da mensagem
else
contFrame = tickFrame * SPACE;
}
}
else
contFrame--;
A rotina char_index devolve o indice no gerador de caracteres para cada caracter:
/ c_index: retorna o índice do caractere na matriz gc
int char_index(char c)
{
if ((c - '0') >= 0 && (c - '0') < 10)
return (c - '0');
if (c == ' ') return CHAR_SPACE;
if (c == ',') return CHAR_COMMA;
if (c == 'k') return CHAR_K;
if (c == 'm') return CHAR_M;
if (c == '/') return CHAR_SLASH;
if (c == 'h') return CHAR_H;

//else (caractere estranho)
return CHAR_SLASH;
}

Com isto acredito ter coberto todo o código. Em caso de dúvida, o formulário de comentários está logo abaixo.

Nenhum comentário: