24 votos

¿Cómo arranca un microcontrolador, paso a paso?

Cuando se escribe el código C, se compila y se carga en un microcontrolador, éste empieza a funcionar. Pero si tomamos este proceso de carga y arranque paso a paso en cámara lenta, tengo algunas confusiones sobre lo que realmente está sucediendo dentro del MCU (memoria, CPU, bootloader). Esto es (probablemente erróneo) lo que respondería si alguien me preguntara:

  1. El código binario compilado se escribe en la flash ROM (o EEPROM) a través del USB
  2. El gestor de arranque copia una parte de este código en la RAM. Si es cierto, ¿cómo sabe el el cargador de arranque sabe qué copiar (qué parte de la ROM debe copiar a la la RAM)?
  3. La CPU comienza a buscar instrucciones y datos del código desde la ROM y la RAM

¿Esto está mal?

¿Es posible resumir este proceso de arranque y puesta en marcha con alguna información sobre cómo interactúan la memoria, el cargador de arranque y la CPU en esta fase?

He encontrado muchas explicaciones básicas sobre cómo arranca un PC a través de la BIOS. Pero estoy atascado con el proceso de arranque del microcontrolador.

36voto

1) el binario compilado se escribe en prom/flash sí. USB, serial, i2c, jtag, etc. depende del dispositivo en cuanto a lo que es soportado por ese dispositivo, irrelevante para entender el proceso de arranque.

2) Esto no suele ser cierto para un microcontrolador, el caso de uso principal es tener las instrucciones en rom/flash y los datos en ram. No importa cuál sea la arquitectura. para un no-microcontrolador, su pc, su portátil, su servidor, el programa se copia desde el no volátil (disco) a la ram y luego se ejecuta desde allí. Algunos microcontroladores te permiten usar la ram también, incluso los que dicen ser de Harvard aunque parezca que viola la definición. No hay nada en harvard que impida mapear la ram en el lado de las instrucciones, sólo hay que tener un mecanismo para obtener las instrucciones allí después de que se encienda (lo que viola la definición, pero los sistemas harvard tendrían que hacer eso para ser útiles además de como microcontroladores).

3) Más o menos.

Cada cpu "arranca" de forma determinista, según su diseño. La forma más común es una tabla de vectores en la que la dirección de las primeras instrucciones que se ejecutan tras el encendido se encuentran en el vector de reinicio, una dirección que el hardware lee y luego utiliza esa dirección para empezar a ejecutarse. La otra forma general es hacer que el procesador comience a ejecutarse sin una tabla de vectores en alguna dirección bien conocida. A veces el chip tendrá "correas", algunos pines que puedes atar alto o bajo antes de liberar el reset, que la lógica utiliza para arrancar de diferentes maneras. Tienes que separar la cpu en sí, el núcleo del procesador del resto del sistema. Entender cómo funciona la cpu, y luego entender que los diseñadores de chips/sistemas han configurado decodificadores de direcciones alrededor del exterior de la cpu para que una parte del espacio de direcciones de la cpu se comunique con una flash, y otra con la ram y otra con los periféricos (uart, i2c, spi, gpio, etc). Puedes tomar ese mismo núcleo de cpu si lo deseas, y envolverlo de forma diferente. Esto es lo que obtienes cuando compras algo basado en arm o mips. arm y mips hacen núcleos de cpu, los cuales la gente de chip compra y envuelve sus propias cosas, por varias razones no hacen esas cosas compatibles de marca a marca. Es por eso que rara vez se puede hacer una pregunta genérica de arm cuando se trata de cualquier cosa fuera del núcleo.

Un microcontrolador intenta ser un sistema en un chip, por lo que su memoria no volátil (flash/rom), volátil (sram), y la cpu están todos en el mismo chip junto con una mezcla de periféricos. Pero el chip está diseñado internamente de tal manera que la memoria flash se mapea en el espacio de direcciones de la cpu que coincide con las características de arranque de esa cpu. Si, por ejemplo, la cpu tiene un vector de reinicio en la dirección 0xFFFC, entonces tiene que haber una flash/rom que responda a esa dirección que podamos programar a través de 1), junto con suficiente flash/rom en el espacio de direcciones para programas útiles. Un diseñador de chips puede elegir tener 0x1000 bytes de flash empezando en 0xF000 para satisfacer esos requisitos. Y tal vez pongan alguna cantidad de ram en una dirección inferior o tal vez 0x0000, y los periféricos en algún lugar en el medio.

Otra arquitectura de cpu podría empezar a ejecutarse en la dirección cero, por lo que tendrían que hacer lo contrario, colocar la flash para que responda a un rango de direcciones alrededor de cero. digamos 0x0000 a 0x0FFF por ejemplo. y luego poner algo de ram en otro lugar.

