CODESYS内置MQTT客户端:PLC免网关直连云端与Zigbee集成实战
简介工业自动化与物联网通信场景下一套基于CODESYS平台的MQTT客户端库与Zigbee2MQTT集成资源包面向PLC工程师和物联网系统集成开发者解决PLC设备与MQTT代理服务器间高效可靠双向数据传输、多代理连接及JSON数据交互问题。压缩包共38个文件、约6.6MB包含13个CODESYS项目工程、10个不同版本的MQTT功能库、6张配置/操作截图、多份txt与md说明文档、1份PDF附赠资料及许可文件从库安装、工程示例到文档查阅形成完整学习链路。目前已有88人学习下载。资源内提供Windows与Raspberry环境下的TLS/非TLS连接示例、主题与负载接口示例、CFC优势示例以及多个现成项目工程可直接参照搭建从PLC到MQTT代理的数据通道不同版本MQTT库便于对比升级结合Zigbee2MQTT集成与JSON数据处理能有效节省协议调试与系统集成时间。1. 基于CODESYS的MQTT客户端库到底解决了PLC上云的什么痛点一条生产线上有五六台CODESYS PLC触摸屏在工位旁跑得好好的可MES要产量、IoT平台要能耗、运维要看温湿度需求一来传统套路就是加一台网关盒子把Modbus/OPC UA转成MQTT再转发到服务器。网关盒子价格不低现场还得给它配电源、配网线、做IP规划多一个设备就多一个故障点。换一个思路干脆把MQTT客户端库直接跑在CODESYS控制器里让PLC自己就是一个MQTT客户端既能连本地的MQTT服务器也能连云端的物联网平台还能通过Zigbee2MQTT把现场Zigbee传感网络并进同一张消息总线。这套方案不需要额外硬件数据从PLC里直接发出延时比网关转发低一个量级而且是双向的PLC既上报数据也订阅外部的控制指令。这篇笔记就把这个方向怎么落地、参数怎么设、坑在哪里一条条讲清楚。2. 选型与原理为什么在CODESYS里做MQTT而不是加一台网关2.1 CODESYS平台上的MQTT客户端库从库加载到生命周期CODESYS 3.5的插件体系里库是功能打包的最小单位后缀名通常为.library内置一堆功能块FB和全局变量。MQTT客户端库的用处是把TCP Socket、报文编解码、发布订阅状态机、心跳发送这些脏活封装成几个功能块让PLC工程师不需要自己算报文头长度也不需要关心ACK包什么时候回来。工程侧的使用方式是在库管理器里右键“添加库”选择已经安装的MQTT库然后在程序里实例化对应的功能块。库本质上是编译进目标运行时的代码和标准库里的TON定时器一样。第三方库一般会提供几个版本区分TLS、JSON、多连接能力。我一般会挑支持动态连接参数、支持TLS、且允许多实例并发的库因为后面接多Broker时同一个功能块类要创建不止一个实例。生命周期上CODESYS里的MQTT客户端块通常是一个长期运行的FB状态在IDLE - CONNECTING - CONNECTED - RECONNECTING_WAIT之间切换。它和普通PLC程序不太一样不能放在一个只执行一次的任务里需要被每个扫描周期反复调用FB自己维护TCP状态机。如果库的设计是阻塞式的那么执行到网络发送时会卡住整个任务所以选库时一个关键问题就是它发送和接收是不是异步的异步状态下FB调用一个周期只推进一次状态机不等待服务器响应。还有一个容易被忽略的点库的运行与CODESYS任务的优先级。MQTT客户端应放在独立的周期任务或事件任务里扫描周期可以调到50ms到200ms不要放在默认的MainTask里跟运动控制抢时间。多Broker场景尤其注意见第3.2节。2.2 MQTT协议机制与CODESYS实现的对应关系MQTT协议看起来简单但实现客户端库时核心就四件事CONNECT握手、SUBSCRIBE订阅、PUBLISH发布、PINGREQ保活。CODESYS客户端库本质就是把这四个过程映射成功能块的输入输出。握手阶段客户端发送连接报文服务器返回CONNACK只有CONNACK报文里的返回码为0才表示连接被接受。多Broker连接时每个连接必须有独立的ClientID如果两台Broker共用同一个ID后连接的会强制断开先连接的这是MQTT协议明文规定的。很多PLC工程师在CODESYS里复用同一个ID连本机和云平台结果发现连上云端后本机连接就掉这就是原因。订阅和发布是异步的所以库一般用回调或者消息队列把收到的消息往PLC变量表上传。CODESYS不是事件驱动的语言因此现实做法是FB内部收到PUBLISH后把主题和载荷放进一块内存缓冲区PLC程序在下一个周期去查“是否有新消息到达”的标志位。消息到达标志应该只维持一个周期处理完手动复位否则重复扫描会重复处理同一条消息。MQTT的QoS等级对应到CODESYS里的参数就是连接建立后的一个枚举值。QoS 0是只管发不做确认QoS 1保证至少到达一次可能重复QoS 2保证恰好一次成本高。PLC侧上报数据用QoS 1最合适因为电机的启停状态、产量计数这些数据丢了不可接受但网络波动时重复一次也没有关系。订阅侧如果只是画面监控QoS 0就够把带宽留给重要的上报链路。这里对应了一个常见问题“mqtt怎么保证不丢失消息至少一次”答案就是发布端用QoS 1并开启Broker持久会话。保活机制对应CODESYS库里的KeepAlive参数单位是秒。PLC和Broker之间如果超过这个时间没有数据包Broker就认为设备掉线会断开连接。工业现场一般设30到60秒太短会导致频繁PINGREQ挤占带宽太长就失去了掉线检测的实时性。遗嘱消息Last Will在多Broker场景里很有用PLC意外断电时Broker会替它广播一条遗嘱消息Zigbee2MQTT或者SCADA收到后就能立刻把对应设备标记为离线而不用等TCP超时。2.3 Zigbee2MQTT的角色把Zigbee网络桥接到MQTT总线Zigbee2MQTT是一个开源桥接组件它把USB或者网络协调器上的Zigbee设备统一映射成MQTT主题。温湿度传感器、门磁、继电器、智能面板原本每种设备都要私有的协议解析Zigbee2MQTT把它们全部转换成JSON格式的MQTT消息对CODESYS来说看一条消息就会知道是哪个设备、什么事件、数值多少。在整体架构里CODESYS PLC、Zigbee2MQTT、MES/SCADA都是同一台MQTT服务器上的客户端。Zigbee2MQTT订阅协调器的串口数据把设备状态发布到zigbee2mqtt/device_name主题PLC如果想获取这个温湿度就去订阅这个主题。反过来PLC要控制Zigbee继电器就向zigbee2mqtt/device_name/set主题发布一条JSONZigbee2MQTT收到后转换成Zigbee协议帧发给继电器。这条链路里CODESYS侧的MQTT客户端库只负责传输真正让传感器数据和PLC变量对应起来的是主题规划与JSON载荷设计。Zigbee2MQTT默认就把数据定义为JSON对象例如{temperature:21.5,linkquality:98}所以客户端库背后必须有一套能处理JSON的代码垫着否则解析载荷就要写一大堆字符串分割逻辑这在CODESYS里是不现实的。3. 在CODESYS 3.5里落地MQTT客户端库建立多Broker连接的完整步骤3.1 工程配置添加库、定义连接参数与TLS证书先在CODESYS工程里确认库管理器已经加载MQTT客户端库。打开“库管理器”点“添加库”在列表中找名字类似IoT MQTT Client的库。如果库文件是外部拿到的.library文件通过“打开库文件”按钮导入工程会把它复制到本地缓存。接下来在全局变量里把两个Broker的连接参数定义清楚。建议不要把IP地址直接写在程序里统一放全局变量列表方便改参数或者用配方功能切换。VAR_GLOBAL // 本地Broker部署在车间交换机旁 BrokerLocal_Host : STRING : 192.168.0.10; BrokerLocal_Port : WORD : 1883; BrokerLocal_ClientID : STRING : plc_line1_mixer; // 云平台Broker用于远程监控 BrokerCloud_Host : STRING : broker.iot-example.com; BrokerCloud_Port : WORD : 8883; BrokerCloud_ClientID : STRING : plc_line1_mixer_cloud; BrokerCloud_KeepAlive : WORD : 30; // 连接开关 xEnableLocal : BOOL : TRUE; xEnableCloud : BOOL : FALSE; END_VAR这段代码定义了本地和云端两套连接参数。注意云端Broker端口用了8883走TLS所以本地Port用1883明文云端8883。两个ClientID必须不同这一点在多Broker场景里是第一优先级。然后配置TLS证书。CODESYS库的TLS认证一般不是直接引用系统证书而是要通过“套接字选项”或者专门的证书管理面板把CA证书文件导入到目标设备上。常见做法是把CA证书拷到控制器的文件系统里然后MQTT库的每个Client连接块提供一个证书路径输入。如果只是车间内部联调可以先关闭证书链验证用明文方式打通逻辑再开TLS。我一般会在库的输入引脚上看到bUseTLS、sCaCertPath这类参数将它们映射到全局变量避免写死在库里。3.2 用结构化文本(ST)实现多Broker连接管理与重连多Broker连接不是把两个Client实例简单堆在一起还要处理优先级切换、掉线重连、以及避免两个连接同时写入同一个内部变量。程序骨架如下。PROGRAM PRG_MqttManager VAR fbMqttLocal : FB_MqttClient; // 本地Broker客户端实例 fbMqttCloud : FB_MqttClient; // 云端Broker客户端实例 eLocalState : INT; // 本地连接状态 0未连接 1连接中 2已连接 3错误 eCloudState : INT; xStatusDirty : BOOL; // 状态有变化需要通知其他逻辑 tonReconnect : TON; // 重连间隔定时器 END_VAR // 本地Broker连接 fbMqttLocal.sHost : BrokerLocal_Host; fbMqttLocal.wPort : BrokerLocal_Port; fbMqttLocal.sClientID : BrokerLocal_ClientID; fbMqttLocal.bEnable : xEnableLocal; fbMqttLocal(); eLocalState : fbMqttLocal.eLinkState; // 云端Broker连接仅当本地连接失败时尝试 tonReconnect(IN : (eLocalState 2) OR (eCloudState 2), PT : T#5S); IF tonReconnect.Q AND xEnableCloud AND (eLocalState 2) THEN fbMqttCloud.sHost : BrokerCloud_Host; fbMqttCloud.wPort : BrokerCloud_Port; fbMqttCloud.sClientID : BrokerCloud_ClientID; fbMqttCloud.bEnable : TRUE; END_IF fbMqttCloud(); eCloudState : fbMqttCloud.eLinkState; // 连接状态一旦发生变化置位通知外部看板或维护程序 IF eLocalState fbMqttLocal.eLinkState OR eCloudState fbMqttCloud.eLinkState THEN xStatusDirty : TRUE; END_IF逻辑上这套代码先把本地Broker作为主通道云端作为故障转移。本地Broker状态不是“已连接”时云端连接才会被允许建立。两个FB每个扫描周期都要调用保证协议栈有时间发握手包、收ACK、处理超时。fbMqttClient.eLinkState是库自带的连接状态枚举每次调用后会刷新。bEnable是库的开关置TRUE时库才开始连接流程置FALSE则主动断开并按cleanSession规则清理会话。重连间隔设5秒已经足够不要小于1秒否则PLC崩溃重启时会把Broker的报文队列打爆。需要强调的一点多Broker连接中两份数据同时修改同一个内部变量时要加锁逻辑。比如产量计数器主Broker和云Broker都订阅了同一个MES指令主题那就要判断指令来源是否允许双写。常见做法是只允许主连接发布和订阅备份连接只做心跳监控主连接掉线后才切换成备份连接执行发布操作。这个思路能有效避免双Broker重复触发。3.3 订阅与发布JSON报文的构造、解析和错误处理MQTT库一般提供Subscribe和Publish两个操作引脚。下面演示发布一条指令到Zigbee2MQTT的温控器。// 构造JSON载荷不使用字符串拼接而是直接写入固定字符数组 sJsonPayload : STRING(80) : {state:heat,setpoint:21.5}; // 发布到温控器set主题QoS1不保留 fbMqttLocal.bPublishReq : TRUE; fbMqttLocal.sPublishTopic : zigbee2mqtt/thermostat/set; fbMqttLocal.sPublishPayload : sJsonPayload; fbMqttLocal.ePublishQoS : 1; fbMqttLocal.bPublishRetain : FALSE;CODESYS里的STRING(80)不是字符串而是定长字节数组。JSON长度一旦超过80后面的字符会被截断所以最稳妥的做法是建一个128或256字节的数组专门放载荷。bPublishReq是脉冲触发信号置TRUE后库开始发送发送完成或者失败时FB内部的xPublishDone、xPublishError会产生一个周期脉冲用户程序应该立刻复位请求位。订阅侧处理JSON更麻烦。Zigbee2MQTT上报的负载默认是JSON对象例如{temperature:21.5,linkquality:98}。解析时不要用FIND和MID去切字符串因为不同设备字段顺序会变而且JSON转义字符在ST里容易看走眼。我一般用库自带的JSON解析功能块按属性名取字段。如果手头库没有JSON能力就写一个固定结构体匹配先用JSONParser把对象拆开循环遍历键名匹配到temperature时把值转成REAL。// 伪代码对收到的载荷做解析并写入全局变量 IF fbMqttLocal.xMsgReceived THEN sReceivedTopic : fbMqttLocal.sMsgTopic; // 保存主题 aRecvPayload : fbMqttLocal.aMsgPayload; // 保存载荷 fbJsonParser.sJson : aRecvPayload; // 交给JSON解析器 fbJsonParser.bParse : TRUE; IF fbJsonParser.bSuccess THEN rTemperature : fbJsonParser.rGetValue(temperature); rSetpoint : fbJsonParser.rGetValue(setpoint); END_IF fbMqttLocal.xMsgReceived : FALSE; // 复位消息标志 END_IF这里需要说明rGetValue不是标准库内置块而是我封装的一个遍历键名取数值的小功能块你也可以直接用库提供的JsonDataStore。重要的是消息标志处理收到消息后必须立即把主题和载荷复制到本地变量然后才复位标志如果复位慢了下一条消息会把缓冲覆盖这会导致PLC侧数据跳跃。从经验来讲解析JSON失败时不要当场死机要保留原始载荷到一个日志数组方便工地现场拉出来对报文。还有一个容易被忽略的参数CODESYS的STRING最多支持多少字节取决于运行时配置。默认是80但MQTT库内部使用的接收缓冲可能更大所以从库缓存拷贝到用户字符串时要拿MIN函数做长度截断防止越界。越界在CODESYS里不会直接蓝屏但可能把相邻变量改掉这就是很多现场“状态灯乱闪”的根源。4. 与Zigbee2MQTT集成从主题规划到PLC侧JSON数据的双向映射4.1 Zigbee2MQTT侧主题与负载设计Zigbee2MQTT启动后默认会在Broker上创建一组主题前缀默认为zigbee2mqtt。设备上线后它会在zigbee2mqtt/friendly_name上发布设备状态其中friendly_name是设备在配置文件里的别名。把温控器命名为thermostat就能看到类似下面这样的主题结构。主题方向负载示例用途zigbee2mqtt/thermostat设备到Broker{temperature:21.5,linkquality:98}温度上报zigbee2mqtt/thermostat/setBroker到设备{state:heat,setpoint:21.5}PLC下发控制zigbee2mqtt/thermostat/action设备到Broker{action:button_on}面板按键事件zigbee2mqtt/bridge/state桥接到Brokeronline/offlineZigbee2MQTT总线状态zigbee2mqtt/thermostat/availability设备到Brokeronline/offline单设备在线状态这个表在Zigbee2MQTT的架构里是固定套路PLC工程里把主题名做成全局常量数组让SCADA组态时改配置就能对接新设备不用动逻辑代码。linkquality字段代表Zigbee信号质量范围0到255低于80时说明设备离协调器太远PLC侧可以在运维看板上弹提示。4.2 PLC侧双向数据流从状态上收到指令下发的完整链路数据流有两个方向。上行方向PLC周期性读取温度值打包成JSON发布到zigbee2mqtt/thermostat/set主题Zigbee2MQTT收到后交给Zigbee协调器最终控制温控器电磁阀。下行方向温控器上报的当前温度、运行状态发布到zigbee2mqtt/thermostatPLC订阅后更新到全局变量供PID或者HMI显示使用。实际编程里上行控制要加防抖。比如操作员在HMI上按了一下“升温”如果PLC每周期都发布一条{setpoint:21.5}Zigbee设备会被命令风暴淹没。我一般会在发布条件里加一个R_TRIG上升沿只有设定值变化时才发。// 发布去重逻辑设定值不变化就不重复发布 IF rSetpointOld rSetpointNew THEN fbPublish.bPublishReq : TRUE; rSetpointOld : rSetpointNew; END_IF下行数据接收要放在独立的PRG建议在Zigbee2MQTT的桥接状态是online时才处理消息。桥接重启期间Zigbee2MQTT会离线一段时间然后恢复如果PLC在离线期间持续收到旧消息变量可能被脏数据覆盖。所以收到状态报文时先判断bridge/state再决定是否把数据写入执行变量。4.3 关键参数设置QoS、Retain、连接超时与心跳这几个参数是设备上云最容易因小失大的地方。参数推荐值说明设备上报QoS1传感器数据可能用于产量统计不能丢控制指令QoS1漏发会造成设备无响应订阅主题QoS0监控广播类数据可容忍少量丢失Retain数据上报OFF状态上报ON掉线后新订阅端能立刻得到最后一次状态KeepAlive30到60秒车间局域网建议30秒云端路径建议60秒CleanSessionTRUE默认无历史积压避免重连后重放旧消息连接超时10秒云端TLS握手需要留足时间Retain要单独强调。PLC向Broker发布zigbee2mqtt/thermostat/set时如果设置RetainTRUEBroker会把这条指令保存下来新设备上线后会立刻收到这条指令可能造成误触发。因此控制命令类主题一律关闭Retain状态类主题如plc/mixer/runtime打开Retain这样MES系统断线重连后会立刻看到PLC的最后状态而不是等下一个上报周期。TLS连接时连接超时至少给10秒因为云端Broker在公网上证书链校验要往返好几轮。CODESYS库的wConnectTimeout参数单位是毫秒默认3000通常不够。把wConnectTimeout调到10000并将断开重连的心跳周期降到4秒。Zigbee2MQTT侧本身有advanced.availability_timeout配置默认0表示禁用availability。建议在configuration.yaml里把availability_timeout设为60设备超过60秒不活动时Zigbee2MQTT会自动发布offline到availability主题PLC订阅后就能自动将对应变量置为安全值。5. CODESYS MQTT集成避坑指南5个高频故障的现象、根源与处理5.1 PLC时间不同步导致TLS证书校验失败现象PLC连本地1883端口正常切到云端8883端口时CODESYS库报tls handshake failed偶尔还伴随connection reset by peer。原因TLS握手时客户端会校验服务器证书的有效期而PLC侧的实时时钟如果停留在2020年或因为断电复位到1970年证书有效期校验就永远失败。很多CODESYS控制器没有默认SNTP同步只有接了同步软件才校准时间车间里毫不在意时间结果一上TLS就翻车。解决在PLC程序里加一个SNTP客户端功能块或者在CODESYS设备树中启用“夏令时/时区”并配置NTP服务器地址。如果现场不能立即改时间先用明文1883端口打通功能所有证书校验逻辑留到控制器的时钟校准后再开。另外云端Broker证书需要包含服务器域名如果用IP地址访问必须确认证书里有对应IP的subjectAltName。5.2 JSON数组与字符串混用导致数据解析崩溃现象程序能收到消息但PLC间歇性进入Stop诊断缓冲区显示“应用程序看门狗超时”或“访问越界”。原因CODESYS的字符串是定长缓冲区JSON里的数组字段如[kitchen,livingroom]转换成CODESYS字符串数组时SPLIT操作容易越界另外Zigbee2MQTT某些设备会发送null值比如{position:null}JSON解析器访问.rGetValue(position)时返回一个无效浮点参与控制运算后导致程序异常。解决解析之前先判断字段是否存在CODESYS的JSON解析库一般提供bHasMember(name)的查询方法先查再取。对于null字段要额外判断类型标识。如果解析出错不要直接把消息丢弃把原始载荷写入环形日志同时把对应状态变量置成安全值。安全值应该根据设备类型定义温度取0读作无效开关量取默认关。5.3 多Broker连接阻塞SCAN周期现象工程里加了两个MQTT客户端实例后PLC扫描周期从正常4ms跳到200ms电机响应明显变迟钝。原因MQTT客户端库的网络操作不是异步的在功能块执行周期内直接调用了TCP套接字的发送和接收等待。当云端Broker网络抖动时发送等待最长会卡到连接超时时间这个时间如果设为10秒整个PLC单次任务周期就被卡住10秒看门狗不炸才怪。解决把MQTT管理程序放到独立周期任务比如新建一个Task_Comm周期设为100ms优先级放到中等与运动控制任务隔离。如果库本身不支持异步模式还有一条路是把两个Broker连接拆分到两个不同任务里各自用100ms的相位错开比如一个在上升沿执行一个在下降沿执行。这样即使一个连接卡住另一个任务周期不会受牵连。最彻底的方案是换一个基于异步Socket实现的MQTT库但目前第三方CODESYS库水平参差不齐我一般优先做任务隔离。5.4 Zigbee2MQTT设备掉线后PLC侧状态不刷新现象温湿度传感器电量耗尽后PLC画面里温度一直停留在最后一次上报值操作员误以为车间温度正常。原因Zigbee2MQTT不会传感器死后立即发布消息TCP层面也没有连接PLC侧只能等到下次上报才知道设备没了。如果PLC只订阅设备数据主题不订阅availability主题就无法感知设备离线。解决在Zigbee2MQTT的configuration.yaml中开启availability并通过availability_timeout自动上报离线PLC侧订阅zigbee2mqtt/thermostat/availability当收到offline时将温度变量强制写入无效标志画面上显示灰色或“无效”同时触发维护看板报警。同时给控制程序加保护设备离线期间禁止下发任何控制指令避免操作员在HMI上点了按钮却没有任何反馈Zigbee设备恢复后再重新开放控制。5.5 库版本与CODESYS编译器不一致导致编译报错现象别人给的工程在库管理器里显示红色叹号编译报Unknown library IoT MQTT 2.0.0或Library Manager - Cannot resolve library。原因第三方MQTT库通常绑定某个CODESYS版本区间比如CODESYS 3.5 SP17编译的库在SP12上可能缺少运行时接口或者库文件的版本号与目标设备运行时的库版本冲突。解决先在库管理器里确认目标设备型号对应的CODESYS运行时版本然后安装对应版本的.library文件不要直接用最新版覆盖。推荐把库文件放到工程根目录下的Libraries文件夹里勾选“从工程加载”而不是从全局库目录加载这样工程拷到现场电脑上还能编译。如果库有源码级的.exp或者.lib优先用和编译器匹配的版本再查库依赖关系树里有没有其他组件版本不一致比如SysSocket版本。6. 双通道验证法用MQTTX和Wireshark给整个链路做体检调试快结束时我发现单靠CODESYS侧的日志很难确认问题出在PLC、Broker还是Zigbee2MQTT。后来养成一个习惯在任何现场先用外部MQTT客户端做“标准消息注入”再用Wireshark抓包验证PLC的真实报文行为。MQTTX这类桌面客户端可以同时连到同一台Broker订阅PLC发布的所有主题。我用来验证两件事第一PLC上报的JSON是否符合预期结构第二从MQTTX手工发布zigbee2mqtt/thermostat/set主题观察PLC是否真的改变了内部变量动作。这比在CODESYS里打断点高效得多因为PLC的断点会停掉整个应用影响生产。Wireshark抓包主要关注三个点连接握手阶段是否收到CONNACK返回码是否为0PUBLISH报文里QoS标记是否和程序里设定一致设备掉线后是否发出DISCONNECT或者被Broker踢掉。抓包过滤器写成tcp.port 1883 or tcp.port 8883然后在报文解码里看MQTT协议层。如果是TLS看不到明文需要去Broker端看连接日志或者在Broker上临时开一个明文端口做对比测试。最后一步验证多Broker切换把本地Broker服务手动停掉观察PLC是否在5秒内切到云端连接并继续发布心跳。切换过程中HMI上的连接状态字要跟着变化Zigbee2MQTT的控制指令不能断超过10秒。这个测试我一般在产线停机时做测完后再把本地Broker恢复观察会不会自动切回主通道。我的经验是CODESYS里做MQTT这个方向值得投入。它省掉了网关的采购成本和维护成本数据从PLC到云端只剩一跳延迟可以压到几百毫秒内。但前提是库要选异步实现的任务要单独隔离JSON解析要做异常兜底。每次去现场前我都会把连接参数、证书、主题路径写成一个检查表先测握手再测心跳最后测业务报文这样真的能少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取