8x PWM na jednom Arduinu

Zápisník experimentátora

Hierarchy: Časovač (timer)

Arduino Uno má 6 PWM kanálů na pinech 3, 5, 6, 9, 10 a 11. V tomto článku si ukážeme, že to nemusí být konečný počet. Hardwarové PWM ale musíme nahradit vlastním kódem. V tomto příkladu využijeme timer, pomocí kterého si budeme generovat přerušení na dostatečně vysoké frekvenci, abychom byli schopni simulovat PWM na osmi kanálech najednou.

Použité součástky

K vytvoření animace můžeme použít:

  • Arduino Pro Mini (link) - Použil jsem ho proto, že se vejde i na nejmenší breadboard.
  • Mini Breadboard (link) - Nejmenší prodávané nepájivé pole. Použil jsem dvě a třeba si dát pozor, že se prodávají dvě modifikace tohoto breadboardu. Ty co mají drobné výstupky se dají takto pěkně navzájem pospojovat.
  • Převodník CP2102 USB to Serial (link) - Převodník slouží k naprogramování Arduina.
  • Desku s rezistory a LED diodami (link) - Protože mám vyrobených několik takových desek, použil jsem ji, abych nemusel komplikovaně připojit 8 LED a 8 rezistorů k pinům Arduina. Ale stejně můžete použít i přímo rezistory a LED, jen si dejte pozor, abyste nepřekročili maximální proud na pinu. Proto používejte rezistory od 330 do 1000 ohmů.

Vše je zapojené podle následujícího obrázku. LED diody jsou připojeny na piny RX (0), TX (1), 2-7 a na obrázku je ještě vidět připojení GND na mé LED desce pomocí dvou drátěných propojek k Arduinu. Arduino Pro Mini má vyvedeny GND i na druhé straně, takže jsem mohl použít i jedinou propojku, ale pak by se to špatně fotografovalo.

RX a TX jsou navzájem přehozené, protože se bude zobrazovat bitová podoba 8bitová čísla pomocí přímého zápisu do rejstříku PORTD a potřebujeme mít piny seřazeny stejně, jako jsou bity v registru.

Programování

Abychom mohli vytvořit efekt různého jasu všech diod, musíme vhodně zapínat a vypínat tyto LED diody a musíme to dělat tak rychle, aby lidské oko nevnímalo blikání, ale různou intenzitu světla. Na minimální plynulý obraz potřebujeme frekvenci minimálně 25 Hz, ale na plynulý dojem je lepší 50-75 Hz. A protože potřebujeme v každém jednom okénku naší animace zobrazit 256 odstínů, potřebujeme výslednou frekvenci 75x256 = 19200 Hz. Zaokrouhlíme to na 20 kHz.

Na této frekvenci potřebujeme vyvolávat přerušení, které na všech diodách nastaví jas tak, že danou diodu zapne nebo vypne. Čím bude dioda po celou dobu více zapnutá, tím bude jasnější. Zjednodušené schéma se třemi políčky po 10 úrovní jasu nám ukazuje, jak svítí různý čas čtyři diody. Přesně stejně to funguje i při naší frekvenci 20 kHz.

123456789012345678901234567890
x         x         x         
xx        xx        xx        
xxxxx     xxxxx     xxxxx     
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

To je celá podstata efektu. A teď pojďme na zdrojový kód.

Na začátku je možné definovat nebo vypoznámkovať dvě makra. Ty nemají vliv na výslednou animaci. Ovlivňují pouze to, zda budeme na port zapisovat rychle nebo funkcí digitalWrite.  A ovlivňují, zda nahradíme Arduino funkci main za naši, abychom docílili stav, že nám jádro Arduino neponastavuje registry podle svého implicitního nastavení. Obě volby v podstatě jen zkrátí výsledný kód na minimum. A případnému odpůrci Arduina ukážou, že přímo na port lze přistupovat iv Arduinu a že chápeme jak vlastně funguje linker.

#define DIRECT_WRITE
#define FAKE_MAIN

Proměnná counter slouží ke sledování pozice v rámci jasu v jednom okénku. Může obsahovat hodnoty 0-255. Podle hodnoty se rozhoduji, zda bude dioda svítit nebo ne. A hodnoty jasu pro každou diodu mám v poli light. Každá hodnota je dvojnásobkem předchozí, abych zohlednil nelineární chování lidského oka.

uint8_t counter = 0;
uint8_t light[] = {1, 2, 4, 8, 16, 32, 64, 128};

Tento kód jsem si kompletně nechal vygenerovat v mém kalkulátoru na CTC timer. Nastavená frekvence je 20 kHz a kalkulátor se postaral o vhodné nastavení registrů. Používám timer1.

void setupTimer() {
  noInterrupts();
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  
  OCR1A = 99; // 20000 Hz
  TCCR1A |= 0;
  TCCR1B |= (1 << WGM12);
  TCCR1B |= (0 << CS12) | (1 << CS11) | (0 << CS10);
  TIMSK1 |= (1 << OCIE1A);
  interrupts();
}

Ve funkci setup pouze nastavuji, které piny mají být výstupní. Díky tomu, že jsou všechny piny na portu D, můžeme využít i přímé nastavení registru.

void setup() {

#ifdef DIRECT_WRITE  
  DDRD = 0b11111111;
#else  
  for (uint8_t i = 0; i < 8; i++)
    pinMode(i, OUTPUT);
#endif    
  setupTimer();
}

Ve funkci loop neděláme nic. Veškerou práci vykoná obsluha přerušení. Pokud použijeme přímý zápis, musíme si připravit všech 8 bitů v pomocné proměnné tmp. Pokud je jas pro konkrétní LED vyšší než hodnota v proměnné counter, bude dioda svítit. V opačném případě bude zhasnutá. Z toho můžeme odvodit, že čím větší je číslo v light[i], tím bude dioda svítit jasnější.

void loop() {
}

ISR(TIMER1_COMPA_vect) {
  
#ifdef DIRECT_WRITE
  uint8_t tmp = 0;
  for (uint8_t i = 0; i < 8; i++)
    if(light[i] > counter)
      bitSet(tmp,i);
  PORTD = tmp;    
#else
  for (uint8_t i = 0; i < 8; i++)
    digitalWrite(i, light[i] > counter ? true : false);
#endif

  counter++;
  counter = counter % 255;
}

Vlastní funkce main. Toto můžeme udělat, protože linker postupně hledá zkompilované kousky kódu a pokud jsou dva s identickým názvem funkce, upřednostní ten první a druhý bude ignorovat. V tomto případě je tím prvním main vždy ta funkce, kterou umístíme do kódu našeho hlavního programu.

V programu si všimněte ještě jednu zajímavost. Ní je nastavení registru UCSR0B. Musíme to udělat, protože Arduino bootloader upraví chování pinů RX a TX a na těchto dvou pinech bychom neviděli naše PWM. Proto oba piny odpojíme od USART a dosáhneme požadované chování.

#ifdef FAKE_MAIN
int main(void) {
  UCSR0B = 0; // disable bootloader USART
  setup();
  while(1)
    loop();
return 0;
}
#endif

Zdrojový kód

Zdrojový kód se nachází na GitHub.

Video

Video je na YouTube.


13.04.2018


Menu