Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Кармин Новиелло - Освоение STM32.pdf
Скачиваний:
2740
Добавлен:
23.09.2021
Размер:
47.68 Mб
Скачать

Продвинутые методы отладки

668

24.1. Введение в исключения отказов Cortex-M

В начале этого долгого путешествия мы увидели, что микроконтроллеры на базе ядер Cortex-M реализуют ряд системных исключений. Некоторые из них связаны с отказами, то есть эти исключения срабатывают, когда что-то происходит не так во время выполнения обычного потока. Реализуя надлежащим образом обработчики для этих исключений отказов, мы можем избавиться от источника отказа. Это очень полезно во время отладки, поскольку все это помогает нам изолировать проблему от остальной части приложения. Однако правильная обработка отказов может быть полезна даже в «рабочей версии» микропрограммы: после обнаружения отказа мы можем перевести устройство в безопасное состояние, прежде чем будем пытаться перезагрузить плату.

Ядра Cortex-M3/4/7 предлагают программистам четыре исключения, связанных с отказами (см. таблицу 1 в Главе 7):

Отказ системы управления памятью: Memory Management Fault

Отказ шины: Bus Fault

Отказ программы: Usage Fault

Тяжелый отказ: Hard Fault

Первые три исключения срабатывают, когда имеют место узкоспециализированные отказы, и они доступны только в ядрах Cortex-M3/4/7. Последнее исключение, Hard Fault, является единственным доступным даже в ядрах Cortex-M0/0+. Оно также называется общим исключением отказа (generic fault exception), поскольку оно не может быть запрещено и действует как сборщик для условий узкоспециализированных отказов, когда соответствующие им исключения отказов запрещены.

Когда возникает исключение отказа, мы можем попытаться определить его причину, проанализировав содержимое некоторых «системных регистров». Более того, простой анализ трассировки стека может привести нас к корню отказа, по крайней мере, в большинстве причин отказа.

Какие обстоятельства могут вызвать системный отказ? Ответ на этот очевидный вопрос не тривиален. Наиболее частым источником отказов является баг в микропрограмме, особенно на этапе разработки. Доступ к неправильной ячейке памяти (довольно часто из-за поврежденного указателя) является наиболее частым источником условий отказа. Недопустимая или плохо реализованная таблица векторов является еще одним распространенным источником отказов. Переполнение стека – еще одно довольно частое условие отказа, особенно в недорогих микроконтроллерах STM32 при работе с ОСРВ.

Иногда происхождение отказа не связано с программным обеспечением, а может быть вызвано внешними факторами, такими как:

Плохие конструкция и разводка печатной платы (бывают чаще, чем вы думаете).

Нестабильный или плохой источник питания (довольно часто встречается в плохих конструкциях).

Электрические шумы.

Электромагнитные помехи (EMI) или электростатический разряд (ESD).

Экстремальные условия эксплуатации (например, температура, влажность и пр.).

Повреждение некоторых компонентов (например, устройства Flash/EEPROM, кварцевые генераторы, электролитические конденсаторы).

Продвинутые методы отладки

669

Радиоактивное излучение.

Диагностировать вышеупомянутые неприятные условия отказа действительно трудно. Это те условия, которые ни один разработчик аппаратного обеспечения никогда не захочет повстречать, и они выходят за рамки данной книги. Здесь мы сосредоточимся только на программных отказах и способах их выявления. Однако, прежде чем приступить к анализу причин, вызывающих эти четыре исключения отказов, необходимо проанализировать, как генерируется исключение с точки зрения программного обеспечения. Это важно для выявления или, по крайней мере, для попытки выявления кода, который приводит к исключению отказа.

24.1.1.Последовательность перехода в исключения Cortex-M и Соглашение ARM о вызовах

