Uprzejmator

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) .

dźwiękoludek

Różnica w czasie dotarcia dźwięku do mikrofonów wynosi:

 \Delta T = \frac{d}{c} \sin{\alpha}

c - prędkość dźwięku (340 m/s)

d - odległość między mikrofonami

Z formuły wynika, że znajomość \Delta T wystarczy do określenia kąta padania fali dźwiękowej.

Schemat połączeń

uprzejmator - schemat

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

schemat blokowy

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ć  \sin{\alpha}

[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);

(Visited 688 times, 1 visits today)

2 komentarze do “Uprzejmator”

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *