1.11.2013

Ultradzwiękowy czujnik odległości HC-SR04

W ręce trafił mi taki oto czujnik więc opiszę jak się do niego "dobrać".  Na pewno się komuś przyda na przykład do konstrukcji robotów.

 HC-SR04
HC-SR04 to ultradźwiękowy czujnik odległości. Ma jedno wejście (Trig) i jedno wyjście (Echo). Zasilany musi być napięciem +5V. Aby dokonać pomiaru należy na wejściu Trig ustawić +5V na czas co najmniej 10us. Na wyjściu Echo dostaniemy wówczas impuls o szerokości proporcjonalnej do odległości od przeszkody. Piny Trig i Echo możemy podłączyć do dowolnego, wolnego pinu na płytce STM32F4Discovery. Ja akurat Trig podłączyłem do PC2, a Echo do PC1:


No dobra. Czas napisać jakiegoś kota. Do obsługi Pina Trig będziemy potrzebowali OutputPort, a pomiar szerokości impulsu pinu Echo zrealizujemy na InterrputPort. Zapamiętamy czas wystąpienia narastającego i opadającego zbocza. Szerokość impulsu to różnica tych czasów. Klasa realizująca obsługę czujki wygląda następująco:

public class HCSR04 : IDisposable
{
    private readonly ManualResetEvent _resetEvent = new ManualResetEvent(false);
    private readonly object _syncRoot = new object();
    private bool _disposing;

    private static InterruptPort _echo;
    private static OutputPort _trigger;
    private long _startTime;
    private long _stopTime;

    public HCSR04(Cpu.Pin triggerPin, Cpu.Pin echoPin)
    {
        _echo = new InterruptPort(echoPin, false,
                                  Port.ResistorMode.PullDown,
                                  Port.InterruptMode.InterruptEdgeBoth);
        _trigger = new OutputPort(triggerPin, false);

        _echo.OnInterrupt += (port, state, time) =>
                                 {
                                     if (state == 0)
                                     {
                                         _stopTime = time.Ticks;
                                         _resetEvent.Set();
                                         return;
                                     }

                                     _startTime = time.Ticks;
                                 };
    }

    public TimeSpan Ping()
    {
        lock (_syncRoot)
        {
            if (!_disposing)
            {
                _trigger.Write(false);
                _startTime = _stopTime = 0;

                _resetEvent.Reset();
                _trigger.Write(true);
                Thread.Sleep(1);
                _trigger.Write(false);
                _resetEvent.WaitOne(60, false);

                if (_startTime > 0 && _stopTime > 0)
                    return TimeSpan.FromTicks(_stopTime - _startTime);
            }
        }

        return TimeSpan.MaxValue;
    }

    public void Dispose()
    {
        lock (_syncRoot)
        {
            _disposing = true;

            _trigger.Dispose();
            _echo.Dispose();
        }
    }
}

Procedura Ping zwraca długość trwania impulsu na wyjściu Echo lub TimeSpan.MaxValue w przypadku niepowodzenia pomiaru. Wyjaśnienia wymaga użycie ManualResetEvent i instrukcji lock.

Najpierw lock. Procedura Ping może być wywoływana z różnych wątków jednocześnie. Próba wykonania kolejnego pomiaru (np. przez inny wątek) przed zakończeniem poprzedniego pomiaru zakłóciłaby cały proces. Dlatego instrukcją lock objęto kot, do którego wątki muszą mieć dostęp pojedynczo. Kolejny wątek, który będzie chciał wykonać pomiar, będzie czekał dopóki nie zakończy się wykonywanie tego bloku. 

Natomiast użycie ManualResetEvent pozwala, w jak najkrótszym czasie, na zakończenie procedury. Przed wysłaniem impulsu Trig resetEvent jest kasowany, a na opadającym zboczu impulsu ustawiany. W linijce z WaitOne w najgorszym razie program będzie więc czekał 60 milisekund. Jeśli zbocze opadające pojawi się wcześniej, to ustawienie resetEvent spowoduje szybsze przejście programu do następnej instrukcji. Oczywiście zamiast resetEvent można by było użyć Thread.Sleep(60), ale wówczas program czekał by zawsze 60 milisekund.

Do pełni szczęścia brakuje jeszcze dwóch statycznych procedur do zamiany długości impulsu na odległość:

public static float ToCentimeters(TimeSpan pulse)
{
    if (pulse.Equals(TimeSpan.MaxValue))
        return Single.MaxValue;

    float result = pulse.TotalMicroseconds()/58f;
    return result;
}

public static float ToInches(TimeSpan pulse)
{
    if (pulse.Equals(TimeSpan.MaxValue))
        return Single.MaxValue;

    float result = pulse.TotalMicroseconds()/148f;
    return result;
}

Teraz można już robić pomiary. Najpierw kawałek programu do wyświetlania odległości:

var sensor = new HCSR04(Stm32F4Discovery.FreePins.PC2,
                        Stm32F4Discovery.FreePins.PC1);

for (;;)
{
    TimeSpan pulse = sensor.Ping();

    float cm = HCSR04.ToCentimeters(pulse);
    string cmStr = cm.Equals(Single.MaxValue) ? "?" : cm.ToString("F1");

    float inch = HCSR04.ToInches(pulse);
    string inStr = inch.Equals(Single.MaxValue) ? "?" : inch.ToString("F1");

    Debug.Print("Distance: " + cmStr + " cm = " + inStr + " in");

    Thread.Sleep(1000);
}

