顯示具有 MP3 標籤的文章。 顯示所有文章
顯示具有 MP3 標籤的文章。 顯示所有文章

2026年8月23日 星期日

[水井村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對話建立基礎。