电赛模拟赛-信号发生器


写在前面

每个野生电子工程师的成长都要经历自己做出信号源和示波器的过程(bushi)。
这个过程的成长与和解,就在发现自己做出的东西总有些地方性能上不去,而氪金在大多数时候都是有用的。
不过也不尽然,通过算法和设计上的优化,总能以小成本逼近最大性能的,就像人生,你总要想办法把手里的烂牌打好。

题目描述

A题信号发生器
信号发生器一般有两种方法,一种是DAC方案,一种是DDS芯片方案。
我们在训练的时候两种方案都做了,DAC方案需要考虑的比较多,主要的问题是DAC的高频衰减和接近极限频率时的步进过大。
前者可以通过外接运放和软件系数修正来缓解,后者是波表打点法的特性,基本没有什么缓解的方法。
而DDS芯片方案使用的是AD9910,可以做到很精细的频率和幅度步进。有多种调制模式,可以生成正弦,方波,三角波,矩形波等任意信号。
但是由于芯片的输出幅度只有0-800mv,还需要接后级的放大电路,如果要做到高频的话,这就要求后级放大电路要能够跟得上。

STM32的片上DAC方案

DAC原理简介

DAC的基本原理就是通过打开不同权重的电阻网络开关把输入的数字量转换成一个模拟量电压输出。
下面是DAC的简化原理图。可以看到,是通过打开电流源的开关,来分配一个加法器的电流,最后转化为一个电压来输出。
DAC电路原理

DAC的性能指标

  • 分辨率
    分辨率是模拟输出电压可被分离的等级数,n位DAC分辨率一般为1/2^n。位数越高,分辨率越高。
    STM32为12位ADC,理论最小步进电压3.3/4096=8mv,还是比较精细的。
  • 转换速度
    用来描述数字量变化引起模拟量变换的转换时间。
    用来衡量此指标的参数有,建立时间和转换速率。
    1)建立时间
    指数字量变化,输出电压在规定误差范围内所需要的时间。这个跟是否使用了output buffer有关。
    2)转换速率
    指大信号工作状态下,模拟输出电压的最大变化率。这个决定了信号的压摆率。

DAC生成波形的原理

已知DAC能输出一个模拟电压量,而一个信号波形是由一系列时间上连续的电压信号组成的。
如果要输出一个正弦信号(称之为目标正弦函数),这就需要每隔一段时间就输出一个电压量,电压量的取值正是正弦函数在对应时间的取值。
那么从时间上看,我们就通过不断的打点获得了一个正弦波形。
正弦波首尾相接的时间,就是正弦的周期。也就是T=每一次打点的时间间隔X波表长度,这里的时间间隔就是定时器的更新时间,转换到频率关系上,就是f=定时器频率/波表长度
在目标正弦函数周期给出的情况下,波表长度也就确定了。
所以我们是通过调整定时器频率和波表长度来确定目标函数的频率。
比如定时器的频率为10MHz,如果我们需要生成一个100K的正弦信号,波表长度就为100。
如果我们需要生成一个1M的信号,波表长度就为10。

波表长度的问题

波表长度最好要大于50位,这是为了保证一个周期有足够的点,以免产生失真。
但是由于单片机的RAM有限,波表也不能做的太大。
如果定时器的频率很高,而我们需要的信号频率很低。
比如定时器频率为100M,要生成一个10Hz的正弦波,就需要10M个点,每点12位,为unsigned short数据类型,占2个byte,也就是20MB的空间,这对于一般的单片机来说是承受不起的。
不过好在单片机的定时器的频率可以自由改动,我们只需要停止计时器,然后用新的配置Initial一下就可以了。
比如将定时器的频率改成100K,这样生成一个10Hz的正弦波,就只需要10K个点了。

DMA+DAC的具体配置

在DAC将定时器设置成TIM4更新触发,output buffer选择关闭(后面会详细讨论),打开DMA,选择从内存到外设,模式选择循环,字长选择半字(2个字节)。
每次TIM4重装载更新时,就会给DMA一个请求,DMA就会从内存中的波表搬运一个数字给到DAC。
这样依次循环下去,就源源不断的生成了波形。
DAC里打开定时器触发
DMA里开循环模式,半字

代码编写

波表生成很简单,这个正弦里的Coefficient是为了补偿高频衰减,将在后面一节讨论。

