Arquitetura ARM e notas de estudo da linguagem C (Wei Dongshan) (1) - a essência da linguagem C


prefácio

Aprenda a arquitetura ARM e as notas de estudo da linguagem C de Wei Dongshan na estação B.


1. Operação simples da linguagem C

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

Para completar o passo a++, três passos foram seguidos:
①Read ADDR: A CPU lê o endereço da variável a na memória e obtém os dados contidos nele, que são os dados do valor de a. Depois que a CPU lê os dados, ela salva os dados na estrutura de armazenamento interna da CPU - um registrador, como R0.

② A unidade de cálculo ALU na CPU completa a operação de acumulação do valor de R0.

③Write ADDR: Escreva o valor de R0 no endereço da variável a na memória.

2. Como a CPU sabe executar instruções

1. Apresente o FLASH

Grave o código no FLASH, e ao ligar a energia, a CPU vai pegar as instruções do FLASH e executá-las, conforme a figura abaixo, tome a++ como exemplo: a++; realmente convertido em linguagem assembly
insira a descrição da imagem aqui
, primeiro:
①LDR R0,[addrA]: lê o endereço Os dados de A são carregados no registrador R0, ou seja, Load
②ADD R0, #1: adiciona um ao valor do registrador R0
③STR R0, [addrA]: escreve o valor de R0 para o endereço A, ou seja, salve o Store e
use o FLASH porque pertence a ROM, é para cair A eletricidade ainda pode salvar os dados, então a instrução não será perdida.

O arquivo .HEX gerado pelo KEIL será programado no FLASH do microcontrolador.

2. Registra-se sob o núcleo córtex-m3

insira a descrição da imagem aqui
(1) Registradores de uso geral (R0-R12): usados ​​para chamar instruções para manipulação de dados.
(2) Registrador de ponteiro de pilha (SP): O registrador de pilha é usado como a função de ponteiro de pilha para realizar o primeiro a entrar, o último a sair dos dados.
(3) Registrador de conexão (LR): registrador de endereço de retorno, quando a sub-rotina é chamada, o endereço retornado é armazenado diretamente no registrador de conexão.
(4) Registrador de contagem de programa (PC): usado para armazenar o endereço da próxima instrução executada.
(5) O grupo de registro de função especial é dividido em três categorias: 1. Registro de palavra de status do programa: usado para registrar o status de execução do sinalizador ALU e o número de interrupção sendo atendido no momento. 2. Registrador de máscara de interrupção: desativa a interrupção. 3. Registrador de controle: define o estado privilegiado e decide qual ponteiro de pilha usar.

3. Quais são as variáveis?

1. Variáveis ​​- quantidades que podem mudar

A maior característica das variáveis ​​é que elas podem ser lidas e escritas, o que determina que elas estejam localizadas na memória.

As variáveis ​​são armazenadas na memória porque a memória possui uma grande capacidade de armazenamento e alta velocidade de leitura e gravação, que pode facilmente armazenar e ler dados no programa. Em contraste, a capacidade do registro é pequena, pode armazenar apenas uma pequena quantidade de dados e a velocidade de leitura e gravação é rápida, mas a velocidade de acesso ao registro é mais rápida que a velocidade de acesso à memória. Portanto, as variáveis ​​no programa geralmente são armazenadas primeiro na memória e depois carregadas nos registradores para operação para melhorar a eficiência de execução do programa. No entanto, o que é armazenado no FLASH é o código do programa e os dados constantes, que não são adequados para armazenar dados variáveis. Embora o FLASH seja legível e gravável, é mais complicado escrever nele.

2. Variáveis ​​globais e variáveis ​​locais

Variável global: Uma variável definida fora de uma função é chamada de variável global e seu escopo começa de onde é definido até o final do arquivo. As variáveis ​​globais podem ser acessadas por todo o programa, portanto seu ciclo de vida também é muito longo e não será destruído até o final do programa. O valor das variáveis ​​globais pode ser modificado em qualquer parte do programa, por isso precisa ser usado com cautela para evitar erros desnecessários.

Variável local: Uma variável definida dentro de uma função é chamada de variável local e seu escopo é limitado à função na qual é definida. As variáveis ​​locais são reinicializadas cada vez que a função é chamada, e os valores das variáveis ​​locais são destruídos após o término da execução da função. O escopo das variáveis ​​locais está apenas dentro da função, então você pode evitar conflitos de nomes de variáveis ​​e variáveis ​​globais desnecessárias.
Olha o 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;
}

