简介:分享一套基于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 主频几十到上百 MHz,RAM 几十到几百 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 到版本 5(37×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,这样扫码枪随便扫都能识别”。但在嵌入式场景中,高纠错等级直接压缩了数据容量。以版本 2(25×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 屏幕配版本 2(S=25)时,水平方向 128/25=5,垂直方向 64/25=2,所以取 m=2,二维码占 50×50 像素,屏幕上居中后上下左右都有留白。如果强行取 m=3,水平方向达到 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 换驱动复杂度大幅下降;其二,整帧刷新时可以利用 DMA,CPU 在刷屏期间可以继续处理其他任务。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,所有计算全部推倒重来。所以宁可多花半小时做参数化设计,也不要贪快写死。二维码显示这种功能,第一版跑通容易,做到扫码率和环境适应性都令人满意,靠的还是对这几个细节的反复打磨。希望这篇笔记能帮你在自己的项目里少踩几个坑,一次把二维码稳定显示出来。
本文还有配套的精品资源,点击获取