| 特性 | 位置式 PID | 增量式 PID |
|---|---|---|
| 輸出 | 絕對控制量 | 控制量增量(ΔU) |
| 公式 | U_k = Kp·E_k + Ki·ΣE + Kd·(E_k-E_{k-1}) | ΔU_k = Kp·(E_k-E_{k-1}) + Ki·E_k + Kd·(E_k-2E_{k-1}+E_{k-2}) |
| 積分作用 | 顯式累加 ΣE | 隱含於 ΔU 累加 |
| 適用執行器 | 無積分特性(伺服閥、加熱器) | 自帶積分(步進馬達、電機位置環) |
| 故障安全性 | 輸出直接控制,故障影響大 | 輸出為增量,故障影響較小 |
| 啟動衝擊 | 大(需追蹤功能) | 小(平滑過渡) |
本實作採用增量式,適合步進馬達速度環、直流馬達位置環等自帶積分的執行器。
// pid.h
#ifndef __PID_H__
#define __PID_H__
typedef struct {
int8_t Kp; // 比例增益
int8_t Ki; // 積分增益
int8_t Kd; // 微分增益
int8_t F; // 採樣頻率因子 (Ki 除數、Kd 乘數)
int8_t BITMOV; // 輸出右移位數 (定點縮放)
int16_t EK[3]; // 誤差歷史: EK[0]=E_k, EK[1]=E_{k-1}, EK[2]=E_{k-2}
int32_t UK_REAL; // 內部高精度累積輸出 (定點)
int16_t UK; // 最終輸出 (已縮放、飽和)
int16_t RK; // 設定值
int16_t CK; // 當前測量值
} PID_t;
void pid_exe(PID_t *pid);
#endif
// pid.c
#include "pid.h"
#define MAX_OUT 0xFF // 輸出飽和限幅 (8-bit PWM)
void pid_exe(PID_t *pid) {
// 1. 更新誤差歷史
pid->EK[2] = pid->EK[1];
pid->EK[1] = pid->EK[0];
pid->EK[0] = pid->RK - pid->CK; // E_k = 設定值 - 實測值
// 2. 增量式 PID 計算 (定點運算)
// ΔU = Kp*(E_k - E_{k-1}) + Ki*E_k/F + Kd*(E_k - 2E_{k-1} + E_{k-2})*F
int32_t delta = 0;
delta += (int32_t)pid->Kp * (pid->EK[0] - pid->EK[1]); // 比例項 (實為微分後積分)
delta += (int32_t)pid->Ki * pid->EK[0] / pid->F; // 積分項
delta += (int32_t)pid->Kd * (pid->EK[0] - 2*pid->EK[1] + pid->EK[2]) * pid->F; // 微分項
// 3. 累積輸出 (位置式輸出 = ΣΔU)
pid->UK_REAL += delta;
// 4. 縮放、飽和限幅、輸出
int16_t out = (int16_t)(pid->UK_REAL >> pid->BITMOV);
if (out >= MAX_OUT) pid->UK = MAX_OUT;
else if (out <= -MAX_OUT) pid->UK = -MAX_OUT;
else pid->UK = out;
}
F 為採樣頻率倒數縮放因子(如 100Hz → F=100),BITMOV 為最終輸出右移位數(等效除以 2^BITMOV)。U_k = Kp·E_k + Ki·ΣE_i + Kd·(E_k-E_{k-1})ΔU_k = U_k - U_{k-1} = Kp·(E_k-E_{k-1}) + Ki·E_k + Kd·(E_k-2E_{k-1}+E_{k-2})ΣE_i ≈ ΣE_i·dt = (ΣE_i)/F → 程式中 Ki*E_k/F。(E_k-E_{k-1})/dt = (E_k-E_{k-1})·F → 程式中 Kd*(...)*F。UK_REAL 累積導致輸出飽和時,實際輸出 UK 被夾住,但內部 UK_REAL 繼續累積。更嚴格的做法:檢測飽和時凍結積分項累積(此處簡化處理)。EK[3] 滾動更新,節省 RAM。// main.c
#include "pid.h"
PID_t motor_pid = {
.Kp = 20, .Ki = 5, .Kd = 2,
.F = 100, // 100Hz 採樣
.BITMOV = 8, // 內部放大 256 倍,輸出右移 8 位
.EK = {0}, .UK_REAL = 0, .UK = 0,
.RK = 3000, // 目標轉速 3000 RPM
.CK = 0
};
void timer_isr_100hz() {
motor_pid.CK = read_encoder_speed(); // 讀取編碼器速度
pid_exe(&motor_pid);
set_pwm_duty(motor_pid.UK); // 輸出 PWM 佔空比
}
Ki=Kd=0,調 Kp 觀察響應。RK, CK, UK, EK[0] 繪製波形分析。BITMOV 太大 → 輸出解析度低;太小 → 易飽和。嘗試 6~10。F 必須與實際 ISR 頻率一致,否則積分/微分時基錯誤。pid/ ├── pid.h // 介面定義 ├── pid.c // 核心演算法 ├── main.c // 應用層範例 └── defines.h // 型別定義 (int8_t 等)
此實作經實測可在 51 單片機(12MHz/24MHz)穩定運行,佔用極少 RAM/ROM。移植到 STM32、ESP32 等 32 位 MCU 時,建議改用 float 或 q15_t/q31_t (CMSIS-DSP) 簡化縮放邏輯。