Um processador simples pode executar operações aritméticas, lógicas, teste e comparação, controle de fluxo e movimentação de dados. Tais operações e os recursos visíveis ao programador são especificados pela ISA.
No artigo de introdução foi apresentada a estrutura básica do processador e os recursos visíveis ao programador. Utilizando a ISA do ARM Cortex-M0+, este artigo apresenta o padrão ARM assembly para criar programas em assembly. Para introdução, serão especificadas as operações que caracterizam a arquitetura: load-store. No próximo artigo da série, os programas serão estruturados considerando as características de um microcontrolador.
Conhecendo a ferramenta de montagem
O desenvolvimento de um programa depende da ferramenta utilizada. Para os códigos mostrados nesse texto será utilizado o padrão ARM assembly. Cabe ressaltar que atualmente os códigos em assembly são escritos seguindo o padrão UAL (do inglês, Unified Assembler Language) [2]. A seguir são apresentadas algumas convenções do montador.
Definindo o código que será executado: Lista de Instruções
Cada linha de código – que possui uma instrução – segue o seguinte formato:
label mnemônico operando1, operando2, . ; Comentários
- label: é utilizado para referenciar o endereço que a instrução está. É opcional, e pode ser utilizado como referência em outras instruções (instruções de desvio). Também pode ser utilizado como referência para o endereço de um dado. É importante ressaltar que o label deve iniciar na primeira coluna da linha (sem espaço).
- mnemônico: é utilizado para indicar a instrução. Dependendo da instrução é necessário indicar os operandos.
- Para operações de escrita na memória, o primeiro operando indica a origem, isto é, o registrador que contém o dado que será gravado na memória.
- Para operações de leitura da memória, o primeiro operando indica o destino, isto é, o registrador que que receberá a informação que está na memória.
- Instruções no formato ARM usam o primeiro operando como destino da operação.
- Por fim, os comentários do programa são precedidos do símbolo ‘;’.
Alguns exemplos são mostrados a seguir. No padrão UAL o sufixo S deve ser utilizado nas instruções que alteram o conteúdo do registrador APSR [2]. Cabe, nesse ponto, apresentar o conteúdo do registrador APSR, em que N, Z, C e V representam, respectivamente: Valor negativo; Valor zero; Carry ou Borrow; Overflow.
Carregar um registrador com um valor constante (#constante) de 8 bits.
- MOVS R0,#10 ; R0 = 10
- MOVS R0,#0x0A ; R0 = 10
Movimentação de dados entre registradores
- MOV R0,R1
Adição entre um registrador e uma constante de 3 bits
- ADDS R0,R0,#1 ; R0 = R0 + 1
- ADDS R1,R0,#1 ; R1 = R0 + 1
Adição entre um registrador e uma constante de 8 bits
- ADDS R0,#10 ; R0 = R0 + 10
Adição entre registradores
- ADD R0,R1 ; R0 = R0 + R1
Comparação entre registradores
- CMP R0,R1 ;Z flag =1 se R0==R1
Desvio condicional
- BEQ label ; salta para o endereço indicado se Z flag = 1
Desvio incondicional
- B label ; salta para o endereço indicado
Instruindo o montador: Diretivas
Além das instruções, algumas diretivas podem ser utilizadas no programa. Para definir um valor constante no programa (#define) e referenciá-lo pelo nome, deve ser utilizada a diretiva EQU [2]. Um exemplo de aplicação é mostrado a seguir:
- MAGIC_NUMBER EQU 10
- MOVS R0, #MAGIC_NUMBER ; R0 = 10
Também é possível definir variáveis e valores constantes na memória. Para tal, as diretivas DCn são utilizadas [2]:
- DCB: Reserva espaço para valores de um byte (-2^7 à 2^8-1). Também é utilizado para sequência de caracteres (string);
- DCW: Reserva espaço para valores de dois bytes (-2^15 à 2^16-1);
- DCD: Reserva espaço para uma palavra de quatro bytes (-2^31 à 2^32-1);
- DCQ: Reserva espaço para uma palavra de oito bytes (-2^63 à 2^64-1);
- DCFS: Ponto flutuante de precisão simples (4 bytes);
- DCFD: Ponto flutuante de precisão dupla (8 bytes);
- DCI: Utilizado para declarar uma instrução utilizando linguagem de máquina.
Exemplo: Loop com 10 iterações.
DLY EQU 10 ;#define EQU 10 Main_Loop MOVS R0,#DLY ;R0 = 10 Dly_Loop SUBS R0,R0,#1 ;R0 = R0 - 1 BNE Dly_Loop ;Se R0!=0 então a flag Z (PSR) será 0. Logo a instrução não toma o desvio B Main_Loop ;Salto incondicional para Main_Loop
DLY EQU 10 Main_Loop MOVS R0,#DLY ;R0 = 10 MOVS R1,#0 ;R1 = 0 Dly_Loop ADDS R1,R1,#1 ;R1 = R1 + 1 CMP R1,R0 ;Compara se R1==R0, altera flag Z em APSR se for igual BNE Dly_Loop ;Salta para se Z == 0 B Main_Loop ;Salto incondicional para Main_Loop
Modos de Endereçamento
Como apresentado, a arquitetura do processador é caracterizada por sua operação load-store [1]. Isso significa que somente instruções load e store acessam a memória [3]. Assim sendo, tais instruções possibilitam várias formas de determinar o operando de origem e destino das operações. Além disso, é importante conhecer os tipos de dados que essas instruções operam [1]:
- Byte: 8 bits;
- Halfword: 16 bits;
- Word: 32 bits;
É importante ressaltar que todo endereço acessado deve estar alinhado [1]. Isto é, o acesso de meia-palavra deve ser alinhado por 16 bits e os acessos de palavra precisam ser alinhados por 32 bits [3]. Qualquer violação gerará uma exceção do tipo Hardware Fault [1].
Para os modos de endereçamento apresentados a seguir:
- Leitura: Rd significa registrador de destino; Rm/Rn, registrador de origem; #imm, constante de N bits.
- Escrita: Rd significa registrador de origem; #imm, constante de N bits.
Imediato
A instrução MOV permite carregar uma constante de 8 bits em um registrador. Esse método é chamado de imediado porque a constante é definida na própria instrução.
|
Descrição |
Sintaxe |
|
Valor imediato de 8 bits |
MOVS Rd, #<imm> |
Por Registradores
Movimentações internas entre registradores são feitos pela instrução MOV.
|
Descrição |
Sintaxe |
|
Entre registradores da parte baixa (low registers) |
MOVS Rd, Rm |
|
Qualquer registrador |
MOV Rd, Rm |
| Altera valor de PC (PC = Rm) | MOV PC, Rm |
Endereçamento por deslocamento
O endereço acessado na memória é dado por uma combinação de registradores/valores constantes.
Registrador base com deslocamento constante
Um valor imediato de 5 bits é somado (offset) com o valor de um registrador (Rn).
|
Descrição |
Sintaxe |
|
Word, offset imediato Rd = MEMÓRIA[Rn + (offset << 2)] |
LDR Rd, [Rn, #<imm>] |
|
Halfword, offset imediato Rd = MEMÓRIA[Rn + (offset << 1)] |
LDRH Rd, [Rn, #<imm>] |
|
Byte, offset imediato Rd = MEMÓRIA[Rn + offset] |
LDRB Rd, [Rn, #<imm>] |
|
Word, offset imediato MEMÓRIA[Rn + offset] = Rd |
STR Rd, [Rn, #<imm>] |
|
Halfword, offset imediato MEMÓRIA[Rn + offset] = Rd |
STRH Rd, [Rn, #<imm>] |
|
Byte, offset imediato MEMÓRIA[Rn + offset] = Rd |
STRB Rd, [Rn, #<imm>] |
Registrador base com deslocamento por registrador
Todos os registradores envolvidos são da região Low Registers.
|
Descrição |
Sintaxe |
|
Word, deslocamento por registrador Rd = MEMÓRIA[Rn + Rm] |
LDR Rd, [Rn, Rm] |
|
Halfword, deslocamento por registrador Rd = MEMÓRIA[Rn + Rm] |
LDRH Rd, [Rn, Rm] |
|
Byte, deslocamento por registrador Rd = MEMÓRIA[Rn + Rm] |
LDRB Rd, [Rn, Rm] |
|
Word, deslocamento por registrador MEMÓRIA[Rn + Rm] = Rd |
STR Rd, [Rn, Rm] |
|
Halfword, deslocamento por registrador MEMÓRIA[Rn + Rm] = Rd |
STRH Rd, [Rn, Rm] |
|
Byte, deslocamento por registrador MEMÓRIA[Rn + Rm] = Rd |
STRB Rd, [Rn, Rm] |
Registrador base com deslocamento (sinalizado) por registrador
O valor de leitura é ajustado considerando o sinal.
|
Descrição |
Sintaxe |
|
halfword sinalizado, offset por registrador Rd = MEMÓRIA[sinalizado(Rn + Rm)] |
LDRSH Rd, [Rn, Rm] |
|
byte sinalizado, offset por registrador Rd = sinalizado(MEMÓRIA[Rn + Rm]) |
LDRSB Rd, [Rn, Rm] |
Relativo
Nessas operações, o valor imediato é uma constante de 8 bits. Cabe ressaltar que essas instruções têm desvio máximo que pode ser alcançado [1].
|
Descrição |
Sintaxe |
|
relativo ao PC Rd = MEMÓRIA[(PC+4) + (offset << 2)] O deslocamento do PC considera o pipeline de 2 estágios |
LDR Rd, <label> |
|
relativo ao SP Rd = MEMÓRIA[SP + (offset << 2)] |
LDR Rd, [SP, #<imm>] |
|
relativo ao SP MEMÓRIA[SP + (offset << 2)] = Rd |
STR Rd, [SP, #<imm>] |
Múltiplos registradores
O argumento loreglist representa a lista de registradores (Low Registers) que serão manipulados. Já Rn, indica o endereço base da operação que ocorre da seguinte maneira: o registrador com menor número acessa o menor endereço; o registrador com maior número acessa o maior endereço. A ordem dos registradores não faz diferença.
|
Descrição |
Sintaxe |
|
Múltiplo, o endereço final da operação é gravado em Rn <loreglist> = MEMÓRIA[Rn] |
LDM Rn!, {<loreglist>} |
|
Múltiplo, o endereço indicado por Rn não é alterado <loreglist> = MEMÓRIA[Rn] |
LDM Rn, {<loreglist>} |
|
Múltiplo MEMÓRIA[Rn] = <loreglist> |
STM Rn!, {<loreglist>} |
Exemplos de aplicação
A seguir são demonstrados dois casos em que um vetor de inteiros é preenchido com os valores armazenados em outro vetor. No primeiro exemplo, a cópia é realizada item por item. No segundo, a cópia é realizada de 4 em 4.
AREA ASM_CODE, CODE, READONLY THUMB Main_Loop MOVS R0,#0 ;Contador. LDR R1,=src ;R1 = [PC+offset até src]. O valor de SRC está gravado na memória de programa. LDR R2,=dst ;R2 = [PC+offset até dst]. O valor de DST está gravado na memória de programa. Copy_Loop LSLS R3,R0,#2 ;R3 = (R0 << 2). Multiplica o valor do contador por 4 (alinhamento de palavra). LDR R4,[R1,R3] ;R4 = MEMÓRIA[R1 + R3] -> Endereço base de SRC + offset -> R4 = SRC[Contador]. STR R4,[R2,R3] ;MEMÓRIA[R2 + R3] = R4. -> Endereço base de DST + offset -> DST[Contador] = R4. ADDS R0,R0,#1 ;R0 = R0 + 1. Contador++. CMP R0,#4 ;R0 == 4 ? (V) Zflag = 1 : (F) Zflag=0. BNE Copy_Loop ;Zflag == 1? (V) Vai para Main_Loop : (F) Vai volta para Copy_Loop. B Main_Loop ;Salta para Main_Loop ;Constante armazenada na memória de programa src DCD 10,20,30,40 AREA ASM_DATA, DATA, READWRITE ;Vetor de destino (memória ram) dst DCD 0,0,0,0 END
Observações:
- THUMB indica que as instruções são Thumb e o código é UAL.
- AREA indica ao assembler que a sequência de comandos, código ou dados, pertence à região especificada.
AREA ASM_CODE, CODE, READONLY
THUMB
Main_Loop
MOVS R0,#0 ;Contador.
LDR R1,=src ;R1 = [PC+offset até src]. O valor de SRC está gravado na memória de programa.
LDR R2,=dst ;R2 = [PC+offset até dst]. O valor de DST está gravado na memória de programa.
Copy_Loop
LSLS R3,R0,#2 ;R3 = (R0 << 2). Multiplica o valor do contador por 4 (alinhamento de palavra).
LDM R1!,{R4,R5,R6,R7} ;Carrega os registradores R4~R7 com os valores de SRC[Cont+0]~SRC[Cont+3]. No final R1 = R1 + 4*4;
STM R2!,{R4,R5,R6,R7} ;Grava em SRC[Cont+0]~SRC[Cont+3] os registradores R4~R7. No final R2 = R2 + 4*4;
ADDS R0,R0,#4 ;R0 = R0 + 4. Contador+=4.
CMP R0,#8 ;R0 == 8 ? (V) Zflag = 1 : (F) Zflag=0.
BNE Copy_Loop ;Zflag == 1? (V) Vai para Main_Loop : (F) Vai volta para Copy_Loop.
B Main_Loop ;Salta para Main_Loop
;Constante armazenada na memória de programa
src DCD 10,20,30,40,50,60,70,80
AREA ASM_DATA, DATA, READWRITE
;Vetor de destino (memória ram)
dst DCD 0,0,0,0,0,0,0,0
Sabia mais
O conjunto de instruções é bastante flexível e também apresenta variações entre arquiteturas. Para conhecer as instruções e suas restrições consulte o conjunto de instruções no manual do processador e também no manual do compilador.
Configuração e inicialização de microcontroladores: Um estudo utilizando ARM Cortex-M0+
TickAttack: um gerenciador de tarefas simples para um ARM Cortex-M0
Referências
[1] Cortex-M0+Technical Reference Manual. [2] Arm® Compiler armasm User Guide. [3] STALLINGS, W. Arquitetura e organização de computadores. Pearson Prentice-Hall, 10ª ed., São Paulo. 2017.










Muito bom Fernando!
show show, saberia onde eu posso comprar um ARM Cortex-M0+ ? ou tipo uma placa pra programar que tenha um deles ?
abraços
Lauriano, você pode comprar kits STM32Fx Discovery que tenha o Cortex M0+ ou M0 na Mouser, Digikey ou em sites com o Aliexpress.
Olá, Lauriano.
Na loja Filipeflop tem duas: FRDM-KL25Z e KL05Z.
https://www.filipeflop.com/categoria/placas-de-desenvolvimento/nxp/
Ótimo artigo, espero pelos próximos