简介:这份教程面向使用西门子 S7-1200 PLC 和博图软件、需通过以太网直接驱动霍尼韦尔或斑马标签打印机的自动化工程师与设备调试人员,重点解决工控设备与打印机之间 TCP 通信建立、ZPL-II 指令转换及打印控制编程等问题。压缩包为 1 个 PDF 文件,约 721KB,内容围绕霍尼韦尔 PX240S 打印机的网络参数配置、ZSim 指令语言切换、标签设计导出 ZPL-II 代码,以及在博图中调用 TSEND_C 建立连接、组织数据块和编写可变编码打印程序的完整思路展开。已有 1660 人学习,说明该方案在同类现场调试中具有参考价值。读者可了解打印机端口需改至 5000 以下、连接状态码 7004 的含义、字符串与单字符发送的取舍、去优化数据块便于绝对寻址等细节,并据此搭建可切换标签格式、按计数生成唯一编码的打印控制逻辑,减少现场联调时的试错成本。
1. 打印机不动:先区分 7004 是正常状态,还是连接卡死
博图(TIA Portal)里挂上 TSEND_C,编译不报错,下载也不报错,可标签打印机就是一点反应都没有,在线诊断里状态码停在 7004——很多人第一反应是这串数字代表出错,然后开始逐个参数乱改,越改越乱。另一边还有一类反直觉的情况:连接建起来了,数据也发出去了,西门子 PLC 那边 DONE 置位,结果斑马打印机吐出一张白纸,或者把 ZPL-II 源码原样打出来,纸上印着^XA^FO40,40^A0N...这种东西。西门子 S7-1200 通过 TCP 控制霍尼韦尔标签机或斑马打印机这类活,卡人的地方几乎不在梯形图逻辑本身,而是集中在三处:端口范围、打印机指令语言、以及 ZPL-II 字符串从博图里送出去时的字节组织方式。这三处任一处理错,表现出来的现象都是同一个——没反应。
2. TSEND_C 卡在状态码 7004:连接阶段和数据阶段的判定
2.1 7004 表示连接已建立,等待发送作业
先把最容易误判的一点说清楚。TSEND_C 的状态码里,7004 的意思是「连接已经建立,正在等待发送作业」,它不是错误码,而是一个中间态。你在博图里勾上监视,如果 REQ 一直为 FALSE,STATUS 就稳稳停在 7004;一旦 REQ 出现上升沿,状态会跳到 7005(正在发送),发送完成变成 7006,然后回到 7004 等下一条。所以看到 7004 别急着改参数,要先确认两件事:连接对象到底通没通,以及 REQ 有没有真的产生上升沿。
真正麻烦的是 7004 之后不动、或者干脆连 7004 都到不了。这种情况下我一般按「谁主动、连谁、连哪个口」这三个问题倒查。TSEND_C 内部已经把建立连接、发数据、断连接打包了,你只要给对 CONNECT 结构,它自己会去握手。CONNECT 是一个 TCON_IP_v4 类型的结构体,必须放在一个非优化的全局 DB 里长期存在,不能塞进 FC 的临时变量——临时变量每个扫描周期都会重建,连接握手刚走一半结构体内容就被覆盖,表现出来就是状态在 7004 和 8085 之间反复横跳,或者直接挂死。
2.2 常见返回码对应关系
下面这张表是我在现场遇到过、以及在同行群里被问得最多的几个码。不同固件版本的措辞可能略有出入,遇到没见过的码直接查博图在线帮助里 TSEND_C 的状态码页,那里列得比任何博客都全。
| 状态码 | 含义 | 常见触发原因 | 先查什么 |
|---|---|---|---|
| 7004 | 连接已建立,等待发送作业 | REQ 无上升沿 | 触发逻辑、HMI 按钮是否点动 |
| 7005 | 连接已建立,正在发送数据 | 正常过程态 | 无需处理 |
| 7006 | 连接已建立,发送完成 | 正常过程态 | 看 DONE 是否复位 |
| 0000 | 无错误 | 正常态 | — |
| 8085 | 建立连接失败,对方无响应 | 打印机 IP 不对、未开机、网线没插、端口无人监听 | ping、端口探测 |
| 8087 | 连接被对方拒绝或中止 | 端口填了但打印机不监听该端口 | 核对打印机端口设置 |
| 8090 | 连接 ID 非法或被占用 | 同一 ID 被两个指令用、和 TCON 混用 | 检查全部通信指令的 ID |
| 80A7 | 通信超时 | 网络时通时断、交换机问题 | 抓包或长期 ping |
| 80C3 | 连接已存在 | CONT 保持时重复触发建连 | 检查 CONT 与 REQ 的组合 |
表里 8085 和 8087 是新手最容易混淆的一对:前者是「压根找不到人」,后者是「找到了人,但对方不开门」。前者去查 IP 和物理链路,后者九成是端口号填错了,或者打印机那边根本没启用该端口。
2.3 一份能直接抄的 TSEND_C 调用
下面这段是 SCL 里的调用骨架,DB 里的 ZPL 指令和长度由前面的组包逻辑准备好,这里只负责发。用 SCL 写是因为参数多,梯形图上摆一堆引脚反而看不清。
// 每条标签一个发送请求,REQ 用上升沿触发,避免一张标签连打多张 #bTrig(CLK := "HMI".btnPrint); // HMI 打印按钮,点动 "DB_Comm".reqTSEND := #bTrig.Q; // 只在按钮按下的那一个扫描周期为 TRUE // TSEND_C 调用:CONT 常 1 保持连接,避免每条标签都重新握手 "TSEND_C_DB"(REQ := "DB_Comm".reqTSEND, CONT := TRUE, // 常 TRUE:连接保持 LEN := "DB_Comm".iSendLen, // 本条 ZPL 的实际字节数 CONNECT := "DB_Comm".tconIP, // TCON_IP_v4 结构,放在非优化 DB DATA := "DB_Comm".aSendBuf, // Array[0..999] of Byte DONE => "DB_Comm".doneSend, BUSY => "DB_Comm".busySend, ERROR => "DB_Comm".errSend, STATUS => "DB_Comm".statSend); // 发送成功后立刻清触发位,为下一次上升沿做准备 IF "DB_Comm".doneSend THEN "DB_Comm".reqTSEND := FALSE; END_IF;几个参数值得单独说明。REQ必须是上升沿,这是重复打印问题的总根源——如果直接把按钮的常开点接上去,按钮按住期间每个扫描周期都会触发一次,一张标签能给你连打几十张。CONT := TRUE表示连接建立后一直保持,这样后续每条标签只发数据、不重新三次握手,单条耗时能压到几十毫秒;把 CONT 设成 FALSE 的话,每发一条都要重连一次,节拍慢不说,打印机那边的连接槽位还可能来不及释放,第二次就撞上 80C3。LEN填的是本条数据的有效字节数,不是缓冲区总长度,这一点在第四章会展开讲,写错了就是打印内容截断。DATA我建议用Array[0..999] of Byte而不是直接给 String,原因同样在第四章。
CONNECT对应的 TCON_IP_v4 结构,参数这样填:
// TCON_IP_v4 结构:全局 DB 内定义,属性里取消「优化的块访问」 "DB_Comm".tconIP.InterfaceId := 64; // PLC 的 PROFINET 接口硬件标识符,设备组态里查 "DB_Comm".tconIP.ID := 1; // 连接 ID,同一 CPU 内唯一 "DB_Comm".tconIP.ConnectionType := 11; // 11 = TCP "DB_Comm".tconIP.ActiveEstablished := TRUE; // PLC 作为主动连接方 "DB_Comm".tconIP.RemoteAddress := '192.168.2.88'; // 打印机 IP "DB_Comm".tconIP.RemotePort := 4000; // 打印机监听端口,必须 ≤ 5000 "DB_Comm".tconIP.LocalPort := 0; // 0 表示由系统自动分配InterfaceId这个值很多人随手填 64 是蒙对的,正确做法是打开设备组态,点 CPU 的 PROFINET 接口,看属性里的硬件标识符,填那个数。填错的典型现象是编译通过、下载正常,运行时直接 8090。LocalPort填 0 让系统分配最省事;如果某个固件版本不接受 0,就填一个 2000 到 4999 之间的固定值,别填 9100 之类的,同样会踩端口范围的坑。
3. 端口号超范围:9100 与西门子 0–5000 的硬冲突
3.1 打印机默认端口为什么在博图里天生连不上
标签打印机出厂默认的原始打印端口通常是 9100,部分型号还有 6101。而 S7-1200 的开放式以太网通信,本地端口和远程端口的取值范围都限制在 5000 以内(0 表示自动分配)。这就意味着你在 TCON_IP_v4 的RemotePort里根本填不进 9100——即便某些版本的编辑器允许你敲进去,下载运行后也建不起连接。这是最典型的「配置看着全对、就是不通」,因为两边都没错,错的是它们不在同一个可选范围里。
解决办法只有一个方向:把打印机侧的端口改到 5000 以下,PLC 跟着改。常见做法是改成 4000、3000、2500 这类不冲突的端口号。改之前先确认这个端口没被产线上的其他设备占用,同一网段里端口冲突比 IP 冲突更难查。
3.2 改端口的正确顺序:先打印机,再组态,最后下载
顺序错了会浪费大量时间。我习惯按这四步走,每步都有明确的验证点:
第一步,进打印机的设置界面,找到通信/端口配置页,把原始打印端口从 9100 改成 4000,保存并让网络服务重启。霍尼韦尔这类带触摸屏的机型是在触屏菜单里改,带网页配置的机型用浏览器访问打印机 IP 进配置页改。第二步,用 PC 做端口探测,确认 4000 真的在监听。第三步,回到博图改RemotePort,重新编译下载。第四步,在线监视状态码。
第二步的验证命令,Windows 上用 PowerShell 的就够:
# 测打印机端口是否真的在监听,TcpTestSucceeded 为 True 才算通 Test-NetConnection -ComputerName 192.168.2.88 -Port 4000 # 顺便确认网络层通不通,这一步通了不代表端口通 ping 192.168.2.88 -n 4Test-NetConnection的结果里重点看两行:PingSucceeded和TcpTestSucceeded。ping 通但 TCP 不通,说明网络层没问题,问题在打印机没在那个端口上监听——要么端口没改成功,要么改完没重启网络服务。两个都不通,先查网线、交换机、IP 网段。我遇到过一次改完端口没断电重启,配置写进了 flash 但没生效,白折腾了半小时,从那以后改完打印机网络参数一律断电重启一次。
3.3 网关留空、掩码一致,以及「ping 通」的误导性
打印机的 IP、子网掩码要和 PLC 在同一网段,网关默认留空。同网段通信本来就不经过网关,但网关填错会带来一个隐蔽的问题:某些打印机的网络栈在启动时会尝试向网关注册或者做路由探测,网关不可达的情况下启动会变慢,甚至短暂不响应连接请求,表现出来就是「刚上电那两分钟连不上,过一会儿又好了」这种玄学现象。所以别图省事乱填。
还有一点必须提醒:ping 通只证明 ICMP 可达,不证明 TCP 端口可用。这两件事在网络层是分开的,很多现场排查就卡在「能 ping 通啊」这句话上,然后在 PLC 程序里翻来覆去改了半天,其实问题一直在打印机那头。判断顺序永远是:先 ping,再测端口,最后才动博图。
4. 连接通了却打不出标签:ZPL 指令、DB 寻址与 LEN 三处翻车
4.1 ZPL 指令的正确来源:打印到文件,不是打印
打印机能连上、数据也发出去了,但纸上是空白或者直接印出指令源码,第一个要怀疑的是打印机当前使用的指令语言。这类打印机出厂可能工作在自家的 PCL 或者别的描述语言下,收到 ZPL 会当成普通文本原样输出,于是你就看到纸上印着^XA。正确做法是进入系统设置,找到常规里的命令语言选项,切换成兼容 ZPL-II 的模式(部分机型叫 ZSim 或 ZPL 兼容模式),保存退出时选择保存。
指令语言对了,接下来是指令本身从哪来。别手写,用标签设计软件画:按实际标签纸尺寸和客户要求的编码规则排版,字体、字号、条码类型都在图形界面里调。画完之后点打印,但必须勾选「打印到文件」,让软件生成一个指令文件而不是真的驱动打印机出纸。把生成的文件用记事本打开,里面就是完整的^XA ... ^XZ指令段。
^XA ^PW812 ^LL609 ^FO40,40^A0N,50,50^FDTEST-000000001^FS ^FO40,120^BY3^BCN,100,Y,N,N^FD000000001^FS ^XZ这段里几个字段值得记一下:^PW是打印宽度(点),^LL是标签长度,^FO是坐标偏移,^A0N,50,50是字体和宽高,^FD...^FS之间就是要打印的内容,^BY3和^BCN是条码的窄条宽度和类型。PLC 要做的,就是把^FD后面那串编码替换成变量,其余部分原样不动。
4.2 DB 去优化访问:绝对寻址失效的根源
这一步是纯纯的血泪经验。你在 DB 里定义好 ZPL 模板和缓冲区,编译下载,运行时 DATA 参数就是报错或者发出去乱码,八成是因为这个 DB 还带着「优化的块访问」属性。优化访问的 DB 里,变量没有固定的绝对地址,编译器只保证符号访问,而 TSEND_C 的 DATA 参数在底层是要拿一段连续内存的起始地址和长度,你没法用P#DB1.DBX0.0 BYTE 200这种写法去指它。
解决方式很直接:打开 DB 属性,取消勾选「优化的块访问」,确定,然后重新编译整个项目。改完之后在 DB 的监视界面才看得到偏移量列,变量地址才固定下来。这个操作有个副作用——一旦取消优化,DB 里所有变量的地址就固定了,后续增删变量要重新编译并注意地址变化,所以我在项目初期就把通信用的 DB 结构定好,尽量少动。
顺带说下 String 的坑。直接把String[200]类型的变量接到 DATA 上,发出去的字节流前面会多出两个字节——String 的类型头(最大长度和当前长度),打印机收到这俩字节会当成无效指令,轻则忽略,重则把后续内容也当垃圾丢掉。所以我用Array[0..999] of Byte做发送缓冲区,组包时就把 String 转成字节数组,长度单独算,发出去干干净净。
4.3 LEN 与组包:编码位数一改就截断
LEN是 TSEND_C 最容易写错的一个参数。它要的是「本次要发送的字节数」,不是缓冲区的容量,也不是 DB 的总长度。很多人图省事直接填个 500 或者填缓冲区的上限,结果就是每次发送都带上一大段填充字节,打印机把填充字节当垃圾指令处理,或者干脆因为收到超长数据包而丢弃整包。
正确的做法是组包时就把有效长度算出来。下面这段 SCL 演示了按格式号选模板、拼流水号、算长度的完整过程:
// 组包:按 HMI 选的格式号取对应 ZPL 模板,拼上本次流水号 CASE "HMI".iFormatNo OF 1: // 格式一:9 位数字编码,一维码 "DB_ZPL".sZPL := "DB_ZPL".sTplA + "DB_ZPL".sSerial9; 2: // 格式二:长编码,带前缀 "DB_ZPL".sZPL := "DB_ZPL".sTplB + "DB_ZPL".sSerial17; ELSE "DB_ZPL".sZPL := "DB_ZPL".sTplA + "DB_ZPL".sSerial9; END_CASE; // String 转字节数组,避免 String 头两字节混进数据流 // Strg_TO_Chars 需 TIA V15.1 及以上;更早版本改用 MOVE_BLK 从 String 的 +2 字节处搬 Strg_TO_Chars(IN := "DB_ZPL".sZPL, Cnt => "DB_ZPL".iCharCnt, CharsArray := "DB_ZPL".aSendBuf); // 发送长度只取有效字符数,绝不用缓冲区容量 "DB_ZPL".iSendLen := "DB_ZPL".iCharCnt; // 流水号累加,超出位数范围回绕,保证编码不重复也不溢出 "DB_ZPL".dwSerial := "DB_ZPL".dwSerial + 1; IF "DB_ZPL".dwSerial > 999999999 THEN "DB_ZPL".dwSerial := 1; END_IF;这里有几个细节。Strg_TO_Chars会把 String 的有效字符逐个搬到 Char 数组,返回值Cnt就是实际字符数,正好拿来当LEN。流水号用 DWord 累加,回绕判断按编码位数来定上限,九位编码就是 999999999,订单量大的时候还要考虑当天清零或者按批次重置。模板字符串(sTplA、sTplB)本身也是在非优化 DB 里,开机时从初始值加载一次,后续由 HMI 选的格式号决定用哪个。编码位数如果客户要求从 9 位改成 12 位,模板里的^FD字段长度和流水号上限都要同步改,改漏了一处,打出来的标签就是位数不对或者条码扫不出来。
5. 避坑现场记录:重复打印、连接占用、空白标签这五件事
5.1 五条现象、原因与解决记录
现象一:点一次按钮,打印机连吐十几张一模一样的标签。原因基本可以确定是 REQ 没做上升沿,按钮按住期间每个扫描周期都在触发发送,而打印机收到一条打一张,就变成了连打。解决是在 REQ 前加一个上升沿检测,或者把触发位在 DONE 之后立刻复位。
现象二:第一张打得好好的,第二张开始状态码跳到 80C3 或者 8090。常见原因是同一连接 ID 被多个指令共用,或者是 TSEND_C 和 TCON/TSEND 混着用——TSEND_C 内部已经管了建连和断连,你再挂个 TCON 去抢同一个 ID,两边打架。也可能是CONT设成了 FALSE,每条发完就断连,打印机侧连接槽位没及时释放。解决是统一用 TSEND_C,连接 ID 全局唯一,CONT常 TRUE。
现象三:纸打出来了,上面印的是^XA^FO...源码。打印机指令语言没切到 ZPL 兼容模式,把指令当普通文本渲染了。进系统设置的命令语言项切换,保存后重启打印机。
现象四:标签内容比设计的短,后半截没了。要么LEN填小了,要么 DB 里 String 定义的长度装不下实际内容被截断,要么编码位数改长了但模板里的字段没跟着改。排查方法是先在 PC 上用脚本把同样的 ZPL 直接发给打印机,确认指令本身完整,再回头查 PLC 侧的组包和长度。
现象五:中文标签打出来是问号或者空白。ZPL 内置字库不含中文字形,直接写中文进^FD打不出来。要么在标签设计软件里把中文转成图形(位图指令^GF)嵌入,要么用支持中文的字体模块。这一条在切换语言时最容易翻车,设计阶段就得确认。
5.2 上线前值得固定下来的三个习惯
第一个习惯:任何一条通信指令上线前,先用 PC 直连打印机把 ZPL 跑通。这一步能把「指令问题」和「PLC 问题」彻底切开,省掉大量在博图里瞎改的时间。用 Python 发一段就是几行代码的事:
import socket # 直接用 PC 绕过 PLC,验证打印机是否吃这条 ZPL 指令 zpl = b"^XA\n^FO40,40^A0N,50,50^FDTEST-000000001^FS\n^XZ\n" with socket.create_connection(("192.168.2.88", 4000), timeout=3) as s: s.sendall(zpl) # 发完即断,打印机收到一条打印一条 print("sent", len(zpl), "bytes")create_connection的两个参数是打印机 IP 和改好的端口,sendall保证整段发完。这段脚本能跑通,说明打印机、端口、指令语言、ZPL 内容全部正确,剩下的问题一定在 PLC 侧。跑不通,就别在 PLC 上浪费时间了。
第二个习惯:把所有通信相关的 DB 集中在一个组里,统一取消优化访问,并且在项目文档里记下每个 DB 的用途和长度上限。第三个习惯:LEN永远由程序算,绝不在参数上写死数字。
6. 快速定位:从「按了没反应」到「第一张标签吐出来」
现场排这类故障,最怕的是没有固定顺序,东改一下西改一下,改到最后自己都忘了哪个参数动过。我现在的固定套路是四层往下剥,每层都有明确的判定信号,剥到哪层断了就停在哪层,不要跳。
| 层级 | 判定手段 | 通过标志 | 不通过时改什么 |
|---|---|---|---|
| 网络层 | ping 打印机IP | 稳定回包,无丢包 | 网线、交换机、IP 与掩码 |
| 端口层 | Test-NetConnection -Port 端口 | TcpTestSucceeded = True | 打印机端口号、网络服务是否重启 |
| 指令层 | PC 用 Python 直发 ZPL | 打印机正常出纸 | 指令语言、ZPL 内容、标签尺寸 |
| 程序层 | 博图在线监视 STATUS 与 LEN | 7004 → 7005 → 7006 → DONE | REQ 沿、CONNECT 结构、LEN、DB 优化属性 |
四层里前三层全部通过、只剩程序层的时候,排查范围其实已经很小了,就那几个参数。我一般的顺序是:先看 LEN 的实际值对不对(在线改一下,看 DONE 之后打印机有没有反应),再看 REQ 有没有上升沿,接着看 CONNECT 结构里的端口和 IP 有没有跟着打印机改动同步,最后才去怀疑 DB 的优化属性。之所以把 DB 属性放最后,是因为它一旦错了,通常第一张标签就出不来,而不是时好时坏,症状比较有辨识度。
还有一个很实用的技巧:在博图里给 STATUS 加一个趋势记录或者简单的历史值捕获,把最近几次发送的状态码和时间戳留下来。有些故障是间歇性的,你盯着屏幕看的时候它偏偏是好的,一转身就 8085 了。留一份状态历史,比现场蹲守有效得多。法宝只有一个——把每次改动的参数记下来,改一处验一处,别一次动三个地方。
从那以后我每次接手这类 TCP 打印项目,都会先花十分钟把四层验证走一遍,哪怕设备是刚出厂的新机、参数是别人配好交付的,也照样走。踩过的坑告诉我,省下的这十分钟,最后往往要拿半天来还。希望帮到你。
本文还有配套的精品资源,点击获取