17. MSP430G2231 AD-muunnin

Alunperin julkaistu: 12.2.2017

Viimeksi muokattu: torstai 11.6.2020

Tämän osan aiheena on mikrokontrollerin AD-muuntimen käyttö ja kuinka esimerkiksi potentiometrin arvoa voidaan lukea muuttujan arvoksi ja miten siitä saadaan jännite laskettua. Tämä toimii seuraavassa osassa edellytyksenä mm. moottorinohjaus sovellukselle, missä moottorin pyörimisnopeutta säädetään suoraan suhteessa AD-muuntimen arvoon.

Ennen esimerkkiä ja sovellusta käydään läpi muutama asia MSP430G2231:n AD-muuntimesta, jotta tiedetään miten sitä voidaan käyttää. Jos analoginen signaali tai AD-muunnin ei vielä ole käsitteenä tuttu, niin kannattaa lukaista alla olevat linkit.

Analoginen signaali
AD-muunnin

Yleistä

Kyseisessä mikroprosessorissa on sisäänrakennettu 10-bitin SAR (engl. Successive Approximation ADC) AD-muunnin. ADC-moduuliin on myös integroitu piirin sisäiset referenssijännitteet (1,5 ja 2,5 V), 8-kanavaa, sisäinen lämpötila-anturi ja kanavat käyttöjännitteen muunnokselle ja ulkoisille referenssijännitteille. Listaan kuuluu vielä joukko muitakin ominaisuuksia, jotka löytyvät käyttöohjeen sivulta 548, kappaleesta 22.1 ADC10 Introduction.

AD-muuntimen toiminta lyhyesti

AD-muunnin konfiguroidaan toimintaan ohjelmallisesti ja AD-muuntimen ytimen toimintaa ohjaavat luonnollisesti rekisterit. AD-muuntimen ydin tuottaa muuntimen tuloksena 10-bittisen luvun, joka voidaan lukea ADC10MEM -rekisteristä.

AD-muunnin voidaan ohjelmoida käyttämään useita eri jännitereferenssejä, mihin muuntimen tulos perustuu ja tulos saadaan halutessaan 2:n komplementtilukuna. AD-muuntimen luku normaalimuodossa saadaan kaavasta:

AD-muunnin tarvitsee toimiakseen kellosignaalin ja tämä kellosignaali voidaan ottaa SMCLK, MCLK, ACLK ja ADC10OSC -kellolinjoista. AD-muuntimen toimintaa säätelee kaksi rekisteriä: ADC10CLT0 ja ADC10CTL1 ja AD-muuntimen ydin saadaan käynnistettyä ADC10ON bitillä.

Lyhyen esittelyn jälkeen on aika tehdä kytkentä ja toteuttaa ohjelma, joten ei muuta kuin tuumasta toimeen.

Analoginen jännite potentiometrillä

Kuten alussa mainittiin, niin tässä osassa käytetään potentiometriä tuottamaan muuttuva jännite väliltä 0 - 3,6 V (Launchpadilla käyttöjännite VCC on ~3,6 volttia). Potentiometri voi olla toki muukin kuin tässä käytetty 200 kΩ. Tämä on valittu siksi koska muuta sopivaa ei nyt äkkiseltään sattunut löytymään. Alla siis kytkentä ja kuva koekytkennästä:

Kuten näkyy, niin emme tarvitse muuta kuin potentiometrin, jonka liuku (yleensä keskimmäinen nasta) kytketään porttiin P1.1. P1.1 on myös A1 -input, eli analogia-input numero 1. A1:stä käytetään sen vuoksi, että voimme käyttää Launchpadin ledejä ohjelmassa hyväksi. Alla olevassa kuvassa on näytettynä kaikkien porttien eri moodit ja voit sieltä huomata, että analogia-inputteja on MSP430G2231 mikrokontrollerilla 8 kappaletta.

Esimerkki 1: AD-muuntimen alustukset

Mikrokontrolleri täytyy yllä olevan kytkennän tapauksessa alustaa ottamaan käyttöön AD-muuntimen ydin, kytkemään A1 sisäisesti käyttöön ja asettaa AD-muuntimen kellot oikein. Muunnoksia täytyy tietenkin myös ottaa. Siksipä seuraavassa on esitetty AD-muuntimen alustuksen koodi, joka on kirjoitettu erilliseen funktioon nimeltä AdcInit. Toisena funktiona on itse muunnoksen luku, joka on kirjoitettu myös erilliseen funktioon nimeltä GetAdcValue.

Muunnin alustetaan ottamaan muunnoksia yksi kerrallaan, aina erillisestä komennosta. Kellolähteenä käytetään samaa kelloa kuin CPU:llekin, eli MCLK:a. Muunnosajaksi asetetaan reilu määrä kellojaksoja ja referenssijännitteeksi piirin käyttöjännite. Alla koodi:

void AdcInit(void)
{
     // asetetaan P1.1 -> analog 1-kanavaksi 
     ADC10AE0 = BIT1;

     // Oletuksena referenssijännitteet ovat Vcc (1) ja Vss (14) 
     // Asetetaan näytteistysaika 64 kellojaksoa ja laitetaan AD-muuntimen ydin päälle 
     ADC10CTL0 = ADC10SHT_3 + ADC10ON;

     // Asetetaan kanava 1, valitaan kellosignaaliksi MCLK (kalibroidaan 1 Mhz:iin) ja otetaan 
     // muuntimelta yksi muunnos kerrallaan
     DC10CTL1 = INCH_1 + ADC10SSEL_2 + CONSEQ_0;

     // muunnosaika on 64 * (1 Mhz^-1) = 64 us 

}

Kun alustusrutiini on kirjoitettu, voidaan luoda funktio joka palauttaa AD-muuntimelta saadun mittausarvon. Funktion tulee asettaa ENC (Enable Conversion) ja ADC10SC (ADC10 Start Conversion) -bitit päälle, jotta muunnos alkaa tapahtumaan. AD-muunninta täytyy odottaa niin kauan, että muunnos on valmis ja sitä ilmaistaan ADC10IFG -bitillä. Bittiä tutkimalla saadaan selville milloin ohjelma voi edetä. Koodi alla:

int GetAdcValue(void)
{
     // käynnistää muuntimen ja odottaa että muunnos on valmis 
     ADC10CTL0 |= ENC + ADC10SC; // muunnin päälle ja käynnistetään muunnos 

     // odotetaan että keskeytyslippu asettuu -> muunnos valmis 
     while( ! (ADC10CTL0 & ADC10IFG));

     ADC10CTL0 &= ~ADC10IFG; // nollataan lippu 

     return ADC10MEM; // palautetaan mitatun kanavan arvo 
}

Alustus- ja muuntimen lukufunktioiden kirjoituksen jälkeen voidaan luoda käytännön esimerkki muuntimen käytöstä.

Esimerkki 2: Jännitevahti

Tässä esimerkissä hyödynnetään yllä olevaa kytkentää ja kirjoitettuja funktioita. Tämän esimerkin tarkoituksena on näyttää potentiometrin avulla, kuinka esimerkiksi jännitevahti voidaan MSP430G2231 mikrokontrollerin AD-muuntimella toteuttaa.

Ohjelmassa hyödynnetään aiemmissa osissa tutuksi tulleita kiteen asetuksia. Kellon asetukset on kirjoitettu tällä kertaa vain omaan funktioonsa.

Pääohjelma lukee muuntimen arvoa ja vertailee onko jännite alle 1,6 volttia. Jos jännite on alhaisempi kuin 1,6 volttia niin punainen LED asetetaan päälle ja vihreä sammutetaan. Muussa tapauksessa vihreä LED loistaa ja punainen on sammuneena. Koodi alla:
#include <msp430g2231.h>

// Ohjelman käyttö tavuina (byte) Flash/RAM = 808/18
// apumakroja käytetään helpottamaan koodin kirjoitusta
#define INIT_LAUNCHPAD_LEDS (P1DIR |= BIT0+BIT6)
#define LED_V_ON (P1OUT |= BIT6)
#define LED_V_OFF (P1OUT &= ~BIT6)
#define LED_P_ON (P1OUT |= BIT0)
#define LED_P_OFF (P1OUT &= ~BIT0)

// AD-muuntimen funktioiden esittelyt
void AdcInit(void);
int GetAdcValue(void);

// Kellon asetusfunktio
void ClockInit(void);

