diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a2b621..0467d2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$") endif() cmake_minimum_required(VERSION 3.21) -project(xone_macos VERSION 0.1.5 LANGUAGES CXX Swift) +project(xone_macos VERSION 0.1.6 LANGUAGES CXX Swift) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/include/mt76/mt76.hpp b/include/mt76/mt76.hpp index 74c003b..1b16a15 100644 --- a/include/mt76/mt76.hpp +++ b/include/mt76/mt76.hpp @@ -41,7 +41,23 @@ public: // MAC address from EFUSE with the 62:45:bd fallback applied. auto mac_address() -> std::array; + // Load the firmware image from a file (port of xone_mt76_load_firmware). + // Resets the MCU if firmware is already loaded. Returns 0 or -errno. + auto load_firmware(char const *path) -> int; + private: + // Send an MCU command (port of xone_mt76_send_command). `cmd` is the + // MT_MCU_MSG_CMD_TYPE field. Returns bytes written, or a negative errno. + auto send_command(std::uint32_t cmd, void const *payload, + std::size_t payload_len) -> int; + + // Firmware load steps (port of xone_mt76_*). + auto load_ivb() -> int; + auto send_firmware_part(std::uint32_t offset, void const *data, + std::size_t len) -> int; + auto send_firmware(void const *fw_data, std::size_t fw_size) -> int; + auto reset_firmware() -> int; + usb::transport &transport_; }; diff --git a/include/mt76/mt76_defs.hpp b/include/mt76/mt76_defs.hpp index 2302fd5..944619d 100644 --- a/include/mt76/mt76_defs.hpp +++ b/include/mt76/mt76_defs.hpp @@ -7,8 +7,11 @@ // init and firmware load increments. // ============================================================================== +#include #include +#include "common/types.hpp" + namespace xone::mt76 { // Bitfield helpers (port of the kernel BIT/GENMASK/FIELD_PREP macros). @@ -54,4 +57,60 @@ enum efuse_mode : std::uint32_t { efuse_physical_read, }; +// MCU message header fields. +constexpr std::uint32_t mt_mcu_msg_len = genmask(15, 0); +constexpr std::uint32_t mt_mcu_msg_cmd_seq = genmask(19, 16); +constexpr std::uint32_t mt_mcu_msg_cmd_type = genmask(26, 20); +constexpr std::uint32_t mt_mcu_msg_port = genmask(29, 27); +constexpr std::uint32_t mt_mcu_msg_type = genmask(31, 30); +constexpr std::uint32_t mt_mcu_msg_type_cmd = bit(30); + +// DMA message ports (MT_MCU_MSG_PORT field). +enum dma_msg_port : std::uint32_t { + wlan_port = 0, + cpu_rx_port, + cpu_tx_port, + host_port, + virtual_cpu_rx_port, + virtual_cpu_tx_port, + discard, +}; + +// MCU message header length (port of MT_CMD_HDR_LEN). +constexpr std::size_t cmd_hdr_len = 4; + +// FCE DMA registers. +constexpr std::uint32_t mt_fce_dma_addr = 0x0230; +constexpr std::uint32_t mt_fce_dma_len = 0x0234; + +// Firmware load registers and constants (port of XONE_MT_FW_*). +constexpr std::uint32_t xone_mt_rf_patch = 0x0130; +constexpr std::uint32_t fw_load_ivb = 0x12; +constexpr std::uint32_t fw_ilm_offset = 0x080000; +constexpr std::uint32_t fw_dlm_offset = 0x110800; +constexpr std::size_t fw_chunk_size = 0x3800; + +// USB DMA control register and bits. +constexpr std::uint32_t mt_usb_u3dma_cfg = 0x9018; +constexpr std::uint32_t mt_usb_dma_cfg_rx_bulk_en = bit(22); +constexpr std::uint32_t mt_usb_dma_cfg_tx_bulk_en = bit(23); + +// Firmware load configuration registers. +constexpr std::uint32_t mt_fce_pse_ctrl = 0x0800; +constexpr std::uint32_t mt_tx_cpu_from_fce_base_ptr = 0x09a0; +constexpr std::uint32_t mt_tx_cpu_from_fce_max_count = 0x09a4; +constexpr std::uint32_t mt_tx_cpu_from_fce_cpu_desc_idx = 0x09a8; +constexpr std::uint32_t mt_fce_pdma_global_conf = 0x09c4; +constexpr std::uint32_t mt_fce_skip_fs = 0x0a6c; + +// Firmware file header (port of struct mt76_fw_header). +struct fw_header { + std::uint32_t ilm_len; // little-endian on disk + std::uint32_t dlm_len; // little-endian on disk + std::uint16_t build_ver; + std::uint16_t fw_ver; + std::uint8_t pad[4]; + char build_time[16]; +} XONE_PACKED; + } // namespace xone::mt76 diff --git a/scripts/download-firmware.sh b/scripts/download-firmware.sh new file mode 100755 index 0000000..6f40e4a --- /dev/null +++ b/scripts/download-firmware.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# Download and extract the wireless dongle firmware from Windows Update. +# Port of install/firmware.sh from medusalix/xone. +# +# The firmware is subject to Microsoft's Terms of Use: +# https://www.microsoft.com/en-us/legal/terms-of-use + +set -eu + +cd "$(dirname "$0")/.." + +firmware_url='http://download.windowsupdate.com/c/msdownload/update/driver/drvs/2017/07/1cd6a87c-623f-4407-a52d-c31be49e925c_e19f60808bdcbfbd3c3df6be3e71ffc52e43261e.cab' +firmware_hash='48084d9fa53b9bb04358f3bb127b7495dc8f7bb0b3ca1437bd24ef2b6eabdf66' + +echo "The firmware for the wireless dongle is subject to Microsoft's Terms of Use:" +echo 'https://www.microsoft.com/en-us/legal/terms-of-use' +echo +echo 'Press enter to continue!' +read -r _ + +command -v curl >/dev/null 2>&1 || { echo 'This script requires curl!' >&2; exit 1; } + +sha256() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + else + shasum -a 256 "$1" | cut -d' ' -f1 + fi +} + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT INT HUP TERM + +echo "Downloading driver package..." +curl -L --fail -o "$tmpdir/driver.cab" "$firmware_url" + +# Extract the firmware image (requires a libarchive-based tar, e.g. macOS). +tar -xf "$tmpdir/driver.cab" -C "$tmpdir" + +actual=$(sha256 "$tmpdir/FW_ACC_00U.bin") +if [ "$actual" != "$firmware_hash" ]; then + echo "Firmware hash mismatch: $actual" >&2 + exit 1 +fi + +mkdir -p firmware +mv "$tmpdir/FW_ACC_00U.bin" firmware/xow_dongle.bin +echo "Firmware written to firmware/xow_dongle.bin" diff --git a/src/mt76/mt76.cpp b/src/mt76/mt76.cpp index e9b057d..a618649 100644 --- a/src/mt76/mt76.cpp +++ b/src/mt76/mt76.cpp @@ -6,6 +6,9 @@ #include #include #include +#include +#include +#include #include @@ -19,6 +22,39 @@ namespace xone::mt76 { // Poll retry count (port of XONE_MT_POLL_RETRIES). constexpr int poll_retries = 50; +// Build a command message (port of xone_mt76_prep_message): +// [u32 header][payload rounded up to u32][zero pad + 4-byte trailer] +auto build_message(std::uint32_t info, void const *payload, + std::size_t payload_len) -> std::vector +{ + auto rounded = (payload_len + 3) & ~std::size_t{3}; + auto buf = std::vector(rounded + 2 * cmd_hdr_len, 0); + xone::store_le32(buf.data(), info | field_prep(mt_mcu_msg_len, rounded)); + if (payload_len > 0) + std::memcpy(buf.data() + cmd_hdr_len, payload, payload_len); + + return buf; +} + +// Read a firmware file into memory (port of request_firmware). +auto read_firmware_file(char const *path) -> std::optional> +{ + std::ifstream in(path, std::ios::binary | std::ios::ate); + if (!in) + return std::nullopt; + + auto size = in.tellg(); + if (size <= 0) + return std::nullopt; + + in.seekg(0); + std::vector buf(static_cast(size)); + if (!in.read(reinterpret_cast(buf.data()), size)) + return std::nullopt; + + return buf; +} + chip::chip(usb::transport &transport) : transport_(transport) {} auto chip::read_register(std::uint32_t addr) -> std::uint32_t @@ -122,4 +158,138 @@ auto chip::mac_address() -> std::array return address; } +auto chip::send_command(std::uint32_t cmd, void const *payload, + std::size_t payload_len) -> int +{ + auto info = mt_mcu_msg_type_cmd + | field_prep(mt_mcu_msg_port, dma_msg_port::cpu_tx_port) + | field_prep(mt_mcu_msg_cmd_type, cmd); + + auto buf = build_message(info, payload, payload_len); + return transport_.bulk_write(buf.data(), buf.size()); +} + +auto chip::load_ivb() -> int +{ + return transport_.send_vendor_request(usb::vendor_request::dev_mode, false, + static_cast(fw_load_ivb), 0, + nullptr, 0); +} + +auto chip::send_firmware_part(std::uint32_t offset, void const *data, + std::size_t len) -> int +{ + for (std::size_t pos = 0; pos < len; pos += fw_chunk_size) { + auto chunk_len = std::min(len - pos, fw_chunk_size); + auto rounded = (chunk_len + 3) & ~std::size_t{3}; + + write_register(mt_fce_dma_addr | mt_vend_type_cfg, offset + pos); + write_register(mt_fce_dma_len | mt_vend_type_cfg, + static_cast(rounded) << 16); + + if (auto ret = send_command(0, + static_cast(data) + pos, + chunk_len); + ret < 0) + return ret; + + auto complete = 0xc0000000u | (static_cast(rounded) << 16); + if (!poll(mt_fce_dma_len | mt_vend_type_cfg, 0xFFFFFFFFu, complete)) + return -ETIMEDOUT; + } + + return 0; +} + +auto chip::send_firmware(void const *fw_data, std::size_t fw_size) -> int +{ + if (fw_size < sizeof(fw_header)) + return -EINVAL; + + auto header = static_cast(fw_data); + auto ilm_len = xone::load_le32(&header->ilm_len); + auto dlm_len = xone::load_le32(&header->dlm_len); + if (fw_size != sizeof(fw_header) + ilm_len + dlm_len) + return -EINVAL; + + char build_time[17] = {}; + std::memcpy(build_time, header->build_time, sizeof(header->build_time)); + xone::log_msg(log_level::info, "mt76: firmware build %s", build_time); + + // Configure the DMA, enable FCE and packet DMA. + write_register(mt_usb_u3dma_cfg | mt_vend_type_cfg, + mt_usb_dma_cfg_tx_bulk_en | mt_usb_dma_cfg_rx_bulk_en); + write_register(mt_fce_pse_ctrl, 0x01); + write_register(mt_tx_cpu_from_fce_base_ptr, 0x00400230); + write_register(mt_tx_cpu_from_fce_max_count, 0x01); + write_register(mt_tx_cpu_from_fce_cpu_desc_idx, 0x01); + write_register(mt_fce_pdma_global_conf, 0x44); + write_register(mt_fce_skip_fs, 0x03); + + auto base = static_cast(fw_data) + sizeof(fw_header); + if (auto ret = send_firmware_part(fw_ilm_offset, base, ilm_len); ret != 0) + return ret; + + return send_firmware_part(fw_dlm_offset, base + ilm_len, dlm_len); +} + +auto chip::reset_firmware() -> int +{ + // Apply power-on RF patch. + auto val = read_register(xone_mt_rf_patch | mt_vend_type_cfg); + write_register(xone_mt_rf_patch | mt_vend_type_cfg, val & ~bit(19)); + + if (auto err = load_ivb(); err != 0) + return err; + + // Wait for the reset. + if (!poll(mt_fce_dma_addr | mt_vend_type_cfg, 0x80000000u, 0x80000000u)) + return -ETIMEDOUT; + + return 0; +} + +auto chip::load_firmware(char const *path) -> int +{ + // If firmware is already loaded, reset the MCU. + if (read_register(mt_fce_dma_addr | mt_vend_type_cfg)) { + int err = reset_firmware(); + if (err == 0) + return 0; + + // The chip keeps its firmware across a USB reset and does not set + // the reset-complete bit on this hardware. If it is still alive + // with the firmware running, use the running firmware as-is. + std::uint8_t id[4] = {}; + if (read_register(mt_fce_dma_addr | mt_vend_type_cfg) & 0x01u && + read_efuse(mt_ee_chip_id, id, sizeof(id)) == 0) { + xone::log_msg(log_level::warn, + "mt76: firmware reset incomplete; using running firmware"); + return 0; + } + + return err; + } + + auto fw = read_firmware_file(path); + if (!fw.has_value()) { + xone::log_msg(log_level::error, "mt76: firmware not found: %s", path); + return -ENOENT; + } + + int ret = send_firmware(fw->data(), fw->size()); + if (ret != 0) + return ret; + + write_register(mt_fce_dma_addr | mt_vend_type_cfg, 0); + if (auto err = load_ivb(); err != 0) + return err; + + // Wait for the firmware to start. + if (!poll(mt_fce_dma_addr | mt_vend_type_cfg, 0x01u, 0x01u)) + return -ETIMEDOUT; + + return 0; +} + } // namespace xone::mt76 diff --git a/src/usb/usb_transport.cpp b/src/usb/usb_transport.cpp index 632bbea..b7b77bb 100644 --- a/src/usb/usb_transport.cpp +++ b/src/usb/usb_transport.cpp @@ -87,7 +87,8 @@ struct transport::state { // struct, and methods are called as (*ref)->Method(ref, ...). io_service_t service = 0; IOCFPlugInInterface **dev_iodev = nullptr; - IOUSBDeviceInterface197 **dev_ref = nullptr; + // Version 500 interface: adds ResetDevice (port of usb_reset_device). + IOUSBDeviceInterface500 **dev_ref = nullptr; bool dev_opened = false; struct iface_conn { @@ -232,12 +233,12 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) -> void *slot = nullptr; HRESULT hr = (*state_->dev_iodev)->QueryInterface(state_->dev_iodev, - CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID197), &slot); + CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID500), &slot); if (hr != S_OK || !slot) { xone::log_msg(log_level::error, "usb: query device interface failed"); return false; } - state_->dev_ref = static_cast(slot); + state_->dev_ref = static_cast(slot); kr = (*state_->dev_ref)->USBDeviceOpen(state_->dev_ref); if (kr != kIOReturnSuccess) { @@ -246,6 +247,14 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) -> } state_->dev_opened = true; + // Reset the chip so it starts from the boot ROM (port of the + // usb_reset_device call in xone_dongle_probe). + kr = (*state_->dev_ref)->ResetDevice(state_->dev_ref); + if (kr != kIOReturnSuccess) { + xone::log_msg(log_level::error, "usb: reset device failed (%d)", kr); + return false; + } + // Open every interface and collect the endpoint pipes we need. io_iterator_t children = 0; kr = IORegistryEntryGetChildIterator(state_->service, kIOServicePlane, &children); diff --git a/tests/test_mt76.cpp b/tests/test_mt76.cpp index 0efbc3e..9229719 100644 --- a/tests/test_mt76.cpp +++ b/tests/test_mt76.cpp @@ -32,10 +32,22 @@ void test_efuse_registers(void) TEST_ASSERT_EQUAL_UINT32(xone::mt76::mt_vend_type_cfg, 0x40000000u); } +// Pin the firmware load layout to transport/mt76.c values. +void test_firmware_layout(void) +{ + TEST_ASSERT_EQUAL_UINT32(xone::mt76::fw_ilm_offset, 0x080000); + TEST_ASSERT_EQUAL_UINT32(xone::mt76::fw_dlm_offset, 0x110800); + TEST_ASSERT_EQUAL_UINT32(xone::mt76::fw_chunk_size, 0x3800); + TEST_ASSERT_EQUAL_UINT32(sizeof(xone::mt76::fw_header), 32); + TEST_ASSERT_EQUAL_UINT32(xone::mt76::mt_fce_dma_addr, 0x0230); + TEST_ASSERT_EQUAL_UINT32(xone::mt76::mt_fce_dma_len, 0x0234); +} + int main(void) { UNITY_BEGIN(); RUN_TEST(test_bitfield_helpers); RUN_TEST(test_efuse_registers); + RUN_TEST(test_firmware_layout); return UNITY_END(); }