8. Arduino

Placas Arduino

Numa primeira análise, a passagem das placas Raspberry Pi para as placas Arduino parece ser um passo atrás. Com efeito, o microcontrolador presente nas placas Arduino (o ATmega328P) é de apenas 8 bits e uma frequência de 16MHz. Contudo a inexistência de qualquer sistema operativo faz com que todo o poder computacional seja destinado à execução do nosso programa (o único a ser executado pelo processador) o que pode permitir tempos de resposta efetivamente menores, aproximando-nos dum funcionamento em tempo-real.

Interface

As placas Arduino Uno possuem igualmente uma interface para ligação a dispositivos exteriores à placa. Nesta página iremos descrever como pode ser utilizada essa interface nomeadamente em termos de entradas/saídas de uso genérico (GPIOs).

A seguinte imagem mostra a localização dos pinos dessa interface com as funcionalidades a eles associados:

R3_pinout

De notar que na placa Arduino Uno (baseada no microcontrolador ATmega328P), o nível de tensão correspondente ao valor lógico 1 é de cerca de 5V. Cuidados especiais deverão ser tomados caso se queira interligar com dispositivos de 3.3V (usando adaptadores de níveis de tensão ou divisores de tensão apropriados).

Criação de projetos

O facto destas placas não possuírem qualquer sistema operativo faz com que o procedimento para correr um programa seja agora diferente:

  • O projeto é criado num PC com o Eclipse
  • Nesse IDE é escrito o código fonte em linguagem C/C++
  • O código é compilado gerando-se o código executável
  • Esse programa é descarregado via porta série para a memória de programa do CPU
  • É realizado um reset à placa iniciando assim a execução do programa
  • Eventuais comunicações entre placa e PC faz-se pela porta série usando o PuTTY

Nesta página iremos começar por criar um projeto chamado test que usaremos depois para apresentar algumas das funcionalidades desta placa. Para isso com a perspetiva Arduino ativa fazemos:

  • File > New > Project… > Arduino > New Arduino sketch
    Next
  • Project name: test
    Next
  • Board: Arduino/Genuino Uno
    Upload Protocol: Default
    Port: COMx
    Next
  • select code: Default cpp file
    Finish

Isto cria dois ficheiros:

  • test.cpp
  • text.h
// Do not remove the include below
#include "test.h"


//The setup function is called once at startup of the sketch
void setup()
{
// Add your initialization code here
}

// The loop function is called in an endless loop
void loop()
{
//Add your repeated code here
}

O ficheiro test.cpp contem basicamente duas funções:

  • setup() – que será executada uma única vez após um reset à placa
  • loop() – que será depois executada ciclicamente

Em geral todo o ficheiro .cpp é acompanhado por um ficheiro .h (designado de “header file“) que contem os protótipos das diversas funções definidas nessa porção de código fonte:

// Only modify this file to include
// - function definitions (prototypes)
// - include files
// - extern variable definitions
// In the appropriate section

#ifndef _test_H_
#define _test_H_
#include "Arduino.h"
//add your includes for the project test here


//end of add your includes here
#ifdef __cplusplus
extern "C" {
#endif
void loop();
void setup();
#ifdef __cplusplus
} // extern "C"
#endif

//add your function definitions for the project test here




//Do not add code below this line
#endif /* _test_H_ */

Comunicação UART

Como já foi referido a comunicação da e para a placa é feita pela porta série (comunicação UART). No exemplo seguinte essa comunicação é iniciada a 115200bps, enviando-se depois uma mensagem de texto:

#include "test.h"

void setup() {
    Serial.begin(115200);
    Serial.println("Arduino test");
}

void loop() {

}

Agora basta compilar o código (carregando no botão verify) e descarregar o código máquina resultante para dentro da memória de programa do microcontrolador ATmega328P da placa Arduino (através do botão upload) recorrendo à ferramenta avrdude.

Nota 1: Antes da descarga (realizada pela ferramenta avrdude), é feita uma pergunta se pretendemos compilar o código (build before upload?).

Nota 2: O processo de descarga pode ser acompanhado na janela Console, e se for bem sucedido terminará com a mensagem: upload done

A abertura duma ligação série (115200,8,N,1) com a placa usando o PuTTY provoca-lhe um reset que inicia o programa e faz surgir a mensagem “Arduino test”:

Arduino_test

