2026年8月23日 星期日

[水井村USR] 從「Web一開、按鈕就停」到雙介面穩定運作:HUB 8735 Ultra+JQ6500除錯全紀錄

水井三寶故事機|V1.0-06

從「Web一開、按鈕就停」到雙介面穩定運作:HUB 8735 Ultra+JQ6500除錯全紀錄

這次實作最寶貴的成果,不只是故事能播放,而是我們透過序列監控、狀態機、電源A/B測試與函式庫原始碼,一步步找出「供電重置、AP阻塞、Web Server阻塞、BUSY判斷與停止提示」之間的關係,最後讓手機Web與實體按鈕可以同時穩定操作。

一、最終完成的系統

1獨立APHUB自行建立Shuijing-Story,不需要外部路由器。
2雙操作介面手機Web為主要介面,實體按鈕是立即可用的離線備援。
3非阻塞狀態機播放、切歌、停止、Reset與提示音由統一狀態機管理。
最終測試結果:沒有手機連線時,三個故事按鈕與停止鍵可正常操作;手機連入後,Web與實體按鈕仍能交替使用,不再因等待Web客戶端而停住。

二、硬體與曲目配置

元件工作本次設計角色
HUB 8735 UltraAP、Web、GPIO與狀態機故事機主控制器
JQ6500MP3解碼、播放、暫停與音量專用音訊播放器
手機瀏覽器控制主要使用介面
實體按鈕故事選擇、停止與音量沒有手機時仍可操作
穩定5V電源供應Wi-Fi與音訊瞬間電流展場穩定運作的基礎

MP3曲目

檔案內容啟動方式
001.mp3白馬故事Web或GPIO10
002.mp3烏龜故事Web或GPIO11
003.mp3姻緣花故事Web或GPIO12
004.mp3水井三寶總故事Web
005.mp3創作者資訊Web
006.mp3開機提示自動
007.mp3準備完成006結束後自動
008.mp3錯誤提示錯誤狀態
009.mp3停止提示停止故事後播放

三、最終接線圖

GPIO8
Serial2 TX
JQ6500 RX
 
播放命令
GPIO15
Serial2 RX
JQ6500 TX
 
狀態回傳
GPIO4
JQ6500 BUSY
 
播放狀態
HUB GND
JQ6500 GND
共同參考地
GPIO功能按鈕接法
GPIO10白馬GPIO10→按鈕→GND
GPIO11烏龜GPIO11→按鈕→GND
GPIO12姻緣花GPIO12→按鈕→GND
GPIO20停止GPIO20→按鈕→GND
GPIO7音量+GPIO7→按鈕→GND
GPIO6音量-GPIO6→按鈕→GND
按鈕電位:程式使用INPUT_PULLUP,未按為1、按下為0。GPIO13裸接仍為0,因此本專案最後改用GPIO20作為停止鍵。

四、除錯歷程:每個症狀都在說話

階段1|HUB AOUT可以播放,但Web難以中途控制
原本使用HUB的阻塞式MP3播放。故事播放期間,主程式無法持續處理Web、停止與切歌,因此改由JQ6500專責播放,HUB只負責控制。
階段2|AP可以連,但開機提示不斷重播
一開始以為006被設定成循環,後來從序列監控發現程式標題與Boot Loader不斷重現,真正原因是整塊HUB反覆重置。
階段3|改用穩定電源後重置消失
使用PC USB孔供電時,Wi-Fi AP與音訊啟動的瞬間負載可能造成壓降;改用合格市電轉5V電源後,系統恢復穩定。
階段4|Web正常,停止與切歌卻會排隊
JQ6500的Pause、Reset與指定曲目命令需要正確時序,因此建立PAUSE_WAIT、RESET_WAIT、STORY_PLAYING與STOP_PROMPT狀態。
階段5|009停止提示讓系統停在STOP_PROMPT
009太短或BUSY沒有形成完整變化時,狀態機無法回到待機,因此加入BUSY啟動逾時與播放上限Watchdog。
階段6|Web輪詢造成FreeRTOS Queue斷言
每秒短連線與大量序列輸出增加底層資源壓力,最後將狀態更新改為每3秒、防止重疊請求,並在關閉Socket後保留釋放時間。
階段7|沒有手機時,系統停在「啟動中」
這是最關鍵的發現:AmebaPro2的WiFiServer預設採用BLOCKING_MODE。沒有手機時,server.available()會一直等待,整個主迴圈因此停止。

五、經驗一:重複語音不一定是MP3循環

曾經看到006開機語音反覆播放,直覺會先懷疑JQ6500的循環模式。然而序列監控不斷重新出現:

================================================
HUB 8735 Ultra + JQ6500 Web Story Player
================================================

甚至再次出現:

== Rtl8735b IoT Platform ==
[Normal mode]
BootFromNORFlash

這代表不是單一曲目循環,而是開發板重新開機。每次進入setup()都會再次播放006,所以聽起來才像無限循環。

除錯原則:先判斷「播放器重播」還是「整板重開」,兩者需要完全不同的修正方向。

六、經驗二:穩定供電是Wi-Fi音訊系統的一部分

單獨測試HUB、JQ6500或Web可能都正常,但整合後會同時產生:

  • Wi-Fi射頻啟動的瞬間電流。
  • JQ6500解碼與儲存讀取負載。
  • 功率放大器及喇叭的動態電流。
  • USB線材與接點造成的壓降。

實測結果是:PC USB供電容易發生重置,使用穩定的市電轉5V電源供應器後,AP、Web及音訊都能長時間正常運作。

安全提醒:市電必須先經合格且規格正確的AC轉DC 5V電源供應器,絕不可將110V直接接入HUB、JQ6500或麵包板。多組電源只共地,不可任意把兩個5V輸出直接並聯。

七、經驗三:AP重試也必須非阻塞

舊設計一次執行6次AP啟動,每次失敗等待10秒:

for (int attempt = 1; attempt <= 6; attempt++) {
    WiFi.apbegin(...);
    delay(10000);
}

最壞情況會有60秒無法掃描按鈕。後來改成一次只嘗試一次,失敗立即返回,15秒後再試:

bool tryStartAPOnce()
{
    int status = WiFi.apbegin(AP_SSID, AP_PASS,
                              AP_CHANNEL, AP_HIDDEN);
    if (status != WL_CONNECTED) {
        wifiReady = false;
        return false;
    }
    wifiReady = true;
    return true;
}

AP失敗不再改變播放器狀態,三個故事鍵、停止鍵、音量與BUSY仍繼續運作。

八、最關鍵發現:WiFiServer預設會阻塞

即使AP重試已經非阻塞,沒有手機連線時,按鈕仍停在開機後第一筆狀態。最後直接查閱AmebaPro2的WiFiServer實作,發現預設值是:

tBlockingMode _is_blocked = BLOCKING_MODE;

因此執行:

WiFiClient client = server.available();

在沒有手機時可能一直等待客戶端,主迴圈無法再執行按鈕、BUSY與播放器狀態機。真正的修正只有一行,但必須放在server.begin()之前:

server.setNonBlockingMode();
server.begin();
修正後:沒有手機時,server.available()立即返回;手機連入後,Web仍可正常回應。這一行讓「Web+實體按鈕」真正可以共存。

九、主迴圈的最終優先順序

展場故事機的核心不是網路,而是使用者按下按鈕後必須立即有反應。因此最終順序設計為:

void loop()
{
    handleButtons();              // 最高:實體按鈕
    updateBusy();                 // 第二:音訊硬體狀態
    updatePlayerStateMachine();   // 第三:播放狀態機

    if (wifiReady) {
        handleWeb();              // 第四:Web
    } else if (到達15秒) {
        tryStartAPOnce();         // 最後:AP恢復
    }

    updateLed();
    delay(3);
}
實體按鈕BUSY播放器狀態機WebAP重試

十、JQ6500非阻塞播放狀態機

狀態意義可接受操作
BOOT_PROMPT播放006可被故事或停止命令中斷
READY_PROMPT播放007可切換故事
IDLE等待操作所有故事與音量
STORY_PLAYING播放001~005停止或立即切歌
PAUSE_WAIT送出Pause後等待150ms更新最終目標,不重複排隊
RESET_WAITReset後等待700ms保留最後命令
STOP_PROMPT播放009可取消或改播故事
ERROR系統錯誤顯示診斷

中途切歌流程:

故事播放中
→ 0x0E Pause立即中斷
→ PAUSE_WAIT 150ms
→ 0x0C Reset清除Pause與待播狀態
→ RESET_WAIT 700ms
→ 恢復音量與ONE_STOP
→ 播放最後選擇的故事

十一、009停止提示不再鎖住系統

009可能很短,BUSY變化可能未被掃描到。因此加入兩層Watchdog:

#define STOP_BUSY_START_TIMEOUT_MS 600
#define STOP_PROMPT_MAX_MS        8000
  • 009送出600ms後沒有BUSY啟動:自動回到待機。
  • BUSY長時間沒有結束:8秒後強制釋放狀態。
  • 009播放時選故事:立即中斷009並切歌。
  • 009播放時再按停止:取消009,不重複播放。

