Sarjaliikenne¶
Sarjaliikenne tarkoittaa latteiden tai komponenttien keskinäisessä tiedonsiirrossa käytettäviä tekniikoita, joissa data siirtyy sarjamuodossa eli peräkkäin bitti kerrallaan. Toinen vaihtoehto olisi rinnakkainen tiedonsiirto, esim. tietokoneen osoite- ja dataväylät, joissa bitit siirtyvät yhtaikaa samanaikaisesti omia linjojaan pitkin. Rinnakkainen tiedonsiirto on siis nopeampaa kuin sarjamuotoinen, mutta koska sulautetuissa laitteissa käytössä on rajattu määrä I/O-pinnejä, niin sarjaliikenne on taloudellisempaa.
Tieto siirretään datalinjoja pitkin laitteelta toiselle bittijonona, jolloin molempien päiden tulee tietää kaikkien bittien tarkoitus, ts. tiedonsiirtoprotokolla ja käytetty siirtonopeus. Protokollan viestikehyksen sisällä siirrämme varsinaisen datan. Tiedonsiirtoprotokollat ovat yleensä standardoituja, joka auttaa (eri valmistajien) erilaisia laitteita kommunikoimaan keskenään. Siirtonopeus taas määrittää sen mikä on jokaisen bitin ajallinen pituus 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, tai missä kohti signaalia bitin arvo luetaan. Sarjaliikennelinjoja voi olla yksi, jolloin samaa linjaa käytetään vuorotellen lähetykseen ja vastaanottoon, kaksi erilliset linjat lähetykseen ja vastaanottoon, ja lisäksi voi olla kellolinja joka synkronoi tiedonsiirron molemmissa päissä.
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.
Tällä kurssilla emme perehdy sarjaliikenteen salaisuuksiin syvemmin ja erilaisia enemmän tai vähemmän standardoituja toteutuksia onkin paljon. Esittelemme 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, kommunikaatiossa oheislaitteen kanssa. Piirin avulla voimme toteuttaa esim. RS-232 Standardin mukaista tiedonsiirtoa. UART on yleiskäyttöinen vanhahko tekniikka, mutta sulautetuissa järjestelmissä yleisesti nykyään tätä protokollaa käytetään tekstimuotoiseen kommunikointiin kaksisuuntaisesti. Esimerkiksi laite voi lähettää informaatiota työasemalle / oheislaitteelle ja vastaanottaa informaatiota työasemalta / oheislaitteelta. Esimerkki tälläisestä oheislaitteesta on GPS-vastaanotin, joka lähettää mm. koordinaattitietoa UART:n avulla ihmisen luettavassa tekstimuodossa. Toinen esimerkki voisi olla ohjelmoijan toteuttama pieni komentotulkki UART-pohjaisena sarjaliikenteenä PC:n ja laitteen välillä. Mutta, UART:ia voidaan käyttää tietenkin binäärimuotoisen ("numeroiksi koodatun") datan siirtämiseen. Tällöin UART-sarjaliikenne käy jopa sulautetun laitteen ohjelmointiin, ts. kun siirrämme käännetyn ohjelmakoodin työasemalta laitteen ohjelmamuistiin.
Menemättä liialti protokollan toteutukseen, todetaan että UART-pohjaiselle sarjaliikenteelle tulee asettaa muutama parametri:
- Nopeus (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ä: 8n1. Kun 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ää terminaaliohjelman avulla kommunikointiin laitteen kanssa.
TI-RTOS UART¶
Seuraavaksi käymme läpi esimerkin avulla miten RTOS:n
UART-kirjastoa
käytetään. Taskissa ensin alustetaan sarjaliikenne (9600,8n1) ja sitten kaiutetaan (lähetetään) takaisin merkkijono, jossa on käyttäjän syöttämä merkki.#include <string.h>
...
#include <ti/drivers/UART.h>
...
// Taskifunktio
Void serialTask(UArg arg0, UArg arg1) {
char input;
char viesti[20];
// UART-kirjaston asetukset
UART_Handle uart;
UART_Params uartParams;
// Alustetaan sarjaliikenne halutusti
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 sarja"porttiin"
uart = UART_open(Board_UART0, &uartParams);
if (uart == NULL) {
System_abort("Error opening the UART");
}
// ikuinen elämä
while (1) {
// Luetaan (=vastaanotetaan) 1 merkki input-muuttujaan
UART_read(uart, &input, 1);
// Kirjoitetaan (=lähetetään) merkkijono takaisin
// UART_write(uart, &input, 1);
// (Tässä nyt halutaan demota sprintf-funktiota)
sprintf(viesti,"Received: %c",input);
UART_write(uart, viesti, strlen(viesti));
}
}
int main(void) {
Board_initGeneral();
// Otetaan sarjaportti käyttöön ohjelmassa
Board_initUART();
...
}
Ensin esimerkissä tuttuun tapaan määritellään kahva ja alustetaan parametrit
UART_Params
-tyyppiseen rakenteeseen. Parametreissä uutta on read / writeDataMode
, joka kertoo minkätyyppistä dataa sarjaliikenteestä odotetaan, vaihtoehdot ovat UART_DATA_TEXT
tai UART_DATA_BINARY
. Tekstidata tarkoittaa siis ihmisen luettavaa dataa, eli tyypillisesti ASCII-muotoisia merkkijonoja. Näiden merkkijonoja käsittelemme sitten string.h
-kirjaston funktioilla. Jos sarjaliikenteen yli siirretään binääridataa, niin luettu data käsitellään binäärilukuina, eli siis numeroarvoina eikä ASCII-merkkeinä. Numeroarvojen tulkitsemiseen meidän tulisi sitten laatia jäsentäjäfunktio (engl. parser). Sarjaliikenne avataan kutsulla
UART_Open
, jonka argumentteina ovat kahva sarjaporttiin, muuttuja johon vastaanotettu data tallennetaan ja luku joka kertoo kuinka paljon dataa odotamme portista saavamme. Oletuksena sarjaliikenne on blokkaavaa, eli uartParams.readMode=UART_MODE_BLOCKING
. Tämä tarkoittaa sitä, että UART_read
-kutsu odottaa kunnes dataa on saatavilla argumenttina annettu määrä, tässä yksi tavu. Kirjasto mahdollistaa sarjaliikenteen toteuttamisen myös keskeytyksiin perustuen, jolloin ei tarvitsisi odottaa kun keskeytyssignaali kertoisi milloin dataa olisi saatavilla. Mutta siitä lisää tulevassa materiaalissa. Luettu data otetaan talteen muuttujaan input
, joka tässä on tyyppiä char
koska otetaan vastaan yksi merkki kerrallaan. Huomatkaa osoitin-muoto. Jos halutaan vastaanottaa useampi merkki, pitää parametrinä olla taulukko. Nyt jos emme tiedä kuinka paljon dataa on tulossa, voimme ottaa sitä talteen merkki kerrallaan ja analysoida onko vastaanotettu jokin protokollassa sovittu lopetusmerkki, esimerkiksi rivinvaihto.Ja koska ohjelmamme vastaa, lähetämme takaisin merkkijonon
viesti
, jossa on tekstiä ja vastaanotettu merkki UART_Write
-funktiolla. (UART:n asetuksissa parametri readEcho
tarkoitaa sitä, että kaiutetaanko lähetetty data takaisin lähettäjälle. Eli näkyviin terminaaliohjelmassa. No tässä esimerkissä ei sitä tehdä automaattisesti vaan ohjelmallisesti esimerkin vuoksi..) Lopuksi sarjaliikenneyhteys pitää sulkea
UART_Close
-kutsulla, mutta yllä sitä ei ole, koska toimimme ikuisessa toistorakenteessa.i2c¶
i2c-väylä on toinen tunnettu sarjaliikenneprotokolla sulautettuihin laitteisiin ja myös työasemiin. i2c-väylässä on aina yksi master ja n kpl slaveja. Master aloittaa kommunikaation ja asettaa tiedonsiirtonopeuden, jota yksi tai useammat slave-laitteet seuraavat. i2c perustuu kahden I/O-pinnin käyttöön: kelloon (looginen nimi SCL) joka tahdistaa signaalia ja datalinjaan (looginen nimi SDA).
Väylässä laitteet/komponentit yksilöidään osoitteilla. Tämän osoitteen on komponentin valmistaja etukäteen määrittänyt ja sen löydämme datakirjasta. Joskus tosin valmistaja tarjoaa 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. Samassa i2c-väylässä voi olla kytkettynä maksimissaan 1008 laitetta.
i2c-viestit koostuvat kahdesta osasta
- Vastaanottajan osoitteesta, jonka pituus on yleensä yksi tavu (josta 7-bittiä on varattu osoitteelle)
- Sisältö, joka voi olla:
- Master lähettää komennon slave:lle. Komento voi olla esim. jokin rekisteriasetus
- Master kysyy slave:lta dataa
- Slave lähettää datan masterille. Datakentän pituus vaihtelee yhdestä useampaan tavuun, laitteen protokollan mukaan.
Alla esimerkki i2c-viestien sisäisestä rakenteesta. Tämä vain tiedoksi, tätä rakennetta ja signalointia ei tarvitse kurssilla osata.
Seuraavaksi tarkastelemme RTOS:n tarjoaman
i2c
-kirjaston käyttöä koodiesimerkin avulla.// i2c-kirjasto mukaan ohjelmaan
#include <ti/drivers/I2C.h>
...
// Vakio: sensorin datarekisterin osoite
// Tämä arvo saadaan datakirjasta (ts. kurssilla annetaan materiaalissa)
// Hox! Tämä arvo ei siis ole sensorin i2c-osoite! Hetki..
#define TMP007_REG_TEMP 0x0003
// Taskifunktio
Void sensorTask(UArg arg0, UArg arg1) {
unsigned int i;
float temperature;
// RTOS:n i2c-muuttujat
I2C_Handle i2c;
I2C_Params i2cParams;
I2C_Transaction i2cTransaction;
// i2c-viesteille lähetys- ja vastaanottopuskurit
// taulukon koko riippu siitä kuinka monta tavua ollaan lähettämässä/vastottamassa
// Nämä arvot on annettu jokaisen rekisterin kohdalta datakirjassa
uint8_t txBuffer[1]; // Nyt lähetetään yksi tavu
uint8_t rxBuffer[2]; // Nyt vastaanotetaan kaksi tavua
// Alustetaan i2c
I2C_Params_init(&i2cParams);
i2cParams.bitRate = I2C_400kHz;
i2c = I2C_open(Board_I2C_TMP, &i2cParams);
if (i2c == NULL) {
System_abort("Error Initializing I2C\n");
}
// Sitten asiaan, tässä luodaan i2c-viesti tietorakenteeseen
txBuffer[0] = TMP007_REG_TEMP; // Laiterekisteri, mihin operaatio kohdistuu
i2cTransaction.slaveAddress = Board_TMP007_ADDR; // No nyt laitteen osoite
i2cTransaction.writeBuf = txBuffer; // Lähetyspuskuri
i2cTransaction.writeCount = 1; // Lähetetään yksi tavu
i2cTransaction.readBuf = rxBuffer; // Vastaanottopuskuri
i2cTransaction.readCount = 2; // Vastaanotetaan kaksi tavua
for (i = 0; i < 10; i++) {
// Lähetetään yllämääritelty i2c-viesti
if (I2C_transfer(i2c, &i2cTransaction)) {
// Ok, saatiin vastaus
// Muunna luettu data rxBufferista lämpötilaksi (datakirjassa kerrotun mukaisesti)
temperature = ...;
System_printf("Lämpötila on %.2f C\n", temperature);
System_flush();
}
else {
System_printf("I2C Bus fault\n");
}
// Taskimme on kohtelias
Task_sleep(1000000 / Clock_tickPeriod);
}
/* Sulje i2c-yhteys */
I2C_close(i2c);
}
int main(void) {
...
Board_initGeneral();
Board_initI2C();
...
BIOS_start();
return (0);
}
Tässä esimerkissä siis luemme SensorTag:iin integroidulta lämpötila-anturilta TMP007 mitatun lämpötilan kymmenen kertaa. Kuten huomaamme, on anturin datakirja täynnä monenlaista vaikeaselkoista informaatiota, meitä kuitenkin kiinnostaa vain i2c-protokollan ja anturin sisäisten rekisterien käyttö (kappale 7.5. datakirjassa).
Ensiksi meidän tarvitsee tietää anturin i2c-osoite. Koska anturi on jo integroitu SensorTagiin, tarjoaa RTOS meille vakioidun osoitteen otsikko-tiedostossa Board.h. Osoite on siis määritelty vakioon
Board_TMP007_ADDR
. Toiseksi meidän täytyy selvittää, missä anturin rekisterissä mitattu lämpötila-arvo sijaitsee. Datakirjasta (kappale 7.5.4) tai no meidän tapauksessa luentomateriaalista selviää, että rekisterin muistiosoite on 0x003, joten koodin luettavuuden vuoksi määrittelemme sille vakion TMP007_REG_TEMP
. Rekisterin kuvauksesta näemme myös, että kysely-komento tarvitsee yhden tavun, ts- tämä on lähetyspuskurin koko, ja lämpötila-datan pituus on 16 bittiä, eli nyt vastaanottopuskurin koko on kaksi tavua. Koodiesimerkki on jo meille muuten tuttua, mutta main-funktiossa emme unohda alustaa i2c-kirjastoa kutsulla
Board_initI2C
. Taskissa
sensorTask
käytämme meille uusia i2c-kirjaston ominaisuuksia. I2C_Params
-rakenteessa määrittelemme tiedonsiirron kellon nopeuden, tässä 400kHz, valmiilla vakiolla. I2C_open
-kutsulla avaamme yhteyden valittuun i2c-pinniin, tässä valmiiksi määritelty pinnin Board_I2C_TMP
, johon anturi on kytketty. Tarkastellaan seuraavaksi i2c-viestin rakennetta.
uint8_t txBuffer[1];
uint8_t rxBuffer[2];
I2C_Transaction i2cTransaction;
...
txBuffer[0] = TMP007_REG_TEMP; // Laiterekisteri, mihin operaatio kohdistuu
i2cTransaction.slaveAddress = Board_TMP007_ADDR; // No nyt laitteen osoite
i2cTransaction.writeBuf = txBuffer; // Lähetyspuskuri
i2cTransaction.writeCount = 1; // Lähetetään yksi tavu
i2cTransaction.readBuf = rxBuffer; // Vastaanottopuskuri
i2cTransaction.readCount = 2; // Vastaanotetaan kaksi tavua
i2c-viestin rakentamisessa hyödynnämme
I2C_Transaction
-tietorakennetta, johon asetetaan anturin i2c-osoite i2cTransaction.slaveAddress
sekä esittelemämme tiedonsiirtopuskurit ja niiden pituudet. Koodissa olemme esitelleet kaksi puskuria, joihin tallennamme i2c-viestin halutun rekisterin osoitteen txBuffer
ja vastaanotetun viestin rxBuffer
. Tässä esimerkissä lähetämme puskurista viestinä yhden tavun i2cTransaction.writeCount = 1
. Joka nyt tässä tapauksessa on vakiolla esitelty laiterekisteri TMP007_REG_TEMP
. Asetamme myös vastaanottopuskurin i2cTransaction.readBuf = rxBuffer
, johon haluamme vastaanottaa 2 tavua dataa i2cTransaction.readCount = 2
. Vastaanottopuskurin koko tulee ym. anturin rekisterin pituudesta, joka oli kaksi tavua. On myös mahdollista laatia i2c-viestejä jotka eivät vastaanota mitään dataa, tällöin jätetään
readBuf
- ja readCount
-kentät määrittelemättä. Funktiolla
I2C_Transfer
lähetämme viestin väylälle. Jos viesti onnistuu, TMP007_REG_TEMP rekisterin arvo 16-bittisenä lukuna on nyt tallennettu puskuriin rxBuffer. Puskuri-taulukkomuuttujia voimme sitten käyttää koodissa halutulla tavalla. Lopuksi tarvitsemme datakirjasta tietoa siitä, miten vastaanotetut tavut muutetaan lämpötila-arvoksi (datakirjan kappaleet 7.5.4 ja 7.3.7) tai luentomateriaali.
Lopuksi muistetaan sulkea i2c-yhteys
I2C-Close
-kutsulla.Kurkistus SensorTagiin¶
Alla kuvassa kurssilla käyttämämme mikrokontrollerin (TI Simplelink CC2650 Wireless MCU) toiminnallisuuksien lohkokaavio. Kuten nähdään, CC2650 on varsin monipuolinen ja -mutkainen mikrokontrolleri. Siihen on jopa integroitu kaksi erillistä ARM Cortex-tuoteperheen ydintä (ts. CPU) joilla on kummallakin mm. omaa muistia. Tehokkaampaa (Main CPU, Cortex M3) käytetään käyttäjän ohjelmien suorittamiseen ja toinen (Rf Core, Cortex M0) on varattu pelkästään langattoman tiedonsiirtoon. Näin myös erittäin nopeaa langatonta tiedonsiirtoa (2.4GHz) varten saadaan riittävästi tehoja, kuitenkaan syömättä tehoa käyttäjien ohjelmilta ja muulta I/O:lta.
Laite on suunniteltu siten, että molempia ytimiä ohjelmoidaan erikseen. Nyt etuna on se, että molempia toimintoja voidaan ohjelmoida toisistaan riippumatta. Esimerkiksi, laite voidaan muuttaa käyttämään toista langatonta tiedonsiirtoteknologiaa koskematta käyttäjän ohjelmiin. CCS-kehitysympäristö tarjoaa työkalut molempien ytimien ohjelmointiin erikseen, mutta tähän emme kurssilla puutu.
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.
Tässä esityksessä ei otettu ollenkaan kantaa lämpötila-anturin asetuksiin tai laitetason kalibrointeihin, koska tiedämme, että RTOS:n ajuri hoitaa alustaa anturin puolestamme. Sen toimintaa voi tietysti muuttaa omalla koodilla, kunhan tietää mitä tekee (ts. lukee datakirjan lähes kannesta kanteen). Usein meidän tulee myös itse kalibroida sensori sen toimintaympäristöön.