简介这是一套基于STM32L476微控制器与FUSB307 USB Type-C/PD控制器的嵌入式软件工程完整实现了USB PD3.0协议面向开发快充充电器、移动电源等设备的嵌入式开发者也适合需要系统学习PD协议栈与STM32 HAL库的工程师或相关专业学生。压缩包共267个文件大小约11.53MB以C语言源码、头文件、HAL底层驱动、构建配置文件以及编译生成的中间文件为主其中76个.h头文件与42个.c源文件构成完整可编译的工程框架。已有434人学习浏览该资源。通过研读工程中的策略代码、VDM处理逻辑与HAL驱动可掌握FUSB307的初始化配置、CC引脚管理及PD协商流程理解在低功耗MCU上实现复杂协议栈的设计思路对开发支持快速充电和电源管理的实际产品具有直接参考价值。1. FUSB307B 资源包PD 协议不再是玄学而是一份可复现的代码套件做过 Type-C 口的人都知道最熬人的不是硬件那几根线而是 PD 协议握手电压广播、隐式协商、显式协商、软复位中间任何一步状态不对板子就卡在 5V 不动或者 Source 端干脆不给电。FUSB307B 是一颗把 CC 检测、PD 物理层和协议状态机全包进去的 USB Type-C/PD 控制器主控只需要通过 I2C 读写寄存器、处理中断就能让设备实现 Sink、Source 或 DRP 角色。这个fusb307b-master.zip资源包里装的就是围绕这颗芯片的参考驱动、初始化序列和协议协商逻辑适合正在做 Type-C 口却不想从零啃 PD 规范的嵌入式工程师也适合想快速把 PD 3.0 功能跑起来的学生开发者。下文从芯片功能边界一路拆到移植踩坑最后给出验证手段。2. 先看寄存器与中断模型FUSB307B 的功能边界决定移植策略2.1 芯片替你做的事CC 检测、PD 状态机与物理层FUSB307B 内部集成了 Type-C 检测所需的 Rd/Rp 电阻通路、CC 引脚上的 BMC 编码收发模块以及一套完整的 PD 状态机。主控不需要自己去翻转 CC 引脚的上下拉电阻也不需要逐位发送 BMC 信号这些工作由芯片的硬件逻辑完成。开发者拿到的是一组状态寄存器和中断标志插拔事件、收到 Source Capabilities、收到 Accept、收到 PS_RDY全部映射到 INT_N 引脚和中断状态寄存器。这颗芯片的价值在于把“协议握手”从“主控实时任务”降级为“事件响应”。如果不用这类控制器纯靠 MCU 的 GPIO 模拟 CC 上下拉和 BMC 时序协议时隙根本压不住。PD 的 BIST、重传、CRC 校验这些耗时操作会让主控连业务代码都跑不动。FUSB307B 的硬件状态机保证了对端发来的消息能被及时回应主控的操作只是配置好策略后等中断。选择 FUSB307B 还要看它的两路 CC。CC1 和 CC2 是独立的芯片会根据插拔方向自动选出有效的一路VCONN 开关也集成在里面。对于做双 Type-C 口或 USB-C 转接头这类需要方向自动识别的板子这种集成度能省掉不少外围器件。2.2 驱动代码的第一眼初始化序列与设备 ID 校验拿到fusb307b-master.zip后先不要急着往工程里拖文件。我习惯先看它的初始化源文件通常初始化序列是这样的读取设备 ID配置中断掩码软复位然后设置角色相关寄存器。下面这段是典型的初始化代码骨架底层 I2C 读写函数需要你在平台层自己实现。#include fusb307b.h #include i2c_platform.h #define FUSB307B_I2C_ADDR 0x52 // 7bit 地址 0x29左移一位后为 0x52 #define DEVICE_ID_REG 0x01 #define CTRL0_REG 0x06 // 角色控制寄存器 #define MASK_REG 0x0E // 中断掩码寄存器 #define SOFT_RESET_BIT 0x02 // CTRL0 中的软复位位 static uint8_t fusb307b_read_reg(uint8_t reg) { uint8_t val 0; i2c_read_byte(FUSB307B_I2C_ADDR, reg, val); return val; } static void fusb307b_write_reg(uint8_t reg, uint8_t val) { i2c_write_byte(FUSB307B_I2C_ADDR, reg, val); } int fusb307b_init(void) { uint8_t dev_id fusb307b_read_reg(DEVICE_ID_REG); if ((dev_id 0xF0) ! 0x20) { // 高四位应为芯片族标识读不出正确值时说明 I2C 或供电有问题 return -1; } // 先软复位让芯片回到上电默认状态 uint8_t ctrl0 fusb307b_read_reg(CTRL0_REG); fusb307b_write_reg(CTRL0_REG, ctrl0 | SOFT_RESET_BIT); // 软复位后芯片会重新初始化需要等一段时间 platform_delay_ms(10); // 打开需要的中断源插拔、PD 消息接收、电源状态变化 fusb307b_write_reg(MASK_REG, 0x07); return 0; }初始化这段代码有几个关键参数要看清楚。DEVICE_ID_REG读取出来的值包含芯片版本号不同批次的值可能不同不要用全字节匹配而是只比对高四位或高六位否则换一批芯片就初始化失败。SOFT_RESET_BIT置位后要留足时间让内部状态机复位一般 10 毫秒是够的但如果外部接了慢速 I2C 上拉建议放到 20 毫秒。MASK_REG写入的 0x07 对应三类最常见的中断源实际值取决于这个资源包头文件里定义的中断位直接沿用包里的宏定义最稳妥。2.3 中断还是轮询主控侧的两种用法FUSB307B 的 INT_N 引脚是低电平有效一旦有事件发生就会拉低主控可以用 GPIO 外部中断触发也可以在定时器里轮询电平。两种方式在资源包工程里都有对应代码。// 方式一GPIO 外部中断适合事件不频繁的 Sink 设备 void EXTI0_IRQHandler(void) { // 清除 GPIO 中断标志 GPIO_ClearITPendingBit(GPIOA, GPIO_Pin_0); // 读取中断状态寄存器逐个 bit 判断事件类型 uint8_t int_status fusb307b_read_reg(INT_STATUS_REG); if (int_status INT_SOURCE_CAP) { // 收到 Source Capabilities进入 PDO 选择流程 pd_handle_source_caps(); } if (int_status INT_POWER_STATUS) { // VBUS 或电源状态变化可能 PS_RDY 到了 pd_handle_power_status(); } // 注意中断状态寄存器读回后硬件自动清位不需要额外写操作 } // 方式二轮询适合主控没有空闲外部中断引脚的场景 void pd_poll_handler(void) { static uint32_t last_poll_tick 0; uint32_t now get_system_ms(); if (now - last_poll_tick 5) { return; // 轮询周期 5msPD 协议消息间隔远大于这个值 } last_poll_tick now; // 先查 INT_N 引脚电平为高说明没有事件跳过 I2C 读取 if (GPIO_ReadInputPin(GPIOA, GPIO_Pin_0) HIGH) { return; } uint8_t int_status fusb307b_read_reg(INT_STATUS_REG); // 后续处理和中断方式相同 if (int_status INT_SOURCE_CAP) { pd_handle_source_caps(); } }两种方式我都试过中肯地说Sink 设备用中断方式更好因为 PD 协商集中在插入后的几百毫秒内过后几乎无事可做事件驱动能让主控在等待 VBUS 建立时去跑别的任务轮询方式适合主控外部中断引脚已经用完的板子但需要保证系统 tick 可用。这里有个常见失误有些工程师在中断处理里读取状态寄存器后,又手动写一个“清中断”寄存器结果把新事件的状态位也清掉导致丢事件。FUSB307B 的中断状态寄存器是读后自动清的不要在中断里做多余写操作。3. Sink 设备 PD 协商实战从 PDO/RDO 解析到 VBUS 电压切换3.1 Sink 还是 Source 还是 DRP角色配置怎么决定FUSB307B 支持的角色都写在控制寄存器里。Sink 模式对应芯片内部把 CC 引脚接 Rd 下拉Source 模式对应接 Rp 上拉DRP 模式则周期性切换两种状态。启动时芯片的默认角色取决于硬件电路上的配置引脚也可以由主控在初始化后随时改写。表格反映角色与 CC 电阻的对应关系角色CC 引脚状态可协商的 PD 行为SinkUFP接 Rd 下拉接收 Source Capabilities请求 PDOSourceDFP接 Rp 上拉发送 Source Capabilities响应请求DRP交替 Rd/Rp根据对端角色决定最终方向这里要留意角色的切换不是改一个寄存器位就立刻生效的。芯片内部的 Type-C 状态机需要时间完成重新检测通常要等至少 20 毫秒。有些资源包里的代码在切换角色后立刻去读状态读到对的插拔方向便以为配置成功其实那只是上一次检测的缓存值。3.2 从 RDO 到电压切换协议协商的完整链路Sink 端插入后芯片会上报一个收到 Source Capabilities 的中断。主控读取 PDO 列表后需要从中选一个条目构造 RDO 发回去。一次完整的协商链路是Sink 收到 Source Caps根据自身功率需求选择一个 PDO构造 RDO 并发送等待 Accept等待 PS_RDY最后切换到目标电压。RDO 里的关键字段是 Object Position它告诉 Source 端要选哪一个 PDO。比如 Source 广播了三个 PDO5V/3A、9V/3A、12V/3ASink 发 RDO 时把 Object Position 设为 2就表示“我请求第二个 PDO即 9V”。// 构建 RDO向 Source 请求指定位置的 PDO typedef struct { uint16_t raw_header; // RDO Header包含 Object Position uint32_t obj[0]; // 后面最多跟 7 个 object } pd_message_t; #define PD_MSG_TYPE_RDO 0x03 #define OBJECT_POSITION_SHIFT 28 #define POSITION_TO_RDO(p) ((uint32_t)(p) OBJECT_POSITION_SHIFT) uint32_t pd_build_rdo(uint8_t object_position, uint16_t operating_current_mA) { uint32_t rdo_raw 0; // Object Position 放在最高 3 bit rdo_raw | POSITION_TO_RDO(object_position); // Operating Current 和 Max Operating Current 的单位是 10mA // 注意这个 10mA 的单位不是 1mA uint32_t current_10ma operating_current_mA / 10; rdo_raw | (current_10ma 0x3FF) 10; // Max Operating Current rdo_raw | (current_10ma 0x3FF); // Operating Current return rdo_raw; } // 发送 RDO 后等待 Accept 和 PS_RDY int pd_sink_request_pdo(uint8_t pdo_pos, uint16_t current_mA) { // 构造完整的 PD 消息头部还要带消息类型 pd_message_t msg; msg.raw_header PD_MSG_TYPE_RDO; // 实际还要加协议版本等字段 msg.obj[0] pd_build_rdo(pdo_pos, current_mA); // 把 msg 交给驱动层发送具体函数见资源包 return fusb307b_send_pd_message(msg); }这段代码里最容易错的是电流单位。PD 协议里 Operating Current 的单位是 10mA想请求 3A 就是填 300不是 3000。坑就在这按 1mA 单位填的话请求电流会被放大 10 倍Source 端检测到超出 PDO 限制会直接拒绝或者回一个 Reject。另一个值得注意的点是 Object Position 从 1 开始编号没有 0 号 PDO。请求 5V 的时候也要带着位置编号不是默认就有 5V。3.3 实际例子请求 5V/3A 或 9V/2A并在 VBUS 变化后确认结合上面的代码实际跑通一个 Sink 请求 9V/2A 的流程主控侧的状态推进如下// 伪代码展示 Sink 协商的状态推进 void pd_sink_task(void) { switch (sink_state) { case ST_WAIT_SRC_CAPS: // 芯片上报收到 Source Capabilities 中断后进入 if (received_source_caps) { // 解析 PDO 列表找到支持 9V 的条目 uint8_t pdo_9v_pos find_pdo_by_voltage(9000); if (pdo_9v_pos 0) { pd_sink_request_pdo(pdo_9v_pos, 2000); // 请求 2A sink_state ST_WAIT_ACCEPT; } } break; case ST_WAIT_ACCEPT: // 收到 Accept 消息等待 PS_RDY if (message_accept_received) { sink_state ST_WAIT_PS_RDY; } break; case ST_WAIT_PS_RDY: // PS_RDY 表示 Source 侧已完成电压切换 if (message_ps_rdy_received) { // 此时 VBUS 已经在向 9V 爬升等待 ADC 采样确认 sink_state ST_WAIT_VBUS_SETTLE; } break; case ST_WAIT_VBUS_SETTLE: // 用 ADC 采样 VBUS等电压稳定到目标值附近 if (vbus_adc_mv() 8500) { // 通知系统电源已就绪 system_set_input_voltage(9000); sink_state ST_RUNNING; } break; } }这套状态机是 Sink 协商的核心。注意在收到 PS_RDY 之后不要立刻认为电压已经到位Source 端的 MOSFET 切换和电容充放电需要时间建议通过 ADC 采样确认 VBUS 进入目标电压区间后再让后级负载工作否则负载可能在上电瞬间吸入过大电流。资源包里一般会有 vbus 采样的参考实现没有的话就用主控自带的 ADC 分压采样阈值建议放到目标电压的 90% 以上比如 9V 对应 8.5V 左右太早判定可能会拉着不稳的电压运行。4. 移植到现有工程I2C 平台层、系统 Tick 与文件裁剪4.1 文件结构与快速上手哪些文件先放进工程解开fusb307b-master.zip后里面通常按驱动、平台接口、示例应用分层。标准做法是先识别出三个部分平台无关的核心驱动文件、需要你填写的平台适配文件、以及可选的示例代码。核心驱动文件的名字一般能看到fusb307b前缀平台适配文件可能叫fusb307b_i2c或fusb307b_platform。表格列一下典型的文件构成文件作用是否需要修改fusb307b.c / fusb307b.h寄存器读写、初始化、消息收发通常不用改fusb307b_i2c.cI2C 底层读写适配必须按你的平台重写fusb307b_platform.c延时、tick、GPIO 中断适配必须修改pd_policy.cSink/Source 策略选择按业务需求改main_demo.c示例调用流程参考即可移植时只把核心驱动和平台适配文件放进工程示例文件不要直接参与编译否则可能出现两个 main 函数或者重复的系统初始化。文件名和目录结构在不同版本的包里会有差别但按“平台无关核心 平台适配”这个思路去剥离通常不会找错。4.2 I2C 平台层对接硬件读写钩子怎么填FUSB307B 对 I2C 时序的要求比较标准100k 或 400k 模式都可以重要的是把阻塞式读写实现成原子操作中间不要被其他中断打断超过几十微秒。下面这段是平台层典型实现。// fusb307b_i2c.c 中的平台适配示例 #include stm32_hal.h I2C_HandleTypeDef hi2c1; int i2c_write_byte(uint8_t dev_addr, uint8_t reg, uint8_t data) { uint8_t buf[2]; buf[0] reg; buf[1] data; // 发送寄存器地址 数据超时设置为 100ms HAL_StatusTypeDef st HAL_I2C_Master_Transmit( hi2c1, (uint16_t)(dev_addr 1), buf, 2, 100); return (st HAL_OK) ? 0 : -1; } int i2c_read_byte(uint8_t dev_addr, uint8_t reg, uint8_t *data) { // 先写寄存器地址再读一个字节 HAL_StatusTypeDef st HAL_I2C_Master_Transmit( hi2c1, (uint16_t)(dev_addr 1), reg, 1, 100); if (st ! HAL_OK) { return -1; } st HAL_I2C_Master_Receive( hi2c1, (uint16_t)(dev_addr 1), data, 1, 100); return (st HAL_OK) ? 0 : -1; }注意dev_addr的处理。FUSB307B 的 7bit 地址在 HAL 里要左移一位变成 8bit 地址这个转换经常被漏掉导致所有 I2C 通信都 NACK。另一种常见问题是 I2C 时钟初始化得不对芯片在低功耗模式下可能不响应或者响应速度变慢。用逻辑分析仪抓波形是最直接的排查手段SCL 上应该有干净的方波SDA 在应答位之后被芯片拉低。如果看到地址字节发送后 SDA 一直是高说明芯片没应答先查地址和供电。4.3 时基与定时器PD 协商和计时依赖的系统 TickFUSB307B 内部的 Type-C 状态机有自己的定时但主控侧的 PD 策略也需要精确时基比如 Sink 等待 Source Capabilities 的超时、BIST 后的重启动等。资源包里的平台适配文件一般要求提供一个毫秒级 tick。裸机工程直接在 SysTick 中断里自增一个全局变量RTOS 工程可以用osKernelGetTickCount或者专用的定时器消息队列。// 裸机环境下的毫秒 tick 实现 volatile uint32_t g_sys_ms 0; void SysTick_Handler(void) { g_sys_ms; } uint32_t get_system_ms(void) { return g_sys_ms; } // FUSB307B 平台适配文件中需要这里的 tick // 例如等待 VBUS 稳定、等待协议消息超时都用这个函数一个隐坑是 SysTick 优先级设置过低导致 PD 消息处理被其他中断长时间打断。PD 协议里的时间参数虽然容忍度较宽但如果在接收消息的过程中 I2C 读取被拖到数百毫秒之后芯片内部的消息缓存会被新消息覆盖表现为“有时能协商成功有时不能”。我一般会把 SysTick 中断优先级调成倒数第二低只比空闲任务高确保 tick 计数稳定但又不抢占关键通信中断。4.4 编译告警与裁剪把用不到的功能关掉不少拿到这个包的开发者会直接把所有源文件都编进去结果出现一堆未使用函数的告警或者因为开了调试宏导致日志输出把存储挤爆。建议把 PD 3.0 PPS、DRP 等暂时用不到的宏定义关掉只保留需要的功能。资源包头文件里通常有类似#define FUSB307B_SUPPORT_PPS的开关注释掉就好。裁剪后重新确认DEVICE_ID_REG和中断掩码寄存器初始化值没有因为宏变化而跟着变有些参数是用宏拼接出来的宏关了初始化也会变。5. 避坑与排查七个从硬件到协议的实战翻车记录5.1 I2C 读回设备 ID 是全 F 或者 0现象初始化时读 DEVICE_ID 寄存器返回值是 0xFF 或 0x00芯片像完全不存在。 原因多数情况是 I2C 地址不对。FUSB307B 的地址引脚接法会改变 7bit 地址原理图上 ADDR 引脚的电平决定了最终地址。另一种可能是芯片的供电脚没吃到电这颗芯片在仅有 CC 线供电的死电池模式下状态特殊主控 I2C 在芯片还没启动完成时就访问也会读回异常。 解决先量 VDD 电压再用逻辑分析仪确认 I2C 起始条件和地址字节是否在总线上出现。把原理图上的 ADDR 引脚电平找出来对照手册的地址表换算别猜地址。如果芯片上电后需要时间初始化在读取前加 50ms 延时。5.2 中断风暴主控陷入死循环现象上电后中断服务程序反复进入主控完全卡死甚至无法响应调试器。 原因典型的寄存器读后未正确清除或中断状态里同时出现多个事件代码只处理了一个其余事件没有被读取清掉INT_N 一直被拉低。 解决把中断状态全部读出来做一个循环逐个 bit 处理全部处理完再退出中断。如果状态寄存器里还有未知 bit不要去写它读取本身就会清除。用示波器挂在 INT_N 上看波形如果一直是低电平问题一定在状态读取链路。5.3 Source 端不广播 CapsSink 一直等现象插入 Source 后Sink 侧始终没有收到 Source Capabilities 中断。 原因最常见的是 CC 线反了芯片的 CC1 和 CC2 检测逻辑虽然会做方向判断但如果原理图上 CC 引脚接错检测方向会失败。另一个原因是 Source 端根本没有进 PD 模式它可能只做了 Type-C 电流能力广播。 解决用万用表量 CC 线上的电压判断 Source 是在 Rp 还是 Rd 状态。Sink 设备插入后 CC 应该是被拉低到地附近。如果 CC 电压在 0.2V 以下说明 Source 认为你是 UFP 且不准备进 PD如果 CC 电压在 1.2V 或 1.6V 附近表示 Source 在使用不同电流档位这时应该有 Protocol 通信能力。5.4 RDO 请求被 Reject现象Sink 发出 RDO 后收到的是 Reject 而不是 Accept。 原因Object Position 指向的 PDO 不存在或者请求的电流超过了该 PDO 的上限。有些 Source 设备实现了 PDO 校验超过 10mA 单位精度后认为非法请求。还有个隐蔽原因是把 Max Operating Current 当成 Operating Current 填了更大的值。 解决打印收到的 PDO 列表确认目标电压对应的位置编号。对照请求电流和 PDO 标称电流留出至少 10% 的余量。试着请求较低的电流先跑通链路再逐步提高。5.5 VBUS 电压切换后没到目标值现象PS_RDY 已经收到但用万用表量 VBUS 还是 5V。 原因Source 端可能在收到 RDO 后先切到新的 PDO但 VBUS 的放电电路或输出电容导致爬升很慢也可能是 Source 端实现的协商策略里把“切换电压”延后到收到 PS_RDY 之后。 解决用示波器看 VBUS 爬升波形确认不是拉弧。如果肉眼可见爬升缓慢把 VBUS 就绪阈值调低或者延长等待时间。这里最危险的是一边判断电压没到位一边反复发 RDO会把 Source 端搞蒙进入软复位流程。5.6 软复位导致不断循环协商现象协商成功后设备每隔几秒重新发起一轮 PD 协商VBUS 电压抖动。 原因FUSB307B 侧检测到协议错误比如 CRC 错误超过重传上限自动发起软复位。主控侧的中断处理没有正确回应部分消息导致芯片判定超时。还有一种可能是主控在空闲时反复读取寄存器干扰了芯片内部状态机的时序。 解决在逻辑分析仪上统计每轮协商的时间间隔如果间隔固定且接近协议超时时间排查中断处理里的耗时操作移掉不必要的 I2C 读取。关闭调试打印对 I2C 中断路径的影响打印放在任务级不要放在中断级。5.7 I2C 波形正常但状态寄存器永远为 0现象逻辑分析仪能看到完整 I2C 通信ACK 也正常但读回的中断状态寄存器始终是 0没有事件位。 原因中断状态寄存器在读取后自动清空如果读取代码为了调试在中断里连续读了两次第二次自然读到 0。还有可能是主控的中断服务程序访问了一个镜像寄存器而不是真正的事件状态寄存器。 解决看数据处理路径里是否对同一个寄存器读取了多于一次。调试时只读一次把值保存下来分析不要边读边打印。检查寄存器地址是否被宏定义重复覆盖多见于两个头文件定义了同名宏。6. 用协议分析仪验证抓 BMC 编码与 VBUS 时序的三大技巧先看 PD 协商有没有走通不能只看“能充电”就下结论。把逻辑分析仪的两路探头分别挂到 CC 和 VBUS 上采样率调到 10MHz抓一次完整的插入协商过程。第一眼先看 CC 波形PD 的 BMC 编码是双相位标记编码每个 bit 中间有一次翻转肉眼看到的是一串高频脉冲而不是普通 UART 那样的电平保持。如果 CC 线上只有缓慢的电平变化没有脉冲簇说明双方根本没进入 PD 通信。第二个技巧是同时抓 VBUS。协商成功后 VBUS 应该有一个从 0V 爬升到目标电压的缓坡而不是瞬间跳变。PS_RDY 消息的位置对应 VBUS 爬升的起点两者时间差能判断 Source 端切换速度。如果 PS_RDY 到了 VBUS 还停在 5V 超过几十毫秒Source 端策略有问题。第三个技巧是抓软复位序列。正常协商不会频繁出现软复位而抓到的波形里如果每隔几百毫秒就有一段重复的 BMC 脉冲簇说明存在协议错误重传。对比 FUSB307B 芯片的 INT_N 引脚波形和协议错误中断状态能快速定位是主控响应慢还是外部 Source 端行为异常。这套验证流程我每次移植新板子都会强制走一遍先抓 CC 看有没有通信再抓 VBUS 确认电压切换最后查软复位频率排除隐患。有一次一根 CC 线序画反的板子光靠功能测试完全正常因为反插后芯片会自动切换方向但逻辑分析仪一抓就发现协商过程每次都要多花几十毫秒做方向探测后来改完线序协商时间立刻缩短。从那以后我再也不会跳过协议分析仪这一步了希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?