Carte „10(zece) proiecte Internet of Things” în format electronic

Primii zece cumpărători pot beneficia de un preț special: 1.99 EURO utilizând codul promoțional PRIMII10 . Cartea poate fi cumpărată din magazinul 10proiecte iar plata se va face utilizând portalul PayPal. Formatul electronic permite accesul complet la formatul PDF al cărții cu posibilitate de tipărire și extragere de conținut.

 

coperta_v1_mic 

Carte „10(zece) proiecte Internet of Things” în format electronic.

Autor: Radu Pietraru.

240 de pagini cu proiecte despre Internetul obiectelor utilizând plăci de dezvoltare Arduino, Raspberry Pi și ESP8266.

10ProiecteIoT_Cuprins

10ProiecteIoT_PrezentareProiecte

Puteți comanda cartea în format tipărit de pe site-ul Robofun la prețul de 39RON.

Solar Power GPRS Test

Întrebarea la care vom încerca să răspundem în cadrul acestui proiect este: putem construi un sistem IoT cu comunicație GPRS total independent de alimentarea de la rețeaua de energie? Sistemul propus pentru testare este un sistem ce se bazează exclusiv pe alimentare solară. Un astfel de sistem poate fi folosit pentru locații izolate în care nu există disponibile alimentare cu energie electrică și Internet.

Ca soluție de alimentare solară vom utiliza o componentă MPPT: Sunny Buddy – MPPT Solar Charger. Această componentă permite conectarea unei celule solare cu ieșirea între 6V și 20V și încărcarea unui acumulator LiIon sau LiPo cu o singură celulă (3.7V). Curentul maxim de încărcare este de 450mA deci este necesară utilizarea unui acumulator de capacitate minimă 450mAh. În testul nostru vom utiliza o celulă solară de 2.5W / 9V și un acumulator LiPo de 800mAh.  Pentru mai multe detalii despre funcționarea componentei MPTT puteți consulta materialul „Sunny Buddy Solar Charger V13 Hookup Guide”.

Pentru partea de achiziție și raportare IoT vom utiliza placa de dezvoltare Adafruit Feather 32u4 FONA echipată cu un microcontroler ATmega32U4 (la fel ca și placa de dezvoltare Arduino Leonardo) și un controler GSM/2G SIM800 quad-band (850/900/1800/1900MHz).

2

Placa va supraveghea nivelul de încărcare a acumulatorului și va raporta prin Internet (prin intermediul conexiunii GPRS) valorile către serviciul cloud iot.robofun.ro. Placa de dezvoltare se va alimenta de la acumulatorul LiPo conectat la componenta MPPT și va servi ca element de descărcare pentru acesta. Pentru mai multe detalii despre funcționarea și utilizarea plăcii de dezvoltare Adafruit Feather 32U4 FONA puteți consulta materialul „Overview | Adafruit Feather 32u4 FONA | Adafruit Learning System”.

Diagrama de interconectare între componentele sistemului de test este următoarea:

4

Funcționarea plăcii de dezvoltare necesită o antenă GSM uFL. Pentru a minimiza consumul și pentru a putea controla inițializarea sistemului (mai ales în cazul în care acumulatorul sistemului nu este încărcat suficient) pinul KEY al modulului GSM se va deconecta de la masă (necesită tăierea jumperului KEY de pe spatele plăcii) și se va conecta la pinul 0 al plăcii de dezvoltare (firul galben din schema de interconectare).  În acest fel modulul GSM de pe placă nu va mai fi activ continuu ci doar în cazul în care îl vom activa din software. ATENȚIE!!! Această operație conduce la pierderea garanției oferite de producător.

Programul sistemului a fost dezvoltat și testat utilizând Arduino IDE 1.8.5 având instalate extensia Adafruit AVR Boards 1.4.11 și bibliotecile Adafruit FONA Library 1.3.3 și Sleep_n0m1 1.1.1.

#include “Adafruit_FONA.h”

#include <Wire.h>

#define FONA_RX 9

#define FONA_TX 8

#define FONA_RST 4

#define FONA_RI 7

#define FONA_KEY 1

#define FONA_ALARM 13

Definirea constantelor apn, apnusername și apnpassword sunt specifice rețelei de telefonie mobile al SIM-ului GSM utilizat. ATENȚIE!!! Placa de dezvoltare necesită un SIM GSM ce suportă standardul 2G, comunicația GSM nu va funcționa în cazul unui SIM GSM exclusiv 3G/4G.

#define apn “net”

#define apnusername “” 

#define apnpassword “” 

char replybuffer[255];

#include <SoftwareSerial.h>

SoftwareSerial fonaSS = SoftwareSerial(FONA_TX, FONA_RX);

SoftwareSerial *fonaSerial = &fonaSS;

Adafruit_FONA fona = Adafruit_FONA(FONA_RST);

#include <Sleep_n0m1.h>

Sleep sleep;

În cadrul secțiunii setup() se vor inițializa pinii de control utilizați pentru controlul modulului GSM (FONA_KEY este utilizat pentru activarea modulului GSM, FONA_ALARM este conectat la LED-ul de semnalizare al plăcii de dezvoltare și va fi utilizat pentru avertizarea imposibilității inițializării corecte a modulului GSM).

void setup() {

  pinMode(FONA_KEY,OUTPUT);

  digitalWrite(FONA_KEY,HIGH);

  pinMode(FONA_ALARM,OUTPUT);

  digitalWrite(FONA_ALARM,LOW);

}

Secțiunea loop() conține algortimul complet de inițializare și comandă pentru modulul GSM, achiziția nivelului de încărcare a bateriei și transmiterea prin Internet a valorilor către serviciul IoT. Primul pas în execuția algoritmului este intrarea în starea de consum redus pentru o oră – stare obligatorie pentru revenirea sistemului dintr-o stare de oprire completă generată de consumarea completă a acumulatorului (acumulatorul este încărcat suficient pentru inițializarea microcontrolerului ATmega32U4 dar nu și pentru inițializarea modulului GSM).

