利用STM32的片上DAC实现DDS(数字频率合成)

利用STM32的片上DAC实现DDS(数字频率合成)

前言

本篇文章参考了《新概念模拟电路-源电路与信号源》中的6.3节。
后续还会发布基于FPGA的DDS版本,敬请期待哦ovo

本文用到的资源

DDS

Direct-Digital Synthesizer是一种频率合成技术,用于产生周期性波形。目前,从低频到上百MHz的正弦波、三角波的产生,绝大多数都用DDS完成。包括我们买到的信号源之类的,里面的核心也是DDS芯片。传统的模拟电路——即振荡器产生正弦、方波、三角波的方式在DDS面前都得叫大哥:D

DDS的优点有:

  • 可精细选择输出频率,实现从低到高的频率选择。
  • 可快速跳频,且可以保证相位连续,这在模拟电路方法中是难以实现的。
  • 可实现正交输出,可实现相位设置
  • 可实现正弦、三角波输出,配合比较器可实现同频同相方波输出
  • 事实上,只要有波表,随便什么波形都能给你打出来。。(你甚至可以在示波器屏幕上看Badapple)

当然DDS也不是浑身是宝,他也有弊端:

  • 在发出高质量的正弦波中,DDS无法实现超低失真度,这是最大的弊端。(因为是一个点一个点地突变,不可避免会引起非线性失真,即增加了高频谐波分量)
  • DDS中使用的DAC位数不会很高(常见的14位),其积分非线性INL不可能做到很小,其次DDS一般采用普通DAC,没有为降低失真度做出更多考虑
  • 目前DDS实现的正弦波输出,失真度一般只能做到-80dB左右。

DDS核心思想

先假设DDS有一个固定的时钟MCLK=36MHz。

要实现高精度的DDS,我们需要一个正弦波的相位-幅度表,且它应具有足够细密的步长,比如0.01°。那么一个完整的正弦波表就需要36000个点。

sin talbe

如上图所示,N代表相位点的序号,phase代表这个点对应的正弦相位,Am代表这个点对应的正弦的值(也就是sin(phase)),Data_10代表这个值对应的10位DAC的Code。

一个10位的DAC全幅度为1023,我们令code=1023时对应正弦的峰值,也就是1。code=0时对应正弦的谷值,也就是-1。所以正弦的值为0时,对应的code为512。

在前12个相位点,正弦的幅度变化非常小,以至于DAC输出的Code一直是512。在第13个相位点,DAC输出Code增加了1。由此可见这个具有36000点的波表几乎记录了一个标准正弦波的全部。

DDS表的前100点

将这个表首位衔接,假设相位步长m=1,以DDS主时钟MCLK为节拍,依序发作:

  • 第一个CLK时,DAC输出N=0时对应的DATA_OUT,即512.
  • 第二个CLK时,DAC输出N=1时对应的DATA_OUT,即512
  • ……
  • 当36000个CLK过去之后,一个完整的正弦波便被打了出来。

之前我们假设DDS时钟为F_MCLK=36MHz
那么这个正弦波的频率就非常容易算出了:


f_{out} = {1 \over T_{MCLK}* {N_{max} \over m}} = {f_{MCLK}*m \over N_{max}} = {36*10^6*1 \over 36000} = 1000Hz

上式代表了DDS最核心的计算,参数含义如下:

  • T_MCLK 为DDS主振荡器时钟周期,即1/36MHz,约为27.78ns。
  • N_max为正弦波波表总点数,为36000
  • m为循环增加中的步长,这里m=1,意味着逐个遍历整个表格。

如果m=2,意味着隔一个数据点扫一遍,那么此时输出信号的频率将翻倍。
m越大意味着间隔越大,扫描完的周期就越短,输出频率就越高。

可见,改变步长m就可以改变输出频率,当然,改变时钟频率也可以。不过一般采用固定的时钟。

那么DDS的最小分辨率就是:


\Delta f_{OUT} = {f_{MCLK}* \Delta m \over N_{max}}

根据上述参数确定的DDS输出最小分辨率为1000Hz。

实际DDS

前面假设的DDS主频不高,样点不多。

我们来看一个专用DDS芯片的内部框图。
AD9833是一款28位分辨率,25MHz完整DDS芯片。

ad9833

DDS核心由相位累加器PA相位幅度表(波表)数模转换器(DAC)组成。

对应上面的框图是这样的(好啦知道某些读者嘤语不好了orz):

  • Phase accumulator(28-BIT)是28位相位累加器
  • SIN ROM是只读存储器(Read-Only Memory),存储了正弦波表
  • 10-BIT DAC就是10位数模转换器
  • 其他的框图你可以暂时忽略不看~

