ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建p-net PROFINET从站:GSDML编写与IO映射实战

从零搭建p-net PROFINET从站:GSDML编写与IO映射实战 工业以太网这块PROFINET 从站开发一直是个绕不开的坎。早些年做从站要么买现成的协议芯片要么用商业协议栈成本高不说出了问题基本只能等原厂支持自己连调试的入口都没有。后来 p-net 这个开源协议栈出来后情况好了不少——它把 PROFINET 从站的核心逻辑用 C 语言实现了一遍代码量可控移植性也不错跑在 STM32、Linux 甚至裸机上都有先例。但真要从零把一个 p-net 从站跑通光看官方 README 是远远不够的GSDML 文件怎么写、IO 数据怎么映射、跟西门子 PLC 联调时地址怎么对应这些坑一个都躲不掉。这篇就把我从零搭一个 p-net 从站的完整过程拆开讲包括协议栈选型理由、GSDML 编写要点、IO 数据映射逻辑、跟 PLC 联调时的排查链路以及实测中踩过的几个典型坑。适合有一定 C 语言和嵌入式基础、想自己动手做 PROFINET 从站的朋友参考。1. 为什么选 p-net 而不是商业协议栈1.1 商业协议栈的隐性成本做 PROFINET 从站市面上能选的商业方案其实不少比如西门子的开发包、HMS 的 Anybus 系列、以及一些第三方协议栈授权。这些方案的好处是成熟稳定认证过的代码拿来就能用。但问题也很明显授权费按产品出货量算小批量的时候单价高得离谱源码不开放遇到问题只能提工单等回复而且很多商业栈是跟特定硬件绑定的换平台就得重新谈。我之前接触过一个项目用某商业协议栈做从站功能都调通了结果客户要求换主控芯片原厂直接说“不支持该平台”整个方案推倒重来。这种被动感是促使我转向开源方案的核心原因。1.2 p-net 的定位与能力边界p-net 是 rt-labs 开源出来的一个 PROFINET 设备端协议栈用 C99 写的核心目标是让开发者能在资源受限的嵌入式平台上实现一个符合 PROFINET 规范的从站。它支持 RT实时通信类别Class 1 的循环数据交换支持报警和诊断也支持 SNMP 和 LLDP 这些辅助协议。但要注意它的边界p-net 不实现 IRT等时实时所以对同步精度要求极高的运动控制场景它搞不定它也不包含 PROFIsafe 功能安全层安全相关的应用得自己另想办法。对于大多数工厂自动化里的普通 IO 设备、传感器、执行器来说RT 已经够用了这也是 p-net 最合适的应用场景。1.3 从零搭建的可行性判断有人会问从零搭一个 PROFINET 从站工作量到底有多大我的实测感受是如果只是跑通基本循环数据交换一个熟练的嵌入式工程师大概两到三周能搞定如果要加上报警、诊断、SNMP 这些完整功能再考虑认证那时间要翻倍。p-net 本身把协议栈的复杂度封装得比较好你主要的工作量在三个地方硬件以太网驱动的适配、GSDML 文件的编写、以及跟 PLC 的联调。这三块里GSDML 和联调是最耗时间的协议栈本身的移植反而相对直接。2. 搭建前的环境与硬件准备2.1 硬件平台的选择逻辑p-net 对硬件的要求其实不高核心就两点一个能跑 C 代码的 MCU加一个以太网 MAC 控制器。我这次用的是 STM32F407 加 LAN8720 PHY原因很简单——手头有现成的板子而且 STM32 的以太网外设驱动资料多出问题好查。如果你用 Linux 平台比如树莓派或者带网口的工控板那更省事p-net 官方就提供了 Linux 的示例移植直接用 socket 收发原始以太网帧就行。但 Linux 平台有个问题非实时系统循环周期抖动比较大如果 PLC 那边对实时性要求高可能会报通信超时。所以如果是产品化我还是建议用 MCU 裸机或者 RTOS 方案。选硬件的时候有个细节容易被忽略PHY 芯片要支持 100Mbps 全双工而且最好支持自动协商。有些便宜的 PHY 只支持 10Mbps跑 PROFINET 会直接不通信因为 PROFINET RT 要求至少 100Mbps。2.2 软件工具链的搭建软件这边需要准备的东西交叉编译工具链我用的是 arm-none-eabi-gcc版本 10.3 以上太老的版本对 C99 支持不完整。p-net 源码从 GitHub 上 clone 最新的 master 分支注意看 release notes有些版本对特定平台有 bug 修复。GSDML 编辑器推荐用免费的 GSDML Editor或者直接手写 XML 也行但编辑器能帮你检查语法错误。PLC 编程软件TIA Portal用来组态和联调版本 V15 以上都行。网络抓包工具Wireshark必须装 PROFINET 解析插件不然你看到的只是一堆原始以太网帧。工具链搭好后先别急着改代码把 p-net 自带的 Linux 示例编译跑一遍确认协议栈本身能正常工作。这一步能帮你排除掉很多环境问题比如编译器版本不兼容、依赖库缺失之类的。2.3 目录结构与移植接口p-net 的源码目录结构比较清晰核心文件在src/下面包括pnet.c主状态机、pf_cmdev.c设备管理、pf_cmsu.c状态机、pf_ppm.c生产者状态机等。移植的时候你主要关注ports/目录下的平台适配层。需要自己实现的接口函数主要有这几个接口函数作用实现要点pnal_init()网络抽象层初始化初始化以太网外设和 PHYpnal_eth_send()发送以太网帧直接操作 MAC 控制器发送pnal_eth_receive()接收以太网帧从接收缓冲区取帧注意处理多帧情况pnal_get_mac_address()获取 MAC 地址从 EEPROM 或芯片唯一 ID 读取pnal_get_system_time()获取系统时间毫秒级用于超时判断这些函数的实现质量直接决定了协议栈能不能稳定运行。特别是接收函数如果处理不好缓冲区溢出或者丢帧会导致 PLC 那边频繁报通信错误。3. GSDML 文件从站的身份说明书3.1 GSDML 到底在描述什么GSDML 文件是 PROFINET 从站的“身份证”PLC 在组态的时候就是靠这个文件来识别从站支持什么功能、有多少个模块、每个模块的 IO 数据长度是多少。没有 GSDML 文件PLC 根本不知道你这是个什么设备组态界面里都找不到。一个完整的 GSDML 文件包含几个核心部分设备基本信息厂商 ID、设备 ID、版本号、模块列表每个模块的 IO 配置、参数定义可配置的参数、以及诊断报警的定义。文件格式是 XML有固定的 schema写错了 TIA Portal 会直接报错连导入都导入不进去。3.2 手写 GSDML 的关键字段我一开始想用编辑器自动生成但发现编辑器生成的模板太死板很多自定义模块加不进去最后还是手写。手写的时候这几个字段必须搞清楚DeviceIdentity VendorID0x1234 DeviceID0x0001 InfoText TextIdT_DeviceName/ VendorName ValueMyVendor/ /DeviceIdentityVendorID和DeviceID是厂商和设备的唯一标识必须跟协议栈代码里设置的值一致否则 PLC 扫描的时候会报“设备类型不匹配”。这个坑我踩过代码里写的是0x1234GSDML 里写的是0x1235结果 PLC 死活连不上查了半天才发现是这里不一致。模块定义部分ModuleItem IDModule_1 ModuleIdentNumber0x00000001 ModuleInfo Name TextIdT_ModuleName/ InfoText TextIdT_ModuleInfo/ /ModuleInfo IOData Input Length4/ Output Length4/ /IOData /ModuleItemModuleIdentNumber是模块的标识号协议栈里在模块插拔的时候会用到。Input Length和Output Length是 IO 数据的字节长度这个长度必须跟协议栈里配置的 IO 缓冲区大小一致不然数据会错位。3.3 模块化设计与实际案例PROFINET 从站支持模块化设计一个设备可以插多个模块每个模块有独立的 IO 数据。比如我做的这个从站设计了三个模块一个 4 字节输入模块、一个 4 字节输出模块、一个 8 字节输入输出混合模块。这样 PLC 组态的时候可以根据实际需求选择插哪些模块。模块化设计的好处是灵活但代价是 GSDML 文件会变复杂而且协议栈里要处理模块插拔的回调。p-net 提供了模块插拔的回调接口你需要在回调里更新 IO 数据的映射关系。这块逻辑如果写错了PLC 那边会看到模块状态异常但具体哪里错了光看 PLC 的诊断信息很难定位得结合 Wireshark 抓包分析。4. IO 数据映射从站跟 PLC 之间的数据通道4.1 循环数据交换的基本原理PROFINET RT 的循环数据交换本质上是 PLC 和从站之间周期性地互相发送以太网帧帧里带着 IO 数据。PLC 作为控制器按照组态时设定的周期比如 1ms、2ms、4ms发送输出数据从站收到后更新自己的输出从站按照同样的周期发送输入数据PLC 收到后更新自己的输入。这个周期叫SendClockFactor在 GSDML 里定义实际周期 SendClockFactor × 31.25μs。比如SendClockFactor32实际周期就是 1ms。从站必须在这个周期内完成数据的收发和处理如果超时PLC 会报通信错误。4.2 p-net 里的 IO 数据结构p-net 用pnet_io_data结构体来管理 IO 数据里面分了输入、输出、以及每个模块对应的缓冲区。你在应用层要做的就是在这个缓冲区跟实际的硬件 IO 之间做映射。// 输入数据从站发给 PLC 的数据 uint8_t input_data[4] {0x01, 0x02, 0x03, 0x04}; pnet_input_set_data_and_iops(net, 0, 0, 0, input_data, 4, PNET_IOPS_GOOD); // 输出数据PLC 发给从站的数据 uint8_t output_data[4]; pnet_output_get_data_and_iops(net, 0, 0, 0, output_data, iops);pnet_input_set_data_and_iops里的参数依次是网络实例、API、槽号、子槽号、数据指针、数据长度、IOPSIO 生产者状态。IOPS 是 PROFINET 里用来表示数据有效性的一个状态位PNET_IOPS_GOOD表示数据有效如果设备故障可以设成PNET_IOPS_BADPLC 那边会收到诊断报警。4.3 数据映射的常见错误与排查IO 数据映射最容易出的问题是“数据错位”。比如你定义了 4 字节输入但实际只填了 2 字节剩下的 2 字节是随机值PLC 读到的数据就是乱的。还有一种情况是字节序问题PROFINET 用的是大端序如果你的 MCU 是小端序在传输多字节数据比如 float、int32的时候需要做字节序转换。排查数据错位最直接的方法是用 Wireshark 抓包看 PROFINET 的 IO 数据帧里实际传的是什么。Wireshark 能解析出每个模块的 IO 数据你对照 GSDML 里的定义就能看出是哪个模块的数据对不上。我遇到过一次PLC 读到的输入数据一直是 0抓包发现从站根本没发输入帧最后查出来是pnet_input_set_data_and_iops的槽号参数写错了写成了 1实际应该是 0。5. 跟西门子 PLC 联调地址对应与通信建立5.1 PLC 侧的组态步骤在 TIA Portal 里组态一个 PROFINET 从站步骤大概是安装 GSDML 文件 → 在硬件目录里找到设备 → 拖到网络视图 → 连接 PLC 的 PROFINET 接口 → 设置设备名称和 IP 地址 → 配置模块和 IO 地址。这里有个关键点设备名称。PROFINET 靠设备名称来识别从站而不是 IP 地址。PLC 在启动的时候会先发一个 DCP发现和配置协议的请求从站回应后PLC 根据设备名称把 IP 地址分配给从站。所以从站的设备名称必须跟 PLC 组态里设置的一致否则通信建立不起来。设备名称在 p-net 里通过pnet_set_name()设置或者在应用层调用pnet_set_state()之前设置好。我建议在代码里把设备名称写死跟 GSDML 里的默认名称保持一致这样调试的时候少一个变量。5.2 地址映射的对应关系PLC 组态的时候会给每个模块分配 IO 地址比如输入地址从I0.0开始输出地址从Q0.0开始。这些地址是 PLC 内部的地址跟从站的槽号、子槽号有对应关系。PLC 地址从站槽号子槽号数据方向长度I0.0-I3.701输入4 字节Q0.0-Q3.702输出4 字节I4.0-I11.711输入8 字节Q4.0-Q11.712输出8 字节这个对应关系必须在 GSDML 里定义清楚而且协议栈里的槽号、子槽号要跟 GSDML 一致。如果对不上PLC 读到的数据就是错的或者干脆读不到。5.3 通信建立失败的排查链路跟 PLC 联调的时候通信建立不起来是最常见的问题。我的排查链路是这样的第一步确认物理连接。看网口的 Link 灯亮不亮如果不亮检查网线、PHY 配置、以及 MAC 控制器的初始化。这一步能排除掉大部分硬件问题。第二步抓包看 DCP 报文。PLC 启动后会发 DCP Identify 请求从站应该回应 DCP Identify Response。如果抓不到从站的回应说明协议栈的 DCP 处理有问题或者设备名称没设置对。第三步检查 GSDML 一致性。如果 DCP 能通但 PLC 报“设备类型不匹配”那就是 GSDML 里的 VendorID、DeviceID 跟协议栈代码里的不一致。这个前面提过是最容易犯的错误。第四步看 AR 建立过程。DCP 通了之后PLC 会发起 AR应用关系建立请求从站回应后AR 建立成功开始循环数据交换。如果 AR 建立失败通常是 IO 配置不匹配比如 GSDML 里定义的模块数量跟协议栈里实际配置的不一致。这个排查链路我走过好几遍每次卡住的时候按这个顺序走一遍基本都能定位到问题。6. 实测中踩过的几个典型坑6.1 以太网驱动的接收缓冲区溢出p-net 在接收以太网帧的时候如果处理不及时接收缓冲区会溢出导致丢帧。丢帧的后果是 PLC 报通信错误但错误信息很模糊只说是“通信中断”不告诉你具体原因。我一开始用的是查询方式接收主循环里轮询接收缓冲区。后来发现循环周期稍微长一点就会丢帧。改成中断方式接收在中断里把帧拷贝到队列主循环从队列里取问题就解决了。但中断方式也有坑中断里不能做太耗时的操作拷贝帧的时候要注意长度别越界。6.2 设备名称的字符编码问题PROFINET 的设备名称有字符集限制只允许小写字母、数字、连字符和点号。我一开始用了下划线结果 PLC 组态的时候报“名称无效”。改成连字符后正常。这个限制在 PROFINET 规范里有明确定义但 p-net 的文档里没强调容易忽略。另外设备名称的长度也有限制最长 240 个字符但实际用的时候建议短一点太长的话 PLC 组态界面显示不全容易看错。6.3 循环周期的抖动问题在 Linux 平台上跑 p-net循环周期的抖动比较大有时候能到几百微秒。如果 PLC 组态的周期是 1ms抖动几百微秒还能接受但如果周期是 500μs抖动就可能导致超时。解决方法是把协议栈的线程优先级调高用SCHED_FIFO调度策略并且绑定 CPU 核心避免被其他线程抢占。如果还是不行那就只能换 MCU 裸机方案了。我在 STM32 上跑裸机循环周期抖动能控制在 10μs 以内稳定性好很多。6.4 GSDML 导入 TIA Portal 报错GSDML 文件写好后导入 TIA Portal 的时候报错提示“XML 架构验证失败”。这种情况通常是 XML 语法问题比如标签没闭合、属性值没加引号、或者用了 schema 里没定义的标签。排查方法是先用 XML 验证工具比如 xmllint验证一遍看具体是哪一行报错。TIA Portal 的报错信息不够详细只告诉你“验证失败”不告诉你具体位置。用 xmllint 能精确定位到行号省很多时间。7. 从站功能扩展与认证考虑7.1 报警和诊断功能的实现p-net 支持 PROFINET 的报警和诊断但默认是不开启的需要你在应用层调用相应的接口。报警分两种诊断报警和过程报警。诊断报警用于报告设备故障比如传感器断线过程报警用于报告过程事件比如阈值超限。实现报警的关键是调用pnet_diag_std_add()或者pnet_diag_usi_add()把诊断信息加到协议栈的诊断缓冲区里协议栈会自动把诊断信息发给 PLC。PLC 收到后会在诊断界面里显示出来。这块功能我建议在产品化的时候加上因为现场调试的时候没有诊断信息排查问题全靠猜效率太低。7.2 认证的必要性与准备如果从站要卖给客户而且客户要求符合 PROFINET 规范那就需要做认证。认证由 PIPROFINET International授权的测试实验室进行测试内容包括协议一致性、互操作性、以及网络负载测试。认证的费用不低而且测试周期长通常要几个月。如果只是内部项目或者小批量应用可以不做认证但要做好跟不同品牌 PLC 的互操作性测试。我实测过跟西门子 S7-1200、S7-1500 的通信基本没问题但跟一些国产 PLC 通信的时候遇到过 DCP 报文格式不兼容的情况需要针对性调整。7.3 性能优化的几个方向如果从站的 IO 数据量比较大或者循环周期要求比较短性能优化就很重要。几个方向减少内存拷贝p-net 在收发数据的时候会有内存拷贝如果数据量大拷贝开销不可忽略。可以通过零拷贝的方式优化但需要改协议栈代码风险较高。优化中断处理以太网接收中断里尽量少做事把帧处理放到主循环或者低优先级任务里。使用 DMA如果 MCU 支持以太网 DMA尽量用 DMA 收发减少 CPU 占用。这些优化不是必须的但如果你的应用对实时性要求高值得花时间做。8. 一些实操心得与建议p-net 这个协议栈我用了大概半年多从最初的跑通 Demo 到后来的产品化踩了不少坑也积累了一些经验。如果你正准备从零搭一个 PROFINET 从站我有几个建议第一先把 Linux 示例跑通。别一上来就移植到 MCU先用 Linux 平台把协议栈的基本功能验证一遍确认你对 GSDML、IO 映射、PLC 联调这些流程都理解了再动手移植。这样能省很多时间。第二GSDML 文件一定要用工具验证。手写 XML 很容易出语法错误用 xmllint 或者 GSDML Editor 验证一遍能避免很多导入报错。第三抓包工具是必备的。Wireshark 加 PROFINET 插件能看到协议栈收发的每一个报文排查问题的时候比看代码和日志高效得多。第四设备名称和 VendorID/DeviceID 一定要跟 GSDML 一致。这是最容易犯的错误也是最难排查的错误因为 PLC 的报错信息不会直接告诉你哪里不一致。第五循环周期的抖动要实测。别只看理论值用示波器或者 GPIO 翻转的方式实测一下确认抖动在可接受范围内。如果抖动太大先优化调度和中断再考虑换平台。最后说一句p-net 虽然开源但它的代码质量还是不错的注释也比较全。遇到问题的时候先看代码再看文档最后再提 issue。很多时候答案就在代码里。
返回列表