【正点原子STM32】DAC数模转换器(DAC特性、DAC工作原理、DAC输出实验配置步骤、DAC输出三角波实验、DAC输出正弦波实验配置步骤、PWM + RC滤波器、PWM DAC技术实现原理)

一、DAC简介

二、DAC工作原理

三、DAC输出实验

四、DAC输出三角波实验

五、DAC输出正弦波实验

六、PWM DAC实验

七、总结

一、DAC简介

1.1、什么是DAC?

在这里插入图片描述
DAC(数字模拟转换器) 是一种电子设备或电路,用于将数字信号转换为相应的模拟信号。在现代电子系统中,DAC通常用于将数字信号转换为模拟电压或电流输出,以供模拟电路或外部设备使用。

DAC的基本工作原理是根据输入的数字信号值,在一定的时间间隔内产生相应的模拟输出。这个输出可以是连续的模拟电压或电流信号,也可以是离散的模拟量。DAC通常由一个数字信号输入端、一个模拟输出端和控制电路组成。

DAC的应用非常广泛,包括音频处理、通信系统、控制系统、测试与测量仪器等。在音频领域,DAC常用于数字音频播放器、音频接口、功放等设备中,将数字音频信号转换为模拟音频信号输出到扬声器或耳机。在控制系统中,DAC可用于生成模拟控制信号,控制电机、阀门、灯光等设备的运动或状态。

总之,DAC是数字电路与模拟电路之间的重要接口,它实现了数字信号到模拟信号的转换,为数字系统与模拟系统之间的数据交互提供了基础。

1.2、DAC的特性参数

在这里插入图片描述
DAC(数字模拟转换器)的特性参数包括以下几个方面:

  1. 分辨率(Resolution):
    分辨率是DAC能够生成的模拟输出的精度,通常以比特数(位)来表示。例如,一个12位的DAC可以产生4096个离散的输出电压级别,即2的12次方个级别。分辨率越高,DAC输出的模拟信号的精度越高。

  2. 建立时间(Settling Time):
    建立时间是指DAC在输入信号发生变化后,输出信号稳定到指定精度所需的时间。它包括建立时间和保持时间两个部分。建立时间是从输入信号发生变化到DAC输出信号达到指定精度所需的时间,而保持时间是DAC输出信号保持在指定精度范围内的时间。建立时间的快慢直接影响DAC的动态性能。

  3. 精度(Accuracy):
    DAC的精度表示DAC输出信号与理想输出信号之间的最大偏差。精度通常以百分比或者绝对电压值表示。DAC的精度受到多种误差源的影响,主要包括:

    • 比例系统误差(Gain Error):表示输出信号与输入数字之间的比例误差。
    • 失调误差(Offset Error):表示DAC输出信号的零点偏差,即输入数字为0时输出信号与理想零点之间的差异。
    • 非线性误差(Nonlinearity Error):表示DAC输出信号的非线性程度,即输入数字变化时输出信号的非线性偏差。
      这些误差源通常由元件参数误差、基准电压不稳定、运算放大器零漂等因素引起。

除了以上特性参数外,DAC的其他重要参数还包括动态范围、失真、抖动等。这些参数综合反映了DAC的性能水平,对于不同应用场景的设计和选择都具有重要意义。

1.3、STM32各系列DAC的主要特性

在这里插入图片描述
以下是STM32各系列DAC的主要特性:

  1. DAC输出类型

    • 通常为单端输出,但某些型号也支持双端输出。
  2. 分辨率(Resolution)

    • 分辨率指DAC能够产生的不同模拟输出电压级别的数量,通常以比特数(位)来表示。
  3. DAC时钟频率

    • DAC模块内部时钟频率,即数字信号转换成模拟信号的速度。较高的时钟频率能够提高DAC的速度和性能。
  4. 建立时间(Settling Time)

    • 建立时间是DAC输出在输入信号发生变化后,稳定到指定精度所需的时间。较短的建立时间可以提高DAC的动态性能。
  5. 供电电压

    • DAC模块的供电电压范围,通常为VDD。
  6. 参考电压(Reference Voltage)

    • DAC模块使用的参考电压,用于将数字信号转换为相应的模拟电压。参考电压的稳定性和精确性直接影响DAC输出的准确性。
  7. 输出通道

    • 每个DAC模块可以具有一个或多个输出通道,允许同时产生多路模拟输出信号。

具体的特性参数可能会因不同的STM32系列和型号而有所不同,需要查阅对应型号的数据手册或参考手册以获取详细信息。

二、DAC工作原理

2.1、DAC框图简介(F1/ F4 /F7/H7)

在这里插入图片描述

  1. 参考电压/模拟部分电压:在数字模拟转换器(DAC)中,参考电压是用来确定数字输入值与模拟输出电压之间关系的基准电压。模拟部分电压指的是DAC输出的模拟电压信号。

  2. DA转换器:DA转换器是数字模拟转换器的简称,它将数字信号转换为模拟信号。

  3. 输出通道:指DAC输出信号传输的通道,可以是电缆、接插件或者其他传输介质。

  4. 数据输出寄存器:用于存储DAC输出的数字信号的寄存器,通常是一个用于临时存储数据的寄存器。

  5. 数据保持寄存器:用于保持DAC输出的数字信号的寄存器,通常是一个用于保持数据不变的寄存器。

  6. 控制逻辑(噪声波/三角波):DAC的控制逻辑负责生成控制信号,以控制DAC的输出波形,可以包括噪声波或三角波等。

  7. DAC控制寄存器:用于存储DAC的控制参数和配置信息的寄存器,可以用来设置DAC的工作模式、输出范围等参数。

  8. 触发源:触发源是指触发DAC开始转换的信号源,可以是外部触发信号、定时器或其他触发源。
    在这里插入图片描述
    在STM32H7系列微控制器中,DAC的框图可以简要介绍如下:

  9. 参考电压/模拟部分电压:提供给DAC的参考电压,用于确定数字输入值与模拟输出电压之间的关系。

  10. DA转换器:负责将数字信号转换为模拟信号的模块。

  11. 输出通道:DAC输出信号传输的通道,可以是电缆、接插件或其他传输介质。

  12. 数据输出寄存器:用于存储要转换的数字信号值。

  13. 数据保持寄存器(DHRx):用于保持DAC输出的数字信号,其中"x"表示DAC通道号。

  14. DAC通道模式:包括正常模式和采样保持模式,用于配置DAC的工作模式。

  15. DAC控制寄存器:用于配置DAC的控制参数和工作模式。

  16. 触发源:触发DAC开始转换的信号源,可以是外部触发信号、定时器或其他触发源。

这些组成部分共同构成了DAC的框图,实现了数字到模拟信号的转换功能,并提供了丰富的配置选项和灵活的工作模式。

2.2、参考电压/模拟部分电压

在这里插入图片描述
参考电压在电子电路中,通常是指一个稳定的已知电压值,用于作为比较或者基准。在这个DAC(数模转换器)电路中, V R E F − V_{REF-} VREF V R E F + _{VREF+} VREF+ 分别表示参考电压的下限和上限,决定了DAC输出电压的范围。例如,如果 V R E F − V_{REF-} VREF=0V, V R E F + V_{REF+} VREF+=3.3V,则DAC的输出电压范围就是0V到3.3V。

RC低通滤波器是一种常见的模拟电路,它由电阻R和电容C组成,主要功能是允许低频信号顺利通过,同时衰减或抑制高频信号,起到平滑输出电压、减少噪声的作用。

DAC供电电源包括 V S S A V_{SSA} VSSA V D D A V_{DDA} VDDA V S S A V_{SSA} VSSA一般代表模拟地, V D D A V_{DDA} VDDA则是为DAC的模拟部分提供的工作电压,其电压范围要求在2.4V至3.6V之间,确保DAC能在指定电压范围内正常工作并提供准确的模拟信号输出。

2.3、DAC数据格式

在这里插入图片描述
在数字到模拟转换器(DAC)中,数据格式指的是数字信号如何被组织和加载到DAC寄存器中,以便转换成相应的模拟信号。对于STM32系列微控制器的DAC模块,它支持8位和12位两种数据格式,并且针对这两种格式有不同的对齐方式:

  1. 8位模式

    • 在8位模式下,数字数据仅占用8位宽度,只使用最低有效位(LSB)部分。数据总是“右对齐”,意味着8位数据被填充到DAC数据寄存器的最低8位中,比如DHR8Rx(单个DAC通道)或DHR8RD(两个DAC通道同时更新)。
  2. 12位模式

    • 右对齐
      • 在12位右对齐模式下,12位数字数据填入到DAC数据寄存器的低12位中,高位剩余位通常是无效的或者保持不变,如DHR12Rx(单个通道)或DHR12RD(双通道)。
    • 左对齐
      • 在12位左对齐模式下,12位数字数据放在DAC数据寄存器的最高12位,低4位(如果是16位寄存器)通常保留为零,这种模式适用于需要更高分辨率的应用,对应的寄存器是DHR12Lx(单个通道)或DHR12LD(双通道)。

每个不同的寄存器设计都是为了适应不同精度需求下的数据加载,确保DAC能够按照所设置的位数和对齐方式进行正确有效的数模转换。当向这些寄存器写入数字数据时,DAC会基于选定的参考电压VREF+和VREF-以及所选的分辨率,生成对应的模拟电压输出。

2.4、触发源

在这里插入图片描述
触发源在不同的电子或控制系统中有着不同的含义,“三种触发转换的方式”,它们在数字逻辑电路、模拟电路和嵌入式系统中的典型意义:

  1. 自动触发(Auto Trigger)

    • 在示波器等测试仪器中,自动触发模式意味着示波器会自动寻找输入信号中的合适边缘(如上升沿或下降沿)作为触发点,不断采集并刷新波形。在这种模式下,用户无需手动设定触发条件,示波器会尽可能保持稳定的波形显示。
  2. 软件触发(Software Trigger)

    • 在计算机系统或嵌入式系统中,软件触发是指通过编写程序代码来控制某一事件的发生。例如,在微控制器中,软件触发可能是通过软件指令来启动ADC转换、定时器中断或者其他硬件模块的动作。软件触发完全依赖于CPU执行指令序列,不涉及硬件级别的触发信号。
  3. 外部事件触发(External Event Trigger)

    • 在模拟和数字电路中,外部事件触发通常是指由独立于处理器系统的外部信号引起的行为改变。例如,ADC(模数转换器)的外部触发可能来自于GPIO引脚上的高低电平转换、特定的脉冲信号或者I/O接口上的特定协议事件。在示波器上,外部事件触发意味着示波器等待特定的外部信号到达后才开始捕获或显示数据。

总结起来,触发源是导致特定动作或过程开始的一个信号源头,它可以是自动检测到的信号变化,也可以是根据预先设定的软件指令,或者是来自系统外部的物理信号。
在这里插入图片描述

2.5、DMA请求

在这里插入图片描述
DMA请求(Direct Memory Access Request)是一种让硬件直接访问内存的技术,无需CPU介入,从而提高数据传输效率,减轻CPU负担。可以这样理解:

  1. DMAENx位置1

    • DMAENx通常代表DMA通道x的使能位,将其置1意味着激活该通道的DMA功能,允许该通道进行DMA传输。
  2. 外部事件触发

    • 在DMA传输的过程中,可以设置不同的触发方式。如果采用外部事件触发,那么DMA传输将在接收到特定外部硬件事件(如定时器溢出、ADC转换完成、SPI/I²C通信完成等)时启动。这与软件触发不同,软件触发需要通过CPU发出指令来启动DMA传输。
  3. 产生DMA请求

    • 当外部事件发生时,相关的硬件模块会向DMA控制器发送一个DMA请求信号。一旦DMA控制器接收到请求并且满足优先级条件,它就开始执行相应的DMA传输任务。
  4. DHRx→DORx

    • DHRx通常是DMA控制器中用于存储源地址或传输数据的寄存器(Data Holding Register),当DMA传输开始时,DHRx中的数据将被转移到目标地址。
    • DORx通常指的是DMA控制器的目标地址寄存器(Destination Output Register),在DMA传输过程中,数据将从源地址经过DMA传输通道写入到DORx指向的目标地址。

综上所述,当设置了DMAENx并选择了外部事件触发模式后,一旦外部事件发生触发DMA请求,数据就会从DHRx指向的源地址通过DMA传输到DORx指定的目标地址。整个过程无需CPU参与数据搬移。

2.6、DAC输出电压

在这里插入图片描述
在数字模拟转换器(DAC)中,DORx寄存器通常用来存放要转换成模拟信号的数字值。VREF+则是DAC参考电压,它是决定输出电压范围上限的基准电压。

  1. 12位模式下

    • DAC输出电压的计算公式为: D A C 输出电压 = ( D O R x / 4096 ) ∗ V R E F + DAC输出电压 = (DORx / 4096) * V_{REF+} DAC输出电压=(DORx/4096)VREF+
    • 这里的4096是因为12位模式下,DORx可以表示从0到4095共4096个不同的数值,每一个数值对应着 V R E F + V_{REF+} VREF+电压的一个细分比例。
  2. 8位模式下

    • DAC输出电压的计算公式为: D A C 输出电压 = ( D O R x / 256 ) ∗ V R E F + DAC输出电压 = (DORx / 256) * V_{REF+} DAC输出电压=(DORx/256)VREF+
    • 在8位模式下,DORx可以表示从0到255共256个不同的数值,每一个数值对应 V R E F + V_{REF+} VREF+电压的256分之一。

举例来说,假设 V R E F + V_{REF+} VREF+为3.3V:

  • 在12位模式下,若DORx=2048,则 D A C 输出电压 = ( 2048 / 4096 ) ∗ 3.3 V = 1.65 V 。 DAC输出电压 = (2048 / 4096) * 3.3V = 1.65V。 DAC输出电压=(2048/4096)3.3V=1.65V
  • 在8位模式下,若DORx=128,则 D A C 输出电压 = ( 128 / 256 ) ∗ 3.3 V = 1.65 V 。 DAC输出电压 = (128 / 256) * 3.3V = 1.65V。 DAC输出电压=(128/256)3.3V=1.65V

通过这样的计算,可以根据DORx寄存器中的数字值,得到相应的模拟电压输出值。

三、DAC输出实验

3.1、实验简要

在这里插入图片描述
实验简要步骤如下:

  1. 功能描述

    • 本实验旨在演示STM32F1系列微控制器的DAC(Digital-to-Analog Converter)通道1(PA4引脚)输出预设的模拟电压,然后通过ADC(Analog-to-Digital Converter)通道1(PA1引脚)采集此模拟电压,并在终端或其他显示设备上显示出ADC转换得到的数字量及其对应的电压值。
  2. 关闭通道1触发(自动触发模式)

    • 为了使DAC输出不受外部触发影响,仅在程序控制下输出预设电压,需设置DAC通道1的相关寄存器,将触发模式关闭。具体操作是在DAC通道1控制寄存器(DAC_CR)中将TEN1位(Trigger Enable for DAC channel1)设置为0,即DAC->CR &= ~(DAC_CR_TEN1);
  3. 关闭输出缓冲

    • 若要关闭DAC通道1的输出缓冲,可以在DAC通道1配置寄存器(DAC_CR)中设置BOFF1位(Buffer Output Disable for DAC channel1)为1,即DAC->CR |= DAC_CR_BOFF1;。这样做的目的是减少输出阻抗,减小对外部负载的影响。
  4. 使用12位右对齐模式

    • 选择12位右对齐模式,意味着要将预设的12位数字量直接写入到DAC通道1的12位右对齐数据寄存器(DAC_DHR12R1)。例如,若要输出数字量data_12bit,则可以通过如下代码写入:
      DAC->DHR12R1 = (uint16_t)data_12bit;
      
  5. 实验流程

    • 首先配置DAC和ADC的相关参数(如时钟、工作模式、采样时间等)。
    • 关闭DAC1通道1的触发和输出缓冲。
    • 将预设的12位数字量写入DAC1通道1的右对齐数据寄存器。
    • 启动DAC1通道1的输出。
    • 通过ADC1通道1采集PA4上的模拟电压。
    • 将ADC转换得到的数字量转换为对应的电压值,然后显示出来。

注意:在实际实验中,还需要确保ADC的参考电压已正确设置,并且在ADC转换前已经完成了必要的校准步骤。同时,DAC输出的电压范围受VREF+电压影响,所以在计算电压值时,需要用到正确的参考电压值。

3.2、DAC寄存器介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.3、DAC输出实验配置步骤

在这里插入图片描述
在STM32的DAC输出实验中,配置步骤的确大致如此,不过为了确保准确,以下是具体的细节步骤:

  1. 初始化DAC

    HAL_DAC_Init(&hdac); // hdac 是 DAC_HandleTypeDef 结构体实例
    

    这一步初始化整个DAC外设,包括但不限于使能DAC时钟,初始化相关的底层硬件资源。

  2. DAC的MSP(微服务处理器)初始化

    HAL_DAC_MspInit(&hdac);
    

    这一步通常是由用户自行实现的回调函数,负责初始化与DAC相关的外围设备,比如配置DAC所使用的GPIO引脚为模拟输出模式,配置与DAC有关的NVIC中断,以及确保DAC工作所需的时钟已经使能。

  3. 配置DAC通道相关参数

    HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1); // sConfig 是 DAC_ChannelConfTypeDef 结构体实例
    

    在这里设置指定通道的具体工作模式(例如12位右对齐模式),触发模式,波形生成模式等参数。

  4. 启动D/A转换

    HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
    

    该函数用于启动选定通道的DAC转换功能。

  5. 设置输出数字量

    HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, output_value);
    

    设置选定通道输出的数字量值,这里的output_value是你要转换成模拟信号的12位数字量(对于12位右对齐模式),DAC_ALIGN_12B_R表示数据格式是12位右对齐。

  6. 读取通道输出数字量(可选)

    uint32_t value;
    HAL_DAC_GetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, &value);
    

    这一步是可选的,用于读取DAC通道当前输出的数字量值,通常在需要反馈输出状态或进行某种闭环控制时使用。

以上代码适用于STM32 HAL库环境下进行DAC输出配置。在实际使用中,请确保根据实际电路连接和需求调整相应的GPIO引脚、通道号等参数。
在这里插入图片描述
在这里插入图片描述

3.4、编程实战:DAC输出实验

在这里插入图片描述
dac.c

// 引入自定义的BSP层DAC驱动头文件
#include "./BSP/DAC/dac.h"

// 定义全局DAC句柄结构体变量
DAC_HandleTypeDef g_dac_handle; // 这个结构体包含了DAC外设的所有配置及状态信息

/* DAC初始化函数 */
void dac_init(void)
{
    // 创建一个DAC通道配置结构体实例
    DAC_ChannelConfTypeDef dac_ch_conf;

    // 将DAC硬件实例设置为全局DAC句柄指向的实际DAC外设地址
    g_dac_handle.Instance = DAC; 

    // 调用HAL库提供的初始化函数,初始化整个DAC外设
    HAL_DAC_Init(&g_dac_handle); // 对整个DAC模块进行初始化操作

    // 配置DAC通道不使用触发功能(即输出不受外部事件触发)
    dac_ch_conf.DAC_Trigger = DAC_TRIGGER_NONE;

    // 关闭DAC通道的输出缓冲(提高输出响应速度,减少延迟)
    dac_ch_conf.DAC_OutputBuffer = DAC_OUTPUTBUFFER_DISABLE;

    // 针对通道1应用上述配置,并调用HAL库函数完成配置
    HAL_DAC_ConfigChannel(&g_dac_handle, &dac_ch_conf, DAC_CHANNEL_1); 

    // 启动指定通道(这里是通道1)的数据转换过程
    HAL_DAC_Start(&g_dac_handle, DAC_CHANNEL_1); 
}

/* DAC的MSP(MCU特定封装)初始化函数 */
void HAL_DAC_MspInit(DAC_HandleTypeDef *hdac)
{
    // 如果传入的DAC实例是指向实际DAC外设
    if (hdac->Instance == DAC)
    {
        // 初始化GPIO结构体
        GPIO_InitTypeDef gpio_init_struct;

        // 使能DAC和GPIOA时钟
        __HAL_RCC_DAC_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();

        // 配置GPIOA的Pin 4为模拟模式(连接到DAC通道对应引脚)
        gpio_init_struct.Pin = GPIO_PIN_4;
        gpio_init_struct.Mode = GPIO_MODE_ANALOG;

        // 初始化GPIOA的Pin 4
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);
    }
}

/* 设置DAC通道输出电压的函数 */
void dac_set_voltage(uint16_t vol)
{
    // 将输入的电压值(单位毫伏)转换为适合DAC的12位右对齐数值
    double temp = vol;
    temp /= 1000; // 将输入电压从毫伏转换为伏特
    temp = temp * 4096 / 3.3; // 计算12位DAC所能表示的最大电压范围内的相应数值

    // 如果计算出的数值超过4096(12位的最大值),则将其设置为4095
    if (temp >= 4096) temp = 4095;  

    // 使用12位右对齐方式,通过HAL库函数设置DAC通道1的输出电压值
    HAL_DAC_SetValue(&g_dac_handle, DAC_CHANNEL_1, DAC_ALIGN_12B_R, (uint32_t)temp); 
}

这段代码主要完成了以下几个步骤:

  1. 初始化了DAC外设及其相关的GPIO端口。
  2. 设置了DAC通道1的工作模式,包括禁用触发器和输出缓冲。
  3. 提供了一个用于设置DAC通道1输出电压的函数,该函数接受一个输入电压值(以毫伏为单位),并将其转换为与DAC兼容的12位数字值,然后通过HAL库函数将该值写入DAC寄存器。由于DAC的分辨率为12位,因此其最大输出电压与参考电压相关联,这里假设参考电压为3.3V。

dac.h

#ifndef __DAC_H
#define __DAC_H

#include "./SYSTEM/sys/sys.h"


void dac_init(void);				/* DAC初始化函数 */
void dac_set_voltage(uint16_t vol);	/* 设置DAC通道输出电压的函数 */

#endif

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"


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 */
    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);
    }
}

在这里插入图片描述
在这里插入图片描述

四、DAC输出三角波实验

4.1、实验简要

在这里插入图片描述
在STM32F1系列微控制器上利用DAC通道1输出三角波,并通过DS100示波器查看波形的实验流程可以概括如下:

  1. 功能描述
    实验目标是在STM32F1系列MCU的DAC1通道1(PA4引脚)上产生一个连续变化的三角波,并通过外部示波器(如DS100)捕获显示这个波形。

  2. 关闭通道1触发
    在DAC的触发模式配置中,确保通道1不采用触发方式工作,而是自动连续更新。这可以通过配置相关的寄存器位实现,对应到STM32F1的DAC_CR寄存器中的TEN1位,将其清零(TEN1 = 0)即可。

  3. 关闭输出缓冲
    为了能够直接控制DAC输出并可能减小输出阻抗,需要关闭DAC通道1的内部输出缓冲。在STM32F1中,这涉及到配置DAC_CR寄存器中的BOFF1位,将其置1(BOFF1 = 1)来禁用输出缓冲。

  4. 设置12位右对齐模式
    使用12位右对齐模式意味着每次向DAC写入的数据都会被当作12位无符号整数对待,最高有效位位于12位位置。为此,需要配置DAC通道的相关控制寄存器,并选择正确的数据对齐模式。

  5. 生成三角波
    要输出三角波,需要在主循环中不断改变写入到DAC_DHR12R1寄存器的数值。从最小有效电压逐渐增加到最大有效电压,然后再从最大降至最小,形成周期性变化,从而得到三角波形。

具体操作步骤如下:

  • 初始化DAC外设及PA4引脚为模拟输出模式。
  • 配置DAC1通道1的工作模式为12位右对齐。
  • 关闭触发和开启输出缓冲。
  • 在主循环中,按照三角波的变化规律,每隔一定时间间隔(可通过定时器实现)更新DAC_DHR12R1寄存器的值。
  • 连接示波器至PA4引脚,观察并调整波形频率、幅度等参数。

需要注意的是,STM32F1并不自带波形发生器功能,因此生成三角波需要编写相应的软件算法来更新DAC的输出值。此外,在实际编程中,上述寄存器操作应当使用HAL库或者直接操作寄存器的方式完成。

4.2、编程实战:DAC输出三角波实验

在这里插入图片描述
dac.c

// 引入BSP层DAC驱动头文件
#include "./BSP/DAC/dac.h"
// 引入系统级延时函数头文件
#include "./SYSTEM/delay/delay.h"

// 定义全局DAC处理句柄
DAC_HandleTypeDef g_dac_handle;

// DAC初始化函数
// 作用:初始化DAC外设及相关GPIO,设置DAC通道1为非触发模式且关闭输出缓冲
void dac_init(void)
{
    DAC_ChannelConfTypeDef dac_ch_conf;

    // 设置DAC实例指针
    g_dac_handle.Instance = DAC;
    // 调用HAL库函数初始化DAC
    HAL_DAC_Init(&g_dac_handle);

    // 配置DAC通道1参数:无触发源,关闭输出缓冲
    dac_ch_conf.DAC_Trigger = DAC_TRIGGER_NONE;
    dac_ch_conf.DAC_OutputBuffer = DAC_OUTPUTBUFFER_DISABLE;
    HAL_DAC_ConfigChannel(&g_dac_handle, &dac_ch_conf, DAC_CHANNEL_1);

    // 启动DAC通道1
    HAL_DAC_Start(&g_dac_handle, DAC_CHANNEL_1);
}

// DAC的MSP初始化函数(MCU特定底层初始化)
void HAL_DAC_MspInit(DAC_HandleTypeDef *hdac)
{
    // 如果hdac指向实际的DAC外设
    if (hdac->Instance == DAC)
    {
        GPIO_InitTypeDef gpio_init_struct;

        // 使能GPIOA和DAC时钟
        __HAL_RCC_GPIOA_CLK_ENABLE();
        __HAL_RCC_DAC_CLK_ENABLE();

        // 配置GPIOA Pin 4为模拟模式(通常连接到DAC输出)
        gpio_init_struct.Pin = GPIO_PIN_4;
        gpio_init_struct.Mode = GPIO_MODE_ANALOG;
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);
    }
}

/**
 * 函数:设置DAC_OUT1输出三角波
 * 注释:
 *   根据给定参数生成三角波形,输出频率近似为 1000 / (dt * samples) kHz。
 *   注意:当延时时间dt较小(如小于5us)时,由于延时函数本身的误差,实际输出频率可能偏低。
 * 参数:
 *   maxval:三角波峰值,取值范围为0 < maxval < 4096,且(maxval + 1)应大于等于samples/2
 *   dt     :每个采样点之间的延时时间(单位:us)
 *   samples:采样点总数,必须满足 samples <= (maxval + 1) * 2,并且maxval不能等于0
 *   n      :输出三角波形的个数,取值范围为0~65535
 * 返回值:
 *   无
 */
void dac_triangular_wave(uint16_t maxval, uint16_t dt, uint16_t samples, uint16_t n)
{
    uint16_t i, j;
    float incval; // 递增步长
    float Curval; // 当前输出值
    
    // 参数合法性检查
    if(samples > ((maxval + 1) * 2)) return;

    // 计算递增步长
    incval = (float)(maxval + 1) / (samples / 2); 

    // 循环输出n个完整三角波形
    for(j = 0; j < n; j++)
    {
        Curval = 0; // 重置当前输出值为0
        HAL_DAC_SetValue(&g_dac_handle, DAC_CHANNEL_1, DAC_ALIGN_12B_R, Curval); // 输出初始值0
        
        // 输出上升沿
        for(i = 0; i < (samples / 2); i++)
        {
            Curval += incval; // 更新当前值
            HAL_DAC_SetValue(&g_dac_handle, DAC_CHANNEL_1, DAC_ALIGN_12B_R, Curval); // 输出新值
            delay_us(dt); // 延时
        }

        // 输出下降沿
        for(i = 0; i < (samples / 2); i++)
        {
            Curval -= incval; // 更新当前值
            HAL_DAC_SetValue(&g_dac_handle, DAC_CHANNEL_1, DAC_ALIGN_12B_R, Curval); // 输出新值
            delay_us(dt); // 延时
        }
    }
}

这个函数dac_triangular_wave用于通过DAC输出三角波形。它根据给定的最大值、采样间隔时间、采样点数量以及波形重复次数来生成三角波形。每次循环内先输出上升沿,再输出下降沿,形成一个完整的三角波周期。并通过HAL库中的HAL_DAC_SetValue函数实时更新DAC输出值,并利用delay_us函数插入固定延时,实现对三角波形的逐点生成。
dac.h

#ifndef __DAC_H
#define __DAC_H

#include "./SYSTEM/sys/sys.h"


void dac_init(void);																	// DAC初始化函数
void dac_triangular_wave(uint16_t maxval, uint16_t dt, uint16_t samples, uint16_t n);	// 设置DAC_OUT1输出三角波

#endif

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/ADC/adc.h"
#include "./BSP/DAC/dac.h"
#include "./BSP/KEY/key.h"


int main(void)
{
    uint8_t t = 0; 
    uint8_t key;

    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    usart_init(115200);                 /* 串口初始化为115200 */
    led_init();                         /* 初始化LED */
    lcd_init();                         /* 初始化LCD */
    key_init();                         /* 初始化按键 */
    dac_init();                         /* 初始化DAC1_OUT1通道 */

    lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30,  70, 200, 16, 16, "DAC Triangular WAVE TEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY0:Wave1  KEY1:Wave2", RED);
    lcd_show_string(30, 130, 200, 16, 16, "DAC None", BLUE); /* 提示无输出 */

    while (1)
    {
        t++;
        key = key_scan(0);                           /* 按键扫描 */

        if (key == KEY0_PRES)                        /* 高采样率 , 100hz波形 , 实际只有65.5hz */
        {
            lcd_show_string(30, 130, 200, 16, 16, "DAC Wave1 ", BLUE);
            dac_triangular_wave(4095, 5, 2000, 100); /* 幅值4095, 采样点间隔5us, 2000个采样点, 100个波形 */
            lcd_show_string(30, 130, 200, 16, 16, "DAC None  ", BLUE);
        }
        else if (key == KEY1_PRES)                   /* 低采样率 , 100hz波形 , 实际99.5hz */
        {
            lcd_show_string(30, 130, 200, 16, 16, "DAC Wave2 ", BLUE);
            dac_triangular_wave(4095, 500, 20, 100); /* 幅值4095, 采样点间隔500us, 20个采样点, 100个波形 */
            lcd_show_string(30, 130, 200, 16, 16, "DAC None  ", BLUE);
        }

        if (t == 10)                                 /* 定时时间到了 */
        {
            LED0_TOGGLE();                           /* LED0闪烁 */
            t = 0;
        }

        delay_ms(10);
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

五、DAC输出正弦波实验

5.1、实验简要

在这里插入图片描述
在STM32F1系列微控制器上通过DAC1通道1输出正弦波,并通过DS100示波器查看波形的实验流程可归纳如下:

  1. 功能描述
    本实验旨在使STM32F1系列MCU上的DAC1通道1(PA4引脚)能按正弦波形规律输出模拟电压信号,并借助DS100示波器观测和验证该正弦波形。

  2. 使用定时器7 TRGO事件触发转换
    将DAC通道1的转换触发源配置为定时器7的TRGO事件触发。在STM32F1的DAC_CR寄存器中,将TEN1位置1启用触发,并设置TSEL1[2:0]位为010,这样每当定时器7的TRGO事件发生时,就会触发DAC通道1的数据更新。

  3. 关闭输出缓冲
    同样地,为保证直接控制DAC输出且可能减小输出阻抗,依然需要关闭DAC通道1的内部输出缓冲。设置DAC_CR寄存器中的BOFF1位为1。

  4. 使用DMA模式
    为了在不占用CPU的情况下高效地更新DAC数据,选用DMA(Direct Memory Access)传输模式。在DAC_CR寄存器中,将DMAEN1位置1,允许DMA访问DAC通道1的数据寄存器,进而自动连续传输正弦波数据序列。

  5. 使用12位右对齐模式
    设置DAC工作于12位右对齐模式,即将每个正弦波采样点作为12位无符号整数,最高有效位在12位位置。正弦波对应的数字量会预先计算好并存储在内存中,然后通过DMA传输到DAC的DHR12R1寄存器。

  6. 正弦波数据生成与传输

    • 计算一串代表正弦波的12位数字量样本,并存储在一个数组中。
    • 配置DMA通道,使其在定时器7触发后从存储正弦波样本的数组地址开始,逐个将数据写入到DAC的DHR12R1寄存器。
    • 启动定时器7,并设置合适的定时周期以匹配期望的正弦波频率。
  7. 连接示波器
    将示波器探头连接至PA4引脚,观察输出的正弦波形,并根据需要调整定时器周期和波形幅度,以获得理想的正弦波形效果。

5.2、DAC输出正弦波实验配置步骤

在这里插入图片描述
在STM32或其他嵌入式系统中,通过DAC输出正弦波的实验配置步骤可以按照以下顺序进行:

  1. 初始化DMA

    • 使用HAL库函数 HAL_DMA_Init() 对将要用于DAC数据传输的DMA通道进行初始化。指定DMA通道的相关参数,例如流向DAC的数据源地址、目标地址(DAC的数据寄存器地址)、传输数据量以及传输模式等。
  2. 连接DMA与DAC句柄

    • 应用HAL库提供的宏函数 __HAL_LINKDMA() 将DMA控制器的句柄与DAC的句柄关联起来。这一步通常是为了解耦硬件资源管理,让HAL库能够自动处理DMA传输与DAC之间的交互。
  3. 初始化DAC

    • 使用 HAL_DAC_Init() 初始化DAC模块,它会对DAC的通用部分进行配置,如时钟使能、复位等基础设置。
  4. DAC的MSP初始化

    • 调用 HAL_DAC_MspInit() 函数实现DAC的微服务层初始化,通常在这个函数里配置与DAC相关的外围设备,比如使能DAC时钟、配置DAC所使用的GPIO引脚模式和速度等。
  5. 配置DAC通道参数

    • 使用 HAL_DAC_ConfigChannel() 函数配置具体DAC通道的各项参数,包括但不限于通道选择、触发源、数据对齐方式、是否开启DMA模式等。
  6. 启动DMA传输

    • 当所有配置完成后,调用 HAL_DMA_Start() 开始DMA传输,使得预计算好的正弦波数据能够按设定的方式自动加载到DAC的数据寄存器中。
  7. 配置定时器溢出频率并启动

    • 使用 HAL_TIM_Base_Init() 初始化定时器,并设置适当的定时器分频系数及预装载寄存器值以达到所需的溢出频率。
    • 调用 HAL_TIM_Base_Start() 开始定时器运行。
  8. 配置定时器触发DAC转换

    • 如果采用定时器事件触发DAC转换,则使用 HAL_TIMEx_MasterConfigSynchronization() 函数配置定时器与DAC之间的同步关系,例如设置定时器的特定事件(如溢出、更新或者特定通道捕获)来触发DAC的转换。
  9. 控制DAC转换与DMA传输的启停

    • 在需要时,可以通过 HAL_DAC_Stop_DMA()HAL_DAC_Start_DMA() 函数来暂停或恢复DAC通道的DMA数据传输。当需要开始输出正弦波时调用 HAL_DAC_Start_DMA();当需要停止输出时,调用 HAL_DAC_Stop_DMA()

以上步骤完成之后,系统会在定时器触发下通过DMA源源不断地将正弦波数字样本送入DAC,从而在DAC输出引脚上产生模拟正弦波形。在实际操作中,还需要确保正弦波的数字样本已经正确计算并存储在适合DMA读取的内存区域中。

5.3、产生正弦波序列函数介绍

在这里插入图片描述
在嵌入式系统中,尤其是STM32等微控制器中,我们可以通过编程计算一组正弦波形的离散数字量,并将这些数字量填入缓冲区,再通过DAC输出。下面是根据给定正弦波函数解析式生成正弦波数字序列的函数示例:

#include <math.h> // 引入数学库以使用sin函数

// 产生正弦波数字缓冲区
void dac_creat_sin_buf(uint16_t maxval, uint16_t samples) {
    float amplitude = maxval - 1; // 最大振幅为maxval - 1,因为实际DAC值的范围通常是从0到maxval
    float frequency = 1.0f / (2.0f * M_PI * samples); // 计算频率,假设采样频率足够高,一个周期内有samples个采样点
    float step = 2 * M_PI / samples; // 计算每个采样点之间的角度差

    uint16_t sin_buf[samples]; // 创建一个存储正弦波形的缓冲区

    for (uint16_t i = 0; i < samples; i++) {
        float angle = step * i; // 计算当前采样点的角度
        sin_buf[i] = (uint16_t)(amplitude * sin(angle) + amplitude); // 计算并存储对应的正弦值,然后加上偏置(这里是振幅的一半,确保正弦波在0-2048范围内)
    }

    // 此处省略将sin_buf缓冲区内容传输给DAC的代码...
}

// 注意:在实际项目中,可能会根据具体DAC的特性(如分辨率、满量程电压等)调整上述计算过程。

这里的函数接受两个参数:

  • maxval:表示DAC所能输出的最大数字量,通常在12位DAC中为4095(11位有效值+符号位),10位DAC中为1023。
  • samples:表示每个正弦波周期内的采样点数量。

函数首先计算出正弦波的振幅、频率和每个采样点间的步进角度,然后遍历采样点数量,根据正弦函数计算出每个点的数字量,并将其存储在缓冲区中。最后,这个缓冲区的内容可以被DMA或CPU直接写入到DAC的数据寄存器中,从而生成连续的正弦波模拟信号。

5.4、编程实战:DAC输出正弦波实验

dac.c

// 引入BSP层DAC驱动头文件
#include "./BSP/DAC/dac.h"

DMA_HandleTypeDef g_dma_dac_handle; // 定义全局DMA控制器句柄,用于DAC传输
DAC_HandleTypeDef g_dac_dma_handle; // 定义全局DAC与DMA交互的句柄

// 外部声明发送数据缓冲区,存放DAC输出的波形数据
extern uint16_t g_dac_sin_buf[4096]; // 缓冲区用于存储正弦波形样本

/* DAC DMA方式输出波形初始化函数 */
void dac_dma_wave_init(void)
{
    // 使能DMA2时钟
    __HAL_RCC_DMA2_CLK_ENABLE();
    
    // 初始化DMA通道配置结构体,这里使用DMA2 Channel3
    g_dma_dac_handle.Instance = DMA2_Channel3;
    g_dma_dac_handle.Init.Direction = DMA_MEMORY_TO_PERIPH; // 内存到外设方向
    g_dma_dac_handle.Init.PeriphInc = DMA_PINC_DISABLE;     // 外设地址不变
    g_dma_dac_handle.Init.MemInc = DMA_MINC_ENABLE;        // 内存地址递增
    g_dma_dac_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 外设数据宽度为半字(16位)
    g_dma_dac_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;   // 内存数据宽度为半字(16位)
    g_dma_dac_handle.Init.Mode = DMA_CIRCULAR;                // 设置为循环模式
    g_dma_dac_handle.Init.Priority = DMA_PRIORITY_MEDIUM;     // 中优先级
    HAL_DMA_Init(&g_dma_dac_handle);                         // 初始化DMA控制器

    // 将DMA句柄关联到DAC句柄的DMA通道上
    __HAL_LINKDMA(&g_dac_dma_handle, DMA_Handle1, g_dma_dac_handle);
    
    // 初始化DAC句柄,这里使用DAC接口
    g_dac_dma_handle.Instance = DAC;
    HAL_DAC_Init(&g_dac_dma_handle);

    // 配置DAC通道1触发源为TIM7 TRGO事件,并关闭输出缓冲
    DAC_ChannelConfTypeDef dac_ch_conf;
    dac_ch_conf.DAC_Trigger = DAC_TRIGGER_T7_TRGO;
    dac_ch_conf.DAC_OutputBuffer = DAC_OUTPUTBUFFER_DISABLE;
    HAL_DAC_ConfigChannel(&g_dac_dma_handle, &dac_ch_conf, DAC_CHANNEL_1);

    // 开始DMA传输,从内存(g_dac_sin_buf)向DAC寄存器传输数据
    HAL_DMA_Start(&g_dma_dac_handle, (uint32_t)g_dac_sin_buf, (uint32_t)&DAC1->DHR12R1, 0);
}

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

        // 使能GPIOA和DAC时钟
        __HAL_RCC_GPIOA_CLK_ENABLE();
        __HAL_RCC_DAC_CLK_ENABLE();

        // 配置GPIOA Pin 4为模拟模式(通常连接到DAC输出)
        gpio_init_struct.Pin = GPIO_PIN_4;
        gpio_init_struct.Mode = GPIO_MODE_ANALOG;
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);
    }
}

/**
 * 函数:启用DAC DMA波形输出
 * 注释:
 *   使用定时器TIM7作为触发源,设定其预分频和自动重装载值后,
 *   DAC输出的波形频率可以通过定时器溢出事件的频率和DMA传输的数据量计算得出。
 *   波形频率 = (TIM7的输入时钟频率 / ((psc + 1) * (arr + 1))) / ndtr;
 * 参数:
 *   ndtr        : DMA通道单次传输数据量
 *   arr         : TIM7的自动重装载值
 *   psc         : TIM7的分频系数
 * 返回值:
 *   无
 */
void dac_dma_wave_enable(uint16_t cndtr, uint16_t arr, uint16_t psc)
{
    TIM_HandleTypeDef tim7_handle = {0}; // 定义TIM7句柄
    TIM_MasterConfigTypeDef tim_mater_config = {0}; // 定义TIM7主配置结构体
    
    // 使能TIM7时钟
    __HAL_RCC_TIM7_CLK_ENABLE();
    
    // 初始化TIM7配置结构体
    tim7_handle.Instance = TIM7;
    tim7_handle.Init.Prescaler = psc; // 设置预分频值
    tim7_handle.Init.Period = arr;    // 设置自动重装载值
    HAL_TIM_Base_Init(&tim7_handle);  // 初始化基本定时器

    // 设置TIM7为主控模式,触发源为UPDATE事件
    tim_mater_config.MasterOutputTrigger = TIM_TRGO_UPDATE;
    tim_mater_config.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
    HAL_TIMEx_MasterConfigSynchronization(&tim7_handle, &tim_mater_config);

    // 启动TIM7
    HAL_TIM_Base_Start(&tim7_handle);

    // 停止DAC DMA并重新开始,设置新的DMA传输数据量和数据对齐方式
    HAL_DAC_Stop_DMA(&g_dac_dma_handle, DAC_CHANNEL_1);
    HAL_DAC_Start_DMA(&g_dac_dma_handle, DAC_CHANNEL_1, (uint32_t *)g_dac_sin_buf, cndtr, DAC_ALIGN_12B_R);
}

该代码段包含两个主要部分,分别用于初始化DAC通过DMA方式输出波形以及启动DAC DMA波形输出。dac_dma_wave_init函数首先初始化DMA控制器,然后初始化DAC并将其与DMA通道关联起来,设置触发源为TIM7的TRGO事件。接下来,在dac_dma_wave_enable函数中,首先初始化并启动定时器TIM7,以便为DAC提供触发信号。最后,根据用户提供的参数调整DMA传输设置,并启动DMA,从而使得DAC能够按照预定的频率和数据流通过DMA从g_dac_sin_buf数组输出波形数据。

dac.h

#ifndef __DAC_H
#define __DAC_H

#include "./SYSTEM/sys/sys.h"


void dac_dma_wave_init(void);											/* DAC DMA方式输出波形初始化函数 */
void dac_dma_wave_enable(uint16_t cndtr, uint16_t arr, uint16_t psc);	/* 启用DAC DMA波形输出 */

#endif

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/ADC/adc.h"
#include "./BSP/DAC/dac.h"
#include "./BSP/KEY/key.h"
#include "math.h"


uint16_t g_dac_sin_buf[4096];            /* 发送数据缓冲区 */

/**
 * @brief       产生正弦波序列函数
 *   @note      需保证: maxval > samples/2
 * @param       maxval : 最大值(0 < maxval < 2048)
 * @param       samples: 采样点的个数
 * @retval      无
 */
void dac_creat_sin_buf(uint16_t maxval, uint16_t samples)
{
    uint8_t i;
    float outdata = 0;                     /* 存放计算后的数字量 */
    float inc = (2 * 3.1415962) / samples; /* 计算相邻两个点的x轴间隔 */

    if(maxval <= (samples / 2))return ;	   /* 数据不合法 */

    for (i = 0; i < samples; i++)
    {
        /* 
         * 正弦波函数解析式:y = Asin(ωx + φ)+ b
         * 计算每个点的y值,将峰值放大maxval倍,并将曲线向上偏移maxval到正数区域
         * 注意:DAC无法输出负电压,所以需要将曲线向上偏移一个峰值的量,让整个曲线都落在正数区域
         */
        outdata = maxval * sin(inc * i) + maxval;
        if (outdata > 4095)
            outdata = 4095; /* 上限限定 */
        //printf("%f\r\n",outdata);
        g_dac_sin_buf[i] = outdata;
    }
}

