Mikrokontrolery - Jak zacząć?

... czyli zbiór praktycznej wiedzy dot. mikrokontrolerów.
Pokazywanie postów oznaczonych etykietą DSP. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą DSP. Pokaż wszystkie posty

środa, 23 marca 2011

Obwiednia ADSR: Regulacja czasu trwania zbocza Release by Wojcik98


Autor: Michał Wójcik
Redakcja: Dondu

Generowanie obwiedni ADSR mikrokontroler ATmega644p by Wojcik98.
Drugie rozwiązaniem konkursu generowania obwiedni ADSR z płynną regulacją czasu trwania zbocza Release zostało nadesłane przez Michała Wójcika i realizowane na mikrokontrolerze ATmega644p.

Rozwiązanie to nie zostało przeze mnie zweryfikowane, ale ponieważ dotyczy rzadziej używanego mikrokontrolera, a przesłany program był przetestowany przez autora, stąd prezentujemy je poniżej.

Do wejścia ADC0 (PA0) podłączony suwak potencjometru tworzącego dzielnik pomiędzy GND i VCC. Potencjometr służy do regulacji czasu opadania ostatniego fragmentu zbocza ADSR, zwanego Release.

Wyjścia PWM realizowane jest na pinie PD5, do którego dodany jest oczywiście filtr dolnoprzepustowy zgodnie z pierwowzorem: Generator obwiedni ADSR na mikrokontrolerze ATmega8

Do pełni szczęścia brakuje jednak obsługi pomiaru ADC, za pomocą przerwań. Michał zastosował pulling flagi ADSC, czyli czekanie w pętli na zakończenie pomiaru. O ile nie ma to wpływu na to zadanie konkursowe, o tyle w końcowej wersji syntezatora dźwięków, na takie rozwiązanie pozwolić sobie nie będziemy mogli :-)

Przesłany program:

/*
 * main.c
 *
 *  Created on: 2013-10-07
 *  Autor: Wojcik98
 *  Mikrokontroler: ATMega644P
 *  Częstotliwość:  16MHz
 */

#include <avr/io.h>
#include <avr/interrupt.h>

#define PWM_MIN  5

#define OBWIEDNIA_ILOSC_ODCINKOW 4

//Tablice z parametrami określającymi kształt obwiedni
volatile unsigned int obwiednia_kroki[OBWIEDNIA_ILOSC_ODCINKOW]=
{250, 100, 350, 300};
volatile int16_t obwiednia_nachylenie[OBWIEDNIA_ILOSC_ODCINKOW]=
{((102<<8)/100), ((-1)*(102<<8)/100), 0<<8, (-1) *(1<<7)};

volatile unsigned int obwiednia_licznik_krokow = 1;
volatile unsigned char obwiednia_indeks = 3;

volatile uint16_t war;
volatile uint8_t flag=0;

uint16_t pomiar(uint8_t kanal)        //funkcja pomiaru ADC
{
  ADMUX = (ADMUX & 0xF0)|kanal;       //Wybór kanalu
  ADCSRA |= (1<<ADSC);                //Start pomiaru
  while(ADCSRA & (1<<ADSC));
  return ADCW;
}

