¿Cómo se inicia y se inicia un microcontrolador, paso a paso?


17

Cuando el código C se escribe, compila y carga en un microcontrolador, el microcontrolador comienza 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 de la MCU (memoria, CPU, gestor de arranque). Aquí está (probablemente muy mal) lo que respondería si alguien me preguntara:

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

¿Esto esta mal?

¿Es posible resumir este proceso de arranque y arranque 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 de cómo una PC arranca a través del BIOS. Pero estoy atascado con el proceso de inicio del microcontrolador.

Respuestas:


31

1) el binario compilado se escribe en prom / flash yes. USB, serial, i2c, jtag, etc. depende del dispositivo en cuanto a lo que es compatible con ese dispositivo, irrelevante para comprender el proceso de arranque.

2) Esto generalmente no es cierto para un microcontrolador, el caso de uso principal es tener instrucciones en rom / flash y datos en ram. No importa cuál sea la arquitectura. para un no microcontrolador, su PC, su computadora portátil, su servidor, el programa se copia de no volátil (disco) a ram y luego se ejecuta desde allí. Algunos microcontroladores también le permiten usar ram, incluso aquellos que afirman que Harvard a pesar de que parece violar la definición. No hay nada en Harvard que le impida mapear el ariete en el lado de la instrucción, solo necesita tener un mecanismo para obtener las instrucciones allí después del encendido (lo que viola la definición, pero los sistemas de Harvard tendrían que hacer eso para ser útil). que como microcontroladores).

3) tipo de.

Cada CPU "arranca" de una manera determinista, según lo diseñado. La forma más común es una tabla de vectores donde la dirección para que se ejecuten las primeras instrucciones después del encendido se encuentra en el vector de reinicio, una dirección que lee el hardware y luego usa esa dirección para comenzar a ejecutarse. La otra forma general es hacer que el procesador comience a ejecutarse sin una tabla de vectores en alguna dirección conocida. A veces, el chip tendrá "correas", algunos pines que puede atar alto o bajo antes de liberar el reinicio, que la lógica utiliza para arrancar de diferentes maneras. Debe separar la CPU en sí, el núcleo del procesador del resto del sistema. Comprenda cómo funciona la CPU, y luego comprenda que los diseñadores de chips / sistemas han configurado decodificadores de direcciones alrededor del exterior de la CPU para que parte del espacio de direcciones de la CPU se comunique con un flash, y algunos con ram y algunos con periféricos (uart, i2c, spi, gpio, etc.). Puede tomar el mismo núcleo de CPU si lo desea, y envolverlo de manera diferente. Esto es lo que obtienes cuando compras algo basado en brazos o mips. arm and mips hacen núcleos de CPU, que los chips compran y envuelven sus propias cosas, por varias razones no hacen que esas cosas sean compatibles de una marca a otra. Es por eso que rara vez se puede hacer una pregunta genérica de brazo cuando se trata de algo 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 cpu están todos en el mismo chip junto con una mezcla de periféricos. Pero el chip está diseñado internamente de modo que el flash se asigna al 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 debe haber 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 a partir de 0xF000 para satisfacer esos requisitos. Y tal vez pusieron una cantidad de ram en una dirección más baja o tal vez 0x0000, y los periféricos en algún lugar en el medio.

Otra arquitectura de CPU podría comenzar a ejecutarse en la dirección cero, por lo que tendrían que hacer lo contrario, colocar el flash para que responda a un rango de direcciones alrededor de cero. diga 0x0000 a 0x0FFF por ejemplo. y luego poner un poco de carnero en otro lugar.

Los diseñadores de chips saben cómo arranca la CPU y han colocado allí un almacenamiento no volátil (flash / rom). Depende de la gente del software escribir el código de arranque para que coincida con el comportamiento bien conocido de esa CPU. Debe colocar la dirección del vector de reinicio en el vector de reinicio y su código de arranque en la dirección que definió en el vector de reinicio. La cadena de herramientas puede ayudarte mucho aquí. a veces, especialmente con ideas de apuntar y hacer clic u otros sandboxes, pueden hacer la mayor parte del trabajo por usted, todo lo que hace es llamar a apis en un lenguaje de alto nivel (C).

