STM32H7利用片上DAC加DMA双缓冲实现DDS

一、前言背景

由于STM32H7的教程较少,以及LL库的教程较少,笔者就将自己的一些微薄见解发表于此,还望网友多多包涵。本笔记也参考了不少资料和文章,笔者只是拾人牙慧的小屁孩。
如果不使用DDS模块,仅通过修改定时器预分频系数(PSC)和自动重装载值(ARR)改变输出波表频率,想实现更加精确的频率步进确实很有限制。在了解STM32的双缓冲机制之后,决定使用DMA双缓冲搭配DDS算法的算法来实现波形发生。实现效果不仅波形好看,频率也相对准确。

二、原理概述

关于双缓冲的机制原理,可以到STM32H7解决DMA伪双缓存中的出现Cache问题,有笔者的相关介绍。
这里着重说一下DDS的原理概述:
DDS(Direct Digital Synthesizer)即数字合成器,是一种新型的频率合成技术,用于产生周期性的波形。到目前,从低频到上百MHz的正弦波、三角波产生等,绝大多数都采用DDS芯片完成,甚至可以说,真正实用的波形发生器,包括我们能买到的信号源,都是采用DDS实现的。
DDS

(一)频率控制

先假设DDS有一个固定的时钟ClkFreq,为36MHz。
然后再有一个正弦的“相位—幅度”的表,而且有非常细密的相位步长,就比如0.01°,对应也就是一个完整周期需要36000个点。

相位幅度
0.000.0000000
0.010.0001745
0.020.0003491
0.030.0005236
359.98-0.000349
359.99-0.000175

我们再用10位DAC来描述正弦波,也就是把刚刚的“相位—幅度”进行量化:

数组下标相位幅度10位DAC的值
00.000.0000000512
10.010.0001745512
20.020.0003491512
30.030.0005236512
110.110.0019199512
120.120.0020944513
35998359.98-0.000349512
35999359.99-0.000175512

其中也就是把 s i n ( 0 ° ) sin(0°) sin()作为DAC的10位范围的中心值 2 10 / 2 = 512 2^{10}/2=512 210/2=512,而 s i n ( 90 ° ) sin(90°) sin(90°)就是最大值 2 10 − 1 = 1023 2^{10}-1=1023 2101=1023,而 s i n ( 270 ° ) sin(270°) sin(270°)就应为最小值 0 0 0
就马上要有同学问了,DAC如果仅有10位的话,波表作这么细,很多DAC值都是重复的,这样有什么意义呢?
这就是DDS频率控制的核心。
现在假设步进为M=1,则DAC以ClkFreq的频率为节拍,每经过1/ClkFreq的时间,便输出对应的DAC值。比如,时间为1/ClkFreq时,输出对应DAC值512;时间为2/ClkFreq时,输出对应DAC值512…可以想象在36000/ClkFreq的时间之后,输出了整个波表。当然,此表首尾衔接,之后便是又一个循环的开始,如此往复。
故,输出波形的频率为:
f O U T = 1 T C l k F r e q × N m a x = C l k F r e q N m a x = 36 × 1 0 6 36000 = 1000 H z f_{OUT} = \frac{1}{T_{ClkFreq} \times N_{max} } = \frac{ClkFreq}{ N_{max} } = \frac{36 \times 10^{6}}{36000} = 1000 Hz fOUT=TClkFreq×Nmax1=NmaxClkFreq=3600036×106=1000Hz
我们考虑将步进M = 2,则每经过1/ClkFreq,便输出对应的DAC值,只不过,每次读表时,要跳着读,每次前进2个单位进行读表,并对应输出。比如,时间为1/ClkFreq时,对应数组下标为0,输出对应DAC值512;时间为2/ClkFreq时,对应数组下标为2,输出对应DAC值512…可以想象在(36000/2)/ClkFreq的时间之后,输出了整个波表。也就是说M越大间隔越大,则就能在更短的时间内输出整个波表。
而且波表作这么细,损失这些点依旧能完整还原波形。
如果把步进叫做频率控制字 F w o r d F_{word} Fword(因为它的数值大小能控制输出信号的频率大小),那么完整的公式就是:
f O U T = 1 T C l k F r e q × N m a x F w o r d = C l k F r e q × F w o r d N m a x = 36 × 1 0 6 × 2 36000 = 2000 H z f_{OUT} = \frac{1}{T_{ClkFreq} \times \frac{ N_{max}}{F_{word}} } = \frac{ClkFreq \times F_{word}}{ N_{max} } = \frac{36 \times 10^{6} \times 2}{36000} = 2000 Hz fOUT=TClkFreq×FwordNmax1=NmaxClkFreq×Fword=3600036×106×2=2000Hz
所以一个DDS能提供的最小分辨率来源于 Δ   F w o r d \Delta\ F_{word} Δ Fword的最小变化量 1 1 1,对应的频率变换 Δ   f O U T \Delta\ f_{OUT} Δ fOUT
Δ f O U T = C l k F r e q × Δ F w o r d N m a x = C l k F r e q N m a x \Delta f_{OUT} = \frac{ClkFreq \times \Delta F_{word}}{ N_{max} } = \frac{ClkFreq}{ N_{max} } ΔfOUT=NmaxClkFreq×ΔFword=NmaxClkFreq