int main(void)
{
  DDRD &= ~(1<<PD0);                  //Ustawiwnie PD0 na wejście
  PORTD |= (1<<PD0);                  //Podciągnięcie do VCC

  DDRD |= (1<<PD5);                   //Ustawienie PD5 na wyjście
  TCCR1A |= (1<<COM1A1)|(1<<COM1A0);  //Wyjście timera na pin PD5

  TCCR1A |= (1<<WGM10);               //Tryb Fast PWM 8-bit
  TCCR1B |= (1<<WGM12)
            |(1<<CS10);               //Brak Preskalera

  OCR1A=0;

  //Inicjalizacja ADC
  ADCSRA |= (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
  ADMUX |= (1 << REFS0);
  ADCSRA |= (1 << ADEN);

  TCCR0A |= (1<<WGM01);          //Tryb CTC
  TCCR0B |= (1<<CS01)|(1<<CS00); //Preskaler 64
  OCR0A = 250;         //Zliczanie do 250: 16MHz/64/1000=250 (przrwanie co 1ms)
  TIMSK0 |= (1<<OCIE0A);         //Włączenie przerwania od timera0

  sei();                         //Globalne włączenie przerwań

  while(1) {                     //Pętla nieskończona
    if(flag) {
      flag=0;
      obwiednia_kroki[3]=5*(pomiar(0)+1);
      obwiednia_nachylenie[3]=((150<<8)/(obwiednia_kroki[3]))*(-1);
    }
  }
}

//Procedura obsługi przerwania od Timera0
ISR(TIMER0_COMPA_vect)
{
  //czy to już ostatnia próbka w tej części obwiedni?
  if(--obwiednia_licznik_krokow == 0) {
    //Czy to już koniec obwiedni?
    if(++obwiednia_indeks > (OBWIEDNIA_ILOSC_ODCINKOW-1)) {
      //koniec obwiedni
      war =  0;  //ustaw minimum wypełnienia PWM
      obwiednia_indeks = 0; //resetuj licznik odcinków obwiedni
      obwiednia_licznik_krokow = obwiednia_kroki[0];
    } else {

      flag=1;
      //nowy fragment obwiedni
      if(obwiednia_nachylenie[obwiednia_indeks]>0) {
        war += (obwiednia_nachylenie[obwiednia_indeks]);
      } else {
        war -= ((-1)*((obwiednia_nachylenie[obwiednia_indeks])));
      }
      obwiednia_licznik_krokow = obwiednia_kroki[obwiednia_indeks];
    }
  } else {
    //dodaj do PWM kolejny krok stopnia nachylenia
    if(obwiednia_nachylenie[obwiednia_indeks]>0) {
      war += (obwiednia_nachylenie[obwiednia_indeks]);
    } else {
      war -= ((-1)*((obwiednia_nachylenie[obwiednia_indeks])));
    }
  }
  OCR1A = (war >> 8);
}

Do pobrania kompletny projekt AVR Studio 4.18 wraz z plikiem .hex dla kwarcu 16 MHz:
ADSR-by-Wojcik98.zip (kopia)

Zużycie pamięci:

AVR Memory Usage
----------------
Device: atmega644p

Program:     750 bytes (1.1% Full)
(.text + .data + .bootloader)

Data:         23 bytes (0.6% Full)
(.data + .bss + .noinit)

Build succeeded with 0 Warnings...

Rozwiązanie to nagrodziliśmy książką (w wersji eBook) wybraną przez Michała:


Kod doskonały. Jak tworzyć oprogramowanie pozbawione błędów. Wydanie II Autor: Steve McConnell.


Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

wtorek, 22 marca 2011

Obwiednia ADSR: Zadania 1 i 2 by Maciej Łukaszewicz

Autor: Maciej Łukaszewicz
Redakcja: Dondu


Program przesłany przez Macieja jest najbardziej zbliżony do wymagań jakie postawiliśmy w zadaniach nr 1 i 2 konkurs dot. generowania obwiedni.

Maciej zastosował metodę obliczenia współczynnika nachylenia bez używania operacji zmiennoprzecinkowych. Zastosował także definicje preprocesora. Dzięki definicjom łatwo zmieniać parametry obwiedni oraz reakcję programu na naciśnięcie przycisku.

Maciej wykonał także eliminację drgań styków przycisku wyzwalającego. Opóźnienie niezbędne do eliminacji drgań styków oparł o timer wykorzystany do generowania ADSR, czyli tak jak to powinno się robić w przypadku większości projektów.

Dodatkowo opracował algorytm powtarzający przebieg dopóki przycisk jest wciśnięty. Nie było to jednak brane pod uwagę, przy ocenianiu przesłanych materiałów.

Jego rozwiązanie zostało przeze mnie sprawdzone i prawidłowo generuje obwiednię ADSR.

Programy działają wyłącznie w oparciu o przerwania, pozostawiając pustą pętlę główną. To najbardziej pożądany sposób realizowania tego typu zadań, pozwalający na równoległe wykonywanie innych czynności przez mikrokontroler lub jego uśpienie.

Schemat

Ponieważ do generowania obwiedni Maciej używa Timer2, stąd wyjście sygnału PWM jest na pinie OC2, czyli PB3. Do niego podłączony jest filtr RC według schematu w tym artykule.


Rozwiązanie zadania nr 1

Generowanie powtarzającej się obwiedni ADSR:

/*
*  Obwiednia.c
*
*  Created: 2013-10-06 09:49:30
*  Author: mlodedrwale (Maciej Łukaszewicz)
*
*  Program dla Atmega8(a) częstotliwość 8mhz
*  częstotoliwość pwm = częstotliwość PWM 31250Hz wyższa niż pasmo akustyczne
*  Wykorzystano Timer0 do generowania przerwania co 1ms
*  oraz Timer2 do generowania sygnału pwm.
*
*/


//######## USTAWIENIA #########

#define ATTACK_MAX 100      //w procentach
#define ATTACK_TIME 250     //w ms

#define DECAY_MAX 60
#define DECAY_TIME 100

#define SUSTAIN_MAX 60
#define SUSTAIN_TIME 350

#define RELEASE_TIME 300

//#############################

#define OUTPUT PB3        //wyjście PWM

#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint16_t current_time = 1;   // licznik przerwań
volatile uint8_t last_pwm_value =  0; // zmienna potrzebna do sprawdzenia, 
                                      //czy wartość pwm uległa zmianie

// funkcja oblicza odpowiednią wartość wypełnienia pwm (rozdielczość 1%)
// na podstawie wartości początkowej i końcowej funkcji liniowej, całkowitego
// czasu jej trwania oraz czasu, który minął od jej rozpoczęcia,
// następnie sprawdzane jest, czy wartość pwm uległa zmianie i jeśeli 
// uległa wprowadzana jest nowa wartość

void set_pwm(uint8_t pwm_start, uint8_t pwm_stop, uint16_t length,
             uint16_t my_current_time)
{
  uint8_t pwm_value;
  if(pwm_start < pwm_stop) {
    pwm_value = (0xFF*(pwm_start+((pwm_stop - pwm_start)
                                  *my_current_time)/length))/100;
  } else if(pwm_start > pwm_stop) {
    pwm_value  = (0xFF*(pwm_start-((pwm_start-pwm_stop)
                                   *my_current_time)/length))/100;
  } else {
    pwm_value = (0xFF * pwm_start) / 100;
  }

  if(last_pwm_value != pwm_value) {
    // jeśli wartość PWM uległa zmianie zostanie zapisana nowa wartość
    OCR2 = pwm_value;
    last_pwm_value = pwm_value;
  }

}

ISR(TIMER0_OVF_vect)   // timer0 overflow interrupt
{
  TCNT0 = 131; //załadowanie licznika tak by przerwanie było
               //co 8000 cykli zegara

  if(current_time <= ATTACK_TIME) {
      //ATTACK
      set_pwm(0, ATTACK_MAX, ATTACK_TIME, current_time);
  } else if(current_time <= ATTACK_TIME + DECAY_TIME) {
      //DECAY
      set_pwm(ATTACK_MAX, DECAY_MAX, DECAY_TIME, current_time - ATTACK_TIME);
  } else if(current_time <= ATTACK_TIME + DECAY_TIME + SUSTAIN_TIME) {
      //SUSTAIN
      set_pwm(DECAY_MAX, SUSTAIN_MAX, SUSTAIN_TIME,
            current_time - (ATTACK_TIME + DECAY_TIME));
  } else if(current_time <= ATTACK_TIME + DECAY_TIME + SUSTAIN_TIME +
            RELEASE_TIME) {
      //RELEASE
      set_pwm(SUSTAIN_MAX, 0, RELEASE_TIME,
            current_time - (ATTACK_TIME + DECAY_TIME + SUSTAIN_TIME));
  } else {
      current_time = 0; // zerujemy przepełniony licznik przerwan :)
  }
  current_time += 1;
}

int main(void)
{
  //Inicjujemy
  //PWM
  DDRB |= (1<<OUTPUT);           //wyjście
  PORTB |= (1<<OUTPUT);          //pullup
  OCR2 = 0;                      //po starcie pwm ma zerowe wypełnienie
  TCCR2 |=(1<<COM21) | (1<<CS20) //brak preskalera - częstotliwość PWM 31250Hz
          | (1<< WGM20) | (1<< WGM21);    //fast PWM

  //Przerwania
  TCCR0 |= (1<< CS01) | (1<< CS00); //preskaler 64
  TIMSK |= (1<<TOIE0);              //włączamy przerwanie przepełnienia Timera0

  sei();                  //włączamy przerwania

  while(1) {
    //pętla główna
    //na razie wszystko w przerwaniach :)
  }
}

AVR Memory Usage
----------------
Device: atmega8

Program: 586 bytes (7.2% Full)
(.text + .data + .bootloader)

Data: 3 bytes (0.3% Full)
(.data + .bss + .noinit)

Build succeeded with 0 Warnings...

A tak działa w praktyce:



Rozwiązanie zadania nr 2

Wersja z przyciskiem podłączonym do pinu PD3:

/*
*  Obwiednia_przycisk.c
*
*  Created: 2013-10-06 09:49:30
*  Author: mlodedrwale (Maciej Łukaszewicz)
*
*  Program dla Atmega8(a) częstotliwość 8mhz
*  częstotoliwość pwm = częstotliwość PWM 31250 Hz - wyższa niż 
*          pasmo akustyczne
*  Wykorzystano Timer0 do generowania przerwania co 1ms
*  oraz Timer2 do generowania sygnału pwm.
*
*/


//######## USTAWIENIA #########

#define ATTACK_MAX      100   //w procentach
#define ATTACK_TIME     250   //w ms

#define DECAY_MAX       60
#define DECAY_TIME      100

#define SUSTAIN_MAX     60
#define SUSTAIN_TIME    350

#define RELEASE_TIME    300

#define BUTTON_TIME     50  // czas w ms po którym uznajemy, że przycisk
       // został wciśnięty
#define BUTTON_REPEAT_TIME  1050 // czas w ms po którym uznajemy że wciśnięty
                                 // przycisk został ponownie wciśnięty. 
                                 // Dla wartości 0 wyłączamy tę funkcję.

//definiujemy przycisk
#define BUTTON_BIT  PD3       
#define BUTTON_DDR  DDRD
#define BUTTON_PORT PORTD
#define BUTTON_PIN  PIND

//#############################

#define OUTPUT PB3        //wyjście PWM

#include <avr/io.h>
#include <avr/interrupt.h>

// licznik przerwań zainicjowany warością wyższą niż wartość właściwa
//dla obwiedni
volatile uint16_t current_time = ATTACK_TIME + DECAY_TIME + SUSTAIN_TIME +
                                 RELEASE_TIME + 1;

volatile uint8_t last_pwm_value = 0;  // zmienna potrzebna do sprawdzenia,
                                      //czy wartość pwm uległa zmianie
volatile uint16_t button_time = 0;    // licznik czasu wciśnięcia przycisku


//----------------------------------------------------------------------

//funkcja sprawdza, czy został wciśnięty przycisk i podejmuje odpowiednie
//działania
void button()
{
  if((BUTTON_PIN & (1<<BUTTON_BIT)) == 0) {
    //przycisk wciśnięty
    button_time++;
    if(button_time == BUTTON_TIME) {
      current_time = 1;  // jeśli przycisk został wciśnięty przez odpowiedni 
       // czas zaczynamy generować obwiednię
    }
    if(BUTTON_REPEAT_TIME == 0) {
      if(button_time > BUTTON_TIME) {
        // jeśli zdecydowaliśmy, że przycisk nie jest powtarzalny
        button_time = BUTTON_TIME; 
      }
      // musimy zadbać aby przy wciśniętym ciągle przycisku
      // jego licznik nie uległ przepełnieniu
    } else if(button_time == BUTTON_REPEAT_TIME) {
      //zerowanie licznika przycisku, gdy ma nastąpić powtórzenie
      button_time = 0;
    }
  } else {
    button_time = 0;         // gdy nie wciśnięty zerujemy!
  }
}

//----------------------------------------------------------------------

// funkcja oblicza odpowiednią wartość wypełnienia pwm (rozdzielczość 1%) 
// na podstawie wartości początkowej i końcowej funkcji liniowej, całkowitego
// czasu jej trwania oraz czasu, który minął od jej rozpoczęcia,
// następnie sprawdzane jest, czy wartość pwm uległa zmianie i jeśeli uległa
// wprowadzana jest nowa wartość

void set_pwm(uint8_t pwm_start, uint8_t pwm_stop, uint16_t length,
             uint16_t my_current_time)
{
  uint8_t pwm_value;
  if(pwm_start < pwm_stop) {
    pwm_value = (0xFF*(pwm_start+((pwm_stop - pwm_start)
                                  *my_current_time)/length))/100;
  } else if(pwm_start > pwm_stop) {
    pwm_value  = (0xFF*(pwm_start-((pwm_start-pwm_stop)
                                   *my_current_time)/length))/100;
  } else {
    pwm_value = (0xFF * pwm_start) / 100;
  }

  if(last_pwm_value != pwm_value) { 
    // jeśli wartość PWM uległa zmianie zostanie zapisana nowa wartość
    OCR2 = pwm_value;
    last_pwm_value = pwm_value;
  }
}

//----------------------------------------------------------------------

ISR(TIMER0_OVF_vect)   // timer0 overflow interrupt
{
  TCNT0 = 131; //załadowanie licznika tak by przerwanie było 
               //co 8000 cykli zegara

  if(current_time <= ATTACK_TIME) { 
      //ATTACK
      set_pwm(0, ATTACK_MAX, ATTACK_TIME, current_time);
  } else if(current_time <= ATTACK_TIME + DECAY_TIME) { 
      //DECAY
      set_pwm(ATTACK_MAX, DECAY_MAX, DECAY_TIME, current_time - ATTACK_TIME);
  } else if(current_time <= ATTACK_TIME + DECAY_TIME + SUSTAIN_TIME){
      //SUSTAIN
      set_pwm(DECAY_MAX, SUSTAIN_MAX, SUSTAIN_TIME,
            current_time - (ATTACK_TIME + DECAY_TIME));
  } else if(current_time <= ATTACK_TIME + DECAY_TIME + SUSTAIN_TIME +
            RELEASE_TIME) {
      //RELEASE
      set_pwm(SUSTAIN_MAX, 0, RELEASE_TIME,
            current_time - (ATTACK_TIME + DECAY_TIME + SUSTAIN_TIME));
  }

  if(current_time <= ATTACK_TIME + DECAY_TIME + SUSTAIN_TIME + RELEASE_TIME) {
      //jeśli jesteśmy w trakcie generowania obwiedni zwiększamy licznik
      current_time +=  1;
  }

  button();
}

//----------------------------------------------------------------------

int main(void)
{
  //Inicjujemy:
  //Przycisk
  BUTTON_DDR &= ~(1<<BUTTON_BIT);   //wejście
  BUTTON_PORT |= (1<<BUTTON_BIT);   //wewnętrzny pullup

  //PWM
  DDRB |= (1<<OUTPUT);    //wyjście
  PORTB |= (1<<OUTPUT);   //pullup
  OCR2 = 0;                     //po starcie pwm ma zerowe wypełnienie
  TCCR2 |=(1<< CS20)      //brak preskalera - częstotliwość PWM 31250 Hz
          | (1<< WGM20) | (1<< WGM21)   //fast PWM
          | (1<< COM21);  //sygnał pwm na wyjściu OC2


  //Przerwania
  TCCR0 |= (1<< CS01) | (1<< CS00);   //preskaler 64
  TIMSK |= (1<<TOIE0);    //włączamy przerwanie przepełnienia Timera0

  sei();                        //włączamy przerwania

  while(1) {
     //pętla główna
     //na razie wszystko w przerwaniach :)
  }
}



Program zajmuje:

AVR Memory Usage
----------------
Device: atmega8

Program: 676 bytes (8.3% Full)
(.text + .data + .bootloader)

Data: 5 bytes (0.5% Full)
(.data + .bss + .noinit)

Build succeeded with 0 Warnings...


Wygenerowana obwiednia wygląda następująco (włączona opcja powtarzania obwiedni podczas trzymania przycisku):




Zwycięzca!

Jako zwycięzca Maciej otrzymał nagrodę w postaci książki w wersji eBook ufundowanej przez eBookPoint:


Inteligentny dom. Automatyzacja mieszkania za pomocą platformy Arduino, systemu Android i zwykłego komputera.

Inteligentny dom. Automatyzacja mieszkania
za pomocą platformy Arduino, systemu Android i zwykłego komputera


Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

czwartek, 10 marca 2011

Konkurs: Generowanie obwiedni ADSR za pomocą mikrokontrolera


Autor: Dondu

Stawiamy przed Wami kolejne proste zadanie, w którym można się wykazać i zdobyć książkę. Tym razem jest to generowanie obwiedni typu ADSR. Brzmi groźnie?

W latach 80 tych tworzyliśmy syntezatory opisywane w miesięczniku "RE Radiolelktronik" na bazie tranzystorów itp. Potężne płytki, ciągłe problemy .... aktualnie wystarczy jeden malutki mikrokontroler i krótki program, by ADSR realizować :-)

Uwaga!!!
Linki do rozwiązań konkursowych znajdziesz na końcu niniejszego artykułu.

W pierwszym konkursie ograniczymy się jedynie do wygenerowania obwiedni ADSR. W następnych konkursach będziemy dodawać dodatkowe elementy. Naszym celem końcowym jest zrobienie prostego syntezatora dźwięków.




Obwiednia ADSR, to nic innego jak regulacja amplitudy (np. dźwięku) podzielona na 4 etapy:
  1. Attack - narastanie amplitudy,
  2. Decay - opadanie amplitudy,
  3. Sustain - wybrzmiewanie,
  4. Release - wybrzmiewanie końcowe (wyciszenie) 
a wygląda to następująco (podstawowa prosta wersja):


Obwiednia ADSR - wersja podstawowa.
Obwiednia ADSR - wersja podstawowa.

w praktyce na przykład tak:

Obwiednia ADSR - przykład rzeczywisty - oscylogram.
Obwiednia ADSR - przykład
rzeczywisty.

Jak rozumieć te wykres? To naprawdę jest proste i wytłumaczę to poniżej :-)


Nagroda

Nagrodą dla zwycięzcy będzie książka (w wersji eBook) ufundowana przez eBookPoint:

Inteligentny dom. Automatyzacja mieszkania za pomocą platformy Arduino, systemu Android i zwykłego komputera.