Prościzna. Można też zamieniać odległość na częstotliwość i sterować wyjściem PWM tak, aby dioda mrugała szybciej jeśli zbliżamy się do przeszkody, a wolniej jeśli się oddalamy. Trochę przypomina to czujnik parkowania. Procedura zamiany na częstotliwość wygląda tak:

public static float ToFrequency(TimeSpan pulse)
{
    //0-30 cm --> 12-1 Hz

    const int barier = 30; //cm

    float cm = HCSR04.ToCentimeters(pulse);
    if (cm > barier)
        return 1;

    const int maxFreq = 12; //Hz

    float result = (cm*(1 - maxFreq))/barier + maxFreq;
    return result;
}

A właściwe użycie tak:

var sensor = new HCSR04(Stm32F4Discovery.FreePins.PC2,
                        Stm32F4Discovery.FreePins.PC1);

var distancePwm = new PWM(Cpu.PWMChannel.PWM_0, 1, 0, false);
distancePwm.Start();

for (;;)
{
    TimeSpan pulse = sensor.Ping();

    distancePwm.Frequency = ToFrequency(pulse);
    distancePwm.DutyCycle = 0.5;

    Thread.Sleep(200);
}

Konstruktorom robotów na pewno się przyda detektor kolizji. Niezależna klasa, która poprzez zdarzenie poinformuje o zbliżeniu się do przeszkody. W detektorze użyto timera do cyklicznego odpytywania czujnika HC-SR04. Detektor generuje zdarzenie w przypadku przekroczenia (zbliżenie i oddalenie) zadanej odległości od przeszkody.

public class CollisionDetector
{
    public delegate void StateChangedEventHandler(bool crash);
    public event StateChangedEventHandler StateChanged;

    public float Barier { get; set; }

    private readonly HCSR04 _sensor;
    private readonly Timer _timer;
    private bool _collision;

    public CollisionDetector(HCSR04 sensor, TimeSpan scanPeriod)
    {
        _sensor = sensor;
        _timer = new Timer(TimerTick, null, TimeSpan.Zero, scanPeriod);
    }

    private void TimerTick(object state)
    {
        TimeSpan pulse = _sensor.Ping();
            
        float distance = HCSR04.ToCentimeters(pulse);
        if(distance.Equals(Single.MaxValue))
            return;

        bool newState = distance <= Barier;
        if (_collision ^ newState)
        {
            _collision = newState;
            StateChangedEventHandler handler = StateChanged;
            if (handler != null)
                StateChanged(_collision);
        }
    }
}

Teraz czerwoną diodę zapalamy jeśli nadmiernie zbliżymy się do przeszkody, a gasimy jeśli odległość będzie bezpieczna. Nic nie stoi na przeszkodzie, aby zamiast diody sterować np. silniczkiem.

var sensor = new HCSR04(Stm32F4Discovery.FreePins.PC2,
                        Stm32F4Discovery.FreePins.PC1);

var crashLed = new OutputPort(Stm32F4Discovery.LedPins.Red, false);
var scanPeriod = new TimeSpan(0, 0, 0, 0, 100); //100ms
var detector = new CollisionDetector(sensor, scanPeriod) {Barier = 10}; //10cm
detector.StateChanged += crashLed.Write;

Thread.Sleep(Timeout.Infinite);

28.09.2013

Do czego służy Debug.Print

Czasami warto napisać o rzeczach oczywistych. To co tutaj opiszę odkryłem dopiero po jakimś czasie zabawy z .Net Micro Framework i od czasu do czasu się to przydaje.

Oczywiście do czego służy Debug.Print to chyba każdy wie. W ten sposób wpisujemy sobie w kodzie informacje diagnostyczne, które możemy obserwować w oknie Output w Visual Studio. Ale te komunikaty możemy obserwować nawet bez debuggera i bez Visual Studio. Wystarczy do tego MFDeploy. To ten programik do ładowania firmware (plików hex) do STM32F4Discovery. Wystarczy z menu Target wybrać Connect lub nacisnąć F5.


22.09.2013

Discovery ma przyszłość

Firma STMicroelectronics wprowadza do sprzedaży nową płytkę startową z STM32F4. Niestety nie ma jej jeszcze w sklepach. Nazywa się to 32F429IDISCOVERY (kod produktu STM32F429I-DISCO). 

32F429IDISCOVERY

To co się od razu rzuca w oczy to wyświetlacz LCD umieszczony bezpośrednio na płytce.
Najważniejsze parametry płytki:
  • mikrokontroler STM32F429ZIT6 (2 MB pamięci flash, 256 KB pamięci RAM)
  • na pokładzie programator ST-LINK/V2 ze złączem SWD
  • kolorowy touchpanel 2.4" QVGA TFT LCD, 240x320 punktów
  • trzyosiowy żyroskop L3GD20
  • pamięć SDRAM 64 Mbity 
  • dwie diody świecące do użycia w programach
  • jeden przycisk do użycia w programach
Zestaw w akcji można zobaczyć tutaj: http://www.stm32.eu/node/325, a dokumentację pdf można poczytać na stronie STM: http://www.st.com/web/catalog/tools/FM116/SC959/SS1532/PF259090.

4.08.2013

.NET Micro Framework 4.3 na STM32F4Discovery

Solucję dla Discovery4 można uzupełnić tak, aby można ją było skompilować dla .NET MF 4.3. Sprawa jest prosta. Poniżej kroki co trzeba zrobić.

Najpierw ściągamy i instalujemy lub rozpakowujemy Porting Kit 4.3 (RTM). Oczywiście najlepiej do jakiegoś katalogu o krótkiej ścieżce np. c:\MicroFrameworkPK_v4_3.

Następnie ze strony NETMF_for_STM32 ściągamy NETMF for STM32 (F4 Edition) Release 4.2 QFE2 RTM i nagrywamy na wcześniej przygotowany PK w wersji 4.3. Oczywiście solucja jest dla wersji 4.2.

Przygotowujemy solucję do PK w wersji 4.3. Tak naprawdę musimy uzupełnić tylko pewne funkcje. Są to:  
  • AD_Uninitialize w pliku DeviceCode\Targets\Native\STM32F4\DeviceCode\STM32F4_Analog\STM32F4_AD_functions.cpp
  • DA_Uninitialize w pliku DeviceCode\Targets\Native\STM32F4\DeviceCode\STM32F4_DA\STM32F4_DA_functions.cpp
Plik z gotowymi zmianami można pobrać tutaj: NETMF for STM32 (F4 Edition) Release 4.3 RTM

Można kompilować. Jak chcemy skompilować za pomocą GNU GCC, to wgrywamy jeszcze do tego pliki NETMF for STM32 (F4 Edition) Release 4.2 QFE2 RTM GCC. Tak jak w poprzednim artykule. Dla GNU GCC również i tutaj kompilujemy tylko TinyCLR. TinyBooter może zostać bez zmian z wersji 4.2.

Gotowe pliki wsadów ER_CONFIG i ER_FLASH można pobrać z https://kodfilemon.googlecode.com/svn/trunk/STM32F4Discovery_Hex/

Do wersji trzeba jednak podchodzić ostrożnie, bo nie sprawdzałem czy wszystkie peryferia działają poprawnie.

14.07.2013

Kompilacja solucji darmowym kompilatorem GCC

Aby skompilować solucję na STM32F4Discovery potrzebujemy płatnych wersji kompilatora Keil MDK lub ARM RVDS. Kompilatory te sporo kosztują więc zakup do zastosowań amatorskich raczej odpada. Jednak już od jakiegoś czasu w projekcie GHI's Open Source NETMF Ports jak i również Netduino używają do kompilacji darmowych kompilatorów GCC. Postanowiłem więc i ja zaadoptować ich rozwiązanie do kompilacji solucji dla STM32F4Discovery. Praca zakończyła się sukcesem i od jakiegoś czasu używam darmowego kompilatora. TinyCLR wychodzi trochę większy (o około 27k) , jest też pewnie mniej wydajny, no ale coś za coś. Poniżej opiszę co i jak trzeba zrobić. Opis dotyczy kompilacji dla wersji PK .NET MF 4.2 RTM QFE2, czyli oficjalnej wersji solucji Discovery4. Katalog z PK trzeba przygotować tak jak we wcześniejszym opisie kompilacji.

Po pierwsze ściągamy i instalujemy kompilator. Ja ściągnąłem wersję zalecaną do kompilacji np. FEZCerberus z GHI z jakieś pół roku temu, czyli GCC ARM Embedded 4.6-2012-q4-update. Jeśli komuś uda się skompilować nowszą wersją, niech da znać w komentarzach. Oczywiście ściągamy plik exe (windows installer) i odpalamy. Wszystko robimy standardowo z jednym małym wyjątkiem. Zmieniamy katalog instalacji na przykład na taki:


Po zainstalowaniu kompilatora trzeba ściągnąć i nagrać na wcześniej przygotowany PK z Discovery4 pliki przygotowane przeze mnie. Pliki te wyciągnąłem z portu GHI. Można je pobrać z mojego svna:
NETMF for STM32 (F4 Edition) Release 4.2 QFE2 RTM GCC

Czas na kompilację. Kompilację TinyBooter możemy sobie darować. Najlepiej użyć oryginalnego lub mojego z katalogu OneWire. On się nie zmienia. Jeśli spróbujemy go skompilować przy pomocy GCC, to może się okazać, że wygenerowany wsad będzie większy niż obszar w którym się ma zmieścić.

To co nas interesuje to kompilacja TinyCLR. Uruchamiamy okno konsoli cmd i przechodzimy do katalogu PK. Odpalamy polecenie:

setenv_gcc 4.6.2 c:\gcc46


Teraz odpalamy tylko kompilację TinyCLR poleceniem:

msbuild /p:flavor=RELEASE;memory=FLASH Solutions\Discovery4\TinyCLR\TinyCLR.proj


i czekamy na zakończenie:


W katalogu C:\MicroFrameworkPK_v4_2\BuildOutput\THUMB2\GCC4.6\le\FLASH\release\Discovery4\bin\tinyclr.hex dostaniemy nasze ER_CONFIG i ER_FLASH, które standardowo, przy pomocy MFDeploy ładujemy do STM32F4Discovery.

Paczka z plikami modyfikującymi solucję, zawiera obsługę karty SD i OneWire, więc jeśli ktoś tego nie chce to musi sobie usunąć z projektu.

2.07.2013

Web server na STM32F4Discovery

Przez sieć możemy w prosty sposób sterować STM32F4Discovery. Dla przykładu program do zapalania i gaszenia diod LED znajdujących się na płytce przez stronę WWW.

Zaczynamy od konfiguracji interfejsu i podpięcia się do zdarzenia tak, aby odbierać żądania HTTP przychodzące na port 80.

Adapter.Start(new byte[] {0x5c, 0x86, 0x4a, 0x00, 0x00, 0xdd},
                "stm32f4", SPI.SPI_module.SPI3,
                Stm32F4Discovery.FreePins.PD1, Stm32F4Discovery.FreePins.PA15);

Adapter.ListenToPort(80);
Adapter.OnHttpReceivedPacketEvent += OnHttpReceivedPacketEvent;

Dodatkowo zdefiniujemy pomocniczy słownik, aby łatwo wybierać odpowiednią diodę na podstawie koloru. Red, Blue, Orange i Green to stałe napisy, które pojawiać się będą w żądaniu. Na przykład wejście na stronę http://stm32f4/led.svc?Orange (tak, zamiast IP można podać nazwę z konfiguracji interfejsu) będzie powodowało zmianę stanu diody pomarańczowej.

_leds = new Hashtable
            {
                {Red, new OutputPort(Stm32F4Discovery.LedPins.Red, false)},
                {Blue, new OutputPort(Stm32F4Discovery.LedPins.Blue, false)},
                {Orange, new OutputPort(Stm32F4Discovery.LedPins.Orange, false)},
                {Green, new OutputPort(Stm32F4Discovery.LedPins.Green, false)}
            };
Została jeszcze implementacja metody obsługującej zdarzenia OnHttpReceivedPacketEvent. Na początku wyciągamy ze ścieżki  zawartość zapytania (po znaku ?) i plik. Na podstawie tych wartości decydujemy czy obsłużyć żądanie czy zwrócić HTTP 404 (linia 34) i którą diodę przełączyć (linia 16). W StringBuilderze produkujemy stronę (odpowiedź serwera), która składa się z 4 linków o odpowiednich adresach.

private static void OnHttpReceivedPacketEvent(HttpRequest request)
{
    lock (SyncRoot)
    {
        int filePos = request.Path.LastIndexOf('/');
        string file = filePos == -1 ? String.Empty : request.Path.Substring(filePos + 1);

        int queryPos = file.LastIndexOf('?');
        string query = queryPos == -1 ? String.Empty : file.Substring(queryPos + 1);

        if (queryPos != -1)
            file = file.Substring(0, queryPos);

        if (file == "led.svc")
        {
            var led = _leds[query] as OutputPort;
            if (led != null)
                led.Write(!led.Read());

            var sb = new StringBuilder("<html><head></head><body>");
            foreach (string key in _leds.Keys)
                sb.Append("<a href=\"/led.svc?" + key + "\">" + key + "</a> ");
            sb.Append("</body></html>");

            byte[] responseBuffer = Encoding.UTF8.GetBytes(sb.ToString());
            using (var responseStream = new MemoryStream(responseBuffer))
            {
                request.SendResponse(new HttpResponse(responseStream));
            }

            return;
        }

        request.SendNotFound();
    }
}

Pełny kot: DemoEnc28J60mIP_WebSrv

1.07.2013

Ethernet na ENC28J60

Długo się przymierzałem do odpalenia sieci na STM32F4Discovery. Na początku nie wiedziałem jakie układy wybrać. Czy iść w stronę MII/RMII czy SPI. Ostatecznie wybrałem SPI czyli coś na układzie ENC28J60. Za tym wyborem przemawia duża liczba publikacji w internecie oraz to, że niektóre płytki do .NET Micro Framework np.: od GHI czy Secret Labs ma interfejsy sieciowe zbudowane na tym układzie. Zaadoptowanie więc rozwiązania do STM32F4Discovery nie powinno być trudne. Jest jeszcze jedna zaleta ENC28J60 jest tani. Gotowy moduł można kupić za około 30 zł. Na początek w sam raz. Ja używam takiego modułu produkcji firmy lcsoft:

ENC28J60 STM32F4Discovery


Sam moduł oczywiście może być inny. Można też samemu zrobić taki interfejs. Ja go po prostu kupiłem.

Kolejnym problemem jest zasilanie. Układ ENC28J60 potrzebuje 3.3V (wg noty katalogowej minimum 3.14V). Na płytce STM32F4 Discovery dostępne mamy tylko 3V i 5V. Trzeba więc użyć zewnętrznego, gotowego zasilacza lub zrobić samemu zasilacz np. tak aby 5V obniżyć do 3.3V. Jeśli mamy zamiar zasilać z 5V to stabilizator powinien być LDO. Ja zrobiłem własny zasilacz na układzie LDO LM1117S według noty katalogowej. Jak to połączyć można zobaczyć na stronie http://fritzing.org/projects/power-3v/.

