
简介Samsung 无线快充私有协议扩展规范 1.0.2 版英文原版文档面向无线充电方案开发、协议逆向与电源管理方向的工程师及研究人员。文档聚焦 Qi 无线充电 Power Class 0 之上的私有功率传输扩展解决三星无线快充在包结构、功率协商与兼容性设计上缺乏权威参考的问题。压缩包内仅 1 个 PDF 文件约 847KB为 2020 年 1 月发布的完整英文电子版共 9 页正文含目录与版本发布记录。内容涵盖三星私有包PRX/PTX的 ASK 与 FSK 报文、信号调制深度要求、包收发时序与错误恢复机制、协议栈分层以及增强功率配置、自适应快充与电池充电管理等扩展条目还专章讨论与 Qi 标准的合规性及共存方式并给出硬件要求、软件架构与认证测试等落地建议。已有 185 人学习下载适合对照章节快速查阅协议细节、梳理私有快充实现路径的读者。1. 9 页私有 PD 扩展规格书藏着无线快充功率协商的那条暗线手机放上充电板界面显示无线快充功率表却停在 9V×1.67A换成原装 25W 适配器立刻就上去了。分界线通常不在线圈而在 USB-C 口上那几帧没人留意的私有报文充电板能给手机多少功率取决于它从适配器要到了多少。三星那份 9 页的 Proprietary Power Delivery Extension SpecificationVer 1.0.22020定义的就是这一段——用厂商自定义报文把无线侧的功率诉求翻译成有线侧的电压电流请求让充电板不用自己吞掉线圈耦合损耗。会读它的通常是三类人写无线充电板/移动电源固件的、做 Type-C 适配器兼容性验证的、以及抓包排查「为什么只谈到 15W」的。规格书给出了私有消息类型、SVID、VDO 位域和时序约束但不会告诉你怎么在真实链路上把它们抓下来。下面按协议结构、规格书解析、抓包复现、联调验证四步推。2. Samsung 私有 VDM 落在 PD 协议栈的哪一层2.1 PD 消息头里 5 个必须先认出来的字段私有扩展不是新协议它是 USB PD 报文里的一种载荷。任何一帧 PD 报文第一个 16 bit 字都是消息头所有上层语义都从这里分叉。把消息头拆错一位后面 VDO 全盘解错所以先把这张表刻进脑子位域名称说明B4:0Message Type消息类型0x0F 即 Vendor_DefinedB5Port Data Role数据角色DFP/UFP 方向判断B7:6Specification Revision001.0012.0103.0B8Port Power Role电源角色谁在供电B11:9Message ID消息序号请求与响应用它配对B14:12Number of Data Objects数据对象个数0 表示控制类报文B15Extended扩展消息标志一个容易踩的坑控制类报文和数据类报文共用同一个 5 bit 取值空间因此不能只看 Message Type 就下结论必须把 Number of Data Objects 一起判断。比如值相同的一帧NDO 为 0 是控制语义NDO 非 0 才是数据语义。# pd_decode.py —— 只依赖标准库把一帧 PD 消息头拆成结构化字典 SAMSUNG_SVID 0x04E8 # 三星电子的 USB-IF 厂商 ID MSG_VDM 0x0F # Vendor_Defined私有扩展的载体 def parse_header(word: int) - dict: word 为 PD 消息头 16 bit已按小端从链路上还原 return { msg_type: word 0x1F, # B4:0 消息类型 data_role: (word 5) 0x01, # B5 数据角色 spec_rev: (word 6) 0x03, # B7:6 规格版本 power_role: (word 8) 0x01, # B8 电源角色 msg_id: (word 9) 0x07, # B11:9 消息序号 ndo: (word 12) 0x07, # B14:12 数据对象个数 extended: (word 15) 0x01, # B15 扩展消息标志 }逻辑上就是把一个整数按位域切开没有额外魔法。参数层面要注意msg_id只有 3 bit回绕很快做长会话重放时不能拿它当唯一键ndo决定后面还要读几个 16 bit 字读少了会把下一帧的头部当成 VDO 解。2.2 结构化 VDM 与非结构化 VDMSamsung SVID 0x04E8 的两种用法VDM 的第一个数据对象是 VDM Header高 16 bit 固定是 SVID。Samsung 的私有扩展挂在 0x04E8 下面所以抓包筛选的第一条规则就是(vdo[0] 16) 0xFFFF 0x04E8。VDM Header 的低 16 bit 分两套解释方式取决于 B15 这个 Structured VDM 标志位域名称说明B31:16SVID厂商 ID三星为 0x04E8B15Structured VDM1结构化0厂商自由定义B14:13SVDM Version结构化 VDM 版本001.0B12:11Reserved保留B10:8Object Position0整体1~6第 N 个对象B7:6Command TypeDiscover Identity / SVIDs / ModesB5Command/Initiated请求还是响应结构化 VDM 承担的是 USB-IF 定义好的发现流程先 Discover Identity 确认对端是不是支持该 SVID 的设备再 Discover Modes 看它声明的模式列表然后进入目标模式。Samsung 的功率协商命令多半走非结构化 VDM——Structured 位置 0后面的 VDO 完全由厂商定义规格书里以命令码加参数表的形式给出。这种设计的好处是兼容性不认识的适配器收到非结构化 VDM 直接回 Not_Supported链路不会崩认识的适配器才进入私有档位。做固件时我一般会在状态机里给非结构化 VDM 单独开一个超时分支超时就退回标准 PDO别让私有协商失败拖垮整个充电流程。2.3 无线快充的协商链条从 Source_Capabilities 到私有功率请求完整链条是三段适配器广播 Source_Capabilities里面有固定 PDO也可能有 PPS 的 APDO→ 充电板发 Request 选一个起步档 → 充电板再通过私有 VDM 提出随负载变化的更高诉求适配器接受后回 Accept 和 PS_RDY。无线侧为什么必须动态调整因为线圈耦合随手机位置、壳厚、温度一直在变。手机在 Qi 侧报过来的需求折算到充电板输入端可能是 11V/2.25A也可能是 20V/2.25A。如果充电板只用固定档位吃电多出来的压差全部变成板上的热。私有扩展的价值就在于让充电板能持续把「我需要多少」说出去。一个判断依据如果抓包看到 PPS 的 Request 电压在一次充电里换了三档以上同时伴随 Samsung SVID 的 VDM这段就是在做负载跟踪如果从头到尾一个固定 PDO 不动那基本可以断定私有协商没跑起来。3. 把 9 页规格书解析成协议常量3.1 用 pdfplumber 抽位域表跨页与合并单元格的处理规格书里的关键信息几乎全在表格里命令码、VDO 位域、时序约束。手工抄一遍再敲进代码几百个位域一定抄错用 pdf 解析脚本过一遍更省事。注意别指望用普通 pdf 阅读器选中复制——位域表列宽窄复制出来的字会串行还是要走结构化抽取。import json, re import pdfplumber SVID_RE re.compile(r0x?04E8, re.I) def normalize(rows): 合并跨页被拆开的行并剔除页眉页脚 out [] for row in rows: cells [(c or ).replace(\n, ).strip() for c in row] if not any(cells): continue head cells[0].lower() if head.startswith((page, version, rev, samsung proprietary)): continue # 页眉页脚不是数据行 out.append(cells) return out with pdfplumber.open(Samsung_PD_Extension_v1.0.2_2020.pdf) as pdf: tables [] for pno, page in enumerate(pdf.pages, 1): for tbl in page.extract_tables({ vertical_strategy: lines, # 依赖表格线避免按空白切错列 horizontal_strategy: lines}): tables.append({page: pno, rows: normalize(tbl)}) json.dump(tables, open(samsung_pd_tables.json, w), ensure_asciiFalse, indent2) print(sum(len(t[rows]) for t in tables), rows /, len(tables), tables)vertical_strategy设成lines是关键规格书表格一般有完整框线按线切比按文本位置切稳得多。如果某些页是跨页长表第一页最后一行会在第二页重出表头normalize里再加一条按表头关键词过滤的规则即可。抽完一定要人工抽检两三页尤其是带 0x 前缀的十六进制列OCR 类 pdf 文档经常把 0 认成 O。3.2 从表格行生成位域解码器把位域表变成(名称, 高位, 低位, 换算)四元组解码就是一个通用的移位取掩码函数不用为每个字段写一遍。# 以 PPS 类型的 APDO 为例位域直接来自规格书表格的转录结果 APDO_PPS_FIELDS [ (apdo_type, 31, 30, enum), # 11 APDO (pps_type, 29, 28, enum), # 00 PPS (max_voltage_mv, 24, 17, x100mV), (min_voltage_mv, 16, 8, x100mV), (max_current_ma, 7, 0, x50mA), ] def extract(vdo: int, fields) - dict: res {} for name, hi, lo, unit in fields: width hi - lo 1 res[name] (vdo lo) ((1 width) - 1) res[name _unit] unit return res # 21V/3A 的 PPS 档位按上表拼出来的原始 VDO print(extract(0xC0DC212D, APDO_PPS_FIELDS))跑出来的max_voltage_mv是 110、min_voltage_mv是 33、max_current_ma是 45乘上单位就是 11V、3.3V、2.25A。这里width必须用hi - lo 1写成hi - lo是最常见的差一位错误症状是最大值永远差一半。3.3 常量白名单与自检SVID、MessageType、命令码解析结果落地成代码常量前先做一遍自检把明显不可能的值挡在编译期之外常量合法范围越界说明SVID0x04E8 或已知厂商抽表把 O 认成 0 会得到假 SVIDMessage Type0x00 ~ 0x1F超过 5 bit 说明位宽抽错Number of Data Objects0 ~ 7规格书表格里的范围列要单独校验电压位域下限 上限抽反了高低位就是这种表现自检脚本不需要复杂把上表写成断言每次重新抽表后跑一遍比人眼比对可靠得多。规格书是 2020 年的 1.0.2 版本后续版本增删命令码时这套断言还能当变更检测用。4. 抓一次真实的私有 PD 会话4.1 硬件链路分析仪、TCPC 旁路和线缆 e-marker抓 CC 线上的 PD 报文有三条路选哪条取决于你要什么方案成本能看到什么局限商用 PD 协议分析仪高完整报文、时序、GoodCRC私有 VDM 只给原始 hexTCPC 旁路FUSB302 等低原始报文与 SOP 类型需要自己写解码逻辑分析仪抓 CC低物理层波形BMC 解码工作量大常见做法是分析仪负责抓脚本负责把私有 VDM 那几帧转成人类可读的功率请求。旁路方案要注意两点一是 e-marker 线缆5A 线才有 e-marker用普通线缆抓 20V/2.25A 档会看到链路直接降档误判成协议问题二是旁路点必须放在充电板输入端放在适配器输出端只能看到适配器视角看不到充电板发出的私有请求。4.2 在树莓派上用 I2C 读 FUSB302 FIFOFUSB302 是常见的低成本 PD PHYI2C 地址 0x22收到报文后 FIFO 里会有一个 token 字节高 3 位标识 SOP 类型低 5 位是后续有效字节数。import smbus2, time from pd_decode import parse_header, MSG_VDM, SAMSUNG_SVID ADDR 0x22 REG_STATUS1 0x41 # 含 RX_EMPTY 位 REG_FIFO 0x43 # 写TX FIFO读RX FIFO bus smbus2.SMBus(1) def wait_for_rx(timeout0.05): 轮询 RX_EMPTY等 FIFO 里出现一帧 t0 time.time() while time.time() - t0 timeout: if not (bus.read_byte_data(ADDR, REG_STATUS1) 0x20): return True return False def read_frame(): tok bus.read_byte_data(ADDR, REG_FIFO) sop, n (tok 5) 0x07, tok 0x1F # SOP 类型 字节数 return sop, [bus.read_byte_data(ADDR, REG_FIFO) for _ in range(n)] def to_words(payload): PD 报文小端每两字节拼一个 16 bit 字首字为消息头 return [payload[i] | (payload[i 1] 8) for i in range(0, len(payload) - 1, 2)] while True: if wait_for_rx(): sop, payload read_frame() words to_words(payload) if not words: continue hdr parse_header(words[0]) print(fSOP{sop} type0x{hdr[msg_type]:02X} fid{hdr[msg_id]} ndo{hdr[ndo]} rev{hdr[spec_rev]}) if hdr[msg_type] MSG_VDM and hdr[ndo] 1: svid (words[1] 16) 0xFFFF print( VDM, hex(words[1]), SVID, hex(svid), PRIVATE if svid SAMSUNG_SVID else )寄存器地址和中断位定义随芯片版本有变化动手前拿数据手册核一遍。wait_for_rx用轮询而不是等中断是为了少接一根线如果要抓长会话且不想丢帧改成接 INT 引脚触发读取更稳。控制类报文 NDO 为 0不会进入 VDM 分支这也是前面强调位宽的原因。4.3 解码输出对不上时先看这四件事第一SOP 类型搞混。同一根线上有 SOP、SOP、SOPSOP 和 SOP 是给线缆内 e-marker 用的把它们当成主链路报文会看到一堆莫名其妙的 SVID。第二字节序反了。PD 是小端payload[i] | (payload[i1] 8)的顺序写反SVID 会变成 0xE804 这类不存在的值。第三只抓到一半。日志里私有 VDM 有去无回先怀疑 GoodCRC 没被分析仪计入再怀疑抓包点被充电板内部的 PD 控制器挡住了。第四把 User 自定义段的保留位当成有效数据。规格书里写 Reserved 的位必须按原值回填解出来当参数传下去会让对端回 Reject。5. 联调与回归功率档位映射、热降额与私有命令的边界验证把协议跑通只是第一步真正花时间的是档位映射和边界。下面这张表是我做兼容性验证时常用的对照私有一侧的具体命令码以你手上那份规格书为准无线侧目标充电板输入常见取值PD 侧动作观察点Qi EPP 15W12V / 1.67A使用固定 PDO线圈整流电压是否稳定25W 档11V / 2.25APPS 上调电压电流是否被适配器压回45W 档20V / 2.25A固定 20V PDO线缆需带 e-marker温升降额9V / 1.67APPS 下调 重发私有请求降额后是否刷新请求降额这一行最容易出问题很多固件在温度触发降额时只改了本地的占空比没有重新下发私有请求结果适配器还在按 11V 供多余功率全压在充电板 LDO 上越降越热。正确做法是先改 PD 档位再调无线侧目标值。回归验证不建议每次人肉盯着功率表把历史抓包转成 JSON用脚本离线回放更快import json from pd_decode import parse_header, MSG_VDM, SAMSUNG_SVID def check_session(path): events json.load(open(path)) prev_mw, errs None, [] for e in events: hdr parse_header(e[header]) if hdr[msg_type] ! MSG_VDM or hdr[ndo] 1: continue if ((e[vdo][0] 16) 0xFFFF) ! SAMSUNG_SVID: continue mw e.get(requested_mw) if prev_mw and mw and mw prev_mw * 2: errs.append((e[ts], prev_mw, mw)) # 单次跳变超过 2 倍 prev_mw mw or prev_mw return errs print(check_session(session_25w_thermal.json))判据是「单次功率跳变超过 2 倍」正常负载跟踪是几十到几百毫瓦的连续调整出现倍数级跳变基本能确定私有请求和实际 PPS 档位没对齐——要么是充电板算错了要么是适配器没接受还硬撑。把这段脚本挂进 CI每次改完固件跑一遍历史抓包比重新插拔一百次手机快得多。唯一要注意的是时间戳必须来自同一时钟源混用分析仪和树莓派的时间会让排序失真误报成跳变。本文还有配套的精品资源点击获取