RGB modul, o katerem smo pisali v predhodnih dveh nadaljevanjih, sprejema podatke od I2C masterja, vendar mu ne pošilja nikakršne povratne informacije. I2C knjižnice za ATtiny mikrokontrolerje, ki smo takrat predstavili, pa omogočajo dvosmerno komunikacijo; v tem članku bomo predstavili enostaven I2C slave in pripadajoče programe, ki bodo znali masterju odgovoriti na poslano vprašanje.
Avtorja: Vladimir Mitrović in Robert Sedak
E-pošta: vmitrovic12@gmail.com
ATtiny I2C slave – dvosmerna komunikacija
Shema I2C read-write slave modula je prikazana je na sliki 12. Ker ne gre za vezje, ki bi imelo neko koristno praktično uporabo, nismo razvijali ustreznega modula, pač pa smo ga realizirali na univerzalni eksperimentalni plošči. Čeprav je enostavno, nam bo vezje omogočilo, da nazorno ilustriramo tehniko programiranja dvosmerne komunikacije po I2C protokolu.
Ta vsebina je samo za naročnike
Definicija programske naloge
Master pošilja sporočilo naslednje strukture:
- START
- write naslov RGB slave mikrokontrolerja (izbrali smo naslov &B11110000)
- številka v razponu 0-255
Ko po START signalu RW slave prepozna lasten 7-bitni naslov “1111000”, v katerem je osmi naslovni bit “0”, bo sprejel naslednji bajt in izračunal dve vrednosti:
številka_1 = številka * 2
številka_2 = 255 – številka
Naslov in sprejeti bajt slave je potrebno potrditi tako, da pošljemo ACK signal. Kadar se na vodilu pojavi naslov nekega drugega čipa, ga RGB slave ne bo potrdil z ACKom in ne bo sprejemal nobenih podatkov dokler ne bo ponovno adresiran.
Master pošilja novo sporočilo naslednje strukture:
- START
- read naslov RGB slave mikrokontrolerja (izbrali smo naslov &B11110001)
Ko po START signalu RW slave prepozna lastni 7-bitni naslov “1111000”, v katerem je osmi adresni bit “1”, bo prebral stanja stikal, ki so vezana na priključke porta B in bo poslal masterju naslednje tri podatke:
- številka_1
- številka_2
- PINB
Master mora prva dva sprejeta bajta potrditi s pošiljanjem ACK signala, po zadnjem pa bo poslal NACK in končal komunikacijo:
- STOP
Bascom-AVR rešitev
Program ATtiny85_RW_slave.bas
Najprej bomo definirati I2C naslov,
Const I2c_slave_address = &B11110000
številka bajta, ki jo slave pričakuje od masterja (1) in številka bajta, ki ga bo poslal masterju (3)
Const I2c_bytes_to_receive = 1
Const I2c_bytes_to_send = 3
in končno, priključke, katere bo uporabljal za komunikacijo:
I2c_port Alias Portb
I2c_pin Alias Pinb
Const Scl = 2 ‘PB.2 = SCL
Const Sda = 0 ‘PB.0 = SDA
Izbrali smo komunikacijske priključke, ki pri mikrokontrolerjih iz serije ATtiny25/45/85 hkrati ustrezajo SCL in SDA vhodom USI vezja, zato bomo v program vključili knjižnico, ki za I2C komunikacijo uporablja USI vezje:
$include Attiny_i2cslave_usi.sub
Knjižnica bo definirala naslednje spremenljivke:
Dim I2c_address As Byte
Dim I2c_rcv_byte(i2c_bytes_to_receive) As Byte
Dim I2c_snd_byte(i2c_bytes_to_send) As Byte
Dolžina nizov je določena z vrednostmi konstant I2c_bytes_to_receive in I2c_bytes_to_send in v našem primeru bo imel enega oziroma tri bajte. Sledi inicializacija komunikacijskih pinov, Watchdog tajmerja in USI vezja, nakar komunikacijski program vstopi v glavno zanko v kateri spremlja promet na I2C vodilu in kliče podprograme iz uporabniškega programa, kot smo to opisali v prvem nadaljevanju (članek “ATtiny I2C slave brez težav in odvečnih stroškov”, Svet elektronike številka 303). Vse to je vsebovano v sami knjižnici in je “nevidno” za programerja, od katerega se pričakuje samo to, da napiše svoje tri podprograme.
Prvi podprogram je Init_slave; v njemu bomo samo definirati vhodne priključke mikrokontrolerja, na katere so vezane tipke S1, S2 in S3:
Init_slave:
‘SW1 = PB1
‘SW2 = PB3
‘SW2 = PB4
Config Portb.1 = Input
Portb.1 = 1
Config Portb.3 = Input
Portb.3 = 1
Config Portb.4 = Input
Portb.4 = 1
Return
Drugi podprogram, ki ga moramo predvideti, je Execute_i2c_command. Ko RW slave prepozna svoj write naslov, bo od masterja prejel še en bajt, ga vpisal v niz I2c_rcv_byte(1) in nato poklical navedeni podprogram. V njemu bomo izvršili računske operacije definirane s programsko nalogo in rezultate pospravili v prva dva člena niza I2c_send_byte():
Execute_i2c_command:
I2c_snd_byte(1) = I2c_rcv_byte(1) * 2
I2c_snd_byte(2) = 255 – I2c_rcv_byte(1)
Return
Tretji podprogram, ki ga moramo predvideti, je Prepare_data_to_be_sent. Komunikacijski program ga bo poklical, ko prepozna svoj read naslov. V njemu pa bomo pripravili podatke, ki jih je treba poslati masterju. Dva podatka smo že pripravili, preostalo je samo branje stanja tipk in jih pospraviti v I2c_send_byte(3):
Prepare_data_to_be_sent:
I2c_snd_byte(3) = Pinb
Return
Tukaj boste opazili, da smo za pošiljanje pripravili stanja vhodnih priključkov celega porta B in ne samo tistih, na katere so vezane tipke. To smo naredili namenoma, ker bi izločanje stanja interesantnih priključkov in njihovo zlaganje skupaj “enega poleg drugega” zahtevalo nekaj programskih ukazov, kar bi nepotrebno upočasnilo izvrševanje podprograma. Tako smo zagotovili, da slave hitro odgovori na zahtevo masterja, masterju smo pa prepustili, da izloči iz poslane informacije tisto, kar mu je zanimivo.
Poglejmo sedaj, kako je enaka programska naloga rešena v Arduino IDE!
Arduino IDE rešitev
Program ATtiny85_RW_slave.ino
Na začetku programa definiramo uporabo knjižnice Wire:
#include <Wire.h>
Definiramo I2C naslov ‘0b1111000’ za ATTiny slave, spremenljivko, v katero bomo shranili vrednost poslano od masterja in polja, v katerem bomo shranili vrednosti preden jih pošljemo masterju:
byte I2C_addr = 0b1111000;
volatile byte i2c_rcv_byte = 0;
volatile byte i2c_snd_byte[3] = {0, 0, 0};
V funkciji setup() inicijaliziramo I2C protokol in dodelimo I2C naslov, zapisan v spremenljivki I2C_addr, definiramo, da se bo izvršila funkcija receiveEvent(), ko knjižnica Wire zazna, da I2C master pošilja podatke in definiramo, da se bo izvršila funkcija requestEvent(), ko knjižnica Wire zazna, da I2C master pošilja zahtevo za sprejemanje podatkov:
void setup() {
Wire.begin(I2C_addr); // join i2c bus with address
Wire.onReceive(receiveEvent); // register event
Wire.onRequest(requestEvent);
Še bomo definirali vhodne priključke mikrokontrolerja, na katereso spojene tipke S1, S2 in S3:
pinMode(PB1, INPUT_PULLUP);
pinMode(PB3, INPUT_PULLUP);
pinMode(PB4, INPUT_PULLUP);
} // end setup()
V funkciji loop() ni potrebno imeti niti enega ukaza:
void loop() {
} // end loop()
V funkciji receiveEvent() prevzemamo podatek z I2C vodila, ga shranimo v spremenljivko i2c_rcv_byte, izvršimo računske operacije po programski nalogi in shranimo njihov rezultat v polje i2c_snd_byte :
void receiveEvent(int howMany) {
i2c_rcv_byte = Wire.read();
i2c_snd_byte[0] = i2c_rcv_byte * 2;
i2c_snd_byte[1] = 255 – i2c_rcv_byte;
} // end receiveEvent()
V funkciji requestEvent() pošiljamo na I2C vodilo pripravljene podatke iz polja i2c_snd_bytein tudi stanja vhodnih priključkov celega porta B:
void requestEvent() {
Wire.write(i2c_snd_byte[0]);
Wire.write(i2c_snd_byte[1]);
Wire.write(PINB);
} // end requestEvent()
Arduino I2C master
Kot I2C master smo uporabili razvojni sistem Shield-A postavljen na Arduino UNO. “Master” program je pisan za takšno okolje, katerega shema je prikazana na sliki 5 v preteklem nadaljevanju. Oznake posameznih komponent izvirajo s Shielda-A, modro pobarvane oznake ustrezajo oznakam z Arduino Uno ploščice. Enako dobro bo služil kateri koli podobni razvojni sistem ali vezje z nekim ATmega mikrokontrolerjem, z ustreznimi prilagoditvami programa.
Slika 13 kaže kako povezujemo Shield-A z RW modulom, izvedenim na eksperimentalni ploščici. Na sliki so simbolično prikazane komponente Shielda-A, ki jih uporabljamo v master programu: tipki SW1 in SW2 in potenciometer RV1. S potenciometrom RV1 nastavljamo vrednost številke, ki jo bomo poslali RW slave-u. S tipko SW1 pošiljamo celotno komunikacijsko sekvenco: adresiramo slave, mu pošiljamo nastavljeno vrednost, ga ponovno adresiramo v read načinu in od njega sprejemamo podatke, ki nam jih pošilja. S tipko SW2 pošiljamo samo drugi del komunikacijske sekvence: adresiramo slave v read načinu in od njega sprejmemo podatke, ki nam jih pošilja. Če tipko SW1 ali SW2 držimo pritisnjeno, bo program ponavljal pridruženo sekvenco vsakih 50 ms.
Da bi povečali promet na I2C vodilu, smo se odločili, da za prikaz podatkov uporabimo I2C LCD namesto “lastnega” LCD-ja postavljenega na razvojni sistem Shield-A. Prikaz na displeju lahko vidite na fotografiji testnega sistema na sliki 14: v gornji vrstici displeja se izpisuje tekst “ADC B_1 B_2 B_3”, v drugi vrstici pa podatki. Pod napisom ADC se izpisuje vrednost, ki jo daje A/D pretvornik ko meri napetost na drsniku potenciometra; z vrtenjem osi dobivamo vrednosti v razponu 0-255. Pod ostalimi napisi se izpisujejo vrednosti treh bajtov, ki jih je master prebral iz slave-a.
Bascom-AVR rešitev
Program Shield-A_RW_master.bas
Program uporablja naslednje spremenljivke:
Adc_value vsebuje trenutno odčitek A/D pretvornika
I2c_byte(3) vsebuje tri bajte koje master sprejema od slave-a
Na začetku programa definiramo I2C naslov slave mikrokontrolerja, kateremu bo master pošiljal sporočila:
Const I2c_address = &B11110000
Potrebno je definirati samo write naslov, read naslov bo program izračunal sam, ko ga bo potreboval.
Da bi za I2C komunikacijo lahko uporabili TWI sklop, bomo vključili I2C_TWI.lbx knjižnico, in nato definirali hitrost I2C komunikacije ter SCL in SDA priključke:
$lib “i2c_twi.lbx”
Config Twi = 100000 ‘SCL = 100kHz
Config Sda = Portc.4
Config Scl = Portc.5
I2cinit
Sledi konfiguracija I2C LCD-ja:
Config Lcd = 16 * 2
$include “I2CLCD.sub”
Lcd$init &B1111
Konstanta &B1111 v Lcd$init ukazu ustreza I2C LCD modulu s PCF8574A čipom in vsi trije naslovni priključki v stanju “1” (slika 13 zgoraj desno). Knjižnica I2CLCD.sub je funkcionalno identična knjižnici PCF8574_LCD$SE.sub, o kateri smo podrobno pisali v Svetu elektronike št. 262, v okviru Barduino serije.
A/D pretvornik bomo konfigurirali tako da kot referenčno napetost uporablja napetost napajanja:
Config Adc = Single , Prescaler =
Auto , Reference = Avcc
Zaradi tega se bo polni razpon A/D pretvorbe (0-1023) pokril z vrtenjem osi potenciometra RV1 od ene do druge skrajne pozicije. Še bomo na običajni način konfigurirali vhodne priključke PC1 in PC2, nato vstopimo v glavno Do-Loop zanko. V njej vsakih 50 ms preverjamo stanja tipk SW1 in SW2 in izvršujemo podprogram Disp_adc:
Do
Debounce Pinc.1 , 0 , Sw1_sub , Sub
Debounce Pinc.2 , 0 , Sw2_sub , Sub
Gosub Disp_adc
Waitms 50
Loop
V podprogramu Disp_adc merimo napetost drsnika potenciometra RV1, delimo izmerjeno vrednost s 4 (tako smo dobili razpon vrednosti 0-255) in jo prikazujemo na LCD-ju:
Disp_adc:
Adc_value = Getadc(0)
Adc_value = Adc_value / 4
Locate 2 , 1
Str_value = Str(adc_value)
Str_value = Format(str_value , “000”)
Lcd Str_value
Return
Podprogram Sw1_sub se izvršuje vsakič ko pritisnemo tipko SW1. V njemu najprej definiramo read naslov slave-a, preberemo trenutno stanje A/D pretvornika in ga prikažemo na LCD-ju
Sw1_sub:
Const I2c_r_address = &B11110000 Or &B00000001
Do
Gosub Disp_adc
nato pa opravimo komunikacijsko sekvenco po programski nalogi
Err = 0
I2cstart
I2cwbyte I2c_address
I2cwbyte Adc_value
‘ Waitms 1
I2cstart
I2cwbyte I2c_r_address
‘ Waitms 1
I2crbyte I2c_byte(1) , Ack
I2crbyte I2c_byte(2) , Ack
I2crbyte I2c_byte(3) , Nack
I2cstop
Waitms ukazi so potrebni samo, če slave potrebuje več od 2 µs za obdelavo naslova oziroma podatkov, ki mu jih je master poslal. Program našega RW modula je dovolj hiter in so Waitms ukazi zakomentirani. In končno bomo prikazali na LCD-ju sprejete podatke in ponavljali to proceduro dokler se ne spusti tipke SW1:
Gosub Disp_i2c_bytes
Waitms 50
Loop Until Pinc.1 = 1
Return
Podprogram za prikaz na displeju, Disp_i2c_bytes, najprej preverja stanje sistemskega bita Err, da bi izpisal ustrezno sporočilo, če je prišlo do napake v komunikaciji:
Disp_i2c_bytes:
Locate 2 , 5
If Err = 1 Then
Lcd “Err ”
Else
…
Endif
Return
Nato podprogram oblikuje in izpisuje vrednosti treh bajtov prebranih iz I2C slave-a; ta del programa tukaj ne bomo analizirali.
Podprogram Sw2_sub se izvršuje vsakič ko pritisnemo tipko SW2. Je identičen podprogramu Sw1_sub, razen tega, da je komunikacijska sekvenca skrajšana in je v njej ostal samo del, v katerem master od slave-a zahteva, da mu pošlje tri bajte:
Sw2_sub:
Do
Gosub Disp_adc
Err = 0
I2cstart
I2cwbyte I2c_r_address
‘ Waitms 1
I2crbyte I2c_byte(1) , Ack
I2crbyte I2c_byte(2) , Ack
I2crbyte I2c_byte(3) , Nack
I2cstop
Gosub Disp_i2c_bytes
Waitms 50
Loop Until Pinc.2 = 1
Return
Arduino IDE rešitev
Program Shield-A_RW_master.ino
Na začetku programa definiramo knjižnice, ki jih bomo uporabili in deklariramo funkcijo debounce(), ki jo bomo kasneje tudi definirali.
#include <Wire.h>
#include <LiquidCrystal.h>
void debounce(byte, byte, void (*)());
Definiramo objekt s pomočjo katerega krmilimo LCD, definiramo potrebne spremenljivke in polja:
LiquidCrystal_I2C lcd(0x27,16,2);
const int I2c_address = 0b1111000;
byte err = 0;
byte adc_value;
byte i2c_byte[3] = {0, 0, 0};
V funkciji setup() bomo definirati vhodne priključke, zagnali I2C komunikacijo, inicijalizirali LCD in vključili osvetlitev ozadja ter izpisali prvo vrstico LCD displeja:
void setup() {
pinMode(A1, INPUT_PULLUP);
pinMode(A2, INPUT_PULLUP);
Wire.begin();
lcd.init();
lcd.backlight();
lcd.print(“ADC B_1 B_2 B_3 “);
} // end setup()
V funkciji disp_i2c_bytes() postavimo kurzor LCD-ja na peto pozicijo v drugi vrstici in te preverjamo uspešnost komunikacije. Če se je zgodila napaka v komunikaciji izpišemo ustrezno sporočilo, če ne, izpišemo prejete podatke:
void disp_i2c_bytes(){
lcd.setCursor(4,1);
if ( err == 1){
lcd.print(“Err “);
} else {
if (i2c_byte[0] < 100) lcd.print(“0”);
if (i2c_byte[0] < 10) lcd.print(“0”);
lcd.print(i2c_byte[0]);
lcd.setCursor(8,1);
if (i2c_byte[1] < 100) lcd.print(“0”);
if (i2c_byte[1] < 10) lcd.print(“0”);
lcd.print(i2c_byte[1]);
lcd.setCursor(12,1);
if ( bitRead(i2c_byte[2], 4) == 0 ){
lcd.print(“1”);
} else {
lcd.print(“0”);
}
if ( bitRead(i2c_byte[2], 3) == 0 ){
lcd.print(“1”);
} else {
lcd.print(“0”);
}
if ( bitRead(i2c_byte[2], 1) == 0 ){
lcd.print(“1”);
} else {
lcd.print(“0”);
}
}
} // end disp_rgb()
V funkciji disp_adc() merimo napetost drsnika potenciometra RV1, delimo izmerjeno vrednost s 4 (tako smo dobili razpon vrednosti 0-255), in jo prikažemo je na LCD-ju:
void disp_adc(){
adc_value = analogRead(A0)/4;
lcd.setCursor(12,1);
if (adc_value < 100) lcd.print(“0”);
if (adc_value < 10) lcd.print(“0”);
lcd.print(adc_value);
} // end disp_adc()
Funkcija sw1_function() se izvršuje ko pritisnemo tipko SW1. Dokler je tipka pritisnjena, vsakih 50 ms ponavljamo naslednjo proceduro:
- beremo napetost drsnika potenciometra,
- izpisujemo prebrano vrednost na LCD-ju,
- adresiramo slave in mu pošljemo prebrano vrednost preko I2C vodila,
- od slave-a zahtevamo, da nam pošlje podatke,
- podatke sprejmemo in jih shranimo v niz i2c_byte.
Če se dogodi da je številka sprejetih bajtova različna od tri, postavljamo spremenljivko err v stanje ena. Sprejete podatke izpisujemo na LCD s pomočjo funkcije disp_i2c_bytes():
void sw1_function(){
do {
disp_adc();
err = 0;
Wire.beginTransmission(I2c_address);
Wire.write(adc_value);
Wire.endTransmission();
Wire.requestFrom(I2c_address, 3);
byte index = 0;
while(Wire.available()) {
i2c_byte[index] = Wire.read();
index++;
}
if (index != 3) err = 1;
disp_i2c_bytes();
delay(50);
} while (digitalRead(A1) == 0);
} // end sw1_function()
Funkcija sw2_function() se izvršuje, ko pritisnemo tipko SW2. Dokler je tipka pritisnjena, vsakih 50 ms ponavljamo prej opisano proceduro, razen da sedaj ne pošiljamo slave-u prebrane vrednosti potenciometra. Če se zgodi da je številka prejetih bajtov je različna od tri, postavimo spremenljivko err v stanje ena. Sprejete podatke izpisujemo na LCD s pomočjo funkcije disp_i2c_bytes():
void sw2_function(){
i2c_byte[0] = 0;
i2c_byte[1] = 0;
i2c_byte[2] = 0;
do {
disp_adc();
err = 0;
Wire.requestFrom(I2c_address, 3);
byte index = 0;
while(Wire.available()) {
i2c_byte[index] = Wire.read();
index++;
}
if (index != 3) err = 1;
disp_i2c_bytes();
delay(50);
} while (digitalRead(A2) == 0);
} // end sw2_function()
***
Poglejmo še enkrat prikaz na displeju na sliki 14, kot potrdilo, da program dela, kot je zamišljeno! Vrednost “120” je master poslal slave-u, v odgovoru je dobil “240” (= 120 * 2), “135” (= 255 – 120) in “xxx10x0x” kot branje vhodnih priključkov porta B. V branju so z “x” označeni nam nezanimivi priključki, numerične vrednosti pripadajo priključkom na katere so vezana tipke S3-S1. V prikazu so te numerične vrednosti “zložene” ena poleg druge in komplementirane, tako da “0” označuje odprto in “1” sklenjeno stikalo. Namesto treh posameznih tipk v testnem vezju je uporabljeno stikalo za binarno kodiranje (binary coded rotary switch), ki je v trenutku, ko je posneta fotografija, bilo postavljen v poziciji “3”.
Opomba: Programe ATtiny85_RW_slave.bas, ATtiny85_RW_slave.ino, Shield-A_RW_master.bas, Shield-A_RW_master.ino in knjižnico I2CLCD.sub lahko dobite brezplačno v uredništvu revije Svet elektronike!
ATtiny85_RGB_slave_2