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

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)