2026年10月19日 星期一

[DIY專案] 4x4x4 LED 立方體:用 Arduino Mega2560 + 麥克風 + 光感做出「會呼吸的音箱」

📺 影片版(約 5 分鐘):想先看實際操作的話可以直接看這支影片,完整步驟、設定與程式碼在下方文字版。
YouTube:https://youtu.be/id-d62W7q2Y(winson-DIY 頻道)

這一個 DIY 專案是「4×4×4 64 顆 LED 的互動式立方體」:用 Arduino Mega2560 做控制核心,外接麥克風模組與光感(LDR),讓立體光譜「聽得到、看得見」——音量越大,底層越亮;環境越暗,頂層抬頭;中層隨時間流動,像一顆會呼吸、會應答的水晶球。


為什麼要「多工」而不能直接 64 腳

64 顆 LED 若各自一個 IO,需要 64 腳——任何 Arduino 都給不起。解法是矩陣掃描(multiplexing):陣列在製作時把垂直方向的 4 顆 LED 陽極焊成一根「柱」(共 16 根),再把同一層 16 顆 LED 的陰極焊在一起成為「層」(共 4 層)。任一瞬間只導通一層,同時送出這一層 16 根柱子該亮的狀態;4 層輪流每秒掃幾百遍,人眼殘像就讀成「64 顆同時亮」——和螢幕的「逐行刷新」是同一個道理。

這樣只需要 16 柱 + 4 層 = 20 個數位腳位,而且每顆 LED 有 1/4 的時間在亮。Mega2560 有 54 個數位腳,從 D22 開始用,刻意避開 D0/D1(那是 USB Serial 的 RX/TX,接 LED 會干擾 Serial 和上傳程式)。


料件清單

料件 數量/規格 說明
4x4x4 LED 陣列 64 顆 3mm LED(本例清/藍)、16+4 pin 排針 陽極每 4 顆串成一柱(16 柱)、陰極每層共接(4 層)
Arduino Mega2560 1 片 用 20 個數位腳位:16 柱(D22~D37)+ 4 層(D38~D41)
聲音感測器(咪頭模組) 1 個(電子元件店的藍板電容咪) 接 A0,類比值隨聲音大小跳動
光感 sensor(LDR) 1 個(10kΩ 光電分壓) 接 A1,亮度 → 電阻 → 模擬值
ULN2003(或 P-MOS 陣列) 1 片 層(陰極)一次要吸 16 顆的電流,IO 直接拉不動;ULN2003 每通道可吸 500mA
電阻 220~330Ω × 16 柱(陽極)限流,每柱串一顆(1kΩ 會偏暗;依 LED 規格調整)
杜邦線 / 麵包板或 PCB — 引腳 20+2,麵包板要夠大
5V 供電 USB 即可 一次只亮一層,最多 16 顆 × 約 10mA ≈ 160mA;要調大電流更亮時再用獨立 5V

接線總覽

物件 目標
16 根柱子(陽極) 各串 220~330Ω → Mega D22~D37
4 層(陰極) → ULN2003 OUT1~OUT4;IN1~IN4 ← Mega D38~D41;ULN2003 GND 接共地
咪頭模組 VCC/GND/OUT 5V / GND / A0
LDR 分壓(10kΩ 下拉) 節點 → A1
GND 全部共地(最重要,浮地 = 亂亮)

控制端程式(重點:掃描)

核心邏輯三件事:(1) 感測 每 30ms 讀一次咪頭/光感,把「音量→底 2 層,暗→頂層」寫進 cube[4][4][4] 模型;(2) 掃描 一次導通一層,送出該層 16 根柱子的狀態,4 層輪巡;(3) 每層停留 約 500µs,4 層一輪 2ms ≈ 500fps,每顆 LED 的亮度 ≈ 直驅的 1/4,所以限流電阻比一般直驅稍小——這是多工的公定代價:

/*
 * 4x4x4 LED 立方體 — 聲音+光度互動版
 * 硬體: Arduino Mega2560
 *   16 根柱子(陽極) → D22..D37 (各串 220~330Ω 限流)
 *   4 層(陰極)      → ULN2003 OUT1..4,由 D38..D41 控制
 *   麥克風模組 AIN  → A0 (類比)
 *   光感 (LDR 分壓) → A1
 *   D0/D1 保留給 Serial,不接 LED
 */

const uint8_t COL_PIN[16]  = {22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37};
const uint8_t LAYER_PIN[4] = {38,39,40,41};   // HIGH = ULN2003 導通 = 該層接地

// cube[x][y][z]:x、y 決定哪一根柱子(柱 = x*4+y),z 是層(0 在最下面)
bool cube[4][4][4] = {0};
uint8_t layer = 0;

