简介:面向西门子S7-1200 PLC与霍尼韦尔、斑马标签打印机联调的实战型PDF教程,重点解决以太网TCP通信建立、打印机指令语言切换与ZPL-II模板下发等常见问题,适合已具备博图基础、需要落地工业打印控制的自动化工程师与项目调试人员。压缩包仅含1个PDF文件,约721KB,内容围绕硬件准备、PX240S通信参数与端口设置、Zebra Designer标签设计及ZPL-II指令获取、TSEND_C通信组态、字符串数据块与打印程序编写、编码测试效果展开。教程还说明ZSim对ZPL-II的兼容关系、打印机默认9000以上端口与西门子0—5000端口范围的冲突处理,以及标签编码位数变化后程序需同步调整等注意事项。已有1660人学习,可作为从组态到联调的排查参考,帮助读者理解PLC侧数据组织、打印机侧格式生成及通信状态码判读,缩短现场调试周期。
1. 产线末端那张标签,为什么值得让 PLC 直接打
产线末端有这么一类活:产品下线,贴一张带流水号的标签,条码规则客户定,格式一年改两三版。常见做法是配台工控机跑打标软件,PLC 把数据交给上位机再转给打印机,链路一长,故障点就多,半夜停线查问题的次数也跟着涨。这套 S7-1200 通过 TCP 通信控制工业标签打印机的方案把中间层砍掉了——PLC 用 TSEND_C 把 ZPL-II 指令原样推给打印机,打印机收到就出纸。适合做非标设备、包装线、仓储贴标的电气和自动化开发,前提是你能看懂博图的组态界面,也愿意为一段字符串里的一个字节较真。
2. 打印机侧准备:IP、端口、ZSim 与 ZPL-II 指令产出
整套链路里最容易出问题的环节不是 PLC 程序,而是打印机那一侧的四个参数。硬件前提先说清:打印机必须带以太网接口,工业机型常见串口加网口双配,只有 USB 的机型接不进来。上电之后不要急着打开博图,先把打印机调通,用电脑单独验证一遍“发一段指令能出纸”,后面 PLC 联调才有对照物。这一节按配置顺序走:网络地址、命令语言、标签设计、指令裁剪。
2.1 触摸屏里配 IP:网关留空、端口压到 5000 以下
打印机的地址配置在机身触摸屏上完成,进入“通信设置”里的配置通信地址页面,填 IP、子网掩码和网关。测试环境用的是 192.168.2.88,这个地址要和 PLC 的网口在同一个网段,网关一栏留空即可,直连场景填了反而会引出莫名其妙的不可达。真正容易翻车的是端口号:打印机出厂默认在 9000 以上,而 S7-1200 的以太网通信端口范围是 0~5000,端口超出这个范围,TSEND_C 永远建不起连接。现场改成一个 5000 以内的值,比如 4000,改完保存。
配完先在笔记本上做一次最小验证,把电脑网卡临时设成 192.168.2.100/24,网线接到打印机:
# 第一层:确认 IP 可达。ping 不通就别往下走了,先查网段和网线 ping 192.168.2.88 # 第二层:确认目标端口被接受。ping 通但端口不通,是打印机端口配错或被占用 # -v 输出详细信息,-z 只扫描不发送数据 nc -vz 192.168.2.88 4000nc -vz返回 succeeded 说明 TCP 层已经通了,这一步比看打印机面板上的指示灯靠谱得多。如果 ping 不通而网线灯亮着,优先怀疑笔记本防火墙和子网掩码,其次怀疑是不是把网关填成了别的网段地址——这个坑我在现场遇到不止一次,配置界面上一行不起眼的网关字段,能让后面两小时全部白费。
2.2 命令语言切 ZSim:让打印机认识 ZPL-II
打印机默认的解释器不一定是 ZPL,有的机型出厂设成自家的指令集,这时候你发过去一串^XA开头的字符,它要么当普通文本打出来,要么直接忽略。切换路径在系统设置里的“常规 → 命令语言”,选中 ZSim,退出时提示是否保存,选是。ZSim 是兼容 ZPL-II 的解释器,选了它,PLC 那边才能用斑马体系那套标签指令。
切换完成后不要凭感觉认定成功,用命令行灌一段最小指令验证一次:
# 一段能打印出 TEST-OK 的最小 ZPL,用来验证解释器语言是否生效 # 如果打出来的是一串原始字符,说明命令语言还是错的 printf '^XA\n^FO50,50^A0N,40,40^FDTEST-OK^FS\n^XZ\n' | nc 192.168.2.88 4000^XA是标签开始,^FO50,50定原点坐标,^A0N,40,40指定字体和字号,^FD...^FS之间的内容才是真正打印的字段,^XZ是标签结束并触发打印。这一整段必须成对出现,缺^XZ打印机就一直攒着不打。验证通过之后,打印机侧的底座就算搭好了,后面 PLC 发的指令只是这段东西的变长版本。
2.3 用标签设计软件把版面画出来,再导出成指令
手写 ZPL 坐标能累死人,正常做法是先用设计软件画版面。选一款支持导出 ZPL 的标签设计工具,新建标签时把宽度、高度、分辨率按实际纸张填准——203dpi 的机器 4 英寸宽对应 812 点,300dpi 就是 1218 点,这个数值决定后面^PW的参数,填错了打印出来是裁切或者留白的。然后按客户给的编码规则摆条码、文字、线框,字体字号都能直接拖。
设计阶段有三件事必须当场定死:一是标签物理尺寸和分辨率,直接决定指令里的^PW、^LL;二是编码规则,几位数、有没有前缀、是不是固定长度,这决定 PLC 里补零的循环写几轮;三是哪些字段是变量。变量字段在设计软件里可以先填占位值,比如123456789,导出后 PLC 只替换^FD后面那一段,其余全部当常量处理,这是最省心的做法。
2.4 打印到文件:拿到一段纯文本,然后做裁剪
设计完点打印,务必勾选“打印到文件”,输出一个.prn或.txt,用记事本打开就能看到生成的 ZPL-II 指令。这一步的意义在于:你拿到的是设计软件按坐标算好的一整段文本,不用自己推算每个字段的位置。
导出后的内容通常比需要的多,先做裁剪再往 PLC 里塞:
^XA ^PW812 ^LL1218 ^CI28 ^FO40,40^A0N,48,48^FDLABEL-A1^FS ^FO40,130^BY3 ^BCN,140,Y,N,N^FD123456789^FS ^FO40,320^A0N,36,36^FD2026-01-01^FS ^XZ逐行说明:^PW812设打印宽度 812 点,^LL1218设标签长度,^CI28声明 UTF-8 编码,要打中文标签必须加,否则中文全是乱码;^FO是字段原点,后面跟的^A0N,48,48是字体和宽高;^BY3设条码模块宽度,^BCN,140,Y,N,N是 Code128 码制,140 是条码高度,Y表示打印可读字符;^FD...^FS是数据段。裁剪时删掉设计软件自动加的注释行、多余的字库下载段,只保留坐标和内容,最后核对^XA和^XZ是否各一个。
提示:
^CI28这段在部分旧固件上要写成^CI28放在^XA之后第一行,位置放错不生效。打完第一张中文标签一定要肉眼确认,别等批量生产才发现是方块字。
3. 博图侧组态:TSEND_C 建链、发数据与 7004 状态判读
打印机侧验证通过之后,PLC 这边的工作量其实不大,麻烦的是参数之间的对应关系。TSEND_C 这一个指令已经把建立连接、发送数据、断开连接三件事打包进去了,但每个引脚都得跟打印机侧的实际配置严丝合缝对上,差一个数字就是连接不上。这一节先把连接参数定死,再逐条过引脚,最后讲状态码怎么读。
3.1 连接参数先定死:IP、端口、连接类型与连接 ID
在博图里做硬件组态时,先确认 PLC 的 PROFINET 接口地址跟打印机同网段,然后在程序里建一个全局 DB 存连接参数,类型选TCON_IP_v4,连接类型填 11(TCP),主动连接。参数对应关系如下:
| 参数 | 取值示例 | 说明 |
|---|---|---|
| InterfaceId | 硬件标识符 | 从设备组态里查 PLC 网口的硬件标识符,填错直接报 809A |
| ID | 1 | 连接编号,同一 PLC 上多个连接不能重号 |
| ConnectionType | 11 | 16#0B,TCP 原生连接 |
| ActiveEstablished | TRUE | PLC 做客户端主动连打印机 |
| RemoteAddress | 192.168.2.88 | 打印机 IP,四个字节分开填 |
| RemotePort | 4000 | 必须与打印机侧配置一致,且在 5000 以内 |
| LocalPort | 0 | 一般留 0,由系统分配 |
这张表里最容易错的是 InterfaceId 和 RemotePort。InterfaceId 不是随便填的数字,要去设备组态的网口属性里找“硬件标识符”,那串十六进制值原样搬过来。RemotePort 则必须和 2.1 节在打印机上改的那个端口号一模一样,两边对不上,状态码会一直停在 7001 或 7002 上打转。
3.2 TSEND_C 引脚逐个过:REQ 给脉冲、CONT 常 TRUE、LEN 给实际值
指令调用建议放在循环中断 OB 或者 OB1 里,实例 DB 单独建一个,不要用多重背景塞在别的地方,现场排查时不好找。核心调用如下:
// 发送指令调用:实例 DB 为 "Inst_TSEND",连接参数来自 "DB_Conn" "Inst_TSEND"( REQ := "DB_Ctrl".bSendReq, // 上升沿触发一次发送,必须是脉冲 CONT := TRUE, // 常连接,避免每次发送都重新建链 CONNECT := "DB_Conn".tcpParam, // TCON_IP_v4 结构体,见 3.1 的参数表 DATA := "DB_Print".sZplBuf, // 待发送的 ZPL 字符串缓冲区 LEN := "DB_Print".iSendLen, // 实际字符数,不是缓冲区最大长度 DONE => "DB_Ctrl".bDone, // 发送完成,只保持一个扫描周期 BUSY => "DB_Ctrl".bBusy, // 发送进行中 ERROR => "DB_Ctrl".bErr, // 出错置位 STATUS => "DB_Ctrl".wStatus); // 状态码,重点看这个几个引脚的含义值得单独说。REQ是上升沿触发,给它一个常 TRUE 的信号只会发第一包,后面全哑火,这是新手最常踩的一个坑,正确做法是用一个按钮变量做上升沿,或者用DONE的下降沿复位。CONT设 TRUE 表示保持长连接,省掉每次发送都三次握手的时间,标签连打的时候差别很明显。DATA可以直接给 String,也可以给 Byte 数组,给 String 的时候发送的就是字符串内容。LEN是最关键的参数——它要的是实际字符数,不是这个 String 声明时给的最大长度,多发了会把缓冲区尾部的空格和零字节一并发出去,打印机收到半截指令会卡住不动。
3.3 7004 与 DONE/BUSY/ERROR:把发送节奏做成状态机
连接建起来之后,STATUS 会停在 16#7004,这个值的含义是“连接已建立,等待发送作业”。看到 7004 就说明网络链路和参数都对上了,剩下的只是发数据的时机问题。发送过程中状态会在 7004、7005、7006 之间跳,7005 表示正在发送,7006 表示发送完成但连接还在。真正的错误码是 8 开头的,809A 是连接参数错,80C4 是对方拒绝,80A4 一般是端口不通。
发送节奏靠一个简单的脉冲加握手撑住:
// 触摸屏按一次"打印",产生一个扫描周期的 REQ 脉冲 "DB_Ctrl".bSendReq := "DB_Ctrl".bTrigPrint AND NOT "DB_Ctrl".bTrigOld; "DB_Ctrl".bTrigOld := "DB_Ctrl".bTrigPrint; // 发送完成后复位按钮状态,避免下一次误触发;出错时也复位,交给报警处理 IF "DB_Ctrl".bDone OR "DB_Ctrl".bErr THEN "DB_Ctrl".bTrigPrint := FALSE; "DB_Ctrl".bSendReq := FALSE; END_IF;这段逻辑解决的是“连打多张”的场景。每按一次按钮产生一个上升沿,REQ 收到脉冲发一包,DONE 置位一个扫描周期后立刻复位按钮变量,下一次按键才能重新产生上升沿。不加这段,第二张标签就打印不出来,现象是第一次正常、后面全无反应,很多人会误以为是打印机坏了,其实是 REQ 一直挂在高电平上。
4. 打印数据区设计:去优化 DB、String 拼装与格式代号切换
数据区设计是这套方案里最见功底的部分。指令能不能发全、编码会不会重复、换一种标签格式要不要改程序,全在这里定。整体思路是分三个 DB:连接参数 DB 存 TCON_IP_v4,控制 DB 存按钮和状态位,打印缓冲区 DB 存 ZPL 字符串和流水号。前两个无所谓,第三个有硬性要求。
4.1 为什么这个 DB 一定要取消“优化的块访问”
新建打印缓冲区 DB 之后,右键属性里有一项“优化的块访问”,默认是勾上的,必须取消。原因在于:优化访问的块里,变量按符号名存取,编译器可以随意调整内存布局,String 的内部结构(前两个字节存最大长度和实际长度,后面才是字符)在绝对寻址下才是确定的。现场调试时经常需要用监视表直接看 DBB 偏移、手工改几个字节试探,优化块下这套手段全部失效。
取消优化之后,String 变量的内存布局就固定了。假设sZplBuf声明为String[1024],它在 DB 里占 1026 个字节,DBB0 是最大长度 1024,DBB1 是当前实际长度,DBB2 开始才是字符内容。这个结构解释了一件事:为什么LEN引脚不能直接填 1024。填 1024 就是把整个缓冲区连同尾部未使用的空白一起推给打印机,打印机解析到一半遇到无效字符,表现就是标签打不出来或者打出半张。
注意:取消优化访问之后要重新编译并下载整个块,只下载改动会留下旧布局,绝对地址全错位。这个动作建议养成习惯,改完 DB 结构先整体编译一次。
4.2 String 拼装与 9 位流水号:补零循环和 LEN 计算
拼装的本质是把模板常量和运行时变量接起来。模板部分直接从 2.4 节裁剪好的 ZPL 抄过来,变量部分只有流水号和日期两处需要替换。流水号要补足 9 位,不足的前面加零,写成一个循环:
// 把 DINT 流水号转成字符串,左补零到固定 9 位 #sSeq := DINT_TO_STRING("DB_Print".diSeqNo); FOR #i := LEN(#sSeq) TO 8 DO #sSeq := '0' + #sSeq; END_FOR; // 拼装完整 ZPL,CR LF 用 CHAR(13) 和 CHAR(10) 拼,方便现场用记事本对照 "DB_Print".sZplBuf := '^XA' + CHAR(13) + CHAR(10) + '^PW812' + CHAR(13) + CHAR(10) + '^LL1218' + CHAR(13) + CHAR(10) + '^CI28' + CHAR(13) + CHAR(10) + '^FO40,130^BY3' + CHAR(13) + CHAR(10) + '^BCN,140,Y,N,N^FD' + #sSeq + '^FS' + CHAR(13) + CHAR(10) + '^FO40,320^A0N,36,36^FD' + "DB_Print".sDate + '^FS' + CHAR(13) + CHAR(10) + '^XZ' + CHAR(13) + CHAR(10); // LEN 取实际字符数,不是 1024 "DB_Print".iSendLen := LEN("DB_Print".sZplBuf);DINT_TO_STRING把数值转成字符,FOR循环从当前长度补到 9 位,LEN返回实际字符数,循环次数就是 9 减去当前长度。如果编码位数从 9 位改成 12 位,把循环上限的 8 改成 11 就行,其他不用动。用 String 而不是逐字符发送的好处就在这里:改编码规则只动一处循环边界,不用去数第几个字符对应条码的第几位。
CHAR(13)和CHAR(10)拼的是回车换行。ZPL 本身不强制换行,但加了之后整段指令在监视表里可读,抓包或者导出对照时一眼能看出是第几行出的问题。最后那句LEN赋值是整个流程的关键,它直接喂给 TSEND_C 的LEN引脚,漏了这句,发出去的就是一整块 1026 字节的缓冲区。
4.3 触摸屏选格式代号:中间转存 DB 与 CASE 分支
一条产线往往要打多种标签,外箱大标签、产品小标签、返修标签,尺寸和内容都不同。做法是在打印缓冲区之外再建一个“模板库”DB,把每种格式的 ZPL 常量模板存在里面,PLC 只根据代号选模板再填变量。代号从触摸屏写进来,用一个 CASE 分支分发:
// iFormatNo 由 HMI 写入:1=外箱标签,2=产品标签,3=返修标签 CASE "DB_Print".iFormatNo OF 1: // 外箱大标签,4x6 英寸,Code128 "DB_Print".sZplBuf := '^XA^PW812^LL1218' + '^FO40,130^BY3^BCN,140,Y,N,N^FD' + #sSeq + '^FS' + '^FO40,320^A0N,36,36^FD' + "DB_Print".sDate + '^FS^XZ'; 2: // 产品小标签,2x1 英寸 "DB_Print".sZplBuf := '^XA^PW406^LL203' + '^FO20,20^A0N,28,28^FD' + #sSeq + '^FS^XZ'; 3: // 返修标签,带 R 前缀 "DB_Print".sZplBuf := '^XA^PW812^LL1218' + '^FO40,130^BY3^BCN,140,Y,N,N^FDR' + #sSeq + '^FS^XZ'; ELSE "DB_Print".sZplBuf := ''; // 代号非法,清空缓冲区避免误发 END_CASE; "DB_Print".iSendLen := LEN("DB_Print".sZplBuf);中间转存 DB 的价值在于解耦。模板段是常量,流水号、日期是变量,两者拼装后才进sZplBuf,模板怎么改都不影响流水号的计数逻辑。ELSE分支不要省,代号非法时把缓冲区清空,LEN变成 0,TSEND_C 收到 0 长度不会发送,等于一道保险。
流水号的唯一性另外说一句。测试阶段用简单累加就够了,diSeqNo := diSeqNo + 1,但生产环境要保证这个变量是断电保持的,DB 里勾上“保持”属性,否则每次上电归零,编码必然重复。更稳的做法是编码里带上日期和班次,比如YYMMDD加三位班次加四位流水,这样即使计数器被重置,也不会跟上一天的数据撞上。
5. 现场排查清单:五个让人熬夜的坑
这套方案跑通之后回头看,真正花时间的不是写程序,是排故障。下面五条都是现场实际撞过的,按“现象 → 原因 → 解决”记下来,下次遇到能省两小时。
5.1 网络与打印机侧的三类故障
现象:TSEND_C 一直报错,状态码 809A 或 80C4,打印机面板上看不出异常。原因:端口号没改,还在出厂默认的 9000 以上,超出 S7-1200 的以太网通信端口范围 0~5000,连接请求根本到不了应用层。解决方法是在打印机通信设置里把端口改到 5000 以内,同时把博图里TCON_IP_v4的 RemotePort 同步改成一样的值,改完重新下载连接参数 DB。这个坑的迷惑性在于 ping 是通的,很多人会一直怀疑程序逻辑。
现象:指令发出去了,打印机有响应但打出来是一串原始字符,或者干脆不出纸。原因:命令语言没切到 ZSim,打印机用自家解释器去解析 ZPL,把^XA当成了普通文本。解决方法是进系统设置 → 常规 → 命令语言,选 ZSim,退出时选择保存,必要时重启打印机。验证方式是 2.2 节的那条 printf 命令,能打出 TEST-OK 才算真通了。
现象:单个标签能打,连续打印时第二张开始内容错位或者丢字段。原因:LEN用了缓冲区的最大长度而不是实际长度,尾部空白字节被推给打印机,打印机把残留的上一包内容当成了新指令的一部分。解决方法是每次拼装完字符串后立刻重算LEN(sZplBuf)赋给发送长度变量,别用常量。另外建议在 ZPL 末尾统一补一个^XZ,让打印机有明确的结束标志,即使收到多余字节也能自行复位。
5.2 PLC 程序侧的两类陷阱
现象:程序编译通过,在线监视看到字符串内容正确,但打印机收到的内容是乱码或者被截断。原因:打印缓冲区 DB 的“优化的块访问”没有取消,String 的内部布局跟绝对寻址对不上,LEN读到的实际长度字节位置偏移了。解决方法是右键 DB 属性取消“优化的块访问”,然后对整个块做一次完整编译并下载。顺带说一句,改完这个属性之后,之前用绝对地址写的所有逻辑都要复查一遍。
现象:按一次按钮打一张正常,再按就没反应,必须重新下载程序才恢复。原因:REQ引脚被给了常 TRUE 信号,TSEND_C 只在上升沿触发一次发送,之后的电平变化不构成新的边沿。解决方法是加一段边沿检测,用bTrigPrint AND NOT bTrigOld生成一个扫描周期的脉冲,并在DONE或ERROR置位后把触发变量复位。这个坑在调试初期几乎人人都会踩一次,血泪经验是:凡是带REQ引脚的通信指令,先想清楚边沿从哪来。
6. 二次开发:把它收成一个可复用的标签模板引擎
第一版程序通常是硬编码的:模板写在 CASE 分支里,流水号逻辑焊死在主程序,换一条产线就要复制一遍改一遍。更省事的做法是把它抽象成一个标签模板引擎,模板内容外置成数据,程序只负责“查表、填变量、发送”三件事。
改造的第一刀是模板存储结构。把CASE分支里那些字符串常量搬到一个独立的模板 DB,声明成Array[1..20] of String[1024],每个下标对应一种标签格式,HMI 上只显示格式名称和对应索引。这样加一种新标签不用改 PLC 程序,只要在 DB 里填一段新模板,把索引告诉触摸屏即可。填模板的时候有个小技巧:在每段模板里用固定占位符标记变量位置,比如{{SN}}、{{DATE}},PLC 侧写一个通用的替换函数循环查找替换,模板本身的可读性也上来了。
// 通用替换:把模板里的 {{SN}} 换成实际流水号 FUNCTION "FC_ReplaceToken" : String VAR_INPUT sTemplate : String; // 原始模板 sToken : String; // 占位符,如 '{{SN}}' sValue : String; // 替换值 END_VAR VAR_TEMP iPos : Int; END_VAR // 找到占位符位置后做三段拼接:前段 + 替换值 + 后段 iPos := FIND(sTemplate, sToken); IF iPos > 0 THEN "FC_ReplaceToken" := LEFT(sTemplate, iPos - 1) + sValue + DELETE(sTemplate, iPos, LEN(sToken)); ELSE "FC_ReplaceToken" := sTemplate; END_IF; END_FUNCTIONFIND返回占位符的起始位置,找不到返回 0;LEFT取前段,DELETE从指定位置删掉占位符长度,剩下的就是后段,三段用加号接起来。这个函数只处理第一次出现的位置,模板里有多个变量就多调几次,或者在外面套一层循环直到FIND返回 0。
第二个改造点是发送层加校验和重试。发送前先检查缓冲区头两字节是不是^X、尾两字节是不是XZ,长度是不是在 1 到 1024 之间,任何一项不过直接置报警不发送,比让打印机吐半张废标签强。发送失败时加一个延时重试,用 TON 定时器等 2 秒再发一次,最多三次,三次都失败才置故障位。
| 改造点 | 改造前 | 改造后 |
|---|---|---|
| 模板来源 | 程序里硬编码 CASE 分支 | 独立 DB 数组,HMI 选索引 |
| 变量替换 | 字符串直接拼接 | 占位符 + 通用替换函数 |
| 发送前校验 | 无 | 头尾标记 + 长度范围检查 |
| 失败处理 | 置位报警等人工 | 延时重试三次后报警 |
| 编码唯一性 | 单纯累加 | 日期 + 班次 + 流水,断电保持 |
第三件事是编码生成。累加计数在单机场景够用,多台设备共用一个编码池就会撞号。改成YYMMDD加两位设备号加三位班次加四位流水,每段都有明确含义,撞号概率降到可以忽略,出问题时也能从编码反推出生产时间。这部分的代码不复杂,难的是跟客户把编码规则谈死,规则没定就写程序,后面改起来比重写还麻烦。
从那以后我每次做这类打印项目,都会先在打印机上用 nc 手工灌一段最小 ZPL 验证通路,确认出纸了再打开博图,绝不跳过这一步直接联调。方向对了,后面的组态和拼装都是体力活;方向错了,你在博图里改一百遍状态码也出不来一张标签。希望帮到你。
本文还有配套的精品资源,点击获取