news 2026/9/14 12:34:50

Unity跨平台开发踩坑实录:数字孪生项目从PC到VR的完整复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity跨平台开发踩坑实录:数字孪生项目从PC到VR的完整复盘

做 Unity 软件开发这两年,踩过的坑比我之前做 Web 前端三年加起来都多。尤其这次这个项目,从 UI 交互、渲染阴影、WebGL 打包到串口通信、XR 设备适配全过了一遍,很多东西回过头看特别想记下来。这篇文章就是一个完整的项目记录,把我当时为什么选这个方案、出了什么问题、最后怎么解决的都写清楚,适合正在做 Unity 跨平台项目、或者准备从传统 MVC 转 Unity 的开发者参考。

项目本身不复杂,核心是一个数字孪生方向的演示系统,需要在 PC 客户端操控,也要发布到 Web 端给客户远程看,另外还要能在 PICO 4 这类 VR 设备上跑起来。正因为目标平台杂,Unity“一次开发、多端发布”的优势就体现出来了。不过跨平台这件事,Unity 帮你省掉一部分麻烦,也一定会在别的地方还给你,比如阴影表现、文件写入、串口访问这些,几乎每个平台都有各自的脾气。

1. 项目背景与工程选型:为什么最后选了 Unity

1.1 这个项目到底要解决什么问题

客户给的需求听起来简单:一个虚拟车间场景,能绕着一个设备模型转,能查看设备参数,最好还能联动外部 PLC 读到实时数据。后来聊深了才发现,他们想要的其实是一个“能放在展厅大屏上的数字孪生原型”,PC 端演示完,还要能在浏览器里通过链接访问,最后 VR 设备也要能沉浸式看车间的整体布局。

这种需求天然适合游戏引擎。参数曲线、摄像机漫游、设备动画、光照阴影,这些都是引擎的强项。如果纯用 Three.js 做 Web 端,一方面光照和交互开发成本高,另一方面 PC 端、VR 端还得重新写一套,根本没有那个交付周期。

所以选 Unity 基本没有悬念。Unity 的 UGUI 系统和 UI Toolkit 做 HUD、菜单很成熟,内置的物理和动画系统也能覆盖演示类需求,Addressables 资源管理在切换场景的时候也顺手。C# 写后端逻辑或者对接通信协议,比蓝图、比 C++ 都舒服得多。

1.2 开发环境和目录组织

我用的版本从 Unity 2022.3 LTS 起步,后来因为要测试新特性,也试过 Unity 6000.3.9f1。说实话从 LTS 版本迁移到 6000 系列,最大的感受是 Shader Graph 和 URP 的默认渲染效果更细腻了,但项目里一些老的 UI 和动画资源也会出现不兼容的情况,所以我的建议是:老项目尽量停在 LTS 上,新项目再考虑用 6000 系列。

工程结构这块吃过不少亏。最开始一个 Assets 文件夹里 Scripts、Prefabs、Scenes 堆得乱七八糟,后面加人一起开发的时候,合代码冲突能吵起来。后来按功能模块重构成这样:

  • Assets/Modules:每个业务模块一个文件夹,模块内部再分 Scripts、Prefabs、Configs
  • Assets/Core:公共框架,事件系统、单例基类、工具类都放这里
  • Assets/UI:Prefab 和 UI 脚本,按 Scene 名字分组
  • Assets/ThirdParty:第三方插件,能锁版本就锁版本
  • Assets/Resources 和 AddressableAssets:资源管理入口,尽量用 Addressables

目录组织和命名规范这种东西,网上教程不怎么提,但实际项目里对开发效率影响极大。特别是多个版本分支并行的时候,一个清晰的目录结构能少吵很多架。

1.3 为什么没有用别的方案

市面上面向数字孪生的方案不少,有基于 Web 的,也有基于商业引擎的。从投入产出比来看,Unity 的社区资料最多,出问题基本都能搜到解决方案。

和一个做 UE 的同事聊过,虚幻的优势在超高画质,但对于这种偏工业数据展示的项目,Unity 的 URP 管线性能开销更可控,WebGL 导出也成熟得多。UE 的 Web 导出虽然这些年进步不少,但工程体量和加载速度对普通客户来说还是一个坎。至于纯 Web 技术栈,开发效率和后期维护成本反而是最不划算的。

2. UI 交互层的实际问题:滑动条、点击范围与 World UI 遮挡

2.1 Slider 滑动条的默认结构和工作逻辑

日常调试里遇到最多的其实是 UI。先说搜索热度很高的“Unity 做一个滑动条”。UGUI 自带的 Slider 组件很强大,它的结构是 Background、Fill Area、Handle Slide Area 三层。Fill Area 下面挂着一个 Filled 类型的 Image,Handle Slide Area 下面挂着一个用来拖拽的 Image,这三层配合 RectTransform 的 anchor 决定滑块外观。

我一开始以为这个组件拖出来就能直接用,实际上要做自定义外观时必须理解这几个子节点的关系。滑动条的核心是数值映射,Min Value 到 Max Value 的范围变化,拖动 Handle 时 Fill 区宽度跟着变。如果 Fill 区图片类型不是 Filled,或者锚点设置不对,就会出现“滑块到头了,填充条还没满”的诡异现象。

我的习惯是先在编辑器里把默认 Slider 结构拆开看一遍,再复制一份改成自己的外观。要调整滑动条灵敏度,可以考虑修改 Handle 的尺寸或者脚本里对 onValueChanged 事件做归一化处理。比如音量条,UI 上显示 0 到 100,内部逻辑其实只需要 0 到 1,在事件里做一次映射就行,避免把界面数值直接传给逻辑层。

2.2 扩大按钮点击范围

很多人问 Unity 如何扩大按钮点击范围,因为美术做出来的图标往往只有 32x32,手指在手机上点起来非常费劲。最简单粗暴的做法是在按钮下挂一个比图标大很多、颜色全透明的 Image,把 Raycast Target 勾上。但这里有个隐藏坑:如果这个透明 Image 挡住的是另一个按钮,就会出现点击穿透失败的体验。

更好的做法是给按钮的 RectTransform 扩展一个可配置的 Padding 区域,重写判断点击是否命中的逻辑。UGUI 的命中检测走的是 GraphicRaycaster,它只在 Image 的 RectTransform 范围内响应。一旦用透明 Image 扩展,那个 Image 就会吞掉所有射线。所以我项目里写了一个 ExpandableButton,继承 Image 并重写 Contains 或直接重写 OnPointerDown 来扩大检测区域,其他 UI 完全不受影响。

还有一个思路是用 alphaHitTestMinimumThreshold。这个属性可以让 Image 根据像素透明程度决定是否可点击,比如把阈值设到 0.1,半透明区域也算可点。但前提是 sprite 必须开启 Read/Write,且图片不能有抗锯齿边缘,否则边缘像素透明度不稳定,点击感受很怪。实际项目里,大多数场景用透明子节点扩展就够了,追求精细化时才需要做自定义 Image。

2.3 World UI 无遮挡问题的处理

“Unity World UI 无遮挡”这个问题,我专门研究过。当 Canvas 渲染模式设为 World Space,UI 会像普通 3D 物体一样参与深度测试,前面有墙或者设备模型时,UI 标签就会被挡住。

网上常见方案是把 Canvas 放到一个单独的相机上。比如主相机渲染场景层,另一个专用 UI 相机通过 Culling Mask 只渲染 UI 层,再把 UICamera 的 Depth 设得比主相机大,这样 UI 永远画在场景前面,同时还能保留世界空间的位置感。

我在项目里用的就是这个双相机方案。UI 相机可以设置 Clear Flags 为 Depth Only,并且把 Canvas 的 Event Camera 指向 UI 相机,保证射线检测正确。另一个方案是通过 VRS 或者 RenderTexture 把世界 UI 单独渲染到一张纹理再贴到屏幕,但那个成本高,没必要。

还有一个小技巧:如果想要一个 3D 世界里的浮动面板始终不被遮挡,但又希望它跟随目标物体移动,可以不让它真的作为世界空间 UI 存在,而是在 Screen Space - Camera 模式下,每一帧根据目标的 WorldToScreenPoint 坐标去设置 UI 位置。这种方式最稳,几乎没有深度问题。

3. 阴影异常的排查链路与渲染参数调整

3.1 阴影消失或者闪烁,先别急着调光源

Unity 阴影出问题是高频需求,特别是“阴影不见了”“阴影闪烁”这类。很多新手一上来就查光源是不是打开了 Cast Shadows,其实大部分问题根本不在这里。

我亲历的一个案例是:一个工业设备模型导入后,周围明明有平行光,但模型就是不产生阴影。查了半天发现它的材质用的是自定义 Shader,而这个 Shader 没有写 ShadowCaster Pass。对,自定义 Shader 如果不实现阴影投射通道,物体在渲染阴影贴图的时候就是一个空洞。要让模型产生阴影,要么在 Shader 里加上 ShadowCaster Pass,要么把材质改回 Standard/URP Lit。

另一个高频原因是 Shadow Distance 设置太小。默认的 Shadow Distance 通常只有几十米,场景一大,远处的物体自然没有阴影。我在项目里把 Shadow Distance 从 40 调到了 120,远处的设备立刻就有了影子。要注意的是,阴影距离变大意味着更大区域的阴影贴图要覆盖同样分辨率的 Atlas,阴影精度会下降,这时候需要同时提高 Shadow Cascades。

在 URP 里,阴影的设置分散在 Light 组件、URP Asset 和 Quality Settings 三个地方。URP Asset 的 Shadows 选项里能设置 Shadow Atlas 尺寸、Cascade Count,还有距离。项目里出现过“PC 正常、WebGL 打包后阴影质量差一大截”的情况,原因就是 WebGL 平台的 Quality Level 和桌面平台不是同一个等级,阴影距离被不同平台的默认 Quality 给覆盖了。遇到这种跨平台差异,别在场景里死磕,先检查 Player Settings 里每个平台的 Quality 等级是否一致。

3.2 分辨率与 UI 适配

“Unity 分辨率设置”也是搜索热门。Canvas Scaler 的 UI Scale Mode 要选 Scale With Screen Size,参考分辨率一般设 1920x1080,Match Width Or Height 的值需要根据目标平台调。PC 上我习惯设成 0.5,这样宽高比变化时 UI 缩放能取一个中间值。但在 VR 平台上它不太适用,XR 里 UI 更合适用 World Space 放在固定距离。根据不同平台用不同 Canvas Scaler 是一个好习惯,否则在 PC 上正常的界面,到 PICO 4 里会变得大到超出视野。

3.3 烘焙和实时阴影的取舍

演示类项目很多物体是静态的,如果能预计算光照,烘焙阴影质量远高于实时阴影,而且运行时不耗性能。但烘焙有个问题:物体一旦移动,阴影就傻了。所以我的原则是:静态场景全部烘焙,动态物体用实时阴影,且实时光源数量控制在 1 到 2 个。烘焙的时候光照贴图分辨率不要贪高,256 和 512 在大多数场景下观感差别不大,但内存开销差很多。

还有一点经验:如果要根据设备性能动态切换质量等级,可以通过 QualitySettings.shadowDistance 和 QualitySettings.shadowCascades 在运行时调整,这比在面板里给每个平台配一套 Quality Level 灵活得多。

4. WebGL 发布与 IDBFS 写入失败的兼容性改造

4.1 WebGL 环境里的“文件系统”到底是什么

Unity 发布 WebGL 后,代码跑在浏览器的 JavaScript 环境里,不具备传统意义上的本地文件系统。Unity 为了让开发者能继续用 File.ReadAllBytes 这类 API,实现了一个虚拟文件系统。这个名字很少有人会注意,但其实它就是 IndexedDB。

也就是说,你在代码里往 Application.persistentDataPath 下写文件,实际是写进浏览器的 IndexedDB 数据库。IndexedDB 本身是异步的,Unity 通过 emscripten 的 IDBFS 机制把异步桥接成同步 API,让 C# 层的调用看起来像同步写文件。但这个桥接并不总是稳定的,一旦 IndexedDB 的写入失败,C# 层就会抛 IOException,这就是很多人搜索“WebGL 使用 IDBFS 写入失败”的根本原因。

4.2 写入失败的几种典型原因

