Avtorja: Vladimir Mitrović in Robert Sedak
E-pošta: vmitrovic12@gmail.com
2022-303-01
Med realizacijo enega od projektov se je pojavila potreba po I2C komunikaciji med “glavnim” in “pomožnim” mikrokontrolerjem, pri čemer je je prvi upravljal s celotnim procesom, medtem, ko je drugi moral delati neko specifično nalogo.
Programski jezik Bascom-AVR ima integrirane I2C komunikacijske rutine za master program, vendar se knjižnica z I2C rutinami za slave program posebej prodaja.
Ta vsebina je samo za naročnike
Vprašal sem se, ali lahko napišem uporabljiv slave program za mikrokontroler iz ATtiny serije brez uporabe komercialnih rutin, in tako se je vse začelo… Kolega Robert se mi je pridružil v tem raziskovanju na Arduino platformi; tukaj je situacija drugačna, ker obstaja nekaj že narejenih slave knjižnic, vendar je tudi te potrebno znati pravilno uporabiti. Po izčrpnem raziskovanju sva dosegla zelo uporabne rezultate, ki bi lahko biti koristni tudi drugim Bascomašem in Arduinašem; zato sva se odločila, da jih objaviva v reviji Svet elektronike.
Kot I2C master sva uporabila razvojni sistem Shield-A postavljen na Arduino UNO, tako da so te članki svojevrstno nadaljevanje naše Shield-A serije člankov. Za preverjanje I2C slave programa sva razvila modul z ATtiny85 mikrokontrolerjem in RGB diodo, katere shema je prikazana na sliki 1. Namen modula je da, ko od master-ja dobi ustrezen ukaz, samostojno zadrži dano kombinacijo barv. Predvideni sta dve izvedbi: za RGB diode s skupno katodo (na sliki 1 zgoraj) in za RGB diode s skupno anodo (na sliki 1 spodaj).
Vrednosti uporov Rr, Rg in Rb je potrebno izbrati glede na namen modula in karakteristik RGB diode. Da preverimo funkcionalnost vezja kadar nam niso potrebne visoke intenzitete, bodo ustrezali upori med 1,5 in 3 kΩ. Če je dioda manj “občutljiva” oziroma kadar so potrebne večje intenzitete, je potrebno vrednosti upora zmanjšati. Tukaj moramo upoštevati tudi možnosti mikrokontrolerja; da ne bi preobremenjevali njegovih izhodnih vrat, vrednosti upora manjše od 150 Ω niso priporočljive.
Osnovni predpogoj za kvalitetno mešanje barv je to, da so intenzitete vseh treh barv enake. To ni nujno tako, če so vrednosti uporov Rr, Rg in Rb enake, ker se v tem primeru lahko dogodi to, da je intenziteta ene barve občutno večja od intenzitete ostalih barv. Če npr. dominira svetlost zelene barve, je potrebno vrednost upora Rg primerno povečati; poskusite z dvojno ali celo trojno vrednostjo. Opomba: pri nekaterih vrstah RGB diod so medsebojno “zamenjani” priključki rdeče in zelene barve – to ne vpliva na funkcionalnost modula, ker se to lahko programsko popravi.
Mikrokontroler kontrolira intenziteto vsake posamezne barve s postopkom širinske modulacije impulza (PWM), katere frekvenca znaša okoli 500 Hz. Možno je izbrati izmed 256 različnih širin impulza, kar bo omogočilo 256 različnih intenzitet vsake posamezne barve. Tukaj smo našteli tudi dva skrajna primera, v katerih pravzaprav ni impulza, pač pa je izhod mikrokontrolerja stalno v stanju logične ničle (= 0 V) ali logične enice (= 5 V). Če uporabimo RGB diodo s skupno katodo, bodo širši impulzi povzročili večjo intenziteto posamezne barve, medtem ko bo pri RGB diodi s skupno anodo intenziteta večja, čim ožji bodo impulzi.
Na shemah na sliki 1 vidimo, da sta upora rdečega in modrega segmenta RGB diode vezana na priključke PB1 in PB4, ki sta znotraj mikrokontrolerja lahko povezana z OC1A in OC1B izhodoma Timera1. Zato lahko intenziteto teh dveh barv krmilimo na hardverskem nivoju: ko ga konfiguriramo na ustrezen način, bo Timer1 generiral impulze dane širine brez kakršne programske aktivnosti. Intenziteto zelene barve bomo krmilili z impulzi generiranimi v prekinitveni rutini povezani z delom Timera0, ker niti enega od njegovih OC0 izhodov ni možno povezati s priključkom PB3.
PB0/SDA in PB2/SCL priključke vežemo na I2C vodilo in preko nje naš slave modul sprejema ukaze od I2C master-ja. Te priključke smo ciljno izbrali, ker jih je mogoče interno povezati s SDA in SCL priključkoma univerzalnega serijskega komunikacijskega vezja mikrokontrolerja (Universal Serial Interface, USI). Ko ga konfiguriramo na ustrezen način, USI vezje “naredi” ključne aktivnosti I2C protokola, s čemer poenostavljamo program in hkrati ustvarimo predpogoje za hitrejšo in bolj zanesljivo komunikacijo.
Tukaj moramo opomniti, da je I2C komunikacijo možno ustvariti tudi samo na programskem nivoju, brez uporabe USI vezja. V tem primeru linije SCL in SDA lahko vežemo na katera koli V/I priključka mikrokontrolerja, kot je prikazano na primeru na sliki 2. Prednost takšne rešitve v naši konkretni uporabi je v tem, ker smo “osvobodili” PB0 priključek, ki ga je znotraj mikrokontrolerja možno povezati z OC0A izhodom Timerja0. Tako smo dosegli da intenziteto vseh treh barv lahko krmilimo na hardverskem nivoju, brez kakšne programske aktivnosti. Če pa želimo I2C komunikacijo realizirati samo softversko, postane pisanje programa precej bolj kritično: komunikacijske rutine morajo biti hitre in natančne tudi zaradi tega, ker jih moramo pisati v asemblerju. V nadaljevanju bomo analizirali obe programski rešitvi; kaj bo v določenem primeru bolj koristno, je odvisno od namena I2C modulov, ki jih projektirate.
Fotografije na sliki 3 prikazujejo tri izvedbe RGB modulov: modul iz “hišnih ročnih del” (zgoraj), verzijo, ki uporablja USI vezje (v sredini) in verzijo, ki ne uporablja USI vezja (spodaj). Na obeh “profi” ploščicah so predvideni tudi kratkospojniki za izbor vrste RGB diode (JP1 za diode s skupno anodo, JP2 za diode s skupno katodo). Na modulih je poleg Attiny85 mikrokontrolerja možno uporabiti tudi njegove “mlajše brate”, ATtiny45 ali ATtiny25.
Definicija programske naloge
Master pošlje sporočilo z naslednjo strukturo:
- START
- naslov RGB slave mikrokontrolerja (izbrali smo naslov &B11110000)
- intenziteta rdeče barve (0-255)
- intenziteta zelene barve (0-255)
- intenziteta modre barve (0-255)
- STOP
Ko po START signalu RGB slave prepozna lasten 7-bitni naslov “1111000” (osmi naslovni bit 0 je pravzaprav sporočilo slave mikrokontrolerju, da mu bo master poslal še nekaj podatkov), bo prevzel naslednje tri bajte in jih uporabil za nastavljanje intenzitete rdeče, zelene in modre svetlobe RGB diode. Naslov in vsak sprejeti bajt mora slave potrditi tako, da pošlje ACK signal. Ko se na vodilu pojavi naslov nekega drugega čipa, ga RGB slave ne bo potrdil z ACK signalom in ne bo sprejel nobenih podatkov, dokler ne bo ponovno naslovljen.
Bascom-AVR rešitev
Ker smo želeli uporabnika osvoboditi pisanja časovno kritičnih rutin, ki spremljajo promet na I2C vodilu in mu tako znatno poenostavili programiranje, smo pripravili tri knjižnice:
- ATtiny_I2Cslave_USI.sub (uporablja USI, ključne rutine so pisane v asemblerju);
- ATtiny_I2Cslave_USI_bascom.sub (uporablja USI, vse rutine so pisane v programskom jeziku Bascom-AVR);
- ATtiny_I2Cslave_noUSI.sub (ne uporablja USI, ključne rutine so pisane v asemblerju).
Prva knjižnica, ATtiny_I2Cslave_USI.sub, je najhitrejša in podpira I2C komunikacijo pri standardni frekvenci SCL takta 100 kHz, v primeru da mikrokontroler dela na 8 MHz ali višji frekvenci. Uporabili jo bomo takrat, ko lahko za I2C komunikacijo uporabimo USI, oziroma SCL in SDA priključke mikrokontrolerja, kot prikazano v shemah s slike 1.
Druga od njih, ATtiny_I2Cslave_USI_bascom.sub, je replika prve, s to razliko, da so vse rutine pisane v Bascomu. To je do neke mere upočasnilo izvrševanje programa, zato je za hitrost I2C komunikacije 100 kHz nujno povečati delovno frekvenco mikrokontrolerja na 16 MHz. To knjižnico smo napisali zato, da bi jo lahko analizirali v članku, ker ne zahteva poznavanje asemblerja. Čeprav je enako funkcionalna kot tudi prva, je ne priporočamo v praksi, ker je počasnejša.
Tretjo knjižnico, ATtiny_I2Cslave_noUSI.sub, bomo uporabljali takrat, ko ne moremo uporabljati USI ali če je nekateri od SCL ali SDA priključkov mikrokontrolerja potreben za nek drugi namen, kot v shemah na sliki 2. Kadar ne uporabljamo USI, je vse situacije, ki se lahko dogodijo v komunikacijskem protokolu, potrebno pokriti programsko, s čemer program postaje bolj kompliciran in se dalj časa izvaja. Za hitrost I2C komunikacije 100 kHz je nujno povečati delovno frekvenco mikrokontrolerja na 16 MHz.
Opomba: tovarniško je za pogon mikrokontrolerja iz družine ATtiny25/45/85 izbran interni RC oscilator frekvence 8 MHz. Slika 4 prikazuje kako lahko mikrokontroler “prisilimo”, da dela dvakrat hitreje, tj. na 16 MHz – namesto standardnih vrednosti CKSEL bitov, “100010: Int. RC Osc. 8 MHz”, je potrebno izbrati postavko “100001: PLL Clock” in jo vpisati jo v mikrokontroler s klikom na “Write FS” tipko. Ta postavka ostane shranjena v Fuse bitih mikrokontrolerja toliko časa, dokler je na isti način ne spremenimo.
Striktno gledano, navedene knjižnice niso knjižnice v pravem pomenu, ker ne vsebujejo novih ukazov ali funkcij: v njih se nahaja glavna programska zanka, ki spremlja komunikacijo na I2C vodilu, prepoznava START in STOP signale, preverja sprejet I2C naslov in, če ustreza lastnemu I2C naslovu, sprejme podatke, ki jih pošilja master in jih potrjuje z ACK signali oziroma, če master to zahteva, mu pošlje enega ali več podatkov. V nadaljevanju ga bomo imenovali “komunikacijski program”. Tehnično gledano, gre za popolno drugačen pristop od tistega, ki se uporablja v komercialni Bascom I2C slave knjižnici in ekvivalentnim Arduino knjižnicah, pri katerih se promet na I2C vodilu spremlja preko prekinitve, ki zažene USI, Timer0 ali Int0 pin.
Na začetku uporabniškega programa je potrebno definirati, kateri priključki se uporabljajo za I2C komunikacijo in I2C slave naslov, ter koliko bajtov I2C slave pričakuje od master-ja in koliko bajtov mu je treba poslati. Nato uporabnik prebere eno od I2C slave knjižnic v svoj program, in ni nam potrebno več skrbeti o sami I2C komunikaciji: komunikacijski program iz knjižnice bo naredil celoten posel in tudi resetiral bo mikrokontroler s pomočjo Watchdog timer-ja v primeru, če nekaj v komunikaciji ni bilo u redu. Knjižnica tudi definira dva niza byte spremenljivk: I2c_rcv_byte() in I2c_snd_byte(). V prvi niz komunikacijski program shranjuje podatke, ki jih je master poslal slave čipu, v drugi program je potrebno vpisati podatke, ki jih bo slave poslal master-ju.
Neodvisno od tega, katera knjižnica je prebrana, se bo komunikacija z uporabniškim programom odvijala na identičen način, preko treh podprogramov, ki jih uporabnik mora vključiti v svoj program. Te podprogrami se morajo imenovati Init_slave, Prepare_data_to_be_sent in Execute_i2c_command.
Komunikacijski program izvršuje podprogram Init_slave samo enkrat, prede prične spremljati komunikacijo na I2C vodilu. V njemu programer definira spremenljivke, ki jih želi uporabljati, konfigurira I/O priključke, inicijalizira prekinitve in vezja mikrokontrolerja (npr, timerje), torej vse to, kar bi sicer napravil v uvodnem delu uporabniškega programa.
Podprogram Prepare_data_to_be_sent se izvršuje vedno kadar komunikacijski program sprejme od master-ja ukaz za branje (zadnji bit naslova = “1”). Tukaj mora programer v niz I2c_snd_byte() vpisati podatke, ki jih želi poslati master-ju, komunikacijski program pa jih bo poslal master-ju takoj, ko se mu vrne kontrola.
Podprogram Execute_i2c_command se izvršuje vedno kadar komunikacijski program sprejme od master-ja ukaz za pisanje (zadnji bit naslova = “0”) in pričakovano število podatkov. Komunikacijski program vpisuje sprejete podatke v niz I2c_rcv_byte(); program jih mora analizirati in izvršiti ustrezne aktivnosti, nakar vrača kontrolo glavnemu programu.
Vsi trije podprogram morajo biti definirani tudi kadar I2C slave ni predviden za neko aktivnost (npr., naš RGB modul nikdar ne bo poslal neko sporočilo master-ju, vendar v programu je vseeno treba predvideti “prazen” Prepare_data_to_be_sent podprogram). Neodvisno od tega, če uporabljamo knjižnico, ki je pisana v asemblerju ali v Bascomu, v podprogramih pišemo “običajne” Bascom ukaze ker trajanje podprograma ne vpliva na hitrost komunikacije. Vendar pa mora master dati dovolj časa slave-u, da izvrši poslani ukaz, kar bomo ilustrirati kasneje.
Program ATtiny85_RGB_slave_1.bas
Poglejmo sedaj, kako so opisani postopki uporabljeni v programu, napisanem za RGB modul po shemi s slike 1! Najprej bomo definirati I2C naslov,
Const I2c_slave_address = &B11110000
število bajtov, ki jih slave pričakuje od master-ja (3) in število bajtov, ki jih bo poslal master-ju (0)
Const I2c_bytes_to_receive = 3
Const I2c_bytes_to_send = 0
in končno, priključke, ki jih bo uporabljal za komunikacijo:
I2c_port Alias Portb
I2c_pin Alias Pinb
Const Scl = 2 ‘PB.2 = SCL
Const Sda = 0 ‘PB.0 = SDA
Izbrani komunikacijski priključki ustrezajo shemi na sliki 1, pri mikrokontrolerju iz serije ATtiny25/45/85 sočasno ustrezajo SCL in SDA vhodom USI vezja. Zato bomo v program vključiti 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
Dolžina niza je določena z vrednostjo konstante I2c_bytes_to_receive in v našem primeru bo imela tri bajte. Sledi inicijalizacija komunikacijskih priključkov, Watchdog tajmerja in USI vezja. Nato komunikacijski program vstopi v glavno zanko, v kateri spremlja promet na I2C vodilu in kliče podprograme iz uporabniškega programa na prej opisani način. Vse to je vključeno v sami knjižnici in je “nevidno” za programerja, od kogar se pričakuje, da samo napiše svoje tri podprograme.
Prvi podprogram je Init_slave; v njemu bomo najprej konfigurirali izhodne priključke mikrokontrolerja, da bi lahko z njimi krmilili rdeči, zeleni in modri segment RGB diode:
Init_slave:
‘R = OCR1A (PB1)
‘G = OCR0A, OCR0B (PB3, int)
‘B = OCR1B (PB4)
Config Portb.1 = Output
Config Portb.3 = Output
Config Portb.4 = Output
in nato postavili začetne vrednosti OCR registra obeh timerjev:
Ocr0a = 0
Ocr0b = 255
Ocr1a = 0
Ocr1b = 0
Ocr1c = 255
Impulze za krmiljenje z intenziteto rdeče in modre barve generiramo s pomočjo Timera1 v PWM modu. Tajmer1 konfiguriramo tako, da krmili impulze frekvence 125 kHz (= 8 MHz / 64) in ko “preide” preko vrednosti vpisane v OCR1C register, postavlja izhoda OC1A in OC1B v stanje “0” in prične šteti od začetka. Pri OCR1C = 255 se bo to zgodilo po vsakem 256 impulzu, to je približno 488-krat v sekundi. Kadar se vrednost števca izenači z vrednostmi v OCR1A in OCR1B registrih, bo Timer1 postavil izhoda OC1A in OC1B v stanje “1”. Timer1 je konfiguriran z direktnim vpisom ustreznih vrednosti v njegove konfiguracijske registre, ker nismo uspeli najti ustrezne formulacije za Bascom Config Timer1 ukaze:
Tccr1 = &B01110111
Gtccr = &B01110000
Z impulzi za nastavljanje intenzitete zelene barve krmilimo s pomočjo Timera0 in njemu pridruženih prekinitvenih rutin Tim0_compa_sub in Tim0_compb_sub. Prekinitvene rutine bomo pridružiti Timeru0 in omogočili ustrezne prekinitve:
On Compare0a Tim0_compa_sub Nosave
On Compare0b Tim0_compb_sub Nosave
Enable Compare0a
Enable Compare0b
Enable Interrupts
Timer0 tudi konfiguriramo tako, da šteje impulze frekvence 125 kHz:
Config Timer0 = Timer , Prescale = 64
prekinitvene rutine pa bomo klicali, ko števec “prešteje” preko vrednosti vpisanih v registre OCR0A in OCR0B. Same prekinitvene rutine bomo namestili na konec programa in v njih delamo isto, kar je Timer1 delal na hardverskem nivoju – v prvi postavljamo PB3 v stanje “1”:
Tim0_compa_sub:
Set Portb.3
Return
v drugi pa v stanje “0”:
Tim0_compb_sub:
Reset Portb.3
Return
Poleg začetnih postavk OCR0 in OCR1 registrov, ki smo jih navedli že prej, smo zagotovili, da so na začetku programa vse tri barve ugasnjene. Na koncu inicijalizacijske rutine bomo poklicali kratek demo program in nato vračamo kontrolo komunikacijskemu programu:
Gosub Rgb_demo
Return
Demo program menja intenziteto vseh treh barv od najmanjšega do največjega in nato nazaj do najmanjšega kot signal, da je RGB slave modul pripravljen. Ko smo naredili to animacijo, nima več nobenega vpliva na delo modula, zato ga tukaj niti ne bomo analizirali, modul pa bo pravilno funkcioniral tudi, če ga popolnoma izpustimo.
Drugi podprogram, ki ga moramo predvideti, je Execute_i2c_command. Kadar RGB slave prepozna svoj naslov, bo sprejel od master-ja še tri bajte, jih vpisal v niz I2c_rcv_byte() in nato poklical navedeni podprogram. Vrednosti vpisane v niz so v razponu od 0-255 in predstavljajo ukaze, s kakšno intenziteto naj svetijo rdeči, zeleni in modri segment RGB diode. Intenzitete rdeče in modre barve bomo samo prenesli v ustrezne registre Timerja1:
Execute_i2c_command:
Ocr1a = I2c_rcv_byte(1)
Ocr1b = I2c_rcv_byte(3)
Z intenzivnostjo zelene barve krmili Timer0 s pomočjo pridruženih prekinitvenih rutin. Vrednosti v razponu od 1-255, vpisane v njegov OCR0A register, bo sprejel v pričakujoči intenziteti, vendar 0 ne bo popolnoma ugasnila zeleni segment. Zato bomo, če je zahtevana intenziteta dejansko 0, zaustavili Timer0 in “ročno” postavili PB3 v stanje “1”:
If I2c_rcv_byte(2) = 0 Then
Stop Timer0
Set Portb.3
V vseh ostalih primerih bomo samo prenesli dane intenzitete v OCR0A in ponovno zagnali Timer0:
Else
Ocr0a = I2c_rcv_byte(2)
Start Timer0
End If
Return
Z ukazom Return vračamo kontrolo komunikacijskemu programu. Striktno gledano, Timer0 bi morali ponovno pognati samo takrat, če je predhodno dana intenziteta bila 0; vendar bi to zahtevalo še nekaj dodatnih ukazov, in je enostavneje Timer0 zagnati pri vsaki spremembi intenzitete, pa tudi, če v tem trenutku deluje.
Opisana rutina ustreza RGB diodam s skupno anodo. Če imamo modul z RGB diodo s skupno katodo moramo dobljene vrednosti pred vpisom v registre komplementirati. Podprogram v tem primeru izgleda tako:
Execute_i2c_command:
Ocr1a = Not I2c_rcv_byte(1)
If I2c_rcv_byte(2) = 0 Then
Stop Timer0
Reset Portb.3
Else
Ocr0a = Not I2c_rcv_byte(2)
Start Timer0
End If
Ocr1b = Not I2c_rcv_byte(3)
Return
Tretji podprogram, ki ga moramo predvideti, je Prepare_data_to_be_sent. Ker RGB slave ne pošilja povratne informacije master-ju, bo ta podprogram ostal prazen:
Prepare_data_to_be_sent:
Return
Program ATtiny85_RGB_slave_2.bas
Ta program je napisan za RGB modul po shemi na sliki 2; poudarili bomo samo razlike glede na prvi program. V uvodnem delu moramo definirati nove SCL in SDA priključke:
Const Scl = 2
Const Sda = 3
in nato prebrati knjižnico, ki ima I2C komunikacijo realizirano brez uporabe USI vezja:
$include Attiny_i2cslave_nousi.sub
V podprogramu Init_slave bomo konfigurirali novo kombinacijo izhodnih priključkov mikrokontrolerja
Init_slave:
‘R = OC1A (PB1)
‘G = OC0A (PB0)
‘B = OC1B (PB4)
Config Portb.1 = Output
Config Portb.0 = Output
Config Portb.4 = Output
in konfigurirali Timer0 za delo v PWM načinu:
Config Timer0 = Pwm , Prescale = 64 ,
Compare_a_pwm = Clear_down
Tako konfiguriran Timer0 bo samostojno na svojem OC0A izhodu generiral impulze frekvence 488 Hz, katerih širina je proporcionalna vrednosti vpisani v OCR0A register. Zato je lahko podprogram Execute_i2c_command enostavnejši, kot prej; za RGB diodo s skupno anodo sedaj izgleda tako:
Execute_i2c_command:
Ocr1a = I2c_rcv_byte(1)
Ocr0a = I2c_rcv_byte(2)
Ocr1b = I2c_rcv_byte(3)
Return
v verziji za RGB diod s skupno katodo pa tako:
Execute_i2c_command:
Ocr1a = Not I2c_rcv_byte(1)
Ocr0a = Not I2c_rcv_byte(2)
Ocr1b = Not I2c_rcv_byte(3)
Return
Prekinitvi Compare0A in Compare0B se v tej verziji programa ne uporabljata, zato lahko zbrišemo tudi njima pridružene rutine Tim0_compa_sub in Tim0_compb_sub.
Ostanek programa se ne razlikuje od predhodnega primera. Vendar pa ta knjižnica zahteva večjo hitrost mikrokontrolerja, zato bomo delovni takt povečali na 16 MHz po prej opisanem postopku.
O realizaciji I2C slave modula na Arduino platformi si preberite v naslednjem nadaljevanju članka, ko bomo pokazali tudi, kako isto programsko nalogi rešiti v Arduino IDE, in tudi kako napisati ustrezne master programe za krmiljenje naših RGB modulov!
Opomba: Programe ATtiny85_RGB_slave_1.bas in ATtiny85_RGB_slave_2.bas in vse omenjene knjižnice lahko brezplačno dobite v uredništvu revije Svet elektronike!
ATtiny_I2Cslave_noUSI