6.08.2017

Protokół portu szeregowego (RS232): wymagania

Już kiedyś pisałem o porcie szeregowym przy okazji modułu bluetooth HC-05. W tamtym wpisie w bardzo prosty sposób (wysyłanie literek r, g, b, o) sterowałem diodami LED. Jednak dla bardziej zaawansowanych czynności taki "protokół" się nie sprawdzi. Na przykład w pierwszej linii wyświetlacza LCD trzeba wyświetlić tekst? Jak To zrobić? Co wysłać? Jak odebrać? W tym i następnych wpisach, spróbuję przekazać jak sobie w takiej sytuacji poradzić.

Przez ostatnie kilka lat naoglądałem się różnych protokołów. Jeśli nie chcecie wymyślać koła do wozu to polecam stronę Embedded Systems/Common Protocols. Może akurat któryś się nada. Szczególnie należy sprawdzić protokół Modbus. Chociaż uważam, że nie jest on najlepszy, bo detekcja ramek bazuje na czasach przerwy w transmisji (RTU). Jeśli jednak już sami będziemy wymyślać protokół, to według mnie należy przestrzegać następujących reguł:

  •  (ten punkt miał być na końcu, ale przeniosłem go na początek, bo jest najważniejszy)
    w dokumentacji protokołu, dajemy przykłady całych ramek, bajt po bajcie. Jeśli ramki występują w sekwencji, to dajmy przykłady bajt po bajcie całych sekwencji. Taka dokumentacja pomoże w napisaniu testów jednostkowych
  • ramka transmisyjna powinna zaczynać się specjalnym znacznikiem początku, a kończyć specjalnym znacznikiem końca
  • jeśli ramka nie jest stałej długości powinna zawierać informację o długości - alternatywą jest byte stuffing/escaping, ale ja osobiście bym tego unikał.
  • w systemach gdzie niedozwolone jest przekłamanie w danych polecenia (prawie wszędzie), ramka powinna zawierać CRC, a w pozostałych przypadkach (zabawy) można dodać sumę kontrolną
  • ramka powinna być jak najprostsza i jak najkrótsza
  • powinniśmy unikać konstrukcji "user frendly" np. przesyłania komend czy parametrów tekstem tylko po to, aby ramka ładnie się wyświetlała w konsoli podczas debugowania
  • jeśli przesyłać będziemy znaczne ilości danych np. obrazy BMP powinniśmy zadbać o kompresję np. RLE
  • każda komenda powinna być zakończona odpowiedzią, nawet jeśli komenda jest błędna lub nieobsługiwana
  • jeśli komenda przestawia jakąś wartość, to powinna też być komenda, która tą wartość pobiera
  • należy unikać komend, które przełączają; lepszym rozwiązaniem jest włączanie i wyłączanie

20.07.2017

Watchdog IWDG w TinyCLR OS

Dzięki klasie Marshal można pisać bardziej zaawansowane funkcje. Na przykład implementacja watchdoga IWDG może wyglądać tak: 
public class WatchDog
{
    public static bool LastReboot
    {
        get
        {
            var rccAddr = new IntPtr(0x40023800);
            int rccCsrValue = Marshal.ReadInt32(rccAddr, 0x74);
            return IsIwdgRstf(rccCsrValue);
        }
    }

    public static void Start(TimeSpan period)
    {
        ResetLastReboot();
        SetTimings(period);
        WriteIwdgKr(0xCCCC);
    }

    public static void Reset()
    {
        WriteIwdgKr(0xAAAA);
    }

    private static void ResetLastReboot()
    {
        var rccAddr = new IntPtr(0x40023800);
        int rccCsrValue = Marshal.ReadInt32(rccAddr, 0x74);

        if (IsIwdgRstf(rccCsrValue))
        {
            const int rmvfMask = 0x01000000;
            rccCsrValue = rccCsrValue | rmvfMask;
            Marshal.WriteInt32(rccAddr, 0x74, rccCsrValue);
        }
    }

    private static void WriteIwdgKr(int value)
    {
        Marshal.WriteInt32(new IntPtr(0x40003000), value);
    }

    private static bool IsIwdgRstf(int rccCsrValue)
    {
        const int iwdgRstfMask = 0x20000000;
        return (rccCsrValue & iwdgRstfMask) > 0;
    }

    private static void SetTimings(TimeSpan period)
    {
        const int kHzLsi = 32000;

        long usPeriod = ((period.Ticks * 1000) / TimeSpan.TicksPerMillisecond);
        int[] dividers = { 4, 8, 16, 32, 64, 128, 256 };
        for (int i = 0; i < dividers.Length; i++)
        {
            int usMin = (dividers[i] * 1000 * 1000) / kHzLsi;
            if (usPeriod >= usMin)
            {
                int counter = (int)(usPeriod / usMin - 1);
                if (counter < 0 || counter > 0xFFF)
                    continue;

                SetIwdgPrAndRlr(i, counter);
                return;
            }
        }

        throw new InvalidOperationException("Invalid period (0.125..32768 ms).");
    }

    private static void SetIwdgPrAndRlr(int prValue, int rlrValue)
    {
        var iwdgKrAddr = new IntPtr(0x40003000);
        Marshal.WriteInt32(iwdgKrAddr, 0x5555);
        Marshal.WriteInt32(iwdgKrAddr, 0x04, prValue);
        Marshal.WriteInt32(iwdgKrAddr, 0x08, rlrValue);
    }
}

17.07.2017

System.Runtime.InteropServices.Marshal w TinyCLR OS

Bardzo przydatną klasą w TinyCLR OS jest System.Runtime.InteropServices.Marshal. Dzięki metodom z tej klasy uzyskamy dostęp do komórek pamięci. Między innymi rejestry procesora to też komórki pamięci, więc będziemy mogli bezpośrednio je zapisywać lub odczytywać.

Adresy rejestrów zaczynają się od wartości 0x4000 0000. W jednym z wcześniejszych wpisów na temat interop był pobierany unikatowy identyfikator procesora. Ten identyfikator przechowywany jest właśnie w jednym z rejestrów i składa się z trzech 32-bitowych komórek. Rejestr zaczyna się od adresu 0x1FFF 7A10. Jak więc uzyskać unikatowy identyfikator? Nic prostszego!

public static string GetDeviceGuid()
{
    var uidAddr = new IntPtr(0x1FFF7A10);
    int uid0 = Marshal.ReadInt32(uidAddr, 0);
    int uid1 = Marshal.ReadInt32(uidAddr, 0x04);
    int uid2 = Marshal.ReadInt32(uidAddr, 0x08);

    string result = uid0.ToString("X8")
                    + "-" + (uid1 >> 16).ToString("X4")
                    + "-" + (uid1 & 0xFFFF).ToString("X4")
                    + "-" + "0000"
                    + "-" + uid2.ToString("X8") + "0000";

    return result;
}

10.07.2017

TinyCLR OS od GHI Electronics

Od jakiegoś czasu na stronie GHI Electronics pojawiały się wzmianki o tym, że pracują nad nowym rozwiązaniem w stylu .NET Micro Framework. Nazwali to TinyCLR OS. Natomiast kilka dni temu pojawiła się informacja o wypuszczeniu wersji Preview 5 oraz o udostępnieniu kodu, który pozwala wygenerować TinyCLR OS na dowolną płytkę z procesorem STMF4. Żart? Nie!

To działa!

Bez problemu wygenerowałem dla poczciwej płytki STM32F4Discovery TinyCLR OS!. Bez problemu zrobiłem projekt w Visual Studio 2017 i bez problemu uruchomiłem prosty program z mrugającą diodą! Wszystko się debuguje, restartuje, przerywa itp. Bez najmniejszego problemu. Przygotowanie portu zajęło mi 15 minut, a kompilacja 5 sekund! TinyCLR OS będzie sukcesywnie rozwijany i uzupełniany o nowe biblioteki i peryferia.

Poniżej kilka przydatnych linków:
Przy rozpoczynaniu pracy trzeba się trzymać dokumentacji. Aha. Trzeba pamiętać o wgraniu sterownika USB, bo w dokumentacji nie ma o tym wzmianki.

TinyCLR OS Library trzeba podłączyć jako repozytorium lokalne NuGeta np.w ten sposób: How to Install a local sources NuGet package or a Prerelease package in Visual Studio

29.12.2015

Wyświetlacz LCD HD44780 na I2C

Wyświetlacze LCD zgodne z HD44780 niewielkim kosztem można przystosować do sterowania poprzez interfejs I2C. Uprości się wówczas sposób podłączania takiego wyświetlacza do układu. Zamiast sześciu linii GPIO wystarczy dwa przewody magistrali I2C plus zasilanie. Taki moduł można za niewielkie pieniądze zakupić i samemu przylutować do LCD lub od razu kupić wyświetlacz z wlutowanym adapterem. Adaptery przeważnie zbudowane są na układzie PCF8574. Najczęściej spotykane są takie adaptery jak na obrazku poniżej:

I2C LCD module
A tutaj adapter przylutowany do wyświetlacza LCD:

Uwaga! Inne adaptery (inaczej wyglądające) lub z innym  układem niż PCF8574T (na przykład z PCF8574TA) mogą mieć inaczej podłączone wyprowadzenia lub inny adres na magistrali I2C niż 0x27. Adapter do STM32F4Discovery (port dla NET MF 4.2) podłączamy następująco:

GND  -  GND
VCC  -  +5V
SDA  -  PB9
SCL  -  PB6

Teraz wystarczy tylko napisać nowy sterownik do biblioteki μLiquidCrystal i po kłopocie. Musimy zacząć od tego, jak jest podłączony układ PCF8574 do wyświetlacza. Dla adaptera takiego jak powyżej bity wartości wysłanej na interfejs I2C mają następujące znaczenie: 

                  (MSB) P7 P6 P5 P4 P3 P2 P1 P0 (LSB)
                        |  |  |  |   |  |  |  |
                   D7 __|  |  |  |   |  |  |  |__ RS (0 - cmd, 1 - data)
                   D6 _____|  |  |   |  |  |_____ RW (0 - write, 1 - read)
                   D5 ________|  |   |  |________ E  (falling edge)
                   D4 ___________|   |___________ Backlight (on/off)


Cała robota sprowadza się więc do napisania procedury, która będzie odpowiednio ustawiać poszczególne bity w wartości wysyłanej na interfejs I2C. Dodatkową komplikacją jest tryb 4 bitowej pracy wyświetlacza: najpierw trzeba wysłać 4 starsze bity wartości, a potem młodsze. Implementacja sterownika może wyglądać tak:

