Attiny2313 jako mini klawiatura

Na rynku istnieje sporo gotowych interfejsów wejściowych. Różnego rodzaju klawiatury numeryczne można kupić za kilkanaście złotych. Bardziej wybredni mogą skusić się na klawiaturę pojemnościową z interfejsem I2C. Jeśli ktoś woli formę Joysticka, to przykład Joystick Shield będzie doskonałym rozwiązaniem. Ostatnio pojawiła się także miniaturowa klawiaturka, którą łatwiej można wpasować do budowanego projektu, ma też interfejs I2C, co dodatkowo przemawia na jej korzyść. Są to doskonałe rozwiązania, ale co zrobić, jeśli chcemy ustawić guziki w jakiś nietypowy, „fikuśny” kształt? Na przykład dookoła wyświetlacza. Albo obudowa projektu, który konstruujemy wyklucza użycie gotowej klawiatury. Najprostszym rozwiązaniem jest użycie pinów cyfrowych Arduino, do których podłączamy guziki. Czasem jednak chcemy, żeby było ich zdecydowanie więcej, niż oferuje nam zwykłe UNO R3. Niekoniecznie też chcemy przesiadać się na płytkę MEGA. W takim wypadku Malutki Attiny2313 przychodzi na ratunek!

Attiny2313 to potężny pomocnik swoich starszych braci Atmega. We wcześniejszych artykułach można było przeczytać, jak używać Attiny2313 jako urządzenia wyjścia sterowanego przez I2C. Głównie do sterowania diodami bez konieczności angażowania nadmiernej ilości tak pinów, jak i kodu z naszego Arduino. Dziś skupimy się na przerobieniu Attiny2313 na cyfrowe urządzenie wejściowe.

Z tego wpisu powinniśmy wiedzieć, że używanie rejestrów pinów zamiast funkcji typu pinMode, czy digitalWrite jest o wiele korzystniejsze z punktu widzenia objętości skompilowanego kodu i tym samym szybkości jego wykonywania, tak więc w przykładach będę się posługiwał tylko nimi.

Jak podłączyć Attiny do Arduino, programować je i używać Arduino IDE dowiecie się tutaj.

Głównym celem tej lekcji jest podłączenie pewnej ilości guzików do Attiny i poprzez komunikację I2C zawiadamianie Arduino, który/które guziki są wciśnięte, a które nie.

Zacznijmy jednak od najprostszego możliwego przykładu bez komunikacji:

void setup (){
  DDRD= B00000000; // Ustaw piny grupy D jako wejścia
  PORTD = B11111111; // Podciągnij rezystory pod piny D
  DDRB = B11111111; // Ustaw piny B jako wyjścia
}

void loop (){
  // sprawdź dla każdego z pinów:
  for (byte i = 0; i < 7; i++){
  // Jeśli PIND jest w stanie wysokim, to włącz
  // Odpowiadający mu pin B 
  if (PIND & _BV(i)) PORTB |= _BV(i);
  // Jeśli w niskim, to wyłącz
  else PORTB &= ~(_BV(i));
  }
}

Kod jest dość krótki i bez większych pułapek. Jego działanie polega na tym, że zaświecamy diodę na pinach grupy B odpowiadającą guzikowi podpiętemu do pinów grupy D. Rejestr PIND przechowuje w sobie stan, w jakim aktualnie znajduje się dany pin. Zainteresować powinien fakt, że po ustawieniu kierunku pinów portu D na wyjście (DDRD = B00000000 lub pinMode(#, INPUT) ) to zapisuję dalej PORTD = B011111111 co do tej pory wykorzystywaliśmy tylko z opcją OUTPUT i zdaje się nie mieć sensu (bo jak niby port w stanie odbierania ma nadawać stan HIGH?). Otóż ma! Gdy DDR danego pinu jest ustawione jako input, a jednocześnie rejestr PORT danego pinu jest ustawiony jako HIGH ( 1 ), to mikrokontroler podłącza do nóżki tego pinu wewnętrzny rezystor ok. 5kΩ. To eliminuje konieczność włączenia do budowanego układu odrębnych rezystorów dla każdego z guzików, a zatem upraszcza konstrukcję!

Na rysunku widać, że z Arduino czerpiemy tylko zasilanie. Cała reszta odbywa się wewnątrz Attiny.

Kolejny krok to dodanie możliwości komunikowania się pomiędzy Attiny2313 a naszym Arduino. Podstawy komunikacji między urządzeniami były już omówione w tym poście. Poniższy przykład zawiera dwie nowe funkcje: onRequest (po stronie Attiny) oraz requestFrom (dla Arduino).

Najpierw kod do załadowania w Attiny:

#include "TinyWireS.h"
#define I2C_SLAVE_ADDR  0x26
void setup (){
  DDRD= B00000000;
  PORTD = B11111111;
  DDRB = B11111111;
  TinyWireS.begin(I2C_SLAVE_ADDR);
  // Poniżej nowa funkcja. onRequest. Gdy Arduino
  // wyśle prośbę o dane, to zostanie wywołana funkcja
  // o nazwie znajdującej się jako parametr w nawiasach.
  TinyWireS.onRequest(sendState);
}
void loop (){
}

// Tutaj widać prostą funkcję wysyłającą 
void sendState(){
  TinyWireS.send(PIND);
}

Teraz kod który musimy umieścić w Arduino

#include <Wire.h>
void setup()
{
  Wire.begin();
  Serial.begin(9600);  
}
void loop()
{
  // Wysyłamy do urządzenia o numerze 0x26 prośbę o przesłanie
  // jednego bajta. Format tej funkcji wygląda następująco:
  // Wire.requestFrom (numer_urządzenia , długość)
  Wire.requestFrom(0x26, 1); 
  while(Wire.available())
  { 
    byte c = Wire.read();
    // Dodatkowy parametr w funkcji println konwertuje bajt
    // c w taki sposób, aby był wyświetlony w postaci bitowej.
    Serial.println(c, BIN);
  }
  delay(500);
}

Mechanizm działania tych programów polega na tym, że Arduino co pół sekundy wysyła zapytanie do Attiny za pomocą komendy requestFrom(), a następnie jeśli otrzyma odpowiedź, to zapisuje ją w zmiennej c  i wyświetla ją w formie binarnej w Monitorze Portu Szeregowego wbudowanego w Arduino IDE (skrót ctrl + shift + M) Jedynki świadczą o braku wciśnięcia guzika, zera świadczą o guziku wciśniętym. Łatwo można zrozumieć całą ideę po skonstruowaniu projektu jak na poniższym rysunku:

Ok. Wszystko pięknie wygląda, ale uważni czytelnicy bloga pewnie już wiedzą czego brakuje w tym programie… debouncing!! Został już doskonale opisany i muszę przyznać, że sam uczyłem się z tego artykułu, więc przedstawię tylko kod zaadaptowanej wersji. Warto zauważyć, że przy takiej konstrukcji programu jak powyżej debouncing zdaje się nie być potrzebny. To wszystko dzięki dużym odstępom czasowym pomiędzy poszczególnymi sprawdzeniami stanów, jednak jeśli mamy miejsce w pamięci, to zawsze lepiej go zastosować, niż nie!:

#include "TinyWireS.h"
#define I2C_SLAVE_ADDR  0x26
byte temp = 0;
byte stanD = B00000000;
byte tempD = B00000000;
unsigned long czas;
unsigned long czas2;

void setup (){
  DDRD= B00000000;
  PORTD = B11111111;
  DDRB = B11111111;
  TinyWireS.begin(I2C_SLAVE_ADDR);
  TinyWireS.onRequest(sendState);
  czas2 = millis();
}

void loop (){
  for (byte i = 0; i < 7; i++){
    // Poniższy if prezentuje debounce. Jeśli stan
    // któregoś pinu się zmienił, to zapisujemy
    // tymczasowy stan i aktualny czas
    if (PIND != stanD && tempD == 0){
       tempD = PIND ^ stanD;
       // dzięki XOR stworzymy bitmaskę, która będzie
       // pokazywała różnice między PIND a stanD
       czas = millis() + 30;
    }
    // jeśli mamy maskę do sprawdzenia i upłynął
    // czas debouncingu, to weryfikujemy zmianę
    else if (tempD && (czas < millis())){
      // sprawdzamy, czy zmiany stanu zarejestrowane
      // 30 ms temu, wciąż są aktualne. Jeśli tak,
      // to zapisujemy nowe wartości
      if (PIND == stanD ^ tempD){
        stanD = PIND;
      }
    }
  }
}

void sendState(){
  TinyWireS.send(stanD);
}

Poza tym ciekawostką godną uwagi jest wykorzystanie operacji XOR w powyższym przykładzie do stworzenia bitmaski pinów, które aktualnie zmieniły stan.

Oczywiście wciskanie guzików i obserwowanie rzędów zer i jedynek na monitorze nie jest szczytem możliwości naszych konstrukcji, dlatego żeby poruszyć dodatkowo wyobraźnię dorzucę do całego układu wyświetlacz, który wymontowałem jakiś czas temu z niepotrzebnego mi już projektu. Kod w Attiny pozostaje bez zmian, natomiast do Arduino ładujemy:

#include <Adafruit_GFX.h>
#include <Adafruit_HX8340B.h>
#include <Wire.h>

#define TFT_MOSI  11		// SDI
#define TFT_CLK   13		// SCL
#define TFT_CS     2		// CS
#define TFT_RESET  9		// RESET
#define	BLACK           0x0000
#define	RED             0xF800
#define	GREEN           0x07E0
#include <SPI.h>
byte message = 0;
Adafruit_HX8340B display(TFT_RESET, TFT_CS);
void setup(void) {
    display.begin();
    Wire.begin();
    display.fillScreen(BLACK);
    for (byte i = 10; i < 140; i = i + 20){
      display.drawRect( i, 15, 10, 10, GREEN);
    }
}

void loop (){

  Wire.requestFrom(0x26, 1);
  if(Wire.available()){
    message = Wire.read();
  for (byte i = 0; i < 7; i++){
      if (message & _BV(i) ){
        display.drawRect ((i*20)+10, 15, 10, 10, RED);
      }
      else{
        display.drawRect ((i*20)+10, 15, 10, 10, GREEN);  
      }
  }
  }
}

Jeszcze schemat podłączenia całości:

Na ekranie zostaną wyrysowane kolorowe kwadraty. W zależności od wciśniętego guzika ich kolor będzie się zmieniał.

W tym miejscu mocno trzeba się zastanowić nad sposobem połączenia wszystkich elementów. Na rysunkach nie uwzględniałem kabli odpowiadających za programowanie Attiny i szczerze mówiąc do tej pory nie było różnicy, czy są podłączone podczas testowania konstrukcji, czy nie. Teraz jednak należy je odpiąć i podłączyć je tylko do wyświetlacza. Inaczej projekt nie zadziała. Wynika to prawdopodobnie z faktu, że Attiny2313 ma na tych samych pinach I2C i SPI.

Do przećwiczenia warto spróbować dodać do obsługi piny rejestrów B (pamiętajcie jednak, że nie wolno używać całego rejestru B, gdyż dwa jego piny są odpowiedzialne za komunikację!!)

Części potrzebne do wykonania tego projektu to:

 

Ten artykuł jest zwieńczeniem czteroczęściowego opisu możliwości Attiny jeśli chodzi o mechanizmy cyfrowego wejścia/wyjścia. W kolejności to:

  1. Attiny2313 i Arduino IDE
  2. Jak dogadać się z Attiny
  3. Optymalizacja kodu
  4. Attiny jako mini klawiatura

Nie jest to jednak koniec możliwości samego Attiny2313.

Po przestudiowaniu tych czterech opracowań powinniście wiedzieć:

  • Jak programować Attiny,
  • Stworzyć komunikację poprzez I2C między Attiny a Arduino
  • Używać pinów Attiny jako wejścia/wyjścia
  • Zastąpić komendy DigitalWrite za pomocą rejestrów
  • używać podstawowych operacji bitowych

Jeśli tak nie jest, to piszcie co jest niejasne. Poprawię, zmienię, wytłumaczę.