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初體驗

2026年8月22日 星期六

把社區故事放上網以前: 個資、肖像權、著作權與AI倫理

封包旅行記 EP08|地方知識的數位倫理

把社區故事放上網以前:
個資、肖像權、著作權與AI倫理

一段長者口述、一張兒童活動照、一件社區工藝、一首背景音樂,從手機進入Django網站後,就不再只是「分享」。它可能同時涉及可識別個人、人格利益、著作利用、文化詮釋與AI再製。最好的數位平台,不只是讓故事被看見,也讓說故事的人保有尊嚴、選擇與權利。

一、先從一句話開始:能拍,不代表能上網

個資照片、聲音、姓名、職業、家庭與活動經歷,可能直接或間接識別個人。
肖像與人格即使照片由你拍攝,被攝者仍有其肖像、隱私與人格利益。
著作權故事文字、照片、錄音、影片、音樂與工藝設計可能分屬不同權利人。
AI倫理AI可能誤寫、扭曲文化、洩漏資料、生成近似內容或讓人誤以為是真實紀錄。

上網前四問:這是誰的資料?誰的形象?誰的創作?誰承擔被誤用、被搜尋、被永久轉傳的風險?

本文是教育與場域治理教材,不是個案法律意見。實際爭議、敏感資料、商業授權或大型計畫,應由所屬機構法遵/法務或合格專業人士依最新法規及具體事實審查。

二、社區採集不是只有一種權利

拍攝「長者介紹姻緣花工藝」的短片,至少可能同時出現下列權利與責任:

素材層可能關係人上網前要確認
長者姓名、臉、聲音與經歷受訪者蒐集目的、公開範圍、使用期限、撤回/更正與聯絡方式
長者原創敘事或講稿講述/撰寫者是否具有創作性、是否授權錄音、改寫與公開傳輸
影片與照片攝影者、製作團隊著作權歸屬及網站、社群、教材、展覽的利用範圍
工藝作品與圖樣工藝師/共同創作者可否拍攝、改編、做成商品或交由AI再設計
背景音樂、字型與插圖各素材權利人授權條款是否涵蓋公開傳輸、剪輯與商業/非商業用途
地方傳說與共同記憶社區與敘事相關人法律之外,是否需要社區確認、共同詮釋與利益回饋

「照片是我拍的」只回答攝影著作的一部分。它沒有自動解決照片中人物的肖像、隱私、個資,也不表示可以任意利用被拍攝的工藝或背景素材。

三、個資:可識別,不只看有沒有姓名

依臺灣個人資料保護法,個人資料包含姓名、特徵、家庭、職業、聯絡方式、社會活動,以及其他得直接或間接識別自然人的資料。因此,「水井村唯一會做某工藝的82歲老師傅」即使沒有姓名,也可能被間接識別。

直接識別

姓名、清楚正臉、電話、住址、帳號、學號、車牌、完整語音自我介紹。

間接識別

村落+職業+年齡+家庭關係、地標旁住家、時間地點與罕見事件的組合。

蒐集前告知,不應只寫「同意拍照」

個資法第8條列有蒐集者名稱、目的、資料類別、利用期間/地區/對象/方式、當事人權利與不提供的影響等應告知事項。實務上可轉成易懂的六句話:

  1. 誰:由哪個學校、社群或計畫負責?聯絡窗口是誰?
  2. 為何:用於地方文化保存、USR教學、網站導覽,還是商品行銷?
  3. 收什麼:姓名、照片、聲音、故事、作品與聯絡資料哪些會被保存?
  4. 怎麼用:放在哪些網站、社群、教材與展覽?會不會交由AI處理?
  5. 多久與多遠:保存多久?可能在全球網路被讀取或轉傳嗎?
  6. 如何選擇:可以不同意某一用途嗎?如何查閱、更正、停止利用或申請下架?

同意必須可被證明,且目的不能無限擴張。「同意參加活動」不等於同意照片永久放在公開網站,更不等於同意拿去訓練AI或製作商品。

四、資料最小化:不是每個故事都要綁定真實身分

