PBL de Sistemas Digitais - Temporizador em display LCD utilizando Assembly na arquitetura ARMv6 para a placa Raspberry Pi 0
- Raspberry Pi 0
- ARMv6
- Windows 10
- Ubuntu
- QEMU Emulator
- Terminal Linux
- Bloco de Notas
- CPUlator
- GDB Debugger
- VSCode
- Assembly
Implementação de um programa de temporização que apresenta uma contagem decrescente no visor LCD. O timer possui a opção de parar a contagem ou reiniciá-la através de 2 botões. O aplicativo também disponibiliza a opção de alterar o tempo através do código.
Para começar a programar na placa Raspberry, é ideal manipular os endereços das pinagens dos GPIOs (General Purpose Input/Output). Através dos endereços da GPIO, é possível definir entradas e saídas ou ligar e desligar um determinado componente. Porém, para que esta manipulação seja possível, é necessário realizar um mapeamento de memória em todo o endereço das GPIO, de maneira que, através desse mapeamento, se possa acessar diretamente qualquer pino através de um simples offset.
Inicialmente, move-se para o registrador 'R0' o valor de endereço de 'fileName'. Esta constante representa o endereço da biblioteca Linux voltada para acesso direto e mapeamento de memória. Posteriormente, se define o tamanho necessário para a leitura do arquivo e, por fim, realiza uma chamada para abrir arquivos sys_open ao sistema operacional.
A primeira instrução foi necessária para abrir o arquivo de mapeamento, mas o processo ainda não está encerrado. Desta vez, move-se para o registrador R5 o endereço dos GPIOs. Em sequência, define-se em pagelen o tamanho (4096) necessário pra realizar a chamada de mapeamento. Por fim, é realizado em R7 a chamada Linux sys_map que retorna para o registrador R8 o mapeamento completo dos pinos da GPIO. Neste ponto, dado um endereço de qualquer componente conectado ou integrado ao GPIO, este pode ser acessado uma vez que se saiba o valor de endereço. Uma vez que o mapeamento foi devidamente concluído, pode-se implementar algoritmos para manipular a GPIO em si.
Neste ponto, dado um endereço de qualquer componente conectado ou integrado ao GPIO, este pode ser acessado uma vez que se saiba o valor de endereço. Uma vez que o mapeamento foi devidamente concluído, pode-se implementar algoritmos para manipular a GPIO em si. Inicialmente, é importante definir alguns componentes associados a pinos do GPIO como saídas do programa, para isto, cria-se uma macro que recebe o valor de endereço do pino, mapeia e o define como uma saída.
Nesta etapa, o valor de endereço do pino passado como parâmetro é movido para o registrador R2 e é feito um offset entre R8 (Registrador que armazena o endereço do mapeamento) e R2. Feito isso, mais uma vez o pino é armazenado em outro registrador e desta vez foi R3, porém, o novo fator de offset é somado a 4. O registrador R3 é utilizado para definir o tamanho de deslocamento da mask '111' que, por sua vez, é usado para limpar o bits de R1, que armazena o endereço mapeado do pino solicitado.
A macro acima tem como função principal ativar/setar um determinado componente de saída conectado a GPIO. Se utilizado em um LED, por exemplo, o LED será acendido. Utiliza um offset de 28 em setregoffset, que é a quantidade padrão e pré-definida pela GPIO da raspberry para set/ligar.
Ao contrário da anterior, esta macro tem a finalidade de desligar/executar um clear em um determinado componente de saída.
Neste projeto, o visor LCD será a saída principal, através dele será ilustrado a contagem de 999 até 0. Para utilizar o visor, é necessário inicialmente conhecer a pinagem de entrada.
Ao total, para este projeto, será utilizado 6 pinos. o pino E ou Enable é utilizado para habilitar uma instrução passada ao LCD, o RS controla o pulso no bit mais significativo e os demais são utilizados para codificar e gerar uma determinada imagem no visor a partir disso. A exemplo da imagem abaixo, se o programador deseja mostrar a letra 'D' em maiúsculo no visor, ele deve seguir a codificação 0100 - 0100.
Antes de codificar os pinos de modo a gerar uma certa letra ou número, é necessário criar algumas funções necessárias para o funcionamento ideal do visor.
A primeira macro é utilizada para definir todos os pinos do LCD como saídas para o Raspberry Pi. Feito isso, pode-se usufruir das macros GPIOTurnOn e GPIOTurnOff para ligar e desligar o pino do LCD que for conveniente pra cada situação.
Esta macro é essencial para executar qualquer instrução no LCD. O enable tem como finalidade não só como ativar uma determinada combinação de pinos passada para o LCD como também controla a velocidade de tempo em que as figuras permanecem em tela. Neste caso, o ideal é colocar um valor em mili segundos consideravelmente baixo, pois a transição se torna mais natural e você pode controlar o tempo parado na tela com outro sleep por fora. Outra particularidade dessa macro é a presença do '.ltorg' ao final do código. Essa instrução é útil para resolver um erro de pool no Assembly.
Voltando ao que foi dito anteriormente, se a ideia for mostrar no visor a letra 'D' maiúscula, então é necessário seguir a codificação estabelecida no datasheet do LCD. Basicamente, há 2 nibbles, em cada um, o bit mais significativo equivale ao pino DB7 e o menos significativo ao pino DB4, a ideia é justamente reproduzir o código binário em combinações de ON e OFF das macros do GPIO.
Seguindo a ordem de cima para baixo, aparece inicialmente o pino RS, que controla o pulso de cada nibble, em seguida OFF ou 0 em DB7, ON ou 1 em DB6, OFF ou 0 em DB5 e mais um OFF ou 0 em DB4. Ou seja, excluindo RS, formou o código '0 1 0 0'.
No kit do laboratório, há 3 botões conectados, respectivamente, no pino 5, pino 19 e pino 26 do GPIO LEV0. Para este projeto, escolheu-se os dois primeiros, o de pino 5 para parar ou iniciar a contagem e o de pino 19 para reiniciar a contagem. Estes dois botões serão mapeados através do GPIO LEV0, pois uma vez que se obtém seu endereço, os botões se encontram no 5° e 19° bit mais significativo dentre os 32 bits disponíveis.
Neste trecho de código, o programa permanecerá em loop enquanto o botão não for pressionado. Inicialmente, o GPIO LEV0 é mapeado e movido para o registrador R6 que, por sua vez, é movido para um registrador auxiliar R7. A lógica fundamental por trás do uso dos botões começa a partir da instrução AND. Supondo que o usuário não tenha apertado o botão, atualmente teríamos o bit 1 na 5° posição dos 32 bits, porém, não apenas este botão, mas todos os demais componentes conectados a este GPIO também estão enviando sinais de 1 ou 0, o que acaba comprometendo com a informação que deseja-se obter (o estado do 5° bit). Devido a isso, realiza-se uma operação de AND entre o R7 e o número 32. A imagem abaixo ilustra o que se obtém após a instrução AND:
Como se pode perceber, independente dos valores que os demais dígitos assumem, após uma operação de AND, todos convergem para o valor 0 com exceção da posição 5, uma vez que 1 é o elemento neutro na operação AND e, portanto, o estado atual do botão de pino 5 será mantido, seja em nível alto ou nível baixo. Em sequência, o programa realiza um deslocamento para a direita em 5 posições no registrador R7. Em outras palavras, o 5° bit que antes representava o estado atual do botão, agora é movido para o bit menos significativo do registrador. Dessa forma, após as duas operações, tem-se duas situações: R7 equivalente a 1, botão não foi pressionado, R7 equivalente a 0, botão foi pressionado. Portanto, o algoritmo compara R7 com #1, se for igual, significa que o botões está inativo, se for igual a 0 o botão foi pressionado e o programa sai do looop.
O contador desenvolvido para este projeto é um contador decrescente de centenas utilizando o BCD (Binary-Coded Decimal) para formar a lógica central por trás de seu funcionamento.
Inicialmente, o programa permanece na label iniciar aguardando o apertar de botão do usuário. Neste ponto, é executado na tela LCD uma mensagem de aviso, "Deseja iniciar?", para mostrar a quem está operando o aplicativo que ele pode executar a ação de iniciar a contagem. (A lógica por trás dos botões já foi explicada anteriormente)
Uma vez apertado o botão e iniciado, chega-se a parte principal do contador: 3 registradores pra centena, dezena e unidade e 1 registrador que descreve o estado atual de contagem, este último varia entre 0 e 1 durante toda a execução do temporizador, se estiver em nível lógico baixo, o contador não está pausado, mas se estiver em nível alto, isso quer dizer que o contador está parado (ou deve parar). Os outros 3 citados anteriormente são expressos em 4 bits e irão variar entre 0000 (0) e 1001 (9).
Após a definição dos registradores, o programa se encaminha para o contador. Um pouco antes, existe a label estado que tem como função principal inverter o valor de R4 (registrador de estado). Esta label é chamada a cada vez que o usuário tenta parar a contagem. E finalmente dentro do contador, mapeia-se dois botões, pino 5 (utilizado anteriormente para iniciar a contagem) e o pino 19 (utilizado para reiniciar a contagem). Importante notar que, se o botão 19 foi pressionado, há um branch direto para a label iniciar, dessa forma o programa voltará a assumir valor inicial para os registradores dos númerosos a serem mostrados. Para o botão 5, responsável por parar a contagem, caso haja sido pressionado, há um branch para a label estado, que inverte o valor de R4. Importante ressaltar que há um sleep entre o mapeamento do GPIO LV0 e a próxima instrução pois essa foi a solução encontrada no projeto para que possibilitasse o usuário conseguir apertar o botão antes que o resto do código se execute (em tempos ínfimos computacionais, é claro).
Caso não haja nenhuma interrupção por parte dos botões, chega-se a parte que efetivamente contabiliza e temporiza. A primeira label a ser executada será a das centenas, seguido de dezenas e por fim unidades, o porquê desta ordem será explicado posteriormente. Primeiramente se verifica o valor atual dos registradores de centena, dezena, unidade e apresenta no display o valor correspondente. Neste projeto, há uma lógica inversa, porque embora esteja decrementando visualmente, estes registradores estão incrementando seu valor. Ou seja, se, por exemplo, o registrador R12 (unidades) estiver valendo '0000', será printado o número '9' no display. Isto vale pra todas as posições, '0001' equivale ao '8', '0010' ao 7, '0011' ao '6' e etc.
Resumindo, como por padrão os registradores iniciam todos em 0, o primeiro número a aparecer no display será 999. Porém ainda atende a um requisito do problema, pois o tempo é completamente ajustável. Se o usuário quiser que ele inicie em '547', por exemplo, basta definir R10 como '0100', R11 como '0101' e R12 como '0010'.
A cada verificação, o programa é encaminhado para labels responsáveis por apresentar o número atual no visor. Por exemplo, se for o momento de printar o valor 8 nas dezenas, a label chamada será oitenta.
A verificicação pra centena chama uma das labels de cem a novecentos e estas, por sua vez, limpam a tela, printam o valor e chamam a label das dezenas. Na label das dezenas, ocorre a verificação, o programa vai pra uma das labels de dez a noventa e estas printam o número sem fazer um clear (ou seja, empurram o cursor pro lado, mostrando 2 números) e volta para a label das unidades. Enfim nas unidades, repete-se o processo de comparar e retornar para as labels de zero a nove. Uma vez dentro de uma label para printar uma unidade, o resultado é apresentado sem clear (mais uma vez o resultado é deslocado, agora totalizando 3 dígitos), executa um comando de sleep para aguardar momentaneamente os 3 dígitos atuais que estão no visor e, por fim, chama uma label não envocada anteriormente: add_u.
A partir de add_u que ocorre o controle e variação da contagem. Começando por add_u, esta label tem como função principal incrementar o valor do registrador R12 (unidade) e verificar se as unidades ultrapassaram o número 9. Se ainda não alcançou um limite, o valor de R12 é incrementado e o programa retorna para o início do contador. Porém, uma vez que a unidade ultrapassa o 9, ou seja, é igual a 10 (1010), a label add_d é chamada.
Em add_d o valor das unidades é zerado e o registrador R11 (dezena) é incrementado. Tal qual a label anterior, aqui as dezenas também não podem ultrapassar o número 9. Se isso não acontecer, retorna para o contador normalmente, mas se alcançou o número 10, é, enfim, chamado a label add_c.
Em add_c o valor das dezenas é zerado e o registrador R10 (centena) é incrementado. Assim como em unidade e em dezena, centena não pode ultrapassar o tamanho 9, mas aqui, caso isso aconteça, isso significa que atingiu-se o valor 1000 (para o display, atingiu os números negativos, já que está em sentido oposto) e, portanto, é o fim da contagem e o programa é encerrado.
Importante ressaltar que em add_u que ocorre a verificação do estado do código. Percebe-se que, se R4 estiver em nível lógico alto, o programa retornará pro contador antes mesmo de realizar as operações com as unidades (e por consequência, as dezenas e centenas). Ou seja, a temporização está parada, não varia.





















