Программирование DSP VDK FAQ Fri, June 23 2017  

Поделиться

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

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


VDK FAQ Печать
Добавил(а) microsin   

Здесь помещены ответы на различные вопросы, которые мне пришлось решать в процессе изучения VDK RTOS.

Если еще не создан поток нужного типа, то нужно создать тип потока. Это делается в проекте VDK, закладка Kernel, раздел Threads -> кликните правой кнопкой на Thread Types, выберите New Thread Type.

Создавая новый тип потока, Вы можете этим добавить в приложение какой-то новый алгоритм. Есть возможность создавать потоки путем настройки проекта VDK, и также можно создавать потоки во время работы программы.

[Автоматическое создание потока во время запуска приложения VDK]

Зайдите в раздел раздел Threads -> кликните правой кнопкой на Boot Threads, выберите New Boot Thread. После этого действия выберите для созданного потока нужный тип в его выпадающем списке Thread Type.

Этим действием Вы конфигурируете приложение VDK на автоматическое создание и автоматический запуск потока во время запуска приложения VDK (именно поэтому такой поток называется Boot Thread).

[Программное (run-time) создание потока (CreateThread)]

Альтернативно поток можно создать и запустить динамически из тела другого, уже работающего потока (например, из потока Boot Thread). Это делается стандартными средствами языка C++ как создание объекта с типом, который был создан на шаге 1, и с помощью вызовов VDK API. Пример создания потока на языке C:

VDK_ThreadID ConsoleIOThread;
ConsoleIOThread = VDK_CreateThread (kConsoleIOThread);

Пример создания потока на языке C++:

VDK::CreateThread(kWriterThread);

Примечание: в этих примерах в функции создания потока передаются числовые идентификаторы типа потока (kConsoleIOThread и kWriterThread), которые можно подсмотреть в автоматически генерируемом файле VDK.h проекта (см. перечисление ThreadType).

Также имеется расширенное создание потока (функции CreateThreadEx и CreateThreadEx2), в которые передается структура для создания типа потока.

[CreateThreadEx]

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

Прототип функции на языке C:

VDK_ThreadID VDK_CreateThreadEx (VDK_ThreadCreationBlock *inOutTCB);

И на языке C++:

VDK::ThreadID VDK::CreateThreadEx (VDK::ThreadCreationBlock *inOutTCB);

Все параметры передаются в функцию через структуру ThreadCreationBlock. Вот эта структура (здесь приведено описание на языке C++, а на языке C описание аналогично, просто префиксы VDK:: заменяются на VDK_):

typedef struct VDK::ThreadCreationBlock
{
   VDK::ThreadType template_id;
   VDK::ThreadID thread_id;
   unsigned int thread_stack_size;
   VDK::Priority thread_priority;
   void *user_data_ptr;
   struct VDK::ThreadTemplate *pTemplate;
   unsigned int *stack_pointer;
} VDK::ThreadCreationBlock;

Описание полей структуры:

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

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

thread_stack_size если этот параметр не равен 0, то он задает выраженный в 32-битных словах размер стека, который отменит значение по умолчанию, заданное для данного типа потока.

thread_priority если не равно 0, то переназначит приритет по умолчанию для потока этого типа.

user_data_ptr этот параметр позволяет передать конструктору потока (без какой-либо интерпретации) произвольное значение. Как это значение будет интерпретировано в коде конструктора - целиком отвественность программиста. Это может быть, к примеру, указатель на некий блок презназначенных для потока данных. Такой подход позволяет создавать несколько экземпляров потока одинакового типа, поведение которых будет зависеть от переданных в поток данных (параметризация потока во время выполнения), без необходимости использовать для этого какие-то глобальные переменные.

pTemplate указатель на шаблон потока, который используется для генерации потока. Этот параметр требуется только в том случае, если template_id установлен в значение kDynamicThreadType. Размер стека и начальный приоритет (опционально) переназначается значениями, указанными в полях thread_stack_size и thread_priority.

stack_pointer указатель на определенную пользователем область памяти для стека потока размером thread_stack_size. Это поле игнорируется в вызове CreateThreadEx(). На процессорах Blackfin библиотека VDK сохраняет контекст в стеке и, таким образом, выделенная область памяти должна быть размером stack_size + context_size, где значение context_size может быть получено вызовом API-функции GetContextRecordSize().

Есть также функция CreateThreadEx2(), но она используется довольно редко, потому что предназначена для поддержки функционала новых версий API, который возможно появится в будущем. Подробнее см. [2].

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

Термины "необслуживаемый регион" и "критический регион" относятся к многопоточной среде выполнения (VDK это как раз такая система), когда имеются несколько работающих независимо потоков. При этом требуется достичь атомарности (защиты) доступа к каким-то общим переменным в памяти со стороны разных потоков. Именно для обеспечения атомарности доступа эти регионы и предназначены. Защищать необходимо те переменные, которые состоят из нескольких элементарных единиц данных, которые нельзя изменить одной неделимой инструкцией или действием (подробнее про атомарность доступа к общим данных см. Википедию).

Необслуживаемый регион кода (unscheduled region). "Необслуживаемый" отностится к планировщику: т. е. во время работы необслуживаемого региона работа планировщика запрещена, чем обеспечивается гарантия, что выполнение необслужваемого региона текущего потока не будет вытеснено другим потоком. Однако обработчик прерывания может прервать выполнение необслуживаемого региона кода. Таким образом, необслуживаемый регион может обеспечить атомарность доступа только когда общие данные используются между потоками, и обработчики прерывания эти данные не используют.

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

Критический регион кода (critical region). Критический регион кода - это такой участок кода, во время выполнения которого прерывания запрещены. Этим обеспечивается полная атомарность, т. е. критическим регионом может обеспечиваться защита общих данных как для потоков, так и для прерываний. Критическим регионом также считается обработчик прерывания, для которого не разрешена вложенность прерываний.

PushCriticalRegion() запрещает прерывания (вход в критический регион), и вызов PopCriticalRegion() заново разрешает прерывания (выход из критического региона).

Подробнее про защищенные регионы см. раздел "Protected Regions" документации VisualDSP++ 5.0 Kernel (VDK) User’s Guide [2].

Окно View -> VDK Windows -> Status предоставляет информацию для расшифровки состояния ядра VDK (VDK Kernel Status). Необработанные исключения VDK (Unhandled VDK exceptions) перехватываются через KernelPanic, и среда VisualDSP++ показывает код (Code), ошибку (Error), связанное с ошибкой значение (Value) и программный счетчик (PC).

VDK Status kUnhandledException

Когда выполнение попадает в Kernel Panic с кодом ошибки kUnhandledException, то это означает, что обработчик по умолчанию UserExceptionHandler попытался обработать исключение, но не нашел код, который соответствовал бы этому исключению. 

В этом случае обработчик сохранит значение регистра EXCAUSE в поле Value ошибки (в данном примере, показанном на скриншоте, 0x2a = Instruction Fetch Misaligned Address Violation, т. е. нарушение выравнивание адреса при попытке выборки инструкции). Просмотрите Program Sequencer: секцию Hardware Errors and Exception Handling руководства по программированию процессора (Blackfin Processor Programming Reference Manual, которое можно найти на сайте analog.com). Коды ошибок в регистре EXCAUSE можно найти в Приложении A статьи [4].

Для других случаев ошибки Kernel Panic имеется таблица (Table 5-22. VDK API Error Codes and Error Values) руководства VDK (VDK Manual), с описанием функций, вызов которых может привести к ошибке с данным кодом, и что при этом сохраняется в поле Value. См. раздел VDK User's Guide -> VDK API Reference -> VDK Error Codes and Error Values.

Код ошибки равен сумме двух кодов 0x00050000 + 0x00000009. Первое слагаемое соответствует службе, в которой возникла ошибка, а второе слагаемое определяет тип ошибки. Код 0x00050000 соответствует ADI_INT_ENUMERATION_START, т. е. код возврата соответствует ошибке Менеджера Прерываний. Значит, расшифровку кода 0x00000009 следует искать в файле adi_int.h, и из него можно узнать, что этот код соответствует ADI_INT_RESULT_DISALLOWED_BY_RTOS (RTOS being targeted disallowed the request).

Поскольку функция adi_int_CECHook задает привязку обработчика прерывания на заданный уровень IVG, то очевидно, что RTOS запрещает привязку обработчика прерывания.

У VDK нет ограничений на использование пользователем DMA. С обработкой прерываний это не так. Имейте в виду, что когда используете VDK, библиотеки SSL специально собраны для ядра VDK, что и приводит к ошибке ADI_INT_RESULT_DISALLOWED_BY_RTOS, когда вручную делается привязка обработчика к уровням IVG. Способ, которым устанавливаются обработчики прерываний в VDK, отличается от того, как обработчики прерываний устанавливаются в стандартном приложении на языке C (т. е. не VDK-приложении). Если в стандартном (не VDK) приложении обработчик прерывания подцеплается вызовом функции adi_int_CECHook, то в приложении VDK все прерывания должны быть сконфигурированы на закладке настроек VDK Kernel.

VDK Interrupts context menu fig324

В Приложении к руководству VDK можно найти список прерываний, которые зарезервированы для использования ядром. Подробнее про добавление и редактирование прерываний см. [5].

Пример такого сообщения после запуска сборки:

[Error li1021]  The following symbols referenced in processor 'p0' could not be resolved:
        'VDK::g_SemaphoreHeap [_g_SemaphoreHeap__3VDK]' referenced from 'vdk-i-BF532.dlb[VDK_API_CreateSemaphore.doj]'
        'VDK::g_SemaphoreHeap [_g_SemaphoreHeap__3VDK]' referenced from 'vdk-i-BF532.dlb[VDK_API_DestroySemaphore.doj]'

Причина в том, что в свойствах VDK-проекта на закладке Kernel было задано нулевое максимальное количество семафоров (см. Semaphores -> Maximum Active Semaphores), но в коде программы использовались функции службы семафоров, такие как adi_sem_Init и adi_dev_Terminate. Укажите для этого свойства ненулевое значение (например 10) и ошибка исчезнет.

В документации [6] написано, что по умолчанию частота указывается в Гц, и чтобы указать частоту в мегагерцах, должна использоваться команда ADI_PWR_CMD_SET_FREQ_AS_MHZ. Но в примерах приложений VDK команда ADI_PWR_CMD_SET_CLKIN используется вместе с параметром частоты в мегагерцах, причем для этого команда ADI_PWR_CMD_SET_FREQ_AS_MHZ не используется. Как оказалось, функция adi_pwr_Init автоматически распознает единицы частоты, которые указываются с командой ADI_PWR_CMD_SET_CLKIN. Например, вот эта команда установит частоту кварца на 16 МГц (частота указывается в мегагерцах):

static ADI_PWR_COMMAND_PAIR power_settings[] =
{
   { ADI_PWR_CMD_SET_PROC_VARIANT, (void*)ADI_PWR_PROC_BF538BBCZ400 },
   { ADI_PWR_CMD_SET_PACKAGE,      (void*)ADI_PWR_PACKAGE_MBGA      },
   { ADI_PWR_CMD_SET_VDDEXT,       (void*)ADI_PWR_VDDEXT_330        },
   { ADI_PWR_CMD_SET_CLKIN,        (void*)16                        },
   { ADI_PWR_CMD_END, 0}
};

В этом примере частота устанавливается в значение 16384000 Гц (частота указывается в Герцах).

static ADI_PWR_COMMAND_PAIR pkrcm_mini[] =
{
   { ADI_PWR_CMD_SET_PROC_VARIANT, (void*)ADI_PWR_PROC_BF538BBCZ400 },
   { ADI_PWR_CMD_SET_PACKAGE,      (void*)ADI_PWR_PACKAGE_MBGA      },
   { ADI_PWR_CMD_SET_VDDEXT,       (void*)ADI_PWR_VDDEXT_330        },
   { ADI_PWR_CMD_SET_CLKIN,        (void*)16384000                  },
   { ADI_PWR_CMD_END, 0}
};

Т. е. частота CLKIN мсжет указываться для команды ADI_PWR_CMD_SET_CLKIN и в Герцах, и в мегагерцах, формат единиц частоты определяется функцией adi_pwr_Init автоматически.

Это сообщение означает, что в настройках ядра разрешено использовать до 10 сообщений (закладка Kernel -> Messages -> Maximum Messages), но нет ни одного типа потока, где бы было разрешено использование сообщений.

Если пока что в VDK-проекте сообщения не используются, то можно не обращать внимания на это сообщение. Если же использование сообщений необходимо, то нужно для соответствующих типов потоков разрешить сообщения (закладка Kernel -> Threads -> Thread Types -> раскройте дерево свойств у типа потока -> измените свойство Message Enabled на true).

1. Откройте текстовым редактором файл *.ldf, который находится в корне проекта. Этот файл автоматически создал Мастер проекта VDK, когда Вы создавали проект. Он может называться, например, VDK-BF538.ldf (если Вы создали VDK-проект для процессора ADSP-BF538).

2. Найдите в этом файле секцию MEMORY, где определяются диапазоны адресов для для разных областей памяти. В конце секции MEMORY есть определения диапазонов адресов для банков памяти SDRAM. Отредактируйте адреса так, чтобы они соответствовали реально установленной в системе внешней памяти SDRAM. Например, вот так может выглядеть настройка для системы, у которой на борту 32 мегабайта памяти SDRAM (выделено жирным шрифтом):

