简介:面向西门子博途TIA Portal 1200系列PLC的MODBUS轮询程序工程包,专为工业自动化初学者和需要快速上手多设备通信的开发者准备。资源以完整项目文件形式呈现,共38个文件、大小1.03MB,包含cfs、xml、db等博途工程配置与数据库文件,以及HMI面板、系统日志和索引文件,形成从PLC程序到人机界面的整体结构,可直接在博途环境中加载参考。目前已有3460人学习下载。包内围绕MODBUS轮询机制展开,覆盖RTU/TCP通信参数设置、从站地址映射、基于MB_READ/MB_WRITE指令的循环请求、数据解析与异常重试处理等核心知识点,有助于理解如何按顺序采集多台设备数据并规避通信故障。对于想提升S7-1200编程能力或搭建监控系统的工程师,提供了可借鉴的完整框架和配置细节,能有效缩短从阅读理论到动手实践的距离,同时节省从零搭建项目的时间。
1. 博图MODBUS轮询程序:让PLC成为车间的“数据中转站”
车间改造时最头疼的事,往往是设备不缺、数据不通。变频器、温控表、智能电表各自带着RS485口,手头又没有合适的网关设备,而暂缓改造又会让生产报表一直靠人工抄录。这套基于博途平台的MODBUS轮询程序,就是解决这类场景的:用一台S7-1500做主站,通过RS485总线轮询多台从站仪表,把现场数据统一收敛到PLC的数据块里,再供HMI或上位系统读取。它适合所有要在博途环境里快速落地MODBUS RTU通信的开发者,尤其是做设备集成、老旧产线技改的电气工程师和上位机程序员。整套程序围绕“轮询调度、报文解析、数据映射”三个核心展开,代码直接用SCL写在功能块里,改点位、加从站都不需要重写逻辑。
2. 项目全景:通信选型、程序框架与三个关键DB设计
2.1 为什么是MODBUS RTU:成本、部署与抗干扰的权衡
当时项目里有两套方案在较量:一是全部走PROFINET,每台仪表配一个通信模块;二是用PLC自带的RS485口挂MODBUS RTU总线。前者单点成本高,几十台仪表的光模块费用直接让预算翻倍;而大家担心的“轮询速度慢、总线不稳定”,在这个点位规模下其实构不成瓶颈。断言选RTU的理由是硬性的:现场已经有现成的屏蔽双绞线,仪表全部支持MODBUS RTU,PLC集成接口可直接复用,整套方案零额外硬件成本。
MODBUS RTU的轮询机制本身是“一问一答”的串行交互:主站依次发出请求帧,从站在规定时间内回复,主站收到后马上切换到下一个从站。相比TCP方式,RTU不依赖网络配置,不涉及IP地址、端口映射,现场接线完成就能跑。它的缺点也明显:物理层是半双工的RS485,同一时刻只能有一台设备说话,轮询一圈的时间取决于从站数量和每个站的响应速度。对这个项目的数据量来说,一圈下来几十毫秒,完全满足监控系统的刷新需求。
这套程序的技术栈集中在博途环境内:SCL编写逻辑,OB1循环组织调用,全局DB存放通信数据,可选配CM1241 RS485模块或CPU集成RS485接口。程序不依赖第三方库,所有报文构造、CRC16校验、状态机调度都是原生SCL实现,迁移到S7-1200或S7-1500时无需额外授权。
2.2 程序结构:从站描述表、轮询控制块、数据映射区
整个程序不是一个大而全的功能块,而是拆成了三个层次。最底层是通信驱动块,负责串口参数初始化、发送接收指令的调用;中间层是轮询调度块,按从站表和点表依次发出请求,管理超时与重试;最上层是数据映射块,把收到的原始寄存器值换算成工程量并写入公共DB。
打开项目后,程序块结构大致是这样的:
PLC程序 ├── OB1 主循环 ├── FB100_MB_Comm_Driver // 通信驱动:封装RS485发送接收 ├── FB200_MB_Poll_Manager // 轮询调度:状态机核心 ├── FB300_MB_Data_Map // 数据映射:寄存器值转工程量 ├── DB100_Comm_Config // 通信参数:波特率、校验、从站数 ├── DB200_Poll_Table // 从站描述表:站号、功能码、寄存器地址 └── DB300_Process_Data // 工艺数据区:HMI直接读取第一次看这套结构的开发者,最容易困惑的是“为什么不把所有逻辑写进一个FB里”。原因在于后期维护:现场经常出现“这个站的地址变了”“那台表的功能码要改”这类需求,如果把点表散落在代码里,每次改动都要下载程序。从站描述表独立成DB之后,点位的增删只需在DB里改数据,轮询调度逻辑一行代码都不用动。
DB200_Poll_Table的设计是整个程序的点睛之处。它不是简单的数组,而是一个结构体数组,每个元素描述一台从站的完整请求信息:
STRUCT SlaveAddr : INT; // 从站地址,1~247 FuncCode : INT; // 功能码:03读保持、04读输入、06写单寄存器 StartReg : INT; // 起始寄存器地址 RegCount : INT; // 读取/写入的寄存器数量 PollEnable : BOOL; // 本行是否参与轮询 Timeout : TIME; // 单次请求超时时间 RetryCount : INT; // 失败重试次数 END_STRUCT每个字段都有明确的工程含义。SlaveAddr对应MODBUS协议里的站号,现场拨码开关设成什么就填什么,范围为1到247,0是广播地址,一般不用。FuncCode这个字段初看冗余,因为多数应用只用03功能码,但加上它之后,同一套轮询框架就能支持“某些站只读、某些站要写”。StartReg和RegCount共同决定了请求帧的内容:读取连续寄存器时Count可以大于1,读取不连续的寄存器时宁可拆成多行也不要用散点读取,后者会让轮询逻辑复杂到难以维护。
2.3 博图组件清单与项目版本选择
项目在博途TIA Portal V16及以上版本中均可打开。若用的是V15或更早的版本,建议先确认SCL语法兼容性,个别指令如“#localVariable”隐式声明方式在两个版本之间有细微差异。硬件配置上,CPU选用S7-1500系列(例如CPU 1511或以上),通信模块有两种选择:集成在CPU上的RS485接口,或单独的CM1241 RS485模块。若CPU本身带有第二个串口,优先用集成接口,因为CM1241模块需要在硬件组态里额外分配地址,出错概率虽然低但多一层配置。
关于RS485的硬件接线有个容易被忽略的点:A/B两线必须用屏蔽双绞线,屏蔽层单端接地。程序侧可以做再多容错,物理层这关过不了,后面全是白搭。如果总线上挂了超过16台设备,建议在末端加一个120欧姆终端电阻。现场设备支持的话,把波特率统一设成9600或19200,别盲目上38400以上——仪表厂家标称支持,实际跑起来受线缆长度和干扰影响,经常是“能通但丢帧”。
3. 通信底层:RTU报文、CRC16与超时参数的工程实现
3.1 报文缓冲区与功能码设计
MODBUS RTU的报文格式是固定套路:从站地址1字节、功能码1字节、数据区N字节、CRC校验2字节。整条报文里没有起始符和结束符,帧与帧之间靠“静默时间”区分——标准规定报文间至少要有3.5个字符时间的空闲。这个细节在程序中直接体现为:发送完请求帧后必须等待一段时间再切换收发方向,否则RS485芯片来不及切换,当前帧就会被自己的回显数据冲掉。
程序里用字节数组作为收发缓冲区。发送请求时,先按功能码拼接命令,再调用通信驱动把整包数据交给串口;接收时,把收到的字节流存进接收缓冲区,然后统一做CRC校验和超时判断。
FB100_MB_Comm_Driver里的发送逻辑是核心,SCL代码如下:
FUNCTION_BLOCK FB100_MB_Comm_Driver VAR_INPUT bSendRequest : BOOL; // 上升沿触发发送 SlaveAddr : INT; // 目标从站地址 FuncCode : INT; // 功能码 StartReg : INT; // 起始寄存器地址 RegCount : INT; // 寄存器数量 SendData : ARRAY[0..7] OF BYTE; // 写入数据区(仅写功能码使用) END_VAR VAR_OUTPUT bBusy : BOOL; // 发送忙标志 bError : BOOL; // 通信错误 ErrorCode : INT; // 错误代码 END_VAR VAR SendBuffer : ARRAY[0..15] OF BYTE; iIndex : INT; END_VAR拼接报文的逻辑非常简单:第0字节放从站地址,第1字节放功能码,之后按功能码不同填充数据区。以读保持寄存器(功能码03)为例,报文是“地址 + 03 + 起始寄存器高字节 + 起始寄存器低字节 + 寄存器数量高字节 + 寄存器数量低字节 + CRC低字节 + CRC高字节”。
值得注意的坑是:报文里寄存器地址和数量都是16位无符号整数,需要拆成高低两个字节发送。现场一个很常见的低级错误是直接用WORD型变量去填充字节数组,博途的字节序默认是大端,而很多仪表固件用的是小端,结果就是读回来的地址永远不对。这段代码在填充时就手动处理字节序,避免把这个问题留到现场。
3.2 CRC16校验的SCL实现与常见错误
CRC16是MODBUS RTU通信里最无法回避的一环,也是新手最容易求助于网上现成代码却怎么都对不上的地方。MODBUS的CRC16是多项式0xA001、初值0xFFFF、低位先传输的变体,标准CRC工具生成的通常不是同一种。直接搬运通用CRC16算法往往校验不通过,原因就是字节序和初值处理不同。
FB100里实现的CRC16计算逻辑如下:
// CRC16 MODBUS 校验计算 // 多项式 0xA001,初值 0xFFFF iIndex := 0; wCRC := 16#FFFF; WHILE iIndex < iDataLen DO wCRC := wCRC XOR SendBuffer[iIndex]; FOR iBit := 0 TO 7 DO IF (wCRC AND 16#0001) <> 0 THEN wCRC := SHR(wCRC,1); wCRC := wCRC XOR 16#A001; ELSE wCRC := SHR(wCRC,1); END_IF; END_FOR; iIndex := iIndex + 1; END_WHILE; // 注意:发送时低字节在前 SendBuffer[iDataLen] := BYTE_TO_BYTE(wCRC AND 16#00FF); SendBuffer[iDataLen+1] := BYTE_TO_BYTE(SHR(wCRC,8) AND 16#00FF);这段算法的工程要点有两个。第一个要点是循环次数必须覆盖完整的数据区,地址、功能码、寄存器区、数量区全部参与计算,漏掉任何一个字节都通不过从站校验。第二个要点是发送顺序,MODBUS规定CRC低字节先发,这里给SendBuffer赋值时先放低字节再放高字节,和报文接收方向正好对应。
在实际调试中遇到的绝大多数CRC问题,都不是算法本身写错,而是数据长度iDataLen算错。比如读4个寄存器时,报文总长度应该是8字节(地址+功能码+起始地址2字节+数量2字节+CRC2字节),但代码里如果把长度写成6字节,从站解析出的CRC区本身就是错位的,自然校验失败。调试时建议先用串口助手抓一次正常的报文做对照,再做代码级排查。
3.3 接收超时、字符间隔与总线时序
RTU通信没有帧起始符,主站怎么知道一帧数据收完了?答案是静默时间:接收过程中,如果超过3.5个字符时间没有新字节进来,就认为一帧结束。3.5个字符时间在9600波特率下约等于4毫秒,在19200波特率下约等于2毫秒。这个时间的设定在两处体现:一是通信驱动里的接收完成判断,二是等待响应帧的超时时间。
接收完成的判断逻辑:
// 接收完成判断:当接收间隔超过 4ms 时认为一帧结束 IF #bReceiving THEN #TimerRun := #TimerRun + 1; IF #TimerRun >= #FrameGapTime THEN #bFrameDone := TRUE; // 一帧接收完毕 #TimerRun := 0; #bReceiving := FALSE; END_IF; END_IF;这里FrameGapTime是字符间隔时间的计数值,它取决于OB1的扫描周期和定时中断的精度。常见做法是把这段接收判断放在一个循环中断OB里(比如OB35,定时10ms),FrameGapTime设为2个中断周期,这样既覆盖了3.5个字符时间的底线,又不会因为扫描周期波动导致误判。想省事直接把接收判断放OB1里也行,但S7-1500的OB1扫描周期会随程序量变化,通信帧间隔就容易抖,现场表现出“时通时不通”的玄学问题。这个项目里把接收判断放在OB35里,是稳定性的关键一步。
总线上同时存在多台从站设备时,还牵扯到一个初始化时序问题:主站切到某个从站之前,需要保证上一站的响应处理完毕。如果上一站的响应还在总线上,立刻发出下一站的请求,就会出现“两帧交叠”的物理层冲突。程序里用bBusy标志阻止这种情况:只有当前请求已经完全结束(收到响应或超时),轮询调度器才会发出下一帧。这个标志是通信驱动和轮询调度之间的握手信号,缺了它整个轮询就是乱序的。
4. 核心模块拆解:轮询状态机从请求到落库的完整链路
4.1 状态机总览:从IDLE到回包校验
FB200_MB_Poll_Manager是整个程序的调度中枢,负责决定“现在该问哪台从站、问什么内容”。它的实现是一个典型的状态机,围绕四个状态流转:空闲、发送请求、等待响应、处理响应。状态机的优势在于每个时刻都清楚自己的位置,无论是正常轮询还是异常重试,都有明确的下一步,不会出现“发出去的请求没下文,程序卡死”的情况。
初始化状态和主轮询状态的划分是关键。程序上电后先进入初始化状态,把通信模块的参数(波特率、校验位、停止位)设置好,再切换到轮询状态。这一步单独做的原因是:串口参数配置在通信模块上电后有一个稳定的时间窗口,如果上电立即发请求,大概率因为模块没准备好而丢帧。有些开发者图省事把参数配置放在OB100里只跑一次,但对于需要热重启的场合,放在FB里每次调用时都检查一遍更稳妥。
状态机骨架如下:
CASE #State OF 0: // 空闲状态:取下一行从站描述表 IF #PollTableIndex < #TotalSlaves THEN #CurrentEntry := #PollTable[#PollTableIndex]; #State := 10; // 进入发送状态 ELSE #PollTableIndex := 0; #CycleCount := #CycleCount + 1; // 轮询圈数计数 END_IF; 10: // 发送请求状态 #bSendRequest := TRUE; #SendDone := FALSE; #State := 20; 20: // 等待响应状态 IF #bFrameDone THEN #State := 30; ELSIF #TimeoutCount >= #MaxTimeout THEN #RetryCount := #RetryCount + 1; IF #RetryCount > #CurrentEntry.RetryCount THEN #ErrorFlag := TRUE; #State := 0; // 重试耗尽,跳到下一站 ELSE #State := 10; // 重新发送 END_IF; END_IF; 30: // 处理响应状态 // 校验CRC、解析数据、写入映射区 #State := 0; #PollTableIndex := #PollTableIndex + 1; END_CASE;这个状态机的调度逻辑里,最容易被忽视的是“重试耗尽后如何处理”。很多初版实现里,重试耗尽就直接置错误标志然后卡在当前状态,结果一台从站掉线,整个轮询链全部停摆。正确做法是:重试次数用完就跳过该从站,把错误位写入对应的状态字,继续轮询后面的站。这样即使某台设备故障,其余设备的数据刷新不受影响,故障点也能在诊断区直观看到。
4.2 发送请求与等待响应的超时控制
发送状态用上升沿触发,避免每个扫描周期都重复发同一帧。SCL里判断上升沿的标准姿势是用一个临时变量保存上一周期的值:
IF #bSendAndNotDone THEN #bSendRequest := TRUE; #bSendAndNotDone := FALSE; END_IF;等待响应状态里,超时计数的单位决定了系统的反应速度。程序里把超时判断放在OB35(10ms循环中断)里累加,MAX_TIMEOUT按从站类型配置。常规MODBUS RTU从站的响应时间在10到50毫秒之间,通信正常的站基本在10毫秒以内返回,所以超时设为200毫秒足够宽裕,还能兼顾链路干扰重试的节奏。有快响应的仪表,也有动作慢的温控器,它们的差异就在这里体现:如果所有站共用同一个超时时间,慢设备后面所有设备都会被拖累。用从站描述表里的Timeout字段做个性化超时,是这套程序的另一个实用细节。
等待响应期间还有一个收发切换问题。RS485是半双工,发送完请求后必须把驱动芯片切到接收状态,这个切换由通信模块内部完成,但程序要注意的是:切换之后不能立刻开始计时,而是要给一个稳定时间窗口。这个窗口就是前面提到的FrameGapTime,既用于帧结束判断,也用于收发方向切换。
4.3 响应解析与数据落库映射
从站返回的响应帧格式与功能码对应:功能码03和04返回的是“地址 + 功能码 + 字节数 + 数据区 + CRC”,功能码06返回的是“地址 + 功能码 + 寄存器地址 + 写入值 + CRC”。FB300_MB_Data_Map要做的就是把数据区里的寄存器值提取出来,按预先定义的点表换算成实际工程值。
解析逻辑的SCL核心片段:
// 从响应帧中提取寄存器值 // ResponseBuffer[3] 起始的数据区字节,每2字节拼成一个WORD FOR i := 0 TO (#RegCount - 1) DO wRawValue := SHL(BYTE_TO_WORD(ResponseBuffer[3 + i * 2]), 8); wRawValue := wRawValue OR BYTE_TO_WORD(ResponseBuffer[4 + i * 2]); // 工程量换算:依据仪表量程和数据类型 // 常见场景:0~4000对应0~100.0℃,线性换算 rEngValue := INT_TO_REAL(WORD_TO_INT(wRawValue)); rEngValue := rEngValue * #ScaleFactor + #OffsetValue; ProcessData[#StartReg + i] := rEngValue; END_FOR;寄存器值是16位无符号数,2个字节组合时需要手动拼出高位和低位,字节序在这里再次成为一个坑点。上面的代码按大端解析——高位字节在前,低位字节在后——但部分仪表厂家会配置为小端输出。当发现读回来的数值“颠倒了”,优先怀疑的不是程序逻辑而是字节序配置。
工程量换算里,ScaleFactor和OffsetValue是两个现场调试点。以温度变送器为例,量程0~200℃,输出寄存器范围0~20000,ScaleFactor就是0.01,OffsetValue是0。如果仪表支持负温度,比如-40~80℃,则OffsetValue是-40。这些参数如果硬编码在代码里,每换一种仪表就要改一次程序,因此程序里把它们也做进了从站描述表——把ScaleFactor和OffsetValue作为结构体成员加入,HMI画面上就可以直接修改换算参数,不用再碰PLC程序。这样不仅省了下载程序的麻烦,还让现场调试人员能从HMI独立完成仪表类型的适配。
数据落库的最后一环是把换算后的工程量写入DB300_Process_Data,同时把该从站的通信状态字(正常、超时、CRC错误、重试中)写入DB300的诊断区。HMI侧只关心DB300,不直接面对通信层,通信层再怎么翻车都不影响HMI的画面稳定——这个分层设计在后续二次开发时的价值尤其明显。
5. 常见问题与排查:现场真正会翻车的五个通信细节
5.1 现象:轮询运行几分钟后通信中断,CPU诊断信息为空
这类问题最抓狂,明明程序没有问题,CPU也没有报错,但通信就是停了。用监控表看状态机,发现某台从站的错误计数在持续累加,其他站正常。但手动把那条请求单独发一遍,从站又能正常回复。
原因:该从站的响应时间波动超过超时设定值。这些仪表里有些内部带有自动校准周期,每隔一段时间会暂停通信几十到几百毫秒,期间对总线的请求一律不回复。轮询请求恰好撞上这个窗口,就表现为超时重试,之后恢复。
解决:把该从站的Timeout字段从200ms加大到500ms或1000ms,同时RetryCount保持默认的2次。切记不要把所有站都改成大超时——那会把整个轮询周期拖慢一个量级,影响到所有站的刷新频率。只让有问题的站吃“偏方”,其他站保持原有参数。
5.2 现象:读回来的数值是反的或差距悬殊,但通信正常
通信请求全部成功,CRC校验通过,但数据显示异常。拿到串口助手里对比原始报文,发现仪表返回的字节序和程序里按大端解析的结果不一致。
原因:这个仪表默认的寄存器输出是小端序(低字节在前),而程序按MODBUS的大端惯例解析。串口助手直接看十六进制时人眼换算不易发现,一旦仪器侧确认,原因就很清楚了。
解决:解析时按小端序处理或加一个字节序配置字段。更通用的做法是在从站描述表里增加ByteOrder字段,解析时根据该字段选择拼接顺序。这样一套程序既能适配大端仪表,也能适配小端仪表,不用改代码。
5.3 现象:写命令返回成功但从站实际值没有变化
功能码06的写单寄存器请求,从站回了正常的写回报文,CRC也正确,但现场设备的状态没有任何改变。
原因:寄存器地址写错了。MODBUS协议里寄存器地址从0开始,但很多仪表文档里标注的地址从1开始(比如“寄存器地址40001”是MODBUS标准里的数据地址,对应协议地址0)。程序里直接用文档上的40001减去40001再减1才是真正的协议地址,遗漏偏移量就会写到一个无关寄存器上。
解决:对照仪表手册的协议地址表格,确认写入的StartReg是协议地址。大部分仪表规格书的地址编号会有明确说明,现场遇到写无效的情况,第一反感应是检查地址偏移,而不是怀疑通信链路。
5.4 现象:轮询所有从站都正常,但偶发一两次主站自己的发送错误
现象是通信偶尔失败,看诊断区错误代码指向发送侧的溢出或发送失败,而不是接收超时。
原因:程序在上一帧处理完毕前就尝试发送下一帧。轮询调度器从“处理响应”状态直接跳到“发送请求”状态,中间没有空出RS485收发切换的时间,模块内部的上一次发送缓冲还没清空。
解决:在状态机的处理响应状态和发送状态之间,显式插入一个短延时状态,等待至少一个字符时间再发下一帧。这个短暂延迟对整体轮询速率影响很小,但能稳定消除大半的偶发发送错误。当时踩这个坑,就是因为把状态机压缩得太紧凑,犯了“逻辑正确但物理时序不对”的毛病。
5.5 现象:加了一台新从站后,原来正常的从站开始报错
总线上的设备数量增加后,原有设备的通信质量下降,错误计数上升,甚至原先的稳定站也开始超时。
原因:总线负载增加后信号反射变严重,且RTU轮询一圈的时间变长。如果新线缆用的是非屏蔽双绞线或者过长,会让信号边沿变差;另一个是总线末端没有按规范接终端电阻,新增设备后反射问题被放大。
解决:检查A/B线是否用的是合格的屏蔽双绞线,确认总线段两端各接一个120欧终端电阻。加点位后重新计算轮询周期,如果超时一圈的时间超过500ms而HMI刷新要求较高,考虑对从站分组用两个RS485模块分别轮询——这样既有互不干扰,也把单条总线的负载降下来。
6. 二次开发:从固定点位到可配置从站表的实战改造
6.1 改造方向:让DB表直接驱动轮询逻辑
拿到这套程序时,从站描述表已经把点位信息从代码里剥离出来,初版改造不用动核心逻辑,只需要扩充结构体字段。最常见的新增需求是“读回来的工程量需要做非线性换算”。仪表输出的是开度百分比,而实际流量和开度的关系是一条曲线,这时在FB300_Data_Map里增加一个查表映射函数,用二维数组存放曲线点,通过线性插值把寄存器值换算成最终工程量。结构体里增加一个CurveIndex字段,指向该从站对应的曲线编号,代码只增加一个查表函数,不破坏原有框架。
另一个主流改造方向是从RTU切换到MODBUS TCP。程序状态机和数据映射部分完全复用,只替换通信驱动层:把FB100里的RS485发送接收指令换成TCP通信指令,IP地址和端口写入从站描述表。这样一套轮询逻辑通吃现场总线和上位机两种通信场景,代码改动面控制在30%以内。
6.2 验证思路与改后检查清单
二次开发完成后,建议先在仿真环境里跑通状态机,再上现场实测。仿真时可以用MODBUS从站模拟软件模拟多台设备,检查轮询顺序、超时重试、错误跳过三个核心行为是否符合预期。现场实测时,检查清单至少包含:各从站第一轮请求全部成功、单台从站断电后其余站刷新不受影响、HMI上工程量与实际仪表显示一致(误差在允许范围内)、通信诊断区无累计异常增长。
以上四项逐一验证通过,才算改造落地。
那之后我每次改造这套轮询程序,都强制自己先做一张从站信息表再动手写代码——把每个站的型号、协议地址、字节序、响应时间、换算关系全部列齐,这张表既是DB填充依据,也是现场排错索引。省掉这一步,看似省了半小时,实际上是把问题从设计阶段推迟到现场调试阶段,代价往往是用更多的深夜加班来偿还。希望这篇拆解能帮你少走几段弯路。
本文还有配套的精品资源,点击获取