波表作这么细,还有一个优点就是样点总数 N m a x N_{max} Nmax除以 F w o r d F_{word} Fword是可以为不等于整数的。因为波表的细密,当样点总数 N m a x N_{max} Nmax除以 F w o r d F_{word} Fword不为整数时,将以一个新的点进行下一个周期而不一定是从 s i n ( 0 ° ) sin(0°) sin(),但 s i n ( 0.39 ° ) sin(0.39°) sin(0.39°)或者 s i n ( 0 ° ) sin(0°) sin()的效果是看不出来的,并且也是满足首尾衔接的。这丝毫不会影响到总体频率上的呈现。

(二)相位控制

对于相位的调整,则非常简单了。只需要在每个取样点的序号上加上一个偏移量,便可实现相位的控制。比如,上面默认的是第1/ClkFreq时输出数组下标为0的数据,那么我们现在在第1/ClkFreq时输出数组下标为0 + P w o r d P_{word} Pword的数据,则将相位左移了 P w o r d N m a x × 360 ° \frac{P_{word}}{N_{max}} \times 360° NmaxPword×360°度,这就是控制相位的原理。
这里的 P w o r d P_{word} Pword数值大小控制输出信号的相位偏移,主要用于相位的信号调制,我们叫做相位控制字。

三、程序实现

这里仅呈现相关核心代码,具体完整过程可以见末尾仓库。

配置相关的STM32CubeMXDAC
DMA
定义相关变量:

    DAC* pdac;
    uint64_t    clkFreq;
    uint32_t    F_WORD = 1;    //频率控制字
    uint32_t    P_WORD = 0;    //相位控制字
    uint32_t    ROM_Index = 0;
    uint16_t*   pROM;
    uint32_t   	ROM_Num;

并给出接口函数

    bool Set_WaveROM(const uint16_t* Buffer, uint32_t Num);
    inline void Set_ClkFreq(uint64_t ClkFreq);
    inline void Set_FWORD(uint32_t FWORD);
    inline void Set_PWORD(uint32_t PWORD);
    void Irq_DMA(void);

我们使能半满中断和全满中断:

	/* DMA地址配置 */
	LL_DMA_SetPeriphAddress(dma, dma_stream , LL_DAC_DMA_GetRegAddr(dac, channel, LL_DAC_DMA_REG_DATA_12BITS_RIGHT_ALIGNED));
	LL_DAC_EnableDMAReq(dac, channel);
	LL_DMA_SetMemoryAddress(dma, dma_stream, (uint32_t)(dac_dma_buffer.Data));
	LL_DMA_SetDataLength(dma, dma_stream, dac_dma_buffer.Size);
	/* DMA中断配置 */
	Platform_DMA_ClearFlag_HT(dma, dma_stream);
    Platform_DMA_ClearFlag_TC(dma, dma_stream);
    LL_DMA_EnableIT_HT(dma, dma_stream);
    LL_DMA_EnableIT_TC(dma, dma_stream);
    /* DMA使能 */
	LL_DMA_EnableStream(dma, dma_stream);

并编写最核心的void Irq_DMA(void)

void DDS::Irq_DMA(void) {
    // 完成中断,处理ROM数组后半段
    if(LL_DMA_IsEnabledIT_TC(pdac->dma, pdac->dma_stream) && PLATFORM_DMA_IsActiveFlag_TC(pdac->dma, pdac->dma_stream))
    {
        for(uint32_t i = 0; i < (pdac->dac_dma_buffer.Size / 2); i++)
        {
            pdac->dac_dma_buffer.Data[ROM_Index] = pROM[P_WORD];
            ROM_Index++;

            P_WORD+=F_WORD;
            if (P_WORD >= ROM_Num)  //仅溢出后才进行取模运算,优化时间
                P_WORD %= ROM_Num;

            if (ROM_Index == pdac->dac_dma_buffer.Size)
                ROM_Index = 0;
        }
        Platform_DMA_ClearFlag_TC(pdac->dma, pdac->dma_stream);
    }
    // 半完成中断,处理ROM数组前半段
    if(LL_DMA_IsEnabledIT_HT(pdac->dma, pdac->dma_stream) && PLATFORM_DMA_IsActiveFlag_HT(pdac->dma, pdac->dma_stream))
    {
        for(uint32_t i = 0; i < (pdac->dac_dma_buffer.Size / 2); i++)
        {
            pdac->dac_dma_buffer.Data[ROM_Index] = pROM[P_WORD];
            ROM_Index++;

            P_WORD+=F_WORD;
            if (P_WORD >= ROM_Num)
                P_WORD %= ROM_Num;

            if (ROM_Index == pdac->dac_dma_buffer.Size)
                ROM_Index = 0;
        }
        Platform_DMA_ClearFlag_HT(pdac->dma, pdac->dma_stream);
    }
}

这里附上必要的解释:
首先,dac_dma_buffer.Data为DMA传输的内存地址,在半满中断时对前半段写值,在完成中断时对后半段写值,实现双缓存高速刷新。(可能存在的问题为DMA传输时间过快,ROM数据过多,以导致未正确刷新完本次的dac_dma_buffer.Data而波形失真)
其次,这里的设计为环形赋值:
举个例子来说

  • DAC的DMA缓冲区只有3个数据
  • 波表有8个数据
  • 步进为2

ddsbuf
它会自己形成一个周期函数,一个周期内实际DAC打点数取决于执行P_WORD %= ROM_Num;之前的总循环次数。按照定时器+DAC+DMA的方式,输出波形频率等于定时器触发频率除以总点数,当然,我们也可以迁移同样的知识,执行P_WORD %= ROM_Num;之前的总循环次数,也就是波表的总数除以步进,从上面的例子也可以看出 8 / 2 = 4 8/2 = 4 8/2=4次,换言之,就是一个周期实际打了4个点,所以它的输出频率:
f O U T = C l k F r e q × F w o r d N m a x = C l k F r e q × 2 8 = C l k F r e q 4 f_{OUT} = \frac{ClkFreq \times F_{word}}{ N_{max} } = \frac{ClkFreq \times 2}{ 8 } = \frac{ClkFreq}{ 4 } fOUT=NmaxClkFreq×Fword=8ClkFreq×2=4ClkFreq
我们发现,输出波形频率等于定时器触发频率除以总点数,也就是DDS的输出频率公式的核心。

最后再聊聊核心函数里一些看上去很笨的写法:

 	P_WORD+=F_WORD;
    if (P_WORD >= ROM_Num)
        P_WORD %= ROM_Num;

为什么不换成

	P_WORD = (P_WORD + F_WORD) % ROM_Num;

因为这个中断相当看重时间效率,每次取模花费的时间已经严重影响到输出波形了。(实战检验)

使用案例
实例化DDS对象
DDS class
编写Matlab脚本用于生成const数组(const数组会被放入Flash区,Flash区相对空间较大;当然,也可以指定放到RAM,加快运行效率,看具体情况取舍吧):

% 可更改的参数
num_points      =   4096;   % 点数
amplitude_mv    =   3.0;        % 振幅(伏)

% 生成正弦波数组
theta = linspace(0, 2*pi, num_points + 1);
U = amplitude_mv / 2;
scaled_sinusoidal_wave = round((U * sin(theta) + U) * 4095 / 3.3) + 300;

% 宏定义表示点数
fprintf('#ifndef DDS_ROM_WAVE_SIZE\n');
fprintf('#define DDS_ROM_WAVE_SIZE    (%d)\n', num_points);
fprintf('#endif\n\n');

% 打印数组数据
fid = fopen('../Platform/Platform-lib/DSP/Waveform_Table/SinWave.h', 'w');
fprintf(fid, '#ifndef DDS_ROM_WAVE_SIZE\n');
fprintf(fid, '#define DDS_ROM_WAVE_SIZE    (%d)\n', num_points);
fprintf(fid, '#endif\n\n');
fprintf(fid, 'const uint16_t SinWave_ROM[DDS_ROM_WAVE_SIZE] = {\n');
fprintf(fid, '    %d', scaled_sinusoidal_wave(1));
for i = 2:num_points
    fprintf(fid, ', %d', scaled_sinusoidal_wave(i));
    if mod(i, 10) == 0  % 每十个数换一行,可根据需要调整
        fprintf(fid, '\n');
    end
end
fprintf(fid, '};\n');
fclose(fid);

% 绘制图形
figure;
plot(scaled_sinusoidal_wave);
xlabel('数组下标');
ylabel('DAC量化值');
title('SinWave ROM');

static void DDS_WaveStart(void)
{
     uint64_t clkFreq = 409600;	//时钟
     uint32_t FWORD = 10;		//频率控制字
    dds.Set_WaveROM(SinWave_ROM, DDS_ROM_WAVE_SIZE);
    cout << "DDS分辨率为" << (double )(clkFreq)/DDS_ROM_WAVE_SIZE
         << "Hz \nF_out" << (double )(clkFreq * FWORD)/DDS_ROM_WAVE_SIZE <<"Hz \n";
    dds.Set_ClkFreq(clkFreq);
    dds.Set_FWORD(FWORD);
    dds.Start();

}

