feat: port MT76 firmware load and download script

Add chip::load_firmware(): validates the xow_dongle.bin header, DMAs
the ILM and DLM images in 0x3800-byte chunks over EP 0x04 OUT with
FCE completion polling, then loads the IVB and waits for the firmware
to start. Probe now issues a USB reset first (port of
usb_reset_device), matching xone_dongle_probe.

The chip keeps its firmware across a USB reset on macOS and does not
set the upstream reset-complete bit, so load_firmware falls back to
the running firmware when the chip is still alive. Verified on
hardware: fresh load and re-plug both return success.

Add scripts/download-firmware.sh (port of install/firmware.sh), which
fetches the driver CAB from Windows Update and extracts
firmware/xow_dongle.bin, hash-verified.

Co-Authored-By: qwen3.8-27b@q2_k_xl: ported firmware load and download script
This commit is contained in:
portersky
2026-08-17 16:35:34 +02:00
parent ccc5bc819d
commit 21dc0e09b3
7 changed files with 318 additions and 4 deletions
+1 -1
View File
@@ -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)
+16
View File
@@ -41,7 +41,23 @@ public:
// MAC address from EFUSE with the 62:45:bd fallback applied.
auto mac_address() -> std::array<std::uint8_t, 6>;
// 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_;
};
+59
View File
@@ -7,8 +7,11 @@
// init and firmware load increments.
// ==============================================================================
#include <cstddef>
#include <cstdint>
#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
+48
View File
@@ -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"
+170
View File
@@ -6,6 +6,9 @@
#include <algorithm>
#include <cerrno>
#include <cstring>
#include <fstream>
#include <optional>
#include <vector>
#include <unistd.h>
@@ -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<std::uint8_t>
{
auto rounded = (payload_len + 3) & ~std::size_t{3};
auto buf = std::vector<std::uint8_t>(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::vector<std::uint8_t>>
{
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<std::uint8_t> buf(static_cast<std::size_t>(size));
if (!in.read(reinterpret_cast<char *>(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<std::uint8_t, 6>
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<std::uint16_t>(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<std::uint32_t>(rounded) << 16);
if (auto ret = send_command(0,
static_cast<std::uint8_t const *>(data) + pos,
chunk_len);
ret < 0)
return ret;
auto complete = 0xc0000000u | (static_cast<std::uint32_t>(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_header const *>(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<std::uint8_t const *>(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
+12 -3
View File
@@ -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<IOUSBDeviceInterface197 **>(slot);
state_->dev_ref = static_cast<IOUSBDeviceInterface500 **>(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);
+12
View File
@@ -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();
}