Kolejnym problemem jest pobór prądu układu ENC28J60. Może on dochodzić do 300mA. Do tego dochodzi jeszcze zasilanie samej płytki STM32F4Discovery. Na przykład przy mruganiu diodami pobiera ona około 40mA. Następne 40mA pobiera część ST-Link/V2 na płytce w stanie spoczynku. W końcu może się okazać, że 500mA z portu USB (maksymalna wydajność dla wersji 2.0) może nie wystarczyć. Port USB 3.0 ma wydajność prądową 900mA więc nie powinno być problemu. Trzeba mieć na uwadze jeszcze to, że wydajność prądowa portu USB może się różnić w zależności od płyty głównej komputera. Jakie jest rozwiązanie? Najprościej użyć zewnętrznego zasilacza do zasilania ENC28J60. Ja poradziłem sobie inaczej. Użyłem dwóch portów USB. Do płytki podłączyłem dwa kabelki USB: do CN1 i CN5. Następnie wyjście PA9 płytki (+5V portu USB CN5) podłączyłem do stabilizatora dla ENC28J60. W taki sposób port CN1 zasila standardowo płytkę, natomiast CN5 zasila moduł ethernet.

No dobra podpinamy moduł ethernet do STM32F4 Discovery. Do tego celu użyłem portu SPI3. Nic jednak nie stoi na przeszkodzie, aby użyć SPI2 (SPI1 nie testowałem). SPI3 ma następujące wyjścia: MSK=PC10, MISO=PC11, MOSI=PC12. Na CS i INT możemy wybrać dowolne, wolne piny np. CS=PA15, a INT=PD1. Wygląda to tak:

ENC28J60 STM32F4Discovery

Jak to uruchomić i się przy tym nie narobić? Z pomocą przychodzi projekt mIP - A C# Managed TCP/IP Stack for .NET Micro Framework. Szkoda tylko, że nie rozwijany. Biblioteka mIP pozwala obsługiwać w prosty sposób ethernet na ENC28J60 bez modyfikacji i kompilacji PK. Nie jest idealna (czasami coś się zacina), ale na razie musi wystarczyć. Do naszego projektu, do referencji, trzeba dodać wcześniej skompilowaną bibliotekę dll lub projekt mIP (NetworkingService i MultiSPI). W źródłach mIP znajdziemy też parę przykładów jak korzystać z biblioteki. Między innymi jak pobrać czas z serwera NTP requestem UDP i prosty serwer http.

Pierwszy kot. Jeśli mamy w sieci serwer DHCP, to będzie bardzo proste. Jeśli DHCP nie mamy to parametry połączenia trzeba ustawić ręcznie (kot znajdziemy w przykładach mIP). Sprawdzamy więc czy wszystko działa:

const SPI.SPI_module spiBus = SPI.SPI_module.SPI3;
const Cpu.Pin chipSelectPin = Stm32F4Discovery.FreePins.PA15;
const Cpu.Pin interruptPin = Stm32F4Discovery.FreePins.PD1;
const string hostname = "stm32f4";
var mac = new byte[] {0x5c, 0x86, 0x4a, 0x00, 0x00, 0xdd};

Adapter.Start(mac, hostname, spiBus, interruptPin, chipSelectPin);
Po uruchomieniu programu w oknie output powinniśmy zobaczyć coś takiego - znaczy wszystko jest ok:

Link is now up :)
Setting IP Address to 10.15.16.109
DHCP SUCCESS! We have an IP Address - 10.15.16.109; Gateway: 10.15.16.1
Updating Gateway Mac from ARP
Done.

W przypadku problemów można zobaczyć bardziej dokładne logi ustawiając przed startem tryb verbose:

Adapter.VerboseDebugging = true;
Adapter.Start(mac, hostname, spiBus, interruptPin, chipSelectPin);

Jak wszystko jest w porządku to "bardziej zaawansowany" przykład. Będziemy pobierać prawdziwą liczbę losową z przedziału 0 do 100 z serwisu www.random.org przez HTTP API.

Aha. Jest pewne ograniczenie w bibliotece mIP. Otóż warstwa TCP może odebrać tylko jeden pakiet (wysyłać może wiele). Więc jeśli odpowiedź przychodzi pofragmentowana to kiszka.

Aha 2. Jest jeszcze jedna sprawa. Aby wykonywać requesty GET ze znakiem ? w url, to trzeba grzebnąć w źródłach mIP. W pliku TCP.cs w linii 556 trzeba wywalić wywołanie funkcji System.Web.HttpUtility.UrlEncode. Czyli linię z:
Path = System.Web.HttpUtility.UrlEncode(url.Substring(Host.Length).Trim(), false);
trzeba zamienić na:
Path = url.Substring(Host.Length).Trim();
No dobra. Teraz już właściwy kot. Jak widać nie jest zbyt skomplikowany:

const SPI.SPI_module spiBus = SPI.SPI_module.SPI3;
const Cpu.Pin chipSelectPin = Stm32F4Discovery.FreePins.PA15;
const Cpu.Pin interruptPin = Stm32F4Discovery.FreePins.PD1;
const string hostname = "stm32f4";
var mac = new byte[] {0x5c, 0x86, 0x4a, 0x00, 0x00, 0xdd};

Adapter.Start(mac, hostname, spiBus, interruptPin, chipSelectPin);

const int minVal = 0;
const int maxVal = 100;

string apiUrl = @"http://www.random.org/integers/?num=1"
    +"&min=" + minVal + "&max=" + maxVal 
    + "&col=1&base=10&format=plain&rnd=new";

var request = new HttpRequest(apiUrl);
request.Headers.Add("Accept", "*/*");

HttpResponse response = request.Send();
if (response != null) 
    Debug.Print("Random number: " + response.Message.Trim());

16.06.2013

ST-Link i Internal command error

