22.05.2013

Pozytywka z PWM

Znalazłem jeszcze jedno zastosowanie PWM, dosyć rozrywkowe. Do wyjścia PWM można podłączyć głośniczek i odtwarzać proste melodie. Najpierw schemat:

STM32F4 Speaker

STM32F4Discovery Speaker
Rezystor może być inny, ale nie mniejszy niż 110, 120 omów. Z wyjścia STM32F407 można pobierać tylko 25mA prądu. Biorąc pod uwagę rezystancję głośniczka mamy równanie: 3V/(8ohm + 120ohm) =  0,023A. Tak więc przy tej rezystancji zmieścimy się w zakresie.

Teraz wystarczy tylko poszukać jakie częstotliwości odpowiadają poszczególnym dźwiękom i można odtworzyć dźwięki gamy:

//C, D, E, F, G, A, H, C 
var scale = new[]{261.6, 293.7, 329.2, 349.6, 
                  391.9, 440.0, 493.9, 523.2}; 
var pwm4 = new PWM(Cpu.PWMChannel.PWM_4, 50, 0, false);
pwm4.Start();

foreach (double note in scale)
{
    pwm4.Frequency = note;
    pwm4.DutyCycle = 0.5;
    Thread.Sleep(1000);
}

pwm4.Stop();


Aby zagrać jakąś prostą melodię trzeba się nieźle nawpisywać tych częstotliwości. Z pomocą przychodzi format RTTL lub MML. Na internecie można znaleźć mnóstwo melodyjek zapisanych w tych formatach.

Konstrukcję odtwarzacza RTTL rozpoczynamy od zdefiniowania interfejsu ISpeaker. Interfejs da możliwość dowolnej implementacji urządzenia wyjściowego. Jak widać są tylko dwie metody Play i Pause. Czyli to co głośniczek najlepiej umie robić: albo milczeć albo grać.

public interface ISpeaker
{
    void Pause();
    void Play(double frequency);
}

Teraz implementacja tego interfejsu na wyjściu PWM z podłączonym głośniczkiem:

public class PwmSpeaker : ISpeaker
{
    private readonly PWM _pwm;

    public PwmSpeaker(PWM pwm)
    {
        _pwm = pwm;
        _pwm.Frequency = 50;
        _pwm.DutyCycle = 0;
        _pwm.Start();
    }
            
    public void Play(double frequency)
    {
        _pwm.Frequency = frequency;
        _pwm.DutyCycle = 0.5;
    }

    public void Pause()
    {
        _pwm.Frequency = 50;
        _pwm.DutyCycle = 0;
    }
}

Przechodzimy do odtwarzacza RTTL. Całe zadanie polega na zdekodowaniu napisu w formacie RTTL na poszczególne częstotliwości dźwięków i czas ich trwania. Później trzeba to wysłać do ISpeakera.

Do dekodowania formatu RTTL na bardziej przyjazne dane służy statyczna metoda Parse klasy Rttl:

public static Rttl Parse(string rttlData)
{
    string[] sections = rttlData.Split(':');

    string name = sections[0].Trim();
    string[] tones = sections[2].Split(',');
    int duration = 4;
    int octave = 6;
    int bpm = 63;

    if (sections[1].Length > 0)
    {
        string[] controls = sections[1].Split(',');
        foreach (string item in controls)
        {
            string control = item.Trim();

            string valueStr = control.Substring(2, control.Length - 2);
            int value = Int32.Parse(valueStr);

            switch (control[0].ToLower())
            {
                case 'd':
                    duration = value;
                    break;

                case 'o':
                    octave = value;
                    break;

                case 'b':
                    bpm = value;
                    break;
            }
        }
    }

    var result = new Rttl
                        {
                            Name = name,
                            Tones = tones,
                            Duration = duration,
                            Octave = octave,
                            Bpm = bpm
                        };
    return result;
}

Jak już mamy wszystkie potrzebne dane to trzeba znaleźć tylko odpowiednie częstotliwości. Klasa RttlPlayer ma zdefiniowane tablice z częstotliwościami poszczególnych dźwięków. Wartości wypełnione są tylko dla pierwszej oktawy. Pozostałe oktawy zostaną uzupełnione poprzez podwojenie częstotliwości z oktawy poprzedzającej w statycznym konstruktorze.

private static readonly double[][] Scales = new[]
                                                {
                                                    new[]
                                                        {
                                                            //C, Cis, D, Dis, E, F, Fis, G, Gis, A, Ais, H 
                                                            261.6, 277.2, 293.7, 311.2, 329.2,
                                                            349.6, 370, 391.9, 415.3, 440.0, 466.2, 493.9
                                                        },
                                                    new double[12],
                                                    new double[12],
                                                    new double[12]
                                                };

static RttlPlayer()
{
    for (int i = 1; i < Scales.Length; i++)
        for (int j = 0; j < Scales[i].Length; j++)
            Scales[i][j] = 2*Scales[i - 1][j];
}

No teraz to wystarczy wszystko połączyć w całość w metodzie Play RttlPlayera.

public void Play(string rttlData)
{
    const int oneBpmWholeNote = 60*4*1000; //one bpm whole note in ms

    Rttl rttl = Rttl.Parse(rttlData);
    int wholeNote = oneBpmWholeNote/rttl.Bpm;

    foreach (string tone in rttl.Tones)
    {
        bool specialDuration;
        string durationStr, noteStr, scaleStr;
        ParseCommand(tone, out durationStr, out noteStr, out scaleStr, out specialDuration);

        int duration = rttl.Duration;
        if (durationStr.Length > 0)
            duration = Int32.Parse(durationStr);

        duration = specialDuration ? (3*wholeNote)/(2*duration) : wholeNote/duration;

        int freqIndex;
        switch (noteStr[0].ToLower())
        {
            case 'c':
                freqIndex = 0;
                break;

            case 'd':
                freqIndex = 2;
                break;

            case 'e':
                freqIndex = 4;
                break;

            case 'f':
                freqIndex = 5;
                break;

            case 'g':
                freqIndex = 7;
                break;

            case 'a':
                freqIndex = 9;
                break;

            case 'b':
                freqIndex = 11;
                break;

            default:
                freqIndex = -1;
                break;
        }

        if (noteStr.Length > 1)//#
            freqIndex++;

        int scale = rttl.Octave;
        if (scaleStr.Length > 0)
            scale = Int32.Parse(scaleStr);

        if (freqIndex >= 0)
        {
            double freq = Scales[scale - 4][freqIndex];

            Debug.Print("Playing: (" + tone + ")" + freq + " " + duration);
            _speaker.Play(freq);
            Thread.Sleep(duration);
            _speaker.Pause();
        }
        else
        {
            Debug.Print("Pausing: (" + tone + ") " + duration);
            _speaker.Pause();
            Thread.Sleep(duration);
        }
    }
}

A tak można odtwarzać melodyjki:

var pwm = new PWM(Cpu.PWMChannel.PWM_4, 50, 0, false);
var player = new RttlPlayer(new PwmSpeaker(pwm));
const string song = "Indiana:d=4,o=5,b=250:e,8p,8f,8g,8p,1c6,8p.,d,8p,8e,1f,p.,g,8p,8a,8b,8p,1f6,p,a,8p,8b,2c6,2d6,2e6,e,8p,8f,8g,8p,1c6,p,d6,8p,8e6,1f.6,g,8p,8g,e.6,8p,d6,8p,8g,e.6,8p,d6,8p,8g,f.6,8p,e6,8p,8d6,2c6";
player.Play(song);

Przykład odtwarzania formatu MTL można znaleźć w projekcie .NET Micro Framework Toolbox.

5.05.2013

PWM

Dzisiaj postanowiłem się pobawić PWM. W PWM chodzi o to, że sterujemy wypełnieniem sygnału prostokątnego przy stałej częstotliwości tego sygnału. Jak to wygląda na obrazkach można zobaczyć tutaj: http://arduino.cc/en/Tutorial/PWM. Na płytce STM32F4Discovery mamy dostępne 8 wyjść PWM:

PWMChannel.PWM_0: PD12 //GREEN LED
PWMChannel.PWM_1: PD13 //ORANGE LED

PWMChannel.PWM_2: PD14 //RED LED
PWMChannel.PWM_3: PD15 //BLUE LED
PWMChannel.PWM_4: PE9
PWMChannel.PWM_5: PE11
PWMChannel.PWM_6: PE13
PWMChannel.PWM_7: PE14


Najlepsze jest to, że pierwsze cztery wyjścia kanałów PWM to diody znajdujące się na płytce, więc aby zobaczyć jakieś efekty nic nie musimy podłączać.

Częstotliwość sygnału i jego wypełnienie możemy regulować na dwa sposoby: ustawiając częstotliwość (właściwość Frequency) i procentowe wypełnienie (właściwość DutyCycle) lub czas jednego okresu sygnału (właściwość Period) i czas trwania stanu wysokiego (właściwość Duration). Tutaj mała uwaga. Ustawiając DytyCycle wpisujemy procenty w postaci ułamka czyli jeśli chcemy 21% to ustawiamy 0.21. Natomiast ustawiając Period i Duration jednostkę wskazujemy we właściwości Scale (nano, mili, mikro sekundy). Przykład - dioda zielona mruga raz na sekundę:

var pwm0 = new PWM(Cpu.PWMChannel.PWM_0, 1, 0.5, false);
pwm0.Start();

No dobra, a czy można równocześnie ustawić czerwoną diodę żeby mrugała 2 razy szybciej? Chyba nic prostszego:

var pwm0 = new PWM(Cpu.PWMChannel.PWM_0, 1, 0.5, false);
pwm0.Start();

var pwm2 = new PWM(Cpu.PWMChannel.PWM_2, 2, 0.5, false);
pwm2.Start();

I co? Kicha! Zielona zaczęła mrugać tak samo jak czerwona. Popatrzymy do pliku platform_selector.h w katalogu Solutions\Discovery4 PK:

#define STM32F4_PWM_TIMER {4,4,4,4,1,1,1,1}
#define STM32F4_PWM_CHNL  {0,1,2,3,0,1,2,3}
#define STM32F4_PWM_PINS  {60,61,62,63,73,75,77,78} // D12-D15,E9,E11,E13,E14

Okazuje się, że pierwsze 4 wyjścia PWM obsługuje ten sam timer (cztery kolejne zresztą też, ale inny). Jeśli jest to ten sam timer to wszystkie wyjścia PWM działają z takim samym okresem. Przestawienie okresu (nie ważne czy robi się to przez właściwość Period czy Frequency) na inną wartość dla jednego wyjścia będzie miało wpływ na pozostałe kanały. Dodatkowo trzeba pamiętać, że DutyCycle nie pozostanie stałe przy zmianie częstotliwości. Czyli jeśli chcemy, aby wypełnienie było stałe na poziomie 50% to zmieniając częstotliwość trzeba równocześnie przypisać wartość 0.5 do DutyCycle. Trzeba o tym pamiętać szczególnie w pętlach, gdzie zmieniana jest częstotliwość lub okres. (http://netmf.codeplex.com/workitem/1749)


Jednym z zastosowań PWM jest regulacja jasności świecenia diody LED. Małe wypełnienie - dioda słabo świeci, duże wypełnienie - doda jasno świeci. Może to być wykorzystane do regulacji podświetlenia w wyświetlaczach. Prosty programik, który rozjaśnia i ściemnia diodę czerwoną:

public static void Main()
{
    var pwm2 = new PWM(Cpu.PWMChannel.PWM_2, 300, 0, false);
    pwm2.Start();

    const int minBright = 0;
    const int maxBright = 100;
    int bright = minBright;
    int step = 1;

    while (true)
    {
        pwm2.DutyCycle = ToDutyCycle(bright);

        bright += step;
        if (bright > maxBright || bright < minBright)
        {
            bright = bright > maxBright ? maxBright : minBright;
            step = -step;

            Thread.Sleep(1000);
        }

        Thread.Sleep(40);
    }
}

private static double ToDutyCycle(double brightness)
{
    return brightness/100;
}

Jak widać kot nie jest skomplikowany. Przelatujemy w pętli od 0 do 100 (to jasność) i przypisujemy wprost proporcjonalnie wypełnienie (dzielimy przez 100, aby otrzymać wartości w procentach: 0..1). Migotanie diody zostało zniwelowane poprzez ustawienie częstotliwości na 300 Hz tak, aby wykorzystać bezwładność oka ludzkiego. Przy mniejszych wartościach niektórzy ludzie mogą dostrzegać migotanie. Program jednak nie działa tak jak trzeba. Dioda bardzo szybko się rozjaśnia i bardzo jasno świeci przez większość czasu. Winę za to ponosi nieliniowa charakterystyka oka ludzkiego. Dlatego musimy skorygować wypełnienie.

Spotkałem dwie "szkoły" takiej korekcji. Jedna używa funkcji wykładniczej o podstawie e (podstawa logarytmu naturalnego), a druga funkcji wykładniczej o podstawie 10 (podstawa logarytmu dziesiętnego). Gdzieś wyczytałem, że oko ludzkie ma charakterystykę logarytmiczną o podstawie 10 więc druga może jest bardziej adekwatna. W praktyce moje oko nie zauważyło różnicy między nimi. Wykres poniżej przedstawia obie funkcje:
pwm brightness dimming curve
A tak trzeba je zapisać w kocie:
private static double ToDutyCycleExp(double brightness)
{
    if (brightness < 1)
        return 0;

    if (brightness > 99)
        return 1;

    return (0.383*System.Math.Exp(0.0555*brightness) - 0.196)/100;
}

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

    if (brightness > 99)
        return 1;

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