排查这个问题,第一步是打开浏览器控制台,看 JavaScript 层有没有报错。常见的有这么几类:

  • 浏览器处于隐私模式:隐私模式下 IndexedDB 可能不可用,或者使用内存存储,刷新页面就清空。
  • 存储配额超出:浏览器给每个站点分配的存储空间是有限的,IndexedDB 和 CacheStorage 共享配额。WebGL 包体本身已经占了一部分 Cache,再写大量持久化数据就可能触顶。
  • 路径或文件名不合法:Windows 上合法的文件名,比如带盘符的绝对路径,在浏览器里会被拒绝。我遇到过写入路径带着中文名字导致失败的案例。
  • Unity 实例尚未完全初始化:某些情况下,在场景启动阶段立刻调用文件写入,底层 FS 模块还没准备好。
  • Site Data 被清理:用户清了浏览器缓存,IndexedDB 一起没了,程序如果假设文件存在,就会出现类似“写失败”的错误。

还有一点很重要:Unity 的 idbfs 是负责持久化 IndexedDB 的,但 WebGL 构建里还有一个 MEMFS 内存文件系统。如果你往 Application.temporaryCachePath 里写,那是写内存,刷新就丢。很多人其实不是写失败,而是把数据写到了 MEMFS,重新加载后文件不见了,误以为是写入失败。

4.3 我最后采用的兼容性改造方案

针对这个项目,我的处理思路分几步。

第一步,统一封装一个存储服务类。不再到处直接使用 File.WriteAllText,所有持久化读写都走 PersistentStorage.Save 和 PersistentStorage.Load。这样后续替换存储方案时不用改业务代码。

第二步,写入前先检查磁盘可用空间。浏览器没法直接拿到剩余空间,但可以通过 navigator.storage.estimate 接口获取。注意这个方法只在 WebGL 平台的 JavaScript 环境里有效,在 C# 里要写一个 .jslib 插件去调用。检查到剩余空间不足时,提前提示用户清理浏览器缓存,而不是等写入时炸。

第三步,尝试降低单次写入量。数据能否拆分就拆分,比如本来要把整张地图坐标一次性写成一个大 JSON,改成每帧保存一个小的增量数据。IndexedDB 单条数据写太大,失败率会明显上升。

第四步,加上写后校验。写完立即读回来比对字符串长度或哈希,发现不一致就重试一次。如果重试还失败,就把数据备份到 localStorage 或者提示用户手动导出。

最后一步,如果是纯浏览器的演示项目,最稳妥的方案是不写浏览器存储,直接通过 HTTP 接口把数据同步到后端服务器。既然演示场景有网络,为什么不把保存直接做成服务器请求呢?这个方案绕过了浏览器的所有限制,还天然支持多端同步。

5. 串口通信与外部设备联调的对接细节

5.1 SerialPort 的基本使用和平台限制

项目里有一个功能是读取真实设备的串口数据。在 Windows 桌面上,Unity 可以用 System.IO.Ports.SerialPort 直接打开 COM 口,代码很简单:指定端口名、波特率、数据位、校验位、停止位,然后调用 Open。

但 Unity 开发者必须清楚一个平台边界:这套 API 在 WebGL 下是完全不工作的。浏览器里没有访问串口的底层能力,除非你用 Web Serial API,但这个 API 默认只在 HTTPS 或 localhost 下可用,而且需要用户主动点击授权。项目里 Web 端最终走的是 WebSocket 转发方案,也就是在 PC 端用一个本地小工具把串口数据转发到 WebSocket 服务,Unity WebGL 页面通过 WebSocket 去订阅实时数据流。

和串口相关的高频坑有几个。第一个是丢权限,串口被其他程序占用,Open 会抛 UnauthorizedAccessException,这个异常要在启动时做 try-catch,给出明确的错误提示。第二个是读取数据溢出,串口缓冲区默认只有 4096 字节,如果设备以高频率发送大量数据,缓冲区会满,读取线程会阻塞。这个可以通过 SerialPort.ReadBufferSize 调大,同时限制设备发送频率。第三个是关闭阻塞,某些情况下 Close 会卡住线程,最好在独立线程里调用或设置超时。

5.2 数据帧解析和线程模型

串口数据的读取有一个原则:不能在主线程里循环读。串口没有数据到达事件,常见做法是开一个后台线程死循环 Read,把读到的字节放到一个队列缓冲区,主线程每帧从缓冲区取数据做解析。这中间涉及线程安全,我的做法是 ConcurrentQueue ,读线程往里塞,主线程往外取。

解析一帧数据要看设备协议。很多工业设备采用帧头 + 长度 + 数据 + 校验的结构。要注意“粘包”和“半包”问题:一次 Read 不一定能读到完整一帧,可能只读到前半帧,也可能一次读到两帧。稳妥的解析方式是用环形缓冲区,每次读取后先按帧头找到起始位置,再根据长度字段判断是否够一整帧,不够就继续等下一轮读到的数据,够了就完整切出来。

5.3 与西门子 PLC 通信的思路

热搜词里提到“unity 与西门子 PLC 通信”,这块我也简单验证过。Unity 与 PLC 通信,主流协议是 S7 协议或者 Modbus TCP。S7 协议在 Windows 下可以使用一些开源库,封装好之后就是 socket 读写,发送特定格式的报文去读写 DB 块和 M 区数据。注意 PLC 端的数据类型和字节序,不然读出来的 float 完全是乱码。PLC 通信更新频率很快,数据不断变化,肯定不能每个 UI 元素直接去同步,正确做法是把 PLC 数据快照保存到单例内存,UI 通过事件或者轮询去取快照。

6. 摄像机跟随、XR 适配与数字孪生场景组织

6.1 摄像机平滑跟随和防抖

“Unity 摄像机跟随”看起来是个简单需求,但要做好有几个细节。最常见的是直接把摄像机的 transform.position 设成目标位置加偏移,然后在 Update 里执行。这样虽然简单,但目标物体一旦有刚体运动或者动画,画面就会晃动,甚至会卡顿。

正确做法是把跟随逻辑放在 LateUpdate 里。LateUpdate 在每一帧的所有 Update 和物理更新之后执行,此时目标的位置已经定下来了,摄像机再去跟随,画面才稳定。跟随方式用 Vector3.SmoothDamp 或者插值平移,避免直接赋值造成的“跳变”感。

如果摄像机要跟随一个移动的物体同时还要保持朝向,可以用 Quaternion.Slerp 做旋转插值。还要注意摄像机穿墙问题:用射线从目标位置射向摄像机位置,如果中间有障碍物,就把摄像机拉近到碰撞点附近。这样可以避免视角穿透墙体。

6.2 PICO 4 和 XR 开发适配

这个项目需要在 VR 设备上展示,我用的 PICO 4 开发流程是:导入 XR Interaction Toolkit 和 PICO 的 OpenXR 插件,在项目设置里勾选 OpenXR 支持,把 XR Origin 预制体放到场景。PICO 4 的控制器映射默认能用,但交互按钮最好还是封装一层,避免后面切换到其他头显设备要改一大片代码。

MR 和 VR 切换是有一个热搜词。准确地说,VR 是纯虚拟场景,MR 是虚拟内容和真实环境融合。PICO 4 透过摄像头看到真实环境的混合现实模式,在 Unity 里通常通过切换 XR Origin 相机的背景类型和 Tracking Origin 模式来实现。要注意的是,MR 模式下虚拟物体的遮挡关系要依赖深度数据,PICO 的 SDK 不一定把所有深度数据暴露给 Unity,所以开发前最好先确认需求边界。

6.3 数字孪生场景的数据同步组织

数字孪生不等于普通 3D 展示,它更强调数据实时驱动虚拟模型。我的组织方式是把模型拆成“静态几何 + 动态节点”两层。静态几何烘焙好光照贴图,动态节点通过设备数据驱动,比如阀门开度、电机转速、传感器数值。

数据同步层做成一个独立的 SimulationManager,不接受 UI 直接绑定设备数据。设备协议可能有几十个数据点,但 UI 只需要其中几个,Manager 负责按需订阅,UI 层通过数据绑定或者事件更新。这样换协议、换设备的时候,UI 不需要动。

7. 性能优化与高频开发碎片速查

7.1 DrawCall 与合批

性能优化第一个要掐的是 DrawCall。UGUI 的每张图片都可能产生一次 DrawCall,场景里的模型如果没有合理合批,数量一多帧率就会掉。UGUI 的合批依赖图集,图集里没有的资源会被单独打成纹理,增加额外绘制。

