Программирование DSP Сложные случаи при поиске ошибок Wed, April 24 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Сложные случаи при поиске ошибок Печать
Добавил(а) microsin   

Хорошо, если ошибку в программе можно легко воспроизвести, тогда её найти довольно просто, особенно при наличии JTAG-отладчика. Но как быть, когда ошибка проявляется случайно, или только в определенных условиях?

Недавно столкнулся с нетривиальной ошибкой - прибор нормально запускается в отладчике, но если выключить питание и подождать 2 минуты, то программа ведет себя непредсказуемо - иногда запускается, а иногда зависает. Если сразу передернуть питание, без задержки, то программа запускается нормально.

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

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

LED connect example

#include < cdefBF538.h >
#include < services_types.h >
 
//Тестовый светодиод, подключенный катодом к ножке порта PE6.
//Анод через резистор подключен к +3.3V.
#define portLED     PE6
 
//Макросы для управления светодиодом:
#define OFFLED()    (*pPORTEIO_SET   = portLED)
#define ONLED()     (*pPORTEIO_CLEAR = portLED)
#define LEDCHANGE() (*pPORTEIO_TOGGLE = portLED)
#define LED(on) (on?ONLED():OFFLED())
 
void ConfigureOutputs (void)
{
   //В этом примере настраивается только одна ножка порта
   // для управления светодиодом, но обычно здесь настраиваются
   // и другие выходы.
   *pPORTEIO_FER |= portLED;     //настроить вывод как GPIO.
   *pPORTEIO_DIR |= portLED;     //настроить как выход.
   *pPORTEIO_SET  = portLED;     //==1, погасить светодиод
}
 
#include "delay.h"
 
void main (void)
{
   Initialize();
   ConfigureOutputs();
   ...
   while(1)
   {
      LED(1);
      delay_ms(500);
      LED(0);
      delay_ms(500);
   }
}

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

Комментирование блоков кода. Если есть возможность, то в поиске ошибки следует постепенно убирать из программы куски функций. Идеальный вариант, если действовать по такому сценарию:

1. Убрать половину алгоритма, где наиболее вероятно наличие ошибки.
2. Если после этого ошибка осталась, то следовательно она находится в оставшемся коде. Если нет, то ошибка находится в закомментированном коде. Тогда нужно оставить код, где вероятно ошибка, и где нет ошибки, код закомментировать.
3. Перейти на шаг 1 с оставшимся блоком кода, где вероятно находится ошибка.

В результате этих итераций останется минимальный блок кода, где отловить ошибку становится намного проще.

Вывод сообщений в лог UART. Если есть такая возможность, то это самый информативный и удобный вариант. Вставляйте в код программы вывод информационных сообщений, и тогда можно определить, где произошел сбой. В окне терминала эти сообщения в любом случае останутся, даже если было зависание, сбой по питанию, его выключение и включение, или перезагрузка. Как использовать UART для отладки, см. в [1].

Вывод сообщений в лог памяти. Смысл идеи в том, чтобы организовать в памяти, которая не сбрасывается при запуске отладчика и/или при загрузке программы, специальный лог сообщений. Это может быть двоичный лог или даже текстовый (если позволяет объем памяти). Когда произошло зависание, то нужно будет подключиться к системе отладчиком. Процессор при этом сбросится, но если питание не выключалось, то отладочные сообщения в памяти останутся, и их можно просмотреть в окне BLACKFIN Memory. Ниже во врезке показан пример такой отладки.

Переменные и функция для записи в лог:

#define MEMLOGMSGLEN 16
#define MEMLOGSIZE 2048
#define MEMLOGMASK (MEMLOGSIZE-1)
 
// Структура для одной записи в лог:
typedef struct _TLog
{
   char mess[MEMLOGMSGLEN];   //текстовое сообщение
   VDK::PanicCode pancode;    //код KernelPanic
   VDK::SystemError panerr;   //код системной ошибки (из регистра SEQSTAT)
   int panval;                //код события ошибки
   u32 panpc;                 //адрес, где вероятно возникла ошибка
}TLog;
 
//Переменные, которые определены в не инициализируемой памяти:
#pragma section ("no_init_sdram", NO_INIT)
u32 memlogidx;
#pragma section ("no_init_sdram", NO_INIT)
TLog memlogarr[MEMLOGSIZE];
 
//Функция для записи сообщения в лог:
void memlog (char *memmsg)
{
   u32 idx = memlogidx & MEMLOGMASK;
   int len;
   
   memset(memlogarr[idx].mess, 0, MEMLOGMSGLEN);
   len = strlen(memmsg);
   if (len>MEMLOGMSGLEN)
      len = MEMLOGMSGLEN;
   strncpy(memlogarr[idx].mess, memmsg, len);
   memlogidx++;
}

Пример использования записи в лог в теле функции KernelPanic проекта VDK:

extern "C" void KernelPanic(VDK::PanicCode, VDK::SystemError, const int );
 
void KernelPanic(VDK::PanicCode pc, VDK::SystemError se, const int val)
{
   int *ppc;      //адрес переменной, где находится PanicPC
   u32 idx = memlogidx & MEMLOGMASK;
   
   memlogarr[idx].pancode = pc;
   memlogarr[idx].panerr  = se;
   memlogarr[idx].panval  = val;
   ppc = (int*)0xff80145c;
   memlogarr[idx].panpc   = *ppc;
   memlog("KernelPanic");
}

Адрес переменной 0xff80145c с адресом кода, где произошла паника ядра PanicPC, был вычислен анализом содержимого MAP-файла линкера (см. ниже секцию VDK: обработка KernelPanic).

Пример использования в теле программы:

void main (void)
{
   memlogidx = 0;
   memset(memlogarr, 0, sizeof(TLog)*256);
   
   ConfigureInputs();
   ConfigureOutputs();
   PowerON();
   Init();
memlog("MT Init");
   soundinit();
memlog("MT soundinit");
   
   uartcns = new CUART(UART_CONSOLE);
memlog("MT CUART");
   ...
   while(1)
   {
      ...
   }
}

Просмотр полученного лога в дампе памяти BLACKFIN Memory (просмотр памяти в символьном виде):

BLACKFIN Memory view log

Обработка исключений. В проекте VisualDSP++ можно настроить пользовательский код обработки исключений. Пример обработчика исключений UserExceptionHandler для проекта VDK см. во врезке (также см. статьи [2, 3]).

Ниже приведен код файла ExceptionHandler-BF538.asm из проекта VDK. Здесь используются 2 переменные для сохранения кода ошибки из регистра кода исключения (переменная mySEQSTAT) и адреса, где вероятно возникла ошибка, из регистра RETX (переменная myRETX). В тексте эти переменные выделены жирным шрифтом. Переменные mySEGSTAT и myRETX должны быть глобально определены в коде программы в сегменте памяти, который не инициализирует загрузчик.

/* =============================================================================
 *  Description: здесь находится код обработки исключений (exception handler).
 * ===========================================================================*/
#include "VDK.h"
 
.file_attr prefersMemNum="30";
.file_attr prefersMem="internal";
.file_attr ISR;
 
.section/doubleany data1;
 
/* Декларирование внешних глобальных переменных */
.extern _ExceptionPanic;
 
.section/doubleany L1_code;
 
/******************************************************************************
 * UserExceptionHandler */
 
/* Точка входа в обработчик исключений пользователя (User Exception Handler) */
.GLOBAL     UserExceptionHandler;
UserExceptionHandler:
   /**
    * Здесь обрабатываются исключения...
    *    * Не все аппаратные исключения достигают UserExceptionHandler.
    * VDK резервирует user exception 0, которое обрабатывается кодом
    * VDK exception handler.
    *
    * Определенные исключения обрабатываются runtime-библиотеками
    * менеджера cplb.
    * Исключения, которые в настоящее время передаются кодом VDK в менеджер cplb:
    *   0x23 Data access CPLB protection violation
    *   0x26 Data CPLB miss
    *   0x2C Instruction CPLB miss
    *
    * В зоне Вашей ответственности обработать любые другие исключения
    * (пользовательские или системные), что должно быть сделано в этом коде.
    */
 
   /**
    * Если любое исключение обработано, то любые прямо или косвенно используемые
    * регистры должны быть сохранены и восстановлены.
    * Мы не сохраняем и не восстанавливаем регистры, используемые в вызове
    * ExceptionPanic, потому что приложение будет остановлено без возможности
    * продолжения.    */
 
   /**
    * Сохранение EXCAUSE в R2 как значения для panic
    * Директива .message удаляет информационное сообщение, связанное с
    * one cycle stall
    */
   .message/suppress 1056 for 2 lines;
   R2 = SEQSTAT;
   R2 < < = 26;
   R2 > > = 26;
   
.EXTERN _mySEQSTAT;
   P1.H = HI(_mySEQSTAT);
   P1.L = LO(_mySEQSTAT);
   [P1] = R2;
    
.EXTERN _myRETX;
   P1.H = HI(_myRETX);
   P1.L = LO(_myRETX);
   [--SP] = RETX;
   R2 = [SP++];
   [P1] = R2;
   R1.H = HI(_VDK_kUnhandledExceptionError_);
   R1.L = LO(_VDK_kUnhandledExceptionError_);
   R0 = _VDK_kUnhandledException_;
   CALL.X _ExceptionPanic;
   RTX;
.UserExceptionHandler.end:

Определение глобальных переменных mySEQSTAT и myRETX в коде C/C++ (используется секция памяти no_init_sdram):

#pragma section ("no_init_sdram", NO_INIT)
u32 mySEQSTAT;
#pragma section ("no_init_sdram", NO_INIT)
u32 myRETX;

Пример определения секции no_init_sdram (кусок LDF-файла проекта):

PROCESSOR p0
{
   ...
   SECTIONS
   {
      ...
      /*$VDSG< insert-new-sections-at-the-start >                 */
      no_init_sdram NO_INIT
      {
         INPUT_SECTION_ALIGN(4)
         INPUT_SECTIONS($OBJECTS(no_init_sdram))
      } > MEM_SDRAM0_BANK3
      /*$VDSG< insert-new-sections-at-the-start >                 */
      ...
   } /* SECTIONS */
} /* p0 */

VDK: обработка KernelPanic. Можно переопределить код библиотечной функции KernelPanic, чтобы она выполняла какие-либо реально полезные действия (каким-либо образом сообщала об ошибке и выдавала соответствующую информацию). Пример:

extern "C" void KernelPanic(VDK::PanicCode, VDK::SystemError, const int );
 
void KernelPanic(VDK::PanicCode paniccode, VDK::SystemError se, const int val)
{
   int *ppc;      //адрес для PanicPC
   u32 idx = memlogidx & MEMLOGMASK;
   
   memlogarr[idx].pancode = pc;
   memlogarr[idx].panerr  = se;
   memlogarr[idx].panval  = val;
   ppc = (int*)0xff80145c;
   memlogarr[idx].panpc   = *ppc;
   memlog("KernelPanic");
}

В эту функцию VDK передает следующие значения:

paniccode код ошибки VDK [4]. Расшифровку этих ошибок см. в документации VDK (Kernel) User’s Guide, или в заголовочных файлах VisualDSP++.
se содержимое регистра SEQSTAT (код системной ошибки). Расшифровку кодов регистра исключений см. в даташите на процессор, также см. для процессора Blackfin таблицу 1 из Приложения A статьи [2].
val дополнительное значение, описывающее событие ошибки. Его расшифровка зависит от значения кода первого параметра paniccode.

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

Обратите внимание на интересную деталь: через абсолютный адрес 0xFF80145C подпрограмма получает значение из переменной PanicPC библиотеки VDK, что позволяет узнать адрес памяти, на котором произошел сбой кода VDK. Значение адреса переменной PanicPC можно узнать из карты памяти, которую генерирует линкер (XML-файл, который можно удобно просмотреть программой XML Notepad). Подробнее про то, как задать в свойствах проекта генерацию карты памяти программы и как смотреть полученные MAP-файлы в формате XML, см. в FAQ [5] вопрос "Q070. VisualDSP: как посмотреть MAP-файл линкера?". Кусок карты памяти, где можно узнать адрес этой переменной:

 Input section vdk-i-BF532.dlb[Initialize.doj](data1) :0x0 

SymbolDemangled nameAddressSizeBinding
_vdkMainMarker vdkMainMarker 0xff80144c 0x1 GLOBAL
_g_KernelPanicCode__3VDK VDK::g_KernelPanicCode 0xff801450 0x4 GLOBAL
_g_KernelPanicError__3VDK VDK::g_KernelPanicError 0xff801454 0x4 GLOBAL
_g_KernelPanicValue__3VDK VDK::g_KernelPanicValue 0xff801458 0x4 GLOBAL
_g_KernelPanicPC__3VDK VDK::g_KernelPanicPC 0xff80145c 0x4 GLOBAL
_g_remainingTicks__3VDK VDK::g_remainingTicks 0xff801460 0x4 GLOBAL
_g_VdkIsInitialized__3VDK VDK::g_VdkIsInitialized 0xff801464 0x1 GLOBAL

VDK: обработка ошибок потоков. Автоматически создаваемые классы потоков VDK содержат шаблон функции ErrorHandler для обработки ошибок потока. Достаточно вставить в неё какой-либо код, который сообщает об ошибке (можно управлять светодиодом или вывести сообщение в лог).

int mainthread::ErrorHandler()
{
memlog("Ошибка потока mainthread!");
   return (VDK::Thread::ErrorHandler());
}

