Arduino a výstup na TV přijímač (2)

Pořídit si z Arduina výstup na TV přijímač nebo videomonitor není jen pro ty, kdo chtějí zkoušet psát jednoduché hry, ale je to praktický a kupodivu velmi levný prostředek, který poslouží při psaní programů a odlaďování funkce zařízení. Tentokrát se podíváme na konkrétní příkazy knihovny TVout a příklady použití.

Příkazy

Přičleníme-li knihovnu TVout, vytvoří se v našem programu na začátku dva řádky:

#include <TVout.h>

#include <video_gen.h>

a my případně (budeme-li chtít využívat různé rozměry písma) přidáme třetí s přičleněním fontů:

#include <fontALL.h>

Následně můžeme používat mimo jiné tyto příkazy. Jako vždy je důležité rozlišení velkých a malých písmen.

  • TVout jmeno; – vytvoření objektu jmeno třídy TVout (píše se typicky mezi deklarace proměnných)

  • jmeno.begin(video); parametr video nabývá hodnot _PAL nebo _NTSC. Nastaví zadanou normu a spustí generování obrazu

  • jmeno.clear_screen(); – smaže obsah displeje (celá pracovní plocha černá)

  • jmeno.invert(); – invertuje celou pracovní plochu displeje

  • jmeno.set_pixel(x,y,barva); vykreslí bod v absolutních souřadnicích x,y. Parametr barva: 0 – černá,1 – bílá, 2 – inverzně. Souřadnice (parametry) jsou 0-127 a 0-95, takže typicky typu int.

  • jmeno.get_pixel(x,y); – funkce vrací barvu bodu v daném bodě x,y: 0–černá, 1-bílá

  • jmeno.draw_line(x1,y1,x2,y2,barva); – vykreslí čáru v absolutních souřadnicích od bodu x1,y1 do bodu x2,y2. Parametr barva: 0–černá,1–bílá, 2-inverzně

  • jmeno.draw_row(y,x0,x1,barva); – velmi rychlevykreslí vodorovnou čáru od bodu x0,y do bodu x1,y barvou 0–černá,1–bílá, 2-inverzně

  • jmeno.draw_column(x,y0,y1,barva);– velmi rychle vykreslí svislou čáru od bodu x,y0 do bodu x,y1 barvou 0–černá,1–bílá, 2-inverzně

  • jmeno.draw_rect(x,y,sirka,vyska,barva,vypln); vykreslí obdélník od bodu x,y (levý horní roh) o šířce sirka, a výšce vyska s barvou čáry (0–černá,1–bílá, 2-inverzně) a s výplní (0–černá,1–bílá, 2-inverzně,-1-nechat původní). Parametr vypln není povinný, bez něj se ponechá původní obsah plochy.

  • jmeno.draw_circle(x,y,polomer,barva,vypln); vykreslí kružnici se středem v bodě x,y o poloměru polomer, s barvou čáry (0–černá,1–bílá, 2-inverzně) a s výplní (0–černá,1–bílá, 2-inverzně,-1-nechat původní). Parametr vypln není povinný.

  • jmeno.shift(vzdalenost,smer); posune obsah displeje o počet bodů udaných parametrem vzdalenost ve směru 0-nahoru, 1-dolů, 2-vlevo, 3-vpravo. Obsah, který „vyjede“ ze zobrazovaného pole se ztrácí. Používá se typicky k „rolování“ obrazovky.

  • jmeno.print_char(x,y,chr); – od bodu x,y (levý horní roh znaku) vypíše znak s kódem chr

  • jmeno.set_cursor(x,y);  – nastaví pozici textového kurzoru na bod x,y

  • jmeno.select_font(font); – nastaví font – parametr může být font4x6, font6x8, font8x8 nebo font8x8ext. Externí fonty lze předefinovat a vytvořit si tak v podstatě libovolnou grafiku.

  • jmeno.print(x,y,str); – vypíše na pozici x,y řetězec str, při přesahu přes kraj obrazovky přechází na další řádek

  • jmeno.print(str); – vypíše na pozici textového kurzoru řetězec str

  • jmeno.print(x,y,int,base); – vypíše od pozice x,y proměnnou typu int (unsigned int, long, unsigned long) udanou v zadané soustavě (parametr base: 2, 8, 10, 16). Převod funguje i pro jakýkoli jiný základ soustavy. Parametr base se nemusí uvádět, implicitní je desítková soustava.

  • jmeno.print(x,y,double,presnost); vypíše na pozici x,y hodnotu proměnné typu double s přesností na daný počet desetinných míst, bez udání přesnosti na 2 desetinná místa. Pozice x,y není povinná, může pracovat podle poslední polohy kuzroru.

  • jmeno.delay_frame(pocet); čeká daný počet snímků podle parametru počet, slouží k synchronizaci s vykreslováním obrazu

  • jmeno.tone(frekvence, delka); – jako generování tónu standardním tone, ale funguje správně i při generování obrazu a vždy je pro pin 11 u Arduina Uno

  • všechny příkazy print existují i ve verzi println s odřádkováním

 

  Jednoduché příklady

První příklad pokreslí obrazovku sítí bodů (rozsvítí bíle sudé body v řádcích, ale jen na sudých řádcích) a potom se snaží cyklicky programem generovat pulzy na výstupu 5. Podobný program byl použit při snímání obrázku dokumentujícího vliv generování obrazu respektive interruptů na pravidelnost pulzů.

//video test - sude body v sudych radcich
#include <TVout.h>               // knihovna TVout
#include <video_gen.h>
TVout TV;                        // objekt TV tridy TVout  

void setup(){                    // nastaveni
  TV.begin(_PAL);                // generovani signalu PAL
  TV.clear_screen();             // smazani obrazovky
  for (int i=0;i<96;i=i+2){      // cyklus pro radky
    for (int j=0;j<128;j=j+2){   // cyklus pro body v radku
      TV.set_pixel(j,i,1);       // vykresleni bodu
    }
  }
  pinMode(5,OUTPUT);             // pin 5 na vystup
}

void loop(){                     // opakovany program
  digitalWrite(5,HIGH);          // pulzy na vystup 5
  digitalWrite(5,LOW);           // 
}

Další příklad smaže obrazovku, počká 1s, vykreslí bílý rámeček kolem celé pracovní plochy, pak 1s počká a vše se opakuje. Na pohled rámeček bliká.

//video test - blikajici ramecek
#include <TVout.h>            // knihovna TVout
#include <video_gen.h>
TVout TV;                     // objekt TV tridy TVout  

void setup(){                 // nastaveni
  TV.begin(_PAL);             // generovani signalu PAL
}

void loop(){                  // opakovany program
  TV.clear_screen();          // smazani obrazovky
  delay(1000);                // pockat 1s 
  TV.draw_rect(0,0,127,95,1); // obdelnik kolem cele plochy
  delay(1000);                // pockat 1s
}

Třetí příklad vypíše největším možným písmem nahoru na obrazovku řadu číslic, pak počká, a nechá je plynule přejet a zmizet směrem dolů.

//video test - rolovani
#include <TVout.h>            // knihovna TVout
#include <video_gen.h>
#include <fontALL.h>          // knihovny vsech fontu
TVout TV;                     // objekt TV tridy TVout
void setup(){                 // nastaveni
  TV.begin(_PAL);             // generovani signalu PAL
  TV.select_font(font8x8);    // nastaveni fontu
}
void loop(){                  // opakovany program
  TV.print(0,0,"1234567890"); // text vypsat na 0,0
  delay(1000);                // pockat 1s
  for(int i=0;i<96;i++){      // 96x
    TV.shift(1,1);            //   rolovat dolu o 1 bod
    delay(1);}                // doba mezi posuny
  delay(1000);                // pockat 1s
 }

Čtvrtý příklad vypíše na obrazovku kousek textu a následně jej nechá po dvou sekundách přezrcadlit. Ukazuje zejména, že na obrazovku se dá nejen zapisovat, ale také z ní obsah číst a je-li to potřeba, třeba i odeslat na tiskárnu.

