对于高职教师来说,必不可少的一个任务就是参加企业实践。这个暑假,本人也没闲着,报名参加了上海市电子信息类教师企业实践。7月8日到13日,有幸来到美丽的泉城济南,远离了上海的酷暑,走进了百科荣创科技发展有限公司。在这短短的一周时间里,我结合自己的教学经验和企业的需求,对一个简易函数信号发生器的嵌入式项目做了教学内容的转换。接下来,就用博客的形式,把这次转换后的成果分享出来,如果您觉得有用,还望多多点赞和转发!

我在百科荣创企业实践——简易函数信号发生器(2)-LMLPHP

我在百科荣创企业实践——简易函数信号发生器(2)-LMLPHP

        针对本项目,笔者根据自己的理解,力求做到循序渐进和逐步深入,计划用6篇文章来展开,本文是第二篇,我们先来聊聊STM32的DAC,并做一个基本输出的实验。

二、STM32的DAC

2.1 DAC简介

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

2.2 STM32的DAC功能概览

        STM32具有片上DAC外设,它的分辨率可配置为8位或12位的数字输入信号,具有两个DAC输出通道,这两个通道互不影响,每个通道都可以使用DMA功能,都具有出错检测能力,可外部触发。图4是STM32的DAC功能框图。

 

        整个DAC模块围绕框图下方的“数字至模拟转换器x”展开,它的左边分别是参考电源的引脚:VDDA、VSSA及VREF+,其中STM32的DAC规定了它的参考电压输入范围为2.4~3.3V。“数字至模拟转换器x”的输入为DAC的数据寄存器“DORx”的数字编码,经过它转换得的模拟信号由图中右侧的“DAC_OUTx”输出。而数据寄存器“DORx”又受“控制逻辑”支配,它可以控制数据寄存器加入一些伪噪声信号或配置产生三角波信号。

        图中的左上角为DAC的触发源,DAC根据触发源的信号来进行DAC转换,其作用就相当于DAC转换器的开关,它可以配置的触发源为外部中断源触发、定时器触发或软件控制触发。如本项目实验中需要控制三角波和正弦波的频率,就需要定时器定时触发DAC进行数据转换。

2.3 参考电压

        与ADC外设类似,DAC也使用VREF+ 引脚作为参考电压,在设计原理图的时候一般把VSSA 接地,把VREF+和VDDA接3.3V,可得到DAC的输出电压范围为:0~3.3V。如果想让输出的电压范围变宽,可以在外部加一个电压调理电路,把0~3.3V的DAC输出抬升到特定的范围即可。

2.4 输出通道

        框图中的“数字至模拟转换器x”是核心部件,整个DAC外设都围绕它而展开。它以左边的VREF+作为参考电源,以DAC的数据寄存器“DORx”的数字编码作为输入,经过它转换得的模拟信号由右侧的“DAC_OUTx”通道输出。其中各个部件中的“x”是指设备的标号,在STM32中具有2个这样的DAC部件,每个DAC有1个对应的输出通道连接到特定的引脚(PA4-通道1,PA5-通道2)为避免干扰,使用DAC功能时,DAC通道引脚需要被配置成模拟输入功能(AIN)。

2.5 触发源及DHRx寄存器

        在使用DAC时,不能直接对上述DORx寄存器写入数据,任何输出到DAC通道x的数据都必须写入到DHRx寄存器中(其中包含DHR8Rx、DHR12Lx等,根据数据对齐方向和分辨率的情况写入到对应的寄存器中)。

        数据被写入到DHRx寄存器后,DAC会根据触发配置进行处理,若使用硬件触发,则DHRx中的数据会在3个APB1时钟周期后传输至DORx,DORx随之输出相应的模拟电压到输出通道;若DAC设置为外部事件触发,可以使用定时器(TIMx_TRGO)、EXTI_9信号或软件触发(SWTRIGx)这几种方式控制数据DAC转换的时机,例如使用定时器触发,配合不同时刻的DHRx数据,可实现DAC输出三角波和正弦波的功能。

2.6 DAC初始化结构体详解

        在ST的标准库中,把控制DAC相关的各种配置封装到了结构体 DAC_InitTypeDef 中,该结构体的主要成员见代码清单1。

//---------------------------------------------
// 代码清单1:DAC_InitTypeDef结构体
//---------------------------------------------

typedef struct {
    //DAC触发方式
    uint32_t DAC_Trigger;
    
    //是否自动输出噪声或三角波
    uint32_t DAC_WaveGeneration;
    
    //选择噪声生成器的低通滤波或三角波的幅值
    uint32_t DAC_LFSRUnmask_TriangleAmplitude;
    
    //选择是否使能输出缓冲器
    uint32_t DAC_OutputBuffer;                                         
} DAC_InitTypeDef;

(1)DAC_Trigger

        本成员用于配置DAC的触发模式,当DAC产生相应的触发事件时,才会把DHRx寄存器的值转移到DORx寄存器中进行转换。本结构体成员可以选择的触发模式如下:

  • 硬件触发模式(DAC_Trigger_None),DHRx寄存器内的数据会在3个APB1时钟周期内自动转换至DORx进行转换;
  • 定时器触发模式(DAC_Trigger_T2/4/5/6/7_TRGO),使用定时器2、4、5、6、7控制DHRx寄存器的数据按时间转移到DORx中进行转换,利用这种方式可以输出特定的波形;
  • EXTI_9 触发方式(DAC_Trigger_Ext_IT9),当产生EXTI_9 事件时(如GPIO中断事件),触发转换;
  • 软件触发模式(DAC_Trigger_Software),在本模式下,向DAC_SWTRIGR寄存器写入配置即可触发信号进行转换。

(2)DAC_WaveGeneration

        本成员用于设置是否使用DAC输出伪噪声或三角波:

  • DAC_WaveGeneration_None/Noise/Triangle:使用伪噪声和三角波输出时,DAC都会把LFSR寄存器的值叠加到DHRx数值上,产生伪噪声和三角波,若希望产生自定义的输出时,直接配置为DAC_WaveGeneration_None即可。

(3)DAC_LFSRUnmask_TriangleAmplitude

  • 当使用伪噪声或三角波输出时要叠加到 DHRx 的值,非噪声或三角波输出模式下,本配置无效。
  • 使用伪噪声输出时,把本结构体成员赋值为DAC_LFSRUnmask_Bit0~DAC_LFSRUnmask_Bit11_0 等宏即可;
  • 使用三角波输出时,本结构体设置三角波的最大幅值,可选择为DAC_TriangleAmplitude_1~DAC_TriangleAmplitude_4096 等宏,如图5所示,DAC在DHRx值的基础上升,达到最大幅度时下降,形成三角波的输出。

(4)DAC_OutputBuffer

        本结构体成员用于控制是否使能 DAC 的输出缓冲(
DAC_OutputBuffer_Enable/Disable),使能了 DAC 的输出缓冲后可以减小输出阻抗,适合直接驱动一些外部负载。

2.7 与DAC相关的库函数

        代码清单2里是几个与DAC有关的库函数,下面简要介绍一下这几个函数:

  • DAC_Cmd() 函数用来使能DAC转换通道;
  • DAC开始工作后,我们就可以使用 DAC_SetChannel1Data() 函数在DAC的输出通道1(PA4引脚)得到不同的电压值了。第一个参数设置对齐方式,可以为12位右对齐 DAC_Align_12b_R、12位左对齐 DAC_Align_12b_L 以及8位右对齐 DAC_Align_8b_R 方式。第二个参数就是DAC的输入值了,这个很好理解,初始可以设置为0。
  • DAC_GetDataOutputValue() 函数看名字也很好理解,是获取DAC指定通道上的值。注意,读到的值是最近一次转换的值。
//--------------------------------------
// 代码清单2:几个常用的DAC库函数
//--------------------------------------

//使能DAC通道1
DAC_Cmd(DAC_Channel_1, ENABLE);

//12位右对齐数据格式设置DAC值
DAC_SetChannel1Data(DAC_Align_12b_R, 0);

//读出DAC通道1最后一次转换的数值
DAC_GetDataOutputValue(DAC_Channel_1);

三、DAC基本输出

        我们先来做一个DAC的基本输出实验,使用DAC通道1输出模拟电压,然后通过万用表对该输出电压进行测量,看是否符合我们设定的值。此外,还可以通过按键对该输出电压进行设置。

3.1 工程文件清单

        如图6所示,在按键的基础工程上新增针对DAC的驱动文件 dac.c 和 dac.h,在 main.c 文件里编写主控制逻辑。

3.2 工程代码剖析

(1)dac.h源码剖析

        该文件源码见代码清单3,主要是DAC初始化和功能函数的声明,每个函数的功能和参数将在剖析 dac.c 源码时解读。

//---------------------------------------------------
// 代码清单3:dac.h
//---------------------------------------------------

#ifndef _DAC_H_
#define _DAC_H_

#include "sys.h"	     			    
	
//---------------------------------------------------
// 函数声明
//---------------------------------------------------
void DAC1_Init(void);				//DAC通道1初始化	 	 
void DAC1_Set_Vol(uint16_t vol);	//设置DAC通道1输出电压

#endif

(2)dac.c源码剖析

        该文件就是所有DAC驱动函数的定义,如代码清单4所示。

/**
 ************************************************************************
 * 代码清单4:dac.c
 * 描    述:DAC初始化、驱动
 * 平    台:百科荣创STM32F407核心板
 * 作    者:老耿
 * 日    期:2024-7-10
 * 固 件 库:ST3.5.0
 * 版    本:V1.0
 * 说    明:
 * 修改记录:无
 ************************************************************************
**/

//必要的头文件
#include "dac.h" 	

/**
 ************************************************************************
 * 函 数 名:DAC1_Init
 * 功    能:DAC通道1的配置
 * 入口参数:无
 * 出口参数:无
 * 说    明:DAC通道1对应PA4引脚,注意要配置成模拟输入
 ************************************************************************
**/
void DAC1_Init(void)
{  
	//定义必要的初始化结构体
	GPIO_InitTypeDef  gpio_initstruct;
	DAC_InitTypeDef   dac_initstruct;
	
	//打开GPIOA和DAC的外设时钟
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
	   
	//PA4初始化为模拟输入
	gpio_initstruct.GPIO_Pin = GPIO_Pin_4;
	gpio_initstruct.GPIO_Mode = GPIO_Mode_AN;
	gpio_initstruct.GPIO_PuPd = GPIO_PuPd_DOWN;
	GPIO_Init(GPIOA, &gpio_initstruct);

	//不使用触发功能
	dac_initstruct.DAC_Trigger = DAC_Trigger_None;
	//不使用波形发生
	dac_initstruct.DAC_WaveGeneration = DAC_WaveGeneration_None;
	//屏蔽、幅值设置
	dac_initstruct.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0;
	//DAC1输出缓存关闭
	dac_initstruct.DAC_OutputBuffer = DAC_OutputBuffer_Disable;
	//初始化DAC通道1
	DAC_Init(DAC_Channel_1,&dac_initstruct);

	//使能DAC通道1
	DAC_Cmd(DAC_Channel_1, ENABLE);
	//12位右对齐数据格式设置DAC值
	DAC_SetChannel1Data(DAC_Align_12b_R, 0);
}

/**
 ************************************************************************
 * 函 数 名:DAC1_Set_Vol
 * 功    能:设置通道1输出电压
 * 入口参数:vol --- 0~3300,代表0~3.3V
 * 出口参数:无
 * 说    明:无
 ************************************************************************
**/
void DAC1_Set_Vol(uint16_t vol)
{
	double temp = vol;
	temp /= 1000;
	temp = temp*4096/3.3;
	DAC_SetChannel1Data(DAC_Align_12b_R, temp);//12位右对齐数据格式设置DAC值
}

(3)main.c源码剖析

        主程序比较简单,如代码清单5所示,在完成必要的初始化之后,检测到S4按下,就累加DAC的值,检测到S3按下,就递减DAC的值。

/******************************************************
 * 代码清单5:main.c
 * 项    目:DAC基本输出
 * 任务描述:使用DAC通道1输出模拟电压,然后通过万用表测量
 *			 该输出电压进行测量,看是否符合我们设定的值。
 *			 此外,还可以通过按键对该输出电压进行设置。
 * 实验平台:百科荣创STM32F407开发板
 * 作    者:老耿
 * 日    期:2024/07/10
 ******************************************************/

//------------------------------------------------------
// 必要的头文件
//------------------------------------------------------
#include "sys.h"
#include "delay.h"
#include "dac.h"
#include "key.h"

//------------------------------------------------------
// 主函数
//------------------------------------------------------
int main(void)
{	 
	uint16_t dacval = 0;    //存放DAC值
	uint8_t  key;           //存放键值
	
	//设置系统中断优先级分组2
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	Delay_Init();	//延时初始化
	Key_Init();		//按键初始化
	DAC1_Init();	//DAC通道1初始化
 	
	//设置DAC初始值为0
	DAC_SetChannel1Data(DAC_Align_12b_R, dacval);	
	
	while(1)
	{	
		key = Key_Scan(0);		//检测按下的键
		
		if(key == 4)			//如果按下S4
		{						//DAC值累加200,约0.16V
			if(dacval < 4000)
				dacval += 200;
			DAC_SetChannel1Data(DAC_Align_12b_R, dacval);
		}
		else if(key == 3)		//如果按下S3
		{						//DAC值递减200
			if(dacval > 200)
				dacval -= 200;
			else
				dacval = 0;
			DAC_SetChannel1Data(DAC_Align_12b_R, dacval);
		}	 
	}	
}

3.3 验证与测试

        将万用表按照图7所示去测量DAC的输出电压,每单击一次S4按键,DAC值增加200,换算成电压值即:200*3.3/4096≈0.161V。同理,每单击一次S3按键,DAC值减少200。具体效果见文末的演示视频。

简易信号发生器项目的前奏,先搞定DAC基本输出

(第二部分完,共六部分)

07-14 17:04