8x PWM na jednom Arduine

Zápisník experimentátora

Hierarchy: Časovač (timer)

Arduino Uno má 6 PWM kanálov na pinoch 3, 5, 6, 9, 10 a 11. V tomto článku si ukážeme, že to nemusí byť konečný počet. Hardverové PWM ale musíme nahradiť vlastným kódom. V tomto príklade využijeme timer, pomocou ktorého si budeme generovať prerušenie na dostatočne vysokej frekvencii, aby sme boli schopní simulovať PWM na ôsmich kanáloch naraz.

Použité súčiastky

Na vytvorenie animácie môžeme použiť:

  • Arduino Pro Mini (link) - Použil som ho preto, lebo sa zmestí aj na najmenšie skúšobné pole.
  • Mini Breadboard (link) - Najmenšie predávané skúšobné pole. Použil som dve a treba si dať pozor, že sa predávajú dve modifikácie tohoto breadboardu. Tie čo majú drobné výstupky sa dajú takto pekne navzájom pospájať.
  • Prevodník CP2102 USB to Serial (link) - Prevodník slúži na naprogramovanie Arduina.
  • Dosku s rezistormi a LED diódami (link) - Pretože mám vyrobených niekoľko takýchto dosiek, použil som ju, aby som nemusel komplikovane pripojiť 8 LED a 8 rezistorov k pinom Arduina. Ale rovnako môžete použiť aj priamo rezistory a LED, len si dajte pozor, aby ste neprekročili maximálny prúd na pine. Preto používajte rezistory od 330 do 1000 ohmov.

Všetko je zapojené podľa nasledovného obrázka. LED diódy sú pripojené na piny RX (0), TX (1), 2-7 a na obrázku je ešte vidno pripojenie GND na mojej LED doske pomocou dvoch drôtených prepojok k Arduinu. Arduino Pro Mini má vyvedené GND aj na druhej strane, takže som mohol použiť aj jedinú prepojku, ale potom by sa to zle fotografovalo.

RX a TX sú navzájom prehodené, pretože sa bude zobrazovať bitová podoba 8bitového čísla pomocou priameho zápisu do registra PORTD a potrebujeme mať piny zoradené rovnako, ako sú bity v registri.

Programovanie

Aby sme mohli vytvoriť efekt rôzneho jasu všetkých diód, musíme vhodne zapínať a vypínať tieto LED diódy a musíme to robiť tak rýchlo, aby ľudské oko nevnímalo blikanie, ale rôznu intenzitu svetla. Na minimálny plynulý obraz potrebujeme frekvenciu minimálne 25 Hz, ale na plynulý dojem je lepšie 50-75 Hz. A pretože potrebujeme v každom jednom okienku našej animácie zobraziť 256 odtieňov, potrebujeme výslednú frekvenciu 75x256=19200 Hz. Zaokrúhlime to na 20 kHz.

Na tejto frekvencii potrebujeme vyvolávať prerušenie, ktoré na všetkých diódach nastaví jas tak, že danú diódu zapne alebo vypne. Čím bude dióda počas celej doby viac zapnutá, tým bude jasnejšia. Zjednodušená schéma s tromi políčkami po 10 úrovní jasu nám ukazuje, ako svietia rôzny čas štyri diódy. Presne rovnako to funguje aj pri našej frekvencii 20 kHz.

123456789012345678901234567890
x         x         x         
xx        xx        xx        
xxxxx     xxxxx     xxxxx     
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

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

Na začiatku je možné zadefinovať alebo vypoznámkovať dve makrá. Tie nemajú vplyv na výslednú animáciu. Ovplyvňujú iba to, či budeme na port zapisovať rýchlo alebo funkciou digitalWrite. A ovplyvňujú, či nahradíme Arduino funkciu main za našu, aby sme docielili stav, že nám jadro Arduina neponastavuje registre podľa svojho implicitného nastavenia. Obe voľby v podstate len skrátia výsledný kód na minimum. A prípadnému odporcovi Arduina ukážu, že priamo na port sa dá pristupovať aj v Arduine a že chápeme ako vlastne funguje linker.

#define DIRECT_WRITE
#define FAKE_MAIN

Premenná counter slúži na sledovanie pozície v rámci jasu v jednom okienku. Môže obsahovať hodnoty 0-255. Podľa hodnoty sa rozhodujem, či bude dióda svietiť alebo nie. A hodnoty jasu pre každú diódu mám v poli light. Každá hodnota je dvojnásobkom predchádzajúcej, aby som zohľadnil nelineárne správanie ľudského oka.

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

Tento kód som si kompletne nechal vygenerovať v mojom kalkulátore na CTC timer. Nastavená frekvencia je 20 kHz a kalkulátor sa postaral o vhodné nastavenie registrov. Používam 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();
}

Vo funkcii setup iba nastavujem, ktoré piny majú byť výstupné. Vďaka tomu, že sú všetky piny na porte D, môžeme využiť aj priame nastavenie registra.

void setup() {

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

Vo funkcii loop nerobíme nič. Všetku prácu vykoná obsluha prerušenia. Ak použijeme priamy zápis do registra, musíme si pripraviť všetkých 8 bitov v pomocnej premennej tmp. Ak je jas pre konkrétnu LED vyšší ako hodnota v premennej counter, bude dióda svietiť. V opačnom prípade bude zhasnutá. Z toho môžeme odvodiť, že čím väčšie je číslo v light[i], tým bude dióda svietiť jasnejšie.

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á funkcia main. Toto môžeme urobiť preto, lebo linker postupne hľadá skompilované kúsky kódu a ak sú dva s identickým názvom funkcie, uprednostní ten prvý a druhý bude ignorovať. V tomto prípade je tým prvým main vždy tá funkcia, ktorú umiestnime do kódu nášho hlavného programu.

V programe si všimnite ešte jednu zaujímavosť. Ňou je nastavenie registra UCSR0B. Musíme to urobiť, pretože Arduino bootloader upraví správanie pinov RX a TX a na týchto dvoch pinoch by sme nevideli naše PWM. Preto oba piny odpojíme od USART a dosiahneme želané správanie.

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

Zdrojový kód

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

Video

Video je na YouTube.


03.02.2017


Menu