int main(void)
{
    uint8_t t = 0;
    uint8_t key;
    
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    usart_init(115200);                 /* 串口初始化为115200 */
    led_init();                         /* 初始化LED */
    lcd_init();                         /* 初始化LCD */
    key_init();                         /* 初始化按键 */

    dac_dma_wave_init();				/* DAC DMA方式输出波形初始化函数 */
    
    lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30,  70, 200, 16, 16, "DAC DMA Sine WAVE TEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY0:3Khz  KEY1:30Khz", RED);
    
    dac_creat_sin_buf(2048, 100);
    dac_dma_wave_enable(100, 10 - 1, 72 - 1);  /* 100Khz触发频率, 100个点, 得到1Khz的正弦波 */
    
    while (1)
    {
        t++;
        key = key_scan(0);                                  /* 按键扫描 */

        if (key == KEY0_PRES)                               /* 高采样率 */
        {
            dac_creat_sin_buf(2048, 100);
            dac_dma_wave_enable(100, 10 - 1, 24 - 1);       /* 300Khz触发频率, 100个点, 得到最高3KHz的正弦波. */
        }
        else if (key == KEY1_PRES)                          /* 低采样率 */
        {
            dac_creat_sin_buf(2048, 10);
            dac_dma_wave_enable(10, 10 - 1, 24 - 1);        /* 300Khz触发频率, 10个点, 可以得到最高30KHz的正弦波. */
        }

        if (t == 40)        /* 定时时间到了 */
        {
            LED0_TOGGLE();  /* LED0闪烁 */
            t = 0;
        }

        delay_ms(5);
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、PWM DAC实验

6.1、 PWM DAC应用背景

在这里插入图片描述

6.2、 PWM DAC技术实现原理

6.2.1、什么是PWM DAC技术?

在这里插入图片描述
PWM(Pulse Width Modulation,脉冲宽度调制)DAC技术是一种利用PWM信号模拟输出连续变化电压或电流的技术。在PWM中,信号是一个周期固定的方波,其特点是占空比(高电平持续时间相对于整个周期的比例)是可以调节的。对于一个PWM信号来说,理论上它可以被看作是由一个不变的直流分量和一系列不同频率的交流分量组成,其中直流分量对应于PWM信号的平均值,而其他交流分量则随着占空比的变化而包含不同的谐波成分。

在PWM DAC应用中,通过改变PWM信号的占空比来模拟不同的电压等级。由于PWM本身是数字信号,要将其转换为模拟信号,就需要通过一个低通滤波器来滤除高频的PWM方波分量,保留并平滑其直流分量。这样一来,随着PWM占空比的改变,经过滤波后的输出信号就变成了一个连续变化且与占空比成比例的模拟电压或电流信号。

因此,PWM DAC是一种经济高效的数模转换方案,尤其适用于嵌入式系统和其他对成本、功耗或集成度有较高要求的应用场合,但由于受到滤波器性能的限制,其输出模拟信号的质量(如分辨率、线性度、噪声和失真)相较于专用的高精度DAC可能较低。为了提高PWM DAC的输出精度,可以通过增加PWM位数(提高分辨率)、优化滤波器设计以及采用多个PWM通道并行合成更高级别的DAC等方式来改善性能。

6.2.2、用分段函数表示PWM波

在这里插入图片描述
在表达式①中,f(t)是一个分段函数,用于表示PWM(脉冲宽度调制)波形。这里,

  • V_H 表示PWM波形的高电平电压(通常为电源电压)。
  • V_L 表示PWM波形的低电平电压(通常为0伏或接地)。
  • k 是自然数,表示PWM周期的计数次数。
  • N 是定时器的自动重载值(ARR),决定了PWM的周期(T=PWM_period = N * Tclk,其中Tclk是定时器的时钟周期)。
  • n 是比较寄存器的值(CCRx),它决定了PWM信号在一个周期内的高电平持续时间(即占空比)。
  • T 是PWM的单个周期时间长度。
  • p 是占空比,其计算公式为 p = n / N。

分段函数f(t)的解释是:当时间t处于第k个周期内,并且满足kNT ≤ t < kNT + nT时,PWM波形输出高电平V_H;当时间t处于第k个周期内,且满足kNT + nT ≤ t ≤ kNT + NT时,PWM波形输出低电平V_L

在定时器的PWM模式下,定时器会根据ARR(自动重载寄存器)的值来设定PWM的周期,而通过设置CCRx(比较寄存器)的值来调整PWM波形在一个周期内的高电平持续时间,即占空比。当定时器计数达到CCR的值时,PWM输出翻转,从而实现占空比的控制。
在这里插入图片描述
在STM32等微控制器中,定时器可以通过配置为PWM模式来输出脉宽调制(PWM)信号。假设采用递增计数模式,PWM的生成原理如下:

  1. 自动重装载寄存器ARR(Auto-Reload Register):ARR寄存器决定了定时器的计数周期,也就是PWM波形的周期。当定时器的计数器(CNT)从0开始递增计数,当CNT达到ARR的值时,计数器将会自动重装载为0并重新开始计数。

  2. 捕获/比较寄存器CCRx(Capture/Compare Register x):在PWM模式下,CCRx用于设定在每个计数周期内的某个时刻切换输出极性的阈值。具体而言,当定时器的CNT计数值小于CCRx的值时,PWM输出引脚会被设置为低电平(0);当CNT计数值等于或大于CCRx的值时,PWM输出引脚会被设置为高电平(1)。

  3. PWM波形参数

    • 周期(Period)或频率:PWM信号的周期由ARR的值决定。ARR越大,PWM信号的周期越长,频率越低。
    • 占空比(Duty Cycle):PWM信号的占空比由CCRx的值相对于ARR的值决定。CCRx值较小则占空比低(高电平时间短),CCRx值较大则占空比高(高电平时间长)。

总结来说,在递增计数模式下,定时器通过比较CNT与CCRx的关系来控制PWM输出端口的状态,从而实现了可调的PWM波形输出。通过配置ARR和CCRx的不同值,可以灵活地调整PWM信号的周期和占空比。

6.2.3、将PWM波分段函数进行傅里叶级数展开

在这里插入图片描述
脉冲宽度调制(PWM)信号的数学表达式。PWM是一种数字模拟转换技术,通过改变占空比来控制输出电压的平均值。

傅里叶级数展开,这是将一个周期函数表示为一系列正弦和余弦函数的和的过程。对于PWM信号,我们可以使用傅里叶级数将其展开为直流分量、基波分量以及高次谐波分量之和。

根据公式,我们可以看到PWM信号的傅里叶级数展开形式。这个展开式由直流分量、基波分量和高次谐波分量组成。

直流分量:这是PWM信号的平均值,它与占空比有关。
基波分量:这是PWM信号的基本频率成分,通常是最主要的成分。
高次谐波分量:这些是PWM信号中包含的更高频率成分,它们是由PWM信号的非线性特性产生的。

为了从PWM信号中提取出期望的模拟电压,我们需要通过低通滤波器来滤除高次谐波分量,只保留直流分量。这样,我们就可以得到一个与PWM信号的占空比成比例的模拟电压。

在公式④中,f(t)表示输出电压, n / N n/N n/N 表示占空比, V H V_H VH表示PWM信号的最大电压。这个公式表明,输出电压与占空比成线性关系,占空比越大,输出电压越高。

总结一下,PWM信号可以通过傅里叶级数展开为直流分量、基波分量和高次谐波分料。通过低通滤波器可以滤除高次谐波分量,从而得到与占空比成比例的模拟电压。

6.2.4、PWM DAC的分辨率

在这里插入图片描述
PWM(Pulse Width Modulation)DAC是一种基于脉宽调制技术模拟数字转换的方法。通过控制PWM信号的占空比(即高电平时间与整个周期的比例)来模拟不同的电压等级。对于PWM DAC,其分辨率是指能够输出的不同电压级别数量,它直接影响模拟输出的精度。

公式④描述了PWM DAC的输出电压与PWM信号占空比的关系,其中f(t)代表某一瞬间的输出电压,n是在一个周期内高电平持续的时间(以计数单位计),N是整个PWM信号的周期被划分为的等分段数,V_H则是PWM信号的高电平电压。

公式⑤直接给出了PWM DAC的分辨率计算方式,它是以2为底的对数形式,意味着分辨率是PWM周期被分成多少个可独立控制的等份,每增加1位分辨率,可控制的电压等级就翻倍。

例如:

  • N = 256时,分辨率是8位,意味着可以产生 2 8 = 256 2^8 = 256 28=256 种不同的电压等级。
  • STM32使用16位或32位定时器作为PWM源时,理论上可以实现高达16位或32位的分辨率,但这并不意味着实际应用中会一直使用最高的分辨率,因为分辨率越高,意味着每个电压级别的间隔更小,需要更低的PWM频率(更大的周期)和更精细的滤波处理,以确保经过低通滤波后能够准确还原出模拟信号。此外,过高的分辨率可能导致实际应用中的速度限制问题。因此,在实际设计中会根据系统需求和资源限制选择合适的分辨率和PWM频率。

6.2.5、8位分辨率下对RC滤波器的设计要求

在这里插入图片描述
在8位分辨率的PWM DAC系统中,为了确保输出模拟信号的精度,确实需要精心设计RC滤波器来滤除PWM波形中的高频成分,特别是1次谐波。以下是关于RC滤波器设计的一些关键点:

  1. 精度要求

    • 为了保证输出电压的精度,要求1次谐波对输出电压的影响不超过1位的精度,即1 LSB(Least Significant Bit)。在3.3V供电的8位系统中,1 LSB相当于 3.3 V / 256 = 0.01289 V 3.3V / 256 = 0.01289V 3.3V/256=0.01289V
  2. 1次谐波最大值

    • 对于最大占空比为50%的PWM信号,1次谐波的峰值为半个周期内的电压变化,即 V H / 2 = 3.3 V / 2 = 1.65 V V_H / 2 = 3.3V / 2 = 1.65V VH/2=3.3V/2=1.65V。而在理想情况下,考虑正弦波形的幅度,1次谐波的最大值约为 2 ∗ V H / π = 2 ∗ 3.3 V / π ≈ 2.1 V 2 * V_H / π = 2 * 3.3V / π ≈ 2.1V 2VH/π=23.3V/π2.1V
  3. RC滤波电路要求

    • 要求RC滤波器提供足够的衰减,使得1次谐波在输出端的幅度低于1 LSB。根据给出的1次谐波最大值2.1V和1 LSB的值0.01289V,衰减率计算为 − 20 l g ( 2.1 V / 0.01289 V ) ≈ − 44 d B -20 lg(2.1V / 0.01289V) ≈ -44 dB 20lg(2.1V/0.01289V)44dB
  4. 截止频率要求

    • 设定PWM的计数频率为72 MHz,分辨率为8位时,PWM的实际频率为 72 M H z / 256 = 281.25 k H z 72 MHz / 256 = 281.25 kHz 72MHz/256=281.25kHz
    • 对于1阶RC低通滤波器,为了滤除大部分1次谐波,截止频率通常会选择远低于PWM频率的值。在这里,建议的截止频率为1.77 kHz,意味着该滤波器在1.77 kHz以下的频率范围内具有较好的衰减效果。
    • 对于2阶RC滤波器,由于其更高的滚降速率,可以选择相对较高的截止频率,这里提出的截止频率为22.34 kHz。这意味着2阶RC滤波器能在保持较好滤波效果的同时,提高系统的响应速度。

实际设计中,应根据具体应用的需求和条件,综合考虑滤波器阶数、截止频率以及带宽等因素,选择合适的RC值来进行滤波器设计。同时,要注意实际RC滤波器并非理想元件,其频率响应会受到元件参数偏差和温度漂移等因素的影响。

6.2.6、PWM DAC二阶低通滤波器原理图

在这里插入图片描述
PWM DAC的二阶低通滤波器设计原理是为了平滑PWM波形,将其转换为更为平滑的模拟信号。二阶RC滤波器相比一阶RC滤波器,具有更快的滚降率和更好的抑制高频噪声的能力,这对于提高PWM DAC的输出精度非常有利。

二阶RC滤波器通常采用Sallen-Key结构,其截止频率计算公式为 f c = 1 / ( 2 ∗ π ∗ √ ( R 1 ∗ R 2 ) ∗ C ) f_c = 1 / (2 * π * √(R1 * R2) * C) fc=1/(2π(R1R2)C),其中R1、R2为电阻,C为电容。在某些设计中,为了简化设计和实现相等的Q值,可以令 R 1 ∗ C = R 2 ∗ C R1 * C = R2 * C R1C=R2C,即 R 1 ∗ R 2 = ( R 54 ∗ C 63 ) = ( R 55 ∗ C 64 ) = R C R1 * R2 = (R54 * C63) = (R55 * C64) = RC R1R2=(R54C63)=(R55C64)=RC,这时截止频率简化为 f c = 1 / ( 2 ∗ π ∗ R C ) f_c = 1 / (2 * π * RC) fc=1/(2πRC)

在实际应用中,考虑到不仅要过滤掉PWM波形中的高频成分,还要确保音频信号(如22.05kHz)能够顺利通过滤波器,因此将二阶RC滤波器的截止频率设计得稍高于音频信号带宽。实测结果显示,该滤波器在保证音频信号通过的同时,还能将PWM DAC的输出精度控制在0.5LSB以内,体现了良好的设计效果。这样既能满足音频输出的需求,也能确保PWM DAC的模拟输出具有较高的精度。

6.3、编程实战: PWM DAC实验

在这里插入图片描述
PWM DAC实验的主要功能流程如下:

  1. 配置定时器1通道1(PA8)输出PWM信号

    • 首先,初始化定时器1并配置其工作模式为PWM模式。
    • 设置定时器的自动重载寄存器ARR和捕获/比较寄存器CCR1,以设定PWM信号的周期和占空比,从而控制输出电压的大小。
    • 根据实验要求,定时器产生的PWM信号将通过PA8引脚输出。
  2. 设计并连接二阶RC滤波器

    • 根据前面讨论的原理,设计一个截止频率为33.88kHz的二阶RC滤波器,以平滑PWM信号,将其转换为近似模拟电压。
    • 将定时器1的PWM输出端PA8连接到二阶RC滤波器的输入端。
  3. ADC1通道1(PA1)采集滤波后的电压

    • 初始化ADC1,并配置通道1用于采集PA1引脚上的电压信号。
    • 根据ADC的参数设置合适的采样时间、分辨率等,并确保ADC的参考电压已正确配置。
  4. 读取ADC转换结果并显示

    • 通过调用ADC读取函数,获取通道1转换得到的数字量。
    • 将采集到的数字量转换为对应的电压值,通常通过以下公式计算:Voltage = ADC_Result * (Vref / (2^n)),其中n是ADC的分辨率(例如8位、10位或12位),Vref是ADC的参考电压。
    • 最后,将转换得到的电压值在显示屏或其他输出设备上显示出来。

通过以上步骤,实验实现了通过定时器输出PWM信号,经过滤波转化为模拟电压,并通过ADC采集转换为数字量,最终显示为电压值的过程,从而验证了PWM DAC的功能和性能。

pwmdac.c

// 引入自定义的PWM和DAC混合控制头文件
#include "./BSP/PWMDAC/pwmdac.h"

// 定义全局PWM-DAC定时器通道句柄
TIM_HandleTypeDef g_timx_pwm_chy_handle;

// PWM DAC初始化函数
// 该函数用于初始化TIM1定时器,使其产生PWM信号用于控制DAC输出电压
void pwmdac_init(uint16_t arr, uint16_t psc)
{
    // 初始化定时器通道PWM配置结构体
    TIM_OC_InitTypeDef timx_oc_pwm_chy = {0};

    // 设置定时器实例为TIM1
    g_timx_pwm_chy_handle.Instance = TIM1;

    // 配置TIM1的基本参数,包括预分频psc和自动重载值arr
    g_timx_pwm_chy_handle.Init.Prescaler = psc;
    g_timx_pwm_chy_handle.Init.Period = arr;
    g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数模式
    g_timx_pwm_chy_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // 自动重装载使能

    // 初始化TIM1定时器PWM模式
    HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle);

    // 配置TIM1通道1为PWM模式1
    timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1;
    timx_oc_pwm_chy.Pulse = 0; // 初始脉宽设置为0
    timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH; // PWM极性为高电平有效
    HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, &timx_oc_pwm_chy, TIM_CHANNEL_1); // 配置TIM1通道1

    // 启动TIM1通道1的PWM输出
    HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, TIM_CHANNEL_1);
}

// TIM MSP初始化函数
// 该函数主要用于初始化TIM1相关的GPIO,使其可以与TIM1通道相配合工作
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM1)
    {
        GPIO_InitTypeDef gpio_init_struct;

        // 使能GPIOA和TIM1时钟
        __HAL_RCC_GPIOA_CLK_ENABLE();
        __HAL_RCC_TIM1_CLK_ENABLE();

        // 配置TIM1通道1对应的GPIOA Pin 8
        gpio_init_struct.Pin = GPIO_PIN_8;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP; // 设置为推挽复用输出模式
        gpio_init_struct.Pull = GPIO_PULLUP; // 上拉使能,防止悬空
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速输出
        HAL_GPIO_Init(GPIOA, &gpio_init_struct); // 初始化GPIOA Pin 8
    }
}

// 设置PWM DAC输出电压函数
// 该函数接收一个电压值vol(单位毫伏),并根据该值调整TIM1通道1的PWM占空比以改变DAC输出电压
void pwmdac_set_voltage(uint16_t vol)
{
    // 将输入电压值从毫伏转换为与TIM1比较寄存器匹配的比例值
    float temp = vol;
    temp /= 1000; // 单位转换为伏特
    temp = temp * 256 / 3.3; // 将电压值映射到0-256区间,假设DAC参考电压为3.3V

    // 设置TIM1通道1的比较寄存器(即PWM占空比)
    __HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, TIM_CHANNEL_1, temp);
}

以上代码片段实现了使用TIM1定时器产生PWM信号,进而控制DAC输出电压的功能。首先通过pwmdac_init函数初始化TIM1定时器,并配置其PWM模式;然后在HAL_TIM_PWM_MspInit函数中初始化与TIM1相连的GPIO;最后通过pwmdac_set_voltage函数根据输入电压值动态调整TIM1的PWM占空比,从而改变DAC的输出电压。这里的DAC通过PWM信号的占空比变化间接调节输出电压。

pwmdac.h

#ifndef __PWMDAC_H
#define __PWMDAC_H

#include "./SYSTEM/sys/sys.h"


void pwmdac_init(uint16_t arr, uint16_t psc);	// PWM DAC初始化函数
void pwmdac_set_voltage(uint16_t vol);			// 设置PWM DAC输出电压函数

#endif

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/ADC/adc.h"
#include "./BSP/PWMDAC/pwmdac.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 */
    led_init();                         /* 初始化LED */
    lcd_init();                         /* 初始化LCD */
    adc_init();                         /* 初始化ADC */
    pwmdac_init(256 - 1, 0);			// PWM DAC初始化函数
    
    pwmdac_set_voltage(2800);			// 设置PWM DAC输出电压函数
    
    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_VOL:0.000V", BLUE); /* 先在固定位置显示小数点 */

    while (1)
    {
        adcx = adc_get_result();
 
        temp = (float)adcx * (3.3 / 4096);              /* 获取计算后的带小数的实际电压值,比如3.1111 */
        adcx = temp;                                    /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
        lcd_show_xnum(134, 110, 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, 110, temp, 3, 16, 0X80, BLUE);/* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */

        LED0_TOGGLE();
        delay_ms(100);
    }
}

在这里插入图片描述

七、总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 26
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咖喱年糕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值