0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 未分類
  3. 正文

PWM 脈衝寬度調變完整教學:從原理到 STM32/ESP32 實作

2026 年 5 月 28 日 7點熱度 0人點贊 0條評論

前言

PWM 脈衝寬度調變(Pulse Width Modulation)是嵌入式系統中最常見的控制技術之一。從 LED 調光、馬達轉速控制、伺服馬達角度到開關電源,PWM 以其簡單、高效、數位友善的特性,成為每個嵌入式工程師必須掌握的技能。STM32 的 Timer 模組支援多通道 PWM 輸出,ESP32 的 LEDC 與 MCPWM 更是專為 PWM 設計。本文將從 PWM 的基本參數講起,搭配 WaveDrom 時序圖解說,最後提供 STM32 和 ESP32 的完整實戰程式碼。

一、PWM 基本概念

1.1 PWM 的三個核心參數

參數 說明 單位 範例
頻率 (Frequency) 每秒週期數 Hz 1 kHz = 1000 次/秒
週期 (Period) 一個完整週期的時間 秒 T = 1/f = 1ms
工作週期 (Duty Cycle) 高電位時間佔週期的比例 % 50% = 一半時間 ON

PWM Duty Cycles

圖 1:PWM 不同工作週期比較 — 同頻率下,10%、50%、90% duty 的 ON 時間比例不同,輸出有效值也不同。

1.2 頻率 vs 週期

T_PWM = 1 / f_PWM
T_ON = Duty × T_PWM
V_avg = Duty × V_CC

PWM Frequency

圖 2:PWM 頻率比較 (50% Duty) — 頻率越高週期越短,1kHz、500Hz、200Hz 在同 duty 下 ON 時間不同。

1.3 頻率選擇原則

  • LED 調光:1~5 kHz(太低會閃爍,人眼可見 flicker)
  • DC 馬達:50~500 Hz(太低會產生噪音與震動)
  • 伺服馬達:50 Hz(標準航模伺服規格)
  • 開關電源:100~500 kHz(Buck/Boost 內建 PWM)
  • Class D 音訊:200 kHz ~ 1 MHz(需濾波還原)

二、Timer 產生 PWM 的原理

PWM 訊號由硬體 Timer 自動產生,不佔用 CPU 資源。原理如下:

  1. Timer 計數器從 0 遞增至 ARR(Auto-Reload Register)
  2. CCR(Capture/Compare Register)儲存比較值
  3. 當 CNT < CCR 時,輸出 HIGH
  4. 當 CNT ≥ CCR 時,輸出 LOW
  5. CNT 歸零時重新開始,自動產生 PWM 輸出

Timer PWM Generation

圖 3:Timer 計數器與 PWM 輸出 (ARR=9) — CNT 從 0~9 遞增,CCR=3 時 duty=40%,CCR=7 時 duty=70%。

計算公式:

PWM_Freq = Timer_Clock / (PSC + 1) / (ARR + 1)
Duty = (CCR + 1) / (ARR + 1) × 100%
解析度 = 1 / (ARR + 1) × 100%

範例:STM32F4 @ 84 MHz,目標 1 kHz PWM,50% duty:

PSC = 83  →  Timer Clock = 84MHz / 84 = 1 MHz
ARR = 999 →  PWM Freq = 1MHz / 1000 = 1 kHz
CCR = 500 →  Duty = 501 / 1000 ≈ 50%

三、STM32 PWM 實作 (HAL + Timer)

3.1 CubeMX 配置

  1. Timer2 設定:Clock Source = Internal Clock
  2. Channel1 = PWM Generation CH1
  3. Prescaler = 83 (84-1), Counter Period = 999 (1000-1)
  4. Pulse (CCR) = 500 (初始 50%)
  5. GPIO: PA0 = TIM2_CH1 (AF1)

3.2 初始化程式碼

#include "stm32f4xx_hal.h"

TIM_HandleTypeDef htim2;

void MX_TIM2_Init(void)
{
    TIM_OC_InitTypeDef sConfigOC = {0};

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 83;          // 84 MHz / 84 = 1 MHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 999;            // 1 MHz / 1000 = 1 kHz
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_PWM_Init(&htim2);

    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 500;              // 50% duty
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
}

void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    GPIO_InitTypeDef gpio = {0};
    __HAL_RCC_TIM2_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    gpio.Mode = GPIO_MODE_AF_PP;
    gpio.Pull = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_LOW;
    gpio.Alternate = GPIO_AF1_TIM2;
    gpio.Pin = GPIO_PIN_0;
    HAL_GPIO_Init(GPIOA, &gpio);
}

void start_pwm(void)
{
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}