IMPORTANTE: A ligação UART é usada quer para a programação da placa, quer para eventuais comunicações entre programa e PC, pelo que será preciso fechar a ferramenta PuTTY para realizar nova programação (e vice-versa). De outro modo surgirá um erro semelhante ao ilustrado na figura seguinte:

port_busy

Entradas/saídas

As várias funcionalidades associadas ao uso das portas de entrada/saída serão seguidamente demonstradas através do seguinte circuito, composto por dois botões (Bp e Bi) e três LEDs (Lr: vermelho, Ly: amarelo, e Lg: verde):

Semaforo_arduino_bb

No exemplo seguinte iremos definir o pino 13 (ao qual está ligado o LED L da placa) como uma saída fazendo-o depois alternar o seu estado a cada segundo:

#include "test.h"

#define DELAY 1000
const int ledPin = 13;

void setup() {
    Serial.begin(115200);
    Serial.println("Arduino test");

    pinMode(ledPin, OUTPUT);
}

void loop() {
    digitalWrite(ledPin, LOW);
    delay(DELAY);
    digitalWrite(ledPin, HIGH);
    delay(DELAY);
}

Vamos agora criar um contador que será incrementado por cada vez que se carrega no botão Bi (pino 11):

#include "test.h"

#define DELAY 1000
const int ledPin = 13;
const int butPin = 11;
int counter = 0;

void setup() {
    Serial.begin(115200);
    Serial.println("Arduino test");

    pinMode(ledPin, OUTPUT);
    pinMode(butPin, INPUT);
}

void loop() {
    digitalWrite(ledPin, LOW);
    delay(DELAY);
    digitalWrite(ledPin, HIGH);
    delay(DELAY);

    if (digitalRead(butPin) == LOW) {
        counter++;
        Serial.println(counter);
    }
}

Este programa apresenta já as 3 funções básicas para o uso de portas de entrada/saída:

Esta solução não funciona contudo de forma correta pois o estado do botão é testado só a cada 2 segundos (tudo o que ocorrer entre esses instantes será ignorado).

#include "test.h"

#define DELAY 1000
const int ledPin = 13;
const int butPin = 11;
int counter = 0;
unsigned long lastTime;
int ledState = LOW;

void setup() {
    Serial.begin(115200);
    Serial.println("Arduino test");

    pinMode(ledPin, OUTPUT);
    pinMode(butPin, INPUT);

    lastTime = millis();
}

void loop() {
    if (millis()- lastTime > DELAY) {
        lastTime += DELAY;
        ledState = (ledState == LOW) ? HIGH : LOW;
        digitalWrite(ledPin, ledState);
    }

    if (digitalRead(butPin) == LOW) {
        counter++;
        Serial.println(counter);
    }
}

Isto implica a realização das temporizações necessárias ao piscar do LED de forma não “bloqueante” recorrendo não à função delay() mas sim à função millis(), que retorna um valor de 32 bits sem sinal (unsigned long) com o número de milisegundos que decorreram desde o reset da placa.

Contudo agora caímos no extremo oposto: o teste do estado do botão é feito a uma cadência tal que um simples toque no botão pode resultar em inúmeros incrementos do contador. Para resolver este problema só incrementaremos o contador caso se prima o botão tendo ele vindo do estado de repouso (not down).

#include "test.h"

#define DELAY 1000
const int ledPin = 13;
const int butPin = 11;
int counter = 0;
unsigned long lastTime;
int ledState = LOW;
bool down = false;

void setup() {
    Serial.begin(115200);
    Serial.println("Arduino test");

    pinMode(ledPin, OUTPUT);
    pinMode(butPin, INPUT);

    lastTime = millis();
}

void loop() {
    if (millis() - lastTime > DELAY) {
        lastTime += DELAY;
        ledState = (ledState == LOW) ? HIGH : LOW;
        digitalWrite(ledPin, ledState);
    }

    if (not down and digitalRead(butPin) == LOW) {
        down = true;
        counter++;
        Serial.println(counter);
    }
    if (down and digitalRead(butPin) == HIGH)
        down = false;
}

Olhando o código verifica-se que ele implementa a FSM da seguinte figura.

fsm1

Esta solução ainda não é perfeita já que devido ao “bouncing” do botão, quer na fase de pressão do botão, quer na de libertação, o estado lógico do pin de entrada pode sofrer várias transições.

Bibliotecas

O problema do “bouncing” será resolvido mais tarde, pelo que para já vamos só colocar este código numa classe que possa ser reaproveitada caso tenhamos mais do que um botão, i.e. vamos criar uma biblioteca. O nome da classe que dará o nome à biblioteca será: Button. Para isso começamos por criar o “header file“:

  • File > New > Other… > C/C++ > Header File
    Next
  • Header file: button.h
    Finish
