add support for Atmel SAMD NOR Flash 84/1684/8
authorAndrey Yurovsky <yurovsky@gmail.com>
Fri, 4 Oct 2013 03:41:33 +0000 (20:41 -0700)
committerSpencer Oliver <spen@spen-soft.co.uk>
Thu, 9 Jan 2014 15:23:27 +0000 (15:23 +0000)
This adds a new NOR Flash driver, "at91samd", which supports the
built-in Flash on Atmel's D-series Cortex M MCUs, starting with the D20.
Parts and their geometry are detected automatically using the DSU and
lookup schemes described in the D20 document, 42129F–SAM–10/2013.
Future D-series variants and families should presumably use this
controller as well (possibly with minor changes and improvements).

Tested on the SAMD20 Xplained Pro board, for which we also add the
corresponding Flash configuration.

Change-Id: Id8d3dd601e9f53121682d1a1190d0be4ea3b83eb
Signed-off-by: Andrey Yurovsky <yurovsky@gmail.com>
Reviewed-on: http://openocd.zylin.com/1684
Tested-by: jenkins
Reviewed-by: Spencer Oliver <spen@spen-soft.co.uk>
src/flash/nor/Makefile.am
src/flash/nor/at91samd.c [new file with mode: 0644]
src/flash/nor/drivers.c
tcl/target/at91samdXX.cfg

index 48e6ab2215dff4d213b81fab2fd2c704d67b2c35..d817cc69cf0ac08ae0b0419756dc3f52b8711a0e 100644 (file)
@@ -11,6 +11,7 @@ NOR_DRIVERS = \
        aduc702x.c \
        at91sam4.c \
        at91sam4l.c \
+       at91samd.c \
        at91sam3.c \
        at91sam7.c \
        avrf.c \
