开发环境:
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+ 以获得更精确的转换结果。
13.1.2 DAC主要特征
● 2个DAC转换器:每个转换器对应1个输出通道
● 8位或者12位单调输出
● 12位模式下数据左对齐或者右对齐
● 同步更新功能
● 噪声波形生成
● 三角波形生成
● 双DAC通道同时或者分别转换
● 每个通道都有DMA功能
● 外部触发转换
● 输入参考电压VREF+
【注意】一旦使能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寄存器。
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 之后,输出即有效,这段时间的长短依电源电压和模拟输出负载的不同会有所变化。
选择DAC触发
如果TENx位被置1,DAC转换可以由某外部事件触发(定时器计数器、外部中断线)。配置控制位TSELx[2:0] 可以选择8个触发事件之一触发DAC转换。
每次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,该寄存器的各位描述如下图所示。
DAC_CR 的低 16 位用于控制通道 1,而高 16 位用于控制通道 2,我们这里仅列出比较重要的最低 8 位的详细描述,如下图所示。
首先,我们来看 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,该寄存器各位描述如下图所示。
该寄存器用来设置 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,因此,只需配置相应的引脚即可。我们在串口的例子的基础上进行配置。
13.4.1 DAC普通方式输出
13.4.1.1 DAC基本配置
我们接下来就是DAC的配置。以PA5为例,也就是DAC的通道2。
然后生成工程即可,是不是很简单。
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配置成软件触发就可以了。
然后在循环体中加入如下语句。
HAL_DAC_Start(&hdac,DAC_CHANNEL_2);
13.4.2 DAC输出正弦波
13.4.2.1 DAC正弦波配置
在上个例子的基础上进行配置。还是使用通道2,只是这里选择定时器2作为触发事件,不使用波形发生器。
接下设置DAC的DMA,方向选为内存到外设。
接下来就是引脚的模式,设置为模拟模式,默认即可。
既然选择了定时器2作为触发事件,那么需要配置定时器2。打开Timers,使能定时器2。配置相应的参数。关于定时器的详细配置,可参考笔者以前的文章。
这里需要注意配置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的电压步进。
当然啦,还需要万用表测量引脚电压即可。你可以使用一个固定值,或者延时更长这样便于测量。为了更好的测量,笔者将转换电压设置为固定值,因此在循环体的前面加了一句话。
da = 2048;
接下来看看实验结果:
我们再来看看实际测量的结果。
这里的计算结果和实际测量稍微有些差异,但都在误差范围内。
13.5.2 DAC正弦波输出
将程序编译好后下载到板子中,通过示波器可看到波形输出。
HAL只打开了通道2,因此输出的波形如下:
欢迎访问我的网站
BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎
资源获取方式
1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[Cortex-M]获取资料提取码