// SPDX-License-Identifier: ISC /* * Copyright (c) 2014-2017 Qualcomm Atheros, Inc. * Copyright (c) 2018-2019, The Linux Foundation. All rights reserved. */ /* Algorithmic part of the firmware download. * To be included in the container file providing framework */ #define wil_err_fw(wil, fmt, arg...) wil_err(wil, "ERR[ FW ]" fmt, ##arg) #define wil_dbg_fw(wil, fmt, arg...) wil_dbg(wil, "DBG[ FW ]" fmt, ##arg) #define wil_hex_dump_fw(prefix_str, prefix_type, rowsize, \ groupsize, buf, len, ascii) \ print_hex_dump_debug("DBG[ FW ]" prefix_str, \ prefix_type, rowsize, \ groupsize, buf, len, ascii) static bool wil_fw_addr_check(struct wil6210_priv *wil, void __iomem **ioaddr, __le32 val, u32 size, const char *msg) { *ioaddr = wmi_buffer_block(wil, val, size); if (!(*ioaddr)) { wil_err_fw(wil, "bad %s: 0x%08x\n", msg, le32_to_cpu(val)); return false; } return true; } /** * wil_fw_verify - verify firmware file validity * * perform various checks for the firmware file header. * records are not validated. * * Return file size or negative error */ static int wil_fw_verify(struct wil6210_priv *wil, const u8 *data, size_t size) { const struct wil_fw_record_head *hdr = (const void *)data; struct wil_fw_record_file_header fh; const struct wil_fw_record_file_header *fh_; u32 crc; u32 dlen; if (size % 4) { wil_err_fw(wil, "image size not aligned: %zu\n", size); return -EINVAL; } /* have enough data for the file header? */ if (size < sizeof(*hdr) + sizeof(fh)) { wil_err_fw(wil, "file too short: %zu bytes\n", size); return -EINVAL; } /* start with the file header? */ if (le16_to_cpu(hdr->type) != wil_fw_type_file_header) { wil_err_fw(wil, "no file header\n"); return -EINVAL; } /* data_len */ fh_ = (struct wil_fw_record_file_header *)&hdr[1]; dlen = le32_to_cpu(fh_->data_len); if (dlen % 4) { wil_err_fw(wil, "data length not aligned: %lu\n", (ulong)dlen); return -EINVAL; } if (size < dlen) { wil_err_fw(wil, "file truncated at %zu/%lu\n", size, (ulong)dlen); return -EINVAL; } if (dlen < sizeof(*hdr) + sizeof(fh)) { wil_err_fw(wil, "data length too short: %lu\n", (ulong)dlen); return -EINVAL; } /* signature */ if (le32_to_cpu(fh_->signature) != WIL_FW_SIGNATURE) { wil_err_fw(wil, "bad header signature: 0x%08x\n", le32_to_cpu(fh_->signature)); return -EINVAL; } /* version */ if (le32_to_cpu(fh_->version) > WIL_FW_FMT_VERSION) { wil_err_fw(wil, "unsupported header version: %d\n", le32_to_cpu(fh_->version)); return -EINVAL; } /* checksum. ~crc32(~0, data, size) when fh.crc set to 0*/ fh = *fh_; fh.crc = 0; crc = crc32_le(~0, (unsigned char const *)hdr, sizeof(*hdr)); crc = crc32_le(crc, (unsigned char const *)&fh, sizeof(fh)); crc = crc32_le(crc, (unsigned char const *)&fh_[1], dlen - sizeof(*hdr) - sizeof(fh)); crc = ~crc; if (crc != le32_to_cpu(fh_->crc)) { wil_err_fw(wil, "checksum mismatch:" " calculated for %lu bytes 0x%08x != 0x%08x\n", (ulong)dlen, crc, le32_to_cpu(fh_->crc)); return -EINVAL; } return (int)dlen; } static int fw_ignore_section(struct wil6210_priv *wil, const void *data, size_t size) { return 0; } static int fw_handle_capabilities(struct wil6210_priv *wil, const void *data, size_t size) { const struct wil_fw_record_capabilities *rec = data; size_t capa_size; if (size < sizeof(*rec)) { wil_err_fw(wil, "capabilities record too short: %zu\n", size); /* let the FW load anyway */ return 0; } capa_size = size - offsetof(struct wil_fw_record_capabilities, capabilities); bitmap_zero(wil->fw_capabilities, WMI_FW_CAPABILITY_MAX); memcpy(wil->fw_capabilities, rec->capabilities, min_t(size_t, sizeof(wil->fw_capabilities), capa_size)); wil_hex_dump_fw("CAPA", DUMP_PREFIX_OFFSET, 16, 1, rec->capabilities, capa_size, false); return 0; } static int fw_handle_brd_file(struct wil6210_priv *wil, const void *data, size_t size) { const struct wil_fw_record_brd_file *rec = data; u32 max_num_ent, i, ent_size; if (size <= offsetof(struct wil_fw_record_brd_file, brd_info)) { wil_err(wil, "board record too short, size %zu\n", size); return -EINVAL; } ent_size = size - offsetof(struct wil_fw_record_brd_file, brd_info); max_num_ent = ent_size / sizeof(struct brd_info); if (!max_num_ent) { wil_err(wil, "brd info entries are missing\n"); return -EINVAL; } wil->brd_info = kcalloc(max_num_ent, sizeof(struct wil_brd_info), GFP_KERNEL); if (!wil->brd_info) return -ENOMEM; for (i = 0; i < max_num_ent; i++) { wil->brd_info[i].file_addr = le32_to_cpu(rec->brd_info[i].base_addr); wil->brd_info[i].file_max_size = le32_to_cpu(rec->brd_info[i].max_size_bytes); if (!wil->brd_info[i].file_addr) break; wil_dbg_fw(wil, "brd info %d: file_addr 0x%x, file_max_size %d\n", i, wil->brd_info[i].file_addr, wil->brd_info[i].file_max_size); } wil->num_of_brd_entries = i; if (wil->num_of_brd_entries == 0) { kfree(wil->brd_info); wil->brd_info = NULL; wil_dbg_fw(wil, "no valid brd info entries, using brd file addr\n"); } else { wil_dbg_fw(wil, "num of brd info entries %d\n", wil->num_of_brd_entries); } return 0; } static int fw_handle_concurrency(struct wil6210_priv *wil, const void *data, size_t size) { const struct wil_fw_record_concurrency *rec = data; const struct wil_fw_concurrency_combo *combo; const struct wil_fw_concurrency_limit *limit; size_t remain, lsize; int i, n_combos; if (size < sizeof(*rec)) { wil_err_fw(wil, "concurrency record too short: %zu\n", size); /* continue, let the FW load anyway */ return 0; } n_combos = le16_to_cpu(rec->n_combos); remain = size - offsetof(struct wil_fw_record_concurrency, combos); combo = rec->combos; for (i = 0; i < n_combos; i++) { if (remain < sizeof(*combo)) goto out_short; remain -= sizeof(*combo); limit = combo->limits; lsize = combo->n_limits * sizeof(*limit); if (remain < lsize) goto out_short; remain -= lsize; limit += combo->n_limits; combo = (struct wil_fw_concurrency_combo *)limit; } return wil_cfg80211_iface_combinations_from_fw(wil, rec); out_short: wil_err_fw(wil, "concurrency record truncated\n"); return 0; } static int fw_handle_comment(struct wil6210_priv *wil, const void *data, size_t size) { const struct wil_fw_record_comment_hdr *hdr = data; u32 magic; int rc = 0; if (size < sizeof(*hdr)) return 0; magic = le32_to_cpu(hdr->magic); switch (magic) { case WIL_FW_CAPABILITIES_MAGIC: wil_dbg_fw(wil, "magic is WIL_FW_CAPABILITIES_MAGIC\n"); rc = fw_handle_capabilities(wil, data, size); break; case WIL_BRD_FILE_MAGIC: wil_dbg_fw(wil, "magic is WIL_BRD_FILE_MAGIC\n"); rc = fw_handle_brd_file(wil, data, size); break; case WIL_FW_CONCURRENCY_MAGIC: wil_dbg_fw(wil, "magic is WIL_FW_CONCURRENCY_MAGIC\n"); rc = fw_handle_concurrency(wil, data, size); break; default: wil_hex_dump_fw("", DUMP_PREFIX_OFFSET, 16, 1, data, size, true); } return rc; } static int __fw_handle_data(struct wil6210_priv *wil, const void *data, size_t size, __le32 addr) { const struct wil_fw_record_data *d = data; void __iomem *dst; size_t s = size - sizeof(*d); if (size < sizeof(*d) + sizeof(u32)) { wil_err_fw(wil, "data record too short: %zu\n", size); return -EINVAL; } if (!wil_fw_addr_check(wil, &dst, addr, s, "address")) return -EINVAL; wil_dbg_fw(wil, "write [0x%08x] <== %zu bytes\n", le32_to_cpu(addr), s); wil_memcpy_toio_32(dst, d->data, s); wmb(); /* finish before processing next record */ return 0; } static int fw_handle_data(struct wil6210_priv *wil, const void *data, size_t size) { const struct wil_fw_record_data *d = data; return __fw_handle_data(wil, data, size, d->addr); } static int fw_handle_fill(struct wil6210_priv *wil, const void *data, size_t size) { const struct wil_fw_record_fill *d = data; void __iomem *dst; u32 v; size_t s = (size_t)le32_to_cpu(d->size); if (size != sizeof(*d)) { wil_err_fw(wil, "bad size for fill record: %zu\n", size); return -EINVAL; } if (s < sizeof(u32)) { wil_err_fw(wil, "fill size too short: %zu\n", s); return -EINVAL; } if (s % sizeof(u32)) { wil_err_fw(wil, "fill size not aligned: %zu\n", s); return -EINVAL; } if (!wil_fw_addr_check(wil, &dst, d->addr, s, "address")) return -EINVAL; v = le32_to_cpu(d->value); wil_dbg_fw(wil, "fill [0x%08x] <== 0x%08x, %zu bytes\n", le32_to_cpu(d->addr), v, s); wil_memset_toio_32(dst, v, s); wmb(); /* finish before processing next record */ return 0; } static int fw_handle_file_header(struct wil6210_priv *wil, const void *data, size_t size) { const struct wil_fw_record_file_header *d = data; if (size != sizeof(*d)) { wil_err_fw(wil, "file header length incorrect: %zu\n", size); return -EINVAL; } wil_dbg_fw(wil, "new file, ver. %d, %i bytes\n", d->version, d->data_len); wil_hex_dump_fw("", DUMP_PREFIX_OFFSET, 16, 1, d->comment, sizeof(d->comment), true); if (!memcmp(d->comment, WIL_FW_VERSION_PREFIX, WIL_FW_VERSION_PREFIX_LEN)) memcpy(wil->fw_version, d->comment + WIL_FW_VERSION_PREFIX_LEN, min(sizeof(d->comment) - WIL_FW_VERSION_PREFIX_LEN, sizeof(wil->fw_version) - 1)); return 0; } static int fw_handle_direct_write(struct wil6210_priv *wil, const void *data, size_t size) { const struct wil_fw_record_direct_write *d = data; const struct wil_fw_data_dwrite *block = d->data; int n, i; if (size % sizeof(*block)) { wil_err_fw(wil, "record size not aligned on %zu: %zu\n", sizeof(*block), size); return -EINVAL; } n = size / sizeof(*block); for (i = 0; i < n; i++) { void __iomem *dst; u32 m = le32_to_cpu(block[i].mask); u32 v = le32_to_cpu(block[i].value); u32 x, y; if (!wil_fw_addr_check(wil, &dst, block[i].addr, 0, "address")) return -EINVAL; x = readl(dst); y = (x & m) | (v & ~m); wil_dbg_fw(wil, "write [0x%08x] <== 0x%08x " "(old 0x%08x val 0x%08x mask 0x%08x)\n", le32_to_cpu(block[i].addr), y, x, v, m); writel(y, dst); wmb(); /* finish before processing next record */ } return 0; } static int gw_write(struct wil6210_priv *wil, void __iomem *gwa_addr, void __iomem *gwa_cmd, void __iomem *gwa_ctl, u32 gw_cmd, u32 a) { unsigned delay = 0; writel(a, gwa_addr); writel(gw_cmd, gwa_cmd); wmb(); /* finish before activate gw */ writel(WIL_FW_GW_CTL_RUN, gwa_ctl); /* activate gw */ do { udelay(1); /* typical time is few usec */ if (delay++ > 100) { wil_err_fw(wil, "gw timeout\n"); return -EINVAL; } } while (readl(gwa_ctl) & WIL_FW_GW_CTL_BUSY); /* gw done? */ return 0; } static int fw_handle_gateway_data(struct wil6210_priv *wil, const void *data, size_t size) { const struct wil_fw_record_gateway_data *d = data; const struct wil_fw_data_gw *block = d->data; void __iomem *gwa_addr; void __iomem *gwa_val; void __iomem *gwa_cmd; void __iomem *gwa_ctl; u32 gw_cmd; int n, i; if (size < sizeof(*d) + sizeof(*block)) { wil_err_fw(wil, "gateway record too short: %zu\n", size); return -EINVAL; } if ((size - sizeof(*d)) % sizeof(*block)) { wil_err_fw(wil, "gateway record data size" " not aligned on %zu: %zu\n", sizeof(*block), size - sizeof(*d)); return -EINVAL; } n = (size - sizeof(*d)) / sizeof(*block); gw_cmd = le32_to_cpu(d->command); wil_dbg_fw(wil, "gw write record [%3d] blocks, cmd 0x%08x\n", n, gw_cmd); if (!wil_fw_addr_check(wil, &gwa_addr, d->gateway_addr_addr, 0, "gateway_addr_addr") || !wil_fw_addr_check(wil, &gwa_val, d->gateway_value_addr, 0, "gateway_value_addr") || !wil_fw_addr_check(wil, &gwa_cmd, d->gateway_cmd_addr, 0, "gateway_cmd_addr") || !wil_fw_addr_check(wil, &gwa_ctl, d->gateway_ctrl_address, 0, "gateway_ctrl_address")) return -EINVAL; wil_dbg_fw(wil, "gw addresses: addr 0x%08x val 0x%08x" " cmd 0x%08x ctl 0x%08x\n", le32_to_cpu(d->gateway_addr_addr), le32_to_cpu(d->gateway_value_addr), le32_to_cpu(d->gateway_cmd_addr), le32_to_cpu(d->gateway_ctrl_address)); for (i = 0; i < n; i++) { int rc; u32 a = le32_to_cpu(block[i].addr); u32 v = le32_to_cpu(block[i].value); wil_dbg_fw(wil, " gw write[%3d] [0x%08x] <== 0x%08x\n", i, a, v); writel(v, gwa_val); rc = gw_write(wil, gwa_addr, gwa_cmd, gwa_ctl, gw_cmd, a); if (rc) return rc; } return 0; } static int fw_handle_gateway_data4(struct wil6210_priv *wil, const void *data, size_t size) { const struct wil_fw_record_gateway_data4 *d = data; const struct wil_fw_data_gw4 *block = d->data; void __iomem *gwa_addr; void __iomem *gwa_val[ARRAY_SIZE(block->value)]; void __iomem *gwa_cmd; void __iomem *gwa_ctl; u32 gw_cmd; int n, i, k; if (size < sizeof(*d) + sizeof(*block)) { wil_err_fw(wil, "gateway4 record too short: %zu\n", size); return -EINVAL; } if ((size - sizeof(*d)) % sizeof(*block)) { wil_err_fw(wil, "gateway4 record data size" " not aligned on %zu: %zu\n", sizeof(*block), size - sizeof(*d)); return -EINVAL; } n = (size - sizeof(*d)) / sizeof(*block); gw_cmd = le32_to_cpu(d->command); wil_dbg_fw(wil, "gw4 write record [%3d] blocks, cmd 0x%08x\n", n, gw_cmd); if (!wil_fw_addr_check(wil, &gwa_addr, d->gateway_addr_addr, 0, "gateway_addr_addr")) return -EINVAL; for (k = 0; k < ARRAY_SIZE(block->value); k++) if (!wil_fw_addr_check(wil, &gwa_val[k], d->gateway_value_addr[k], 0, "gateway_value_addr")) return -EINVAL; if (!wil_fw_addr_check(wil, &gwa_cmd, d->gateway_cmd_addr, 0, "gateway_cmd_addr") || !wil_fw_addr_check(wil, &gwa_ctl, d->gateway_ctrl_address, 0, "gateway_ctrl_address")) return -EINVAL; wil_dbg_fw(wil, "gw4 addresses: addr 0x%08x cmd 0x%08x ctl 0x%08x\n", le32_to_cpu(d->gateway_addr_addr), le32_to_cpu(d->gateway_cmd_addr), le32_to_cpu(d->gateway_ctrl_address)); wil_hex_dump_fw("val addresses: ", DUMP_PREFIX_NONE, 16, 4, d->gateway_value_addr, sizeof(d->gateway_value_addr), false); for (i = 0; i < n; i++) { int rc; u32 a = le32_to_cpu(block[i].addr); u32 v[ARRAY_SIZE(block->value)]; for (k = 0; k < ARRAY_SIZE(block->value); k++) v[k] = le32_to_cpu(block[i].value[k]); wil_dbg_fw(wil, " gw4 write[%3d] [0x%08x] <==\n", i, a); wil_hex_dump_fw(" val ", DUMP_PREFIX_NONE, 16, 4, v, sizeof(v), false); for (k = 0; k < ARRAY_SIZE(block->value); k++) writel(v[k], gwa_val[k]); rc = gw_write(wil, gwa_addr, gwa_cmd, gwa_ctl, gw_cmd, a); if (rc) return rc; } return 0; } static const struct { int type; int (*load_handler)(struct wil6210_priv *wil, const void *data, size_t size); int (*parse_handler)(struct wil6210_priv *wil, const void *data, size_t size); } wil_fw_handlers[] = { {wil_fw_type_comment, fw_handle_comment, fw_handle_comment}, {wil_fw_type_data, fw_handle_data, fw_ignore_section}, {wil_fw_type_fill, fw_handle_fill, fw_ignore_section}, /* wil_fw_type_action */ /* wil_fw_type_verify */ {wil_fw_type_file_header, fw_handle_file_header, fw_handle_file_header}, {wil_fw_type_direct_write, fw_handle_direct_write, fw_ignore_section}, {wil_fw_type_gateway_data, fw_handle_gateway_data, fw_ignore_section}, {wil_fw_type_gateway_data4, fw_handle_gateway_data4, fw_ignore_section}, }; static int wil_fw_handle_record(struct wil6210_priv *wil, int type, const void *data, size_t size, bool load) { int i; for (i = 0; i < ARRAY_SIZE(wil_fw_handlers); i++) if (wil_fw_handlers[i].type == type) return load ? wil_fw_handlers[i].load_handler( wil, data, size) : wil_fw_handlers[i].parse_handler( wil, data, size); wil_err_fw(wil, "unknown record type: %d\n", type); return -EINVAL; } /** * wil_fw_process - process section from FW file * if load is true: Load the FW and uCode code and data to the * corresponding device memory regions, * otherwise only parse and look for capabilities * * Return error code */ static int wil_fw_process(struct wil6210_priv *wil, const void *data, size_t size, bool load) { int rc = 0; const struct wil_fw_record_head *hdr; size_t s, hdr_sz; for (hdr = data;; hdr = (const void *)hdr + s, size -= s) { if (size < sizeof(*hdr)) break; hdr_sz = le32_to_cpu(hdr->size); s = sizeof(*hdr) + hdr_sz; if (s > size) break; if (hdr_sz % 4) { wil_err_fw(wil, "unaligned record size: %zu\n", hdr_sz); return -EINVAL; } rc = wil_fw_handle_record(wil, le16_to_cpu(hdr->type), &hdr[1], hdr_sz, load); if (rc) return rc; } if (size) { wil_err_fw(wil, "unprocessed bytes: %zu\n", size); if (size >= sizeof(*hdr)) { wil_err_fw(wil, "Stop at offset %ld" " record type %d [%zd bytes]\n", (long)((const void *)hdr - data), le16_to_cpu(hdr->type), hdr_sz); } return -EINVAL; } return rc; } /** * wil_request_firmware - Request firmware * * Request firmware image from the file * If load is true, load firmware to device, otherwise * only parse and extract capabilities * * Return error code */ int wil_request_firmware(struct wil6210_priv *wil, const char *name, bool load) { int rc, rc1; const struct firmware *fw; size_t sz; const void *d; rc = request_firmware(&fw, name, wil_to_dev(wil)); if (rc) { wil_err_fw(wil, "Failed to load firmware %s rc %d\n", name, rc); return rc; } wil_dbg_fw(wil, "Loading <%s>, %zu bytes\n", name, fw->size); /* re-initialize board info params */ wil->num_of_brd_entries = 0; kfree(wil->brd_info); wil->brd_info = NULL; for (sz = fw->size, d = fw->data; sz; sz -= rc1, d += rc1) { rc1 = wil_fw_verify(wil, d, sz); if (rc1 < 0) { rc = rc1; goto out; } rc = wil_fw_process(wil, d, rc1, load); if (rc < 0) goto out; } out: release_firmware(fw); if (rc) wil_err_fw(wil, "Loading <%s> failed, rc %d\n", name, rc); return rc; } /** * wil_brd_process - process section from BRD file * * Return error code */ static int wil_brd_process(struct wil6210_priv *wil, const void *data, size_t size) { int rc = 0; const struct wil_fw_record_head *hdr = data; size_t s, hdr_sz = 0; u16 type; int i = 0; /* Assuming the board file includes only one file header * and one or several data records. * Each record starts with wil_fw_record_head. */ if (size < sizeof(*hdr)) return -EINVAL; s = sizeof(*hdr) + le32_to_cpu(hdr->size); if (s > size) return -EINVAL; /* Skip the header record and handle the data records */ size -= s; for (hdr = data + s;; hdr = (const void *)hdr + s, size -= s, i++) { if (size < sizeof(*hdr)) break; if (i >= wil->num_of_brd_entries) { wil_err_fw(wil, "Too many brd records: %d, num of expected entries %d\n", i, wil->num_of_brd_entries); break; } hdr_sz = le32_to_cpu(hdr->size); s = sizeof(*hdr) + hdr_sz; if (wil->brd_info[i].file_max_size && hdr_sz > wil->brd_info[i].file_max_size) return -EINVAL; if (sizeof(*hdr) + hdr_sz > size) return -EINVAL; if (hdr_sz % 4) { wil_err_fw(wil, "unaligned record size: %zu\n", hdr_sz); return -EINVAL; } type = le16_to_cpu(hdr->type); if (type != wil_fw_type_data) { wil_err_fw(wil, "invalid record type for board file: %d\n", type); return -EINVAL; } if (hdr_sz < sizeof(struct wil_fw_record_data)) { wil_err_fw(wil, "data record too short: %zu\n", hdr_sz); return -EINVAL; } wil_dbg_fw(wil, "using info from fw file for record %d: addr[0x%08x], max size %d\n", i, wil->brd_info[i].file_addr, wil->brd_info[i].file_max_size); rc = __fw_handle_data(wil, &hdr[1], hdr_sz, cpu_to_le32(wil->brd_info[i].file_addr)); if (rc) return rc; } if (size) { wil_err_fw(wil, "unprocessed bytes: %zu\n", size); if (size >= sizeof(*hdr)) { wil_err_fw(wil, "Stop at offset %ld record type %d [%zd bytes]\n", (long)((const void *)hdr - data), le16_to_cpu(hdr->type), hdr_sz); } return -EINVAL; } return 0; } /** * wil_request_board - Request board file * * Request board image from the file * board file address and max size are read from FW file * during initialization. * brd file shall include one header and one data section. * * Return error code */ int wil_request_board(struct wil6210_priv *wil, const char *name) { int rc, dlen; const struct firmware *brd; rc = request_firmware(&brd, name, wil_to_dev(wil)); if (rc) { wil_err_fw(wil, "Failed to load brd %s\n", name); return rc; } wil_dbg_fw(wil, "Loading <%s>, %zu bytes\n", name, brd->size); /* Verify the header */ dlen = wil_fw_verify(wil, brd->data, brd->size); if (dlen < 0) { rc = dlen; goto out; } /* Process the data records */ rc = wil_brd_process(wil, brd->data, dlen); out: release_firmware(brd); if (rc) wil_err_fw(wil, "Loading <%s> failed, rc %d\n", name, rc); return rc; } /** * wil_fw_verify_file_exists - checks if firmware file exist * * @wil: driver context * @name: firmware file name * * return value - boolean, true for success, false for failure */ bool wil_fw_verify_file_exists(struct wil6210_priv *wil, const char *name) { const struct firmware *fw; int rc; rc = request_firmware(&fw, name, wil_to_dev(wil)); if (!rc) release_firmware(fw); else wil_dbg_fw(wil, "<%s> not available: %d\n", name, rc); return !rc; }