[153] Gadający woltomierz cz. 2
Poprzedni artykuł zakończyłem wtłoczeniem w pamięć Arduino Uno trzy i półsekundowego sampla. Myślę, że niejednego zaskoczy fakt, iż ten komunikat zajmuje tylko 26 kB. Oczywiście głośniczek podłączony wprost pod pin nie da wielkiej głośności i w razie potrzeby będzie trzeba wstawić wzmacniacz, choćby najprostszy, z jednego tranzystora. Po chwili zachwytów jednak zacznie nas irytować pisk. Cóż, PWM pracuje z częstotliwością 8 kHz, a to jest słyszalne i niemiłe. Zróbmy coś z tym.
#include <avr/pgmspace.h> // Zapisy co prawda nie są wymagane, ale zalecane.
#include <avr/interrupt.h>
const unsigned char sample[] PROGMEM = { // Tablica zawierająca próbki sampla.
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x81, 0x81, 0x81, 0x81,
0x81, 0x81, 0x80, 0x80, 0x81, 0x82, 0x81, 0x81, 0x80, 0x80, 0x81, 0x80,
0x80, 0x81, 0x81, 0x7F, 0x7F, 0x81, 0x81, 0x81, 0x80, 0x82, 0x83, 0x80,
// Dla czytelności usunąłem pozostałe wiersze tablicy.
0x80, 0x7F, 0x80, 0x81, 0x80, 0x80, 0x80, 0x80
};
const int sampleLen = sizeof(sample); // Długość sampla (dźwięku).
volatile int samplePos = 0; // Adres aktualnie odtwarzanej próbki sampla.
volatile byte oversampling = 0; // Licznik ominięcia zmiany stanu PWM dla nadpróbkowania.
void setup() {
pinMode(9, OUTPUT); // Ustaw port PWM jako wyjściowy.
TCCR1A = 0b10000010; // Konfiguruj timer 1: nieodwracające PWM, szybkie PWM.
TCCR1B = 0b00011001; // Zliczaj do ICR1, praca bez prescalera (16 MHz)
ICR1 = 499; // Ustaw częstotliwość przerwań na 32 kHz (16MHz/32kHz-1)
OCR1A = 128; // Startuj od wartości ciszy.
samplePos = 0; // Zeruj adres odtwarzanej próbki sampla.
TIMSK1 = 0b00000010; // Podłącz do przerwań źródło pochodzące z timera 1
sei(); // Włącz przerwania.
}
void loop() {
}
ISR(TIMER1_COMPA_vect) { // Obsługa przerwań od timera 1
if (++oversampling & 3) return; // Opuszczaj trzy na cztery wejścia w przerwanie.
if (samplePos < sampleLen) { // Jeśli wciąż trwa obsługa odtwarzania sampla...
OCR1A = pgm_read_byte(&sample[samplePos++]); // Ładuj próbkę z tablicy do rejestru PWM i zwiększ jej adres.
} else { // Jeśli to już była ostatnia próbka...
TIMSK1 = 0; // Wyłącz przerwania.
}
}Poprawka będzie prosta: powołamy do życia jeszcze jedną zmienną oversampling. W przerwaniu tym razem pozostaniemy co czwarty raz. Pozostałe trzy będziemy opuszczać od razu. Po co tak? Teraz PWM będzie pracować szybciej – z częstotliwością 32 kHz. Taki dźwięk siedzi już wysoko w ultradźwiękach. Ale nasz plik ma nadal częstotliwość próbkowania równą 8 kHz, więc dane nie będą się zmieniać w czterech kolejnych cyklach. Nazywa się to PWM oversampling, bo dotyczy tylko sygnału taktującego. Nie ma wpływu na poprawienie jakości samego odtwarzania, ale usuwa irytujący pisk. Teraz nasza pozytywka zabrzmi już czysto.
Czas zrobić coś użytecznego. Będziemy mierzyć napięcia na pinie A1, do którego na płytce edukacyjnej TME podpięto potencjometr. Oczywiście w praktycznych rozwiązaniach można sobie tam podpiąć jakąkolwiek wartość mierzoną. Żeby Arduino nie gadało bez końca, odczyt nastąpi po naciśnięciu przycisku przypiętego do pinu ósmego. Na chwilę wrócimy do mojego pliku z poprzedniego artykułu i podzielimy go, zgodnie z wcześniej narysowanymi markerami na dziesięć fragmentów.

