PID 精準調控黑科技 | ESP32 加 Fourier 幫你一眼看穿問題本質


Fourier + PID 控制器 是自動控制領域中歷久不衰的組合。我們不只是調 PID,我們讓 PID 會看頻譜、會分析誤差震盪頻率,打造能看透系統不穩定根源的智慧控制器。
我們將帶你實作如何結合 ESP32 的高效運算能力,並運用 傅立葉分析這雙頻域的「透視眼」,從訊號中挖掘隱藏模式,打造新一代能夠看穿控制問題本質的智慧型 PID 系統!

Fourier

什麼是回授控制?它和 PID 有什麼關係?

回授控制(Feedback Control)是一種讓系統具備「自我調整」能力的控制方式。它的核心概念是”不斷觀察輸出、比較目標與實際差距,並根據誤差自動修正控制輸入“。
想像你開車在高速公路上開定速 100km/h(目標值)。當你發現實際速度變成 90km/h(實際值)時,你會踩多一點油門(控制輸出)來補足速度差,直到達到設定值。這種「觀察→調整→再觀察」的循環,就是回授控制的本質。

PID 控制器,就是回授控制的經典實現方式之一。
它的任務,就是根據「誤差」來計算調整量:

  • P(比例):誤差越大,調整越大
  • I(積分):長期偏差會累積起來補償
  • D(微分):預測誤差變化趨勢,避免震盪

這三項合在一起,就能形成穩定、快速且準確的控制效果

為什麼要用 Fourier + PID?

在自動控制的世界裡,PID 控制器是工程師們最熟悉的夥伴。從機械臂、馬達控制到空調系統,它幾乎無所不在。但隨著控制需求愈趨複雜,光靠 PID 已經不夠,你需要能看見「隱藏問題」的能力——這時,Fourier 頻譜分析就派上用場了。

用 ESP32 打造一套結合 傅立葉轉換(FFT)+ PID 控制器 的嵌入式智慧控制系統。我們不只實作,更深入理解:

  • 回授控制與 PID 如何運作?
  • Fourier 分析與控制系統有什麼關係?
  • 怎麼讓 ESP32 一邊控制 plant、一邊即時分析震盪頻率?
  • 如何從 log 中快速辨識出「震盪過多」、「共振點」、「PID 調太兇」的現象?

開發環境

在開始編程之前,請確保已完成以下準備工作:

專案結構

建立一個乾淨的 fourier-pid-esp32 專案如下:

fourier-pid-esp32/
├── main/
│   ├── main.c                 ← 主要程式碼(PID、ADC、PWM、FFT整合)
│   └── CMakeLists.txt         ← 定義 main 元件的編譯設定
├── CMakeLists.txt             ← 專案根目錄的 CMake 設定,用來啟動專案
└── sdkconfig                  ← ESP-IDF 專案設定檔,記錄配置選項

為了讓讀者更容易上手,本範例將 PID 演算法、傅立葉分析(FFT)、ADC 訊號讀取與 PWM 控制 整合於同一個檔案(main.c)中。這樣的寫法更加直觀,方便快速理解各模組之間的關聯與資料流,特別適合初學者快速掌握「頻域診斷 + 控制調整」的核心概念。

使用 ESP-DSP

ESP-DSP 是由 Espressif 官方推出、為 ESP32 系列晶片量身打造的高效能數位訊號處理函式庫。

它的目的是:

  • 提供高速的 DSP 函數(FFT、FIR、IIR、convolution、矩陣、統計等)
  • 對 ESP32 硬體架構最佳化(使用 SIMD、MAC、浮點計算指令)
  • 提供 即時處理能力,可應用於馬達控制、音訊、通訊等嵌入式領域

請打開 VS Code 的「Terminal(終端機)」視窗(通常位於下方),然後在你的 ESP-IDF 專案根目錄執行以下指令:

idf.py add-dependency "espressif/esp-dsp=*"

Code

#include <stdio.h>
#include <math.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_dsp.h"

// Log tag
static const char *TAG = "PID_SIM";

// ==== Simulation Parameters ====
#define SAMPLE_RATE_HZ     20
#define SAMPLE_PERIOD_MS   (1000 / SAMPLE_RATE_HZ)
#define FFT_SIZE           256

// ==== PID Controller ====
typedef struct {
    float Kp, Ki, Kd;
    float setpoint;
    float integral;
    float prev_error;
    float out_min, out_max;
} PIDController;

void pid_init(PIDController *pid, float Kp, float Ki, float Kd, float out_min, float out_max) {
    pid->Kp = Kp;
    pid->Ki = Ki;
    pid->Kd = Kd;
    pid->integral = 0;
    pid->prev_error = 0;
    pid->out_min = out_min;
    pid->out_max = out_max;
}

float pid_compute(PIDController *pid, float input, float dt) {
    float error = pid->setpoint - input;
    pid->integral += error * dt;
    float derivative = (error - pid->prev_error) / dt;
    float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative;
    pid->prev_error = error;

    // Clamp output
    if (output > pid->out_max) output = pid->out_max;
    if (output < pid->out_min) output = pid->out_min;
    return output;
}

// ==== Virtual Plant Simulation ====
float plant_sim(float input, float last_output) {
    // Simple first-order system: y[n] = 0.9 * y[n-1] + 0.1 * input
    return 0.9f * last_output + 0.1f * input;
}

// ==== FFT Buffers ====
float fft_input[FFT_SIZE];
float fft_mag[FFT_SIZE / 2];
int fft_index = 0;

// ==== Control Task ====
void control_task(void *arg) {
    // Initialize PID
    PIDController pid;
    pid_init(&pid, 1.2f, 0.2f, 0.1f, -100, 100);
    pid.setpoint = 50.0f;

    float plant_output = 0;

    // Initialize FFT
    dsps_fft2r_init_fc32(NULL, CONFIG_DSP_MAX_FFT_SIZE);

    while (1) {
        float dt = 1.0f / SAMPLE_RATE_HZ;

        // Get plant output as PID input
        float input = plant_output;

        // Compute PID output
        float pid_out = pid_compute(&pid, input, dt);

        // Simulate plant response
        plant_output = plant_sim(pid_out, plant_output);

        // Store error in FFT input buffer
        float error = pid.setpoint - input;
        fft_input[fft_index++] = error;

        if (fft_index >= FFT_SIZE) {
            fft_index = 0;
            float fft_temp[FFT_SIZE * 2];  // Complex buffer

            for (int i = 0; i < FFT_SIZE; i++) {
                fft_temp[i * 2] = fft_input[i];     // Real part
                fft_temp[i * 2 + 1] = 0.0f;         // Imaginary part
            }

            dsps_fft2r_fc32((float *)fft_temp, FFT_SIZE);
            dsps_bit_rev_fc32((float *)fft_temp, FFT_SIZE);

            // Calculate magnitude
            for (int i = 0; i < FFT_SIZE / 2; i++) {
                float re = fft_temp[2 * i];
                float im = fft_temp[2 * i + 1];
                fft_mag[i] = sqrtf(re * re + im * im);
            }

            // Find peak frequency component
            float max_mag = 0;
            int max_bin = 0;
            for (int i = 1; i < FFT_SIZE / 2; i++) {
                if (fft_mag[i] > max_mag) {
                    max_mag = fft_mag[i];
                    max_bin = i;
                }
            }

            float freq_resolution = (float)SAMPLE_RATE_HZ / FFT_SIZE;
            float dominant_freq = max_bin * freq_resolution;

            // Output FFT results
            ESP_LOGI(TAG, "FFT Peak Frequency: %.2f Hz, Magnitude: %.2f", dominant_freq, max_mag);
        }

        // Output PID status
        ESP_LOGI(TAG, "Setpoint: %.2f | Input: %.2f | Error: %.2f | Output: %.2f",
                 pid.setpoint, input, pid.prev_error, pid_out);

        vTaskDelay(pdMS_TO_TICKS(SAMPLE_PERIOD_MS));
    }
}

void app_main() {
    ESP_LOGI(TAG, "Starting PID + FFT simulation...");
    xTaskCreate(control_task, "control_task", 4096 * 2, NULL, 5, NULL);
}

編譯和燒錄

完成程式碼後,您可以使用 ESP-IDF 提供的命令進行編譯、燒錄和監控。

在 VS Code 的左下角 ESP-IDF 工具列:

  • 點選 Build project
  • 點選 Flash device
  • 點選 Monitor device

輸出說明

以下是模擬控制過程中的部份 log 輸出:

I (0) PID_SIM: Starting PID + FFT simulation...
I (100) PID_SIM: Setpoint: 50.00 | Input: 0.00 | Error: 50.00 | Output: 100.00
I (150) PID_SIM: Setpoint: 50.00 | Input: 10.00 | Error: 40.00 | Output: 86.00
I (200) PID_SIM: Setpoint: 50.00 | Input: 18.60 | Error: 31.40 | Output: 74.52
...
I (5200) PID_SIM: Setpoint: 50.00 | Input: 49.89 | Error: 0.11 | Output: 0.37
I (5250) PID_SIM: Setpoint: 50.00 | Input: 49.93 | Error: 0.07 | Output: 0.24
I (5300) PID_SIM: Setpoint: 50.00 | Input: 49.95 | Error: 0.05 | Output: 0.19

I (5300) PID_SIM: FFT Peak Frequency: 0.78 Hz, Magnitude: 68.34

解讀說明:

  • Setpoint: 期望值(目標值),此例為 50.0。
  • Input: Plant 輸出(實際值)。
  • Error: 誤差(Setpoint – Input)。
  • Output: PID 控制輸出,送進 plant。
  • FFT Peak Frequency: 最近一次 FFT 結果中,頻譜中的主頻率(代表誤差訊號的震盪主頻率)。
  • Magnitude: 該頻率成分的強度,用於判斷「是否有明顯震盪」。

小技巧:當你在 Console 中看到 FFT Peak Frequency ≈ 0 Hz 時,表示控制效果穩定;但若頻率高達數 Hz 甚至有週期性震盪,表示 PID 參數過大或 plant 本身存在不穩定頻率。

結論

我們透過 ESP32 結合 ESP-DSP 函式庫,示範了如何將 PID 調控誤差結合 FFT 頻譜分析,讓「系統震盪」、「回授不穩」、「調不到位」這些問題不再只是「感覺不對」,而是能具體觀察出頻率分佈與共振現象

這種做法的好處是:

  • 將時間域誤差轉換為頻率域,問題變得可視化、可量化
  • 不需額外硬體,只靠 ESP32 就能做頻譜分析
  • 利用 ESP-DSP,可在 即時系統中輕量執行 FFT
  • 幫助你快速判斷是 PID 參數不當?還是系統結構共振?

對於開發馬達控制、雲台穩定器、自平衡機器人或其他閉迴路系統的開發者來說,這樣的頻譜診斷方法,就像是加了一雙「頻率域的眼睛」,讓你不再盲調參數,而是看見問題本質,精準修正