Conteúdos, Práticas e Experimentações em Eletrônica
26/JAN/2022
A IDE do Arduino, bem como a sua linguagem C++ permitem o acesso direto às portas do microprocessador Atmel ATmega 328P. Como explicado na Wikipedia portas de um microprocessador são instâncias de I/O (input / output), pelas quais a CPU se comunica com o "mundo" externo (os dispositivos periféricos). As portas de um microprocessador admitem acesso por software através de instruções especiais, permitindo a transferência de dados de / para a memória principal e a realização de operações específicas sobre os bits disponibilizados em cada porta.
No caso do processador Atmel ATmega328P, utilizado no Arduino UNO R3, existem 4 portas:
Este texto vai focar no uso das portas B e D configuradas para I/O Digital. Deve-se notar que os mesmos conceitos poderiam se aplicar também a porta C, desde que configurada para I/O Digital. Assim, os pinos físicos associados a estas portas serão tratadados de forma agrupada, como pertencentes aos registradores associados a cada porta. Eventualmente, o texto fará referência às aplicações alternativas dos bits disponíveis em cada porta. Entretanto, para maiores detalhes a esse respeito, recomenda-se a leitura do próprio manual da Atmel ATmega 328P, referenciado anteriormente. Alternativamente, pode-se consultar a página de manipulação de registradores do Arduino, que traz vários exemplos sobre como usar os registradores associados às portas B e D.
As portas disponíveis são bi-direcionais, o que significa que podem ser usadas tanto como entradas quanto como saídas. Do ponto de vista do hardware, todos os pinos associados às portas de I/O possuem diodos de proteção conectados tanto a Vcc quanto a GND. No que concerne à sua capacidade de corrente, as portas apresentam características simétricas tanto quando atuam fornecendo (source) ou consumindo (sink) a corrente de carga. Cada pino possui também um resistor de "pull up" que pode ser habilitado ou desabilitado, em função da sua configuração como entrada ou saída. Os pinos serão referidos indicando-se a porta a que se referem e a sua posição na referida porta. A nomenclatura a ser usada é PORTxn, onde x se refere a porta (A, B, C ou D) e n se refere ao bit. A figura a seguir ilustra uma porta típica, exemplificando o pino 3 da porta D:
Figura 1 - Esquemático típico de um pino I/O
Nesta figura, toma-se como exemplo o pino PORTD3. De acordo com a nomenclatura empregada, este pino pertence a porta D e indica o bit 3. No ATMEL 328P, na configuração de 28 pinos PDIP, PORTD3 corresponde ao pino 5. No Arduino UNO R3, este pino é conhecido como porta digital 3, sendo notado, na placa, com o número 3, e ocupando a quarta posição da direita para a esquerda, quando se olha a placa por cima (com a serigrafia aparente) e com os pinos digitais a frente (e o conector USB a esquerda).
As diversas portas disponíveis são associadas a endereços válidos da memória de I/O permitindo o acesso, para cada uma delas, a três registradores:
Como indicado, PORTx (pode ser PORTB, PORTC ou PORTD) indica o registrador de dados da porta em questão. Este registrador permite leitura e escrita e reflete o estado que se deseja para os pinos da porta em questão. Neste contexto, e a título de exemplo, ao escrever o byte 0x00 (ou 00h, ou ainda, B00000000) no registrador PORTD, vai-se alterar os oito pinos da porta D para o valor lógico '0'. Ou seja, de uma única vez, atribui-se o valor LOW aos oito bits digitais de 0 a 7. No que concerne a programação, a instrução:
PORTD = 0; //Alternativalmente pode-se escrever PORTD = 0x00 ou PORTD = B00000000
Equivale a:
digitalWrite(0,LOW);
digitalWrite(1,LOW);
digitalWrite(2,LOW);
digitalWrite(3,LOW);
digitalWrite(4,LOW);
digitalWrite(5,LOW);
digitalWrite(6,LOW);
digitalWrite(7,LOW);
Em outro exemplo, pode-se atribuir a PORTx um número diferente de 0. Considerando que o número decimal 11 é representado em binário como B00001011, a instrução:
PORTD = 11; //Alternativalmente pode-se escrever PORTD = 0x0B ou PORTD = B00001011
Equivale a:
digitalWrite(0,HIGH);
digitalWrite(1,HIGH);
digitalWrite(2,LOW);
digitalWrite(3,HIGH);
digitalWrite(4,LOW);
digitalWrite(5,LOW);
digitalWrite(6,LOW);
digitalWrite(7,LOW);
Já o registrador DDRx indica a direção de dados dos bits da porta em questão. Tal como o registrador PORTx, DDRx admite leitura e escrita. Por direção de dados deve-se entender a definição de que o bit em questão será configurado bit de entrada ou bit de saída. Como exemplo, considerando-se que o número 193 é representado em binário como B11000001, a instrução:
DDRD = 193; //Alternativalmente pode-se escrever PORTD = 0xC1 ou PORTD = B11000001
Equivale a:
pinMode(0,OUTPUT);
pinMode(1,INPUT);
pinMode(2,INPUT);
pinMode(3,INPUT);
pinMode(4,INPUT);
pinMode(5,INPUT);
pinMode(6,OUTPUT);
pinMode(7,OUTPUT);
Finalmente, o registrador PINx indica o estado dos pinos da porta. Ao contrário de PORTx e DDRx, PINx só admite leitura. Por outro lado, como indicado no datasheet do ATmega328P, escrever o bit '1' em algum pino deste registrador vai causar a comutação ("toggle") do bit em questão. Este resultado pode causar bastante confusão e levar a resultados inesperados pelo que é altamente recomendado que se evite escrever qualquer valor em PINx. Desta forma, PINx será usado fundamentalmente para leitura dos pinos da porta x. Como exemplo, admitindo-se que os oito bits digitais de 0 a 7 foram configurados como entrada, e que, ao pino digital 5 foi aplicada uma tensão de 5V, a instrução:
d = PIND;
Equivale a:
d0 = digitalRead(0);
d1 = digitalRead(1);
d2 = digitalRead(2);
d3 = digitalRead(3);
d4 = digitalRead(4);
d5 = digitalRead(5);
d6 = digitalRead(6);
d7 = digitalRead(7);
E vai resultar no seguinte:
d == 32
d0 == 0
d1 == 0
d2 == 0
d3 == 0
d4 == 0
d5 == 1
d6 == 0
d7 == 0
Eventualmente vai-se deparar em situações em que alguns bits de uma porta serão configurados como entradas e outros como saídas. Pode também ocorrer um cenário em que se deseja alterar apenas alguns pinos digitais, sem, entretanto, impactar o estado dos demais pinos. Nestes casos, o uso da instrução PORTx pode ser um pouco mais "tinhoso"... Como exemplo, vamos considerar que se deseja alterar os pinos digitais 2, 3 e 4, para os valores '1', '1' e '0', respectivamente, sem alterar os pinos digitais 0, 1, 5, 6 e 7. Neste caso, o mais seguro seria utilizar "máscaras" para separar os bits que não se deseja alterar daqueles que se vai alterar. Vamos por partes.
A alteração dos três bits indicados implicaria em escrever o número B---011--. Note que eu usei o '-' para indicar as posições que não devem ser alteradas. Nas posições 2, 3 e 4, já indiquei os valores '1', '1' e '0', como desejado. Se substituirmos as posições indicadas com '-' por '0', o número resultante seria 12 decimal (ou 0x0C, ou ainda B00001100).
Numa abordagem simplista, pode-se imaginar a possibilidade de simplesmente escrever:
PORTD = 12;
Note que esta instrução causaria um "estrago" no seu circuito uma vez que, além de alterar os três bits desejados, alteraria todos os demais (o que seria altamente indesejado). Assim, antes de escrever qualquer coisa no registrador PORTD, faz-se necessário "proteger" os bits que não se deseja alterar.
A proteção destes bits se dá com a máscara B11100011. Note que esta máscara usa o '0' nas posições 2, 3 e 4 e '1' nas demais posições. Esta máscara se constitui no número 227 decimal (ou 0xD3).
Assim, uma operação segura para obter o resultado desejado consistiria em:
PORTD = (PIND&B11100011)|B00001100;
Assim:
Como exemplo, imagine-se que as tensões aplicadas nos pinos digitais de 0 a 7 formem o número B10000110 (ou 0x86 ou 134 decimal). Então:
Deve-se notar que o conjunto de instruções discutidos (DDRx, PORTx e PINx) permitem alterar o conteúdo da porta x de forma muito flexível e atuando simultaneamente em todos os seus bits. É oportuno considerar que além das operações lógicas e de atribuição, podem-se usar diversas outras operações, inclusive rotação de bits e operações aritméticas.
Por fim, um comentário adicional diz respeito aos bits digitais 0 e 1 (da porta D). Além da função de I/O, tais bits são utilizados na comunicação serial nas funções de Rx e Tx respectivamente. Neste sentido, o uso dos pinos 0 e 1 requer algum cuidado, especialmente quando se pretende usar a comunicação serial no programa a ser desenvolvido. Neste cenário, qualquer alteração efetuada nestes bits está alterando também a comunicação serial. Isto pode atrapalhar as operações de upload de novas versões de programa e mesmo a implementação de algum a forma de depuração do código, em caso de "bugs" de progrmação. Uma boa prática é simplesmente não utilizar tais pinos no programa a ser implementado.
Como conclusão, o uso das instruções DDRx, PORTx e PINx vai reduzir bastante o código a ser escrito no programa, especialmente quando se deseja acessar múltiplos bits de entrada ou saída. Além disso, apresenta como benefício a realização das operações de leitura e escrita de forma simultânea, evitando um atraso ("delay") na leitura ou escrita dos diversos pinos, quando se utiliza as funções digitalRead() e digitalWrite() de forma sequencial, bit a bit. Por outro lado, o código resultante é bem menos "legível" e com depuração bem mais complexa.