40. PWM DAC
一、什么是PWM DAC
虽然 STM32F407ZGT6 具有内部 DAC,但是也仅仅只有两条 DAC 通道,而 STM32 还有其他的很多型号是没有 DAC 的。通常情况下,采用专用的 D/A 芯片来实现,但是这样就会带来成本的增加。不过 STM32 所有的芯片都有 PWM 输出,并且 PWM 输出通道很多,资源丰富。因此,我们可以使用 PWM+ 简单的 RC 滤波来实现 DAC 的输出从而节省成本。
DAC 是根据我们的源电压,按指定的 8 位、12 位、16 位等精度对源电压进行分割,其输出按最小精度 LSB(的倍数输出,即得到我们需要的 DAC 电压。最后,我们得到的 DAC 的电压为直流有效信号。
PWM 是周期固定,占空比可调的数字信号。PWM 可以被分解为一个直流分量和一个占空比固定,但是平均幅度为零的方波。 如果使 PWM 信号的占空比随时间改变,那么其直流分量随之改变,信号滤除交流分量后,将输出幅度变化的模拟信号。这种技术称为 PWM DAC。
从电做功的角度,可以把一个 PWM 波等效成一个 “总有效值为 0” 的交流波形和一个直流的电信号的叠加,直流部分的特性可根据占空比的改变而改变,这符合 DAC 的特性。
上面的等效原理我们从图形上就很容易等效出来,对于一个典型的 PWM 波型,它的输出波形和时间的关系如下图所示。
由公式可得 PWM 的占空比为:\(p = \frac{n}{N}\)。PWM 周期由 ARR(N) 决定。PWM 占空比由 CCRx(n) 决定。
二、PWM DAC分辨率
根据傅里叶理论,任意周期波形都可以分解为无限个频率为其整数倍的谐波之和。于是上述式子展开成傅里叶级数,可以得到下述式子:
想要得到 PWM DAC输出,我们只保留直流分量,通过低通滤波器过滤掉谐波分量即可。此时,公式可以简化为:\(f(t) = \frac{n}{N} * V_{H}\)。
当 DAC 的参考电压为 \(V_{REF+}\) 的时候,DAC 的输出电压是线性的从 0 ~ \(V_{REF+}\),M 位模式下 DAC 输出电压与 \(V_{REF+}\) 以及 DORx 的计算公式如下:
结合这两个式子,我们可以看出 PWM DAC 的分辨率表达式为:\(分辨率 = \log_{2}(N)\)。假设 n 的最小变化是 1。当 N=256 时,分辨率就是 8 位。当 N=4096 时,分辨率就是 12 位,以此类推。STM32 的定时器都是 16/32 位的,可以很容易得到更高分辨率的 PWM DAC。当然分辨率越高,速度就慢,低通滤波电路的要求也越高。
这里,存在两个主要误差源影响 PWM 方式 DAC 分辨率。
首先,PWM 信号的占空比只能表示有限的分辨率。这是因为 STM32 的 PWM 的占空比是输出比较寄存器 CCRx 与 TIMx_CNT 进行比较的结果,而 CCRx 在STM32F4 系列中大部分是 16 位的(TIM2 和 TIM5 是 32 位的)。那么很显然地,用 PWM 实现的 DAC 分辨率就与 TIMx_CNT 有关,即定时器的时钟频率越高则 CCRx 可以设置的值越多,分辨率相应地越高。定时器最高时钟是 168MHz,而某些定时器只能到 84MHz,定时器的频率越高,DAC 的速度越慢。
第二个误差源是 PWM 信号中不期望的谐波分量产生的峰峰值。前面 PWM 的频域展开公式说明 PWM 信号需要通过滤波器才能输出一个纹波较小的直流信号,但实际上对于简单设计的滤波器对交流信号的过滤能力是有限的,所以输出信号还会带有一定的交流成份。
三、8位分辨率下对RC滤波器的设计要求
- 精度要求
- 一般要求 1 次谐波对输出电压的影响不要超过 1 个位的精度,也就是 3.3/256=0.01289V
- 1 次谐波最大值
- 假设 \(V_{H}\) 为 3.3V,\(V_{L}\) 为 0V,那么一次谐波的最大值是 2*3.3/π=2.1V
- RC 滤波电路要求
- RC 滤波电路提供至少 -20lg(2.1/0.01289) =-44dB 的衰减
- 截止频率要求
- 当定时器的计数频率为 84Mhz,PWM DAC 分辨率为 8 位时,PWM 频率为 84M/256=328125Hz。若是 1 阶 RC 滤波,则要求截止频率为 2.07KHz,若是 2 阶 RC 滤波,则要求截止频率为 26.14KHz。
四、原理图
从电路图中可以看出,通过 PF7 的 TIM11_CH1 输出 PWM,经过 2 阶 RC 滤波后转换为直流输出,实现 PWM DAC 的功能。直流输出连接在 J4 排针的 4 号管脚上,要让 ADC1_IN5 检测 PWM-DAC 输出的电压,只需要使用短接片将 STM_ADC(开发板对应丝印为 ADC)和 PWM_AUDIO(开发板对应丝印为 PWM)短接即可。
分析电路可知,开发板上 PWM DAC 输出采用的是 2 阶 RC 滤波。2 阶 RC 滤波截止频率计算公式为:\(f = \frac{1}{2πRC}\)。该电路要求 \(R_{35} * C_{68} = R_{36} * C_{69} = RC\)。根据这个公式,我们计算出的截止频率为 33.8KHz 超过了 26.14KHz,这是因为该电路我们还可以用作 PWM DAC 音频输出,而音频信号带宽是 32.05KHz,为了让音频信号也能够通过该低通滤波,同时也为了标准化参数选取,所以确定了这样的参数。实测精度在 0.5LSB 左右。
五、程序源码
定时器初始化函数:
TIM_HandleTypeDef g_tim11_handle;
/**
* @brief 定时器PWM功能初始化函数
*
* @param htim 定时器句柄
* @param TIMx 定时器寄存器基地址,可选值: TIMx, x可选范围: 1 ~ 5, 8 ~ 14
* @param prescaler 预分频系数,可选值: 0 ~ 65535
* @param period 自动重装载值,可选值: 0 ~ 65535
* @param channel 输出PWM的通道,可选值: TIM_CHANNEL_x, x可选范围: 1 ~ 4
* @param polarity 输出比较极性,可选值: [TIM_OCPOLARITY_LOW, TIM_OCPOLARITY_HIGH]
* @param pluse 输出比较值,可选值: 0 ~ 65535
*/
void TIM_PWM_Init(TIM_HandleTypeDef *htim, TIM_TypeDef *TIMx, uint16_t prescaler, uint16_t period, uint32_t channel, uint32_t polarity, uint32_t pluse)
{
TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
htim->Instance = TIMx; // 定时器寄存器基地址
htim->Init.CounterMode = TIM_COUNTERMODE_UP; // 计数模式
htim->Init.Prescaler = prescaler; // 预分频系数
htim->Init.Period = period; // 自动重装载值
htim->Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // 使能缓冲
HAL_TIM_PWM_Init(htim);
TIM_OC_InitStruct.OCMode = TIM_OCMODE_PWM1; // PWM模式1
TIM_OC_InitStruct.Pulse = pluse; // 比较值
TIM_OC_InitStruct.OCPolarity = polarity; // 输出比较极性
HAL_TIM_PWM_ConfigChannel(htim, &TIM_OC_InitStruct, channel);
}
定时器 11 底层初始化函数:
/**
* @brief 定时器PWM模式底层初始化函数
*
* @param htim 定时器句柄
*/
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (htim->Instance == TIM11)
{
__HAL_RCC_TIM11_CLK_ENABLE(); // 使能TIM11的时钟
__HAL_RCC_GPIOF_CLK_ENABLE(); // 使能TIM11的Channel 1对应的GPIO时钟
GPIO_InitStruct.Pin = GPIO_PIN_7; // TIM14的Channel 1对应的GPIO引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用功能
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // GPIO输出速度
GPIO_InitStruct.Alternate = GPIO_AF3_TIM11; // 复用功能选择
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
}
}
PWM DAC 函数:
/**
* @brief PWM DAC函数
*
* @param htim 定时器句柄
* @param channel 定时器通道
* @param voltage 电压值
*/
void PWM_DAC_SetVoltage(TIM_HandleTypeDef *htim, uint32_t channel, double voltage)
{
voltage = voltage * 256 / 3.3;
__HAL_TIM_SET_COMPARE(htim, channel, voltage);
}
main() 函数:
int main(void)
{
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
TIM_PWM_Init(&g_tim11_handle, TIM11, 0, 255, TIM_CHANNEL_1, TIM_OCPOLARITY_HIGH, 0);
HAL_TIM_PWM_Start(&g_tim11_handle, TIM_CHANNEL_1);
PWM_DAC_SetVoltage(&g_tim11_handle, TIM_CHANNEL_1, 2.5);
while (1)
{
}
return 0;
}