登月小車 AMB82 MINI AI 自駕車(五)自動駕駛:循線、辨識標誌與參數調校

本系列使用 登月小車(AMB82 MINI),從硬體安裝、蒐集與標註資料、訓練 YOLOv7-tiny 模型,一路做到能辨識交通標誌的自駕小車。這是第五篇。
這個單元我們會將轉換後的模型放入SD Card,讓程式讀取模型並控制登月小車。
5-1. 將模型放入SD Card
官方有制定放入規則,我們必須遵守才能讓程式讀取到模型,首先將SD Card 透過讀卡機接上電腦。
將之前的圖片全部刪除,並新增一個名為「NN_MDL」的資料夾,這個名稱不可更改。

接著將轉換好的模型複製到這個資料夾內,並改名為「yolov7_tiny」,這個名稱也是規定,不要隨意更改。

然後就可以將SD Card 退出電腦,並插在AMB82 MINI 上。
5-2. 自動駕駛程式撰寫
因為程式碼很長,所以挑重點說明:
宣告兩組 VideoSetting 物件,一組作為 RTSP 720p 影像串流(提供手機或電腦即時監看並疊加 OSD),另一組專門作為神經網路(NN)輸入。configNN 設定為 576×320、20 FPS 與 VIDEO_RGB 格式,精準符合客製化 YOLOv7-tiny 模型的輸入長寬比與推論頻率。
VideoSetting config(VIDEO_HD, 30, VIDEO_H264, 0); // RTSP 串流 1280x720 (16:9)
VideoSetting configNN(NNWIDTH, NNHEIGHT, 20, VIDEO_RGB, 0); // NN 輸入 576x320,20 FPS
config.setBitrate(2 * 1024 * 1024);
Camera.configVideoChannel(CHANNEL, config);
Camera.configVideoChannel(CHANNELNN, configNN);
Camera.videoInit();
因為 AMB82 在小車機構上是上下顛倒安裝,所以也需要將取得的畫面旋轉 180 度才可以。CAMERA_ORIENTATION 設定為 3,代表進行 180 度(水平鏡像 + 垂直翻轉)。必須在 Camera.channelBegin() 啟動頻道後呼叫 isp_set_orientation(3),使 ISP 在底層完成硬體翻轉,確保 RTSP 畫面、OSD 標籤與神經網路辨識座標皆為正立。
#define CAMERA_ORIENTATION 3
extern "C" {
int isp_set_orientation(int val);
}
// 在 setup() 內、啟動影像頻道後執行:
if (CAMERA_ORIENTATION > 0) {
isp_set_orientation(CAMERA_ORIENTATION);
}
神經網路部分,使用 CUSTOMIZED_YOLOV7TINY 從 SD 卡載入自訂訓練的模型檔(NN_MDL/yolov7_tiny.nb),涵蓋 line、park、speed、stop 四個類別。透過 ObjDet.configThreshold() 在 NPU 底層設定一次性信心門檻(0.60f),低於此門檻的候選框直接在底層過濾,完全免除微控制器二次判定的效能浪費。
ObjDet.configVideo(configNN);
ObjDet.setResultCallback(ODPostProcess);
ObjDet.modelSelect(OBJECT_DETECTION, CUSTOMIZED_YOLOV7TINY, NA_MODEL, NA_MODEL);
ObjDet.configThreshold(NN_CONF_THRESHOLD, NN_NMS_THRESHOLD); // 信心度 60%、NMS 30%
ObjDet.begin();
為避免在數公尺外遠遠看到標誌就提早煞車,程式加入了 BBox 寬度測距過濾機制。藉由計算標誌在畫面中的相對寬度比例,只有當寬度達到門檻(代表小車已接近標誌)時才觸發動作;若距離太遠,則在 OSD 畫面上標示 [FAR] 提示,車輛保持正常行駛。
float boxWidth = item.xMax() - item.xMin();
bool isFar = false;
if ((obj_type == LABEL_PARK && boxWidth < MIN_SIGN_WIDTH_PARK) ||
(obj_type == LABEL_SPEED && boxWidth < MIN_SIGN_WIDTH_SPEED) ||
(obj_type == LABEL_STOP && boxWidth < MIN_SIGN_WIDTH_STOP)) {
isFar = true;
}
// 僅在非遠距標誌時觸發對應控制旗標
if (!isFar) {
if (obj_type == LABEL_PARK) frameParkDetected = true;
if (obj_type == LABEL_SPEED) frameSpeedDetected = true;
if (obj_type == LABEL_STOP) frameStopDetected = true;
}
採用事件驅動(Event-driven)控制架構。捨棄傳統 loop() 內部 delay() 輪詢模式,直接在神經網路完成物件辨識的回呼函式 ODPostProcess() 結尾呼叫 processCarControl()。只要 NPU 一產出辨識結果,微秒之內立即計算轉向差速並寫入馬達 PWM,達成零等待響應。
void ODPostProcess(std::vector<ObjectDetectionResult> results) {
// ... 解析物件、更新旗標、繪製 OSD ...
// 事件驅動:NN 辨識完成瞬間立即執行控制,零輪詢延遲!
processCarControl();
}
循線演算法以小車左側的黃色中線(line)為基準,選取畫面中最靠近車身底部的一段黃色中線,以其右側邊界加上預設車道偏移量 LINE_OFFSET 計算目標前進中心。透過 P 控制器計算偏差量 error,並換算為左右輪差速 steer。根據實車物理特性,左輪快右輪慢會向右轉,因此左輪為 baseSpeed + steer、右輪為 baseSpeed - steer。
float targetX = lineRightEdgeX + LINE_OFFSET;
float error = targetX - 0.5f;
int steer = (int)(KP * error);
int leftSpeed = baseSpeed + steer;
int rightSpeed = baseSpeed - steer;
leftSpeed = constrain(leftSpeed, MIN_SPEED, MAX_SPEED);
rightSpeed = constrain(rightSpeed, MIN_SPEED, MAX_SPEED);
motorDrive(leftSpeed, rightSpeed);
當遇到直角彎或急彎導致鏡頭短暫看不到左側黃色中線時,小車不會在原地亂晃,而是切換至丟線無條件左轉尋線模式。藉由左輪停止(0)、右輪前進(70)的差速,主動把視野外左側的跑道線段拉回鏡頭視野內;若連續遺失超過 LINE_LOST_MAX 幀(預設 80 幀,約 4 秒)仍未見黃色中線,則觸發安全停車機制。
if (lineDetected) {
// 正常 P 控制器循線 ...
} else if (LINE_LOST_MAX == 0 || lineLostCount < LINE_LOST_MAX) {
// 丟線無條件向左轉尋線
int leftSpeed = SEARCH_LEFT_SPEED_L; // 0
int rightSpeed = SEARCH_LEFT_SPEED_R; // 70
motorDrive(leftSpeed, rightSpeed);
} else {
// 超時安全煞停
motorStop();
}
Stop 標誌具備停車 2 秒與防重複觸發冷卻機制。當偵測到 stop 標誌且距離達標時,小車煞停並啟動計時器;2 秒後自動恢復行駛,並隨即進入 5 秒的冷卻防護期(STOP_COOLDOWN_MS),確保小車起步後尚未完全駛離標誌區時,不會被同一個標誌連續二次觸發。
// 停車 2 秒倒數
if (stopHandling) {
motorStop();
if (millis() - stopStartTime >= STOP_DURATION_MS) {
stopHandling = false;
inStopCooldown = true;
stopCooldownEnd = millis() + STOP_COOLDOWN_MS; // 啟動 5 秒防重複觸發冷卻
}
return;
}
// 新觸發(冷卻期內不重複觸發)
if (stopDetected && !inStopCooldown) {
motorStop();
stopHandling = true;
stopStartTime = millis();
return;
}
在背景 loop() 任務中,以按鈕 A(Pin 18)搭配 50ms 軟體防彈跳作為小車的動力總開關。開機時預設停止,按下按鈕切換為運轉模式;同時 I2C 128×64 OLED 即時顯示小車目前 IP、RTSP 埠號、模型狀態與馬達運轉狀態(RUNNING 或 STOPPED)。
int reading = digitalRead(BUTTON_PIN);
if (reading != lastButtonState) {
lastDebounceTime = now;
}
if ((now - lastDebounceTime) > debounceDelay) {
if (reading != currentStableState) {
currentStableState = reading;
if (currentStableState == LOW) {
motorEnabled = !motorEnabled; // 切換馬達運轉狀態
if (!motorEnabled) motorStop();
updateOLED();
}
}
}
lastButtonState = reading;
完整程式碼:
記得輸入自己的Wi-Fi 名稱與密碼
/*
* AMB82 MINI 自駕小車 — YOLOv7-tiny 循線 + 交通標誌辨識控制
*
* 功能:
* 1. 從 SD 卡載入自訂 YOLOv7-tiny 模型 (NN_MDL/yolov7_tiny.nb)
* 2. 偵測 line / park / speed / stop 四種標籤
* 3. 根據 line 的位置進行差速循線(P 控制器)
* 4. park → 完全靜止(直到標誌消失)
* speed → 減速行駛(直到標誌消失後恢復)
* stop → 停車 2 秒再啟動(含冷卻防重複觸發)
* 5. WiFi + RTSP 串流(OSD 疊加辨識框)
*
* 硬體接線:
* 右輪馬達 M1A = Pin 4 (正轉), M1B = Pin 1 (反轉)
* 左輪馬達 M2A = Pin 5 (正轉), M2B = Pin 2 (反轉)
* 攝影機旋轉 180° (isp_set_orientation(3))
* SD 卡內建 MicroSD 槽
*
* Arduino IDE 設定:
* Tools > Load NN Model > Load SD
*/
#include "WiFi.h"
#include "StreamIO.h"
#include "VideoStream.h"
#include "RTSP.h"
#include "NNObjectDetection.h"
#include "VideoStreamOverlay.h"
#include <Wire.h>
#include <Adafruit_OLED_libraries/Adafruit_GFX.h>
#include <Adafruit_OLED_libraries/Adafruit_SSD1306.h>
// =====================================================================
// YOLOv7-tiny 模型類別定義(原 ObjectClassList.h 整合)
// =====================================================================
#define LABEL_LINE 0
#define LABEL_PARK 1
#define LABEL_SPEED 2
#define LABEL_STOP 3
struct ObjectDetectionItem {
uint8_t index;
const char* objectName;
uint8_t filter;
};
// 自訂 YOLOv7-tiny 模型的 4 個類別(Index 必須與模型訓練時的 label 順序一致)
// filter = 1 表示啟用偵測,0 表示忽略
ObjectDetectionItem itemList[4] = {
{LABEL_LINE, "line", 1}, // 小車左側的黃色中線(用於循線)
{LABEL_PARK, "park", 1}, // 停車標誌 → 完全靜止
{LABEL_SPEED, "speed", 1}, // 限速標誌 → 減速行駛
{LABEL_STOP, "stop", 1}, // 停車再開標誌 → 停 2 秒再啟動
};
// =====================================================================
// OLED 顯示器 (SSD1306 I2C 128x64)
// =====================================================================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
bool oledFound = false;
// =====================================================================
// 硬體腳位定義
// =====================================================================
#define M1A 4 // 右輪正轉 (PWM)
#define M1B 1 // 右輪反轉 (PWM)
#define M2A 5 // 左輪正轉 (PWM)
#define M2B 2 // 左輪反轉 (PWM)
#define BUTTON_PIN 18 // 按鈕腳位(INPUT_PULLUP,按下為 LOW)
// =====================================================================
// 速度參數(可依實際測試調整)
// =====================================================================
#define NORMAL_SPEED 100 // 正常行駛速度 (PWM 0~255)
#define SLOW_SPEED 80 // 減速行駛速度 (偵測到 speed 標誌)
#define MIN_SPEED 0 // 最小速度
#define MAX_SPEED 255 // 最大速度
// =====================================================================
// 循線參數(可依實際測試調整)
// =====================================================================
#define LINE_OFFSET 0.25f // line 右邊緣到道路中央的偏移量(歸一化座標 0~1)
#define KP 90 // P 控制器比例增益(調大轉向更猛,見可調參數指南)
#define LINE_LOST_MAX 80 // 連續丟失 line 的最大 frame 數(超過則安全停車;設為 0 表示持續左轉不超時)
// =====================================================================
// 丟線尋線參數(未偵測到 line 時無條件左轉)
// =====================================================================
#define SEARCH_LEFT_SPEED_L 0 // 丟線無條件左轉時左輪速度 (差速轉向)
#define SEARCH_LEFT_SPEED_R 70 // 丟線無條件左轉時右輪速度 (右快左慢帶動向左前進轉向)
// =====================================================================
// NN 信心指數門檻(0.0~1.0)
// 由 ObjDet.configThreshold() 設定一次性全域門檻,NPU 底層直接過濾,無二次判定
// =====================================================================
#define NN_CONF_THRESHOLD 0.60f // 一次信心門檻(調高至 60%,低於此值的結果在底層直接捨棄)
#define NN_NMS_THRESHOLD 0.30f // NMS 重疊抑制門檻
// =====================================================================
// 交通標誌觸發尺寸門檻(歸一化寬度 0.0~1.0,未達門檻表示距離太遠,不觸發動作)
// =====================================================================
#define MIN_SIGN_WIDTH_PARK 0.08f // park 標誌寬度需 >= 畫面 8% 才完全停車
#define MIN_SIGN_WIDTH_SPEED 0.05f // speed 標誌寬度需 >= 畫面 5% 才進入減速
#define MIN_SIGN_WIDTH_STOP 0.10f // stop 標誌寬度需 >= 畫面 10% 才停車 2 秒
// =====================================================================
// 效能優化與除錯開關(加速控制迴圈、降低延遲)
// =====================================================================
#define ENABLE_DEBUG_PRINT 0 // 設為 0 關閉每幀高頻 Serial/printf 輸出,大幅降低 UART 阻塞延遲(預設關閉以加速)
#define ENABLE_OSD_DRAW 1 // 1: 在 RTSP 串流繪製辨識框; 0: 完全關閉 OSD 繪圖以釋放 CPU 與記憶體頻寬
// =====================================================================
// Stop 標誌參數
// =====================================================================
#define STOP_DURATION_MS 2000 // stop 停車持續時間(毫秒)
#define STOP_COOLDOWN_MS 5000 // stop 冷卻期(毫秒,防止重複觸發)
// =====================================================================
// Speed 標誌參數
// =====================================================================
#define SPEED_LOST_FRAMES 100 // speed 消失後延遲恢復的 frame 數
// =====================================================================
// 攝影機 / NN / RTSP 設定
// =====================================================================
#define CHANNEL 0 // RTSP 串流頻道 (H264)
#define CHANNELNN 3 // NN 推論頻道 (RGB)
#define NNWIDTH 576 // NN 輸入寬度
#define NNHEIGHT 320 // NN 輸入高度
#define CAMERA_ORIENTATION 3 // 旋轉 180°(Mirror + Flip)
// ISP 硬體翻轉函式宣告
#ifdef __cplusplus
extern "C" {
#endif
int isp_set_orientation(int val);
#ifdef __cplusplus
}
#endif
// =====================================================================
// 全域物件
// =====================================================================
VideoSetting config(VIDEO_HD, 30, VIDEO_H264, 0); // RTSP 串流 1280x720 (16:9,與 NN 長寬比匹配)
VideoSetting configNN(NNWIDTH, NNHEIGHT, 20, VIDEO_RGB, 0); // NN 輸入 576x320,提升至 20fps 加快取像頻率
NNObjectDetection ObjDet;
RTSP rtsp;
StreamIO videoStreamer(1, 1); // Camera → RTSP
StreamIO videoStreamerNN(1, 1); // Camera → NN
// Wi-Fi 帳密(請依實際環境修改)
char ssid[] = ""; // 記得輸入自己的Wi-Fi 名稱
char pass[] = ""; // 記得輸入自己的Wi-Fi 密碼
int wifiStatus = WL_IDLE_STATUS;
IPAddress ip;
int rtsp_portnum;
// =====================================================================
// 控制狀態變數(由回呼函式更新,loop 中讀取)
// =====================================================================
// --- 循線相關 ---
volatile bool lineDetected = false; // 本幀是否偵測到 line
volatile float lineRightEdgeX = 0.5f; // line 右邊緣的歸一化 X 座標
volatile int lineLostCount = 0; // 連續丟失 line 的 frame 計數
// --- 標誌旗標 ---
volatile bool parkDetected = false; // 本幀是否偵測到 park
volatile bool speedDetected = false; // 本幀是否偵測到 speed
volatile bool stopDetected = false; // 本幀是否偵測到 stop
// --- Speed 減速控制 ---
volatile int speedLostCount = 0; // speed 消失後的 frame 計數
bool isSlowMode = false; // 目前是否處於減速模式
// --- Stop 停車再開控制 ---
bool stopHandling = false; // 是否正在執行 stop 停車流程
unsigned long stopStartTime = 0; // stop 開始停車的時間戳
unsigned long stopCooldownEnd = 0; // stop 冷卻期結束時間戳
bool inStopCooldown = false; // 是否處於 stop 冷卻期
// --- 上一次的轉向紀錄(用於 line 丟失時維持方向) ---
int lastSteerError = 0; // 上一次的轉向誤差
// --- 按鈕控制馬達開關 ---
bool motorEnabled = false; // 馬達輸出開關(初始為關閉)
int lastButtonState = HIGH; // 上一次按鈕狀態
int currentStableState = HIGH; // 穩定後的按鈕狀態
unsigned long lastDebounceTime = 0; // 消跳計時器
const unsigned long debounceDelay = 50; // 消跳延遲 50ms
// --- 即時控制宣告 ---
void processCarControl(); // 事件驅動車輛控制函式宣告
// =====================================================================
// 馬達控制函式
// =====================================================================
// 前進:左右輪分別設定速度(差速控制)
void motorDrive(int leftSpeed, int rightSpeed) {
// 左輪
if (leftSpeed >= 0) {
analogWrite(M2A, constrain(leftSpeed, 0, MAX_SPEED));
analogWrite(M2B, 0);
} else {
analogWrite(M2A, 0);
analogWrite(M2B, constrain(-leftSpeed, 0, MAX_SPEED));
}
// 右輪
if (rightSpeed >= 0) {
analogWrite(M1A, constrain(rightSpeed, 0, MAX_SPEED));
analogWrite(M1B, 0);
} else {
analogWrite(M1A, 0);
analogWrite(M1B, constrain(-rightSpeed, 0, MAX_SPEED));
}
}
// 完全停止
void motorStop() {
analogWrite(M1A, 0);
analogWrite(M1B, 0);
analogWrite(M2A, 0);
analogWrite(M2B, 0);
}
// =====================================================================
// OLED 畫面更新函式
// =====================================================================
void updateOLED() {
if (!oledFound) return;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("AMB82 Smart Car"));
display.drawFastHLine(0, 10, SCREEN_WIDTH, SSD1306_WHITE);
display.setCursor(0, 14);
display.print(F("IP: "));
display.println(ip);
display.setCursor(0, 26);
display.print(F("RTSP Port: "));
display.println(rtsp_portnum);
display.setCursor(0, 38);
display.println(F("Model: YOLOv7-tiny"));
display.setCursor(0, 50);
display.print(F("Motor: "));
display.println(motorEnabled ? F("RUNNING [ON]") : F("STOPPED [OFF]"));
display.display();
}
// =====================================================================
// setup()
// =====================================================================
void setup() {
Serial.begin(115200);
// ----- OLED 初始化 -----
if (display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
oledFound = true;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("Smart Car Booting..."));
display.display();
Serial.println("[INFO] OLED initialized.");
} else {
oledFound = false;
Serial.println("[WARN] OLED not found, skipping.");
}
// 馬達腳位初始化
pinMode(M1A, OUTPUT);
pinMode(M1B, OUTPUT);
pinMode(M2A, OUTPUT);
pinMode(M2B, OUTPUT);
motorStop(); // 開機先停車
// 按鈕初始化(內部上拉,按下為 LOW)
pinMode(BUTTON_PIN, INPUT_PULLUP);
// ----- WiFi 連線 -----
Serial.println("\n[INFO] Connecting to WiFi...");
while (wifiStatus != WL_CONNECTED) {
Serial.print("Attempting to connect to WPA SSID: ");
Serial.println(ssid);
wifiStatus = WiFi.begin(ssid, pass);
delay(2000);
}
ip = WiFi.localIP();
Serial.print("[INFO] WiFi connected. IP: ");
Serial.println(ip);
// ----- 攝影機設定 -----
config.setBitrate(2 * 1024 * 1024); // RTSP 2Mbps
Camera.configVideoChannel(CHANNEL, config);
Camera.configVideoChannel(CHANNELNN, configNN);
Camera.videoInit();
// ----- RTSP 串流 -----
rtsp.configVideo(config);
rtsp.begin();
rtsp_portnum = rtsp.getPort();
// ----- NN 物件偵測設定 -----
// 使用 CUSTOMIZED_YOLOV7TINY 從 SD 卡載入自訂模型
// SD 卡路徑: NN_MDL/yolov7_tiny.nb
ObjDet.configVideo(configNN);
ObjDet.setResultCallback(ODPostProcess);
ObjDet.modelSelect(OBJECT_DETECTION, CUSTOMIZED_YOLOV7TINY, NA_MODEL, NA_MODEL);
// 設定全域一次性信心門檻與 NMS 門檻(單層過濾,NPU 底層直接捨棄 < NN_CONF_THRESHOLD 的候選框)
ObjDet.configThreshold(NN_CONF_THRESHOLD, NN_NMS_THRESHOLD);
ObjDet.begin();
// ----- StreamIO: Camera → RTSP -----
videoStreamer.registerInput(Camera.getStream(CHANNEL));
videoStreamer.registerOutput(rtsp);
if (videoStreamer.begin() != 0) {
Serial.println("[ERROR] StreamIO Camera->RTSP link failed");
}
Camera.channelBegin(CHANNEL);
// ----- StreamIO: Camera → NN -----
videoStreamerNN.registerInput(Camera.getStream(CHANNELNN));
videoStreamerNN.setStackSize();
videoStreamerNN.setTaskPriority();
videoStreamerNN.registerOutput(ObjDet);
if (videoStreamerNN.begin() != 0) {
Serial.println("[ERROR] StreamIO Camera->NN link failed");
}
Camera.channelBegin(CHANNELNN);
// 攝影機旋轉 180°(因機構倒裝)
// 必須在 Camera.channelBegin() 之後呼叫,ISP 才能正確套用硬體翻轉
delay(500);
if (CAMERA_ORIENTATION > 0) {
isp_set_orientation(CAMERA_ORIENTATION);
}
// ----- OSD 疊加層 -----
OSD.configVideo(CHANNEL, config);
OSD.begin();
// ----- 列印啟動資訊 -----
Serial.println("\n==============================================");
Serial.println(" AMB82 MINI Smart Car — Ready!");
Serial.println("==============================================");
Serial.print("RTSP URL: rtsp://");
Serial.print(ip);
Serial.print(":");
Serial.println(rtsp_portnum);
Serial.println("Model: CUSTOMIZED_YOLOV7TINY (SD Card)");
Serial.println("Labels: line, park, speed, stop");
Serial.println("==============================================\n");
// ----- OLED 顯示連線資訊與初始狀態 -----
updateOLED();
}
// =====================================================================
// NN 辨識結果回呼函式
// - 由 NN 推論執行緒呼叫(約 10fps)
// - 更新全域旗標,繪製 OSD
// =====================================================================
void ODPostProcess(std::vector<ObjectDetectionResult> results) {
uint16_t im_h = config.height();
uint16_t im_w = config.width();
// 本幀的暫存旗標
bool frameLineDetected = false;
float frameLineRightX = 0.0f;
float frameLineBottomY = 0.0f; // 追蹤最靠近車身的 line(yMax 最大)
bool frameParkDetected = false;
bool frameSpeedDetected = false;
bool frameStopDetected = false;
#if ENABLE_OSD_DRAW
OSD.createBitmap(CHANNEL);
#endif
if (ObjDet.getResultCount() > 0) {
for (int i = 0; i < ObjDet.getResultCount(); i++) {
int obj_type = results[i].type();
// 檢查 index 是否在有效範圍內,且 filter 為啟用
if (obj_type >= 0 && obj_type < 4 && itemList[obj_type].filter) {
ObjectDetectionResult item = results[i];
// 計算此目標的歸一化寬度 (0.0~1.0) 與是否為遠距離標誌
float boxWidth = item.xMax() - item.xMin();
bool isFar = false;
if ((obj_type == LABEL_PARK && boxWidth < MIN_SIGN_WIDTH_PARK) ||
(obj_type == LABEL_SPEED && boxWidth < MIN_SIGN_WIDTH_SPEED) ||
(obj_type == LABEL_STOP && boxWidth < MIN_SIGN_WIDTH_STOP)) {
isFar = true;
}
#if ENABLE_OSD_DRAW || ENABLE_DEBUG_PRINT
// 座標轉換(歸一化 0~1 → 像素座標)
int xmin = (int)(item.xMin() * im_w);
int xmax = (int)(item.xMax() * im_w);
int ymin = (int)(item.yMin() * im_h);
int ymax = (int)(item.yMax() * im_h);
#endif
#if ENABLE_OSD_DRAW
// 依類別設定不同的 OSD 顏色
uint32_t boxColor;
switch (obj_type) {
case LABEL_LINE: boxColor = OSD_COLOR_GREEN; break;
case LABEL_PARK: boxColor = OSD_COLOR_RED; break;
case LABEL_SPEED: boxColor = OSD_COLOR_YELLOW; break;
case LABEL_STOP: boxColor = OSD_COLOR_ORANGE; break;
default: boxColor = OSD_COLOR_WHITE; break;
}
// 繪製辨識框
OSD.drawRect(CHANNEL, xmin, ymin, xmax, ymax, 3, boxColor);
// 繪製標籤文字 + 信心分數(遠處標誌標記 [FAR])
char text_str[32];
if (obj_type != LABEL_LINE && isFar) {
snprintf(text_str, sizeof(text_str), "%s %d [FAR]", itemList[obj_type].objectName, item.score());
} else {
snprintf(text_str, sizeof(text_str), "%s %d", itemList[obj_type].objectName, item.score());
}
OSD.drawText(CHANNEL, xmin, ymin - OSD.getTextHeight(CHANNEL), text_str, OSD_COLOR_CYAN);
#endif
// --- 更新各類別偵測旗標(交通標誌需達最小寬度才觸發動作) ---
switch (obj_type) {
case LABEL_LINE:
frameLineDetected = true;
// 選取畫面最下方的 line(yMax 最大 = 最靠近車身)
// 用該 line 的右邊緣 xMax 作為循線參考
if (item.yMax() > frameLineBottomY) {
frameLineBottomY = item.yMax();
frameLineRightX = item.xMax();
}
break;
case LABEL_PARK:
if (!isFar) {
frameParkDetected = true;
}
break;
case LABEL_SPEED:
if (!isFar) {
frameSpeedDetected = true;
}
break;
case LABEL_STOP:
if (!isFar) {
frameStopDetected = true;
}
break;
}
#if ENABLE_DEBUG_PRINT
// Serial 除錯輸出
printf("[OD] %s (score:%d, w:%.2f%s) x:[%d-%d] y:[%d-%d]\r\n",
itemList[obj_type].objectName, item.score(), boxWidth,
isFar ? " [FAR]" : "", xmin, xmax, ymin, ymax);
#endif
}
}
}
#if ENABLE_OSD_DRAW
// 繪製畫面中心參考線(垂直白色虛線,方便觀察循線偏差)
int centerX = im_w / 2;
OSD.drawLine(CHANNEL, centerX, 0, centerX, im_h, 1, OSD_COLOR_WHITE);
OSD.update(CHANNEL);
#endif
// --- 更新全域控制變數 ---
lineDetected = frameLineDetected;
parkDetected = frameParkDetected;
speedDetected = frameSpeedDetected;
stopDetected = frameStopDetected;
if (frameLineDetected) {
lineRightEdgeX = frameLineRightX;
lineLostCount = 0;
} else {
lineLostCount++;
}
// Speed 消失計數
if (frameSpeedDetected) {
speedLostCount = 0;
} else {
if (isSlowMode) {
speedLostCount++;
}
}
// =========================================================
// 事件驅動:NN 辨識完成瞬間立即執行控制,零輪詢延遲!
// =========================================================
processCarControl();
}
// =====================================================================
// 車輛控制函式(由 ODPostProcess 辨識完成後立即呼叫,零等待延遲)
// =====================================================================
void processCarControl() {
if (!motorEnabled) {
motorStop();
return;
}
unsigned long now = millis();
// =========================================================
// 優先級 1:Park — 完全靜止
// =========================================================
if (parkDetected) {
motorStop();
Serial.println("[CTRL] PARK detected -> FULL STOP");
return;
}
// =========================================================
// 優先級 2:Stop 正在執行中(等待 2 秒計時)
// =========================================================
if (stopHandling) {
motorStop();
unsigned long elapsed = now - stopStartTime;
if (elapsed >= STOP_DURATION_MS) {
// 停車 2 秒結束,恢復行駛
stopHandling = false;
inStopCooldown = true;
stopCooldownEnd = now + STOP_COOLDOWN_MS;
Serial.println("[CTRL] STOP 2s done -> Resume driving (cooldown started)");
}
return;
}
// =========================================================
// 優先級 3:Stop 新觸發(非冷卻期內)
// =========================================================
if (stopDetected && !inStopCooldown) {
motorStop();
stopHandling = true;
stopStartTime = now;
Serial.println("[CTRL] STOP detected -> Begin 2s stop");
return;
}
// =========================================================
// 決定當前基礎速度(Speed 減速 or 正常)
// =========================================================
int baseSpeed = NORMAL_SPEED;
if (speedDetected) {
isSlowMode = true;
speedLostCount = 0;
baseSpeed = SLOW_SPEED;
Serial.println("[CTRL] SPEED detected -> Slow mode");
} else if (isSlowMode) {
if (speedLostCount >= SPEED_LOST_FRAMES) {
isSlowMode = false;
baseSpeed = NORMAL_SPEED;
Serial.println("[CTRL] SPEED gone -> Normal speed restored");
} else {
baseSpeed = SLOW_SPEED;
}
}
// =========================================================
// 循線與丟線搜尋控制(未偵測到 line 時無條件左轉尋線)
// =========================================================
if (lineDetected) {
// --- 正常循線:P 控制器差速導正 ---
float targetX = lineRightEdgeX + LINE_OFFSET;
float error = targetX - 0.5f;
// P 控制器差速轉向(配合實體小車動力回授)
// 當目標偏右 (error > 0) 時,左輪減速、右輪加速導正路線
int steer = (int)(KP * error);
int leftSpeed = baseSpeed + steer;
int rightSpeed = baseSpeed - steer;
// 限制速度範圍
leftSpeed = constrain(leftSpeed, MIN_SPEED, MAX_SPEED);
rightSpeed = constrain(rightSpeed, MIN_SPEED, MAX_SPEED);
// 左右差速驅動(M2 為左輪,M1 為右輪)
motorDrive(leftSpeed, rightSpeed);
#if ENABLE_DEBUG_PRINT
printf("[CTRL] Line follow: target=%.2f err=%.2f steer=%d L=%d R=%d base=%d\r\n",
targetX, error, steer, leftSpeed, rightSpeed, baseSpeed);
#endif
} else if (LINE_LOST_MAX == 0 || lineLostCount < LINE_LOST_MAX) {
// --- 未偵測到 line:無條件左轉尋線(線段在左側鏡頭未拍到) ---
int leftSpeed = SEARCH_LEFT_SPEED_L;
int rightSpeed = SEARCH_LEFT_SPEED_R;
// 若處於減速模式,等比例降速以策安全
if (isSlowMode) {
leftSpeed = (leftSpeed * SLOW_SPEED) / NORMAL_SPEED;
rightSpeed = (rightSpeed * SLOW_SPEED) / NORMAL_SPEED;
}
leftSpeed = constrain(leftSpeed, MIN_SPEED, MAX_SPEED);
rightSpeed = constrain(rightSpeed, MIN_SPEED, MAX_SPEED);
motorDrive(leftSpeed, rightSpeed);
#if ENABLE_DEBUG_PRINT
printf("[CTRL] Line lost (%d/%d): Unconditional LEFT turn L=%d R=%d\r\n",
lineLostCount, LINE_LOST_MAX, leftSpeed, rightSpeed);
#endif
} else {
// 連續超過 LINE_LOST_MAX 幀都找不到 line → 安全停車
motorStop();
Serial.println("[CTRL] Line LOST too long -> STOP for safety");
}
}
// =====================================================================
// loop() — 背景按鈕輪詢與狀態更新
// =====================================================================
void loop() {
unsigned long now = millis();
// === 檢查 Stop 冷卻期是否結束 ===
if (inStopCooldown && now >= stopCooldownEnd) {
inStopCooldown = false;
Serial.println("[CTRL] Stop cooldown ended.");
}
// =========================================================
// 按鈕讀取 + 防彈跳 + 切換馬達開關 (Button A - Pin 18)
// =========================================================
int reading = digitalRead(BUTTON_PIN);
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != currentStableState) {
currentStableState = reading;
if (currentStableState == LOW) { // 按下的瞬間(下降緣觸發切換)
motorEnabled = !motorEnabled;
if (!motorEnabled) {
motorStop(); // 關閉時立即停車
}
updateOLED(); // 同步更新 OLED 狀態
Serial.print("[BTN] Motor ");
Serial.println(motorEnabled ? "ENABLED (RUNNING)" : "DISABLED (STOPPED)");
}
}
}
lastButtonState = reading;
// 若馬達開關關閉,確保馬達停止
if (!motorEnabled) {
motorStop();
}
delay(10); // 快速輪詢按鈕並釋放 CPU 給 FreeRTOS / NN
}
5-3. 實際執行
透過Arduino IDE 將程式下載至AMB82 MINI。