3.3 運行中調整 Duty

void set_duty(uint8_t percent)
{
    if (percent > 100) percent = 100;
    uint32_t ccr = (uint32_t)percent * 999 / 100;
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, ccr);
}

// 呼吸燈效果
void breathe_effect(void)
{
    for (int i = 0; i <= 100; i++) { set_duty(i); HAL_Delay(10); } for (int i = 100; i >= 0; i--)
    {
        set_duty(i);
        HAL_Delay(10);
    }
}

3.4 多通道 PWM(互補輸出 + Dead Time)

// TIM1 進階定時器支援互補輸出,適合 H-Bridge 馬達控制
// CH1 = PA8, CH1N = PB13 (互補), CH2 = PA9, CH2N = PB14

sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_SET;
sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);

// Dead Time 設定 (BDTR 暫存器)
htim1.Init.DeadTime = 10;  // 100ns dead time (取決於 Timer clock)
HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1);  // 啟動互補輸出

四、ESP32 PWM 實作

ESP32 有兩套 PWM 硬體:LEDC(通用 PWM)和 MCPWM(專用於馬達控制)。

4.1 LEDC 基本 PWM(Arduino)

#include <driver/ledc.h>

#define LEDC_CH    LEDC_CHANNEL_0
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_PIN   2  // GPIO2 (內建 LED)
#define LEDC_FREQ  5000  // 5 kHz
#define LEDC_RES   10     // 10-bit 解析度 (0~1023)

void setup()
{
    ledcSetup(LEDC_CH, LEDC_FREQ, LEDC_RES);
    ledcAttachPin(LEDC_PIN, LEDC_CH);

    // 呼吸燈
    for (int duty = 0; duty <= 1023; duty++) { ledcWrite(LEDC_CH, duty); delay(2); } for (int duty = 1023; duty >= 0; duty--)
    {
        ledcWrite(LEDC_CH, duty);
        delay(2);
    }
}

void loop()
{
    // 外部控制
    int pot = analogRead(34);  // 電位器 0~4095
    int duty = map(pot, 0, 4095, 0, 1023);
    ledcWrite(LEDC_CH, duty);
    delay(20);
}

4.2 LEDC ESP-IDF 框架

#include "driver/ledc.h"
#include "esp_err.h"

#define LEDC_GPIO    GPIO_NUM_2
#define LEDC_FREQ    5000
#define LEDC_RES     LEDC_TIMER_10_BIT

void pwm_init(void)
{
    ledc_timer_config_t timer_conf = {
        .speed_mode      = LEDC_HIGH_SPEED_MODE,
        .timer_num       = LEDC_TIMER_0,
        .duty_resolution = LEDC_RES,
        .freq_hz         = LEDC_FREQ,
        .clk_cfg         = LEDC_AUTO_CLK,
    };
    ledc_timer_config(&timer_conf);

    ledc_channel_config_t ch_conf = {
        .gpio_num   = LEDC_GPIO,
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .channel    = LEDC_CHANNEL_0,
        .timer_sel  = LEDC_TIMER_0,
        .duty       = 0,
        .hpoint     = 0,
    };
    ledc_channel_config(&ch_conf);
}

void set_pwm_fade(uint32_t target_duty, int time_ms)
{
    ledc_set_fade_with_time(LEDC_HIGH_SPEED_MODE,
        LEDC_CHANNEL_0, target_duty, time_ms);
    ledc_fade_start(LEDC_HIGH_SPEED_MODE,
        LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT);
}

4.3 伺服馬達控制 (50 Hz)

Servo PWM

圖 5:伺服馬達 PWM 訊號 (50 Hz / 20ms 週期) — 0°=0.5ms、90°=1.5ms、180°=2.5ms。

#include <ESP32Servo.h>

Servo myServo;
#define SERVO_PIN 13

void setup()
{
    myServo.attach(SERVO_PIN, 500, 2500);  // min=500μs, max=2500μs
}

void loop()
{
    myServo.write(0);    // 0°
    delay(1000);
    myServo.write(90);   // 90°
    delay(1000);
    myServo.write(180);  // 180°
    delay(1000);
}

ESP-IDF 使用 MCPWM 控制伺服馬達:

#include "driver/mcpwm_prelude.h"
#include "driver/mcpwm_timer.h"
#include "driver/mcpwm_oper.h"
#include "driver/mcpwm_cmpr.h"
#include "driver/mcpwm_gen.h"

