30c6e34cb480e915b47d93a7bf7c3b6b2f893014
[openocd.git] / src / rtos / FreeRTOS.c
1 /***************************************************************************
2 * Copyright (C) 2011 by Broadcom Corporation *
3 * Evan Hunter - ehunter@broadcom.com *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program. If not, see <http://www.gnu.org/licenses/>. *
17 ***************************************************************************/
18
19 #ifdef HAVE_CONFIG_H
20 #include "config.h"
21 #endif
22
23 #include <helper/time_support.h>
24 #include <jtag/jtag.h>
25 #include "target/target.h"
26 #include "target/target_type.h"
27 #include "rtos.h"
28 #include "helper/log.h"
29 #include "helper/types.h"
30 #include "rtos_standard_stackings.h"
31 #include "target/armv7m.h"
32 #include "target/cortex_m.h"
33
34
35
36 #define FREERTOS_MAX_PRIORITIES 63
37
38 #define FreeRTOS_STRUCT(int_type, ptr_type, list_prev_offset)
39
40 /* FIXME: none of the _width parameters are actually observed properly!
41 * you WILL need to edit more if you actually attempt to target a 8/16/64
42 * bit target!
43 */
44
45 struct FreeRTOS_params {
46 const char *target_name;
47 const unsigned char thread_count_width;
48 const unsigned char pointer_width;
49 const unsigned char list_next_offset;
50 const unsigned char list_width;
51 const unsigned char list_elem_next_offset;
52 const unsigned char list_elem_content_offset;
53 const unsigned char thread_stack_offset;
54 const unsigned char thread_name_offset;
55 const struct rtos_register_stacking *stacking_info_cm3;
56 const struct rtos_register_stacking *stacking_info_cm4f;
57 const struct rtos_register_stacking *stacking_info_cm4f_fpu;
58 };
59
60 static const struct FreeRTOS_params FreeRTOS_params_list[] = {
61 {
62 "cortex_m", /* target_name */
63 4, /* thread_count_width; */
64 4, /* pointer_width; */
65 16, /* list_next_offset; */
66 20, /* list_width; */
67 8, /* list_elem_next_offset; */
68 12, /* list_elem_content_offset */
69 0, /* thread_stack_offset; */
70 52, /* thread_name_offset; */
71 &rtos_standard_Cortex_M3_stacking, /* stacking_info */
72 &rtos_standard_Cortex_M4F_stacking,
73 &rtos_standard_Cortex_M4F_FPU_stacking,
74 },
75 {
76 "hla_target", /* target_name */
77 4, /* thread_count_width; */
78 4, /* pointer_width; */
79 16, /* list_next_offset; */
80 20, /* list_width; */
81 8, /* list_elem_next_offset; */
82 12, /* list_elem_content_offset */
83 0, /* thread_stack_offset; */
84 52, /* thread_name_offset; */
85 &rtos_standard_Cortex_M3_stacking, /* stacking_info */
86 &rtos_standard_Cortex_M4F_stacking,
87 &rtos_standard_Cortex_M4F_FPU_stacking,
88 },
89 {
90 "nds32_v3", /* target_name */
91 4, /* thread_count_width; */
92 4, /* pointer_width; */
93 16, /* list_next_offset; */
94 20, /* list_width; */
95 8, /* list_elem_next_offset; */
96 12, /* list_elem_content_offset */
97 0, /* thread_stack_offset; */
98 52, /* thread_name_offset; */
99 &rtos_standard_NDS32_N1068_stacking, /* stacking_info */
100 &rtos_standard_Cortex_M4F_stacking,
101 &rtos_standard_Cortex_M4F_FPU_stacking,
102 },
103 };
104
105 static bool FreeRTOS_detect_rtos(struct target *target);
106 static int FreeRTOS_create(struct target *target);
107 static int FreeRTOS_update_threads(struct rtos *rtos);
108 static int FreeRTOS_get_thread_reg_list(struct rtos *rtos, int64_t thread_id,
109 struct rtos_reg **reg_list, int *num_regs);
110 static int FreeRTOS_get_symbol_list_to_lookup(struct symbol_table_elem *symbol_list[]);
111
112 struct rtos_type FreeRTOS_rtos = {
113 .name = "FreeRTOS",
114
115 .detect_rtos = FreeRTOS_detect_rtos,
116 .create = FreeRTOS_create,
117 .update_threads = FreeRTOS_update_threads,
118 .get_thread_reg_list = FreeRTOS_get_thread_reg_list,
119 .get_symbol_list_to_lookup = FreeRTOS_get_symbol_list_to_lookup,
120 };
121
122 enum FreeRTOS_symbol_values {
123 FreeRTOS_VAL_pxCurrentTCB = 0,
124 FreeRTOS_VAL_pxReadyTasksLists = 1,
125 FreeRTOS_VAL_xDelayedTaskList1 = 2,
126 FreeRTOS_VAL_xDelayedTaskList2 = 3,
127 FreeRTOS_VAL_pxDelayedTaskList = 4,
128 FreeRTOS_VAL_pxOverflowDelayedTaskList = 5,
129 FreeRTOS_VAL_xPendingReadyList = 6,
130 FreeRTOS_VAL_xTasksWaitingTermination = 7,
131 FreeRTOS_VAL_xSuspendedTaskList = 8,
132 FreeRTOS_VAL_uxCurrentNumberOfTasks = 9,
133 FreeRTOS_VAL_uxTopUsedPriority = 10,
134 };
135
136 struct symbols {
137 const char *name;
138 bool optional;
139 };
140
141 static const struct symbols FreeRTOS_symbol_list[] = {
142 { "pxCurrentTCB", false },
143 { "pxReadyTasksLists", false },
144 { "xDelayedTaskList1", false },
145 { "xDelayedTaskList2", false },
146 { "pxDelayedTaskList", false },
147 { "pxOverflowDelayedTaskList", false },
148 { "xPendingReadyList", false },
149 { "xTasksWaitingTermination", true }, /* Only if INCLUDE_vTaskDelete */
150 { "xSuspendedTaskList", true }, /* Only if INCLUDE_vTaskSuspend */
151 { "uxCurrentNumberOfTasks", false },
152 { "uxTopUsedPriority", true }, /* Unavailable since v7.5.3 */
153 { NULL, false }
154 };
155
156 /* TODO: */
157 /* this is not safe for little endian yet */
158 /* may be problems reading if sizes are not 32 bit long integers. */
159 /* test mallocs for failure */
160
161 static int FreeRTOS_update_threads(struct rtos *rtos)
162 {
163 int retval;
164 unsigned int tasks_found = 0;
165 const struct FreeRTOS_params *param;
166
167 if (rtos->rtos_specific_params == NULL)
168 return -1;
169
170 param = (const struct FreeRTOS_params *) rtos->rtos_specific_params;
171
172 if (rtos->symbols == NULL) {
173 LOG_ERROR("No symbols for FreeRTOS");
174 return -3;
175 }
176
177 if (rtos->symbols[FreeRTOS_VAL_uxCurrentNumberOfTasks].address == 0) {
178 LOG_ERROR("Don't have the number of threads in FreeRTOS");
179 return -2;
180 }
181
182 uint32_t thread_list_size = 0;
183 retval = target_read_u32(rtos->target,
184 rtos->symbols[FreeRTOS_VAL_uxCurrentNumberOfTasks].address,
185 &thread_list_size);
186 LOG_DEBUG("FreeRTOS: Read uxCurrentNumberOfTasks at 0x%" PRIx64 ", value %" PRIu32,
187 rtos->symbols[FreeRTOS_VAL_uxCurrentNumberOfTasks].address,
188 thread_list_size);
189
190 if (retval != ERROR_OK) {
191 LOG_ERROR("Could not read FreeRTOS thread count from target");
192 return retval;
193 }
194
195 /* wipe out previous thread details if any */
196 rtos_free_threadlist(rtos);
197
198 /* read the current thread */
199 uint32_t pointer_casts_are_bad;
200 retval = target_read_u32(rtos->target,
201 rtos->symbols[FreeRTOS_VAL_pxCurrentTCB].address,
202 &pointer_casts_are_bad);
203 if (retval != ERROR_OK) {
204 LOG_ERROR("Error reading current thread in FreeRTOS thread list");
205 return retval;
206 }
207 rtos->current_thread = pointer_casts_are_bad;
208 LOG_DEBUG("FreeRTOS: Read pxCurrentTCB at 0x%" PRIx64 ", value 0x%" PRIx64,
209 rtos->symbols[FreeRTOS_VAL_pxCurrentTCB].address,
210 rtos->current_thread);
211
212 if ((thread_list_size == 0) || (rtos->current_thread == 0)) {
213 /* Either : No RTOS threads - there is always at least the current execution though */
214 /* OR : No current thread - all threads suspended - show the current execution
215 * of idling */
216 char tmp_str[] = "Current Execution";
217 thread_list_size++;
218 tasks_found++;
219 rtos->thread_details = malloc(
220 sizeof(struct thread_detail) * thread_list_size);
221 if (!rtos->thread_details) {
222 LOG_ERROR("Error allocating memory for %d threads", thread_list_size);
223 return ERROR_FAIL;
224 }
225 rtos->thread_details->threadid = 1;
226 rtos->thread_details->exists = true;
227 rtos->thread_details->extra_info_str = NULL;
228 rtos->thread_details->thread_name_str = malloc(sizeof(tmp_str));
229 strcpy(rtos->thread_details->thread_name_str, tmp_str);
230
231 if (thread_list_size == 1) {
232 rtos->thread_count = 1;
233 return ERROR_OK;
234 }
235 } else {
236 /* create space for new thread details */
237 rtos->thread_details = malloc(
238 sizeof(struct thread_detail) * thread_list_size);
239 if (!rtos->thread_details) {
240 LOG_ERROR("Error allocating memory for %d threads", thread_list_size);
241 return ERROR_FAIL;
242 }
243 }
244
245 /* Find out how many lists are needed to be read from pxReadyTasksLists, */
246 if (rtos->symbols[FreeRTOS_VAL_uxTopUsedPriority].address == 0) {
247 LOG_ERROR("FreeRTOS: uxTopUsedPriority is not defined, consult the OpenOCD manual for a work-around");
248 return ERROR_FAIL;
249 }
250 uint32_t top_used_priority = 0;
251 retval = target_read_u32(rtos->target,
252 rtos->symbols[FreeRTOS_VAL_uxTopUsedPriority].address,
253 &top_used_priority);
254 if (retval != ERROR_OK)
255 return retval;
256 LOG_DEBUG("FreeRTOS: Read uxTopUsedPriority at 0x%" PRIx64 ", value %" PRIu32,
257 rtos->symbols[FreeRTOS_VAL_uxTopUsedPriority].address,
258 top_used_priority);
259 if (top_used_priority > FREERTOS_MAX_PRIORITIES) {
260 LOG_ERROR("FreeRTOS top used priority is unreasonably big, not proceeding: %" PRIu32,
261 top_used_priority);
262 return ERROR_FAIL;
263 }
264
265 /* uxTopUsedPriority was defined as configMAX_PRIORITIES - 1
266 * in old FreeRTOS versions (before V7.5.3)
267 * Use contrib/rtos-helpers/FreeRTOS-openocd.c to get compatible symbol
268 * in newer FreeRTOS versions.
269 * Here we restore the original configMAX_PRIORITIES value */
270 unsigned int config_max_priorities = top_used_priority + 1;
271
272 symbol_address_t *list_of_lists =
273 malloc(sizeof(symbol_address_t) * (config_max_priorities + 5));
274 if (!list_of_lists) {
275 LOG_ERROR("Error allocating memory for %u priorities", config_max_priorities);
276 return ERROR_FAIL;
277 }
278
279 unsigned int num_lists;
280 for (num_lists = 0; num_lists < config_max_priorities; num_lists++)
281 list_of_lists[num_lists] = rtos->symbols[FreeRTOS_VAL_pxReadyTasksLists].address +
282 num_lists * param->list_width;
283
284 list_of_lists[num_lists++] = rtos->symbols[FreeRTOS_VAL_xDelayedTaskList1].address;
285 list_of_lists[num_lists++] = rtos->symbols[FreeRTOS_VAL_xDelayedTaskList2].address;
286 list_of_lists[num_lists++] = rtos->symbols[FreeRTOS_VAL_xPendingReadyList].address;
287 list_of_lists[num_lists++] = rtos->symbols[FreeRTOS_VAL_xSuspendedTaskList].address;
288 list_of_lists[num_lists++] = rtos->symbols[FreeRTOS_VAL_xTasksWaitingTermination].address;
289
290 for (unsigned int i = 0; i < num_lists; i++) {
291 if (list_of_lists[i] == 0)
292 continue;
293
294 /* Read the number of threads in this list */
295 uint32_t list_thread_count = 0;
296 retval = target_read_u32(rtos->target,
297 list_of_lists[i],
298 &list_thread_count);
299 if (retval != ERROR_OK) {
300 LOG_ERROR("Error reading number of threads in FreeRTOS thread list");
301 free(list_of_lists);
302 return retval;
303 }
304 LOG_DEBUG("FreeRTOS: Read thread count for list %u at 0x%" PRIx64 ", value %" PRIu32,
305 i, list_of_lists[i], list_thread_count);
306
307 if (list_thread_count == 0)
308 continue;
309
310 /* Read the location of first list item */
311 uint32_t prev_list_elem_ptr = -1;
312 uint32_t list_elem_ptr = 0;
313 retval = target_read_u32(rtos->target,
314 list_of_lists[i] + param->list_next_offset,
315 &list_elem_ptr);
316 if (retval != ERROR_OK) {
317 LOG_ERROR("Error reading first thread item location in FreeRTOS thread list");
318 free(list_of_lists);
319 return retval;
320 }
321 LOG_DEBUG("FreeRTOS: Read first item for list %u at 0x%" PRIx64 ", value 0x%" PRIx32,
322 i, list_of_lists[i] + param->list_next_offset, list_elem_ptr);
323
324 while ((list_thread_count > 0) && (list_elem_ptr != 0) &&
325 (list_elem_ptr != prev_list_elem_ptr) &&
326 (tasks_found < thread_list_size)) {
327 /* Get the location of the thread structure. */
328 rtos->thread_details[tasks_found].threadid = 0;
329 retval = target_read_u32(rtos->target,
330 list_elem_ptr + param->list_elem_content_offset,
331 &pointer_casts_are_bad);
332 if (retval != ERROR_OK) {
333 LOG_ERROR("Error reading thread list item object in FreeRTOS thread list");
334 free(list_of_lists);
335 return retval;
336 }
337 rtos->thread_details[tasks_found].threadid = pointer_casts_are_bad;
338 LOG_DEBUG("FreeRTOS: Read Thread ID at 0x%" PRIx32 ", value 0x%" PRIx64,
339 list_elem_ptr + param->list_elem_content_offset,
340 rtos->thread_details[tasks_found].threadid);
341
342 /* get thread name */
343
344 #define FREERTOS_THREAD_NAME_STR_SIZE (200)
345 char tmp_str[FREERTOS_THREAD_NAME_STR_SIZE];
346
347 /* Read the thread name */
348 retval = target_read_buffer(rtos->target,
349 rtos->thread_details[tasks_found].threadid + param->thread_name_offset,
350 FREERTOS_THREAD_NAME_STR_SIZE,
351 (uint8_t *)&tmp_str);
352 if (retval != ERROR_OK) {
353 LOG_ERROR("Error reading first thread item location in FreeRTOS thread list");
354 free(list_of_lists);
355 return retval;
356 }
357 tmp_str[FREERTOS_THREAD_NAME_STR_SIZE-1] = '\x00';
358 LOG_DEBUG("FreeRTOS: Read Thread Name at 0x%" PRIx64 ", value '%s'",
359 rtos->thread_details[tasks_found].threadid + param->thread_name_offset,
360 tmp_str);
361
362 if (tmp_str[0] == '\x00')
363 strcpy(tmp_str, "No Name");
364
365 rtos->thread_details[tasks_found].thread_name_str =
366 malloc(strlen(tmp_str)+1);
367 strcpy(rtos->thread_details[tasks_found].thread_name_str, tmp_str);
368 rtos->thread_details[tasks_found].exists = true;
369
370 if (rtos->thread_details[tasks_found].threadid == rtos->current_thread) {
371 char running_str[] = "State: Running";
372 rtos->thread_details[tasks_found].extra_info_str = malloc(
373 sizeof(running_str));
374 strcpy(rtos->thread_details[tasks_found].extra_info_str,
375 running_str);
376 } else
377 rtos->thread_details[tasks_found].extra_info_str = NULL;
378
379 tasks_found++;
380 list_thread_count--;
381
382 prev_list_elem_ptr = list_elem_ptr;
383 list_elem_ptr = 0;
384 retval = target_read_u32(rtos->target,
385 prev_list_elem_ptr + param->list_elem_next_offset,
386 &list_elem_ptr);
387 if (retval != ERROR_OK) {
388 LOG_ERROR("Error reading next thread item location in FreeRTOS thread list");
389 free(list_of_lists);
390 return retval;
391 }
392 LOG_DEBUG("FreeRTOS: Read next thread location at 0x%" PRIx32 ", value 0x%" PRIx32,
393 prev_list_elem_ptr + param->list_elem_next_offset,
394 list_elem_ptr);
395 }
396 }
397
398 free(list_of_lists);
399 rtos->thread_count = tasks_found;
400 return 0;
401 }
402
403 static int FreeRTOS_get_thread_reg_list(struct rtos *rtos, int64_t thread_id,
404 struct rtos_reg **reg_list, int *num_regs)
405 {
406 int retval;
407 const struct FreeRTOS_params *param;
408 int64_t stack_ptr = 0;
409
410 if (rtos == NULL)
411 return -1;
412
413 if (thread_id == 0)
414 return -2;
415
416 if (rtos->rtos_specific_params == NULL)
417 return -1;
418
419 param = (const struct FreeRTOS_params *) rtos->rtos_specific_params;
420
421 /* Read the stack pointer */
422 uint32_t pointer_casts_are_bad;
423 retval = target_read_u32(rtos->target,
424 thread_id + param->thread_stack_offset,
425 &pointer_casts_are_bad);
426 if (retval != ERROR_OK) {
427 LOG_ERROR("Error reading stack frame from FreeRTOS thread");
428 return retval;
429 }
430 stack_ptr = pointer_casts_are_bad;
431 LOG_DEBUG("FreeRTOS: Read stack pointer at 0x%" PRIx64 ", value 0x%" PRIx64,
432 thread_id + param->thread_stack_offset,
433 stack_ptr);
434
435 /* Check for armv7m with *enabled* FPU, i.e. a Cortex-M4F */
436 int cm4_fpu_enabled = 0;
437 struct armv7m_common *armv7m_target = target_to_armv7m(rtos->target);
438 if (is_armv7m(armv7m_target)) {
439 if (armv7m_target->fp_feature == FPV4_SP) {
440 /* Found ARM v7m target which includes a FPU */
441 uint32_t cpacr;
442
443 retval = target_read_u32(rtos->target, FPU_CPACR, &cpacr);
444 if (retval != ERROR_OK) {
445 LOG_ERROR("Could not read CPACR register to check FPU state");
446 return -1;
447 }
448
449 /* Check if CP10 and CP11 are set to full access. */
450 if (cpacr & 0x00F00000) {
451 /* Found target with enabled FPU */
452 cm4_fpu_enabled = 1;
453 }
454 }
455 }
456
457 if (cm4_fpu_enabled == 1) {
458 /* Read the LR to decide between stacking with or without FPU */
459 uint32_t LR_svc = 0;
460 retval = target_read_u32(rtos->target,
461 stack_ptr + 0x20,
462 &LR_svc);
463 if (retval != ERROR_OK) {
464 LOG_OUTPUT("Error reading stack frame from FreeRTOS thread");
465 return retval;
466 }
467 if ((LR_svc & 0x10) == 0)
468 return rtos_generic_stack_read(rtos->target, param->stacking_info_cm4f_fpu, stack_ptr, reg_list, num_regs);
469 else
470 return rtos_generic_stack_read(rtos->target, param->stacking_info_cm4f, stack_ptr, reg_list, num_regs);
471 } else
472 return rtos_generic_stack_read(rtos->target, param->stacking_info_cm3, stack_ptr, reg_list, num_regs);
473 }
474
475 static int FreeRTOS_get_symbol_list_to_lookup(struct symbol_table_elem *symbol_list[])
476 {
477 unsigned int i;
478 *symbol_list = calloc(
479 ARRAY_SIZE(FreeRTOS_symbol_list), sizeof(struct symbol_table_elem));
480
481 for (i = 0; i < ARRAY_SIZE(FreeRTOS_symbol_list); i++) {
482 (*symbol_list)[i].symbol_name = FreeRTOS_symbol_list[i].name;
483 (*symbol_list)[i].optional = FreeRTOS_symbol_list[i].optional;
484 }
485
486 return 0;
487 }
488
489 #if 0
490
491 static int FreeRTOS_set_current_thread(struct rtos *rtos, threadid_t thread_id)
492 {
493 return 0;
494 }
495
496 static int FreeRTOS_get_thread_ascii_info(struct rtos *rtos, threadid_t thread_id, char **info)
497 {
498 int retval;
499 const struct FreeRTOS_params *param;
500
501 if (rtos == NULL)
502 return -1;
503
504 if (thread_id == 0)
505 return -2;
506
507 if (rtos->rtos_specific_params == NULL)
508 return -3;
509
510 param = (const struct FreeRTOS_params *) rtos->rtos_specific_params;
511
512 #define FREERTOS_THREAD_NAME_STR_SIZE (200)
513 char tmp_str[FREERTOS_THREAD_NAME_STR_SIZE];
514
515 /* Read the thread name */
516 retval = target_read_buffer(rtos->target,
517 thread_id + param->thread_name_offset,
518 FREERTOS_THREAD_NAME_STR_SIZE,
519 (uint8_t *)&tmp_str);
520 if (retval != ERROR_OK) {
521 LOG_ERROR("Error reading first thread item location in FreeRTOS thread list");
522 return retval;
523 }
524 tmp_str[FREERTOS_THREAD_NAME_STR_SIZE-1] = '\x00';
525
526 if (tmp_str[0] == '\x00')
527 strcpy(tmp_str, "No Name");
528
529 *info = malloc(strlen(tmp_str)+1);
530 strcpy(*info, tmp_str);
531 return 0;
532 }
533
534 #endif
535
536 static bool FreeRTOS_detect_rtos(struct target *target)
537 {
538 if ((target->rtos->symbols != NULL) &&
539 (target->rtos->symbols[FreeRTOS_VAL_pxReadyTasksLists].address != 0)) {
540 /* looks like FreeRTOS */
541 return true;
542 }
543 return false;
544 }
545
546 static int FreeRTOS_create(struct target *target)
547 {
548 for (unsigned int i = 0; i < ARRAY_SIZE(FreeRTOS_params_list); i++)
549 if (strcmp(FreeRTOS_params_list[i].target_name, target->type->name) == 0) {
550 target->rtos->rtos_specific_params = (void *)&FreeRTOS_params_list[i];
551 return 0;
552 }
553
554 LOG_ERROR("Could not find target in FreeRTOS compatibility list");
555 return -1;
556 }

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)