原始版本風險降低做法
完整姓名+住家位置+聯絡電話公開頁僅顯示經同意的稱呼與村落,聯絡資訊放在受控後台
兒童清楚正臉+學校班級+活動時間優先拍背影、手部作品或團體遠景;不公開可定位的作息資訊
長者健康狀況寫入故事若非敘事必要就刪除;必要時另做明確告知、評估敏感性與存取權限
原始錄音直接公開先轉錄、由受訪者校對,再選擇匿名文字版或經同意的剪輯版
照片保留GPS與裝置資訊上傳前移除EXIF定位與不必要中繼資料

最小化不是讓故事失去溫度:是只公開完成目的真正需要的資訊,把聯絡資料、原始檔與公開內容分層管理。

五、肖像權:攝影著作與被攝者權益是兩回事

臺灣法制並非以一部「肖像權法」處理所有情況;實務常從民法人格權、侵權行為、隱私及個資等脈絡判斷。司法院公開裁判資料也可見肖像被視為個人外部形象及人格利益。因此,社區網站最安全的做法仍是取得清楚、具體且可證明的同意。

場景風險判斷建議
公開活動全景,人物非主體仍須評估可識別性、活動告知與使用情境入口明確公告,另設不入鏡/識別標示機制
長者單人專訪高度可識別,且包含聲音與人生經歷採訪前說明,發布前校對,取得可證明的授權
兒童活動特寫兒少保護及未來數位足跡風險較高取得適當監護人同意,也用兒童聽得懂的方式詢問其意願
原同意為成果報告,後改作商品廣告用途明顯改變重新取得對應用途的同意,不沿用模糊舊表單
AI把真實長者做成年輕化代言人可能改變形象、語意與商業意涵另行取得明確同意,標示為AI合成,不虛構本人言論

倫理標準可高於最低法律標準:即使某些拍攝可能有法律上的例外或權衡空間,USR合作更重視長期信任;對方不舒服,就應提供不入鏡、匿名、更換照片或下架的可行選項。

六、兒童與長者:同意之外還要避免權力壓力

兒童

適當取得法定代理人/監護人的同意之外,也應向兒童用簡單語言說明;孩子拒絕拍攝、當下不舒服或後來反悔,都要有替代參與方式。

長者

避免因老師、學生或計畫團隊的身分造成「不好意思拒絕」。以口語慢慢說明,不把簽名當成唯一理解證據,必要時讓家屬或信任者協助。

不同意不應失去活動資格或服務。除非影像或資料確為活動不可缺少的必要部分,否則應提供不入鏡貼紙、特定座位、局部拍攝或不公開版本。

七、著作權:買到作品,不等於買到著作權

著作權法保護語文、音樂、美術、攝影、視聽、錄音、電腦程式等著作。把素材放上公開網站通常涉及重製及公開傳輸;修改、翻譯、剪輯或AI轉換還可能涉及改作。

所有權擁有木雕、照片沖印或手稿原件。
著作財產權控制重製、公開傳輸、改作等利用。
著作人格權公開發表、姓名表示與禁止不當歪曲等。
授權權利人允許特定人在約定範圍利用。

常見錯誤:社區買下工藝作品,就以為能掃描、製成Logo、AI改圖及販售周邊。原件所有權與著作權是不同問題,應以契約確認。

八、授權書要說清楚什麼?

欄位應說清楚的內容
素材哪一段故事、哪幾張照片、哪件作品、哪個錄音/影片
權利與行為拍攝、錄音、重製、剪輯、改寫、翻譯、公開傳輸、展示或製作教材
媒介與對象特定網站、社群、簡報、展覽、出版品或合作夥伴
期間與地區何時開始、多久、是否全球網路公開
營利與否僅非營利教育,或包含募款、商品與商業宣傳
姓名表示本名、別名、團體名、匿名,及署名位置
AI利用是否允許轉錄、摘要、翻譯、生成圖、聲音模擬或模型訓練
撤回/下架聯絡窗口、處理期限及已散布素材的實際限制
報酬與回饋是否有費用、成品、署名、收益分享或社區回饋