Inteligentny dom. Automatyzacja mieszkania
za pomocą platformy Arduino, systemu Android i zwykłego komputera


Wszyscy uczestnicy (łącznie ze zwycięzcą) będą mieli drugą szansę, tym razem na dowolną książkę w wersji papierowej, o czym piszę pod koniec niniejszego artykułu.





Zadanie 1 - podstawa

Pierwszym zadaniem do wykonania będzie więc otrzymanie powyższego przebiegu za pomocą timera z PWM przy następujących założeniach czasowych:
  • czas trwania ataku: AT = 250ms,
  • czas trwania opadania: DT = 100ms,
  • czas trwania wybrzmiewania: ST = 350ms,
  • czas trwania wyciszania: RT = 300ms.

Założenia te na wykresie będą wyglądały więc następująco:

Obwiednia ADSR - czasy trwania.
Obwiednia ADSR - czasy trwania.

Pozostaje nam jeszcze ustalić amplitudy ataku (A) oraz wybrzmiewania (S):
  • amplitudę ataku przyjmiemy jako maksymalną, którą reprezentuje PWM o wypełnieniu 100% czyli dla timera 8-bitowego będzie to liczba 255, stąd APWM=255,
  • amplitudę poziomy wybrzmiewania (S) ustawimy na poziomie np. 60% czyli dla nas będzie to liczba SPWM = 255 * 60% = 153.