Sin embargo, sin embargo, el programa cargado en el flash / rom debe coincidir con el comportamiento de arranque cableado de la CPU. Antes de la parte C de su programa main () y si usa main como su punto de entrada, hay que hacer algunas cosas. El programador de AC asume que cuando declaran una variable con un valor inicial, esperan que realmente funcione. Bueno, las variables, además de las constantes, están en ram, pero si tiene una con un valor inicial, ese valor inicial debe estar en ram no volátil. Así que este es el segmento .data y la rutina de carga C necesita copiar cosas .data de flash a ram (donde generalmente lo determina la cadena de herramientas). Se supone que las variables globales que declara sin un valor inicial son cero antes de que comience su programa, aunque realmente no debe suponer eso y, afortunadamente, algunos compiladores comienzan a advertir sobre variables no inicializadas. Este es el segmento .bss, y el C bootstrap ceros que en ram, el contenido, ceros, no tiene que almacenarse en la memoria no volátil, sino la dirección de inicio y cuánto. Nuevamente, la cadena de herramientas te ayuda mucho aquí. Y, por último, lo mínimo es que necesita configurar un puntero de pila ya que los programas C esperan poder tener variables locales y llamar a otras funciones. Entonces, tal vez se hagan otras cosas específicas del chip, o dejemos que el resto de las cosas específicas del chip sucedan en C. no tiene que almacenarse en una memoria no volátil, sino la dirección de inicio y cuánto. Nuevamente, la cadena de herramientas te ayuda mucho aquí. Y, por último, lo mínimo es que necesita configurar un puntero de pila ya que los programas C esperan poder tener variables locales y llamar a otras funciones. Entonces, tal vez se hagan otras cosas específicas del chip, o dejemos que el resto de las cosas específicas del chip sucedan en C. no tiene que almacenarse en una memoria no volátil, sino la dirección de inicio y cuánto. Nuevamente, la cadena de herramientas te ayuda mucho aquí. Y, por último, lo mínimo es que necesita configurar un puntero de pila ya que los programas C esperan poder tener variables locales y llamar a otras funciones. Entonces, tal vez se hagan otras cosas específicas del chip, o dejemos que el resto de las cosas específicas del chip sucedan en C.

Los núcleos de la serie cortex-m del brazo harán algo de esto por usted, el puntero de la pila está en la tabla de vectores, hay un vector de reinicio para señalar el código que se ejecutará después del reinicio, de modo que aparte de lo que tenga que hacer para generar la tabla de vectores (para la cual usas asm de todos modos) puedes usar C puro sin asm. ahora no puede copiar su .data ni poner a cero su .bss, por lo que debe hacerlo usted mismo si quiere intentar ir sin asm en algo basado en cortezax-m. La característica más grande no es el vector de reinicio, sino los vectores de interrupción donde el hardware sigue la convención de llamada C recomendada por los brazos y conserva los registros para usted, y utiliza el retorno correcto para ese vector, de modo que no tenga que envolver el asm correcto alrededor de cada controlador ( o tener directivas específicas de la cadena de herramientas para que su objetivo haga que la cadena de herramientas lo envuelva por usted).

Las cosas específicas del chip pueden ser, por ejemplo, los microcontroladores a menudo se usan en sistemas basados ​​en baterías, por lo que la energía es baja, por lo que algunos salen del reinicio con la mayoría de los periféricos apagados, y debe encender cada uno de estos subsistemas para que pueda usarlos. . Uarts, gpios, etc. A menudo se usa una velocidad de reloj baja, directamente desde un cristal o un oscilador interno. Y el diseño de su sistema puede mostrar que necesita un reloj más rápido, por lo que debe inicializarlo. su reloj puede ser demasiado rápido para el flash o la memoria RAM, por lo que es posible que haya tenido que cambiar los estados de espera antes de subir el reloj. Es posible que necesite configurar el uart, o usb u otras interfaces. entonces su aplicación puede hacer lo suyo.

Una computadora de escritorio, una computadora portátil, un servidor y un microcontrolador no son diferentes en la forma en que arrancan / funcionan. Excepto que no están principalmente en un chip. El programa BIOS a menudo se encuentra en un chip flash / rom separado de la CPU. Aunque recientemente los cpus x86 están extrayendo más y más de lo que solían ser chips de soporte en el mismo paquete (controladores pcie, etc.), pero todavía tiene la mayoría de sus chips ram y rom, pero todavía es un sistema y todavía funciona exactamente Lo mismo en un alto nivel. El proceso de arranque de la CPU es bien conocido, los diseñadores de la placa colocan el flash / rom en el espacio de direcciones donde se inicia la CPU. ese programa (parte del BIOS en una PC x86) hace todas las cosas mencionadas anteriormente, arranca varios periféricos, inicializa dram, enumera los buses pcie, etc. A menudo es bastante configurable por el usuario en función de la configuración de BIOS o lo que solíamos llamar configuración de CMOS, porque en ese momento eso era lo que se usaba la tecnología. No importa, hay configuraciones de usuario que puede ir y cambiar para decirle al código de arranque de la BIOS cómo variar lo que hace.

