ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
pd.cpp
Go to the documentation of this file.
1#include "pd.h"
2
3#include <cstring>
4
5#include "esphome/core/hal.h"
6#include "esphome/core/log.h"
7
8namespace esphome::fusb302b {
9
10ESPHOME_LOG_TAG(TAG, "fusb302b.pd");
11
12static constexpr uint32_t AMS_TIMEOUT_MS = 2000;
13// tSinkRequest: delay before repeating a request that the source answered with Wait
14static constexpr uint32_t SINK_REQUEST_MS = 100;
15static constexpr uint8_t MAX_WAIT_RETRIES = 10;
16
17// USB PD 6.4.1.3.1 Sink Fixed Supply Power Data Object flags; only the first (5 V) object carries them
18static constexpr uint32_t SINK_PDO_HIGHER_CAPABILITY = 1u << 28;
19static constexpr uint32_t SINK_PDO_USB_COMMS_CAPABLE = 1u << 26;
20// Operational current advertised for every sink object: 3 A (300 x 10 mA)
21static constexpr uint16_t SINK_PDO_CURRENT = 300;
22
23// USB PD 6.4.1.2.3 Source Fixed Supply Power Data Object; other PDO types only report their type
24static PdContract parse_pdo(uint32_t pdo) {
25 PdContract info{};
26 info.type = static_cast<PdPdoType>(pdo >> 30);
27 if (info.type == PD_PDO_TYPE_FIXED_SUPPLY) {
28 info.max_v = (pdo >> 10) & 0x3FF;
29 info.max_i = pdo & 0x3FF;
30 }
31 return info;
32}
33
34// USB PD 6.4.1.3.1 Sink Fixed Supply Power Data Object; voltage in 50 mV units, current in 10 mA units
35static constexpr uint32_t sink_fixed_pdo(uint16_t voltage, uint16_t current) {
36 return (static_cast<uint32_t>(PD_PDO_TYPE_FIXED_SUPPLY) << 30) | (static_cast<uint32_t>(voltage & 0x3FF) << 10) |
37 (current & 0x3FF);
38}
39
40void PdMsg::set_header(uint16_t header) {
41 this->type = header & 0x1F;
42 this->spec_rev = static_cast<PdSpecRevision>((header >> 6) & 0x3);
43 this->id = (header >> 9) & 0x7;
44 this->num_of_obj = (header >> 12) & 0x7;
45 this->extended = (header >> 15) & 0x1;
46}
47
48uint16_t PdMsg::get_coded_header() const {
49 // Data role UFP (bit 5 = 0), power role sink (bit 8 = 0)
50 return static_cast<uint16_t>(this->type & 0x1F) | (static_cast<uint16_t>(this->spec_rev) << 6) |
51 (static_cast<uint16_t>(this->id & 0x7) << 9) | (static_cast<uint16_t>(this->num_of_obj & 0x7) << 12) |
52 (static_cast<uint16_t>(this->extended) << 15);
53}
54
56 PdMsg msg;
57 msg.type = type;
58 msg.id = this->msg_counter_ % 8;
59 return msg;
60}
61
63 PdMsg msg;
64 msg.type = type;
65 msg.id = this->msg_counter_ % 8;
66 msg.num_of_obj = len;
67 memcpy(msg.data_objects, objects, len * sizeof(uint32_t));
68 return msg;
69}
70
72 this->last_received_msg_id_ = 255;
73 this->msg_counter_ = 0;
74 this->active_ams_ = false;
75 this->ps_rdy_expected_ = false;
76 this->retry_request_pending_ = false;
77 this->wait_retries_ = 0;
78}
79
81 if (this->state_.exchange(state) != state)
82 this->on_state_changed();
83}
84
86 this->accepted_contract_ = contract;
87 const uint32_t packed = PackedContract::from(contract).value;
88 if (this->contract_.exchange(packed) != packed)
89 this->on_state_changed();
90}
91
93 this->accepted_contract_ = {};
94 if (this->contract_.exchange(0) != 0)
95 this->on_state_changed();
96}
97
99 this->accepted_contract_ = contract;
100 const uint32_t packed = PackedContract::from(contract).value;
101 // The contract is stored first, so a reader that sees the new state also sees the new contract
102 const bool contract_changed = this->contract_.exchange(packed) != packed;
103 const bool state_changed = this->state_.exchange(state) != state;
104 if (contract_changed || state_changed)
105 this->on_state_changed();
106}
107
108void PowerDelivery::set_ams_(bool active) {
109 this->active_ams_ = active;
110 // A new sequence has no accepted request yet, and an ended one can no longer complete
111 this->ps_rdy_expected_ = false;
112 if (active)
113 this->ams_start_ = millis();
114}
115
117 if (this->active_ams_ && millis() - this->ams_start_ > AMS_TIMEOUT_MS) {
118 this->set_ams_(false);
119 // Every sequence carries a request. In TRANSITION, check_transition_timeout_() handles the missing PS_RDY.
121 ESP_LOGW(TAG, "Source did not complete the request in time");
122 this->request_failed_();
123 }
124 }
125 return this->active_ams_;
126}
127
129 if (!this->retry_request_pending_ || millis() - this->wait_received_ms_ < SINK_REQUEST_MS)
130 return;
131 if (this->send_request_()) {
132 this->retry_request_pending_ = false;
133 } else if (this->wait_retries_ < MAX_WAIT_RETRIES) {
134 // Try again after another tSinkRequest; failed attempts count against the same limit as Wait replies
135 this->wait_retries_++;
136 this->wait_received_ms_ = millis();
137 } else {
138 ESP_LOGW(TAG, "Giving up repeating the request");
139 this->retry_request_pending_ = false;
140 this->request_failed_();
141 }
142}
143
144bool PowerDelivery::send_request_() {
145 this->set_ams_(true);
146 if (this->send_message(this->make_data_msg_(PD_DATA_REQUEST, &this->last_rdo_, 1)))
147 return true;
148 ESP_LOGW(TAG, "Sending the request failed");
149 this->set_ams_(false);
150 return false;
151}
152
153void PowerDelivery::request_failed_() {
154 // A contract already in place stays valid. Without one, report that negotiation is over at default power.
157 this->on_request_failed();
158}
159
161 // The AMS that sent the request only ends on PS_RDY, so an ended AMS while in transition means a timeout
162 if (this->state_ != PdState::PD_STATE_TRANSITION || this->check_ams_())
163 return;
164 ESP_LOGW(TAG, "Source did not report the new supply ready, falling back to default power");
166 this->on_transition_timeout();
167}
168
170 // USB PD 6.12.2.2: the counter also moves on after a failed transmission. The source may have received the
171 // message and only its GoodCRC was lost, so reusing the MessageID would make it drop the next one as a repeat.
172 ESP_LOGD(TAG, "Message %u not acknowledged", this->msg_counter_ % 8);
173 this->msg_counter_++;
174}
175
177 if (msg.num_of_obj == 0 && msg.type == PD_CNTRL_GOODCRC) {
178 // A GoodCRC carries the MessageID it acknowledges; a duplicate or stray one must not advance the counter
179 if (msg.id != this->msg_counter_ % 8) {
180 ESP_LOGD(TAG, "Ignoring GoodCRC for MessageID %u, expected %u", msg.id, this->msg_counter_ % 8);
181 return;
182 }
183 // Our last message was received, so the next one gets a new MessageID
184 this->msg_counter_++;
185 return;
186 }
187 // Soft_Reset always has MessageID 0, so it must not be dropped as a retransmission of the last message
188 if (msg.num_of_obj == 0 && msg.type == PD_CNTRL_SOFT_RESET) {
189 this->handle_soft_reset_(msg);
190 return;
191 }
192 // Retransmissions repeat the MessageID of a message that was already handled
193 if (msg.id == this->last_received_msg_id_)
194 return;
195 this->last_received_msg_id_ = msg.id;
196
197 if (msg.num_of_obj == 0) {
198 this->handle_control_message_(msg);
199 } else {
200 this->handle_data_message_(msg);
201 }
202}
203
204void PowerDelivery::handle_data_message_(const PdMsg &msg) {
205 if (msg.extended || msg.type != PD_DATA_SOURCE_CAP) {
206 // BIST and Vendor_Defined messages must not be answered with Reject, so unsupported data messages are ignored
207 ESP_LOGV(TAG, "Ignoring data message type %u%s", msg.type,
208 msg.extended ? LOG_STR_LITERAL(" (extended)") : LOG_STR_LITERAL(""));
209 return;
210 }
212 this->respond_to_source_caps_(msg);
213}
214
215void PowerDelivery::handle_control_message_(const PdMsg &msg) {
216 switch (msg.type) {
217 case PD_CNTRL_ACCEPT:
218 if (!this->check_ams_()) {
219 ESP_LOGD(TAG, "Ignoring Accept outside a request");
220 break;
221 }
222 this->ps_rdy_expected_ = true;
223 if (this->requested_contract_ != this->accepted_contract_)
224 this->set_state_and_contract_(PdState::PD_STATE_TRANSITION, this->requested_contract_);
225 break;
226 case PD_CNTRL_PS_RDY:
227 // check_ams_() first, so a PS_RDY that arrives after the sequence timed out is ignored too
228 if (!this->check_ams_() || !this->ps_rdy_expected_) {
229 ESP_LOGD(TAG, "Ignoring PS_RDY without an accepted request");
230 break;
231 }
232 this->set_ams_(false);
234 break;
235 case PD_CNTRL_REJECT:
236 if (!this->check_ams_()) {
237 ESP_LOGD(TAG, "Ignoring Reject outside a request");
238 break;
239 }
240 ESP_LOGW(TAG, "Source rejected the request");
241 this->set_ams_(false);
242 this->request_failed_();
243 break;
244 case PD_CNTRL_WAIT:
245 if (!this->check_ams_()) {
246 ESP_LOGD(TAG, "Ignoring Wait outside a request");
247 break;
248 }
249 this->set_ams_(false);
250 if (this->wait_retries_ < MAX_WAIT_RETRIES) {
251 ESP_LOGD(TAG, "Source asked to wait, repeating the request");
252 this->wait_retries_++;
253 this->retry_request_pending_ = true;
254 this->wait_received_ms_ = millis();
255 } else {
256 ESP_LOGW(TAG, "Source kept answering Wait, giving up");
257 this->request_failed_();
258 }
259 break;
260 case PD_CNTRL_PING:
262 break;
264 // 5 V first, then the requested voltage. Higher Capability tells the source that 5 V is not enough for
265 // full operation.
266 const uint16_t request_v = this->request_voltage_ * 20; // 50 mV units
267 uint32_t pdos[2];
268 uint8_t count = 0;
269 pdos[count++] = sink_fixed_pdo(100, SINK_PDO_CURRENT) | SINK_PDO_USB_COMMS_CAPABLE |
270 (request_v > 100 ? SINK_PDO_HIGHER_CAPABILITY : 0);
271 if (request_v > 100)
272 pdos[count++] = sink_fixed_pdo(request_v, SINK_PDO_CURRENT);
274 break;
275 }
276 default:
277 // Messages are sent as PD 2.0, which has no Not_Supported message
279 break;
280 }
281}
282
283void PowerDelivery::handle_soft_reset_(const PdMsg &msg) {
284 this->reset_protocol_();
285 this->last_received_msg_id_ = msg.id;
286 // If this fails the source escalates to a hard reset. The hook still runs, so the PHY starts waiting for source
287 // capabilities and recovers on its own if neither follows.
289 ESP_LOGW(TAG, "Accepting the soft reset failed");
290 }
291 // A soft reset keeps the power level, but the outcome of a transition in progress is unknown
295}
296
297void PowerDelivery::respond_to_source_caps_(const PdMsg &msg) {
298 // Pick the highest fixed supply PDO that does not exceed the requested voltage.
299 // The first PDO is always the 5 V fixed supply.
300 const uint16_t request_v = this->request_voltage_ * 20; // 50 mV units
301 PdContract selected{};
302 uint8_t position = 0; // 1-based object position, 0 = none
303 for (uint8_t idx = 0; idx < msg.num_of_obj; idx++) {
304 PdContract info = parse_pdo(msg.data_objects[idx]);
305 if (info.type != PD_PDO_TYPE_FIXED_SUPPLY)
306 continue;
307 if (position == 0 || (info.max_v <= request_v && info.max_v > selected.max_v)) {
308 selected = info;
309 position = idx + 1;
310 }
311 }
312
313 // USB PD 6.4.2 Request Data Object: no USB suspend (bit 24), USB communications capable (bit 25)
314 uint32_t rdo = (1u << 24) | (1u << 25);
315 if (position == 0) {
316 ESP_LOGW(TAG, "No usable PDO in source capabilities, requesting 5 V");
317 // Object 1 (5 V fixed supply), 300 mA maximum, 100 mA operating current
318 selected = PdContract{PD_PDO_TYPE_FIXED_SUPPLY, 100, 30};
319 position = 1;
320 rdo |= (30u << 0) | (10u << 10);
321 } else {
322 // Request the full current the source offers
323 const uint32_t current = selected.max_i;
324 rdo |= (current << 0) | (current << 10);
325 }
326 rdo |= static_cast<uint32_t>(position) << 28;
327 ESP_LOGD(TAG, "Requesting PDO %u: %.2f V", position, selected.max_v * 0.05f);
328 this->requested_contract_ = selected;
329 this->last_rdo_ = rdo;
330 this->retry_request_pending_ = false;
331 this->wait_retries_ = 0;
332 // A repeat would come too late: the source resets if it gets no Request within tSenderResponse (24-30 ms)
333 if (!this->send_request_())
334 this->request_failed_();
335}
336
337} // namespace esphome::fusb302b
PdMsg make_control_msg_(PdControlMsgType type) const
Definition pd.cpp:55
virtual void on_source_caps_received()=0
Called when source capabilities arrive, before the request for them is sent.
void set_contract_(const PdContract &contract)
Definition pd.cpp:85
std::atomic< uint8_t > request_voltage_
Definition pd.h:157
PdMsg make_data_msg_(PdDataMsgType type, const uint32_t *objects, uint8_t len) const
Definition pd.cpp:62
void set_ams_(bool active)
Definition pd.cpp:108
std::atomic< uint32_t > contract_
Definition pd.h:156
void set_state_(PdState state)
Definition pd.cpp:80
std::atomic< PdState > state_
Definition pd.h:155
void handle_message_(const PdMsg &msg)
Definition pd.cpp:176
void check_request_retry_()
Sends the last request again once tSinkRequest has passed after the source answered Wait.
Definition pd.cpp:128
virtual void on_state_changed()=0
Called after state_ or contract_ changed, to let the main loop publish it.
virtual bool send_message(const PdMsg &msg)=0
Returns false when the message could not be handed to the chip; the PHY logs the failure.
void handle_transmit_failed_()
Called by the PHY when the chip gave up on a message after its retries without receiving a GoodCRC.
Definition pd.cpp:169
virtual void on_soft_reset_received()=0
Called after the source soft reset the protocol; it sends new source capabilities next.
virtual void on_request_failed()=0
Called when a request ended without a new contract: it was rejected, the source kept answering Wait o...
bool check_ams_()
Returns true while an atomic message sequence is in progress.
Definition pd.cpp:116
void set_state_and_contract_(PdState state, const PdContract &contract)
Stores both before calling on_state_changed() once, so the main loop never sees the new state with th...
Definition pd.cpp:98
void check_transition_timeout_()
Falls back to default power when the source accepted a request but did not report the new supply read...
Definition pd.cpp:160
virtual void on_transition_timeout()=0
Called after an accepted request was not followed by PS_RDY in time and the state fell back to defaul...
float position
Definition cover.h:0
uint8_t type
bool state
Definition fan.h:2
unsigned count
Definition gif.cpp:51
@ PD_DATA_SOURCE_CAP
Definition pd.h:22
@ PD_DATA_REQUEST
Definition pd.h:23
@ PD_DATA_SINK_CAP
Definition pd.h:24
ESPHOME_LOG_TAG(TAG, "fusb302b")
@ PD_STATE_PD_TIMEOUT
Negotiation is over without a PD contract (no capabilities, or the request was not accepted); the sou...
@ PD_CNTRL_PING
Definition pd.h:31
@ PD_CNTRL_SOFT_RESET
Definition pd.h:36
@ PD_CNTRL_REJECT
Definition pd.h:30
@ PD_CNTRL_GOODCRC
Definition pd.h:28
@ PD_CNTRL_WAIT
Definition pd.h:35
@ PD_CNTRL_GET_SINK_CAP
Definition pd.h:34
@ PD_CNTRL_NOT_SUPPORTED
Definition pd.h:37
@ PD_CNTRL_ACCEPT
Definition pd.h:29
@ PD_CNTRL_PS_RDY
Definition pd.h:32
@ PD_PDO_TYPE_FIXED_SUPPLY
Definition pd.h:41
const void size_t len
Definition hal.h:68
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t
static PackedContract from(const PdContract &contract)
Definition pd.h:83
PdSpecRevision spec_rev
Definition pd.h:93
uint8_t num_of_obj
Definition pd.h:95
void set_header(uint16_t header)
Definition pd.cpp:40
uint16_t get_coded_header() const
Definition pd.cpp:48
uint32_t data_objects[PD_MAX_NUM_DATA_OBJECTS]
Definition pd.h:97