十二、Web輪詢與FreeRTOS Queue經驗

早期Web每秒建立一次狀態連線,並把每個請求與六個GPIO值大量印到序列監控,曾觸發:

line 1473
freertos_v202012.00/Source/queue.c

最後採取:

  • 狀態輪詢改為每3秒。
  • 前一個請求未完成時,不送出重疊請求。
  • 不列印每一次/api/status
  • client.stop()後保留短暫資源釋放時間。
  • 按鈕原始電位診斷降低輸出頻率。

這提醒我們:嵌入式Web不是一般伺服器,Socket、記憶體、RTOS Queue與序列輸出都必須節制。

十三、如何用序列監控快速判讀

序列現象代表意義處理方向
程式標題不斷重現整板重置檢查電源與Reset
Download Image over UART1進入UART燒錄模式檢查功能鍵與BOOT_MODE
只有一筆BUTTON RAW後停止主迴圈被阻塞檢查WiFiServer模式
G10由1變0白馬按鈕硬體正常追查狀態機
BUSY離開idle再回idle曲目完整開始與結束狀態可回IDLE
STOP_PROMPT沒有BUSY009未啟動或太短交由Watchdog釋放
queue.c斷言RTOS資源或非執行緒安全問題降低輪詢與輸出頻率

十四、最終驗收結果

V1.0-06驗收通過:使用穩定5V供電,HUB可建立AP;沒有手機時按鈕持續更新,手機連入後Web與按鈕都能正常;故事播放、播放中切歌、停止提示、BUSY狀態及AP重試均不再長時間鎖住主迴圈。
  • AP名稱:Shuijing-Story
  • Web網址:http://192.168.1.1/
  • Web狀態更新:每3秒一次
  • 按鈕:GPIO10、11、12、20
  • 播放器:JQ6500 UART+BUSY
  • 核心原則:按鈕最高優先、網路可以失敗、故事機仍能工作

十五、這次最重要的學習

真正可靠的互動裝置,不能只在「所有條件都正常」時工作。網路可能沒有客戶端、AP可能啟動失敗、提示音可能太短、BUSY可能漏掉、USB供電可能壓降。良好的系統設計必須讓每個外部條件都可以失敗,但不能拖垮故事播放與實體按鈕。

一句話總結:讓Web與實體按鈕共存的關鍵,不只是寫出Web頁面,而是確保任何網路等待都不能阻塞主迴圈。
Blogger貼文提醒:請切換至Blogger「HTML檢視」後貼入本檔全部內容。本檔沒有<html><head><body>外層標籤;表格與程式碼已設定橫向捲動,不會把右側資訊欄擠出畫面。

[水井村USR] HUB 8735 Ultra+JQ6500實測:AP可以連線,為什麼開機語音卻不斷重播?

水井三寶故事機|V1.0-05

HUB 8735 Ultra+JQ6500實測:AP可以連線,為什麼開機語音卻不斷重播?

本次完成HUB 8735 Ultra、JQ6500與手機Web控制整合測試。系統功能原本都能運作,但使用電腦USB孔供電時,常在Wi-Fi AP啟動階段反覆重置,造成開機提示語音一次又一次播放。改用合格的市電轉5V電源供應器後,整套系統恢復穩定。

一、本次測試目標

1建立獨立APHUB自行建立Wi-Fi,手機不需要外部路由器即可連線。
2Web控制故事透過手機選擇五段內容、停止播放及調整音量。
3驗證供電穩定比較PC USB供電與市電轉5V供電的運作差異。

二、系統組成與功能

元件主要工作說明
HUB 8735 UltraAP、Web伺服器、按鈕與狀態控制建立Shuijing-Story無線網路,接收手機操作命令。
JQ6500MP3解碼與播放透過UART接收播放、暫停、音量及指定曲目命令。
手機主要操作介面連接HUB的AP,以瀏覽器開啟Web控制頁。
實體按鈕離線備援即使沒有手機,也能選擇三個角色故事與停止、調整音量。
5V電源供應器穩定供電供應Wi-Fi射頻啟動與音訊播放所需的瞬間電流。

三、AP與手機Web操作

HUB 8735 Ultra不連接現場路由器,而是自己建立AP:

Wi-Fi名稱:Shuijing-Story
Wi-Fi密碼:shuijing8735
Web網址:以序列監控顯示為準,測試常見為 http://192.168.1.1/
手機連接Shuijing-Story輸入http網址選擇故事HUB送UART命令JQ6500播放

測試時,手機成功取得以下IP,代表AP與DHCP服務已正常:

DHCP assign ip = 192.168.1.100

四、MP3曲目配置

檔名/索引內容啟動方式
001.mp3白馬故事Web或GPIO10
002.mp3烏龜故事Web或GPIO11
003.mp3姻緣花故事Web或GPIO12
004.mp3水井三寶總故事Web
005.mp3創作者資訊Web
006.mp3開機提示系統啟動後自動播放
007.mp3準備完成006播放完成後自動播放
008.mp3錯誤提示系統錯誤時播放
009.mp3停止提示停止故事後播放

五、UART與按鈕接線

HUB GPIO8
Serial2 TX
JQ6500 RX
 
UART命令
HUB GPIO15
Serial2 RX
JQ6500 TX
 
模組回傳
HUB GPIO4
JQ6500 BUSY
 
播放狀態
HUB GND
JQ6500 GND
共地
GPIO實體功能接法
GPIO10白馬故事按鈕另一端接GND
GPIO11烏龜故事按鈕另一端接GND
GPIO12姻緣花故事按鈕另一端接GND
GPIO13停止播放按鈕另一端接GND
GPIO7音量增加按鈕另一端接GND
GPIO6音量降低按鈕另一端接GND

六、異常現象:開機語音不斷重播

測試過程中,序列監控一再出現相同的程式標題:

================================================
HUB 8735 Ultra + JQ6500 Web Story Player
Version: V1.0-05 AP Mode
================================================
[VOLUME] 20
[JQ6500] Play mode: ONE_STOP
[JQ6500] Select and play track: 6
[AP] Starting: Shuijing-Story
[AP] Attempt 1 / 6

這並不是006.mp3設定成循環播放,而是HUB反覆重新執行setup()。每重開一次,程式就重新播放一次006,因此聽起來像開機語音無限循環。

判讀重點:如果序列監控不斷重新出現程式標題、Boot Loader或Rtl8735b IoT Platform,代表整塊開發板正在重置,不是單純的MP3循環問題。

七、原因分析:PC USB供電的瞬間電流不足

HUB原本由電腦USB孔供電。單獨執行一般程式時可能沒有問題,但本系統同時包含:

  • Wi-Fi AP射頻初始化與封包傳輸。
  • JQ6500讀取及解碼MP3。
  • 喇叭或功率放大器的瞬間負載。
  • LED、按鈕與其他周邊。

AP射頻啟動與音訊播放都可能產生瞬間電流需求。部分電腦USB埠、USB Hub、過長或品質不佳的USB線,可能造成5V電壓短暫下降,進而讓RTL8735B重置。

這不是指所有PC USB埠都一定不足。實際結果會受到USB埠供電能力、線材壓降、JQ6500與功放負載、接地及電源濾波影響。本次是依現場A/B測試判定:PC USB供電會重置,改用穩定的市電轉5V供電後即正常。

八、改善後的供電接線圖

AC市電
合格AC轉DC
5V電源供應器
HUB 8735 Ultra
USB供電
穩定5V
JQ6500/功放
喇叭
HUB GND
JQ6500 GND
功放GND
安全注意:此處的「市電供電」是使用有安規認證、輸出規格正確的AC轉DC 5V電源供應器,絕對不可將110V市電直接接到HUB、JQ6500或麵包板。若HUB與音訊模組使用不同5V供應器,只連接訊號與GND共地,避免把兩個5V輸出直接並聯。

九、程式面的穩定化處理

1. 先建立AP,再啟動音訊

bool apStarted = startAccessPoint();

if (apStarted) {
    delay(500);
    jqSetVolume(currentVolume);
    jqSetOneTrackStopMode();
    jqPlayTrack(TRACK_BOOT);
    currentStory = "系統啟動中";
}

這樣可錯開Wi-Fi射頻初始化與JQ6500開始播放的瞬間負載。

2. 明確設定單曲播放一次

void jqSetOneTrackStopMode()
{
    uint8_t mode[1] = {0x04};
    jqSendCommand(0x11, mode, 1);
}

3. 006完成後才播放007

if (finishedTrack == TRACK_BOOT &&
    !readyPromptPlayed && wifiReady) {
    readyPromptPlayed = true;
    jqPlayTrack(TRACK_READY);
    currentStory = "系統準備完成";
}

十、PC USB與穩定5V供電比較

測試項目PC USB孔供電合格市電轉5V供電
HUB開機可開機正常
AP射頻啟動可能反覆重置穩定
006開機提示因HUB重開而重複播放只播放一次
手機連接AP可能中途消失穩定連線
Web控制重置時無法操作正常操作
整體結論適合燒錄與短時間測試,但需視USB埠能力較適合故事機長時間展示與實際使用

十一、最終測試結果

測試通過:改用穩定的市電轉5V電源供應器後,HUB 8735 Ultra不再反覆重置,006開機提示、AP、Web頁面、故事選擇及JQ6500播放均能穩定運作。
  • HUB可穩定建立Shuijing-Story AP。
  • 手機可以取得IP並開啟Web控制頁。
  • 006開機提示不再因系統重置而反覆播放。
  • 三個角色故事、總故事及創作者資訊可由Web選擇。
  • 實體按鈕仍可作為離線備援。

十二、本次實作的重要經驗

嵌入式系統「程式可以編譯、模組可以單獨運作」不代表整合後一定穩定。Wi-Fi、音訊與功率放大器同時啟動時,供電品質往往比程式更容易成為問題。看到開機語音重複時,也不能只從MP3循環模式思考;序列監控是否重複出現Boot Loader與程式標題,是判斷整板重置的重要證據。

這次測試證明,透過「序列紀錄判讀、最小硬體測試、供電A/B比較、啟動順序調整」,可以把看似軟體錯誤的問題,追查到真正的電源穩定性原因。

Blogger貼文提醒:請在Blogger的「HTML檢視」貼入本檔全部內容。本檔不包含<html><head><body>標籤,程式區塊與表格已設定橫向捲動,不會把部落格右側資訊欄擠出畫面。

[水井村USR] HUB 8735 Ultra實作:用三個實體按鈕選擇三個水井三寶故事

水井三寶故事機|V1.0-04

HUB 8735 Ultra實作:用三個實體按鈕選擇三個水井三寶故事

V1.0-03已完成microSD卡與三個MP3檔案的依序播放測試;本次V1.0-04加入三個實體按鈕,讓使用者可直接選擇白馬、烏龜或姻緣花故事,故事機正式進入互動操作階段。

一、測試目標與結果

本次使用HUB 8735 Ultra讀取microSD卡中的三個MP3檔案,並以GPIO10、GPIO11、GPIO12分別連接三個按鈕。按下不同按鈕後,播放對應的故事。

測試結果:功能正常。三個按鈕都能正確播放各自指定的MP3故事。

二、按鈕與故事配置

GPIO10
白馬按鈕

001.mp3
GPIO11
烏龜按鈕

002.mp3
GPIO12
姻緣花按鈕

003.mp3
GPIO腳位實體按鈕MP3檔案故事內容
GPIO10白馬按鈕001.mp3白馬故事
GPIO11烏龜按鈕002.mp3烏龜故事
GPIO12姻緣花按鈕003.mp3姻緣花故事

三、準備材料

  • HUB 8735 Ultra開發板
  • microSD卡一張
  • 瞬時按鈕三個
  • XPT8871單聲道功率放大板
  • 喇叭一顆
  • 5V電源、麵包板與杜邦線

四、按鈕接線圖

程式使用INPUT_PULLUP,所以每個按鈕的一端接GPIO,另一端接GND;未按下時讀到HIGH,按下時讀到LOW,不需要另外加外部上拉電阻。

GPIO10
白馬按鈕
GND
GPIO11
烏龜按鈕
GND
GPIO12
姻緣花按鈕
GND
注意:四腳按鈕同一側的兩隻腳通常已互相導通,請使用按鈕兩側的腳位接線。若接到同一側,GPIO可能一直維持相同狀態。

五、音訊接線圖

HUB AOUT
XPT8871 IN+
 
音訊訊號
HUB GND
XPT8871 IN−
 
共地
5V
XPT8871 5V+
 
放大板電源
XPT8871 OUT+/OUT−
喇叭 +/−
 
橋接輸出
重要:XPT8871的OUT−是橋接功率輸出,不可以接GND。喇叭必須接在OUT+與OUT−之間。若有雜聲,應縮短AOUT音訊線、讓訊號線遠離電源線與喇叭輸出線,並確認HUB與放大板共地。

六、microSD卡檔案配置

將三個MP3檔直接放在microSD卡根目錄,檔名必須完全一致:

microSD 根目錄
├── 001.mp3  (白馬)
├── 002.mp3  (烏龜)
└── 003.mp3  (姻緣花)

七、完整Arduino程式|V1.0-04

/*
 * HUB 8735 Ultra 三按鈕故事播放器
 * Version: V1.0-04
 * GPIO10 → 白馬 → 001.mp3
 * GPIO11 → 烏龜 → 002.mp3
 * GPIO12 → 姻緣花 → 003.mp3
 */

#include "AmebaFatFS.h"
#include "AmebaFatFSFile.h"

#define MP3_VOLUME        0x90
#define STORY_COUNT       3
#define DEBOUNCE_MS       50

#define BUTTON_HORSE_PIN   10
#define BUTTON_TURTLE_PIN  11
#define BUTTON_FLOWER_PIN  12

AmebaFatFS fs;

const int buttonPins[STORY_COUNT] = {
    BUTTON_HORSE_PIN,
    BUTTON_TURTLE_PIN,
    BUTTON_FLOWER_PIN
};

const char *storyFiles[STORY_COUNT] = {
    "001.mp3",
    "002.mp3",
    "003.mp3"
};

const char *storyNames[STORY_COUNT] = {
    "White Horse",
    "Turtle",
    "Marriage Flower"
};

String getFullPath(const char *filename)
{
    return String(fs.getRootPath()) + String(filename);
}

bool checkMP3File(const char *filename)
{
    String fullPath = getFullPath(filename);
    File testFile = fs.open(fullPath);

    Serial.print("[CHECK] ");
    Serial.print(fullPath);
    Serial.print(" ... ");

    if (!testFile.isOpen()) {
        Serial.println("NOT FOUND");
        return false;
    }

    Serial.print("OK, ");
    Serial.print(testFile.size());
    Serial.println(" bytes");
    testFile.close();
    return true;
}

bool checkAllStoryFiles()
{
    bool allReady = true;

    for (int i = 0; i < STORY_COUNT; i++) {
        if (!checkMP3File(storyFiles[i])) {
            allReady = false;
        }
    }
    return allReady;
}

bool playStory(int storyIndex)
{
    if (storyIndex < 0 || storyIndex >= STORY_COUNT) {
        return false;
    }

    String fullPath = getFullPath(storyFiles[storyIndex]);
    File mp3File = fs.open(fullPath);

    Serial.println();
    Serial.println("========================================");
    Serial.print("[BUTTON] GPIO: ");
    Serial.println(buttonPins[storyIndex]);
    Serial.print("[PLAY] Story: ");
    Serial.println(storyNames[storyIndex]);
    Serial.print("[PLAY] File: ");
    Serial.println(fullPath);

    if (!mp3File.isOpen()) {
        Serial.println("[ERROR] Unable to open MP3 file.");
        return false;
    }

    mp3File.setMp3DigitalVol(MP3_VOLUME);
    Serial.println("[AUDIO] Playback started.");

    // playMp3()會阻塞,直到目前故事播放完成
    mp3File.playMp3();

    Serial.println("[AUDIO] Playback completed.");
    mp3File.close();
    Serial.println("========================================");
    return true;
}

void waitButtonRelease(int pin)
{
    while (digitalRead(pin) == LOW) {
        delay(10);
    }
    delay(DEBOUNCE_MS);
}

void setup()
{
    Serial.begin(115200);
    delay(2000);

    Serial.println();
    Serial.println("========================================");
    Serial.println("HUB 8735 Ultra Story Player V1.0-04");
    Serial.println("========================================");

    for (int i = 0; i < STORY_COUNT; i++) {
        pinMode(buttonPins[i], INPUT_PULLUP);
    }

    Serial.println("[SD] Initializing microSD card...");
    fs.begin();
    Serial.print("[SD] Root path: ");
    Serial.println(fs.getRootPath());

    if (!checkAllStoryFiles()) {
        Serial.println("[FAIL] Check microSD card and filenames.");
        while (true) {
            delay(1000);
        }
    }

    Serial.println("[READY] Press a story button.");
}

void loop()
{
    for (int i = 0; i < STORY_COUNT; i++) {
        if (digitalRead(buttonPins[i]) == LOW) {
            delay(DEBOUNCE_MS);

            if (digitalRead(buttonPins[i]) == LOW) {
                playStory(i);
                waitButtonRelease(buttonPins[i]);
                Serial.println("[READY] Press a story button.");
            }
        }
    }

    delay(10);
}

八、操作方式

  1. 將三個MP3檔放入microSD卡根目錄。
  2. 依接線圖接好三個按鈕、XPT8871與喇叭。
  3. 上傳程式後,開啟115200 baud序列監控視窗。
  4. 按白馬按鈕播放001.mp3;按烏龜按鈕播放002.mp3;按姻緣花按鈕播放003.mp3。

