瑞芯微(EASY EAI)RV1126B 二维码生成
做嵌入式开发久了就会发现,真正让你卡壳的往往不是高大上的AI算法,而是“显示一个二维码”这种不起眼的小需求。我在用瑞芯微EASY EAI评估板做视觉项目时,就遇到了这么个需求:设备启动后需要在屏幕上弹出一个二维码,手机扫码就能完成配网和设备绑定。刚开始想着这不是现成的开源库一调就完事吗?结果真上手才发现,RV1126B这套平台想生成一个能用的二维码,中间隔着交叉编译、图像输出、内存管理、显示刷新一系列坎。这篇文章就把我在EASY EAI RV1126B上从零搞定二维码生成的完整过程拆开讲,包括库怎么选、交叉编译怎么配、代码怎么写、板端怎么验证,以及那些不跑一遍根本发现不了的坑。
如果你也正在用RV1126B或类似的瑞芯微平台做带屏或带摄像头的产品,这篇内容应该能帮你省下至少两三天折腾时间。就算你不用瑞芯微,只要是在Linux嵌入式环境下做二维码生成,里面关于库的选型逻辑、framebuffer显示、中文字符容量这些经验同样可以直接抄作业。
1. 为什么需要在板端生成二维码——设备配网与信息展示的硬需求
很多人第一反应是:二维码这种东西,手机端、服务器端生成好了再传给设备不就行了?确实,纯展示场景完全可以这样干。但当我们把场景落到RV1126B的实际项目里,就会发现“在板端本地生成”不是矫情,而是现实需求。
1.1 设备初始化场景:没有网络的时候怎么办
智能摄像头、门禁机、工业采集终端这类设备,第一次上电时通常处于“未配网”状态。这时候设备没有IP地址,没有网络连接,更不可能连上云端去拉取“二维码图片”。让用户在网页端生成二维码再想办法传给设备?对于批量出货的场景完全不现实。正确流程是:设备本地根据自身的唯一序列号、临时密钥、设备类型等信息,在开机时动态生成一个二维码,显示在屏幕或者叠加到预览画面上,用户拿手机App扫码,App把二维码里的设备信息解析出来,再引导设备完成Wi-Fi配网或者账号绑定。
这个场景决定了二维码必须“现场生成”,内容还必须包含设备当前状态相关的动态信息,比如一次性随机Token。Token每次开机都变,没法提前烧录在固件里,只能靠板端实时算。
1.2 不带屏设备的信息输出:摄像头画面叠码
RV1126B更多时候是带摄像头的视觉主板,不带HDMI也不带MIPI屏幕。那用户怎么看到二维码?最常见的做法是把二维码直接叠加到摄像头预览画面上——设备开机后进入AP配网模式,此时摄像头采集到的画面会叠加上一个二维码,用户打开手机App扫描画面中的二维码就能绑定设备。
这种场景在智能家居摄像头、门铃、婴儿监护器上非常常见。二维码需要跟视频流一起编码推流出去,整个过程都在板端完成,关键是指挥视频处理单元把二维码图像叠加到视频帧上。所以二维码不光是“生成了”,生成完之后还要有办法在板内流转到下一个模块去。
1.3 和服务器端生成相比,本地生成带来的实际收益
服务器端生成二维码的方案听上去省事,但实际会引入一连串问题:设备必须保证在线、服务器必须保证稳定、二维码内容涉及设备密钥时还得加密传输。而且很多设备部署在局域网甚至隔离网环境里,云端根本碰不到它。本地生成方案把这一整条链路砍掉了:设备自己知道自己的身份信息,自己生成二维码,完全离线可用,数据不出设备,安全性上反而更好。
成本方面也很现实。增加一个云端二维码生成接口,意味着要开发和维护对应的接口文档、考虑接口限流、处理高并发;而本地生成几乎零成本,只要在固件里塞一个开源库就行。
2. 二维码生成方案选型——为什么最终选了libqrencode而不是zxing-cpp
确定要本地生成二维码之后,接下来就是选库。我当时的候选方案有三个:libqrencode、zxing-cpp、还有纯手写QR编码器。这里做个对比,也顺便说一下各自的定位差异。
| 候选方案 | 主要语言/依赖 | 生成能力 | 识别能力 | 嵌入式适配难度 | 许可证 |
|---|---|---|---|---|---|
| libqrencode | C,可选依赖libpng | 只生成 | 不支持 | 低,交叉编译容易 | LGPL |
| zxing-cpp | C++,依赖较多 | 生成+识别 | 支持 | 中高,需要处理C++标准库 | Apache 2.0 |
| 纯手写QR编码 | 任意 | 实现复杂 | 不支持 | 极高,仅适合学习 | 自主可控 |
2.1 libqrencode的优势:单一职责、依赖干净
libqrencode做的事情非常纯粹:输入一段字符数据,通过QR编码算法,输出一个二进制矩阵,告诉你在哪个位置放黑色点、哪个位置放白色点。它不关心PNG编码,也不关心屏幕刷新,这些全由你自己来管。这种“单一职责”的设计在嵌入式环境下太舒服了——出了问题你一定知道问题出在哪一层,不用担心库内部的黑魔法。
另外它的依赖极其干净。默认不装libpng的话,连第三方库都不需要。我用的版本只需要标准C库就能编译通过,这对交叉编译来说是巨大优势。
2.2 为什么不用zxing-cpp
zxing-cpp确实很强,生成识别一把抓,而且是C++模板库,代码优雅。但问题也出在这里:第一,它依赖较新的C++标准库特性,RV1126B的交叉编译工具链虽然支持C++11/14,但有些SDK的glibc版本比较旧,编译时容易出现标准库头文件冲突;第二,zxing-cpp体积偏大,编译完的动态库比我整个业务代码还要胖不少;第三,它的主要精力其实放在二维码识别上,生成只是顺带功能,我只需要生成,没必要为一个用不上的识别功能引入一堆模板代码。
2.3 纯手写QR编码器:我只在调试时干过一次
网上确实有极简的QR编码器实现,几百行代码就能跑。我调试阶段试过一次,当你想支持中文、支持UTF-8、支持不同纠错级别、保证各种扫描软件都能秒识别的时候,纯手写就有点顶不住了。QR编码涉及的掩码选择、格式信息计算、版本信息BCH码,每个细节搞错一点,生成的二维码扫不出来你都不知道该从哪里查起。结论是:除非你的目标是学习协议本身,做产品直接上成熟开源库,把时间省下来干正事。
2.4 libqrencode的许可证:LGPL在实际项目中的处理
用开源库之前我习惯先看许可证。libqrencode用的是LGPL,这意味着我可以把它作为一个独立动态库跟我的闭源程序链接,只要做到“用户可以替换这个库”就行。对嵌入式Linux设备来说,动态链接是常规操作,所以问题不大。但要注意一点:如果你图省事把libqrencode静态编译进主程序,LGPL会要求你的整体程序以LGPL兼容的方式提供,这在商业产品里就可能是个麻烦。我在项目里直接把它编成libqrencode.so,业务模块通过标准链接方式引用,许可证问题就彻底绕开了。
3. 交叉编译libqrencode到RV1126B平台的一步步实操
选型拍板之后,真正开始干活的第一关就是交叉编译。RV1126B的SDK一般会带一个完整工具链,不同的评估板SDK位置略有不同,但逻辑是一致的。
3.1 找到工具链并设置环境变量
我用的EASY EAI SDK里,工具链名字大致形如arm-rockchip-linux-gnueabihf-或arm-rockchip810-linux-gnueabihf-,具体以你SDK为准。先定位到编译器所在的bin目录,然后把环境变量导出来:
export PATH=$PATH:/path/to/sdk/prebuilts/gcc/linux-x86/arm/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin export CROSS_COMPILE=arm-linux-gnueabihf-设置好之后,用arm-linux-gnueabihf-gcc -v验证一下工具链是否可用。这里的“arm-linux-gnueabihf”具体前缀取决于你的SDK,有的是arm-rockchip-linux-gnueabihf,别照抄,先看目录里实际有什么。
3.2 配置与编译libqrencode
libqrencode使用标准的autotools构建体系,交叉编译的参数也比较经典:
./configure --host=arm-linux-gnueabihf --prefix=$PWD/install --without-tools --without-tests make -j8 make install这里关键选项是--host指定目标架构。--without-tools是去掉它自带的命令行工具,那个工具在板端用不上,还省掉一堆依赖;--without-tests则是跳过测试程序编译。
我试过单独编一个动态库其实不需要--prefix也能出.so文件,但加上--prefix之后头文件、库文件都被规整到一个目录下,后面给业务代码交叉编译时引用起来很方便。
3.3 libpng依赖:要编还是不要编
libqrencode如果启用PNG输出,需要系统里有libpng。RV1126B的SDK本身可能已经带了libpng,但版本未必跟你的编译环境匹配。这里有个取舍:生成“二维码矩阵”其实完全不需要PNG,只有当你打算直接把二维码存成图片文件时才有必要。我在板端是直接把二维码矩阵渲染到framebuffer或叠加到视频帧上,根本不需要PNG格式,所以编译libqrencode时直接禁用PNG支持,这样连libpng都省了,依赖上彻底干净。
如果你确实需要PNG输出,在configure时加--with-png,并且确保交叉编译环境下能拿到libpng的头文件和库路径。这个需求一般发生在你要把二维码文件保存到TF卡供售后调试使用的情况,正常功能开发用不上。
3.4 把编译产物拷到板端跑通的验证方法
交叉编译完成后,生成的libqrencode.so拷贝到板子的/usr/lib目录,或者使用SDK的rootfs打包工具将它打进固件。验证是否可用的最笨但最有效的方式就是:
# 板端执行 ldconfig # 刷新动态库缓存 ldd /usr/bin/your_app | grep qrencode能看到类似libqrencode.so.4 => /usr/lib/libqrencode.so.4的输出,就说明动态库链接正常。这个检查一定要在板端做,因为交叉编译环境下链接成功不代表板端运行环境里有这个库。
4. 核心代码实现——从字符串到屏幕图像的完整调用链
库编译好了,接下来就是写业务代码。我在项目里封装了一个模块,把二维码生成的细节全藏在几个接口后面,业务层只管给字符串、拿图像数据。
4.1 调用libqrencode生成二维码矩阵
libqrencode核心API非常简单,整个调用过程像三句话:
#include <qrencode.h> QRcode *qrcode = QRcode_encodeString( "your_text", // 要编码的内容 3, // 版本号,0表示自动选择最小版本 QR_ECLEVEL_M, // 纠错级别,M级是常用折中 QR_MODE_8, // 编码模式,8代表字节模式 1 // 是否启用中文/日文模式标记,1表示使用UTF-8字节流 ); if (qrcode == NULL) { // 编码失败,常见原因是UTF-8模式下中文内容过长超出容量 return -1; }这里重点说一下几个参数的含义。版本号传0时,库会根据输入数据长度自动选择合适的QR版本,版本越高的二维码点阵越大,但信息容量也越大。纠错级别有L/M/Q/H四个档,L容量最大但抗污损能力最差,H反之。我实际测试下来,用在摄像头画面叠加这种场景,建议至少用M级,因为视频编码压缩本身就会给二维码图像引入噪声,H级虽然更保险但生成的图更密,反而对扫码距离要求更高,M级是识别率最均衡的选择。
QRcode_encodeString返回的结构体里有两个关键字段:width代表二维码矩阵的边长像素点(每个“点”在内存里是1字节0或1),data指向width*width个字节的矩阵数组。注意这个矩阵只包含QR码本体,四周一圈白边默认是1个点的宽度,这个细节后面说显示时再细说。
4.2 生成灰度位图并做边界扩展
QR码识别算法要求码的四周必须有静区(quiet zone),通常建议至少4个点的白边。libqrencode默认在矩阵外围留了1个点的白边,直接缩放显示就会导致扫码识别率下降。正确做法是自己扩展边界:
#define QUIET_ZONE 4 int scale = 6; // 每个QR点的放大倍数 int img_size = (qrcode->width + QUIET_ZONE * 2) * scale; uint8_t *rgb = malloc(img_size * img_size * 3); // 先全填白色 memset(rgb, 255, img_size * img_size * 3); // 把QR码矩阵放大填充进去 for (int y = 0; y < qrcode->width; y++) { for (int x = 0; x < qrcode->width; x++) { if (qrcode->data[y * qrcode->width + x] & 0x01) { for (int dy = 0; dy < scale; dy++) { for (int dx = 0; dx < scale; dx++) { int px = (x + QUIET_ZONE) * scale + dx; int py = (y + QUIET_ZONE) * scale + dy; rgb[(py * img_size + px) * 3 + 0] = 0; rgb[(py * img_size + px) * 3 + 1] = 0; rgb[(py * img_size + px) * 3 + 2] = 0; } } } } }实际项目里的scale取值不宜过大,因为二维码是要叠加到摄像头视频画面上的,一个码放太大就会遮挡画面内容,放太小又会扫不出来。我后来用QVGA分辨率做预览画面时,scale取6到8,二维码显示在画面右下角,手机实测大概20到40厘米距离能识别。如果你的输出分辨率更高,可以适当加大。
4.3 不经过PNG,直接把二维码矩阵投给framebuffer
如果板子带了HDMI或MIPI屏,那显示路径更直接。RV1126B的framebuffer节点一般是/dev/fb0,你只需要把生成的RGB图像按屏幕分辨率缩放后写入即可。一个简化版本是:
int fb_fd = open("/dev/fb0", O_RDWR); // 获取屏幕分辨率等参数 struct fb_fix_screeninfo finfo; struct fb_var_screeninfo vinfo; ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo); // 计算屏幕像素总量,把二维码RGB数据按屏幕像素格式刷新 // RV1126B的framebuffer常用ARGB8888格式,每像素4字节二维码只有一个优点:它不需要用户“看懂”,只需要能扫。所以哪怕你不用framebuffer而用Qt、用GStreamer、用DirectFB,核心都一样的,把点阵数据渲染成图像即可。
4.4 叠加到摄像头画面上的进阶用法
如果你做的是摄像头设备,可能需要把二维码直接叠加到Camera预览画面。RV1126B的ISP链路中,有一种做法是通过RGN(Region)模块做OSD叠加,但这个模块通常只支持简单色块和字符,不支持复杂图像。更通用的做法是用OpenCV或者自绘的方式,在获取到视频帧之后、编码推流之前,把二维码RGB数据做alpha blending贴到帧数据的指定区域。
我实际调试时遇到一个问题:视频帧的格式是NV12,而二维码数据是RGB。必须先做颜色空间转换,这个转换放在视频帧做,消耗不小。我的优化思路是:二维码在画面中的位置固定不变,那我就不需要每次都转换整个视频帧,而是只把二维码区域做一次YUV转换,再拷贝到对应位置的Y平面和UV平面。具体代码不放了,但思路供参考——能省一点CPU是一点,RV1126B虽然性能足够,但这种可以省的地方还是尽量省。
5. 实测效果与排坑记录——容量、中文编码、白边和显示异常
代码跑起来之后,真正的调试才刚开始。很多问题不实际在板子上跑一遍,光看文档是看不见的。我把自己踩过的坑,以及实测经验整理在这里。
5.1 中文内容编码:UTF-8字节流超容量
我的第一个需求是二维码内容里包含设备序列号加中文设备名,比如“客厅摄像头”。直接用QRcode_encodeString传入中文字符串,结果生成了,手机也扫出来了,但成品二维码明显比较大——因为libqrencode在QR_MODE_8字节模式下,会把中文字符串按UTF-8编码后的每个字节单独处理,一个中文汉字要占3个字节,10个汉字就是30个字节,这直接撑大了二维码版本。
实测下来,如果内容超过200个ASCII字符,二维码的版本会自动升到比较大的版本,点阵变得密密麻麻,对扫码距离和画面清晰度的要求都高了不少。排错时发现的一种情况是:内容超出QR码最大容量(4131字节),QRcode_encodeString直接返回NULL。这个在边界测试时特别容易遇到。
解决思路:尽量缩短二维码内容,只放必要信息,哪怕是长序列号也能用设备ID代替,没必要把完整的产品名称塞进去。
5.2 静区不足导致识别率低:手机扫不出来的真正原因
我最初直接用qrcode->data放大显示,没有额外加白边。代码跑起来后,手机扫码时偶尔能扫出来,但经常要凑很近、多试几次才行。一开始以为是画面太小,后来对比了电脑端生成的同内容二维码,才发现问题出在静区——libqrencode默认只有1个点的静区,远小于QR规范建议的4个点。
加上足够的白边之后,手机扫码速度提升非常明显,基本是凑近就秒识别。这里牵扯到一个原理:二维码解码器在定位三个角上的回字型定位图案之前,需要先估算整个码的边界范围。如果白边太窄,解码器会把周围画面中的其他纹理误判为二维码的一部分,导致边界估计错误,定位失败。所以白边不是可选项,是刚需。
5.3 framebuffer上二维码显示偏色或花屏
RV1126B的framebuffer像素格式不一定和你生成RGB数据的格式一致。我测试的评估板默认是ARGB8888,每个像素4字节。如果直接把RGB24的数据写进去,屏幕显示会整体偏色、颜色错乱。当时排查了很久,最后用ioctl把FBIOGET_VSCREENINFO打印出来,确认了bits_per_pixel和red、green、blue通道偏移才破案。
这里给个通用建议:不管用什么平台,写framebuffer之前先把vinfo.grafics_offset、line_length这些字段打印到日志里确认一遍,不要想当然。二维码是黑白的,按说对颜色格式最不敏感,但如果你RGB数据里填充的绿色通道值不对,显示出来的可能不是纯黑也不是纯白,扫描软件只认黑白对比度的话就会很吃力。
5.4 反复生成二维码导致内存增长:动态库的释放问题
一开始我每生成一次二维码就调用一次QRcode_encodeString,用完直接free返回的指针。但跑了几百次之后发现内存占用持续上涨。排查后发现问题不在QRcode_free,而在于我在生成过程中为了调试把每个二维码都存了一份PNG到临时目录,调试代码忘了释放,属于自己挖的坑。但同时也提醒我注意一点:libqrencode的QRcode_encodeString内部会分配一个QRcode结构体,用完必须调用QRcode_free()来释放内存,而不是简单的free,因为它内部的数据区是连续分配在同一个堆块上的。
对于长时间运行的摄像头设备来说,内存泄漏是不可接受的。建议在代码里对二维码生成做统一的内存释放管理,配合定期检查/proc/meminfo观察内存曲线是否平稳。
5.5 识别失败的另类原因:镜像翻转
还有一次,生成的二维码怎么都扫不出来,仔细检查代码逻辑也没问题。后来发现是我在叠到视频帧时做了水平翻转,因为摄像头画面默认是镜像的,我想让用户在预览里看到“镜子效果”,结果二维码也跟着左右反了,扫码软件自然不认。QR码没有镜像对称性,左右翻转后定位图案的相对位置完全变了。
解决方案是:不管视频画面是否镜像,二维码图像必须按正方向输出。正确做法是先画好正的二维码,再叠到视频帧上,或者只在做Camera预览翻转时把二维码区域单独做一次反方向翻转,让最终显示出来是正的。这属于显示层面的细节,容易忽略但一旦搞错排查起来很费劲。
6. 性能分析与优化思路——二维码生成对RV1126B来说是小菜一碟
很多做嵌入式的人可能担心,在板端跑二维码生成会不会很吃资源。以RV1126B的算力来说,这个担心完全是多余的。libqrencode的二维码编码过程基本是纯CPU逻辑运算,不涉及浮点,纯粹是循环和位操作,生成一个普通二维码大概只消耗几毫秒CPU时间。我实际跑下来,在编译时开-O2优化后,单次生成加内存拷贝的整体耗时大约在5到10毫秒之间,对1000MHz级别的处理器来说完全无感。
6.1 CPU耗时实测
为了确认性能是否够用,我用clock_gettime在版子上做了个简单计时:
- 生成QRcode矩阵(版本3,纠错M,内容30字节):约0.8ms
- 从矩阵放大到256x256 RGB图(scale=8):约3ms
- 从RGB复制到framebuffer:取决于屏幕分辨率,约2-5ms
- 合成到视频帧的单次开销:如果每次都要做颜色空间转换,约15-20ms
前两项的耗时基本可以忽略。真正可能拖后腿的是视频帧合成那一步,优化方向在前文已经提过,固定区域只做局部转换。
6.2 多次生成场景下的预生成策略
如果二维码内容在设备运行期间极少变化,那完全没必要反复生成。常见做法是:只在开机、配网模式切换、Token刷新这几个时间点生成一次,把生成的RGB图缓存下来,后续需要叠加显示时直接查缓存。我在项目里维护了一个“当前有效二维码图像区”,只有扫码流程开始时才更新,结束之后立即清除,避免二维码长期挂在画面上遮挡内容。
6.3 与RV1126B硬件加速的配合思路
如果确实遇到极端场景,二维码要频繁变化、同时还要做大分辨率视频编码,那可以考虑用RV1126B的2D硬件加速模块,比如G2D,来做图像缩放和拷贝。G2D支持格式转换和Blend操作,可以把二维码RGB图直接转换成NV12格式,再交给ISP/RGA模块合成到视频帧上。这属于性能优化很深的玩法,一般项目用不到,知道有这么个备份方案就好。
7. 结合EASY EAI开发环境的几个工程化心得
最后聊几个工程化层面的经验,这些不一定写在芯片手册里,但实际开发时影响很大。
7.1 交叉编译时头文件和库路径的匹配
RV1126B的SDK里rootfs的库结构和主机上可能有差异。交叉编译业务代码时,需要把libqrencode的头文件和库文件放在SDK的sysroot下,或者用-I和-L指定的位置,同时注意链接时用-lqrencode。如果出现链接失败,优先检查是不是工具链sysroot下的usr/include和usr/lib有没有对应文件。
7.2 设备重启后二维码需要重新生成
如果二维码内容里带了例如时间戳、随机数这类的动态字段,那么每次开机或者每次进入配网模式都要重新生成一次。这个逻辑建议放在状态机里,跟设备的配网状态绑定,避免出现二维码显示的是旧的Token,而设备端实际鉴权值已经刷新了。
7.3 产品化时要考虑的二维码风格问题
黑底白字、非标准颜色、加了圆角装饰的二维码,在手机上识别都很受影响。如果设计师追求“好看”想给二维码加个背景图或者改颜色,一定提醒他们保留标准黑白色块和足够的白边。我见过太多产品最后死在扫码识别率上,不是算法不行,而是UI乱改。二维码识别依靠的是黑白模块的对比度,颜色越接近纯黑纯白、边缘越锐利,识别越稳。
7.4 日志里不要打二维码内容
还有个小教训:调试时为了方便,我把要生成的二维码文本内容直接printf到了串口日志里。如果二维码内容包含设备密钥或Token,串口日志泄漏就等于密钥泄漏。嵌入式设备的串口日志经常被人摸到,不该往日志里打的东西一律不打,这是做设备安全的基本素养。
8. 后续可以怎么扩展
二维码生成这个功能本身做完了,但如果产品形态允许,它还能延伸出不少玩法。比如把二维码和手机App配对流程结合,做成“设备显示二维码、App扫码后通过蓝牙回传配网信息”的双向链路;或者把二维码内容做成带签名的防伪凭证,设备生成的每个二维码都含有一段用私钥签名过的数据,手机端验签通过才认为是可信设备。这些扩展建立在“设备本地能生成二维码”这个基础上,所以把这个基础功能做扎实了,后面的想象力空间很大。
我在实际使用中的体会是,二维码这种“土办法”反而是嵌入式产品里连接用户和设备最顺畅的桥梁。它不挑手机系统、不需要额外装驱动、不受网络环境影响,只要屏幕能显示图像就能工作。把这条链路做通,对后续配网、诊断、售后都会省很多事。如果文章里哪一步你没跑通,反复确认一下编译链接参数和环境变量,多半问题就出在那些不起眼的细节上。