#ifndef BUTTON_H_
#define BUTTON_H_

#include "Arduino.h"

class Button {
    public:
        Button(int butpin, int mode);
        void proc();
        bool clicked();
    private:
        int pin;
        int state;
        int flagHit;
};

#endif /* BUTTON_H_ */

Em seguida cria-se o ficheiro button.cpp:

  • File > New > Other… > C/C++ > Source File
    Next
  • Source file: button.cpp
    Finish

Aqui pode-se criar automaticamente os protótipos dos diversos métodos clicando com o botão direito sobre a área de escrita do ficheiro button.h e escolhendo:
Source > Implement Methods…

#include "button.h"

#define UP 0
#define DOWN 1

Button::Button(int butpin, int mode) {
    pin = butpin;
    pinMode(pin, mode);
    state = UP;
    flagHit = false;
}

void Button::proc() {
    switch (state) {
        case UP:
            if (digitalRead(pin) == LOW) {
                state = DOWN;
                flagHit = true;
            }
            break;
        case DOWN:
            if (digitalRead(pin) == HIGH)
                state = UP;
            break;
    }
}

bool Button::clicked() {
    if (flagHit) {
        flagHit = false;
        return true;
    }
    else
        return false;
}

Agora já é possível o nosso programa tirar partido desta classe para a implementação dos botões. No seguinte exemplo iremos usar o botão Bp (pino 12) para decrementar o contador e manter o botão Bi (pino 11) para decrementá-lo:

#include "test.h"
#include "button.h"

#define DELAY 1000
const int ledPin = 13;
unsigned long lastTime;
int ledState = LOW;

int counter = 0;
Button Bp(12, INPUT);
Button Bi(11, INPUT);

void setup() {
    Serial.begin(115200);
    Serial.println("Arduino test");
 
    pinMode(ledPin, OUTPUT);

    lastTime = millis();
}

void loop() {
    if (millis() - lastTime > DELAY) {
        lastTime += DELAY;
        ledState = (ledState == LOW) ? HIGH : LOW;
        digitalWrite(ledPin, ledState);
    }

    Bp.proc();
    Bi.proc();
    if (Bp.clicked()) {
        counter--;
        Serial.println(counter);
    }
    if (Bi.clicked()) {
        counter++;
        Serial.println(counter);
    }
}

Para se resolver agora o problema do “bouncing” dos botões vamos introduzir na FSM um outro estado:

fsm2

Daqui resultam as seguintes alterações no ficheiro button.h:

#ifndef BUTTON_H_
#define BUTTON_H_

#include "Arduino.h"

class Button {
    public:
        Button(int butpin, int mode);
        void proc();
        bool clicked();
    private:
        int pin;
        int state;
        int flagHit;
        unsigned long time;
};

#endif /* BUTTON_H_ */

E no ficheiro button.cpp:

#include "button.h"

#define UP 0
#define DOWN 1
#define WAIT 2

#define DELAY 50

Button::Button(int butpin, int mode) {
    pin = butpin;
    pinMode(pin, mode);
    state = UP;
    flagHit = false;
}

void Button::proc() {
    switch (state) {
        case UP:
            if (digitalRead(pin) == LOW) {
                state = WAIT;
                time = millis();
            }
            break;
        case WAIT:
            if (millis() - time > DELAY)
                if (digitalRead(pin) == LOW) {
                    state = DOWN;
                    flagHit = true;
                }
                else
                    state = UP;
            break;
        case DOWN:
            if (digitalRead(pin) == HIGH)
                state = UP;
            break;
    }
}

bool Button::clicked() {
    if (flagHit) {
        flagHit = false;
        return true;
    }
    else
        return false;
}

Resta agora colocar ambos os ficheiros dentro duma pasta Button e colocá-la na pasta que contém as bibliotecas privadas, por exemplo em C:\Alunos\EDM\libraries, e que deve ser defenida em: Window > Preferences > Arduino > Private Library path

Adicione igualmente a esta pasta, a biblioteca UARTserver que será usada para facilitar o processo de envio e principalmente receção de mensagens UART do PC. Para tal bastará descarregar o zip e descompactá-lo dentro da pasta das bibliotecas.

Uso duma bliblioteca

Doravante podemos incluir e utilizar estas bibliotecas desde que a importemos para o nosso projeto:

  • Import > Arduino > Import Arduino libraries into the current project
    Next
  • Button + UARTserver
    Finish

Para além das nossas bibliotecas Button e UARTserver, existe todo um conjunto de bibliotecas para utilização de diversas funcionalidades nomeadamente as comunicações série SPI e I2C (aqui designada de Wire).

No seguinte exemplo o programa test é complementado com a possibilidade de se enviar pelo PuTTY mensagens do tipo d<enter> ou i<enter> que decrementam ou incrementam o contador:

#include "test.h"
#include "button.h"
#include "uartserver.h"

#define DELAY 1000
const int ledPin = 13;
unsigned long lastTime;
int ledState = LOW;

int counter = 0;
Button Bp(12, INPUT);
Button Bi(11, INPUT);

UARTserver server;

void setup() {
    server.begin(115200);
    server.sendMsg("Arduino test");

    pinMode(ledPin, OUTPUT);

    lastTime = millis();
}

void loop() {
    if (millis() - lastTime > DELAY) {
        	lastTime += DELAY;
        ledState = (ledState == LOW) ? HIGH : LOW;
        digitalWrite(ledPin, ledState);
    }

    Bp.proc();
    Bi.proc();
    if (Bp.clicked()) {
        counter--;
        Serial.println(counter);
    }
    if (Bi.clicked()) {
        counter++;
        Serial.println(counter);
    }

    char *cmd;
    if ((cmd = server.getCmd()) != 0) {
        switch(cmd[0]) {
            case 'd':
                Serial.println(--counter);
                break;
            case 'i':
                Serial.println(++counter);
                break;
        }
    }
}

Por fim há que relembrar que cada um dos botões usados no circuito apresentam uma resistência de pull-up necessária para a definição do valor lógico na entrada quando o botão não está premido. Seria contudo possível prescindir dessa resistência dando a indicação ao microcontrolador para a colocar internamente. Nesse caso o modo a escolher seria não INPUT mas INPUT_PULLUP.


 

Analog

As placas Arduino para além de entradas digitais possuem também entradas analógicas (algo que não acontecia nas placas Raspberry Pi). É que dentro do microcontrolador ATmega328P existente nas Arduino existe um conversor analógico/digital (ADC – Analog to Digital Converter). Este componente transforma uma tensão na gama de 0V a 5V num valor numérico de 10 bits (de 0 a 1023).

Como a figura dos pinos das placas Arduino mostra, em alguns deles (A0 a A5) é possível aplicar e medir sinais analógicos. A função que executa essa conversão é a analogRead(ch) onde ch é o número do canal (0 a 5).

Para testarmos esta funcionalidade vamos usar o seguinte circuito onde o potenciómetro ligado nas suas extremidades às tensões de 0 e 5V, vai permitir obter tensões intermédias no seu pino central que vai ser ligado ao canal A5 da placa Arduino.

analog_pwm

Vamos agora criar um novo projeto Arduino chamado analog que começará por apresentar periodicamente o valor da conversão analógico/digital. Para tal o conteúdo do ficheiro analog.cpp seria o seguinte:

#include "analog.h"

void setup() {
    Serial.begin(115200);
}

void loop() {
    int n = analogRead(5);
    Serial.print(n);
    Serial.print(" - ");
    Serial.println(n*5.0/1024);

    delay(50);
}

Saídas analógicas

Embora o microcontrolador ATmega328P presente na placa Arduino não possua um DAC (Digital to Analog Converter) é possível tal como já foi feito nas placas Raspberry Pi, simular a geração de sinais analógicos recorrendo a sinais digitais PWM. Tratam-se de sinais digitais periódicos cujo valor médio pode ser alterado variando o seu duty-cycle.

Existe mesmo a função analogWrite(pin, v) que gera um sinal de 490Hz no pino pin cujo argumento v (de 0 a 255) permite controlar o seu duty-cycle de 0% (a que corresponde uma tensão média de 0V), até 100% correspondente a 5V de valor médio.

No seguinte exemplo a tensão medida no potenciómetro vai ser usada para controlar a intensidade luminosa dum LED a ligar ao pino 10. De notar que apenas é possível gerar sinais PWM em certos pinos da placa Arduino (marcados a vermelho na figura do pinout da placa) e identificados por um ~ junto ao seu número, o que impossibilita o uso do LED presente na placa (pino 13).

#include "analog.h"

void setup() {
    Serial.begin(115200);
}

void loop() {
    int n = analogRead(5);
    Serial.print(n);
    Serial.print(" - ");
    Serial.println(n*5.0/1024);

    analogWrite(10, n>>2);

    delay(50);
}

De notar que a gama de valores obtida pelo ADC (0..1023) pode ser facilmente convertida na gama 0..255 fazendo uma divisão por 4, ou como foi usado no código acima apresentado, um deslocamento de 2 bits para a direita (>>2).

Servomotores

Por fim vamos também usar sinais PWM para controlar a posição do eixo dum servomotor SG90. Essa posição está dependente da largura do impulso dum sinal de 50Hz (20ms de período) a aplicar a uma das suas entradas. Existe mesmo uma biblioteca (Servo) que pode ser usada para este efeito.

O seguinte código controla a posicão do eixo do servomotor com base na posição do potenciómetro, gerando o sinal PWM adequado no pino 11:

analog_servo

#include "analog.h"
#include "Servo.h"

Servo motor;

void setup() {
    Serial.begin(115200);
    motor.attach(11);
}

void loop() {
    int n = analogRead(5);
    Serial.print(n);
    Serial.print(" - ");
    Serial.println(n*5.0/1024);

    analogWrite(3, n>>2);

    int a = map(n, 0, 1023, 0, 180);
    motor.write(a);

    delay(50);
}

Neste exemplo foi usada a função map() que permite transformar valores compreendidos numa determinada gama, numa outra gama. Neste exemplo vamos transformar o resultado da conversão (0..1023) no ângulo (0º..180º) pretendido para o servomotor e que é o argumento da função write.

De notar que caso o LED fosse mantido no pino 10, a função analogWrite deixaria de funcionar. Isto é um alerta para o facto de certas funcionalidades por usarem os mesmos recursos poderem entrar em conflito. Uma forma fácil de resolver o problema seria ligar o LED num pino pertencente a outra porta (que não a usada pelo servo). Neste caso passaríamos do pino 10 (bit 2 da porta B) para o pino 3 (bit 3 da porta D).


Projetos propostos

Seguidamente serão propostos dois projetos a desenvolver pelos estudantes com base nos conhecimentos adquiridos ao longo desta página.

Projeto Semaforo

Pretende-se desenvolver um programa que controle um semáforo para automóveis situado junto a uma passadeira para peões semelhante ao já proposto quando do estudo da placa Raspberry Pi. Ou seja:

O ciclo normal de funcionamento será de 9 segundos em verde (LED Lg), 1 segundo em amarelo (Ly) e 5 segundos em vermelho (Lr).

Caso seja premido o botão dos peões (Bp) o tempo de verde deverá ser imediatamente terminado desde que já tenham decorrido 4 segundos. Se esse tempo ainda não decorreu a passagem para amarelo deverá ocorrer só após esses 4 segundos.

O botão Bi servirá para que na central de controlo do tráfego se inicie/termine o modo “intermitente” no semáforo. Neste modo o semáforo deverá ter o amarelo a piscar ciclicamente (1 segundo ligado, 1 segundo desligado).

Sugestão: Comece por implementar duas máquinas de estado (classes Normal e Blink) que implementem respetivamente a sequência dos LEDs num funcionamento normal e intermitente. Construa essas FSMs seguindo uma estrutura semelhante à usada no exemplo Button).

Tal como foi proposto aqui, implemente também a contagem decrescente 5, 4, 3, 2, 1, 0 (quando o LED vermelho está ligado) no display de 7 segmentos.

Projeto LCD

Neste projeto pretende-se usar o LCD Nokia 5110 para escrita de mensagens. Sugere-se o uso da biblioteca disponibilizada no GitHub para o efeito. Esta biblioteca permite usar o protocolo SPI implementado em hardware pelo microcontrolador presente na placa (pinos MOSI e SCK) ou o implementado em software (usando neste caso qualquer conjunto de pinos GPIO). Comece por analisar o código fonte do exemplo incluído!

As ligações no seguinte circuito pressupõem que tal como no exemplo foi usada a implementação por software, onde:

  • pino 7 – Serial clock out (SCLK)
  • pino 6 – Serial data out (DIN)
  • pino 5 – Data/Command select (D/C)
  • pino 4 – LCD chip select (CS)
  • pino 3 – LCD reset (RST)

É ainda de destacar o uso do circuito integrado CD4050 destinado a adaptar os níveis 0/5V da placa Arduino para os 0/3.3V requeridos pelo LCD.

LCDarduino_bb

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *