試實(shí)戰(zhàn))
簡介面向嵌入式視覺與條碼識別場景的開發(fā)者該工程以 STM32F407VET6 為主控搭配 OV2640 攝像頭實(shí)現(xiàn) 640×480 分辨率 RGB 圖像的采集并通過自定義協(xié)議將數(shù)據(jù)上傳至上位機(jī)完成一維碼/二維碼解碼適合需要快速搭建攝像頭傳輸鏈路或系統(tǒng)學(xué)習(xí) HAL 庫驅(qū)動的中級開發(fā)者。壓縮包共 244 個文件以 37 個 C 源碼、62 個頭文件為核心同時(shí)包含編譯過程產(chǎn)生的 .o/.d 中間文件、.ld/.map 鏈接腳本、makefile、CubeMX 配置 .ioc以及 2 個可直接燒錄的 .bin 固件整體僅 9.26MB工程結(jié)構(gòu)完整便于理解從源碼到固件的完整編譯流程。已有 811 人學(xué)習(xí)下載。工程覆蓋串口、USB、定時(shí)器等外設(shè)初始化與中斷處理可配合作者提供的上位機(jī)解碼軟件直接驗(yàn)證圖像采集到解碼的整套流程若需二次開發(fā)也能基于現(xiàn)有協(xié)議調(diào)整分辨率、幀率或更換傳輸方式有效節(jié)省底層搭建時(shí)間。 你把這個叫 STM32F407VET6_OV2640_Barcode_Common.rar 的壓縮包解開了。里面沒有 readme也沒有像樣的二次開發(fā)文檔只有幾個看起來眼熟的工程目錄、一堆 .c 和 .h 文件以及某個藏在深處的 BarcodeLib。這不是你第一次從網(wǎng)上下 STM32 例程了但這次的需求有點(diǎn)特殊你想讓 OV2640 攝像頭在 F407 上跑出條碼/二維碼識別結(jié)果而不是僅僅把圖像搬到 LCD 上顯示。這個包其實(shí)正是一條可行的路徑也是我今天想拆開講清楚的東西。F407VET6 屬于 STM32F4 系列里的高性價(jià)比型號主頻 168MHz帶硬件 DCMI 攝像頭接口192KB SRAM、1MB Flash用來跑中等分辨率的圖像采集綽綽有余OV2640 是一顆 200 萬像素的 CMOS 傳感器能直接輸出 JPEG、RGB565、YUV422 等格式非常適合做嵌入式視覺。把兩者組合起來做條碼識別等于是在一顆低功耗 MCU 上解決“掃碼”這件事不依賴 Linux 板卡也不依賴專用掃碼模組。這篇內(nèi)容適合正在搞 STM32 攝像頭項(xiàng)目、想在 F407 上實(shí)現(xiàn)條碼解碼或者單純想搞懂這類 RAR 工程模板到底怎么用的人。1. 一個 RAR 壓縮包的命名里藏著怎樣的工程分層先看名字STM32F407VET6_OV2640_Barcode_Common.rar。這類命名方式在量產(chǎn)項(xiàng)目和培訓(xùn)例程里都特別常見它不是在隨意堆關(guān)鍵詞而是直接標(biāo)明了三件事。第一主控平臺是 STM32F407VET6LQFP100 封裝512KB Flash、192KB SRAM帶 DCMI屬于 F407 系列里性價(jià)比比較均衡的一顆。第二圖像采集設(shè)備是 OV2640它的輸出格式、分辨率、幀率全部可以通過 SCCB 總線動態(tài)配置。第三Barcode 是應(yīng)用層目標(biāo)也就是最終要識別一維碼或二維碼而 Common 通常表示整個工程里可復(fù)用的公共代碼層一般會包含時(shí)鐘初始化、串口 printf、延時(shí)函數(shù)、內(nèi)存池、低通濾波這些跟業(yè)務(wù)無關(guān)的底層模塊。解壓后你大概率會看到這幾個目錄層次Project存放 Keil MDK 或 IAR 的工程文件芯片型號選擇 STM32F407VET6或者正點(diǎn)原子風(fēng)格拆分HARDWARE 目錄下放著 CAMERA 驅(qū)動OV2640 初始化、SCCB 讀寫、DCMIDMA 采集SYSTEM 目錄下放著 sys、delay、usart 等公共基礎(chǔ)文件BarcodeLib 或 Decode這部分是核心算法層常見的是移植了 Quirc、ZBar 的裁剪版或者自己實(shí)現(xiàn)的二值化 定位 解碼流程Common 目錄存放上面說的那些“跟攝像頭和條碼無關(guān)但每個工程都離不開”的基礎(chǔ)函數(shù)。為什么要把 Common 單獨(dú)提出來因?yàn)樽鲞@套工程的人很清楚攝像頭驅(qū)動會改、條碼算法會換、LCD 顯示、SD 卡存儲這些外設(shè)功能也會不斷調(diào)整唯獨(dú)時(shí)鐘、串口、內(nèi)存管理是穩(wěn)定不變的。把這些東西從業(yè)務(wù)代碼里抽出來單獨(dú)維護(hù)本質(zhì)上是在為后面的項(xiàng)目復(fù)用做準(zhǔn)備。你拿到這個包之后不應(yīng)該急著去翻 BarcodeLib 里的解碼細(xì)節(jié)而應(yīng)該先搞清楚 Common 層初始化了什么因?yàn)檫@決定了后邊的延時(shí)是否準(zhǔn)確、串口能不能正常打日志、DMA 內(nèi)存從哪里分配。另外一個需要注意的隱藏點(diǎn)是很多這類 RAR 包是從某個完整 Demo 里抽出來的文件名帶 Common 往往意味著它不是一個可直接燒錄的最終程序而是一個“基礎(chǔ)框架 部分功能模塊”的整合體。如果你的版本沒有帶完整 example那你需要自己寫 main 函數(shù)里的業(yè)務(wù)循環(huán)初始化攝像頭、抓一幀圖像、調(diào)用解碼函數(shù)、打印結(jié)果。這反而是件好事因?yàn)槟惚黄劝颜麄€流程看了一遍而不是直接編譯跑完就完事。2. F407VET6 與 OV2640 的硬件協(xié)作DCMI 總線、SCCB 控制與引腳分配聊完目錄結(jié)構(gòu)接下來要面對硬件層面的協(xié)作關(guān)系。F407VET6 和 OV2640 之間不是隨便接幾根線就能工作的它們之間實(shí)際上有兩條通道。第一條是控制通道叫 SCCB也就是簡化版的 I2C。OV2640 的所有寄存器配置——分辨率、輸出格式、曝光、增益、白平衡——都通過這條兩線總線寫入。F407 側(cè)可以使用硬件 I2C也可以直接用 GPIO 模擬。我的建議是 GPIO 模擬原因很簡單硬件 I2C 在 F407 上的時(shí)鐘配置、中斷優(yōu)先級和錯誤處理比較繁瑣而 SCCB 本身速率要求不高400kHz 以內(nèi)就能跑得很好GPIO 模擬反而更加可控出問題時(shí)也容易用邏輯分析儀抓時(shí)序。第二條是數(shù)據(jù)通道叫 DCMI。F407 內(nèi)置的數(shù)字?jǐn)z像頭接口是一個 8 位并行總線接口支持外部像素時(shí)鐘同步還帶有內(nèi)嵌碼和外部行場同步兩種模式。OV2640 輸出數(shù)據(jù)時(shí)會同時(shí)給出 PCLK像素時(shí)鐘、VSYNC幀同步、HREF行同步DCMI 把這些信號拾取后將像素?cái)?shù)據(jù)塞進(jìn) DMA直接寫入內(nèi)存整個過程不需要 CPU 干預(yù)。這正是嵌入式視覺項(xiàng)目的核心優(yōu)勢攝像頭在持續(xù)“拍照”而 CPU 可以同時(shí)做別的事直到一幀圖像真正到達(dá)內(nèi)存后再觸發(fā)中斷去處理。典型的引腳分配如下表所示這套分配方式是參考了市面上最常見的 STM32F407 開發(fā)板接口比如探索者系列和野火系列都采用類似布局功能信號名推薦引腳DCMI 數(shù)據(jù)D0~D6PC6、PC7、PC8、PC9、PC10、PC11、PC12DCMI 數(shù)據(jù)D7PD3像素時(shí)鐘PCLKPA6行同步HREFPA4幀同步VSYNCPB7攝像頭主時(shí)鐘XCLKPA8輸出 24MHz/12MHzSCCB 時(shí)鐘SIOCPB3SCCB 數(shù)據(jù)SIODPB4這個表里有兩個容易被忽略的細(xì)節(jié)。第一VSYNC 用的是 PB7而很多人的第一直覺是把 PB7 留給 I2C1 的 SDA如果你在同一個工程里還要用硬件 I2C 掛別的傳感器引腳沖突立刻就會出現(xiàn)所以這里通常用 GPIO 模擬 SCCB 來避開 PB6/PB7 的 I2C 復(fù)用問題。第二XCLK 必須是攝像頭的工作主時(shí)鐘OV2640 可以接受 6MHz 到 27MHz 的輸入時(shí)鐘常用 24MHz 或 12MHzF407 的 MCO1 引腳 PA8 可以直接輸出 HSE 分頻后的時(shí)鐘省掉一顆外部有源晶振。順便回應(yīng)一個搜索這個標(biāo)題時(shí)經(jīng)常出現(xiàn)的關(guān)聯(lián)問題STM32F407VET6 集成 PHY 嗎答案是不集成。F407 內(nèi)部只有以太網(wǎng) MAC物理層收發(fā)器 PHY 是外面另外接一顆芯片常見的像 LAN8720A、DP83848 都是外掛方案。這個細(xì)節(jié)跟攝像頭、條碼識別沒有直接關(guān)系但它說明一個事實(shí)F407 是一顆外設(shè)很全、但很多外設(shè)都需要外部配套芯片的 MCU。真正屬于芯片內(nèi)部集成的“視覺外設(shè)”就是 DCMI所以你在這個項(xiàng)目里不需要額外加任何圖像采集芯片F(xiàn)407 的 DCMI OV2640 就是一套完整的數(shù)字?jǐn)z像頭解決方案。3. 初始化 OV2640寄存器配置順序、輸出格式與 LENC 增益表OV2640 是一顆非?!俺耘渲谩钡膫鞲衅魃想娭笏J(rèn)輸出的圖像格式和幀率很難直接滿足條碼識別需求必須通過 SCCB 寫寄存器把它設(shè)置到合適的模式。這也是整個工程里最繁瑣、最容易出問題的一步。初始化流程通常是這樣的上電后延時(shí) 20ms 左右等待 Sensor 內(nèi)部電路穩(wěn)定通過 SCCB 寫入 OV2640 的 bank寄存器組切換命令比如 0xFF0x01 切換到 DSP bank0xFF0x00 切回 sensor bank設(shè)置輸出窗口、分辨率、像素時(shí)鐘分頻設(shè)置輸出格式JPEG、RGB565、YUV422 三選一設(shè)置幀率常見 15fps 或 25fps打開或關(guān)閉自動曝光、自動白平衡配置亮度、飽和度、對比度。這里我要重點(diǎn)展開的是輸出格式選擇因?yàn)樗苯記Q定后面內(nèi)存占用和解碼難度。如果你只做一維碼識別比如 EAN-13、Code128OV2640 輸出灰度圖或者 YUV422 是最直接的做法因?yàn)闂l碼本身就是黑白條紋不需要彩色信息。但在 F407 只有 192KB SRAM 的情況下一張 640*480 的 YUV422 圖像就需要約 614KB640×480×2這一張圖就直接超出內(nèi)存容量必須縮小到 320×240也就是 QVGA 分辨率才能緩沖得下。而 QVGA 對于一維碼來說通常夠用只要條碼在畫面里占夠?qū)挾冉獯a成功率并不差。如果你要做的是二維碼比如掃微信支付碼、設(shè)備銘牌二維碼建議改用 JPEG 輸出。因?yàn)槎S碼的圖案更致密像素信息損失對解碼影響更大需要更高分辨率但高分辨率的 RGB565 內(nèi)存又扛不住。JPEG 的好處是 OV2640 內(nèi)部有硬件編碼器能將同樣一幀畫面壓縮到二十分之一的體積VGA 分辨率的 JPG 常見大小是 30KB 到 80KB放在 192KB SRAM 里完全可以接受。代價(jià)是 MCU 側(cè)要再跑一個 JPEG 解碼器把壓縮數(shù)據(jù)還原成灰度圖后再喂給條碼識別算法。流程就變成DCMIDMA 抓取 JPEG 數(shù)據(jù)流存入內(nèi)存CPU 調(diào)用 TjpgDec 等嵌入式 JPEG 解碼器將 JPEG 解碼到一塊臨時(shí)灰度緩沖二值化后交給 Quirc 或 ZBar 進(jìn)行二維碼定位與解碼。這條路是很成熟的代價(jià)就是解碼耗時(shí)更長但換來了更高的分辨率和更好的識別率。再說 LENC 增益表。搜索這個項(xiàng)目時(shí)經(jīng)常關(guān)聯(lián)到 “ov2640 的 lenc 增益表”這其實(shí)是 Omnivision 傳感器內(nèi)部的一組鏡頭校正寄存器。LENC 的主要作用是修正鏡頭邊緣造成的亮度衰減和色彩偏移OV2640 內(nèi)部提供了兩組校正表一組是 Sensor 默認(rèn)內(nèi)置的固定表另一組是用戶可以通過 SCCB 寫入的自定義表。工程代碼里常會看到 0x44、0x45、0x46 這類寄存器出現(xiàn)在初始化序列中就是對 LENC 增益進(jìn)行調(diào)節(jié)。如果你發(fā)現(xiàn)拍出來的圖像中央?yún)^(qū)域正常、四周明顯偏暗或者色彩在邊緣處偏色多半是 LENC 增益表沒調(diào)對。但如果你用的是市面上的現(xiàn)成 OV2640 模塊一般不需要手動改 LENC模塊默認(rèn)配置已經(jīng)夠用。只有在自己做板子、換鏡頭、或者低照度環(huán)境仍然偏色時(shí)才需要去逐個修改增益表寄存器。還有一個容易踩的坑OV2640 的寄存器很多要分 bank 訪問修改 LENC 前沒有切到正確的 bank改完發(fā)現(xiàn)根本不生效這是最常見的問題。4. 在 F407 上跑通條碼識別算法選型、內(nèi)存博弈與性能調(diào)優(yōu)Barcode 識別是這套工程的核心也是從“能出圖”到“能掃碼”最關(guān)鍵的一跳。在 F407 這種 MCU 上做條碼識別算法選型首先要考慮的不是識別率最高而是內(nèi)存占用和計(jì)算量能不能壓進(jìn)這顆 168MHz 的芯片里??蛇x方案大致有三類第一類是嵌入式專用解碼庫比如 Quirc。Quirc 對二維碼支持好代碼量小數(shù)據(jù)結(jié)構(gòu)可以靜態(tài)分配是 F407 上移植二維碼識別最常用的庫第二類是 ZBar 的裁剪版。ZBar 對一維碼支持比 Quirc 好庫體積偏大需要自己裁剪掉不必要的碼制能控制在可接受范圍第三類是商業(yè)識別 SDK像 Dynamsoft Barcode Reader 也有面向嵌入式或 Linux 的版本識別率和碼制覆蓋最全適合產(chǎn)品量產(chǎn)場景按官方渠道申請授權(quán)即可。我自己最常用的組合是一維碼走 ZBar 裁剪版二維碼走 Quirc。整個解碼流程可以用下面這段偽代碼概括// 假設(shè)已經(jīng)通過 DCMIDMA 抓到了一幀 JPEG 數(shù)據(jù) uint8_t *jpg_buf; // JPEG 數(shù)據(jù)緩沖區(qū) uint32_t jpg_len; // JPEG 數(shù)據(jù)長度 uint8_t gray_buf[W * H]; // 解碼后的灰度圖緩沖區(qū) // JPEG 解碼TjpgDec 輸出灰度圖 jd_prepare(jd, jpg_buf, jpg_len); jd_decode(jd, gray_buf, W, H, 0); // 二值化 定位 解碼 quirc_resize(q, W, H); uint8_t *image quirc_begin(q, w, h); threshold_binarize(gray_buf, image, W * H); quirc_end(q); // 遍歷檢測到的二維碼 int count quirc_count(q); for (int i 0; i count; i) { struct quirc_code code; struct quirc_data data; quirc_extract(q, i, code); if (quirc_decode(code, data) 0) { printf(QR Code: %s\n, data.payload); } }這段流程看起來簡單真正動手時(shí)會發(fā)現(xiàn)內(nèi)存和時(shí)間的博弈非常現(xiàn)實(shí)。拿 VGA 分辨率舉例灰度圖需要 640×480 307200 字節(jié)也就是 300KB這已經(jīng)超出了 192KB SRAM。所以你要么把 JPEG 解碼目標(biāo)分辨率降到 QVGA灰度緩沖變成 76KB要么使用 F407 的外部 SDRAM。但很多 F407VET6 板子沒有外部存儲器總線或者沒有布線所以更穩(wěn)妥的方案是固定使用 QVGA 灰度圖來解碼畫面夠不夠清楚用鏡頭和補(bǔ)光來彌補(bǔ)。時(shí)間開銷上也要有心理準(zhǔn)備。OV2640 輸出 JPEG、DMA 搬運(yùn)一幀只占很少的 CPU 時(shí)間真正的耗時(shí)大頭是 JPEG 解碼。TjpgDec 在 F407 這種 M4 內(nèi)核上解碼一幀 QVGA JPEG 大約需要 100 到 250ms解碼質(zhì)量配置會影響這個數(shù)字Quirc 在 QVGA 圖上查找和解碼二維碼大約需要 20 到 80ms。整體算下來每幀的處理時(shí)間大約在 150 到 350ms 之間換算成“一秒掃兩三幀”是合理預(yù)期。如果這個速度滿足不了需求建議優(yōu)先優(yōu)化 JPEG 解碼器的工作內(nèi)存或者降低分辨率到 320×240 以下而不是盲目去改主頻。解碼庫移植過程中還有幾個非常容易坑人的地方。第一Quirc 的 quirc_begin / quirc_end 接口中間必須確保圖像緩沖區(qū)是灰度圖不是 RGB 也不是二值圖很多人直接喂二值圖導(dǎo)致找不到定位角點(diǎn)其實(shí) Quirc 內(nèi)部會自己做梯度計(jì)算提前二值化反而破壞了信息。第二ZBar 如果裁剪不當(dāng)編譯后 Flash 占用會輕松超過 100KB建議只保留要識別的碼制像 EAN13、QR 這種按需打開。第三條碼識別對圖像清晰度極其敏感OV2640 輸出的 JPEG 如果壓縮率太高會出現(xiàn)色塊和邊緣模糊解碼率直線下降。這時(shí)候要檢查寄存器里的 JPEG 質(zhì)量配置而不是一味怪算法不行。5. 調(diào)試過程中真正難纏的問題噪聲、幀同步與解碼失敗代碼都寫完了合上 Keil連接 ST-Link下載復(fù)位結(jié)果屏幕上黑屏、花屏、或者串口打出來的全是解碼失敗。這類問題我調(diào)試了無數(shù)次把最常見的幾個坑記錄下來每個都對應(yīng)一組快速排查手段。先看 SCCB 總線。OV2640 模塊上電后如果 SCCB 讀寫不正常你連寄存器 ID 都讀不回來。最典型的表現(xiàn)是讀寄存器一直返回 0xFF 或者 0x00。排查順序應(yīng)該是先確認(rèn)模塊供電VCC 和 GND 有沒有接反很多攝像頭模塊絲印上 D0 和 D7 之間還藏著一個電源 LED燈亮不代表供電正常要用萬用表量然后確認(rèn) SIOC 和 SIOD 引腳是否接對了這類模塊經(jīng)常要求上拉電阻如果你的模塊沒有板載上拉那么省略了外部上拉也會導(dǎo)致通信失敗。GPIO 模擬 SCCB 時(shí)時(shí)序必須把起始條件、停止條件、ACK 響應(yīng)這些細(xì)節(jié)寫完整我見過不少人把 stop 信號省掉結(jié)果每次寫完寄存器后總有幾個字節(jié)丟數(shù)據(jù)。再查幀同步。DCMI 采集圖像最詭異的現(xiàn)象是畫面正常但每隔幾幀就出現(xiàn)一條條紋斷裂或者圖像“錯位半邊”。這通常是因?yàn)?VSYNC 信號的處理方式不對。DCMI 有兩種啟動方式一種是硬件 VSYNC 邊沿觸發(fā)另一種是軟件開啟連續(xù)采集。幀中斷在圖像還在傳輸途中就被觸發(fā)再開啟下一次 DMA就會踩到上一幀的尾部數(shù)據(jù)。建議使用 DMA 雙緩沖模式配合 VSYNC 中斷在安全時(shí)刻切換緩沖區(qū)同時(shí)把 DMA 傳輸完成中斷的優(yōu)先級調(diào)成比幀中斷更高這樣能避免一半新幀一半舊幀的拼接問題。然后是畫面質(zhì)量。如果圖像已經(jīng)出來了但條碼識別率很低先不要懷疑算法用 LCD 或者串口把圖像導(dǎo)出來看一眼。常見的現(xiàn)象是畫面過曝條碼黑色區(qū)域和白色區(qū)域?qū)Ρ榷炔蛔?。OV2640 的自動曝光在大面積白色背景下會表現(xiàn)得很激進(jìn)比如掃白色底色的二維碼時(shí)容易把畫面整體提亮導(dǎo)致碼的黑色模塊發(fā)灰。解決方法是設(shè)置 AE 鎖定或者把曝光目標(biāo)值調(diào)低一點(diǎn)。另一個現(xiàn)象是運(yùn)動模糊手持條碼對著攝像頭晃動時(shí)解碼率下降嚴(yán)重這是因?yàn)閹侍推毓鈺r(shí)間又長。這時(shí)候把幀率拉到 15fps 以上盡量讓補(bǔ)光充足而不是一條路降分辨率。還有一個容易忽略的點(diǎn)是灰度二值化閾值。二維碼解碼并不需要你把整幅圖處理成干凈的黑白圖Quirc 內(nèi)部有自己的算法但如果你是自己寫的一維碼定位程序二值化的閾值就非常重要。全局固定閾值在環(huán)境光照變化時(shí)基本必死最好在大津法或者自適應(yīng)局部閾值中選一個。實(shí)測下來全局大津法在均勻光照下效果很好處理時(shí)間也短但如果畫面里同時(shí)存在高光和陰影就得用分塊自適應(yīng)閾值代價(jià)是處理時(shí)間會增加不少。最后說一個 ZBar 移植的細(xì)節(jié)。ZBar 默認(rèn)會掃描整幅圖像尋找條碼方向如果沒有提前做粗定位計(jì)算量會非常嚇人。我建議的做法是先對灰度圖做一次垂直邊緣密度統(tǒng)計(jì)把畫面里存在大量豎條紋的區(qū)域標(biāo)記為候選條碼區(qū)域然后只對候選區(qū)域執(zhí)行完整解碼。這個方法能省掉三分之一以上的計(jì)算時(shí)間尤其在 F407 這種 MCU 上效果立竿見影。根據(jù)我個人經(jīng)驗(yàn)這個工程跑通之后你順手還能得到一套不錯的圖像采集基礎(chǔ)框架OV2640 驅(qū)動、DCMI 采集、JPEG 解碼、內(nèi)存管理都是現(xiàn)成的之后想擴(kuò)展成二維碼識別器、顏色識別傳感器、或者簡單的視覺分揀模塊都只需要在這個框架上換算法層硬件層完全不用動。最后再分享一個細(xì)節(jié)Naming 里的 Common 目錄里的 delay 和 printf 一定別隨手刪掉調(diào)試時(shí)串口日志和時(shí)間基準(zhǔn)全都指著它們我見過太多人為了省編譯時(shí)間清掉 Common結(jié)果后面排查問題時(shí)兩眼一抹黑又灰溜溜地加回來。這種看似“不重要的公共代碼”恰恰是整個工程的保命繩。本文還有配套的精品資源點(diǎn)擊獲取