//video test - zrcadleni
#include <TVout.h>                  // knihovna TVout
#include <video_gen.h>
#include <fontALL.h>                // knihovny vsech fontu
TVout TV;                           // objekt TV tridy TVout  
int nahore, dole;                   // promenne pro prohozeni

void setup(){                       // nastaveni
  TV.begin(_PAL);                   // generovani signalu PAL
  TV.select_font(font8x8);          // nastaveni fontu
  TV.print(0,0, "123456789012345"); // text vypsat
  TV.print(0,12,"Toto je pokusny"); // text vypsat
  TV.print(0,24,"text k ukazce  "); // text vypsat
  TV.print(0,36,"zrcadleni");       // text vypsat
}

void loop(){                        // opakovany program
  for(int i=0;i<47;i++){            // cyklus pro radky
    for(int j=0;j<128;j++){         // cyklus pro sloupce
      nahore=TV.get_pixel(j,i);     // nacist horni bod
      dole=TV.get_pixel(j,95-i);    // nacist dolni bod 
      TV.set_pixel(j,i,dole);       // ulozit horni bod
      TV.set_pixel(j,95-i,nahore);  // ulozit dolni bod
    }
  }    
  delay(2000);                      // pockat 2s
}

Pátý příklad představí více z možností grafiky jednoduchým (ale nikoli krátkým) programem. Cílem je nechat cyklicky střídat tři „obrazovky“. První ukáže základní vykreslené tvary, druhá fonty a třetí předvede náhodný „pohyb“ objektu v reálném čase. Nic se neovládá, zapojen je jen základní videoadaptér k Arduinu. Je-li to možné, nastavte televizní přijímač nebo monitor na poměr stran obrazu 4:3, pak jsou body téměř čtvercové a zejména kružnice vypadají jak mají.

//video1 - predvedeni grafiky
#include <TVout.h>                      // knihovna TVout
#include <video_gen.h>
#include <fontALL.h>                    // vsechny fonty
TVout TV;                               // objekt TV tridy TVout  
int x1,y1,x2,y2;                        // pracovni promenne pro pohyb
float krok,krokx,kroky;                 // pracovni promenne pro pohyb
void setup(){                           // nastaveni
  TV.begin(_PAL);                       // generovani signalu PAL
  TV.clear_screen();                    // smazani obrazovky
}
void loop(){                            // smycka programu
// Cast 1 - Obrazovka Zakladni tvary  
  TV.draw_rect(0,0,127,95,1);           // ramecek kolem cele plochy
  for (int i=0; i<7; i++){              // ctverce
    TV.draw_rect(3*i+8,3*i+8,50-6*i,40-6*i,1); 
  }
  for (int i=0; i<5; i++){              // kruznice
    TV.draw_circle(100,28,i*5,1); 
  }
  delay(500);                           // pockat 0,5s
  for (int i=5; i<123; i++){            // cary invertujici
    TV.draw_line(64,55,i,90,2);
    delay(20); 
  }
  delay(6000);                          // pockat 6s
  for (int i=0; i<98; i++){             // odrolovani nahoru
    TV.shift(1,0);
    delay(5); 
  }
// Cast 2 - Obrazovka Fonty a vypis hodnot  
  TV.select_font(font8x8);              // nastavit font 8x8
  TV.print(0,0,"Font 8x8 - ABCD"); 
  TV.select_font(font6x8);              // nastavit font 6x8
  TV.print(0,10,"Font 6x8 - ABCDEFGHIJ");  
  TV.select_font(font4x6);              // nastavit font 4x6
  TV.print(0,18,"Font 4x6 - ABCDEFGHIJKLMNOPQRST");
  TV.select_font(font6x8);              // dale font 6x8
  for (int j=0; j<6; j++){              // vypis tabulky znaku
    for (int i=0; i<16; i++){
      TV.print_char(i*8,j*8+40,j*16+i+32); 
      delay(20);
    }
  }
  delay(6000);                          // pockat 6s
  for (int i=0; i<129; i++){            // odrolovani vlevo
    TV.shift(1,2);
    delay(5); 
  }  
// Cast 3 - Obrazovka nahodny pohyb
  TV.print(0,0,"Nahodny pohyb");        // nadpis  
  x1=64; y1=48;                         // pocatecni poloha
  for (int i=0;i<20;i++){               // 20 pohybu
    x2=random(0,120);                   // nove souradnice
    y2=random(8,87);                      
    krok=sqrt(pow(x2-x1,2)+pow(y2-y1,2)); // delka pohybu
    krokx=(x2-x1)/krok;                 // krok pro x
    kroky=(y2-y1)/krok;                 // krok pro y
    for (int j=0;j<int(krok);j++){      // pohyb po krocich
      TV.draw_rect(int(krokx*j+x1-1),int(kroky*j+y1),6,6,1);// vypsat  
      TV.delay_frame(1);                // pockat a synchronizovat 
      TV.draw_rect(int(krokx*j+x1-1),int(kroky*j+y1),6,6,0);// smazat 
    }
  x1=x2; y1=y2;                         // dalsi pohyb
  }
  for (int i=0; i<129; i++){            // odrolovani vpravo
    TV.shift(1,3);
    delay(5); 
  } 
}                                       // konec programu

Zatím uvedené příklady měly za úkol jen předvést možnosti a ukázat způsob zápisu, poslední bude mít trochu praktičtější použití. Doplníme zapojení o dva trimry, dvě tlačítka a piezoreproduktor. Jeden trimr vytvoří změny napětí, to budeme měřit a zobrazíme pomocí virtuálního ručkového měřidla. Druhý trimr udává svým napětím mezní povolenou hodnotu. Je-li jím stanovená mez na první vstupu překročena, oznámí se to jako chyba přerušovaným tónem. Chyba a její indikace trvá i když následně skončí důvody pro její vyhlášení. Jedno tlačítko resetuje sledování minima a maxima, druhé tlačítko ruší hlášení chyby. Žádná část měřidla nesmí za provozu viditelně blikat, tón musí znít čistě.

merak

//video2 - analogove meridlo
#include <TVout.h>                         // knihovna TVout
#include <video_gen.h>
#include <fontALL.h>                       // vsechny fonty
TVout TV;                                  // objekt TV tridy TVout
float krok=0.004887586;                    // prevod bit/napeti V
int U1,U2,U1s,U2s,Umax,Umin,Umez;          // pracovni promenne napeti
unsigned long cykl;                        // pocitani cyklu
boolean chyba;                             // prekroceni meze napeti

void setup(){                              // nastaveni
  pinMode(2,INPUT_PULLUP);                 // vstup tlacitko
  pinMode(3,INPUT_PULLUP);                 // vstup tlacitko
  TV.begin(_PAL);                          // generovani signalu PAL
  TV.clear_screen();                       // smazani obrazovky
  TV.draw_rect(0,0,127,95,1);              // ramecek meridlo
  for (int i=14;i<=114;i=i+2){             // stupnice male dilky
    TV.draw_line(i,30,i,25,1);}
  for (int i=14;i<=114;i=i+10){            // stupnice velke dilky
    TV.draw_line(i,30,i,20,1);}
  TV.select_font(font4x6);                 // nastavit font 4x6
  for (int i=0;i<6;i++){                   // popis meritka
    TV.print(i*20+13,13,i);}
  TV.select_font(font6x8);                 // nastavit font 6x8
  TV.print(7,85,"Min");                    // vypis Min
  TV.print(75,85,"Max");                   // vypis Max
  Umax=0; Umin=1023; chyba=false;          // pocatecni stavy
}

