14.12.2012

Karta SD. Przygotowanie i kompilacja PK NET MF

Jak połączenia już mamy zrobione to teraz trzeba przygotować odpowiednio porting kit i skompilować. Aha. Kartę SD wcześniej formatujemy z życiem FAT32. Możemy też utworzyć na niej jakieś pliki. Najlepiej tekstowe. Kartę od razu wkładamy w holder. Przykład modyfikacji i kompilacji dla .NET Micro Framework PK 4.2 QFE2. W innej wersji PK powinno być podobnie.

Uruchamiamy SolutionWizard. Do czego służy i mniej więcej jak korzystać pokazałem wcześniej. Wybieramy modyfikację projektu Discovery4 i nic nie zmieniamy, aż do ekranu "Feature Selection". Tutaj zaznaczamy: "FileSystem" i "SD". A na następnym ekranie "Generate Template" dla tych dwóch nowych funkcjonalności. Dalej standardowo.

SolutionWizard- add FSSolutionWizard - add SD Card
 SolutionWizard - generate template

SolutionWizard zrobił dwie rzeczy w katalogu naszej solucji (C:\MicroFrameworkPK_v4_2\Solutions\Discovery4). Zmodyfikował plik TinyCLR\TinyCLR.proj oraz utworzył katalogi: DeviceCode\FileSystem_Config_PAL i DeviceCode\SD_Config_HAL. W pliku TinyCLR.proj dodał funkcjonalności: Stream, FileSystem i SD oraz odpowiednie drivery: Stream, SPOT_IO, fs_pal, SD_BL, FS_FAT, FileSystem_Config_PAL_Discovery4, SD_Config_HAL_Discovery4. Wywalił zaś zaślepki fs_pal_stubs i FS_Config_stubs. Nas tak naprawdę interesują dodane katalogi.

Katalog DeviceCode\SD_Config_HAL zawiera dwa pliki: proj i cpp. Plików proj nie ruszamy. Plik cpp zawiera konfigurację drivera SD. Czyli np. gdzie jest podłączona karta (który SPI), z jaka częstotliwością odbywa się komunikacja, który pin to CS (ChipSelect) itp. Wprowadzamy tam modyfikacje.

W linii 30 dodajemy:

#pragma arm section rwdata = "g_SD_DeviceRegisters"

Tak aby cały ten blok pragma wyglądał tak:

#if defined(ADS_LINKER_BUG__NOT_ALL_UNUSED_VARIABLES_ARE_REMOVED)
#pragma arm section rwdata = "g_SD_BS_Config"
#pragma arm section rwdata = "g_SD_DeviceRegisters"
#endif

W linii 81 (po definicji struct SD_BLOCK_CONFIG g_SD_BS_Config) dodajemy:

struct SD_DEVICE_REGISTERS g_SD_DeviceRegisters;

Tak aby cały blok definicji struct wyglądał tak:

struct SD_BLOCK_CONFIG g_SD_BS_Config =
{
    {
        SD_WP_GPIO_PIN,            // GPIO_PIN             Pin;
        SD_WP_ACTIVE,              // BOOL                 ActiveState;
    },

     &g_SD_DeviceInfo,             // BlockDeviceinfo
};

struct SD_DEVICE_REGISTERS g_SD_DeviceRegisters;

W linii 83 zaraz po definicjach struct zmieniamy pin z GPIO_PIN_NONE na PB12(dziesiętnie = 28), SD_MSK_SAMPLE_EDGE na TRUE (dane na zboczu narastającym zegara), częstotliwość zegara, oraz z którego interfejsu SPI ma korzystać (SPI1=0, SPI2=1, SPI3=2).

#define SD_CS                    (GPIO_PIN)28 // PB12
#define SD_MSK_SAMPLE_EDGE       TRUE // important
#define SD_CLOCK_RATE_KHZ        400 //check 10000
#define SD_MODULE                1 // SPI2

Tak aby cała konfiguracja wyglądała tak:

#define SD_CS                    (GPIO_PIN)28 // PB12
#define SD_CS_ACTIVE             FALSE
#define SD_MSK_IDLE              TRUE
#define SD_MSK_SAMPLE_EDGE       TRUE // important
#define SD_16BIT_OP              FALSE
#define SD_CLOCK_RATE_KHZ        400 //check 10000
#define SD_CS_SETUP_USEC         0
#define SD_CS_HOLD_USEC          0
#define SD_MODULE                1 // SPI2
#define SD_INSERT_ISR_PIN        GPIO_PIN_NONE
#define SD_EJECT_ISR_PIN         GPIO_PIN_NONE
#define SD_LOW_VOLTAGE_FLAG      FALSE

Częstotliwość zegara można zostawić na 400 kHz lub spróbować wyższą. U mnie karta chodzi bez problemów z częstotliwością 10000 kHz. Dodatkowo dla bezpieczeństwa można ustawić SD_CS_SETUP_USEC i SD_CS_HOLD_USEC na 1.

Katalog DeviceCode\FileSystem_Config_PAL również zawiera dwa pliki. I tak jak poprzednio modyfikujemy tylko cpp. Najpierw dodajemy obsługę FAT32.

W linii 10 po definicjach pragma dodajemy:

extern FILESYSTEM_DRIVER_INTERFACE g_FAT32_FILE_SYSTEM_DriverInterface;
extern STREAM_DRIVER_INTERFACE g_FAT32_STREAM_DriverInterface;

W linii 23 i 28 zmieniamy g_AvailableFSInterfaces i g_InstalledFSCount:

FILESYSTEM_INTERFACES g_AvailableFSInterfaces[] =
{
    { &g_FAT32_FILE_SYSTEM_DriverInterface, &g_FAT32_STREAM_DriverInterface },
};

const size_t g_InstalledFSCount = 1;


To nie wszystko. Trzeba jeszcze dodać kod odpowiedzialny za jej podmontowanie. Inaczej nie będziemy jej widzieli w .NET MF.

W linii 12 po dodanych liniach extern dodajemy:

extern struct BlockStorageDevice  g_SD_BS;
extern struct IBlockStorageDevice g_SD_BS_DeviceTable;
extern struct BLOCK_CONFIG        g_SD_BS_Config;

A w linii 23 ciało procedury FS_MountRemovableVolumes:

void FS_MountRemovableVolumes()
{
 if (BlockStorageList::AddDevice( &g_SD_BS, &g_SD_BS_DeviceTable, &g_SD_BS_Config, TRUE ));
 {
  FS_MountVolume( "SD", 0, 0, &g_SD_BS );  
 }
}

Uff. Cały plik FS_config_Discovery4.cpp ma wyglądać tak:

#include <tinyhal.h>

#if defined(ADS_LINKER_BUG__NOT_ALL_UNUSED_VARIABLES_ARE_REMOVED)
#pragma arm section rwdata = "g_AvailableFSInterfaces"
#endif

extern FILESYSTEM_DRIVER_INTERFACE g_FAT32_FILE_SYSTEM_DriverInterface;
extern STREAM_DRIVER_INTERFACE g_FAT32_STREAM_DriverInterface;

extern struct BlockStorageDevice  g_SD_BS;
extern struct IBlockStorageDevice g_SD_BS_DeviceTable;
extern struct BLOCK_CONFIG        g_SD_BS_Config;

void FS_AddVolumes()
{
}

void FS_MountRemovableVolumes()
{
 if (BlockStorageList::AddDevice( &g_SD_BS, &g_SD_BS_DeviceTable, &g_SD_BS_Config, TRUE ));
 {
  FS_MountVolume( "SD", 0, 0, &g_SD_BS );  
 }
}

FILESYSTEM_INTERFACES g_AvailableFSInterfaces[] =
{
    { &g_FAT32_FILE_SYSTEM_DriverInterface, &g_FAT32_STREAM_DriverInterface },
};
const size_t g_InstalledFSCount = 1;

#if defined(ADS_LINKER_BUG__NOT_ALL_UNUSED_VARIABLES_ARE_REMOVED)
#pragma arm section rwdata
#endif

Kompilujemy zgodnie z tym co napisałem wcześniej: Kompilacja Porting Kit dla STM32F4Discovery. Tutaj mała uwaga. Można śmiało poprawiać pliki i  kompilować bez usuwania katalogu C:\MicroFrameworkPK_v4_2\BuildOutput za każdym razem. Wówczas kompilacja przebiega znacznie szybciej. W wyniku kompilacji otrzymujemy pliki ER_CONFIG + ER_FLASH do wgrania tradycyjnie za pomocą MFDeploy.

Po zaprogramowaniu STM32F4Discovery kolejna próba. Uruchamiamy program napisany kilka postów wcześniej. Program normalnie się uruchomi, a VolumeInfo będzie jak na obrazku poniżej.


SD Card in NET MF on STM32F4Discovery

13.12.2012

Karta SD. Podłączenie sprzętu

Kartę SD będziemy podłączać do STM32F4Discovery przez interfejs SPI. Wyprowadzeń SD card nie będę rozpisywał, bo można sobie popatrzeć na wikipedi jak to wygląda. Przechodzimy do sedna. Użyjemy następujących wyprowadzeń karty:
  • 1 - CS (Card Select)
  • 2 - MOSI (Data IN)
  • 3 - GND (Ground)
  • 4 - VCC (Power)
  • 5 - SCL (Clock)
  • 6 - GND (Ground)
  • 7 - MISO(Data Out)
SD card + STM32F4DiscoveryPodłączanie karty na pająka czy lutowanie bezpośrednio do styków karty pozostawiam hardkorowcom. Ja użyłem holdera do karty SD kupionego za grosze. Przylutowałem do golpinów wyprowadzenia holdera i do testów powinno wystarczyć. Zastosowanie holdera ma jeszcze jedną zaletę. Przez specjalne ustawienie styków łączących, podczas wkładania i wyciągania karty, podłączanie i odłączanie zasilania i sygnałów danych odbywa się w odpowiedniej, bezpiecznej kolejności . 

Jak mowa o zasilaniu to miałem pewne obawy czy napięcie +3V (takie VDD ma płytka STM32F4 Discovery) wystarczy. Wszędzie piszą o zasilaniu z +3.3V. Popatrzyłem więc do dokumentacji SanDiska. Oni dopuszczają napięcie zasilające w zakresie: od +2.7V do +3.6V. Teraz z doświadczenia już wiem, że +3.0V w zupełności wystarczy. A używam karty Kongstona 128MB.

