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

17.02.2014

Wersja .NET MF 4.3 RTM (QFE1)

Dokładnie tydzień temu została opublikowana wersja QFE1 .NET Micro Framework 4.3. Najważniejsze zmiany to:
  •  poprawna kompilacja projektów zawierających kropkę w nazwie
  • dodanie BitConverter
  • poprawki w StringBuilder.Replace, Array.BinarySearch i klasie Uri

4.02.2014

Prawdziwy ethernet na ENC28J60

Może i mIP jest dobre na początek, ale na dłuższą metę nie da się tego używać. Dlatego postanowiłem udostępnić (na razie) skompilowaną wersję CLR dla STM32F4Discovery z obsługą ethernetu na ENC28J60. Od razu ostrzegam: nie wiem czy wszystko poprawnie chodzi. Pliki źródłowe udostępnię w późniejszym czasie.

Wszystko trzeba połączyć tak jak w opisie dla mIP: Ethernet na ENC28J60. Za pomocą jakiegoś kota z mIP można też sprawdzić czy wszystko jest dobrze połączone. Jeśli tak, to teraz wystarczy wgrać ER_CONFIG i ER_FLASH. Pliki znajdują się tutaj: OneWire+SDCard+Enc28j60.

No dobra. Szybkie sprawdzenie czy wszystko jest ok. Wyświetlamy informacje o dostępnych interfejsach i włączamy DHCP, tak aby automatycznie pobierać adres IP.

NetworkInterface[] eths =  NetworkInterface.GetAllNetworkInterfaces();
if(eths.Length == 0)
{
    Debug.Print("Ethernet error");
    return;
}

NetworkInterface networkInterface = eths[0];
networkInterface.EnableDhcp();

Debug.Print("Ethernet ok");

Mała dygresja na temat EnableDhcp. Po użyciu tej funkcji (jeśli wcześniej był statyczny IP, a taki jest domyślnie ustawiony) potrzebny jest reset płytki. Funkcja ta zmienia parametr w konfiguracji, a nie w czasie rzeczywistym odświeża adres z DHCP. Równie dobrze możemy to zrobić jednorazowo z MFDeploy w menu Target->Configuration->Network. Może jest jakaś poprawka na takie zachowanie tej funkcji, ale na razie nie sprawdzałem.
STM32F4Discovery network

3.02.2014

18.01.2014

.NET Micro Framework na STM32F429

Jestem w szoku. STMicroelectronics, na swojej stronie, udostępnił paczkę z .NET Micro Framework do płytki STM32F429 Discovery, o której wcześniej wspominałem (Discovery ma przyszłość).

W paczce STSW-STM32141 znajdują się pliki hex (tinybooter, CLR i config) do załadowania, sterownik USB (WinUSB) oraz przykłady w C#. Między innymi obsługa LCD, żyroskopu i ekranu dotykowego.Całość została przygotowana dla NETMF SDK 4.3.

13.01.2014

Parametry msbuild

Kto choć raz skompilował solucję Discovery4 (z minimalnymi parametrami np. msbuild /p:flavor=DEBUG;memory=flash Solutions\Discovery4\dotNetMF.proj) nie mógł się zapewne nadziwić, jak coś można z tych szlaczków, co przelatują na ekranie, wyczytać. Ekran przypomina coś takiego...

.NET MF Compilation
Aha! Nie do końca. Pięć ekranów wcześniej był żółty, a 2 ekrany wcześniej czerwony. Czy idzie to jakoś ogarnąć? Tak. Potrzebne są dodatkowe parametry. Po kolei.

Właściwości projektu to np. /p:flavor=debug;memory=flash. Zamiast debug możemy użyć następujących wartości: instrumented, release, rtm. Do końcowych wersji CLR powinniśmy użyć release lub rtm.

Elementy docelowe projektu to np: /t:build. Jest to domyślna wartość. Pozwala przyrostowo kompilować solucję. Gdy coś testujemy, dodając elementy do solucji, lub wyskoczy błąd kompilacji to ta opcja najszybciej skompiluje nam ponownie. Do czystej kompilacji powinniśmy użyć wartości /t:clean;build.

