27. DAC——输出正弦波

27.1. DAC简介

DAC 为数字/模拟转换模块,顾名思义,它的作用就是把输入的数字编码,转换成对应的模拟电压输出,它的功能与ADC相反。 在常见的数字信号系统中,大部分传感器信号被转化成电压信号,而ADC把电压模拟信号转换成易于计算机存储、处理的数字编码, 由计算机处理完成后,再由DAC输出电压模拟信号,该电压模拟信号常常用来驱动某些执行器件,使人类易于感知。如音频信号的采集及还原就是这样一个过程。

RA6M5 和 RA4M2 具有片上DAC外设,总共有两路DAC输出通道,每路的分辨率可配置为12位, 这两个通道之间互不影响,每个通道都可以使用ELC功能可外部触发或者进行与ADC单元1的同步转换。 RA2L1 仅有一路DAC输出通道,分辨率也为12位。

DAC模块特性

RA6M5 和 RA4M2 的 DAC 外设模块是完全一样的,没有什么区别; 而 RA2L1 的 DAC 外设模块有些许不同。它们的DAC模块特性如下表所示:

RA6M5/RA4M2/RA2L1 的DAC特性

特性

RA6M5/RA4M2

RA2L1

分辨率

12-bit

12-bit

DAC转换器数量

1

1

输出通道

2(DA0和DA1)

1(仅DA0)

DA 输出放大器

支持

不支持

模块停止功能

模块停止状态可以被设置以减少功耗

模块停止状态可以被设置以减少功耗

事件链接功能(输入)

DA0和DA1通道的转换可以由输入事件信号启动

DA0通道的转换可以由输入事件信号启动

DAC 的相关引脚或信号及其功能用途如下表所示:

DAC 相关引脚或信号及其功能

引脚

功能

AVCC0

作为ADC12和DAC12的电源输入引脚

AVSS0

作为ADC12和DAC12的接地输入引脚

VREFH

作为DAC12的模拟参考高电压电源引脚

VREFL

作为DAC12的模拟参考低电压电源引脚

DA0

DAC输出通道0,可连接到GPIO引脚进行信号输出

DA1

DAC输出通道1,可连接到GPIO引脚进行信号输出

27.2. DAC的结构框图

RA6M5 与 RA4M2 的 DAC 模块是一样的,而 RA2L1 的 DAC 模块结构比 RA6M5 和 RA4M2 的更加简单。 因此,下面我们就来讲解它们的 DAC 模块框图。

RA6M5 和 RA4M2 的 DAC 模块框图如下图所示:

图

RA2L1 的 DAC 模块框图如下图所示:

图

以上框图中的“12-bit D/A”是核心部件,几乎所有的结构都是围绕着这个 12 bit 的 D/A 转换器进行工作的。

27.2.1. DAC模块相关引脚

见图中标注 ① 处。

27.2.1.1. 电源相关引脚

RA6M5 和 RA4M2 的 DAC 模块的电源引脚分为模块供电引脚和DA转换参考电源引脚; 而 RA2L1 的 DAC 模块的电源引脚其实没有区分,模块供电引脚和DA转换参考电源引脚是共用相同的引脚。 如下表所示。

电源相关引脚

电源引脚

RA6M5/RA4M2

RA2L1

AVCC0

模拟电源输入引脚

模拟电源输入与DA转换参考电压正极引脚

AVSS0

模拟电源接地引脚

模拟电源接地与DA转换参考电压负极引脚

VREFH

DA转换参考电压正极引脚

(RA2L1 无此引脚)

VREFL

DA转换参考电压负极引脚

(RA2L1 无此引脚)

注:细心的读者也许已经发现,RA6M5 和 RA4M2 的DAC模块 VREFH/VREFL 引脚其实也是 ADC Unit 1 的参考电源引脚。 因此,RA6M5 和 RA4M2 的 DAC 模块与 ADC Unit 1 是共用电源的。 而这会造成一些问题,比如当 D/A 转换器启动时,DAC 模块会产生浪涌电流, 产生的浪涌电流会对 ADC Unit 1 的操作造成干扰。对此也有解决办法,也就是让 D/A 和 A/D 转换器进行同步转换。

可通过以下方法减少 DAC 和 ADC 模块之间的干扰:

  • 同步D/A转换控制,使D/A转换数据的更新时间由 ADC12 (uint1) 输入信号同步

  • 通过合理的控制DAC12的启动信号来减少干扰对D/A转换精度的影响

27.2.1.2. 模拟转换输出引脚

DA0 和 DA1,这是 DAC 输出通道0和输出通道1的信号, 它们可以连接到实际的物理 IO 引脚进行DA转换后信号的输出。

下面的表格展示了 RA6M5/RA4M2/RA2L1 的DA输出通道与实际 IO 引脚可以进行的连接:

DA输出通道与实际 IO 引脚的连接

DA通道

RA6M5/RA4M2

RA2L1

DAC通道0

P014

P014

DAC通道1

P015

(RA2L1没有DAC通道1)

27.2.2. 12位DA转换器和输出放大器

见图中标注 ② 处。

DA 转换器为12位的分辨率。

RA6M5 和 RA4M2 的 DAC 输出可以选择使用输出放大器,也可以旁路输出放大器、不使用。 而 RA2L1 不支持输出放大器控制功能。

27.2.3. 同步电路

见图中标注 ③ 处。

同步电路:同步电路由 D/A A/D 转换器同步使能输入信号进行控制。

同步功能是指将 DA12 与 ADC12 (Unit 1) 进行同步,这样可以抑制两者之间由于共用模拟电源而带来的相互干扰。

27.2.4. 总线、寄存器及控制电路

见图中标注 ④ 处。

CPU 通过总线访问 DAC 模块的寄存器,通过相应的寄存器配置来操作控制电路, 最终控制12位的DA转换器。这些只需简单理解即可,我们比较需要关注的是DAC数据寄存器。 RA6M5 和 RA4M2 的 DAC 模块因为有两个DA输出通道,因此有两个DAC数据寄存器(DADR0 和 DADR1); 而 RA2L1 的 DAC 模块只有一个DA输出通道,也只有一个DAC数据寄存器(DADR0)。

DAC数据寄存器:DADRn (n=0, 1)

DADRn 寄存器是16位读/写寄存器,它存储用于D/A转换的数据。 当开启模拟输出时 DADRn 中的值被转换并输出到模拟输出引脚上。 之后每当我们改变DADRn中的数值时,就可以改变输出的模拟电压。

写入的 12-bit 数据可以进行右对齐或左对齐设置。 右对齐时16位数据寄存器的低12位有效;左对齐时16位数据寄存器的高12位有效。 因而虽然 DADRn 数据寄存器有16位数据,但是只用到了前12位(左对齐)或后12位(右对齐),其余的位没有用到。

27.2.5. 触发源

见图中标注 ⑤ 处。

我们可以设置软件来触发DAC、或者通过使用ELC进行触发、甚至我们还可以使用外部中断进行触发。 最终我们的目的是为了使得DACR.DAOEn(n=0,1)位被置1,

我们可以编写相应的寄存器控制代码以至于我们可以使用不同的方式进行触发。

当设置DACR中的DAOEn位(n = 0,1)为1时,启用DAC12并输出转换结果。当设置DACR中的DAOEn位(n = 0,1)为0时,关闭DAC输出转换。

下面是使用软件触发方式来使能DAC的代码:

/**
* @brief  软件使能并输出电压
* @param  输入DAC模块
* @retval 无
*/
void trigger_dac(dac_ctrl_t * p_api_ctrl)
{
   dac_instance_ctrl_t * p_ctrl = (dac_instance_ctrl_t *) p_api_ctrl;
   p_ctrl->p_reg->DACR_b.DAOE0 = 1U;   //使能DAC通道一使其输出相应电压
   //p_ctrl->p_reg->DACR_b.DAOE1 = 1U;   //使能DAC通道二使其输出相应电压
}

DA0 和 DA1 通道的转换除了可以通过软件触发以外, 还可以由输入事件信号触发启动。可供用户设置的触发源如下图所示:

DAC 触发源

27.3. 电压值转换

如果我们直接通过输入一个有效位数为12位的数字值,通过 DAC 来输出其相应的电压,这样并没有那么直观。 但是如果我们可以输入一个电压值,然后通过软件代码把这个电压转换为与其对应的数字值, 再把这个数字值写入 DA 数据寄存器,这样中间经过了一个电压值到 DA 数据寄存器值的映射转换, 可以让我们设置 DAC 输出电压时变得更加直观。

那么如何实现这样的转换呢?以下是 DA 数据寄存器值与输出电压值之间的转换公式:

\[\frac {Output\ Voltage}{3.3V} = \frac {Setting\ in\ DADRn}{4096}\]

在上面的公式中:

  • 3.3V 表示的是D/A转换的参考基准电压为 3.3V;

  • “Output Voltage” 为要输出的目标电压值;

  • 4096 等于 2 的 12 次方,对应的是 DAC 的分辨率是 12 位;

  • “Setting in DADRn” 表示的是输出目标电压值对应需要设置的 DA 数据寄存器值的值。

最终,我们根据上面的公式写成如下的函数。 函数中最后一行通过调用FSP库函数 R_DAC_Write() 将计算结果即“Setting in DADRn”的值写入 DA 数据寄存器。

/**
* @brief  设置当前的电压
* @param  需要控制的电压(范围为0~3.3V)
* @retval 无
*/
void DAC_SetVoltage(float voltage)
{
   uint16_t dac_data;
   dac_data = (uint16_t)(4096*((voltage)/3.3f));
   R_DAC_Write(&g_dac0_ctrl, dac_data);
}

27.4. 生成正弦波数据表

要输出正弦波,实质是要控制DAC以v=sin(t)的正弦函数关系输出电压,其中v为电压输出,t为时间。

而由于模拟信号连续而数字信号是离散的,所以使用DAC产生正弦波时,只能按一定时间间隔输出正弦曲线上的点,在该时间段内输出相同 的电压值,若缩短时间间隔,提高单个周期内的输出点数,可以得到逼近连续正弦波的图形,见 图26_3,若在外部电路加上适当的电容滤波,可得到更完美的图形。

图 26‑3 DAC按点输出正弦波数据(左:32个点,右:128个点)

图 26‑3 DAC按点输出正弦波数据(左:32个点,右:128个点)

由于正弦曲线是周期函数,所以只需要得到单个周期内的数据后按周期重复即可,而单个周期内取样输出的点数又是有限的,所以为了得到呈v=sin(t)函数关系电压值的数据通常不会实时计算获取,而是预先计算好函数单个周期内的电压数据表,并且转化成以DAC寄存器表示的值。

如sin函数值的范围为[-1: +1],而RA6M5的DAC输出电压范围为[0~3.3]V,按12位DAC分辨率表示的方法,可写入寄存器的最大值为212 = 4096,即范围为[0:4096]。所以,实际输出时,会进行如下处理:

  1. 抬升sin函数的输出为正值:v = sin(t)+1 ,此时,v的输出范围为[0:2];

  2. 扩展输出至DAC的全电压范围: v = 3.3*(sin(t)+1)/2 ,此时,v的输出范围为[0:3.3],正是DAC的电压输出范围,扩展至全电压范围可以充分利用DAC的分辨率;

  3. 把电压值以DAC寄存器的形式表示:Reg_val = 212/3.3 * v = 211*(sin(t)+1),此时,存储到DAC寄存器的值范围为[0:4095];

  4. 实践证明,在sin(t)的单个周期内,取32个点进行电压输出已经能较好地还原正弦波形,所以在t∈[0:2π]区间内等间距根据上述Reg_val公式运算得到32个寄存器值,即可得到正弦波表;

  5. 控制DAC输出时,每隔一段相同的时间从上述正弦波表中取出一个新数据进行输出,即可输出正弦波。改变间隔时间的单位长度,可以改变正弦波曲线的周期。

为方便起见,我们使用了Python和Matlab脚本制作正弦波表,脚本的代码存储在本工程的目录下,感兴趣可以打开文件查看,以下列出Python脚本代码,见代码清单 26‑1。

代码清单 26‑1 制作正弦波数据表的python脚本(工程目录下的sinWave.py文件)
#! python3
#coding=utf-8

"""
Python版本:3.x
外部库:matplotlib1.5.3、numpy1.11.2

运行方式:
在命令行中输入:python sinWave.py

运行结果:
命令行中会打印计算得的各点数据,
在当前目录下会生成py_dac_sinWav.c文件,包含上述数据,
并且会弹出描绘曲线的对话框。
"""

import matplotlib.pyplot as plt
import numpy as np
import math

#修改本变量可以更改点数,如16、32、64等
POINT_NUM = 32

pi = math.pi

#一个周期 POINT_NUM 个点
n = np.linspace(0,2*pi,POINT_NUM)

#计算POINT_NUM个点的正弦值
a = map(math.sin,n)

r =[]
for i in a:
   #调整幅值至在0~1区间
   i+=1

   #按3.3V电压调整幅值
   i*= 3.3/2

   #求取dac数值,12位dac LSB = 3.3V/2**12
   ri = round(i*2**12/3.3)

   #检查参数
   if ri >= 4095:
      ri = 4095

   #得到dac数值序列
   r.append( ri )

print(list(map(int,r)))

#写入序列到文件
with open("py_dac_sinWav.c",'w',encoding= 'gb2312') as f:
   print(list(map(int,r)),file= f)

#绘图
plt.plot(n,r,"-o")
plt.show()

Python脚本的实现原理就是前面介绍的正弦波数据表的制作过程,运行后,该脚本把得到的正弦波表数据输出到目录下的py_dac_sinWav.c文件中,见代码清单 26‑2,并且根据取样点描绘出示意图,见图 26‑4。Matlab脚本原理相同,此处不再列出,实际上使用C语言也能制作正弦波表,只是画图不方便而已。

代码清单 26‑2 生成的正弦波数据表
[2048, 2460, 2856, 3218, 3532, 3786, 3969, 4072, 4093, 4031, 3887, 3668,
3382, 3042, 2661, 2255, 1841, 1435, 1054, 714, 428, 209, 65, 3, 24, 127,
310, 564, 878, 1240, 1636, 2048]
图 26‑4 python 脚本根据正弦波表描绘的曲线图

图 26‑4 python 脚本根据正弦波表描绘的曲线图

27.5. 实验:使用DAC输出正弦波信号

27.5.1. 硬件设计

野火启明6M5开发板的引出引脚电路图如图所示。

图

野火启明4M2开发板的引出引脚电路图如图所示。

图

RA6M5 和 RA4M2 都有2个DAC通道,两个通道分别可以连接到 P014 和 P015 引脚上。

RA6M5 和 RA4M2 的DAC引脚

DAC通道0

P014

DAC通道1

P015

野火启明2L1开发板的引出引脚电路图如图所示。

图

RA2L1 只有1个DAC通道,该通道可以连接到 P014 引脚上。

RA2L1 的DAC引脚

DAC通道0

P014

注意:在本实验中,启明6M5、启明4M2和启明2L1开发板使用的都是 P014 引脚(DAC通道0)来输出模拟正弦波信号。

27.5.2. 软件设计

27.5.2.1. 新建工程

对于 e2 studio 开发环境:

拷贝一份我们之前的 e2s 工程模板 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “26_DAC”,最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前的 Keil 工程模板 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “26_DAC”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。

工程新建好之后,在工程根目录的 “src” 文件夹下面新建 “dac” 文件夹, 再进入该文件夹里面新建源文件和头文件:“bsp_dac.c” 和 ““bsp_dac.h”。 工程文件结构如下。

文件结构
25_DAC
├─ ......
└─ src
   ├─ led
   │  ├─ bsp_led.c
   │  └─ bsp_led.h
   ├─ debug_uart
   │  ├─ bsp_debug_uart.c
   │  └─ bsp_debug_uart.h
   ├─ adc
   │  ├─ bsp_dac.c
   │  └─ bsp_dac.h
   └─ hal_entry.c

27.5.2.2. FSP配置

打开该工程的 FSP 配置界面进行配置。

首先依次点击 “Stacks” -> “Pins” -> “Peripherals” -> “DAC0” 来配置通道 DA0 对应的引脚为 P014。 如下图所示。

图

然后依次点击 “Stacks” -> “New Stack” -> “Analog” -> “DAC (r_dac)” 来配置DAC模块。 如下图所示。

图

DAC 的属性配置:

图

实际上除了DA0引脚选择以外,在本次实验中这些属性都不怎么需要配置,按照默认值即可。

DAC 属性介绍

ADC属性

描述

Name

模块实例名,默认设置为“g_dac0”即可

Channel

通道。选择通道0

Synchronize with ADC

与ADC同步

Date Format

数据格式:右对齐/左对齐

Output Amplifer

使能DAC输出放大器

Charge Pump (Requires MOCO active)

使能DAC充电泵

Internal Output

内部输出

ELC Trigger Source

选择 ELC 触发源

Pins > DA0

选择 P014 引脚

配置完成之后可以按下快捷键“Ctrl + S”保存, 最后点右上角的 “Generate Project Content” 按钮,让软件自动生成配置代码即可。

27.5.2.3. DAC初始化函数

DAC 初始化函数如下:

代码清单 26‑3 DAC初始化函数
/**
* @brief  初始化DAC
* @param  无
* @retval 无
*/
void DAC_Init()
{
   R_DAC_Open(&g_dac0_ctrl, &g_dac0_cfg);
   R_DAC_Start(&g_dac0_ctrl);
}
  1. R_DAC_Open()配置单个 DAC 通道,启动通道,并提供用于 DAC API 写入和关闭函数的句柄。

  2. R_DAC_Start()启动 D/A 转换输出。

27.5.2.4. 设置DAC输出电压函数

代码清单 26‑4 设置DAC输出电压函数
/**
* @brief  设置当前的电压
* @param  需要控制的电压
* @retval 无
*/
void DAC_SetVoltage(float voltage)
{
   uint16_t dac_data;
   dac_data = (uint16_t)(4095*((voltage)/3.3f));
   R_DAC_Write(&g_dac0_ctrl, dac_data);
}

通过电压转换公式将输入的模拟量转换为数值量,输入到R_DAC_Write()函数里, R_DAC_Write()会将数据写入到 D/A 转换器里。

27.5.2.5. DAC输出正弦波

代码清单 26‑5 DAC输出正弦波
//正弦波数据数组变量
uint16_t var[] = {
   2048, 2460, 2856, 3218, 3532, 3786, 3969, 4072, 4093, 4031, 3887, 3668,
   3382, 3042, 2661, 2255, 1841, 1435, 1054, 714, 428, 209, 65, 3, 24, 127,
   310, 564, 878, 1240, 1636, 2048
};


/**
* @brief  生成正弦波波形
* @param  输入的值可以调节波形的周期
* @retval 无
*/
void DAC_SinWave_Cycle(uint32_t time_interval)
{
   for(uint32_t i = 0 ; i < (sizeof(var)/sizeof(var[0])); i++)
   {
      R_DAC_Write(&g_dac0_ctrl, var[i]);
      R_BSP_SoftwareDelay(time_interval, BSP_DELAY_UNITS_MILLISECONDS);
   }
}

通过轮循的方式将之前python生成的正弦波数据输入到R_DAC_Write()函数里面, 并且延时一段时间。而延时时间就是我们输入到函数里面的数值, 通过改变这一数值我们就可以调节正弦波的周期以及频率。

27.5.2.6. hal_entry入口函数

代码清单 26‑6 hal_entry入口函数
/* 用户头文件包含 */
#include "led/bsp_led.h"
#include "debug_uart/bsp_debug_uart.h"
#include "dac/bsp_dac.h"

void hal_entry(void)
{
   /* TODO: add your own code here */

   LED_Init();         // LED 初始化
   Debug_UART4_Init(); // SCI4 UART 调试串口初始化
   DAC_Init();         // DAC 初始化

   printf("这是一个DAC输出正弦波的实验例程\r\n");
   printf("使用示波器测量 P014 引脚(DAC 0)\r\n");

   while(1)
   {
      DAC_SinWave_Cycle(1);
   }


#if BSP_TZ_SECURE_BUILD
   /* Enter non-secure code */
   R_BSP_NonSecureEnter();
#endif
}

27.5.3. 下载验证

用USB TYPE-C线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。

使用示波器测量 P014 引脚输出的正弦波形,参考波形如下图所示。 注意观察示波器测量出波形的频率值和电压峰值。

图 26‑6 DAC输出的电压波形