打開電源後除了連上網路外,為了保護車子突然暴衝,一開始會關閉馬達輸出。

只要按下「按鈕 A」即可讓車子開始自動駕駛。

將登月小車放在跑道上,並在跑道上佈置標誌。

記得打開VLC 實時觀看辨識狀況。

5-4. 完整可調參數指南 (Tunable Parameters)
1. 速度控制參數
| 參數名稱 | 預設值 | 建議範圍 | 功能詳細說明與實車影響 |
|---|---|---|---|
NORMAL_SPEED | 100 | 80 ~ 140 | 小車直線正常巡航速度 (PWM 0~255)。 • 調大:直線加速快、過彎慣性大,對轉向靈敏度要求高。 • 調小:行駛穩定,適合小半徑賽道。 |
SLOW_SPEED | 80 | 60 ~ 90 | 限速模式行駛速度 (偵測到 speed 標誌時)。• 當偵測到限速標誌時的基準巡航速度。 • 需低於 NORMAL_SPEED,但不得低於馬達死區門檻(低於 50 可能卡死)。 |
MIN_SPEED | 0 | 0 | 馬達輸出下限保護值,防止差速計算出負值造成非預期的劇烈倒退。 |
MAX_SPEED | 255 | 200 ~ 255 | 馬達輸出上限保護值,防止 PWM 溢位。 |
2. 循線幾何與 P 控制器參數
| 參數名稱 | 預設值 | 建議範圍 | 功能詳細說明與實車影響 |
|---|---|---|---|
LINE_OFFSET | 0.25f | 0.10f ~ 0.35f | 黃色中線右側到小車前進中軸的偏移量(歸一化 0~1)。 • 調大(如 0.28f):車身往右靠,與左側黃色中線拉開更大安全距離,防止左側輪胎壓線。 • 調小(如 0.15f):車身貼近左側黃色中線行駛。 • 若賽道車道較寬,建議設定在 0.22f ~ 0.28f 之間。 |
KP | 90 | 60 ~ 130 | P 控制器比例增益 (Proportional Gain)。 • 控制轉向的修正力矩大小( steer = KP × error)。• 調大(如 110):轉向更猛烈,能迅速切入大角度急彎,但容易出現 S 型蛇行晃動。 • 調小(如 75):走線平穩流暢,但若過大彎可能轉向不足衝出賽道。 |
LINE_LOST_MAX | 80 | 40 ~ 120 | 連續丟失黃色中線的最大畫格數。 • 以 20 FPS 計算,80 幀約為 4 秒搜尋時間。 • 超過此幀數依然未見黃色中線時觸發安全緊急煞車。 • 若設為 0:表示小車將持續旋轉搜尋,永不超時停車。 |
3. 丟線尋線(無條件左轉)參數
| 參數名稱 | 預設值 | 建議範圍 | 功能詳細說明與實車影響 |
|---|---|---|---|
SEARCH_LEFT_SPEED_L | 0 | 0 ~ 40 | 丟線尋線時左輪速度。 • 設為 0:左輪固定停止,以左輪為圓心進行單邊快速左轉。• 設為負值(如 -30):左右輪反向原地旋轉。• 設為小正值(如 30):邊向前切邊向左弧線轉彎。 |
SEARCH_LEFT_SPEED_R | 70 | 60 ~ 100 | 丟線尋線時右輪速度。 • 右輪動力提供向左轉向的外側推力。 • 數值越大左轉推力越強、找線半徑越緊湊。 |
4. NPU 推論與信心指數門檻
| 參數名稱 | 預設值 | 建議範圍 | 功能詳細說明與實車影響 |
|---|---|---|---|
NN_CONF_THRESHOLD | 0.60f | 0.50f ~ 0.70f | NPU 單層全域信心指數門檻。 • 0.60f 代表信心度需 ≥ 60%。• 低於此門檻的候選框直接在 NPU 底層拋棄,不進入微控制器記憶體。 • 賽場光線充足時建議設為 0.60f ~ 0.65f,極佳消除背景雜訊。 |
NN_NMS_THRESHOLD | 0.30f | 0.20f ~ 0.40f | NMS (非極大值抑制) 重疊度門檻。 • 用於消除同一個物體周圍重複出現的多個重疊候選框。 |
5. 交通標誌觸發尺寸門檻 (測距過濾)
💡 原理:影像中標誌框的寬度比例(0.0 ~ 1.0)直接反映車輛與標誌的物理距離。未達此寬度表示標誌太遠,畫面標示
[FAR]且不動作。
| 參數名稱 | 預設值 | 建議範圍 | 觸發動作與調校重點 |
|---|---|---|---|
MIN_SIGN_WIDTH_PARK | 0.08f | 0.06f ~ 0.12f | park 停車標誌最小寬度(預設 8% 畫面寬度)。• 達標時觸發:完全煞停直到標誌離開。 • 調大可讓小車行駛到標誌正前方更近處才煞停。 |
MIN_SIGN_WIDTH_SPEED | 0.05f | 0.03f ~ 0.08f | speed 限速標誌最小寬度(預設 5% 畫面寬度)。• 達標時觸發:切換至減速模式 ( SLOW_SPEED)。• 限速標誌通常立於路旁且需提早反應,設定較低以利提早降速。 |
MIN_SIGN_WIDTH_STOP | 0.10f | 0.08f ~ 0.14f | stop 停車再開標誌最小寬度(預設 10% 畫面寬度)。• 達標時觸發:煞停 2 秒後再自動啟動。 |
6. 交通標誌時序參數
| 參數名稱 | 預設值 | 建議範圍 | 功能詳細說明 |
|---|---|---|---|
STOP_DURATION_MS | 2000 | 1000 ~ 3000 | stop 煞停維持時間(毫秒)。• 倒數計時期間馬達強制完全靜止輸出。 |
STOP_COOLDOWN_MS | 5000 | 3000 ~ 7000 | stop 停車後冷卻防護時間(毫秒)。• 停車 2 秒結束起步後,小車在冷卻期內不再重複觸發同一個 stop 標誌,直到駛離標誌區。 |
SPEED_LOST_FRAMES | 100 | 40 ~ 150 | speed 標誌離開視野後的減速維持幀數。• 標誌出鏡後維持約 3~5 秒緩行(100 幀),之後平穩恢復為 NORMAL_SPEED。 |
7. 效能優化與除錯開關
| 參數名稱 | 預設值 | 可選值 | 功用與效益說明 |
|---|---|---|---|
ENABLE_DEBUG_PRINT | 0 | 0 / 1 | 每畫格串口除錯列印開關。 • 0 (預設關閉):消除高頻 printf,每幀節省 5~15ms UART 阻塞等待,實車競賽推薦關閉。• 1 (開啟除錯):每幀輸出座標、BBox 寬度、偏差量與左右馬達速度。 |
ENABLE_OSD_DRAW | 1 | 0 / 1 | RTSP 影像 OSD 框繪製開關。 • 1 (預設開啟):在視訊畫面上繪製色彩邊框、標籤與 [FAR] 狀態。• 0 (脫機極速):完全關閉 OSD 繪圖與顯存拷貝,釋放 CPU 給即時差速。 |
結論
以上就是所有的教學內容了,從基礎操作到蒐集資料,還經歷了最重要的模型訓練與轉換,看到登月小車跑起來是不是很感動呢!如果不順利,可以依照「可調參數指南」調整,希望大家學習愉快,下個專案再見!
系列文章
- (一)基礎認識
- (二)資料蒐集
- (三)資料標註
- (四)訓練模型與轉換
- (五)自動駕駛(本篇)