小組成員 : 411214227 楊皓宇 411214223 翁永傑 411214 徐浩程
動機
作品介紹
藍牙遙控車 :本作品為一台可透過手機操控的自走車,利用藍牙模組實現與手機的無線連接,使用者可透過專屬應用程式或藍牙指令控制車輛的前進、後退與轉向,操作直覺、簡便,展示了無線通訊與嵌入式控制的應用整合。
循線車 :本作品為一台能自動沿著黑線行駛的循線車,透過紅外線感測器偵測路徑,並自動調整方向以維持在線上行駛。展現了感測器應用與自動控制技術的結合,實現簡易的智慧型導航功能。
原理
藍牙遙控車:
主要由手機端與車體端兩部分組成。手機端利用 App Inventor 自行設計操作介面,透過 HC-05 藍牙模組 將控制指令(如前進、後退、左轉、右轉、停止)傳送至體。 車體端使用微控制器(如 Arduino)接收來自藍牙模組的訊號,經過程式解碼後,控制馬達運作以實現相對應的動作。整體系統以無線藍牙通訊為核心,實現遠端即時控制,展現了手機應用設計與嵌入式系統整合的應用能力。
循線車:
整體架構與驅動模組
自走車以雙輪差速驅動(differential drive)為基礎,左右輪各由一顆直流馬達驅動。
L298N 為 H-bridge 馬達驅動模組,可透過 PWM 控制馬達速度,並可透過兩個數位腳位控制方向(正轉/反轉)。
Arduino 讀取紅外線循跡感測器(IR sensor)的數位輸出,並依此決定左右馬達的啟停或轉向。
循線功能與感測邏輯
紅外線循跡感測器基於發射與接收紅外線,遇到白或亮底時反射強,遇到黑線或暗底時反射弱;透過內部比較電路轉成高/低電位的數位訊號(0/1)。
兩側感測器分別裝於車底,間距、距地高度與靈敏度需調整到合適位置,以在既能清楚辨識黑/白差異,又避免對地板細微凹凸過度敏感。
程式邏輯為:
兩側同時偵測到紅外線(此時兩側均讀到 0):車輛向前。
只有一側偵測到紅外線(左或右其中一側訊號變化):判定需向相反方向轉彎,以將車身導回線上。
兩側同時未偵測到紅外線(兩側均讀到 1):特殊情況(如車子被拿起),車子停止
轉彎方式:
最初嘗試「一側車輪停止、一側車輪正轉」來實現轉向,雖簡單但在速度較快或賽道曲線較急時容易過彎失控。
改為「當觸線轉彎時,加入另一輪的反轉」,即一側正轉、另一側反轉,形成就地旋轉,可大幅縮短轉彎半徑,提高轉向靈活度並減少偏移出界的機率。
PWM 調速:
根據左右馬達特性和載重,必須微調 PWM 佔空比,以讓車輛直行時保持較直的軌跡。如一邊馬力偏弱、轉向就會偏移。
代碼
#include <SoftwareSerial.h>
SoftwareSerial BT(2, 3); // RX | TX
char cmd;
bool isRunning = false;
String currentAction = "";
// 左馬達控制設定
const byte LEFT1 = 8;
const byte LEFT2 = 9;
const byte LEFT_PWM = 10;
// 右馬達控制設定
const byte RIGHT1 = 7;
const byte RIGHT2 = 6;
const byte RIGHT_PWM = 5;
// 設定PWM輸出值(車速)
byte motorspeed = 200;
void forward() {
digitalWrite(LEFT1, HIGH);
digitalWrite(LEFT2, LOW);
analogWrite(LEFT_PWM, motorspeed);
Serial.println("左輪: 前進");
digitalWrite(RIGHT1, HIGH);
digitalWrite(RIGHT2, LOW);
analogWrite(RIGHT_PWM, motorspeed);
Serial.println("右輪: 前進");
}
void backward() {
digitalWrite(LEFT1, LOW);
digitalWrite(LEFT2, HIGH);
analogWrite(LEFT_PWM, motorspeed);
Serial.println("左輪: 後退");
digitalWrite(RIGHT1, LOW);
digitalWrite(RIGHT2, HIGH);
analogWrite(RIGHT_PWM, motorspeed);
Serial.println("右輪: 後退");
}
void turnLeft() {
analogWrite(LEFT_PWM, 0);
digitalWrite(LEFT1, LOW);
digitalWrite(LEFT2, HIGH);
analogWrite(LEFT_PWM, motorspeed);
digitalWrite(RIGHT1, HIGH);
digitalWrite(RIGHT2, LOW);
analogWrite(RIGHT_PWM, motorspeed);
Serial.println("左轉: 右輪前進,左輪停止");
}
void turnRight() {
digitalWrite(LEFT1, HIGH);
digitalWrite(LEFT2, LOW);
analogWrite(LEFT_PWM, motorspeed);
analogWrite(RIGHT_PWM, 0);
digitalWrite(RIGHT1, LOW);
digitalWrite(RIGHT2, HIGH);
analogWrite(RIGHT_PWM, motorspeed);
Serial.println("右轉: 左輪前進,右輪停止");
}
void stopMotor() {
analogWrite(LEFT_PWM, 0);
analogWrite(RIGHT_PWM, 0);
digitalWrite(LEFT1, LOW);
digitalWrite(LEFT2, LOW);
digitalWrite(RIGHT1, LOW);
digitalWrite(RIGHT2, LOW);
isRunning = false;
Serial.println("馬達停止");
}
void setup() {
Serial.begin(9600);
BT.begin(38400);
pinMode(LEFT1, OUTPUT);
pinMode(LEFT2, OUTPUT);
pinMode(LEFT_PWM, OUTPUT);
pinMode(RIGHT1, OUTPUT);
pinMode(RIGHT2, OUTPUT);
pinMode(RIGHT_PWM, OUTPUT);
Serial.println("系統啟動");
}
void loop() {
if (BT.available()) {
cmd = BT.read();
if (cmd != 'w' && cmd != 'x' && cmd != 'a' && cmd != 'd' && cmd != 's') {
Serial.print("忽略無效指令:");
Serial.println(cmd);
return;
}
Serial.print("收到藍牙指令:");
Serial.println(cmd);
if (cmd == 's') {
stopMotor();
currentAction = "";
isRunning = false;
} else if (!isRunning) {
switch (cmd) {
case 'w':
currentAction = "forward";
forward();
isRunning = true;
break;
case 'x':
currentAction = "backward";
backward();
isRunning = true;
break;
case 'a':
currentAction = "turnLeft";
turnLeft();
isRunning = true;
break;
case 'd':
currentAction = "turnRight";
turnRight();
isRunning = true;
break;
}
}
}
if (isRunning) {
if (currentAction == "forward") forward();
else if (currentAction == "backward") backward();
else if (currentAction == "turnLeft") turnLeft();
else if (currentAction == "turnRight") turnRight();
}
}
#include <SoftwareSerial.h>
SoftwareSerial BT(2, 3);
char cmd;
const byte LEFT1 = 8;
const byte LEFT2 = 9;
const byte LEFT_PWM = 10;
const byte RIGHT1 = 7;
const byte RIGHT2 = 6;
const byte RIGHT_PWM = 5;
const byte IR_RIGHT = 2;
const byte IR_LEFT = 3;
byte motorspeed_L = 115;
byte motorspeed_R = 145;
void setup(){
pinMode(LEFT1, OUTPUT);
pinMode(LEFT2, OUTPUT);
pinMode(LEFT_PWM, OUTPUT);
pinMode(RIGHT1, OUTPUT);
pinMode(RIGHT2, OUTPUT);
pinMode(RIGHT_PWM, OUTPUT);
pinMode(IR_LEFT, INPUT);
pinMode(IR_RIGHT, INPUT);
Serial.begin(9600); // 開啟序列監控
}
void forward(){
digitalWrite(LEFT1, HIGH);
digitalWrite(LEFT2, LOW);
analogWrite(LEFT_PWM, motorspeed_L);
digitalWrite(RIGHT1, LOW);
digitalWrite(RIGHT2, HIGH);
analogWrite(RIGHT_PWM, motorspeed_R);
}
void backward(){
digitalWrite(LEFT1, LOW);
digitalWrite(LEFT2, HIGH);
analogWrite(LEFT_PWM, motorspeed_L);
digitalWrite(RIGHT1, HIGH);
digitalWrite(RIGHT2, LOW);
analogWrite(RIGHT_PWM, motorspeed_R);
}
void turnLeft(){
digitalWrite(RIGHT1, LOW);
digitalWrite(RIGHT2, HIGH);
analogWrite(RIGHT_PWM, motorspeed_R);
digitalWrite(LEFT1, LOW);
digitalWrite(LEFT2, HIGH);
analogWrite(LEFT_PWM, motorspeed_L);
}
void turnRight(){
digitalWrite(LEFT1, HIGH);
digitalWrite(LEFT2, LOW);
analogWrite(LEFT_PWM, motorspeed_L);
digitalWrite(RIGHT1, HIGH);
digitalWrite(RIGHT2, LOW);
analogWrite(RIGHT_PWM, motorspeed_R);
}
void stopMotor() {
analogWrite(LEFT_PWM, 0);
analogWrite(RIGHT_PWM, 0);
}
void loop() {
int leftIR = digitalRead(IR_LEFT);
int rightIR = digitalRead(IR_RIGHT);
/*
Serial.print("左IR腳位 (D3): ");
Serial.print(leftIR);
Serial.print(" | 右IR腳位 (D2): ");
Serial.println(rightIR);
*/
if (leftIR == 1 and rightIR == 1){
stopMotor();
} else if (rightIR == 1){
turnRight();
delay(250);
} else if (leftIR == 1){
turnLeft();
delay(250);
} else {
forward();
}
}
藍芽遙控車app介面
藍芽遙控車app 積木代碼
功能包括:
連結藍芽:通過 BluetoothClient1 與車子建立連接。
確認連結狀態:在 app 上顯示 Label1 的字眼及顏色(例如綠色表示已連結,紅色表示未連結),方便使用者確認是否成功連線。
全域變數 currentActiveButton:設置這個變數目的是防止同時按下多個按鈕導致訊息錯亂,確保操作順暢。
ButtonBackward(後退):觸發 Clock1 計時器,設定 currentActiveButton 為 "Backward",並透過藍芽發送後退指令。
ButtonForward(前進):觸發 Clock3 計時器,設定 currentActiveButton 為 "Forward",發送前進指令。
ButtonLeft(左轉):觸發 Clock2 計時器,設定 currentActiveButton 為 "Left",發送左轉指令。
ButtonRight(右轉):觸發 Clock4 計時器,設定 currentActiveButton 為 "Right",發送右轉指令。
每個流程都使用藍芽客戶端 BluetoothClient1 發送特定字元(例如 "s"),並在計時器結束時停止動作。
後退(Backward):當 Clock1 計時器觸發時,發送後退指令,計時 450 毫秒後停止。
前進(Forward):當 Clock3 計時器觸發時,發送前進指令,計時 450 毫秒後停止。
左轉(Left):當 Clock2 計時器觸發時,發送左轉指令,計時 450 毫秒後停止。
右轉(Right):當 Clock4 計時器觸發時,發送右轉指令,計時 450 毫秒後停止。
每個流程都依賴 currentActiveButton 來確保只有一個動作同時執行,避免指令衝突。
成果影片
心得
楊皓宇 :
一開始,我發現藍芽模組的連線經常不穩定,遙控車在運行時會斷線。我最初懷疑是Arduino的波特率(baud rate)設定錯誤,導致藍芽通訊不穩定。於是我反覆檢查程式碼與波特率設定,確認與藍芽模組匹配,但問題依然存在。後來,我注意到一個現象:當Arduino接上電腦供電時,藍芽連線與車子運作都正常;但當使用電池供電時,車子行駛到一半,藍芽連線就會斷開。經過進一步排查,我發現問題的根源在於電源分配。馬達啟動時,車子重量導致馬達需要較大的電流,而藍芽模組與馬達是並聯供電的。電池的電流輸出有限,馬達啟動時搶走了大部分電流,導致藍芽模組供電不足,進而斷線。我為馬達和藍芽模組分別使用獨立的電源確保藍芽模組有穩定的電壓和電流,本來是想利用電容來穩定電源突波,減少馬達啟動時對藍芽模組的干擾,但是奈何資源不夠,所以只好利用而外的獨立電源。這次製作讓我深刻體會到電路設計中電源管理的重要性,從一開始懷疑波特率,到最終發現電源問題,我學會了如何有系統地排查問題,先從軟體檢查,再到硬體測試,逐步縮小問題範圍。馬達等高耗電元件對電路的影響遠比我預想的複雜,必須考慮電流分配與穩壓設計。未來,我會更加注意電源管理,並在設計初期就考慮電流與電壓的分配問題。這次經驗不僅提升了我的技術能力,也增強了我對物理與電子學的興趣。
翁永傑:
一開始我們的循線車非常不穩定,連走直線都會直接出界,因此我們一步步排查問題,先測試感測器,透過序列埠連續印出訊號才發現原來此模組輸出0/1是相反的,需要在程式裡修正判斷,修正好後,靜態測試結果是沒問題的,但動態測試中,車子移動還是直接出界,我們猜測是模組反應不靈敏所導致,因此需要讓感測器有更長反應時間。
我們嘗試加粗軌道黑線,但發現這會導致轉向過頭,因此放棄。調整左右馬達的 PWM 輸入,希望車子降速,發現輸入太小馬達轉不起來,夠力了又跑太快出界,且即使降速,能提升的偵測時間也是微乎其微。後來,隊友提出嘗試原地轉向的設計,嘗試前原本我覺得不會成功,因為這會讓轉向速度更快,偵測時間更少,但實際測試結果,車子竟然開始有走線的感覺,能實現偵測到黑線並旋轉。
我們細調 delay 時間,也曾試過非阻塞 millis() 的做法,想即時打斷並修正轉向,但實際測試發現對我們目前硬體特性與時間精度需求而言,非阻塞設計反而帶來複雜狀態管理的困擾 ,最後還是回到簡單的阻塞式延遲,在觸線時以固定延遲 delay() 完成明確的轉向動作,雖然這樣做會少了部分即時調整的能力,但在調整 PWM、感測器靈敏度和車速時,我們可以更直接看到車子的反應,更快找到合適的參數,實驗中更容易掌握整體行為並做好記錄。
經過我們反覆測試不同的馬達功率、感測器高度與轉向持續時間,發現配合合理的延遲時間和完整的測試流程,大部分情況下都能有效避免越界,讓我們能測試更複雜的路線。此外,每次只更動一個變數並記錄結果,確保思路清晰,這段經驗讓我們體會到,設計複雜度要與硬體能力匹配,過度追求理論上的快速反應不一定最實際;反而透過簡單穩定的邏輯、不斷實驗與微調,能更快達到穩定。
徐浩程:
這次製作自走車的過程中,我獲得了許多寶貴的實作經驗。剛開始時,循跡車的表現非常不穩定,常常無法保持直行。我們先從感測器著手檢查,發現輸出訊號的邏輯與預期中的相反,因此在程式中做了修正。雖然靜態測試結果良好,但車輛在行駛中仍然容易偏離軌道,說明實際運動中還存在許多複雜因素需要克服。
我們嘗試過加粗軌道線條、調整馬達速度,但這些方式都未能有效解決問題。後來有人提出了讓車子原地轉向的策略,起初我對此抱持懷疑,擔心控制會更難,但實際測試後發現,這種做法反而大幅提升了循跡的穩定性。最終,我們採用了簡單且明確的延遲控制(delay),配合適當的轉向時間,讓車子的行動更加穩定且易於調整。
這次經驗讓我體會到,成功的關鍵不在於追求複雜的控制技術,而是在於找出與硬體相容且穩定的解決方案。經過不斷測試和細心調整,我們不僅讓自走車穩定循跡,也增強了解決問題與實驗調整的能力。