DevBoy:制作信号发生器

朋友您好!

在过去的文章中,我谈到了我的项目 及其软件部分 。 在本文中,我将告诉您如何为4个通道(两个模拟通道和两个PWM通道)制作一个简单的信号发生器。



模拟通道


STM32F415RG微控制器在两个独立的通道中集成了一个12位DAC (数模)转换器,从而可以产生不同的信号。 您可以将数据直接加载到转换器的寄存器中,但这不适用于生成信号。 最好的解决方案是使用一个阵列,在该阵列中产生一个信号波,然后使用来自计时器和DMA的触发器来运行DAC。 通过更改计时器的频率,可以更改生成信号的频率。

经典 ”波形包括:正弦,曲折,三角形和锯齿形。

图片

在缓冲区中生成这些波的功能如下
// ***************************************************************************** // *** 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; } 

在函数中,您需要将指针传递给数组的开头,数组的大小,最大值和所需的波形。 调用之后,该阵列将充满所需形状的一个波形的样本,您可以启动计时器以定期将新值加载到DAC中。

该微控制器中的DAC有一个局限性:典型的建立时间(将新值加载到DAC中并在输出出现时的时间 )为3 ms。 但并非一切都那么简单-这次是最大的时间,即 从最小值更改为最大值,反之亦然。 尝试撤回弯道时,这些乱蓬蓬的正面非常清晰可见:



如果输出正弦波,则由于波形的原因,前部的障碍不再那么明显。 但是,如果频率增加,则正弦信号变成三角信号,并且随着频率的进一步增加,信号幅度减小。

以1 KHz( 振幅90% )产生:



以10 KHz( 振幅90% )产生:



以100 KHz( 振幅90% )产生:



步骤已经可见-因为新数据以4 MHz的频率加载到DAC中。

此外,锯齿波信号的后沿杂乱,信号从下方未达到其应有的值。 这是因为信号没有时间达到指定的低电平,并且软件正在加载新值

以200 KHz( 振幅90% )产生:



在这里,您已经可以看到所有波浪如何变成三角形。

数字频道


使用数字通道,一切都变得更加简单-几乎在任何微控制器中都有计时器,可让您将PWM信号输出到微控制器的输出。 最好使用32位定时器-在这种情况下,您无需对定时器预定时器进行计数,只需将周期加载到一个寄存器中,并将所需的占空比加载到另一个寄存器中。

使用者介面


决定将用户界面组织为四个矩形,每个矩形均带有输出信号,频率和幅度/占空比的图片。 对于当前选择的通道,文本数据显示为白色,其余显示为灰色。



决定对编码器进行控制:左边的一个负责频率和当前选择的通道( 按下按钮时改变 ),右边的一个负责振幅/占空比和波形( 按下按钮时改变 )。

此外,还实现了对触摸屏的支持-单击非活动通道时,它将变为活动状态;单击活动通道时,波形将发生变化。

当然,DevCore用于完成所有这一切。 用于初始化用户界面和更新屏幕上的数据的代码如下所示:

包含所有UI对象的结构
  // ************************************************************************* // *** 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]; 
用户界面初始化代码
  // 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); } 
屏幕更新代码
  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(); 

实现了按钮单击的一个有趣的实现(它是一个在其上绘制其余元素的矩形 )。 如果看了一下代码,您应该已经注意到这样的事情: ch_dsc [i] .box.SetCallback(&Callback,this,nullptr,i); 循环调用。 这是按下按钮时将调用的回调函数的工作。 将以下内容传递给该函数:类的静态函数的静态函数的地址,this指针以及两个将传递给回调函数的用户参数-一个指针( 在这种情况下不使用-传递nullptr )和一个数字( 发送通道号 )。

我在大学里记得这样的假设:“ 静态函数无法访问非静态班级成员 。” 所以这是不正确的 。 由于静态函数是类的成员,因此如果它具有指向该类的链接/指针,则它可以访问该类的所有成员 。 现在看一下回调函数:

 // ***************************************************************************** // *** 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; } 

在此函数的第一行中,出现“ magic ”,之后您可以访问该类的任何成员,包括私有成员。

顺便说一下,此函数在另一个任务( 渲染屏幕 )中被调用,因此在此函数内部您需要注意同步。 在这个简单的“ 两晚 ”项目中,我没有这样做,因为在这种特殊情况下它不是必需的。

生成器源代码上传到GitHub: https : //github.com/nickshl/WaveformGenerator
现在, DevCore已分配给单独的存储库,并作为子模块包含在内。

好吧,为什么我需要一个信号发生器,它会在下一篇文章( 或以下文章之一 )中出现。

Source: https://habr.com/ru/post/zh-CN425409/


All Articles