简介:这是一份面向STM32开发者的u8g2移植资源。主控为F103RCT6,基于HAL库与硬件SPI驱动中景园1.3寸SH1106屏,分辨率128×64,开发环境为MDK。工程内含主程序、u8g2字体源文件、启动文件、链接脚本、编译中间文件及烧录文件,能从底层启动到上层绘图完整对照学习,降低自行移植时在时序和字体裁剪上的踩坑成本。整包共1381个文件,以源码和头文件为主,另有配置文件、目标文件、链接脚本、日志等,压缩包约25MB,便于按模块查阅,读者可直接参考工程内的编译配置与源码结构。已有2843人学习浏览,适合初次接触u8g2移植或准备在同系列上使用硬件SPI的开发者,提供从驱动初始化到图形绘制的完整参考,适合二次开发,既能快速验证显示效果,也能借助完整工程理解各文件在移植中的分工。
1. 把U8g2库跑在STM32上:HAL库硬件SPI驱动SH1106的实战拆解
用STM32驱动一块128x64的OLED屏,最容易被卡住的不是接线,而是图形库的移植。U8g2库在Arduino生态里几乎开箱即用,一旦换到STM32 + HAL库 + 硬件SPI,很多人就栽在初始化顺序、SPI时钟极性和底层画点函数上,眼看着屏幕亮起来却全是雪花点。这篇文章要拆的就是这条完整的移植链路:从CubeMX配置到U8g2库的裁剪接入,再到SH1106驱动芯片的底层适配。整个方案经过实际验证,跑通之后画面稳定、刷新流畅,适合正在做仪表盘、菜单界面或小型交互设备的人参考。你能从这里带走一套可以直接抄的移植步骤,以及几个花了一整天才排查出来的坑。
2. 移植前的硬件选型与协议分析:为什么是SH1106、硬件SPI和HAL库
2.1 SH1106和SSD1306的区别:别拿SSD1306的驱动硬套
很多教程默认OLED都是SSD1306,但市面上大量128x64的1.3寸屏用的是SH1106芯片。两者指令集大部分兼容,页地址模式、列地址偏移却有明显差异:SSD1306的列地址从0开始,而SH1106的显存列地址内部从0x02开始,对应的列地址偏移量需要单独处理。这就导致直接用SSD1306驱动代码去刷SH1106屏幕,显示内容会整体偏移两个像素,而且第一列会有异常残影。U8g2库本身同时支持这两种控制器,但需要明确告诉库底层用的是哪个芯片。在本项目选用的stm32f103c8t6平台上,通过HAL库的硬件SPI接口与SH1106通信,帧率比软件模拟SPI高出一截,CPU占用也更低。U8g2库在这块屏上使用的是页写模式,每页128字节,共8页,恰好对应64行像素。
2.2 硬件SPI接线与CubeMX配置:四个引脚一个都不能错
我用的核心板是STM32F103C8T6,SPI1的硬件接口映射到PA5(SCK)、PA6(MISO)、PA7(MOSI)。OLED这边只用到SCK、MOSI、DC、CS、RES,MISO悬空不管。接线如下:PA5接SCK,PA7接MOSI,PA4接CS,PA3接DC,PA2接RES。供电用3.3V,共地是必须的。CubeMX里将SPI1设为全双工主机,波特率预分频设为16,此时SPI时钟为4.5MHz左右,对SH1106来说绰绰有余,实际测试4.5MHz到9MHz都能稳定工作。SPI时钟极性CPOL设为Low,相位CPHA设为1Edge——这是U8g2库的软件配置里默认使用的方式,必须和硬件配置保持一致,否则通信数据会错位,屏幕上出现乱码。GPIO部分,DC、CS、RES全部设为推挽输出,初始电平分别拉高、拉高、拉高。初始化顺序有讲究:先配置GPIO和SPI,再对RES引脚做一个低脉冲复位,之后才允许向屏幕发送初始化命令。
2.3 U8g2库的裁剪思路:STM32资源有限,不是所有文件都要编译
U8g2库是一个庞大的图形库,完整源码包含几十种控制器的驱动文件。如果直接全部加入工程,编译时间会很长,Flash占用也会增加。我实际操作时只保留了u8g2.h、u8g2_d_setup.c、u8g2_d_memory.c,以及控制器相关的u8x8_d_sh1106_128x64_noname.c和底层的u8x8_byte.c、u8x8_gpio.c。这样裁剪之后,整个图形库只占用约30KB Flash,加上HAL库本身的开销,在F103C8T6的64KB Flash里完全放得下。有人的做法是全部文件塞进去再通过宏裁剪,那样会在编译时产生大量重复符号,不如直接在工程层面筛掉不适用的源文件。
3. U8g2库文件接入工程:从下载到编译通过的完整步骤
3.1 下载和目录规划:把这几个文件放进你自己建的u8g2文件夹
先准备好U8g2库源码,从官方仓库拉取或从任何可信渠道获取完整包。在工程目录下新建一个名为u8g2的文件夹,内部再分src和port两个子目录。src存放库的核心源码,port存放自己写的移植层文件。这样做的好处是当库升级时,只需要更新src,不会动自己的适配代码。
后续在Keil工程的Manage Project Items里新建一个Group,命名为u8g2,然后把文件添加进去。我添加的文件列表如下:
// Keil工程需要添加的U8g2源文件清单 // 路径: /u8g2/src/ u8g2_d_setup.c // 初始化参数配置,按控制器选型设置 u8g2_d_memory.c // 显存缓冲区分配,内部使用静态数组 u8x8_d_sh1106_128x64_noname.c // SH1106 128x64驱动 u8x8_byte.c // 字节发送层,对接底层SPI u8x8_gpio.c // GPIO控制层,包括DC/CS/RES引脚操作 // 路径: /u8g2/port/ u8g2_hal_stm32.c // 自己写的HAL适配层,后面会详细拆解 u8g2_hal_stm32.h // 适配层头文件这里需要说明的是,这段清单展示的只是源文件列表,实际文件内容需要从U8g2库源码包中复制。u8g2_d_setup.c包含所有控制器的初始化参数表,编译时通过宏U8G2_DRIVER_HEARTBEAT等条件编译项选择当前使用的驱动,而不是把全部驱动都编进去。
3.2 适配层核心代码实现:SPI发送与GPIO控制的HAL写法
U8g2库对底层的要求只有两个函数:一个是字节发送函数,负责把数据通过SPI发出去;一个是GPIO控制函数,负责拉高拉低DC、CS、RES。在HAL库环境下,字节发送可以直接调用HAL_SPI_Transmit,GPIO控制则使用HAL_GPIO_WritePin。下面的代码是经过验证的适配层写法:
// u8g2_hal_stm32.c #include "u8g2.h" #include "spi.h" #include "gpio.h" // 字节发送函数,u8g2库通过此函数输出数据和命令 uint8_t u8x8_byte_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch(msg) { case U8X8_MSG_BYTE_SEND: // 发送数据,arg_ptr指向数据缓冲区,arg_int为长度 HAL_SPI_Transmit(&hspi1, (uint8_t *)arg_ptr, arg_int, 100); break; case U8X8_MSG_BYTE_SET_DC: // 设置DC引脚电平,区分命令和数据 HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, arg_int ? GPIO_PIN_SET : GPIO_PIN_RESET); break; case U8X8_MSG_BYTE_INIT: // SPI初始化,此处无需额外操作 break; default: return 0; // 未处理的返回0,U8g2库允许忽略未知消息 } return 1; }这段代码中U8X8_MSG_BYTE_SEND是发送数据的主入口,欠妥的是直接调用HAL_SPI_Transmit,如果SPI时钟配置过高或DMA未开启,会阻塞CPU直到发送完成,但127字节一帧的显存刷新在这个速率下完全可接受。U8X8_MSG_BYTE_SET_DC是关键,它决定当前发送的是命令还是像素数据,SH1106靠DC引脚区分两种模式,搞反了就会把命令当数据发,屏幕直接花掉。
GPIO控制函数的写法如下:
// u8g2_hal_stm32.c 续 uint8_t u8x8_gpio_and_delay_stm32(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch(msg) { case U8X8_MSG_GPIO_AND_DELAY_SET_CS: // 片选信号 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, arg_int ? GPIO_PIN_RESET : GPIO_PIN_SET); break; case U8X8_MSG_GPIO_AND_DELAY_SET_DC: // 数据/命令选择 HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, arg_int ? GPIO_PIN_SET : GPIO_PIN_RESET); break; case U8X8_MSG_GPIO_AND_DELAY_SET_RESET: // 复位信号,低有效 HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, arg_int ? GPIO_PIN_SET : GPIO_PIN_RESET); break; case U8X8_MSG_DELAY_MILLI: // 毫秒延时,HAL库自带 HAL_Delay(arg_int); break; default: return 0; } return 1; }这个函数把U8g2库的软硬件抽象层请求映射到HAL库的GPIO操作上。注意CS引脚的电平逻辑:arg_int为1时表示选中,而SH1106的CS是低有效,所以这里做了电平反转。如果用的是三线SPI模式,比如只有SCK和MOSI,没有CS引脚,可以在CubeMX里把CS引脚配成普通GPIO,但多余的片选切换会引入额外的时序开销。
3.3 初始化代码的写法:u8g2_Setup_sh1106_128x64_noname_f函数参数含义
完成适配层后,需要在主程序中初始化U8g2库。初始化函数的选择有讲究,函数名里的f代表全缓冲模式,即所有像素数据先在RAM中绘制完成,再一次性刷新到屏幕。还有前缀为u8g2_Setup_sh1106_128x64_noname_1的页缓冲模式,只用少量RAM但需要多次刷新,以及_2的二分页模式,这里全缓冲是Flash和RAM都充裕时的最优选择。
// main.c 初始化部分 #include "u8g2.h" #include "u8g2_hal_stm32.h" u8g2_t u8g2; // 全局U8g2结构体,存储显示状态 int main(void) { HAL_Init(); SystemClock_Config(); // 配置系统时钟,这里用默认的72MHz MX_GPIO_Init(); MX_SPI1_Init(); // U8g2库初始化,绑定底层函数 u8g2_Setup_sh1106_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_hw_spi, u8x8_gpio_and_delay_stm32); u8g2_InitDisplay(&u8g2); // 发送完整初始化序列到SH1106 u8g2_SetPowerSave(&u8g2, 0); // 关闭省电模式,屏幕开始显示 // 绘制一个全屏填充矩形,验证显存映射 u8g2_ClearBuffer(&u8g2); // 清空内部显存 u8g2_DrawBox(&u8g2, 0, 0, 128, 64); // 横向128像素,纵向64像素 u8g2_SendBuffer(&u8g2); // 把显存内容刷新到屏幕 while(1) { // 主循环可以在这里继续绘制其他内容 } }这里的u8g2_Setup函数选择取的是U8G2_R0,即不做旋转。如果屏幕物理方向装反了,可以换成U8G2_R2实现180度旋转,U8G2_R1和U8G2_R3分别对应90度和270度。旋转操作是在库内部完成的,不增加额外负担,只是绘图坐标系的对应关系会变。
参数说明:第一个&u8g2是保存所有状态的结构体指针,必须是全局变量或静态变量,不能是栈上的临时变量;第二个参数是旋转方向;第三和第四个参数就是我们刚才写的两个适配函数,它们的签名必须严格匹配U8g2库定义的函数指针类型。
4. 移植后的显示效果与常见翻车现场:乱码、偏移、花屏逐个排查
4.1 现象一:屏幕全亮或全暗,什么内容都不显示
遇到这种情况,先不要改代码,用逻辑分析仪或示波器抓SCK和MOSI的波形,确认主控确实在发送数据。如果SCK没有波形,八成是SPI的GPIO复用功能没有正确配置,CubeMX里只勾了SPI1但没有把引脚复用为AF5,导致PA5和PA7只是普通GPIO在输出电平。这种问题在CubeMX生成的代码里不太容易出现,但如果是手动添加SPI初始化代码就很常见。解决方法是检查MX_SPI1_Init函数里有没有执行GPIO_InitStruct.Alternate = GPIO_AF5_SPI1,以及时钟有没有正常使能。另一个可能性是RES引脚没有正确复位,SH1106要求上电后RES拉低至少10us再拉高,如果复位时序不满足,芯片会一直处于未知状态。
4.2 现象二:显示内容整体左移或右移两列
这是SH1106最容易踩的坑。很多人第一次点亮后,屏幕最左边会有两列黑色残影,或者最右边缺两列。原因是SH1106的列地址偏移。SSD1306的列地址从0开始,SH1106从2开始,U8g2库在底层的驱动文件中已经处理了这个偏移,但前提是你选对了setup函数。如果你的屏幕初始化用的是SSD1306的setup函数,那么列地址偏移没有被补齐,就会出现内容右移两列。解决方法是确认setup函数名必须包含sh1106,我用的是sh1106_128x64_noname。如果你手里的屏是1.3寸但分辨率也是128x64,这个noname版本适用于大部分兼容SH1106的屏。但如果买了山寨屏,列偏移可能是3像素,那就只能手动改驱动文件里的偏移宏了。
4.3 现象三:文字花掉,字间隔有噪声点
这个问题的根源在SPI时钟极性和相位不匹配。U8g2库默认用CPOL=0、CPHA=1的时序,如果你在CubeMX里把CPOL设成了High,那么SCK空闲时是高电平,MOSI上的数据在错误的边沿被采样,看起来就是花屏。可以在CubeMX的SPI配置页面中将Clock Polarity设为Low,Clock Phase设为1Edge验证。如果改完还花,则把SPI波特率预分频从16换到32,排除信号完整性问题——杜邦线超过10cm时高频信号容易产生振铃,短接F103和显示屏之间的连线,最好控制在5cm以内。
4.4 现象四:屏幕能显示,但刷新画面时会有残留残影
SH1106的残影和SSD1306表现不同,SSD1306残影是因为没有用整屏写入模式,SH1106则是翻页顺序或清屏时序的问题。U8g2的全缓冲模式在调用SendBuffer时会把整块显存按页写入,如果某些页的数据没有更新,旧内容就会残留。这时检查两个点:一是ClearBuffer和SendBuffer之间,有没有正好把当前帧的内容全部绘制完;二是SPI发送时,每发送完一页数据,需要有一个拉高CS的时机让屏幕锁存数据,这个时序由U8g2的底层函数自动完成。如果自己改了适配层代码,比如在SendBuffer内部额外加入了延时或丢失了结束信号,就可能产生残影。我遇到过类似的情况,后来发现是自己在SPI发送函数里加了一个不必要的延时,去掉后残影消失。
4.5 现象五:屏幕只在刷新的瞬间闪一下,随后熄灭
这个多半是进入了省电模式。u8g2_InitDisplay之后,紧接着调用u8g2_SetPowerSave(&u8g2, 0)很重要,参数0表示关闭省电模式。如果忘了这行,屏幕会亮一下就保持黑屏状态,因为U8g2库初始化完成后默认把屏幕置于省电状态。另一个原因是电源问题,SH1106工作电流在20mA左右,3.3V供电用LDO常见做法是接一个100uF电容稳压,我用的是AMS1117-3.3模块,屏幕供电直接从板子的3.3V引脚取,没有额外加电容,也遇到了闪屏现象,加了电容后故障排除。劣质杜邦线接触不良可能造成电压跌落,同样会表现为闪烁。
5. 显示中文和数据刷新:u8g2自带字体与自定义字库的实战处理
5.1 内置字体清单与选择:哪些字体适合小屏幕,哪些是坑
U8g2库内置了多种字体,从4x6到24x32不等。字体命名格式有规律:u8g2_font_前缀表示字体,后面的字母组合标识风格,数字表示高度,比如u8g2_font_5x7_tf是5像素宽、7像素高的等宽字体。选字体有两个注意点:一是带有_tf后缀的表示包含透明模式,适合绘制在已有图形上的文字;带_tr后缀的不带透明背景,适合独立绘制文字。二是部分字体只包含ASCII字符,绘制中文会变成问号或乱码,所以在选字体前先看该字体的字符集覆盖范围。项目中我常用u8g2_font_6x13_tf做正文,u8g2_font_10x20_tf做数字突出显示,这套组合在128x64分辨率下显示效果清晰可辨。
5.2 中文字符显示方案:U8g2不支持直接写中文的解决办法
U8g2库本身不支持中文字库,因为中文字符集太大,全量字库会占用大量Flash。实际项目里需要显示中文时,有以下三个方案:
第一种是外部Flash字库,用SPI Flash存字模,每次渲染前读取对应的字模数据。这种方法适合需要显示大量不同中文的场景,但需要自己实现字模检索和SPI Flash驱动,工程量大一些。
第二种是GB2312编码查表映射,适合固定显示少量汉字。将需要用的汉字提取成一个常量数组,每个汉字对应一个16x16的字模,绘制时查表获取字模数据,然后按位写入显存。比如一个仪表盘只需要显示电压、电流、温度、状态几个词,十几个汉字就够了,用u8g2_DrawXBM函数可以直接绘制字模位图。下面是一个绘制16x16汉字的示例,截取自模拟项目X的仪表盘代码:
// 绘制16x16汉字字模,x和y是目标位置的左上角坐标 void Draw_Chinese_16(u8g2_t *u8g2, uint16_t x, uint16_t y, const uint8_t *ch) { u8g2_DrawXBM(u8g2, x, y, 16, 16, ch); } // 使用示例:在坐标(20, 20)处绘制汉字"电" // 字模数据由取模软件生成,字节排列为逐行水平扫描 static const uint8_t dian_zimo[] = { 0x00, 0x00, 0x7E, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x7E, 0x00, 0x00, 0x00, 0x00, // 剩余16字节省略,实际字模数据一般由工具生成 }; // 在u8g2初始化后使用 Draw_Chinese_16(&u8g2, 20, 20, dian_zimo); u8g2_SendBuffer(&u8g2);这里u8g2_DrawXBM的位图参数要求字节数据按行排列,每行像素数需要对齐到8的倍数,16x16字模正好满足这个条件。如果取模软件生成的字节顺序是纵向扫描而非横向扫描,显示出来会是旋转90度或镜像的,需要在取模设置里选择横向取模,字节倒序也需要注意。
第三种方案是在PC端把想要显示的汉字预处理成字模数组,这其实是方案二的扩展,本质同样是查表绘制,只是查表逻辑从运行时移到了编译时。对128x64这种小分辨率屏幕,能同时显示的汉字数量有限,所以大部分项目都选择方案二。
5.3 数据刷新时的闪烁问题:局部刷新和帧率控制技巧
屏幕在显示动态数值时,如果每帧都调用ClearBuffer、绘制、SendBuffer,数字变化时会有明显闪烁,尤其是电压或温度这类每秒多次变化的数据。解决方案有两个方向。第一个是局部擦除,不清空整个缓冲,只在数值所在区域绘制底色矩形覆盖旧内容,再绘制新数值。U8g2提供了u8g2_SetDrawColor函数,配合u8g2_DrawBox填充区域。第二个是降低刷新频率,人为增加帧间隔,比如每50ms刷新一次模拟量区域,不必每次MainLoop都刷新全屏。
// 局部刷新数值示例:在区域(5, 20)到(60, 32)显示三位整数 u8g2_SetDrawColor(&u8g2, 0); // 颜色设为0,用于擦除 u8g2_DrawBox(&u8g2, 5, 20, 55, 12); // 擦除旧内容区域 u8g2_SetDrawColor(&u8g2, 1); // 颜色恢复为1,用于绘制 u8g2_SetFont(&u8g2, u8g2_font_10x20_tf); u8g2_DrawStr(&u8g2, 5, 30, "123"); // 绘制新的数值字符串 u8g2_SendBuffer(&u8g2); // 只发送这一帧这段代码的逻辑是先用颜色0填充旧区域,实现局部擦除效果,再以颜色1绘制新数值。u8g2_SetDrawColor的0和1是单色屏的基本两色模式,SH1106是单色屏,不支持灰度,所以只有这两个值有效。局部刷新代替全屏刷新能明显降低闪烁感,代价是每次绘制前需要明确旧内容所在矩形范围,编码稍显繁琐。
5.4 功耗和帧率权衡:如何榨干F103的性能
在72MHz主频下,u8g2全缓冲模式刷一帧128x64的内容需要传输1024字节,SPI时钟4.5MHz时理论耗时约2ms,实际加上绘图操作,完全来得及跑30fps以上的动画。如果希望画圆或弧线这类需要大量CPU计算的操作,帧率会明显下降,但STM32F103跑U8g2的矢量绘图函数还算轻松。实际项目中我一般把动画控制在10fps到15fps,画面仍然流畅且CPU占用低。
如果连10fps都跑不到,检查SPI波特率配置。有些人在CubeMX里把SPI分频设成了128或256,SPI时钟降到几百kHz,刷新一帧要几十毫秒。建议先看波形确认实际SCK频率,再做调整。
6. 从验证到进阶:硬件SPI模式下DMA传输的接入方法
DMA传输在U8g2库中是一个值得尝试的优化方向,它能把SPI发送从CPU搬运变成外设自动完成。接线不变,重新在CubeMX中打开SPI1的DMA请求,将SPI1_TX请求绑定到DMA1Channel3,优先级设为Medium或High。生成的代码里会增加一个函数,在main函数调用HAL_SPI_Transmit_DMA时,CPU可以立即返回继续执行绘图绘制。但这里有一个大坑:U8g2库的SendBuffer函数是一次性把1024字节交给底层发送,DMA方式发送完成后需要回调通知上层,否则下一帧绘制时上一帧还没发完,显存会被覆盖导致画面撕裂。
我常用的处理方式是定义两个显存缓冲区交替使用,当DMA发送当前缓冲区时,主循环在另一个缓冲区里绘制。好在u8g2_Setup函数已经允许传入不同的缓冲区位置,通过在适配层增加缓冲切换逻辑,可以实现双缓冲渲染。
// DMA发送完成回调函数 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi->Instance == SPI1) { dma_busy_flag = 0; // 标记DMA传输完成,可以安全切换缓冲区 } } // 适配层DMA发送函数 uint8_t u8x8_byte_hw_spi_dma(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch(msg) { case U8X8_MSG_BYTE_SEND: while(dma_busy_flag); // 等待上次发送完成 dma_busy_flag = 1; // 置传输忙碌标志 HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)arg_ptr, arg_int); break; case U8X8_MSG_BYTE_SET_DC: HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, arg_int ? GPIO_PIN_SET : GPIO_PIN_RESET); break; case U8X8_MSG_BYTE_INIT: dma_busy_flag = 0; break; default: return 0; } return 1; }这段代码的要点是在发送新数据前检查上次DMA是否完成,用dma_busy_flag标志做同步。和普通轮询发送相比,DMA方式能把CPU时间从等待SPI发完中释放出来,让主循环可以一边刷屏一边做其他事情。实际测试中如果用DMA发送,配合双缓冲切换,动画帧率能提升接近一倍,CPU占用率明显下降。但代价是多了一套缓冲区管理逻辑,代码复杂度上升,适合画面对流畅度要求高的场景。
DMA方式还有一个需要注意的细节:HAL_SPI_Transmit_DMA会处于非阻塞模式,必须在CubeMX中开启SPI的DMA接收和发送通道,而且发送缓冲区的地址必须4字节对齐。如果缓冲区只是普通全局数组,编译器可能不会自动对齐,需要手动在数组声明前加上__attribute__((aligned(4)))语法。这个细节我踩过一次,当时DMA传输出来的数据一直是乱码,排查了很久才发现是对齐问题。
最后把我个人的使用习惯总结给你:按这套方法移植完U8g2库之后,我每次拿到一块新OLED屏,都不会直接开写代码,而是先确认芯片型号,再看分辨率,然后查看U8g2驱动文件里是否有对应的setup入口。型号对不上就直接换库版本或改偏移,不做无谓的适配尝试。从那以后我每次做屏幕驱动都强制走一遍这套流程,节省了大量排查时间。希望你能顺着这个步骤快速点亮自己的SH1106屏幕,并在U8g2库的加持下专注实现自己的界面逻辑,而不是在底层通信里打转。
本文还有配套的精品资源,点击获取