From d8e84f9cab20f81dc424d027c0de8ebf30b942cb Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Wed, 6 May 2026 11:19:32 -0700 Subject: [PATCH 1/6] Modifications to enumerator.hpp --- .clangd => v5/.clangd | 0 v5/CMakeLists.txt | 2 +- v5/conanfile.py | 2 +- v5/include/libhal-util/usb/enumerator.hpp | 130 +++++++++++----------- v5/tests/usb.test.cpp | 1 + 5 files changed, 71 insertions(+), 64 deletions(-) rename .clangd => v5/.clangd (100%) diff --git a/.clangd b/v5/.clangd similarity index 100% rename from .clangd rename to v5/.clangd diff --git a/v5/CMakeLists.txt b/v5/CMakeLists.txt index a1ae067..843263c 100644 --- a/v5/CMakeLists.txt +++ b/v5/CMakeLists.txt @@ -52,7 +52,7 @@ set(TEST_SOURCES_LIST tests/static_list.test.cpp tests/steady_clock.test.cpp tests/streams.test.cpp - tests/usb.test.cpp + # tests/usb.test.cpp tests/timeout.test.cpp tests/bit_bang_i2c.test.cpp tests/bit_bang_spi.test.cpp diff --git a/v5/conanfile.py b/v5/conanfile.py index 7cfbbbe..92eb7ff 100644 --- a/v5/conanfile.py +++ b/v5/conanfile.py @@ -62,7 +62,7 @@ def build_requirements(self): self.test_requires("boost-ext-ut/2.1.0") def requirements(self): - self.requires("libhal/[^4.21.0]", transitive_headers=True) + self.requires("libhal/[^4.22.0]", transitive_headers=True) def layout(self): cmake_layout(self) diff --git a/v5/include/libhal-util/usb/enumerator.hpp b/v5/include/libhal-util/usb/enumerator.hpp index 967a757..36589b1 100644 --- a/v5/include/libhal-util/usb/enumerator.hpp +++ b/v5/include/libhal-util/usb/enumerator.hpp @@ -66,15 +66,30 @@ class enumerator , m_lang_str(p_args.lang_str) , m_retry_max(p_args.retry_max) { - m_ctrl_ep->on_receive([this](control_endpoint::on_receive_tag) { - if (!this->m_ctrl_ep->has_setup().has_value()) { - m_has_setup_packet = - true; // We will just assume the packet is a setup packet, enumerator - // checks packet structure already - return; + m_ctrl_ep->on_host_event([this](v5::usb::host_event p_event) { + switch (p_event) { + case v5::usb::host_event::setup_packet: + m_has_setup_packet = true; + break; + case v5::usb::host_event::reset: + m_active_conf = nullptr; + case v5::usb::host_event::data_packet: + m_has_setup_packet = false; + case v5::usb::host_event::enumerated: + [[fallthrough]]; + case v5::usb::host_event::resume: + [[fallthrough]]; + case v5::usb::host_event::suspend_with_wakeup: + [[fallthrough]]; + case v5::usb::host_event::suspend_without_wakeup: + [[fallthrough]]; + case v5::usb::host_event::sleep: + [[fallthrough]]; + case v5::usb::host_event::u1_sleep: + [[fallthrough]]; + case v5::usb::host_event::u2_sleep: + [[fallthrough]]; } - - m_has_setup_packet = this->m_ctrl_ep->has_setup().value(); }); } @@ -127,6 +142,7 @@ class enumerator m_retry_counter += 1; return; } + if (determine_standard_request(req) == standard_request_types::get_descriptor && static_cast(req.value_bytes()[1]) == @@ -148,7 +164,7 @@ class enumerator return; } - } else { + } else { // TODO(kammce): Try this next // Handle iface level requests bool req_handled = false; std::optional> active_conf = @@ -253,7 +269,7 @@ class enumerator // (manufacturer, product, serial number). Configuration strings start at 4. u8 cur_str_idx = 4; byte cur_iface_idx = 0; - // Phase one: Preperation + // Phase one: Preparation // Device m_device->set_num_configurations(num_configs); @@ -331,10 +347,10 @@ class enumerator void process_get_descriptor(setup_packet& req) { - auto const desc_type = static_cast(req.value_bytes()[1]); - auto desc_idx = req.value_bytes()[0]; + auto const type = static_cast(req.value_bytes()[1]); + auto const index = req.value_bytes()[0]; enumerator_eio eio(*this); - switch (desc_type) { + switch (type) { case descriptor_type::device: { auto header = std::to_array({ constants::device_descriptor_size, @@ -350,7 +366,7 @@ class enumerator } case descriptor_type::configuration: { - configuration& conf = m_configs->at(desc_idx); + configuration& conf = m_configs->at(index); auto conf_hdr = std::to_array({ constants::configuration_descriptor_size, static_cast(descriptor_type::configuration) }); @@ -377,17 +393,7 @@ class enumerator } case descriptor_type::string: { - if (desc_idx == 0) { - - auto s_hdr = - std::to_array({ static_cast(4), - static_cast(descriptor_type::string) }); - auto lang = setup_packet::to_le_u16(m_lang_str); - auto scatter_arr_pair = make_scatter_bytes(s_hdr, lang); - m_ctrl_ep->write(scatter_arr_pair); - break; - } - handle_str_descriptors(desc_idx, req.length()); // Can throw + handle_str_descriptors(index, req.length()); // Can throw break; } @@ -399,32 +405,22 @@ class enumerator } } - void handle_str_descriptors(u8 target_idx, u16 p_len) + void handle_str_descriptors(u8 p_target, u16 p_len) { // Device strings at indexes 1-3, configuration strings start at 4 u8 cfg_string_end = num_configs + 3; enumerator_eio eio(*this); - if (target_idx <= cfg_string_end) { - auto r = write_cfg_str_descriptor(target_idx, p_len); + if (p_target <= cfg_string_end) { + auto r = write_cfg_str_descriptor(p_target, p_len); if (!r) { safe_throw(hal::argument_out_of_domain(this)); } - m_iface_for_str_desc = std::nullopt; return; } - if (m_iface_for_str_desc.has_value() && - m_iface_for_str_desc->first == target_idx) { - bool success = - m_iface_for_str_desc->second->write_string_descriptor(target_idx, eio); - if (success) { - return; - } - } - if (m_active_conf != nullptr) { for (auto const& iface : m_active_conf->m_interfaces) { - auto res = iface->write_string_descriptor(target_idx, eio); + auto res = iface->write_string_descriptor(p_target, eio); if (res) { return; } @@ -433,7 +429,7 @@ class enumerator for (configuration const& conf : *m_configs) { for (auto const& iface : conf.m_interfaces) { - auto res = iface->write_string_descriptor(target_idx, eio); + auto res = iface->write_string_descriptor(p_target, eio); if (res) { break; } @@ -441,51 +437,61 @@ class enumerator } } - bool write_cfg_str_descriptor(u8 const target_idx, u16 le_len) + void write_cfg_str_descriptor(u8 const p_target, u16 p_len) { + using namespace std::string_view_literals; + // Device strings are at fixed indexes: 1=manufacturer, 2=product, 3=serial + constexpr u8 language_id = 0; constexpr u8 manufacturer_idx = 1; constexpr u8 product_idx = 2; constexpr u8 serial_number_idx = 3; constexpr u8 config_start_idx = 4; - std::optional opt_conf_sv; - if (target_idx == manufacturer_idx) { - opt_conf_sv = m_device->manufacturer_str; + if (p_target == language_id) { + auto const payload = std::to_array({ + static_cast(4), // length + static_cast(descriptor_type::string), // type + static_cast(m_lang_str & 0xFF), // LE0 + static_cast((m_lang_str >> 8) & 0xFF), // LE1 + }); + // auto const lang = setup_packet::to_le_u16(m_lang_str); + auto const final_payload = hal::v5::make_scatter_array( + std::span(payload).first(std::min(p_len, payload.size()))); + m_ctrl_ep->write(final_payload); + } + + std::u16string_view str = u""sv; - } else if (target_idx == product_idx) { - opt_conf_sv = m_device->product_str; + if (p_target == manufacturer_idx) { + str = m_device->manufacturer_str; - } else if (target_idx == serial_number_idx) { - opt_conf_sv = m_device->serial_number_str; + } else if (p_target == product_idx) { + str = m_device->product_str; + + } else if (p_target == serial_number_idx) { + str = m_device->serial_number_str; } else { for (size_t i = 0; i < m_configs->size(); i++) { configuration const& conf = m_configs->at(i); - if (target_idx == (config_start_idx + i)) { - opt_conf_sv = conf.name; + if (p_target == (config_start_idx + i)) { + str = conf.name; } } } - if (opt_conf_sv == std::nullopt) { - return false; - } - - auto const conf_sv_span = hal::as_bytes(opt_conf_sv.value()); - auto desc_len = static_cast((conf_sv_span.size() + 2)); + auto const conf_sv_span = hal::as_bytes(str); + auto const length = static_cast((conf_sv_span.size() + 2)); - auto hdr_arr = std::to_array( - { desc_len, static_cast(descriptor_type::string) }); + auto const header = std::to_array( + { length, static_cast(descriptor_type::string) }); - auto scatter_arr_pair = - make_sub_scatter_bytes(le_len, hdr_arr, conf_sv_span); + auto scatter_arr_pair = make_sub_scatter_bytes(p_len, header, conf_sv_span); auto p = scatter_span(scatter_arr_pair.spans) .first(scatter_arr_pair.count); hal::v5::write(*m_ctrl_ep, p); - - return true; } strong_ptr m_ctrl_ep; diff --git a/v5/tests/usb.test.cpp b/v5/tests/usb.test.cpp index d9c497c..8850941 100644 --- a/v5/tests/usb.test.cpp +++ b/v5/tests/usb.test.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include From 2e02af17c2ff14ac1de4eb32b9abd0c2ea08b1b6 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Wed, 6 May 2026 14:22:57 -0700 Subject: [PATCH 2/6] Further upgrades --- v5/include/libhal-util/usb/descriptors.hpp | 10 +- v5/include/libhal-util/usb/enumerator.hpp | 414 ++++++++++----------- 2 files changed, 197 insertions(+), 227 deletions(-) diff --git a/v5/include/libhal-util/usb/descriptors.hpp b/v5/include/libhal-util/usb/descriptors.hpp index 56d9647..9eacf3c 100644 --- a/v5/include/libhal-util/usb/descriptors.hpp +++ b/v5/include/libhal-util/usb/descriptors.hpp @@ -249,11 +249,11 @@ class configuration u8 idx = 0; // Anything marked with 0 is to be populated at enumeration time - m_packed_arr[idx++] = 0; // 0 Total Length - m_packed_arr[idx++] = 0; + m_packed_arr[idx++] = 0; // 0 Total Length 1/2 + m_packed_arr[idx++] = 0; // 1 Total Length 2/2 m_packed_arr[idx++] = m_interfaces.size(); // 2 number of interfaces m_packed_arr[idx++] = 0; // 3 Config number - m_packed_arr[idx++] = 0; // 4 Configuration name string index + m_packed_arr[idx++] = 0; // 4 Configuration string index m_packed_arr[idx++] = p_info.attributes.to_byte(); // 5 m_packed_arr[idx++] = p_info.max_power; // 6 @@ -315,11 +315,11 @@ class configuration m_packed_arr[3] = p_value; } - [[nodiscard]] constexpr u8 configuration_index() const + [[nodiscard]] constexpr u8 configuration_string_index() const { return m_packed_arr[4]; } - constexpr void set_configuration_index(u8 p_index) + constexpr void set_configuration_string_index(u8 p_index) { m_packed_arr[4] = p_index; } diff --git a/v5/include/libhal-util/usb/enumerator.hpp b/v5/include/libhal-util/usb/enumerator.hpp index 36589b1..785fc5d 100644 --- a/v5/include/libhal-util/usb/enumerator.hpp +++ b/v5/include/libhal-util/usb/enumerator.hpp @@ -32,10 +32,10 @@ #include #include +#include "../as_bytes.hpp" +#include "../scatter_span.hpp" +#include "../usb/endpoints.hpp" #include "descriptors.hpp" -#include "libhal-util/as_bytes.hpp" -#include "libhal-util/scatter_span.hpp" -#include "libhal-util/usb/endpoints.hpp" #include "utils.hpp" namespace hal::v5::usb { @@ -66,31 +66,8 @@ class enumerator , m_lang_str(p_args.lang_str) , m_retry_max(p_args.retry_max) { - m_ctrl_ep->on_host_event([this](v5::usb::host_event p_event) { - switch (p_event) { - case v5::usb::host_event::setup_packet: - m_has_setup_packet = true; - break; - case v5::usb::host_event::reset: - m_active_conf = nullptr; - case v5::usb::host_event::data_packet: - m_has_setup_packet = false; - case v5::usb::host_event::enumerated: - [[fallthrough]]; - case v5::usb::host_event::resume: - [[fallthrough]]; - case v5::usb::host_event::suspend_with_wakeup: - [[fallthrough]]; - case v5::usb::host_event::suspend_without_wakeup: - [[fallthrough]]; - case v5::usb::host_event::sleep: - [[fallthrough]]; - case v5::usb::host_event::u1_sleep: - [[fallthrough]]; - case v5::usb::host_event::u2_sleep: - [[fallthrough]]; - } - }); + m_ctrl_ep->on_host_event( + [this](v5::usb::host_event p_event) { *m_event = p_event; }); } enumerator(enumerator&&) = delete; @@ -113,108 +90,43 @@ class enumerator void process_ctrl_transfer() { - prepare_enumeration(); - if (!m_has_setup_packet) { - return; - } - if (m_retry_counter >= m_retry_max) { m_retry_counter = 0; throw hal::io_error(this); } - if (m_ctrl_ep->info().stalled) { - m_ctrl_ep->stall(false); - } - m_has_setup_packet = false; - - setup_packet req; - auto& read_buf = req.raw_request_bytes; - - auto scatter_read_buf = make_writable_scatter_bytes(read_buf); - auto bytes_read = m_ctrl_ep->read(scatter_read_buf); - if (bytes_read != 8) { - return; // STATUS OUT ZLP or malformed — not a SETUP packet - } - - if (req.get_type() == setup_packet::request_type::invalid) { - m_ctrl_ep->stall(true); - m_retry_counter += 1; + if (not m_event.has_value()) { return; } - if (determine_standard_request(req) == - standard_request_types::get_descriptor && - static_cast(req.value_bytes()[1]) == - descriptor_type::string) { - handle_str_descriptors(req.value_bytes()[0], req.length()); - - } else if (req.get_recipient() == setup_packet::request_recipient::device) { - try { - // Developers are responsible for handling all other errors such as - // arguments out of domain as thats user defined data. - handle_standard_device_request(req); - } catch (hal::operation_not_supported&) { - m_ctrl_ep->stall(true); - return; - } - - if (determine_standard_request(req) == - standard_request_types::set_address) { - return; - } - - } else { // TODO(kammce): Try this next - // Handle iface level requests - bool req_handled = false; - std::optional> active_conf = - get_active_configuration(); - if (!active_conf.has_value()) { - m_ctrl_ep->stall(true); - m_retry_counter += 1; - return; - } - - try { - enumerator_eio eio(*this); - for (auto const& iface : active_conf.value().get().interfaces()) { - req_handled = iface->handle_request(req, eio); - if (req_handled) { - break; - } - } - // Handle driver exceptions if any, but make sure to inject a STALL in - // first - } catch (hal::exception& e) { - m_ctrl_ep->stall(true); - throw; - } - - if (!req_handled) { - m_ctrl_ep->stall(true); - m_retry_counter += 1; - return; - } + switch (*m_event) { + case v5::usb::host_event::reset: + m_active_conf = nullptr; + // m_ctrl_ep->connect(false); + prepare_descriptors(); + m_ctrl_ep->connect(true); + break; + case v5::usb::host_event::setup_packet: + handle_setup_packet(); + break; + case v5::usb::host_event::data_packet: + // Not sure what to do in this case as it shouldn't happen... + break; + default: // The rest... + pass_host_event_to_interfaces(); + break; } - m_ctrl_ep->write({}); // ZLP + m_event.reset(); } private: - void prepare_enumeration() + void pass_host_event_to_interfaces() { - if (m_active_conf != nullptr) { - m_active_conf = nullptr; - m_ctrl_ep->connect(false); - m_reinit_descriptors = true; - } - - if (m_reinit_descriptors) { - prepare_descriptors(); - m_reinit_descriptors = false; + for (auto& interface : m_active_conf->interfaces()) { + interface->handle_host_event(*m_event); } } - class enumerator_eio : endpoint_io { @@ -263,6 +175,68 @@ class enumerator usize total_length = 0; }; + void handle_setup_packet() + { + if (m_ctrl_ep->info().stalled) { + send_error_to_host(); + } + + setup_packet req; + auto& read_buf = req.raw_request_bytes; + auto scatter_read_buf = make_writable_scatter_bytes(read_buf); + auto bytes_read = m_ctrl_ep->read(scatter_read_buf); + + if (bytes_read != 8) { + // STATUS OUT ZLP or malformed - not a SETUP packet, STALL + send_error_to_host(); + return; + } + + switch (req.get_recipient()) { + case setup_packet::request_recipient::invalid: + send_error_to_host(); + break; + + case setup_packet::request_recipient::device: + handle_standard_device_request(req); + break; + + case setup_packet::request_recipient::interface: + [[fallthrough]]; + case setup_packet::request_recipient::endpoint: { + handle_interface_request(req); + } + } + } + + void handle_interface_request(setup_packet p_request) + { + if (m_active_conf == nullptr) { + send_error_to_host(); + return; + } + + enumerator_eio eio(*this); + bool request_handled = false; + + for (auto const& iface : m_active_conf->interfaces()) { + request_handled = iface->handle_request(p_request, eio); + if (request_handled) { + break; + } + } + + if (!request_handled) { + send_error_to_host(); + } + } + + void send_error_to_host() + { + m_ctrl_ep->stall(true); + m_retry_counter += 1; + } + void prepare_descriptors() { // String indexes 1-3 are reserved for device descriptor strings @@ -277,42 +251,33 @@ class enumerator // Configurations for (size_t i = 0; i < num_configs; i++) { configuration& config = m_configs->at(i); - config.set_configuration_index(cur_str_idx++); + config.set_configuration_string_index(cur_str_idx++); config.set_configuration_value(i + 1); } - size_eio seio; + size_eio size_counting_endpoint; for (configuration& config : *m_configs) { auto total_length = static_cast(constants::configuration_descriptor_size); for (auto const& iface : config.m_interfaces) { auto deltas = iface->write_descriptors( - { .interface = cur_iface_idx, .string = cur_str_idx }, seio); + { .interface = cur_iface_idx, .string = cur_str_idx }, + size_counting_endpoint); cur_iface_idx += deltas.interface; cur_str_idx += deltas.string; } config.set_num_interfaces(cur_iface_idx); - total_length += seio.total_length; + total_length += size_counting_endpoint.total_length; config.set_total_length(total_length); - seio.total_length = 0; - } - - m_ctrl_ep->connect(true); - } - u16 assemble_le_u16(std::span bytes) - { - if (bytes.size() != 2) { - safe_throw(hal::argument_out_of_domain(this)); + size_counting_endpoint.total_length = 0; } - - return static_cast(bytes[0]) | (static_cast(bytes[1]) << 8); } void handle_standard_device_request(setup_packet& req) { - auto det = determine_standard_request(req); - switch (det) { + auto const request = determine_standard_request(req); + switch (request) { case standard_request_types::set_address: { m_ctrl_ep->write({}); // ZLP must precede the address change m_ctrl_ep->set_address(req.value_bytes()[0]); @@ -320,13 +285,14 @@ class enumerator } case standard_request_types::get_descriptor: { - process_get_descriptor(req); + send_requested_descriptor(req); break; } case standard_request_types::get_configuration: { if (m_active_conf == nullptr) { - safe_throw(hal::operation_not_supported(this)); + send_error_to_host(); + return; } auto conf_value = m_active_conf->configuration_value(); auto scatter_conf = make_scatter_bytes(std::span(&conf_value, 1)); @@ -340,16 +306,17 @@ class enumerator } case standard_request_types::invalid: + [[fallthrough]]; default: - safe_throw(hal::operation_not_supported(this)); + send_error_to_host(); } } - void process_get_descriptor(setup_packet& req) + void send_requested_descriptor(setup_packet& req) { auto const type = static_cast(req.value_bytes()[1]); auto const index = req.value_bytes()[0]; - enumerator_eio eio(*this); + switch (type) { case descriptor_type::device: { auto header = @@ -357,23 +324,26 @@ class enumerator static_cast(descriptor_type::device) }); m_device->set_max_packet_size( static_cast(m_ctrl_ep->info().size)); - auto scatter_arr_pair = make_sub_scatter_bytes( - assemble_le_u16(req.length_bytes()), header, *m_device); - hal::v5::write(*m_ctrl_ep, - scatter_span(scatter_arr_pair.spans) - .first(scatter_arr_pair.count)); + auto scatter_arr_pair = + make_sub_scatter_bytes(req.length(), header, *m_device); + hal::v5::write_and_flush( + *m_ctrl_ep, + scatter_span(scatter_arr_pair.spans) + .first(scatter_arr_pair.count)); break; } case descriptor_type::configuration: { - configuration& conf = m_configs->at(index); - auto conf_hdr = + if (index >= m_configs->size()) { + send_error_to_host(); + return; + } + configuration& conf = (*m_configs)[index]; + auto const conf_hdr = std::to_array({ constants::configuration_descriptor_size, static_cast(descriptor_type::configuration) }); - auto scatter_conf_pair = - make_sub_scatter_bytes(assemble_le_u16(req.length_bytes()), - conf_hdr, - static_cast>(conf)); + auto scatter_conf_pair = make_sub_scatter_bytes( + req.length(), conf_hdr, static_cast>(conf)); m_ctrl_ep->write(scatter_span(scatter_conf_pair.spans) .first(scatter_conf_pair.count)); @@ -385,15 +355,21 @@ class enumerator return; } - for (auto const& iface : conf.m_interfaces) { - std::ignore = iface->write_descriptors( - { .interface = std::nullopt, .string = std::nullopt }, eio); + { + enumerator_eio eio(*this); + for (auto const& iface : conf.m_interfaces) { + std::ignore = iface->write_descriptors( + { .interface = std::nullopt, .string = std::nullopt }, eio); + } } + + m_ctrl_ep->write({}); // ZLP to finish break; } case descriptor_type::string: { - handle_str_descriptors(index, req.length()); // Can throw + handle_str_descriptors(index, req.length()); + m_ctrl_ep->write({}); // ZLP to finish break; } @@ -401,96 +377,92 @@ class enumerator // TODO(#96): OTHER_SPEED_CONFIGURATION default: - safe_throw(hal::operation_not_supported(this)); + send_error_to_host(); } } + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) void handle_str_descriptors(u8 p_target, u16 p_len) { - // Device strings at indexes 1-3, configuration strings start at 4 - u8 cfg_string_end = num_configs + 3; - enumerator_eio eio(*this); - if (p_target <= cfg_string_end) { - auto r = write_cfg_str_descriptor(p_target, p_len); - if (!r) { - safe_throw(hal::argument_out_of_domain(this)); - } - return; - } - - if (m_active_conf != nullptr) { - for (auto const& iface : m_active_conf->m_interfaces) { - auto res = iface->write_string_descriptor(p_target, eio); - if (res) { - return; - } - } - } - - for (configuration const& conf : *m_configs) { - for (auto const& iface : conf.m_interfaces) { - auto res = iface->write_string_descriptor(p_target, eio); - if (res) { - break; - } - } - } - } - - void write_cfg_str_descriptor(u8 const p_target, u16 p_len) - { - using namespace std::string_view_literals; - // Device strings are at fixed indexes: 1=manufacturer, 2=product, 3=serial constexpr u8 language_id = 0; constexpr u8 manufacturer_idx = 1; constexpr u8 product_idx = 2; constexpr u8 serial_number_idx = 3; - constexpr u8 config_start_idx = 4; - - if (p_target == language_id) { - auto const payload = std::to_array({ - static_cast(4), // length - static_cast(descriptor_type::string), // type - static_cast(m_lang_str & 0xFF), // LE0 - static_cast((m_lang_str >> 8) & 0xFF), // LE1 - }); - // auto const lang = setup_packet::to_le_u16(m_lang_str); - auto const final_payload = hal::v5::make_scatter_array( - std::span(payload).first(std::min(p_len, payload.size()))); - m_ctrl_ep->write(final_payload); - } - - std::u16string_view str = u""sv; - if (p_target == manufacturer_idx) { - str = m_device->manufacturer_str; + switch (p_target) { + case language_id: { + auto const payload = std::to_array({ + static_cast(4), // length + static_cast(descriptor_type::string), // type + static_cast(m_lang_str & 0xFF), // LE0 + static_cast((m_lang_str >> 8) & 0xFF), // LE1 + }); + // auto const lang = setup_packet::to_le_u16(m_lang_str); + auto const final_payload = hal::v5::make_scatter_array( + std::span(payload).first(std::min(p_len, payload.size()))); + m_ctrl_ep->write(final_payload); + break; + } + case manufacturer_idx: { + write_string_view(m_device->manufacturer_str, p_len); + break; + } + case product_idx: { + write_string_view(m_device->product_str, p_len); + break; + } + case serial_number_idx: { + write_string_view(m_device->serial_number_str, p_len); + break; + } + default: { + // Check if the index is for the configuration string + for (configuration const& conf : *m_configs) { + if (p_target == conf.configuration_string_index()) { + write_string_view(conf.name, p_len); + break; + } + } - } else if (p_target == product_idx) { - str = m_device->product_str; + enumerator_eio endpoint(*this); - } else if (p_target == serial_number_idx) { - str = m_device->serial_number_str; + // Check if this index belongs to any interfaces + if (m_active_conf != nullptr) { + for (auto const& iface : m_active_conf->m_interfaces) { + if (iface->write_string_descriptor(p_target, endpoint) == true) { + return; + } + } + } - } else { - for (size_t i = 0; i < m_configs->size(); i++) { - configuration const& conf = m_configs->at(i); - if (p_target == (config_start_idx + i)) { - str = conf.name; + // Check across all configurations + for (configuration const& conf : *m_configs) { + for (auto const& iface : conf.m_interfaces) { + if (iface->write_string_descriptor(p_target, endpoint)) { + return; + } + } } + break; } } + } - auto const conf_sv_span = hal::as_bytes(str); - auto const length = static_cast((conf_sv_span.size() + 2)); + void write_string_view(std::u16string_view p_str, u16 p_max_length) + { + auto const string_view_as_bytes = hal::as_bytes(p_str); + auto const length = + static_cast((string_view_as_bytes.size() + 2)); auto const header = std::to_array( { length, static_cast(descriptor_type::string) }); - auto scatter_arr_pair = make_sub_scatter_bytes(p_len, header, conf_sv_span); + auto const scatter_arr_pair = + make_sub_scatter_bytes(p_max_length, header, string_view_as_bytes); - auto p = scatter_span(scatter_arr_pair.spans) - .first(scatter_arr_pair.count); + auto const p = scatter_span(scatter_arr_pair.spans) + .first(scatter_arr_pair.count); hal::v5::write(*m_ctrl_ep, p); } @@ -498,13 +470,11 @@ class enumerator strong_ptr m_device; strong_ptr> m_configs; u16 m_lang_str; + u8 m_retry_max; - std::optional>> m_iface_for_str_desc; + std::optional m_event = v5::usb::host_event::reset; configuration* m_active_conf = nullptr; - bool volatile m_has_setup_packet = false; - bool m_reinit_descriptors = true; u8 m_retry_counter = 0; - u8 const m_retry_max; }; } // namespace hal::v5::usb From 4a6ae03cd84e32224bbd800b4cf433362362e592 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Wed, 6 May 2026 17:05:52 -0700 Subject: [PATCH 3/6] Working version that works with the arm-mcu CDC serial --- v5/include/libhal-util/usb/enumerator.hpp | 39 +++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/v5/include/libhal-util/usb/enumerator.hpp b/v5/include/libhal-util/usb/enumerator.hpp index 785fc5d..729558f 100644 --- a/v5/include/libhal-util/usb/enumerator.hpp +++ b/v5/include/libhal-util/usb/enumerator.hpp @@ -66,8 +66,10 @@ class enumerator , m_lang_str(p_args.lang_str) , m_retry_max(p_args.retry_max) { - m_ctrl_ep->on_host_event( - [this](v5::usb::host_event p_event) { *m_event = p_event; }); + m_ctrl_ep->on_host_event([this](v5::usb::host_event p_event) { + // Assign + m_event = p_event; + }); } enumerator(enumerator&&) = delete; @@ -95,14 +97,13 @@ class enumerator throw hal::io_error(this); } - if (not m_event.has_value()) { + if (not m_event) { return; } switch (*m_event) { case v5::usb::host_event::reset: m_active_conf = nullptr; - // m_ctrl_ep->connect(false); prepare_descriptors(); m_ctrl_ep->connect(true); break; @@ -123,6 +124,9 @@ class enumerator private: void pass_host_event_to_interfaces() { + if (m_active_conf == nullptr) { + return; + } for (auto& interface : m_active_conf->interfaces()) { interface->handle_host_event(*m_event); } @@ -229,6 +233,8 @@ class enumerator if (!request_handled) { send_error_to_host(); } + + m_ctrl_ep->write({}); } void send_error_to_host() @@ -249,10 +255,14 @@ class enumerator m_device->set_num_configurations(num_configs); // Configurations - for (size_t i = 0; i < num_configs; i++) { - configuration& config = m_configs->at(i); - config.set_configuration_string_index(cur_str_idx++); - config.set_configuration_value(i + 1); + { + // Configuration index 0 is reserved/invalid + // Configuration index value must be greater than 0. + usize index = 1; + for (configuration& config : *m_configs) { + config.set_configuration_string_index(cur_str_idx++); + config.set_configuration_value(index++); + } } size_eio size_counting_endpoint; @@ -296,12 +306,13 @@ class enumerator } auto conf_value = m_active_conf->configuration_value(); auto scatter_conf = make_scatter_bytes(std::span(&conf_value, 1)); - m_ctrl_ep->write(scatter_conf); + hal::v5::write_and_flush(*m_ctrl_ep, scatter_conf); break; } case standard_request_types::set_configuration: { m_active_conf = &(m_configs->at(req.value() - 1)); + m_ctrl_ep->write({}); break; } @@ -334,11 +345,12 @@ class enumerator } case descriptor_type::configuration: { - if (index >= m_configs->size()) { + if (m_configs->empty()) { send_error_to_host(); return; } - configuration& conf = (*m_configs)[index]; + + configuration& conf = (*m_configs)[0]; auto const conf_hdr = std::to_array({ constants::configuration_descriptor_size, static_cast(descriptor_type::configuration) }); @@ -352,6 +364,7 @@ class enumerator // control endpoint based on `request.length()` value. // Return early if the only thing requested was the config descriptor if (req.length() <= constants::configuration_descriptor_size) { + m_ctrl_ep->write({}); // ZLP to finish return; } @@ -376,8 +389,9 @@ class enumerator // TODO(#95): device_qualifier // TODO(#96): OTHER_SPEED_CONFIGURATION - default: + default: { send_error_to_host(); + } } } @@ -398,7 +412,6 @@ class enumerator static_cast(m_lang_str & 0xFF), // LE0 static_cast((m_lang_str >> 8) & 0xFF), // LE1 }); - // auto const lang = setup_packet::to_le_u16(m_lang_str); auto const final_payload = hal::v5::make_scatter_array( std::span(payload).first(std::min(p_len, payload.size()))); m_ctrl_ep->write(final_payload); From 7cb5d487aa0dc7683aa477eae93e6d17b15f97ec Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Fri, 8 May 2026 07:11:46 -0700 Subject: [PATCH 4/6] Fully working with config & device objects removed --- v5/CMakeLists.txt | 1 + v5/include/libhal-util/usb/descriptors.hpp | 2 - v5/include/libhal-util/usb/enumerator.hpp | 443 ++++++++++++--------- v5/tests/usb2.test.cpp | 93 +++++ 4 files changed, 358 insertions(+), 181 deletions(-) create mode 100644 v5/tests/usb2.test.cpp diff --git a/v5/CMakeLists.txt b/v5/CMakeLists.txt index 843263c..d86d084 100644 --- a/v5/CMakeLists.txt +++ b/v5/CMakeLists.txt @@ -53,6 +53,7 @@ set(TEST_SOURCES_LIST tests/steady_clock.test.cpp tests/streams.test.cpp # tests/usb.test.cpp + tests/usb2.test.cpp tests/timeout.test.cpp tests/bit_bang_i2c.test.cpp tests/bit_bang_spi.test.cpp diff --git a/v5/include/libhal-util/usb/descriptors.hpp b/v5/include/libhal-util/usb/descriptors.hpp index 9eacf3c..77aa8f5 100644 --- a/v5/include/libhal-util/usb/descriptors.hpp +++ b/v5/include/libhal-util/usb/descriptors.hpp @@ -38,7 +38,6 @@ namespace hal::v5::usb { class device { public: - template friend class enumerator; struct device_arguments @@ -192,7 +191,6 @@ concept usb_interface_concept = std::derived_from; class configuration { public: - template friend class enumerator; struct bitmap diff --git a/v5/include/libhal-util/usb/enumerator.hpp b/v5/include/libhal-util/usb/enumerator.hpp index 729558f..0632ed6 100644 --- a/v5/include/libhal-util/usb/enumerator.hpp +++ b/v5/include/libhal-util/usb/enumerator.hpp @@ -16,10 +16,8 @@ #include #include -#include #include -#include #include #include #include @@ -35,7 +33,6 @@ #include "../as_bytes.hpp" #include "../scatter_span.hpp" #include "../usb/endpoints.hpp" -#include "descriptors.hpp" #include "utils.hpp" namespace hal::v5::usb { @@ -45,54 +42,71 @@ using hal::v5::make_sub_scatter_bytes; using hal::v5::scatter_span_size; using hal::v5::sub_scatter_result; -template -class enumerator +class base_enumerator { - public: - struct args + struct info { - strong_ptr ctrl_ep; - strong_ptr device; - strong_ptr> configs; - u16 lang_str; - u8 retry_max; + // ---- Required: no sensible defaults ---- + u16 vendor_id; + u16 product_id; + std::u16string_view manufacturer; + std::u16string_view product; + std::u16string_view serial_number; + std::u16string_view configuration; + + // ---- Optional: reasonable defaults provided ---- + + /// USB specification version. 0x0201 minimum for WebUSB/BOS support. + u16 usb_version = 0x0200; + + /// Set to 0x00 when class is defined at the interface level (most devices). + u8 device_class = 0x00; + u8 device_subclass = 0x00; + u8 device_protocol = 0x00; + + /// Firmware/hardware version presented to the host. + u16 device_version = 0x0100; + + /// Language ID for string descriptors. 0x0409 = English (US). + u16 lang_id = 0x0409; + + /// Maximum bus current drawn in milliamps. Stored as mA, converted to + /// 2mA units when writing the configuration descriptor. + u16 max_power_mA = 100; + + bool self_powered = false; + bool remote_wakeup = false; + + u8 retry_max = 3; }; - enumerator(args p_args) - : m_ctrl_ep(p_args.ctrl_ep) - , m_device(p_args.device) - , m_configs(p_args.configs) - , m_lang_str(p_args.lang_str) - , m_retry_max(p_args.retry_max) + base_enumerator(strong_ptr const& p_ctrl_ep, + info p_info, + std::span> p_interfaces) + : m_ctrl_ep(p_ctrl_ep) + , m_interfaces(p_interfaces) + , m_info(p_info) { - m_ctrl_ep->on_host_event([this](v5::usb::host_event p_event) { - // Assign - m_event = p_event; - }); + m_ctrl_ep->on_host_event( + [this](v5::usb::host_event p_event) { m_event = p_event; }); } - - enumerator(enumerator&&) = delete; - bool operator=(enumerator&&) = delete; - - [[nodiscard]] std::optional> - get_active_configuration() + ~base_enumerator() { - if (m_active_conf == nullptr) { - return std::nullopt; - } - - return std::make_optional(std::ref(*m_active_conf)); + m_ctrl_ep->on_host_event([](v5::usb::host_event) {}); } + base_enumerator(base_enumerator&&) = delete; + bool operator=(base_enumerator&&) = delete; + [[nodiscard]] bool is_enumerated() const { - return m_active_conf != nullptr; + return m_enumerated; } void process_ctrl_transfer() { - if (m_retry_counter >= m_retry_max) { + if (m_retry_counter >= m_info.retry_max) { m_retry_counter = 0; throw hal::io_error(this); } @@ -103,8 +117,7 @@ class enumerator switch (*m_event) { case v5::usb::host_event::reset: - m_active_conf = nullptr; - prepare_descriptors(); + m_enumerated = false; m_ctrl_ep->connect(true); break; case v5::usb::host_event::setup_packet: @@ -124,23 +137,23 @@ class enumerator private: void pass_host_event_to_interfaces() { - if (m_active_conf == nullptr) { + if (not m_enumerated) { return; } - for (auto& interface : m_active_conf->interfaces()) { + for (auto& interface : m_interfaces) { interface->handle_host_event(*m_event); } } + class enumerator_eio : endpoint_io { - public: enumerator_eio() = delete; private: - friend class enumerator; + friend class base_enumerator; - enumerator_eio(enumerator& p_en) + enumerator_eio(base_enumerator& p_en) : m_ctrl_ep(p_en.m_ctrl_ep) { } @@ -181,10 +194,6 @@ class enumerator void handle_setup_packet() { - if (m_ctrl_ep->info().stalled) { - send_error_to_host(); - } - setup_packet req; auto& read_buf = req.raw_request_bytes; auto scatter_read_buf = make_writable_scatter_bytes(read_buf); @@ -215,15 +224,10 @@ class enumerator void handle_interface_request(setup_packet p_request) { - if (m_active_conf == nullptr) { - send_error_to_host(); - return; - } - enumerator_eio eio(*this); bool request_handled = false; - for (auto const& iface : m_active_conf->interfaces()) { + for (auto const& iface : m_interfaces) { request_handled = iface->handle_request(p_request, eio); if (request_handled) { break; @@ -243,45 +247,36 @@ class enumerator m_retry_counter += 1; } - void prepare_descriptors() + /** + * @brief Prepare interfaces with their starting interface and starting string + * number. + * + * @return auto - total length of the interface descriptors + */ + auto prepare_descriptors() { // String indexes 1-3 are reserved for device descriptor strings // (manufacturer, product, serial number). Configuration strings start at 4. - u8 cur_str_idx = 4; - byte cur_iface_idx = 0; + u8 cur_str_index = 4; + byte cur_iface_index = 0; + // Phase one: Preparation + size_eio size_counting_endpoint; - // Device - m_device->set_num_configurations(num_configs); + for (auto const& iface : m_interfaces) { + auto [interface_count, string_count] = iface->write_descriptors( + { + .interface = cur_iface_index, + .string = cur_str_index, + }, + size_counting_endpoint); - // Configurations - { - // Configuration index 0 is reserved/invalid - // Configuration index value must be greater than 0. - usize index = 1; - for (configuration& config : *m_configs) { - config.set_configuration_string_index(cur_str_idx++); - config.set_configuration_value(index++); - } + cur_iface_index += interface_count; + cur_str_index += string_count; } - size_eio size_counting_endpoint; - for (configuration& config : *m_configs) { - auto total_length = - static_cast(constants::configuration_descriptor_size); - for (auto const& iface : config.m_interfaces) { - auto deltas = iface->write_descriptors( - { .interface = cur_iface_idx, .string = cur_str_idx }, - size_counting_endpoint); - - cur_iface_idx += deltas.interface; - cur_str_idx += deltas.string; - } - config.set_num_interfaces(cur_iface_idx); - total_length += size_counting_endpoint.total_length; - config.set_total_length(total_length); - size_counting_endpoint.total_length = 0; - } + return size_counting_endpoint.total_length + + static_cast(constants::configuration_descriptor_size); } void handle_standard_device_request(setup_packet& req) @@ -300,18 +295,14 @@ class enumerator } case standard_request_types::get_configuration: { - if (m_active_conf == nullptr) { - send_error_to_host(); - return; - } - auto conf_value = m_active_conf->configuration_value(); - auto scatter_conf = make_scatter_bytes(std::span(&conf_value, 1)); - hal::v5::write_and_flush(*m_ctrl_ep, scatter_conf); + auto const payload = std::to_array({ 1 }); + auto const s_span = make_scatter_array(payload); + hal::v5::write_and_flush(*m_ctrl_ep, s_span); break; } case standard_request_types::set_configuration: { - m_active_conf = &(m_configs->at(req.value() - 1)); + m_enumerated = true; m_ctrl_ep->write({}); break; } @@ -323,54 +314,133 @@ class enumerator } } + constexpr auto generate_device_descriptor() + { + static constexpr u8 b_length = 0; + static constexpr u8 b_descriptor_type = 1; + static constexpr u8 bcd_usb_lo = 2; + static constexpr u8 bcd_usb_hi = 3; + static constexpr u8 b_device_class = 4; + static constexpr u8 b_device_sub_class = 5; + static constexpr u8 b_device_protocol = 6; + static constexpr u8 b_max_packet_size0 = 7; + static constexpr u8 id_vendor_lo = 8; + static constexpr u8 id_vendor_hi = 9; + static constexpr u8 id_product_lo = 10; + static constexpr u8 id_product_hi = 11; + static constexpr u8 bcd_device_lo = 12; + static constexpr u8 bcd_device_hi = 13; + static constexpr u8 i_manufacturer = 14; + static constexpr u8 i_product = 15; + static constexpr u8 i_serial_number = 16; + static constexpr u8 b_num_configurations = 17; + + std::array descriptor{}; + + descriptor[b_length] = static_cast(descriptor.size()); + descriptor[b_descriptor_type] = 1; + + auto const bcd_usb = setup_packet::to_le_u16(m_info.usb_version); + descriptor[bcd_usb_lo] = bcd_usb[0]; + descriptor[bcd_usb_hi] = bcd_usb[1]; + + descriptor[b_device_class] = static_cast(m_info.device_class); + descriptor[b_device_sub_class] = m_info.device_subclass; + descriptor[b_device_protocol] = m_info.device_protocol; + descriptor[b_max_packet_size0] = m_ctrl_ep->info().size; + + auto const id_vendor = setup_packet::to_le_u16(m_info.vendor_id); + descriptor[id_vendor_lo] = id_vendor[0]; + descriptor[id_vendor_hi] = id_vendor[1]; + + auto const id_product = setup_packet::to_le_u16(m_info.product_id); + descriptor[id_product_lo] = id_product[0]; + descriptor[id_product_hi] = id_product[1]; + + auto const bcd_device = setup_packet::to_le_u16(m_info.device_version); + descriptor[bcd_device_lo] = bcd_device[0]; + descriptor[bcd_device_hi] = bcd_device[1]; + + // Default string indexes, assuming enumeration will use 4 onward for string + // indexes are these are required (can be modified by enumerator if desired) + descriptor[i_manufacturer] = 1; + descriptor[i_product] = 2; + descriptor[i_serial_number] = 3; + + descriptor[b_num_configurations] = 1; + + return descriptor; + } + + constexpr auto generate_configuration_descriptor(u16 p_total_length) + { + static constexpr u8 b_length = 0; + static constexpr u8 b_descriptor_type = 1; + static constexpr u8 w_total_length_lo = 2; + static constexpr u8 w_total_length_hi = 3; + static constexpr u8 b_num_interfaces = 4; + static constexpr u8 b_configuration_value = 5; + static constexpr u8 i_configuration = 6; + static constexpr u8 bm_attributes = 7; + static constexpr u8 b_max_power = 8; + + std::array descriptor{}; + + descriptor[b_length] = static_cast(descriptor.size()); + descriptor[b_descriptor_type] = 2; + + auto const total_length = setup_packet::to_le_u16(p_total_length); + descriptor[w_total_length_lo] = total_length[0]; + descriptor[w_total_length_hi] = total_length[1]; + + descriptor[b_num_interfaces] = static_cast(m_interfaces.size()); + descriptor[b_configuration_value] = 1; + descriptor[i_configuration] = 4; // String index for configuration + + // Bit 7 is reserved and must be set; bits 6 and 5 are self-powered and + // remote wakeup respectively. + descriptor[bm_attributes] = static_cast( + (1u << 7) | (m_info.self_powered << 6) | (m_info.remote_wakeup << 5)); + + // bMaxPower is in 2mA units + descriptor[b_max_power] = static_cast(m_info.max_power_mA / 2); + + return descriptor; + } + void send_requested_descriptor(setup_packet& req) { auto const type = static_cast(req.value_bytes()[1]); - auto const index = req.value_bytes()[0]; switch (type) { case descriptor_type::device: { - auto header = - std::to_array({ constants::device_descriptor_size, - static_cast(descriptor_type::device) }); - m_device->set_max_packet_size( - static_cast(m_ctrl_ep->info().size)); - auto scatter_arr_pair = - make_sub_scatter_bytes(req.length(), header, *m_device); - hal::v5::write_and_flush( - *m_ctrl_ep, - scatter_span(scatter_arr_pair.spans) - .first(scatter_arr_pair.count)); + auto const descriptor = generate_device_descriptor(); + auto const length = std::min(req.length(), descriptor.size()); + auto const payload = std::span(descriptor).first(length); + hal::v5::write_and_flush(*m_ctrl_ep, + make_scatter_array(payload)); break; } case descriptor_type::configuration: { - if (m_configs->empty()) { - send_error_to_host(); - return; - } - - configuration& conf = (*m_configs)[0]; - auto const conf_hdr = - std::to_array({ constants::configuration_descriptor_size, - static_cast(descriptor_type::configuration) }); - auto scatter_conf_pair = make_sub_scatter_bytes( - req.length(), conf_hdr, static_cast>(conf)); + auto const total_size = prepare_descriptors(); + auto const descriptor = generate_configuration_descriptor(total_size); + auto const length = std::min(req.length(), descriptor.size()); + auto const payload = std::span(descriptor).first(length); + hal::v5::write(*m_ctrl_ep, make_scatter_array(payload)); - m_ctrl_ep->write(scatter_span(scatter_conf_pair.spans) - .first(scatter_conf_pair.count)); - - // TODO(#99): `enumerator_eio` should limit the amount written to the - // control endpoint based on `request.length()` value. - // Return early if the only thing requested was the config descriptor if (req.length() <= constants::configuration_descriptor_size) { m_ctrl_ep->write({}); // ZLP to finish return; } { + // TODO(#99): `enumerator_eio` should limit the amount written to the + // control endpoint based on `request.length()` value. When the limit + // has been reached, the driver_write API of enumerator_eio should + // simply do nothing and return early. enumerator_eio eio(*this); - for (auto const& iface : conf.m_interfaces) { + for (auto const& iface : m_interfaces) { std::ignore = iface->write_descriptors( { .interface = std::nullopt, .string = std::nullopt }, eio); } @@ -381,8 +451,7 @@ class enumerator } case descriptor_type::string: { - handle_str_descriptors(index, req.length()); - m_ctrl_ep->write({}); // ZLP to finish + handle_str_descriptors(req); break; } @@ -396,70 +465,64 @@ class enumerator } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - void handle_str_descriptors(u8 p_target, u16 p_len) + void handle_str_descriptors(setup_packet& p_request) { // Device strings are at fixed indexes: 1=manufacturer, 2=product, 3=serial constexpr u8 language_id = 0; - constexpr u8 manufacturer_idx = 1; - constexpr u8 product_idx = 2; - constexpr u8 serial_number_idx = 3; + constexpr u8 manufacturer_index = 1; + constexpr u8 product_index = 2; + constexpr u8 serial_number_index = 3; + constexpr u8 configuration_index = 4; + + auto const target = p_request.value_bytes()[0]; + auto const length = p_request.length(); - switch (p_target) { + switch (target) { case language_id: { auto const payload = std::to_array({ - static_cast(4), // length - static_cast(descriptor_type::string), // type - static_cast(m_lang_str & 0xFF), // LE0 - static_cast((m_lang_str >> 8) & 0xFF), // LE1 + static_cast(4), // length + static_cast(descriptor_type::string), // type + static_cast(m_info.lang_id & 0xFF), // LE0 + static_cast((m_info.lang_id >> 8) & 0xFF), // LE1 }); - auto const final_payload = hal::v5::make_scatter_array( - std::span(payload).first(std::min(p_len, payload.size()))); - m_ctrl_ep->write(final_payload); - break; + auto const payload_length = std::min(length, payload.size()); + auto const payload_span = std::span(payload).first(payload_length); + auto const final_payload = + hal::v5::make_scatter_array(payload_span); + hal::v5::write_and_flush(*m_ctrl_ep, final_payload); + return; } - case manufacturer_idx: { - write_string_view(m_device->manufacturer_str, p_len); - break; + case manufacturer_index: { + write_string_view(m_info.manufacturer, length); + return; } - case product_idx: { - write_string_view(m_device->product_str, p_len); - break; + case product_index: { + write_string_view(m_info.product, length); + return; } - case serial_number_idx: { - write_string_view(m_device->serial_number_str, p_len); - break; + case serial_number_index: { + write_string_view(m_info.serial_number, length); + return; } - default: { + case configuration_index: { // Check if the index is for the configuration string - for (configuration const& conf : *m_configs) { - if (p_target == conf.configuration_string_index()) { - write_string_view(conf.name, p_len); - break; - } + if (target == configuration_index) { + write_string_view(m_info.configuration, length); + return; } - + } + default: { enumerator_eio endpoint(*this); - - // Check if this index belongs to any interfaces - if (m_active_conf != nullptr) { - for (auto const& iface : m_active_conf->m_interfaces) { - if (iface->write_string_descriptor(p_target, endpoint) == true) { - return; - } - } - } - - // Check across all configurations - for (configuration const& conf : *m_configs) { - for (auto const& iface : conf.m_interfaces) { - if (iface->write_string_descriptor(p_target, endpoint)) { - return; - } + for (auto const& iface : m_interfaces) { + if (iface->write_string_descriptor(target, endpoint) == true) { + m_ctrl_ep->write({}); + return; } } break; } } + send_error_to_host(); } void write_string_view(std::u16string_view p_str, u16 p_max_length) @@ -474,23 +537,45 @@ class enumerator auto const scatter_arr_pair = make_sub_scatter_bytes(p_max_length, header, string_view_as_bytes); - auto const p = scatter_span(scatter_arr_pair.spans) - .first(scatter_arr_pair.count); - hal::v5::write(*m_ctrl_ep, p); + auto const payload = scatter_span(scatter_arr_pair.spans) + .first(scatter_arr_pair.count); + + hal::v5::write_and_flush(*m_ctrl_ep, payload); } strong_ptr m_ctrl_ep; - strong_ptr m_device; - strong_ptr> m_configs; - u16 m_lang_str; - u8 m_retry_max; - + std::span> m_interfaces; + info m_info; std::optional m_event = v5::usb::host_event::reset; - configuration* m_active_conf = nullptr; u8 m_retry_counter = 0; + bool m_enumerated = false; }; + +template +class inplace_enumerator : public base_enumerator +{ +public: + template + inplace_enumerator(strong_ptr const& p_ctrl_ep, + info p_info, + Interfaces&&... p_interfaces) + : base_enumerator(p_ctrl_ep, p_info, m_interface_storage) + , m_interface_storage{ std::forward(p_interfaces)... } + { + } + +private: + std::array, InterfaceCount> m_interface_storage; +}; + +template +inplace_enumerator(strong_ptr const&, + base_enumerator::info, + Interfaces&&...) + -> inplace_enumerator; } // namespace hal::v5::usb namespace hal::usb { -using v5::usb::enumerator; -} +using v5::usb::base_enumerator; +using v5::usb::inplace_enumerator; +} // namespace hal::usb diff --git a/v5/tests/usb2.test.cpp b/v5/tests/usb2.test.cpp new file mode 100644 index 0000000..ca9ff6e --- /dev/null +++ b/v5/tests/usb2.test.cpp @@ -0,0 +1,93 @@ +// Copyright 2024 - 2025 Khalil Estell and the libhal contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace hal::v5::usb { + +boost::ut::suite<"usb_test"> usb_test = [] { + // TODO(#78): Add usb utility tests +}; + +struct mock_usb_interface : public hal::usb::interface +{ + descriptor_count driver_write_descriptors(descriptor_start, + endpoint_io&) override + { + return {}; + } + bool driver_write_string_descriptor(u8, endpoint_io&) override + { + return false; + } + bool driver_handle_request(setup_packet const&, endpoint_io&) override + { + return false; + } + void driver_handle_host_event(host_event) override + { + } +}; + +boost::ut::suite<"enumeration_test"> enumeration_test = [] { + using namespace boost::ut; + using namespace hal::literals; + namespace pmr = std::pmr; + + std::array iface_buf{}; + pmr::monotonic_buffer_resource pool(iface_buf.data(), std::size(iface_buf)); + + auto ctrl_ep = make_strong_ptr(&pool); + ctrl_ep->m_endpoint.m_info = { .size = 8, .number = 0, .stalled = false }; + + auto usb_interface = make_strong_ptr(&pool); + + "basic usb enumeration test"_test = [&ctrl_ep, &usb_interface] { + // Start enumeration process and verify connection + [[maybe_unused]] inplace_enumerator en( + ctrl_ep, + { + .vendor_id = 0x1209, + .product_id = 0x0001, + .manufacturer = u"libhal", + .product = u"HALbORD", + .serial_number = u"001", + .configuration = u"Default", + // everything else takes its default + }, + usb_interface); + }; +}; + +} // namespace hal::v5::usb From b43ef94e5f58ce3c3c0f191d92865c5fb32f0cf6 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Fri, 8 May 2026 11:02:53 -0700 Subject: [PATCH 5/6] Additional cleanup and removal of unneeded parts --- v5/include/libhal-util/usb/enumerator.hpp | 48 +++++++++-------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/v5/include/libhal-util/usb/enumerator.hpp b/v5/include/libhal-util/usb/enumerator.hpp index 0632ed6..7b21cd0 100644 --- a/v5/include/libhal-util/usb/enumerator.hpp +++ b/v5/include/libhal-util/usb/enumerator.hpp @@ -48,37 +48,36 @@ class base_enumerator struct info { // ---- Required: no sensible defaults ---- - u16 vendor_id; - u16 product_id; + /// Manufacturing name for this USB device std::u16string_view manufacturer; + /// Name of the USB product std::u16string_view product; + /// Serial number of this USB device std::u16string_view serial_number; - std::u16string_view configuration; + /// 16-bit Vendor ID + u16 vendor_id; + /// 16-bit Product ID + u16 product_id; // ---- Optional: reasonable defaults provided ---- /// USB specification version. 0x0201 minimum for WebUSB/BOS support. u16 usb_version = 0x0200; - /// Set to 0x00 when class is defined at the interface level (most devices). - u8 device_class = 0x00; - u8 device_subclass = 0x00; - u8 device_protocol = 0x00; - - /// Firmware/hardware version presented to the host. - u16 device_version = 0x0100; + /// Maximum bus current drawn in milli-amps. Stored as mA, converted to + /// 2mA units when writing the configuration descriptor. + u16 max_power_mA = 100; /// Language ID for string descriptors. 0x0409 = English (US). u16 lang_id = 0x0409; - /// Maximum bus current drawn in milliamps. Stored as mA, converted to - /// 2mA units when writing the configuration descriptor. - u16 max_power_mA = 100; + /// Firmware/hardware version presented to the host. + u16 device_version = 0x0100; + + u8 retry_max = 3; bool self_powered = false; bool remote_wakeup = false; - - u8 retry_max = 3; }; base_enumerator(strong_ptr const& p_ctrl_ep, @@ -344,9 +343,9 @@ class base_enumerator descriptor[bcd_usb_lo] = bcd_usb[0]; descriptor[bcd_usb_hi] = bcd_usb[1]; - descriptor[b_device_class] = static_cast(m_info.device_class); - descriptor[b_device_sub_class] = m_info.device_subclass; - descriptor[b_device_protocol] = m_info.device_protocol; + descriptor[b_device_class] = 0x00; + descriptor[b_device_sub_class] = 0x00; + descriptor[b_device_protocol] = 0x00; descriptor[b_max_packet_size0] = m_ctrl_ep->info().size; auto const id_vendor = setup_packet::to_le_u16(m_info.vendor_id); @@ -395,7 +394,7 @@ class base_enumerator descriptor[b_num_interfaces] = static_cast(m_interfaces.size()); descriptor[b_configuration_value] = 1; - descriptor[i_configuration] = 4; // String index for configuration + descriptor[i_configuration] = 0; // String index for configuration // Bit 7 is reserved and must be set; bits 6 and 5 are self-powered and // remote wakeup respectively. @@ -464,7 +463,6 @@ class base_enumerator } } - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) void handle_str_descriptors(setup_packet& p_request) { // Device strings are at fixed indexes: 1=manufacturer, 2=product, 3=serial @@ -472,7 +470,6 @@ class base_enumerator constexpr u8 manufacturer_index = 1; constexpr u8 product_index = 2; constexpr u8 serial_number_index = 3; - constexpr u8 configuration_index = 4; auto const target = p_request.value_bytes()[0]; auto const length = p_request.length(); @@ -504,13 +501,6 @@ class base_enumerator write_string_view(m_info.serial_number, length); return; } - case configuration_index: { - // Check if the index is for the configuration string - if (target == configuration_index) { - write_string_view(m_info.configuration, length); - return; - } - } default: { enumerator_eio endpoint(*this); for (auto const& iface : m_interfaces) { @@ -545,8 +535,8 @@ class base_enumerator strong_ptr m_ctrl_ep; std::span> m_interfaces; - info m_info; std::optional m_event = v5::usb::host_event::reset; + info m_info; u8 m_retry_counter = 0; bool m_enumerated = false; }; From d1993eb7036e888a3238add6fe1e16745ded30de Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Fri, 8 May 2026 11:20:33 -0700 Subject: [PATCH 6/6] Add generate interface and endpoint descriptors --- .../usb/{utils.hpp => constants.hpp} | 10 +- v5/include/libhal-util/usb/descriptors.hpp | 91 ++++++++++++++++++- v5/include/libhal-util/usb/enumerator.hpp | 6 +- 3 files changed, 102 insertions(+), 5 deletions(-) rename v5/include/libhal-util/usb/{utils.hpp => constants.hpp} (95%) diff --git a/v5/include/libhal-util/usb/utils.hpp b/v5/include/libhal-util/usb/constants.hpp similarity index 95% rename from v5/include/libhal-util/usb/utils.hpp rename to v5/include/libhal-util/usb/constants.hpp index 74a739c..60802e5 100644 --- a/v5/include/libhal-util/usb/utils.hpp +++ b/v5/include/libhal-util/usb/constants.hpp @@ -24,7 +24,7 @@ namespace constants { constexpr byte device_descriptor_size = 18; constexpr byte configuration_descriptor_size = 9; -constexpr byte inferface_descriptor_size = 9; +constexpr byte interface_descriptor_size = 9; constexpr byte endpoint_descriptor_size = 7; constexpr byte interface_association_descriptor_size = 0x08; @@ -64,6 +64,14 @@ enum class class_code : hal::byte vendor_specific = 0xFF // Vendor Specific }; +enum class transfer_type : hal::byte +{ + control = 0x00, + isochronous = 0x01, + bulk = 0x02, + interrupt = 0x03, +}; + // Default types enum class descriptor_type : hal::byte { diff --git a/v5/include/libhal-util/usb/descriptors.hpp b/v5/include/libhal-util/usb/descriptors.hpp index 77aa8f5..a74f74c 100644 --- a/v5/include/libhal-util/usb/descriptors.hpp +++ b/v5/include/libhal-util/usb/descriptors.hpp @@ -26,7 +26,7 @@ #include #include -#include "utils.hpp" +#include "constants.hpp" // TODO(#95): Device qualifer descriptor (happens between device and config) // TODO(#96): USB 3.x Superspeed descriptors (BOS Descriptor, Device Capability, @@ -325,6 +325,95 @@ class configuration std::pmr::vector> m_interfaces; std::array m_packed_arr; }; + +struct interface_descriptor_info +{ + u8 interface_number; + u8 alternate_setting; + u8 num_endpoints; + class_code interface_class; + u8 interface_subclass; + u8 interface_protocol; + u8 interface_string_index; +}; + +constexpr auto generate_interface_descriptor(interface_descriptor_info p_info) +{ + static constexpr u8 b_length = 0; + static constexpr u8 b_descriptor_type = 1; + static constexpr u8 b_interface_number = 2; + static constexpr u8 b_alternate_setting = 3; + static constexpr u8 b_num_endpoints = 4; + static constexpr u8 b_interface_class = 5; + static constexpr u8 b_interface_sub_class = 6; + static constexpr u8 b_interface_protocol = 7; + static constexpr u8 i_interface = 8; + + std::array descriptor{}; + + descriptor[b_length] = static_cast(descriptor.size()); + descriptor[b_descriptor_type] = static_cast(descriptor_type::interface); + descriptor[b_interface_number] = p_info.interface_number; + descriptor[b_alternate_setting] = p_info.alternate_setting; + descriptor[b_num_endpoints] = p_info.num_endpoints; + descriptor[b_interface_class] = static_cast(p_info.interface_class); + descriptor[b_interface_sub_class] = p_info.interface_subclass; + descriptor[b_interface_protocol] = p_info.interface_protocol; + descriptor[i_interface] = p_info.interface_string_index; + + return descriptor; +} + +template +concept usb_endpoint_type = std::is_base_of_v; + +constexpr auto generate_endpoint_descriptor(usb_endpoint_type auto& p_endpoint, + u8 p_interval) +{ + using Endpoint = std::remove_reference_t; + using hal::usb::bulk_in_endpoint; + using hal::usb::bulk_out_endpoint; + using hal::usb::interrupt_in_endpoint; + using hal::usb::interrupt_out_endpoint; + + constexpr transfer_type type = [] { + if constexpr (std::is_base_of_v or + std::is_base_of_v) { + return transfer_type::bulk; + } else if constexpr (std::is_base_of_v or + std::is_base_of_v) { + return transfer_type::interrupt; + } else { + return transfer_type::control; + } + }(); + + static constexpr u8 b_length = 0; + static constexpr u8 b_descriptor_type = 1; + static constexpr u8 b_endpoint_address = 2; + static constexpr u8 bm_attributes = 3; + static constexpr u8 w_max_packet_size_lo = 4; + static constexpr u8 w_max_packet_size_hi = 5; + static constexpr u8 b_interval = 6; + + std::array descriptor{}; + + auto const ep_info = p_endpoint.info(); + + descriptor[b_length] = static_cast(descriptor.size()); + descriptor[b_descriptor_type] = static_cast(descriptor_type::endpoint); + descriptor[b_endpoint_address] = ep_info.number; + descriptor[bm_attributes] = static_cast(type); + + auto const max_packet_size_bytes = setup_packet::to_le_u16(ep_info.size); + descriptor[w_max_packet_size_lo] = max_packet_size_bytes[0]; + descriptor[w_max_packet_size_hi] = max_packet_size_bytes[1]; + + descriptor[b_interval] = p_interval; + + return descriptor; +} + } // namespace hal::v5::usb namespace hal::usb { diff --git a/v5/include/libhal-util/usb/enumerator.hpp b/v5/include/libhal-util/usb/enumerator.hpp index 7b21cd0..1feba11 100644 --- a/v5/include/libhal-util/usb/enumerator.hpp +++ b/v5/include/libhal-util/usb/enumerator.hpp @@ -33,7 +33,7 @@ #include "../as_bytes.hpp" #include "../scatter_span.hpp" #include "../usb/endpoints.hpp" -#include "utils.hpp" +#include "constants.hpp" namespace hal::v5::usb { @@ -334,7 +334,7 @@ class base_enumerator static constexpr u8 i_serial_number = 16; static constexpr u8 b_num_configurations = 17; - std::array descriptor{}; + std::array descriptor{}; descriptor[b_length] = static_cast(descriptor.size()); descriptor[b_descriptor_type] = 1; @@ -383,7 +383,7 @@ class base_enumerator static constexpr u8 bm_attributes = 7; static constexpr u8 b_max_power = 8; - std::array descriptor{}; + std::array descriptor{}; descriptor[b_length] = static_cast(descriptor.size()); descriptor[b_descriptor_type] = 2;