Cypress USB3.0驱动开发实战:从枚举到300MB/s带宽调优

1. 为什么我要啃 Cypress USB3.0 驱动这块硬骨头

搞嵌入式的人多少都碰过 Cypress 的 USB 芯片,尤其是 FX3 这一系列,在工业相机、数据采集卡、高速传输模块里出镜率极高。但很多人拿到开发板之后,跑个官方例程、点个灯、传几帧数据就觉得完事了,真正到了自己要做产品、要对接上层软件、要处理各种异常的时候,才发现驱动这一层的水比想象中深得多。我这次做的项目,就是围绕 Cypress USB3.0 控制器做一套完整的驱动程序适配,从设备枚举、固件加载、端点配置到批量传输的稳定性调优,全部走了一遍。

先说清楚这套东西是什么。Cypress 的 USB3.0 方案核心是一颗带可编程接口的控制器芯片,内部集成了 USB3.0 物理层和链路层,对外通过 GPIF II 或者从 FIFO 接口连接 FPGA、图像传感器、ADC 这类数据源,对内通过 USB3.0 总线和主机通信。驱动程序要干的事情,就是在主机操作系统这一侧,把这个设备识别出来、配置好、建立数据通道,然后让上层应用能够稳定地收发数据。听起来简单,但实际做下来,枚举失败、驱动签名、端点缓冲区分配、DMA 描述符管理、传输超时、带宽跑不满,每一个环节都能卡你两三天。

这套内容适合谁看?如果你正在用 Cypress 的 USB3.0 芯片做产品开发,或者你手头有一个需要高速数据传输的项目,再或者你是个嵌入式软件工程师想补齐 USB 驱动这一块的短板,那这篇东西应该能帮你少走不少弯路。我会把整个设计思路、关键细节、实操步骤和踩过的坑都摊开讲,代码和配置能给的地方尽量给全,让你看完能直接上手改。

2. 整体方案设计与技术选型拆解

2.1 驱动架构的两种路线:内核态还是用户态

做 USB 设备驱动,第一个要做的决策就是驱动跑在哪一层。Cypress 官方给了两套参考:一套是基于操作系统原生 USB 驱动框架的内核态驱动,另一套是借助通用 USB 访问库在用户态直接操作设备。这两条路各有各的活法,选错了后面会很难受。

内核态驱动的优势在于性能和系统集成度。数据从设备到应用层走的是内核缓冲区,省掉了用户态和内核态之间的多次拷贝,对于高带宽场景更友好。而且内核态驱动可以注册标准的设备接口,上层应用通过文件操作或者 IOCTL 就能访问,和其他系统组件的配合也更顺畅。但代价是开发调试成本高,一个空指针就能让整个系统蓝屏,而且每次改代码都要重新编译、签名、重启,迭代速度慢得让人抓狂。

用户态方案则是通过通用的 USB 访问库直接打开设备,用库提供的 API 做控制传输和批量传输。开发效率高,调试方便,改完代码直接跑,不用碰内核。缺点是性能上限受限于库的实现方式,而且对设备的独占访问、电源管理、热插拔响应这些系统级行为的控制力弱一些。

我这次的选型思路是这样的:项目对传输带宽有明确要求,目标是在 USB3.0 下稳定跑到 300MB/s 以上的有效吞吐,同时需要支持多设备并发和热插拔自动重连。综合考虑之后,我选择了内核态驱动为主、用户态工具为辅的混合架构。内核态驱动负责设备枚举、端点管理和高速数据通道,用户态则做一个轻量的测试和配置工具,方便现场调试。这个决策背后的逻辑是:性能关键路径必须走内核态,但调试和运维的灵活性不能丢。

提示:如果你只是做原型验证或者小批量数据采集,用户态方案完全够用,别一上来就啃内核驱动,时间成本划不来。等带宽需求真的上去了再迁移也不迟。

2.2 端点规划与缓冲区分配的核心逻辑

USB3.0 的端点模型和 USB2.0 有本质区别,最明显的就是批量传输端点多了流的概念,而且端点缓冲区的大小直接决定了单次传输的效率。Cypress 这颗芯片通常配置一个控制端点、若干个批量输入和批量输出端点,具体数量取决于固件里的描述符配置。

