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..d86d084 100644 --- a/v5/CMakeLists.txt +++ b/v5/CMakeLists.txt @@ -52,7 +52,8 @@ 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/usb2.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/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 56d9647..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, @@ -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 @@ -249,11 +247,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 +313,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; } @@ -327,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 967a757..1feba11 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 @@ -32,11 +30,10 @@ #include #include -#include "descriptors.hpp" -#include "libhal-util/as_bytes.hpp" -#include "libhal-util/scatter_span.hpp" -#include "libhal-util/usb/endpoints.hpp" -#include "utils.hpp" +#include "../as_bytes.hpp" +#include "../scatter_span.hpp" +#include "../usb/endpoints.hpp" +#include "constants.hpp" namespace hal::v5::usb { @@ -45,170 +42,117 @@ 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 ---- + /// 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; + /// 16-bit Vendor ID + u16 vendor_id; + /// 16-bit Product ID + u16 product_id; - 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) - { - 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; - } + // ---- Optional: reasonable defaults provided ---- - m_has_setup_packet = this->m_ctrl_ep->has_setup().value(); - }); - } + /// USB specification version. 0x0201 minimum for WebUSB/BOS support. + u16 usb_version = 0x0200; - enumerator(enumerator&&) = delete; - bool operator=(enumerator&&) = delete; + /// Maximum bus current drawn in milli-amps. Stored as mA, converted to + /// 2mA units when writing the configuration descriptor. + u16 max_power_mA = 100; - [[nodiscard]] std::optional> - get_active_configuration() - { - if (m_active_conf == nullptr) { - return std::nullopt; - } + /// Language ID for string descriptors. 0x0409 = English (US). + u16 lang_id = 0x0409; - return std::make_optional(std::ref(*m_active_conf)); + /// Firmware/hardware version presented to the host. + u16 device_version = 0x0100; + + u8 retry_max = 3; + + bool self_powered = false; + bool remote_wakeup = false; + }; + + 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) { m_event = p_event; }); + } + ~base_enumerator() + { + 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() { - prepare_enumeration(); - if (!m_has_setup_packet) { - return; - } - - if (m_retry_counter >= m_retry_max) { + if (m_retry_counter >= m_info.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) { 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 { - // 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_enumerated = false; + 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 (not m_enumerated) { + return; } - - if (m_reinit_descriptors) { - prepare_descriptors(); - m_reinit_descriptors = false; + 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) { } @@ -247,56 +191,97 @@ class enumerator usize total_length = 0; }; - void prepare_descriptors() + void handle_setup_packet() { - // 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; - // Phase one: Preperation - - // Device - 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_index(cur_str_idx++); - config.set_configuration_value(i + 1); + 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); + } } + } - size_eio seio; - 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); + void handle_interface_request(setup_packet p_request) + { + enumerator_eio eio(*this); + bool request_handled = false; - cur_iface_idx += deltas.interface; - cur_str_idx += deltas.string; + for (auto const& iface : m_interfaces) { + request_handled = iface->handle_request(p_request, eio); + if (request_handled) { + break; } - config.set_num_interfaces(cur_iface_idx); - total_length += seio.total_length; - config.set_total_length(total_length); - seio.total_length = 0; } - m_ctrl_ep->connect(true); + if (!request_handled) { + send_error_to_host(); + } + + m_ctrl_ep->write({}); + } + + void send_error_to_host() + { + m_ctrl_ep->stall(true); + m_retry_counter += 1; } - u16 assemble_le_u16(std::span bytes) + + /** + * @brief Prepare interfaces with their starting interface and starting string + * number. + * + * @return auto - total length of the interface descriptors + */ + auto prepare_descriptors() { - if (bytes.size() != 2) { - safe_throw(hal::argument_out_of_domain(this)); + // String indexes 1-3 are reserved for device descriptor strings + // (manufacturer, product, serial number). Configuration strings start at 4. + u8 cur_str_index = 4; + byte cur_iface_index = 0; + + // Phase one: Preparation + size_eio size_counting_endpoint; + + 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); + + cur_iface_index += interface_count; + cur_str_index += string_count; } - return static_cast(bytes[0]) | (static_cast(bytes[1]) << 8); + return size_counting_endpoint.total_length + + static_cast(constants::configuration_descriptor_size); } 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]); @@ -304,204 +289,283 @@ 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)); - } - 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); + 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; } 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) + constexpr auto generate_device_descriptor() { - auto const desc_type = static_cast(req.value_bytes()[1]); - auto desc_idx = req.value_bytes()[0]; - enumerator_eio eio(*this); - switch (desc_type) { + 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] = 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); + 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] = 0; // 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]); + + 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( - 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 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: { - configuration& conf = m_configs->at(desc_idx); - auto 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)); - - 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 + 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)); + if (req.length() <= constants::configuration_descriptor_size) { + m_ctrl_ep->write({}); // ZLP to finish return; } - for (auto const& iface : conf.m_interfaces) { - std::ignore = iface->write_descriptors( - { .interface = std::nullopt, .string = std::nullopt }, eio); + { + // 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 : 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: { - 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(req); break; } // TODO(#95): device_qualifier // TODO(#96): OTHER_SPEED_CONFIGURATION - default: - safe_throw(hal::operation_not_supported(this)); + default: { + send_error_to_host(); + } } } - void handle_str_descriptors(u8 target_idx, u16 p_len) + void handle_str_descriptors(setup_packet& p_request) { - // 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 (!r) { - safe_throw(hal::argument_out_of_domain(this)); + // Device strings are at fixed indexes: 1=manufacturer, 2=product, 3=serial + constexpr u8 language_id = 0; + constexpr u8 manufacturer_index = 1; + constexpr u8 product_index = 2; + constexpr u8 serial_number_index = 3; + + auto const target = p_request.value_bytes()[0]; + auto const length = p_request.length(); + + switch (target) { + case language_id: { + auto const payload = std::to_array({ + 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 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; } - 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) { + case manufacturer_index: { + write_string_view(m_info.manufacturer, length); return; } - } - - if (m_active_conf != nullptr) { - for (auto const& iface : m_active_conf->m_interfaces) { - auto res = iface->write_string_descriptor(target_idx, eio); - if (res) { - return; - } + case product_index: { + write_string_view(m_info.product, length); + return; } - } - - for (configuration const& conf : *m_configs) { - for (auto const& iface : conf.m_interfaces) { - auto res = iface->write_string_descriptor(target_idx, eio); - if (res) { - break; + case serial_number_index: { + write_string_view(m_info.serial_number, length); + return; + } + default: { + enumerator_eio endpoint(*this); + for (auto const& iface : m_interfaces) { + if (iface->write_string_descriptor(target, endpoint) == true) { + m_ctrl_ep->write({}); + return; + } } + break; } } + send_error_to_host(); } - bool write_cfg_str_descriptor(u8 const target_idx, u16 le_len) + void write_string_view(std::u16string_view p_str, u16 p_max_length) { - // Device strings are at fixed indexes: 1=manufacturer, 2=product, 3=serial - 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; - - } else if (target_idx == product_idx) { - opt_conf_sv = m_device->product_str; - - } else if (target_idx == serial_number_idx) { - opt_conf_sv = 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; - } - } - } + auto const string_view_as_bytes = hal::as_bytes(p_str); + auto const length = + static_cast((string_view_as_bytes.size() + 2)); - 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 header = std::to_array( + { length, static_cast(descriptor_type::string) }); - auto hdr_arr = std::to_array( - { desc_len, static_cast(descriptor_type::string) }); + auto const scatter_arr_pair = + make_sub_scatter_bytes(p_max_length, header, string_view_as_bytes); - auto scatter_arr_pair = - make_sub_scatter_bytes(le_len, hdr_arr, conf_sv_span); + auto const payload = scatter_span(scatter_arr_pair.spans) + .first(scatter_arr_pair.count); - auto p = scatter_span(scatter_arr_pair.spans) - .first(scatter_arr_pair.count); - hal::v5::write(*m_ctrl_ep, p); - - return true; + 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; - - std::optional>> m_iface_for_str_desc; - configuration* m_active_conf = nullptr; - bool volatile m_has_setup_packet = false; - bool m_reinit_descriptors = true; + std::span> m_interfaces; + std::optional m_event = v5::usb::host_event::reset; + info m_info; u8 m_retry_counter = 0; - u8 const m_retry_max; + 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/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 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