65.9K
CodeProject 正在变化。 阅读更多。
Home

用于生成和显示数字信号的 C# 库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (12投票s)

2022年3月3日

MIT

13分钟阅读

viewsIcon

13073

downloadIcon

1019

生成和显示 Windows Forms 中各种数字信号的 C# 库

引言

数字信号通常用于压缩数据。在某个范围内,将模拟信号的所有值存储在某些介质上实际上是不可能的(考虑到介于两个实数之间的数字的数量是无限的,无论它们有多接近)。相反,我们会在固定的时间间隔内采样,然后存储以供进一步处理。生成和显示数字信号的 C# 库将按照其组成数据结构和函数的需求顺序进行描述。附带的 ZIP 文件包含所有代码。

时间和幅度点

数字信号只是时间-幅度点的序列,它们在时间(水平)维度上等距分布。下面的类将用于跟踪数据点。

   /// <summary>Class to encapsulate one data point of a digital signal.
   /// </summary>
   public class TimeMagnitude
   {
      public double Time,      // Time (in seconds) of signal magnitude generation.
                    Magnitude; // Signal magnitude in volts.

      public TimeMagnitude( double time, double magnitude )
      {
         Time = time; Magnitude = magnitude;
      }

      public TimeMagnitude Copy()
      {
         return new TimeMagnitude( this.Time, this.Magnitude );
      }// Copy

   }// TimeMagnitude (class)

在某些应用程序中,有必要创建现有 TimeMagnitude 实例的新副本。这就是 Copy 函数的目的。

信号参数

为了生成数字信号,必须假设在固定时间间隔内对相应模拟信号的幅度进行采样。必须指定几个参数才能正确生成信号,如下面的类所定义。大多数参数是不言而喻的,有些参数(如频率和周期)是冗余的,因为如果已知其中一个,则可以从另一个计算出来。samplingFactor 参数对于正确生成数字信号数据点以供进一步处理至关重要。

/// <summary>Class to encapsulate the parameters of a digital signal.
/// </summary>
public class SignalParameters
{
   public double amplitude, offset,   // Signal amplitude and DC offset in volts.
                 frequency,           // Signal frequency in Hertz.
                 period,              // Signal period in seconds.
                 halfPeriod,          // Half of the signal period.
                 samplingFrequency,   // Frequency (in Hertz) for signal generation.
                 frequencyResolution, // Spacing (in Hertz) between adjacent signal values.
                 timeStep;            // Spacing (in seconds) between two adjacent time points.

   public int samplingFactor,         // Factor to multiply by the signal frequency.
              nSamples;               // Number of signal "samples" to be generated.

   /// <summary>Create an instance of signal parameters.
   /// </summary>
   /// <param name="_amplitude">Amplitude of the signal in volts.</param>
   /// <param name="_frequency">Frequency of the signal in hertz (cycles/second).</param>
   /// <param name="_offset">DC offset (in volts) to be added to the amplitude.</param>
   /// <param name="_nSamples">Number of "samples" to be generated for the signal.</param>
   /// <param name="_samplingFactor">Factor to multiply by {_frequency} for signal-processing.
   /// </param>
   public SignalParameters( double _amplitude, double _frequency,                // Mandatory.
                            double _offset, int _nSamples, int _samplingFactor ) // Optional.
   {
      double one = (double)1.0;

      amplitude =_amplitude;
      frequency = _frequency;
      samplingFactor = _samplingFactor;
      nSamples = _nSamples;
      offset = _offset;

      period = one / frequency;
      halfPeriod = period / (double)2.0;
      samplingFrequency = (double)samplingFactor * frequency;
      frequencyResolution = samplingFrequency / (double)nSamples;

      timeStep = one / samplingFrequency;
   }
}// SignalParameters (class)

数字信号

生成数字信号涉及模拟在等距时间点上对其相应模拟信号幅度的采样。为了生成任意数量的连续数据点,可以将信号生成函数实现为枚举器,它们在调用之间保持其状态。要生成的信号是正弦波、余弦波、方波、锯齿波、三角波和白噪声。

数字信号由 GeneratingFn 类中的函数成员生成。该类的数据成员如下

public SignalParameters parameters; // Parameters of a signal to be generated.

public List<TimeMagnitude> sineSignalValues, cosineSignalValues,
                                 squareSignalValues, sawtoothSignalValues,
                                 triangleSignalValues, whiteNoiseValues;

public List<double> zeroCrossings; // Signal crossings on the time (X) axis.

private SignalPlot sineSignal, cosineSignal, squareSignal,
                   sawtoothSignal, triangleSignal, whiteNoise;

构造函数创建一个类实例,初始化信号参数、将包含信号值的列表、零交叉点的列表以及信号图。

/// <summary>Create an instance of a signal-generating function.
/// </summary>
/// <param name="_amplitude">Amplitude of the signal in Volts.</param>
/// <param name="_frequency">Frequency of the signal in Hertz (cycles/second).</param>
/// <param name="_offset">DC offset to be added to the magnitude of the signal.</param>
/// <param name="_nSamples">Number of samples to generate for signal-processing.</param>
/// <param name="_samplingFactor">Factor to multiply by the frequency.
/// </param>
public GeneratingFn( double _amplitude, double _frequency, double _offset = 0.0,
                     int _nSamples = 512, int _samplingFactor = 32 )
{
   parameters = new SignalParameters( _amplitude, _frequency,                // Mandatory 
                                                                             // arguments.
                                      _offset, _nSamples, _samplingFactor ); // Optional 
                                                                             // arguments.

   sineSignalValues = new List<TimeMagnitude>();
   cosineSignalValues = new List<TimeMagnitude>();
   squareSignalValues = new List<TimeMagnitude>();
   sawtoothSignalValues = new List<TimeMagnitude>();
   triangleSignalValues = new List<TimeMagnitude>();
   whiteNoiseValues = new List<TimeMagnitude>();

   sineSignal = new SignalPlot( "sine-signal", SignalShape.sine );
   sineSignal.Text = "Sine signal plot";
   cosineSignal = new SignalPlot( "cosine-signal", SignalShape.cosine );
   cosineSignal.Text = "Cosine signal plot";
   squareSignal = new SignalPlot( "square-signal", SignalShape.square,
                                   SignalContinuity.discontinuous );
   squareSignal.Text = "Square signal plot";
   sawtoothSignal = new SignalPlot( "sawtooth-signal", SignalShape.sawtooth,
                                     SignalContinuity.discontinuous );
   sawtoothSignal.Text = "Sawtooth signal plot";
   triangleSignal = new SignalPlot( "triangle-signal", SignalShape.triangle );
   triangleSignal.Text = "Triangle signal plot";
   whiteNoise = new SignalPlot( "white-noise signal", SignalShape.whiteNoise );
   whiteNoise.Text = "White noise plot";
}// GeneratingFn

在某些应用程序中,方便地从 TimeMagnitude 元素的列表中收集幅度,这可以通过以下方式简单地完成

/// <summary>Collect the magnitudes from a list of {TimeMagnitude} elements.
/// </summary>
/// <param name="tmList">List of (time, magnitude) elements.</param>
/// <returns>List of magnitudes.
/// </returns>
public double[] Magnitudes( List<TimeMagnitude> tmList )
{
   double[] mags = null;

   if ( tmList != null )
   {
      int n = tmList.Count;

      mags = new double[ n ];
      for ( int i = 0; i < n; ++i )
      {
         mags[ i ] = tmList[ i ].Magnitude;
      }
   }
   return mags;
}// Magnitudes 

GeneratingFn 构造函数及其 public 函数成员由驱动程序或用户程序调用。稍后将在本文中通过测试控制台应用程序来说明这一点。信号生成函数会重复调用任意次数。每次调用它们时,函数都会创建一个新的 TimeMagnitude 元素,将其添加到相应的列表中,并返回该元素的双重幅度。

正弦波和余弦波信号

下面的枚举器用于生成正弦信号的元素。

/// <summary>Generate the next sine-signal value.
/// </summary>
/// <returns>Current magnitude of the sine signal.
/// </returns>
public IEnumerator<double> NextSineSignalValue()
{
   double angularFreq = 2.0 * Math.PI * parameters.frequency; // w == 2 * pi * f
   double time = 0.0;
   double wt, sinOFwt, magnitude = 0.0;
   TimeMagnitude previousTM = null;
   
   zeroCrossings = new List<double>();
   
   while ( true )
   {
      try
      {
         wt = angularFreq * time;
         sinOFwt = Math.Sin( wt );
         magnitude = parameters.offset + ( parameters.amplitude * sinOFwt );
         
         TimeMagnitude tm = new TimeMagnitude( time, magnitude );
         
         sineSignalValues.Add( tm  );
         CheckZeroCrossing( previousTM, tm );
         previousTM = tm.Copy();
      }
      catch ( Exception exc )
      {
         MessageBox.Show( exc.Message );
         Abort();
      }
      yield return magnitude;
      
      time += parameters.timeStep;
   }
}// NextSineSignalValue

该函数被定义为 IEnumerator。第一次调用时,它会初始化其局部变量,然后进入无限循环。如果 try-catch 子句中的一切都按预期进行,则会更新 sineSignalValues 列表,并调用 CheckZeroCrossing 函数来确定信号是否已穿过时间轴。

/// <summary>If the {magnitude} of the current {TimeMagnitude} element is near 0.0,
///          or if there is a magnitude transition through the time axis from the
///          previous {TimeMagnitude} element to the current {TimeMagnitude}
///          element, then update the {zeroCrossings} list.
/// </summary>
/// <param name="previousTM">Previous {TimeMagnitude} element.</param>
/// <param name="tm">Current {TimeMagnitude} element.
/// </param>
private void CheckZeroCrossing( TimeMagnitude previousTM, TimeMagnitude tm )
{
   if ( UtilFn.NearZero( tm.Magnitude ) )
   {
      zeroCrossings.Add( tm.Time );
   }
   else if ( previousTM != null && MagnitudeTransition( previousTM, tm ) )
   {
      zeroCrossings.Add( previousTM.Time + ( ( tm.Time - previousTM.Time ) / 2.0 ) );
   }
}// CheckZeroCrossing

零交叉可能以三种方式发生。在最佳情况下,信号幅度可能非常接近 0.0。但是,由于精确比较双精度数的棘手问题,类 UtilFn 中定义在文件 Util_Lib.cs 中的以下实用程序代码用于将 double 与零进行比较。

// Definitions to deal with zero-comparisons of {double}s.
//
// After Microsoft.Docs "Double.Equals Method".
// https://docs.microsoft.com/en-us/dotnet/api/system.double.equals?view=net-5.0

private static double fraction = (double)0.333333,
                      dTolerance = Math.Abs( fraction * (double)0.00001 ),
                      zero = (double)0.0;
//
// In comparisons, use Math.Abs( {double}1 - {double}2 ) <= {dTolerance} )
// (see function {EQdoubles}).

/// <summary>Determine whether two {double} numbers are "equal".
/// </summary>
/// <param name="d1">Double number.</param>
/// <param name="d2">Double number.
/// </param>
/// <returns>Whether {d1} "equals" {d2}.
/// </returns>
public static bool EQdoubles( double d1, double d2 )
{
   d1 = Math.Abs( d1 );
   d2 = Math.Abs( d2 );
   
   return Math.Abs( d1 - d2 ) <= dTolerance;
}// EQdoubles

/// <summary>Determine whether a {double} is close to {zero}.
/// </summary>
/// <param name="d">{double} to be tested.
/// </param>
/// <returns>Whether {d} is close to {zero}.
/// </returns>
public static bool NearZero( double d )
{
   return EQdoubles( d, zero );
}// NearZero

零交叉发生的另外两种情况是:当前 TimeMagnitude 元素的幅度低于时间轴而前一个元素的幅度高于时间轴,或者前一个 TimeMagnitude 元素的幅度低于时间轴而当前元素的幅度高于时间轴。这些情况由以下函数检查

/// <summary>Determine whether there is a magnitude transition through the time
///          axis from the previous {TimeMagnitude} element to the current element.
/// </summary>
/// <param name="previousTM">Previous {TimeMagnitude} element.</param>
/// <param name="currentTM">Current {TimeMagnitude} element.</param>
/// <returns>{true} if there was a transition, {false} otherwise.
/// </returns>
private bool MagnitudeTransition( TimeMagnitude previousTM, TimeMagnitude currentTM )
{
   return ( previousTM.Magnitude > 0.0 && currentTM.Magnitude < 0.0 )
          ||
          ( previousTM.Magnitude < 0.0 && currentTM.Magnitude > 0.0 );
}// MagnitudeTransition

在检查零交叉后,更新变量 previousTM,函数退出 try-catch 子句并执行 yield return magnitude 以返回信号值。

下次调用该函数时,执行将继续在 yield return 语句之后,时间局部变量会更新,无限循环继续。请注意,实际上,将函数实现为枚举器并使用 yield return 语句使函数的局部变量表现得像老式的 C 和 C++ static 变量。这一点非常 remarkable,因为 C# 本身不支持 static 变量。

如果执行到达 try-catch 子句的 catch 部分,则发生了异常。函数将显示一个带有异常消息的 MessageBox,然后调用 Abort 函数终止执行。

/// <summary>Abort execution of the 'user'.
/// </summary>
private void Abort()
{
   if ( System.Windows.Forms.Application.MessageLoop )
   {
      // Windows application
      System.Windows.Forms.Application.Exit();
   }
   else
   {
      // Console application
      System.Environment.Exit( 1 );
   }
}// Abort

下一个余弦值的生成方式类似,通过调用 Math.Cos 而不是 Math.Sin 来完成,这里不展示。(同样,完整代码在附带的 ZIP 文件中。)

方波信号

方波信号的生成几乎是直观的。唯一的困难是处理垂直不连续性,这必须在辅助时间变量 t 接近信号周期的一半(parameters.halfPeriod)时发生。

/// <summary>Generate the next square-signal value.
/// </summary>
/// <returns>Current magnitude of the square signal.
/// </returns>
public IEnumerator<double> NextSquareSignalValue()
{
   double _amplitude = parameters.amplitude,
          magnitude = parameters.offset + _amplitude;
   double time = 0.0, t = 0.0;
   bool updateZeroCrossings = magnitude > (double)0.0;
   
   zeroCrossings = new List<double>();
   
   while ( true )
   {
      try
      {
         TimeMagnitude tm = new TimeMagnitude( time, magnitude );
         
         squareSignalValues.Add( new TimeMagnitude( time, magnitude ) );
      }
      catch ( Exception exc )
      {
         MessageBox.Show( exc.Message );
         Abort();
      }
      yield return magnitude;
      
      time += parameters.timeStep;
      t += parameters.timeStep;
      if ( UtilFn.NearZero( t - parameters.halfPeriod ) )
      {
         _amplitude = -_amplitude; // Vertical discontinuity.
         t = 0.0;
         if ( updateZeroCrossings )
         {
            zeroCrossings.Add( time );
         }
      }
      magnitude = parameters.offset + _amplitude;
   }
}// NextSquareSignalValue

除了处理垂直不连续性之外,方波信号的枚举器遵循与正弦波和余弦波信号的枚举器相同的逻辑。

锯齿波信号

锯齿波信号是通过重复一条斜直线生成的,其方程为

其中 m 是斜率,by 轴截距。除了处理垂直不连续性的部分,相应枚举器的实现也是直观的。