九、序列監控預期訊息

HUB 8735 Ultra Story Player V1.0-04
[SD] Initializing microSD card...
[CHECK] ...001.mp3 ... OK
[CHECK] ...002.mp3 ... OK
[CHECK] ...003.mp3 ... OK
[READY] Press a story button.

[BUTTON] GPIO: 10
[PLAY] Story: White Horse
[PLAY] File: ...001.mp3
[AUDIO] Playback started.
[AUDIO] Playback completed.

十、V1.0-03與V1.0-04比較

項目V1.0-03V1.0-04
播放方式開機後依序播放三個故事按下實體按鈕選擇故事
互動性三個獨立按鈕
microSD三個MP3檔沿用相同三個MP3檔
GPIO未使用按鈕GPIO10、11、12

十一、目前限制與後續方向

目前使用的playMp3()會等待故事播放完畢,因此播放期間不會偵測其他按鈕。對「一按一個完整故事」的操作方式而言很單純穩定;未來若需要中途停止、切換故事或調整音量,則要再研究非阻塞播放或額外的播放控制機制。

下一階段可以加入LED播放指示、音量按鈕、停止鍵、開機提示音,或進一步加入網路語音與AI對話功能。

部落格貼文注意事項:請在Blogger的「HTML檢視」中貼上本檔內容。本檔刻意不包含<html><head><body>標籤,並已限制程式碼區塊寬度,避免破壞Blogger版型或把右側資訊欄擠出畫面。

[水井村USR] HUB 8735 Ultra實作:microSD卡與三個MP3故事檔案測試

水井三寶故事機|V1.0-03

HUB 8735 Ultra實作:microSD卡與三個MP3故事檔案測試

本次測試讓HUB 8735 Ultra從microSD卡讀取三個MP3故事, 經由AOUT類比音訊輸出,再透過XPT8871功率擴大板推動喇叭, 逐步完成水井三寶樹藝故事機的聲音系統。

測試結果:功能通過,音訊品質仍需改善。

三個MP3故事均可正常讀取及播放,但目前仍有些許雜聲。 初步研判主要與音訊線過長、麵包板接觸、電源干擾及接地方式有關。

一、V1.0-03測試目標

microSD卡 確認HUB 8735 Ultra能正確掛載與讀取記憶卡。
三個故事檔案 檢查001.mp3、002.mp3及003.mp3是否存在。
MP3解碼 驗證三個故事能否依序完成解碼與播放。
音訊輸出 驗證AOUT、XPT8871擴大板與喇叭的完整聲音路徑。

二、使用材料

材料 數量 用途
HUB 8735 Ultra 1 讀取microSD卡並解碼MP3
microSD卡 1 儲存三個水井三寶故事
XPT8871音訊擴大板 1 放大AOUT類比音訊
4Ω 3W或8Ω喇叭 1 播放故事語音
5V穩壓電源 1 供應XPT8871擴大板
麵包板、杜邦線 若干 原型接線與功能測試
USB Type-C資料線 1 程式燒錄及序列監控

三、microSD卡準備

將microSD卡格式化為FAT32,再把三個MP3故事檔案直接放在根目錄:

microSD
├── 001.mp3 姻緣花故事
├── 002.mp3 烏龜故事
└── 003.mp3 白馬故事
檔名建議: 第一階段只使用英文或數字,不加入中文、空格及特殊符號; 副檔名統一使用小寫的.mp3

四、HUB 8735 Ultra與XPT8871接線圖

microSD MP3音訊播放接線圖
HUB 8735 Ultra microSD+MP3解碼
AOUT:類比音訊
GND:音訊參考地
AOUT → IN+
GND → IN−
XPT8871 單聲道功率擴大板
IN+:音訊輸入
IN−:音訊地
OUT+:喇叭輸出
OUT−:喇叭輸出
OUT+ → 喇叭+
OUT− → 喇叭−
喇叭
建議4Ω 3W或8Ω 2W~3W
獨立穩壓5V電源 → XPT8871
● +5V → +5V ● GND → 5V−
來源端 XPT8871端 功能
HUB 8735 Ultra AOUT IN+ 類比音訊訊號
HUB 8735 Ultra GND IN−/GND 音訊參考地
獨立5V正極 +5V 擴大板電源
獨立5V負極 5V− 擴大板電源地
喇叭正端 OUT+ BTL喇叭輸出
喇叭負端 OUT− BTL喇叭輸出
重要: XPT8871採BTL橋接輸出,OUT−不是GND。 喇叭必須接在OUT+OUT−之間, 不可將OUT−接到HUB 8735 Ultra的GND。

五、V1.0-03完整測試程式

/*
 * ============================================================
 * 專案:HUB 8735 Ultra水井三寶故事機
 * 版本:V1.0-03
 * 功能:microSD卡與三個MP3故事檔案測試
 * ============================================================
 *
 * microSD卡根目錄:
 *   001.mp3
 *   002.mp3
 *   003.mp3
 */

#include "AmebaFatFS.h"
#include "AmebaFatFSFile.h"

#define STORY_COUNT 3

// 0xAF為最大值0dB
// 初次測試降低音量,避免擴大器輸入過載
#define MP3_VOLUME 0x90

#define STORY_INTERVAL_MS 2000

AmebaFatFS fs;

const char *storyFiles[STORY_COUNT] = {
    "001.mp3",
    "002.mp3",
    "003.mp3"
};

const char *storyNames[STORY_COUNT] = {
    "姻緣花故事",
    "烏龜故事",
    "白馬故事"
};

String getFullPath(const char *filename)
{
    return String(fs.getRootPath()) + String(filename);
}

bool checkMP3File(const char *filename)
{
    String fullPath = getFullPath(filename);
    File testFile;

    Serial.print("[CHECK] ");
    Serial.print(fullPath);
    Serial.print(" ... ");

    testFile = fs.open(fullPath);

    if (!testFile.isOpen()) {
        Serial.println("NOT FOUND");
        return false;
    }

    Serial.print("OK, ");
    Serial.print(testFile.size());
    Serial.println(" bytes");

    testFile.close();
    return true;
}

bool checkAllStoryFiles()
{
    bool allFilesReady = true;

    Serial.println();
    Serial.println("Checking MP3 story files...");
    Serial.println("--------------------------------");

    for (int i = 0; i < STORY_COUNT; i++) {
        if (!checkMP3File(storyFiles[i])) {
            allFilesReady = false;
        }
    }

    Serial.println("--------------------------------");

    if (allFilesReady) {
        Serial.println("[PASS] All three MP3 files are ready.");
    } else {
        Serial.println("[FAIL] One or more MP3 files are missing.");
    }

    return allFilesReady;
}

bool playStory(int storyIndex)
{
    if (storyIndex < 0 || storyIndex >= STORY_COUNT) {
        Serial.println("[ERROR] Invalid story index.");
        return false;
    }

    String fullPath = getFullPath(storyFiles[storyIndex]);
    File mp3File;

    Serial.println();
    Serial.println("================================");

    Serial.print("[PLAY] Story: ");
    Serial.println(storyNames[storyIndex]);

    Serial.print("[PLAY] File: ");
    Serial.println(fullPath);

    mp3File = fs.open(fullPath);

    if (!mp3File.isOpen()) {
        Serial.println("[ERROR] Unable to open MP3 file.");
        return false;
    }

    Serial.print("[INFO] File size: ");
    Serial.print(mp3File.size());
    Serial.println(" bytes");

    mp3File.setMp3DigitalVol(MP3_VOLUME);

    Serial.println("[AUDIO] Playback started.");

    mp3File.playMp3();

    Serial.println("[AUDIO] Playback completed.");

    mp3File.close();

    return true;
}

void playAllStories()
{
    Serial.println();
    Serial.println("Starting three-story playback test...");

    for (int i = 0; i < STORY_COUNT; i++) {
        if (!playStory(i)) {
            Serial.print("[FAIL] Story ");
            Serial.print(i + 1);
            Serial.println(" playback failed.");
        }

        if (i < STORY_COUNT - 1) {
            delay(STORY_INTERVAL_MS);
        }
    }

    Serial.println();
    Serial.println("[DONE] Three-story playback test completed.");
}

void setup()
{
    Serial.begin(115200);
    delay(2000);

    Serial.println();
    Serial.println("================================================");
    Serial.println("HUB 8735 Ultra Story Player");
    Serial.println("Version: V1.0-03");
    Serial.println("================================================");

    Serial.println("[SD] Initializing microSD card...");

    fs.begin();

    Serial.print("[SD] Root path: ");
    Serial.println(fs.getRootPath());

    if (!checkAllStoryFiles()) {
        Serial.println("[SYSTEM] Playback cancelled.");
        fs.end();
        return;
    }

    playAllStories();

    fs.end();

    Serial.println("[SYSTEM] V1.0-03 test finished.");
}

void loop()
{
    // 本版本只在開機後測試一次
    delay(1000);
}

