// SPDX-License-Identifier: GPL-2.0 #include #include #include #include #include #include #include "aquantia.h" #define UP_RESET_SLEEP 100 /* addresses of memory segments in the phy */ #define DRAM_BASE_ADDR 0x3FFE0000 #define IRAM_BASE_ADDR 0x40000000 /* firmware image format constants */ #define VERSION_STRING_SIZE 0x40 #define VERSION_STRING_OFFSET 0x0200 /* primary offset is written at an offset from the start of the fw blob */ #define PRIMARY_OFFSET_OFFSET 0x8 /* primary offset needs to be then added to a base offset */ #define PRIMARY_OFFSET_SHIFT 12 #define PRIMARY_OFFSET(x) ((x) << PRIMARY_OFFSET_SHIFT) #define HEADER_OFFSET 0x300 struct aqr_fw_header { u32 padding; u8 iram_offset[3]; u8 iram_size[3]; u8 dram_offset[3]; u8 dram_size[3]; } __packed; enum aqr_fw_src { AQR_FW_SRC_NVMEM = 0, AQR_FW_SRC_FS, }; static const char * const aqr_fw_src_string[] = { [AQR_FW_SRC_NVMEM] = "NVMEM", [AQR_FW_SRC_FS] = "FS", }; /* AQR firmware doesn't have fixed offsets for iram and dram section * but instead provide an header with the offset to use on reading * and parsing the firmware. * * AQR firmware can't be trusted and each offset is validated to be * not negative and be in the size of the firmware itself. */ static bool aqr_fw_validate_get(size_t size, size_t offset, size_t get_size) { return offset + get_size <= size; } static int aqr_fw_get_be16(const u8 *data, size_t offset, size_t size, u16 *value) { if (!aqr_fw_validate_get(size, offset, sizeof(u16))) return -EINVAL; *value = get_unaligned_be16(data + offset); return 0; } static int aqr_fw_get_le16(const u8 *data, size_t offset, size_t size, u16 *value) { if (!aqr_fw_validate_get(size, offset, sizeof(u16))) return -EINVAL; *value = get_unaligned_le16(data + offset); return 0; } static int aqr_fw_get_le24(const u8 *data, size_t offset, size_t size, u32 *value) { if (!aqr_fw_validate_get(size, offset, sizeof(u8) * 3)) return -EINVAL; *value = get_unaligned_le24(data + offset); return 0; } /* load data into the phy's memory */ static int aqr_fw_load_memory(struct phy_device *phydev, u32 addr, const u8 *data, size_t len) { u16 crc = 0, up_crc; size_t pos; /* PHY expect addr in LE */ addr = (__force u32)cpu_to_le32(addr); phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_MAILBOX_INTERFACE1, VEND1_GLOBAL_MAILBOX_INTERFACE1_CRC_RESET); phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_MAILBOX_INTERFACE3, VEND1_GLOBAL_MAILBOX_INTERFACE3_MSW_ADDR(addr)); phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_MAILBOX_INTERFACE4, VEND1_GLOBAL_MAILBOX_INTERFACE4_LSW_ADDR(addr)); /* We assume and enforce the size to be word aligned. * If a firmware that is not word aligned is found, please report upstream. */ for (pos = 0; pos < len; pos += sizeof(u32)) { u32 word; /* FW data is always stored in little-endian */ word = get_unaligned((const u32 *)(data + pos)); phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_MAILBOX_INTERFACE5, VEND1_GLOBAL_MAILBOX_INTERFACE5_MSW_DATA(word)); phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_MAILBOX_INTERFACE6, VEND1_GLOBAL_MAILBOX_INTERFACE6_LSW_DATA(word)); phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_MAILBOX_INTERFACE1, VEND1_GLOBAL_MAILBOX_INTERFACE1_EXECUTE | VEND1_GLOBAL_MAILBOX_INTERFACE1_WRITE); /* calculate CRC as we load data to the mailbox. * We convert word to big-endian as PHY is BE and mailbox will * return a BE CRC. */ word = (__force u32)cpu_to_be32(word); crc = crc_ccitt_false(crc, (u8 *)&word, sizeof(word)); } up_crc = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_MAILBOX_INTERFACE2); if (crc != up_crc) { phydev_err(phydev, "CRC mismatch: calculated 0x%04x PHY 0x%04x\n", crc, up_crc); return -EINVAL; } return 0; } static int aqr_fw_boot(struct phy_device *phydev, const u8 *data, size_t size, enum aqr_fw_src fw_src) { u16 calculated_crc, read_crc, read_primary_offset; u32 iram_offset = 0, iram_size = 0; u32 dram_offset = 0, dram_size = 0; char version[VERSION_STRING_SIZE]; u32 primary_offset = 0; int ret; /* extract saved CRC at the end of the fw * CRC is saved in big-endian as PHY is BE */ ret = aqr_fw_get_be16(data, size - sizeof(u16), size, &read_crc); if (ret) { phydev_err(phydev, "bad firmware CRC in firmware\n"); return ret; } calculated_crc = crc_ccitt_false(0, data, size - sizeof(u16)); if (read_crc != calculated_crc) { phydev_err(phydev, "bad firmware CRC: file 0x%04x calculated 0x%04x\n", read_crc, calculated_crc); return -EINVAL; } /* Get the primary offset to extract DRAM and IRAM sections. */ ret = aqr_fw_get_le16(data, PRIMARY_OFFSET_OFFSET, size, &read_primary_offset); if (ret) { phydev_err(phydev, "bad primary offset in firmware\n"); return ret; } primary_offset = PRIMARY_OFFSET(read_primary_offset); /* Find the DRAM and IRAM sections within the firmware file. * Make sure the fw_header is correctly in the firmware. */ if (!aqr_fw_validate_get(size, primary_offset + HEADER_OFFSET, sizeof(struct aqr_fw_header))) { phydev_err(phydev, "bad fw_header in firmware\n"); return -EINVAL; } /* offset are in LE and values needs to be converted to cpu endian */ ret = aqr_fw_get_le24(data, primary_offset + HEADER_OFFSET + offsetof(struct aqr_fw_header, iram_offset), size, &iram_offset); if (ret) { phydev_err(phydev, "bad iram offset in firmware\n"); return ret; } ret = aqr_fw_get_le24(data, primary_offset + HEADER_OFFSET + offsetof(struct aqr_fw_header, iram_size), size, &iram_size); if (ret) { phydev_err(phydev, "invalid iram size in firmware\n"); return ret; } ret = aqr_fw_get_le24(data, primary_offset + HEADER_OFFSET + offsetof(struct aqr_fw_header, dram_offset), size, &dram_offset); if (ret) { phydev_err(phydev, "bad dram offset in firmware\n"); return ret; } ret = aqr_fw_get_le24(data, primary_offset + HEADER_OFFSET + offsetof(struct aqr_fw_header, dram_size), size, &dram_size); if (ret) { phydev_err(phydev, "invalid dram size in firmware\n"); return ret; } /* Increment the offset with the primary offset. * Validate iram/dram offset and size. */ iram_offset += primary_offset; if (iram_size % sizeof(u32)) { phydev_err(phydev, "iram size if not aligned to word size. Please report this upstream!\n"); return -EINVAL; } if (!aqr_fw_validate_get(size, iram_offset, iram_size)) { phydev_err(phydev, "invalid iram offset for iram size\n"); return -EINVAL; } dram_offset += primary_offset; if (dram_size % sizeof(u32)) { phydev_err(phydev, "dram size if not aligned to word size. Please report this upstream!\n"); return -EINVAL; } if (!aqr_fw_validate_get(size, dram_offset, dram_size)) { phydev_err(phydev, "invalid iram offset for iram size\n"); return -EINVAL; } phydev_dbg(phydev, "primary %d IRAM offset=%d size=%d DRAM offset=%d size=%d\n", primary_offset, iram_offset, iram_size, dram_offset, dram_size); if (!aqr_fw_validate_get(size, dram_offset + VERSION_STRING_OFFSET, VERSION_STRING_SIZE)) { phydev_err(phydev, "invalid version in firmware\n"); return -EINVAL; } strscpy(version, (char *)data + dram_offset + VERSION_STRING_OFFSET, VERSION_STRING_SIZE); if (version[0] == '\0') { phydev_err(phydev, "invalid version in firmware\n"); return -EINVAL; } phydev_info(phydev, "loading firmware version '%s' from '%s'\n", version, aqr_fw_src_string[fw_src]); /* stall the microcprocessor */ phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_CONTROL2, VEND1_GLOBAL_CONTROL2_UP_RUN_STALL | VEND1_GLOBAL_CONTROL2_UP_RUN_STALL_OVD); phydev_dbg(phydev, "loading DRAM 0x%08x from offset=%d size=%d\n", DRAM_BASE_ADDR, dram_offset, dram_size); ret = aqr_fw_load_memory(phydev, DRAM_BASE_ADDR, data + dram_offset, dram_size); if (ret) return ret; phydev_dbg(phydev, "loading IRAM 0x%08x from offset=%d size=%d\n", IRAM_BASE_ADDR, iram_offset, iram_size); ret = aqr_fw_load_memory(phydev, IRAM_BASE_ADDR, data + iram_offset, iram_size); if (ret) return ret; /* make sure soft reset and low power mode are clear */ phy_clear_bits_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_SC, VEND1_GLOBAL_SC_SOFT_RESET | VEND1_GLOBAL_SC_LOW_POWER); /* Release the microprocessor. UP_RESET must be held for 100 usec. */ phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_CONTROL2, VEND1_GLOBAL_CONTROL2_UP_RUN_STALL | VEND1_GLOBAL_CONTROL2_UP_RUN_STALL_OVD | VEND1_GLOBAL_CONTROL2_UP_RUN_STALL_RST); usleep_range(UP_RESET_SLEEP, UP_RESET_SLEEP * 2); phy_write_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_CONTROL2, VEND1_GLOBAL_CONTROL2_UP_RUN_STALL_OVD); return 0; } static int aqr_firmware_load_nvmem(struct phy_device *phydev) { struct nvmem_cell *cell; size_t size; u8 *buf; int ret; cell = nvmem_cell_get(&phydev->mdio.dev, "firmware"); if (IS_ERR(cell)) return PTR_ERR(cell); buf = nvmem_cell_read(cell, &size); if (IS_ERR(buf)) { ret = PTR_ERR(buf); goto exit; } ret = aqr_fw_boot(phydev, buf, size, AQR_FW_SRC_NVMEM); if (ret) phydev_err(phydev, "firmware loading failed: %d\n", ret); kfree(buf); exit: nvmem_cell_put(cell); return ret; } static int aqr_firmware_load_fs(struct phy_device *phydev) { struct device *dev = &phydev->mdio.dev; const struct firmware *fw; const char *fw_name; int ret; ret = of_property_read_string(dev->of_node, "firmware-name", &fw_name); if (ret) return ret; ret = request_firmware(&fw, fw_name, dev); if (ret) { phydev_err(phydev, "failed to find FW file %s (%d)\n", fw_name, ret); return ret; } ret = aqr_fw_boot(phydev, fw->data, fw->size, AQR_FW_SRC_FS); if (ret) phydev_err(phydev, "firmware loading failed: %d\n", ret); release_firmware(fw); return ret; } int aqr_firmware_load(struct phy_device *phydev) { int ret; /* Check if the firmware is not already loaded by pooling * the current version returned by the PHY. If 0 is returned, * no firmware is loaded. */ ret = phy_read_mmd(phydev, MDIO_MMD_VEND1, VEND1_GLOBAL_FW_ID); if (ret > 0) goto exit; ret = aqr_firmware_load_nvmem(phydev); if (!ret) goto exit; ret = aqr_firmware_load_fs(phydev); if (ret) return ret; exit: return 0; }