Notas de estudio de arquitectura ARM y lenguaje C (Wei Dongshan) (1) - la esencia del lenguaje C


prefacio

Aprenda la arquitectura ARM y las notas de estudio del lenguaje C de Wei Dongshan en la estación B.


1. Operación simple del lenguaje C

#include "stdio.h"
int main(){
    
    
    int a=1;
    a++;
    return 0;
}

Para completar el paso a++, se han realizado tres pasos:
①Leer ADDR: La CPU lee la dirección de la variable a en la memoria y obtiene los datos en ella, que son los datos de valor de a. Después de que la CPU lee los datos, los guarda en la estructura de almacenamiento interna de la CPU: un registro, como R0.

② La unidad de cálculo ALU en la CPU completa la operación de acumulación del valor de R0.

③Escribir DIRECCIÓN: Escriba el valor de R0 en la dirección de la variable a en la memoria.

2. ¿Cómo sabe la CPU ejecutar instrucciones?

1. Introducir FLASH

Grabe el código en la FLASH, y cuando se encienda la alimentación, la CPU extraerá las instrucciones de la FLASH y las ejecutará, como se muestra en la siguiente figura, tome a++ como ejemplo: a++; en realidad convertido a lenguaje ensamblador
inserte la descripción de la imagen aquí
, primero:
①LDR R0,[addrA]: lee la dirección Los datos de A se cargan en el registro R0, es decir, carga
②ADD R0, #1: suma uno al valor del registro R0
③STR R0, [addrA]: escribe el valor de R0 a la dirección A, es decir, guarde la tienda y
use FLASH porque pertenece a la ROM, es para soltar La electricidad aún puede guardar los datos, por lo que la instrucción no se perderá.

El archivo .HEX generado por KEIL se programará en el FLASH del microcontrolador.

2. Registros bajo el núcleo cortex-m3

inserte la descripción de la imagen aquí
(1) Registros de propósito general (R0-R12): utilizados para llamar instrucciones para la manipulación de datos.
(2) Registro de puntero de pila (SP): El registro de pila se utiliza como función de puntero de pila para realizar el primero en entrar, el último en salir de los datos.
(3) Registro de conexión (LR): registro de dirección de retorno, cuando se llama a la subrutina, la dirección devuelta se almacena directamente en el registro de conexión.
(4) Registro de conteo de programa (PC): se utiliza para almacenar la dirección de la siguiente instrucción ejecutada.
(5) El grupo de registro de función especial se divide en tres categorías: 1. Registro de palabra de estado del programa: se utiliza para registrar el estado de ejecución de la bandera ALU y el número de interrupción que se está sirviendo actualmente. 2. Registro de máscara de interrupción: deshabilitar la interrupción. 3. Registro de control: define el estado privilegiado y decide qué puntero de pila usar.

3. ¿Cuáles son las variables?

1. Variables: cantidades que pueden cambiar

La característica más importante de las variables es que se pueden leer y escribir, lo que determina que estén ubicadas en la memoria.

Las variables se almacenan en la memoria porque la memoria tiene una gran capacidad de almacenamiento y una alta velocidad de lectura y escritura, lo que puede almacenar y leer datos fácilmente en el programa. Por el contrario, la capacidad del registro es pequeña, solo puede almacenar una pequeña cantidad de datos y la velocidad de lectura y escritura es rápida, pero la velocidad de acceso al registro es más rápida que la velocidad de acceso a la memoria. Por lo tanto, las variables en el programa generalmente se almacenan primero en la memoria y luego se cargan en registros para que la operación mejore la eficiencia de ejecución del programa. Sin embargo, lo que se almacena en FLASH es código de programa y datos constantes, lo cual no es adecuado para almacenar datos variables.Aunque FLASH es legible y escribible, es más complicado escribir en él.

2. Variables globales y variables locales

Variable global: una variable definida fuera de una función se denomina variable global y su alcance comienza desde donde se define hasta el final del archivo. Todo el programa puede acceder a las variables globales, por lo que su ciclo de vida también es muy largo y no se destruirá hasta el final del programa. El valor de las variables globales se puede modificar en cualquier parte del programa, por lo que debe usarse con precaución para evitar errores innecesarios.

Variable local: una variable definida dentro de una función se denomina variable local y su alcance se limita a la función en la que se define. Las variables locales se reinicializan cada vez que se llama a la función, y los valores de las variables locales se destruyen una vez que la función termina de ejecutarse. El alcance de las variables locales está solo dentro de la función, por lo que puede evitar conflictos de nombres de variables y variables globales innecesarias.
Mira el código:

#include "stdio.h"
int add_val(volatile int v){
    
    
    volatile int a=321;
    v=v+a;
    return v;
}
int main(){
    
    
    static volatile int s_a=1;
    volatile int b;
    b=add_val(s_a);
    return 0;
}

El segmento de código utiliza dos palabras clave, que se explican aquí:

① palabra clave estática

Use static para variables locales: las variables decoradas con static dentro de una función se denominan variables locales estáticas . Esta variable solo se inicializará una vez y no se destruirá con la salida de la función. Todavía mantendrá su valor, y la próxima vez que se llama a la función, el valor de la variable no se restablecerá. El alcance de las variables locales estáticas está solo dentro de la función y no afectará a otras funciones.

Use static para variables globales: las variables decoradas con static fuera de la función se denominan variables globales estáticas , a las que solo pueden acceder las funciones en este archivo y no pueden acceder otros archivos. El alcance de las variables globales estáticas está solo dentro de este archivo y no afectará a otros archivos.

Use static para funciones: una función decorada con static fuera de la función se denomina función estática Esta función solo puede ser llamada por otras funciones en este archivo y no puede ser llamada por otros archivos. El alcance de las funciones estáticas está solo dentro de este archivo y no afectará a otros archivos.

El archivo actual hace referencia al archivo de código fuente (archivo .c o archivo .cpp, etc.) que define la variable estática. En este archivo fuente, se puede acceder a la variable estática en cualquier función, pero las funciones definidas en otros archivos no pueden acceder a la variable. Esto se debe a que el alcance de las variables estáticas está limitado al alcance del archivo fuente actual y otros archivos fuente no pueden acceder a las variables en este alcance.

② palabra clave volátil

Volatile es una palabra clave en lenguaje C, que se usa para decirle al compilador que no optimice la variable durante la compilación, para garantizar que los datos se lean de la memoria o se escriban en la memoria cada vez que se acceda a la variable, evitando la compilación. El compilador optimiza la variable, resultando en un valor inesperado para la variable .

En un controlador de interrupciones o de subprocesos múltiples, cuando una variable es compartida por varios subprocesos o controladores de interrupciones, para garantizar la consistencia de los datos, se debe usar la palabra clave volatile. Dado que varios subprocesos o interrupciones pueden tener acceso a la variable al mismo tiempo, si no se usa la palabra clave volatile, el compilador puede optimizar la variable, lo que genera incoherencia en los datos.

3. Análisis de funciones

Aquí usamos el método de depuración STLINK de KEIL y STM32, compilamos y depuramos el programa, puedes ver:
inserte la descripción de la imagen aquí

1.volátil en un

Para esta variable local que no puede ser optimizada por el compilador, se guardará temporalmente en la pila. Entonces, ¿qué es una pila?

Stack (Pila) es una estructura de datos, que es una tabla lineal que solo se puede insertar y eliminar en un extremo. La pila funciona según el principio de "primero en entrar, último en salir", es decir, el último elemento insertado es el primero en eliminarse. La operación de insertar elementos en la pila se llama "Push", y la operación de eliminar elementos de la pila se llama "Pop".

En una computadora, una pila generalmente se refiere a una sección de memoria utilizada cuando se ejecuta un programa y se usa para almacenar información como variables locales, parámetros de funciones y direcciones de retorno cuando se llama a una función. Cada vez que se llama a una función, se asigna un espacio en la pila para almacenar la información; cuando la función regresa, la información se sacará de la pila y el puntero de la pila vuelve a la parte superior de la pila de la función anterior, y el continúa la ejecución de la función anterior. Debido a que la característica de la pila es el último en entrar, primero en salir, cuando se llama a la función, la nueva función se insertará primero en la pila y no aparecerá hasta que se complete la ejecución.
En STM32F103, la estructura de la memoria es la siguiente:
inserte la descripción de la imagen aquíinserte la descripción de la imagen aquí
el espacio de la pila se abre, obviamente en el área interna de SRAM, y el límite es 0x20000000~0x3FFFFFFF, dirección base 0x20000000. El área SRAM necesita almacenar variables globales, variables estáticas, etc., y también abrir un espacio de pila para almacenar variables locales. Por lo tanto, los programadores pueden programar para establecer la dirección inicial de la pila para distinguir otras variables.

2. El papel del compilador

