Аппаратные средства отладки ESP32/ESP32S2/ESP32S3 и их использование у себя в скетчах.
Умеет наш любимый ESP32/S2/S3 аппаратную отладку: поддерживаются 2 аппаратные точки останова по коду и две - по данным.
В терминах Espressif это, соответственно, IBREAK и DBREAK, а API, которое Espressif предланает использовать вот такое:
esp_cpu_set_watchpoint()
esp_cpu_set_breakpoint()
esp_cpu_clear_watchpoint()
esp_cpu_clear_breakpoint()
На этом документация и заканчивается.
Если попробовать установить break или watch, то он, как ни странно, сработает и вывалит на экран
дамп регистров и Guru Meditation: excepttion was unhandled (DebugException)
А нам так не нужно, нам хотелось бы получить контроль и что-то сделать, залоггировать событие , например.
Или еще что.
Ниже - скетч для ESP32/ESP32-S2 и ESP32-S3. На RISCV тоже самое можно сделать, но я не сделал, заленился.
Скетч состоит из двух файлов, их надобно будет положить в один каталог.
Что делает скетч: устанавливает watchpoint#0 на запись в переменную. И пишет туда, выводя на экран кое какую статистику (WP#1 съели канарейки из freertos): выводится количество срабатываний защиты, текущее значение защищаемой переменной и адрес инструкции, которая вызвала срабатывание защиты.
Тонкости работы кода - в комментариях.
Для чего это было нужно мне: я делал логгирование доступа к регистрам WIFI (доступ к адресам 0x60033xxx), чтобы срисовать последовательность инициализации и тайминг.
Для чего это может быть нужно кому-то: как пример обработки высокоприоритетного прерывания с вызовом Си-обработчика. При небольшой модификации код можно переделать в пошаговое выполнение.
Секретные Файлы: di.ino
// Скетч-пример, который показывает, как на ESP32, ESP32-S2 и ESP32-S3 :
//
// 1. Устанавливать защиту на запись в память ( в примере - на переменную) (см. esp_cpu_set_watchpoint())
// 2. Пропуск инструкции (debug_exception_handler)
// 3. Обработку высокоприоритетного прерывания (di.S)
//
// Похожим образом можно установить и перехватить breakpoint. (см. esp_cpu_set_breakpoint())
// Можно комбинировать DBREAK и IBREAK для контролируемого пошагового выполнения программы
#include <Arduino.h>
// Exception Frame.
//
struct exc_frame {
uint32_t ps;
uint32_t pc; // указатель на инструкцию вызвавшую прерывание
uint32_t registers[20];
};
extern "C" void debug_exception_handler(struct exc_frame *frame);
// Счетчик сработавших прерываний
static int triggered = 0;
// Адрес инструкции, вызвавшей прерывание
static uintptr_t address = 0;
// Обработчик DebugInterrupt. (Прерывание генерируется CPU в момент доступа к защищаемой памяти)
// Сюда прилетают Data watchpoints и Code breakpoints, но в данном примере мы будем только ловить data watchpoint
// Пропускает инструкцию вызвавшую прерывание, увеличивает счетчик событий на единичку
// Вызывается из di.S, параметры передаются через стек
//
void IRAM_ATTR debug_exception_handler(struct exc_frame *f) {
// Массив длин инструкций.
// Длина ассемблерной инструкции по ее первому байту. Длина может варьировать от 2 до 4 байт, может быть 3 байта.
//
static const uint8_t xt_insn_len[] = { XCHAL_BYTE0_FORMAT_LENGTHS };
// Регистр PC = адрес инструкции, которая вызвала прерывание.
// Прочитаем первый байт инструкции через IBUS. Гарвард как-никак.
// 1. IBUS требует выровненного 32битового доступа.
// 2. Инструкции Xtensa могут быть 2,3 или 4 байта.
// Поэтому читаем выровненно, потом сдвигаем. По-другому - никак, все повиснет.
//
// Первый байт инструкции (`opcode`) определяет длину инструкции в байтах.
//
uintptr_t pc = (uintptr_t)f->pc;
// Сохраним адрес инструкции, вызвавшей прерывание
address = pc;
uint32_t aligned = pc & ~3;
uint32_t word = *(volatile uint32_t *)aligned;
uint32_t shift = (pc & 3) * 8;
uint8_t opcode = (word >> shift) & 255;
// Пропускаем инструкцию
f->pc = f->pc + xt_insn_len[opcode];
// Или так, если не хотим пропускать:
// esp_cpu_clear_watchpoint(0);
//
// Увеличиваем счетчик
triggered++;
}
// Тестовая переменная. Ее будем защищать. volatile, чтобы компилятор не оптимизнул
static volatile int test = 666;
void setup() {
Serial.begin(115200);
// Включаем защиту средствами ESP-IDF: Watchpoint#0, переменная test, длина - 4 байта
// Длина может быть от 1 до 64 байт, однако, адрес обязан быть выровнен на значение длины: для защиты
// 16-байтового массива, данный массив должен быть выровнен в памяти на 16 байт. __attribute__((aligned)) вам в помощь
// Watchpoint#1 уже занят под FreeRTOS а других нету. Всего два watchpoint'а аппаратных есть.
//
esp_cpu_set_watchpoint(0, (const void *)&test, 4 , ESP_CPU_WATCHPOINT_STORE);
}
void loop() {
// Пишем в защищаемую память: это должно вызвать прерывание и вызов нашего debug_exception_handler().
// Значение переменной НЕ изменится потому, что наш обработчик просто пропускает инструкцию. Обработчик
// может повторить инструкцию - для этого не нужно менять PC, но следует esp_cpu_clear_watchpoint(0) сделать.
// Иначе, после повтора инструкции, прерывание будет сгенерировано снова и так до бесконечности
test = triggered;
// Скетч без делея - не скетч.
delay(1000);
// Ожидаемый вывод (например): "Triggered=321, test=666, address=4200xxxx"
Serial.printf("Triggered=%d, test=%d, address=%x\r\n", triggered, test, address);
}
Файл di.S
// Перехват DebugInterrupt и вызов обработчика, написанного на Си (xtensa, windowed abi)
//
// Обработчики прерываний уровней 456 и 7 должны быть написаны на асскмблере. Это связано с тем, что система
// вызывает эти обработчики не так, как обычную Си-функцию. Здесь, собственно, находится адаптер, который
// написан на ассемблере но вызывает Си-код обычный.
//
#ifdef __XTENSA__
#include <xtensa/coreasm.h>
#include <xtensa/corebits.h>
#include <xtensa/config/system.h>
#include "sdkconfig.h"
#include "xtensa_rtos.h"
#include "soc/soc.h"
#include "xt_asm_utils.h"
#include "xtensa_context.h"
#define CAUSE_DEBUGEXCEPTION (1)
#define XT_DEBUGCAUSE_DI (5)
#if (CONFIG_ESP32_ECO3_CACHE_LOCK_FIX && CONFIG_BTDM_CTRL_HLI)
// В ESP32 v3.00 есть бага в железе. Подробности нам не интересны, подробностей мы, увы, не понимаем.
// Код ниже содержит две вставки-затычки, которые фиксят эту багу. Затычки скопированы (без
// малейшего понимания их работы) из xtensa_vectors.S
//
# define BUGGY_CHIP
#endif
.section .iram1, "ax"
// Си-функция, пользовательский код
.extern debug_exception_handler
.type debug_exception_handler, @function
// weak-символ, переопределение
.global xt_debugexception
.type xt_debugexception, @function
.align 4
.literal_position
.align 4
// DebugExceptionVector сохраняет регистр A0 и прыгает по адресу xt_debugexception
// A0 сохранен как "wsr a0, EXCSAVE+XCHAL_DEBUGLEVEL" (см. xtensa_vectors.S:_DebugExceptionVector
//
xt_debugexception:
#ifdef BUGGY_CHIP
// В оригинальном коде затычки есть странная строчка.
// Я ее на всякий случай закомментировал: А0 у нас и так сохранен, не будем чужой стек трешить
// s32i a0, sp, XT_STK_EXIT
// Читаем 13-й бит CPU CoreID. Если 0, то мы на ядре 0, иначе - на ядре 1.
rsr.prid a0
extui a0, a0, 13, 1
// Пытаемся понять юыло ли прерывание от BT или это было обычное DebugInterrupt
// Если мы думаем, что прерывание было от BT, тогда выполняем некую магическую задержку.
// Я не знаю , зачем. Так Espressif делает в своем коде. Не будем перечить.
//
#if (CONFIG_BTDM_CTRL_PINNED_TO_CORE == PRO_CPU_NUM)
beqz a0, 1f
#else
bnez a0, 1f
#endif
rsr a0, DEBUGCAUSE
extui a0, a0, XT_DEBUGCAUSE_DI, 1
bnez a0, debug_di_exc
1:
#endif // BUGGY_CHIP
// Устанавливаем код исключения в регистре EXCCAUSE. На всякий случай.
//
movi a0, CAUSE_DEBUGEXCEPTION
wsr a0, EXCCAUSE
// Код трамплина написан по образу и подобию трамплина GDBStub. Надобно его переделать: нет нужды копировать
// данные в Level1 - нужно исправить код, чтобы везде был Level6. А пока...
//
// .. копируем EPC и EXCSAVE с уровня DEBUGLEVEL (Level6) в соответствующие регистры EXCEPTIONLEVEL (level 1)
//
rsr a0,(EPC + XCHAL_DEBUGLEVEL)
wsr a0,EPC_1
// Восстановим наш A0, который был сохранен _DebugExceptionVector, и сохраним его так же в Level1
//
rsr a0,(EXCSAVE + XCHAL_DEBUGLEVEL)
wsr a0,EXCSAVE_1
// Постройка трамплина
// Прерывания с уровнями 4,5,6 и 7 являются высокоприоритетными и их обработчик должен быть
// написан на ассемблере. Т.к. писать ВСЕ на ассемблере нам неохота, мы настраиваем окружение так,
// чтобы можно было вызвать Си функцию и вызываем ее.
//
// Выделяем память на стеке для exception frame. Указатель на эту память будет передан Си обработчику,
// который может менять содержимое фрейма (например, менять значение PC) ,
//
addi sp, sp, -XT_STK_FRMSZ
// Погнали сохранять контекст
// Сохраняем А0 (адрес возврата)
s32i a0, sp, XT_STK_EXIT
s32i a0, sp, XT_STK_A0
// Сохраняем Processor State
rsr a0, PS
s32i a0, sp, XT_STK_PS
// Сохраняем PC (его скопировали на Level1 чуть выше, поэтому EPC_1 а не EPC_6)
rsr a0, EPC_1
s32i a0, sp, XT_STK_PC
// Сохраняем стандартный контекст
call0 _xt_context_save
// Сохраняем SP
addi a7, sp, XT_STK_FRMSZ
s32i a7, sp, XT_STK_A1
// _xt_context_save() не сохраняет A12 и A13. Все приходится делать самим :(.
s32i a12, sp, XT_STK_A12
s32i a13, sp, XT_STK_A13
// Сохраняем EXCCAUSE и VADDR. Они нам не нужны, но пусть будут, для полноты
rsr a0, EXCCAUSE
s32i a0, sp, XT_STK_EXCCAUSE
rsr a0, EXCVADDR
s32i a0, sp, XT_STK_EXCVADDR
rsr a0, EXCSAVE_1 // эта строчка под удаление.
// Устанавливаем PS для вызова Си-функции: обнуляем EXCM, запрещаем прерывания level1..level5
// (т.е. все прерывания, кроме NMI и DEBUGINTERRUPT)
movi a0, PS_INTLEVEL(5) | PS_UM | PS_WOE
wsr a0, PS
// Сохраняем PC. Этот PC будет доступен в Си-функции через frame->pc.
// Его можно менять и тогда, при выходе из обработчика мы прыгнем по новому значению PC
//
rsr a0,(EPC + XCHAL_DEBUGLEVEL)
s32i a0, sp, XT_STK_PC
// Передаем указатель на стек нашему обработчику. На стеке лежит фрейм, который мы только что сформировали.
// После вызова callx4, регистр А6 станет регистром A2 (т.е. - первым параметром функции)
mov a6, sp
rsr a9, EPS_6
s32i a9, sp, XT_STK_PS
// Наконец добрались. Вызываем наш Си-обработчик.
movi a11, debug_exception_handler
callx4 a11
// Восстанавливаем регистр EPC_6 из exception frame->pc
//
l32i a0, sp, XT_STK_PC
wsr a0,(EPC + XCHAL_DEBUGLEVEL)
// Раскручиваем все назад, восстанавливаем все регистры и выходим.
// При выходе управление будет передано по EPC_6
call0 _xt_context_restore
l32i a12, sp, XT_STK_A12
l32i a13, sp, XT_STK_A13
// Адрес возврата (A0)
l32i a0, sp, XT_STK_EXIT
// Стек (A1==SP)
addi sp, sp, XT_STK_FRMSZ
rfi XCHAL_DEBUGLEVEL
#ifdef BUGGY_CHIP
.align 4
debug_di_exc:
// Задержка тут, всего-навсего
movi a0, 243
2:
addi a0, a0, -1
.rept 4
nop
.endr
bnez a0, 2b
// Восстанавливаем адрес возврата (А0) и выходим
rsr a0, EXCSAVE+XCHAL_DEBUGLEVEL
rfi XCHAL_DEBUGLEVEL
#endif // BUGGY_CHIP
// Две строчки ниже - обязательны. Если их удалить, то линкер не будет линковать этот файл к проекту.
.global ld_include_xt_debugexception
ld_include_xt_debugexception:
#endif // __XTENSA__