串口输出结果为

DDS分辨率为100.000Hz 
F_out1000.000Hz 

实际效果为
sin1
sin2
sin3

附上存在RAM里的版本:

void generateSineWaveTable(uint32_t Num, uint16_t* Buffer, float Amplitude) //正弦波生成波表代码
{
    Num++;  // Increment to include the last point
    float U = Amplitude / 2.0;

    for (uint32_t i = 0; i < Num - 1; ++i)
    {
        Buffer[i] = (uint16_t)((U*sin((1.0 * i / (Num - 1)) * 2 * 3.1415926) + U) * 4095 / 3.3)+300;
    }
}

#define DDS_ROM_WAVE_SIZE (262144)
uint16_t SinWave_ROM[DDS_ROM_WAVE_SIZE];

static void DDS_WaveStart(void)
{
    generateSineWaveTable(DDS_ROM_WAVE_SIZE, SinWave_ROM, 3);
    DDS_Config(dds, SinWave_ROM, DDS_ROM_WAVE_SIZE, 40000, 0).Start();
}

这个DDS_Config其目的是想根据不同输出频率而变换DDS主频以提高分辨率:

DDS& DDS_Config(DDS& dds, const uint16_t* Buffer, uint32_t Num, uint32_t Freq, float Phase)
{
    uint64_t clkFreq;
    uint32_t FWORD;
    if (Freq <= xxx)
        clkFreq = yyy;
    else if (Freq <= xxx)
        clkFreq = yyy;
    else if (Freq <= xxx)
        clkFreq = yyy;
    else
        clkFreq = yyy;
    FWORD = (double )Freq * Num / clkFreq;
    dds.Set_WaveROM(Buffer, Num);
    dds.Set_ClkFreq(clkFreq);
    dds.Set_FWORD(FWORD);
    dds.Set_PWORD((double )Phase/360 * Num);
    return dds;
}

当然,STM32实现的DDS其最大的缺陷和定时器直接触发DMA的DAC打点类似,当DDS主频高上去之后,定时器设置DDS的主频就很难准确,同样频率也会精度下降,而且也受限片上DAC转换时间。

完整工程可见
Github: 笔者的STM32 C++库 持续开发中

参考资料

  1. STM32F407学习之DMA双缓冲模式HAL库实现
  2. 【边学边记_11】——DDS基本原理与FPGA实现
  3. 《新概念模拟电路5——源电路-信号源和电源》,作者:杨建国
  • 16
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
STM32是一种广泛使用的微控制器系列,可以通过使用外设来驱动可编程的硬件设备,例如DDS(Direct Digital Synthesis,直接数字合成器)。 要驱动DDS,首先需要了解DDS的工作原理和相关参数。DDS是一种通过数字方式生成多种波形信号的技术。它通常由一个相位累器和一个改变幅度的数字输入控制器组成。 要在STM32上驱动DDS,可以按照以下步骤进行操作: 1.配置GPIO引脚:根据DDS的硬件连接,将STM32的GPIO引脚配置为输出或输入模式,以将数据发送到DDS或接收DDS生成的信号。 2.设置时钟:DDS通常需要一个稳定的时钟源,例如外部晶体振荡器。可以按照STM32的参考手册配置外部时钟源,以便与DDS的工作频率匹配,并确保工作的准确性。 3.编程相位累器:DDS的核心组件是相位累器,它可以设置或控制生成的信号的相位。在STM32上,可以使用定时器/计数器外设实现相位累器功能。通过配置定时器的计数值和时钟频率,可以实现在一定时间内对相位值进行累,并调整输出信号的频率。 4.控制幅度:DDS的幅度通常由外部输入控制器设置。在STM32上,可以使用ADC或DAC模块读取或生成幅度值,并将其传递给DDS设备。 5.发送数据:根据DDS的接口协议,使用SPI、I2C、USART等通信接口将相位和幅度数据发送到DDS设备。可以通过编写STM32的相应驱动程序来实现数据传输。 6.接收数据:如果DDS设备生成的信号需要在STM32上进行进一步处理,可以使用相同的通信接口接收DDS设备返回的数据。从DDS设备接收到的数据可以用于频率和相位的调整或其他应用。 总之,驱动DDS需要根据具体的DDS设备和STM32型号来确定实现方式。通过配置GPIO、设置时钟、编程相位累器和控制幅度,以及使用适当的通信接口进行数据传输和接收,可以很好地实现DDS的驱动。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值