我在规划端点的时候遵循了几个原则。第一,控制端点只用来做枚举和少量配置命令,绝不拿它传业务数据,因为控制传输的协议开销大、速率低。第二,批量输入和批量输出分开使用独立的端点,避免双向数据流互相干扰。第三,每个批量端点的最大包长按照 USB3.0 的规范设成 1024 字节,这是硬性上限,设大了枚举直接失败。第四,端点缓冲区的大小要结合突发长度来算,不能拍脑袋定。

这里展开说一下缓冲区大小的计算过程。USB3.0 批量传输的突发长度最大可以到 16 个包,也就是单次突发最多 16×1024=16KB。驱动侧的缓冲区如果小于这个值,就会频繁触发短包和中断,吞吐量上不去。我的做法是把驱动接收缓冲区设成突发长度的整数倍,比如 64KB 或者 128KB,这样每次中断能处理多个完整突发,减少了上下文切换的开销。实测下来,64KB 缓冲区在 300MB/s 的速率下中断频率大约在每秒四千多次,CPU 占用可以接受;如果降到 16KB,中断频率翻四倍,CPU 占用明显上升。

2.3 固件加载策略:谁先谁后的问题

Cypress 的 USB3.0 芯片有个特点,上电后它首先是一个未配置的默认设备,需要主机把固件下载进去,它才会重新枚举成目标设备。这就带来一个时序问题:驱动必须在设备第一次枚举时就识别出它是待加载状态,然后完成固件下载,再等待它重新枚举。

我的方案是在驱动里内置一个状态机,分三个阶段处理。第一阶段,设备以默认标识枚举,驱动匹配到这个标识后,通过控制端点把固件镜像分块写入芯片的 RAM。第二阶段,固件写入完成后触发芯片复位,设备断开并以新的标识重新枚举。第三阶段,驱动匹配到新标识后,按照目标设备的配置完成端点初始化和数据通道建立。

这个流程里最容易出问题的是第二阶段到第三阶段的过渡。设备复位后重新枚举需要时间,如果驱动在这段时间内做了错误的假设,比如以为设备还在、继续往旧端点发数据,就会触发传输错误。我的处理方式是监听系统的设备变更通知,在收到设备移除事件后清理所有资源,然后等待新的设备到达事件,重新走一遍初始化流程。这个逻辑听起来简单,但实际写的时候要处理好并发和资源释放的顺序,否则很容易出现资源泄漏或者竞态条件。

3. 核心细节解析与实操要点

3.1 设备枚举阶段的描述符解析

设备枚举是整个驱动工作的起点,枚举成功与否直接决定了后面能不能干活。枚举过程本质上是主机通过控制端点读取设备的一系列描述符,包括设备描述符、配置描述符、接口描述符和端点描述符。驱动要做的就是解析这些描述符,提取出端点数量、传输类型、最大包长、间隔等关键信息。

我在解析描述符时踩过一个坑:配置描述符的总长度字段和实际读取到的数据长度不一致。原因是设备返回的配置描述符包含了多个接口和端点,总长度比较大,而我在第一次读取时只请求了前 9 个字节,拿到总长度后再读剩余部分,但第二次读取的缓冲区没有按总长度精确分配,导致解析越界。后来改成先读 9 字节拿到总长度,再按总长度精确分配缓冲区重新读取,问题解决。

另一个需要注意的点是端点描述符里的最大包长字段。USB3.0 的批量端点最大包长固定是 1024 字节,但有些固件配置里会写成 512,这时候驱动如果按 1024 去分配缓冲区,实际传输时会出现包边界错位。我的做法是在解析时校验这个值,如果发现不符合 USB3.0 规范就记录警告,并按实际值来配置,避免后续传输出错。

/* 描述符解析的核心逻辑示意 */ int parse_config_descriptor(uint8_t *buf, int len, struct dev_config *cfg) { int offset = 0; while (offset < len) { uint8_t desc_len = buf[offset]; uint8_t desc_type = buf[offset + 1]; if (desc_len == 0) break; switch (desc_type) { case DESC_TYPE_INTERFACE: cfg->iface_num = buf[offset + 2]; cfg->alt_setting = buf[offset + 3]; cfg->ep_count = buf[offset + 4]; break; case DESC_TYPE_ENDPOINT: /* 提取端点地址、属性、最大包长 */ add_endpoint(cfg, &buf[offset]); break; default: break; } offset += desc_len; } return 0; }