diferentes personas usarán terminología diferente. un chip arranca, ese es el primer código que se ejecuta. a veces llamado bootstrap. un gestor de arranque con el cargador de palabras a menudo significa que si no hace nada para interferir es un arranque que lo lleva del arranque genérico a algo más grande, su aplicación o sistema operativo. pero la parte del cargador implica que puede interrumpir el proceso de arranque y luego quizás cargar otros programas de prueba. si alguna vez ha usado uboot, por ejemplo, en un sistema Linux integrado, puede presionar una tecla y detener el arranque normal, luego puede descargar un kernel de prueba en ram y arrancarlo en lugar del que está en flash, o puede descargar su programas propios, o puede descargar el nuevo kernel y luego hacer que el gestor de arranque lo escriba en flash para que la próxima vez que arranque ejecute el nuevo material.

En cuanto a la CPU en sí, el procesador central, que no conoce la memoria RAM del flash de los periféricos. No existe una noción de gestor de arranque, sistema operativo, aplicación. Es solo una secuencia de instrucciones que se introducen en la CPU para su ejecución. Estos son términos de software para distinguir entre sí las diferentes tareas de programación. Conceptos de software el uno del otro.

Algunos microcontroladores tienen un gestor de arranque separado proporcionado por el proveedor del chip en un flash separado o en un área de flash separada que es posible que no pueda modificar. En este caso, a menudo hay un pin o un conjunto de pines (los llamo correas) que si los ata alto o bajo antes de que se restablezca el restablecimiento, le está diciendo a la lógica y / o al gestor de arranque qué hacer, por ejemplo, una combinación de correas puede dígale al chip que ejecute ese gestor de arranque y espere en el uart para que los datos se programen en la memoria flash. Configure las correas de la otra manera y su programa no arrancará con el cargador de arranque de los proveedores de chips, lo que permitirá la programación de campo del chip o la recuperación de la falla de su programa. A veces es solo lógica pura lo que le permite programar el flash. Esto es bastante común en estos días,

La razón por la cual la mayoría de los microcontroladores tienen mucho más flash que ram es que el caso de uso principal es ejecutar el programa directamente desde flash y solo tener suficiente ram para cubrir la pila y las variables. Aunque en algunos casos puede ejecutar programas desde ram que debe compilar correctamente 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 una corteza-m0, la corteza-ms funciona igual en lo que respecta a este ejemplo. El chip particular, para este ejemplo, tiene flash de aplicación en la dirección 0x00000000 en el espacio de dirección del brazo y ram en 0x20000000.

La forma en que arranca un cortex-m 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, por lo que 0x20001000 será suficiente, obviamente tiene que haber ram debajo de esa dirección (la forma en que empuja el brazo, se resta primero y luego empuja, así que si configura 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 controlador 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 de corteza, posiblemente hasta 128 o 256, si no los usa, no necesita configurar la tabla para ellos, agregué algunos para demostración propósitos

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

Entonces, la pila está configurada, check, .data cuidada, check, .bss, check, para que el material de arranque de C esté listo, puede ramificarse a la función de entrada para C. Porque algunos compiladores agregarán basura adicional si ven la función main () y en el camino a main, no uso ese nombre exacto, utilicé notmain () aquí como mi punto de entrada C. Entonces, el controlador de reinicio llama a notmain () y luego, si / cuando notmain () regresa, se cuelga, que es solo un bucle infinito, posiblemente mal nombrado.

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

Entonces, al ensamblar, compilar y vincular con 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 solo direcciones como nombres de funciones o nombres de variables, etc.), así que no tuve que contar bytes y completar el vector manualmente, utilicé un nombre de etiqueta y el ensamblador lo hizo por mí. Ahora pregunta, si restablecer 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 solo se ejecuta en modo de pulgar. Con ARM cuando se bifurca a una dirección si se bifurca al modo de pulgar, se debe configurar lsbit, si se activa el modo de armado. Así que siempre necesitas ese conjunto de bits. Conozco las herramientas y al poner .thumb_func antes de una etiqueta, si esa etiqueta se usa tal como está en la tabla de vectores o para bifurcarse o lo que sea. La cadena de herramientas sabe establecer el lsbit. Entonces tiene aquí 0x14 | 1 = 0x15. Lo mismo para colgar. Ahora el desensamblador no muestra 0x1D para la llamada a notmain () pero no se preocupe, las herramientas han construido correctamente las instrucciones.