Dzisiaj przytrafiła mi się ciekawa sytuacja. Ostatnio przeinstalowałem system operacyjny i musiałem na nowo wgrywać wszystkie sterowniki i programy, które potrzebuję. Zainstalowałem też najnowszy (wersja 3.0.0) ST-Link utility. Uruchomiłem go, bez problemu skasowałem chip STM32F4Discovery i wgrałem tinybooter.

Wszystko było ok. Visual Studio widział płytkę. Wgrywałem i debugowałem programy itp. Standardowo. Po jakimś czasie znowu potrzebowałem wyczyścić całkowicie chip i wgrać tinybooter. Jednak po odpaleniu ST-Link utility i wybraniu z menu Connect otrzymałem komunikat Internal command error.

Zacząłem sprawdzać podłączenie USB, zasilanie, "wysiadłem i wsiadłem do windy", czy gdzieś nie mam zwarcia na płytce itp. Z mojej strony wszystko było ok. MFDeploy działał poprawnie. Bez problemu mogłem wgrać plik CLR i konfig. O co chodzi?

Tknięty przeczuciem odinstalowałem ST-Link w wersji 3.0.0 i zainstalowałem starszą wersję (na szczęście jej nie usunąłem z dysku) w wersji 2.3.0. Wszystko wróciło do normy! Bez problemu ST-Link się łączył z płytką. Dziwna sprawa.

22.05.2013

Pozytywka z PWM

Znalazłem jeszcze jedno zastosowanie PWM, dosyć rozrywkowe. Do wyjścia PWM można podłączyć głośniczek i odtwarzać proste melodie. Najpierw schemat:

STM32F4 Speaker

STM32F4Discovery Speaker
Rezystor może być inny, ale nie mniejszy niż 110, 120 omów. Z wyjścia STM32F407 można pobierać tylko 25mA prądu. Biorąc pod uwagę rezystancję głośniczka mamy równanie: 3V/(8ohm + 120ohm) =  0,023A. Tak więc przy tej rezystancji zmieścimy się w zakresie.

Teraz wystarczy tylko poszukać jakie częstotliwości odpowiadają poszczególnym dźwiękom i można odtworzyć dźwięki gamy:

//C, D, E, F, G, A, H, C 
var scale = new[]{261.6, 293.7, 329.2, 349.6, 
                  391.9, 440.0, 493.9, 523.2}; 
var pwm4 = new PWM(Cpu.PWMChannel.PWM_4, 50, 0, false);
pwm4.Start();

foreach (double note in scale)
{
    pwm4.Frequency = note;
    pwm4.DutyCycle = 0.5;
    Thread.Sleep(1000);
}

pwm4.Stop();


Aby zagrać jakąś prostą melodię trzeba się nieźle nawpisywać tych częstotliwości. Z pomocą przychodzi format RTTL lub MML. Na internecie można znaleźć mnóstwo melodyjek zapisanych w tych formatach.

Konstrukcję odtwarzacza RTTL rozpoczynamy od zdefiniowania interfejsu ISpeaker. Interfejs da możliwość dowolnej implementacji urządzenia wyjściowego. Jak widać są tylko dwie metody Play i Pause. Czyli to co głośniczek najlepiej umie robić: albo milczeć albo grać.

public interface ISpeaker
{
    void Pause();
    void Play(double frequency);
}

Teraz implementacja tego interfejsu na wyjściu PWM z podłączonym głośniczkiem:

public class PwmSpeaker : ISpeaker
{
    private readonly PWM _pwm;

    public PwmSpeaker(PWM pwm)
    {
        _pwm = pwm;
        _pwm.Frequency = 50;
        _pwm.DutyCycle = 0;
        _pwm.Start();
    }
            
    public void Play(double frequency)
    {
        _pwm.Frequency = frequency;
        _pwm.DutyCycle = 0.5;
    }

    public void Pause()
    {
        _pwm.Frequency = 50;
        _pwm.DutyCycle = 0;
    }
}

Przechodzimy do odtwarzacza RTTL. Całe zadanie polega na zdekodowaniu napisu w formacie RTTL na poszczególne częstotliwości dźwięków i czas ich trwania. Później trzeba to wysłać do ISpeakera.

Do dekodowania formatu RTTL na bardziej przyjazne dane służy statyczna metoda Parse klasy Rttl:

public static Rttl Parse(string rttlData)
{
    string[] sections = rttlData.Split(':');

    string name = sections[0].Trim();
    string[] tones = sections[2].Split(',');
    int duration = 4;
    int octave = 6;
    int bpm = 63;

    if (sections[1].Length > 0)
    {
        string[] controls = sections[1].Split(',');
        foreach (string item in controls)
        {
            string control = item.Trim();

            string valueStr = control.Substring(2, control.Length - 2);
            int value = Int32.Parse(valueStr);

            switch (control[0].ToLower())
            {
                case 'd':
                    duration = value;
                    break;

                case 'o':
                    octave = value;
                    break;

                case 'b':
                    bpm = value;
                    break;
            }
        }
    }

    var result = new Rttl
                        {
                            Name = name,
                            Tones = tones,
                            Duration = duration,
                            Octave = octave,
                            Bpm = bpm
                        };
    return result;
}

Jak już mamy wszystkie potrzebne dane to trzeba znaleźć tylko odpowiednie częstotliwości. Klasa RttlPlayer ma zdefiniowane tablice z częstotliwościami poszczególnych dźwięków. Wartości wypełnione są tylko dla pierwszej oktawy. Pozostałe oktawy zostaną uzupełnione poprzez podwojenie częstotliwości z oktawy poprzedzającej w statycznym konstruktorze.