六、預期序列監控結果

Arduino IDE的序列監控視窗設定為115200 baud。 正常執行時會看到:

HUB 8735 Ultra Story Player
Version: V1.0-03

[SD] Initializing microSD card...
[CHECK] 0:/001.mp3 ... OK
[CHECK] 0:/002.mp3 ... OK
[CHECK] 0:/003.mp3 ... OK
[PASS] All three MP3 files are ready.

[PLAY] Story: 姻緣花故事
[AUDIO] Playback started.
[AUDIO] Playback completed.

[PLAY] Story: 烏龜故事
[AUDIO] Playback started.
[AUDIO] Playback completed.

[PLAY] Story: 白馬故事
[AUDIO] Playback started.
[AUDIO] Playback completed.

[DONE] Three-story playback test completed.

七、實際測試結果

  • microSD卡可以正常掛載。
  • 三個MP3故事檔案均可找到。
  • 三個故事可以依序播放。
  • HUB 8735 Ultra的AOUT可以輸出MP3音訊。
  • XPT8871可以驅動外接喇叭。
  • 目前音訊仍有些許雜聲,需要改善接線及供電。
V1.0-03判定:功能測試成功。

下一步不是立即增加功能,而是先把類比音訊、接地與電源整理好。

八、為什麼目前會有雜聲?

1. AOUT音訊線過長

AOUT屬於小訊號類比音訊。使用過長且沒有屏蔽的杜邦線, 容易接收USB、Wi-Fi、開關電源及數位GPIO產生的干擾。

建議將AOUT與GND縮短至5~10公分,並把兩條線互相纏繞。 正式版本可以改用屏蔽音訊線:

中心導線 → HUB 8735 Ultra AOUT
屏蔽層   → HUB 8735 Ultra GND

2. 音訊線與電源線交錯

AOUT到XPT8871的輸入線應遠離:

  • 5V大電流電源線
  • USB傳輸線
  • 開關電源模組
  • WS2812燈條電源線
  • 伺服馬達與馬達線路
  • XPT8871的喇叭輸出線

3. 多點接地形成地迴路

HUB 8735 Ultra由電腦USB供電,而擴大板由另一組5V電源供電時, 應採用單點共地,避免GND經過多條不同路徑。

建議採用單點共地
單一共地點
↓ ↓ ↓
HUB 8735 Ultra
GND
XPT8871音訊輸入
IN−/GND
5V電源
負極

4. 5V電源紋波

XPT8871播放較大音量時,負載電流會快速變化。 如果5V電源含有開關紋波,雜訊可能直接進入擴大器。

建議在XPT8871電源端附近並聯一顆470µF電解電容, 再並聯一顆0.1µF陶瓷電容:

+5V ───┬──── XPT8871 +5V
       ├──── 470µF正極
       └──── 0.1µF

GND ───┬──── XPT8871 GND
       ├──── 470µF負極
       └──── 0.1µF

5. 輸入音量過高

若HUB 8735 Ultra輸出音量太大,可能讓XPT8871輸入過載, 產生破音或沙沙聲。目前程式使用:

#define MP3_VOLUME 0x90

如果仍有明顯失真,可以降低為:

#define MP3_VOLUME 0x80

如果降低音量後破音明顯改善,表示問題可能包含輸入過載, 不完全是外部電磁干擾。

九、建議的改善接線

HUB 8735 Ultra 5~10公分屏蔽音訊線 XPT8871 短喇叭線 喇叭
  1. 將AOUT與GND線縮短到5~10公分。
  2. 把AOUT與GND互相纏繞,或改用屏蔽音訊線。
  3. 將XPT8871移到靠近喇叭的位置。
  4. 讓音訊線遠離USB線、開關電源及5V大電流線。
  5. 在XPT8871旁增加470µF及0.1µF電容。
  6. 使用單點共地,避免形成地迴路。
  7. 將MP3數位音量由0x90降低到0x80進行比較。
  8. 正式版本改用焊接或洞洞板,減少麵包板接觸問題。
電源安全: 如果使用具有裸露端子的金屬外殼開關電源, AC市電輸入端必須加裝絕緣護蓋。 通電時不要在裸露的L、N端子附近調整杜邦線。 原型測試建議先使用有外殼、具安全認證的5V變壓器。

十、從聲音播放走向樹藝AI

microSD卡 三個MP3故事 HUB 8735解碼 AOUT XPT8871 喇叭

V1.0-03證明HUB 8735 Ultra除了可以進行影像辨識, 也能結合microSD卡、MP3解碼、類比音訊輸出與外接擴大器, 成為地方故事播放裝置。

對水井三寶而言,這不只是「播放三個MP3」, 而是讓姻緣花、烏龜與白馬開始擁有自己的聲音, 為後續樹藝作品導覽、影像辨識與AI對話建立基礎。

[水井村USR] 樹藝AI故事機開發紀錄V1.0-02:用三個按鈕選擇水井三寶故事

樹藝AI故事機開發紀錄V1.0-02:用三個按鈕選擇水井三寶故事

完成V1.0-01功能鍵與板載LED測試後,本階段將HUB 8735 Ultra擴充成三個獨立故事按鈕,分別代表「脂片龜/烏龜」、「白馬」及「姻緣花」。實測過程中發現GPIO 13無法正常觸發,因此將第三顆按鈕改接GPIO 12後,三個故事事件均能正確辨識。

系列前導閱讀:
HUB 8735 Ultra開發環境與基本設定,可先參考: 《國產Wi-Fi晶片8735 Ultra初體驗》

一、本階段開發目標

V1.0-02預定完成以下功能:

  • 建立三個水井三寶故事按鈕。
  • 每顆按鈕對應一個故事編號及故事名稱。
  • 加入按鍵除彈跳,避免按一次卻產生多次事件。
  • 按住按鈕時不連續重複觸發。
  • 使用LED閃爍次數區分三個故事。
  • 透過序列監控顯示完整故事事件。
按下故事按鈕 → 按鍵除彈跳 → 判斷故事角色 → 顯示事件 → LED閃爍

二、實際測試電路


▲ HUB 8735 Ultra連接三顆外接按鈕,分別作為脂片龜、白馬及姻緣花故事選擇鍵。

三顆按鈕可用不同顏色協助辨識:

按鈕 故事角色 GPIO LED提示
按鈕1 脂片龜/烏龜故事 GPIO 10 閃爍1次
按鈕2 白馬故事 GPIO 11 閃爍2次
按鈕3 姻緣花故事 GPIO 12 閃爍3次

三、實測發現:GPIO 13無法正常工作

原始規劃使用:

GPIO 10:脂片龜/烏龜故事
GPIO 11:白馬故事
GPIO 13:姻緣花故事

實際接線與測試後,GPIO 10及GPIO 11可以正常讀取按鍵,但GPIO 13未能正確觸發第三個故事事件。

將第三顆按鈕改接GPIO 12,並把程式改為:

const int FLOWER_BUTTON_PIN = 12;

重新燒錄後,三顆故事按鈕均能正常運作。

原始配置:GPIO 10、11、13
實測修正:GPIO 10、11、12
本次結果代表GPIO 13在目前HUB 8735 Ultra板卡、開發板套件及接線配置中,不適合直接作為第三顆故事按鈕。這是本次實機測試結果,不宜直接推論所有RTL8735B板卡的GPIO 13都不能使用。

四、按鈕接線方式

程式使用內部上拉電阻,因此每顆按鈕只需接在GPIO與GND之間。

故事按鈕 按鈕一端 按鈕另一端
脂片龜/烏龜 GPIO 10 GND
白馬 GPIO 11 GND
姻緣花 GPIO 12 GND

三顆按鈕可以共用同一個GND。

接線安全提醒:使用INPUT_PULLUP時,按鈕應接在GPIO與GND之間,不要接到5V。若接到5V,不但邏輯不符,也可能增加接線錯誤或硬體損壞的風險。

五、GPIO 12與功能鍵的注意事項

HUB 8735 Ultra板上的功能鍵也與GPIO 12相關,因此第三顆姻緣花按鈕使用GPIO 12後,必須特別注意開機與燒錄操作。

  • 正常執行程式時,GPIO 12可以作為姻緣花故事按鈕。
  • 燒錄前仍可使用板上功能鍵進入Flash Mode。
  • 一般重新開機時,不要持續按住GPIO 12外接按鈕。
  • 若GPIO 12在RESET時維持低電位,可能影響開機模式判斷。
  • 燒錄完成並按RESET後,先放開所有外接按鈕。
GPIO 12同時承擔故事輸入與功能鍵相關用途。正式製作故事機時,應避免參觀者在系統開機或重新啟動的瞬間持續按住姻緣花按鈕。

六、V1.0-02完整修正版程式

以下程式已將姻緣花按鈕由GPIO 13修正為GPIO 12。

