gcc linker embedded microcontroller binutils

Creación de una imagen de firmware de dos partes utilizando la cadena de herramientas GCC



linker embedded (2)

Tengo un firmware creado con GCC que se ejecuta en un microcontrolador ARM Cortex M0. La compilación genera actualmente una sola imagen binaria que se puede escribir en la memoria del programa del microcontrolador.

Por razones relacionadas con la actualización de campo, necesito dividir esta imagen en dos partes que se pueden actualizar por separado. Llamaré a estos Core y App .

  • Núcleo : contiene la tabla de vectores de interrupción, la rutina main() y varios controladores y rutinas de la biblioteca. Se ubicará en la primera mitad de la memoria del programa.

  • Aplicación : contiene un código específico de la aplicación. Se ubicará en la segunda mitad de la memoria del programa. Tendrá un único punto de entrada, en una dirección conocida, que el núcleo llama para iniciar la aplicación. Accederá a funciones y datos en el núcleo a través de direcciones conocidas.

Aquí hay algunas limitaciones obvias, que yo conozco muy bien:

  • Al compilar la aplicación, será necesario conocer las direcciones de los símbolos en el núcleo. Por lo tanto, el núcleo debe construirse primero y debe estar disponible al vincular la aplicación.

  • Una imagen de la aplicación solo será compatible con la imagen central específica sobre la que se creó.

  • Será posible actualizar la aplicación sin actualizar el núcleo, pero no al revés.

Todo eso está bien.

Mi pregunta es simplemente, ¿cómo puedo construir estas imágenes usando GCC y los binutils de GNU?

Esencialmente, quiero construir el núcleo como una imagen de firmware normal, y luego construir la imagen de la aplicación, con la aplicación tratando el núcleo como una biblioteca. Pero ni los enlaces compartidos (que requerirían un mecanismo de enlace dinámico) ni los enlaces estáticos (que copiarían las funciones básicas utilizadas en el binario de la aplicación) son aplicables aquí. Lo que estoy tratando de hacer es en realidad mucho más simple: vincularlo contra un binario existente usando sus direcciones conocidas y fijas. Simplemente no tengo claro cómo hacerlo con las herramientas.


Antes que nada ... si esto es solo para la actualización de campo, no necesita confiar en la tabla de vectores de interrupción en el espacio central para la aplicación . Creo que las partes de ARM M0 siempre tienen la capacidad de moverlo. Sé que se puede hacer en algunas (¿todas?) Cosas de STM32Fx, pero creo que esto es algo de ARM Mx, no de ST. Observe esto antes de comprometerse con la decisión de hacer que los ISR de su aplicación sean ganchos llamados desde el núcleo.

Si planeas tener mucha interacción con tu núcleo (por cierto, siempre llamo a la pieza que hace la auto-actualización de un "gestor de arranque" en MCU), aquí hay una sugerencia alternativa:

¿El núcleo debe pasar un puntero a una estructura / tabla de funciones que describa sus capacidades en el punto de entrada de la aplicación ?

Esto permitiría la separación completa del código para la aplicación frente al núcleo a excepción de un encabezado compartido (asumiendo que su ABI no cambia) y evitará las colisiones de nombres.

También proporciona una forma razonable de evitar que GCC optimice cualquier función que pueda invocar solo desde la aplicación sin estropear su configuración de optimización o atormentarla con pragmas.

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); }

La desventaja / costo es un ligero tiempo de ejecución y sobrecarga de memoria y más código.


Tenemos esto funcionando ahora, así que voy a responder mi propia pregunta. Aquí está lo que era necesario para hacer esto, comenzando con una construcción de imagen única normal, convirtiendo eso en el "núcleo" y luego configurando la compilación para la "aplicación".

  1. Decide cómo dividir el flash y la memoria RAM en áreas separadas para el núcleo y la aplicación. Defina la dirección de inicio y el tamaño de cada área.

  2. Crea una secuencia de comandos del enlazador para el núcleo. Esta será la misma que la secuencia de comandos del enlazador estándar para la plataforma, excepto que solo debe usar las áreas reservadas para el núcleo. Esto se puede hacer cambiando el ORIGIN y la LENGTH de las entradas de flash y RAM en la sección MEMORY del script del enlazador.

  3. Crea un archivo de cabecera que declare el punto de entrada para la aplicación. Esto solo necesita un prototipo, por ejemplo:

void app_init(void); .

  1. Incluya este encabezado desde el código C central y app_init() llamada principal app_init() inicie la aplicación.

  2. Cree un archivo de símbolos que indique la dirección del punto de entrada, que será la dirección de inicio del área de flash para la aplicación. Llamaré a esta app.sym . Puede ser solo una línea en el siguiente formato:

app_init = 0x00010000;

  1. Construya el núcleo, usando el script del enlazador central y agregando --just-symbols=app.sym a los parámetros del enlazador para dar la dirección de app_init . Retenga el archivo ELF de la compilación, que llamaré core.elf .

  2. Crea un script enlazador para la aplicación. De nuevo, esto se basará en el script del enlazador estándar para la plataforma, pero con los rangos de memoria Flash y RAM cambiados a los reservados para la aplicación. Además, necesitará una sección especial para asegurarse de que app_init se coloque al inicio del área de flash de la aplicación, antes que el resto del código en la sección .text :

SECTIONS { .text : { KEEP(*(.app_init)) *(.text*)

  1. Escribe la función app_init . Esto tendrá que ser en ensamblaje, ya que debe hacer algún trabajo de bajo nivel antes de que se pueda invocar cualquier código C en la aplicación. .section .app_init estar marcado con .section .app_init para que el enlazador lo coloque en el lugar correcto al inicio del área de flash de la aplicación. La función app_init necesita:

    1. Rellenar variables en la sección .data la aplicación con los valores iniciales de flash.
    2. Establezca las variables en la sección .bss la aplicación en cero.
    3. Llame al punto de entrada C para la aplicación, que llamaré app_start() .
  2. Escriba la función app_start() que inicia la aplicación.

  3. Cree la aplicación utilizando el script del enlazador de la aplicación. Este paso de enlace debe pasar los archivos de objeto que contienen app_init , app_start y cualquier código llamado por app_start que no esté ya en el núcleo. El parámetro del enlazador --just-symbols=core.elf debe pasar a las funciones de enlace en el núcleo por sus direcciones. Además, se deben pasar los -nostartfiles para dejar afuera el código de inicio normal de C en tiempo de ejecución.

Me tomó un tiempo entender todo esto, pero ahora está funcionando muy bien.