Progress bar pro Arduino a LCD displej

Občas se hodí graficky vyjádřit průběh nějakého děje, vytvořit na LCD znakovém displeji „progress bar“. Následující článek ukazuje „jak na to“ s I2C displejem 20×4 a Arduinem UNO, s jiným typem displeje se musí program upravit.

Nejjednodušším způsobem, jak hrubý progress bar na znakovém displeji zobrazit, je zvolit si nějaký znak, asi nejvhodnější je plně vybarvené znakové pole nebo naopak třeba tenká pomlčka, a tento znak zobrazit v řádku tolikrát za sebou, kolik odpovídá požadované hodnotě.  Má to ale své nevýhody. Zobrazení je hrubé, rozliší se nejvýš tolik stavů, kolik je znaků na řádku, jednotlivé znaky jsou vzhledem ke konstrukci displeje od sebe vždy odděleny jednobodovou mezerou a pokud jde o proces, který vyžaduje přesné časování, může vadit, že zobrazení trvá dost dlouho.

To, že v našem případě použijeme displej řízený I2C, je zcela vedlejší. O obsluhu displeje se v tomto případě stará knihovna LiquidCrystal_I2C.h, o komunikaci prostřednictvím dvou vodičů (SCL = A5, SDA = A4) knihovna Wire.h .

Chceme-li zobrazit postup co nejjemněji, můžeme využít možnosti nadefinovat si uživatelské grafické znaky, přenést je do paměti displeje a zobrazovat. Při šířce jednoho znaku 5 obrazových bodů budeme potřebovat znaky, v nichž jsou postupně vyplněny jednotlivé sloupečky zleva, pro zvýraznění indikace ale zobrazíme také rámeček kolem.

Bez rámečku by nám stačilo 5 uživatelských znaků (obvykle jich máme k dispozici 8), s rámečkem se ale situace komplikuje kvůli zobrazení posledního znaku vpravo. Na něj nám dokonce nestačí 3 zbývající grafické znaky a podle situace budeme muset měnit obsah grafické RAM podle toho, co na posledním znaku potřebujeme zrovna zobrazit. Je-li v displeji opravdu grafická RAM, nijak moc to nevadí (jen je to časově náročnější), je-li tam ale grafická EEPROM s mnohem menší životností z hlediska počtu zápisů, nejde tento způsob použít.

IMGP1648b

Progress bar s pěti stavy na každý znak

Kolik vlastně dovolí tento systém zobrazit stavů? Na obrázku je příklad na displeji 16*2 a s rámečkem, takže (16*5)-2+1 . Dva stavy odečítáme na rámeček, jeden navíc je nula, kdy žádný segment v progress baru nesvítí.

Takto navržené zobrazení je jemné, ale neodstraňuje viditelné dělicí linie mezi znaky a pokud se vždy (při každém zobrazení) překresluje celý graf, i docela dlouho trvá. Toto řešení bylo použito třeba v měřiči velkých kapacit, který včetně programu najdete v samostatném článku.

Půjdeme trochu jinou cestou. Obětujeme část jemného rozlišení, nadefinujeme si také vlastní grafické znaky, ale do každého vložíme jen tři čárky oddělené jednobodovou mezerou. Tím, že mezery jsou stejné jako mezi znaky, na pohled splynou a v progress baru nejde rozlišit předěly znaků. Výhodou je, že nyní už stačí 6 uživatelských znaků a nemusíme žádný nahrávat znova v průběhu práce programu. Kolik stavů lze zobrazit? Pro náš displej 20*4 je to (20*3)-2+1 = 59… opět musíme dva odečíst kvůli rámečku vlevo i vpravo a naopak jeden přičíst za nulu, kdy nesvítí žádný segment.

IMGP2018b

Demo zde uvedeného programu

Při inicializaci se do zvoleného řádku (zde pevně nastaveno na řádek 3) vykreslí prázdný rámeček, na dobu průběhu se nehledí. Při následném použití se volá metoda „updateProgress“ s parametrem zobrazované hodnoty 0 až 60, hodnoty mimo rozsah nevadí. Metoda je postavná tak, že předpokládá a očekává pravidelné (či nepravidelné) časté volání, její průběh trvá vždy přibližně stejně dlouho (kolem 2 ms) a je relativně velmi rychlý.

Pokud se volá se stejnou hodnotou, která už je zobrazena, nic se nevykresluje, pokud se volá příliš často, také se vrací bez kreslení, protože je v programu minimální doba, po níž se může překreslit (zde nastaveno na 10 ms). Když se nový požadovaný stav liší od zobrazovaného, překreslí se jen jeden nebo dva znaky podle situace a nová zobrazená hodnota je „o jednu čárku blíž“ k požadované. Jestliže se tedy prudce změní požadovaná hodnota, například skokem spadne na nulu, progress bar vyžaduje až 59 volání, než zobrazení plynule „sjede“ do nuly. S ohledem na to, že nelze zobrazit novou hodnotu dřív než za 10 ms, nejrychlejší přejezd grafu z kraje do kraje trvá při tomto nastavení kolem 0,6 s.

Uvedený postup se může zdát zbytečně složitý a když nemá Arduino nic jiného na práci, než vykreslovat graf, tak opravdu zbytečný je. Když vyžadujete plynulé zobrazení bez blikání a současně nelze vnášet nepravidelnosti do řízeného procesu, není jiná cesta, než urychlit zobrazení a zařídit, aby se z metody vrátilo bez ohledu na zobrazovanou hodnotu za (v tomto případě) 2 ms. A protože nejdelší je přenos dat do displeje, ani nelze na jedno volání překreslovat víc než dva znaky.

Demo použití ukazuje pomalý růst hodnoty (5 čárek za sekundu), kdy vykreslení s velkou rezervou stíhá sledovat požadavky, a následnou skokovou změnu na nuly, kdy je vidět rychlý a plynulý pokles za nejkratší možnou dobu.

IMGP1968b

Příklad použití v měřiči kapacity

Poslední ukázka na obrázku výše zachycuje praktické použití tohoto grafu v měřiči superkondenzátorů. Zbývá snad jen dodat, že z výšky znaku 8 obrazových bodů tento progress bar využívá jen 5 spodních bodů, aby nebyl v porovnání s textem až moc výrazný a také aby se od textu oddělil širší mezerou. Toto vlastně nijak nesouvisí s programem, je to výhradně věc definování uživatelských znaků na začátku a jejich změnou lze zvýšit čárky až na maximálních 8 bodů nebo naopak omezit třeba na jediný bod.


#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// nastavena adresa 0x3F a poradi pinu displeje na prevodniku: en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x3F, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

unsigned int currentProgress = 0; // Jaky stav je prave ted
unsigned long lastUpdate = 0;     // Cas zmeny
int smer = 0; 

void setup() {                    // Graficke znaky 6 a 7 jsou volne pro jine pouziti
  byte g0[8] = {B00000, B00000, B00000, B10101, B10000, B10000, B10000, B10101}; //0 |__
  byte g1[8] = {B00000, B00000, B00000, B10101, B10100, B10100, B10100, B10101}; //1 ||_
  byte g2[8] = {B00000, B00000, B00000, B10101, B10101, B10101, B10101, B10101}; //2 |||
  byte g3[8] = {B00000, B00000, B00000, B10101, B00000, B00000, B00000, B10101}; //3 ___
  byte g4[8] = {B00000, B00000, B00000, B10101, B00001, B00001, B00001, B10101}; //4 __|
  byte g5[8] = {B00000, B00000, B00000, B10101, B10001, B10001, B10001, B10101}; //5 |_|

  lcd.begin(20, 4);               // zavedeni grafiky do RAM displeje 20x4
  lcd.createChar(0, g0); lcd.createChar(1, g1); lcd.createChar(2, g2); lcd.createChar(3, g3);
  lcd.createChar(4, g4); lcd.createChar(5, g5);
}

// Inicializace nastavi progress a cas na nuly, vykresli prazdny progress bar na 3. radku
void initProgressBar() {
  smer = 0;
  currentProgress = 0;
  lastUpdate = 0;
  lcd.setCursor(0, 3);
  lcd.write((byte)0);             // posilame byte 0
  for (int i = 1; i < 19; i++) {
    lcd.write(3);
  }
  lcd.write(4);
  lcd.setCursor(0, 3);
}

// Posle progress baru pozadavek na zmenu. Posune se max o 1 "carku" bliz k pozadovane hodnote.
// Prubeh trva zhruba stejne dlouho pri jakemkoliv scenari.
// Pokud je moc "brzo" - nic nevykresli, pokud chci zobrazit uz zobrazovany stav - nic neprekresluje.
// @param progress - pozadovany cil 0-60 (na 20 znacich) respektive zobrazi 59 ruznych hodnot.
void updateProgress(int progress) {
  unsigned long currentMillis = millis();
  if (currentMillis - lastUpdate < 10) {   // 10 ms = refresh rate, kdy NEJDRIV se muze menit
    delay(1);
    return;
  }
  lastUpdate = currentMillis;
  progress = min(progress, 59);  progress = max(progress, 1); //60 znakovy bar, namapujem na 59 hodnot
  if (currentProgress == progress) {         //jen rostem
    delay(1);
    return;
  } else if (currentProgress < progress) {
    if(smer<0 && currentProgress%3==2){
      lcd.setCursor(currentProgress / 3, 3);  //presun na spravne souradnice 
      lcd.write(2);
      currentProgress++;
      smer = 1;
      return;
    }
    currentProgress++;
    lcd.setCursor(currentProgress / 3, 3);  //presun na spravne souradnice
    smer = 1;
    if (currentProgress < 57) {
      lcd.write(currentProgress % 3);       //vypise 1, 2 nebo 3 "carky" do znaku
    } else if (currentProgress == 57) {
      lcd.write(5);  // predposledni znak
    }  else if (currentProgress == 58) {
      lcd.write(2); //posledni znak
    }
  } else {
    if(smer>0 && currentProgress%3==0){
      lcd.setCursor(currentProgress / 3, 3);  //presun na spravne souradnice 
      lcd.write(3);
      currentProgress--;
      smer = -1;
      return;
    }
    currentProgress--;
    lcd.setCursor(currentProgress / 3, 3); //presun na spravne souradnice
    smer = -1;
    if (currentProgress < 57) {
      switch (currentProgress % 3) {
        case 0: lcd.write(3); break;
        case 1: lcd.write((byte)0); break;
        case 2: lcd.write(1); break;
      }
    } else if (currentProgress == 57) {
      lcd.write(4);                       // predposledni znak
    }  else if (currentProgress == 58) {
      lcd.write(5);                       //posledni znak
    }
  }
}

// demo na predvedeni - pomalu vykresli rostouci hodnoty 0 az 60, pocka, pak maximalni rychlosti dolu
void loop() {
  initProgressBar();                      // inicializace ProgressBaru
  while (1) {                             // nekonecny cyklus 
    for (int i = 0; i <= 60; i++) {       // pomalu rizene nahoru
      updateProgress(i);
      delay(200);
    }
    delay(1000);                          // pockat 1 s
    for (int i = 0; i < 1000; i++) {      // max rychlosti rovnou dolu (vcetne prodlevy asi 2 s)
      updateProgress(0);
      delay(2);
    }
  }
}