diff --git a/.clangd b/v5/.clangd similarity index 100% rename from .clangd rename to v5/.clangd 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/mock/usb.hpp b/v5/include/libhal-util/mock/usb.hpp index bc2f18f..7460a27 100644 --- a/v5/include/libhal-util/mock/usb.hpp +++ b/v5/include/libhal-util/mock/usb.hpp @@ -28,7 +28,7 @@ #include #include -#include "../usb/utils.hpp" +#include "../usb/constants.hpp" namespace hal::v5::usb { constexpr u8 interface_description_length = 9; diff --git a/v5/include/libhal-util/usb.hpp b/v5/include/libhal-util/usb.hpp index abfbfb3..02e008a 100644 --- a/v5/include/libhal-util/usb.hpp +++ b/v5/include/libhal-util/usb.hpp @@ -14,7 +14,7 @@ #pragma once +#include "usb/constants.hpp" #include "usb/descriptors.hpp" #include "usb/endpoints.hpp" #include "usb/enumerator.hpp" -#include "usb/utils.hpp" 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..fcad1b7 100644 --- a/v5/include/libhal-util/usb/utils.hpp +++ b/v5/include/libhal-util/usb/constants.hpp @@ -21,15 +21,12 @@ namespace hal::v5::usb { 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; - constexpr byte standard_request_size = 8; - } // namespace constants // Maybe move these enum classes into the constants namespace @@ -64,6 +61,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..2550e31 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,10 +325,101 @@ 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 { using v5::usb::configuration; using v5::usb::device; +using v5::usb::generate_endpoint_descriptor; +using v5::usb::generate_interface_descriptor; using v5::usb::usb_interface_concept; } // namespace hal::usb diff --git a/v5/include/libhal-util/usb/enumerator.hpp b/v5/include/libhal-util/usb/enumerator.hpp index 967a757..70372fd 100644 --- a/v5/include/libhal-util/usb/enumerator.hpp +++ b/v5/include/libhal-util/usb/enumerator.hpp @@ -16,10 +16,9 @@ #include #include -#include #include -#include +#include #include #include #include @@ -32,11 +31,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,163 +43,136 @@ using hal::v5::make_sub_scatter_bytes; using hal::v5::scatter_span_size; using hal::v5::sub_scatter_result; -template class 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; + + // ---- Optional: reasonable defaults provided ---- + + /// USB specification version. 0x0201 minimum for WebUSB/BOS support. + u16 usb_version = 0x0200; + + /// 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; + + /// Firmware/hardware version presented to the host. + u16 device_version = 0x0100; + + u8 retry_max = 3; + + bool self_powered = false; + bool remote_wakeup = false; }; - 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) + 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_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_has_setup_packet = this->m_ctrl_ep->has_setup().value(); - }); + m_ctrl_ep->on_host_event( + [this](v5::usb::bus_event p_event) { m_event = p_event; }); + } + ~enumerator() + { + m_ctrl_ep->on_host_event([](v5::usb::bus_event) {}); } enumerator(enumerator&&) = delete; bool operator=(enumerator&&) = delete; - [[nodiscard]] std::optional> - get_active_configuration() - { - if (m_active_conf == nullptr) { - return std::nullopt; - } - - return std::make_optional(std::ref(*m_active_conf)); - } - [[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; - } + auto event = *m_event; - } 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; - } + using bus_event = v5::usb::bus_event; - 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; - } + switch (event) { + case bus_event::reset: + m_enumerated = false; + m_ctrl_ep->connect(true); + pass_host_event_to_interfaces(host_event::reset); + break; + case bus_event::setup_packet: + handle_setup_packet(); + break; + case bus_event::data_packet: + // Explanation: If a setup packet appears that has a data stage (length + // > 0), then the setup packet is stored within the `m_pending_setup` + // and returns as we wait for the data_packet bus_event to occur. Once + // the data packet is received, then the setup packet can actually be + // dispatched. When the usb::interface attempts to read from the + // `endpoint_io::read()` API, the data should be ready. + if (m_pending_setup) { + dispatch_setup_packet(*m_pending_setup); + m_pending_setup.reset(); } - // 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; - } + break; + case bus_event::resume: + pass_host_event_to_interfaces(host_event::resume); + break; + case bus_event::suspend: + if (m_ctrl_ep->remote_wakeup_granted()) { + pass_host_event_to_interfaces(host_event::suspend_with_wakeup); + } + pass_host_event_to_interfaces(host_event::suspend_without_wakeup); + break; + case bus_event::sleep: + pass_host_event_to_interfaces(host_event::sleep); + break; + case bus_event::u1_sleep: + pass_host_event_to_interfaces(host_event::u1_sleep); + break; + case bus_event::u2_sleep: + pass_host_event_to_interfaces(host_event::u2_sleep); + break; } - m_ctrl_ep->write({}); // ZLP + m_event.reset(); } private: - void prepare_enumeration() + void pass_host_event_to_interfaces(host_event p_event) { - 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_interfaces) { + interface->handle_host_event(p_event); } } class enumerator_eio : endpoint_io { - public: enumerator_eio() = delete; @@ -215,7 +186,7 @@ class enumerator usize driver_read(scatter_span p_buffer) override { - auto read = m_ctrl_ep->read(p_buffer); + auto const read = m_ctrl_ep->read(p_buffer); return read; } @@ -239,269 +210,448 @@ class enumerator usize driver_write(scatter_span p_buffer) override { - auto s = scatter_span_size(p_buffer); - total_length += s; - return s; + auto const length = scatter_span_size(p_buffer); + total_length += length; + return length; } 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 request; + // Reset the m_event before the read. After read the data packet can come in + // at any time, meaning we want this disengaged this function doesn't get + // called + m_event.reset(); + auto bytes_read = + m_ctrl_ep->read(make_writable_scatter_bytes(request.raw_request_bytes)); + + if (bytes_read != 8) { + // STATUS OUT ZLP or malformed - not a SETUP packet, STALL + send_error_to_host(); + return; + } + + if (not request.is_device_to_host() and request.length() > 0) { + m_pending_setup = request; + return; + } + + dispatch_setup_packet(request); + } + + void dispatch_setup_packet(setup_packet& p_request) + { + switch (p_request.get_recipient()) { + case setup_packet::request_recipient::invalid: + send_error_to_host(); + break; + + case setup_packet::request_recipient::device: + handle_standard_device_request(p_request); + break; + + case setup_packet::request_recipient::interface: + [[fallthrough]]; + case setup_packet::request_recipient::endpoint: + handle_interface_request(p_request); + break; } + } - 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) + void handle_standard_device_request(setup_packet& p_request) { - auto det = determine_standard_request(req); - switch (det) { + enum struct feature : u8 + { + device_remote_wakeup = 1, + test_mode = 2 + }; + + auto const request = determine_standard_request(p_request); + 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]); + m_ctrl_ep->set_address(p_request.value_bytes()[0]); return; } case standard_request_types::get_descriptor: { - process_get_descriptor(req); + send_requested_descriptor(p_request); 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; + pass_host_event_to_interfaces(hal::v5::usb::host_event::enumerated); + m_ctrl_ep->write({}); + break; + } + + case hal::v5::usb::standard_request_types::get_status: { + byte const status = (m_info.self_powered << 0) | + (m_ctrl_ep->remote_wakeup_granted() << 1); + auto const payload = std::to_array({ status, 0 }); + auto const s_span = make_scatter_array(payload); + hal::v5::write_and_flush(*m_ctrl_ep, s_span); break; } + case standard_request_types::clear_feature: { + auto const value = static_cast(p_request.value()); + if (value == feature::device_remote_wakeup) { + m_ctrl_ep->remote_wakeup_enable(false); + m_ctrl_ep->write({}); + break; + } + m_ctrl_ep->stall(true); + break; + } + case standard_request_types::set_feature: { + auto const value = static_cast(p_request.value()); + if (value == feature::device_remote_wakeup and m_info.remote_wakeup and + m_ctrl_ep->supports_lpm().remote_wakeup_supported()) { + m_ctrl_ep->remote_wakeup_enable(true); + m_ctrl_ep->write({}); + break; + } + m_ctrl_ep->stall(true); + break; + } case standard_request_types::invalid: 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& p_request) + { + auto const type = static_cast(p_request.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(p_request.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 - if (req.length() <= constants::configuration_descriptor_size) { + auto const total_size = prepare_descriptors(); + auto const descriptor = generate_configuration_descriptor(total_size); + auto const length = + std::min(p_request.length(), descriptor.size()); + auto const payload = std::span(descriptor).first(length); + hal::v5::write(*m_ctrl_ep, make_scatter_array(payload)); + + if (p_request.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(p_request); 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 lang_id_le = setup_packet::to_le_u16(m_info.lang_id); + auto const payload = std::to_array({ + byte{ 4 }, // length + static_cast(descriptor_type::string), // type + lang_id_le[0], // LE0 + lang_id_le[1], // 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::bus_event::reset; + std::optional m_pending_setup; + info m_info; u8 m_retry_counter = 0; - u8 const m_retry_max; + bool m_enumerated = false; +}; + +template +class inplace_enumerator : public enumerator +{ +public: + template + inplace_enumerator(strong_ptr const& p_ctrl_ep, + enumerator::info p_info, + Interfaces&&... p_interfaces) + : enumerator(p_ctrl_ep, p_info, m_interface_storage) + , m_interface_storage{ std::forward(p_interfaces)... } + { + } + + inplace_enumerator() = delete; + inplace_enumerator(inplace_enumerator const&) = delete; + inplace_enumerator& operator=(inplace_enumerator const&) = delete; + inplace_enumerator(inplace_enumerator&&) noexcept = default; + inplace_enumerator& operator=(inplace_enumerator&&) noexcept = default; + +private: + std::array, InterfaceCount> m_interface_storage; }; + +template +inplace_enumerator(strong_ptr const&, + enumerator::info, + Interfaces&&...) + -> inplace_enumerator; } // namespace hal::v5::usb namespace hal::usb { using v5::usb::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..ad268aa 100644 --- a/v5/tests/usb.test.cpp +++ b/v5/tests/usb.test.cpp @@ -12,20 +12,17 @@ // 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 @@ -40,239 +37,52 @@ 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; - static std::array iface_buf; - - iface_buf.fill(0); - device dev({ .bcd_usb = 0x0002, - .device_class = class_code::application_specific, - .device_subclass = 0, - .device_protocol = 0, - .id_vendor = 0xffff, - .id_product = 0x0000, - .bcd_device = 0x0001, - .p_manufacturer = u"libhal", - .p_product = u"Unit Test", - .p_serial_number_str = u"123456789" }); + std::array iface_buf{}; pmr::monotonic_buffer_resource pool(iface_buf.data(), std::size(iface_buf)); - std::array conf_arr{ configuration{ - { .name = u"Test Config", - .attributes = configuration::bitmap(true, false), - .max_power = 1, - .allocator = &pool }, - make_strong_ptr(&pool, mock(u"Mock Iface")) } }; - - mock_usb_control_endpoint ctrl_ep; - ctrl_ep.m_endpoint.m_info = { .size = 8, .number = 0, .stalled = false }; - auto ctrl_ptr = make_strong_ptr(&pool, ctrl_ep); - auto const conf_arr_ptr = - make_strong_ptr>(&pool, conf_arr); - auto dev_ptr = make_strong_ptr(&pool, dev); - "basic usb enumeration test"_test = [&ctrl_ptr, &dev_ptr, &conf_arr_ptr] { - // Start enumeration process and verify connection - enumerator<1> en(enumerator<1>::args{ .ctrl_ep = ctrl_ptr, - .device = dev_ptr, - .configs = conf_arr_ptr, - .lang_str = 0x0409, - .retry_max = 3 }); - - auto f = [&en]() { - while (!en.is_enumerated()) { - en.process_ctrl_transfer(); - } - }; - constexpr byte delay_time_ms = 1000; - auto& ctrl_buf = ctrl_ptr->m_out_buf; - std::jthread ejh(f); - std::this_thread::sleep_for(std::chrono::milliseconds( - delay_time_ms)); // Should be enough time to connect - expect(that % true == ctrl_ptr->m_is_connected); - ctrl_buf.clear(); - - u16 expected_addr = 0x30; - setup_packet set_addr{ - { .device_to_host = false, - .type = setup_packet::request_type::standard, - .recipient = setup_packet::request_recipient::device, - .request = static_cast(standard_request_types::set_address), - .value = 0x30, - .index = 0, // a - .length = 0 } - }; - - simulate_sending_payload(ctrl_ptr, set_addr); - ctrl_ptr->simulate_interrupt(); - std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); - expect(that % expected_addr == ctrl_ptr->m_address); - ctrl_buf.clear(); - - // Get device descriptor - u16 desc_t_idx = static_cast(descriptor_type::device) << 8; - setup_packet get_desc( - { .device_to_host = true, - .type = setup_packet::request_type::standard, - .recipient = setup_packet::request_recipient::device, - .request = static_cast(standard_request_types::get_descriptor), - .value = desc_t_idx, - .index = 0, - .length = 18 }); - simulate_sending_payload(ctrl_ptr, get_desc); - ctrl_ptr->simulate_interrupt(); - std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); - std::span dev_actual(ctrl_buf.data(), 18); - std::array dev_expected{ - 0x12, // length - static_cast(descriptor_type::device), // type - 0x02, // usb bcd - 0x00, - static_cast(class_code::application_specific), - 0, // subclass - 0, // protocol - 8, // max packet size - 0xff, // vendor id - 0xff, - 0, // product id - 0, - 0x1, // bcd device - 0x0, - 1, // manufactures index - 2, // product index - 3, // product index - 1 // num configuration - }; - - expect(std::ranges::equal(std::span(dev_expected), dev_actual)); - ctrl_buf.clear(); - - // Get a string descriptor header from device - // where 1 is the manufacture string index - constexpr u16 str_desc_t_idx = - static_cast(descriptor_type::string) << 8 | 1; - setup_packet str_hdr_req( - { .device_to_host = true, - .type = setup_packet::request_type::standard, - .recipient = setup_packet::request_recipient::device, - .request = static_cast(standard_request_types::get_descriptor), - .value = str_desc_t_idx, - .index = 0, - .length = 2 }); - simulate_sending_payload(ctrl_ptr, str_hdr_req); - ctrl_ptr->simulate_interrupt(); - std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); - std::array expected_manu_str_hdr{ - static_cast(14), // string is "libhal" - static_cast(descriptor_type::string) - }; - std::span actual_dev_str_hdr(ctrl_buf.data(), 2); - expect(std::ranges::equal(std::span(expected_manu_str_hdr), - actual_dev_str_hdr)); - ctrl_buf.clear(); + auto ctrl_ep = make_strong_ptr(&pool); + ctrl_ep->m_endpoint.m_info = { .size = 8, .number = 0, .stalled = false }; - // Get a string descriptor from device - setup_packet str_req(str_hdr_req); - str_req.length(expected_manu_str_hdr[0]); - simulate_sending_payload(ctrl_ptr, str_req); - ctrl_ptr->simulate_interrupt(); - std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); - std::u16string_view expected_manu_str = u"libhal"; + auto usb_interface = make_strong_ptr(&pool); - auto expected_manu_str_scatter = - make_scatter_bytes(expected_manu_str_hdr, - std::span(reinterpret_cast( - expected_manu_str.data()), - expected_manu_str.length() * 2)); - auto actual_manu_str_scatter = make_scatter_bytes(ctrl_buf); - expect(that % (scatter_span(expected_manu_str_scatter) == - scatter_span(actual_manu_str_scatter))); - ctrl_buf.clear(); - - // Get Configuration length - setup_packet conf_hdr_req( - { .device_to_host = true, - .type = setup_packet::request_type::standard, - .recipient = setup_packet::request_recipient::device, - .request = static_cast(standard_request_types::get_descriptor), - .value = static_cast(descriptor_type::configuration) << 8, - .index = 0, - .length = 9 }); - - // Expected Config + interface descriptor - std::array expected_conf_iface_desc{ - // config descriptor - 0x9, // len - static_cast(descriptor_type::configuration), // type - 0x12, // HH: total length - 0x0, // LL: tl - 0x1, // number of interfaces - 0x1, // config value - 0x4, // name string index - 0xc0, // bmattributes (selfpowered = true) - 0x1, // max power - - // Interface descriptor - interface_description_length, - interface_description_type, - 0x0, // iface number - 0x0, // alt settings - 0x1, // number of endpoints - 0x2, // class - 0x3, // subclass - 0x4, // protocol - 0x5 // iface name string index - }; - - // Get Configuration descriptor header - simulate_sending_payload(ctrl_ptr, conf_hdr_req); - ctrl_ptr->simulate_interrupt(); - std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); - - auto expected_tl_hh = expected_conf_iface_desc[2]; - auto expected_tl_ll = expected_conf_iface_desc[3]; - auto expected_total_len = - setup_packet::from_le_bytes(expected_tl_hh, expected_tl_ll); - - auto actual_tl_hh = ctrl_buf[2]; - auto actual_tl_ll = ctrl_buf[3]; - auto actual_total_len = - setup_packet::from_le_bytes(actual_tl_hh, actual_tl_ll); - expect(that % expected_total_len == actual_total_len); - ctrl_buf.clear(); - - // Get Configuration Descriptor + interface descriptor + endpoint - // descriptor - setup_packet conf_req(conf_hdr_req); - conf_req.length(expected_total_len); - simulate_sending_payload(ctrl_ptr, conf_req); - ctrl_ptr->simulate_interrupt(); - std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); - expect(std::ranges::equal(std::span(expected_conf_iface_desc), - std::span(ctrl_buf))); - - // Set configuration - setup_packet set_conf_req( - { .device_to_host = false, - .type = setup_packet::request_type::standard, - .recipient = setup_packet::request_recipient::device, - .request = - static_cast(standard_request_types::set_configuration), - .value = 1, - .index = 0, - .length = 0 }); - - simulate_sending_payload(ctrl_ptr, set_conf_req); - ctrl_ptr->simulate_interrupt(); - std::this_thread::sleep_for(std::chrono::milliseconds(delay_time_ms)); - - ejh.join(); - // Verify active config - expect(std::ranges::equal( - std::span(expected_conf_iface_desc.data() + 2, 7), - std::span(en.get_active_configuration()->get()))); + "basic usb enumeration test"_test = [&ctrl_ep, &usb_interface] { + // Start enumeration process and verify connection + [[maybe_unused]] inplace_enumerator en( + ctrl_ep, + { + .manufacturer = u"libhal", + .product = u"HALbORD", + .serial_number = u"001", + .vendor_id = 0xDEAD, + .product_id = 0xBEEF, + // everything else takes its default + }, + usb_interface); }; };