void servo_init(void)
{
    mcpwm_timer_handle_t timer;
    mcpwm_oper_handle_t oper;
    mcpwm_cmpr_handle_t cmpr;
    mcpwm_gen_handle_t gen;

    mcpwm_timer_config_t timer_cfg = {
        .group_id = 0,
        .clk_src  = MCPWM_TIMER_CLK_SRC_DEFAULT,
        .resolution_hz = 1 * 1000 * 1000,  // 1 MHz
        .count_mode    = MCPWM_TIMER_COUNT_MODE_UP,
        .period_ticks  = 20000,  // 20ms = 50 Hz
    };
    mcpwm_new_timer(&timer_cfg, &timer);

    mcpwm_operator_config_t oper_cfg = {.group_id = 0};
    mcpwm_new_operator(&oper_cfg, &oper);
    mcpwm_operator_connect_timer(oper, timer);

    mcpwm_comparator_config_t cmpr_cfg = {.flags.update_cmp_on_tez = true};
    mcpwm_new_comparator(oper, &cmpr_cfg, &cmpr);

    mcpwm_generator_config_t gen_cfg = {.gen_gpio_num = SERVO_PIN};
    mcpwm_new_generator(oper, &gen_cfg, &gen);

    mcpwm_generator_set_action_on_timer_event(gen,
        MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
            MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH));
    mcpwm_generator_set_action_on_compare_event(gen,
        MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
            cmpr, MCPWM_GEN_ACTION_LOW));

    mcpwm_timer_enable(timer);
    mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP);

    mcpwm_comparator_set_compare_value(cmpr, 1500);  // 90° = 1.5ms
}

五、實戰專案:DC 馬達 PID 速度控制

結合 PWM + ENC(編碼器)+ PID 實現閉環速度控制:

// 系統架構
// PID_Task → set_duty() → PWM → Motor Driver(DRV8833) → DC Motor
//                                                          │
//                      PID_Task ← calc_rpm() ← Encoder ←──┘

volatile int32_t encoder_count = 0;
int target_rpm = 100;  // 目標轉速

void encoder_isr(void)
{
    encoder_count += (digitalRead(ENC_B) == HIGH) ? 1 : -1;
}

float calc_rpm(int dt_ms)
{
    const float pulses_per_rev = 20 * 4;  // 20 PPR × 4 倍頻
    float rpm = (float)encoder_count / pulses_per_rev * 60000.0 / dt_ms;
    encoder_count = 0;
    return rpm;
}

void pid_task(void *pv)
{
    float kp = 1.5, ki = 0.2, kd = 0.05;
    float integral = 0, prev_error = 0;

    while (1)
    {
        float rpm = calc_rpm(50);  // 50ms 一次
        float error = target_rpm - rpm;
        integral += error * 0.05;
        float derivative = (error - prev_error) / 0.05;
        float output = kp * error + ki * integral + kd * derivative;

        output = constrain(output, 0, 1023);
        ledcWrite(MOTOR_CH, (uint32_t)output);

        prev_error = error;
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

六、常見問題與除錯

6.1 PWM 無輸出?

  1. GPIO 複用功能:確認 GPIO 設定為 AF PP,且 AF 號碼正確
  2. Timer 未啟動:HAL 要呼叫 HAL_TIM_PWM_Start()
  3. CCR > ARR:若 CCR ≥ ARR,PWM 永遠 HIGH
  4. 頻率過高:解析度不足時可降低頻率或增加 Timer clock

6.2 馬達不順 / 有噪音

  • 提高 PWM 頻率至超聲波範圍 (>18 kHz)
  • 確認 Dead Time 設定(H-Bridge 不能上下臂同時導通)
  • 電源不足導致電壓下降

6.3 LED 閃爍

  • PWM 頻率低於 60 Hz → 人眼可見閃爍
  • 建議 1 kHz 以上,若用攝影機拍攝則建議 5~20 kHz 避開掃描線

七、總結

PWM 是三種參數(頻率、週期、工作週期)決定的簡單但強大的技術。從 STM32 的 Timer 到 ESP32 的 LEDC/MCPWM,硬體自動產生 PWM 訊號幾乎不耗 CPU。實務重點:

  1. 頻率決定應用場景:LED=1~5k、馬達=50~500、伺服=50、電源=100k+
  2. 解析度取決於 ARR:ARR=999 → 0.1% 解析度,ARR=1999 → 0.05%
  3. 硬體 Timer 自動輸出:設定好 CCR 就能調整 duty,CPU 可以專心做別的事
  4. 互補 PWM + Dead Time:馬達控制必備,防止上下臂短路

PWM Applications

圖 4:PWM 常見應用領域 — LED 調光、DC 馬達、伺服馬達、開關電源、Class D 音訊放大、超音波測距。

標籤: RS485 工業通訊
最後更新:2026 年 5 月 28 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang