extern const uint32_t __freertos_irq_stack_top[];
extern const uint32_t __freertos_irq_stack_bottom[];
/* Prologue and epilogue for HPE-enabled interrupts */
#define ISR_prologue_epilogue_HPE(Vector, ISRcode) /* Vector is ISR vector name, ISR code is actual handler function declared with "WCH-Interrupt-fast" attribute */ \
void __attribute__((naked)) Vector(void) { /* No registers out of HPE storage are used, so declare ISR vector as naked */ \
asm volatile("lui t0, %hi(__freertos_irq_stack_bottom);" /* Load stack bottom boundary address, higher 20 bits */ \
"ori t0, t0, %lo(__freertos_irq_stack_bottom);" /* Load stack bottom boundary address, lower 12 bits */ \
"bgeu sp, t0, 1f;" /* Compare current stack value against lower ISR stack boundary */ \
"add t0, zero, zero;" /* We are in task stack. Stack switch to ISR stack is needed. Set return value zero. */ \
"csrrw sp, mscratch, sp;" /* Switch stack pointer to dedicated ISR stack to not pollute current task stack */ \
"1:" /* Continue here if stack switch is not needed */ \
"addi sp, sp, -4;" /* Reserve a word in ISR stack */ \
"sw t0, 0(sp);"); /* Store stack switch requirement into ISR stack */ \
ISRcode(); /* Call actual ISR code. ISRcode should be declared with an "WCH-Interrupt-fast" attribute and may use a registers out of HPE storage this way */ \
asm volatile("lw t0, 0(sp);" /* Restore stack switch flag from a stack */ \
"addi sp, sp, 4;" /* Free a word from ISR stack */ \
"bne t0, zero, 2f;" /* Compare flag with zero. Branch if not zero. */ \
"csrrw sp, mscratch, sp;" /* Switch stack pointer from dedicated ISR stack back to current task stack */ \
"2:" /* Go to here if no stack switch is needed */ \
"mret;"); /* Exit an interrupt */ \
} В ассемблере выглядит так (если полезный обработчик объявить как static inline):
/* Начало прерывания, сюда указывает вектор. Тут условный переключатель стека. */
00000804: lui t0,0x2001c 00000808: ori t0,t0,0 0000080c: bgeu sp,t0,0x818 <SysTick_Handler+20> 00000810: add t0,zero,zero 00000814: csrrw sp,mscratch,sp 00000818: addi sp,sp,-4 0000081a: sw t0,0(sp) /* Тут полезный код обработчика прерывания. Ниже - эпилог. */
000008d0: lw t0,0(sp) 000008d2: addi sp,sp,4 000008d4: bnez t0,0x8dc <SysTick_Handler+216> 000008d8: csrrw sp,mscratch,sp 000008dc: mret
Фишка в чём: FreeRTOS, у неё стеки задач отдельны от общего стека всех прерываний. Поэтому, при входе в прерывание, нужно сначала переключить стек на стек прерываний, чтобы не занимать стек задачи, которую застало прерывание. А так как разрешены вложенные прерывания, переключение стека приходится делать условное. И ассемблерный пролог+эпилог это делают:
- При входе в прерывание, проверяется текущее значение указателя стека. Если оно вне стека прерываний (не в самом веру памяти), то выполняется переключение. Если прерывание вложенное и указатель стека находится уже в стеке прерываний - то переключение не делаем. Результат - ноль если переключали, не ноль - если не переключали - сохраняем в стек прерываний.
- При выходе, загружаем флаг переключения из стека. Если ноль - переключаем обратно. Если не ноль - переключения стека не нужно. Просто выходим из прерывания.
Была проблема с сохранением регистров. В сообщении выше, я решил её сохранением всех регистров, не сохраняемых HPE. И оно сработало. Но червячок перфекционизма не переставал меня глодать. И, когда мне напомнили, что можно вызвать не-naked функцию из naked-прерывания, картинка сложилась.
Итого:
- Пишем функцию обработчика прерывания. Объявляем её с атрибутом WCH-Interrupt-fast (или как там в сборке у тов. Клёна?). Так код обработчика не сохраняет регистры, сохраняемые HPE, но если портит регистры, не сохраняемые HPE, то эти регистры будут сохранены в стек прерываний автоматически. Функцию объявляем static inline __always_inline__, чтобы избежать лишнего вызова и возврата.
- Используем макрос, давая ему имя вектора прерывания и имя обработчика.
Перфекционизм удовлетворён. Ни одного лишнего сохранения, ни одного лишнего перехода. Была мысль вместо адреса стека, контролировать вложенность прерываний. И если вложенное - стек не переключать. Но нет. Прерывание низкого приоритета может не успеть переключить контекст, когда в него вложится другое прерывание. Поэтому - только контроль указателя стека по факту попадания в область стека прерываний.
P.S. По ходу внедрения переключалки, выявил такую ложку дёгтя - оно работает только для функций, которые нлайнятся. Толко заинлайненная функция, имея атрибут interrupt("WCH-Interrupt-fast"), будет сохранять контекст, но не будет делать mret в конце. Как только появляется вызов, то в конце функции, объявленной как прерывание, закономерно появляется mret, что не даёт восстановить стек - выход из прерывания выполняется раньше, чем дело доходит до части эпилога.