MATLAB we współpracy z Arduino
Wakacje w pełni, jednak być może warto już dziś pomyśleć o tym co będzie, kiedy się skończą. W ciągu kilku tygodni z górskich schronisk i nadmorskich plaż do domów i akademików zaczną wracać wypoczęci wczasowicze, radośni i pełni energii do codziennej aktywności. Oczywiście nie wszyscy z nas wykorzystują wakacje do dalekich, czy nawet bliskich podróży. W zasadzie nie ma przecież jakiegoś obowiązku, by w tym okresie brać urlop i gdzieś jechać. Nic nie zmieni jednak faktu, że wakacje to specyficzny okres. Biura i domy trochę pustoszeją, bo w końcu ktoś na te wakacje jedzie, atmosfera dzięki temu staje się luźniejsza. To sprawia, że często mamy sposobność realizacji projektów, nad którymi przez większość roku nie mamy szans przysiąść. Nikt nam w końcu nie przeszkadza, w ciszy i spokoju możemy skoncentrować się na interesujących nas zagadnieniach. Już niebawem, gdy z wakacyjnych wojaży wrócą nasi współpracownicy i współlokatorzy, wszystko się zmieni. Cóż, żyjemy w rozwiniętej kulturze, której wiele zawdzięczamy, ale która również pewnych rzeczy od nas wymaga. Gdy kolega stanie przy naszym biurku i zacznie opowiadać o tym jak fajnie było gdziekolwiek tam by nie był, to podstawowe zasady dobrego wychowania zmuszą nas do zwrócenia głowy w jego kierunku. Dobrze będzie również prowadzić szczątkową konwersację lub przynajmniej grzecznie potakiwać. Choć te obowiązkowe uprzejmości nie zajmują wiele czasu, są jednak w stanie wytrącić nas ze stanu podwyższonej koncentracji, niezbędnego przy podejmowaniu intelektualnych wyzwań. Ignorowanie natręta, choć skuteczne, nie jest na dłuższą metę dobrym rozwiązaniem. Ludzka życzliwość to coś, czego każdy z nas prędzej czy później potrzebuje, więc nie warto jej głupio tracić. Cóż więc począć? Oto ciekawa propozycja rozwiązania problemu: Uprzejmator, elektroniczny awatar, który wyręczy nas w uprzejmościowych obowiązkach. Zasada działania urządzenia jest bardzo prosta. Uprzejmator wykrywa kierunek, z którego dociera do niego dźwięk, zwraca się w jego stronę i opcjonalnie grzecznie przytakuje. Złóżcie, zaprogramujcie i postawcie blisko siebie na biurku. Odtąd żaden kolega, żona, ani nawet mama wołająca na pomidorową nie wytrącą was niepotrzebnie z równowagi i vice versa. Budowa uprzejmatora jest niezwykle prosta, do jego stworzenia potrzebne będą:
- MALTAB i Signal Processing Toolbox,
- Arduino (ja użyłem MEGA, jednak Uno powinno wystarczyć),
- Serwomechanizm,
- Dwa mikrofony analogowe ze wzmacniaczem,
- Kartofel (do wystrugania awatara),
- Opcjonalnie diody LED do sygnalizacji.
Idea działania
W jaki sposób można rozpoznać kierunek, z którego dobiega dźwięk? Uprzejmator, podobnie jak nasze głowy, wykorzystuje fakt, że fala dźwiękowa dociera do lewego mikrofonu (ucha) w innym czasie, niż do mikrofonu (ucha) prawego (Wiki) .
Różnica w czasie dotarcia dźwięku do mikrofonów wynosi:
c - prędkość dźwięku (340 m/s)
d - odległość między mikrofonami
Z formuły wynika, że znajomość wystarczy do określenia kąta padania fali dźwiękowej.
Schemat połączeń
Programowanie – Arduino
Firma MathWorks postarała się o uproszczenie współpracy MATLABa i Arduino, wydając specjalną, bezpłatną paczkę Arduino Support from MATLAB, jednak w przypadku Uprzejmatora nie możemy z niej skorzystać. Standardowa częstotliwość próbkowania dla pojedynczego wejścia analogowego w Arduino wynosi około 8000 Hz. My potrzebujemy większych częstotliwości a odpowiednia konfiguracja Arduino z poziomu MATLABa nie jest możliwa. W naszym przypadku MATLAB będzie się komunikował z Ardkiem tak jak z każdym innym urządzeniem, z którym możliwa jest komunikacja przy pomocy portu szeregowego. Tak na marginesie oznacza to, że w tym projekcie wcale nie trzeba korzystać z Arduino. Jeśli macie coś innego pod ręką być może też może się nadać.
Zaprogramowanie Arduino nie jest skomplikowane. Sprzęt wykorzystujemy jedynie jako system akwizycji danych dźwiękowych oraz jako generator przebiegów PWM sterujących serwomechanizmem. Niezbędne jest jednak wymuszenie podwyższonej częstotliwości próbkowania przez ustawienie odpowiednich rejestrów w bloku setup
. W głównej pętli program oczekuje na transmisję danych z zewnątrz (czyli ze strony MATLABa). Jeżeli na wejściu pojawia się wartość 999, realizowana jest procedura akwizycji i zwrotnego przesyłania danych, w przeciwnym wypadku dane wejściowe wykorzystywane są do ustawienia serwomechanizmu.
Wydając kilkanaście złotych na klon Arduino, czy nawet oryginalną wersję, nie możemy liczyć na jakość profesjonalnej karty pomiarowej. Pierwsze próbki danych pochodzących z wejść analogowych są na tyle niedorzeczne, że najlepiej je po prostu odrzucić. Ile z nich konkretnie nadaje się do kosza jest najprawdopodobniej indywidualną cechą konkretnego układu, w poniższym kodzie odrzucona jest cała partia – stąd podwójna pętla for.
Schemat blokowy programu Arduino
Kod programu Arduino
#include <Servo.h> // wykorzystywane piny const int micIn0 = A0; const int micIn1 = A15; const int LED1 = 12; const int LED2 = 13; const int servoPin = 9; const int N = 512; // szerokość okna, ilość próbek rejestrowanych podczas odczytu danych Servo myServo; int servoPosition; int servoDesiredPosition; unsigned long timerStart; unsigned long timerStop; int audioData0[N]; int audioData1[N]; void setup() { // zmiana częstotliwości próbkowania na ok. 30000 Hz na kanał cli(); bitClear(ADCSRA, ADPS0); bitClear(ADCSRA, ADPS1); bitSet(ADCSRA, ADPS2); sei(); // wejścia/wyjścia pinMode(micIn0, INPUT); pinMode(micIn1, INPUT); pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT); myServo.attach(servoPin); // transmisja szeregowa Serial.begin(115200); while (!Serial) { ; //oczekiwanie na połączenie } servoPosition = 90; myServo.write(servoPosition); } void loop() { delay(100); if (Serial.available() > 0) { servoDesiredPosition = Serial.parseInt(); // odebranie danych while (Serial.available() > 0) { Serial.read(); // usunięcie z bufora pozostałych danych } if (servoDesiredPosition != 999) { digitalWrite(LED1, HIGH); // aktualizacja docelowej pozycji serwomechanizmu if (servoDesiredPosition < servoPosition) { for (int i = servoPosition; i > servoDesiredPosition; i--) { myServo.write(i); delay(15); } } else if (servoDesiredPosition > servoPosition) { for (int i = servoPosition; i < servoDesiredPosition; i++) { myServo.write(i); delay(15); } } servoPosition = servoDesiredPosition; digitalWrite(LED1, LOW); } else if (servoDesiredPosition == 999) { // odczytanie danych z wejść analogowych u digitalWrite(LED2, HIGH); // podwójny odczyt, pierwsza seria jest odrzucana for (int j = 0; j < 2; j++) { // szacowanie czasu pomiaru timerStart = micros(); for (int i = 0; i < N; i++) { audioData0[i] = analogRead(micIn0); audioData1[i] = analogRead(micIn1); } timerStop = micros(); } // wysłanie informacji o ilości próbek i czasie akwizycji Serial.println(N); Serial.println((timerStop - timerStart)); // wysłanie pozostałych danych for (int i = 0; i < N; i++) { Serial.println(audioData0[i]); Serial.println(audioData1[i]); } digitalWrite(LED2, LOW); } } }
MATLAB
W celu komunikacji z Arduino w środowisku MATLAB należy utworzyć obiekt klasy serial, który będzie pośredniczył w wymianie danych. Gdy tworzymy obiekt, niezbędne jest określenie portu komunikacyjnego. Który port jest właściwy, najlepiej sprawdzić w środowisku Arduino IDE lub, jeśli mamy zainstalowany Arduino Support from MATLAB, wpisując polecenie arduino
.
s = serial('COM15')
Niezbędna okaże się również dodatkowa konfiguracja.
s.BaudRate = 115200; s.InputBufferSize = 8096;
W celu nawiązania komunikacji wymagana jest jeszcze instrukcja fopen
fopen(s);
Od tej pory możemy wysyłać do Arduino dane przy pomocy instrukcji fprintf
, jak i je odczytywać przy pomocy instrukcji fscanf
. Jeśli nie chcemy dłużej nawiązywać komunikacji, należy wykonać polecenie fclose(s)
w celu zamknięcia portu komunikacyjnego.
Kolejna istotna kwestia to częsta aktualizacja wykresów w oparciu o nadchodzące dane. Nie warto tutaj polegać na funkcji plot
. Każdorazowe wywołanie funkcji plot wiąże się z wykonaniem całej masy dodatkowych instrukcji co może spowolnić działanie aplikacji. O wiele lepszym sposobem jest przygotowanie wykresu, a następnie aktualizacja danych, które mają być wyświetlane.
f1 = figure(1); ax1 = axes(f1); p1 = plot(ax1, [0 1], [0 0], 'b'); hold(ax1, 'on'); p2 = plot(ax1, [0 1], [0 0], 'r'); grid(ax1, 'on'); ylim([-350 350]);
Umieszczenie nowych danych na wykresie można teraz wykonać w następujący sposób:
p1.XData = 1:10; p1.Ydata = rand(1,10);
W bardzo prosty sposób możemy sprawić, że nasz Uprzejmator przemówi ludzkim głosem. Wystarczy wykorzystać fakt, że w MATLABie istnieje możliwość posłużenia się bibliotekami .NET
NET.addAssembly('System.Speech'); speak = System.Speech.Synthesis.SpeechSynthesizer; speak.Volume = 100; Speak(speak, 'Dzień dobry!');
Czas przejść do kluczowej części programu, czyli wyznaczania kąta, o który należy obrócić serwomechanizm. Jest to możliwe dzięki funkcji xcorr
znajdującej się w przyborniku Signal Processing Toolbox. Interesuje nas maksymalna wartość funkcji i korespondujące z nią opóźnienie. Ponieważ znamy (mniej więcej) częstotliwość próbkowania dźwięku, możemy wprost ze wzoru wyznaczyć
[acor, lag] = xcorr(audio1, audio2, N_max, 'unbiased'); [M, I] = max(acor); lagDiff = lag(I); timeDiff = lagDiff * dt; sin_alpha = timeDiff * c / d;
Czas na pełny kod programu. Jeśli macie jakieś pytania, piszcie w komentarzach.
d = 0.29; % Odległość między mikrofonami c = 340; % prędkość dźwięku BaudRate= 115200; treshold = 80; % próg dla korelacji port = 'COM15'; teksty = {'Acha, jasne'; 'No, świetnie'; 'Tak... ale wiesz jak to jest'; ... 'Też tak myślę'; 'Zaczekaj chwilkę';... 'Chyba ty'}; % komunikacja if ~exist('s', 'var') s = serial(port); s.BaudRate = BaudRate; s.InputBufferSize = 8096; end if ~strcmp(s.Status, 'open') fopen(s); end pause(1); % przygotwanie wykresów f1 = figure(1); if isempty(f1.Children) ax1 = axes(f1); p1 = plot(ax1, [0 1], [0 0], 'b'); hold(ax1, 'on'); p2 = plot(ax1, [0 1], [0 0], 'r'); grid(ax1, 'on'); ylim([-350 350]); end f2 = figure(2); if isempty(f2.Children); ax5 = axes(f2); p5 = plot(ax5, [0 1], [0 0], 'b'); title(ax5, 'xcorr - unbiased'); grid(ax5, 'on'); end f3 = figure(3); if isempty(f3.Children) ax6 = axes(f3); hold(ax6, 'on'); axis(ax6, 'equal'); grid(ax6, 'on'); x = linspace(-d/2, d/2); y = sqrt((d/2)^2 - x.^2); plot(x, y); plot([-d/2 0 d/2], [0 0 0], 'rx', 'MarkerSize', 12) x = linspace(-d, d); y = sqrt((d)^2 - x.^2); plot(x, y, 'b') p6 = plot(0, d, 'ko', 'MarkerSize', 12); txt = text(0, 1.2*d, '\alpha = 0', 'FontSize', 12); end pb = uicontrol(f3, 'Style', 'pushbutton', 'String', 'Koniec'); pb.Callback = 'finish = true'; finish = false; % Syntezator mowy - .net NET.addAssembly('System.Speech'); speak = System.Speech.Synthesis.SpeechSynthesizer; speak.Volume = 100; Speak(speak, 'Dzień dobry!'); % wysłanie polecenia startu do arduino pause(0.5); fprintf(s, '999'); % Pętla główna while ~finish N = str2num(fscanf(s)); T = str2num(fscanf(s)); %czas akwizycji fs = (N/T)*10^6; dt = 1/fs; t = 0:dt:dt*(N-1); audio1_raw = zeros(1,N); audio2_raw = zeros(1,N); for i = 1:N audio1_raw(i) = str2num(fscanf(s)); audio2_raw(i) = str2num(fscanf(s)); end % przetwarzanie sygnału % usunięcie trendu pol1 = polyfit(1:N, audio1_raw, 1); pol2 = polyfit(1:N, audio2_raw, 1); y1 = polyval(pol1, 1:N); y2 = polyval(pol2, 1:N); audio1 = audio1_raw - y1; audio2 = audio2_raw - y2; % wykres przebiegu czasowego p1.XData = t; p1.YData = audio1; p2.XData = t; p2.YData = audio2; % wyznaczenie korelacji i wykres N_max = floor(d / (c*dt)); [acor, lag] = xcorr(audio1, audio2, N_max, 'unbiased'); p5.XData = lag; p5.YData = acor; % wyznaczanie kąta [M, I] = max(acor); lagDiff = lag(I); timeDiff = lagDiff * dt; sin_alpha = timeDiff * c / d; if abs(sin_alpha) > 1 warning('|sin(alpha)| > 1, srawdź ustawienia!') sin_alpha = sin_alpha / abs(sin_alpha); end if M > treshold && abs(sin_alpha) < 1 alpha = asin(sin_alpha); p6.XData = d*sin_alpha; p6.YData = d*cos(alpha); alpha = round(rad2deg(alpha)); beta = alpha + 90; txt.Position = [p6.XData, 1.2*d]; txt.String = ['\alpha = ' num2str(alpha) '^o']; drawnow; fprintf(s, num2str(beta)); Speak(speak, teksty{randi(length(teksty))}); end fprintf(s, '999'); drawnow; pause(0.001); end % pause(1) flushinput(s); fclose(s);
to żeś pojechał 😉
Świetne!