void SineWave_Coefficient_Generate(int num, unsigned short *D, double Vlow, double Vhigh, int fre) // 正弦波生成
{
  double gap = fabs(Vhigh - Vlow) / 2, mid = (Vhigh + Vlow) / 2;
  double Coefficient = 18.28 * pow(fre / 1000, -0.02011) - 15.56;
  //double Coefficient = 1;
  printf("%d KHz Coefficient %lf\n", fre / 1000, Coefficient);
  for (int i = 0; i < num; i++)
  {
    if (fre >= 150000) // 若频率大于150K,增加系数补偿
      D[i] = ((mid + gap * sinf(2.0 * PI * 1.0 * i / (num - 1)) / Coefficient) * 4095 / 3.3);
    else
      D[i] = ((mid + gap * sinf(2.0 * PI * 1.0 * i / (num - 1))) * 4095 / 3.3);
  }
}

void SquareWave_Generate(int num, unsigned short *D, double Vlow, double Vhigh, int duty) // 方波生成
{
  int high = (1.0 * duty / 100) * num;
  for (int i = 0; i < high; i++)
  {
    D[i] = Vhigh / 3.3 * 4096;
  }
  for (int i = high; i < num; i++)
  {
    D[i] = Vlow / 3.3 * 4096;
  }
}

void TriangleWave_Generate(int num, unsigned short *D, double Vlow, double Vhigh, int duty) // 三角波生成
{
  int rising_time = (1.0 * duty / 100) * num;
  int falling_time = num - rising_time;
  double gap = fabs(Vhigh - Vlow);

  for (int i = 0; i < rising_time; i++)
  {
    D[i] = (Vlow + (Vhigh - Vlow) * i / (rising_time - 1)) * 4095 / 3.3;
  }

  for (int i = rising_time; i < num; i++)
  {
    D[i] = (Vhigh - (Vhigh - Vlow) * (i - rising_time) / (falling_time - 1)) * 4095 / 3.3;
  }
}

生成波形的代码

void Wave_Start(int type, double mvpp, int fre) // 启动波形发生
{
  int i, cnt, actual_fre; // i频率分辨率 cnt波表点数 actual_fre实际的频率

  /*根据频率确定定时器时基*/
  if (fre >= 10000)
    SetTimBase(10000000);
  else
    SetTimBase(100000);

  /*根据频率计算波表长度*/
  i = fre / (Global_TIMBASE / Table_MAXLength);
  cnt = Table_MAXLength / i;
  actual_fre = Global_TIMBASE / cnt;

  /*生成波表*/
  if (type == Sin)
    SineWave_Coefficient_Generate(cnt, WaveData, 1.6 - 1.0 * mvpp / 2000, 1.6 + 1.0 * mvpp / 2000, fre);
  else if (type == Square)
    SquareWave_Generate(cnt, WaveData, 1.6 - 1.0 * mvpp / 2000, 1.6 + 1.0 * mvpp / 2000, fre);
  else if (type == Triangle)
    TriangleWave_Generate(cnt, WaveData, 1.6 - 1.0 * mvpp / 2000, 1.6 + 1.0 * mvpp / 2000, fre);

  /*关闭并重启DAC输出*/
  HAL_TIM_Base_Stop(&htim4);
  HAL_DAC_Stop_DMA(&hdac1, DAC_CHANNEL_2);
  HAL_TIM_Base_Start(&htim4);
  HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_2, (uint32_t *)WaveData, cnt, DAC_ALIGN_12B_R);

  /*显示频率,幅值,种类*/
  Display_Vpp(mvpp);
  Display_ActualVpp(mvpp);
  Display_Fre(fre);
  Display_ActualFre(fre);
  Display_Type(type);
}

增强DAC的性能

上一节我们从弄明白了DAC是如何产生一个波形的,这一节我们从电路原理入手,探究影响DAC性能的原因。
找到合理的解决方式来最大程度的提醒电路的性能。

内置Buffer的问题

DAC等效电路图,此时为关闭内置buffer的状态
根据DAC的等效电路图,在不打开内置的output Buffer的情况下,DAC的输出端后面可以看成一个RC低通滤波器,R是DAC的内阻和运放反馈电阻之和,C是由于导线的寄生电容和后级的一些容性负载产生的。在高频下会对信号产生较大的衰减。
而如果使用了内置的运放,即S1、S2都闭合,根据电路图,这是一个反向跟随器,由于负反馈的存在,输入阻抗接近于0,也就是时间常数τ=RC≈0
这样就基本消除了RC低通滤波器的影响,但是…但是…. 凡事都没那么十全十美。
问题就在于这个内置的运放性能比较垃,压摆率不高,带宽也不大,带载能力也不强,(这些都是相比)这就导致了输出高频信号时,波形会很“生硬”。
对没错,就是你,输出的正弦波像三角波。
打开buffer的高频正弦波