using MicroLiquidCrystal;
using Microsoft.SPOT.Hardware;

public class Pcf8574 : ILcdTransferProvider
{
    private readonly I2CDevice _device;
    private readonly I2CDevice.Configuration _config;
    private readonly I2CDevice.I2CTransaction[] _transactions;

    public bool FourBitMode
    {
        get { return true; }
    }

    public Pcf8574(I2CDevice device, ushort address = 0x27)
    {
        _device = device;
        _config = new I2CDevice.Configuration(address, 100);

        I2CDevice.I2CTransaction tran = I2CDevice.CreateWriteTransaction(new byte[1]);
        _transactions = new[] {tran};
    }

    public void Send(byte data, bool mode, bool backlight)
    {
        //Hi 4 bits
        int send = data & 0xF0;

        if (backlight)
            send |= 0x08; //P3

        if (mode)
            send |= 0x01; //P0

        Send((byte) (send | 0x04)); //E = 1
        Send((byte) send); //E = 0

        //Lo 4 bits + backlight + mode
        send = (data << 4) | (send & 0x0F);
        Send((byte) (send | 0x04)); //E = 1
        Send((byte) send); //E = 0
    }

    private void Send(byte data)
    {
        lock (_device)
        {
            _device.Config = _config;
            _transactions[0].Buffer[0] = data;
            _device.Execute(_transactions, 100);
        }
    }
}

Może wyjaśnienia troszkę wymaga konstruktor oraz druga procedura Send. W konstruktorze tworzymy i zapamiętujemy transakcję dla I2C tak, aby nie tworzyć jej za każdym razem. W procedurze Send przypisujemy bezpośrednio wartość do bufora tej transakcji. Dodatkowo w tej procedurze blokujemy I2CDevice i ponownie przypisujemy konfigurację. Pozwala to na współdzielenie magistrali I2C przez inne peryferia. Jeśli zawsze będziemy się trzymali takiego wzorca, żadne dodatkowe opakowania dla I2CDevice nie będą potrzebne. Poniżej przykład użycia:

using (I2CDevice device = new I2CDevice(null))
{
    Pcf8574 provider = new Pcf8574(device);

    Lcd lcd = new Lcd(provider);
    lcd.Begin(16, 2);

    lcd.Write("ABCDEFGHIJKLMNOP");
    lcd.SetCursorPosition(0, 1);
    lcd.Write("abcdefghijklmnop");
}


15.08.2015

Więcej interop: watchdog (IWDG)

Nasz mikrokontroler ma wbudowany system zabezpieczający przed zawieszeniem systemu. Nazywa się to watchdog. Właściwie to ma dwie takie funkcje: IWDG - independent watchdog i WWDG - window watchdog. Znacznie prostszy w użyciu i adekwatny do nieprzewidywalnego czasu działania NET MF jest IWDG. Spróbujemy zatem zaimplementować obsługę takiego watchdoga. W watchdogu chodzi o to, aby cyklicznie co jakiś czas poinformować go, że nasz program działa poprawnie. W przypadku, gdy watchdog nie dostanie takiego sygnału to mikrokontroler samoczynnie się zrestartuje. Nasza implementacja będzie bardzo zbliżona do tej na stronie stm32f4-discovery.com: Library 20- Independent watchdog timer on STM32F4.

A więc do dzieła. Do naszego projektu STM32F4Helper dodajemy nowy plik: Watchdog.cs. Statyczna klasa Watchdog będzie miała dwie funkcje: Start i Reset. Nasz mikrokontroler ma jeszcze jedną dodatkowa informację, która może być bardzo użyteczna: ostatni reset wykonał watchdog. Tą informację zwrócimy przez statyczną właściwość: LastReset. Tak więc cały "kadłubek" klasy wygląda tak:

namespace STM32F4Helper
{
    public static class Watchdog
    {
        public static bool LastReset { get { throw new NotImplementedException(); } } 

        public static void Start(TimeSpan period)
        {
            throw new NotImplementedException();
        }

        public static void Reset()
        {
            throw new NotImplementedException();
        }
    }
}

No dobra uzupełniamy funkcje. Aha. W dokumentacji zobaczymy, że do poprawnego działania watchdoga musimy odpowiednio ustawić rejestry prescallera (IWDG_PR) i licznika (IWDG_RLR). Te wartości obliczymy po stronie kodu zarządzanego na podstawie parametru period metody Start. Jeśli nie popełniłem żadnego błędu to cała klasa powinna wyglądać tak:

namespace STM32F4Helper
{
    public static class Watchdog
    {
        public static bool LastReset { get { return LastResetIwdg(); }  } 

        public static void Start(TimeSpan period)
        {
            SetTimings(period);
            StartIwdg();
        }

        public static void Reset()
        {
            ResetIwdg();
        }

        private static void SetTimings(TimeSpan period)
        {
            const int kHzLsi = 32000;

            long usPeriod = (period.Ticks * 1000) / TimeSpan.TicksPerMillisecond;
            int[] dividers = { 4, 8, 16, 32, 64, 128, 256 };
            for (int i = 0; i < dividers.Length; i++)
            {
                int usMin = (dividers[i] * 1000 * 1000) / kHzLsi;
                if (usPeriod >= usMin)
                {
                    long counter = usPeriod / usMin - 1;
                    if (counter < 0 || counter > 0xFFF)
                        continue;

                    SetupIwdg(i, (int)counter);
                    return;
                }
            }

            throw new InvalidOperationException("Invalid period (0.125..32768 ms).");
        }

        [MethodImpl(MethodImplOptions.InternalCall)]
        private static extern void SetupIwdg(int divider, int counter);

        [MethodImpl(MethodImplOptions.InternalCall)]
        private static extern void StartIwdg();

        [MethodImpl(MethodImplOptions.InternalCall)]
        private static extern void ResetIwdg();

        [MethodImpl(MethodImplOptions.InternalCall)]
        private static extern bool LastResetIwdg();
    }
}

Teraz standardowo generujemy pliki po stronie native. I uzupełniamy funkcje w pliku *_Watchdog.cpp. Oczywiście zmiany z innych plików (dotNetMF.proj, *_Native.cpp, *_Native.h) musimy odpowiednio przenieść do już istniejącej biblioteki. Funkcje po stronie native będą wyglądały tak:

#include "STM32F4Helper_Native.h"
#include "STM32F4Helper_Native_STM32F4Helper_Watchdog.h"

using namespace STM32F4Helper;

void Watchdog::SetupIwdg( INT32 param0, INT32 param1, HRESULT &hr )
{
  IWDG->KR = 0x5555;
  IWDG->PR = param0;
  IWDG->RLR = param1;
  
  RCC->CSR |= RCC_CSR_RMVF;
}

void Watchdog::StartIwdg( HRESULT &hr )
{
  IWDG->KR = 0xCCCC;
  IWDG->KR = 0xAAAA;
}

void Watchdog::ResetIwdg( HRESULT &hr )
{
  IWDG->KR = 0xAAAA;
}

INT8 Watchdog::LastResetIwdg( HRESULT &hr )
{
    INT8 retVal = 0;
    
    if (RCC->CSR & RCC_CSR_WDGRSTF)
       retVal = 1;
     
    return retVal;
}

I już. Zostaje tylko skompilowanie solucji, wgranie przez MFDeploy obrazów hex i przetestowanie. Na przykład takim programem. Diody informują po restarcie jaki był powód: czerwona - watchdog, zielona - zasilanie. Naciśnięcie przycisku wymusza blokadę programu, tak aby zadziałał watchdog.

public class Program
{
    public static void Main()
    {
        OutputPort redLed = new OutputPort(Stm32F4Discovery.LedPins.Red, false);
        OutputPort greenLed = new OutputPort(Stm32F4Discovery.LedPins.Green, false);
        OutputPort blueLed = new OutputPort(Stm32F4Discovery.LedPins.Blue, false);
        InputPort button = new InputPort(Stm32F4Discovery.ButtonPins.User, false, Port.ResistorMode.PullDown);

        OutputPort led = STM32F4Helper.Watchdog.LastReset ? redLed : greenLed;
        led.Write(true);
        Thread.Sleep(1000);
        led.Write(false);

        STM32F4Helper.Watchdog.Start(new TimeSpan(0, 0, 0, 5, 0));
            
        for (;;)
        {
            STM32F4Helper.Watchdog.Reset();

            Thread.Sleep(500);
            blueLed.Write(!blueLed.Read());
                
            if (button.Read())
            {
                while (button.Read())
                {
                }

                while (true)
                {
                    blueLed.Write(!blueLed.Read());
                    Thread.Sleep(100);

                    if (button.Read())
                    {
                        while (button.Read())
                        {
                        }

                        blueLed.Write(false);
                        break;
                    }
                }
            }
        }
    }
}

6.08.2015

Serwo Tower Pro SG90

Tower Pro SG-90 to małe, lekkie i przede wszystkim tanie, uniwersalne serwo. Bez problemu da się podłączyć do STM32F4Discovery. Urządzenie ma 3 przewody połączeniowe: brązowy, czerwony i żółty. Brązowy podłączamy do masy (GND), czerwony do zasilania (+5V), a żółty do wyjścia PWM na płytce. Ja wybrałem wyjście PWMChannel.PWM_0 (czyli pin PD12 - zielona dioda LED).
Tower Pro SG90

Sterowanie kątem obrotu odbywa się poprzez zmianę szerokości impulsu na wejściu PWM. Według dokumentacji częstotliwość sygnału sterującego powinna wynosić 50Hz (okres 20ms), natomiast szerokość impulsu dla wychyleń -90..+90 stopni powinna zawierać się w przedziale: 1ms..2ms. Po zbadaniu skrajnych położeń orczyka wyszło mi, że szerokość impulsu powinna być z przedziału 0.53ms..2.35ms. Do takiego sterowania nadaje się drugi sposób regulacji parametrów PWM: zmiana właściwości Period (czas jednego okresu sygnału) oraz Duration (czas trwania stanu wysokiego). Poniżej kot, który wychyla orczyk do skrajnych położeń:

