搜索
bottom↓
回复: 0

《STM32MP1 M4裸机CubeIDE开发指南 V1.5.2》第二十四章 DAC实验

[复制链接]

出0入234汤圆

发表于 2022-8-9 11:19:56 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2022-8-9 12:50 编辑

1)实验平台:正点原子STM32MP157开发板
2) 章节摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南 V1.5.2
3)购买链接:https://detail.tmall.com/item.htm?id=631745687288
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/arm-linux/zdyzmp157.html1.0.0 文档
5)正点原子官方B站:https://space.bilibili.com/394620890
6)STM32MP157技术交流QQ群:703341432 lQLPJxaFi2zaB4UWWrDAMgIsFEW2pwLb3abnwDMA_90_22.png
lQDPJxaFi2nfFizMjM0CbLCPlxn_FVheIQLb3aGrwFQA_620_140.jpg

lQLPJxaFi2nfFhLMkM0BXrDNvOUyeU_FPgLb3aGvQNIA_350_144.png
第二十四章 DAC实验
前面章节我们学习了ADC实验,本章,我们将介绍STM32MP157的DAC(Digital -to- analog converters,数模转换器)功能。我们将通过三个实验来学习DAC,分别是DAC输出实验、DAC输出三角波实验和DAC输出正弦波实验。
本章分为如下几个小节:
24.1 、DAC简介;
24.2 、DAC输出实验;
24.3 、DAC输出三角波实验;
24.4 、DAC输出正弦波实验;
24.1 DAC简介

        数字/模拟转换器(Digital-to-Analog Converter),是指将离散的数字信号转换为连续变量的模拟信号的器件,简称DAC。在模拟电路中,电流电压信号的变化是连续的,而数字电路处理的数据都是离散的数据,DAC将表示一定比例电压值的数字信号转换为模拟信号,这使得主控芯片不再只是输出数字0或者1,从而得到更广泛应用。
(1)分辨率
        DAC的分辨率定义为输出满刻度电压与2n的比值,其中n为DAC的位数,所以DAC的位数也可以用来表示分辨率,例如某个DAC的分辨率是12bit、10bit。
        DAC的分辨率反应了输出模拟电压的最小变化值,以一个12位的DAC为例,其数据变化范围是0~4096,对于3.3V的满量程,采用8位的DAC时,分辨率为3.3V/256=12.89mV,当采用12位的DAC时,分辨率则为3.3V/4096=0.81mV。显然,位数越多,其输出电压的取值个数就越多(2n 个),也就越能反映出输出电压的细微变化,分辨能力就越高。
(2)建立时间
        建立时间是描述DAC转换速度快慢的参数,定义为从输入数字量变化到输出(DAC_OUT)达到终值误差值±1LSB时所需的时间。从参考手册上可以找到STM32MP157的建立时间tSETTLING的典型值是2us。
(3)基准电压
        基准电压也叫参考电压,也就是当做参照作用。我们测试外部电压时会以基准电压作为参考,先把基准电压分成多少分(根据分辨率来分),然后再和被测电压进行比较,然后得到比较的结果,这样就能测试出输入电压是多少了。
24.1.1 DAC特性
STM32MP157的DAC模块(数字/模拟转换模块)是12位数字输入,电压输出型的DAC。DAC可以配置为8位或12位模式,也可以与DMA控制器配合使用。DAC工作在12位模式时,数据可以设置成左对齐或右对齐。DAC模块有2个输出通道,可以在每个DAC输出通道上进行单独的校准,每个通道都有单独的转换器,DAC输出通道支持低功耗模式。在双DAC模式下,2个通道可以独立地进行转换,也可以同时进行转换并同步地更新2个通道的输出。DAC可以通过引脚输入参考电压VREF+(和ADC共用)以获得更精确的转换结果。当DAC输出与焊盘断开并连接至片上外设时,DAC_OUTx引脚可用作通用输入/输出引脚(GPIO)。
STM32MP157的DAC模块主要特点有:
① 1个DAC转换器,每个DAC最多2个输出通道 ;
② 12位模式下数据左对齐或者右对齐 ;
③ 同步更新功能 ;
④ 噪声波形生成 ;
⑤ 三角波形生成 ;
⑥ 双DAC通道同时或者分别转换;
⑦ 每个通道都有DMA功能;
⑧ 通过外部触发信号进行转换;
⑨ 每路 DAC 输出均可与 DAC_OUTx 输出引脚断开连接;
⑩ DAC 输出可与片上外设连接;
⑪ 可在停止模式下通过采样和保持模式实现低功耗运行;
⑫ 来自VREF+引脚的输入参考电压。
DAC通道框图如下图所示:
24.1.2 DAC框图
image002.jpg
图23.1.2. 5 DAC通道框图

        ADC框图中的输入/输出引脚信息如下表所示:
lQLPJxaS5-0coQfMgc0CLbCXlILM-QfwfwLxwc1CgKIA_557_129.png
表23.1.2. 2 DAC输入/输出引脚

        除了上表列出的输入/输出引脚,DAC通道框图还有一些内部输入/输出信号,具体如下表所示:
lQLPJxaS5_c-nbPM2M0CLrAPLLvbQjBPlgLxwd2IQFoA_558_216.png
表23.1.2. 3 DAC内部输入/输出信号

关于DAC框图的内容,我们做如下讲解:
1. DAC的输入/输出引脚以及内部信号
图中VDDA和VSSA为DAC模块模拟部分的供电。VREF+ 是正模拟参考电压输入,选择范围是1.62V~3.6V,开发板上我们一般给VREF+接入的电压时3.3V。在ADC实验章节我们说过,STM32MP157有ADC和DAC共用的内部基准电压VREFBUF,可通过VREFBUF_CSR寄存器进行配置,可选1.5 V、1.8 V、2.048 V和2.5 V。
要注意的是,DAC只能在VDDA高于或等于1.8V时使用,使用内部参考电压VREF+时要求VDDA高于或等于VERF+++0.3V。
DAC_OUT1/2就是DAC的两个输出通道了(对应PA4和PA5引脚):
image004.jpg
图23.1.2. 6DAC的两个输出通道引脚

注意:表中的dac_pclk即DAC的时钟源,转换和寄存器访问都是靠这个时钟,该时钟来自APB1,最大可以配置为104.5Mhz。

从通道框图可以看出,DAC输出是受DAC_DORx寄存器直接控制的,但是我们不能直接往DAC_DORx寄存器写入数据,而是通过DAC_DHRx间接的传给DAC_DORx寄存器,实现对DAC输出的控制。
2. DAC通道使能/转换速度和输出电压
(1)DAC通道使能
        将 DAC_CR 寄存器中的相应 ENx位置1,即可使能对应DAC 通道。经过一段启动时间     tWAKEUP后,DAC 通道被真正使能。从数据手册查询,tWAKEUP 的典型值为2us或者5 us:
image006.jpg
图23.1.2. 7 tWAKEUP 时间

        一旦使能DAC通道,相应的GPIO引脚(PA4和PA5)就会自动与DAC的模拟输出(DAC_OUTx)相连,为了避免寄生的干扰和额外的功耗,引脚PA4和PA5应当设置成模拟输入。
(2)DAC的转换速度
        DAC的转换速度最快是1MSPS:
image008.jpg
图23.1.2. 8 DAC的转换速率

(3)DAC的输出电压
        DAC就是数字量转换为模拟量,数字输入经过DAC被线性地转换为模拟电压输出。当DAC的参考电压为VREF+的时候,DAC的输出电压是线性的从0~VREF+,12位模式下DAC输出电压与VREF +以及DORx的计算公式如下:
DACx输出电压= VREF *(DORx/4096)
3. DAC的8/12位数据格式
前面我们提到,STM32MP157的DAC支持8/12位模式,8位模式的时候是固定的右对齐的,而12位模式又可以设置左对齐/右对齐。DAC单通道模式下的数据寄存器对齐方式,总共有3种情况,如下图所示:
image010.jpg
图23.1.2. 9 DAC单通道模式下的数据寄存器对齐方式

①8位数据右对齐:用户将数据写入DAC_DHR8Rx[7:0]位(实际存入DAC_DHRx寄存器的DHRx[11:4]位,后面的描述中我们只写DHRx[11:4]位)。
②12位数据左对齐:用户将数据写入DAC_DHR12Lx[15:4]位(实际存入DHRx[11:0]位)。
③12位数据右对齐:用户将数据写入DAC_DHR12Rx[11:0]位(实际存入DHRx[11:0]位)。
我们本章实验中使用的都是单通道模式下的DAC通道1,采用12位右对齐格式,所以采用第③种情况。另外DAC还具有双通道转换功能。对于 DAC 双通道(可用时),也有三种可能的方式,如下图所示:
image012.jpg
图23.1.2. 10 DAC双通道模式下的数据寄存器对齐方式

①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]位)。
4. DAC的触发选择
写入到DAC_DHRyyy(就是上述提到的DAC_DHR8Rx、DAC_DHR12Lx和DAC_DHR12Rx等)的值,经过相应的移位操作后会被转存到DHRx寄存器中,随后,DHRx寄存器的内容可以通过触发后被自动地传送到DAC_DORx寄存器中,当 DAC_DORx加载了DAC_DHRx内容时,模拟输出电压将在一段时间 tSETTLING 后可用,也就是从DAC_OUTx引脚上输出,输出的值就是我们要获取的转换值。tSETTLING 的具体时间取决于电源电压和模拟输出负载,我们可以从《STM32MP157A&D数据手册》找到此值的典型值是2us。
DAC可以通过软件或者硬件触发转换,通过配置TENx控制位来决定。
(1)不使用触发
TENx为0时不使用触发,DAC通道x的触发被禁用,写入到DAC_DHRx寄存器的数据在一个APB1时钟后自动传至寄存器DAC_DORx中。不使用触发转换的时间框图如下图所示,dac_pclk的时钟其实也就是APB1总线时钟:
image014.jpg
图23.1.2. 11 TEN=0时DAC模块转换时间框图

(2)使用触发
TENx为1时,表示使能触发,这里分为软件触发和硬件触发。DAC_CR寄存器的TSELx[3:0]为0000时表示选择软件触发,DAC_CR寄存器的TSELx[3:0]不为0000时(具体设置什么可以根据此位的设置来),表示选择硬件触发。
软件触发
如果选择软件触发,一旦DAC_SWTRGR 寄存器的SWTRIGx(x=1,2)位置 1,转换随即开始。DAC_DHRx的内容加载到DAC_DORx寄存器中后,SWTRIG 即由硬件清0。如果选择软件触发,DAC_DHRx寄存器的内容只需一个dac_pclk(也就是一个APB1)时钟周期即可转移到DAC_DORx寄存器中。ENx 位置1(使能DAC通道)时就无法更改TSELx[3:0]位。
硬件触发
如果选中硬件触发,每次DAC接口检测到触发信号时(定时器TRGO输出,或者外部中断线9的上升沿),最新存放在DAC_DHRx的值会被传入DAC_DORx中,发生触发后再经过3个dac_pclk周期(3个APB1时钟周期)后,DAC_DORx寄存器将会得到更新。一旦DAC_DORx寄存器的值更新后,再经过时间tSETTLING之后,输出即有效,这段时间的长短依电源电压和模拟输出负载的不同会有所变化。
如果使用触发(TEN=1),可通过TSELx[3:0]控制位来决定选择16个触发事件中的某一个来触发DAC转换。这16个触发事件的部分事件如下表所示:

lQLPJxaS6APC9WbNASvNAk2w2KS-gv7Pq_QC8cHxzgAnAA_589_299.png
表23.1.2. 4 DAC触发选择

采样和保持模块(低功耗)及其关联寄存器可在停止模式下使用 LSI时钟源(dac_hold_ck)运行。
5. DAC的DMA请求
每个DAC通道都有DMA功能,两个DMA通道分别用于处理两个DAC通道的DMA 请求。如果DMAENx 位置1时,如果发生硬件触发(而不是软件触发),就会产生一个DMA 请求,然后DAC_DHRx的数据被转移到DAC_DORx中。
DAC_DMA请求没有缓冲队列,如果第二个外部触发到达时,尚未收到第一个外部触发的应答,将不会发出新的请求,DMA通道下溢标志 DMAUDRx将置1,产生中断(DMA下溢),DAC 通道仍将继续转换旧有数据。软件通过写1到DMAUDRx中来清除标志位,将所用 DMA 数据流的 DMAEN 位清零,并重新初始化 DMA 和 DAC 通道,以便正确地重新开始 DMA 传输。
lQLPJxaS6AuCnXUszQGRsGtsv6l-HonFAvHB_y3AogA_401_44.png
表23.1.2. 5 DAC中断

24.2 DAC输出实验
本实验我们来学习DAC输出实验。
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 17-1 DAC_OUT1。
24.2.1 DAC寄存器
下面,我们介绍要实现DAC的通道1输出,需要用到的一些DAC寄存器。
1. DAC控制寄存器(DAC_CR)
DAC控制寄存器描述如下图所示:
image016.jpg
图23.2.1. 1 DAC_CR寄存器

DAC_CR的低16位用于控制通道1,而高16位用于控制通道2,我们这里仅列出本实验需要设置的一些位:
EN1位:用于DAC通道1的使能,我们需要用到DAC通道1的输出,该位必须设置为1。
TEN1位:用于DAC通道1的触发使能,我们设置该位为0,不使用硬件触发。写入DAC_DHR1的值会在1个APB1周期后传送到DAC_DOR1,然后输出到PA4口上。
TSEL[3:0]位:用于选择DAC通道1的触发方式,本实验设置为0,使用软件触发。
WAVE[1:0]位:用于控制DAC通道1的噪声/波形输出功能,默认设置为00,不使能噪声/波形输出。
DMAEN1位:用于控制DAC通道1的DMA使能,本实验不使用DMA,设置该位为0即可。
CEN1位:用于控制DAC通道1的输出缓冲校准使能,本章不使用校准功能(默认有一个出厂校准值,我们使用默认的校准值即可),设置该位为0即可。
2. DAC模式控制寄存器(DAC_ MCR)
DAC模式控制寄存器描述如下图所示:
image018.jpg
图23.2.1. 2 DAC_ MCR寄存器

该寄存器我们只关心MODE1[2:0],这三个位用于设置DAC通道1的工作模式。本章使用普通模式,且使用输出缓冲,设置MODE1[2:0]=0即可。MODE2[2:0] 设置通道2的工作模式,本实验没用到。
3. DAC通道1 12位右对齐数据保持寄存器(DAC_ DHR12R1)
DAC通道1 12位右对齐数据保持寄存器描述如图下图所示:
image020.jpg
图23.2.1. 3 DAC_ DHR12R1寄存器

        该寄存器用来设置DAC输出,通过写入12位数据到该寄存器,就可以在DAC输出通道1(PA4)得到我们所要的结果。
24.2.2 DAC的HAL库驱动
DAC在HAL库中的驱动代码在stm32mp1xx_hal_dac.c和stm32mp1xx_hal_dac_ex.c文件(及其头文件)中,可以翻看这两个文件来了解。
1. HAL_DAC_Init函数
DAC的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_DAC_Init(DAC_HandleTypeDef *hdac);
        函数描述:
用于初始化DAC。
        函数形参:
形参1是DAC_HandleTypeDef结构体类型指针变量,其定义如下:
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               
从该结构体看到该函数并没有设置任何DAC相关寄存器,即没有对DAC进行任何配置,它只是HAL库提供用来在软件上初始化DAC,为后面HAL库操作DAC做好准备。
        函数返回值:
HAL_StatusTypeDef枚举类型的值。
        注意事项:
