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 波型,它的输出波形和时间的关系如下图所示。

PWM波形的时域表示

\[f(t) = \begin{cases} V_{H}, kNT ≤ t < kNT + nT\\ V_{L}, kNT + nT ≤ t ≤ kNT + NT \end{cases} \]

  由公式可得 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 的计算公式如下:

\[DACx(输出电压) = \frac{DORx}{2^{M}} * V_{REF+} \]

  结合这两个式子,我们可以看出 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。

四、原理图

PWM-DAC模块

PWM-DAC引脚

PWM-DAC输出引脚

  从电路图中可以看出,通过 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;
}
posted @ 2024-01-07 18:48  星光樱梦  阅读(36)  评论(0编辑  收藏  举报