O segmento de código usa duas palavras-chave, que são explicadas aqui:

① palavra-chave estática

Use static para variáveis ​​locais: Variáveis ​​decoradas com static dentro de uma função são chamadas de variáveis ​​locais estáticas . Esta variável será inicializada apenas uma vez e não será destruída com a saída da função. Ela ainda manterá seu valor e, na próxima vez, o função for chamada, o valor da variável não será redefinido. O escopo das variáveis ​​locais estáticas está apenas dentro da função e não afetará outras funções.

Use static para variáveis ​​globais: Variáveis ​​decoradas com static fora da função são chamadas de variáveis ​​globais estáticas , que só podem ser acessadas por funções neste arquivo e não podem ser acessadas por outros arquivos. O escopo das variáveis ​​globais estáticas está apenas dentro deste arquivo e não afetará outros arquivos.

Use static para funções: Uma função decorada com static fora da função é chamada de static function .Esta função só pode ser chamada por outras funções neste arquivo e não pode ser chamada por outros arquivos. O escopo das funções estáticas está apenas dentro deste arquivo e não afetará outros arquivos.

O arquivo atual refere-se ao arquivo de código-fonte (arquivo .c ou arquivo .cpp, etc.) que define a variável estática. Neste arquivo fonte, a variável estática pode ser acessada em qualquer função, mas as funções definidas em outros arquivos não podem acessar a variável. Isso ocorre porque o escopo das variáveis ​​estáticas é limitado ao escopo do arquivo de origem atual e outros arquivos de origem não podem acessar variáveis ​​nesse escopo.

②palavra-chave volátil

Volatile é uma palavra-chave em linguagem C, que é usada para dizer ao compilador para não otimizar a variável durante a compilação, para garantir que os dados sejam lidos da memória ou gravados na memória toda vez que a variável for acessada, evitando a compilação O compilador otimiza a variável, resultando em um valor inesperado para a variável .

Em um manipulador multiencadeado ou de interrupção, quando uma variável é compartilhada por vários encadeamentos ou manipuladores de interrupção, para garantir a consistência dos dados, a palavra-chave volátil precisa ser usada. Como vários threads ou interrupções podem acessar a variável ao mesmo tempo, se a palavra-chave volátil não for usada, o compilador poderá otimizar a variável, resultando em inconsistência de dados.

3. Análise funcional

Aqui usamos o método de depuração STLINK de KEIL e STM32, compilamos e depuramos o programa, você pode ver:
insira a descrição da imagem aqui

1. volátil int a

Para esta variável local a que não pode ser otimizada pelo compilador, ela será salva temporariamente na pilha. Então, o que é uma pilha?

Pilha (Stack) é uma estrutura de dados, que é uma tabela linear que só pode ser inserida e deletada em uma extremidade. A pilha opera segundo o princípio "primeiro a entrar, último a sair", ou seja, o último elemento inserido é o primeiro a ser excluído. A operação de inserção de elementos na pilha é chamada de "Push" e a operação de exclusão de elementos da pilha é chamada de "Pop".

Em um computador, uma pilha geralmente se refere a uma seção de memória usada quando um programa está sendo executado e é usada para armazenar informações como variáveis ​​locais, parâmetros de função e endereços de retorno quando uma função é chamada. Sempre que uma função é chamada, um espaço é alocado na pilha para armazenar as informações; quando a função retornar, as informações serão retiradas da pilha e o ponteiro da pilha retornará ao topo da pilha da função anterior, e o a execução da função anterior continua. Como a característica da pilha é o último a entrar, o primeiro a sair, quando a função é chamada, a nova função será colocada primeiro na pilha e não será exibida até que a execução seja concluída.
No STM32F103, a estrutura da memória é a seguinte:
insira a descrição da imagem aquiinsira a descrição da imagem aqui
então o espaço da pilha é aberto, obviamente na área interna da SRAM, e o limite é 0x20000000~0x3FFFFFFF endereço base 0x20000000. A área SRAM precisa armazenar variáveis ​​globais, variáveis ​​estáticas, etc., e também abrir um espaço de pilha para armazenar variáveis ​​locais.Portanto, os programadores podem programar para definir o endereço inicial da pilha para distinguir outras variáveis.