void loop() {

  sleep.pwrDownMode();

  sleep.sleepDelay(3600000);

Pornirea modulului GSM necesită un impuls de 2 secunde pe pinul FONA_KEY. Sistemul va încerca de 10 ori inițializarea modulului GSM după care va relua perioada de o oră de consum redus.

  digitalWrite(FONA_KEY,LOW);

  digitalWrite(FONA_ALARM,HIGH);

  delay(2000);

  digitalWrite(FONA_KEY,HIGH);

  digitalWrite(FONA_ALARM,LOW);

  delay(2000);

  fonaSerial->begin(4800);

  byte i = 0;

  while (!fona.begin(*fonaSerial)) {

      i++;

      if (i==10) break;

      digitalWrite(FONA_KEY,LOW);

      digitalWrite(FONA_ALARM,HIGH);

      delay(2000);

      digitalWrite(FONA_KEY,HIGH);

      digitalWrite(FONA_ALARM,LOW);

      delay(2000);      

  }

În cazul în care modulul GSM s-a inițializat corect se va încerca deblocarea cartelei SIM pe baza codului PIN (trebuie modificat în program în funcție de codul cartelei folosite). Dacă deblocarea cartelei SIM nu se poate face se va relua perioada de o oră de consum redus – este posibil ca acumulatorul să scadă sub pragul de alimentare corectă a modulului GSM între inițializare și deblocarea cartelei SIM.

if (i!=10) {

    if (! fona.unlockSIM(“0000”) ) {

digitalWrite(FONA_ALARM,HIGH);

delay(1000); i=10; }

    else {  

Programul va aștepta înregistrarea în rețeaua GSM a cartelei SIM a sistemului pentru o perioadă de 100 de secunde.

    uint8_t n = fona.getNetworkStatus();

     i=0;

      while (n!=1) {

        i++;

        if (i==10) break;

        digitalWrite(FONA_ALARM,HIGH);

        delay(5000);

        digitalWrite(FONA_ALARM,LOW);

        delay(5000);

        n = fona.getNetworkStatus();

       }

     }

Postarea valorii de încărcare a acumulatorului se va face doar dacă se va inițializa cu succes partea de comunicații de date (fona.enableGPRS). Măsurarea se va face chiar de modulul GSM. În cadrul programului trebuie personalizate cheile de autentificare (SENSOR_TOKEN1 și SENSOR_TOKEN2) obținute prin înregistrarea gratuită în cadrul serviciului iot.robofun.ro.

if(i!=10) {

      fona.setGPRSNetworkSettings(F(apn),

F(apnusername),F(apnpassword));

      if (fona.enableGPRS(true)) {

      uint16_t vbat;

      uint16_t pbat;

      fona.getBattVoltage(&vbat);

      fona.getBattPercent(&pbat);

      delay(1000);

      uint16_t statuscode;

      int16_t length;

      String SENSOR_TOKEN1=”…”;

      String SENSOR_TOKEN2=”…”;

      String data =

         String(“http”) + “://iot.robofun.ro/api/v1/senzor/” + SENSOR_TOKEN1 +  “/input?value=” + String(vbat, DEC);

      char url[100];

      data.toCharArray(url,data.length()+1);

      fona.HTTP_GET_start(url, &statuscode,

(uint16_t *)&length);

      while (length > 0) {

          while (fona.available()) {

            char c = fona.read();

            length–;

            if (! length) break;

          }

          break;

      } 

      fona.HTTP_GET_end();

      delay(100);

      data = String(“http”) + “://iot.robofun.ro/api/v1/senzor/”) + SENSOR_TOKEN2 + “/input?value=” + String(pbat, DEC);

      data.toCharArray(url,data.length()+1);

      fona.HTTP_GET_start(url, &statuscode,

(uint16_t *)&length);

      while (length > 0) {

          while (fona.available()) {

            char c = fona.read();

            length–;

            if (! length) break;

          }

          break;

      } 

      fona.HTTP_GET_end();

      delay(100);

    }

La final, înainte de reintrarea în modul de consum pentru o oră, se va opri modulul GSM printr-un puls de 2 secunde pe pinul FONA_KEY.

      digitalWrite(FONA_KEY,LOW);

      delay(2000);

      digitalWrite(FONA_KEY,HIGH);

    }

  }

}

Testul a fost efectuat pornind cu acumulatorul sistemului complet consumat (tensiune sub 3.3V). După o zi cu soare, când tensiunea acumulatorului a ajuns la 3.7V, sistemul s-a inițializat complet cu succes și a început să posteze pe serviciul IoT. Acest lucru înseamnă că sistemul își poate reveni dintr-o stare de oprire completă generată de descărcarea acumulatorului (mai multe zile fără lumină solară) fără intervenție umană.

5

În timpul unei zile cu soare acumulatorul câștigă aproximativ 30% din nivelul de încărcare iar procentul de descărcare într-o zi înorată este de circa 15%. Asta înseamnă că o zi cu soare poate fi urmată de două zile înorate fără ca sistemul să descarce acumulatorul. Dacă acumulatorul sistemului este complet încărcat, sistemul poate funcționa fără lumină solară pentru un interval de 6-7 zile

6

Testul efectuat dovește că se poate construi un sistem de raportare IoT GSM/GPRS care să funcționeze în mod autonom pe baza energiei solare fără să fie nevoie de intervenția umană în cazuri speciale de descărcare a acumulatorului.

Solar Power WiFi Test

Implementarea unui proiect cu alimentare solară prezintă mai multe avantaje printre care, bineînțeles, se evidențiază independența de rețeaua electrică tradițională. În același timp un astfel de proiect ridică și multe probleme legate de dimensionarea soluției de alimentare și alegerea componentelor potrivite. Care este dimensiunea panoului solar? Ce soluție de stocare a energiei electrice alegem? Ce soluție de încărcare a acumulatorilor este cea mai potrivită? Acestea sunt câteva dintre întrebările la care vom încerca să răspundem în cadrul acestui proiect. Chiar dacă se poate face o evaluare inițială la nivel teoretic, în cadrul testului nostru vom alege varianta testării efective a soluției alese și evidențierea avantajelor și a punctelor slabe ale acesteia.

Ca soluție de maximizare a eficienței captării energiei solare și de încărcare a acumulatorului vom utiliza o componentă MPPT: Sunny Buddy – MPPT Solar Charger.

