ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CODESYS中文字符串乱码解决方案:WSTRING与Unicode实战详解

CODESYS中文字符串乱码解决方案:WSTRING与Unicode实战详解 干过几年CODESYS现场项目的人十有八九都被中文字符串坑过。我在汇川、信捷这些国产CODESYS平台上调程序时踩过最深的一个坑就是用STRING类型传中文上位机收到的是一堆乱码或者触摸屏上显示成问号。明明代码逻辑全对就是字符串这块怎么都对不上。后来把WSTRING和Unicode这套东西彻底搞明白之后才算真正解决了问题。这篇东西不是教科书式地罗列函数而是把我实际调试中验证过的WSTRING用法、Unicode编码原理、以及那些文档里不会告诉你的坑按实战顺序梳理出来。适合正在用CODESYS做设备联网、HMI交互、协议解析的工程师尤其是设备需要跟MES系统、上位机软件交换中文数据的场景读完之后你应该能少走很多弯路。1. 为什么非要用WSTRING——先看清STRING的历史包袱1.1 你在CODESYS里写中文乱码根源在STRING的字节思维很多刚接触CODESYS的工程师会疑惑我明明在程序里写了“温度正常”四个字STRING变量里也能赋值为什么一通信就乱码核心原因在于STRING类型的设计思路太老了。IEC 61131-3标准里的STRING本质上是单字节字符串一个字符占用一个字节它默认你处理的是ASCII字符集。而中文字符在UTF-8编码下需要3个字节在GBK编码下需要2个字节这已经超出了STRING“一个字符等于一个字节”的基本假设。更麻烦的是CODESYS里的STRING长度用字节数来算。你声明STRING(20)意思是20个字节不是20个字符。中文字符串“温度正常”如果按UTF-8编码4个汉字占12字节加上结尾的0勉强塞得进STRING(20)可一旦字符串里还有英文字母和数字混排长度计算就彻底混乱了。LEN函数返回的是字节数MID、LEFT这些函数按字节位置截取非常容易把一个汉字的编码切成两半切出来就是乱码。1.2 WSTRING的核心改变字符与字节解耦WSTRING的引入就是把“字符”这个概念从字节里解放出来。WSTRING里的每个元素是WCHAR类型占用16位也就是2个字节对应Unicode编码里的一个码元。对中文来说绝大多数常用汉字都在Unicode的基本多语言平面内一个汉字在WSTRING里就是一个WCHAR长度计数、索引、截取都是按“字符”来数不再有半个UTF-8编码的问题。我打个比方你就明白了。STRING处理中文字符串就像你手里拿着一串串好的珠子但珠子和珠子之间没有明确的色块区分你想从中间拿掉一颗红色珠子结果把旁边两颗蓝色的也带出来了。WSTRING则相当于每颗珠子都带独立编号你说拿第几颗就是第几颗绝不牵连别的。从编程体验上讲WSTRING最直观的优势是LEN返回的字符数和你眼睛看到的文字个数是一致的。比如“温度正常”四个字在WSTRING里LEN就是4而不是12或者8。这对后续做字符串截取、拼接、比较、搜索的逻辑设计有着决定性的意义。2. WSTRING的编码原理与内存结构不懂这个后面全懵2.1 Unicode、UTF-16和WCHAR的关系要真正用好WSTRING绕不开Unicode和UTF-16这两个概念。Unicode是一个字符集给全世界几乎所有文字都分配了一个唯一的数字编号这个编号叫码点。中文汉字“温”的码点是U6E29这个数字本身不代表存储格式。UTF-8和UTF-16都是编码方案解决的是“用多少个字节表示这个码点”的问题。WSTRING在CODESYS的实现中采用UTF-16编码。UTF-16的特点是对于Unicode中基本多语言平面内的字符也就是码点从U0000到UFFFF直接用2个字节表示。中文最常用的几千个汉字基本全在这个范围内所以每个汉字占一个WCHAR。超出这个范围的生僻字、emoji等属于辅助平面字符会用2个WCHAR也就是4个字节来表示这个技术叫代理对。这意味着什么呢 绝大多数工业场景里你要处理的中文都在基本多语言平面内所以可以简单理解为一个WCHAR容纳一个汉字。但如果你要处理特殊表情符号或者极其生僻的汉字就必须考虑代理对的存在。我在实际的设备告警推送里就遇到过类似情况现场操作工从手机复制了一个生僻字粘贴到输入框上位机通过TCP发过来解码时如果不按照代理对规则处理整个字符串顺序就会错位。2.2 WSTRING在PLC内存里的真实布局WSTRING在CODESYS中的内存布局有两个版本需要分清。在默认的普通WSTRING类型下它以一个32位的长度前缀开头这个前缀记录了字符串占用多少个字节不是字符数紧接着是WCHAR数组最后以一个16位的0值WCHAR作为字符串结束符。举个例子声明一个WSTRING(10)并赋值“温度”两个字内存分布就是前4字节存长度值42个字符乘以2字节中间4字节存“温”和“度”的UTF-16编码最后2字节存0x0000作为结束。总共占用10个字节。但这里有个更隐蔽的细节。在CODESYS的较新版本里WSTRING默认是“即时初始化”的也就是说变量声明之后内存就已经按最大长度分配好了。WSTRING(10)不管实际只存了几个字符都固定占用32位长度头加20字节的字符数组空间再加2字节结束符一共26字节。这一点在做通信报文组包、内存优化时需要特别注意。我见过有人声明了一堆很大的WSTRING数组用来存储配方文本结果程序尺寸和内存占用比预期大出一截排查半天才发现是WSTRING的固定长度分配机制在“吃”内存。2.3 STRING和WSTRING的六大差异对照为了直观看见问题我把STRING和WSTRING的核心差异整理成一张表。这张表我实际在给团队做培训时用过反馈很好基本上把最常见的疑惑都覆盖到了。对比维度STRINGWSTRING字符单位1字符1字节1字符2字节UTF-16码元存储编码依赖字符集设置常用UTF-8固定UTF-16中文存储1汉字占2~3字节1汉字占1个字符位2字节LEN返回值字节数字符数结束符1字节的02字节的0x0000内存头无特殊头32位长度前缀通信友好性与多数上位机默认编码不兼容需显式处理编码转换从这张表可以清楚看到WSTRING并不是WHERE“更好”而是“更贴合字符处理逻辑”。如果你只是在PLC内部存储几个固定ASCII码字符串STRING反而更省内存。但一旦涉及中文以及多变长度文本处理WSTRING的优势是压倒性的。3. WSTRING函数库的核心操作我用实测结果说话3.1 CODESYS里到底提供了哪些WSTRING相关函数CODESYS的标准库对WSTRING的支持经历了一个渐进过程。早期版本里字符串函数基本都是针对STRING设计的WSTRING只能做赋值和比较想拼接得自己写循环。近几年的版本标准库已经补全了大部分针对WSTRING的函数只是名字上做了区分使用的时候要留意。我用过的几个常用函数包括WSTRING_TO_STRING、STRING_TO_WSTRING是编码转换的核心WLEN返回WSTRING的字符长度WCONCAT用于拼接两个WSTRINGWLEFT、WRIGHT、WMID分别处理从左侧取、从右侧取、从中间取的子串操作WFIND做子串搜索。这些函数在CODESYS的Standard库和字符串处理工具库中都有具体名称可能因版本略有差异你可以在库管理器里搜索“WSTRING”关键字把相关的函数一次性拉出来看看。不过有个点必须提醒WCONCAT这类函数在使用时需要预先保证目标变量有足够的空间。它不会自动扩容目标WSTRING声明成多大就是多大超过长度后要么报错要么静默截断。我在项目里就吃过这个亏两个字符串拼接后长度超过目标变量容量程序不报错但结果被截掉了后续匹配逻辑怎么都对不上排查了很久才发现是截断问题。3.2 STRING与WSTRING互转编码一致性是命门STRING和WSTRING之间的转换是日常开发中频率最高的操作。上位机通过Modbus发送过来的字符串通常是字节流形式CODESYS默认当作STRING处理如果你后续要用WSTRING来做字符级操作就必须转换。转换的核心在于编码必须匹配。CODESYS的STRING_TO_WSTRING默认认为STRING里的内容是UTF-8编码逐字节按UTF-8规则解析成Unicode码点再存成WSTRING。如果你的字符串源头是GBK编码直接转出来的中文字符就是乱的。这个坑我踩得很深有一次对接某国产扫码枪它输出的中文是GBK编码我用STRING_TO_WSTRING一转出来的全是乱码后来在读写字节层面做了GBK到UTF-8的转换才解决问题。所以做转换前一定要确认数据源的编码格式。串口设备、扫码枪、老款HMI大多默认GBK或ASCII而TCP/IP通信、MQTT、数据库接口基本默认UTF-8。确认不了的时候最稳妥的办法是把原始字节流用HEX显示出来判断汉字编码开头是E开头的UTF-8还是类似D6C2这种GBK特征码。3.3 手写一个WSTRING字符搜索函数备用虽然标准库提供了WFIND但我在一些老版本库或者特殊平台上发现它不支持模糊匹配、不支持大小写不敏感搜索这时候自己手写一个反而更可靠。下面是我实测过的一个WSTRING字符搜索函数逻辑简单但很扎实。FUNCTION F_FindWStringChar : BOOL VAR_INPUT pSource : POINTER TO WCHAR; nMaxLen : DINT; wTarget : WCHAR; END_VAR VAR_OUTPUT nFindPos : DINT; END_VAR VAR i : DINT; END_VAR F_FindWStringChar : FALSE; nFindPos : -1; IF pSource 0 THEN RETURN; END_IF FOR i : 0 TO nMaxLen - 1 DO IF pSource[i] 0 THEN EXIT; END_IF IF pSource[i] wTarget THEN nFindPos : i; F_FindWStringChar : TRUE; RETURN; END_IF END_FOR这个函数通过指针遍历WCHAR数组找到目标字符就立即返回位置。它的好处是不依赖任何库函数在国产CODESYS平台上也能直接编译运行。你可能会问为什么不直接用WFIND我的经验是库函数在异常输入下比如空指针、超长字符串的防御能力比较弱自己写反而更可控还能顺便加上日志输出。3.4 拼接性能问题反复内存分配是隐形杀手WSTRING的拼接性能问题是我在大量告警消息组包时发现的。连续拼接几十个字符串如果使用WCONCAT逐个累加程序执行时间会明显变长逻辑扫描周期都受到了影响。原因在于WCONCAT每次拼接都要检查目标字符串长度、确定新结束符位置有时还要移动整段内存。更高效的做法是先用一个大数组做缓冲一次性组包完成再整体赋值给目标WSTRING。我实际项目中组告警消息每条消息由设备号、故障代码、故障描述、时间戳四段拼接组成如果用WCONCAT连续拼接四次两千条告警组包耗时大约是700多毫秒。改成先写入WCHAR数组、最后一次性转成WSTRING后耗时降到200毫秒左右。对于需要快速响应的设备状态上报这个差异是能感知到的。4. 实操演练从通信接收到页面显示一次走通中文链路4.1 环境准备与工程设置别让编辑器编码拖后腿在开始写代码之前有一个很容易被忽略但极其关键的步骤CODESYS开发环境的文件编码设置。CODESYS IDE默认可能使用本地系统编码保存源文件如果你在程序里直接写中文字符串字面量比如strMsg : 温度正常;在项目文件被Git等版本管理工具共享、或者在不同语言版本的CODESYS上打开时可能因为编码不一致导致中文变成乱码。我建议在工程设置里把源文件编码明确设为UTF-8并在所有编写WSTRING字面量的地方使用前缀N也就是写成N温度正常。加了N前缀后编译器的行为就是明确创建WSTRING常量而不是先按STRING处理再隐式转换少了这层转换就少了一个出错的可能。这个细节是我在跨平台项目里被坑了几次之后总结出来的项目组一起开发时尤其管用。4.2 实战TCP报文里的中文怎么解出来设备通过网络模块接入MES系统MES下发一个JSON格式指令其中包含中文的产品名称设备执行后返回中文结果。完整链路是TCP接收原始字节流从JSON里提取中文名称对应的UTF-8编码字节段转成WSTRING再进行业务逻辑判断最后把结果转成UTF-8字节流回传。我从CODESYS里抽取关键代码片段来说明。假设接收缓冲区recvBuf里已经存了完整报文找到“name”:这段后后面的就是UTF-8编码的字符串数据。VAR rawBytes : ARRAY[0..255] OF BYTE; nameLen : INT : 12; (* 假设name字段值的UTF-8字节长度 *) wsName : WSTRING(64); asciiName : STRING(128); pRaw : POINTER TO BYTE; pAscii : POINTER TO BYTE; i : INT; END_VAR FOR i : 0 TO nameLen - 1 DO rawBytes[i] : recvBuf[headerLen i]; END_FOR pRaw : ADR(rawBytes); pAscii : ADR(asciiName); MEMCPY(pAscii, pRaw, nameLen); asciiName[nameLen] : 0; wsName : STRING_TO_WSTRING(asciiName);这里最关键的一步是STRING_TO_WSTRING前提是它能把asciiName里的UTF-8字节流正确解析为WSTRING。如果接收到的JSON是别的编码比如GBK这个转换就会出乱码。所以你在做项目时一定要跟软件工程师确认报文到底走的是什么编码标准最好在报文解析前先做编码探测或者固定格式约定。4.3 实战PLC内部用WSTRING拼一条设备告警假设设备有三个告警维度设备编号字符串、错误码整型、描述中文。需要拼成“设备A-1001电机过温”然后传给HMI显示。这个例子能比较全面展示WSTRING用到的主要函数组合。VAR wsDeviceID : WSTRING(32) : N设备A; nErrCode : INT : 1001; wsDesc : WSTRING(64) : N电机过温; wsErrCode : WSTRING(8); wsResult : WSTRING(128); fullLen : DINT; END_VAR wsErrCode : INT_TO_WSTRING(nErrCode); wsResult : WCONCAT(wsDeviceID, N-); wsResult : WCONCAT(wsResult, wsErrCode); wsResult : WCONCAT(wsResult, N); wsResult : WCONCAT(wsResult, wsDesc); fullLen : WLEN(wsResult);执行之后wsResult的内容是“设备A-1001电机过温”WLEN返回的字符数是9。这里有个细节值得注意中文冒号和英文冒号在WSTRING里都是1个字符但在UTF-8编码里占的字节数不一样如果后续要转成网络字节流必须按编码规则重新计算长度不能直接用WLEN的返回值乘2来当字节数。4.4 中英混排场景下的索引陷阱很多设备名称是“CH-01加热段”“AB-18冷却泵”这种中英混排格式处理这种字符串时WSTRING按字符索引的优势体现得特别明显。中文字符和英文字符在WSTRING里都占一个索引位在STRING里则占不同字节数索引完全是乱的。比如字符串“CH-01加热段”在WSTRING里取右边3个字符WMID返回的就是“热段”前面那个字符逻辑简单清晰。换成STRING你要先数编码字节再小心避开UTF-8的连续字节代码写起来又臭又长还容易错。这也是我后来新项目一律用WSTRING做业务字符串处理的核心原因。5. 高频报错与排查技巧全是现场换来的经验5.1 转换后中文全变问号现象是STRING_TO_WSTRING转换后英文字母正常中文全部变成“?”或者HMI显示成方框。这基本可以确定是源编码不是UTF-8而是GBK或ANSI。排查方法很简单把原始STRING的字节按十六进制打印出来中文如果是“正常”两个字的GBK码D5FD B3A3那毫无疑问源头是GBK。解决方案是先把GBK字节流转成UTF-8字节流再做STRING_TO_WSTRING。这个过程CODESYS没有现成函数需要写一个转换函数网上有很多GBK到UTF-8的查表转换例程移植过来即可。5.2 数组越界导致PLC进入STOPWSTRING的底层还是固定长度数组一旦写入超过声明长度的数据就可能触发内存越界轻则数据被破坏重则导致PLC进入STOP模式。我在现场就见过一次因为拼接告警时没确认目标数组大小结果把相邻变量的内存覆盖了PLC跑着跑着突然停机排查了很久才发现是字符串越界。规避手段是两条第一所有WSTRING互相转换前先比较目标容量和源长度第二在程序里加一道保护逻辑任何外部传入的字符串先用WLEN检查长度超过预设上限就直接丢弃并置一个报错标志。宁可数据丢了也不能让PLC停机。5.3 上位机显示中文正常但PLC内部比较始终不相等遇到过一个奇怪问题设备启动自检时把设备名称和预设值做比较HMI上显示中文没问题但比较结果一直是FALSE。后来发现原因是两者的中文字符串来源不同一个是用户在HMI上输入的经过HMI组态软件的编码转换后存到PLC另一个是PLC程序里的常量两者的Unicode编码完全一致但长度信息被格式化了导致比较失败。这种情况直接用等号比较不可靠最好是把两个字符串都转成BYTE数组逐个字节比较并且跳过长度头。我在代码里会用MEMCPY把两者的实际字符区拷出来再比较或者直接用SysStrUtils库里的宽字符串比较函数这类函数通常能正确处理编码头和结束符。5.4 常见问题排查速查表现象可能原因解决方案中文变问号源头编码非UTF-8确认数据源编码转成UTF-8再做转换字符串被截断目标WSTRING空间不足加大声明长度拼接前做容量检查PLC进入STOP字符串写入越界检查数组边界增加长度保护逻辑字符串比较不相等编码或格式化不一致字节级比较跳过长度头WSTRING显示OK但通信乱码通信传输层未统一编码明确协议编码封装收发转换函数IDE里中文变乱码工程文件编码不一致统一保存为UTF-8字面量加N前缀6. 面向复杂场景的进阶技巧6.1 触摸屏与WSTRING的联动细节CODESYS配套的HMI或者第三方触摸屏访问WSTRING变量时组态软件内部也要做编码适配。有些触摸屏对WSTRING支持不好你从屏上写入的中文字符串到PLC里是正常的但从PLC写到屏上就显示乱码这往往是HMI变量配置里把数据类型选成了STRING。我的做法是在HMI变量表里显式配置成WSTRING并且把字符集设定为UTF-8。如果HMI不支持直接配置就在PLC里把WSTRING先转成UTF-8编码的STRING再映射给HMI变量相当于把编码转换的活放到PLC侧做HMI只负责显示反而更稳定。6.2 通过指针直接操作WSTRING的内存块有几种极端场景需要直接操作WSTRING内存。比如要快速把一个WSTRING的所有大写字母转成小写或者批量替换某个字符用标准函数一个个处理比较慢。直接指针操作就快很多。VAR wsText : WSTRING(64) : NHELLO World 温度; pWChar : POINTER TO WCHAR; i : DINT; END_VAR pWChar : ADR(wsText); (* 跳过长度头指向第一个有效字符 *) pWChar : pWChar 2; FOR i : 0 TO 63 DO IF pWChar[i] 0 THEN EXIT; END_IF IF pWChar[i] 65 AND pWChar[i] 90 THEN pWChar[i] : pWChar[i] 32; END_IF END_FOR这段代码把wsText里所有大写英文字母转成小写中文字符不受影响因为它们的Unicode码位远大于127。关键点是ADR得到的地址是WSTRING内存起始地址前4字节是长度头必须跳过才能操作真正的字符区。这个细节我最初不知道直接从头开始遍历结果把长度头当字符处理了数据全乱了。6.3 与其他编程语言协同时的编码对标PLC不是孤岛必然要和C#、Python、Java等上位机程序协同。这些语言里的字符串处理都基于Unicode对标WSTRING很自然。C#的string本质就是UTF-16和WSTRING基本同构。Python3的str内部是Unicode码点序列转换时注意编码方法就行。我给团队定过一个编码协作规范网络传输统一用UTF-8字节流业务逻辑内部统一用WSTRING数据库存储统一用NVARCHAR。传输层和业务层之间只做一次编码转换中间绝不过度转换每多一次转换就多一次出错的机会。这个规范执行后跨语言的中文通信问题骤降。6.4 WSTRING的初始化与掉电保持别被“零初始化”坑了CODESYS中WSTRING默认零初始化意思是说声明一个WSTRING变量初始状态下内存全是0长度头的值也是0。但需要注意掉电保持区里的WSTRING上电后会保留掉电前的值如果掉电瞬间数据没写完恢复后可能是一个不完整的字符串结尾结束符缺失。冲过一次电后我发现WSTRING变量在保持区里恢复出来的字符串用WLEN计算长度时返回的值比正常情况大因为内存里残留了旧数据结束符没写对。解决办法是在上电初始化逻辑里对关键WSTRING变量做一次显式赋值比如wsData : N;确保长度头和结束符被正确重置。这个动作看起来多余但对长期运行设备的稳定性很关键。7. 一个完整的编码转换工具函数封装我建议每个项目都封装一套自己的编码工具函数不要到处裸调STRING_TO_WSTRING。下面是我在几个项目中沉淀下来的核心工具函数供你参考。它把接收的UTF-8字节流一次性转换成WSTRING同时做了长度校验从源头防止越界。FUNCTION F_UTF8BytesToWString : BOOL VAR_INPUT pBytes : POINTER TO BYTE; nByteLen : DINT; wsOut : REFERENCE TO WSTRING; END_VAR VAR tempStr : STRING(512); i : DINT; END_VAR F_UTF8BytesToWString : FALSE; IF pBytes 0 OR nByteLen 510 THEN RETURN; END_IF FOR i : 0 TO nByteLen - 1 DO tempStr[i] : BYTE_TO_CHAR(pBytes[i]); END_FOR tempStr[nByteLen] : CHAR_TO_BYTE(0); wsOut : STRING_TO_WSTRING(tempStr); F_UTF8BytesToWString : TRUE;这段代码的思路是把WSTRING的存储需求拆开先借用STRING做字节缓存再用系统函数完成UTF-8到UTF-16的转换。它的优点是兼容性好几乎所有CODESYS平台都能编译运行。缺点是STRING临时变量最长512字节更长的报文需要分块处理。实际项目里我通常把报文分帧后再调用这个函数单帧长度控制在500字节以内足够覆盖绝大多数工业场景。最后再分享一个我个人的习惯。凡是涉及中文显示的界面变量我都坚持走一遍“WSTRING存储、UTF-8传输、显示层转换”三个环节每个环节单独做一次单元测试。这个习惯看着麻烦但正是靠着它我在多个设备联调现场几乎没有因为中文乱码返过工。希望这篇东西能让你少折腾几个通宵。
返回列表