void loop(){                               // smycka programu
  U1=analogRead(5);                        // mereni napeti
  U2=analogRead(4);                        // mereni meze
  TV.draw_rect(13,32,102,52,0,0);          // mazani rucky
  TV.draw_line(64,85,map(U1,0,1023,14,114),32,1); // rucka
  TV.draw_circle(64,85,3,1,1);             // stred meridla
  if (U1>Umax)Umax=U1;                     // kontrola mezi
  if (U1<Umin)Umin=U1;                     //
  TV.select_font(font6x8);                 // nastavit font 6x8  
  TV.print(28,85,float(Umin*krok));        // vypis Min
  TV.print(98,85,float(Umax*krok));        // vypis Max
  Umez=map(U2,0,1023,14,114);              // vypocet meze
  TV.set_pixel(Umez,32,1);                 // ukazatel meze
  TV.draw_row(33,Umez-1,Umez+2,1);         // ukazatel meze
  if(digitalRead(2)==LOW){                 // TL - mazani min a max
    Umax=0; Umin=1023;}
  if(digitalRead(3)==LOW) chyba=false;     // TL - mazani chyby
  if(U1>U2) chyba=true;
  cykl++;                                  // pocitani cyklu
  if(chyba && cykl%25==0) TV.tone(800,250);// zvuk chyby
  TV.delay_frame(1);                       // synchronizace
}                                          // konec programu

IMGP9899b

Na tomto programu stojí za to si podrobněji všimnout nejen způsobu vytvoření virtuálních měřidel, ale ještě něčeho. Jen ty části měřidla, které se mění a tudíž musí překreslovat, se obsluhují v cyklu. Tím se výrazně šetří čas.

Další věcí je na první pohled nenápadná, ale velmi důležitá synchronizace, kterou má na starosti poslední příkaz TV.delay_frame . Pokud jej vypustíte, program samozřejmě poběží, ale uvidíte obrovský rozdíl. Programová smyčka trvá určitou dobu a pracuje v situaci, kdy se (naprosto pravidelně) odskakuje interruptem do zobrazení a přenáší obsah paměti na obrazovku. Když jsou tyto dva děje nezávislé na sobě, bude se stávat, že se například čára smaže těsně před tím, než by měla být zobrazena, pak proběhne zobrazení a čára se vykreslí do paměti v době, kdy už je po vykreslení na obrazovku. Důsledkem toho je, že čára není vidět, dokud se zase načasování zobrazení a běhu prohramu „nerozjede“. Na pohled se to projevuje jako velmi rušivé blikání některých nebo všech částí grafiky, těch, které jsou pravidelně překreslovány.

Celá smyčka našeho programu je schopná proběhnout v době mezi vykreslováním dat na obrazovku. Proto je na konci (mohlo by to být i na začátku) příkaz TV.delay_frame(1), který na rozdíl od „obyčejného“ delay nemá za úkol jen čekat, ale synchronizovat program podle zobrazení, čekat až proběhne vykreslení jednoho snímku na obrazovku (čas je udáván v těchto snímcích). Jeden průběh smyčky se dokončí dřív než se vykresluje, takže v době vykreslování už nedochází k žádným změnám a obraz je klidný, nebliká.

Pokud by byl program složitější a trval déle, než se dá mezi snímky stihnout, bylo by nutné „rozsekat“ více příkazy delay_frame program na více částí, které by se stihnout mezi snímky daly, přitom každá část by musela plně obsloužit své objekty (čáry, rámečky, body, …). Zajistit klidný výsledný obraz znamená nejen naprogramovat úlohu tak, aby vůbec fungovala, ale současně ji celou správně načasovat a synchronizovat s generováním obrazu.

Podobné je to s generováním zvuku. Obvyklý příkaz tone zde selže a vytvoří jen chrčení, je třeba použít příkazu z knihovny TV.tone, ten na daném pinu fungovat bude.

Závěr

Na stejných knihovnách obohacených využitím možnosti měnit externí fonty 8×8 bodů je postavena řada her dostupných na internetu – například  Tetris, TV tenis nebo Life. To je jistě také zajímavé a víceméně se tím Arduino dostává do pozice, v níž byly před mnoha léty osmibitové počítače, jen je za nesrovnatelně nižší cenu. Jak už jsem uvedl na začátku potenciál spolupráce s televizí nebo videomonitorem ale vidím hlavně jinde, v názornějším výstupu dat z čidel při zkouškách zapojení, ve výpisech při ladění programu a podobně. Používáte-li Arduino, zkuste to, není to nijak složité a žádné náklady to nevyžaduje.