2

Această componentă permite conectarea unei celule solare cu ieșirea între 6V și 20V și încărcarea unui acumulator LiIon sau LiPo cu o singură celulă (3.7V). Curentul maxim de încărcare este de 450mA deci este necesară utilizarea unui acumulator de capacitate minimă 450mAh. În testul nostru vom utiliza o celulă solară de 2.5W / 9V și un acumulator LiPo de 800mAh. Pentru a supraveghea procesul de încărcare / descărcare a acumulatorului vom utiliza o placă de dezvoltare Adafruit Feather HUZZAH ce va raporta datele măsurate prin intermediul conexiunii WiFi către serviciul cloud iot.robofun.ro. Placa de dezvoltare se va alimenta de la acumulatorul LiPo conectat la componenta MPPT și va servi ca element de descărcare pentru acesta. Pentru o acuratețe mai bună a măsurătorilor vom utiliza o componentă ce va monitoriza nivelul de încărcare a bateriei: LiPo Fuel Gauge. Componenta de monitorizare se va intercala între ieșirea componentei MPPT și alimentarea plăcii de dezvoltare și va raporta prin intermediul unei magistrale I2C nivelul de încărcare a bateriei.

3

Diagrama de interconectare între componentele sistemului de test este următoarea:

4

Înainte de realizarea montajului și punerea lui în funcțiune este recomandată parcurgerea documentațiilor oficiale ale componentelor utilizate:

Atenție!!! Schema precedentă este schema finală de test. În momentul programării plăcii de dezvoltare trebuie să deconectăm alimentarea plăcii prin intermediul componentei de măsurare (să deconectăm placa de dezvoltare de componenta LiPo Fuel Gauge) și să deconectăm pinul Reset de pinul Wake (GPIO16) – firul galben.

Programul sistemului a fost dezvoltat și testat utilizând Arduino IDE 1.8.5, extensia ESP8266 Community 2.4.0 și biblioteca LiFuelGauge 1.0.

#include <Wire.h>

#include <ESP8266WiFi.h>

#include <ESP8266HTTPClient.h>

#include <LiFuelGauge.h>

În cadrul programului trebuie personalizate datele de conectare la rețeaua WiFi (variabilele ssid[] și pass[]).

char ssid[] = “…”;

char pass[] = “…”;

LiFuelGauge gauge(MAX17043);

Secțiunea setup() se va ocupa cu inițializarea comunicației cu modulul de măsurare a tensiunii din acumulator și cu inițializarea comunicației WiFi.

void setup() {

  gauge.reset();

  WiFi.mode(WIFI_STA);

  WiFi.hostname(“ESP_SolarMeter”);

  WiFi.begin(ssid, pass);

  byte retry = 0;

  while ((WiFi.status() != WL_CONNECTED) && (retry<10)) {

    delay(1000);

    retry++;

    if (retry==10) ESP.restart();

  }

}

Secțiunea loop() conține partea de citire a măsurătorilor efectuate de componenta LiPo Fuel Gauge și de trimiterea acestor măsurători către serviciul IoT. În cadrul programului trebuie personalizate cheile de autentificare (SENZOR_TOKEN1 și SENZOR_TOKEN2) obținute în urma înregistrării gratuite pe iot.robofun.ro. Postarea măsurătorilor se va face la un interval de o oră. Între măsurători sistemul va funcționa în mod de consum redus (deepSleep) pentru a minimiza consumul de energie și maximiza durata de funcționare.

