资讯详情

Solax SK-SU逆变器Modbus TCP协议实战指南:字节序、版本适配与寄存器解析

📅 2026/11/4 23:54:46 | 华诺云谱 👁 阅读
Solax SK-SU逆变器Modbus TCP协议实战指南:字节序、版本适配与寄存器解析
简介本资源是Solax SK-SU系列混合太阳能逆变器的官方Modbus TCP通信协议规范文档面向光伏系统集成商、能源监控平台开发者、嵌入式工程师及工业自动化运维人员用于实现对逆变器的远程数据读取、状态监控与指令控制。文档完整覆盖V1.0至V2.50全部版本演进详述寄存器地址如0x0000起始的设备信息区、0x0015起始的电压/频率保护阈值区、功能定义、数据格式uint16/int16、单位换算如0.1V、0.01Hz及通信参数端口502、帧格式、超时规则并记录了EPS功能、远程唤醒、BMS告警、电池健康度SOH、充电器输入能量等关键新增寄存器的迭代过程。资源为单个PDF文件大小1.58MB结构清晰、版本标注明确便于开发对接与现场调试。目前已有89人学习下载是实施Solax设备IoT接入、构建SCADA系统或编写Modbus主站程序不可或缺的技术依据。1. Solax SK-SU 逆变器 Modbus TCP 协议不是“查手册就通”的黑匣子它是一份带版本演进痕迹的现场调试契约专治监控断连、读数跳变、写入失效三类翻车现场你手头有一台 Solax SK-SU 系列混合太阳能逆变器刚接上局域网用 Modbus 调试工具比如 QModMaster 或 modbus-cli发了0x03读保持寄存器请求地址0x0000长度10结果返回0x83 0x02—— 非法数据地址再换0x04读输入寄存器地址0x0000又卡在超时或者更玄学的是明明读到了GridVoltage0x0000数值却是2765你心算276.5V感觉合理但下一秒GridCurrent0x0001返回65535换算成6553.5A显然离谱——这根本不是通信失败而是你没踩中协议里埋的三个关键地雷字节序方向、有符号/无符号混用、以及寄存器地址偏移的隐式约定。这份 V2.49 版本的 Modbus TCP 协议文档不是教科书里的理想模型而是某公司某实验室在 2014–2016 年间由 Liming、xiezhengxun、weir、wangjianxing 等工程师在真实产线、海外项目、客户投诉倒逼下一版一版打补丁、修单位、调格式、加安全配置的血泪日志。它解决的不是“能不能通”而是“通了之后数据准不准、写入稳不稳、多机并发会不会撞车”。适合谁是正在把 Solax 接入 SCADA 系统的自动化集成商是自建家庭能源看板需要稳定拉取Etoday和feedin_power的 DIY 工程师更是被BatSOH寄存器0x0023返回值忽高忽低搞到怀疑人生的技术支持一线人员。它不承诺“开箱即用”但只要你愿意按它的版本逻辑拆解寄存器表、校验字节序、绕过保留区REV、处理好int16与uint16的边界就能把一台黑盒逆变器变成可编程、可验证、可审计的能量节点。1.1 这份协议的本质不是标准是“带注释的现场操作日志”很多人第一眼看到 “Modbus TCP” 就默认它是通用协议直接套用 Modbus 标准库。错。Solax SK-SU 的这份文档本质是一份嵌入式设备固件与上位机之间的私有契约快照。你看它的历史列表2014 年 9 月 V1.0 还是草稿2014 年 10 月 V2.1 就因“移除 charger2 寄存器”而更新2015 年 3 月 V2.3 开始解释 PV 模式含义2015 年 12 月 V2.49 加入Battery SOH—— 这些不是功能迭代是现场问题倒逼的协议修正。比如VpvLowStop0x0018单位是0.1V但早期固件可能误传为1V用户反馈低压保护误触发于是 V2.2 就明确写了单位再比如ChargerStartTime1_Hours0x0092必须是0–23但若上位机传了24固件不会报错而是静默归零或溢出导致定时充电失效。所以这份文档的每一行“Unit”、“dataformat”、“W/R”都是前人踩坑后刻下的路标不是可选参数是强制约束。你跳过它去抄别人家的 Modbus 示例代码大概率会在凌晨三点收到客户告警“逆变器突然脱网RunMode寄存器读出来是65535”。1.2 为什么必须盯死 V2.49 这个版本号跨版本寄存器语义漂移是最大隐患文档末尾明确标注V2.49 Add 2016-06-03并新增了Battery SOH0x0023和Charger Input Energy0x0020/0x0021。但注意V2.49 不是终点而是分水岭。V2.48 加了Remote wake up Idle (0x90)V2.49 在此基础上修正了Etotal寄存器0x0052/0x0053的描述而 V2.50 又新增了Charger InputEnergy。这意味着如果你的逆变器固件是 V2.47你去读0x0023得到的可能是0或乱码因为该地址在 V2.47 中还是REV保留区反之若固件是 V2.50你用 V2.49 文档去读0x0020/0x0021会发现单位是0.1kWh但实际固件可能已升级为0.01kWh。这种“寄存器地址相同、语义不同”的漂移在Safety类寄存器0x001D中尤为致命V2.46 加了Manager EE Reset configurationV2.47 又加了Relay Control如果上位机未做固件版本协商直接向0x0056Relay1 State写1而固件不支持该功能轻则无响应重则触发安全锁死。因此任何基于此协议的开发第一步不是写代码而是通过FirmwareVersion_InverterMaster0x007D和FirmwareVersion_Manager0x0083两个寄存器实测读取设备真实固件版本并严格匹配文档版本号。这是所有稳定性的前提不是可选项。1.3 它能解决什么具体问题从“能连上”到“敢用于生产系统”的三道门槛很多用户卡在第一关连不上。但真正拦住项目交付的是后面两关。第一关“连接层”靠基础网络排查确认逆变器 IP 与上位机同网段、502 端口开放、防火墙放行、Modbus TCP 帧格式正确事务标识符、协议标识符、长度字段需自动生成不能硬编码。第二关“数据层”才是核心GridPower0x0002是int16值-1234表示反向送电123.4W但若你用uint16解析就变成64202W整个功率平衡计算崩盘Battery1_ChargeMaxCurrent0x0090单位是0.01A返回5000应是50.00A但若忽略小数点直接当整数用保护阈值就设错了十倍。第三关“控制层”最易翻车wExt_SetChargerPower0x00D4是wInt16正数充电、负数放电但Machine switch0x00DA是uint16MSB 是软件开关LSB 是硬件开关写0x0001只开软件写0x0002才开硬件写0x0003才全开——少一位比特指令就石沉大海。这份协议的价值就是把这三道门槛的“为什么失败”转化成“怎么验证成功”它告诉你0x0000的GridVoltage必须在1800–2700即180.0–270.0V之间才合理0x0009的RunMode值0–5对应待机、PV、市电、电池、EPS、故障六种状态超出即异常。它不教你 Modbus 原理只给你一把量尺让你亲手丈量数据是否可信。2. Modbus TCP 基础配置与帧结构解析端口、超时、字节序三个参数定生死2.1 端口与连接502 端口不是“默认”而是唯一合法入口协议明确写着port502这不是建议是硬性规定。Solax SK-SU 固件的 Modbus TCP 服务只监听 502 端口其他端口如常见的 503、8888一律拒绝连接。很多初学者用 nmap 扫描发现502/tcp open就以为通了但实际测试时仍超时原因往往出在连接建立后的保活机制缺失。Modbus TCP 本身无心跳但工业现场要求长连接稳定。常见做法是上位机在建立 TCP 连接后每 30 秒发送一个空闲请求例如读0x0000长度为 0 的寄存器或启用 TCP Keep-Alive 选项Linux 下setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, keepalive, sizeof(keepalive))。否则中间路由器或交换机的 NAT 表项超时通常 300 秒连接会被静默断开下次发请求时 TCP 层才报Connection reset by peer而 Modbus 层只显示超时。此外IP Method0x00BC寄存器决定设备是 DHCP 还是静态 IP若设为0DHCP但网络无 DHCP 服务器设备会卡在获取 IP 阶段此时MyAddress0x0084读出来是0所有后续通信必然失败。务必先用网线直连电脑用arp -a查看设备 MAC 地址再 ping 其默认网关确认物理连通性最后才发起 Modbus 连接。# Linux 下检查 TCP 连接状态替换为你的逆变器IP nc -zv 192.168.1.100 502 # 若返回 succeeded!说明端口可达若超时检查网线、IP、防火墙提示某些企业内网策略会封禁 502 端口需联系网络管理员白名单放行。不要尝试改端口——固件不支持。2.2 时间参数1 秒间隔不是性能瓶颈而是防冲突的生存法则协议规定The least interval time between two instructions 1 Sec即两次请求最小间隔 1 秒。这不是因为设备慢而是为防止寄存器读写冲突。Solax 固件的 Modbus 处理是单线程轮询若连续快速发包如 100ms 间隔后一个请求会覆盖前一个的响应缓冲区导致数据错乱。例如你同时读0x0000GridVoltage和0x0001GridCurrent若间隔小于 1 秒0x0001的响应可能携带0x0000的数据。实测中将间隔设为1200ms是更稳妥的选择留出 200ms 余量应对网络抖动。Response timeout 1 Sec同样关键若设备忙于处理 EPS 切换或自检响应可能延迟。设为1500ms更鲁棒。Character-gap time out 100ms指同一帧内字节间隔Modbus TCP 无此概念TCP 已保证字节流有序可忽略但若用 Modbus RTU 转换器此值必须大于 100ms。# Python 示例使用 pymodbus 3.6 设置合规超时 from pymodbus.client import ModbusTcpClient from pymodbus.transaction import ModbusSocketFramer client ModbusTcpClient( host192.168.1.100, port502, # 关键设置响应超时和连接超时 timeout1.5, # 响应超时 1.5 秒 retries1, # 重试 1 次避免无限等待 retry_on_emptyTrue, # 禁用自动重连由上层控制 auto_openFalse, auto_closeFalse ) client.connect() # 发送请求前确保距上次请求 1.2 秒 import time last_request_time 0 def safe_read(client, address, count, unit1): global last_request_time now time.time() if now - last_request_time 1.2: time.sleep(1.2 - (now - last_request_time)) result client.read_holding_registers(address, count, slaveunit) last_request_time time.time() return result2.3 帧格式与字节序MSB/LSB 不是概念是寄存器地址映射的物理真相Modbus TCP 帧格式包含 7 字节头 功能码 数据。头字段中Transaction Identifier2 字节由客户端自增Protocol Identifier2 字节固定0x0000Length2 字节是后续字节数含功能码Unit Identifier1 字节通常是0x01。但真正决定数据能否正确解析的是寄存器地址的字节序约定。文档中SeriesNumber0x0000–0x0006注明MSBSN[14]即最高字节存放在地址0x0000最低字节存放在0x0006。这意味着若序列号是SKSU1234567890AB14 字符其 ASCII 码为0x53 0x4B 0x53 0x55 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30那么0x0000返回0x534BSK0x0001返回0x5355SU依此类推。若你用小端序解析就会得到乱码。同样feedin_power0x0046–0x0047是Int32需合并两个uint160x0046是 LSB0x0047是 MSB组合为value (reg_0047 16) | reg_0046。文档中所有length为1的寄存器如0x0000都是uint16或int16length为2的如0x0046才是uint32/int32必须严格按此规则拼接。# 解析 Int32 类型寄存器如 feedin_power 0x0046 def parse_int32(registers): # registers 是 [reg_0046, reg_0047]即 [LSB, MSB] if len(registers) 2: raise ValueError(Need at least 2 registers for Int32) lsb registers[0] msb registers[1] # 合并为 32 位整数 value (msb 16) | lsb # 转换为有符号整数因 feedin_power 可为负 if value 0x80000000: value - 0x100000000 return value # 示例假设读到 [0x0123, 0x4567] - 0x45670123 - 1164273955 # 但实际值应为 1164273955 / 1 1164273955 W不对需查单位 # 文档写 unit: 1w所以 1164273955 W 1.16 GW显然错误 # 正确做法先确认寄存器是否真的返回了有效值再计算注意Int32类型寄存器的单位如feedin_power是1w是最终值不是每个uint16的单位。拼接后再乘单位。3. 寄存器详解与实战读写从只读监控到安全写入的完整链路3.1 只读寄存器GridVoltage、RunMode、Etoday 的精准解析与校验逻辑只读寄存器是监控系统的基石但“读出来”不等于“数据可用”。以GridVoltage0x0000为例文档写unit: 0.1V,dataformat: uint16,length: 1。实测中正常值应在1800–2700180.0–270.0V之间。若读到0可能是设备未并网若读到65535则是通信错误或寄存器未就绪。RunMode0x0009是uint16但值域是0–5对应Standby,PV,Grid,Battery,EPS,Fault。若读到6或65535说明固件异常或版本不匹配必须告警。Etoday0x0050是uint16单位0.1kwh最大值6553.5 kWh对家庭系统足够但若读到65535需检查是否当天电表清零或固件 bug。关键校验逻辑是对每个关键寄存器定义合理值域并在读取后立即验证。# 定义寄存器校验规则 REGISTER_RULES { 0x0000: {name: GridVoltage, unit: 0.1, type: uint16, min: 1800, max: 2700, desc: Grid voltage in 0.1V}, 0x0009: {name: RunMode, unit: 1, type: uint16, min: 0, max: 5, desc: Operating mode: 0Standby,1PV,2Grid,3Battery,4EPS,5Fault}, 0x0050: {name: Etoday, unit: 0.1, type: uint16, min: 0, max: 65535, desc: Energy today in 0.1kWh}, } def validate_register(address, value): rule REGISTER_RULES.get(address) if not rule: return True, No rule defined if rule[type] uint16 and (value 0 or value 65535): return False, fValue {value} out of uint16 range if value rule[min] or value rule[max]: return False, f{rule[name]} {value * rule[unit]:.1f} out of range [{rule[min]*rule[unit]:.1f}, {rule[max]*rule[unit]:.1f}] return True, OK # 使用示例 result client.read_input_registers(0x0000, 1, slave1) if not result.isError(): grid_volt_raw result.registers[0] is_valid, msg validate_register(0x0000, grid_volt_raw) if is_valid: grid_volt grid_volt_raw * 0.1 # 0.1V unit print(fGrid Voltage: {grid_volt:.1f} V) else: print(fValidation failed: {msg})3.2 读写寄存器PowerLimitsPercent、SolarChargerUseMode 的安全写入流程读写寄存器W/R是控制的核心但风险极高。PowerLimitsPercent0x0025是uint16范围0–100表示输出功率百分比。写入前必须确认1设备当前RunMode不是Fault0x0009 ! 52Machine switch0x00DA的 LSB硬件开关为13写入值在0–100内。否则写入无效或触发保护。SolarChargerUseMode0x008B控制充电模式0Self use,1ForceTimeUse,2Remote control。切换模式需先读当前值再写新值并等待1.5秒后读回确认。严禁在RunMode为EPS时写PowerLimitsPercent会导致 EPS 切换失败。# 安全写入 PowerLimitsPercent (0x0025) def safe_write_power_limit(client, limit_percent, slave1): # 步骤1检查 RunMode run_mode_result client.read_input_registers(0x0009, 1, slaveslave) if run_mode_result.isError() or run_mode_result.registers[0] 5: raise RuntimeError(Cannot write power limit in Fault mode) # 步骤2检查硬件开关 machine_result client.read_holding_registers(0x00DA, 1, slaveslave) if machine_result.isError(): raise RuntimeError(Failed to read Machine switch) machine_val machine_result.registers[0] hardware_on (machine_val 0x0001) 1 # LSB if not hardware_on: raise RuntimeError(Hardware switch is OFF, cannot write) # 步骤3校验 limit_percent if not (0 limit_percent 100): raise ValueError(Power limit must be between 0 and 100) # 步骤4写入 write_result client.write_register(0x0025, int(limit_percent), slaveslave) if write_result.isError(): raise RuntimeError(fWrite failed: {write_result}) # 步骤5等待并验证 time.sleep(1.5) verify_result client.read_holding_registers(0x0025, 1, slaveslave) if not verify_result.isError() and verify_result.registers[0] int(limit_percent): print(fPower limit set to {limit_percent}% successfully) return True else: raise RuntimeError(Verification failed after write) # 使用 safe_write_power_limit(client, 80) # 设为 80%3.3 多字节寄存器feedin_energy、Etotal 的 Int32 拼接与溢出处理feedin_energy0x0048–0x0049和Etotal0x0052–0x0053是Uint32单位0.01kwh和0.001kwh。拼接时0x0048是 LSB0x0049是 MSB。但Uint32最大值4294967295乘以0.01是42949672.95 kWh对十年运行的电站足够。然而固件可能有溢出重置逻辑。实测中若Etotal从4294967294突然跳回0说明发生了 32 位溢出。解决方案是记录上一次读取值若本次值远小于上次如差值 1e9则视为溢出累加2^32。class EnergyCounter: def __init__(self): self.last_value 0 self.overflow_count 0 def update(self, lsb, msb, unit0.001): # 拼接为 Uint32 raw_value (msb 16) | lsb # 处理溢出若 raw_value last_value 且差值巨大则累加 2^32 if raw_value self.last_value and (self.last_value - raw_value) 1000000000: self.overflow_count 1 self.last_value raw_value total_raw raw_value (self.overflow_count 32) return total_raw * unit # 使用 counter EnergyCounter() # 读取 Etotal (0x0052 LSB, 0x0053 MSB) etotal_regs client.read_input_registers(0x0052, 2, slave1) if not etotal_regs.isError(): lsb etotal_regs.registers[0] msb etotal_regs.registers[1] etotal_kwh counter.update(lsb, msb, unit0.001) print(fEtotal: {etotal_kwh:.3f} kWh)4. 常见问题与避坑指南那些让工程师凌晨三点爬起来的 5 个血泪现场4.1 现象读GridCurrent0x0001总是65535但GridVoltage0x0000正常原因GridCurrent是int16类型值65535对应十六进制0xFFFF是有符号数-1。文档中unit: 0.1A所以实际电流是-0.1A表示微弱反向电流如待机损耗。但若你用uint16解析就误判为6553.5A。解决严格按文档dataformat字段解析。0x0001是int16必须进行符号扩展。Python 中struct.unpack(h, bytes)或numpy.int16()。4.2 现象向wExt_SetChargerPower0x00D4写-500充电 5A但BatCurrent_Charge10x0015无变化原因wExt_SetChargerPower是wInt16但BatCurrent_Charge1单位是0.01A且受SolarChargerUseMode0x008B控制。若模式是0Self use该寄存器被忽略必须先设为2Remote control。解决写入前先读0x008B若非2则写2等待 1 秒再写0x00D4。4.3 现象Etoday0x0050每天 0 点归零但Etotal0x0052/0x0053不增长原因Etotal是Uint32但固件 V2.47 之前存在Etotal wrong Register问题见历史列表 V2.47 fix。若固件版本低于 V2.470x0052/0x0053实际指向其他寄存器。解决先读FirmwareVersion_Manager0x0083若 0x0247则Etotal不可用只能用Etoday累加需处理每日归零。4.4 现象Relay1 State0x0056读为1但继电器无动作原因Relay1 State仅表示软件指令状态实际动作由Relay1 Signal0x005A和Relay1 Answer0x005B控制。0x005A是触发信号0x005B是执行反馈。解决要控制继电器需写Relay1 Signal0x005A为1然后轮询0x005B直到返回1表示硬件已响应。4.5 现象批量读0x0000–0x000F0x000APowerdc1和0x000BPowerdc2返回0但光伏板有输出原因Powerdc1和Powerdc2是uint16单位1W但若直流电压低于VpvStart0x0015设定值逆变器未启动 MPPT功率为0。解决先读VpvStart0x0015再用万用表测 PV 输入电压确认是否高于该阈值。若低于检查 PV 接线或光照。5. 进阶技巧固件版本自动协商、寄存器批量读取优化与安全写入熔断机制5.1 固件版本自动协商让上位机主动适配设备能力而非硬编码 V2.49硬编码 V2.49 文档去读所有寄存器是最大风险源。正确做法是上位机启动时先读取关键版本寄存器动态构建支持的寄存器列表。FirmwareVersion_InverterMaster0x007D和FirmwareVersion_Manager0x0083各占 1 个uint16值0x0249表示 V2.49。但注意0x007D是主逆变器固件0x0083是管理器固件两者版本可能不同。应以Manager版本为准因其控制通信协议栈。构建一个版本映射表定义每个版本支持的寄存器及单位。# 固件版本映射简化版 FIRMWARE_MAP { 2.46: {supports: [0x0000, 0x0009, 0x0050], units: {0x0000: 0.1, 0x0009: 1, 0x0050: 0.1}}, 2.47: {supports: [0x0000, 0x0009, 0x0050, 0x0052], units: {0x0052: 0.001}}, 2.49: {supports: [0x0000, 0x0009, 0x0050, 0x0052, 0x0023], units: {0x0023: 1}}, # Battery SOH } def get_firmware_version(client, slave1): try: # 读 Manager Firmware Version result client.read_holding_registers(0x0083, 1, slaveslave) if result.isError(): return unknown ver_code result.registers[0] # 转换为 X.YZ 格式如 0x0249 - 2.49 major (ver_code 0xFF00) 8 minor ver_code 0x00FF return f{major}.{minor:02d} except Exception as e: return unknown # 使用 fw_ver get_firmware_version(client) print(fDetected firmware: {fw_ver}) if fw_ver in FIRMWARE_MAP: supported_regs FIRMWARE_MAP[fw_ver][supports] print(fSupported registers: {supported_regs})5.2 批量读取优化用最少请求覆盖最多关键数据规避 1 秒间隔瓶颈频繁单寄存器读取效率极低。应按功能域分组合并为最少数量的请求。例如电网参数0x0000–0x00089 个寄存器可一次读取电池参数0x0014–0x0023本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