既然如此,我们可以不用内置的运放,而是选择自己加一块反向跟随器。
通过选择性能合适的运放,我这里选的是OPA2211,很强的一块低噪精密运放,这样基本能极大缓解DAC的高频衰减。
电路图啥的我就不放了,很简单,就是一个反向跟随器。

系数修正的波表

但是还是没有那么绝对,当信号来到MHz以上时,示波器上还是能看出肉眼可见的衰减。
所以我们只能通过软件修正。我这里使用的方法是记录下高频段的幅值,然后用matlab拟合出一条曲线。
然后再在DAC波表生成函数里面对于某一特定的频率乘上这个系数,这样就达到了幅值系数修正的效果。
要注意的是,此时的电压上限就被降低了。
这是由于高频下有较大的线路损耗(暂且这么称呼和理解),必须抬高DAC的输出电压,在测量端看到的结果才能和低频下相同。
举个例子。比如最大频率下的幅值只有低频下的0.5,那么整个系统的最大输出电压就会被限制在3.3X0.5=1.65V
Matlab拟合出了一条幂函数曲线

高频下频率步进大

DAC+DMA的形式虽然解放了CPU,但是有一个麻烦的一点,就是在接近极限频率时,频率步进越来越大,这是由于单片机的波表在DMA下做不到FIFO循环的形式。
比如最大频率是10MHz,那么波表长度为10,波形频率为1MHz,波表长度为9,波形频率为1.11MHz,波表长度为8,波形频率为1.25MHz
可以看到它的频率步进是越来越大的。
究其本质原因,是DMA的波表点数和值一旦确定就不能更改,所以只能以定时器频率/N的形式来步进。

既然这样的话,那么我们想到,可以用一个长波表,做成首尾相接,无限延展的模式,我们在其中间隔取点。那样的话,频率就取决于每次间隔点的个数,而且可以全频段以最小频率步进。
或者直接根据正弦函数算出下一个值并输出。
20220719133120
具体到STM32上的操作,可以用定时器的周期中断来触发DAC,在中断处理函数中为了避免计算耗时,推荐使用查表法,能在一次取址时间内获得需要的值。
例如定时器的频率是1KHz,波表的长度是100,那么频率步进就可以做到10Hz。
这里举一个如何生成30Hz的例子,每3点取一个点,也就是说,第1次在中断中读出的是Buffer[0],第2次是Buffer[3],第3次是Buffer[6],第4次是Buffer[9],……,第34次是Buffer[99],
那么第35次取的应该是99+3=102,由于溢出了,所以需要减去100,也就是Buffer[2],以此类推,就算波表长度不能被取点间隔数整除,我还是可以将波表里的100个点都走到,所以频率分辨率还是保持为10Hz。
这种灵活的方式让我们摆脱了DMA下波表一旦固定,就无法更改的情况。
但是这样的话,在高频下CPU容易被占满,无法再干别的事,甚至无法响应按键。
不过也没有关系,想要别的频率Reset重设就好了。

DDS芯片方案

AD9910的数据手册

使用的是AD9910,一块单通道的DDS芯片,最高能到420M,幅度0-600mv可调。
DDS芯片的介绍我就不多说了,其基本原理与DAC比较类似。但是由于其时钟频率可以做到很高,如AD9910的时钟频率为1GHz,
所以它最大能输出的频率也非常高,AD9910最大正弦输出能到400M左右。
同时由于它的波表非常长,于是频率步进又可以做到很精细,例如AD9910的累加计数器是32位的,等效于一个2^32的波表。
1G/(2^32) = 0.233Hz,频率步进能做到这么低的一个程度,而且还是全频段都是如此,
驱动方式为GPIO驱动。有多种调制方式,可以生成正弦,方波,三角,锯齿,甚至是任意波形。
我写了一份的驱动,封装好了正弦,方波,三角。改一下管脚直接调用即可。
AD9910.c
https://megrez-hong.oss-cn-shanghai.aliyuncs.com/blogs/AD9910.c
AD9910.h
https://megrez-hong.oss-cn-shanghai.aliyuncs.com/blogs/AD9910.h

一些图片

DAC输出的正弦波
DDS输出的锯齿波

参考链接

利用STM32的片上DAC实现DDS(数字频率合成):https://www.emoe.xyz/stm32-dac-direct-digital-synthesis/


文章作者: Allen Hong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Allen Hong !
  目录