我们介绍过了 stm32f1 的模数转换器 adc,接下来我们学习下stm32f1 的数模转换器 dac。要实现的功能是:通过 k_up 与k_down 按键控制 stm32f1 dac1 输出电压,通过串口将 dac1 输出的电压值打印显示,d1 指示灯闪烁提示系统运行。学习时可以参考《stm32f10x 中文参考手册》-12 数模转换器(dac)章节,特别是寄存器介绍部分。
stm32f1 dac简介
dac(digital 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功能
● 外部触发转换
● 输入参考电压 vref+
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 左右。
本文我们介绍的是不使用硬件触发(tenx=0),其转换时序图如下图所示:
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 章节内容,里面有详细的讲解。如果看不懂的可以暂时放下,因为我们使用的是库函数开发。
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_trigger=dac_trigger_none; //不使用触发功能ten1=0dac_initstructure.dac_wavegeneration=dac_wavegeneration_none;//不使用波形发生dac_initstructure.dac_lfsrunmask_triangleamplitude=dac_lfsrunmask_bit0;//屏蔽、幅值设置dac_initstructure.dac_outputbuffer=dac_outputbuffer_disable ;//dac1 输出缓存关闭 boff1=1dac_init(dac_channel_1,&dac_initstructure); //初始化dac通道1
(3)使能 dac 的输出通道
初始化 dac 后,我们就需要开启它,使能 dac 输出通道的库函数为:
void dac_cmd(uint32_t dac_channel, functionalstate newstate);
例如:使能 dac 通道1输出,那么调用函数如下:
dac_cmd(dac_channel_1, enable); //使能 dac 通道 1
(4)设置 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 对应的通道输出模拟电压了。
本实验使用到硬件资源如下:
(1)d1 指示灯
(2)k_up 和 k_down按键
(3)串口 1
(4)dac 的通道 1
d1指示灯、k_up 和 k_down 按键、串口 1 电路在前面章节都介绍过,这里就不多说,至于 dac 的通道1它属于 stm32f1 芯片内部的资源,对应芯片的pa4引脚。dac 模块电路如下图所示:
如果直接使用 stm32的dac通道输出信号给负载,可直接连接 pa4 或者pa5引脚。
我们开发板上还集成信号放大电路,如下所以:
如果需要输出较强的信号,可将负载接在 dac0 上,这个对应开发板 dac 模块的 p23 插针,旁边有对应的丝印注释。一定要注意需将 da_vcc连接5v电源,否则输出无效。对于 pwm1 可使用 stm32 的定时器来模拟输出,这个大家可以对应学习。如果参考电压源有误差, 那么 dac 输出的电压可能也会存在有一点误差。
如果需要使用 adc 来检测dac 输出电压, 可以使用杜邦线将对应的 dac输出通道管脚与 adc 通道管脚连接。
d1指示灯用来提示系统运行状态,k_up 按键用来增加 dac输入值,k_down按键用来减小 dac 输入值,输入值的改变将控制 dac1_out 电压输出。通过串口1 将 dac1_out 输出的电压值打印出来。
实现的功能是:通过 k_up 与 k_down 按键控制 stm32f1 dac1 输出电压,通过串口将 dac1输出的电压值打印显示,d1 指示灯闪烁提示系统运行。
程序框架如下:
(1)初始化 dac 通道1相关参数
(2)编写主函数
前面介绍 dac 配置步骤时, 就已经讲解如何初始化 dac。下面我们打开 “dac 数模转换实验”工程,在 app 工程组中添加dac.c 文件(里面包含了 dac 驱动程序),在 stdperiph_driver 工程组中添加stm32f10x_dac.c库文件。dac 操作的库函数都放在 stm32f10x_dac.c和stm32f10x_dac.h 文件中,所以使用到 dac 就必须加入 stm32f10x_dac.c文件,同时还要包含对应的头文件路径。这里我们分析几个重要函数,其他部分程序大家可以打开工程查看。
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_pin=gpio_pin_4;//dac_1 gpio_initstructure.gpio_speed=gpio_speed_50mhz; gpio_initstructure.gpio_mode=gpio_mode_ain;//模拟量输入 gpio_init(gpioa,&gpio_initstructure); dac_initstructure.dac_trigger=dac_trigger_none; //不使用触发功能 ten1=0 dac_initstructure.dac_wavegeneration=dac_wavegeneration_none;//不使用波形发生 dac_initstructure.dac_lfsrunmask_triangleamplitude=dac_lfsrunmask_bit0;//屏蔽、幅值设置 dac_initstructure.dac_outputbuffer=dac_outputbuffer_disable ; //dac1 输出缓存关闭 boff1=1 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 是类似的。
主函数
编写好 dac 通道 1 的初始化函数后, 接下来就可以编写主函数了, 代码如下:
/***************************************************************** 函 数 名 : main* 函数功能 : 主函数* 输 入 : 无* 输 出 : 无*****************************************************************/int main(){ u8 i=0; u8 key; int dac_value=0; u16 dacval; float dac_vol; systick_init(72); nvic_prioritygroupconfig(nvic_prioritygroup_2); //中断优先级分组 分2 组 led_init(); usart1_init(9600); key_init(); dac1_init(); while(1) { key=key_scan(0); if(key==key_up) { dac_value+=400; if(dac_value>=4000) { dac_value=4095; } dac_setchannel1data(dac_align_12b_r,dac_value); } else if(key==key_down) { dac_value-=400; if(dac_value<=0) { dac_value=0; } dac_setchannel1data(dac_align_12b_r,dac_value); } i++; if(i%20==0) { led1=!led1; } if(i%50==0) { dacval=dac_getdataoutputvalue(dac_channel_1); dac_vol=(float)dacval*(3.3/4096); printf(输出dac 电压值为:%.2fv,dac_vol); } delay_ms(10); }}
主函数实现的功能很简单,首先调用之前编写好的硬件初始化函数,包括systick系统时钟, 中断分组, led初始化等。然后调用我们前面编写的dac1_init函数。最后进入 while 循环,调用 key_scan 函数,不断检测 k_up 和 k_down 按键是否按下,如果 k_up 按键按下,调用 dac_setchannel1data 函数增加 dac1的输入值;如果 k_down 按键按下,调用 dac_setchannel1data 函数减小 dac1的输入值。间隔 500ms 调用 dac_getdataoutputvalue 函数读取 dac1 最后一次的输入值, 根据dac电压计算公式即可知道dac1输出的电压大小, 同时通过printf打印出电压值。d1 指示灯间隔200ms 闪烁,提示系统正常运行。
将工程程序编译后下载到开发板内,可以看到 d1 指示灯不断闪烁,表示程序正常运行。同时打印 dac 通道 1(pa4)输出的电压值,当按下 k_up 按键输出电压增大,当按下 k_down 按键输出电压减小。如果想在串口调试助手上看到输出信息,可以打开“串口调试助手”,首先勾选下标号 1 dtr 框,然后再取消勾选。这是因为此串口助手启动时会把系统复位住,通过 dtr 状态切换下即可。然后设置好波特率等参数后,串口助手上即会收到 printf 发送过来的信息。(串口助手上先勾选下标号1 dtr框,然后再取消勾选)如下图所示:
实验说明:可以使用万用表电压档来测量 pa4 引脚的输出电压,将测量的电压值与打印出的电压值对比下,其实精度还是不错的。
如果发现 dac 输出电压最高达不到 3.3v,那可能是你的电源并没有达到标准的 3.3v。我们知道 dac 输出电压的范围取决于参考电压 vref+,参考电压我们已经将它连接在 vdda 上,即 3.3v,所以如果电源不稳定,dac 输出的电压可能也会存在一点误差。
我们要如何去避免继电保护中的不正确动作呢?
互联网巨头正在把AI技术快速加入云端 加快AI云落地
面对芯片荒,行业须警惕从“一场灾难”到“另外一场灾难”
卡萨帝智能家居首个高端智慧成套家电
基于PIC18F452的测频仪设计[图]
STM32教程实例-DAC数模转换实验
手电筒线性IC 线性降压恒流驱动芯片 AP5152
谐波电流失真率和畸变的关系
欧洲科技企业联盟向微软发起反竞争投诉 30多家欧洲公司投诉微软垄断
专业薄膜表面瑕疵在线检测系统的原理及功能
吉林省委书记巴音朝鲁点赞农村淘宝:推动农产品上行 让农村生活更美好
正式发布!PPEC-86CA3A移相全桥控制芯片,样片申请开启
2017年中国半导体将加速整并 建构虚拟IDM产业生态
400G QSFP-DD AOC线缆优势介绍
可穿戴医疗的三大痛点与六大突破要点
一种高可靠性软件测试方案
AR远程协同平台让工厂管理更加高效便捷
存储体系结构有哪些
华为P10闪存门最新消息:开发者推出检测软件,果然有需求就有市场
开关管MOSFET的损耗分析及其优化方法