Estudando o conjunto de instruções do processador ARM. Estudando o conjunto de instruções do processador ARM Exemplos de instruções assembler ARM

GBA ASM - Dia 2: Algumas informações sobre o montador ARM - Arquivo WASM.RU

ARM é a empresa que fabrica o processador GBA. Os processadores ARM são processadores RISC (em oposição aos processadores INTEL). RISC significa Computadores com Conjunto de Instruções Reduzido (CISC - Complexo...). Embora esses processadores não tenham muitas instruções (o que é uma coisa boa), as instruções ARM (e talvez outros processadores RISC, não sei) têm muitos propósitos e combinações diferentes, o que torna os processadores RISC tão poderosos quanto são. .

Registros

Não conheço outros processadores ARM, mas o usado no GBA possui 16 registros e, diferentemente dos processadores Intel (e outros), todos os registros podem ser usados ​​com segurança (normalmente). Os registros são os seguintes:

r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,r13,r14,r15

Uau! Um monte de! Vou explicar em ordem.

ro: Faça o que quiser!

r2 a r12: mesmo

r13: Em alguns sistemas ARM, r13 é um stack pointer (SP em processadores INTEL). Não tenho certeza se o r13 desempenha a mesma função no GBA, só posso alertar para ter cuidado com ele ao trabalhar com a pilha.

r14: Contém o endereço de retorno dos procedimentos chamados. Se você não os usar, poderá fazer o que quiser com eles.

r15: Contador de programa e sinalizadores, iguais ao IP (Instruction Pointer na Intel). Ele difere do registro IP da Intel porque você tem acesso livre a ele como qualquer outro registro, mas observe que alterá-lo fará com que o controle seja transferido para outra seção do código e os sinalizadores serão alterados.

Vamos fazer um pouco de matemática... 16 menos 3 (normalmente) nos dá 13 registros. Não é legal? Acalmar.

Agora você pode estar se perguntando o que realmente são os registros. Os registradores são áreas especiais da memória que fazem parte do processador e não possuem endereço real, mas são conhecidos apenas pelos seus nomes. Os registros são de 32 bits. Quase tudo em qualquer linguagem assembly usa registros, então você deve conhecê-los tão bem quanto seus primos.

Instruções do montador ARM

Primeiramente, quero começar dizendo que na minha opinião quem inventou o montador ARM é um gênio.

Em segundo lugar, quero apresentar o meu bom amigo CMP. Diga oi para ele e talvez, apenas talvez, ele também se torne seu amigo. CMP significa CoMPare (comparar). Esta instrução pode comparar registro e número, registro e registro ou registro e localização de memória. Então, após a comparação, o CMP define sinalizadores de status que informam o resultado da comparação. Como você deve se lembrar, o registrador r15 contém sinalizadores. Eles relatam o resultado da comparação. A instrução CMP foi projetada especificamente para definir o valor desses sinalizadores e nada mais.

Os sinalizadores podem conter os seguintes estados:

    EQ EQual / Igual

    NE não é igual

    Conjunto de excesso de VS

    VC oVerflow claro

    HI Mais alto / Mais alto

    LS Inferior ou Igual / Abaixo ou Igual

    PL Mais / Mais

    MI MENOS / Menos

    Conjunto de transporte CS

    CC transportar claro

    GE Maior ou Igual / Maior ou Igual

    GT maior que / mais

    LE Menor ou Igual / Menor ou igual

    LT menor que/menos

    Z é Zero/Zero

    NZ não é zero / não é zero

Esses estados desempenham um papel muito importante no montador ARM.

NOTA: os sinalizadores armazenam apenas condições (Igual, Menor que e assim por diante). Eles não são mais importantes.

Sufixos de condição

Você já viu a instrução B (ramo). A instrução B faz o que é chamado de salto incondicional (como GoTo no Basic ou JMP no assembly INTEL). Mas pode ter um sufixo (um dos listados acima), então verifica se o estado dos sinalizadores corresponde a ele. Caso contrário, a instrução de salto simplesmente não será executada. Então se você quiser verificar se o registro r0 é igual ao registro r4 e depois ir para um rótulo chamado label34, então você precisa escrever o seguinte código:

    CMP r0, r4; Comentários em assembler vêm depois de ponto e vírgula (;)

    Etiqueta BEQ34; B é uma instrução de salto e EQ é um significado de sufixo

    ; "Se Igual"

