
最近剛把手頭這臺(tái)“能聯(lián)網(wǎng)的智能貓眼”完整跑通從硬件焊接到代碼調(diào)通前前后后折騰了大概兩個(gè)周末。這里把整個(gè)項(xiàng)目從選型到實(shí)現(xiàn)的完整過(guò)程整理出來(lái)包括所有踩過(guò)的坑和最終能直接跑的代碼給想做類(lèi)似東西的朋友一個(gè)參考。這套系統(tǒng)用的是ESP32-S3做主控搭配OV2640攝像頭模塊和ILI9488屏幕。核心做的事情其實(shí)就三件攝像頭上電后持續(xù)采集門(mén)口畫(huà)面畫(huà)面在室內(nèi)端的ILI9488屏幕上實(shí)時(shí)顯示同時(shí)通過(guò)WiFi把畫(huà)面以視頻流的形式推送到手機(jī)或電腦瀏覽器做到人在客廳甚至不在家也能看到門(mén)口情況。相比市面上成品智能貓眼DIY這套的成本大概在一百五到兩百塊之間而且代碼完全可控想加什么功能自己改就行。這套方案適合有基礎(chǔ)單片機(jī)經(jīng)驗(yàn)、能看懂基本電路、愿意動(dòng)手焊線(xiàn)的朋友。如果你只是想把代碼燒進(jìn)去能亮屏出畫(huà)面那花一個(gè)小時(shí)就能搞定如果想做成一款真正能日常使用、穩(wěn)定不重啟的成品那這篇文里的供電、接線(xiàn)、內(nèi)存分配這些問(wèn)題都得認(rèn)真看。1. 整體設(shè)計(jì)思路為什么是這三顆芯片的組合1.1 選型邏輯每一顆芯片在系統(tǒng)中扮演的角色先說(shuō)ESP32-S3。很多人會(huì)問(wèn)為什么不用經(jīng)典的ESP32-CAM或者ESP32-CAM-MB那玩意兒連攝像頭都集成了焊上就能用。ESP32-CAM確實(shí)便宜三十幾塊錢(qián)一塊板子OV2640攝像頭直接排線(xiàn)接好但這塊板子的短板太明顯了一是PSRAM頻率和帶寬有限在較高分辨率下采集JPEG再通過(guò)WiFi推流幀率很難超過(guò)15幀實(shí)際使用中卡頓明顯二是ESP32-CAM板載的PCB天線(xiàn)在金屬外殼或者門(mén)體附近信號(hào)衰減非常厲害三是IO口太少想接門(mén)鈴、PIR傳感器、補(bǔ)光燈、喇叭這些外設(shè)基本就沒(méi)引腳可用了。而ESP32-S3這顆芯片在ESP32系列里屬于“偏科但偏得特別準(zhǔn)”的一顆。它的兩個(gè)核心最高跑到240MHz內(nèi)置的向量指令對(duì)圖像處理有幫助但真正讓它適合做智能貓眼的是它最高支持16MB的Quad SPI PSRAM而且PSRAM和主頻之間的通信帶寬比老款ESP32高了不少。攝像頭采集出來(lái)的原始圖像數(shù)據(jù)量巨大一張480x320的RGB565圖像就是300KB如果沒(méi)有PSRAM根本存不下幾幀緩沖而把圖像以JPEG格式壓縮后一張圖大概30-60KB但對(duì)MCU來(lái)說(shuō)壓縮過(guò)程本身就需要大量臨時(shí)內(nèi)存。S3的大PSRAM意味著我可以同時(shí)開(kāi)多個(gè)幀緩沖一個(gè)用于屏幕顯示一個(gè)用于網(wǎng)絡(luò)推流互相不干擾。再說(shuō)OV2640。這個(gè)傳感器在DIY圈子里用的時(shí)間非常久200萬(wàn)像素支持UXGA1600x1200輸出也能直接輸出JPEG壓縮格式。選擇它而不是更新的OV5640首要原因是資料多、坑少、驅(qū)動(dòng)成熟esp32-camera庫(kù)對(duì)它的支持非常完善幾乎插上就能出圖像。另一個(gè)重要原因是功耗和發(fā)熱OV2640在正常工作時(shí)的電流在60mA左右而OV5640動(dòng)輒上百毫安在貓眼這種要7x24小時(shí)通電的場(chǎng)景里穩(wěn)定性和發(fā)熱控制更重要。最后是ILI9488屏幕。480x320的分辨率在MCU直驅(qū)的屏幕里已經(jīng)算大尺寸了作為貓眼室內(nèi)端的顯示設(shè)備能比較清楚地看到門(mén)外人臉和身體輪廓。ILI9488是SPI接口四線(xiàn)制接線(xiàn)非常簡(jiǎn)單而且這顆驅(qū)動(dòng)IC非常成熟幾乎所有主流GUI庫(kù)都原生支持。另外ILI9488的可視角度比普通IPS略差但貓眼的使用場(chǎng)景是固定角度觀看影響不大而且它的價(jià)格便宜一塊3.5寸屏大概三十塊上下性?xún)r(jià)比很高。1.2 系統(tǒng)三條核心數(shù)據(jù)鏈路這個(gè)項(xiàng)目的系統(tǒng)架構(gòu)本質(zhì)上可以拆成三條獨(dú)立的數(shù)據(jù)鏈路所有代碼和硬件設(shè)計(jì)都是圍繞這三條鏈路展開(kāi)的。第一條是采集鏈路OV2640攝像頭通過(guò)DVP并行接口把圖像數(shù)據(jù)傳給ESP32-S3。DVP是一條8位并行數(shù)據(jù)總線(xiàn)加上PCLK像素時(shí)鐘、VSYNC幀同步、HREF行同步等控制信號(hào)。ESP32-S3的硬件外設(shè)中有專(zhuān)門(mén)的LCD_CAM接口可以直接對(duì)接這類(lèi)并行圖像傳感器據(jù)說(shuō)比用IO模擬時(shí)序要穩(wěn)定得多。數(shù)據(jù)進(jìn)入芯片后DMA控制器會(huì)直接把數(shù)據(jù)搬運(yùn)到PSRAM中的幀緩沖區(qū)域整個(gè)過(guò)程不占用CPU核心CPU只需要在幀完成時(shí)收到一個(gè)中斷通知。第二條是顯示鏈路ESP32-S3從PSRAM中取出圖像數(shù)據(jù)經(jīng)過(guò)縮放和格式轉(zhuǎn)換后通過(guò)SPI接口寫(xiě)入ILI9488屏幕。這里的關(guān)鍵是SPI速度盡量拉高同時(shí)必須開(kāi)啟DMA傳輸否則SPI傳輸一個(gè)大尺寸圖像會(huì)長(zhǎng)時(shí)間阻塞CPU。如果直接把一整塊480x320的RGB565數(shù)據(jù)刷到屏幕上數(shù)據(jù)量是300KB在40MHz SPI時(shí)鐘下單次傳輸也需要約60ms所以顯示刷新率做到15-20幀是合理范圍實(shí)際肉眼看完全夠用不會(huì)覺(jué)得特別卡。第三條是網(wǎng)絡(luò)鏈路ESP32-S3把JPEG格式的圖像幀通過(guò)WiFi發(fā)送出去瀏覽器端用MJPEG格式解碼顯示。MJPEG本質(zhì)就是把一組JPEG圖片按順序播放每一幀是獨(dú)立的完整JPEG文件所以接收端解碼邏輯非常簡(jiǎn)單瀏覽器原生支持手機(jī)端也不需要裝任何App打開(kāi)網(wǎng)頁(yè)就能看。這條鏈路的關(guān)鍵在于WiFi的帶寬和TCP/IP協(xié)議棧的處理能力決定了能推多高的幀率而S3在局域網(wǎng)內(nèi)實(shí)際測(cè)試推VGA分辨率640x480的MJPEG流可以穩(wěn)定跑在20幀以上。這三條鏈路核心的矛盾點(diǎn)在于內(nèi)存和帶寬的分配。采集鏈路要占一塊幀緩沖顯示鏈路再占一塊網(wǎng)絡(luò)推流還要一塊如果不提前規(guī)劃好PSRAM的分區(qū)和使用策略后面調(diào)試必然后悔。2. 硬件選型與接線(xiàn)避坑從板子到屏幕的完整清單2.1 開(kāi)發(fā)板怎么選帶PSRAM是硬性條件ESP32-S3的模組和開(kāi)發(fā)板市面上非常多選擇的時(shí)候最核心的一條就是必須選帶PSRAM的型號(hào)而且最好是Octal PSRAM。因?yàn)橹霸贓SP32-CAM上吃過(guò)沒(méi)內(nèi)存的虧這次我直接選了ESP32-S3-WROOM-1-N16R8這個(gè)模組的開(kāi)發(fā)板其中16表示16MB FlashR8表示8MB的Octal PSRAM。在實(shí)際選擇開(kāi)發(fā)板時(shí)需要注意幾個(gè)細(xì)節(jié)。第一看板子是否引出了完整的攝像頭FPC排線(xiàn)座有些S3開(kāi)發(fā)板雖然芯片支持?jǐn)z像頭接口但板上沒(méi)有焊接攝像頭座子需要自己飛線(xiàn)或者轉(zhuǎn)接板非常痛苦。第二看引腳是否兼容常見(jiàn)庫(kù)的默認(rèn)配置esp32-camera庫(kù)在GitHub上針對(duì)S3開(kāi)發(fā)板有專(zhuān)門(mén)的引腳定義文件選板時(shí)盡量選那些已知配置的板子能省很多調(diào)試時(shí)間。第三看板上有沒(méi)有USB轉(zhuǎn)串口芯片S3原生支持USB CDC可以直接通過(guò)USB口燒錄和打印日志但有些低端板子偷工減料USB電路做得不穩(wěn)定燒錄時(shí)經(jīng)常掉線(xiàn)。我買(mǎi)的是合宙ESP32-S3開(kāi)發(fā)板帶8MB PSRAM攝像頭FPC座子直接焊好引出IO也基本兼容官方示例配置。這塊板子看起來(lái)比較簡(jiǎn)陋也沒(méi)有金屬屏蔽罩但實(shí)際測(cè)試WiFi信號(hào)和PSRAM穩(wěn)定性都還不錯(cuò)價(jià)格大概六十塊出頭。當(dāng)然如果預(yù)算充足用樂(lè)鑫官方的DevKitC也是好選擇做工和穩(wěn)定性更有保障。2.2 接線(xiàn)表與引腳說(shuō)明照著接就行OV2640攝像頭采用DVP并行接口總共需要將近15根信號(hào)線(xiàn)。我在實(shí)際接線(xiàn)時(shí)沒(méi)有直接用跳線(xiàn)飛線(xiàn)而是讓攝像頭模塊通過(guò)FPC排線(xiàn)連到一個(gè)轉(zhuǎn)接板再?gòu)霓D(zhuǎn)接板引出插針到開(kāi)發(fā)板。這樣做的原因是DVP的數(shù)據(jù)線(xiàn)D0-D7頻率較高如果全部用杜邦線(xiàn)懸空飛線(xiàn)信號(hào)串?dāng)_會(huì)導(dǎo)致圖像出現(xiàn)彩虹紋和噪點(diǎn)這種問(wèn)題排查起來(lái)非常頭疼。以下是本次項(xiàng)目的完整接線(xiàn)表攝像頭信號(hào)GPIO說(shuō)明SIODI2C數(shù)據(jù)GPIO8攝像頭寄存器配置用I2CSIOCI2C時(shí)鐘GPIO9攝像頭寄存器配置用I2CVSYNC幀同步GPIO6一幀圖像開(kāi)始/結(jié)束的標(biāo)志HREF行同步GPIO7一行像素開(kāi)始/結(jié)束的標(biāo)志PCLK像素時(shí)鐘GPIO13每個(gè)像素的同步時(shí)鐘XCLK主時(shí)鐘GPIO15由ESP32輸出的攝像頭工作時(shí)鐘D0-D7數(shù)據(jù)線(xiàn)GPIO11, 12, 14, 16, 17, 18, 48, 478位并行像素?cái)?shù)據(jù)PWDN電源控制GPIO10低電平使能攝像頭RESET復(fù)位GPIO5攝像頭復(fù)位控制可懸空屏幕接線(xiàn)相對(duì)簡(jiǎn)單就是標(biāo)準(zhǔn)的SPI屏屏幕信號(hào)GPIO說(shuō)明CS片選GPIO3SPI片選低電平有效DC數(shù)據(jù)/命令選擇GPIO40表示寫(xiě)命令1表示寫(xiě)數(shù)據(jù)SCKSPI時(shí)鐘GPIO1最高建議不超過(guò)40MHzMOSI主出從入GPIO2屏幕數(shù)據(jù)線(xiàn)RST復(fù)位GPIO42屏幕復(fù)位低電平有效BLK背光GPIO41高電平點(diǎn)亮背光支持PWM調(diào)光這里有幾個(gè)值得注意的點(diǎn)。首先攝像頭和屏幕共用SPI或者I2C總線(xiàn)會(huì)互相干擾嗎我的方案是兩者各自占用獨(dú)立的GPIO和總線(xiàn)攝像頭走專(zhuān)用并行接口屏幕走SPI互不干擾。其次背光引腳不要直接接3.3V高電平最好接一個(gè)GPIO這樣在代碼里可以控制屏幕休眠時(shí)熄滅背光能明顯降低整機(jī)功耗。第三如果板子上的GPIO定義和上面這張表不一致一定要先查開(kāi)發(fā)板的原理圖再接線(xiàn)不要盲目照抄否則可能燒壞傳感器。2.3 供電設(shè)計(jì)貓眼死機(jī)重啟的第一大元兇智能貓眼是7x24小時(shí)通電設(shè)備供電穩(wěn)定性決定了整個(gè)項(xiàng)目的成敗。很多人DIY的時(shí)候直接用USB線(xiàn)接一個(gè)手機(jī)充電頭結(jié)果發(fā)現(xiàn)設(shè)備每隔幾分鐘就重啟一次或者WiFi一推流就重啟大概率都是供電問(wèn)題。先算一下整套系統(tǒng)的功耗。ESP32-S3在WiFi開(kāi)啟且CPU全速運(yùn)行時(shí)的電流大約300-400mA攝像頭工作電流約60-80mAILI9488屏幕點(diǎn)亮背光時(shí)約100-150mA三個(gè)加起來(lái)正常工作電流在500mA左右峰值瞬間電流甚至能到800mA。所以供電方案至少要能持續(xù)提供1A以上的電流。普通的USB充電頭大多能輸出1-2A問(wèn)題不大但連接線(xiàn)一定要短線(xiàn)徑要粗我實(shí)測(cè)過(guò)某些劣質(zhì)MicroUSB線(xiàn)在1A電流下的壓降居然有0.5V以上直接導(dǎo)致3.3V電壓跌落MCU當(dāng)場(chǎng)復(fù)位。另外如果打算把這塊貓眼做成一個(gè)壁掛裝置強(qiáng)烈建議用5V 2A的電源適配器供電而不是用鋰電池。鋰電池方案在ESP32-S3這種高負(fù)載場(chǎng)景下需要非常精細(xì)的電源管理否則電池很快饋電反而影響使用體驗(yàn)。當(dāng)然如果門(mén)邊有預(yù)留86盒電源線(xiàn)直接接一個(gè)5V降壓模塊是最理想的。還有一個(gè)非常關(guān)鍵的細(xì)節(jié)攝像頭模塊的AVDD和DOVDD通常由板載LDO提供但如果使用獨(dú)立攝像頭模組要注意它的供電是否和主控共地。如果地線(xiàn)接觸不良畫(huà)面會(huì)出現(xiàn)嚴(yán)重的橫紋干擾這種問(wèn)題用萬(wàn)用表量電壓是量不出來(lái)的只能重新整理地線(xiàn)解決。3. 軟件框架與代碼實(shí)現(xiàn)從初始化到推流的完整閉環(huán)3.1 開(kāi)發(fā)環(huán)境直接用PlatformIO別用Arduino IDE代碼層面我使用的是PlatformIO ESP-IDF框架。這里先說(shuō)明一下為什么不用Arduino IDEESP32-S3的攝像頭、WiFi推流、大屏顯示都需要精細(xì)化控制內(nèi)存和時(shí)序Arduino IDE雖然配置簡(jiǎn)單但出了問(wèn)題尤其是內(nèi)存分配和編譯優(yōu)化相關(guān)的問(wèn)題排查起來(lái)非常麻煩而且它對(duì)多個(gè)文件的工程管理很弱。PlatformIO底層用的是ESP-IDF編譯出來(lái)的固件更高效內(nèi)存管理更透明而且生態(tài)里的庫(kù)管理非常方便一條配置命令就能搞定依賴(lài)。在platformio.ini配置文件里核心配置如下[env:esp32s3] platform espressif32 board esp32-s3-devkitc-1 framework arduino board_build.flash_mode qio board_build.f_flash 80000000L board_build.psram_type qio board_build.f_psram 80000000L build_flags -DARDUINO_USB_CDC_ON_BOOT1 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue lib_deps me-no-dev/ESP32-Camera1.0.0 moononournation/GFX Library for Arduino1.4.8 moononournation/TFT_eSPI2.5.0這里有三個(gè)關(guān)鍵點(diǎn)。第一PSRAM類(lèi)型和頻率必須和模組實(shí)際參數(shù)一致如果是Octal PSRAM這里要改配置如果配置錯(cuò)了系統(tǒng)會(huì)在啟動(dòng)時(shí)檢測(cè)到內(nèi)存異常攝像頭初始化必失敗。第二-mfix-esp32-psram-cache-issue這個(gè)標(biāo)志位很重要早期S3芯片在PSRAM和Cache協(xié)同工作時(shí)存在bug編譯時(shí)必須帶上這個(gè)修復(fù)選項(xiàng)否則運(yùn)行一兩個(gè)小時(shí)就會(huì)隨機(jī)死機(jī)。第三TFT_eSPI這個(gè)庫(kù)是屏幕顯示的核心它支持SPI DMA傳輸而且針對(duì)ILI9488有非常成熟的驅(qū)動(dòng)直接用它比自己寫(xiě)寄存器初始化要穩(wěn)定得多。TFT_eSPI庫(kù)在首次使用前需要修改它的User_Setup.h文件配置屏幕型號(hào)、引腳、SPI頻率。我使用的是以下配置#define ILI9488_DRIVER #define TFT_MISO -1 #define TFT_MOSI 2 #define TFT_SCLK 1 #define TFT_CS 3 #define TFT_DC 4 #define TFT_RST 42 #define TFT_BL 41 #define SPI_FREQUENCY 40000000 #define SPI_READ_FREQUENCY 20000000 #define SPI_TOUCH_FREQUENCY 2500000注意TFT_MISO設(shè)成了-1因?yàn)镮LI9488屏幕實(shí)際上不需要回傳數(shù)據(jù)給主控我們只需要單向?qū)憟D像數(shù)據(jù)這樣能省一根線(xiàn)。SPI頻率設(shè)為40MHz這是ILI9488在3.3V供電下比較穩(wěn)妥的上限再高可能因?yàn)樾盘?hào)完整性導(dǎo)致花屏。3.2 攝像頭初始化內(nèi)存布局和幀率的平衡攝像頭的初始化是整個(gè)項(xiàng)目里最容易出問(wèn)題的一步99%的“初始化失敗”“白屏”都出在內(nèi)存配置上。以下是我最終調(diào)通的初始化代碼#include esp_camera.h #define CAM_PIN_PWDN 10 #define CAM_PIN_RESET 5 #define CAM_PIN_XCLK 15 #define CAM_PIN_SIOD 8 #define CAM_PIN_SIOC 9 #define CAM_PIN_D7 11 #define CAM_PIN_D6 12 #define CAM_PIN_D5 14 #define CAM_PIN_D4 16 #define CAM_PIN_D3 17 #define CAM_PIN_D2 18 #define CAM_PIN_D1 48 #define CAM_PIN_D0 47 #define CAM_PIN_VSYNC 6 #define CAM_PIN_HREF 7 #define CAM_PIN_PCLK 13 void camera_init() { camera_config_t config; config.ledc_channel LEDC_CHANNEL_0; config.ledc_timer LEDC_TIMER_0; config.pin_d0 CAM_PIN_D0; config.pin_d1 CAM_PIN_D1; config.pin_d2 CAM_PIN_D2; config.pin_d3 CAM_PIN_D3; config.pin_d4 CAM_PIN_D4; config.pin_d5 CAM_PIN_D5; config.pin_d6 CAM_PIN_D6; config.pin_d7 CAM_PIN_D7; config.pin_xclk CAM_PIN_XCLK; config.pin_pclk CAM_PIN_PCLK; config.pin_vsync CAM_PIN_VSYNC; config.pin_href CAM_PIN_HREF; config.pin_sccb_sda CAM_PIN_SIOD; config.pin_sccb_scl CAM_PIN_SIOC; config.pin_pwdn CAM_PIN_PWDN; config.pin_reset CAM_PIN_RESET; config.xclk_freq_hz 20000000; config.pixel_format PIXFORMAT_JPEG; config.frame_size FRAMESIZE_VGA; config.jpeg_quality 10; config.fb_count 2; esp_err_t err esp_camera_init(config); if (err ! ESP_OK) { Serial.printf(攝像頭初始化失敗: 0x%x\n, err); return; } }這段代碼里最核心的配置項(xiàng)是這幾個(gè)xclk_freq_hz設(shè)為20MHz。XCLK是ESP32輸出給攝像頭的時(shí)鐘信號(hào)OV2640最高支持24MHz但實(shí)測(cè)在20MHz時(shí)畫(huà)面最穩(wěn)定超過(guò)之后偶爾會(huì)出現(xiàn)花屏和數(shù)據(jù)錯(cuò)位。20MHz下OV2640的輸出幀率已經(jīng)足夠。pixel_format設(shè)為PIXFORMAT_JPEG。這一點(diǎn)非常關(guān)鍵OV2640內(nèi)置了JPEG編碼器可以直接輸出壓縮后的JPEG流。如果這里設(shè)置成PIXFORMAT_RGB565一幀VGA畫(huà)面是614400字節(jié)640x480x2而JPEG格式可能只有20-50KB差距有十幾倍。對(duì)于網(wǎng)絡(luò)推流場(chǎng)景必須用JPEG格式否則WiFi帶寬根本不夠。fb_count設(shè)為2這是雙緩沖模式。雙緩沖的意思是在攝像頭采集完一幀寫(xiě)入一個(gè)緩沖區(qū)的同時(shí)CPU可以處理上一幀的數(shù)據(jù)不會(huì)出現(xiàn)采集和顯示互相等待的死鎖。如果沒(méi)有雙緩沖畫(huà)面會(huì)明顯卡頓幀率至少下降一半。jpeg_quality設(shè)為10這個(gè)值范圍是0-63數(shù)值越小畫(huà)質(zhì)越好但文件越大。10是經(jīng)過(guò)測(cè)試的平衡點(diǎn)畫(huà)面細(xì)節(jié)足夠看清楚人臉五官單幀大小在30-50KB在局域網(wǎng)推流時(shí)流暢度和清晰度都能兼顧。3.3 屏幕顯示把攝像頭畫(huà)面搬到ILI9488上的關(guān)鍵代碼攝像頭采集到JPEG數(shù)據(jù)后屏幕顯示需要先把JPEG解碼成RGB565格式再縮放并繪制到屏幕上。這里有一個(gè)非常重要的優(yōu)化點(diǎn)不要直接使用JPEG數(shù)據(jù)往屏幕上傳ILI9488不認(rèn)JPEG格式必須解碼成像素?cái)?shù)據(jù)。TFT_eSPI庫(kù)提供了一個(gè)高效的JPEG解碼接口配合PNGdec庫(kù)使用。但我的項(xiàng)目中用的是更輕量的方法直接調(diào)用esp32-camera庫(kù)自帶的fmt2rgb888函數(shù)把JPEG幀解碼成RGB888格式再轉(zhuǎn)換成TFT_eSPI需要的RGB565格式。核心代碼如下void display_frame(camera_fb_t *fb) { uint8_t *rgb888 (uint8_t *)ps_malloc(fb-width * fb-height * 3); if (rgb888 NULL) return; fmt2rgb888(fb-buf, fb-len, PIXFORMAT_JPEG, rgb888); uint16_t *rgb565 (uint16_t *)ps_malloc(320 * 240 * 2); if (rgb565 NULL) { free(rgb888); return; } // 將RGB888轉(zhuǎn)換為RGB565并縮放到240P for (int y 0; y 240; y) { for (int x 0; x 320; x) { int src_x x * fb-width / 320; int src_y y * fb-height / 240; int src_idx (src_y * fb-width src_x) * 3; uint16_t r rgb888[src_idx] 3; uint16_t g rgb888[src_idx 1] 2; uint16_t b rgb888[src_idx 2] 3; rgb565[y * 320 x] (r 11) | (g 5) | b; } } tft.pushImage(0, 0, 320, 240, rgb565); free(rgb888); free(rgb565); }這段代碼里有幾個(gè)關(guān)鍵決定。第一這里把顯示分辨率設(shè)成了320x240而不是ILI9488的全分辨率480x320因?yàn)閿z像頭輸出的是4:3比例的VGA畫(huà)面而屏幕也是4:3等比縮放后320x240剛好占滿(mǎn)屏幕的中間區(qū)域不需要裁剪。如果想要480x320全屏顯示需要把縮放目標(biāo)改成480x320但這樣每幀的轉(zhuǎn)換和傳輸時(shí)間會(huì)翻倍幀率會(huì)降到10幀以下不太劃算。第二臨時(shí)內(nèi)存全部用ps_malloc分配在PSRAM而不是普通的malloc這一點(diǎn)至關(guān)重要。普通的heap內(nèi)存只有幾百KB如果用來(lái)存放RGB888幀緩沖直接就把內(nèi)部RAM榨干了系統(tǒng)會(huì)因?yàn)閮?nèi)存不足不斷重啟。而PSRAM有8MB放兩個(gè)幀緩沖綽綽有余。第三這里實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的雙線(xiàn)性縮放——當(dāng)然嚴(yán)格來(lái)說(shuō)不是雙線(xiàn)性是等比例采樣效果在實(shí)際使用中夠用。如果對(duì)畫(huà)面質(zhì)量要求高可以換成真正的雙線(xiàn)性插值但MCU的處理速度會(huì)變慢幀率損失大約3-5幀貓眼場(chǎng)景下沒(méi)必要。這個(gè)功能在實(shí)際測(cè)試中的顯示幀率大約15-18幀橫豎方向都流暢看門(mén)口的人走動(dòng)完全不會(huì)覺(jué)得卡。3.4 聯(lián)網(wǎng)推流瀏覽器直接看實(shí)時(shí)畫(huà)面聯(lián)網(wǎng)部分是整個(gè)項(xiàng)目名稱(chēng)里“能聯(lián)網(wǎng)”三個(gè)字的核心。用esp32-camera庫(kù)自帶的httpd組件實(shí)現(xiàn)MJPEG流服務(wù)器這是最成熟穩(wěn)定的方案而且這個(gè)庫(kù)本身就內(nèi)置了對(duì)應(yīng)示例代碼只需要綁定一下URL路由就行。#include esp_http_server.h static esp_err_t stream_handler(httpd_req_t *req) { esp_camera_fb_t *fb NULL; esp_err_t res ESP_OK; httpd_resp_set_type(req, multipart/x-mixed-replace; boundaryframe); while (true) { fb esp_camera_fb_get(); if (!fb) { res ESP_FAIL; } else { char part[64]; snprintf(part, sizeof(part), --frame\r\nContent-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n, fb-len); httpd_resp_send_chunk(req, part, strlen(part)); httpd_resp_send_chunk(req, (const char *)fb-buf, fb-len); httpd_resp_send_chunk(req, \r\n, 2); } esp_camera_fb_return(fb); if (res ! ESP_OK) break; } return res; } void start_camera_server() { httpd_config_t config HTTPD_DEFAULT_CONFIG(); config.server_port 80; config.max_uri_handlers 4; config.stack_size 8192; httpd_handle_t server NULL; if (httpd_start(server, config) ESP_OK) { httpd_uri_t stream_uri { .uri /stream, .method HTTP_GET, .handler stream_handler, .user_ctx NULL }; httpd_register_uri_handler(server, stream_uri); } }MJPEG流的網(wǎng)絡(luò)協(xié)議本質(zhì)上是一個(gè)普通的HTTP長(zhǎng)連接服務(wù)器每隔一段時(shí)間就發(fā)一張JPEG圖片客戶(hù)端瀏覽器收到后按順序刷新顯示就達(dá)到了“視頻”的效果。瀏覽器端訪(fǎng)問(wèn)http://設(shè)備IP/stream就能看到實(shí)時(shí)畫(huà)面。我用手機(jī)和電腦各測(cè)了一遍局域網(wǎng)內(nèi)延遲大約200-300ms這個(gè)延遲對(duì)看門(mén)口夠用畢竟貓眼場(chǎng)景不需要像游戲那樣低延遲。這里有幾個(gè)需要特別說(shuō)明的工程化細(xì)節(jié)第一httpd_resp_send_chunk使用分塊傳輸編碼每幀JPEG數(shù)據(jù)作為一個(gè)獨(dú)立的chunk發(fā)送。這樣做的原因是MJPEG流本身數(shù)據(jù)量不穩(wěn)定單幀大小會(huì)隨著畫(huà)面復(fù)雜度變化如果一次性把一個(gè)巨大的數(shù)據(jù)包發(fā)給瀏覽器緩沖壓力很大容易出現(xiàn)畫(huà)面延遲越來(lái)越大的情況。分塊發(fā)送可以讓瀏覽器邊收邊顯示。第二config.stack_size設(shè)成了8192。HTTP流處理是在一個(gè)獨(dú)立的任務(wù)中運(yùn)行的這個(gè)任務(wù)需要足夠的??臻g來(lái)處理網(wǎng)絡(luò)協(xié)議棧和JPEG幀讀取如果棧設(shè)太小推流一段時(shí)間后會(huì)發(fā)生棧溢出表現(xiàn)為設(shè)備重啟或者流被切斷。8192是一個(gè)經(jīng)過(guò)實(shí)測(cè)比較安全的數(shù)值。第三幀率控制。這段代碼沒(méi)有主動(dòng)做幀率限制攝像頭的幀率是多少就推多少。如果發(fā)現(xiàn)推流占用帶寬過(guò)高可以在每次循環(huán)里加一個(gè)vTaskDelay(pdMS_TO_TICKS(30))把幀率限制在30幀以?xún)?nèi)。對(duì)貓眼場(chǎng)景來(lái)說(shuō)15-25幀已經(jīng)非常流暢再高只會(huì)增加帶寬和發(fā)熱。3.5 完整代碼流程三大任務(wù)如何協(xié)同工作整個(gè)固件的main函數(shù)采用的是多任務(wù)框架void setup() { Serial.begin(115200); camera_init(); tft.begin(); tft.setRotation(0); tft.fillScreen(TFT_BLACK); WiFi.begin(你的WiFi名稱(chēng), 你的WiFi密碼); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(\nWiFi連接成功IP地址: WiFi.localIP().toString()); start_camera_server(); xTaskCreatePinnedToCore(display_task, display, 8192, NULL, 1, NULL, 0); } void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); } void display_task(void *param) { while (true) { camera_fb_t *fb esp_camera_fb_get(); if (fb) { display_frame(fb); esp_camera_fb_return(fb); } } }整體任務(wù)劃分是這樣的loop()主循環(huán)只做心跳?;顢z像頭采集在esp_camera_fb_get的調(diào)用中自動(dòng)完成display_task這個(gè)任務(wù)專(zhuān)門(mén)負(fù)責(zé)把采集到的幀顯示到屏幕上網(wǎng)絡(luò)推流則由httpd服務(wù)器的獨(dú)立任務(wù)處理。三個(gè)任務(wù)可以并行運(yùn)行互不阻塞這是SPI屏幕和WiFi推流能同時(shí)工作的基礎(chǔ)。這里還有一個(gè)容易踩的坑不要在主循環(huán)里同時(shí)做推流和屏幕刷新因?yàn)閃iFi推流的網(wǎng)絡(luò)I/O會(huì)阻塞CPU屏幕刷新的時(shí)間也會(huì)被拉長(zhǎng)兩邊都會(huì)變卡。把不同功能拆到不同任務(wù)里綁定到不同的CPU核心上ESP32-S3是雙核才是真正能長(zhǎng)期穩(wěn)定運(yùn)行的結(jié)構(gòu)。4. 整機(jī)裝配與功能擴(kuò)展從“能跑”到“好用”4.1 外殼設(shè)計(jì)與鏡頭角度完成電路和代碼之后剩下的就是整機(jī)裝配。智能貓眼和普通開(kāi)發(fā)板項(xiàng)目的不同之處在于它需要長(zhǎng)期掛在門(mén)上所以外殼設(shè)計(jì)、鏡頭安裝角度和散熱都必須考慮進(jìn)去。我用FreeCAD設(shè)計(jì)了一個(gè)簡(jiǎn)單的兩件式外殼外層是固定在門(mén)外側(cè)的鏡頭面板開(kāi)一個(gè)12mm的圓孔正好卡住OV2640攝像頭的鏡頭鏡頭視場(chǎng)角約120度安裝時(shí)需要把鏡頭略微朝下傾斜10度左右這樣能同時(shí)拍到人臉和胸前區(qū)域視野比較合理。內(nèi)層是室內(nèi)側(cè)的屏幕面板ILI9488屏幕卡在一個(gè)凹槽里四周用熱熔膠固定。兩個(gè)外殼通過(guò)一條約1厘米寬的排線(xiàn)連接排線(xiàn)中間開(kāi)槽穿門(mén)。外殼材料我用的PLA3D打印參數(shù)0.2mm層高、20%填充打印時(shí)間大約4小時(shí)。PLA的耐熱性雖然一般但門(mén)內(nèi)環(huán)境溫度不會(huì)太高完全夠用。如果有條件PETG更耐潮濕和高溫但打印難度會(huì)高一些。如果沒(méi)有3D打印機(jī)也可以用亞克力板做框架或者直接用萬(wàn)能盒加開(kāi)孔器改裝。重點(diǎn)是攝像頭和屏幕都要固定牢靠不要晃動(dòng)否則畫(huà)面會(huì)抖。4.2 夜間與弱光場(chǎng)景優(yōu)化智能貓眼如果沒(méi)有夜視功能晚上基本就是瞎子。OV2640本身對(duì)光線(xiàn)的靈敏度一般在微弱光線(xiàn)下畫(huà)面噪點(diǎn)非常嚴(yán)重。我的方案分兩步解決第一步是加補(bǔ)光燈。在攝像頭鏡頭旁邊并排安裝兩顆白光LED由GPIO控制光照不足時(shí)自動(dòng)打開(kāi)??刂七壿媽?xiě)在主循環(huán)里每隔10秒讀取一次攝像頭畫(huà)面的平均亮度如果亮度低于閾值就開(kāi)燈。但這里要注意白色補(bǔ)光燈在夜間從門(mén)外看非常顯眼容易暴露自家門(mén)口裝了智能貓眼如果有隱私顧慮建議改用850nm紅外補(bǔ)光燈配合去掉紅外截止濾鏡的攝像頭可以實(shí)現(xiàn)真正的黑白夜視。第二步是調(diào)節(jié)攝像頭的曝光和增益。OV2640的傳感器寄存器支持下發(fā)自動(dòng)曝光和自動(dòng)增益控制參數(shù)#include sensor.h sensor_t *sensor esp_camera_sensor_get(); sensor-set_brightness(sensor, 1); sensor-set_contrast(sensor, 1); sensor-set_saturation(sensor, 0); sensor-set_quality(sensor, 10); sensor-set_gain_ctrl(sensor, 1); // 自動(dòng)增益 sensor-set_exposure_ctrl(sensor, 1); // 自動(dòng)曝光這些參數(shù)在夜間會(huì)自動(dòng)拉高增益和曝光時(shí)間畫(huà)面會(huì)變亮一些但噪點(diǎn)也會(huì)增加。實(shí)際使用中如果門(mén)口有樓道感應(yīng)燈或者有夜燈OV2640基本能看清人形輪廓如果要看清楚人臉建議還是加紅外補(bǔ)光。4.3 功能擴(kuò)展方向門(mén)鈴聯(lián)動(dòng)、PIR觸發(fā)、SD卡本地存儲(chǔ)基礎(chǔ)版本能出畫(huà)面、能聯(lián)網(wǎng)看已經(jīng)算是一個(gè)完整產(chǎn)品了。但如果想讓這套貓眼進(jìn)一步滿(mǎn)足日常使用需求還有幾個(gè)性?xún)r(jià)比很高的擴(kuò)展方向。第一個(gè)是門(mén)鈴聯(lián)動(dòng)。在門(mén)外側(cè)加一個(gè)按鈕按下后觸發(fā)GPIO中斷ESP32-S3在中斷中拍一張照片同時(shí)播放一段門(mén)鈴音效通過(guò)I2S接一個(gè)小喇叭再把這個(gè)事件推送到手機(jī)。實(shí)現(xiàn)起來(lái)并不難GPIO中斷拍照HTTP推送幾個(gè)功能拼在一起就行。第二個(gè)是PIR人體感應(yīng)聯(lián)動(dòng)。在門(mén)上方安裝一個(gè)PIR傳感器比如HC-SR505檢測(cè)到有人靠近時(shí)自動(dòng)喚醒屏幕開(kāi)始錄像或拍照人走之后屏幕休眠這樣可以大幅降低平均功耗讓屏幕不用7x24小時(shí)點(diǎn)亮。第三個(gè)是SD卡本地存儲(chǔ)。ESP32-S3支持SDMMC和SPI兩種方式讀寫(xiě)SD卡如果接上SD卡可以把門(mén)鈴觸發(fā)時(shí)的照片或視頻片段存到本地即使斷網(wǎng)也能保留證據(jù)。這個(gè)功能在安防場(chǎng)景下很有意義。這些擴(kuò)展方向的核心邏輯都是復(fù)用已經(jīng)跑通的“采集-顯示-網(wǎng)絡(luò)”鏈路只是在特定事件上增加觸發(fā)條件沒(méi)有想象中的復(fù)雜。后面我計(jì)劃把PIR觸發(fā)和SD卡存儲(chǔ)加上做一個(gè)真正能當(dāng)安防設(shè)備使用的完整版。5. 常見(jiàn)問(wèn)題排查與調(diào)試實(shí)錄5.1 一張實(shí)用的故障速查表在整個(gè)項(xiàng)目調(diào)試過(guò)程中我遇到了不少問(wèn)題把最有代表性的幾個(gè)列在下面供大家參考?,F(xiàn)象可能原因解決方案攝像頭初始化失敗返回0x10005PSRAM未正確啟用或內(nèi)存不足檢查platformio.ini的PSRAM配置確認(rèn)BOARD_HAS_PSRAM已定義攝像頭初始化成功但畫(huà)面全白XCLK頻率過(guò)高或攝像頭供電不足把XCLK從24MHz降為20MHz檢查攝像頭供電線(xiàn)直徑畫(huà)面有彩虹紋或花屏DVP數(shù)據(jù)線(xiàn)信號(hào)干擾改用FPC排線(xiàn)連接縮短數(shù)據(jù)線(xiàn)長(zhǎng)度確認(rèn)地線(xiàn)可靠連接屏幕白屏不顯示SPI引腳配置錯(cuò)誤或屏幕初始化失敗檢查User_Setup.h中的引腳定義是否和接線(xiàn)一致測(cè)試SCK是否有波形屏幕畫(huà)面撕裂屏幕刷新和攝像頭采集之間沒(méi)有同步使用fb_count2雙緩沖模式確認(rèn)代碼中使用esp_camera_fb_get返回后及時(shí)釋放WiFi推流卡頓嚴(yán)重緩沖區(qū)太小或SPI刷新占用CPU過(guò)高網(wǎng)絡(luò)推流任務(wù)和屏幕刷新任務(wù)分核運(yùn)行增大httpd配置中stack_size設(shè)備運(yùn)行幾小時(shí)后重啟供電不足或PSRAM Cache bug檢查電源線(xiàn)壓降編譯時(shí)加-mfix-esp32-psram-cache-issue標(biāo)記手機(jī)瀏覽器打不開(kāi)/streamWiFi連接不穩(wěn)定或端口被占用確認(rèn)設(shè)備IP地址正確電腦和手機(jī)在同一網(wǎng)段路由器關(guān)閉AP隔離表格里的這些現(xiàn)象前四個(gè)是硬件層面的后四個(gè)是軟件層面的。在實(shí)際調(diào)試時(shí)我建議先解決硬件問(wèn)題再調(diào)軟件否則軟件怎么調(diào)都看不到正確結(jié)果。特別是白屏、花屏這類(lèi)問(wèn)題如果攝像頭輸出信號(hào)不對(duì)后面所有的圖像處理代碼都是白搭。5.2 幀率提升的真實(shí)調(diào)優(yōu)過(guò)程項(xiàng)目初期這套系統(tǒng)的實(shí)際幀率只有8-9幀看起來(lái)非??āMㄟ^(guò)三步優(yōu)化最終穩(wěn)定到了20幀以上。整個(gè)過(guò)程很有參考價(jià)值。第一步確認(rèn)瓶頸在CPU還是在內(nèi)存帶寬。在代碼里加一個(gè)簡(jiǎn)單的幀率計(jì)數(shù)器用millis()計(jì)算每秒處理的幀數(shù)。結(jié)果顯示在屏幕刷新和WiFi推流同時(shí)開(kāi)啟時(shí)幀率只有8幀而單獨(dú)關(guān)閉屏幕刷新后幀率能到20幀這說(shuō)明瓶頸在屏幕刷新這條鏈路上。第二步優(yōu)化屏幕刷新的傳輸方式。TFT_eSPI庫(kù)默認(rèn)是阻塞式傳輸即每次發(fā)送一幀數(shù)據(jù)都要等SPI發(fā)送完成才能做別的。我改成了DMA方式把圖像數(shù)據(jù)先放到DMA緩沖區(qū)然后啟動(dòng)DMA傳輸CPU不等待繼續(xù)做下一幀的解碼和縮放。這一步優(yōu)化之后幀率提升到了14幀左右雖然有效果但還不夠。第三步也是最關(guān)鍵的一步降低縮放分辨率。原來(lái)我把VGA畫(huà)面縮放到了480x320屏幕全屏每幀要傳輸300KB數(shù)據(jù)。改成縮放至320x240后每幀傳輸數(shù)據(jù)量降為150KBSPI傳輸時(shí)間直接減半。同時(shí)我在display_frame函數(shù)里加了一個(gè)判斷如果上一幀還沒(méi)傳完就跳過(guò)當(dāng)前幀保證每幀都是完整顯示而不是覆蓋顯示。這一步完成后幀率直接到了20幀以上。這三步的優(yōu)化思路其實(shí)很通用先找熱點(diǎn)再優(yōu)化傳輸方式最后降低數(shù)據(jù)量。5.3 一個(gè)容易忽略的坑WiFi信號(hào)穩(wěn)定性最后一個(gè)想重點(diǎn)提醒的坑是WiFi信號(hào)在金屬門(mén)體上的衰減。我一開(kāi)始直接把開(kāi)發(fā)板裸奔放在室內(nèi)側(cè)手機(jī)在同一個(gè)房間看畫(huà)面很流暢。但裝進(jìn)外殼、掛到門(mén)上之后發(fā)現(xiàn)推流經(jīng)??DWiFi信號(hào)強(qiáng)度從-45dBm降到了-70dBm。原因是金屬門(mén)對(duì)WiFi信號(hào)有很強(qiáng)的屏蔽作用而ESP32-S3的PCB天線(xiàn)方向是可變的裝進(jìn)外殼后天線(xiàn)位置剛好被金屬門(mén)遮擋。解決方法是把開(kāi)發(fā)板天線(xiàn)端朝下安裝同時(shí)外殼內(nèi)部避免使用金屬支撐柱盡量用塑料螺絲。如果門(mén)體是全金屬的信號(hào)衰減非常嚴(yán)重這時(shí)候只能把WiFi天線(xiàn)外置——部分S3開(kāi)發(fā)板自帶IPEX天線(xiàn)座可以外接一根2.4G天線(xiàn)貼到門(mén)框內(nèi)側(cè)信號(hào)問(wèn)題就徹底解決了。這個(gè)問(wèn)題的排查思路是在設(shè)備上打印出當(dāng)前WiFi信號(hào)強(qiáng)度WiFi.RSSI()然后不斷調(diào)整設(shè)備位置觀察數(shù)值變化。如果RSSI低于-65dBm推流就會(huì)出現(xiàn)明顯的卡頓和斷流??偟膩?lái)說(shuō)這個(gè)項(xiàng)目做下來(lái)我自己最大的收獲并不是“做出了一個(gè)智能貓眼”這個(gè)結(jié)果而是完整走通了一條“傳感器采集-本地顯示-網(wǎng)絡(luò)推流”的嵌入式開(kāi)發(fā)鏈路。ESP32-S3的大PSRAM、高速SPI和WiFi能力剛好在一個(gè)非常合適的點(diǎn)上把這三件事同時(shí)做好。如果你也照著這個(gè)方案做了一臺(tái)遇到問(wèn)題歡迎在評(píng)論區(qū)交流我看到的都會(huì)回復(fù)。最后分享一個(gè)小技巧在調(diào)試階段把攝像頭的幀率、屏幕刷新幀率、WiFi信號(hào)RSSI這三個(gè)數(shù)據(jù)通過(guò)串口定時(shí)打印出來(lái)每5秒一次可以快速定位整條鏈路到底哪一環(huán)是瓶頸。很多“畫(huà)面卡頓”問(wèn)題看了這三個(gè)數(shù)據(jù)之后基本就能確定是CPU太慢、傳輸太慢還是網(wǎng)絡(luò)太慢接下來(lái)針對(duì)性?xún)?yōu)化就行不用瞎猜。