← Parte 3. Endereçamento indireto e controle de fluxo
Parte 5. Projetando Aplicativos Multiencadeados. →
Biblioteca do gerador de código do assembler para microcontroladores AVR
Parte 4. Programando Periféricos e Manipulando Interrupções
Nesta parte do post, como prometido, trataremos de um dos aspectos mais populares da programação de microcontroladores - ou seja, trabalhar com dispositivos periféricos. Existem duas abordagens mais comuns para programação periférica. Primeiro, o sistema de programação não sabe nada sobre dispositivos periféricos e fornece apenas meios de acesso às portas de controle do dispositivo. Essa abordagem praticamente não difere do trabalho com dispositivos no nível do montador e requer um estudo completo do objetivo de todas as portas associadas à operação de um dispositivo periférico específico. Para facilitar o trabalho dos programadores, existem programas especiais, mas sua ajuda, em regra, termina com a geração de uma sequência de inicialização inicial do dispositivo. A vantagem dessa abordagem é o acesso total a todos os recursos periféricos, e a desvantagem é a complexidade da programação e a grande quantidade de código do programa.
O segundo - trabalho com dispositivos periféricos é realizado no nível de dispositivos virtuais. A principal vantagem dessa abordagem é a simplicidade do gerenciamento de dispositivos e a capacidade de trabalhar com eles sem se aprofundar na implementação específica do hardware. A desvantagem dessa abordagem é a limitação dos recursos dos dispositivos periféricos pelo objetivo e pelas funções do dispositivo virtual emulado.
A biblioteca NanoRTOS implementa uma terceira abordagem. Cada dispositivo periférico é descrito por uma classe especializada, cujo objetivo é simplificar a configuração e operação do dispositivo, mantendo toda a sua funcionalidade. É melhor demonstrar os recursos dessa abordagem usando exemplos, então vamos começar.
Vamos começar com o dispositivo periférico mais simples e comum - a porta de entrada / saída digital. Essa porta combina até 8 canais, cada um dos quais pode ser configurado independentemente para entrada ou saída. O esclarecimento para 8 significa que a arquitetura do controlador implica a possibilidade de atribuir funções alternativas para bits de porta individuais, o que exclui seu uso como portas de água / saída, reduzindo assim o número de bits disponíveis. A configuração e o trabalho adicional podem ser realizados no nível de um bit separado e no nível da porta como um todo (escrevendo e lendo todos os 8 bits com um comando). O controlador Mega328 usado nos exemplos possui 3 portas: B, C e D. No estado inicial, do ponto de vista da biblioteca, as descargas de todas as portas são neutras. Isso significa que, para sua ativação, é necessário indicar o modo de uso. No caso de uma tentativa de acessar uma porta não ativada, o programa gerará um erro de compilação. Isso é feito para eliminar possíveis conflitos ao atribuir funções alternativas. Para alternar as portas para o modo de entrada / saída, use os comandos Modo para definir o modo de bit único e Direção para definir o modo de todos os bits de porta com um comando. Do ponto de vista da programação, todas as portas são iguais e seu comportamento é descrito por uma classe.
var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT;
O exemplo acima demonstra como a saída de dados através das portas pode ser organizada. O trabalho com a porta B aqui é realizado no nível de uma categoria e com a porta C no nível da porta, como um todo. Preste atenção ao comando de ativação Activate () . Seu objetivo é gerar no código de saída uma sequência de comandos de inicialização do dispositivo, de acordo com as propriedades definidas anteriormente. Portanto, o comando Activate () sempre usa o conjunto de parâmetros do conjunto que é atual no momento da execução. Considere um exemplo de leitura de dados de uma porta.
m.PortB.Activate();
Neste exemplo, um novo tipo de dados Bit apareceu. O análogo mais próximo desse tipo em idiomas de alto nível é o tipo bool . O tipo de dados Bit é usado para armazenar apenas um bit de informação e permite que seu valor seja usado como condição nas operações de ramificação. Para economizar memória, as variáveis de bit durante o armazenamento são combinadas em blocos de forma que um registro RON seja usado para armazenar 8 variáveis do tipo Bit . Além do tipo descrito, a biblioteca contém mais dois tipos de dados de bits: Pin , que possui a mesma funcionalidade que Bit, mas usa registros IO e Mbit para armazenar variáveis de bit na memória RAM. Vamos ver como você pode usar variáveis de bits para organizar ramificações
m.IF(m.PortB[0], () => AVRASM.Comment(", = 1")); var b = m.BIT(); b.Set(); m.IF(b, () => AVRASM.Comment(", b "));
A primeira linha verifica o status da porta de entrada e, se na entrada 1, o código do bloco condicional é executado. A última linha contém um exemplo em que uma variável do tipo Bit é usada como uma condição de ramificação.
O próximo dispositivo periférico comum e frequentemente usado pode ser considerado um contador / temporizador de hardware. Nos microcontroladores AVR, este dispositivo possui um grande conjunto de funções e, dependendo da configuração, pode ser usado para gerar um atraso, gerar um meandro com uma frequência programável, medir a frequência de um sinal externo e também como um modulador PWM multimodo. Diferentemente das portas de E / S, cada um dos temporizadores Mega328 possui um conjunto exclusivo de recursos. Portanto, cada timer é descrito por uma classe separada.
Vamos considerá-los com mais detalhes. Como fonte de sinal de cada timer, pode ser usado um sinal externo e o relógio interno do processador. As configurações de hardware do microcontrolador permitem configurar o uso da frequência total para dispositivos periféricos ou ativar o divisor único para todos os dispositivos periféricos por 8. Como o microcontrolador permite a operação em uma ampla faixa de frequência, o cálculo correto dos valores do divisor do temporizador para o atraso necessário durante o clock interno exige a especificação da frequência do processador e modo prescaler. Assim, a seção de configurações do timer assume o seguinte formato
var m = new Mega328(); m.FCLK = 16000000;
Obviamente, definir o timer requer o estudo da documentação do fabricante para selecionar o modo correto e entender a finalidade de várias configurações, mas o uso da biblioteca torna o trabalho com o dispositivo mais fácil e compreensível, mantendo a capacidade de usar todos os modos do dispositivo.
Agora, sugiro um pouco de distração da descrição do uso de dispositivos específicos e, antes de continuar, discuta o problema da operação assíncrona. A principal vantagem dos dispositivos periféricos é que eles são capazes de executar determinadas funções sem usar os recursos da CPU. Pode haver complexidade na organização da interação entre o programa e o dispositivo, pois os eventos que ocorrem durante a operação do dispositivo periférico são assíncronos em relação ao fluxo de execução de código na CPU. Os métodos de interação síncrona, nos quais o programa contém ciclos de espera pelo status desejado do dispositivo, anulam quase todas as vantagens da periferia como dispositivos independentes. Mais eficiente e preferido é o modo de interrupção. Nesse modo, o processador executa continuamente o código do encadeamento principal e, quando o evento ocorre, alterna o encadeamento de execução para seu manipulador. No final do processamento, o controle retorna ao thread principal. Os méritos dessa abordagem são óbvios, mas seu uso pode ser complicado pela complexidade da configuração. No assembler, para usar uma interrupção, você deve:
- defina o endereço correto na tabela de interrupção,
- configurar o próprio dispositivo para trabalhar com interrupções,
- Descrever a função de manipulação de interrupção
- fornece a preservação de todos os registros e sinalizadores usados nele, para que a interrupção não afete o progresso do encadeamento principal
- ativar interrupções globais.
Para simplificar a programação do trabalho através de interrupções, as classes de descrição de dispositivos periféricos da biblioteca contêm as propriedades de um manipulador de eventos. Ao mesmo tempo, para organizar o trabalho com um dispositivo periférico por meio de interrupções, você só precisa descrever o código para processar o evento necessário, e a biblioteca executará todas as outras configurações por conta própria. Vamos retornar à configuração do timer e complementá-la com a definição do código que deve ser executado quando os limites para comparar os canais de comparação do timer forem atingidos. Suponha que desejemos que, quando os limites dos canais de comparação forem acionados, certos bits das portas de E / S sejam redefinidas ao transbordar. Em outras palavras, queremos implementar, com a ajuda de um temporizador, a função de gerar um sinal PWM em portas arbitrárias selecionadas com um ciclo de trabalho determinado pelos valores da OCRA para o primeiro e OCRB para o segundo canal. Vamos ver como o código ficará neste caso.
var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var bit1 = m.PortB[0]; bit1.Mode = ePinMode.OUT; var bit2 = m.PortB[1]; bit2.Mode = ePinMode.OUT; m.PortB.Activate();
A parte referente à configuração dos modos do timer foi considerada anteriormente, então vamos para os manipuladores de interrupção imediatamente. No exemplo, três manipuladores são usados para implementar dois canais PWM usando um timer. O código dos manipuladores é bastante óbvio, mas pode surgir a questão de como a economia de estado mencionada anteriormente é implementada para que a chamada de interrupção não afete a lógica do encadeamento principal. A solução, na qual todos os registros e sinalizadores são salvos, parece claramente redundante; portanto, a biblioteca analisa o uso de recursos na interrupção e economiza apenas o mínimo necessário. O loop principal vazio confirma a ideia de que a tarefa de gerar continuamente vários sinais PWM funciona sem a participação do programa principal.
Deve-se notar que a biblioteca implementa uma abordagem unificada para trabalhar com interrupções para todas as classes de descrição de dispositivos periféricos. Isso simplifica a programação e reduz erros.
Continuaremos a estudar o trabalho com interrupções e considerar uma situação em que clicar nos botões conectados às portas de entrada deve causar determinadas ações por parte do programa. No processador que estamos considerando, há duas maneiras de gerar interrupções quando o estado das portas de entrada muda. O mais avançado é o uso do modo de interrupção externa. Nesse caso, somos capazes de gerar interrupções separadas para cada uma das conclusões e configurar a reação apenas para um evento específico (frente, recessão, nível). Infelizmente, existem apenas dois deles em nosso cristal. Outro método permite controlar por meio de interrupções qualquer um dos bits da porta de entrada, mas o processamento é mais complicado devido ao fato de o evento ocorrer no nível da porta quando o sinal de entrada de qualquer um dos bits configurados muda, e mais esclarecimentos sobre a causa da interrupção devem ser realizados no nível do algoritmo pelo software .
Como ilustração, tentaremos resolver o problema de controlar o estado da saída da porta usando dois botões. Um deles deve definir o valor da porta indicada por nós como 1 e o outro redefinir. Como existem apenas dois botões, usaremos a oportunidade para usar interrupções externas.
var m = new Mega328(); m.PortD[0].Mode = ePinMode.OUT; m.PortD.Write(0x0C);
O uso de interrupções externas nos permitiu resolver nosso problema da maneira mais simples e clara possível.
Gerenciar portas externas programaticamente não é a única maneira possível. Em particular, os temporizadores possuem uma configuração que lhes permite controlar diretamente a saída do microcontrolador. Para fazer isso, na configuração do timer, você deve especificar o modo de controle de saída
m.Timer0.CompareModeA = eCompareMatchMode.Set;
Após ativar o timer, o sexto bit da porta D receberá uma função alternativa e será controlado por um timer. Assim, somos capazes de gerar um sinal PWM na saída do processador apenas no nível do hardware, usando o software apenas para definir os parâmetros do sinal. Ao mesmo tempo, se tentarmos usar as ferramentas da biblioteca para acessar a porta ocupada como uma porta de entrada / saída, obteremos um erro no nível de compilação.
O último dispositivo que veremos nesta parte do artigo será a porta serial USART. A funcionalidade deste dispositivo é muito ampla, mas até agora abordaremos apenas um dos casos de uso mais comuns desse dispositivo.
O caso de uso mais popular para essa porta é conectar um terminal serial às informações de texto de entrada / saída. A parte do código referente às configurações de porta neste caso pode ser da seguinte maneira
m.FCLK = 16000000;
As configurações especificadas coincidem com as configurações padrão do USART na biblioteca; portanto, elas podem ser parcial ou completamente ignoradas no texto do programa.
Considere um pequeno exemplo no qual produzimos texto estático para o terminal. Para não inflar o código, nos restringimos à saída do terminal do clássico "Olá, mundo!" no início do programa.
var m = new Mega328(); var ptr = m.ROMPTR();
Neste programa, a partir do novo, a declaração da constante string str . A biblioteca coloca todas as variáveis constantes na memória do programa, portanto, para trabalhar com elas, você deve usar o ponteiro ROMPtr . A saída de dados para o terminal começa com a saída do primeiro caractere da sequência de strings, após o qual o controle vai imediatamente para o loop principal, sem aguardar o final da saída. A conclusão do processo de transferência de bytes causa uma interrupção, no manipulador do qual o próximo caractere da linha é lido. Se o caractere não for igual a 0 (a biblioteca usa o formato terminado em zero para armazenar seqüências de caracteres), esse caractere é enviado para a porta da interface serial. Se chegarmos ao final da linha, o caractere não é enviado para a porta e o ciclo de envio termina.
A desvantagem dessa abordagem é o algoritmo de processamento fixo de interrupção. Não permitirá que a porta serial seja usada de outra maneira que não seja a saída de seqüências estáticas. Outra desvantagem dessa implementação é a falta de um mecanismo para monitorar a ocupação da porta. Se você tentar enviar várias linhas seqüencialmente, pode haver uma situação em que a transmissão das linhas anteriores será interrompida ou as linhas serão misturadas.
Métodos mais eficazes para resolver esse e outros problemas, além de trabalhar com outros dispositivos periféricos, veremos na próxima parte do post. Nele, examinaremos mais de perto a programação usando a classe especial de gerenciamento de tarefas paralelas .