- dwa tryby pracy: generator lub wobulator,
- wyjście sygnału sinusoidalnego i prostokątnego,
- regulacja częstotliwości w zakresie od 1 Hz do 40 MHz, z krokiem regulowanym w zakresie od 1 Hz do 1 MHz,
- regulacja poziomu wyjściowego sygnału sinusoidalnego za pomocą potencjometru,
- możliwość zastosowania jednego z dwóch dostępnych na rynku, gotowych modułów generatorów DDS z chipem AD9850.
Oprogramowanie sterujące oraz montaż urządzenia
Program sterujący układem został napisany w całości w języku C dla mikrokontrolerów AVR i skompilowany w środowisku LINUX za pomocą popularnego kompilatora AVR-GCC z użyciem bibliotek z pakietu AVR-LIBC. Kod źródłowy został napisany w dość przejrzystym stylu i opatrzony wyczerpującymi komentarzami, dzięki czemu jego modyfikacje nie powinny być bardzo dużym wyzwaniem także dla mniej doświadczonych programistów, stając się dla nich jednocześnie źródłem inspiracji oraz polem do pożytecznych prac rozwojowych.
Na źródła programu sterującego pracą przyrządu składają się:
- AVT-5980.c - główny plik, zawierający m.in. funkcję main(),
- AVT-5980-lcd.c - plik zapewniający obsługę wyświetlacza LCD,
- AVT-5980-lcd.h - plik nagłówkowy dla pliku AVT-5980-lcd.c,
- AVT-5980-functions.c - plik zawierający pomocnicze funkcje sterowania urządzeniem,
- AVT-5980-functions.h - plik nagłówkowy dla pliku AVT-5980-functions.c.
Przyjęty podział nie był w zasadzie niezbędny, jednak zdaniem autora istotnie zwiększył czytelność opracowanego kodu. W dalszej części artykułu znajduje się zwięzły opis zawartości wymienionych zasobów.
Główny plik AVT-5980.c rozpoczynają definicje stałych (literałów) F_CPU i Fg_tab_MAX, niezbędnych do ustalenia: poprawnej częstotliwości pracy mikrokontrolera oraz liczby próbek generowanych częstotliwości dla wobulatora. Następnie dyrektywami #include włączone zostały wszystkie niezbędne dla prawidłowej kompilacji omawianego modułu pliki nagłówkowe. Dalej następują deklaracje wszystkich zmiennych i stałych o zasięgu globalnym dla tego pliku. Główny blok programu zawiera wyłącznie funkcję main(), ponieważ wszystkie funkcje pomocnicze zostały zadeklarowane i zdefiniowane w pozostałych plikach tego projektu.
W pierwszym kroku zaimplementowano konfigurację portów mikrokontrolera a także przypisano początkowe wartości tym z nich, które zdefiniowano jako wyjściowe. Port D w większości wykorzystano do obsługi wyświetlacza LCD (wyprowadzenia PD.4...PD.7 oraz PD.0...PD.2) z wyjątkiem linii PD.3, która stanowi wejście dla sygnału zewnętrznego przerwania INT1, przeznaczonego do precyzyjnego śledzenia zmian stanów wyprowadzeń enkodera obrotowego.
Wyprowadzenia PB.0 i PB.1 w porcie B są wejściami stanów dwóch styków enkodera a pin PB.2 jest wyjściem sygnału przetwornika cyfrowo-analogowego DAC, zrealizowanego w technice PWM z użyciem wewnętrznego wyjścia "B" licznika TCNT1 (OC1B). Pozostałe dostępne wyprowadzenia portu B (PB.3...PB.5) zarezerwowano dla sygnałów MOSI/MISO/SCK zewnętrznego programatora ISP i są wyprowadzone na wbudowanym w przyrząd złączu P201.
Wyprowadzenia PC.0 i PC.1 portu C zdefiniowano kolejno jako: wejście przetwornika pomiarowego ADC0 oraz sensor przycisku enkodera SW1. Natomiast wyprowadzenia PC.2...PC.5 są wyjściami dla sygnałów sterujących modułem DDS - kolejno: DDS_RESET, DDS_DATA, DDS_FQ_UD oraz DDS_W_CLK.
W dalszej części głównej funkcji programu sterującego następuje inicjalizacja pracy ekranu LCD, enkodera i modułu DDS. Warto tu zwrócić uwagę na fakt, że zaraz po zainicjowaniu obsługi zewnętrznego przerwania INT1 następuje włączenie globalnej flagi przerwań, co jest warunkiem koniecznym poprawnej obsługi enkodera obrotowego SW1 w dalszej części programu. Dodatkowo, zaraz po uruchomieniu modułu DDS zostaje wpisana do niego częstotliwość pracy równa 0 Hz, co sprawia, że fizycznie nie generuje on jeszcze żadnego sygnału.
Następnie realizowane jest ustawianie początkowych wartości kluczowych parametrów roboczych urządzenia. Odbywa się to z wykorzystaniem wbudowanej w mikrokontroler pamięci EEPROM. Najpierw wczytywana jest jednobajtowa flaga, której sześć najmłodszych bitów wskazuje na to, czy poszczególne parametry robocze były już wcześniej (kiedykolwiek) zapisywane w pamięci EEPROM, czy nie. Jeśli tak, to dany parametr roboczy wczytywany jest do odpowiedniej zmiennej z kolejnych komórek pamięci EEPROM. Jeśli nie, to taka zmienna przyjmuje z góry ustaloną wartość domyślną, którą oczywiście można zmienić w trakcie normalnej pracy przyrządu. Opisany proces dotyczy parametrów: eeprom_stamp (wspomniana flaga), mode (tryb pracy urządzenia), Fg, Fg_old, dFg_ind, Fl, Fh, Fw_ind a także dFg, Fw oraz Tw, pobieranych ze stałych tablic programu bazujących na odpowiednich indeksach, załadowanych wcześniej z pamięci EEPROM.
Dalej wyświetlane są dwa ekrany powitalno-informacyjne, z których każdy zawiera po dwie linijki tekstu. Przejście do drugiego ekranu oraz do dalszej części programu następuje w wyniku naciśnięcia (i zwolnienia) przycisku enkodera SW1.
Warto tu zwrócić uwagę na sposób programowej obsługi tego zdarzenia. Zwykle, aby uniknąć zjawiska zinterpretowania przez program wielokrotnego naciśnięcia, po zidentyfikowaniu zmiany stanu przycisku wprowadza się programowe opóźnienie za pomocą wbudowanej funkcji bibliotecznej lub po prostu bardzo długiej pętli. Oba te rozwiązania często nastręczają programistom (i późniejszym użytkownikom programu) sporo problemów z uwagi na to, że szybkość (a zatem i czas) wykonywania takich procedur może zależeć nie tylko od szybkości taktowania mikrokontrolera, ale także od rozbudowania kodu (liczby zagnieżdżeń wywoływanych funkcji i pętli) oraz wybranego dla procesu kompilacji poziomu optymalizacji. W grę bowiem wchodzi sposób użycia rejestrów procesora oraz jego stosu. W tej sytuacji najlepszym, często zalecanym przez doświadczonych programistów rozwiązaniem jest użycie do odliczania czasu któregoś z wbudowanych timerów mikrokontrolera lub zastosowanie procedury, która w ogóle nie wymaga odliczania czasu. W tym przypadku zastosowano (i powtórzono z powodzeniem w wielu miejscach omawianego programu) ostatnie z wymienionych podejść. W szczególności, zastosowano dwie nieskończone pętle z użyciem komendy while, z których pierwsza działa tak długo, aż zostanie wykryte naciśnięcia przycisku w SW1, a druga działa do momentu jego zwolnienia. Nie występuje tu ryzyko wpływu drgań styków przycisku, przed skutkami których zapobiega równolegle dołączona do styków pojemność C34.