S7-1200 PLC用SCL实现G代码解析与运动控制功能块 开头部分先交代清楚这次做的事。接了个小型三轴专机的改造甲方给的表控系统老化严重坏了修起来麻烦提了一嘴“能不能直接用PLC读G代码”。当时手边正好有西门子S7-1200和两三台带脉冲接口的伺服我就想着干脆写一个基于SCL语言的G代码解释执行功能块把G代码解析、运动控制、状态管理全部塞进PLC里。做完以后效果还行点位和直线段跑起来很稳定这篇文章就是把我从零到一整出来的过程、源码结构、还有踩过的坑全部复盘一遍。想省钱的设备厂、做专机集成的朋友或者正在犹豫“PLC到底能不能干数控活”的工程师可以仔细看看这个方案有边界但确实能落地。1. 为什么要用S7-1200去啃G代码这块硬骨头1.1 数控系统与通用PLC的分界线在哪里很多人一听“PLC解析G代码”第一反应就是不靠谱因为数控系统是专门为插补和路径控制设计的东西S7-1200说到底是一台逻辑控制器它的强项在开关量、模拟量、通信和工艺逻辑上。但要先搞清楚边界G代码并不等于高档数控系统。G代码只是一套文本指令格式核心内容是“走到哪个坐标、以什么速度、走直线还是走圆弧、做不做辅助动作”。真正的数控系统贵在插补引擎、前瞻运动规划、刀具补偿、高精度伺服总线、平滑加减速算法这些才是绕不开的技术壁垒。而S7-1200能做的是把G代码拆成一条一条的坐标指令和辅助指令按顺序解析成内部结构化数据再以合理的时序发给伺服轴执行。点位运动、直线插补这些东西完全可以在PLC的循环周期里做出来只是精度和速度级别和专用数控系统有差距。我在做之前给自己定了个原则不碰高档模具加工只做“点位加直线”的低精度运动控制。这样S7-1200完全够用成本却比专用数控系统低一大截。搞清楚边界和需求你就不会觉得这个标题是异想天开。1.2 适合用PLC方案解决的场景清单基于这次实践我总结出几个真正适合用S7-1200跑G代码的场景小型专机比如型材切割、钻孔攻丝一体机、点胶机、绕线机。这类设备路径简单多为点位加直线对插补精度要求不算高用专用数控系统纯属杀鸡用牛刀。教学与实验台架很多职业院校和实验室需要让学生理解G代码的解析流程用PLC加SCL更直观也更容易做二次开发。老设备改造原控制器坏了或者停产电气柜里换一台S7-1200保留原伺服或步进驱动器写一个功能块就能恢复功能维护也统一到熟悉PLC的维修团队手上。定制化程度高的设备需要在运动中穿插逻辑判断、气缸动作、传感器检测的场景PLC比专用数控系统灵活得多。数控系统做逻辑往往很死板PLC则是主场。坦白讲S7-1200不适合加工中心、车铣复合这种重插补场景。它没有专用的硬件插补器所有计算靠CPU跑SCL循环周期和脉冲输出频率决定了上限。另外它也不擅长处理宏程序、固定循环、刀具补偿这种复杂指令集。所以做之前先把自己要做的事摸清楚比急着写代码重要得多。2. 功能块源文件的组织方式DB、UDT与FB的分工2.1 数据区规划与内部单位约定开始写代码前我花了不少时间在数据组织上。PLC程序最怕的就是到处散落变量G代码解析涉及的数据项特别多如果不好好规划后面调试简直噩梦。我的做法是在PLC里建一套完整的数据模型核心是三个部分UDT轴参数定义一个轴的数据结构包括轴序号、脉冲当量、最大速度、加速度、软限位、当前位置、目标位置、使能信号等。TYPE UDT_AxisParam VERSION : 0.1 STRUCT iAxisID : INT; // 轴序号 rPulsePerMM : REAL : 100.0; // 脉冲当量每毫米脉冲数 rMaxSpeed : REAL : 6000.0; // 最大速度单位mm/min rAccel : REAL : 2000.0; // 加速度单位mm/min² rSoftLimitP : REAL : 1000.0; // 正软限位 rSoftLimitN : REAL : -1000.0; // 负软限位 rActualPos : REAL; // 实际位置 xPowerOn : BOOL; // 轴使能 xInPosition : BOOL; // 到位标志 END_STRUCT; END_TYPEUDT指令结构体定义一条经过解析后的“内部指令”这是整个功能块的核心数据载体。TYPE UDT_GCodeCmd VERSION : 0.1 STRUCT iCmdType : INT; // 0:无指令 1:快速定位G00 2:直线G01 // 3:圆弧G02/G03 4:主轴M03 5:主轴停止M05 rX : REAL; // X目标 rY : REAL; // Y目标 rZ : REAL; // Z目标 rF : REAL; // 进给速度 rI : REAL; // 圆弧圆心偏移I rJ : REAL; // 圆弧圆心偏移J xValid : BOOL; // 该条指令有效 END_STRUCT; END_TYPEG代码队列背景DB用一个长度为40的数组当环形缓冲区存储解析后等待执行的指令块。PLC循环扫描的特点决定了不能每行G代码执行完才解析下一行那样运动会一卡一卡必须预取一批指令填满队列执行引擎再从队列里一条条取。单位约定这块我踩过比较大的坑。G代码里的F值标准是mm/min比如“F600”表示每分钟600毫米。伺服驱动器的速度命令有些是mm/s有些是脉冲频率这就需要一个明确的换算层。我在功能块内部统一用mm/min作为速度单位输出到驱动器前再乘脉冲当量做换算。这个约定写进程序注释里免得三个月后自己看都懵。2.2 输入输出接口的引脚设计功能块的引脚设计直接影响外部调用是否方便。我这版FB的接口分成了几个层次FUNCTION_BLOCK FB_GCODE_INTERPRETER VERSION : 0.1 VAR_INPUT xEnable : BOOL; // 功能块总使能 xStart : BOOL; // 启动 xHold : BOOL; // 暂停/继续 xAbort : BOOL; // 急停复位 sGCodeLine : STRING[254]; // 上位机下发的单行G代码 iLineLen : INT; // 行字符串有效长度 AxisParam : ARRAY[1..3] OF UDT_AxisParam; // 三轴参数 END_VAR VAR_OUTPUT xBusy : BOOL; // 功能块忙 xDone : BOOL; // 当前批指令执行完成 xError : BOOL; // 错误标志 iErrorCode : INT; // 错误代码 sErrorMsg : STRING[80]; // 错误信息 AxisCmd : ARRAY[1..3] OF UDT_AxisCmd; // 输出给伺服的速度和方向 END_VAR这组接口的好处是分层清晰上方只负责输入原始G代码和启停控制下方把运动指令统一打包成“UDT_AxisCmd”再交给伺服驱动层去执行功能块内部不直接输出脉冲也不直接访问硬件轴。我特意把加减速时间和脉冲频率规划留在外部而不是塞进功能块因为不同设备的伺服驱动器和机械机构差异太大功能块只负责“该走到哪、走多快”怎么加速减速让外部运动控制层去处理这样复用性最高。一台设备默认三轴需要用多大行程或速度改轴参数DB就行不用动功能块内部逻辑。3. 解析引擎的SCL实现从字符串到结构化指令3.1 SCL在处理文本上的优势梯形图做字符串解析基本没戏一个字一个字去拆能写到怀疑人生。SCL语法接近Pascal有FOR循环、WHILE循环、CASE语句还有完整的字符串处理函数库这才是编写文本解释器最顺手的环境。S7-1200的字符串处理函数我实际用过FIND可以查找子串位置LEFT/MID/RIGHT做截取DELETE做删减CONCAT做拼接足够覆盖G代码解析需求。虽说不像高级语言里正则表达式那么爽但逐字扫描的写法也不复杂关键是逻辑清楚。我用SCL写了解析和状态机两个核心部分加起来大概七八百行。这个体量在SCL里完全可控放在FB里能整体下载和调试比分散在多个FC里舒服得多。3.2 逐字符扫描与浮点提取G代码每一行格式基本是“指令字母数字指令字母数字”比如“G01 X10.5 Y20.0 F600”。解析的核心逻辑就是按字母扫描看到字母就存为当前指令字跟在后面的数字串再提取成浮点数。这块我封装了一个“提取浮点数”的子功能它的任务是从字符串某个位置开始扫描到下一个字母或字符串结尾把中间的数字提取成REAL同时返回新的扫描位置。FUNCTION F_ParseFloat : REAL VERSION : 0.1 VAR_INPUT sLine : STRING; iStartPos : INT; END_VAR VAR_IN_OUT iNewPos : INT; // 返回下一个扫描位置 END_VAR VAR_TEMP sNumPart : STRING[20]; iPos : INT; ch : CHAR; bNeg : BOOL; rValue : REAL; END_VAR BEGIN sNumPart : ; iPos : iStartPos; WHILE (iPos LEN(sLine)) DO ch : MID(sLine, iPos, 1); // 遇到字母或空格就停止 IF (ch A AND ch Z) OR (ch a AND ch z) OR ch THEN EXIT; END_IF; sNumPart : CONCAT(sNumPart, ch); iPos : iPos 1; END_WHILE; iNewPos : iPos; // 空数字串返回0 IF LEN(sNumPart) 0 THEN rValue : 0.0; ELSE rValue : STRING_TO_REAL(sNumPart); END_IF; F_ParseFloat : rValue; END_FUNCTION主解析循环就用FIND定位每个字母再调用F_ParseFloat提取数值。每遇到一个字母就把当前数字值存入对应的命令字段。例如遇到“X”就把提取到的值写入指令的rX遇到“F”就写入rF。这段逻辑用SCL写很直观逐个字符扫过去几乎没有性能压力一行60个字符以内的典型G代码解析耗时在毫秒级。解析完之后会产生一条完整的UDT_GCodeCmd塞进环形队列。对于不认识的指令字比如“T”刀具指令我的处理是跳过并记录警告不会中止整段程序。但如果遇到“G”字母后跟着无法识别的G代码号比如“G15”那必须报错停机这种属于结构性问题继续跑会出事故。3.3 指令队列与预取策略预取策略是直接影响运动平顺度的关键。如果PLC每次只解析一行、执行一行伺服轴走完再解析下一行那么字符串处理时间和网络传输时间都叠加在路径里导致停顿和抖动。我的方案是把解析器做得比执行器快一步。上位机或者HMI把整个加工文件逐行下发解析器以比执行快得多的速度把指令塞进40个槽位的环形队列执行器从队尾取指令。这样执行器永远有料可吃只要队列不空运动就不会断。环形队列的读写指针管理我用了两个INT变量分别作为写索引和读索引。写入时自动加1并对39取模读到索引等于写索引时表示队列已空就停止执行等待新指令。队列满的时候解析器暂停接收G代码文件逐行输入的总线流量本身不会太高这个节奏完全跟得上。// 环形队列写操作示例 IF #iQueueCount 40 THEN #CmdQueue[#iWriteIdx] : #cmdTemp; #iWriteIdx : (#iWriteIdx MOD 39) 1; #iQueueCount : #iQueueCount 1; ELSE // 队列满本次解析失败等待下周期 END_IF;同时我留了一个“预处理校验”的扩展点解析出来的指令马上检查坐标是否超软限位G00快速定位的F值是否超过轴最大速度有问题直接拒绝入队错误信息指出来。免得执行到一半才发现路径不对急停都来不及。4. 执行状态机G代码跑起来的主干逻辑4.1 五个核心状态与切换条件整个功能块的主干不是解析器而是执行状态机。我用一个INT变量存储当前状态CASE语句驱动切换。一共设计了五个状态空闲、解析中、执行中、暂停、错误停止。空闲IDLE等待启动命令。收到xStart上升沿后进入解析中。解析中PARSING连续从输入缓冲区取字符串解析成指令并入队。队列积累到一定深度或收到“结束批”标志后转到执行中。执行中RUNNING从队列取一条指令和外部运动控制层配合逐轴移动到目标位置。执行完当前批队列清空后回到空闲设置xDone。暂停HOLDxHold为真时进入运动控制层会冻结输出但不清除队列解除后原地继续。错误停止ERROR解析失败、坐标超限、驱动器报警等情况下进入清空队列并输出错误代码只有xAbort才能复位。状态切换这层逻辑不难难的是要把边界条件想全启动时队列为空怎么办执行中来了新文件要不要清队列暂停状态下伺服驱动器会不会因为一直得不到脉冲而报警这些问题在调试时一个个冒出我当时的做法是每个状态都做了“进入动作”“持续动作”“退出动作”三段式设计写在注释旁边方便后续排错。4.2 位置到达判定与节拍配合执行器从队列取出指令后要做的事情是“让轴到达目标位置”。这里有个重要的接口设计问题S7-1200本体并不直接输出高频脉冲给步进驱动器很多时候是通过运动控制指令或总线协议送给伺服驱动器位置闭环在驱动器内部完成。所以PLC侧只需要做两件事一是给出目标位置和速度二是确认轴到位。到位信号可以来自驱动器反馈比如伺服驱动器“位置到达”信号接到PLC数字量输入也可以用轴当前值做比较差值小于一定阈值就认为到位。我做的是两者结合驱动器到位信号为主位置比较为辅双保险。一个容易被忽略的问题是PLC扫描周期和运动控制的节拍配合。如果一条“G01 X100 Y50 F600”指令要同时驱动两个轴两个轴必须并行启动不能等X轴走完了再让Y轴动。我的做法是先把目标位置和速度写给所有轴让驱动器同时开始运动然后统一等待到位信号。每个轴的到位逻辑相互独立又彼此关联等所有需要运动的轴都到位了才认为当前指令执行完成这里时序上很容易写错务必用统一的中断或定时器扫描驱动器状态字。CASE #iState OF 3: // RUNNING // 启动轴运动一次性把目标坐标下发到所有轴 FOR #i : 1 TO 3 DO #AxisCmd[#i].xEnable : TRUE; #AxisCmd[#i].rTargetPos : #curCmd.rX; // 按轴映射 #AxisCmd[#i].rSpeed : #curCmd.rF; END_FOR; #iState : 31; // 等待到位 31: // 每个周期检查到位信号 #allInPos : TRUE; FOR #i : 1 TO 3 DO IF #curCmd.iCmdType 1 THEN // G00速度不受F约束 END_IF; IF NOT #AxisParam[#i].xInPosition THEN #allInPos : FALSE; END_IF; END_FOR; IF #allInPos THEN // 取下一指令 END_IF; END_CASE;这里我特别要提一句G00和G01的语义差别要体现出来。G00是快速定位不受F值限制通常用轴最大速度G01是直线切削必须严格遵守F值。所以CASE里要先按指令类型决定速度来源再发给运动层。早期版本我图省事把G00也按F值跑结果一条快速定位路径走得比G01还慢被现场的人笑了一顿。5. 实测中的经典坑与对策5.1 字符串大小写和尾部隐藏字符G代码文件很多是从CAD/CAM软件导出来的不同软件生成的格式五花八门。有些会输出小写字母“g01”有些在行尾带回车换行有些在结尾带空格。如果解析器不做预处理直接拿去匹配“G01”小写格式就会掉进不识别分支。我的解法是在解析入口统一做两件事将字符串全部转成大写再用TRIM函数去掉尾部空格和换行符。这俩操作耽误不了几十微秒但能避免一大堆兼容性问题。另外字符串截取之后要用包含判断而不是直接用等号比如“G01”和“G01 ”在截取时容易带尾巴这就要靠去掉空格之后再匹配。5.2 单位换算、软限位与回零F值的单位坑我前面说过再补充一个更隐蔽的伺服驱动器的电子齿轮比不同脉冲当量会变化。如果PLC内部按“毫米”算驱动器按“脉冲”算那么“每毫米脉冲数”这个参数必须和驱动器的电子齿轮严格对应。我的做法是做一个轴参数校准程序手动给轴发一个已知距离的脉冲量出实际移动距离反推脉冲当量然后写进轴参数DB。不要相信铭牌和手册的理论值机械传动链的减速比、丝杠导程的实际误差会直接影响换算准确度。软限位检查必须在解析时做一次、执行前再做一次。G代码程序里可能会出现多个轴同时运动到边缘的情况单轴超限容易处理多轴同时到达一个工作区角落则要考虑对角线动作。我的功能块策略很简单但有效每条指令入队前逐个轴比较目标值和软限位任何一个超限就把整条指令废弃并报错不会把半截路径放进队列。回零逻辑值得单独说。设备上电以后伺服位置往往是未知的不回零就执行G代码等于自杀。我的功能块不支持自己回零而是用外部逻辑主动回零后把轴当前位置通过“轴参数写入”接口同步到功能块然后才允许启动。这个设计避免了一个复杂异步流程塞进解析器里排错也清晰。5.3 队列读写竞争与急停保护S7-1200里的FB默认是全局实例不存在多线程并发写同一个DB的问题但有一个竞争场景容易忽略外部程序可能在解析器写入队列的过程中修改队列的读索引或写索引或者HMI急停时同时调用xAbort和xStart导致状态机乱跳。我在FB实现中给队列操作和状态切换做了完整的优先级处理xAbort信号一出现不管当前状态是什么立即清空队列、关断轴使能、把执行状态置为错误停止禁止新的xStart生效直到xAbort被明确复位。这个处理逻辑必须在状态机最顶部判断优先级要高于其他所有条件。// 急停优先处理放在CASE之前 IF #xAbort THEN #iQueueCount : 0; #iWriteIdx : 1; #iReadIdx : 1; #iState : 5; // ERROR #xError : TRUE; #iErrorCode : 100; END_IF;实际调试中驱动器报警是最常触发的急停来源。我把驱动器的报警信号经过一个带有锁存逻辑的中继块接到xAbort上这样即便外部扫描周期晚了几个周期报警状态也不会丢避免报警后驱动器已经停止而PLC还在傻等到位信号那可真会把机械撞坏。6. 从点位到圆弧扩展思路与性能边界6.1 圆弧插补的逼近方法现在的版本只做了G00和G01但我在架构里预留了G02和G03的字段。说到底S7-1200做圆弧插补并不是不可能只是必须有足够清晰的思路。PLC做圆弧的标准做法是“微小直线段逼近法”把一整段圆弧按固定角度步长切成几十上百段微小直线每段起点终点都是圆弧上的点用G01的方式逐段执行。角度步长越小逼近误差越小但单周期计算量越大、轴运动越频繁抖动。具体的公式是已知起点、圆心、半径和圆弧方向用三角函数逐点计算。圆心坐标从I和J偏移量得到起点角度用ATAN2求得每递增一个单位角度用COS和SIN算出新点位。这里的核心是“增量式位置输出”每周期计算新点位直接输出给中断或运动控制层而不是每次复位到位再启动否则圆弧会变成停顿的多边形。S7-1200的运算速度跑这个是可以的但脉冲输出频率受CPU最大脉冲输出限制我实测过的上限大概在几十千赫兹到两百千赫兹左右。所以圆弧插补只适合小半径、中低速的场合大半径高速铣圆肯定不行。6.2 前瞻减速与倍率切换纯解析加执行的问题在于没有前瞻遇到连续快速转角时两个运动段之间的速度过渡会硬生生阶跃。我计划在后面版本加一个简单前瞻在队列里向前多看5条指令如果发现两条相邻的G01方向夹角比较大就将当前段的执行速度提前降下来等过了拐角再加回去。原理和数控系统里的“拐角减速”一致只是更粗糙一些。倍率切换Feedrate Override也是现场必备功能。操作工喜欢在加工过程中随时调整速度比如感觉走快了就拧到50%。我在接口里预留了一个rOverride输入范围0到200%在速度换算输出前乘进去。这个功能看着简单但一定要注意倍率变化后当前位置和目标位置的比例关系不能被破坏否则轴位置会跑偏。我采用的做法是倍率只影响输出速度不改变坐标计算轴多走一段不会错。6.3 S7-1200的实时性边界这是整个方案最重要的边界认知。S7-1200没有专门的运动控制协处理器所有解析、插补、位置判定都跑在同一个CPU循环里。循环周期如果设成1毫秒单周期能做的事非常有限大概只能做一次指令解析加几次坐标计算。循环周期设成5毫秒甚至10毫秒实时性又不够平滑。我实际使用下来这个方案适合的轴速度范围大约在0到3000mm/min以内且路径以点位和直线为主。如果说要跑到5米每分钟以上并且要求路径完全平滑S7-1200会吃力那种需求老老实实上专用数控系统或者带运动控制功能的中型PLC。另外一个容易被忽略的点是S7-1200和上位机的通信延迟会影响G代码逐行下发速度。我最终采用的方式是上位机通过Modbus TCP把整个G代码文件分块写入PLC的数据块PLC内部一次性把文件块解析完形成指令队列再进入执行。这样通信延迟不会插进运动路径中实时性就稳定多了。写在最后从标题里那句“高级语言SCL数控G代码功能块源文件”出发折腾了差不多三周才把第一个能稳定跑的版本做出来。回头看最大的收获不是写了多少行SCL而是彻底理解了一个道理方案边界比方案本身更重要。你知道哪些能做哪些不能做才能在选型的时候不被坑。最后分享一个小技巧调试G代码解析器时不要直接接伺服轴。先在功能块里加一个“仿真模式”把所有输出到轴命令的值打印到HMI屏幕或者监控表里用手写一条G代码逐个检查解析结果和执行顺序。等仿真正确了再接实际机械能省去大量撞机风险。这个习惯我一直保留到现在。