Arduino i pulpit sterowniczy cz. 1
Poniżej opiszę krok po kroku w jaki sposób robię pulpit sterowniczy w oparciu o Arduino Uno.
Arduino posiada niewiele użytecznych wejść, które można wykorzystać jako wejścia sygnałów od przycisków. Do obsługi kilku przycisków wystarczy, ale przy większej ilości pojawia się problem. Ponadto, chcąc dodać do projektu coś więcej niż przyciski np. wyświetlacz, moduł komunikacyjny bluetooth, buzzer, czy diody sygnalizacyjne, wolnych wejść pozostaje nam naprawdę niewiele.
Aby temu zaradzić, niezbędny okazuje się tzw. expander portów. Ja zastosowałem 16 bitowy expander oznaczony jako MCP23017. Daje on możliwość wykorzystania 16 wejść/wyjść kosztem tylko dwóch pinów Arduino, wykorzystywanych do komunikacji z układem po magistrali i2c oraz dwóch linii zasilania.
Opis pinów układu MCP23017 wygląda tak:
Interesują nas następujące piny:
GPA0 – GPA7 – 8 bitów portu A
GPB0 – GPB7 – 8 bitów portu B
VDD – zasilanie 5V pociągnięte z Arduino
VSS – masa, również „wyciągnięta” z Arduino
SCK, SDA – linie magistrali i2c, podłączone do Arduino, o czym niżej
A0, A1, A2 – trzy wyjścia adresu układu, które muszą być podpięte do +5V zasilania albo do masy.
Adresowanie układu pozwala na zastosowanie większej liczby układów na jednej linii (na dwóch kablach), przy czym, żeby wiadomo było z którym z nich „gada” Arduino, posiadają one swoje unikatowe adresy.
Przy zastosowaniu jednego układu adresowanie nie ma aż tak istotnego znaczenia, jednak jest konieczne, żeby układ był „widziany” przez Arduino.
Możemy ustawić dowolny adres, zwierając wszystkie piny A0-A2 do masy. Wtedy 3 mniej znaczące bity 8-bitowego adresu (3 bity z prawej) będą ustawione na 000. Fabrycznie adres wygląda tak:
Czyli zwierając A0-A2 do masy, otrzymujemy adresy naszego modułu:
0100|000|0 – czyli 64 dziesiętnie – adres układu do zapisu np. komend
0100|000|1 – czyli 65 dziesiętnie – adres układu do odczytu danych.
Zwierając do masy pin A1-A2 a pin A0 do +5V, otrzymalibyśmy adresy
0100|001|0 – czyli 66 – dla zapisu do układu
0100|001|1 – czyli 67 – dla odczytu z układu….. itd…itd..
Układ MCP23017 posiada 2 porty wejścia/wyjścia, niezależnie konfigurowalne w układzie, które można wykorzystać niezależnie. Mogą one posłużyć do obsługi matrycy klawiszy o wielkości 8×8 przycisków, wg poniższego rysunku
Mamy matrycę (przykładową, nieco okrojoną) 2×3 – dwa wiersze A0 i A1 oraz 3 kolumny B0, B1, B2.
Idea działania jest taka: program wystawia stan wysoki (jedynkę) na kolejne piny portu A (kolejne wiersze matrycy) i natychmiast odczytuje stan portu B (stan jednocześnie na wszystkich kolumnach B0-B2). W chwili naciśnięcia przycisku, wiedząc który wiersz aktualnie badamy (wartość wiersza) otrzymujemy na porcie B liczbę odpowiadającą (binarnie) kolumnie, w której został naciśnięty przycisk.
I to cała tajemnica 😀
Przejdźmy do pisania programu.
Będzie do tego potrzebna biblioteka SoftI2CMaster dostępna pod tym adresem: https://github.com/felias-fogg/SoftI2CMaster
Można oczywiście skorzystać ze sprzętowego i2c i pinów SCK, SDA Arduino, jednak ja wybrałem software’ową obsługę i2c, która pozwala dowolnie zdefiniować, które piny Arduino będą wykorzystane do komunikacji z MCP23017, pozostawiają przy tym sprzętowe i2c np. do obsługi wyświetlacza.
Na początku definiujemy zmienne wykorzystywane przez bibliotekę
//software I2c dla obslugi MCP23017
#define SCL_PIN 3
#define SCL_PORT PORTC
#define SDA_PIN 2
#define SDA_PORT PORTC
#define I2C_FASTMODE 1
#define I2C_TIMEOUT 1000
#define I2C_PULLUP 1
#define ADDRLEN 1 // dlugosc adresu 1 bajt
#define MCP 0x23 //adres układu MCP23017
#include <SoftI2CMaster.h> //uzywane przez MCP23017
Jak widać na i2c wybrałem piny A2 i A3 Arduino. Adres układu MCP mam ustawiony na 0x23 (szestnastkowo), 35 dziesiętnie, czyli 100|011|x, co oznacza, że wyjścia adresowe MCP23017 A0 i A1 mam podpięte do +5V a wyjście A2 podpięte do masy.
byte dana[7] = {0,0,0,0,0,0,0};
byte stan[8] = {1,2,4,8,16,32,64,128};
byte row, col;
Definiuję zmienne wykorzystywane w programie. Wynikowymi zmiennymi określającymi położenie naciśniętego przycisku będą: row i col
void setup() {
//start MCP23017
i2c_start(MCP <<1 | I2C_READ);
delay(50);
i2c_read(true);
delay(50);
i2c_stop();
//ustawienia portów MCP23017
//ustawienie portu A jako OUTPUT
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x00);i2c_write(0);
i2c_stop();
//zerowanie portu A
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x12);i2c_write(0);
i2c_stop();
//ustawienie portu B jako INPUT
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x01);i2c_write(255);
i2c_stop();
//podciąganie portu B
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x0D);i2c_write(255);
i2c_stop();
//odwracanie logiki portu B
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x03);i2c_write(255);
i2c_stop();
}
Inicjuję komunikację i2c z układem i ustawiam jego parametry.
PortA – ustawiony jako wyjście z wyzerowanymi wartościami na wyjściu
PortB – ustawiony jako wejście, w wewnętrznym podciąganiem pinów do plusa i odwróconą logiką prezentacji wyniku na wejściu (normalnie pojawienie się stanu wysokiego na pinie B0 dawałoby wartość 11111110. Po zmianie logiki wartość ta wyniesie 00000001, czyli jeden).
for (byte i=0; i<=7; i++) //pętla dla wyjść A0-A7 na ktorych kolejno ustawiamy stan 1
{
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x12);
i2c_write(stan[i]); //ustawiamy wysokie linie A0-A7 i sprawdzamy odpowiednie linie B0-B7
i2c_stop();
i2c_start(MCP <<1 | I2C_WRITE); //odczytujemy PortB
i2c_write(0x13);
i2c_stop();
i2c_start(MCP <<1 | I2C_READ);
dana[i] = i2c_read(1);
i2c_stop();
}
W głównej pętli programu mam wykonującą się 8 razy sup-pętlę, która wystawia na kolejne wyjścia portuA wartość 1 i od razu odczytuje stan portuB a wynik zapisuję do tablicy dana[].
Tablica dana[] bez wciśnięcia przycisku wypełniona jest zerami. W momencie naciśnięcia przycisku powinien się w niej pojawić stan portuB na pozycji aktualnie ustawianego jako 1 pinu portuA. Ponieważ odczytuję na raz cały portB, pojawi się liczba z zakresu 1-255.
Najpierw sprawdzam, gdzie wyskoczyła ta liczba.
row=0;
for (byte i=0; i<=7; i++)
{
if (dana[i]!=0) { row=dana[i]; };
}
Potem sprawdzam jaka liczba się pojawiła i zamieniam ją na liczbę z zakresu 1-7, czyli oznaczającą kolumnę
col=0;
for (byte i=0; i<=6; i++)
{
if (dana[i]==0 and row!=0) { col=i+1; }
if (bitRead(row,i)==1) { row=i+1; }
}
Przed zakończeniem pętli głównej mam do dyspozycji dwie zmienne: row, col, określające który przycisk został naciśnięty.
Dalsza zabawa to zaprogramowanie odpowiedniej „akcji” jaką powinno podjąć Arduino w zależności od wybranego przycisku.
Ponieważ w moim systemie sterowania TO Arduino będzie się komunikowało poprzez BT z centralą właściwą pod makietą i mam przyjęty odpowiedni protokół przesyłu danych, dalsza część to przypisanie odpowiednim przyciskom kilku bajtów danych i wysłanie ich do centrali właściwej, która zareaguje przestawieniem odpowiedniego rozjazdu, zapaleniem świateł, opuszczeniem rogatek, itp.
Na koniec cały kod programu w jednym kawałku
//software I2c dla obslugi MCP23017
#define SCL_PIN 3
#define SCL_PORT PORTC
#define SDA_PIN 2
#define SDA_PORT PORTC
#define I2C_FASTMODE 1
#define I2C_TIMEOUT 1000
#define I2C_PULLUP 1
#define ADDRLEN 1 // dlugosc adresu 1 bajt
#define MCP 0x23 //adres układu MCP23017
#include <SoftI2CMaster.h> //uzywane przez MCP23017
byte dana[7] = {0,0,0,0,0,0,0};
byte stan[8] = {1,2,4,8,16,32,64,128};
byte row, col;
void setup() {
//start MCP23017
i2c_start(MCP <<1 | I2C_READ);
delay(50);
i2c_read(true);
delay(50);
i2c_stop();
//ustawienia portów MCP23017
//ustawienie portu A jako OUTPUT
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x00);i2c_write(0);
i2c_stop();
//zerowanie portu A
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x12);i2c_write(0);
i2c_stop();
//ustawienie portu B jako INPUT
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x01);i2c_write(255);
i2c_stop();
//podciąganie portu B
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x0D);i2c_write(255);
i2c_stop();
//odwracanie logiki portu B
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x03);i2c_write(255);
i2c_stop();
}
void loop()
{
for (byte i=0; i<=7; i++) //pętla dla wyjść A0-A7 na ktorych kolejno ustawiamy stan 1
{
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x12);
i2c_write(stan[i]); //ustawiamy wysokie linie A0-A7 i sprawdzamy odpowiednie linie B0-B7
i2c_stop();
i2c_start(MCP <<1 | I2C_WRITE);
i2c_write(0x13);
i2c_stop();
i2c_start(MCP <<1 | I2C_READ);
dana[i] = i2c_read(1);
i2c_stop();
}
row=0;
for (byte i=0; i<=7; i++)
{
if (dana[i]!=0) { row=dana[i]; };
}
col=0;
for (byte i=0; i<=6; i++)
{
if (dana[i]==0 and row!=0) { col=i+1; }
if (bitRead(row,i)==1) { row=i+1; }
}
//tu mamy row i col do dalszej obsługi
}