DAC的MSP初始化函数HAL_DAC_MspInit,该函数声明如下:
void HAL_DAC_MspInit(DAC_HandleTypeDef* hdac);
2. HAL_DAC_ConfigChannel函数
DAC 的通道参数初始化函数,其声明如下:
HAL_StatusTypeDef HAL_DAC_ConfigChannel(DAC_HandleTypeDef *hdac,
DAC_ChannelConfTypeDef *sConfig, uint32_t Channel);
        函数描述:
该函数用来配置DAC通道的触发类型以及输出缓冲。
        函数形参:
形参1是DAC_HandleTypeDef结构体类型指针变量。
形参2是DAC_ChannelConfTypeDef结构体类型指针变量,其定义如下:
typedef struct
{
  uint32_t DAC_SampleAndHold;                      /* 设置是否使能低功耗模式,即采样和保持模式 */
  uint32_t DAC_Trigger;                             /* DAC触发源的选择 */
  uint32_t DAC_OutputBuffer;                       /* 启用或者禁用DAC通道输出缓冲区 */
  uint32_t DAC_ConnectOnChipPeripheral; /* 指定DAC输出是否连接到片上外设 */
  uint32_t DAC_UserTrimming;   /* 设置DAC的校准方式,采用出厂模式还是用户模式 */
  uint32_t DAC_TrimmingValue;  /* 设置用户校准模式的偏移值 */
  DAC_SampleAndHoldConfTypeDef  DAC_SampleAndHoldConfig; /* 设置采样保持具体参数 */
} DAC_ChannelConfTypeDef;
形参3用于选择要配置的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。
        函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_DAC_Start函数
使能启动DAC转换通道函数,其声明如下:
HAL_StatusTypeDef HAL_DAC_Start(DAC_HandleTypeDef *hdac, uint32_t Channel);
        函数描述:
使能启动DAC转换通道。
        函数形参:
形参1是DAC_HandleTypeDef结构体类型指针变量。
形参2用于选择要启动的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。
        函数返回值:
HAL_StatusTypeDef枚举类型的值。
4. HAL_DAC_SetValue函数
DAC的通道输出值函数,其声明如下:
HAL_StatusTypeDef HAL_DAC_SetValue(DAC_HandleTypeDef *hdac, uint32_t Channel,
uint32_t Alignment, uint32_t Data);
        函数描述:
配置DAC的通道输出值。
        函数形参:
形参1是DAC_HandleTypeDef结构体类型指针变量。
形参2用于选择要输出的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。
形参3用于指定数据对齐方式。
形参4设置要加载到选定数据保存寄存器中的数据。
        函数返回值:
HAL_StatusTypeDef枚举类型的值。
5. HAL_DAC_GetValue函数
DAC读取通道输出值函数,其声明如下:
uint32_t HAL_DAC_GetValue(DAC_HandleTypeDef *hdac, uint32_t Channel);
        函数描述:
获取所选DAC通道的最后一个数据输出值。
        函数形参:
形参1是DAC_HandleTypeDef结构体类型指针变量。
形参2用于选择要读取的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。
        函数返回值:
获取到的输出值。
24.2.3 硬件设计
1. 例程功能
使用KEY0/KEY1两个按键,控制STM32MP157内部DAC的通道1输出电压大小,然后通过ADC1的通道19采集DAC输出的电压,并通过串口将ADC采集到的电压值以及DAC的设定输出电压值等信息打印出来。实验中,LED0闪烁,提示程序在运行。
实验时,请用跳线帽将PA4和PA5接在一起:
image022.jpg
图23.2.3. 1 硬件连接

本节实验ADC的配置和相关代码部分和ADC实验章节的 单通道ADC采集实验 部分一样。
2. 硬件资源
1)LED灯:LED0
2)按键KEY0和KEY1
3)串口4
4)ADC1的通道19引脚(PA5)
5)DAC的通道1
lQLPJxaS6BifjEAozQJysKr67vbhXPSgAvHCFKcAWgA_626_40.png
表23.2.3. 1硬件资源

3. 原理图
        ADC和DAC属于STM32MP157的内部资源,实际上我们只需要软件设置就可以正常工作。
image024.jpg
图23.2.3. 2 原理图部分

24.2.4 软件设计
1. 新建和配置工程
(1)配置LED0、KEY0、KEY1和UART4
        新建工程DAC_OUT1,本节实验我们会用到UART4发送数据,还会使用到按键KEY1、KEY0以及LED0,所以我们这里按照前面的实验章节配置LED0、KEY0、KEY1。UART4的配置我们这里就不再赘述了,请参考前面串口通信实验章节的来配置。
image026.jpg
图23.2.4. 1 按键和LED的引脚部分配置

image028.jpg
图23.2.4. 2 UART4引脚部分配置

        LED0和按键以及UART4的代码我们本节实验就不讲解了,大家可以参考本实验工程里的代码,也可以参考前面相关实验章节部分。
(2)配置ADC
        ADC1的配置可参考前面 单通道ADC采集实验 部分。选择ADC1的通道19:
image030.jpg
图23.2.4. 3选择ADC1的通道19

        ADC的配置参数我们前面ADC实验章节已经做了详细讲解,这里就不再说明,配置如下:
image032.jpg
图23.2.4. 4配置ADC1的参数

(3)配置DAC
        DAC配置如下,DAC的每路输出均可与DAC_OUTx输出引脚断开连接,然后可与片上外设连接。这里我们可以配置DAC输出连接到外部引脚DAC_OUTx。
        DAC参数配置部分:
        Output Buffer配置使能;
        Trigger配置触发源,可以配置软件触发和硬件触发以及不触发,我们配置不触发;
        DAC High Frequency配置DAC是否以指定频率接口模式工作,可以开启或者关闭高频接口模式,或者设置高频接口模式为自动模式。
        User Trimming设置DAC的校准方式,采用出厂模式还是用户模式。校准的目的是为了减少误差,这里我们就默认使用出厂模式;
        Sample And Hold用于设置是否使能低功耗模式,即采样和保持模式,这里我们就不选择了。
image034.jpg
图23.2.4. 5配置DAC的输出通道1
(4)UART相关的参数配置
        UART4的参数配置以及中断配置请参考前面串口通信实验。
(5)时钟配置
        这里配置MCU的时钟为209MHz,使用外部时钟HSE,这部分配置可以参考前面实验章节:
image036.jpg
图23.2.4. 6时钟树配置

        ADC1的时钟使用PER,其中PER时钟源默认使用HSI,即为64MHz。上面ADC参数配置中,我们配置分频系数为2,所以实际ADC的时钟是32MHz。DAC因为接在APB1上,DAC的时钟是104.5MHz。
(6)配置生成独立的.c和.h文件
image038.jpg
图23.2.4. 7配置生成独立的文件

2. 生成工程
        保存配置,生成工程后,将前面按键输入实验章节使用的BSP文件夹拷贝到工程的Src文件夹下,本节我们要使用LED0、KEY0和KEY1的驱动,所以将LED1和WKUP相关的代码注释掉即可。
image040.jpg
图23.2.4. 8生成工程

3. 初始化代码分析
        ADC的初始化代码我们在前面章节的实验已经介绍过,这里就不再介绍了。DAC的初始化代码在dac.c文件及其头文件中,代码附上了详细的注释,如下:
/**
* @brief   DAC外设初始化函数
* @note    DAC的输入时钟来自APB1, 时钟频率=104.5Mhz=9.5ns
                   DAC在输出buffer关闭的时候, 输出建立时间:
                   tSETTLING = 2us (STM32MP1数据手册有写)
                   因此DAC输出的最高速度约为:500Khz, 以10个点为一个周期,
                   最大能输出50Khz左右的波形。
* @param   无
* @retval  无
*/
#include "dac.h"
DAC_HandleTypeDef hdac1;                                                         /* DAC句柄 */
void MX_DAC1_Init(void)
{
  DAC_ChannelConfTypeDef sConfig = {0};
  hdac1.Instance = DAC1;                                                                /* DAC1,只有一个DAC */
  if (HAL_DAC_Init(&hdac1) != HAL_OK)                                 /* DAC初始化 */
  {
    Error_Handler();
  }
  /* 关闭高频接口模式 */
  sConfig.DAC_HighFrequency = DAC_HIGH_FREQUENCY_INTERFACE_MODE_DISABLE;
  /* 不使能低功耗模式 */
  sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE;
  sConfig.DAC_Trigger = DAC_TRIGGER_NONE;                        /* 不使用触发源 */
  /* 输出Buffer使能 */
  sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
  /* DAC输出不连接到外围设备 */
  sConfig.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_DISABLE;
  /* 校准模式为出厂模式 */
  sConfig.DAC_UserTrimming = DAC_TRIMMING_FACTORY;
  /* DAC通道初始化 */
  if (HAL_DAC_ConfigChannel(&hdac1, &sConfig, DAC_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
}
/**
* @brief   DAC的引脚初始化函数
* @param   dacHandle DAC句柄
* @retval  无
*/
void HAL_DAC_MspInit(DAC_HandleTypeDef* dacHandle)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(dacHandle->Instance==DAC1)
  {
    __HAL_RCC_DAC12_CLK_ENABLE();                                /* 开启DAC时钟 */
    __HAL_RCC_GPIOA_CLK_ENABLE();                                /* 开启GPIOA时钟 */
    /**DAC1 GPIO Configuration
    PA4     ------> DAC1_OUT1
    */
    GPIO_InitStruct.Pin = GPIO_PIN_4;                        /* 引脚4 */
    GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;        /* 模拟模式 */
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);        /* 初始化GPIO引脚 */
  }
}
/**
* @brief   DAC外设取消初始化函数
* @note    如果有必要,可以调用此函数来取消初始化DAC
* @param   dacHandle DAC句柄
* @retval  无
*/
void HAL_DAC_MspDeInit(DAC_HandleTypeDef* dacHandle)
{
  if(dacHandle->Instance==DAC1)
  {
    __HAL_RCC_DAC12_CLK_DISABLE();                                /* 关闭DAC时钟 */
    /**DAC1 GPIO Configuration
    PA4     ------> DAC1_OUT1
    */
    HAL_GPIO_DeInit(GPIOA, GPIO_PIN_4);                        /* 取消初始化DAC */
  }
}
4. 添加用户代码
(1)修改adc.c文件
        按照前面 单通道ADC采集实验 章节,在adc.c中添加如下代码:
/* USER CODE BEGIN 1 */
/**
* @brief       获取ADC1通道ch的转换结果,先取times次,然后取平均
* @param       ch      : 通道 ,为0~19
* @param       times   : 获取次数
* @retval      通道ch的times次转换结果的平均值
*/
uint32_t adc_get_result_average(uint32_t ch, uint8_t times)
{
    uint32_t temp_val = 0;
    uint8_t t;
    /* ADC校准 */
    HAL_ADCEx_Calibration_Start(&hadc1, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED);
    HAL_ADC_Start(&hadc1);                             /* 启动ADC */
    HAL_ADC_PollForConversion(&hadc1, 10);          /* 轮询转换 */;
    for (t = 0; t < times; t++)                                                  /* 获取times次数 */
    {
        /* 获取转换结果,并将每次转换结果进行相加 */
        temp_val +=  HAL_ADC_GetValue(&hadc1);
        HAL_Delay(5);
    }
    return temp_val / times;                                                    /* 返回转换后的平均值*/
}
/* USER CODE END 1 */
        adc.h文件添加如下代码:
/* USER CODE BEGIN Private defines */
uint32_t adc_get_result_average(uint32_t ch, uint8_t times);   
/* USER CODE END Private defines */
(2)修改main.c文件
        在main.c文件添加如下代码,当KEY0按下时,DAC_DHR12R1寄存器的值为2048,然后ADC1的通道19获取DAC1_OUT1的值,根据此值计算出对应的电压值。
1   #include "main.h"
2   #include "adc.h"
3   #include "dac.h"
4   #include "usart.h"
5   #include "gpio.h"
6  
7   /* USER CODE BEGIN Includes */
8   #include "./BSP/Include/led.h"
9   #include "./BSP/Include/key.h"
10  /* USER CODE END Includes */
11
12  void SystemClock_Config(void);
13  void PeriphCommonClock_Config(void);
14
15  int main(void)
16  {
17    HAL_Init();                                                                /* 初始化HAL库 */
18    if(IS_ENGINEERING_BOOT_MODE())
19    {
20      SystemClock_Config();                                /* 配置系统时钟 */
21    }
22    if(IS_ENGINEERING_BOOT_MODE())
23    {
24      PeriphCommonClock_Config();                        /* 配置外设时钟 */
25    }
26    MX_GPIO_Init();                                                        /* 初始化GPIO */
27    MX_DAC1_Init();                                                        /* 初始化DAC */
28    MX_UART4_Init();                                                /* 初始化UART4 */
29    MX_ADC1_Init();                                                        /* 初始化ADC1 */
30    /* USER CODE BEGIN 2 */
31      led_init();                                         /* 初始化LED */
32      HAL_UART_Receive_IT(&huart4,&RxBuffer,1);/* 以中断方式接收函 */
33      uint16_t adcx,dacx;
34      float adc_temp, dac_temp;
35      uint8_t t = 0;
36      uint8_t key;
37      HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);  /* 启动DAC通道1 */
38    /* USER CODE END 2 */
39   
40    while (1)
41    {
42      /* USER CODE BEGIN 3 */
43      t++;
44      key = key_scan(0);                                          /* 按键扫描 */
45      if (key == KEY0_PRES)                                        /* 如果是KEY0按下 */
46       {
47          /* 设置DAC通道1为 12位数据右对齐模式,DAC_DHR12R1寄存器的值为4095 */
48          HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 4095);
49       }
50      else if (key == KEY1_PRES)                                /* 如果是KEY1按下 */
51       {
52          /* 设置DAC通道1为 12位数据右对齐模式,DAC_DHR12R1寄存器的值为4000 */
53          HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 4000);
54       }
55       /* KEY0/KEY1按下了,或者指定定时时间到了 */
56      if (t == 10 || key == KEY1_PRES || key == KEY0_PRES)
57       {
58          printf("\r\n");
59          /* 读取前面设置DAC1_OUT1的值 */
60          dacx = HAL_DAC_GetValue(&hdac1, DAC_CHANNEL_1);
61          dac_temp = (float)dacx * (3.3f / 4096); /* 得到DAC电压值 */
62          /* 打印DAC_DHR12R1寄存器的值 */
63     printf("DAC Output Value = %d, DAC Output Voltage = %.2f\r\n", dacx, dac_temp);
64           /* 得到ADC通道19的转换值 */
65          adcx=adc_get_result_average(ADC_CHANNEL_19, 10);
66          /* 计算ADC对应的电压值 */
67          adc_temp = (float)adcx * (3.3 / 65536);            
68          /* 打印ADC的转换值以及对应的电压值 */
69     printf("ADC Input Value  = %d, ADC Input Voltage  = %.2f\r\n", adcx, adc_temp);
70          /* DAC_DHR12R1寄存器清零 */
71          HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0);
72          LED0_TOGGLE();                                                  /* LED0闪烁 */
73          t = 0;
74       }
75          HAL_Delay(1000);                                                /* 延时1s */
76    }
77    /* USER CODE END 3 */
78  }
        第37行,先启动DAC,DAC启动后才可以进行后续操作。
        第44~54行,如果是KEY0按下,则设置DAC通道1为 12位数据右对齐模式,DAC_DHR12R1寄存器的值为4095;如果是KEY1按下,设置DAC通道1为 12位数据右对齐模式,DAC_DHR12R1寄存器的值为4000。这里注意的是,ADC是16位的,而DAC是12位的,DAC_DHR12R1寄存器的值最大只能设置为4095。
        按照上述参数计算,如果是按键KEY0按下,电压值为3.3V(最大值),如果是按键KEY1按下,电压值应为3.3*4000/4095=3.22V。
        第56~71行:
        如果是KEY0或者KEY1按下了,或者是10s的时间到了,UART4会打印DAC输出的值和计算出的电压值,以及ADC采集到的转换值以及计算出的电压值。
        当没有按键按下时,UART4打印的这些值都应该为0,因为第71行使用HAL_DAC_SetValue函数对DAC_DHR12R1寄存器进行了清零操作。实际的实验中可能会有一些误差,会出现一些非0的情况,不过数值比较小,我们可以认为其值为0。
        第72行,LED0闪烁。
        第75行,延时1秒,当然也可以将此延迟时间改短一些。
24.2.5 编译和测试
        程序中使用浮点运算,所以要设置工程属性,支持浮点打印。
image042.jpg
图23.2.5. 1配置工程支持浮点打印

        程序测试结果如下,和上面的计算结果一致:
image044.jpg
图23.2.4. 9测试结果

24.3 DAC输出三角波实验
本实验我们来学习使用如何让DAC输出三角波,DAC初始化部分还是使用上一章DAC输出实验 章节的,所以操作本实验的前提是先学习DAC输出实验 章节的内容。
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 17-2 DAC_Triangle_Wave。
24.3.1 生成三角波原理
1. 使用外部触发方式生成三角波
要生成三角波,DAC_CR寄存器的TENx位置1来使能DAC触发,WAVEx[1:0]要设置为1:x(x表示0或1)来使能生成三角波。如下图是生成DAC三角波示意图,DAC_DHRx是三角波的基电压值(最低电压值),该值可以调节。MAMPx[3:0]是DAC_CR寄存器的位,通过设置该位可以控制三角波的振幅。DAC_DHRx+MAMPx[3:0]是DAC输出的三角波的最高电压值(电压幅值)。三角波的数据生成是由一个三角波计数器往复加减生成的,每次触发之后该计数器会加1或减1:
每次发生触发事件后,经过三个APB1时钟周期,内部三角波计数器将会递增,在不发生溢出的情况下,该计数器的值将与DAC_DHRx寄存器内容相加,所得总和将传输到DAC_DORx寄存器中,只要该三角波计数器的值小MAMPx[3:0]位定义的最大振幅值,三角波计数器就会一直递增,一旦达到MAMPx[3:0]配置的振幅值,计数器将递减至零,然后再递增,以此类推,这个过程如下图所示:
image046.jpg
图24.3.1. 1三角波示意图

        使用触发方式,我们可以选择定时器作为触发源,使用定时器来触发。可以设置定时器每周期计时结束时DAC_DHRx寄存器的内容加1,当使用12位模式时,DAC_DHRx寄存器的最高赋值为4095,所以在配置定时器的时候注意此范围。
image048.jpg
图24.3.1. 2可选的外部触发源

2. 不使用触发方式
当然,我们也可以不使用触发的方式来生成三角波,我们通过每隔一定时间dt往DAC通道12位右对齐数据保持寄存器 (DAC_DHR12R1)中写入一个值,每次写入的值递增incval,总共递增samples/2次,然后再将DAC_DHR12R1的值每隔dt递减incval,总共递减samples/2次,如此反复,DAC_DHR12R1的值传输给DAC_DORx,最后由DAC_OUTx输出转换的电压值,这样就类似一个三角波了。这里,dt我们称为每个采样点的延时时间,samples我们称为采样率,我们用如下一张图来描述:
image050.jpg
图24.3.1. 3 三角波示意图

其中波形的振幅对应DAC_DHR12R1的值最大为4095(DAC_DHR12R1是12位的寄存器,最大值为4095),实验中的dt值尽量小,这样得出的波形就越接近三角形,上图中为了讲解,画的dt的间隔比较大。图中显示的是一个三角波的过程,三角的斜边的某个点的值是DAC_OUTx输出的电压值,是由DAC_DHR12R1的值转换得来的,根据勾股定理,三角斜边值大于直角边的值,所以(DAC_DHR12R1+1)>(samples/2),此三角波的周期T=samples*dt,那么此三角波的频率就是1/T。
本实验我们就采用以上第二种方法,实验中,我们通过修改dt的值和samples的值来实现输出两种波形:
1)设置dt为5us,samples为2000,则输出的三角波频率为100Hz;
2)设置dt为500us,samples为20,则输出的三角波也是100Hz。
24.3.2 DAC寄存器  
1. DAC_CR寄存器
        本实验用到的DAC_DHRx和DAC_DORx寄存器在上一章节已经介绍了,这里我们介绍DAC_CR寄存器和三角波设置有关的位,如下:
image052.jpg
图24.3.2. 1 DAC_CR寄存器

        如果使用触发方式,对应位设置如下:
        TENx位用于使能DAC的通道,将该位置1则使能对应的通道;
        WAVEx[1:0]用于使能或者禁止生成波,如果要生成三角波,此位设置为1x;
        DAC_CR寄存器的MAMP1[3:0]和MAMP2[3:0]分别对应的是的是通道1和通道2的掩码/振幅选择器配置项,这些位由软件写入,用于在生成噪声波模式下选择掩码,或者在生成三角波模式下选择振幅,MAMP1[3:0]配置如下:
        0000:不屏蔽 LFSR 的位 0/三角波振幅等于 1
        0001:不屏蔽 LFSR 的位 [1:0]/三角波振幅等于 3
        0010:不屏蔽 LFSR 的位 [2:0]/三角波振幅等于 7
        0011:不屏蔽 LFSR 的位 [3:0]/三角波振幅等于 15
        0100:不屏蔽 LFSR 的位 [4:0]/三角波振幅等于 31
        0101:不屏蔽 LFSR 的位 [5:0]/三角波振幅等于 63
        0110:不屏蔽 LFSR 的位 [6:0]/三角波振幅等于 127
        0111:不屏蔽 LFSR 的位 [7:0]/三角波振幅等于 255
        1000:不屏蔽 LFSR 的位 [8:0]/三角波振幅等于 511
        1001:不屏蔽 LFSR 的位 [9:0]/三角波振幅等于 1023
        1010:不屏蔽 LFSR 的位 [10:0]/三角波振幅等于 2047
        ≥ 1011:不屏蔽 LFSR 的位 [11:0]/三角波振幅等于 4095
        WAVE1[1:0]和WAVE2[1:0]用于使能DAC通道1和通道2的噪声/三角波生成,这些位由软件置 1 或清零:
        00:禁止生成波
        01:使能生成噪声波
        1x:使能生成三角波
2. 其它寄存器
        关于本节实验用到的DAC的HAL库驱动,我们前面的实验章节已经介绍过,这里就不再重复介绍了。
24.3.3 DAC的HAL库驱动
        本实验中用到的HAL库驱动前面的实验代码都有介绍到,这里就不再赘述。
24.3.4 硬件设计
1. 例程功能
使用DAC输出三角波,通过KEY0/KEY1两个按键,控制DAC1的通道1输出两种三角波,需要通过示波器接PA4进行观察波形。LED0闪烁,提示程序运行。
我们只需要把示波器的探头接到DAC1通道1(PA4)引脚,就可以在示波器上显示DAC输出的波形。PA4的引脚排针已经引出,硬件连接如下图所示:
image054.jpg
图24.3.4. 1硬件连接示意图

2. 硬件资源
1)LED灯:LED0
2)按键KEY0和KEY1
3)DAC的通道1
lQLPJxaS6CSLTasrzQFhsHoUyca4tJHOAvHCKCMAJwA_353_43.png
表24.3.4. 1硬件资源

3. 原理图
        DAC属于STM32MP157的内部资源,实际上我们只需要软件设置就可以正常工作。
image056.jpg
图24.3.4. 2原理图部分

24.3.5 软件设计
1. 创建和配置工程
        本实验使用到DAC,可以直接在上一章节的实验的基础上操作。为了方便,我们新建了一个工程DAC_Triangle_Wave,DAC工程的配置和上一章节的一样,这里我们没有用到ADC,关于ADC部分我们就不配置了。