3.2 端点配置与传输类型的选择依据

端点配置这一步,核心是把每个端点的传输类型、方向、最大包长和间隔设置对。Cypress 这颗芯片支持控制传输、批量传输和中断传输,具体用哪种取决于业务需求。批量传输适合大数据量、对实时性要求不高的场景,比如图像数据、文件传输;中断传输适合小数据量、对延迟敏感的场景,比如状态上报、命令响应。

我的项目里,数据通道用的是批量传输,因为图像数据量大、允许一定的延迟抖动。命令通道用的是中断传输,因为控制命令需要及时响应,而且数据量很小。这个搭配是经过实测验证的:批量传输在 300MB/s 的速率下延迟在毫秒级,完全满足图像采集的需求;中断传输的间隔设成 1 毫秒,命令响应延迟稳定在 2 毫秒以内。

端点配置里还有一个容易被忽略的参数是突发长度。USB3.0 的批量端点支持配置突发长度,范围是 1 到 16。突发长度设得越大,单次传输的效率越高,但对端点的缓冲区要求也越高。我一开始把突发长度设成 16,结果发现设备侧的 FIFO 深度不够,频繁出现溢出错误。后来降到 8,配合 64KB 的驱动缓冲区,传输就稳定了。这个参数没有万能值,必须结合设备侧的硬件资源来调。

端点类型传输类型最大包长突发长度缓冲区大小适用场景
EP1 IN批量1024864KB图像数据上传
EP2 OUT批量1024864KB配置数据下发
EP3 IN中断102414KB状态上报
EP4 OUT中断102414KB命令下发

3.3 驱动签名与系统兼容性处理

在主流操作系统上加载内核态驱动,签名是绕不过去的坎。没有签名的驱动在默认配置下会被系统拒绝加载,开发阶段可以通过临时关闭签名强制来绕过,但产品发布必须走正式签名流程。

我的处理策略是分环境对待。开发调试阶段,在测试机上开启测试签名模式,用自签名证书给驱动签名,这样改完代码重新签名就能加载,迭代速度快。产品发布阶段,走正式的签名流程,把驱动提交给签名服务,拿到正式签名后再集成到安装包里。这里要注意的是,测试签名和正式签名的证书链不同,不能混用,否则会出现签名验证失败的问题。

还有一个兼容性细节:不同版本的操作系统对驱动的要求不一样。较新的系统版本对驱动的安全要求更严格,比如强制要求驱动支持某些安全特性。我在适配时就遇到过驱动在旧版本上跑得好好的,在新版本上加载失败的情况,排查后发现是新系统要求驱动必须声明兼容性等级。解决办法是在驱动配置文件里补充相应的声明,重新编译签名后问题解决。

注意:驱动签名证书有有效期,产品发布前一定要检查证书是否在有效期内,过期证书签出来的驱动在部分系统上会被拒绝加载。这个坑我踩过一次,临发布前三天发现证书过期,紧急重新签名,差点耽误交付。

4. 实操过程与核心环节实现

4.1 开发环境搭建与依赖准备

动手写代码之前,环境得先搭好。我用的开发环境是主流桌面操作系统加对应的驱动开发工具链,编译器、调试器、驱动框架库一个都不能少。具体来说,需要安装驱动开发包、调试工具、以及用于测试的 USB 协议分析工具。

驱动开发包提供了内核态驱动开发所需的头文件、库和构建脚本。调试工具用于内核态调试,可以附加到运行中的系统上查看驱动状态、设置断点、检查内存。USB 协议分析工具则是硬件层面的,接在主机和设备之间抓取总线上的数据包,枚举失败或者传输异常的时候,这个工具能救命。

环境搭建的顺序建议是:先装驱动开发包,验证工具链能编译通过一个空驱动;再装调试工具,验证能附加到系统;最后接上协议分析工具,验证能抓到枚举过程的数据包。这个顺序的好处是每一步都有明确的验证点,出了问题容易定位。

# 驱动构建脚本的核心步骤示意 # 1. 清理旧的构建产物 make clean # 2. 编译驱动 make ARCH=x86_64 # 3. 生成签名请求文件 make sign_request # 4. 用测试证书签名(开发阶段) sign_tool sign -cert test_cert.pem -key test_key.pem driver.sys # 5. 安装驱动 install_tool install driver.sys