Ostatnie dwa piny holdera na razie nie są wykorzystywane. Przedostatni pin podłączony jest do styków sygnalizujących obecność karty, a ostatni do styków zabezpieczenia przed zapisem. W kartach SD to programista musi zadbać o rozpoznanie stanu blokady zapisu poprzez ten pin i odpowiednią jego obsługę.

Jak już wcześniej wspomniałem będziemy używać interfejsu SPI. Płytka STM32F4Discovery ma 3 interfejsy SPI. Piny drugiego z kolei portu: SPI2, są wolne więc użyjemy ich: PB13-SCL, PB14-MISO, PB15-MOSI, oraz PB12 jako CS. Schemat podłączenia poniżej:

SD card + NET MF + STM32F4Discovery


SD card + NET MF + STM32F4Discovery

12.12.2012

Karta SD w .NET MF na STM32F4Discovery

No oczywiście obsługi karty SD (SD card) nie mamy w porcie .NET Micro Framework dla STM32F4 Discovery. Sprawdzić to można prostym programem. Najpierw pobieramy obsługiwane systemy plików, a następnie odpytujemy o dostępne zasoby (wolumeny). Kot wygląda tak:

using Microsoft.SPOT;
using Microsoft.SPOT.IO;

public class Program
{
    public static void Main()
    {
        string[] availFs = VolumeInfo.GetFileSystems();
        if (availFs.Length == 0)
        {
            Debug.Print("No FS found");
            return;
        }

        foreach (string fs in availFs)
            Debug.Print("Available FS: " + fs);

        VolumeInfo[] volumes = VolumeInfo.GetVolumes();
        if(volumes.Length == 0)
        {
            Debug.Print("No volumes found");
            return;
        }

        foreach (VolumeInfo volume in volumes)
            Debug.Print("Volume: " + volume.Name);
    }
}

W wyniku dostaniemy wyjątek:

An unhandled exception of type 'System.NotSupportedException' occurred in Program.exe

Czyli na 100% nie mamy obsługi żadnego FAT, a co za tym idzie karty SD, w porcie NET MF dla STM32F4Discovery. Trzeba dodać FS i SD do solucji. 

Przez chwilę myślałem, że się nie da i miałem zrezygnować, ale teraz mogę powiedzieć, że karta SD działa w NET MF na STM32F4 Discovery poprawnie. I dodanie jej wcale nie jest trudne. Największym problemem dla takiego lamera jak ja było rozgryzienie co i jak trzeba poupychać w solucji.


8.12.2012

Brak komunikacji z VS (zdechły kot)

Czasami może się zdarzyć, że coś sknocimy w naszym programie i Visual Studio nie będzie się mógł połączyć z płytką STM32F4 Discovery. Mnie się to zdarzyło kilka razy podczas zabaw z portem USB. 

Co wówczas robimy? Wciskamy na płytce przycisk user button (to ten niebieski), przytrzymujemy go i naciskamy przycisk resetu (to ten czarny). Puszczamy oba przyciski. Powinny zapalić się trzy diody na płytce (bez niebieskiej). Oznacza to, że uruchomił się jedynie tinybooter, a nie nasz program. Teraz odpalamy MFDeploy i przyciskiem Erase usuwamy deployment. Proste?

7.12.2012

Nowa wersja .NET MF

Kilka dni temu pojawił się .NET MF 4.3 (RTM). Zawiera wszystkie funkcjonalności i poprawki z wersji .NET MF 4.2 QFE.

Najważniejsze zmiany to:
  • przystosowanie do Visual Studio 2012
  • zmniejszenie czasu uruchamiania
  • skrót do MFDeploy w menu start
  • poprawka funkcji ToString("d4")
  • wynik funkcji ToBase64String zgodny z wersją .NET na PC
  • poprawki driverów sieci i usb
  • dodanie do ExtendedWeakReferences funkcji Flush
Przystosowanie do Visual Studio 2012 w praktyce oznacza, że wersja 4.3 nie będzie już dostępna dla Visual Studio 2010.

12.09.2012

Co ma pod maską STM32F4Discovery

Jak się dowiedzieć w .NET Micro Framework na jakich portach GPIO działa SPI2 czy I2C? Ile jest kanałów PWM, albo wejść analogowych i na jakich portach działają? Czy aby uzyskać te informacje trzeba ich szukać w PK? Nie! Z pomocą przychodzi HardwareProvider.

HardwareProvider (właściwie to singleton tej klasy: HardwareProvider.HwProvider) posiada te wszystkie informacje. Wystarczy go tylko odpytać. Na przykład: na jakich portach działa interfejs I2C:

Cpu.Pin scl, sda;
HardwareProvider.HwProvider.GetI2CPins(out scl, out sda);

Ile jest interfejsów SPI i na jakich portach działają:

HardwareProvider provider = HardwareProvider.HwProvider;
var cnt = provider.GetSpiPortsCount();
for (int i = 0; i < cnt; i++)
{
    var module = (SPI.SPI_module) i;
    Cpu.Pin msk, miso, mosi;
    provider.GetSpiPins(module, out msk, out miso, out mosi);
    Debug.Print("SPI_module" + (i + 1) +
                ": (msk, miso, mosi)=(" +
                Stm32F4Discovery.GetPinName(msk) + ", " +
                Stm32F4Discovery.GetPinName(miso) + ", " +
                Stm32F4Discovery.GetPinName(mosi) + ")");
}
W HardwareProvider znajdziemy również informacje o portach szeregowych, USB, kanałach PWM, wejściach i wyjściach analogowych itd. Dla STM32F4Discovery wygląda to tak:

SystemClock: 168000000
SlowClock: 1000000
GlitchFilterTime: 20 ms
PowerLevel: 16
Uptime: 00:00:02.9007640
OEMString: Copyright Oberon microsystems, Inc.
Version: 4.2.0.0
Total pins:80

I2C pins: scl=PB6 sda=PB9

PWMChannel0: pin=PD12
PWMChannel1: pin=PD13
PWMChannel2: pin=PD14
PWMChannel3: pin=PD15
PWMChannel4: pin=PE9
PWMChannel5: pin=PE11
PWMChannel6: pin=PE13
PWMChannel7: pin=PE14

AnalogOutputChannel0: pin=PA4 precisions=12
AnalogOutputChannel1: pin=PA5 precisions=12

AnalogChannel0: pin=PA1 precisions=12
AnalogChannel1: pin=PA2 precisions=12
AnalogChannel2: pin=PA3 precisions=12
AnalogChannel3: pin=PB0 precisions=12
AnalogChannel4: pin=PB1 precisions=12
AnalogChannel5: pin=PC4 precisions=12
AnalogChannel6: pin=PC5 precisions=12

SPI_module1: (msk, miso, mosi)=(PA5, PA6, PA7)
SPI_module2: (msk, miso, mosi)=(PB13, PB14, PB15)
SPI_module3: (msk, miso, mosi)=(PC10, PC11, PC12)

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

USB0: running
USB1: stopped

Możemy również w programie nadpisać domyślny HardwareProvider. Tak zrobiłem w klasie Stm32F4Discovery w bibliotece Common tworząc klasę Stm32F4DiscoveryHardwareProvider:

namespace Common
{
    public class Stm32F4Discovery
    {
        static Stm32F4Discovery()
        {
            HardwareProvider.Register(new Stm32F4DiscoveryHardwareProvider());
        }

        private sealed class Stm32F4DiscoveryHardwareProvider : HardwareProvider
        {
        }
    }
}

Nie zmienia ona działania domyślnej klasy HardwareProvider. Jednak nic nie stoi na przeszkodzie, aby to zrobić nadpisując odpowiednie funkcje, ale w ten sposób nie zwiększymy ilości kanałów PWM czy innych interfejsów.

Ciekawym miejscem jest statyczny konstruktor klasy Stm32F4Discovery, gdzie przeprowadzana jest rejestracja (podmiana) nowego HardwareProvidera. W .NET Micro Framework statyczne konstruktory są wywoływane przed uruchomieniem programu, a nie podczas pierwszego odwołania do klasy. Taka klasa nawet nie musi być nigdzie użyta, a i tak jej statyczny konstruktor zostanie uruchomiony.

Pełny kot: DemoHardwareInfo

4.09.2012

RTC DS1307 i STM32F4Discovery

Podłączamy układ RTC, aby mieć zawsze aktualną datę i czas. Najprościej użyć właśnie wspomnianego układu DS1307 na magistrali I2C. Na internecie bez problemu znajdziemy biblioteki do obsługi tego układu. Moja biblioteka zawiera dodatkowe funkcje, ale o tym za chwile. Na razie schemat jak podłączyć. Wyświetlacz LCD został z projektu termometru na OneWire. Na pewno się przyda.

STM32F4Discovery DS1307
STM32F4Discovery DS1307

Składowe daty i czasu w rejestrach układu DS1307 przechowywane są w formacie BCD, więc zaczynamy od pomocniczych funkcji umieszczonych w projekcie Common. Jedna zamienia BCD na bajt, a druga konwertuje bajt na BCD.

public static class ByteExtension
{
    public static byte FromBCD(this byte @this)
    {
        var result = (byte) (((@this & 0xf0) >> 4)*10 + (@this & 0x0f));
        return result;
    }

    public static byte ToBCD(this byte @this)
    {
        return (byte) (@this/10 << 4 | @this%10);
    }
}

W konstruktorze klasy DS1307 tworzymy i konfigurujemy I2CDevice. Tworzymy również pomocnicze funkcje do zapisywania i odczytywania danych z rejestrów przy pomocy urządzenia I2C utworzonego w konstruktorze.

public DS1307()
{
    var config = new I2CDevice.Configuration(Address, ClockRate);
    _device = new I2CDevice(config);
}

private byte[] ReadRam(byte address, int expectedBytes)
{
    var result = new byte[expectedBytes];
    var transactions = new I2CDevice.I2CTransaction[2];

    transactions[0] = I2CDevice.CreateWriteTransaction(new[] {address});
    transactions[1] = I2CDevice.CreateReadTransaction(result);

    int actual = _device.Execute(transactions, Timeout);
    int expected = 1 + expectedBytes;

    if (actual != expected)
        throw new IOException("Unexpected I2C transaction result");

    return result;
}