Każdy będzie reprezentował jedną z cyfr. Ważne jest, by w miejscach podziału głośność schodziła do zera, inaczej odtwarzanie będzie pełne stuków. Teraz czeka nas mozolna konwersja każdej cyferki na tablicę – zgodnie ze sposobem przedstawionym w poprzednim artykule. W związku z czym tablic będzie dziesięć i będą się nazywać sample z cyfrą, która będzie tam zapisana.
#include <avr/pgmspace.h> // Zapisy co prawda nie są wymagane, ale zalecane.
#include <avr/interrupt.h>
const unsigned char sample0[] PROGMEM = { // Tablice zawierająca próbki sampli.
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x81, 0x81, 0x81, 0x81,
0x81, 0x81, 0x80, 0x80, 0x81, 0x82, 0x81, 0x81, 0x80, 0x80, 0x81, 0x80,
0x80, 0x81, 0x81, 0x7F, 0x7F, 0x81, 0x81, 0x81, 0x80, 0x82, 0x83, 0x80,
// Dla czytelności usunąłem pozostałe wiersze tablicy i wszystkie w pozostałych tablicach.
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80
};
const unsigned char sample1[] PROGMEM = {
};
const unsigned char sample2[] PROGMEM = {
};
const unsigned char sample3[] PROGMEM = {
};
const unsigned char sample4[] PROGMEM = {
};
const unsigned char sample5[] PROGMEM = {
};
const unsigned char sample6[] PROGMEM = {
};
const unsigned char sample7[] PROGMEM = {
};
const unsigned char sample8[] PROGMEM = {
};
const unsigned char sample9[] PROGMEM = {
};
const int sampleLens[] PROGMEM = { // Tablica z długościami sampli (dźwięków).
sizeof(sample0), sizeof(sample1), sizeof(sample2), sizeof(sample3),
sizeof(sample4), sizeof(sample5), sizeof(sample6), sizeof(sample7),
sizeof(sample8), sizeof(sample9)
};
const byte *const samples[] PROGMEM = { // Tablica z indeksami tablic z samplami.
sample0, sample1, sample2, sample3, sample4,
sample5, sample6, sample7, sample8, sample9
};
const int divisors[4] PROGMEM = { 1000, 100, 10, 1 }; // Tablica z podzielnikami dla kolejnych cyfr mierzonej wartości.
volatile int samplePos = 0; // Adres aktualnie odtwarzanej próbki sampla.
volatile byte oversampling = 0; // Licznik ominięcia zmiany stanu PWM dla nadpróbkowania.
volatile byte currentDigit = 0; // Bieżąca cyfra do odtwarzania (0-3)
volatile int value = 0; // Wartość mierzona.
volatile bool leadingZero = true; // Flaga pomijania nieznaczących zer.
static bool lastButton = LOW; // Poprzedni stan przycisku wyzwalającego czytanie napięcia.
const byte button = 8; // Adres przycisku wyzwalającego czytanie napięcia.
const byte voltmeter = A1; // Adres portu pomiaru napięcia.
void setup() {
pinMode(button, INPUT_PULLUP); // Ustaw port przycisku jako wejściowy.
pinMode(9, OUTPUT); // Ustaw port PWM jako wyjściowy.
TCCR1A = 0b10000010; // Konfiguruj timer 1: nieodwracające PWM, szybkie PWM.
TCCR1B = 0b00011001; // Zliczaj do ICR1, praca bez prescalera (16 MHz)
ICR1 = 499; // Ustaw częstotliwość przerwań na 32 kHz (16MHz/32kHz-1)
OCR1A = 128; // Startuj od wartości ciszy.
sei(); // Włącz przerwania.
}
void loop() {
bool presentButton = digitalRead(button); // Czytaj stan przycisku.
if (presentButton == HIGH && lastButton == LOW) { // Jeśli został wciśnięty i był puszczony...
value = analogRead(voltmeter); // Odczytaj napięcie z portu.
currentDigit = 0; // Zacznij odczyt od pierwszej cyfry.
leadingZero = true; // Ustaw status nieznaczących zer.
samplePos = 0; // Zeruj adres odtwarzanej próbki sampla.
TIMSK1 = 0b00000010; // Podłącz do przerwań źródło pochodzące z timera 1
}
lastButton = presentButton; // Przenieś do poprzedniego stanu przycisku stan obecny.
}
ISR(TIMER1_COMPA_vect) { // Obsługa przerwań od timera 1
if (++oversampling & 3) return; // Opuszczaj trzy na cztery wejścia w przerwanie.
byte numberDigit = (value / pgm_read_word(&divisors[currentDigit])) % 10; // Wyłuskaj ze zmiennej value aktualną cyfrę.
if (leadingZero && numberDigit == 0 && currentDigit < 3) { // Jeśli wykryłeś wiodące zero...
currentDigit++; // Pomiń czytanie.
return; // Zakończ obsługę przerwania.
}
leadingZero = false; // Nie ma już zer nieznaczących.
if (samplePos < pgm_read_word(&sampleLens[numberDigit])) { // Jeśli wciąż trwa obsługa odtwarzania sampla...
OCR1A = pgm_read_byte((byte *)pgm_read_word(&samples[numberDigit]) + samplePos++); // Ładuj próbkę z tablicy do rejestru PWM.
} else if (++currentDigit > 3) { // Jeśli to była ostatnia cyfra...
TIMSK1 = 0; // Wyłącz przerwania.
} else { // Jeśli nie ostatnia...
samplePos = 0; // Zeruj adres odtwarzanej próbki sampla.
}
}Wyciąłem je tutaj wszystkie oprócz fragmentu zerowej, bo to zajmuje mnóstwo miejsca, ale chyba wiadomo o co chodzi. Tablic będzie tu więcej. W kolejnej sampleLens mamy długości każdego z sampli, a w jeszcze następnej – samples indeksy, czyli nazwy tablic, by program mógł je łatwo wybierać za pomocą liczb. W końcu mamy jeszcze pomocniczą tablicę divisors z podzielnikami do konwersji czterocyfrowych liczb na cztery oddzielne cyfry.
Przybyło trochę stałych, związanych głównie z zamienianiem czterocyfrowej liczby na składowe, pomijaniem zer nieznaczących i obsługę przycisku. Część startowa nie różni się wiele od tego, co było w poprzednim szkicu, natomiast pętla główna nie jest już pusta. Sczytujemy tutaj stan przycisku i porównujemy ze stanem sczytanym w poprzednim przebiegu pętli. Tylko po jego dopiero co wciśnięciu zmierzymy napięcie, przygotujemy domyślny zestaw zmiennych i włączymy przerwania. Dzięki temu odczyt nastąpi zawsze po wciśnięciu przycisku, ale nie będzie się powielał. Resztą zajmują się już przerwania.
Mechanizm jest ten sam, co poprzednio, ale całość jest mocno zagmatwana, ponieważ teraz trzeba będzie sięgać do różnych tablic, jako że mamy dziesięć sampli odpowiadających dziesięciu cyfrom. Najpierw wyciągamy jedną z czterech cyfr ze zmierzonej wartości. Jeśli jest to zero wiodące – z wyjątkiem ostatniej liczby – opuszczamy przerwanie.
Jeśli odczytano jakąkolwiek inną liczbę, zerujemy flagę zer nieznaczących, żeby nie pominąć zera w środku liczby. Następnie wyciągamy adres konkretnej próbki, tym razem rozbudowanym wzorem, uwzględniającym także wyciągniętą przed chwilą cyfrę. Jeśli wysłaliśmy ostatni bajt ostatniej cyfry, kończymy odtwarzanie.
Teraz już możemy się bawić albo wykorzystać ten moduł jakoś pożyteczniej. Moduł jest w pełni użyteczny, choć ma wyraźne ograniczenie: nie łączy cyfr i nie odmienia ich. Nie przeszkadza to w pracy, ale jeśli ktoś chciałby posiąść bardziej elegancki woltomierze – zapraszam do kolejnego artykuł. I na koniec jeszcze – to nie wszystko, co można wycisnąć z 32 kB pamięci. Istnieje możliwość użycia kompresji ADPCM, która redukuje zużycie pamięci jeszcze dwukrotnie. Ale o tym innym razem.
W załączniku można znaleźć pełną wersję szkicu oraz sample użyte w projekcie.

Inne artykuły z tej kategorii
Jak wyświetlić cyfry o rozmiarze 10x8 i 10x16 pikseli?














































































