public static void Main()
{
    const uint period = 20000;    //20ms in us
    const uint minDuration = 530; //0.53ms in us
    const uint maxDuration = 2350;//2.35ms in us
    const uint midDuration = minDuration + (maxDuration - minDuration)/2;

    var servo = new PWM(Cpu.PWMChannel.PWM_0, period, minDuration,
                        PWM.ScaleFactor.Microseconds, false);
    servo.Start();

    for (;;)
    {
        servo.Duration = minDuration;
        Thread.Sleep(2000);

        servo.Duration = midDuration;
        Thread.Sleep(2000);

        servo.Duration = maxDuration;
        Thread.Sleep(2000);

        servo.Duration = midDuration;
        Thread.Sleep(2000);
    }
}

Jeśli byśmy chcieli sterować kątem obrotu podając stopnie, to przyda się jedna bardzo fajna funkcja z Arduino. Funkcja "map":

static uint Map(uint x, uint minX, uint maxX, uint outMin, uint outMax)
{
    return (x - minX)*(outMax - outMin)/(maxX - minX) + outMin;
}

A użyć jej możemy tak (orczyk zmienia położenie co 30 stopni):

public static void Main()
{
    const uint period = 20000; //20ms in us
    const uint minDuration = 530; //0.53ms in us
    const uint maxDuration = 2350; //2.35ms in us

    var servo = new PWM(Cpu.PWMChannel.PWM_0, period, minDuration,
                        PWM.ScaleFactor.Microseconds, false);
    servo.Start();

    uint step = 30;
    for (uint angle = 0;; angle += step)
    {
        if (angle > 180)
        {
            step = (uint) -step;
            continue;
        }

        servo.Duration = Map(angle, 0, 180, minDuration, maxDuration);
        Thread.Sleep(2000);
    }
}

4.08.2015

Bliskie spotkanie z .NET Micro Framework 4.4

Jakiś czas temu dla wersji NET MF 4.4 (4.4 Beta 2 is here!) pojawiła się solucja dla STM32F4Discovery. Postanowiłem więc skompilować SDK oraz solucję. Może się komuś przyda. Pliki dostępne są w repozytorium: MicroFrameworkSDK i obrazów hex. Najpierw zainstalować trzeba SDK z pliku MicroFrameworkSDK.msi, a następnie rozszerzenie vsix dla odpowiedniej wersji Visual Studio. Pliki hex wgrywamy standardowo poprzez MFDeploy. Instalacja przebiega gładko. Natomiast później można napotkać pewne problemy, które opiszę poniżej.

Na systemie Windows 8 i Visual Studio 2015 nie powinno być większych problemów. Schody mogą się pojawić na wersji Windows 7 i Visual Studio 2013.

Problem 1: sterownik USB.
W tej wersji NET MF nie są wymagane jakieś specjalne sterowniki. System sam powinien wykryć i zainstalować sterownik WinUSB dla STM32F4Discovery. Czasami może się automatycznie podpiąć jakiś inny sterownik. Wówczas trzeba go usunąć i pozwolić systemowi na zainstalowanie domyślnego. Jak ma to poprawnie wyglądać jest poniżej:

Windows 8Windows 7

W pewnych przypadkach na Windows 7 sterowniki nie zostaną zainstalowane automatycznie. Trzeba wówczas samemu pobrać sterownik i zainstalować ręcznie. Sterownik Microsoft - Other hardware - WinUsb Device można pobrać ze strony http://catalog.update.microsoft.com (tylko Internet Explorer).

Problem 2: MetaDataProcessor exited with code -1073741515.
Jeśli używamy tylko Visual Studio 2013 (nie mamy zainstalowanego Visual Studio 2015), to prawdopodobnie, przy pierwszej kompilacji, dostaniemy komunikat:


Ręczne uruchomienie MetaDataProcessor.exe pozwoli na uzyskanie dokładniejszej informacji o błędzie: The program can't start because api-ms-win-crt-runtime-l1-1-0.dll is missing. Musimy zainstalować Visual C++ Redistributable for Visual Studio 2015.

Problem 3: zmienione piny.
W solucji zostały zmienione niektóre piny w stosunku do poprzedniej wersji. Różnice poniżej:

I2C pins: scl=PB8 sda=PB9

Brak PWMChannel4 .. PWMChannel7

AnalogChannel0: pin=PA0
AnalogChannel1: pin=PA1
AnalogChannel2: pin=PA2
AnalogChannel3: pin=PA3
AnalogChannel4: pin=PF6
AnalogChannel5: pin=PF7
AnalogChannel6: pin=PF8
AnalogChannel7: pin=PF9
AnalogChannel8: pin=PF10
AnalogChannel9: pin=PF3
AnalogChannel10: pin=PC0
AnalogChannel11: pin=PC1
AnalogChannel12: pin=PC2
AnalogChannel13: pin=PC3
AnalogChannel14: pin=PF4
AnalogChannel15: pin=PF5

COM1: (rx, tx, cts, rts)=(PB7, PB6, PP15, PP15)
COM2: (rx, tx, cts, rts)=(PD6, PD5, PD3, PD4)
COM3: (rx, tx, cts, rts)=(PC11, PC10, PD11, PD12)
COM4: (rx, tx, cts, rts)=(GPIO_NONE, GPIO_NONE, GPIO_NONE, GPIO_NONE)
COM5: (rx, tx, cts, rts)=(GPIO_NONE, GPIO_NONE, GPIO_NONE, GPIO_NONE)
COM6: (rx, tx, cts, rts)=(GPIO_NONE, GPIO_NONE, GPIO_NONE, GPIO_NONE)

To wszystko. Możemy spróbować sił z NET MF 4.4. Jeśli pojawią się jakieś poprawki będę próbował na bieżąco kompilować i SDK i solucję.

17.05.2015

Dodawanie nowych funkcji (Interop w .NET Micro framework))

Przychodzi taki czas, gdy czegoś nam w .NET MF brakuje. Potrzebna nam jest jakaś funkcja, a jej nie ma. Jeśli to prosta funkcja, bardzo łatwo sami możemy rozszerzyć nasz system. Jedynym wymaganiem jest umiejętność kompilacji naszej solucji. Bardzo przystępny przewodnik, o tworzeniu takiej dodatkowej biblioteki, znajduje się na stronie: Using Interop in the .NET Micro Framework. Bazując na tym przykładzie zbudujemy własny interop.

Na pewno przyda się jakaś dokumentacja mikrokontrolera STM32F407, bo taki ma nasza płytka:
W pierwszym dokumencie wyczytamy (w punkcie 39.1), że każdy mikrokontroler posiada, zapisany w pamięci, unikalny identyfikator. Taki identyfikator możemy użyć np. przy komunikacji z serwerem do identyfikacji urządzenia. Spróbujemy więc dodać możliwość odczytania tego identyfikatora. Unikalny id znajduje się pod adresem 0x1FFF7A10 i składa się z 96 bitów (12 bajtów). Możemy go zobaczyć w ST-Link:


Tylko jak ten identyfikator odczytać? Na przykład tak: Reading the STM32 unique device ID in C. Wystarczy przenieść ten przykład do naszej solucji. Ale po kolei.

Musimy utworzyć nowy projekt, który będzie nasza biblioteką. Ja projekt nazwałem STM32F4Helper i dodałem jedną klasę: Device. Ta klasa będzie miała jedną statyczna metodę: GetGuid. W tej procedurze zamienimy liczby (pokazane na obrazku) w guid. Od razu trzeba też zaplanować funkcję zewnętrzną (GetUuid32), którą pobierzemy te wartości z pamięci. Cała klasa może wyglądać tak:

using System.Runtime.CompilerServices;

namespace STM32F4Helper
{
    public static class Device
    {
        public static string GetGuid()
        {
            var buffer = new uint[3];
            GetUuid32(buffer);
            string result = buffer[0].ToString("X8")
                            + "-" + (buffer[1] >> 16).ToString("X4")
                            + "-" + (buffer[1] & 0xFFFF).ToString("X4")
                            + "-" + "0000"
                            + "-" + buffer[2].ToString("X8") + "0000";
            return result;
        }

        [MethodImplAttribute(MethodImplOptions.InternalCall)]
        private static extern void GetUuid32(uint[] buffer);
    }
}

Ustawiamy w projekcie opcję generowania native stub i kompilujemy projekt.



We wskazanym katalogu powstanie cześć natywna naszej biblioteki. Nie trzeba się przejmować ilością tych plików. Tak naprawdę, dla prostych funkcji, trzeba uzupełnić tylko plik *Device.cpp (świadomie użyłem  tutaj gwiazdki, aby pokazać, że nazwa pliku wcale nie jest taka skomplikowana, tylko trzeba odpowiednio patrzeć). Pliki przenosimy do katalogu solucji, aby powstała taka struktura:


W katalogu managed można sobie umieścić projekt części zarządzanej biblioteki lub wynik jej kompilacji czyli pliki z katalogu bin\debug lub bin\release. W docelowym projekcie będziemy mogli wówczas dodawać referencję tylko do dll, a nie cały projekt (ale o tym kiedy indziej). Musimy mieć na uwadze to, że jeśli do biblioteki dodamy nowe funkcje lub klasy, to trzeba ponownie wygenerować część natywną.

Przed kompilacją umiejętnie trzeba zmienić niektóre pliki, no i uzupełnić funkcję. Najpierw plik STM32F4Helper.featureproj.  Możemy uzupełnić element description i musimy zmienić ścieżkę w elemencie RequiredProjects na taką:

<!--MMP_DAT_CreateDatabase Include="$(SPOCLIENT)\Solutions\Discovery4\Libraries\STM32F4Helper\Managed\$(ENDIANNESS)\STM32F4Helper.pe" /-->
<RequiredProjects Include="$(SPOCLIENT)\Solutions\Discovery4\Libraries\STM32F4Helper\Native\dotnetmf.proj" />
Dodatkowo komentujemy lub usuwamy z tego pliku element MMP_DAT_CreateDatabase. Dopóki do docelowego programu będziemy dodawać referencję do całego projektu to tej linii nie potrzebujemy.

Dokładamy też od razu naszą bibliotekę do TinyCLR.proj w naszej solucji. Do grupy featureproj dodajemy STM32F4Helper.featureproj:

<Import Project="$(SPOCLIENT)\Solutions\Discovery4\Libraries\STM32F4Helper\Native\STM32F4Helper.featureproj" />

Poniżej całego featureproj dodajemy pozycję z driverlibs:

<ItemGroup>
  <DriverLibs Include="STM32F4Helper.$(LIB_EXT)" />
  <RequiredProjects Include="$(SPOCLIENT)\Solutions\Discovery4\Libraries\STM32F4Helper\Native\dotNetMF.proj" />
</ItemGroup>

Pozostało tylko uzupełnić naszą natywną funkcję. Jak widać nie jest zbyt skomplikowana:

#include <stdint.h>
#include "STM32F4Helper_Native.h"
#include "STM32F4Helper_Native_STM32F4Helper_Device.h"

#define STM32_UUID ((uint32_t *)0x1FFF7A10)

using namespace STM32F4Helper;

void Device::GetUuid32( CLR_RT_TypedArray_UINT32 param0, HRESULT &hr )
{
  param0[0] = STM32_UUID[0];
  param0[1] = STM32_UUID[1];
  param0[2] = STM32_UUID[2];
}

Teraz trzeba tylko skompilować solucję (TinyCLR.proj) i wgrać na płytkę przy pomocy MFDeploy. W programie można teraz użyć takiej funkcji:

using Microsoft.SPOT;
using STM32F4Helper;

namespace DemoInterop
{
    public class Program
    {
        public static void Main()
        {
            string guid = Device.GetGuid();
            Debug.Print("Guid: " + guid);
        }
    }
}
 

A wynik działania jest taki:
Guid: 002B001C-3231-4713-0000-383031370000

26.04.2015

.NET Micro framework w projektach komeryjnych

Czy .NET Micro Framework da się użyć w projektach komercyjnych? Myślę, że tak. Przy dobrze napisanym programie nigdy nie miałem problemu ze stabilnością czy zawieszaniem się układu. Jednak do celów profesjonalnych chyba użyłbym jednego z modułów od GHI Electronics i ich bibliotek np: G400-S (Flash: 1.4MB, RAM: 92MB) czy G120 (Flash: 2.87MB, RAM: 13.67MB).

Sceptykom "czy da się tego użyć profesjonalnie" polecam dwa linki:

 

2.03.2015

Akcelerometr LIS302DL

Jak już mamy podpięty bluetooth, to aż się prosi, aby wysyłać przez port com jakieś ciekawe dane. Tylko jakie? Na płytce STM32F4Discovery znajduje się akcelerometr LIS302DL. Dzięki niemu możemy uzyskać dane np. o położeniu płytki w przestrzeni (pochylenie i przekręcenie). Wizualizacja takich danych może być bardzo interesującym doświadczeniem.
LIS302DL .NET Micro framework
Ale od początku. Akcelerometr mierzy przyspieszenie. Taki akcelerometr można zobrazować sobie jako kulkę, pośrodku pudełka, zawieszoną z każdej strony na sprężynach. Z akcelerometru otrzymujemy informację o ugięciu sprężyn w osiach x, y i z. Jak to wygląda na obrazkach można pooglądać tutaj: The Accelerometer Tutorial. Na płytce STM32F4Discovery akcelerometr LIS302DL podłączony jest do portu SPI1. Wybranie akcelerometru do komunikacji (chipselect) następuje poprzez pin PE3.

Teraz kot. Najpierw kilka funkcji pomocniczych. Dzięki nim komunikacja z akcelerometrem będzie bardzo prosta.

private byte[] Read(byte startAddress, byte count)
{
    startAddress |= 0x80;
    if (count > 1)
        startAddress |= 0x40;

    var writeBuffer = new byte[1 + count];
    writeBuffer[0] = startAddress;

    var readBuffer = new byte[1 + count];
    _spi.WriteRead(writeBuffer, readBuffer);

    byte[] result = Utility.ExtractRangeFromArray(readBuffer, 1, count);
    return result;
}

private void Write(byte address, byte value)
{
    byte[] writeBuffer = { address, value};
    _spi.Write(writeBuffer);
}

Funkcja Read służy do odczytu danych ze wskazanego rejestru akcelerometru, a Write do zapisu. W funkcji Read zastosowałem pewną właściwość portu SPI do odczytu kolejnych wartości. Cały pomysł sprowadza się do specjalnego przygotowania tablicy writeBuffer. Na pierwszej pozycji tablicy występuje adres rejestru, natomiast na kolejnych pozycjach są zera. Pozycji z zerami jest tyle, ile chcemy odczytać kolejnych bajtów. Taka technika przyda się np. podczas odczytu wartości z rejestrów x, y i z. Zamiast odczytywać je pojedynczo, za jednym razem odczytamy wszystkie, ponieważ rejestry te występują po sobie. Tak więc klasa do obsługi akcelerometru (bez ww funkcji) będzie wyglądała tak:
public class Lis302Dl : IDisposable
{
    private readonly SPI _spi;
    private double _currentSensitivity;

    private const byte WhoAmiReg = 0x0F;
    private const byte CtrlReg1 = 0x20;
    private const byte CtrlReg2 = 0x21;
    private const byte OutXReg = 0x29;

    public enum Scale { Full2K3, Full9K2 }

    public Lis302Dl(SPI.SPI_module spiModule, Cpu.Pin chipSelect)
    {
        var spiCfg = new SPI.Configuration(chipSelect, false,
                                            0, 0, //5,8
                                            true, true,
                                            5000, spiModule);

        _spi = new SPI(spiCfg);

        byte[] whoAmI = Read(WhoAmiReg, 1);
        if(whoAmI[0] != 0x3B)
            throw new InvalidOperationException("LIS302DL not available");

        Write(CtrlReg1, 0x47);
        _currentSensitivity = ToSensitivity(Scale.Full2K3);
    }

    private double ToSensitivity(Scale scale)
    {
        return scale == Scale.Full2K3 ? 0.018 : 0.072;
    }

    public void GetRaw(out sbyte x, out sbyte y, out sbyte z)
    {
        byte[] register = Read(OutXReg, 5);
        x = (sbyte)register[0];
        y = (sbyte)register[2];
        z = (sbyte)register[4];
    }

    public void GetAcc(out double x, out double y, out double z)
    {
        byte[] register = Read(OutXReg, 5);
        x = _currentSensitivity*(sbyte) register[0];
        y = _currentSensitivity*(sbyte) register[2];
        z = _currentSensitivity*(sbyte) register[4];
    }

    public void Dispose()
    {
        _spi.Dispose();
    }
}

Teraz trzeba tylko przygotować krótki programik główny, który pobierze dane z akcelerometru i wyśle na port COM2. Przyspieszenie wysyłane jest przetworzone na m/s^2. Wysyłane są od razu 3 wartości: x, y i z rozdzielone średnikiem.

public static void Main()
{
    const double g = 9.80665;

    var serial = new SerialPort("COM2");
    serial.Open();

    var mems = new Lis302Dl(Stm32F4Discovery.SpiDevices.SPI1,
                            Stm32F4Discovery.Pins.PE3);
    for (;;)
    {
        double x, y, z;
        mems.GetAcc(out x, out y, out z);
                
        x = x*g;
        y = y*g;
        z = z*g;

        string sendStr = x.ToString("F3") + ";" 
                         + y.ToString("F3") + ";" 
                         + z.ToString("F3") + "\r\n";

        byte[] sendBuffer = Encoding.UTF8.GetBytes(sendStr);
        serial.Write(sendBuffer, 0, sendBuffer.Length);
        Thread.Sleep(50);
    }
}

Pozostała wizualizacja danych. Świetnie się do tego celu nadaje program Processing. Jest bardzo prosty w obsłudze i nauce, a możliwości ma przeogromne. Na przykład takim programikiem: ProcessingPlot2D można wyrysować wartości x, y i z na wykresie:


Można też spróbować sił w 3D. Takim programikiem: ProcessingPlot3D uzyskamy coś takiego:


Cały kot programu: DemoLIS302DL.

10.02.2015

Bluetooth HC-05

Za niewielkie pieniądze można kupić moduł bluetooth HC-05. Dzięki niemu STM32F4Discovery otrzyma możliwość komunikacji ze światem zewnętrznym poprzez port szeregowy i to bezprzewodowo. Dobrze jest zakupić moduł przylutowany do adaptera PCB, dzięki niemu łatwo go podłączymy. Ja posiadam moduł HC-05 taki jak poniżej.
HC-05 STM32F4DiscoveryHC-05 STM32F4Discovery


STM32F4Discovery posiada następujące porty COM:

COM1: (rx, tx, cts, rts)=(PA10, PA9 , PA11     , PA12)
COM2: (rx, tx, cts, rts)=(PA3 , PA2 , PD3      , PA1)
COM3: (rx, tx, cts, rts)=(PD9 , PD8 , PD11     , PD12)
COM4: (rx, tx, cts, rts)=(PC11, PC10, GPIO_NONE, GPIO_NONE)
COM5: (rx, tx, cts, rts)=(PD2 , PC12, GPIO_NONE, GPIO_NONE)
COM6: (rx, tx, cts, rts)=(PC7 , PC6 , GPIO_NONE, GPIO_NONE)

Moduł możemy podłączyć do dowolnego portu COM, ale nie sprawdzałem wszystkich. Ja podłączyłem do portu COM2. Minimum co musimy podpiąć, oprócz zasilania, to piny RXD i TXD. Łączymy je na krzyż do wyjść TX i RX. Schemat podłączenia wygląda tak:

HC-05 GND -> STM32F4Discovery GND
HC-05 VCC -> STM32F4Discovery VCC (+5V)
HC-05 RXD -> STM32F4Discovery PA2
HC-05 TXD -> STM32F4Discovery PA3

Dodatkowo można podłączyć wejście WAKEUP (przeważnie podpisane KEY) i wyjście STATE. Ustawienie stanu wysokiego na WAKEUP (KEY) powoduje przejście modułu w tryb komend AT. Umożliwia to konfigurację i sterowanie modułu. Na przykład zmianę nazwy sieciowej, ustawienia innych parametrów transmisji, wykrycie innych modułów itp. Natomiast na wyjściu STATE jest ustawiany stan wysoki w przypadku sparowania modułu. Ja tych dwóch pinów nie podłączałem. Aha. Domyślnie HC-05 działa jako slave z następującymi parametrami: prędkość 9600, brak parzystości, 8 bitów danych, 1 bit stopu, hasło parowania 1234. No oczywiście do poprawnego komunikowania się z modułem niezbędne jest sparowanie z innym urządzeniem bluetooth. Ja do testu sparowałem moduł z PeCetem.

Ok. Prosty test. Wysyłamy bez przerwy tekst kontrolny i wyświetlamy informację jeśli coś odbierzemy. Pozwoli to zorientować się czy moduł działa.

public class Program
{
    public static void Main()
    {
        using (var serial = new SerialPort("COM2"))
        {
            serial.Open();
            serial.DataReceived += (s, e) =>
            {
                Debug.Print("Data received");
                while (serial.BytesToRead > 0)
                    serial.ReadByte();
            };
                

            byte[] buffer = Encoding.UTF8.GetBytes("Ping from STM32F4Discovery\r\n");
            for (;;)
            {
                serial.Write(buffer, 0, buffer.Length);
                Thread.Sleep(3000);
            }
        }
    }
}
Teraz wystarczy tylko odpalić jakiś program terminalowy (np. realterm) i zobaczyć co się będzie działo. Można też zbudować taki prosty program do wysyłania i odbierania danych i odpalić na komputerze (u mnie sparowany port to COM18).

class Program
{
    static void Main()
    {
        using (var serial = new SerialPort("COM18"))
        {
            serial.Open();
            serial.DataReceived += (s, e) => Console.WriteLine(serial.ReadLine());

            byte[] buffer = Encoding.ASCII.GetBytes("Ping from PC\r\n");
            while (!Console.KeyAvailable)
            {
                serial.Write(buffer, 0, buffer.Length);
                Thread.Sleep(3000);
            }
        }
    }
}
Dobra. Bardziej zaawansowany program. Zdalne włączanie i wyłączanie kolorowych diod na płytce. Wysłanie znaków r, g, b, o spowoduje odpowiednio zapalanie i gaszenie skojarzonych diod. Najpierw kot w .NETMF.

Definiujemy komendy i w tablicy kojarzymy je z odpowiednimi diodami. Dodatkowo w głównej pętli programu wysyłamy tekst testowy (co w poprzednim programie) do klienta:

public enum Command
{
    Unknown,
    Red,
    Green,
    Blue,
    Orange
}

private static readonly Hashtable Led = new Hashtable();

public static void Main()
{
    Led.Add(Command.Red, new OutputPort(Stm32F4Discovery.LedPins.Red, false));
    Led.Add(Command.Green, new OutputPort(Stm32F4Discovery.LedPins.Green, false));
    Led.Add(Command.Blue, new OutputPort(Stm32F4Discovery.LedPins.Blue, false));
    Led.Add(Command.Orange, new OutputPort(Stm32F4Discovery.LedPins.Orange, false));

    using (var serial = new SerialPort("COM2"))
    {
        serial.Open();
        serial.DataReceived += DataReceived;

        byte[] buffer = Encoding.UTF8.GetBytes("Ping from STM32F4Discovery\r\n");
        for (; ; )
        {
            serial.Write(buffer, 0, buffer.Length);
            Thread.Sleep(10000);
        }
    }
}

Najważniejsza część programu odbywa się w procedurze DataReceived. Czytamy po kolei wszystkie bajty z bufora portu, konwertujemy na komendy, i zmieniamy stan odpowiedniej diody. Dodatkowo wysyłamy poprzez port aktualny stan diody:

private static void DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    var port = (SerialPort) sender;
    while (port.BytesToRead > 0)
    {
        int b = port.ReadByte();
        if (b == -1)
            continue;

        Command command = ToCommand(b);
        if (command == Command.Unknown)
        {
            Debug.Print(b.ToString("X2"));
            continue;
        }

        var led = (OutputPort) Led[command];
        bool newValue = !led.Read();
        led.Write(newValue);

        string response = (char)b + "=" + (newValue ? "on" : "off") + "\r\n";
        byte[] buffer = Encoding.UTF8.GetBytes(response);
        port.Write(buffer, 0, buffer.Length);
    }
}

private static Command ToCommand(int value)
{
    if (value == 'r')
        return Command.Red;

    if (value == 'g')
        return Command.Green;

    if (value == 'b')
        return Command.Blue;

    if (value == 'o')
        return Command.Orange;

    return Command.Unknown;
}
Teraz kot programu sterującego na PC. Program wyświetla odebrane dane w oknie konsoli, a tekst wpisany i zatwierdzony enterem wysyła:

class Program
{
    static void Main()
    {
        using (var port = new SerialPort("COM18"))
        {
            port.DataReceived += DataReceived;
            port.Open();
                
            for(;;)
            {
                string send = Console.ReadLine();
                if(send.Length == 1 && send[0] == 'q')
                    return;

                port.Write(send);
            }
        }
    }

    static void DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        var port = (SerialPort) sender;
        while (port.BytesToRead > 0)
        {
            int b = port.ReadByte();
            if (b == -1) 
                continue;

            bool printable = (b >= ' ' && b <= '~')
                             || b == 0x0d
                             || b == 0x0a;
            if (printable)
                Console.Write(Convert.ToChar(b));
            else
                Console.Write(b.ToString("X2"));
        }
    }
}

Kot programu i klienta: DemoBTHC05, SerialController

24.06.2014

Dobre wieści

Ostatnio same dobre wieści dotyczące .NET Micro Framework. Na stronie głównej projektu netmf.codeplex.com pojawił się wpis, że dokonywane są właśnie pewne zmiany na codeplex. Między innymi przejście systemu kontroli wersji na GIT. Dodatkowo została utworzona nowa wersja .NET MF v4.Next. Zobaczymy co z tego wyniknie.

Kolejna dobra informacja pochodzi od GHI: The 2014 plan for NETMF and Gadgeteer. Po obniżce ceny FEZ Hydra jest teraz najbardziej atrakcyjnym starterem dla .NET Micro Framework. Dodatkowo biblioteki premium będzie można używać (w celach niekomercyjnych) na płytkach zgodnych z rodziną Cerb (STM32F40X), ale niekoniecznie od GHI.

7.06.2014

Sterownik HAL dla LCD LM15SGFNZ07

Poprzednio był opis jak wyświetlacz LM15SGFNZ07 podłączyć do STM32F4Discovery i obsługiwać z kodu zarządzanego w C#. Były tam procedury, których nie przerobiłem na rysowanie w buforze z jednego prostego powodu - to był tylko test czy wszystko będzie działać. Moim celem było napisanie drivera obsługującego ten wyświetlacz z poziomu CLR. Ale po kolei.

W Microsoft.SPOT.Graphics jest klasa Bitmap, która jest kontenerem do przechowywania informacji o obrazie. Obiekt zawiera informacje o poszczególnych pikselach obrazu oraz posiada metody do wykonywania operacji na nich (np. DrawLine, DrawText etc.). Metoda Flush pozwala wysłać taką bitmapę na ekran wyświetlacza. Jest tylko jeden problem - wyświetlacz musi mieć sterownik wkompilowany w CLR. W katalogu MicroframeworkPK\DeviceCode\Drivers\Display umieszczone są drivery do niektórych wyświetlaczy. Katalog stubs zawiera zaślepkę dla takiego sterownika. Celem jest  uzupełnienie funkcji w pliku Display_stubs_functions.cpp, tak aby obsługiwać nasz wyświetlacz.

Nie jestem programistą C++ i nie znam się na programowaniu w tym języku więc mój kod może być (czytaj jest) mocno nieoptymalny, niezgodny z zasadami pisania w C++ i nie należy się na nim wzorować. Moim celem było zbudowanie działającego drivera jak najszybciej i jak najmniejszym kosztem. Implementację drivera podzieliłem na kilka etapów. Dzięki temu, że miałem poprawnie działający kod w C# mogłem sprawdzić poprawność działania kolejnych funkcji. Najpierw w driverze napisałem procedurę inicjalizacji. W kodzie C# sprawdziłem czy dam radę coś wyświetlić. Jeśli to się powiodło sukcesywnie uzupełniałem kolejne elementy. Najpierw jednak trzeba przygotować katalogi, pliki i TinyCLR.proj.

W katalogu Solutions\Discovery4\DeviceCode zakładamy podkatalog Display, a w nim kolejny podkatalog LM15SGFNZ07. To jest nasz katalog bazowy. Do niego kopiujemy z DeviceCode\Drivers\Display\stubs pliki Display_stubs_functions.cpp i dotNetMF.proj. Nazwę pliku cpp zmieniamy na LM15SGFNZ07_functions.cpp.



Modyfikujemy następujące elementy w dotNetMF.proj:

<AssemblyName>LM15SGFNZ07</AssemblyName>

<ProjectGuid>{D34AB75E-29B0-4408-AE73-F8741FC6D19F}</ProjectGuid>

<Description>LM15SGFNZ07 display driver</Description>

<LibraryFile>LM15SGFNZ07.$(LIB_EXT)</LibraryFile>

<ProjectPath>$(SPOCLIENT)\Solutions\Discovery4\DeviceCode\Display\LM15SGFNZ07\dotNetMF.proj</ProjectPath>

<ManifestFile>LM15SGFNZ07.$(LIB_EXT).manifest</ManifestFile>

<IsStub>False</IsStub>

<Directory>Solutions\Discovery4\DeviceCode\Display\LM15SGFNZ07</Directory>

<ItemGroup>
  <Compile Include="LM15SGFNZ07_functions.cpp" />
</ItemGroup> 

Teraz trzeba zmodyfikować Solutions\Discovery4\TinyCLR\TinyCLR.proj, tak aby dodać biblioteki graficzne i nasz nowy sterownik.

Zamieniamy element
<Import Project="$(SPOCLIENT)\Framework\Features\Core.featureproj" />
na
<Import Project="$(SPOCLIENT)\Framework\Features\TinyCore.featureproj" />
<Import Project="$(SPOCLIENT)\Framework\Features\Graphics.featureproj" />

Teraz musimy odnaleźć element

<ItemGroup>
  <PlatformIndependentLibs Include="Graphics_stub.$(LIB_EXT)" />
  <RequiredProjects Include="$(SPOCLIENT)\CLR\Graphics\dotNetMF_stub.proj" />
</ItemGroup> 

i zamienić na:

<ItemGroup>
  <PlatformIndependentLibs Include="Graphics.$(LIB_EXT)" />
  <RequiredProjects Include="$(SPOCLIENT)\CLR\Graphics\dotNetMF.proj" />
</ItemGroup>
<ItemGroup>
  <DriverLibs Include="graphics_pal.$(LIB_EXT)" />
  <RequiredProjects Include="$(SPOCLIENT)\DeviceCode\PAL\Graphics\dotNetMF.proj" />
</ItemGroup>
<ItemGroup>
  <PlatformIndependentLibs Include="SPOT_Graphics.$(LIB_EXT)" />
  <RequiredProjects Include="$(SPOCLIENT)\CLR\Libraries\SPOT_Graphics\dotNetMF.proj" />
</ItemGroup>
<ItemGroup>
  <PlatformIndependentLibs Include="Graphics_Bmp.$(LIB_EXT)" />
  <RequiredProjects Include="$(SPOCLIENT)\CLR\Graphics\BMP\dotNetMF.proj" />