2. O papel do compilador

Um compilador é um programa que traduz código-fonte em código-objeto. Ele pode converter o código-fonte (como C, C++, Java, etc.) escrito por programadores em código de máquina que o computador possa entender e executar. Os compiladores geralmente consistem em vários módulos, incluindo pré-processadores, analisadores léxicos, analisadores de sintaxe, analisadores semânticos, otimizadores e geradores de código, entre outros.

As principais funções do compilador incluem:

Convertendo o código-fonte em código-objeto: um compilador traduz o código-fonte escrito por um programador em código de máquina que um computador pode entender e executar. Esse processo inclui várias etapas, como análise léxica, análise sintática, análise semântica, otimização e geração de código.
Otimização do código objeto: O compilador pode otimizar o código objeto gerado, eliminar código redundante, melhorar a eficiência da execução do código, etc., para que o programa possa ser executado mais rapidamente.
Verifique a exatidão do código: O compilador pode verificar o código-fonte quanto a erros de sintaxe, erros de tipo, etc., para melhorar a exatidão e a confiabilidade do programa.
Forneça informações de depuração: o compilador pode adicionar informações de depuração ao código de objeto gerado, para que os programadores possam analisar e solucionar problemas ao depurar o programa.

Em outras palavras, o compilador é muito poderoso e o código que escrevemos será convertido em código de máquina para processamento da CPU.

3. Atribuição e inicialização de variáveis ​​locais

Ponto de conhecimento 1:
SP (Stack Pointer): registrador de ponteiro de pilha, apontando para o endereço do topo da pilha. Durante a execução do programa, a pilha é uma estrutura de dados importante usada para armazenar informações como variáveis ​​locais, parâmetros de função e endereços de retorno quando uma função é chamada. O ponteiro de pilha SP aponta para o topo da pilha e o tamanho da pilha é definido pelo programa.
LR (Link Register): O registrador de link é usado para armazenar o endereço de retorno da instrução de salto. Quando a função é chamada, o registrador LR salvará o endereço quando a função retornar, para que a função retorne ao endereço correto após a execução da função.
PC (Program Counter): O contador de programa é usado para armazenar o endereço da próxima instrução a ser executada. Durante a execução do programa, o PC é constantemente atualizado para apontar a próxima instrução a ser executada, de forma que o programa possa ser executado sequencialmente.

Ponto de conhecimento 2:
char: 1 byte
short: 2 bytes
int: 4 bytes
long: 4 bytes ou 8 bytes (dependendo do compilador e sistema operacional)
float: 4 bytes
double: 8 bytes
long double: 8 bytes ou 16 bytes (dependendo no compilador e sistema operacional)
tipo de ponteiro: 4 bytes (sistema de 32 bits) ou 8 bytes (sistema de 64 bits)

Modifique a função principal, adicione uma matriz de caracteres e atribua um 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) Se você não atribuir valores a b e name, o compilador não alocará um espaço de pilha para ele.
(2)

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

POP {r2-r3, pc} A função desta instrução é retirar 4 bytes da pilha e armazená-los nos registradores r2, r3 e no contador de programa (pc) respectivamente. Essa instrução geralmente é usada para restaurar a cena da pilha e retornar ao local da função de chamada quando a função retornar.
MOVS r0, r0, a função desta instrução é copiar o valor do registrador r0 para o registrador r0. Esta instrução parece não ter nenhum efeito prático, mas na verdade pode ser usada para limpar o valor no registrador r0, pois sobrescreve o valor de r0 para o valor original, o que equivale a não fazer nada.
2.

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

PUSH {lr}, a função desta instrução é colocar o valor do registrador de link (lr) na pilha. Essa instrução geralmente é usada quando uma função é chamada para salvar o endereço de retorno da função na pilha para que ela possa retornar ao local correto após a execução da função.
SUB sp, sp, #0x68
A função desta instrução é subtrair 0x68 do ponteiro da pilha (sp), ou seja, alocar 0x68 bytes de espaço no topo da pilha. Esta instrução é normalmente usada para alocação do quadro de pilha de funções e é usada para salvar as variáveis ​​locais e os dados temporários da função. A variável int ocupa 4 bytes, e o array composto por 100 caracteres ocupa 100 bytes. 0x68 é 104 em decimal, então a CPU subtrai 4 do endereço do topo da pilha, como 0x20010000, ou seja, o valor do retorno LR registrador de endereço. Em seguida, subtraia 104, ou seja, aloque 104 bytes de espaço de pilha para duas variáveis ​​locais.
3.

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

