深圳海冈 - 敖阳 MPPT 太阳能控制器涂鸦智能 Wi-Fi MCU 控制串口协议分析

笔者最近在折腾一台深圳海冈 - 敖阳 MPPT 太阳能控制器。机器可以通过附带的 Wi-Fi 模块接入「涂鸦智能」App,查看电池状态和调整参数。虽然通过涂鸦集成也能接入 Home Assistant,但笔者还是希望进一步摆脱云端依赖,用 ESP32 和 ESPHome 直接与控制器主板通信。

要实现这个目标,首先需要弄清楚 Wi-Fi 模块和主控之间交换的是什么数据。于是,笔者拆开控制器,拆焊原本的 Wi-Fi 模块,设计了一块用于引出信号的转接板,再接入逻辑分析仪进行被动抓包。

目前已经确认:这台设备使用的是 9600 baud、8N1 的标准涂鸦 MCU 串口协议。双向通信中的心跳、DP 下发和状态上报都已通过长度及校验和验证;电池电压、电量相关的数据也有了初步对应关系。本文记录从拆机到协议解析的过程,ESPHome 替换模块的部分留待后续验证。

原机 Wi-Fi 模块

打开控制器后,可以看到主板上焊接固定了 Wi-Fi 模块,连接位置附近印有WIFI。下图中可以看到两块板之间的连接引脚及焊点。

原机Wi-Fi模块通过六路引脚直接焊接固定在主控板上

模块上带有CBU字样,标签型号为CBU-IPEX,并带有二维码和外接天线。原机能够使用涂鸦 App,是判断协议方向的一条线索。不过,仅凭模块标签还不能确定主控侧的通信格式,需要抓到实际数据才能验证。

拆焊模块并改为可插拔连接

为了在主控板与 Wi-Fi 模块之间插入自定义抓包电路板,笔者先将原本焊死的模块拆下,再在主控板上焊接 2.0mm 间距的公排针,在 Wi-Fi 模块上焊接对应的排母,改成可插拔连接。

下图中的排针、排母已经是这次改装后的状态。这样既可以把原模块直接插回主板,也可以在两者之间插入转接板,便于后续接线和反复测试。

拆焊并重焊排针、排母后,Wi-Fi模块改为可插拔连接

改装后的原Wi-Fi模块,另一侧可见CBU-IPEX标签和外接天线

制作抓包转接板

原来的连接位置比较紧凑,直接把多根测试线焊在主板上不方便反复操作。完成可插拔改装后,笔者让 GPT-6 帮忙制作了一块直通转接板,插在主板和原 Wi-Fi 模块之间:六路连接逐路直通,同时引出测试接口,供万用表、示波器和逻辑分析仪使用。

转接板通过 GPT-6 的浏览器控制能力,在嘉立创 EDA 专业版中绘制,设计尺寸约为 31×28mm。板上有主板侧和 Wi-Fi 模块侧的 2.0mm 接口、一组额外的 2.54mm 排针,以及六个独立的镀通测试孔。测试孔孔径为 1.5mm,方便接线;主板侧排母靠近板边放置,以适应原机内部的空间限制。

下面是 GPT-6 绘制转接板过程的 80 倍速录屏。

这块板子只负责引出信号,不包含电平转换和隔离电路。分析时仍然保留原 Wi-Fi 模块,让它与主控正常通信,逻辑分析仪只在旁边监听:

flowchart LR
    A[控制器主板] <--> B[直通转接板]
    B <--> C[原Wi-Fi模块]
    B -->|被动监听| D[逻辑分析仪]
    D -->|USB| E[Windows电脑]

PCB 到手后,便可以把测试线接到引出的接口上,而不必每次都重新拆焊主板。下面,就可以用测试线接逻辑分析仪来抓取主控与 Wi-Fi 模块之间的通信信号。

使用 sigrok 采集信号

本次使用的是一台由fx2lafw驱动识别的八通道 USB 逻辑分析仪,Windows 端使用sigrok-cli 0.7.2。工具通过 Chocolatey 安装,命令路径如下:

1
2
$cli = 'C:\ProgramData\chocolatey\bin\sigrok-cli.exe'
& $cli --scan

设备识别为fx2lafw:conn=1.8。需要注意,六路连接的排列需要根据实际情况确认。下文的D1D3均为本次测试时逻辑分析仪的通道名,并不是控制器排针的物理脚号。

最开始以 1MHz 采样率抓取了一秒的数据,所有通道都没有跳变。这说明通讯在该时间段内可能处于静默状态,因此需要连续启动多次采集,以增加命中间歇通信的机会:

1
2
3
4
5
6
7
8
9
10
11
$cli = 'C:\ProgramData\chocolatey\bin\sigrok-cli.exe'
1..12 | ForEach-Object {
$output = ".\logic-watch-1mhz-part$_.sr"
& $cli -d 'fx2lafw:conn=1.8' `
-c samplerate=1MHz `
-C D0,D1,D2,D3,D4,D5,D6,D7 `
--samples 3921920 -o $output
if ($LASTEXITCODE -ne 0) {
throw "Capture part $_ failed"
}
}

这次终于抓到了活动。D1 在第 3、6 段各出现 776 次跳变,第 10 段出现 104 次跳变;D3 也出现少量伴随变化。接下来便集中分析这两路。

从边沿间隔判断波特率

在 1MHz 采样率下,一个采样点对应 1μs。D1 相邻边沿的短间隔主要集中在 103~105 个采样点,即约 104μs。与常见串口波特率比较:

1
1 / 9600 ≈ 104.17 μs

因此先尝试9600 baud、8个数据位、无校验、1个停止位、非反相。下面的命令对 D1 进行十六进制解码,并保留采样点时间信息:

1
2
3
& $cli -i '.\logic-watch-1mhz-part3.sr' `
-P 'uart:rx=D1:baudrate=9600:data_bits=8:parity=none:stop_bits=1.0:format=hex' `
-A 'uart=rx-data' --protocol-decoder-samplenum

rx=D1改成rx=D3即可分析另一路。解码后出现了稳定的55 AA帧头,D1 中的许多帧进一步以55 AA 03开头,数据里还夹着CM-D20这样的 ASCII 字符串。结合原机使用涂鸦模块,协议范围就已经相当明确了。

确认涂鸦 MCU 帧格式

对照涂鸦官方串口协议文档,并逐帧核对抓包,得到以下结构:

1
55 AA | Version | Command | Length_H Length_L | Payload | Checksum
字段长度本次解析方式
帧头2 字节固定为55 AA
版本1 字节本次模块发送00,主控发送03
命令1 字节区分心跳、DP 下发、状态上报等
数据长度2 字节大端整数,仅计算 Payload 长度
数据N 字节由命令决定内容
校验和1 字节前面所有字节之和取低 8 位

完整帧长为N + 7字节。识别到帧头后,应先读出长度,再验证校验和,而不是把每次出现55 AA 03都当作新帧:另一方向使用的版本字段不同,负载里也可能恰好出现相同字节。

在 App 操作期间的另一批采集中,共保存 9 个分段文件,有效窗口合计约 31.64 秒。解析得到 79 个完整帧,其中 D3 有 27 帧,D1 有 52 帧,完整帧的校验失败数为 0,解析结果中也未出现截断帧。这说明上述格式能够解释本次捕获的数据。

两路的角色可以通过命令和应答关系确认:

分析仪通道方向观察到的命令
D3Wi-Fi 模块 → 主控 MCU0x00心跳、0x06下发 DP
D1主控 MCU → Wi-Fi 模块0x00心跳回复、0x07上报 DP

心跳与应答

一次实际捕获的心跳如下:

1
2
模块 → MCU:55 AA 00 00 00 00 FF
MCU → 模块:55 AA 03 00 00 01 01 04

第一帧数据长度为 0,没有 Payload,最后的FF就是校验和。第二帧数据长度为 1,Payload 为01,表示主控并非刚重启后的首次心跳回复,与官方定义一致。本次样本中,两帧起始时间相差约 22.109ms。

DP 数据单元

涂鸦用 DP(Data Point,功能数据点)表示设备的状态和设置。本次下发、上报帧中可以解析出以下数据单元:

1
DPID | Type | Length_H Length_L | Value

例如,抓到的一个单元为:

1
0B 02 00 04 00 00 00 8A

其中0B是十进制 DP11,02表示 value 类型,数据长度为 4,大端整数值为 138。知道它是 138,并不意味着已经知道它是什么参数:其单位、倍率、范围和读写含义还需要与 App 显示及操作对应。

基础状态上报中观察到了这些 DP。下表 DP 编号均为十进制,原始字节使用十六进制:

DP类型观察值或原始数据当前判断
7bitmap01位图,是否为故障以及每一位的含义待确认
101value0未知
102value0未知
103value0未知
104raw00 00 00 00 00 00未知
105raw00 76 00 00 01 72与电池电压、电量显示吻合
106raw00 00 00 00 00 00未知
108value27未知
118string CM-D20型号字符串,含前导空格
119string12 位 ASCII 字符串疑似设备标识,具体定义未知

电池数据的初步对应

DP105 最容易与 App 对照。在一次采集中,App 显示电池电压 11.8V、电量 37%,对应负载为:

1
00 76 00 00 01 72

前两个字节按大端整数解释:

1
2
0x0076 = 118
118 / 10 = 11.8 V

后四个字节同样按大端解释:

1
2
0x00000172 = 370
370 / 10 = 37.0 %

数值与 App 吻合,是识别电池数据的有力线索。

App 的「读取」也会下发数据

在 App 执行读取相关操作期间,D3 出现了0x06命令,D1 随后也上报了相关 DP。新增观察值如下:

DP类型观察值
11value138
12value106
15bool0、1
107bool1
109bool1
110value126
111enum0
112enum2
113value24
114value100

这一批抓包中没有看到标准的全量状态查询帧:

1
55 AA 00 08 00 00 07

实际观察到的是模块通过0x06下发一组 DP,主控用0x07上报。因此,App 界面里的「读取」并不必然对应串口侧的一条只读查询;它可能包含缓存设置同步,或者通过某个私有 DP 触发操作。具体是哪一种,目前还没有足够证据。

下一步接入 ESPHome

到这里,已经确认了传输参数、双向角色和帧格式,也找到了电池数据的候选映射。整个分析过程只使用逻辑分析仪监听,没有通过外接串口主动向控制器发送命令;原 Wi-Fi 模块仍会按其正常逻辑下发数据。

后续可以先实现电压、电量的只读展示,再逐项开放已经验证的控制参数。ESPHome 已有 Tuya MCU 组件,可以作为替换模块时的协议基础。不过,本机 DP105 使用 raw 类型,内部结构仍需要产品相关的解析逻辑。

本文的结果适用于本次实测设备及其固件。即使外观、品牌或型号相近,也应重新核对接口和 DP 定义。目前协议层已经可以解析,剩下的工作主要是把这些编号和数值逐一还原成控制器的实际功能。