diff --git a/src/flash/nor/at91samd.c b/src/flash/nor/at91samd.c
new file mode 100644 (file)
index 0000000..86acbc0
--- /dev/null
@@ -0,0 +1,586 @@
+/***************************************************************************
+ *   Copyright (C) 2013 by Andrey Yurovsky                                 *
+ *   Andrey Yurovsky <yurovsky@gmail.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.           *
+ ***************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "imp.h"
+
+#define SAMD_NUM_SECTORS       16
+
+#define SAMD_FLASH                     0x00000000      /* physical Flash memory */
+#define SAMD_DSU                       0x41002000      /* Device Service Unit */
+#define SAMD_NVMCTRL           0x41004000      /* Non-volatile memory controller */
+
+#define SAMD_DSU_DID           0x18            /* Device ID register */
+
+#define SAMD_NVMCTRL_CTRLA             0x00    /* NVM control A register */
+#define SAMD_NVMCTRL_CTRLB             0x04    /* NVM control B register */
+#define SAMD_NVMCTRL_PARAM             0x08    /* NVM parameters register */
+#define SAMD_NVMCTRL_INTFLAG   0x18    /* NVM Interupt Flag Status & Clear */
+#define SAMD_NVMCTRL_STATUS            0x18    /* NVM status register */
+#define SAMD_NVMCTRL_ADDR              0x1C    /* NVM address register */
+#define SAMD_NVMCTRL_LOCK              0x20    /* NVM Lock section register */
+
+#define SAMD_CMDEX_KEY         0xA5UL
+#define SAMD_NVM_CMD(n)                ((SAMD_CMDEX_KEY << 8) | (n & 0x7F))
+
+/* NVMCTRL commands.  See Table 20-4 in 42129F–SAM–10/2013 */
+#define SAMD_NVM_CMD_ER                0x02            /* Erase Row */
+#define SAMD_NVM_CMD_WP                0x04            /* Write Page */
+#define SAMD_NVM_CMD_EAR       0x05            /* Erase Auxilary Row */
+#define SAMD_NVM_CMD_WAP       0x06            /* Write Auxilary Page */
+#define SAMD_NVM_CMD_LR                0x40            /* Lock Region */
+#define SAMD_NVM_CMD_UR                0x41            /* Unlock Region */
+#define SAMD_NVM_CMD_SPRM      0x42            /* Set Power Reduction Mode */
+#define SAMD_NVM_CMD_CPRM      0x43            /* Clear Power Reduction Mode */
+#define SAMD_NVM_CMD_PBC       0x44            /* Page Buffer Clear */
+#define SAMD_NVM_CMD_SSB       0x45            /* Set Security Bit */
+#define SAMD_NVM_CMD_INVALL    0x46            /* Invalidate all caches */
+
+/* Known identifiers */
+#define SAMD_PROCESSOR_M0      0x01
+#define SAMD_FAMILY_D          0x00
+#define SAMD_SERIES_20         0x00
+
+struct samd_part {
+       uint8_t id;
+       const char *name;
+       uint32_t flash_kb;
+       uint32_t ram_kb;
+};
+
+/* Known SAMD20 parts. See Table 12-8 in 42129F–SAM–10/2013 */
+static struct samd_part samd20_parts[] = {
+       { 0x0, "SAMD20J18A", 256, 32 },
+       { 0x1, "SAMD20J17A", 128, 16 },
+       { 0x2, "SAMD20J16A", 64, 8 },
+       { 0x3, "SAMD20J15A", 32, 4 },
+       { 0x4, "SAMD20J14A", 16, 2 },
+       { 0x5, "SAMD20G18A", 256, 32 },
+       { 0x6, "SAMD20G17A", 128, 16 },
+       { 0x7, "SAMD20G16A", 64, 8 },
+       { 0x8, "SAMD20G15A", 32, 4 },
+       { 0x9, "SAMD20G14A", 16, 2 },
+       { 0xB, "SAMD20E17A", 128, 16 },
+       { 0xC, "SAMD20E16A", 64, 8 },
+       { 0xD, "SAMD20E15A", 32, 4 },
+       { 0xE, "SAMD20E14A", 16, 2 },
+};
+
+/* Each family of parts contains a parts table in the DEVSEL field of DID.  The
+ * processor ID, family ID, and series ID are used to determine which exact
+ * family this is and then we can use the corresponding table. */
+struct samd_family {
+       uint8_t processor;
+       uint8_t family;
+       uint8_t series;
+       struct samd_part *parts;
+       size_t num_parts;
+};
+
+/* Known SAMD families */
+static struct samd_family samd_families[] = {
+       { SAMD_PROCESSOR_M0, SAMD_FAMILY_D, SAMD_SERIES_20,
+               samd20_parts, ARRAY_SIZE(samd20_parts) },
+};
+
+struct samd_info {
+       uint32_t page_size;
+       int num_pages;
+       int sector_size;
+
+       bool probed;
+       struct target *target;
+       struct samd_info *next;
+};
+
+static struct samd_info *samd_chips;
+
+static struct samd_part *samd_find_part(uint32_t id)
+{
+       uint8_t processor = (id >> 28);
+       uint8_t family = (id >> 24) & 0x0F;
+       uint8_t series = (id >> 16) & 0xFF;
+       uint8_t devsel = id & 0xFF;
+
+       for (unsigned i = 0; i < ARRAY_SIZE(samd_families); i++) {
+               if (samd_families[i].processor == processor &&
+                       samd_families[i].series == series &&
+                       samd_families[i].family == family) {
+                       for (unsigned j = 0; j < samd_families[i].num_parts; j++) {
+                               if (samd_families[i].parts[j].id == devsel)
+                                       return &samd_families[i].parts[j];
+                       }
+               }
+       }
+
+       return NULL;
+}
+
+static int samd_protect_check(struct flash_bank *bank)
+{
+       int res;
+       uint16_t lock;
+
+       res = target_read_u16(bank->target,
+                       SAMD_NVMCTRL + SAMD_NVMCTRL_LOCK, &lock);
+       if (res != ERROR_OK)
+               return res;
+
+       /* Lock bits are active-low */
+       for (int i = 0; i < bank->num_sectors; i++)
+               bank->sectors[i].is_protected = !(lock & (1<<i));
+
+       return ERROR_OK;
+}
+
+static int samd_probe(struct flash_bank *bank)
+{
+       uint32_t id, param;
+       int res;
+       struct samd_info *chip = (struct samd_info *)bank->driver_priv;
+       struct samd_part *part;
+
+       if (chip->probed)
+               return ERROR_OK;
+
+       res = target_read_u32(bank->target, SAMD_DSU + SAMD_DSU_DID, &id);
+       if (res != ERROR_OK) {
+               LOG_ERROR("Couldn't read Device ID register");
+               return res;
+       }
+
+       part = samd_find_part(id);
+       if (part == NULL) {
+               LOG_ERROR("Couldn't find part correspoding to DID %08" PRIx32, id);
+               return ERROR_FAIL;
+       }
+
+       res = target_read_u32(bank->target,
+                       SAMD_NVMCTRL + SAMD_NVMCTRL_PARAM, &param);
+       if (res != ERROR_OK) {
+               LOG_ERROR("Couldn't read NVM Parameters register");
+               return res;
+       }
+
+       bank->size = part->flash_kb * 1024;
+
+       chip->sector_size = bank->size / SAMD_NUM_SECTORS;
+
+       /* The PSZ field (bits 18:16) indicate the page size bytes as 2^(3+n) so
+        * 0 is 8KB and 7 is 1024KB. */
+       chip->page_size = (8 << ((param >> 16) & 0x7));
+       /* The NVMP field (bits 15:0) indicates the total number of pages */
+       chip->num_pages = param & 0xFFFF;
+
+       /* Sanity check: the total flash size in the DSU should match the page size
+        * multiplied by the number of pages. */
+       if (bank->size != chip->num_pages * chip->page_size) {
+               LOG_WARNING("SAMD: bank size doesn't match NVM parameters. "
+                               "Identified %uKB Flash but NVMCTRL reports %u %uB pages",
+                               part->flash_kb, chip->num_pages, chip->page_size);
+       }
+
+       /* Allocate the sector table */
+       bank->num_sectors = SAMD_NUM_SECTORS;
+       bank->sectors = calloc(bank->num_sectors, sizeof((bank->sectors)[0]));
+       if (!bank->sectors)
+               return ERROR_FAIL;
+
+       /* Fill out the sector information: all SAMD sectors are the same size and
+        * there is always a fixed number of them. */
+       for (int i = 0; i < bank->num_sectors; i++) {
+               bank->sectors[i].size = chip->sector_size;
+               bank->sectors[i].offset = i * chip->sector_size;
+               /* mark as unknown */
+               bank->sectors[i].is_erased = -1;
+               bank->sectors[i].is_protected = -1;
+       }
+
+       samd_protect_check(bank);
+
+       /* Done */
+       chip->probed = true;
+
+       LOG_INFO("SAMD MCU: %s (%uKB Flash, %uKB RAM)", part->name,
+                       part->flash_kb, part->ram_kb);
+
+       return ERROR_OK;
+}
+
+static int samd_protect(struct flash_bank *bank, int set, int first, int last)
+{
+       int res;
+       struct samd_info *chip = (struct samd_info *)bank->driver_priv;
+
+       for (int s = first; s <= last; s++) {
+               /* Load an address that is within this sector (we use offset 0) */
+               res = target_write_u32(bank->target, SAMD_NVMCTRL + SAMD_NVMCTRL_ADDR,
+                               s * chip->sector_size);
+               if (res != ERROR_OK)
+                       return res;
+
+               /* Tell the controller to lock that sector */
+               res = target_write_u16(bank->target,
+                               SAMD_NVMCTRL + SAMD_NVMCTRL_CTRLA,
+                               SAMD_NVM_CMD(SAMD_NVM_CMD_LR));
+               if (res != ERROR_OK)
+                       return res;
+       }
+
+       samd_protect_check(bank);
+
+       return ERROR_OK;
+}
+
+static bool samd_check_error(struct flash_bank *bank)
+{
+       int ret;
+       bool error;
+       uint16_t status;
+
+       ret = target_read_u16(bank->target,
+                       SAMD_NVMCTRL + SAMD_NVMCTRL_STATUS, &status);
+       if (ret != ERROR_OK) {
+               LOG_ERROR("Can't read NVM status");
+               return true;
+       }
+
+       if (status & 0x001C) {
+               if (status & (1 << 4)) /* NVME */
+                       LOG_ERROR("SAMD: NVM Error");
+               if (status & (1 << 3)) /* LOCKE */
+                       LOG_ERROR("SAMD: NVM lock error");
+               if (status & (1 << 2)) /* PROGE */
+                       LOG_ERROR("SAMD: NVM programming error");
+
+               error = true;
+       } else {
+               error = false;
+       }
+
+       /* Clear the error conditions by writing a one to them */
+       ret = target_write_u16(bank->target,
+                       SAMD_NVMCTRL + SAMD_NVMCTRL_STATUS, status);
+       if (ret != ERROR_OK)
+               LOG_ERROR("Can't clear NVM error conditions");
+
+       return error;
+}
+
+static int samd_erase_row(struct flash_bank *bank, uint32_t address)
+{
+       int res;
+       bool error = false;
+
+       /* Set an address contained in the row to be erased */
+       res = target_write_u32(bank->target,
+                       SAMD_NVMCTRL + SAMD_NVMCTRL_ADDR, address >> 1);
+       if (res == ERROR_OK) {
+               /* Issue the Erase Row command to erase that row */
+               res = target_write_u16(bank->target,
+                               SAMD_NVMCTRL + SAMD_NVMCTRL_CTRLA,
+                               SAMD_NVM_CMD(SAMD_NVM_CMD_ER));
+
+               /* Check (and clear) error conditions */
+               error = samd_check_error(bank);
+       }
+
+       if (res != ERROR_OK || error)  {
+               LOG_ERROR("Failed to erase row containing %08X" PRIx32, address);
+               return ERROR_FAIL;
+       }
+
+       return ERROR_OK;
+}
+
+static int samd_erase(struct flash_bank *bank, int first, int last)
+{
+       int res;
+       int rows_in_sector;
+       struct samd_info *chip = (struct samd_info *)bank->driver_priv;
+
+       if (bank->target->state != TARGET_HALTED) {
+               LOG_ERROR("Target not halted");
+
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       if (!chip->probed) {
+               if (samd_probe(bank) != ERROR_OK)
+                       return ERROR_FLASH_BANK_NOT_PROBED;
+       }
+
+       /* Make sure the sectors make sense. */
+       if (first >= bank->num_sectors || last >= bank->num_sectors) {
+               LOG_ERROR("Erase range %d - %d not valid (%d sectors total)",
+                               first, last, bank->num_sectors);
+               return ERROR_FAIL;
+       }
+
+       /* The SAMD NVM has row erase granularity.  There are four pages in a row
+        * and the number of rows in a sector depends on the sector size, which in
+        * turn depends on the Flash capacity as there is a fixed number of
+        * sectors. */
+       rows_in_sector = chip->sector_size / (chip->page_size * 4);
+
+       /* For each sector to be erased */
+       for (int s = first; s <= last; s++) {
+               /* For each row in that sector */
+               for (int r = s * rows_in_sector; r < (s + 1) * rows_in_sector; r++) {
+                       res = samd_erase_row(bank, r * chip->page_size * 4);
+                       if (res != ERROR_OK) {
+                               LOG_ERROR("SAMD: failed to erase sector %d", s);
+                               return res;
+                       }
+               }
+
+               bank->sectors[s].is_erased = 1;
+       }
+
+       return ERROR_OK;
+}
+
+/* Write an entire row (four pages) from host buffer 'buf' to row-aligned
+ * 'address' in the Flash. */
+static int samd_write_row(struct flash_bank *bank, uint32_t address,
+               uint8_t *buf)
+{
+       int res;
+       struct samd_info *chip = (struct samd_info *)bank->driver_priv;
+
+       /* Erase the row that we'll be writing to */
+       res = samd_erase_row(bank, address);
+       if (res != ERROR_OK)
+               return res;
+
+       /* Now write the pages in this row. */
+       for (unsigned int i = 0; i < 4; i++) {
+               bool error;
+
+               /* Write the page contents to the target's page buffer.  A page write
+                * is issued automatically once the last location is written in the
+                * page buffer (ie: a complete page has been written out). */
+               res = target_write_memory(bank->target, address, 4,
+                               chip->page_size / 4, buf);
+               if (res != ERROR_OK) {
+                       LOG_ERROR("%s: %d", __func__, __LINE__);
+                       return res;
+               }
+
+               error = samd_check_error(bank);
+               if (error)
+                       return ERROR_FAIL;
+
+               /* Next page */
+               address += chip->page_size;
+               buf += chip->page_size;
+       }
+
+       return res;
+}
+
+/* Write partial contents into row-aligned 'address' on the Flash from host
+ * buffer 'buf' by writing 'nb' of 'buf' at 'row_offset' into the Flash row. */
+static int samd_write_row_partial(struct flash_bank *bank, uint32_t address,
+               uint8_t *buf, uint32_t row_offset, uint32_t nb)
+{
+       int res;
+       struct samd_info *chip = (struct samd_info *)bank->driver_priv;
+       uint32_t row_size = chip->page_size * 4;
+       uint8_t *rb = malloc(row_size);
+       if (!rb)
+               return ERROR_FAIL;
+
+       assert(row_offset + nb < row_size);
+       assert((address % row_size) == 0);
+
+       /* Retrieve the full row contents from Flash */
+       res = target_read_memory(bank->target, address, 4, row_size / 4, rb);
+       if (res != ERROR_OK) {
+               free(rb);
+               return res;
+       }
+
+       /* Insert our partial row over the data from Flash */
+       memcpy(rb + (row_offset % row_size), buf, nb);
+
+       /* Write the row back out */
+       res = samd_write_row(bank, address, rb);
+       free(rb);
+
+       return res;
+}
+
+static int samd_write(struct flash_bank *bank, uint8_t *buffer,
+               uint32_t offset, uint32_t count)
+{
+       int res;
+       uint32_t address;
+       uint32_t nb = 0;
+       struct samd_info *chip = (struct samd_info *)bank->driver_priv;
+       uint32_t row_size = chip->page_size * 4;
+
+       if (bank->target->state != TARGET_HALTED) {
+               LOG_ERROR("Target not halted");
+
+               return ERROR_TARGET_NOT_HALTED;
+       }
+
+       if (!chip->probed) {
+               if (samd_probe(bank) != ERROR_OK)
+                       return ERROR_FLASH_BANK_NOT_PROBED;
+       }
+
+       if (offset % row_size) {
+               /* We're starting at an unaligned offset so we'll write a partial row
+                * comprising that offset and up to the end of that row. */
+               nb = row_size - (offset % row_size);
+               if (nb > count)
+                       nb = count;
+       } else if (count < row_size) {
+               /* We're writing an aligned but partial row. */
+               nb = count;
+       }
+
+       address = (offset / row_size) * row_size + bank->base;
+
+       if (nb > 0) {
+               res = samd_write_row_partial(bank, address, buffer,
+                               offset % row_size, nb);
+               if (res != ERROR_OK)
+                       return res;
+
+               /* We're done with the row contents */
+               count -= nb;
+               offset += nb;
+               buffer += row_size;
+       }
+
+       /* There's at least one aligned row to write out. */
+       if (count >= row_size) {
+               int nr = count / row_size + ((count % row_size) ? 1 : 0);
+               unsigned int r = 0;
+
+               for (unsigned int i = address / row_size;
+                               (i < (address / row_size) + nr) && count > 0; i++) {
+                       address = (i * row_size) + bank->base;
+
+                       if (count >= row_size) {
+                               res = samd_write_row(bank, address, buffer + (r * row_size));
+                               /* Advance one row */
+                               offset += row_size;
+                               count -= row_size;
+                       } else {
+                               res = samd_write_row_partial(bank, address,
+                                               buffer + (r * row_size), 0, count);
+                               /* We're done after this. */
+                               offset += count;
+                               count = 0;
+                       }
+
+                       r++;
+
+                       if (res != ERROR_OK)
+                               return res;
+               }
+       }
+
+       return ERROR_OK;
+}
+
+FLASH_BANK_COMMAND_HANDLER(samd_flash_bank_command)
+{
+       struct samd_info *chip = samd_chips;
+
+       while (chip) {
+               if (chip->target == bank->target)
+                       break;
+               chip = chip->next;
+       }
+
+       if (!chip) {
+               /* Create a new chip */
+               chip = calloc(1, sizeof(*chip));
+               if (!chip)
+                       return ERROR_FAIL;
+
+               chip->target = bank->target;
+               chip->probed = false;
+
+               bank->driver_priv = chip;
+
+               /* Insert it into the chips list (at head) */
+               chip->next = samd_chips;
+               samd_chips = chip;
+       }
+
+       if (bank->base != SAMD_FLASH) {
+               LOG_ERROR("Address 0x%08" PRIx32 " invalid bank address (try 0x%08" PRIx32
+                               "[at91samd series] )",
+                               bank->base, SAMD_FLASH);
+               return ERROR_FAIL;
+       }
+
+       return ERROR_OK;
+}
+
+COMMAND_HANDLER(samd_handle_info_command)
+{
+       return ERROR_OK;
+}
+
+static const struct command_registration at91samd_exec_command_handlers[] = {
+       {
+               .name = "info",
+               .handler = samd_handle_info_command,
+               .mode = COMMAND_EXEC,
+               .help = "Print information about the current at91samd chip"
+                       "and its flash configuration.",
+       },
+       COMMAND_REGISTRATION_DONE
+};
+
+static const struct command_registration at91samd_command_handlers[] = {
+       {
+               .name = "at91samd",
+               .mode = COMMAND_ANY,
+               .help = "at91samd flash command group",
+               .usage = "",
+               .chain = at91samd_exec_command_handlers,
+       },
+       COMMAND_REGISTRATION_DONE
+};
+
+struct flash_driver at91samd_flash = {
+       .name = "at91samd",
+       .commands = at91samd_command_handlers,
+       .flash_bank_command = samd_flash_bank_command,
+       .erase = samd_erase,
+       .protect = samd_protect,
+       .write = samd_write,
+       .read = default_flash_read,
+       .probe = samd_probe,
+       .auto_probe = samd_probe,
+       .erase_check = default_flash_blank_check,
+       .protect_check = samd_protect_check,
+};
index b85ff13ada83dce7e51f4832e718bda0a4302c42..39c2f9fcd7ea949062e000e8713b3a82a06e95a7 100644 (file)
@@ -30,6 +30,7 @@ extern struct flash_driver cfi_flash;
 extern struct flash_driver at91sam3_flash;
 extern struct flash_driver at91sam4_flash;
 extern struct flash_driver at91sam4l_flash;
+extern struct flash_driver at91samd_flash;
 extern struct flash_driver at91sam7_flash;
 extern struct flash_driver str7x_flash;
 extern struct flash_driver str9x_flash;
@@ -69,6 +70,7 @@ static struct flash_driver *flash_drivers[] = {
        &at91sam3_flash,
        &at91sam4_flash,
        &at91sam4l_flash,
+       &at91samd_flash,
        &str7x_flash,
        &str9x_flash,
        &aduc702x_flash,
index 0a1ef26c9162d6a1609651c5c687756cd1a68414..072459dff9e2b424ab799664de4bcd8bc6e69e30 100644 (file)
@@ -58,4 +58,5 @@ adapter_nsrst_delay 100
 # perform a soft reset
 cortex_m reset_config sysresetreq
 
-# no flash defined yet
+set _FLASHNAME $_CHIPNAME.flash
+flash bank $_FLASHNAME at91samd 0x00000000 0 1 1 $_TARGETNAME

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)