我在项目中把 UI 图片统一放入图集,并关闭不需要 Raycast Target 的 Image 的 Raycast Target,这样既减少了 UI 的绘制开销,也降低了点击射线检测的成本。3D 场景里的静态物体在建模或导出时尽量使用相同材质,这比在 Unity 里强行做静态合批更有用。

7.2 宏定义、混淆与 GameAssembly.dll

Unity 宏定义配合不同平台或版本切换逻辑非常方便。比如我只想在 Editor 下打印调试日志,就写 #if UNITY_EDITOR,发布版本里这些代码会被剪掉。在 Player Settings 的 Scripting Define Symbols 里可以加自定义宏,比如区分“演示版”和“正式版”。

关于混淆,Unity 项目想保护 C# 代码不被轻易反编译,一般用第三方混淆工具配合 IL2CPP 使用。IL2CPP 会把 C# 转成 C++ 再编译成原生代码,PC 构建会生成 GameAssembly.dll,WebGL 则是编译成 wasm。IL2CPP 本身已经有相当强的抗反编译能力,缺点是有代码裁剪,反射调用被裁掉是常见问题。尽量避免对使用反射的类名做混淆,或者在混淆配置里加入白名单。

7.3 脚本控制逐渐消失与噪声地图

“Unity 脚本控制逐渐消失”这个热搜场景很常见:UI 渐隐、物体淡出。最简单的实现就是在协程里每帧修改 CanvasGroup.alpha 或者材质颜色的 alpha,持续几秒。如果用 DOTween,直接 DOTween.To 一个值就行,不过要处理好 OnComplete 里销毁物体。注意要判断对象是否还在场景里,避免协程访问空对象崩溃。

“Mathf.PerlinNoise 地图生成”属于另一类需求。PerlinNoise 能生成平滑的噪声值,用来做地形高度、程序化纹理很合适。使用时注意它会在一段时间后出现重复,最好结合不同的采样比例生成多层噪声,实现类似自然地形起伏的效果。

7.4 包体瘦身与加载优化

WebGL 包体的加载体验直接影响客户第一印象。大项目建议启用 Compression Format 为 Brotli,并配好服务器对 .br 文件的 Content-Encoding 头。Unity WebGL 的默认解压是流式加载,如果体积太大,首次加载和白屏时间都会很长。另一个建议是启动时只加载必要场景,其他场景用 Addressables 按需加载,不要一股脑全打进主包。

PC 客户端也同理,以 Addressables 方式加载大资源,能极大降低启动速度和内存占用。

这个项目从 PC 到 Web 再到 VR 设备,几乎每个平台都有各自的坑,但整体上 Unity 的跨平台能力帮我省了大量重复工作。如果让我给后来者一个建议,那就是在做多平台项目时,第一天就要把“平台差异”四个字当成一等公民来对待。Shadow、IDBFS、SerialPort、XR 追踪,这些模块的代码都尽量做成可替换的抽象层。等你交付给客户的那天,你会感谢当初多写的这几层封装。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 12:34:37

SpringBoot 3 + Vue 3 校园社团管理系统实战

简介:这是一套面向Java与前端初学者的全栈实践项目,聚焦校园社团管理场景,帮助开发者掌握SpringBoot后端开发与Vue前端框架的协同应用。资源包含133个文件,主体为55个Java源码(涵盖TeamsService、NoticesController等核…

作者头像 李华
网站建设 2026/9/14 12:31:58

Canvas+SVG协同实现水泡分裂动画:双图层分工与状态机设计

简介:一份基于HTML5 Canvas与SVG的彩色水泡分裂动画特效源码,代码量虽小却完整实现了从绘制到交互的动画链路,非常适合前端学习者、动画爱好者及Web交互视觉工程师用来研究两种图形技术的配合方式。压缩包本身只有3KB,没有多余文件…

作者头像 李华
网站建设 2026/9/14 12:30:57

STM32CubeIDE驱动ST7735S:从SPI初始化到DMA刷屏完整指南

简介:这是基于STM32CubeIDE非常详细地从零开始驱动ST7735S液晶屏的完整资料包,面向嵌入式初学者及需要快速点亮LCD显示界面的开发者。屏幕采用的驱动芯片为ST7735S,属于1.8英寸TFT全彩屏,通过SPI接口通信,分辨率128乘以…

作者头像 李华