Informatyka 
 
Adruino i czujnik zbliżeniowy
Narcyz
Szeroko stosowanym i tanim czujnikiem zbliżeniowym jest czujnik podczerwony zawierający diodę świecącą w podczerwieni oraz fotodiodę wrażliwą na tą długość fali. I trochę elektroniki by na wyjściu był prosty jednobitowy sygnał: Przeszkoda jest. Albo nie.

Czujnik działa bardzo prosto: Podczerwona dioda LED świeci przed siebie, i jeśli jej światło trafi na przeszkodę – to ją oświetla. Światło odbite od przeszkody dociera między innymi do fotodiody, której prąd zależy od jasności odbieranego światła. Sygnał ten jest porównywany z wartością zadaną potencjometrem – w komparatorze, na którego wyjściu mamy już sygnał cyfrowy wskazujący czy wartość światła odbitego przekracza określoną wartość – co przekłada się na zmianę czułości a co za tym idzie – zasięgu takiego czujnika. Zasięgu, który zależy także od rodzaju odbijającej powierzchni – co pozwala wykrywać zmiany z podłożu po którym porusza się robot pozwalając wykryć nie tylko uskoki, ale narysowaną linię.

Czujniki te doskonale sprawdzają się we wszelkiego rodzaju robotach od zabawek w stylu line-followerów, po czujniki strefy bezpieczeństwa wykrywające jej naruszenie, które daje sygnał do zatrzymania ruchu. Do tego są proste – a więc tanie. My użyjemy czujnika w laboratorium fizycznym – do liczenia okresu wahadła, a właściwie czasu między kolejnymi przejściami przez położenie równowagi. I tu zaczyna się problem…

Liczenie impulsów liczy źle. Liczy stanowczo za dużo.

 

Winne są tu płynne zmiany odległości wykrywanej przeszkody – i idą ze wraz z nimi płynne zmiany sygnału na komparatorze. Sygnał na który nakłada się szum związany na przykład z natężeniem otaczającego światła. Ponadto układy mechaniczne drgają, więc granicę możemy wykryć nawet kilka razy.

W przypadku sterowania silnikami robota, lub jakimkolwiek układem mechanicznym – tego typu zakłócenia nie mają efektu. Kolejne odczyty są już poprawne, a przynajmniej w sporej większości. Układ mechaniczny ma swój czas reakcji i na błędne informacje po prostu nie zdążyłby zareagować. W przypadku zliczania impulsów przez komputer taką bezwładność musimy dodatkowo zaprogramować – na przykład sprawdzając czy impuls ma nadal stan wysoki po określonym czasie. Metoda całkiem dobra i pozwalająca na uzycie funkcji pulseIn:

    unsigned long newTime = 0;
    
    do {
        pulseIn(input_pin, HIGH);
        newTime = micros();
        delay(1);  
    } while(digitalRead(input_pin));

Ten fragment kodu zachowuje się jak semafor oczekując zdarzenia nad czujnikiem. Rozpoczynamy od oczekiwania na impuls na zadanym pinie. Co prawda funkcja pulseIn została stworzona do mierzenia długości impulsu, ale ponieważ by zmierzyć długośc trzeba doczekać do końca – funkcja ta czeka aż stan zmieni się z zadanego na przeciwny. W naszym przypadku – kiedy zmieni się z wysokiego na niski – czyli kiedy coś minie detektor. Jeśli taka sytuacja nastąpiła, to czekamy jedną milisekundę i sprawdzamy czy sygnał pozostał niski. Jeśli tak – wykryliśmy impuls.

Potrzebujemy mierzyć czas pomiędzy impulsami. Po wykryciu końca impulsu, zapamiętamy bieżącą wartość czasu w dodatkowej zmiennej i w następnym oczekiwaniu na impuls, odejmiemy od czasu w którym impuls opadł. Możemy użyć globalnej zmiennej, ale bardzie elegancko będzie umieścić ją, razem z funkcją oczekującą – w klasie

class pulse_reader {
    private:
        unsigned long timeOfLastPulse;
        byte input_pin;

    public:
        pulse_reader(byte pin);
        
        double getPulse();      
}; 

W tej klasie dodatkowo umieściliśmy numer wejścia na którym oczekujemy na impuls oraz konstruktor – który przygotuje obiekt do użycia inicjując czas początkowy oraz zapamiętujący wejście. Nie zapomnijmy też o ustawieniu pinu wejściowego tak – by pracował jako wejście:

 pulse_reader::pulse_reader(byte pin)
:   timeOfLastPulse(micros())
,   input_pin(pin)
{   
    pinMode(input_pin, INPUT);
} 

