DevBoy: Machen Sie einen Signalgenerator

    Hallo Freunde!

    In früheren Artikeln habe ich über mein Projekt und seinen Programmteil gesprochen . In diesem Artikel werde ich erklären, wie ein einfacher Signalgenerator für 4 Kanäle hergestellt wird - zwei analoge Kanäle und zwei PWM-Kanäle.



    Analoge Kanäle


    Der Mikrocontroller STM32F415RG enthält einen 12-Bit- DAC -Wandler (Digital-Analog-Wandler) für zwei unabhängige Kanäle, mit dem Sie unterschiedliche Signale erzeugen können. Sie können Daten direkt in die Konverterregister laden, dies ist jedoch nicht sehr gut für die Signalerzeugung geeignet. Die beste Lösung ist, ein Array zu verwenden, in dem eine einzelne Welle des Signals erzeugt wird, und dann den DAC mit einem Trigger vom Timer und DMA zu starten. Durch Ändern der Frequenz des Timers können Sie die Frequenz des erzeugten Signals ändern.

    " Klassische " Wellenformen umfassen: Sinuswelle, Rechteckwelle, Dreieck- und Sägezahnwelle.

    Bild

    Die Erzeugung dieser Wellen im Puffer ist wie folgt
    // *****************************************************************************// ***   GenerateWave   ********************************************************// *****************************************************************************
    Result Application::GenerateWave(uint16_t* dac_data, uint32_t dac_data_cnt, uint8_t duty, WaveformType waveform)
    {
      Result result;
      uint32_t max_val = (DAC_MAX_VAL * duty) / 100U;
      uint32_t shift = (DAC_MAX_VAL - max_val) / 2U;
      switch(waveform)
      {
        case WAVEFORM_SINE:
          for(uint32_t i = 0U; i < dac_data_cnt; i++)
          {
            dac_data[i] = (uint16_t)((sin((2.0F * i * PI) / (dac_data_cnt + 1)) + 1.0F) * max_val) >> 1U;
            dac_data[i] += shift;
          }
          break;
        case WAVEFORM_TRIANGLE:
          for(uint32_t i = 0U; i < dac_data_cnt; i++)
          {
            if(i <= dac_data_cnt / 2U)
            {
              dac_data[i] = (max_val * i) / (dac_data_cnt / 2U);
            }
            else
            {
              dac_data[i] = (max_val * (dac_data_cnt - i)) / (dac_data_cnt / 2U);
            }
            dac_data[i] += shift;
          }
          break;
        case WAVEFORM_SAWTOOTH:
          for(uint32_t i = 0U; i < dac_data_cnt; i++)
          {
            dac_data[i] = (max_val * i) / (dac_data_cnt - 1U);
            dac_data[i] += shift;
          }
          break;
        case WAVEFORM_SQUARE:
          for(uint32_t i = 0U; i < dac_data_cnt; i++)
          {
            dac_data[i] = (i < dac_data_cnt / 2U) ? max_val : 0x000;
            dac_data[i] += shift;
          }
          break;
        default:
          result = Result::ERR_BAD_PARAMETER;
          break;
      }
      return result;
    }

    In der Funktion müssen Sie einen Zeiger auf den Anfang des Arrays, die Größe des Arrays, den Maximalwert und die gewünschte Wellenform übergeben. Nach dem Aufruf wird das Array mit Samples für eine Welle des erforderlichen Formulars gefüllt. Sie können einen Timer starten, um den neuen Wert regelmäßig in den DAC zu laden.

    Der DAC dieses Mikrocontrollers hat eine Einschränkung: Die typische Einschwingzeit (die Zeit vom Laden eines neuen Werts in den DAC und sein Auftreten am Ausgang ) beträgt 3 ms. Aber nicht alles ist so einfach - diese Zeit ist das Maximum, d. H. wechseln Sie von Minimum zu Maximum und umgekehrt. Wenn Sie versuchen, den Mäander herauszuholen, sind diese verputzten Fronten sehr deutlich sichtbar:



    Wenn jedoch eine Sinuswelle ausgegeben wird, ist der Zusammenbruch der Fronten aufgrund der Wellenform nicht so wahrnehmbar. Wenn jedoch die Frequenz erhöht wird, wird das Sinussignal dreieckig, und mit weiterer Erhöhung nimmt die Signalamplitude ab.

    Erzeugung bei 1 KHz ( 90% Amplitude ):



    Erzeugung bei 10 KHz ( 90% Amplitude ):



    Erzeugung bei 100 KHz ( 90% Amplitude ):



    Schritte sind bereits sichtbar - weil der DAC bei 4 MHz geladen ist.

    Außerdem ist die hintere Vorderseite der Rampe verunreinigt und das Signal von unten erreicht nicht den Wert, auf den es sich beziehen sollte. Dies liegt daran, dass das Signal den angegebenen niedrigen Pegel nicht erreicht und die Software bereits neue Werte lädt.

    Erzeugung bei 200 KHz ( 90% Amplitude ):



    Hier können Sie bereits sehen, wie sich alle Wellen in ein Dreieck verwandeln.

    Digitale Kanäle


    Mit digitalen Kanälen ist alles viel einfacher - in fast jedem Mikrocontroller gibt es Timer, mit denen Sie das PWM-Signal an die Mikrocontroller-Pins ausgeben können. Verwenden Sie am besten einen 32-Bit-Timer. In diesem Fall müssen Sie den Vorgänger-Timer nicht neu berechnen. Es reicht aus, die Periode in ein Register zu laden und das erforderliche Verhältnis in das andere Register zu laden.

    Benutzeroberfläche


    Es wurde beschlossen, die Benutzeroberfläche in vier Rechtecke einzuteilen, von denen jedes ein Bild des Ausgangssignals, der Frequenz und der Amplitude / des Tastverhältnisses enthält. Für den aktuell ausgewählten Kanal werden Textdaten in weißer Schrift angezeigt, für den Rest - in grau.



    Die Verwaltung der Encoder wurde beschlossen: die linke ist für die Frequenz und den aktuell ausgewählten Kanal verantwortlich ( ändert sich beim Drücken der Taste ), die rechte ist für die Amplitude / Einschaltdauer und die Wellenform ( ändert sich, wenn Sie die Taste drücken ).

    Darüber hinaus ist die Unterstützung für den Touchscreen implementiert. Wenn Sie auf einen inaktiven Kanal klicken, wird dieser aktiv, wenn Sie auf einen aktiven Kanal klicken, ändert sich die Wellenform.

    Natürlich wird DevCore verwendet, um all dies zu implementieren. Der Code zum Initialisieren der Benutzeroberfläche und zum Aktualisieren der Daten auf dem Bildschirm sieht folgendermaßen aus:

    Eine Struktur, die alle UI-Objekte enthält
    // *************************************************************************// ***   Structure for describes all visual elements for the channel   *****// *************************************************************************structChannelDescriptionType
        {// UI data
          UiButton box;
          Image img;
          String freq_str;
          String duty_str;
          char freq_str_data[64] = {0};
          char duty_str_data[64] = {0};
          // Generator data
          ...
        };
        // Visual channel descriptions
        ChannelDescriptionType ch_dsc[CHANNEL_CNT];
    UI-Initialisierungscode
    // Create and show UIint32_t half_scr_w = display_drv.GetScreenW() / 2;
      int32_t half_scr_h = display_drv.GetScreenH() / 2;
      for(uint32_t i = 0U; i < CHANNEL_CNT; i++)
      {
        // Generator data
        ...
        // UI dataint32_t start_pos_x = half_scr_w * (i%2);
        int32_t start_pos_y = half_scr_h * (i/2);
        ch_dsc[i].box.SetParams(nullptr, start_pos_x, start_pos_y, half_scr_w, half_scr_h, true);
        ch_dsc[i].box.SetCallback(&Callback, this, nullptr, i);
        ch_dsc[i].freq_str.SetParams(ch_dsc[i].freq_str_data, start_pos_x + 4, start_pos_y + 64, COLOR_LIGHTGREY, String::FONT_8x12);
        ch_dsc[i].duty_str.SetParams(ch_dsc[i].duty_str_data, start_pos_x + 4, start_pos_y + 64 + 12, COLOR_LIGHTGREY, String::FONT_8x12);
        ch_dsc[i].img.SetImage(waveforms[ch_dsc[i].waveform]);
        ch_dsc[i].img.Move(start_pos_x + 4, start_pos_y + 4);
        ch_dsc[i].box.Show(1);
        ch_dsc[i].img.Show(2);
        ch_dsc[i].freq_str.Show(3);
        ch_dsc[i].duty_str.Show(3);
      }
    Aktualisieren Sie den Code auf dem Bildschirm
    for(uint32_t i = 0U; i < CHANNEL_CNT; i++)
          {
            ch_dsc[i].img.SetImage(waveforms[ch_dsc[i].waveform]);
            snprintf(ch_dsc[i].freq_str_data, NumberOf(ch_dsc[i].freq_str_data), "Freq: %7lu Hz", ch_dsc[i].frequency);
            if(IsAnalogChannel(i)) snprintf(ch_dsc[i].duty_str_data, NumberOf(ch_dsc[i].duty_str_data), "Ampl: %7d %%", ch_dsc[i].duty);
            elsesnprintf(ch_dsc[i].duty_str_data, NumberOf(ch_dsc[i].duty_str_data), "Duty: %7d %%", ch_dsc[i].duty);
            // Set gray color to all channels
            ch_dsc[i].freq_str.SetColor(COLOR_LIGHTGREY);
            ch_dsc[i].duty_str.SetColor(COLOR_LIGHTGREY);
          }
          // Set white color to selected channel
          ch_dsc[channel].freq_str.SetColor(COLOR_WHITE);
          ch_dsc[channel].duty_str.SetColor(COLOR_WHITE);
          // Update display
          display_drv.UpdateDisplay();

    Interessanterweise implementierte Bearbeitung des Knopfdrucks ( ist ein Rechteck, über dem die restlichen Elemente gezeichnet werden ). Wenn Sie sich den Code angesehen haben, sollten Sie so etwas bemerkt haben: ch_dsc [i] .box. SetCallback (& ​​Callback, this, nullptr, i); in einer Schleife aufgerufen. Dies ist die Aufgabe der Callback-Funktion, die aufgerufen wird, wenn die Taste gedrückt wird. Die folgenden Funktionen werden an die Funktion übergeben: Die Adresse der statischen Funktion der statischen Klassenfunktion, der Zeiger this und zwei Benutzerparameter, die an die Rückruffunktion übergeben werden - ein Zeiger ( in diesem Fall nicht verwendet - es wird nullptr übergeben ) und eine Nummer (die Kanalnummer wird übertragen ).

    Von der Universität erinnere ich mich an das Postulat: " Statische Funktionenhaben keinen Zugriff auf nicht statische Klassenmitglieder ". Dies ist also nicht wahr . Da eine statische Funktion Mitglied einer Klasse ist, hat sie Zugriff auf alle Mitglieder der Klasse, wenn sie über einen Link / Zeiger auf diese Klasse verfügt. Sehen Sie sich nun die Callback-Funktion an:

    // *****************************************************************************// ***  Callback for the buttons   *********************************************// *****************************************************************************void Application::Callback(void* ptr, void* param_ptr, uint32_t param)
    {
      Application& app = *((Application*)ptr);
      ChannelType channel = app.channel;
      if(channel == param)
      {
        // Second click - change wave type
        ...
      }
      else
      {
        app.channel = (ChannelType)param;
      }
      app.update = true;
    }

    In der ersten Zeile dieser Funktion wird " Magie " ausgeführt , woraufhin Sie auf alle Mitglieder der Klasse zugreifen können, auch auf private.

    Der Aufruf dieser Funktion erfolgt übrigens in einer anderen Task ( Bildschirm zeichnen ), so dass Sie sich innerhalb dieser Funktion um die Synchronisation kümmern müssen. Bei diesem unprätentiösen Projekt " paar Abende " habe ich dies nicht getan, weil es in diesem speziellen Fall nicht unbedingt erforderlich ist.

    Der Generator-Quellcode wird in GitHub hochgeladen: https://github.com/nickshl/WaveformGenerator
    DevCore ist jetzt in ein separates Repository aufgeteilt und als Submodul aktiviert.

    Naja, warum brauche ich einen Signalgenerator, wird in den nächsten ( oder einem der folgenden ) Artikel sein.

    Jetzt auch beliebt: