2024年11月19日 星期二

製作鋰聚合物電池包充電器

前陣子在露天買了兩個 JST PH 2.0mm 接頭的鋰電池包 (因為 LOLIN D32 開發板的電池插槽是 PH 2.0mm 母座), 參考 : 


但是要怎麼給電池包充電呢? 市面上並無現成的充電器可買, 只好自己動手製作了. 我在零件櫃翻出很久之前買的小型雙面洞洞板, 尺寸 3cm*7cm 中間鋸一半剛好可以用來製作兩個充電器 :




目前需要為兩種 LiPo 包的接頭製作充電器, 一是 LOLIN D32 開發板的 JST PH2 鋰電池插頭; 二是之前買的三架固定翼與一架四軸遙控飛機使用的扁平接頭 (我在長明街的大林電子有找到公插頭與母座). 

先量好充電模組 TP4060 在洞洞板上的位置, 插上 JST PH2 母座與針腳, 用快乾劑固定後就可以進行焊接, 最後將航模 LiPo 的扁平充電母座線頭也焊在 BAT 側就完成了 : 





插上航模電池包測量充電電流 :




改插 LOLIN D32 的 JST PH2 電池包測量 : 




TP4060 在充電時板上 LED 亮紅色, 充飽後亮藍色. 充電電流會隨著逐漸充飽而遞減.

露天購買機械堂五檔變速無線小電鑽鐵盒全套組

11/11購物節時在 momo 買了四本書, 另外在 momo 店+ 買了一組機械堂五檔變速無線小電鑽 485 元, 參考 :


但結帳時沒發現這項並未加入總價中, 也沒出現在訂購單商品項目裡, 我以為那是店+廠商自行送貨. 但所有貨上周末都已收到了, 唯獨小電鑽沒有, 回去點店+商品頁也不見了, 真是太奇怪了, 總之結論是小電鑽根本沒買. 今天上露天找到一模一樣的, 還免運費更便宜 : 


 



全家取貨免運 435 元. 

2024年11月18日 星期一

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

今天繼續測試 ESP32-WROVER-DEV 開發板應用, 本篇要利用一個 PIR 紅外線感測器來觸發 OV2640 來拍照, 這樣當 PIR 感測到人或動物的體溫發射的紅外線時就會觸發影像擷取動作. 


關於 PIR 模組特性參考好久之前玩 Arduino 時整理的筆記 :
摘要如下 :
  • PIR=Passive InRared Sensor (被動人體紅外線感測器)
  • 紅外線波長 780nm (近紅外線, 無輻射熱) ~ 100um (遠紅外線, 有輻射熱)
  • 人或動物常溫下釋放的紅外線波長約為 10um
  • PIR 偵測四周紅外線能量分布, 將其分成兩部分來比較能量分布是否平衡, 偵測到能量失衡即表示有在移動而發出觸發信號, 但 PIR 只能偵測熱輻射源是否有在移動, 無法偵測移動的距離, 方向與位置
  • PIR 模組感應距離最遠 7 公尺, 範圍可達 110 度角
PIR 模組內主要是利用焦電型 (pyroelectric) 紅外線感測器 D203S 將溫度變化轉成電子訊號, 外面有一個聚乙烯平凸透鏡外罩, 用來過濾與聚焦波長 8um~14um 的紅外線 : 




背面是電路板與接腳 : 




PIR 模組有三隻接腳 :
  • VCC : +5V
  • GND : 接地
  • OUT : 3.3V (偵測到人體紅外線) 與 0V (未偵測到人體紅外線)
PIR 模組運作電源為 5V (不要接 3.3V 電源, 會無法偵測紅外線), 但輸出腳 OUT 偵測到人體紅外線時卻是輸出 3.3V, 因此可直接與 3.3V 系統的 ESP32 相連而無須做位準轉換. 在此次實驗中, 我將此 OUT 腳接到 GPIO13, 將其設定為輸入腳以接收 PIR 的偵測觸發信號. 


1. PIR 模組輸出信號檢視 : 

接下來我將上面 "Arduino 測試 : PIR 紅外線移動偵測 (二)" 這篇 Arduino 程式改寫為 MicroPython 版本, 用來觀察 PIR 模組是否正確輸出信號 : 

from machine import Pin
import time

pir_pin=Pin(13, Pin.IN)  # 初始化 GPIO13 為輸入
counter=0  # 用來計算已顯示的標記數量
while True:
    val=pir_pin.value()  # 讀取 PIR 模組的輸出
    mark='-'             # 預設標記 '-' 表示未偵測到
    if val == 1:         # 偵測到人體標記改為 '*'
        mark='*'  
    print(mark, end='')  # 列印標記不換行
    counter += 1         # 標記計數增量 1
    if counter >= 50:    # 超過 50 個標記換行
        print()  
        counter=0        # 重設計數器
    time.sleep(0.1)      # 等待 100 毫秒避免過多輸出

執行結果如下 : 

-----------------------------------***************
************************************************--
--------------------------------------------------
------------------****************************----
------------*******************************-------
--------------------*******************-----******
*******-----*************-------------------------

其中 ---- 表示未偵測到人體紅外線, 而 **** 表示有偵測到人體紅外線, 確認 PIR 模組有正常輸出. 接下來就可以利用 PIR 輸出來觸發 IRQ, 讓中斷處理函式呼叫拍照函式擷取影像並存檔. 


2. 用 PIR 觸發中斷拍照並存檔 : 

此例使用 GPIO13 作為中斷輸入腳, 上面範例的接線不用改, PIR 輸出仍然是接到 GPIO13, 但程式改為將 GPIO13 設為偵測 PIR 送來的上升緣觸發中斷信號. 關於中斷用法參考 :


透過 ChatGPT 協作, 經過多次測試修改後得到下面可正常運作的程式 : 

# pir_camera_capture.py
import camera
import time
from machine import Pin
from time import localtime
import gc

def init_camera():  # 初始化鏡頭
    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)
    print("Camera initialized.")

def capture_image():  # 擷取影像 & 存檔
    now=localtime()
    file_name='cap_{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}.jpg'.format(
        now[0], now[1], now[2], now[3], now[4], now[5])
    buf=camera.capture()
    if buf:
        with open(file_name, 'wb') as f:
            f.write(buf)
        print(f'Image has been saved as {file_name}')
    else:
        print('Failed to capture image')
    del buf
    gc.collect()  # 強制垃圾回收

def pir_trigger(pin):  # 有防抖動的 IRQ 中斷處理函式
    global motion_detected, last_trigger_time  # 取得全域變數
    current_time=time.ticks_ms()  # 紀錄現在毫秒數時戳
    if time.ticks_diff(current_time, last_trigger_time) > 2000:  # 防抖
        print('Motion detected!') # 超過 2 秒表示已穩定(非信號抖動)
        motion_detected=True      # 設定拍照旗標
        last_trigger_time=current_time  # 更新防抖動時戳

# 初始化全域變數
motion_detected=False  # 觸發中斷旗標 : 用來通知主程式執行拍照
last_trigger_time=0    # PIR 輸出防抖動時間戳
# 初始化攝像頭
init_camera()   
# 設定 PIR 引腳及中斷
pir_pin=Pin(13, Pin.IN, Pin.PULL_DOWN)
pir_pin.irq(trigger=Pin.IRQ_RISING, handler=pir_trigger)
# 主迴圈負責監視拍照旗標, 若被設定就拍照存檔
try:
    while True:
        if motion_detected:
            capture_image()
            motion_detected=False  # 拍照旗標
        time.sleep(0.1)
except KeyboardInterrupt:
    print("Exiting...")
    camera.deinit()

此程式與之前的範例有一個重大差別, 那就是此處攝像頭只初始化一次, 之前的作法是每拍一次就呼叫 camera.deinit() 釋放記憶體資源, 下次拍照前重新呼叫 init_camera() 初始化, 這樣做是多此一舉, 因為初始化只要一次即可, 就能多次呼叫 capture() 進行多次拍照, 也可以避免在中段處理中減少了頻繁的初始化與資源釋放. 