Stąd:


Obwiednia ADSR - poziomy PWM.
Obwiednia ADSR - poziomy PWM.


Jak zrealizować zadanie nr 1?

Należy wykorzystać timer do generowania sygnału PWM oraz ten sam timer lub inny do generowania przerwań:
  • ustawiasz wybrany timer tak, by generował sygnał PWM o zadanym wypełnieniu,
  • przerwanie za pomocą programu zmienia wypełnienie timera w zależności od miejsca na osi czasu (t), w którym znajduje się program generujący przebieg ADSR,
  • jeżeli dojdziemy do końca generowania obwiedni, rozpoczynamy generować ją od początku.

Należy przyjąć z jaką dokładnością będziemy odtwarzać wykres obwiedni. Przyjmijmy więc, że przerwanie powinno być wywoływane co około 1ms - to wystarczająca dla nas dokładność.


Jak obliczać wartość PWM na skośnych zboczach obwiedni ADSR?

Sądzę, że nie sprawi Ci to problemu ponieważ zbocza są odcinkami prostymi, czyli zwykłą funkcją liniową.

Wystarczy więc ustalić współczynnik nachylenia zbocza za pomocą czasu jego trwania i zakresu jego zmiany w tym czasie (różnica wartości PWM na początku i końcu zbocza w stosunku do ilości kroków, które mamy na wykonanie tej zmiany - w naszym przypadku 1 krok, to 1ms).

Współczynnik ten będzie przyjmował wartości dodatnie (dla zbocza narastającego) lub ujemne (dla zbocza opadającego). Tak obliczony współczynnik należy za każdym przerwaniem dodawać do ustawionej aktualnie wartości PWM.

Gdy kończymy dany fragment obwiedni np. A, obliczamy współczynnik dla kolejnego odcinka w tym wypadku D i dalej nasze przerwanie już działa normalnie. Można także przed wygenerowaniem obwiedni, obliczyć współczynniki dla każdego odcinka.

Wyjątek stanowi oczywiście współczynnik nachylenia dla wybrzmiewania (S), który będzie wynosił zero, ponieważ ten fragment wykresu obwiedni jest płaski.

Czy to jest trudne? Nie to jest naprawdę proste, ale zapewne przydałby się jakiś przykład ...


Przykład - wzorzec

Na przykładzie z Drzaśkowego PamiętnikaFAST PWM - Sposób na DAC
możesz prześledzić jak generowane są przebiegi sinus i piła, czyli jak ustawić PWM, a jak timer generujący przerwania zmieniające parametry PWM oraz jak zrobić prosty schemat.

Filtr dolnoprzepustowy

Na powyżej wskazanym schemacie znajdziesz na wyjściach PWM prosty schemat filtru dolnoprzepustowego, który pozwala zamienić sygnał PWM na żądany przez nas przebieg widziany na oscyloskopie. Bez tego filtru na oscyloskopie będziesz widział jedynie prostokątny przebieg sygnału PWM, co słusznie zauważył Miłosz Rachwał, w komentarzu do tego artykułu.

Jeżeli chcesz możesz oczywiście opracować własny filtr dolnoprzepustowy, by dokładniej odtwarzać naszą obwiednię. Na pewno się on przyda w dalszych konkursach z tego cyklu, co pozwoli uzyskać znacznie lepszy dźwięk.



Oscyloskop 

W efekcie na oscyloskopie powinieneś zaobserwować powyższy wykres. Jeżeli nie masz oscyloskopu możesz wykorzystać kartę dźwiękową komputera: Oscyloskop za darmo

UWAGA!!!
Jeżeli nie masz dostępnego oscyloskopu cyfrowego i będziesz korzystał z darmowego powyżej, to możesz skrócić czasy ADSR np. o 50 razy, by na oscyloskopie uzyskać powtarzalne przebiegi w zakresie pasma przenoszenia karty dźwiękowej komputera.






Zadanie 2 - dodajemy przycisk

Jeżeli już mamy dokładnie odtworzoną obwiednię i generowaną raz za razem, należałoby wprowadzić, jakiś rodzaj inicjatora startu generowania obwiedni. Najlepiej byłoby, gdyby był to po prostu przycisk.

Naciśnięcie przycisku powinno więc:
  1. rozpocząć generowanie obwiedni,
  2. lub przerwać generowanie aktualnej obwiedni i rozpocząć generowanie nowej.  
W takim przypadku oczekujemy dodatkowo, by po wygenerowaniu jednego przebiegu obwiedni, nie następowało automatyczne generowanie nowej. Innymi słowy przyciśnięcie przycisku ma wygenerować tylko jeden przebieg.




Rozwiązania zadań 1 i 2

Jako pierwsze prezentuję rozwiązanie moje (pozakonkursowe), które stanowi jednocześnie podstawę kolejnego konkursu: Generator obwiedni ADSR na mikrokontrolerze ATmega8 (wersja Dondu)

Zwycięzca!

Zwycięskie rozwiązania zadań 1 i 2 nadesłał Maciej Łukaszewicz:  Obwiednia ADSR: Zadania 1 i 2 by Maciej Łukaszewicz




Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

niedziela, 6 marca 2011

Generator obwiedni ADSR na mikrokontrolerze ATmega8


Autor: Dondu

Niniejszy artykuł pokazuje przykład generowania obwiedni za pomocą mikrokontrolera. Przykład oparłem o obwiednię typu ADSR nawiązując do konkursu ogłoszonego w październiku 2013r.

Na końcu artykułu znajdziesz nowe zadanie do rozwiązania oraz termin.

Dla przypomnienia obwiednia wygląda następująco:




Zauważ, że obwiednia ADSR jest złożona z czterech odcinków będących liniami prostymi, co znacząco wpływa na prostotę algorytmu programu. W artykule konkursowym podałem prosty algorytm generowania takiej obwiedni i nie będę go tutaj powtarzał. Wskazałem tam także jak podzielić zadania pomiędzy timery.

