蝦皮買的 ESP32-CAM 上周到貨, 周末帶回鄉下將接腳焊上, 並把攝影鏡頭 OV2640/OV5640 裝上去, 方法參考之前 ESP32-WROOM-DEV 開發板的筆記 :
然後將 UART 埠連接 PC USB 插槽, 開啟裝置管理員查看 COM 埠編號 (此處為 COM4) :

開啟命令提示字元視窗, 先查詢 Flash 資訊 :
D:\ESP32>esptool --port COM4 flash_id
esptool.py v4.6.2
Serial port COM4
Connecting.............
Detecting chip type... ESP32-S3
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:dd:d4
Uploading stub...
Running stub...
Stub running...
Manufacturer: 68
Device: 4018
Detected flash size: 16MB
Flash type set in eFuse: quad (4 data lines)
Hard resetting via RTS pin...
可見 Flash 確實是 16MB. 用下列指令清除 Flash :
D:\ESP32>esptool --chip esp32s3 --port COM4 erase_flash
esptool.py v4.6.2
Serial port COM4
Connecting..................
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:dd:d4
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 3.6s
Hard resetting via RTS pin...
然後到 cnadler86 的 Camera API (目前社群首推) repo 庫, 下載有 OCTAL 字眼的 S3 韌體, 這款才能使用到 8MB PSRAM (N16R8 中的 8 是指 PSRAM/SPI RAM). 注意, ESP32 和 ESP32-S3 的 CPU 架構不同 (Xtensa LX6 vs LX7), 韌體完全不通用, ESP32-S3 若燒錄 ESP32, 板子可能會完全無法開機, 就算能開機也抓不到那 8MB 的 PSRAM 記憶體.
解開下載的 mpy_cam-v1.27.0-ESP32_GENERIC_S3-SPIRAM_OCT.zip 會得到映像檔 firmware.bin. 檔名中的 mpy_cam 表示內建 camera 模組, GENERIC_S3 表示適用於標準 ESP32-S3 開發板, SPIRAM_OCT 代表支援 Octal SPI RAM, 這正是 N16R8 (R8=8MB Octal RAM) 必須要有的驅動. 沒有 OCTAL 的是一般 SPIRAM (Quad) 的版本, 雖然能開機, 但只會抓到 2MB 或更少的記憶體且速度較慢.
接下來參考之前 ESP32-S3 開發板燒錄韌體指令將此 MicroPython 韌體燒錄到 Flash 中 :
D:\ESP32>esptool --chip esp32s3 --port COM4 write_flash -z 0 firmware.bin
esptool.py v4.6.2
Serial port COM4
Connecting...................
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:dd:d4
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash will be erased from 0x00000000 to 0x001dbfff...
Compressed 1948272 bytes to 1238576...
Wrote 1948272 bytes (1238576 compressed) at 0x00000000 in 108.1 seconds (effective 144.1 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
燒錄成功後在 Thonny 點選 "執行/停止-重新啟動" 出現 "not in bootloader mode" 訊息 :
Device is busy or does not respond. Your options:
- wait until it completes current work;
- use Ctrl+C to interrupt current work;
- reset the device and try again;
- check connection properties;
- make sure the device has suitable MicroPython / CircuitPython / firmware;
- make sure the device is not in bootloader mode.
按 RST 鍵 (左鍵) 出現 "waiting for download" :
ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x1 (POWERON),boot:0x20 (DOWNLOAD(USB/UART0))
waiting for download
我詢問 Gemini, 它說這個訊息的意思是 "我現在處於下載模式 (Download Mode), 因為有人按著我的 BOOT 鍵 (右鍵), 所以我正在等待電腦傳送新的程式給我", 板子根本沒有試著去讀取剛燒進去的 MicroPython, 而是以為還要繼續燒錄, 所以一直停在門口發呆, 這通常是由硬體原因造成的, 最常見的兇手是鏡頭模組, 相機的某些腳位可能與 Strapping Pins (決定開機模式的腳位) 共用, 當插著 OV2640/OV5640 開機時, 鏡頭內部的電路可能會把板子上的 GPIO 0 拉低, 導致晶片誤以為 BOOT 鍵被按著不放.
拔掉電源拆下鏡頭後重新插入 USB 槽, 果然就順利進入 MicroPython 執行環境了 :
MPY: soft reboot
MicroPython v1.27.0-dirty on 2026-01-12; Generic ESP32S3 module with Octal-SPIRAM with ESP32S3
Type "help()" for more information.
>>> import esp
>>> print(f"Flash 大小: {esp.flash_size() / 1024 / 1024:.2f} MB")
Flash 大小: 16.00 MB
然後用下列程式檢查是否可載入 camera 模組, 以及 PSRAM 是否啟用 :
>>> import gc
import camera
import micropython
# 分配記憶體與整理
gc.collect()
print("="*30)
print("🚀 N16R8 Vibe Check")
print("="*30)
# 1. 檢查 RAM (關鍵指標)
# N16R8 成功驅動後,這裡應該要顯示 4MB ~ 8MB 之間的數值
free_ram_mb = gc.mem_free() / 1024 / 1024
print(f"✅ 剩餘 RAM: {free_ram_mb:.2f} MB")
if free_ram_mb > 4:
print(" -> 狀態:完美!Octal PSRAM 已啟用。")
else:
print(" -> 警告:RAM 過少,可能燒錄成 Non-Octal 版本。")
# 2. 檢查 Camera 模組
try:
print(f"✅ Camera 模組版本: {camera}")
print(" -> 驅動載入成功。")
except Exception as e:
print(f"❌ Camera 模組載入失敗: {e}")
print("="*30)
==============================
🚀 N16R8 Vibe Check
==============================
✅ 剩餘 RAM: 7.93 MB
-> 狀態:完美!Octal PSRAM 已啟用。
✅ Camera 模組版本: <module 'camera'>
-> 驅動載入成功。
==============================
可見 8MB 的 PSRAM 已成功啟用, 剩餘 PSRAM 為 7.93MB.
做完上述測試確認可讀到 8MB PSRAM 後拔除 USB, 將鏡頭插回去, 找了一片 8GB TF 卡插入背板的卡槽, 重新插上 USB, 這次居然順利進入 MicroPython 環境, 沒有卡在等待 Bootloader 那邊 (萬一出現此情況, 可以試著多按幾次 RST 鍵, 按一下 RST 等 2 秒再按一下; 或者按住 BOOT 鍵不放, 按一下 RST 鍵然後放開, 繼續按著 BOOT 鍵等待 1-2 秒再放開).
先用 Gemini 建議的程式測試看看能否讀取 TF 卡 :
MicroPython v1.27.0-dirty on 2026-01-12; Generic ESP32S3 module with Octal-SPIRAM with ESP32S3
Type "help()" for more information.
>>> import machine
import os
def mount_sd():
try:
# 針對 Generic ESP32-S3 CAM,通常使用 SDMMC Slot 1
# width=1 (1-bit mode) 比較省腳位且常見,若失敗可改 width=4
# 注意:不同板子的 clk/cmd/d0 腳位可能不同,請查閱您的板子 Schematic
# 以下是常見的 Freenove/Generic S3 CAM 預設腳位:
sd = machine.SDCard(slot=1, width=1,
clk=39, cmd=38, d0=40)
# 若上面的腳位報錯,試試不指定腳位: machine.SDCard(slot=1, width=1)
os.mount(sd, "/sd")
print("✅ SD Card mounted at /sd")
# 檢查容量
fs_stat = os.statvfs('/sd')
free_mb = (fs_stat[0] * fs_stat[3]) / 1024 / 1024
print(f"📦 Free space: {free_mb:.2f} MB")
return True
except Exception as e:
print(f"❌ Failed to mount SD: {e}")
return False
# 測試
mount_sd()
❌ Failed to mount SD: extra keyword arguments given
False
回報給 Gemini, 它說 extra keyword arguments given 意思是 "您給了我一個我不認識的參數名稱", 原因是上面的程式碼中使用了 clk 這個參數名稱, 但在 MicroPython 的標準 machine.SDCard 函式庫中, 時鐘腳位的標準參數名稱通常是 sck (Serial Clock) 而不是 clk, 它建議使用標準 ESP32-S3 的寫法, 將 clk 改為 sck 並明確使用 Pin() 物件, 程式修正如下 :
>>> import machine
import os
from machine import Pin
def mount_sd():
try:
# 修正點:
# 1. clk 改為 sck
# 2. 建議加上 Pin() 包裝 (比較保險)
sd = machine.SDCard(slot=1, width=1,
sck=Pin(39),
cmd=Pin(38),
d0=Pin(40))
os.mount(sd, "/sd")
print("✅ SD Card mounted at /sd")
# 檢查容量
fs_stat = os.statvfs('/sd')
free_mb = (fs_stat[0] * fs_stat[3]) / 1024 / 1024
print(f"📦 Free space: {free_mb:.2f} MB")
return True
except Exception as e:
print(f"❌ Failed to mount SD: {e}")
return False
mount_sd()
❌ Failed to mount SD: extra keyword arguments given
False
結果還是不行, 回報 Gemini, 它說可能是韌體底層把 cmd 和 d0 對應到了 mosi 和 miso 的參數位置 (這是某些 S3 移植版的怪癖), 叫我試試看下面這個版本測試程式 :
>>> import machine
import os
from machine import Pin
def mount_sd():
try:
# 修正點:
# 1. clk 改為 sck
# 2. 建議加上 Pin() 包裝 (比較保險)
sd = machine.SDCard(slot=1, width=1,
sck=Pin(39),
mosi=Pin(38), # 對應 CMD
miso=Pin(40)) # 對應 D0
os.mount(sd, "/sd")
print("✅ SD Card mounted at /sd")
# 檢查容量
fs_stat = os.statvfs('/sd')
free_mb = (fs_stat[0] * fs_stat[3]) / 1024 / 1024
print(f"📦 Free space: {free_mb:.2f} MB")
return True
except Exception as e:
print(f"❌ Failed to mount SD: {e}")
return False
mount_sd()
❌ Failed to mount SD: invalid config: SDMMC slot with SPI pin arguments
False
錯誤訊息變成 invalid config 了, Gemini 綜合上面測試結果, 認為是軟硬體不匹配問題, 目前燒錄的這版 cnadler86 "Generic" 韌體為了維持通用性鎖住了 SDMMC 的腳位設定功能, 它強制使用 ESP32-S3 的官方預設腳位 (CLK=14, CMD=15, D0=2), 不允許透過 Python 程式碼 (clk=39...) 去修改它. 但這塊開發板 (N16R8 CAM) 的硬體線路是 (CLK=39, CMD=38, D0=40), 軟體想走東但硬體在西所以讀不到卡.
Gemini 認為我的板子應該是 Freenove ESP32-S3 開發板的仿板, 建議我改燒錄 Freenove 專為 S3 WROOM (N16R8) 設計的韌體才能解決此問題, 還好這塊開發板我買了三塊, 打算用另一塊來燒錄 Freenove 的韌體, 等測試 OK 再洗掉目前這塊重新燒錄.
2026-02-10 補充 :
此韌體雖然有 camera 模組, 但拍照卻失敗, 檢視開發板的記憶體資訊 :
>>> import micropython
>>> micropython.mem_info()
stack: 736 out of 15360
GC: total: 64000, used: 16752, free: 47248, max new split: 8257536
No. of 1-blocks: 408, 2-blocks: 48, max blk sz: 32, max free sz: 2941
請 Gemini 解析如下 :
1. stack: 736 out of 15360 (堆疊記憶體)
意義 : 這是 C 語言層面的堆疊, 用來處理函數呼叫, 遞迴和局部變數.
解讀 : 總容量約 15 KB (15360 bytes), 目前使用 736 bytes.
結論 : 非常健康, 程式邏輯沒有造成深層遞迴或堆疊溢位 (Stack Overflow).
2. GC: total: 64000, used: 16752, free: 47248 (堆積記憶體 / Heap)
意義 : 這是 MicroPython 用來存放物件 (變數, 字串, 列表, 一般的 Python 物件) 的主要區域.
解讀 : total: 64000 (約 62.5 KB), 這是一個警訊!對於一般的 ESP32 來說這很正常, 但對於號稱 N16R8 (8MB RAM) 的板子來說這個數字太小了. 這代表 MicroPython 的主堆積 (Main Heap) 只被配置在速度最快的 "內部 SRAM", 而沒有直接把那 8MB 的外掛 RAM (SPIRAM) 納入這個主要的管理池. free: 47248 (約 46 KB), 現在只剩下 46KB 的空間可以用來建立 Python 物件. 這讓相機拍照容易失敗, 一張 VGA (640x480) 的 JPEG 照片通常在 30KB~50KB 之間, 如果要把照片讀入變數 (例如 buf = cam.capture()), 這個變數可能會瞬間佔滿剩下的所有空間甚至塞不下.
3. max new split: 8257536 (最大可分割區塊) :
意義 : 這是最大的亮點!
解讀 : 8,257,536 bytes (約 7.8 MB) 表示板子確實有 8MB 的 SPIRAM (R8), 而且 MicroPython 抓到了它. 雖然主堆積 (GC total) 只有 64KB, 但 MicroPython 知道旁邊還有一個巨大的 8MB 倉庫, 當需要分配超大記憶體 (例如相機的 Frame Buffer) 時, 系統會嘗試去切這塊 8MB 的蛋糕.
4. max free sz: 2941 (最大連續可用空間)
意義 : 這代表在那個 64KB 的主堆積中最大的一塊 "連續空地" 有多大.
解讀 : 2941 bytes (約 2.8 KB) 是致命傷, 雖然總共有 46KB 空閒 (free), 但因為記憶體碎片化 (Fragmentation), 最大的連續空間只有 2.8 KB. 後果是如果在程式中做字串相加或建立一個稍微大一點的陣列 (超過 2.9KB), 程式就會報錯 (MemoryError), 即使總空間還夠.
從以上記憶體資訊來看, ESP32-S3 硬體是強大的, 有 8MB SPIRAM (max new split 證實了這點), 但軟體配置是保守的, 主堆積被限制在 64KB 內部 RAM, 導致一般 Python 操作很擁擠, 但相機運作時, 相機的驅動 (camera.capture()) 會聰明地使用了那 8MB 的 SPIRAM 來存原始數據, 所以能拍到照片. 但當 Python 嘗試處理這張照片 (例如存檔) 時會遇到瓶頸, 如果操作不當, 試圖把數據搬回狹小的 64KB 主堆積會導致溢位或失敗.
Gemini 的總評是 : ESP32-S3 CAM 擁有巨大的財富 (8MB PSRAM), 但皮夾 (Heap) 卻很小 (64KB) 而且零錢很亂 (Fragmentation). 解決這個問題需要改變軟體使用記憶體的方式 :
1. 強制執行垃圾回收 :
在每次耗費記憶體的大動作例如拍照或存檔前, 手動呼叫垃圾車 :
import gc
# 1. 拍照前清理:確保有最大的連續空間給相機緩衝區
gc.collect()
buf = cam.capture()
# 2. 存檔前清理:確保寫入檔案時的緩衝區足夠
gc.collect()
with open("image.jpg", "wb") as f:
f.write(buf)
# 3. 用完馬上丟:切斷變數參照,再次清理
buf = None
gc.collect()
此處 gc.collect() 會把散落在記憶體裡的碎片整合在一起, 把小的 free 區塊合併成大的區塊以便能塞進更大的資料.
2. 程式碼優化 :
避免 "中間人" 與字串相加, 不要直接用 print(buf) 印出內容或用 str(buf) 把緩衝區資料 buf (在 PSRAM) 轉成字串 (在 Heap), 這樣會瞬間吃掉幾十 KB, 正確做法例如 :
# 直接檢查數據長度,不要印出內容
print(f"Size: {len(buf)}")
# 使用 f-string (Python 3.6+) 效率較好,或預先定義好檔名
filename = f"img_{count}.jpg"
3. 利用 bytearray 佔領 PSRAM :
MicroPython 通常設定小物件放內部 SRAM (速度快但只有 64KB); 大物件放外部 PSRAM (速度慢但有 8MB 大容量), 如果需要緩衝區儲存大資料, 不要一點一點 append, 應該用 bytearray() 直接要一塊大的 (例如 100KB), 強迫 MicroPython 把資料丟到 PSRAM 儲存 (系統會因為內部 Heap 放不下自動把它分配到那一塊 8MB 的 PSRAM 裡), 例如 :
large_buffer = bytearray(100 * 1024)
事實上 camera.capture() 內部已經是這樣做了 (它回傳的那個 bytes 物件通常就直接指在 PSRAM 上), 但關鍵在於千萬不要把它複製到內部 Heap.
4. 改用 SPIRAM 專用韌體 :
如果上面三招都無法解決, 還是經常遇到 MemoryError, 那可能是因為目前使用的 MicroPython 韌體採取了分離式記憶體管理方法 (SRAM 當 Heap, PSRAM 當倉庫), 這時可以改用 "全域 SPIRAM (Generic SPIRAM) " 編譯設定的韌體, 它會直接把那 8MB 的 PSRAM 全部當作 Python 的 Heap 來用. 這種針對記憶體配置的韌體版本檔名中帶有 "SPIRAM", 且描述中有 "Unified" 或 "Board Variant: SPIRAM" (不是 OCT).