简介:面向嵌入式视觉学习者的STM32二维码识别工程资料,围绕OV5642摄像头图像采集与二维条码解码场景,适合具备单片机基础、希望实践图像输入与识别流程的开发者参考。压缩包共316个文件,约12.05MB,以C源码和H头文件为主体,包含外设驱动、解码逻辑与工程配置,并带有Keil工程、库文件、编译中间文件及少量说明文档,便于直接打开、编译和对照目录结构梳理模块。已有2445人学习下载,说明该案例在嵌入式图像识别学习中具有一定参考价值。读者可从中了解STM32工程组织、摄像头数据获取、二维码识别程序的基本实现路径,对图像预处理、定位与解码等环节也能形成直观认识,并熟悉编译配置与常见工程文件的用途,适合作为课程设计、毕业设计或自学实验的参考资料。
1. 一块 STM32 加一颗 OV5642,能不能自己认出二维码
产线上一个工位,操作员把贴好码的工件往镜头底下一放,屏幕立刻跳出型号和批次——这套东西如果上工控机加工业相机,成本压不下来,也塞不进那个巴掌大的外壳。很多小设备厂商最后选的路子,是用一块 STM32 单片机加一颗 OV5642 做离线二维码识别:摄像头出图、MCU 自己算、结果走串口,不联网、不上云、不依赖手机。
这条路能走通,靠的是三件事凑齐了:OV5642 能直接输出 RGB565 原始数据,省掉 JPEG 解码这道坎;STM32 的 DCMI 并口加 DMA 能零 CPU 干预地把一帧图搬进内存;再加上一份几百行的轻量二维码解码库,把二值化、定位、纠错全包了。适合做低功耗离线扫码设备、门禁读头、工装夹具识别、或者拿来做图像处理的课程设计。下面按我拆这套东西的顺序,从数据通路讲到能跑起来的代码,再到改起来要注意的地方。
2. 系统结构:一帧图像从镜头到串口的完整通路
2.1 硬件选型:为什么要走 DCMI 并口而不是 SPI 摄像头
先说清楚为什么是 OV5642 这种并口摄像头。市面上常见的 SPI 接口摄像头模块接线少、驱动简单,但带宽是硬伤:QVGA 灰度图一帧就要 76800 字节,SPI 跑到 20MHz 也得三十多毫秒,而且 CPU 全程被占着搬数据,根本没余量做解码。OV5642 是 DVP 并口输出,8 位数据线加 PCLK、VSYNC、HREF 三根同步信号,数据由一个专门的 DCMI 外设接收,配合 DMA 直接落到内存,CPU 在这期间是空闲的。
接线层面有个很容易翻车的细节:OV5642 的数据口是 10 位的,而 STM32 的 DCMI 在 8 位模式下只认 D0-D7。正确的接法是取摄像头的高 8 位 D[9:2] 接到 MCU 的 D[7:0],低两位悬空。如果图省事接了 D[7:0],出来的图像会整体偏暗、颜色发灰——因为丢掉的是高字节,等于把每个像素除以 4。
控制通道是 SCCB,本质就是一个只支持写单字节的 I2C。OV5642 的从机地址是 0x78(写)/0x79(读),寄存器地址是 16 位的,这一点和很多 8 位地址的传感器不一样,移植代码时最容易在这里对不上。时钟方面,OV5642 需要一个 6~27MHz 的外部时钟输入到 XCLK,常见做法是让 MCU 的 MCO 引脚输出 24MHz,省掉一颗有源晶振。
2.2 内存账本:帧缓冲、灰度平面和解码工作区怎么分
这套系统跑不跑得动,一半取决于内存怎么摆。640×480 的 RGB565 一帧是 614400 字节,STM32 片内 SRAM 最大的型号也就 512KB,还不够放一帧。所以外挂 SDRAM 基本是必需品,用 FMC 挂在 Bank2 上,起始地址 0xD0000000。
我一般按下面这张表来分配,宁可多留一点也不要卡着算:
| 用途 | 大小 | 存放位置 | 说明 |
|---|---|---|---|
| 帧缓冲 A | 614400 B | SDRAM | 640×480 RGB565 原始帧 |
| 帧缓冲 B | 614400 B | SDRAM | 双缓冲,换帧时不出撕裂 |
| 灰度平面 | 76800 B | SDRAM | 320×240,交给解码库 |
| 解码工作区 | 约 250 KB | SDRAM | 随分辨率增长,320×240 大概这个量级 |
| 显示显存 | 307200 B | SDRAM | 接了 RGB 屏才有,320×240 RGB888 |
加起来接近 1.9MB,所以板子上至少配 8MB 的 SDRAM 才宽松。要注意的是 SDRAM 是共享带宽的,DCMI 写、LTDC 读、CPU 取指令三方抢同一条总线,配置时把 DCMI 的 DMA 设成增量突发、优先级调高,能明显减少花屏。
2.3 软件分层:驱动层、采集层、算法层、应用层
代码结构上我习惯切成四层,每层只跟相邻层打交道,这样换摄像头或者换解码库时改动范围可控:
project/ ├── drivers/ │ ├── sccb.c # SCCB 读写,只认寄存器和值 │ ├── ov5642.c # 寄存器表 + 初始化流程 │ ├── sdram.c # FMC 时序配置 │ └── uart.c # 结果输出 ├── camera/ │ ├── dcmi.c # DCMI + DMA 配置,只管搬帧 │ └── frame.c # 帧就绪标志、双缓冲切换 ├── qr/ │ ├── preprocess.c # RGB565 → 灰度、降采样、ROI 裁剪 │ ├── qr_engine.c # 解码库的薄封装 │ └── quirc/ # 第三方解码库源码 ├── app/ │ ├── main.c # 主循环,串起采集和解码 │ └── protocol.c # 结果组包、校验、上报 └── board/ └── clock.c # 时钟树、MCO 输出层与层之间的接口就那么几个,定下来之后基本不会改:
| 接口 | 参数 | 返回 | 说明 |
|---|---|---|---|
camera_start(buf, bytes) | SDRAM 缓冲地址、帧字节数 | 0 成功 | 启动连续采集 |
frame_wait(timeout_ms) | 超时毫秒 | 1 有新帧 | 阻塞等一帧完成 |
qr_engine_init(w, h) | 灰度图宽高 | 0 成功 | 分配解码工作区,只调一次 |
qr_scan(gray) | 灰度平面指针 | 识别到的码数量 | 内部完成定位和解码 |
这样分层的好处很直接:想换 OV5640,只动drivers/ov5642.c里的表;想换解码库,只动qr/qr_engine.c;业务侧永远只看到「有几个码、内容是什么」。
3. OV5642 驱动与 DCMI 采集链路:把寄存器配对
3.1 SCCB 时序:16 位寄存器地址的读写实现
SCCB 的时序和 I2C 几乎一样,唯一的差别是它不支持多字节连读,也不支持重复起始之后的批量读。写一个寄存器要发五个字节:从机地址、寄存器高字节、寄存器低字节、数据字节。读则是先写地址再重起始切方向。
#define SCCB_ADDR_W 0x78 /* 7 位地址 0x3C 左移一位 */ #define SCCB_ADDR_R 0x79 /* 写:地址两字节 + 数据一字节,顺序错了摄像头会直接不理你 */ uint8_t sccb_write_reg(uint16_t reg, uint8_t val) { if (i2c_start(SCCB_ADDR_W)) return 1; if (i2c_write_byte(reg >> 8)) goto fail; /* 寄存器索引高 8 位 */ if (i2c_write_byte(reg & 0xFF)) goto fail; /* 寄存器索引低 8 位 */ if (i2c_write_byte(val)) goto fail; i2c_stop(); return 0; fail: i2c_stop(); return 1; } /* 读:写完地址后重起始,只读一个字节,最后发 NACK 告诉从机别再加了 */ uint8_t sccb_read_reg(uint16_t reg, uint8_t *val) { if (i2c_start(SCCB_ADDR_W)) return 1; if (i2c_write_byte(reg >> 8)) goto fail; if (i2c_write_byte(reg & 0xFF)) goto fail; if (i2c_start(SCCB_ADDR_R)) goto fail; /* 重复起始,方向切到读 */ *val = i2c_read_byte(0); /* 0 = 回 NACK */ i2c_stop(); return 0; fail: i2c_stop(); return 1; }i2c_start和i2c_write_byte的返回值我用「非 0 即失败」的约定,方便链式判断。读函数里那个i2c_read_byte(0)的参数是「是否回 ACK」,只有连续读多个字节时前面才回 1。这套代码跑通的标志不是能写进去,而是能读回——上电后先读一下芯片 ID 或者随便一个已知默认值的寄存器(比如 PLL 控制那组),读回来对得上,说明 SCCB 通了,后面才有意义。
3.2 初始化表:分辨率、输出格式、PCLK 分频怎么定
OV5642 的初始化是一张几百行的寄存器表,没有捷径,只能整段从驱动包里搬。我在这里只留几个决定性的位置,讲清楚它们各自管什么,剩下的照抄就行。
typedef struct { uint16_t reg; uint8_t val; } reg_val_t; /* 只截关键项:640×480 / RGB565 / DVP 输出,完整表请整段搬,别手抄 */ static const reg_val_t ov5642_rgb565_640x480[] = { {0x3103, 0x11}, /* 系统时钟先切到外部晶振 */ {0x3008, 0x82}, /* 软复位,写完必须延时,否则后面的写入全丢 */ {0x3017, 0x7F}, /* 输出使能:VSYNC/HREF/PCLK/D[9:0] 全开 */ {0x3018, 0xFC}, {0x3035, 0x21}, /* PLL 控制,和 0x3036 一起决定 PCLK 实际频率 */ {0x3036, 0x54}, /* PLL 倍频系数,改大 PCLK 变快,帧率上去但容易丢数据 */ {0x4300, 0x6F}, /* ISP 输出格式 = RGB565,高字节先出 */ {0x501F, 0x01}, /* ISP 输入源选 DVP 并口 */ {0x4740, 0x21}, /* VSYNC 低有效、HREF 高有效,必须和 DCMI 极性对上 */ {0x503D, 0x00}, /* 测试彩条关掉,调试阶段可以先置 0x80 看通路通不通 */ }; void ov5642_init(void) { for (size_t i = 0; i < sizeof(ov5642_rgb565_640x480) / sizeof(reg_val_t); i++) { sccb_write_reg(ov5642_rgb565_640x480[i].reg, ov5642_rgb565_640x480[i].val); if (ov5642_rgb565_640x480[i].reg == 0x3008) { delay_ms(10); /* 复位后给足稳定时间,这一步省了会调一整天 */ } } /* 输出窗口寄存器在 0x3800 段,具体地址以你手上的驱动包为准 */ ov5642_set_window(640, 480); delay_ms(50); }几个参数值得单独说。0x3035和0x3036是 PLL 的分频和倍频,它决定 PCLK 跑多快。PCLK 太快,DCMI 的 DMA 跟不上会丢行;太慢帧率上不去。640×480 下我一般把 PCLK 压在 12~16MHz,DCMI 时钟配成 HCLK 的一半,留够余量。
0x4300决定输出格式。设成 RGB565 之后,每个像素两个字节,而且在总线上是先高字节后低字节。这一点直接决定了后面灰度化代码里怎么拼像素,也决定了 DCMI 那边要按 16 位宽度去理解数据流。
0x4740是极性控制,它和 DCMI 的VSPolarity、HSPolarity必须是同一套约定。摄像头说 VSYNC 低有效,DCMI 就得配低有效,两边对不上,采集出来的图像会上下错位或者干脆只有半帧。
3.3 DCMI 与 DMA:让搬运这件事不占 CPU
DCMI 的配置本身不复杂,麻烦的是 DMA 那一侧的长度单位和地址递增。
DCMI_HandleTypeDef hdcmi; void dcmi_init(void) { hdcmi.Instance = DCMI; hdcmi.Init.SynchroMode = DCMI_SYNCHRO_HARDWARE; /* 用引脚同步,不解析内嵌码 */ hdcmi.Init.PCKPolarity = DCMI_PCKPOLARITY_RISING; /* 对齐 0x4740 里的 PCLK 极性 */ hdcmi.Init.VSPolarity = DCMI_VSPOLARITY_LOW; /* VSYNC 低有效 */ hdcmi.Init.HSPolarity = DCMI_HSPOLARITY_HIGH; /* HREF 高有效 */ hdcmi.Init.CaptureMode = DCMI_CAPTURE_MODE_CONTINUOUS; hdcmi.Init.ExtendedDataMode = DCMI_EXTEND_DATA_8B; /* 并口只接 D[9:2] */ if (HAL_DCMI_Init(&hdcmi) != HAL_OK) { Error_Handler(); } } #define FRAME_W 640 #define FRAME_H 480 #define FRAME_BYTES (FRAME_W * FRAME_H * 2) /* RGB565,每像素两字节 */ void camera_start(uint8_t *buf) { /* 注意:最后一个参数的单位是 32 位字,不是字节 */ HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)buf, FRAME_BYTES / 4); }ExtendedDataMode选 8 位,是因为硬件上只接了 8 根数据线。但摄像头一个像素吐两个字节,DCMI 会把这两个字节拼成一个 16 位字写进内存——这个拼接是按小端来的,也就是先收到的字节放在低地址。而摄像头给的是高字节先出,两者一凑,内存里每个像素的高低字节是反的。这不算错,只是约定,后面灰度化时按正确的顺序取位就行,不用额外做字节交换。
FRAME_BYTES / 4这个除四是 HAL 层的约定,DMA 的 NDTR 寄存器本身按字节计,但 HAL 内部会把数据宽度设成 word,长度参数就跟着变成字数。写成FRAME_BYTES的后果是 DMA 只搬了四分之一的图,屏幕上四分之三是噪点,这个坑我在第 5 章还会再提一次。
3.4 一帧的时间账:帧率到底卡在哪
把链路配通之后,先别急着接解码库,算一笔账能省下很多盲目调参。
| 环节 | 数值 | 说明 |
|---|---|---|
| 单帧数据量 | 614400 B | 640×480×2 |
| 单帧 PCLK 周期数 | 614400 | 每像素两个时钟 |
| PCLK = 12MHz 时 | 约 51 ms | 未计消隐,实际约 60 ms |
| PCLK = 16MHz 时 | 约 38 ms | 实际约 45 ms |
| 灰度降采样到 320×240 | 约 76800 次写 | CPU 侧,几十毫秒量级 |
| 解码 320×240 | 几十毫秒 | 主频越高越快,M7 明显优于 M4 |
也就是说,即使在 M7 上,整套链路跑到 8~10 帧每秒是比较现实的目标;用 M4 的话,解码那一环会明显拖后腿,通常只能做到 2~3 帧,得靠「只在检测到运动时解码」或者「只处理 ROI 区域」来提速。这个账算明白,就不会盲目去调 PLL 倍频想着上 30 帧。
4. 预处理与解码:把 640×480 压到能算的规模
4.1 RGB565 转灰度:定点系数和 2×2 降采样一起做
640×480 的彩色图对二维码解码没有价值,颜色信息只是噪声。所以第一件事是转灰度,顺便做 2×2 的盒式降采样,一次循环同时完成两件事,省掉一遍内存遍历。
/* src: RGB565 原始帧(高字节在前),dst: 输出灰度平面,尺寸为源图的一半 */ void rgb565_to_gray_half(const uint8_t *src, uint8_t *dst, int src_w, int src_h) { const int dst_w = src_w / 2, dst_h = src_h / 2; for (int y = 0; y < dst_h; y++) { const uint8_t *r0 = src + (size_t)(2 * y) * src_w * 2; const uint8_t *r1 = src + (size_t)(2 * y + 1) * src_w * 2; uint8_t *out = dst + (size_t)y * dst_w; for (int x = 0; x < dst_w; x++) { uint32_t acc = 0; for (int dy = 0; dy < 2; dy++) { const uint8_t *row = dy ? r1 : r0; for (int dx = 0; dx < 2; dx++) { /* 高字节在前:先取 src[0] 当高 8 位 */ uint16_t px = ((uint16_t)row[(2*x+dx)*2] << 8) | row[(2*x+dx)*2 + 1]; uint32_t r = (px >> 11) & 0x1F; /* 5 位红 */ uint32_t g = (px >> 5) & 0x3F; /* 6 位绿 */ uint32_t b = px & 0x1F; /* 5 位蓝 */ /* 5/6/5 位左移补到 8 位,再用 BT.601 的定点系数 77/150/29 */ acc += ((r << 3) * 77 + (g << 2) * 150 + (b << 3) * 29) >> 8; } } out[x] = (uint8_t)(acc >> 2); /* 四个像素取平均 */ } } }系数 77/150/29 加起来正好是 256,所以右移 8 位就是标准的定点加权平均,不用浮点,也不会有溢出。四个像素累加后右移 2 位得到均值,这样降采样出来的灰度图比直接抽点要平滑,二维码的黑白模块边界不会因为抽样恰好落在边缘而糊掉。
调用时源尺寸必须和 DCMI 配置的一致,rgb565_to_gray_half(frame_buf, gray, 640, 480),输出就是 320×240。这个尺寸和后面quirc_resize传的宽高必须严格对齐,否则解码库会越界读,表现是随机崩溃,很难查。
4.2 解码库接入:工作区只初始化一次
解码这一层我推荐用社区里流传的那份几百行的轻量库(搜 quirc 能直接拿到源码),它把自适应二值化、连通域分析、三个定位图案的查找、透视校正和 Reed-Solomon 纠错全做完了,纯 C、无依赖,加起来不到三千行。相比把 ZXing 那一套 C++ 移植过来,它在 MCU 上的内存占用和代码体积都友好得多。
#include "quirc.h" #define QR_W 320 #define QR_H 240 static struct quirc *qr; int qr_engine_init(void) { qr = quirc_new(); if (!qr) return -1; /* 工作区按分辨率一次分配好,绝不能每帧重来,否则堆碎片迟早咬人 */ if (quirc_resize(qr, QR_W, QR_H) < 0) { quirc_destroy(qr); qr = NULL; return -2; } return 0; }quirc_new只创建句柄,真正的大块内存是quirc_resize时分配的,320×240 大概需要两百多 KB,具体随内部结构浮动。这个量级放进片内 SRAM 会挤掉其他东西,所以我一般把它指到 SDRAM。如果用的是带内存管理的外部 SRAM,记得把分配器的堆开大到 512KB 以上。
还有一点:quirc_resize在运行时重新调用会重新分配,理论上支持,但每帧调一次必然产生碎片。正确做法就是开机初始化一次,之后只换quirc_begin返回的那块灰度平面内容。
4.3 主循环:一帧的完整处理流程
主循环的逻辑很朴素,但要处理好几件事:等帧、填灰度、解析、上报,还要统计失败次数方便现场排查。
volatile uint8_t frame_ready = 0; /* DCMI 的帧中断回调里置位,别在这里做任何耗时操作 */ void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *h) { frame_ready = 1; } void qr_task_step(void) { if (!frame_ready) return; frame_ready = 0; int w = 0, h = 0; uint8_t *gray = quirc_begin(qr, &w, &h); /* 拿到灰度平面指针 */ rgb565_to_gray_half(frame_buf, gray, 640, 480); /* 尺寸必须和 resize 一致 */ quirc_end(qr); /* 触发内部二值化和定位 */ int n = quirc_count(qr); for (int i = 0; i < n; i++) { struct quirc_code code; struct quirc_data data; quirc_extract(qr, i, &code); if (quirc_decode(&code, &data) == QUIRC_SUCCESS) { report_payload(data.payload, data.payload_len); } else { stat_decode_fail++; /* 定位成功但解不出,多半是模糊或反光 */ } } }quirc_begin返回的是内部灰度平面,宽高通过指针带出来。有的版本会把它 resize 到调用时传的尺寸,所以填图之前最好断言一下w == QR_W && h == QR_H。quirc_end完成的是阈值化和连通域分析,这一步是整条链路里最耗时的,320×240 下占掉解码总时间的六七成。
quirc_count返回的是这一帧里检测到的候选码数量,注意它是候选,不一定能解出内容——扫到一半的码、被遮挡的码都会算进去。所以quirc_decode的返回值必须判断,QUIRC_SUCCESS才是真的成功,其余几种错误码(格式不对、校验失败、版本不支持)在调试时打印出来很有用。
上报部分我一般组一个简单的帧:起始字节、长度、内容、校验和、结束字节。串口速率 115200 下,一个几十字节的二维码内容加上帧头帧尾,一帧也就几毫秒,不会成为瓶颈。
5. 避坑实录:五条让工程跑不起来的常见问题
5.1 图像整体偏绿、人脸像外星人
现象:接上屏或者把灰度图打印出来,颜色明显不对,红色物体发暗,绿色过饱和。
原因:字节序。OV5642 输出 RGB565 时先发高字节,而 DCMI 在 8 位模式下按小端拼成 16 位字,先收到的字节落在低地址。如果你按「低地址是低字节」去取r = px >> 11,实际拿到的红蓝是互换的。
解决:拼像素的时候明确写成px = (buf[2i] << 8) | buf[2i+1],把先到的字节当高 8 位。另一个办法是改0x4300让摄像头低字节先出,但我倾向于在软件里处理,因为换摄像头时这个约定可能又变,代码里显式写出来至少一眼能看懂。
5.2 只搬了四分之一帧,后面全是上一帧的残留
现象:屏幕上只有左上角一小块是新的,其余部分像是上上帧的画面,或者花屏。
原因:HAL_DCMI_Start_DMA的长度参数单位是 32 位字,不是字节。传了 614400 进去,实际只会搬 614400 个字,早就越界写到别的内存去了,表现出来就是乱七八糟。
解决:长度统一按帧字节数 / 4传,并且加一句编译期断言:_Static_assert(FRAME_BYTES % 4 == 0, "frame bytes must be word aligned");。另外帧缓冲的起始地址也要 4 字节对齐,放 SDRAM 里通常天然满足,但结构体里包一层就要检查。
5.3 SCCB 全部返回成功,但一点图像都没有
现象:初始化函数跑完没报错,VSYNC 上用示波器看也没有信号,DCMI 的帧中断一次都不进。
原因通常有三个。一是寄存器地址被当成 8 位写,OV5642 是 16 位寄存器索引,少发一个字节,所有配置都写到了错误的位置;二是软复位之后没延时,紧接着的写入全被丢掉,但 SCCB 硬件层面照样回 ACK,所以你看到的是「成功」;三是 PWDN 和 RESET 两个引脚没拉对电平,芯片根本没上电。
解决:先读回验证,找一个有确定默认值的寄存器读一下,对不上就别往下走。复位后强制delay_ms(10)以上。PWDN 拉低、RESET 先拉低再拉高,中间留 5ms。这三点都过了再去看 PCLK 有没有输出。
5.4 能框出二维码位置,但一直解不出内容
现象:调试时能看到检测框在图上的位置是对的,quirc_count返回大于 0,quirc_decode一律失败。
原因:分辨率不够是头号嫌疑。二维码的每个模块在图像里至少要占 3 个像素才可能稳定识别,一个最小的版本 1 二维码是 21×21 模块,加上静区大概 29 个模块宽,在 320 像素宽的图里最多也就占满 1/3 画面。如果码在画面里偏小,降采样之后模块就糊成一片了。
解决:优先让码在画面里占大一点(换镜头或者靠近),或者干脆不做 2×2 降采样,直接把 640×480 的灰度图喂进去,代价是解码时间翻几倍。其次是检查宽高比,如果摄像头窗口设置和实际输出不一致,图像会被拉伸,正方形的定位图案变成矩形,透视校正就会失败。
5.5 跑几分钟就死机,重启后又正常
现象:连续跑十几分钟,突然卡死或者花屏,复位后能继续跑一段时间。
原因多半在内存。一是解码工作区放进了片内 SRAM,把主栈挤爆了,表现是随机的 HardFault;二是quirc_resize被放在了循环里,每帧重新分配几百 KB,堆很快就碎了;三是 SDRAM 带宽被 LTDC 抢完,DCMI 的 DMA 拿不到总线,帧同步丢失之后 DCMI 不会自动重启。
解决:把解码工作区的堆指到 SDRAM,主栈保持在片内;quirc_resize只在初始化时调一次;LTDC 的层优先级调低,DCMI 的 DMA 通道优先级调到最高;再在帧中断里加一个超时检测,超过 500ms 没有新帧就HAL_DCMI_Stop再重新Start,用软件把 DCMI 捞回来。
6. 二次开发:换场景时改哪几处、怎么验证
这套东西真正落地时,业务需求千差万别,但改动点其实集中在几个地方,我按改动成本从低到高排一下。
| 改动需求 | 动哪个文件 | 具体操作 | 风险 |
|---|---|---|---|
| 换二维码内容格式 | app/protocol.c | 只改组包和校验 | 无 |
| 提高识别距离 | drivers/ov5642.c | 换更长焦距的镜头,或提高输出分辨率 | 解码时间上升 |
| 提高帧率 | qr/preprocess.c | 只处理画面中央 ROI | ROI 外的码扫不到 |
| 换 OV5640 等同类传感器 | drivers/ | 换寄存器表,改窗口寄存器地址 | 极性和字节序要重验 |
| 加补光/触发 | app/main.c | 帧就绪后拉 GPIO | 无 |
其中 ROI 裁剪是最划算的优化,改起来就几行:
/* 只把画面中央 320×240 的区域送进解码器,四周留边给定位图案 */ void rgb565_to_gray_roi(const uint8_t *src, uint8_t *dst, int src_w, int off_x, int off_y, int roi_w, int roi_h) { for (int y = 0; y < roi_h; y++) { const uint8_t *row = src + (size_t)(y + off_y) * src_w * 2 + off_x * 2; for (int x = 0; x < roi_w; x++) { uint16_t px = ((uint16_t)row[x*2] << 8) | row[x*2 + 1]; uint32_t r = (px >> 11) & 0x1F, g = (px >> 5) & 0x3F, b = px & 0x1F; dst[(size_t)y * roi_w + x] = (uint8_t)(((r << 3) * 77 + (g << 2) * 150 + (b << 3) * 29) >> 8); } } }off_x、off_y是 ROI 在原始帧里的左上角坐标,roi_w、roi_h是裁剪后的尺寸,也是quirc_resize要用的尺寸。注意裁剪后没有降采样,像素密度反而更高,小码的识别率会上去,代价是解码耗时增加,需要自己权衡。
验证方法我固定这么走:先在0x503D打开测试彩条,确认 DCMI 搬进来的图是规则的八色彩条,这一步排除接线和时钟问题;再关彩条,把灰度图通过串口按 8 位原始数据吐出来,用上位机当图片看,确认图像方向和宽高比正确;最后才接解码库,并且先拿一张打印得很大的标准码在 20cm 内测,能从 100% 识别率开始往下退,才知道边界在哪。
说个我自己踩过的教训:早期调这套东西时,我图省事把灰度缓冲和帧缓冲放在了同一块 SDRAM 的相邻地址,DMA 在写当前帧的时候,CPU 正在读上一帧算灰度,两个地址一重叠,出来的图偶尔会有一道横条。当时以为是摄像头坏了,换了两颗芯片才发现是自己地址算错。从那以后我每次分配缓冲都强制走一遍「地址区间打印 + 重叠检查」,宁可多花十分钟,也不要在这儿赌运气。希望帮到你。
本文还有配套的精品资源,点击获取