Plik projektu do kompilacji to np. Solutions\Discovery4\dotNetMF.proj. W tym przypadku zostanie skompilowany TinyBooter, a następnie TinyCLR. Nic nie stoi jednak na przeszkodzie, aby kompilować tylko pojedyncze projekty. Najczęściej kompiluje się TinyCLR. Czyli powinniśmy użyć takiego projektu Solutions\Discovery4\TinyCLR\TinyCLR.proj. Jak chcemy skompilować tylko TinyBooter to Solutions\Discovery4\TinyBooter\TinyBooter.proj.

Teraz opcje loggera. Parametry logowania konsoli określa się przełącznikiem /clp. Na przykład, aby nie pojawiały się na konsoli żadne informacje można użyć /clp:Verbosity=quiet. Przecież i tak nic z tego co w "czarnym okienku" się pojawia nie jesteśmy w stanie ogarnąć. Wartości jakie możemy użyć w verbosity to: quiet, minimal, normal, detailed i diagnostic. Możemy też skorzystać z dodatkowych opcji np. ErrorsOnly, WarningsOnly i Summary. Pierwsze dwa włączają pokazywanie tylko określonych komunikatów, a trzeci wskazuje czy na końcu ma pokazać podsumowanie. Summary działać będzie tylko wówczas, gdy Verbosity>=normal i nie będzie parametrów ErrorsOnly i WarningsOnly. Według mnie najlepszymi parametrami dla loggera konsoli to /clp:ErrorsOnly;Verbosity=quiet.

Jednak na wypadek błędu pasowałoby mieć jakieś informacje, co poszło nie tak. Możemy włączyć logowanie do pliku. Dzięki przełącznikowi /fl w katalogu, z którego odpalamy msbuild, powstanie plik msbuild.log. Dla tego loggera możemy wskazać takie same parametry, jak dla loggera konsolowego np.: /fl /flp:Summary;Verbosity=normal.

Dodatkowe parametry jakie możemy dodać do polecenia to /nologo - nie pokazuje informacji o wersji msbuild i /m - pozwala użyć do kompilacji wiele procesów. Na koniec otrzymamy więc takie polecenie:

msbuild /t:build /p:flavor=release;memory=flash Solutions\Discovery4\TinyCLR\TinyCLR.proj /clp:ErrorsOnly;Verbosity=quiet /fl /flp:Summary;Verbosity=normal /nologo /m

Najlepiej jednak zrobić sobie plik bat w katalogu PK na przykład z taką zawartością:

rem Discovery4 build script
call setenv_gcc 4.6.2 c:\gcc46
msbuild /t:build /p:flavor=release;memory=flash Solutions\Discovery4\TinyCLR\TinyCLR.proj /clp:ErrorsOnly;Verbosity=quiet /fl /flp:Summary;Verbosity=normal /nologo /m
pause

11.01.2014

Pliki *.targets

W podkatalogu tools\Targets MicroFramework PK znajdziemy specjalne pliki *.targets. Co to takiego?

Są to pliki z instrukcjami dla msbuild (zapisane w xmlu), w jaki sposób ma wywoływać zewnętrzne narzędzia kompilacji np. z MDK lub GCC. Czyli np. arm-none-eabi-gcc.exe z chyba 'miliardem' przełączników i opcji.

Popatrzmy na przykład do pliku Microsoft.Spot.system.mdk.targets. Jest tam pełno elementów xml z przeróżnymi atrybutami (np. Condition). Połapanie się w tym wszystkim nie jest proste. Najciekawsze jest jednak na początku pliku. Jest tam pełno takich linii:

<CC      Condition="'$(COMPILER_TOOL_VERSION)'=='MDK3.1'">"$(MDK_TOOL_PATH)\bin31\armcc.exe"</CC>
<CPP     Condition="'$(COMPILER_TOOL_VERSION)'=='MDK3.1'">"$(MDK_TOOL_PATH)\bin31\armcc.exe"</CPP>
<AS      Condition="'$(COMPILER_TOOL_VERSION)'=='MDK3.1'">"$(MDK_TOOL_PATH)\bin31\armasm.exe"</AS>
<LINK    Condition="'$(COMPILER_TOOL_VERSION)'=='MDK3.1'">"$(MDK_TOOL_PATH)\bin31\armlink.exe"</LINK>
<AR      Condition="'$(COMPILER_TOOL_VERSION)'=='MDK3.1'">"$(MDK_TOOL_PATH)\bin31\armar.exe"</AR>
<FROMELF Condition="'$(COMPILER_TOOL_VERSION)'=='MDK3.1'">"$(MDK_TOOL_PATH)\bin31\fromelf.exe"</FROMELF>

Są to wskazania, gdzie znajdują się poszczególne narzędzia w zależności od wersji MDK. Nazwa elementu to zmienna pod jaką będzie w dalszej części pliku występował dany program. Na przykład wszędzie, gdzie będzie potrzebne wywołanie kompilatora armcc.exe, zostanie wstawione $(CC). Jak łatwo się zorientować możemy użyć tylko niektórych wersji MDK. A są to: 3.1, 3.80a, 4.12, 4.13, 4.54. Dlatego tak ważne jest ustawienie odpowiedniej wersji przy wywołaniu setenv_mdk.cmd.

Ale czy można dodać obsługę nowszej wersji MDK? Na przykład 4.60? Jest jeden problem. W parametrach wywołania konsolidatora (linker), gdzieś około linii 222, jest na stałe wstawiona ścieżka do bibliotek: $(MDK_TOOL_PATH)\RV31\LIB. Wersja 4.60 biblioteki ma w zupełnie innym katalogu: $(MDK_TOOL_PATH)\armcc\lib. Łatwo znaleźć, bo jest to katalog z podkatalogami armlib i cpplib.

Trzeba jakoś to obejść. Najlepiej zdefiniować zmienną dla każdej wersji, która będzie przechowywać tą ścieżkę. Trzeba więc zamienić $(MDK_TOOL_PATH)\RV31\LIB na coś takiego: $(MDKLIB). Trzeba jeszcze do każdej wersji (na początku pliku) dodać taką definicję, umiejętnie zmieniając numerek wersji przy każdej nowej sekcji:

<MDKLIB  Condition="'$(COMPILER_TOOL_VERSION)'=='MDK3.1'">"$(MDK_TOOL_PATH)\rv31\lib"</MDKLIB>

Podobnie dokładamy elementy dla wersji 4.60:

<CC      Condition="'$(COMPILER_TOOL_VERSION)'=='MDK4.60'">"$(MDK_TOOL_PATH)\armcc\bin\armcc.exe"</CC>
<CPP     Condition="'$(COMPILER_TOOL_VERSION)'=='MDK4.60'">"$(MDK_TOOL_PATH)\armcc\bin\armcc.exe"</CPP>
<AS      Condition="'$(COMPILER_TOOL_VERSION)'=='MDK4.60'">"$(MDK_TOOL_PATH)\armcc\bin\armasm.exe"</AS>
<LINK    Condition="'$(COMPILER_TOOL_VERSION)'=='MDK4.60'">"$(MDK_TOOL_PATH)\armcc\bin\armlink.exe"</LINK>
<AR      Condition="'$(COMPILER_TOOL_VERSION)'=='MDK4.60'">"$(MDK_TOOL_PATH)\armcc\bin\armar.exe"</AR>
<FROMELF Condition="'$(COMPILER_TOOL_VERSION)'=='MDK4.60'">"$(MDK_TOOL_PATH)\armcc\bin\fromelf.exe"</FROMELF>
<MDKLIB  Condition="'$(COMPILER_TOOL_VERSION)'=='MDK4.60'">"$(MDK_TOOL_PATH)\armcc\lib"</MDKLIB>

I jeszcze jeden element gdzieś koło linii 77:

<CC_CPP_ASM_INTERLEAVE Condition="'$(COMPILER_TOOL_VERSION)'=='MDK4.60'" ></CC_CPP_ASM_INTERLEAVE>

To wszystko. Można kompilować uruchamiając wcześniej setenv_mdk.cmd 4.60