Sviečkové grafy na OLED displeji

Zápisník experimentátora

Hierarchy: Sviečkové grafy

Prednedávnom som sa venoval zobrazovaniu sviečkových grafov na displeji Nokia 5110. Teraz prichádza modifikácia knižnice pre OLED displej. V tomto článku si ukážeme, ako zobraziť graf na displeji 0,96 OLED s čipom SSD1306.

Použité moduly

Použité sú nasledovné komponenty. Všetky hyperlinky smerujú na Banggood. Displeje sa dajú kúpiť aj na Ebay alebo Aliexpress.

  • 7pin 0.96 in SPI OLED 128x64 White (link) - Podstatné je, aby bol displej označený ako SPI. Tieto sú výrazne rýchlejšie než I2C verzie. Predávajú sa v troch farebných modifikáciách. Bielej, modrej a dvojfarebnej. Displej znesie napätie 5 V.
  • Arduino Pro Mini (link) - Miniatúrna verzia Arduina.
  • Skúšobné pole (link) - Do skúšobného poľa sa to všetko zasunie a prepojí vodičmi.

Program

Program je rozšírením predchádzajúcich zdrojových textov. Pretože som teraz pridal dva nové renderery, čim sa ich celkový počet zodvihol na 5, bolo potrebné zdrojové texty rozdeliť na viac súborov. Prispelo to k prehľadnosti. Zdrojové texty teraz obsahujú súbory:

  • ohlc_oled_random.ino - Hlavný program.
  • candlestick.h - Šablóny sviečkových grafov.
  • nokia5110.h - Renderery pre displej Nokia 5110.
  • oled.h - Renderery pre displej OLED.
  • serial.h - Renderer pre sériový port.

Na ovládanie OLED dipleja pouźívam knižnicu U8g2. Dá sa nainštalovať v Správcovi knižníc.

ohlc_oled_random.ini

Pretože som využil knižnicu U8g2, je v tomto kóde veľa zmien. V tomto článku preberieme iba základy kreslenia pomocou knižnice. Na samotnú knižnicu venujem neskôr samostatný článok.

Ak máte dostatok RAM, zadefinujte si makro BIG_MEMORY. Arduino Pro Mini nemá dostatok voľnej pamäte, resp. jej pri nastavenom makre ostane tak málo, že je program možné použiť iba na demonstráciu možností. Ale Arduina s väčšou pamäťou môžu používať makro a dosiahnuť tak skoro dvojnásobnú rýchlosť vykresľovania. Čo sa udeje pri zapnutom makre?

  • Použije sa trieda U8G2_SSD1306_128X64_NONAME_F_4W_HW_SPI. V krkolomnom názve F znamená, že kreslíme všetko v jednom prechode a HW_SPI znamená, že používame hardverové SPI.
  • Začiatok kreslenia vykoná funkcia u8g2.clearBuffer().
  • Koniec kreslenia vykoná funkcia u8g2.sendBuffer().

Ak je makro vypnuté.

  • Použije sa trieda U8G2_SSD1306_128X64_NONAME_1_4W_HW_SPI. V krkolomnom názve 1 znamená, že kreslíme vo viac prechodoch a HW_SPI znamená, že používame hardverové SPI.
  • Začiatok kreslenia vykoná funkcia u8g2.firstPage().
  • Koniec kreslenia vykoná funkcia u8g2.nextPage().

Rozdiel je v rýchlosti, pretože druhá možnosť musí pripravovať stránku postupne a to stojí čas. Na druhej strane ale spotrebuje čo najmenej RAM.

// If you have more than 2k RAM
//#define BIG_MEMORY

#if defined(BIG_MEMORY)
  #define SSD1306 U8G2_SSD1306_128X64_NONAME_F_4W_HW_SPI
#else
  #define SSD1306 U8G2_SSD1306_128X64_NONAME_1_4W_HW_SPI
#endif

#include <U8g2lib.h>
#include "candlestick.h"
#include "oled.h"

SSD1306 u8g2(U8G2_R0, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8);

OHLCChart<double, 19, 10000, OHLCOledBarRender<double> > ohlc;
//OHLCChart<double, 19, 10000, OHLCOledLineRender<double> > ohlc;

double value=22.4;

void setup() {
  Serial.begin(9600);

  u8g2.begin();

  // connect renderer to OLED
  ohlc.getRender().setDisplay(&u8g2);
  ohlc.setGridStep(1.);
}

