shanyuxiang 发表于 2025-9-7 16:52

玩转APM32的DMA-用DAC和DMA生成正弦波

本帖最后由 shanyuxiang 于 2025-9-20 13:19 编辑

#申请原创# @21小跑堂

玩转APM32的DMA-用DAC和DMA生成正弦波



一、前言
DAC可以产生0到3.3V的模拟电压,如果动态改变DAC输出的电压,那么就可以生成各种各样的模拟波形。
这里我们就用DAC+DMA+TMR配合来生成正弦波。

首先看看APM32E103上的相关外设资源:
2个12位的DAC;

2个16位基本定时器TMR6/7;

两个DMA,DMA1有7个通道,DMA2有5个通道。



DAC有两个,一个是DAC_OUT1(PA4) ,还有一个是DAC_OUT2(PA5),两个可以同时输出,这里以DAC_OUT1为例。



DAC支持定时器触发,也就是说定时器溢出时更新一次DAC的数据寄存器,这里用基础定时器就够了。








每触发一次DAC转换需要更新DAC数据寄存器的值,用DMA把数组中的数传输到DAC数据寄存器中


二、DAC、TMR、DMA的初始化配置
2.1、DAC的配置
这里以DAC1为例,DAC的配置和单独使用DAC类似,先初始化GPIO为模拟复用。
触发配成TMR6触发,不用内部的波形发生功能,使能输出缓存。
最后一定记得开启DAC的DMA功能。

#define DAC_PIN          GPIO_PIN_4
#define DAC_GPIO         GPIOA
#define DAC_CHANNEL      DAC_CHANNEL_1

#define DAC_TIM          TMR6
#define DAC_TRIG         DAC_TRIGGER_TMR6_TRGO

//gpio config
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOA);

GPIO_Config_T GPIO_ConfigStruct;
GPIO_ConfigStruct.pin= DAC_PIN;
GPIO_ConfigStruct.mode = GPIO_MODE_ANALOG;
GPIO_Config(DAC_GPIO, &GPIO_ConfigStruct);

//dac config
RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_DAC);

DAC_Config_T DAC_ConfigStruct;
DAC_ConfigStruct.trigger             = DAC_TRIG;
DAC_ConfigStruct.waveGeneration      = DAC_WAVE_GENERATION_NONE;
DAC_ConfigStruct.maskAmplitudeSelect = DAC_TRIANGLE_AMPLITUDE_1;
DAC_ConfigStruct.outputBuffer      = DAC_OUTPUT_BUFFER_ENBALE;
DAC_Config((uint32_t)DAC_CHANNEL, &DAC_ConfigStruct);
DAC_DMA_Enable(DAC_CHANNEL);
DAC_Enable(DAC_CHANNEL);




2.2、TMR的配置
通过用户手册可知,DAC支持多种触发源,这里我们用基础定时器TMR6去触发DAC转换。
定时器配置成最简单的向上计数即可,不需要开中断,更新事件用于触发输出。
定时器的溢出频率也就决定了DAC输出电压的变化快慢,也就是division 和 period,
这两个值越小,生成的正弦波频率越高;或者减小正弦波的采用点数,正弦波频率也能提高。


    //timer config
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_TMR6);
      
    TMR_BaseConfig_T TMR_BaseConfigStruct;
    TMR_BaseConfigStruct.clockDivision = TMR_CLOCK_DIV_1;
    TMR_BaseConfigStruct.countMode = TMR_COUNTER_MODE_UP;
    TMR_BaseConfigStruct.division= 60-1;
    TMR_BaseConfigStruct.period    = 4;
    TMR_ConfigTimeBase(DAC_TIM, &TMR_BaseConfigStruct);
    TMR_EnableAutoReload(DAC_TIM);
    TMR_SelectOutputTrigger(DAC_TIM, TMR_TRGO_SOURCE_UPDATE);
    TMR_Enable(DAC_TIM);


2.3、DMA的配置
DMA配成从内存到DAC,内存地址要自增,而DAC数据寄存器地址不变。
因为这里用的DAC是12位,所以内存和外设的数据宽度都设为半字,也就是16位。
如果想开启一次后一直产生波形,则使用循环模式;如果想每个波形周期都要手动启动,则使用正常模式。
通过手册上的请求映射表可看出对应的DMA通道是DMA2-channel3。


特别要注意DAC的数据寄存器地址,这里需要仔细看手册。





    //DMA config
    RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA2);
   
    DMA_Config_T dmaConfig;
    DMA_Reset(DAC_DMA_CHANNEL);
    dmaConfig.peripheralBaseAddr = (uint32_t)DAC_DATA_ADDRESS;
    dmaConfig.dir                = DMA_DIR_PERIPHERAL_DST;
    dmaConfig.memoryBaseAddr   = (uint32_t)NULL;
    dmaConfig.bufferSize         = 0;
    dmaConfig.peripheralInc= DMA_PERIPHERAL_INC_DISABLE;
    dmaConfig.memoryInc      = DMA_MEMORY_INC_ENABLE;
    dmaConfig.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_HALFWORD;
    dmaConfig.memoryDataSize = DMA_MEMORY_DATA_SIZE_HALFWORD;
    dmaConfig.loopMode       = DMA_MODE_CIRCULAR;
    dmaConfig.priority       = DMA_PRIORITY_HIGH;
    dmaConfig.M2M            = DMA_M2MEN_DISABLE;
    DMA_Config(DAC_DMA_CHANNEL, &dmaConfig);


2.4 启动DMA的传输
上面的初始化完成后,需要启动使能相关外设才会产生波形。
先设置定时器的自动加载值,这个值决定了波形的频率,
然后波形采样点数组的地址和长度赋值给DMA的地址、长度寄存器,
最后使能DAM传输。

//启动传输
void dac_dma_transmit(unsigned short *addr, unsigned int len, unsigned short period)
{
    if (len > 65535)len = 65535;
    DMA_Disable(DAC_DMA_CHANNEL);

    TMR_ConfigAutoreload(DAC_TIM,period-1);

    DAC_DMA_CHANNEL->CHNDATA = len;
    DAC_DMA_CHANNEL->CHMADDR = (uint32_t)addr;

    DMA_Enable(DAC_DMA_CHANNEL);
}


三、使用与测试
为了展示最终效果,还需要制作一个正弦波的采样点数组。
方法很多,这里就通过正弦函数sin来计算一个360点的波形数据。
正弦函数计算结果有正有负,为了好看,统统平移到0V以上。
#define WaveLength 360
unsigned short WaveBuffer;

#include <math.h>
#define PI 3.14159
int generate_sine_wave(unsigned short buf[])
{
    double temp;
    unsigned short i;

    for (i = 0; i < WaveLength; i++)
    {
      temp = sin(i * PI / 180.0) * 2048.0;
      temp = temp + 2047.0;

      buf = (unsigned short)temp;
    }
}


最后一步,调用刚才写好的的函数即可。
int main(void)
{
    usart_printf_init();

    printf("This a dac and dma demo(sine wave)\r\n");

    generate_sine_wave(WaveBuffer);

    dac_dma_init();
    dac_dma_transmit(WaveBuffer, WaveLength, 4);


    while (1)
    {
    }
}

用示波器观察PA4脚上的波形。



顺便又做了个锯齿波。


以上方法除了用来产生各种自定义的波形,也可以用来播放wav音频。




shanyuxiang 发表于 2025-9-7 16:57

#申请原创#  @21小跑堂

阳光爆裂 发表于 2025-9-7 18:59

使用Timer6来控制生成波形的频率。
学习了,谢谢楼主

xch 发表于 2025-9-7 20:45

能不能生成任意频率的正弦波?

xch 发表于 2025-9-7 20:49

把局部变量地址传给DMA寄存器用不靠谱。仅这个表演程序可用

shanyuxiang 发表于 2025-9-7 23:43

xch 发表于 2025-9-7 20:45
能不能生成任意频率的正弦波?

最高频率会受DAC的转换速度限制,低于这个频率都可以

shanyuxiang 发表于 2025-9-7 23:44

xch 发表于 2025-9-7 20:49
把局部变量地址传给DMA寄存器用不靠谱。仅这个表演程序可用

局部变量在函数执行后会被释放,所以这里用的是全局变量

shanyuxiang 发表于 2025-9-7 23:47

阳光爆裂 发表于 2025-9-7 18:59
使用Timer6来控制生成波形的频率。
学习了,谢谢楼主

{:handshake:}{:handshake:}

xch 发表于 2025-9-8 01:10

shanyuxiang 发表于 2025-9-7 23:43
最高频率会受DAC的转换速度限制,低于这个频率都可以

不超过 DAC 转换速率。 任意频率低频。

wangwu1976@ 发表于 2025-9-8 08:04

学习了

xch 发表于 2025-9-8 11:10

TMR_BaseConfigStruct.period    = 4;这句啥意义?

shanyuxiang 发表于 2025-9-8 13:31

xch 发表于 2025-9-8 11:10
TMR_BaseConfigStruct.period    = 4;这句啥意义?

定时器的计数周期,决定了DAC多久转换一次

天鹅绒星星 发表于 2025-9-9 17:06

使用DMA来输出波形,这样还不影响MCU的通讯。否则,通讯就会导致输出波形变型了。
楼主,厉害!
有机会我也复刻一个

梦之一瞥 发表于 2025-9-13 10:09

原来这个是这样实现的啊!
以前看人家的作品 也复刻不出来

CloudKiss 发表于 2025-9-14 21:54

还真是的,如果把Timer6的频率设置为48000的话,输出的波形就是声音了

cooldog123pp 发表于 2025-9-27 10:30

xch 发表于 2025-9-7 20:45
能不能生成任意频率的正弦波?

应该是可以的吧,该表周期就行了吧,不过也得试一试。

xch 发表于 2025-9-28 09:31

cooldog123pp 发表于 2025-9-27 10:30
应该是可以的吧,该表周期就行了吧,不过也得试一试。

非整除分频系数的。不好随便做

魔法森林精灵 发表于 2025-9-30 20:38

这个DAC和DMA的结合使用确实挺巧妙的,对于学习APM32的外设操作很有帮助。

星空魔法师 发表于 2025-10-15 22:55

DAC和DMA的结合使用确实能提高效率,减少CPU的负担。楼主的代码示例很清晰,学习了。

雾里闲逛 发表于 2025-10-15 10:28

这个和PWM的优势在哪里啊
页: [1] 2
查看完整版本: 玩转APM32的DMA-用DAC和DMA生成正弦波