Do generowania obwiedni użyjemy więc dwóch timerów:
  • Timer0 do odmierzania czasu i podawania nowych próbek do generowania PWM,
  • Timer2 do generowania 8-bitowego sygnału PWM na wyjściu OC2.

Timer2 ustawimy w ten sposób, by z maksymalną możliwą częstotliwością (preskaler równy 1), generował sygnał PWM. Dlatego też mikrokontroler będziemy taktować z zewnętrznego kwarcu 16MHz.

Timer0 ustawimy w taki sposób, by generował przerwania. Funkcja przerwania za pomocą programu odliczać będzie czas zbliżony do 1ms (milisekundy) i po jej upływie zmieniała wypełnienie sygnału PWM podając na niego kolejną wartość wypełnienia (ang. duty cycle). W przyszłości przerwania będziemy mogli także wykorzystać do eliminowania drgań styków przycisków wyzwalających dźwięki.


Filtr dolnoprzepustowy

Aby sygnał PWM zamienić na sygnał analogowy tworzący obwiednię, potrzebny będzie filtr dolnoprzepustowy, który zrealizujemy możliwie najprościej za pomocą rezystora i kondensatora. Nasz schemat będzie więc wyglądał następująco:




Wyjaśnienia wymaga zastosowanie przyciski S1. Podczas testów warto go dodać, by nie przeszkadzać niepotrzebnie przebiegom na pinie MOSI w czasie programowania mikrokontrolera. Bez niego zapewne zaprogramujesz układ, ale nie należy ryzykować w szczególności podczas programowania fusebitów, by nie zablokować mikrokontrolera.

Przykład takiego sposobu odtwarzania sygnału za pomocą PWM szczegółowo przedstawił Drzasiek w artykule: FAST PWM - Sposób na DAC



Próba w CManiaku

Na początek do sprawdzenia algorytmu wykorzystamy CManiak-a. Poniżej znajduje się program, który symuluje działanie Timera0 (pętla while) wywołującego kolejne przerwania. Timer2 generujący sygnał PWM wraz z filtrem dolnoprzepustowym są symulowane za pomocą funkcji rysuj_fragment_wykresu().

Przykład 1 (w kompilatorze)
// ***********************************************************
// Generator obwiedni 
// wersja: 1.0
//
// Przykład do artykułu: http://mikrokontrolery.blogspot.com/2011/03/ATmega8-Generowanie-obwiedni-przyklad-ADSR.html
//
// Autor: Dondu 
// Data:  2013.10.03
// 
// Mikrokontroler: ATmega8
// Częstotliwość F_CPU: 16MHz (zewnętrzny kwarc)
// ***********************************************************


#include <stdio.h>

//definicje graniczne sygnału PWM
#define PWM_MAX  250
#define PWM_MIN  0

//definicja ilości odcinków, z których składa się obwiednia
//obwiednia ADSR składa się z 4 odcinków
#define OBWIEDNIA_ILOSC_ODCINKOW 4 

//Na jedną milisekundę przypada około 8 przerwań
#define PRZERWAN_NA_MILISEKUNDE  8

//Tablice z parametrami określającymi kształt obwiedni
unsigned int obwiednia_kroki[OBWIEDNIA_ILOSC_ODCINKOW]=
      {15, 4, 8, 5};
signed char obwiednia_nachylenie[OBWIEDNIA_ILOSC_ODCINKOW]=
      {1, -1, 0, -2};

signed char OCR2; //symulujemy rejestr OCR2

//----------------------------------------------------------------------

//Funkcja rysująca fragment wykresu w terminalu CManiaka
//dla ułatwienia wykres jest odwrócony o 90 stopni
void rysuj_fragment_wykresu(signed char ocr2){
 unsigned int f;
 for(f=0; f<OCR2; f++) printf(" "); //najpierw odpowiednią ilość spacji
 printf(". \r");      //a na szczycie pokaż kropkę
}

//----------------------------------------------------------------------

int main(void) {


 //Zmienne lokalne statyczne dostępne tylko w tej funkcji i przechowujące
 //dane pomiędzy kolejnymi przerwaniami. Więcej na ich temat:
 //http://mikrokontrolery.blogspot.com/2011/02/kurs-jezyka-c-zmienne-lokalne-statyczne.html
 
 //Parametry startowe ustawiają algorytm na końcu obwiedni,
 //stąd po uruchomieniu programu algorytm rozpocznie od warunku, 
 //w którym sam ustawi się na początek obwiedni.
 
 //przerwań dla odliczenia 1ms
 static unsigned char licznik_przerwan = 0; 

  //pozostała ilość kroków w danym odcinku obwiedni
 static unsigned int obwiednia_licznik_krokow = 1;

 //aktualny nr generowanego odcinka obwiedni
 static unsigned char obwiednia_indeks = 3;


 //zmienna pomocnicza do zatrzymania programu 
 //i pokazania ilości wykonanych przerwań
 unsigned int temp_i=0;
 
 //Pętla nieskończona symulująca kolejne przerwania timera
 while(1){
  
  //czy już upłynęła 1 milisekunda?
  if(++licznik_przerwan > (PRZERWAN_NA_MILISEKUNDE-1)){
 
   //upłynęła 1ms czas zmienić próbkę PWM

   licznik_przerwan = 0;
   
   //czy to już ostatnia próbka w tej części ADSR?
   if(--obwiednia_licznik_krokow == 0){
    //tak
    
    //Czy to już koniec obwiedni?
    if(++obwiednia_indeks > (OBWIEDNIA_ILOSC_ODCINKOW-1)) {

     //koniec obwiedni
     OCR2 =  PWM_MIN;  //ustaw minimum wypełnienia PWM
     obwiednia_indeks = 0; //resetuj licznik odcinków obwiedni
     obwiednia_licznik_krokow = obwiednia_kroki[0];

     //kończymy, gdy już narysowliśmy jedną pełną obwiednię
     //potrzebna dodatkowa zmienna, ponieważ program zaczyna
     //od przypadku, w którym ustawiony jest na końcu obwiedni
     if(temp_i>7){
      printf("ilość przerwań = %d  \r", temp_i);
      return 0;
     }
 
    }else{

     //nowy fragment obwiedni
     OCR2 = OCR2 + obwiednia_nachylenie[obwiednia_indeks];
     obwiednia_licznik_krokow = obwiednia_kroki[obwiednia_indeks];
    }
    
   }else{
    
    //dodaj do PWM kolejny krok stopnia nachylenia  
    OCR2 = OCR2 + obwiednia_nachylenie[obwiednia_indeks];

   }

   rysuj_fragment_wykresu(OCR2);
  }
  
  temp_i++;
 }
}

