Tämän osan aiheena onkin hieman erilaisempi epistola, missä käsittelemme tiedon siirtoa laitteelta toiselle - tässä tapauksessa siis Launchpadilta tietokoneelle. Ennen kuin mennään itse koodin kimppuun, on hyvä käydä asioita yleisellä tasolla lyhyesti läpi.
Jos sarjaliikenne ja sarjaportti on vähemmän tuttu, niin kannattaa vilkaista sarjaliikenne sivua, missä kerrotaan enemmän sarjaportista. Emme käy kaikkia asioita välttämättä pohjamutia myöten läpi, sillä se ei ole mielekästä - tietoliikenne ja varsinkin sarjamuotoinen sellainen on niin laaja käsite, että pienempi pala kakkua kerralla riittää varmasti hyvin.
Yleistä tietoliikenteestä
Tietoliikenne kahden laitteen välillä on tietotekniikassa varsin usein esiintyvä tapahtuma. Esimerkiksi PC eli tavallinen tietokone siirtää tietoa kiintolevyltä välimuistiin tai toiseen tiedon tallennuspaikkaan erilaisten sarjaväylien kautta. Mm. SATA-väylä toimii lähettämällä ja vastaanottamalla bittejä yksi toisensa jälkeen. Niin ikään USB on nimensä mukaisesti sarjaväylä (Universal Serial Bus) ja USB-tikut ovat varmasti monelle tuttuja tiedon tallennusvälineitä.
Kuten yllä olevasta käy ilmi, niin tietoliikennettä tarvitaan siis tiedon siirtoon. Esimerkiksi lämpötilaa, ilman kosteutta ja muita ilmakehän ominaisuuksia mittaava sääasema voi ilmoittaa antureilta havaitut tiedot tietokoneelle, joka tallentaa ne kiintolevyn muistiin. Yhtä hyvin tieto voidaan kyseisessä sääasemassa tallentaa vaikkapa muistikortille tietojen myöhempää tarkastelua varten. Olipa asia kummin päin tahansa, niin tietoliikenne on edellytys laitteen toiminnalle ja tietojen keruulle.
Sarjaportin käyttöön ottaminen PC:ssä
MSP430 mikrokontrollereissa, kuten monissa muissakin mikrokontrollereissa on siis mahdollista siirtää tietoa sarjamuotoisessa formaatissa. Sarjaporttia varten emme tarvitse mitään uutta kytkentää, sillä Launchpadiin on sisällytetty virtuaalisarjaportti, joka toimii Launchpadin USB-kaapelin kautta.
Kun kytket Launchpadin USB-johdolla tietokoneeseen, ilmestyy virtuaalinen sarjaportti tietokoneen laitehallintaan (windows). MSP430 Application UART -nimellä löytyvän sarjaportin numero kannattaa painaa mieleen, sillä sitä tarvitaan terminaaliohjelmassa yhteyden avaamiseen. Alla olevassa kuvassa ko. sarjaportti on COM10 ja sinun tietokoneessa se varmaankin on eri numerolla.
Jotta sarjaporttia voidaan käyttää ja sinne tulostettua tietoa voidaan lukea, niin tarvitaan terminaaliohjelma, joka käsittelee tietokoneen sarjaporttia, luo uuden yhteyden ja hoitaa muut hommat "konepellin alla".
Terminaaliohjelmia on olemassa erilaisia ja tässä esimerkissä olen käyttänyt Tera Term Pro (versio 2.3) nimistä ohjelmaa. Ohjelman lataussivulle on linkki tässä. Mikäli ohjelman käyttö on sinulle tuttua, niin hyvä juttu. Mutta jos ei ole, niin lyhyet ohjeet ohjelman käyttöön tulevat tässä:
1) Lataa ja asenna ohjelmisto
2) Asennuksen jälkeen avaa ttermpro.exe -tiedosto ohjelman asennuskansiosta
3) Ohjelma kysyy auettuaan mikä yhteys avataan (vasen kuva alla) ja valitaan alasvetovalikosta oikea COM-portti. Painetaan OK.
4) Avataan valikosta sarjaportin asetukset Setup -> Serial port... ja valitaan kuvan mukaiset asetukset valitulle COM-portille (oikea kuva alla). Painetaan OK. Nyt virtuaalisarjaportti on valmiina käyttöön.
Mikäli COM-porttia ei näy listassa, niin avaa Tera Term Pro -ohjelman asennuskansio ja avaa tiedosto TERATERM.INI notepad editorilla tai muulla tekstieditorilla. Etsi tiedostosta rivi, missä asetetaan MaxComPort -muuttujan arvo. Muuta siihen arvo, joka vastaa virtuaalisarjaportin COM-numeroa. Esimerkiksi kuvan tapauksessa arvoksi voisi laittaa:
MaxComPort=10
Itse käytän tässä arvoa 255 (maksimi), jolloin portin alasvetovalikossa näkyy 255 COM-porttia. Tallenna tiedosto ja käynnistä terminaaliohjelma uudestaan.
Asetusten tekemisen jälkeen voimmekin keskittyä mikrokontrollerin ohjelmoimiseen. Alla on esitetty tämän luvun ensimmäinen esimerkki sarjaportin käytöstä Timerin avulla.
Esimerkki 1: "Hello World!" sarjaporttiohjelma
Koska MSP430G2231 mikrokontrollerissa ei ole sisäänrakennettua UART-moduulia, täytyy sarjaportti tehdä ohjelmallisesti. Ohjelmallinen sarjaportti toteutetaan Timer A:n avulla ja kellosignaalina käytetään aiemmin asennettua 32,768 kHz:n kellokidettä.
Sarjaportti alustetaan toimimaan 9600 bit/s nopeudella ja sarjaporttiin tulostetaan teksti "Hello World!". Kun sarjaportista lähetetään dataa, niin väläytetään punaista lediä. Sarjaportissa ei käytetä pariteettia ja stop-bittien määrä on yksi. Asetukset notaatiolla merkittynä ovat näin ollen 8N1.
#include <msp430g2231.h> // Ohjelman koko tavuina (byte) Flash/RAM = 434/12 // apumakroja käytetään helpottamaan koodin kirjoitusta #define INIT_LAUNCHPAD_LEDS (P1DIR |= BIT0+BIT6) #define LED_PUN (BIT0) #define LED_PUN_ON (P1OUT |= LED_PUN) #define LED_PUN_OFF (P1OUT &= ~LED_PUN) // sarjaportin määrityksiä #define UART_TX BIT1 // Timer0_A OUT0 #define UART_MASTER_CLK 1000000 // 1 MHz kello SMCLK #define UART_BAUDRATE 9600 // baudinopeus #define UART_BITLENGTH (UART_MASTER_CLK / UART_BAUDRATE) // bitin pituus // globaali data mitä tarvitaan keskeytyksessä unsigned int tx; // tähän ladataan lähetettävä data sekä start että stop-bitit // lähetysfunktio void UART_tx(unsigned char tavu); void main(void) { WDTCTL = WDTPW + WDTHOLD; // watchdog pysäytys INIT_LAUNCHPAD_LEDS; // Ledien alustus // alustetaan CPU:n kello 1 MHz BCSCTL1 = CALBC1_1MHZ; DCOCTL = CALDCO_1MHZ; // kiteen alustus BCSCTL3 = XCAP_3; // SW-UART pinnien alustus P1OUT = 0; P1SEL |= UART_TX; // Timer ohjaa TXD pinniä (P1.1) P1DIR |= UART_TX + LED_PUN; // TX pinni ja punainen LED lähdöksi // Timerin alustus TA0CCTL0 = OUT; // idle-tila "1" TACTL = TASSEL_2 + MC_2; // Kellona SMCLK ja timer laskee jatkuvasti __enable_interrupt(); // sallitaan globaalit keskeytykset while(1) { UART_tx('H'); // Kirjoitetaan 'Hello World' kirjain kerrallaan sarjaporttiin. UART_tx('e'); // Kun halutaan lähettää esim. e-kirjainta vastaava numero UART_tx('l'); // niin se täytyy laittaa heittomerkkien ' sisään. Tällöin kääntäjä UART_tx('l'); // tulkitsee kirjaimen numerona ja välittää sen funktiolle oikein. UART_tx('o'); UART_tx(' '); UART_tx('W'); UART_tx('o'); UART_tx('r'); UART_tx('l'); UART_tx('d'); UART_tx('!'); UART_tx(' '); // rivinvaihto UART_tx(' '); // kelataan rivin alkuun } } // UART-lähetysfunktio void UART_tx(unsigned char tavu) { while(TACCTL0 & CCIE); // odotellaan että entinen tavu on lähetetty TACCR0 = TAR; // nykyinen laskurin tila capture/compare rekisterin arvoksi TACCR0 += UART_BITLENGTH; // päivitetään capture/compare rekisterin arvo TACCTL0 = OUTMOD0 + CCIE; // timer asettaa pinnin OUT-bitin mukaisesti, sallitaan myös keskeytys LED_PUN_ON; // LED päälle (sammutus keskeytyksessä) tx = tavu; // ladataan tavu globaaliin muuttujaan tx |= 0x100; // lisätään stop-bitti "1" lähetettävään dataan tx <<= 1; // lisätään start-bitti "0" lähetettävään dataan // esimerkiksi jos tavu on 0x55 = 0101 0101, niin tästä tulee ensin: // 1 0101 0101 jonka jälkeen siirto ja saadaan: // 10 1010 1010 } //------------------------------------------------------------------------------ // Timer_A UART - Lähetyskeskeytys. // Tämä keskeytys lähettää kaikki bitit muuttujasta tx. Kaikkinensa siis 10 bittiä // lähetetään ja kukin niistä yksi kerrallaan. //------------------------------------------------------------------------------ #pragma vector = TIMERA0_VECTOR __interrupt void Timer_A0_ISR(void) { static unsigned char txCnt = 10; // static estää muuttujan häviämisen kun funktiosta poistutaan // alustus suoritetaan kuitenkin vain kerran TACCR0 += UART_BITLENGTH; // Lisätään aikaa seuraavaan bittiin asti if(txCnt == 0) // tarkastetaan onko kaikki bitit lähetetty { TACCTL0 &= ~CCIE; // kaikki bitit lähetetty, kielletään keskeytys txCnt = 10; // palautetaan muuttujaan alkuperäinen määrä bittejä LED_PUN_OFF; // sammutetaan LED (sytytys UART_tx -funktiossa) } else { if(tx & 1) // tarkastetaan onko lähetettävä bitti 1 vai 0 { TACCTL0 &= ~OUTMOD2; // lähetetään '1' bitti } else { TACCTL0 |= OUTMOD2; // lähetetään '0' bitti } tx >>= 1; // siirretään lähetettävän bitin jonoa oikealle jolloin // lähetettyä bittiä ei enää ole jonossa txCnt--; // vähennetään lähetettävien bittien määrää yhdellä } }
Ohjelman kulku
Ohjelma alustaa ensin porttipinnit, kiteen ja timerin. Kun alustukset on suoritettu, niin pääohjelman silmukassa tulostetaan kirjain kerrallaan "Hello World!" sarjaporttiin, minkä terminaaliohjelma ottaa vastaan. Alla olevassa kuvassa nähdään terminaaliohjelman vastaanottamaa tietoa, joka on juuri kuten pitääkin olla. Erikoismerkit ja ovat yhdessä rivinvaihtomerkki, millä terminaaliohjelma siirtää kursorinsa aina uuden rivin alkuun. Merkit vastaavat siis esimerkiksi notepadiin kirjoittaessa enterin painallusta.
Ohjelman lähetysfunktio UART_tx ottaa parametrina vastaan yhden kirjaimen (tai numeron) ja asettaa sen globaaliin muuttujaan tx. Ennen muuttujan asetusta funktio myös alustaa timerin asetuksia niin, että lähetys onnistuu ilman virheitä ja myös timerin keskeytys sallitaan. Lähetettävä tieto on pohjimmiltaan vain numeroita, joita tietokoneen päässä tulkitaan kirjaimiksi. Itse asiassa kyseessä on ASCII-merkistö, ja kun katsotaan mitä numeroa kirjain (iso hoo) H vastaa ASCII-taulukossa (linkki1, linkki2), niin huomataan että se on desimaalina 72 ja heksalukuna 0x48. Mikäli H-kirjaimen paikalle vaihdetaan luku 72, niin sarjaportista vastaanotetaan yhä sama rimpsu kuin aiemminkin.
Ohjelma lähettää 8-bitin merkit bitti kerrallaan keskeytyksessä ja kun kaikki bitit on lähetetty, nollataan bittien lähetyslaskuri txCnt sekä kielletään keskeytys. Koska timer 'juoksee' koko ajan, niin keskeytys pitää kieltää viimeisen merkin jälkeen jotta ohjelma toimii oikein. Tämän ohjelman voi toki tehdä toisellakin tavalla, mutta tämä koodi on sovellettu TI:n luomasta esimerkkikoodista ja asiat on pyritty pitämään yksinkertaisena.
Kun suoritat ohjelmaa, niin huomaat että punainen LED palaa koko ajan ja terminaaliohjelmaan ilmestyy paljon samaa tekstiä. Tämä johtuu siitä, että ohjelma tulostaa merkkejä sarjaporttiin hyvinkin tiuhaan tahtiin, koska ohjelmaan ei tehty mitään viivettä. Jos asetat debuggerin avulla breakpointin jollekin pääohjelman riville, voit huomata että tulostus pysähtyy kun ohjelma saavuttaa sen.
Voitkin alkaa esimerkkikoodin avulla tutkimaan millaisia merkkejä saat sarjaporttiin tulostettua ja saisiko sinne vaikka hymiön ilmaantumaan? 😉
Voit kokeilla muokata ohjelmaa myös niin, että tulostus tapahtuu vain napin painalluksesta. Lisäksi edellisiä osia hyödyntämällä saat tehtyä itsellesi vielä tästäkin monipuolisempiakin sovelluksia.
Ongelmanratkaisua:
Jos mitään ei terminaaliohjelmassa näy, niin kannattaa koettaa seuraavia asioita:
1) Irroita Launchpad USB-johdosta
2) Sulje CCS
3) Sulje yhteys terminaaliohjelmasta ja sulje vasta sitten terminaaliohjelma
4) Kytke Launchpad uudestaan tietokoneeseen
5) Avaa terminaaliohjelma. Valitse valikosta Control -> Reset Terminal ja Reset port.Ota nyt uusi yhteys oikeaan sarjaporttiin (tarkasta tietokoneen hallinnasta/laitehallinnasta COM-portin numero)
6) Käynnistä CCS ja yritä ajaa ohjelma uudestaan
Mikäli yllä oleva ei millään tehoa, niin vaihda COM-portin numeromääritystä (Ohjeita: linkkivinkki1, linkkivinkki2, linkkivinkki3) jonka jälkeen toista kohdat 1-6.
Esimerkki 2: sarjaporttiin 'printtaus'
Esimerkki 1:ssä oli siis yksinkertainen merkki kerrallaan tulostus sarjaporttiin ja kaikki (toivon mukaan) sujui kuten pitikin. Merkki kerrallaan tulostuksessa ei mitään väärää ole, mutta varsin työlästähän se on jos pitäisi tulostaa vaikkapa 100 merkkiä ruudulle. Tämän vuoksi muokataan edellistä esimerkkiä niin, että sarjaporttiin voidaan tulostaa merkkijonoja pelkkien yksittäisten merkkien sijasta.
Ohjelmaan voidaan helposti yllä mainittu ominaisuus lisätä kirjoittamalla koodiin tulostusfunktio, joka hyödyntää aiemmin tehtyä merkin tulostusfunktiota. Kyseinen koodinpätkä on kirjoitettu alle:
// UART-printtifunktio void UART_print(char *merkkijono) { int i=0; while(merkkijono[i] != '') // tulostetaan niin kauan kunnes tämä merkki (NULL) kohdataan { UART_tx(merkkijono[i]); i++; } }
Tulostusfunktio ottaa vastaan osoittimen merkkijonoon, minkä perusteella se voi tietää mitä kirjaimia täytyy tulostaa. Mikäli et tunne osoittimien käyttöä, niin esimerkiksi wikipediassa on lyhkäisesti kerrottu tästä. Osoitinta ilmaistaan tähdellä ja käytännössä osoitin on vain muistipaikkaan osoittava muuttuja, tässä tapauksessa osoitetaan merkkijonon "Hello World " ensimmäiseen kirjaimeen H. Tämänkin oppaan aihepiiriin kuuluu kertoa osoittimien käytöstä selvemmin, mutta niistä en ole vielä kerennyt kirjoittamaan. Noniin, mutta takaisin funktion toimintaan.
Funktio toimii siis siten, että saadusta merkkijonosta poimitaan ensimmäinen merkki, joka lähetetään UART_tx -funktiolle, tässä tapauksessa siis H-kirjain. Tulostusta jatketaan niin kauan kunnes kohdataan erikoismerkki '' eli NULL-merkki eli 0 (nolla), joka kertoo funktiolle että tulostusta ei enää jatketa. NULL tarkoittaakin aina tyhjää ja nollalla voidaan tämä merkkijonojen kohdalla kätevästi ilmaista.
Kun koodi lisätään, niin pääohjelman pääsilmukan sisältökin muuttuu yksinkertaisemmaksi käsittää. Olen korostanut lisätyt/muokatut koodirivit keltaisella pohjavärillä. Alla koodi kokonaisuudessaan.
#include <msp430g2231.h> // Ohjelman koko tavuina (byte) Flash/RAM = 434/12 // apumakroja käytetään helpottamaan koodin kirjoitusta #define INIT_LAUNCHPAD_LEDS (P1DIR |= BIT0+BIT6) #define LED_PUN (BIT0) #define LED_PUN_ON (P1OUT |= LED_PUN) #define LED_PUN_OFF (P1OUT &= ~LED_PUN) // sarjaportin määrityksiä #define UART_TX BIT1 // Timer0_A OUT0 #define UART_MASTER_CLK 1000000 // 1 MHz kello SMCLK #define UART_BAUDRATE 9600 // baudinopeus #define UART_BITLENGTH (UART_MASTER_CLK / UART_BAUDRATE) // bitin pituus // globaali data mitä tarvitaan keskeytyksessä unsigned int tx; // tähän ladataan lähetettävä data sekä start että stop-bitit // lähetysfunktiot void UART_print(char *merkkijono); // uuden funktion määrittely void UART_tx(unsigned char tavu); void main(void) { WDTCTL = WDTPW + WDTHOLD; // watchdog pysäytys INIT_LAUNCHPAD_LEDS; // Ledien alustus // alustetaan CPU:n kello 1 MHz BCSCTL1 = CALBC1_1MHZ; DCOCTL = CALDCO_1MHZ; // kiteen alustus BCSCTL3 = XCAP_3; // SW-UART pinnien alustus P1OUT = 0; P1SEL |= UART_TX; // Timer ohjaa TXD pinniä (P1.1) P1DIR |= UART_TX + LED_PUN; // TX pinni ja punainen LED lähdöksi // Timerin alustus TA0CCTL0 = OUT; // idle-tila "1" TACTL = TASSEL_2 + MC_2; // Kellona SMCLK ja timer laskee jatkuvasti __enable_interrupt(); // sallitaan globaalit keskeytykset while(1) { // Nyt tarvitaan vain yksi rivi koodia printtifunktion ansiosta! UART_print("Hello World! "); // Kirjoitetaan 'Hello World' sarjaporttiin. // = rivinvaihto = null -merkki } } // UART-printtifunktio void UART_print(char *merkkijono) { int i=0; while(merkkijono[i] != '') // tulostetaan niin kauan kunnes tämä merkki (NULL) kohdataan { UART_tx(merkkijono[i]); i++; } } // UART-lähetysfunktio void UART_tx(unsigned char tavu) { while(TACCTL0 & CCIE); // odotellaan että entinen tavu on lähetetty TACCR0 = TAR; // nykyinen laskurin tila capture/compare rekisterin arvoksi TACCR0 += UART_BITLENGTH; // päivitetään capture/compare rekisterin arvo TACCTL0 = OUTMOD0 + CCIE; // timer asettaa pinnin OUT-bitin mukaisesti, sallitaan myös keskeytys LED_PUN_ON; // LED päälle (sammutus keskeytyksessä) tx = tavu; // ladataan tavu globaaliin muuttujaan tx |= 0x100; // lisätään stop-bitti "1" lähetettävään dataan tx <<= 1; // lisätään start-bitti "0" lähetettävään dataan // esimerkiksi jos tavu on 0x55 = 0101 0101, niin tästä tulee ensin: // 1 0101 0101 jonka jälkeen siirto ja saadaan: // 10 1010 1010 } //------------------------------------------------------------------------------ // Timer_A UART - Lähetyskeskeytys. // Tämä keskeytys lähettää kaikki bitit muuttujasta tx. Kaikkinensa siis 10 bittiä // lähetetään ja kukin niistä yksi kerrallaan. //------------------------------------------------------------------------------ #pragma vector = TIMERA0_VECTOR __interrupt void Timer_A0_ISR(void) { static unsigned char txCnt = 10; // static estää muuttujan häviämisen kun funktiosta poistutaan // alustus suoritetaan kuitenkin vain kerran TACCR0 += UART_BITLENGTH; // Lisätään aikaa seuraavaan bittiin asti if(txCnt == 0) // tarkastetaan onko kaikki bitit lähetetty { TACCTL0 &= ~CCIE; // kaikki bitit lähetetty, kielletään keskeytys txCnt = 10; // palautetaan muuttujaan alkuperäinen määrä bittejä LED_PUN_OFF; // sammutetaan LED (sytytys UART_tx -funktiossa) } else { if(tx & 1) // tarkastetaan onko lähetettävä bitti 1 vai 0 { TACCTL0 &= ~OUTMOD2; // lähetetään '1' bitti } else { TACCTL0 |= OUTMOD2; // lähetetään '0' bitti } tx >>= 1; // siirretään lähetettävän bitin jonoa oikealle jolloin // lähetettyä bittiä ei enää ole jonossa txCnt--; // vähennetään lähetettävien bittien määrää yhdellä } }
Ohjelman toiminta
Ohjelma toimii täysin samoin kuten esimerkissä 1, tai ainakin siltä se näyttää, vaikka sisäisesti ohjelma tekee asioita nyt hieman tehokkaammin ja pienemmällä määrällä koodia. Ohjelman toimintaa ei varmaankaan tämän vuoksi tarvitse enempää selittää.
Toivottavasti sarjaportin käyttö on nyt jotenkuten helpompi aihe ja tästä on hyötyä lukijalle.