Для высокоуровневых программистов2 вызов процедуры кажется очевидным. Мы просто записываем имя функции, которую будем вызывать, передавая ей определенное количество параметров. Однако, с точки зрения процессора, то, что происходит «под капотом», должно быть конкретизировано до мельчайших деталей, и оно должно соответствовать как архитектуре процессора, так и семантике языка программирования. По этой причине принято говорить о соглашении о вызовах (calling convention) при описании процесса размещения новой процедуры в стеке.

Стандарт архитектуры ARM о вызове процедур (ARM Architecture Procedure Call Standard, AAPCS) точно определяет соглашение о вызовах для архитектур на базе ARM. В Главе 1 мы увидели, что микроконтроллеры на базе Cortex-M предоставляют несколько регистров ядра, которые для вашего удобства вновь показаны на рисунке 1. Не все эти регистры ядра доступны во всех ядрах Cortex-M: например, регистры S0-S31 модуля FPU доступны только в ядрах Cortex-M4F и Cortex-M7, когда FPU включен и используется.

Рисунок 1: Регистры ядра процессора Cortex-M

2 Как программисты Си, мы все «высокоуровневые программисты», верите вы этому или нет.

Продвинутые методы отладки

670

Некоторые регистры ядра играют особую роль, потому что они используются для выполнения действий процессора. R13 – это указатель стека (Stack Pointer, SP), то есть указатель на адрес SRAM (так что он похож на 0x2000 XXXX в STM32), а именно на адрес самой последней записи, помещенной в стек. Эта запись представляет собой область локальной памяти данной функции, и в автодекрементном стеке по записи (full-descendent stack) SP совпадает с самым младшим адресом стека. R14 – это регистр (обратной) связи (Link Register, LR), то есть адрес инструкции во FLASH3 (так что он похож на 0x0800X XXXX в STM32), следующей за инструкцией, вызвавшей данную функцию в стеке. R15 – это счетчик команд (Program Counter, PC), то есть регистр, который содержит адрес во Flashпамяти текущей ассемблерной инструкции.

Регистры R0-R3 играют еще одну важную роль в Соглашении ARM о вызовах. Они используются для хранения первых четырех параметров, передаваемых вызываемой функцией. Если вызываемая функция использует менее четырех параметров, то в первые четыре регистра общего назначения помещается содержимое этих параметров. Очевидно, здесь мы предполагаем, что аргументы выровнены по словам (выровнены на границе четырех байт). Если, напротив, наша функция принимает более четырех параметров или их общий размер превышает шестнадцать байт, то нам нужно выделить достаточно места в стеке вызываемой функции для хранения других параметров, прежде чем передать ей управление. Такое использование регистров R0-R3 позволяет ускорить процесс вызова и уменьшить количество используемой SRAM. Наконец, регистры R0-R1 также используются для хранения возвращаемого значения функции. Таким образом, хорошим тоном было бы ограничить количество параметров максимум четырьмя, где это возможно. Если это невозможно, то вы должны попытаться поместить наиболее часто используемые параметры в R0-R3 (то есть определить их как первые четыре параметра функции), чтобы минимизировать доступ к стеку вызываемой функции.

Поскольку некоторые из регистров общего назначения играют определенные роли, как вызываемая функция, мы не можем свободно изменять их содержимое, но мы должны придерживаться следующих соглашений:

Вызываемая функция может свободно изменять регистры R0, R1, R2 и R3.

Это подразумевает, что вызывающая функция должна сохранить свой контекст (если он используется для хранения важных данных для вызывающей функции) перед передачей управления вызываемой функции.

Вызываемая функция не может предполагать что-либо в отношении содержимого R0, R1, R2 и R3, если только они не играют роль параметров.

Вызываемая функция может свободно изменять регистр LR, но его значение при переходе в функцию будет необходимо при выходе из нее (поэтому это значение необходимо хранить в кадре стека вызываемой функции).

Вызываемая функция может изменять все оставшиеся регистры, если их значения восстанавливаются после выхода из функции. Они включают в себя SP и регистры R4-R11. Это означает, что после вызова функции мы должны предполагать, что (только) регистры R0-R3, R12 и LR были перезаписаны.