授權契約應配合實際權利關係與機構規範;上表是教學檢核,不是可直接取代法律審查的定型契約。

九、「教育用途」不等於全部都可以用

著作權法第52條等規定在特定條件下容許為教學、研究等正當目的,在合理範圍引用已公開著作;第64條要求特定利用明示出處,第65條則列出目的與性質、著作性質、利用質量比例、對市場影響等判斷因素。

問題比較安全的判斷方式
只要註明來源就能整張使用?不一定。標示來源不能取代授權,也不能自動成立合理使用。
非營利網站就一定合理使用?不一定。非營利只是考量因素之一,還要看比例、必要性與市場影響。
網路搜尋得到就是公開素材?公開可看不代表自由重製。先查權利人、授權條款或改用合法開放素材。
改顏色或裁切就變成自己的?不一定,仍可能涉及原著作的重製或改作。
引用地方新聞全文可以嗎?應評估必要範圍;優先摘要、短引、清楚標示並連結原文。

十、AI倫理:AI可以協作,但不能代替責任

AI用途主要風險治理做法
整理長者訪談把方言、年代或人物關係整理錯誤保留原始檔,逐段查證,發布前由受訪者或社區代表校對
生成地方故事AI補出不存在的傳說或把推測寫成史實區分史料、口述、推論與創作,不得把生成內容冒充真實見證
製作人物圖片冒用真實人形象、改變年齡或虛構發言取得明確同意,清楚標示AI合成,避免誤導
模擬長者聲音高度人格、詐騙及錯誤歸因風險原則上不做;如有必要,另行明確授權、限制用途及加上顯著揭露
上傳訪談給雲端AI個資、未公開文化知識可能被外部服務處理先去識別化、確認契約與資料政策,未經授權不輸入敏感內容
生成工藝衍生設計掩蓋原作者、近似他作、利益未回到社區保留創作歷程、權利檢查、署名、共同決策與收益/回饋機制

三不一要:不把個資直接丟給AI、不把AI幻覺當史實、不把AI生成冒充長者原話;要保留人類查證、編修、決策與負責的紀錄。

十一、AI生成內容的著作權,不要寫得太肯定

經濟部智慧財產局近年公開說明指出:若AI只是輔助工具,且有人類實際創意投入,相關創作成果部分仍可能受保護;若完全由AI演算獨立完成、沒有人類實際創意投入,則可能無法享有著作權。是否受保護或侵權,仍須依個案事實判斷。

保存人類創作證據

訪談研究、草稿、提示詞演進、選擇與編排、手工修改、事實查核及版本紀錄。

檢查輸出風險

反向搜尋圖像、檢查Logo與角色近似、查音樂與字型授權、避免要求模仿特定在世創作者。

「AI產生」不代表無人有權,也不代表一定能商用。還要確認AI服務條款、輸入素材權利、輸出是否與他人著作實質近似,以及真實人物與商標等其他權利。

十二、地方文化還有「法律之外」的倫理

共同詮釋由社區確認名稱、脈絡與禁忌,不由外部團隊單方面定義。
尊重不公開祭儀、家族故事、位置資訊或技藝祕訣可能不適合全面上網。
利益回饋流量、品牌、商品或研究成果應讓貢獻者及社區看得見回饋。
可持續修正網站應提供更正、補充、下架及版本紀錄,不把首次採集當永久定稿。

地方故事不是免費原料。USR的價值是與社區共同保存與再創造,而不是把故事帶走、由學校署名,再將社區留在網站之外。

十三、建立「紅黃綠」內容分級

等級內容例處理方式
可公開權利清楚的場域介紹、已授權作品、去識別化成果編輯校對後公開,保留來源與授權紀錄
限條件可識別人物、兒童活動、生活經歷、未確認口述史取得具體同意、縮小範圍、校對並設定期限/權限
暫不公開健康、財務、家庭衝突、精確住址、祭儀禁忌、未授權工藝祕訣不放公開網站;必要時受控保存並由專責人員審查

十四、從採集到下架:八道倫理閘門

