1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#
第三十四章 PWM DAC实验
前面的章节,我们介绍了STM32F1自带DAC模块的使用,虽然STM32F103ZET6具有内部DAC,但是也仅仅只有两条DAC通道,而STM32还有其它的很多型号是没有DAC的。通常情况下,采用专用的D/A芯片来实现,但是这样就会带来成本的增加。不过STM32所有的芯片都有PWM输出,并且PWM输出通道很多,资源丰富。因此,我们可以使用PWM + 简单的RC滤波来实现DAC的输出从而节省成本。
本章我们将向大家介绍如何使用STM32F1的PWM来设计一个DAC。我们将使用按键(或USMART)控制STM32F1的PWM输出,从而控制PWMDAC的输出电压,通过ADC1的通道1采集PWM DAC的输出电压,并在LCD模块上面显示ADC获取到的电压值以及PWM DAC的设定输出电压值等信息本章分为如下几个小节:
34.1 PWM DAC技术的实现原理
34.2 硬件设计
34.3 程序设计
34.4 下载验证
34.1 PWM DAC技术的实现原理
DAC工作过程是将源电压按照8位、12位、16位等分辨率进行分割,其输出的电压是最小精度LSB(即1/28、1/212、1/216等)的整数倍,这就是DAC输出的电压。
我们来分析一下PWM波形的特性。PWM信号可以被分解为一个直流分量和一个占空比固定,但是平均幅度为零的方波,如图34.1.1.1所示。
图34.1.1.1 PWM波形的等效分解
如果使PWM信号的占空比随时间改变,那么其直流分量随之改变,信号滤除交流分量后,将会输出幅度变化的模拟信号。因此通过改变PWM 信号的占空比,可以产生不同的模拟信号。这种技术称之为PWM DAC。
PWM是周期固定,占空比可调的数字信号。在实际电路中,典型的PWM波形,如图34.1.1.2所示。
图34.1.1.2 实际电路典型PWM波形图
下面根据高数与信号与系统课程的知识我们作一个简单的推导,感兴趣的同学可以查阅对应的知识,如果不感兴趣,直接跳过推导过程,看最后的结论即可。
我们可以把PWM波形用分段函数①表示出来。占空比可以用②的表达式来表示。
公式⑩正好验证了图34.1.1.1的PWM等效原理。由此我们可知PWM的输出波形为一个与占空比有关的直流等效信号,同时伴有多个不同频率的信号的叠加。如果能把这些频率信号尽可能过滤掉,那么我们通过调整PWM的占空比即可方便实现我们需要的DAC结果,即VDAC=(VH-VL)*p。
分辨率也是DAC一个重要的参数,它可以表示DAC输出的最小精度。存在两个主要误差源影响PWM方式DAC分辨率。首先,PWM信号的占空比只能表示有限的分辨率。这是因为STM32的PWM的占空比是输出比较寄存器CCRx与TIMx_CNT进行比较的结果,而CCRx在STM32F1系列中最多能设置为16位。那么很显然地,用PWM实现的DAC分辨率就与TIMx_CNT有关,即定时器的时钟频率越高则CCRx可以设置的值越多,分辨率相应地越高。但由于定时器最高时钟是72M,这也会导致分辨率越高,DAC的速度越慢。
第二个误差源是PWM信号中不期望的谐波分量产生的峰峰值。前面PWM的频域展开公式⑩说明PWM信号需要通过滤波器才能输出一个纹波较小的直流信号,但实际上对于简单设计的滤波器对交流信号的过滤能力是有限的,所以输出信号还会带有一定的交流成份。
根据公式⑧,将k=1代入我们可以算出PWM的一次谐波幅度:
当sin(πp-π)=1时滤波器需要达到衰减峰值,可知PWM占空比为50%时,一次谐波的幅度最大。为了减少这个基波的影响,我们希望滤波器在这个最大幅度下也能把基波的交流影响衰减到1/2LSB以下,即后外围滤波器至少需要满足以下条件才能避免DAC输出干扰过大:
根据公式可知=,当DAC为12位精度时,代入Y=12可知我们设计的滤波器需要衰减74dB以上,当为8位精度时,衰减需要达到50dB。
我们知道一阶RC电路截止频率计算公式为:
把电容等效成一个电阻,对于一阶分压时电压的等效衰减的表达式可以是:
根据以上公式就能很好地设计一个满足我们需要的滤波器参数了。为了实现低成本的RC电路,我们使用两个一阶RC电路串联起来作为滤波器。
STM32F103的定时器最快的计数频率是72Mhz,8位分辨率的时候,PWM频率为72M/256=281.25Khz。我们需要把交流信号至少衰减50dB左右。
34.2 硬件设计
- 例程功能
我们将设计一个8位的DAC,使用按键(或USMART)控制STM32F1的PWM输出(占空比),从而控制PWM DAC的输出电压。为了得知PWM的输出电压,通过ADC1的通道1采集PWM DAC的输出电压,并在LCD模块上面显示ADC获取到的电压值以及PWM DAC的设定输出电压值等信息。 - 硬件资源
1)LED灯
LED0 – PB5
2)独立按键
KEY0 – PE4
KEY_UP – PA0
3)PWM输出通道
TIM1的通道1,对应IO是PA8 - 原理图
根据前面分析的原理,在硬件设计上就比较简单了。PWM可以由STM32定时器输出,我们只需要在外围增加一个滤波电路即可。我们使用的是RC滤波电路,其电路设计如下图所示:
图25.2.1 PWM DAC连接原理设计
根据我们的设计,输出8位DAC时,经过一阶滤波后DAC输出的交流信号大概的衰减可以达到117dB,可见我们设计是符合要求的。
这里有个特别需要注意的地方:因为PWM_DAC和OV_VSYNC共用了PA8引脚,所以在做本实验的时候,不能插摄像头模块或OLED模块,否则可能会影响PWM转换结果。
如下图所示,本实验需要用短路帽将PDC和ADC排针连接起来。
图25.2.2 PCB对应PWM DAC的位置
34.3 程序设计
34.3.1 程序流程图
图34.3.1.1 PWM DAC实验程序流程图
34.3.2 程序解析
- PWM DAC驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PWM DAC驱动源码包括两个文件:pwmdac.c和pwmdac.h。
在pwmdac.h中,定义的宏定义如下:
/*
- PWMDAC 默认是使用 PA8, 对应的定时器为 TIM1_CH1, 如果你要修改成其他IO输出, 则相应
- 的定时器及通道也要进行修改. 请根据实际情况进行修改.
*/
#define PWMDAC_GPIO_PORT GPIOA
#define PWMDAC_GPIO_PIN GPIO_PIN_8
/* PA口时钟使能 */
#define PWMDAC_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE();}while(0)
#define PWMDAC_TIMX TIM1
#define PWMDAC_TIMX_CHY TIM_CHANNEL_1 /* 通道Y, 1<= Y <=4 */
#define PWMDAC_TIMX_CCRX PWMDAC_TIMX->CCR1 /* 通道Y的输出比较寄存器 */
/* TIM1 时钟使能 */
#define PWMDAC_TIMX_CLK_ENABLE() do{ __HAL_RCC_TIM1_CLK_ENABLE();}while(0)
下面介绍pwmdac.c文件的函数,首先是pwmdac_init函数,其定义如下:
/**
* @brief PWM DAC初始化, 实际上就是初始化定时器
* @note
* 定时器的时钟来自APB1 / APB2, 当APB1 / APB2 分频时, 定时器频率自动翻倍
* 所以, 一般情况下, 我们所有定时器的频率, 都是72Mhz 等于系统时钟频率
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft = 定时器工作频率, 单位: Mhz
* @param arr: 自动重装值。
* @param psc: 时钟预分频数
* @retval 无
*/
void pwmdac_init(uint16_t arr, uint16_t psc)
{
TIM_OC_InitTypeDef timx_oc_pwmdac = {0};
PWMDAC_TIMX_CLK_ENABLE(); /* PWM DAC 定时器时钟使能 */
g_tim1_handle.Instance = TIM1; /* 定时器1 */
g_tim1_handle.Init.Prescaler = psc; /* 定时器分频 */
g_tim1_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 */
g_tim1_handle.Init.Period = arr; /* 自动重装载值 */
/* 使能TIMx_ARR进行缓冲 */
g_tim1_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(&g_tim1_handle); /* 初始化PWM */
timx_oc_pwmdac.OCMode = TIM_OCMODE_PWM1; /* CH1/2 PWM模式1 */
timx_oc_pwmdac.Pulse = 0; /* 设置比较值,此值用来确定占空比 */
timx_oc_pwmdac.OCPolarity = TIM_OCPOLARITY_HIGH; /* 输出比较极性为高 */
/* 配置TIM1通道1 */
HAL_TIM_PWM_ConfigChannel(&g_tim1_handle, &timx_oc_pwmdac, PWMDAC_TIMX_CHY);
HAL_TIM_PWM_Start(&g_tim1_handle, TIM_CHANNEL_1); /* 开启定时器1通道1 */
}
HAL_TIM_PWM_Init初始化TIM1并设置TIM1的ARR和PSC等参数,其次通过调用函数HAL_TIM_PWM_ConfigChannel设置定时器通道使用PWM1模式以及设置比较值等参数,最后通过调用函数HAL_TIM_PWM_Start来使能TIM1以及使能PWM通道TIM1_CH1输出。
HAL_TIM_PWM_Init会调用HAL_TIM_PWM_MspInit函数,用于存放GPIO、NVIC和时钟相关的代码。HAL_TIM_PWM_MspInit函数定义如下:
/**
* @brief 定时器底层驱动,时钟使能,引脚配置
* @note
* 此函数会被HAL_TIM_PWM_Init()调用
* @param htim:定时器句柄
* @retval 无
*/
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef gpio_init_struct;
if (htim->Instance == TIM1)
{
__HAL_RCC_TIM1_CLK_ENABLE(); /* 使能定时器1 */
__HAL_AFIO_REMAP_TIM1_PARTIAL(); /* TIM1通道引脚部分重映射使能 */
PWMDAC_GPIO_CLK_ENABLE(); /* GPIO 时钟使能 */
gpio_init_struct.Pin = PWMDAC_GPIO_PIN;
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(PWMDAC_GPIO_PORT, &gpio_init_struct);
}
}
下面介绍pwmdac_set_voltage函数,该函数先计算得到比较值,然后通过设置比较值来改变占空比,从而控制PWM DAC输出的电压值,其定义如下:
/**
* @brief 设置PWM DAC输出电压
* @param vol : 0~3300,代表0~3.3V
* @retval 无
*/
void pwmdac_set_voltage(uint16_t vol)
{
float temp = vol;
temp /= 100; /* 缩小100倍, 得到实际电压值 */
temp = temp * 256 / 3.3; /* 将电压转换成PWM占空比 */
__HAL_TIM_SET_COMPARE(&g_tim1_handler, PWMDAC_TIMX_CHY, temp);/*设置新的占空比*/
}
最后在main函数里面编写如下代码:
extern TIM_HandleTypeDef g_tim1_handler;
int main(void)
{
uint16_t adcx;
float temp;
uint8_t t = 0;
uint8_t key;
uint16_t pwmval = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
usmart_dev.init(72); /* 初始化USMART */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按键 */
adc3_init(); /* 初始化ADC */
pwmdac_init(256 - 1, 0); /* PWM DAC初始化, Fpwm = 72M/256 =281.25Khz */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "PWM DAC TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY_UP:+ KEY1:-", RED);
lcd_show_string(30, 130, 200, 16, 16, "PWM VAL:", BLUE);
lcd_show_string(30, 150, 200, 16, 16, "DAC VOL:0.000V", BLUE);
lcd_show_string(30, 170, 200, 16, 16, "ADC VOL:0.000V", BLUE);
while (1)
{
t++;
key = key_scan(0); /* 按键扫描 */
if (key == WKUP_PRES) /* PWM占空比调高 */
{
if (pwmval < 250) /* 范围限定 */
{
pwmval += 10;
}
/* 输出新的PWM占空比 */
__HAL_TIM_SET_COMPARE(&g_tim1_handler, PWMDAC_TIMX_CHY, pwmval);
}
else if (key == KEY1_PRES) /* PWM占空比调低 */
{
if (pwmval > 10) /* 范围限定 */
{
pwmval -= 10;
}
else
{
pwmval = 0;
}
/* 输出新的PWM占空比 */
__HAL_TIM_SET_COMPARE(&g_tim1_handler, PWMDAC_TIMX_CHY, pwmval);
}
if (t == 10 || key == KEY1_PRES || key == WKUP_PRES)
{ /* WKUP / KEY1 按下了, 或者定时时间到了 */
/* PWM DAC 定时器输出比较值 */
adcx = __HAL_TIM_GET_COMPARE(&g_tim1_handler,PWMDAC_TIMX_CHY);
lcd_show_xnum(94, 130, adcx, 3, 16, 0, BLUE); /* 显示CCRX寄存器值 */
temp = (float)adcx * (3.3 / 256); /* 得到DAC电压值 */
adcx = temp;
lcd_show_xnum(94, 150, temp, 1, 16, 0, BLUE); /* 显示电压值整数部分 */
temp -= adcx;
temp *= 1000;
lcd_show_xnum(110, 150, temp, 3, 16, 0X80, BLUE);/* 电压值的小数部分 */
adcx = adc3_get_result_average(ADC3_CHY, 10); /* ADC3通道1的转换结果 */
temp = (float)adcx * (3.3 / 4096); /* 得到ADC电压值(adc是12bit的) */
adcx = temp;
lcd_show_xnum(94, 170, temp, 1, 16, 0, BLUE); /* 显示电压值整数部分 */
temp -= adcx;
temp *= 1000;
lcd_show_xnum(110, 170, temp, 3, 16, 0X80, BLUE);/* 电压值的小数部分 */
LED0_TOGGLE(); /* LED0闪烁 */
t = 0;
}
delay_ms(10);
}
}
main函数初始化了LED和LCD用于显示效果,初始化按键和ADC用于修改ADC的占空比,辅助显示ADC。
34.4 下载验证
下载代码后,LED0不停的闪烁,提示程序已经在运行了。此时,可以通过按下KEY_UP按键增大输出电压,按下KEY1按键则电压变小。
图34.4.1 TFTLCD显示效果图
注意:因为PWM_DAC和OV_VSYNC共用了PA8引脚,所以在做本例程的时候,不能插摄像头模块或OLED模块,否则可能会影响PWM转换结果!!!