此程式使用全域變數來記錄拍照旗標狀態, 在中斷處理函式中只是簡單判斷一下是否為穩定之處發信號而非抖動以過濾雜訊, 若為穩定之觸發信號就設定拍照旗標讓主程式的無窮迴圈去處理拍照事宜, 因為中斷處理函式不應占用太長的處理時間, 否則可能運作會不如預期. 


測試結果如下 : 

True
Camera initialized.
<IRQ>
Motion detected!
5214
Image has been saved as capture_20241117_194500.jpg
Motion detected!
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
8688
Image has been saved as capture_20241117_194507.jpg
Motion detected!
7787
Image has been saved as capture_20241117_194514.jpg
Motion detected!
cam_hal: EV-EOF-OVF
7787
Image has been saved as capture_20241117_194520.jpg
Motion detected!
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
8742
Image has been saved as capture_20241117_194523.jpg
Motion detected!
cam_hal: EV-EOF-OVF
8272
Image has been saved as capture_20241117_194526.jpg
Motion detected!
7016
Image has been saved as capture_20241117_194533.jpg
Motion detected!
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
7314
Image has been saved as capture_20241117_194537.jpg
Motion detected!
8005
Image has been saved as capture_20241117_194540.jpg
Motion detected!
cam_hal: EV-EOF-OVF
8005
Image has been saved as capture_20241117_194545.jpg
Motion detected!
cam_hal: EV-EOF-OVF
7881
Image has been saved as capture_20241117_194547.jpg
Motion detected!
cam_hal: EV-EOF-OVF
8102
Image has been saved as capture_20241117_194554.jpg
Motion detected!
cam_hal: EV-EOF-OVF
7914
Image has been saved as capture_20241117_194600.jpg
Motion detected!
cam_hal: EV-EOF-OVF
7023
Image has been saved as capture_20241117_194606.jpg
Motion detected!
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
8062
Image has been saved as capture_20241117_194613.jpg
Motion detected!
8543
Image has been saved as capture_20241117_194616.jpg
Motion detected!
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
.... (略) ....
Motion detected!
7794
Image has been saved as capture_20241117_195246.jpg
Motion detected!
7928
Image has been saved as capture_20241117_195248.jpg
Motion detected!
8078
Image has been saved as capture_20241117_195311.jpg
Motion detected!
cam_hal: EV-EOF-OVF
8084
Image has been saved as capture_20241117_195316.jpg
Motion detected!
7924
Image has been saved as capture_20241117_195403.jpg
Motion detected!
7924
Image has been saved as capture_20241117_195407.jpg
Exiting...
True

檢視開發板根目錄果然建立了許多 jpeg 圖檔 :




下面是擷取影像中的一張圖 capture_20241117_195240.jpg :




這是我鄉下家的書房兼臥室哈哈. 

2024年11月17日 星期日

2024 年第 46 周記事

本周的空閒時間主要在研究 ESP32-WROVER-DEV 開發板的影像擷取測試, 此板利用特製 MicroPython 韌體中的 camera 模組拍照沒問題, 但特效我經測試似乎無效. 接下來可以試試用 Aduino 的 C 語言來控制, 可用的功能應該會更多. 

菁菁週二空閒時打電話說她找了很久的 MAZDA MX-5 有一個新北車商通知有一台 2018 年式里程數三萬多的二手車上架, 她覺得很喜歡打算買, 下周一就要交車付尾款, 但要考駕照客戶又排滿滿沒時間去臨櫃領錢, 我說我先墊, 但她線上匯款給我要分好幾次且跨行要收手續費, 叫我去開中國信託帳戶. 所以週四午休時間就去民族分行預約下周一開戶, 同時還辦了中信的中油卡, 綁卡加油有 4% 回饋挺划算的. 

週四附近的診所來公司打流感與新冠疫苗 (莫德納), 一隻手臂打一支疫苗, 過了兩天打新冠的右手臂還有腫痛感, 回來查之前的疫苗卡, 發現這次是第六劑因新冠了, 上一次已經是快兩年前的事了. 那時曾因菁菁確診我還居家上班了半年哩. 

今天繼續上周未完成的浴室太陽能感應燈安裝, 由於要穿過兩層鋁門窗, 洞是穿好了, 但是電線要穿過去卻不是那麼容易, 費了好多時間才終於穿線完成, 測試功能如預期, 晚上趁洗澡順便測試, 故意不開燈讓感應燈偵測到人在動一直開啟, 雖然亮度不如一般燈泡, 但至少停電時不怕沒照明啦! 






2024年11月16日 星期六

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

完成 camera 模組的初始化與擷取函式測試後, 接下來就可以來做一些應用實驗了, 本篇要做的測試是每當按下按鈕時就擷取鏡頭影像並存檔 (即按鈕拍照). 

本系列之前的文章參考 :


首先準備一個按鈕, 它的短邊 (距離較短) 接點平時是 OFF 不相接, 按下時 ON 相接, 將其一端接 ESP32 的 GND, 另一端接一個 GPIO 腳, 例如 GPIO13, 在程式中設定其為輸入腳並開啟此上拉電阻, 這樣當按鈕未按下時輸入為 HIGH, 按下時為 LOW. 

程式碼如下 :

# push_shot_1.py
import time, camera
from machine import reset, Pin
    
def init_camera(**config): # 初始化鏡頭
    config.setdefault('fb_location', camera.PSRAM)
    config.setdefault('format', camera.JPEG)
    config.setdefault('xclk_freq', camera.XCLK_10MHz)
    config.setdefault('framesize', camera.FRAME_QVGA)
    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, **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
    
button=Pin(13, Pin.IN, Pin.PULL_UP)
file_counter=1  # 檔案計數器
try:
    while True:
        if button.value() == 0:  # 按鈕被按下
            file_name=f'capture{file_counter:02d}.jpg'  # 格式化檔名
            print(f'Button pressed! Capturing image as {file_name}...')
            init_camera()  # 初始化鏡頭
            capture_image(file_name)  # 擷取影像
            file_counter += 1  # 更新檔案編號
            time.sleep(1)  # 避免按鈕抖動影響
except KeyboardInterrupt:
    print("Program stopped.")

此程式使用 polling 法以無窮迴圈偵測鈕是否欸按下, 是的話就先初始化鏡頭後拍照存檔, 下面是執行後連按 5 次的輸出 :

Button pressed! Capturing image as capture01.jpg...
0
4
10000000
5
True
10132
Image has been saved as capture01.jpg
True
Button pressed! Capturing image as capture02.jpg...
0
4
10000000
5
True
9959
Image has been saved as capture02.jpg
True
Button pressed! Capturing image as capture03.jpg...
0
4
10000000
5
True
9948
Image has been saved as capture03.jpg
True
Button pressed! Capturing image as capture04.jpg...
0
4
10000000
5
True
9898
Image has been saved as capture04.jpg
True
Button pressed! Capturing image as capture05.jpg...
0
4
10000000
5
True
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
9974
Image has been saved as capture05.jpg
True

開發板上的檔案列表顯示有 5 個 jpeg 檔 :




這五張 jpeg 都是如下相同的圖 : 




其實就是我的電腦桌啦.

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

本篇主要是紀錄 camera 模組的初始化函式的改寫, 本系列之前的文章參考 :


在前兩篇的測試中所使用的初始化函式寫法其實有 bug, 原寫法如下 :

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

其中 init_camera() 函式的傳入參數使用可變長度參數 **config,  原意是想如果要更改這些參數的預設值 (例如解析度預設是 QVGA 想改為 VGA), 就在呼叫 init_camera() 時傳給 **config, 例如 :

init_camera(framesize=camera.FRAME_VGA) 

關於關鍵字參數 **kwargs 用法參考 :



但這樣會出現參數重複的錯誤 (extra keyword arguments given) : 

>>> init_camera(framesize=camera.FRAME_VGA)      
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in init_camera
TypeError: extra keyword arguments given   

實踐原意的正確做法應該是要用字典的 setefault() 函式來設定傳入之關鍵字參數 :

import time, camera
from machine import reset
    
def init_camera(**config): # 初始化鏡頭
    config.setdefault('fb_location', camera.PSRAM) 
    config.setdefault('format', camera.JPEG)  
    config.setdefault('xclk_freq', camera.XCLK_10MHz)  
    config.setdefault('framesize', camera.FRAME_QVGA)   
    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, **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

其實這四個用 setdefault() 設定初始值的參數通常只有 xclk_freq 與 framesize 會需要設定.

測試結果如下 :

不傳入參數的話預設解析度是 320*240 : 

>>> init_camera()      
0
4
10000000
5
True
>>> capture_image()      
8872
Image has been saved as capture.jpg
True

可見呼叫 dict 的 setdefault() 時會傳回設定值. 




如果傳入 framesize 參數指定 VGA, 則解析度變成 640*480 :

>>> init_camera(framesize=camera.FRAME_VGA)   
0
4
10000000
8
True
>>> capture_image()    
cam_hal: EV-EOF-OVF
cam_hal: EV-EOF-OVF
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
21140
Image has been saved as capture.jpg
True



下面範例是同時設定 framesize=VGA 與 xclk_freq=20mM 參數 :

>>> init_camera(framesize=camera.FRAME_VGA, xclk_freq=camera.XCLK_20MHz)   
0
4
20000000
8
True
>>> capture_image()    
cam_hal: EV-EOF-OVF
16460   
Image has been saved as capture.jpg
True




結果頻率高檔案大小比較小, 這可能是因為時脈越高, 影像數據擷取速率快, 引入較多信號噪音, 提高了演算法的壓縮比所致. 

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

本篇繼續來測試 ESP32-WROVER-DEV 開發板的影像擷取, 本系列前一篇測試參考 :


使用的程式碼如下 : 

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

先抓一張預設未調整影像 : 

>>> init_camera()   
True
>>> capture_image(file_name='capture1.jpg')     
14487
Image has been saved as capture1.jpg
True




接下來測試 camera 模組的各個函式. 


1. 亮度 (brightness) : 

預設的圖片亮度似乎有點暗, 呼叫 brightness(2) 設到最亮 : 

>>> init_camera()   
True
>>> camera.brightness(2)   
>>> capture_image(file_name='capture2.jpg')  
9985
Image has been saved as capture2.jpg
True




感覺反而比預設圖暗 (也可能是室內燈光關係). 下面是設成最暗 :

>>> init_camera()    
True
>>> camera.brightness(-2)   
>>> capture_image(file_name='capture3.jpg')   
9388
Image has been saved as capture3.jpg 
True




有比預設圖暗.  


2. 對比度 (contrast) : 

接著用 camera.contrast(2) 將對比度調到最大 : 

>>> init_camera()    
True
>>> camera.contrast(2)     
>>> capture_image(file_name='capture4.jpg')    
9422
Image has been saved as capture4.jpg
True




下面是對比最弱 :

>>> init_camera()    
True
>>> camera.contrast(-2)     
>>> capture_image(file_name='capture5.jpg')  
9417
Image has been saved as capture5.jpg
True




跟預設圖似乎差不多, 看不出明顯的對比變化. 


3. 飽和度 (saturation) : 

參數範圍 [-2, 2] 預設為 0, 負值偏灰, 正值偏彩. 下面是設定飽和度=2 : 

>>> init_camera()   
True
>>> camera.saturation(2)   
>>> capture_image(file_name='capture6.jpg')    
9402
Image has been saved as capture6.jpg 
True




下面測試飽和度 -2 :

>>> init_camera()       
True
>>> camera.saturation(-2)    
>>> capture_image(file_name='capture7.jpg')     
9339
Image has been saved as capture7.jpg
True




看不出有差異. 


4. 對比度 (contrast) : 

camera.contrast() 參數範圍 [-2, 2] 預設為 0, 設為 2 對比最高, -2 最低. 

>>> init_camera()    
True
>>> camera.contrast(2)       
>>> capture_image(file_name='capture8.jpg')    
9360
Image has been saved as capture8.jpg  
True




下面是對比度 -2 效果 :

>>> init_camera()   
True
>>> camera.contrast(-2)   
>>> capture_image(file_name='capture9.jpg')   
9233
Image has been saved as capture9.jpg
True




看不出有何變化. 


5. 特效 (speffect) : 

camera 有六種特效, 但測試發現並無明顯效果, 下面是負片測試 :

>>> init_camera()    
True
>>> camera.speffect(camera.EFFECT_NEG)    
>>> capture_image(file_name='capture10.jpg')     
9314
Image has been saved as capture10.jpg
True




下面黑白片測試無效 :

>>> init_camera()  
True
>>> camera.speffect(camera.EFFECT_BW)   
>>> capture_image(file_name='capture11.jpg')    
8717
Image has been saved as capture11.jpg
True




下面是紅色濾鏡效果 :

>>> init_camera()    
True
>>> camera.speffect(camera.EFFECT_RED)   
>>> capture_image(file_name='capture12.jpg')   
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
8751
Image has been saved as capture12.jpg
True




綠色濾鏡特效 :

>>> init_camera()   
True
>>> camera.speffect(camera.EFFECT_GREEN)    
>>> capture_image(file_name='capture13.jpg')    
8719
Image has been saved as capture13.jpg
True

藍色濾鏡效果 : 

>>> init_camera()    
True
>>> camera.speffect(camera.EFFECT_BLUE)    
>>> capture_image(file_name='capture14.jpg')    
8650
Image has been saved as capture14.jpg
True




>>> init_camera()     
True
>>> camera.speffect(camera.EFFECT_RETRO)     
>>> capture_image(file_name='capture15.jpg')    
8793
Image has been saved as capture15.jpg
True




這些特效似乎與預設圖沒啥差異. 


6. 垂直翻轉 (flip) : 

呼叫 flip() 並傳入 1 或 True 可讓擷取之影像垂直翻轉, 但並無效果 :

>>> init_camera()     
True
>>> camera.flip(True)    
>>> capture_image(file_name='capture16.jpg')    
8617
Image has been saved as capture16.jpg
True




7. 水平翻轉 (mirror) : 

呼叫 mirror() 並傳入 1 或 True 可讓擷取之影像水平翻轉, 但測試無效果 :

>>> init_camera()    
True
>>> camera.mirror(True)   
>>> capture_image(file_name='capture17.jpg')    
8497
Image has been saved as capture17.jpg
True



8. 畫質 (quality) : 

呼叫 quality() 設定影像品質, 參數範圍 [10, 63] 預設為 12, 值越大品質越高, 但測試無效果.

>>> init_camera()     
True
>>> camera.quality(30)       
>>> capture_image(file_name='capture18.jpg')     
cam_hal: EV-EOF-OVF
8545
Image has been saved as capture18.jpg
True




圖檔大小仍是一般 8KB, 解析度仍是 320*240, 畫質其實沒有變化. 

以上測試結果顯示, camera 模組雖然可以正常擷取影像, 但它的許多函式目前可能只是一個空殼的介面並未實作, 所以並無效果. 

2024年11月15日 星期五

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

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


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




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




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




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

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


韌體下載網址如下 :


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


在原著 Lemariva 的 GitHub 教學文件最後面有說明如何自行編譯韌體 :


ESP32-WROVER-DEV 開發板針腳配置如下圖 :




參考 :



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, 兩三次之後還是會出現溢位現象. 既然不影響影像擷取就不管它了.  

2024年11月13日 星期三

市圖還書 1 本 : 88張圖看懂技術分析

今天下班順路去河堤還下列這本 (被預約) :
我最近都在玩 AI 與 MicroPython, 沒時間看投資理財的書, 還了也好. 

露天購買 ESP32-S3 開發板 N16R8 (已焊) x 3

昨天解決 ESP32-S3 RGB LED 問題後, 覺得這塊開發板有 SPI RAM 真不錯, N16R8 有 16MB Flash 與 8MB SPI RAM 容量超大, 我找到下面這賣家一片才 223 元就買了三片 (之前一片買了 399 元) :





全家取貨免運 669 元, 比之前買兩片還便宜 (N16R8 與 N8R8 價格一樣當然是買 N16R8).