1. 为什么要在STM32F103上跑Zbar?从零开始的思考
你可能已经玩过不少STM32的项目了,点亮个屏幕、驱动个摄像头、读个传感器,这些都不在话下。但有没有想过,让这块小小的单片机去“看懂”一张二维码?这听起来像是把一头大象塞进冰箱,感觉有点勉强。我刚开始接触这个需求时也是这么想的,市面上明明有成品的二维码识别模块,I2C或者串口一发一收,结果就出来了,多省事啊。干嘛非要自己折腾,把PC上用的Zbar库往资源紧张的STM32里塞呢?
我后来想明白了,也正是在几个实际项目里踩了坑,才坚定了移植Zbar这条路。首先,成本控制是硬道理。一个外置的识别模块,少则几十,多则上百,而STM32F103这颗芯片本身才多少钱?对于量产的消费类产品,每一分钱都得精打细算。其次,是系统集成度和灵活性。外挂模块意味着多一块电路板、多一组接线、多一个供电考虑,也多了潜在的故障点。把识别算法集成到主控MCU内部,整个产品结构更紧凑,可靠性理论上也更高。更重要的是,数据流的掌控权完全在自己手里。从摄像头采集到图像,到预处理,再到识别解码,整个流程你都可以深度优化和干预,比如针对特定照明环境调整图像二值化阈值,这是黑盒模块做不到的。
所以,当项目对成本、体积和可控性都有要求时,在STM32F103上移植Zbar就从一个“炫技”的想法,变成了一个切实可行的工程选择。STM32F103C8T6(也就是常说的“蓝桥杯”最小系统板核心芯片)拥有72MHz的主频、20KB的RAM和64KB的Flash,而更高级的ZET6型号资源更丰富。Zbar库本身经过精心裁剪和优化后,是可以在这样的资源环境下跑起来的。这个过程就像是在小户型里做极致收纳,虽然挑战不小,但一旦搞定,那种成就感和带来的产品优势是非常实在的。接下来,我就带你一步步实现这个“收纳”过程。
2. 动手之前:你的开发环境与物料清单
工欲善其事,必先利其器。在开始敲代码之前,咱们得先把摊子支起来。这里我假设你已经有一定的STM32开发经验,会用Keil MDK或者IAR,也玩过一两个简单的裸机或RTOS项目。如果没有,建议先点个灯、串口打印个“Hello World”热热身。
2.1 硬件准备:你需要哪些家伙事儿
硬件是算法的舞台,舞台没搭好,戏没法唱。下面这个清单是我实际跑通项目用到的,你可以根据手头资源灵活调整:
- 主控芯片:STM32F103系列,首选
STM32F103ZET6或STM32F103VET6。为什么?因为RAM大。Zbar解码过程需要缓冲区存放图像数据,RAM越大越从容。ZET6有64KB RAM,VET6有48KB,而C8T6只有20KB,会比较吃力,需要更极致的优化。我的演示主要以ZET6为例。 - 摄像头模块:
OV7670带FIFO的版本。这是关键中的关键。一定要选带AL422B这类FIFO芯片的!因为OV7670输出像素数据很快,STM32F103如果不用DMA,直接去读会跟不上,导致图像撕裂。FIFO相当于一个蓄水池,摄像头把数据快速写进去,MCU可以慢慢读出来,完美解决速度匹配问题。不带FIFO的OV7670调试起来会非常痛苦,新手强烈不推荐。 - 显示屏:
TFT-LCD屏幕,分辨率至少240x320,用于实时显示摄像头画面和识别结果。SPI接口的屏速度慢,建议用FSMC驱动的并口屏,刷屏流畅,调试时直观很多。 - 外部SRAM(可选但强烈推荐):
IS62WV51216这类512KB的SRAM芯片。为什么推荐?因为一帧QVGA(320x240)的灰度图,就需要320*240 = 76,800字节,约75KB。这已经超过了大多数F103芯片的内部RAM大小。有了外部SRAM,你可以把整帧图像、各种缓冲区都放进去,内部RAM只留给栈、堆和关键变量,系统会稳定得多。很多开发板(如正点原子战舰板)已经集成了这个芯片。 - 基础外设:USB转串口模块(用于调试打印)、若干按键、LED指示灯。这些是调试时的“眼睛”和“手指”,必不可少。
把以上硬件按照电路图连接好,确保摄像头、屏幕、SRAM都能被正常驱动。你可以先分别跑通摄像头采集显示、SRAM读写测试这些基础例程,确保硬件底层没问题。
2.2 软件准备:代码仓库与核心库
软件方面,我们需要准备好三样东西:Zbar库的移植版本、一个可靠的malloc内存管理实现、以及你的工程框架。
- Zbar库源码:原始的Zbar库是为PC设计的,直接拿来用肯定不行。幸运的是,开源社区的前辈们已经做了大量移植工作。我们不需要从头造轮子。你可以搜索“ZBar-MDK”或者参考我项目里使用的版本。这个版本通常已经对代码进行了裁剪,去掉了不必要的依赖(如
<stdlib.h>、<stdio.h>),并将内存分配、打印输出等函数替换为了STM32可实现的接口。 - 内存管理:Zbar库内部会调用
malloc和free来动态分配内存。在嵌入式系统中,直接使用标准库的malloc往往不可靠,容易产生内存碎片。这里我强烈推荐移植正点原子提供的内存管理函数。它实现了分块式的内存池管理,效率高且稳定。我们需要两个池:一个用于内部RAM(SRAMIN),一个用于外部SRAM(SRAMEX)。代码在正点原子的标准例程里可以找到。 - 工程框架:你需要一个完整的STM32工程,包含正确的启动文件、芯片支持包、以及驱动摄像头、LCD、FSMC(用于驱动LCD和外部SRAM)的底层代码。如果你用的是常见的开发板,厂家提供的例程就是最好的起点。
我的开源项目stm32f103zet6-qrcode-detect里,已经把这些组件整合好了。你可以把它作为参考模板,但更重要的是理解每一部分是如何衔接的。直接复制粘贴虽然快,但出了问题你可能会懵。
3. 核心攻坚战:Zbar库的移植与瘦身
这是整个项目最核心、也最具挑战性的一步。我们的目标是把一个为“大房子”(PC)设计的库,搬进我们的“小公寓”(STM32)里,还要保证它能正常工作。
3.1 文件结构与移植要点
拿到为MDK优化过的Zbar库源码后,你会发现它主要包含以下几个文件夹:zbar、qrcode、decoder等。首先,把这些文件夹整个复制到你的MDK工程目录下,并在IDE中添加相应的源文件(.c)和头文件路径。
移植的关键在于替换库中对操作系统和标准C库的依赖。你需要重点关注以下几个文件:
-
zbar/config.h:这个文件是配置的总开关。里面有很多#define宏,用于启用或禁用特定功能。对于STM32F103,我们要做极限瘦身。通常可以关闭以下功能来节省代码空间和内存:#define ENABLE_EAN 0 // 禁用EAN条码(我们只要二维码) #define ENABLE_CODE128 0 // 禁用CODE128 #define ENABLE_I25 0 // 禁用交叉25码 // ... 其他非二维码的编码类型都可以关掉 #define ZBAR_FIXED 1 // 使用定点数运算,代替浮点数,速度更快只保留
QRCODE相关的定义。这样一裁剪,库的体积会小很多。 -
内存分配替换:Zbar库内部会调用
malloc、free、calloc、realloc。我们需要实现这些函数,并指向我们自己的内存池。例如,在zbar/system.c文件(或者类似的系统抽象层文件)中,你会找到这样的函数定义:void *zbar_malloc (size_t size) { //return malloc(size); // 注释掉标准库版本 return mymalloc(SRAMEX, size); // 使用外部SRAM池分配 } void zbar_free (void *ptr) { //free(ptr); myfree(SRAMEX, ptr); }这里我统一使用外部SRAM池(
SRAMEX)来分配,确保大块图像数据有地方放。内部RAM池(SRAMIN)留给系统关键任务。 -
输出函数替换:Zbar库中的调试输出
printf,需要重定向到你的串口。这通过重写_write或fputc函数实现,这是STM32开发的常规操作,此处不再赘述。
3.2 图像接口适配:告诉Zbar数据在哪
Zbar识别需要一个图像对象。我们需要创建一个zbar_image_t,并把我们的图像数据“喂”给它。这是调用Zbar的核心步骤,我把它封装成了一个函数Zbar_Test。
// 假设 image_buffer 是存放灰度图像数据的数组,位于外部SRAM
extern uint8_t image_buffer[240*240];
int Zbar_Test(int width, int height) {
zbar_image_scanner_t *scanner = NULL;
zbar_image_t *image = NULL;
// 1. 创建扫描器
scanner = zbar_image_scanner_create();
if (!scanner) {
printf("扫描器创建失败!\r\n");
return -1;
}
// 2. 配置扫描器(启用所有配置,或针对性配置)
zbar_image_scanner_set_config(scanner, 0, ZBAR_CFG_ENABLE, 1);
// 3. 创建图像对象
image = zbar_image_create();
zbar_image_set_format(image, *(int*)"Y800"); // 设置格式为灰度图
zbar_image_set_size(image, width, height); // 设置图像尺寸
// 关键:关联数据!注意最后一个参数是释放数据的回调,我们用自己的myfree
zbar_image_set_data(image, (void*)image_buffer, width * height, zbar_image_free_data);
// 4. 执行扫描!
int n = zbar_scan_image(scanner, image);
printf("检测到 %d 个二维码\r\n", n);
// 5. 遍历并输出结果
const zbar_symbol_t *symbol = zbar_image_first_symbol(image);
for(; symbol; symbol = zbar_symbol_next(symbol)) {
zbar_symbol_type_t type = zbar_symbol_get_type(symbol);
const char *data = zbar_symbol_get_data(symbol);
printf("类型: %s, 内容: %s\r\n", zbar_get_symbol_name(type), data);
// 这里你可以把数据存起来,或者通过无线模块发送出去
}
// 6. 清理现场,释放内存!非常重要!
zbar_image_destroy(image);
zbar_image_scanner_destroy(scanner);
return n; // 返回识别到的个数
}
这个函数就是Zbar工作的核心流程。你需要传入图像的宽度和高度,并确保image_buffer里存放的是正确的灰度图像数据(Y800格式)。每个像素一个字节,0代表黑,255代表白。
3.3 避坑指南:我踩过的那些雷
移植过程不会一帆风顺,分享几个我遇到的典型问题:
- 内存对齐问题:STM32F103对非对齐的内存访问会触发硬件错误(HardFault)。确保你从外部SRAM分配的内存地址是4字节对齐的。正点原子的
mymalloc通常已经处理了对齐,但如果你自己写分配函数要特别注意。 - 栈溢出:Zbar的一些函数调用链可能比较深,容易导致栈溢出。在启动文件(
startup_stm32f10x_hd.s)中,适当增大栈空间(Stack_Size),比如从0x400增加到0x800或更大。 - 初始化顺序的“巨坑”:这是我原文章里强调的,这里再强调一遍。FSMC(用于驱动LCD和外部SRAM)和内存池的初始化,必须放在其他外设初始化之后! 具体来说,
FSMC_SRAM_Init()、my_mem_init(SRAMIN)、my_mem_init(SRAMEX)这几句代码,要放在LCD_Init()、OV7670_Init()等函数之后。如果放在前面,后续一些外设的初始化操作可能会意外写入SRAM区域,导致程序跑飞。这个bug非常隐蔽,我调试了大半天才找到原因。 - 图像格式必须为灰度:Zbar的
zbar_image_set_format函数要求输入Y800格式。如果你的摄像头输出是RGB565,必须先转换成灰度图。一个简单但有效的转换公式是:Gray = (R*30 + G*59 + B*11) / 100。你也可以直接取绿色通道(G)的值,因为人眼对绿色最敏感。
4. 从摄像头到识别:构建完整数据流水线
现在Zbar库已经能在你的工程里编译通过了,但它还需要“粮食”——图像数据。这一步,我们要把摄像头、内存、LCD和Zbar识别模块串联起来,形成一条自动化的流水线。
4.1 图像采集与预处理:二值化的艺术
OV7670带FIFO的驱动,通常的工作流程是:配置好摄像头寄存器 -> 等待帧同步信号 -> 触发FIFO读使能 -> 循环读取FIFO数据到缓冲区。读出来的数据通常是RGB565格式。
如前所述,Zbar需要灰度图。但我们甚至可以进行更极端的优化:直接二值化。因为二维码本身就是黑白的,在光照条件可控的情况下(比如我们的手持设备自带补光灯),二值化图像不仅能大幅减少数据量(1个像素只用1位,理论上),还能提升Zbar的识别速度和抗噪能力。我用的方法非常“粗暴”但有效:
// 假设从FIFO读出的一个像素点是rgb565颜色值
uint16_t rgb_color = OV7670_ReadFIFO();
// 提取RGB分量(简化版)
uint8_t r = (rgb_color >> 11) & 0x1F;
uint8_t g = (rgb_color >> 5) & 0x3F;
uint8_t b = rgb_color & 0x1F;
// 转换为近似灰度值(扩大到0-255范围)
uint8_t gray = (r*249 + g*123 + b*63) / 435; // 一个快速的近似算法
// 二值化:根据一个阈值判断黑白
#define THRESHOLD 128 // 这个阈值需要根据实际光照调整
uint8_t binary_pixel = (gray > THRESHOLD) ? 255 : 0;
// 存入最终的图像缓冲区,准备送给Zbar
image_buffer[i++] = binary_pixel;
这个THRESHOLD阈值是关键。我项目里用的0x4500是针对RGB565原始值的经验阈值。你可以通过实验确定:在典型使用环境下,拍摄一张清晰的二维码,统计图像中心区域的灰度值分布,取一个中间值作为阈值。更高级的做法是采用动态阈值(比如大津法),但这对STM32F103的计算能力有一定要求。
4.2 主循环设计:状态机让流程更清晰
不建议把所有代码都堆在main函数的while(1)里。一个好的实践是使用一个简单的状态机来管理识别流程,这样逻辑清晰,也便于后期添加功能(比如识别成功后的声光提示、数据发送)。
typedef enum {
STATE_IDLE, // 空闲状态
STATE_CAPTURING, // 正在捕获图像
STATE_PROCESSING, // 图像处理中(二值化)
STATE_RECOGNIZING, // Zbar识别中
STATE_DISPLAY_RESULT // 显示结果
} SystemState_t;
SystemState_t sys_state = STATE_IDLE;
void main(void) {
// 所有硬件初始化(注意顺序!)
// ... GPIO, USART, Systick, LCD, OV7670, FSMC_SRAM, my_mem_init ...
uint16_t width = 240;
uint16_t height = 240;
while(1) {
switch(sys_state) {
case STATE_IDLE:
// 等待一个触发信号,比如按键按下
if(Trigger_Button_Pressed()) {
sys_state = STATE_CAPTURING;
}
break;
case STATE_CAPTURING:
// 启动一次图像捕获
OV7670_CaptureFrame(image_raw_buffer); // 存到原始缓冲区
sys_state = STATE_PROCESSING;
break;
case STATE_PROCESSING:
// 将RGB565原始数据转换为二值化灰度图
convert_rgb565_to_binary(image_raw_buffer, image_buffer, width*height);
sys_state = STATE_RECOGNIZING;
break;
case STATE_RECOGNIZING:
// 调用Zbar识别函数
int qr_count = Zbar_Test(width, height);
if(qr_count > 0) {
// 识别成功,可以点亮LED,蜂鸣器响一下
LED_On();
Buzzer_Beep();
// 获取识别结果(在Zbar_Test函数内部已打印,这里可以存储)
} else {
// 识别失败
LED_Off();
}
sys_state = STATE_DISPLAY_RESULT;
break;
case STATE_DISPLAY_RESULT:
// 在LCD上显示结果或原图,延迟一段时间
delay_ms(1000);
sys_state = STATE_IDLE; // 回到空闲,等待下一次触发
break;
}
}
}
这样的结构清晰易懂,哪个环节出问题了也容易定位。你可以把图像捕获和预处理放到中断里,主循环只负责调度和识别,进一步提高效率。
4.3 性能优化与调试技巧
在STM32F103上跑图像算法,性能是必须关注的点。一帧240x240的图像有57600个像素,每个像素都要经过RGB转灰度、二值化处理,计算量不小。
- 降低分辨率:如果识别距离固定且较近,完全可以降低摄像头输出分辨率,比如降到160x120。数据量变为原来的1/4,处理速度和识别成功率可能反而提升。
- 局部识别:如果二维码在画面中的位置大致固定,可以只截取画面中心区域送去识别,进一步减少数据量。
- 使用DMA:如果摄像头支持,使用DMA将FIFO数据直接搬运到内存中,可以极大解放CPU。
- 定时识别而非连续识别:如果不是需要实时扫描,可以设置每500ms或1秒识别一次,避免CPU一直满负荷运转。
调试方面,串口打印是你的最佳伙伴。在关键步骤(如内存分配前后、图像处理前后)打印内存使用量、函数执行时间(用系统滴答计时),可以帮助你快速定位性能瓶颈和内存泄漏。LCD屏幕则可以实时显示摄像头画面和二值化后的图像,直观地判断图像质量是否达标。
5. 项目进阶:让二维码识别更实用
基础功能跑通后,我们可以思考如何让它变成一个真正可用的产品功能。
5.1 集成无线传输:让数据飞起来
项目标题里有“无线手持”,这意味着识别结果需要发送出去。我们可以很容易地集成一个蓝牙模块(如HC-05/06)或Wi-Fi模块(如ESP8266)。
// 在识别成功后的STATE_RECOGNIZING或STATE_DISPLAY_RESULT状态中
if(qr_count > 0) {
const zbar_symbol_t *sym = zbar_image_first_symbol(image);
const char *qr_data = zbar_symbol_get_data(sym);
// 通过串口发送给无线模块
printf("AT+SEND=%s\r\n", qr_data); // 假设是自定义AT指令
// 或者通过蓝牙串口直接发送
Bluetooth_SendString(qr_data);
}
你需要根据所选无线模块的通信协议(通常是串口AT指令)编写相应的发送函数。这样,手持设备识别到二维码后,就能将内容实时发送到手机或电脑的上位机软件。
5.2 设计一个简单的交互界面
在LCD屏幕上可以显示更多信息,提升用户体验:
- 待机界面:显示“就绪”或电池电量。
- 识别中界面:显示一个对焦框或“识别中...”提示。
- 识别成功界面:显示“√”和识别出的文本前几个字符。
- 识别失败界面:显示“×”或提示“未识别到,请重试”。
通过几个按键可以实现模式切换、手动触发识别、历史记录查看等功能。
5.3 应对复杂环境:提升鲁棒性
在实际使用中,光线不均、二维码污损、部分遮挡都是常见问题。除了调整二值化阈值,还可以在软件上做一些增强:
- 多次采样取平均:连续捕获3-5帧图像,分别识别,取出现次数最多的结果作为最终结果,避免单次误判。
- 图像预处理滤波:在二值化前,对灰度图进行简单的中值滤波或均值滤波,可以去除一些小噪点。当然,这需要额外的计算开销。
- 识别区域提示:在LCD上画一个方框,提示用户将二维码对准该区域,确保二维码在画面中大小合适、正对摄像头。
最后,关于内存管理,在整个项目开发中要养成好习惯:谁申请,谁释放。确保Zbar_Test函数中zbar_image_destroy和zbar_image_scanner_destroy被正确执行。定期使用my_mem_perused(SRAMEX)函数检查外部SRAM的利用率,防止内存泄漏。当所有这些模块稳定地协同工作时,你会看到一个由STM32F103驱动的小小设备,流畅地完成图像采集、处理、识别和解码的全过程,那种把复杂算法塞进有限资源的MCU并成功运行的满足感,正是嵌入式开发的乐趣所在。


被折叠的 条评论
为什么被折叠?



