This is an automated email from Gerrit. Tarek BOCHKATI ([email protected]) just uploaded a new patch set to Gerrit, which you can find at http://openocd.zylin.com/5395
-- gerrit commit 0dfd27abdd78fd6b7ef37e619a1a65ba2932364c Author: Tarek BOCHKATI <[email protected]> Date: Sat Jan 11 15:18:16 2020 +0100 [WIP] flash/stm32l4: enhance protect handler to use efficiently all WRP areas stm32l4_protect: is always using one WRP area per bank, without checking if it is already protecting some sectors. protection algo is more complicated than that, before using a WRP area we should check if it is already used, then either reuse it for extension (or reduction) or use a free area. another thing to be considered. for some devices like STM32L4R/S in single bank mode, all 4 WRP areas are usable for that bank. To do that, a helper (interval) was introduced to optimize WRP areas' usage Change-Id: I6fc28346b4f6dd0217dd98bc585c5b4265cfe24d Signed-off-by: Tarek BOCHKATI <[email protected]> diff --git a/src/flash/nor/stm32l4x.c b/src/flash/nor/stm32l4x.c index 3d15377..48ffaaf 100644 --- a/src/flash/nor/stm32l4x.c +++ b/src/flash/nor/stm32l4x.c @@ -25,6 +25,7 @@ #include "imp.h" #include <helper/binarybuffer.h> +#include <helper/interval.h> #include <target/algorithm.h> #include <target/armv7m.h> #include "bits.h" @@ -137,6 +138,9 @@ struct stm32l4_part_info { const size_t num_revs; const uint16_t max_flash_size_kb; const bool has_dual_bank; + /* this field is used for dual bank devices only, it indicates if the + * 4 WRPxx are usable if the device is configured in single-bank mode*/ + const bool use_all_wrpxx; const uint32_t flash_regs_base; const uint32_t fsize_addr; }; @@ -150,6 +154,21 @@ struct stm32l4_flash_bank { const struct stm32l4_part_info *part_info; }; +enum stm32_bank_id { + STM32_BANK1, + STM32_BANK2, + STM32_ALL_BANKS +}; + +struct stm32l4_wrp { + uint32_t addr; + uint32_t value; + bool used; + int first; + int last; + int offset; +}; + static const struct stm32l4_rev stm32_415_revs[] = { { 0x1000, "1" }, { 0x1001, "2" }, { 0x1003, "3" }, { 0x1007, "4" } }; @@ -190,6 +209,7 @@ static const struct stm32l4_part_info stm32l4_parts[] = { .device_str = "STM32L47/L48xx", .max_flash_size_kb = 1024, .has_dual_bank = true, + .use_all_wrpxx = false, .flash_regs_base = 0x40022000, .fsize_addr = 0x1FFF75E0, }, @@ -200,6 +220,7 @@ static const struct stm32l4_part_info stm32l4_parts[] = { .device_str = "STM32L43/L44xx", .max_flash_size_kb = 256, .has_dual_bank = false, + .use_all_wrpxx = false, .flash_regs_base = 0x40022000, .fsize_addr = 0x1FFF75E0, }, @@ -210,6 +231,7 @@ static const struct stm32l4_part_info stm32l4_parts[] = { .device_str = "STM32L49/L4Axx", .max_flash_size_kb = 1024, .has_dual_bank = true, + .use_all_wrpxx = false, .flash_regs_base = 0x40022000, .fsize_addr = 0x1FFF75E0, }, @@ -220,6 +242,7 @@ static const struct stm32l4_part_info stm32l4_parts[] = { .device_str = "STM32L45/L46xx", .max_flash_size_kb = 512, .has_dual_bank = false, + .use_all_wrpxx = false, .flash_regs_base = 0x40022000, .fsize_addr = 0x1FFF75E0, }, @@ -230,6 +253,7 @@ static const struct stm32l4_part_info stm32l4_parts[] = { .device_str = "STM32L41/L42xx", .max_flash_size_kb = 128, .has_dual_bank = false, + .use_all_wrpxx = false, .flash_regs_base = 0x40022000, .fsize_addr = 0x1FFF75E0, }, @@ -240,6 +264,7 @@ static const struct stm32l4_part_info stm32l4_parts[] = { .device_str = "STM32L4R/L4Sxx", .max_flash_size_kb = 2048, .has_dual_bank = true, + .use_all_wrpxx = true, .flash_regs_base = 0x40022000, .fsize_addr = 0x1FFF75E0, }, @@ -250,6 +275,7 @@ static const struct stm32l4_part_info stm32l4_parts[] = { .device_str = "STM32L4P5/L4Q5x", .max_flash_size_kb = 1024, .has_dual_bank = true, + .use_all_wrpxx = true, .flash_regs_base = 0x40022000, .fsize_addr = 0x1FFF75E0, }, @@ -260,6 +286,7 @@ static const struct stm32l4_part_info stm32l4_parts[] = { .device_str = "STM32WB5x", .max_flash_size_kb = 1024, .has_dual_bank = false, + .use_all_wrpxx = false, .flash_regs_base = 0x58004000, .fsize_addr = 0x1FFF75E0, }, @@ -438,46 +465,76 @@ static int stm32l4_write_option(struct flash_bank *bank, uint32_t reg_offset, ui return retval; } -static int stm32l4_protect_check(struct flash_bank *bank) +static int stm32l4_get_wrpxy(struct flash_bank *bank, enum stm32_bank_id bank_id, + struct stm32l4_wrp *wrpxy, int *n_wrp) { struct stm32l4_flash_bank *stm32l4_info = bank->driver_priv; + int ret; - uint32_t wrp1ar, wrp1br, wrp2ar, wrp2br; - stm32l4_read_flash_reg(bank, STM32_FLASH_WRP1AR, &wrp1ar); - stm32l4_read_flash_reg(bank, STM32_FLASH_WRP1BR, &wrp1br); - stm32l4_read_flash_reg(bank, STM32_FLASH_WRP2AR, &wrp2ar); - stm32l4_read_flash_reg(bank, STM32_FLASH_WRP2BR, &wrp2br); - - const uint8_t wrp1a_start = wrp1ar & 0xFF; - const uint8_t wrp1a_end = (wrp1ar >> 16) & 0xFF; - const uint8_t wrp1b_start = wrp1br & 0xFF; - const uint8_t wrp1b_end = (wrp1br >> 16) & 0xFF; - const uint8_t wrp2a_start = wrp2ar & 0xFF; - const uint8_t wrp2a_end = (wrp2ar >> 16) & 0xFF; - const uint8_t wrp2b_start = wrp2br & 0xFF; - const uint8_t wrp2b_end = (wrp2br >> 16) & 0xFF; - - for (int i = 0; i < bank->num_sectors; i++) { - if (i < stm32l4_info->bank1_sectors) { - if (((i >= wrp1a_start) && - (i <= wrp1a_end)) || - ((i >= wrp1b_start) && - (i <= wrp1b_end))) - bank->sectors[i].is_protected = 1; - else - bank->sectors[i].is_protected = 0; - } else { - uint8_t snb; - snb = i - stm32l4_info->bank1_sectors; - if (((snb >= wrp2a_start) && - (snb <= wrp2a_end)) || - ((snb >= wrp2b_start) && - (snb <= wrp2b_end))) + *n_wrp = 0; + + /* for single bank devices there is 2 WRP regions. + * for dual bank devices there is 2 WRP regions per bank, + * if configured as single bank only 2 WRP are usable + * except for STM32L4R/S and STM32G4 cat3, all 4 WRP are usable + * note: this should be revised, if a device will have the SWAP banks option + */ + + int wrp2y_sectors_offset = -1; /* -1 : unused */ + + /* if bank_id is BANK1 or ALL_BANKS */ + if (bank_id != STM32_BANK2) { + wrpxy[*n_wrp++] = (struct stm32l4_wrp){.addr = STM32_FLASH_WRP1AR, .offset = 0}; + wrpxy[*n_wrp++] = (struct stm32l4_wrp){.addr = STM32_FLASH_WRP1BR, .offset = 0}; + /* for some devices (like STM32L4R/S) in single-bank mode, the 4 WRPxx are usable */ + if (stm32l4_info->part_info->use_all_wrpxx && !stm32l4_info->dual_bank_mode) + wrp2y_sectors_offset = 0; + } + + /* if bank_id is BANK2 or ALL_BANKS */ + if (bank_id != STM32_BANK1 && stm32l4_info->dual_bank_mode) + wrp2y_sectors_offset = stm32l4_info->bank1_sectors; + + if (wrp2y_sectors_offset > -1) { + wrpxy[*n_wrp++] = (struct stm32l4_wrp){.addr = STM32_FLASH_WRP2AR, .offset = wrp2y_sectors_offset}; + wrpxy[*n_wrp++] = (struct stm32l4_wrp){.addr = STM32_FLASH_WRP2BR, .offset = wrp2y_sectors_offset}; + } + + /* read available WRPxx */ + for (int i = 0; i < *n_wrp; i++) { + ret = stm32l4_read_flash_reg(bank, wrpxy[i].addr, &wrpxy[i].value); + if (ret != ERROR_OK) + return ret; + + wrpxy[i].first = (wrpxy[i].value & 0xff) + wrpxy[i].offset; + wrpxy[i].last = ((wrpxy[i].value >> 16) & 0xff) + wrpxy[i].offset; + wrpxy[i].used = wrpxy[i].first <= wrpxy[i].last; + } + + return ERROR_OK; +} + +static int stm32l4_protect_check(struct flash_bank *bank) +{ + int n_wrp; + struct stm32l4_wrp wrpxy[4]; + + int ret = stm32l4_get_wrpxy(bank, STM32_ALL_BANKS, wrpxy, &n_wrp); + if (ret != ERROR_OK) + return ret; + + /* initialize all sectors as unprotected */ + for (int i = 0; i < bank->num_sectors; i++) + bank->sectors[i].is_protected = 0; + + /* now check WRPxy and mark the protected sectors */ + for (int i = 0; i < n_wrp; i++) { + if (wrpxy[i].used) { + for (int s = wrpxy[i].first; s <= wrpxy[i].last; s++) bank->sectors[i].is_protected = 1; - else - bank->sectors[i].is_protected = 0; } } + return ERROR_OK; } @@ -542,35 +599,124 @@ static int stm32l4_protect(struct flash_bank *bank, int set, int first, int last { struct target *target = bank->target; struct stm32l4_flash_bank *stm32l4_info = bank->driver_priv; + int ret = ERROR_OK; + int i; if (target->state != TARGET_HALTED) { LOG_ERROR("Target not halted"); return ERROR_TARGET_NOT_HALTED; } - int ret = ERROR_OK; - /* Bank 2 */ - uint32_t reg_value = 0xFF; /* Default to bank un-protected */ + /* the requested sectors could be located into bank1 and/or bank2 */ + bool use_bank2 = false; if (last >= stm32l4_info->bank1_sectors) { - if (set == 1) { - uint8_t begin = first > stm32l4_info->bank1_sectors ? first : 0x00; - reg_value = ((last & 0xFF) << 16) | begin; + if (first < stm32l4_info->bank1_sectors) { + /* the requested sectors for (un)protection are shared between + * bank 1 and 2, then split the operation */ + + /* 1- deal with bank 1 sectors */ + LOG_INFO("The requested sectors for %s are shared between bank 1 and 2", + set ? "protection" : "unprotection"); + ret = stm32l4_protect(bank, set, first, stm32l4_info->bank1_sectors - 1); + if (ret != ERROR_OK) + return ret; + + /* 2- then continue with bank 2 sectors */ + first = stm32l4_info->bank1_sectors; } - ret = stm32l4_write_option(bank, STM32_FLASH_WRP2AR, reg_value, 0xffffffff); + use_bank2 = true; } - /* Bank 1 */ - reg_value = 0xFF; /* Default to bank un-protected */ - if (first < stm32l4_info->bank1_sectors) { - if (set == 1) { - uint8_t end = last >= stm32l4_info->bank1_sectors ? 0xFF : last; - reg_value = (end << 16) | (first & 0xFF); + + /* refresh the sectors' protection */ + ret = stm32l4_protect_check(bank); + if (ret != ERROR_OK) + return ret; + + /* check if the desired protection is already configured */ + for (i = first; i <= last; i++) { + if (bank->sectors[i].is_protected != set) + break; + else if (i == last) { + LOG_INFO("The specified sectors are already %s", set ? "protected" : "unprotected"); + return ERROR_OK; } + } + + /* all sectors from first to last (or part of them) could have different + * protection other than the requested */ + int n_wrp; + struct stm32l4_wrp wrpxy[4]; - ret = stm32l4_write_option(bank, STM32_FLASH_WRP1AR, reg_value, 0xffffffff); + ret = stm32l4_get_wrpxy(bank, use_bank2 ? STM32_BANK2 : STM32_BANK1, wrpxy, &n_wrp); + if (ret != ERROR_OK) + return ret; + + /* use the interval helper to optimize the WRP usage */ + interval_t *wrp_intervals = NULL; + + for (i = 0; i < n_wrp; i++) { + if (wrpxy[i].used) { + ret = interval_append(&wrp_intervals, wrpxy[i].first, wrpxy[i].last); + if (ret != ERROR_OK) + return ret; + } } - return ret; + interval_print_all(wrp_intervals); + + if (set) { /* flash protect */ + ret = interval_append(&wrp_intervals, first, last); + if (ret != ERROR_OK) + return ret; + } else { /* flash unprotect */ + ret = interval_delete(&wrp_intervals, first, last); + if (ret != ERROR_OK) + return ret; + } + + /* reorder the WRP intervals */ + interval_print_all(wrp_intervals); + interval_reorder(&wrp_intervals); + interval_print_all(wrp_intervals); + + int n_intervals = interval_count(wrp_intervals); + if (n_intervals > n_wrp) { + LOG_ERROR("the device WRPxy are not enough to set the requested protection"); + return ERROR_FAIL; + } + + /* re-init all WRPxy as disabled (first > last)*/ + for (i = 0; i < n_wrp; i++) { + wrpxy[i].first = wrpxy[i].offset + 1; + wrpxy[i].last = wrpxy[i].offset; + } + + /* then configure WRPxy areas */ + interval_t *tmp = wrp_intervals; + i = 0; + while (tmp) { + wrpxy[i].first = tmp->start; + wrpxy[i].last = tmp->end; + i++; + tmp = tmp->next; + } + + + /* finally write WRPxy registers */ + for (i = 0; i < n_wrp; i++) { + int wrp_start = wrpxy[i].first - wrpxy[i].offset; + int wrp_end = wrpxy[i].last - wrpxy[i].offset; + + uint32_t wrp_value = (wrp_start & 0xff) | ((wrp_end & 0xff) << 16); + + ret = stm32l4_write_option(bank, wrpxy[i].addr, wrp_value, 0x00ff00ff); + + if (ret != ERROR_OK) + return ret; + } + + return ERROR_OK; } /* Count is in halfwords */ diff --git a/src/helper/Makefile.am b/src/helper/Makefile.am index 2b3523f..d18fc6f 100644 --- a/src/helper/Makefile.am +++ b/src/helper/Makefile.am @@ -15,6 +15,7 @@ noinst_LTLIBRARIES += %D%/libhelper.la %D%/util.c \ %D%/jep106.c \ %D%/jim-nvp.c \ + %D%/interval.c \ %D%/binarybuffer.h \ %D%/bits.h \ %D%/configuration.h \ @@ -30,7 +31,8 @@ noinst_LTLIBRARIES += %D%/libhelper.la %D%/system.h \ %D%/jep106.h \ %D%/jep106.inc \ - %D%/jim-nvp.h + %D%/jim-nvp.h \ + %D%/interval.h if IOUTIL %C%_libhelper_la_SOURCES += %D%/ioutil.c diff --git a/src/helper/interval.c b/src/helper/interval.c new file mode 100644 index 0000000..cc6d59a --- /dev/null +++ b/src/helper/interval.c @@ -0,0 +1,291 @@ +/*************************************************************************** + * * + * Copyright (C) 2020 by Tarek Bochkati for STMicroelectronics * + * [email protected] * + * * + * 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, see <http://www.gnu.org/licenses/>. * + ***************************************************************************/ + +#include "interval.h" + +#include <stdlib.h> +#include "log.h" + +/* this enum is used to compare two intervals + * - interval1 defined by [s1, e1] (s1 <= e1) + * - interval2 defined by [s2, e2] (s2 <= e2) + */ +enum interval_comparison_result { + INTERVAL_EQUAL, /* intervals are equal */ + INTERVAL_BEFORE, /* [s1 <= e1] < [s2 <= e2] */ + INTERVAL_AFTER, /* [s2 <= e2] < [s1 <= e1] */ + INTERVAL_BEFORE_WITH_OVELAP, /* s1 < [s2 <= e1 <= e2] */ + INTERVAL_AFTER_WITH_OVERLAP, /* [s2 <= s1 <= e2] < e1 */ + INTERVAL_INTO, /* s2 <= [s1 <= e1] <= e2 */ + INTERVAL_EXTO /* s1 < [s2 <= e2] < e1, (not sure about the name) */ +}; + +int interval_count(interval_t *head) +{ + interval_t *cur = head; + int count = 0; + + while (cur) { + cur = cur->next; + count++; + } + + return count; +} + +int interval_append(interval_t **head_ref, int start, int end) +{ + if (start > end) { + LOG_ERROR("interval error: start > end"); + return ERROR_FAIL; + } + + interval_t *new_interval = malloc(sizeof(interval_t)); + if (!new_interval) + return ERROR_FAIL; + + new_interval->start = start; + new_interval->end = end; + new_interval->next = NULL; + + if (*head_ref) { + interval_t *last = *head_ref; + + while (last->next) + last = last->next; + + last->next = new_interval; + } else { + /* first insertion */ + *head_ref = new_interval; + } + + return ERROR_OK; +} + +static enum interval_comparison_result interval_compare(interval_t *interval1, interval_t *interval2) +{ + if ((interval1->start == interval2->start) && (interval1->end == interval2->end)) + return INTERVAL_EQUAL; + + if (interval1->start < interval2->start) { + if (interval1->end < interval2->start) + return INTERVAL_BEFORE; + else if (interval1->end > interval2->end) + return INTERVAL_EXTO; + return INTERVAL_BEFORE_WITH_OVELAP; + } else if (interval1->start <= interval2->end) { + if (interval1->end <= interval2->end) + return INTERVAL_INTO; + else + return INTERVAL_AFTER_WITH_OVERLAP; + } + + return INTERVAL_AFTER; +} + +int interval_delete(interval_t **head_ref, int del_start, int del_end) +{ + interval_t *cur = *head_ref, *prev = NULL, *tmp; + interval_t del_interval = {del_start, del_end, NULL}; /* the interval to delete */ + + while (cur) { + switch (interval_compare(cur, &del_interval)) { + case INTERVAL_BEFORE: + case INTERVAL_AFTER: + /* nothing to do */ + break; + case INTERVAL_INTO: + case INTERVAL_EQUAL: + cur->start = cur->end + 1; /* just a hint to remove the interval */ + break; + case INTERVAL_BEFORE_WITH_OVELAP: + cur->end = del_start - 1; /* check later the interval validity */ + break; + case INTERVAL_AFTER_WITH_OVERLAP: + cur->start = del_end + 1; /* check later the interval validity */ + break; + case INTERVAL_EXTO: + /* split cur */ + tmp = malloc(sizeof(interval_t)); + if (!tmp) + return ERROR_FAIL; + tmp->start = del_end + 1; + tmp->end = cur->end; + tmp->next = cur->next; + cur->end = del_start - 1; + cur->next = tmp; + /* jump to next (tmp) */ + cur = cur->next; + break; + } + + /* check the cur interval validity */ + if (cur->start > cur->end) { + /* invalid, remove it */ + if (prev == NULL) { + /* cur is the first element in the list */ + *head_ref = cur->next; + free(cur); + cur = *head_ref; + } else { + prev->next = cur->next; + free(cur); + cur = prev->next; + } + } else if (cur) { + /* valid, normal execution */ + prev = cur; + cur = cur->next; + } + } + + return ERROR_OK; +} + +/* seek for the first occurrences of interval1 and 2 into the list defined by + * head_ref and swap them */ +static int interval_swap(interval_t **head_ref, interval_t *interval1, interval_t *interval2) +{ + assert(interval1 && interval2); + + if (interval1 == interval2) /* nothing to do */ + return ERROR_OK; + + interval_t *tmp = *head_ref; + interval_t *prev1 = NULL; + interval_t *prev2 = NULL; + + /* search for prev1 */ + if (*head_ref != interval1) { + while (tmp && !prev1) { + if (tmp->next == interval1) + prev1 = tmp; + tmp = tmp->next; + } + if (!prev1) + return ERROR_FAIL; + } + + /* search for prev2 */ + tmp = *head_ref; + if (*head_ref != interval2) { + while (tmp && !prev2) { + if (tmp->next == interval2) + prev2 = tmp; + tmp = tmp->next; + } + if (!prev2) + return ERROR_FAIL; + } + + /* change prev relation chain */ + if (prev1) + prev1->next = interval2; + else /* interval1 is the first element, put interval2 instead */ + *head_ref = interval2; + + if (prev2) + prev2->next = interval1; + else /* interval2 is the first element, put interval1 instead */ + *head_ref = interval1; + + /* change next relation chain */ + tmp = interval1->next; + interval1->next = interval2->next; + interval2->next = tmp; + + return ERROR_OK; +} + +int interval_reorder(interval_t **head_ref) +{ + if (interval_count(*head_ref) < 2) + return ERROR_OK; + + interval_t *cur, *next; + bool touched; + + /* bubble like algorithm */ + do { + touched = false; + cur = *head_ref; + + while (cur && cur->next) { + next = cur->next; + + switch (interval_compare(cur, next)) { + case INTERVAL_BEFORE: + /* normal order, nothing to do */ + break; + case INTERVAL_BEFORE_WITH_OVELAP: + /* merge into cur and remove next */ + cur->end = next->end; + cur->next = next->next; + free(next); + touched = true; + break; + case INTERVAL_EQUAL: + case INTERVAL_EXTO: + /* next is into cur, remove next */ + cur->next = next->next; + free(next); + touched = true; + break; + case INTERVAL_AFTER_WITH_OVERLAP: + /* merge into cur and remove next */ + cur->start = next->start; + cur->next = next->next; + free(next); + touched = true; + break; + case INTERVAL_INTO: + case INTERVAL_AFTER: + interval_swap(head_ref, cur, next); + touched = true; + break; + } + + if (cur) + cur = cur->next; + } + } while (touched); + + return ERROR_OK; +} + +void interval_print(interval_t *interval) +{ + if (interval->start == interval->end) + printf("[%d]", interval->start); + else + printf("[%d, %d]", interval->start, interval->end); +} + +void interval_print_all(interval_t *head) +{ + interval_t *cur = head; + + while (cur) { + interval_print(cur); + cur = cur->next; + } + + printf("\n"); +} diff --git a/src/helper/interval.h b/src/helper/interval.h new file mode 100644 index 0000000..a1a86ec --- /dev/null +++ b/src/helper/interval.h @@ -0,0 +1,36 @@ +/*************************************************************************** + * * + * Copyright (C) 2020 by Tarek Bochkati for STMicroelectronics * + * [email protected] * + * * + * 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, see <http://www.gnu.org/licenses/>. * + ***************************************************************************/ + +#ifndef OPENOCD_HELPER_INTERVAL_H +#define OPENOCD_HELPER_INTERVAL_H + +typedef struct interval { + int start, end; + struct interval *next; +} interval_t; + +int interval_count(interval_t *head); +int interval_append(interval_t **head_ref, int start, int end); +int interval_delete(interval_t **head_ref, int start, int end); +int interval_reorder(interval_t **head_ref); +void interval_print(interval_t *interval); +void interval_print_all(interval_t *head); + + +#endif /* OPENOCD_HELPER_INTERVAL_H */ -- _______________________________________________ OpenOCD-devel mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/openocd-devel
