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é sú nasledovné komponenty. Všetky hyperlinky smerujú na Banggood. Displeje sa dajú kúpiť aj na Ebay alebo Aliexpress.
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:
Na ovládanie OLED dipleja pouźívam knižnicu U8g2. Dá sa nainštalovať v Správcovi knižníc.
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?
Ak je makro vypnuté.
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
}
Tu toho veľa nepribudlo. Iba jedno makro a dve funkcie. Všetko slúži na to, aby sa pohodlnejšie ladilo.
#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);
}
}
};
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 sa nachádza na GitHub.
15.12.2016