/*
 * HUB 8735 Ultra 樹藝AI故事機
 * V1.0-02:三個水井三寶故事按鈕
 *
 * GPIO 10:脂片龜/烏龜故事
 * GPIO 11:白馬故事
 * GPIO 12:姻緣花故事
 */

const int TURTLE_BUTTON_PIN = 10;
const int HORSE_BUTTON_PIN  = 11;
const int FLOWER_BUTTON_PIN = 12;

// 按鍵除彈跳時間
const unsigned long DEBOUNCE_TIME = 50;

// 儲存每顆按鈕的狀態
struct StoryButton {
  int pin;
  const char* storyId;
  const char* storyName;
  int ledBlinkCount;

  bool stableState;
  bool lastReading;
  unsigned long lastChangeTime;
};

// 建立三顆故事按鈕
StoryButton storyButtons[] = {
  {
    TURTLE_BUTTON_PIN,
    "001",
    "脂片龜/烏龜故事",
    1,
    HIGH,
    HIGH,
    0
  },
  {
    HORSE_BUTTON_PIN,
    "002",
    "白馬故事",
    2,
    HIGH,
    HIGH,
    0
  },
  {
    FLOWER_BUTTON_PIN,
    "003",
    "姻緣花故事",
    3,
    HIGH,
    HIGH,
    0
  }
};

const int BUTTON_COUNT =
  sizeof(storyButtons) / sizeof(storyButtons[0]);

unsigned long eventCounter = 0;

void setup() {
  Serial.begin(115200);

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);

  for (int i = 0; i < BUTTON_COUNT; i++) {
    pinMode(storyButtons[i].pin, INPUT_PULLUP);
  }

  Serial.println();
  Serial.println("======================================");
  Serial.println("HUB 8735 Ultra");
  Serial.println("Tree Art AI Story Machine");
  Serial.println("V1.0-02 Three Story Buttons");
  Serial.println("======================================");
  Serial.println("GPIO 10:脂片龜/烏龜故事");
  Serial.println("GPIO 11:白馬故事");
  Serial.println("GPIO 12:姻緣花故事");
  Serial.println("--------------------------------------");
  Serial.println("系統準備完成,請按下故事按鈕。");
}

void loop() {
  for (int i = 0; i < BUTTON_COUNT; i++) {
    updateStoryButton(storyButtons[i]);
  }
}

/*
 * 讀取、除彈跳及判斷單次按鍵事件
 */
void updateStoryButton(StoryButton &button) {
  bool currentReading = digitalRead(button.pin);

  // 發現讀值改變,重新計算穩定時間
  if (currentReading != button.lastReading) {
    button.lastChangeTime = millis();
    button.lastReading = currentReading;
  }

  // 讀值穩定超過除彈跳時間
  if ((millis() - button.lastChangeTime) >=
      DEBOUNCE_TIME) {

    // 穩定狀態真的發生改變
    if (currentReading != button.stableState) {
      button.stableState = currentReading;

      // INPUT_PULLUP:LOW表示按下
      if (button.stableState == LOW) {
        triggerStory(button);
      }
    }
  }
}

/*
 * 單次故事觸發事件
 */
void triggerStory(const StoryButton &button) {
  eventCounter++;

  Serial.println();
  Serial.println("======================================");

  Serial.print("EVENT NO:");
  Serial.println(eventCounter);

  Serial.print("STORY ID:");
  Serial.println(button.storyId);

  Serial.print("STORY NAME:");
  Serial.println(button.storyName);

  Serial.print("BUTTON GPIO:");
  Serial.println(button.pin);

  Serial.println("EVENT TYPE:STORY_SELECTED");
  Serial.println("STATUS:TRIGGERED");
  Serial.println("======================================");

  blinkStoryLED(button.ledBlinkCount);
}

/*
 * 利用LED閃爍次數辨識故事
 */
void blinkStoryLED(int blinkCount) {
  for (int i = 0; i < blinkCount; i++) {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(200);

    digitalWrite(LED_BUILTIN, LOW);
    delay(200);
  }
}
由於文章以HTML方式發表,程式中的小於符號、比較符號及參考符號已使用HTML實體編碼,貼到Blogger後會正常顯示為Arduino程式。

七、按鍵除彈跳與單次觸發

1. 為什麼需要除彈跳?

機械按鈕按下時,接點不會立即形成一次乾淨的電位變化,而可能在數毫秒內產生多次高低跳動。如果直接讀取GPIO,按一次可能被誤判成按了很多次。

本程式設定50毫秒除彈跳:

const unsigned long DEBOUNCE_TIME = 50;

2. 為什麼按住不會一直觸發?

程式只在穩定狀態由HIGH變成 LOW時觸發故事。

if (button.stableState == LOW) {
  triggerStory(button);
}

因此操作結果為:

  • 按下:觸發一次。
  • 持續按住:不重複觸發。
  • 放開:回到等待狀態。
  • 再次按下:產生下一次事件。

八、LED閃爍代表哪個故事?

LED閃爍 故事 未來對應音訊
1次 脂片龜/烏龜 001.mp3
2次 白馬 002.mp3
3次 姻緣花 003.mp3

目前LED閃爍是故事事件的視覺測試。進入下一階段後,將把LED事件延伸成SD卡MP3播放。

九、正確燒錄流程

按住板上功能鍵 → 按下並放開RESET → 放開功能鍵 → 上傳 → 成功後再按RESET
步驟1:確認Arduino IDE已選擇HUB 8735 Ultra及正確COM埠。
步驟2:按住板上的功能鍵。
步驟3:按下並放開RESET鍵。
步驟4:最後放開功能鍵。
步驟5:在Arduino IDE按下「上傳」。
步驟6:等待顯示upload success。
步驟7:燒錄完成後,確定三顆外接按鈕都已放開,再按一下RESET。

十、序列監控開機結果

燒錄完成並按下RESET後,序列監控顯示:

== Rtl8735b IoT Platform ==

[Normal mode]

BootFromNORFlash

[Start Boot ROM...]

=== Load PARTBL ===

這些訊息代表:

  • RTL8735B已經重新啟動。
  • 目前處於Normal Mode。
  • 系統由NOR Flash載入剛才燒錄的程式。
  • Boot ROM開始讀取系統分割表。

完成底層開機後,程式會顯示:

======================================
HUB 8735 Ultra
Tree Art AI Story Machine
V1.0-02 Three Story Buttons
======================================
GPIO 10:脂片龜/烏龜故事
GPIO 11:白馬故事
GPIO 12:姻緣花故事
--------------------------------------
系統準備完成,請按下故事按鈕。

十一、預期的故事事件

按下脂片龜/烏龜按鈕

EVENT NO:1
STORY ID:001
STORY NAME:脂片龜/烏龜故事
BUTTON GPIO:10
EVENT TYPE:STORY_SELECTED
STATUS:TRIGGERED

按下白馬按鈕

EVENT NO:2
STORY ID:002
STORY NAME:白馬故事
BUTTON GPIO:11
EVENT TYPE:STORY_SELECTED
STATUS:TRIGGERED

按下姻緣花按鈕

EVENT NO:3
STORY ID:003
STORY NAME:姻緣花故事
BUTTON GPIO:12
EVENT TYPE:STORY_SELECTED
STATUS:TRIGGERED

十二、本次測試結果

V1.0-02測試完成:
  • GPIO 10可以觸發脂片龜/烏龜故事。
  • GPIO 11可以觸發白馬故事。
  • GPIO 13在本次配置中無法正常觸發。
  • 第三顆按鈕改接GPIO 12後,可以觸發姻緣花故事。
  • 三顆按鈕均可維持單次觸發。
  • 按住按鈕時不會連續產生故事事件。
  • LED可依故事分別閃爍1、2、3次。
  • 序列監控可顯示故事ID、名稱、GPIO及事件編號。
測試項目 結果 處理方式
GPIO 10 正常 保留作為脂片龜按鈕
GPIO 11 正常 保留作為白馬按鈕
GPIO 13 未正常觸發 本版本暫不使用
GPIO 12 正常 改作姻緣花按鈕
按鍵除彈跳 正常 50毫秒穩定判斷
單次觸發 正常 放開後才能再次觸發

十三、從三個按鈕走向故事播放

V1.0-02已經建立完整的故事選擇架構:

三個實體按鈕 → 三個故事ID → 三種LED提示 → 三個故事事件

下一步只要將故事事件連接到音訊播放函式:

001 → 播放脂片龜故事
002 → 播放白馬故事
003 → 播放姻緣花故事

就能從「按鈕測試器」進一步發展成真正的離線樹藝故事機。

結語

硬體開發不能只依照腳位圖推測,仍須透過實際接線、序列輸出及功能驗證確認可用性。本次原先選用GPIO 13,但實測無法正常觸發;改用GPIO 12後,三個水井三寶故事按鈕均能穩定工作。

這次修正也再次說明「逐層測試」的重要性:先確認按鈕事件,再加入SD卡及MP3播放,可以大幅降低整合過程的除錯難度。