private static readonly double[][] Scales = new[]
                                                {
                                                    new[]
                                                        {
                                                            //C, Cis, D, Dis, E, F, Fis, G, Gis, A, Ais, H 
                                                            261.6, 277.2, 293.7, 311.2, 329.2,
                                                            349.6, 370, 391.9, 415.3, 440.0, 466.2, 493.9
                                                        },
                                                    new double[12],
                                                    new double[12],
                                                    new double[12]
                                                };

static RttlPlayer()
{
    for (int i = 1; i < Scales.Length; i++)
        for (int j = 0; j < Scales[i].Length; j++)
            Scales[i][j] = 2*Scales[i - 1][j];
}

No teraz to wystarczy wszystko połączyć w całość w metodzie Play RttlPlayera.

public void Play(string rttlData)
{
    const int oneBpmWholeNote = 60*4*1000; //one bpm whole note in ms

    Rttl rttl = Rttl.Parse(rttlData);
    int wholeNote = oneBpmWholeNote/rttl.Bpm;

    foreach (string tone in rttl.Tones)
    {
        bool specialDuration;
        string durationStr, noteStr, scaleStr;
        ParseCommand(tone, out durationStr, out noteStr, out scaleStr, out specialDuration);

        int duration = rttl.Duration;
        if (durationStr.Length > 0)
            duration = Int32.Parse(durationStr);

        duration = specialDuration ? (3*wholeNote)/(2*duration) : wholeNote/duration;

        int freqIndex;
        switch (noteStr[0].ToLower())
        {
            case 'c':
                freqIndex = 0;
                break;

            case 'd':
                freqIndex = 2;
                break;

            case 'e':
                freqIndex = 4;
                break;

            case 'f':
                freqIndex = 5;
                break;

            case 'g':
                freqIndex = 7;
                break;

            case 'a':
                freqIndex = 9;
                break;

            case 'b':
                freqIndex = 11;
                break;

            default:
                freqIndex = -1;
                break;
        }

        if (noteStr.Length > 1)//#
            freqIndex++;

        int scale = rttl.Octave;
        if (scaleStr.Length > 0)
            scale = Int32.Parse(scaleStr);

        if (freqIndex >= 0)
        {
            double freq = Scales[scale - 4][freqIndex];

            Debug.Print("Playing: (" + tone + ")" + freq + " " + duration);
            _speaker.Play(freq);
            Thread.Sleep(duration);
            _speaker.Pause();
        }
        else
        {
            Debug.Print("Pausing: (" + tone + ") " + duration);
            _speaker.Pause();
            Thread.Sleep(duration);
        }
    }
}

A tak można odtwarzać melodyjki:

var pwm = new PWM(Cpu.PWMChannel.PWM_4, 50, 0, false);
var player = new RttlPlayer(new PwmSpeaker(pwm));
const string song = "Indiana:d=4,o=5,b=250:e,8p,8f,8g,8p,1c6,8p.,d,8p,8e,1f,p.,g,8p,8a,8b,8p,1f6,p,a,8p,8b,2c6,2d6,2e6,e,8p,8f,8g,8p,1c6,p,d6,8p,8e6,1f.6,g,8p,8g,e.6,8p,d6,8p,8g,e.6,8p,d6,8p,8g,f.6,8p,e6,8p,8d6,2c6";
player.Play(song);

Przykład odtwarzania formatu MTL można znaleźć w projekcie .NET Micro Framework Toolbox.

5.05.2013

PWM

Dzisiaj postanowiłem się pobawić PWM. W PWM chodzi o to, że sterujemy wypełnieniem sygnału prostokątnego przy stałej częstotliwości tego sygnału. Jak to wygląda na obrazkach można zobaczyć tutaj: http://arduino.cc/en/Tutorial/PWM. Na płytce STM32F4Discovery mamy dostępne 8 wyjść PWM:

PWMChannel.PWM_0: PD12 //GREEN LED
PWMChannel.PWM_1: PD13 //ORANGE LED

PWMChannel.PWM_2: PD14 //RED LED
PWMChannel.PWM_3: PD15 //BLUE LED
PWMChannel.PWM_4: PE9
PWMChannel.PWM_5: PE11
PWMChannel.PWM_6: PE13
PWMChannel.PWM_7: PE14


Najlepsze jest to, że pierwsze cztery wyjścia kanałów PWM to diody znajdujące się na płytce, więc aby zobaczyć jakieś efekty nic nie musimy podłączać.

Częstotliwość sygnału i jego wypełnienie możemy regulować na dwa sposoby: ustawiając częstotliwość (właściwość Frequency) i procentowe wypełnienie (właściwość DutyCycle) lub czas jednego okresu sygnału (właściwość Period) i czas trwania stanu wysokiego (właściwość Duration). Tutaj mała uwaga. Ustawiając DytyCycle wpisujemy procenty w postaci ułamka czyli jeśli chcemy 21% to ustawiamy 0.21. Natomiast ustawiając Period i Duration jednostkę wskazujemy we właściwości Scale (nano, mili, mikro sekundy). Przykład - dioda zielona mruga raz na sekundę:

var pwm0 = new PWM(Cpu.PWMChannel.PWM_0, 1, 0.5, false);
pwm0.Start();