/// <summary>Generate the next sawtooth-signal value.
/// </summary>
/// <returns>Current magnitude of the sawtooth signal.
/// </returns>
public IEnumerator<double> NextSawtoothSignalValue()
{
   /*
    * A sawtooth signal is generated by repeating a sloped straight
    * line, whose equation is
    *
    *                y = m * t + b
    *
    * where {m} is the slope and {b} is the y-axis ordinate.
   */
   double m = 10.0 / parameters.period,
          b = -parameters.amplitude;
   double time = 0.0, t = 0.0;
   double magnitude = 0.0;
   TimeMagnitude previousTM, tm;
   
   zeroCrossings = new List<double>();
   
   while ( true )
   {
      previousTM = tm = null;
      try
      {
         magnitude = parameters.offset + ( m * t + b );
         
         tm = new TimeMagnitude( time, magnitude );
         
         sawtoothSignalValues.Add( tm );
         CheckZeroCrossing( previousTM, tm );
         previousTM = tm.Copy();
      }
      catch ( Exception exc )
      {
         MessageBox.Show( exc.Message );
         Abort();
      }
      yield return magnitude;
      
      if ( UtilFn.NearZero( t - parameters.period ) )
      {
         t = 0.0;
         if ( tm.Magnitude > (double)0.0 )
         {
            zeroCrossings.Add( time ); // Vertical discontinuity.
         }
      }
      time += parameters.timeStep;
      t += parameters.timeStep;
   }
}// NextSawtoothSignalValue

请注意,锯齿波信号的垂直不连续性发生在信号周期的整数倍处。

三角波信号

三角波信号可以看作是锯齿波信号的两个镜像斜线,上升线用于周期的前半部分,下降线用于周期的后半部分。

/// <summary>Generate the next triangle-signal value.
/// </summary>
/// <returns>Current magnitude of the triangle signal.
/// </returns>
public IEnumerator<double> NextTriangleSignalValue()
{
   /*
    * A triangle signal consists of mirrored sloped straight lines,
    * which can be obtained using part of the code for a sawtooth signal.
   */
   double m = 10.0 / parameters.period,
          b = -parameters.amplitude;
   double time = 0.0, t = 0.0;
   double magnitude = 0.0;
   int j = 0;
   TimeMagnitude previousTM, tm;
   
   tm = previousTM = null;
   bool mirror = false; // No mirroring.
   
   zeroCrossings = new List<double>();
   
   while ( true )
   {
      try
      {
         if ( !mirror ) // Line with ascending slope.
         {
            magnitude = parameters.offset + ( m * t + b );
            
            tm = new TimeMagnitude( time, magnitude );
            
            triangleSignalValues.Add( tm );
            ++j;
         }
         else // Mirroring: line with descending slope.
         {
            if ( j > 0 )
            {
               magnitude = triangleSignalValues[ --j ].Magnitude;
               
               tm = new TimeMagnitude( time, magnitude );
               
               triangleSignalValues.Add( tm );
            }
         }
      }
      catch ( Exception exc )
      {
         MessageBox.Show( exc.Message );
         Abort();
      }
      CheckZeroCrossing( previousTM, tm );
      previousTM = tm.Copy();
      yield return magnitude;
      
      if ( UtilFn.NearZero( t - parameters.halfPeriod ) )
      {
         mirror = true; // Start mirroring.
      }
      if ( UtilFn.NearZero( t - parameters.period ) )
      {
         t = 0.0;
         j = 0;
         mirror = false; // Stop mirroring.
      }
      time += parameters.timeStep;
      t += parameters.timeStep;
    }
}// NextTriangleSignalValue

白噪声信号

白噪声是一种随机信号。其数据点根据从随机数生成器获得的值随机出现。为了或多或少均匀地分布数据点,可以使用两个随机数生成器:一个用于幅值,另一个用于符号。

/// <summary>Generate the next white-noise value.
/// </summary>
/// <returns>Current value of white noise.
/// </returns>
public IEnumerator<double> NextWhiteNoiseSignalValue()
{
   double magnitude = 0.0, time = 0.0, sign;
   Random magRand, signRand;
   
   magRand = new Random();             // Magnitude random number generator.
   signRand = new Random();            // Random number generator for signal sign.
   
   zeroCrossings = new List<double>(); // This list will remain empty.
   
   while ( true )
   {
      sign = ( signRand.Next( 10 ) > 5 ) ? 1.0 : -1.0;
      magnitude = parameters.offset
                  + sign * ( magRand.NextDouble() * parameters.amplitude );
                  
      whiteNoiseValues.Add( new TimeMagnitude( time, magnitude ) );
      
      yield return magnitude;
      
      time += parameters.timeStep;
   }
}// NextWhiteNoiseSignalValue

此函数初始化零交叉点的列表,但从不向其中插入元素。原因是白噪声不能被视为对应于正弦波、余弦波、方波、锯齿波或三角波等具有确定趋势的幅值值的函数。

信号图

GeneratingFn 类构造函数使用 SignalPlot 类实例初始化一些 private 数据成员,这些成员用于显示显示生成信号数据点的 Windows 窗体。此类定义在文件 SignalPlot.cs 中。有两个枚举,一个用于指定信号的连续性,一个用于指定信号的形状。

   public enum SignalContinuity { continuous, discontinuous };
   public enum SignalShape { sine, cosine, square, sawtooth, triangle, whiteNoise };

用于创建绘制信号的 Windows 窗体实例的类的数据成员和构造函数定义如下