下一階段將進入V1.0-03:microSD卡與三個MP3故事檔案測試,讓三顆按鈕真正播放脂片龜、白馬及姻緣花的故事。

延伸閱讀: 《國產Wi-Fi晶片8735 Ultra初體驗》

[水井村USR] 樹藝AI故事機開發紀錄V1.0-01:用HUB 8735 Ultra功能鍵控制板載LED

樹藝AI故事機開發紀錄V1.0-01:用HUB 8735 Ultra功能鍵控制板載LED

要讓樹藝作品成為一部「會看、會聽、會說、會回應」的AI故事機,第一步不是立刻加入大型語言模型,而是先確認開發板最基本的輸入與輸出功能。本次使用HUB 8735 Ultra,完成「功能鍵控制板載LED」測試,成功驗證按鍵輸入、LED輸出、序列監控與程式燒錄流程。

系列前導文章:
尚未完成Arduino IDE與HUB 8735 Ultra開發環境設定者,可先閱讀: 《國產Wi-Fi晶片8735 Ultra初體驗》

一、為什麼從按鍵與LED開始?

樹藝AI故事機未來預計整合故事按鈕、SD卡音訊、喇叭、燈光、攝影機、語音辨識、RAG知識庫與AI Agent。若一開始就將所有模組接在一起,發生問題時很難判斷究竟是按鍵、電源、音訊、網路還是程式造成。

因此,開發採用逐層驗證方式:

  1. 先確認開發板可以正常燒錄。
  2. 確認功能鍵可以被程式讀取。
  3. 確認板載LED可以被程式控制。
  4. 確認序列監控能顯示系統狀態。
  5. 完成後再擴充三個故事按鈕與音訊播放。
功能鍵輸入 → RTL8735B程式判斷 → 板載LED輸出 → 序列監控顯示

二、HUB 8735 Ultra硬體位置


▲ HUB 8735 Ultra腳位配置。板上具有功能鍵、RESET鍵、攝影機介面、GPIO及兩個USB Type-C連接埠。

本次使用的主要元件如下:

元件 功能
HUB 8735 Ultra 執行按鍵判斷及LED控制程式
功能鍵/GPIO 12 作為使用者輸入
LED_BUILTIN 顯示按鍵狀態
USB Type-C 供電、序列通訊及程式燒錄
CH340序列晶片 讓Windows以COM埠辨識開發板


▲ HUB 8735 Ultra實際測試情形。本階段先確認按鍵、板載LED與USB燒錄功能,作為樹藝AI故事機的輸入輸出基礎。

三、V1.0-01測試目標

本階段預定完成四項驗證:

01|燒錄成功 Arduino IDE顯示 upload success
02|程式啟動 燒錄完成後按RESET,開發板開始執行新程式。
03|序列輸出 序列監控可看到開機及程式啟動畫面。
04|按鍵控制 按下功能鍵後,板載LED狀態可以正確改變。

四、功能鍵控制板載LED程式

本次測試程式如下:

/*
 * HUB 8735 Ultra 樹藝AI故事機
 * V1.0-01:功能鍵控制板載LED
 */

const int FUNCTION_BUTTON_PIN = 12;

void setup() {
  Serial.begin(115200);

  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(FUNCTION_BUTTON_PIN, INPUT_PULLUP);

  digitalWrite(LED_BUILTIN, LOW);

  Serial.println();
  Serial.println("==============================");
  Serial.println("HUB 8735 Ultra");
  Serial.println("Tree Art AI Story Machine");
  Serial.println("V1.0-01 Button Test");
  Serial.println("==============================");
  Serial.println("請按下功能鍵");
}

void loop() {
  bool buttonPressed =
    (digitalRead(FUNCTION_BUTTON_PIN) == LOW);

  if (buttonPressed) {
    digitalWrite(LED_BUILTIN, HIGH);
    Serial.println("BUTTON: PRESSED | LED: ON");
  } else {
    digitalWrite(LED_BUILTIN, LOW);
  }

  delay(100);
}

五、程式重點說明

1. 功能鍵腳位

const int FUNCTION_BUTTON_PIN = 12;

以GPIO 12作為功能鍵輸入腳位。使用較完整的 FUNCTION_BUTTON_PIN 名稱,也能避免與開發板套件既有的巨集名稱發生衝突。

2. 使用內部上拉電阻

pinMode(FUNCTION_BUTTON_PIN, INPUT_PULLUP);

設定為INPUT_PULLUP後,按鍵未按下時讀到 HIGH,按下並接地時讀到 LOW

3. 判斷按鍵是否按下

bool buttonPressed =
  (digitalRead(FUNCTION_BUTTON_PIN) == LOW);

程式將腳位狀態轉換成容易理解的布林變數。當 buttonPressed 為真,就代表使用者已按下按鍵。

4. 控制板載LED

digitalWrite(LED_BUILTIN, HIGH);

使用開發板套件提供的LED_BUILTIN名稱,可以避免自行指定腳位時產生錯誤,也能提高程式的可讀性。

5. 顯示按鍵事件

Serial.println("BUTTON: PRESSED | LED: ON");

每次偵測到按鍵按下,序列監控會顯示按鍵與LED狀態。未來加入故事播放、Wi-Fi和Agent後,這些除錯訊息會非常重要。

六、HUB 8735 Ultra燒錄操作

HUB 8735 Ultra燒錄前必須手動進入Flash Mode。這項操作與一般可以自動重置的Arduino開發板不同。

按住功能鍵 → 按下並放開RESET → 放開功能鍵 → 上傳程式 → 完成後再按RESET
步驟1:使用USB Type-C線連接HUB 8735 Ultra的燒錄端與電腦。
步驟2:在Arduino IDE選擇HUB 8735 Ultra開發板。
步驟3:選擇Windows裝置管理員顯示的COM埠。本次測試使用COM10。
步驟4:按住板上的功能鍵不放。
步驟5:按一下RESET鍵並放開。
步驟6:最後放開功能鍵,讓開發板進入Flash Mode。
步驟7:在Arduino IDE按下「上傳」。
步驟8:看到upload success後,再按一下RESET鍵執行新程式。
重要:燒錄完成後必須再按一次RESET。如果只看到upload success卻沒有按RESET,開發板可能仍停留在燒錄狀態,新程式不會立即開始執行。

七、燒錄成功訊息

本次Arduino IDE顯示的燒錄訊息如下:

Enter Flash Mode!
Start Upload Flash
Uploading................upload success
End Upload Flash

其中upload success代表程式映像檔已成功寫入RTL8735B的Flash記憶體。

八、序列監控開機訊息

燒錄完成並按下RESET後,序列監控首先顯示RTL8735B平台的開機資訊:

== Rtl8735b IoT Platform ==

[Normal mode]

BootFromNORFlash

[Start Boot ROM...]

=== Load PARTBL ===

這些訊息代表:

  • RTL8735B已重新啟動。
  • 目前進入Normal Mode,而不是Flash Mode。
  • 系統由NOR Flash載入程式。
  • Boot ROM開始讀取分割表並啟動韌體。

完成底層開機後,才會進入Arduino程式的setup(),並顯示自訂啟動畫面。

==============================
HUB 8735 Ultra
Tree Art AI Story Machine
V1.0-01 Button Test
==============================
請按下功能鍵

按下功能鍵時,序列監控會顯示:

BUTTON: PRESSED | LED: ON

九、本次實測結果

V1.0-01四項測試全部正常:
  1. Arduino IDE顯示 upload success
  2. 燒錄完成後按RESET,程式可以正常執行。
  3. 序列監控可以看到RTL8735B開機及程式啟動畫面。
  4. 功能鍵可以正確改變板載LED狀態。
測試項目 結果 代表意義
CH340/COM10 正常 USB序列通訊正常
Flash Mode 正常 可正常進入燒錄狀態
程式燒錄 成功 Flash寫入工具正常
RESET啟動 正常 可由NOR Flash執行新程式
功能鍵輸入 正常 GPIO數位輸入正常
板載LED輸出 正常 GPIO數位輸出正常
序列監控 正常 可用於後續除錯與事件觀察

十、這一步和樹藝AI故事機有什麼關係?

本次雖然只完成一顆按鍵和一顆LED,但已建立故事機最基本的「感知—判斷—回應」架構:

  • 感知:功能鍵偵測使用者操作。
  • 判斷:RTL8735B判斷按鍵是否被按下。
  • 回應:LED改變狀態,序列埠顯示事件。

未來只要將LED回應換成故事播放,就能形成:

按下水井三寶按鈕 → 判斷故事角色 → 播放MP3 → 啟動對應燈光

再進一步連接Raspberry Pi或雲端AI,就能擴充為:

按住說話 → 語音辨識 → RAG搜尋 → LLM回答 → TTS播放

結語

V1.0-01完成的不只是LED控制,而是確認HUB 8735 Ultra從USB連線、Flash燒錄、系統啟動、GPIO輸入、GPIO輸出到序列監控的完整基礎流程。

