lauantai 23. huhtikuuta 2016

PID-säädin ja liukuluku

Koska kokonaisluvuilla tarkasti laskeminen on hiukan hankalaa, niin ratkaisu ongelmaan on liukuluvut (float). Analogiatulot ja –lähdöt käyttävät kokonaislukuja (tulo 10 bitin muunnos, lukualue 0 .. 1023 ja lähtö 8 bitin muunnos, lukualue 0 .. 255), mutta välissä olevassa signaalin ”jalostuksessa” voidaan käyttää liukulukuja, eli ottaa desimaalit mukaan.
Varsinkin teollisuudessa (erityisesti kemian) on paljon säätöpiirejä. Tuossa PID – termissä P tarkoittaa vahvistusta. Aluksi lasketaan asetusarvon ja mittausarvon erotus, tämä kerrotaan termillä P ja johdetaan säätöpiirin lähtöön. I tarkoittaa integroimista, eli ajan edetessä muutetaan pienin portain (jokin osa erotuksesta) muutetaan säätöpiirin lähtöarvoa joko kasvattaen tai vähentäen, aina tarpeen mukaan, kunnes asetusarvon ja mittausarvon erotus on nolla (0). Tällöin P:n vaikutus on nolla, sillä nolla kertaa jotain on nolla. D-termi tarkoittaa derivointia. Kun säätöpiirin laskenta havaitsee muutoksen erosignaalissa, aiheutetaan lähtöön hetkellinen muutos korjaavaan suuntaan. D-temi tulee kyseeseen lähinnä lämpötilansäädöissä, esim. uunien säädöissä, missä osa energiaa tarvitaan tuotteen lämmittämiseen ja osa itse uunin lämmittämiseen. D-termin vaikutus riippuu muutosnopeudesta ja muutoksen suuruudesta (ja tietysti myös viritysarvosta, eli millainen vaikutus sille on määritelty). Seuraavassa sovelluksessa D-termi jätetään pois.
Asetusarvo annetaan edelleen sarjaliikenteellä, mutta P-termi sekä I-termi syötetään analogiatulojen kautta trimmeripotentiometrien avulla. P-termin lukualue on 0 .. 10 ja I-termi vaikuttaa ajastimen kautta siihen, miten usein lähtöarvoa muutetaan. Jos vahvistus (P) on liian suuri tai integrointiaika (I) liian nopea, alkaa säätöpiiri värähdellä.


Edelliseen kytkinkaavaan tulee muutama lisäys, eli juuri nuo trimmerit.


Melko hyvä nyrkkisääntö säätöpiirin virittämiseksi on ensin saattaa piiri hiukan (siis juuri ja juuri) värähtelemään kasvattamalla vahvistusta. Tämä vahvistuksen arvo on lähtökohta lopulliselle arvolle. Tästä värähtelyn aiheuttamasta arvosta vähennetään 30% ja asetetaan se P:n arvoksi. Toinen talletettava tieto on värähtelyn jakson aika. Se puolitetaan. Jos esim. P saa arvoksi 2,7 on lopullinen arvo 0, 7 * 2,7 = 1,9. Jos värähtelyn jaksonaika on 2 sekuntia, on I:n arvo 1 sekunti, eli sekunnin välein korjataan lähtöä. Nämä säännöt ovat ainakin hyvä lähtökohta säätöpiirin virittämiseen.

Säätäessäni P-arvoa (=vahvistus * (asetusarvo - mittaus)), alkoi LEDi vilkkua noin 51 kohdalla. Tästä arvosta pois 30% antaa tulokseksi ja vahvistuskertoimeksi noin 36. Tosin LEDissä näkyy koko ajan pientä, nopeaa värähtelyä, koska mittausalue on nyt kapeampi; 600 .. 750, merkitsee 5mV A/D-muunnos jo noin 14% muutosta mittaustarkkuudessa. Laajempi värähtely oli kuitenkin selvästi havaittavissa. Värähtelytaajuudeksi arvioin noin 2s. Tuon ajan lyhyydestä johtuen, en ottanut integroinnin päivitysaikaa tarkasta ajastimesta (millis()), vaan muodostin päivitysajan suoraan ohjelmakierroksia laskemalla. Kierrosten lukumäärä 741 näytti antavan varsin hyvän säädön. Tarkkaa aikaa en täten tiedä, eikä sillä ole niin väliäkään, sillä säätämällä aikaa voi havainnoida säätöpiirin käyttäytymistä. Kun tietokoneelta antoi sarjaliikenteellä uuden asetusarvolukeman (SP-arvo = Set Point), muuttui LEDin valo nopeasti jälleen vakaaksi. Valaisin LEDiä myös taskulampulla, ja vastaavasti LEDin valo himmeni ja eroarvo palautui nopeasti lähelle nollaa. Tuossa lopullisessa ohjelmaversiossa olen poistanut tulostuksen (Bol_Tulosta = false), mutta jätin tulostusrivit aliohjelmaan kommenteiksi ja malliksi. Helpottaa tulostusta, kun tarpeettomat merkitsee kommenteiksi ja poistamalla kommenttimerkin (//), aktivoi vain kulloinkin tarvittavat rivit.

Ohjelma 15
/***************************************
 *  Ohjelma_15
 *  23.04.2016vv
 *  PI-säädin ja liukulukulaskenta
 **************************************/

// MÄÄRITTELYT:
// Kellon määrittely
unsigned long Ulo_MilliSek = 0;
unsigned long Ulo_UusiMilliSek = 0;
const long CoL_EroSekunti = 999;
int Int_Sekunti = 0;
boolean Bol_Tulosta = false; // Tulostusta ohjataan kellolla

// P-arvo, eli vahvistus
const int Con_P_Arvo = 1;
int Int_P_Arvo = 0;
float Flo_P_Arvo = 0.00;

// I-arvo, eli integrointitermi
const int Con_I_Arvo = 2;
int Int_I_Arvo = 0;
int Int_I_Ohjaus = 0;
int Int_I_Viive = 500;
int Int_Integraali = 0;

// LDR valovastus ja LEDi
const int Con_LDRTulo = 0;
const int Con_Valo = 3;
float Flo_Mittaus = 0;
int Int_LDRTulo = 0;

// Säätölaskenta
float Flo_EroArvo = 0;
int Int_Ohjaus = 0;
char Cha_Merkki = 48; // ASCII vastaa nollaa (0)
int Int_Numero = 0;
float Flo_Asetusarvo = 50;
const int Con_Hyster = 10;

// ASETUKSET:
void setup(){                 
 Serial.begin(9600);          
 pinMode(Con_Valo, OUTPUT);
}// Asetuksen loppu

// PÄÄLOOPPI Varsinainen suoritusosa. Jatkuva suoritus
void loop(){
// Sisäisen kellon käyttö
Ulo_MilliSek = millis();
  if(Ulo_MilliSek - Ulo_UusiMilliSek > CoL_EroSekunti){
    Ulo_UusiMilliSek = Ulo_MilliSek;
    Int_Sekunti++;
    if(Bol_Tulosta == true){Fun_Tulostus();}
      Int_Numero = 0; // Sarjasyötön nollaus!!
  }

// Sarjaliikenteen käsittely
if(Serial.available()){
 Cha_Merkki = Serial.read();
 if(isDigit(Cha_Merkki)){
  Int_Numero = (Int_Numero * 10) + (Cha_Merkki -'0');}
  Flo_Asetusarvo = Int_Numero; 
} // Serial.available loppu

// P-kertoimen käsittely
Int_P_Arvo = analogRead(Con_P_Arvo);
Flo_P_Arvo = map(Int_P_Arvo, 0, 1023, 0, 1000);
Flo_P_Arvo = Flo_P_Arvo / 10;

// I-ajan käsittely
Int_I_Arvo = analogRead(Con_I_Arvo);
Int_I_Viive--;
  if(Int_I_Viive == 0){
    Int_I_Ohjaus = Flo_EroArvo / 2;
    Int_Integraali = Int_Integraali + Int_I_Ohjaus;
    Int_I_Viive = Int_I_Arvo;
  }

// Analogiatulon käsittely
Int_LDRTulo = analogRead(Con_LDRTulo);
Flo_Mittaus = constrain(Int_LDRTulo, 600, 750);
Flo_Mittaus = (Int_LDRTulo - 600) / 1.5;

// Sätölaskenta ja ohjauslähtö
Flo_EroArvo = Flo_Asetusarvo - Flo_Mittaus;
Int_Ohjaus = Flo_EroArvo * Flo_P_Arvo + Int_Integraali;
Int_Ohjaus = constrain(Int_Ohjaus, 0, 255);
analogWrite(Con_Valo, Int_Ohjaus);

delay(1);
} // Pääohjelma LOPPU

// FUNKTIOT
void Fun_Tulostus(){
//Serial.print("Teksti :");  Serial.println(Muuttuja);
//Serial.print("Raaka P arvo :");  Serial.println(Int_P_Arvo);
//Serial.print("P arvo :");  Serial.println(Flo_P_Arvo);
//Serial.print("Mittaus :");  Serial.println(Flo_Mittaus);
//Serial.print("SetPoint :");  Serial.println(Flo_Asetusarvo);
//Serial.print("Eroarvo :");  Serial.println(Flo_EroArvo);
//Serial.print("P arvo :");  Serial.println(Flo_P_Arvo);
//Serial.print("Ohjaus :");  Serial.println(Int_Ohjaus);
//Serial.print("I-arvo :");  Serial.println(Int_I_Arvo);
 }

Muutama huomautus ohjelmasta. Tuossa sisäisen kellon osuudessa on lause Int_Numero = 0; // Sarjasyötön nollaus!! Sillä kerran sekunnissa nollataan se numerotieto, mikä lähetettiin sarjaliikenteellä edellisellä kerralla. Jos näin ei tehdä, kytkeytyy uusi syötetty luku edellisen perään. Mittausosassa mittaus rajoitetaan välille 600 .. 750. käsky constrain hyväksyy minkä tahansa numerotyypin. Toisin on käskyn map() kanssa. Se hyväksyy ainoastaan integerin. Siksi mittaus on skaalattu 0 .. 100% vähentämällä lukemasta 600 ja jakamalla sen jälkeen luvulla 1,5. Tämän jälkeen mittaus ja asetusarvo vastaavat lukualueeltaan toisiaan. Ohjausarvo rajoitetaan lähtöjen lukualueelle 0 .. 255 (8bit) ja varsinainen teho-ohjaus tapahtuu pulssinleveysmodulaationa (PWM).


tiistai 19. huhtikuuta 2016

ON - OFF - säätö ja Serial Send

Aiemmassa käytävävalosovelluksessa hämäräkytkin toimi tavallaan ON–OF – säätimenä. Se antoi luvan kytkeä valot päälle ja pois. Helpompi on kuitenkin ymmärtää tässä yhteydessä huonetermostaatti tai talon öljypoltin. Kun lämpötila laskee, kytketään lämmitysteho päälle, ja kun lämpötila nousee hystereesin verran, kytketään teho pois.
Kun esimerkiksi veden keitin kytketään päälle, ei vesi kiehu heti, vaan kuluu jokin aika. Riippuu veden määrästä, kylmän veden lämpötilasta ja vedenkeittimen tehosta. Kaikissa prosesseissa on aina hitautta. Tästä johtuu, että lämpötila jatkaa vielä hetken nousuaan sen jälkeen, kun teho on kytketty pois. Samoin tapahtuu toiseen suuntaan. Kun termostaatti huomaa lämpötilan laskeneen, jatkaa se vielä hiukan laskuaan, ennen kuin se ehtii nousta termostaatin asetusarvon tasolle. Lämpötila siis sahaa edestakaisin asetusarvon molemmin puolin. Tämä ei tietenkään koske ainoastaan vain lämmitystä, vaan kaikkea ON-OFF-säätöä.

Tässä sovelluksessa käytetään edellistä kytkentää. Painike tosin jää pois. Toinen muutos on se, että LEDi joko palaa täysillä tai on sammuneena.


Toinen oleellinen muutos on kondensaattorin muuttaminen kymmenkertaiseksi. Tämä siksi, että vilkkuminen olisi silmälle havainnollisempaa. Käytän edellistä kuvaa merkitsemällä muutokset. Tästä on nähtävissä myös video. Siinä on alussa 35% asetusarvo, minkä tilalle syötetään tietokoneesta sarjaliikenteellä (send – ikkunasta) uudeksi asetusarvoksi 73%. Lähetyshetki näkyy lyhyenä välähdyksenä ja sen jälkeen seuraa yhtenäinen latausvaihe, minkä jatkoksi vilkutus hitaammalla nopeudella. Video on osoitteessa: antinarduvideo14. youtube.com

Ohjelma 14
/***************************************
 *  Ohjelma_14
 *  21.04.2016
 *  ON-OFF-säädin, asetusarvo seriel
 **************************************/

// MÄÄRITTELYT:
unsigned long Ulo_MilliSek = 0;
unsigned long Ulo_UusiMilliSek = 0;
const long CoL_EroSekunti = 999;
int Int_Sekunti = 0;
boolean Bol_Tulosta = true; // Tulostusta ohjataan kellolla

// LDR valovastus ja LEDi
const int Con_LDRTulo = 0;
const int Con_Valo = 3;
int Int_Mittaus = 0;
int Int_LDRTulo = 0;

// Sarjaliikenne
char Cha_Merkki = 48; // ASCII vastaa nollaa (0)
int Int_Numero = 0;
int Int_Asetusarvo = 50;
const int Con_Hyster = 10;

// ASETUKSET:
void setup(){                 
 Serial.begin(9600);         
 pinMode(Con_Valo, OUTPUT);
}// Asetuksen loppu

// PÄÄLOOPPI Varsinainen suoritusosa. Jatkuva suoritus
void loop(){
// Sisäisen kellon käyttö
Ulo_MilliSek = millis();
  if(Ulo_MilliSek - Ulo_UusiMilliSek > CoL_EroSekunti){
    Ulo_UusiMilliSek = Ulo_MilliSek;
    Int_Sekunti++;
    if(Bol_Tulosta == true){Fun_Tulostus();}
     if(Int_Numero > 0){Int_Asetusarvo = Int_Numero;}
      Int_Numero = 0;
  }
 
// Analogiatulon käsittely
Int_LDRTulo = analogRead(Con_LDRTulo);
Int_LDRTulo = constrain(Int_LDRTulo, 600, 750);
Int_Mittaus = map(Int_LDRTulo, 600, 750, 0, 100);

// Sarjaliikenteen käsittely
if(Serial.available()){
 Cha_Merkki = Serial.read();
 if(isDigit(Cha_Merkki)){
  Int_Numero = (Int_Numero * 10) + (Cha_Merkki -'0');}
} // Serial.available loppu

// LEDin ohjaus 0% tai 100%
if(Int_Mittaus < Int_Asetusarvo - Con_Hyster){
  digitalWrite(Con_Valo, HIGH);}
if(Int_Mittaus > Int_Asetusarvo){
  digitalWrite(Con_Valo, LOW);}

delay(1);
} // Pääohjelma LOPPU

// FUNKTIOT
void Fun_Tulostus(){
//Serial.print("Teksti :");  Serial.println(Muuttuja);
Serial.print("Asetusarvo :");  Serial.println(Int_Asetusarvo);

}

Ohjelman alussa on tavalliset määrittelyt. Sarjaliikennesyöttöä varten määritellään Merkki, mikä on tyyppiä character. Se alku arvotetaan kokonaisluvulla 48, mikä on numeron nolla (0) ASCII-numero. Muuttuja Asetusarvo saa oletukseksi 50% ja Hystereesi 10%. Näitä käytettään tehon (tässä tapauksessa LEDin) ohjauksessa.
Pääohjelmassa luetaan ensin analogiatulo muuttujaan LDRTulo, minkä jälkeen rajoitetaan lukualueelle 600 .. 750. Seuraavaksi lukualue muutetaan 0 .. 100% alueelle ja sijoitetaan muuttujaan Mittaus.

Uusi asetusarvo syötetään sarjaliikenteellä ohjelmointityökalusta. Ohjelma tutkii, onko sarjaliikennepuskurissa dataa (if(Serial.available(){  } Jos on, niin merkki luetaan käskyllä Serial.read() ja sijoitetaan muuttujaan Merkki. Sitten katsotaan, onko lähetetty merkki numero (if(isDigit(Cha_Merkki)){ ja tämän if-lauseen sisällä lähetetty luku otetaan talteen muuttujaan Numero. Koska sarjaliikenteessä kukin merkki tulee peräkkäin, pitää lukua kasvattaa kymmenellä ennen uutta merkkiä. Seuraavaksi uudesta tulevasta merkistä vähennetään ’0’, mikä on ASCII – merkki nolla (0) eli desimaalisena 48. Täten jäljelle jää todellinen numero. Koska tässä asetusarvoa ei ole rajoitettu välille 0 .. 100, voi tietokoneelta lähettää pidempiäkin lukusarjoja (kokeilun vuoksi, ei tämän sovelluksen takia). Ylärajana on integerin lukualue.


sunnuntai 17. huhtikuuta 2016

Mittaus

Valovastus ja valo LED ovat vastakkain ”suojattuna” hiukan ympäristön valolta. Ensimmäisessä mittauksessa lukema nousi lähelle tuhatta jo pienilläkin, muutaman prosentin valo-ohjausarvoilla. Siksi LDR (ohjataan analogisesti PWM) piti sulkea pahvin sisään ja puhkaista vain pieni valoaukko. Jokainen painikkeen painallus kasvatti lähtöarvoa 25:dellä eli noin 10 %. Alla mittaustulokset:

Arvo %        0        10        20        30        40        50        60        70        80        90       100
Ohjaus         0        25        50        75       100      125      150      175      200      225       250
AnaTulo      55      435      548      612       625      655      670      690      715      733       750

Oheisesta pylväsdiagrammista voi havaita, että 30% yläpuolella ohjaus- ja mittausarvot kasvavat melko lineaarisesti. 
Kytkennässä on paljon samaa edellisen kanssa, mutta jotakin on jäänyt pois ja jotakin on tullut lisää. Yksi ero on se, että lamppu (LED) tähtää nyt suoraan valovastukseen (LDR) ja analogiatulo ei mittaa suoraan valovastuksen muodostamaa jännitettä, vaan välillä on RC-suodin. Koska valovastuksen kuormasta lähtee melko suuri (100 kohm) vastus elektrolyyttikondensaattoriin, ovat muutokset analogiatulossa hitaampia kuin LDR:n navassa. 


Kuvat puhukoot puolestaan. Elekrolyytikondensaattori, harmaa komponentti pahvitötterön vieressä, melko keskellä koelevyä, näkyy kokonaiskuvan alaosassa. Toisessa kuvassa LDR on suojattu pahvikääröön ja valoreikä näkyy vaaleampana.


Ohjelma 13
/***************************************
 *  Ohjelma 13
 *  19.04.2016
 *  Mittaus ja muunnos
 **************************************/

// MÄÄRITTELYT:
// Kellon määrittely
unsigned long Ulo_MilliSek = 0;
unsigned long Ulo_UusiMilliSek = 0;
const long CoL_EroSekunti = 999;
int Int_Sekunti = 0;
boolean Bol_Tulosta = true; // Tulostusta ohjataan kellolla

// Muutamia perusmäärityksiä
const int Con_LDRTulo = 0;
int Int_LDRTulo = 0;
const int Con_Painike = 2;
boolean Bol_PainikeTila = false;
const int Con_PainikeViive = 3;
int Int_PainikeViive = 0;
int Seq_Painike = 1;
const int Con_Valo = 3;
int Int_Ohjaus = 0;

// ASETUKSET:
void setup(){                 
 Serial.begin(9600);         
 //while (!Serial) { ; } // Odottaa sarjaportin kytkeytymistä
pinMode(Con_Painike, INPUT);
pinMode(Con_Valo, OUTPUT);
}// Asetuksen loppu

// PÄÄLOOPPI Varsinainen suoritusosa. Jatkuva suoritus
void loop(){
// Sisäisen kellon käyttö
Ulo_MilliSek = millis();
  if(Ulo_MilliSek - Ulo_UusiMilliSek > CoL_EroSekunti){
    Ulo_UusiMilliSek = Ulo_MilliSek;
    Int_Sekunti++;
    if (Bol_Tulosta == true){Fun_Tulostus();}
  }
 
// Päivitetään tulot ja lähdöt
Int_LDRTulo = analogRead(Con_LDRTulo);
Bol_PainikeTila = digitalRead(Con_Painike);
Int_Ohjaus = constrain(Int_Ohjaus, 0, 255);
analogWrite(Con_Valo, Int_Ohjaus);

// KOSKETTIMEN-tilan käsittely, nouseva reuna
switch (Seq_Painike) {
  case 1:
    if (Bol_PainikeTila == true){
      Int_PainikeViive ++;
        if (Int_PainikeViive > Con_PainikeViive){
          Int_Ohjaus = Int_Ohjaus + 25;
          Int_PainikeViive = 0;
          Seq_Painike = 2;
        }
    }
    break;
  case 2:
    if (Bol_PainikeTila == false){
          Seq_Painike = 1;
        }
    break;
}
 delay(1);
} // Pääohjelma LOPPU

// FUNKTIOT
void Fun_Tulostus(){
//Serial.print("Teksti :");  Serial.println(Muuttuja);
Serial.print("Ohjausarvo :");  Serial.println(Int_Ohjaus);
Serial.print("LDR lukema :");  Serial.println(Int_LDRTulo);
 }

torstai 14. huhtikuuta 2016

Käytävävalo hämäräkytkimellä

Tässä ratkaisussa on ”käytävän kummassakin päässä” painike, joista voidaan lamppu sytyttää ja sammuttaa. Lisäksi valot voidaan sytyttää ainoastaan silloin kun niitä tarvitaan. On siis tarpeeksi hämärää. Lisäksi on huolehdittava siitä, että käytävän valoa ei osu liikaa valoa mittaavaan valovastukseen.


Kytkentä on aika paljon edellisen kaltainen. Nyt on tullut lisää toinen painike, toinen LED (valo) ja LDR, valovastus (Light Dependent Resistor). Sen vastusarvo muuttuu sen mukaan, miten paljon siihen osuu valoa. Mitä enemmän valoa, sitä pienempi vastus, ja sitä suurempi jännite syntyy analogiatuloon ja muunnoksen jälkeen lukema monitoriin. Ohjelmassa olikin ensin vain tuo valon mittausosa. Monitoroimalla analogiatuloa A0, pääsi näkemään lukemat. Enpä huomannut laittaa niitä tarkasti ylös, mutta peittämällä ja puristamalla valovastus, oli tulon lukema muutama kymmenen (esim. 50) ja valaisemalla taskulampulla lukema oli 1000 paikkeilla. Kun lukema on 512 (1023 / 2), on valovastuksen arvo 10 kilo-ohmia, sillä tuolloin 5V:in jännite jakaantuu kahtia. Eppabasicilla voi laskea noiden ääripäiden vastusarvot. Jokainen muunnoksen porras on 4,89mV (5000 / 1023) ja tästä saadaan laskettua valovastuksen yli vaikuttava jännite.


Dim Lukema = InputNumber ("Anna lukema: ")
Dim Jännite = Lukema * 0.00489
Print Jännite & (" V")
Dim Virta = Jännite / 10000
Print Virta & (" A")
Dim LDRjännite = 5 - Jännite
Print ("Jännite valovastuksen yli: ") & LDRjännite & (" V")
Dim Vastus = LDRjännite / Virta / 1000
Print Vastus & (" kilo Ohmia")

Kun lukemaksi (= analogiatulon lukema monitoroinnissa) syöttää 50, saadaan tulokseksi 194,49 kolo-ohmia ja syötettäessä 1000, saadaan vastusarvoksi 0,2449 kilo-ohmia, eli 244,9 ohmia. Tuossa ohjelmassa virtaa laskettaessa luku 10.000 on vastus R3, eli analogiatulosta maapotentiaaliin. Sen arvo on 10 kohmia. Jos lukemaksi syöttää 511, saadaan vastuksen arvoksi melko tarkasti 10 kohmia. Jos tämä on valaistustilanne, jakaantuu syöttävä 5V:ia puoliksi LDR:n ja kuoritusvastuksen kesken.
Tätä ohjelmaa tehdessä ja testatessa oli niin kirkas päivä, että valovastus piti peittää pienen potentiometrin varjoon. Tätä sovellusta voi tietysti kehittää myös monipuolisemmaksi. Porrasvaloissa on tyypillisesti ajastin, mikä sammuttaa valon tietyn ajan kuluttua automaattisesti. Se on joskus huono, jos valot ovat jo päällä, niin valo voi sammua kesken nousun (tai laskun). Pieni apu saattaisi olla, jos valoviive alkaisi joka painalluksella alusta. Uimahallin suihkut toimivat ajastimen kannalta juuri näin.

Tuossa esimerkkiohjelmassa, kellon määrittelyn osuudessa tulostus (Bol_Tulosta = false;) eli EPÄTOSI, mutta ohjelmaa tehdessä oli monitorointi todella tarpeen. Olenkin jättänyt ne testilauseet tulostus aliohjelmaan kommenteiksi. Se onkin hyvä ja helpottava tapa. Laittaa ne tulostukset kommenteiksi, joita ei tarvitse ja aktivoi kulloinkin tarvittavat. Kun katselee valmista toimivaa ohjelmaa, näyttää se yksinkertaiselta ja selvältä. Totta kai se on noin! On usein kuitenkin turhauttavaa etsiä niitä virheitä. Siinä on hyvänä apuna monen rivin kommenttipari ( /* .. */), millä voi poistaa osia ohjelmasta ja täten rajata ja etsiä sitä, mihin asti ohjelma kääntyy ja toimii halutulla tavalla. Ei tämäkään ohjelma kivutta syntynyt. Enkä odota sitä tulevaltakaan.
Tuo valoraja (400) on otettu ihan vaan hihasta, mutta tuntui toimivalta. Samoin hystereesi (80). Hystereesi on tärkeä, sillä jos vallitaan vain suurempi tai pienempi, niin toiminnasta tulee hyvin epästabiili. Määrityksissä ei ole mitään erikoista, määritellään sarjaliikenne monitorointia varten, kaksi lähtöä (OUTPUT) ja kaksi tuloa (INPUT). Analogiatuloa (A0) ei tarvitse erikseen asettaa tuloksi, sillä sen pinni määräytyy jo määrittelyissä
(int Int_AnaTulo = 0;). Analogiakäsittelyssä tulon lukemat rajoitetaan välille 100 .. 750
(constrain(Int_AnaTulo, 100, 750); Tämä käsky ei muuta lukemia miksikään, vaan ainostaan asettaa rajat. Aiemmin oli käytössä skaalauskäsky (map), millä koko tuloalue muutetaan toiseksi alueeksi. Esim: tulo 0 .. 1023 muutetaan lähtöalueelle 0 .. 255. MerkkiLEDiä ohjataan suoraan hämäräkytkimen sekvenssistä, mutta varsinaisen valon aliohjelmaa (Fun_Valo(boolean Tila) { .. }) kutsutaan kolmesta eri kohdasta pääohjelmaa.


Ohjelma 12 
/***************************************
 *  Ohjelma 12
 *  15.04.2016
 *  Käytävävalo hämäräkytkimellä
 **************************************/

// MÄÄRITTELYT:
// Kellon määrittely
unsigned long Ulo_MilliSek = 0;
unsigned long Ulo_UusiMilliSek = 0;
const long CoL_EroSekunti = 999;
int Int_Sekunti = 0;
boolean Bol_Tulosta = false; // Tulostus lopuksi pois

const int Con_AnaTulo = 0;
int Int_AnaTulo = 0;
const int Con_ValoRaja = 400;
const int Con_Hystereesi = 80;
const int Con_MerkkiLED = 3;
int Seq_Valo = 1; // Käynnistetään valaistuksen hallinta
const int Con_Valo = 4;
boolean Bol_Valo = false;
const int Con_Kytkin_1 = 2;
const int Con_Kytkin_1Viive = 3;
int Int_Kytkin_1Viive = 0;
boolean Bol_Kytkin_1Tila = false;
int Seq_Kytkin_1 = 0;
const int Con_Kytkin_2 = 5;
const int Con_Kytkin_2Viive = 7;
int Int_Kytkin_2Viive = 0;
boolean Bol_Kytkin_2Tila = false;
int Seq_Kytkin_2 = 0;

// ASETUKSET:
void setup(){                 
 Serial.begin(9600);         
 pinMode(Con_MerkkiLED, OUTPUT);
 pinMode(Con_Valo, OUTPUT);
 pinMode(Con_Kytkin_1, INPUT);
 pinMode(Con_Kytkin_2, INPUT);
}// Asetuksen loppu

// PÄÄLOOPPI Varsinainen suoritusosa. Jatkuva suoritus
void loop(){
// Sisäisen kellon käyttö
Ulo_MilliSek = millis();
  if(Ulo_MilliSek - Ulo_UusiMilliSek > CoL_EroSekunti){
    Ulo_UusiMilliSek = Ulo_MilliSek;
    Int_Sekunti++;
    if(Bol_Tulosta == true){Fun_Tulostus();}
  } // Kellon loppu

// Analogiatulon käsittely
Int_AnaTulo = analogRead(Con_AnaTulo);
Int_AnaTulo = constrain(Int_AnaTulo, 100, 750);

switch (Seq_Valo) {
  case 1: // tämä (1) on vakio, siis ei muuttuja
    if(Int_AnaTulo < Con_ValoRaja){
      Seq_Valo = 2;
    }
    break;
  case 2: // askel 2
    digitalWrite(Con_MerkkiLED, HIGH);
    Seq_Kytkin_1 = 1; // Kytkinsekvenssien käynnistys
    Seq_Kytkin_2 = 1;
    Seq_Valo = 3;
    break;
  case 3: // askel 3
    if(Int_AnaTulo > Con_ValoRaja + Con_Hystereesi){
      digitalWrite(Con_MerkkiLED, LOW);
      Seq_Kytkin_1 = 0; // Kytkinsekvenssien lopetus  
      Seq_Kytkin_2 = 0;
      Bol_Valo = false;
      Fun_Valo(Bol_Valo);
      Seq_Valo = 1; // paluu askeleeseen 1
    }
    break;
} // Valo-sekvenssin loppu

// Kytkimien käsittely
Bol_Kytkin_1Tila = digitalRead(Con_Kytkin_1);
Bol_Kytkin_2Tila = digitalRead(Con_Kytkin_2);

// KYTKIMEN 1 -tilan käsittely, nouseva reuna
switch (Seq_Kytkin_1) {
  case 1:
    if (Bol_Kytkin_1Tila == true){ // painiketta painettu
      Int_Kytkin_1Viive ++;
        if (Int_Kytkin_1Viive > Con_Kytkin_1Viive){
          Bol_Valo = !Bol_Valo;
          Fun_Valo(Bol_Valo);         
          Int_Kytkin_1Viive = 0;
          Seq_Kytkin_1 = 2;
        }
    }
    break;
  case 2:
    if (Bol_Kytkin_1Tila == false){
        Seq_Kytkin_1 = 1;
        }
    break;
} // Kytkin 1, sekvenssin loppu

// KYTKIMEN 2 -tilan käsittely, nouseva reuna
switch (Seq_Kytkin_2) {
  case 1:
    if (Bol_Kytkin_2Tila == true){
      Int_Kytkin_2Viive ++;
        if (Int_Kytkin_2Viive > Con_Kytkin_2Viive){
          Bol_Valo = !Bol_Valo;
          Fun_Valo(Bol_Valo);         
          Int_Kytkin_2Viive = 0;
          Seq_Kytkin_2 = 2;
        }
    }
    break;
  case 2:
    if (Bol_Kytkin_2Tila == false){
        Seq_Kytkin_2 = 1;
        }
    break;
} // Kytkin 2, sekvenssin loppu

delay(1);
} // Pääohjelma LOPPU

// FUNKTIOT
void Fun_Tulostus(){
//Serial.print("Teksti :");  Serial.println(Muuttuja);
//Serial.print("Analogiatulon raaka arvo :");  Serial.println(Int_AnaTulo);
//Serial.print("Kytkin 1 tila :");  Serial.println(Bol_Kytkin_1Tila);
//Serial.print("Kytkin 1 seq :");  Serial.println(Seq_Kytkin_1);
}

void Fun_Valo(boolean Tila){
  digitalWrite(Con_Valo, Tila);
} // Valo-aliohjeman loppu

tiistai 12. huhtikuuta 2016

Kosketinvärähtely sekä switch ... case - rakenne


Käytännössä monia toimintoja ohjataan painikkeilla tai kytkimillä. Aina kun mekaaniset koskettimet ”lyövät” yhteen, värähtelevät koskettimet muutaman kerran. Monissa tapauksissa siitä ei ole haittaa, koska tuo tarkoittaa paria värähdystä millisekuntien aikana, mutta tuo ominaisuus on tarpeen eliminoida, varsinkin silloin, kun ohjaus tapahtuu kytkimen nousevalla reunalla ( kytkin suljetaan) tai laskevalla reunalla ( kytkin avataan). Kytkentäkaava on hyvin yksinkertainen. Siinä on painike, LED ja pari vastusta.

Esimmäisessä ohjelmassa ei tarvitse ottaa värähtelyä huomioon, sillä LED palaa, kun painiketta painetaan. Tuo alasvetovastus, mihin painike kytkee 5V:n jännitteen on merkitty 10 kilo’ohmia ( = 10 000 ohmia), mutta se voi hyvin olla väliltä 4,7 kohm .. 15 kohm. Tuloa D2 ei voi kuitenkaan jättää vapaaksi, ”ilmaan”, sillä silloin syntyy häiriöitä. Toinen mahdollisuus on asetuksissa määritellä Arduinon sisäinen ylösvetovastus. Tässä tapauksessa painike kytketään pinnistä D2 maihin ja toiminta on päinvastainen kuin yllä olevassa kytkennässä.

Ohjelma 9
/***************************************
 *  Ohjelma 9
 *  12.04.2016
 *  Painikkeen käsittely
 **************************************/
// MÄÄRITTELYT:
const int Con_Painike = 2;
const int Con_LEDout = 3;
boolean Bol_Painike = false;

// ASETUKSET:
void setup(){                 
pinMode(Con_Painike, INPUT);
pinMode(Con_LEDout, OUTPUT);
}// Asetuksen loppu
// PÄÄLOOPPI Varsinainen suoritusosa. Jatkuva suoritus

void loop(){
Bol_Painike = digitalRead(Con_Painike);
digitalWrite(Con_LEDout, Bol_Painike);
delay(1);
} // Pääohjelma LOPPU

Seuraavat kaksi ohjelmaesimerkkiä käyttävät samaa kytkentää kuin tuo edellinen ohjelmakin. Ero on toiminnallinen. Ohjelmassa 10 LED syttyy ja sammuu, kun painike painetaan alas. Tämä on nouseva reuna, sillä jännite kontrollerin digitaalitulossa (D2) nousee 0V -> 5volttiin, eli digitaalitila muuttuu nollasta (0) ykköseksi (1). Ohjelmassa 11 LED syttyy ja sammuu painike päästettäessä. Tämä on laskeva reuna, sillä tulo muuttuu ykkösestä nollaksi (1 -> 0).

Tällaiset toiminnat voidaan tehdä myös if else –rakenteilla tallentamalla edellinen tila sekä tekemällä mutkikkaampia ehtoja ja vertailuja, mutta huomattavasti näppärämpi on
switch (var) { case 1: … break; case 2: break; ……. dafault; } ( kytke (muuttuja) { tapaus 1: seis; tapaus 2: … seis; jne. oletus: … }. If-rakenteessa ohjelma käy läpi kaikki vaihtoehdot ja suorittaa ne, joiden kohdalla ehto täyttyy. Switch-rakenteessa suoritetaan ainoastaan tarpeellinen. Se ensinnäkin yksinkertaistaa ohjelma rakennetta, ja ennen kaikkea nopeuttaa ohjelma suoritusta, koska tarpeeton ohjelmarivien ”kelaaminen” jää pois. Tämä on myös tapa rakentaa suoritusnauhoja, jolloin tärkeämpiä asioita käydään läpi tiheämmässä tahdissa ja hitaampaa tarvetta suoritetaan harvemmin. Käsky millis() on tässäkin mielessä tärkeä toiminto, sillä ohjelmien osat saadaan täten synkronoitua reaaliaikaan. Jos tämä kappale vaikuttaa ”vaahtoamiselta”, niin se on todella sitä, sillä tämä rakenne on yksi tärkeimmistä ohjelmointityökaluista.

Mutta ensin LEDin sytytys ja sammutus painikkeen ohjaamana.

Ohjelma 10
/***************************************
 *  Ohjelma 10
 *  13.04.2016
 *  LEDin tilan vaihto nousevasta reunasta
 **************************************/

// MÄÄRITTELYT:
const int Con_Painike = 2;
const int Con_LEDout = 3;
const int Con_PainikeViive = 3;
boolean Bol_PainikeTila = false;
boolean Diu_PainikeTila = false; // Nouseva reuna
boolean Did_PainikeTila = false; // Laskeva reuna
int Int_PainikeViive = 0;
int Seq_OhjausUp = 1;

// ASETUKSET:
void setup(){                  
 Serial.begin(9600);         
 pinMode(Con_Painike, INPUT);
  pinMode(Con_LEDout, OUTPUT);
}// Asetuksen loppu

// PÄÄLOOPPI Varsinainen suoritusosa. Jatkuva suoritus
void loop(){
// Luetaan digitaalitulo 
Bol_PainikeTila = digitalRead(Con_Painike);

// KOSKETTIMEN-tilan käsittely, nouseva reuna
switch (Seq_OhjausUp) {
  case 1:
    if (Bol_PainikeTila == true){
      Int_PainikeViive ++;
        if (Int_PainikeViive > Con_PainikeViive){
          Diu_PainikeTila = !Diu_PainikeTila;
          Fun_LedOut(Diu_PainikeTila);
          Int_PainikeViive = 0;
          Seq_OhjausUp = 2;
        }
    }
    break;
  case 2:
    if (Bol_PainikeTila == false){
          Seq_OhjausUp = 1;
        }
    break;
}
delay(1);
} // Pääohjelma LOPPU

// FUNKTIOT
void Fun_LedOut(boolean Tila){
  digitalWrite(Con_LEDout, Tila);

}

Ohjelma 11 äskeinen toiminta, mutta nyt laskevasta reunasta.

Ohjelma 11
/***************************************
 *  Ohjelma 11
 *  13.04.2016
 *  LEDin tilan vaihto laskevasta reunasta
 **************************************/

// MÄÄRITTELYT:
const int Con_Painike = 2;
const int Con_LEDout = 3;
const int Con_PainikeViive = 3;
boolean Bol_PainikeTila = false;
boolean Diu_PainikeTila = false; // Nouseva reuna
boolean Did_PainikeTila = false; // Laskeva reuna
int Int_PainikeViive = 0;
int Seq_OhjausDn = 1;

// ASETUKSET:
void setup(){                 
 Serial.begin(9600);         
 pinMode(Con_Painike, INPUT);
 pinMode(Con_LEDout, OUTPUT);
}// Asetuksen loppu

// PÄÄLOOPPI Varsinainen suoritusosa. Jatkuva suoritus
void loop(){
// Luetaan digitaalitulo 
Bol_PainikeTila = digitalRead(Con_Painike);

// KOSKETTIMEN-tilan käsittely, laskeva reuna
switch (Seq_OhjausDn) {
  case 1:
    if (Bol_PainikeTila == true){
      Int_PainikeViive ++;
        if (Int_PainikeViive > Con_PainikeViive){
          Int_PainikeViive = 0;
          Seq_OhjausDn = 2;
        }
    }
    break;
  case 2:
    if (Bol_PainikeTila == false){
         Did_PainikeTila = !Did_PainikeTila;
          Fun_LedOut(Did_PainikeTila);
          Seq_OhjausDn = 1;
        }
    break;
}
delay(1);
} // Pääohjelma LOPPU

// FUNKTIOT
void Fun_LedOut(boolean Tila){
  digitalWrite(Con_LEDout, Tila);

}