Skip to content

Commit d52af48

Browse files
authored
✨ (minor) Add control_endpoint::has_setup() and explain interface::handle_request error handling (libhal#200)
* ✨ usb: add has_setup() and explain handle_request error handling - Add `has_setup()` method to control endpoint with optional<bool> return for implementations that don't support setup packet detection - Add `driver_has_setup()` virtual with default nullopt implementation - Move `setup_packet` offset constants (`value_offset`, `index_offset`, `length_offset`) to top of struct and use them in all byte-access methods - Rename `p_ep_req` parameters to `p_endpoint` in `usb::interface` APIs - Expand `handle_request()` docs to explain setup-abort exception protocol - Document `operation_not_permitted` throws in `read()` and `write()` - Drop redundant `enum` keyword in `static_cast<request_type>(...)` - Drop `hal::` qualifier on `callback<>` in `on_receive()` - Add `has_setup` tests to `usb.test.cpp` and include `<optional>` * ✏️ Give clarity to docs & add missing test * ✏️ Elaborate on the throws conditions for endpoint_io
1 parent 81ab645 commit d52af48

2 files changed

Lines changed: 151 additions & 35 deletions

File tree

v4/include/libhal/usb.hpp

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -255,18 +255,47 @@ class control_endpoint : public endpoint
255255
* @param p_callback The callback function to be called when a USB request
256256
* command is received on the control endpoint.
257257
*/
258-
void on_receive(hal::callback<void(on_receive_tag)> const& p_callback)
258+
void on_receive(callback<void(on_receive_tag)> const& p_callback)
259259
{
260260
driver_on_receive(p_callback);
261261
}
262262

263+
/**
264+
* @brief Returns whether the endpoint's receive buffer contains a setup
265+
* packet.
266+
*
267+
* Returns `true` until all 8 bytes of the setup packet have been extracted
268+
* via `read()`. Returns `false` once the setup packet has been fully consumed
269+
* or if the last received packet was not a setup packet.
270+
*
271+
* Returns `std::nullopt` if the implementation does not support setup packet
272+
* detection. Enumerators that require this information should accept a
273+
* callable (as a template parameter or type-erased input parameter) to work
274+
* around legacy implementations.
275+
*
276+
* @note Safe to call from ISR context. Implementations must not throw,
277+
* call throwing functions, or perform I/O within this method.
278+
*
279+
* @return `std::nullopt` if setup packet detection is unsupported.
280+
* @return `true` if the receive buffer contains an unconsumed setup packet.
281+
* @return `false` otherwise.
282+
*/
283+
[[nodiscard]] std::optional<bool> has_setup() const noexcept
284+
{
285+
return driver_has_setup();
286+
}
287+
263288
private:
264289
virtual void driver_connect(bool p_should_connect) = 0;
265290
virtual void driver_set_address(u8 p_address) = 0;
266291
virtual void driver_write(scatter_span<byte const> p_data) = 0;
267292
virtual usize driver_read(scatter_span<byte> p_buffer) = 0;
268293
virtual void driver_on_receive(
269294
callback<void(on_receive_tag)> const& p_callback) = 0;
295+
[[nodiscard]] virtual std::optional<bool> driver_has_setup() const noexcept
296+
{
297+
return {};
298+
}
270299
};
271300

