Взято із: "Getting printf Output from Target to Debugger". |
Найпростіший варіант -- мати "Debug" i "Release" версію програми, і в останній SemiHosting повністю викидати. В принципі, враховуючи, що даний інтерфейс -- чисто відладочний, це, певне, найкращий варіант -- не варто залишати відладочний код в приладі. Але він має й свої мінуси -- зростає громіздкість коду, більше способів його зламати (див. весь список заперечень проти використання умовної компіляції), все таке.
Щоб знати, що ще можна зробити, слід розібратися, а чого, власне, програма зупиняється на використанні SemiHosting. Потім -- придумати, як із тим боротися. Наперед скажу -- на жаль, повністю вирішити проблему не вдасться. Все рівно буде потрібно вимкнути-ввімкнути живлення мікроконтролера (демоплати), перш ніж код зможе працювати автономно.
Update 12.12.2017: Описано особливості ARM Cortex M0 та M0+.
Заради лаконічності обмежуся лише ARM Cortex M -- для "великих" ARM-ів є нюанси. Що таке SemiHosting? Це інтерфейс взаємодії відладчика і мікроконтролера. Щоб передати команду відладчику використовується спеціальна команда, BKPT -- breakpoint, точка зупинки, після якої стоїть однобайтовий аргумент (як його позначають в документації, imm8 -- immediate-8-bit). Мікроконтролер аргумент ігнорує повністю, він потрібен лише зневаджувачу, але ARM рекомендує для SemiHosting використовувати лише значення 0xAB. Таким чином, якщо в коді трапиться команда:
Update 12.12.2017: Описано особливості ARM Cortex M0 та M0+.
Заради лаконічності обмежуся лише ARM Cortex M -- для "великих" ARM-ів є нюанси. Що таке SemiHosting? Це інтерфейс взаємодії відладчика і мікроконтролера. Щоб передати команду відладчику використовується спеціальна команда, BKPT -- breakpoint, точка зупинки, після якої стоїть однобайтовий аргумент (як його позначають в документації, imm8 -- immediate-8-bit). Мікроконтролер аргумент ігнорує повністю, він потрібен лише зневаджувачу, але ARM рекомендує для SemiHosting використовувати лише значення 0xAB. Таким чином, якщо в коді трапиться команда:
bkpt 0xAB
процесор зупиняється на ній до тих пір, поки про неї не "потурбується" дебаггер або не відбудеться виключення HardFault, якщо дебагера в системі немає. (Детальніше див. далі). Це, власне, і є причиною зависання -- коли програма працює без дебаггера, на першій же bkpt стається HardFault, а його обробник, щоб контролер чого не начудив, перебуваючи в некоректному стані, просто входить в нескінчений цикл. Див., наприклад, startup/startup_stm32f303xc.s, згенерований STM32CubeMX:
Default_Handler:
Infinite_Loop:
b Infinite_Loop
.size Default_Handler, .-Default_Handler
................................
.weak HardFault_Handler
.thumb_set HardFault_Handler,Default_Handler
Зауважте, що в ролі HardFault_Handler використано Default_Handler, який зациклюється, але при тому, сам HardFault_Handler оголошено як "слабкий", weak, символ. Це сильно допоможе нам в майбутньому.
Вихід, відповідно (окрім просто не використовувати команду bkpt), так написати HardFault_Handler, щоб він її пропускав.
Як це зробити? Спочатку потрібно знати, як відбувається обробка виключень в ARM Cortex M. В подробиці тут не вдаватимуся (інакше пост, як і більшість інших, мине всілякі розумні межі розміру), рекомендую почитати відповідні глави (7, 8, 12) чудової книжки Joseph Yiu, "The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors", 3rd edition, 2014. (Легко знаходиться в Інтернеті).
Якщо коротко, коли стається виключення або переривання, перш ніж перейти до відповідного обробника, процесор повинен зберегти достатньо багато свого стану, щоб змогти продовжити виконання після повернення із обробника. При чому, так, щоб перерваний код не зауважив, що його переривали. Стрибнувши на інший рівень абстракції, ARM ABI (application binary interface) вказує, які регістри функція можуть змінювати вільно, а які повинна залишити незмінними, коли завершиться (а значить, зберегти їх і відновити потім, якщо вони їй потрібні) -- ці домовленості називають угодами про виклики, calling conventions. Так от, наші ARM-и оптимізовані під написання коду на С -- при виникненні виключення автоматично зберігаються, крім адреси повернення та регістра прапорців, ті регістри, які функція може змінювати вільно. Таким чином, в ролі обробника може бути використано звичайну функцію C.
Після входу в обробник, стек виглядає так:
Після входу в обробник, стек виглядає так:
Тепер потрібно знати деякі подробиці. Процесор підтримує два стека, на які є відповідні вказівники, Main Stack Pointer, (MSP) та Process Stack Pointer, (PSP). Вони призначені розділити стеки привілейованого та непривілейованого режимів, якщо такі використовуються. Переривання завжди використовує привілейований стек, однак, стан зберігається у стеку, активному на момент виникнення переривання. (За подробицями див. книгу Yiu). Регістр LR, який використовується для повернення із підпрограм, при вході в обробник містить спеціальне значення, EXC_RETURN -- 27 одиничок у старших бітах, а 5 молодших бітів використовуються для службової інформації (див. таблицю 8.1 книги Yiu, 278 сторінка у 3-му виданні), зокрема, біт 2 вказує, з котрого стеку відновлювати стан при виході із переривання -- із PSP (1) чи MSP (0).
Адреса повернення в стеку (LR на малюнку вище) містить вказівник на команду, що викликала HardFault, що дає можливість перевірити, яка команда викликала HardFault -- пропускати всі джерела цього виключення буде нерозумним, якщо це не bkpt, справді краще зависнути (можливо, перед тим надрукувавши кадр стеку та іншу важливу для відладки інформацію).
Для симуляції команд SemiHosting нам також може знадобиться вміст регістрів R0 i R1. Справа в тому, що команда для SemiHosting передається в R0, все решта, потрібне команді, передається в пам'яті, вказівник на яку міститься в R1. Результат повертається в R0 (який може псуватися, навіть якщо дана команда нічого не повертає). (Див. також "The semihosting interface". Детальніше про SemiHosting, використані машинні команди та стандартний набір команд для відлачика див. офіційну документацію: "What is Semihosting?").
Отож, приступаємо до написання обробника. Хоча його можна писати й виключно на С, добиратися до кадру стеку так незручно -- ми будемо занадто залежні від його оптимізацій і взагалі, це все віддаватиме чорною магією. Тому частину пишемо на асемблері (inline-асемблера тут не допоможе). Якщо наш мікроконтролер на базі ARM Cortex M3 чи M4, можемо зробити так:
Де HardFault_Handler_C -- функція С, із наступним прототипом, яка має бути визначена у котромусь із файлів проекту:.global HardFault_Handler
.global HardFault_Handler_C
.type HardFault_Handler, %function
HardFault_Handler:
tst LR, #4
ite eq
mrseq R0, MSP /* stacking was using MSP */
mrsne R0, PSP /* stacking was using PSP */
mov R1, LR /* second parameter */
ldr R2,=HardFault_Handler_C
bx R2
void HardFault_Handler_C(exception_stack_frame_t* frame, unsigned int lr_value);Про асемблер ARM Cortex M (та й в цілому -- їх архітектуру) напишу колись окремо. Зараз лише коротко:
- В асемблерному коді різниці між великими та малими літерами немає. Можна писати, скажімо, і BKPT i bkpt. Також, асемблер arm розуміє коментарі в стилі С.
- #4 -- безпосереднє значення, це просто 4 = 0b0100 -- встановлено біт номер 2.
- Тому "tst LR, #4" перевіряє біт 2 в LR, три наступних команди -- то така хитра конструкція, if без розгалуження (і branch misprediction, відповідно) -- mrseq виконається, якщо результат tst рівний 0, mrsne -- в іншому випадку.
- Згідно вже згадуваного ARM ABI, перший аргумент функції має йти в R0, другий в R1. Тобто, кадр стеку після цих команд потрапить в аргумент frame (тип якого розберемо нижче), а вміст LR -- в lr_value .
ARM Cortex M0 та M0+ помітно
примітивніші (звичайно, не просто так -- заради економії транзисторів та
електроенергії, але, все ж), команд сімейства ite у них немає, tst не вміє працювати із безпосередніми значеннями, тому для них пишемо так:
Перш, ніж тестувати біти, завантажуємо 4 в R0, використовуємо явний умовний перехід -- beq, branch-if-equal, але, в цілому, код -- той же. Він навіть успішно виконуватимуться на ARM Cortex M3/M4, але не будемо передчасно песимізувати -- залишимо їм більш просунутий, написаний вище.HardFault_Handler:
movs R0, #4
mov R1, LR
tst R0, R1
beq stacking_used_MSP
mrs R0, PSP /* Stacking was using PSP */
b get_LR_and_branch
stacking_used_MSP:
MRS R0, MSP /* Stacking was using MSP */
get_LR_and_branch:
mov R1, LR
ldr R2,=HardFault_Handler_C
bx R2
Зізнаюся, не зміг дійти толку із умовною компіляцією в GNU as. Ні, з .ifdef/.else/.endif все зрозуміло, але вже фокус .elseif <way_to_check_if_smth_is_defined> не вдалося змусити працювати, не зважаючи на десятки проглянутих прикладів... Тому просто два різних файли, для M3/M4 i для M0/M0+.Можна переходити до реалізації HardFault_Handler_C(). З нею трішки складніше. Перша думка, яка приходить -- якщо відладчика немає, але нам трапилася bkpt, пропускаємо її. Інакше -- зависаємо, бо цей HardFault -- з іншої причини.
Однак, халяви немає. По перше, способу перевірити, чи є там, на іншому кінці, дебагер, немає -- можна лише перевірити, чи є процесор в режимі відладки (біт DEBUGEN регістра CoreDebug->DHCSR). По друге, цей режим зберігається між перевантаженнями мікропроцесора -- аж до вимкнення живлення! І нарешті, по третє, програматор, заливаючи прошивку, переводить мікроконтролер у режим відладки.
Тобто, виникає наступна ситуація. Ми ввімкнули демоплату, завантажили прошивку -- при цьому OpenOCD (наш програматор) встановив біт DEBUGEN в 1 і назад не повернув. Відповідно, згідно схеми поведінки апаратури зневадження ARM:
після цього виконання BKPT приведе до переходу в стан HALT та очікування на реакцію зневаджувача. Якої не буде, бо він не під'єднаний. Програма зависає, як і раніше, іншими словами -- навіть якщо обробник HardFault правильний. Але вимкнули живлення (нарешті зануливши DEBUGEN), ввімкнули, і вона працюватиме.
Хоча це не зручно, на жаль, після тривалого читання документації, так виглядає, нічого із тим зробити не можна. Взагалі, OpenOCD мав би той біт очищати, але він цього не вміє, судячи по всьому... (Див. його код, та й в документації нічого на ту тему не знайшов, хоча, не виключаю, що погано шукав)
Тобто, виникає наступна ситуація. Ми ввімкнули демоплату, завантажили прошивку -- при цьому OpenOCD (наш програматор) встановив біт DEBUGEN в 1 і назад не повернув. Відповідно, згідно схеми поведінки апаратури зневадження ARM:
після цього виконання BKPT приведе до переходу в стан HALT та очікування на реакцію зневаджувача. Якої не буде, бо він не під'єднаний. Програма зависає, як і раніше, іншими словами -- навіть якщо обробник HardFault правильний. Але вимкнули живлення (нарешті зануливши DEBUGEN), ввімкнули, і вона працюватиме.
Хоча це не зручно, на жаль, після тривалого читання документації, так виглядає, нічого із тим зробити не можна. Взагалі, OpenOCD мав би той біт очищати, але він цього не вміє, судячи по всьому... (Див. його код, та й в документації нічого на ту тему не знайшов, хоча, не виключаю, що погано шукав)
Але й ігнорувати всі HardFault -- не варіант. Тому, залишається одне -- дивитися на команду, яка породила його, і якщо це brkp 0xAB -- пропускати її. Опкод brkp -- 0xBE.Тому, по великому рахунку, ні перевірка вмісту Debug fault status register (SCB->DFSR), біт 1 якого, BKPT (відповідна бітмаска SCB_DFSR_BKPT_Msk), вказує, що подія відладки сталася через bkpt, ні Hard Fault Status Register (SCB->HFSR) біт номер 31, DEBUGEVT (SCB_HFSR_DEBUGEVT_Msk), якого стверджує, що HardFault стався через bkpt, особливого сенсу перевіряти в обробнику HardFault немає.
Те ж стосується перевірки біту 0, C_DEBUGEN (CoreDebug_DHCSR_C_DEBUGEN_Msk), із Debug Halting Control and Status Register (CoreDebug->DHCSR), щоб взнати, чи дозволено дебагер, перш ніж скористатися bkpt -- як описано вище, він може бути дозволеним, скажімо, внаслідок недавньої прошивки, а відладчика все рівно немає і ми зависнемо.
Див. також Joseph Yiu, "The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors", "ARM v7-M Architecture Reference Manual" та, чисто для сміху: [1], [2].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | typedef struct { uint32_t r0; uint32_t r1; uint32_t r2; uint32_t r3; uint32_t r12; uint32_t lr; uint32_t pc; uint32_t psr; #if (__CORTEX_M >= 0x04U) /* only for Cortex-M4 and above */ uint32_t s[16]; uint32_t fpscr; #endif } exception_stack_frame_t; void HardFault_Handler_C(exception_stack_frame_t* frame, unsigned int lr_value) { if( *(uint16_t*)(frame->pc) == 0xBEAB) { HAL_GPIO_WritePin(LD6_GPIO_Port, LD6_Pin, 1); frame->r0 = 0; // Simulate successful semihosting call // -- not always correct! frame->pc += 2; // Skip bkpt SCB->HFSR = SCB_HFSR_DEBUGEVT_Msk; // Sticky bit -- reset by writing 1 return; } //! Hang for all other hard faults while (1){} } |
exception_stack_frame_t -- структура, що відповідатиме кадру стеку. frame->pc -- вказівник на команду, що викликала HardFault. Якщо це 0xBEAB (нагадую, thumb-команди двобайтові), тобто bkpt 0xAB, пропускаємо: додаємо до pc двійку, безпосередньо в кадрі стеку -- при поверненні із переривання отримаємо саме це значення. Під кінець, очищаємо біт DEBUGEVT із HFSR. А біт цей такий хитрий, що очищається записом одинички.
Якщо б ви хотіли перевірити лише опкод команди -- 0xBE, слід було б писати так:*(uint8_t*)(frame->pc+1) == 0xBE
Чого +1? Бо little endianness.
Для ARM Cortex M0/M0+ буде ще одна відмінність -- біта SCB_HFSR_DEBUGEVT_Msk там просто немає, тому рядок 25 із коду вище слід викинути. Або скористатися умовною компіляцією -- тут вона від нас вже не втече!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void HardFault_Handler_C(exception_stack_frame_t* frame, unsigned int lr_value) { if( *(uint16_t*)(frame->pc) == 0xBEAB) { frame->r0 = 0; // Simulate successful semihosting call -- not always correct! frame->pc += 2; // Skip bkpt #if defined __TARGET_CPU_CORTEX_M3 || defined __TARGET_CPU_CORTEX_M4 || __TARGET_CPU_CORTEX_M4_FP SCB->HFSR = SCB_HFSR_DEBUGEVT_Msk; // Sticky bit -- reset by writing 1 #endif return; } //! Hang for all other hard faults while (1) { } } |
Інші наперед визначені символи (макроси) можуть бути такими: __TARGET_CPU_CORTEX_M0PLUS, __TARGET_CPU_CORTEX_M0_, __TARGET_CPU_CORTEX_M0. Глянути список можна за допомогою наступного виклику компілятора (вибравши вашу модель процесора):
arm-none-eabi-gcc -mcpu=cortex-m4 -dM -E -x c /dev/null
Для armcc:
armcc -mcpu=cortex-m4 --list_macros /dev/null
Під Windows "компілювати" /dev/null воно відмовиться, тоді слід просто передати порожній файл із розширенням .c.
Тепер програма працюватиме і без дебагера. Але з описаними вище обмовками -- після того, як до плати під'єднувався дебагер (навіть для прошивки), слід вимкнути-ввімкнути живлення.
Нову версію системних викликів, описаних в "Зовсім просто про semihosting" (архів semihosting_n_libc_support.zip), завантажу окремо -- вони якраз в процесі серйозної переробки, але поки необхідні зміни ви можете внести й самостійно.
Нову версію системних викликів, описаних в "Зовсім просто про semihosting" (архів semihosting_n_libc_support.zip), завантажу окремо -- вони якраз в процесі серйозної переробки, але поки необхідні зміни ви можете внести й самостійно.
Ремарка: як самостійно створити HardFault? Наприклад, для відладки коду вище. Найпростіший спосіб:
volatile int a = 0, b = 1, c;
SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk;
c = b/a;
Дозволяємо HardFault у відповідь на ділення на нуль та й ділимо на нього. Volatile є необхідним, щоб компілятор таки виконав це ділення, а то він, бачачи, що а) ділимо на нуль, б) не використовуємо результат, в) знаючи, що це невизначена поведінка, зробить із ним щось нехороше. Наприклад, взагалі викине.
Правда, знову ж таки, цей фокус не працює під ARM Cortex M0/M0+. Допоможе інший -- ці MCU не вміють виконувати не вирівняних звертань, при них генерується HardFault:
volatile int a = 0;
*(uint16_t*)(((int)&a)+1) = 1; // Unaligned access
Бонус -- трішки про функціонування semihosting
Реалізація виводу через SemiHosting, описана в моїх старіших постах, взята із компоненти CoIDE, і доволі примітивна. (Зокрема, вона намагалася боротися із HardFault, але виходило в неї не дуже... Особливо, якщо хотілося подружити її код із кодом з STM32CubeMX). Прийшов час реалізувати цю функціональність самостійно (хоч і під впливом, що приховувати :-).
Відладчик для ARM підтримує набір команд, які може отримати по SemiHosting. Список є тут: "What is Semihosting?". Поміж них:
- Команда SYS_WRITE0 із кодом 0x04, яка записує передану C-стрічку, вказівник на яку знаходиться в R1 у консоль дебагера.
- команда SYS_WRITEC (0x03), котра записує в консоль літеру, вказівник на яку -- в R1,
- SYS_READC (0x07) -- читає літеру із консолі дебагера та кладе її в R0.
- Гляньте також SYS_READ (0x06) -- їй в R1 передається вказівник на структуру аж із трьох елементів.
Напряму викликати bkpt із C ми не можемо. Тому є два варіанти -- скористатися окремим асемблерним файлом, або inline-асемблером GCC. Нехай функція, в обох випадках, матиме такий прототип, приймаючи значення R0 в перший аргумент, R1 -- в другий, а результат кладучи за вказівником, переданим у третьому (як ви могли здогадатися, спеціально так підігнано, під вимоги ABI):
int SH_DoCommand(int in_r0, int in_r1, int *out_r0); |
Перший варіант:
.global SH_DoCommand
.type SH_DoCommand, %function
SH_DoCommand:
bkpt 0xAB /* Waits for Debugger or HardFault */
/* Debugger will step over BKPT directly */
cmp R2, #0 /* Save return value? */
beq SH_End
str R0, [R2] /* Save the return value to *out_r0 */
SH_End:
movs R0, #1 /* Set return value to 1 */
bx LR /* Return */
- Завдяки ABI, перший аргумент вже в R0, другий -- в R1.
- cmp -- порівняти із нулем,
- beq -- Branch-If-Equal -- перейти до SH_End, якщо попередня команда сказала -- рівно, тобто в R2 -- 0, нульовий вказівник.
- str -- зберегти вміст R0 за адресою, що є в R2.
- movs кладе в R0 1.
- Нарешті, bx LR -- повернутися у функцію, що викликала цю.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | __attribute__ ((always_inline)) static inline int SH_DoCommand(int in_r0, int in_r1, int *out_r0) { int res = 0; asm volatile ( " mov r0, %[cmd] \n" " mov r1, %[arg] \n" " bkpt 0xAB \n" " mov %[res], r0 \n" : [res] "=r" (out_r0) /* Output */ : [cmd] "r" (in_r0), [arg] "r" (in_r1) /* Inputs */ : "r0", "r1", "cc", "memory" ); if(out_r0) *out_r0 = res; } return res; } |
- Асемблерний код, базово, той же -- покласти аргументи в R0, R1, викликати bkpt, забрати результат із R0.
- Зауважте, що в кінці кожної асемблерної команди слід явно писати \n.
- volatile після asm вказує, що команди можуть мати побічні ефекти -- це вимикає деякі (небажані за наявності таких ефектів) оптимізації.
- В кінці вставки пояснюємо компілятору, що результат вставки, який в асемблерному коді називається res, має відповідати out_r0; cmd i arg, відповідно, беруться із in_r0 та in_r1.
- Ще один важливий аспект, вставка має пояснити компілятору, які регістри вона псує: r0, r1, регістр прапорців ("cc" -- "condition code flags") та може змінювати пам'ять -- "memory".
- "Заклинання" перед оголошенням -- "__attribute__ ((always_inline)) inline" змушує компілятор вбудовувати функцію, а static забороняє (безпосередньо) викликати її із інших файлів .
Такий підхід простіший у використанні, але код виходить трішки гіршим --
це можна побачити, наприклад, якщо компілятору передати -save-temps --
команду зберігати проміжні файли, а потім проаналізувати асемблерний
лістинг -- там буде трохи зайвих перетасовувань регістрів. Або глянути в дебагері:
Детальніше про inline-асемблер див., наприклад:
- "ARM GCC Inline Assembler Cookbook"
- "Extended Asm - Assembler Instructions with C Expression Operands"
SH_DoCommand(SH_SYS_WRITE0, (int)str, NULL); |
В майбутньому ще повернуся детальніше до написання системних викликів для Newlib, а поки -- все,
Немає коментарів:
Дописати коментар