private void WriteRam(byte address, params byte[] data)
{
    if (data == null) 
        throw new ArgumentNullException("data");

    byte[] buffer = Utility.CombineArrays(new[] {address}, data);
    I2CDevice.I2CWriteTransaction transaction = I2CDevice.CreateWriteTransaction(buffer);
    var transactions = new I2CDevice.I2CTransaction[] {transaction};

    int actual = _device.Execute(transactions, Timeout);
    int expected = buffer.Length;

    if (actual != expected)
        throw new IOException("Unexpected I2C transaction result");
}

Dzięki takim pomocniczym funkcjom procedury zapisu i odczytu daty i czasu z DS1307 są bardzo proste.

public DateTime GetDateTime()
{
    byte[] buffer = ReadRam(SecondsAddr, 7);

    byte value = buffer[SecondsAddr];
    bool halted = (value & 0x80) > 0; //CH bit is set
    if (halted)
        throw new InvalidOperationException("DS1307 halted");

    byte second = value.FromBCD();
    byte minute = buffer[MinutesAddr].FromBCD();

    value = buffer[HoursAddr];
    bool mode12 = (value & 0x40) > 0; //12-hour mode
    if (mode12)
        throw new InvalidOperationException("DS1307 in 12-hour mode");

    byte hour = value.FromBCD();
    byte day = buffer[DateAddr].FromBCD();
    byte month = buffer[MonthAddr].FromBCD();
    int year = 2000 + buffer[YearAddr].FromBCD();

    var result = new DateTime(year, month, day, hour, minute, second);
    return result;
}

public void SetDateTime(DateTime dateTime)
{
    var second = (byte) dateTime.Second;
    var minute = (byte) dateTime.Minute;
    var hour = (byte) dateTime.Hour;
    var dayOfWeek = (byte) (dateTime.DayOfWeek + 1);
    var day = (byte) dateTime.Day;
    var month = (byte) dateTime.Month;
    var year = (byte) (dateTime.Year - 2000);

    var buffer = new[]
                     {
                         second.ToBCD(),
                         minute.ToBCD(),
                         hour.ToBCD(),
                         dayOfWeek.ToBCD(),
                         day.ToBCD(),
                         month.ToBCD(),
                         year.ToBCD()
                     };

    WriteRam(SecondsAddr, buffer);
}

Układ DS1307 posiada przestrzeń do przechowywania dowolnych informacji. Możemy tam przechowywać np. konfigurację. Dlatego zrobiłem dodatkowe funkcje do zapisu i odczytu danych z tej przestrzeni. Ostatnie dwie funkcje, to przykład jak możemy w tej przestrzeni przechowywać własne dane. Akurat tutaj na przykładzie string, ale z innymi typami (czy nawet obiektami) jest podobnie. Specjalne dwa markery na początku tej przestrzeni RAM informują nas czy są tam poprawne dane (po resecie na pewna będą losowe wartości). Kolejny bajt przechowuje rozmiar danych.

public void WriteRam(byte[] data)
{
    if (data == null) 
        throw new ArgumentNullException("data");

    int len = data.Length;
    const int maxDataLen = RamLength - 3; // 2*Marker + length = 3
    if(len > maxDataLen) 
        throw new InvalidOperationException("Only " + maxDataLen + " bytes allowed");

    data = Utility.CombineArrays(new[] {OkMarker, OkMarker, (byte)data.Length}, data);
    WriteRam(RamAddr, data);
}

public byte[] ReadRam()
{
    byte[] buffer = ReadRam(RamAddr, RamLength);
    if(buffer[0] != OkMarker || buffer[1] != OkMarker)
        return new byte[0];

    byte len = buffer[2];
    byte[] result = Utility.ExtractRangeFromArray(buffer, 3, len);
    return result;
}

public void WriteRamString(string data)
{
    byte[] buffer = Reflection.Serialize(data, typeof(string));
    WriteRam(buffer);
}

public string ReadRamString()
{
    byte[] buffer = ReadRam();
    if(buffer.Length == 0)
        return String.Empty;

    var result = (string) Reflection.Deserialize(buffer, typeof(string));
    return result;
}

Jak już mamy bibliotekę, to teraz trzeba jakoś zainicjować DS1307 aktualną datą i czasem. No niestety z komputera nie da się magicznie pobrać takich danych do płytki. Można by było to zrobić przez interfejs np. RS232 lub ethernet, ale na razie takich nie mamy. Najprościej to zrobić tak: ustawimy w programie stałą datę i czas, ale z przyszłości tzn. dodajemy minutę lub dwie. Ten czas będzie potrzebny na kompilację i wgranie programu. Czekamy aż nadejdzie pora i naciskamy userbutton. Dokładność ustawionego czasu będzie zależna od naszego refleksu. Taki program inicjujący uruchamiamy tylko raz. Dodatkowo, dla przykładu, do nieulotnej przestrzeni RAM wgrywamy tą ustawianą wartość (DateTime). Później dzięki niej łatwo policzymy czas pracy RTC (uptime).