採用逐步驗證的方法,可以讓後續的SD卡、MP3播放、燈光、攝影機與AI對話模組,都建立在已確認正常的基礎上。

下一階段將進入V1.0-02:水井三寶故事按鈕,為烏龜、白馬與姻緣花分別建立獨立按鍵事件,並加入按鍵除彈跳與單次觸發機制。

延伸閱讀: 《國產Wi-Fi晶片8735 Ultra初體驗》

[水井村USR] HUB 8735 Ultra第一次燒錄實測:從COM埠消失、LED巨集衝突到成功進入Flash Mode

HUB 8735 Ultra第一次燒錄實測:從COM埠消失、LED巨集衝突到成功進入Flash Mode

HUB 8735 Ultra採用瑞昱RTL8735系列晶片,整合Wi-Fi、BLE、攝影機、音訊與NPU人工智慧運算能力,是一款很適合影像辨識、AIoT與智慧生活應用的國產IC開發板。本篇記錄實際使用Arduino IDE測試板載LED的完整過程,包含COM埠辨識、程式編譯錯誤、Flash Mode操作,以及燒錄完成後如何正確執行程式。

前導閱讀:
如果尚未安裝HUB 8735 Ultra開發板套件,建議先閱讀: 《國產Wi-Fi晶片8735 Ultra初體驗》 。 本文將接續該篇文章,聚焦在實際連線、除錯與燒錄測試。

一、測試目標

這次測試希望依序確認以下功能:

  • Windows能否辨識HUB 8735 Ultra的USB序列埠。
  • Arduino IDE能否找到正確的COM埠。
  • 開發板能否正確進入Flash Mode。
  • 程式能否成功編譯及燒錄。
  • 板載LED能否每秒閃爍一次。

LED閃爍看似簡單,卻是檢查開發環境、USB連線、開發板套件、燒錄工具與GPIO輸出的最佳第一步。

二、準備項目

項目 用途
HUB 8735 Ultra 本次測試開發板
可傳輸資料的USB Type-C線 供電、燒錄與序列通訊
Windows電腦 執行Arduino IDE及查看裝置管理員
Arduino IDE 編譯及上傳程式
ideasHatch開發板套件 提供HUB 8735 Ultra板型、腳位及燒錄工具
特別注意:HUB 8735 Ultra具有兩個USB Type-C埠。進行Arduino程式燒錄時,應使用連接CH340燒錄晶片的USB/Debug端,而不是OTG功能端。

三、確認Windows是否辨識開發板

將HUB 8735 Ultra接到電腦後,開啟Windows「裝置管理員」,展開:

連接埠(COM和LPT)

本次測試顯示:

USB-SERIAL CH340 (COM12)

這代表:

  • USB線具有資料傳輸能力。
  • 電腦已辨識板上的CH340序列晶片。
  • 目前使用的序列埠為COM12。

COM編號會依電腦與USB插孔而不同,不一定都是COM12。請以自己的裝置管理員顯示結果為準。

四、為什麼打開Arduino IDE後,COM埠好像不見了?

測試初期,在尚未打開Arduino IDE時,可以在裝置管理員看到COM12;打開Arduino IDE後,卻出現COM埠不穩定或找不到的現象。

遇到這種情況,可先依序檢查:

  1. 先開啟Arduino IDE,等它完全啟動後再接上開發板。
  2. 使用可傳輸資料的Type-C線。
  3. 直接接到電腦USB埠,不經過USB Hub。
  4. 確認沒有開啟其他序列監控或燒錄軟體。
  5. 確認選擇的是CH340所對應的COM埠。
  6. 依正確按鍵順序讓開發板進入Flash Mode。
本次實測最後確認:CH340、COM12及USB線均可正常工作,關鍵是HUB 8735 Ultra需要以功能鍵和RESET鍵手動進入Flash Mode。

五、第一個程式錯誤:LED_B名稱衝突

最初嘗試使用以下寫法:

const int LED_B = 26;

編譯時出現:

note: in expansion of macro 'LED_B'
const int LED_B = 26;
          ^~~~~
exit status 1

原因是HUB 8735 Ultra開發板套件已經將 LED_B 定義成巨集。當我們再次宣告相同名稱時,就會產生命名衝突。

因此,不需要重新定義LED腳位,可以直接使用開發板套件提供的:

  • LED_BUILTIN:板載LED。
  • LED_B:板載藍色LED定義。
  • LED_G:板載綠色LED定義。
在Arduino開發板套件中,腳位名稱常以巨集預先定義。若自行宣告相同名稱,就可能出現「in expansion of macro」錯誤。最安全的方法是直接使用套件提供的名稱,或改用不重複的變數名稱,例如 BLUE_LED_PIN

六、成功使用的LED閃爍程式

最後使用Arduino標準的板載LED名稱,程式如下:

void setup() {
  // 將板載LED腳位設定為輸出
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  // LED切換為高電位
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);

  // LED切換為低電位
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
}

這段程式每隔一秒改變一次LED輸出狀態,用來驗證GPIO與程式執行是否正常。

七、HUB 8735 Ultra正確燒錄方法

HUB 8735 Ultra不像部分Arduino開發板會自動進入燒錄模式。本次實測必須先利用「功能鍵+RESET鍵」手動進入Flash Mode。

按住功能鍵 → 按下並放開RESET → 放開功能鍵 → 開始上傳 → 完成後再按RESET
步驟1:使用USB Type-C線將HUB 8735 Ultra的燒錄/Debug端接到電腦。
步驟2:在Arduino IDE選擇正確的HUB 8735 Ultra開發板及COM埠。
步驟3:按住左側「功能鍵」不放。
步驟4:按一下右側「RESET鍵」,然後放開RESET鍵。
步驟5:最後才放開「功能鍵」。此時開發板已進入Flash Mode。
步驟6:在Arduino IDE按下「上傳」。
步驟7:等待Arduino IDE顯示燒錄成功訊息。
步驟8:燒錄完成後,再按一下RESET鍵,讓新程式開始執行。

八、如何判斷燒錄成功?

本次Arduino IDE顯示的關鍵訊息如下:

Enter Flash Mode!
Start Upload Flash
Uploading................upload success
End Upload Flash

其中最重要的是:

upload success

這表示韌體已成功寫入HUB 8735 Ultra。

看到「upload success」後,程式不一定會立即執行。必須再按一下RESET鍵,開發板才會退出燒錄狀態並啟動剛才上傳的新程式。

九、這次除錯過程學到什麼?

遇到的現象 原因或處理方式
裝置管理員看到COM12 表示Windows已辨識CH340燒錄介面
Arduino IDE內COM埠不穩定 確認USB線、燒錄端及Flash Mode操作
宣告LED_B時編譯失敗 LED_B已由開發板套件定義,產生巨集名稱衝突
程式無法直接上傳 燒錄前需以功能鍵和RESET鍵進入Flash Mode
顯示upload success但LED沒閃 燒錄完成後需再按一次RESET鍵
LED成功閃爍 代表USB、驅動、開發板套件、Flash及GPIO均正常

十、後續開發前的快速檢查表

  • 使用具備資料傳輸功能的USB Type-C線。
  • 接到HUB 8735 Ultra的燒錄/Debug端。
  • 在裝置管理員確認CH340及COM編號。
  • 在Arduino IDE選擇正確板型和COM埠。
  • 避免自行重新宣告LED_B、LED_G等既有巨集。
  • 燒錄前先手動進入Flash Mode。
  • 確認出現「upload success」。
  • 燒錄完成後按RESET執行新程式。
本次測試結果:
HUB 8735 Ultra已成功透過COM12完成程式燒錄,板載LED可依程式每秒改變一次狀態。這也確認開發環境、CH340序列通訊、Flash燒錄工具及GPIO輸出功能均正常。

十一、從LED閃爍走向AIoT應用

完成LED測試後,下一階段就可以逐步加入Wi-Fi、攝影機、影像串流、物件辨識、麥克風、音訊播放及MQTT通訊。

建議依照以下順序進行:

  1. 板載LED與按鈕測試。
  2. 序列埠輸出測試。
  3. Wi-Fi連線測試。
  4. 攝影機影像串流。
  5. 板端物件辨識。
  6. MQTT或HTTP資料傳輸。
  7. 與Raspberry Pi、Django或雲端AI平台整合。
  8. 發展具備影像、語音與Agent控制能力的AI機器人。

結語

第一次測試開發板時,真正重要的不是讓一顆LED亮起來,而是建立一套可重複的除錯方法:先確認電腦是否辨識硬體,再確認開發板與COM埠,接著排除程式命名問題,最後掌握正確的Flash Mode與RESET操作。

HUB 8735 Ultra整合了國產IC、Wi-Fi、BLE、攝影機、音訊及邊緣AI能力。當最基本的燒錄流程穩定後,就能進一步發展智慧辨識、AIoT、地方導覽、樹藝AI與可以談天的互動機器人。

延伸閱讀: 國產Wi-Fi晶片8735 Ultra初體驗