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