2024年11月15日 星期五

MicroPython 學習筆記 : ESP32-WROVER-DEV 開發板測試 (一)

我十月底於露天買了一塊類似 ESP32-CAM 的開發板 ESP32-WROVER-DEV, 參考 : 


此開發板上有一個 OV2640 攝像頭插槽, 將上面的黑色長條往上撥變成垂直會露出底下的插槽 : 




將隨附的 OV2640 攝像頭線排放入插槽內 (要塞到底) : 




將黑色長條桿往下壓回, 扣住攝像頭的線排即完成攝像頭安裝 :




剪一小片雙面膠將鏡頭固定在金屬殼上以免晃動. 

接下來是燒錄韌體, 但不是下載 MicroPython 官網的 ESP32 韌體, 而是參考下面 Yesa 這篇踩坑文章所使用的韌體 :


韌體下載網址如下 :


我有複製一份放在我的 GitHub :



1. 燒錄韌體 :

韌體燒錄程序參考下面這篇 :


檢視 Flash 記憶體 :

D:\ESP32>esptool --port COM6 flash_id    
esptool.py v4.6.2
Serial port COM6
Connecting....
Detecting chip type... Unsupported detection protocol, switching and trying again...
Connecting....
Detecting chip type... ESP32
Chip is ESP32-D0WDQ6 (revision v1.0)
Features: WiFi, BT, Dual Core, VRef calibration in efuse, BLK3 partially reserved, Coding Scheme 3/4
Crystal is 40MHz
MAC: 30:ae:a4:62:cc:c4
Uploading stub...
Running stub...
Stub running...
Manufacturer: c8
Device: 6016
Detected flash size: 4MB
Hard resetting via RTS pin...

可見此板有 4MB Flash 記憶體. 在燒錄之前先將 Flash 舊內容抹除 :

D:\ESP32>esptool --chip esp32 --port COM6 erase_flash   
esptool.py v4.6.2
Serial port COM6
Connecting....
Chip is ESP32-D0WDQ6 (revision v1.0)
Features: WiFi, BT, Dual Core, VRef calibration in efuse, BLK3 partially reserved, Coding Scheme 3/4
Crystal is 40MHz
MAC: 30:ae:a4:62:cc:c4
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 7.5s
Hard resetting via RTS pin...

然後燒錄韌體 : 

