Зачем?!
Наверное, это первая эмоция большинства людей, прочитавших название статьи. Однако, давайте представим следующую ситуацию: в процессе исследования устройства вы доходите до точки, когда можете исполнить внутри устройства свой код, и хотите вывести через 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$) может показаться достаточно "кусачей" для независимых исследователей, но никто не мешает собрать свой :)
А какой самый необычный способ отладки использовали вы? Предлагаем поделиться своей историей исследований и обсудить их в комментариях.
JerleShannara
Самые «укуренные способы дебага»:
Третье место(третье только потому, что это один из стандартных способов отладки):
Coreboot и его портирование. Когда я пытался добавить(в итоге не получилось — спалил) материнскую плату в этот проект у меня возникла проблема — исполнение не доходило даже до инициализации памяти, зависая где-то ещё на использовании кеша процессора в роли оперативки. При этом SIO полноценно не поднимался, что исключало вывод отладки в COM порт, с отладкой через USB тоже что-то не получилось(толи чипсет в неё не умел, толи у меня слишком мал радиус кривизны рук был). В итоге был применён speaker modem: отладочные сообщения кодировались в сигналы спикера, рядом с ним лежал микрофон, который подключался к другому ПК, на котором уже был запущен декодер этого самого speaker modem-а. Скорость была бесподобной — что-то около 150 бод, но на безрыбье…
Второе место: опять Coreboot, плата другая, но всё весело — в определённом режиме почему-то нормально не работают все прерывания, надо снять дампы того, как настроился чипсет, что выдаёт линуксовое ядро. Но тут возникает проблема — дальше initramfs никуда прогрузиться не получается (т.к. прерывания не работают, sata/usb при этом тоже не работает). Ну ладно, можно с тем минимумом, что нам это даёт получить нужную информацию, да и свой образ собрать. Возникает вопрос: а как это передать, сохранить на флешку/диск — не выходит, COM порт почему-то не работает, сетевая карта… ну тоже тамже. В итоге опытным путём выяснилось, что остался живым NVMe диск т.к. та модель работала через PME прерывания, чтож, cat cool_log.txt>/dev/nvme0n0p1. На IRC канале в тот момент у нас уже шли обсуждения вида «Так, а давайте сейчас в коде инициализации памяти не будем занулять воон тот кусок памяти, загрузимся в битый режим, сделаем вывод в /dev/kmem на этот кусок, потом в нормальном режиме прочитаем.»
Первое место: полностью переделываю прошивку платы, разработчик которой постарался сделать максимально непригодное решение для отладки — подцепиться некуда, UART/SPI/I2C и прочее — отсутствует, плюс от слова «паяльник» у всех инфаркт случается. Зато есть светодиод статуса… азбука Морзе, пришло твоё время!
Pyhesty
светодиод прекрасно превращается в софтварный уарт ;)
JerleShannara
Только если:
1) К нему можно подпаяться/подоткнуться щупом (тут был облом — паяльник ни-ни, шуп нужен был совсем миниатюрный.
2) Он подключен по нормальной схеме (тут был конденсатор зачем-то в параллель) — иначе прощай хоть что-то выше 2400.
Pyhesty
=) снаружи подвешивается фотодиод или оптрон — оптрон заводится в уарт =)
конденсатор выкусывается и 115200, ну или хотя бы 19200 =)
по идее ничего паять не нужно =)
ps: сам раньше светодиоддебагингом занимался, пока не стал даже на 8 ножечных контроллерах любой вывод заводить в уарт… стало на порядок все проще
JerleShannara
От кусачек там тоже инсульт бы случился у заказчика, а с ним там блин фронты такие, что морзянка на уровне только пришедшего школьника в ДОСААФ была.