Un compilador es un programa que traduce el código fuente en código objeto. Puede convertir el código fuente (como C, C++, Java, etc.) escrito por programadores en código de máquina que la computadora puede entender y ejecutar. Los compiladores generalmente constan de múltiples módulos, incluidos preprocesadores, analizadores léxicos, analizadores de sintaxis, analizadores semánticos, optimizadores y generadores de código, entre otros.

Las funciones principales del compilador incluyen:

Conversión de código fuente en código objeto: un compilador traduce el código fuente escrito por un programador en código de máquina que una computadora puede entender y ejecutar. Este proceso incluye múltiples etapas como análisis léxico, análisis de sintaxis, análisis semántico, optimización y generación de código.
Optimización del código objeto: el compilador puede optimizar el código objeto generado, eliminar el código redundante, mejorar la eficiencia de ejecución del código, etc., para que el programa pueda ejecutarse más rápido.
Verifique la corrección del código: el compilador puede verificar el código fuente en busca de errores de sintaxis, errores de tipo, etc. para mejorar la corrección y confiabilidad del programa.
Proporcionar información de depuración: el compilador puede agregar información de depuración al código objeto generado, de modo que los programadores puedan analizar y solucionar problemas al depurar el programa.

En otras palabras, el compilador es muy poderoso y el código que escribimos se convertirá en código de máquina para el procesamiento de la CPU.

3. Asignación e inicialización de variables locales

Punto de conocimiento 1:
SP (Stack Pointer): registro de puntero de pila, que apunta a la dirección superior de la pila. Durante la ejecución del programa, la pila es una estructura de datos importante que se utiliza para almacenar información, como variables locales, parámetros de funciones y direcciones de retorno cuando se llama a una función. El puntero de pila SP apunta a la parte superior de la pila, y el programa establece el tamaño de la pila.
LR (registro de enlace): el registro de enlace se utiliza para almacenar la dirección de retorno de la instrucción de salto. Cuando se llama a la función, el registro LR guardará la dirección cuando la función regrese, de modo que la función regrese a la dirección correcta después de que se ejecute la función.
PC (Contador de programa): El contador de programa se utiliza para almacenar la dirección de la siguiente instrucción a ejecutar. Durante la ejecución del programa, la PC se actualiza constantemente para apuntar a la siguiente instrucción a ejecutar, de modo que el programa pueda ejecutarse secuencialmente.

Punto de conocimiento 2:
char: 1 byte
corto: 2 bytes
int: 4 bytes
de largo: 4 bytes u 8 bytes (según el compilador y el sistema operativo)
float: 4 bytes
doble: 8 bytes
de largo doble: 8 bytes o 16 bytes (según en el compilador y el sistema operativo)
tipo de puntero: 4 bytes (sistema de 32 bits) u 8 bytes (sistema de 64 bits)

Modifique la función principal, agregue una matriz de caracteres y asigne un valor.

int main(){
    
    
    static volatile int s_a=1;
    volatile int b = 456;
    volatile char name[100];
    name[0]='A';
    b=add_val(s_a);
    return 0;
}

(1) Si no asigna valores a b y nombre, entonces el compilador inteligentemente no le asignará un espacio de pila.
(2)

POP {r2-r3, pc}
MOVS r0, r0

POP {r2-r3, pc} La función de esta instrucción es extraer 4 bytes de la pila y almacenarlos en los registros r2, r3 y el contador de programa (pc) respectivamente. Esta instrucción generalmente se usa para restaurar la escena de la pila y volver a la ubicación de la función de llamada cuando la función regresa.
MOVS r0, r0, la función de esta instrucción es copiar el valor del registro r0 al registro r0. Esta instrucción parece no tener ningún efecto práctico, pero en realidad se puede usar para borrar el valor en el registro r0, porque sobrescribe el valor de r0 al valor original, lo que equivale a no hacer nada.
2.

PUSH {lr}
SUB sp, sp, #0x68

PUSH {lr}, la función de esta instrucción es empujar el valor del registro de enlace (lr) a la pila. Esta instrucción generalmente se usa cuando se llama a una función para guardar la dirección de retorno de la función en la pila para que pueda regresar a la ubicación correcta después de ejecutar la función.
SUB sp, sp, #0x68
La función de esta instrucción es restar 0x68 del puntero de pila (sp), es decir, asignar 0x68 bytes de espacio en la parte superior de la pila. Esta instrucción generalmente se usa para la asignación del marco de pila de la función y se usa para guardar las variables locales y los datos temporales de la función. La variable int ocupa 4 bytes, y la matriz compuesta por 100 caracteres ocupa 100 bytes.0x68 es 104 en decimal, por lo que la CPU resta 4 de la dirección superior de la pila, como 0x20010000, es decir, el valor de la devolución LR registro de dirección Luego reste 104, es decir, asigne 104 bytes de espacio de pila para dos variables locales.
3.

MOV r0, #0x1C8
STR r0, {sp, #0x00}

MOV r0, #0x1C8
La función de esta instrucción es mover el valor inmediato 0x1C8 (es decir, la representación hexadecimal de 456) al registro r0. Esta instrucción se usa típicamente para cargar constantes en registros.

STR r0, {sp, #0x00}
La función de esta instrucción es almacenar el valor en el registro r0 en la dirección del puntero de pila (sp) más el valor inmediato 0x00. Esta instrucción generalmente se usa para almacenar el valor en el registro en la memoria, aquí el valor en r0 se almacena en la primera posición de la pila. Guarde el valor de b en la dirección en la memoria.

4. Liberación de variables locales

inserte la descripción de la imagen aquí
De manera similar, cuando se ejecuta la función add_val(volatile int v), la CPU crea otra pila, mueve el valor inmediato al registro r0 y guarda el valor de r0 en la dirección a la que apunta sp (la dirección a la que apunta sp cambios de puntero) + dirección propia.
La operación de v=v+a usa la operación de lectura-acumulación-escritura, pero aquí debido a que a es una variable local y v es un parámetro de función, es relativamente complicada. Finalmente, lea el valor de r0 en la dirección señalada por sp.
inserte la descripción de la imagen aquí
Cabe señalar aquí que la instrucción PUSH empuja el valor del registro a la pila, y la instrucción POP extrae los datos de la pila y los almacena en el registro. Debido a que se ejecuta la función add_val(volatile int v), la CPU abrirá un espacio de pila, y después de que finalice la función La pila debe liberarse, y la instrucción pop se puede usar para restaurar la escena y liberar el espacio.El registro sp también apuntará a la dirección guardada por la función principal principal, esperando la siguiente llamada de función.

Cuarto, la pila de corteza-m3

La pila de corteza-m3 es una pila completa que crece hacia abajo.

Una pila completa que crece hacia abajo se refiere a un espacio de pila de tamaño fijo asignado en la memoria que crece de direcciones altas a direcciones bajas. Cuando el espacio de la pila está lleno, se produce un error de desbordamiento de la pila (Stack Overflow), lo que hace que el programa se bloquee o se comporte de forma anormal.
En el modelo de pila completa de crecimiento hacia abajo, el puntero de pila (Stack Pointer) apunta a la parte superior actual de la pila, y la dirección de la parte superior de la pila se mueve hacia abajo a medida que se usa la pila. Cuando el uso de la pila excede el tamaño del espacio de la pila, la parte superior de la pila cruzará la parte inferior de la pila y cubrirá otras áreas de la memoria, lo que provocará un error de desbordamiento de la pila.
El modelo de pila completa de crecimiento descendente generalmente se usa en computadoras con arquitectura x 86. Esta arquitectura usa Little-Endian (Little-Endian), es decir, los bytes de orden bajo se almacenan en direcciones bajas y los bytes de orden alto son almacenado en direcciones altas. Por lo tanto, el espacio de pila crece de direcciones altas a direcciones bajas, lo que está más en línea con el método de almacenamiento de las computadoras.
La pila de cortex-m3 es una pila completa que crece hacia abajo y la dirección se reduce de arriba hacia abajo. El registro sp primero ajusta la posición del puntero y luego lo almacena en la operación.
inserte la descripción de la imagen aquí

Comience con PUSH y termine con POP. En PUSH, primero ajuste sp menos 4 y guárdelo en r0. Luego ajuste el sp menos 4 y guárdelo en LR. En POP, proporcione el valor devuelto de v a r2, agregue 4 a sp (es decir, emergente, el puntero apunta a la posición anterior), luego proporcione el valor de r0 a r3, agregue 4 a sp y finalmente proporcione el El valor de LR a PC, es darle a la PC la dirección guardada por LR cuando la función principal ingresa a la función de agregar, deje que la CPU ingrese a la función principal para continuar para completar la operación, y luego agregue cuatro al sp, regrese al la parte superior de la pila y libere el espacio de la pila de la función de adición.

Supongo que te gusta

Origin blog.csdn.net/qq_53092944/article/details/131022356
Recomendado
Clasificación