Создание образа прошивки, состоящего из двух частей, с использованием набора инструментов GCC

У меня есть прошивка, созданная с помощью GCC, которая работает на микроконтроллере на базе ARM Cortex M0. Сборка в настоящее время генерирует одно двоичное изображение, которое может быть записано в программную память микроконтроллера.

По причинам, связанным с обновлением полей, мне нужно разделить это изображение на две части, которые можно обновлять отдельно. Я назову их Core и App.

  • Ядро: содержит таблицу векторов прерываний, main() процедуру, а также различные драйверы и библиотечные процедуры. Он будет расположен в первой половине программной памяти.

  • Приложение: содержит код конкретного приложения. Он будет расположен во второй половине программной памяти. У него будет одна точка входа по известному адресу, которая вызывается ядром для запуска приложения. Он будет получать доступ к функциям и данным в ядре через известные адреса.

Здесь есть некоторые очевидные ограничения, о которых я хорошо осведомлен:

  • При создании приложения необходимо знать адреса символов в ядре. Таким образом, сначала необходимо создать ядро, и оно должно быть доступно при связывании приложения.

  • Образ приложения будет совместим только с конкретным основным образом, для которого он был создан.

  • Можно будет обновить приложение без обновления ядра, но не наоборот.

Все в порядке.

У меня простой вопрос: как создать эти образы с помощью GCC и GNU binutils?

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


person Martin L    schedule 03.02.2016    source источник
comment
Определите весь экспорт Core во фрагменте сценария компоновщика (легко автоматизируется) и INCLUDE этот фрагмент в сценарии компоновщика приложения.   -  person user58697    schedule 03.02.2016
comment
Любая причина, по которой у вас не может быть x проектов C, которые все используют общие файлы ядра друг с другом, а затем построить x полностью отдельных двоичных файлов в зависимости от приложения, все они включают оба ядра + приложение? Альтернатива кажется слишком сложной. Кроме того, таким образом вы можете гарантировать, что приложение всегда синхронизировано с ядром, на случай, если ядро ​​потребуется изменить в какой-то момент.   -  person Lundin    schedule 04.02.2016
comment
@ user58697: Кажется, это работает. Я удивлен, что для этого требуется внешний скрипт? Похоже, что это должно быть возможно с существующей функциональностью в binutils.   -  person Martin L    schedule 04.02.2016
comment
@Lundin На самом деле меня не интересует запуск нескольких отдельных проектов на этой платформе. У меня есть только один проект, который нужно разделить на две части из-за технических ограничений, связанных с процессом обновления полей.   -  person Martin L    schedule 04.02.2016
comment
Я нашел этот вопрос и это продолжение, спрашивая примерно то же самое. Использование objcopy для создания файла символов для ядра и передача его ld с --just-symbols кажется частью решения, но по-прежнему оставляли неопределенные ссылочные ошибки.   -  person Martin L    schedule 04.02.2016
comment
Для любой случайной IDE вы должны иметь возможность добавлять одни и те же файлы в несколько проектов. IDE может поддерживать или не поддерживать одновременное открытие нескольких проектов.   -  person Lundin    schedule 04.02.2016
comment
@Lundin Мне очень жаль, но я не думаю, что вы поняли вопрос.   -  person Martin L    schedule 04.02.2016
comment
Я понимаю это, я просто сомневаюсь, что такие требования имеют смысл. Похоже, вы стоите перед выбором между простым и надежным решением с одним единственным двоичным файлом и сложным беспорядочным решением с двумя двоичными файлами на одном процессоре, которое потребует развертывания ваших собственных драйверов для программирования флэш-памяти, загрузчиков и т. Д., А также похоже на кошмар, который нужно поддерживать на случай, если два двоичных файла окажутся несовместимыми где-то по пути.   -  person Lundin    schedule 04.02.2016
comment
@Lundin Основная проблема заключается в том, что на оборудовании недостаточно памяти для хранения единственного монолитного образа во время его получения и проверки перед записью в программную область. При получении обновления используется сложный коммуникационный код, который также используется приложением, поэтому совместное использование имеет важное значение, и простой загрузчик, который получает и мигает постепенно, здесь не вариант, поскольку соединение может быть потеряно. Разделение изображения было выбрано как лучший способ удовлетворить требования. Всем хорошо известны ограничения, которые она накладывает.   -  person Martin L    schedule 04.02.2016


Ответы (2)


Сейчас это работает, поэтому я отвечу на свой вопрос. Вот что было необходимо для этого, начиная с обычной сборки одного образа, превращая его в «ядро» и затем настраивая сборку для «приложения».

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

  2. Создайте сценарий компоновщика для ядра. Это будет то же самое, что и стандартный сценарий компоновщика для платформы, за исключением того, что он должен использовать только области, зарезервированные для ядра. Это можно сделать, изменив ORIGIN и LENGTH записей флэш-памяти и ОЗУ в разделе MEMORY сценария компоновщика.

  3. Создайте файл заголовка, объявляющий точку входа для приложения. Для этого нужен только прототип, например:

void app_init(void);.

  1. Включите этот заголовок из основного кода C и получите основной вызов app_init() для запуска приложения.

  2. Создайте файл символов, объявляющий адрес точки входа, который будет начальным адресом флэш-области для приложения. Я назову это app.sym. Это может быть одна строка в следующем формате:

app_init = 0x00010000;

  1. Соберите ядро, используя сценарий компоновщика ядра и добавив --just-symbols=app.sym к параметрам компоновщика, чтобы получить адрес app_init. Сохраните файл ELF из сборки, которую я назову core.elf.

  2. Создайте сценарий компоновщика для приложения. Это снова будет основано на стандартном сценарии компоновщика для платформы, но с изменением диапазонов флэш-памяти и оперативной памяти на те, которые зарезервированы для приложения. Кроме того, потребуется специальный раздел, чтобы гарантировать, что app_init помещается в начало области флэш-памяти приложения перед остальной частью кода в разделе .text:

SECTIONS
{
    .text :
    {
        KEEP(*(.app_init))
        *(.text*)
  1. Напишите функцию app_init. Он должен быть в сборке, так как он должен выполнить некоторую работу на низком уровне, прежде чем любой код C в приложении может быть вызван. Его нужно будет пометить .section .app_init, чтобы компоновщик поместил его в нужное место в начале области флэш-памяти приложения. Функция app_init должна:

    1. Populate variables in the app's .data section with initial values from flash.
    2. Обнулить переменные в разделе .bss приложения.
    3. Вызовите точку входа C для приложения, которую я назову app_start().
  2. Напишите app_start() функцию, запускающую приложение.

  3. Создайте приложение, используя скрипт компоновщика приложений. Этот шаг связи должен быть передан объектным файлам, содержащим app_init, app_start и любой код, вызываемый app_start, который еще не находится в ядре. Параметр компоновщика --just-symbols=core.elf должен быть передан для связывания функций в ядре по их адресам. Кроме того, необходимо передать -nostartfiles, чтобы исключить обычный код запуска среды выполнения C.

Потребовалось время, чтобы разобраться во всем этом, но теперь все работает нормально.

person Martin L    schedule 10.02.2016

Прежде всего ... если это только для обновления полей, вам не нужно полагаться на таблицу векторов прерываний в основном пространстве для приложения. Я думаю, что части ARM M0 всегда могут его перемещать. Я знаю, что это можно сделать на некоторых (всех?) Материалах STM32Fx, но я считаю, что это вещь ARM M-x, а не ST. Изучите это, прежде чем принимать решение сделать все ISR вашего приложения хуками, вызываемыми из ядра.

Если вы планируете много взаимодействовать со своим ядром (кстати, я всегда называю часть, которая выполняет самообновление «загрузчиком» на микроконтроллерах), вот альтернативное предложение:

Передает ли Ядро указатель на структуру / таблицу функций, описывающих его возможности, в точку входа Приложение?

Это позволит полностью разделить код приложения и ядра, за исключением общего заголовка (при условии, что ваш ABI не изменится), и предотвратит конфликты имен.

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

core.h:

struct core_functions
{
    int (*pcore_func1)(int a, int b);
    void (*pcore_func2)(void);
};

core.c:

int core_func1(int a, int b){ return a + b; }
void core_func2(void){ // do something here }

static const struct core_functions cfuncs= 
{
    core_func1,
    core_func2
};

void core_main()
{
   // do setup here
   void (app_entry*)(const struct core_functions *) = ENTRY_POINT;
   app_entry( &cfuncs );
}

app.c

void app_main(const struct core_functions * core)
{
   int res;
   res = core->pcore_func1(20, 30);
}

Обратной стороной / стоимостью является небольшая нагрузка на время выполнения, память и дополнительный код.

person Brian McFarland    schedule 03.02.2016
comment
Спасибо за Ваш ответ. Во-первых, я боюсь, что это именно тот подход, которого я хотел избежать. Есть много функций, которые нужно экспортировать, поэтому поддержание этого было бы проблемой, помимо накладных расходов. Он может быть написан / сгенерирован, но в этот момент мне было бы лучше с предложением в комментарии @ user58697 выше. Это проблема компоновщика - это не должно быть сделано в коде. Во-вторых, причина, по которой я не использовал термин загрузчик, заключается в том, что на самом деле есть загрузчик перед всем этим для прямого программирования. Ядро выполняет более сложный процесс удаленного обновления. - person Martin L; 04.02.2016