...
MEMORY
{
   mem_l1_scratch      { TYPE(RAM) START(0xFFB00000) END(0xFFB00FFF) WIDTH(8) }
 
#if INSTR_CACHE
   mem_l1_code_cache   { TYPE(RAM) START(0xFFA10000) END(0xFFA13FFF) WIDTH(8) }
   mem_l1_code         { TYPE(RAM) START(0xFFA00000) END(0xFFA0FFFF) WIDTH(8) }
#else
   mem_l1_code         { TYPE(RAM) START(0xFFA00000) END(0xFFA13FFF) WIDTH(8) }
#endif#if DATAB_CACHE 
   mem_l1_data_b_cache { TYPE(RAM) START(0xFF904000) END(0xFF907FFF) WIDTH(8) }
   mem_l1_data_b       { TYPE(RAM) START(0xFF900000) END(0xFF903FFF) WIDTH(8) }
#else 
   mem_l1_data_b       { TYPE(RAM) START(0xFF900000) END(0xFF907FFF) WIDTH(8) }
#endif
#if DATAA_CACHE 
   mem_l1_data_a_cache { TYPE(RAM) START(0xFF804000) END(0xFF807FFF) WIDTH(8) }
   mem_l1_data_a       { TYPE(RAM) START(0xFF800000) END(0xFF803FFF) WIDTH(8) }
#else 
   mem_l1_data_a       { TYPE(RAM) START(0xFF800000) END(0xFF807FFF) WIDTH(8) }
#endif 
 
    /* Контроллер внешней шины (EBIU) процессора ADSP-BF538 позволяет применить
    ** до 4 подбанков, к которым можно обращаться одновременно.
    ** Настройки LDF, делящие доступное пространство SDRAM на 4 или 8 банков,
    ** позволяют использовать EBIU наиболее эффективно, снижая количество
    ** пустых циклов ожидания (stall cycles) при доступе к SDRAM. Обычно
    ** банки распределяют следующим образом: bank0 задействуют для кучи (heap),
    ** bank1 для данных (data), bank2 для обнуляемых при старте данных (data/bsz),
    ** bank3 для кода программы (program).
    ** Для дополнительной информации по аппаратному конфигурированию памяти
    ** см. ADSP-BF538 Hardware Reference Manual,секцию, посвященную контроллеру
    ** SDRAM.    */
    /*
    mem_sdram0_bank0  { TYPE(RAM) START(0x00000004) END(0x00FFFFFF) WIDTH(8) }
    mem_sdram0_bank1  { TYPE(RAM) START(0x01000000) END(0x01FFFFFF) WIDTH(8) }
    mem_sdram0_bank2  { TYPE(RAM) START(0x02000000) END(0x02FFFFFF) WIDTH(8) }
    mem_sdram0_bank3  { TYPE(RAM) START(0x03000000) END(0x03FFFFFF) WIDTH(8) }
    */
    mem_sdram0_bank0  { TYPE(RAM) START(0x00000004) END(0x007FFFFF) WIDTH(8) }
    mem_sdram0_bank1  { TYPE(RAM) START(0x00800000) END(0x00FFFFFF) WIDTH(8) }
    mem_sdram0_bank2  { TYPE(RAM) START(0x01000000) END(0x017FFFFF) WIDTH(8) }
    mem_sdram0_bank3  { TYPE(RAM) START(0x01800000) END(0x01FFFFFF) WIDTH(8) }
} /* MEMORY */
...

3. Задайте в проекте глобальную опцию USE_SDRAM для препроцессора линкера. Это делается в настройках проекта, Project Options... -> Link -> LDF Preprocessing, добавьте в поле ввода через запятую текст USE_SDRAM.

VDK USE SDRAM option

4. Для размещения переменных в SDRAM используйте директиву section с именами секций sdram0_bank0, sdram0_bank1, sdram0_bank2, sdram0_bank3. Пример размещения массива в bank1 внешней памяти SDRAM:

section ("sdram0_bank1") u8 bufTXdebug[UARTDEBUG_TXBUFSIZE];

Предупреждение линкера li2060 (The following input section(s) ... not been placed into the executable...) и ошибка линкера li1060 может быть связана с тем, что в коде Вы используете директиву section для размещения в памяти SDRAM, но либо не определили макрос USE_SDRAM, либо неправильно сконфигурировали настройки областей памяти SDRAM в файле LDF или соответствующих настройках проекта. Пример такого сообщения после компиляции:

[Warning li2060]  The following input section(s) that contain program code
        and/or data have not been placed into the executable for processor 'p0'
        as there are no relevant commands specified in the LDF:
        .\Debug\UartIO.doj(sdram0_bank0)
        __initsbsz532.doj(bsz_init)
 
[Error li1060]  The following symbols are referenced, but not mapped:
        '_bufTXconsole' referenced from .\Debug\UartIO.doj(program)
        '_bufTXdebug' referenced from .\Debug\UartIO.doj(program)
 
Linker finished with 1 error and 1 warning
cc3089: fatal error: Link failed

Чтобы исправить ошибку, выполните шаги 1..4.

Внимание! Для использования памяти SDRAM в программе требуется специальная инициализация контроллера SDRAM (SDC) и контроллера внешней шины (EBIU). Обычно такая настройка делается в коде инициализации или в загрузчике системы. Подробнее см. [7].

Библиотека VDK имеет встроенную функцию VDK::Sleep(), которая позволяет приостанавливать выполнение потока на указанное количество тиков системы. Пример:

VDK::Sleep(1000);

Если тик системы имеет длительность 0.1 мс (установка по умолчанию для проекта VDK), то этот пример приостановит работу потока на 1000*0.1 = 100 мс.

Можно создать функции delay_mks и delay_ms, которые будут принимать значение задержки в микросекундах и миллисекундах соответственно.

void delay_mks(clock_t mks)
{
   float tpms = VDK::GetTickPeriod();
   VDK::Ticks sleeptiks = mks/(1000*tpms);
   VDK::Sleep(sleeptiks);
}
 void delay_ms(clock_t ms)
{
   float tpms = VDK::GetTickPeriod();
   VDK::Ticks sleeptiks = ms/tpms;
   VDK::Sleep(sleeptiks);
}

Код проектов VDK компилируется с опцией -threads [8], что автоматически определит макроопределение _ADI_THREADS. Это позволяет в коде программы проверять, активна ли многопоточная среда выполнения. Например, программа задержки может быть реализована по-разному:

#ifndef _ADI_THREADS
// Обычный проект, где не работает многопоточная среда VDK.
// Для формирования задержки здесь используется счетчик тиков ядра.
 
void delay_mks(clock_t mks)
{
   clock_t end_tick = clock() + mks*MKS_TICKS;
   while (end_tick > clock())
      ;
}
 
void delay_ms(clock_t ms)
{
   clock_t end_tick = clock() + ms*MS_TICKS;
   while (end_tick > clock())
      ;
}
#else
// VDK-проект, работающий в среде с вытеснением задач. Для формирования
// задержек может использоваться функция приостановки потока Sleep.
#include "VDK.h"
 
void delay_mks(clock_t mks)
{
   float tpms = VDK::GetTickPeriod();
   VDK::Ticks sleeptiks = mks/(1000*tpms);
   VDK::Sleep(sleeptiks);
}
 
void delay_ms(clock_t ms)
{
   float tpms = VDK::GetTickPeriod();
   VDK::Ticks sleeptiks = ms/tpms;
   VDK::Sleep(sleeptiks);
}
#endif

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

В VisualDSP++ начиная с версии 5.0 update 9 и далее компилятор Blackfin предоставляет возможность детектирования переполнений стека (опция командной строки -stack-detect). При этом используется глобальная структура, обновляемая информацией по использованию стека в программе. В многопоточных приложениях это соответствует моментам переключения контекста (когда меняется текущий работающий поток).

Если Вам нужно определить случаи переполнения стека, то не делайте переключения контекста более редкими. VDK предоставляет специальную версию кода для переключения контекста, которая должна использоваться, если разрешено детектирование переполнения стека.

Чтобы выбрать корректное переключение контекста VDK для приложения, LDF-файл Вашего проекта приложения должен быть изменен таким образом, чтобы был добавлен код, заменяющий библиотеку обслуживания ядра TMK-BF532.dlb на такую же библиотеку, но с поддержкой детектирования переполнения стека TMK-BF532_sov.dlb.

1. Откройте файл *.ldf проекта и найдите в нем место, где подключается библиотека TMK-BF532.dlb. Обычно с этой библиотеки начинается список подключаемых библиотек $LIBRARIES. Пример:

$LIBRARIES =
   TMK-BF532.dlb
   ,VDK_LIB_NAME_(CORE)
   ,VDK_LIB_NAME_(VDK_IFLAG_)
   ...

Вставьте вместо строки с TMK-BF532 следующий код:

LIBRARIES =
#ifdef _ADI_SOV_DETECTION
   TMK-BF532_sov.dlb
#else
   TMK-BF532.dlb
#endif
   ,VDK_LIB_NAME_(CORE)
   ,VDK_LIB_NAME_(VDK_IFLAG_)
   ...

2. Добавьте к опциям командной строки линкера опцию -MD_ADI_SOV_DETECTION. Это можно сделать в настройках проекта, раздел Link -> LDF Preprocessing -> поле ввода Additional options:

VisualDSP VDK ADI SOV DETECTION

Когда будет определено переполнение стека, копилятор вызовет функцию adi_stack_overflowed, которая, в случае приложения VDK, вызовет KernelPanic с panic-кодом kStackCheckFailure.

Для дополнительной информации по детектированию переполнения стека компилятором Blackfin см. руководство компилятора "C/C++ Compiler Manual for SHARC Processors".

[Признаки переполнения стека приложения VDK]

1. Программа случайным образом или каждый раз одинаково падает в KernelPanic с panic-кодом kUnhandledException. При этом Value может быть 0x24, 0x2e, 0x21, а PC указывает место в коде, которое вызвало ошибку.

VisualDSP VDK Status kUnhandledException

Примечание: значение Value события kUnhandledException показывает код ошибки, расшифровку которого можно посмотреть в приложении A апноута EE-307 [4]. Однако при переполнении стека этот код случайным образом может меняться, и расшифровка мало что даст, просто говорит о факте возникновения ошибки.

2. Когда программа скопилирована в конфигурации Debug, то вероятность возникновения ошибки больше, чем когда программа скомпилирована в конфигурации Release, либо в конфигурации Release ошибка не происходит. Характер появления сбоев зависит от настроек оптимизации, от выбранной опции поддержки отладки VDK (Kernel -> System -> Instrumentation Level).

3. Падение программы начинает проявлятся при добавлении новых потоков.

4. Окно состояния прилжения VDK (VDK Status) показывает неадекватное состояние того потока, где произошло переполнение стека:

VisualDSP VDK Status Thread stack overflov

[Как бороться с переполнением стека]

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

Ниже описаны методы борьбы с переполнением стека потоков.

1. Уменьшите количество локальных переменных, которые используются потоком. Локальные переменные потока определяются в функции ::Run, а также во всех функциях (в том числе и callback-функциях), которые запускаются в контексте потока.

2. На закладке Kernel в разделе Threads -> Thread Types увеличьте параметр Stack Size у того потока, у которого потенциально может призойти переполнение. Информацию по использованию стека можно получить в окне VDK Status, если развернуть дерево параметров потока, см. параметр Max Stack Used:

VisualDSP VDK Status Thread Max Stack Used

Если значение Max Stack Used приближается к значению Stack Size, то это повышает риск возникновения ошибки переполнения стека.

3. Планируйте построение VDK-приложения таким образом, чтобы используемых уровней приоритетов было как можно меньше. Избегайте использования одинаковых приритетов у потоков, где происходят интенсивные вычисления. Избегайте увеличения количества одновременно работающих потоков.

4. Постарайтесь по возможности уменьшить частоту запуска callback-функций драйверов библиотек ADI SSL.

В приложении используются динамически создаваемые потоки. Почему через несколько циклов создание/завершение потока при попытке создания нового потока ядро падает в KernelPanic с кодом ошибки kThreadCreationFailure?

Такое может произойти, если все работающие потоки VDK-приложения не дают запуститься потоку Idle, который занимается сбором мусора - освобождением системной памяти при завершении работы потока. Если ни один поток не освобождает процессорное время какой-либо блокировкой (либо на семафоре, либо вызовом функции Sleep, либо каким-то другим способом блокировки), то поток Idle не запускается, и соответственно не освобождает ресурсы потока, который завершил работу.

Чтобы исправить ошибку, в цикле всех работающих потоков добавьте вызов функции Sleep, либо вызовите FreeDestroyedThreads() в цикле одного из потоков, который периодически получает управление.

Подробнее про создание/уничтожение потоков и работу потока Idle см. [1, 9].

[1. Переполнение стека потока]

Обычно это приводит к случайному падению приложения по непонятной причине (kUnhandledException). Вероятность падения зависит от конфигурации компиляции Debug/Release (в Release обычно падения встречаются реже). Чаще причина в переполнении стека одного из потоков. Дело в том, что размер стека по умолчанию для потока 255 слов, но этого слишком мало для реального приложения.

Оценить реальный объем, который каждый поток тратит на стек, можно в окне VDK Status (меню View -> VDK Windows -> Status). Остановите отлаживаемый проект (Shift+F5), в окне VDK Status разверните список потоков Threads и просмотрите список параметров каждого потока. Интересующие параметры: Stack Size (размер стека) и Max Stack Used (сколько стека реально использовалось по максимуму). Убедитесь, что Max Stack Used не приближается по значению к Stack Size, т. е. должно выполняться условие Max Stack Used < 0.75 * Stack Size. Если для какого-то потока это не так, то для него нужно увеличить размер стека.

Как изменить размер стека потока: на закладке Kernel разверните дерево Kernel -> Threads -> Thread Types, выберите нужный тип, и измените у него параметр Stack Size.

[2. Проблема с динамическим созданием потока]

На какой-то из попыток создания потока при вызове функции создания потока (CreateThread, CreateThreadEx) приложение падает с ошибкой kThreadCreationFailure. Чаше всего это происходит из-за того, что не осталось свободного места в системной куче по умолчанию (system_heap, по умолчанию она размещена в памяти L1).

Причина опустошения кучи чаще всего две. Первая причина: куча слишком мала для такого количества создаваемых системных объектов. Чтобы исправить ситуацию, придется пересмотреть архитектуру приложения - уменьшить количество потоков, оптимизировать количество памяти, которое потребляет поток, либо придется переконфигурировать кучу по умолчанию с целью увеличения её размера. Если куча находится в L1, то обычно радикально увеличить её размер не получится, возможно придется перенести кучу в L3 (SDRAM).

Вторая причина опустошения кучи: работающие потоки не оставляют процессорного времени для сборщика мусора (он работает в потоке ожидания Idle), и память, которая должна высвободиться при удалении потока, не освобождается. В этом случае после нескольких попыток создания/удаления потока куча опустошится, и потоки перестанут создаваться. Чтобы исправить ошибку, нужно предусмотреть блокировку работающих потоков, чтобы в какое-то время успел запуститься и выполнить свою работу поток ожидания Idle. Блокировки могут быть разными - блокировка на семафоре, на очереди, или простая задержка выполнения потока с помощью функции VDK::Sleep.

[3. Проблема с приоритетами]

Если неправильно назначить приоритеты потокам, то приложение будет работать неустойчиво. Например, если поток, который рисует в памяти индикатора, имеет приоритет меньше того потока, который обновляет экран (например, через DMA), то картинка на экране будет случайным образом портиться. Постройте свое приложение так, чтобы потоки, которые зависят друг от друга, использовали правильно настроенную синхронизацию с помощью штатных средств VDK [10]. Например, если имеется обработка данных, отображение результата обработки и обновление экрана, то они должны запускаться друг за другом, и у обновления экрана должен быть самый низкий приоритет.

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

VDK API имеет функцию VDK::GetTickPeriod(), которая возвратит длительность тика в миллисекундах. В помощью этой функции выполнить перевод абсолютного времени в тики и обратно.

[Как перевести время в тики (на примере ожидания на семафоре)]

Функция ожидания на семафоре PendSemaphore во втором аргументе принимает длительность таймаута, выраженное в тиках. Следовательно, чтобы получить время ожидания в тиках, нужно это время поделить на результат, который возвратит функция VDK::GetTickPeriod(). Предположим, что ожидание на семафоре должно быть 100 мс:

VDK::PendSemaphore(semaphore_id, 100/VDK::GetTickPeriod());

[Как перевести тики в абсолютное время]

Предположим, что переменная Nticks хранит количество тиков системы. Тогда соответствующее абсолютное время в миллисекундах будет равно произведению Nticks на результат функции VDK::GetTickPeriod():

float timems = Nticks * VDK::GetTickPeriod();

См. также вопрос "Q010-160527. Формирование задержек".

Процессор, который выполняет VDK-приложение Blackfin, никогда не останавливается, и всегда выполняет какие-то вычисления, которые распределены между потоками системы. Не производительные вычисления, которые не связаны с полезной работой приложения, сосредоточены в потоке ожидания (Idle Thread). Таким образом, чтобы узнать, насколько загружен процессор полезной работой, нужно:

1. Узнать, сколько процессорного времени прошло. В тиках или в единицах счета таймера ядра, не важно. Предположим, что это время равно T.
2. Узнать, сколько времени процессор находится в потоке ожидания. Предположим, что это время равно Tidle.
3. Вычислить загрузку процессора полезной работой по формуле (в процентах):

LOAD = 100 * (1 - Tidle/T)