void loop() {
  // update random data
  int valuemove=random(30)-15;
  value+=valuemove/100.;
  value=constrain(value,10,40);
  //Serial.println(value);
  ohlc.addValue(value);

#if defined(BIG_MEMORY)
  u8g2.clearBuffer();
#else  
  u8g2.firstPage();
  do {
#endif
  u8g2.setFont(u8g2_font_6x10_tn);

  // draw candlesticks
  ohlc.draw();

  // draw last value
  u8g2.setDrawColor(0);
  u8g2.drawBox(5,4,5+5*5+3,11);
  u8g2.setDrawColor(1);
  u8g2.drawFrame(5,4,5+5*5+3,11);
  u8g2.setCursor(7,13);
  u8g2.print(value);

  // draw indicator
  int _v=fmap(value,ohlc.minimum,ohlc.maximum,0.,64.);
  u8g2.drawLine(126,63-_v,128,63-_v);
  u8g2.drawLine(127,63-_v+1,127,63-_v-1);

#if defined(BIG_MEMORY)
  u8g2.sendBuffer(); // transfer internal memory to the display
#else
  } while ( u8g2.nextPage() );
#endif

  delay(200); // 5 values/second
} 

candlestick.h

Tu toho veľa nepribudlo. Iba jedno makro a dve funkcie. Všetko slúži na to, aby sa pohodlnejšie ladilo.

  • TRACE - Makro slúži na vypísanie hodnoty premenných na sériový port.
  • addOhlc - Pridanie celej OHLCData štruktúry v jednom kroku. Funkciu som používal prevažne na ladenie a nastavenie grafu do podoby, ktorý mi umožnila otestovať všetky kombinácie open, high, low a close.
  • dump - Vypísanie kompletného grafu na sériový port. Využíva sa makro TRACE.