void loop() {

  float batPercentage = gauge.getSOC();

  float batVoltage = gauge.getVoltage(); 

  String SENSOR_TOKEN1=”…”;

  String SENSOR_TOKEN2=”…”;

  HTTPClient http;

  String data =

String(“http://iot.robofun.ro/api/v1/senzor/&#8221;) + SENSOR_TOKEN1 + “/input?value=” + String(batVoltage, DEC);

  http.begin(data);

  int httpCode = http.GET();

  delay(100);

  http.end();

  data = String(“http://iot.robofun.ro/api/v1/senzor/&#8221;) + SENSOR_TOKEN2 + “/input?value=”  + String(batPercentage, DEC);

  http.begin(data);

  httpCode = http.GET();

  delay(100);

  http.end();

  ESP.deepSleep(3600L*1000000L);

}

Testarea sistemului va indica următoarele lucruri esențiale în funcționarea acestuia:

  • Sistemul de încărcare solar este capabil să înmagazineze într-o zi însorită suficientă energie pentru funcționarea sistemului pentru 24 de ore – sistemul poate funcționa autonom într-o perioadă însorită. Mai jos este graficul furnizat de serviciul iot.robofun pentru o succesiune de două zile însorite consecutive. Se poate observa că sistemul recuperează energia consumată peste noapte în timpul zilei.

5

6

  • Peste noapte sistemul pierde circa 30% din capacitatea acumulatorului. Asta înseamnă că sistemul poate funcționa circa trei zile fără reîncărcare (zile fără soare / înorate).

7

  • După epuizarea completă a energiei din acumulator sistemul nu este capabil să repornească singur chiar dacă urmează o zi însorită. Acest lucru este cauzat de curentul mare necesar la inițializarea plăcii de dezvoltare (peste 450mA, peste curentul furnizat de componenta Sunny Buddy). Sistemul nu va reuși să intre în mod de consum redus pentru a lăsa acumulatorul să se reîncarce și va rămâne într-o secvență infinită de inițializare ce va opri procesul de reacumulare a energiei solare.

Testul relevă posibilitatea limitată de funcționare a sistemului propus. Acesta poate funcționa o perioadă nedeterminată de timp în mod autonom atâta timp cât nu apare o succesiune de mai mult de două zile complet înnorate. Curentul mare necesar la inițializarea plăcii de dezvoltare blochează reluarea procesului de reîncărcare solară. Totuși, cu un alt consumator ce necesită un curent mai mic de inițializare, sistemul ar putea funcționa autonom și și-ar putea relua activitatea după o perioadă lungă fără lumină solară.

Optimizarea programelor în mediul Arduino IDE

Optimizarea procesului de compilare

Mediul Arduino IDE utilizează compilatorul GCC. Pe lângă procesul de compilare propriu-zis, mediul de dezvoltare efectuează o serie de operații (pre-procesare) ce permit implementarea funcțiilor specifice Arduino (de exemplu includerea automată a definițiilor din Arduino.h). Pentru detalierea procesului de compilare se poate parcurge materialul „Arduino Build Process”.

Procesul de compilare este efectuat, invizibil pentru programator, la un anumit nivel de optimizare (-Os sau -O2 în funcție de versiunea mediului de dezvoltare). Pentru detalierea nivelurilor de optimizare recomandăm parcurgerea materialului „Using the GNU Compiler Collection (GCC): Optimize Options”. Modificarea nivelului de optimizare la compilarea programului se poate face la nivel de fișiere de configurare a mediului de dezvoltare sau chiar la nivel de program.

Prin introducerea unor directive de compilare putem modica nivelul de optimizare pentru întregul program:

#pragma GCC optimize (“-O0”)

#pragma GCC push_options

void setup() { }

void loop() { }

#pragma GCC pop_options

sau doar pentru o anumită secțiune, funcție sau procedură:

void loop() __attribute__((optimize(“-O0”)));

void loop() { }

 

Optimizarea instrucțiunilor specifice mediului Arduino IDE

Unul din obiectivele principale ale mediului Arduino IDE este înlesnirea accesului programatorului la diversele mecanisme interne ale microcontrolerului. Din aceste motiv s-a efectuat rebotezarea pinilor și tot din același motiv au fost introduse instrucțiuni specifice mediului Arduino IDE de tipul pinMode, digitalRead sau digitalWrite. Din punct de vedere al procesului de învățare și dezvoltare aceste facilități accelerează foarte mult timpul de lucru dar din punctul de vedere al eficienței execuției aduc penalizări destul de mari. Cel mai bun exemplu, prezentat și în materialul „Arduino Is Slow”, este înlocuirea instrucțiunii digitalWrite cu o instrucțiune de modificare a registrului intern ce stochează starea pinului pe care dorim să-l modificăm. Se poate testa diferența dintre timpul de execuție a celor două variante rulând următorul exemplu de program:

void setup()

{

 Serial.begin(9600);

}

void loop()

{

 int initial = 0;

 int final = 0;

 initial = micros();

 for(int i = 0; i < 500; i++)

 {

    digitalWrite(13,HIGH);

    digitalWrite(13,LOW);

 }

 final = micros();

 Serial.print(“Time for digitalWrite(): “);

 Serial.print(final-initial);

 Serial.println(“”);

 initial = micros();

 for(int i = 0; i < 500; i++)

 {

    PORTB |= _BV(PB5);

    PORTB &= ~_BV(PB5);

 

 }

 final = micros();

 Serial.print(“Time for true c command: “);

 Serial.print(final-initial);

 while(1);

}

Programul înlocuiește instrucțiunea digitalWrite cu o scriere în registrul intern al microcontrolerului (pinul 13 este pinul PB5). Rezultatul este destul de convingător:

2

Instrucțiunea digitalWrite nu este singura care poate fi înlocuită în cadrul programelor Arduino pentru a obține performanțe mai bune. O altă instrucțiune ”delicată” ce poate afecta funcționarea unor montaje mai complicate este instrucțiunea shiftOut.  Dacă examinăm fișierul wiring_shift.c din directorul Arduino putem vedea că această funcție se bazează pe instrucțiunea digitalWrite deci poate fi accelerată prin înlocuirea acestei instrucțiuni.

void shiftOut(uint8_t dataPin, uint8_t clockPin,

uint8_t bitOrder, uint8_t val)

{

    uint8_t i;

    for (i = 0; i < 8; i++)  {

         if (bitOrder == LSBFIRST)

             digitalWrite(dataPin, !!(val & (1 << i)));

         else

             digitalWrite(dataPin, !!(val & (1 <<

(7 – i))));

         digitalWrite(clockPin, HIGH);

         digitalWrite(clockPin, LOW);     

    }

}

ATENȚIE!!! Optimizările prezentate conduc la pierderea portabilității programelor între diverse plăci Arduino. Toate indicațiile se aplică direct doar pentru plăcile ce sunt echipate cu un microcontroler ATmega328P (Arduino Uno de exemplu).

Pentru exemplificarea optimizării instrucțiunii shiftOut vom considera următorul montaj bazat pe o placă de dezvoltare Arduino Uno, un registru de deplasare 74HC595 și 8 leduri:

3

Programul propus va implementa un mini joc de lumini ce constă în aprinderea succesivă a câte un led din cele 8. Programul obișnuit (ce utilizează instrucțiunea shiftOut) este:

const int latchPin = 4;

const int clockPin = 3;

const int dataPin = 2;

void setup() {               

  pinMode(latchPin, OUTPUT);

  pinMode(dataPin, OUTPUT); 

  pinMode(clockPin, OUTPUT);

  digitalWrite(latchPin, LOW);

  shiftOut(dataPin, clockPin, MSBFIRST, 0); 

  digitalWrite(latchPin, HIGH);

}

void loop() {

  int afisare = 1;

  while (1) {

   for (int i=0;i<8;i++) {

      digitalWrite(latchPin, LOW);

      shiftOut(dataPin, clockPin, MSBFIRST, afisare << i); 

      digitalWrite(latchPin, HIGH);

      delay(500);

     }

   }

}

Varianta nouă ce nu folosește instrucțiunile shiftOut și digitalWrite este:

void loop() {

  int afisare = 1;

  while (1) {

   for (int i=0;i<8;i++) {

    PORTD |= _BV(PD4);

    for (uint8_t b = 0; b < 8; b++)  {

      if(!!((afisare << i) & (1 << (7 – b)))) PORTD |=

_BV(PD2);

      else PORTD &= ~_BV(PD2);

      PORTD |= _BV(PD3);

      PORTD &= ~_BV(PD3);

    }

    PORTD &= ~_BV(PD4);

    delay(500);

   }

  }

}

Chiar dacă la prima vedere exemplul funcționează la fel, se poate justifica necesitatea modificării codului prin imaginarea situației în care procesul de transmitere serială se repetă de un număr suficient de mare de ori încât produce o întârziere inacceptabilă (de exemplu încascadarea unui număr mare de registre de deplasare).

Utilizarea limbajului de asamblare în Arduino IDE

Ce este ”Inline Assembler”?

Facilitatea de ”Inline Assembler” permite inserarea de cod în limbaj de asamblare în cadrul programelor de C/C++ compilate cu ajutorul GCC (compilatorul utilizat de mediul Arduino IDE). Utilizarea de cod în limbaj de asamblare permite optimizarea unor porțiuni de cod și obținerea unor programe mai mici ca dimensiune (în format binar). Pentru mai multe informații se recomandă consultarea materialului „Inline Assembler Cookbook”.

Inserarea de cod în limbaj de asamblare se face utilizând directiva asm (sau ___asm___) direct în program. De exemplu (instrucțiunea NOP în limbaj de asamblare nu are nici un efect):

asm ( “nop \n”);

Bineînțeles, în cadrul secțiunii de cod în limbaj de asamblare nu vom beneficia de aceleași avantaje și înlesniri ca într-un program obișnuit în limbaj de nivel înalt. Pentru a înțelege mai bine vom reface exemplul clasic din Arduino IDE – programul Blink:

void setup() {

  pinMode(13, OUTPUT);

}

void loop() {

  digitalWrite(13, HIGH);

  delay(1000);

  digitalWrite(13, LOW);  

  delay(1000);     

}

O variantă a acestui program utilizând directiva asm este:

void setup() {

 asm(“sbi  0x04, 0x5 \n”);

}

void loop() {

 asm(“cbi  0x05, 0x5 \n”);  

 delay(1000);                

 asm(“sbi  0x05, 0x5 \n”); 

 delay(1000);           

}

După cum se poate observa instrucțiunile pinMode și digitalWrite, specifice mediului Arduino IDE, au fost înlocuite cu instrucțiuni în limbaj de asamblare: sbi și cbi ce permit setarea sau ștergerea unui bit de la o anumită adresă din memorie. Mai mult decât atâta, nu am mai folosit referința la pinul plăcii Arduino așa cum suntem obișnuiți (pinul 13) ci adrese de memorie la care se află registrele interne de configurare ale pinului (registrul de sens DDRB – adresa 0x04 și registrul de ieșire PORTB – adresa 0x05, în ambele registre am manipulat bitul 5 corespondent pinului PB5 adică pinul 13 al plăcii Arduino). Comparați memoria program ocupată de exemplul original și cel care utilizează directiva asm.

2

Registre interne și echivalarea pinilor între mediul Arduino și arhitectura microcontrolerului ATmega328P

Pentru a ușura lucrul cu pinii I/O mediul Arduino IDE are propria modalitate de identificare a acestora (D0-D13, A0-A5) dar în realitate aceștia sunt organizați în trei porturi a câte 8 pini (PB0-PB7, PC0-PC7, PD0-PD7), echivalența între cele două organizări este reprezentată în diagrama următoare (nu toți pinii sunt prezenți la varianta THT a circuitului ATmega328P):

3

Pentru a putea manipula pinii microcontrolerului (la nivel de limbaj de asamblare) este nevoie să cunoaștem adresele registrelor DDRx (registrul de sens) și PORTx (registrul de ieșire) al portului din care face parte pinul. Pentru mai multe informații despre organizarea internă a registrelor interne este utilă consultarea manualului circuitului ATmega328P.

4 

Exemplu de program: joc de lumini

Presupunem următoarea schemă de interconectare a 8 leduri cu placa de dezvoltare Arduino Uno în mod individual – fiecare led este comandat în mod direct de câte un pin al plăcii de dezvoltare (led 1 – pin 2, led 2 – pin 3…. led 8 – pin 9):

5

Funcționalitatea sistemului va consta în realizarea a două jocuri de lumini. Secțiunea setup a programului va trebui să configureze toți cei opt pini utilizați ca fiind pini de ieșire. Varianta inițială ce utilizează instrucțiunea pinMode este:

void setup() {               

  pinMode(2, OUTPUT);

  pinMode(3, OUTPUT);  

  pinMode(4, OUTPUT); 

  pinMode(5, OUTPUT);   

  pinMode(6, OUTPUT); 

  pinMode(7, OUTPUT); 

  pinMode(8, OUTPUT); 

  pinMode(9, OUTPUT);

  Serial.begin(9600);

}

Prima variantă propusă utilizează instrucțiunea sbi ca și în exemplul precedent (adresa 0x0a este adresa registrului DDRD iar adresa 0x04 adresa registrului DDRB):

asm (

  “sbi 0x0a, 2 \n”

  “sbi 0x0a, 3 \n”

  “sbi 0x0a, 4 \n”

  “sbi 0x0a, 5 \n”

  “sbi 0x0a, 6 \n”

  “sbi 0x0a, 7 \n”

  “sbi 0x04, 0 \n”

  “sbi 0x04, 1 \n”

 );

O variantă mai scurtă este configurarea biților din cele două registre (DDRD și DDRB) simultan utilizând registrul de uz general R26 ca intermediar pentru transmiterea valorii către cele două registre:

asm (

    “ldi r26, 0b11111100 \n”

    “out 0x0a, r26 \n”

    “ldi r26, 0b00000011 \n”

    “out 0x04, r26 \n”

    : : : “r26”

    );

Utilizarea unui registru de uz general trebuie semnalizată compilatorului pentru a nu apărea suprapuneri în utilizarea registrelor – ultima linie din cod, a se vedea și materialul „Arduino Inline Assembly Tutorial #3 (Clobbers)”.

Prima variantă de joc de lumini va aprinde alternativ la un interval de 1 secundă ledurile de rang impar și ledurile de rang par (led 1, led 3, led 5, led 7 – led 2, led 4, led 6, led 8). Secțiunea loop (utilizând cod Arduino) este:

void loop() {

  for (int i=2; i<10; i++) {

      if ((i%2)==0) digitalWrite(i,HIGH);

      else digitalWrite(i,LOW);

   }

   delay(1000); 

   for (int i=2; i<10; i++) {

      if ((i%2)==0) digitalWrite(i,LOW);

      else digitalWrite(i,HIGH);

   }

   delay(1000);

}

Transpunerea în limbaj de asamblare este:

void loop() {

asm (

      “sbi 0x0b,2 \n”

      “cbi 0x0b,3 \n”

      “sbi 0x0b,4 \n”

      “cbi 0x0b,5 \n”     

      “sbi 0x0b,6 \n”

      “cbi 0x0b,7 \n”

      “sbi 0x05,0 \n”

      “cbi 0x05,1 \n”

      “ldi r25, 0x7F \n”

      “wait1: ldi r26, 0xFF \n”

      “wait2: ldi r27, 0xFF \n”

      “wait3: dec r27 \n”

      “nop \n”

      “brne wait3 \n”

      “dec r26 \n”

      “brne wait2 \n”

      “dec r25 \n”

      “brne wait1 \n”     

      “cbi 0x0b,2 \n”

      “sbi 0x0b,3 \n”

      “cbi 0x0b,4 \n”

      “sbi 0x0b,5 \n”     

      “cbi 0x0b,6 \n”

      “sbi 0x0b,7 \n”

      “cbi 0x05,0 \n”

      “sbi 0x05,1 \n”

      “ldi r25, 0x7F \n”

      “wait1b: ldi r26, 0xFF \n”

      “wait2b: ldi r27, 0xFF \n”

      “wait3b: dec r27 \n”

      “nop \n”

      “brne wait3b \n”

      “dec r26 \n”

      “brne wait2b \n”

      “dec r25 \n”

      “brne wait1b \n”

      : : : “r25” , “r26”, “r27”

  ); 

Cea de a doua variantă de joc de lumini va aprinde unul câte unul (pornind de la led-ul 1 până la led-ul 8) toate led-urile și apoi le va stinge în mod similar (în ordine inversă). Operația de aprindere sau stingere a unui led se va efectua la un interval de 500 milisecunde. Secțiunea loop (utilizând cod Arduino) este:

void loop() {

   for (int i=2;i<10;i++) {

     digitalWrite(i,HIGH);

     delay(500); }

   for (int i=9;i>1;i–) {

     digitalWrite(i,LOW);

     delay(500);

   }

  }

Transpunerea în limbaj de asamblare este:

void loop() {

asm (

      “start: sbi 0x0b,2 \n”

      “rcall wait \n”

      “sbi 0x0b,3 \n”

      “rcall wait \n”

      “sbi 0x0b,4 \n”

      “rcall wait \n”

      “sbi 0x0b,5 \n”

      “rcall wait \n”     

      “sbi 0x0b,6 \n”

      “rcall wait \n”

      “sbi 0x0b,7 \n”

      “rcall wait \n”

      “sbi 0x05,0 \n”

      “rcall wait \n”

      “sbi 0x05,1 \n”

      “rcall wait \n”     

      “cbi 0x0b,2 \n”

      “rcall wait \n”

      “cbi 0x0b,3 \n”

      “rcall wait \n”

      “cbi 0x0b,4 \n”

      “rcall wait \n”

      “cbi 0x0b,5 \n”

      “rcall wait \n”     

      “cbi 0x0b,6 \n”

      “rcall wait \n”

      “cbi 0x0b,7 \n”

      “rcall wait \n”

      “cbi 0x05,0 \n”

      “rcall wait \n”

      “cbi 0x05,1 \n”

      “rcall wait \n”     

      “rjmp start \n”

      “wait: ldi r25, 0x3F \n”

      “wait12: ldi r26, 0xFF \n”

      “wait22: ldi r27, 0xFF \n”

      “wait32: dec r27 \n”

      “nop \n”

      “brne wait32 \n”

      “dec r26 \n”

      “brne wait22 \n”

      “dec r25 \n”

      “brne wait12 \n”

      “ret \n”

      : : : “r25” , “r26”, “r27”

  );

}

Ambele jocuri de lumini utilizează instrucțiunile sbi și cbi pentru manipularea pinilor I/0 (pentru o listă completă a instrucțiunilor în limbaj de asamblare se recomandă parcurgerea materialului „AVR Instruction Set Manual”). Funcția de temporizare delay din mediul Arduino IDE este înlocuită cu o succesiune de bucle ce realizează o întârziere aproximativă (în a doua variantă această succesiune este implementată ca o subrutină).

Toilet Time

Ignorat sau râvnit, tihnit sau zbuciumat, relaxare sau chiar plăcut timp de lectură… putem descrie în multe moduri timpul petrecut la toaletă dar știm de fapt care este acest timp? În cadrul proiectului de față vom prezenta realizarea unui sistem ce permite măsurarea timpului petrecut la toaletă. Chiar dacă poate părea puțin bizară tematica propusă vă asigurăm că mulți pasionați de sisteme electronice programabile s-au gândit cel puțin o dată la un sistem care să accesorizeze toaleta proprie; câteva exemple de astfel de proiecte:

Sistemul propus de noi se bazează pe o placă de dezvoltare Adafruit Feather HUZZAH echipată cu microprocesorul WiFi ESP8266. Conectivitatea WiFi va permite consultarea timpului total petrecut la toaletă prin intermediul oricărui dispozitiv inteligent cu conectivitate de rețea (tabletă, telefon mobil). În plus, specific tuturor plăcilor din familia Feather, placa de dezvoltare se poate alimenta de la un acumulator LiPo de 3.7V permițând crearea simplă a unui sistem independent de alimentarea USB. Detectarea ocupării toaletei se va face cu ajutorul unui buton brick ce se va poziționa sub colacul toaletei. Greutatea colacului nu este suficientă pentru a apăsa butonul, butonul se va apăsa doar dacă toaleta este ocupată. Bineînțeles, se poate înlocui componenta buton brick cu un senzor de apăsare brick dacă se dorește perfecționarea dedectării ocupării toaletei sau dacă se dorește realizarea unui sistem cu mai mulți ”utilizatori”.

Schema de interconectare dintre placa de dezvoltare și brick-ul buton este următoarea:

2

Brick-ul se va alimenta la 3.3V iar ieșirea se va conecta la pinul GPIO12 al plăcii de dezvoltare.

Programul a fost dezvoltat și testat utilizând Arduino IDE 1.8.5 având instalată extensia ESP8266 Community 2.3.0. În cadrul programului trebuie personalizate datele de conectare WiFi (variabilele ssid și password).

#include <ESP8266WiFi.h>

#include <WiFiClient.h>

#include <ESP8266WebServer.h>

#include <EEPROM.h>

const char *ssid = “…”;

const char *password = “…”;

ESP8266WebServer server (80);

#define pinButton 12

Timpul total de utilizarea a toaletei este stocat în variabila globală toilettime ca număr de secunde. Consultarea timpului total se poate face accesând adresa IP a sistemului (oferită prin DHCP de către AP-ul WiFi) dintr-un client web (browser web). Procedura handleRoot() este responsabilă de construirea paginii HTML trimisă către clientul web. Timpul total se va afișa în format HH:MM:SS. Programul poate fi îmbunătățit în această privință pentru a afișa durate mai mari de timp (sub formă de zile, săptămâni, ani).

3

char temp[600];

unsigned long toilettime;

void handleRoot() {

int sec = toilettime % 60;

int min = (toilettime / 60) % 60;

int hr = toilettime / 3600;

snprintf ( temp, 600,”%02d:%02d:%02d”, hr, min, sec);

String page = “<html>\

<head>\

<meta http-equiv=’refresh’ content=’10’/>\

<title>Toillet Time</title>\

<style>\

body { background-color: #cccccc; font-family:

Arial, Helvetica, Sans-Serif; Color: #000088; }\

</style>\

</head>\

<body>\

<h1>Toilet Time</h1>\

<p><b>Your total time is:</b> ” + String(temp) + “

</p>\

</body>\

</html>”;

page.toCharArray(temp,page.length()+1);

server.send ( 200, “text/html”, temp );

}

Variabila ocupat are semnificația de toaletă ocupată. Când variabila ocupat are valoare true sistemul va contoriza timpul scurs și îl va adăuga la variabila toilettime. Conținutul variabilei toilettime se va salva în memoria flash a plăcii de dezvoltare la fiecare trecere din true în false a variabilei ocupat – acest lucru va asigura păstrarea timpului total și după un reset al plăcii de dezvoltare. Pentru operațiile cu memoria flash programul utilizează biblioteca Arduino IDE EEPROM. Placa de dezvoltare nu are memorie EEPROM, versiunea pentru ESP8266 a bibliotecii EEPROM utilizează aceleași funcții și metode ca la plăcile Arduino dar se folosește de memoria program flash. În cadrul secțiunii setup(), la fiecare nouă inițializare a programului, variabila toilettime se inițializează din memoria non-volatilă program.

boolean ocupat;

void setup() {

pinMode(pinButton, INPUT);

EEPROM.begin(512);

toilettime = 0;

toilettime += (long)EEPROM.read(0)<<24;

toilettime += (long)EEPROM.read(1)<<16;

toilettime += (long)EEPROM.read(2)<<8;

toilettime += (long)EEPROM.read(3);

WiFi.begin ( ssid, password );

while ( WiFi.status() != WL_CONNECTED ) {

delay ( 500 );

}

server.on ( “/”, handleRoot );

server.begin();

ocupat = false;

}

Variabilele start și stopp memorează momentele trecerii variabilei ocupat din false în true (așezarea pe toaletă) și din true în false (ridicarea de pe toaletă). În momentul adunării timpului scurs la variabila globală toilettime, conținutul acesteia este salvată și în memoria flash.

unsigned long start, stopp;

void loop() {

if ((!ocupat) && (digitalRead(pinButton)==1)) {

start = millis();

ocupat = true;

}

if (ocupat && (digitalRead(pinButton)==0)) {

stopp = millis();

ocupat = false;

toilettime += (stopp – start)/1000;

byte temp;

temp = (toilettime>>24) & 0xFF;

EEPROM.write(0, temp);

temp = (toilettime>>16) & 0xFF;

EEPROM.write(1, temp);

temp = (toilettime>>8) & 0xFF;

EEPROM.write(2, temp);

temp = toilettime & 0xFF;

EEPROM.write(3, temp);

EEPROM.commit();

}

server.handleClient();

}

În cazul în care vă plictisiți testând sistemul vă recomandăm aplicația mobilă Android Toilet Time – Mini-games for the bathroom.

New Year Countdown

Un element important al oricărui revelion este, bineînțeles, numărătoarea inversă până la trecerea în noul an – cronometrul care ne arată cât timp mai este până la deschiderea sticlei de șampanie și până la pornirea focurilor de artificii. În cadrul proiectului de față ne propunem să implementăm un sistem care să ne arate cu precizie cât timp mai este până la un anume moment de timp, poate să fie trecerea în noul an, o aniversare sau orice alt moment important.

Pentru a putea calcula cu exactitate timpul rămas trebuie să știm în primul rând data și ora la momentul actual. Pentru acest lucru vom utiliza o placă cu conectivitate WiFi ce ne va permite sincronizare de timp NTP și anume placa de dezvoltare Adafruit Feather M0 WiFi. Această placă este similară cu placa Arduino MKR1000, montajul propus poate fi realizat cu oricare dintre ele. Pe lângă conectivitatea WiFi, ambele plăci de dezvoltare permit alimentarea de la un acumulator LiPo de 3.7V permițând astfel realizarea simplă a unui montaj portabil. Pentru afișare vom utiliza un modul cu afișaj pe 7 segmente cu 4 caractere și circuit de comandă TM1637. Schema de interconectare dintre placa de dezvoltare și modulul de afișare este:

2

Modulul de afișare se va alimenta la 3.3V (pinul VCC se va conecta la pinul de 3V al plăcii de dezvoltare iar pinul GND la pinul de GND). Pinul CLK se va conecta la pinul 5 al plăcii de dezvoltare și pinul DIO la pinul 6. Comunicația între placa de dezvoltare și modulul de afișare se face serial.

Programul a fost dezvoltat și testat utilizând Arduino IDE 1.8.5 având instalate bibliotecile TM1637Display, Time 1.5.0, WiFi101 0.14.5 și extensia Adafruit SAMD Boards 1.0.21.

#include <TM1637Display.h>

#define CLK 5

#define DIO 6

TM1637Display display(CLK, DIO);

#include <SPI.h>

#include <WiFi101.h>

#include <WiFiUdp.h>

În cadrul programului trebuie personalizate informațiile de conectare WiFi (ssid[] și pass[]). Constanta timeZone conține decalajul de fus orar specific zonei grografice în care ne aflăm (2 pentru ora de iarnă în România).

int status = WL_IDLE_STATUS;

char ssid[] = “…”;       

char pass[] = “…”;   

unsigned int localPort = 2390;

IPAddress timeServer(129, 6, 15, 28);

const int NTP_PACKET_SIZE = 48;

byte packetBuffer[ NTP_PACKET_SIZE];

WiFiUDP Udp;

#include <TimeLib.h>

const int timeZone = 2;

tmElements_t TheDay;

Structurile SEG_DONE și dy conțin configurația de segmente pentru mesajul done și pentru literele d și Y. Mesajul done va fi afișat după ce numărătoare inversă se va încheia. Litele d și Y vor fi utilizate când se vor afișa anii și zilele rămase pâna la evenimentul programat.

const uint8_t SEG_DONE[] = {

  SEG_B | SEG_C | SEG_D | SEG_E | SEG_G,           // d

  SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F,   // O

  SEG_C | SEG_E | SEG_G,                           // n

  SEG_A | SEG_D | SEG_E | SEG_F | SEG_G            // E

  };

const uint8_t dy[] = {

  SEG_B | SEG_C | SEG_D | SEG_E | SEG_G,           // d

  SEG_B | SEG_E | SEG_F | SEG_G ,                  // Y

  };

În cadrul secțiunii setup se realizează conectarea la rețeaua WiFi și programarea evenimentului dorit (în exemplul nostru 1 ianuarie 2018 ora 0:00, anul este înregistrat ca diferență față de anul 1970). Instrucțiunea WiFi.setPins este necesară pentru placa Adafruit Feather M0, se va șterge în cazul plăcii Arduino MKR1000.

void setup() {

  WiFi.setPins(8,7,4,2);

  if (WiFi.status() == WL_NO_SHIELD) {

    while (true);

  }

  while ( status != WL_CONNECTED) {

    status = WiFi.begin(ssid, pass);

    delay(10000);

  }

  Udp.begin(localPort);

  setSyncProvider(getNtpTime);

  setSyncInterval(3600);

  TheDay.Second = 0;

  TheDay.Minute = 0;

  TheDay.Hour = 0;

  TheDay.Day = 1;

  TheDay.Month = 1;

  TheDay.Year = 48;

  }

Secțiunea loop implementează partea de verificare a diferenței de timp între momentul actual și data evenimentului (diferența se calculează în secunde și este stocată în variabila dif) și partea de afișare. Dacă diferența este negativă înseamnă că evenimentul a trecut și se afișează done. Dacă diferența este mai mare de 31556926 secunde (adică un an) pe ecran se va afișa diferența în ani întregi, pe trei caractere urmate de litera Y. Dacă diferența este mai mare de 86400 secunde (adică o zi) pe ecran se va afișa diferența în zile, pe trei caractere urmate de litera d. Dacă diferența este mai mică de o zi dar mai mare de o oră (3600 secunde) pe ecran se va afiș diferența în ore:min. În final, când avem un interval mai mic de o oră, pe ecran se va afișa diferența în format min:sec.

void loop() {

  time_t dif;

  dif = makeTime(TheDay) – now();

  display.setBrightness(0x0f);

  if (dif < 0) display.setSegments(SEG_DONE);

  else if (dif>31556926L) {

    display.showNumberDec(((1970+TheDay.Year) –

(year()+1)),true,3,0);

display.setSegments(dy+1, 1, 3); }

  else if (dif>86400L) {

display.showNumberDec((dif/86400L),true,3,0); display.setSegments(dy, 1, 3); }

  else if (dif>3600) {

display.showNumberDecEx((dif/3600L),0xFF,true,2,0); display.showNumberDec(((dif%3600L)/60),true,2,2); }

  else {

display.showNumberDecEx((dif/60),0xFF,true,2,0); display.showNumberDec((dif%60L),true,2,2); }

  delay(1000);

}

Funcții getNtpTime și sendNTPpacket sunt utilizate de biblioteca Time pentru sincronizarea de timp.

time_t getNtpTime() {

   sendNTPpacket(timeServer);

  delay(1000);

  if ( Udp.parsePacket() ) {

    Udp.read(packetBuffer, NTP_PACKET_SIZE);

    unsigned long highWord = word(packetBuffer[40],

packetBuffer[41]);

    unsigned long lowWord = word(packetBuffer[42],

packetBuffer[43]);

    unsigned long secsSince1900 = highWord << 16 | lowWord;

    const unsigned long seventyYears = 2208988800UL;

    unsigned long epoch = secsSince1900 – seventyYears;

    return (time_t) (secsSince1900 – 2208988800UL +

timeZone * SECS_PER_HOUR);

  }

  else return 0;

}

unsigned long sendNTPpacket(IPAddress& address) {

  memset(packetBuffer, 0, NTP_PACKET_SIZE);

  packetBuffer[0] = 0b11100011;  

  packetBuffer[1] = 0;    

  packetBuffer[2] = 6;    

  packetBuffer[3] = 0xEC;

  packetBuffer[12]  = 49;

  packetBuffer[13]  = 0x4E;

  packetBuffer[14]  = 49;

  packetBuffer[15]  = 52;

  Udp.beginPacket(address, 123);

  Udp.write(packetBuffer, NTP_PACKET_SIZE);

  Udp.endPacket();

}

Dacă se dorește implementarea unui afișaj pe 7 segmente utilizând o componentă proprie (un afișaj de mari dimensiuni de exemplu) se poate utiliza un circuit de tip shift register cu o schemă de interconectare de genul:

3

Pentru circuitul 74HC595 se poate avea în vedere utilizarea bibliotecii SparkFun 74HC595 Arduino Library pentru implementarea comenzii către caracterele pe 7 segmente.