public partial class SignalPlot : Form
{
   private string description;         // Windows-form title.
   private SignalShape shape;
   private SignalContinuity continuity;
   private Bitmap bmp;
   private int xAxis_Y,                // Y coordinate of x-axis (middle of bmp.Height).
               sigMin_Y,               // Minimum Y coordinate of a signal
                                       // (has nothing to do with the actual signal value).
               sigMax_Y;               // Maximum Y coordinate of a signal
                                       // (has nothing to do with the actual signal value).
   private Graphics gr;
   private Font drawFont;
   private StringFormat drawFormat;
   private int iScale,                 // Scaling factor for plotting signals.
               iScaleDIV2;
   private double dScale;              // {double} of {iScale}.

   private int nextParameter_Y;        // Y coordinate to draw {deltaY}
                                       // and {timeStep} in function {DrawDelta_Y_X}.

   public SignalPlot( string _description, SignalShape _shape,
                      SignalContinuity _continuity = SignalContinuity.continuous )
   {
      InitializeComponent();

      bmp = new Bitmap( pictureBox1.Width, pictureBox1.Height );

      pictureBox1.Image = bmp;
      gr = Graphics.FromImage( bmp );
      gr.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
      gr.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
      gr.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
      gr.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
      gr.Clear( Color.Transparent );

      drawFont = new System.Drawing.Font( "Calibri", 10,
                                          FontStyle.Regular, GraphicsUnit.Point );
      drawFormat = new StringFormat();
      description = _description;
      shape = _shape;
      continuity = _continuity;
      xAxis_Y = bmp.Height / 2;        // Y coordinate of x-axis
      iScale = 10;                     // Arbitrary scaling factor.
      iScaleDIV2 = iScale / 2;
      dScale = (double)iScale;
   }// SignalPlot (constructor)

GeneratingFn 类构造函数初始化了几个 private 数据成员(sineSignalcosineSignalsquareSignalsawtoothSignaltriangleSignalwhiteNoiseSignal)以及 SignalPlot 类实例。这些实例可用于通过调用 SignalPlot.Plot 函数的以下函数来绘制生成的信号。

public void PlotSineSignal()
{
   sineSignal.Plot( parameters, sineSignalValues );
}// PlotSineSignal

public void PlotCosineSignal()
{
   cosineSignal.Plot( parameters, cosineSignalValues );
}// PlotSineSignal

public void PlotSquareSignal()
{
   squareSignal.Plot( parameters, squareSignalValues, SignalContinuity.discontinuous );
}// PlotSquareSignal

public void PlotSawtoothSignal()
{
   sawtoothSignal.Plot
   ( parameters, sawtoothSignalValues, SignalContinuity.discontinuous );
}// PlotSawtoothSignal

/// <summary>
/// By definition, a triangle signal is discontinuous because the derivative (slope)
/// of the function at the peaks does not exist. However, the discontinuity at
/// the peaks is not as sharp (i.e., vertical) as in a square signal or a sawtooth
/// signal. Hence, the third argument to function {triangleSignal.Plot} is left as
/// {SignalContinuity.continuous}.
/// </summary>
public void PlotTriangleSignal()
{
   triangleSignal.Plot( parameters, triangleSignalValues );
}// PlotTriangleSignal

/// <summary>
/// By definition, a white noise signal is completely discontinuous because it is
/// made up by random points on the amplitude vs. time scales. However, the
/// discontinuities are not as sharp (i.e., vertical) as in a square signal or a
/// sawtooth signal. Therefore, the third argument to function {whiteNoise.Plot}
/// is left as {SignalContinuity.continuous}.
/// </summary>
public void PlotWhiteNoiseSignal()
{
   whiteNoiseSignal.Plot( parameters, whiteNoiseValues );
}// PlotWhiteNoiseSignal

绘制信号的函数(SignalPlot.Plot)几乎是直观的。它将信号参数、时间和幅度点列表以及信号的连续性作为参数。

/// <summary>Plot a list of {TimeMagnitude} points.
/// </summary>
/// <param name="parameters">Parameters of the signal to be plotted.</param>
/// <param name="list">List containing the (time, magnitude) points.</param>
/// <param name="continuity">Continuity of the signal.
/// </param>
public void Plot( SignalParameters parameters, List<TimeMagnitude> list,
                  SignalContinuity continuity = SignalContinuity.continuous )
{
   int n, m;
   
   if ( list == null || ( n = list.Count ) == 0 )
   {
      MessageBox.Show(
         String.Format( "No {0} values to plot", description ) );
   }
   else
   {
      int x, deltaX, currY, nextY;
      
      // Increasing signal-magnitude values are drawn from the
      // bottom of the {Bitmap} to its top.
      sigMax_Y = 0;
      sigMin_Y = bmp.Height;
      
      Draw_X_axis();
      Draw_Y_axis();
      
      drawFormat.FormatFlags = StringFormatFlags.DirectionRightToLeft;
      
      DrawParameters( parameters, shape );
      
      deltaX = this.Width / n;
      x = 0;
      m = n - 2;
      
      drawFormat.FormatFlags = StringFormatFlags.DirectionVertical;
      
      for ( int i = 0; i < n; ++i )
      {
         int iScaledMag = ScaledMagnitude( list[ i ], dScale );
         
         currY = xAxis_Y - iScaledMag;
         
         if ( currY > sigMax_Y )
         {
            sigMax_Y = currY;
         }
         if ( currY < sigMin_Y )
         {
            sigMin_Y = currY;
         }
         if ( x >= bmp.Width )
         {
            break;
         }
         bmp.SetPixel( x, currY, Color.Black );
         
         if ( UtilFn.IsDivisible( list[ i ].Time, parameters.period ) )
         {
            string label = String.Format( "___ {0:0.0000}", list[ i ].Time );
            SizeF size = gr.MeasureString( label, drawFont );
            
            gr.DrawString( label, drawFont, Brushes.Red,
                           new Point( x, bmp.Height - (int)size.Width ),
                           drawFormat );
         }
         if ( continuity == SignalContinuity.discontinuous && i <= m )
         {
            int iP1ScaledMag = ScaledMagnitude( list[ i + 1 ], dScale );
            
            nextY = xAxis_Y - iP1ScaledMag;
            
            if ( x > 0 && ( shape == SignalShape.square || 
                            shape == SignalShape.sawtooth ) )
            {
               if ( i < m )
               {
                  CheckVerticalDiscontinuity( x, currY, nextY );
               }
               else // i == m
               {
                  DrawVerticalDiscontinuity( x + deltaX, currY );
               }
            }
         }
         x += deltaX;
      }
      Draw_Y_axisNotches( parameters );
      this.ShowDialog();                // Display form in modal mode.
   }
}// Plot

该函数确定 Y 坐标的最大值和最小值,绘制信号参数,为缩放的幅度数据点设置像素,并在 X(时间)坐标上绘制标签,这些坐标是信号周期可整除的,通过调用文件 Util_Lib.cs 中的实用程序函数来确定。

/// <summary>Determine whether a double is divisible by another double.
/// </summary>
/// <param name="x">Numerator of division.
/// </param>
/// <param name="y">Denominator of division.
/// </param>
/// <returns>Whether {x} is divisible by {y}.
/// </returns>
public static bool IsDivisible( double x, double y )
{
   return Math.Abs( ( ( Math.Round( x / y ) * y ) - x ) ) <= ( 1.0E-9 * y );
}// IsDivisible

在文件 Util_Lib.cs 中(两次)使用代码的原因是作者在其他应用程序中使用此类文件中的函数。该文件包含与数字信号的生成和绘制无关的其他函数。

由函数 SignalPlot.Plot 调用的函数几乎是不言而喻的。其中大多数(Draw_X_axisDraw_Y_axisDrawParametersScaledMagnitudeDraw_Y_axisNotchesDrawNotch)将在文章中不作描述。

函数 DrawDelta_Y_X 负责绘制信号的幅度和时间步长。时间步长是采样频率的倒数,采样频率是采样因子和信号频率的乘积。这些参数对于在其他应用程序中正确处理数字信号至关重要。

/// <summary>Draw the {amplitude} and the {timeStep} from the
///          parameters of a signal.
/// </summary>
/// <param name="parameters">Signal parameters.
/// </param>
private void DrawDelta_Y_X( SignalParameters parameters )
{
   // There are 11 notches on the Y axis.
   
   string delta_Y_X_str = String.Format( "deltaY: {0:00.000} V, time step: {1:0.00000} sec",
                                         parameters.amplitude / 5.0, parameters.timeStep );
                                         
   drawFormat.FormatFlags = StringFormatFlags.DirectionRightToLeft;
   SizeF size = gr.MeasureString( delta_Y_X_str, drawFont );
   
   int x = (int)size.Width + 8;
   
   Point point = new Point( x, nextParameter_Y );
   
   gr.DrawString( delta_Y_X_str, drawFont, Brushes.Red, point, drawFormat );
}// DrawDelta_Y_X

绘制方波和锯齿波信号垂直不连续性的两个有趣的函数。在到达方波或锯齿波信号的末尾之前,有必要检查是否必须绘制不连续性。此外,对于方波信号,不连续性必须绘制为向上或向下。对于锯齿波信号,不连续性总是向下。

/// <summary>Conditionally draw the discontinuity of a square or sawtooth signal.
/// </summary>
/// <param name="x">Position on the x axis (time).</param>
/// <param name="currY">Current position in the y dimension (units of magnitude).</param>
/// <param name="nextY">Next position in the y dimension (units of magnitude).
/// </param>
private void CheckVerticalDiscontinuity( int x, int currY, int nextY )
{
   if ( x >= bmp.Width )
   {
      return;
   }
   int discLength = Math.Abs( currY - nextY );
   
   if ( discLength > iScaleDIV2 )
   {
      int y;
      
      if ( currY < nextY )
      {
         for ( y = currY; y <= nextY; ++y ) // Discontinuity going down.
         {
            bmp.SetPixel( x, y, Color.Black );
         }
      }
      else // nextY < currY, Discontinuity going up.
      {
         for ( y = currY; y >= nextY; --y )
         {
            bmp.SetPixel( x, y, Color.Black );
         }
      }
   }
}// CheckVerticalDiscontinuity

在方波或锯齿波信号的末尾,不连续性被无条件绘制。

/// <summary>Draw the vertical discontinuity at the end of a square or sawtooth signal.
/// </summary>
/// <param name="x">Position on the x axis (time).</param>
/// <param name="currY">Current position in the y dimension (units of magnitude).
/// </param>
private void DrawVerticalDiscontinuity( int x, int currY )
{
   if ( x >= bmp.Width )
   {
      return;
   }
   int y;
   
   if ( currY < sigMax_Y )
   {
      for ( y = currY; y <= sigMax_Y; ++y )
      {
         bmp.SetPixel( x, y, Color.Black );
      }
   }
   else if ( currY > sigMin_Y )
   {
      for ( y = sigMin_Y; y <= currY; ++y )
      {
         bmp.SetPixel( x, y, Color.Black );
      }
   }
}// DrawVerticalDiscontinuity

测试信号生成库

可以编写一个简单的控制台应用程序来测试信号生成库。此应用程序的代码位于附带 ZIP 文件中 TestSignalGenLib 目录下的 Program.cs 文件中。Program 类定义了两个私有的文件相关变量,用于写入发送到控制台应用程序命令提示符窗口的输出。这些变量在应用程序的 Main 函数中初始化如下

fs = new FileStream( @"..\..\_TXT\out.txt", FileMode.Create );
sw = new StreamWriter( fs );

唯一的 public global 变量是 genFnMain 函数将此变量绑定到 GeneratingFn 类的一个实例,然后定义生成信号的枚举器。

IEnumerator<double> Sine = genFn.NextSineSignalValue();
IEnumerator<double> Cosine = genFn.NextCosineSignalValue();
IEnumerator<double> Square = genFn.NextSquareSignalValue();
IEnumerator<double> Sawtooth = genFn.NextSawtoothSignalValue();
IEnumerator<double> Triangle = genFn.NextTriangleSignalValue();
IEnumerator<double> WhiteNoise = genFn.NextWhiteNoiseSignalValue();

所有信号发生器都会被调用固定次数,以在命令提示符窗口中枚举和显示信号值。然后,显示时间步长和零交叉点。最后,在 Windows 窗体中绘制信号值。例如,以下代码对应于正弦信号的情况。

int n = 512;
string signalName;

signalName = "Sine";
EnumerateValues( signalName, Sine, genFn.sineSignalValues, n );
DisplayTimeStepAndZeroCrossings( genFn, signalName );
genFn.PlotSineSignal();

在执行结束时,文件 @"..\..\_TXT\out.txt" 包含发送到控制台应用程序命令提示符窗口的所有文本输出。为简洁起见,此处仅显示信号的第一周期和最后一个周期。为了便于参考,对 TimeMagnitude 数据点和零交叉点进行了编号。

正弦信号值