Функция не должна делать какие-либо предположения относительно содержи-

мого регистра текущего состояния программы (Current Program Status Register, CPSR).

3 Это не совсем так, потому что ЦПУ может выполнять код, размещенный в SRAM, а также в других внешних памятях. Но в данном контексте это можно считать правдой.

Продвинутые методы отладки

671

Если FPU включен и используется, вызываемая функция может свободно изменять регистры S0-S15, которые должны быть сохранены (вместе с регистром FPSCR)

вызывающей функцией перед вызовом вызываемой. И наоборот, вызываемой функ-

ции необходимо сохранить содержимое регистров S16-S31 перед изменением их содержимого.

R12 – это специальный «помеченный регистр», используемый компоновщиками для динамического связывания. Он не очень полезен во встроенных микроконтроллерах, таких как Cortex-M, но это регистр, который должен быть сохранен

вызывающей функцией в соответствии с AAPCS4.

Итак, подведем итоги. С точки зрения вызывающей функции, прежде чем вызывать другую процедуру, нам нужно сохранить содержимое следующих регистров: R0-R3, R12, R14, CPSR (плюс S0-S15 и FPSCR, если FPU включен). Эти регистры выделены красным на

рисунке 1.

Как высокоуровневым программистам, нам не нужно заботиться об этих правилах. Обеспечение соблюдения правил AAPCS является задачей компилятора. В Главе 7 мы увидели, что отличительной особенностью ядер Cortex-M является возможность использовать обычные функции Си в качестве обработчиков исключений. Это означает, что обработчики исключений «складываются» в основной стек как обычная процедура Си. Но это подразумевает, что для того, чтобы разрешить использование функции Си в качестве обработчика исключений, механизм исключений должен соответствовать требованиям соглашения о вызовах AAPCS, и поэтому ему необходимо автоматически сохранять эти «красные» регистры на рисунке 1 при переходе в исключение, и восстанавливать их при выходе из исключения под контролем процессора. Поэтому при возврате в прерванную программу все регистры будут иметь те же значения, что и при запуске последовательности перехода в прерывание.

Кроме того, поскольку исключение соответствует прерыванию основного потока программы, и поскольку оно может сработать в любое время, нам необходимо сохранять содержимое PC, в противном случае у нас не будет способа вернуться к основному потоку при выходе из исключения. При обычном вызове функции значение PC сохраняется в регистре LR с помощью инструкций ветвления. При срабатывании же исключения значение адреса возврата (PC) не сохраняется в LR (механизм исключения помещает специальный код EXC_RETURN в LR при переходе в исключение, который используется при возврате из исключения – мы проанализируем его в чуть позже), поэтому значение адреса возврата также необходимо сохранять с помощью последовательности исключения

(exception sequence).

Таким образом, во время последовательности обработки исключения на микроконтроллерах на базе Cortex-M, в общем, необходимо сохранять восемь регистров:

R0-R3

R12

SP

LR

CPSR

4 Важно подчеркнуть, что такое же Соглашение ARM о вызовах применимо и к микропроцессорам на базе Cortex-A, которые имеют все возможности для обработки динамического связывания для высокоуровневых операционных систем, таких как Linux и Windows.

Продвинутые методы отладки

672

Кроме того, должны быть сохранены S0-S15 и регистр FPSCR, если используется FPU.

Рисунок 2: Как регистры ядра загружаются в стек ЦПУ при переходе в исключение

А где же процессор хранит эти регистры? Очевидно, что они хранятся в стеке5, в самом начале кадра стека обработчика исключения. Данная процедура называется загрузкой в стек (stacking), и этот процесс четко показан на рисунке 2. Обратите внимание, что на рисунке 2 цвет регистров ядра светлее, чем тот, который использовался на рисунке 1. Это потому, что важно подчеркнуть, что процессоры хранят в этих ячейках содержимое регистров ядра до перехода в последовательность исключения. Когда срабатывает исключение, содержимое регистров ядра обновляется данными контекста исключения (например, PC будет указывать на первую инструкцию обработчика исключения, или SP будет указывать на вершину MSP сразу после загруженных в стек регистров ядра).

Содержимое сохраненных регистров ядра может быть действительно полезным для оценки того, что именно породило исключение отказа. Например, если исключение отказа срабатывает из-за доступа к неверной ячейке памяти (возможно, из-за поврежденного указателя), проверяя эти регистры, мы можем попытаться понять место, где осуществляется недопустимый доступ к памяти. Поэтому возникает вопрос: как высокоуровневые программисты, можем ли мы получить доступ к этим значениям? Конечно же! Нам нужно всего лишь немного программирования на ассемблере.

Предположим, мы хотим получить доступ к содержимому загруженного в стек регистра, когда вызывается EXTI15_10_IRQHandler() (это ISR, которая вызывается, когда вывод PC13, связанный с синей кнопкой Nucleo, сконфигурирован в режиме прерываний на

5 Здесь ситуация немного сложнее. В зависимости от того, используется ОСРВ или нет, может быть «несколько» стеков одновременно: основной стек (Main Stack) или стек, специфичный для отдельного потока, называемый стеком процесса (Process Stack). Эта тема выходит за рамки данной книги. Более подробную информацию о них можно найти в превосходной книге Джозефа Ю (http://amzn.to/1P5sZwq) об архи-

тектурах Cortex-M.

Продвинутые методы отладки

673

большинстве микроконтроллеров STM32). Мы можем определить ISR следующим образом:

1void EXTI15_10_IRQHandler(void) {

2asm volatile(

3

" tst lr,#4

\n"

4

" ite

eq

\n"

5

" mrseq r0,msp

\n"

6

"

mrsne r0,psp

\n"

7

"

mov

r1,lr

\n"

8" ldr r2,=EXTI15_10_IRQHandler_C \n"

9" bx r2"

10);

11}

12

13EXTI15_10_IRQHandler (uint32_t *core_registers, uint32_t lr) {

14/* core_registers указывает на регистры R0-R3, R13, SP и CPSR,

15

в то время как аргумент lr содержит содержимое регистра LR

16

непосредственно перед переходом в исключение */

17....

18}

Приведенный выше ассемблерный код может показаться сложным для понимания, но это не искусство черной магии. Инструкция tst выполняет побитовое сравнение между содержимым регистра LR (текущего регистра, а не того, который сохранен в стеке) и константой 4. Если они совпадают (то есть четвертый бит регистра LR установлен в 1), тогда стек PSP использовался во время перехода в исключение. В противном случае MSP был текущим используемым стеком. Причина, по которой эта проверка выполняется, скоро станет ясна. Примите это как есть.

Команда в строке 7 делает простую вещь (это хитрая часть): содержимое текущего регистра LR помещается в регистр R1, и вызывается функция EXTI15_10_IRQHandler_C() (обратите внимание на окончание _C). Эта другая функция принимает два параметра: core_registers и lr. Согласно спецификации AAPCS, core_registers будет совпадать с регистром R06, а lr – с содержимым R1. Когда происходит переход в обработчик исключения, R0 совпадает с начальным адресом в текущем стеке (MSP или PSP), где были сохранены регистры ядра.

Рисунок 3 ясно разъясняет это. Как видите, core_registers соответствует регистру R0, который содержит базовый адрес загруженных в стек регистров. lr соответствует регистру R1, содержимое которого заполнено регистром LR с помощью ассемблерной инструкции в строке 7. Таким образом, мы можем получить доступ к загруженным в стек регистрам из процедуры EXTI15_10_IRQHandler_C() и выполнить анализ их содержимого, как мы увидим позже.

6 Обратите внимание, что параметр core_registers является указателем, поэтому регистр R0 будет содержать адрес ячейки памяти (32-разрядное целое число), с которой были сохранены регистры ядра.