Samą funkcję oczekująca na impuls już znamy – ale warto trochę ja obudować. Po pierwsze, czas będziemy zwracali jako liczbę rzeczywistą – przedstawiająca liczbę sekund, dlatego od razu go sobie przeliczymy. Po drugie – czas jest w Arduino reprezentowany jako liczba milisekund w zmiennej o długości czterech bajtów – i taki licznik może się „przekręcić”. Na taką sytuację także musimy być przygotowani:

 double pulse_reader::getPulse()  {
    unsigned long newTime = 0;
    // put your main code here, to run repeatedly:
    do {
        pulseIn(input_pin, HIGH);
        newTime = micros();
        delay(1);  
    } while(digitalRead(input_pin));
    double pulseTime = (newTime-timeOfLastPulse)/1000000.0;
    if (pulseTime < 0) { 
        pulseTime = 4294.967296 - pulseTime;
    }
    timeOfLastPulse = newTime;
    return pulseTime;
} 

Teraz wystarczy w głównej pętli programu poczekać dwa razy i wysłać informację o sumarycznym czasie – by dostać okres drgań wahadła.

Możemy także już na poziomie Arduino wstępnie przesortować dane i wysyłać je tylko wtedy gdy zmierzona wartość nie różni się od średniej z ostatnich kilku ruchów. Nie z pełnej średniej – ale właśnie średniej z ostatnich kilku – powiedzmy – pięciu ruchów.

 class average {
    private:
        static const int queueSize = 5; 
        
        double queueData[queueSize];
        int elementsCount;
        double total;
    public:
        average()
        :   elementsCount(0)
        ,   total(0)
        {   }

        bool isValid();
        bool isClose(double d);
        double get();
        void put(double val);
        void clear();
};

Także i ten kawałek kodu zapakujemy w klasę. Warto zwrócić uwagę, że jeśli musimy uśredniać tylko ostatnich kilka dodanych wartości, to właśnie te ostatnie kilka – musimy pamiętać. Możemy użyć bufora cyklicznego, ale w przypadku zaledwie kilku wartości, możemy je po prostu przepisywać.

 void average::put(double val)  {
    if (elementsCount == queueSize) {
        total -= queueData[queueSize-1];
    }
    else  {
        ++elementsCount;
    }
    for (int i=queueSize-1; i; --i)  {
        queueData[i] = queueData[i-1];
    }
    total += val;
    queueData[0] = val;
} 

Zauważmy, że pamiętamy tu także sumę, po to, by można było bez problemy policzyć średnią, ale można byłoby także liczyć ją „na życzenie” Pamiętajmy, że w Arduino mamy mało pamięci programu i niewiele pamięci na dane, więc czasem trzeba opracowywać najprostsze, choć nie najszybsze, rozwiązania.

Przydadzą się nam funkcje sprawdzające czy jest już po czym liczyć poprawną średnią i druga – sprawdzająca czy podana wartość jest bliska średniej.

 bool average::isValid()  {
    return elementsCount == queueSize;
}

bool average::isClose(double val)  {
    if (!isValid())  {
        return false;
    }
    double deviation = abs(val/get() - 1);
    return deviation < 0.25;
} 

I wreszcie możliwość resetu obiektu uśredniającego oraz pobrania średniej:

 double average::get()  {
    if (isValid())  {
        return total/elementsCount;
    }
    return 0.0;
}

void average::clear() {
    total = 0;
    elementsCount = 0;
} 

Użycie tych elementów można zobaczyć w głównej pętli programu:

average           lastTimes;
average           goodResults;
pulse_reader      detector(7);
double	pendulumLength = 1.0;

void setup() {
    detector.getPulse();
    Serial.begin(9600);
};
 
void loop() {
    double periodTime = detector.getPulse();
    periodTime += detector.getPulse();

    if (lastTimes.isClose(periodTime)) {
        const double pi = 3.1415926536;
        double g = 4*pi*pi*pendulumLength/ periodTime / periodTime;
        goodResults.put(g);
        if (goodResults.isValid()) {
            lcd.display_val(goodResults.get(), 'g', 1);
        }
    }
lastTimes.put(periodHalfTime);
} 

Zadanie:
Znaleźć sytuację w której czujnik nie zadziała prawidłowo – dla którego pierwszy przykład kodu nie będzie działał prawidłowo. Chodzi o taki rozkład „drgania brzegu” w czasie działania programu, kiedy nie zauważymy że impuls już się pojawił.

Wskazówka: funcja pulseIn najpierw czeka aż mierzony impuls nadejdzie (chyba że wybrany stan jest już obecny na wejściu.

 
Opinie
 
Facebook
 
  
42343 wyświetlenia

numer 3-4/2017
2017-04-01


nowyOlimp.net na Twitterze

nowy Olimp - internetowe czasopismo naukowe dla młodzieży.
Kolegium redakcyjne: gaja@nowyolimp.net; hefajstos@nowyolimp.net