#define TRACE(var)       \
 {                       \ 
 Serial.print("Trace: "); \
 Serial.print(#var);     \
 Serial.print("=");      \
 Serial.println(var);    \
 }

template<typename ohlcvalue>
ohlcvalue fmap(ohlcvalue x, ohlcvalue in_min, ohlcvalue in_max, ohlcvalue out_min, ohlcvalue out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

///
/// One OHLC bar data
///
template<typename ohlcvalue>
struct OHLCData {
  ohlcvalue o;
  ohlcvalue h;
  ohlcvalue l;
  ohlcvalue c;  
};

///
/// OHLC recorder and renderer
/// ohlcvalue - int or float
/// len - number of bars
/// stp - time window (10000 = 10 seconds)
/// renderclass - which renderer we want to use
///
template<typename ohlcvalue, const int len, const long stp, typename renderclass>
class OHLCChart {
  OHLCData<ohlcvalue> data[len];
  const unsigned long ohlc_step;
  int ohlc_first;
  OHLCData<ohlcvalue> *last;
  unsigned long ohlc_next_step;
  renderclass render;
  ohlcvalue grid_step;
  
public:
  ohlcvalue minimum;
  ohlcvalue maximum;

  OHLCChart() : ohlc_step(stp), ohlc_first(len), ohlc_next_step(0) {
    last = data + (len - 1);
  }

  renderclass& getRender() {
    return render;
  }

  void setGridStep(ohlcvalue value) {
    grid_step = value;
  }
  
  boolean addValue(ohlcvalue v) {
    unsigned long m=millis();
    if(m>ohlc_next_step) {
      if(ohlc_first>0)
        ohlc_first--;
      for(int i=0;i<(len-1);i++)
        data[i] = data[i+1];
      last->o = v;
      last->h = v;
      last->l = v;
      last->c = v;
      ohlc_next_step+=ohlc_step;
      return true;
    } else {
      if(last->h<v)
        last->h=v;
      if(last->l>v)
        last->l=v;
      last->c=v;
    }
  return false;
  }

  void addOhlc(OHLCData<ohlcvalue> d) {
    if(ohlc_first>0)
      ohlc_first--;
    for(int i=0;i<(len-1);i++)
      data[i] = data[i+1];
    *last = d;  
  }

  void draw() {
    render.drawHeader();
    if(ohlc_first!=len) {
      minimum = data[ohlc_first].l;
      maximum = data[ohlc_first].h;
      for(int i=ohlc_first+1;i<len;i++) {
        if(data[i].l<minimum)
          minimum = data[i].l;
        if(data[i].h>maximum)
          maximum = data[i].h;
      }
      if(grid_step>0 && minimum!=maximum)
        render.drawGrid(minimum,maximum,grid_step);
    }
    for(int i=ohlc_first;i<len;i++)
      render.drawBar(i,data+i,minimum,maximum);
    render.drawFooter();
  }

  void dump() {
    TRACE(len);
    TRACE(ohlc_first);
    for(int i=ohlc_first;i<len;i++)
      {
      TRACE(i);
      TRACE(data[i].o);
      TRACE(data[i].h);
      TRACE(data[i].l);
      TRACE(data[i].c);
      }
  }

};

oled.h

Renderery. Opäť som použil osvedčenú kombináciu bázovej triedy so všeobecne platnými funkciami a dvomi špecializovanými renderermi. Jeden na kreslenie čiarových grafov a druhý na kreslenie stĺpcových grafov. V podstate sa tento kód nijako mimoriadne neodlišuje od kódu pre Nokiu 5110. Iba sú zväčšené konštanty na 64 pixelov na výšku a používajú sa funkcie knižnice U8g2 na kreslenie.

///
/// OLED base renderer
///
template<class ohlcvalue>
class OHLCOledBaseRender {
protected:
  SSD1306 *display;
public:
  OHLCOledBaseRender() : display(NULL) {}

  void drawHeader() {}
  void drawFooter() {}
  void setDisplay(SSD1306 *d) {display=d;}
  void drawGrid(ohlcvalue minimum, ohlcvalue maximum, ohlcvalue grid_step) {
    const int HEIGHT = 63;
    int dv = minimum / grid_step;
    int _gs = fmap(minimum+grid_step,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT);
    ohlcvalue bg = (dv + 0) * grid_step;
    for(ohlcvalue i = bg;i<maximum+grid_step;i+=grid_step) {
      for(int j=0;j<=128;j+=5)
        display->drawPixel(j,HEIGHT-(int)fmap(i,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT));
      if(_gs>7) { // not too dense
        display->setCursor(0,HEIGHT-(int)fmap(i,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT)-1);
        display->print(i);
        }
      }
    }
};

///
/// OLED Line Candlestick renderer
///
template<typename ohlcvalue>
class OHLCOledLineRender : public OHLCOledBaseRender<ohlcvalue> {
public:
  void drawBar(int pos, OHLCData<ohlcvalue> *bar, ohlcvalue minimum, ohlcvalue maximum) {
    if(this->display==NULL)
      return;
    if(minimum==maximum)
      return;

    OHLCData<ohlcvalue> _bar=*bar;
      
    int START = 5 + pos * 6;
    const int BW = 2;
    const int HEIGHT = 63;

    // transformation
    _bar.o = fmap(_bar.o,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT);
    _bar.h = fmap(_bar.h,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT);
    _bar.l = fmap(_bar.l,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT);
    _bar.c = fmap(_bar.c,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT);

    // open
    this->display->drawLine(START,HEIGHT-_bar.o,START+1*BW,HEIGHT-_bar.o);

    // high - low
    this->display->drawLine(START+1*BW,HEIGHT-_bar.h,START+1*BW,HEIGHT-_bar.l);

    // close
    this->display->drawLine(START+1*BW,HEIGHT-_bar.c,START+2*BW,HEIGHT-_bar.c);
    }
};

///
/// OLED Bar Candlestick renderer
///
template<class ohlcvalue>
class OHLCOledBarRender : public OHLCOledBaseRender<ohlcvalue> {
public:
  void drawBar(int pos, OHLCData<ohlcvalue> *bar, ohlcvalue minimum, ohlcvalue maximum) {
    if(this->display==NULL)
      return;
    if(minimum==maximum)
      return;

    //TRACE(minimum);
    //TRACE(maximum);

    OHLCData<ohlcvalue> _bar=*bar;
      
    int START = 5 + pos * 6;
    const int BW = 2;
    const int HEIGHT = 63;
    int top;
    int bottom;

    // transformation
    _bar.o = fmap(_bar.o,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT);
    _bar.h = fmap(_bar.h,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT);
    _bar.l = fmap(_bar.l,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT);
    _bar.c = fmap(_bar.c,minimum,maximum,(ohlcvalue)0,(ohlcvalue)HEIGHT);

    //TRACE(pos);
    //TRACE(_bar.o);
    //TRACE(_bar.h);
    //TRACE(_bar.l);
    //TRACE(_bar.c);

    if(_bar.o < _bar.c) {
      top = _bar.c;
      bottom = _bar.o;
    } else {
      top = _bar.o;
      bottom = _bar.c;
    }

    //TRACE(top);
    //TRACE(bottom);

    // high
    this->display->drawLine(START+1*BW,HEIGHT-_bar.h,START+1*BW,HEIGHT-top);

    // low
    this->display->drawLine(START+1*BW,HEIGHT-_bar.l,START+1*BW,HEIGHT-bottom);

    // bar
    if(_bar.o < _bar.c) {
      this->display->setDrawColor(0);
      this->display->drawBox(START,HEIGHT-top,2*BW+1,top-bottom);
      this->display->setDrawColor(1);
      this->display->drawFrame(START,HEIGHT-top,2*BW+1,top-bottom);
      }
    else
      this->display->drawBox(START,HEIGHT-top,2*BW+1,top-bottom);
    }
};

Zdrojový kód

Zdrojový kód sa nachádza na GitHub.


15.12.2016


Menu