在機器人製作和自動化專案中,精準的馬達控制是核心關鍵之一。LEGO Mindstorms EV3 提供了一個高度整合、易於使用的馬達系統,而 Arduino 則提供了一個更具彈性、成本更低但需要更多電子知識的平台。
本教學將分為兩部分:
1. 分析 LEGO EV3 馬達的特色與功能。
2. 說明如何使用 Arduino Uno 搭配相應硬體,來實現與 EV3 馬達類似的強大功能。
LEGO EV3 的馬達之所以強大且適合教學,主要歸功於其內建的整合式設計。使用者無需處理複雜的電路和底層程式碼,即可實現精準控制。
這是 EV3 馬達最核心的功能。每個大型和中型馬達都內建一個高精度的旋轉感測器(Encoder),能夠:
* 測量旋轉角度:可以精確知道馬達轉了幾度,精度高達 1 度。
* 計算旋轉圈數:可以累計馬達總共轉了多少圈。
* 提供即時回饋:程式可以隨時讀取馬達的當前位置,實現閉環控制(Closed-loop Control)。
EV3 馬達是「伺服馬達」,意味著它們不僅僅是轉動,更能:
* 維持特定速度:你可以命令馬達以設定的功率(速度)穩定轉動,EV3 主機會自動調整功率以應對負載變化。
* 轉到指定位置:可以命令馬達轉到某個絕對角度(例如:90度位置)並保持不動。
* 鎖定位置:在停止時,馬達會主動施加力量抵抗外力,以維持目前的位置。
使用 Arduino 要達到 EV3 馬達的功能,我們無法使用單一元件,而需要組合一個系統。核心思想是「馬達 + 感測器 + 驅動器」。
Arduino 可以控制多種馬達,每種都有其獨特的應用場景。
analogWrite()) 控制轉速。Servo.h 函式庫,控制非常方便。要讓一個普通的直流馬達(DC Motor)變得像 EV3 馬達一樣「聰明」,關鍵就在於編碼器(Encoder)。
一個基本的直流馬達是開環的——你給它電,它就轉,但你不知道它轉了多快、轉了多少圈。編碼器提供了一個回饋迴路,把馬達的物理運動轉換成 Arduino 能讀懂的電子訊號,從而實現閉環控制(Closed-loop Control)。它回答了兩個關鍵問題:
1. 我轉了多遠? (位置)
2. 我轉了多快? (速度)
市面上常見的編碼器通常已和小型直流減速馬達整合在一起,方便直接使用。
* 霍爾效應編碼器 (Hall Effect Encoder):通常與黃色的 TT 減速馬達配套,在馬達尾部有一個小磁環和兩個霍爾感測器。精度相對較低,但便宜耐用,適合入門。
* 光學編碼器 (Optical Encoder):如 JGA25-370、Pololu 等品牌的馬達,內建了光學碼盤,精度較高,反應更靈敏,適合需要精準控制的場合。
這是在選購或使用零件時非常關鍵的一點。
1. 看接線數量(最準確的方法)
2. 看外觀
結論:一個只有兩條線的 TT 馬達,絕對沒有編碼器。
馬達轉速可能很快,每秒產生數千個脈衝。如果使用傳統的 digitalRead() 在主迴圈 loop() 中讀取,極易錯過脈衝,導致計數不準。正確的方法是使用「外部中斷」(External Interrupts)。
這是一個只讀取編碼器數值並透過序列埠輸出的基礎範例。
// --- 硬體定義 ---
// 將編碼器的A相和B相輸出分別接到Arduino的2號和3號腳位
// 這兩個腳位在Arduino Uno上支援外部中斷
#define ENCODER_A_PIN 2
#define ENCODER_B_PIN 3
// --- 全域變數 ---
// 使用 volatile 關鍵字,因為這個變數會在中斷函式中被修改
// 確保主程式能讀取到最新的值
volatile long encoder_pos = 0;
void setup() {
// 初始化序列埠,用於顯示結果
Serial.begin(9600);
// 設定編碼器腳位為輸入模式,並啟用內部上拉電阻
// 這樣可以避免腳位懸空時的訊號干擾
pinMode(ENCODER_A_PIN, INPUT_PULLUP);
pinMode(ENCOER_B_PIN, INPUT_PULLUP);
// 設定中斷
// 當 ENCODER_A_PIN 的電位發生變化 (RISING, FALLING, CHANGE) 時,呼叫 do_encoder 函式
attachInterrupt(digitalPinToInterrupt(ENCODER_A_PIN), do_encoder, CHANGE);
}
void loop() {
// 在主迴圈中,每50毫秒讀取一次計數值並輸出
// 注意這裡並沒有做任何複雜操作,以確保能即時反應中斷的結果
Serial.print("Encoder Position: ");
Serial.println(encoder_pos);
delay(50);
}
// --- 中斷服務函式 (ISR) ---
// 每次 ENCODER_A_PIN 腳位的電位改變時,此函式就會被觸發
void do_encoder() {
// 讀取A相和B相的目前狀態
int state_a = digitalRead(ENCODER_A_PIN);
int state_b = digitalRead(ENCODER_B_PIN);
// 根據A相和B相的電位組合來判斷方向
// 這是標準的正交編碼器解碼邏輯
if (state_a == state_b) {
encoder_pos++; // 順時針轉動
} else {
encoder_pos--; // 逆時針轉動
}
}
僅有編碼器我們只能「知道」馬達的狀態,但要「控制」它穩定在某個速度或位置,就需要閉環控制演算法,其中最經典的就是 PID 控制。
PID 是一種回饋控制演算法,它會持續計算一個「誤差值」(Error,即 目標值 - 目前值),並試圖最小化這個誤差。它由三個部分組成:
P - 比例 (Proportional)
P_output = Kp * errorI - 積分 (Integral)
I_output = Ki * (累積誤差)Ki 值太大,可能導致系統震盪(Overshoot)。D - 微分 (Derivative)
D_output = Kd * (目前誤差 - 上次誤差)三者結合,最終輸出 = P_output + I_output + D_output,就能達到快速、穩定且精準的控制效果。Kp, Ki, Kd 這三個常數需要根據實際的馬達和負載進行「調參」(Tuning),以達到最佳效果。
這個範例整合了編碼器讀取和 PID 速度控制。
// --- 硬體定義 ---
#define ENCODER_A_PIN 2
#define ENCODER_B_PIN 3
#define MOTOR_PWM_PIN 5 // 馬達速度控制 PWM 腳位
#define MOTOR_DIR_PIN 4 // 馬達方向控制腳位
// --- PID 控制參數 ---
// !!注意!! 這些值需要根據你的馬達和負載進行調整 (Tuning)
double Kp = 2.0;
double Ki = 5.0;
double Kd = 1.0;
// --- PID 運算變數 ---
double target_speed = 100; // 目標速度 (單位: 脈衝數/取樣週期)
double current_speed = 0;
double error = 0;
double last_error = 0;
double integral_error = 0;
double derivative_error = 0;
int motor_output = 0;
// --- 編碼器變數 ---
volatile long encoder_pos = 0;
long last_encoder_pos = 0;
// --- 時間控制變數 ---
long last_time = 0;
void setup() {
Serial.begin(9600);
// 編碼器設定
pinMode(ENCODER_A_PIN, INPUT_PULLUP);
pinMode(ENCODER_B_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_A_PIN), do_encoder, CHANGE);
// 馬達驅動設定
pinMode(MOTOR_PWM_PIN, OUTPUT);
pinMode(MOTOR_DIR_PIN, OUTPUT);
digitalWrite(MOTOR_DIR_PIN, HIGH); // 設定一個固定方向
last_time = millis();
}
void loop() {
// --- 計算當前速度 ---
long current_time = millis();
long dt = current_time - last_time;
// 每隔 10ms 計算一次速度和PID
if (dt >= 10) {
// 速度 = (目前位置 - 上次位置) / 時間差
current_speed = (encoder_pos - last_encoder_pos) / (double)dt;
// --- PID 計算 ---
error = target_speed - current_speed;
integral_error += error * dt;
derivative_error = (error - last_error) / dt;
// 計算PID總輸出
double pid_output = (Kp * error) + (Ki * integral_error) + (Kd * derivative_error);
// --- 驅動馬達 ---
// 限制輸出範圍在 0-255 之間
motor_output = constrain(pid_output, 0, 255);
analogWrite(MOTOR_PWM_PIN, motor_output);
// --- 更新變數 ---
last_encoder_pos = encoder_pos;
last_error = error;
last_time = current_time;
// --- 監控輸出 ---
Serial.print("Target: "); Serial.print(target_speed);
Serial.print(" | Current: "); Serial.print(current_speed);
Serial.print(" | Output: "); Serial.println(motor_output);
}
}
// --- 中斷服務函式 (ISR) ---
void do_encoder() {
if (digitalRead(ENCODER_A_PIN) == digitalRead(ENCODER_B_PIN)) {
encoder_pos++;
} else {
encoder_pos--;
}
}
VCC 和 GND。OUT1, OUT2)接到直流馬達的兩個接腳。GND 與 L298N 的 GND 相連(共地)。~5, 4)接到 L298N 的 ENA, IN1 等對應腳位。VCC 和 GND 接到 Arduino 的 5V 和 GND。D2 和 D3)。| EV3 功能 | Arduino 實現方式 |
|---|---|
| 讀取旋轉角度/圈數 | 使用中斷讀取編碼器脈衝數,再透過數學換算得到。 |
| 以特定速度轉動 | 結合編碼器回饋和 PID 控制演算法,動態調整 PWM 輸出,實現精準的閉環速度控制。 |
| 轉到指定位置 | 結合編碼器回饋和 PID 位置控制演算法,計算與目標位置的誤差,並驅動馬達消除誤差。 |
本章節將前面討論的零件整合起來,建立一台可以前進、後退、轉彎和調速的雙輪機器人。我們將使用最常見的 L298N 馬達驅動板和兩個標準的 TT 馬達(無編碼器版本,做開環控制)。
重要:接線前請確保所有電源都已斷開!
L298N 電源設定:
+12V 端子。GND 端子。GND 端子用一條杜邦線連接到 Arduino 的任意一個 GND 腳位。(此為共地,非常重要!)連接左輪馬達 (L298N 的 Motor A):
OUT1 和 OUT2 端子。L298N IN1 <--> Arduino D7L298N IN2 <--> Arduino D6L298N ENA <--> Arduino D5 (注意:D5 是 PWM 腳位)連接右輪馬達 (L298N 的 Motor B):
OUT3 和 OUT4 端子。L298N IN3 <--> Arduino D4L298N IN4 <--> Arduino D3 (注意:D3 是 PWM 腳位)L298N ENB <--> Arduino D9 (注意:D9 是 PWM 腳位)為 Arduino 供電:
這個程式提供了控制機器人基本動作的函式,並在主迴圈中進行簡單的展示。
// --- 左輪馬達 (Motor A) 的控制腳位 ---
#define ENA 5 // 速度控制 (PWM)
#define IN1 7 // 方向控制
#define IN2 6 // 方向控制
// --- 右輪馬達 (Motor B) 的控制腳位 ---
#define ENB 9 // 速度控制 (PWM)
#define IN3 4 // 方向控制
#define IN4 3 // 方向控制
void setup() {
// 將所有控制腳位設定為輸出模式
pinMode(ENA, OUTPUT);
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
pinMode(ENB, OUTPUT);
pinMode(IN3, OUTPUT);
pinMode(IN4, OUTPUT);
// 初始狀態讓馬達停止
stop_motors();
}
void loop() {
// --- 動作展示 ---
// 全速前進 2 秒
move_forward(255); // 速度值 0-255
delay(2000);
// 停止 1 秒
stop_motors();
delay(1000);
// 半速後退 2 秒
move_backward(150); // 速度值 0-255
delay(2000);
// 停止 1 秒
stop_motors();
delay(1000);
// 全速原地右轉 1 秒
turn_right(255);
delay(1000);
// 停止 1 秒
stop_motors();
delay(1000);
// 全速原地左轉 1 秒
turn_left(255);
delay(1000);
// 停止 1 秒
stop_motors();
delay(1000);
}
// --- 運動控制函式 ---
// 前進
void move_forward(int speed) {
// 左輪前轉
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
analogWrite(ENA, speed);
// 右輪前轉
digitalWrite(IN3, HIGH);
digitalWrite(IN4, LOW);
analogWrite(ENB, speed);
}
// 後退
void move_backward(int speed) {
// 左輪後轉
digitalWrite(IN1, LOW);
digitalWrite(IN2, HIGH);
analogWrite(ENA, speed);
// 右輪後轉
digitalWrite(IN3, LOW);
digitalWrite(IN4, HIGH);
analogWrite(ENB, speed);
}
// 原地右轉
void turn_right(int speed) {
// 左輪前轉
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
analogWrite(ENA, speed);
// 右輪後轉
digitalWrite(IN3, LOW);
digitalWrite(IN4, HIGH);
analogWrite(ENB, speed);
}
// 原地左轉
void turn_left(int speed) {
// 左輪後轉
digitalWrite(IN1, LOW);
digitalWrite(IN2, HIGH);
analogWrite(ENA, speed);
// 右輪前轉
digitalWrite(IN3, HIGH);
digitalWrite(IN4, LOW);
analogWrite(ENB, speed);
}
// 停止
void stop_motors() {
// 左輪停止
digitalWrite(I1, LOW);
digitalWrite(IN2, LOW);
analogWrite(ENA, 0);
// 右輪停止
digitalWrite(IN3, LOW);
digitalWrite(IN4, LOW);
analogWrite(ENB, 0);
}
上一章的開環控制(Open-loop Control)很簡單,但有明顯缺點:
* 無法走直線:兩個馬達的實際速度幾乎總有微小差異,導致機器人會慢慢偏向一邊。
* 無法走指定距離:delay(2000) 只能控制「時間」,無法控制「距離」。地面摩擦力、電池電壓變化都會影響實際行走的距離。
本章節將引入編碼器,實現閉環控制(Closed-loop Control),解決上述問題。
大部分硬體與上一章相同,只有馬達需要替換。
馬達驅動部分的接線與上一章完全相同,我們只需要新增編碼器的接線。
共地: L298N、Arduino、電池盒的 GND 必須全部連在一起。
連接左輪編碼器:
編碼器 VCC <--> Arduino 5V編碼器 GND <--> Arduino GND編碼器 A相 <--> Arduino D2 (中斷腳位 0)編碼器 B相 <--> Arduino D8 (任意數位腳位)連接右輪編碼器:
編碼器 VCC <--> Arduino 5V編碼器 GND <--> Arduino GND編碼器 A相 <--> Arduino D3 (中斷腳位 1)編碼器 B相 <--> Arduino D10 (任意數位腳位)這個程式碼會定義一個目標脈衝數,讓機器人走到該距離後停止。並在行進中,不斷比較左右輪的脈衝數,微調速度以保持直線。
// --- 馬達控制腳位定義 (與上一版相同) ---
#define ENA 5
#define IN1 7
#define IN2 6
#define ENB 9
#define IN3 4
#define IN4 3
// --- 編碼器腳位定義 ---
#define LEFT_ENCODER_A 2 // 中斷 0
#define LEFT_ENCODER_B 8
#define RIGHT_ENCODER_A 3 // 中斷 1
#define RIGHT_ENCODER_B 10
// --- 全域變數 ---
volatile long left_pos = 0;
volatile long right_pos = 0;
void setup() {
Serial.begin(9600);
// 設定馬達腳位為輸出
pinMode(ENA, OUTPUT);
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
pinMode(ENB, OUTPUT);
pinMode(IN3, OUTPUT);
pinMode(IN4, OUTPUT);
// 設定編碼器腳位為輸入 (啟用上拉電阻)
pinMode(LEFT_ENCODER_A, INPUT_PULLUP);
pinMode(LEFT_ENCODER_B, INPUT_PULLUP);
pinMode(RIGHT_ENCODER_A, INPUT_PULLUP);
pinMode(RIGHT_ENCODER_B, INPUT_PULLUP);
// 設定中斷
attachInterrupt(digitalPinToInterrupt(LEFT_ENCODER_A), read_left_encoder, RISING);
attachInterrupt(digitalPinToInterrupt(RIGHT_ENCODER_A), read_right_encoder, RISING);
stop_motors();
}
void loop() {
Serial.println("準備前進 1000 脈衝...");
move_forward_distance(1000, 200); // 目標脈衝數, 基礎速度
stop_motors();
Serial.println("已停止.");
Serial.print("左輪最終位置: "); Serial.println(left_pos);
Serial.print("右輪最終位置: "); Serial.println(right_pos);
delay(5000); // 等待5秒後重複
}
// --- 運動控制函式 ---
// 走指定距離 (脈衝數)
void move_forward_distance(long target_pos, int base_speed) {
// 重置計數器
left_pos = 0;
right_pos = 0;
// 簡易 P 控制器 (比例控制器) 參數
float Kp = 0.5;
while (left_pos < target_pos && right_pos < target_pos) {
// 計算左右輪誤差
long error = left_pos - right_pos;
// 根據誤差調整速度
int left_speed = base_speed - (error * Kp);
int right_speed = base_speed + (error * Kp);
// 限制速度範圍
left_speed = constrain(left_speed, 0, 255);
right_speed = constrain(right_speed, 0, 255);
// 驅動馬達
set_motor(IN1, IN2, ENA, left_speed, 1); // 1 代表前進
set_motor(IN3, IN4, ENB, right_speed, 1); // 1 代表前進
// 透過序列埠監控
Serial.print("L: "); Serial.print(left_pos);
Serial.print(" R: "); Serial.print(right_pos);
Serial.print(" Error: "); Serial.print(error);
Serial.print(" L_Spd: "); Serial.print(left_speed);
Serial.print(" R_Spd: "); Serial.println(right_speed);
}
}
// 停止
void stop_motors() {
set_motor(IN1, IN2, ENA, 0, 0);
set_motor(IN3, IN4, ENB, 0, 0);
}
// 泛用馬達設定函式
// direction: 1=前, 0=停, -1=後
void set_motor(int in_a, int in_b, int en, int speed, int direction) {
if (direction == 1) {
digitalWrite(in_a, HIGH);
digitalWrite(in_b, LOW);
} else if (direction == -1) {
digitalWrite(in_a, LOW);
digitalWrite(in_b, HIGH);
} else {
digitalWrite(in_a, LOW);
digitalWrite(in_b, LOW);
}
analogWrite(en, speed);
}
// --- 中斷服務函式 (ISRs) ---
void read_left_encoder() {
// 簡單的計數,如果需要判斷方向,則需讀取 B 相
left_pos++;
}
void read_right_encoder() {
right_pos++;
}
move_forward_distance(1000, ...) 中的 1000 是一個脈衝數。你需要自己進行校正,例如讓它跑 1000 脈衝,然後用尺量一下實際走了多少公分,這樣就能算出 脈衝數/公分 的比例,未來就能讓機器人走精確的物理距離。float Kp = 0.5; 是 P 控制器的增益值。如果你的機器人修正過頭,左右搖擺,可以試著調低 Kp 值(如 0.3)。如果修正力道不足,依然走偏,可以試著調高 Kp 值(如 0.8)。這個值需要實驗來找到最佳點。digitalRead(LEFT_ENCODER_B) 來判斷是該 ++ 還是 --。