Los diseñadores del chip saben cómo arranca la cpu y han colocado allí un almacenamiento no volátil (flash/rom). Luego depende de la gente del software escribir el código de arranque para que coincida con el comportamiento bien conocido de esa cpu. Tienes que colocar la dirección del vector de reinicio en el vector de reinicio y tu código de arranque en la dirección que definiste en el vector de reinicio. La cadena de herramientas puede ayudarte mucho aquí. a veces, especialmente con los ides de apuntar y hacer clic u otras cajas de arena pueden hacer la mayor parte del trabajo por ti todo lo que haces es llamar a apis en un lenguaje de alto nivel (C).

Pero, sea como sea, el programa cargado en la flash/rom tiene que coincidir con el comportamiento de arranque de la cpu. Antes de la porción en C de su programa main() y en si usa main como punto de entrada, algunas cosas tienen que ser hechas. Un programador de C asume que cuando declara una variable con un valor inicial, espera que realmente funcione. Bueno, las variables, excepto las const, están en ram, pero si tienes una con un valor inicial ese valor inicial tiene que estar en ram no volátil. Así que este es el segmento .data y el bootstrap de C necesita copiar las cosas de .data de la flash a la ram (donde normalmente lo determina la cadena de herramientas). Las variables globales que declaras sin un valor inicial se asume que son cero antes de que tu programa comience aunque realmente no deberías asumir eso y afortunadamente algunos compiladores están empezando a advertir sobre variables no inicializadas. Este es el segmento .bss, y el bootstrap de C lo pone a cero en la memoria RAM, el contenido, los ceros, no tienen que ser almacenados en la memoria no volátil, pero la dirección inicial y la cantidad sí. Nuevamente la cadena de herramientas te ayuda mucho aquí. Y por ultimo lo minimo es que necesitas configurar un puntero de pila ya que los programas C esperan poder tener variables locales y llamar a otras funciones. Entonces tal vez algunas otras cosas específicas del chip se hacen, o dejamos que el resto de las cosas específicas del chip sucedan en C.

Los núcleos de la serie cortex-m de arm harán algo de esto por ti, el puntero de la pila está en la tabla de vectores, hay un vector de reinicio para apuntar al código que se ejecutará después del reinicio, de modo que, aparte de lo que tienes que hacer para generar la tabla de vectores (que normalmente usas asm de todos modos) puedes ir puramente en C sin asm. ahora no tienes tus datos copiados ni tu bss puesto a cero, así que tienes que hacerlo tú mismo si quieres intentar ir sin asm en algo basado en cortex-m. La característica más importante no es el vector de reinicio, sino los vectores de interrupción donde el hardware sigue la convención de llamadas de C recomendada por las armas y preserva los registros para usted, y utiliza el retorno correcto para ese vector, de modo que usted no tiene que envolver el asm correcto alrededor de cada controlador (o tener directivas específicas de la cadena de herramientas para que la cadena de herramientas lo envuelva por usted).

Cosas específicas de los chips pueden ser, por ejemplo, los microcontroladores se utilizan a menudo en los sistemas basados en la batería, por lo que de baja potencia por lo que algunos salen de reinicio con la mayoría de los periféricos apagados, y usted tiene que encender cada uno de estos subsistemas para que pueda utilizarlos. Uarts, gpios, etc. A menudo se utiliza una velocidad de reloj baja, directamente desde un cristal u oscilador interno. Y el diseño de su sistema puede mostrar que usted necesita un reloj más rápido, por lo que se inicializa que. su reloj puede ser demasiado rápido para la flash o la memoria RAM por lo que puede haber necesitado para cambiar los estados de espera antes de subir el reloj. Puede que necesites configurar el uart, o el usb u otras interfaces. entonces tu aplicación puede hacer lo suyo.

Un ordenador de sobremesa, un portátil, un servidor y un microcontrolador no difieren en su forma de arrancar/trabajar. Excepto que no están en su mayoría en un solo chip. El programa de la bios suele estar en un chip flash/rom separado de la cpu. Aunque recientemente los cpus x86 están tirando más y más de lo que solían ser chips de apoyo en el mismo paquete (controladores pcie, etc) pero todavía tienes la mayor parte de tu ram y rom fuera del chip, pero sigue siendo un sistema y sigue funcionando exactamente igual a un alto nivel. El proceso de arranque de la cpu es bien conocido, los diseñadores de la placa colocan la flash/rom en el espacio de direcciones donde arranca la cpu. ese programa (parte de la BIOS en un pc x86) hace todas las cosas mencionadas anteriormente, arranca varios periféricos, inicializa la dram, enumera los buses pcie, etc. Suele ser bastante configurable por el usuario en base a la configuración de la bios o lo que antes llamábamos configuración del cmos, porque en su momento esa era la tecnología que se utilizaba. No importa, hay configuraciones de usuario que puedes ir y cambiar para decirle al código de arranque de la bios cómo variar lo que hace.

