How can we help you today?
【演算】PID基礎 – PingPong球架
前言
PID演算法起源可追溯至1890年代(wikipedia),迄今在控制工程上仍是一項重要的演算與技術實務。PID經常融入其他演算,如kalman濾波、模糊、ML…等演算法融合進而形成更加智慧型的控制系統,成為了近年無人載具的工程設計上不可或缺的篇章。
歷久彌新的PID對於初學者而言,經常存在一個無形門檻。因為理解PID控制,隱含著要理解【數學(比例、微分、積分)、物理(慣性、阻尼..)與科技(實踐方法)】之間的對應關係,抽象思考力需要高些,也需要較多的先備知識與實作技能。為了讓初學者可在實作中認識並反思PID控制理論架構,一套簡易、材料容易取得的PID實驗教具,似乎有了必要性。於是我開展了這dual2s-x微專題:運用乒乓球、5mm塑料管(吸管…)、超音波、SG90伺服馬達與3D列印結構等材料,製作一套PID個人實驗教具。
👉玩家請留意:我們都知道PID的效能與系統的結構線性度、感測與制動響應速率,有著密不可分的關係。不過,結構與響應越好的元件,價格也隨之而上。此處使用的超音波、SG90伺服馬達元件響應速率有限,對於進階、高精度與響應度的PID系統要求者而言,此系統是無法滿足的呦~😊
Quick DEMO
3D模型
專題材料
控制器:dual2s系列 (read more) / esp32
元件:超音波、SG90(s)伺服馬達
螺絲:M3-16L x 2, 螺釘:M3-8L x 3
結構件:3D列印(模型下載)、5mm管材(塑料管)
範例程式
#include <Arduino.h>
#include <Servo.h>
#include <Adafruit_NeoPixel.h>
// WS2812B 設定
#define LED_PIN 2
#define NUM_LEDS 1
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
bool swTarget = false;
int TargetCnt = 0;
bool sw = 0.0;
bool doPID = 0.0;
#define TRIG_PIN 27
#define ECHO_PIN 13
#define SERVO_PIN 19
#define SvoZero 80
Servo myservo;
// ====== PID 參數 ======
float Kp = 1.0; // 比例增益
float Ki = 0.0; // 積分增益
float Kd = 0.1; // 微分增益
// PID 變數
float target = 8.0; // 目標距離 10 cm
float error, lastError = 0;
float integral = 0;
unsigned long lastTime;
// 非阻塞時間控制
unsigned long lastPID = 0;
const unsigned long PID_INTERVAL = 50; // 每 50ms 執行一次
enum USState {
US_IDLE,
US_TRIG_HIGH,
US_WAIT_ECHO_HIGH,
US_MEASURE_ECHO,
};
USState usState = US_IDLE;
unsigned long trigTime = 0;
unsigned long echoStart = 0;
unsigned long echoEnd = 0;
float distanceCM = 0; //超音波量到的距離, for error calculation.
unsigned long lastMeasure = 0;
void updateUltrasonic() {
unsigned long nowMicros = micros();
unsigned long nowMillis = millis();
// --- 1. 啟動一個新量測 ---
if (usState == US_IDLE && (nowMillis - lastMeasure >= PID_INTERVAL)) {
digitalWrite(TRIG_PIN, HIGH);
trigTime = nowMicros;
usState = US_TRIG_HIGH;
}
// --- 2. TRIG HIGH 持續 10us ---
if (usState == US_TRIG_HIGH) {
if (nowMicros - trigTime >= 10) {
digitalWrite(TRIG_PIN, LOW);
usState = US_WAIT_ECHO_HIGH;
}
}
// --- 3. 等候 Echo 變 HIGH ---
if (usState == US_WAIT_ECHO_HIGH) {
if (digitalRead(ECHO_PIN) == HIGH) {
echoStart = micros();
usState = US_MEASURE_ECHO;
}
else if (micros() - trigTime > 20000) {
usState = US_IDLE; // timeout
lastMeasure = millis();
}
}
// --- 4. 計算 Echo HIGH 時間 ---
if (usState == US_MEASURE_ECHO) {
if (digitalRead(ECHO_PIN) == LOW) {
echoEnd = micros();
unsigned long duration = echoEnd - echoStart;
distanceCM = duration * 0.0343 / 2.0; // 計算距離(cm)
lastMeasure = millis();
usState = US_IDLE;
}
else if (micros() - echoStart > 30000) {
usState = US_IDLE; // timeout
lastMeasure = millis();
}
}
}
// ===== 解析 Serial 指令 =====
void parseSerialInput(String cmd) {
cmd.trim();
if (cmd.startsWith("kp=")) {
Kp = cmd.substring(3).toFloat();
}
else if (cmd.startsWith("ki=")) {
Ki = cmd.substring(3).toFloat();
}
else if (cmd.startsWith("kd=")) {
Kd = cmd.substring(3).toFloat();
}
else if (cmd.startsWith("target=")) {
target = cmd.substring(7).toFloat();
}
else if (cmd.startsWith("sw=")) {
int i = cmd.substring(3).toInt(); // 讀入 0 或 1
sw = (i != 0);
}
else if (cmd.startsWith("stop")) {
doPID = 0;
sw= 0;
myservo.write(SvoZero); // 初始水平
}
else if (cmd.startsWith("doPID")) {
Serial.println("--- PID START ---");
doPID = 1;
}
else {
Serial.println("Unknown command. Use: kp=, ki=, kd=, target=");
return;
}
}
void setup() {
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
strip.begin();
strip.clear();
strip.setBrightness(10); // 設定亮度 (0~255)
myservo.attach(SERVO_PIN);
myservo.write(SvoZero); // 初始水平
lastTime = millis();
digitalWrite(TRIG_PIN, LOW);
Serial.println("Start PID ball balance control...");
}
void loop() {
//讀取序列窗命令
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
parseSerialInput(cmd);
}
if(!doPID)
{
strip.setPixelColor(0, strip.Color(0, 255, 0));
strip.show();
delay(250);
strip.setPixelColor(0, strip.Color(0, 0, 0));
strip.show();
delay(250);
return;
}
//超音波偵測球體位置, by none-blocking method.
updateUltrasonic();
//PID程序主體
if (millis() - lastPID >= PID_INTERVAL) {
lastPID = millis();
// 讀取距離
if (distanceCM <= 0 || distanceCM > 35) return; //Protection for 異常值
// PID 計算
unsigned long now = millis();
float dt = (now - lastTime) / 1000.0;
if(dt <= 0) dt = 0.001; // 避免 derivative 爆炸
lastTime = now;
error = target - distanceCM; // 正值→超過目標點, 負值→未達目標點
// 誤差在 4cm 之內才積分, 可調。
if (abs(error) < 4.0) { integral += error * dt; }
float derivative = (error - lastError) / dt;
lastError = error;
float output = Kp * error + Ki * integral + Kd * derivative;
// 避免 servo 劇烈震動
output = constrain(output, -30, 30);
// 轉換成 SG90 角度(~90° 為水平)
// 限制 SG90 角度 0~180
float angle = constrain(SvoZero + output, 0, 180);
myservo.write(angle);
//調教參考資訊輸出
Serial.print("Target:"); Serial.print(target);
Serial.print(" , distanceCM(cm):"); Serial.print(distanceCM);
Serial.print(" , Error:"); Serial.print(error);
Serial.print(" , Angle:"); Serial.print(angle);
Serial.print(" , sw:"); Serial.print(sw);
Serial.print(" , kp:"); Serial.print(Kp);
Serial.print(" , ki:"); Serial.print(Ki);
Serial.print(" , kd:"); Serial.println(Kd);
//兩點target調教
if(sw){
if(swTarget){
if(TargetCnt++>400){ swTarget=false; target = 5.0; TargetCnt = 0; }
}
else{
if(TargetCnt++>400){ swTarget=true; target = 10.0; TargetCnt = 0; }
}
}
}
}