</ItemGroup>
<ItemGroup>
  <PlatformIndependentLibs Include="Graphics_Gif.$(LIB_EXT)" />
  <RequiredProjects Include="$(SPOCLIENT)\CLR\Graphics\GIF\dotNetMF.proj" />
</ItemGroup>
<ItemGroup>
  <PlatformIndependentLibs Include="Graphics_Jpeg.$(LIB_EXT)" />
  <RequiredProjects Include="$(SPOCLIENT)\CLR\Graphics\Jpeg\dotNetMF.proj" />
</ItemGroup>
<ItemGroup>
  <DriverLibs Include="LM15SGFNZ07.$(LIB_EXT)" />
  <RequiredProjects Include="$(SPOCLIENT)\Solutions\Discovery4\DeviceCode\Display\LM15SGFNZ07\dotNetMF.proj" />
</ItemGroup>

Jak można się domyślić właśnie dodaliśmy obsługę BMP, GIF i JPG. Pozwoli to umieszczać takie formaty w zasobach programu i ładować je do bitmap. Jeśli jakieś formaty są niepotrzebne to trzeba zamienić na zaślepki.

Powyższe operacje generowania gotowego katalogu sterownika oraz modyfikacji projektu TinyCLR można również wykonać z programu SolutionWizard. Ja jednak chciałem pokazać, że ręczna modyfikacja tych plików też jest możliwa.

Teraz trzeba tylko uzupełnić funkcje w  LM15SGFNZ07_functions.cpp. Minimum co musimy dodać to funkcje: LCD_GetWidth, LCD_GetHeight i LCD_BitBltEx. Ponieważ LCD LM15SGFNZ07 musi być wcześniej zainicjowany musimy jeszcze uzupełnić procedurę LCD_Initialize.

Na początku pliku dodajemy definicję użytych pinów oraz konfigurację portu SPI:

#include "tinyhal.h"

#define PE9 (4*16 + 9)
#define PE10 (4*16 + 10)
#define PE11 (4*16 + 11)

#define CS_PIN PE11
#define RESET_PIN PE10
#define RS_PIN PE9

SPI_CONFIGURATION spiCfg = {
                            CS_PIN,                // Chip select
                            FALSE,                 // Chip Select polarity
                            TRUE,                  // MSK_IDLE
                            TRUE,                  // MSK_SAMPLE_EDGE
                            FALSE,                 // 16-bit mode
                            5000,                  // SPI Clock Rate KHz
                            0,                     // CS setup time us
                            0,                     // CS hold time us
                            1,                     // SPI Module (SPI2)
                            {
                                GPIO_PIN_NONE,     // SPI BusyPin
                                FALSE,             // SPI BusyPinActiveState
                            }
                          };

Następnie funkcje pomocnicze:

void Lm15Sgfnz07_SendCommand(UINT8* data, INT32 count) 
{
  CPU_GPIO_SetPinState(RS_PIN, TRUE);
  CPU_SPI_nWrite8_nRead8(spiCfg, data, count, NULL, 0, 0);  
}

void Lm15Sgfnz07_SendData(UINT8* data, INT32 count) 
{
  CPU_GPIO_SetPinState(RS_PIN, FALSE);
  CPU_SPI_nWrite8_nRead8(spiCfg, data, count, NULL, 0, 0);  
}

void Lm15Sgfnz07_SendData16(UINT16* data, INT32 count) 
{
  CPU_GPIO_SetPinState(RS_PIN, FALSE);
  spiCfg.MD_16bits = TRUE;
  CPU_SPI_nWrite16_nRead16(spiCfg, data, count, NULL, 0, 0);
  spiCfg.MD_16bits = FALSE;
}

void Lm15Sgfnz07_Window(UINT8 x1, UINT8 y1, UINT8 x2, UINT8 y2) 
{
    x1 <<= 1;
    x1 += 6;
    x2 <<= 1;
    x2 += 7;
    
    UINT8 data[10];
    
    data[0] = 0xf0;
    data[1] = 0x00 | (x1 & 0x0f);
    data[2] = 0x10 | (x1 >> 4);
    data[3] = 0x20 | (y1 & 0x0f);
    data[4] = 0x30 | (y1 >> 4);
    data[5] = 0xf5;
    data[6] = 0x00 | (x2 & 0x0f);
    data[7] = 0x10 | (x2 >> 4);
    data[8] = 0x20 | (y2 & 0x0f);
    data[9] = 0x30 | (y2 >> 4);
    
    Lm15Sgfnz07_SendCommand(data, 10);
}

void Lm15Sgfnz07_Contrast(UINT8 contrast)
{
  UINT8 buf[3];
  buf[0] = 0xf4;
  buf[1] = 0xb0 | (contrast >> 4);
  buf[2] = 0xa0 | (contrast & 0x0f);
  
  Lm15Sgfnz07_SendCommand(buf, 3);
}

Uzupełniamy funkcje Width i Height:

INT32 LCD_GetWidth()
{
    NATIVE_PROFILE_HAL_DRIVERS_DISPLAY();
    return 101;
}

INT32 LCD_GetHeight()
{
    NATIVE_PROFILE_HAL_DRIVERS_DISPLAY();
    return 80;
}

Teraz nadszedł czas na procedurę inicjalizacji. Jest to ten sam kot co w bibliotece C# przeniesiony do C++:

BOOL LCD_Initialize()
{
    NATIVE_PROFILE_HAL_DRIVERS_DISPLAY();
    
    CPU_GPIO_EnableOutputPin(CS_PIN, TRUE);
    CPU_GPIO_EnableOutputPin(RESET_PIN, TRUE);
    CPU_GPIO_EnableOutputPin(RS_PIN, FALSE);
    
    CPU_GPIO_SetPinState(RESET_PIN, FALSE);
    HAL_Time_Sleep_MicroSeconds_InterruptEnabled(10*1000);//10ms
    CPU_GPIO_SetPinState(RESET_PIN, TRUE);
    HAL_Time_Sleep_MicroSeconds_InterruptEnabled(10*1000);
        
    UINT8 initData1[139] = {0xF4, 0x90, 0xB3, 0xA0, 0xD0,
                             0xF0, 0xE2, 0xD4, 0x70, 0x66, 0xB2, 0xBA, 0xA1, 0xA3, 0xAB, 0x94, 0x95,
                             0x95, 0x95, 0xF5, 0x90, 0xF1, 0x00, 0x10, 0x22, 0x30, 0x45, 0x50, 0x68,
                             0x70, 0x8A, 0x90, 0xAC, 0xB0, 0xCE, 0xD0, 0xF2, 0x0F, 0x10, 0x20, 0x30,
                             0x43, 0x50, 0x66, 0x70, 0x89, 0x90, 0xAB, 0xB0, 0xCD, 0xD0, 0xF3, 0x0E,
                             0x10, 0x2F, 0x30, 0x40, 0x50, 0x64, 0x70, 0x87, 0x90, 0xAA, 0xB0, 0xCB,
                             0xD0, 0xF4, 0x0D, 0x10, 0x2E, 0x30, 0x4F, 0x50, 0xF5, 0x91, 0xF1, 0x01,
                             0x11, 0x22, 0x31, 0x43, 0x51, 0x64, 0x71, 0x86, 0x91, 0xA8, 0xB1, 0xCB,
                             0xD1, 0xF2, 0x0F, 0x11, 0x21, 0x31, 0x42, 0x51, 0x63, 0x71, 0x85, 0x91,
                             0xA6, 0xB1, 0xC8, 0xD1, 0xF3, 0x0B, 0x11, 0x2F, 0x31, 0x41, 0x51, 0x62,
                             0x71, 0x83, 0x91, 0xA4, 0xB1, 0xC6, 0xD1, 0xF4, 0x08, 0x11, 0x2B, 0x31,
                             0x4F, 0x51, 0x80, 0x94, 0xF5, 0xA2, 0xF4, 0x60, 0xF0, 0x40, 0x50, 0xC0,
                             0xF4, 0x70};
    
    Lm15Sgfnz07_SendCommand(initData1, 139);
    HAL_Time_Sleep_MicroSeconds_InterruptEnabled(10*1000);
    
    UINT8 initData2[15] = {0xF0, 0x81,
                            0xF4, 0xB3, 0xA0,
                            0xF0, 0x06, 0x10, 0x20, 0x30,
                            0xF5, 0x0F, 0x1C, 0x2F, 0x34};
                                          
    Lm15Sgfnz07_SendCommand(initData2, 15);
    HAL_Time_Sleep_MicroSeconds_InterruptEnabled(10*1000);
    
    Lm15Sgfnz07_Contrast(42);
    
    return TRUE;
}

No i najważniejsza procedura rysująca. Tutaj potrzebne jest małe wyjaśnienie. Do procedury trafiają dane UINT32 data[]. Jest to tablica poszczególnych pikseli bitmapy w formacie RGB 5:6:5. Przy czym jedna komórka tablicy (data[n]) zawiera informacje o dwóch, kolejnych pikselach. W little endian (tak jak u nas)  16 młodszych bitów (maska 0x0000FFFF) zawiera informacje o pierwszym (z pary) pikselu, a starsze 16 bitów (0xFFFF0000) o drugim pikselu. Cała logika sprowadza się do wyłuskania kolejnych pikseli RGB 5:6:5 i skonwertowanie ich na RGB 4:4:4.

void LCD_BitBltEx( int x, int y, int width, int height, UINT32 data[] )
{
    NATIVE_PROFILE_HAL_DRIVERS_DISPLAY();
        
    const int size = width * height;
    UINT16* buf = (UINT16*)private_malloc(size*2);
        
    int widthInWords = Graphics_GetWidthInWords(width);
    for(int py = 0; py < height; py++)
    {
      for(int px = 0; px < width; px++)
      {
        UINT32 shift = (px % 2) * 16;
        UINT32 mask = 0x0000FFFF << shift;
      
        UINT16 pixel = (data[py*widthInWords + px/2] & mask) >> shift;
        //from RGB 5:6:5
        //to RGB 0:4:4:4 
        pixel = ((pixel >> 4) & 0x0F00) | ((pixel >> 3) & 0x00F0) | ((pixel >> 1) & 0x000F);
        buf[py*width + px] = pixel;
      }
    }
    
    Lm15Sgfnz07_Window(x, y, x + width - 1, y + height - 1);
    Lm15Sgfnz07_SendData16(buf, size);
    private_free(buf);
}

No i tyle. Teraz wystarczy skompilować solucję i wgrać do STM32F4Discovery. W ten sposób uzyskujemy dostęp do funkcji graficznych .NET Micro Framework. Jako demo może posłużyć lekko zmodyfikowany przykład z katalogu MicroFrameworkPK\Product\Sample\Test\LCD


Kot dema razem z driverem (katalog Discovery4): DemoLM15SGFNZ07Driver

1.06.2014

Wyświelacz LCD LM15SGFNZ07

Znalazłem jakiś stary telefon i wygrzebałem z niego wyświetlacz LCD. Zastanawiałem się czy da się to jakoś podłączyć do STM32F4Discovery i obsłużyć w .NET Micro Framework. Trochę poszukałem no i się da. Bardzo pomocne były informacje na tych stronach:
Wyświetlacz LM15SGFNZ07 zamontowany jest w następujących telefonach komórkowych: Siemens A60,  A65, C60, MC60, M55 i S55. Ważne jest, aby wyświetlacz miał zielone PCB. W tych telefonach był też montowany inny wyświetlacz - ze "złotym" PCB i ten opis takiego LCD nie dotyczy. Wyświetlacz nie powala rozdzielczością i kolorami. Może wyświetlić 4096 kolorów i ma rozmiar 101 x 80 pikseli. Komunikacja z wyświetlaczem odbywa się przez interfejs SPI. Ja użyłem SPI 2 (MSK=PB13, MISO=PB14, MOSI= PB15), ale nic nie stoi na przeszkodzie, aby użyć innego. Podłączenie do STM32F4Discovery poniżej.
LM15SGFNZ07
Piny na wyświetlaczu (od lewej do prawej) mają następujące znaczenie:
  • Pin 1 -> PE11: CS (chip select: Lo)
  • Pin 2 -> PE10: RESET (reset: Lo)
  • Pin 3 -> PE9  : RS (command: Hi, data: Lo)
  • Pin 4 -> PB13: SCLK (clock)
  • Pin 5 -> PB15: MOSI (data)
  • Pin 6              : VCC (+2.9V...+3.3V)
  • Pin 7              : GND
  • Pin 8              : LED1 backlight A
  • Pin 9              : LED1, LED2 K
  • Pin10             : LED2 backlight A
Programowa obsługa wyświetlacza sprowadza się do wysłania przez SPI odpowiednich instrukcji sterujących lub danych. Aby wyświetlić coś na ekranie musimy wysłać najpierw komendę informującą o oknie, które chcemy zapełnić, a następnie dane o poszczególnych pikselach. Jeden piksel może mieć 4096 kolorów. Poszczególne kolory składowe piksela (R, G, B) zakodowane są na 4 bitach. Wartość szesnastkowa koloru pojedynczego piksela ma następującą postać: 0x0RGB. Cała sztuka polega na tym, aby dane, które mają zapełnić okno, wysłać jak najszybciej przez interfejs SPI. W przeciwnym wypadku  wyświetlacz "będzie działał wolno". Najlepiej poszczególne piksele rysować najpierw w buforze, a następnie taki bufor wysłać za jednym razem. Na szczęście klasa SPI ma metodę do wysyłania wartości bufora typu ushort (0x0000..0xFFFF), która się świetnie tutaj nadaje: void Write(ushort[] writeBuffer).

Na początek konstruktor i dwie bardzo przydatne procedury:

public class Lm15Sgfnz07
{
    public const int Width = 101;
    public const int Height = 80;

    private readonly SPI _spi;
    private readonly OutputPort _reset;
    private readonly OutputPort _rs;

    public Lm15Sgfnz07(SPI.SPI_module spi, Cpu.Pin cs, Cpu.Pin reset, Cpu.Pin rs)
    {
        var spiCfg = new SPI.Configuration(cs, false, 0, 0, true, true, 5000, spi);
        _spi = new SPI(spiCfg);
        _reset = new OutputPort(reset, true);
        _rs = new OutputPort(rs, false);

        Initialize();
    }

    private void SendCommand(params byte[] values)
    {
        _rs.Write(true);
        _spi.Write(values);
    }

    private void SendData(params ushort[] values)
    {
        _rs.Write(false);
        _spi.Write(values);
    }
}
W konstruktorze została użyta procedura inicjalizacji wyświetlacza. Procedura resetuje wyświetlacz i wysyła (bliżej mi nieznane) sekwencje konfigurujące.

private void Initialize()
{
    _reset.Write(false);
    Thread.Sleep(10);
    _reset.Write(true);

    SendCommand(0xF4, 0x90, 0xB3, 0xA0, 0xD0,
                0xF0, 0xE2, 0xD4, 0x70, 0x66, 0xB2, 0xBA, 0xA1, 0xA3, 0xAB, 0x94, 0x95,
                0x95, 0x95, 0xF5, 0x90, 0xF1, 0x00, 0x10, 0x22, 0x30, 0x45, 0x50, 0x68,
                0x70, 0x8A, 0x90, 0xAC, 0xB0, 0xCE, 0xD0, 0xF2, 0x0F, 0x10, 0x20, 0x30,
                0x43, 0x50, 0x66, 0x70, 0x89, 0x90, 0xAB, 0xB0, 0xCD, 0xD0, 0xF3, 0x0E,
                0x10, 0x2F, 0x30, 0x40, 0x50, 0x64, 0x70, 0x87, 0x90, 0xAA, 0xB0, 0xCB,
                0xD0, 0xF4, 0x0D, 0x10, 0x2E, 0x30, 0x4F, 0x50, 0xF5, 0x91, 0xF1, 0x01,
                0x11, 0x22, 0x31, 0x43, 0x51, 0x64, 0x71, 0x86, 0x91, 0xA8, 0xB1, 0xCB,
                0xD1, 0xF2, 0x0F, 0x11, 0x21, 0x31, 0x42, 0x51, 0x63, 0x71, 0x85, 0x91,
                0xA6, 0xB1, 0xC8, 0xD1, 0xF3, 0x0B, 0x11, 0x2F, 0x31, 0x41, 0x51, 0x62,
                0x71, 0x83, 0x91, 0xA4, 0xB1, 0xC6, 0xD1, 0xF4, 0x08, 0x11, 0x2B, 0x31,
                0x4F, 0x51, 0x80, 0x94, 0xF5, 0xA2, 0xF4, 0x60, 0xF0, 0x40, 0x50, 0xC0,
                0xF4, 0x70);

    Thread.Sleep(10);

    SendCommand(0xF0, 0x81,
                0xF4, 0xB3, 0xA0,
                0xF0, 0x06, 0x10, 0x20, 0x30,
                0xF5, 0x0F, 0x1C, 0x2F, 0x34);
}
Teraz dwie bardzo ważne procedury: do regulacji kontrastu i do ustawienia okna dla danych. Doświadczalnie stwierdziłem, że najlepszą wartością dla kontrastu to 42.

public void Contrast(byte contrast)
{
    SendCommand(0xF4,
               (byte) (0xB0 | (contrast >> 4)),
               (byte) (0xA0 | (contrast & 0x0F)));
}

private void ViewPort(int x1 = 0, int y1 = 0, int x2 = Width-1, int y2 = Height-1)
{
    x1 <<= 1;
    x1 += 6;
    x2 <<= 1;
    x2 += 7;

    SendCommand(0xf0,
               (byte) (0x00 | (x1 & 0x0f)),
               (byte) (0x10 | (x1 >> 4)),
               (byte) (0x20 | (y1 & 0x0f)),
               (byte) (0x30 | (y1 >> 4)),
                0xf5,
               (byte) (0x00 | (x2 & 0x0f)),
               (byte) (0x10 | (x2 >> 4)),
               (byte) (0x20 | (y2 & 0x0f)),
               (byte) (0x30 | (y2 >> 4)));
}
Teraz kilka przykładów użycia wyżej wymienionych procedur w funkcjach rysujących. Procedura SetPixel jest nieefektywna i należy jej używać tylko w celach testowych.

public void SetPixel(ushort color, int x, int y)
{
    ViewPort(x, y, x, y);
    SendData(color);
}

public void DrawImage(ushort[] image, int x, int y, int width, int height)
{
    ViewPort(x, y, x + width - 1, y + height - 1);
    SendData(image);
}

public void FillRectangle(ushort color, int x, int y, int width, int height)
{
    var buffer = new ushort[width * height];
    for (int i = 0; i < buffer.Length; i++)
        buffer[i] = color;

    DrawImage(buffer, x, y, width, height);
}

public void Clear(ushort color)
{
    FillRectangle(color, 0, 0, Width, Height);
}

Biblioteka została uzupełniona jeszcze o następujące procedury rysujące, których nie ma sensu tu wklejać:
  • Line(ushort color, int x1, int x2, int y1, int y2) - nieefektywana, używa SetPixel
  • Rectangle(ushort color, int x, int y, int width, int height, int thickness) - tak jak wyżej
  • Text(string text, int x, int y, ushort foreColor, ushort backColor, ILm15Sgfnz07Font font)
  • int MeasureTextWidth(string text, ILm15Sgfnz07Font font)
  • static ushort[] PpmToImage(byte[] ppmContent, out int width, out int height, out int depth)
Niektóre procedury używają nieefektywnego rysowania pojedynczych pikseli. Dlaczego? Bo nie chciało mi się tego optymalizować. Ale o tym w następnych wpisach. Statyczna procedura PpmToImage konwertuje dane obrazu PPM na format procedury DrawImage. Obraz PPM można wcześniej przygotować np. w programie Paint.Net (potrzebny jest plugin do zapisu plików PPM) i umieścić w zasobach aplikacji. Do rysowania napisów potrzebne są definicje fontów. Jeden (domyślny) załączyłem w przykładzie. Inne fonty można wygenerować programem MikroElektronika GLCD Font Creator.