public class Program
{
    public static void Main()
    {
        //set current date and time + 1 or 2 minutes
        var newDateTime = new DateTime(2012, 09, 04, 21, 30, 45);

        Debug.Print("Wait for " + newDateTime);

        using (var userButton = new InterruptPort(Stm32F4Discovery.ButtonPins.User,
                                                  false, Port.ResistorMode.PullDown,
                                                  Port.InterruptMode.InterruptEdgeLow))
        {
            var ds1307 = new DS1307();
            byte[] storeData = Reflection.Serialize(newDateTime, typeof (DateTime));
            ds1307.WriteRam(storeData);
            //push userbutton when time comes
            userButton.OnInterrupt += (d1, d2, t) =>
                                          {
                                              ds1307.SetDateTime(newDateTime);
                                              Debug.Print("Initialized");
                                          };

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

Do ustawienia w systemie aktualnej daty i czasu służy procedura Utility.SetLocalTime(dt). Jeśli w taki sposób ustawimy czas przy starcie naszego program, to bieżący czas będziemy mogli pobrać zawsze przez DateTime.Now. Wyświetlenie takiej wartości na LCD to już trywialna sprawa. Dodatkowo po naciśnięciu userbutton pokazuje się przez pewien okres czas pracy RTC (uptime).

public class Program
{
    private const int Columns = 16;
    private const int ShowUptimeInterval = 10; //seconds

    private static readonly DS1307 Ds1307 = new DS1307();

    public static void Main()
    {
        DateTime dt = Ds1307.GetDateTime();
        Utility.SetLocalTime(dt);

        var lcdProvider = new GpioLcdTransferProvider(Stm32F4Discovery.Pins.PD1,
                                                      Stm32F4Discovery.Pins.PD2,
                                                      Stm32F4Discovery.Pins.PD9,
                                                      Stm32F4Discovery.Pins.PD11,
                                                      Stm32F4Discovery.Pins.PD10,
                                                      Stm32F4Discovery.Pins.PD8);

        var lcd = new Lcd(lcdProvider);
        lcd.Begin(Columns, 2);

        var userButton = new InterruptPort(Stm32F4Discovery.ButtonPins.User,
                                           false, Port.ResistorMode.PullDown,
                                           Port.InterruptMode.InterruptEdgeLow);

        DateTime showUptimeMode = DateTime.MinValue;
        userButton.OnInterrupt += (d1, d2, t) => showUptimeMode = DateTime.Now
                                                                      .AddSeconds(ShowUptimeInterval);
        
        for (;;)
        {
            var now = DateTime.Now;

            string line1, line2;

            if(showUptimeMode > now)
            {
                TimeSpan uptime = GetUptime();
                string uptimeStr = uptime.ToString();
                int endIndex = uptimeStr.LastIndexOf('.');
                if(endIndex > Columns)
                    endIndex = Columns;

                line1 = "Uptime:   ";
                line2 = uptimeStr.Substring(0, endIndex);
            }
            else
            {
                line1 = now.ToString("yyyy-MM-dd");
                line2 = now.ToString("HH:mm:ss        ");
            }

            lcd.SetCursorPosition(0, 0);
            lcd.Write(line1);
            lcd.SetCursorPosition(0, 1);
            lcd.Write(line2);

            Thread.Sleep(100);
        }
    }

    private static TimeSpan GetUptime()
    {
        TimeSpan result = TimeSpan.MinValue;

        byte[] store = Ds1307.ReadRam();
        if (store.Length > 0)
        {
            var setTime = (DateTime) Reflection.Deserialize(store, typeof (DateTime));
            result = DateTime.Now - setTime;
        }

        return result;
    }
}

Pełny kot: DemoDS1307Init i DemoDS1307

3.09.2012

Odbiornik podczerwieni cd. - dekodowanie RC5

Jak już wcześniej napisałem, chyba najpowszechniejszym protokołem transmisji używanym w pilotach TV jest protokół RC5. Jak mamy piloty Philipsa lub "no name" to prawdopodobnie używają RC5. Można również taki pilot zakupić. Najtańsze już za około 10, 15 zł. Spróbujemy zdekodować taki sygnał.

Bardzo dokładnie protokół został opisany na stronie http://www.sbprojects.com/. Najważniejsze informacje jakie musimy wiedzieć:
  • bit ramki to pojedynczy impuls niski lub wysoki
  • ramka zawsze zaczyna się od bitu 0
  • pojedynczy bit ramki trwa 889us
  • każdy impuls powyżej 1.2*889us to dwa bity
  • odbieramy 28 bitów ramki
  • bity ramki zakodowane są kodem menchester - występują tylko pary 01 i 10
  • pary 01 zamieniamy na 1, a 10 na 0
  • po zamianie bitów ramki otrzymujemy 14 bitów danych
  • dane zawsze zaczynają się dwoma jedynkami i bitem toggle
  • adres urządzenia to kolejne 5 bitów, a kod przycisku to następne 6
Dobra. Sprawdzamy krótkim programikiem czy nasz pilot (piloty) używają RC5. Wykrywamy długość pierwszego impulsu 1 i następującego 0. Jeśli długości będą zbliżone do 889us, to jest to RC5 (pokaże się informacja w oknie output).

public class Program
{
    private const int MinWidth = 640; //us
    private const int MaxWidth = 1140; //us

    private static DateTime _nextCommand = DateTime.MinValue;
    private static int _okImpulses;

    public static void Main()
    {
        using(var receiver = new IRReceiver(Stm32F4Discovery.FreePins.PB5))
        {
            receiver.Pulse += ConsumePulse;
            Thread.Sleep(Timeout.Infinite);
        }
    }

    private static void ConsumePulse(TimeSpan width, bool state)
    {
        DateTime now = DateTime.Now;
        if (now < _nextCommand)
            return;

        long usWidth = width.TotalMicroseconds();
        //czekamy na 1
        if (state)
        {
            if (usWidth > MinWidth && usWidth < MaxWidth)
                _okImpulses++;
        }
        else
        {
            if (_okImpulses == 1)
            {
                if (usWidth > MinWidth && usWidth < MaxWidth)
                    Debug.Print("RC5!");

                _okImpulses = 0;
                _nextCommand = now.AddMilliseconds(500);
            }
        }
    }
}

Jeśli mamy zgodny pilot to budujemy dekoder RC5 do podglądania kodów klawiszy. Najciekawsza jest procedura dekodująca impulsy. Algorytm mógłby się opierać na tablicy 28 bitów ramki, które po zebraniu dekodowalibyśmy na 14 bitów danych. Jednak tablica nie jest potrzebna. Bity zbieramy i dekodujemy parami.

private void ConsumePulse(TimeSpan width, bool state)
{
    long usWidth = width.TotalMicroseconds();

    //poczatek ramki
    if (usWidth > NextFrame || _cnt == 0)
    {
        _cnt = 1;
        _prevBit = false; //zawsze zaczyna sie od 0
        _frame = 0;
        return;
    }

    if (_cnt == 0)
        return;

    //jesli szerokosc impulsu szersza niz 1 bit to dwa bity
    int bitCnt = usWidth > MaxOneBitTime ? 2 : 1;
    for (int i = 1; i <= bitCnt; i++)
    {
        _cnt++;

        if (_cnt%2 == 0)
            DecodeMenchester(_prevBit, state); //co dwa bity dekodujemy bit ramki
        else
        {
            //jesli mamy przedostatni bit to nie czekamy na ostatni mamy całą ramke
            if (_cnt == 27)
            {
                _cnt++;
                //ostatni bit jest przeciwienstwem przedostatniego
                //tutaj błąd w dekodowaniu menchester nie moze się pojawić
                DecodeMenchester(state, !state);
                //mamy ramke
                OnFrame(_frame);
                //zaczynamy od nowa
                _cnt = 0;
            }
            else
                _prevBit = state; //to tylko kolejny bit
        }
    }
}

private void DecodeMenchester(bool bit0, bool bit1)
{
    //kontrola czy jest ok
    if (!(bit0 ^ bit1))
    {
        Debug.Print("Invalid frame data");
        //jesli nie jest ok to zaczynamy ramke od nowa
        _cnt = 0;
    }

    //dekodowanie menchester: 01->1 , 10->0
    if (bit0) 
        return;

    _frame |= 1 << (Framelength - _cnt/2);
}

Jeśli zbierzemy bity całej ramki, to dekodujemy je na adres i kod przycisku i przekazujemy poprzez zdarzenie zainteresowanym.

private void OnFrame(int frame)
{
    int command = (frame & 0x3F);
    bool toggle = (frame & 0x0800) > 0;
    bool extended = (frame & 0x1000) == 0;
    int address = (frame & 0x1F) >> 6;
    //korekta dla extended RC5
    if (extended)
        command |= (1 << 6);

    //Debug.Print(" Addr:" + address + " Cmd:" + command + " Toggle: " + toggle);
    var args = new FrameEventArgs
                   {
                       Command = command,
                       Address = address,
                       Toggle = toggle
                   };

    if (Frame != null)
        Frame(this, args);
}

Krótki, główny programik pozwoli nam wyświetlić te informacje w oknie output.

public class Program
{
    private static DateTime _nextCommand = DateTime.MinValue;

    public static void Main()
    {
        using(var receiver = new IRReceiver(Stm32F4Discovery.FreePins.PB5))
        {
            var detector = new RC5Decoder(receiver);
            detector.Frame += (s, f) =>
                                  {
                                      DateTime now = DateTime.Now;
                                      if (now < _nextCommand)
                                          return;

                                      Debug.Print("Addr:" + f.Address +
                                                  " Cmd:" + f.Command +
                                                  " Toggle: " + f.Toggle);

                                      _nextCommand = now.AddMilliseconds(500);
                                  };

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

Pełny kot: DemoIRReceiverRC5

1.09.2012

Odbiornik podczerwieni (IRReceiver)

Bardzo łatwo można zbudować układ do sterowania STM32F4Discovery za pomocą pilota telewizyjnego (TV Remote). Na początek trochę teorii. 

Najbardziej powszechne są dwa rodzaje nadajników i odbiorników podczerwieni: 36kHz i 38kHz. Częstotliwości te są używane do modulowania impulsów danych. Odbiornik 36kHz nie będzie reagował na piloty 38kHz i na odwrót. Pilot wysyła impulsy, które po odebraniu przez odbiornik można zamienić na kod przycisku naciśniętego na pilocie. Sposób w jaki są zakodowane impulsy danych (liczba impulsów i ich szerokości), to protokół transmisyjny. Piloty mogą mieć różne protokoły transmisji (zależy od firmy). Najbardziej powszechnym jest chyba protokół RC5, wymyślony przez Philipsa. Więcej o tym można poczytać tutaj: TR Remote Control Theory

Ja akurat zakupiłem (w ciemno bo nie wiedziałem jakie mam piloty) odbiornik podczerwieni na 36kHz: TSOP31236, ale jest podobny na 38kHz: TSOP31238. Zresztą chyba i inne się nadadzą tylko trzeba w notach katalogowych popatrzeć jak je podłączyć. Ja mój podłączyłem tak jak na obrazku poniżej (kondensator i rezystor do testów można sobie darować):

TVIR + STM32F4Discovery
TVIR + STM32F4Discovery
Teraz kot. Konstruujemy klasę, która podłączona pod nóżkę odbiornika będzie informowała (EventHandler) o odebranych danych. Interesuje nas stan (1 lub 0) oraz czas trwania każdego impulsu. Obsługujemy każdą zmianę sygnału na pinie odbiornika (InterruptEdgeBoth).
public class IRReceiver : IDisposable
{
    public delegate void PulseEventHandler(TimeSpan width, bool state);
    public event PulseEventHandler Pulse;

    private readonly InterruptPort _receiverPort;
    private long _lastTick = DateTime.Now.Ticks;

    public IRReceiver(Cpu.Pin pin)
    {
        _receiverPort = new InterruptPort(pin, false,
                                          Port.ResistorMode.PullUp,
                                          Port.InterruptMode.InterruptEdgeBoth);

        _receiverPort.OnInterrupt += PortInterrupt;
    }

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

    private void PortInterrupt(uint port, uint state, DateTime time)
    {
        long current = time.Ticks;
        TimeSpan pulseWidth = TimeSpan.FromTicks(current - _lastTick);
        _lastTick = current;

        //Debug.Print("pulse " + pulseWidth.TotalMicroseconds()+ "us " + state);

        if (Pulse != null) 
            Pulse(pulseWidth, state == 1);
    }
}

Teraz program, który będzie reagował na impulsy z IRReceiver. Na razie bez rozpoznawania protokołu transmisyjnego i dekodowania impulsów. Będziemy włączać i wyłączać diody na płytce STM32F4Discovery dowolnym klawiszem na pilocie. Nie możemy jednak reagować na każdy impuls, bo w ramce transmisyjnej może ich być kilka czy kilkanaście i diody będą miały losowy stan. Robimy więc opóźnienie, tak aby reagować na pierwszy impuls, a na kolejny dopiero po 500 milisekundach.

public class Program
{
    private const int DelayBetweenCommands = 500; //ms

    public static void Main()
    {
        var leds = new[]
                       {
                           new OutputPort(Stm32F4Discovery.LedPins.Green, true),
                           new OutputPort(Stm32F4Discovery.LedPins.Orange, true),
                           new OutputPort(Stm32F4Discovery.LedPins.Red, true),
                           new OutputPort(Stm32F4Discovery.LedPins.Blue, true)
                       };

        var receiver = new IRReceiver(Stm32F4Discovery.FreePins.PB5);

        DateTime nextCommand = DateTime.MinValue;
        receiver.Pulse += (width, state) =>
                              {
                                  DateTime now = DateTime.Now;
                                  if (now < nextCommand)
                                      return;

                                  nextCommand = now.AddMilliseconds(DelayBetweenCommands);
                                  Toggle(leds);
                              };

        Thread.Sleep(Timeout.Infinite);
    }

    private static void Toggle(OutputPort[] leds)
    {
        bool state = !leds[0].Read();
        foreach (OutputPort led in leds)
            led.Write(state);
    }
}

Piloty w dłoń i celujemy w odbiornik. Naciskamy dowolne klawisze. U mnie w domu na trzy piloty dwa były 36kHz, a jeden prawdopodobnie 38kHz.

Pełny kot: DemoIRReceiver

28.08.2012

Termometr z LCD na STM32F4Discovery

Bez rozłączania poprzedniego układu (z DS18B20), możemy do płytki podłączyć wyświetlacz LCD. Najprościej podłączyć LCD zgodny z HD44780. Ja akurat miałem pod ręką YM1602C. Łączymy według schematu poniżej.
HD44780 + STM32F4Discovery

HD44780 + STM32F4Discovery
No dobra. Zapalamy wyświetlacz. Tylko jak? na szczęście ktoś już za nas zrobił bibliotekę do obsługi takich wyświetlaczy: μLiquidCrystal. Aha. Jest jeden problem. Jak już ściągniemy źródła, to trzeba w projekcie μLiquidCrystal zmienić framework na 4.2 (domyślnie jest 4.1 bodajże). No to start. Testujemy LCD krótkim programikiem.

var lcdProvider = new GpioLcdTransferProvider(Stm32F4Discovery.Pins.PD1, Stm32F4Discovery.Pins.PD2,
                                              Stm32F4Discovery.Pins.PD9, Stm32F4Discovery.Pins.PD11,
                                              Stm32F4Discovery.Pins.PD10, Stm32F4Discovery.Pins.PD8);

var lcd = new Lcd(lcdProvider);
lcd.Begin(16, 2); //columns, rows

//znaki specjalne
//http://www.quinapalus.com/hd44780udg.html
var customCharacters = new[]
                           {
                               new byte[] {0x00, 0x0a, 0x15, 0x11, 0x11, 0x0a, 0x04, 0x00}, //serce
                               new byte[] {0x04, 0x02, 0x01, 0x1f, 0x01, 0x02, 0x04, 0x00} //strzalka
                           };

//ladowanie znakow specjalnych
for (int i = 0; i < customCharacters.Length; i++)
    lcd.CreateChar(i, customCharacters[i]);

lcd.Clear();
lcd.Write("* Hello World! *");
Thread.Sleep(3000);

lcd.Clear();
lcd.WriteByte(0); //pierwszy znak specjalny
Thread.Sleep(2000);
lcd.WriteByte(1); //drugi znak specjalny
Thread.Sleep(3000);

//nastepna linia
lcd.SetCursorPosition(0, 1);
lcd.Write("#     Bye...   #");

 Pełny kot: DemoLcd

Na wyświetlaczu powinny pojawić się jakieś napisy. No niestety polskich znaków nie wyświetlimy. Jak wszystko jest OK to robimy kolejny display do termometru z poprzedniego postu. Najważniejsza jest procedura, która wyświetla temperaturę.
public void ShowTemperature(float temperature)
{
    _lcd.SetCursorPosition(0, 0);
    string txt = "Temperatura:";
    int padsCnt = Columns - txt.Length;
    _lcd.Write(txt + new string(' ', padsCnt));

    _lcd.SetCursorPosition(0, 1);

    string tempStr = temperature.ToString("F2");
    padsCnt = Columns - tempStr.Length;
    _lcd.Write(tempStr);
    _lcd.WriteByte(0);

    if (padsCnt > 0)
    {
        var pads = new String(' ', padsCnt);
        _lcd.Write(pads);
    }
}

Dodajemy jeszcze LCD do listy wyświetlaczy.

public Display()
{
    var debugDisplay = new DebugDisplay();
    var ledDisplay = new FourLedDisplay();
    var lcd = new LcdDisplay();
    _displays = new IDisplay[] {debugDisplay, ledDisplay, lcd};
}

W taki oto sposób mamy termometr z 3 wyświetlaczami: w oknie debug, na diodach z płytki i na LCD.

Pełny kot: DemoDS18B20

26.08.2012

Termometr na DS18B20 (1-Wire) i STM32F4Discovery

DS18B20 + STM32F4Discovery
DS18B20 + STM32F4Discovery
W internecie jest wiele bibliotek do DS18B20 i wiele przykładów jak pobierać informację o temperaturze. Moja biblioteka radzi sobie dobrze z wartościami ujemnymi i obsługuje zarówno DS18B20 jak i DS18S20. Poniżej zamieszczam tylko samo sedno biblioteki, czyli metody do odczytu temperatury z czujnika i konwersji na wartość.

Komunikacja z DSem musi odbywać się według ustalonej sekwencji: inicjalizacja (reset linii 1-Wire), wysłanie komendy ROM (np. match=0x55), wysłanie funkcji. Poniżej pomocnicza metoda, która taką sekwencję uruchamia.
private void RunSequence(byte command)
{
    if(_bus.TouchReset() == 0) //0 = no devices, 1 = device(s) exist
        throw new IOException("DS18X20 communication error");

    Write(MatchROMCommand);
    Write(_presensePulse);
    Write(command);
}

private void Write(params byte[] sendValues)
{
    foreach (byte sendValue in sendValues)
        _bus.WriteByte(sendValue);
}

Właściwa funkcja odczytu temperatury wysyła 2 komendy. Pierwsza inicjuje pomiar temperatury, a druga go pobiera. Właściwie to układ zwraca 2 bajty, które trzeba dopiero na temperaturę zamienić. Wyliczenie wartości temperatury uzależnione jest od typu układu i uwzględnia znak.
public float GetTemperature()
{
    RunSequence(ConvertTCommand);

    DateTime timeBarier = DateTime.Now.AddMilliseconds(TimeoutMiliseconds);
    while(_bus.ReadByte() == 0)
    {
        if(DateTime.Now > timeBarier)
            throw new IOException("DS18X20 read timeout");
    }

    RunSequence(ReadScratchpadCommand);

    int reading = _bus.ReadByte() | (_bus.ReadByte() << 8); //lsb msb
    bool minus = (reading & 0x8000) > 0;
    if (minus)
        reading = (reading ^ 0xffff) + 1; //uzupelnienie do 2 (U2)

    float result = _sSeries
                       ? CalculateS(reading)
                       : CalculateB(reading);
    if(minus)
        result = -result;

    return result;
}

private static float CalculateB(int reading)
{
    float result = 6*reading + reading/4; // multiply by (100 * 0.0625) or 6.25
    result = result/100;
    return result;
}

private static float CalculateS(int reading)
{
    float result = (reading & 0x00FF)*0.5f;
    return result;
}

No dobra. Czas temperaturę wyświetlić. Wyświetlacz będzie implementował interfejs IDisplay (od razu dla przykładu najprostsza implementacja):
public interface IDisplay
{
    void ShowTemperature(float temperature);
    void ShowError(string message);
}

public class DebugDisplay : IDisplay
{
    public void ShowTemperature(float temperature)
    {
        Debug.Print("Temperatura: " + temperature.ToString("F2") + " °C");
    }

    public void ShowError(string message)
    {
        if (message == null) 
            throw new ArgumentNullException("message");

        Debug.Print("Error: " + message);
    }
}

Dodajemy jeszcze jedną klasę pomocniczą implementującą IDisplay. Z niej będziemy korzystali w programie. Dzięki temu, w przyszłości, łatwo będzie można dodać nowe wyświetlacze. Można sobie nawet wyobrazić taką sytuację, że w zależności od np. ustawionych zworek raz będzie działał jeden wyświetlacz, a raz inny.
public class Display : IDisplay
{
    private readonly IDisplay[] _displays;

    public Display()
    {
        var debugDisplay = new DebugDisplay();
        _displays = new IDisplay[] {debugDisplay};
    }

    public void ShowTemperature(float temperature)
    {
        foreach (IDisplay display in _displays)
            display.ShowTemperature(temperature);
    }

    public void ShowError(string message)
    {
        if (message == null) 
            throw new ArgumentNullException("message");

        foreach (IDisplay display in _displays)
            display.ShowError(message);
    }
}

Teraz wszystko trzeba poskładać w całość. Na początku głównej procedury programu mamy inicjalizację OneWire i wykrycie czujników. W moim przypadku zawsze jeden, więc bierzemy pierwszy lepszy.
var op = new OutputPort(Stm32F4Discovery.FreePins.PA15, false);
var ow = new OneWire(op);

IDisplay display = new Display();
DS18X20[] devices = DS18X20.FindAll(ow);
if (devices.Length == 0)
{
    display.ShowError("Brak DS18B20");
    return;
}

DS18X20 tempDev = devices[0];

Samo sedno programu stanowi kot wykonywany w nieskończoność, który pobiera temperaturę i wyświetla wartość na wyświetlaczu. Dzięki przechwyceniu wyjątku jest odporny na błędy transmisji (np. wyciągnięcie kabelka sygnałowego).

for (;;)
{
    float currentTemp = 0;
    string exceptionMsg = String.Empty;
    try
    {
        currentTemp = tempDev.GetTemperature();
    }
    catch(IOException ex)
    {
        exceptionMsg = ex.Message;
    }

    if (exceptionMsg.Length == 0)
        display.ShowTemperature(currentTemp);
    else
        display.ShowError(exceptionMsg);

    Thread.Sleep(1000);
}

Jeśli wszystko mamy dobrze podłączone i .NET MF z OneWire w STM32F4Discovery, to otrzymamy w oknie debug informacje o aktualnej temperaturze:

Temperatura: 26.75 °C
Temperatura: 26.81 °C

Możemy też skonstruować i dodać do programu drugi wyświetlacz - na diodach LED, które znajdują się na płytce. Każda kolejna dioda będzie się zapalać po przekroczeniu określonej temperatury. Najciekawszym elementem klasy jest chyba konstruktor, w którym określamy progi zapalania się poszczególnych diod oraz procedura realizująca wyświetlanie temperatury.
public FourLedDisplay()
{
    var blueLed = new OutputPort(Stm32F4Discovery.LedPins.Blue, false);
    var greenLed = new OutputPort(Stm32F4Discovery.LedPins.Green, false);
    var orangeLed = new OutputPort(Stm32F4Discovery.LedPins.Orange, false);
    var redLed = new OutputPort(Stm32F4Discovery.LedPins.Red, false);

    _ranges = new[]
                  {
                      new LedRange(blueLed, 20),
                      new LedRange(greenLed, 25),
                      new LedRange(orangeLed, 30),
                      new LedRange(redLed, 35)
                  };
}

public void ShowTemperature(float temperature)
{
    if(_timer != null)
        _timer.Change(Timeout.Infinite, BlinkPeriod);

    foreach (LedRange range in _ranges)
        range.Check(temperature);
} 

Nowy wyświetlacz musimy jeszcze tylko dodać do listy w konstruktorze klasy Display.
public Display()
{
    var debugDisplay = new DebugDisplay();
    var ledDisplay = new FourLedDisplay();
    _displays = new IDisplay[] {debugDisplay, ledDisplay};
}

Pełny kot: DemoDS18B20

21.08.2012

OneWire i STM32F4Discovery - mówisz i masz

Kompilacja PK opanowana. Czas dodać 1-Wire. Bułka z masłem. Żeby się za dużo nie narobić trzeba zlikwidować błędy w PK (wersja QFE2). Otwieramy (może być notatnik lub inny zaawansowany edytor xml) plik C:\MicroFrameworkPK_v4_2\Framework\Features\Analog_DA_HAL.libcatproj. Guid w linijce Guid zmieniamy z  54DABCD1-8C9D-485c-8C48-8ECEE7D27454 na

00E400EA-0018-00CA-B59A-A2B3F9586139

Jest to ten sam guid z linijki MFComponent z pliku C:\MicroFrameworkPK_v4_2\DeviceCode\Drivers\Stubs\Processor\stubs_DA\dotNetMF.proj. Po prostu w pliku Analog_DA_HAL.libcatproj jest pomylony.

Następnie otwieramy plik C:\MicroFrameworkPK_v4_2\Framework\Features\OneWire_PAL.libcatproj i w linijce z StubLibrary zmieniamy guid z 24E1C771-7E4F-471C-A85C-78D693C259B6 na

238A3F72-46C6-4267-88BE-D15C09594103

A ten guid odpowiada ProjGuid z pliku C:\MicroFrameworkPK_v4_2\DeviceCode\pal\OneWire\Stubs\dotNetMF.proj (stubs to nic innego jak zaślepki). Nie wiem skąd się wziął w oryginale guid od zaślepki piezo? (C:\MicroFrameworkPK_v4_2\DeviceCode\pal\piezo\stubs\dotNetMF.proj).

W tym samym pliku zmieniamy jeszcze jeden guid. W linijce FeatureAssociations jest guid C9D30638-8B83-42B7-90A6-C96899B185F1 projektu SD (z pliku C:\MicroFrameworkPK_v4_2\Framework\Features\SD.featureproj) zamiast guidu

3401dd69-cab5-45fc-a759-1d1eded247c7

z projektu C:\MicroFrameworkPK_v4_2\Framework\Features\OneWire.featureproj.

Uff! Najgorsze za nami. Odpalamy program C:\MicroFrameworkPK_v4_2\tools\bin\SolutionWizard\SolutionWizard.exe i nic nie zmieniamy...

... aż do tego miejsca. Tutaj zaznaczamy OneWire.
Wygląd jak na obrazku poniżej oznacza, że wszystko jest ok. Tutaj zaznaczmy "Show All Choices" i ...
... patrzymy (lub zaznaczmy) czy jest wybrany dobry pal.
To wszystko. SolutionWizard zmienił dokładnie 2 pliki: C:\MicroFrameworkPK_v4_2\Solutions\Discovery4\TinyBooter\TinyBooter.proj (uzupełnił stub od analaog_DA) oraz C:\MicroFrameworkPK_v4_2\Solutions\Discovery4\TinyCLR\TinyCLR.proj (dodał wpisy od OneWire). Kto ciekawy co się zmieniło niech sobie porówna. Kompilujemy PK dokładnie tak samo jak wcześniej opisałem. W wyniku otrzymujemy pliki:
Wgrywamy je na płytkę przy pomocy ST_LINK i MFDeploy, tak jak opisałem wcześniej. Czas na test. Uruchamiamy ponownie krótki programik:
var op = new OutputPort(Stm32F4Discovery.FreePins.PA15, false);
var ow = new OneWire(op);

ArrayList devices = ow.FindAllDevices();
Debug.Print("Found " + devices.Count);
Wynik w postaci: Found 0 bez żadnego wyjątku to bardzo dobry znak! OneWire powinno działać bez problemu. Montujemy więc szybko układ z czujnikiem temperatury DS18B20.

1-wire stm32f4discovery
Uruchomienie programu takiego jak powyżej pokaże Found 1. OneWire działa poprawnie.

Jeden kabelek USB

Denerwujące jest podłączanie dwóch kabelków USB do STM32F4Discovery. Teoretycznie USB CN1 jest potrzebne tylko podczas wgrywania pliku tinybooter za pomocą ST-LINK. Praktycznie zawsze musi być podpięte, bo z tego gniazda brane jest zasilanie. Ale istnieje na to sposób. Wystarczy podłączyć PA9 do +5V, jak na rysunku poniżej:
stm32f4discovery
Teraz wystarczy, że będziemy podpinać USB CN5 i wszystko powinno ładnie działać. Nie wiem tylko czy podłączenie na raz CN1 i CN5 jest bezpieczne. Na wszelki wypadek ja wpinam albo CN1 albo CN5 - nigdy obydwa, bo i nie mam na razie takiej potrzeby.

20.08.2012

Kompilacja Porting Kit dla STM32F4Discovery

Postanowiłem skompilować PK. Na razie czysty, bez żadnych zmian, aby nabrać wprawy i zobaczyć jak to wszystko pójdzie.

Podstawową sprawą jest użycie do kompilacji Keil MDK-ARM w wersji Standard. Dla wersji 4.12 na 100% poprawnie się wszystko skompiluje. Program instalujemy na dysku C, reszta standardowo. Powinna powstać taka struktura katalogów: C:\Keil\ARM. Następnie trzeba ściągnąć i zainstalować Porting Kit 4.2 (RTM QFE2). PK również trzeba zainstalować na dysku C (powstanie katalog C:\MicroFrameworkPK_v4_2). Teraz trzeba ściągnąć NETMF for STM32 (F4 Edition) Release 4.2 QFE2 RTM i wypakować wszystko jak idzie do katalogu PK, tak aby nadpisać lub dodać nowe pliki i katalogi. Nas interesuje katalog C:\MicroFrameworkPK_v4_2\Solutions\Discovery4.

PK jest przygotowane do kompilacji. Otwieramy okno konsoli cmd i przechodzimy do katalogu C:\MicroFrameworkPK_v4_2. Uruchamiamy polecenie:  setenv_mdk 4.12
Teraz można odpalić właściwą kompilację poleceniem:

msbuild /p:flavor=DEBUG;memory=FLASH Solutions\Discovery4\dotNetMF.proj

Zamiast DEBUG można podać RELEASE - zależy którą wersję chce się otrzymać. Rozpocznie się kompilacja.
Na koniec zobaczymy podsumowanie. Nie powinno być żadnych błędów.
Interesujące są 3 pliki:
  1. C:\MicroFrameworkPK_v4_2\BuildOutput\THUMB2FP\MDK4.12\le\FLASH\debug\Discovery4\bin\Tinybooter.hex
  2. C:\MicroFrameworkPK_v4_2\BuildOutput\THUMB2FP\MDK4.12\le\FLASH\debug\Discovery4\bin\tinyclr.hex\ER_CONFIG
  3. C:\MicroFrameworkPK_v4_2\BuildOutput\THUMB2FP\MDK4.12\le\FLASH\debug\Discovery4\bin\tinyclr.hex\ER_FLASH
Pliki wgrywamy standardowo, zgodnie z opisem w: Przygotowanie STM32F4Discovery do pracy.
Jeśli kompilujemy od nowa trzeba usunąć katalog C:\MicroFrameworkPK_v4_2\BuildOutput.

16.08.2012

Nowy NET MF i port dla STM32F4

Dwa dni temu ukazała się nowa wersja .NET Micro Framework (.NET MF 4.2 RTM QFE2), a dzisiaj wersja NETMF dla STM32 F4

W wersji QFE2 dodano obsługę WinUSB oraz wyjścia analogowego. Poprawiono, miedzy innymi, błędy w StringBuilder.Append, StreamReader, File.Exists, VolumeInfo.Format oraz inne błędy związane z File System i SerialPort. Zaimplementowano AppDomain.GetAssemblies i przyspieszono działanie niektórych składników. Zmieniono też sposób przygotowania do kompilacji PK - jeden plik do ustawiania zmiennych.

1.08.2012

OneWire i STM32F4Discovery

Dzisiaj postanowiłem podłączyć czujnik temperatury DS18B20 do STM32F4Discovery. Niestety moje próby spełzły na niczym, ponieważ pliki hex (pliki binarne z NETMF) dla tej płytki ściągnięte ze strony http://netmf4stm32.codeplex.com nie zawierają obsługi protokołu 1-Wire. Możemy się o tym przekonać uruchamiając malutki programik:
var op = new OutputPort(Stm32F4Discovery.FreePins.PA15, false);
var ow = new OneWire(op);

ArrayList devices = ow.FindAllDevices();
W wyniku otrzymamy:

An unhandled exception of type 'System.NotSupportedException' occurred in Microsoft.SPOT.Hardware.OneWire.dll

Wiedziałem, że trzeba będzie kiedyś spróbować skompilować PK (Porting Kit) samemu, ale nie myślałem, że nastąpi to tak szybko. Na razie stopień skomplikowania PK mnie odstrasza.

29.07.2012

STM32F4Discovery + NETMF żyje!

Postanowiłem napisać bardziej zaawansowany program niż mruganie jedną diodą. Mrugać będzie wszystkie 4 diody! To będzie taki Hello World dla .NET Micro Framework na STM32F4Discovery. Cztery diody na płytce będą się kręcić w lewo lub w prawo. Naciśniecie przycisku (User button) będzie zmieniać kierunek. Dodatkowo można podłączyć jeszcze jedną diodę led (jak na rysunku poniżej). Ta dioda będzie wskazywała kierunek obrotów.
Podłączenie diody led do STM32F4Discovery
Na początek trzeba zrobić klasę pomocniczą z definicjami pinów po to, aby za każdym razem ich nie liczyć, no i nie wpisywać na sztywno numerków. Dodatkowo klasę umieściłem w oddzielnej bibliotece. 'Worek' na dodatkowe funkcje i definicje zawsze się przydaje.
public class Stm32F4Discovery
{
    static Stm32F4Discovery()
    {
        HardwareProvider.Register(new Stm32F4DiscoveryHardwareProvider());
    }

    private sealed class Stm32F4DiscoveryHardwareProvider : HardwareProvider
    {
    }

    public class Pins
    {
        // ReSharper disable InconsistentNaming
        public const Cpu.Pin GPIO_NONE = Cpu.Pin.GPIO_NONE;

        public const Cpu.Pin PA0 = 0*16 + 0; //0
        public const Cpu.Pin PA1 = (Cpu.Pin) (0*16 + 1); //1 ADC0 COM2(rts)
        public const Cpu.Pin PA2 = (Cpu.Pin) (0*16 + 2); //2 ADC1 COM2(tx)
        public const Cpu.Pin PA3 = (Cpu.Pin) (0*16 + 3); //3 ADC2 COM2(rx)
        public const Cpu.Pin PA4 = (Cpu.Pin) (0*16 + 4); //4

        //...cała definicja pinów jest za długa żeby ją tutaj zmieścić        

        public const Cpu.Pin PE14 = (Cpu.Pin) (4*16 + 14); //78 PWM7
        public const Cpu.Pin PE15 = (Cpu.Pin) (4*16 + 15); //79
        // ReSharper restore InconsistentNaming
    }

    public class ButtonPins
    {
        public const Cpu.Pin User = Pins.PA0;
    }

    public class LedPins
    {
        public const Cpu.Pin Green = Pins.PD12; //60
        public const Cpu.Pin Orange = Pins.PD13; //61
        public const Cpu.Pin Red = Pins.PD14; //62
        public const Cpu.Pin Blue = Pins.PD15; //63
    }

    public class FreePins
    {
        // ReSharper disable InconsistentNaming
        public const Cpu.Pin PA1 = Pins.PA1;
        public const Cpu.Pin PA2 = Pins.PA2;

        //...cała definicja pinów jest za długa żeby ją tutaj zmieścić  

        public const Cpu.Pin PE14 = Pins.PE14;
        public const Cpu.Pin PE15 = Pins.PE15;
        // ReSharper restore InconsistentNaming
    }

    //... i jeszcze inne definicje
}
Następnie zrobiłem oddzielna klasę, która odpowiada tylko za sterowanie diodami led. Dokładnie chodzi o kręcenie ich w prawo lub w lewo:
internal class LedRotator
{
    int _currentIndex;
    private readonly OutputPort[] _leds;
    private readonly int _maxIndex;
    private bool _right = true;
    public bool Right
    {
        get { return _right; }
    }

    public LedRotator(params OutputPort[] leds)
    {
        if (leds == null) 
            throw new ArgumentNullException("leds");

        _maxIndex = leds.Length - 1;
        _leds = leds;
    }

    // ReSharper disable FunctionNeverReturns
    public void Run()
    {
        //krecimy diodamy
        for (;;)
        {
            _leds[_currentIndex].Write(true);
            Thread.Sleep(120);
            _leds[_currentIndex].Write(false);
            _currentIndex = GetNextIndex();
        }
    }
    // ReSharper restore FunctionNeverReturns

    private int GetNextIndex()
    {
        if(Right)
            return _currentIndex == _maxIndex ? 0 : _currentIndex + 1;

        return _currentIndex == 0 ? _maxIndex : _currentIndex - 1;
    }

    public void ChangeDirection()
    {
        _right = !_right;
    }
}
W głównej klasie Program mamy najpierw definicje dwóch portów: UserButton oraz DirectionLed. Port UserButton, jak popatrzymy do dokumentacji STM32F4Discovery, ma rezystor podpięty do masy - dlatego jest pull-down.
private static readonly InterruptPort UserButton = new InterruptPort(Stm32F4Discovery.ButtonPins.User,
                                                                     false,
                                                                     Port.ResistorMode.PullDown,
                                                                     Port.InterruptMode.InterruptEdgeLow);

private static readonly OutputPort DirectionLed = new OutputPort(Stm32F4Discovery.FreePins.PA15, false);
Natomiast w samej głównej procedurze Main jest deklaracja portów diod led zgrupowanych w tablicy. Tablica jest przekazywana do konstruktora klasy LedRotator. Dzięki temu można w łatwy sposób zmieniać liczbę diod do sterowania.
var leds = new[]
                {
                    new OutputPort(Stm32F4Discovery.LedPins.Green, true),
                    new OutputPort(Stm32F4Discovery.LedPins.Orange, true),
                    new OutputPort(Stm32F4Discovery.LedPins.Red, true),
                    new OutputPort(Stm32F4Discovery.LedPins.Blue, true)
                };

var rotator = new LedRotator(leds);
DirectionLed.Write(rotator.Right);
Kolejnym elementem jest podpięcie pod InterruptPort (User button) procedury obsługi. Definicja procedury jest zapisana przy użyciu wyrażenia lambda. Po każdym naciśnięciu przycisku zmieniany jest kierunek obrotów, oraz stan dodatkowej diody. Dodatkowa instrukcja (ClearInterrupt) powoduje skasowanie stanu portu, tak aby zareagował na kolejne zdarzenie.
UserButton.OnInterrupt += (u, data2, time) =>
                                {
                                    rotator.ChangeDirection();
                                    DirectionLed.Write(rotator.Right);
                                    UserButton.ClearInterrupt();
                                };

Blink(leds, 6);
rotator.Run();
Po definicji procedury obsługi przycisku jest uruchomienie LedRotatora. Dodatkowa procedura Blink, mruga wszystkimi diodami - to sekwencja startowa.
private static void Blink(OutputPort[] leds, int blinkCnt)
{
    //mrugamy diodami kilka razy (diody musza zgasnac)
    bool ledState = leds[0].Read();
    for (; blinkCnt > 0 || ledState; blinkCnt--)
    {
        ledState = !ledState;
        foreach (OutputPort led in leds)
            led.Write(ledState);

        Thread.Sleep(1000);
    }
}

Kot dostępny na: https://kodfilemon.googlecode.com/svn/trunk/STM32F4Discovery_Demo/DemoBlink1 ( Checkout )

28.07.2012

Prawda oczywista: CPU.Pin to nie nóżka procesora

Lamer to lamer. Pierwsza skucha. Chciałem wypróbować na szybko STM32F4Discovery, więc napisałem najprostszy program: mruganie zielonej led. Popatrzyłem do dokumentacji płytki: zielony led na porcie PD12 - nóżka 59 i wyszedł taki kot:

using System.Threading;
using Microsoft.SPOT.Hardware;

namespace Led1
{
    public class Program
    {
        public static void Main()
        {
            const Cpu.Pin ledPin = (Cpu.Pin) 59;
            const int delay = 1000;

            using (var ledPort = new OutputPort(ledPin, false))
            {
                while (true)
                {
                    ledPort.Write(true);
                    Thread.Sleep(delay);
                    ledPort.Write(false);
                    Thread.Sleep(delay);
                }
            }
        }
    }
}

Kompiluję, we właściwościach projektu ustawiam odpowiednią konfigurację.


Uruchamiam przez F5, w oknie wynikowym poleciały informacje o wysłaniu kodu do płytki, przeleciały informacje od debuggera, czekam, czekam, czekam i nic. Nie mruga! Nie działa! Dlaczego?

CPU.Pin to nie nóżka procesora. Pisząc programy na mikro-kontrolery trzeba operować na pinach GPIO. Piny GPIO są zgrupowane w porty. Akurat STM32F4 ma 5 portów (od 0 do 5) oznaczonych: A, B, C, D i E. Każdy port ma 16 pinów (od 0 do 15): PA0..PA15, PB0..PB15, PC0...PC15 itd. (dlatego tak oznaczone są szpilki złączy goldpin na płytce). Na tej podstawie możemy określić numery pinów:
  • PA0 =   0*16 + 0   = 0
  • PA1 =   0*16 + 1   = 1
  • PA2 =   0*16 + 2   = 2
  • ...
  • PA15 = 0*16 + 15 = 15
  • PB0 =   1*16 + 0   = 16
  • ...
  • PB15 = 1*16 + 15 = 31
  • PC0 =   2*16 + 0   = 32
  • ...
  • PC15 = 2*16 + 15 = 47
  • ...
  • ...
  • i ostatni
  • PE15 = 4*16 + 15 = 79
Dla zielonej diody (PD12) CPU.Pin będzie równy: 3*16 + 12 = 60. Po podstawieniu do kodu wszystko działa tak jak należy.
const Cpu.Pin ledPin = (Cpu.Pin) 60;

Kot dostępny na: https://kodfilemon.googlecode.com/svn/trunk/STM32F4Discovery_Demo/DemoLed1/ ( Checkout )

27.07.2012

Przygotowanie STM32F4Discovery do pracy z NETMF

Płytka STM32F4Discovery wygląda jak na zdjęciu poniżej. Jak widać urządzenie nie jest zbyt duże.

STM32F4Discovery

Przez producenta mamy wgrany program demonstracyjny. Po podłączeniu płytki przez USB CN1 (zasilanie płytki) powinny zacząć mrugać diody led. Jeśli teraz podłączymy również płytkę przez port USB CN5 do komputera i naciśniemy przycisk użytkownika (ten niebieski), to w komputerze zostanie wykryta dodatkowa mysz i poruszając płytką będziemy mogli sterować kursorem. Jeśli demonstracja przebiegła pomyślnie to płytka jest w porządku.

Przygotowujemy płytkę do wgrania NETMF. Rozłączamy wszystkie USB i ściągamy oraz instalujemy STM32 ST-LINK utility  (zakładka "Design support", na samym dole). Drugi plik nie jest potrzebny, bo instalator ST-LINK instaluje driver USB. Podłączamy płytkę przez USB CN1. Może się zdarzyć, że po ponownym podłączeniu płytki nie zostanie ona wykryta poprawnie, wówczas w menadżerze urządzeń trzeba usunąć nieznane urządzenie i uruchomić ponowne skanowanie zmian sprzętu. Płytka powinna zostać poprawnie wykryta jako "STMicroelectronics STLink dongle" w kontrolerach USB. Teraz można uruchomić ST-LINK. I wgrać tinybooter.

Ze strony projektu NETMF for STM32 (F4 Edition) ściągamy stm32f4discovery.zip oraz STM32_USB_drivers_(for_evaluation_purposes_only).zip. Uruchamiamy ST-LINK i z menu Target wybieramy Connect. Powinno pojawić się coś takiego:


Następnie znowu z menu Target wybieramy Erase Chip i po skończeniu operacji ponownie z menu Target wybieramy tym razem pozycję Program. Wybieramy plik Tinybooter.hex wypakowany z stm32f4discovery.zip i wciskamy przycisk Program. Koniecznie po tej operacji musimy zresetować płytkę przez naciśnięcie czarnego przycisku na niej. Podłączamy płytkę przez USB CN5 do komputera i po wykryciu urządzenia instalujemy driver z pliku STM32_USB_drivers_(for_evaluation_purposes_only).zip. Od teraz USB CN1 służy tylko do zasilania, a USB CN5 do komunikacji z NETMF czyli wgrywania programów i debugowania.

W programach w menu start Windowsa musimy teraz znaleźć "Microsoft .NET Micro Framework 4.2"->"Tools". Z tego katalogu uruchamiamy MFDeploy.exe. Wybieramy w Device USB, a wartość obok powinna wskoczyć automatycznie. Wykonujemy Ping:


W sekcji "Image File" wybieramy plik ER_CONFIG.hex wypakowany z stm32f4discovery.zip i uruchamiamy "Deploy". Następnie to samo robimy z plikiem ER_FLASH.hex. Po zakończeniu operacji (potrwa trochę dłużej), wykonujemy Ping:


Płytka STM32F4Discovery jest przygotowana do działania z .NET Micro Framework.

24.07.2012

Prosty emulator w .NET Micro Framework

Oczekując na płytkę STM32F4Discovery postanowiłem sprawdzić możliwość zbudowania własnego emulatora. Tak jest to możliwe! Możemy wykonać programowy emulator urządzenia i rozwijać oraz testować programy bez udziału fizycznej elektroniki.

Emulator jest bardzo prosty. Ma dwa przyciski, 5 kwadratów udających diody led oraz textbox, który emuluje port szeregowy:

Led i przycisk są zrobione jako kontrolki wizualne (UserControl), a port COM jako komponent (Component), a co za tym idzie można prosto projektować (przeciągając na formatkę) wygląd emulatora. (Uwaga: jeśli w toolboxie nie pokazują się kontrolki sprawdzić rozwiązania).


Kod kontrolki led i przycisku jest następujący:

public partial class LedControl : GpioUserControl
{
    public LedControl()
    {
        InitializeComponent();

        Port.ModesExpected = GpioPortMode.OutputPort;
        Port.ModesAllowed = GpioPortMode.OutputPort;
        Port.OnGpioActivity += Port_OnGpioActivity;
    }

    void Port_OnGpioActivity(GpioPort sender, bool edge)
    {
        Action action = () =&gt; BackColor = edge ? Color.Red : Color.White;
        UpdateUI(action);
    }
}
public partial class ButtonControl : GpioUserControl
{
    public ButtonControl()
    {
        InitializeComponent();

        Port.ModesExpected = GpioPortMode.InputPort;
        Port.ModesAllowed = GpioPortMode.InputPort;
    }

    private void Button1Click(object sender, System.EventArgs e)
    {
        Port.Write(true);
        Port.Write(false);
    }
}

Kontrolki dziedziczą ze wspólnego komponentu GpioUserControl, który oprócz tego że zawiera wspólne elementy dla potomnych to implementuje magiczny interfejs IEmulatorComponent. Interfejs ten jest potrzebny po to, aby automatycznie zarejestrować komponenty emulatora (EmulatorComponent) w emulatorze.

Standardowo rejestrację komponentów trzeba robić ręcznie w pliku xml (Emulator.config). Jednak przez dodanie takiego kodu w formatce emulatora, nastąpi ich automatyczna rejestracja niezależnie od ilości.

public MainForm(Microsoft.SPOT.Emulator.Emulator emulator)
{
    InitializeComponent();

    foreach (IEmulatorComponent control in Controls.OfType<IEmulatorComponent>())
        emulator.RegisterComponent(control.GetComponent());

    foreach (IEmulatorComponent control in components.Components.OfType<IEmulatorComponent>())
        emulator.RegisterComponent(control.GetComponent());
}

Przed uruchomieniem właściwego programu trzeba skompilować projekt emulatora, a następnie wybrać odpowiednio platformę do uruchomienia:


Kot dostępny na: https://kodfilemon.googlecode.com/svn/trunk/EmulatorTest/ ( Checkout )

20.07.2012

Płytka do .NET Micro Framework

Trenowanie na sucho nie sprawia jednak takiej frajdy jak uruchomienie programu w rzeczywistym systemie. Postanowiłem się rozejrzeć za czymś co pozwoli mi w prosty sposób testować programy na prawdziwym procesorze. 

Od razu odrzuciłem możliwość zmontowania samemu płytki, lutowania elementów itp. Wydaje mi się, że trzeba mieć trochę doświadczenia w konstrukcji układów mikroprocesorowych, aby samemu (nawet wg. schematów i plików z gotowymi PCB) wykonać taką płytkę. Lutowanie procesora w obudowie LQFP100 też na razie jest poza moim zasięgiem manualnym. Jedynym wyjściem pozostaje zakup gotowej płytki.

Gotowe płytki do NETMF posiada wiele firm w swojej ofercie. Po rozpoznaniu dostępności (zakup w Polsce), ceny (rozbudowane i drogie płytki ewaluacyjne odrzuciłem), popularności (przykłady i wsparcie na internecie) i możliwości (dużo portów, pamięci, jakieś gotowe diody czy przyciski) na polu boju zostały następujące pozycje:
Po głębszym rozpoznaniu tematu okazało się, że najlepsze według mnie pozycje to FEZ Cerberus i Netduino Go. Obydwie płytki działają na tym samym procesorze STM32F4. Na obu płytkach dla użytkownika dostępne jest około 300K FLASH na program (całość 1MB), około 100KB RAMu, dostępne interfejsy: USB, SPI, I2C, UART (port szeregowy), CAN, wejścia/wyjścia analogowe, PWM, OneWire itp. Możliwe jest również podłączenie modułów z interfejsem sieciowym. Zastanowiło mnie to, że obydwie płytki zbudowane są na procesorze STM32F4 (chyba nie bez powodu). Wówczas przypomniałem sobie, że gdzieś czytałem o STM32F4Discovery.

STM32F4Discovery to płytka testowa produkowana przez producenta procesorów STM32F4 - STMMicroelectronics. Na niej znajduje się dokładnie taki sam procesor jak na wyżej wspomnianych FEZ Cerberus i Netduino Go (ma tylko inny oscylator kwarcowy). Na płytce są już zamontowane (do wykorzystania): 4 diody led, jeden przycisk, czujnik położenia, mikrofon i przetwornik dźwięku. Do tego wszystkie porty wyprowadzone są na goldpiny i łatwo można się pod nie podłączać. Jeszcze mało? Cena to około 90 zł z przesyłką w Polsce, no i przede wszystkim na stronie netmf4stm32.codeplex.com specjalnie przygotowana wersja NETMF dla tej płytki! Myślę, że ta płytka będzie najbardziej sensowna, a jak coś nie wyjdzie to niewiele pieniędzy się zmarnuje. Jedyny mankament jaki widzę to brak gotowego portu ethernet, ale wszystko po kolei. Na to jeszcze przyjdzie czas...

Dodane po zakupie: do podłączenia płytki potrzebne są 2 kabelki USB. Jeden 'type A to mini-B' (u mnie spasował kabelek od aparatu cyfrowego) i drugi 'type A to micro-B' (ten musiałem dokupić). Bez tego ani rusz. Wtyki mini-B i micro-B lądują w gniazdach na płytce, a wtyki A (te tradycyjne USB) w komputerze.

30.06.2012

Pierwszy program NETMF

Aby rozpocząć programować w NETMF nie trzeba kupować od razu żadnej elektroniki, procesorów itp. Nie trzeba również umieć lutować (na razie). Po prostu ze strony NETMF ściągamy aktualny SDK, instalujemy i możemy napisać pierwszy program!. Na przykład coś takiego:

using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

namespace MFConsoleApplication1
{
    public class Program
    {
        private const int Delay = 500;

        public static void Main()
        {
            Debug.Print("start");

            var led1 = new OutputPort((Cpu.Pin)3, false);

            using (var serial = new SerialPort("COM1"))
            {
                Debug.Print("open serial port");
                serial.Open();
                WriteHello(serial);
            
                while (true)
                {
                    ChangePort(led1, serial);
                }
            }
        }

        private static void WriteHello(SerialPort serial)
        {
            var sb = new StringBuilder("Hello ");
            sb.Append(SystemInfo.OEMString);
            sb.Append(" Version: ");
            sb.Append(SystemInfo.Version);
            sb.Append(" Model: ");
            sb.Append(SystemInfo.SystemID.Model);

            if (SystemInfo.IsEmulator)
                sb.Append(" running on emulator");

            sb.AppendLine(String.Empty);

            string msg = sb.ToString();
            Debug.Print(msg);

            byte[] msgBuff = Convert(msg);
            serial.Write(msgBuff, 0, msg.Length);
        }

        private static void ChangePort(OutputPort port, SerialPort serial)
        {
            bool nextState = !port.Read();
            port.Write(nextState);

            var sb = new StringBuilder("Port ");
            sb.Append(port.Id);
            sb.AppendLine(nextState ? " on" : " off");

            string msg = sb.ToString();
            Debug.Print(msg);

            byte[] msgBuff = Convert(msg);
            serial.Write(msgBuff, 0, msgBuff.Length);

            Thread.Sleep(Delay);
        }

        private static byte[] Convert(string message)
        {
            byte[] result = Encoding.UTF8.GetBytes(message);
            return result;
        }
    }
}

Program najpierw przedstawia się na porcie COM, a następnie w nieskończoność mruga (powiedzmy diodą) na porcie 3 procesora jednocześnie wysyłając informację na port szeregowy:

Hello Microsoft Copyright (C) Microsoft Corporation. All rig Version: 4.2.0.0 Model: 2 running on emulator

Port 3 on

Port 3 off

27.06.2012

Początki w .NET Micro Framework

Zapomniałem zupełnie, że kiedyś założyłem bloga. Teraz jest okazja żeby go użyć, bo potrzebuję miejsca do opisywania moich doświadczeń z mikro-kontrolerami i programowaniem w NETMF. A i może się komuś jeszcze przyda.

Kiedyś natrafiłem na jakąś stronę o .NET Micro Framework i jakież było moje zdziwienie, że nic o tym wcześniej nie słyszałem. Używam Visual Studio na co dzień i używam C#, ale programowanie mikro-kontrolerów to coś zupełnie nowego i niesamowitego. Jeśli mogę używać Visual Studio i Resharpera (Reflector na pewno też się przyda) do edycji i uruchamiania oprogramowania, które działa na jakiś płytkach elektronicznych to ja chcę to robić (czytaj: spróbować się pobawić)! A do tego pisać w C#, a nie w C/C++ czy asemblerze (przypomniały mi się czasy 8051). Oczywiście zdaję sobie sprawę z tego, że program będzie mniej wydajny niż napisany w C/C++, no ale coś za coś. Poza tym nie będą to jakieś systemy przemysłowe działające w czasie rzeczywistym. Do mrugania diodami lub do zrobienia sterownika grupy czytników RFID (tak mi teraz przyszło do głowy) chyba w zupełności wystarczy.

NETMF działa jednak tylko na mikro-kontrolerach (a może procesorach) ARM (ARM7, ARM Cortex M3, ARM Cortex M4, ARM9). Czyli o programowaniu w NETMF na mikro-kontrolery AVR (popularne AtmegiXXX) możemy zapomnieć. Ceny tych procesorów też są nie małe: około 60, 70zł za sztukę (np. za STM32F4), co w porównaniu do 5, 10 zł za AVRa jest sumą ogromną. Niemniej wydaje mi się że gra jest warta świeczki (dla lamera takiego jak ja). Również nie wszystkie ARM będą działały z NETMF. Muszą mieć odpowiednią pojemność pamięci RAM i Flash. Poniżej lista procesorów na których NETMF na pewno będzie działał (może będzie uzupełniana):