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 tai dataa ja outputit komponenttien lähettämiä signaaleja tai dataa. Input-signaali voi olla esim. näppäimen painallus tai viesti oheislaitteelta. Output taas vastaavasti on viesti oheislaitteelle, esimerkiksi ohjauskomennon lähettäminen laitteelle.
Sulautettuun laitteeseen (piirikortille) on usein integroituna useita erilaisia komponentteja ja oheispiirejä mm. I/O:ta tai käyttöliittymää varten Alla listattuna tyypillisimpiä, joista osaa tulemme käyttämään kurssilla.
- Painonappeja, liukukytkin, 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, kuten jännite, on ensin muunnettava digitaaliksi eli numeroarvoksi. Tätä varten mikrokontrollereissa on valmiiksi integroituna analogi-digitaalimuuntimia, jotka tuottavat numeroarvoa sovitussa tarkkuudessa. Koodissa otamme sitten numeroarvon talteen muuttujiin ja voimme käsittellä sitä.
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 (TI CC2560) pinneistä. Pinneille annetaan niiden toimintaan liittyvä looginen nimi, esimerkiksi DIO_16, joka on yleiskäyttöinen digitaalinen I/O-pinni (engl. General Purpose I/O, GPIO), jonka käyttötarkoituksen voi ohjelmoija vapaasti määritellä.
Alla SensorTagin I/O-pinneille annettuja vakioita (ioc.h-otsikkotiedosto). 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ää!#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
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 (2.7V suuremmat arvot tulkiseen ykköseksi). Loogista nollaa vastaa maataso (0V, tai 0.7V pienempi jännite)
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ä. Voi myös olla osan ohjausväylää keskeytyspinni, jonka tila tai lähettämä keskeytyssignaali MCU:lle kertoo ohjelmallemme että oheislaitteella on asiaa. Vastaavasti dataväyläksi valitaan joukko I/O-pinnejä, jotka kytketään oheislaitteen datapinneihin ja näihin saadaan sille välitettyä dataa (binäärilukuina).
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ä yksittäisinä, ellei ohjelmoija voi itse määritellä haluamansa portit haluamilleen 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 koodissa tekemään, vaan RTOS:n kirjastot tarjoavat meille tässä valmiita funktiokutsuja asettaa bittejä päälle / pois päältä. Jos tarkastelemme näiden funktiokutsujen koodia, löytyvät kyllä tutut bittioperaatiot sieltä konepellin alta. Muistiinkuvattu I/O¶
Sen lisäksi, että kommunikoimme oheislaitteen kanssa I/O-pinnien kautta, voimme myös kommunikoida käyttäen MCU:n keskusmuistia. Joissain oheislaitteissa kuvataan sen toiminnallisuus ohjelmille laitekohtaisten muistipaikkojen eli rekistereiden avulla. Tällöin puhutaan muistiinkuvatusta I/O:sta. Nyt siis käsittelemällä ohjelmakoodissa näitä varattuja muistipaikkoja tavallisen muuttujan tavoin, voidaan oheislaitteelle lähettää tai vastaanottaa tietoa. Laitteiden käsikirjat kertovat nämä (suunnitteluvaiheessa) määritellyt muistipaikkojen osoitteet ja (yleensä) niitä vastaavat valmiit vakiot.
Toinen mahdollisuus olisi porttikuvattu I/O, jossa rekistereitä käsitellään erillisten in- ja out-käskyjen avulla.
Rekistereitä on kolmea tyyppiä: osoite-, kontrolli/ohjaus- ja datarekisterit. Oheislaite voi tarjota useita rekistereitä, mikä vain kombinaatio näistä kolmesta tyypistä laitteen toteutuksesta riippuen on mahdollinen. Ohjausrekisterin 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ää..
- Yleinen tarkoitus ja toiminta
- Yksittäisten bittien tarkoitus ja toiminta
- Muistiosoitteet, joissa rekisterit ovat ja tarjoaako kääntäjäympäristö 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.// Lukuoperaatio: rekisterin arvo muuttujaan patteri
uint32_t patteri = 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
. Painonapin käyttöönottamiseksi ohjelmassa täytyy tehdä neljä asiaa
- Esitellä RTOS:n muuttujat joilla painonappia käsitellään
- Alla buttonHandle, buttonState ja taulukko buttonConfig[]
- Alustaa napin asetukset halutusti
- Alla taulukkoon buttonConfig[]
- Laatia napinpainalluksen käsittelijäfunktio
- Alla keskeytysrutiin buttonFxn
- main-funktiossa ottaa nappeja vastaavat I/O-pinnit käyttöön ohjelmassa
SensorTagissa on kaksi painonappia ja nämä määrittelyt tulee tehdä molemmille erikseen.
Tämä esimerkkiohjelma siis, joka kerta kun nappia painetaan, suorittaa funktion
buttonFxn
, jossa lediä vastaavan pinnin tila vaihtuu, joka siis ohjaa laitteen lediä päälle / pois päältä. Sen takia ohjelmassa on myös I/O-määrittelyt LEDiä vastaavalle pinnille. #include <ti/drivers/PIN.h>
#include <ti/drivers/pin/PINCC26XX.h>
...
// 1. RTOS:n muuttujat pinnien käyttöön
static PIN_Handle buttonHandle;
static PIN_State buttonState;
static PIN_Handle ledHandle;
static PIN_State ledState;
// 2. Pinnien määrittelyt, molemmille pinneille oma konfiguraatio
PIN_Config buttonConfig[] = {
Board_BUTTON0 | PIN_INPUT_EN | PIN_PULLUP | PIN_IRQ_NEGEDGE, // Hox! TAI-operaatio
PIN_TERMINATE // Määritys lopetetaan aina tähän vakioon
};
PIN_Config ledConfig[] = {
Board_LED0 | PIN_GPIO_OUTPUT_EN | PIN_GPIO_LOW | PIN_PUSHPULL | PIN_DRVSTR_MAX,
PIN_TERMINATE // Määritys lopetetaan aina tähän vakioon
};
// 3. 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();
// 4. 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);
}
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. Alla taulukko on yhden pinnin asetus. Kaikki nämä käytetyt vakiot ja niiden tarkoitukset löytyvät Pin-kirjaston dokumentaatiosta. Asettaminen tapahtuu TAI-operaatiolla, jossa alustamme kaikki haluamamme asetusbitit, esimerkiksi
Board_BUTTON0 | PIN_INPUT_EN | PIN_PULLUP | PIN_IRQ_NEGEDGE
. SensorTagin toista painonapin pinniä voimme ohjelmassa käsitellä vakiolla Board_BUTTON0
ja toiselle napille löytyy vakio Board_BUTTON1
. Ledeille on myös omat vakiot Board_LED0..2
. Huomataan hieman erilaiset vakiot ja asetukset input- ja output-pinneille. 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. Taulukon viimeinen alkio on aina vakio
PIN_TERMINATE
. 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
};
(Lisäksi voime määritellä pinnin sähköiseen tomintaan liittyviä asetusparametreja. Mutta, menemättä syvemmälle komponenttien sähköiseen toimintaan suosittelemme yo. parametrien käyttöä aina tässä yhteydessä.)
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.
Pinnit mukaan ohjelmaan¶
Seuraavaksi menemme
main
-funktioon. Pinnit varataan ohjelmamme käyttöön Pin_open
-funktiolla, jonka parametreiksi tulee äsken esittelemämme RTOS:n pinnimuuttujat 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äfunktion
buttonFxn
toteutuksella. PIN_registerIntCb
-funktiossa siis määritellään kyseiselle pinnille, käyttäen sen kahvaa, keskeytyksen käsittelijäfunktio buttonFxn
. Samalla huomataan että keskeytyksen antava pinni alustettiin lisäksi 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 lediä vastaavan pinnin tilan (siis, onko se päällä "1" / pois päältä "0") funktiolla PIN_getOutputValue
, joka ottaa parametrikseen pinniä vastaavan vakion Board_LED0
. Arvolle tehdään negaatio, huomaa operaattori !
, joka asetetaan led-pinnin Board_LED0
arvoksi, siis takaisin saman pinnin arvoksi, funktiolla PIN_setOutputValue
. 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! Oisko kakkukahvien paikka?
Anna palautetta
Kommentteja materiaalista?