从 PLC 读取 STRING / WSTRING:UCS-2、GBK 与 Python 实战
1. PLC 中 STRING / WSTRING 的内存布局
以西门子 S7 的标准 DB 字符串布局为例,STRING 与 WSTRING 的头部不同,不能只通过“每个字符乘 2”共用同一个读取函数。使用 Snap7 直接读取 DB 时,还要按工程要求设置可访问的 DB 和读写权限。
STRING
| 字节偏移 | 内容 |
|---|---|
| 0 | 最大长度,1 字节 |
| 1 | 当前数据长度,1 字节 |
| 2 起 | 字符数据,最大长度个字节 |
标准 STRING 使用单字节字符单元,STRING[20] 占 22 字节。若工程把 GBK 字节写进这个区域,读取边界按实际数据字节数确定;不能把 Python 解码后的汉字个数直接写入长度字节。
WSTRING
| 字节偏移 | 内容 |
|---|---|
| 0~1 | 最大长度,2 字节的大端 WORD |
| 2~3 | 当前长度,2 字节的大端 WORD |
| 4 起 | 每个字符单元 2 字节,按大端序读取 |
WSTRING[20] 占 4 + 20 × 2 = 44 字节。S7 的 WSTRING 字符数据按 UTF-16BE 解码,不能使用两个单字节头部,也不能默认使用 UTF-16LE。
2. UCS-2、UTF-16 与 GBK 的区别
- UCS-2 使用两字节字符单元,覆盖基本多文种平面;UTF-16 与它并非完全相同,UTF-16 还可用代理对表示补充平面字符。PLC 实际允许的字符范围要以型号和工程文档为准。
- UTF-16BE / UTF-16LE 的区别是字节顺序。Windows 上某些文件使用小端序,不代表 S7 的 DB 数据也使用小端序。
- GBK 中英文通常占 1 字节,中文通常占 2 字节。它是数据区的编码约定,与 STRING 的长度头部是两件事。
这里按工程变量类型选择解码函数,再按工程约定选择编码。仅凭“能解码、不报错”无法可靠判断编码,尤其不应读到字符串边界之外再猜测。
3. 如何确认 PLC 存储格式
- 在 TIA Portal 中确认变量是
STRING[n]、WSTRING[n],还是自行打包的字节数组。 - 确认 DB 号、偏移、声明长度以及 DB 的访问方式。
- 写入已知的英文或中文样本,检查读取到的原始十六进制字节。
- 如果工程使用自定义 GBK 或 UTF-16LE 字节数组,就按该数组的协议解析,不套用标准 WSTRING 的头部。
4. Python 实战:按类型读取与解码
下面两个函数只解析已读到的字节,不连接或写入 PLC:
def decode_s7_string(data: bytes, byte_offset: int = 0, encoding: str = 'ascii') -> str:
"""解析标准 S7 STRING;自定义 GBK 数据区可显式传 encoding='gbk'。"""
if byte_offset < 0 or byte_offset + 2 > len(data):
raise ValueError('STRING 头部不完整或偏移无效')
max_len = data[byte_offset]
actual_len = data[byte_offset + 1]
if max_len > 254 or actual_len > max_len:
raise ValueError('STRING 长度头部无效')
start = byte_offset + 2
end = start + actual_len
if end > len(data):
raise ValueError('读取的数据不足以覆盖 STRING 实际内容')
return data[start:end].decode(encoding, errors='strict')
def decode_s7_wstring(data: bytes, byte_offset: int = 0) -> str:
"""解析标准 S7 WSTRING:四字节头部,UTF-16BE 字符数据。"""
if byte_offset < 0 or byte_offset + 4 > len(data):
raise ValueError('WSTRING 头部不完整或偏移无效')
max_len = int.from_bytes(data[byte_offset:byte_offset + 2], 'big')
actual_len = int.from_bytes(data[byte_offset + 2:byte_offset + 4], 'big')
if max_len > 16382 or actual_len > max_len:
raise ValueError('WSTRING 长度头部无效')
start = byte_offset + 4
end = start + actual_len * 2
if end > len(data):
raise ValueError('读取的数据不足以覆盖 WSTRING 实际内容')
return data[start:end].decode('utf-16-be', errors='strict')
使用示例
# WSTRING[20],当前内容有三个字符单元:你好!
sample_wstring = (20).to_bytes(2, 'big') + (3).to_bytes(2, 'big') + '你好!'.encode('utf-16-be')
print(decode_s7_wstring(sample_wstring)) # 你好!
# STRING 数据区显式采用 GBK;长度字节记录实际字节数
payload = '中文'.encode('gbk')
sample_string = bytes([20, len(payload)]) + payload
print(decode_s7_string(sample_string, encoding='gbk')) # 中文
5. 与 Snap7、OPC UA 的交互
假设工程的 DB1 偏移 0 处声明了 WSTRING[20]:
import snap7
client = snap7.client.Client()
try:
client.connect('192.168.0.10', 0, 1) # rack/slot 按实际设备配置
raw = client.db_read(1, 0, 44)
print(decode_s7_wstring(raw))
finally:
client.disconnect()
也可以使用已安装版本提供的 snap7.util.get_string、get_wstring。不同版本的函数参数可能不同,先核对版本文档。通过 OPC UA 读取 String 节点时,客户端通常已经返回 Python str,不应再按 DB 的 STRING 头部解析。
6. 常见陷阱与调试方法
- 头部混用:STRING 是 2 字节头部,WSTRING 是 4 字节头部。
- 字节序错误:标准 S7 WSTRING 使用大端序,不能依据 Windows 的文件编码判断 PLC 的字节序。
- 长度理解错误:STRING 的数据区按字节取边界;WSTRING 的字符单元数乘 2 后得到数据字节数。
- 偏移或读取长度错误:先按工程声明计算总大小,再读取对应区域,不把后面的变量当作字符串补齐。
- 静默吞错:
errors='ignore'、自动尝试多个编码或截断至缓冲区末尾,可能掩盖错误。调试时保留原始字节并明确报出长度或编码错误。
7. 性能与边界检查
频繁读取时可以合并相邻 DB 区域,再按各变量的偏移解析。批量读取仍要检查每个变量的头部与边界,不能为了减少请求而忽略字段布局。
评论将在接近此处时加载,也可点击立即加载。