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.
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!
- 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.
- 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
Mała poprawka, do programu na ATmega oraz załączonych plików. W kodzie był zamarkowany właściwy warunek, a dodany warunek dla testów zwiększający 8-krotnie częstotliwość generowania obwiedni (w celu wykonania zdjęcia na ekranie oscyloskopu):
OdpowiedzUsuń//if(++licznik_przerwan > (TIMER0_PRZERWAN_NA_MILISEKUNDE-1)){
if(++licznik_przerwan > 0){
prawidłowy jest warunek zamarkowany, a ten drugi należy usunąć. Czyli powinno zostać:
if(++licznik_przerwan > (TIMER0_PRZERWAN_NA_MILISEKUNDE-1)){
Ci którzy pobrali spakowane pliki wcześniej powinni pobrać je ponownie lub dokonać poprawek w kodzie.
*************** U W A G A ! ! ! ******************
OdpowiedzUsuńMaciej zadał pytanie cyt.: "Mam jeszcze jedno pytanie apropos nowego konkursu: w jakich granicach ma się zmieniać czas trwania (tym samym jego nachylenie) odcinka R obwiedni? Ma być od 0 do 70, czy w jakichś innych granicach? "
Moja odpowiedź:
"Przyjmijmy, że czas trwania R może wynosić do 5 sekund."
Zapomniałem dodać:
OdpowiedzUsuń******************** N A G R O D A **********************
Nagrodą będzie wybrana przez zwycięzcę książka wydawnictwa Helion w formie eBook. Innymi słowy zwycięzca sam wybierze nagrodę :-)
A ADSR nie dotyczy przypadkiem obwiedni? Przecież to powinno być sinusem, który ma obwiednię ADSR.
OdpowiedzUsuńW tym artykule i programach wyszukiwarka (Ctrl+F) znalazła 101 słów "obwiedni" :-)
UsuńObwiednia, to tylko regulacja amplitudy sygnału. Sygnałem natomiast może być sinus, prostokąt, piła, trójkąt lub dowolny inny.