Bootstrap

STM32 DAC学习日记

STM32 DAC学习日记

1. DAC简介

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

STM32 的 DAC 模块主要特点有:

① 2 个 DAC 转换器:每个转换器对应 1 个输出通道

② 8 位或者 12 位单调输出

③ 12 位模式下数据左对齐或者右对齐

④ 同步更新功能

⑤ 噪声\三角波形生成

⑥ 双 DAC 双通道同时或者分别转换

⑦ 每个通道都有 DMA 功能

image-20241031214706101

2.DAC工作原理

image-20241031215508337

2.1 参考电压/模拟部分电压

图中 VDDA 和 VSSA 为 DAC 模块模拟部分的供电,而 VREF+则是 DAC 模块的参考电压。DAC_OUTx 就是 DAC 的两个输出通道了(对应 PA4 或者 PA5 引脚)。ADC 的这些输入/输出引脚信息如下表所示:

image-20241031215614735

2.3 DAC数据格式

从图 33.1.1 可以看出,DAC 输出是受 DORx(x=1/2,下同)寄存器直接控制的,但是我们不能直接往 DORx 寄存器写入数据,而是通过 DHRx 间接的传给 DORx 寄存器,实现对 DAC输出的控制。

前面我们提到,STM32F103 的 DAC 支持 8/12 位模式,8 位模式的时候是固定的右对齐的,而 12 位模式又可以设置左对齐/右对齐。DAC 单通道模式下的数据寄存器对齐方式,总共有3种情况,如下图所示:

image-20241031215746101

① 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 通道 1,采用 12 位右对齐格式,所以采用第③种情况。另外 DAC 还具有双通道转换功能。

对于 DAC 双通道(可用时),也有三种可能的方式,如下图所示:

image-20241031215817914

① 8 位数据右对齐:用户将 DAC 通道 1 的数据写入 DAC_DHR8RD[7:0]位(实际存入DHR1[11:4]位),将 DAC 通道 2 的数据写入 DAC_DHR8RD[15:8]位(实际存入DHR2[11:4]位)。

② 12 位数据左对齐:用户将 DAC 通道 1 的数据写入 DAC_DHR12LD[15:4]位(实际存入DHR1[11:0]位),将 DAC 通道 2 的数据写入 DAC_DHR12LD[31:20]位(实际存入DHR2[11:0]位)。

③ 12 位数据右对齐:用户将 DAC 通道 1 的数据写入 DAC_DHR12RD[11:0]位(实际存入DHR1[11:0]位),将 DAC 通道 2 的数据写入 DAC_DHR12RD[27:16]位(实际存入DHR2[11:0]位)。

2.4 触发源

DAC 可以通过自动触发、软件触发、外部事件触发,通过配置 TENx 控制位来决定。

如果没有选中硬件触发(寄存器 DAC_CR 的 TENx 位置 0),存入寄存器 DAC_DHRx 的数据会在1个APB1时钟周期后自动传至寄存器DAC_DORx。如果选中硬件触发(寄存器DAC_CR的 TENx 位置 1),数据传输在触发发生以后 3个 APB1时钟周期后完成。一旦数据从DAC_DHRx寄存器装入 DAC_DORx 寄存器,在经过时间 tSETTLING之后,输出即有效,这段时间的长短依电源电压和模拟输出负载的不同会有所变化。我们可以从《《STM32F103ZET6.pdf》数据手册查到tSETTLING的典型值为 3us,最大是 4us,所以 DAC 的转换速度最快是 333K 左右。

image-20241031220039703

如果使用硬件触发(TENx=1),可通过外部事件(定时计数器、外部中断线)触发 DAC 转换。由 TSELx[2:0]控制位来决定选择 8 个触发事件中的一个来触发转换。触发事件如下表所示:

image-20241031220424679

image-20241031220216613

2.5 DMA请求

image-20241031220302608

每个 DAC 通道都有 DMA 功能,两个 DMA 通道分别用于处理两个 DAC 通道的 DMA 请求。如果 DMAENx 位置 1 时,如果发生外部触发(而不是软件触发),就会产生一个 DMA 请求,然后 DAC_DHRx 寄存器的数据被转移到 DAC_NORx 寄存器。

2.6 DAC输出电压

image-20241031220334130

3.DAC输出实验

3.1 实验简要

  1. 功能描述:通过DAC1通道1(PA4)输出预设电压,然后由ADC1通道1 (PA1) 采集,最后显示ADC转换的数字量及换算后的电压值

  2. 关闭通道1触发(即自动):TEN1位置0

  3. 关闭输出缓冲:BOFF1位置1

  4. 使用12位右对齐模式:将数字量写入DAC_DHR12R1寄存器

DAC_CR寄存器框图

image-20241031220910575

image-20241031220828384

3.2 相关HAL函数介绍

函数主要寄存器主要功能
HAL_DAC_Init()配置DAC工作状态(HAL库内部使用)
HAL_DAC_MspInit()存放NVIC、CLOCK、GPIO初始化代码
HAL_DAC_ConfigChannel()CR配置DAC相应通道的相关参数
HAL_DAC_Start()CR、SWTRIGR启动D/A转换
HAL_DAC_SetValue()DHR12Rx设置输出数字量
HAL_DAC_GetValue()DORx读取通道输出数字量

3.3 关键结构体介绍

typedef struct 
{ 
	DAC_TypeDef *Instance; 				/* DAC 寄存器基地址 */
 	__IO HAL_DAC_StateTypeDef State; 	/* DAC 工作状态 */
 	HAL_LockTypeDef Lock; 				/* DAC 锁定对象 */
 	DMA_HandleTypeDef *DMA_Handle1; 	/* 通道 1 的 DMA 处理句柄指针 */
 	DMA_HandleTypeDef *DMA_Handle2; 	/* 通道 2 的 DMA 处理句柄指针 */
 	__IO uint32_t ErrorCode; 			/* DAC 错误代码 */ 
} DAC_HandleTypeDef; 

typedef struct 
{
 	uint32_t DAC_Trigger; 		/* DAC 触发源的选择 */
 	uint32_t DAC_OutputBuffer; 	/* 启用或者禁用 DAC 通道输出缓冲区 */
 } DAC_ChannelConfTypeDef;

3.4 例程代码

dac.c

#include "./BSP/DAC/dac.h"
DAC_HandleTypeDef g_dac_handle;

/* DAC初始化函数 */
void dac_init(void)
{
    DAC_ChannelConfTypeDef dac_ch_conf;

    g_dac_handle.Instance = DAC;
    HAL_DAC_Init(&g_dac_handle);                                        /* 初始化DAC */

    dac_ch_conf.DAC_Trigger = DAC_TRIGGER_NONE;                         /* 不使用触发功能 */
    dac_ch_conf.DAC_OutputBuffer = DAC_OUTPUTBUFFER_DISABLE;            /* DAC输出缓冲关闭 */

    HAL_DAC_ConfigChannel(&g_dac_handle, &dac_ch_conf, DAC_CHANNEL_1);  /* 配置DAC通道1 */
    HAL_DAC_Start(&g_dac_handle, DAC_CHANNEL_1);                        /* 开启DAC通道1 */
}

/* DAC MSP初始化函数 */
void HAL_DAC_MspInit(DAC_HandleTypeDef *hdac)
{
    if (hdac->Instance == DAC)
    {
        GPIO_InitTypeDef gpio_init_struct;

        __HAL_RCC_DAC_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();

        gpio_init_struct.Pin = GPIO_PIN_4;
        gpio_init_struct.Mode = GPIO_MODE_ANALOG;
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);
    }
}

/* 设置通道输出电压 */
void dac_set_voltage(uint16_t vol)
{
    double temp = vol;
    temp /= 1000;
    temp = temp * 4096 / 3.3;

    if (temp >= 4096)temp = 4095;   /* 如果值大于等于4096, 则取4095 */

    HAL_DAC_SetValue(&g_dac_handle, DAC_CHANNEL_1, DAC_ALIGN_12B_R, temp); /* 12位右对齐数据格式设置DAC值 */
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/DAC/dac.h"
#include "./BSP/ADC/adc.h"
#include "./USMART/usmart.h"

int main(void)
{
    uint16_t adcx;
    float temp;
    
    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟, 72Mhz */
    delay_init(72);                             /* 延时初始化 */
    usart_init(115200);                         /* 串口初始化为115200 */
    usmart_init(72);							/* 我这里用了USMART调试电压,也可以不用 */
    led_init();                                 /* 初始化LED */
    lcd_init();                                 /* 初始化LCD */
    key_init();                                 /* 初始化按键 */
    adc_init();                                 /* 初始化ADC */
    dac_init();                                 /* 初始化DAC1_OUT1通道 */

    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30, 70, 200, 16, 16, "ADC TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH1_VAL:", BLUE);
    lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH1_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */

    dac_set_voltage(2000);

    while (1)
    {
        adcx = adc_get_result();
        lcd_show_xnum(134, 110, adcx, 5, 16, 0, BLUE);   /* 显示ADCC采样后的原始值 */
 
        temp = (float)adcx * (3.3 / 4096);               /* 获取计算后的带小数的实际电压值,比如3.1111 */
        adcx = temp;                                     /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
        lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);   /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */

        temp -= adcx;                                    /* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
        temp *= 1000;                                    /* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数。 */
        lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);/* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */

        LED0_TOGGLE();
        delay_ms(250);
    }
}
;