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
+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