C#读写西门子S7 PLC实战:基于S7netplus的数据采集与上位机开发 1. 项目概述与核心价值最近在做一个工厂数据采集的项目客户现场清一色都是西门子的S7系列PLC从老款的S7-200 SMART到主流的S7-1200/1500甚至还有几台S7-300。项目需求很明确就是要用C#写一个稳定可靠的上位机把这些PLC里的生产数据比如设备状态、产量、温度压力等工艺参数实时读上来展示在监控大屏上同时还要能写入一些控制指令。这听起来像是工业自动化领域的“Hello World”但真做起来从通讯协议的理解、库的选择到异常处理和性能优化每一步都有不少门道。网上资料虽然多但要么过于零散只讲某个点要么就是封装好的商业库只给用法不讲原理出了问题两眼一抹黑。所以我决定结合这个实际项目把用C#读写西门子S7系列PLC的完整流程、核心原理、踩过的坑以及优化心得系统地梳理出来并附上经过实战检验的源码。无论你是刚接触工控领域的C#开发者还是正在为数据采集头疼的工程师这篇文章都能给你提供一条从零到一、清晰可循的实现路径。我们不止要“跑通”更要理解背后的“为什么”这样才能在复杂的现场环境中快速定位和解决问题。2. S7通讯协议基础与库选型2.1 西门子S7协议简介要和西门子PLC通讯首先得搞清楚它“说”什么语言。西门子S7系列PLC主要指S7-200/300/400/1200/1500使用一套私有的、基于以太网的通讯协议通常我们称之为S7协议或S7 Communication。它运行在OSI模型的传输层和应用层之上底层基于标准的TCP/IP。你可以把它想象成一种“方言”规定了数据包该怎么组织、怎么寻址、怎么确认。这套协议的核心是读写存储区。PLC的内存被划分成几个明确的区域I区 (Inputs) 物理输入映像区对应PLC的输入端子状态。Q区 (Outputs) 物理输出映像区对应PLC的输出端子状态。M区 (Marks/Bits) 中间标志位存储器相当于全局变量常用于程序内部逻辑。DB区 (Data Blocks) 数据块这是最常用、最灵活的数据存储区。我们程序里定义的变量比如DB1.DBD10表示浮点数基本都放在这里。T区 (Timers)和C区 (Counters) 定时器和计数器区域。我们的C#程序就是要按照S7协议的“语法”构造一个数据请求包告诉PLC“请把DB1里从第10个字节开始连续4个字节的数据发给我”。PLC收到后会解析这个请求找到对应数据再按照协议格式打包回复给我们。注意S7协议是西门子的私有协议官方并未完全公开其所有细节。因此我们通常使用第三方开源库来实现这些库是社区通过逆向工程和分析网络报文逐步完善的。2.2 C#通讯库选型与对比既然要写C#程序选一个靠谱的通讯库是第一步。市面上主流的几个选择各有优劣1. S7netplus这是目前.NET平台包括.NET Core/.NET 5下最活跃、最受欢迎的开源S7通讯库。它是经典库S7.Net的现代化分支支持异步操作代码结构清晰社区支持好。它完美支持S7-1200/1500等新一代PLC对S7-300/400以及S7-200 SMART作为S7客户端时也支持良好。对于绝大多数应用场景S7netplus是我的首选推荐。2. Sharp7 (S7Sharp)另一个轻量级、高性能的选择。它的API设计非常简洁性能据说在某些场景下比S7netplus稍好。但相对而言其社区活跃度和文档丰富度略逊于S7netplus。如果你追求极致的简洁和可控可以尝试。3. Libnodave / dotnetnodave这是一个更底层、跨平台甚至支持C/C的库。功能强大但API相对复杂对新手不太友好。通常在对性能有极端要求或者需要深度定制通讯过程时才会考虑。为什么本项目选择S7netplus对于快速开发、稳定至上、需要良好社区支持的上位机项目S7netplus提供了最佳平衡点。它封装了协议细节提供了如ReadBytesWriteBytesReadClass直接映射到C#类等高阶API大大降低了开发难度。同时它的源代码可读性强当遇到奇怪的问题时我们可以深入源码去排查这是商业库无法比拟的优势。接下来的教程和源码都将基于S7netplus展开。3. 开发环境准备与项目搭建3.1 软件与硬件准备在开始写代码之前我们需要把环境准备好。软件清单Visual Studio 2022 社区版即可确保安装了.NET桌面开发或.NET Core跨平台开发 workload。西门子TIA Portal博图 用于配置PLC特别是设置PLC的IP地址、组态PLC变量、以及启用PUT/GET通信访问。这一步至关重要PLC默认可能禁止来自其他设备的读写请求。网络调试助手如Wireshark 非必须但在通讯调试失败时它是终极的排错利器可以抓取和分析你和PLC之间的原始网络数据包。硬件与网络连接PLC 一台西门子S7-1200或S7-1500本文以S7-1200为例。PC 运行C#上位机的电脑。交换机/网线 将PLC和PC连接到同一个局域网中确保它们IP地址在同一网段且能互相Ping通。例如PLC IP设为192.168.0.1 PC IP设为192.168.0.100。PLC关键配置在TIA Portal中完成设置IP地址 在设备视图的网络配置中为PLC的以太网接口分配固定IP。启用PUT/GET访问 这是最容易忽略的一步。在PLC的设备配置中找到“防护与安全” - “连接机制”必须勾选“允许来自远程对象的PUT/GET通信访问”。如果不勾选外部读写请求会被PLC拒绝。组态DB块并取消优化块访问 在S7-1200/1500中默认生成的DB块是“优化的块访问”其变量没有固定的绝对地址如DB1.DBX0.0不利于传统方式通讯。我们需要右键点击DB块 - 属性 - 取消勾选“优化的块访问”。这样变量才会有我们熟悉的字节偏移地址。3.2 创建C#项目并引入S7netplus打开Visual Studio新建一个“控制台应用”或“WinForms应用”项目根据你的UI需求选择项目名称例如S7PlcReader。通过NuGet包管理器安装S7netplus。在“工具”-“NuGet包管理器”-“管理解决方案的NuGet程序包”中搜索S7netplus并安装。或者直接在程序包管理器控制台输入Install-Package S7netplus。安装成功后你可以在代码中通过using S7.Net;来引入命名空间。现在基础环境就搭建好了。我们可以开始进入核心的通讯代码部分。4. 核心通讯代码实现详解4.1 建立连接与基础读写首先我们需要创建一个PLC连接对象。S7netplus支持多种CPU类型通过CpuType枚举来指定。using S7.Net; using System.Net; // 1. 创建PLC实例 // 参数CPU类型, IP地址, 机架号, 槽号 // S7-1200/1500的槽号通常为0或1参看TIA Portal硬件组态 Plc plc new Plc(CpuType.S71200, 192.168.0.1, 0, 1); // 对于S7-300/400需要根据硬件配置设置正确的机架号和槽号 // Plc plc new Plc(CpuType.S7300, 192.168.0.1, 0, 2); try { // 2. 打开连接 plc.Open(); Console.WriteLine(成功连接到PLC); // 3. 读取一个字节例如读取M区第0个字节 byte mByte plc.ReadByte(DataType.DataBlock, 0, 0); // 注意对于M区DB号传0 Console.WriteLine($M0.0 - M0.7的值: {Convert.ToString(mByte, 2).PadLeft(8, 0)}); // 4. 读取DB块中的数据例如读取DB1中前10个字节 byte[] dbData plc.ReadBytes(DataType.DataBlock, 1, 0, 10); Console.WriteLine($DB1.DBB0 - DB1.DBB9: {BitConverter.ToString(dbData)}); // 5. 写入一个位例如将DB1.DBX0.0置为True plc.WriteBit(DataType.DataBlock, 1, 0, 0, true); Console.WriteLine(已写入DB1.DBX0.0 True); // 6. 写入多个字节例如向DB1.DBB10开始写入4个字节 byte[] dataToWrite new byte[] { 0x01, 0x02, 0x03, 0x04 }; plc.WriteBytes(DataType.DataBlock, 1, 10, dataToWrite); } catch (Exception ex) { Console.WriteLine($通讯发生错误: {ex.Message}); } finally { // 7. 关闭连接 plc.Close(); }代码解析与注意事项DataType.DataBlock 指定要操作的数据类型是DB块。如果要操作M区则使用DataType.Memory并且DB号参数传0。地址计算 S7netplus的读写方法参数顺序通常是(数据类型, DB号, 起始字节地址, 长度或值)。这里的字节地址是相对于存储区起始的偏移量从0开始。例如DB1.DBB10对应的字节地址就是10。位地址 对于WriteBit后两个参数分别是字节地址和位索引0-7。DB1.DBX0.5就是字节地址0位索引5。连接管理 务必使用try-catch-finally确保连接被关闭避免资源泄漏。在生产环境中你可能需要实现连接池或重连机制。4.2 读取复杂数据类型Int, DInt, RealPLC中存储的不只是字节更多的是有实际意义的整数、浮点数等。西门子PLC的数据存储有字节序Endianness的问题即多字节数据如Int、DWord、Real在内存中的字节排列顺序。西门子PLC采用大端序Big-Endian而我们的PCx86/x64架构通常采用小端序Little-Endian。幸运的是S7netplus已经帮我们处理好了这个转换。// 假设PLC中数据定义如下在DB1中 // DB1.DBW0 (Int, 16位整数) // DB1.DBD2 (DInt, 32位整数) // DB1.DBD6 (Real, 32位浮点数即float) // 1. 读取一个Int2字节 short dbInt (short)plc.Read(DataType.DataBlock, 1, 0, VarType.Int, 1); Console.WriteLine($DB1.DBW0 (Int): {dbInt}); // 2. 读取一个DInt4字节 int dbDInt (int)plc.Read(DataType.DataBlock, 1, 2, VarType.DInt, 1); Console.WriteLine($DB1.DBD2 (DInt): {dbDInt}); // 3. 读取一个Real4字节 float dbReal (float)plc.Read(DataType.DataBlock, 1, 6, VarType.Real, 1); Console.WriteLine($DB1.DBD6 (Real): {dbReal}); // 更简洁的方式使用泛型方法ReadT short dbInt2 plc.Readshort(DataType.DataBlock, 1, 0); int dbDInt2 plc.Readint(DataType.DataBlock, 1, 2); float dbReal2 plc.Readfloat(DataType.DataBlock, 1, 6);写入复杂数据类型同样简单plc.Write(DataType.DataBlock, 1, 0, (short)100); // 写入Int plc.Write(DataType.DataBlock, 1, 2, 50000); // 写入DInt plc.Write(DataType.DataBlock, 1, 6, 3.14f); // 写入Real实操心得 在调试时务必先在TIA Portal的监控表中确认PLC中变量的地址和当前值然后用C#程序去读取对比结果是否一致。这是验证通讯是否正常、地址是否正确的最直接方法。如果读上来的数据是乱码或极大/极小的数字很可能是地址错误或字节序处理有问题虽然S7netplus已处理但如果你自己用ReadBytes然后BitConverter转换就需要手动调整字节顺序。4.3 批量读取与性能优化在实时监控场景中我们往往需要周期性地读取几十甚至上百个变量。如果为每个变量都发起一次TCP请求会产生大量网络开销和延迟效率极低。S7协议支持在一个请求包中读取多个连续的地址区域S7netplus也提供了相应的ReadMultipleVars方法。低效做法不推荐var temp1 plc.Readfloat(DataType.DataBlock, 100, 0); var pressure1 plc.Readfloat(DataType.DataBlock, 100, 4); var speed1 plc.Readint(DataType.DataBlock, 100, 8); // ... 每个Read都是一次独立的网络请求高效做法批量读取// 1. 定义要读取的变量列表 ListDataItem dataItems new ListDataItem { new DataItem { DataType DataType.DataBlock, DB 100, StartByteAdr 0, VarType VarType.Real, // 对应float Count 1 // 读取1个Real }, new DataItem { DataType DataType.DataBlock, DB 100, StartByteAdr 4, VarType VarType.Real, Count 1 }, new DataItem { DataType DataType.DataBlock, DB 100, StartByteAdr 8, VarType VarType.DInt, Count 1 } }; // 2. 执行批量读取 plc.ReadMultipleVars(dataItems); // 3. 获取结果 float temperature (float)dataItems[0].Value; float pressure (float)dataItems[1].Value; int speed (int)dataItems[2].Value;性能优化核心要点合并请求 尽可能将地址连续的变量安排在一起然后用一次ReadBytes读取一大段数据然后在内存中解析。这比使用多个DataItem每个可能对应一个PDU有时更高效因为S7协议单个PDU有长度限制默认约240字节有效数据。读取周期 根据数据变化频率设置合理的读取周期。状态信号可能需要100ms而温度等缓变信号1秒甚至5秒读一次即可。避免不必要的频繁读取。异步操作 S7netplus支持ReadAsyncWriteAsync等方法。在UI程序中使用异步可以防止界面卡死。但要注意PLC通讯本身是I/O密集型操作异步主要改善的是客户端响应性对PLC端的压力减轻有限。连接保持 对于需要长期运行的监控程序不要频繁地Open和Close连接。建立一次连接后保持长连接定时发送心跳包如读取一个固定的标志位来检测连接是否中断并实现自动重连逻辑。5. 高级应用与实战技巧5.1 使用ReadClass/WriteClass进行对象映射如果你厌倦了手动计算每个变量在DB块中的字节偏移量S7netplus提供了一个非常强大的功能通过C#类Class与PLC的DB块进行自动映射。这需要你的DB块结构是已知且固定的。首先在PLC中定义一个DB块例如DB100包含以下结构DB100 - StartByte 0: StationID (Int) - StartByte 2: Status (Word) - StartByte 4: Temperature (Real) - StartByte 8: Pressure (Real) - StartByte 12: IsRunning (Bool) // 注意Bool在DB中占1个位但通常占用1个字节或打包然后在C#中定义一个对应的类并使用S7.Net.Types命名空间下的特性来标注偏移量using S7.Net.Types; public class MachineData { [S7NetPlusProperty(Offset 0)] public short StationID { get; set; } [S7NetPlusProperty(Offset 2)] public ushort Status { get; set; } // Word对应C#的ushort [S7NetPlusProperty(Offset 4)] public float Temperature { get; set; } [S7NetPlusProperty(Offset 8)] public float Pressure { get; set; } // 对于Bool需要指定字节偏移和位偏移 [S7NetPlusProperty(Offset 12, Bit 0)] public bool IsRunning { get; set; } }现在你可以像操作普通对象一样读写整个DB块// 读取整个DB块到对象 MachineData data new MachineData(); plc.ReadClass(data, 100); // 第二个参数是DB号 Console.WriteLine($Station: {data.StationID}, Temp: {data.Temperature}); data.IsRunning true; // 将对象写回PLC plc.WriteClass(data, 100);这种方法极大地提高了代码的可读性和可维护性尤其适合读写结构化的配方数据或设备参数。5.2 处理数组与字符串读取数组 PLC中的数组在内存中是连续存储的。例如DB1.DBB20开始的一个Array[0..9] of Int占用20个字节10个元素 * 2字节/元素。// 读取10个Int的数组 short[] intArray new short[10]; plc.Read(DataType.DataBlock, 1, 20, VarType.Int, 10); // 注意Read方法返回object需要转换。更推荐用ReadBytes然后手动转换或使用Buffer.BlockCopy byte[] bytes plc.ReadBytes(DataType.DataBlock, 1, 20, 20); Buffer.BlockCopy(bytes, 0, intArray, 0, bytes.Length);读写字符串 西门子PLC中的字符串通常有两种格式String有最大长度和Char数组。S7-1200/1500的String类型前面有2个字节的头最大长度和当前长度。处理起来需要小心。// 假设DB1.DBB30开始是一个String[20]最大20个字符实际占22字节220 // 读取 byte[] stringBytes plc.ReadBytes(DataType.DataBlock, 1, 30, 22); int currentLen stringBytes[1]; // 第二个字节是当前长度 string plcString Encoding.ASCII.GetString(stringBytes, 2, currentLen); // 写入 string myString Hello PLC; byte[] strToWrite new byte[22]; strToWrite[0] 20; // 最大长度 strToWrite[1] (byte)myString.Length; // 当前长度 Encoding.ASCII.GetBytes(myString, 0, myString.Length, strToWrite, 2); plc.WriteBytes(DataType.DataBlock, 1, 30, strToWrite);注意事项 字符串编码要一致PLC通常使用ASCII或ANSI编码。如果PLC中字符串是WString宽字符串Unicode则需要使用Encoding.Unicode。5.3 构建一个简单的上位机监控界面一个完整的项目通常需要UI。这里以WinForms为例展示一个极简的监控循环。在窗体上放置几个Label和TextBox用于显示数据一个Button用于手动读取一个Timer控件用于定时自动读取。将PLC连接对象plc声明为窗体类成员变量。在窗体加载事件中初始化连接在窗体关闭事件中关闭连接。在Timer的Tick事件或按钮的Click事件中执行数据读取并更新UI。关键代码片段private Plc plc; private System.Windows.Forms.Timer readTimer; private void Form1_Load(object sender, EventArgs e) { plc new Plc(CpuType.S71200, 192.168.0.1, 0, 1); try { plc.Open(); readTimer.Start(); // 启动定时读取 } catch (Exception ex) { MessageBox.Show($连接失败: {ex.Message}); } } private void readTimer_Tick(object sender, EventArgs e) { // 注意在UI线程中直接进行同步网络操作会卡住界面 // 应该使用异步或后台线程如BackgroundWorker/Task Task.Run(async () { try { float temp await plc.ReadAsyncfloat(DataType.DataBlock, 100, 4); // 使用Invoke回UI线程更新控件 this.Invoke(new Action(() { textBoxTemperature.Text temp.ToString(F2); })); } catch (Exception ex) { // 处理异常如记录日志、尝试重连 this.Invoke(new Action(() { labelStatus.Text $读取错误: {ex.Message}; })); } }); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { readTimer.Stop(); plc?.Close(); }UI线程安全 务必记住所有网络IO操作都不应该在UI主线程上同步执行。使用async/await或Task.Run将其放到后台线程然后通过Control.Invoke来安全地更新UI控件。6. 故障排查与常见问题即使代码看起来正确在实际连接PLC时也可能会遇到各种问题。下面是一个常见问题排查清单。问题现象可能原因排查步骤与解决方案PlcException: Error 0x00000000或连接超时1. 网络不通。2. PLC IP地址错误。3. 防火墙阻止了TCP 102端口。4. PLC未上电或网线故障。1. 在PC上Ping PLC的IP地址确认物理连通性。2. 检查TIA Portal中PLC的IP配置。3. 暂时关闭PC和PLC的防火墙测试或将102端口加入例外。4. 检查PLC运行状态和网线指示灯。PlcException: Error 0x00000500(资源不可用)1. PLC的PUT/GET访问未启用。2. 同时连接的客户端过多PLC有连接数限制。3. 机架号(Rack)/槽号(Slot)设置错误。1.最重要的一步在TIA Portal中确认PLC属性-“防护与安全”-“连接机制”已勾选允许PUT/GET。2. 检查PLC的连接资源断开不必要的HMI、编程软件连接。3. 对于S7-300/400机架号和槽号必须与硬件组态完全一致。对于S7-1200/1500通常Rack0 Slot1参看设备视图。能连接但读上来的数据全是0或错误1. DB块号或字节地址错误。2. DB块未取消“优化的块访问”。3. 变量类型不匹配如用Int读Real。4. PLC程序未运行或变量未被写入值。1. 使用TIA Portal的监控表输入绝对地址如%DB1.DBW0查看当前值与C#代码中的地址仔细核对。2. 确认DB块属性中“优化的块访问”已取消勾选。3. 核对PLC中变量的数据类型确保与C#读取时指定的VarType一致。4. 将PLC切换到RUN模式并确保有程序在向该变量写数。写入成功但PLC无反应1. 写入的地址是只读的如I区。2. PLC程序中有其他地方覆盖了你的写入值如每扫描周期都赋初值。3. 写入的数据格式或单位错误。1. 确保写入的是Q、M、DB区等可写区域。2. 在PLC程序中搜索该地址看是否有其他逻辑如MOVE指令在修改它。3. 在监控表中手动写入一个值看PLC是否有预期动作以排除程序逻辑问题。通讯间歇性中断或速度慢1. 网络拥塞或干扰。2. 读取频率过高PLC或网络处理不过来。3. 未使用批量读取请求次数过多。1. 检查网络设备使用独立的工业交换机避免与办公网络混用。2. 降低读取周期非关键数据延长读取间隔。3. 重构代码使用ReadMultipleVars或合并ReadBytes请求。使用ReadClass时映射错误1. C#类中的[S7NetPlusProperty]偏移量设置错误。2. C#类属性类型与PLC变量类型不匹配。3. DB块中有未在C#类中定义的变量导致整体偏移计算错误。1. 使用TIA Portal查看DB块的离线视图精确核对每个变量的偏移量Byte Offset。2. 参考S7NetPlus的文档确认.NET类型与PLC类型的对应关系如Word对应ushort。3. 确保C#类覆盖了从起始偏移量开始的所有字节或者使用[S7NetPlusProperty(Offset..., Length...)]来跳过某些区域。终极调试武器Wireshark当所有常规手段都失效时使用Wireshark抓包。过滤条件设为tcp.port 102。你可以看到你的C#程序发出的S7请求包和PLC的回复包。通过分析原始报文可以精确判断是请求包格式错误还是PLC回复了错误代码。例如如果看到PLC回复了错误代码0x05资源不可用那就回头去检查PUT/GET设置和连接数。7. 项目源码结构与扩展建议一个健壮的上位机项目代码不应该全部堆在按钮事件里。这里提供一个建议的项目结构S7PlcCommunicator/ ├── S7PlcCommunicator.csproj ├── App.config (可配置IP、机架号等) ├── Services/ │ ├── IPlcService.cs (接口) │ └── S7PlcService.cs (实现类封装所有读写、连接管理、重连逻辑) ├── Models/ │ ├── PlcConnectionConfig.cs (配置类) │ └── MachineData.cs (映射PLC DB的类) ├── Utilities/ │ └── Logger.cs (日志工具) └── Forms/ └── MainForm.cs (主界面)核心服务类S7PlcService示例public class S7PlcService : IPlcService, IDisposable { private Plc _plc; private PlcConnectionConfig _config; private Timer _heartbeatTimer; private bool _isConnected false; public event EventHandlerbool ConnectionStateChanged; public event EventHandlerstring DataUpdateReceived; public S7PlcService(PlcConnectionConfig config) { _config config; _plc new Plc(config.CpuType, config.IpAddress, config.Rack, config.Slot); _plc.ReadTimeout 2000; _plc.WriteTimeout 2000; _heartbeatTimer new Timer(5000); // 5秒心跳 _heartbeatTimer.Elapsed async (s, e) await CheckConnectionAsync(); } public async Taskbool ConnectAsync() { try { await _plc.OpenAsync(); _isConnected true; _heartbeatTimer.Start(); ConnectionStateChanged?.Invoke(this, true); return true; } catch (Exception ex) { Logger.Error($连接PLC失败: {ex.Message}); return false; } } private async Task CheckConnectionAsync() { try { // 尝试读取一个固定的标志位作为心跳 var value await _plc.ReadAsyncbool(DataType.DataBlock, 1, 0, 0); if (!_isConnected) { _isConnected true; ConnectionStateChanged?.Invoke(this, true); } } catch { if (_isConnected) { _isConnected false; ConnectionStateChanged?.Invoke(this, false); // 可以在这里触发重连逻辑 await ReconnectAsync(); } } } public async TaskMachineData ReadMachineDataAsync(int dbNumber) { if (!_isConnected) throw new InvalidOperationException(PLC未连接); var data new MachineData(); await Task.Run(() _plc.ReadClass(data, dbNumber)); DataUpdateReceived?.Invoke(this, 数据已更新); return data; } // ... 其他读写方法 Dispose方法等 }扩展方向配置化 将PLC的IP、DB块地址、变量映射关系等放到JSON或XML配置文件中实现灵活配置无需重新编译。数据持久化 将读取到的数据存入数据库如SQLite, SQL Server, InfluxDB用于历史查询、报表分析。报警与通知 设定变量的上下限超限时触发报警并通过邮件、短信等方式通知相关人员。OPC UA集成 如果你的系统需要更标准的工业互联可以考虑将S7数据通过S7NetPlus读取后再通过一个OPC UA服务器暴露出去方便其他支持OPC UA的SCADA或MES系统访问。多PLC/多线程管理 在一个上位机中同时连接多台PLC需要使用多线程或异步编程来管理多个连接避免阻塞。通过这样的结构化和封装你的C#上位机程序将从一个简单的测试脚本进化成一个可维护、可扩展、健壮的工业数据采集应用。记住工业环境下的软件稳定性和可靠性永远是第一位的良好的错误处理和日志记录是必不可少的保障。