Input / Output¶
Input / Output, eli tuttavallisesti I/O ja suomeksi "siirräntä" (huoh..), tarkoittaa tiedon siirtämistä tietokoneen komponenttien välillä. Inputit ovat komponentin vastaanottamia signaaleja ja outputit komponenttien lähettämiä signaaleja. Input-signaali voi olla esim. näppäimen painallus tai viesti oheislaitteelta. Output taas vastaavasti on viesti oheislaitteelle, esimerkiksi datan kirjoitus.
Sulautettun laitteen piirikortille on usein integroituna useita erilaisia komponentteja ja oheispiirejä mm. I/O:ta varten Alla listattuna tyypillisimpiä, joista osaa näistä tulemme käyttämään kurssilla.
- Painonappeja, liuku, kosketusnäyttö (ts. laitteen "näppäimistö")
- LEDejä
- Analogia-digitaalimuunnin (AD-muunnin, ADC)
- Ajastinpiiri (engl. Timer)
- (PWM-piiri (Pulssinleveysmodulaatio, engl. Pulse Width Modulation, PWM)
- Sarjaliikennepiirejä, kuten UART, SPI ja I2C
- USB- ja/tai Ethernet-ohjain
- LCD-Näyttö
Usein oheiskomponentit ovat analogisia, joiden inputtien ja outputtien jännitearvot voivat olla mitä vain maatason ja käyttöjännitteen väliltä. Esimerkiksi, monet anturit ilmaisevat mittausarvonsa analogisena jännitteenä minimi- ja maksimijännitteen välillä. Tietokoneelle analogiset signaalit on ensin muunnettava digitaaliksi, jota varten mikrokontrollereissa on valmiiksi integroituna analogia-digitaalimuuntimia, jotka muuttavat analogisen jännitteen numeroarvoksi. Koodissa otamme sitten numeroarvon talteen muuttujiin ja käsittelemme sitä ohjelmassa.
Pinni¶
Pinni (engl. pin) tarkoittaa komponentin fyysistä jalkaa tai liittimen piikkiä. Pinnien tarkoitus on komponentin sähköinen ja mekaaninen liittäminen piirilevylle. Piirin jokaisella pinnillä on tietty käyttötarkoitus ja joskus yhdellä pinnillä on useitakin eri käyttötarkoituksia. Pinnijärjestys kuvaa mikä tai mitkä ovat pinnien käyttötarkoitukset.
Alla esimerkin vuoksi Intel 4004:n (maailman ensimmäinen kaupallinen mikroprosessori vuodelta 1971) pinnijärjestys.
Alla kuvaus armaan SensorTag:n mikrokontrollerin (CC2560) pinneistä. Pinneille annetaan niitten toimintaan liittyvä looginen nimi, esimerkiksi pinni DIO_16 on yleiskäyttöinen digitaalinen I/O-pinni (General Purpose I/O, GPIO) jonka käyttötarkoituksen voi ohjelmoija vapaasti määritellä.
Esimerkiksi alla SensorTagin I/O-pinneille annettuja vakioita (ioc.h-otsikkotiedosto).
#define IOID_15 0x0000000F // IO Id 15
#define IOID_16 0x00000010 // IO Id 16
#define IOID_17 0x00000011 // IO Id 17
#define IOID_18 0x00000012 // IO Id 18
#define IOID_19 0x00000013 // IO Id 19
#define IOID_20 0x00000014 // IO Id 20
Pinnin loogista nimeä käytämme usein koodissa suoraan vakiona, jonka kääntäjät kirjastot valmiiksi on määritellyt. Nyt omassa koodissa voimme käyttää vakiota
IOID_17
osoittamaan laitteen fyysiseen pinniin DIO_17, jne. Kätevää!Pinnit ja bitit¶
Ohjelmakoodissa jokaista mikrokontrollerin pinniä vastaa yksi bitti. Asettamalla tämä bitti joko loogiseen nollaan tai ykköseen, fyysisen pinnin jännitetaso muuttuu ja näin ohjelmallisesti ohjataan pinniä ja siihen kytkettyä oheispiiriä. Vastaavasti lukiessa fyysisen pinnin tilaa, luetaan sen jännite ja sitä vastaava bitti asettuu vastaavaan loogiseen arvoon. Loogista ykköstä yleensä vastaa käyttöjännite (tyypillisesti 3.3V tai 5V) tai lähes tämä käyttöjännite sovituissa rajoissa. Loogista nollaa vastaa maataso (0V).
Nyt, oheiskomponenttien ja -laitteiden fyysiset pinnit kytkentään piirilevyllä MCU:n I/O pinneihin, muodostaen oheislaitteen ohjausväylän. Nyt voimme koodista ohjata oheislaitetta käsittelemällä vastaavia bittejä. Vastaavasti dataväyläksi valitaan joukko I/O-pinnejä, jotka kytketään oheislaitteen datapinneihin ja näihin saadaan sille välitettyä dataa (binäärilukuina). Lisäksi voi olla erikseen keskeytyspinni, jonka tila tai lähettämä keskeytyssignaali MCU:lle kertoo ohjelmallemme, että oheislaitteella on asiaa.
Joissain mikrokontrollereissa (esim. ATmelin MCU:n perustuvat Arduinot) I/O-laitteiden kytkemiseen varatut pinnit on rytmitelty I/O-porteiksi, esimerkiksi 8 pinniä tulkitaan loogisesti yhdeksi portiksi. Tämä on kätevää jos laite tarvitsee useita I/O-linjoja, niin niitä voidaan käsitellä loogisesti yhtenä yksikkönä. SensorTag käsittelee kuitenkin kaikkia I/O-pinnejä erikseen ja ohjelmoija voi itse määritellä haluamansa portit pinneille.
Esimerkiksi LED-pinnin asetus päälle SensorTag:ssa. Avuliaat pienet koodaustontut ovat meille kertoneet, että laitteen toinen ledi sijaitsee I/O-pinnissä DIO_15, jota käsitellään vakiolla
IOID_15
ja sitä vastaava I/O-portin vakio on GPIO_O_DOUT3_0
. Nyt osaisimme asettaa ledin päälle joko bittioperaatiolla (muistetaan aiemmasta materiaalista TAI-operaatio) tai sijoitusoperaatiolla, kuten asia on RTOS:ssa tehty. Näin emme tule oikeassa koodissa tekemään, vaan kirjastot tarjoavat meille tässä valmiita funktiokutsuja asettaa bittejä päälle / pois päältä. Muistiinkuvattu I/O¶
Sen lisäksi, että kommunikoimme oheislaitteen kanssa I/O-pinnien kautta, voimme myös kommunikoidan käyttäen MCU:n keskusmuistia. Joissain oheislaitteissa sen toiminnallisuus tarjotaan ohjelmille laitekohtaisten muistipaikkojen eli rekistereiden avulla. Tällöin puhutaan muistiinkuvatusta I/O:sta. Nyt oheislaitteen rekisterit näkyvät varattuina muistipaikkoina MCU keskusmuistissa, joten niitä voidaan käyttää tavallisen muistipaikan tavoin muuttujana koodissa! Oheislaitteiden ajurikirjastot määrittelevät näiden muistipaikkojen paikat muistissa.
Toinen mahdollisuus olisi porttikuvattu I/O, jossa rekistereitä käsitellään erillisten in- ja out-käskyjen avulla. Kurssilla käytämme muistiinkuvattua I/O:ta koska se on SensorTag:n valittu tapa.
Rekistereitä on kolmea tyyppiä: osoite-, kontrolli/ohjaus- ja datarekisterit. Oheislaite voi tarjota useita rekistereitä, jolloin niiden osoite tulee ensin valita ohjausrekisterillä. Kontrollirekisterin bittejä käytetään laitteen toiminnan ohjaamiseen, vaikkapa taustavalon asettamiseksi päälle LCD-näytössä. Datarekistereihin syötetään laitteelle menevä tieto, esim. LCD-näytölle tuleva teksti merkki kerrallaan. Jos samaa datarekisteriä käytetään sekä datan kirjoittamiseen ja lukemiseen, kontrollirekisterin pinni määrittää tiedonsiirron suunnan.
Jokaisesta (oheislaitteen) rekisteristä meidän täytyy siis tietää..
- tarkoitus ja toiminta
- yksittäisten bittien tarkoitus ja toiminta
- muistiosoitteet: kääntäjäympäristö tarjoaa näitä vastaavat vakiot
Esimerkkinä SensorTag:n yksi rekisterikuvaus. Tämä datarekisteri pitää sisällään laitteen patterin jännitetason. Varsin hyödyllinen rekisteri siis!
Kuvasta nähdään, että rekisterin koko on 32 bittiä, siis bitit
31-0
(kuvassa keltainen kenttä). Huomataan, että bitit 31-11
ovat varattuja laitteen sisäiseen käyttöön, mutta muissa biteissä on meitä kiinnostavaa tietoa. Bitit 10-8
pitävät sisällään patterijännitteen kokonaisosan ja bitit 7-0
patterijännitteen desimaaliosan koodattuna tietyllä tavalla. Nyt jos haluaisimme tietää patterin jännitteen, kysyisimme asiaa tältä datarekisterilta ja tekisimme sen arvolle muunnoksen vaikkapa liukuluvuksi. RTOS tarjoaa suoraan rekisterin arvon lukemiseenja asettamiseen
HWREG
-makron.uint32_t arvo = HWREG(...); // Argumentiksi osoite vakion kautta
Datakirja¶
Komponentin tai mikrokontrollerien manuaali, eli datakirja, selittää yksityiskohtaisen pilkuntarkasti kaiken sen sisäisen sekä jokaisen integroidun piirin toiminnan. Datakirja on tyypillisesti englanninkielinen, vahvasti ammattisanastoon perustuva ja satoja ellei tuhansia sivuja pitkä, joten ei ole tarkoituksenmukaista opetella sitä ulkoa. Käytetään datakirjaa referenssinä laitteistoa suunniteltaessa ja ohjelmoitaessa. Monimutkaisilla laitteilla voi olla datakirjojen lisäksi manuaaleja, esimerkiksi SensorTag Technical Reference Manual. Tässä käsikirjassa on 1742 sivua!
Onneksi, sen sijaan että kahlaisimme läpi satoja sivuja datakirjoja, kehitysympäristöihin löytyy valmiiksi kirjastoja, funktioita, makroja ja vakioita, joissa on toteutettu valmiiksi oheislaitteen ohjausta korkeamman tason toimintoja varten. Tässä jos jossain EI kannata keksiä pyörää uudelleen.
I/O-pinnien käyttö¶
Seuraavaksi perehdymme I/O-pinnien käyttämiseen SensorTagissa koodiesimerkin avulla. Käytössä olevat SensorTag-spesifiset vakiot löytyvät kätevästi ohjelmistoprojektiimme automaattisesti ilmestyvästä otsikkotiedostoista Board.h ja CC2650STK.h.
Esimerkkimme, kaikessa kauneudessaan, käyttää toista SensorTagin kahdesta painonappia on/off-kytkimenä yhdelle laitteen ledeistä.
Käytämme valmista kääntäjäympäristön tarjoamaa
Pin
-kirjastoa. Koska SensorTag:ssa tietyt I/O-pinnit ovat valmiiksi kytkettynä painonappeihin ja ledeihin, saamme niiden määritykset mukaan koodiin otsikkotiedostolla PINCC26XX.h
. #include <ti/drivers/PIN.h>
#include <ti/drivers/pin/PINCC26XX.h>
...
// RTOS:n muuttujia pinnien käyttöön
static PIN_Handle buttonHandle;
static PIN_State buttonState;
static PIN_Handle ledHandle;
static PIN_State ledState;
// Pinnien alustus, molemmille pinneille oma konfiguraatio
PIN_Config buttonConfig[] = {
Board_BUTTON0 | PIN_INPUT_EN | PIN_PULLUP | PIN_IRQ_NEGEDGE, // Hox! TAI-operaatio
PIN_TERMINATE // Määritys pitää lopettaa tähän
};
PIN_Config ledConfig[] = {
Board_LED0 | PIN_GPIO_OUTPUT_EN | PIN_GPIO_LOW | PIN_PUSHPULL | PIN_DRVSTR_MAX,
PIN_TERMINATE
};
// Napinpainalluksen keskeytyksen käsittelijäfunktio
void buttonFxn(PIN_Handle handle, PIN_Id pinId) {
// Vaihdetaan led-pinnin tilaa negaatiolla
PIN_setOutputValue( ledHandle, Board_LED0, !PIN_getOutputValue( Board_LED0 ) );
}
int main(void) {
Board_initGeneral();
// Otetaan pinnit käyttöön ohjelmassa
ledHandle = PIN_open(&ledState, ledConfig);
if(!ledHandle) {
System_abort("Error initializing LED pins\n");
}
buttonHandle = PIN_open(&buttonState, buttonConfig);
if(!buttonHandle) {
System_abort("Error initializing button pins\n");
}
// Asetetaan toiselle pinnille keskeytyksen käsittellijä
// Keskeytys siis tulee kun nappia painetaan!
if (PIN_registerIntCb(buttonHandle, &buttonFxn) != 0) {
System_abort("Error registering button callback function");
}
BIOS_start();
return (0);
}
Tämä esimerkkiohjelma siis, joka kerta kun nappia painetaan, suorittaa funktion buttonFxn, jossa pinnin tila vaihtuu. Pinnien tila ohjaa laitteen lediä päälle / pois päältä.
Puretaanpas ohjelmaesimerkki osiin. Ensin esittelemme joukon muuttujia per käyttämämme pinni. Tarvitsemme jälleen kerran kahvat pinnille (
Pin_Handle
) sekä muuttujia (tyyppiä Pin_State
), tosin näitä muuttujia emme omassa koodissa alustuksen jälkeen tarvitse. Noh, pidetään näillä RTOS tyytyväisenä. Pinnien alustus¶
Seuraavaksi alustamme jokaisen pinnin joko sisääntuloksi (input) tai ulostuloksi (output)
Pin_Config
-tyyppiseen taulukkoon. Jokainen taulukon alkio on yhden pinnin asetus. Asettaminen tapahtuu TAI-operaatiolla, johon laitamme kaikki haluamamme asetusbitit pinnille. Alla esimerkissä alustus on tehty siten, että jokaiselle pinnille taulukkoon tulee oma alkio (pilkulla erotettuna). Taulukon viimeinen alkio on aina vakio PIN_TERMINATE
. Kaikki nämä käytetyt vakiot ja niiden tarkoitukset löytyvät Pin-kirjaston dokumentaatiosta. PIN_Config buttonConfig[] = {
Board_BUTTON0 | PIN_INPUT_EN | PIN_PULLUP | PIN_IRQ_NEGEDGE, // Pinnin asetukset TAI-operaatiolla
PIN_TERMINATE // Tämä vakio lopettaa määrittelyn
};
PIN_Config ledConfig[] = {
Board_LED0 | PIN_GPIO_OUTPUT_EN | PIN_GPIO_LOW | PIN_PUSHPULL | PIN_DRVSTR_MAX,
PIN_TERMINATE
};
Nyt tässä käytämme kahta eri Pin_Config-taulukkomuuttujaa, koska haluamme erottaa toisistaan painonappi- ja ledi-pinnit. Jos alustaisimme toisen painonapin tai ledin, sille tulisi vastaavaan taulukkoon oma alkio (ts. rivi) pilkulla erotettuna. (Lisäksi voime määritellä pinnin sähköiseen tomintaan liittyviä asetusparametreja. Mutta, menemättä syvemmälle komponenttien sähköiseen toimintaan suosittelemme ym. parametrien käyttöä aina tässä yhteydessä.) Huomatkaa hieman erilaiset parametrit input- ja output-pinneille.
SensorTagin erästä painonapin pinniä voimme ohjelmassa käsitellä vakiolla
Board_BUTTON0
. Huomataan, että SensorTagin toiselle napille löytyy vakio Board_BUTTON1
ja ledeille myös omat vakiot Board_LED0..2
. Painonappia vastaava pinni asetetaan sisääntuloksi vakiolla PIN_INPUT_EN
ja lediä vastaava pinni ulostuloksi vakiolla PIN_GPIO_OUTPUT_EN
. Vakiolla PIN_GPIO_LOW
pinnin jännite asetetaan alas maatasoon, joten ledi ei ole päällä. Vakiolla PIN_GPIO_HIGH
asetettaisiin pinnin jännite käyttöjännitteeseen eli tässä ledi päälle. Pinnit mukaan ohjelmaan¶
Seuraavaksi menemme main-funktioon. Pinnit varataan ohjelmamme käyttöön
Pin_open
-funktiolla, jonka parametreiksi tulee äsken esittelemämme pin-muuttujat ja määritykset. ledHandle = PIN_open( &ledState, ledConfig );
if(!ledHandle) {
System_abort("Error initializing LED pins\n");
}
Sitten huomataan, ettei ohjelmassamme ei olekaan taskia mitä suorittaa! Tästä emme vielä ole puhuneet, mutta RTOS käsittelee taskit ja keskeytyksien käsittelijäfunktiot toisiaan vastaavasti, eli ohjelmassa voi olla molempia toteuttamassa toiminnallisuutta. Keskeytyksistä lisää myöhemmin, mutta nyt siis korvaamme taskin painonappiin liitettävällä keskeytyksellä ja sen käsittelijä-funktiolla
Samalla huomataan että pinni alustettiin vakiolla
buttonFxn
. PIN_registerIntCb
-funktiossa siis määritellään kyseiselle pinnille, käyttäen sen kahvaa, keskeytyksen käsittelijäfunktio. Samalla huomataan että pinni alustettiin vakiolla
PIN_IRQ_NEGEDGE
, joka asettaa tietyntyyppisen kesketyksen päälle pinnille. if (PIN_registerIntCb( buttonHandle, &buttonFxn ) != 0 ) {
System_abort("Error registering button callback function");
}
Hox! Olisimme tietenkin voineet toteuttaa saman toiminnallisuuden ohjelmaan laatimalla taskin, joka ikuisessa toistorakenteessa kyselisi painonapin pinnin tilaa ja vastaavasti muuttaisi led-pinnin tilaa. Mutta tämän ratkaisun haittana on jatkuva pinnin tilan kysely, joka vie MCU:n suoritusaikaa ja varaa muistia omiin tilamuuttujiin koodissa. Nyt keskeytyksen käsittelijärutiini suoritetaan automaattisesti vain tarvittaessa, kun fyysisen pinnin tila vaihtuu. Aikaa ja vaivaa säästävä keino.
Pinnin tilan asetus¶
Seuraavaksi, käsittelijäfunktiossa
buttonFxn
luemme pinnin tilan (siis, onko se päällä 1
/ pois päältä 0
) funktiolla PIN_getOutputValue
, joka ottaa parametrikseen pinniä vastaavan vakion. Arvo voidaan asettaa funktiolla PIN_setOutputValue
, parametreinä pinnin kahva, sitä vastaava vakio ja tila johon asetetaan. Tässä asetamme arvoksi luetun arvon negaation !
-operaattorilla.PIN_setOutputValue( ledHandle, Board_LED0, !PIN_getOutputValue(Board_LED0) );
Voimme itseasiassa asettaa saman käsittelijäfunktion useille pinneille, edellyttäen että ne on määritelty samassa Pin_config-rakenteessa. Tällöin meidän täytyy käsittelijäfunktion argumentista
pinId
selvittää mikä painonappi onkaan kyseessä....
void buttonFxn(PIN_Handle handle, PIN_Id pinId) {
if(pinId == Board_BUTTON0) {
tee_jotain1();
} else if(pinId == Board_BUTTON1) {
tee_jotain2();
}
}
...
Lopuksi¶
Tässä kappaleessa kerrottiin lyhyesti yleisestä mikrokontrollerien I/O:sta oheislaitteiden kanssa toimimiseen. Tulevissa kappaleissa palaamme uudelleen monimutkaisempiin I/O-ratkaisuihin.
Materiaalin perusteella osaat jo tehdä sulautetun ohjelman, joka vilkuttaa laitteen lediä ja reagoida käyttäjän napin painallukseen!
Anna palautetta
Kommentteja materiaalista?