Зачем?!
Наверное, это первая эмоция большинства людей, прочитавших название статьи. Однако, давайте представим следующую ситуацию: в процессе исследования устройства вы доходите до точки, когда можете исполнить внутри устройства свой код, и хотите вывести через UART заветное "Hello, Habr!", помигать светодиодами на плате как на новогодней ёлке или включить JTAG, но всего этого в вашем распоряжении нет.
В этой статье мы покажем необычный способ трассировки прошивки устройств с помощью эмулятора SPI-флешек.
Введение
В статье Реверс USB-SATA-адаптера (история одного стажера) мы уже рассказывали об SPI-эмуляторе EM100-Pro. Основное его применение — это имитировать работу различных SPI-flash микросхем памяти, а также сохранять лог трафика на SPI шине. Это очень удобно в случае необходимости частого изменения содержимого памяти, поскольку не нужно выпаивать микросхемы с платы или подключаться специальными клипсами. Достаточно изменить образ памяти и нажать в GUI эмулятора кнопку загрузки.
Так что же с трассировкой через SPI? В эмуляторе есть и такая функция. Она почему-то не очень афишируется в руководстве, а в единственной презентации, которую удалось найти, сказано, что спецификацию можно запросить по почте у компании-разработчика. Не очень удобно, правда?
Расскажем как это работает.
Теперь обо всем по порядку
SPI-эмулятор предназначен для быстрой смены прошивки или конфигурации, хранящихся во внешней SPI-флешке устройства. Помимо своего прямого назначения эмулятор умеет взаимодействовать с мастер-устройством с помощью дополнительного протокола, обеспечивающего поддержку отладочных функций. Для тестирования данных возможностей был собран лабораторный стенд.
На отладочной плате SM32F4DISCOVERY включен интерфейс SPI1 и к этим выводам подключен эмулятор. Естественно, в реальных исследованиях эмулятор подключается непосредственно вместо SPI-Flash ПЗУ.
Из прочитанной документации понятно, что можно управлять SPI-эмулятором с микроконтроллера. Ниже приведена таблица с доступными командами и их форматом.
Для организации отладочного интерфейса нам понадобится команда Write uFIFO. uFIFO — буфер размером 512 байт внутри эмулятора, который служит для передачи данных от микроконтроллера к ПК. Данный буфер может заполняться данными, а с ПК можно его читать. Самое интересное, что если заполнять uFIFO данными специального формата, то GUI эмулятора автоматически их распознает. Ниже в таблице представлены формат пакетов и типы данных.
В общем виде посылка данных в uFIFO выгладит следующим образом:
techCmd | CMD write uFIFO | preByte1 | preByte2 | preByte3 | preByte4 | typeData | lenData | Data..... | |
---|---|---|---|---|---|---|---|---|---|
11h | don't care | C0h | 40h | 44h | 36h | 47h | 1-7 | х |
В качестве магической последовательности в компании Dediprog решили использовать слово "@D6G", явно намекающее на английское "debug".
Режим ASCII
Для начала было очень интересно заменить UART. Обычно туда выбрасываются некоторые отладочные строки или трассируется выполнение программы. Из таблицы выше видно, что ASCII строки распознаются эмулятором, если записать в uFIFO в поле Data Type код 05h. Ниже приведен листинг, позволяющий организовать вывод текстовой информации в ASCII-кодировке.
// Функция записи данных в uFIFO
void SpiFlash_WriteUFifo(unsigned int count, unsigned char* buffer)
{
// 0x11 - custom-команда эмулятора
// 0x00 - don't care byte
// 0xC0 - выбираем запись в uFIFO
char writeFifoCmd[3] = {0x11, 0x00, 0xC0};
CSEnable();
HAL_SPI_Transmit(&hspi1, writeFifoCmd, sizeof(writeFifoCmd), HAL_MAX_DEALY);
HAL_SPI_Transmit(&hspi1, buffer, count, HAL_MAX_DEALY); // Отправляем данные
CSDisable();
}
// Функция отправки ASCII строки
void SpiFlash_TxString(char* str)
{
// preamble data packet, 0x05 - ASCII type
char premsg[6] = {0x40, 0x44, 0x36, 0x47, 0x05};
premsg[5] = strlen(str);
Flash_WaitBusy();
SpiFlash_WriteUFifo(sizeof(premsg), premsg); // Отправляем "магию"
SpiFlash_WriteUFifo(strlen(str), str); // Выводим строку
}
// Вызов функции
SpiFlash_TxString("Hello, Habr!\n\0");
SpiFlash_TxString("Program start!\n\0");
SpiFlash_TxString("While loop:\n\0");
Компилируем, прошиваем МК, включаем GUI эмулятора, запускаем SPI Hyper Terminal и при старте работы МК видим в терминале наши строки.
Режим HEX
Конечно же, эмулятор поддерживает вывод не только ASCII-данных. С его помощью можно организовать отображение, например, содержимого внутренней Flash-памяти МК. Для этого нужно в поле Data Type установить тип данных 04h. Ниже показан код для отправки данных в формате HEX.
// Функция отправки HEX данных
void SpiFlash_TxHexArray(unsigned char* hexArray, unsigned int count){
char premsg[6] = {0x40, 0x44, 0x36, 0x47, 0x04};
premsg[5] = count;
Flash_WaitBusy();
SpiFlash_WriteUFifo(sizeof(premsg), premsg);
SpiFlash_WriteUFifo(count, hexArray);
}
// Небольшой массив-счетчик
unsigned char array[0x10];
for(int i = 0; i < 0x10; i++)
{
array[i] = i;
}
SpiFlash_TxString("Array dump:\0");
SpiFlash_TxHexArray(array, 0x10);
// Дампим внутреннюю флешку МК
Spi_Flash_TxString("Memory dump:\0");
Spi_Flash_TxHexArray((char*)0x08000000, 0x10);
На рисунке ниже показана работа кода, а также окно из ST-LINK Utility. Можно наблюдать некоторые различия в выводе информации. Так происходит из-за того, что внутри STM32 находится ядро Cortex-M, в котором информация хранится в little-endian.
Режим Checkpoint
Нужен для добавления контрольных точек в отладочную информацию, и позволяет организовать удобную и компактную трассировку исполнения функции. В этом случае ПО эмулятора будет искать в корневой директории файл "Checkpoint.txt" в ini-формате, содержащий имена контрольных точек. При этом в окне ПО откроется дополнительный листинг контрольных точек, позволяющий контролировать последовательность исполнения кода и выполнять быструю навигацию в отладочной консоли.
// Установка checkpoint-a
void SpiFlash_SetCheckPoints(unsigned char* points, unsigned char countPoints)
{
char premsg[6] = {0x40, 0x44, 0x36, 0x47, 0x01};
premsg[5] = countPoints;
Flash_WaitBusy();
SpiFlash_WriteUFifo(sizeof(premsg), premsg);
SpiFlash_WriteUFifo(countPoints, points);
}
//Вызов функции
unsigned char checkPoint[1];
checkPoint[0] = 0x1;
SpiFlash_SetCheckPoints(checkPoint, 1);
//...немного кода
checkPoint[0] = 0x2;
SpiFlash_SetCheckPoints(checkPoint, 1);
//...немного кода
checkPoint[0] = 0x3;
SpiFlash_SetCheckPoints(checkPoint, 1);
Теперь достаточно в любом месте микропрограммы вызвать функцию SpiFlash_SetCheckPoints(checkPoint, 1)
, и спокойно трассировать поток исполнения.
Режим Lookup table
В качестве дополнительного инструмента для отладки можно использовать Lookup table. Данный режим является симбиозом режимов ASCII и Checkpoint. Он позволяет подтягивать заранее заготовленные сообщения из файла, выбираемого в конфигурации GUI, и добавлять к ним динамическую строчку из вызываемого в МК кода. Такой подход экономит драгоценное место в МК и позволяет, например, показывать текущие значения переменных, обрамляя их информативной подписью.
// Функция вывода строки из справочной таблицы
void SpiFlash_LookUpTable(uint16_t index, uint16_t AsciiChar)
{
char premsg[10] = {0x40, 0x44, 0x36, 0x47, 0x07, 0x04};
premsg[6] = (index >> 8) & 0xFF;
premsg[7] = index & 0xFF;
premsg[8] = (AsciiChar >> 8) & 0xFF;
premsg[9] = AsciiChar & 0xFF;
Flash_WaitBusy();
SpiFlash_WriteUFifo(10, premsg);
}
// Вызов функции
uint16_t varShow = 0x3031; // ASCII: 01
SpiFlash_LookUpTable(0x0, varShow); // Выбрать 0-ю строку из таблицы
varShow++; // Increment-> ASCII: 02
SpiFlash_TxString("IncrementVar\n\0");
SpiFlash_LookUpTable(0x0, varShow);
SpiFlash_TxString("Reg2:\n\0");
SpiFlash_LookUpTable(0x1, 0x3535); // Выбрать 1-ю строку из таблицы (ASCII: 55)
SpiFlash_TxString("LookUpDone\n\0");
На рисунке ниже мы вывели текущее значение переменной varShow
, добавив к ней соответствующую подпись. На наш взгляд, данному режиму не хватает поддержки HEX-режима вывода данных.
Режим Timestamp
Бывает так, что необходимо не просто трассировать поток выполнения, но и замерять время, которое уходит на выполнение блока кода. Для таких случаев в портфолио эмулятора есть специальный режим Timestamp, поддерживающий вывод временных меток. Пробуем режим, предварительно запустив на МК таймер. Если настроить инкрементацию переменной, связанной с таймером так, чтобы она увеличивалась на 1 каждые 10 нс, то тогда GUI эмулятора будет отображать время в реальном масштабе. Ниже приведен код функции, которая создает в буфере uFIFO time stamp.
// Функция отправки временной метки
void SpiFlash_TxTimeStamp(int timeValue)
{
char premsg[] = {0x40, 0x44, 0x36, 0x47, 0x06, 0x04};
Flash_WaitBusy();
SpiFlash_WriteUFifo(sizeof(premsg), premsg);
unsigned char timeBuffer[4];
timeBuffer[0] = (timeValue >> 24) & 0xFF;
timeBuffer[1] = (timeValue >> 16) & 0xFF;
timeBuffer[2] = (timeValue >> 8) & 0xFF;
timeBuffer[3] = timeValue & 0xFF;
SpiFlash_WriteUFifo(sizeof(timeBuffer), timeBuffer);
}
// Вызов функции
SpiFlash_TxTimeStamp(100000000); // 100000000 * 10 нс = 1 сек
Во время тестового вывода в функцию SpiFlash_TxTimeStamp
передается значение 100000000 отсчетов, умножаем на 10 нс и получаем 1 сек. Таким образом создается временная метка, соответствующая 1 секунде.
Период 10 нс соответствует частоте 100000000 Гц = 100МГц, которая может не поддерживаться вашим устройством. В этом случае достаточно пересчитать нашу магическую константу в соответствии с вашими потребностями.
Подводя итог
В этой небольшой статье мы попытались описать необычный способ трассировки устройств, в которых нет привычных всем UART или JTAG. Конечно же, способ не универсален, так как не все объекты исследования имеют на борту SPI-флешку или хотя бы сам интерфейс. К тому же, стоимость используемого нами эмулятора (~600$) может показаться достаточно "кусачей" для независимых исследователей, но никто не мешает собрать свой :)
А какой самый необычный способ отладки использовали вы? Предлагаем поделиться своей историей исследований и обсудить их в комментариях.