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".
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.
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 laLENGTH
de las entradas de flash y RAM en la secciónMEMORY
del script del enlazador.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);
.
Incluya este encabezado desde el código C central y
app_init()
llamada principalapp_init()
inicie la aplicación.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;
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 deapp_init
. Retenga el archivo ELF de la compilación, que llamarécore.elf
.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*)
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ónapp_init
necesita:- Rellenar variables en la sección
.data
la aplicación con los valores iniciales de flash. - Establezca las variables en la sección
.bss
la aplicación en cero. - Llame al punto de entrada C para la aplicación, que llamaré
app_start()
.
- Rellenar variables en la sección
Escriba la función
app_start()
que inicia la aplicación.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 porapp_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.