NOTA: No Goldroad Assembler, os rótulos não precisam ser acompanhados por (, e não deve haver nada na linha além do nome do rótulo.

NOTA II: Não é necessário escrever CMP e BEQ em letras maiúsculas, isso é simplesmente para deixar mais claro para você.

Agora você já sabe fazer uma transição dependendo do estado das flags, mas o que você não sabe é que pode fazer qualquer coisa dependendo do estado das flags, basta adicionar o sufixo desejado em qualquer instrução!

Além disso, você não precisa usar CMP para definir o estado dos sinalizadores. Se você quiser, por exemplo, que a instrução SUB (Subtrair) defina sinalizadores, adicione o sufixo "S" à instrução (significa "Definir sinalizadores"). Isso é útil se você não quiser definir o estado dos sinalizadores com uma instrução CMP extra, então você pode fazer isso e pular se o resultado for zero assim:

    SUBS r0,r1,0x0FF ; Define flags de acordo com o resultado da execução

    ; instruções

    ldrZ r0,=0x0FFFF ; Carregará o registrador r0 com 0x0FFFF somente se o estado

    sinalizadores é igual a Zero.

Análise

Hoje aprendemos (um pouco mais) sobre registradores. Também aprendemos sobre a flexibilidade das instruções ARM, que podem ser executadas (ou não executadas) dependendo do estado dos sinalizadores. Aprendemos muito hoje.

Amanhã utilizaremos o conhecimento de montador ARM adquirido hoje para exibir uma imagem na tela do GBA.

Algo impossível só é assim até que se torne possível / Jean-Luc Picard, Capitão. ,USS Enterprise/. Mike H, trad. Áquila

Os processadores CISC executam operações bastante complexas em uma instrução, incluindo operações aritméticas e lógicas no conteúdo das células de memória. As instruções do processador CISC podem ter comprimentos diferentes.

Em contraste, o RISC possui um sistema de instruções relativamente simples com uma divisão clara por tipo de operação:

  • trabalhar com memória (ler da memória em registradores ou escrever de registradores na memória),
  • processamento de dados em registradores (aritmético, lógico, deslocamento de dados para esquerda/direita ou rotação de bits em um registrador),
  • comandos de transições condicionais ou incondicionais para outros endereços.

Como regra (mas nem sempre, e somente se o código do programa entrar na memória cache do controlador), um comando é executado dentro de um ciclo do processador. O comprimento de uma instrução do processador ARM é fixo - 4 bytes (uma palavra de computador). Na verdade, um processador ARM moderno pode mudar para outros modos de operação, por exemplo, para o modo THUMB, quando o comprimento da instrução chega a 2 bytes. Isso permite tornar o código mais compacto. No entanto, não abordamos este modo neste artigo, uma vez que não é suportado no processador Amber ARM v2a. Pelo mesmo motivo, não consideraremos modos como Jazelle (otimizado para execução de código Java) e não consideraremos comandos NEON - comandos para operações em múltiplos dados. Afinal, estamos estudando o sistema de instrução ARM puro.

Registros do processador ARM.

O processador ARM possui vários conjuntos de registros, dos quais apenas 16 estão atualmente disponíveis para o programador. Existem vários modos de operação do processador, dependendo do modo de operação, o banco de registros apropriado é selecionado. Estes modos de operação:

  • modo de aplicação (USR, modo de usuário),
  • modo supervisor ou modo de sistema operacional (SVC, modo supervisor),
  • modo de processamento de interrupção (IRQ, modo de interrupção) e
  • modo de processamento de “interrupção urgente” (FIRQ, modo de interrupção rápida).

Ou seja, por exemplo, quando ocorre uma interrupção, o próprio processador vai até o endereço do programa manipulador de interrupções e “troca” automaticamente de banco de registradores.

Processadores ARM de versões mais antigas, além dos modos operacionais acima, possuem modos adicionais:

  • Abortar (usado para lidar com exceções de acesso à memória),
  • Indefinido (usado para implementar um coprocessador em software) e
  • modo de tarefa privilegiada do sistema operacional System.

O processador Amber ARM v2a não possui esses três modos adicionais.

Para Amber ARM v2a, o conjunto de registros pode ser representado da seguinte forma:

Os registros r0-r7 são iguais para todos os modos.
Os registros r8-r12 são comuns apenas para os modos USR, SVC, IRQ.
O registrador r13 é um ponteiro de pilha. Ele é seu em todos os modos.
Registrador r14 - o registrador de retorno da subrotina também é diferente em todos os modos.
O registro r15 é um ponteiro para instruções executáveis. É comum para todos os modos.

Percebe-se que o modo FIRQ é o mais isolado, pois possui a maior parte de registros próprios. Isso é feito para que alguma interrupção muito crítica possa ser processada sem salvar registros na pilha, sem perda de tempo.

Atenção especial deve ser dada ao registro r15, também conhecido como pc (Program Counter) - um ponteiro para comandos executáveis. Você pode realizar diversas operações aritméticas e lógicas em seu conteúdo, assim a execução do programa passará para outros endereços. Porém, especificamente para o processador ARM v2a implementado no sistema Amber existem algumas sutilezas na interpretação dos bits deste registro.

O fato é que neste processador, no registro r15 (pc), além do próprio ponteiro para os comandos executáveis, estão contidas as seguintes informações:

Bits 31:28 – flags para o resultado de uma operação aritmética ou lógica
Bits 27 - máscara IRQ de interrupção, as interrupções são desabilitadas quando o bit é definido.
Bits 26 - Máscara de interrupção FIRQ, interrupções rápidas são desabilitadas quando o bit é energizado.
Bits 25:2 – o ponteiro real para instruções do programa ocupa apenas 26 bits.
Bits 1:0 - modo de operação atual do processador.
3 – Supervisor
2 – Interromper
1 – Interrupção Rápida
0 - Usuário

Em processadores ARM mais antigos, todos os flags e bits de serviço estão localizados em registradores separados Registro de status do programa atual(cpsr) e Registro de status de programa salvo (spsr), para acesso ao qual existem comandos especiais separados. Isso é feito para expandir o espaço de endereço disponível para programas.

Uma das dificuldades em dominar o assembler ARM são os nomes alternativos de alguns registradores. Então, como mencionado acima, r15 é o mesmo pc. Há também r13 - este é o mesmo sp (Stack Pointer), r14 é lr (Link Register) - o registro de endereço de retorno do procedimento. Além disso, r12 é o mesmo ip (registro de scratch de chamada intra-procedimento) usado pelos compiladores C de maneira especial para acessar parâmetros na pilha. Essa nomenclatura alternativa às vezes é confusa quando você olha o código do programa de outra pessoa - ambas as designações de registro são encontradas lá.

Recursos de execução de código.

Em muitos tipos de processadores (por exemplo, x86), apenas uma transição para outro endereço de programa pode ser realizada por condição. Este não é o caso do ARM. Cada instrução do processador ARM pode ou não ser executada condicionalmente. Isso permite minimizar o número de transições no programa e, portanto, usar o pipeline do processador com mais eficiência.

Afinal, o que é um pipeline? Uma instrução do processador é agora selecionada do código do programa, a anterior já está sendo decodificada e a anterior já está sendo executada. É o caso do pipeline de 3 estágios do processador Amber A23, que utilizamos em nosso projeto para a placa Mars Rover2Mars Rover2. A modificação do processador Amber A25 possui um pipeline de 5 estágios, é ainda mais eficiente. Mas, há um grande MAS. Os comandos de salto forçam o processador a limpar o pipeline e reabastecê-lo. Assim, um novo comando é selecionado, mas ainda não há nada para decodificar e, além disso, nada para executar imediatamente. A eficiência da execução do código diminui com transições frequentes. Os processadores modernos possuem todos os tipos de mecanismos de previsão de ramificação que de alguma forma otimizam o preenchimento do pipeline, mas nosso processador não possui isso. De qualquer forma, o ARM foi sensato ao possibilitar que cada comando fosse executado condicionalmente.

Em um processador ARM, em qualquer tipo de instrução, os quatro bits da condição de execução da instrução são codificados nos quatro bits mais altos do código da instrução:

Há um total de 4 sinalizadores de condição no processador:
. Negativo - o resultado da operação foi negativo,
. Zero - o resultado é zero,
. Carry - ocorreu um carry ao realizar uma operação com números não assinados,
. oVerflow - ocorreu um overflow ao realizar uma operação com números assinados, o resultado não cabe no registro)

Esses 4 sinalizadores formam muitas combinações de condições possíveis:

Código Sufixo Significado Bandeiras
4"h0 equação Igual Conjunto Z
4"h1 não Não igual Z claro
4"h2 cs/hs Carry set/unsigned superior ou igual Conjunto C
4"h3 cc/lo Leve claro / não assinado inferior C claro
4"h4 mi Menos/negativo N conjunto
4"h5 por favor Mais/positivo ou zero N claro
4"h6 contra Transbordar Conjunto V
4"h7 vc Sem estouro V claro
4"h8 oi Não assinado superior C definido e Z limpo
4"h9 eu Não assinado inferior ou igual C limpo ou Z definido
4"ha ge Assinado maior ou igual N == V
4"hb isso Assinado há menos de N! = V
4"hc GT Assinado maior que Z == 0,N == V
4" HD eu Assinado menor ou igual Z == 1 ou N! = V
4"ele tudo Sempre (incondicional)
4"hf - Condição inválida

Agora, isso leva a outra dificuldade no aprendizado das instruções do processador ARM – os muitos sufixos que podem ser adicionados ao código de instrução. Por exemplo, a adição, desde que o sinalizador Z esteja definido, é o comando addeq como add + suffix eq . Salte para a sub-rotina se o sinalizador N=0 for blpl como bl + sufixo pl .

Bandeiras (Negativo, Zero, Carry, Overflow) o mesmo nem sempre é definido durante operações aritméticas ou lógicas, como acontece, digamos, em um processador x86, mas apenas quando o programador deseja. Para isso, existe outro sufixo nos mnemônicos do comando: “s” (no código do comando é codificado pelo bit 20). Assim, o comando de adição não altera os sinalizadores, mas o comando de adição altera os sinalizadores. Ou também pode haver um comando de adição condicional, mas que altera os sinalizadores. Por exemplo: addgts. É claro que o número de combinações possíveis de nomes de comandos com diferentes sufixos para execução condicional e configuração de flags torna o código assembly de um processador ARM muito peculiar e difícil de ler. Porém, com o tempo você se acostuma e começa a entender esse texto.

Operações aritméticas e lógicas (Processamento de Dados).

O processador ARM pode realizar várias operações aritméticas e lógicas.

O código de operação real de quatro bits (Opcode) está contido nos bits de instrução do processador.

Qualquer operação é executada no conteúdo do registro e no chamado shifter_operand. O resultado da operação é colocado no cadastro. Os Rn e Rd de quatro bits são índices de registradores no banco ativo do processador.

Dependendo do bit I 25, shifter_operand é tratado como uma constante numérica, ou como um índice do segundo registro do operando, e até mesmo uma operação de deslocamento no valor do segundo operando.

Exemplos simples de comandos assembler seriam assim:

adicione r0,r1,r2 @ coloque a soma dos valores dos registros r1 e r2 no registro r0
sub r5,r4,#7 @ coloque a diferença (r4-7) no registro r5

As operações realizadas são codificadas da seguinte forma:

4"h0 e Lógico AND Rd:= Rn AND shifter_operand
4"h1 eor Lógico exclusivo OR Rd:= Rn XOR shifter_operand
4"h2 sub Subtração aritmética Rd:= Rn - shifter_operand
4"h3 rsb Subtração reversa aritmética Rd:= shifter_operand - Rn
4"h4 adicionar adição aritmética Rd:= Rn + shifter_operand
4"h5 adc Adição aritmética mais bandeira de transporte Rd:= Rn + shifter_operand + bandeira de transporte
4"h6 sbc Subtração aritmética com carry Rd:= Rn - shifter_operand - NOT(Carry Flag)
4"h7 rsc Subtração reversa aritmética com carry Rd:= shifter_operand - Rn - NOT(Carry Flag)
4"h8 tst AND lógico, mas sem armazenar o resultado, apenas os flags Rn AND shifter_operand S bit sempre definidos são alterados
4"h9 teq OR lógico exclusivo, mas sem armazenar o resultado, apenas os flags Rn EOR shifter_operand são alterados
S bit sempre definido
4"ha cmp Comparação, ou melhor, subtração aritmética sem armazenar o resultado, apenas os flags Rn mudam - bit shifter_operand S sempre definido
4"hb cmn Comparação de adição inversa, ou melhor, adição aritmética sem armazenar o resultado, apenas os flags Rn + shifter_operand S bit sempre definem mudança
4"hc orr Lógico OU Rd:= Rn OU shifter_operand
4"hd mov Copiar valor Rd:= shifter_operand (sem primeiro operando)
4"he bic Reset bits Rd:= Rn AND NOT(shifter_operand)
4"hf mvn Copia o valor inverso Rd:= NÃO shifter_operand (sem primeiro operando)

Deslocador de barril.

O processador ARM possui um circuito especial “barrel shifter” que permite que um dos operandos seja deslocado ou girado por um número especificado de bits antes de qualquer operação aritmética ou lógica. Este é um recurso bastante interessante do processador, que permite criar código muito eficiente.

Por exemplo:

@multiplicar por 9 é multiplicar um número por 8
@ deslocando 3 bits para a esquerda mais outro número
adicione r0, r1, r1, lsl #3 @ r0= r1+(r1<<3) = r1*9

@ multiplicar por 15 é multiplicar por 16 menos o número
rsb r0, r1, r1, lsl #4 @ r0= (r1<<4)-r1 = r1*15

@ acesso a uma tabela de palavras de 4 bytes, onde
@r1 é o endereço base da tabela
@r2 é o índice do elemento na tabela
ldr r0,

Além do deslocamento lógico para a esquerda lsl, há também um deslocamento lógico para a direita lsr e um deslocamento aritmético para a direita asr (um deslocamento que preserva o sinal, o bit mais significativo é multiplicado à esquerda simultaneamente com o deslocamento).

Há também rotação dos bits ror - os bits se movem para a direita e os que são puxados se movem para a esquerda.
Há uma mudança de um bit por meio do sinalizador C - este é o comando rrx. O valor do registro é deslocado um bit para a direita. À esquerda, o sinalizador C é carregado no bit mais significativo do registro.

O deslocamento pode ser realizado não por um número constante fixo, mas pelo valor do registrador do terceiro operando. Por exemplo:

adicione r0, r1, r1, lsr r3 @ isso é r0 = r1 + (r1>>r3);
adicione r0, r0, r1, lsr r3 @ isso é r0 = r0 + (r1>>r3);

Então shifter_operand é o que descrevemos nos comandos assembler, por exemplo como "r1, lsr r3" ou "r2, lsl #5".

O mais interessante é que utilizar turnos nas operações não custa nada. Essas mudanças (normalmente) não requerem ciclos de clock adicionais, o que é muito bom para o desempenho do sistema.

Usando operandos numéricos.

As operações aritméticas ou lógicas podem usar não apenas o conteúdo de um registro, mas também uma constante numérica como segundo operando.

Infelizmente, há uma limitação importante aqui. Como todos os comandos têm comprimento fixo de 4 bytes (32 bits), não será possível codificar “qualquer” número nele. No código de operação, 4 bits já são ocupados pelo código de condição de execução (Cond), 4 bits pelo próprio código de operação (Opcode), depois 4 bits - o registrador receptor Rd, e outros 4 bits - o registrador do primeiro operando Rn, mais vários sinalizadores I 25 (denota apenas uma constante numérica no código da operação) e S 20 (configuração de sinalizadores após a operação). No total, restam apenas 12 bits para uma constante possível, o chamado shifter_operand - vimos isso acima. Como 12 bits só podem codificar números em uma faixa estreita, os desenvolvedores do processador ARM decidiram codificar a constante da seguinte maneira. Os doze bits do shifter_operand são divididos em duas partes: o indicador de rotação de quatro bits encode_imm e o valor numérico real de oito bits imm_8.

Em um processador ARM, uma constante é definida como um número de oito bits dentro de um número de 32 bits, girado para a direita por um número par de bits. Aquilo é:

imm_32 = imm_8 ROR (encode_imm *2)

Acabou sendo muito complicado. Acontece que nem todo número constante pode ser usado em comandos assembler.

Você pode escrever

adicione r0, r2, #255 @ constante na forma decimal
adicione r0, r3, #0xFF @ constante em hexadecimal

já que 255 está na faixa de 8 bits. Esses comandos serão compilados assim:

0: e28200ff adiciona r0, r2, #255; 0xff
4: e28300ff adiciona r0, r3, #255; 0xff

E você pode até escrever

adicione r0, r4, #512
adicione r0, r5, 0x650000

O código compilado ficará assim:

0: e2840c02 adicione r0, r4, #512; 0x200
4: e2850865 adicione r0, r5, #6619136 ; 0x650000

Nesse caso, o próprio número 512, é claro, não cabe no byte. Mas então imaginamos na forma hexadecimal 32’h00000200 e vemos que isso é 2 expandido para a direita em 24 bits (1 ou 24). O coeficiente de rotação é duas vezes menor que 24, ou seja, 12. Acontece que shifter_operand = ( 4’hc , 8’h02 ) - esses são os doze bits menos significativos do comando. O mesmo vale para o número 0x650000. Para ele, shifter_operand = ( 4’h8, 8’h65 ).

É claro que você não pode escrever

adicione r0, r1,#1234567

ou você não pode escrever

movimento r0, #511

já que aqui o número não pode ser representado na forma de imm_8 e encode_imm - o fator de rotação. O compilador assembler gerará um erro.

O que fazer quando uma constante não pode ser codificada diretamente em shifter_operand ? Teremos que fazer todos os tipos de truques.
Por exemplo, você pode primeiro carregar o número 512 em um registro gratuito e depois subtrair um:

movimento r0, #511
subr0,r0,#1

A segunda maneira de carregar um número específico em um registrador é lê-lo a partir de uma variável especialmente reservada localizada na memória:

ldr r7, minha_var
.....
minha_var: .word 0x123456

A maneira mais fácil de escrever é assim:

ldr r2,=511

Neste caso (observe o sinal "="), se a constante puder ser representada como imm_8 e encode_imm , se puder caber no bit 12 de shifter_operand , então o compilador assembly compilará automaticamente o ldr em uma instrução mov. Mas se o número não puder ser representado dessa maneira, o próprio compilador reservará uma célula de memória no programa para essa constante, dará um nome a essa célula de memória e compilará o comando em ldr .

Isto é o que eu escrevi:

ldr r7, minha_var
ldr r8,=511
ldr r8,=1024
ldr r9,=0x3456
........
Minha_var: .word 0x123456

Após a compilação, obtive isto:

18: e59f7030 ldr r7, ; 50
1c: e59f8030 ldr r8,; 54
20: e3a08b01 movimento r8, #1024; 0x400
24: e59f902c ldr r9, ; 58
.............
00000050 :
50: 00123456.palavra 0x00123456
54: 000001ff.palavra 0x000001ff
58: 00003456.palavra 0x00003456

Observe que o compilador usa endereçamento de memória relativo ao registrador pc (também conhecido como r15).

Lendo uma célula de memória e escrevendo um registrador na memória.

Como escrevi acima, o processador ARM só pode realizar operações aritméticas ou lógicas no conteúdo dos registradores. Os dados das operações devem ser lidos da memória e o resultado das operações deve ser gravado de volta na memória. Existem comandos especiais para isso: ldr (provavelmente da combinação “LoaD Register”) para leitura e str (provavelmente “STore Register”) para escrita.

Parece que existem apenas duas equipes, mas na verdade elas têm muitas variações. Basta observar como os comandos ldr /str são codificados no processador Amber ARM para ver quantos bits de sinalização auxiliares são L 20, W 21, B 22, U 23, P 24, I 25 - e eles determinam o comportamento específico de o comando:

  • Bit L 20 determina escrita ou leitura. 1 - ldr, leia, 0 - str, escreva.
  • O bit B 22 determina a leitura/gravação de uma palavra de 32 bits ou de um byte de 8 bits. 1 significa operação de byte. Quando um byte é lido em um registrador, os bits mais significativos do registrador são zerados.
  • O bit I 25 determina a utilização do campo Offset. Se I 25 ==0, então Offset é interpretado como um deslocamento numérico que deve ser adicionado ao endereço base do registrador ou subtraído. Mas adicionar ou subtrair depende do bit U 23.

(Cond) - condição para realização da operação. Interpretado da mesma forma que para comandos lógicos/aritméticos - a leitura ou a escrita podem ser condicionais.

Assim, no texto assembly você pode escrever algo assim:

ldr r1, @ no registro r1 lê a palavra no endereço do registro r0
ldrb r1, @ no registrador r1 lê o byte no endereço do registrador r0
ldreq r2, @ leitura condicional de palavras
ldrgtb r2, @ byte condicional lido
ldr r3, @ lê a palavra no endereço 8 relativa ao endereço do registrador r4
ldr r4, @ lê a palavra no endereço -16 relativo ao endereço do registrador r5

Depois de compilar este texto, você pode ver os códigos reais destes comandos:

0: e5901000 ldr r1,
4: e5d01000 ldrb r1,
8: 05912000 ldreq r2,
c: c5d12000 ldrbgt r2,
10: e5943008 ldr r3,
14: e5154010 ldr r4,

No exemplo acima estou usando apenas ldr , mas str é usado da mesma maneira.

Existem modos de acesso à memória de write-back pré-índice e pós-índice. Nestes modos, o ponteiro de acesso à memória é atualizado antes ou depois da execução da instrução. Se você está familiarizado com a linguagem de programação C, então está familiarizado com construções de acesso de ponteiro como ( *pfonte++;) ou ( a=*++pfonte;). O processador ARM implementa este modo de acesso à memória. Quando um comando de leitura é executado, dois registros são atualizados ao mesmo tempo - o registro receptor recebe o valor lido da memória e o valor no registro ponteiro para a célula de memória é movido para frente ou para trás.

Escrever esses comandos é, na minha opinião, um tanto ilógico. Demora muito para se acostumar.

ldr r3, ! @psrc++; r3 = *psrc;
ldr r3, , #4 @ r3 = *psrc; psrc++;

O primeiro comando ldr primeiro incrementa o ponteiro e depois lê. O segundo comando primeiro lê e depois incrementa o ponteiro. O valor do ponteiro psrc está no registro r0.

Todos os exemplos discutidos acima foram para o caso em que o bit I 25 no código de comando foi redefinido. Mas ainda pode ser instalado! Então o valor do campo Offset não conterá uma constante numérica, mas sim o terceiro registro participante da operação. Além disso, o valor do terceiro registo ainda pode ser pré-alterado!

Aqui estão alguns exemplos de possíveis variações de código:

0: e7921003 ldr r1, @ read address - soma dos valores dos registradores r2 e r3
4: e7b21003 ldr r1, ! @ o mesmo, mas após a leitura r2 será aumentado pelo valor de r3
8: e6932004 ldr r2, , r4 @ primeiro haverá uma leitura no endereço r3, e então r3 aumentará em r4
c: e7943185 ldr r3, @ endereço de leitura r4+r5*8
10: e7b43285 ldr r3, ! @ leia o endereço r4+r5*32, após a leitura r4 será definido com o valor deste endereço
14: e69431a5 ldr r3, , r5, lsr #3 @ endereço para leitura de r4, após executar o comando r4 será definido como r4+r5/8

Estas são as variações dos comandos de leitura/gravação no processador ARM v2a.

Nos modelos mais antigos de processadores ARM, essa variedade de comandos é ainda maior.
Isso se deve ao fato de o processador permitir, por exemplo, ler não apenas palavras (números de 32 bits) e bytes, mas também meias palavras (16 bits, 2 bytes). Em seguida, o sufixo “h”, da palavra meia palavra, é adicionado aos comandos ldr/str. Os comandos serão parecidos com ldrh ou strh . Existem também comandos para carregar meias palavras ldrsh ou bytes ldrsb interpretados como números assinados. Nestes casos, o bit mais significativo da palavra ou byte carregado é multiplicado pelos bits mais significativos de toda a palavra no registrador receptor. Por exemplo, carregar a meia palavra 0xff25 com o comando ldrsh no registro de destino resulta em 0xffffff25 .

Várias leituras e gravações.

Os comandos ldr /str não são os únicos para acessar a memória. O processador ARM também possui comandos que permitem realizar transferências de blocos - você pode carregar o conteúdo de várias palavras consecutivas da memória e de vários registros de uma só vez. Você também pode gravar os valores de vários registros sequencialmente na memória.

Os mnemônicos do comando de transferência de bloco começam na raiz ldm (LoaD Multiple) ou stm (Store Multiple). Mas então, como sempre no ARM, começa a história dos sufixos.

Em geral, o comando fica assim:

op(cond)(modo) Rd(, {Register list} !}

O sufixo (Cond) é compreensível, é uma condição para a execução do comando. O sufixo (modo) é o modo de transmissão, falaremos mais sobre isso mais tarde. Rd é um registrador que determina o endereço base na memória para leitura ou escrita. Um ponto de exclamação após o registrador Rd indica que ele será modificado após uma operação de leitura/gravação. A lista de registros que são carregados da memória ou paginados na memória é (Lista de registros).

A lista de registros é especificada entre chaves separadas por vírgulas ou como um intervalo. Por exemplo:

stm r0,(r3,r1, r5-r8)

A memória será escrita fora de ordem. A lista simplesmente indica quais registros serão gravados na memória e pronto. O código de comando contém 16 bits reservados para Register List, exatamente o número de registradores do banco do processador. Cada bit deste campo indica qual registrador participará da operação.

Agora sobre o modo de leitura/gravação. Há espaço para confusão aqui. O fato é que diferentes nomes de modos podem ser usados ​​para a mesma ação.

Se fizermos uma pequena digressão lírica, então precisamos falar sobre... a pilha. Uma pilha é uma forma de acessar dados do tipo LIFO - Último a entrar, primeiro a sair (wiki) - último a entrar, primeiro a sair. A pilha é amplamente utilizada na programação ao chamar procedimentos e salvar o estado dos registradores na entrada das funções e restaurá-los na saída, bem como ao passar parâmetros para procedimentos chamados.

Existem, quem diria, quatro tipos de pilha de memória.

O primeiro tipo é Descendente Completo. É quando o ponteiro da pilha aponta para um elemento da pilha ocupado e a pilha cresce em direção a endereços decrescentes. Quando você precisa colocar uma palavra na pilha, primeiro o ponteiro da pilha é diminuído (Decrement Before), depois a palavra é escrita no endereço do ponteiro da pilha. Quando você precisa remover uma palavra de computador da pilha, a palavra é lida usando o valor atual do ponteiro da pilha e, em seguida, o ponteiro se move para cima (Increment After).

O segundo tipo é Totalmente Ascendente. A pilha não cresce para baixo, mas para cima, em direção a endereços maiores. O ponteiro também aponta para o elemento ocupado. Quando você precisa colocar uma palavra na pilha, primeiro o ponteiro da pilha é incrementado e depois a palavra é escrita no ponteiro (Increment Before). Quando você precisa remover da pilha, primeiro você lê o ponteiro da pilha, pois ele aponta para um elemento ocupado, depois o ponteiro da pilha é diminuído (Decrement After).

O terceiro tipo é Vazio Descendente. A pilha cresce para baixo, como no caso do Descendente Completo, mas a diferença é que o ponteiro da pilha aponta para uma célula desocupada. Assim, quando você precisa colocar uma palavra na pilha, uma entrada é feita imediatamente, então o ponteiro da pilha é diminuído (Decrement After). Ao removê-lo da pilha, o ponteiro é primeiro incrementado e depois lido (incrementar antes).

O quarto tipo é Vazio Ascendente. Espero que tudo esteja claro - a pilha cresce. O ponteiro da pilha aponta para um elemento vazio. Colocar na pilha significa escrever uma palavra no endereço do ponteiro da pilha e incrementar o ponteiro da pilha (Increment After). Sair da pilha - diminua o ponteiro da pilha e leia a palavra (Decrement Before).

Assim, ao realizar operações na pilha, é necessário aumentar ou diminuir o ponteiro - (Incremento/Decremento) antes ou depois (Antes/Depois) de leitura/gravação na memória, dependendo do tipo de pilha. Os processadores Intel, por exemplo, possuem comandos especiais para trabalhar com a pilha, como PUSH (colocar uma palavra na pilha) ou POP (retirar uma palavra da pilha). Não há instruções especiais no processador ARM, mas são usadas as instruções ldm e stm.

Se você implementar a pilha usando instruções do processador ARM, obterá a seguinte imagem:

Por que a mesma equipe precisou receber nomes diferentes? Não entendo nada... Aqui, é claro, deve-se notar que o padrão de pilha para ARM ainda é Full Decrescente.

O ponteiro de pilha em um processador ARM é o registrador sp ou r13. Geralmente é este o acordo. É claro que escrever stm ou ler ldm também pode ser feito com outros registradores base. Porém, é preciso lembrar como o registrador sp difere de outros registradores - ele pode ser diferente em diferentes modos de operação do processador (USR, SVC, IRQ, FIRQ), pois eles possuem seus próprios bancos de registradores.

E mais uma nota. Escreva uma linha como esta no código assembly ARM empurrar (r0-r3), Claro que você pode. Só que na realidade será o mesmo time stmfd sp!,(r0-r3).

Por fim, darei um exemplo de código assembly e seu texto desmontado compilado. Nós temos:


stmfd sp!,(r0-r3)
stmdb sp!,(r0-r3)
empurrar (r0-r3)

@estas três instruções são iguais e fazem a mesma coisa
pop(r0-r3)
ldmia sp!,(r0-r3)
ldmfdr13!,(r0-r3)

Padrão r4,(r0-r3,r5,r8)
stmea r4!,(r0-r3,r7,r9,lr,pc)
ldm r5,(r0,pc)

Após a compilação obtemos:

0: e92d000f empurrar (r0, r1, r2, r3)
4: empurrar e92d000f (r0, r1, r2, r3)
8: empurrar e92d000f (r0, r1, r2, r3)
c: e8bd000f pop (r0, r1, r2, r3)
10: e8bd000f pop (r0, r1, r2, r3)
14: e8bd000f pop (r0, r1, r2, r3)
18: e904012f stmdb r4, (r0, r1, r2, r3, r5, r8)
1c: e8a4c28f stmia r4!, (r0, r1, r2, r3, r7, r9, lr, pc)
20: e8958001 ldm r5, (r0, computador)

Transições em programas.

A programação não é possível sem transições. Em qualquer programa há execução cíclica de código e chamadas de procedimentos e funções, e também há execução condicional de seções de código.

O processador Amber ARM v2a possui apenas dois comandos: b (da palavra Branch - branch, transaction) e bl (Branch with Link - transição mantendo o endereço de retorno).

A sintaxe do comando é muito simples:

b(cond)rótulo
rótulo bl(cond)

É claro que quaisquer transições podem ser condicionais, ou seja, o programa pode conter palavras estranhas como estas, formadas a partir das raízes “b” e “bl” e dos sufixos de condição (Cond):

beq, bne, bcs, bhs, bcc, blo, bmi, bpl, bvs, bvc, bhi, bls, bge, bgt, ble, bal, b

bleq, blne, blcs, blhs, blcc, bllo, blmi, blpl, blvs, blvc, blhi, blls, blge, blgt, blle, blal, bl

A variedade é incrível, não é?

O comando de salto contém um deslocamento de deslocamento de 24 bits. O endereço de salto é calculado como a soma do valor atual do ponteiro pc e o número de deslocamento deslocado 2 bits para a esquerda, interpretado como um número assinado:

Novo pc = pc + deslocamento *4

Assim, o intervalo de transições é de 32 MB para frente ou para trás.

Vejamos o que é uma transição preservando o endereço de retorno bl. Este comando é usado para chamar sub-rotinas. Uma característica interessante deste comando é que o endereço de retorno do procedimento ao chamar o procedimento não é armazenado na pilha, como nos processadores Intel, mas no registro r14 normal. Então, para retornar do procedimento, você não precisa de um comando ret especial, como acontece com os mesmos processadores Intel, mas pode simplesmente copiar o valor de r14 de volta para o pc. Agora está claro porque o registro r14 tem um nome alternativo lr (Link Register).

Vejamos o procedimento outbyte do projeto hello-world para o Amber SoC.

000004a0<_outbyte>:
4a0: e59f1454 ldr r1, ; 8fc< адрес регистра данных UART >
4a4: e59f3454 ldr r3, ; 900< адрес регистра статуса UART >
4a8: e5932000 ldr r2, ; leia o status atual
4ac: e2022020 e r2, r2, #32
4b0: e3520000 cmp r2, #0; verifique se o UART não está ocupado
4b4: 05c10000 strbeq r0, ; escreva um caractere no UART somente se ele não estiver ocupado
4b8: 01b0f00e movseq pc, lr ; retorno condicional do procedimento se o UART não estiver ocupado
4bc: 1afffff9 bne 4a8<_outbyte+0x8>; loop para verificar o status do UART

Acho que pelos comentários deste fragmento fica claro como funciona esse procedimento.

Outra observação importante sobre transições. O registro r15 (pc) pode ser usado em operações aritméticas ou lógicas comuns como um registro receptor. Portanto, um comando como add pc,pc,#8 é uma instrução e tanto para mudar para outro endereço.

Mais uma observação precisa ser feita em relação às transições. Processadores ARM mais antigos também possuem instruções de ramificação adicionais bx, blx e blj. Estes são comandos para saltar para fragmentos de código com um sistema de comando diferente. Bx /blx permite alternar para o código THUMB de 16 bits dos processadores ARM. Blj é uma chamada aos procedimentos do sistema de instrução Jazelle (suporte à linguagem Java em processadores ARM). Nosso Amber ARM v2a não possui esses comandos.

Olá a todos!
Por profissão sou programador Java. Os últimos meses de trabalho me obrigaram a me familiarizar com o desenvolvimento para Android NDK e, consequentemente, a escrever aplicações nativas em C. Aqui me deparei com o problema de otimização de bibliotecas Linux. Muitos revelaram-se completamente não otimizados para ARM e carregaram pesadamente o processador. Antes eu praticamente nunca tinha programado em linguagem assembly, então no começo foi difícil começar a aprender essa linguagem, mas mesmo assim resolvi tentar. Este artigo foi escrito, por assim dizer, de iniciante para iniciante. Tentarei descrever o básico que já aprendi, espero que interesse a alguém. Além disso, terei prazer em receber críticas construtivas de profissionais.

Introdução
Então, primeiro, vamos descobrir o que é ARM. A Wikipedia dá esta definição:

A arquitetura ARM (Advanced RISC Machine, Acorn RISC Machine, advanced RISC machine) é uma família de núcleos de microprocessadores licenciados de 32 e 64 bits desenvolvidos pela ARM Limited. A empresa desenvolve exclusivamente kernels e ferramentas para eles (compiladores, ferramentas de depuração, etc.), ganhando dinheiro licenciando a arquitetura para fabricantes terceirizados.

Se alguém não sabe, agora a maioria dos dispositivos móveis e tablets são desenvolvidos nesta arquitetura de processador. A principal vantagem desta família é o baixo consumo de energia, por isso é frequentemente utilizada em diversos sistemas embarcados. A arquitetura evoluiu ao longo do tempo e, a partir do ARMv7, foram definidos 3 perfis: 'A' (aplicação) - aplicações, 'R' (tempo real) - tempo real, 'M' (microcontrolador) - microcontrolador. Você pode ler a história do desenvolvimento desta tecnologia e outros dados interessantes na Wikipedia ou pesquisando no Google na Internet. ARM suporta diferentes modos de operação (Thumb e ARM, além disso, apareceu recentemente Thumb-2, que é uma mistura de ARM e Thumb). Neste artigo, veremos o próprio modo ARM, no qual um conjunto de instruções de 32 bits é executado.

Cada processador ARM é criado a partir dos seguintes blocos:

  • 37 registros (dos quais apenas 17 são visíveis durante o desenvolvimento)
  • Unidade Lógica Aritmética (ALU) - executa tarefas aritméticas e lógicas
  • Barrel shifter - um dispositivo projetado para mover blocos de dados por um certo número de bits
  • O CP15 é um sistema especial que controla coprocessadores ARM
  • Decodificador de instruções - trata da conversão de instruções em uma sequência de microoperações
Nem todos esses são componentes do ARM, mas mergulhar na selva da construção do processador está além do escopo deste artigo.
Execução de pipeline
Os processadores ARM usam um pipeline de 3 estágios (a partir do ARM8, um pipeline de 5 estágios foi implementado). Vejamos um pipeline simples usando o processador ARM7TDMI como exemplo. A execução de cada instrução consiste em três etapas:

1. Etapa de amostragem (F)
Neste estágio, as instruções fluem da RAM para o pipeline do processador.
2. Estágio de decodificação (D)
As instruções são decodificadas e seu tipo é reconhecido.
3. Fase de execução (E)
Os dados entram na ALU e são executados e o valor resultante é gravado no registrador especificado.

Mas no desenvolvimento deve-se levar em consideração que existem instruções que utilizam diversos ciclos de execução, por exemplo, load(LDR) ou store. Neste caso, a etapa de execução (E) é dividida em etapas (E1, E2, E3...).

Execução condicional
Uma das funções mais importantes do montador ARM é a execução condicional. Cada instrução pode ser executada condicionalmente e sufixos são usados ​​para isso. Se um sufixo for adicionado ao nome de uma instrução, os parâmetros serão verificados antes de executá-la. Se os parâmetros não atenderem à condição, a instrução não será executada. Sufixos:
MI - número negativo
PL - positivo ou zero
AL - sempre execute instruções
Existem muitos outros sufixos de execução condicional. Leia o restante dos sufixos e exemplos na documentação oficial: Documentação ARM
Agora é hora de considerar...
Sintaxe básica do montador ARM
Para aqueles que já trabalharam com assembler antes, você pode pular este ponto. Para todos os outros, descreverei os fundamentos do trabalho com esta linguagem. Portanto, todo programa em linguagem assembly consiste em instruções. A instrução é criada desta forma:
(rótulo) (instrução | operandos) (@ comentário)
O rótulo é um parâmetro opcional. A instrução é um mnemônico direto de instruções para o processador. As instruções básicas e seu uso serão discutidas abaixo. Operandos - constantes, endereços de registradores, endereços em RAM. Um comentário é um parâmetro opcional que não afeta a execução do programa.
Registrar nomes
Os seguintes nomes de registro são permitidos:
1.r0-r15

3.v1-v8 (registros variáveis, r4 a r11)

4.sb e SB (registro estático, r9)

5.sl e SL (r10)

6.fp e FP (r11)

7.ip e IP (r12)

8.sp e SP (r13)

9.lr e LR (r14)

10.pc e PC (contador de programa, r15).

Variáveis ​​e constantes
No ARM assembler, como qualquer (praticamente) outra linguagem de programação, variáveis ​​e constantes podem ser usadas. Eles são divididos nos seguintes tipos:
  • Numérico
  • quebra-cabeças
  • Corda
Variáveis ​​numéricas são inicializadas assim:
um SETA 100; uma variável numérica "a" é criada com o valor 100.
Variáveis ​​de sequência:
improvisar SETS "literal"; uma variável improb é criada com o valor “literal”. ATENÇÃO! O valor da variável não pode exceder 5.120 caracteres.
Variáveis ​​booleanas usam os valores TRUE e FALSE respectivamente.
Exemplos de instruções assembler ARM
Nesta tabela reuni as instruções básicas que serão necessárias para um desenvolvimento posterior (no estágio mais básico:):

Para reforçar o uso das instruções básicas, vamos escrever alguns exemplos simples, mas primeiro precisaremos de um conjunto de ferramentas para braços. Eu trabalho no Linux, então escolhi: frank.harvard.edu/~coldwell/toolchain (arm-unknown-linux-gnu toolchain). Ele pode ser instalado tão facilmente quanto qualquer outro programa no Linux. No meu caso (Fedora russo) só precisei instalar pacotes rpm do site.
Agora é hora de escrever um exemplo simples. O programa será absolutamente inútil, mas o principal é que funcionará :) Aqui está o código que ofereço:
start: @ Linha opcional indicando o início do programa mov r0, #3 @ Carrega o registro r0 com o valor 3 mov r1, #2 @ Faça o mesmo com o registro r1, só que agora com o valor 2 adicione r2, r1, r0 @ Some os valores de r0 e r1, a resposta é escrita em r2 mul r3, r1, r0 @ Multiplique o valor do registro r1 pelo valor do registro r0, a resposta é escrita em r3 stop: b stop @ Linha de terminação do programa
Compilamos o programa para obter o arquivo .bin:
/usr/arm/bin/arm-unknown-linux-gnu-as -o arm.o arm.s /usr/arm/bin/arm-unknown-linux-gnu-ld -Ttext=0x0 -o ​​​​arm. elf arm .o /usr/arm/bin/arm-unknown-linux-gnu-objcopy -O binário arm.elf arm.bin
(o código está no arquivo arm.s e o conjunto de ferramentas no meu caso está no diretório /usr/arm/bin/)
Se tudo correr bem, você terá 3 arquivos: arm.s (o código real), arm.o, arm.elf, arm.bin (o programa executável real). Para verificar o funcionamento do programa, não é necessário possuir dispositivo de braço próprio. Basta instalar o QEMU. Para referência:

QEMU é um programa gratuito e de código aberto para emular hardware de diversas plataformas.

Inclui emulação de processadores Intel x86 e dispositivos de E/S. Pode emular 80386, 80486, Pentium, Pentium Pro, AMD64 e outros processadores compatíveis com x86; PowerPC, ARM, MIPS, SPARC, SPARC64, m68k - apenas parcialmente.

Funciona em Syllable, FreeBSD, FreeDOS, Linux, Windows 9x, Windows 2000, Mac OS X, QNX, Android, etc.

Então, para emular o arm você precisará do qemu-system-arm. Este pacote está no yum, então para quem tem Fedora não precisa se preocupar e apenas executar o comando:
yum instalar qemu-system-arm

Em seguida, precisamos iniciar o emulador ARM para que ele execute nosso programa arm.bin. Para fazer isso, criaremos um arquivo flash.bin, que será a memória flash do QEMU. É muito fácil fazer isso:
dd if=/dev/zero of=flash.bin bs=4096 count=4096 dd if=arm.bin of=flash.bin bs=4096 conv=notrunc
Agora carregamos o QEMU com a memória flash resultante:
qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
A saída será algo assim:

$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
Monitor QEMU 0.15.1 – digite “help” para obter mais informações
(qemu)

Nosso programa arm.bin teve que alterar os valores de quatro registros, portanto, para verificar o correto funcionamento, vejamos esses mesmos registros. Isso é feito com um comando muito simples: info registradores
Na saída você verá todos os 15 registros ARM, e quatro deles terão valores alterados. Verifique :) Os valores do registro correspondem aos que podem ser esperados após a execução do programa:
(qemu) registros de informações R00 = 00000003 R01 = 00000002 R02 = 00000005 R03 = 00000006 R04 = 00000000 R05 = 00000000 R06 = 00000000 R07 = 00000000 R08 = 00000000 R09 = 00 000000 R10=00000000 R11=00000000 R12=00000000 R13=00000000 R14= 00000000 R15=00000010 PSR=400001d3 -Z-- A svc32

P.S. Neste artigo tentei descrever os fundamentos da programação em ARM assembler. Espero que tenha gostado! Isso será suficiente para mergulhar ainda mais na selva dessa linguagem e escrever programas nela. Se tudo der certo, escreverei mais sobre o que descobri. Se houver erros, por favor não me chute, pois sou novo em assembler.

Olá a todos!
Por profissão sou programador Java. Os últimos meses de trabalho me obrigaram a me familiarizar com o desenvolvimento para Android NDK e, consequentemente, a escrever aplicações nativas em C. Aqui me deparei com o problema de otimização de bibliotecas Linux. Muitos revelaram-se completamente não otimizados para ARM e carregaram pesadamente o processador. Antes eu praticamente nunca tinha programado em linguagem assembly, então no começo foi difícil começar a aprender essa linguagem, mas mesmo assim resolvi tentar. Este artigo foi escrito, por assim dizer, de iniciante para iniciante. Tentarei descrever o básico que já aprendi, espero que interesse a alguém. Além disso, terei prazer em receber críticas construtivas de profissionais.

Introdução
Então, primeiro, vamos descobrir o que é ARM. A Wikipedia dá esta definição:

A arquitetura ARM (Advanced RISC Machine, Acorn RISC Machine, advanced RISC machine) é uma família de núcleos de microprocessadores licenciados de 32 e 64 bits desenvolvidos pela ARM Limited. A empresa desenvolve exclusivamente kernels e ferramentas para eles (compiladores, ferramentas de depuração, etc.), ganhando dinheiro licenciando a arquitetura para fabricantes terceirizados.

Se alguém não sabe, agora a maioria dos dispositivos móveis e tablets são desenvolvidos nesta arquitetura de processador. A principal vantagem desta família é o baixo consumo de energia, por isso é frequentemente utilizada em diversos sistemas embarcados. A arquitetura evoluiu ao longo do tempo e, a partir do ARMv7, foram definidos 3 perfis: 'A' (aplicação) - aplicações, 'R' (tempo real) - tempo real, 'M' (microcontrolador) - microcontrolador. Você pode ler a história do desenvolvimento desta tecnologia e outros dados interessantes na Wikipedia ou pesquisando no Google na Internet. ARM suporta diferentes modos de operação (Thumb e ARM, além disso, apareceu recentemente Thumb-2, que é uma mistura de ARM e Thumb). Neste artigo, veremos o próprio modo ARM, no qual um conjunto de instruções de 32 bits é executado.

Cada processador ARM é criado a partir dos seguintes blocos:

  • 37 registros (dos quais apenas 17 são visíveis durante o desenvolvimento)
  • Unidade Lógica Aritmética (ALU) - executa tarefas aritméticas e lógicas
  • Barrel shifter - um dispositivo projetado para mover blocos de dados por um certo número de bits
  • O CP15 é um sistema especial que controla coprocessadores ARM
  • Decodificador de instruções - trata da conversão de instruções em uma sequência de microoperações
Nem todos esses são componentes do ARM, mas mergulhar na selva da construção do processador está além do escopo deste artigo.
Execução de pipeline
Os processadores ARM usam um pipeline de 3 estágios (a partir do ARM8, um pipeline de 5 estágios foi implementado). Vejamos um pipeline simples usando o processador ARM7TDMI como exemplo. A execução de cada instrução consiste em três etapas:

1. Etapa de amostragem (F)
Neste estágio, as instruções fluem da RAM para o pipeline do processador.
2. Estágio de decodificação (D)
As instruções são decodificadas e seu tipo é reconhecido.
3. Fase de execução (E)
Os dados entram na ALU e são executados e o valor resultante é gravado no registrador especificado.

Mas no desenvolvimento deve-se levar em consideração que existem instruções que utilizam diversos ciclos de execução, por exemplo, load(LDR) ou store. Neste caso, a etapa de execução (E) é dividida em etapas (E1, E2, E3...).

Execução condicional
Uma das funções mais importantes do montador ARM é a execução condicional. Cada instrução pode ser executada condicionalmente e sufixos são usados ​​para isso. Se um sufixo for adicionado ao nome de uma instrução, os parâmetros serão verificados antes de executá-la. Se os parâmetros não atenderem à condição, a instrução não será executada. Sufixos:
MI - número negativo
PL - positivo ou zero
AL - sempre execute instruções
Existem muitos outros sufixos de execução condicional. Leia o restante dos sufixos e exemplos na documentação oficial: Documentação ARM
Agora é hora de considerar...
Sintaxe básica do montador ARM
Para aqueles que já trabalharam com assembler antes, você pode pular este ponto. Para todos os outros, descreverei os fundamentos do trabalho com esta linguagem. Portanto, todo programa em linguagem assembly consiste em instruções. A instrução é criada desta forma:
(rótulo) (instrução | operandos) (@ comentário)
O rótulo é um parâmetro opcional. A instrução é um mnemônico direto de instruções para o processador. As instruções básicas e seu uso serão discutidas abaixo. Operandos - constantes, endereços de registradores, endereços em RAM. Um comentário é um parâmetro opcional que não afeta a execução do programa.
Registrar nomes
Os seguintes nomes de registro são permitidos:
1.r0-r15

3.v1-v8 (registros variáveis, r4 a r11)

4.sb e SB (registro estático, r9)

5.sl e SL (r10)

6.fp e FP (r11)

7.ip e IP (r12)

8.sp e SP (r13)

9.lr e LR (r14)

10.pc e PC (contador de programa, r15).

Variáveis ​​e constantes
No ARM assembler, como qualquer (praticamente) outra linguagem de programação, variáveis ​​e constantes podem ser usadas. Eles são divididos nos seguintes tipos:
  • Numérico
  • quebra-cabeças
  • Corda
Variáveis ​​numéricas são inicializadas assim:
um SETA 100; uma variável numérica "a" é criada com o valor 100.
Variáveis ​​de sequência:
improvisar SETS "literal"; uma variável improb é criada com o valor “literal”. ATENÇÃO! O valor da variável não pode exceder 5.120 caracteres.
Variáveis ​​booleanas usam os valores TRUE e FALSE respectivamente.
Exemplos de instruções assembler ARM
Nesta tabela reuni as instruções básicas que serão necessárias para um desenvolvimento posterior (no estágio mais básico:):

Para reforçar o uso das instruções básicas, vamos escrever alguns exemplos simples, mas primeiro precisaremos de um conjunto de ferramentas para braços. Eu trabalho no Linux, então escolhi: frank.harvard.edu/~coldwell/toolchain (arm-unknown-linux-gnu toolchain). Ele pode ser instalado tão facilmente quanto qualquer outro programa no Linux. No meu caso (Fedora russo) só precisei instalar pacotes rpm do site.
Agora é hora de escrever um exemplo simples. O programa será absolutamente inútil, mas o principal é que funcionará :) Aqui está o código que ofereço:
start: @ Linha opcional indicando o início do programa mov r0, #3 @ Carrega o registro r0 com o valor 3 mov r1, #2 @ Faça o mesmo com o registro r1, só que agora com o valor 2 adicione r2, r1, r0 @ Some os valores de r0 e r1, a resposta é escrita em r2 mul r3, r1, r0 @ Multiplique o valor do registro r1 pelo valor do registro r0, a resposta é escrita em r3 stop: b stop @ Linha de terminação do programa
Compilamos o programa para obter o arquivo .bin:
/usr/arm/bin/arm-unknown-linux-gnu-as -o arm.o arm.s /usr/arm/bin/arm-unknown-linux-gnu-ld -Ttext=0x0 -o ​​​​arm. elf arm .o /usr/arm/bin/arm-unknown-linux-gnu-objcopy -O binário arm.elf arm.bin
(o código está no arquivo arm.s e o conjunto de ferramentas no meu caso está no diretório /usr/arm/bin/)
Se tudo correr bem, você terá 3 arquivos: arm.s (o código real), arm.o, arm.elf, arm.bin (o programa executável real). Para verificar o funcionamento do programa, não é necessário possuir dispositivo de braço próprio. Basta instalar o QEMU. Para referência:

QEMU é um programa gratuito e de código aberto para emular hardware de diversas plataformas.

Inclui emulação de processadores Intel x86 e dispositivos de E/S. Pode emular 80386, 80486, Pentium, Pentium Pro, AMD64 e outros processadores compatíveis com x86; PowerPC, ARM, MIPS, SPARC, SPARC64, m68k - apenas parcialmente.

Funciona em Syllable, FreeBSD, FreeDOS, Linux, Windows 9x, Windows 2000, Mac OS X, QNX, Android, etc.

Então, para emular o arm você precisará do qemu-system-arm. Este pacote está no yum, então para quem tem Fedora não precisa se preocupar e apenas executar o comando:
yum instalar qemu-system-arm

Em seguida, precisamos iniciar o emulador ARM para que ele execute nosso programa arm.bin. Para fazer isso, criaremos um arquivo flash.bin, que será a memória flash do QEMU. É muito fácil fazer isso:
dd if=/dev/zero of=flash.bin bs=4096 count=4096 dd if=arm.bin of=flash.bin bs=4096 conv=notrunc
Agora carregamos o QEMU com a memória flash resultante:
qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
A saída será algo assim:

$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
Monitor QEMU 0.15.1 – digite “help” para obter mais informações
(qemu)

Nosso programa arm.bin teve que alterar os valores de quatro registros, portanto, para verificar o correto funcionamento, vejamos esses mesmos registros. Isso é feito com um comando muito simples: info registradores
Na saída você verá todos os 15 registros ARM, e quatro deles terão valores alterados. Verifique :) Os valores do registro correspondem aos que podem ser esperados após a execução do programa:
(qemu) registros de informações R00 = 00000003 R01 = 00000002 R02 = 00000005 R03 = 00000006 R04 = 00000000 R05 = 00000000 R06 = 00000000 R07 = 00000000 R08 = 00000000 R09 = 00 000000 R10=00000000 R11=00000000 R12=00000000 R13=00000000 R14= 00000000 R15=00000010 PSR=400001d3 -Z-- A svc32

