середа, 6 грудня 2017 р.

SemiHosting без дебаггера

Взято із: "Getting printf Output from Target to Debugger".
Якщо ви користувалися SemiHosting, то могли зауважити -- коли програма виконується без під'єднаного відладчика (зневаджувача), нічого корисного вона не робить. Звичайно, без згаданого відладчика, чим вона зайнята, не видно, але жодної зовнішньої активності (мигання світлодіодами, комунікація шинами) після спроби скористатися семихостингом не буде. В чому справа і як із тим боротися? 

Найпростіший варіант -- мати "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. Таким чином, якщо в коді трапиться команда:
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.

Після входу в обробник, стек виглядає так:
Стек при вході в обробник виключення, за умови, що FPU не використовується -- floating-point контекст не зберігається. Якщо б зберігався, над xPSR було б ще 16 регістрів S0-S15 i FPSCR.
Взято із згаданої вище книжки,  Joseph Yiu, "The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors"
Тепер потрібно знати деякі подробиці. Процесор підтримує два стека, на які є відповідні вказівники, 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, можемо зробити так:
.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
Де HardFault_Handler_C -- функція С, із наступним прототипом, яка має бути визначена у котромусь із файлів проекту:
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 не вміє працювати із безпосередніми значеннями, тому для них пишемо так:
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
Перш, ніж тестувати біти, завантажуємо 4 в R0, використовуємо явний умовний перехід -- beq, branch-if-equal, але, в цілому, код -- той же. Він навіть успішно виконуватимуться на ARM Cortex M3/M4, але не будемо передчасно песимізувати -- залишимо їм більш просунутий, написаний вище.
Зізнаюся, не зміг дійти толку із умовною компіляцією в 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:
Взагалі, є два режими зневадження -- Halt i Monitor. Але так як C_DEBUGEN має вищий пріоритет, інший режим не розглядатимемо.
Взято із:  Joseph Yiu, "The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors"
після цього виконання BKPT приведе до переходу в стан HALT та очікування на реакцію зневаджувача. Якої не буде, бо він не під'єднаний. Програма зависає, як і раніше, іншими словами -- навіть якщо обробник HardFault правильний. Але вимкнули живлення (нарешті зануливши DEBUGEN), ввімкнули, і вона працюватиме. 

Хоча це не зручно, на жаль, після тривалого читання документації, так виглядає, нічого із тим зробити не можна. Взагалі, OpenOCD мав би той біт очищати, але він цього не вміє, судячи по всьому... (Див. його код, та й в документації нічого на ту тему не знайшов, хоча, не виключаю, що погано шукав)
Тому, по великому рахунку, ні перевірка вмісту 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].
Але й ігнорувати всі HardFault -- не варіант. Тому, залишається одне -- дивитися на команду, яка породила його, і якщо це brkp 0xAB -- пропускати її. Опкод brkp  -- 0xBE.


 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), завантажу окремо -- вони якраз в процесі серйозної переробки, але поки необхідні зміни ви можете внести й самостійно.
Ремарка: як самостійно створити 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-асемблер див., наприклад:
Тепер вивести C-стрічку в консоль дебагера можна так:



SH_DoCommand(SH_SYS_WRITE0, (int)str, NULL);

В майбутньому ще повернуся детальніше до написання системних викликів для Newlib, а поки -- все,

Дякую за увагу!


Немає коментарів:

Дописати коментар