4.2 固件下载流程的完整实现

固件下载是驱动初始化里最关键的环节,流程走不对,设备根本进不了工作状态。我把这个过程拆成了五个步骤,每一步都有明确的完成标志。

第一步,打开设备。驱动通过系统提供的设备访问接口打开默认标识的设备,拿到设备句柄。这一步的完成标志是句柄有效,如果打开失败,通常是权限问题或者设备没被系统识别。

第二步,查询设备信息。通过控制传输读取设备的描述符,确认芯片型号和固件加载状态。这一步的完成标志是读到有效的设备描述符,并且状态字段表明设备处于待加载状态。

第三步,分块写入固件。把固件镜像按控制端点的最大包长切分成若干块,逐块通过控制传输写入芯片的 RAM。每写一块都要检查返回状态,任何一块失败都要回滚重来。这一步的完成标志是所有块都写入成功,并且芯片返回确认。

第四步,触发复位。通过一个特定的控制命令让芯片复位,设备会从总线上断开,然后以新的标识重新枚举。这一步的完成标志是收到设备移除通知。

第五步,等待重新枚举。监听设备变更通知,等待新标识的设备到达,然后重新走一遍枚举和端点配置流程。这一步的完成标志是新设备枚举成功,端点配置完成。

/* 固件下载的核心流程示意 */ int download_firmware(device_handle_t dev, const uint8_t *fw, size_t fw_len) { size_t offset = 0; int ret; while (offset < fw_len) { size_t chunk = min(fw_len - offset, CTRL_MAX_PACKET); ret = control_write(dev, REQ_FW_DOWNLOAD, offset, fw + offset, chunk); if (ret < 0) { log_error("firmware download failed at offset %zu", offset); return ret; } offset += chunk; } ret = control_write(dev, REQ_FW_RESET, 0, NULL, 0); if (ret < 0) { log_error("firmware reset command failed"); return ret; } return 0; }

这个流程里有个细节值得单独说:分块写入的块大小。控制端点的最大包长是 64 字节,但实际写入时不一定非要按 64 字节切。我一开始严格按 64 字节切,结果固件有 200 多 KB,切出来三千多块,每块一次控制传输,整个下载过程花了将近十秒。后来改成按 4KB 切块,每块内部再按 64 字节分包,控制传输的次数降到五十多次,下载时间缩短到一秒以内。这个优化的原理是减少控制传输的协议开销,因为每次控制传输都有固定的建立和握手阶段,块太小的话开销占比太高。

4.3 批量传输通道的建立与调优

批量传输通道建立起来之后,真正的挑战才开始:怎么把带宽跑满、怎么保证长时间运行的稳定性。我在这上面花的时间比前面所有环节加起来都多。

先说带宽。USB3.0 的理论带宽是 5Gbps,换算成字节是 625MB/s,但实际有效吞吐受协议开销、端点缓冲区、驱动处理效率等多重因素影响。我最初跑出来的吞吐只有 150MB/s 左右,离目标差了一大截。排查过程是这样的:先用协议分析工具看总线上的数据包,发现突发传输的利用率不高,很多突发只传了几个包就结束了;再看驱动侧的中断统计,发现中断频率异常高,说明缓冲区太小导致频繁中断。

调整方案分两步。第一步,把驱动接收缓冲区从 16KB 增加到 64KB,中断频率降下来,吞吐提升到 220MB/s。第二步,把设备侧的突发长度从 4 调到 8,让每次突发传输的数据量更大,吞吐进一步提升到 310MB/s。两步调整之后,总线利用率明显改善,协议分析工具上能看到连续的满突发传输。

再说稳定性。长时间跑批量传输,最怕的是偶发的传输错误导致通道卡死。我遇到过两种情况:一种是设备侧 FIFO 溢出,表现为传输超时;另一种是驱动侧缓冲区管理出错,表现为数据错位。第一种情况的解决办法是在设备固件里增加流控机制,当 FIFO 快满时主动降低发送速率;第二种情况的解决办法是在驱动里严格管理缓冲区的读写指针,确保不会出现覆盖或者越界。