Diferentes personas utilizarán diferente terminología. un chip arranca, es el primer código que se ejecuta. a veces se llama bootstrap. un bootloader con la palabra loader a menudo significa que si no haces nada para interferir es un bootstrap que te lleva desde el arranque genérico a algo más grande, tu aplicación o sistema operativo. pero la parte del loader implica que puedes interrumpir el proceso de arranque y entonces quizás cargar otros programas de prueba. si alguna vez has usado uboot, por ejemplo, en un sistema linux embebido, puedes pulsar una tecla y detener el arranque normal, entonces puedes descargar un kernel de prueba en la memoria ram y arrancarlo en lugar del que está en la flash, o puedes descargar tus propios programas, o puedes descargar el nuevo kernel y luego hacer que el gestor de arranque lo escriba en la flash para que la próxima vez que arranques ejecute el nuevo material. pero el término gestor de arranque se usa a menudo para cualquier tipo de arranque, incluso si no tiene una parte de cargador.

En cuanto a la cpu en sí, el núcleo del procesador, que no conoce la ram de la flash de los periféricos. No existe la noción de cargador de arranque, sistema operativo, aplicación. Es sólo una secuencia de instrucciones que se introducen en la cpu para ser ejecutadas. Estos son términos de software para distinguir diferentes tareas de programación entre sí. Conceptos de software entre sí.

Algunos microcontroladores tienen un gestor de arranque independiente proporcionado por el proveedor del chip en una memoria flash separada o en un área separada de la memoria flash que es posible que no puedas modificar. En este caso a menudo hay un pin o un conjunto de pines (los llamo straps) que si los pones en alto o en bajo antes de que se libere el reset le estás diciendo a la lógica y/o al gestor de arranque lo que tiene que hacer, por ejemplo una combinación de straps puede decirle al chip que ejecute ese gestor de arranque y que espere en el uart a que se programen los datos en la flash. Si las correas se colocan al revés, el programa arranca y no el gestor de arranque de los vendedores de chips, lo que permite la programación de campo del chip o la recuperación de su programa de estrellarse. A veces es sólo la lógica pura que le permite programar la flash. Esto es bastante común hoy en día, pero si te remontas al pasado, necesitabas/querías tu propio gestor de arranque por las mismas razones (por supuesto, si te remontas demasiado, sacabas la eeprom/prom/chip del zócalo y la sustituías por otra o la reprogramabas en un dispositivo. Y aún puedes tener tu propio bootloader si quieres, aunque haya formas de programar por hardware (avr/arduino).

La razón por la que la mayoría de los microcontroladores tienen mucha más flash que ram es que el caso de uso principal es ejecutar el programa directamente desde la flash, y sólo tienen suficiente ram para cubrir la pila y las variables. Aunque en algunos casos se pueden ejecutar programas desde ram que hay que compilar bien y almacenar en flash y luego copiar antes de llamar.

EDITAR

flash.s

.cpu cortex-m0
.thumb

.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang
.word hang
.word hang

.thumb_func
reset:
    bl notmain
    b hang

.thumb_func
hang:   b .

notmain.c

int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;

    return(0);
}

flash.ld

MEMORY
{
    bob : ORIGIN = 0x00000000, LENGTH = 0x1000
    ted : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > bob
    .rodata : { *(.rodata*) } > bob
    .bss : { *(.bss*) } > ted
    .data : { *(.bss*) } > ted AT > bob
}

Así que este es un ejemplo para un cortex-m0, los cortex-ms funcionan todos igual en cuanto a este ejemplo. El chip en particular, para este ejemplo, tiene la aplicación flash en la dirección 0x00000000 en el espacio de direcciones del brazo y la ram en 0x20000000.

La forma en que un cortex-m arranca es la palabra de 32 bits en la dirección 0x0000 es la dirección para inicializar el puntero de la pila. No necesito mucha pila para este ejemplo así que 0x20001000 será suficiente, obviamente tiene que haber ram por debajo de esa dirección (la forma en que el brazo empuja, es que resta primero y luego empuja así que si pones 0x20001000 el primer elemento de la pila está en la dirección 0x2000FFFC no tienes que usar 0x2000FFFC). La palabra de 32 bits en la dirección 0x0004 es la dirección del manejador de reinicio, básicamente el primer código que se ejecuta después de un reinicio. Luego hay más controladores de interrupciones y eventos que son específicos para ese núcleo y chip Cortex M, posiblemente hasta 128 o 256, si no los usas entonces no necesitas configurar la tabla para ellos, yo puse algunos para propósitos de demostración. Pero tendrías que asegurarte de tener el vector correcto en la dirección codificada en la lógica para una interrupción/evento particular (por ejemplo una interrupción de datos uart rx, o una interrupción de cambio de estado de pin gpio, así como las instrucciones indefinidas, abortos de datos y demás).

