(1)实验平台普中STM32F103朱雀、玄武开发板前面章节中我们介绍了 STM32F1 的模数转换器 ADC 这一章我们来学习下STM32F1 的数模转换器 DAC。 本章要实现的功能是 通过 KEY_UP 与 KEY1 按键控制 STM32F1 DAC1 输出电压 通过串口将 DAC1 输出的电压值打印显示 DS0 指示灯闪烁提示系统运行。 学习本章可以参考《STM32F10x 中文参考手册》 -12 数模转换器DAC 章节 特别是寄存器介绍部分。 本章分为如下几部分内容31.1 STM32F1 DAC 介绍31.1.1 STM32F1 DAC 简介31.1.2 STM32F1 DAC 结构框图31.2 STM32F1 DAC 配置步骤31.3 硬件设计31.4 软件设计31.4.1 DAC 通道 1 初始化函数31.4.2 主函数31.5 实验现象课后作业31.1 STM32F1 DAC 介绍31.1.1 STM32F1 DAC 简介DACDigital to analog converter 即数字模拟转换器 它可以将数字信号转换为模拟信号。 它的功能与 ADC 相反。 在常见的数字信号系统中 大部分传感器信号被转化成电压信号 而 ADC 把电压模拟信号转换成易于计算机存储、处理的数字编码 由计算机处理完成后 再由 DAC 输出电压模拟信号 该电压模拟信号常常用来驱动某些执行器件 使人类易于感知。 如音频信号的采集及还原就是这样一个过程。STM32F1 DAC 模块是 12 位电压输出数模转换器 它可以配置为 8 位或 12位模式 也可以与 DMA 控制器配合使用。 DAC 工作在 12 位模式下 数据可以采用左对齐或右对齐。 DAC 工作在 8 位模式下 数据只有右对齐方式。 DAC 有两个输出通道 每个通道各有一个转换器。 在 DAC 双通道模式下 每个通道可以单独进行转换 当两个通道组合在一起同步执行更新操作时 也可以同时进行转换。DAC 可通过一个输入参考电压引脚 VREF 与 ADC 共享 来提高转换后的数据精度。STM32F1 DAC 主要特性● 2 个 DAC 转换器 每个转换器对应 1 个输出通道● 8 位或者 12 位单调输出● 12 位模式下数据左对齐或者右对齐● 同步更新功能● 噪声波形生成● 三角波形生成● 双 DAC 通道同时或者分别转换● 每个通道都有 DMA 功能● 外部触发转换● 输入参考电压 VREF31.1.2 STM32F1 DAC 结构框图STM32F1 DAC 拥有这么多功能 是由 DAC 内部结构决定。 要更好的理解STM32F1 的 DAC 就需要了解它内部的结构。 如下图所示 大家也可以查看《STM32F10x 中文参考手册》 -12 数模转换器DAC -12.3 章节内容我们把 DAC 结构框图分成 5 个子模块 按照顺序依次进行简单介绍。1 标号 1 电压输入引脚同 ADC 一样 VDDA 与 VSSA 是 DAC 模块的供电引脚 而 VREF是 DAC 模块的参考电压 开发板上已经将 VREF连接到 VDDA 所以参考电压范围是 0-3.3V。2 标号 2 DAC 转换DAC 输出是受 DORx 寄存器直接控制的 但是我们不能直接往 DORx 寄存器写入数据 而是通过 DHRx 间接的传给 DORx 寄存器 实现对 DAC 输出的控制。如果未选择硬件触发 DAC_CR 寄存器中的 TENx 位复位 那么经过一个 APB1时钟周期后 DAC_DHRx 寄存器中存储的数据将自动转移到 DAC_DORx 寄存器。但是 如果选择硬件触发置位 DAC_CR 寄存器中的 TENx 位 且触发条件到来将在三个 APB1 时钟周期后进行转移。当 DAC_DORx 加载了 DAC_DHRx 内容时 模拟输出电压将在一段时间tSETTLING 后可用 具体时间取决于电源电压和模拟输出负载。 我们可以从STM32F103ZET6 的数据手册查到的典型值为 3us 最大是 4us。 所以 DAC 的转换速度最快是 250K 左右。本章我们介绍的是不使用硬件触发TENx0 其转换时序图如下图所示DHRx 内装载着我们要输出的数据 前面我们提到 STM32F1 的 DAC 支持8/12 位模式 8 位模式的时候数据是固定的右对齐的 而 12 位模式数据可以设置左对齐/右对齐。 对于 DAC 单通道 x 总共有 3 种情况① 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 位右对齐方式 所以采用第 3种情况。每个 DAC 通道都具有 DMA 功能。 两个 DMA 通道用于处理 DAC 通道的 DMA请求。 当 DMAENx 位置 1 时 如果发生外部触发而不是软件触发 则将产生 DAC DMA 请求。 DAC_DHRx 寄存器的值随后转移到 DAC_DORx 寄存器。 在双通道模式下 如果两个 DMAENx 位均置 1 则将产生两个 DMA 请求。 如果只需要一个 DMA 请求 应仅将相应 DMAENx 位置 1。 这样 应用程序可以在双通道模式下通过一个 DMA 请求和一个特定 DMA 通道来管理两个 DAC 通道。由于 DAC DMA 请求没有缓冲队列。 这样 如果第二个外部触发到达时尚未收到第一个外部触发的确认 将不会发出新的请求 并且 DAC_SR 寄存器中的DAM 通道下溢标志 DMAUDRx 将置 1 以报告这一错误状况。 DMA 数据传输随即禁止 并且不再处理其他 DMA 请求。 DAC 通道仍将继续转换旧有数据。 这时软件应通过写入“ 1” 来将 DMAUDRx 标志清零 将所用 DMA 数据流的 DMAEN 位清零 并重新初始化 DMA 和 DAC 通道 以便正确地重新开始 DMA 传输。 软件应修改 DAC 触发 转换频率或减轻 DMA 工作负载 以避免再次发生 DMA 下溢。最后 可通过使能 DMA 数据 传输和转换触发来继续完成 DAC 转换。对于各 DAC 通道 如果使能 DAC_CR 寄存器中相应的 DMAUDRIEx 位 还将产生中断。 本章我们没有使用到 DMA 所以将其相应位设置为 0 即可。3 标号 3 DAC 触发选择如果 TENx 控制位置 1 可通过外部事件定时计数器、 外部中断线 触发转换。 TSELx[2:0]控制位将决定通过 8 个可能事件中的哪一个来触发转换 外部触发源如下图所示每当 DAC 接口在所选定时器 TRGO 输出或所选外部中断线 9 上检测到上升沿时 DAC_DHRx 寄存器中存储的最后一个数据即会转移到 DAC_DORx 寄存器中。 发生触发后再经过三个 APB1 周期 DAC_DORx 寄存器将会得到更新。如果选择软件触发 一旦 SWTRIG 位置 1 转换即会开始。 DAC_DHRx 寄存器的内容只需一个 APB1 时钟周期即可转移到 DAC_DORx 寄存器 加载完成后 SWTRIG 即由硬件复位。4 标号 4 DAC 输出DAC_OUTx 就是 DAC 的输出通道 DAC1_OUT 对应 PA4 引脚 DAC2_OUT 对应PA5 引脚。 要让 DAC 通道正常输出 需将 DAC_CR 寄存器中的相应 ENx 位置 1这样就可接通对应 DAC 通道。 经过一段启动时间 tWAKEUP 后 DAC 通道被真正使能。 使能 DAC 通道 x 后 相应 GPIO 引脚 PA4 或 PA5 将自动连接到模拟转换器输出(DAC_OUTx)。 为了避免寄生电流消耗 应首先将 PA4 或 PA5 引脚配置为模拟输入模式 (AIN)。当 DAC 的参考电压为 Vref的时候 DAC 的输出电压是线性的从 0~Vref 12 位模式下 DAC 输出电压与 Vref以及 DORx 的计算公式如下DAC 集成了两个输出缓冲器 可用来降低输出阻抗并在不增加外部运算放大器的情况下直接驱动外部负载。 通过 DAC_CR 寄存器中的相应 BOFFx 位 可使能或禁止各 DAC 通道输出缓冲器。 STM32F1 的 DAC 输出缓存做的有些不好 如果使能的话 虽然输出能力增强了一些 但是输出没法到 0 这是个很严重的问题。 所以通常我们不使用输出缓存 即设置 BOFFx 位为 1。DAC 还可以生成噪声和三角波。 生成可变振幅的伪噪声 可使用 LFSR线性反馈移位寄存器 。 将 WAVEx[1:0] 置为 “ 01” 即可选择生成噪声。 LFSR 中的预加载值为 0xAAA。 在每次发生触发事件后 经过三个 APB1 时钟周期 该寄存器会依照特定的计算算法完成更新。LFSR 值可以通过 DAC_CR 寄存器中的 MAMPx[3:0] 位来部分或完全屏蔽在不发生溢出的情况下 该值将与 DAC_DHRx 的内容相加 然后存储到 DAC_DORx寄存器中。 如果 LFSR 为 0x0000 将向其注入“ 1” 防锁定机制 。 可以通过复位 WAVEx[1:0] 位来将 LFSR 波形产生功能关闭。 要生成噪声 必须通过将DAC_CR 寄存器中的 TENx 位置 1 来使能 DAC 触发。将 WAVEx[1:0] 置为“ 10”即可选择 DAC 生成三角波。 振幅通过 DAC_CR 寄存器中的 MAMPx[3:0] 位进行配置。 每次发生触发事件后 经过三个 APB1 时钟周期 内部三角波计数器将会递增。 在不发生溢出的情况下 该计数器的值将与DAC_DHRx 寄存器内容相加 所得总和将存储到 DAC_DORx 寄存器中。 只要小于MAMPx[3:0] 位定义的最大振幅 三角波计数器就会一直递增。 一旦达到配置的振幅 计数器将递减至零 然后再递增 以此类推。 可以通过复位 WAVEx[1:0] 位来将三角波产生功能关闭。要生成三角波 必须通过将 DAC_CR 寄存器中的 TENx 位置 1 来使能 DAC触发。 MAMPx[3:0] 位必须在使能 DAC 之前进行配置 否则将无法更改。 本章我们不使用噪声和三角波功能 所以可以将相应的寄存器位清零。由于篇幅限制 本章并没有对 DAC 相关寄存器进行介绍 大家可以参考《STM32F10x 中文参考手册》 -12 数模转换器DAC -12.5 章节内容 里面有详细的讲解。 如果看不懂的可以暂时放下 因为我们使用的是库函数开发。31.2 STM32F1 DAC 配置步骤接下来我们介绍下如何使用库函数对 DAC 进行配置。 这个也是在编写程序中必须要了解的。 具体步骤如下 DAC 相关库函数在 stm32f10x_dac.c 和stm32f10x_dac.h 文件中1 使能端口及 DAC 时钟 设置引脚为模拟输入DAC 的两个通道对应的是 PA4、 PA5 引脚 这个在芯片数据手册内可以查找到如下图。 因此使用 DAC 某个通道输出的时候需要使能 GPIOA 端口和 DAC 时钟DAC模块时钟是由 APB1 提供 并且还要将对应通道的引脚配置为模拟输入模式。这里需要特别说明一下 虽然 DAC 引脚设置为输入 但是如果使能 DACx 通道后相应的管脚会自动连接在 DAC 模拟输出上,在前面介绍框图时也提到了。例如要让 DAC1_OUT 输出 其对应的是 PA4 引脚 所以使能时钟代码如下RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);// 使 能 GPIOA 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);//使能 DAC 时钟配置 PA4 引脚为模拟输入模式 代码如下GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN;//模拟输入 GPIO_Init(GPIOA, GPIO_InitStructure);//初始化2 初始化 DAC 设置 DAC 工作模式要使用 DAC 必须对其相关参数进行设置 包括 DAC 通道 1 使能、 DAC 通道 1 输出缓存关闭、 不使用触发、 不使用波形发生器等设置 该部分设置通过DAC 初始化函数 DAC_Init 完成的void DAC_Init(uint32_t DAC_Channel, DAC_InitTypeDef* DAC_InitStruct);函数中第一个参数是用来确定哪个 DAC 通道 例如 DAC 通道 1 DAC_Channel_1 第二个参数是一个结构体指针变量 结构体类型是DAC_InitTypeDef 其内包含了 DAC 初始化的成员变量。 下面我们简单介绍下它的成员typedef struct { uint32_t DAC_Trigger; //DAC 触发选择 uint32_t DAC_WaveGeneration; //DAC 波形发生 uint32_t DAC_LFSRUnmask_TriangleAmplitude; //屏蔽/幅值选择器 uint32_t DAC_OutputBuffer; //DAC 输出缓存 }DAC_InitTypeDef;DAC_Trigger设置是否使用触发功能。 前面介绍框图时已经说了 DAC 具有多个触发源 有定时器触发 外部中断线 9 触发 软件触发和不使用触发。 其配置参数可在 stm32f10x_dac.h 找到 如下例如 不使用触发功能 所以参数为 DAC_Trigger_None。DAC_WaveGeneration设置是否使用波形发生。 在前面框图介绍也讲过 其配置参数可在 stm32f10x_dac.h 找到 如下例如 不使用波形发生功能 所以参数为 DAC_WaveGeneration_None。DAC_LFSRUnmask_TriangleAmplitude设置屏蔽/幅值选择器。 这个变量只在使用波形发生器的时候才有用通常我们设置为0即可值为DAC_LFSRUnmask_Bit0。 其他配置参数同样可在 stm32f10x_dac.h 找到。DAC_OutputBuffer设置输出缓存控制位。 通常我们不使用输出缓存功能所以配置参数为 DAC_OutputBuffer_Disable。 如果使用的话可以配置为使能DAC_OutputBuffer_Enable。了解结构体成员功能后 就可以进行配置 本章实验配置代码如下DAC_InitTypeDef DAC_InitStructure; DAC_InitStructure.DAC_TriggerDAC_Trigger_None; //不使用触发功能 TEN10 DAC_InitStructure.DAC_WaveGenerationDAC_WaveGeneration_None;// 不使用波形发生 DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitudeDAC_LFSRUnmask_Bit0;// 屏蔽、 幅值设置 DAC_InitStructure.DAC_OutputBufferDAC_OutputBuffer_Disable ; //DAC1 输出缓存关闭 BOFF11 DAC_Init(DAC_Channel_1,DAC_InitStructure); //初始化 DAC 通道 13 使能 DAC 的输出通道初始化 DAC 后 我们就需要开启它 使能 DAC 输出通道的库函数为void DAC_Cmd(uint32_t DAC_Channel, FunctionalState NewState);例如 使能 DAC 通道 1 输出 那么调用函数如下DAC_Cmd(DAC_Channel_1, ENABLE); //使能 DAC 通道 14 设置 DAC 的输出值通过前面 4 个步骤的设置 DAC 就可以开始工作了 如果我们使用 12 位右对齐数据格式 我们通过设置 DHR12R1 就可以在 DAC 输出引脚PA4 得到不同的电压值了。 设置 DHR12R1 的库函数是DAC_SetChannel1Data(DAC_Align_12b_R, 0); //12 位右对齐数据格式设置 DAC 值第一个参数是设置数据对其方式 可以为 12 位右对齐 DAC_Align_12b_R 12 位左对齐 DAC_Align_12b_L 以及 8 位右对齐 DAC_Align_8b_R 方式。第二个参数就是 DAC 的输入值 初始化时我们一般设置输入值为 0。库函数中 还提供一个读取 DAC 对应通道最后一次转换的数值 函数是uint16_t DAC_GetDataOutputValue(uint32_t DAC_Channel);参数 DAC_Channel 用于选择读取的 DAC 通道。 可以为 DAC_Channel_1 和DAC_Channel_2。将以上几步全部配置好后 我们就可以使用 DAC 对应的通道输出模拟电压了。31.3 硬件设计本实验使用到硬件资源如下1 DS0 指示灯2 KEY_UP 和 KEY1 按键3 串口 14 DAC 的通道 1DS0 指示灯、 KEY_UP 和 KEY1 按键、 串口 1 电路在前面章节都介绍过 这里就不多说 至于 DAC 的通道 1 它属于 STM32F1 芯片内部的资源 对应芯片的 PA4引脚。 DAC 模块电路如下图所示如果需要使用 ADC 来检测 DAC 输出电压 可以使用一根导线将 STM_DAC板子丝印是 DAC 与 STM_ADC 板子丝印是 ADC 短接。 如下DS0 指示灯用来提示系统运行状态 KEY_UP 按键用来增加 DAC 输入值 KEY1按键用来减小 DAC 输入值 输入值的改变将控制 DAC 电压输出。 通过串口 1 将DAC 输出的电压值打印出来。31.4 软件设计本章所要实现的功能是 通过 KEY_UP 与 KEY1 按键控制 STM32F1 DAC1 输出电压 通过串口将 DAC1 输出的电压值打印显示 DS0 指示灯闪烁提示系统运行。程序框架如下1 初始化 DAC 通道 1 相关参数2 编写主函数前面介绍 DAC 配置步骤时 就已经讲解如何初始化 DAC。 下面我们打开“\4--实验程序\1--基础实验\23-DAC 数模转换实验” 工程 在 APP 工程组中可以看到添加了 dac.c 文件里面包含了 DAC 驱动程序 在 StdPeriph_Driver 工程组中添加了 stm32f10x_dac.c 库文件。 DAC 操作的库函数都放在 stm32f10x_dac.c和 stm32f10x_dac.h 文件中 所以使用到 DAC 就必须加入 stm32f10x_dac.c 文件同时还要包含对应的头文件路径。这里我们分析几个重要函数 其他部分程序大家可以打开工程查看。31.4.1 DAC 通道 1 初始化函数要使用 DAC 我们必须先对它进行配置。 初始化代码如下/******************************************************************************* * 函 数 名 : DAC1_Init * 函数功能 : DAC1初始化函数 * 输 入 : 无 * 输 出 : 无 *******************************************************************************/ void DAC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; DAC_InitTypeDef DAC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能GPIOA时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);//使能DAC时钟 GPIO_InitStructure.GPIO_PinGPIO_Pin_4;//DAC_1 GPIO_InitStructure.GPIO_SpeedGPIO_Speed_50MHz; GPIO_InitStructure.GPIO_ModeGPIO_Mode_AIN;//模拟量输入 GPIO_Init(GPIOA,GPIO_InitStructure); DAC_InitStructure.DAC_TriggerDAC_Trigger_None; //不使用触发功能 TEN10 DAC_InitStructure.DAC_WaveGenerationDAC_WaveGeneration_None;//不使用波形发生 DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitudeDAC_LFSRUnmask_Bit0;//屏蔽、幅值设置 DAC_InitStructure.DAC_OutputBufferDAC_OutputBuffer_Disable ; //DAC1输出缓存关闭 BOFF11 DAC_Init(DAC_Channel_1,DAC_InitStructure); //初始化DAC通道1 DAC_SetChannel1Data(DAC_Align_12b_R, 0); //12位右对齐数据格式设置DAC值 DAC_Cmd(DAC_Channel_1, ENABLE); //使能DAC通道1 }在 DAC1_Init()函数中 首先使能 GPIOA 端口和 DAC 时钟 并配置 PA4 为模拟输入模式。 然后初始化 DAC_InitStructure 结构体。 最后开启 DAC_Channel_1。在初始化函数中还调用了 DAC_SetChannel1Data 函数 设置数据格式为 12 位右对齐 并且设置 DAC 初始值为 0。 这一过程在前面步骤介绍中已经提了。 如果你会使用 DAC 的通道 1 对于 DAC 的通道 2 是类似的。31.4.2 主函数编写好 DAC 通道 1 的初始化函数后 接下来就可以编写主函数了 代码如下#include system.h #include SysTick.h #include led.h #include usart.h #include key.h #include dac.h /******************************************************************************* * 函 数 名 : main * 函数功能 : 主函数 * 输 入 : 无 * 输 出 : 无 *******************************************************************************/ int main() { u8 i0; u8 key; int dac_value0; u16 dacval; float dac_vol; SysTick_Init(72); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组 分2组 LED_Init(); USART1_Init(115200); KEY_Init(); DAC1_Init(); while(1) { keyKEY_Scan(0); if(keyKEY_UP_PRESS) { dac_value400; if(dac_value4000) { dac_value4095; } DAC_SetChannel1Data(DAC_Align_12b_R,dac_value); } else if(keyKEY1_PRESS) { dac_value-400; if(dac_value0) { dac_value0; } DAC_SetChannel1Data(DAC_Align_12b_R,dac_value); } i; if(i%200) { LED1!LED1; } if(i%500) { dacvalDAC_GetDataOutputValue(DAC_Channel_1); dac_vol(float)dacval*(3.3/4096); printf(输出DAC电压值为%.2fV\r\n,dac_vol); } delay_ms(10); } }主函数实现的功能很简单 首先调用之前编写好的硬件初始化函数 包括SysTick 系统时钟 中断分组 LED 初始化等。然后调用我们前面编写的 DAC1_Init函数。 最后进入 while 循环 调用 KEY_Scan 函数 不断检测 KEY_UP 和 KEY1 按键是否按下 如果 KEY_UP 按键按下 调用 DAC_SetChannel1Data 函数增加 DAC1的输入值 如果 KEY1 按键按下 调用 DAC_SetChannel1Data 函数减小 DAC1 的输入值。 间隔 500ms 调用 DAC_GetDataOutputValue 函数读取 DAC1 最后一次的输入值 根据 DAC 电压计算公式即可知道 DAC1 输出的电压大小 同时通过 printf打印出电压值。 DS0 指示灯间隔 200ms 闪烁 提示系统正常运行。31.5 实验现象将工程程序编译后下载到开发板内 可以看到 DS0 指示灯不断闪烁 表示程序正常运行。 同时打印 DAC 通道 1PA4 输出的电压值 当按下 KEY_UP 按键输出电压增大 当按下 KEY1 按键输出电压减小。 如果想在串口调试助手上看到输出信息 可以打开“\5--开发工具\4-常用辅助开发软件\串口调试助手\串口调试助手丁丁 ” 内串口调试助手 实验现象如下 前提一定要连接好线路 USB 线一端连接电脑 另一端连接开发板“USB 转串口模块” 上的 USB 下载口并且在“USB 转 TTL电源” 模块上 P4 端子短接片已插上实验说明 可以使用万用表电压档来测量 DAC 或 PA4 引脚的输出电压 将测量的电压值与打印出的电压值对比下 其实精度还是不错的。如果发现 DAC 输出电压最高达不到 3.3V 那可能是你的电源并没有达到标准的 3.3V。 我们知道 DAC 输出电压的范围取决于参考电压 VREF 参考电压我们已经将它连接在 VDDA 上 即 3.3V 所以如果电源不稳定 DAC 输出的电压可能也会存在一点误差。测量位置如下课后作业1 使用 ADC1_IN1 检测 DAC1 输出的电压值 通过串口打印输出。 温馨提示 将 ADC 数模转换实验程序与本章实验结合 注意 使用一根导线将插针上的 ADC 与 DAC 短接 如下图所示