В Debug программа работает, а в Release нет. Это частый случай, когда в программе есть ошибочная привязка к ресурсам памяти или ресурсу времени выполнения процессора. Однако обычная пошаговая отладка по исходному коду в Release тут не поможет, и просмотреть содержимое переменных по их имени тоже нельзя (по причине работы оптимизатора кода). Как быть, как просмотреть значения нужных переменных в памяти?

Не все так плохо - можно воспользоваться картой памяти, которую генерирует линкер, и по ней найти абсолютные адреса переменных. Дальше все просто - при запущенной сессии отладки остановите программу в любом месте, и просмотрите значение нужной переменной в дампе памяти BLACKFIN Memory.

Процесс по шагам:

1. Настройте для конфигурации Release генерацию MAP-файла линкера. Как это сделать, см. в FAQ [5] вопрос "Q070. VisualDSP: как посмотреть MAP-файл линкера?"

2. После того, как скомпилировали проект, откройте полученный файл карты памяти с помощью XML Notepad, перейдите на закладку просмотра содержимого файла XSL Output.

3. Нажмите комбинацию клавиш Ctrl+F (запуск поиска строки) и введите в строке поиска адрес переменной, которую нужно найти.

Ниже на скриншоте показан процесс поиска адреса переменной KernelPanicPC, который содержит адрес, где произошел сбой проекта VDK.

XML Notepad find variable in linker MAP file

[Дополнительные замечания]

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

1. Компромисс между рутинной работой, когда необходимо решить конкретные текущие задачи, и выбором времени для самообразования.
2. Компромисс при выборе рабочей платформы - необходимо рассмотреть разные варианты в плане цены, достаточного количества ресурсов (с учетом запаса на развитие системы и отладку) и удобства использования.
3. Компромисс при выборе языка программирования - можно выбрать более удобный и знакомый язык, или можно выбрать более перспективный, или более эффективный.
4. Компромисс между размером кода программы (и/или данных) и быстродействием.
5. Компромисс между строгим соблюдением спецификаций при работе с оборудованием и обеспечением нужного быстродействия.
6. Компромисс между эффективностью кода и удобством в его поддержке.
7. Компромисс между кодированием и правильным оформлением кода и документированием своих действий.
8. Компромисс между выбором времени на кодирование и тестирование с исправлением ошибок.
9. Компромисс при выборе библиотек программирования. Можно работать с железом напрямую, а можно ускорить разработку, применяя библиотеки и системы распределения времени процессора (RTOS).

Конечно, мы люди-человеки, поэтому совершаем и всегда, и неизбежно будем совершать ошибки. Особенно это касается работы программиста. В наших силах только стараться избегать ошибок, и стараться их исправлять в любом случае, даже при минимальном подозрении на ошибку. Приведу несколько примеров проблем, вызванных недостаточным вниманием к деталям при разработке.

Случай 1. При разработке очень редко возникали ситуации, когда программа неожиданным образом зависала и не запускалась. Из-за того, что эти случаи были очень редки, я не обращал на них внимание. Программа была уже практически готова, когда на новых экземплярах приборов ошибка стала проявляться намного чаще, и в полный рост заявила о себе проблема - прибор то работает, то не работает. Причина оказалась в переполнении буфера из-за шума на входе UART, когда входной буфер мог переполниться, что проявлялось случайным образом в определенных условиях. В этом случае неправильно был выбран компромисс 8. Если бы ошибка была устранена на начальном этапе проектирования, то многих проблем удалось бы избежать, потому что в сложной программе, когда она уже почти написана, искать ошибку труднее.

Случай 2. Для того, чтобы ускорить работу с графическим индикатором, я пренебрег соблюдением диаграмм времени при управлении выборкой ~CS индикатора. Вместо того, чтобы управлять этим сигналом при передаче пакетов данных, я навсегда выбрал индикатор, жестко подав лог. 0 на сигнал ~CS. Результат получился примерно такой же, как в предыдущем случае - в определенных условиях программа работает, а иногда по совершенно непонятным причинам не работает. В этом случае неправильно был выбран компромисс 5, поиск и устранение этой ошибки занял много времени и сил. Надо было выбрать путь точного соблюдения спецификации.

[Ссылки]

1. Blackfin: форматированный вывод в окно терминала через UART.
2. EE-307: советы по отладке для Blackfin.
3. EE-312: построение сложных LwIP-приложений на основе VDK Blackfin.
4. VDK: коды и значения ошибок.
5. Blackfin FAQ.

 

Добавить комментарий


Защитный код
Обновить

Top of Page