MOV r0, #0x1C8
A função desta instrução é mover o valor imediato 0x1C8 (ou seja, a representação hexadecimal de 456) para o registro r0. Esta instrução é normalmente usada para carregar constantes nos registradores.

STR r0, {sp, #0x00}
A função desta instrução é armazenar o valor no registrador r0 no endereço do ponteiro da pilha (sp) mais o valor imediato 0x00. Esta instrução é normalmente usada para armazenar o valor do registrador na memória, aqui o valor em r0 é armazenado na primeira posição da pilha. Salve o valor de b no endereço da memória.

4. Liberação de variáveis ​​locais

insira a descrição da imagem aqui
Da mesma forma, quando a função add_val(volatile int v) é executada, a CPU cria outra pilha, move o valor imediato para o registrador r0 e salva o valor de r0 no endereço apontado por sp (o endereço apontado por sp mudanças de ponteiro) + próprio endereço.
A operação de v=v+a usa a operação de leitura-acumulação-gravação, mas aqui como a é uma variável local e v é um parâmetro de função, é relativamente complicado. Finalmente, leia o valor de r0 para o endereço apontado por sp.
insira a descrição da imagem aqui
Deve-se notar aqui que a instrução PUSH empurra o valor no registrador para a pilha, e a instrução POP extrai os dados da pilha e os armazena no registrador. Como a função add_val(volatile int v) é executada, a CPU abrirá um espaço de pilha e, após o término da função, a pilha deve ser liberada e a instrução pop pode ser usada para restaurar a cena e liberar o espaço. O registrador sp também apontará para o endereço salvo pela função principal main, aguardando a próxima chamada de função.

Em quarto lugar, a pilha de córtex-m3

A pilha do córtex-m3 é uma pilha completa que cresce para baixo.

Uma pilha completa que cresce para baixo refere-se a um espaço de pilha de tamanho fixo alocado na memória que cresce de endereços altos para endereços baixos. Quando o espaço da pilha está cheio, ocorre um erro de estouro de pilha (estouro de pilha), fazendo com que o programa trave ou se comporte de forma anormal.
No modelo de pilha completa de crescimento descendente, o ponteiro da pilha (Stack Pointer) aponta para o topo atual da pilha e o endereço do topo da pilha se move para baixo conforme a pilha é usada. Quando o uso da pilha excede o tamanho do espaço da pilha, o topo da pilha cruzará a parte inferior da pilha e cobrirá outras áreas de memória, resultando em um erro de estouro de pilha.
O modelo full-stack descendente é geralmente usado em computadores com arquitetura x86. Essa arquitetura usa Little-Endian (Little-Endian), ou seja, os bytes de ordem baixa são armazenados em endereços baixos e os bytes de ordem alta são armazenados em endereços altos. Portanto, o espaço da pilha cresce de endereços altos para endereços baixos, o que está mais de acordo com o método de armazenamento dos computadores.
A pilha do córtex-m3 é uma pilha completa que cresce para baixo e o endereço é reduzido de cima para baixo.O registrador sp primeiro ajusta a posição do ponteiro e depois o armazena na operação.
insira a descrição da imagem aqui

Comece com PUSH e termine com POP. Em PUSH, primeiro ajuste sp menos 4 e armazene-o em r0. Em seguida, ajuste o sp menos 4 e armazene-o em LR. No POP, forneça o valor retornado de v a r2, adicione 4 a sp (ou seja, pop-up, o ponteiro aponta para a posição acima), forneça o valor de r0 a r3, adicione 4 a sp e, finalmente, forneça o valor de LR para PC , é fornecer ao PC o endereço salvo por LR quando a função principal entra na função add, deixe a CPU entrar na função principal para continuar a concluir a operação e, em seguida, adicione quatro ao sp, retorne ao topo da pilha e libere o espaço da pilha da função add.

Acho que você gosta

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