D:\ESP32>esptool --chip esp32 --port COM6 write_flash -z 0x1000 micropython_v1.21.0_camera_no_ble.bin     
esptool.py v4.6.2
Serial port COM6
Connecting....
Chip is ESP32-D0WDQ6 (revision v1.0)
Features: WiFi, BT, Dual Core, VRef calibration in efuse, BLK3 partially reserved, Coding Scheme 3/4
Crystal is 40MHz
MAC: 30:ae:a4:62:cc:c4
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash will be erased from 0x00001000 to 0x0015efff...
Compressed 1433072 bytes to 926248...
Wrote 1433072 bytes (926248 compressed) at 0x00001000 in 81.5 seconds (effective 140.7 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...


2. 檢視記憶體 :

按 Reset 鈕硬啟動可知此韌體是在 v1.21 版原始碼基礎上編譯來的 : 

MicroPython v1.21.0 on 2023-10-13; ESP32 module with Camera with ESP32
Type "help()" for more information.

先用 gc.mem_free() 檢視可用的 heap 記憶體容量 (包含內部 SRAM 與外部 PSRAM 總和) :

>>> import gc  
>>> gc.mem_free()   
4116880

有超過 4MB 的 heap 記憶體可用, 推測此開發板採用 ESP32-WROVER N4R4 模組. 

micropython.mem_info() 提供更詳細的記憶體分配 (區塊與堆疊) 與垃圾回收資訊 : 

>>> import micropython   
>>> micropython.mem_info()     
stack: 736 out of 15360
GC: total: 64000, used: 10800, free: 53200, max new split: 4063232  
 No. of 1-blocks: 236, 2-blocks: 31, max blk sz: 32, max free sz: 3313

其中 GC 的可用記憶體 53200 表示 SRAM 中剩餘的 heap 記憶體容量, 而 max new split 則顯示 PSRAM 有 4MB 可用. 


2. 檢視 camera 的函式與其參數 :

此韌體的主角是內建的 camera 模組, 匯入後用 dir() 檢視其內容 :

>>> import camera   
>>> dir(camera)   
['__class__', '__name__', '__dict__', 'DRAM', 'EFFECT_BLUE', 'EFFECT_BW', 'EFFECT_GREEN', 'EFFECT_NEG', 'EFFECT_NONE', 'EFFECT_RED', 'EFFECT_RETRO', 'FRAME_240X240', 'FRAME_96X96', 'FRAME_CIF', 'FRAME_FHD', 'FRAME_HD', 'FRAME_HQVGA', 'FRAME_HVGA', 'FRAME_P_3MP', 'FRAME_P_FHD', 'FRAME_P_HD', 'FRAME_QCIF', 'FRAME_QHD', 'FRAME_QQVGA', 'FRAME_QSXGA', 'FRAME_QVGA', 'FRAME_QXGA', 'FRAME_SVGA', 'FRAME_SXGA', 'FRAME_UXGA', 'FRAME_VGA', 'FRAME_WQXGA', 'FRAME_XGA', 'GRAYSCALE', 'JPEG', 'PSRAM', 'RGB565', 'WB_CLOUDY', 'WB_HOME', 'WB_NONE', 'WB_OFFICE', 'WB_SUNNY', 'XCLK_10MHz', 'XCLK_20MHz', 'YUV422', 'brightness', 'capture', 'contrast', 'deinit', 'flip', 'framesize', 'init', 'mirror', 'quality', 'saturation', 'speffect', 'whitebalance']

其中大寫的是呼叫設定函式時會用到的常數, 小寫的則是函式, 摘要如下表 :


 camera 的函式 說明
 init() 傳入接腳, 時脈頻率, 影像格式與解析度等參數以初始化攝像頭, 成功傳回 True
 deinit() 釋放攝像頭所佔資源
 framesize() 攝硬視訊框尺寸, 例如 FRAME_QVGA (320*240), FRAME_VGA (640x480)等
 flip() 設定是否要垂直翻轉影像 (True/False)
 mirror() 設定是否要水平翻轉影像 (True/False)
 speffect() 設定特效例如負片, 濾鏡等 
 whitebalance() 設定白平衡
 saturation() 設定色彩飽和度 , 參數範圍 [-2, 2] 預設為 0, 負值偏灰, 正值偏彩
 brightness() 設定亮度, 參數範圍 [-2, 2] 預設為 0, 設為 2 最亮, -2 最暗
 contrast() 設定對比度, 參數範圍 [-2, 2] 預設為 0, 設為 2 對比最高, -2 最低
 quality() 設定影像品質, 參數範圍 [10, 63] 預設為 12, 值越大品質越高
 capture() 擷取攝像頭影像並傳回 bytes 資料 (無傳入參數)


我將上面的兩個參考資料網址貼給 ChatGPT, 然後詢問它這些函式有哪些參數, 其意義與用法如何? 結果整理如下 : 

init() 函式用來初始化鏡頭, 成功會傳回 True, 否則 False, 其傳入參數較多, 有些有預設值如果接受不用刻意傳入, 但有些參數無預設值則必須在呼叫時傳入 (例如 d0~d7, xclk, pclk, sioc, siod, vsync, href 等 GPIO 接腳編號), 否則初始化不會成功, 說明如下 :
  • sensor_id : 攝像頭 id 編號, 一般開發板只有一個鏡頭, 預設是 0. 
  • reset : 重設腳編號, 大部分攝像頭初始化不需要重設, 傳入 -1 即可.
  • pwdn : 省電模式接腳編號, 此腳設為 High 攝像頭進入省電模式, 不使用傳入 -1 即可.
  • xclk : 主時脈 (20MHz) 接腳編號.
  • sioc 與 siod : 攝像頭的 I2C 通訊接腳編號, siod 為資料線 (SDA), sioc 為時脈線 (SCLK).
  • d0~d7 : 將影像傳給 ESP32 的資料接腳編號.
  • vsync : 影像垂直同步信號接腳編號.
  • href : 影像水平同步參考信號接腳編號.
  • pclk : 像素時鐘接腳編號, 此腳有變化時 ESP32 才會讀取像素資料. 
  • fb_location :  設定影像緩衝器的儲存位置, 可選 PSRAM (預設)/DRAM. 
  • format : 圖像輸出格式, 可選 JPEG (預設)/RGB565/GRAYSCALE. 
  • jpeg_quality : JPEG 品質, 值範圍 1~63 (預設 10), 值越小品質越高. 
  • xclk_freq : 主時脈頻率, 可選 XCLK_10MHz/XCLK_20MHz. 
其中 format 參數用來設定影像輸出格式, 有三個選項 :
  • JPEG : 有損壓縮但體積小, 適合無須再處理要直接上傳到伺服器場合, 值為 4 (預設)
  • RGB565 : 未壓縮的原始峨格式, 適合需要再處理場合, 值為 0 
  • GRAYSCALE : 以灰階格式儲存檔案小, 無彩色, 值為 3
fb_location 參數用來設定影像緩衝器的儲存位置, 有兩個選項 :
  • PSRAM : 即 SPI RAM, 容量較大 (WROVER 有 4M/8M), 值為 0  (預設)
  • DRAM : 容量有限 (ESP32 為 512KB SRAM), 解析度要小才行, 值為 1
注意, 除了 sensor_id, reset, pwdn, fb_location, format, jpeg_quality 這六個有預設值的參數外, 其於參數都必須刻意傳入, 否則初始化會失敗. 

其它函式的參數都只有一個, 說明如下 : 

framesize() 函式可傳入如下值來設定擷取的影像解析度 :
  • FRAME_QQVGA (160x120) : 值為 1
  • FRAME_QVGA (320x240) : 值為 5 (預設)
  • FRAME_VGA (640x480) : 值為 8
  • FRAME_XGA (1024x768) : 值為 10
  • FRAME_UXGA (1600x1200) : 值為 13
speffect() 可傳入下列值設定特效 :
  • EFFECT_NONE (無特效) : 值為 0
  • EFFECT_NEG (負片效果) : 值為 1
  • EFFECT_BW (黑白效果) : 值為 2
  • EFFECT_RED (紅色濾鏡) : 值為 3
  • EFFECT_GREEN (綠色濾鏡) : 值為 4
  • EFFECT_BLUE (藍色濾鏡) :值為 5
  • EFFECT_RETRO (復古效果) : 值為 6
whitelance() 可傳入下列值設定影像的白平衡模式 :
  • WB_NONE (自動白平衡) : 值為 0
  • WB_SUNNY (晴天) : 值為 1
  • WB_CLOUDY (陰天) : 值為 2
  • WB_OFFICE (辦公室燈光) : 值為 3
  • WB_HOME (家中燈光) : 值為 4
參考 :


3. 擷取影像 :

使用 camera 模組擷取影像之前須先呼叫 init() 函式並傳入無預設值的參數 (例如  d0~d7 等 GPIO 接腳編號) 來初始化攝像頭, 這樣才會初始化成功. 例如只傳入 format 與 fb_location 這兩個參數初始化會失敗 (傳入的都是預設值) :

>>> camera.init(0, format=camera.JPEG, fb_location=camera.PSRAM)  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: Camera Init Failed

傳入參數不正確也無法初始化成功, 例如 :

>>> camera.init(0, d0=32, d1=35, d2=34, d3=5, d4=39, d5=18, d6=36, d7=19,
            format=camera.JPEG, framesize=camera.FRAME_VGA, xclk_freq=camera.XCLK_10MHz,
            href=26, vsync=25, reset=15, sioc=23, siod=22, xclk=27, pclk=21, fb_location=camera.PSRAM)    
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
OSError: Camera Init Failed

使用 Yesa 的參數可成功初始化傳回 True :

>>> camera.init(0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
            format=camera.JPEG, framesize=camera.FRAME_VGA, xclk_freq=camera.XCLK_10MHz,
            href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21, pclk=22, fb_location=camera.PSRAM)   
True   

傳回 True 表示已成功初始化鏡頭, 接下來只要呼叫 camera.capture() 函式就可以擷取鏡頭的影像, 它會傳回一個 bytes 型態的影像資料 :

>>> buf=camera.capture()   
>>> type(buf)    
<class 'bytes'>   

內容節錄如下 :

>>> buf   
b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00\x00\x00\x00\x00\x00\xff\xdb\x00C\x00\x0c\x08\t\x0b\t\x08\x0c\x0b\n\x0b\x0e\r\x0c\x0e\x12\x1e\x14\x12\x11\x11\x12%\x1a\x1c\x16\x1e,&.-+&*)06E;03A4)*<R=AGJMNM/:U[TKZELMJ\xff\xdb\x00C\x01\r\x0e\x0e\x12\x10\x12#\x14\x14#J2*2JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ\xff\xc4\x00 .....

用 with open 語法將此影像的 bytes 資料寫入檔案 : 

>>> with open('/capture.jpg', 'wb') as f:    
    f.write(buf)   
    
14631

這樣影像就存入開發板根目錄下了, 但是 Thonny 左下方的開發板檔案總管不會即時更新檔案列表, 因此看不到此圖檔名稱, 按右鍵也沒有 refresh 選項可手動刷新檔案列表, 解決辦法是從左上方本機檔案總管中隨便上傳一個檔案 (例如 config.py), 或者刪除左下角開發板上不要的檔案亦可 (例如前次擷取的圖檔), 這樣就會刷新左下方開發板的檔案列表了 :




點選該圖檔按滑鼠右鍵 "點選下載到 xxx" 即可在本機瀏覽擷取的圖檔了 : 




可見確實是 640*480 解析度的 VGA 圖檔. 

擷取影像後要呼叫 deinit() 釋放此次擷取所佔用的記憶體等資源, 才能進行下一次擷取 :

>>> camera.deinit()    
True

傳回 True 表示釋放資源成功. 注意, 如果沒有呼叫 deinit() 釋放資源而繼續呼叫 capture(), 則 buf 裡面的影像資料不會被更新, 存檔後還是之前的舊影像> 

初始化參數也可以先存在字典中, 然後用 ** 關鍵字引數傳入 init(), 例如 : 

>>> camera_config={
    'xclk': 21,
    'siod': 26,
    'sioc': 27,
    'd7': 35,
    'd6': 34,
    'd5': 39,
    'd4': 36,
    'd3': 19,
    'd2': 18,
    'd1': 5,
    'd0': 4,
    'vsync': 25,
    'href': 23,
    'pclk': 22
    }
>>> camera.init(0, **camera_config)   
True

挪動鏡頭擷取新影像 :

>>> buf=camera.capture()   
>>> with open("/picture2.jpg", "wb") as f:
    f.write(buf)
    
13012
>>> camera.deinit()  
True

結果如下 :




總之, 擷取影像就這四步驟 :

1. camera.init(**config) 初始化
2. buf=camera.capture() 擷取
3. with open("/picture.jpg", "wb") as f:  # 存檔
        f.write(buf)
4. camera.deinit()

我將原作的函式修改如下 : 

import time, camera
from machine import reset

def init_camera(**config): # 初始化鏡頭
    camera.init(
        0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
        href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21,
        pclk=22, fb_location=camera.PSRAM, format=camera.JPEG, 
        xclk_freq=camera.XCLK_10MHz, framesize=camera.FRAME_QVGA,
        **config)
    
def capture_image(file_name='capture.jpg'): # 拍攝照片並存檔 
    time.sleep(2)    # 等待攝像頭穩定
    buf=camera.capture()
    if buf:
        with open(f'/{file_name}', 'wb') as f:
            f.write(buf)
        print(f'Image has been saved as {file_name}')
    else:
        print('Failed to capture image')
    camera.deinit()
    del buf

測試結果如下 :

>>> import time, camera   
>>> from machine import reset    
>>> def init_camera(**config): # 初始化鏡頭
    camera.init(
        0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
        href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21,
        pclk=22, fb_location=camera.PSRAM, format=camera.JPEG, 
        xclk_freq=camera.XCLK_10MHz, framesize=camera.FRAME_VGA,
        **config)
>>> def capture_image(file_name='capture.jpg'): # 拍攝照片並存檔 
    time.sleep(2)    # 等待攝像頭穩定
    buf=camera.capture()
    if buf:
        with open(f'/{file_name}', 'wb') as f:
            f.write(buf)
        print(f'Image has been saved as {file_name}')
    else:
        print('Failed to capture image')
    camera.deinit()
    del buf
>>> init_camera()     
True
>>> capture_image(file_name='capture.jpg')      
7694
Image has been saved as capture.jpg
True

但是再次拍照時 camera.capture() 卻傳回 "cam_hal: EV-EOF-OVF" 訊息 : 

>>> init_camera()      
True
>>> capture_image(file_name='capture1.jpg')      
cam_hal: EV-EOF-OVF   
16796
Image has been saved as capture1.jpg 
True
>>> init_camera()   
True

繼續拍照還出現 "am_hal: EV-VSYNC-OVF" 訊息 : 

>>> capture_image(file_name='capture2.jpg')     
cam_hal: EV-EOF-OVF
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
16211
Image has been saved as capture2.jpg
True

雖然似乎也不影響影響擷取, 但我還是拿去問 ChatGPT, 它指出 EV-EOF-OVF 與影像緩衝區的溢位有關 (OVF=OVer Flow), 原因是影像解析度較高或影像頻率過高, 導致緩衝區無法及時處理數據而造成數據溢位. EV-VSYNC-OVF 則是在標記新影像幀開始的垂直同步 (VSYNC) 事件時發生溢位, 原因與影像的幀速率, 緩衝空間不足或時鐘設置不匹配有關.

我已經將主時脈頻率設為較穩定較低頻的 10MHz, 剩下只有 framesize 可調, 於是在初始化後先呼叫 camera.framesize() 將影像解析度設為 QVGA (320*240) : 

>>> init_camera()      
True   
>>> camera.framesize(camera.FRAME_QVGA)      
>>> capture_image(file_name='capture4.jpg')      
25725
Image has been saved as capture4.jpg
True

果然這樣就沒出現溢位訊息了, 所以最好將 init_camera() 函式中的 framesize 參數改成 camera.FRAME_QVGA, 這樣就不需要呼叫 framesize() 函式了 : 

def init_camera(**config): # 初始化鏡頭
    camera.init(
        0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
        href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21,
        pclk=22, fb_location=camera.PSRAM, format=camera.JPEG, 
        xclk_freq=camera.XCLK_10MHz, framesize=camera.FRAME_QVGA,
        **config)

不過, 經過多次測試發現, 即使改為 QVGA, 兩三次之後還是會出現溢位現象. 既然不影響影像擷取就不管它了.  

沒有留言:

張貼留言