image058.jpg
图24.3.5. 1DAC配置

        关于本节实验用到的LED0、KEY1和KEY0的配置清参考前面相关的实验章节部分。时钟配置使用HSE作为锁相环PLL3的输入时钟源,我们配置APB1为104.5MHz,关于时钟配置也请参考前面实验有关章节部分。
2. 生成工程
        生成工程,并将上一章节用到的BSP文件夹拷贝到本工程的Src目录下,因为我们会用到上一章节实验的LED0和KEY0以及KEY1的驱动程序,然后本节实验会用到微秒延时函数(dt采用us来计时),所以将SysTick高精度延时实验的delay.c和delay.h文件拷贝到本工程的BSP文件夹下。
image060.jpg
图24.3.5. 2生成工程

3. 添加用户代码
(1)添加三角波生成函数
        如下,我们在dac.c文件中添加如下代码:
1   /* USER CODE BEGIN 1 */
2   /**
3    * @brief   设置DAC_OUT1输出三角波
4    * @note    输出频率 ≈1/(dt*samples),注意单位换算。不过在dt较小
5    *          的时候,比如小于5us时, 由于delay_us本身就不准了(调用函数,
6    *          计算等都需要时间,延时很小的时候,这些时间会影响到延时), 频率会偏小.
7    *
8    * @param   maxval : 最大值(0<maxval<4096),(maxval+1)必须大于等于samples/2
9    * @param   dt     : 每个采样点的延时时间(单位: us)
10   * @param   samples: 采样点的个数,samples必须小于等于(maxval+1)*2,maxval不能等于0
11   * @param   n      : 输出波形个数,0~65535
12   * @retval  无
13   */
14  void dac_triangular_wave(uint16_t maxval, uint16_t dt, uint16_t samples, uint16_t n)
15  {
16      uint16_t i, j;
17      float incval;                                           /* 递增量 */
18      if((maxval+1)<=samples)return ;                 /* 数据不符合范围 */
19      incval = (maxval+1)/(samples/2);          /* 计算递增量 */
20      for(j=0;j<n;j++)
21      {
22          DAC1->DHR12R1 = 0;                                          /* 先输出0 */
23          for(i = 0; i < (samples/2); i++)          /* 输出递增 */
24          {
25               DAC1->DHR12R1 +=  incval;                /* 递增量incval */
26               delay_us(dt);                                        /* 延时dt us */
27          }
28          for(i = 0; i < (samples / 2); i++)          /* 输出递减 */
29          {
30               DAC1->DHR12R1 -=  incval;                /* 递减量incval */
31               delay_us(dt);                                        /* 延时dt us */
32          }
33      }
34  }
35                /****** 两种方法,本质一样。上半部分直接操作寄存器,下半部分直接调用HAL库 ******/
36  /*
37  void dac_triangular_wave(uint16_t maxval,uint16_t dt, uint16_t samples, uint16_t n)
38  {
39      uint16_t i, j;
40      float incval;
41      float Curval;
42
43      if((maxval + 1) <= samples)return ;
44      incval = (maxval + 1) / (samples / 2);
45      for(j = 0; j < n; j++)
46      {
47     HAL_DAC_SetValue(&g_dac_handle,DAC_CHANNEL_1,DAC_ALIGN_12B_R,Curval);
48          for(i = 0; i < (samples / 2); i++)
49          {
50              Curval  +=  incval;
51              HAL_DAC_SetValue(&g_dac_handle,DAC_CHANNEL_1,DAC_ALIGN_12B_R,Curval);
52              delay_us(dt);
53          }
54      for(i = 0; i < (samples / 2); i++)
55          {
56              Curval  -=  incval;
57              HAL_DAC_SetValue(&g_dac_handle,DAC_CHANNEL_1,DAC_ALIGN_12B_R,Curval);
58              delay_us(dt);
59          }
60      }
61  }
62  */
63  /* USER CODE END 1 */
        以上代码中,DAC_DHR12R1的值一开始是0(DAC输出的三角波的基准电压为0),然后每隔一段时间通过给DAC_DHR12R1递增incval,当递减samples/2次时,给DAC_DHR12R1递减incval,当递减samples/2次时,DAC_DHR12R1再做递增运算,如此反复,最后就得到一个三角波。
        第14~34行,我们通过直接操作寄存器的方法,将递增的值写入到DHR12R1寄存器中。
        第37~61行的方法,我们通过调用HAL库的API函数HAL_DAC_SetValue来设置DHR12R1寄存器的值。两种方法等效。
(2)修改main.c文件
        main.c文件部分代码如下,其中红色字体之间的代码是我们手动添加的:
1   #include "main.h"
2   #include "dac.h"
3   #include "gpio.h"
4
5   /* USER CODE BEGIN Includes */
6   #include "./BSP/Include/led.h"
7   #include "./BSP/Include/key.h"
8   #include "./BSP/Include/delay.h"
9   /* USER CODE END Includes */
10
11  void SystemClock_Config(void);
12  int main(void)
13  {
14    HAL_Init();                                                        /* 初始化HAL库 */
15    if(IS_ENGINEERING_BOOT_MODE())
16    {
17      SystemClock_Config();                        /* 配置系统时钟 */
18    }
19    MX_GPIO_Init();                                         /* 初始化GPIO */
20    MX_DAC1_Init();                                                /* 初始化DAC */
21    /* USER CODE BEGIN 2 */
22    delay_init(209);                                /* 延时初始化 */
23    led_init();                                   /* 初始化LED0 */
24    HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);  /*启动DAC通道 */
25    uint8_t t = 0;
26       uint8_t key;
27    /* USER CODE END 2 */
28   
29      while (1)
30      {
31      /* USER CODE BEGIN 3 */
32          t++;
33          key = key_scan(0);                          /* 按键扫描 */
34          if (key == KEY0_PRES)               /* 高采样率,约100hz波形 */
35          {
36              /* 幅值4095,, 采样点间间隔5us, 2000个采样点,100个波 */
37              dac_triangular_wave(4095, 5, 2000, 100);
38          }
39          else if (key == KEY1_PRES)  /* 低采样率,约100hz波形 */
40          {
41              /* 幅值4095,, 采样点间间隔500us, 20个采样点,100个波 */
42              dac_triangular_wave(4095, 500, 20, 100);
43          }
44          if (t == 10 )                               /* 定时时间到了 */
45          {
46              LED0_TOGGLE();                          /* LED0闪烁 */
47              t = 0;
48          }
49          // HAL_Delay(10);
50          delay_ms(10);                                         /* 延时10ms */
51      }
52    /* USER CODE END 3 */
53  }
        第22行,我们要使用us延时函数,所以要初始化延时函数。
        第24行,自动生成的DAC初始化代码并没有开启DAC,这里我们启动DAC通道,启动后DAC通道才可以工作。
        第34~43行,如果是KEY0按下,则DAC以5us的采样时间间隔进行采样,采样点个数为2000个。如果是KEY1按下,则DAC以500us时间的间隔进行采样,采样点的个数是20个。两种采样参数配置,最后得出的波形约为100Hz。
24.3.6 编译和测试
        编译后进行测试,实验中我们使用示波器测试PA4引脚输出的波形。如下所示,两种波形的频率接近100Hz。
        1)当KEY0按下时:
image062.jpg
图24.3.6. 1三角波1

        2)当KEY1按下时:
image064.jpg
图24.3.6. 2三角2


24.4 DAC输出正弦波实验
        本实验我们来学习使用如何让DAC的通道输出正弦波,其中DAC使用12位右对齐模式,也就是使用到DAC_DHR12Rx [11:0] 位。实验将用定时器7来触发DAC进行转换输出正弦波,并以DMA传输数据。
        本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 17-3 DAC_Sine_Wave。
24.4.1 DAC输出正弦波原理
1. y=sinx
        通过前面输出三角波实验,本节的正弦波实验更加容易理解。正弦函数为y=sinx,一个周期的波形中,0≤x≤2π,-1≤y≤1。如下是一个周期的正弦波形图,实验中可以将2π分为samples等份,每一份的长度是inc,将每一份对应的y值依次写入DAC_DHR12Rx中,DAC_DHR12Rx的值会写入DAC_DORx里,最后由DAC_OUTx输出转换值,输出的波形也类似与一个正弦波。
image066.jpg
图24.4.1. 1 y=sinx函数

2. y=1+sinx
        以上的y值有负数,最小为-1,由于单片机不能直接使用负数来计算,于是我们将上述的公式变换一下:y=1+sinx,如下图所示,这个时候的y值最大为2,最小为0:
image068.jpg
图24.4.1. 2 y=1+sinx函数

3. y=maxval * (1 + sinx)
        装入DAC_DHR12Rx的值不可能最大只有2,所以可以将此波形放大一定的倍数,这里要注意了,因为12位的DAC_DHR12Rx最大只能是2^12=4096,而4096/2=2048,所以最大只能放大2048倍,于是可以得到公式:y=maxval * (1 + sinx),其中maxval是波形放大的倍数(maxval最大2048),波形如下:
image070.jpg
图24.4.1. 3 y=maxval * (1 + sinx)函数

        分析到这里,0≤x≤2π,放大倍数maxval 最大为2048,装入DAC_DHRx的值0≤y<4096,如果想显示正弦波,那么装入DAC_DHR12Rx的值就按照上述的波形图变化。
        实验中,我们通过将2π分成samples等份,每份为inc=2π/samples,x= inc * i,其中0≤i≤samples,这样y=maxval * (1 + sin(inc * i)),此y值就是以上的波形值,将此值写入DAC_DHR12Rx后,经过DAC_OUTx输出的转换值刚好就形成一个正弦波了。从0~2π之间的点是连续变化的,实验中我们不可能把每个点对应的值都写入DAC_DHR12Rx中,我们选择其中的一些点来实验,当取的点数越多时,输出的波形越接近正弦波。
24.4.2 DAC寄存器
        本实验用到的寄存器在前面的实验都有介绍,这里就不再赘述。
24.4.3 DAC的HAL库驱动
        本实验用到的HAL库API函数前面介绍过一部分,下面我们将介绍本实验用到且没有介绍过的几个API函数。
1. HAL_DAC_Start_DMA函数
        使能DAC并开始通道转换,并使用DMA方式传输函数,其声明如下:
HAL_StatusTypeDef HAL_DAC_Start_DMA(DAC_HandleTypeDef *hdac, uint32_t Channel,
uint32_t *pData, uint32_t Length, uint32_t Alignment);
        函数描述:
用于启动DAC通道转换,并使用DMA的方式进行传输。
        函数形参:
形参1是DAC_HandleTypeDef结构体类型指针变量。
形参2用于选择要启动的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。
形参3是使用DAC输出数据缓冲区的指针。
形参4是DAC输出数据的长度。
形参5是指定DAC通道的数据对齐方式,有:DAC_ALIGN_8B_R(8位右对齐)、DAC_ALIGN_12B_L(12位左对齐)和DAC_ALIGN_12B_R(12位右对齐)三种方式。
        函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. HAL_DAC_Stop_DMA函数
        停止DAC的DMA方式函数,其声明如下:
HAL_StatusTypeDef HAL_DAC_Stop_DMA(DAC_HandleTypeDef *hdac, uint32_t Channel);
        函数描述:
用于停止DAC通道转换和DMA传输。
        函数形参:
形参1是DAC_HandleTypeDef结构体类型指针变量。
形参2用于选择要启动的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。
        函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_TIMEx_MasterConfigSynchronization函数
        配置主模式下的定时器触发输出选择函数,其声明如下:
HAL_StatusTypeDef HAL_TIMEx_MasterConfigSynchronization(
TIM_HandleTypeDef *htim, TIM_MasterConfigTypeDef *sMasterConfig);
        函数描述:
用于配置主模式下的定时器触发输出选择。
        函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是TIM_MasterConfigTypeDef结构体类型指针变量,用于配置定时器工作在主/从模式,以及触发输出(TRGO和TRGO2)的选择。
        函数返回值:
HAL_StatusTypeDef枚举类型的值。
24.4.4 硬件设计
1. 例程功能
        使用TIM7触发DAC输出正弦波,通过KEY0/KEY1两个按键,控制DAC1的通道1输出两种正弦波,实验中需要通过示波器接PA4进行观察,实验中通过LED0闪烁来提示程序在正常运行中。实验中,我们只需要把示波器的探头接到DAC1通道1(PA4)引脚,就可以在示波器上显示DAC输出的波形。PA4的引脚排针已经引出,硬件连接如下图所示:
image071.jpg
图24.4.4. 1硬件连接示意图

        本章节实验我们采用硬件触发(TIM7上溢事件)的方式来触发,每当DAC获取一个触发事件,DAC_DORx就会加一个固定的值outdata,此值通过DMA方式来传输。
2. 硬件资源
1)LED灯:LED0
2)按键KEY0和KEY1
3)DAC的通道1
4)DMA2(使用到DMA2数据流6DMA2_Stream6)
5)TIM7
lQLPJxaS6DEPps0uzQFisNIHtlu8-1m_AvHCPMKAogA_354_46.png
表24.4.4. 1硬件资源

3. 原理图
        TIM7、DMA和DAC属于STM32MP157的内部资源,实际上我们只需要软件设置就可以正常工作。
image072.jpg
图24.4.4. 2原理图部分

24.4.5 软件设计
1. 新建和配置工程
(1)配置DAC
        新建工程DAC_Sine_Wave,本实验用到的LED0、KEY0和KEY1可以按照前面的实验来配置。下面我们先配置DAC:
image074.jpg
图24.4.5. 1配置DAC

        以上配置中,注意Trigger选项我们配置为使用片上定时器的内部信号来触发DAC通道,这里选择定时器7。注意的是,DAC这里没有DMA的配置选项,前面实验章节会有DMA配置项,而且以前的STM32CubeMX软件中,DAC也是有DMA配置选项的,这里没有,不知是不是软件的bug?本篇教程使用的STM32CubeIDE是V1.4.0版本,更新的高版本笔者还未去验证,既然这里没有DMA选项,那么后面我们就手动添加DMA相关部分的代码吧。
(2)配置TIM7
image076.jpg
图24.4.5. 2配置TIM7

        以上配置中,TIM7的分频值为209-1,向上计数模式,计数周期(TIMx_ARR的值)为1,自动重载使能,触发事件为上溢更新事件。也就是计数器每计数一次就触发DAC通道,那么触发信号的频率怎么计算呢?还记得我们前面学习基本定时器时,基本定时器的溢出时间计算方法:
T_out=(ARR[15:0]+1)*1/f_(CK_CNT) =   ((ARR[15:0]+1)*(PSC[15:0]+1))/f_(CK_PSC)   
        这里,f_(CK_PSC)为209MHz,ARR[15:0]=1,PSC[15:0]=209-1,所以:
f_out==   f_(CK_PSC)/((ARR[15:0]+1)*(PSC[15:0]+1) )=209MHz/(2*209)=500KHz
        也就是说,由定时器触发的触发信号频率为500KHz。
(3)时钟配置
        关于本节实验用到的LED0、KEY1和KEY0的配置清参考前面相关的实验章节部分。本节使用HSE作为锁相环PLL3的输入时钟源,我们配置APB1为104.5MHz,关于时钟配置也请参考前面实验有关章节部分。
2. 生成工程
        配置好工程后,保存修改,生成工程。本实验我们会用到LED0、KEY0和KEY2,所以将上一章节实验的BSP文件夹直接拷贝到本节实验工程的Src目录下,这样我们就可以直接使用里边的驱动程序:
image088.jpg
图24.4.5. 3生成的工程

3. 添加用户代码
        TIM7和DAC的初始化代码我们前面已经分析过了,这里就不再分析了。
(1)添加DMA初始化代码
        由于本实验DAC转换需要DMA,而前面的STM32CubeMX配置中没有DMA配置选项,那么我们就手动添加DMA相关的初始化代码,在dac.c文件中直接添加DMA的初始化代码。
DMA_HandleTypeDef g_dma_dac_handle;                         /* DMA句柄 */
void dma_init(void)
{
    __HAL_RCC_DMAMUX_CLK_ENABLE();
    __HAL_RCC_DMA2_CLK_ENABLE();
    g_dma_dac_handle.Instance = DMA2_Stream6; /* 使用的DAM2 Stream6 */
    g_dma_dac_handle.Init.Request = DMA_REQUEST_DAC1; /* DAC触发DMA传输 */  
    /* 存储器到外设模式 */  
    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; /* 存储器地址自增 */
    /* 外设数据长度:16位 */  
    g_dma_dac_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    /* 存储器数据长度:16位 */
    g_dma_dac_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    g_dma_dac_handle.Init.Mode = DMA_CIRCULAR;                          /* 循环模式 */
    g_dma_dac_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
    g_dma_dac_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; /* 不使用FIFO */               
    HAL_DMA_Init(&g_dma_dac_handle);                                           /* 初始化DMA */                           
    /* DMA句柄与DAC句柄关联 */
    __HAL_LINKDMA(&hdac1, DMA_Handle1, g_dma_dac_handle);               
    /* 抢占优先级为1,子优先级为0 */
    HAL_NVIC_SetPriority(DMA2_Stream6_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(DMA2_Stream6_IRQn);           /* 使能DMA中断通道 */                              
}
/**
* @brief       DAC DMA中断服务函数
* @param       无
* @retval      无
*/
void DMA2_Stream6_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&g_dma_dac_handle);
}
(2)添加产生正弦波序列函数
        在dac.c文件中添加如下代码,dac_creat_sin_buf函数用于产生正弦波需要的序列,也就是前面我们说的要写入DAC_DHR12R1的值,将这些值存入事先定义好的一个Buffer  g_dac_sin_buf[4095]中。前面我们已经分析了产生正弦波的原理,这里的代码也就是按照前面的原理分析来实现的。
/**
* @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;
    /* 计算增量,π约等于3.1415926,这里将2π分为samples等份 */
    float inc = (2 * 3.1415926) / samples;      
    float outdata = 0;

    for (i = 0; i < samples; i++)
    {
    /* sinx函数上移1个单位,然后放大maxval倍,outdata是写入DAC_DHR12R1的值 */
        outdata = maxval * (1 + sin(inc * i));
        /* 12位的DAC,值为0~4095,上限值为4095 */
        if (outdata > 4095) outdata = 4095;     
        g_dac_sin_buf = outdata;/* 将outdata存入定义的一个Buffer中 */
    }
}
        在dac.h文件中添加如下代码:
/* USER CODE BEGIN Private defines */
void dma_init(void);
void dac_creat_sin_buf(uint16_t maxval, uint16_t samples);
uint16_t g_dac_sin_buf[4096];    /* 发送数据缓冲区 */
/* USER CODE END Private defines */
(3)main.c文件实现代码
        main.c文件的代码如下,我们在标红的字体之间手动添加代码
1   #include "main.h"
2   #include "dac.h"
3   #include "tim.h"
4   #include "gpio.h"
5   /* USER CODE BEGIN Includes */
6   #include "./BSP/Include/led.h"
7   #include "./BSP/Include/key.h"
8   /* USER CODE END Includes */
9   void SystemClock_Config(void);
10  int main(void)
11  {
12    HAL_Init();                               /* HAL库初始化 */
13    if(IS_ENGINEERING_BOOT_MODE())
14    {
15      SystemClock_Config();                 /* 系统时钟配置 */
16    }
17    MX_GPIO_Init();                           /* GPIO初始化 */
18    MX_DAC1_Init();                           /* DAC初始化 */
19    MX_TIM7_Init();                           /* 定时器7初始化 */
20    /* USER CODE BEGIN 2 */
21      uint8_t t = 0;
22      uint8_t key;
23      dma_init();                                 /* DMA初始化 */
24      dac_creat_sin_buf(2048, 100);       /* 产生正弦波序列 */
25      /* 启动DAC转换,并使用DMA传输 */
26 HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t *)g_dac_sin_buf,\
                                                                                                        100, DAC_ALIGN_12B_R);
27      HAL_TIM_Base_Start(&htim7);         /* 使能定时器7 */
28  /* USER CODE END 2 */
29    while (1)
30    {
31      /* USER CODE BEGIN 3 */
32      t++;
33      key = key_scan(0);                      /* 按键扫描 */
34      if (key == KEY0_PRES)                        /* 高采样率,约1Khz波形 */
35      {
36          dac_creat_sin_buf(2048, 100);                            /* 产生正弦波序列 */
37          HAL_DAC_Stop_DMA(&hdac1, DAC_CHANNEL_1);   /* 先停止之前的传输 */
38          /* 500Khz触发频率, 100个点, 得到最高5KHz的正弦波. */
39 HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t *)g_dac_sin_buf,\
                                                                                                        100, DAC_ALIGN_12B_R);
40      }
41      else if (key == KEY1_PRES)                 /* 低采样率 , 约1Khz波形 */
42      {
43          dac_creat_sin_buf(2048, 10);                      /* 产生正弦波序列 */
44          HAL_DAC_Stop_DMA(&hdac1, DAC_CHANNEL_1); /* 先停止之前的传输 */
45          /* 500Khz触发频率, 10个点, 可以得到最高50KHz的正弦波. */
46  HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t*)g_dac_sin_buf,\
                                                                                                                10, DAC_ALIGN_12B_R);
47      }
48      if (t == 10 )                                /* 定时时间到了 */
49      {
50          LED0_TOGGLE();                          /* LED0闪烁 */
51          t = 0;
52      }
53      HAL_Delay(10);                              /* 延时10毫秒 */
54    }
55    /* USER CODE END 3 */
56  }
        前面我们分析过,定时器7产生的触发信号频率为500KHz,如果设置采样点的个数,那么得到不同频率的正弦波。
        第23行,DMA初始化,实验中我们需要用DMA将存在发送数据缓冲区g_dac_sin_buf[4096]里的数据发送到DAC_DHR12R1寄存器中,使用DMA传输数据可以大大提高CPU的利用率。
        第24行,设置产生正弦波序列,这里取100个采集点,所得波形是500KHz/100=5KHz;
        第26行,启动DAC转换和DMA传输。当进入main函数后,会得到一个约5KHz的正弦波;
        第27行,启动定时器7;
        第34~47行,以上代码中,先设置产生正弦波的序列,然后再调用HAL_DAC_Stop_DMA函数将DAC转换以及DMA传输关闭,避免之前的传输干扰,然后再调用HAL_DAC_Start_DMA函数重新开启DAC转换以及DMA传输。
        第34~40行,若KEY0按下,则取100个采集点,得到的正弦波频率约5KHz;
        第41~47行,若KEY1按下,则取10个采集点,得到的正弦波频率约为50KHz。
24.4.6 编译和测试
        编译无报错后,进入Debug模式,测试PA4引脚输出的波形。
        1)进入Debug模式运行程序后的一个波形如下,此波形频率约为5KHz:
image090.jpg
图24.4.6. 1进入Debug模式时的第一个波形

        2)KEY0按下后的波形得到频率约为5KHz的正弦波形:
image092.jpg
图24.4.6. 2 KEY0按下后的波形

        3)KEY1按下后得到频率约为50KHz的正弦波形:
image094.jpg
图24.4.6. 3 KEY1按下后的波形

        经过验证可以发现,取的点数越多,波形就越接近正弦波。

阿莫论坛20周年了!感谢大家的支持与爱护!!

一只鸟敢站在脆弱的枝条上歇脚,它依仗的不是枝条不会断,而是自己有翅膀,会飞。
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-3-29 12:55

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表