Teraz zaglądnij do CManiak-a, gdzie znajdziesz ten program (jako przykład nr 1). Wybierz go i skompiluj, a później zobacz wynik jego pracy w zakładce Terminal. Twoim oczom ukaże się wykres naszej obwiedni obrócony o 90 stopni (by łatwo było napisać program wyświetlający wykres).


Jak działa program?

W zasadzie w kodzie są wszelkie komentarze istotne do zrozumienia jego działania. Tutaj dodam więc tylko parę zdań. Zacznę od tablic przechowujących parametry obwiedni:


obwiednia_kroki[]

Tablica ta przechowuje informacje o ilości kroków, które mają poszczególne fragmenty obwiedni. W naszym przypadku, będzie to ilość milisekund, które ma trwać dany fragment obwiedni:



Na rysunku są zaznaczone czasy zgodne z zadaniem konkursowym, ale w naszej tablicy w programie symulowanym w CManiak-u skróciłem je do następujących:

unsigned int obwiednia_kroki[OBWIEDNIA_ILOSC_ODCINKOW]= {15, 4, 8, 5};

by wykres wygenerowany w CManiak-u nie był zbyt duży.


obwiednia_nachylenie[]

Tablica ta zwiera współczynnik nachylenia, który dodawany jest za każdym krokiem do rejestru OCR2, odpowiedzialnego za wypełnienie sygnału PWM. Tablica ta zwierać może liczby dodatnie (zbocze narastające) oraz ujemne (zbocze opadające). Im wartość bezwzględna współczynnika nachylenia jest większa, tym bardziej strome jest zbocze, i odwrotnie.

signed char obwiednia_nachylenie[OBWIEDNIA_ILOSC_ODCINKOW]= {1, -1, 0, -2};

Algorytm

Za każdym przerwaniem zliczane są przerwania składające się na 1ms. Gdy zostanie stwierdzony upływ 1ms, następuje dodanie kolejnego współczynnika nachylenia dla aktualnego fragmentu obwiedni. Po skończeniu generowania aktualnego odcinka, program przechodzi do następnego. Po skończeniu całej obwiedni program zatrzymuje się, bo to tylko symulator i wystarczy, że narysuje tylko jedną pełną obwiednię.


Czas na test w realnym środowisku

Skoro mamy już poprawnie działający algorytm, przeniesiemy go na nasz mikrokontroler ATmega8.

Różnice dotyczą ustawienia timerów oraz dodaniu funkcji obsługi przerwania i przekopiowaniu do niej części kodu. Minimalnie pozmieniałem też nazwy definicji dodając przedrostki TIMER0. Kod jest dobrze opisany więc nie powinno być problemów z jego analizą i zrozumieniem.

Program w przeciwieństwie do tego w CManiaku, nie zatrzymuje się, tylko po skończeniu generowania obwiedni, rozpoczyna generowanie następnej.

// ***********************************************************
// Generator obwiedni 
// wersja: 1.0
//
// Przykład do artykułu: http://mikrokontrolery.blogspot.com/2011/03/ATmega8-Generowanie-obwiedni-przyklad-ADSR.html
// Autor: Dondu 
// Data:  2013.10.03
// 
// Mikrokontroler: ATmega8
// Częstotliwość F_CPU: 16MHz (zewnętrzny kwarc)
// ***********************************************************

#include <avr/io.h>
#include <stdint.h>
#include <avr/interrupt.h> 
#include <math.h> 

//definicje graniczne sygnału PWM
#define PWM_MAX  250
#define PWM_MIN  5

//definicja ilości odcinków, z których składa się obwiednia
//obwiednia ADSR składa się z 4 odcinków
#define OBWIEDNIA_ILOSC_ODCINKOW 4 

//Timer0 odmierza odcinki czasowe potrzebne do odliczenia 1 milisekundy
//Na jedną milisekundę przypada około 8 przerwań
#define TIMER0_PRZERWAN_NA_MILISEKUNDE 8

//Definicje dot. pinu wyjściowego PWM
#define PWM_PIN  (1<<PB3)
#define PWM_DDR  DDRB

//Tablice z parametrami określającymi kształt obwiedni
volatile unsigned int obwiednia_kroki[OBWIEDNIA_ILOSC_ODCINKOW]=
      {240, 100, 350, 70};
volatile signed char obwiednia_nachylenie[OBWIEDNIA_ILOSC_ODCINKOW]=
      {1, -1, 0, -2};


int main(void) 
{

   PWM_DDR |= PWM_PIN; //ustaw pin PWM Timera1 jako wyjście
 
   //Timer 2 odpowiada za generowanie sygnału PWM
   TCCR2 = (1<<WGM21) | (1<<WGM20)  //tryb Fast PWM
      | (1<<COM21) //pin OC2 ustawiany gdy timer ma wartość 0
       //i zerowany gdy osianie zadaną wartość OCR2
   | (1<<CS20); //włącz Timer2 ustawiając preskaler 1

   //Timer0 (jego zadania opisane wyżej)
   TCCR0 = (1<<CS01);   // włącz Timer0 ustawiając preskaler 8
   TIMSK |= (1<<TOIE0); // włącz przerwania overflow (przepełnienie timera)

 
   sei();   //włącz przerwania

   while(1);  //pętla nieskończona

} 



//Procedura obsługi przerwania od przepełnienia Timera0
ISR(TIMER0_OVF_vect)   
{

 //Zmienne lokalne statyczne dostępne tylko w tej funkcji i przechowujące
 //dane pomiędzy kolejnymi przerwaniami. Więcej na ich temat:
 //http://mikrokontrolery.blogspot.com/2011/02/kurs-jezyka-c-zmienne-lokalne-statyczne.html
 
 //Parametry startowe ustawiają algorytm na końcu obwiedni,
 //stąd po uruchomieniu programu algorytm rozpocznie od warunku,
 //w którym sam ustawi się na początek obwiedni.

 //przerwań dla odliczenia 1ms
 static unsigned char licznik_przerwan = 0; 

  //pozostała ilość kroków w danym odcinku obwiedni
 static unsigned int obwiednia_licznik_krokow = 1;

 //aktualny nr generowanego odcinka obwiedni
 static unsigned char obwiednia_indeks = 3;


 //--- główna część funkcji --------------------------

 //czy już upłynęła 1 milisekunda?
 if(++licznik_przerwan > (TIMER0_PRZERWAN_NA_MILISEKUNDE-1)){

  //upłynęła 1ms czas zmienić próbkę PWM

  licznik_przerwan = 0;
  
  //czy to już ostatnia próbka w tej części obwiedni?
  if(--obwiednia_licznik_krokow == 0){
   //tak
   
   //Czy to już koniec obwiedni?
   if(++obwiednia_indeks > (OBWIEDNIA_ILOSC_ODCINKOW-1)) {

    //koniec obwiedni
    OCR2 =  PWM_MIN;  //ustaw minimum wypełnienia PWM
    obwiednia_indeks = 0; //resetuj licznik odcinków obwiedni
    obwiednia_licznik_krokow = obwiednia_kroki[0];

   }else{

    //nowy fragment obwiedni
    OCR2 = OCR2 + obwiednia_nachylenie[obwiednia_indeks];
    obwiednia_licznik_krokow = obwiednia_kroki[obwiednia_indeks];
   }
   
  }else{
   
   //dodaj do PWM kolejny krok stopnia nachylenia  
   OCR2 = OCR2 + obwiednia_nachylenie[obwiednia_indeks];

  }
 }
} 

Do pobrania

W załączniku projekt AVR Studio 4.18:  ADSR-ver1.zip  kopia: ADSR-ver1.zip
W załączniku znajdziesz także plik HEX skompilowany dla mikrokontrolera ATmega8 z kwarcem 16MHz.


Rezultat

Rezultat jaki zaobserwujesz na ekranie oscyloskopu w wyniku działania powyższego programu (oczywiście po naciśnięciu przycisku S1) będzie następujący:




Po prawej stronie widać już początek kolejnej obwiedni. I to właśnie chcieliśmy osiągnąć w zadaniu nr 1 :-)


Zadanie 2

Konkursowe zadanie nr 2 jest na tyle proste do wykonania, że nie będę go przedstawiał - wystarczy że sam zmodyfikujesz powyższy program. Zamiast tego ogłaszam:





NOWY KONKURS
Zadanie nr 3


Jak już wspomniałem w artykule konkursowym, dążymy do wykonania prostego syntezatora dźwięków. Powinniśmy więc mieć możliwość płynnej regulacji parametrów ADSR. Zadanie jest więc następujące:

Na bazie powyższego programu, należy utworzyć funkcję(-e), które umożliwią obliczenie współczynnika nachylenia (dla uproszczenia) tylko części R obwiedni w zależności od stanu napięcia na wejściu ADC do którego podłączymy dzielnik rezystorowy w postaci potencjometru.

EDIT 2013.10.09: Czas trwania części R ma być regulowany od 0 do 5 sekund.

Będziesz więc musiał tak zmodyfikować powyższy program, by przeliczał odpowiednio parametry w ostatnich komórkach tablic obwiednia_kroki[] oraz obwiednia_nachylenie[].

Zauważ, że tablica współczynników nachyleń jest typu char (liczby całkowite). Nam zależy na tym, by regulacja potencjometrem była w miarę płynna. Oznacza to, że należy wprowadzić ułamkowe wartości współczynnika nachylenia. Jednakże:

Nie dopuszczamy możliwości stosowania liczb zmiennoprzecinkowych.

Dlatego też Twoim zadaniem będzie także modyfikacja sposobu reprezentacji liczb w powyższym programie. Oczywiście rozwiązanie tego problemu zależy tylko od Ciebie :-)

Po dodaniu potencjometru oraz kondensatora C6 na pinie AREF (z powodu użycia przetwornika ADC), nasz schemat będzie więc wyglądał na przykład tak:





Reasumując

Rozwiązanie ma doprowadzić do sytuacji, w której za pomocą potencjometru będzie można regulować nachylenie odcinka R obwiedni, co zaobserwujemy na oscyloskopie.



Termin nadsyłania rozwiązań

Termin upływa w piątek 11 października 2013r. o godz. 23:59:59
Tak jak poprzednie zadania, także i to należy wysyłać na adres:


UWAGA!
  1. Gdyby adres był nieczytelny to podpowiem, że składa się z liczby mnogiej słowa mikrokontroler, kropki i mojego nicku, a domena to: gmail.com.
  2. Po dotarciu maila na wyżej podane konto, automatycznie otrzymasz mailem informację o tym.


Nagroda

Nagrodą będzie wybrana przez zwycięzcę książka wydawnictwa Helion w formie eBook. Innymi słowy zwycięzca sam wybierze nagrodę :-)


41
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

Działy
Działy dodatkowe
Inne
O blogu




Dzisiaj
--> za darmo!!! <--
1. USBasp
2. microBOARD M8


Napisz artykuł
--> i wygraj nagrodę. <--


Co nowego na blogu?
Śledź naszego Facebook-a



Co nowego na blogu?
Śledź nas na Google+

/* 20140911 Wyłączona prawa kolumna */
  • 00

    dni

  • 00

    godzin

  • :
  • 00

    minut

  • :
  • 00

    sekund

Nie czekaj do ostatniego dnia!
Jakość opisu projektu także jest istotna (pkt 9.2 regulaminu).

Sponsorzy:

Zapamiętaj ten artykuł w moim prywatnym spisie treści.