Ahora que el código no es principal, esas variables locales son, no se usan, son código muerto. El compilador incluso comenta sobre ese hecho diciendo que y se establece pero no se usa.

Tenga en cuenta el espacio de direcciones, todas estas cosas comienzan en la dirección 0x0000 y van desde allí para que la tabla de vectores se coloque correctamente, el espacio de texto o programa también se coloque correctamente, cómo obtuve flash.s delante del código notmain.c es por Conociendo las herramientas, un error común es no hacerlo bien y chocar y quemar duro. En mi opinión, debe desmontar para asegurarse de que las cosas se coloquen justo antes de arrancar la primera vez, una vez que tenga las cosas en el lugar correcto, no necesariamente tiene que verificar cada vez. Solo para nuevos proyectos o si cuelgan.

Ahora, algo que sorprende a algunas personas es que no hay razón alguna para esperar que dos compiladores produzcan la misma salida desde la misma entrada. O incluso el mismo compilador con diferentes configuraciones. Usando clang, el compilador llvm obtengo estos dos resultados 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 que 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 ram pero en la pila no en direcciones fijas, verán con globales que eso cambios Pero el compilador se dio cuenta de que podía calcular y en tiempo de compilación y no había razón para hacerlo en tiempo de ejecución, por lo que simplemente colocó un 1 en el espacio de pila asignado para xy un 2 para el espacio de pila asignado para y. el compilador "asigna" este espacio con tablas internas declaro stack plus 0 para la variable y y stack plus 4 para la variable x. el compilador puede hacer lo que quiera siempre que el código que implemente se ajuste al estándar C o las expectativas de un programador C. No hay ninguna razón por la cual el compilador tenga que dejar x en stack + 4 durante la duración de la función,

Si agrego 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 no principal necesita preservar su dirección de retorno, para que pueda bloquear la dirección de retorno de la llamada anidada. esto se debe a que el brazo usa un registro para devoluciones, si usara la pila como, por ejemplo, un x86 u otros bien ... todavía usaría la pila pero de manera diferente. Ahora preguntas por qué presionó 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 límites de 32 bits y una palabra. Por lo tanto, deben presionar algo para mantener la pila alineada, por lo que el compilador eligió arbitrariamente r4 por alguna razón, no importa por qué. Irrumpir en r4 sería un error, aunque según la convención de llamada para este objetivo, no atacamos a r4 en una llamada de función, podemos golpear r0 a r3. r0 es el valor de retorno. Parece que está haciendo una optimización de la cola, tal vez,

Pero vemos que las matemáticas x e y están optimizadas para un valor codificado de 2 que se pasa a la función ficticia (el ficticio se codificó específicamente en un archivo separado, en este caso asm, para que el compilador no optimice completamente la función, si tuviera una función ficticia que simplemente regresó en C en notmain.c, el optimizador habría eliminado la llamada a la función ficticia x, y, porque todos son código muerto / inútil).

También tenga en cuenta que debido a que el código flash.s se hizo más grande, no es mayor y la cadena de herramientas se ha encargado de reparar todas las direcciones para que no tengamos que hacerlo manualmente.

sonido metálico no optimizado para 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}

sonido metálico 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}

ese autor del compilador eligió usar r7 como la variable ficticia para alinear la pila, también está creando un puntero de marco usando r7 a pesar de que no tiene nada en el marco de la pila. básicamente la instrucción podría haberse optimizado. pero usó el pop para devolver no tres instrucciones, eso probablemente fue sobre mí, apuesto a que podría hacer que gcc lo haga con las opciones de línea de comando correctas (especificando el procesador).

esto debería responder principalmente 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);
}

Tengo globals ahora. entonces entran en .data o .bss si no se optimizan.

antes de ver el resultado final, veamos el objeto intermedio

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

ahora falta información de esto, pero da una idea de lo que está sucediendo, el enlazador es el que toma objetos y los vincula con la información proporcionada (en este caso flash.ld) que le dice dónde .text y. datos y tal va. el compilador no sabe tales cosas, solo puede enfocarse en el código que se presenta, cualquier externo debe dejar un hueco para que el enlazador complete la conexión. Cualquier dato tiene que dejar una forma de vincular esas cosas, por lo 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 que no se muestra aquí que el enlazador utiliza 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 básicamente ha pedido dos variables de 32 bits en ram. Uno está en .bss porque no lo inicialicé, por lo que se supone que se inicia como cero. el otro es .data porque lo inicialicé en la declaración.

Ahora, debido a que estas son variables globales, se supone que otras funciones pueden modificarlas. el compilador no hace suposiciones sobre cuándo no se puede llamar a main, por lo que no puede optimizar con lo que puede ver, las matemáticas y = x + 1, por lo que tiene que hacer ese tiempo de ejecución. Tiene que leer de RAM las dos variables agregarlas y guardar de nuevo.

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

No voy a mostrar eso aquí. puedes leer mis divagaciones aún más largas en .data y .bss y por qué nunca las necesito en mi código de metal desnudo, pero si sientes que tienes que hacerlo y quieres dominar las herramientas en lugar de esperar que alguien más lo haya hecho bien ... .

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

los scripts del enlazador y los bootstraps son algo específicos del compilador, por lo que todo lo que aprenda sobre una versión de un compilador podría lanzarse en la próxima versión o con algún otro compilador, otra razón más por la que no pongo un montón de esfuerzo en la preparación de .data y .bss solo para ser tan flojo:

unsigned int x=1;

Prefiero hacer esto

unsigned int x;
...
x = 1;

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

ahora, ¿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);
}

bien

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 ahora puede en tiempo de compilación optimizar el código muerto, como lo hizo 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 locales, ahora usa ram para 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

Cambié x para ser preinicial con 0x12345678. Mi script de enlazador (esto es para gnu ld) tiene este ted en bob. eso le dice al enlazador que quiero que el lugar final esté en el espacio de direcciones ted, pero guárdelo en el binario en el espacio de direcciones ted y alguien lo moverá por usted. Y podemos ver que sucedió. Este es el formato Intel hexadecimal. 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


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

1.) "el caso de uso principal es tener instrucciones en rom / flash y datos en ram". cuando dice "datos en RAM aquí", se refiere a los datos generados en el proceso del programa. o también incluye los datos inicializados. Es decir, cuando cargamos el código a la ROM, ya hay datos inicializados en nuestro código. por ejemplo en nuestro modo si tenemos: int x = 1; int y = x +1; En el código anterior hay instrucciones y hay un dato inicial que es 1. (x = 1). estos datos también se copian en la RAM o se quedan solo en la ROM.
user16307

13
ja, ahora sé el límite de caracteres para una respuesta de intercambio de pila!
old_timer

2
Debes escribir un libro explicando tales conceptos a los novatos. "Tengo millones de ejemplos en github" - ¿Es posible compartir algunos ejemplos?
AkshayImmanuelD

1
Lo acabo de hacer. No es uno que haga nada útil, pero aún así es un ejemplo de código para un microcontrolador. Y puse un enlace de github desde el que puedes encontrar todo lo demás que he compartido, bueno, malo o de otro tipo.
old_timer

8

Esta respuesta se centrará más en el proceso de arranque. Primero, una corrección: las escrituras en flash se realizan después de que la MCU (o al menos parte de ella) ya se ha iniciado. En algunas MCU (generalmente las más avanzadas), la CPU misma puede operar los puertos seriales y escribir en los registros flash. Así que escribir y ejecutar el programa son procesos diferentes. Voy a suponer que el programa ya se ha escrito en flash.

Aquí está el proceso básico de arranque. Voy a nombrar algunas variaciones comunes, pero sobre todo estoy manteniendo esto simple.

  1. Restablecer: hay dos tipos básicos. El primero es un reinicio de encendido, que se genera internamente mientras los voltajes de suministro aumentan. El segundo es un interruptor de clavija externo. En cualquier caso, el reinicio fuerza a todos los flip-flops en la MCU a un estado predeterminado.

  2. Inicialización de hardware adicional: es posible que se necesiten más tiempo y / o ciclos de reloj antes de que la CPU comience a funcionar. Por ejemplo, en las MCU de TI en las que trabajo, hay una cadena de escaneo 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 solo la ejecución normal del programa.

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

    • Recuperar: lea una instrucción (valor de 8, 16 o 32 bits) de la dirección almacenada en el registro del contador del programa (PC), luego incremente la PC.
    • Decodificación: Convierta la instrucción binaria en un conjunto de valores para las señales de control interno de la CPU.
    • Ejecutar: Lleve a cabo la instrucción: agregue dos registros, lea o escriba en la memoria, ramifique (cambie la PC) o lo que sea.

    (En realidad, es más complicado que esto. Las CPU generalmente están canalizadas , 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 canalización. Luego hay tuberías paralelas, predicción de ramificación , y todas las cosas de arquitectura de computadora sofisticadas que hacen que esas CPU Intel tomen un billón de transistores para diseñar).

    Tal vez se pregunte cómo funciona la búsqueda. La CPU tiene un bus que consta de señales de dirección (salida) y datos (entrada / salida). Para realizar una búsqueda, la CPU establece sus líneas de dirección en el valor del contador del 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 coloca 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 de la PC.

    Las CPU con una arquitectura von Neumann tienen un solo bus que se usa tanto para instrucciones como para datos. Las CPU con arquitectura Harvard tienen un bus para instrucciones y otro para datos. En MCU reales, ambos buses pueden estar conectados a los mismos recuerdos, por lo que a menudo (pero no siempre) es algo de lo que no tiene que preocuparse.

    Volver al proceso de arranque. Después del reinicio, la PC se carga con un valor inicial llamado vector de reinicio. Esto puede integrarse en el hardware o (en las CPU ARM Cortex-M) puede leerse automáticamente de la memoria. La CPU obtiene las instrucciones 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 alguna configuración de bajo nivel que debe hacerse para que el resto de la MCU esté operativa. Esto puede incluir cosas como borrar RAM y cargar ajustes de ajuste de fabricación para componentes analógicos. También puede haber una opción para cargar código desde una fuente externa como un puerto serie o memoria externa. La MCU puede incluir una 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. Este es básicamente un código normal, solo lo proporciona el fabricante para que no tenga que escribirlo usted mismo. :-) En una PC, el BIOS es el equivalente de la ROM de arranque.

  5. Configuración del entorno C: C espera tener una pila (área 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 Dwelch está hablando. Las variables globales inicializadas tienen sus valores de inicialización copiados de flash a RAM en este paso. Las variables globales no inicializadas tienen direcciones RAM que están muy juntas, por lo que todo el bloque de memoria se puede inicializar a cero con mucha facilidad. La pila no necesita ser inicializada (aunque puede serlo); todo lo que realmente necesita hacer es configurar el registro del puntero de la pila de la CPU para que apunte a una región asignada en la RAM.

  6. Función principal : una vez que se configura el entorno C, el cargador C llama a la función main (). Ahí es donde normalmente comienza su código de aplicación. Si lo desea, puede omitir la biblioteca estándar, omitir la configuración del entorno C y escribir su propio código para llamar a main (). Algunas MCU pueden permitirle escribir su propio gestor de arranque, y luego puede hacer toda la configuración de bajo nivel por su cuenta.

Cosas diversas: muchas MCU le permitirán ejecutar código fuera de RAM para un mejor rendimiento. Esto generalmente se configura en la configuración del vinculador. El vinculador asigna dos direcciones a cada función: una dirección de carga , que es donde se almacena el código por primera vez (generalmente flash), y una dirección de ejecución , que es la dirección cargada en la PC para ejecutar la función (flash o RAM). Para ejecutar el código desde la RAM, debe escribir el código para hacer que la CPU copie el código de la función desde su dirección de carga en flash a su dirección de ejecución en la RAM, luego llame a la función en la dirección de ejecución. El enlazador puede definir variables globales para ayudar con esto. Pero ejecutar código fuera de RAM es opcional en MCU. Normalmente solo lo haría si realmente necesita un alto rendimiento o si desea reescribir el flash.


1

Su resumen es aproximadamente correcto para la arquitectura de Von Neumann . El código inicial generalmente se carga en la RAM a través de un gestor de arranque, pero no (normalmente) un gestor de arranque de software al que comúnmente se refiere el término. Esto es normalmente un comportamiento de "cocido en el silicio". La ejecución de código en esta arquitectura a menudo implica un almacenamiento en caché predictivo o instrucciones de la ROM de tal manera que el procesador maximice su tiempo de ejecución del código y no espere a que el código se cargue en la RAM. Leí en alguna parte que el MSP430 es un ejemplo de esta arquitectura.

En un dispositivo de Harvard Architecture , las instrucciones se ejecutan directamente desde la ROM mientras se accede a la memoria de datos (RAM) a través de un bus separado. 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 al giro real de bits que inicia estos procesos, eso puede variar de un dispositivo a otro y puede involucrar depuradores, JTAG, métodos patentados, etc.


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

Tienes razón, salté mucho. Los otros chicos tienen mejores respuestas. Me alegra que hayas conseguido lo que estabas buscando.
ligeramente
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.