目的告知同意/授權最小採集共同校對分級發布持續管理更正/下架
  1. 目的:先寫清楚為何需要這份素材,沒有必要就不蒐集。
  2. 告知:用對方聽得懂的方式說明用途、公開程度與AI處理。
  3. 同意/授權:個資、肖像與著作利用分開確認,保存證據。
  4. 最小採集:只取需要內容,原始檔與公開檔分層保存。
  5. 共同校對:人物、年代、地名、方言、署名與文化脈絡由社區確認。
  6. 分級發布:公開、限閱或不公開;設定必要的期限與權限。
  7. 持續管理:盤點授權到期、外部連結、AI標示與存取紀錄。
  8. 更正/下架:提供明確窗口、處理時限、備份與搜尋快取的後續處理。

十五、網站後台也要支援倫理治理

# Django Model概念:不要只存content,還要存治理資訊
class CommunityAsset(models.Model):
    title = models.CharField(max_length=200)
    visibility = models.CharField(max_length=20)  # public / restricted / private
    consent_record = models.FileField(upload_to="consents/", blank=True)
    rights_holder = models.CharField(max_length=200, blank=True)
    license_scope = models.TextField(blank=True)
    expires_at = models.DateTimeField(null=True, blank=True)
    ai_assisted = models.BooleanField(default=False)
    ai_disclosure = models.TextField(blank=True)
    review_status = models.CharField(max_length=20, default="pending")
    takedown_contact = models.EmailField()
    published_at = models.DateTimeField(null=True, blank=True)

授權書本身含有個資,不應放在公開Media URL。應使用受控儲存、權限檢查、加密與存取紀錄;公開頁只顯示必要的授權狀態與署名。

十六、發布頁面的透明標示範例

故事提供|王○○老師(依本人意願部分匿名)
訪談整理|尚虎雲USR學生團隊
影像拍攝|○○○
社區校對|水井村文化工作小組
AI協作說明|AI協助語句整理與版面草擬;
             人名、年代、地方用語及最終內容由團隊與受訪者查核。
授權範圍|本網站非營利教育展示;未經同意不得另行商業利用。
更正/下架|請聯絡:project@example.org
更新日期|2026-08-22

透明標示不是把責任推給AI,而是讓讀者知道內容如何形成、誰查核、可怎麼聯絡,也讓社區貢獻被看見。

十七、學生實作:從「會上傳」走向「負責任發布」

No-AI|權利拆解

選一段社區採訪影片,逐項列出人物、個資、故事、攝影、音樂、工藝與平台權利;任何一項不清楚就不得按發布。

AI Pair|倫理紅隊

提供已去識別化的文章草稿,請AI找風險;學生對照法規與場域倫理,判斷哪些建議正確、過度或不足,完成修訂紀錄。

Challenge AI|治理流程

設計Django素材後台:授權範圍、公開分級、到期提醒、AI揭露、審查、版本與下架流程,並以兒童活動照及長者口述做情境驗收。

十八、發布前60秒檢核

問題通過證據
有明確目的與合法/適當基礎嗎?告知內容、同意或其他依據可被查證
人物真的知道會公開上網嗎?用途、媒介、期間、AI處理與下架方式說明清楚
兒童與長者能自由說不嗎?有替代參與方案,拒絕不影響權益
每個素材都有權利來源嗎?照片、音樂、字型、故事與工藝均有授權或適用依據
只公開必要資料嗎?已移除住址、聯絡資料、EXIF及不必要背景人物
內容經本人/社區校對嗎?人名、年代、方言、文化脈絡與署名已確認
AI角色透明且可追查嗎?有AI協作標示、人工查核與版本紀錄
出錯能更正或下架嗎?公開聯絡窗口、內部處理人與時限

十九、延伸閱讀(臺灣官方來源)

個資法頁面顯示部分修正條文施行日期尚待決定;實作時應確認當下有效條文、所屬機構規範與個案事實。

封包旅行記 EP08|把故事放上網,不只是把檔案公開;而是用技術替每一位說故事的人保留選擇、署名、尊嚴與回家的路。