272301
/**
@@ -454,6 +483,10 @@ concept in_endpoint_type =
454483
*/
455484
struct setup_packet
456485
{
486+
constexpr static usize value_offset = 2;
487+
constexpr static usize index_offset = 4;
488+
constexpr static usize length_offset = 6;
489+
457490
/**
458491
* @brief Request type classification for setup packets
459492
*
@@ -578,7 +611,7 @@ struct setup_packet
578611
return request_type::invalid;
579612
}
580613

581-
return static_cast<enum request_type>(t);
614+
return static_cast<request_type>(t);
582615
}
583616

584617
/**
@@ -629,47 +662,50 @@ struct setup_packet
629662
*/
630663
[[nodiscard]] constexpr u16 value() const
631664
{
632-
return from_le_bytes(raw_request_bytes[2], raw_request_bytes[3]);
665+
return from_le_bytes(raw_request_bytes[value_offset],
666+
raw_request_bytes[value_offset + 1]);
633667
}
634668

635669
/**
636670
* @brief Get wValue field as raw LE bytes for USB bus transmission
637671
*/
638672
[[nodiscard]] constexpr std::span<hal::byte const> value_bytes() const
639673
{
640-
return std::span(raw_request_bytes).subspan(2, 2);
674+
return std::span(raw_request_bytes).subspan(value_offset, sizeof(u16));
641675
}
642676

643677
/**
644678
* @brief Get wIndex field in native endianness for program use
645679
*/
646680
[[nodiscard]] constexpr u16 index() const
647681
{
648-
return from_le_bytes(raw_request_bytes[4], raw_request_bytes[5]);
682+
return from_le_bytes(raw_request_bytes[index_offset],
683+
raw_request_bytes[index_offset + 1]);
649684
}
650685

651686
/**
652687
* @brief Get wIndex field as raw LE bytes for USB bus transmission
653688
*/
654689
[[nodiscard]] constexpr std::span<hal::byte const> index_bytes() const
655690
{
656-
return std::span(raw_request_bytes).subspan(4, 2);
691+
return std::span(raw_request_bytes).subspan(index_offset, sizeof(u16));
657692
}
658693

659694
/**
660695
* @brief Get wLength field in native endianness for program use
661696
*/
662697
[[nodiscard]] constexpr u16 length() const
663698
{
664-
return from_le_bytes(raw_request_bytes[6], raw_request_bytes[7]);
699+
return from_le_bytes(raw_request_bytes[length_offset],
700+
raw_request_bytes[length_offset + 1]);
665701
}
666702

667703
/**
668704
* @brief Get wLength field as raw LE bytes for USB bus transmission
669705
*/
670706
[[nodiscard]] constexpr std::span<hal::byte const> length_bytes() const
671707
{
672-
return std::span(raw_request_bytes).subspan(6, 2);
708+
return std::span(raw_request_bytes).subspan(length_offset, sizeof(u16));
673709
}
674710

675711
/**
@@ -751,10 +787,6 @@ struct setup_packet
751787
}
752788

753789
std::array<byte, 8> raw_request_bytes;
754-
755-
constexpr static usize value_offset = 2;
756-
constexpr static usize index_offset = 4;
757-
constexpr static usize length_offset = 6;
758790
};
759791

760792
/**
@@ -809,7 +841,6 @@ enum class standard_request_types : hal::byte
809841

810842
return static_cast<standard_request_types>(p_packet.request());
811843
}
812-
813844
/**
814845
* @brief Endpoint I/O interface for USB request handling
815846
*
@@ -820,8 +851,17 @@ enum class standard_request_types : hal::byte
820851
* phase of control transfers.
821852
*
822853
* The direction of data transfer depends on the setup packet:
854+
*
823855
* - Device-to-host (IN): Use `write()` to send response data to the host
824856
* - Host-to-device (OUT): Use `read()` to receive data sent by the host
857+
*
858+
* @throws hal::operation_not_permitted - If a new setup packet is received
859+
* before the current control transfer completes, this exception is thrown to
860+
* interrupt the data phase. Only the enumerator should handle this exception;
861+
* it must catch it and evaluate the new setup packet. If the underlying
862+
* control endpoint implementation does not support setup_packet detection,
863+
* this should not be thrown and the enumerator will have to make assumptions
864+
* about which packets are setup packets and which are not.
825865
*/
826866
class endpoint_io
827867
{
@@ -836,6 +876,7 @@ class endpoint_io
836876
* endpoint
837877
* @return usize - the number of bytes read into the provided buffers. Value
838878
* is 0 if there is no data available within the endpoint.
879+
* @throws hal::operation_not_permitted - See class documentation.
839880
*/
840881
usize read(scatter_span<byte> p_buffer)
841882
{
@@ -851,6 +892,7 @@ class endpoint_io
851892
* @param p_buffer - scatter span of const byte buffers containing data to
852893
* write to the endpoint
853894
* @return usize - the number of bytes written from the provided buffers
895+
* @throws hal::operation_not_permitted - See class documentation.
854896
*/
855897
usize write(scatter_span<byte const> p_buffer)
856898
{
@@ -934,13 +976,13 @@ class interface
934976
* @param p_start - the starting values for interface numbers and string
935977
* indexes. The `string` field should be cached by the interface in order to
936978
* allow `write_string_descriptor` to work correctly.
937-
* @param p_ep_req - endpoint I/O interface used to write descriptor data to
979+
* @param p_endpoint - endpoint I/O interface used to write descriptor data to
938980
* the host.
939981
*/
940982
[[nodiscard]] descriptor_count write_descriptors(descriptor_start p_start,
941-
endpoint_io& p_ep_req)
983+
endpoint_io& p_endpoint)
942984
{
943-
return driver_write_descriptors(p_start, p_ep_req);
985+
return driver_write_descriptors(p_start, p_endpoint);
944986
}
945987

946988
/**
@@ -955,56 +997,72 @@ class interface
955997
* and descriptor type fields.
956998
*
957999
* @param p_index - Which string index's descriptor should be written.
958-
* @param p_ep_req - endpoint I/O interface used to write the string
1000+
* @param p_endpoint - endpoint I/O interface used to write the string
9591001
* descriptor to the host.
9601002
*
9611003
* @returns true - if the string was located and written via the endpoint I/O.
9621004
* @returns false - if the string requested does not belong to this interface.
9631005
*/
964-
[[nodiscard]] bool write_string_descriptor(u8 p_index, endpoint_io& p_ep_req)
1006+
[[nodiscard]] bool write_string_descriptor(u8 p_index,
1007+
endpoint_io& p_endpoint)
9651008
{
966-
return driver_write_string_descriptor(p_index, p_ep_req);
1009+
return driver_write_string_descriptor(p_index, p_endpoint);
9671010
}
968-
9691011
/**
9701012
* @brief Handle USB requests directed to this interface or its endpoints
9711013
*
972-
* This method is called when the USB device receives a setup packet that
973-
* targets this interface (interface-specific requests) or one of its
1014+
* This method is called when the USB device receives a setup packet that may
1015+
* target this interface (interface-specific requests) or one of its
9741016
* endpoints (endpoint-specific requests). The interface should examine
975-
* the setup packet and handle any requests it recognizes.
1017+
* the setup packet and handle any requests it recognizes. If the setup packet
1018+
* is not for this interface, this API must return false.
1019+
*
1020+
* The interface must handle:
9761021
*
977-
* The interface may handle:
9781022
* - Standard requests specific to this interface type
9791023
* - Class-specific requests defined by the interface's USB class
9801024
* - Vendor-specific requests for custom functionality
9811025
* - Endpoint-specific requests for its endpoints
982-
* - Anything that isn't a device or configuration level request
1026+
*
1027+
* The `endpoint_io` object is constructed by the enumerator to provide
1028+
* limited access to the control endpoint.
9831029
*
9841030
* If the request has a data phase (wLength > 0), use the endpoint I/O
985-
* interface to send response data (device-to-host) or receive data
986-
* (host-to-device).
1031+
* interface to send data from device-to-host via the `write` API or
1032+
* receive data from host-to-device via the `read` API.
1033+
*
1034+
* When a new setup packet is received from the HOST, the enumerator ensures
1035+
* that `hal::operation_not_permitted` is thrown from all `endpoint_io` APIs.
1036+
* Implementations of `driver_handle_request` MUST NOT catch this exception;
1037+
* it must propagate to the enumerator for handling. Correct use of RAII to
1038+
* undo state changes is advised. If cleanup requires a catch block, the
1039+
* exception must be rethrown via `throw;` before exiting.
9871040
*
9881041
* @param p_setup - Setup request from the host.
989-
* @param p_ep_req - endpoint I/O interface for reading or writing data
990-
* during the data phase of the control transfer.
1042+
* @param p_endpoint - endpoint I/O interface for reading or writing data to
1043+
* the host via the control endpoint during the data phase of the control
1044+
* transfer.
9911045
* @return true - if the request was handled by the interface.
9921046
* @return false - if the request could not be handled by interface.
1047+
* @throws hal::operation_not_permitted - If a new setup packet is received
1048+
* before the current control transfer completes. This exception must not be
1049+
* caught by the implementation; it is intended for the enumerator.
9931050
*/
994-
bool handle_request(setup_packet const& p_setup, endpoint_io& p_ep_req)
1051+
bool handle_request(setup_packet const& p_setup, endpoint_io& p_endpoint)
9951052
{
996-
return driver_handle_request(p_setup, p_ep_req);
1053+
return driver_handle_request(p_setup, p_endpoint);
9971054
}
9981055

9991056
virtual ~interface() = default;
10001057

10011058
private:
1002-
virtual descriptor_count driver_write_descriptors(descriptor_start p_start,
1003-
endpoint_io& p_ep_req) = 0;
1059+
virtual descriptor_count driver_write_descriptors(
1060+
descriptor_start p_start,
1061+
endpoint_io& p_endpoint) = 0;
10041062
virtual bool driver_write_string_descriptor(u8 p_index,
1005-
endpoint_io& p_ep_req) = 0;
1063+
endpoint_io& p_endpoint) = 0;
10061064
virtual bool driver_handle_request(setup_packet const& p_setup,
1007-
endpoint_io& p_ep_req) = 0;
1065+
endpoint_io& p_endpoint) = 0;
10081066
};
10091067
} // namespace hal::v5::usb
10101068

