
搞工控的兄弟应该都遇到过这种需求手头一批温湿度传感器、电表、变频器或者扫码枪设备本身不带以太网口只有一路RS485跑Modbus RTU更原始一点的干脆是自定义协议。上位机又要统一把数据采集上来怎么办最朴素也最可靠的办法就是PLC做主站用自由口通讯做轮询一主多从把数据挨个拽回来。用博途SCL写这套轮询逻辑比梯形图清晰不止一个档次。这篇文章是“西门子博途系列学习笔记SCL”的第三篇专门把RS485自由口轮询程序的思路、代码细节和现场调试经验掰开揉碎讲一遍。如果你是刚接触SCL不久正在为怎么写一个稳定可靠的多从站采集程序发愁或者想搞明白自由口通讯从报文组帧、CRC校验到状态机调度到底是怎么回事这篇应该能帮上忙。我尽量按实际项目的套路来写不是说教就是分享我自己的做法和踩过的坑。1. 自由口通讯与轮询机制的整体认知1.1 为什么现场采集大都选RS485自由口工业现场做设备数据采集通讯方式翻来覆去就那么几种RS232、RS485、CAN、以太网。RS232虽然简单但只能点对点距离超过15米基本就废了工业现场根本不够用。以太网当然好速度快、组网灵活但很多老设备、低成本仪表压根没配以太网口加交换机、布线、配IP成本一下就上去了。CAN总线多用于汽车、运动控制这种高速实时场合PLC要接CAN设备往往得加专门的通讯模块也不是普通项目的第一选择。RS485不一样。两线制半双工最远能拉到1200米左右一条总线上能挂32个标准负载用高输入阻抗芯片还可以更多抗干扰能力比RS232强得多。大部分工业仪表、电表、变频器、扫码枪哪怕没有以太网基本都会留一路RS485。对PLC来说只要CPU集成了串口或者加一块便宜的通讯模块就能直接走自由口协议自己定义报文格式想读什么就读什么。所以RS485自由口就成了中小型数据采集项目里最常见、性价比最高的组合。所谓自由口通俗讲就是串口通道完全由用户程序控制收发什么字节、怎么组帧、怎么校验都由你自己写逻辑。西门子博途里对应的概念是Point-to-Point通讯S7-1200/1500通过串口模块或集成RS485口可以用系统块收发数据也可以用SCL自己封装读写逻辑。它不像Modbus RTU协议那样有固定的标准帧格式自由度更高但对应的组帧、容错、超时处理这些工作也都落到你头上。1.2 一主多从轮询到底在解决什么问题在RS485总线上所有设备共享一对线同一时刻只能有一个设备往总线上发数据。所以必须有个主站来掌控节奏从站只能被动等主站点名被点名了才能回话。这就是轮询机制主站按顺序挨个向从站发送请求帧从站解析地址、确认是发给自己的然后返回响应帧如果从站没响应或响应异常主站就跳过它继续问下一个。用生活化的比喻这就像老师上课点名。老师拿着名单挨个喊学号喊到谁谁就起立答到。没人应答就跳过去下一轮再喊。RS485轮询就是这套逻辑主站是老师每个从站分配一个唯一的地址从站必须在收到和自己地址相同的请求时才应答否则保持静默避免多个设备同时抢占总线。这样做的好处很明显逻辑简单可靠不受从站数量限制增减设备只要改地址表就行。缺点是实时性有限因为所有从站要轮流排队从站越多轮询一圈的时间越长。比如9600波特率下一个30字节左右的请求帧加响应帧单站耗时大概100到200毫秒挂10个从站一轮下来就是1到2秒。所以自由口轮询适合数据量小、实时性要求不高秒级以内的场合这也是它在仪表采集、能耗监控这类项目里经久不衰的原因。1.3 什么项目适合自由口轮询什么不适合我自己的判断标准是这样适合的从站数量在2到16个左右、单帧数据量不大几十个字节、通讯周期在百毫秒到秒级都能接受的设备数据采集比如电表、温控器、变频器状态、扫码枪读码结果。设备协议如果是Modbus RTU自由口也能直接模拟不冲突。如果现场设备协议五花八门那就更能体现自由口的价值了——主站程序完全可以只做一个通用框架把不同设备的报文组帧和解析做成可配置的。不适合的从站特别多比如三五十个、要求毫秒级同步、或者大量数据需要连续传输比如固件升级、图片传输。这时候RS485的物理瓶颈就摆在那了自由口做得再漂亮也突破不了波特率和半双工的极限数据量一大整个系统的响应时间会变得很难看。这种情况还是老老实实上以太网或者CAN。2. 硬件准备与组网细节2.1 RS485网络拓扑手拉手才是正道硬件上最大的坑往往不是程序而是接线。RS485组网必须采用菊花链手拉手拓扑也就是从主站出发A线、B线分别把设备一个接一个串起来像一串糖葫芦而不是像星星一样从中心向四周放射。为什么RS485用差分信号传输要求整个总线在电气上是一条完整的通路如果搞星型接线分支过长会产生信号反射总线上的驻波会让通讯出现莫名其妙的不稳定、偶发错误。实际接线时A/B线最好用屏蔽双绞线屏蔽层单端接地一般在PLC侧或电源侧不要两端都接地形成地环路。线径建议0.5平方毫米以上距离越长线径越粗。另外总线上所有设备的A/B极性必须一致很多初学者栽在这里——有的设备A/B标识反的接上去一台设备不回查半天发现是线序反了。从站设备的供电也要注意RS485只是通讯线不负责供电。如果从站和主站距离远、或者现场有大功率设备启停建议从站单独供电不要和主站共用电源避免地电位差过大导致通讯口烧毁或通讯异常。2.2 终端电阻到底加不加终端电阻是RS485组网的另一大细节。它的作用是吸收总线末端的信号反射让信号波形更干净。按照标准应该在总线最远的两端各并联一个120欧姆电阻。注意是两端不是每个设备都加。如果只有两个设备点对点通讯通常两端都要加但有些设备的RS485芯片内置了120欧姆电阻并可以通过跳线切换加了之后总线阻值可能变成60欧姆虽然通讯也能工作但驱动电流会增大可能导致收发器发烫。我自己的习惯是短距离几十米内、只有两三个设备可以不加终端电阻多数情况下也能稳定跑。距离超过100米、或者设备数量超过5个就一定要加。加了还通讯不稳优先检查接地和线缆质量而不是一味加电阻。终端电阻的检查方法也简单断电后用万用表电阻档在总线两端量A/B之间的电阻。如果量到约60欧姆说明两端电阻都加上了量到120欧姆说明只有一端加了量到接近无穷大说明都没加。2.3 TTL转RS485的自动收发电路问题很多传感器模块、扫码枪内部其实是TTL电平串口要通过一块TTL转RS485的收发芯片比如MAX485、SP3485才能挂到RS485总线上。这种扩展板最常见的问题是“自动收发电路”的可靠性。所谓自动收发就是电路里用三极管根据发送数据自动切换收发状态省去MCU或PLC控制DE/RE引脚。这电路在9600波特率下一般没问题但波特率提高后三极管翻转需要时间切换不及时就会把帧头发丢或者把最后一两个字节吞掉。我踩过这种坑某扫码枪模块设成115200波特率发出去的请求帧它收不全降回9600一切正常。后来换了一款带真自动方向控制的模块问题才彻底解决。所以选TTL转RS485模块时别光看价格便宜优先选逻辑处理可靠的方案。如果模块上有DE/RE引脚可以手动控制那就别偷懒直接用MCU的IO或PLC的输出点控制收发切换更可控。实在只能自动收发就把波特率控制在9600到19200之间稳定性会好很多。2.4 现场的EMC与防雷考虑RS485在工业现场跑最大的敌人不是设备故障而是干扰。变频器启动、继电器吸合、电机换向都可能通过电源和空间辐射在RS485线上感应出干扰脉冲轻则误码重则烧通讯口。所以接口保护电路不能省。正规的做法是在RS485接口上加三级防护气体放电管泄放大电流TVS管钳制浪涌电压共模电感抑制高频干扰。具体到电路上常见的是A/B线之间并TVS管A/B线对地串气体放电管再串共模电感进收发芯片。很多成品RS485转接板已经集成了这些保护器件买的时候看清规格别买那种裸芯片电阻的“光板”模块。另外如果通讯距离长且跨区域比如从配电房到车间还建议在总线两端设备上加防雷器防雷器的接地线要单独走线不能和通讯线捆在一起。之前给一个水处理项目做过控制器现场要求标配双电源、防雷接口和足够的RS485通道这些看似硬性的指标本质上都是为通讯可靠性兜底。软件写得再好硬件被一次浪涌打穿一切归零。3. SCL程序框架设计与核心代码3.1 为什么要用状态机来写轮询轮询程序的本质是一个循环往复、带等待和超时的流程控制。发送请求帧后你没法知道从站什么时候回、会不会回所以程序必须能“挂起”当前动作在等待的同时还能做别的比如刷新看门狗、记录故障状态等条件满足后再往下走。如果只写一个从头到尾顺序执行的逻辑从站响应稍慢程序就卡死在等待里了其他逻辑全部停摆。用梯形图写这种跳转逻辑特别容易乱触点线圈一大片分支多了自己都看晕。SCL就不一样了CASE语句天然适合状态机一个整型变量表示当前状态每个状态对应一个处理分支分支之间靠状态变量跳转逻辑清清楚楚。我习惯把轮询状态机分为几个基础状态空闲IDLE、组帧SEND、等待应答WAIT、解析数据PARSE、错误处理ERROR。主程序里再根据这些状态决定底层收发动作。3.2 用UDT管理从站数据别堆一堆数组热词里有人搜“UDT怎么用SCL”这里正好用得上。轮询程序里每个从站除了地址还会有一堆对应的数据比如实时值、状态字、错误码、在线标志。如果每个数据都单独建一个数组程序里地址和数据对不上号维护起来非常痛苦。我建议定义一个用户数据类型UDT代表一个“从站记录”TYPE UDT_SlaveData STRUCT addr : BYTE; // 从站地址 online : BOOL; // 在线标志 errCode : WORD; // 最近一次通讯错误码 value1 : REAL; // 采集值1 value2 : REAL; // 采集值2 rawBuf : ARRAY[0..63] OF BYTE; // 原始响应帧留档方便查问题 END_STRUCT END_TYPE然后再建一个数组元素类型就是UDTVAR slaves : ARRAY[1..16] OF UDT_SlaveData; slaveCount : INT : 4; // 实际从站数 END_VAR这样不仅程序里可读性好调试时在监控表里也能一眼看到哪个从站在线、值是多少、最后收到的原始报文是什么。比零散的变量组清晰太多。3.3 底层串口指令的封装思路在博途里S7-1200/1500的串口收发通常通过系统块或扩展指令完成。不同CPU固件、不同通讯模块指令名字略有差异但思路都一样配置串口参数发送指定长度的字节数组接收并统计收到的字节数。我在项目里习惯再包一层把系统块的差异隔离掉上层SCL只调自己定义的块。比如封装一个FUN_SendFrame和一个FUN_RcvFrame底层是系统块上层轮询程序只跟这两个接口打交道。好处是将来换了CPU型号或者通讯模块只需改这两个封装块轮询逻辑一行不用动。封装块的内部实现大概是// FUN_SendFrame 发送一帧数据 // 底层调用系统串口发送指令把data数组里的len个字节发出去 FUNCTION FUN_SendFrame : BOOL VAR_INPUT data : ARRAY[0..63] OF BYTE; len : INT; END_VAR // 这里实现取决于具体指令最终返回发送是否成功 FUN_SendFrame : TRUE; END_FUNCTION3.4 轮询状态机SCL核心代码下面是我常用的轮询主状态机框架代码做了简化但整体结构和逻辑跟实际项目一致。变量声明部分我写在一起方便你看懂每个变量的作用。FUNCTION_BLOCK FB_PollingMaster VAR_INPUT enable : BOOL; // 总使能 pollIntervalMs : INT : 1000; // 每轮轮询间隔 END_VAR VAR state : INT : 0; // 状态机状态 idx : INT : 0; // 当前轮询到的从站索引 txLen : INT; // 发送帧长度 rxLen : INT; // 接收帧长度 txBuf : ARRAY[0..63] OF BYTE; // 发送缓冲区 rxBuf : ARRAY[0..255] OF BYTE; // 接收缓冲区 reqAddr : BYTE; // 当前请求的从站地址 sendDone : BOOL; // 发送完成标志 rcvDone : BOOL; // 接收完成标志 tonGap : TON; // 帧间隔定时器用来判断一帧接收完毕 tonWait : TON; // 等待超时定时器 slaves : ARRAY[1..16] OF UDT_SlaveData; slaveCount : INT : 4; END_VARCASE state OF // 状态0空闲等使能和周期定时 0: IF enable THEN idx : idx 1; IF idx slaveCount THEN idx : 1; END_IF; state : 10; END_IF; // 状态10构造请求帧 10: txLen : 0; txBuf[0] : slaves[idx].addr; // 地址 txBuf[1] : 16#03; // 功能码读保持寄存器 txBuf[2] : 16#00; // 起始寄存器高字节 txBuf[3] : 16#00; // 起始寄存器低字节 txBuf[4] : 16#00; // 寄存器数量高字节 txBuf[5] : 16#02; // 寄存器数量低字节读2个寄存器 txLen : 6; // 添加CRC校验CRC函数见后文 FB_CRC16(data : txBuf, len : txLen, crc : crcVal); txBuf[txLen] : BYTE_TO_BYTE(crcVal AND 16#FF); // CRC低字节在前 txBuf[txLen 1] : BYTE_TO_BYTE(SHR(INT(crcVal), 8)); // CRC高字节 txLen : txLen 2; state : 20; // 状态20发送请求帧 20: sendDone : FUN_SendFrame(data : txBuf, len : txLen); IF sendDone THEN rxLen : 0; rcvDone : FALSE; tonWait(IN : TRUE, PT : T#500MS); // 单站等待超时500ms state : 30; END_IF; // 状态30等待从站响应 30: // 收到一个字节就存进缓冲区并重置帧间隔定时器 IF rcvNewByte THEN rxBuf[rxLen] : rcvByte; rxLen : rxLen 1; tonGap(IN : FALSE); tonGap(IN : TRUE, PT : T#20MS); // 按波特率计算9600下约10ms足够 rcvNewByte : FALSE; END_IF; // 帧间隔定时器到说明一帧接收完毕 IF tonGap.Q THEN rcvDone : TRUE; END_IF; // 整帧超时到达不管收到多少都结束本次轮询 IF tonWait.Q THEN state : 40; ELSIF rcvDone THEN state : 40; END_IF; // 状态40解析响应帧 40: tonWait(IN : FALSE); IF rxLen 4 THEN // 校验CRC是否正确、地址和功能码是否匹配 FB_CRC16(data : rxBuf, len : rxLen - 2, crc : calcCrc); IF calcCrc WORD(16#0000) // 简化写法实际应做高低字节比较 AND rxBuf[0] slaves[idx].addr AND rxBuf[1] 16#03 THEN slaves[idx].online : TRUE; slaves[idx].errCode : 16#0000; // 数据解析高低字节合并存成REAL或INT slaves[idx].value1 : INT_TO_REAL(WORD_TO_INT( SHL(INT(rxBuf[3]), 8) OR INT(rxBuf[4]))); ELSE slaves[idx].online : FALSE; slaves[idx].errCode : 16#0001; // CRC错误或帧不匹配 END_IF; ELSE slaves[idx].online : FALSE; slaves[idx].errCode : 16#0002; // 超时无响应 END_IF; rxLen : 0; state : 0; END_CASE;上面代码逻辑是完整的只是接收底层变量rcvByte、rcvNewByte需要你在封装块里和实际的通讯指令连起来。状态机的精髓在于每个状态只做一件事状态之间靠标志和定时器驱动永远不会有死循环卡死的问题。3.5 CRC16校验函数的SCL实现自由口通讯最常见的是Modbus RTU协议它的CRC校验是CRC16多项式0xA001初始值0xFFFF结果低字节在前、高字节在后。这个算法不复杂但用SCL写的时候要注意位操作的正确性。FUNCTION FC_CRC16 : WORD VAR_INPUT data : ARRAY[0..255] OF BYTE; // 要校验的数据 len : INT; // 数据长度 END_VAR VAR i : INT; j : INT; crc : WORD; END_VAR BEGIN crc : 16#FFFF; FOR i : 0 TO len - 1 DO crc : crc XOR WORD(data[i]); FOR j : 0 TO 7 DO IF (crc AND 16#0001) 16#0000 THEN crc : SHR(INT(crc), 1) XOR 16#A001; ELSE crc : SHR(INT(crc), 1); END_IF; END_FOR; END_FOR; FC_CRC16 : crc; END_FUNCTION注意点SCL的SHR函数第一个参数是INT类型所以要把WORD转成INT再移位否则某些版本会编译不过。另外CRC最终结果需要按Modbus RTU规范先发低字节、后发高字节发送端和接收端要保持一致。我曾经写反过一次程序逻辑通篇找不出错误后来拿串口调试助手抓报文一对比才发现高低字节顺序反了这类细节一旦错了整个链路上的设备都会互相“看不懂”。3.6 接收完成判断字符间隔定时器的使用SCL轮询程序里怎么判断一帧数据“收完了”Modbus RTU标准规定一帧结束后至少要有3.5个字符时间的静默期接收端看到这个静默期就知道帧结束了。在程序里我通常用一个TON定时器来实现每收到一个字节就重启定时器定时时间到了就认为当前帧结束。9600波特率下一个字符约1毫秒多一点3.5个字符时间约3.6毫秒取整设置成10到20毫秒完全够用。115200波特率下字符时间约0.087毫秒3.5字符约0.3毫秒这时候定时器就要设小建议设5毫秒。我上面的示例代码中写的是20毫秒那是给9600波特率用的你在实际项目里一定要根据波特率换算这直接影响接收的可靠性——设太长会吞掉下一帧帧头设太短会把一帧拆成两帧。4. 实操过程与联调排障4.1 博途组态从零配置串口通道在博途里跑自由口通讯第一步是在设备视图里把串口模块的接口参数配好。以S7-1200搭配串口模块为例双击模块在属性里配置协议模式为“自由口”或PtP通讯设置波特率、数据位、停止位、奇偶校验这些参数必须和从站设备完全一致。一个很常见的坑是校验位很多国产仪表默认为无校验但部分进口设备默认偶校验两边的数据位和校验位对不上通讯结果就是一片乱码。有些型号还需要在程序里用指令激活配置而不是仅仅在硬件组态里改参数。这个务必查阅对应CPU的手册不同型号处理方式不一样。另外模块的硬件标识符Hardware Identifier在调用系统块时要填对组态后可以在PLC变量的系统常量里找到别填错了填错编译能过运行起来收发直接报错。4.2 没有实物从站用Modbus Slave和串口助手模拟项目开发最怕硬件还没到程序先写好了没法验证。我的经验是用PC上的Modbus Slave模拟软件或者串口调试助手就能完成80%的调试。把PLC的RS485口通过USB转485接到电脑PC端开一个Modbus Slave软件地址设成和PLC程序里一致的从站地址寄存器表建几个假数据。然后PLC程序跑起来观察PLC侧接收缓冲区的数据。如果PC端收到了PLC发来的请求帧说明发送链路OKPC端自动应答后PLC侧能解析出数据说明接收链路OK。两边通了再拿实物设备替换模拟器通常只需微调帧格式或地址就能跑通。遇到报文对不上、解析出来是乱码的情况千万不要瞎猜直接在PC端开串口监听把总线上的原始十六进制报文抓出来一帧一帧和协议手册比对。我调试的时候抓到的最经典的一个问题PLC请求帧没问题从站也回了但响应帧里的寄存器数量和请求不一致从站把8个寄存器一股脑全回过来了导致我按2个寄存器解析时数据错位。这种问题只看PLC侧程序永远想不明白看报文一眼就能定位。4.3 现场通讯不稳定的排查顺序程序逻辑没问题、模拟器也通了但一上现场就偶发通讯超时或者数据跳变这是最磨人的情况。我总结了一套排查顺序按优先级来第一看接线。A/B有没有接反屏蔽层是否单端接地是不是星型拓扑。第二看终端电阻。距离长有没有在两端加120欧电阻。第三看电源。从站供电是不是和主站共地、有没有大功率设备在同一回路上。第四看干扰源。通讯线是否和动力电缆同槽走线变频器是否紧挨着通讯线。第五看波特率。现场干扰严重时把波特率从9600往下降一档往往立竿见影。软件层面也要配合。轮询超时不要设得太长太长会让错误状态累积我一般设300到500毫秒连续错误计数达到3次才把从站标记为离线避免偶发干扰导致误报离线。同时如果某个从站连续离线可以把它暂时从轮询列表里摘除等它恢复正常再自动加回来这样就不会因为一个掉线设备拖慢整个轮询周期。4.4 常见问题速查表现象可能原因排查与解决所有从站全部无响应A/B接反、通讯参数不一致、主站未配置好串口助手监听总线确认主站是否有请求发出单个从站无响应该设备地址错误、接线松动、设备掉电单独测试该设备检查地址拨码偶发超时重试后恢复干扰、接地不良、终端电阻缺失检查屏蔽接地、加终端电阻、降波特率响应数据乱码或错位波特率/校验位不一致、高低字节顺序反了抓原始报文逐字节比对通讯正常但数据跳变寄存器地址读错、数据类型解析错误核对设备寄存器表用Modbus Slave验证程序运行一段时间后通讯冻结接收缓冲区溢出、定时器未复位检查接收长度是否越界确认每轮正常清零4.5 关于FactoryIO和虚拟仿真的补充有不少朋友问FactoryIO和博途能不能做联合调试。FactoryIO本身主要用来仿真自动化流水线和传感器执行器场景并不直接仿真RS485总线上的从站设备。如果你想在虚拟环境里验证轮询程序更实用的方案是用Modbus Slave模拟软件替代从站配合PLC的虚拟PLC仿真模式或者用真实PLC连USB转485再接电脑上的模拟软件。这样既能把通讯链路跑通又能反复修改程序验证状态机的健壮性。等实物设备到场后再做一轮现场验证基本能省掉大半调试时间。最后再分享一点小体会RS485自由口轮询程序本身并不复杂真正考验人的是现场那些说不清道不明的干扰和时序问题。真遇到问题时我习惯先把问题拆成“电气”和“逻辑”两半。逻辑问题用串口调试助手抓报文一帧一帧看基本都能定位电气问题就得靠万用表、示波器甚至现场经验去磨了。程序里的坑踩过一次下次就能绕开但现场那些接地、干扰、感应电压才是真正磨人的地方。希望这篇文章能帮你把程序层面的路先走通少踩几个我踩过的坑。