Sarjaliikenne¶
Osaamistavoitteet: Kaksi tapaa sarjaliikenteen toteutukseen sulautetussa laitteessa.
Sarjaliikenne tarkoittaa latteiden tai komponenttien keskinäisessä tiedonsiirrossa käytettäviä tekniikoita, joissa data siirtyy sarjamuodossa eli peräkkäin bitti kerrallaan yhtä linjaa pitkin. Data voi siirtyä yhtä linjaa yhteen suuntaan, jolloin lähetykseen ja vastaanottoon on omat linjansa, tai synkronoidusti voidaan käyttää samaa linjaa vuorotellen lähetykseen ja vastaanottoon. Lisäksi sarjaliikennettä usein tahdistaa oma kellolinja.
Kuvassa yksinkertaistettuna sarjaliikenteen idea. Kaksi laitetta on määritellyt itselleen I/O-pinneistä lähetys- (Tx-pinni) ja vastaanotto-linjat (Rx-pinni). Laitteet kytketään toisiinsa siten, että toisen lähetys on toisen vastaanotto. Näin lähettäjän bittijono (Tx-pinnin tilat ajan funktiona) näkyvät vastaanottajan Rx-pinnin tiloissa.
Tieto siirretään datalinjoja pitkin laitteelta toiselle bittijonona, jolloin molempien päiden tulee tietää viestin formaatti (kaikkien bittien tarkoitus) ts. tiedonsiirtoprotokolla ja käytetty siirtonopeus. Tiedonsiirtoprotokollan viestikehyksen sisällä siirrämme varsinaisen informaation, mutta lisäksi protokollassa voi olla esimerkiksi virheenkorjaukseen liittyvää tietoa lisänä. Sarjaliikenneprotokollat ovat yleensä standardoituja, joka auttaa (eri valmistajien) erilaisia laitteita kommunikoimaan keskenään. Siirtonopeus taas määrittää sen mikä on bitin ajallinen kesto linjassa. Jos tätä ei tiedetä, on vastaanottopään vaikea osata tulkita bitit oikein jonosta, eli se ei tiedä missä kohti signaalia bitti alkaa ja päättyy ja missä kohti signaalia bitin arvo luetaan. Siirtonopeuden voi myös määrittää ym. kello.
Aiemmin esitelty vaihtoehto komponenttien (ja järjestelmien) väliselle tiedonsiirrolle on rinnakkainen tiedonsiirto, eli tyyppiesimerkkinä juurikin tietokoneen osoite-, ohjaus- ja dataväylät, joissa bitit siirtyvät yhtaikaa samanaikaisesti omia johtimiaan pitkin. Rinnakkainen tiedonsiirto on siis nopeampaa kuin sarjamuotoinen, mutta hintana on lisätarve rinnakkaisille johtimille ja I/O-pinneille. Tässä mielessä sarjaliikenne on taloudellisempaa ja siirtonopeudetkin ovat nykyään kovasti kehittyneet.
Tällä kurssilla emme perehdy sarjaliikenteen salaisuuksiin sen syvemmin. Erilaisia enemmän tai vähemmän standardoituja toteutuksia onkin paljon, joista esittelemme (ja käytämme) SensorTagiin toteutetun yleisen sarjaliikennetoteutuksen UART:n ja integroitujen anturien kanssa käytettävän i2c-protokollan. i2c on todella yleinen ja varsin nopea (kellotaajuus 100-400kHz) sarjaliikenneprotokolla sulautettuihin järjestelmiin.
Universal Asynchronous Receiver/Transmitter¶
Universal Asynchronous Receiver/Transmitter (UART), on sarjaliikennepiiri, joka muuntaa rinnakkaismuotoista tietoa sarjamuotoiseksi kommunikaatioon oheislaitteen kanssa. Piirin avulla voimme toteuttaa esim. tunnetun RS-232 standardin mukaista tiedonsiirtoa. UART on yleiskäyttöinen jo hieman vanhahko tekniikka, mutta sulautetuissa järjestelmissä yleisesti käytössä ASCII/tekstimuotoiseen kommunikointiin kaksisuuntaisesti. UART:ia voidaan käyttää myös binäärimuotoisen ("numeroiksi koodatun") datan siirtämiseen, josta esimerkkinä sulautetun laitteen ohjelmamuistiin kirjoittaminen, kun päivitämme ohjelmamme laitteelle.
Tyyppiesimerkki sulatetuissa järjestelmissä UARTia käyttävästä oheislaitteesta on GPS-vastaanotin, joka lähettää koordinaattitietoa ihmisen luettavassa NMEA-tekstimuodossa. Toinen esimerkki voisi olla ohjelmoijan toteuttama pienehkö komentotulkki UART-pohjaisena sarjaliikenteenä työasema-PC:n ja sulautetun laitteen välillä. Ja tietysti CSV-muotoista informaatiota voi siirtää UART:n avulla..
Menemättä liialti protokollan toteutukseen, todetaan että UART-pohjaiselle sarjaliikenteelle tulee asettaa muutama tiedonsiirtoparametri:
- Siirtonopeus (baudia). Tyypillisiä nopeuksia ovat 9600, 19200, 38400, 57600 ja 115200 bittiä/sekunti.
- Databittien määrä: kurssilla aina 8.
- Pariteettibitti: kurssilla ei käytetä.
- Stop-bittien määrä: kurssilla aina 1.
Tällöin sarjaliikenteen parametreistä käytetään lyhennettä (esim) 9600 8n1. Oleellista on tietysti konfiguroida sekä lähetyspää että vastaanottopää samoilla parametreillä, jotta ne ymmärtävät toisiaan. Etuna tässä on, että kun parametrit tiedetään, niin kellolinjalle ei ole tarvetta.
Hox! Joskun kommunikoimme työaseman kanssa, sen sarjaportille (COM1, /dev/ttyS, jne) pitää tietenkin asettaa vastaavat nopeus ja asetukset. SensorTag-laitteen ajurit luovat työasemissa USB-liitynnästä yhden loogisen sarjaportin (nimeltään XDS110 Class Application/User UART), jota voidaan käyttää esim. terminaaliohjelman avulla kommunikointiin laitteen kanssa. Tätä tulemme harjoittelemaan laboratoriotyössä.
TI-RTOS UART¶
Seuraavaksi käymme läpi esimerkin avulla miten RTOS:n sarjaliikenteen
UART-kirjastoa
käytetään. Tässä ao.
serialTask
-tehtävässä siis:- Ensin alustetaan sarjaliikenne parametreillä (9600,8n1) RTOS:n tietorakenteeseen
UART_params
- Avataan yhteys
UART_open
-funktiolla. Nyt ideana on että taskissa yhteys avataan kerran, ennen taskin ikuista silmukkaa, joten yhteys on auki kokoajan. Jos yhteys jostain syystä tarvitsee sulkea, on siihenUART_Close
. - Ikuisessa silmukassa odotetaan
UART_Read
-funktiolla, kunnes sarjaliikenteen kautta laitteelle lähetetään merkki. Nyt tässä siis parametreinä merkkimuuttuja ja odotettu pituus eli 1 merkki. - Kun merkki on saapunut,
UART_write
-funktiolla lähetetään vastaanotettu merkki takaisin sarjaliikenteen kautta osana merkkijonoa. Parametreinä siis merkkijono ja sen pituus.
#include <string.h>
#include <ti/drivers/UART.h>
...
// Taskifunktio
Void serialTask(UArg arg0, UArg arg1) {
char input;
char echo_msg[30];
// UART-kirjaston asetukset
UART_Handle uart;
UART_Params uartParams;
// Alustetaan sarjaliikenne
UART_Params_init(&uartParams);
uartParams.writeDataMode = UART_DATA_TEXT;
uartParams.readDataMode = UART_DATA_TEXT;
uartParams.readEcho = UART_ECHO_OFF;
uartParams.readMode=UART_MODE_BLOCKING;
uartParams.baudRate = 9600; // nopeus 9600baud
uartParams.dataLength = UART_LEN_8; // 8
uartParams.parityType = UART_PAR_NONE; // n
uartParams.stopBits = UART_STOP_ONE; // 1
// Avataan yhteys laitteen sarjaporttiin vakiossa Board_UART0
uart = UART_open(Board_UART0, &uartParams);
if (uart == NULL) {
System_abort("Error opening the UART");
}
// Ikuinen elämä
while (1) {
// Vastaanotetaan 1 merkki kerrallaan input-muuttujaan
UART_read(uart, &input, 1);
// Lähetetään merkkijono takaisin
sprintf(echo_msg,"Received: %c\n",input);
UART_write(uart, echo_msg, strlen(echo_msg));
// Kohteliaasti nukkumaan sekunniksi
Task_sleep(1000000L / Clock_tickPeriod);
}
}
int main(void) {
...
// Otetaan sarjaportti käyttöön ohjelmassa
Board_initGeneral();
Board_initUART();
...
return 0;
}
Sarjaliikenne UART:n kautta SensorTagilla on siis yksinkertaista luku- ja kirjoitusfunktioiden käyttämistä, kunhan sarjaliikenneyhteys on ensin avattu.
Mutta tarkastellaanpa hieman tietorakenteen
UART_params
jäseniä:readDataMode / writeDataMode
asettaa minkätyyppistä dataa sarjaliikenteessä käytetään. Vaihtoehdot:- Asetus
UART_DATA_TEXT
ASCII-muotoiselle "ihmisen luettavalle" tekstille, kuten esimerkiksi CSV-muotoiset viestit. - Asetus
UART_DATA_BINARY
. Kun siirretään binäärimuotoista tietoa. Tällöin viestiä ei käsitellä merkkijonoina vaan binäärilukuina. Lukuarvojen tulkitsemiseen sitten tyypillisesti laaditaan jäsentäjäfunktio (engl. parser). readMode
kertoo onko sarjaliikenne blokkaavaa, asetus (UART_MODE_BLOCKING
). Eli siis ohjelman toiminta lukufunktiossaUART_read
pysähtyy odottamaan niin kauaksi aikaa kunnes dataa on saatavilla sarjaliikenteen kautta odotettu määrä.- UART-kirjasto mahdollistaa sarjaliikenteen toteuttamisen myös keskeytyksiin perustuen, jolloin ei tarvitsisi pysähtyä odottelemaan, vaan keskeytyssignaali kertoisi milloin dataa olisi saatavilla. Tästä lisää tulevassa materiaalissa..
- Asetus
readEcho
tarkoitaa sitä, että kaiutetaanko automaattisesti lähetetty data takaisin lähettäjälle. Eli näkyviin lähettäjän terminaaliohjelmassa. No, tässä esimerkissä ei sitä tehdä automaattisesti vaan ohjelmallisesti esimerkin vuoksi..
Lopuksi sarjaliikenneyhteys pitää sulkea
UART_Close
-kutsulla, mutta esimerkissä sitä ei ole, koska toimimme ikuisessa silmukassa.i2c-väylä¶
i2c-sarjaliikenneväylä on hyvin tunnettu binäärimuotoinen sarjaliikenneprotokolla sulautettuihin laitteisiin.
i2c-väylän toiminta perustuu tietotekniikan alkuajoista asti käytettyyn master/slave-arkkitehtuuriin. Väylässä on siis aina väylää ohjaava master-laite ja n kpl slave-laitteita. Tässä master aloittaa yhteyden ja asettaa tiedonsiirtonopeuden, jota slave-laitteet seuraavat. i2c tarvitsee kaksi I/O-pinniä, kellon (Serial Clock Line, SCL) ja datalinjan (Serial DAta Line, SDA).
Nyt i2c-väylässä laitteet/komponentit yksilöidään osoitteilla. Osoitteen on komponentin valmistaja etukäteen määrittänyt ja sen löydämme komponentin datakirjasta. Joskus valmistaja voi tarjota komponentille joukon i2c-osoitteita, mistä käyttäjä voi asettaa osoitteen. Tämä on hyödyllistä silloin, kun samassa väylässä on useita samoja komponentteja, esimerkiksi antureita. Yhdessä i2c-väylässä voi olla kytkettynä maksimissaan 1008 laitetta!
i2c-viestit koostuvat kolmesta osasta: vastaanottajan osoite, rekisterin osoite, data
- Vastaanottajan osoite: pituus on yleensä yksi tavu (8 bittiä)
- Viestin sisältö: Rekisterin osoite (8 bittiä), data (n bittiä)
- Master lähettää komennon laitteen ohjausrekisteriin. Komento voi olla esim. slave-laitteen ohjaus jollain rekisteriasetuksella
- Master kysyy slave:lta dataa sen datarekisteristä
- Slave lähettää datan masterille. Datakentän pituus vaihtelee yhdestä useampaan tavuun, laitteen protokollan mukaan.
Alla esimerkki i2c-viestien kehysrakenteesta. Tämä vain tiedoksi.
i2c SensorTag:ssa¶
Seuraavaksi katsotaanpa RTOS:n tarjoamaa
i2c
-kirjaston käyttöä koodiesimerkin avulla. Esimerkissä luemme SensorTag:iin integroidulta lämpötila-anturilta TMP007 lämpötilan kymmenen kertaa. Kuten huomaamme, on anturin datakirja täynnä monenlaista vaikeaselkoista informaatiota, johon kurssilla ei tarvitse perehtyä. Kurssilla meitä kuitenkin kiinnostaisi anturin rekisterien käyttö (kappale 7.5. datakirjassa) i2c-väylän avulla. Mutta teemme sen nyt alla perustuen valmiisiin vakioihin. // 1. i2c-kirjasto mukaan ohjelmaan
#include <ti/drivers/I2C.h>
// Taskifunktio
Void sensorTask(UArg arg0, UArg arg1) {
uint8_t i;
float temperature;
// RTOS:n i2c-muuttujat ja alustus
I2C_Handle i2c;
I2C_Params i2cParams;
// Muuttuja i2c-viestirakenteelle
I2C_Transaction i2cMessage;
// Alustetaan i2c-väylä
I2C_Params_init(&i2cParams);
i2cParams.bitRate = I2C_400kHz;
// Avataan yhteys
i2c = I2C_open(Board_I2C_TMP, &i2cParams);
if (i2c == NULL) {
System_abort("Error Initializing I2C\n");
}
// i2c-viesteille lähetys- ja vastaanottopuskurit
uint8_t txBuffer[1]; // Nyt lähetetään yksi tavu
uint8_t rxBuffer[2]; // Nyt vastaanotetaan kaksi tavua
// i2c-viestirakenne
i2cMessage.slaveAddress = Board_TMP007_ADDR;
txBuffer[0] = TMP007_REG_TEMP; // Rekisterin osoite lähetyspuskuriin
i2cMessage.writeBuf = txBuffer; // Lähetyspuskurin asetus
i2cMessage.writeCount = 1; // Lähetetään 1 tavu
i2cMessage.readBuf = rxBuffer; // Vastaanottopuskurin asetus
i2cMessage.readCount = 2; // Vastaanotetaan 2 tavua
while (1) {
// Lähetetää viesti funktiolla I2C_Transfer
if (I2C_transfer(i2c, &i2cMessage)) {
// Muunnetaan 2-tavuinen data rxBuffer:ssa
// lämpötilaksi (kaava harjoitustehtävissä)
temperature = ...;
// Lämpötila-arvo tiedoksi konsoli-ikkunaan
sprintf(merkkijono,"...",temperature);
System_printf(merkkijono);
System_flush();
}
else {
System_printf("I2C Bus fault\n");
System_flush();
}
// Taski nukkumaan!
Task_sleep(1000000 / Clock_tickPeriod);
}
// i2c-yhteyden sulkeminen, tosin ikuinen silmukka ei koskaan päädy tänne
I2C_close(i2c);
}
int main(void) {
...
// Väylä mukaan ohjelmaan
Board_initI2C();
...
}
Puretaanpas esimerkki..
Alustukset ja käyttöönotto¶
Samoin kuten muidenkin oheiskomponenttien kanssa, RTOS haluaa myös i2c-väylän käyttämiseen omia muuttujiaan. Noh, RTOS on syytä pitää tyytyväisenä. Esittelemme vastaavasti tässä tietorakenteen tyyppiä
I2C_Params
johon väylän asetukset laitetaan. Lisäksi meillä on muuttuja tyyppiä I2C_Transaction
, johon koostamme lähetettävät i2c-viestit. Huomataan, että muuttujat ovat tässä taskin
sensorTask
sisällä. Nyt ideana onkin että tämä taski hoitaisi ohjelmassa kaiken i2c-liikenteen anturien kanssa. Void sensorTask(UArg arg0, UArg arg1) {
...
// RTOS:n i2c-muuttujat
I2C_Handle i2c;
I2C_Params i2cParams;
I2C_Transaction i2cMessage;
...
}
Sitten i2c-väylä alustetaan ohjelmamme käyttöön
main
-funktiossa kutsulla Board_initI2C
. #include <ti/drivers/I2C.h>
...
int main(void) {
...
Board_initI2C();
...
}
Väylä avataan taskissa
I2C_open
-kutsulla, jonka parametriksi sen pinnin id, missä sensori on kiinni, tässä siis vakio Board_I2C_TMP
. SensorTagissa on myös toinen i2c-väylä omassa eri pinnissään, mutta tästä lisää myöhemmin..Void sensorTask(UArg arg0, UArg arg1) {
...
i2c = I2C_open(Board_I2C_TMP, &i2cParams);
if (i2c == NULL) {
System_abort("Error Initializing I2C\n");
}
...
}
Kommunikointi i2c-väylällä¶
Esimerkissä kysymme siis lämpötila-anturilta TMP007 mittausarvoja. Alla tätä varten laadittava i2c-viestirakenne.
...
// i2c-viesteille lähetys- ja vastaanottopuskurit
uint8_t txBuffer[1]; // Nyt lähetetään yksi tavu
uint8_t rxBuffer[2]; // Nyt vastaanotetaan kaksi tavua
// i2c-viestirakenne
i2cMessage.slaveAddress = Board_TMP007_ADDR;
txBuffer[0] = TMP007_REG_TEMP; // Rekisterin osoite lähetyspuskuriin
i2cMessage.writeBuf = txBuffer; // Lähetyspuskurin asetus
i2cMessage.writeCount = 1; // Lähetetään 1 tavu
i2cMessage.readBuf = rxBuffer; // Vastaanottopuskurin asetus
i2cMessage.readCount = 2; // Vastaanotetaan 2 tavua
...
Aluksi määritämme puskurit (lue: viestien säilytyspaikat) taulukkoina, jotka asetamme tietorakenteen jäseniksi:
- Lähetyspuskuri
txBuffer
, jonka koko kerrotaan datakirjassa (ok, meillä kurssimateriaalissa). Tyypillisesti lukuoperaatiossa tarvitsee lähettää ainoastaan sen rekisterin osoite, jonka arvon haluamme lukea. - Esimerkissä asetamme TMP007-anturin datarekisterin osoitteen lähetyspuskuriin
txBuffer[0] = TMP007_REG_TEMP;
käyttäen kirjastoissa annettua vakiota. - Vastaanottopuskuri
rxBuffer
, jonne vastaanotettu i2c-viesti tallennetaan. Vastaavasti puskurin koko kerrotaan datakirjassa. - Kurssimateriaalista tiedämme, että TMP007-anturi palauttaa lämpötilan arvon 16-bittisenä lukuna, joten vastaanottopuskurin koko on kaksi tavua.
Seuraavaksi täytetään i2c-viestirakenne. Eli meidän täytyy asettaa puskurit ja viestin lähetystä ja vastauksen vastaanottoa varten i2c-viestirakenteeseen:
i2cMessage.writeBuf = txBuffer; // Lähetyspuskurin asetus
i2cMessage.writeCount = 1; // Lähetetään 1 tavu
i2cMessage.readBuf = rxBuffer; // Vastaanottopuskurin asetus
i2cMessage.readCount = 2; // Vastaanotetaan 2 tavua
Hox! On myös mahdollista laatia i2c-viestejä, jotka eivät vastaanota mitään dataa, kuten jotkut komennot. Tällöin jätetään
readBuf
- ja readCount
-kentät määrittelemättä. Kun viestin tietorakenne on täytetty asianmukaisesti, voimme lähettää itse viestin väylälle
I2C_transfer
-funktiokutsulla. if (I2C_transfer(i2c, &i2cMessage)) {
// Muunnetaan 2-tavuinen data rxBuffer:ssa
// lämpötilaksi (kaava harjoitustehtävissä)
temperature = ...;
// Lämpötila-arvo tiedoksi konsoli-ikkunaan
sprintf(debug_msg,"...",temperature);
System_printf(debug_msg);
System_flush();
}
else {
System_printf("I2C Bus fault\n");
System_flush();
}
...
Jos viesti onnistuu, anturin datarekisterin 16-bittinen arvo on nyt tallentunut vastaanottopuskurin
rxBuffer
kahteen tavuun. Ohjelmoijan tehtäväksi jää muuntaan rxBuffer
:n arvo lämpötilaksi. Kaava tähän on esitetty harjoitustehtävissä ja myöhemmässä kurssimateriaalissa. Jos viesti ei jostain syystä mene perille, tai tapahtui jotain muuta ongelmaa, tulostetaan tästä virheilmoitus konsoli-ikkunaan. i2c-yhteyden sulkeminen¶
Ohjelmassa voi tulla tarve sulkea i2c-yhteys. Esimerkiksi kun otetaan käyttöön SensorTagin toinen i2c-väylä sitten siellä harjoitustyössä.
...
I2C_close(i2c);
...
Tässä nyt jos emme muista sulkea yhteyttä ja se jää päälle, niin toisen väylän käyttö ei onnistu, koska i2c-resurssit pysyvät varattuna tälle yhteydelle. Palataan toisen i2c-väylän käyttöön myöhemmässä luentomateriaalissa.
Lopuksi¶
Sarjaliikenne sulautettujen maailmassa on huomattavan monimutkainen asia, useine eri protokollineen ja toteutuksineen, mutta tämän esityksen tiedoilla pääsemme alkuun SensorTagin anturien ohjaamisessa ja niiden datan lukemisessa.
i2c-protokollan lisäksi SPI on toinen tunnettu sarjaliikenneprotokolla sulautettujen maailmassa. Protokollan valinta riippuu tietysti komponenttivalmistajan tekemistä ratkaisuista.
Esimerkeissä ei otettu ollenkaan kantaa lämpötila-anturin asetuksiin säätämiseen tai anturin kalibrointiin, koska tiedämme, että RTOS:n firmiksen ajuri alustaa anturin puolestamme. Tätä toimintaa voi tietysti muuttaa omalla koodilla, kunhan tietää mitä tekee rekisterien ja bittioperaatioiden kanssa.