v4/tests/usb.test.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
#include <array>
16+
#include <optional>
1617

1718
#include <libhal/error.hpp>
1819
#include <libhal/scatter_span.hpp>
@@ -62,6 +63,7 @@ class mock_usb_control_endpoint : public control_endpoint
6263
usize m_read_result{ 0 };
6364
callback<void(on_receive_tag)> m_receive_callback{};
6465
bool m_on_receive_called{ false };
66+
std::optional<bool> m_has_setup_state{};
6567

6668
private:
6769
[[nodiscard]] endpoint_info driver_info() const override
@@ -106,6 +108,11 @@ class mock_usb_control_endpoint : public control_endpoint
106108
{
107109
m_endpoint.reset();
108110
}
111+
112+
std::optional<bool> driver_has_setup() const noexcept override
113+
{
114+
return m_has_setup_state;
115+
}
109116
};
110117

111118
// Mock implementation for usb_in_endpoint
@@ -366,6 +373,57 @@ boost::ut::suite<"usb_control_endpoint_test"> control_endpoint_test = []() {
366373
endpoint.reset();
367374
expect(that % endpoint.m_endpoint.m_reset_called);
368375
};
376+
377+
"mock_usb_control_endpoint has_setup test"_test = []() {
378+
mock_usb_control_endpoint endpoint;
379+
endpoint.m_has_setup_state = std::nullopt;
380+
auto const val1 = endpoint.has_setup();
381+
expect(std::nullopt == val1);
382+
383+
endpoint.m_has_setup_state = true;
384+
auto const val2 = endpoint.has_setup();
385+
expect(that % true == *val2);
386+
387+
endpoint.m_has_setup_state = false;
388+
auto const val3 = endpoint.has_setup();
389+
expect(that % false == *val3);
390+
};
391+
392+
"mock_usb_control_endpoint default has_setup test"_test = []() {
393+
struct test_ctrl_ep : public hal::usb::control_endpoint
394+
{
395+
void driver_connect(bool) override
396+
{
397+
}
398+
void driver_set_address(u8) override
399+
{
400+
}
401+
void driver_write(scatter_span<byte const>) override
402+
{
403+
}
404+
usize driver_read(scatter_span<byte>) override
405+
{
406+
return 0;
407+
}
408+
void driver_on_receive(callback<void(on_receive_tag)> const&) override
409+
{
410+
}
411+
[[nodiscard]] endpoint_info driver_info() const override
412+
{
413+
return {};
414+
}
415+
void driver_stall(bool) override
416+
{
417+
}
418+
void driver_reset() override
419+
{
420+
}
421+
// driver_has_setup skipped!
422+
} endpoint;
423+
424+
auto const val1 = endpoint.has_setup();
425+
expect(std::nullopt == val1);
426+
};
369427
};
370428

371429
// Test for usb_in_endpoint interface

0 commit comments

Comments
 (0)