DevBoy: making a signal generator

    Hello friends!

    In past articles, I talked about my project and its software part . In this article I will tell you how to make a simple signal generator for 4 channels - two analog channels and two PWM channels.



    Analog channels


    The STM32F415RG microcontroller incorporates a 12-bit DAC (digital-to-analog) converter into two independent channels, which allows generating different signals. You can directly load data into the registers of the converter, but this is not very suitable for generating signals. The best solution is to use an array into which to generate one wave of the signal, and then run the DAC with a trigger from the timer and DMA. By changing the frequency of the timer, you can change the frequency of the generated signal.

    Classic ” waveforms include: sinusoidal, meander, triangular and sawtooth.

    image

    The function of generating these waves in the buffer is as follows
    // *****************************************************************************
    // ***   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 the function, you need to pass a pointer to the beginning of the array, the size of the array, the maximum value and the desired waveform. After the call, the array will be filled with samples for one wave of the required shape and you can start the timer to periodically load the new value into the DAC.

    The DAC in this microcontroller has a limitation: the typical settling time (the time from loading a new value into the DAC and when it appears on the output ) is 3 ms. But not everything is so simple - this time is maximum, i.e. change from minimum to maximum and vice versa. When trying to withdraw the meander, these littered fronts are very clearly visible:



    If a sinusoidal wave is output, then the obstruction of the fronts is no longer so noticeable due to the waveform. However, if the frequency is increased, the sinusoidal signal turns into a triangular one, and with a further increase, the signal amplitude decreases.

    Generation at 1 KHz ( 90% amplitude ):



    Generation at 10 KHz ( 90% amplitude ):



    Generation at 100 KHz ( 90% amplitude ):



    Steps are already visible - because new data are loaded into the DAC at a frequency of 4 MHz.

    In addition, the trailing edge of the sawtooth signal is cluttered and from below the signal does not reach the value to which it should. This is because the signal does not have time to reach the specified low level, and the software is loading new values

    Generation at 200 KHz ( 90% amplitude ):



    Here you can already see how all the waves turned into a triangle.

    Digital channels


    With digital channels, everything is much simpler - in almost any microcontroller there are timers that allow you to output a PWM signal to the outputs of the microcontroller. It is best to use a 32-bit timer - in this case, you do not need to count the timer pre-timer, just load the period in one register and load the required duty cycle in another register.

    User interface


    It was decided to organize the user interface into four rectangles, each with a picture of the output signal, frequency and amplitude / duty cycle. For the currently selected channel, the text data is displayed in white, for the rest in gray.



    It was decided to do control on the encoders: the left one is responsible for the frequency and the current selected channel ( changes when the button is pressed ), the right one is responsible for the amplitude / duty cycle and waveform ( changes when the button is pressed ).

    In addition, support for the touch screen is implemented - when you click on an inactive channel, it becomes active, when you click on an active channel, the waveform changes.

    Of course, DevCore is used to do all this. The code for initializing the user interface and updating the data on the screen looks like this:

    Structure containing all UI objects
        // *************************************************************************
        // ***   Structure for describes all visual elements for the channel   *****
        // *************************************************************************
        struct ChannelDescriptionType
        {
          // 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];
    User interface initialization code
      // Create and show UI
      int32_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 data
        int32_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);
      }
    Screen update code
          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);
            else                   snprintf(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();

    An interesting implementation of the button click is implemented (it is a rectangle over which the remaining elements are drawn ). If you looked at the code, you should have noticed such a thing: ch_dsc [i] .box.SetCallback (& ​​Callback, this, nullptr, i); called in a loop. This is the job of the callback function that will be called when the button is pressed. The following are transferred to the function: the address of the static function of the static function of the class, the this pointer, and two user parameters that will be passed to the callback function - a pointer ( not used in this case - nullptr is passed ) and a number ( channel number is transmitted ).

    From the university bench, I remember the postulate: " Static functionsthey don’t have access to non-static members of the class . "So this is not true . Since the static function is a member of the class, it has access to all members of the class if it has a link / pointer to this class. Now let's look at the callback function:

    // *****************************************************************************
    // ***  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 the first line of this function, " magic " occurs, after which you can access any members of the class, including private ones.

    By the way, this function is called in another task ( rendering the screen ), so inside this function you need to take care of synchronization. In this simple " couple of evenings " project, I did not do this, because in this particular case it is not essential.

    The generator source code is uploaded to GitHub: https://github.com/nickshl/WaveformGenerator
    DevCore is now allocated to a separate repository and included as a submodule.

    Well, why do I need a signal generator, it will be in the next ( or one of the following ) article.

    Also popular now: