资讯动态

STM32直接生成二维码:从编码原理到OLED/LCD显示实践

发布时间:2026/10/14 1:34:50 来源:尧图企业网站定制
简介分享一套基于STM32F103ZET6红牛开发板的LCD二维码图片显示工程面向需要快速在单片机端实现二维码展示功能的嵌入式开发者。目前阶段由上位机提供图片内容、单片机负责显示作者同时调研对比了qrencode、Zbar、Zint、ZiXing等开源库认为qrencode精简、适合嵌入式移植为后续单片机自主生成二维码的技术方向做了铺垫。压缩包共120个文件、679KB含45个h头文件、42个c源文件、32个s汇编文件和1个uvproj工程文件覆盖LCD驱动GLCD.c、常用外设stm32f10x_tim/adc/i2c/usart/can等以及二维码输入相关模块qrinput.c层次清晰便于按模块阅读或裁剪。工程在MDK4.72下编译通过已有2451人学习适合从事物联网终端、通讯设备显示交互的开发者和学习者参考。1. 为什么要在 STM32 上直接画二维码一个被很多人绕开但真实存在的需求提到二维码显示绝大多数嵌入式开发者的第一反应是“用串口摄像头扫码枪去读或者干脆外挂一个带二维码生成功能的串口屏模块”。但在实际项目中设备本地需要动态生成二维码的场景远比想象中多设备没有联网能力、不能依赖云平台下发二维码、需要离线生成包含设备序列号或校验信息的码这时交给 STM32 自己完成二维码编码和显示就成了唯一不增加硬件成本、不增加外部依赖的可靠路径。STM32 主频几十到上百 MHzRAM 几十到几百 KB看似不适合跑二维码编码这种“桌面端的事”但只要选择合适的 QR 版本、控制纠错等级、用对显示驱动方式在 F103 或 F405 这类常规芯片上完全能把从字符串到二维码上屏的耗时压到几百毫秒以内而且刷新过程不会卡死主循环。这篇文章就从编码原理、内存开销、LCD 驱动方式和实测避坑四个方向把这条路完整走一遍让后面接到类似需求的人不用再走弯路。2. 二维码在单片机上的可行性边界先算清楚 RAM 和 CPU 账再动手2.1 QR 码的核心结构为什么最小版本也要 21×21 模块二维码不是一张乱码图它内部有严格的版本体系。QR 码从版本 1 到版本 40版本 1 是 21×21 个模块module每增加一个版本边长增加 4 个模块。这里的“模块”是二维码里的最小黑白单位不是屏幕像素。一个 21×21 的版本 1 二维码如果每个模块用 2×2 像素显示在屏幕上是 42×42 像素如果模块尺寸为 4×4则是 84×84 像素。STM32 驱动 128×64 的 OLED 或 240×320 的 TFT 时版本 1 到版本 537×37 模块是最常见的区间超过版本 7 之后数据密度提升但解码容错下降对屏幕分辨率和模块像素尺寸都提出更高要求一般不建议在单片机方案里使用高版本。QR 码内的数据分为功能图形和编码数据两部分功能图形包括定位图案、分隔符、校正图案、时序图案这些是解码器找位置和定方向的基准程序生成时必须固定绘制编码数据区域里除了实际内容还有格式信息、版本信息和纠错码字。STM32 上做二维码生成本质上是自己完成“数据编码 → 生成码字序列 → 按规则填充矩阵 → 应用掩码 → 绘制功能图形”这五步。对版本的依赖体现在版本决定了容量上限、校正图案的位置和数量、以及掩码计算时引用的参考矩阵。好在这些规则是公开且固定的按规范查表就能实现不需要任何第三方闭源算法。2.2 内存占用模型静态 buffer 与动态分配的选择STM32 上生成二维码内存开销主要来自三个地方输入数据缓存、纠错码字计算用的临时区、最终的模块矩阵。模块矩阵可以用每模块 1 bit 来存储版本 1 是 21×21 即 441 bit约 55 字节版本 5 是 37×37 即 1369 bit约 172 字节。这个量对于任何 STM32 来说都不算压力。真正的内存大头在 RS 纠错码字计算上它需要为每一块数据分块计算纠错码临时缓冲区大小取决于版本和纠错级别最高需要几百字节。总体来看一个完整的二维码编码过程峰值 RAM 占用在 1~2 KB 以内。我建议在工程里使用静态缓冲区而不是动态分配。原因有两个第一编码过程中有多次中间结果复制malloc/free 的碎片化会让长时间运行的设备逐渐产生不可预测的失败第二二维码生成往往是“按需触发”的操作比如按键或指令到达时才生成静态 buffer 可以复用且不会产生分配失败。常见的做法是定义一个结构体内部包含数据缓存区、最大版本对应的矩阵空间、以及纠错信息工作区。在 STM32 上运行这个方案时需要在启动文件里确认堆栈足够或者直接把缓冲区定义在全局区。2.3 版本与纠错等级的权衡不是越高越稳项目里最容易出现的误区是“纠错等级选 H这样扫码枪随便扫都能识别”。但在嵌入式场景中高纠错等级直接压缩了数据容量。以版本 225×25 模块为例纠错等级 L 可存 32 字节等级 Q 只能存 18 字节等级 H 只有 14 字节。如果设备要生成的二维码包含较长 SN 序列号或一段配置字符串高纠错等级会导致需要升级到更大版本模块数增多屏幕上的模块像素尺寸被迫缩小反而降低了解码的成功率。实际测试过一组对比同样内容在 128×64 OLED 上版本 1 用 4×4 模块显示时扫码距离 5~15 cm 稳定识别强行上版本 3 后模块像素只有 1~2 像素扫码枪经常提示“无法识别”。所以选型的原则是内容能压短就压短版本越小越好纠错等级在 L 和 M 之间选择除非屏幕分辨率富余很多才考虑 Q。项目里默认选 M 是一个比较均衡的选择。3. 从字符串到二维码点阵STM32 上跑通编码全流程3.1 引入二维码编码实现桌面库移植还是自研核心二维码编码的核心算法并不长但涉及 bit 流处理、RS 纠错、掩码评分这些环节从头造轮子的代价不小。常见的做法是引入一份开源实现的 C 语言二维码生成库这类库通常只依赖标准 C 库的 memcpy 和 memset没有平台相关的系统调用。移植到 STM32 工程时只需要把核心源文件加入编译然后确认所有缓冲区由自己提供即可。但也有一个现实问题很多桌面库的 API 是面向 PC 的内部可能包含大内存申请或假定内存充足的安全检查移植时要做裁剪。从工程安全角度我更推荐的做法是把核心编码逻辑按模块拆开。生成矩阵只是“把 byte 数组变成黑白矩阵”的过程与具体屏幕没有任何关系所以放在一个独立的 c 文件里不包含任何 LCD 驱动头文件。后续换屏或者换平台时只需要重写显示适配层。很多开发者刚开始图省事直接把编码库和 OLED 驱动写在同一个函数里结果后面换 TFT 屏幕时被迫把整个文件拆开血泪教训。在项目里我会把接口设计成输入字符串和纠错等级输出一个 bitmap 数组每个 bit 对应一个二维码模块后续屏幕怎么显示完全由调用方决定。3.2 最小可跑通的移植流程移植过程按下面的步骤执行即可。先建立一个裸机工程把二维码编码库加入编译然后建立 main.c 做三件事调用编码接口得到模块矩阵按模块矩阵操作屏幕 buffer最后刷屏显示。以 0.96 寸 I2C OLED 为例完整流程可以写成下面的示意代码/* main.c - STM32 上二维码生成与 OLED 显示的最小骨架 */ #include qr_encoder.h /* 二维码编码核心只依赖标准 C */ #include oled_ssd1306.h /* I2C OLED 驱动提供 buffer 操作和刷屏 */ /* 静态缓冲区避免动态内存全部在编译期分配 */ static uint8_t qr_modules[ (QR_VERSION_MAX 1) * (QR_VERSION_MAX 1) / 8 ]; static uint8_t framebuffer[ 128 * 64 / 8 ]; /* 128x64 OLED 的显存区 */ #define MODULE_SIZE_PIXEL 2 /* 每个二维码模块用 2x2 像素显示 */ int main(void) { const char *payload S/N:20250513-TEST88; /* 要编码的字符串 */ QR_EncodeResult res; /* 1. 编码字符串到二维码模块矩阵 */ QR_EncodeString((const uint8_t *)payload, QR_LEVEL_M, /* 纠错等级M约 15% 纠错能力 */ 1, /* 最小版本限制1 表示从版本 1 开始 */ 10, /* 最大版本限制防止超出版本 10 */ res, qr_modules); /* 2. 清屏后按模块矩阵绘制到显存 */ OLED_ClearFrameBuffer(framebuffer); for (int row 0; row res.size; row) { for (int col 0; col res.size; col) { int bit_offset row * res.size col; if ( (qr_modules[bit_offset / 8] (bit_offset % 8)) 0x01 ) { OLED_FillRect(framebuffer, col * MODULE_SIZE_PIXEL, row * MODULE_SIZE_PIXEL, MODULE_SIZE_PIXEL, MODULE_SIZE_PIXEL); } } } /* 3. 刷屏到 LCD */ OLED_Refresh(framebuffer); while (1) { /* 主循环二维码生成一次即可不需要重复编码 */ } }这段代码的关键点在于把编码和显示严格分开。QR_EncodeString只负责把字符串变成模块矩阵它不关心矩阵最终显示在 OLED 还是 TFT 上。OLED_FillRect则是在显存中填充一个矩形区域。注意这里res.size是编码库返回的实际版本边长比如版本 1 时 size 为 21版本 2 时 size 为 25代码里没有硬编码这个值避免版本跳动后显示错乱。MODULE_SIZE_PIXEL这个值的设定直接决定二维码在屏幕上的物理大小设为 2 意味着 128×64 的屏幕最多能完整显示边长不超过 64 像素的二维码也就是版本 4 左右的极限。如果设成 1版本 5 甚至 6 也能显示但扫码距离明显变短。3.3 编码接口参数说明版本限制背后的容量与性能关系上面代码里的版本限制参数最小版本 1、最大版本 10在实际项目中很值得展开讲。二维码编码器的行为是在最小版本和最大版本之间寻找一个能装下当前数据的最小编号版本。但并不是所有版本都能容纳任意长度的字符串版本越高校正图案越多绘制难度和内存消耗也会增大。建议在实际项目里把最大版本限定在 4~6 之间。如果数据太长导致编码失败编码库会返回错误码而不是向上继续找更大的容量——这样做的好处是避免了高版本二维码在低分辨率屏幕上显示不清的尴尬。另一个容易被忽略的参数是输入数据的类型。同一个字符串“12345”如果作为纯数字编码可以进入数字模式每 10 位用 1 个字节承载如果包含字母和符号就进入字母数字模式再有其他字符则进入字节模式。不同模式的数据密度不同同样是 30 个字符数字模式可能版本 2 就够字节模式则需要版本 3。很多人在嵌入式上为了省事直接统一走字节模式这在小数据量时没问题但如果要存储完整的设备信息链接或配置串建议先判断字符类型能走数字就走数字。二维码编码库通常已经内置了这个判断逻辑但输出的版本可能因此变化所以调试时看到版本号和预期不同不要慌张先确认输入字符串的字符构成。3.4 模块矩阵到屏幕像素的三种映射方式对比拿到模块矩阵后怎么把它绘制到屏幕上工程里主要有三种做法。第一种是上面示例里用的显存矩形填充适合有完整 framebuffer 的 OLED 或小尺寸 TFT代码直观缺点是内存占用和刷新时间与屏幕分辨率成正比。第二种是逐点扫描绘制适合像 1.54 寸黑白电子墨水屏这类支持局部刷新的屏幕可以只刷二维码区域的缓冲。第三种是预生成图像数据的查表法把二维码模块矩阵转换成屏幕控制器直接使用的显存格式比如 SSD1306 的页列格式或 ILI9341 的 RGB565 格式这种方式显示速度最快可读性稍差但如果二维码区域固定可以直接把转换后的数据存进 Flash。从调试便利角度看我推荐第一步先用第一种方式跑通因为显存 buffer 可以随时通过调试器查看出问题时能直接判断是编码失败还是绘制失败。第二种方式适合追求省内存的项目第三种方式适合做产品化时减少每次刷屏的时间。三种方式的切换点在于屏幕控制器接口类型SPI 屏和 I2C 屏时序完全不同I2C 的 OLED 每次刷新一帧 1024 字节都需要不少时间如果还用逐点画的方式二维码区域稍大就会出现刷新肉眼可见的闪烁。所以在 I2C OLED 上千万不要用逐点刷新的方式显示二维码一定要先把整帧画到 buffer 再一次性刷出去。4. 在 LCD 上稳定显示二维码驱动设置、尺寸计算和刷新时序4.1 屏幕分辨率与模块像素尺寸的计算公式在做二维码显示适配时最核心的计算任务是根据屏幕实际像素尺寸决定每个模块对应多少像素。假设屏幕宽度为 W 像素高度为 H 像素二维码模块矩阵边长为 S那么模块像素尺寸 m 的计算方式是取(W / S)和(H / S)的整数部分中的较小值然后二维码实际占用的像素区域是m × S宽、m × S高剩余区域留白。举例来说128×64 屏幕配版本 2S25时水平方向 128/255垂直方向 64/252所以取 m2二维码占 50×50 像素屏幕上居中后上下左右都有留白。如果强行取 m3水平方向达到 75 像素没问题垂直方向 75 像素超出屏幕二维码会被裁剪。实际项目里还要考虑二维码和屏幕上其他内容的共存问题。比如设备界面上同时要显示时间、电量图标、提示文字那么二维码可用区域往往不是整个屏幕而是屏幕的一部分。这时 m 值必须按可用区域计算而不是按完整屏幕计算。很多翻车案例都是因为先按整屏尺寸算了模块像素等到界面调整后二维码被压缩扫码枪疯狂报警。建议把二维码显示区域定义成 x, y, w, h 四个全局变量所有计算基于这套参数后续改 UI 布局时只改这几个值。4.2 SPI TFT 与 I2C OLED 的刷屏差异刷新缓冲策略不同屏幕控制器的显存组织方式差异较大对二维码显示效果影响直接。SSD1306 这类 OLED 的显存是按页组织的一页 8 个像素行页内每个字节代表一列 8 个像素所以从线性 framebuffer 映射到屏幕时必须做字节位序转换。ILI9341 这类 TFT 控制器的显存是连续 RGB 像素如果使用 RGB565每个像素 2 字节写一个二维码区域需要先构造好对应的像素颜色数组再走 SPI DMA 发送。两种方式各有坑OLED 的坑是位序容易画反TFT 的坑是颜色转换可能引入额外的 CPU 开销。我的工程实践里推荐统一做法在代码中维护一个和屏幕显存格式完全一致的应用层 framebuffer二维码绘制时直接操作这个 framebuffer最后调用一次整帧刷新函数。这样做的理由有两个其一二维码生成是不频繁的操作牺牲一点 RAM 换驱动复杂度大幅下降其二整帧刷新时可以利用 DMACPU 在刷屏期间可以继续处理其他任务。STM32F103 的 SPI DMA 刷 128×64 OLED 一帧大约 15~20 ms刷 240×320 TFT 一帧大约 50~70 ms这个时间对于“按下按键后显示二维码”的场景完全够用。4.3 二维码在屏幕上的方向问题一维是左上到右下二维有三个旋转角二维码解码器本身支持旋转识别定位图案保证了 0 度、90 度、180 度、270 度都能读出来。但在嵌入式屏幕上有一个比旋转更隐蔽的方向坑屏幕坐标系原点和二维码矩阵原点的对应关系。有的屏幕驱动里 y 轴向下为正有的屏控制器在设置窗口模式下地址会自动递增如果绘制循环里行和列的嵌套顺序写反了二维码就会变成转置矩阵虽然定位图案仍然存在但数据区域全部错位。这个问题很多初学者会在第一版驱动代码里遇到现象是扫不出内容但看着像二维码。排查这类问题的标准操作是先不显示二维码而是显示一张带明显方向特征的测试图比如在右上角画一个 1 像素的实心点或者画一条对角斜线看屏幕上出现的方向是否和代码逻辑一致。确认方向无误后再接二维码绘制函数。另外要注意如果屏幕是竖屏而二维码编码的矩阵是正方形居中后不需要旋转但如果 UI 设计里强制要求二维码占满屏幕宽度而矩阵边长比屏幕短宁可缩小模块像素也不要放大拉伸拉伸会让模块之间的间隔出现非整数像素解码器对模块尺寸比例敏感一旦深色模块和浅色模块的边界出现锯齿在小屏上就会解码失败。4.4 逆向构造用屏蔽测试图案检查显示驱动如果不确定显示驱动的行列扫描方向和地址递增方式可以用一个很直接的测试方法往 framebuffer 里写入一组已知的递增数据然后观察屏幕上的实际显示顺序。具体做法是在 framebuffer 的左上角、右上角、左下角、右下角分别填充不同的固定灰度值刷屏后看屏幕四个角是否和预期一致。这一步排除了坐标映射问题之后再显示二维码就不会出现方向错乱。项目里曾见过花了整整一天排查“二维码扫不出”的问题最后发现是屏幕硬件本身安装方向是倒置的驱动方向和矩阵方向在软件里对了和物理方向反了。所以碰到显示类问题永远先怀疑坐标方向再怀疑编码库。5. 避坑与排查二维码上屏最常见的 5 个翻车现场5.1 直接进 HardFault编码库内部内存拷越界现象调用二维码生成函数后程序立刻进入硬件错误中断看调用栈发现是编码库内部执行字节拷贝时出错。原因编码库对输入缓冲区长度有假设比如在版本身份信息区写入固定偏移但外部传入的最大版本限制过小导致内存写入越界。解决确认所有传给编码库的缓冲区指针都指向静态分配且足够大的内存区域。我在工程里会统一在移植时给编码库加一层调试封装每次编码前后对比一个固定内存区域的填充值如果被改写就说明越界这种手段在裸机调试时非常实用。5.2 二维码显示出来扫码枪完全没反应现象屏上能清楚看到一个方方正正的二维码图案轮廓、定位角都在但任何扫码设备都无法读取。原因二维码矩阵里每个模块的像素尺寸过小导致模块间的深浅边界模糊或者屏幕亮度和对比度不够。解决用手机摄像头拍下屏幕上的二维码放大看如果发现每个小块边缘粘连大概率是模块像素尺寸取小了把MODULE_SIZE_PIXEL调大一个档。另一个常见原因是白底黑字还是黑底白字没有做归一化有些扫码引擎只认标准深色模块在浅色背景上的形态OLED 上默认黑底白字画出来的二维码实际是反色的这时要把矩阵取反。OLED 上黑底白字显示二维码时要把模块矩阵的值按位取反否则定位图案变成空心识别率急剧下降。5.3 相邻两次生成的内容不同但屏上显示的二维码没变化现象设备在运行时收到新指令需要更新二维码内容但屏上的二维码和上一次内容一样。原因上层代码在更新 framebuffer 时没有先清空旧的二维码区域旧的深色模块像素和新的深色模块像素叠加后新的浅色模块位置仍是深色。解决在每次二维码绘制前先调用矩形填充函数把显示区域清成背景色再开始逐模块绘制。这个问题在区域刷新模式下特别隐蔽因为整帧刷新模式下清屏成本高很多人偷懒不清就直接画结果出问题。5.4 SPI 刷屏时二维码显示撕裂现象使用 SPI TFT 屏时二维码上半部分和下半部分的显示内容出现水平错位像被人横向切了一刀。原因在刷屏过程中主循环的任务和 DMA 搬运数据之间产生了竞争或 SPI 时钟频率过高导致控制器端采样不稳定。解决给刷屏函数加上临界区保护或者在刷屏期间挂起低优先级任务。SPI 时钟频率在 18 MHz 以上时建议先用逻辑分析仪对比 MOSI 电平时序很多国产屏控制器的实际时钟上限没有数据手册标的那么高降一档频率往往直接解决。5.5 二维码区域闪白条现象OLED 屏幕上二维码显示时上方有一条横贯整个屏的白线刷新后消失。原因OLED 的显存是按页扫描显示的如果 framebuffer 的某页数据没有完整更新控制器会维持旧的数据而出现残影。解决确认 framebuffer 初始化为固定背景值每次绘制矩形区域时严格按页边界对齐。二维码显示区域的 y 坐标如果是奇数页地址和行地址的映射会产生半字节错位最简单的方式是让二维码区域的 y 坐标固定为 8 的倍数这样每页都完整覆盖。5.6 缓冲区尺寸不足导致的最大版本限制失效现象明明限制了最大版本 5但编码库还是把内容塞进版本 6 的矩阵里导致屏幕显示溢出。原因编码库在实现中版本限制参数只在编码开始时生效如果数据长度超出限制版本容量有些实现会直接报错而不会自动升级版本但另一些实现会在特定模式下悄悄放宽限制以完成编码。解决检查编码库的返回值如果返回的版本号大于屏幕可支持的最大版本则拒绝显示并在上层提示“数据过长”。从这里也能看出把版本检查放在应用层而不是依赖库内部约束是更稳妥的设计。6. 让扫码体验更稳从模块定位到亮暗边界的显示调优技巧二维码能扫出来只是第一步要让它扫得快、远距离也能识别、在日光下也不丢失还需要对显示细节做进一步处理。这里分享两个项目实测有效且成本极低的调优技巧适合在最后阶段对显示代码微调时使用。第一个技巧是给二维码加定位辅助边距。QR 规范要求二维码四周必须有至少 4 个模块宽度的空白区域这个区域叫 quiet zone。在实际屏幕上如果二维码紧贴屏幕边缘或者靠近其他 UI 元素扫码枪的取景框会同时捕捉到干扰图形解码时间变长甚至失败。我的习惯是在计算绘制位置时预留出模块尺寸 4 倍的额外边距不要把那部分空间用来显示文字或图标。比如版本 1 二维码 21 个模块每个模块 3 像素宽quiet zone 就是 12 像素居中时 x 和 y 各减去这个偏移画出来之后四周有干净的留白。这个改动在 UI 拥挤的界面里对扫码命中率提升非常明显。第二个技巧是对反白模式的亮暗对比做增强。OLED 屏幕上黑色像素是直接熄灭的周围环境光强时黑色区域会反光变灰导致深色模块和浅色模块的亮度差缩小。TFT 屏上浅色背景如果用 RGB(255,255,255)深色模块用 RGB(0,0,0)在户外阳光下同样存在反光问题。我习惯把背景色从纯白改成 RGB(220,220,220)深色模块用 RGB(20,20,20)这样整体对比度略微降低但反光时的可辨识度反而更高。OLED 上则可以把浅色模块用最大的亮度等级绘制确保和熄灭状态的差异达到控制器允许的极限。第三个技巧是把模块像素设为单数而不是双数。当模块像素尺寸为单数时比如 3、5、7 像素二维码模块的中心点正好落在一个物理像素上深浅模块之间的分界线更锐利。双数尺寸时模块边缘会产生 1 像素的“过渡列”在扫码枪的图像传感器上容易形成模糊带。这个现象在模块尺寸小的时候尤其明显所以能选 3 不选 2能选 5 不选 4。当然如果屏幕分辨率制约只能选 2 或 4那就把这个因素记在心里优先保证版本尽量小不要为了凑奇数而强行降低模块像素数量。代码层面最终只需要维护一个绘制函数把版本边长、模块像素尺寸、quiet zone 宽度、背景色四组参数集中管理所有界面调整都通过这四组参数完成。我自己在项目里吃过亏最初把 quiet zone 写死在绘图循环里后来屏幕从 128×64 换到 240×320所有计算全部推倒重来。所以宁可多花半小时做参数化设计也不要贪快写死。二维码显示这种功能第一版跑通容易做到扫码率和环境适应性都令人满意靠的还是对这几个细节的反复打磨。希望这篇笔记能帮你在自己的项目里少踩几个坑一次把二维码稳定显示出来。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