28位相位累加器意味着什么呢?意味着它可以计数0~2^28,或者说,它的相位表点数为2^28=268435456点(2亿6800万),远远大于36000点。

如果我们要使用这个DDS,我们需要输入一个计数步长m(当然这个m小于2^28)。此后外部时钟MCLK每出现一个脉冲,相位累加器PA完成一次累加(以计数步长m为步进,在SIN ROM里等间隔抽值),DAC输出一个从正弦波表里抠出来的值。

那么这个DDS的最小频率分辨率是:

\Delta f = {1 \over 2^{28} }*f_{MCLK}= {25MHz \over 2^{28}} = 0.0931Hz

多么恐怖的分辨率.jpg
也就意味着你可以设置这个DDS输出频率为10khz,10.0000931kHz,10.0001862kHz……

这个DDS输出的最高频率是FMCLK的一半。这时候整个波表中只有2个点会被扫描到,这时候输出的是一个三角波,而非正弦。

那么..32怎么做呢

斯密马赛,我们的STM32并不是DDS专用芯片,是存储不了如此恐怖的数据量(指超长波表)的。
那么我们从DAC的工作方式出发吧——

我们先解释方法,原因之后讲。
通常如果要使用STM32的DAC进行高速转换,我们需要用到DMA传输,数据流如下图所示:

DAC DMA

首先我们手搓一个波表出来(具体后面讲),然后把他存到RAM中(当然是RAM咯,实时运算出来的…),然后我们开启一个定时器,比如TIM8,配置成更新事件模式(Update Event)。

接下来,开启DMA传输,将DAC与DMA总线链接起来(Link),并指定DMA传输的源地址(source address),也就是我们存储在RAM里的波表数组的起始地址,同时指定DMA传输的长度(也就是波表长度)。

完成上述初始化操作之后,每当一个定时器更新事件出现时,DMA就会将一个波形数据点传输到DAC的输出保持寄存器中,过一会儿这个数据就会被自动装载到DAC的输出寄存器中,DAC便按这个数据进行相应的输出。

看到这里是不是明白了?我们将这个系统与前文的DDS对比一下:

  • 定时器更新事件的频率 = DDS的主振荡器频率
  • RAM中的波形数据长度 = DDS的波表长度
  • 相位累加器步长m = 1,而DDS的m是可变的

为什么我们不用像真正DDS那样的固定波表呢?原因有2:

  • STM32的DMA的 增量地址是连续的,也就是说DMA搬运数据时只能搬运地址连续的存储空间。
  • STM32的存储空间有限,无法容纳如此巨量的数据。(毕竟是身娇体柔的单片机嘛)

那么我们如果需要一个可变长度的波表, 最好的办法就是现算现用 ,这样才能实现可调步长,也即实现输出频率可调。
并且,使用STM32有一个专用DDS芯片不太好实现的功能——可变时钟频率,只要更改定时器的更新事件触发频率,就改变了"DDS"的时钟频率。

那么我么来实践一下。

STM32的DAC

STM32的DAC在手册中注明了转换速率可达1Msps。但是这还不够—— 我们要更快!

参考这篇手册,Extending the DAC Performance of STM32 microcontrollers

文中提到了一件非常重要的事——STM32的DAC等效模型,以及为何DAC的输出速度提不上去

STM32的DAC等效模型

model

可以看到,当输出Buffer关闭的时候,DAC的等效输出阻抗为Ra+Rb,RDAC=2Ra。当输出Buffer开启时,一个内置的运放配置为反相放大模式,增益为-1,输出阻抗接近于0。(低阻)

但是当使用了输出Buffer的时候也带来了几个问题:

  • 输出幅度被限制为正负电源轨±0.2V (因为这个运放并非轨到轨运放)
  • 输出速度取决于这个运放的性能(通常只能到1MHz)

同时手册举了个例子,见下图:

explain

简单来说,在输出Buffer关闭的情况下,如果输出有容性负载(事实上绝大多数情况下不可避免),如果要得到1LSB精度的输出值(也就是DAC输出稳定下来),通过RC充电电路的计算公式可以得出 DAC的Settling time

算出来约为1.8us,也就是DAC的输出速率最高只能到 555kHz 左右!
而且这个计算还没考虑到DAC在高速运行时的其他寄生参数带来的影响,也就是说实际上这个最大速率会更低。

外部运放-提升性能

诶..我这么好的性能,怎么就速度上不去呢?(我没有开某人(bushi))

既然内部的Buffer太弟弟了,我们就外接一个牛逼点的。

Ex op

在这个模型中,限制DAC输出速度的主要有3个因素:

  1. RC常数
  2. 运放速度(压摆率,增益带宽积)
  3. DAC的数据更新率(主要是单片机限制)

当运用了外置运放之后,RC的影响就很小了,可以暂时忽略掉。现在对外置运放提出了要求——也就是高压摆率和高增益带宽积。(这个简单,氪金就行了。)

我使用了OPA2211,性能很牛的片子。
以下是我的电路图,用Kicad绘制。

opa2211

其中,0.5VBUS使用一个精密运放作为Buffer产生:

opa27

DAC的数据更新率

解决了RC和运放的问题,最后就是DAC的数据更新率了。
纵使我的外围硬件电路再牛逼,DAC的数据更新慢,输出频率一样上不去嘛。
先康康手册的描述:

update rate

有几种将数据传送到DAC的方法:

  1. 阻塞式,CPU将RAM中的数据搬运到DAC输出寄存器中(DOR
  2. 非阻塞式,利用DMA将CPU从繁重的苦力活里解放出来。DMA接管整个数据传输过程。

当然,我们是不希望CPU去当黑奴的,我们还要单片机干别的事情呢…
非阻塞情况下,我们使用定时器的更新事件去触发DMA搬运数据。所以数据更新率就取决于DMA总线的速度上限了。具体体现在以下几个参数:

  • APB总线或AHB总线的时钟周期(视型号不同)
  • DMA传输周期(从RAM到DAC的DOR寄存器)
  • 定时器触发频率

比如STM32F407x的APB1总线上,当触发信号之后的3个周期,DHR中的数据被转移到DOR中,同时DMA产生请求,DMA花费至少一个时钟周期将数据搬运完成。
满打满算算下来搬运一个数据总共需要4个时钟周期。如果APB1总线频率42MHz,那么DAC的数据更新率能达到10.5Msps(M sample per second)

当然这是F407的情况,别的型号具体还得查数据手册。
总之,DAC你还是蛮快的嘛(笑)

DMA Double Data Mode

还有一个小细节。

dma ddm

有些型号的DAC支持双数据模式。什么意思呢?
就是说DMA的总线是32-bit宽的,而DAC的数据是12-bit宽的,一般存储的话就使用uint16_t类型,是16-bit宽度。那么在DMA传输的时候,将2个数据一并传过去,占满DMA总线的位宽,可以提升2倍的数据速率(在定时器触发频率不变的情况下)。

当然,上限还是摆在那里的,这样做的意义是可以降低一半的定时器触发频率。

至此,我们找到了提升DAC输出速率的方法,从硬件到软件,从理论到实践,All Pass.

配置STM32

在CubeMx中配置STM32F303CCT6,这里就放最关键的配置啦,具体的可以看开头我传的工程文件~

配置DAC通道2,触发器选择 TIM8触发输出事件,同时禁用Buffer(具体为什么请看前文)

DAC Param

然后配置DAC的DMA通道:

  • 选择DMA2_CHANNEL_4(我在网上看有人说F3的DAC只能用DMA2,我也没验证过DMA1能不能用…)
  • Priority(优先级)设置为High
  • Mode 选择 Circular(传输完成之后回到开头自动重新开始,即不间断传输)
  • Data Width选择Word,即32位位宽,可以使用Double data Mode

dma conf

接下来是配置DAC的Trigger——TIM8

  • Clock source选择Internal,查看时钟树可以看到TIM8的clock是72MHz
  • 然后Prescaler(预分频)设置为0,不分频
  • Counter Period(计数周期)设置为18-1那么定时器的触发频率就是 72MHz / 18 = 4MHz
  • Trigger Event Selection TRGO选择Update Event,即更新事件,这个事件用于触发DAC的DMA去搬数据

别忘了,我们开了Double data Mode,定时器更新事件频率为4MHz,实际的DAC 输出 sampling rate应该是8MHz哦

TIM8

生成波形算法

其实很简单,就是算。
比如以下是一个算正弦的例子

#define DAC_AMP                         (uint16_t)1600
#define LUT_MAX_LENGTH                  (uint32_t)4096

volatile uint16_t        dds_lut[LUT_MAX_LENGTH];

if (type == SINE_WAVE)
    {
        float sin_step = 2.0f * 3.14159f / (float)(length-1);
        for (uint16_t i = 0; i < length; i++)
        {
            dds_lut[i] = (uint16_t)(DAC_AMP+(DAC_AMP*sinf(sin_step*(float)i)));
        }
    }

方波和三角波更为简单了,详细的请去文章开头的Git仓库看看吧~

测试!

输出200kHz的正弦波

200k

输出40kHz的正弦波+小板子合影

40k

还有更多测试图,待后续上传.jpg

发表回复