/* 批量传输的缓冲区管理核心逻辑示意 */ struct transfer_ctx { uint8_t *buf; size_t buf_size; size_t write_pos; size_t read_pos; spinlock_t lock; }; int submit_bulk_transfer(struct transfer_ctx *ctx, struct urb *req) { unsigned long flags; size_t available; spin_lock_irqsave(&ctx->lock, flags); available = ctx->buf_size - (ctx->write_pos - ctx->read_pos); if (available < req->transfer_buffer_length) { spin_unlock_irqrestore(&ctx->lock, flags); return -ENOSPC; } /* 环形缓冲区写入,处理回绕 */ copy_to_ring_buffer(ctx, req->transfer_buffer, req->transfer_buffer_length); ctx->write_pos += req->transfer_buffer_length; spin_unlock_irqrestore(&ctx->lock, flags); return 0; }

4.4 热插拔与异常恢复机制

产品化阶段,热插拔是必须支持的场景。用户不会管你是不是在传输数据,拔了线就得能恢复。我的实现思路是监听系统的设备变更通知,在设备移除时立即停止所有传输、释放资源、标记通道为断开状态;在设备到达时重新走初始化流程,恢复通道。

这个机制里最难处理的是传输过程中的突然断开。如果驱动正在等待一个批量传输完成,设备突然没了,这个传输请求会一直挂起,直到超时。我的处理方式是在设备移除通知到达时,主动取消所有挂起的传输请求,让等待方立即返回错误,而不是傻等超时。取消请求的接口由系统提供,调用时要注意加锁,避免和传输完成回调产生竞态。

异常恢复还有一个场景是传输错误后的通道重建。有时候设备没断开,但某个端点进入了错误状态,后续传输全部失败。我的做法是在检测到连续多次传输错误后,主动复位端点,清空缓冲区,重新配置端点参数,然后恢复传输。这个逻辑要设置一个重试上限,避免在设备真的故障时无限重试。

提示:热插拔测试一定要用真实的物理拔插,不要用软件模拟的设备移除。软件模拟和物理拔插在时序上有差异,很多问题只有物理拔插才能暴露出来。我就是在物理拔插测试时发现了一个资源释放顺序的 bug,软件模拟完全测不出来。

5. 常见问题与排查技巧实录

5.1 枚举失败的五种典型原因

枚举失败是新手最常遇到的问题,表现是设备插上之后系统没有任何反应,或者设备管理器里显示一个未知设备。我把遇到过的情况整理成了下面这张表,按出现频率排序。

现象可能原因排查方法解决方式
设备完全无反应供电不足或硬件连接问题测量供电电压,检查连接器更换供电方案或线缆
显示未知设备驱动未匹配或描述符错误用协议分析工具抓枚举数据修正驱动匹配规则或描述符
枚举中途失败描述符长度或内容不合法逐字节检查描述符按规范修正描述符
固件加载后不重新枚举复位命令未生效检查复位命令的返回状态修正复位时序或命令参数
枚举成功但端点配置失败端点参数超出芯片能力对照芯片手册检查端点配置调整端点数量或缓冲区大小

这张表里的每一条我都实际遇到过。最隐蔽的是第三条,描述符长度不合法。当时设备能枚举到一半,读到配置描述符就失败了,协议分析工具上看到主机请求了 9 字节,设备返回了 9 字节,但总长度字段写的是 32,主机接着请求剩余 23 字节,设备却返回了错误。排查了半天才发现是固件里配置描述符的总长度字段算错了,少算了端点描述符的长度。这种问题只能靠逐字节对照规范来查。

5.2 传输超时与数据丢失的排查思路

传输超时和数据丢失是批量传输阶段的高频问题,两者的表现相似但根因不同,排查思路也不一样。

传输超时的典型表现是传输请求发出后长时间没有完成,最终返回超时错误。根因通常是设备侧没有及时响应,可能是 FIFO 空了或者满了、固件处理逻辑卡住、端点配置错误。排查时先看设备侧的状态寄存器,确认 FIFO 的水位;再看固件里是否有死循环或者阻塞操作;最后检查端点配置是否和固件里的一致。

数据丢失的典型表现是传输完成了,但收到的数据不完整或者有错位。根因通常是缓冲区管理问题,可能是驱动侧缓冲区溢出、读写指针不同步、或者短包处理不当。排查时先在驱动里加日志,记录每次传输的长度和缓冲区状态;再对照协议分析工具上的数据包,看总线上实际传了多少数据;两者对比就能定位是驱动丢了数据还是设备没发全。