void setup() {
  for (uint8_t i = 0; i < 16; i++) { pinMode(COL_PIN[i], OUTPUT);   digitalWrite(COL_PIN[i], LOW); }
  for (uint8_t i = 0; i < 4;  i++) { pinMode(LAYER_PIN[i], OUTPUT); digitalWrite(LAYER_PIN[i], LOW); }
  Serial.begin(115200);
}

void loop() {
  /* ---------- 1. 讀感測 → 更新 cube ---------- */
  static uint32_t lastSense = 0;
  uint32_t now = millis();
  if (now - lastSense > 30) {          // 30ms 感測一次
    lastSense = now;

    int16_t mic  = analogReadA0avg(20);   // 平均20次降噪
    int16_t light= analogReadA1avg(20);
    uint16_t micDB   = map(mic,   200, 800, 0, 1023);  // 依你的音量範圍校正
    uint16_t darkPct = map(light, 1023, 0, 0, 1023);   // 越暗越大

    /* 規則: 底層=音量, 頂層=光線, 中層=呼吸 */
    uint8_t vol  = (uint8_t)(micDB   / 1024.0 * 4);      // 0..3(音量4層)
    uint8_t lig  = (uint8_t)(darkPct/ 1024.0 * 4);       // 0..3
    uint32_t t = now / 200;                                // 呼吸節拍
    for (uint8_t x = 0; x < 4; x++)
      for (uint8_t y = 0; y < 4; y++)
        for (uint8_t z = 0; z < 4; z++) {
          if      (z < 2) cube[x][y][z] = (z < vol);                          // 音量填底2層
          else if (z == 3) cube[x][y][z] = (lig <= 1);                        // 暗 → 頂層亮
          else            cube[x][y][z] = ((t + x + y) % 4 == 0);             // 中層流動
        }
  }

  /* ---------- 2. 掃描:一次一層,4 層輪流,人眼殘像為「64 顆同時亮」 ---------- */
  digitalWrite(LAYER_PIN[layer], LOW);                   // 先關掉上一層,避免換資料時殘影
  layer = (layer + 1) & 3;
  for (uint8_t c = 0; c < 16; c++) {
    uint8_t x = c >> 2, y = c & 3;
    digitalWrite(COL_PIN[c], cube[x][y][layer] ? HIGH : LOW);   // 柱子拉高 = 供電
  }
  digitalWrite(LAYER_PIN[layer], HIGH);                  // 打開這一層
  delayMicroseconds(500);                                // 4 × 0.5ms = 2ms/幀 → ~500 fps
}

/* 20 次類比取平均, 擋住模組的抖動 */
int16_t analogReadA0avg(uint8_t n){ long s=0; for(uint8_t i=0;i<n;i++){s+=analogRead(A0); delayMicroseconds(60);} return s/n; }
int16_t analogReadA1avg(uint8_t n){ long s=0; for(uint8_t i=0;i<n;i++){s+=analogRead(A1); delayMicroseconds(60);} return s/n; }

調試時最常遇到的三個坑

  • 浮地=亂亮:陣列、ULN2003、感測模組的 GND 沒有共地,會看到「隨機一兩顆閃」——不是程式錯,是地線錯。共地是最優先的一條線。
  • 太暗或某層特別暗:先看限流電阻是不是太大(1kΩ 在 1/4 佔空比下會偏暗,改 220~330Ω);某一層特別暗通常是 ULN2003 那一路沒接好或 GND 沒共到。
  • 掃描太慢,閃爍肉眼可見:4 層一輪的總時間 > 20ms 就會看到「一層一層跑」。每層停留 300~800µs 之間都很穩;另外感測那段每次會卡住約 5ms,讀太頻繁也會讓畫面微閃。

玩法建議(感測→效果)

  • 音量柱:底層亮 = 音量高;拍手/說話 = 立方體「長高」,最直觀的音頻視覺化。
  • 暗室模式:LDR 越暗,頂層越亮 + 掃描頻率越慢,變成一盞「夜燈」;手遮光 → 瞬間熄滅。
  • 風暴模式:音量 > 閾值 → 隨機閃爍 2 秒(類似警報燈),閾值在 map() 裡調。
  • 呼吸模式:中層隨 sin(t) 慢速起伏,不用感測也好看,做「常亮桌面小物」用。

結語

這個專案把「多工、電流、時序」三個電子學核心一次走完:64 顆 LED 靠「快」而不是「多」跑起來——人眼看不見的是 500fps 的掃描,感覺到的是 64 顆同時亮。做完它,LED 矩陣、OLED、甚至 VGA framebuffer 的「逐行刷新」就全部通了。

更新紀錄

2026-10-28:修正接線架構。初版寫成「16 行 + 16 列、32 腳」,這和 64 顆 LED 對不起來(16×16 是 256 個交點),且用到了 Serial 專用的 D0/D1。現改為標準的「16 柱 + 4 層、20 腳」接法(D22~D41),程式、料件、電流估算與調試建議一併更新。



延伸閱讀

沒有留言:

張貼留言