news 2026/10/18 18:30:34

FUSB302 PD协议代码示例:从CC检测到PDO请求的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FUSB302 PD协议代码示例:从CC检测到PDO请求的完整实现

简介:这是一份面向嵌入式开发者的FUSB302 USB PD协议实现参考代码,适合正在开发Type-C快充功能、需要快速验证PD协商流程的工程师。资源围绕PD1.0与PD2.0协议展开,涵盖器件初始化、CC引脚状态检测、FIFO缓冲区读写、消息ID管理以及电压电流请求处理等核心环节,并演示了通过按键调整PPS模式输出电压,支持1V、100mV与20mV级别的精细调节,便于理解源能力消息、请求消息与接受消息之间的交互逻辑。压缩包共2个文件,以inscode工程配置与html页面为主,整体约3KB,结构轻量,便于直接导入或对照阅读。目前已有293人学习下载。对于希望降低PD协议集成复杂度、快速搭建可运行验证环境的开发者而言,这份源码可作为理解物理层与链路层通信流程的实践起点。

1. 从一块 FUSB302 说起:PD 协议协商到底在 MCU 里跑什么

如果你手上有块带 Type-C 口的小板子,插上充电器却只能跑 5V,大概率不是硬件焊错了,而是 PD 协议协商没跑起来。FUSB302 这颗芯片在嵌入式圈子里出镜率很高——它把 Type-C 的 CC 引脚检测、BMC 编解码、PD 报文收发这些脏活揽下来,MCU 只需要通过 I2C 读写寄存器就能完成一轮 Power Delivery 协商。但真到动手写代码,很多人会卡在「寄存器写进去了、中断也来了、PDO 却读不出来」这种玄学状态。这份 FUSB302 PD 协议代码示例就是冲着这个场景来的:它是一套可运行的源码,把 CC 检测、PD 报文收发、Source/Sink 角色切换、PDO 解析这几件事串成了一条能跑通的链路。适合正在做 USB PD 快充、Type-C 接口固件、或者想给自家小板子加上 9V/12V 诱骗功能的嵌入式开发者。下面我按自己拆包复现的顺序,把这份代码怎么用、参数怎么调、坑在哪讲清楚。

2. 拆开源码包:FUSB302 的 I2C 寄存器模型与初始化链路

拿到一份 FUSB302 的 PD 代码,第一件事不是急着编译烧录,而是先搞清楚它怎么跟芯片对话。FUSB302 的所有行为都挂在寄存器上,I2C 从地址固定为 0x22(7 位),内部寄存器分页管理,页选通过 0x01 寄存器的低两位切换。代码里如果这一步没做对,后面所有读写都会落到错误的页上,现象就是「写进去读出来全是 0」或者「中断一直触发但状态位不变」。

2.1 寄存器分页与关键寄存器速查

FUSB302 的寄存器大致分四组:设备 ID 与复位、CC 状态与 BMC 控制、PD 收发 FIFO、中断与屏蔽。初始化阶段真正要碰的核心寄存器不多,但每一个都不能写错。下面这张表是我从代码里反推出来的关键寄存器清单,配合源码看会顺很多。

寄存器地址名称作用典型值
0x01Switches0页选、CC 引脚上拉/下拉、测量块使能0x07 切页,0x22 开 CC1 下拉
0x02Switches1PD 收发方向、BMC 时钟0x80 使能 PD 发送
0x03Measure电压测量通道选择0x49 测 VBUS
0x06Control0中断屏蔽、自动重试0x00 全开中断
0x0CControl3发送自动重试、CRC 使能0x07 自动重试+CRC
0x0EMask中断屏蔽位按需
0x3DStatus0A接收状态、发送状态只读
0x3FStatus1CC 电平、VBUS OK只读
0x43FIFOs收发 FIFO 读写入口读写

代码里初始化函数一般长这样,我把它简化成能直接对照的版本:

// FUSB302 初始化:切页、复位、配置 CC 和 PD 收发 void fusb302_init(void) { i2c_write(FUSB302_ADDR, 0x01, 0x07); // 切到页 0,开 CC1 下拉(Sink 角色) i2c_write(FUSB302_ADDR, 0x0C, 0x07); // Control3:自动重试 + CRC 使能 i2c_write(FUSB302_ADDR, 0x06, 0x00); // Control0:不屏蔽任何中断 i2c_write(FUSB302_ADDR, 0x02, 0x80); // Switches1:使能 PD 发送通道 i2c_write(FUSB302_ADDR, 0x03, 0x49); // Measure:VBUS 测量通道 // 清空收发 FIFO,避免上电残留数据干扰 i2c_write(FUSB302_ADDR, 0x43, 0x04); // 复位 RX FIFO i2c_write(FUSB302_ADDR, 0x43, 0x02); // 复位 TX FIFO }

这段代码的逻辑顺序不能乱:先切页,再配 Control3 和 Control0,最后开 Switches1。如果先开 Switches1 再切页,PD 发送通道会挂在错误的页上,现象是发送 FIFO 写进去但 Status0A 的 txbusy 一直不置位。参数上,0x01 写 0x07 表示页 0 加 CC1 下拉,这是 Sink 角色的标准配置;如果你要做 Source 给别的设备供电,这里要改成 CC1 上拉,值一般是 0x05 或 0x06,具体看代码里对角色切换的处理。

2.2 中断驱动的 PD 报文收发流程

FUSB302 的 PD 收发是中断驱动的,代码里一般会挂一个 GPIO 中断到 MCU,然后在中断服务函数里读 Status0A 判断是接收还是发送完成。接收流程是:中断触发 → 读 Status0A 确认 rxempty 为 0 → 从 FIFO 读报文头 → 根据报文头长度读数据 → 解析 PDO。发送流程反过来:写 FIFO → 置 Switches1 的 txstart → 等中断确认发送完成。

// PD 接收中断处理:从 FIFO 读出完整报文 void fusb302_rx_handler(void) { uint8_t status = i2c_read(FUSB302_ADDR, 0x3D); if (status & 0x01) { // rxempty 为 0 表示有数据 uint8_t header[2]; i2c_read_buf(FUSB302_ADDR, 0x43, header, 2); // 先读 2 字节报文头 uint8_t len = header[0] & 0x0F; // 低 4 位是数据对象个数 if (len > 0) { uint8_t payload[28]; i2c_read_buf(FUSB302_ADDR, 0x43, payload, len * 4); // 每个对象 4 字节 pd_parse_pdo(payload, len); // 解析 PDO,更新可用电压档位 } } }

这里有个容易翻车的点:FIFO 读操作必须一次性连续读完,不能分多次 I2C 事务读,否则 FIFO 指针会错位。代码里用i2c_read_buf而不是循环调i2c_read,就是为了保证连续性。另外len * 4这个长度计算,len 是报文头低 4 位,表示数据对象个数,每个对象 4 字节,所以总长度是 len 乘 4。如果代码里写成 len 直接当字节数用,读出来的 PDO 会少一半,现象是电压档位只解析出前两个。

3. 跑通一轮 PD 协商:从 CC 检测到 PDO 请求的完整时序

初始化做完只是让芯片能动,真正要拿到 9V 或 12V,得走完一轮完整的 PD 协商。这轮协商的时序是固定的:CC 检测确认连接 → 发送 Source_Capabilities 或等待对方发 → 解析 PDO → 发 Request → 等 Accept → 等 PS_RDY → VBUS 到位。代码里如果哪一步的超时没处理,就会卡在半路,充电器那边看到的是「设备不响应」,直接退回 5V。

3.1 CC 检测与角色判定

CC 引脚的电平决定了谁是 Source 谁是 Sink。FUSB302 的 Status1 寄存器里有一位 cc1_level 和 cc2_level,读出来就能判断当前连接状态。代码里一般会轮询这个寄存器,直到检测到稳定的 CC 电平。

// 等待 CC 连接稳定,返回检测到的角色 pd_role_t fusb302_detect_cc(void) { uint32_t timeout = 100000; while (timeout--) { uint8_t status = i2c_read(FUSB302_ADDR, 0x3F); if (status & 0x80) { // bit7 表示 CC 有连接 uint8_t level = (status >> 4) & 0x03; // bit5:4 是 CC 电平 if (level == 0x01) return ROLE_SINK; // 下拉,对方是 Source if (level == 0x02) return ROLE_SOURCE; // 上拉,对方是 Sink } delay_us(10); } return ROLE_NONE; }

这段代码的超时设成 100000 次循环,每次 10 微秒,总共约 1 秒。实际调试时如果充电器插上后 1 秒内没检测到,要么是 CC 引脚配置错了,要么是充电器本身不支持 PD。参数上,level 的判断值 0x01 和 0x02 对应的是 FUSB302 内部比较器的输出,不同批次的芯片可能有微小差异,如果发现角色判断反了,先查 Switches0 里的上拉/下拉配置是不是跟预期一致。

3.2 构造 Request 报文与 PDO 索引映射

解析完对方的 Source_Capabilities 后,代码里会拿到一组 PDO,每个 PDO 包含电压和最大电流。要请求某个电压档位,需要构造一个 Request 报文,里面带上 PDO 索引和电流值。这里最容易出错的是 PDO 索引的映射——有些代码从 0 开始算,有些从 1 开始,而 PD 协议里 Request 报文的 Object Position 是从 1 开始的。

// 根据目标电压选择 PDO 并构造 Request 报文 void pd_send_request(uint16_t target_mv, uint16_t current_ma) { uint8_t pdo_index = 0; for (int i = 0; i < pdo_count; i++) { if (pdo_list[i].voltage_mv == target_mv) { pdo_index = i + 1; // 协议里 Object Position 从 1 开始 break; } } if (pdo_index == 0) return; // 没找到匹配电压,放弃 uint8_t request[4]; request[0] = (pdo_index << 4) | 0x02; // 最高位是索引,低两位是 USB 通信能力 request[1] = (current_ma / 10) & 0xFF; // 电流以 10mA 为单位 request[2] = ((current_ma / 10) >> 8) & 0x03; request[3] = 0x00; fusb302_send_pd(request, 4); // 写入 FIFO 并触发发送 }

pdo_index << 4这一步是把索引放到报文第一个字节的高 4 位,低 4 位里的 0x02 表示设备支持 USB 2.0 通信。电流字段以 10mA 为单位,比如要 3A 就填 300。如果代码里直接把毫安数填进去,充电器会认为你请求了一个超大电流,直接拒绝或者触发过流保护。这个坑我在早期调试时踩过,现象是 Request 发出去后收到 Reject,查了半天才发现是单位没换算。

4. 避坑与排查:FUSB302 调试中最容易翻车的五个点

FUSB302 这颗芯片本身不复杂,但 PD 协议栈的时序要求严,加上 I2C 读写和中断处理的细节多,实际调试时很容易卡在一些看起来莫名其妙的地方。下面这五条是我和身边同行踩过的真实坑,按「现象 → 原因 → 解决」整理,照着排查能省不少时间。

现象一:I2C 能读到设备 ID,但写寄存器后读回来全是 0。原因:页选没做对。FUSB302 上电默认在页 0,但有些代码在初始化前先读了别的页,导致后续写操作落到错误页上。 解决:每次读写寄存器前,先写 0x01 寄存器确认页选,或者在初始化函数最开头强制切页。代码里加一句i2c_write(FUSB302_ADDR, 0x01, 0x07)再开始其他配置。

现象二:中断一直触发,但 Status0A 的 rxempty 始终为 1。原因:中断屏蔽寄存器没配对,或者中断触发方式是边沿触发但代码里配成了电平触发,导致中断反复进但没数据。 解决:检查 Control0 的屏蔽位,确保接收中断没被屏蔽;同时确认 MCU 侧的 GPIO 中断配置是下降沿触发,跟 FUSB302 的 INT_N 引脚行为匹配。

现象三:PD 报文发出去后收不到 GoodCRC,协商卡在第一步。原因:BMC 时钟配置不对,或者发送 FIFO 写入的数据长度跟报文头里的长度字段不一致。 解决:检查 Switches1 里的 BMC 时钟分频设置,确保跟 MCU 主频匹配;同时核对发送时写入 FIFO 的字节数,报文头里的长度字段要跟实际数据长度一致,多写或少写都会导致 CRC 校验失败。

现象四:能收到 Source_Capabilities,但解析出的 PDO 电压全是 0。原因:FIFO 读取时没有连续读,或者字节序搞反了。PD 报文是小端序,低字节在前。 解决:用连续读的方式一次性把 FIFO 数据读出来,解析时按小端序组合。比如电压字段是 10 位,低 8 位在前,高 2 位在后,组合时要注意移位顺序。

现象五:请求 9V 成功,但 VBUS 实际只有 5V。原因:Request 报文里的电流值超过了充电器 Source_Capabilities 里声明的最大电流,充电器接受了请求但实际不切换电压。 解决:在构造 Request 前,先检查目标 PDO 的 Max Current 字段,确保请求电流不超过它。代码里加一个判断,如果目标电流大于 PDO 声明的最大值,就取最大值或者换一个 PDO。

提示:FUSB302 的调试最好配一个能看 PD 报文的分析仪,没有的话至少用逻辑分析仪抓 I2C 波形,确认每一步读写的数据跟预期一致。光靠串口打印很容易漏掉时序问题。

5. 进阶用法:把 PD 协商封装成可复用状态机与参数调优

跑通一轮协商之后,下一步通常是把这套流程封装成一个状态机,方便在不同项目里复用。我一般会把 PD 协商拆成几个状态:IDLE、CC_DETECT、WAIT_CAP、SEND_REQUEST、WAIT_ACCEPT、WAIT_PS_RDY、READY。每个状态带超时,超时后回退到 IDLE 重新检测。这样即使充电器中途拔插,状态机也能自己恢复,不会卡死。

// PD 协商状态机:带超时和回退 typedef enum { PD_IDLE, PD_CC_DETECT, PD_WAIT_CAP, PD_SEND_REQ, PD_WAIT_ACCEPT, PD_WAIT_PSRDY, PD_READY } pd_state_t; void pd_state_machine(void) { static pd_state_t state = PD_IDLE; static uint32_t state_tick = 0; switch (state) { case PD_IDLE: if (fusb302_detect_cc() != ROLE_NONE) { state = PD_WAIT_CAP; state_tick = get_tick(); } break; case PD_WAIT_CAP: if (pd_rx_flag) { // 收到 Source_Capabilities pd_parse_pdo(rx_buf, rx_len); state = PD_SEND_REQ; } else if (get_tick() - state_tick > 1000) { state = PD_IDLE; // 超时回退 } break; case PD_SEND_REQ: pd_send_request(9000, 2000); // 请求 9V/2A state = PD_WAIT_ACCEPT; state_tick = get_tick(); break; case PD_WAIT_ACCEPT: if (pd_rx_flag && pd_msg_type == PD_ACCEPT) { state = PD_WAIT_PSRDY; state_tick = get_tick(); } else if (get_tick() - state_tick > 500) { state = PD_IDLE; } break; case PD_WAIT_PSRDY: if (pd_rx_flag && pd_msg_type == PD_PS_RDY) { state = PD_READY; // 协商完成,VBUS 已切换 } else if (get_tick() - state_tick > 500) { state = PD_IDLE; } break; case PD_READY: if (fusb302_detect_cc() == ROLE_NONE) { state = PD_IDLE; // 拔出后复位 } break; } }

这个状态机里每个等待状态都带了超时,WAIT_CAP 给 1 秒,WAIT_ACCEPT 和 WAIT_PSRDY 各给 500 毫秒。超时时间可以根据实际充电器的响应速度调整,但不要设得太短,有些充电器处理 Request 需要 200 毫秒以上。参数上,pd_send_request(9000, 2000)里的 9000 是目标电压毫伏,2000 是请求电流毫安,实际项目里可以根据 PDO 列表动态选。

调优方面,有两个参数值得关注:一是 Control3 里的自动重试次数,默认是 3 次,如果充电器响应慢可以适当增加;二是 Measure 寄存器里的 VBUS 测量阈值,用来判断 VBUS 是否到位。这两个参数在代码里一般有宏定义,改起来方便。另外,如果项目里需要支持多个电压档位切换,状态机里可以加一个 PD_READY 下的子状态,收到新请求时重新走 SEND_REQUEST 流程,不用退回 IDLE。

从那以后我每次拿到新的 FUSB302 代码,都强制先跑一遍 CC 检测和寄存器读写回环,确认 I2C 链路和页选没问题,再往下调 PD 协商。这一步花不了几分钟,但能挡掉后面一大半的玄学问题。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 18:28:57

knowledge-work-plugins:用Claude Code插件标准化知识工作流

1. 从零认识 knowledge-work-plugins&#xff1a;它到底解决什么问题第一次看到knowledge-work-plugins这个仓库名&#xff0c;很多人会以为它只是某个工具的插件合集&#xff0c;随手 clone 下来翻两眼就丢到一边。但如果你真的在 Claude Code 或 Claude Cowork 里干过一段时间…

作者头像 李华
网站建设 2026/10/18 18:28:54

用ChatGPT精读英文PDF文献:三步流程与万能提示词模板

1. 为什么我劝你别再一页页啃英文PDF了过去一年里&#xff0c;我帮不少人处理过读文献的活儿&#xff0c;从某高校实验室的同学到某公司做技术调研的开发者&#xff0c;大家遇到的核心问题出奇一致&#xff1a;拿到一篇英文学术PDF&#xff0c;读起来又慢又痛苦&#xff0c;尤其…

作者头像 李华
网站建设 2026/10/18 18:25:43

Claude Code 知识工作插件实战:从安装配置到定制开发

1. 项目缘起与核心定位1.1 这个仓库到底在解决什么问题第一次看到knowledge-work-plugins这个仓库名&#xff0c;我的直觉是&#xff1a;这大概率是一套围绕 Claude Code 生态构建的插件集合&#xff0c;专门面向“知识工作”场景。事实也确实如此。它不是一个框架&#xff0c;…

作者头像 李华
网站建设 2026/10/18 18:24:32

SM2密文格式转换:C1C3C2与ASN.1 DER互转实战解析

做国密 SM2 接入的同学应该都遇到过这种尴尬&#xff1a;自己手里拿到的密文是一长串十六进制&#xff0c;开头固定一个 04&#xff0c;后面跟着 65 字节的点坐标、32 字节摘要和一段密文&#xff0c;也就是标准 C1C3C2 拼接格式。结果对端接口偏偏只要 ASN.1 DER 编码的密文&a…

作者头像 李华
网站建设 2026/10/18 18:23:22

深度压缩感知与LISTA实现:从ISTA迭代到网络展开的工程实践

简介&#xff1a;这份资源面向深度学习与信号处理方向的学习者和开发者&#xff0c;聚焦压缩感知重构速度慢的痛点&#xff0c;提供基于PyTorch的LISTA&#xff08;学习迭代收缩阈值算法&#xff09;完整项目代码。内容涵盖ISTA与LISTA的算法原理对照、稀疏信号重构仿真实验&am…

作者头像 李华
网站建设 2026/10/18 18:23:15

南京矢量数据包实战:SHP图层解析、坐标系处理与空间分析全流程

简介&#xff1a;这份南京市区县乡镇街道及交通设施矢量数据包&#xff0c;面向城市规划、地理信息研究与GIS教学实训场景&#xff0c;解决本地基础空间数据获取零散、图层不全的问题。资源覆盖南京市全域&#xff0c;包含市、区、县、镇、村多级行政边界&#xff0c;国道、省道…

作者头像 李华