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ää kaikkien bittien tarkoitus, ts. tiedonsiirtoprotokolla ja käytetty siirtonopeus. Protokollan viestikehyksen sisällä siirrämme varsinaisen datan. Nämä protokollat 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.
Toinen vaihtoehto tiedonsiirrolle 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.
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. 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.
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ä.
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ä, eli 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.
i2c SensorTag:ssa¶
Tarkastelemme RTOS:n tarjoaman
i2c
-kirjaston käyttöä koodiesimerkin avulla. Esimerkissä 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). // 1. Alustukset
// i2c-kirjasto mukaan ohjelmaan
#include <ti/drivers/I2C.h>
// Taskifunktio
Void sensorTask(UArg arg0, UArg arg1) {
unsigned int i;
float temperature;
// 2. i2c-väylän alustus
// RTOS:n i2c-muuttujat
I2C_Handle i2c;
I2C_Params i2cParams;
I2C_Transaction i2cTransaction;
// 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");
}
// 3. Kommunikointi i2c-väylän kautta
// i2c-viesteille lähetys- ja vastaanottopuskurit
// Taulukkojen 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
// Luodaan i2c-viesti tietorakenteeseen
// Vakio: sensorin DATAREKISTERIN osoite TMP007_REG_TEMP
// Saadaan datakirjasta, ts. täältä kurssimateriaalista
// Vakio: laitteen i2c-osoite Board_TMP007_ADDR saadaan Board.h-tiedostosta
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);
}
// 4. i2c-yhteyden sulkeminen
I2C_close(i2c);
}
int main(void) {
...
Board_initGeneral();
Board_initI2C();
...
BIOS_start();
return (0);
}
Puretaanpas esimerkki..
1. Alustukset¶
i2c-väylä alustetaan ohjelmamme käyttöön
main
-funktiossa kutsulla Board_initI2C
. // i2c-kirjasto mukaan ohjelmaan
#include <ti/drivers/I2C.h>
...
int main(void) {
...
Board_initI2C();
...
}
2. i2c-väylän käyttöönotto¶
Tässä esimerkissä alustamme i2c-väylän toimimaan yhdessä taskissa
sensorTask
. Taustalla on modulaarisuuden ajatus, eli käytämme yhtä taskia sensoreiden kanssa kommunikoimiseen. Väylä tarvitsee toimiakseen mielikuvituksellisesti nimetyn kahvan i2c
ja lisäksi esitellään kaksi tietorakennetta, eli I2C_Params
-tyypin tietorakenteessa alustamme väylän parametrit, tässä tiedonsiirron kellon nopeuden eli 400kHz. Toista tietorakennetta I2C_Transaction
käytetään tiedonsiirtoon, josta lisää hetken päästä. Väylä avataan tiedonsiirtoa varten I2C_open
-kutsulla, jonka parametriksi sen pinnin id, missä sensori on kiinni. Tässä siis pinnissä 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) {
...
// 2. i2c-väylän alustus
// RTOS:n i2c-muuttujat
I2C_Handle i2c;
I2C_Params i2cParams;
I2C_Transaction i2cTransaction;
// 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");
}
...
3. Kommunikointi i2c-väylällä¶
Seuraavaksi esimerkissä kysymme lämpötila-anturilta mittausarvoja.
...
// i2c-viesteille lähetys- ja vastaanottopuskurit
// Taulukkojen koko riippuu 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
// Luodaan i2c-viesti tietorakenteeseen
// Vakio: sensorin DATAREKISTERIN osoite TMP007_REG_TEMP
// Saadaan datakirjasta, ts. täältä kurssimateriaalista
// Vakio: laitteen i2c-osoite Board_TMP007_ADDR saadaan Board.h-tiedostosta
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
...
// Lähetetään yllämääritelty i2c-viesti
if (I2C_transfer(i2c, &i2cTransaction)) {
// Ok, saatiin vastaus
...
}
else {
System_printf("I2C Bus fault\n");
}
...
}
Aluksi määritämme lähetys- ja vastaanottopuskurit, ts. taulukot, joihin kirjoitamme lähetettävän viestin ja johon vastaanotamme viestin laitteelta, eli lähetykseen
rxBuffer
ja vastaanottoon txBuffer
. Taulukkojen koko, siis montako tavua, selviää luentomateriaalista (myöhemmin..). Nyt vain tiedämme, että lähetettävän viestin koko on yksi tavu ja vastaanotetun kaksi tavua. i2c-viestin rakentamisessa hyödynnämme
I2C_Transaction
-tietorakennetta. Ensimmäiseen ja ainoaan lähetettävään tavuun laitamme laiterekisterin osoitteen TMP007_REG_TEMP
. Viesti siis lukee anturilta annetun rekisterin arvon, joka nyt sattuu olemaan haluttu mittausarvo. Seuraavaksi asetamme tietorakenteeseen anturin i2c-osoitteen i2cTransaction.slaveAddress = Board_TMP007_ADDR;
. Tämän vakion tarjoaa RTOS meille otsikko-tiedostossa Board.h.Datakirjasta (kappale 7.5.4), tai no meidän tapauksessa luentomateriaalista selviää, että anturin kysely-komento tarvitsee yhden tavun, eli tämä on lähetyspuskurin koko, ja lämpötila-datan pituus on 16 bittiä eli nyt vastaanottopuskurin koko on kaksi tavua. Toisinsanoen, lähetämme puskurista
i2cTransaction.writeBuf = txBuffer
yhden tavun i2cTransaction.writeCount = 1
. Asetamme myös vastaanottopuskurin i2cTransaction.readBuf = rxBuffer
ja sille koko i2cTransaction.readCount = 2
. 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ä. Itse viestin lähetys tapahtuu funktiolla
I2C_Transfer
, jolle parametriksi i2c-yhteyden kahva ja täytetty tietorakenne. Jos viesti onnistuu, rekisterin arvo 16-bittisenä lukuna on nyt tallennettu puskuriin rxBuffer
. Puskuri-taulukkomuuttujia voimme sitten käyttää koodissa halutulla tavalla, esim. muuntaa lukuarvoiksi. 4. i2c-yhteyden sulkeminen¶
Taskin suorituksen lopuksi muistetaan sulkea i2c-yhteys. Jos emme muista sulkea yhteyttä, se jää päälle, mikä radikaalisti vaikeuttaa datan lukemista muilta sensoreilta.
...
// 4. i2c-yhteyden sulkeminen
I2C_close(i2c);
...
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 sovittaa, eli kalibroida, sensori sen toimintaympäristöön.