Информационный портал технической поддержки Центра проектирования интегральных микросхем |
Пример приведен для микроконтроллера К1986ВЕ92QI (Cortex-M3). Проект доступен для загрузки в конце статьи, раздел "Файлы для скачивания".
Микроконтроллер позволяет обрабатывать различные некорректные манипуляции через выработку исключений. В общем случае, код обработчиков исключений, как и обработчиков прерываний, выглядит следующим образом
HardFault_Handler PROC HardFault_Handler [WEAK] B . ENDP
Фрагмент кода 1 В этом коде ассемблерная строка "B ." означает зацикливание на текущей инструкции. То есть при возникновении исключения, ядро впадает в бесконечный цикл.
В общем же случае считается, что программист может реализовать код обработки возникшего исключения. Другими словами, программное обеспечение должно исправить возникший конфликт и продолжить штатное исполнение программы, если такое возможно. Для выхода из обработчика инструкцию зацикливания необходимо убрать, в этом случае исполнение возвращается на инструкцию, вызвавшую прерывание. Эта инструкция исполняется вновь, и, если конфликт, приведший к исключению не решен, то исключение будет сгенерировано снова. Таким образом, снова образуется зацикливание, но несколько по-другому.
Разработчику программного обеспечения необходимо решить самостоятельно, как ему обрабатывать возникающую ситуацию. В некоторых случаях решением может быть выход из прерывания не на сбойную инструкцию, а на инструкцию, следующую за ней. Например, это помогает в случае, если то, что должна была выполнить сбойная инструкция, было обработано в обработчике иным способом.
Чтобы реализовать такой выход, используется следующий код:
HardFault_Handler PROC
HardFault_Handler [WEAK]
TST LR, #4
ITE EQ
MRSEQ R0, MSP
MRSNE R0, PSP
LDR R1, [R0, #24]
ADD R1, #4
STR R1, [R0, #24]
BX LR
ENDP
Фрагмент кода 2Назначение этого кода будет разобрано подробнее позже, но если коротко, то он изменяет значение регистра PC, сохраненное в стеке на момент возникновения исключения. А регистр PC задает адрес инструкции, которая будет считана из памяти и исполнена. Увеличивая значение PC, которое сохранено в стеке, контроллер при выходе из исключения (восстановлении регистров из стека), попадёт на следующую инструкцию, относительно той, что вызвала исключение. В противном случае при восстановлении из стека исполнялась бы та же самая инструкция, которая приводит к исключению, и тогда получилось бы зацикливание.
Важно отметить, что с выходом из обработчика может возникнуть проблема, заключающаяся в том, что в общем случае неизвестно, сколько байт занимает инструкция, которая привела к исключению. Набор инструкций Thumb2 содержит 32-битные инструкции, поэтому в Cortex-M3/M4 высока вероятность того, что инструкция окажется 4-х байтной. Тогда, действительно, для выхода на следующую инструкцию необходимо увеличить PC на +4. Но если инструкция, вызвавшая исключение, была 2-х байтной, тогда при модификации +4 процессор либо перескочит следующую 16-битную инструкцию, либо попадёт в середину следующей 32-битной инструкции. В Cortex-M0/M1 практически все инструкции 2-х байтные.
В рассматриваемом далее примере деление производится 32-битной инструкцией, поэтому для данного варианта оправдан выход по +4. Но при чтении памяти обычно компилятором используются 16-битные инструкции. Поэтому, например, для микроконтроллеров, в которых при чтении памяти, несогласованной с ECC, возникает исключение HardFault, выход из него на следующую инструкцию необходимо делать через +2.
Способ решения данной проблемы с +4 или +2 предложил на форуме prostoRoman.
В сети достаточно много вариантов того, как обратиться к стеку. Например:
Обычно, этот код реализован как ассемблерная вставка в исходный файл на языке Си. Но при использовании легко столкнуться с тем, что эти коды утратили свою работоспособность. Вероятнее всего, встроенный ассемблер был сильно урезан в правах при развитии компиляторов. В частности, никакими ассемблерными командами на основе вышеперечисленных статей не удаётся прописать регистры R0-R4. Эти регистры, по соглашению языка Си, используются для передачи в функции первых 4-х входных значений, остальные значения, если необходимо, передаются через стек. Через R0 также возвращается значение из функции.
Вероятно, по этой причине встроенный ассемблер запрещает писать в регистры R0-R3.
Например, команда:
MOV r0, r1
Приводит к:
main.c(60): error: #1093: Must be a modifiable lvalue
Ближайший код, который был скомпилирован, выглядел так:
register uint32_t R0 __ASM("r0");
register uint32_t LR __ASM("lr");
void HardFault_Handler(void)
{
__asm volatile
{
TST LR, #4
ITE EQ
MRSEQ R0, MSP
MRSNE R0, PSP
BL HardFault_Handler_C
}
}
Фрагмент кода 3 Но при выполнении фрагмента кода 3 получается, что компилятор вместо записей в R0 вставляет везде NOP. Код, в итоге, оказывается нерабочим.
Ограничения на встроенный ассемблер прописаны на сайте ARM, и там же указано, что компилятор уполномочен делать с ассемблерными вставками все, что пожелает. Единственный выход - использовать ассемблер напрямую. То есть создавать ассемблерный файл *.s и писать код в нем. И линкеру совершенно не важно то, какие файлы он будет соединять.
Подробную интересную информацию про языки Си и ассемблер можно поискать в документе по следующей ссылке: Chapter 7 Using the Inline and Embedded Assemblers of the ARM Compiler
Ради маленького куска кода отдельный asm-файл создавать нерационально, поэтому код передачи указателя на стек был протестирован вставкой прямо в обработчик HardFault_Handler внутри startup_MDR32F9Qx.s, который все равно подключается по умолчанию.
; Комментарии в asm файле начинаются с ';'
HardFault_Handler\
PROC EXPORT HardFault_Handler [WEAK]
; Вызов обработчика на языке C
; Импорт внешней функции
IMPORT HardFault_Handler_C
; По LR определяем какой указатель стека активный
TST lr, #4 ; Сравниваем 2-й бит
ITE EQ ; ITE позволяет 4-м след. командам использовать флаги
MRSEQ r0, MSP ; Копирование спец регистра в общий регистр если bit = 0
MRSNE r0, PSP ; Копирование спец регистра в общий регистр если bit != 0
LDR R1, =HardFault_Handler_C ; Копирование указателя на функцию в регистр R1
BX R1 ; Переход на функцию без модификации LR ENDP
Фрагмент кода 4 В этом коде получается указатель на стек, и он передаётся во внешнюю функцию HardFault_Handler_C(), написанную на языке Си. Здесь используется особенность, что первый входной параметр уходит в функцию на Си через регистр R0.
Работа со стеком при возникновении исключения будет рассмотрена на примере деления на ноль. По умолчанию деление на ноль не генерирует прерывание в Cortex-M, поэтому генерацию такого исключения необходимо вначале разрешить. Далее будет произведено деление на ноль, которое приведет к вызову HardFault_Handler. В этом обработчике можно посмотреть состояние стека и вызвать обработчик на языке Си, в котором и будет произведен перевод регистра PC на следующую инструкцию.
Код примера:
#include <stdint.h>
#include <MDR32F9Qx_config.h>
void HardFault_TrapDivByZero(void)
{
volatile uint32_t *confctrl = (uint32_t *) 0xE000ED14;
*confctrl |= (1<<4);
}
uint32_t RiseDivZero(void)
{
uint32_t b = 0;
return 10 / b;
}
int main(void)
{
volatile uint32_t result;
HardFault_TrapDivByZero();
// Call Exception
result = RiseDivZero();
// MainLoop
while (1);
}
enum { r0, r1, r2, r3, r12, lr, pc, psr};
void HardFault_Handler_C(uint32_t stack[])
{
// Изменяем значение регистра PC
// на адрес инструкции на которую произойдет выход из исключения.
stack[pc] = stack[pc] + 4;
// Обычно состояние стека выводят куда-нибудь для отладки
// printf("r0 = 0x%08x\n", stack[r0]);
// printf("r1 = 0x%08x\n", stack[r1]);
// printf("r2 = 0x%08x\n", stack[r2]);
// printf("r3 = 0x%08x\n", stack[r3]);
// printf("r12 = 0x%08x\n", stack[r12]);
// printf("lr = 0x%08x\n", stack[lr]);
// printf("pc = 0x%08x\n", stack[pc]);
// printf("psr = 0x%08x\n", stack[psr]);
}
Фрагмент кода 5 Для того чтобы разобраться, как работают регистры и стек, рассмотрим следующие рисунки ниже с алгоритмом работы каждого шага.
На рисунке 1 изображено начальное состояние в отладчике, что является точкой отсчёта. Следующей командой произойдет вход в функцию RiseDivZero(). Следует напомнить назначение регистров, на которые следует обратить внимание при разборе:
Рисунок 1 - Вызов функции RiseDivZero() при отладке в IDE Keil
Из рисунка 1 видно, что
Далее в ассемблерном отладчике необходимо войти по шагам F11 в функцию RiseDivZero() и дойти до инструкции вызова деления, как это показано на рисунке 2. Следующий шаг произойдет в обработчике HardFault_Handler, следует обратить внимание на регистры до этого шага:
Рисунок 2 - Вызов инструкции деления в функции RiseDivZero() при отладке в IDE Keil
При входе в обработчик происходит в стек сохраняются регистры R0-R3, R12, R13(SP), R14(LR), R15(PSR). Сами регистры получают новые значения:
Отладчиком в окне дизассемблера необходимо пройти шагами F11 до инструкции перехода на обработчик HardFault_Handler_C(). Состояние в отладчике выглядит так, как показано на рисунке 3:
Рисунок 3 - Переход в обработчик HardFault_Handler() при отладке в IDE Keil
При входе в функцию важно обратить внимание, что регистр LR сохранил значение 0xFFFFFFF9. Это произошло потому, что выполнен прямой переход на адрес функции. Это аналог GoTo. По этой причине при выходе из обработчика HardFault_Handler_С возврат будет не в ассемблерный HardFault_Handler, а сразу к коду, вызвавшему исключение.
Варианты вызова перехода на обработчик:
// Вариант GoTo
LDR R1, =HardFault_Handler_C
BX R1
// Вариант Call
BL HardFault_Handler_C
Фрагмент кода 6 На рисунке 4 выделено, какую ячейку стека меняет код, само значение этой ячейки будет также видно из рисунка.
Рисунок 4 - Обработчик на Си HardFault_Handler_C при отладке в IDE Keil
В ассемблерном окне видно команду выхода по LR. Но поскольку LR равно одному из значений EXC_RETURN, то выход произойдет не по адресу в LR, а будет восстановлено значение регистров из стека. То есть по факту произойдет переход исполнения по адресу, загруженному из стека в регистр PC.
Следуя далее по шагам F11, исполнение возвращается на инструкцию, следующую за той, с которой возникло исключение по делению на ноль. То есть происходит возврат из исключения назад. Наглядно показано на рисунке 5.
Рисунок 5 - Возврат в функцию RiseDivZero() при отладке в IDE Keil
Инструкция, которая теперь будет выполнена - это возврат в main() по адресу в LR.
Необходимо помнить, что:
Сайт: | https://support.milandr.ru |
E-mail: | support@milandr.ru |
Телефон: | +7 495 221-13-55 |