No necesito tratar con .data ni con .bss en este ejemplo porque ya sé que no hay nada en esos segmentos mirando el código. Si lo hubiera me ocuparía de ello, y lo haré en un segundo.

Así que la pila está configurada, comprobado, .data se encarga, comprobado, .bss, comprobado, así que las cosas de C bootstrap están hechas, puede bifurcarse a la función de entrada para C. Debido a que algunos compiladores añadirán basura extra si ven la función main() y en el camino a main, no uso ese nombre exacto, usé notmain() aquí como mi punto de entrada de C. Así que el manejador de reinicio llama a notmain() entonces si/cuando notmain() regresa va a colgar lo cual es solo un bucle infinito, posiblemente mal llamado.

Creo firmemente en el dominio de las herramientas, mucha gente no lo hace, pero lo que encontrarás es que cada desarrollador de bare metal hace su propia cosa, debido a la casi completa libertad, ni remotamente tan restringida como estarías haciendo aplicaciones o páginas web. De nuevo hacen lo suyo. Yo prefiero tener mi propio código bootstrap y linker script. Otros confían en la cadena de herramientas, o juegan en la caja de arena de los vendedores donde la mayor parte del trabajo lo hace otra persona (y si algo se rompe estás en un mundo de dolor, y con el metal desnudo las cosas se rompen a menudo y de manera dramática).

Así que ensamblando, compilando y enlazando con las herramientas gnu obtengo:

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

Entonces, ¿cómo sabe el gestor de arranque dónde están las cosas? Porque el compilador hizo el trabajo. En el primer caso el ensamblador generó el código para flash.s, y al hacerlo sabe dónde están las etiquetas (las etiquetas son sólo direcciones al igual que los nombres de las funciones o los nombres de las variables, etc) así que no tuve que contar bytes y llenar la tabla de vectores manualmente, usé un nombre de etiqueta y el ensamblador lo hizo por mí. Ahora te preguntarás, si reset es la dirección 0x14 por qué el ensamblador puso 0x15 en la tabla de vectores. Bueno, este es un cortex-m y arranca y sólo se ejecuta en modo thumb. Con ARM cuando se bifurca a una dirección si se bifurca al modo pulgar el lsbit necesita ser activado, si el modo brazo entonces el reset. Así que siempre se necesita ese bit establecido. Conozco las herramientas y al poner .thumb_func antes de una etiqueta, si esa etiqueta se usa como está en la tabla de vectores o para bifurcarse a o lo que sea. La cadena de herramientas sabe que debe establecer el lsbit. Así que tiene aquí 0x14|1 = 0x15. De la misma manera para colgar. Ahora el desensamblador no muestra 0x1D para la llamada a notmain() pero no te preocupes las herramientas han construido correctamente la instrucción.

Ahora ese código en notmain, esas variables locales están, no se usan, son código muerto. El compilador incluso comenta ese hecho diciendo que y se establece pero no se utiliza.

Observe el espacio de direcciones, todas estas cosas comienzan en la dirección 0x0000 y van desde allí por lo que la tabla de vectores está correctamente colocada, el espacio .text o de programa también está correctamente colocado, cómo conseguí flash.s delante del código de notmain.c es conociendo las herramientas, un error común es no hacerlo bien y estrellarse y quemarse con fuerza. IMO tienes que desensamblar para asegurarte de que las cosas están bien colocadas antes de arrancar la primera vez, una vez que tienes las cosas en el lugar correcto no necesariamente tienes que comprobar cada vez. Sólo para los nuevos proyectos o si se cuelgan.

Ahora bien, algo que sorprende a algunos es que no hay ninguna razón para esperar que dos compiladores produzcan la misma salida a partir de la misma entrada. O incluso el mismo compilador con diferentes configuraciones. Usando clang, el compilador llvm obtengo estas dos salidas con y sin optimización

llvm/clang optimizado

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

no optimizado

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   b082        sub sp, #8
  1e:   2001        movs    r0, #1
  20:   9001        str r0, [sp, #4]
  22:   2002        movs    r0, #2
  24:   9000        str r0, [sp, #0]
  26:   2000        movs    r0, #0
  28:   b002        add sp, #8
  2a:   4770        bx  lr

así que es una mentira el compilador optimizó la adición, pero asignó dos elementos en la pila para las variables, ya que estas son variables locales que están en la memoria RAM, pero en la pila no en direcciones fijas, verá con los globales que eso cambia. Pero el compilador se dio cuenta de que podía computar y en tiempo de compilación y no había razón para computarlo en tiempo de ejecución así que simplemente colocó un 1 en el espacio de la pila asignado para x y un 2 para el espacio de la pila asignado para y. el compilador "asigna" este espacio con tablas internas declaro pila más 0 para la variable y y pila más 4 para la variable x. el compilador puede hacer lo que quiera mientras el código que implemente se ajuste al estándar C o a las expectativas de un programador C. No hay ninguna razón por la que el compilador tenga que dejar x en la pila + 4 durante la duración de la función, puede moverla tanto como quiera, pero recuerde que los humanos hacen los compiladores y los humanos tienen que depurar los compiladores y usted tiene que equilibrar el mantenimiento y la depuración con el rendimiento, y muy a menudo verá que el código generado por el compilador tiende a configurar un marco de pila una vez y mantener todo en relación con el puntero de la pila durante toda la función.

Si añado una función ficticia en ensamblador

.thumb_func
.globl dummy
dummy:
    bx lr

y luego llamarlo

void dummy ( unsigned int );
int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;
    dummy(y);
    return(0);
}

la salida cambia

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f804   bl  20 <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <dummy>:
  1c:   4770        bx  lr
    ...

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

ahora que tenemos funciones anidadas, la función notmain necesita preservar su dirección de retorno, de modo que pueda clocar la dirección de retorno para la llamada anidada. esto es porque el brazo usa un registro para los retornos, si usara la pila como digamos un x86 o algunos otros bien... todavía usaría la pila pero de manera diferente. Ahora te preguntarás por qué se empuja r4? Bueno, la convención de llamadas no hace mucho cambió para mantener la pila alineada en límites de 64 bits (dos palabras) en lugar de 32 bits, límites de una palabra. Así que necesitan empujar algo para mantener la pila alineada, por lo que el compilador eligió arbitrariamente r4 por alguna razón, no importa por qué. El hecho de que la pila esté en r4 sería un error, ya que según la convención de llamadas para este objetivo, no se puede bloquear r4 en una llamada a una función, sino que se puede bloquear de r0 a r3. r0 es el valor de retorno. Parece que está haciendo una optimización de la cola tal vez, no sé por alguna razón que no utiliza lr para volver.

Pero vemos que la matemática de x e y está optimizada a un valor hardcoded de 2 que se pasa a la función dummy (dummy se codificó específicamente en un archivo separado, en este caso asm, para que el compilador no optimizara la llamada a la función por completo, si tuviera una función dummy que simplemente retornara en C en notmain.c el optimizador habría eliminado la llamada a la función x, y, y dummy porque son todo código muerto/inútil).

También hay que tener en cuenta que debido a que el código de flash.s se hizo más grande notmain está en otro lugar y el toolchain se ha encargado de Parcheando todas las direcciones por nosotros para que no tengamos que hacerlo manualmente.

clang no optimizado como referencia

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   b082        sub sp, #8
  26:   2001        movs    r0, #1
  28:   9001        str r0, [sp, #4]
  2a:   2002        movs    r0, #2
  2c:   9000        str r0, [sp, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   b002        add sp, #8
  36:   bd80        pop {r7, pc}

clang optimizado

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   2002        movs    r0, #2
  26:   f7ff fff9   bl  1c <dummy>
  2a:   2000        movs    r0, #0
  2c:   bd80        pop {r7, pc}

el autor del compilador eligió usar r7 como variable ficticia para alinear la pila, también está creando un puntero de marco usando r7 aunque no tenga nada en el marco de la pila. básicamente la instrucción podría haber sido optimizada. pero usó el pop para devolver no tres instrucciones, eso fue probablemente en mí apuesto que podría conseguir que gcc haga eso con las opciones de línea de comandos correctas (especificando el procesador).

esto debería responder al resto de sus preguntas

void dummy ( unsigned int );
unsigned int x=1;
unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

Ahora tengo globals. así que van en .data o en .bss si no se optimizan.

antes de ver el resultado final veamos el objeto itermedio

00000000 <notmain>:
   0:   b510        push    {r4, lr}
   2:   4b05        ldr r3, [pc, #20]   ; (18 <notmain+0x18>)
   4:   6818        ldr r0, [r3, #0]
   6:   4b05        ldr r3, [pc, #20]   ; (1c <notmain+0x1c>)
   8:   3001        adds    r0, #1
   a:   6018        str r0, [r3, #0]
   c:   f7ff fffe   bl  0 <dummy>
  10:   2000        movs    r0, #0
  12:   bc10        pop {r4}
  14:   bc02        pop {r1}
  16:   4708        bx  r1
    ...

Disassembly of section .data:
00000000 <x>:
   0:   00000001    andeq   r0, r0, r1

el enlazador es el que toma los objetos y los enlaza con la información que se le proporciona (en este caso flash.ld) que le dice donde va el .text y el .data y demás. el compilador no sabe esas cosas, solo puede enfocarse en el código que se le presenta, cualquier externo tiene que dejar un hueco para que el enlazador llene la conexión. Cualquier dato tiene que dejar una manera de enlazar esas cosas juntas, así que las direcciones para todo están basadas en cero aquí simplemente porque el compilador y este desensamblador no lo saben. hay otra información no mostrada aquí que el enlazador usa para colocar cosas. el código aquí es lo suficientemente independiente de la posición para que el enlazador pueda hacer su trabajo.

entonces vemos al menos un desmontaje de la salida vinculada

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   4b05        ldr r3, [pc, #20]   ; (38 <notmain+0x18>)
  24:   6818        ldr r0, [r3, #0]
  26:   4b05        ldr r3, [pc, #20]   ; (3c <notmain+0x1c>)
  28:   3001        adds    r0, #1
  2a:   6018        str r0, [r3, #0]
  2c:   f7ff fff6   bl  1c <dummy>
  30:   2000        movs    r0, #0
  32:   bc10        pop {r4}
  34:   bc02        pop {r1}
  36:   4708        bx  r1
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

Disassembly of section .bss:

20000000 <y>:
20000000:   00000000    andeq   r0, r0, r0

Disassembly of section .data:

20000004 <x>:
20000004:   00000001    andeq   r0, r0, r1

el compilador ha pedido básicamente dos variables de 32 bits en ram. Una está en .bss porque no la inicialicé, así que se asume que se inicializa como cero. la otra es .data porque la inicialicé en la declaración.

Ahora bien, debido a que estas son variables globales se asume que otras funciones pueden modificarlas. el compilador no hace suposiciones en cuanto a cuando notmain puede ser llamado por lo que no puede optimizar con lo que puede ver, la matemática y = x + 1, por lo que tiene que hacer eso en tiempo de ejecución. Tiene que leer de ram las dos variables, sumarlas y guardarlas de nuevo.

Ahora claramente este código no funcionará. ¿Por qué? porque mi bootstrap como se muestra aquí no prepara la ram antes de llamar a notmain, por lo que cualquier basura estaba en 0x20000000 y 0x20000004 cuando el chip se despertó es lo que se utilizará para y y x.

No voy a mostrar eso aquí. puedes leer mi divagación aún más larga sobre .data y .bss y por qué no los necesito nunca en mi código bare metal, pero si sientes que tienes que hacerlo y quieres dominar las herramientas en lugar de esperar que alguien lo haga bien...

https://github.com/dwelch67/raspberrypi/tree/master/bssdata

Los scripts del enlazador, y los bootstraps son de alguna manera específicos del compilador, así que todo lo que aprendes sobre una versión de un compilador podría ser desechado en la siguiente versión o con algún otro compilador, otra razón por la que no pongo una tonelada de esfuerzo en la preparación de los .data y los .bss para ser así de perezoso:

unsigned int x=1;

Preferiría hacer esto

unsigned int x;
...
x = 1;

y dejar que el compilador lo ponga en .text para mí. A veces se ahorra flash de esa manera a veces se quema más. Definitivamente es mucho más fácil programar y portar de una versión de la cadena de herramientas o de un compilador a otro. Mucho más fiable, menos propenso a errores. Sí, no se ajusta al estándar C.

¿Y si hacemos estos globales estáticos?

void dummy ( unsigned int );
static unsigned int x=1;
static unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

así

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

Obviamente, esas variables no pueden ser modificadas por otro código, por lo que el compilador puede ahora, en tiempo de compilación, optimizar el código muerto, como lo hacía antes.

no optimizado

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   4804        ldr r0, [pc, #16]   ; (38 <notmain+0x18>)
  26:   6800        ldr r0, [r0, #0]
  28:   1c40        adds    r0, r0, #1
  2a:   4904        ldr r1, [pc, #16]   ; (3c <notmain+0x1c>)
  2c:   6008        str r0, [r1, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   bd80        pop {r7, pc}
  36:   46c0        nop         ; (mov r8, r8)
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

este compilador que usaba la pila para los locales, ahora usa ram para los globales y este código como está escrito está roto porque no manejé .data ni .bss correctamente.

y una última cosa que no podemos ver en el desmontaje.

:1000000000100020150000001B0000001B00000075
:100010001B00000000F004F8FFE7FEE77047000057
:1000200080B500AF04480068401C04490860FFF731
:10003000F5FF002080BDC046040000200000002025
:08004000E0FFFF7F010000005A
:0400480078563412A0
:00000001FF

He cambiado x para que sea pre-init con 0x12345678. Mi linker script (esto es para gnu ld) tiene esta cosa de ted at bob. que le dice al linker que quiero que el lugar final esté en el espacio de direcciones de ted, pero lo almacena en el binario en el espacio de direcciones de ted y alguien lo moverá por ti. y podemos ver que eso ocurrió. este es el formato hexadecimal de intel. y podemos ver el 0x12345678

:0400480078563412A0

está en el espacio de direcciones flash del binario.

readelf también muestra esto

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  EXIDX          0x010040 0x00000040 0x00000040 0x00008 0x00008 R   0x4
  LOAD           0x010000 0x00000000 0x00000000 0x00048 0x00048 R E 0x10000
  LOAD           0x020004 0x20000004 0x00000048 0x00004 0x00004 RW  0x10000
  LOAD           0x030000 0x20000000 0x20000000 0x00000 0x00004 RW  0x10000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

la línea LOAD donde la dirección virtual es 0x20000004 y la física es 0x48

0 votos

Al principio tengo dos imágenes borrosas de las cosas:

0 votos

1.) "el caso de uso principal es tener las instrucciones en la rom/flash y los datos en la ram." cuando dices "los datos en la RAM aquí", te refieres a los datos generados en el proceso del programa. o también incluyes los datos inicializados. me refiero a que cuando subimos el código a la ROM, ya hay datos inicializados en nuestro código. por ejemplo en nuestro oode si tenemos: int x = 1; int y = x +1; el código anterior hay instrucciones y hay un dato inicial que es 1. (x = 1). este dato también se copia a la RAM o se queda solo en la ROM.

0 votos

2.)¿Cómo sabe el gestor de arranque dónde se colocan los datos y dónde las instrucciones?

9voto

Al pacino Puntos 415

Esta respuesta se va a centrar más en el proceso de arranque. En primer lugar, una corrección: las escrituras en la flash se realizan después de que la MCU (o al menos parte de ella) haya arrancado. En algunos MCUs (normalmente los más avanzados), la propia CPU puede operar los puertos serie y escribir en los registros de la flash. Así que escribir y ejecutar el programa son procesos diferentes. Voy a asumir que el programa ya ha sido escrito en la flash.

Este es el proceso básico de arranque. Voy a nombrar algunas variaciones comunes, pero sobre todo voy a mantener esto simple.

  1. Reiniciar: Hay dos tipos básicos. El primero es un reinicio de encendido, que se genera internamente mientras los voltajes de alimentación están aumentando. El segundo es un cambio de pin externo. En cualquier caso, el reinicio fuerza a todos los flip-flops de la MCU a un estado predeterminado.

  2. Inicialización extra del hardware: Es posible que se necesite más tiempo y/o ciclos de reloj antes de que la CPU comience a funcionar. Por ejemplo, en los MCU de TI en los que trabajo, hay una cadena de exploración de configuración interna que se carga.

  3. Arranque de la CPU: La CPU obtiene su primera instrucción de una dirección especial llamada vector de reinicio. Esta dirección se determina cuando se diseña la CPU. A partir de ahí, es sólo la ejecución normal del programa.

    La CPU repite tres pasos básicos una y otra vez:

    • Buscar: Leer una instrucción (valor de 8, 16 o 32 bits) desde la dirección almacenada en el contador de programas (PC), entonces incrementa el PC.
    • Descodificar: Convertir la instrucción binaria en un conjunto de valores para las señales de control internas de la CPU.
    • Ejecutar: Llevar a cabo la instrucción - sumar dos registros, leer o escribir en la memoria, bifurcarse (cambiar el PC), o lo que sea.

    (En realidad es más complicado que esto. Las CPUs suelen ser en tuberías , lo que significa que pueden estar haciendo cada uno de los pasos anteriores en diferentes instrucciones al mismo tiempo. Cada uno de los pasos anteriores puede tener múltiples etapas de pipeline. Luego están los pipelines paralelos, la predicción de bifurcaciones y todas las cosas sofisticadas de la arquitectura de los ordenadores que hacen que el diseño de esas CPUs de Intel requiera mil millones de transistores).

    Puede que te preguntes cómo funciona la búsqueda. La CPU tiene un autobús que consta de señales de dirección (salida) y de datos (entrada/salida). Para hacer un fetch, la CPU ajusta sus líneas de dirección al valor del contador de programa, y luego envía un reloj por el bus. La dirección se decodifica para habilitar una memoria. La memoria recibe el reloj y la dirección, y pone el valor en esa dirección en las líneas de datos. La CPU recibe este valor. Las lecturas y escrituras de datos son similares, excepto que la dirección proviene de la instrucción o de un valor en un registro de propósito general, no del PC.

    CPUs con un arquitectura von Neumann tienen un único bus que se utiliza tanto para las instrucciones como para los datos. Las CPUs con un Arquitectura de Harvard tienen un bus para las instrucciones y otro para los datos. En los MCU reales, ambos buses pueden estar conectados a las mismas memorias, por lo que a menudo (pero no siempre) es algo de lo que no hay que preocuparse.

    De vuelta al proceso de arranque. Tras el reinicio, el PC se carga con un valor inicial llamado vector de reinicio. Esto puede estar incorporado en el hardware, o (en las CPUs ARM Cortex-M) puede ser leído de la memoria automáticamente. La CPU obtiene la instrucción del vector de reinicio y comienza a recorrer los pasos anteriores. En este punto, la CPU se está ejecutando normalmente.

  4. Cargador de arranque: A menudo hay que hacer alguna configuración de bajo nivel para que el resto de la MCU sea operativa. Esto puede incluir cosas como la limpieza de las memorias RAM y la carga de los ajustes de fabricación de los componentes analógicos. También puede haber una opción para cargar código desde una fuente externa, como un puerto serie o una memoria externa. La MCU puede incluir un ROM de arranque que contiene un pequeño programa para hacer estas cosas. En este caso, el vector de reinicio de la CPU apunta al espacio de direcciones de la ROM de arranque. Esto es básicamente código normal, sólo que el fabricante lo proporciona para que no tengas que escribirlo tú mismo. :-) En un PC, la BIOS es el equivalente a la ROM de arranque.

  5. Configuración del entorno C: C espera tener un pila (área de RAM para almacenar el estado durante las llamadas a funciones) y ubicaciones de memoria inicializadas para las variables globales. Estas son las secciones .stack, .data y .bss de las que habla Dwelch. Las variables globales inicializadas tienen sus valores de inicialización copiados de la flash a la RAM en este paso. Las variables globales no inicializadas tienen direcciones de RAM que están cerca, por lo que todo el bloque de memoria puede ser inicializado a cero muy fácilmente. La pila no necesita ser inicializada (aunque puede serlo) -- todo lo que necesitas hacer es establecer el valor de la CPU puntero de pila para que apunte a una región asignada en la RAM.

  6. Función principal : Una vez configurado el entorno C, el cargador C llama a la función main(). Ahí es donde normalmente comienza el código de tu aplicación. Si quieres, puedes omitir la biblioteca estándar, saltarte la configuración del entorno C y escribir tu propio código para llamar a main(). Algunos MCUs pueden permitirte escribir tu propio cargador de arranque, y entonces puedes hacer toda la configuración de bajo nivel por tu cuenta.

Cosas varias: Muchos MCUs te permitirán ejecutar código fuera de la RAM para mejorar el rendimiento. Esto se suele establecer en la configuración del enlazador. El enlazador asigna dos direcciones a cada función -- una dirección de carga que es donde se almacena el código en primer lugar (normalmente flash), y un dirección de ejecución que es la dirección cargada en el PC para ejecutar la función (flash o RAM). Para ejecutar código fuera de la RAM, se escribe código para que la CPU copie el código de la función desde su dirección de carga en la flash a su dirección de ejecución en la RAM, y luego llame a la función en la dirección de ejecución. El enlazador puede definir variables globales para ayudar con esto. Pero la ejecución de código fuera de la RAM es opcional en los MCUs. Normalmente sólo lo harías si realmente necesitas un alto rendimiento o si quieres reescribir la flash.

1voto

Samuel Danielson Puntos 1043

Su resumen es aproximadamente correcto para el Arquitectura Von Neumann . El código inicial suele cargarse en la RAM a través de un cargador de arranque, pero no (normalmente) un cargador de arranque por software al que el término se refiere comúnmente. Normalmente se trata de un comportamiento "incorporado al silicio". La ejecución del código en esta arquitectura suele implicar un almacenamiento en caché predictivo de las instrucciones de la ROM, de forma que el procesador maximiza su tiempo de ejecución del código y no espera a que se cargue el código en la RAM. He leído en alguna parte que el MSP430 es un ejemplo de esta arquitectura.

En un Arquitectura de Harvard las instrucciones se ejecutan directamente desde la ROM, mientras que el acceso a la memoria de datos (RAM) se realiza a través de un bus independiente. En esta arquitectura, el código simplemente comienza a ejecutarse desde el vector de reinicio. El PIC24 y el dsPIC33 son ejemplos de esta arquitectura.

En cuanto a la activación de los bits que ponen en marcha estos procesos, esto puede variar de un dispositivo a otro y puede implicar depuradores, JTAG, métodos propietarios, etc.

0 votos

Pero te estás saltando algunos puntos rápidamente. Vamos a tomarlo a cámara lenta. Digamos que el código binario "primero" se escribe en la ROM. Ok... Después de eso escribes "Se accede a la memoria de datos".... ¿Pero de dónde vienen los datos "a la RAM" primero en el arranque? ¿Viene de nuevo de la ROM? Y si es así, ¿cómo sabe el cargador de arranque qué parte de la ROM se escribirá en la RAM al principio?

0 votos

Tienes razón, me he saltado muchas cosas. Los demás tienen mejores respuestas. Me alegro de que hayas conseguido lo que buscabas.

i-Ciencias.com

I-Ciencias es una comunidad de estudiantes y amantes de la ciencia en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros usuarios, hacer tus propias preguntas o resolver las de los demás.

Powered by:

X