No dobra. Poniżej przykład jak tego wszystkiego użyć:
public class Program
{
    public static void Main()
    {
        const Cpu.Pin csPin = Stm32F4Discovery.FreePins.PE11;
        const Cpu.Pin resetPin = Stm32F4Discovery.FreePins.PE10;
        const Cpu.Pin rsPin = Stm32F4Discovery.FreePins.PE9;

        const ushort black = 0x0000;
        const ushort white = 0x0FFF;
        const ushort red = 0x0F00;
        const ushort blue = 0x000F;
        const ushort green = 0x00F0;
        const ushort yellow = 0x0FF0;

        var lcd = new Lm15Sgfnz07(SPI.SPI_module.SPI2, csPin, resetPin, rsPin);
        lcd.Contrast(43);
        lcd.Clear(white);

        lcd.FillRectangle(red, 10, 10, Lm15Sgfnz07.Width - (2*10), 20);
        lcd.Text(".NET MF STM32F4", 6, 35, black, white);

        lcd.Rectangle(blue, 5, 5, Lm15Sgfnz07.Width - (2*5), Lm15Sgfnz07.Height - (2*5));

        lcd.Line(0x888, 0, Lm15Sgfnz07.Width, 0, Lm15Sgfnz07.Height);
        lcd.Line(0x888, Lm15Sgfnz07.Width, 0, 0, Lm15Sgfnz07.Height);

        byte[] res = Resources.GetBytes(Resources.BinaryResources.smile);

        int width, height, depth;
        ushort[] img = Lm15Sgfnz07.PpmToImage(res, out width, out height, out depth);
        lcd.DrawImage(img, (Lm15Sgfnz07.Width - width)/2, 50, width, height);

        Thread.Sleep(Timeout.Infinite);
    }
}
LM15SGFNZ07 STM32F4Discovery


Pełny kot: DemoLM15SGFNZ07Managed.

4.05.2014

Problem z Socket.Connect

Robiąc jakiś przykład systemu klient-serwer napotkałem problem w metodzie Connect dla socketa. Problem pojawia się w chwili gdy np. restartując usługę na serwerze, klient .Net Micro Framework próbuje się do niego połączyć. Cały klient się po prostu zawiesza właśnie na metodzie Connect. Takie zachowanie można wywołać np. takim kotem:
IPAddress serverIp = Dns.GetHostEntry("www.google.pl").AddressList[0];
EndPoint endPoint = new IPEndPoint(serverIp, 10000);

using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
    try
    {
        socket.Connect(endPoint);
    }
    catch (SocketException ex)
    {
        Debug.Print(ex.Message + ": " + ex.ErrorCode);
    }
}
Próbowałem to jakoś obejść. Zacząłem kombinować tak, aby uruchomić Connect w osobnym wątku, a jeśli się zawiesi to go ubić. Wyszło mi coś takiego:

IPAddress serverIp = Dns.GetHostEntry("www.google.pl").AddressList[0];
EndPoint endPoint = new IPEndPoint(serverIp, 10000);

retryConnect:
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
    bool connected = false;
    var t = new Thread(() =>
                            {
                                try
                                {
                                    socket.Connect(endPoint);
                                    connected = true;
                                }
                                catch (SocketException ex)
                                {
                                    Debug.Print("SocketException: " + ex.ErrorCode);
                                }
                            });

    t.Start();
    if (!t.Join(1000))
    {
        t.Abort();
        Debug.Print("Timeout");
        goto retryConnect;
    }

    if (!connected)
        goto retryConnect;


    Debug.Print("Connected!!!");
}
Moje rozwiązanie działa poprawnie, ale przypomniałem sobie o pewnej klasie: Microsoft.SPOT.ExecutionConstraint. ExecutionConstraint pilnuje, aby program zakończył się w założonym czasie. Jeśli wykonywanie programu będzie trwało dłużej, zostanie on przerwany i zostanie wygenerowany wyjątek ConstraintException. Poniżej przykład jak tego używać:
const int runMilisec = 10000;
ExecutionConstraint.Install(runMilisec, 0);
try
{
    //operation
    //code here
}
catch (ConstraintException)
{
    //timeout
}
finally
{
    ExecutionConstraint.Install(Timeout.Infinite, 0);
}
Tak więc na bazie tej klasy można zbudować rozszerzenie klasy Socket - nową funkcję Connect, która będzie próbować się połączyć w zadanym czasie, i zwróci informację czy połączenie przebiegło poprawnie czy nie:
public static class SocketExtension
{
    public static bool Connect(this Socket @this, EndPoint endPoint, int timeoutMiliseconds)
    {
        ExecutionConstraint.Install(timeoutMiliseconds, 0);
        try
        {
            @this.Connect(endPoint);
            return true;
        }
        catch (ConstraintException)
        {
            return false;
        }
        finally
        {
            ExecutionConstraint.Install(Timeout.Infinite, 0);
        }
    }
}

18.02.2014

Wejście analogowe

Obsługa wejść analogowych w NET MF jest bardzo prosta. Na STM32F4Discovery mamy dostępne 7 wejść analogowych, wszystkie mają rozdzielczość 12 bitów (0...4096):

Cpu.AnalogChannel.ANALOG_0: PA1
Cpu.AnalogChannel.ANALOG_1: PA2
Cpu.AnalogChannel.ANALOG_2: PA3
Cpu.AnalogChannel.ANALOG_3: PB0
Cpu.AnalogChannel.ANALOG_4: PB1
Cpu.AnalogChannel.ANALOG_5: PC4
Cpu.AnalogChannel.ANALOG_6: PC5

Do testów możemy po prostu do wejścia podłączyć potencjometr. Ja akurat miałem o wartości 10K, ale w zasadzie każdy może być powiedzmy w zakresie od 1K do 100K.

STM32F4Discovery AnalogInput PotencjometrSTM32F4Discovery AnalogInput Potencjometr

Inicjalizacja i użycie jest bardzo proste. Odczytujemy wartości bezpośrednio z wejścia. Kręcimy potencjometrem zmieniając wartość napięcia na wejściu w zakresie od 0 do 3V:

using (var analogInput = new AnalogInput(Cpu.AnalogChannel.ANALOG_0))
{
    for (;;)
    {
        double readVal = analogInput.Read();
        int rawVal = analogInput.ReadRaw();
        Debug.Print("Sample: " + readVal + " (" + rawVal + ")");

        Thread.Sleep(5000);
    }
}

Jak widać powyżej mamy dwie funkcje do odczytu: ReadRaw i Read. Funkcja ReadRaw zwraca wartość nieprzetworzoną (u nas w zakresie od 0 do 4096), a Read wartość przeskalowaną i przesuniętą (przetworzoną). Szczególnie druga funkcja jest bardzo użyteczna. Na przykład, aby otrzymać wartość napięcia na wejściu analogowym (od 0 do +3V) wystarczy ustawić następujące parametry:

using (var analogInput = new AnalogInput(Cpu.AnalogChannel.ANALOG_0))
{
    analogInput.Scale = 3;  //+3V
    analogInput.Offset = 0; //od 0

    for (;;)
    {
        double readVal = analogInput.Read();
        Debug.Print("U=" + readVal);
        Thread.Sleep(1000);
    }
}

Prawda, że proste? Parametr Offset pozwala przesunąć pozycję zerową. Na przykład gdyby pozycja środkowa potencjometru była dla nas wartością zerową i chcielibyśmy, aby wartości były z przedziału -100 w jednym skrajnym położeniu i +100 w drugim skrajnym położeniu (-100%..+100%), to trzeba ustawić Scale na 200, a Offset na -100.

Jeszcze jeden przykład. Sterowanie jasnością świecenia diody (przez PWM) w zależności od ustawienia potencjometru:

var analog = new AnalogInput(Cpu.AnalogChannel.ANALOG_0);
analog.Scale = 100; //brightness: 0..100%

var pwm = new PWM(Cpu.PWMChannel.PWM_0, 300, 0, false);
pwm.Start();

double prev = Double.MinValue;
for (;;)
{
    double curr = analog.Read();

    if (Math.Abs(curr - prev) >= 1)
    {
        pwm.DutyCycle = ToDutyCycle(curr);
        prev = curr;
    }
}

Funkcja przeliczająca oczekiwaną jasność na wypełnienie taka jak w poprzednim wpisie o PWM:

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

    if (brightness > 99)
        return 1;

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

A niech będzie jeszcze jeden przykład, z użyciem interfejsu sieciowego, bo na razie nie było żadnego. Potencjometr przez wejście analogowe będzie sterował głośnością w XBMC uruchomionym na innym komputerze. Oczywiście trzeba w XBM włączyć Webserver, będziemy zdalnie wywoływać JSON-RPC_API.

Najważniejsza jest funkcja wysyłająca odpowiednio spreparowany request do XBMC. Trzeba jedynie pamiętać o tym, że wysyłając żądanie POST musimy ustawić odpowiednio ContentType, no i nie zapomnieć o użytkowniku i haśle do serwisu XBMC.

private static void SendXbmcVolume(int volume)
{
    const string xbmcHost = "192.168.1.2";
    const string xbmcPort = "8081";
    const string xbmcUser = "xbmc";
    const string xbmcPassword = "xbmc";

    string json = "{\"jsonrpc\": \"2.0\", " +
                    "\"method\": \"Application.SetVolume\", " +
                    "\"params\": {\"volume\": " + volume + "}, " +
                    "\"id\": 1}";

    const string xbmcRpc = @"http://" + xbmcHost + ":" + xbmcPort + "/jsonrpc";

    using (var request = (HttpWebRequest) WebRequest.Create(xbmcRpc))
    {
        byte[] content = Encoding.UTF8.GetBytes(json);

        request.Method = "POST";
        request.ContentType = "application/json";
        request.Accept = "application/json";
        request.ContentLength = content.Length;
        request.KeepAlive = false;
        request.Credentials = new NetworkCredential(xbmcUser,
                                                    xbmcPassword,
                                                    AuthenticationType.Basic);

        using (var stream = request.GetRequestStream())
            stream.Write(content, 0, content.Length);
    }
}

Wywołanie powyższej funkcji można zrealizować tak:

using (var analogInput = new AnalogInput(Cpu.AnalogChannel.ANALOG_0))
{
    analogInput.Scale = 100;

    double prevVal = Double.MinValue;
    for (;;)
    {
        double currentVal = analogInput.Read();

        if (Math.Abs(currentVal - prevVal) >= 1)
        {
            int volume = ToVolume(currentVal);
            SendXbmcVolume(volume);
            prevVal = currentVal;
        }
    }
}

Pozostaje jeszcze funkcja ToVolume. Dostosowuje ona wartość podawaną z potencjometru do logarytmicznej charakterystyki ucha ludzkiego. Funkcję naprędce wymyśliłem, aby tylko miała charakterystykę logarytmiczną, więc może nie być w pełni poprawna. Ale działa!
private static int ToVolume(double volume)
{
    double loud = 50*Math.Log10(volume);

    if (loud < 0)
        return 0;

    if (loud > 100)
        return 100;

    return (int) Math.Round(loud);
}

Pełny kot: DemoAnalogInput