General Electric Healthcare's PPD has a secondary processor from NXP's Kinetis K20 series. It's firmware can be updated from Linux using the EzPort interface. This driver implements the firmware updating process. Signed-off-by: Sebastian Reichel <sebastian.reichel@xxxxxxxxxxxxxxx> --- Documentation/devicetree/bindings/misc/ge-achc.txt | 19 +- drivers/misc/Kconfig | 9 + drivers/misc/Makefile | 1 + drivers/misc/ezport-firmware.c | 487 +++++++++++++++++++++ 4 files changed, 513 insertions(+), 3 deletions(-) create mode 100644 drivers/misc/ezport-firmware.c diff --git a/Documentation/devicetree/bindings/misc/ge-achc.txt b/Documentation/devicetree/bindings/misc/ge-achc.txt index 77df94d7a32f..6c6bd6568504 100644 --- a/Documentation/devicetree/bindings/misc/ge-achc.txt +++ b/Documentation/devicetree/bindings/misc/ge-achc.txt @@ -7,7 +7,13 @@ Note: This device does not expose the peripherals as USB devices. Required properties: -- compatible : Should be "ge,achc" +- compatible : Should be + "ge,achc" (normal interface) + "ge,achc-ezport" (flashing interface) + +Required properties (flashing interface only): + +- reset-gpios: GPIO Specifier for the reset GPIO Required SPI properties: @@ -19,8 +25,15 @@ Required SPI properties: Example: -spidev0: spi@0 { - compatible = "ge,achc"; +spidev1: spi@0 { + compatible = "ge,achc-ezport"; reg = <0>; + spi-max-frequency = <300000>; + reset-gpios = <&gpio3 6 GPIO_ACTIVE_LOW>; +}; + +spidev0: spi@1 { + compatible = "ge,achc"; + reg = <1>; spi-max-frequency = <1000000>; }; diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig index 03605f8fc0dc..841249111204 100644 --- a/drivers/misc/Kconfig +++ b/drivers/misc/Kconfig @@ -92,6 +92,15 @@ config DUMMY_IRQ The sole purpose of this module is to help with debugging of systems on which spurious IRQs would happen on disabled IRQ vector. +config EZPORT_FW + tristate "Kinetis K20 EzPort firmware update support + depends on SPI && SYSFS && OF + select FW_LOADER + ---help--- + EzPort is the flash interface from NXP Kinetis K20, that can be used to + access the microcontroller's flash memory. This driver supports flashing + a new image using the kernel's firmware API. + config IBM_ASM tristate "Device driver for IBM RSA service processor" depends on X86 && PCI && INPUT diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile index c3c8624f4d95..61eb228d2a7b 100644 --- a/drivers/misc/Makefile +++ b/drivers/misc/Makefile @@ -11,6 +11,7 @@ obj-$(CONFIG_INTEL_MID_PTI) += pti.o obj-$(CONFIG_ATMEL_SSC) += atmel-ssc.o obj-$(CONFIG_ATMEL_TCLIB) += atmel_tclib.o obj-$(CONFIG_DUMMY_IRQ) += dummy-irq.o +obj-$(CONFIG_EZPORT_FW) += ezport-firmware.o obj-$(CONFIG_ICS932S401) += ics932s401.o obj-$(CONFIG_LKDTM) += lkdtm.o obj-$(CONFIG_TIFM_CORE) += tifm_core.o diff --git a/drivers/misc/ezport-firmware.c b/drivers/misc/ezport-firmware.c new file mode 100644 index 000000000000..069981b13e69 --- /dev/null +++ b/drivers/misc/ezport-firmware.c @@ -0,0 +1,487 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * NXP Kinetis EzPort is a flash interface found on NXP microcontrollers + * from the Kinetis K20 series. It can be used to update the firmware. + * The interface is enabled by enabling the chip-select while resetting + * the processor. + * + * Copyright (C) 2018 Collabora + * Copyright (C) 2018 GE Healthcare + */ + +#include <linux/delay.h> +#include <linux/firmware.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/gpio/consumer.h> +#include <linux/spi/spi.h> +#include <linux/of_device.h> + +#define EZPORT_SPI_RESET_DELAY_MS 50 +#define EZPORT_SPI_POST_RESET_DELAY_MS 500 +#define EZPORT_TRANSFER_SIZE 2048 + +#define EZPORT_CMD_SP 0x02 /* flash section program*/ +#define EZPORT_CMD_RDSR 0x05 /* read status register */ +#define EZPORT_CMD_WREN 0x06 /* write enable */ +#define EZPORT_CMD_FAST_READ 0x0b /* flash read data at high speed */ +#define EZPORT_CMD_RESET 0xb9 /* reset chip */ +#define EZPORT_CMD_BE 0xc7 /* bulk erase */ +#define EZPORT_CMD_SE 0xd8 /* sector erase */ + +#define EZPORT_DUMMY 0x00 + +#define EZPORT_FAST_READ_SIZE 5 +#define EZPORT_SP_SIZE 4 + +#define EZPORT_SECTOR_SIZE 4096 +#define EZPORT_SECTOR_MASK (EZPORT_SECTOR_SIZE - 1) +#define EZPORT_WRITE_WAIT_MS 50 + +#define EZPORT_STATUS_WIP BIT(0) /* write in progress */ +#define EZPORT_STATUS_WEN BIT(1) /* write enable */ +#define EZPORT_STATUS_WEF BIT(6) /* write error flag */ +#define EZPORT_STATUS_FS BIT(7) /* flash security */ + +struct ezport_device_info { + const char *fwname; + u32 flash_size; +}; + +/* https://www.nxp.com/part/MK20FN1M0VMD12 */ +static const struct ezport_device_info data_ge_achc = { + .fwname = "gehc-achc.fw", + .flash_size = 1024*1024, +}; + +static int ezport_acquire(struct spi_device *spi, + struct gpio_desc *reset) +{ + struct spi_message msg; + struct spi_transfer assert_cs = { + .cs_change = 1, + }; + struct spi_transfer release_cs = { }; + int ret; + + spi_bus_lock(spi->master); + + /* assert chip select */ + spi_message_init(&msg); + spi_message_add_tail(&assert_cs, &msg); + ret = spi_sync_locked(spi, &msg); + if (ret) + goto fail; + + /* reset to return into normal mode */ + gpiod_set_value(reset, 1); + msleep(EZPORT_SPI_RESET_DELAY_MS); + gpiod_set_value(reset, 0); + + msleep(EZPORT_SPI_POST_RESET_DELAY_MS); + + /* release chip select */ + spi_message_init(&msg); + spi_message_add_tail(&release_cs, &msg); + ret = spi_sync_locked(spi, &msg); + +fail: + spi_bus_unlock(spi->master); + return ret; +} + +static void ezport_release(struct gpio_desc *reset) +{ + /* reset without chip select to return into normal mode */ + gpiod_set_value(reset, 1); + msleep(EZPORT_SPI_RESET_DELAY_MS); + gpiod_set_value(reset, 0); + + msleep(EZPORT_SPI_POST_RESET_DELAY_MS); +} + +static inline int ezport_get_status_register(struct spi_device *spi) +{ + return spi_w8r8(spi, EZPORT_CMD_RDSR); +} + +static bool ezport_ready(int statusreg, bool write) +{ + if (statusreg < 0) + return false; + if (statusreg & EZPORT_STATUS_WIP) + return false; + if (statusreg & EZPORT_STATUS_WEF) + return false; + if (statusreg & EZPORT_STATUS_FS) + return false; + if (write && !(statusreg & EZPORT_STATUS_WEN)) + return false; + + return true; +} + +static int ezport_wait_write(struct spi_device *spi) +{ + int ret; + u32 i; + + /* + * poll status for write in progress bit. It usually needs 100ms + * to get cleared, but we wait 250ms to be sure. + */ + for (i = 0; i < 5; i++) { + ret = ezport_get_status_register(spi); + if (ret < 0) + return ret; + if (!(ret & EZPORT_STATUS_WIP)) + break; + msleep(EZPORT_WRITE_WAIT_MS); + } + + return ret; +} + +static int ezport_write_enable(struct spi_device *spi) +{ + static const u8 cmd = EZPORT_CMD_WREN; + int ret; + + ret = spi_write(spi, &cmd, 1); + if (ret < 0) + return ret; + + return ezport_get_status_register(spi); +} + +static int ezport_bulk_erase(struct spi_device *spi) +{ + int ret; + static const u8 cmd = EZPORT_CMD_BE; + + ret = ezport_write_enable(spi); + if (ret < 0) + return ret; + + if (!(ret & EZPORT_STATUS_WEN)) + return -EIO; + + ret = spi_write(spi, &cmd, 1); + if (ret < 0) + return ret; + + ret = ezport_wait_write(spi); + if (ret < 0) + return ret; + + return 0; +} + +static int ezport_section_erase(struct spi_device *spi, u32 address) +{ + u8 query[] = {EZPORT_CMD_SE, address >> 16, address >> 8, address}; + int ret; + + if (address & EZPORT_SECTOR_MASK) + return -EINVAL; + + ret = ezport_write_enable(spi); + if (ret < 0) + return ret; + + ret = spi_write(spi, query, ARRAY_SIZE(query)); + if (ret < 0) + return ret; + + return ezport_wait_write(spi); +} + +static int ezport_flash_transfer(struct spi_device *spi, u32 address, + const u8 *payload, size_t size) +{ + struct spi_transfer xfers[2] = {}; + u8 *query; + int ret; + + query = kmalloc(EZPORT_SP_SIZE, GFP_KERNEL); + if (!query) + return -ENOMEM; + + query[0] = EZPORT_CMD_SP; + query[1] = address >> 16; + query[2] = address >> 8; + query[3] = address >> 0; + + xfers[0].len = EZPORT_SP_SIZE; + xfers[0].tx_buf = query; + + xfers[1].len = size; + xfers[1].tx_buf = payload; + + ret = spi_sync_transfer(spi, xfers, 2); + + kfree(query); + + if (ret < 0) + return ret; + + return ezport_wait_write(spi); +} + +static int ezport_read_data(struct spi_device *spi, u32 address, + u8 *buffer, size_t size) +{ + struct spi_transfer xfers[2] = {}; + u8 *query = kmalloc(EZPORT_FAST_READ_SIZE, GFP_KERNEL); + int ret; + + if (!query) + return -ENOMEM; + + query[0] = EZPORT_CMD_FAST_READ; + query[1] = address >> 16; + query[2] = address >> 8; + query[3] = address >> 0; + query[4] = EZPORT_DUMMY; + + xfers[0].len = EZPORT_FAST_READ_SIZE; + xfers[0].tx_buf = query; + + xfers[1].len = size; + xfers[1].rx_buf = buffer; + + ret = spi_sync_transfer(spi, xfers, 2); + + kfree(query); + + return ret; +} + +static int ezport_firmware_flash_data(struct spi_device *spi, + const u8 *data, size_t size) +{ + int ret; + u32 address = 0; + u32 transfer_size; + + ret = ezport_get_status_register(spi); + if (ret < 0) + return ret; + + if (ret & EZPORT_STATUS_FS) { + dev_dbg(&spi->dev, "bulk erase"); + ret = ezport_bulk_erase(spi); + if (!ezport_ready(ret, false)) + return ret; + } + + ret = ezport_get_status_register(spi); + if (!ezport_ready(ret, false)) { + dev_err(&spi->dev, "flash memory is not ready!"); + return ret; + } + + while (address < size) { + if (!(address & EZPORT_SECTOR_MASK)) { + dev_dbg(&spi->dev, "section erase: %x", address); + ret = ezport_section_erase(spi, address); + if (!ezport_ready(ret, false)) + return ret; + } + + ret = ezport_write_enable(spi); + if (!ezport_ready(ret, true)) + return ret; + + transfer_size = min((u32) EZPORT_TRANSFER_SIZE, size - address); + + dev_dbg(&spi->dev, "transfer: %x", address); + ret = ezport_flash_transfer(spi, address, + data+address, transfer_size); + if (!ezport_ready(ret, false)) + return ret; + + address += transfer_size; + } + + return 0; +} + +static int ezport_firmware_verify_data(struct spi_device *spi, + const u8 *data, size_t size) +{ + u32 transfer_size; + u32 address = 0; + u8 *buffer; + int ret; + + ret = ezport_get_status_register(spi); + if (!ezport_ready(ret, false)) { + dev_err(&spi->dev, "flash not ready: %d\n", ret); + return -EBUSY; + } + + buffer = kmalloc(EZPORT_TRANSFER_SIZE, GFP_KERNEL); + if (!buffer) + return -ENOMEM; + + while (address < size) { + transfer_size = min((u32) EZPORT_TRANSFER_SIZE, size - address); + + ret = ezport_read_data(spi, address, buffer, transfer_size); + if (ret < 0) + goto exit_free_buffer; + + ret = memcmp(data + address, buffer, transfer_size); + if (ret) { + ret = -EBADMSG; + goto exit_free_buffer; + } + + address += transfer_size; + } + +exit_free_buffer: + kfree(buffer); + return ret; +} + +static int ezport_flash(struct spi_device *spi, bool verify) +{ + struct ezport_device_info *data = spi_get_drvdata(spi); + const struct firmware *fw; + struct gpio_desc *reset; + int ret; + + ret = request_firmware(&fw, data->fwname, &spi->dev); + if (ret) { + dev_err(&spi->dev, "Could not get firmware: %d\n", ret); + return ret; + } + + if (fw->size > data->flash_size) { + dev_err(&spi->dev, "FW image does not fit into flash memory!\n"); + ret = -EFBIG; + goto exit_release_fw; + } + + reset = gpiod_get(&spi->dev, "reset", GPIOD_OUT_LOW); + if (IS_ERR(reset)) { + ret = PTR_ERR(reset); + dev_err(&spi->dev, "Could not get reset gpio: %d\n", ret); + goto exit_release_fw; + } + + ret = ezport_acquire(spi, reset); + if (ret) + goto exit_release_gpio; + + if (verify) + ret = ezport_firmware_verify_data(spi, fw->data, fw->size); + else + ret = ezport_firmware_flash_data(spi, fw->data, fw->size); + + if (ret > 0) { + dev_err(&spi->dev, "Failure, Status Register: 0x%02x\n", ret); + ret = -EIO; + } + + ezport_release(reset); + +exit_release_gpio: + gpiod_put(reset); +exit_release_fw: + release_firmware(fw); + return ret; +} + +static ssize_t verify_fw_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + static const char *success = "Firmware matches!\n"; + static const char *failure = "Firmware does not match!\n"; + struct spi_device *spi = to_spi_device(dev); + int ret = ezport_flash(spi, true); + + if (ret == 0) { + strcpy(buf, success); + return strlen(success); + } else if (ret == -EBADMSG) { + strcpy(buf, failure); + return strlen(failure); + } else { + return ret; + } +} + +static ssize_t update_fw_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct spi_device *spi = to_spi_device(dev); + int ret; + + dev_dbg(dev, "Firmware update started!"); + ret = ezport_flash(spi, false); + if (!ret) + dev_dbg(dev, "Firmware update finished!"); + + return ret ? ret : count; +} + +static DEVICE_ATTR_WO(update_fw); +static DEVICE_ATTR_RO(verify_fw); + +static struct attribute *ezport_attrs[] = { + &dev_attr_update_fw.attr, + &dev_attr_verify_fw.attr, + NULL +}; + +static const struct attribute_group ezport_attr_group = { + .attrs = ezport_attrs, +}; + +static int ezport_probe(struct spi_device *spi) +{ + const struct ezport_device_info *data; + struct ezport_device_info *copy; + int ret; + + data = of_device_get_match_data(&spi->dev); + if (!data) + return -ENODEV; + + copy = devm_kmemdup(&spi->dev, data, sizeof(*data), GFP_KERNEL); + if (!copy) + return -ENOMEM; + + spi_set_drvdata(spi, copy); + + ret = devm_device_add_group(&spi->dev, &ezport_attr_group); + if (ret) { + dev_err(&spi->dev, "create sysfs failed: %d\n", ret); + return ret; + } + + return 0; +} + +static const struct of_device_id ezport_of_match[] = { + { .compatible = "ge,achc-ezport", .data = &data_ge_achc, }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, ezport_of_match); + +static struct spi_driver ezport_fw_spi_driver = { + .driver = { + .name = "ezport-fw", + .of_match_table = ezport_of_match, + }, + .probe = ezport_probe, +}; +module_spi_driver(ezport_fw_spi_driver); + +MODULE_DESCRIPTION("Kinetis EzPort firmware driver"); +MODULE_AUTHOR("Sebastian Reichel <sebastian.reichel@xxxxxxxxxxxxxxx>"); +MODULE_LICENSE("GPL"); -- 2.16.2 -- To unsubscribe from this list: send the line "unsubscribe devicetree" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html