  0  0.0000   0.0000     1  0.0003   0.9755     2  0.0006   1.9134     3  0.0009   2.7779
  4  0.0013   3.5355     5  0.0016   4.1573     6  0.0019   4.6194     7  0.0022   4.9039
  8  0.0025   5.0000     9  0.0028   4.9039    10  0.0031   4.6194    11  0.0034   4.1573
 12  0.0038   3.5355    13  0.0041   2.7779    14  0.0044   1.9134    15  0.0047   0.9755
 16  0.0050   0.0000

. . .

496  0.1550   0.0000   497  0.1553  -0.9755   498  0.1556  -1.9134   499  0.1559  -2.7779
500  0.1562  -3.5355   501  0.1566  -4.1573   502  0.1569  -4.6194   503  0.1572  -4.9039
504  0.1575  -5.0000   505  0.1578  -4.9039   506  0.1581  -4.6194   507  0.1584  -4.1573
508  0.1587  -3.5355   509  0.1591  -2.7779   510  0.1594  -1.9134   511  0.1597  -0.9755   

GeneratingFn 构造函数设置的时间步长:0.00031

GeneratingFn.NextSineSignalValue 找到的零交叉点

  0  0.0000    1  0.0050    2  0.0052    3  0.0100    4  0.0150    5  0.0200    6  0.0250
  7  0.0300    8  0.0350    9  0.0400   10  0.0450   11  0.0500   12  0.0550   13  0.0600
 14  0.0650   15  0.0652   16  0.0700   17  0.0702   18  0.0750   19  0.0752   20  0.0800
 21  0.0802   22  0.0850   23  0.0852   24  0.0900   25  0.0902   26  0.0950   27  0.0952
 28  0.1000   29  0.1002   30  0.1050   31  0.1052   32  0.1100   33  0.1102   34  0.1150
 35  0.1152   36  0.1200   37  0.1202   38  0.1250   39  0.1252   40  0.1300   41  0.1302
 42  0.1350   43  0.1352   44  0.1400   45  0.1402   46  0.1450   47  0.1452   48  0.1500  

49 0.1502 50 0.1550 51 0.1552

在命令提示符窗口中显示信号值和零交叉点后,应用程序会显示如下所示的信号图

该窗体以模态模式显示(通过在 SignalGenLib.Plot 函数中调用 Form.ShowDialog)。作为第二个示例,下图显示了一个 500 Hz 正弦信号的图。零交叉点列在图下方

GeneratingFn.NextSineSignalValue 找到的零交叉点

