Arduino i semafory świetlne
Opis sterowania sygnalizacją świetlną na przykładzie semafora 5-komorowego.
Do projektu wykorzystane:
– Arduino Uno lub Arduino Nano
– MCP23017 – 16-kanałowy ekspander z linią I2C
Arduino Uno (jak też Nano) posiada tylko 8 pinów wyjściowych na porcie PD (piny z arduinowym oznaczeniem 0-7) oraz 6 pinów na porcie PB (piny 8-13) co jest o wiele za mało do wysterowania więcej niż dwóch semaforów 5-komorowych. Potrzebne będzie rozszerzenie portów, tzw. ekspander wyjść, który zapewni większą ilość „pinów” do obsługi większej ilości diod LED, podłączonych po jednej do każdego takiego wyjścia.
Taką rolę spełni układ MCP23017, który współpracując z Uno na linii I2C „zużyje” nam tylko 2 piny, pozostawiając resztę do innych zastosowań. Opis wyprowadzeń układu jest następujący:
Interesują nas piny 1-8 jako port B, 9 jako VCC, 10 jako GND, 12-13 jako linia I2C, 21-28 jako port A. Dodatkowo pinami 15-17 ustawiamy indywidualny adres układu, co oznacza, że możemy wykorzystać 8 układów na jednej linii I2C, co z kolei zapewni obsługę max. 8×16 = 128 diod LED (25 semaforów 5-komorowych).
Do własnego projektu użyłem „gołego” układu MCP23017, dorabiając do niego stosowną płytkę PCB z „przelotową” linią I2C i trzema wyprowadzeniami na semafory. Widzę teraz jednak, że pojawiają się w sieci układy w formie modułów współpracujących z Arduino, z gotowymi wyprowadzeniami. Odniosę się jednak do tego co sam zrobiłem, czyli gołego MCP23017.
Schemat połączeń przedstawia się jak niżej
Diody podzielone są na grupy A, B i C, będące osobnymi semaforami. Oczywiście zastosowanie semaforów 2-, 3- i 4- komorowych wymusi inny podział grup i pewne komplikacje w programie (ale o tem potem).
Na schemacie linia I2C wykorzystuje sprzętową obsługę transmisji. W moim projekcie wykorzystuję programową obsługę linii I2C, która wykorzystuje piny 2 i 3 Arduino.
Cała przygotowana płytka wygląda tak:
Mając tak przygotowany układ można przystąpić do programowania. Ponieważ z różnych względów zastosowałem programową obsługę magistrali I2C, do jej oprogramowania potrzebna mi była dodatkowa biblioteka SoftI2CMaster. Biblioteka jest do pobrania pod tym adresem.
A więc:
//software I2c dla obslugi MCP23017 - obsluga semaforow
#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
Definiuję podstawowe zmienne konfiguracyjne biblioteki SoftI2CMaster. Jak widać używam pinu 3 jako SCL i pinu 2 jako SDA. Można użyć dowolnych pinów Uno z zakresu 2-12, pamiętając o odpowiedniej konfiguracji w tym miejscu programu.
#include <SoftI2CMaster.h>
#include <SoftwareSerial.h>
SoftwareSerial BT(0, 1); // RX | TX
Dołączamy biblioteki. Biblioteka SoftwareSerial służy mi do komunikacji z komputerem po linii RS232. Tą drogą wysyłam do Arduino komendy w postaci kilku bajtów, na podstawie których Arduino poznaje który semafor ma być ustawiany, jaki stan ma być na nim wyświetlony, czy któraś z diod ma migać. Implementacja tej części programu – tj. na jakie sygnały zewnętrzne ma zareagować Arduino – pozostaje w gestii użytkownika. Mogą to być sygnały z przełączników, przekaźników, czujników, itp. czyli dowolny impuls lub seria impulsów, który wymusi odpowiednią reakcję wybranego semafora.
Definiujemy zmienne niezbędne do obsługi semaforów.
//zmienne obslugi MCP do semaforow (8szt)
//obsługa po programowym i2c (A2,A3)
byte sem_adr[] = {32,33,34,35,36,37,38,39}; //adresy kolejnych modulow
byte sem[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; //stany semaforów domyslnie wylaczone
byte sem_mig[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; //miganie semaforow domyslnie wylaczone
byte sem_mod[3]; //zmienna ustalająca miejsce bieżaco zapisywanego sem w sekwencji 16-bitowej
uint16_t trzy_sem=0; //zmienna pomocnicza 16bit do rozdzielenia 3x5bit sygnalu semafora na 16bit wartosc portu MCP23017
byte tmp;
int timer1_counter;
volatile byte migacz=0;
//zmienne obslugi Bluetooth
byte dana_in[9];
W programie wykorzystywanych jest max. ilość układów MCP23017, adresowanych za pomocą ich wyjść A0-A2 (piny 15, 16, 17) odpowiednio połączonych z masą lub „plusem”. Nie wdając się w szczegóły taka adresacja układów powoduje, że pierwszym adresem jest 32 i dalej kolejno adresy następnych układów na tej samej linii.
W standardowej funkcji setup() inicjuję SoftwareSerial i linię I2C:
void setup()
{
Serial.begin(57600);
//inicjalizacja Soft I2C dla MCP23017
if(!i2c_init())
{
//Serial.println("Sem: Brak modulu");
}
else
{
i2c_skan(); //skanowanie adresow semaforow
i2c_setup(); //inicjalizacja modulow semaforow
}
//ustawienie TIMER1 do pulsacji swiatel
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
timer1_counter = 34286; //miganie co ok 1 sekunde
TCNT1 = timer1_counter; //zaladowanie timera
TCCR1B |= (1 << CS12); //ustawienie preskalera na 256
TIMSK1 |= (1 << TOIE1); //ustawienie przerwania timer overflow
interrupts(); //wlaczenie prezerwan
}
Są tu widoczne 2 odwołania do funkcji i2c_skan() oraz i2c_setup(). Ponieważ są to funkcje użytkownika, poza główną pętlą programu loop(), opiszę je później – zachowując strukturę programu. Najpierw, przed pętlą główną, zdefiniujemy procedurę obsługi przerwania, które „automatycznie” co ok. 1 sekundę będzie nam migać wybraną diodą.
//przerwanie timer1 do migania diodami semafora
ISR(TIMER1_OVF_vect)
{
TCNT1 = timer1_counter; //przeladowanie timera
for(byte i=0;i<8;i++) //przelecenie wszystkich osmiu MCP
{
if(sem_adr[i]!=255) //jesli obecny uklad o adresie z sem_adr
{
tmp=3*i; //ustalenie nru semafora wsrod osmiu MCP
tmp=tmp+1;
if(migacz==0) //czy w cklu migania dioda ma gasnac czy sie zapalac
{
trzy_sem = (sem_mig[tmp+1] & 0x1f) << 10 | (sem_mig[tmp] & 0x1f) << 5 | (sem_mig[tmp-1] & 0x1f);
}
if(migacz==1)
{
trzy_sem = (sem[tmp+1] & 0x1f) << 10 | (sem[tmp] & 0x1f) << 5 | (sem[tmp-1] & 0x1f);
}
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x12); // GPIOA
i2c_write(lowByte(trzy_sem)); // port A
i2c_stop();
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x13); // GPIOB
i2c_write(highByte(trzy_sem)); // port B
i2c_stop();
}
} //koniec FOR
migacz++;
if(migacz==2) { migacz=0; }
}
No i przechodzimy do pętli głównej
void loop()
{
//odebrane 9 bajtow z odbiornika BT
if(Serial.available()>=9)
{
//sprawdzenie komendy odebranej z PC przez BT i przeslanie jej do zwrotnic albo do semaforow
//ustawienie semafora led na MCP
if((dana_in[0]>=20) && (dana_in[0]<=50))
{
tmp=dana_in[0]-20; //nr semafora 0-sem1, 1=sem2, 2=sem3 itd
sem[tmp]=dana_in[2]; //nowy stan semafora
sem_mig[tmp]=dana_in[2] xor dana_in[3]; //które diody mają migać
//okreslenie miejsca semafora w 3 sekcjach zmiennej 16 bitowej
//zeby jednym wzorem tmp zapisac do MCP stany 3 semaforów na raz
//ustalenie trzech semaforów sem_mod[2], sem_mod[1], sem_mod[0]
//i numeru (tmp) semafora zmienianego w tej trojce
switch (tmp % 3) {
case 0:
{ sem_mod[0]=tmp;sem_mod[1]=tmp+1;sem_mod[2]=tmp+2; }
break;
case 1:
{ sem_mod[0]=tmp-1;sem_mod[1]=tmp;sem_mod[2]=tmp+1; }
break;
case 2:
{ sem_mod[0]=tmp-2;sem_mod[1]=tmp-1;sem_mod[2]=tmp; }
break;
}
//sklejenie stanów 3 semaforów
trzy_sem = (sem[sem_mod[2]] & 0x1f) << 10 | (sem[sem_mod[1]] & 0x1f) << 5 | (sem[sem_mod[0]] & 0x1f);
i2c_start(sem_adr[dana_in[1]] <<1 | I2C_WRITE);
i2c_write(0x12); // GPIOA
i2c_write(lowByte(trzy_sem)); // port A
i2c_stop();
i2c_start(sem_adr[dana_in[1]] <<1 | I2C_WRITE);
i2c_write(0x13); // GPIOB
i2c_write(highByte(trzy_sem)); // port B
i2c_stop();
dana_in[0]=0;
}
}
}
Cała komplikacja w zapaleniu diody na semaforze 5-komorowym polega na tym, że MCP23017 posiada dwa porty 8-bitowe, czyli jakby 2 semafory 8-komorowe. A ja potrzebuję 5 pinów portu A wykorzystać dla semafora A, 3 piny portu A i 2 piny portu B dla semafora B i 5 pinów portu B na semafor C, pozostawiając niewykorzystany ostatni pin portu B. Wygląda to mniej więcej tak:
Dlatego przed zapisem do portu A (8-bitów, 1 bajt) trzeba ustalić dolną część zmiennej trzy_sem (16-bitowej, 2-bajtowej) a przed zapisem do portu B – górną część zmiennej trzy_sem. Taki prosty podział zapisze za jednym razem do portu A 5-bitów semafora A + 3 bity semafora B a do portu B – 2 bity semafora B i cały semafor C.
Zastosowanie semaforów 4-komorowych wymagało by innych, choć może ciut prostszych (z racji równego podziału na połówki bajtów) operacji „sklejania” stanu semaforów w zmienną trzy_sem by potem dwoma operacjami zapisu po I2C właściwie wypełnić porty A i B układu MCP23017. Mogłoby to wyglądać tak:
A dla semaforów 3-komorowych tak:
Rozkaz przychodzący z komputera składa się z bajtów określających:
1) że rozkaz dotyczy semaforów (centralka obsługuje również inne „urządzenia”) będący również nr semafora na którym zmienia się stan,
2) nr MCP23017, na którym znajduje się semafor
3) stan do ustawienia, kodowany bitowo, zgodnie z numeracją bitów w porcie układu MCP23017, czyli 0-wszystko wyłączone, 1-dolny bit, czyli dioda podpięta pod pin 0, 2-dioda na pinie 2, itp…
4) dioda która powinna migać, kodowane bitowo j.w.
Na koniec programu pozostało pokazać wymieniane wcześniej funkcje i2c_setup() i i2c_skan(). Funkcja i2c_setup() to po prostu „poinformowanie” układu MCP02317, że jego porty mają pracować jako wyjścia i wyzerowanie tych portów:
void i2c_setup()
{
//inicjalizacja modulow MCP - 8 modułów z adresami zapisanymi w sem_adr
for(byte i=0;i<8;i++)
{
//ustawienie portu A jako wyjścia
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x00);i2c_write(0x00);
i2c_stop();
//ustawienie portu B jako wyjścia
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x01);i2c_write(0x00);
i2c_stop();
//zerowanie portu A
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x12);i2c_write(0x00);
i2c_stop();
//zerowanie portu B
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x13);i2c_write(0x00);
i2c_stop();
}
}
Natomiast funkcja i2c_skan() sprawdza obecność podłączonych do linii I2C układów MCP23017. Tu można zaimplementować reakcje Uno na niewykrycie któregoś z układów, np. na skutek jakiejś przerwy na stykach łącza. Ja tego nie zrobiłem.
void i2c_skan()
{
for(byte i=0;i<8;i++)
{
if (i2c_start(sem_adr[i] <<1 | I2C_READ))
{
delay(50);
i2c_read(true);
delay(50);
i2c_stop();
}
else
{
i2c_stop();
//tu moze byc obsluga przypadku niewykrycia MCP
//brakujacy MCP oznaczam 255
sem_adr[i]=255;
}
}
}
I to by było wszystko w temacie programowania obsługi semaforów 😉
Pełny kod prezentowanego tu programu
//software I2c dla obslugi MCP23017 - obsluga semaforow
#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
#include <SoftI2CMaster.h>
#include <SoftwareSerial.h>
SoftwareSerial BT(0, 1); // RX | TX
//zmienne obslugi MCP do semaforow (8szt)
//obsługa po programowym i2c (A2,A3)
byte sem_adr[] = {32,33,34,35,36,37,38,39}; //adresy kolejnych modulow
byte sem[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; //stany semaforów domyslnie wylaczone
byte sem_mig[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; //miganie semaforow domyslnie wylaczone
byte sem_mod[3]; //zmienna ustalająca miejsce bieżaco zapisywanego sem w sekwencji 16-bitowej
uint16_t trzy_sem=0; //zmienna pomocnicza 16bit do rozdzielenia 3x5bit sygnalu semafora na 16bit wartosc portu MCP23017
byte tmp;
int timer1_counter;
volatile byte migacz=0;
//zmienne obslugi Bluetooth
byte dana_in[9];
void setup()
{
Serial.begin(57600);
//inicjalizacja Soft I2C dla MCP23017
if(!i2c_init())
{
Serial.println("Sem: Brak modulu");
}
else
{
i2c_skan(); //skanowanie adresow semaforow
i2c_setup(); //inicjalizacja modulow semaforow
}
//ustawienie TIMER1 do pulsacji swiatel
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
timer1_counter = 34286; //miganie co ok 1 sekunde
TCNT1 = timer1_counter; //zaladowanie timera
TCCR1B |= (1 << CS12); //ustawienie preskalera na 256
TIMSK1 |= (1 << TOIE1); //ustawienie przerwania timer overflow
interrupts(); //wlaczenie prezerwan
}
//przerwanie timer1 do migania diodami semafora
ISR(TIMER1_OVF_vect)
{
TCNT1 = timer1_counter; //przeladowanie timera
for(byte i=0;i<8;i++) //przelecenie wszystkich osmiu MCP
{
if(sem_adr[i]!=255) //jesli obecny uklad o adresie z sem_adr
{
tmp=3*i; //ustalenie nru semafora wsrod osmiu MCP
tmp=tmp+1;
if(migacz==0) //czy w cklu migania dioda ma gasnac czy sie zapalac
{
trzy_sem = (sem_mig[tmp+1] & 0x1f) << 10 | (sem_mig[tmp] & 0x1f) << 5 | (sem_mig[tmp-1] & 0x1f);
}
if(migacz==1)
{
trzy_sem = (sem[tmp+1] & 0x1f) << 10 | (sem[tmp] & 0x1f) << 5 | (sem[tmp-1] & 0x1f);
}
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x12); // GPIOA
i2c_write(lowByte(trzy_sem)); // port A
i2c_stop();
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x13); // GPIOB
i2c_write(highByte(trzy_sem)); // port B
i2c_stop();
}
} //koniec FOR
migacz++;
if(migacz==2) { migacz=0; }
}
void loop()
{
//odebrane 9 bajtow z odbiornika BT
if(Serial.available()>=9)
{
//sprawdzenie komendy odebranej z PC przez BT i przeslanie jej do zwrotnic albo do semaforow
//ustawienie semafora led na MCP
if((dana_in[0]>=20) && (dana_in[0]<=50))
{
tmp=dana_in[0]-20; //nr semafora 0-sem1, 1=sem2, 2=sem3 itd
sem[tmp]=dana_in[2]; //nowy stan semafora
sem_mig[tmp]=dana_in[2] xor dana_in[3]; //które diody mają migać
//okreslenie miejsca semafora w 3 sekcjach zmiennej 16 bitowej
//zeby jednym wzorem tmp zapisac do MCP stany 3 semaforów na raz
//ustalenie trzech semaforów sem_mod[2], sem_mod[1], sem_mod[0]
//i numeru (tmp) semafora zmienianego w tej trojce
switch (tmp % 3) {
case 0:
{ sem_mod[0]=tmp;sem_mod[1]=tmp+1;sem_mod[2]=tmp+2; }
break;
case 1:
{ sem_mod[0]=tmp-1;sem_mod[1]=tmp;sem_mod[2]=tmp+1; }
break;
case 2:
{ sem_mod[0]=tmp-2;sem_mod[1]=tmp-1;sem_mod[2]=tmp; }
break;
}
//sklejenie stanów 3 semaforów
trzy_sem = (sem[sem_mod[2]] & 0x1f) << 10 | (sem[sem_mod[1]] & 0x1f) << 5 | (sem[sem_mod[0]] & 0x1f);
i2c_start(sem_adr[dana_in[1]] <<1 | I2C_WRITE);
i2c_write(0x12); // GPIOA
i2c_write(lowByte(trzy_sem)); // port A
i2c_stop();
i2c_start(sem_adr[dana_in[1]] <<1 | I2C_WRITE);
i2c_write(0x13); // GPIOB
i2c_write(highByte(trzy_sem)); // port B
i2c_stop();
dana_in[0]=0;
}
}
}
void i2c_setup()
{
//inicjalizacja modulow MCP - 8 modułów z adresami zapisanymi w sem_adr
for(byte i=0;i<8;i++)
{
//ustawienie portu A jako wyjścia
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x00);i2c_write(0x00);
i2c_stop();
//ustawienie portu B jako wyjścia
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x01);i2c_write(0x00);
i2c_stop();
//zerowanie portu A
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x12);i2c_write(0x00);
i2c_stop();
//zerowanie portu B
i2c_start(sem_adr[i] <<1 | I2C_WRITE);
i2c_write(0x13);i2c_write(0x00);
i2c_stop();
}
}
void i2c_skan()
{
for(byte i=0;i<8;i++)
{
if (i2c_start(sem_adr[i] <<1 | I2C_READ))
{
delay(50);
i2c_read(true);
delay(50);
i2c_stop();
}
else
{
i2c_stop();
//tu moze byc obsluga przypadku niewykrycia MCP
//brakujacy MCP oznaczam 255
sem_adr[i]=255;
}
}
}