【ARM Cortex-M开发实战指南(基础篇)】第13章 DAC

开发环境:
MDK:Keil 5.30
STM32CubeMX:V6.4.0
MCU:STM32F103ZET6

13.1 DAC工作原理

13.1.1 DAC介绍

数字/模拟转换模块(DAC)是12位数字输入,电压输出的数字/模拟转换器。DAC可以配置为8位或12位模式,也可以与DMA控制器配合使用。DAC工作在12位模式时,数据可以设置成左对齐或右对齐。DAC模块有2个输出通道,每个通道都有单独的转换器。在双DAC模式下,2个通道可以独立地进行转换,也可以同时进行转换并同步地更新2个通道的输出。DAC可以通过引脚输入参考电压VREF+ 以获得更精确的转换结果。

xlrS7q.md.png

13.1.2 DAC主要特征

● 2个DAC转换器:每个转换器对应1个输出通道
● 8位或者12位单调输出
● 12位模式下数据左对齐或者右对齐
● 同步更新功能
● 噪声波形生成
● 三角波形生成
● 双DAC通道同时或者分别转换
● 每个通道都有DMA功能
● 外部触发转换
● 输入参考电压VREF+

xlrAc4.md.jpg

【注意】一旦使能DACx通道,相应的GPIO引脚(PA4或者PA5)就会自动与DAC的模拟输出相连(DAC_OUTx)。为了避免寄生的干扰和额外的功耗,引脚PA4或者PA5在之前应当设置成模拟输入(AIN)。

13.1.3 DAC功能描述

使能DAC通道

将DAC_CR寄存器的ENx位置’1’ 即可打开对DAC通道x 的供电。经过一段启动时间tWAKEUP,DAC通道x 即被使能。

注意:ENx位只会使能DAC通道x的模拟部分,即便该位被置’0’,DAC通道x的数字部分仍然工作。
使能DAC输出缓存
DAC集成了2个输出缓存,可以用来减少输出阻抗,无需外部运放即可直接驱动外部负载。每个DAC通道输出缓存可以通过设置DAC_CR寄存器的BOFFx位来使能或者关闭。

DAC输出电压
数字输入经过DAC被线性地转换为模拟电压输出,其范围为0到VREF+。任一DAC通道引脚上的输出电压满足下面的关系:
DAC输出 = VREF x (DOR / 4096)

DAC数据格式
根据选择的配置模式,数据按照下文所述写入指定的寄存器:
●8位数据右对齐:用户须将数据写入寄存器DAC_DHR8Rx[7:0]位(实际是存入寄存器DHRx[11:4]位)
●12位数据左对齐:用户须将数据写入寄存器DAC_DHR12Lx[15:4]位(实际是存入寄存器DHRx[11:0]位)
●12位数据右对齐:用户须将数据写入寄存器DAC_DHR12Rx[11:0]位(实际是存入寄存器DHRx[11:0]位)
根据对DAC_DHRyyyx寄存器的操作,经过相应的移位后,写入的数据被转存到DHRx寄存器中(DHRx是内部的数据保存寄存器x) 。随后,DHRx寄存器的内容或被自动地传送到DORx寄存器,或通过软件触发或外部事件触发被传送到DORx寄存器。

xlrZu9.md.jpg

DAC转换

不能直接对寄存器DAC_DORx写入数据,任何输出到DAC通道x 的数据都必须写入DAC_DHRx寄存器(数据实际写入DAC_DHR8Rx、DAC_DHR12Lx、DAC_DHR12Rx、DAC_DHR8RD、DAC_DHR12LD、或者DAC_DHR12RD寄存器)。

如果没有选中硬件触发(寄存器DAC_CR1的TENx位置’0’),存入寄存器DAC_DHRx的数据会在一个APB1 时钟周期后自动传至寄存器DAC_DORx 。如果选中硬件触发(寄存器DAC_CR1 的TENx位置’1’),数据传输在触发发生以后3个APB1 时钟周期后完成。

一旦数据从DAC_DHRx寄存器装入DAC_DORx寄存器,在经过时间tSETTLING 之后,输出即有效,这段时间的长短依电源电压和模拟输出负载的不同会有所变化。

xlrmH1.md.png

选择DAC触发

如果TENx位被置1,DAC转换可以由某外部事件触发(定时器计数器、外部中断线)。配置控制位TSELx[2:0] 可以选择8个触发事件之一触发DAC转换。

xlr8jH.md.jpg

每次DAC接口侦测到来自选中的定时器TRGO输出,或者外部中断线9的上升沿,最近存放在寄存器DAC_DHRx中的数据会被传送到寄存器DAC_DORx中。在3个APB1 时钟周期后,寄存器 DAC_DORx更新为新值。

如果选择软件触发,一旦SWTRIG位置’1’,转换即开始。在数据从DAC_DHRx寄存器传送到DAC_DORx寄存器后,SWTRIG位由硬件自动清’0’。

13.2 DAC寄存器描述

我们介绍一下要实现 DAC 的通道1输出,需要用到的一些寄存器。首先是 DAC控制寄存器 DAC_CR,该寄存器的各位描述如下图所示。

xlrYDA.md.png

DAC_CR 的低 16 位用于控制通道 1,而高 16 位用于控制通道 2,我们这里仅列出比较重要的最低 8 位的详细描述,如下图所示。

xlrtHI.png

首先,我们来看 DAC 通道 1 使能位(EN1),该位用来控制 DAC 通道 1 使能的,本章我们就是用的 DAC 通道 1,所以该位设置为 1。

再看关闭 DAC 通道 1 输出缓存控制位(BOFF1),这里 STM32 的 DAC 输出缓存做的有些不好,如果使能的话,虽然输出能力强一点,但是输出没法到 0,这是个很严重的问题。所以本章我们不使用输出缓存。即设置该位为 1。DAC 通道 1 触发使能位(TEN1),该位用来控制是否使用触发,里我们不使用触发,所以设置该位为 0。DAC 通道 1 触发选择位(TSEL1[2:0]),这里我们没用到外部触发,所以设置这几个位为 0就行了。DAC 通道 1 噪声/三角波生成使能位(WAVE1[1:0]),这里我们同样没用到波形发生器,故也设置为 0 即可。DAC 通道 1 屏蔽/幅值选择器(MAMP[3:0]),这些位仅在使用了波形发生器的时候有用,本章没有用到波形发生器,故设置为 0 就可以了。

最后是 DAC 通道 1 DMA 使能位(DMAEN1)。在 DAC_CR 设置好之后, DAC 就可以正常工作了, 我们仅需要再设置 DAC 的数据保持寄存器的值,就可以在 DAC 输出通道得到你想要的电压了(对应 IO 口设置为模拟输入)。假设我们用的是 DAC 通道 1 的 12 位右对齐数据保持寄存器: DAC_DHR12R1,该寄存器各位描述如下图所示。

xlrUEt.md.png

该寄存器用来设置 DAC 输出,通过写入 12 位数据到该寄存器,就可以在 DAC 输出通道 1得到我们所要的结果。

13.3 DAC应用代码实现-标准库

13.3.1 DAC普通方式输出

本章我们将使用库函数的方法来设置 DAC 模块的通道2来输出模拟电压,其详细设置步骤如下:

1)开启 PA 口时钟,设置 PA5为模拟输入。

STM32F103ZET6 的 DAC 通道2在 PA5上,所以,我们先要使能PA5的时钟, 然后设置 PA5为模拟输入。 DAC 本身是输出,但是为什么端口要设置为模拟输入模式呢?因为一但使能 DACx 通道后,相应的 GPIO 引脚(PA4 或者 PA5)会自动与 DAC 的模拟输出相连,设置为输入,是为了避免额外的干扰。

使能 GPIOA 时钟:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE ); //使能 PORTA时钟

设置 PA5为模拟输入只需要设置初始化参数即可:

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入

2)使能 DAC时钟。

同其他外设一样,要想使用,必须先开启相应的时钟。STM32 的 DAC 模块时钟是由 APB1提供的,所以我们调用函数 RCC_APB1PeriphClockCmd()设置 DAC 模块的时钟使能。

RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE ); //使能 DAC 通道时钟

3)初始化 DAC,设置 DAC 的工作模式。
该部分设置全部通过 DAC_CR 设置实现,包括:DAC 通道2 使能、 DAC 通道2输出缓存关闭、不使用触发、不使用波形发生器等设置。 这里DMA 初始化是通过函数 DAC_Init 完成的:

void DAC_Init(uint32_t DAC_Channel, DAC_InitTypeDef* DAC_InitStruct)

跟前面一样,首先我们来看看参数设置结构体类型 DAC_InitTypeDef 的定义:

typedef struct
{
uint32_t DAC_Trigger;
uint32_t DAC_WaveGeneration;
uint32_t DAC_LFSRUnmask_TriangleAmplitude;
uint32_t DAC_OutputBuffer;
}DAC_InitTypeDef;
```c

这个结构体的定义还是比较简单的,只有四个成员变量,下面我们一一讲解。

第一个参数 DAC_Trigger 用来设置是否使用触发功能,前面已经讲解过这个的含义,这里我们不是用触发功能,所以值为 DAC_Trigger_None。

第二个参数 DAC_WaveGeneratio 用来设置是否使用波形发生,这里我们前面同样讲解过不使用。所以值为 DAC_WaveGeneration_None。

第三个参数 DAC_LFSRUnmask_TriangleAmplitude 用来设置屏蔽/幅值选择器,这个变量只在使用波形发生器的时候才有用,这里我们设置为 0 即可,值为 DAC_LFSRUnmask_Bit0。

第四个参数 DAC_OutputBuffer 是用来设置输出缓存控制位,前面讲解过,我们不使用输出缓存,所以值为 DAC_OutputBuffer_Disable。到此四个参数设置完毕。看看我们的实例代码:
```c
DAC_InitTypeDef DAC_InitType;
DAC_InitType.DAC_Trigger=DAC_Trigger_None; //不使用触发功能 TEN1=0
DAC_InitType.DAC_WaveGeneration=DAC_WaveGeneration_None;//不使用波形发生
DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude=DAC_LFSRUnmask_Bit0;
DAC_InitType.DAC_OutputBuffer=DAC_OutputBuffer_Disable ; //DAC输出缓存关闭
DAC_Init(DAC_Channel_2,&DAC_InitType); //初始化 DAC 通道 2

4)使能 DAC 转换通道
初始化 DAC 之后,理所当然要使能 DAC 转换通道,库函数方法是:

DAC_Cmd(DAC_Channel_2, ENABLE); //使能 DAC1

5)设置 DAC 的输出值。
通过前面 4 个步骤的设置, DAC 就可以开始工作了,我们使用 12 位右对齐数据格式,所以我们通过设置 DHR12R1,就可以在 DAC 输出引脚(PA5)得到不同的电压值了。 库函数的函数是:

DAC_SetChannel2Data(DAC_Align_12b_R, 0);

第一个参数设置对齐方式,可以为 12 位右对齐 DAC_Align_12b_R, 12 位左对齐DAC_Align_12b_L 以及 8 位右对齐 DAC_Align_8b_R 方式。

第二个参数就是 DAC 的输入值了,这个很好理解,初始化设置为 0。这里,还可以读出 DAC 的数值,函数是:

DAC_GetDataOutputValue(DAC_Channel_2);

因此DAC通道2的整体配置如下:

/**
  * @brief  配置DAC
  * @param  None
  * @retval None
  */
void DAC_Configuration(void)
{
   DAC_InitTypeDef DAC_InitStructure;

   GPIO_Configuration();//端口初始化

   /* DAC Periph clock enable */
   RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);

   DAC_InitStructure.DAC_Trigger=DAC_Trigger_None;//不使用触发功能
   DAC_InitStructure.DAC_WaveGeneration=DAC_WaveGeneration_None;//不使用三角波
   //屏蔽 幅值设置
   DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude=DAC_LFSRUnmask_Bit0;
   //关闭缓存
   DAC_InitStructure.DAC_OutputBuffer=DAC_OutputBuffer_Disable;
   DAC_Init(DAC_Channel_2,&DAC_InitStructure);//初始化DAC通道

   DAC_Cmd(DAC_Channel_2,ENABLE);//使能DAC
}

主函数如下:

/**
  * @brief     主函数
  * @param     None
  * @retval    int
  */
int main(void)
{
    uint8_t i=0;
    uint16_t da=0;

    /*SysTick Init*/
    SysTick_Init();

    /* USART1 config 115200 8-N-1 */
    USART_Config();

    /*DAC初始化*/
    DAC_Configuration();//调用DAC配置

    for(;;)
    {
        da=0;

        for(i=0;i<=10;i++)
        {
            da=i*400;

            DAC_SetChannel2Data(DAC_Align_12b_R,da);//12位 右对齐 PA5 端口输出

            printf("da=%f v\r\n",3.3*((float)da/4096));

            //printf("%3.2f\r\n",3.3*((float)da/4096));
            Delay_ms(1000); 
        }
    }
}

这代码很简单,首先是对串口等进行初始化,接下来就是循环设置电压并输出。

如果想要使用软件触发,则需要将DAC_InitTypeDef的DAC_Trigger配置为
DAC_Trigger_Software,然后在主函数中需要进行软件触发。

/* Start DAC Channe2 conversion by software */
DAC_SoftwareTriggerCmd(DAC_Channel_2, ENABLE);

13.3.2 DAC正弦波输出实现

本章我们还要通过DAC实现正弦波输出,那么就需要找到正弦波的曲线散点,其计算方式如下所示:

原系统时钟周期:T_Systick=1/72M(单位:秒)

因为定时时钟预分频:TIM_Prescaler=0

所以定时时钟周期:T_TIM=T_Systick*(TIM_Prescaler+1)=1/72M(单位:秒)

因为设置的定时更新周期:TIM_Period=19

所以定时器更新周期:T_update = T_TIM * (TIM_Period+1) = 20/72M

而DAC数据更新率等于定时器更新速率:即DAC的数据更新周期为:
DAC_update=T_update = 20/72M

本实验有32个数据点,则正弦波的周期为:
T_sin = DAC_update * 点数 = 640/72M

最后求的正弦波的频率为:
f_sin = 1/T_sin = 112500Hz

因此正弦波的频率为:
f_sin=1/T_Systick/(TIM_Prescaler+1)/(TIM_Period+1)/点数

其波形数据如下:

const uint16_t Sine12bit[32] = {
    2448,2832,3186,3496,3751,3940,4057,4095,4057,3940,
    3751,3496,3186,2832,2448,2048,1648,1264,910,600,345,
    156,39,0,39,156,345,600,910,1264,1648,2048
};

接下来看看主函数。

/**
  * @brief  main
  * @param  None
  * @retval None
  */
int main(void)
{
    /*初始化DAC,开始DAC转换*/
    DAC_Mode_Init();

    while(1);
}

主函数就一句代码,那我们进入DAC_Mode_Init()看看吧。

/**
  * @brief  DAC初始化函数
  * @param  None
  * @retval None
  */
void DAC_Mode_Init(void)
{
    uint32_t Idx = 0;

    DAC_Config();
    DAC_TIM_Config();
    DAC_DMA_Config();

    /* 填充正弦波形数据,双通道右对齐*/
    for (Idx = 0; Idx < 32; Idx++)
    {
        DualSine12bit[Idx] = (Sine12bit[Idx] << 16) + (Sine12bit[Idx]);
    }
}

DAC_Mode_Init()函数初始化了DAC、DMA和TIM,启动定时器,利用定时器的触发DAC数据更新。

13.4 DAC应用代码实现-HAL库

STM32F1有两个DAC通道,分别是PA4和PA5,因此,只需配置相应的引脚即可。我们在串口的例子的基础上进行配置。

串口通信(HAL库)

13.4.1 DAC普通方式输出

13.4.1.1 DAC基本配置

我们接下来就是DAC的配置。以PA5为例,也就是DAC的通道2。

xlrrvQ.md.png

然后生成工程即可,是不是很简单。

13.4.1.2 DAC基本输出实现

我们先看DAC的基本输出的实例,主函数如下:


/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  uint8_t i=0;
  uint16_t da=0;

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_DAC_Init();
  /* USER CODE BEGIN 2 */
  HAL_DAC_Start(&hdac,DAC_CHANNEL_2);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    da=0;

    for(i=0;i<=10;i++)
    {
        da = i*400;
        //12位 右对齐 PA5 端口输出
        HAL_DAC_SetValue(&hdac, DAC_CHANNEL_2, DAC_ALIGN_12B_R, da);

        printf("da=%f v\r\n",3.3*((float)da/4096));

        //printf("%3.2f\r\n",3.3*((float)da/4096));
        HAL_Delay(2000);
    }
  }
  /* USER CODE END 3 */
}

这代码很简单,首先是对串口等进行初始化,接下来就是循环设置电压并输出。
当然也可使用软件触发方式,在中STM32CubeMX配置成软件触发就可以了。

xlryuj.md.png

然后在循环体中加入如下语句。

HAL_DAC_Start(&hdac,DAC_CHANNEL_2);

13.4.2 DAC输出正弦波

13.4.2.1 DAC正弦波配置

在上个例子的基础上进行配置。还是使用通道2,只是这里选择定时器2作为触发事件,不使用波形发生器。

xlr6Ds.md.png

接下设置DAC的DMA,方向选为内存到外设。

xlrcbn.md.png

接下来就是引脚的模式,设置为模拟模式,默认即可。

xlr52F.md.png

既然选择了定时器2作为触发事件,那么需要配置定时器2。打开Timers,使能定时器2。配置相应的参数。关于定时器的详细配置,可参考笔者以前的文章。

定时器(HAL库)

xlr7r9.md.png

这里需要注意配置TIM2的Trigger Output参数,Trigger Event设置为Update Event。好了,配置就完成了,生成工程即可。

13.4.2.2 DAC正弦波输出实现

本章我们还要通过DAC实现正弦波输出,那么就需要找到正弦波的曲线散点,其计算方式在讲解标准库的时候已经讲解过了。

接下来看看主函数。

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  const uint16_t Sine12bit[32] = {
    2448,2832,3186,3496,3751,3940,4057,4095,4057,3940,
    3751,3496,3186,2832,2448,2048,1648,1264,910,600,345,
    156,39,0,39,156,345,600,910,1264,1648,2048
  };

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_DAC_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start(&htim2);
  HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_2, (uint32_t*)Sine12bit, 32, DAC_ALIGN_12B_R);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

启动定时器,利用定时器的触发DAC数据更新。

13.5实验现象

13.5.1 DAC普通方式输出

将程序编译好后下载到板子中,通过串口助手可以看到在接收区有电压值输出。这个和ADC输入不同,我们使用DAC的目的是通过板子得到相应的模拟电压值,看到串口的输出值只是我们的调试手段,要想确认实验是否成功,是需要通过电压表测量PA4的电压值是否串口的输出一致。我们设置的步进是400,因此电压值也是在以400 * 3.3 / 4096的电压步进。

xlrqV1.md.png

当然啦,还需要万用表测量引脚电压即可。你可以使用一个固定值,或者延时更长这样便于测量。为了更好的测量,笔者将转换电压设置为固定值,因此在循环体的前面加了一句话。

da = 2048;

接下来看看实验结果:

xlrLUx.md.png

我们再来看看实际测量的结果。

xlrO56.md.jpg

这里的计算结果和实际测量稍微有些差异,但都在误差范围内。

13.5.2 DAC正弦波输出

将程序编译好后下载到板子中,通过示波器可看到波形输出。

xlrjPK.md.png

HAL只打开了通道2,因此输出的波形如下:

xlrv8O.md.png


欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎


资源获取方式

1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[Cortex-M]获取资料提取码

Related posts

Leave a Comment