基于DSView的快充协议解码器设计与实现——解析FCP/SCP/AFC信号

基于DSView的快充协议解码器设计与实现——解析FCP/SCP/AFC信号 简介DSView平台专用快充协议解码插件面向DSView/DSLogic逻辑分析仪用户覆盖华为FCP、三星SCP、OPPO/AFC等基于相同物理层与握手机制的私有快充协议可实时解析USB PD协商之外的厂商定制快充通信数据流适用于快充芯片验证、充电器兼容性测试及Type-C接口协议逆向分析等场景。压缩包共6个文件以Python编写的pd.py解码器与__init__.py初始化模块为核心另含预编译字节码、配置说明等整包仅6KB适配Python 3.6运行环境轻量易集成。解码器通过DSView标准插件接口加载支持波形触发、字段高亮、时序标注与CSV导出通过直观的高亮与时序视图可快速识别协议请求、应答与状态字段显著提升排障效率。目前已有77人浏览学习适合具备Python基础和逻辑分析仪使用经验的嵌入式开发、电源工程师及协议分析爱好者。 搞硬件调试这些年我手里最常用的工具除了万用表就是逻辑分析仪。前阵子朋友拿了一台快充充电器过来说手机偶尔能触发快充、偶尔又掉回普通5V充电让我帮忙看看是不是协议握手有问题。我接上DSView抓了一段USB D/D-上的波形电平翻转倒是拍得清清楚楚可盯着那些方波看了半天完全看不出充电器回了什么、手机又请求了什么。市面上现成的解码器基本都是UART、I2C、SPI这一类通用协议针对FCP/SCP/AFC这些快充协议的现成解码工具少得可怜。没办法我只能自己动手基于DSView平台写了一套快充协议解码工具专门把D/D-上的协商报文翻译成人能看懂的内容。这篇文章会把整套工具的完整思路、协议分析过程、核心代码实现和实测中踩过的坑都记录下来。内容主要面向硬件工程师、嵌入式开发者和对快充协议感兴趣的DIY玩家已经熟悉DSView基本操作的人可以直接跳到第3节看解码器实现。1. 项目背景抓得到波形看不懂内容1.1 为什么大家需要一套快充协议解码工具快充协商的本质是手机和充电器通过USB的D/D-两根线做带外通信。普通充电线里的D/D-在纯充电场景下不传输数据正好被快充协议拿来当天线用充电器插入的瞬间设备端会通过D/D-上的电平变化或数据帧告诉充电器“我需要更高的电压”或者“我可以承受更大的电流”。问题在于逻辑分析仪抓下来的原始波形只是一串0和1的电平序列。你用肉眼能看出这里有个脉宽、那里有个毛刺但看不出这串波形对应的语义。比如华为FCP/SCP这类走UART风格通信的协议波形的字节序、校验方式、包结构都是半公开的没有对应解码器时分析一次握手过程要手动把波形一位位抠出来再拼成字节效率极低。三星AFC则是另一套思路它不靠数据帧通信主要靠D/D-上的电平组合来触发电压切换分析这种波形需要的是状态识别不是字节解析。所以我做这类解码工具的目标很明确把DSView采集到的D/D-原始信号实时转换成协议报文列表在波形窗口上直接标注出“请求9V”“回执成功”“电压切换”这类事件让分析快充协商过程从“看波形猜含义”变成“直接读报文”。配合DSView自带的波形缩放和搜索功能排查握手失败会快非常多。1.2 FCP/SCP/AFC三种协议的核心特征做解码器之前得先把三个协议的特点理清楚。华为FCPFast Charge Protocol是比较早的快充方案它通过在D/D-上建立一套类似UART的通信链路协商输出电压等级。SCPSuper Charge Protocol是在FCP基础上演进出的超级快充协议通信方式类似但协商内容从电压档位扩展到了更精细的电流控制。这两个协议在D/D-上的信号形式高度相似都是一帧一帧的二进制数据所以解码器可以把它们的帧解析合并成同一套逻辑只在包结构解析时做区分。三星AFCAdaptive Fast Charging跟上面两个协议完全不同。它不是通过连续的数据帧来通信而是靠D/D-上的电平组合来配置充电器输出。充电器检测到特定的电平状态后直接把输出从5V切到9V或者切回来。这更像是“硬件状态机”而不是“通信协议”。给AFC写解码器核心不是解析字节流而是识别电平状态跳变并把跳变标注成“进入AFC请求”“退出AFC回落到5V”这类事件。我最初也试图按UART的思路去解AFC结果波形完全对不上后来调整了思路才对。还有一个共同点值得注意这三种协议的协商过程都发生在充电器插入后的几百毫秒内一次握手可能只有几十到几百个字节。而且D/D-上的信号幅度本身比较小逻辑分析仪采样到的波形经常混着噪声。这对解码器的触发设计和噪声容错提出了要求后面第4节我会专门讲怎么处理。2. 解码器整体设计从波形到可读报文的四层转换2.1 工具架构为什么选DSView加Python扩展解码工具不是独立软件而是做成DSView的协议解码器插件。DSView本身是DSLogic逻辑分析仪的配套软件支持最多16通道采样底层有一套协议解码框架允许用户用Python编写自定义解码器。解码器被放到指定目录后DSView启动时会自动加载在通道设置界面里就能像选UART一样选到自定义协议。我选择这个方案而不是写一个独立的桌面程序有几个现实原因。首先是省时间DSView已经处理了采样、波形绘制、缩放、导出一整套繁琐工作我只需要关注“怎么把电平变成协议报文”这一件事。其次是跨平台DSView在Windows、Linux、macOS上都能跑解码器只要写一遍到哪都能用。第三是数据分析环境好解码结果能直接叠加在波形上拖动时间轴就能看某一帧对应的原始电平排查问题非常直观。整个解码工具的数据流大致是四层第一层是原始采样DSView把D/D-的电平按采样率转换成逻辑0/1序列第二层是位同步解码器在电平序列中识别起始位、数据位和停止位把物理电平还原成字节第三层是报文组装解码器根据协议格式把连续字节拼成完整的报文帧第四层是语义解析根据协议状态机把报文翻译成“请求电压”“协商成功”这类事件以注释形式标注在DSView波形上。2.2 FCP/SCP的UART式信号解码思路FCP/SCP在D/D-上的通信波形特征和UART非常接近一个起始位接着8个左右数据位带停止位低电平有效。但有一点和标准UART不一样标准UART是单根线收发而快充协议的通信链路里D/D-两根线都参与了电平配置设备端和充电器端的信号是交替出现的。所以解码器不能只盯着一个通道要把两个通道都纳入分析范围。我的具体做法是把D和D-分别作为两个逻辑通道采进来。在解码时先按“哪一个通道出现下降沿”来判定当前是哪个方向的数据传输然后用该通道的后续采样点去恢复字节序列。这里有个取舍完全按UART的波特率去采样实现简单但如果充电头用的通信速率和预设不一致就会解码失败。为了兼容性我的解码器支持两种模式手动指定波特率以及自动估算波特率。自动估算的原理是测量起始位低电平的时长再根据该时长反推每一位的时间宽度。实测下来自动估算在绝大多数场景下都能得到准确结果。字节重组完成之后就是报文帧解析。FCP/SCP这类私有协议不会把帧格式完整公开但它们的报文基本都遵循“包头长度数据校验”的结构。解码器先把连续字节缓冲起来通过包头特征判断属于FCP还是SCP再按对应协议的长度字段截取完整报文最后做一次校验计算。即使校验算法不完全匹配解码器也会把原始字节保留下来这样拿到错误校验结果时也能看到完整的通信内容。2.3 AFC的电平状态解码思路AFC的解码思路跟FCP/SCP完全不一样刚开始我很不适应。AFC不产生连续的数据帧它的通信过程更像“充电器检测到D/D-处于某个电平组合然后切换输出电压”。比如设备想请求9V输出就会在D/D-上呈现一种特定的电平组合充电器检测到之后拉高输出电压设备想退出快充又会切换另一种组合充电器看到后回落到5V。对这种协议解码器的任务不再是“解析字节”而是“跟踪电平状态”。我把两个通道的电平组合看成一个状态机的输入当D/D-从空闲状态跳变到高电平组合时输出一条“AFC请求高电压”的事件当电平组合回落到普通状态时输出“退出AFC”的事件。因为AFC协商发生得很快电平维持时间可能只有几十毫秒解码器必须一直监控两个通道的电平变化不能依赖起始位触发。实现上我用了一个简单的状态变量每次通道电平变化都重新评估当前状态并在状态发生切换时输出标注。这样处理下来AFC的协商过程在DSView里看起来就是一串清晰的事件时间线。3. 核心实操从零写一个DSView协议解码器3.1 解码器目录结构与基本框架DSView的协议解码器本质上是一个Python包放在解码器目录下的独立文件夹里。目录结构大概是这样decoders/ └── fcp_scp_afc/ ├── __init__.py └── ...__init__.py里定义一个继承自解码框架基类的Decoder类并通过类属性声明协议信息、通道和注解类型。以我实际使用的写法为例代码骨架大致如下import sigrokdecode as srd class Decoder(srd.Decoder): api_version 3 id fcp_scp_afc name FCP/SCP/AFC longname Fast Charge Protocol Decoder desc Decode FCP/SCP/AFC fast charge signals on USB D/D- license gplv2 inputs [logic] outputs [fcp_scp_afc] tags [USB, Charger, Fast charge] channels ( {id: dp, name: D, desc: USB D line}, {id: dm, name: D-, desc: USB D- line}, ) annotations ( (bit, Bit), (byte, Byte), (packet, Packet), (warning, Warning), ) annotation_rows ( (bits, Bits, (0,)), (bytes, Bytes, (1,)), (packets, Packets, (2,)), (warnings, Warnings, (3,)), )这里有几个关键点。channels里声明了两个通道分别对应D和D-采样时DSView会提示你把这些通道绑到实际的逻辑分析仪通道上。annotations声明了解码器能在波形上标注的不同内容类型bit是最原始的电平标注byte是重组后的字节packet是解析完成的报文warning用来标出校验错误等异常。这样的分层设计让我在调试时可以先看byte对不对再看packet对不对定位问题非常方便。不同版本的DSView对解码器API的支持会有细微差别。如果你的DSView版本较老APIv3的某些字段可能不支持最稳妥的办法是参考DSView自带解码器的写法复制一个现有解码器的骨架再改。我第一次写的时候直接照搬了UART解码器的模式省了不少事。3.2 位同步与字节重组核心代码实现解码器的核心逻辑在decode()方法里。DSView会持续调用这个方法并把采集到的逻辑采样数据送进来。整个解码过程是一个循环循环体里等待通道状态变化然后根据变化做处理。简化后的位同步和字节重组逻辑大概是这样的def decode(self): self.state IDLE while True: # 等待D或D-出现电平变化 (dp, dm) self.wait({0: e, 1: e}) if self.state IDLE: # 检测到下降沿认为是起始位进入字节收集 if dp 0 or dm 0: self.state RECV self.bit_pos 0 self.current_byte 0 # 进入起始位后的位采样循环 continue elif self.state RECV: # 按位时钟采样每周期采样一次中间位置 self.bit_pos 1 if 1 self.bit_pos 8: # 数据位低位在前 self.current_byte 1 if dp 1 or dm 1: self.current_byte | 0x80 elif self.bit_pos 9: # 停止位整理一个完整的字节 self.put(self.samplenum, self.samplenum, self.out_ann, [1, 0x%02X % self.current_byte]) self.bytebuf.append(self.current_byte) self.state IDLE这段代码的核心是状态切换和位采样。检测到下降沿之后进入RECV状态每来一个采样周期就把当前位写入字节缓冲区。因为D/D-上的信号是双向交替的我用dp和dm两个通道的取值共同决定当前位是1还是0实际使用中不同协议可能让D和D-分别承担不同含义所以这一块逻辑要根据实测波形微调。我最初写死只用D通道判断数据位结果SCP协议解析错误率很高后来改成双通道联合判断才解决。这里必须强调一个采样周期和波特率的对应关系。DSView送到解码器里的采样点带时间戳samplenum要准确地在一个bit中间位置采样必须知道当前波特率。手动设置波特率时用采样率除以波特率就能得到每个bit的采样点数然后在起始位之后偏移半个bit开始采样。自动估算波特率时起始位低电平的时长除以预估的bit宽度就是波特率。我建议调试阶段先用手动方式把波特率固定下来等报文能正确解析了再开自动估算不然一个问题里混着两个变量排查起来很痛苦。3.3 报文解析状态机组装出有意义的报文字节重组完成之后原始字节都攒在bytebuf缓冲区里接下来要做的是报文解析。我用了一个简单的状态机def parse_packet(self): # 简化逻辑示意报文解析流程 while len(self.bytebuf) 4: # 查找包头 if self.bytebuf[0] FCP_HEADER: pkt_len self.bytebuf[1] if len(self.bytebuf) pkt_len 2: pkt self.bytebuf[:pkt_len 2] # 校验 if self.check_crc(pkt): self.put(self.samplenum, self.samplenum, self.out_ann, [2, FCP packet: %s % pkt.hex()]) else: self.put(self.samplenum, self.samplenum, self.out_ann, [3, CRC ERROR]) del self.bytebuf[:pkt_len 2] continue # 没找到包头丢弃一个字节 self.bytebuf.pop(0)状态机的关键设计是容错。因为D/D-上的信号在开始阶段可能不稳定字节缓冲区里会有一些噪声字节所以解析器不能假设第一个字节就是包头而是不断扫描缓冲区找到符合包头特征的字节才开始拼帧。为了应对校验算法不匹配的情况我把原始报文用十六进制同时输出出来即使解析器不认识内容使用者也能看到完整的数据这个设计在实际排查中帮了我大忙。FCP和SCP的包头特征不同解析器会根据第一个字节的值判断该走哪套解析逻辑。AFC不走这个状态机它单独走状态跟踪逻辑也就是上一节说的电平状态机。因为两种协议的解析逻辑差异很大我把它们写成了两个独立的处理分支在decode()主循环里根据当前解析模式调用。这里有个经验不要把FCP/SCP的字节解析逻辑和AFC的状态跟踪逻辑混在一个函数里后面维护会非常痛苦。3.4 在DSView中加载解码器并验证结果解码器写完之后把它放到DSView的解码器目录下重启软件就能在协议选择列表里看到“FCP/SCP/AFC”。使用时先把逻辑分析仪的CH0接到USB线的D测试点CH1接到D-测试点GND接GND然后打开DSView开始采样。建议采样率设在2M Sa/s或更高预算充足的话直接上5M Sa/s这样每个bit至少能采到几十个点解码容错率会高很多。接线是最容易翻车的地方。D/D-在USB线内部必须把线皮剥开找到对应的测试点或者用USB调试板引出。我一开始拿杜邦线直接怼在USB座上乱戳波形全是毛刺根本没法解。后来老老实实焊了一个测试点再并上逻辑分析仪的探头波形才干净起来。采样完成后在DSView里添加解码器分配通道调整波特率波形窗口就会立刻显示解析出来的字节和报文。我第一次跑通时看到“FCP packet”整整齐齐列出来那种感觉确实很爽——至少在那一刻之前手工扒位的日子算是过去了。验证解码器是否正确的标准不能只看“有没有输出”要看“输出对不对”。我建议用一台协议已知的充电器和一台手机做基准测试手机正常触发快充时解码器应该能看到完整的电压请求报文和握手成功事件。如果只有零散的字节没有完整报文或者报文全是CRC错误那就要回到位同步和波特率配置上排查。4. 实测中的问题与排查记录4.1 采样率不足导致乱码刚写好的时候我拿一个FCP充电器做实测解码器输出的字节乱七八糟报文完全拼不出来。我第一反应是协议格式写错了排查了很久才发现是采样率设得太低。当时用了500k Sa/s的采样率去抓波形一个bit只有四五个采样点起始位位置判断稍微偏一点后面的数据位就全部错位。后来我算了一笔账FCP/SCP这类通信的波特率通常在几十到几百kbps量级就算按115200bps算一个bit的时长大约是8.68微秒。采样率至少要有10倍于信号频率也就是大概2M Sa/s以上才能保证每个bit有足够的采样点来定位起始位和数据位中间位置。把采样率提到5M Sa/s之后乱码问题立刻消失。这个经验也让我养成了一个习惯解码任何未知协议前先用高采样率抓一遍波形再根据实际波形宽度调整采样率而不是一开始就为了省存储空间压低采样率。4.2 噪声导致误触发和额外字节D/D-上的信号幅度本来就不大如果接地处理不好波形上会有明显毛刺。这些毛刺在解码器看来就是额外的高低电平变化可能触发误判的起始位也可能在字节中间多出一个“假数据位”。我遇到的典型问题是报文主体解析是对的但每个报文前面都多出一个0x00字节导致包头识别失败。排查下来问题出在逻辑分析仪的地线太长形成了天线效应。把地线换成短弹簧地线之后波形毛刺明显减少。代码层面我也加了一道保险检测到起始位后如果发现起始位持续的时间比预设波特率对应的bit宽度短太多就判定为毛刺并丢弃不进入字节收集状态。这个去抖逻辑简单有效尤其是采样率较高的时候可以让解码器忽略那些只有一两个采样点的窄脉冲。4.3 私有协议变种的兼容性问题快充协议虽然没有对外完全公开但不同品牌、不同批次的充电头在实现上会有细微差别。我遇到过同一个品牌的两款充电器一个能正常解析另一个报文头总是差一个字节后来对比原始波形才发现是字节序不同。这种问题在解码器里很难完全规避我采取的做法是解码器保留原始字节流输出当报文解析失败时把原始字节以十六进制列表展示出来方便使用者手动确认同时在解码器设置里提供“字节序”和“校验方式”两个可配置项遇到异常协议变种时可以手动切换。靠这个思路后续再遇到兼容性问题基本不需要改代码调整配置就能适配。这也算是个设计上的取舍与其猜一个万能的协议格式不如把不确定的部分暴露给使用者自己判断。解码器不是万能的能提供清晰的原始数据就已经解决了大部分实际问题。5. 写在最后的实操体会这套解码工具做下来我最大的体会是写协议解码器七分靠波形分析三分靠写代码。真正花时间的地方不是Python语法的实现而是对着逻辑分析仪的波形一段一段确认起始位位置、bit宽度、字节顺序和报文边界。如果你也想给自己的项目写类似的自定义协议解码器我建议先从最简单的手动波特率配置开始跑通一条完整链路之后再考虑自动波特率、容错这些进阶功能。还有一个小技巧DSView支持把解码结果导出成带时间戳的CSV配合脚本做批量数据分析效率很高分析一次完整充电过程的协商日志会非常方便。本文还有配套的精品资源点击获取