前段时间学习了STM32使用DAC模块输出正弦波的功能,在学习过程中遇到了一些问题,在此和各位分享。
DAC是数字/模拟转换模块的简称,STM32中的DAC是12位数字输入,这个就决定了其精度。STM32的DAC模块具有两个通道,可单独进行转换,也就是说可以同时输出两个正弦波或其他波形。输出正弦波的原理简单讲就是每隔一定时间向DAC的数据寄存器写入数据,然后进行数据转换,输出不同电压,然后在时间轴上显示出波形。
这里比较重要的一个公式就是数字量和模拟量的转换公式:
STM32芯片内部有个参考电压:VREF,这个电压约为3.3V,由于DAC是12位的,所以最大可以表示为4095,将这3.3V电压均分为4095份,如果你向寄存器内写入2048,那么转换后的电压就是3.3V的一半,如果写入4095,那么转换后的电压就约为3.3V,实际输出可能会略有偏差。
简单介绍了下DAC的原理,下面直接来说我所遇到的问题:
下图就是我当时写好代码输出的正弦波,只有一半的正弦波是对的,另外一半有问题。
我的设计方式采用的是网上分享比较多的DAC+TIMER+DMA的方式,DMA简单理解就是一个通道,从通道一端写入数据,另外一端直达DAC的数据寄存器。这么做有什么好处呢?不占用CPU资源,速度快速,由于我们输出波形的时候,需要很快频率的DAC转换,如果我们写入过程太慢,产生的波形就会出现锯齿。
下面是我写的代码:
1.首先是GPIO的配置,STM32103的芯片DAC输出管脚对应的是PA4,PA5,配置成模拟输入方式就可以了,这比较简单。
void aw_gpio_init(void)
{GPIO_InitTypeDef aw_led_gpio_struct;GPIO_InitTypeDef aw_dac_gpio_struct;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC | RCC_APB1Periph_TIM2, ENABLE);RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE);aw_led_gpio_struct.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12;aw_led_gpio_struct.GPIO_Mode = GPIO_Mode_Out_PP;aw_led_gpio_struct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOC, &aw_led_gpio_struct);GPIO_SetBits(GPIOC, GPIO_Pin_10 | GPIO_Pin_12);GPIO_ResetBits(GPIOC, GPIO_Pin_11); // green led onaw_dac_gpio_struct.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;aw_dac_gpio_struct.GPIO_Mode = GPIO_Mode_AIN;GPIO_Init(GPIOA, &aw_dac_gpio_struct); // init PA4 and PA5 for DAC1
}
2.TIMER的配置,在这里我使用的是TIM2,时钟不分频,自动重装载计数值用的是19,这和ST官方的例程中分享的也是一致的。定时器在这里的作用就是定时,每隔一段时间向DMA接口写入数据。
#ifndef __AW_TIME_H__
#define __AW_TIME_H__#include "stm32f10x_tim.h"#define AW_DAC_PSC (0x0)
#define AW_DAC_PERIOD (0x19)void aw_time_init(void);#endif /* __AW_TIME_H__ */
void aw_time_init(void)
{TIM_TimeBaseInitTypeDef aw_time_struct;aw_time_struct.TIM_Prescaler = AW_DAC_PSC;aw_time_struct.TIM_CounterMode = TIM_CounterMode_Up;aw_time_struct.TIM_Period = AW_DAC_PERIOD; // timer delay 0.278us onceaw_time_struct.TIM_ClockDivision = TIM_CKD_DIV1;TIM_TimeBaseInit(TIM2, &aw_time_struct);TIM_Cmd(TIM2, ENABLE);TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
}
3.DAC的配置,在这里我使用了双通道,也就是PA4,PA5管脚都可以输出正弦波。
void aw_dac_init(void)
{DAC_InitTypeDef aw_dac_struct;aw_dac_struct.DAC_Trigger = DAC_Trigger_T2_TRGO; // use TIM2 to trigger DACaw_dac_struct.DAC_WaveGeneration = DAC_WaveGeneration_None;aw_dac_struct.DAC_OutputBuffer = DAC_OutputBuffer_Disable;DAC_Init(DAC_Channel_1, &aw_dac_struct);DAC_Init(DAC_Channel_2, &aw_dac_struct);DAC_Cmd(DAC_Channel_1, ENABLE); // enable DAC_Channel_1DAC_Cmd(DAC_Channel_2, ENABLE); // enable DAC_Channel_2DAC_DMACmd(DAC_Channel_2, ENABLE);
}
以上部分和各位网友的配置都一样,都没有问题,关键的地方在DMA的配置上。
4.DMA配置
我先给出DMA的配置代码,后续我再给大家阐述:
void aw_dma_init(void)
{DMA_InitTypeDef aw_dma_struct;aw_dma_struct.DMA_PeripheralBaseAddr = DAC_DHR12RD_ADDRESS;aw_dma_struct.DMA_MemoryBaseAddr = (AW_U32)&dual_sine_wave_data;aw_dma_struct.DMA_DIR = DMA_DIR_PeripheralDST;aw_dma_struct.DMA_BufferSize = SINE_WAVE_DATA_MAX;aw_dma_struct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;aw_dma_struct.DMA_MemoryInc = DMA_MemoryInc_Enable;aw_dma_struct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;aw_dma_struct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;aw_dma_struct.DMA_Mode = DMA_Mode_Circular;aw_dma_struct.DMA_Priority = DMA_Priority_High;aw_dma_struct.DMA_M2M = DMA_M2M_Disable;DMA_Init(DMA2_Channel4, &aw_dma_struct);DMA_Cmd(DMA2_Channel4, ENABLE);
}
(1)DAC_DHR12RD_ADDRESS对应的是DAC的数据寄存器的地址,我在.h文件中是这样定义的:#define DAC_DHR12RD_ADDRESS (DAC_BASE + 0x20),其实也可以定义成:#define DAC_DHR12RD_ADDRESS 0x40007420,其实都是一样的,前者是野火中使用的方法,后者是ST官方例程的用法。这里各位和我一样的新手要注意理解。
(2)然后就是DMA的DMA_MemoryBaseAddr这一项的配置,这是数据的输入接口,我使用的是一个数组buff,给这项传递指针。数字量的计算过程如下所示:
#ifndef __AW_DMA_H__
#define __AW_DMA_H__#include "aw_type.h"#define DAC_DHR12RD_ADDRESS (DAC_BASE + 0x20)
#define SINE_WAVE_DATA_MAX (256)
#define PI (3.14159)void aw_dma_init(void);
void aw_sine_wave_data(AW_U16 cycle);#endif /* __AW_DMA_H__ */
static AW_U16 dual_sine_wave_data[SINE_WAVE_DATA_MAX] = {0};
static AW_U16 sine_wave_data[SINE_WAVE_DATA_MAX] = {0};/*** @brief This function is used to get sine value.*/
void aw_sine_wave_data(AW_U16 cycle)
{AW_U16 i = 0;for (i = 0; i < cycle; i++) {sine_wave_data[i] = 2048 * sin(1.0 * i / (cycle - 1) * 2 * PI) + 2048;}for (i = 0; i < SINE_WAVE_DATA_MAX; i++) {dual_sine_wave_data[i] = (sine_wave_data[i] << 16) + (sine_wave_data[i]);}
}
我所配置的正弦波幅度是2048,最小值为0,最大值为4096。数据的计算过程简单来讲就是把正弦波的一个周期均分成256份,那么每份的x轴长度就是1*(2*PI)/255,1.0的作用就是使得后面不要整除。得到正弦波的数子量数据sine_wave_data,然后拆分成双通道的数据dual_sine_wave_data。
关键的配置在这两项,如下所示:
aw_dma_struct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;aw_dma_struct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
各位和我一样的新手一定要注意,由于我们之前数字量数据dual_sine_wave_data定义的是16位的,所以在这里传输也是16位的传输,一定不要配置成整字32位,不然就出现我之前的问题,正弦波只有一半是好的,另外一半输出不确定的波形,而且这个问题在之前的各位博友的文章中都没有讲述。
最后来看我的主函数:
int main(void)
{aw_sine_wave_data(SINE_WAVE_DATA_MAX);aw_gpio_init();aw_dac_init();aw_time_init();aw_dma_init();while (1) {;}
}
主函数很简单,就是配置好各个模块,然后while循环一下。
最后我试验测试的正确波形:
如有疑问,欢迎各位博友一起讨论!