void main(void)
{
static int n = 0; // AD-muuntimen arvo haetaan tähän muuttujaan
static float voltit = 0.0; // voltit lasketaan ja talletetaan tähän muuttujaan
WDTCTL = WDTPW + WDTHOLD; // watchdog pysäytys
INIT_LAUNCHPAD_LEDS; // Ledien alustus
LED_V_OFF; // LED1 ja LED2 pois päältä
LED_P_OFF;

ClockInit(); // alustetaan CPU:n kello
AdcInit(); // alustetaan AD-muunnin

while(1)
{
n = GetAdcValue(); // otetaan jatkuvasti uusia muunnoksia
voltit = (n * 3.6)/1023;
if(voltit < 1.6)
{
/ --- "Jännite virhe" ---
LED_P_ON; // punainen led päälle
LED_V_OFF; // vihreä led pois päältä
}
else
{
// --- "Jännite OK" ---
LED_V_ON; // vihreä led päälle
LED_P_OFF; // punainen led pois päältä
}
}
}

void ClockInit(void)
{
// asetetaan CPU:n kelloksi kalibroitu 1 Mhz
BCSCTL1 = CALBC1_1MHZ;
DCOCTL = CALDCO_1MHZ;

// Kiteelle sisäiset kondensaattorit aktiiviseksi
BCSCTL3 = XCAP_3; // 12,5 pf
}

void AdcInit(void)
{
// asetetaan P1.1 -> analog 1-kanavaksi
ADC10AE0 = BIT1;

// Oletuksena referenssijännitteet ovat Vcc (1) ja Vss (14)
// Asetetaan näytteistysaika 64 kellojaksoa ja laitetaan AD-muuntimen ydin päälle
ADC10CTL0 = ADC10SHT_3 + ADC10ON;

// Asetetaan kanava 1, valitaan kellosignaaliksi MCLK (kalibroidaan 1 Mhz:iin) ja otetaan
// muuntimelta yksi muunnos kerrallaan
ADC10CTL1 = INCH_1 + ADC10SSEL_2 + CONSEQ_0;

// muunnosaika on 64 * (1 Mhz^-1) = 64 us
}

int GetAdcValue(void)
{
// käynnistää muuntimen ja odottaa että muunnos on valmis
ADC10CTL0 |= ENC + ADC10SC; // muunnin päälle ja käynnistetään muunnos

// odotetaan että keskeytyslippu asettuu -> muunnos valmis
while( ! (ADC10CTL0 & ADC10IFG));

ADC10CTL0 &= ~ADC10IFG; // nollataan lippu

return ADC10MEM; // palautetaan mitatun kanavan arvo
}

Ohjelman kulku

Aluksi ohjelma varaa muistista tilaa kahdelle muuttujalle n ja voltit. Voltit -muuttuja on tyypiltään float, eli liukuluku mihin tallennetaan lasketut voltit. Liukulukuja käytetään laskennassa aina silloin, kun kokonaisluvuilla ei voida tai ei ole järkevää laskea. Muuttujien määrityksessä käytetään sanaa static. Static määrittelee, että muuttuja varataan muistista ja että sen arvo säilyy funktion kutsukertojen välillä - vaikka tässä ohjelmassa main-funktiota ei kutsu mikään. Static määritystä tarvitaan jos muuttujien sisältöä halutaan debuggerin avulla tutkia. Muutoin static voidaan jättää pois. (testattu ainoastaan CCS v4.2.4 ja v5)

Muut alustukset ovat jo tuttuja tai selitetty aiemmin. ClockInit() -funktiossa asetetaan CPU:n kellotaajuus 1 Mhz:iin ja otetaan kellokiteelle 12,5 pF:n sisäiset kondensaattorit käyttöön.

Pääohjelman silmukassa luetaan AD-muuntimen arvoa ja suoritetaan vertailu ja ledien asetus.

Muunnin käynnistetään asettamalla ENC ja ADC10SC -bitit '1'-tilaan ADC10CTL0 -rekisterissä. Muunnos kestää hetken aikaa (tarkemmasta ajan laskemisesta joskus myöhemmin) ja sillä välin tarkastetaan while-lausekkeen ehdossa onko muuntimen keskeytyslippu asettunut. Kun muuntimen keskeytyslippu asettuu, sulkujen sisällä testattava ehto saa arvon 1, mutta tämä käännetäänkin huutomerkillä (!) nollaksi, jolloin while-silmukasta poistutaan. Lopuksi nollataan keskeytyslippu ja palautetaan arvo muunnosrekisteristä ADC10MEM.

Ohjelma vie tilaa flash-muistista 808 bytea ja RAM-muistista 18 bytea. Ohjelman tekee suureksi liukulukulaskenta ja muut ei-optimoidut rakenteet. Esimerkiksi samaan rekisteriin tehtävät käskyt eri ajankohtina eivät ole kovin optimaalisia.

Esimerkki 3: Jännitevahti koodin optimointi

Toisinaan koodia tarvitsee optimoida, eli pyrkiä käyttämään mahdollisimman vähän, mutta tehokkaasti käytössä olevia resursseja. Alle olen ikään kuin optimointi-esimerkiksi (vain yhdeksi sellaiseksi) väsännyt pienemmän version samasta ohjelmasta, joka käyttää laskennassa ns. fixed point -matematiikkaa ja ohjelman rakenne on muutettu hieman erilaisemmaksi. Ohjelman toiminta on kuitenkin täysin samanlainen ja ledit vilkkuvat kuten aiemmin.

#include <msp430g2231.h>
// Ohjelmamuistin koko tavuina (byte) Flash/RAM = 152/2
// apumakroja käytetään helpottamaan koodin kirjoitusta 

#define INIT_LAUNCHPAD_LEDS (P1DIR |= BIT0+BIT6)
#define LED_OFF (P1OUT = 0)
#define LED_P_ON (P1OUT = BIT0)
#define LED_V_ON (P1OUT = BIT6)

void main(void)
{
     // Tämä ohjelma toimii samoin kuin esimerkissä 2, mutta tässä 
     // koodia on optimoitu. Koodi on tehty n. 81 % pienemmäksi ja vie tilaa 
     // flashista vain 152 bytea ja 2 bytea RAM-muistia. Alla on lueteltu mitä
     // koodiin on tehty. 
     // 
     // - Flash/RAM = 152/2 B 
     // - kaikki funktiot poistettu ja kirjoitettu mainiin 
     // - rekisterikäskyt sievennetty (P1OUT) ja käskyjä yhdistelty (ADC10CTLx) 
     // - laskenta fixed point unsigned int voltit_fxp = 0; 
     // voltit lasketaan fixed point-muodossa 

     WDTCTL = WDTPW + WDTHOLD; // watchdog pysäytys 
     INIT_LAUNCHPAD_LEDS; // Ledien alustus 
     LED_OFF; // LED1 ja LED2 pois päältä 

     // alustetaan CPU:n kello 
     BCSCTL1 = CALBC1_1MHZ;
     DCOCTL = CALDCO_1MHZ;

     // kiteen alustus 
     BCSCTL3 = XCAP_3;

     // asetetaan P1.1 analog 1 -kanavaksi 
     ADC10AE0 = BIT1;

     // Asetetaan kanava 1, valitaan kellosignaaliksi MCLK (1 Mhz) ja asetetaan yksi muunnos
     ADC10CTL1 = INCH_1 + ADC10SSEL_2 + CONSEQ_0;

     while(1)
     {
          // Oletuksena referenssijännitteet ovat Vcc (1) ja Vss (14) 
          // Asetetaan näytteistysaika 64 kellojaksoa ja laitetaan ydin ja muunnos päälle
          ADC10CTL0 = ENC + ADC10SC + ADC10SHT_3 + ADC10ON;

          while( ! (ADC10CTL0 & ADC10IFG));
          // odotetaan että muunnos valmis 

          ADC10CTL0 &= ~ADC10IFG; // nollataan lippu 
          // lasketaan arvo 
          voltit_fxp = ADC10MEM * 35; // 3.6 V / 1023 = 3,51 mV -> 1023 * 35 = 35805 = 3,6 V 

          // vertailu ja ledien asetus 
          if(voltit_fxp < 457) // 457 * 35 = 15995 -> ~1.6 V
          {
               // --- "Jännite virhe" --- 
               LED_P_ON; // punainen led päälle ja vihreä pois 
          }
          else 
          {
               // --- "Jännite OK" --- 
               LED_V_ON; // vihreä led päälle ja punainen pois 
          }
     }
}

Yllä oleva (käsin) optimoitu koodi vie siis flash-muistista 152 tavua ja 2 tavua RAM-muistista - mikä tämmöiseen toimintaan on jo ihan OK! Parannusta on siis 81% alkuperäiseen verrattuna.