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;
                    }
                }
            }
        }
    }
}