  0  0.0000    1  0.0010    2  0.0020    3  0.0030    4  0.0040    5  0.0050    6  0.0060
  7  0.0070    8  0.0080    9  0.0090   10  0.0100   11  0.0110   12  0.0120   13  0.0130
 14  0.0140   15  0.0150   16  0.0160   17  0.0170   18  0.0180   19  0.0190   20  0.0200
 21  0.0210   22  0.0220   23  0.0230   24  0.0240   25  0.0250   26  0.0260   27  0.0270
 28  0.0280   29  0.0290   30  0.0300   31  0.0310

请注意,尽管 100 Hz 和 500 Hz 的正弦信号看起来相同,但它们并不相同,因为时间轴标记的值不同。此外,由于它们的频率不同,第一个信号穿过时间轴 52 次,而第二个信号穿过时间轴 32 次。

当通过单击其右上角的交叉按钮关闭 Windows 窗体时,程序会运行类似的代码,使用与生成正弦信号相同的参数来生成余弦波、方波、锯齿波、三角波和白噪声信号的图。每次显示信号图时,必须关闭它才能生成并显示下一个信号图。以下两张图显示了方波和白噪声信号的图。

命令提示符窗口显示白噪声情况下没有零交叉点。这是因为白噪声不像所有其他离散信号,其相邻点遵循规则的趋势。

作为额外示例,下图显示了一个 100 Hz 的三角波信号,其幅度为 6 伏特,直流偏移量为 2.5 伏特,这是由以下代码生成的

genFn = new GeneratingFn( 6.0, 100.0, 2.5 );

IEnumerator<double> Triangle = genFn.NextTriangleSignalValue();

signalName = "Triangle";
EnumerateValues( signalName, Triangle, genFn.triangleSignalValues, n );
DisplayTimeStepAndZeroCrossings( genFn, signalName );
genFn.PlotTriangleSignal();

Using the Code

附带的 ZIP 文件包含三个目录中的八个文件。Util_Lib 目录包含文件 UtilFn.csSignalGenLib 目录包含文件 GeneratingFn.csSignalParameters.csSignalPlot.csSignalPlot.Designer.csSignalPlot.resxTimeMagnitude.csTestSignalGenLib 目录包含文件 Program.cs

创建一个目录“Generation of Digital Signals”。在 Visual Studio 中,单击“文件”,选择“新建”,然后单击“项目”。选择“类库”,将创建的目录指定为“位置”,并将“名称”指定为“Util_Lib”。在解决方案资源管理器窗格中,右键单击“Class1.cs”,选择“重命名”并将类名更改为“UtilFn.cs”。将附带 ZIP 文件中“UtilFn.cs”文件的代码复制到创建的“UtilFn.cs”文件中。单击“生成”,然后单击“生成解决方案”。构建应成功。单击“文件”,然后单击“关闭解决方案”。

重复以上步骤创建一个名为“SignalGenLib”的库。在解决方案资源管理器窗格中,右键单击“Class1.cs”,选择“重命名”并将类名更改为“GeneratingFn.cs”。右键单击“引用”,选择“添加引用”,单击“.NET”选项卡,选择“System.Windows.Forms”并单击“确定”;同样添加对“System.Drawing”的引用。右键单击“引用”,选择“添加引用”,单击“浏览”选项卡,导航到目录“Util_Lib\bin\Debug”,选择“Util_Lib.dll”并单击“确定”。用附带的“GeneratingFn.cs”文件的内容替换刚刚创建的“GeneratingFn.cs”文件的全部内容。选择“文件”并单击“全部保存”。

将文件 "SignalParameters.cs"、"SignalPlot.cs"、"SignalPlot.Designer.cs"、"SignalPlot.resx" 和 "TimeMagnitude.cs" 复制到 "SignalGenLib" 目录。对于每个复制的文件,在 **解决方案资源管理器** 窗格中,右键单击“SignalGenLib”,选择“添加”,单击“现有项”,选择要添加的文件并单击“添加”。错误列表窗格应显示 0 错误,0 警告和 0 消息。单击“生成”选项卡,然后单击“生成解决方案”。构建应成功。单击“文件”,然后单击“关闭解决方案”。

单击“文件”,选择“新建”,然后单击“项目”,选择“控制台应用程序”,名称为“TestSignalGenLib”。在 **解决方案资源管理器** 中,右键单击“引用”,单击“添加引用”,单击“浏览”选项卡,导航到目录“SignalGenLib\bin\Debug”,选择“SignalGenLib.dll”并单击“确定”。单击“文件”然后单击“全部保存”。添加对“SignalGenLib.dll”的引用。将文件中的全部内容替换为附带的“Program.cs”文件的全部内容。单击“生成”然后单击“生成解决方案”。构建应成功。在 "TestSignalGenLib" 目录下创建一个名为 "_TXT" 的目录。单击“调试”然后单击“开始但不调试”。控制台应用程序应为每个信号生成信号值并显示一个图。关闭当前 Windows 窗体图以生成下一个信号的值并显示其图。关闭白噪声图后,按任意键退出控制台应用程序。

结论

本文讨论了一个 C# 库的设计、实现和测试,用于生成和显示一些常见的数字信号。信号生成函数实现为枚举器,通过使用 yield return,它们实际上在连续调用之间维护了局部变量的状态。该库将再次用于测试数字双二阶带通滤波器的实现。此类测试的结果将在即将发表的文章中报告。

历史

  • 2022年3月2日:初始版本
© . All rights reserved.