<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Equipment Software on 纪伟的个人博客</title><link>https://www.jiwei.space/categories/equipment-software/</link><description>Recent content in Equipment Software on 纪伟的个人博客</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Wed, 05 Aug 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://www.jiwei.space/categories/equipment-software/index.xml" rel="self" type="application/rss+xml"/><item><title>设备通信与协议编程（十一）：OPC UA 信息模型、订阅与互操作边界</title><link>https://www.jiwei.space/posts/equipment/device-communication/11-opc-ua/</link><pubDate>Wed, 05 Aug 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/11-opc-ua/</guid><description>&lt;p&gt;串口、Modbus 和厂商 TCP 协议主要解决“如何交换字段”，OPC UA 更进一步：设备把对象、变量、方法、事件和它们之间的关系组织为可浏览的信息模型，客户端通过标准服务访问。&lt;/p&gt;
&lt;p&gt;OPC UA 不是一个简单的“统一寄存器协议”。真正的互操作来自一致的信息模型、profile、安全配置和语义约定。&lt;/p&gt;
&lt;h2 id="1-addressspace-是带类型的图"&gt;&lt;a href="#1-addressspace-%e6%98%af%e5%b8%a6%e7%b1%bb%e5%9e%8b%e7%9a%84%e5%9b%be" class="header-anchor"&gt;&lt;/a&gt;1. AddressSpace 是带类型的图
&lt;/h2&gt;&lt;p&gt;服务器向客户端暴露的 AddressSpace 由 Node 构成，Node 通过 Reference 相连。常见 NodeClass 包括 Object、Variable、Method、ObjectType、VariableType、ReferenceType 和 DataType。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Machine (Object)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── State (Variable)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── Temperature (Variable)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── Start (Method)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── Alarm (Event source)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Variable 不只是值，还可带 DataType、ValueRank、AccessLevel、工程单位、量程和时间戳。客户端应读取并验证这些元数据，而不是只把所有值转成字符串。&lt;/p&gt;
&lt;h2 id="2-nodeidbrowsename-与-displayname"&gt;&lt;a href="#2-nodeidbrowsename-%e4%b8%8e-displayname" class="header-anchor"&gt;&lt;/a&gt;2. NodeId、BrowseName 与 DisplayName
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;标识&lt;/th&gt;
 &lt;th&gt;用途&lt;/th&gt;
 &lt;th&gt;稳定性注意&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;NodeId&lt;/td&gt;
 &lt;td&gt;在服务器 AddressSpace 中标识 Node&lt;/td&gt;
 &lt;td&gt;namespace index 可能随配置变化&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ExpandedNodeId&lt;/td&gt;
 &lt;td&gt;可携带 namespace URI 和 server index&lt;/td&gt;
 &lt;td&gt;更适合跨地址空间表达&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;BrowseName&lt;/td&gt;
 &lt;td&gt;浏览路径中的限定名&lt;/td&gt;
 &lt;td&gt;不保证全局唯一&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;DisplayName&lt;/td&gt;
 &lt;td&gt;面向用户的本地化文本&lt;/td&gt;
 &lt;td&gt;不应作为程序主键&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;客户端配置应优先保存 namespace URI 与标识符的组合，并在连接后解析当前 namespace index。把 &lt;code&gt;ns=2;s=Machine.State&lt;/code&gt; 永久写死，服务器新增 namespace 后可能指向错误模型。&lt;/p&gt;
&lt;h2 id="3-类型模型带来的价值"&gt;&lt;a href="#3-%e7%b1%bb%e5%9e%8b%e6%a8%a1%e5%9e%8b%e5%b8%a6%e6%9d%a5%e7%9a%84%e4%bb%b7%e5%80%bc" class="header-anchor"&gt;&lt;/a&gt;3. 类型模型带来的价值
&lt;/h2&gt;&lt;p&gt;ObjectType 和 VariableType 允许服务器声明设备类型及其实例结构。Companion Specification 则为机器人、分析仪器、机床等领域定义共同语义。&lt;/p&gt;
&lt;p&gt;互操作的层次可分为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;能建立安全连接；&lt;/li&gt;
&lt;li&gt;能调用 Read、Write、Browse 等服务；&lt;/li&gt;
&lt;li&gt;能理解相同 DataType 与单位；&lt;/li&gt;
&lt;li&gt;能识别相同设备类型、状态和方法语义。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只达到前两层，往往仍需要项目级点表和适配代码。&lt;/p&gt;
&lt;h2 id="4-从发现到会话"&gt;&lt;a href="#4-%e4%bb%8e%e5%8f%91%e7%8e%b0%e5%88%b0%e4%bc%9a%e8%af%9d" class="header-anchor"&gt;&lt;/a&gt;4. 从发现到会话
&lt;/h2&gt;&lt;p&gt;客户端通常经历：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;发现服务器/端点 → 选择 Endpoint 与安全策略
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 建立 SecureChannel → 创建并激活 Session
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 浏览、读写、调用或创建订阅
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Endpoint 描述安全模式、SecurityPolicy、传输 profile 和用户令牌类型。客户端不能仅按 URL 选第一个端点，也不能为了“先连上”自动信任未知证书。&lt;/p&gt;
&lt;p&gt;SecureChannel 保护消息通道；Session 管理客户端与服务器的逻辑交互上下文。两者生命周期相关但不是同一个概念。&lt;/p&gt;
&lt;h2 id="5-readwrite-的质量与时间戳"&gt;&lt;a href="#5-readwrite-%e7%9a%84%e8%b4%a8%e9%87%8f%e4%b8%8e%e6%97%b6%e9%97%b4%e6%88%b3" class="header-anchor"&gt;&lt;/a&gt;5. Read/Write 的质量与时间戳
&lt;/h2&gt;&lt;p&gt;读取 Variable 得到的 DataValue 可包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Value；&lt;/li&gt;
&lt;li&gt;StatusCode；&lt;/li&gt;
&lt;li&gt;SourceTimestamp；&lt;/li&gt;
&lt;li&gt;ServerTimestamp。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;显示数值前先判断 StatusCode。&lt;code&gt;Good&lt;/code&gt;、&lt;code&gt;Uncertain&lt;/code&gt; 和 &lt;code&gt;Bad&lt;/code&gt; 表达数据质量；一个缓存的旧值即使类型正确，也不应被当成当前可信测量。&lt;/p&gt;
&lt;p&gt;SourceTimestamp 表示数据源产生值的时间，ServerTimestamp 表示服务器处理该值的时间；具体提供哪些字段取决于服务器与请求参数。&lt;/p&gt;
&lt;h2 id="6-monitoreditem-与-subscription"&gt;&lt;a href="#6-monitoreditem-%e4%b8%8e-subscription" class="header-anchor"&gt;&lt;/a&gt;6. MonitoredItem 与 Subscription
&lt;/h2&gt;&lt;p&gt;客户端创建 MonitoredItem 监视 Variable 的值变化或 Object 的事件，再把它们归入 Subscription。几个周期不能混为一谈：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;参数&lt;/th&gt;
 &lt;th&gt;含义&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;SamplingInterval&lt;/td&gt;
 &lt;td&gt;服务器对 MonitoredItem 采样的请求间隔&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;PublishingInterval&lt;/td&gt;
 &lt;td&gt;Subscription 尝试发布 NotificationMessage 的周期&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;QueueSize&lt;/td&gt;
 &lt;td&gt;MonitoredItem 可缓存的通知数量&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;DiscardOldest&lt;/td&gt;
 &lt;td&gt;队列满时丢旧值还是拒绝新值&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KeepAlive/Lifetime&lt;/td&gt;
 &lt;td&gt;无通知和通信中断时维持订阅的机制&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;服务器可根据能力修订请求值，客户端必须读取 revised 值。采样 10 ms、发布 100 ms 可能在一次 NotificationMessage 中携带多个变化；QueueSize 为 1 则只保留一个候选通知，不能还原全部中间过程。&lt;/p&gt;
&lt;h2 id="7-确认重发与数据不丢失的边界"&gt;&lt;a href="#7-%e7%a1%ae%e8%ae%a4%e9%87%8d%e5%8f%91%e4%b8%8e%e6%95%b0%e6%8d%ae%e4%b8%8d%e4%b8%a2%e5%a4%b1%e7%9a%84%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;7. 确认、重发与数据不丢失的边界
&lt;/h2&gt;&lt;p&gt;客户端通过 Publish 请求接收 NotificationMessage 并确认序列号；在服务器重传队列仍保留消息时，可用 Republish 请求缺失消息。普通订阅能跨越短暂通信中断，但可靠性受 Subscription lifetime、MonitoredItem 队列和重传队列限制。&lt;/p&gt;
&lt;p&gt;OPC UA Part 4 还定义了可选的 durable Subscription 能力，用于更长中断甚至服务器重启场景。客户端必须先确认服务器 profile 和能力，不能假设所有服务器都支持持久订阅。&lt;/p&gt;
&lt;h2 id="8-deadband-与数据量"&gt;&lt;a href="#8-deadband-%e4%b8%8e%e6%95%b0%e6%8d%ae%e9%87%8f" class="header-anchor"&gt;&lt;/a&gt;8. Deadband 与数据量
&lt;/h2&gt;&lt;p&gt;DataChangeFilter 可按状态、值或时间戳触发；Deadband 可减少微小变化通知。但设置前要确认：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据类型是否支持相应 deadband；&lt;/li&gt;
&lt;li&gt;百分比 deadband 依赖 EURange；&lt;/li&gt;
&lt;li&gt;过滤发生在服务器侧，不等于改变设备采样；&lt;/li&gt;
&lt;li&gt;报警、审计和控制反馈不能因错误过滤而丢失关键变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;订阅不是“越快越好”。应按变量变化速度、业务延迟、服务器资源和网络预算分组。&lt;/p&gt;
&lt;h2 id="9-安全配置"&gt;&lt;a href="#9-%e5%ae%89%e5%85%a8%e9%85%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;9. 安全配置
&lt;/h2&gt;&lt;p&gt;生产客户端至少应做到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择满足组织要求且双方均支持的 MessageSecurityMode 与 SecurityPolicy；避免规范已标记为过时的 Basic128Rsa15 和 Basic256，并根据服务器 profile 与组织要求在 Basic256Sha256、Aes128_Sha256_RsaOaep、Aes256_Sha256_RsaPss 等未过时策略中选择，而不是硬编码一个通用“基线”；&lt;/li&gt;
&lt;li&gt;验证服务器应用证书、主机名/应用 URI、有效期和信任链；&lt;/li&gt;
&lt;li&gt;将首次信任作为受控部署步骤，而不是运行时自动接受；&lt;/li&gt;
&lt;li&gt;使用最小权限的用户身份；&lt;/li&gt;
&lt;li&gt;管理证书续期、吊销列表与私钥权限；&lt;/li&gt;
&lt;li&gt;禁止将测试环境的匿名或 &lt;code&gt;None&lt;/code&gt; 策略直接复制到生产。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OPC UA 的安全能力需要正确配置才能生效。网络隔离仍有价值，但不能替代端点身份验证和权限控制。&lt;/p&gt;
&lt;h2 id="10-clientserver-与-pubsub"&gt;&lt;a href="#10-clientserver-%e4%b8%8e-pubsub" class="header-anchor"&gt;&lt;/a&gt;10. Client/Server 与 PubSub
&lt;/h2&gt;&lt;p&gt;本文讨论的是 Client/Server 模型：客户端主动建立 Session，调用服务并维护 Subscription。OPC UA PubSub 是另一套发布/订阅模型，可通过不同消息映射分发 DataSet，不使用同样的 Session/MonitoredItem 关系。&lt;/p&gt;
&lt;p&gt;选择时考虑拓扑、发现、安全、可靠性和实时需求，而不是因为两者都叫“订阅”就混用配置概念。&lt;/p&gt;
&lt;h2 id="11-c-接入策略"&gt;&lt;a href="#11-c-%e6%8e%a5%e5%85%a5%e7%ad%96%e7%95%a5" class="header-anchor"&gt;&lt;/a&gt;11. C# 接入策略
&lt;/h2&gt;&lt;p&gt;OPC Foundation 维护 OPC UA .NET Standard 开源栈。生产接入时应固定经过验证的包版本，并在升级时回归：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Endpoint 选择与证书验证；&lt;/li&gt;
&lt;li&gt;自定义 DataType 编解码；&lt;/li&gt;
&lt;li&gt;Session 重连与 Subscription 转移/重建；&lt;/li&gt;
&lt;li&gt;revised 采样和发布参数；&lt;/li&gt;
&lt;li&gt;序列号缺口、Republish 与数据质量；&lt;/li&gt;
&lt;li&gt;服务器限制和 profile 差异。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将 SDK 封装在适配层，向业务暴露类型化设备能力和带质量的状态快照，不要让 ViewModel 到处持有 &lt;code&gt;NodeId&lt;/code&gt; 和 SDK Session。&lt;/p&gt;
&lt;h2 id="12-小结"&gt;&lt;a href="#12-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;12. 小结
&lt;/h2&gt;&lt;p&gt;OPC UA 的核心不是“能读一个点”，而是用标准 AddressSpace、类型和服务表达设备语义。客户端必须正确处理 Node 标识、DataValue 质量、订阅队列、安全端点和服务器能力；只有模型语义也达成一致，连接成功才会变成真正的互操作。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://reference.opcfoundation.org/" target="_blank" rel="noopener"
 &gt;OPC UA Online Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://reference.opcfoundation.org/specs/OPC-10000-3" target="_blank" rel="noopener"
 &gt;OPC UA Part 3：Address Space Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://reference.opcfoundation.org/specs/OPC-10000-4" target="_blank" rel="noopener"
 &gt;OPC UA Part 4：Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/OPCFoundation/UA-.NETStandard" target="_blank" rel="noopener"
 &gt;OPC Foundation：UA .NET Standard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备通信与协议编程（十）：抓包、记录回放与协议契约测试</title><link>https://www.jiwei.space/posts/equipment/device-communication/10-debugging-testing/</link><pubDate>Tue, 04 Aug 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/10-debugging-testing/</guid><description>&lt;p&gt;设备通信问题常常只在现场、特定固件或某个故障窗口出现。若日志只有“通信失败”，开发者只能反复猜测。可维护的通信模块应把原始证据、解析结果、请求关联和设备状态串成一条可回放链路。&lt;/p&gt;
&lt;h2 id="1-先明确要观察哪一层"&gt;&lt;a href="#1-%e5%85%88%e6%98%8e%e7%a1%ae%e8%a6%81%e8%a7%82%e5%af%9f%e5%93%aa%e4%b8%80%e5%b1%82" class="header-anchor"&gt;&lt;/a&gt;1. 先明确要观察哪一层
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;层次&lt;/th&gt;
 &lt;th&gt;证据&lt;/th&gt;
 &lt;th&gt;常用工具&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;电气信号&lt;/td&gt;
 &lt;td&gt;电压、边沿、时序、干扰&lt;/td&gt;
 &lt;td&gt;示波器、逻辑分析仪&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;总线/链路&lt;/td&gt;
 &lt;td&gt;UART 字节、CAN 帧、EtherCAT datagram&lt;/td&gt;
 &lt;td&gt;串口/CAN/工业网络分析仪&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;网络&lt;/td&gt;
 &lt;td&gt;TCP 段、连接、重传、TLS 握手&lt;/td&gt;
 &lt;td&gt;Wireshark、&lt;code&gt;tcpdump&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;应用协议&lt;/td&gt;
 &lt;td&gt;帧、命令、事务 ID、错误码&lt;/td&gt;
 &lt;td&gt;内置 dissector（Modbus/TCP、EtherCAT、CAN 等）、专有协议自研 dissector、结构化日志&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;业务&lt;/td&gt;
 &lt;td&gt;设备状态、流程步骤、Recipe&lt;/td&gt;
 &lt;td&gt;业务审计与状态快照&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Wireshark 看不到 USB 串口芯片另一侧的真实 RS-485 电气波形；示波器也不知道某个功能码的业务含义。工具必须匹配问题层次。&lt;/p&gt;
&lt;h2 id="2-原始数据日志要可重建"&gt;&lt;a href="#2-%e5%8e%9f%e5%a7%8b%e6%95%b0%e6%8d%ae%e6%97%a5%e5%bf%97%e8%a6%81%e5%8f%af%e9%87%8d%e5%bb%ba" class="header-anchor"&gt;&lt;/a&gt;2. 原始数据日志要可重建
&lt;/h2&gt;&lt;p&gt;对每个收发块至少记录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Timestamp, Direction, DeviceId, ConnectionGeneration,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ChunkSequence, ByteCount, RawBytes, TransportMetadata
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;随后记录解析事件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FrameSequence, Command, TransactionId, ParseResult,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ChecksumResult, PayloadSummary, CorrelationId
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;“块序号”与“帧序号”必须分开，因为一块可能含多帧，一帧也可能跨多块。时间戳注明来源和精度：墙上时间方便跨系统关联，单调时钟适合计算持续时间，硬件时间戳适合分析总线时序。&lt;/p&gt;
&lt;h2 id="3-十六进制日志的边界"&gt;&lt;a href="#3-%e5%8d%81%e5%85%ad%e8%bf%9b%e5%88%b6%e6%97%a5%e5%bf%97%e7%9a%84%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;3. 十六进制日志的边界
&lt;/h2&gt;&lt;p&gt;原始字节很有价值，也可能包含 Recipe、序列号、账户或工艺数据。生产日志应：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认限制每条记录的最大字节数；&lt;/li&gt;
&lt;li&gt;对已知敏感字段脱敏，而不是事后搜索字符串；&lt;/li&gt;
&lt;li&gt;保留原始总长度与截断标志；&lt;/li&gt;
&lt;li&gt;使用二进制归档保存完整证据，并实施访问控制和保留周期；&lt;/li&gt;
&lt;li&gt;不在实时回调中同步格式化大型 hex 字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了可读性，显示层可输出 &lt;code&gt;AA 01 00 02&lt;/code&gt;；存储层更适合保存原始 byte 或 Base64，避免文本解析歧义。&lt;/p&gt;
&lt;h2 id="4-网络抓包如何不误判"&gt;&lt;a href="#4-%e7%bd%91%e7%bb%9c%e6%8a%93%e5%8c%85%e5%a6%82%e4%bd%95%e4%b8%8d%e8%af%af%e5%88%a4" class="header-anchor"&gt;&lt;/a&gt;4. 网络抓包如何不误判
&lt;/h2&gt;&lt;p&gt;TCP 抓包中，一个应用帧可能跨多个 TCP segment，也可能多个帧位于同一 segment。分析时应让 Wireshark 重组 TCP 流，再按应用协议长度解析，不能把 packet 边界当 message 边界。&lt;/p&gt;
&lt;p&gt;常用流程：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Windows 上先列出捕获接口；接口编号因机器而异。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tshark&lt;/span&gt; &lt;span class="n"&gt;-D&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# 只示意捕获过滤器，先替换接口编号、设备地址和端口。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tshark&lt;/span&gt; &lt;span class="n"&gt;-i&lt;/span&gt; &lt;span class="mf"&gt;3&lt;/span&gt; &lt;span class="o"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;host 192.0.2.10 and tcp port 5000&amp;#34;&lt;/span&gt; &lt;span class="n"&gt;-w&lt;/span&gt; &lt;span class="nb"&gt;device-session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;pcapng&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Windows 上实时捕获常规网络接口需要安装并加载 Npcap（Wireshark 安装器可一并安装）；否则 &lt;code&gt;tshark -D&lt;/code&gt; 可能仍显示部分 extcap 等接口，但无法正常捕获本机常规网卡流量。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;192.0.2.0/24&lt;/code&gt; 是文档示例网段。真实环境应先确认捕获权限、接口位置和数据合规要求。TLS 会隐藏应用载荷；不要为了抓包在生产环境关闭证书验证或加密。&lt;/p&gt;
&lt;h2 id="5-记录格式要版本化"&gt;&lt;a href="#5-%e8%ae%b0%e5%bd%95%e6%a0%bc%e5%bc%8f%e8%a6%81%e7%89%88%e6%9c%ac%e5%8c%96" class="header-anchor"&gt;&lt;/a&gt;5. 记录格式要版本化
&lt;/h2&gt;&lt;p&gt;可回放记录不应只是程序日志。建议文件头包含：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FormatVersion
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ProtocolName / ProtocolVersion
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CaptureStartUtc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ClockFrequency / TimestampOrigin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DeviceIdentity / FirmwareVersion
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TransportConfiguration
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PayloadEncoding
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;每条记录使用相对单调时间、方向、连接世代和原始数据。格式版本不兼容时明确拒绝，避免新回放器悄悄误解旧数据。&lt;/p&gt;
&lt;h2 id="6-回放的三种时间模式"&gt;&lt;a href="#6-%e5%9b%9e%e6%94%be%e7%9a%84%e4%b8%89%e7%a7%8d%e6%97%b6%e9%97%b4%e6%a8%a1%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;6. 回放的三种时间模式
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;模式&lt;/th&gt;
 &lt;th&gt;行为&lt;/th&gt;
 &lt;th&gt;用途&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Step&lt;/td&gt;
 &lt;td&gt;测试代码逐条推进&lt;/td&gt;
 &lt;td&gt;确定性单元测试&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Accelerated&lt;/td&gt;
 &lt;td&gt;保留顺序、压缩间隔&lt;/td&gt;
 &lt;td&gt;快速回归长会话&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Real-time&lt;/td&gt;
 &lt;td&gt;尽量复现原间隔&lt;/td&gt;
 &lt;td&gt;超时、拥塞与竞态分析&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;回放器默认不应连接真实设备或执行真实输出。把传输接口替换为内存端点，并通过显式开关才能进入硬件测试环境。&lt;/p&gt;
&lt;h2 id="7-模拟器不是返回固定成功"&gt;&lt;a href="#7-%e6%a8%a1%e6%8b%9f%e5%99%a8%e4%b8%8d%e6%98%af%e8%bf%94%e5%9b%9e%e5%9b%ba%e5%ae%9a%e6%88%90%e5%8a%9f" class="header-anchor"&gt;&lt;/a&gt;7. 模拟器不是返回固定成功
&lt;/h2&gt;&lt;p&gt;有效模拟器应复现协议和状态机，而不是每条命令都返回 OK。至少支持：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正常请求/响应与主动事件；&lt;/li&gt;
&lt;li&gt;分片、粘包、乱序事件和迟到响应；&lt;/li&gt;
&lt;li&gt;CRC 错误、非法长度、未知命令；&lt;/li&gt;
&lt;li&gt;设备 Busy、Faulted、NotReady；&lt;/li&gt;
&lt;li&gt;执行后断线、重启和会话世代变化；&lt;/li&gt;
&lt;li&gt;可控的虚拟时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;模拟器越靠近传输层，越能测试真实解析器；越靠近业务能力层，越适合流程测试。两者用途不同，可以同时保留。设备行为模拟器与实机/HIL 测试梯度见《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/equipment-architecture/07-simulation-hil/" &gt;设备软件架构与控制模型（七）：实机、模拟器与 HIL&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="8-契约测试"&gt;&lt;a href="#8-%e5%a5%91%e7%ba%a6%e6%b5%8b%e8%af%95" class="header-anchor"&gt;&lt;/a&gt;8. 契约测试
&lt;/h2&gt;&lt;p&gt;同一组协议向量应同时验证编码器、解码器和设备模拟器：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;请求对象 → 编码 → 期望字节
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;原始字节 → 解码 → 期望对象
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;编码结果 → 解码 → 等价对象
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;错误字节 → 明确的拒绝原因
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;向量来源按可信度排序：标准规范示例、设备厂商确认样例、经人工标注的实机抓包、人工构造边界样例。不要把一次未知是否正确的现场抓包直接封为“黄金数据”。&lt;/p&gt;
&lt;h2 id="9-模糊测试与不变量"&gt;&lt;a href="#9-%e6%a8%a1%e7%b3%8a%e6%b5%8b%e8%af%95%e4%b8%8e%e4%b8%8d%e5%8f%98%e9%87%8f" class="header-anchor"&gt;&lt;/a&gt;9. 模糊测试与不变量
&lt;/h2&gt;&lt;p&gt;解析器适合做基于属性的测试。对任意 byte 输入，应满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不越界、不死循环、不无限分配；&lt;/li&gt;
&lt;li&gt;每次返回要么需要更多数据，要么消费至少一个字节；&lt;/li&gt;
&lt;li&gt;帧长永不超过契约上限；&lt;/li&gt;
&lt;li&gt;错误输入不会产生未经验证的业务命令；&lt;/li&gt;
&lt;li&gt;取消和关闭后没有后台任务泄漏。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;模糊测试发现崩溃后，把最小化输入加入永久回归语料。&lt;/p&gt;
&lt;h2 id="10-现场问题闭环"&gt;&lt;a href="#10-%e7%8e%b0%e5%9c%ba%e9%97%ae%e9%a2%98%e9%97%ad%e7%8e%af" class="header-anchor"&gt;&lt;/a&gt;10. 现场问题闭环
&lt;/h2&gt;&lt;p&gt;一次完整问题包应包含：软件版本、设备身份与固件、配置快照、单调时间线、原始通信记录、解析日志、状态转换和复现步骤。修复后用同一记录回放，并增加一个能在旧版本失败、在新版本通过的自动化测试。&lt;/p&gt;
&lt;p&gt;这比“多打一些日志再去现场”更重要：证据必须能驱动可重复验证。&lt;/p&gt;
&lt;h2 id="11-小结"&gt;&lt;a href="#11-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;11. 小结
&lt;/h2&gt;&lt;p&gt;协议诊断的关键是保持层次和可重建性：原始块、完整帧、请求关联和业务状态分别记录，再用版本化回放格式、真实状态模拟器和契约测试形成闭环。这样现场偶发故障才会变成可重复的软件输入。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.wireshark.org/docs/wsug_html_chunked/" target="_blank" rel="noopener"
 &gt;Wireshark User&amp;rsquo;s Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://datatracker.ietf.org/doc/draft-ietf-opsawg-pcapng/" target="_blank" rel="noopener"
 &gt;IETF：PCAP Next Generation (pcapng) Capture File Format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/testing/unit-testing-csharp-with-xunit" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Unit testing C# with xUnit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备通信与协议编程（九）：EtherCAT 周期通信、分布式时钟与软件边界</title><link>https://www.jiwei.space/posts/equipment/device-communication/09-ethercat/</link><pubDate>Mon, 03 Aug 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/09-ethercat/</guid><description>&lt;p&gt;EtherCAT 使用标准以太网物理层，却不是“把设备接到交换机后用 TCP 发命令”。它面向确定性的周期过程数据：MainDevice 构造帧，SubDevice 在帧经过时直接读取或写入对应数据区域，并由专用控制器完成高速处理。&lt;/p&gt;
&lt;p&gt;本文讨论上位机开发者需要理解的模型，不替代主站协议栈、设备 ESI 和 ETG 规范。&lt;/p&gt;
&lt;h2 id="1-为什么普通以太网直觉会失效"&gt;&lt;a href="#1-%e4%b8%ba%e4%bb%80%e4%b9%88%e6%99%ae%e9%80%9a%e4%bb%a5%e5%a4%aa%e7%bd%91%e7%9b%b4%e8%a7%89%e4%bc%9a%e5%a4%b1%e6%95%88" class="header-anchor"&gt;&lt;/a&gt;1. 为什么普通以太网直觉会失效
&lt;/h2&gt;&lt;p&gt;EtherCAT 帧使用 EtherType &lt;code&gt;0x88A4&lt;/code&gt;，通常不经过 TCP/IP。一个帧可携带多个 EtherCAT datagram；SubDevice 在帧流经时“on the fly”处理相关数据，末端再利用全双工链路返回 MainDevice。&lt;/p&gt;
&lt;p&gt;因此常规企业交换网络、Socket API 和 IP 地址不是其周期数据路径。拓扑、网卡、驱动与主站栈必须按 EtherCAT 系统设计。&lt;/p&gt;
&lt;h2 id="2-过程映像与周期交换"&gt;&lt;a href="#2-%e8%bf%87%e7%a8%8b%e6%98%a0%e5%83%8f%e4%b8%8e%e5%91%a8%e6%9c%9f%e4%ba%a4%e6%8d%a2" class="header-anchor"&gt;&lt;/a&gt;2. 过程映像与周期交换
&lt;/h2&gt;&lt;p&gt;主站启动时根据配置把各 SubDevice 的过程数据映射到逻辑过程映像。实时任务通常按固定周期执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;读取上一周期输入 → 执行控制算法 → 写入输出过程映像
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 发送/接收 EtherCAT 帧 → 检查 Working Counter 与状态
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;PDO 表示周期过程数据映射；参数和诊断常通过 mailbox 协议访问，例如 CAN application protocol over EtherCAT（CoE，复用上一篇 CANopen 的对象字典与 SDO/PDO 机制）中的 SDO。周期 PDO 与非周期邮箱流量应分别预算。&lt;/p&gt;
&lt;h2 id="3-ethercat-state-machine"&gt;&lt;a href="#3-ethercat-state-machine" class="header-anchor"&gt;&lt;/a&gt;3. EtherCAT State Machine
&lt;/h2&gt;&lt;p&gt;典型状态包括：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INIT → PRE-OP → SAFE-OP → OP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;INIT：基础初始化，尚无邮箱或过程数据通信；&lt;/li&gt;
&lt;li&gt;PRE-OP：可进行邮箱通信和参数配置；&lt;/li&gt;
&lt;li&gt;SAFE-OP：输入过程数据可用，输出保持安全行为；&lt;/li&gt;
&lt;li&gt;OP：输入和输出过程数据均进入运行状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体转换条件、错误确认和安全输出由设备实现及 profile 决定。总线进入 OP 也不等于设备业务状态已 Ready。&lt;/p&gt;
&lt;h2 id="4-working-counter-不是业务成功码"&gt;&lt;a href="#4-working-counter-%e4%b8%8d%e6%98%af%e4%b8%9a%e5%8a%a1%e6%88%90%e5%8a%9f%e7%a0%81" class="header-anchor"&gt;&lt;/a&gt;4. Working Counter 不是业务成功码
&lt;/h2&gt;&lt;p&gt;每个 datagram 带 Working Counter（WKC），SubDevice 成功执行相关内存操作时按规则更新。主站将实际 WKC 与配置期望值比较，可发现节点缺失或操作未被处理。&lt;/p&gt;
&lt;p&gt;WKC 正确只能证明本周期总线访问符合预期，不能证明伺服已到位、气缸已动作或工艺已经完成。这些仍要通过状态字、反馈量和设备状态机判断。&lt;/p&gt;
&lt;h2 id="5-distributed-clocks"&gt;&lt;a href="#5-distributed-clocks" class="header-anchor"&gt;&lt;/a&gt;5. Distributed Clocks
&lt;/h2&gt;&lt;p&gt;Distributed Clocks（DC）让支持 DC 的 SubDevice 本地时钟对齐到共同系统时间，并补偿传播延迟。动作可由本地 SYNC 信号触发，而不是依赖帧恰好到达的时刻，从而降低通信抖动对同步动作的影响。&lt;/p&gt;
&lt;p&gt;ETG 资料给出可达到显著低于 1 µs 的同步抖动，但这是协议与合格实现的能力描述，不是任意 PC、拓扑和配置下自动获得的端到端控制精度。实际系统还受控制任务抖动、采样、驱动器和机械响应影响。&lt;/p&gt;
&lt;h2 id="6-maindevice-可以用普通网卡不等于普通应用就实时"&gt;&lt;a href="#6-maindevice-%e5%8f%af%e4%bb%a5%e7%94%a8%e6%99%ae%e9%80%9a%e7%bd%91%e5%8d%a1%e4%b8%8d%e7%ad%89%e4%ba%8e%e6%99%ae%e9%80%9a%e5%ba%94%e7%94%a8%e5%b0%b1%e5%ae%9e%e6%97%b6" class="header-anchor"&gt;&lt;/a&gt;6. MainDevice 可以用普通网卡，不等于普通应用就实时
&lt;/h2&gt;&lt;p&gt;ETG 说明 MainDevice 可使用标准 Ethernet MAC；但周期是否确定还取决于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主站协议栈和网卡驱动路径；&lt;/li&gt;
&lt;li&gt;实时操作系统或实时扩展；&lt;/li&gt;
&lt;li&gt;CPU 隔离、调度优先级和中断配置；&lt;/li&gt;
&lt;li&gt;内存分配、GC、日志和其他任务干扰；&lt;/li&gt;
&lt;li&gt;周期、SubDevice 数量与过程数据规模。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 Windows/WPF 线程里用普通定时器每 1 ms 调一次 SDK，不等于建立了 1 ms 硬实时控制。&lt;/p&gt;
&lt;h2 id="7-推荐的软件分层"&gt;&lt;a href="#7-%e6%8e%a8%e8%8d%90%e7%9a%84%e8%bd%af%e4%bb%b6%e5%88%86%e5%b1%82" class="header-anchor"&gt;&lt;/a&gt;7. 推荐的软件分层
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;UI / Recipe / 流程编排
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ 非实时命令、状态快照
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;实时控制进程或主站运行时
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ 固定过程映像、无阻塞周期任务
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EtherCAT 主站栈与网卡驱动
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SubDevices
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实时侧避免动态分配、阻塞 I/O、同步日志和不可控回调。上位机通过共享内存、IPC 或厂商 API 获取降采样状态，并携带周期号、DC 时间和质量状态；UI 卡顿不能阻塞总线周期。&lt;/p&gt;
&lt;h2 id="8-esieni-与配置治理"&gt;&lt;a href="#8-esieni-%e4%b8%8e%e9%85%8d%e7%bd%ae%e6%b2%bb%e7%90%86" class="header-anchor"&gt;&lt;/a&gt;8. ESI、ENI 与配置治理
&lt;/h2&gt;&lt;p&gt;ESI（EtherCAT SubDevice Information）是厂商提供的 XML 设备描述，包含身份、对象、过程数据映射和同步能力。配置工具基于网络扫描和 ESI 生成主站配置；部分生态将最终网络配置表示为 ENI。&lt;/p&gt;
&lt;p&gt;生产系统应版本化保存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ESI 文件及校验值；&lt;/li&gt;
&lt;li&gt;设备型号、产品码、修订号与序列号规则；&lt;/li&gt;
&lt;li&gt;PDO 映射、周期和 DC 参数；&lt;/li&gt;
&lt;li&gt;主站栈、驱动和网卡版本；&lt;/li&gt;
&lt;li&gt;配置变更的验证记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现场设备修订变化时，不能只看节点顺序相同就直接运行。&lt;/p&gt;
&lt;h2 id="9-诊断指标"&gt;&lt;a href="#9-%e8%af%8a%e6%96%ad%e6%8c%87%e6%a0%87" class="header-anchor"&gt;&lt;/a&gt;9. 诊断指标
&lt;/h2&gt;&lt;p&gt;至少观察：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MainDevice 与各 SubDevice 状态；&lt;/li&gt;
&lt;li&gt;期望/实际 WKC；&lt;/li&gt;
&lt;li&gt;丢周期、周期超时与任务执行时间分布；&lt;/li&gt;
&lt;li&gt;DC 偏差和同步状态；&lt;/li&gt;
&lt;li&gt;各端口链路错误计数；&lt;/li&gt;
&lt;li&gt;mailbox 超时和设备错误码；&lt;/li&gt;
&lt;li&gt;拓扑变化与设备身份不匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;日志应放在非实时路径，通过预分配环形缓冲传递摘要；不要在周期回调里格式化大段字符串。Wireshark 自带 EtherCAT 协议解析器，可直接查看帧中的 Working Counter 与命令明细。&lt;/p&gt;
&lt;h2 id="10-小结"&gt;&lt;a href="#10-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;10. 小结
&lt;/h2&gt;&lt;p&gt;EtherCAT 的价值在于高效的 on-the-fly 过程数据交换和分布式同步，但实时性是整条系统链路的属性。上位机应把硬实时周期与 UI、日志、流程编排隔离，并用 WKC、DC 和状态反馈分别判断总线健康、同步质量与业务结果。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.ethercat.org/en/technology.html" target="_blank" rel="noopener"
 &gt;EtherCAT Technology Group：Technology&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.ethercat.org/en/compendium.htm" target="_blank" rel="noopener"
 &gt;EtherCAT Technology Group：EtherCAT Compendium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.ethercat.org/pdf/english/ETG_Brochure_EN.pdf" target="_blank" rel="noopener"
 &gt;EtherCAT Technology Group：EtherCAT Brochure&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备通信与协议编程（八）：CAN 与 CANopen 的报文、对象字典和实时性边界</title><link>https://www.jiwei.space/posts/equipment/device-communication/08-can-canopen/</link><pubDate>Sun, 02 Aug 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/08-can-canopen/</guid><description>&lt;p&gt;CAN 与 CANopen 经常被连在一起说，但它们不是同一层：CAN 定义总线访问、帧和错误处理等机制；CANopen 在其上定义对象字典、通信对象、网络管理和设备配置。只会收发 CAN ID，不等于已经实现 CANopen 节点。&lt;/p&gt;
&lt;h2 id="1-can-帧表达的是什么"&gt;&lt;a href="#1-can-%e5%b8%a7%e8%a1%a8%e8%be%be%e7%9a%84%e6%98%af%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;1. CAN 帧表达的是什么
&lt;/h2&gt;&lt;p&gt;经典 CAN 数据帧的有效载荷最多 8 byte，位速率上限为 1 Mbit/s；CAN FD 将单帧有效载荷扩展到最多 64 byte，并允许帧内切换到更快的位速率。设备、控制器、收发器和分析工具必须共同支持相应模式。&lt;/p&gt;
&lt;p&gt;CAN 标识符用于仲裁并由系统赋予消息语义，它不是内建的节点地址。标准帧使用 11 bit 标识符，扩展帧使用 29 bit 标识符。&lt;/p&gt;
&lt;h2 id="2-非破坏性仲裁"&gt;&lt;a href="#2-%e9%9d%9e%e7%a0%b4%e5%9d%8f%e6%80%a7%e4%bb%b2%e8%a3%81" class="header-anchor"&gt;&lt;/a&gt;2. 非破坏性仲裁
&lt;/h2&gt;&lt;p&gt;多个节点同时发送时，标识符逐 bit 仲裁；显性位会覆盖隐性位，失去仲裁的节点停止发送而不破坏获胜帧。对同一帧格式而言，数值更小的标识符通常具有更高总线优先级。&lt;/p&gt;
&lt;p&gt;这提供优先级调度，却不自动保证每条消息的截止时间。实时性还取决于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;总线负载与最高优先级报文频率；&lt;/li&gt;
&lt;li&gt;位填充、错误帧和重传；&lt;/li&gt;
&lt;li&gt;驱动与应用调度延迟；&lt;/li&gt;
&lt;li&gt;网关转发和接收队列；&lt;/li&gt;
&lt;li&gt;标识符分配是否造成低优先级饥饿。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="3-can-的可靠性不等于端到端成功"&gt;&lt;a href="#3-can-%e7%9a%84%e5%8f%af%e9%9d%a0%e6%80%a7%e4%b8%8d%e7%ad%89%e4%ba%8e%e7%ab%af%e5%88%b0%e7%ab%af%e6%88%90%e5%8a%9f" class="header-anchor"&gt;&lt;/a&gt;3. CAN 的可靠性不等于端到端成功
&lt;/h2&gt;&lt;p&gt;CAN 控制器提供 CRC、位监测、确认、错误计数和故障隔离等机制。它们提高链路可靠性，但应用仍需处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消息过期或重复；&lt;/li&gt;
&lt;li&gt;接收队列溢出；&lt;/li&gt;
&lt;li&gt;节点复位后的状态恢复；&lt;/li&gt;
&lt;li&gt;“帧已被总线确认”但业务未执行；&lt;/li&gt;
&lt;li&gt;网关另一侧的失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关键命令仍要定义序列、状态确认或幂等策略。&lt;/p&gt;
&lt;h2 id="4-canopen-的对象字典"&gt;&lt;a href="#4-canopen-%e7%9a%84%e5%af%b9%e8%b1%a1%e5%ad%97%e5%85%b8" class="header-anchor"&gt;&lt;/a&gt;4. CANopen 的对象字典
&lt;/h2&gt;&lt;p&gt;对象字典（Object Dictionary，OD）是 CANopen 设备的核心数据模型。对象使用 16 bit index 和 8 bit sub-index 寻址，包含通信参数、设备参数和过程数据。与 Modbus 寄存器映射的对照见本系列第 7 篇。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Index 0x1000 Device type
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Index 0x1018 Identity object
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Index 0x1000-0x1FFF Communication profile area
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Index 0x2000-0x5FFF Manufacturer-specific area
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Index 0x6000-0x9FFF Standardized device-profile area
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;精确对象及访问权限应以设备 EDS/XDD 和适用的 CiA profile 为准，不能把示例索引当作所有设备都支持的寄存器表。&lt;/p&gt;
&lt;h2 id="5-sdo-与-pdo"&gt;&lt;a href="#5-sdo-%e4%b8%8e-pdo" class="header-anchor"&gt;&lt;/a&gt;5. SDO 与 PDO
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;通信对象&lt;/th&gt;
 &lt;th&gt;主要用途&lt;/th&gt;
 &lt;th&gt;特点&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;SDO&lt;/td&gt;
 &lt;td&gt;读取/写入对象字典、配置设备&lt;/td&gt;
 &lt;td&gt;请求/响应，可传较长数据，开销较大&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;PDO&lt;/td&gt;
 &lt;td&gt;交换实时过程数据&lt;/td&gt;
 &lt;td&gt;映射预先配置的 OD 项，帧短、无逐条应用确认&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;PDO 可由事件、定时器或 SYNC 触发。它适合周期数据，不适合拿来临时读取任意对象。SDO 适合配置，但不应挤占高优先级实时流量。&lt;/p&gt;
&lt;h2 id="6-nmtheartbeat-与-emcy"&gt;&lt;a href="#6-nmtheartbeat-%e4%b8%8e-emcy" class="header-anchor"&gt;&lt;/a&gt;6. NMT、Heartbeat 与 EMCY
&lt;/h2&gt;&lt;p&gt;CANopen Network Management（NMT）管理节点状态，例如 Initialization、Pre-operational、Operational 和 Stopped。节点进入 Operational 不代表机械系统已经安全 Ready；设备 profile 和应用状态机仍在更高层。&lt;/p&gt;
&lt;p&gt;Heartbeat 用于观察节点是否持续存活，超时阈值应大于生产周期并考虑总线拥塞。Emergency（EMCY）用于报告紧急错误状态，但它不是可靠事件队列；上位机收到后应读取设备诊断对象并与状态机对账。&lt;/p&gt;
&lt;h2 id="7-上位机的软件边界"&gt;&lt;a href="#7-%e4%b8%8a%e4%bd%8d%e6%9c%ba%e7%9a%84%e8%bd%af%e4%bb%b6%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;7. 上位机的软件边界
&lt;/h2&gt;&lt;p&gt;通用 PC 应用通常通过 USB-CAN、PCIe-CAN 或以太网网关接入。适配层至少应暴露：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Frame(timestamp, channel, id, flags, data)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BusState(error counters, bus-off, overflow)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SendAsync(frame)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不要只暴露 &lt;code&gt;byte[]&lt;/code&gt;。时间戳来源、是否为硬件时间戳、接收溢出、错误帧和 bus-off 状态都关系到诊断结论。&lt;/p&gt;
&lt;p&gt;CANopen 层再把 PDO/SDO/NMT 转成类型化能力，业务层不应直接拼 COB-ID 和字节偏移。&lt;/p&gt;
&lt;h2 id="8-bus-off-与恢复"&gt;&lt;a href="#8-bus-off-%e4%b8%8e%e6%81%a2%e5%a4%8d" class="header-anchor"&gt;&lt;/a&gt;8. Bus-off 与恢复
&lt;/h2&gt;&lt;p&gt;错误计数达到协议条件后，节点可能进入 bus-off 并停止参与总线。自动恢复策略要符合控制器、设备和系统安全要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;记录错误计数、总线状态与最近报文；&lt;/li&gt;
&lt;li&gt;停止依赖该节点的流程并进入安全状态；&lt;/li&gt;
&lt;li&gt;修复或等待物理故障消失；&lt;/li&gt;
&lt;li&gt;按设备规则复位通信和应用状态；&lt;/li&gt;
&lt;li&gt;重新确认节点身份、PDO 映射与运行状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;反复自动复位可能掩盖终端、电磁干扰或波特率配置问题。&lt;/p&gt;
&lt;h2 id="9-验证与诊断"&gt;&lt;a href="#9-%e9%aa%8c%e8%af%81%e4%b8%8e%e8%af%8a%e6%96%ad" class="header-anchor"&gt;&lt;/a&gt;9. 验证与诊断
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;用总线分析仪确认 nominal/data bitrate、帧格式与实际负载；&lt;/li&gt;
&lt;li&gt;验证高优先级流量下低优先级消息的最坏延迟；&lt;/li&gt;
&lt;li&gt;注入错误、断开终端、制造队列溢出并观察恢复；&lt;/li&gt;
&lt;li&gt;对照 EDS/XDD 验证对象类型、访问权限和 PDO 映射；&lt;/li&gt;
&lt;li&gt;区分适配器接收时间、驱动时间和硬件总线时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="10-小结"&gt;&lt;a href="#10-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;10. 小结
&lt;/h2&gt;&lt;p&gt;CAN 提供带优先级仲裁和错误隔离的共享总线，CANopen 提供可互操作的设备对象与通信模型。工程设计不能停留在“这个 CAN ID 是什么”，而要同时管理对象字典、网络状态、实时流量预算和故障恢复。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.can-cia.org/can-knowledge/" target="_blank" rel="noopener"
 &gt;CAN in Automation：CAN knowledge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.can-cia.org/can-knowledge/canopen/" target="_blank" rel="noopener"
 &gt;CAN in Automation：CANopen knowledge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.bosch-semiconductors.com/products/ip-modules/can-protocols/can-fd/" target="_blank" rel="noopener"
 &gt;Bosch：CAN FD Protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备通信与协议编程（七）：Modbus RTU/TCP、功能码与寄存器映射</title><link>https://www.jiwei.space/posts/equipment/device-communication/07-modbus/</link><pubDate>Sat, 01 Aug 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/07-modbus/</guid><description>&lt;p&gt;Modbus 看起来只是“读写寄存器”，真正的工程问题却集中在地址表示、数据类型、字节顺序、轮询一致性和异常响应上。若这些契约没有写清，两个都声称兼容 Modbus 的程序仍可能完全无法互通。&lt;/p&gt;
&lt;p&gt;本文以 Modbus Application Protocol V1.1b3 和 Serial Line Protocol V1.02 为边界。&lt;/p&gt;
&lt;h2 id="1-先区分-pduadu-和传输"&gt;&lt;a href="#1-%e5%85%88%e5%8c%ba%e5%88%86-pduadu-%e5%92%8c%e4%bc%a0%e8%be%93" class="header-anchor"&gt;&lt;/a&gt;1. 先区分 PDU、ADU 和传输
&lt;/h2&gt;&lt;p&gt;Modbus 的核心协议数据单元（PDU）由功能码和数据组成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PDU = Function Code + Data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不同传输再包一层 ADU：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RTU ADU = Server Address + PDU + CRC
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TCP ADU = MBAP Header + PDU
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Modbus TCP 的 MBAP 头包含 Transaction Identifier、Protocol Identifier、Length 和 Unit Identifier。TCP 传输不附加 RTU CRC；TCP 校验和也不提供应用层身份认证。TCP 传输默认使用 502 端口；Modbus 报文实现指南将其规定为强制默认监听端口。&lt;/p&gt;
&lt;h2 id="2-四类数据模型"&gt;&lt;a href="#2-%e5%9b%9b%e7%b1%bb%e6%95%b0%e6%8d%ae%e6%a8%a1%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;2. 四类数据模型
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;数据块&lt;/th&gt;
 &lt;th&gt;单元&lt;/th&gt;
 &lt;th&gt;常用功能码&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Coils&lt;/td&gt;
 &lt;td&gt;1 bit，可读写&lt;/td&gt;
 &lt;td&gt;01 (0x01)、05 (0x05)、15 (0x0F)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Discrete Inputs&lt;/td&gt;
 &lt;td&gt;1 bit，只读&lt;/td&gt;
 &lt;td&gt;02 (0x02)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Input Registers&lt;/td&gt;
 &lt;td&gt;16 bit，只读&lt;/td&gt;
 &lt;td&gt;04 (0x04)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Holding Registers&lt;/td&gt;
 &lt;td&gt;16 bit，可读写&lt;/td&gt;
 &lt;td&gt;03 (0x03)、06 (0x06)、16 (0x10)、22 (0x16)、23 (0x17)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;只读/可写是协议数据模型语义，具体设备仍可能对某些地址施加额外限制。&lt;/p&gt;
&lt;h2 id="3-地址为什么总差-1"&gt;&lt;a href="#3-%e5%9c%b0%e5%9d%80%e4%b8%ba%e4%bb%80%e4%b9%88%e6%80%bb%e5%b7%ae-1" class="header-anchor"&gt;&lt;/a&gt;3. 地址为什么总差 1
&lt;/h2&gt;&lt;p&gt;协议 PDU 中的起始地址是从 0 开始的 16 位地址。设备手册常用 &lt;code&gt;40001&lt;/code&gt;、&lt;code&gt;400001&lt;/code&gt; 或“寄存器 1”等人类表示法标记 holding register；这些前缀和基数不直接在线路上传输。&lt;/p&gt;
&lt;p&gt;例如手册中的 &lt;code&gt;40001&lt;/code&gt; 经常对应 PDU 地址 &lt;code&gt;0&lt;/code&gt;，但这不是所有厂商文档的强制格式。接入前必须记录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;手册标号 | 数据块 | PDU 地址 | 读写功能码 | 数据类型 | 单位 | 缩放 | 字节/字顺序
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不要在代码中到处写 &lt;code&gt;address - 1&lt;/code&gt;；在设备映射层一次性转换。&lt;/p&gt;
&lt;h2 id="4-16-位寄存器不等于业务类型"&gt;&lt;a href="#4-16-%e4%bd%8d%e5%af%84%e5%ad%98%e5%99%a8%e4%b8%8d%e7%ad%89%e4%ba%8e%e4%b8%9a%e5%8a%a1%e7%b1%bb%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;4. 16 位寄存器不等于业务类型
&lt;/h2&gt;&lt;p&gt;规范定义寄存器是 16 bit，并规定多字节量在 PDU 中高位字节先传。设备把 &lt;code&gt;float32&lt;/code&gt;、&lt;code&gt;int32&lt;/code&gt;、ASCII 或位域放入多个寄存器时，&lt;strong&gt;寄存器之间的字顺序通常是厂商约定&lt;/strong&gt;，不能仅凭 Modbus 标准推断。&lt;/p&gt;
&lt;p&gt;同一个浮点数可能采用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;AB CD // 高字在前
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CD AB // 低字在前
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BA DC // 每字节交换
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DC BA // 字与字节都交换
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;应使用手册给出的已知值或设备模拟器验证，代码中以明确枚举表达顺序。&lt;/p&gt;
&lt;h2 id="5-构造读-holding-registers-请求"&gt;&lt;a href="#5-%e6%9e%84%e9%80%a0%e8%af%bb-holding-registers-%e8%af%b7%e6%b1%82" class="header-anchor"&gt;&lt;/a&gt;5. 构造读 Holding Registers 请求
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Buffers.Binary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;BuildReadHoldingRegisters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;serverAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;startAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;125&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 读请求必须发给单播服务器地址；0 是广播地址，不产生响应。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serverAddress&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;247&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serverAddress&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;startAddress&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0x1&lt;/span&gt;&lt;span class="n"&gt;_0000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;startAddress&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serverAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0x03&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;BinaryPrimitives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteUInt16BigEndian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;startAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;BinaryPrimitives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteUInt16BigEndian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ModbusCrc16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// RTU CRC 低字节先发送&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;ModbusCrc16&lt;/code&gt; 沿用本系列第 4 篇《校验和与 CRC 的能力边界》中的实现。串行链路中的地址 0 用于广播，1—247 是单播服务器地址；读请求需要响应，因此示例拒绝地址 0。读取数量上限来自功能码 03 的规范；其他功能码有各自限制，不应复用一个全局“最大寄存器数”。&lt;/p&gt;
&lt;h2 id="6-正常响应与异常响应"&gt;&lt;a href="#6-%e6%ad%a3%e5%b8%b8%e5%93%8d%e5%ba%94%e4%b8%8e%e5%bc%82%e5%b8%b8%e5%93%8d%e5%ba%94" class="header-anchor"&gt;&lt;/a&gt;6. 正常响应与异常响应
&lt;/h2&gt;&lt;p&gt;功能码 03 的正常响应包含字节数和寄存器数据。客户端必须同时验证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server/Unit 地址与事务 ID 是否匹配当前请求；&lt;/li&gt;
&lt;li&gt;功能码是否为期望值；&lt;/li&gt;
&lt;li&gt;字节数是否等于寄存器数量乘 2；&lt;/li&gt;
&lt;li&gt;RTU CRC 或 TCP MBAP 长度是否正确；&lt;/li&gt;
&lt;li&gt;整帧没有多余或缺失数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;异常响应把原功能码最高位置 1，例如 &lt;code&gt;0x03&lt;/code&gt; 变为 &lt;code&gt;0x83&lt;/code&gt;，随后携带异常码。常见异常码：01 非法功能、02 非法数据地址、03 非法数据值、04 服务器设备故障（V1.1b3 第 7 节）。异常码表示服务器明确拒绝或无法处理请求，不应与本地超时、CRC 错误混为一类。&lt;/p&gt;
&lt;h2 id="7-rtu-的时间边界"&gt;&lt;a href="#7-rtu-%e7%9a%84%e6%97%b6%e9%97%b4%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;7. RTU 的时间边界
&lt;/h2&gt;&lt;p&gt;Modbus RTU 使用字符间隔和帧间静默识别边界。串行线路指南对不高于 19200 bit/s 的链路以字符时间描述 &lt;code&gt;t1.5&lt;/code&gt; 和 &lt;code&gt;t3.5&lt;/code&gt;；更高速率建议使用固定的 750 µs 与 1.750 ms。RS-485 电气层与半双工约束见本系列第 1 篇。&lt;/p&gt;
&lt;p&gt;普通桌面操作系统和托管定时器不适合用“每字节启动一个高精度定时器”硬凑边界。更稳健的方案是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由串口接收循环累积字节；&lt;/li&gt;
&lt;li&gt;使用协议允许的完整长度和 CRC 尽快验证；&lt;/li&gt;
&lt;li&gt;把时间间隔作为帧结束/失效条件；&lt;/li&gt;
&lt;li&gt;若需要严格实时主站，由实时控制器或成熟协议栈承担。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="8-轮询与一致性"&gt;&lt;a href="#8-%e8%bd%ae%e8%af%a2%e4%b8%8e%e4%b8%80%e8%87%b4%e6%80%a7" class="header-anchor"&gt;&lt;/a&gt;8. 轮询与一致性
&lt;/h2&gt;&lt;p&gt;Modbus 没有通用订阅机制，主站通常轮询。优化时应：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;合并连续地址，但不要跨越设备声明的非法区；&lt;/li&gt;
&lt;li&gt;按变化速度设置不同轮询周期；&lt;/li&gt;
&lt;li&gt;限制单设备在途请求数，RTU 通常一次一个；&lt;/li&gt;
&lt;li&gt;给每个值附带采集时间与质量状态；&lt;/li&gt;
&lt;li&gt;多寄存器业务值一次读完，避免跨轮询拼接撕裂数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;读取多个寄存器不必然意味着设备内部快照一致；需要一致性保证时必须查设备手册。超时与重试设计见本系列第 6 篇。&lt;/p&gt;
&lt;h2 id="9-写入与安全"&gt;&lt;a href="#9-%e5%86%99%e5%85%a5%e4%b8%8e%e5%ae%89%e5%85%a8" class="header-anchor"&gt;&lt;/a&gt;9. 写入与安全
&lt;/h2&gt;&lt;p&gt;写单寄存器、写多寄存器和读写多寄存器的原子性边界由设备实现与功能码契约决定。关键动作应在业务层检查设备状态、权限、范围和互锁，不能因为 Modbus 返回成功就绕过安全逻辑。&lt;/p&gt;
&lt;p&gt;传统 Modbus 不提供认证和加密。Modbus Organization 另有基于 TLS 与 X.509 的 Modbus Security Protocol；生产网络应结合设备支持、网络分区和访问控制选择方案。&lt;/p&gt;
&lt;h2 id="10-小结"&gt;&lt;a href="#10-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;10. 小结
&lt;/h2&gt;&lt;p&gt;Modbus 的难点不在功能码本身，而在地址映射和业务类型契约。明确 PDU 地址、数据块、功能码、缩放、单位、字顺序和错误分类，再谈轮询优化，才能避免“读到了数但解释错了”的隐蔽故障。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.modbus.org/modbus-specifications" target="_blank" rel="noopener"
 &gt;Modbus Organization：Specifications and Implementation Guides&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备通信与协议编程（六）：超时、重连与幂等性设计</title><link>https://www.jiwei.space/posts/equipment/device-communication/06-failure-retry-idempotency/</link><pubDate>Fri, 31 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/06-failure-retry-idempotency/</guid><description>&lt;p&gt;设备通信最危险的失败不是明确返回错误，而是：命令已发出，连接随后中断，上位机不知道设备是否执行。简单地“超时就重试”可能让运动轴重复移动、阀门重复动作或配方重复下发。&lt;/p&gt;
&lt;p&gt;本篇建立一套与串口、TCP 都适用的故障模型。&lt;/p&gt;
&lt;h2 id="1-把一次调用拆成阶段"&gt;&lt;a href="#1-%e6%8a%8a%e4%b8%80%e6%ac%a1%e8%b0%83%e7%94%a8%e6%8b%86%e6%88%90%e9%98%b6%e6%ae%b5" class="header-anchor"&gt;&lt;/a&gt;1. 把一次调用拆成阶段
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;排队 → 编码 → 发送 → 设备接收 → 设备执行 → 响应发送 → 客户端接收
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;客户端观察到超时，只能证明最后期限内没有得到期望响应，不能反推出失败发生在哪一步。尤其在“执行完成、响应丢失”时，无条件重试会重复副作用。&lt;/p&gt;
&lt;h2 id="2-三类结果"&gt;&lt;a href="#2-%e4%b8%89%e7%b1%bb%e7%bb%93%e6%9e%9c" class="header-anchor"&gt;&lt;/a&gt;2. 三类结果
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;结果&lt;/th&gt;
 &lt;th&gt;含义&lt;/th&gt;
 &lt;th&gt;典型处理&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;已成功&lt;/td&gt;
 &lt;td&gt;收到可验证的成功响应&lt;/td&gt;
 &lt;td&gt;提交上层状态&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;已失败&lt;/td&gt;
 &lt;td&gt;收到明确拒绝，且设备保证未执行&lt;/td&gt;
 &lt;td&gt;修正参数或进入故障处理&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;未知&lt;/td&gt;
 &lt;td&gt;超时、断线、响应无法关联&lt;/td&gt;
 &lt;td&gt;查询状态、对账或人工确认&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;把“未知”强行映射成失败，是许多重复动作事故的根源。API 可以显式返回 &lt;code&gt;Outcome.Unknown&lt;/code&gt;，迫使上层选择恢复策略。&lt;/p&gt;
&lt;h2 id="3-幂等键与命令语义"&gt;&lt;a href="#3-%e5%b9%82%e7%ad%89%e9%94%ae%e4%b8%8e%e5%91%bd%e4%bb%a4%e8%af%ad%e4%b9%89" class="header-anchor"&gt;&lt;/a&gt;3. 幂等键与命令语义
&lt;/h2&gt;&lt;p&gt;幂等不是“重试次数小”。同一个逻辑操作重复提交后，设备端应识别它并返回原结果，而不是再次执行。&lt;/p&gt;
&lt;p&gt;常见设计：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;去重键 = StableClientId + CommandId
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;校验属性 = Operation + Parameters
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;StableClientId&lt;/code&gt; 来自持久配置或已认证的客户端身份，不能使用每次重连都会变化的传输 Session ID。客户端生成的 &lt;code&gt;CommandId&lt;/code&gt; 在该身份范围内保持唯一，设备在约定窗口内保存去重键、操作参数与结果：重复请求的操作和参数完全相同则重放原结果；同一去重键携带不同内容必须拒绝。若要求跨设备重启重试，设备端去重记录也必须按协议要求持久化；仅在内存中保存无法覆盖重启窗口。&lt;/p&gt;
&lt;p&gt;并非所有命令天然幂等：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SetPosition(100)&lt;/code&gt; 可能是幂等的，但仍受坐标系和会话状态影响；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MoveRelative(+10)&lt;/code&gt; 通常不是幂等的；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Start()&lt;/code&gt; 在已运行时是否成功，必须由契约定义；&lt;/li&gt;
&lt;li&gt;“写配置”若伴随闪存计数或触发重启，也不能只看最终值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="4-没有设备端幂等支持怎么办"&gt;&lt;a href="#4-%e6%b2%a1%e6%9c%89%e8%ae%be%e5%a4%87%e7%ab%af%e5%b9%82%e7%ad%89%e6%94%af%e6%8c%81%e6%80%8e%e4%b9%88%e5%8a%9e" class="header-anchor"&gt;&lt;/a&gt;4. 没有设备端幂等支持怎么办
&lt;/h2&gt;&lt;p&gt;可按风险从低到高选择：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;重连后查询设备状态，确认目标是否已达到；&lt;/li&gt;
&lt;li&gt;使用设备提供的任务号、事件号或最后命令记录对账；&lt;/li&gt;
&lt;li&gt;将非幂等动作改造成“设置绝对目标 + 查询完成状态”；&lt;/li&gt;
&lt;li&gt;无法证明安全时停止自动恢复，要求人工确认。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;客户端本地去重无法覆盖“设备执行后客户端崩溃”的窗口，不能替代设备端协议支持。&lt;/p&gt;
&lt;h2 id="5-重试策略"&gt;&lt;a href="#5-%e9%87%8d%e8%af%95%e7%ad%96%e7%95%a5" class="header-anchor"&gt;&lt;/a&gt;5. 重试策略
&lt;/h2&gt;&lt;p&gt;只有同时满足以下条件时才自动重试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;错误被判定为瞬时；&lt;/li&gt;
&lt;li&gt;操作天然幂等或具有端到端幂等键；&lt;/li&gt;
&lt;li&gt;剩余截止时间足够；&lt;/li&gt;
&lt;li&gt;重试不会违反设备状态机和安全约束。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;退避可使用带抖动的指数策略，并设置最大间隔、最大次数和总体截止时间。所有客户端若固定每秒同时重连，会在设备恢复瞬间形成惊群。&lt;/p&gt;
&lt;p&gt;协议错误、认证失败、参数非法和安全互锁通常不是“多试几次”能解决的。&lt;/p&gt;
&lt;h2 id="6-重连是新会话"&gt;&lt;a href="#6-%e9%87%8d%e8%bf%9e%e6%98%af%e6%96%b0%e4%bc%9a%e8%af%9d" class="header-anchor"&gt;&lt;/a&gt;6. 重连是新会话
&lt;/h2&gt;&lt;p&gt;连接恢复后，不要直接把状态标为 Ready。稳健流程通常是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;断线 → 取消旧请求 → 退避 → 建连 → 握手 → 身份核对
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 能力/版本核对 → 查询设备状态 → 必要的重新同步 → Ready
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;每次连接分配 &lt;code&gt;ConnectionGeneration&lt;/code&gt;。接收、超时和回调都携带世代号，旧连接迟到的完成事件不能改变新连接状态。&lt;/p&gt;
&lt;p&gt;串口重开前还可能需要等待线路静默或丢弃残留输入；TCP 重连则得到新的字节流，旧连接的事务 ID 不能无条件沿用。&lt;/p&gt;
&lt;h2 id="7-超时预算而不是孤立超时"&gt;&lt;a href="#7-%e8%b6%85%e6%97%b6%e9%a2%84%e7%ae%97%e8%80%8c%e4%b8%8d%e6%98%af%e5%ad%a4%e7%ab%8b%e8%b6%85%e6%97%b6" class="header-anchor"&gt;&lt;/a&gt;7. 超时预算而不是孤立超时
&lt;/h2&gt;&lt;p&gt;一次业务操作可能经过队列、发送、设备执行和响应。用单调时间计算每一步剩余预算：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Deadline&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;TimeProvider&lt;/span&gt; &lt;span class="n"&gt;_timeProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;_startedAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;_budget&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Deadline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeProvider&lt;/span&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_timeProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_startedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetTimestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_budget&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;Remaining&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;get&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_budget&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_timeProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetElapsedTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_startedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;TimeProvider.GetTimestamp()&lt;/code&gt; 提供单调时间戳，避免墙上时钟校准造成跳变；测试中还可以注入可控的时间提供器。&lt;/p&gt;
&lt;h2 id="8-熔断与限流放在哪里"&gt;&lt;a href="#8-%e7%86%94%e6%96%ad%e4%b8%8e%e9%99%90%e6%b5%81%e6%94%be%e5%9c%a8%e5%93%aa%e9%87%8c" class="header-anchor"&gt;&lt;/a&gt;8. 熔断与限流放在哪里
&lt;/h2&gt;&lt;p&gt;设备通常是低并发、强状态资源。比通用 HTTP 客户端更重要的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有界命令队列，满时明确拒绝或合并；&lt;/li&gt;
&lt;li&gt;同一设备的并发度符合协议能力；&lt;/li&gt;
&lt;li&gt;连续失败后进入 Faulted，避免无限轰炸设备；&lt;/li&gt;
&lt;li&gt;恢复探测只允许少量安全命令；&lt;/li&gt;
&lt;li&gt;急停和安全回路不依赖普通应用重试链路。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;熔断器不能代替设备状态机，也不能把未知结果变成失败。设备级故障闭环见《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/equipment-architecture/06-fault-recovery/" &gt;设备软件架构与控制模型（六）：故障处理、报警与安全恢复&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="9-可观测性"&gt;&lt;a href="#9-%e5%8f%af%e8%a7%82%e6%b5%8b%e6%80%a7" class="header-anchor"&gt;&lt;/a&gt;9. 可观测性
&lt;/h2&gt;&lt;p&gt;一次操作至少关联这些字段：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DeviceId, ConnectionGeneration, CommandId, Operation,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Attempt, QueueDuration, RoundTripDuration, Outcome, ErrorCategory
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;指标应区分连接失败、协议坏帧、设备拒绝、业务超时和未知结果。只统计一个“通信失败率”，无法指导恢复。&lt;/p&gt;
&lt;h2 id="10-测试故障窗口"&gt;&lt;a href="#10-%e6%b5%8b%e8%af%95%e6%95%85%e9%9a%9c%e7%aa%97%e5%8f%a3" class="header-anchor"&gt;&lt;/a&gt;10. 测试故障窗口
&lt;/h2&gt;&lt;p&gt;模拟器应能在每个阶段注入故障：收到命令前断开、执行前断开、执行后响应前断开、发送半帧、重复响应、迟到响应、重启后去重表丢失。然后验证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非幂等动作不会自动重复；&lt;/li&gt;
&lt;li&gt;旧世代响应不能完成新请求；&lt;/li&gt;
&lt;li&gt;队列和等待表最终清空；&lt;/li&gt;
&lt;li&gt;未知结果被显式上报；&lt;/li&gt;
&lt;li&gt;停止信号能中断退避和重连。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="11-小结"&gt;&lt;a href="#11-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;11. 小结
&lt;/h2&gt;&lt;p&gt;超时只是观察，不是结论；重连是新会话，不是恢复旧 socket；重试只有在端到端幂等成立时才安全。把“未知结果”建模出来，并通过查询、对账和连接世代恢复，设备软件才能在最棘手的故障窗口中保持可控。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9293.html" target="_blank" rel="noopener"
 &gt;RFC 9293：Transmission Control Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/datetime/timeprovider-overview" target="_blank" rel="noopener"
 &gt;Microsoft Learn：TimeProvider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/azure/architecture/patterns/retry" target="_blank" rel="noopener"
 &gt;Microsoft Azure Architecture Center：Retry pattern&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备通信与协议编程（五）：TCP 设备协议与请求响应模型</title><link>https://www.jiwei.space/posts/equipment/device-communication/05-tcp-device-protocol/</link><pubDate>Thu, 30 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/05-tcp-device-protocol/</guid><description>&lt;p&gt;把设备从串口换成 TCP，并不会自动获得消息边界、请求关联、心跳或重连语义。TCP 提供可靠、有序的双向字节流；设备应用协议仍要定义一条消息如何编码，以及连接中断后业务处于什么状态。&lt;/p&gt;
&lt;h2 id="1-tcp-保证什么不保证什么"&gt;&lt;a href="#1-tcp-%e4%bf%9d%e8%af%81%e4%bb%80%e4%b9%88%e4%b8%8d%e4%bf%9d%e8%af%81%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;1. TCP 保证什么，不保证什么
&lt;/h2&gt;&lt;p&gt;RFC 9293 定义的 TCP 关键性质包括可靠、有序、字节流传输。它不保证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一次 &lt;code&gt;Send&lt;/code&gt; 对应对端一次 &lt;code&gt;Receive&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;应用命令执行且持久化；&lt;/li&gt;
&lt;li&gt;连接空闲时设备一定仍在线；&lt;/li&gt;
&lt;li&gt;自动区分请求、响应和设备事件；&lt;/li&gt;
&lt;li&gt;断线重连后延续旧会话状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“没有丢字节”与“业务操作成功”是两个层次。&lt;/p&gt;
&lt;h2 id="2-在字节流上建立帧"&gt;&lt;a href="#2-%e5%9c%a8%e5%ad%97%e8%8a%82%e6%b5%81%e4%b8%8a%e5%bb%ba%e7%ab%8b%e5%b8%a7" class="header-anchor"&gt;&lt;/a&gt;2. 在字节流上建立帧
&lt;/h2&gt;&lt;p&gt;TCP 接收端必须复用本系列《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/device-communication/03-binary-framing/" &gt;设备通信与协议编程（三）：二进制协议帧、消息边界与版本兼容&lt;/a&gt;》建立的增量解析模型。长度前缀是常见选择：先读固定头、验证长度上限，再累积载荷。不要用 &lt;code&gt;NetworkStream.DataAvailable&lt;/code&gt; 判断消息结束；它只表示此刻是否有可立即读取的数据。&lt;/p&gt;
&lt;p&gt;同样不要假设 &lt;code&gt;ReadAsync&lt;/code&gt; 会填满缓冲区。需要固定长度时应循环读取：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;ReadExactlyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Stream&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Memory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;..],&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;EndOfStreamException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Peer closed the connection.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;.NET 的 &lt;code&gt;Stream.ReadExactlyAsync&lt;/code&gt; 已提供类似能力；手写版本用于说明循环读取的不变量。&lt;/p&gt;
&lt;h2 id="3-连接生命周期"&gt;&lt;a href="#3-%e8%bf%9e%e6%8e%a5%e7%94%9f%e5%91%bd%e5%91%a8%e6%9c%9f" class="header-anchor"&gt;&lt;/a&gt;3. 连接生命周期
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Net.Sockets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;RunSessionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TcpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NetworkStream&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetStream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 在这里完成协议握手，再启动唯一的接收循环。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ReceiveFramesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;ReceiveFramesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Stream&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 把 buffer[0..count] 交给增量帧解码器。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;TcpClient.Connected&lt;/code&gt; 只反映最近一次 I/O 已知的状态，不能作为当前在线证明。真正发现失效通常依赖读写结果、协议心跳或业务超时。&lt;/p&gt;
&lt;h2 id="4-请求响应协调器"&gt;&lt;a href="#4-%e8%af%b7%e6%b1%82%e5%93%8d%e5%ba%94%e5%8d%8f%e8%b0%83%e5%99%a8" class="header-anchor"&gt;&lt;/a&gt;4. 请求/响应协调器
&lt;/h2&gt;&lt;p&gt;若协议支持并发请求，每条请求应携带事务 ID（例如第 3 篇帧结构中的 Sequence 字段）。客户端维护：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;transactionId → TaskCompletionSource&amp;lt;Response&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;接收循环解析完整响应后完成对应任务；设备事件则进入独立事件通道。实现时要保证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注册等待项发生在发送前，避免响应过快造成竞态；&lt;/li&gt;
&lt;li&gt;超时、取消、断线都会从表中移除等待项；&lt;/li&gt;
&lt;li&gt;事务 ID 有界且可回绕，不与在途请求重复；&lt;/li&gt;
&lt;li&gt;未知或重复响应会记录并丢弃，不完成错误请求；&lt;/li&gt;
&lt;li&gt;断线时一次性失败当前连接世代的全部等待项。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不支持事务 ID 的协议通常只能串行执行请求。&lt;/p&gt;
&lt;h2 id="5-发送也需要单一所有者"&gt;&lt;a href="#5-%e5%8f%91%e9%80%81%e4%b9%9f%e9%9c%80%e8%a6%81%e5%8d%95%e4%b8%80%e6%89%80%e6%9c%89%e8%80%85" class="header-anchor"&gt;&lt;/a&gt;5. 发送也需要单一所有者
&lt;/h2&gt;&lt;p&gt;多个任务同时向同一 &lt;code&gt;NetworkStream&lt;/code&gt; 写入时，即使每次写都成功，两个应用帧仍可能在调用层交错。用发送队列或锁保证“一帧编码完成后整体写入”。大帧要考虑背压，不要让无限队列吞掉内存。&lt;/p&gt;
&lt;p&gt;高频小消息是否启用 Nagle 算法要通过协议时延和吞吐测试决定。&lt;code&gt;NoDelay = true&lt;/code&gt; 不是设备通信的通用必选项。Nagle 与 TCP_NODELAY 的机制与参数详见《&lt;a class="link" href="https://www.jiwei.space/posts/networking/tcp-ip/09-tcp-advanced/" &gt;TCP/IP（九）：TCP 进阶——SACK、Nagle、keepalive 与端口复用&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="6-超时分层"&gt;&lt;a href="#6-%e8%b6%85%e6%97%b6%e5%88%86%e5%b1%82" class="header-anchor"&gt;&lt;/a&gt;6. 超时分层
&lt;/h2&gt;&lt;p&gt;至少区分：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;超时&lt;/th&gt;
 &lt;th&gt;含义&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;连接超时&lt;/td&gt;
 &lt;td&gt;TCP 建连在预算内未完成&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;写入超时&lt;/td&gt;
 &lt;td&gt;数据未能在预算内交给传输栈&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;首字节/完整帧超时&lt;/td&gt;
 &lt;td&gt;对端未及时开始或完成响应&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;业务超时&lt;/td&gt;
 &lt;td&gt;设备操作本身未在预期时间完成&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;空闲心跳超时&lt;/td&gt;
 &lt;td&gt;长期无业务时检测会话可用性&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一个总的 &lt;code&gt;Timeout = 3000&lt;/code&gt; 无法告诉运维究竟慢在哪里。超时预算还应由上层截止时间向下传递，避免每层各等三秒。&lt;/p&gt;
&lt;h2 id="7-tcp-keepalive-与应用心跳"&gt;&lt;a href="#7-tcp-keepalive-%e4%b8%8e%e5%ba%94%e7%94%a8%e5%bf%83%e8%b7%b3" class="header-anchor"&gt;&lt;/a&gt;7. TCP keepalive 与应用心跳
&lt;/h2&gt;&lt;p&gt;TCP keepalive 是实现可选的连接探测机制，RFC 9293 要求默认关闭且默认探测间隔不得短于两小时。操作系统允许调整时，它仍主要判断传输连接，不了解设备业务线程是否卡死。keepalive 机制与参数调优同样见《&lt;a class="link" href="https://www.jiwei.space/posts/networking/tcp-ip/09-tcp-advanced/" &gt;TCP/IP（九）&lt;/a&gt;》。&lt;/p&gt;
&lt;p&gt;应用心跳可以携带协议版本、设备状态和会话世代，但会增加负载。若正常业务本就持续往返，可直接以业务响应作为活性证据，避免重复心跳。&lt;/p&gt;
&lt;h2 id="8-安全边界"&gt;&lt;a href="#8-%e5%ae%89%e5%85%a8%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;8. 安全边界
&lt;/h2&gt;&lt;p&gt;原始 TCP 不提供机密性和对端身份。跨越不可信网络时应使用 TLS、VPN 或协议规定的安全层，并验证证书和设备身份。不要自创“异或加密”。&lt;/p&gt;
&lt;p&gt;即使在隔离网络，解析器仍应限制帧长、并发请求数、发送队列和日志载荷，防止故障设备拖垮上位机。&lt;/p&gt;
&lt;h2 id="9-小结"&gt;&lt;a href="#9-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;9. 小结
&lt;/h2&gt;&lt;p&gt;TCP 解决可靠有序字节流，设备协议负责消息、事务和业务结果。稳定客户端需要唯一接收循环、明确帧边界、可关联的请求响应、有界发送队列和分层超时。下一篇将在此基础上处理断线、迟到响应和“操作到底执行了没有”的灰色状态。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9293.html" target="_blank" rel="noopener"
 &gt;RFC 9293：Transmission Control Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/fundamentals/networking/sockets/sockets-overview" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Sockets in .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/api/system.io.stream.readexactlyasync" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Stream.ReadExactlyAsync&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备通信与协议编程（四）：校验和与 CRC 的能力边界</title><link>https://www.jiwei.space/posts/equipment/device-communication/04-checksum-crc/</link><pubDate>Wed, 29 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/04-checksum-crc/</guid><description>&lt;p&gt;设备协议里常见“校验和不对”“CRC 失败”，但 CRC 不是某个唯一算法，校验通过也不等于消息可信。真正落地时，必须同时确定算法参数、覆盖范围、字节顺序和错误后的恢复行为。&lt;/p&gt;
&lt;h2 id="1-校验到底解决什么"&gt;&lt;a href="#1-%e6%a0%a1%e9%aa%8c%e5%88%b0%e5%ba%95%e8%a7%a3%e5%86%b3%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;1. 校验到底解决什么
&lt;/h2&gt;&lt;p&gt;校验字段用于发现传输、存储或组帧过程中的偶发错误。常见方法包括：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;方法&lt;/th&gt;
 &lt;th&gt;特点&lt;/th&gt;
 &lt;th&gt;适用边界&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;XOR&lt;/td&gt;
 &lt;td&gt;极易实现，检测能力有限&lt;/td&gt;
 &lt;td&gt;简单遗留协议&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;加法和&lt;/td&gt;
 &lt;td&gt;可检测部分错误，容易碰撞&lt;/td&gt;
 &lt;td&gt;资源受限或既有协议&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Fletcher / Adler&lt;/td&gt;
 &lt;td&gt;计算便宜，能力取决于参数与报文长度&lt;/td&gt;
 &lt;td&gt;软件数据完整性场景&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;CRC&lt;/td&gt;
 &lt;td&gt;对特定错误模式有可分析的检测能力&lt;/td&gt;
 &lt;td&gt;串口、总线、文件和网络帧&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;密码学 MAC&lt;/td&gt;
 &lt;td&gt;同时验证完整性与共享密钥持有者&lt;/td&gt;
 &lt;td&gt;需要抵抗主动攻击的场景&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;CRC 的检测能力取决于位宽、多项式和受保护消息的最大长度。设计良好的 n 位 CRC 可保证检出长度不超过 n bit 的突发错误；能否保证检出全部单比特、双比特或奇数个 bit 错误，还要检查具体多项式及码字长度。&lt;code&gt;2⁻ⁿ&lt;/code&gt; 只能近似描述特定随机错误模型下的残余概率，不能当作所有链路和错误模式的固定漏检率。测试时应先声明目标消息长度和预期检测的错误类别，再选择多项式并注入验证。&lt;/p&gt;
&lt;p&gt;CRC 不含秘密。攻击者修改数据后可以重新计算 CRC，因此它不是认证机制。&lt;/p&gt;
&lt;h2 id="2-crc-16信息不够"&gt;&lt;a href="#2-crc-16%e4%bf%a1%e6%81%af%e4%b8%8d%e5%a4%9f" class="header-anchor"&gt;&lt;/a&gt;2. “CRC-16”信息不够
&lt;/h2&gt;&lt;p&gt;一个 CRC 实例至少由以下参数确定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;width（位宽，多项式隐含但单独列出更清晰）；&lt;/li&gt;
&lt;li&gt;多项式（poly）；&lt;/li&gt;
&lt;li&gt;初始值（init）；&lt;/li&gt;
&lt;li&gt;输入、输出是否反射（refin/refout）；&lt;/li&gt;
&lt;li&gt;输出异或值（xorout）；&lt;/li&gt;
&lt;li&gt;校验值在线路上的字节顺序；&lt;/li&gt;
&lt;li&gt;CRC 覆盖哪些字段。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个协议都写“CRC-16”，结果仍可能完全不同。实现前应从设备协议或标准获得完整参数，并用规范测试向量核对，而不是挑一个搜索结果相似的函数。&lt;/p&gt;
&lt;h2 id="3-modbus-rtu-的-crc-示例"&gt;&lt;a href="#3-modbus-rtu-%e7%9a%84-crc-%e7%a4%ba%e4%be%8b" class="header-anchor"&gt;&lt;/a&gt;3. Modbus RTU 的 CRC 示例
&lt;/h2&gt;&lt;p&gt;Modbus 串行线路规范定义 RTU 帧使用 CRC-16；CRC 字段在消息中先发送低字节，再发送高字节。下面函数返回数值形式的 CRC，序列化时再明确字节顺序：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ModbusCrc16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;Compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0xFFFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="p"&gt;^=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;bit&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;lsb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lsb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="p"&gt;^=&lt;/span&gt; &lt;span class="m"&gt;0xA001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;WriteLittleEndian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;CRC needs two bytes.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;可用标准检查字符串验证核心实现：ASCII &lt;code&gt;123456789&lt;/code&gt; 的该 CRC 结果应为 &lt;code&gt;0x4B37&lt;/code&gt;。这只证明参数化核心吻合；仍要用真实协议帧验证覆盖范围和线路字节序。&lt;/p&gt;
&lt;h2 id="4-覆盖范围要写成可执行规则"&gt;&lt;a href="#4-%e8%a6%86%e7%9b%96%e8%8c%83%e5%9b%b4%e8%a6%81%e5%86%99%e6%88%90%e5%8f%af%e6%89%a7%e8%a1%8c%e8%a7%84%e5%88%99" class="header-anchor"&gt;&lt;/a&gt;4. 覆盖范围要写成可执行规则
&lt;/h2&gt;&lt;p&gt;“对整帧做 CRC”仍然含糊。应明确类似规则：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CRC = Compute(Address || Function || Data)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CRC 字段自身不参与计算
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;发送顺序 = CRC low byte, CRC high byte
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;若协议含转义，还要说明 CRC 计算的是转义前原始字节还是线路上的转义后字节。若含长度字段，要说明长度是否参与计算。编码器和解码器应共享同一条规则，避免各自拼切片。&lt;/p&gt;
&lt;h2 id="5-校验失败后怎么办"&gt;&lt;a href="#5-%e6%a0%a1%e9%aa%8c%e5%a4%b1%e8%b4%a5%e5%90%8e%e6%80%8e%e4%b9%88%e5%8a%9e" class="header-anchor"&gt;&lt;/a&gt;5. 校验失败后怎么办
&lt;/h2&gt;&lt;p&gt;校验失败意味着当前候选帧不可交付，但不一定说明端口已断开。恢复策略取决于定界方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;固定长度帧：丢弃当前候选帧，再寻找下一同步点；&lt;/li&gt;
&lt;li&gt;Magic + 长度：从后续可能的 Magic 重新扫描，同时限制扫描量；&lt;/li&gt;
&lt;li&gt;静默间隔：丢弃该时间窗口内的消息；&lt;/li&gt;
&lt;li&gt;TCP 长度帧：若头部已失去可信度，通常应关闭会话，避免无限错位解析。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要把坏帧原样交给业务层“看看能不能用”，也不要在每个 CRC 错误后立刻重连。应记录计数、端口、方向、帧长度和有限的十六进制摘要，避免把整段敏感数据写入日志。&lt;/p&gt;
&lt;h2 id="6-表驱动与硬件实现"&gt;&lt;a href="#6-%e8%a1%a8%e9%a9%b1%e5%8a%a8%e4%b8%8e%e7%a1%ac%e4%bb%b6%e5%ae%9e%e7%8e%b0" class="header-anchor"&gt;&lt;/a&gt;6. 表驱动与硬件实现
&lt;/h2&gt;&lt;p&gt;查表法能减少逐 bit 计算，硬件 CRC 外设则可进一步降低 CPU 占用。但它们必须与协议参数完全一致。不同 CPU 指令或硬件外设支持的多项式、反射和初值集合并不相同。&lt;/p&gt;
&lt;p&gt;先以清晰的参考实现和测试向量建立正确性，再替换为表驱动或硬件加速版本；优化后对同一语料做逐帧差分测试。&lt;/p&gt;
&lt;h2 id="7-测试清单"&gt;&lt;a href="#7-%e6%b5%8b%e8%af%95%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;7. 测试清单
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;空消息、单字节、全零、全 &lt;code&gt;0xFF&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;标准检查字符串和规范示例帧；&lt;/li&gt;
&lt;li&gt;分别翻转每一个 bit，确认检测结果符合预期；&lt;/li&gt;
&lt;li&gt;CRC 字节交换、覆盖范围少一个字节等常见错误；&lt;/li&gt;
&lt;li&gt;编码后再解析的往返测试；&lt;/li&gt;
&lt;li&gt;与设备抓包、厂商工具或另一独立实现交叉验证。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="8-小结"&gt;&lt;a href="#8-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;8. 小结
&lt;/h2&gt;&lt;p&gt;校验算法的名字不是完整契约。只有参数、覆盖范围和字节序全部一致，双方才会得到相同结果；CRC 的职责是检测偶发错误，而不是认证或加密。把参考向量固化为自动化测试，是避免设备现场反复猜算法的最低成本手段。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.modbus.org/modbus-specifications" target="_blank" rel="noopener"
 &gt;Modbus Organization：Modbus Serial Line Protocol and Implementation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc1071.html" target="_blank" rel="noopener"
 &gt;RFC 1071：Computing the Internet Checksum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://technav.ieee.org/area/cyclic-redundancy-check/" target="_blank" rel="noopener"
 &gt;IEEE Technology Navigator：Cyclic Redundancy Check&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://reveng.sourceforge.io/crc-catalogue/" target="_blank" rel="noopener"
 &gt;Catalogue of parametrised CRC algorithms&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备通信与协议编程（三）：二进制协议帧、消息边界与版本兼容</title><link>https://www.jiwei.space/posts/equipment/device-communication/03-binary-framing/</link><pubDate>Tue, 28 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/03-binary-framing/</guid><description>&lt;p&gt;串口和 TCP 都提供字节流或字节块，却不会替应用识别“这一条命令到哪里结束”。二进制协议设计的第一要务，是让接收端在拆包、粘包（一次读取返回半帧或多帧，见上一篇）、噪声和版本升级下仍能确定消息边界；TCP 传输下的完整请求/响应模型见本系列第 5 篇。&lt;/p&gt;
&lt;h2 id="1-一帧需要承担什么"&gt;&lt;a href="#1-%e4%b8%80%e5%b8%a7%e9%9c%80%e8%a6%81%e6%89%bf%e6%8b%85%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;1. 一帧需要承担什么
&lt;/h2&gt;&lt;p&gt;一个常见帧结构如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Magic | Version | Flags | Command | Sequence | PayloadLength | Payload | Check
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;字段&lt;/th&gt;
 &lt;th&gt;作用&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Magic&lt;/td&gt;
 &lt;td&gt;快速识别协议并在噪声后重新同步&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Version&lt;/td&gt;
 &lt;td&gt;明确解析规则，不靠猜测&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Flags&lt;/td&gt;
 &lt;td&gt;表示应答、错误、压缩等可选语义&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Command&lt;/td&gt;
 &lt;td&gt;说明载荷类型&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Sequence&lt;/td&gt;
 &lt;td&gt;关联请求与响应、识别迟到消息&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;PayloadLength&lt;/td&gt;
 &lt;td&gt;给出边界，并在分配前做上限检查&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Check&lt;/td&gt;
 &lt;td&gt;检测传输或组帧错误，不等同于安全认证&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;字段不是越多越好。每个字段都应对应一个真实的演进、诊断或恢复需求。&lt;/p&gt;
&lt;h2 id="2-三种常见定界方式"&gt;&lt;a href="#2-%e4%b8%89%e7%a7%8d%e5%b8%b8%e8%a7%81%e5%ae%9a%e7%95%8c%e6%96%b9%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;2. 三种常见定界方式
&lt;/h2&gt;&lt;h3 id="21-固定长度"&gt;&lt;a href="#21-%e5%9b%ba%e5%ae%9a%e9%95%bf%e5%ba%a6" class="header-anchor"&gt;&lt;/a&gt;2.1 固定长度
&lt;/h3&gt;&lt;p&gt;实现最简单且时间可预测，但浪费带宽、扩展困难。适合数据结构长期稳定且长度很小的周期报文。&lt;/p&gt;
&lt;h3 id="22-长度前缀"&gt;&lt;a href="#22-%e9%95%bf%e5%ba%a6%e5%89%8d%e7%bc%80" class="header-anchor"&gt;&lt;/a&gt;2.2 长度前缀
&lt;/h3&gt;&lt;p&gt;头部携带载荷长度，适合二进制协议和 TCP。解析前必须满足：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0 &amp;lt;= PayloadLength &amp;lt;= MaxPayloadLength
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最大长度必须来自协议契约，而不是当前机器可用内存。&lt;/p&gt;
&lt;h3 id="23-分隔符与转义"&gt;&lt;a href="#23-%e5%88%86%e9%9a%94%e7%ac%a6%e4%b8%8e%e8%bd%ac%e4%b9%89" class="header-anchor"&gt;&lt;/a&gt;2.3 分隔符与转义
&lt;/h3&gt;&lt;p&gt;文本协议常用换行，二进制链路可用特殊字节加 byte stuffing。若载荷也可能出现分隔符，就要转义；解析器必须定义转义字节本身、连续转义和截断转义如何处理。&lt;/p&gt;
&lt;p&gt;“静默时间作为边界”只适用于明确规定该语义的协议，例如 Modbus RTU。普通异步程序不能依赖一次 &lt;code&gt;Read&lt;/code&gt; 的停顿猜帧结束。&lt;/p&gt;
&lt;h2 id="3-端序必须逐字段定义"&gt;&lt;a href="#3-%e7%ab%af%e5%ba%8f%e5%bf%85%e9%a1%bb%e9%80%90%e5%ad%97%e6%ae%b5%e5%ae%9a%e4%b9%89" class="header-anchor"&gt;&lt;/a&gt;3. 端序必须逐字段定义
&lt;/h2&gt;&lt;p&gt;多字节整数在协议中应明确为 little-endian 或 big-endian。不要把 C# 结构体直接复制到线路上：运行时布局、填充、CPU 端序和版本演进都会让它失去可移植性。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Buffers.Binary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;stackalloc&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0xA5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;BinaryPrimitives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteUInt16BigEndian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;2.&lt;/span&gt;&lt;span class="p"&gt;.],&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;BinaryPrimitives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteUInt32BigEndian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;4.&lt;/span&gt;&lt;span class="p"&gt;.],&lt;/span&gt; &lt;span class="n"&gt;payloadLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;对应读取也使用 &lt;code&gt;BinaryPrimitives.Read*Endian&lt;/code&gt;，让线路端序在代码里可见。&lt;/p&gt;
&lt;h2 id="4-增量解析器的状态"&gt;&lt;a href="#4-%e5%a2%9e%e9%87%8f%e8%a7%a3%e6%9e%90%e5%99%a8%e7%9a%84%e7%8a%b6%e6%80%81" class="header-anchor"&gt;&lt;/a&gt;4. 增量解析器的状态
&lt;/h2&gt;&lt;p&gt;解析器面对的是任意切片，至少需要这几个状态：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;寻找 Magic → 等待固定头 → 校验长度 → 等待完整载荷 → 校验并产出帧
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;关键不变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据不足时返回“需要更多”，而不是把半帧判为错误；&lt;/li&gt;
&lt;li&gt;数据非法时至少丢弃一个字节并继续寻找同步点；&lt;/li&gt;
&lt;li&gt;产出一帧后继续解析缓冲区，处理粘连的下一帧；&lt;/li&gt;
&lt;li&gt;累积缓冲区和单帧长度都有硬上限；&lt;/li&gt;
&lt;li&gt;解析失败不会读取到缓冲区之外。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面展示固定 8 字节头的最小解析函数。它只解析当前连续缓冲区，调用方负责保留未消费尾部：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Buffers.Binary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;DeviceFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ReadOnlyMemory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;ParseResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;NeedMoreData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InvalidData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FrameParser&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;Magic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0xA5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;HeaderSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;MaxPayloadLength&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ParseResult&lt;/span&gt; &lt;span class="n"&gt;TryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ReadOnlyMemory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;DeviceFrame&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;consumed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;consumed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ParseResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NeedMoreData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;Magic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;consumed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ParseResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvalidData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;HeaderSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ParseResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NeedMoreData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BinaryPrimitives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadUInt16BigEndian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;2.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BinaryPrimitives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadUInt32BigEndian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;4.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MaxPayloadLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;consumed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ParseResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvalidData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;frameLength&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HeaderSize&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;frameLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ParseResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NeedMoreData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ToArray 让产出的帧不依赖调用方随后复用的接收缓冲区。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HeaderSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;consumed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frameLength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ParseResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;生产实现还要把校验字段纳入帧长，并决定错误后是丢一个字节、跳到下一个 Magic，还是重置整个会话。Magic 可能出现在载荷中，因此“直接跳到下一个 Magic”只是恢复启发式，不是正确性证明。&lt;/p&gt;
&lt;h2 id="5-请求响应和异步事件"&gt;&lt;a href="#5-%e8%af%b7%e6%b1%82%e5%93%8d%e5%ba%94%e5%92%8c%e5%bc%82%e6%ad%a5%e4%ba%8b%e4%bb%b6" class="header-anchor"&gt;&lt;/a&gt;5. 请求、响应和异步事件
&lt;/h2&gt;&lt;p&gt;设备协议通常同时存在三类消息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求：主机发起操作；&lt;/li&gt;
&lt;li&gt;响应：携带相同序列号或明确的关联字段；&lt;/li&gt;
&lt;li&gt;事件：设备主动上报，不应被误配给当前请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只有一个在途请求时，也建议保留序列号。超时响应、重连前残留数据和日志关联都会用到它。序列号回绕时，要结合连接世代和有限的在途窗口判断，不能假设整数永不重复。命令合法性与排队执行的模型见《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/equipment-architecture/04-state-command-workflow/" &gt;设备软件架构与控制模型（四）：状态机、命令队列与流程编排&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="6-版本兼容策略"&gt;&lt;a href="#6-%e7%89%88%e6%9c%ac%e5%85%bc%e5%ae%b9%e7%ad%96%e7%95%a5" class="header-anchor"&gt;&lt;/a&gt;6. 版本兼容策略
&lt;/h2&gt;&lt;p&gt;版本字段不是万能开关。更稳健的演进规则是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;固定头尽量稳定，新字段放在有长度的扩展区；&lt;/li&gt;
&lt;li&gt;未识别的可选字段可以跳过，未识别的关键语义必须拒绝；&lt;/li&gt;
&lt;li&gt;枚举预留未知值处理，不把未来值映射成当前默认值；&lt;/li&gt;
&lt;li&gt;请求和响应都显式协商能力，不仅比较一个版本数字；&lt;/li&gt;
&lt;li&gt;旧固件无法安全理解的命令，不要靠“尽量解析”冒险执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="7-安全与资源边界"&gt;&lt;a href="#7-%e5%ae%89%e5%85%a8%e4%b8%8e%e8%b5%84%e6%ba%90%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;7. 安全与资源边界
&lt;/h2&gt;&lt;p&gt;二进制解析器直接处理不可信输入。即使设备位于内网，也应防御：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;长度字段导致的超大分配；&lt;/li&gt;
&lt;li&gt;整数加法溢出；&lt;/li&gt;
&lt;li&gt;无限等待未完成帧；&lt;/li&gt;
&lt;li&gt;垃圾输入导致 O(n²) 扫描；&lt;/li&gt;
&lt;li&gt;压缩载荷解压炸弹；&lt;/li&gt;
&lt;li&gt;日志直接输出敏感载荷。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CRC 只能检测偶发错误，不能证明发送者身份，也不能阻止恶意篡改。需要安全边界时使用经过评审的认证与加密方案。&lt;/p&gt;
&lt;h2 id="8-测试矩阵"&gt;&lt;a href="#8-%e6%b5%8b%e8%af%95%e7%9f%a9%e9%98%b5" class="header-anchor"&gt;&lt;/a&gt;8. 测试矩阵
&lt;/h2&gt;&lt;p&gt;最少覆盖：空输入、逐字节输入、头部截断、载荷截断、两帧粘连、Magic 错误、未知版本、长度为零、最大合法长度、超过上限、校验错误、随机噪声后恢复和序列号回绕。&lt;/p&gt;
&lt;p&gt;属性测试或模糊测试尤其适合解析器：对任意输入，函数都不应越界、挂死或无界分配；成功产出的帧再编码后应满足约定的不变量。与模拟器/回放环境的整体测试梯度见《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/equipment-architecture/07-simulation-hil/" &gt;设备软件架构与控制模型（七）：实机、模拟器与 HIL&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="9-小结"&gt;&lt;a href="#9-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;9. 小结
&lt;/h2&gt;&lt;p&gt;可靠协议帧的核心是明确边界和失败语义。长度、端序、版本、序列号与资源上限必须写进契约；解析器要按任意分片增量工作，而不是依赖底层读取次数。下一篇将继续讨论校验和与 CRC 的能力边界。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9293.html" target="_blank" rel="noopener"
 &gt;RFC 9293：Transmission Control Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/api/system.buffers.binary.binaryprimitives" target="_blank" rel="noopener"
 &gt;Microsoft Learn：BinaryPrimitives&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/io/pipelines" target="_blank" rel="noopener"
 &gt;Microsoft Learn：System.IO.Pipelines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备通信与协议编程（二）：C# 串口生命周期、缓冲区与异步接收</title><link>https://www.jiwei.space/posts/equipment/device-communication/02-csharp-serial-port/</link><pubDate>Mon, 27 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/02-csharp-serial-port/</guid><description>&lt;p&gt;C# 的 &lt;code&gt;SerialPort&lt;/code&gt; API 很容易打开端口，却不容易写出可停止、可重连、不会丢边界的长期运行通信模块。本篇以 .NET 10 和 &lt;code&gt;System.IO.Ports&lt;/code&gt; 包为基线，重点讨论端口所有权、单一接收循环、发送串行化与取消操作。&lt;/p&gt;
&lt;h2 id="1-串口对象不是协议客户端"&gt;&lt;a href="#1-%e4%b8%b2%e5%8f%a3%e5%af%b9%e8%b1%a1%e4%b8%8d%e6%98%af%e5%8d%8f%e8%ae%ae%e5%ae%a2%e6%88%b7%e7%ab%af" class="header-anchor"&gt;&lt;/a&gt;1. 串口对象不是协议客户端
&lt;/h2&gt;&lt;p&gt;建议把通信栈拆成四层：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;业务命令 → 请求/响应协调器 → 编解码器 → 串口传输
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;SerialPort&lt;/code&gt; 只负责传输字节。不要让 UI 直接调用 &lt;code&gt;ReadLine&lt;/code&gt;，也不要在 &lt;code&gt;DataReceived&lt;/code&gt; 里解析业务、更新控件和重试命令。否则端口生命周期、线程上下文与协议状态会纠缠在一起。把厂商差异挡在业务之外的做法见《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/equipment-architecture/02-hal-adapters/" &gt;设备软件架构与控制模型（二）：硬件抽象层与设备适配器&lt;/a&gt;》。&lt;/p&gt;
&lt;p&gt;项目需显式引用包：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Ports&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;包版本应与项目目标框架和组织依赖策略匹配，不要从文章复制一个未来会过期的固定版本号。&lt;/p&gt;
&lt;h2 id="2-配置必须来自设备契约"&gt;&lt;a href="#2-%e9%85%8d%e7%bd%ae%e5%bf%85%e9%a1%bb%e6%9d%a5%e8%87%aa%e8%ae%be%e5%a4%87%e5%a5%91%e7%ba%a6" class="header-anchor"&gt;&lt;/a&gt;2. 配置必须来自设备契约
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.IO.Ports&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SerialPort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;COM3&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;BaudRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;115200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DataBits&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Parity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Parity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;StopBits&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StopBits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;One&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Handshake&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Handshake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ReadTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;WriteTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这些值不是通用默认答案。尤其不要在没有接线依据时随意启用 &lt;code&gt;RtsEnable&lt;/code&gt;、&lt;code&gt;DtrEnable&lt;/code&gt; 或硬件流控。&lt;/p&gt;
&lt;p&gt;端口名也不应永久写死。Windows 上同一 USB 设备换插口后可能获得不同 &lt;code&gt;COM&lt;/code&gt; 号，生产软件可同时记录 VID/PID、序列号和用户确认结果。&lt;/p&gt;
&lt;h2 id="3-选择一种接收所有权模型"&gt;&lt;a href="#3-%e9%80%89%e6%8b%a9%e4%b8%80%e7%a7%8d%e6%8e%a5%e6%94%b6%e6%89%80%e6%9c%89%e6%9d%83%e6%a8%a1%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;3. 选择一种接收所有权模型
&lt;/h2&gt;&lt;p&gt;官方文档明确说明：&lt;code&gt;DataReceived&lt;/code&gt; 不保证每收到一个字节都触发，事件可能延迟，并在辅助线程上执行。因此事件只能表示“现在可能有数据可读”，不能表示“一帧到达”。&lt;/p&gt;
&lt;p&gt;对新代码，更容易推理的模型是：打开端口后由一个后台任务持续读取 &lt;code&gt;BaseStream&lt;/code&gt;，所有收到的字节都交给同一个解码器。不要同时使用 &lt;code&gt;SerialPort.Read*&lt;/code&gt;、&lt;code&gt;DataReceived&lt;/code&gt; 和 &lt;code&gt;BaseStream.ReadAsync&lt;/code&gt; 争抢同一输入流。&lt;/p&gt;
&lt;h2 id="4-一个可控的传输骨架"&gt;&lt;a href="#4-%e4%b8%80%e4%b8%aa%e5%8f%af%e6%8e%a7%e7%9a%84%e4%bc%a0%e8%be%93%e9%aa%a8%e6%9e%b6" class="header-anchor"&gt;&lt;/a&gt;4. 一个可控的传输骨架
&lt;/h2&gt;&lt;p&gt;下面示例只负责字节传输，不假装解决协议边界：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;span class="lnt"&gt;58
&lt;/span&gt;&lt;span class="lnt"&gt;59
&lt;/span&gt;&lt;span class="lnt"&gt;60
&lt;/span&gt;&lt;span class="lnt"&gt;61
&lt;/span&gt;&lt;span class="lnt"&gt;62
&lt;/span&gt;&lt;span class="lnt"&gt;63
&lt;/span&gt;&lt;span class="lnt"&gt;64
&lt;/span&gt;&lt;span class="lnt"&gt;65
&lt;/span&gt;&lt;span class="lnt"&gt;66
&lt;/span&gt;&lt;span class="lnt"&gt;67
&lt;/span&gt;&lt;span class="lnt"&gt;68
&lt;/span&gt;&lt;span class="lnt"&gt;69
&lt;/span&gt;&lt;span class="lnt"&gt;70
&lt;/span&gt;&lt;span class="lnt"&gt;71
&lt;/span&gt;&lt;span class="lnt"&gt;72
&lt;/span&gt;&lt;span class="lnt"&gt;73
&lt;/span&gt;&lt;span class="lnt"&gt;74
&lt;/span&gt;&lt;span class="lnt"&gt;75
&lt;/span&gt;&lt;span class="lnt"&gt;76
&lt;/span&gt;&lt;span class="lnt"&gt;77
&lt;/span&gt;&lt;span class="lnt"&gt;78
&lt;/span&gt;&lt;span class="lnt"&gt;79
&lt;/span&gt;&lt;span class="lnt"&gt;80
&lt;/span&gt;&lt;span class="lnt"&gt;81
&lt;/span&gt;&lt;span class="lnt"&gt;82
&lt;/span&gt;&lt;span class="lnt"&gt;83
&lt;/span&gt;&lt;span class="lnt"&gt;84
&lt;/span&gt;&lt;span class="lnt"&gt;85
&lt;/span&gt;&lt;span class="lnt"&gt;86
&lt;/span&gt;&lt;span class="lnt"&gt;87
&lt;/span&gt;&lt;span class="lnt"&gt;88
&lt;/span&gt;&lt;span class="lnt"&gt;89
&lt;/span&gt;&lt;span class="lnt"&gt;90
&lt;/span&gt;&lt;span class="lnt"&gt;91
&lt;/span&gt;&lt;span class="lnt"&gt;92
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.IO.Ports&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SerialTransport&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncDisposable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SerialPort&lt;/span&gt; &lt;span class="n"&gt;_port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SemaphoreSlim&lt;/span&gt; &lt;span class="n"&gt;_writeLock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_lifetime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_receiveTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;SerialTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;portName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;baudRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SerialPort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;portName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baudRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Parity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StopBits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;One&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Handshake&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Handshake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReadOnlyMemory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_lifetime&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Transport is already open.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_lifetime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_receiveTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReceiveLoopAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_lifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;ReceiveLoopAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReadOnlyMemory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;EndOfStreamException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Serial stream ended.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// onBytes 必须在返回前消费或复制数据，不能保存这段可复用缓冲区。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;onBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ReadOnlyMemory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_writeLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FlushAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_writeLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lifetime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Interlocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_lifetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lifetime&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CancelAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 同时解除部分平台上无法及时响应取消的阻塞 I/O。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 注意：§2 的 ReadTimeout 只约束同步 Read*（超时抛 TimeoutException）；&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Windows 实现上 BaseStream.ReadAsync 不按 ReadTimeout 超时，&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 接收循环的退出依赖取消令牌与这里的 Close()。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_receiveTask&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_receiveTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_writeLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这个骨架仍需由调用方定义：异常如何上报、断线后是否重建对象、关闭超时多久，以及回调过慢时采用有界队列还是背压。关闭时若仍有写入在途，直接释放 &lt;code&gt;_writeLock&lt;/code&gt; 会让其 &lt;code&gt;Release&lt;/code&gt; 抛出 &lt;code&gt;ObjectDisposedException&lt;/code&gt;——生产实现应先等待写入静默、设置关闭宽限，或让锁的生命周期跟随最后一个写入者。&lt;/p&gt;
&lt;h2 id="5-缓冲区和消息边界"&gt;&lt;a href="#5-%e7%bc%93%e5%86%b2%e5%8c%ba%e5%92%8c%e6%b6%88%e6%81%af%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;5. 缓冲区和消息边界
&lt;/h2&gt;&lt;p&gt;一次 &lt;code&gt;ReadAsync&lt;/code&gt; 可能返回半帧、一帧或多帧。正确的数据流是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;任意字节块 → 累积缓冲区 → 帧解码器 → 完整消息
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;解码器必须保留未完成尾部，并限制最大帧长。否则错误长度字段可能让程序无限等待或持续扩容。若回调把 &lt;code&gt;ReadOnlyMemory&amp;lt;byte&amp;gt;&lt;/code&gt; 放入队列，必须先复制，因为读取循环下一次会覆盖底层数组。&lt;/p&gt;
&lt;h2 id="6-请求响应不能靠多个线程同时读"&gt;&lt;a href="#6-%e8%af%b7%e6%b1%82%e5%93%8d%e5%ba%94%e4%b8%8d%e8%83%bd%e9%9d%a0%e5%a4%9a%e4%b8%aa%e7%ba%bf%e7%a8%8b%e5%90%8c%e6%97%b6%e8%af%bb" class="header-anchor"&gt;&lt;/a&gt;6. 请求/响应不能靠多个线程同时读
&lt;/h2&gt;&lt;p&gt;串口协议常规定“发一条命令，等一条应答”。不要让每个请求各自读取端口；应该只有一个接收循环，再由协调器按序列号、命令字或当前在途请求分发响应。&lt;/p&gt;
&lt;p&gt;没有事务 ID 的半双工协议通常只能保留一个在途请求。超时后迟到的旧响应可能被误认作下一请求的响应，因此恢复策略往往需要清空输入、等待静默窗口或重新建立会话，而不只是立即重发。&lt;/p&gt;
&lt;h2 id="7-关闭拔插与重连"&gt;&lt;a href="#7-%e5%85%b3%e9%97%ad%e6%8b%94%e6%8f%92%e4%b8%8e%e9%87%8d%e8%bf%9e" class="header-anchor"&gt;&lt;/a&gt;7. 关闭、拔插与重连
&lt;/h2&gt;&lt;p&gt;把以下状态区分开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主动停止：取消任务，不应记为通信故障；&lt;/li&gt;
&lt;li&gt;端口被拔出：读取或写入可能抛出 &lt;code&gt;IOException&lt;/code&gt;、&lt;code&gt;UnauthorizedAccessException&lt;/code&gt; 等；&lt;/li&gt;
&lt;li&gt;端口被占用：&lt;code&gt;Open&lt;/code&gt; 失败，应提示占用而不是无限重试；&lt;/li&gt;
&lt;li&gt;协议超时：物理端口仍可能正常，不能直接等同于“串口断开”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;重连应创建新的传输会话并重新执行设备握手。指数退避要设置上限和抖动，同时允许用户立即停止；不要在异常回调里递归调用 &lt;code&gt;Open&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="8-如何验证"&gt;&lt;a href="#8-%e5%a6%82%e4%bd%95%e9%aa%8c%e8%af%81" class="header-anchor"&gt;&lt;/a&gt;8. 如何验证
&lt;/h2&gt;&lt;p&gt;没有实机时，可用成对虚拟串口或 USB 转串口回环测试：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;分别测试 1 字节、半帧、多帧合并和超长垃圾输入；&lt;/li&gt;
&lt;li&gt;在读取期间拔出设备，确认任务能退出且资源释放；&lt;/li&gt;
&lt;li&gt;并发调用 &lt;code&gt;WriteAsync&lt;/code&gt;，确认帧不会交错；&lt;/li&gt;
&lt;li&gt;让消费回调故意变慢，观察内存是否无界增长；&lt;/li&gt;
&lt;li&gt;重连后验证旧请求不会收到新会话的响应。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="9-小结"&gt;&lt;a href="#9-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;9. 小结
&lt;/h2&gt;&lt;p&gt;稳定串口模块的核心不是 &lt;code&gt;Open()&lt;/code&gt;，而是明确所有权：一个端口对象、一个接收循环、一个解码器、串行化写入，以及可取消的生命周期。下一篇将在这条字节流之上实现真正的二进制帧边界。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/api/system.io.ports.serialport" target="_blank" rel="noopener"
 &gt;Microsoft Learn：SerialPort&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/api/system.io.ports.serialport.datareceived" target="_blank" rel="noopener"
 &gt;Microsoft Learn：SerialPort.DataReceived&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/api/system.io.ports.serialport.basestream" target="_blank" rel="noopener"
 &gt;Microsoft Learn：SerialPort.BaseStream&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备通信与协议编程（一）：RS-232、RS-485、UART 与 USB 虚拟串口</title><link>https://www.jiwei.space/posts/equipment/device-communication/01-serial-foundations/</link><pubDate>Sun, 26 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/device-communication/01-serial-foundations/</guid><description>&lt;p&gt;设备接上电脑、系统里出现一个 &lt;code&gt;COM&lt;/code&gt; 口，并不意味着它“使用 RS-232 协议”。工程现场常把 UART、RS-232、RS-485 和 USB 虚拟串口混为一谈，结果是线缆、电平、拓扑和软件协议同时排错，越查越乱。&lt;/p&gt;
&lt;p&gt;本文先建立分层模型。读完后，你应该能回答：设备两端的电气接口是什么、字节怎样传输、谁定义一帧消息，以及上位机究竟在操作哪一层。&lt;/p&gt;
&lt;h2 id="1-先把四个名字放回各自层次"&gt;&lt;a href="#1-%e5%85%88%e6%8a%8a%e5%9b%9b%e4%b8%aa%e5%90%8d%e5%ad%97%e6%94%be%e5%9b%9e%e5%90%84%e8%87%aa%e5%b1%82%e6%ac%a1" class="header-anchor"&gt;&lt;/a&gt;1. 先把四个名字放回各自层次
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;名称&lt;/th&gt;
 &lt;th&gt;主要解决的问题&lt;/th&gt;
 &lt;th&gt;典型位置&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;UART&lt;/td&gt;
 &lt;td&gt;字节如何串行化，波特率、数据位、校验位、停止位如何组织&lt;/td&gt;
 &lt;td&gt;MCU 外设或串口控制器&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;RS-232&lt;/td&gt;
 &lt;td&gt;单端电气信号、连接器与点对点连接&lt;/td&gt;
 &lt;td&gt;仪器、调试口、旧式 PC 串口&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;RS-485&lt;/td&gt;
 &lt;td&gt;差分电气信号与多节点总线能力&lt;/td&gt;
 &lt;td&gt;工业现场总线、远距离设备链路&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;USB CDC ACM / 厂商驱动&lt;/td&gt;
 &lt;td&gt;USB 设备如何向主机呈现串口式接口&lt;/td&gt;
 &lt;td&gt;USB 转串口芯片、设备 USB 口&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;UART 不是线缆标准，RS-485 也不是完整应用协议。它们约定字符传输或电气信号层面的机制；命令字、长度、CRC、地址和应答规则仍由 Modbus RTU 或厂商协议定义——上层协议的具体形态见本系列《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/device-communication/07-modbus/" &gt;设备通信与协议编程（七）：Modbus RTU/TCP&lt;/a&gt;》，隔离厂商差异的分层做法见《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/equipment-architecture/02-hal-adapters/" &gt;设备软件架构与控制模型（二）：硬件抽象层与设备适配器&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="2-uart-帧不是应用协议帧"&gt;&lt;a href="#2-uart-%e5%b8%a7%e4%b8%8d%e6%98%af%e5%ba%94%e7%94%a8%e5%8d%8f%e8%ae%ae%e5%b8%a7" class="header-anchor"&gt;&lt;/a&gt;2. UART 帧不是应用协议帧
&lt;/h2&gt;&lt;p&gt;常见的 &lt;code&gt;115200 8N1&lt;/code&gt; 表示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;名义波特率为 115200；&lt;/li&gt;
&lt;li&gt;每个字符含 8 个数据位；&lt;/li&gt;
&lt;li&gt;无校验位（None）；&lt;/li&gt;
&lt;li&gt;1 个停止位。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若再计入 1 个起始位，一个 8N1 字符通常在线路上占 10 bit。因此 115200 bit/s 的理论上限约为 11520 byte/s，尚未扣除应用协议头、校验、应答间隔和系统调度延迟。波特率不等于有效载荷吞吐率。&lt;/p&gt;
&lt;p&gt;“UART 帧”描述一个字符的起止与校验；“应用协议帧”可能由几十个 UART 字符组成。上位机解析消息边界时，不能把一次驱动回调或一次 &lt;code&gt;Read&lt;/code&gt; 当成一帧。&lt;/p&gt;
&lt;h2 id="3-rs-232点对点单端全双工"&gt;&lt;a href="#3-rs-232%e7%82%b9%e5%af%b9%e7%82%b9%e5%8d%95%e7%ab%af%e5%85%a8%e5%8f%8c%e5%b7%a5" class="header-anchor"&gt;&lt;/a&gt;3. RS-232：点对点、单端、全双工
&lt;/h2&gt;&lt;p&gt;RS-232 通常以发送、接收和信号地构成点对点链路。它使用相对于信号地的单端电压，不能把 MCU 的 3.3 V TTL UART 引脚直接接到 RS-232 接口；中间需要匹配的电平转换器。&lt;/p&gt;
&lt;p&gt;常见误区有三个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DB9 只是常见连接器，针脚定义仍要查设备手册；不能只看插头形状。&lt;/li&gt;
&lt;li&gt;“能收到乱码”往往说明路径已部分连通，应继续核对波特率、数据位、校验和地线。&lt;/li&gt;
&lt;li&gt;RTS/CTS、DTR/DSR 等握手线是否使用取决于双方设计；软件打开硬件流控前要确认实际接线。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="4-rs-485差分总线不是自动多机协议"&gt;&lt;a href="#4-rs-485%e5%b7%ae%e5%88%86%e6%80%bb%e7%ba%bf%e4%b8%8d%e6%98%af%e8%87%aa%e5%8a%a8%e5%a4%9a%e6%9c%ba%e5%8d%8f%e8%ae%ae" class="header-anchor"&gt;&lt;/a&gt;4. RS-485：差分总线不是自动多机协议
&lt;/h2&gt;&lt;p&gt;RS-485 通过两根线之间的差分电压传输信号，抗共模干扰能力更适合工业环境。常见的两线制链路是半双工：同一时刻只有一个方向驱动总线；四线制可实现全双工，但设备和布线都必须支持。&lt;/p&gt;
&lt;p&gt;多节点能力只说明多个收发器可以挂在总线上，不会自动解决以下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点地址如何分配；&lt;/li&gt;
&lt;li&gt;谁可以发言，如何避免冲突；&lt;/li&gt;
&lt;li&gt;应答超时与重试规则；&lt;/li&gt;
&lt;li&gt;帧边界、端序和校验算法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都属于更高层协议。工程上还要按收发器、线缆长度和速率设计终端匹配、偏置与接地，不能机械地在每个节点都并联终端电阻。&lt;/p&gt;
&lt;h2 id="5-usb-虚拟串口软件像串口链路不是-uart"&gt;&lt;a href="#5-usb-%e8%99%9a%e6%8b%9f%e4%b8%b2%e5%8f%a3%e8%bd%af%e4%bb%b6%e5%83%8f%e4%b8%b2%e5%8f%a3%e9%93%be%e8%b7%af%e4%b8%8d%e6%98%af-uart" class="header-anchor"&gt;&lt;/a&gt;5. USB 虚拟串口：软件像串口，链路不是 UART
&lt;/h2&gt;&lt;p&gt;USB CDC ACM 或 USB 转串口驱动可以在操作系统中暴露 &lt;code&gt;COMx&lt;/code&gt;、&lt;code&gt;/dev/ttyACM*&lt;/code&gt; 或 &lt;code&gt;/dev/ttyUSB*&lt;/code&gt;。应用仍能使用串口 API，但底层数据经 USB 事务和驱动缓冲传输。&lt;/p&gt;
&lt;p&gt;因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“波特率”可能被设备真实采用，也可能只是驱动传给设备的配置提示；&lt;/li&gt;
&lt;li&gt;拔插会使设备节点消失并重新枚举，端口名不一定稳定；&lt;/li&gt;
&lt;li&gt;USB 包边界、串口 &lt;code&gt;Read&lt;/code&gt; 返回边界和应用消息边界互不等价；&lt;/li&gt;
&lt;li&gt;VID/PID 只能识别产品类型，多个同型号设备还需序列号或物理端口路径区分。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="6-排错时沿层次向上走"&gt;&lt;a href="#6-%e6%8e%92%e9%94%99%e6%97%b6%e6%b2%bf%e5%b1%82%e6%ac%a1%e5%90%91%e4%b8%8a%e8%b5%b0" class="header-anchor"&gt;&lt;/a&gt;6. 排错时沿层次向上走
&lt;/h2&gt;&lt;p&gt;推荐按下面顺序定位：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;物理与电气层&lt;/strong&gt;：接口类型、电平、A/B 极性、地线、终端和供电是否正确。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统与驱动层&lt;/strong&gt;：设备是否枚举、驱动是否加载、端口是否被其他进程占用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字符层&lt;/strong&gt;：波特率、数据位、校验位、停止位、流控是否一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议层&lt;/strong&gt;：地址、命令、长度、端序、转义、校验和超时是否匹配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务层&lt;/strong&gt;：设备当前状态是否允许该命令，参数是否越界。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;先用逻辑分析仪、示波器或串口分析工具确认线上确实有字节，再讨论 JSON、CRC 或业务状态，能显著缩短排错路径。&lt;/p&gt;
&lt;h2 id="7-选型边界"&gt;&lt;a href="#7-%e9%80%89%e5%9e%8b%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;7. 选型边界
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;场景&lt;/th&gt;
 &lt;th&gt;更自然的选择&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;板内 MCU 与外设&lt;/td&gt;
 &lt;td&gt;UART、SPI 或 I²C，取决于距离、速率和布线&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;单台仪器、短距离维护口&lt;/td&gt;
 &lt;td&gt;RS-232 或 USB 虚拟串口&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;多节点、较长距离、强干扰环境&lt;/td&gt;
 &lt;td&gt;RS-485 加明确的上层协议&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;高带宽图像、持续大数据&lt;/td&gt;
 &lt;td&gt;USB、GigE Vision 等，不应强行套串口&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;多轴硬实时同步&lt;/td&gt;
 &lt;td&gt;CANopen、EtherCAT 等工业网络，而非普通串口轮询&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选择接口时不要只比较标称速率，还要评估确定性、隔离、连接器可靠性、驱动维护、诊断能力和设备生命周期。&lt;/p&gt;
&lt;h2 id="8-小结"&gt;&lt;a href="#8-%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;8. 小结
&lt;/h2&gt;&lt;p&gt;UART 定义字符传输，RS-232/RS-485 主要定义电气接口，USB 虚拟串口提供操作系统抽象，真正的消息语义属于应用协议。把这几层分开，是后续编写串口接收循环、二进制帧解析器和故障恢复逻辑的前提。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/api/system.io.ports.serialport" target="_blank" rel="noopener"
 &gt;Microsoft Learn：System.IO.Ports.SerialPort&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.usb.org/document-library/class-definitions-communication-devices-12" target="_blank" rel="noopener"
 &gt;USB-IF：CDC 规范与模型文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.modbus.org/modbus-specifications" target="_blank" rel="noopener"
 &gt;Modbus Organization：Modbus Specifications&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>C# 原生互操作与设备 SDK（八）：C++ ABI 与跨平台封装</title><link>https://www.jiwei.space/posts/equipment/native-interop/08-cpp-abi-cross-platform/</link><pubDate>Sat, 25 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/native-interop/08-cpp-abi-cross-platform/</guid><description>&lt;p&gt;很多设备 SDK 的核心由 C++ 编写，却不适合把类、异常、&lt;code&gt;std::string&lt;/code&gt; 和 STL 容器直接暴露给 C#。C++ 没有覆盖 MSVC、GCC、Clang 和所有 .NET 支持平台的统一 ABI；编译器版本、运行库和构建选项变化，也可能改变名称修饰与对象布局。&lt;/p&gt;
&lt;p&gt;更稳定的边界是保留 C++ 实现，在外面导出一层窄 C ABI。本文给出完整设计原则，并说明怎样让同一托管适配器连接 Windows &lt;code&gt;.dll&lt;/code&gt;、Linux &lt;code&gt;.so&lt;/code&gt; 和 macOS &lt;code&gt;.dylib&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="1-为什么不直接导出-c-类"&gt;&lt;a href="#1-%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%8d%e7%9b%b4%e6%8e%a5%e5%af%bc%e5%87%ba-c-%e7%b1%bb" class="header-anchor"&gt;&lt;/a&gt;1. 为什么不直接导出 C++ 类
&lt;/h2&gt;&lt;p&gt;下面的接口对同一工具链内的 C++ 调用者很自然：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Camera&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;Camera&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;跨 ABI 时却产生一串隐含约定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类名和方法名如何修饰；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this&lt;/code&gt; 怎样传递、虚表怎样布局；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::string&lt;/code&gt;/&lt;code&gt;std::vector&lt;/code&gt; 使用哪个标准库实现；&lt;/li&gt;
&lt;li&gt;对象由哪个运行库分配和释放；&lt;/li&gt;
&lt;li&gt;C++ 异常怎样传播；&lt;/li&gt;
&lt;li&gt;编译器、调试/发布运行库和迭代器调试选项是否一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这些内部细节复制到 P/Invoke 签名不是稳定集成。Microsoft 的 .NET ABI 指南也建议通过 &lt;code&gt;extern &amp;quot;C&amp;quot;&lt;/code&gt; 导出 C 函数来连接 C++。&lt;/p&gt;
&lt;h2 id="2-用不透明句柄隐藏-c-对象"&gt;&lt;a href="#2-%e7%94%a8%e4%b8%8d%e9%80%8f%e6%98%8e%e5%8f%a5%e6%9f%84%e9%9a%90%e8%97%8f-c-%e5%af%b9%e8%b1%a1" class="header-anchor"&gt;&lt;/a&gt;2. 用不透明句柄隐藏 C++ 对象
&lt;/h2&gt;&lt;p&gt;公共头文件只暴露 C 能表达的类型：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#pragma once
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stddef.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdint.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#ifdef __cplusplus
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define DEVICE_EXTERN extern &amp;#34;C&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define DEVICE_EXTERN
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#ifdef _WIN32
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 按 SDK 构建方视角书写；完整的 SDK 头文件通常再用构建宏在 dllexport 与 dllimport 间切换。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define DEVICE_EXPORT DEVICE_EXTERN __declspec(dllexport)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define DEVICE_EXPORT DEVICE_EXTERN __attribute__((visibility(&amp;#34;default&amp;#34;)))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device_context&lt;/span&gt; &lt;span class="n"&gt;device_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device_open_options&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;struct_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;api_version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;device_index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;device_open_options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DEVICE_EXPORT&lt;/span&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;device_open_options&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device_context&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;out_context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DEVICE_EXPORT&lt;/span&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device_context&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;recipe_utf8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;recipe_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_required&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DEVICE_EXPORT&lt;/span&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_context&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;device_context&lt;/code&gt; 的定义只存在于 C++ 实现文件中。C# 看到的只是一个不透明句柄，再用 &lt;code&gt;SafeHandle&lt;/code&gt; 管理。这样可以修改内部类、容器和继承关系，而不改变公开布局。&lt;/p&gt;
&lt;h2 id="3-异常不能穿过-c-边界"&gt;&lt;a href="#3-%e5%bc%82%e5%b8%b8%e4%b8%8d%e8%83%bd%e7%a9%bf%e8%bf%87-c-%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;3. 异常不能穿过 C 边界
&lt;/h2&gt;&lt;p&gt;C++ 包装函数要在边界内捕获异常并转换为稳定错误码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;#34;device_api.h&amp;#34;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;#34;camera.hpp&amp;#34;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;new&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;device_context&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;explicit&lt;/span&gt; &lt;span class="nf"&gt;device_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Camera&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;device_open_options&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device_context&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;out_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;nullptr&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;out_context&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;struct_size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_open_options&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;device_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device_index&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;bad_alloc&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_context&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不要让 C++ 异常越过 C 函数进入 .NET。若需要错误文本，可提供“调用者缓冲区 + 长度”的错误查询函数，并定义错误信息是按线程、按 context 还是按最近调用保存，避免全局字符串在并发下相互覆盖。&lt;/p&gt;
&lt;p&gt;析构函数原则上也不应抛异常；包装层的 &lt;code&gt;catch (...)&lt;/code&gt; 是边界兜底，不是用来掩盖不可恢复的资源损坏。&lt;/p&gt;
&lt;h2 id="4-abi-从第一版就为演进留位置"&gt;&lt;a href="#4-abi-%e4%bb%8e%e7%ac%ac%e4%b8%80%e7%89%88%e5%b0%b1%e4%b8%ba%e6%bc%94%e8%bf%9b%e7%95%99%e4%bd%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;4. ABI 从第一版就为演进留位置
&lt;/h2&gt;&lt;p&gt;公开结构加入 &lt;code&gt;struct_size&lt;/code&gt; 与 &lt;code&gt;api_version&lt;/code&gt;，可以让新库识别旧调用者实际提供了多少字段。常见兼容策略是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只在结构尾部追加字段；&lt;/li&gt;
&lt;li&gt;保留并清零 &lt;code&gt;reserved&lt;/code&gt; 字段；&lt;/li&gt;
&lt;li&gt;不改变已发布字段的类型、偏移和含义；&lt;/li&gt;
&lt;li&gt;新能力用新函数或能力查询暴露；&lt;/li&gt;
&lt;li&gt;不复用已经发布的错误码和枚举值；&lt;/li&gt;
&lt;li&gt;提供 &lt;code&gt;device_get_api_version&lt;/code&gt; 与运行时版本信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“DLL 文件名没变”不代表 ABI 兼容。应保存公共头文件基线，并在 CI 中对导出符号、结构大小和兼容样例做回归。&lt;/p&gt;
&lt;h2 id="5-内存始终回到分配它的模块"&gt;&lt;a href="#5-%e5%86%85%e5%ad%98%e5%a7%8b%e7%bb%88%e5%9b%9e%e5%88%b0%e5%88%86%e9%85%8d%e5%ae%83%e7%9a%84%e6%a8%a1%e5%9d%97" class="header-anchor"&gt;&lt;/a&gt;5. 内存始终回到分配它的模块
&lt;/h2&gt;&lt;p&gt;优先让调用者提供输出缓冲区。必须返回动态内存时，C API 应成对提供分配与释放：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DEVICE_EXPORT&lt;/span&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_create_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device_context&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;out_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DEVICE_EXPORT&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;device_free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;C# 复制数据后调用 &lt;code&gt;device_free&lt;/code&gt;。不要要求调用者猜测内部使用 &lt;code&gt;new[]&lt;/code&gt;、&lt;code&gt;malloc&lt;/code&gt;、COM 任务分配器还是特定 CRT 堆。&lt;/p&gt;
&lt;p&gt;同理，字符串使用 UTF-8 字节加显式长度可以避开 &lt;code&gt;wchar_t&lt;/code&gt; 在平台间宽度不同的问题。若必须以零结尾，要写清长度是否包含终止符。&lt;/p&gt;
&lt;h2 id="6-托管声明保持同一逻辑库名"&gt;&lt;a href="#6-%e6%89%98%e7%ae%a1%e5%a3%b0%e6%98%8e%e4%bf%9d%e6%8c%81%e5%90%8c%e4%b8%80%e9%80%bb%e8%be%91%e5%ba%93%e5%90%8d" class="header-anchor"&gt;&lt;/a&gt;6. 托管声明保持同一逻辑库名
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LibraryName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;device_bridge&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// DeviceOpenOptionsNative 遵循本系列（三）的 *Native 布局约定（Sequential + struct_size）。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(LibraryName, EntryPoint = &amp;#34;device_open&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DeviceOpenOptionsNative&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;SafeDeviceHandle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(LibraryName, EntryPoint = &amp;#34;device_close&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CloseRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;构建系统分别产出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;runtimes/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── win-x64/native/device_bridge.dll
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── linux-x64/native/libdevice_bridge.so
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── osx-arm64/native/libdevice_bridge.dylib
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;NuGet/RID 资产或自定义 &lt;code&gt;DllImportResolver&lt;/code&gt; 负责把逻辑名映射到当前平台文件。每个产物仍需在对应 OS、架构和运行库环境中测试，不能因为 C API 相同就只测试 Windows。&lt;/p&gt;
&lt;h2 id="7-c-包装层不等于最低公分母"&gt;&lt;a href="#7-c-%e5%8c%85%e8%a3%85%e5%b1%82%e4%b8%8d%e7%ad%89%e4%ba%8e%e6%9c%80%e4%bd%8e%e5%85%ac%e5%88%86%e6%af%8d" class="header-anchor"&gt;&lt;/a&gt;7. C 包装层不等于最低公分母
&lt;/h2&gt;&lt;p&gt;稳定 C ABI 可以继续表达现代能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不透明句柄表达对象；&lt;/li&gt;
&lt;li&gt;结构 + &lt;code&gt;struct_size&lt;/code&gt; 表达可演进参数；&lt;/li&gt;
&lt;li&gt;函数指针 + context 表达事件；&lt;/li&gt;
&lt;li&gt;调用者缓冲区表达高吞吐数据；&lt;/li&gt;
&lt;li&gt;明确错误码和查询函数表达诊断；&lt;/li&gt;
&lt;li&gt;能力位或查询函数表达可选特性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;边界应该窄，但不应把所有错误压成一个 &lt;code&gt;false&lt;/code&gt;，也不应让上层通过几十次 getter 拼出一份本可原子返回的状态快照。&lt;/p&gt;
&lt;h2 id="8-何时考虑其他桥接方式"&gt;&lt;a href="#8-%e4%bd%95%e6%97%b6%e8%80%83%e8%99%91%e5%85%b6%e4%bb%96%e6%a1%a5%e6%8e%a5%e6%96%b9%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;8. 何时考虑其他桥接方式
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;C++/CLI：适合 Windows 内部已有大量 C++ 类且团队能维护 &lt;code&gt;.vcxproj&lt;/code&gt; 的场景；现代 .NET 的 C++/CLI 支持仅限 Windows，不能作为跨平台桥。&lt;/li&gt;
&lt;li&gt;COM：适合已有稳定 COM 契约、注册与版本治理体系的 Windows 组件。&lt;/li&gt;
&lt;li&gt;独立进程/RPC：适合 SDK 崩溃隔离、位数隔离、许可隔离或跨机器部署，但引入序列化、进程管理和故障恢复成本。&lt;/li&gt;
&lt;li&gt;C ABI：通常是跨平台、跨语言、部署在同一进程中的默认选择。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选择不是只看调用方便程度，还要评估故障是否会拖垮主进程、厂商库能否重入、升级是否需要独立回滚，以及许可是否允许重新封装。&lt;/p&gt;
&lt;h2 id="9-发布前验证矩阵"&gt;&lt;a href="#9-%e5%8f%91%e5%b8%83%e5%89%8d%e9%aa%8c%e8%af%81%e7%9f%a9%e9%98%b5" class="header-anchor"&gt;&lt;/a&gt;9. 发布前验证矩阵
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;维度&lt;/th&gt;
 &lt;th&gt;最低验证&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;编译器&lt;/td&gt;
 &lt;td&gt;每个受支持工具链构建公开头文件与桥接库&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;OS/架构&lt;/td&gt;
 &lt;td&gt;每个声明支持的 RID 加载并运行冒烟测试&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ABI&lt;/td&gt;
 &lt;td&gt;导出名、调用约定、结构大小和偏移断言&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;版本&lt;/td&gt;
 &lt;td&gt;旧托管适配器连接新库，新适配器识别旧库&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;错误&lt;/td&gt;
 &lt;td&gt;空指针、缓冲区不足、设备断连、异常转换&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;资源&lt;/td&gt;
 &lt;td&gt;重复打开关闭、失败注入、句柄和内存泄漏&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;并发&lt;/td&gt;
 &lt;td&gt;多线程调用、回调与关闭竞争、重入限制&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;跨平台不是“能编译三份文件”，而是每个承诺的平台都拥有可重复的构建、打包和测试证据。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;C++ 实现可以保持复杂，公开 ABI 应保持简单、明确、可演进。用 &lt;code&gt;extern &amp;quot;C&amp;quot;&lt;/code&gt;、不透明句柄、定宽类型、显式长度、配对释放和错误码建立稳定边界，再由 &lt;code&gt;LibraryImport&lt;/code&gt;、&lt;code&gt;SafeHandle&lt;/code&gt; 和托管适配器恢复面向对象语义，是设备 SDK 长期维护成本较低的组合。&lt;/p&gt;
&lt;p&gt;至此，本系列从 P/Invoke 入口一路走到类型、布局、内存、加载、回调、句柄和跨平台桥接。下一阶段可以在这层可靠边界之上展开串口、TCP、Modbus、CAN 与工业协议编程。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/abi-support" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native interoperability ABI support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/best-practices" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native interoperability best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/porting/cpp-cli" target="_blank" rel="noopener"
 &gt;Microsoft Learn：How to port a C++/CLI project to .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/nuget/create-packages/native-files-in-net-packages" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native files in .NET packages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>C# 原生互操作与设备 SDK（七）：SafeHandle 与资源生命周期</title><link>https://www.jiwei.space/posts/equipment/native-interop/07-safehandle-lifetime/</link><pubDate>Fri, 24 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/native-interop/07-safehandle-lifetime/</guid><description>&lt;p&gt;把设备句柄保存为 &lt;code&gt;nint&lt;/code&gt; 很方便，也把所有风险留给调用者：忘记关闭会泄漏资源，并发关闭可能让调用使用失效句柄，异常路径又容易跳过清理。手写终结器并不能可靠解决这些问题。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SafeHandle&lt;/code&gt; 把非托管句柄的所有权、有效值和释放动作封装成受运行时支持的资源类型。本文说明怎样为设备 SDK 正确派生它，以及它能保证什么、不能保证什么。&lt;/p&gt;
&lt;h2 id="1-先确认句柄契约"&gt;&lt;a href="#1-%e5%85%88%e7%a1%ae%e8%ae%a4%e5%8f%a5%e6%9f%84%e5%a5%91%e7%ba%a6" class="header-anchor"&gt;&lt;/a&gt;1. 先确认句柄契约
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;device_handle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_handle&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_get_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_handle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_mm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_handle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;本篇假设 &lt;code&gt;device_close&lt;/code&gt; 返回状态码（0 表示成功）；第一篇的最小示例曾把它简化为 &lt;code&gt;void&lt;/code&gt;，真实 SDK 一律以头文件为准。&lt;/p&gt;
&lt;p&gt;接入前必须确认：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无效句柄是空指针、&lt;code&gt;-1&lt;/code&gt;，还是另一个哨兵；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;device_close&lt;/code&gt; 能否重复调用；&lt;/li&gt;
&lt;li&gt;关闭是否会阻塞，是否要求特定线程；&lt;/li&gt;
&lt;li&gt;关闭前是否必须停止采集、注销回调或等待任务；&lt;/li&gt;
&lt;li&gt;句柄能否被多个线程并发调用；&lt;/li&gt;
&lt;li&gt;子资源是否必须早于父句柄释放。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;SafeHandle&lt;/code&gt; 只负责句柄释放，不会自动推导这些会话规则。&lt;/p&gt;
&lt;h2 id="2-为每种释放函数定义独立类型"&gt;&lt;a href="#2-%e4%b8%ba%e6%af%8f%e7%a7%8d%e9%87%8a%e6%94%be%e5%87%bd%e6%95%b0%e5%ae%9a%e4%b9%89%e7%8b%ac%e7%ab%8b%e7%b1%bb%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;2. 为每种释放函数定义独立类型
&lt;/h2&gt;&lt;p&gt;若空指针和 &lt;code&gt;-1&lt;/code&gt; 都表示无效，可继承 &lt;code&gt;SafeHandleZeroOrMinusOneIsInvalid&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Win32.SafeHandles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SafeDeviceHandle&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SafeHandleZeroOrMinusOneIsInvalid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// LibraryImport 通过 out/返回值创建 SafeHandle 时需要 public 无参构造函数。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;SafeDeviceHandle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ownsHandle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;override&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;ReleaseHandle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CloseRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_close&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CloseRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;若只有零无效，应直接继承 &lt;code&gt;SafeHandle&lt;/code&gt; 并实现准确的 &lt;code&gt;IsInvalid&lt;/code&gt;。文件句柄、内存映射句柄和设备会话可能分别要求 &lt;code&gt;CloseHandle&lt;/code&gt;、&lt;code&gt;UnmapViewOfFile&lt;/code&gt;、&lt;code&gt;device_close&lt;/code&gt;，不能为了复用而共用一个“万能句柄类”。&lt;/p&gt;
&lt;h2 id="3-让-pinvoke-直接产生-safehandle"&gt;&lt;a href="#3-%e8%ae%a9-pinvoke-%e7%9b%b4%e6%8e%a5%e4%ba%a7%e7%94%9f-safehandle" class="header-anchor"&gt;&lt;/a&gt;3. 让 P/Invoke 直接产生 SafeHandle
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_open&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;SafeDeviceHandle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_get_position&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;GetPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;SafeDeviceHandle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;millimeters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;在 .NET 8 及以上，源生成互操作通过返回值、&lt;code&gt;ref&lt;/code&gt; 或 &lt;code&gt;out&lt;/code&gt; 创建 &lt;code&gt;SafeHandle&lt;/code&gt; 派生类型时，该类型必须具有 &lt;code&gt;public&lt;/code&gt; 无参构造函数。&lt;/p&gt;
&lt;p&gt;封装打开过程时，失败路径也要释放 SDK 可能写出的有效句柄：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceSessionFactory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;DeviceSession&lt;/span&gt; &lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;SafeDeviceHandle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceSdkException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;device_open&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 源生成封送下 handle 不会为 null；null 检查是防御式写法。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsInvalid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;SDK returned an invalid handle&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="4-safehandle-保护单次原生调用"&gt;&lt;a href="#4-safehandle-%e4%bf%9d%e6%8a%a4%e5%8d%95%e6%ac%a1%e5%8e%9f%e7%94%9f%e8%b0%83%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;4. SafeHandle 保护单次原生调用
&lt;/h2&gt;&lt;p&gt;当 P/Invoke 参数直接声明为 &lt;code&gt;SafeDeviceHandle&lt;/code&gt; 时，运行时会在调用期间保持安全句柄存活，避免另一个线程的 &lt;code&gt;Dispose&lt;/code&gt; 在原生调用尚未返回时立即释放底层句柄。这比先调用 &lt;code&gt;DangerousGetHandle()&lt;/code&gt; 再传 &lt;code&gt;nint&lt;/code&gt; 更可靠。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SafeDeviceHandle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SafeDeviceHandle&lt;/span&gt; &lt;span class="n"&gt;_handle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;GetPosition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;millimeters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceSdkException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;device_get_position&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;millimeters&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这项保证只覆盖 P/Invoke 调用期间。它不代表设备允许并发命令，也不阻止业务层在“准备动作”和“真正调用”之间发生关闭。会话仍需用状态机、命令队列或锁定义并发语义。&lt;/p&gt;
&lt;h2 id="5-releasehandle-必须短小且不抛异常"&gt;&lt;a href="#5-releasehandle-%e5%bf%85%e9%a1%bb%e7%9f%ad%e5%b0%8f%e4%b8%94%e4%b8%8d%e6%8a%9b%e5%bc%82%e5%b8%b8" class="header-anchor"&gt;&lt;/a&gt;5. ReleaseHandle 必须短小且不抛异常
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;ReleaseHandle&lt;/code&gt; 可能从显式 &lt;code&gt;Dispose&lt;/code&gt; 或运行时清理路径进入。实现应：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只调用可靠的原生释放函数；&lt;/li&gt;
&lt;li&gt;不抛异常；&lt;/li&gt;
&lt;li&gt;不依赖普通业务对象仍然存活；&lt;/li&gt;
&lt;li&gt;不做 UI、网络、复杂日志或长时间等待；&lt;/li&gt;
&lt;li&gt;以返回值报告是否成功，但不能指望调用者在终结阶段恢复。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果关闭设备必须先执行异步停止、保存或注销回调，应在上层 &lt;code&gt;DisposeAsync&lt;/code&gt;/显式 &lt;code&gt;StopAsync&lt;/code&gt; 中完成受控关闭，最后再释放 &lt;code&gt;SafeHandle&lt;/code&gt;。&lt;code&gt;ReleaseHandle&lt;/code&gt; 只作为底层资源兜底，不能承担完整停机流程。&lt;/p&gt;
&lt;h2 id="6-danger-前缀的方法确实危险"&gt;&lt;a href="#6-danger-%e5%89%8d%e7%bc%80%e7%9a%84%e6%96%b9%e6%b3%95%e7%a1%ae%e5%ae%9e%e5%8d%b1%e9%99%a9" class="header-anchor"&gt;&lt;/a&gt;6. Danger 前缀的方法确实危险
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;DangerousGetHandle&lt;/code&gt; 暴露原始值，却不自动延长句柄寿命。只有第三方 API 无法直接接受 &lt;code&gt;SafeHandle&lt;/code&gt; 时，才考虑配合 &lt;code&gt;DangerousAddRef&lt;/code&gt;/&lt;code&gt;DangerousRelease&lt;/code&gt; 建立严格的 &lt;code&gt;try/finally&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;addedRef&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DangerousAddRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;addedRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DangerousGetHandle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;LegacyCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;finally&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addedRef&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DangerousRelease&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;优先修改 P/Invoke 声明直接接收 &lt;code&gt;SafeHandle&lt;/code&gt;。手工引用计数代码越多，越容易在异常和并发路径上失衡。&lt;/p&gt;
&lt;h2 id="7-句柄所有权不能重复"&gt;&lt;a href="#7-%e5%8f%a5%e6%9f%84%e6%89%80%e6%9c%89%e6%9d%83%e4%b8%8d%e8%83%bd%e9%87%8d%e5%a4%8d" class="header-anchor"&gt;&lt;/a&gt;7. 句柄所有权不能重复
&lt;/h2&gt;&lt;p&gt;同一个原始句柄不能由两个 &lt;code&gt;ownsHandle: true&lt;/code&gt; 的对象分别拥有，否则会重复关闭。包装已有句柄时要明确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有权转移：新 &lt;code&gt;SafeHandle&lt;/code&gt; 负责释放，旧所有者不再关闭；&lt;/li&gt;
&lt;li&gt;借用：包装对象不拥有句柄，但必须保证真实所有者覆盖全部使用期；&lt;/li&gt;
&lt;li&gt;共享：需要 SDK 明确支持的引用计数或复制句柄机制，不能自行假设。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若原生 API 接管了句柄所有权，可在成功后调用 &lt;code&gt;SetHandleAsInvalid()&lt;/code&gt;，防止托管侧再次释放；只有文档明确说明“成功后接管”时才能这样做。&lt;/p&gt;
&lt;h2 id="8-关闭顺序与回调协同"&gt;&lt;a href="#8-%e5%85%b3%e9%97%ad%e9%a1%ba%e5%ba%8f%e4%b8%8e%e5%9b%9e%e8%b0%83%e5%8d%8f%e5%90%8c" class="header-anchor"&gt;&lt;/a&gt;8. 关闭顺序与回调协同
&lt;/h2&gt;&lt;p&gt;典型设备会话的关闭顺序是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;阻止新命令进入；&lt;/li&gt;
&lt;li&gt;请求采集/运动停止并确认物理状态；&lt;/li&gt;
&lt;li&gt;注销回调并等待在途回调退出；&lt;/li&gt;
&lt;li&gt;释放子资源；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dispose&lt;/code&gt; 主设备 &lt;code&gt;SafeHandle&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;发布已关闭状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;顺序必须以 SDK 契约为准。若先关闭主句柄再注销回调，原生线程可能访问已释放设备；若先释放回调 context，而注销仍有在途调用，则会形成 use-after-free。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;SafeHandle&lt;/code&gt; 是原生句柄的默认托管表示：它集中定义无效值和释放函数，并保护单次 P/Invoke 调用期间的句柄寿命。但它不是设备状态机，也不会自动处理异步停机、回调屏障和线程安全。把底层释放交给 &lt;code&gt;SafeHandle&lt;/code&gt;，把完整关闭协议留在会话层。&lt;/p&gt;
&lt;p&gt;下一篇把系列收束到 SDK 设计端：如何用稳定 C ABI 桥接 C++ 实现，并同时交付 Windows DLL 与 Linux &lt;code&gt;.so&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/fundamentals/runtime-libraries/system-runtime-interopservices-safehandle" target="_blank" rel="noopener"
 &gt;Microsoft Learn：SafeHandle class&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/compatibility/interop/8.0/safehandle-constructor" target="_blank" rel="noopener"
 &gt;Microsoft Learn：SafeHandle types must have public constructor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/garbage-collection/unmanaged" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Cleaning up unmanaged resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/garbage-collection/implementing-dispose" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Implement a Dispose method&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>C# 原生互操作与设备 SDK（六）：回调、原生线程与事件</title><link>https://www.jiwei.space/posts/equipment/native-interop/06-callbacks-native-threads/</link><pubDate>Thu, 23 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/native-interop/06-callbacks-native-threads/</guid><description>&lt;p&gt;设备 SDK 常通过回调上报图像到达、运动完成、报警和连接变化。注册成功只完成了第一步：原生库可能在任意线程、任意时刻调用函数指针，并继续使用注册时传入的上下文。委托被 GC 回收、设备句柄已关闭、缓冲区已失效或异常穿过 ABI 边界，都可能导致进程级崩溃。&lt;/p&gt;
&lt;p&gt;本文建立回调的完整生命周期，并用非托管函数指针（C# 9/.NET 5 起可用）在 .NET 10 环境下演示一条可验证的实现路径。&lt;/p&gt;
&lt;h2 id="1-先读清原生回调契约"&gt;&lt;a href="#1-%e5%85%88%e8%af%bb%e6%b8%85%e5%8e%9f%e7%94%9f%e5%9b%9e%e8%b0%83%e5%a5%91%e7%ba%a6" class="header-anchor"&gt;&lt;/a&gt;1. 先读清原生回调契约
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stddef.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdint.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="nf"&gt;void&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;device_event_callback&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;event_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;payload_length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_register_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device_handle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device_event_callback&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_unregister_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_handle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;头文件之外还要确认：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;回调使用哪种调用约定；&lt;/li&gt;
&lt;li&gt;SDK 是同步回调还是由内部线程异步回调；&lt;/li&gt;
&lt;li&gt;是否可能并发、重入或在注销期间继续进入；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;payload&lt;/code&gt; 只在本次回调有效，还是可由调用者释放；&lt;/li&gt;
&lt;li&gt;注销返回时是否保证所有在途回调结束；&lt;/li&gt;
&lt;li&gt;关闭设备句柄前必须执行什么顺序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2-使用函数指针表达精确-abi"&gt;&lt;a href="#2-%e4%bd%bf%e7%94%a8%e5%87%bd%e6%95%b0%e6%8c%87%e9%92%88%e8%a1%a8%e8%be%be%e7%b2%be%e7%a1%ae-abi" class="header-anchor"&gt;&lt;/a&gt;2. 使用函数指针表达精确 ABI
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.CompilerServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_register_callback&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;unsafe&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;RegisterCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;unmanaged&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Cdecl&lt;/span&gt;&lt;span class="p"&gt;]&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;*,&lt;/span&gt; &lt;span class="n"&gt;nuint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_unregister_callback&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;UnregisterCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;delegate* unmanaged[Cdecl]&amp;lt;...&amp;gt;&lt;/code&gt; 表达的是非托管函数指针，参数顺序与原生 typedef 一一对应。若 SDK 不是 &lt;code&gt;cdecl&lt;/code&gt;，两侧必须同时改为真实调用约定。&lt;/p&gt;
&lt;h2 id="3-用-unmanagedcallersonly-暴露静态入口"&gt;&lt;a href="#3-%e7%94%a8-unmanagedcallersonly-%e6%9a%b4%e9%9c%b2%e9%9d%99%e6%80%81%e5%85%a5%e5%8f%a3" class="header-anchor"&gt;&lt;/a&gt;3. 用 UnmanagedCallersOnly 暴露静态入口
&lt;/h2&gt;&lt;p&gt;实例方法带有隐含的 &lt;code&gt;this&lt;/code&gt;，不能直接作为普通 C 回调。可以暴露一个静态入口，把实例放在 &lt;code&gt;context&lt;/code&gt; 中：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;span class="lnt"&gt;58
&lt;/span&gt;&lt;span class="lnt"&gt;59
&lt;/span&gt;&lt;span class="lnt"&gt;60
&lt;/span&gt;&lt;span class="lnt"&gt;61
&lt;/span&gt;&lt;span class="lnt"&gt;62
&lt;/span&gt;&lt;span class="lnt"&gt;63
&lt;/span&gt;&lt;span class="lnt"&gt;64
&lt;/span&gt;&lt;span class="lnt"&gt;65
&lt;/span&gt;&lt;span class="lnt"&gt;66
&lt;/span&gt;&lt;span class="lnt"&gt;67
&lt;/span&gt;&lt;span class="lnt"&gt;68
&lt;/span&gt;&lt;span class="lnt"&gt;69
&lt;/span&gt;&lt;span class="lnt"&gt;70
&lt;/span&gt;&lt;span class="lnt"&gt;71
&lt;/span&gt;&lt;span class="lnt"&gt;72
&lt;/span&gt;&lt;span class="lnt"&gt;73
&lt;/span&gt;&lt;span class="lnt"&gt;74
&lt;/span&gt;&lt;span class="lnt"&gt;75
&lt;/span&gt;&lt;span class="lnt"&gt;76
&lt;/span&gt;&lt;span class="lnt"&gt;77
&lt;/span&gt;&lt;span class="lnt"&gt;78
&lt;/span&gt;&lt;span class="lnt"&gt;79
&lt;/span&gt;&lt;span class="lnt"&gt;80
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.CompilerServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading.Channels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceCallbackBridge&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [UnmanagedCallersOnly(CallConvs = new[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CallConvCdecl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;unsafe&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;OnEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;eventCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;payloadLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GCHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromIntPtr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Target&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;DeviceEventSink&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadLength&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxPayloadLength&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;payloadLength&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CallbackFailureLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidDataException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Invalid callback payload&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;payloadLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryPublish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;NativeDeviceEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CallbackFailureLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;NativeDeviceEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceEventSink&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NativeDeviceEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_events&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateBounded&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NativeDeviceEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DeviceEventSink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;maxPayloadLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxPayloadLength&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;maxPayloadLength&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxPayloadLength&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;MaxPayloadLength&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxPayloadLength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;MaxPayloadLength&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ChannelReader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NativeDeviceEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Events&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;TryPublish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NativeDeviceEvent&lt;/span&gt; &lt;span class="n"&gt;deviceEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deviceEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CallbackFailureLog&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 回调边界不能因诊断失败再次抛出。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里使用有界通道，队列已满时 &lt;code&gt;TryWrite&lt;/code&gt; 会失败；生产代码应记录丢弃计数或根据事件类型采用受控降载策略，不能静默假装事件已经处理。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UnmanagedCallersOnly&lt;/code&gt; 方法必须是静态方法，签名只能使用本系列（三）定义的 blittable 类型。异常绝不能穿过原生边界；回调入口要捕获全部异常，并把故障写入不再抛出的最小日志通道。&lt;/p&gt;
&lt;p&gt;示例立即复制 &lt;code&gt;payload&lt;/code&gt;，因为没有契约允许回调返回后继续读取该指针。最大长度由创建 &lt;code&gt;DeviceEventSink&lt;/code&gt; 时结合设备数据模型配置，避免损坏的 SDK 值触发超大分配，又不武断限制合法图像或波形大小。&lt;/p&gt;
&lt;h2 id="4-上下文句柄必须活到最后一次回调结束"&gt;&lt;a href="#4-%e4%b8%8a%e4%b8%8b%e6%96%87%e5%8f%a5%e6%9f%84%e5%bf%85%e9%a1%bb%e6%b4%bb%e5%88%b0%e6%9c%80%e5%90%8e%e4%b8%80%e6%ac%a1%e5%9b%9e%e8%b0%83%e7%bb%93%e6%9d%9f" class="header-anchor"&gt;&lt;/a&gt;4. 上下文句柄必须活到最后一次回调结束
&lt;/h2&gt;&lt;p&gt;注册时用普通 &lt;code&gt;GCHandle&lt;/code&gt; 保持托管对象可达。这里固定的是对象的生命周期，不是内存地址；默认 &lt;code&gt;GCHandleType.Normal&lt;/code&gt; 仍允许 GC 移动对象：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;unsafe&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CallbackRegistration&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;_device&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="n"&gt;GCHandle&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_registered&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CallbackRegistration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeviceEventSink&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_device&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GCHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;DeviceCallbackBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;GCHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToIntPtr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Free&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceSdkException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;device_register_callback&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_registered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;_registered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnregisterCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_device&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceSdkException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;device_unregister_callback&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 只有 SDK 保证注销返回后无在途回调，才能在这里释放。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Free&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_registered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果注销不等待在途回调，这个简化版本不安全。需要增加计数/屏障，或调用 SDK 提供的同步停止函数，按“停止产生新回调 → 等待已有回调退出 → 释放 context → 关闭设备”的顺序执行。&lt;/p&gt;
&lt;p&gt;异常路径也要设计：如果注销失败，立即释放 &lt;code&gt;GCHandle&lt;/code&gt; 可能形成悬空上下文；无限保留则泄漏。应根据厂商契约决定重试、强制停止或把会话转入不可恢复状态，而不是在 &lt;code&gt;finally&lt;/code&gt; 中盲目释放。&lt;/p&gt;
&lt;h2 id="5-回调线程不等于-ui-线程"&gt;&lt;a href="#5-%e5%9b%9e%e8%b0%83%e7%ba%bf%e7%a8%8b%e4%b8%8d%e7%ad%89%e4%ba%8e-ui-%e7%ba%bf%e7%a8%8b" class="header-anchor"&gt;&lt;/a&gt;5. 回调线程不等于 UI 线程
&lt;/h2&gt;&lt;p&gt;相机或运动控制 SDK 常从内部原生线程调用。回调中应只做有界、非阻塞工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;复制必要数据和时间戳；&lt;/li&gt;
&lt;li&gt;写入 &lt;code&gt;Channel&amp;lt;T&amp;gt;&lt;/code&gt;、无界风险受控的队列或专用调度器；&lt;/li&gt;
&lt;li&gt;立即返回，让 SDK 线程继续工作；&lt;/li&gt;
&lt;li&gt;在消费者侧解析、记录、更新状态并切换到 UI Dispatcher。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要在回调里等待 UI、获取长时间持有的业务锁、同步调用关闭设备，或执行不可控的磁盘/网络 I/O。否则不仅丢帧，还可能与 SDK 内部锁形成死锁。锁与死锁的通用机制见《&lt;a class="link" href="https://www.jiwei.space/posts/architecture/thread-safety-essentials/" &gt;线程安全的本质：从 CPU 缓存到内存模型，锁到底在保证什么&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="6-委托回调仍然可用但必须扎根"&gt;&lt;a href="#6-%e5%a7%94%e6%89%98%e5%9b%9e%e8%b0%83%e4%bb%8d%e7%84%b6%e5%8f%af%e7%94%a8%e4%bd%86%e5%bf%85%e9%a1%bb%e6%89%8e%e6%a0%b9" class="header-anchor"&gt;&lt;/a&gt;6. 委托回调仍然可用，但必须扎根
&lt;/h2&gt;&lt;p&gt;旧 API 或不适合函数指针的场景可以声明委托：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;[UnmanagedFunctionPointer(CallingConvention.Cdecl)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;unsafe&lt;/span&gt; &lt;span class="k"&gt;delegate&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;DeviceEventCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;eventCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;payloadLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果原生代码只在一次同步调用期间使用委托，可在调用后用 &lt;code&gt;GC.KeepAlive(callback)&lt;/code&gt; 保证其活到调用结束。如果原生库保存函数指针，就必须把委托存入与注册同寿命的字段，不能只依赖局部变量、&lt;code&gt;GC.KeepAlive&lt;/code&gt; 或“目前 GC 还没发生”。&lt;/p&gt;
&lt;h2 id="7-回调测试要覆盖时间窗口"&gt;&lt;a href="#7-%e5%9b%9e%e8%b0%83%e6%b5%8b%e8%af%95%e8%a6%81%e8%a6%86%e7%9b%96%e6%97%b6%e9%97%b4%e7%aa%97%e5%8f%a3" class="header-anchor"&gt;&lt;/a&gt;7. 回调测试要覆盖时间窗口
&lt;/h2&gt;&lt;p&gt;至少验证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注册失败时 context 不泄漏；&lt;/li&gt;
&lt;li&gt;回调在工作线程、并发和重入时仍正确；&lt;/li&gt;
&lt;li&gt;空载荷、最大载荷和非法长度受到保护；&lt;/li&gt;
&lt;li&gt;回调处理异常不会越过 ABI；&lt;/li&gt;
&lt;li&gt;注销与回调竞争时没有 use-after-free；&lt;/li&gt;
&lt;li&gt;关闭设备后不会再向已释放队列或 UI 发布；&lt;/li&gt;
&lt;li&gt;多次启动/停止不会重复注册。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;模拟原生线程的测试应主动随机化回调时序，而不是只在单线程中顺序调用入口。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;回调是一段跨线程、跨 GC、跨 ABI 的长期租约。函数签名正确只是起点；托管目标、委托或 &lt;code&gt;GCHandle&lt;/code&gt; 必须保持到最后一次回调结束，载荷要在有效期内复制，异常不能穿过边界，耗时工作应转移到托管队列。&lt;/p&gt;
&lt;p&gt;下一篇用 &lt;code&gt;SafeHandle&lt;/code&gt; 管理设备句柄，让关闭时机和调用期间的存活保证进入类型系统。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/pinvoke" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Platform Invoke（P/Invoke）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/calling-conventions" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Unmanaged calling conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute" target="_blank" rel="noopener"
 &gt;Microsoft Learn：UnmanagedCallersOnlyAttribute&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/best-practices" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native interoperability best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>C# 原生互操作与设备 SDK（五）：调用约定、位数与 DLL 加载诊断</title><link>https://www.jiwei.space/posts/equipment/native-interop/05-loading-diagnostics/</link><pubDate>Wed, 22 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/native-interop/05-loading-diagnostics/</guid><description>&lt;p&gt;“DLL 明明就在目录里，为什么仍然提示找不到？”这是设备 SDK 接入中最常见、也最容易误诊的问题。报错里的库名只是加载链入口；真正失败的可能是进程位数、二级依赖、导出符号、调用约定或搜索路径。&lt;/p&gt;
&lt;p&gt;本文建立一套从文件、架构、依赖、导出到 ABI 的诊断顺序，并说明如何用 &lt;code&gt;NativeLibrary&lt;/code&gt; 管理多平台库选择。&lt;/p&gt;
&lt;h2 id="1-调用约定必须与头文件一致"&gt;&lt;a href="#1-%e8%b0%83%e7%94%a8%e7%ba%a6%e5%ae%9a%e5%bf%85%e9%a1%bb%e4%b8%8e%e5%a4%b4%e6%96%87%e4%bb%b6%e4%b8%80%e8%87%b4" class="header-anchor"&gt;&lt;/a&gt;1. 调用约定必须与头文件一致
&lt;/h2&gt;&lt;p&gt;调用约定（Calling Convention）规定参数如何传递、栈如何清理以及返回值如何交付。现代 64 位平台往往统一了许多历史差异，但声明仍应匹配原生构建约定，尤其是 Windows x86 和回调函数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LibraryImport&lt;/code&gt; 可配合 &lt;code&gt;UnmanagedCallConv&lt;/code&gt; 显式声明 &lt;code&gt;cdecl&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.CompilerServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_open&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [UnmanagedCallConv(CallConvs = new[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CallConvCdecl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;旧式 &lt;code&gt;DllImport&lt;/code&gt; 则使用 &lt;code&gt;CallingConvention&lt;/code&gt; 属性：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LegacyNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [DllImport(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; &amp;#34;device_sdk&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; EntryPoint = &amp;#34;device_open&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; ExactSpelling = true,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; CallingConvention = CallingConvention.Cdecl)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不要根据函数名或别的 SDK 猜 &lt;code&gt;Cdecl&lt;/code&gt;/&lt;code&gt;StdCall&lt;/code&gt;。先看导出宏、函数指针 typedef、项目设置和厂商支持的目标平台。&lt;/p&gt;
&lt;h2 id="2-进程架构决定可加载的库"&gt;&lt;a href="#2-%e8%bf%9b%e7%a8%8b%e6%9e%b6%e6%9e%84%e5%86%b3%e5%ae%9a%e5%8f%af%e5%8a%a0%e8%bd%bd%e7%9a%84%e5%ba%93" class="header-anchor"&gt;&lt;/a&gt;2. 进程架构决定可加载的库
&lt;/h2&gt;&lt;p&gt;64 位操作系统可以运行 32 位进程，但单个进程不能把 32 位和 64 位机器代码混装。先输出当前进程信息：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;OS: {RuntimeInformation.OSDescription}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;OS architecture: {RuntimeInformation.OSArchitecture}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;Process architecture: {RuntimeInformation.ProcessArchitecture}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;64-bit process: {Environment.Is64BitProcess}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;检查的是进程架构，不是只看操作系统或 CPU。若 SDK 只有 x86 版本，应用必须以 x86 运行；若同时有 x64 和 Arm64，部署时要按 Runtime Identifier（RID）选择对应资产。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BadImageFormatException&lt;/code&gt; 常见于架构不匹配，但也可能表示文件损坏或目标根本不是当前平台可加载的动态库，因此仍要查看文件头。&lt;/p&gt;
&lt;h2 id="3-主库存在不代表依赖完整"&gt;&lt;a href="#3-%e4%b8%bb%e5%ba%93%e5%ad%98%e5%9c%a8%e4%b8%8d%e4%bb%a3%e8%a1%a8%e4%be%9d%e8%b5%96%e5%ae%8c%e6%95%b4" class="header-anchor"&gt;&lt;/a&gt;3. 主库存在不代表依赖完整
&lt;/h2&gt;&lt;p&gt;设备 SDK 经常形成依赖链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DeviceAdapter.dll
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── device_sdk.dll
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── vendor_runtime.dll
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── camera_transport.dll
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Microsoft Visual C++ Runtime / system libraries
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;当 &lt;code&gt;vendor_runtime.dll&lt;/code&gt; 缺失时，.NET 仍可能把入口库报告为加载失败。还要检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;依赖库是否部署在加载器能找到的位置；&lt;/li&gt;
&lt;li&gt;依赖版本和架构是否一致；&lt;/li&gt;
&lt;li&gt;Linux 的 &lt;code&gt;SONAME&lt;/code&gt;、macOS install name 是否匹配；&lt;/li&gt;
&lt;li&gt;运行账户是否有读取/执行权限；&lt;/li&gt;
&lt;li&gt;厂商驱动或运行时是否必须单独安装。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要通过把一堆未知 DLL 复制进系统目录来“试到能跑”。这会隐藏版本来源并污染整机环境。&lt;/p&gt;
&lt;h2 id="4-导出名以二进制为准"&gt;&lt;a href="#4-%e5%af%bc%e5%87%ba%e5%90%8d%e4%bb%a5%e4%ba%8c%e8%bf%9b%e5%88%b6%e4%b8%ba%e5%87%86" class="header-anchor"&gt;&lt;/a&gt;4. 导出名以二进制为准
&lt;/h2&gt;&lt;p&gt;头文件中的 C++ 函数可能被名称修饰（Name Mangling），宏也可能改变导出名。常用检查命令如下：&lt;/p&gt;
&lt;p&gt;Windows 的 Visual Studio Developer Command Prompt：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dumpbin&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="n"&gt;device_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;dll&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;findstr&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dumpbin&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;device_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;dll&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Linux：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;file libdevice_sdk.so
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ldd libdevice_sdk.so
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;readelf -Ws libdevice_sdk.so
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;macOS：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;file libdevice_sdk.dylib
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;otool -L libdevice_sdk.dylib
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nm -gU libdevice_sdk.dylib
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这些命令应在库所在目录执行。&lt;code&gt;dumpbin&lt;/code&gt; 随 Visual Studio C++ 工具链提供；&lt;code&gt;ldd&lt;/code&gt; 会触发动态加载器解析，不能对来源不可信的二进制随意运行。&lt;/p&gt;
&lt;p&gt;若导出表中只有类似 &lt;code&gt;?Open@Device@@...&lt;/code&gt; 的符号，说明它暴露的是编译器相关 C++ ABI。长期方案通常是增加 &lt;code&gt;extern &amp;quot;C&amp;quot;&lt;/code&gt; 的 C 包装层，而不是把修饰名硬编码进 C#。&lt;/p&gt;
&lt;h2 id="5-明确管理动态库选择"&gt;&lt;a href="#5-%e6%98%8e%e7%a1%ae%e7%ae%a1%e7%90%86%e5%8a%a8%e6%80%81%e5%ba%93%e9%80%89%e6%8b%a9" class="header-anchor"&gt;&lt;/a&gt;5. 明确管理动态库选择
&lt;/h2&gt;&lt;p&gt;固定库名适合简单部署。若不同平台或 CPU 特性需要选择不同实现，可以为当前程序集注册一个解析器：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Reflection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LibraryName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;device_sdk&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;NativeLibrary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetDllImportResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ResolveLibrary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;ResolveLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;libraryName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Assembly&lt;/span&gt; &lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DllImportSearchPath&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;searchPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libraryName&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;LibraryName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 交回默认解析流程&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OperatingSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsWindows&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;device_sdk.dll&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OperatingSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsLinux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;libdevice_sdk.so&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OperatingSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsMacOS&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;libdevice_sdk.dylib&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PlatformNotSupportedException&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;architecture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RuntimeInformation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessArchitecture&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;AppContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;native&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;architecture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NativeLibrary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(LibraryName, EntryPoint = &amp;#34;device_open&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这段代码定义的是项目自己的部署布局，发布过程必须把文件放到对应目录。每个程序集只能注册一个 &lt;code&gt;DllImportResolver&lt;/code&gt;，应集中管理，不能让多个 SDK 初始化器互相争抢。&lt;/p&gt;
&lt;p&gt;NuGet 包可以把平台资产放在 &lt;code&gt;runtimes/{rid}/native/&lt;/code&gt; 下，由恢复与发布流程按 RID 选择。实际支持的 RID 应由构建和测试矩阵决定，不要根据字符串临时拼出一个未测试的平台。适配器层如何把这类平台差异挡在业务之外，见《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/equipment-architecture/02-hal-adapters/" &gt;设备软件架构与控制模型（二）：硬件抽象层与设备适配器&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="6-搜索路径也是安全边界"&gt;&lt;a href="#6-%e6%90%9c%e7%b4%a2%e8%b7%af%e5%be%84%e4%b9%9f%e6%98%af%e5%ae%89%e5%85%a8%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;6. 搜索路径也是安全边界
&lt;/h2&gt;&lt;p&gt;动态加载器搜索当前目录、应用目录、系统目录或环境变量的规则因平台和配置而异。把可写目录加入全局 &lt;code&gt;PATH&lt;/code&gt;，或从当前工作目录加载同名 DLL，可能让攻击者或误操作放入错误二进制。&lt;/p&gt;
&lt;p&gt;更稳妥的策略是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SDK 文件随应用或受控安装器部署；&lt;/li&gt;
&lt;li&gt;使用明确的应用内路径或 RID 资产；&lt;/li&gt;
&lt;li&gt;校验安装包来源、签名或摘要；&lt;/li&gt;
&lt;li&gt;记录实际加载的文件版本与路径；&lt;/li&gt;
&lt;li&gt;不从用户上传目录、临时目录和网络共享自动加载原生库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="7-按固定顺序定位问题"&gt;&lt;a href="#7-%e6%8c%89%e5%9b%ba%e5%ae%9a%e9%a1%ba%e5%ba%8f%e5%ae%9a%e4%bd%8d%e9%97%ae%e9%a2%98" class="header-anchor"&gt;&lt;/a&gt;7. 按固定顺序定位问题
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;记录操作系统、进程架构和 .NET 版本；&lt;/li&gt;
&lt;li&gt;确认入口文件真实存在且格式、架构正确；&lt;/li&gt;
&lt;li&gt;检查所有原生依赖及驱动前置条件；&lt;/li&gt;
&lt;li&gt;核对实际导出名；&lt;/li&gt;
&lt;li&gt;对照头文件确认调用约定和完整签名；&lt;/li&gt;
&lt;li&gt;用最小控制台程序调用无副作用的版本查询函数；&lt;/li&gt;
&lt;li&gt;最后才接入复杂 UI、服务容器和设备流程。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个顺序把“无法加载”“找不到符号”和“调用后损坏”分开，避免在 ABI 错误上反复调整文件路径。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;原生库加载是一条依赖链，不是一次文件查找。进程架构、依赖库、导出符号、调用约定和搜索路径必须逐层确认；&lt;code&gt;NativeLibrary&lt;/code&gt; 能让选择逻辑显式化，但不能修复错误 ABI。&lt;/p&gt;
&lt;p&gt;下一篇讨论反向调用：当厂商 SDK 从原生线程回调 C# 时，如何管理函数指针、对象寿命与线程切换。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/calling-conventions" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Unmanaged calling conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/native-library-loading" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native library loading&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/rid-catalog" target="_blank" rel="noopener"
 &gt;Microsoft Learn：.NET RID Catalog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/nuget/create-packages/native-files-in-net-packages" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native files in .NET packages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>C# 原生互操作与设备 SDK（四）：字符串、数组、指针与 Marshalling</title><link>https://www.jiwei.space/posts/equipment/native-interop/04-marshalling-ownership/</link><pubDate>Tue, 21 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/native-interop/04-marshalling-ownership/</guid><description>&lt;p&gt;互操作中的数据错误通常不在“能不能转换”，而在“谁分配、谁释放、能写多少、指针能留多久”。同一个 &lt;code&gt;char*&lt;/code&gt; 可能表示只读输入、调用者提供的输出缓冲区、库分配的返回值，或只在下一次 SDK 调用前有效的借用视图。&lt;/p&gt;
&lt;p&gt;本文把字符串、数组、&lt;code&gt;ref&lt;/code&gt;/&lt;code&gt;out&lt;/code&gt; 和指针放回完整契约中讨论。示例以 UTF-8 C API 为主，目标环境为 .NET 10。&lt;/p&gt;
&lt;h2 id="1-先为每个指针写五项契约"&gt;&lt;a href="#1-%e5%85%88%e4%b8%ba%e6%af%8f%e4%b8%aa%e6%8c%87%e9%92%88%e5%86%99%e4%ba%94%e9%a1%b9%e5%a5%91%e7%ba%a6" class="header-anchor"&gt;&lt;/a&gt;1. 先为每个指针写五项契约
&lt;/h2&gt;&lt;p&gt;看到 &lt;code&gt;T*&lt;/code&gt;、&lt;code&gt;void*&lt;/code&gt; 或 &lt;code&gt;char*&lt;/code&gt; 时，先记录：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;方向：输入、输出还是输入输出；&lt;/li&gt;
&lt;li&gt;长度：固定、以零结尾、由参数给出，还是由返回值给出；&lt;/li&gt;
&lt;li&gt;分配者：调用者、SDK 还是操作系统；&lt;/li&gt;
&lt;li&gt;生命周期：仅本次调用、直到下一次调用、直到显式释放，还是与句柄同寿命；&lt;/li&gt;
&lt;li&gt;可变性与线程：原生代码能否写入，是否会跨线程保留指针。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺少任一项，都不应急着写 P/Invoke 声明。&lt;/p&gt;
&lt;h2 id="2-只读输入字符串显式指定编码"&gt;&lt;a href="#2-%e5%8f%aa%e8%af%bb%e8%be%93%e5%85%a5%e5%ad%97%e7%ac%a6%e4%b8%b2%e6%98%be%e5%bc%8f%e6%8c%87%e5%ae%9a%e7%bc%96%e7%a0%81" class="header-anchor"&gt;&lt;/a&gt;2. 只读输入字符串显式指定编码
&lt;/h2&gt;&lt;p&gt;原生接口：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_set_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_handle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;utf8_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;若函数只在调用期间读取字符串，&lt;code&gt;LibraryImport&lt;/code&gt; 可以生成 UTF-8 转换：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; &amp;#34;device_sdk&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; EntryPoint = &amp;#34;device_set_name&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; StringMarshalling = StringMarshalling.Utf8)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;SetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这不允许原生库保存收到的指针。若 API 会在返回后继续使用它，就必须按 SDK 约定分配稳定内存，并在注销或关闭后释放。&lt;/p&gt;
&lt;h2 id="3-输出文本优先采用调用者缓冲区"&gt;&lt;a href="#3-%e8%be%93%e5%87%ba%e6%96%87%e6%9c%ac%e4%bc%98%e5%85%88%e9%87%87%e7%94%a8%e8%b0%83%e7%94%a8%e8%80%85%e7%bc%93%e5%86%b2%e5%8c%ba" class="header-anchor"&gt;&lt;/a&gt;3. 输出文本优先采用调用者缓冲区
&lt;/h2&gt;&lt;p&gt;一个边界清晰的 C API 会同时接收缓冲区和容量，并返回实际所需长度：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_get_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device_handle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;utf8_buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_required&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;下面的示例约定：&lt;code&gt;out_required&lt;/code&gt; 包含结尾零；以空缓冲区查询长度时返回成功；若第二次调用期间名称变长，函数返回可识别的“缓冲区不足”状态并更新长度。其他 SDK 可能采用不同约定，必须相应调整封装。&lt;/p&gt;
&lt;p&gt;托管声明保留原始指针语义：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_get_name&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;unsafe&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;GetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;封装层可以先询问长度，再固定缓冲区调用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNameReader&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;unsafe&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ReadName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceSdkException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;device_get_name(size)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidDataException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;SDK returned an invalid name length&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllocateUninitializedArray&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 分配在固定对象堆（POH）上，内存在 GC 下保持固定&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// pinned: true 保证内存固定，fixed 在这里只负责取得指向它的指针。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fixed&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pointer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nuint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceSdkException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;device_get_name(data)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nuint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidDataException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;SDK returned an invalid name length&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;terminator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;terminator&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidDataException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;SDK returned a non-terminated name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;terminator&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;真实封装还应识别“缓冲区不足”状态并按更新后的所需长度重试，避免名称在两次调用间变化造成误判。&lt;/p&gt;
&lt;h2 id="4-不要用-out-string-充当可写缓冲区"&gt;&lt;a href="#4-%e4%b8%8d%e8%a6%81%e7%94%a8-out-string-%e5%85%85%e5%bd%93%e5%8f%af%e5%86%99%e7%bc%93%e5%86%b2%e5%8c%ba" class="header-anchor"&gt;&lt;/a&gt;4. 不要用 Out string 充当可写缓冲区
&lt;/h2&gt;&lt;p&gt;托管字符串不可变。把按值字符串标成 &lt;code&gt;[Out] string&lt;/code&gt; 让原生代码写入，可能破坏运行时假设；Microsoft 的互操作建议明确反对这种写法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;StringBuilder&lt;/code&gt; 可用于某些旧 API，但封送过程通常需要额外分配和复制，而且容量与结尾零仍容易出错。新设计优先使用显式字节/字符缓冲区加长度；旧接口则严格按官方签名处理。&lt;/p&gt;
&lt;h2 id="5-数组必须与元素数量一起传递"&gt;&lt;a href="#5-%e6%95%b0%e7%bb%84%e5%bf%85%e9%a1%bb%e4%b8%8e%e5%85%83%e7%b4%a0%e6%95%b0%e9%87%8f%e4%b8%80%e8%b5%b7%e4%bc%a0%e9%80%92" class="header-anchor"&gt;&lt;/a&gt;5. 数组必须与元素数量一起传递
&lt;/h2&gt;&lt;p&gt;原生数组本质上通常只是首元素指针，长度不会自动跟随：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_read_samples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device_handle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果原生代码只在调用期间写入，可固定托管数组：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SampleReader&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_read_samples&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;unsafe&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ReadSamplesNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;unsafe&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ReadSamples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fixed&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pointer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReadSamplesNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nuint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceSdkException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;device_read_samples&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nuint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidDataException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;SDK returned an invalid sample count&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;fixed&lt;/code&gt; 的保证只覆盖代码块。若 SDK 异步保留指针，必须使用拥有明确释放时机的固定内存或非托管内存，并避免长期固定大对象造成 GC 压力。&lt;/p&gt;
&lt;h2 id="6-refout-和指针的层级要一致"&gt;&lt;a href="#6-refout-%e5%92%8c%e6%8c%87%e9%92%88%e7%9a%84%e5%b1%82%e7%ba%a7%e8%a6%81%e4%b8%80%e8%87%b4" class="header-anchor"&gt;&lt;/a&gt;6. ref、out 和指针的层级要一致
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;C 参数&lt;/th&gt;
 &lt;th&gt;常见 C# 表达&lt;/th&gt;
 &lt;th&gt;含义&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;int32_t value&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;int value&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;按值输入&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;const int32_t* value&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;in int value&lt;/code&gt; 或指针&lt;/td&gt;
 &lt;td&gt;指向只读单值&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;int32_t* out_value&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;out int value&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;输出一个单值&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;int32_t* values&lt;/code&gt; + 长度&lt;/td&gt;
 &lt;td&gt;数组/&lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; 固定后的指针&lt;/td&gt;
 &lt;td&gt;连续元素&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;device_handle* out_handle&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;out nint handle&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;输出一个句柄&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;void** out_buffer&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;out nint buffer&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;输出一根指向内存的指针&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;ref&lt;/code&gt;/&lt;code&gt;out&lt;/code&gt; 只是表达一层间接寻址，不能自动推导数组长度、分配器或所有权。&lt;/p&gt;
&lt;h2 id="7-谁分配谁提供释放方式"&gt;&lt;a href="#7-%e8%b0%81%e5%88%86%e9%85%8d%e8%b0%81%e6%8f%90%e4%be%9b%e9%87%8a%e6%94%be%e6%96%b9%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;7. 谁分配，谁提供释放方式
&lt;/h2&gt;&lt;p&gt;如果 SDK 返回自己分配的内存，应该同时提供释放函数：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_create_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_handle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;out_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;device_free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;托管侧复制完后在 &lt;code&gt;finally&lt;/code&gt; 中调用同一库的释放函数：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceReportReader&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_create_report&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CreateReportNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_free&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;CreateReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CreateReportNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nuint&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;$&amp;#34;device_create_report failed: {status}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;managed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;managed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;managed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;managed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不能看到 &lt;code&gt;malloc&lt;/code&gt; 就随意改用 &lt;code&gt;Marshal.FreeHGlobal&lt;/code&gt;，也不能用托管分配器释放 DLL 内部堆上的内存。Windows 上不同 CRT/模块之间混用分配器尤其容易造成堆损坏。&lt;/p&gt;
&lt;h2 id="8-借用指针不要包装成永久对象"&gt;&lt;a href="#8-%e5%80%9f%e7%94%a8%e6%8c%87%e9%92%88%e4%b8%8d%e8%a6%81%e5%8c%85%e8%a3%85%e6%88%90%e6%b0%b8%e4%b9%85%e5%af%b9%e8%b1%a1" class="header-anchor"&gt;&lt;/a&gt;8. 借用指针不要包装成永久对象
&lt;/h2&gt;&lt;p&gt;有些 SDK 返回指向内部缓存的指针，并注明“直到下一次调用有效”。此时应在有效窗口内立即复制，并序列化可能使缓存失效的调用。把它转换成 &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; 或 &lt;code&gt;ReadOnlySpan&amp;lt;T&amp;gt;&lt;/code&gt; 不会延长原生内存寿命；Span 只描述一段内存，不拥有它。&lt;/p&gt;
&lt;p&gt;如果指针与设备句柄同寿命，读取期间还必须确保句柄不会并发关闭。后续 &lt;code&gt;SafeHandle&lt;/code&gt; 文章会处理这项约束。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;Marshalling 不是类型转换清单，而是跨边界的数据契约。编码、容量、实际长度、可写性、保留时间和释放函数必须同时明确。优先使用调用者缓冲区和显式长度；对于 SDK 分配的内存，始终由匹配的 SDK 函数释放。&lt;/p&gt;
&lt;p&gt;下一篇进入加载阶段：调用约定、进程位数、导出符号和依赖库如何系统诊断。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/best-practices" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native interoperability best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/customize-parameter-marshalling" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Customize parameter marshalling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/marshalling-strings" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Marshalling strings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/default-marshalling-for-arrays" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Default marshalling for arrays&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>C# 原生互操作与设备 SDK（三）：StructLayout、内存布局与对齐</title><link>https://www.jiwei.space/posts/equipment/native-interop/03-struct-layout/</link><pubDate>Mon, 20 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/native-interop/03-struct-layout/</guid><description>&lt;p&gt;函数参数的基础类型都正确，结构体仍可能读出离谱数据。原因是 ABI 不只规定字段类型，还规定字段顺序、对齐、填充、整体大小，以及结构是按值传递还是通过指针传递。&lt;/p&gt;
&lt;p&gt;本文从一份可计算的布局开始，说明 &lt;code&gt;StructLayout&lt;/code&gt;、&lt;code&gt;Pack&lt;/code&gt;、联合体和 blittable 类型的边界，并建立“原生侧与托管侧同时验证”的方法。&lt;/p&gt;
&lt;h2 id="1-字段之间可能存在填充"&gt;&lt;a href="#1-%e5%ad%97%e6%ae%b5%e4%b9%8b%e9%97%b4%e5%8f%af%e8%83%bd%e5%ad%98%e5%9c%a8%e5%a1%ab%e5%85%85" class="header-anchor"&gt;&lt;/a&gt;1. 字段之间可能存在填充
&lt;/h2&gt;&lt;p&gt;考虑以下 C 结构：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdint.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device_status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;struct_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;position_mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;device_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;在常见的自然对齐规则下，各字段偏移依次为 &lt;code&gt;0&lt;/code&gt;、&lt;code&gt;4&lt;/code&gt;、&lt;code&gt;8&lt;/code&gt;、&lt;code&gt;16&lt;/code&gt;、&lt;code&gt;17&lt;/code&gt;，整体为 24 字节。这个结果不是靠字段大小相加猜出的，而是由目标 ABI 和编译选项决定。&lt;/p&gt;
&lt;p&gt;对应的 C# 声明可以写为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;[StructLayout(LayoutKind.Sequential)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;unsafe&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;DeviceStatusNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;StructSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;PositionMillimeters&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;fixed&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;Reserved&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;LayoutKind.Sequential&lt;/code&gt; 保持字段声明顺序，并由运行时按平台规则放置字段。&lt;code&gt;fixed byte Reserved[7]&lt;/code&gt; 是经典的定长缓冲写法；现代 .NET（C# 12+）也可用 &lt;code&gt;[InlineArray]&lt;/code&gt; 自定义结构体表达定长内联数组，但用于互操作时仍要验证整体大小与字段偏移。不要因为看到“结构体大小不对”就立即添加 &lt;code&gt;Pack = 1&lt;/code&gt;；只有原生头文件确实使用紧凑布局或 &lt;code&gt;#pragma pack&lt;/code&gt; 时，托管侧才应使用相同设置。&lt;/p&gt;
&lt;h2 id="2-用大小与偏移断言验证布局"&gt;&lt;a href="#2-%e7%94%a8%e5%a4%a7%e5%b0%8f%e4%b8%8e%e5%81%8f%e7%a7%bb%e6%96%ad%e8%a8%80%e9%aa%8c%e8%af%81%e5%b8%83%e5%b1%80" class="header-anchor"&gt;&lt;/a&gt;2. 用大小与偏移断言验证布局
&lt;/h2&gt;&lt;p&gt;对布局敏感的结构，应在测试中固定预期：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.CompilerServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Unsafe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SizeOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DeviceStatusNative&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Unexpected DeviceStatusNative size&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OffsetOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DeviceStatusNative&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeviceStatusNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PositionMillimeters&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="n"&gt;ToInt32&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Unexpected position field offset&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;原生侧也应使用同一构建链验证：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stddef.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;_Static_assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;device_status size mismatch&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;_Static_assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;offsetof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position_mm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;position_mm offset mismatch&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果 SDK 支持多个编译器和架构，不应把未经验证的 24 写成普遍真理，而应在每个受支持目标上执行这些检查。&lt;/p&gt;
&lt;h2 id="3-pack-是-abi-约定不是修复按钮"&gt;&lt;a href="#3-pack-%e6%98%af-abi-%e7%ba%a6%e5%ae%9a%e4%b8%8d%e6%98%af%e4%bf%ae%e5%a4%8d%e6%8c%89%e9%92%ae" class="header-anchor"&gt;&lt;/a&gt;3. Pack 是 ABI 约定，不是修复按钮
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Pack&lt;/code&gt; 控制字段对齐的上限。下面的声明只有在原生侧明确采用 1 字节打包时才正确：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;[StructLayout(LayoutKind.Sequential, Pack = 1)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;PackedHeader&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;PayloadLength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;紧凑布局可能减少空间，也可能产生未对齐访问并改变调用 ABI。常见错误是厂商示例在一个头文件中 &lt;code&gt;#pragma pack(push, 1)&lt;/code&gt; 后未被注意，或者托管侧随意设置 &lt;code&gt;Pack = 1&lt;/code&gt; 来“对齐数值”。正确做法是追踪头文件的 &lt;code&gt;push&lt;/code&gt;/&lt;code&gt;pop&lt;/code&gt; 范围和项目编译选项。&lt;/p&gt;
&lt;h2 id="4-联合体使用-explicit-布局"&gt;&lt;a href="#4-%e8%81%94%e5%90%88%e4%bd%93%e4%bd%bf%e7%94%a8-explicit-%e5%b8%83%e5%b1%80" class="header-anchor"&gt;&lt;/a&gt;4. 联合体使用 Explicit 布局
&lt;/h2&gt;&lt;p&gt;C 联合体让多个字段共享同一段内存：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="n"&gt;device_value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;int_value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;double_value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;device_value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;C# 用 &lt;code&gt;LayoutKind.Explicit&lt;/code&gt; 和相同的 &lt;code&gt;FieldOffset&lt;/code&gt; 表达：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;[StructLayout(LayoutKind.Explicit)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;DeviceValueNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [FieldOffset(0)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;IntValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [FieldOffset(0)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;DoubleValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;读取哪个字段必须由联合体外部的类型标签决定。重叠字段只复刻存储，不会自动验证当前分支。&lt;/p&gt;
&lt;h2 id="5-固定数组与指针字段含义不同"&gt;&lt;a href="#5-%e5%9b%ba%e5%ae%9a%e6%95%b0%e7%bb%84%e4%b8%8e%e6%8c%87%e9%92%88%e5%ad%97%e6%ae%b5%e5%90%ab%e4%b9%89%e4%b8%8d%e5%90%8c" class="header-anchor"&gt;&lt;/a&gt;5. 固定数组与指针字段含义不同
&lt;/h2&gt;&lt;p&gt;这两个 C 字段不是一回事：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;serial_inline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="cm"&gt;/* 32 字节就在结构体内部 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;serial_ptr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* 结构体里只有一个指针 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;内联数组需要固定缓冲区或相应的字段封送；指针字段则用 &lt;code&gt;nint&lt;/code&gt;/指针表达，并另行定义指向内存的长度、编码和生命周期。把指针误写成 &lt;code&gt;[MarshalAs(UnmanagedType.ByValArray)]&lt;/code&gt; 会直接改变结构布局。&lt;/p&gt;
&lt;h2 id="6-blittable-让双方共享位表示"&gt;&lt;a href="#6-blittable-%e8%ae%a9%e5%8f%8c%e6%96%b9%e5%85%b1%e4%ba%ab%e4%bd%8d%e8%a1%a8%e7%a4%ba" class="header-anchor"&gt;&lt;/a&gt;6. blittable 让双方共享位表示
&lt;/h2&gt;&lt;p&gt;blittable 类型在托管和非托管表示之间具有相同的位级布局，通常可以固定后直接传递，避免逐字段转换。定宽整数、浮点数、指针以及只包含这类字段的顺序结构更容易做到 blittable。&lt;/p&gt;
&lt;p&gt;以下内容会引入额外规则或转换：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bool&lt;/code&gt; 和 &lt;code&gt;char&lt;/code&gt; 的表示依赖封送设置；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;string&lt;/code&gt;、普通托管数组和对象引用需要转换或固定；&lt;/li&gt;
&lt;li&gt;带自动布局的类型不能作为稳定 ABI；&lt;/li&gt;
&lt;li&gt;关闭运行时封送后，受支持集合与默认规则会变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“C# &lt;code&gt;unmanaged&lt;/code&gt; 泛型约束”和“启用运行时封送时的 blittable”概念接近但不完全等价，尤其要关注 &lt;code&gt;bool&lt;/code&gt; 与 &lt;code&gt;char&lt;/code&gt;。不要在文章或代码评审中把两者当同义词。&lt;/p&gt;
&lt;h2 id="7-按值和按引用必须照抄签名"&gt;&lt;a href="#7-%e6%8c%89%e5%80%bc%e5%92%8c%e6%8c%89%e5%bc%95%e7%94%a8%e5%bf%85%e9%a1%bb%e7%85%a7%e6%8a%84%e7%ad%be%e5%90%8d" class="header-anchor"&gt;&lt;/a&gt;7. 按值和按引用必须照抄签名
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;device_apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_status&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="cm"&gt;/* 按值复制 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;device_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_status&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="cm"&gt;/* 传指针 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;对应 C# 签名分别是值参数和 &lt;code&gt;out&lt;/code&gt;/指针参数。结构较大并不意味着原生 API 一定按引用传递，也不能因为 C# 中 &lt;code&gt;struct&lt;/code&gt; 是值类型，就给原生指针参数省略 &lt;code&gt;ref&lt;/code&gt; 或 &lt;code&gt;out&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于带 &lt;code&gt;struct_size&lt;/code&gt; 或 &lt;code&gt;version&lt;/code&gt; 字段的 API，调用前要按厂商约定初始化：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceStatusNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;StructSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;Unsafe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SizeOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DeviceStatusNative&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这类字段常用于在新旧结构版本间协商可访问范围，不能留为默认零值。&lt;/p&gt;
&lt;h2 id="8-布局审查清单"&gt;&lt;a href="#8-%e5%b8%83%e5%b1%80%e5%ae%a1%e6%9f%a5%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;8. 布局审查清单
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;使用了哪套头文件、编译器、架构和打包选项；&lt;/li&gt;
&lt;li&gt;每个字段的原生大小、托管大小和偏移是否一致；&lt;/li&gt;
&lt;li&gt;原生 &lt;code&gt;bool&lt;/code&gt;、枚举、&lt;code&gt;long&lt;/code&gt; 和 &lt;code&gt;wchar_t&lt;/code&gt; 是否已显式确认；&lt;/li&gt;
&lt;li&gt;固定数组、指针、柔性数组成员是否被正确区分；&lt;/li&gt;
&lt;li&gt;结构是按值、指针还是指针的指针传递；&lt;/li&gt;
&lt;li&gt;是否含版本/大小字段，谁负责初始化；&lt;/li&gt;
&lt;li&gt;原生与托管测试是否同时断言 &lt;code&gt;sizeof&lt;/code&gt; 和关键 &lt;code&gt;offsetof&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;结构体互操作不是“加一个 &lt;code&gt;StructLayout&lt;/code&gt;”就结束。字段顺序、自然对齐、显式打包、联合体、内联数组和传递方式必须共同匹配。最可靠的办法是在两侧写大小与偏移断言，让 ABI 漂移在测试阶段失败。&lt;/p&gt;
&lt;p&gt;下一篇讨论结构之外最复杂的边界：字符串、数组、&lt;code&gt;ref&lt;/code&gt;/&lt;code&gt;out&lt;/code&gt;、指针与内存所有权。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/customize-struct-marshalling" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Customize structure marshalling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/framework/interop/blittable-and-non-blittable-types" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Blittable and non-blittable types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/disabled-marshalling" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Disabled runtime marshalling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.unsafe.sizeof" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Unsafe.SizeOf&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>C# 原生互操作与设备 SDK（二）：C# 类型与 C ABI</title><link>https://www.jiwei.space/posts/equipment/native-interop/02-c-abi-types/</link><pubDate>Sun, 19 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/native-interop/02-c-abi-types/</guid><description>&lt;p&gt;互操作签名最危险的错误往往没有编译提示：C# 和 C 都存在名为 &lt;code&gt;long&lt;/code&gt;、&lt;code&gt;bool&lt;/code&gt;、&lt;code&gt;char&lt;/code&gt; 的类型，但名称相同不代表大小、编码或 ABI 表示相同。一个字段宽度错了，后续参数就可能全部错位。&lt;/p&gt;
&lt;p&gt;本文建立从原生头文件到 C# 声明的核对方法。目标不是背一张万能映射表，而是根据平台、编译器和头文件选择位宽明确的托管类型。&lt;/p&gt;
&lt;h2 id="1-优先从定宽类型开始"&gt;&lt;a href="#1-%e4%bc%98%e5%85%88%e4%bb%8e%e5%ae%9a%e5%ae%bd%e7%b1%bb%e5%9e%8b%e5%bc%80%e5%a7%8b" class="header-anchor"&gt;&lt;/a&gt;1. 优先从定宽类型开始
&lt;/h2&gt;&lt;p&gt;现代 C 接口如果使用 &lt;code&gt;&amp;lt;stdint.h&amp;gt;&lt;/code&gt;，映射最直接：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;C 类型&lt;/th&gt;
 &lt;th style="text-align: right"&gt;位宽&lt;/th&gt;
 &lt;th&gt;C# 类型&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;int8_t&lt;/code&gt; / &lt;code&gt;uint8_t&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;8&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;sbyte&lt;/code&gt; / &lt;code&gt;byte&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;int16_t&lt;/code&gt; / &lt;code&gt;uint16_t&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;16&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;short&lt;/code&gt; / &lt;code&gt;ushort&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;int32_t&lt;/code&gt; / &lt;code&gt;uint32_t&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;32&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;int&lt;/code&gt; / &lt;code&gt;uint&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;int64_t&lt;/code&gt; / &lt;code&gt;uint64_t&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;64&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;long&lt;/code&gt; / &lt;code&gt;ulong&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;通常 32&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;double&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: right"&gt;通常 64&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;double&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对于设备寄存器、协议字段和文件格式，定宽整数同时表达了范围和二进制布局。若厂商公开接口仍使用 &lt;code&gt;short&lt;/code&gt;、&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;long&lt;/code&gt;，应查目标平台 ABI 和编译器文档，不能只按名称映射。&lt;/p&gt;
&lt;h2 id="2-cc-long-不是-c-long"&gt;&lt;a href="#2-cc-long-%e4%b8%8d%e6%98%af-c-long" class="header-anchor"&gt;&lt;/a&gt;2. C/C++ long 不是 C# long
&lt;/h2&gt;&lt;p&gt;C# &lt;code&gt;long&lt;/code&gt; 固定为 64 位。C/C++ &lt;code&gt;long&lt;/code&gt; 至少 32 位，但常见 64 位 ABI 并不统一：64 位 Windows 仍是 32 位，而许多 64 位 Unix 系统是 64 位。&lt;/p&gt;
&lt;p&gt;因此跨平台 C 接口应优先导出 &lt;code&gt;int32_t&lt;/code&gt;、&lt;code&gt;int64_t&lt;/code&gt; 等定宽类型。无法修改旧 SDK 时，Windows 的原生 &lt;code&gt;long&lt;/code&gt; 通常映射为 C# &lt;code&gt;int&lt;/code&gt;；目标是 Linux/macOS 时必须按该库实际构建 ABI 重新确认，不能共享一份未经验证的声明。.NET 6+ 为这种场景提供了 &lt;code&gt;System.Runtime.InteropServices.CLong&lt;/code&gt;/&lt;code&gt;CULong&lt;/code&gt;，按目标平台自动以正确宽度承载 C &lt;code&gt;long&lt;/code&gt;/&lt;code&gt;unsigned long&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="3-指针大小跟随进程架构"&gt;&lt;a href="#3-%e6%8c%87%e9%92%88%e5%a4%a7%e5%b0%8f%e8%b7%9f%e9%9a%8f%e8%bf%9b%e7%a8%8b%e6%9e%b6%e6%9e%84" class="header-anchor"&gt;&lt;/a&gt;3. 指针大小跟随进程架构
&lt;/h2&gt;&lt;p&gt;以下类型表达的是地址或与地址同宽的整数：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;原生含义&lt;/th&gt;
 &lt;th&gt;C# 表达&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;void*&lt;/code&gt;、不透明句柄&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;nint&lt;/code&gt; / &lt;code&gt;IntPtr&lt;/code&gt;，资源句柄优先用 &lt;code&gt;SafeHandle&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;const void*&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;nint&lt;/code&gt; 或 &lt;code&gt;void*&lt;/code&gt;，同时在封装层保持只读语义&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;size_t&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;nuint&lt;/code&gt; / &lt;code&gt;UIntPtr&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;ptrdiff_t&lt;/code&gt;、&lt;code&gt;intptr_t&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;nint&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;uintptr_t&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;nuint&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;nint&lt;/code&gt; 与 &lt;code&gt;nuint&lt;/code&gt; 随进程位数变化。它们不是“任意大整数”，也不表示内存所有权；一个返回指针究竟是借用、转移还是句柄，仍要看 API 契约。&lt;/p&gt;
&lt;h2 id="4-bool-必须先认清原生定义"&gt;&lt;a href="#4-bool-%e5%bf%85%e9%a1%bb%e5%85%88%e8%ae%a4%e6%b8%85%e5%8e%9f%e7%94%9f%e5%ae%9a%e4%b9%89" class="header-anchor"&gt;&lt;/a&gt;4. bool 必须先认清原生定义
&lt;/h2&gt;&lt;p&gt;常见布尔表示至少有三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C99 &lt;code&gt;_Bool&lt;/code&gt; 或 C++ &lt;code&gt;bool&lt;/code&gt; 通常占 1 字节；&lt;/li&gt;
&lt;li&gt;Win32 &lt;code&gt;BOOL&lt;/code&gt; 是 4 字节有符号整数，零为假、非零为真；&lt;/li&gt;
&lt;li&gt;COM &lt;code&gt;VARIANT_BOOL&lt;/code&gt; 是 2 字节，&lt;code&gt;VARIANT_TRUE&lt;/code&gt; 为 &lt;code&gt;-1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;.NET 运行时封送启用时，C# &lt;code&gt;bool&lt;/code&gt; 默认按 4 字节 Win32 &lt;code&gt;BOOL&lt;/code&gt; 处理。它不能直接代表原生 C++ &lt;code&gt;bool&lt;/code&gt;。一种显式声明是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LegacyNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [DllImport(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; &amp;#34;device_sdk&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; EntryPoint = &amp;#34;device_is_ready&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; ExactSpelling = true)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [return: MarshalAs(UnmanagedType.I1)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;对于新设计的 C ABI，更稳妥的做法是导出 &lt;code&gt;uint8_t&lt;/code&gt; 或 &lt;code&gt;int32_t&lt;/code&gt;，C# 也使用 &lt;code&gt;byte&lt;/code&gt; 或 &lt;code&gt;int&lt;/code&gt;，再在封装层转换为布尔值。这样签名不依赖默认封送规则：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_is_ready&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;IsReadyRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;IsReadyRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="5-char-首先是整数编码另行约定"&gt;&lt;a href="#5-char-%e9%a6%96%e5%85%88%e6%98%af%e6%95%b4%e6%95%b0%e7%bc%96%e7%a0%81%e5%8f%a6%e8%a1%8c%e7%ba%a6%e5%ae%9a" class="header-anchor"&gt;&lt;/a&gt;5. char 首先是整数，编码另行约定
&lt;/h2&gt;&lt;p&gt;在 .NET 支持的常见目标平台上，C 的 &lt;code&gt;char&lt;/code&gt; 占 1 个 8 位字节，但它是有符号还是无符号由实现决定；C# &lt;code&gt;char&lt;/code&gt; 则是 16 位 UTF-16 代码单元。以下映射更可靠：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原生单字节数值或原始字节：&lt;code&gt;byte&lt;/code&gt; / &lt;code&gt;sbyte&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;UTF-8 文本：&lt;code&gt;byte*&lt;/code&gt;、字节缓冲区，或显式 UTF-8 字符串封送；&lt;/li&gt;
&lt;li&gt;Windows UTF-16 &lt;code&gt;wchar_t*&lt;/code&gt;：显式 UTF-16 字符串封送；&lt;/li&gt;
&lt;li&gt;Unix &lt;code&gt;wchar_t*&lt;/code&gt;：不能假设是 UTF-16，应按平台定义处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要用 C# &lt;code&gt;char&lt;/code&gt; 表达任意原生 &lt;code&gt;char&lt;/code&gt;，也不要把“ANSI”理解成一种固定的跨平台编码。&lt;/p&gt;
&lt;h2 id="6-枚举的语义和底层宽度都要固定"&gt;&lt;a href="#6-%e6%9e%9a%e4%b8%be%e7%9a%84%e8%af%ad%e4%b9%89%e5%92%8c%e5%ba%95%e5%b1%82%e5%ae%bd%e5%ba%a6%e9%83%bd%e8%a6%81%e5%9b%ba%e5%ae%9a" class="header-anchor"&gt;&lt;/a&gt;6. 枚举的语义和底层宽度都要固定
&lt;/h2&gt;&lt;p&gt;C# 枚举默认以 &lt;code&gt;int&lt;/code&gt; 为底层类型，但 C/C++ 枚举的 ABI 宽度可能受编译器、选项和枚举值范围影响。设备 SDK 若把枚举放入公开结构或函数参数，最好由 C 边界固定为 &lt;code&gt;int32_t&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;device_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define DEVICE_STATE_OFFLINE 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define DEVICE_STATE_READY 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define DEVICE_STATE_BUSY 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;DeviceState&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Offline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Ready&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Busy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;解析返回值时还要允许未知数字。原生库升级后可能新增状态，托管代码不应因为 &lt;code&gt;Enum.IsDefined&lt;/code&gt; 为假就丢失原始值。&lt;/p&gt;
&lt;h2 id="7-句柄不是普通整数"&gt;&lt;a href="#7-%e5%8f%a5%e6%9f%84%e4%b8%8d%e6%98%af%e6%99%ae%e9%80%9a%e6%95%b4%e6%95%b0" class="header-anchor"&gt;&lt;/a&gt;7. 句柄不是普通整数
&lt;/h2&gt;&lt;p&gt;厂商有时把句柄声明为 &lt;code&gt;void*&lt;/code&gt;，有时是 &lt;code&gt;uint32_t&lt;/code&gt;，还有时是结构体指针。三者不能凭名称互换：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指针型句柄用 &lt;code&gt;nint&lt;/code&gt; 或 &lt;code&gt;SafeHandle&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;明确定宽的数字 ID 用对应整数；&lt;/li&gt;
&lt;li&gt;Windows &lt;code&gt;HANDLE&lt;/code&gt; 是指针大小，不应写成固定 32 位整数；&lt;/li&gt;
&lt;li&gt;只有文档明确指定的无效值才可作为失败判断，例如空指针或 &lt;code&gt;-1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后续文章会用 &lt;code&gt;SafeHandle&lt;/code&gt; 把“何时关闭、由谁关闭、调用期间能否关闭”纳入类型系统。&lt;/p&gt;
&lt;h2 id="8-用大小断言把猜测变成测试"&gt;&lt;a href="#8-%e7%94%a8%e5%a4%a7%e5%b0%8f%e6%96%ad%e8%a8%80%e6%8a%8a%e7%8c%9c%e6%b5%8b%e5%8f%98%e6%88%90%e6%b5%8b%e8%af%95" class="header-anchor"&gt;&lt;/a&gt;8. 用大小断言把猜测变成测试
&lt;/h2&gt;&lt;p&gt;对于公开结构，最好让原生侧和托管侧各自输出或断言大小、对齐和偏移。纯托管的基础检查可以写成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.CompilerServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;Process pointer size: {IntPtr.Size}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;int: {Unsafe.SizeOf&amp;lt;int&amp;gt;()}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;long: {Unsafe.SizeOf&amp;lt;long&amp;gt;()}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;nint: {Unsafe.SizeOf&amp;lt;nint&amp;gt;()}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这只能证明托管一侧，不能替代用同一套头文件和编译选项得到的原生 &lt;code&gt;sizeof&lt;/code&gt;、&lt;code&gt;alignof&lt;/code&gt; 与 &lt;code&gt;offsetof&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;互操作类型映射的原则是按 ABI 含义和位宽选择类型，而不是按名称翻译。定宽整数、显式布尔表示、指针大小整数和清晰的句柄契约，可以消除大量“在一台机器上恰好能跑”的问题。&lt;/p&gt;
&lt;p&gt;下一篇进入组合类型：结构体的字段顺序、填充、对齐、联合体和 blittable 边界。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/best-practices" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native interoperability best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/marshalling-data-with-platform-invoke" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Marshalling data with Platform Invoke&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/built-in-types" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Built-in types（C# reference）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/integral-numeric-types#native-sized-integers" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native-sized integer types&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>C# 原生互操作与设备 SDK（一）：P/Invoke、DllImport 与 LibraryImport</title><link>https://www.jiwei.space/posts/equipment/native-interop/01-pinvoke-libraryimport/</link><pubDate>Sat, 18 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/native-interop/01-pinvoke-libraryimport/</guid><description>&lt;p&gt;设备厂商常把 SDK 交付为 C/C++ 头文件、动态库和少量示例。C# 程序不能仅凭函数名调用这些二进制接口：双方还必须对参数布局、调用约定、字符串编码、资源所有权和错误模型达成完全一致的约定，这份约定就是应用二进制接口（Application Binary Interface，ABI）。&lt;/p&gt;
&lt;p&gt;本文以 .NET 10 为环境，从一个最小 C 接口出发，说明 P/Invoke 的工作模型，以及 &lt;code&gt;LibraryImport&lt;/code&gt; 和 &lt;code&gt;DllImport&lt;/code&gt; 各自适合什么场景；运行时手动选择库文件的加载方式见本系列《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/native-interop/05-loading-diagnostics/" &gt;C# 原生互操作与设备 SDK（五）：调用约定、位数与 DLL 加载诊断&lt;/a&gt;》。业务侧的硬件抽象方法见《&lt;a class="link" href="https://www.jiwei.space/posts/equipment/equipment-architecture/02-hal-adapters/" &gt;设备软件架构与控制模型（二）：硬件抽象层与设备适配器&lt;/a&gt;》；本系列聚焦适配器下面的二进制边界。&lt;/p&gt;
&lt;h2 id="1-pinvoke-连接的是-abi不是源代码"&gt;&lt;a href="#1-pinvoke-%e8%bf%9e%e6%8e%a5%e7%9a%84%e6%98%af-abi%e4%b8%8d%e6%98%af%e6%ba%90%e4%bb%a3%e7%a0%81" class="header-anchor"&gt;&lt;/a&gt;1. P/Invoke 连接的是 ABI，不是源代码
&lt;/h2&gt;&lt;p&gt;假设厂商头文件给出以下 C API：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdint.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#ifdef _WIN32
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define DEVICE_API __declspec(dllimport)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define DEVICE_API
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;device_handle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DEVICE_API&lt;/span&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_handle&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DEVICE_API&lt;/span&gt; &lt;span class="kt"&gt;int32_t&lt;/span&gt; &lt;span class="nf"&gt;device_get_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_handle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out_mm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DEVICE_API&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;device_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_handle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;device_open&lt;/code&gt; 的托管声明不能只做到“看起来像”。必须逐项回答：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;动态库的逻辑名称和导出符号是什么；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int32_t&lt;/code&gt; 是否映射为 32 位有符号整数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;device_handle&lt;/code&gt; 是整数、指针还是需要释放的资源；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;device_handle*&lt;/code&gt; 是输出一个句柄，还是传入句柄数组；&lt;/li&gt;
&lt;li&gt;返回值是 SDK 状态码，还是操作系统最后错误；&lt;/li&gt;
&lt;li&gt;Windows 导出是否使用了非默认调用约定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;任何一项不匹配，都可能表现为错误值、找不到入口点，甚至栈或堆损坏。&lt;/p&gt;
&lt;h2 id="2-net-7-及以上优先使用-libraryimport"&gt;&lt;a href="#2-net-7-%e5%8f%8a%e4%bb%a5%e4%b8%8a%e4%bc%98%e5%85%88%e4%bd%bf%e7%94%a8-libraryimport" class="header-anchor"&gt;&lt;/a&gt;2. .NET 7 及以上优先使用 LibraryImport
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;LibraryImportAttribute&lt;/code&gt; 让源生成器在编译期生成封送代码。声明方法必须是 &lt;code&gt;static partial&lt;/code&gt;，包含它的类型也要是 &lt;code&gt;partial&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LibraryName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;device_sdk&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(LibraryName, EntryPoint = &amp;#34;device_open&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(LibraryName, EntryPoint = &amp;#34;device_get_position&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;GetPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;millimeters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LibraryImport(LibraryName, EntryPoint = &amp;#34;device_close&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;库名不写扩展名时，运行时会按平台尝试相应变体，例如 Windows 的 &lt;code&gt;.dll&lt;/code&gt;、Linux 的 &lt;code&gt;.so&lt;/code&gt; 和 macOS 的 &lt;code&gt;.dylib&lt;/code&gt;；Unix 平台还可能尝试 &lt;code&gt;lib&lt;/code&gt; 前缀。绝对路径则按原样处理，不会追加这些变体。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LibraryImport&lt;/code&gt; 的优势不是让错误签名变安全，而是让许多封送步骤可在编译期生成，便于分析器检查，也更适合 Native AOT。使用它需要在工程文件中设置 &lt;code&gt;&amp;lt;AllowUnsafeBlocks&amp;gt;true&amp;lt;/AllowUnsafeBlocks&amp;gt;&lt;/code&gt;，否则构建会报 SYSLIB1062 错误。头文件仍然是签名、布局和调用约定的最终依据。&lt;/p&gt;
&lt;h2 id="3-dllimport-仍有适用场景"&gt;&lt;a href="#3-dllimport-%e4%bb%8d%e6%9c%89%e9%80%82%e7%94%a8%e5%9c%ba%e6%99%af" class="header-anchor"&gt;&lt;/a&gt;3. DllImport 仍有适用场景
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;DllImportAttribute&lt;/code&gt; 由运行时建立 P/Invoke 存根，在旧版 .NET、源生成器尚不支持的封送方式，或分析器明确建议保留时仍然合理：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LegacyNative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [DllImport(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; &amp;#34;device_sdk&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; EntryPoint = &amp;#34;device_get_position&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; ExactSpelling = true)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;GetPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;millimeters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;面向 .NET 7 及以上的新代码，可启用分析器并关注 &lt;code&gt;SYSLIB1054&lt;/code&gt;：它会标出可用源生成器改写的 &lt;code&gt;DllImport&lt;/code&gt;；迁移过程中若封送配置不被源生成器支持，SYSLIB1051/SYSLIB1052 会明确指出，此时保留 &lt;code&gt;DllImport&lt;/code&gt; 即可。不要为了形式统一而忽略分析器给出的限制。&lt;/p&gt;
&lt;h2 id="4-sdk-状态码和系统最后错误是两条通道"&gt;&lt;a href="#4-sdk-%e7%8a%b6%e6%80%81%e7%a0%81%e5%92%8c%e7%b3%bb%e7%bb%9f%e6%9c%80%e5%90%8e%e9%94%99%e8%af%af%e6%98%af%e4%b8%a4%e6%9d%a1%e9%80%9a%e9%81%93" class="header-anchor"&gt;&lt;/a&gt;4. SDK 状态码和系统最后错误是两条通道
&lt;/h2&gt;&lt;p&gt;许多设备 SDK 用返回整数表示自身错误，例如 &lt;code&gt;0&lt;/code&gt; 成功、负数失败。这与 Windows &lt;code&gt;GetLastError&lt;/code&gt; 或 Unix &lt;code&gt;errno&lt;/code&gt; 不是一回事：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;ReadPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceSdkException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;device_get_position&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceSdkException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;{operation} failed with SDK status {status}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;只有头文件明确说明函数通过系统最后错误报告细节时，才设置 &lt;code&gt;SetLastError = true&lt;/code&gt;，并在判断失败后立即读取缓存值：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;[LibraryImport(&amp;#34;device_sdk&amp;#34;, EntryPoint = &amp;#34;device_wait&amp;#34;, SetLastError = true)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;internal&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nint&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt; &lt;span class="n"&gt;timeoutMilliseconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;调用方沿用本篇&amp;quot;0 成功、非 0 失败&amp;quot;的约定：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceNative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nativeError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetLastPInvokeError&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;device_wait failed: {nativeError}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里沿用示例约定，把非零返回值视为失败；&lt;code&gt;GetLastPInvokeError&lt;/code&gt; 提供的是系统错误细节，不能替代 SDK 状态码。真实项目必须照抄厂商约定，不能根据 Win32 API 的习惯猜测。&lt;/p&gt;
&lt;h2 id="5-把原生声明限制在最薄的一层"&gt;&lt;a href="#5-%e6%8a%8a%e5%8e%9f%e7%94%9f%e5%a3%b0%e6%98%8e%e9%99%90%e5%88%b6%e5%9c%a8%e6%9c%80%e8%96%84%e7%9a%84%e4%b8%80%e5%b1%82" class="header-anchor"&gt;&lt;/a&gt;5. 把原生声明限制在最薄的一层
&lt;/h2&gt;&lt;p&gt;推荐把代码分成三层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;DeviceNative&lt;/code&gt; 精确复刻头文件，不加入业务语义；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DeviceSession&lt;/code&gt; 管理句柄、错误转换、单位和线程限制；&lt;/li&gt;
&lt;li&gt;上层 &lt;code&gt;IDevice&lt;/code&gt;、&lt;code&gt;IAxis&lt;/code&gt; 等能力接口服务于流程和测试。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样升级 SDK 时，可以先用头文件和导出表审查第一层，再验证适配器，不必让 &lt;code&gt;nint&lt;/code&gt;、错误码和厂商枚举扩散到界面与业务流程。&lt;/p&gt;
&lt;h2 id="6-首次接入按故障类型定位"&gt;&lt;a href="#6-%e9%a6%96%e6%ac%a1%e6%8e%a5%e5%85%a5%e6%8c%89%e6%95%85%e9%9a%9c%e7%b1%bb%e5%9e%8b%e5%ae%9a%e4%bd%8d" class="header-anchor"&gt;&lt;/a&gt;6. 首次接入按故障类型定位
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;异常或现象&lt;/th&gt;
 &lt;th&gt;常见原因&lt;/th&gt;
 &lt;th&gt;首要检查&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;DllNotFoundException&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;主库不存在或其依赖缺失&lt;/td&gt;
 &lt;td&gt;部署目录、依赖库、搜索路径&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;EntryPointNotFoundException&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;导出名、大小写或名称修饰不匹配&lt;/td&gt;
 &lt;td&gt;头文件和实际导出表&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;BadImageFormatException&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;进程与库架构不匹配，或文件并非有效动态库&lt;/td&gt;
 &lt;td&gt;x86/x64/Arm64 与文件格式&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;返回值偶发错误&lt;/td&gt;
 &lt;td&gt;参数类型、布局、编码或生命周期不匹配&lt;/td&gt;
 &lt;td&gt;原生签名逐字段对照&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;调用后崩溃&lt;/td&gt;
 &lt;td&gt;调用约定、缓冲区、回调或所有权错误&lt;/td&gt;
 &lt;td&gt;最小化签名并启用原生调试&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;先用只含一次调用的控制台程序建立最小闭环，再接入 UI、DI 和状态机。能够加载动态库只证明加载阶段成功，不证明 ABI 声明正确。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;P/Invoke 的核心是让托管声明与原生 ABI 精确一致。现代 .NET 项目优先考虑 &lt;code&gt;LibraryImport&lt;/code&gt;，在不支持的场景保留 &lt;code&gt;DllImport&lt;/code&gt;，需要运行时选择库或符号时再使用 &lt;code&gt;NativeLibrary&lt;/code&gt;。无论入口方式如何变化，头文件、实际导出和资源契约始终是事实来源。&lt;/p&gt;
&lt;p&gt;下一篇将逐一处理最容易写错的类型：整数、布尔、枚举、句柄和指针。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/pinvoke" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Platform Invoke（P/Invoke）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/pinvoke-source-generation" target="_blank" rel="noopener"
 &gt;Microsoft Learn：P/Invoke source generation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/best-practices" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native interoperability best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/native-interop/native-library-loading" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Native library loading&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备软件架构与控制模型（八）：日志、诊断、审计与升级</title><link>https://www.jiwei.space/posts/equipment/equipment-architecture/08-diagnostics-upgrade/</link><pubDate>Fri, 17 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/equipment-architecture/08-diagnostics-upgrade/</guid><description>&lt;p&gt;设备交付以后，开发者面对的通常不是一个可调试的异常，而是一句话：“昨天夜班偶尔停了一次，重启后好了。”如果软件没有把命令、状态、设备观测和配置版本关联起来，现场只能靠猜；如果升级又缺少兼容检查和回退路径，一次修复还可能制造更大的停机。&lt;/p&gt;
&lt;p&gt;本系列最后一篇把可维护性视为架构能力：日志、指标、追踪、报警和审计各自记录不同事实，诊断包把证据安全地带离现场，升级流程则让软件、Recipe、固件和数据结构能够协同演进。&lt;/p&gt;
&lt;h2 id="1-五类信息各司其职"&gt;&lt;a href="#1-%e4%ba%94%e7%b1%bb%e4%bf%a1%e6%81%af%e5%90%84%e5%8f%b8%e5%85%b6%e8%81%8c" class="header-anchor"&gt;&lt;/a&gt;1. 五类信息各司其职
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;类型&lt;/th&gt;
 &lt;th&gt;回答的问题&lt;/th&gt;
 &lt;th&gt;典型内容&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;日志（Log）&lt;/td&gt;
 &lt;td&gt;某个时刻发生了什么&lt;/td&gt;
 &lt;td&gt;命令接收、设备响应、异常与上下文&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;指标（Metric）&lt;/td&gt;
 &lt;td&gt;一段时间内系统怎样变化&lt;/td&gt;
 &lt;td&gt;周期时间、失败数、队列深度、温度趋势&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;追踪（Trace）&lt;/td&gt;
 &lt;td&gt;一次操作经过了哪些组件&lt;/td&gt;
 &lt;td&gt;UI 命令 → 流程 → 轴/相机 → 数据保存&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;报警（Alarm）&lt;/td&gt;
 &lt;td&gt;现在需要人采取什么行动&lt;/td&gt;
 &lt;td&gt;联锁未满足、位置未知、关键设备失联&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;审计（Audit）&lt;/td&gt;
 &lt;td&gt;谁在何时改变了受控对象&lt;/td&gt;
 &lt;td&gt;Recipe 发布、权限变更、维护复位、升级&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;日志不是审计日志，异常也不自动成为报警。把所有内容写进一个文本文件，会同时损害检索、保留策略和访问控制。&lt;/p&gt;
&lt;p&gt;.NET 原生提供 &lt;code&gt;ILogger&lt;/code&gt;、&lt;code&gt;System.Diagnostics.Metrics&lt;/code&gt; 和 &lt;code&gt;ActivitySource&lt;/code&gt;；OpenTelemetry 可以收集这些信号并导出到不同后端。是否部署集中式平台取决于现场网络和运维条件，但应用内部的事件结构与关联方式应尽早统一。半导体设备侧分级报警与可观测性落地的领域实践见《&lt;a class="link" href="https://www.jiwei.space/posts/semiconductor/equipment-software/07-observability/" &gt;半导体设备软件（七）：日志、报警与可观测性&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="2-从一次操作的关联-id-开始"&gt;&lt;a href="#2-%e4%bb%8e%e4%b8%80%e6%ac%a1%e6%93%8d%e4%bd%9c%e7%9a%84%e5%85%b3%e8%81%94-id-%e5%bc%80%e5%a7%8b" class="header-anchor"&gt;&lt;/a&gt;2. 从一次操作的关联 ID 开始
&lt;/h2&gt;&lt;p&gt;每个外部命令生成或接收一个 &lt;code&gt;CorrelationId&lt;/code&gt;，长流程再分配 &lt;code&gt;RunId&lt;/code&gt; 和步骤 ID。它们应贯穿日志、状态转换、设备调用和结果数据：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CorrelationId = 01J5Z...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RunId = RUN-20260817-0042
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CommandId = CMD-018827
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Step = Capture.TopCamera
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Device = Camera.Top
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Recipe = Product-A-Scan@17
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;关联 ID 用于串起证据，不代表鉴权或幂等。外部传入的值要校验长度和字符范围，日志中也不能无条件信任用户输入。&lt;/p&gt;
&lt;p&gt;结构化日志保留字段语义，便于查询和聚合：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EquipmentLog&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LoggerMessage(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; EventId = 2101,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; Level = LogLevel.Information,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; Message = &amp;#34;Command {CommandId} started on {DeviceId} for run {RunId}&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;CommandStarted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; [LoggerMessage(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; EventId = 2102,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; Level = LogLevel.Error,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt; Message = &amp;#34;Command {CommandId} failed with fault {FaultKind} ({VendorCode})&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;CommandFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;faultKind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string?&lt;/span&gt; &lt;span class="n"&gt;vendorCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;LoggerMessage&lt;/code&gt; 源生成器可以在编译时生成高效日志代码并提供诊断。事件 ID 要稳定，消息模板字段名也应保持一致；否则仪表盘和现场脚本会随着文字修改失效。&lt;/p&gt;
&lt;h2 id="3-日志要能解释状态转换"&gt;&lt;a href="#3-%e6%97%a5%e5%bf%97%e8%a6%81%e8%83%bd%e8%a7%a3%e9%87%8a%e7%8a%b6%e6%80%81%e8%bd%ac%e6%8d%a2" class="header-anchor"&gt;&lt;/a&gt;3. 日志要能解释状态转换
&lt;/h2&gt;&lt;p&gt;有用的状态日志至少包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;旧状态、新状态和触发事件；&lt;/li&gt;
&lt;li&gt;命令、运行和设备标识；&lt;/li&gt;
&lt;li&gt;守卫条件拒绝的明确原因；&lt;/li&gt;
&lt;li&gt;设备返回的原始码和稳定故障分类；&lt;/li&gt;
&lt;li&gt;Recipe、校准、软件和固件版本；&lt;/li&gt;
&lt;li&gt;使用单调计时测得的持续时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要在高频循环中无条件记录每次采样。可以把原始数据送入专门数据管线，把日志保留给状态变化和异常；趋势使用指标，特定故障前后的短窗口再通过环形缓冲保存。日志量控制不能简单丢弃所有 &lt;code&gt;Information&lt;/code&gt;，应保证命令开始、状态转换和失败证据能够关联。&lt;/p&gt;
&lt;p&gt;墙上时间用于跨系统对齐和审计，持续时间则应使用单调计时来源，例如 &lt;code&gt;Stopwatch&lt;/code&gt; 或 &lt;code&gt;TimeProvider.GetTimestamp()&lt;/code&gt;，避免系统时间校准造成负耗时。&lt;/p&gt;
&lt;h2 id="4-指标关注可行动的趋势"&gt;&lt;a href="#4-%e6%8c%87%e6%a0%87%e5%85%b3%e6%b3%a8%e5%8f%af%e8%a1%8c%e5%8a%a8%e7%9a%84%e8%b6%8b%e5%8a%bf" class="header-anchor"&gt;&lt;/a&gt;4. 指标关注可行动的趋势
&lt;/h2&gt;&lt;p&gt;设备软件常见指标包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令和流程的成功数、失败数与耗时分布；&lt;/li&gt;
&lt;li&gt;当前命令队列深度、丢弃或等待次数；&lt;/li&gt;
&lt;li&gt;设备断连次数和恢复耗时；&lt;/li&gt;
&lt;li&gt;报警发生次数、持续时间和重复量；&lt;/li&gt;
&lt;li&gt;数据采集速率、处理积压与磁盘剩余空间；&lt;/li&gt;
&lt;li&gt;设备温度、压力等健康信号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;标签维度必须受控。把 &lt;code&gt;RunId&lt;/code&gt;、序列号或完整错误消息作为指标标签，会产生近乎无限的时间序列；这些高基数字段应该进入日志或追踪。指标适合发现“最近一小时相机断连增加”，日志再回答具体是哪几次。&lt;/p&gt;
&lt;p&gt;阈值不要凭经验写成通用常数。先采集基线，再结合设备规格、工艺窗口和响应流程定义报警条件，并记录适用版本。&lt;/p&gt;
&lt;h2 id="5-诊断包保存足够但不过量的证据"&gt;&lt;a href="#5-%e8%af%8a%e6%96%ad%e5%8c%85%e4%bf%9d%e5%ad%98%e8%b6%b3%e5%a4%9f%e4%bd%86%e4%b8%8d%e8%bf%87%e9%87%8f%e7%9a%84%e8%af%81%e6%8d%ae" class="header-anchor"&gt;&lt;/a&gt;5. 诊断包保存足够但不过量的证据
&lt;/h2&gt;&lt;p&gt;现场导出的诊断包可以包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;软件、操作系统、驱动、固件和硬件标识；&lt;/li&gt;
&lt;li&gt;当前状态快照与最近状态转换；&lt;/li&gt;
&lt;li&gt;生效的 Recipe/配置/校准版本及摘要；&lt;/li&gt;
&lt;li&gt;故障前后受限时间窗口内的日志与关键遥测；&lt;/li&gt;
&lt;li&gt;活动报警、原始错误码和通信统计；&lt;/li&gt;
&lt;li&gt;存储、内存和线程等运行健康信息；&lt;/li&gt;
&lt;li&gt;清单文件、生成时间和文件摘要。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;默认不要包含凭据、私钥、访问令牌、完整客户数据和无限量原始图像。导出前执行脱敏，诊断包设置访问权限和保留期限；跨组织传输时按双方的数据规则处理。生成诊断包本身也要有容量上限，避免磁盘已满时再次写入大量数据。&lt;/p&gt;
&lt;h2 id="6-审计记录受控变更"&gt;&lt;a href="#6-%e5%ae%a1%e8%ae%a1%e8%ae%b0%e5%bd%95%e5%8f%97%e6%8e%a7%e5%8f%98%e6%9b%b4" class="header-anchor"&gt;&lt;/a&gt;6. 审计记录受控变更
&lt;/h2&gt;&lt;p&gt;以下动作通常值得审计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;登录、角色和权限变更；&lt;/li&gt;
&lt;li&gt;Recipe 的创建、审核、发布、激活和停用；&lt;/li&gt;
&lt;li&gt;校准与设备常数更新；&lt;/li&gt;
&lt;li&gt;报警禁用、阈值和优先级修改；&lt;/li&gt;
&lt;li&gt;维护模式、强制输出和故障复位；&lt;/li&gt;
&lt;li&gt;软件、驱动和固件升级。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;审计事件记录操作者身份、动作、对象、旧值/新值或版本、时间、理由和结果。仅写“修改成功”无法还原变化。审计存储应限制修改和删除，并将时间同步异常本身纳入监控；具体保存期限由行业、合同和组织制度决定，不能从通用架构直接给出一个年限。&lt;/p&gt;
&lt;h2 id="7-升级是一项受控设备变更"&gt;&lt;a href="#7-%e5%8d%87%e7%ba%a7%e6%98%af%e4%b8%80%e9%a1%b9%e5%8f%97%e6%8e%a7%e8%ae%be%e5%a4%87%e5%8f%98%e6%9b%b4" class="header-anchor"&gt;&lt;/a&gt;7. 升级是一项受控设备变更
&lt;/h2&gt;&lt;p&gt;一次升级可能同时改变应用、数据库、Recipe Schema、厂商 SDK 和控制器固件。升级包至少应明确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;来源、版本、摘要或数字签名；&lt;/li&gt;
&lt;li&gt;支持的操作系统、架构、驱动和固件组合；&lt;/li&gt;
&lt;li&gt;配置与数据结构的迁移路径；&lt;/li&gt;
&lt;li&gt;升级前检查、预计停机和空间需求；&lt;/li&gt;
&lt;li&gt;安装后健康检查与验收步骤；&lt;/li&gt;
&lt;li&gt;可回退组件、回退前提和不可逆步骤。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“复制新文件覆盖旧目录”会让旧 DLL、配置和新程序混杂。更稳健的方式是部署到独立版本目录，停止控制入口，备份受控数据，执行迁移，再切换启动目标。切换是否能做到原子性取决于操作系统和部署机制，不能只凭重命名目录就假定整个升级事务原子。&lt;/p&gt;
&lt;p&gt;数据库或固件迁移可能不可逆。此时所谓回退不能只恢复旧可执行文件，还必须有向前修复、数据备份恢复或整机维护方案。升级前应阻止新任务，等待当前任务到安全检查点，并记录设备实际状态。&lt;/p&gt;
&lt;h2 id="8-用健康检查决定是否接管设备"&gt;&lt;a href="#8-%e7%94%a8%e5%81%a5%e5%ba%b7%e6%a3%80%e6%9f%a5%e5%86%b3%e5%ae%9a%e6%98%af%e5%90%a6%e6%8e%a5%e7%ae%a1%e8%ae%be%e5%a4%87" class="header-anchor"&gt;&lt;/a&gt;8. 用健康检查决定是否接管设备
&lt;/h2&gt;&lt;p&gt;进程启动成功不等于升级成功。自动验证至少包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读取应用版本和部署清单；&lt;/li&gt;
&lt;li&gt;加载并验证配置；&lt;/li&gt;
&lt;li&gt;检查数据结构版本；&lt;/li&gt;
&lt;li&gt;建立设备会话并核对身份/固件；&lt;/li&gt;
&lt;li&gt;验证关键联锁和只读状态；&lt;/li&gt;
&lt;li&gt;在允许条件下执行受控自检；&lt;/li&gt;
&lt;li&gt;确认日志、数据和磁盘路径可写；&lt;/li&gt;
&lt;li&gt;最后才允许进入 &lt;code&gt;Ready&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;验证失败时，系统应保持不可生产状态并输出可诊断原因，而不是为了“服务已启动”强行忽略。对无人值守设备，还要验证 Windows Service、看门狗或外部监管器的重启策略不会形成无限崩溃循环。&lt;/p&gt;
&lt;h2 id="9-可维护性从设计阶段开始"&gt;&lt;a href="#9-%e5%8f%af%e7%bb%b4%e6%8a%a4%e6%80%a7%e4%bb%8e%e8%ae%be%e8%ae%a1%e9%98%b6%e6%ae%b5%e5%bc%80%e5%a7%8b" class="header-anchor"&gt;&lt;/a&gt;9. 可维护性从设计阶段开始
&lt;/h2&gt;&lt;p&gt;日志字段、故障分类、配置摘要、命令 ID 和版本清单都依赖前几篇建立的边界。若 UI 直接调用 SDK、流程没有唯一 RunId、Recipe 可以运行中被修改，再先进的日志平台也只能收集互相矛盾的信息。&lt;/p&gt;
&lt;p&gt;可以用一次“离线故障演练”检验体系：不给开发者远程调试权限，只提供诊断包、操作时间和现象描述，看能否还原使用的版本、命令路径、设备状态、首个根因和恢复动作。无法回答的问题，就是下一轮需要补充的可观测性需求。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;日志记录离散事实，指标显示趋势，追踪串起一次操作，报警要求人采取行动，审计保护受控变更。诊断包把这些证据以安全、有限的方式带离现场；升级则是一条包含兼容检查、迁移、验证和回退边界的设备变更流程。&lt;/p&gt;
&lt;p&gt;至此，系列从职责边界出发，依次建立 HAL、生命周期、状态机与队列、Recipe、故障恢复、测试和可维护性闭环。它们共同指向同一个原则：上位机架构的核心不是界面有多少功能，而是每个命令、状态、参数、故障和资源都有清晰且可验证的所有者。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/diagnostics/observability-with-otel" target="_blank" rel="noopener"
 &gt;Microsoft Learn：.NET observability with OpenTelemetry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/extensions/logging/source-generation" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Compile-time logging source generation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/diagnostics/metrics-instrumentation" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Metrics instrumentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/extensions/data-redaction" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Data redaction in .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://csrc.nist.gov/pubs/sp/800/128/upd1/final" target="_blank" rel="noopener"
 &gt;NIST SP 800-128：Guide for Security-Focused Configuration Management&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备软件架构与控制模型（七）：模拟器、集成测试与 HIL</title><link>https://www.jiwei.space/posts/equipment/equipment-architecture/07-simulation-hil/</link><pubDate>Thu, 16 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/equipment-architecture/07-simulation-hil/</guid><description>&lt;p&gt;设备软件测试最现实的困难是硬件稀缺：开发机旁没有整机，实验设备不能随时制造故障，运动和加热又会让测试变慢甚至带来风险。结果往往是单元测试只覆盖工具类，真正重要的状态机、恢复流程和设备协同只能等到现场联调。&lt;/p&gt;
&lt;p&gt;解决办法不是用 Mock 把所有 SDK 调用设成成功，而是建立逐级增加真实性的测试体系：纯逻辑模型验证规则，行为模拟器提供时间和故障，协议回放保护边界，最后用 Hardware-in-the-Loop（HIL）验证真实接口、时序和接线。&lt;/p&gt;
&lt;h2 id="1-按风险建立测试梯度"&gt;&lt;a href="#1-%e6%8c%89%e9%a3%8e%e9%99%a9%e5%bb%ba%e7%ab%8b%e6%b5%8b%e8%af%95%e6%a2%af%e5%ba%a6" class="header-anchor"&gt;&lt;/a&gt;1. 按风险建立测试梯度
&lt;/h2&gt;&lt;p&gt;设备软件可以采用以下测试层次：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;层次&lt;/th&gt;
 &lt;th&gt;主要对象&lt;/th&gt;
 &lt;th&gt;速度与环境&lt;/th&gt;
 &lt;th&gt;擅长发现的问题&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;纯逻辑测试&lt;/td&gt;
 &lt;td&gt;转换函数、校验器、计算模型&lt;/td&gt;
 &lt;td&gt;毫秒级，无 I/O&lt;/td&gt;
 &lt;td&gt;状态规则、边界值、算法错误&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;组件测试&lt;/td&gt;
 &lt;td&gt;流程执行器 + Fake/Simulator&lt;/td&gt;
 &lt;td&gt;快，可并行&lt;/td&gt;
 &lt;td&gt;超时、取消、恢复和资源竞争&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;协议/契约测试&lt;/td&gt;
 &lt;td&gt;适配器 + 模拟服务或回放&lt;/td&gt;
 &lt;td&gt;中等&lt;/td&gt;
 &lt;td&gt;报文解析、兼容性、错误映射&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;软件集成测试&lt;/td&gt;
 &lt;td&gt;完整进程 + 虚拟设备&lt;/td&gt;
 &lt;td&gt;中等&lt;/td&gt;
 &lt;td&gt;DI、配置、持久化、进程生命周期&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;HIL&lt;/td&gt;
 &lt;td&gt;上位机 + 真实控制器/仿真负载&lt;/td&gt;
 &lt;td&gt;慢，受实验台约束&lt;/td&gt;
 &lt;td&gt;驱动、接线、时序和真实固件差异&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;整机验收&lt;/td&gt;
 &lt;td&gt;完整设备与受控工况&lt;/td&gt;
 &lt;td&gt;最慢&lt;/td&gt;
 &lt;td&gt;端到端功能、性能和安全验证&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;层次越高越接近现场，但覆盖组合越困难。HIL 不应替代低层测试；它应该聚焦只有真实接口才能发现的风险，而不是重复一万组 Recipe 边界值。&lt;/p&gt;
&lt;h2 id="2-fakestubmock-和-simulator-各有用途"&gt;&lt;a href="#2-fakestubmock-%e5%92%8c-simulator-%e5%90%84%e6%9c%89%e7%94%a8%e9%80%94" class="header-anchor"&gt;&lt;/a&gt;2. Fake、Stub、Mock 和 Simulator 各有用途
&lt;/h2&gt;&lt;p&gt;这些测试替身解决的问题不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stub 为特定输入返回预设结果，适合简单分支；&lt;/li&gt;
&lt;li&gt;Mock 重点验证交互是否发生，适合边界协作；&lt;/li&gt;
&lt;li&gt;Fake 是可工作的轻量实现，例如内存仓储；&lt;/li&gt;
&lt;li&gt;Simulator 模拟状态随命令和时间变化，并能够注入设备故障。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设备流程通常更需要 Simulator。一个始终立即成功的 &lt;code&gt;IAxis&lt;/code&gt; 无法发现“移动尚未完成就开始采集”、取消后轴继续移动、回零前误用绝对坐标等问题。&lt;/p&gt;
&lt;p&gt;下面的最小模拟轴保留了位置、运动耗时和故障注入：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;span class="lnt"&gt;58
&lt;/span&gt;&lt;span class="lnt"&gt;59
&lt;/span&gt;&lt;span class="lnt"&gt;60
&lt;/span&gt;&lt;span class="lnt"&gt;61
&lt;/span&gt;&lt;span class="lnt"&gt;62
&lt;/span&gt;&lt;span class="lnt"&gt;63
&lt;/span&gt;&lt;span class="lnt"&gt;64
&lt;/span&gt;&lt;span class="lnt"&gt;65
&lt;/span&gt;&lt;span class="lnt"&gt;66
&lt;/span&gt;&lt;span class="lnt"&gt;67
&lt;/span&gt;&lt;span class="lnt"&gt;68
&lt;/span&gt;&lt;span class="lnt"&gt;69
&lt;/span&gt;&lt;span class="lnt"&gt;70
&lt;/span&gt;&lt;span class="lnt"&gt;71
&lt;/span&gt;&lt;span class="lnt"&gt;72
&lt;/span&gt;&lt;span class="lnt"&gt;73
&lt;/span&gt;&lt;span class="lnt"&gt;74
&lt;/span&gt;&lt;span class="lnt"&gt;75
&lt;/span&gt;&lt;span class="lnt"&gt;76
&lt;/span&gt;&lt;span class="lnt"&gt;77
&lt;/span&gt;&lt;span class="lnt"&gt;78
&lt;/span&gt;&lt;span class="lnt"&gt;79
&lt;/span&gt;&lt;span class="lnt"&gt;80
&lt;/span&gt;&lt;span class="lnt"&gt;81
&lt;/span&gt;&lt;span class="lnt"&gt;82
&lt;/span&gt;&lt;span class="lnt"&gt;83
&lt;/span&gt;&lt;span class="lnt"&gt;84
&lt;/span&gt;&lt;span class="lnt"&gt;85
&lt;/span&gt;&lt;span class="lnt"&gt;86
&lt;/span&gt;&lt;span class="lnt"&gt;87
&lt;/span&gt;&lt;span class="lnt"&gt;88
&lt;/span&gt;&lt;span class="lnt"&gt;89
&lt;/span&gt;&lt;span class="lnt"&gt;90
&lt;/span&gt;&lt;span class="lnt"&gt;91
&lt;/span&gt;&lt;span class="lnt"&gt;92
&lt;/span&gt;&lt;span class="lnt"&gt;93
&lt;/span&gt;&lt;span class="lnt"&gt;94
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SimulatedAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeProvider&lt;/span&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAxis&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;_sync&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Millimeters&lt;/span&gt; &lt;span class="n"&gt;_position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_activeMove&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;FailNextMove&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AxisSnapshot&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;AxisSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;IsMoving&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_activeMove&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;IsServoOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PositiveLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;NegativeLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;FaultCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;MoveAbsoluteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Millimeters&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;MillimetersPerSecond&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FailNextMove&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;FailNextMove&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceFaultException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;DriveFault&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;SIM-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;moveCancellation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateLinkedTokenSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_activeMove&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;moveCancellation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;The axis is already moving.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_activeMove&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;moveCancellation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;moveCancellation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReferenceEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_activeMove&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;moveCancellation&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_activeMove&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;moveCancellation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;StopAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;AxisStopMode&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_activeMove&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这是教学版：它会阻止并发移动，并把停止简化为取消等待、保持起点位置，并没有模拟加速度、中间位置和停止距离。模拟器不是越逼真越好，而是要忠实实现测试关注的契约；未实现的行为应明确说明，避免团队把它误当数字孪生。&lt;/p&gt;
&lt;h2 id="3-把时间变成可控制依赖"&gt;&lt;a href="#3-%e6%8a%8a%e6%97%b6%e9%97%b4%e5%8f%98%e6%88%90%e5%8f%af%e6%8e%a7%e5%88%b6%e4%be%9d%e8%b5%96" class="header-anchor"&gt;&lt;/a&gt;3. 把时间变成可控制依赖
&lt;/h2&gt;&lt;p&gt;真实等待会让超时和重试测试又慢又不稳定。&lt;code&gt;.NET 8+&lt;/code&gt; 内置 &lt;code&gt;TimeProvider&lt;/code&gt;，&lt;code&gt;Microsoft.Extensions.TimeProvider.Testing&lt;/code&gt; 包提供 &lt;code&gt;FakeTimeProvider&lt;/code&gt;，可以在测试中推进虚拟时间。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Time.Testing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;FakeTimeProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SimulatedAxis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;moving&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MoveAbsoluteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Millimeters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;MillimetersPerSecond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Advance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;moving&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;AxisSnapshot&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActualPosition&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Millimeters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Simulation result is incorrect.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;包名是 &lt;code&gt;Microsoft.Extensions.TimeProvider.Testing&lt;/code&gt;，命名空间是 &lt;code&gt;Microsoft.Extensions.Time.Testing&lt;/code&gt;，两者不要混淆。虚拟时间只能控制使用同一个 &lt;code&gt;TimeProvider&lt;/code&gt; 的代码；若业务中仍直接调用 &lt;code&gt;DateTime.Now&lt;/code&gt;、&lt;code&gt;Task.Delay&lt;/code&gt; 或 &lt;code&gt;System.Threading.Timer&lt;/code&gt;，测试就会重新依赖真实时间。&lt;/p&gt;
&lt;h2 id="4-用故障脚本描述场景"&gt;&lt;a href="#4-%e7%94%a8%e6%95%85%e9%9a%9c%e8%84%9a%e6%9c%ac%e6%8f%8f%e8%bf%b0%e5%9c%ba%e6%99%af" class="header-anchor"&gt;&lt;/a&gt;4. 用故障脚本描述场景
&lt;/h2&gt;&lt;p&gt;仅有 &lt;code&gt;FailNextMove&lt;/code&gt; 很快不够。可以把故障注入描述成脚本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Given 轴已回零，当前位置 10 mm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;When 收到移动到 100 mm 的命令
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;And 运动 40% 时通信中断
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Then 上位机进入 PositionUnknown
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;And 不再接受后续绝对移动
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;And 保存目标、最后观测位置和控制器错误码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;When 通信恢复并重新读取编码器
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Then 根据回读和恢复策略决定重新回零或人工确认
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;脚本应覆盖命令前失败、执行中失败、执行完成但确认丢失这三个关键时间窗口。对于并发场景，还要控制事件顺序、重复和迟到，而不是依赖线程调度“碰巧复现”。&lt;/p&gt;
&lt;h2 id="5-契约测试保护真实与模拟实现"&gt;&lt;a href="#5-%e5%a5%91%e7%ba%a6%e6%b5%8b%e8%af%95%e4%bf%9d%e6%8a%a4%e7%9c%9f%e5%ae%9e%e4%b8%8e%e6%a8%a1%e6%8b%9f%e5%ae%9e%e7%8e%b0" class="header-anchor"&gt;&lt;/a&gt;5. 契约测试保护真实与模拟实现
&lt;/h2&gt;&lt;p&gt;同一个 &lt;code&gt;IAxis&lt;/code&gt; 可以有厂商适配器和模拟器，但实现同一接口不等于语义一致。为接口建立一组契约测试，分别运行在每个实现上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;未回零时绝对移动是否被拒绝；&lt;/li&gt;
&lt;li&gt;超出软限位时是否在动作前失败；&lt;/li&gt;
&lt;li&gt;返回完成时是否满足约定的到位条件；&lt;/li&gt;
&lt;li&gt;取消后动作和状态如何变化；&lt;/li&gt;
&lt;li&gt;重复停止和关闭是否幂等；&lt;/li&gt;
&lt;li&gt;原始错误码是否保留。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真实硬件无法在普通 CI 中运行所有契约时，可以给测试打环境标签，在专用实验台定时执行；模拟器仍需在每次提交中执行相同的通用部分。&lt;/p&gt;
&lt;h2 id="6-记录回放用于协议和现场问题"&gt;&lt;a href="#6-%e8%ae%b0%e5%bd%95%e5%9b%9e%e6%94%be%e7%94%a8%e4%ba%8e%e5%8d%8f%e8%ae%ae%e5%92%8c%e7%8e%b0%e5%9c%ba%e9%97%ae%e9%a2%98" class="header-anchor"&gt;&lt;/a&gt;6. 记录回放用于协议和现场问题
&lt;/h2&gt;&lt;p&gt;记录回放（Record/Replay）能够把一次真实交互转成可重复输入。记录内容可包括时间间隔、请求字节、响应字节、连接事件和关键环境版本。回放有三种常见模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;严格回放：请求必须与记录完全一致，适合协议回归；&lt;/li&gt;
&lt;li&gt;状态回放：根据当前模拟状态生成响应，适合流程测试；&lt;/li&gt;
&lt;li&gt;扰动回放：在原记录中插入延迟、丢包、断连或坏帧，适合鲁棒性验证。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回放数据要脱敏，特别是序列号、客户 Recipe、生产标识和访问凭据。还要记录协议/固件版本，否则几年后无法判断样本代表哪个实现。&lt;/p&gt;
&lt;h2 id="7-hil-验证软件无法伪造的部分"&gt;&lt;a href="#7-hil-%e9%aa%8c%e8%af%81%e8%bd%af%e4%bb%b6%e6%97%a0%e6%b3%95%e4%bc%aa%e9%80%a0%e7%9a%84%e9%83%a8%e5%88%86" class="header-anchor"&gt;&lt;/a&gt;7. HIL 验证软件无法伪造的部分
&lt;/h2&gt;&lt;p&gt;HIL 将真实控制器、I/O 模块或驱动器接到仿真负载上，让上位机面对真实通信栈和固件（半导体设备的领域化实践见《&lt;a class="link" href="https://www.jiwei.space/posts/semiconductor/equipment-software/10-testing-hil/" &gt;半导体设备软件（十）：仿真、单元测试与 Hardware-in-the-Loop&lt;/a&gt;》）。它适合验证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;驱动安装、位数和 SDK 依赖；&lt;/li&gt;
&lt;li&gt;电平、接线、信号极性和抖动；&lt;/li&gt;
&lt;li&gt;控制器扫描周期、缓冲和响应时序；&lt;/li&gt;
&lt;li&gt;断电、复位、拔线和看门狗行为；&lt;/li&gt;
&lt;li&gt;多设备同时运行时的资源与时序边界。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HIL 台必须有物理保护、运动范围和自动复位方案。测试脚本不能因为运行在“实验环境”就绕过急停和联锁。破坏性测试应使用专用工装，并由风险评估决定是否允许自动执行。&lt;/p&gt;
&lt;h2 id="8-测试断言最终事实"&gt;&lt;a href="#8-%e6%b5%8b%e8%af%95%e6%96%ad%e8%a8%80%e6%9c%80%e7%bb%88%e4%ba%8b%e5%ae%9e" class="header-anchor"&gt;&lt;/a&gt;8. 测试断言最终事实
&lt;/h2&gt;&lt;p&gt;“调用过 &lt;code&gt;MoveAbsoluteAsync&lt;/code&gt; 一次”只证明编排器发出了意图。更有价值的断言包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设备最终状态和实际位置；&lt;/li&gt;
&lt;li&gt;Recipe 与校准快照是否绑定到运行记录；&lt;/li&gt;
&lt;li&gt;失败后是否拒绝危险的后续命令；&lt;/li&gt;
&lt;li&gt;补偿动作是否在正确前提下执行；&lt;/li&gt;
&lt;li&gt;报警、诊断证据和审计事件是否完整；&lt;/li&gt;
&lt;li&gt;重启后是否从可信检查点恢复。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;交互次数仍有用，但它是验证边界协议的手段，不应代替业务结果。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;设备软件需要从纯逻辑、行为模拟、协议契约、完整软件、HIL 到整机验收的测试梯度。模拟器应表现时间、状态和失败，&lt;code&gt;TimeProvider&lt;/code&gt; 让时间相关测试可控，契约测试防止真实与仿真实现语义漂移，HIL 则专注真实接口和时序风险。&lt;/p&gt;
&lt;p&gt;下一篇将完成系列：建立日志、指标、追踪、审计和诊断包，并把软件升级设计成可验证、可回退的设备变更流程。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/testing/unit-testing-best-practices" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Unit testing best practices for .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/datetime/timeprovider-overview" target="_blank" rel="noopener"
 &gt;Microsoft Learn：What is TimeProvider?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/extensions/timeprovider-testing" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Testing with FakeTimeProvider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/aspnet/core/test/integration-tests" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Integration tests in ASP.NET Core&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备软件架构与控制模型（六）：故障处理、报警与安全恢复</title><link>https://www.jiwei.space/posts/equipment/equipment-architecture/06-fault-recovery/</link><pubDate>Wed, 15 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/equipment-architecture/06-fault-recovery/</guid><description>&lt;p&gt;设备现场最常见的“恢复方案”是弹窗、重试和重启：捕获异常后提示用户，用户点击确定，程序再发一次命令；仍然失败就重启软件。这种做法偶尔能恢复通信，却无法证明机械位置、物料状态和安全条件已经恢复，反而可能覆盖最有价值的故障证据。&lt;/p&gt;
&lt;p&gt;可靠的故障处理不是一个 &lt;code&gt;catch&lt;/code&gt; 代码块，而是一条从检测、隔离、报警到恢复验证的闭环。本文讨论通用方法，不替代具体设备的风险评估、安全标准和操作规程；半导体设备异常恢复的领域实践见《&lt;a class="link" href="https://www.jiwei.space/posts/semiconductor/equipment-software/06-realtime-concurrency-recovery/" &gt;半导体设备软件（六）：实时性、并发与异常恢复&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="1-事件提示报警和故障不要混用"&gt;&lt;a href="#1-%e4%ba%8b%e4%bb%b6%e6%8f%90%e7%a4%ba%e6%8a%a5%e8%ad%a6%e5%92%8c%e6%95%85%e9%9a%9c%e4%b8%8d%e8%a6%81%e6%b7%b7%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;1. 事件、提示、报警和故障不要混用
&lt;/h2&gt;&lt;p&gt;系统中出现的每条消息并不都需要操作员处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;事件（Event）&lt;/strong&gt;：值得记录的事实，例如连接建立、Recipe 激活、任务完成；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提示（Notification）&lt;/strong&gt;：需要了解但通常不要求立即行动的信息，例如维护周期临近；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;报警（Alarm）&lt;/strong&gt;：需要人在规定时间内采取行动的异常条件；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;故障（Fault）&lt;/strong&gt;：设备或软件无法满足预期能力的状态，可能触发报警和联锁；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志（Log）&lt;/strong&gt;：供诊断使用的技术记录，数量通常远多于报警。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一次相机断连是故障；系统可以产生一条报警提醒操作员，并写入多条重连和驱动日志。若把每次重试都弹成报警，真正需要行动的信息会被报警洪泛淹没。&lt;/p&gt;
&lt;p&gt;ISA-18.2 面向过程工业的报警管理，但其中的生命周期思想同样值得设备软件借鉴：报警需要经过识别、合理化、详细设计、运行监测和变更管理等阶段，而不是开发者看到异常就随手加一个红色弹窗。离散设备采用时仍应结合自身行业和风险边界。&lt;/p&gt;
&lt;h2 id="2-故障模型同时保存判断和证据"&gt;&lt;a href="#2-%e6%95%85%e9%9a%9c%e6%a8%a1%e5%9e%8b%e5%90%8c%e6%97%b6%e4%bf%9d%e5%ad%98%e5%88%a4%e6%96%ad%e5%92%8c%e8%af%81%e6%8d%ae" class="header-anchor"&gt;&lt;/a&gt;2. 故障模型同时保存判断和证据
&lt;/h2&gt;&lt;p&gt;一条可操作的故障记录应包含稳定分类、来源、严重级别、时间和原始证据：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;FaultSeverity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Recoverable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Critical&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;EquipmentFault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FaultId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;FaultSeverity&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;OperatorMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string?&lt;/span&gt; &lt;span class="n"&gt;VendorCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;OccurredAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CorrelationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;RequiresAcknowledgement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsLatched&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;注意&amp;quot;严重级别&amp;quot;和&amp;quot;可恢复性&amp;quot;其实是两个维度：Warning/Critical 描述影响程度，Recoverable 描述恢复方式。示例为保持简单混用在一个枚举里；生产代码中若两个维度都需要独立决策，应拆成两个属性分别建模。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Kind&lt;/code&gt; 应是软件长期维护的稳定分类，例如 &lt;code&gt;CommunicationLost&lt;/code&gt;、&lt;code&gt;InterlockOpen&lt;/code&gt;、&lt;code&gt;PositionUnknown&lt;/code&gt;；&lt;code&gt;VendorCode&lt;/code&gt; 保留厂商证据。面向操作员的消息说明“发生什么、影响什么、建议做什么”，技术堆栈和原始报文进入诊断记录，而不是全部塞进弹窗。&lt;/p&gt;
&lt;p&gt;严重级别不应只根据异常类型决定。同一个读取超时，在非关键温度趋势采样中可能只是降级，在等待夹具到位的步骤中却可能使物料状态未知。&lt;/p&gt;
&lt;h2 id="3-先判断物理副作用再决定重试"&gt;&lt;a href="#3-%e5%85%88%e5%88%a4%e6%96%ad%e7%89%a9%e7%90%86%e5%89%af%e4%bd%9c%e7%94%a8%e5%86%8d%e5%86%b3%e5%ae%9a%e9%87%8d%e8%af%95" class="header-anchor"&gt;&lt;/a&gt;3. 先判断物理副作用，再决定重试
&lt;/h2&gt;&lt;p&gt;网络和 SDK 调用失败时，最重要的问题不是“能否再试一次”，而是第一次操作有没有发生：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;结果&lt;/th&gt;
 &lt;th&gt;例子&lt;/th&gt;
 &lt;th&gt;处理重点&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;明确未执行&lt;/td&gt;
 &lt;td&gt;本地参数校验失败&lt;/td&gt;
 &lt;td&gt;修正输入后可以重新提交&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;明确已完成&lt;/td&gt;
 &lt;td&gt;控制器返回完成并通过回读确认&lt;/td&gt;
 &lt;td&gt;不应重复执行&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;执行中断&lt;/td&gt;
 &lt;td&gt;移动过程中驱动报警&lt;/td&gt;
 &lt;td&gt;先停止并确认位置&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;结果未知&lt;/td&gt;
 &lt;td&gt;命令发出后连接断开&lt;/td&gt;
 &lt;td&gt;先查询或进入人工确认，不能盲重试&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;读取状态通常比改变物理世界的命令更容易安全重试，但也要考虑设备和总线负载。写命令只有在协议提供幂等键、序列号或可查询结果时，才能可靠地去重。没有这些能力时，自动重试可能造成重复运动、重复出料或覆盖数据。&lt;/p&gt;
&lt;h2 id="4-恢复是一台独立状态机"&gt;&lt;a href="#4-%e6%81%a2%e5%a4%8d%e6%98%af%e4%b8%80%e5%8f%b0%e7%8b%ac%e7%ab%8b%e7%8a%b6%e6%80%81%e6%9c%ba" class="header-anchor"&gt;&lt;/a&gt;4. 恢复是一台独立状态机
&lt;/h2&gt;&lt;p&gt;不要在各层 &lt;code&gt;catch&lt;/code&gt; 中零散地写复位命令。恢复流程本身应建模并保留检查点：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Detect
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Contain（阻止新命令、隔离故障设备）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → ReachSafeState（请求受控停止或依赖安全链）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Diagnose（采集状态、错误码和上下文）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Correct（重连、复位、重新初始化或人工维修）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Verify（回读、联锁、回零和试运行检查）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Resume / Abort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;“复位命令返回成功”只是 Correct 阶段的一步。只有 Verify 重新证明必要条件，状态机才可以从 &lt;code&gt;Faulted&lt;/code&gt; 回到 &lt;code&gt;Ready&lt;/code&gt;。确认报警也只是说明操作员已经看见，不应自动清除故障条件。&lt;/p&gt;
&lt;p&gt;恢复步骤需要声明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是否自动执行，还是必须授权人员确认；&lt;/li&gt;
&lt;li&gt;可执行的设备状态和安全前提；&lt;/li&gt;
&lt;li&gt;超时及失败后的终止状态；&lt;/li&gt;
&lt;li&gt;是否幂等，重复进入会不会造成额外动作；&lt;/li&gt;
&lt;li&gt;成功判据采用什么独立回读；&lt;/li&gt;
&lt;li&gt;是否需要重新回零、重新校准或报废当前物料。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="5-安全动作必须有独立保证"&gt;&lt;a href="#5-%e5%ae%89%e5%85%a8%e5%8a%a8%e4%bd%9c%e5%bf%85%e9%a1%bb%e6%9c%89%e7%8b%ac%e7%ab%8b%e4%bf%9d%e8%af%81" class="header-anchor"&gt;&lt;/a&gt;5. 安全动作必须有独立保证
&lt;/h2&gt;&lt;p&gt;应用层的异常处理无法替代安全功能。GC 暂停、线程阻塞、操作系统故障和进程崩溃都可能让上位机不能及时执行。涉及人身或设备危险的联锁、急停和能量切断，需要按风险评估放在具备相应安全完整性的硬件、安全 PLC、驱动器或回路中。&lt;/p&gt;
&lt;p&gt;上位机的责任是：不绕过安全条件、准确显示安全链状态、在触发后停止业务流程、保存上下文，并引导受控复位。软件中的 &lt;code&gt;try/finally&lt;/code&gt; 可以释放资源，却不能证明继电器已经断开或气缸已经回到安全位置。&lt;/p&gt;
&lt;h2 id="6-降级比全好或全坏更实用"&gt;&lt;a href="#6-%e9%99%8d%e7%ba%a7%e6%af%94%e5%85%a8%e5%a5%bd%e6%88%96%e5%85%a8%e5%9d%8f%e6%9b%b4%e5%ae%9e%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;6. 降级比“全好或全坏”更实用
&lt;/h2&gt;&lt;p&gt;并非所有故障都要求整机停机。诊断相机不可用时，可以禁用高级诊断但保留手动维护；非关键温度传感器失效时，可以降低速度并禁止自动生产；关键联锁失效则必须阻止危险动作。&lt;/p&gt;
&lt;p&gt;降级模式必须显式定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪些功能仍可使用；&lt;/li&gt;
&lt;li&gt;性能或质量承诺如何变化；&lt;/li&gt;
&lt;li&gt;操作员需要看到什么持续提示；&lt;/li&gt;
&lt;li&gt;多久后必须升级为停机；&lt;/li&gt;
&lt;li&gt;恢复到正常模式需要哪些验证。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要在异常处理中临时“忽略一次”，这会形成未记录的隐式降级。&lt;/p&gt;
&lt;h2 id="7-抑制报警洪泛但不抹掉事实"&gt;&lt;a href="#7-%e6%8a%91%e5%88%b6%e6%8a%a5%e8%ad%a6%e6%b4%aa%e6%b3%9b%e4%bd%86%e4%b8%8d%e6%8a%b9%e6%8e%89%e4%ba%8b%e5%ae%9e" class="header-anchor"&gt;&lt;/a&gt;7. 抑制报警洪泛，但不抹掉事实
&lt;/h2&gt;&lt;p&gt;断开一根总线可能让十个从设备同时报错。如果每个轮询周期都创建新报警，操作员无法找到根因。可以采用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;相同来源和故障键合并为一条活动报警；&lt;/li&gt;
&lt;li&gt;记录首次发生、最近发生和重复次数；&lt;/li&gt;
&lt;li&gt;基于因果关系抑制下游派生报警；&lt;/li&gt;
&lt;li&gt;对抖动信号设置经过工程确认的延时和死区；&lt;/li&gt;
&lt;li&gt;恢复后保留历史，而不是删除记录；&lt;/li&gt;
&lt;li&gt;定期统计报警频率、持续时间和洪泛区间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;抑制只改变呈现和通知，不应删除底层事件。关键报警的停用、阈值和优先级变更也应纳入审计和测试。&lt;/p&gt;
&lt;h2 id="8-用故障注入验证恢复路径"&gt;&lt;a href="#8-%e7%94%a8%e6%95%85%e9%9a%9c%e6%b3%a8%e5%85%a5%e9%aa%8c%e8%af%81%e6%81%a2%e5%a4%8d%e8%b7%af%e5%be%84" class="header-anchor"&gt;&lt;/a&gt;8. 用故障注入验证恢复路径
&lt;/h2&gt;&lt;p&gt;正常路径跑一百次，不能证明恢复路径可靠。模拟器和硬件在环（Hardware-in-the-Loop，HIL）环境应能够注入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令发送前、发送后和完成确认前断连；&lt;/li&gt;
&lt;li&gt;状态长时间不变化；&lt;/li&gt;
&lt;li&gt;迟到、重复或乱序响应；&lt;/li&gt;
&lt;li&gt;传感器抖动和不合理组合；&lt;/li&gt;
&lt;li&gt;磁盘写满、日志后端不可用；&lt;/li&gt;
&lt;li&gt;进程在检查点前后终止。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;验证的不只是“抛出了预期异常”，还包括设备最终状态、是否继续接受命令、证据是否完整、报警是否可操作，以及重启后是否拒绝从未知状态继续。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;可靠故障处理先区分事件、报警和故障，再根据物理副作用判断能否重试。恢复是一条带安全前提、检查点和成功判据的状态机；确认消息、清除报警和修复故障是不同动作。安全链负责最后保障，上位机负责不绕过它并保存足够证据。&lt;/p&gt;
&lt;p&gt;下一篇将搭建从纯软件模拟器到 HIL 的测试梯度，说明如何模拟时间、失败和设备动态，而不是只让 Mock 永远返回成功。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.isa.org/standards-and-publications/isa-standards/isa-18-series-of-standards" target="_blank" rel="noopener"
 &gt;ISA：ISA-18 Series of Standards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.isa.org/intech-home/2018/march-april/features/alarm-management-life-cycle" target="_blank" rel="noopener"
 &gt;ISA：Alarm management life cycle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.iec.ch/functional-safety" target="_blank" rel="noopener"
 &gt;IEC：Functional safety&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/exceptions/best-practices-for-exceptions" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Exception best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备软件架构与控制模型（五）：Recipe 与配置版本管理</title><link>https://www.jiwei.space/posts/equipment/equipment-architecture/05-recipe-configuration/</link><pubDate>Tue, 14 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/equipment-architecture/05-recipe-configuration/</guid><description>&lt;p&gt;设备软件中的“参数”看起来都像键值对，实际上承担着完全不同的责任。产品 Recipe、设备常数、校准结果、通信地址和用户偏好如果共用一份可随时编辑的 JSON，不仅难以追溯，还可能让一次运行前后使用了两套参数。&lt;/p&gt;
&lt;p&gt;本文建立一套通用配置模型：先区分参数归属，再为 Recipe 设计验证、审批、激活和执行快照。重点不是选择 JSON、数据库还是某个配置中心，而是保证系统能够回答“当时究竟使用了什么”。半导体设备 Recipe 管理的领域实践见《&lt;a class="link" href="https://www.jiwei.space/posts/semiconductor/equipment-software/03-recipe-management/" &gt;半导体设备软件（三）：Recipe 配方管理与版本控制&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="1-先按责任区分参数"&gt;&lt;a href="#1-%e5%85%88%e6%8c%89%e8%b4%a3%e4%bb%bb%e5%8c%ba%e5%88%86%e5%8f%82%e6%95%b0" class="header-anchor"&gt;&lt;/a&gt;1. 先按责任区分参数
&lt;/h2&gt;&lt;p&gt;常见参数至少分为以下几类：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;类型&lt;/th&gt;
 &lt;th&gt;示例&lt;/th&gt;
 &lt;th&gt;谁负责变更&lt;/th&gt;
 &lt;th&gt;典型生命周期&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Recipe&lt;/td&gt;
 &lt;td&gt;曝光时间、扫描速度、检测阈值&lt;/td&gt;
 &lt;td&gt;工艺或授权用户&lt;/td&gt;
 &lt;td&gt;随产品/工艺版本发布&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;设备配置&lt;/td&gt;
 &lt;td&gt;轴数量、相机型号、通信端点&lt;/td&gt;
 &lt;td&gt;设备工程师&lt;/td&gt;
 &lt;td&gt;安装或硬件变更时修改&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;设备常数&lt;/td&gt;
 &lt;td&gt;软限位、脉冲当量、机构偏置&lt;/td&gt;
 &lt;td&gt;调试与维护人员&lt;/td&gt;
 &lt;td&gt;受控维护后更新&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;校准结果&lt;/td&gt;
 &lt;td&gt;标定矩阵、零点、温度补偿系数&lt;/td&gt;
 &lt;td&gt;校准流程&lt;/td&gt;
 &lt;td&gt;与设备、时间和方法绑定&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;运行策略&lt;/td&gt;
 &lt;td&gt;超时、重试上限、数据保留策略&lt;/td&gt;
 &lt;td&gt;软件/运维人员&lt;/td&gt;
 &lt;td&gt;随软件或现场策略演进&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;用户偏好&lt;/td&gt;
 &lt;td&gt;窗口布局、曲线颜色&lt;/td&gt;
 &lt;td&gt;当前用户&lt;/td&gt;
 &lt;td&gt;可自由修改，不影响工艺事实&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;凭据与密钥&lt;/td&gt;
 &lt;td&gt;令牌、证书私钥&lt;/td&gt;
 &lt;td&gt;安全设施&lt;/td&gt;
 &lt;td&gt;不应作为普通配置明文保存&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这种分类决定权限、验证和审计强度。校准数据不是普通 Recipe，操作员也不应通过“导入配方”覆盖运动软限位。凭据应进入操作系统或专用密钥存储，而不是为了方便一起导出。&lt;/p&gt;
&lt;h2 id="2-recipe-是受控版本不是可变表单"&gt;&lt;a href="#2-recipe-%e6%98%af%e5%8f%97%e6%8e%a7%e7%89%88%e6%9c%ac%e4%b8%8d%e6%98%af%e5%8f%af%e5%8f%98%e8%a1%a8%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;2. Recipe 是受控版本，不是可变表单
&lt;/h2&gt;&lt;p&gt;编辑界面中的对象只是草稿。发布后的 Recipe 应成为不可变版本，至少包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;稳定的 Recipe 标识；&lt;/li&gt;
&lt;li&gt;业务版本和数据结构版本；&lt;/li&gt;
&lt;li&gt;创建者、创建时间和变更说明；&lt;/li&gt;
&lt;li&gt;适用的设备能力或软件版本范围；&lt;/li&gt;
&lt;li&gt;完整参数内容；&lt;/li&gt;
&lt;li&gt;对保存字节或规范化内容计算的摘要；&lt;/li&gt;
&lt;li&gt;审核、发布和停用状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CaptureRecipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;RecipeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Revision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;SchemaVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;Exposure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;ScanSpeedMillimetersPerSecond&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;FrameCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CreatedBy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;RecipeSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CaptureRecipe&lt;/span&gt; &lt;span class="n"&gt;Recipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;ActivatedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;C# &lt;code&gt;record&lt;/code&gt; 和 &lt;code&gt;init&lt;/code&gt; 属性有助于减少意外修改，但它们不会自动让内部集合深度不可变。若 Recipe 含列表或字典，应复制为只读/不可变结构，避免外部仍持有可变引用。&lt;/p&gt;
&lt;p&gt;摘要也不能直接理解为数字签名。SHA-256 可以发现内容是否变化，却不能证明修改者身份；需要防篡改或跨组织验真时，还要使用带受控密钥的签名机制。若对 JSON 计算摘要，应对&lt;strong&gt;实际保存的字节&lt;/strong&gt;求值，或先定义稳定的规范化格式，不能假设任意序列化结果的属性顺序和空白永远一致。&lt;/p&gt;
&lt;h2 id="3-验证分为四层"&gt;&lt;a href="#3-%e9%aa%8c%e8%af%81%e5%88%86%e4%b8%ba%e5%9b%9b%e5%b1%82" class="header-anchor"&gt;&lt;/a&gt;3. 验证分为四层
&lt;/h2&gt;&lt;p&gt;只检查数据类型远远不够。Recipe 验证可以分层执行：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;结构验证&lt;/strong&gt;：必填字段、类型、数据结构版本；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;范围验证&lt;/strong&gt;：曝光时间、速度和数量是否在单参数范围内；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关联验证&lt;/strong&gt;：速度、采样率和缓存容量组合后是否可行；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备验证&lt;/strong&gt;：目标设备是否具备所需能力，当前校准是否有效。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CaptureRecipeValidator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CaptureRecipe&lt;/span&gt; &lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exposure&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exposure&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Exposure must be within (0, 10s].&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ScanSpeedMillimetersPerSecond&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Scan speed must be within (0, 500] mm/s.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FrameCount&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Frame count must be within [1, 100000].&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;示例中的上限只是演示，不能复制成真实设备指标。实际范围应来自机械、电气、控制器和工艺共同确认的能力模型，并与单位一起保存。&lt;/p&gt;
&lt;p&gt;对于进程启动配置，.NET Options 模式支持强类型绑定和 &lt;code&gt;ValidateOnStart&lt;/code&gt;；但运行中导入 Recipe 时仍应经过领域验证服务。配置框架验证通过，只说明对象满足应用配置规则，不代表它适用于当前硬件和工艺。&lt;/p&gt;
&lt;h2 id="4-激活采用准备切换而不是边跑边改"&gt;&lt;a href="#4-%e6%bf%80%e6%b4%bb%e9%87%87%e7%94%a8%e5%87%86%e5%a4%87%e5%88%87%e6%8d%a2%e8%80%8c%e4%b8%8d%e6%98%af%e8%be%b9%e8%b7%91%e8%be%b9%e6%94%b9" class="header-anchor"&gt;&lt;/a&gt;4. 激活采用“准备—切换”而不是边跑边改
&lt;/h2&gt;&lt;p&gt;Recipe 生效可以设计为两个阶段：&lt;/p&gt;
&lt;h3 id="41-准备阶段"&gt;&lt;a href="#41-%e5%87%86%e5%a4%87%e9%98%b6%e6%ae%b5" class="header-anchor"&gt;&lt;/a&gt;4.1 准备阶段
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;读取指定版本，并验证摘要；&lt;/li&gt;
&lt;li&gt;执行结构、范围、关联和设备能力校验；&lt;/li&gt;
&lt;li&gt;检查当前整机状态是否允许切换；&lt;/li&gt;
&lt;li&gt;将参数转换为各设备可应用的设置；&lt;/li&gt;
&lt;li&gt;生成完整、不可变的执行快照。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="42-切换阶段"&gt;&lt;a href="#42-%e5%88%87%e6%8d%a2%e9%98%b6%e6%ae%b5" class="header-anchor"&gt;&lt;/a&gt;4.2 切换阶段
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;阻止新任务进入；&lt;/li&gt;
&lt;li&gt;在安全状态下应用参数；&lt;/li&gt;
&lt;li&gt;读取设备回读值或确认结果；&lt;/li&gt;
&lt;li&gt;原子地替换“当前 Recipe 快照”的引用；&lt;/li&gt;
&lt;li&gt;记录激活者、版本、摘要和结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里的“原子替换”只针对上位机内存中的当前快照，不代表多个物理设备能像数据库事务一样同时提交。若相机参数应用成功而运动控制器失败，应进入受控故障状态，记录每个设备的实际结果，再由恢复策略决定重新应用、回到已知配置或要求人工处置。&lt;/p&gt;
&lt;h2 id="5-每次运行固定一份快照"&gt;&lt;a href="#5-%e6%af%8f%e6%ac%a1%e8%bf%90%e8%a1%8c%e5%9b%ba%e5%ae%9a%e4%b8%80%e4%bb%bd%e5%bf%ab%e7%85%a7" class="header-anchor"&gt;&lt;/a&gt;5. 每次运行固定一份快照
&lt;/h2&gt;&lt;p&gt;流程启动时读取一次当前 Recipe，并把其版本和摘要写入任务上下文。后续每个步骤都使用同一份快照，即使管理员在运行期间发布了新版本，正在执行的任务也不应悄悄切换。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RunId: RUN-20260811-0042
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RecipeId: Product-A-Scan
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Revision: 17
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SchemaVersion: 3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sha256: 8F...C2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EquipmentConfigVersion: EQ-2026.08.3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CalibrationSet: CAL-CAMERA-20260801
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SoftwareVersion: 2.6.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这组信息把结果数据与其产生条件绑定起来。仅记录文件名不够，因为同名文件可能已经被覆盖；只保存“当前版本”也无法解释历史任务。&lt;/p&gt;
&lt;h2 id="6-迁移兼容与回滚要分开"&gt;&lt;a href="#6-%e8%bf%81%e7%a7%bb%e5%85%bc%e5%ae%b9%e4%b8%8e%e5%9b%9e%e6%bb%9a%e8%a6%81%e5%88%86%e5%bc%80" class="header-anchor"&gt;&lt;/a&gt;6. 迁移、兼容与回滚要分开
&lt;/h2&gt;&lt;p&gt;数据结构升级时，不要在反序列化失败后默默填默认值。应根据 &lt;code&gt;SchemaVersion&lt;/code&gt; 选择显式迁移器，保留原始版本，并验证迁移结果。迁移后的 Recipe 是新版本还是运行时视图，要在审计规则中说清楚。&lt;/p&gt;
&lt;p&gt;软件兼容性和工艺版本回滚也是两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新软件能否读取旧 Recipe，由兼容策略和迁移器决定；&lt;/li&gt;
&lt;li&gt;旧软件能否读取新 Recipe，不能默认成立；&lt;/li&gt;
&lt;li&gt;回滚 Recipe 只能恢复参数版本，不能撤销已经加工的工件或已经改变的设备状态；&lt;/li&gt;
&lt;li&gt;校准或硬件变化后，旧 Recipe 即使格式兼容，也可能不再适用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生产系统应保留最后一个经过验证的版本，但切换失败时不要盲目“自动回滚并继续生产”。先确认设备回读和物理状态，再决定能否恢复。&lt;/p&gt;
&lt;h2 id="7-权限与审计围绕动作设计"&gt;&lt;a href="#7-%e6%9d%83%e9%99%90%e4%b8%8e%e5%ae%a1%e8%ae%a1%e5%9b%b4%e7%bb%95%e5%8a%a8%e4%bd%9c%e8%ae%be%e8%ae%a1" class="header-anchor"&gt;&lt;/a&gt;7. 权限与审计围绕动作设计
&lt;/h2&gt;&lt;p&gt;权限不应只有“能否打开配置页面”。至少区分查看、编辑草稿、验证、审核、发布、激活和停用。高风险参数可以要求双人复核或维护模式，但具体控制强度应来自风险评估。&lt;/p&gt;
&lt;p&gt;审计记录需要回答：谁在何时基于哪个旧版本做了什么变更，验证结果如何，哪个设备在何时激活，以及激活是否成功。审计日志本身也要限制修改和删除权限，且不能把密钥或完整敏感配置写进去。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;Recipe、设备配置、校准和用户偏好虽然都表现为参数，却不能共享同一套生命周期。发布后的 Recipe 应不可变、可验证、可追溯；激活前完成准备，运行时固定快照；多设备应用失败时承认物理世界不具备通用事务回滚。&lt;/p&gt;
&lt;p&gt;下一篇将把视角转向失败：如何区分事件、报警和故障，如何设计不会无限重试的恢复流程，以及为什么“清除报警”不等于问题已经消失。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/extensions/options" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Options pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/immutability" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Use immutable types and properties（System.Text.Json）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/api/system.security.cryptography.sha256.hashdata" target="_blank" rel="noopener"
 &gt;Microsoft Learn：SHA256.HashData&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://csrc.nist.gov/pubs/sp/800/128/upd1/final" target="_blank" rel="noopener"
 &gt;NIST SP 800-128：Guide for Security-Focused Configuration Management&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备软件架构与控制模型（四）：状态机、命令队列与流程编排</title><link>https://www.jiwei.space/posts/equipment/equipment-architecture/04-state-command-workflow/</link><pubDate>Mon, 13 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/equipment-architecture/04-state-command-workflow/</guid><description>&lt;p&gt;设备接入增多以后，最危险的复杂度往往不是算法，而是“谁在什么时候可以做什么”。UI 可以点启动，远程接口也能启动；后台监控发现异常要停止；流程内部同时控制相机、光源和运动轴。若这些入口直接调用硬件，锁只能减少数据竞争，却不能保证业务顺序正确。&lt;/p&gt;
&lt;p&gt;本篇把控制模型拆成三个互补部分：状态机决定命令是否合法，命令队列确定写操作的顺序，流程编排负责跨设备的长事务。三者职责不同，不能用一个不断增长的 &lt;code&gt;switch&lt;/code&gt; 或一把全局锁替代。半导体设备状态机与任务调度的领域实践见《&lt;a class="link" href="https://www.jiwei.space/posts/semiconductor/equipment-software/02-state-machine-scheduling/" &gt;半导体设备软件（二）：设备状态机与任务调度&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="1-状态机保护业务不变量"&gt;&lt;a href="#1-%e7%8a%b6%e6%80%81%e6%9c%ba%e4%bf%9d%e6%8a%a4%e4%b8%9a%e5%8a%a1%e4%b8%8d%e5%8f%98%e9%87%8f" class="header-anchor"&gt;&lt;/a&gt;1. 状态机保护业务不变量
&lt;/h2&gt;&lt;p&gt;状态机由四个基本元素组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态：系统对当前阶段的稳定判断；&lt;/li&gt;
&lt;li&gt;事件：已经发生的输入或事实；&lt;/li&gt;
&lt;li&gt;守卫条件：允许转换必须满足的条件；&lt;/li&gt;
&lt;li&gt;动作：转换过程中产生的副作用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如设备只有在 &lt;code&gt;Ready&lt;/code&gt; 且门联锁、气压和回零状态均满足时才能进入 &lt;code&gt;Running&lt;/code&gt;。把规则集中到状态转换中，比在每个按钮里各写一遍判断可靠得多。&lt;/p&gt;
&lt;p&gt;纯转换函数便于穷举测试：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;MachineState&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Offline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Paused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Faulted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;MachineEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;StartRequested&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;InterlocksSatisfied&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MachineEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;PauseRequested&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MachineEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ResumeRequested&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MachineEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;FaultDetected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MachineEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ResetCompleted&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MachineEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MachineTransitions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;MachineState&lt;/span&gt; &lt;span class="n"&gt;Apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MachineState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MachineEvent&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MachineState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StartRequested&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;InterlocksSatisfied&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MachineState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MachineState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PauseRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MachineState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Paused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MachineState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Paused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ResumeRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MachineState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FaultDetected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MachineState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Faulted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MachineState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Faulted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ResetCompleted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MachineState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;$&amp;#34;Event {message.GetType().Name} is invalid in {state}.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这个函数只决定目标状态，真正的硬件动作由命令处理器执行。若先把状态改成 &lt;code&gt;Running&lt;/code&gt;，随后启动硬件失败，就会制造假状态。更稳健的顺序是：验证守卫条件，执行必要动作，确认结果，再提交状态转换；执行期间可以使用 &lt;code&gt;Starting&lt;/code&gt; 之类的过渡状态防止重复命令。&lt;/p&gt;
&lt;h2 id="2-不要造超级状态机"&gt;&lt;a href="#2-%e4%b8%8d%e8%a6%81%e9%80%a0%e8%b6%85%e7%ba%a7%e7%8a%b6%e6%80%81%e6%9c%ba" class="header-anchor"&gt;&lt;/a&gt;2. 不要造“超级状态机”
&lt;/h2&gt;&lt;p&gt;整机、工位和单个设备的状态变化速度与责任不同。把所有组合塞进一个枚举，会迅速出现 &lt;code&gt;RunningButCameraRecoveringAndDoorOpen&lt;/code&gt; 之类不可维护的状态。&lt;/p&gt;
&lt;p&gt;可以采用分层模型：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;整机状态：Offline / Ready / Running / Paused / Faulted
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ 工位 A：Idle / Processing / Completed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ 工位 B：Idle / Processing / Completed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─ 设备健康：Camera Healthy，Axis Faulted，PLC Healthy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;上层状态聚合下层事实，但不要简单复制全部状态。整机 &lt;code&gt;Ready&lt;/code&gt; 的含义应由明确规则计算，例如所有必需设备健康、流程未占用、联锁满足且 Recipe 已验证。&lt;/p&gt;
&lt;h2 id="3-命令队列建立单一写入顺序"&gt;&lt;a href="#3-%e5%91%bd%e4%bb%a4%e9%98%9f%e5%88%97%e5%bb%ba%e7%ab%8b%e5%8d%95%e4%b8%80%e5%86%99%e5%85%a5%e9%a1%ba%e5%ba%8f" class="header-anchor"&gt;&lt;/a&gt;3. 命令队列建立单一写入顺序
&lt;/h2&gt;&lt;p&gt;状态机回答“能不能做”，队列回答“先做哪个”。对拥有唯一写控制权的设备控制器，可以使用 &lt;code&gt;System.Threading.Channels&lt;/code&gt; 建立有界异步队列：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading.Channels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IMachineCommand&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ICommandFailureHandler&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;IMachineCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommandPump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICommandFailureHandler&lt;/span&gt; &lt;span class="n"&gt;failureHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IMachineCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateBounded&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IMachineCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BoundedChannelOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;SingleReader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;SingleWriter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;FullMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BoundedChannelFullMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;EnqueueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;IMachineCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMachineCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAllAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;failureHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryComplete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;有界队列使过载可见。&lt;code&gt;FullMode = Wait&lt;/code&gt; 会对生产者施加背压，但这只适合不能静默丢弃的控制命令；高频遥测可以采用另一条管线和不同丢弃策略。队列容量不是越大越好，过大的命令积压会让用户看到“停止已点击”却迟迟不执行。&lt;/p&gt;
&lt;p&gt;每条命令还应携带命令 ID、提交者、截止时间和权限上下文，并通过独立的完成源返回结果。入队成功只表示已被接受排队，不表示设备动作完成。&lt;/p&gt;
&lt;p&gt;示例把单条命令异常交给 &lt;code&gt;ICommandFailureHandler&lt;/code&gt;，由它记录失败、完成调用方结果并决定队列是否还能继续；若故障已经使设备状态不可知，处理器应让状态机进入 &lt;code&gt;Faulted&lt;/code&gt;，也可以再次抛出异常终止命令泵。不能未经分类就吞掉异常并继续执行下一条硬件命令。&lt;/p&gt;
&lt;h2 id="4-停止类动作不能都排在队尾"&gt;&lt;a href="#4-%e5%81%9c%e6%ad%a2%e7%b1%bb%e5%8a%a8%e4%bd%9c%e4%b8%8d%e8%83%bd%e9%83%bd%e6%8e%92%e5%9c%a8%e9%98%9f%e5%b0%be" class="header-anchor"&gt;&lt;/a&gt;4. 停止类动作不能都排在队尾
&lt;/h2&gt;&lt;p&gt;暂停、取消、正常停止和急停的语义不同：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;动作&lt;/th&gt;
 &lt;th&gt;目标&lt;/th&gt;
 &lt;th&gt;常见处理&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;暂停&lt;/td&gt;
 &lt;td&gt;在可恢复点暂时挂起&lt;/td&gt;
 &lt;td&gt;完成当前原子步骤，保存上下文&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;取消&lt;/td&gt;
 &lt;td&gt;放弃当前业务任务&lt;/td&gt;
 &lt;td&gt;协作式取消，执行必要清理&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;正常停止&lt;/td&gt;
 &lt;td&gt;让设备受控回到非运行态&lt;/td&gt;
 &lt;td&gt;阻止新任务，减速或完成安全序列&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;急停&lt;/td&gt;
 &lt;td&gt;尽快消除危险能量&lt;/td&gt;
 &lt;td&gt;独立安全回路或安全控制器执行&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;急停不应作为普通软件命令排在 100 个动作后面。软件可以观察急停、停止继续发命令并引导复位，但人身安全相关的紧急停止必须按设备风险评估和适用安全标准实现，不能依赖通用操作系统调度或托管队列。&lt;/p&gt;
&lt;p&gt;正常停止也通常需要高优先级控制通道，或者通过取消当前执行上下文后进入受控停止流程。不要给普通队列随意加入“插队”能力，否则命令顺序重新变得不可推理。&lt;/p&gt;
&lt;h2 id="5-流程编排管理长事务"&gt;&lt;a href="#5-%e6%b5%81%e7%a8%8b%e7%bc%96%e6%8e%92%e7%ae%a1%e7%90%86%e9%95%bf%e4%ba%8b%e5%8a%a1" class="header-anchor"&gt;&lt;/a&gt;5. 流程编排管理长事务
&lt;/h2&gt;&lt;p&gt;一次设备任务可能包含：夹紧工件、移动、开光源、采集、处理、保存和释放。它不是数据库事务，已经发生的物理动作无法统一回滚。流程编排器需要显式描述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个步骤的前置条件和完成条件；&lt;/li&gt;
&lt;li&gt;步骤的超时、取消点和可重试性；&lt;/li&gt;
&lt;li&gt;失败时进入安全状态的补偿动作；&lt;/li&gt;
&lt;li&gt;可持久化检查点，以及重启后允许恢复的位置；&lt;/li&gt;
&lt;li&gt;资源占用关系，避免两个流程争用同一设备。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补偿不等于撤销。例如“夹爪闭合”失败后的补偿可能是切断驱动并要求人工检查，而不是盲目发送“张开”。只有当前物理状态可确认、逆操作本身安全且幂等时，自动补偿才成立。&lt;/p&gt;
&lt;p&gt;流程步骤最好是粗粒度业务动作，而不是把每次寄存器读写都做成节点。过细会淹没意图，过粗又无法设置检查点和诊断失败位置。&lt;/p&gt;
&lt;h2 id="6-状态发布要避免乱序"&gt;&lt;a href="#6-%e7%8a%b6%e6%80%81%e5%8f%91%e5%b8%83%e8%a6%81%e9%81%bf%e5%85%8d%e4%b9%b1%e5%ba%8f" class="header-anchor"&gt;&lt;/a&gt;6. 状态发布要避免乱序
&lt;/h2&gt;&lt;p&gt;控制循环拥有可变状态，UI 和外部系统读取快照。可以为每次状态变更增加单调递增的 &lt;code&gt;Revision&lt;/code&gt;，让订阅者丢弃迟到更新。时间戳有助于诊断，但不同线程、进程和机器上的墙上时钟不适合单独承担排序责任。&lt;/p&gt;
&lt;p&gt;持久化事件日志可以帮助重建执行过程，但不要默认把所有设备状态都改造成事件溯源。高频遥测量大且带噪声，通常更适合时序存储；真正需要审计和恢复的命令、状态转换与 Recipe 快照才值得长期保留。&lt;/p&gt;
&lt;h2 id="7-验证控制模型"&gt;&lt;a href="#7-%e9%aa%8c%e8%af%81%e6%8e%a7%e5%88%b6%e6%a8%a1%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;7. 验证控制模型
&lt;/h2&gt;&lt;p&gt;至少覆盖以下测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个状态允许和拒绝的命令；&lt;/li&gt;
&lt;li&gt;重复启动、重复停止和迟到事件；&lt;/li&gt;
&lt;li&gt;命令排队时发生取消或故障；&lt;/li&gt;
&lt;li&gt;队列满时生产者的行为；&lt;/li&gt;
&lt;li&gt;流程每一步失败后的最终状态；&lt;/li&gt;
&lt;li&gt;暂停、取消和恢复跨越检查点的行为；&lt;/li&gt;
&lt;li&gt;状态通知乱序、重复和订阅者变慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;测试重点是最终状态、已发生的物理副作用和保存的证据，而不只是某个 Mock 方法被调用几次。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;状态机负责守住不变量，命令队列负责建立唯一写顺序，流程编排负责跨设备长事务。三者组合后，UI、远程接口和后台任务都必须通过同一入口提交意图，设备控制权才不会被多个线程和模块瓜分。&lt;/p&gt;
&lt;p&gt;下一篇将讨论 Recipe 和配置版本：哪些参数属于产品工艺，哪些属于设备常数与校准，以及如何保证一次运行始终绑定到可追溯的参数快照。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/extensions/channels" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Channels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/api/system.threading.channels.boundedchannelfullmode" target="_blank" rel="noopener"
 &gt;Microsoft Learn：BoundedChannelFullMode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/threading/cancellation-in-managed-threads" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Cancellation in managed threads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.iec.ch/functional-safety" target="_blank" rel="noopener"
 &gt;IEC：Functional safety&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备软件架构与控制模型（三）：设备生命周期与连接管理</title><link>https://www.jiwei.space/posts/equipment/equipment-architecture/03-device-lifecycle/</link><pubDate>Sun, 12 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/equipment-architecture/03-device-lifecycle/</guid><description>&lt;p&gt;设备对象不是创建出来就能用。它可能尚未通电、驱动未加载、端口已打开但初始化失败，也可能刚刚断连，而上一次运动仍留在控制器中。若代码只用一个 &lt;code&gt;bool IsConnected&lt;/code&gt; 描述这些情况，重复连接、退出卡死和“界面显示在线但命令全部失败”几乎不可避免。&lt;/p&gt;
&lt;p&gt;本文建立一套通用的设备生命周期模型，重点不是枚举多少个状态，而是让启动、运行、停止和故障后的每一步都有唯一责任、明确超时和可验证的最终状态。&lt;/p&gt;
&lt;h2 id="1-连接健康和就绪是三件事"&gt;&lt;a href="#1-%e8%bf%9e%e6%8e%a5%e5%81%a5%e5%ba%b7%e5%92%8c%e5%b0%b1%e7%bb%aa%e6%98%af%e4%b8%89%e4%bb%b6%e4%ba%8b" class="header-anchor"&gt;&lt;/a&gt;1. 连接、健康和就绪是三件事
&lt;/h2&gt;&lt;p&gt;“TCP 已连接”或“句柄不为空”只能证明通信资源曾经建立，不能证明设备可以执行任务。至少要区分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;连接性（Connectivity）&lt;/strong&gt;：端口、会话或驱动句柄是否可用；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;健康度（Health）&lt;/strong&gt;：设备是否报告故障、心跳是否新鲜、关键传感器是否可信；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;就绪度（Readiness）&lt;/strong&gt;：完成当前业务操作所需的前置条件是否满足。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一台相机可以处于已连接但过温报警的状态；一根轴可以健康，却因为尚未回零而不能执行绝对定位。把三者压成一个绿色图标，会让 UI 和自动流程得到错误结论。&lt;/p&gt;
&lt;p&gt;推荐由设备模型发布不可变快照：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Offline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Connecting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Initializing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Busy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Disconnecting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Faulted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;DeviceSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsConnected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsHealthy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsHomed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string?&lt;/span&gt; &lt;span class="n"&gt;ActiveFault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;ObservedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;快照里的时间表示“这份观测何时产生”，调用方还要判断其新鲜度。通信中断后保留最后位置用于诊断是有价值的，但不能把旧位置继续当成当前事实。&lt;/p&gt;
&lt;h2 id="2-生命周期是一组受控转换"&gt;&lt;a href="#2-%e7%94%9f%e5%91%bd%e5%91%a8%e6%9c%9f%e6%98%af%e4%b8%80%e7%bb%84%e5%8f%97%e6%8e%a7%e8%bd%ac%e6%8d%a2" class="header-anchor"&gt;&lt;/a&gt;2. 生命周期是一组受控转换
&lt;/h2&gt;&lt;p&gt;一条常见主路径是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Offline → Connecting → Initializing → Ready → Busy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↑ │ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──── Disconnecting ←─────┴───────────┴───────┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↘ Faulted
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;实际系统可以增加 &lt;code&gt;Degraded&lt;/code&gt;、&lt;code&gt;Recovering&lt;/code&gt; 或 &lt;code&gt;Maintenance&lt;/code&gt;，但不要为每个布尔组合造一个状态。状态机应回答三个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当前允许哪些命令？&lt;/li&gt;
&lt;li&gt;哪个事件能够触发下一次转换？&lt;/li&gt;
&lt;li&gt;转换失败后，系统能诚实确认的状态是什么？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;Faulted&lt;/code&gt; 不是所有异常的垃圾桶。参数校验失败时设备状态可能完全没变；读取一次遥测超时也未必意味着设备故障。只有当失败改变了设备可信度，或者无法确认物理状态时，才需要降级到故障或未知状态。&lt;/p&gt;
&lt;h2 id="3-初始化应当可分步可清理"&gt;&lt;a href="#3-%e5%88%9d%e5%a7%8b%e5%8c%96%e5%ba%94%e5%bd%93%e5%8f%af%e5%88%86%e6%ad%a5%e5%8f%af%e6%b8%85%e7%90%86" class="header-anchor"&gt;&lt;/a&gt;3. 初始化应当可分步、可清理
&lt;/h2&gt;&lt;p&gt;把所有初始化塞进构造函数会带来两个问题：构造无法异步等待，部分成功后的资源也难以释放。更好的做法是把初始化拆成显式阶段，并为每一步记录结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;打开通信资源
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 读取身份与固件版本
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 校验能力和兼容性
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 加载受控参数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 注册回调或启动采集
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 首次状态同步
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 标记 Ready
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;失败清理按已完成步骤的逆序执行，但“逆序补偿”并不保证物理世界回到原点。例如阀门已经打开后通信断开，上位机无法仅凭调用 &lt;code&gt;CloseAsync&lt;/code&gt; 证明阀门已关闭。此时应进入未知或故障状态，并依赖下位机的失联策略和现场确认。&lt;/p&gt;
&lt;h2 id="4-用异步互斥保护生命周期转换"&gt;&lt;a href="#4-%e7%94%a8%e5%bc%82%e6%ad%a5%e4%ba%92%e6%96%a5%e4%bf%9d%e6%8a%a4%e7%94%9f%e5%91%bd%e5%91%a8%e6%9c%9f%e8%bd%ac%e6%8d%a2" class="header-anchor"&gt;&lt;/a&gt;4. 用异步互斥保护生命周期转换
&lt;/h2&gt;&lt;p&gt;连接和断开不能同时执行，也不能让两个按钮各自启动一轮初始化。下面的简化实现使用 &lt;code&gt;SemaphoreSlim&lt;/code&gt; 串行化生命周期转换：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;span class="lnt"&gt;58
&lt;/span&gt;&lt;span class="lnt"&gt;59
&lt;/span&gt;&lt;span class="lnt"&gt;60
&lt;/span&gt;&lt;span class="lnt"&gt;61
&lt;/span&gt;&lt;span class="lnt"&gt;62
&lt;/span&gt;&lt;span class="lnt"&gt;63
&lt;/span&gt;&lt;span class="lnt"&gt;64
&lt;/span&gt;&lt;span class="lnt"&gt;65
&lt;/span&gt;&lt;span class="lnt"&gt;66
&lt;/span&gt;&lt;span class="lnt"&gt;67
&lt;/span&gt;&lt;span class="lnt"&gt;68
&lt;/span&gt;&lt;span class="lnt"&gt;69
&lt;/span&gt;&lt;span class="lnt"&gt;70
&lt;/span&gt;&lt;span class="lnt"&gt;71
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IDeviceTransport&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncDisposable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;OpenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;CloseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDeviceTransport&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncDisposable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SemaphoreSlim&lt;/span&gt; &lt;span class="n"&gt;_gate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Offline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Offline&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Faulted&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;$&amp;#34;Cannot connect while state is {State}.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connecting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Initializing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeviceLifecycleState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Faulted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;TryCloseAfterFailureAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;TryCloseAfterFailureAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CloseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;catch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 这里应记录原始清理异常，但不能覆盖最初的连接异常。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;示例刻意没有把 &lt;code&gt;Faulted&lt;/code&gt; 自动改回 &lt;code&gt;Offline&lt;/code&gt;：清理调用完成不等于已经确认物理状态安全。生产代码还应实现受限时的 &lt;code&gt;DisconnectAsync&lt;/code&gt;，阻止释放期间的新命令，并避免 &lt;code&gt;DisposeAsync&lt;/code&gt; 与正在执行的方法竞态。&lt;/p&gt;
&lt;p&gt;生命周期互斥锁不应覆盖数分钟的工艺流程。长任务应交给后续的命令队列和流程执行器；生命周期锁只保护建立、切换和销毁会话这些短而关键的转换。&lt;/p&gt;
&lt;h2 id="5-取消超时和停止的含义不同"&gt;&lt;a href="#5-%e5%8f%96%e6%b6%88%e8%b6%85%e6%97%b6%e5%92%8c%e5%81%9c%e6%ad%a2%e7%9a%84%e5%90%ab%e4%b9%89%e4%b8%8d%e5%90%8c" class="header-anchor"&gt;&lt;/a&gt;5. 取消、超时和停止的含义不同
&lt;/h2&gt;&lt;p&gt;设备调用至少涉及三种时间边界：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用方取消：用户或上层流程不再需要结果；&lt;/li&gt;
&lt;li&gt;操作超时：在约定期限内没有达到完成条件；&lt;/li&gt;
&lt;li&gt;应用停止：进程正在优雅退出，不再接受新工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CancellationToken&lt;/code&gt; 传递的是协作式取消请求，不会强制终止线程，更不会天然停止硬件。适配器收到取消后，需要根据操作契约发出停止、等待安全点，或者明确报告“等待已取消，但设备动作可能仍在继续”。&lt;/p&gt;
&lt;p&gt;清理操作也不应继续使用已经取消的调用方令牌，否则 &lt;code&gt;CloseAsync&lt;/code&gt; 可能在执行任何清理步骤之前就因取消而抛出异常；但使用 &lt;code&gt;CancellationToken.None&lt;/code&gt; 又可能让退出永久卡住。常见做法是为清理创建独立、受限时的令牌，并把超时后的不确定状态记录下来。&lt;/p&gt;
&lt;h2 id="6-重连必须重新建立事实"&gt;&lt;a href="#6-%e9%87%8d%e8%bf%9e%e5%bf%85%e9%a1%bb%e9%87%8d%e6%96%b0%e5%bb%ba%e7%ab%8b%e4%ba%8b%e5%ae%9e" class="header-anchor"&gt;&lt;/a&gt;6. 重连必须重新建立事实
&lt;/h2&gt;&lt;p&gt;重连不是简单再次调用 &lt;code&gt;Open&lt;/code&gt;。旧会话失效后，至少要重新核对：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设备身份、序列号和固件是否仍是预期对象；&lt;/li&gt;
&lt;li&gt;轴是否移动过、是否需要重新回零；&lt;/li&gt;
&lt;li&gt;输出、阀门和光源的真实状态；&lt;/li&gt;
&lt;li&gt;控制器内是否仍有未完成或排队的命令；&lt;/li&gt;
&lt;li&gt;当前 Recipe、校准和上位机缓存是否仍匹配；&lt;/li&gt;
&lt;li&gt;谁仍持有当前任务和物料的所有权。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此自动重连通常只能恢复“通信”，不能自动恢复“生产”。是否继续任务应由恢复策略根据可证明的检查点决定。涉及材料损失或人身安全时，宁可要求人工确认，也不要从一个推测状态继续。&lt;/p&gt;
&lt;h2 id="7-进程启动与设备启动分开"&gt;&lt;a href="#7-%e8%bf%9b%e7%a8%8b%e5%90%af%e5%8a%a8%e4%b8%8e%e8%ae%be%e5%a4%87%e5%90%af%e5%8a%a8%e5%88%86%e5%bc%80" class="header-anchor"&gt;&lt;/a&gt;7. 进程启动与设备启动分开
&lt;/h2&gt;&lt;p&gt;应用进程成功启动，不等于所有设备都必须已经 Ready。可以让 Generic Host 先建立日志、配置和诊断能力，再由 &lt;code&gt;DeviceSupervisor&lt;/code&gt; 并行或按依赖关系初始化设备。对非关键设备，可以进入降级模式；对关键设备，则阻止流程进入可运行状态。&lt;/p&gt;
&lt;p&gt;同样，应用退出应先停止接收新命令，再取消流程、等待设备进入安全点、关闭会话，最后释放 Host。不要直接调用 &lt;code&gt;Environment.Exit&lt;/code&gt; 跳过受控关闭；但也不能假设优雅停止必然发生，硬件安全仍需独立保证。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;设备生命周期不是一个连接布尔值，而是从资源建立、能力确认到安全关闭的一组受控转换。连接性、健康度和就绪度应分别建模；初始化要能处理部分失败；取消等待不能冒充物理停止；重连后必须重新建立事实。&lt;/p&gt;
&lt;p&gt;下一篇将把执行入口收敛到状态机和有界命令队列，并区分暂停、取消、正常停止和急停，避免多个调用方争夺设备写控制权。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/extensions/generic-host" target="_blank" rel="noopener"
 &gt;Microsoft Learn：.NET Generic Host&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/aspnet/core/fundamentals/host/hosted-services" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Background tasks with hosted services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/threading/cancellation-in-managed-threads" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Cancellation in managed threads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/garbage-collection/implementing-disposeasync" target="_blank" rel="noopener"
 &gt;Microsoft Learn：IAsyncDisposable&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备软件架构与控制模型（二）：硬件抽象层与设备适配器</title><link>https://www.jiwei.space/posts/equipment/equipment-architecture/02-hal-adapters/</link><pubDate>Sat, 11 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/equipment-architecture/02-hal-adapters/</guid><description>&lt;p&gt;设备软件很少从零控制硬件。相机、运动控制卡、光源和传感器通常附带 C/C++ DLL、.NET 封装或通信手册。直接调用这些 API 可以很快点亮设备，却会把厂商枚举、句柄、错误码、线程限制和单位扩散到整个项目，最终让流程难以测试、硬件难以替换。&lt;/p&gt;
&lt;p&gt;硬件抽象层（Hardware Abstraction Layer，HAL）的价值不是“给 SDK 再套一层接口”，而是建立一套由业务需要定义、语义稳定且可验证的设备能力模型。本文将用运动轴作为贯穿示例，说明能力接口、适配器和模拟器应该如何分工。半导体设备中运动控制与硬件抽象的领域实践见《&lt;a class="link" href="https://www.jiwei.space/posts/semiconductor/equipment-software/04-motion-hardware-abstraction/" &gt;半导体设备软件（四）：运动控制与硬件抽象&lt;/a&gt;》。&lt;/p&gt;
&lt;h2 id="1-抽象能力不抽象厂商函数"&gt;&lt;a href="#1-%e6%8a%bd%e8%b1%a1%e8%83%bd%e5%8a%9b%e4%b8%8d%e6%8a%bd%e8%b1%a1%e5%8e%82%e5%95%86%e5%87%bd%e6%95%b0" class="header-anchor"&gt;&lt;/a&gt;1. 抽象能力，不抽象厂商函数
&lt;/h2&gt;&lt;p&gt;假设厂商 SDK 提供以下函数：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OpenCard(cardNo)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SetProfile(axisNo, velocity, acceleration)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MoveAbs(axisNo, pulse)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GetMotionStatus(axisNo)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GetLastError()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;机械地把每个函数改成 C# 方法，只是把调用位置搬了家。上层仍然要知道脉冲换算、状态位、到位条件和错误码，并不能替换实现。&lt;/p&gt;
&lt;p&gt;能力接口应该从调用者真正需要的语义出发。例如流程需要“以指定速度移动到某个物理位置，并在到位或失败后结束”：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Millimeters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;MillimetersPerSecond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;AxisStopMode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Decelerated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Immediate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;AxisSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Millimeters&lt;/span&gt; &lt;span class="n"&gt;ActualPosition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsMoving&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsServoOn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;PositiveLimit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;NegativeLimit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string?&lt;/span&gt; &lt;span class="n"&gt;FaultCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IAxis&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AxisSnapshot&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;MoveAbsoluteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Millimeters&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;MillimetersPerSecond&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;StopAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;AxisStopMode&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里没有暴露卡号、脉冲数或厂商状态字。强类型单位让位置和速度不容易被交换，也迫使适配器集中处理脉冲当量、零点和符号方向。接口是否使用 &lt;code&gt;ValueTask&lt;/code&gt; 不是架构重点；若实现通常异步完成，使用 &lt;code&gt;Task&lt;/code&gt; 同样合理，不应为了微小分配收益增加调用复杂度。&lt;/p&gt;
&lt;h2 id="2-把语义翻译集中在适配器"&gt;&lt;a href="#2-%e6%8a%8a%e8%af%ad%e4%b9%89%e7%bf%bb%e8%af%91%e9%9b%86%e4%b8%ad%e5%9c%a8%e9%80%82%e9%85%8d%e5%99%a8" class="header-anchor"&gt;&lt;/a&gt;2. 把语义翻译集中在适配器
&lt;/h2&gt;&lt;p&gt;适配器负责把稳定能力映射到具体 SDK，它至少需要处理五件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生命周期：打开、初始化、关闭句柄；&lt;/li&gt;
&lt;li&gt;数据映射：单位、坐标、枚举和位域；&lt;/li&gt;
&lt;li&gt;完成语义：区分“命令已接受”和“物理动作已完成”；&lt;/li&gt;
&lt;li&gt;错误映射：保留原始错误码，同时提供稳定的领域分类；&lt;/li&gt;
&lt;li&gt;并发约束：遵守 SDK 对调用线程和重入的要求。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;简化后的轮询等待可以写成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;span class="lnt"&gt;58
&lt;/span&gt;&lt;span class="lnt"&gt;59
&lt;/span&gt;&lt;span class="lnt"&gt;60
&lt;/span&gt;&lt;span class="lnt"&gt;61
&lt;/span&gt;&lt;span class="lnt"&gt;62
&lt;/span&gt;&lt;span class="lnt"&gt;63
&lt;/span&gt;&lt;span class="lnt"&gt;64
&lt;/span&gt;&lt;span class="lnt"&gt;65
&lt;/span&gt;&lt;span class="lnt"&gt;66
&lt;/span&gt;&lt;span class="lnt"&gt;67
&lt;/span&gt;&lt;span class="lnt"&gt;68
&lt;/span&gt;&lt;span class="lnt"&gt;69
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VendorAxisAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;IVendorMotionApi&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;axisNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;pulsesPerMillimeter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TimeProvider&lt;/span&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAxis&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;MoveAbsoluteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Millimeters&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;MillimetersPerSecond&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;targetPulse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pulsesPerMillimeter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;MidpointRounding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AwayFromZero&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 速度同样按脉冲当量换算为 pulse/s，与目标位置保持同一单位域。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartAbsoluteMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;axisNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;targetPulse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pulsesPerMillimeter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;VendorAxisState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axisNumber&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FaultCode&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceFaultException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;AxisMotionFailed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FaultCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsMoving&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InPosition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AxisSnapshot&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;VendorAxisState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axisNumber&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;AxisSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Millimeters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActualPulse&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pulsesPerMillimeter&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsMoving&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsServoOn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PositiveLimit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NegativeLimit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FaultCode&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="n"&gt;StopAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;AxisStopMode&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axisNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;AxisStopMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Immediate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;其中 &lt;code&gt;IVendorMotionApi&lt;/code&gt; 和 &lt;code&gt;VendorAxisState&lt;/code&gt; 是对原始 SDK 的窄封装：前者负责调用厂商函数，后者把一次读取所需的状态位和实际脉冲数打包返回。它们没有在示例中展开，因为具体签名必须以所用 SDK 版本为准。&lt;/p&gt;
&lt;p&gt;这段代码只是说明职责位置，不代表所有轴都应以 20 ms 轮询。支持事件回调或硬件完成通知时，应按厂商文档选择机制；轮询周期也要结合控制器负载和业务响应要求测量。取消等待只表示调用方不再等待，适配器还必须按照设备策略决定是否发减速停止，不能假定取消 &lt;code&gt;Task&lt;/code&gt; 会让物理轴自动停下。&lt;/p&gt;
&lt;h2 id="3-错误需要两层信息"&gt;&lt;a href="#3-%e9%94%99%e8%af%af%e9%9c%80%e8%a6%81%e4%b8%a4%e5%b1%82%e4%bf%a1%e6%81%af" class="header-anchor"&gt;&lt;/a&gt;3. 错误需要两层信息
&lt;/h2&gt;&lt;p&gt;只抛出 &lt;code&gt;Exception(&amp;quot;移动失败&amp;quot;)&lt;/code&gt; 会丢失现场证据；让流程判断 &lt;code&gt;-2017&lt;/code&gt; 又会把厂商实现泄漏出去。一个可维护的错误模型通常同时保留：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;稳定分类：超时、通信中断、联锁不满足、设备报警、参数非法；&lt;/li&gt;
&lt;li&gt;原始证据：厂商错误码、命令、控制器状态字和发生时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeviceFaultException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;faultKind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;vendorCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$&amp;#34;Device operation failed: {faultKind}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FaultKind&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faultKind&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;VendorCode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vendorCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;预期内且高频出现的业务拒绝，例如“门未关闭，不能启动”，可以返回显式结果；真正无法继续的驱动故障更适合异常。关键不是统一用返回值还是异常，而是调用契约必须说明哪些结果可恢复、物理动作是否已经开始，以及失败后设备可能处于什么状态。&lt;/p&gt;
&lt;h2 id="4-资源所有权必须唯一"&gt;&lt;a href="#4-%e8%b5%84%e6%ba%90%e6%89%80%e6%9c%89%e6%9d%83%e5%bf%85%e9%a1%bb%e5%94%af%e4%b8%80" class="header-anchor"&gt;&lt;/a&gt;4. 资源所有权必须唯一
&lt;/h2&gt;&lt;p&gt;多数设备 SDK 背后持有非托管句柄、回调或驱动缓冲区。创建适配器的一方应该明确拥有并释放这些资源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原生句柄优先封装为 &lt;code&gt;SafeHandle&lt;/code&gt;，避免在普通业务类里手写终结器；&lt;/li&gt;
&lt;li&gt;异步关闭需要等待时，实现 &lt;code&gt;IAsyncDisposable&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;不要让多个适配器分别关闭同一个全局 SDK；&lt;/li&gt;
&lt;li&gt;回调注册和注销使用同一个委托实例，并在关闭前停止产生新回调；&lt;/li&gt;
&lt;li&gt;关闭过程设计为幂等，允许在部分初始化失败后再次执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;.NET 的垃圾回收器只管理托管内存，不会自动理解厂商句柄代表的硬件资源。&lt;code&gt;Dispose&lt;/code&gt; 也不是“将来有空再清理”的提示，而是资源所有权协议的一部分。&lt;/p&gt;
&lt;h2 id="5-线程安全不能靠猜"&gt;&lt;a href="#5-%e7%ba%bf%e7%a8%8b%e5%ae%89%e5%85%a8%e4%b8%8d%e8%83%bd%e9%9d%a0%e7%8c%9c" class="header-anchor"&gt;&lt;/a&gt;5. 线程安全不能靠猜
&lt;/h2&gt;&lt;p&gt;厂商 SDK 是否线程安全，应以对应版本文档和实测为准。文档没有明确保证时，保守做法是让一个所有者串行化对设备的写操作，而不是在 UI、流程和监控线程中同时调用 SDK。&lt;/p&gt;
&lt;p&gt;读状态也不一定可以任意并发：有的 SDK 使用进程级“最后错误码”，有的回调只能在初始化线程处理，还有的底层总线本来就是半双工。适配层应隐藏这些限制，并向上层提供一致的异步契约。若必须切换到专用线程，应把线程调度封装在适配器内部，而不是要求每个调用方记住 &lt;code&gt;Invoke&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="6-模拟器要实现同一份契约"&gt;&lt;a href="#6-%e6%a8%a1%e6%8b%9f%e5%99%a8%e8%a6%81%e5%ae%9e%e7%8e%b0%e5%90%8c%e4%b8%80%e4%bb%bd%e5%a5%91%e7%ba%a6" class="header-anchor"&gt;&lt;/a&gt;6. 模拟器要实现同一份契约
&lt;/h2&gt;&lt;p&gt;模拟器不是返回固定成功值的 Mock。它至少要保留真实接口的重要行为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作需要时间，状态会经历变化；&lt;/li&gt;
&lt;li&gt;非法状态下拒绝命令；&lt;/li&gt;
&lt;li&gt;支持取消、超时和故障注入；&lt;/li&gt;
&lt;li&gt;单位、范围与软限位规则一致；&lt;/li&gt;
&lt;li&gt;能构造断连、卡住和部分完成等失败场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于流程只依赖 &lt;code&gt;IAxis&lt;/code&gt;，组合根可以决定使用真实适配器还是模拟器：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 需要 using Microsoft.Extensions.Configuration;（Worker 项目模板的隐式 using 已包含它）。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Equipment:Simulation&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAxis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SimulatedAxis&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAxis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VendorAxisAdapter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不要在业务代码里到处判断 &lt;code&gt;if (isSimulation)&lt;/code&gt;。真实与仿真实现的差异应留在组合根和适配层，否则两条逻辑路径会逐渐漂移。&lt;/p&gt;
&lt;h2 id="7-评审一份-hal-的问题清单"&gt;&lt;a href="#7-%e8%af%84%e5%ae%a1%e4%b8%80%e4%bb%bd-hal-%e7%9a%84%e9%97%ae%e9%a2%98%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;7. 评审一份 HAL 的问题清单
&lt;/h2&gt;&lt;p&gt;在接口稳定之前，可以逐项检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法描述的是业务能力，还是照搬厂商函数名？&lt;/li&gt;
&lt;li&gt;单位、坐标系、精度和范围是否明确？&lt;/li&gt;
&lt;li&gt;返回完成时，物理设备究竟到达了什么状态？&lt;/li&gt;
&lt;li&gt;取消或超时后，动作仍可能继续吗？&lt;/li&gt;
&lt;li&gt;原始错误证据是否保留，同时避免流程依赖厂商码？&lt;/li&gt;
&lt;li&gt;谁拥有句柄、回调和关闭责任？&lt;/li&gt;
&lt;li&gt;并发调用是否有文档依据，还是仅仅“目前没出过问题”？&lt;/li&gt;
&lt;li&gt;同一套契约能否由模拟器实现并用于测试？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个接口越短，不一定越好；一个接口越通用，也不一定越抽象。好的 HAL 会准确表达本设备需要保证的能力，并主动拒绝无法诚实实现的“万能接口”。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;硬件抽象层是语义边界：上层说物理能力，适配器处理厂商细节。单位换算、完成条件、错误映射、线程限制和资源所有权都应在这条边界上得到统一解释。这样做不仅为了将来换硬件，更为了让今天的流程能够被模拟、测试和诊断。&lt;/p&gt;
&lt;p&gt;下一篇将继续讨论设备从离线到可运行的生命周期，解决初始化一半失败、重复连接、退出清理和重启后状态重建等问题。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/garbage-collection/implementing-dispose" target="_blank" rel="noopener"
 &gt;Microsoft Learn：实现 Dispose 方法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/fundamentals/runtime-libraries/system-runtime-interopservices-safehandle" target="_blank" rel="noopener"
 &gt;Microsoft Learn：SafeHandle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/standard/datetime/timeprovider-overview" target="_blank" rel="noopener"
 &gt;Microsoft Learn：TimeProvider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/extensions/dependency-injection#service-lifetimes" target="_blank" rel="noopener"
 &gt;Microsoft Learn：依赖注入中的服务生存期&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>设备软件架构与控制模型（一）：上位机与下位机的职责边界</title><link>https://www.jiwei.space/posts/equipment/equipment-architecture/01-boundaries/</link><pubDate>Fri, 10 Jul 2026 10:00:00 +0800</pubDate><guid>https://www.jiwei.space/posts/equipment/equipment-architecture/01-boundaries/</guid><description>&lt;p&gt;一套设备能在实验室里“跑起来”，不代表它已经具备可交付的软件架构。相机、运动轴、传感器和 PLC 接入之后，最容易出现的问题不是少一个按钮，而是职责散落：界面直接调用厂商 SDK，流程代码同时处理通信重连，控制器与上位机各自保存一份状态，安全动作甚至依赖 Windows 程序及时响应。&lt;/p&gt;
&lt;p&gt;本文面向刚进入上位机或设备软件开发的工程师，先回答一个决定后续设计的问题：&lt;strong&gt;什么应该由上位机负责，什么必须留在下位机或独立安全链中？&lt;/strong&gt; 示例以 C#/.NET 10 为背景，但职责划分并不依赖某一种 UI 框架或控制器品牌。半导体场景下的领域化展开（状态机、Recipe、SECS/GEM、晶圆厂集成）见《&lt;a class="link" href="https://www.jiwei.space/posts/semiconductor/equipment-software/01-architecture/" &gt;半导体设备软件（一）：设备软件的整体架构&lt;/a&gt;》系列，本组只讲与行业无关的通用方法。&lt;/p&gt;
&lt;h2 id="1-上位机和下位机不是按机器外壳划分"&gt;&lt;a href="#1-%e4%b8%8a%e4%bd%8d%e6%9c%ba%e5%92%8c%e4%b8%8b%e4%bd%8d%e6%9c%ba%e4%b8%8d%e6%98%af%e6%8c%89%e6%9c%ba%e5%99%a8%e5%a4%96%e5%a3%b3%e5%88%92%e5%88%86" class="header-anchor"&gt;&lt;/a&gt;1. “上位机”和“下位机”不是按机器外壳划分
&lt;/h2&gt;&lt;p&gt;上位机通常运行在工业 PC 上，负责操作界面、流程编排、数据管理、诊断和对外集成；下位机可能是 PLC、运动控制器、嵌入式控制板或仪器固件，负责贴近硬件的采样和控制。但这只是常见形态，不是定义。&lt;/p&gt;
&lt;p&gt;更可靠的划分方法是看四个约束：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;约束&lt;/th&gt;
 &lt;th&gt;更适合放在哪里&lt;/th&gt;
 &lt;th&gt;原因&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;必须在确定的短周期内闭环&lt;/td&gt;
 &lt;td&gt;实时控制器或设备固件&lt;/td&gt;
 &lt;td&gt;桌面操作系统的调度延迟不具备硬实时保证&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;断网、进程崩溃后仍必须生效&lt;/td&gt;
 &lt;td&gt;下位机或独立安全回路&lt;/td&gt;
 &lt;td&gt;不能把安全依赖于上位机存活&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;经常变化且需要复杂业务规则&lt;/td&gt;
 &lt;td&gt;上位机&lt;/td&gt;
 &lt;td&gt;更适合版本管理、测试和快速迭代&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;需要大量存储、分析和人机交互&lt;/td&gt;
 &lt;td&gt;上位机&lt;/td&gt;
 &lt;td&gt;工业 PC 的算力、存储和 UI 能力更合适&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;因此，“电机移动到 125.0 mm”可以由上位机发起，但位置环通常在运动控制器内闭合；“检测到门打开后切断危险输出”也不应等待 WPF 事件处理程序执行。上位机可以展示联锁状态、阻止流程继续并记录证据，却不应成为唯一的安全屏障。&lt;/p&gt;
&lt;p&gt;边界不是越靠下越好。把 Recipe 校验、批次追踪和复杂调度全部塞进 PLC，同样会让系统难以演进。目标是让每项职责位于能够满足其时序、安全和维护要求的最低复杂度层级。&lt;/p&gt;
&lt;h2 id="2-从五类职责看完整设备"&gt;&lt;a href="#2-%e4%bb%8e%e4%ba%94%e7%b1%bb%e8%81%8c%e8%b4%a3%e7%9c%8b%e5%ae%8c%e6%95%b4%e8%ae%be%e5%a4%87" class="header-anchor"&gt;&lt;/a&gt;2. 从五类职责看完整设备
&lt;/h2&gt;&lt;p&gt;可以先把系统按职责分成五层。它们是逻辑边界，不要求对应五个进程或五个项目：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;操作与集成层 UI、权限、MES/上层系统接口
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;流程与领域层 状态机、任务编排、Recipe、业务规则
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;设备能力层 相机采集、轴运动、光源控制、传感器读取
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;厂商适配与通信层 SDK、串口、TCP、现场总线、错误码映射
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;控制器与安全层 实时闭环、硬联锁、安全 PLC、驱动器保护
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最重要的依赖方向是：流程依赖“设备能力”，而不是依赖某个品牌的 DLL。厂商适配层实现能力接口，并把厂商类型、线程模型和错误码限制在边界内。这样更换相机或增加模拟器时，流程层不需要认识新的 SDK。&lt;/p&gt;
&lt;p&gt;UI 也不是控制核心。按钮应提交一个带权限和前置条件的意图，由应用层决定能否执行；设备状态则通过只读快照或事件投影给 UI。若点击事件里同时出现 &lt;code&gt;MessageBox&lt;/code&gt;、数据库事务和 &lt;code&gt;VendorAxis.Move()&lt;/code&gt;，说明边界已经被穿透。&lt;/p&gt;
&lt;h2 id="3-命令状态和事实要分开"&gt;&lt;a href="#3-%e5%91%bd%e4%bb%a4%e7%8a%b6%e6%80%81%e5%92%8c%e4%ba%8b%e5%ae%9e%e8%a6%81%e5%88%86%e5%bc%80" class="header-anchor"&gt;&lt;/a&gt;3. 命令、状态和事实要分开
&lt;/h2&gt;&lt;p&gt;设备软件里经常混淆三类信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命令（Command）&lt;/strong&gt; 表达“希望系统做什么”，例如开始运行或移动轴；命令可能被拒绝、取消或失败。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态（State）&lt;/strong&gt; 表达系统当前允许什么，例如 &lt;code&gt;Ready&lt;/code&gt;、&lt;code&gt;Running&lt;/code&gt;、&lt;code&gt;Faulted&lt;/code&gt;；状态是经过规则解释后的模型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事实（Observation/Event）&lt;/strong&gt; 表达实际发生了什么，例如限位输入变为有效、控制器返回故障码、图像采集完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;命令成功返回也不一定代表物理动作已经完成。某些 SDK 的 &lt;code&gt;Move&lt;/code&gt; 只表示命令已进入控制器队列，真正完成需要等待到位信号，并同时检查伺服报警、限位和超时。适配层必须把这种厂商语义翻译成统一契约，不能让每条业务流程自行猜测。&lt;/p&gt;
&lt;p&gt;状态也不能由 UI 随意写入。较稳健的数据流是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;操作意图 → 命令处理器 → 设备能力接口 → 适配器/控制器
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 观测值与结果事件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 状态投影 → UI
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这条单向链路能避免“界面显示 Ready，但控制器仍在初始化”这样的双重事实源。&lt;/p&gt;
&lt;h2 id="4-进程边界与架构边界不是一回事"&gt;&lt;a href="#4-%e8%bf%9b%e7%a8%8b%e8%be%b9%e7%95%8c%e4%b8%8e%e6%9e%b6%e6%9e%84%e8%be%b9%e7%95%8c%e4%b8%8d%e6%98%af%e4%b8%80%e5%9b%9e%e4%ba%8b" class="header-anchor"&gt;&lt;/a&gt;4. 进程边界与架构边界不是一回事
&lt;/h2&gt;&lt;p&gt;小型设备完全可以先采用模块化单体：一个进程中包含 UI、流程、设备抽象和适配器，通过明确的项目引用与接口维持边界。把每个模块拆成服务并不会自动获得可靠性，反而会引入网络故障、部署协调和分布式状态一致性问题。&lt;/p&gt;
&lt;p&gt;只有当故障隔离、权限边界、独立升级或资源占用确有需要时，才值得引入进程边界。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不稳定的 32 位厂商 SDK 无法加载进 64 位主进程，可以用独立进程承载；&lt;/li&gt;
&lt;li&gt;图像处理需要独立 GPU 进程并允许单独重启；&lt;/li&gt;
&lt;li&gt;控制服务必须在无人登录时作为 Windows Service 运行，而 UI 只是客户端；&lt;/li&gt;
&lt;li&gt;多个客户端需要通过受控 API 访问同一台设备。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即使拆成多个进程，设备的写控制权也应有唯一所有者。两个进程同时向同一控制器下发命令，通常比单进程故障更难诊断。&lt;/p&gt;
&lt;h2 id="5-用-generic-host-管理进程生命周期"&gt;&lt;a href="#5-%e7%94%a8-generic-host-%e7%ae%a1%e7%90%86%e8%bf%9b%e7%a8%8b%e7%94%9f%e5%91%bd%e5%91%a8%e6%9c%9f" class="header-anchor"&gt;&lt;/a&gt;5. 用 Generic Host 管理进程生命周期
&lt;/h2&gt;&lt;p&gt;.NET Generic Host 把依赖注入、配置、日志、后台服务和优雅停止组合在统一生命周期中。它既能承载 Worker Service，也能作为 WinForms/WPF 应用中的组合根。下面只展示骨架，具体设备接口将在下一篇展开：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;HostApplicationBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateApplicationBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IMachineController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MachineController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAxis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SimulatedAxis&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHostedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DeviceSupervisor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;IHost&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;对于桌面 UI，通常由应用启动代码调用 &lt;code&gt;StartAsync&lt;/code&gt;，在退出流程中调用 &lt;code&gt;StopAsync&lt;/code&gt; 并释放 Host，而不是同时调用会阻塞至关闭的 &lt;code&gt;RunAsync&lt;/code&gt;。还要注意，优雅停止不是崩溃恢复机制：断电、进程被强制终止时，&lt;code&gt;StopAsync&lt;/code&gt; 可能没有机会执行，所以安全状态必须由控制器、驱动器或独立回路兜底。&lt;/p&gt;
&lt;h2 id="6-用场景验证边界"&gt;&lt;a href="#6-%e7%94%a8%e5%9c%ba%e6%99%af%e9%aa%8c%e8%af%81%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;6. 用场景验证边界
&lt;/h2&gt;&lt;p&gt;架构评审不要只看方框图，可以逐个追问失败场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;UI 无响应时，正在运行的轴由谁停止？&lt;/li&gt;
&lt;li&gt;网线拔掉后，控制器继续执行还是进入安全状态？这个策略由谁定义？&lt;/li&gt;
&lt;li&gt;上位机重启后，如何确认设备真实位置和当前物料，而不是加载一份过期缓存？&lt;/li&gt;
&lt;li&gt;Recipe 更新到一半失败，下一次运行使用哪个版本？&lt;/li&gt;
&lt;li&gt;两个客户端同时发送启动命令，谁拥有控制权？&lt;/li&gt;
&lt;li&gt;厂商 SDK 卡死时，能否隔离、超时或重启，而不破坏物理安全？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果答案是“UI 会处理”“正常不会发生”或“重启试试”，说明职责边界还没有真正落地。边界的价值正是在非正常路径上决定谁负责检测、谁负责处置、谁保存证据。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;上位机适合承担变化快、数据多、交互复杂的业务；下位机适合承担贴近硬件、时序确定的控制；独立安全链负责不能依赖通用软件存活的保护。逻辑分层不等于强行拆进程，但设备的事实源和写控制权必须清楚。&lt;/p&gt;
&lt;p&gt;下一篇将沿着“流程只依赖设备能力”的原则，设计硬件抽象层和厂商适配器，并处理单位、错误语义、线程模型与资源所有权这些最容易泄漏的细节。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/extensions/generic-host" target="_blank" rel="noopener"
 &gt;Microsoft Learn：.NET Generic Host&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/extensions/workers" target="_blank" rel="noopener"
 &gt;Microsoft Learn：Worker Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/dotnet/core/extensions/windows-service" target="_blank" rel="noopener"
 &gt;Microsoft Learn：使用 BackgroundService 创建 Windows Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.iec.ch/functional-safety" target="_blank" rel="noopener"
 &gt;IEC：Functional safety&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>