Все довольно просто, осталось только получить значения T и Tidle. Проще всего это сделать, если воспользоваться статистикой, которую накапливает так называемая инструментальная сборка проекта VDK. Сборка называется инструментальной по терминологии компании Analog Devices, которая разработала библиотеки VDK таким образом, что проект VDK можно собрать в 3 вариантах - Full Instrumentation (вариант полной инструментальной поддержки, который накапливает статистику по системе и потокам), Error Checking (этот вариант снабжен только проверкой ошибок) и None (программисту не предоставляется дополнительной отладочной информации по отлаживаемому проекту VDK).

Информацию Full Instrumentation можно посмотреть двумя способами - если остановить приложение в отладчике, и открыть окошки VDK State History, VDK Target Load, VDK Status (доступно через меню View -> VDK Windows). Но как получить эту информацию в приложении, runtime, не прибегая к помощи отладчика, не нарушая работы приложения?

К счастью, существует специальное API [11, 12], которое позволяет добраться до этой информации из кода приложения. Значение времени T в тиках можно получить вызовом функции GetUptime(), а время Tidle в тиках можно получить вызовом функции GetThreadTickData. Пример:

VDK::Ticks outUpTime;   //Это общее время T
VDK::Ticks outIdleTime; //Это время Tidle
 
outUpTime = VDK::GetUptime();
VDK::GetThreadTickData ((VDK::ThreadID)0,
                        (VDK::Ticks *)NULL,
                        (VDK::Ticks *)NULL,
                        (VDK::Ticks *)NULL,
                        &outIdleTime);float Load = 100 * (1 - (float)outIdleTime/outUpTime);
printf("Загрузка системы %.1%%\n", Load);

Обратие внимание, что в функцию GetThreadTickData передан 0 в качестве идентификатора потока, потому что этому значению всегда равен идентификатор потока ожидания. Имейте в виду, что выведенная загрузка в процентах не учитывает процессорное время, которое ядро проводит в вычислениях драйверов устройств, если их код работает в контексте домена прерываний.

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

VDK::Ticks outUpTime;      //Общее время, сколько проработала система
VDK::Ticks outThreadTime;  //Время работы потока
int totalthreads;          //Сколько потоков имеется в системе
 
//Выделение памяти под идентификаторы потоков:
VDK::ThreadID *outThreadIDArray = 
   (VDK::ThreadID *)heap_malloc(1, VDK_kMaxNumThreads * sizeof(VDK::ThreadID));
//Получение информации о потоках:
totalthreads = VDK::GetAllThreads (outThreadIDArray, VDK_kMaxNumThreads);
//Сколько проработала система всего:
outUpTime = VDK::GetUptime();
//Цикл по потокам:
for (int idx=0; idx < totalthreads; idx++)
{
   //Получение имени потока:
   char *outName;          //Указатель на имя потока
   VDK::GetThreadTemplateName(outThreadIDArray[idx],
                              &outName);
   //Получение времени, сколько проработал поток:
   VDK::GetThreadTickData (outThreadIDArray[idx],
                           (VDK::Ticks *)NULL,
                           (VDK::Ticks *)NULL,
                           (VDK::Ticks *)NULL,
                           &outThreadTime);
   printf ("%i: %s, %.1f%%\n", outThreadIDArray[idx],
                               outName,
                               (float)100*outThreadTime/outUpTime);
}
heap_free(1, outThreadIDArray);

Буде выведен список наподобие следующего (ID потока, имя потока, % процессорного времени):

0: Idle Thread, 80.2%
1: kmainthread, 0.3%
2: kuartRXthread, 0.2%
3: kuartTXthread, 0.1%
4: kscreenthread, 0.9%
5: kreadbattvoltage, 0.1%
6: kkeybrdthread, 1.7%
7: kDSPthread, 16.7%

Имейте в виду, что эти примеры кода будут работать и для конфигурации Debug, и для конфигурации Release, но только при условии, если выбран вариант библиотек VDK с полной инструментальной поддержкой (настраивается в свойствах проекта на закладке Kernel, раздел System -> Instrumentation Level, здесь должен быть выбран вариант Full Instrumentation).

[Ссылки]

1. Обзор VisualDSP++ Kernel RTOS (VDK).
2. VisualDSP++ 5.0 Kernel (VDK) User’s Guide site:analog.com.
3. Blackfin FAQ.
4. EE-307: советы по отладке для Blackfin.
5. VisualDSP: конфигурирование проекта для использования VDK.
6. Служба управления питанием процессоров Blackfin.
7. ADSP-BF538: блок интерфейса внешней шины.
8. Опции командной строки компилятора Blackfin.
9. VDK: потоки.
10. VDK: сигналы, взаимодействие потоков и ISR (синхронизация).
11VDK API для получения информации инструментальной сборки.
12VDK API для получения информации состояния приложения.

 

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


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

Top of Page