Boa noite, hoje iremos falar de como usar os pinos do nosso micro-controlador como entradas e saídas digitais, este deveria ter sido até o primeiro tema a estudar nesta série de tutoriais pois apesar de relativamente simples é de grande uso.
Então, para começar vamos ver quais os registos que cada porto tem associado a ele. Muito basicamente um porto é um conjunto de 8 pinos do micro-controlador que associados aos mesmos registos de controlo e que são numerados alfabeticamente, PORTA, PORTB, PORTC, o numero de portos depende do numero de pinos que o encapsulamento do nosso micro-controlador tem, no caso do atmega328p temos os portos B,C e D, mas o unico que tem os 8 pinos disponível é o porto D, isto acontece porque cada pino pode ter variadas funcionalidades, por exemplo os pinos 6 e 7 do porto B também são os pinos onde se liga o oscilador(cristal), o pino 6 do porto C é onde fica o botão do reset, no porto C do pino 0 ao pino 5 temos as 6 entradas analógicas, e assim sucessivamente para os outros pinos, e isto variada de modelo para modelo pois cada um tem mais ou menos funcionalidades e mais ou menos pinos que permitem ter implementadas essas mesmas funcionalidades, porque no caso do atmega328p se o encapsulamento for TQFP(é um dos vários tipos de smd) temos mais dois pinos ligados ao ADC.
Nesta imagem podem ver a funcionalidade de cada pino e a que porto ele está associado, o chip em questão é o atmega328p usado no arduino:
E a sua relação com os nomes dados aos pinos no arduino:
Então, como vamos nós interagir com os nossos pinos digitais?
Para começar temos um registo que define se um pino é uma entrada ou uma saída digital, esse registo é o DDRx, sendo x a letra da porta que queremos configurar, no nosso caso temos o DDRB, o DDRC e o DDRD.
Como qualquer valor lógico, cada bit do registo DDRx pode ser 0 ou 1, sendo que um bit a 1 neste registo configura um pino como saída e um 0 configura o pino como uma entrada, vou agora usar um exemplo para mostrar como é que configuramos o porto D em que os pinos 0,1,2,3 são entradas e os pinos 4,5,6,7 são saídas:
DDRD = 0b11110000;
Se quisermos que sejam todos saidas:
DDRD = 0b11111111;
;
E se forem todos entradas, e se eu quiser que o primeiro e o segundo sejam saídas e o resto entradas?Respondam vocês!
No caso do porto D temos que ter cuidado porque os pinos 0 e 1 são os pinos que fazem a comunicação serial, e não convém mudar os valores do registo DDRD se não podemos ter problemas de recepção ou de emissão de dados, dai usar o |= e o o &= e não o = apenas, assim declara-mos os nossos DDRD, fazemos a inicialização da USART e sabemos que vai funcionar.
Agora que sabemos como configurar os pinos vamos ver como é que fazemos os pinos tomarem um valor definido por nós, para escrever nos pinos, ou seja usar os pinos como saídas, usamos um novo registo o registo PORTx, tal como o DDRx trocamos o x pelo porto que queremos usar.
Após configurar um pino como saída podemos escrever o que quisermos nesse mesmo pino, para isso, e partindo do pressuposto que todo o porto está configurado como saída, eis o método usado para escrever no porto:
DDRD = 0b11111111; //Configura todos os pinos do porto D como saídas
PORTD = 0b11111111; //Coloca todos os pinos do porto D a 1(HIGH)
De notar que usamos = e não |=, isto poderá causar-nos problemas, como vou explicar mais á frente.
Se quisermos colocar todos os pinos a 0 fazemos assim:
DDRD = 0b11111111; //Configura todos os pinos do porto D como saídas
PORTD = 0b00000000; //Coloca todos os pinos do porto D a 0(LOW)
E se for um padrão de ligado, desligado,lidago, desligado, até preencher os 8 bits?
Agora só nos falta saber como ler um pino, afinal nós não queremos só passar dados para o exterior, também queremos recolher dados, e para os recolher usamos um terceiro registo, o registo PINx, que tal como o DDRx e PORTx trocamos o x pela letra do porto que queremos ler, primeiramente temos de configurar os pinos do porto escolhido como entradas e depois lê-mos essas mesmas entradas, assim:
DDRD = 0b00000000; //Configura todos os pinos do porto D como entradas
char my_var=0; //Criamos uma variável com 8 bits para armazenar o que lemos do porto D
my_var = PIND; //E lê-mos os pinos e armazenamos esses dados na nossa variável
Mas nem sempre queremos ler um porto inteiro, podemos querer só a informação de um pino, por exemplo um botão, para isso podemos usar um método diferente:
DDRD = 0b11111110; //Configura os 7 bits mais significativos do porto D como saídas e o bit menos significativo como saída;
if(PIND & (1<<PD0)){
//O código que colocar-mos aqui dentro é executado quando o pino 0 está a 1(HIGH)
}
else{
//Esta parte é executada quando o pino 0 está a 0(LOW)
}
[/code
Ou tambem podemos ler o valor de um pino para uma variavel, assim:
[code]DDRD = 0b11111110;
char my_var //Variavel onde vamos guardar os dados
my_var = (PIND & (1<<PD0)); //E assim lemos o estado do pino 0 para a variavel my_var
Assim como temos o PD0 temos qualquer Px0..7 em que x é a letra do porto que queremos usar e o numero entre 0 e 7 é o numero do pino, usando o operador | podemos comparar vários pinos assim:
DDRD = 0b1111110; //Configura os 7 bits mais significativos do porto D como saídas e o bit menos significativo como saída;
if(PIND & ((1<<PD0) | (1<<PD1))){
//o código aqui colocado será executado quando os pinos 0 e 1 estiverem a 1(HIGH)
}
Penso que já estão a perceber a ideia.
Mas ainda podemos fazer mais umas coisas com o nosso pequeno chip, quando usamos um botão como já devem ter reparado, quando o botão é premido normalmente liga o pino á massa, o que provoca um 0(LOW), mas quando se deixa de premir o botão o pino fica a flutuar, ou seja o seu valor é indefinido e isso não é bom e pode gerar problemas, então o nosso atmega328p tem pull-ups internos, o que são pull-ups?
Pull-ups são resistências com um valor de resistência elevado, 10K ohms ou mais para permitir que o pino possa continuar a ser ligado a 0(LOW) sem causar um curto-circuito e queimar alguma coisa, mas que impede o pino de ficar a flutuar num estado indefinido, para isso primeiro temos de configurar os nosso pinos como entradas, e depois isto vai parecer estranho, nos pinos que escolhemos para entradas, vamos usar o registo PORTx para escrever o valor 1(HIGH) nos pinos que escolhemos como entradas e isso vai activar os pull-ups em que escrever-mos 1(HIGH) nos pinos que estão configurados como saídas, um exemplo para clarificar:
DDRD = 0b00111100; //Os primeiros 2 e os últimos 2 bits estão configurados como entradas, os restantes são saídas
PORTD = 0b00000011; //Os primeiros 2 pinos continuam sem pull-ups mas os últimos 2 pinos agora têm os pull-ups activados
//Sendo os primeiros pinos os que ficam logo ao lado do 0b e os últimos estão ao lado do ponto e virgula
Agora, relembrando uma pergunta feita á algum tempo atrás, porque razão usar = e não o |= e &=~ me pode trazer problemas?
Porque como podemos ter um porto com pinos configurados como entradas e outros como saídas, e podemos ter pinos com pull-ups e outros sem como no exemplo acima, vou introduzir mais um conceito util na manipulação básica de registos, máscaras de bits.
E o que são, máscaras de bits são variáveis de 8 bits que têm um determinado padrão de 0's e 1's que estão lá para evitar por exemplo que desactivemos os pull-ups de um pino quando escrevemos para outros pinos que são saídas, vou aqui dar um pequeno exemplo usando o código de cima para exemplificar:
char mascara = 0b00000011; //Esta mascara impede-me de mexer nos últimos dois bits quando utilizar operadores lógicos
DDRD = 0b00111100; //Os primeiros 2 e os últimos 2 bits estão configurados como entradas, os restantes são saídas
PORTD = 0b00000011; //Os primeiros 2 pinos continuam sem pull-ups mas os últimos 2 pinos agora têm os pull-ups activados
PORTD = 0b00101010; //Se fizer isto vou desligar o pull-up do pino 0
//Então em vez de usar o perigoso igual uso o |= para colocar os bits que quero a 1
PORTD &= 0b00101010; //Mas mesmo assim continuo a remover o pull-up do pino 0
PORTD &= (0b00101010 | mascara); //Assim já fica o pino 0 a 1(HIGH) e os outros bits continuam como eu quero
Assim já podem usar os vossos pinos como entradas e saídas digitais e tirar proveitos dos pull-ups que estão incluídos no atmega.
Sei que o assunto de máscaras e operadores lógicos com o AND( & ou &=) o OR(| ou |=) o NOT(~ ou ~=) e o XOR(^ ou ^=) são de difícil compreensão, mas qualquer duvida sobre os mesmos é só dizer, e volto a referir o tutorial alojado no AvrFreaks.com, mais concretamente aqui:
http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=37871
Este tutorial foi algo mais teórico, por isso vamos lá criar um programinha que lê um pino e que acende e apaga o led do arduino:
Main{
Configurar os pinos, neste exemplo será do porto D
Loop infinito{
ler o valor do botão
Se led estiver ligado e botão==1 desligar led
Se led estiver desligado e botão==0 ligar led
}
}
Antes de passar para C tenho de falar noutro assunto, e que nos pode trazer problemas se não o tivermos em atenção, que é o facto de o botão não criar uma transição limpa de 0(LOW) para 1(HIGH) nem de 1(HIGH) para 0(LOW), mas sim, transita muitas vezes de estado até parar num estado definitivo, isto chama-se oscilação e pode levar a que o led se ligue e desligue várias vezes apesar de só premir-mos o botão uma vez, para evitar isso temos de usar uma técnica chamada deboucing, e existem muitos meios para atingir esse mesmo deboucing, mas o que vou usar é um método muito muito simples que recorre a um delay, podem ler mais sobre as funções de delay aqui:
http://www.nongnu.org/avr-libc/user-manual/group__util__delay.html
O método é simples, lê-mos o estado do botão, se for 1(HIGH) quer dizer que não carregamos no mesmo(já mostro uma foto do circuito para perceberem melhor), se for 0(LOW) quer dizer que pressionei o botão, então vou esperar um determinado tempo, neste caso irei usar 25 mili-segundos, mas se acharem que o valor deve ser outro digam, e se o valor se mantiver 0(LOW) considero isso como uma leitura válida.
Esta lógica invertida deve-se ao facto de ter o pull-up activado, logo está a 1(HIGH) quando não estou a tocar no botão e fica 0(LOW) quando carrego no botão.
#include <avr/io.h>
#define F_CPU 16000000UL
#include <util/delay.h>
uint8_t readButton(void);
int main(void){
DDRD &= ~(1<<PD2); //Configura o pino 2 do porto D como entrada
PORTD |= (1<<PD2); //Activa os pull-ups no pino 2 do porto D
DDRB |= (1<<PB5); //Configura o pino 5 do porto B como saida, é o digital 13 do arduino
while(1){ //Loop infinito
if(readButton()==1){ //Verifico o estado do botão
PORTB ^=(1<<PB5); //O operado ^ chama-se XOR e se um bit está a 0 ele coloca-o a 1 e se está a 1 coloca-o a 0
}
_delay_ms(250); //Delay entre toques no botão
}
}
uint8_t readButton(void){
if((PIND & (1<<PD2)) == 0){ //Se o botão foi premido
_delay_ms(25); } //Faço o debounce do mesmo
if((PIND & (1<<PD2)) == 0){ //Verifico o valor do botão
return 1; } //Se continuar a 0 é uma leitura válida
else{ //Se não continuar a 0, é uma leitura inválida
return 0; }
}
Penso que já não é necessário mostrar passo a passo como se usa o AvrStudio e o Avrdude para programar o atmega, mas relembro-vos de que têm de carregar no botão de reset sempre que querem fazer upload de um programa novo.
O circuito usado é o seguinte:
Pino 2 do porto D(digital 2 do arduino) ligado a uma das pernas do botão, e a outra perna do botão é ligada á massa(Um dos pinos chamados GND no arduino).
Aqui fica um pequeno vídeo do código a correr no arduino, apesar do pequeno debounce que implementei nem sempre funciona perfeitamente, isso pode-se dever ao facto de ter pontaria e carregar no botão enquanto ele está a fazer o delay na função main e algumas vezes pode-se ver que o led liga e desliga apesar de só carregar uma vez, isso acontece porque os botões não são todos iguais e um delay de 25ms pode não ser suficiente.
Ignorem o ruído de fundo, é apenas um filme qualquer que estava a passar na tv, mais uma vez, qualquer duvida digam e boa programação.