]> www.infradead.org Git - users/dwmw2/linux.git/commitdiff
net: phy: aquantia: add firmware load support
authorRobert Marko <robimarko@gmail.com>
Tue, 14 Nov 2023 14:08:43 +0000 (15:08 +0100)
committerDavid S. Miller <davem@davemloft.net>
Thu, 16 Nov 2023 22:10:29 +0000 (22:10 +0000)
Aquantia PHY-s require firmware to be loaded before they start operating.
It can be automatically loaded in case when there is a SPI-NOR connected
to Aquantia PHY-s or can be loaded from the host via MDIO.

This patch adds support for loading the firmware via MDIO as in most cases
there is no SPI-NOR being used to save on cost.
Firmware loading code itself is ported from mainline U-boot with cleanups.

The firmware has mixed values both in big and little endian.
PHY core itself is big-endian but it expects values to be in little-endian.
The firmware is little-endian but CRC-16 value for it is stored at the end
of firmware in big-endian.

It seems the PHY does the conversion internally from firmware that is
little-endian to the PHY that is big-endian on using the mailbox
but mailbox returns a big-endian CRC-16 to verify the written data
integrity.

Co-developed-by: Christian Marangi <ansuelsmth@gmail.com>
Signed-off-by: Robert Marko <robimarko@gmail.com>
Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Signed-off-by: David S. Miller <davem@davemloft.net>
drivers/net/phy/aquantia/Kconfig
drivers/net/phy/aquantia/Makefile
drivers/net/phy/aquantia/aquantia.h
drivers/net/phy/aquantia/aquantia_firmware.c [new file with mode: 0644]
drivers/net/phy/aquantia/aquantia_main.c

index 226146417a6a7a31c09aa9f653d48f1a87130d57..a35de4b9b5545bd9b9301b06791bba050e2ecdfb 100644 (file)
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: GPL-2.0-only
 config AQUANTIA_PHY
        tristate "Aquantia PHYs"
+       select CRC_CCITT
        help
          Currently supports the Aquantia AQ1202, AQ2104, AQR105, AQR405
index 346f350bc0846b648411fca3b18dd81afdc394ec..aa77fb63c8ecf13a5cb9ab091eb1278c381eade2 100644 (file)
@@ -1,5 +1,5 @@
 # SPDX-License-Identifier: GPL-2.0
-aquantia-objs                  += aquantia_main.o
+aquantia-objs                  += aquantia_main.o aquantia_firmware.o
 ifdef CONFIG_HWMON
 aquantia-objs                  += aquantia_hwmon.o
 endif
index f0c767c4fad1bd6b35a25c67ff0bbd681bcb4b21..9ed38972abdbcbc4251178058193bbd0502a4f07 100644 (file)
 #include <linux/phy.h>
 
 /* Vendor specific 1, MDIO_MMD_VEND1 */
+#define VEND1_GLOBAL_SC                                0x0
+#define VEND1_GLOBAL_SC_SOFT_RESET             BIT(15)
+#define VEND1_GLOBAL_SC_LOW_POWER              BIT(11)
+
 #define VEND1_GLOBAL_FW_ID                     0x0020
 #define VEND1_GLOBAL_FW_ID_MAJOR               GENMASK(15, 8)
 #define VEND1_GLOBAL_FW_ID_MINOR               GENMASK(7, 0)
 
+#define VEND1_GLOBAL_MAILBOX_INTERFACE1                        0x0200
+#define VEND1_GLOBAL_MAILBOX_INTERFACE1_EXECUTE                BIT(15)
+#define VEND1_GLOBAL_MAILBOX_INTERFACE1_WRITE          BIT(14)
+#define VEND1_GLOBAL_MAILBOX_INTERFACE1_CRC_RESET      BIT(12)
+#define VEND1_GLOBAL_MAILBOX_INTERFACE1_BUSY           BIT(8)
+
+#define VEND1_GLOBAL_MAILBOX_INTERFACE2                        0x0201
+#define VEND1_GLOBAL_MAILBOX_INTERFACE3                        0x0202
+#define VEND1_GLOBAL_MAILBOX_INTERFACE3_MSW_ADDR_MASK  GENMASK(15, 0)
+#define VEND1_GLOBAL_MAILBOX_INTERFACE3_MSW_ADDR(x)    FIELD_PREP(VEND1_GLOBAL_MAILBOX_INTERFACE3_MSW_ADDR_MASK, (u16)((x) >> 16))
+#define VEND1_GLOBAL_MAILBOX_INTERFACE4                        0x0203
+#define VEND1_GLOBAL_MAILBOX_INTERFACE4_LSW_ADDR_MASK  GENMASK(15, 2)
+#define VEND1_GLOBAL_MAILBOX_INTERFACE4_LSW_ADDR(x)    FIELD_PREP(VEND1_GLOBAL_MAILBOX_INTERFACE4_LSW_ADDR_MASK, (u16)(x))
+
+#define VEND1_GLOBAL_MAILBOX_INTERFACE5                        0x0204
+#define VEND1_GLOBAL_MAILBOX_INTERFACE5_MSW_DATA_MASK  GENMASK(15, 0)
+#define VEND1_GLOBAL_MAILBOX_INTERFACE5_MSW_DATA(x)    FIELD_PREP(VEND1_GLOBAL_MAILBOX_INTERFACE5_MSW_DATA_MASK, (u16)((x) >> 16))
+#define VEND1_GLOBAL_MAILBOX_INTERFACE6                        0x0205
+#define VEND1_GLOBAL_MAILBOX_INTERFACE6_LSW_DATA_MASK  GENMASK(15, 0)
+#define VEND1_GLOBAL_MAILBOX_INTERFACE6_LSW_DATA(x)    FIELD_PREP(VEND1_GLOBAL_MAILBOX_INTERFACE6_LSW_DATA_MASK, (u16)(x))
+
 /* The following registers all have similar layouts; first the registers... */
 #define VEND1_GLOBAL_CFG_10M                   0x0310
 #define VEND1_GLOBAL_CFG_100M                  0x031b
 #define VEND1_GLOBAL_CFG_RATE_ADAPT_PAUSE      2
 
 /* Vendor specific 1, MDIO_MMD_VEND2 */
+#define VEND1_GLOBAL_CONTROL2                  0xc001
+#define VEND1_GLOBAL_CONTROL2_UP_RUN_STALL_RST BIT(15)
+#define VEND1_GLOBAL_CONTROL2_UP_RUN_STALL_OVD BIT(6)
+#define VEND1_GLOBAL_CONTROL2_UP_RUN_STALL     BIT(0)
+
 #define VEND1_THERMAL_PROV_HIGH_TEMP_FAIL      0xc421
 #define VEND1_THERMAL_PROV_LOW_TEMP_FAIL       0xc422
 #define VEND1_THERMAL_PROV_HIGH_TEMP_WARN      0xc423
@@ -83,3 +113,5 @@ int aqr_hwmon_probe(struct phy_device *phydev);
 #else
 static inline int aqr_hwmon_probe(struct phy_device *phydev) { return 0; }
 #endif
+
+int aqr_firmware_load(struct phy_device *phydev);
diff --git a/drivers/net/phy/aquantia/aquantia_firmware.c b/drivers/net/phy/aquantia/aquantia_firmware.c
new file mode 100644 (file)
index 0000000..c5f292b
--- /dev/null
@@ -0,0 +1,370 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <linux/bitfield.h>
+#include <linux/of.h>
+#include <linux/firmware.h>
+#include <linux/crc-ccitt.h>
+#include <linux/nvmem-consumer.h>
+
+#include <asm/unaligned.h>
+
+#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;
+}
index 4498426e9a52625ef00310029c94f6cb111ce1b4..cc4a97741c4a386872910bc7ce196ad070b52d8e 100644 (file)
@@ -658,11 +658,17 @@ static int aqr107_resume(struct phy_device *phydev)
 
 static int aqr107_probe(struct phy_device *phydev)
 {
+       int ret;
+
        phydev->priv = devm_kzalloc(&phydev->mdio.dev,
                                    sizeof(struct aqr107_priv), GFP_KERNEL);
        if (!phydev->priv)
                return -ENOMEM;
 
+       ret = aqr_firmware_load(phydev);
+       if (ret)
+               return ret;
+
        return aqr_hwmon_probe(phydev);
 }