P.S. Neste artigo tentei descrever os fundamentos da programação em ARM assembler. Espero que tenha gostado! Isso será suficiente para mergulhar ainda mais na selva dessa linguagem e escrever programas nela. Se tudo der certo, escreverei mais sobre o que descobri. Se houver erros, por favor não me chute, pois sou novo em assembler.

1. O contador do relógio em tempo real deve estar habilitado (1); O bit de seleção da fonte de clock é limpo (2) se o clock não for fornecido pelo gerador de clock principal.

2. Um ou ambos os bits de seleção de eventos de interrupção (3) devem ser definidos. E é selecionado quais eventos irão acionar a solicitação de interrupção (5).

3. As máscaras de eventos de interrupção (4, 7) devem ser especificadas.

2.5 Sobre programação ARM7 em assembler

O conjunto de instruções ARM7 (Seção 1.4) inclui apenas 45 instruções, que são bastante complexas devido à variedade de métodos de endereçamento, campos condicionais e modificadores. O programa assembler é complicado e

Com difícil de ler. Portanto, o assembler raramente é usado na programação da arquitetura ARM7.

Ao mesmo tempo, a linguagem C de alto nível esconde muitos recursos arquitetônicos do programador. O programador praticamente não toca em procedimentos como escolha do modo kernel, alocação de memória para a pilha e tratamento de interrupções. Para aprender esses procedimentos, é útil escrever pelo menos um programa simples em linguagem assembly.

Além disso, mesmo usando C, você ainda precisa recorrer à linguagem assembly.

1. Deve ser controlado O compilador C monitora se excluiu comandos importantes durante a otimização, considerando-os desnecessários. O compilador está gerando código extremamente ineficiente para uma operação relativamente simples devido à otimização insuficiente? Para garantir que o compilador realmente use os recursos de hardware projetados para aumentar a eficiência de um algoritmo específico.

2. Ao procurar erros ou causas de exceções (seção 2.4.1).

3. Obter código absolutamente ideal em termos de desempenho ou consumo de memória (seções 2.2.20, 3.1.5).

Vejamos as técnicas básicas para escrever um programa em assembler

Com o objetivo é demonstrar todo o código executado pelo microcontrolador, como está, e sem mediação Compilador C.

O procedimento para criar um projeto baseado em assembler é quase o mesmo dos programas C (seções 2.3.1–2.3.3). Existem apenas duas exceções:

a) ao arquivo de texto fonte é atribuída a extensão *.S;

b) aqui assume-se que o arquivo STARTUP.S não está conectado ao programa.

2.5.1 Regras básicas para escrever programas em assembler

O texto de um programa assembler geralmente é formatado em quatro colunas. Podemos dizer que cada linha é composta por quatro campos, a saber: rótulos, operações, operandos, comentários. Os campos são separados uns dos outros por caracteres de tabulação ou espaços.

Os campos principais são operações e operandos. As operações válidas e sua sintaxe são fornecidas na tabela (1.4.2)

Um rótulo é uma designação simbólica do endereço do comando. Em todos os lugares, em vez de um rótulo, será substituído o endereço do comando precedido pelo rótulo. Na maioria das vezes, as tags são usadas em comandos de transferência de controle. Cada rótulo deve ser exclusivo e é opcional. Ao contrário de muitas outras versões, no assembler do RealView, os rótulos não terminam com dois pontos (":").

Os comentários são opcionalmente colocados no final da linha e separados por ponto e vírgula (“;”).

Vamos dar um exemplo simples.

2.5.2 Pseudocomandos

O montador RealView suporta as chamadas pseudo-instruções. Uma pseudoinstrução é uma notação mnemônica que na verdade não corresponde ao conjunto de instruções do processador, mas é substituída por uma ou (raramente) várias instruções. Os pseudocomandos são uma espécie de macros e servem para simplificar a sintaxe. A lista de pseudocomandos suportados é fornecida na tabela (2.5.1).

2.5.3 Diretrizes de montagem

Ao contrário dos comandos, as diretivas não criam código executável que é carregado na memória do microcontrolador. As diretivas são apenas instruções para o montador; elas controlam a formação do código executável.

Vejamos as diretivas assembler do RealView 4 usadas com frequência.

Nome Constante EQU

Atribui a designação simbólica Nome à Constante, que se torna sinônimo da constante. O objetivo principal é apresentar os nomes dos registradores de controle,

Nome da ÁREA, Parâmetros

Define uma área de memória com o Nome fornecido. Usando parâmetros, você especifica a finalidade da área de memória, por exemplo, DATA (dados) ou CODE (código). Os endereços da área definida dependem do destino selecionado. A área CODE está localizada a partir do endereço 0x00000000, a área DATA - no endereço 0x40000000. O programa deve possuir uma área CODE chamada RESET. Constantes colocadas na memória do programa devem ser declaradas em uma seção com um par de parâmetros CODE, READONLY.

Indica o ponto de entrada no programa, mostra o seu “início”. Uma dessas directivas deve estar sempre presente no programa. Normalmente colocado imediatamente após a diretiva AREA RESET, CODE.

Tabela 2.5.1 – Pseudo-instruções suportadas pelo montador RealView 4

Notação mnemônica

Operação

Implementação real

e sintaxe

ADR(Cond.)

para o registro

Adicionando ou subtraindo uma constante do PC co-

Comandos ADD ou SUB

ADRL(Cond.)

para o registro

ADD ou SUB duplo envolvendo PC

(intervalo de endereços estendido)

ASR(Cond.) (S)

Deslocamento aritmético para a direita

ASR(Cond.) (S)

operando de mudança

LDR(Cond.)

para o registro

endereçamento (PC + deslocamento imediato)

Colocando uma constante

na memória do programa

LDR (do endereço de índice-

ção. PC serve como deslocamento.

LSL(Condicional)(S)

Mudança lógica para a esquerda

LSL(Condicional)(S)

operando de mudança

LSR(Cond.) (S)

Mudança lógica para a direita

LSR(Cond.) (S)

operando de mudança

POP(Cond.)

Restaurar registros da pilha

Recuperação

registros

equipe

LDMIA R13!,(...)

PRESSIONE (Cond.)

Preservação

registros

equipe

STMDB R13!,(...)

ROR(Condicional)(S)

Mudança cíclica para a direita

ROR(Condicional)(S)

operando de mudança

RRX(Cond.)(S)

Percorra direto

transferir por 1 dígito

operando de mudança

Nome ESPAÇO Tamanho

Reserva memória para armazenar dados de um determinado tamanho. O nome passa a ser sinônimo do endereço do espaço reservado. A unidade do espaço de endereço permite que esta diretiva seja usada tanto para memória permanente quanto para RAM. O objetivo principal é criar variáveis ​​globais na RAM (na área DATA).

Etiqueta DCB/DCW/DCD Constante

Dados “flash” (constantes numéricas) na memória do programa. A etiqueta passa a ser sinônimo do endereço onde os dados serão gravados. Diferentes diretivas (DCB, DCW e DCD) servem para dados de diferentes tamanhos: byte, palavra de 16 bits, palavra de 32 bits (respectivamente).

Serve como sinal do fim do arquivo. Todo o texto após esta diretiva é ignorado pelo montador.

2.5.4 Macros

Uma macro é um fragmento de programa predefinido que executa alguma operação comum. Ao contrário das sub-rotinas chamadas por meio de comandos de transferência de controle, o uso de macros não reduz o desempenho, mas não reduz o consumo de memória do programa. Porque toda vez que uma macro é chamada, o montador incorpora todo o seu texto no programa.

Para declarar uma macro, use a seguinte construção

$ Parâmetro1, $ Parâmetro2, ...

Os parâmetros permitem modificar o texto da macro cada vez que você acessá-lo. Dentro (no corpo) da macro, os parâmetros também são usados ​​com um sinal “$” precedendo. Em vez de parâmetros no corpo da macro, os parâmetros especificados durante a chamada são substituídos.

A macro é chamada assim:

Nomeie Parâmetro1, Parâmetro2, ...

É possível organizar verificação de condições e ramificações.

SE "$ Parâmetro" == "Valor"

Observe que este projeto não leva a uma verificação de software da condição pelo microcontrolador. A condição é verificada pelo montador durante a geração do código executável.