This document describes the current public API of PipPD.
The goal is accuracy over marketing. PipPD is not a full USB-PD stack. It is a compact FUSB302-oriented library with a clean split between PHY access, packet encoding, and sink policy.
PipPD uses C++17.
For PlatformIO:
build_unflags = -std=gnu++11
build_flags = -std=gnu++17The library is split into three layers:
PipPD: FUSB302 PHY / register / FIFO / CC / VBUS handlingProtocol: USB-PD packet encoding, parsing, revision-aware TX/RX, a small unchunked PD 3.x extended-message subset, and minimal structured VDM helpersSinkPolicy: sink-side negotiation, retries, timeouts, PPS selection, fallback, recovery, and compact PD 3.x service-message handlingCableDiscovery: compact helper forSOP' Discover Identity/ e-marker identity probing
Important boundaries:
PipPDdoes not implement high-level negotiation policyProtocoldoes not know about contract selection or recovery policySinkPolicyworks throughPipPD + Protocoland does not reach into FUSB302 registers directly
Main classes:
PipPDProtocolSinkPolicyCableDiscovery
Core config and state:
ConfigStatusInterruptsDebugRegistersThresholdScanDeviceId
PHY enums:
ModeCCLineCurrentTransmitStartSpecRevision
Protocol types:
MessageHeaderPdPacketRxMessageTxResultPacketTargetSourceCapabilitiesFixedSupplyPdoProgrammableSupplyPdoFixedRequestDataObjectPpsRequestDataObjectExtendedHeaderExtendedMessageTypeAlertDataObjectStatusDataBlockPpsStatusDataBlockStructuredVdmHeaderIdHeaderVdoCertStatVdoProductVdoCableVdo
Sink policy types:
SinkPreferencesSinkContractSinkContractTypeSinkStateSinkErrorSupplyPreferencePowerPreferencePd3ServiceRequestTypePd3ServiceResult
Cable discovery types:
CableDiscoveryStateCableIdentityQualityCableDiscoveryTraceEventTypeCableIdentityCableDiscoveryTraceEvent
#include <PipPD.h>
PipPD pd;
Config config;
config.wire = &Wire;
config.i2cAddress = 0x22;
config.interruptPin = 4;
config.mode = Sink;
config.specRevision = Rev30;
config.sourceCurrent = UsbDefault;
config.enableInternalPullDowns = true;
config.transmitStart = StartWithControl0;
pd.begin(config);Fields:
wirei2cAddressinterruptPinmodespecRevisionsourceCurrentenableInternalPullDownsenableVbusMonitoringenableSopPrimeenableSopDoublePrimeenableAutoCrctransmitStart
Notes:
enableInternalPullDowns = trueuses FUSB302 internal sink pull-downs on CC1/CC2- Set
enableInternalPullDowns = falseonly if the board already provides the sink pull-down network externally StartWithControl0starts TX withCONTROL0.TX_STARTStartWithFifoTokenstarts TX with theTXONFIFO token
pd.begin(config);
pd.reset();
pd.update();pd.restartDetection();
pd.stopDetection();
pd.setMode(Sink);
pd.setSourceCurrent(Current1A5);pd.online();
pd.attached();
pd.hasInterrupt();
pd.vbusPresent();
pd.mode();
pd.role();
pd.orientation();
pd.sinkCurrent();
pd.deviceId();
pd.status();
pd.lastInterrupts();pd.flushRxFifo();
pd.flushTxFifo();
pd.enableAutoCrc(true);
pd.enableSopPrime(true);
pd.enableSopDoublePrime(false);
pd.prepareTransmit();
pd.sendHardReset();
pd.writeTxFifo(data, size);
pd.readRxFifo(data, size);
pd.startTransmit();DebugRegisters debug;
pd.readDebugRegisters(debug);
bool high = false;
pd.measureCC(CC1, 20, high);
pd.measureVBUS(30, high);
ThresholdScan scan;
pd.scanCC(CC1, scan);
pd.scanVBUS(scan);#include <Protocol.h>
Protocol protocol(pd);- encode/decode
MessageHeader - build/parse
PdPacket - classify control and data messages
- encode/decode fixed PDOs
- encode/decode PPS APDOs
- encode/decode fixed RDOs
- encode/decode PPS RDOs
- encode/decode unchunked extended headers
- build/parse typed
StatusandPPS Status - build/parse typed
Alert - build TX frames for SOP and
SOP' - receive and decode PD messages from FUSB302 RX FIFO
- extract
SourceCapabilities - build/parse minimal structured VDM identity headers and cable identity VDOs
uint32_t rawPdo = Protocol::makeFixedSourcePdo(fixedPdo);
auto parsedFixed = Protocol::parseFixedSourcePdo(rawPdo);
uint32_t rawApdo = Protocol::makeProgrammableSupplyPdo(ppsApdo);
auto parsedPps = Protocol::parseProgrammableSupplyPdo(rawApdo);
uint32_t rawRdo = Protocol::makePpsRequest(ppsRequest);
auto parsedRdo = Protocol::parsePpsRequest(rawRdo);
AlertDataObject alert = Protocol::parseAlertDataObject(rawAlert);RxMessage message;
if (protocol.receiveMessage(message)) {
if (Protocol::isSourceCapabilities(message.packet)) {
SourceCapabilities caps;
Protocol::extractSourceCapabilities(message.packet, caps);
}
}PdPacket packet{};
packet.header.isDataMessage = false;
packet.header.messageType = CtrlGetSourceCap;
protocol.transmitSop(packet);Protocol can also transmit to SOP' through PacketTarget:
protocol.transmit(packet, TargetSopPrime);TxResult exposes step-by-step TX diagnostics:
payloadBuiltframeBuiltflushedpreparedwrittenstarteddebugReadokusedTxOnTokenframeSizeafter
Useful helpers:
encodeExtendedHeader(...)decodeExtendedHeader(...)extendedType(...)isExtendedMessage(...)isAlertMessage(...)isStatusMessage(...)isPpsStatusMessage(...)makeAlertDataObject(...)parseAlertDataObject(...)buildStatusData(...)extractStatusData(...)buildPpsStatusData(...)extractPpsStatusData(...)
CableDiscovery is a narrow helper for one MVP flow only:
- send structured
Discover IdentityonSOP' - wait for one response / timeout / NAK / BUSY
- decode a compact cable identity result
It does not try to be a general VDM framework or a cable policy engine.
#include <CableDiscovery.h>
PipPD pd;
Protocol protocol(pd);
CableDiscovery cableDiscovery(pd, protocol);
cableDiscovery.setLogStream(&Serial);start()update()state()busy()finished()success()result()decodeIdentityResponse(...)clearTrace()traceCount()traceEvent(...)printTrace(...)printSummary(...)
CableIdentity intentionally stays compact.
Useful fields:
responseReceivedmarkerPresentqualityproductTypeactiveCablepassiveCablecurrentCapabilityusbSpeedvendorIdproductIdbcdDevicexidhasProductVdorawVdmHeaderrawIdHeaderrawCertStatrawProductVdorawCableVdo
quality tells how usable the decoded result is:
CableIdentityUnavailableCableIdentityMarkerAbsentCableIdentityPartialCableIdentityComplete
if (cableDiscovery.start()) {
while (!cableDiscovery.finished()) {
cableDiscovery.update();
}
}CableDiscoveryuses the same PD PHY/channel asSinkPolicy- do not run active
SinkPolicy.update()and activeCableDiscovery.update()in parallel - this MVP does not perform
VCONN_SWAP - discovery success depends on real cable / source / topology behavior; some sink-side topologies will never expose an e-marker from this side
The helper distinguishes these outcomes:
CableDiscoveryNoResponse: no SOP' response before timeoutCableDiscoveryNotSupported:Discover Identityreturned NAKCableDiscoveryBusy:Discover Identityreturned BUSYCableDiscoveryParseError: a response arrived, but it was malformed or not usable for this MVPCableDiscoverySuccess+CableIdentityMarkerAbsent: a structured identity response arrived, but it does not describe a cable markerCableDiscoverySuccess+CableIdentityPartial: the response was usable, but some expected cable VDO data was missing
CableDiscovery keeps a small fixed-size trace buffer with events such as:
- discovery started
- request sent
- identity response received
- identity decoded
- marker absent
- parse partial
- no response
- not supported / busy
- parse failure
- PHY failure
#include <SinkPolicy.h>
PipPD pd;
Protocol protocol(pd);
SinkPolicy sinkPolicy(pd, protocol);
SinkPreferences preferences;
preferences.minVoltageMv = 5000;
preferences.maxVoltageMv = 20000;
preferences.preferredVoltageMv = 9000;
preferences.minCurrentMa = 500;
preferences.preferredCurrentMa = 2000;
preferences.supply = PreferPps;
preferences.preference = PreferMaxPower;
sinkPolicy.setLogStream(&Serial);
sinkPolicy.begin(preferences);In loop():
sinkPolicy.update();
if (sinkPolicy.hasContract()) {
const auto& contract = sinkPolicy.contract();
}begin(...)configure(...)setLogStream(...)update()evaluateSourceCapabilities(...)requestPps(...)requestStatus()requestPpsStatus()isPpsRequestValid(...)state()contract()hasContract()isPpsContract()lastError()preferences()sourceCapabilities()activeServiceRequest()lastServiceType()lastServiceResult()hasStatusData()statusData()hasPpsStatusData()ppsStatusData()hasAlert()alert()clearAlert()retryCount()clearTrace()traceCount()traceCapacity()traceEvent(...)printTrace(...)printPd3Summary(...)
States:
IdleUnattachedWaitAttachWaitSourceCapsEvaluateCapabilitiesSendRequestWaitAcceptWaitPsRdyReadyErrorRecovery
SinkPolicy currently supports:
- attach wait
Source_Capabilitiesreceive and decode- fixed PDO selection
- PPS APDO selection
- explicit
PreferPps -> fixedfallback Request -> Accept -> PS_RDY -> Ready- retries and timeouts
- detach recovery
- hard-reset recovery
- dynamic PPS updates on top of an active PPS contract
- on-demand
Get_Status - on-demand
Get_PPS_Status - typed
Alertreceive while staying inReady
Current PD 3.x support is intentionally narrow:
- revision-aware protocol headers and TX/RX path
- unchunked extended messages only
- typed
Status - typed
PPS Status - typed
Alert - sink-side access through
SinkPolicy
Not included in this stage:
- chunked extended-message orchestration
- EPR / AVS
- source/DRP policy
- role-swap engine
- full PD 3.x event framework
if (sinkPolicy.hasContract()) {
const bool queued = sinkPolicy.requestStatus();
}Result handling:
if (sinkPolicy.lastServiceType() == Pd3ServiceStatus &&
sinkPolicy.lastServiceResult() == Pd3ServiceResultSuccess &&
sinkPolicy.hasStatusData()) {
const auto& status = sinkPolicy.statusData();
}if (sinkPolicy.isPpsContract()) {
const bool queued = sinkPolicy.requestPpsStatus();
}Rules:
- works only when the active contract is PPS
- does not replace
requestPps(...) - timeout / not-supported does not tear down the contract
SinkPolicy stores the last typed Alert received while running the normal sink flow:
if (sinkPolicy.hasAlert()) {
const auto& alert = sinkPolicy.alert();
sinkPolicy.clearAlert();
}lastServiceResult() reports:
Pd3ServiceResultPendingPd3ServiceResultSuccessPd3ServiceResultNotSupportedPd3ServiceResultBusyPd3ServiceResultTimeoutPd3ServiceResultParseError
These outcomes are intentionally separate from the main negotiation state machine. A failed Get_Status or Get_PPS_Status does not by itself force recovery or clear the current contract.
Reject/Not_Supportedmap toPd3ServiceResultNotSupportedWaitmaps toPd3ServiceResultBusy- timeout maps to
Pd3ServiceResultTimeout - malformed or unsupported extended payload maps to
Pd3ServiceResultParseError
sinkPolicy.printPd3Summary(Serial);SinkPreferences preferences;
preferences.supply = PreferPps;
preferences.preferredVoltageMv = 9000;
preferences.preferredCurrentMa = 2000;When preferences.supply == PreferPps, SinkPolicy:
- Tries to find a matching PPS APDO
- Validates and normalizes the requested PPS voltage/current
- Builds a PPS request if valid
- Falls back to a fixed PDO if no suitable PPS APDO is available
if (sinkPolicy.isPpsContract()) {
const auto& contract = sinkPolicy.contract();
}Or check:
if (sinkPolicy.contract().type == PpsContract) {
}if (sinkPolicy.isPpsContract()) {
const bool queued = sinkPolicy.requestPps(8000, 1500);
}Rules:
- works only from
Ready - works only when the active contract is actually PPS
- the request must stay within the active APDO limits
- the request must use valid PPS steps:
- voltage step =
20 mV - current step =
50 mA
- voltage step =
Initial PPS negotiation:
- if no suitable PPS APDO exists,
SinkPolicyfalls back to fixed PDO selection - if the initial PPS request is rejected or answered with
Wait,SinkPolicyfalls back to the fixed path
Active PPS update:
- if
requestPps(...)is invalid, it returnsfalseandlastError()becomesPpsRequestInvalid - if a PPS update is rejected, the old contract stays active and
lastError()becomesPpsUpdateRejected - if a PPS update gets
Wait, the old contract stays active andlastError()becomesRequestWait - if a PPS update times out on
AcceptorPS_RDY, the old contract stays active andlastError()becomesPpsUpdateTimeout
SinkPolicy keeps a compact fixed-size ring buffer of policy events.
Stored events include:
- attach / detach
Source_Capabilitiesreceived- fixed PDO or PPS APDO selection
- request sent
Accept/Reject/Wait/PS_RDY- contract ready
- PPS update request / apply / reject / timeout
- hard reset
- timeout
- retry
PreferPps -> fixedfallback- error recovery enter / exit
API:
sinkPolicy.clearTrace();
SinkTraceEvent event;
if (sinkPolicy.traceEvent(0, event)) {
}
sinkPolicy.printTrace(Serial);Notes:
- trace is policy-level history, not a full packet capture
- the buffer is fixed-size and uses no dynamic allocation
- use it to inspect the last negotiation/recovery decisions during bring-up
Fields:
specRevisionminVoltageMvmaxVoltageMvpreferredVoltageMvminCurrentMapreferredCurrentMaminPowerMwsupplypreference
Meaning:
specRevisionis the preferred TX revision before the partner revision is knownpreferredVoltageMvis the target voltage for fixed and PPS selectionpreferredCurrentMais the preferred operating currentsupply = FixedOnlydisables PPS selectionsupply = PreferPpstries PPS first and may fall back to fixed
Fields:
validtypeobjectPositionrawPdorawRdovoltageMvoperatingCurrentMamaximumCurrentMapowerMwppsMinVoltageMvppsMaxVoltageMvppsMaxCurrentMa
Meaning:
typeisFixedContractorPpsContract- for fixed contracts, PPS fields remain zero
- for PPS contracts, the active APDO limits are exposed explicitly
Important values:
NoErrorPhyFailureProtocolFailureSourceCapsTimeoutInvalidSourceCapsNoMatchingPdoRequestTransmitFailedBusyNoPdResponseAcceptTimeoutRequestRejectedRequestWaitPsRdyTimeoutPpsNotActivePpsRequestInvalidPpsUpdateRejectedPpsUpdateTimeoutHardResetRecoveryDetachedRetryLimitReached
Practical meaning:
NoPdResponse: the source advertised Type-C current but did not answer PD messagesPpsNotActive:requestPps(...)was called when the active contract is not PPSPpsRequestInvalid: requested PPS voltage/current is outside the active APDO or uses invalid PPS step sizesPpsUpdateRejected: the source rejected a PPS update while keeping the previous contract active
- empty or malformed
Source_Capabilities:evaluateSourceCapabilities(...)fails, or runtime negotiation enters recovery withInvalidSourceCaps - unsupported PDO set only: selection fails with
NoMatchingPdo - request sent but
Acceptnever arrives:AcceptTimeout Acceptarrives butPS_RDYnever arrives:PsRdyTimeout- initial PPS request rejected or answered with
Wait: policy falls back to fixed PDO selection - active PPS update rejected, answered with
Wait, or timed out: previous contract remains active Get_Status/Get_PPS_Statustimeout: service result becomesPd3ServiceResultTimeoutand the current contract stays active- malformed
Status/PPS Statuspayload: service result becomesPd3ServiceResultParseError
PreferPpstries PPS selection first- if no suitable PPS APDO exists, policy falls back to fixed PDO selection
- if the initial PPS request is rejected or answered with
Wait, policy falls back to fixed PDO selection - active PPS update failures do not clear the current contract; they keep the previous contract and surface the reason through
lastError()
- FUSB302 PHY/core support
- PD protocol TX/RX
- revision-aware protocol TX/RX up to PD 3.0
- unchunked extended-message support for
StatusandPPS Status - typed
Alertdecode - on-demand
Get_Status - on-demand
Get_PPS_Status SOP'TX support for cable discovery- minimal structured VDM Discover Identity request/response support
Source_Capabilitiesdecode- fixed PDO negotiation
- PPS APDO negotiation
PreferPps -> fixed PDOfallback- active PPS voltage/current update
- minimal e-marker / cable identity discovery helper
- retries, timeouts, and recovery
- debug register dump
- chunked extended messages
- general VDM framework
- EPR / AVS / 240 W directions
VCONN_SWAPorchestration- full cable policy engine
- source mode policy
- DRP policy
- event system
- packet trace/history buffer
- richer high-level profile layer
- a full USB-PD stack beyond the current sink negotiation path