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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user