我遇到过一次很诡异的数据丢失:传输成功率大概 99%,每隔一两分钟就丢一包。查了很久才发现是驱动里的一个计数器溢出问题,传输次数累计到某个值之后回绕,导致缓冲区指针计算错误。这种问题靠代码审查很难发现,最后是在驱动里加了断言,一旦指针异常就打印现场,才定位到根因。

5.3 带宽不达标的系统性调优方法

带宽不达标是性能优化阶段的核心问题,我总结了一套从下往上的排查方法,按层次逐级确认。

第一层,确认物理链路。USB3.0 对线缆和连接器有要求,劣质线缆会导致链路降速到 USB2.0。用协议分析工具看链路训练的结果,确认协商速率是 5Gbps 而不是 480Mbps。这一步不过关,后面怎么调都没用。

第二层,确认设备侧的数据源能力。设备侧的 FIFO 深度、固件处理速度、数据源本身的速率,任何一个环节成为瓶颈,整体带宽都上不去。我的做法是让设备侧先跑一个内部环回测试,确认设备自己能跑到的上限,再和主机侧对比。

第三层,确认驱动侧的缓冲区配置。缓冲区太小导致频繁中断,缓冲区太大导致延迟增加,要找到平衡点。我的经验值是缓冲区大小取突发长度的四到八倍,中断频率控制在每秒几千次以内。

第四层,确认应用侧的读取效率。应用层如果读取不及时,驱动缓冲区会满,传输就会阻塞。我的做法是在应用层用独立的线程做读取,读取缓冲区设得比驱动缓冲区大,确保应用层不会成为瓶颈。

这四层逐级排查下来,基本能定位到带宽瓶颈在哪。我那次从 150MB/s 调到 310MB/s,瓶颈就在第三层和第四层,缓冲区配置和应用层读取效率各贡献了一半的提升。

5.4 驱动卸载与资源清理的注意事项

驱动卸载看起来是个收尾工作,但处理不好会留下各种后遗症,比如设备无法重新加载、系统资源泄漏、甚至系统不稳定。我在这个环节踩过的坑不比前面少。

资源清理的顺序很重要。正确的顺序是:先停止所有传输、取消挂起的请求,再释放缓冲区内存,然后注销设备接口,最后释放设备句柄。这个顺序反了就会出问题,比如先释放了缓冲区再取消传输,传输回调里访问已释放的内存,直接崩溃。

还有一个容易忽略的点是并发访问的保护。驱动卸载时,可能还有应用层的线程在访问设备接口,如果直接释放资源,这些线程会访问到野指针。我的做法是在卸载流程开始时先设置一个标志位,拒绝新的访问请求,然后等待所有正在进行的访问完成,再释放资源。这个等待要有超时机制,避免某个线程卡死导致卸载流程永远完不成。

注意:开发阶段频繁加载卸载驱动时,一定要检查每次卸载后系统资源是否完全释放。我遇到过卸载后设备句柄没释放,导致下次加载时打开设备失败的问题,重启系统才能恢复。后来在卸载流程里加了资源泄漏检测,每次卸载后打印未释放的资源列表,问题就一目了然了。

6. 我在这个项目里攒下的几条实在经验

驱动调通那天,我把整个流程从头到尾又跑了一遍,确认每个环节都稳定,才敢松口气。回过头看,这个项目里最耗时间的不是写代码,而是排查那些看起来莫名其妙的问题。枚举失败查描述符查了一整天,带宽调优反复试参数试了三天,热插拔的竞态问题更是断断续续跟了一个星期。

如果让我给后来者几条建议,第一条是协议分析工具的钱不能省。很多问题在代码层面看不出来,但总线上抓包一看就明白了。第二条是缓冲区大小和突发长度这两个参数要一起调,单独调一个往往达不到最优。第三条是热插拔和异常恢复一定要在开发早期就纳入测试,不要等到产品化阶段再补,那时候改架构的成本太高了。

这套驱动现在跑在几个不同的硬件配置上,连续运行几十个小时没有出现传输错误,带宽稳定在 300MB/s 以上,热插拔恢复时间在两秒以内。后续我打算把固件升级功能也集成到驱动里,让设备可以在不拆机的情况下更新固件,这个功能在工业现场很有用,等做完了再单独整理一篇。