No dobra, a czy można równocześnie ustawić czerwoną diodę żeby mrugała 2 razy szybciej? Chyba nic prostszego:

var pwm0 = new PWM(Cpu.PWMChannel.PWM_0, 1, 0.5, false);
pwm0.Start();

var pwm2 = new PWM(Cpu.PWMChannel.PWM_2, 2, 0.5, false);
pwm2.Start();

I co? Kicha! Zielona zaczęła mrugać tak samo jak czerwona. Popatrzymy do pliku platform_selector.h w katalogu Solutions\Discovery4 PK:

#define STM32F4_PWM_TIMER {4,4,4,4,1,1,1,1}
#define STM32F4_PWM_CHNL  {0,1,2,3,0,1,2,3}
#define STM32F4_PWM_PINS  {60,61,62,63,73,75,77,78} // D12-D15,E9,E11,E13,E14

Okazuje się, że pierwsze 4 wyjścia PWM obsługuje ten sam timer (cztery kolejne zresztą też, ale inny). Jeśli jest to ten sam timer to wszystkie wyjścia PWM działają z takim samym okresem. Przestawienie okresu (nie ważne czy robi się to przez właściwość Period czy Frequency) na inną wartość dla jednego wyjścia będzie miało wpływ na pozostałe kanały. Dodatkowo trzeba pamiętać, że DutyCycle nie pozostanie stałe przy zmianie częstotliwości. Czyli jeśli chcemy, aby wypełnienie było stałe na poziomie 50% to zmieniając częstotliwość trzeba równocześnie przypisać wartość 0.5 do DutyCycle. Trzeba o tym pamiętać szczególnie w pętlach, gdzie zmieniana jest częstotliwość lub okres. (http://netmf.codeplex.com/workitem/1749)


Jednym z zastosowań PWM jest regulacja jasności świecenia diody LED. Małe wypełnienie - dioda słabo świeci, duże wypełnienie - doda jasno świeci. Może to być wykorzystane do regulacji podświetlenia w wyświetlaczach. Prosty programik, który rozjaśnia i ściemnia diodę czerwoną:

public static void Main()
{
    var pwm2 = new PWM(Cpu.PWMChannel.PWM_2, 300, 0, false);
    pwm2.Start();

    const int minBright = 0;
    const int maxBright = 100;
    int bright = minBright;
    int step = 1;

    while (true)
    {
        pwm2.DutyCycle = ToDutyCycle(bright);

        bright += step;
        if (bright > maxBright || bright < minBright)
        {
            bright = bright > maxBright ? maxBright : minBright;
            step = -step;

            Thread.Sleep(1000);
        }

        Thread.Sleep(40);
    }
}

private static double ToDutyCycle(double brightness)
{
    return brightness/100;
}

Jak widać kot nie jest skomplikowany. Przelatujemy w pętli od 0 do 100 (to jasność) i przypisujemy wprost proporcjonalnie wypełnienie (dzielimy przez 100, aby otrzymać wartości w procentach: 0..1). Migotanie diody zostało zniwelowane poprzez ustawienie częstotliwości na 300 Hz tak, aby wykorzystać bezwładność oka ludzkiego. Przy mniejszych wartościach niektórzy ludzie mogą dostrzegać migotanie. Program jednak nie działa tak jak trzeba. Dioda bardzo szybko się rozjaśnia i bardzo jasno świeci przez większość czasu. Winę za to ponosi nieliniowa charakterystyka oka ludzkiego. Dlatego musimy skorygować wypełnienie.

Spotkałem dwie "szkoły" takiej korekcji. Jedna używa funkcji wykładniczej o podstawie e (podstawa logarytmu naturalnego), a druga funkcji wykładniczej o podstawie 10 (podstawa logarytmu dziesiętnego). Gdzieś wyczytałem, że oko ludzkie ma charakterystykę logarytmiczną o podstawie 10 więc druga może jest bardziej adekwatna. W praktyce moje oko nie zauważyło różnicy między nimi. Wykres poniżej przedstawia obie funkcje:
pwm brightness dimming curve
A tak trzeba je zapisać w kocie:
private static double ToDutyCycleExp(double brightness)
{
    if (brightness < 1)
        return 0;

    if (brightness > 99)
        return 1;

    return (0.383*System.Math.Exp(0.0555*brightness) - 0.196)/100;
}

private static double ToDutyCycle10(double brightness)
{
    if (brightness < 1)
        return 0;

    if (brightness > 99)
        return 1;

    return System.Math.Pow(10, (2.55*brightness - 1)/84.33 - 1)/100;
}

20.04.2013

Źródła PK bez instalacji MSI

Kod budzi się po zimowym letargu. Wiosna pozytywnie wpływa na chęci, a i czasu jest trochę więcej. Na początek coś prostego dla wprawy.

Często zachodzi potrzeba użycia czystych źródeł z różnych wersji PK. Jak je uzyskać z oryginalnych instalatorów, ale bez instalacji? Uruchamiamy msiexec w trybie instalacji administracyjnej na przykład poleceniem:

msiexec /a plik.msi /qb TARGETDIR=katalog

Przykład. Aby uzyskać źródła Porting Kit 4.2 z pliku MicroFrameworkPK.msi wykonujemy polecenie:

msiexec /a MicroFrameworkPK.MSI /qb TARGETDIR=c:\

Pliki zostaną wypakowane do katalogu c:\MicroFrameworkPK_v4_2