flash/nor: add mrvlqspi flash controller driver 80/2280/4
authorMahavir Jain <mjain@marvell.com>
Thu, 4 Sep 2014 10:01:16 +0000 (15:31 +0530)
committerSpencer Oliver <spen@spen-soft.co.uk>
Mon, 22 Sep 2014 19:37:09 +0000 (19:37 +0000)
This patch adds support for QSPI flash controller driver for
Marvell's Wireless Microcontroller platform.
For more information please refer,
https://origin-www.marvell.com/microcontrollers/wi-fi-microcontroller-platform/

Following things have been tested on 88MC200 (Winbond W25Q80BV flash chip):
1. Flash sector level erase
2. Flash chip erase
3. Flash write in normal SPI mode
4. Flash fill (write and verify) in normal SPI mode

Change-Id: If4414ae3f77ff170b84e426a35b66c44590c5e06
Signed-off-by: Mahavir Jain <mjain@marvell.com>
Reviewed-on: http://openocd.zylin.com/2280
Tested-by: jenkins
Reviewed-by: Spencer Oliver <spen@spen-soft.co.uk>
contrib/loaders/flash/mrvlqspi_write.S [new file with mode: 0644]
src/flash/nor/Makefile.am
src/flash/nor/drivers.c
src/flash/nor/mrvlqspi.c [new file with mode: 0644]

diff --git a/contrib/loaders/flash/mrvlqspi_write.S b/contrib/loaders/flash/mrvlqspi_write.S
new file mode 100644 (file)
index 0000000..064192c
--- /dev/null
@@ -0,0 +1,232 @@
+/***************************************************************************
+ *   Copyright (C) 2014 by Mahavir Jain <mjain@marvell.com>                *
+ *                                                                         *
+ *   Adapted from (contrib/loaders/flash/lpcspifi_write.S):                *
+ *   Copyright (C) 2012 by George Harris                                   *
+ *   george@luminairecoffee.com                                            *
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) any later version.                                   *
+ *                                                                         *
+ *   This program is distributed in the hope that it will be useful,       *
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
+ *   GNU General Public License for more details.                          *
+ *                                                                         *
+ *   You should have received a copy of the GNU General Public License     *
+ *   along with this program; if not, write to the                         *
+ *   Free Software Foundation, Inc.,                                       *
+ *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.           *
+ ***************************************************************************/
+
+       .text
+       .syntax unified
+       .cpu cortex-m3
+       .thumb
+       .thumb_func
+
+/*
+ * For compilation:
+ * arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -c contrib/loaders/flash/mrvlqspi_write.S
+ * arm-none-eabi-objcopy -O binary mrvlqspi_write.o code.bin
+ * Copy code.bin into mrvlqspi flash driver
+ */
+
+/*
+ * Params :
+ * r0 = workarea start, status (out)
+ * r1 = workarea end
+ * r2 = target address (offset from flash base)
+ * r3 = count (bytes)
+ * r4 = page size
+ * r5 = qspi base address
+ * Clobbered:
+ * r7 - rp
+ * r8 - wp, tmp
+ * r9 - send/receive data
+ * r10 - current page end address
+ */
+
+#define CNTL   0x0
+#define CONF   0x4
+#define DOUT   0x8
+#define DIN    0xc
+#define INSTR   0x10
+#define ADDR    0x14
+#define RDMODE  0x18
+#define HDRCNT 0x1c
+#define DINCNT  0x20
+
+#define SS_EN (1 << 0)
+#define XFER_RDY (1 << 1)
+#define RFIFO_EMPTY (1 << 4)
+#define WFIFO_EMPTY (1 << 6)
+#define WFIFO_FULL (1 << 7)
+#define FIFO_FLUSH (1 << 9)
+#define RW_EN (1 << 13)
+#define XFER_STOP (1 << 14)
+#define XFER_START (1 << 15)
+
+#define INS_WRITE_ENABLE 0x06
+#define INS_READ_STATUS 0x05
+#define INS_PAGE_PROGRAM 0x02
+
+init:
+       mov.w   r10, #0x00
+find_next_page_boundary:
+       add     r10, r4         /* Increment to the next page */
+       cmp     r10, r2
+       /* If we have not reached the next page boundary after the target address, keep going */
+       bls     find_next_page_boundary
+write_enable:
+       /* Flush read/write fifo's */
+       bl      flush_fifo
+
+       /* Instruction byte 1 */
+       movs    r8, #0x1
+       str     r8, [r5, #HDRCNT]
+
+       /* Set write enable instruction */
+       movs    r8, #INS_WRITE_ENABLE
+       str     r8, [r5, #INSTR]
+
+       movs    r9, #0x1
+       bl      start_tx
+       bl      stop_tx
+page_program:
+       /* Instruction byte 1, Addr byte 3 */
+       movs    r8, #0x31
+       str     r8, [r5, #HDRCNT]
+       /* Todo: set addr and data pin to single */
+write_address:
+       mov     r8, r2
+       str     r8, [r5, #ADDR]
+       /* Set page program instruction */
+       movs    r8, #INS_PAGE_PROGRAM
+       str     r8, [r5, #INSTR]
+       /* Start write transfer */
+       movs    r9, #0x1
+       bl      start_tx
+wait_fifo:
+       ldr     r8, [r0]        /* read the write pointer */
+       cmp     r8, #0          /* if it's zero, we're gonzo */
+       beq     exit
+       ldr     r7, [r0, #4]    /* read the read pointer */
+       cmp     r7, r8          /* wait until they are not equal */
+       beq     wait_fifo
+write:
+       ldrb    r9, [r7], #0x01 /* Load one byte from the FIFO, increment the read pointer by 1 */
+       bl      write_data      /* send the byte to the flash chip */
+
+       cmp     r7, r1          /* wrap the read pointer if it is at the end */
+       it      cs
+       addcs   r7, r0, #8      /* skip loader args */
+       str     r7, [r0, #4]    /* store the new read pointer */
+       subs    r3, r3, #1      /* decrement count */
+       cmp     r3, #0          /* Exit if we have written everything */
+       beq     write_wait
+       add     r2, #1          /* Increment flash address by 1 */
+       cmp     r10, r2         /* See if we have reached the end of a page */
+       bne     wait_fifo       /* If not, keep writing bytes */
+write_wait:
+       bl      stop_tx         /* Otherwise, end the command and keep going w/ the next page */
+       add     r10, r4         /* Move up the end-of-page address by the page size*/
+check_flash_busy:              /* Wait for the flash to finish the previous page write */
+       /* Flush read/write fifo's */
+       bl      flush_fifo
+       /* Instruction byte 1 */
+       movs    r8, #0x1
+       str     r8, [r5, #HDRCNT]
+       /* Continuous data in of status register */
+       movs    r8, #0x0
+       str     r8, [r5, #DINCNT]
+       /* Set write enable instruction */
+       movs    r8, #INS_READ_STATUS
+       str     r8, [r5, #INSTR]
+       /* Start read transfer */
+       movs    r9, #0x0
+       bl      start_tx
+wait_flash_busy:
+       bl      read_data
+       and.w   r9, r9, #0x1
+       cmp     r9, #0x0
+       bne.n   wait_flash_busy
+       bl      stop_tx
+       cmp     r3, #0
+       bne.n   write_enable    /* If it is done, start a new page write */
+       b       exit            /* All data written, exit */
+
+write_data:                    /* Send/receive 1 byte of data over QSPI */
+       ldr     r8, [r5, #CNTL]
+       lsls    r8, r8, #24
+       bmi.n   write_data
+       str     r9, [r5, #DOUT]
+       bx      lr
+
+read_data:                     /* Read 1 byte of data over QSPI */
+       ldr     r8, [r5, #CNTL]
+       lsls    r8, r8, #27
+       bmi.n   read_data
+       ldr     r9, [r5, #DIN]
+       bx      lr
+
+flush_fifo:                    /* Flush read write fifos */
+       ldr     r8, [r5, #CONF]
+       orr.w   r8, r8, #FIFO_FLUSH
+       str     r8, [r5, #CONF]
+flush_reset:
+       ldr     r8, [r5, #CONF]
+       lsls    r8, r8, #22
+       bmi.n   flush_reset
+       bx      lr
+
+start_tx:
+       ldr     r8, [r5, #CNTL]
+       orr.w   r8, r8, #SS_EN
+       str     r8, [r5, #CNTL]
+xfer_rdy:
+       ldr     r8, [r5, #CNTL]
+       lsls    r8, r8, #30
+       bpl.n   xfer_rdy
+       ldr     r8, [r5, #CONF]
+       bfi     r8, r9, #13, #1
+       orr.w   r8, r8, #XFER_START
+       str     r8, [r5, #CONF]
+       bx lr
+
+stop_tx:
+       ldr     r8, [r5, #CNTL]
+       lsls    r8, r8, #30
+       bpl.n   stop_tx
+wfifo_wait:
+       ldr     r8, [r5, #CNTL]
+       lsls    r8, r8, #25
+       bpl.n   wfifo_wait
+       ldr     r8, [r5, #CONF]
+       orr.w   r8, r8, #XFER_STOP
+       str     r8, [r5, #CONF]
+xfer_start:
+       ldr     r8, [r5, #CONF]
+       lsls    r8, r8, #16
+       bmi.n   xfer_start
+ss_disable:
+       # Disable SS_EN
+       ldr     r8, [r5, #CNTL]
+       bic.w   r8, r8, #SS_EN
+       str     r8, [r5, #CNTL]
+wait:
+       ldr     r8, [r5, #CNTL]
+       lsls    r8, r8, #30
+       bpl.n   wait
+       bx      lr
+
+error:
+       movs    r0, #0
+       str     r0, [r2, #4]    /* set rp = 0 on error */
+exit:
+       mov     r0, r6
+       bkpt    #0x00
+
+       .end
index 316814738d5e5e9518e63483a37b56bb79844fb9..bae42fd5dfcaa6f074e415a252bcb72027059375 100644 (file)
@@ -43,7 +43,8 @@ NOR_DRIVERS = \
        kinetis.c \
        mini51.c \
        nuc1x.c \
-       nrf51.c
+       nrf51.c \
+       mrvlqspi.c
 
 noinst_HEADERS = \
        core.h \
index ed631a3b0ba812e43ea6d61f7370c0653fb87e2b..8959f0cad7d11a957c08f19daa41cd003971f10e 100644 (file)
@@ -56,6 +56,7 @@ extern struct flash_driver mdr_flash;
 extern struct flash_driver mini51_flash;
 extern struct flash_driver nuc1x_flash;
 extern struct flash_driver nrf51_flash;
+extern struct flash_driver mrvlqspi_flash;
 
 /**
  * The list of built-in flash drivers.
@@ -96,6 +97,7 @@ static struct flash_driver *flash_drivers[] = {
        &mini51_flash,
        &nuc1x_flash,
        &nrf51_flash,
+       &mrvlqspi_flash,
        NULL,
 };
 
diff --git a/src/flash/nor/mrvlqspi.c b/src/flash/nor/mrvlqspi.c
new file mode 100644 (file)
index 0000000..a5cc1ca
--- /dev/null
@@ -0,0 +1,960 @@
+/***************************************************************************
+ *   Copyright (C) 2014 by Mahavir Jain <mjain@marvell.com>                *
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) any later version.                                   *
+ *                                                                         *
+ *   This program is distributed in the hope that it will be useful,       *
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
+ *   GNU General Public License for more details.                          *
+ *                                                                         *
+ *   You should have received a copy of the GNU General Public License     *
+ *   along with this program; if not, write to the                         *
+ *   Free Software Foundation, Inc.,                                       *
+ *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.           *
+ *                                                                         *
+ ***************************************************************************/
+
+ /*
+  * This is QSPI flash controller driver for Marvell's Wireless
+  * Microcontroller platform.
+  *
+  * For more information please refer,
+  * https://origin-www.marvell.com/microcontrollers/wi-fi-microcontroller-platform/
+  */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "imp.h"
+#include "spi.h"
+#include <helper/binarybuffer.h>
+#include <target/algorithm.h>
+#include <target/armv7m.h>
+
+#define QSPI_R_EN (0x0)
+#define QSPI_W_EN (0x1)
+#define QSPI_SS_DISABLE (0x0)
+#define QSPI_SS_ENABLE (0x1)
+#define WRITE_DISBALE (0x0)
+#define WRITE_ENABLE (0x1)
+
+#define QSPI_TIMEOUT (1000)
+#define FIFO_FLUSH_TIMEOUT (1000)
+#define BLOCK_ERASE_TIMEOUT (1000)
+#define CHIP_ERASE_TIMEOUT (10000)
+
+#define SS_EN (1 << 0)
+#define XFER_RDY (1 << 1)
+#define RFIFO_EMPTY (1 << 4)
+#define WFIFO_EMPTY (1 << 6)
+#define WFIFO_FULL (1 << 7)
+#define FIFO_FLUSH (1 << 9)
+#define RW_EN (1 << 13)
+#define XFER_STOP (1 << 14)
+#define XFER_START (1 << 15)
+#define CONF_MASK (0x7)
+#define CONF_OFFSET (10)
+
+#define INS_WRITE_ENABLE 0x06
+#define INS_WRITE_DISABLE 0x04
+#define INS_READ_STATUS 0x05
+#define INS_PAGE_PROGRAM 0x02
+
+#define CNTL 0x0 /* QSPI_BASE + 0x0 */
+#define CONF 0x4
+#define DOUT 0x8
+#define DIN 0xc
+#define INSTR 0x10
+#define ADDR 0x14
+#define RDMODE 0x18
+#define HDRCNT 0x1c
+#define DINCNT 0x20
+
+struct mrvlqspi_flash_bank {
+       int probed;
+       uint32_t reg_base;
+       uint32_t bank_num;
+       const struct flash_device *dev;
+};
+
+static inline uint32_t mrvlqspi_get_reg(struct flash_bank *bank, uint32_t reg)
+{
+       struct mrvlqspi_flash_bank *mrvlqspi_info = bank->driver_priv;
+       return reg + mrvlqspi_info->reg_base;
+}
+
+static inline int mrvlqspi_set_din_cnt(struct flash_bank *bank, uint32_t count)
+{
+       struct target *target = bank->target;
+
+       return target_write_u32(target, mrvlqspi_get_reg(bank, DINCNT), count);
+}
+
+static inline int mrvlqspi_set_addr(struct flash_bank *bank, uint32_t addr)
+{
+       struct target *target = bank->target;
+
+       return target_write_u32(target, mrvlqspi_get_reg(bank, ADDR), addr);
+}
+
+static inline int mrvlqspi_set_instr(struct flash_bank *bank, uint32_t instr)
+{
+       struct target *target = bank->target;
+
+       return target_write_u32(target, mrvlqspi_get_reg(bank, INSTR), instr);
+}
+
+static inline int mrvlqspi_set_hdr_cnt(struct flash_bank *bank, uint32_t hdr_cnt)
+{
+       struct target *target = bank->target;
+
+       return target_write_u32(target, mrvlqspi_get_reg(bank, HDRCNT), hdr_cnt);
+}
+
+static int mrvlqspi_set_conf(struct flash_bank *bank, uint32_t conf_val)
+{
+       int retval;
+       uint32_t regval;
+       struct target *target = bank->target;
+
+       retval = target_read_u32(target,
+                       mrvlqspi_get_reg(bank, CONF), &regval);
+       if (retval != ERROR_OK)
+               return retval;
+
+       regval &= ~(CONF_MASK << CONF_OFFSET);
+       regval |= (conf_val << CONF_OFFSET);
+
+       return target_write_u32(target,
+                       mrvlqspi_get_reg(bank, CONF), regval);
+}
+
+static int mrvlqspi_set_ss_state(struct flash_bank *bank, bool state, int timeout)
+{
+       int retval;
+       uint32_t regval;
+       struct target *target = bank->target;
+
+       retval = target_read_u32(target,
+                       mrvlqspi_get_reg(bank, CNTL), &regval);
+       if (retval != ERROR_OK)
+               return retval;
+
+       if (state)
+               regval |= SS_EN;
+       else
+               regval &= ~(SS_EN);
+
+       retval = target_write_u32(target,
+                       mrvlqspi_get_reg(bank, CNTL), regval);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* wait for xfer_ready to set */
+       for (;;) {
+               retval = target_read_u32(target,
+                               mrvlqspi_get_reg(bank, CNTL), &regval);
+               if (retval != ERROR_OK)
+                       return retval;
+               LOG_DEBUG("status: 0x%x", regval);
+               if ((regval & XFER_RDY) == XFER_RDY)
+                       break;
+               if (timeout-- <= 0) {
+                       LOG_ERROR("timed out waiting for flash");
+                       return ERROR_FAIL;
+               }
+               alive_sleep(1);
+       }
+       return ERROR_OK;
+}
+
+static int mrvlqspi_start_transfer(struct flash_bank *bank, bool rw_mode)
+{
+       int retval;
+       uint32_t regval;
+       struct target *target = bank->target;
+
+       retval = mrvlqspi_set_ss_state(bank, QSPI_SS_ENABLE, QSPI_TIMEOUT);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = target_read_u32(target,
+                       mrvlqspi_get_reg(bank, CONF), &regval);
+       if (retval != ERROR_OK)
+               return retval;
+
+       if (rw_mode)
+               regval |= RW_EN;
+       else
+               regval &= ~(RW_EN);
+
+       regval |= XFER_START;
+
+       retval = target_write_u32(target,
+                       mrvlqspi_get_reg(bank, CONF), regval);
+       if (retval != ERROR_OK)
+               return retval;
+
+       return ERROR_OK;
+}
+
+static int mrvlqspi_stop_transfer(struct flash_bank *bank)
+{
+       int retval;
+       uint32_t regval;
+       struct target *target = bank->target;
+       int timeout = QSPI_TIMEOUT;
+
+       /* wait for xfer_ready and wfifo_empty to set */
+       for (;;) {
+               retval = target_read_u32(target,
+                               mrvlqspi_get_reg(bank, CNTL), &regval);
+               if (retval != ERROR_OK)
+                       return retval;
+               LOG_DEBUG("status: 0x%x", regval);
+               if ((regval & (XFER_RDY | WFIFO_EMPTY)) ==
+                                       (XFER_RDY | WFIFO_EMPTY))
+                       break;
+               if (timeout-- <= 0) {
+                       LOG_ERROR("timed out waiting for flash");
+                       return ERROR_FAIL;
+               }
+               alive_sleep(1);
+       }
+
+       retval = target_read_u32(target,
+                       mrvlqspi_get_reg(bank, CONF), &regval);
+       if (retval != ERROR_OK)
+               return retval;
+
+       regval |= XFER_STOP;
+
+       retval = target_write_u32(target,
+                       mrvlqspi_get_reg(bank, CONF), regval);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* wait for xfer_start to reset */
+       for (;;) {
+               retval = target_read_u32(target,
+                               mrvlqspi_get_reg(bank, CONF), &regval);
+               if (retval != ERROR_OK)
+                       return retval;
+               LOG_DEBUG("status: 0x%x", regval);
+               if ((regval & XFER_START) == 0)
+                       break;
+               if (timeout-- <= 0) {
+                       LOG_ERROR("timed out waiting for flash");
+                       return ERROR_FAIL;
+               }
+               alive_sleep(1);
+       }
+
+       retval = mrvlqspi_set_ss_state(bank, QSPI_SS_DISABLE, QSPI_TIMEOUT);
+       if (retval != ERROR_OK)
+               return retval;
+
+       return ERROR_OK;
+}
+
+static int mrvlqspi_fifo_flush(struct flash_bank *bank, int timeout)
+{
+       int retval;
+       uint32_t val;
+       struct target *target = bank->target;
+
+       retval = target_read_u32(target,
+                       mrvlqspi_get_reg(bank, CONF), &val);
+       if (retval != ERROR_OK)
+               return retval;
+
+       val |= FIFO_FLUSH;
+
+       retval = target_write_u32(target,
+                       mrvlqspi_get_reg(bank, CONF), val);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* wait for fifo_flush to clear */
+       for (;;) {
+               retval = target_read_u32(target,
+                               mrvlqspi_get_reg(bank, CONF), &val);
+               if (retval != ERROR_OK)
+                       return retval;
+               LOG_DEBUG("status: 0x%x", val);
+               if ((val & FIFO_FLUSH) == 0)
+                       break;
+               if (timeout-- <= 0) {
+                       LOG_ERROR("timed out waiting for flash");
+                       return ERROR_FAIL;
+               }
+               alive_sleep(1);
+       }
+       return ERROR_OK;
+}
+
+static int mrvlqspi_read_byte(struct flash_bank *bank, uint8_t *data)
+{
+       int retval;
+       uint32_t val;
+       struct target *target = bank->target;
+
+       /* wait for rfifo_empty to reset */
+       for (;;) {
+               retval = target_read_u32(target,
+                               mrvlqspi_get_reg(bank, CNTL), &val);
+               if (retval != ERROR_OK)
+                       return retval;
+               LOG_DEBUG("status: 0x%x", val);
+               if ((val & RFIFO_EMPTY) == 0)
+                       break;
+               usleep(10);
+       }
+
+       retval = target_read_u32(target,
+                       mrvlqspi_get_reg(bank, DIN), &val);
+       if (retval != ERROR_OK)
+               return retval;
+
+       *data = val & 0xFF;
+
+       return ERROR_OK;
+}
+
+static int mrvlqspi_flash_busy_status(struct flash_bank *bank, int timeout)
+{
+       uint8_t val;
+       int retval;
+
+       /* Flush read/write fifo's */
+       retval = mrvlqspi_fifo_flush(bank, FIFO_FLUSH_TIMEOUT);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set instruction/addr count value */
+       retval = mrvlqspi_set_hdr_cnt(bank, 0x1);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Read flash status register in continuous manner */
+       retval = mrvlqspi_set_din_cnt(bank, 0x0);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set instruction */
+       retval = mrvlqspi_set_instr(bank, INS_READ_STATUS);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set data and addr pin length */
+       retval = mrvlqspi_set_conf(bank, 0x0);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Enable read mode transfer */
+       retval = mrvlqspi_start_transfer(bank, QSPI_R_EN);
+       if (retval != ERROR_OK)
+               return retval;
+
+       for (;;) {
+               retval = mrvlqspi_read_byte(bank, &val);
+               if (retval != ERROR_OK)
+                       return retval;
+               if (!(val & 0x1))
+                       break;
+               if (timeout-- <= 0) {
+                       LOG_ERROR("timed out waiting for flash");
+                       return ERROR_FAIL;
+               }
+               alive_sleep(1);
+       }
+
+       return mrvlqspi_stop_transfer(bank);
+}
+
+static int mrvlqspi_set_write_status(struct flash_bank *bank, bool mode)
+{
+       int retval;
+       uint32_t instr;
+
+       /* Flush read/write fifo's */
+       retval = mrvlqspi_fifo_flush(bank, FIFO_FLUSH_TIMEOUT);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set instruction/addr count value */
+       retval = mrvlqspi_set_hdr_cnt(bank, 0x1);
+       if (retval != ERROR_OK)
+               return retval;
+
+       if (mode)
+               instr = INS_WRITE_ENABLE;
+       else
+               instr = INS_WRITE_DISABLE;
+
+       /* Set instruction */
+       retval = mrvlqspi_set_instr(bank, instr);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = mrvlqspi_start_transfer(bank, QSPI_W_EN);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = mrvlqspi_stop_transfer(bank);
+       if (retval != ERROR_OK)
+               return retval;
+
+       return retval;
+}
+
+static int mrvlqspi_read_id(struct flash_bank *bank, uint32_t *id)
+{
+       uint8_t id_buf[3] = {0, 0, 0};
+       int retval, i;
+
+       LOG_DEBUG("Getting ID");
+
+       /* Flush read/write fifo's */
+       retval = mrvlqspi_fifo_flush(bank, FIFO_FLUSH_TIMEOUT);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set instruction/addr count value */
+       retval = mrvlqspi_set_hdr_cnt(bank, 0x1);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set count for number of bytes to read */
+       retval = mrvlqspi_set_din_cnt(bank, 0x3);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set instruction */
+       retval = mrvlqspi_set_instr(bank, SPIFLASH_READ_ID);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set data and addr pin length */
+       retval = mrvlqspi_set_conf(bank, 0x0);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = mrvlqspi_start_transfer(bank, QSPI_R_EN);
+       if (retval != ERROR_OK)
+               return retval;
+
+       for (i = 0; i < 3; i++) {
+               retval = mrvlqspi_read_byte(bank, &id_buf[i]);
+               if (retval != ERROR_OK)
+                       return retval;
+       }
+
+       LOG_DEBUG("ID is 0x%x 0x%x 0x%x", id_buf[0], id_buf[1], id_buf[2]);
+       retval = mrvlqspi_set_ss_state(bank, QSPI_SS_DISABLE, QSPI_TIMEOUT);
+       if (retval != ERROR_OK)
+               return retval;
+
+       *id = id_buf[2] << 16 | id_buf[1] << 8 | id_buf[0];
+       return ERROR_OK;
+}
+
+static int mrvlqspi_block_erase(struct flash_bank *bank, uint32_t offset)
+{
+       int retval;
+       struct mrvlqspi_flash_bank *mrvlqspi_info = bank->driver_priv;
+
+       /* Set flash write enable */
+       retval = mrvlqspi_set_write_status(bank, WRITE_ENABLE);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set instruction/addr count value */
+       retval = mrvlqspi_set_hdr_cnt(bank, (0x1 | (0x3 << 4)));
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set read offset address */
+       retval = mrvlqspi_set_addr(bank, offset);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set instruction */
+       retval = mrvlqspi_set_instr(bank, mrvlqspi_info->dev->erase_cmd);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = mrvlqspi_start_transfer(bank, QSPI_W_EN);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = mrvlqspi_stop_transfer(bank);
+       if (retval != ERROR_OK)
+               return retval;
+
+       return mrvlqspi_flash_busy_status(bank, BLOCK_ERASE_TIMEOUT);
+}
+
+static int mrvlqspi_bulk_erase(struct flash_bank *bank)
+{
+       int retval;
+       struct mrvlqspi_flash_bank *mrvlqspi_info = bank->driver_priv;
+
+       /* Set flash write enable */
+       retval = mrvlqspi_set_write_status(bank, WRITE_ENABLE);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set instruction */
+       retval = mrvlqspi_set_instr(bank, mrvlqspi_info->dev->chip_erase_cmd);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = mrvlqspi_start_transfer(bank, QSPI_W_EN);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = mrvlqspi_stop_transfer(bank);
+       if (retval != ERROR_OK)
+               return retval;
+
+       return mrvlqspi_flash_busy_status(bank, CHIP_ERASE_TIMEOUT);
+}
+
+static int mrvlqspi_flash_erase(struct flash_bank *bank, int first, int last)
+{
+       struct target *target = bank->target;
+       struct mrvlqspi_flash_bank *mrvlqspi_info = bank->driver_priv;
+       int retval = ERROR_OK;
+       int sector;
+
+       LOG_DEBUG("erase from sector %d to sector %d", first, last);
+
+       if (target->state != TARGET_HALTED) {
+               LOG_ERROR("Target not halted");
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       if ((first < 0) || (last < first) || (last >= bank->num_sectors)) {
+               LOG_ERROR("Flash sector invalid");
+               return ERROR_FLASH_SECTOR_INVALID;
+       }
+
+       if (!(mrvlqspi_info->probed)) {
+               LOG_ERROR("Flash bank not probed");
+               return ERROR_FLASH_BANK_NOT_PROBED;
+       }
+
+       for (sector = first; sector <= last; sector++) {
+               if (bank->sectors[sector].is_protected) {
+                       LOG_ERROR("Flash sector %d protected", sector);
+                       return ERROR_FAIL;
+               }
+       }
+
+       /* If we're erasing the entire chip and the flash supports
+        * it, use a bulk erase instead of going sector-by-sector. */
+       if (first == 0 && last == (bank->num_sectors - 1)
+               && mrvlqspi_info->dev->chip_erase_cmd !=
+                                       mrvlqspi_info->dev->erase_cmd) {
+               LOG_DEBUG("Chip supports the bulk erase command."\
+               " Will use bulk erase instead of sector-by-sector erase.");
+               retval = mrvlqspi_bulk_erase(bank);
+               if (retval == ERROR_OK) {
+                       return retval;
+               } else
+                       LOG_WARNING("Bulk flash erase failed."
+                               " Falling back to sector-by-sector erase.");
+       }
+
+       for (sector = first; sector <= last; sector++) {
+               retval = mrvlqspi_block_erase(bank,
+                               sector * mrvlqspi_info->dev->sectorsize);
+               if (retval != ERROR_OK)
+                       return retval;
+       }
+
+       return retval;
+}
+
+static int mrvlqspi_flash_write(struct flash_bank *bank, const uint8_t *buffer,
+       uint32_t offset, uint32_t count)
+{
+       struct target *target = bank->target;
+       struct mrvlqspi_flash_bank *mrvlqspi_info = bank->driver_priv;
+       int retval = ERROR_OK;
+       uint32_t page_size, fifo_size;
+       struct working_area *fifo;
+       struct reg_param reg_params[6];
+       struct armv7m_algorithm armv7m_info;
+       struct working_area *write_algorithm;
+       int sector;
+
+       LOG_DEBUG("offset=0x%08" PRIx32 " count=0x%08" PRIx32,
+               offset, count);
+
+       if (target->state != TARGET_HALTED) {
+               LOG_ERROR("Target not halted");
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       if (offset + count > mrvlqspi_info->dev->size_in_bytes) {
+               LOG_WARNING("Writes past end of flash. Extra data discarded.");
+               count = mrvlqspi_info->dev->size_in_bytes - offset;
+       }
+
+       /* Check sector protection */
+       for (sector = 0; sector < bank->num_sectors; sector++) {
+               /* Start offset in or before this sector? */
+               /* End offset in or behind this sector? */
+               if ((offset <
+                       (bank->sectors[sector].offset + bank->sectors[sector].size))
+                       && ((offset + count - 1) >= bank->sectors[sector].offset)
+                       && bank->sectors[sector].is_protected) {
+                       LOG_ERROR("Flash sector %d protected", sector);
+                       return ERROR_FAIL;
+               }
+       }
+
+       page_size = mrvlqspi_info->dev->pagesize;
+
+       /* See contrib/loaders/flash/mrvlqspi.S for src */
+       static const uint8_t mrvlqspi_flash_write_code[] = {
+               0x4f, 0xf0, 0x00, 0x0a, 0xa2, 0x44, 0x92, 0x45,
+               0x7f, 0xf6, 0xfc, 0xaf, 0x00, 0xf0, 0x6b, 0xf8,
+               0x5f, 0xf0, 0x01, 0x08, 0xc5, 0xf8, 0x1c, 0x80,
+               0x5f, 0xf0, 0x06, 0x08, 0xc5, 0xf8, 0x10, 0x80,
+               0x5f, 0xf0, 0x01, 0x09, 0x00, 0xf0, 0x6b, 0xf8,
+               0x00, 0xf0, 0x7d, 0xf8, 0x5f, 0xf0, 0x31, 0x08,
+               0xc5, 0xf8, 0x1c, 0x80, 0x90, 0x46, 0xc5, 0xf8,
+               0x14, 0x80, 0x5f, 0xf0, 0x02, 0x08, 0xc5, 0xf8,
+               0x10, 0x80, 0x5f, 0xf0, 0x01, 0x09, 0x00, 0xf0,
+               0x5a, 0xf8, 0xd0, 0xf8, 0x00, 0x80, 0xb8, 0xf1,
+               0x00, 0x0f, 0x00, 0xf0, 0x8b, 0x80, 0x47, 0x68,
+               0x47, 0x45, 0x3f, 0xf4, 0xf6, 0xaf, 0x17, 0xf8,
+               0x01, 0x9b, 0x00, 0xf0, 0x30, 0xf8, 0x8f, 0x42,
+               0x28, 0xbf, 0x00, 0xf1, 0x08, 0x07, 0x47, 0x60,
+               0x01, 0x3b, 0x00, 0x2b, 0x00, 0xf0, 0x05, 0x80,
+               0x02, 0xf1, 0x01, 0x02, 0x92, 0x45, 0x7f, 0xf4,
+               0xe4, 0xaf, 0x00, 0xf0, 0x50, 0xf8, 0xa2, 0x44,
+               0x00, 0xf0, 0x2d, 0xf8, 0x5f, 0xf0, 0x01, 0x08,
+               0xc5, 0xf8, 0x1c, 0x80, 0x5f, 0xf0, 0x00, 0x08,
+               0xc5, 0xf8, 0x20, 0x80, 0x5f, 0xf0, 0x05, 0x08,
+               0xc5, 0xf8, 0x10, 0x80, 0x5f, 0xf0, 0x00, 0x09,
+               0x00, 0xf0, 0x29, 0xf8, 0x00, 0xf0, 0x13, 0xf8,
+               0x09, 0xf0, 0x01, 0x09, 0xb9, 0xf1, 0x00, 0x0f,
+               0xf8, 0xd1, 0x00, 0xf0, 0x34, 0xf8, 0x00, 0x2b,
+               0xa4, 0xd1, 0x00, 0xf0, 0x53, 0xb8, 0xd5, 0xf8,
+               0x00, 0x80, 0x5f, 0xea, 0x08, 0x68, 0xfa, 0xd4,
+               0xc5, 0xf8, 0x08, 0x90, 0x70, 0x47, 0xd5, 0xf8,
+               0x00, 0x80, 0x5f, 0xea, 0xc8, 0x68, 0xfa, 0xd4,
+               0xd5, 0xf8, 0x0c, 0x90, 0x70, 0x47, 0xd5, 0xf8,
+               0x04, 0x80, 0x48, 0xf4, 0x00, 0x78, 0xc5, 0xf8,
+               0x04, 0x80, 0xd5, 0xf8, 0x04, 0x80, 0x5f, 0xea,
+               0x88, 0x58, 0xfa, 0xd4, 0x70, 0x47, 0xd5, 0xf8,
+               0x00, 0x80, 0x48, 0xf0, 0x01, 0x08, 0xc5, 0xf8,
+               0x00, 0x80, 0xd5, 0xf8, 0x00, 0x80, 0x5f, 0xea,
+               0x88, 0x78, 0xfa, 0xd5, 0xd5, 0xf8, 0x04, 0x80,
+               0x69, 0xf3, 0x4d, 0x38, 0x48, 0xf4, 0x00, 0x48,
+               0xc5, 0xf8, 0x04, 0x80, 0x70, 0x47, 0xd5, 0xf8,
+               0x00, 0x80, 0x5f, 0xea, 0x88, 0x78, 0xfa, 0xd5,
+               0xd5, 0xf8, 0x00, 0x80, 0x5f, 0xea, 0x48, 0x68,
+               0xfa, 0xd5, 0xd5, 0xf8, 0x04, 0x80, 0x48, 0xf4,
+               0x80, 0x48, 0xc5, 0xf8, 0x04, 0x80, 0xd5, 0xf8,
+               0x04, 0x80, 0x5f, 0xea, 0x08, 0x48, 0xfa, 0xd4,
+               0xd5, 0xf8, 0x00, 0x80, 0x28, 0xf0, 0x01, 0x08,
+               0xc5, 0xf8, 0x00, 0x80, 0xd5, 0xf8, 0x00, 0x80,
+               0x5f, 0xea, 0x88, 0x78, 0xfa, 0xd5, 0x70, 0x47,
+               0x00, 0x20, 0x50, 0x60, 0x30, 0x46, 0x00, 0xbe
+       };
+
+       if (target_alloc_working_area(target, sizeof(mrvlqspi_flash_write_code),
+                       &write_algorithm) != ERROR_OK) {
+               LOG_ERROR("Insufficient working area. You must configure"\
+                       " a working area > %zdB in order to write to SPIFI flash.",
+                       sizeof(mrvlqspi_flash_write_code));
+               return ERROR_TARGET_RESOURCE_NOT_AVAILABLE;
+       };
+
+       retval = target_write_buffer(target, write_algorithm->address,
+                       sizeof(mrvlqspi_flash_write_code),
+                       mrvlqspi_flash_write_code);
+       if (retval != ERROR_OK) {
+               target_free_working_area(target, write_algorithm);
+               return retval;
+       }
+
+       /* FIFO allocation */
+       fifo_size = target_get_working_area_avail(target);
+
+       if (fifo_size == 0) {
+               /* if we already allocated the writing code but failed to get fifo
+                * space, free the algorithm */
+               target_free_working_area(target, write_algorithm);
+
+               LOG_ERROR("Insufficient working area. Please allocate at least"\
+                       " %zdB of working area to enable flash writes.",
+                       sizeof(mrvlqspi_flash_write_code) + 1
+               );
+
+               return ERROR_TARGET_RESOURCE_NOT_AVAILABLE;
+       } else if (fifo_size < page_size)
+               LOG_WARNING("Working area size is limited; flash writes may be"\
+                       " slow. Increase working area size to at least %zdB"\
+                       " to reduce write times.",
+                       (size_t)(sizeof(mrvlqspi_flash_write_code) + page_size)
+               );
+
+       if (target_alloc_working_area(target, fifo_size, &fifo) != ERROR_OK) {
+               target_free_working_area(target, write_algorithm);
+               return ERROR_TARGET_RESOURCE_NOT_AVAILABLE;
+       };
+
+       armv7m_info.common_magic = ARMV7M_COMMON_MAGIC;
+       armv7m_info.core_mode = ARM_MODE_THREAD;
+
+       init_reg_param(&reg_params[0], "r0", 32, PARAM_IN_OUT); /* buffer start, status (out) */
+       init_reg_param(&reg_params[1], "r1", 32, PARAM_OUT);    /* buffer end */
+       init_reg_param(&reg_params[2], "r2", 32, PARAM_OUT);    /* target address */
+       init_reg_param(&reg_params[3], "r3", 32, PARAM_OUT);    /* count (halfword-16bit) */
+       init_reg_param(&reg_params[4], "r4", 32, PARAM_OUT);    /* page size */
+       init_reg_param(&reg_params[5], "r5", 32, PARAM_OUT);    /* qspi base address */
+
+       buf_set_u32(reg_params[0].value, 0, 32, fifo->address);
+       buf_set_u32(reg_params[1].value, 0, 32, fifo->address + fifo->size);
+       buf_set_u32(reg_params[2].value, 0, 32, offset);
+       buf_set_u32(reg_params[3].value, 0, 32, count);
+       buf_set_u32(reg_params[4].value, 0, 32, page_size);
+       buf_set_u32(reg_params[5].value, 0, 32, (uint32_t) mrvlqspi_info->reg_base);
+
+       retval = target_run_flash_async_algorithm(target, buffer, count, 1,
+                       0, NULL,
+                       6, reg_params,
+                       fifo->address, fifo->size,
+                       write_algorithm->address, 0,
+                       &armv7m_info
+       );
+
+       if (retval != ERROR_OK)
+               LOG_ERROR("Error executing flash write algorithm");
+
+       target_free_working_area(target, fifo);
+       target_free_working_area(target, write_algorithm);
+
+       destroy_reg_param(&reg_params[0]);
+       destroy_reg_param(&reg_params[1]);
+       destroy_reg_param(&reg_params[2]);
+       destroy_reg_param(&reg_params[3]);
+       destroy_reg_param(&reg_params[4]);
+       destroy_reg_param(&reg_params[5]);
+
+       return retval;
+}
+
+int mrvlqspi_flash_read(struct flash_bank *bank, uint8_t *buffer,
+                               uint32_t offset, uint32_t count)
+{
+       struct target *target = bank->target;
+       struct mrvlqspi_flash_bank *mrvlqspi_info = bank->driver_priv;
+       int retval;
+       uint32_t i;
+
+       if (target->state != TARGET_HALTED) {
+               LOG_ERROR("Target not halted");
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       if (!(mrvlqspi_info->probed)) {
+               LOG_ERROR("Flash bank not probed");
+               return ERROR_FLASH_BANK_NOT_PROBED;
+       }
+
+       /* Flush read/write fifo's */
+       retval = mrvlqspi_fifo_flush(bank, FIFO_FLUSH_TIMEOUT);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set instruction/addr count value */
+       retval = mrvlqspi_set_hdr_cnt(bank, (0x1 | (0x3 << 4)));
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set count for number of bytes to read */
+       retval = mrvlqspi_set_din_cnt(bank, count);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set read address */
+       retval = mrvlqspi_set_addr(bank, offset);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set instruction */
+       retval = mrvlqspi_set_instr(bank, SPIFLASH_READ);
+       if (retval != ERROR_OK)
+               return retval;
+
+       /* Set data and addr pin length */
+       retval = mrvlqspi_set_conf(bank, 0x0);
+       if (retval != ERROR_OK)
+               return retval;
+
+       retval = mrvlqspi_start_transfer(bank, QSPI_R_EN);
+       if (retval != ERROR_OK)
+               return retval;
+
+       for (i = 0; i < count; i++) {
+               retval = mrvlqspi_read_byte(bank, &buffer[i]);
+               if (retval != ERROR_OK)
+                       return retval;
+       }
+
+       retval = mrvlqspi_set_ss_state(bank, QSPI_SS_DISABLE, QSPI_TIMEOUT);
+       if (retval != ERROR_OK)
+               return retval;
+
+       return ERROR_OK;
+}
+
+static int mrvlqspi_probe(struct flash_bank *bank)
+{
+       struct target *target = bank->target;
+       struct mrvlqspi_flash_bank *mrvlqspi_info = bank->driver_priv;
+       uint32_t id = 0;
+       int retval;
+       struct flash_sector *sectors;
+
+       /* If we've already probed, we should be fine to skip this time. */
+       if (mrvlqspi_info->probed)
+               return ERROR_OK;
+
+       if (target->state != TARGET_HALTED) {
+               LOG_ERROR("Target not halted");
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       mrvlqspi_info->probed = 0;
+       mrvlqspi_info->bank_num = bank->bank_number;
+
+       /* Read flash JEDEC ID */
+       retval = mrvlqspi_read_id(bank, &id);
+       if (retval != ERROR_OK)
+               return retval;
+
+       mrvlqspi_info->dev = NULL;
+       for (const struct flash_device *p = flash_devices; p->name ; p++)
+               if (p->device_id == id) {
+                       mrvlqspi_info->dev = p;
+                       break;
+               }
+
+       if (!mrvlqspi_info->dev) {
+               LOG_ERROR("Unknown flash device ID 0x%08x", id);
+               return ERROR_FAIL;
+       }
+
+       LOG_INFO("Found flash device \'%s\' ID 0x%08x",
+               mrvlqspi_info->dev->name, mrvlqspi_info->dev->device_id);
+
+       /* Set correct size value */
+       bank->size = mrvlqspi_info->dev->size_in_bytes;
+
+       /* create and fill sectors array */
+       bank->num_sectors = mrvlqspi_info->dev->size_in_bytes /
+                                       mrvlqspi_info->dev->sectorsize;
+       sectors = malloc(sizeof(struct flash_sector) * bank->num_sectors);
+       if (sectors == NULL) {
+               LOG_ERROR("not enough memory");
+               return ERROR_FAIL;
+       }
+
+       for (int sector = 0; sector < bank->num_sectors; sector++) {
+               sectors[sector].offset =
+                               sector * mrvlqspi_info->dev->sectorsize;
+               sectors[sector].size = mrvlqspi_info->dev->sectorsize;
+               sectors[sector].is_erased = -1;
+               sectors[sector].is_protected = 0;
+       }
+
+       bank->sectors = sectors;
+       mrvlqspi_info->probed = 1;
+
+       return ERROR_OK;
+}
+
+static int mrvlqspi_auto_probe(struct flash_bank *bank)
+{
+       struct mrvlqspi_flash_bank *mrvlqspi_info = bank->driver_priv;
+       if (mrvlqspi_info->probed)
+               return ERROR_OK;
+       return mrvlqspi_probe(bank);
+}
+
+static int mrvlqspi_flash_erase_check(struct flash_bank *bank)
+{
+       /* Not implemented yet */
+       return ERROR_OK;
+}
+
+static int mrvlqspi_protect_check(struct flash_bank *bank)
+{
+       /* Not implemented yet */
+       return ERROR_OK;
+}
+
+int mrvlqspi_get_info(struct flash_bank *bank, char *buf, int buf_size)
+{
+       struct mrvlqspi_flash_bank *mrvlqspi_info = bank->driver_priv;
+
+       if (!(mrvlqspi_info->probed)) {
+               snprintf(buf, buf_size,
+                       "\nQSPI flash bank not probed yet\n");
+               return ERROR_OK;
+       }
+
+       snprintf(buf, buf_size, "\nQSPI flash information:\n"
+               "  Device \'%s\' ID 0x%08x\n",
+               mrvlqspi_info->dev->name, mrvlqspi_info->dev->device_id);
+
+       return ERROR_OK;
+}
+
+FLASH_BANK_COMMAND_HANDLER(mrvlqspi_flash_bank_command)
+{
+       struct mrvlqspi_flash_bank *mrvlqspi_info;
+
+       if (CMD_ARGC < 7)
+               return ERROR_COMMAND_SYNTAX_ERROR;
+
+       mrvlqspi_info = malloc(sizeof(struct mrvlqspi_flash_bank));
+       if (mrvlqspi_info == NULL) {
+               LOG_ERROR("not enough memory");
+               return ERROR_FAIL;
+       }
+
+       /* Get QSPI controller register map base address */
+       COMMAND_PARSE_NUMBER(u32, CMD_ARGV[6], mrvlqspi_info->reg_base);
+       bank->driver_priv = mrvlqspi_info;
+       mrvlqspi_info->probed = 0;
+
+       return ERROR_OK;
+}
+
+struct flash_driver mrvlqspi_flash = {
+       .name = "mrvlqspi",
+       .flash_bank_command = mrvlqspi_flash_bank_command,
+       .erase = mrvlqspi_flash_erase,
+       .protect = NULL,
+       .write = mrvlqspi_flash_write,
+       .read = mrvlqspi_flash_read,
+       .probe = mrvlqspi_probe,
+       .auto_probe = mrvlqspi_auto_probe,
+       .erase_check = mrvlqspi_flash_erase_check,
+       .protect_check = mrvlqspi_protect_check,
+       .info = mrvlqspi_get_info,
+};

Linking to existing account procedure

If you already have an account and want to add another login method you MUST first sign in with your existing account and then change URL to read https://review.openocd.org/login/?link to get to this page again but this time it'll work for linking. Thank you.

SSH host keys fingerprints

1024 SHA256:YKx8b7u5ZWdcbp7/4AeXNaqElP49m6QrwfXaqQGJAOk gerrit-code-review@openocd.zylin.com (DSA)
384 SHA256:jHIbSQa4REvwCFG4cq5LBlBLxmxSqelQPem/EXIrxjk gerrit-code-review@openocd.org (ECDSA)
521 SHA256:UAOPYkU9Fjtcao0Ul/Rrlnj/OsQvt+pgdYSZ4jOYdgs gerrit-code-review@openocd.org (ECDSA)
256 SHA256:A13M5QlnozFOvTllybRZH6vm7iSt0XLxbA48yfc2yfY gerrit-code-review@openocd.org (ECDSA)
256 SHA256:spYMBqEYoAOtK7yZBrcwE8ZpYt6b68Cfh9yEVetvbXg gerrit-code-review@openocd.org (ED25519)
+--[ED25519 256]--+
|=..              |
|+o..   .         |
|*.o   . .        |
|+B . . .         |
|Bo. = o S        |
|Oo.+ + =         |
|oB=.* = . o      |
| =+=.+   + E     |
|. .=o   . o      |
+----[SHA256]-----+
2048 SHA256:0Onrb7/PHjpo6iVZ7xQX2riKN83FJ3KGU0TvI0TaFG4 gerrit-code-review@openocd.zylin.com (RSA)