3. Operações sobre valores numéricos

Nesta secção são realizadas operações sobre valores numéricos constantes ou valores em variáveis. As variáveis são representadas em registos do processador.

3.1. Afetação

Considerando que as variáveis a e b se encontram representadas nos registos R0 e R1, afetar a com b corresponde a executar a instrução mov r0, r1 que copia o conteúdo do registo R1 para R0.

Tabela 3.1 Afetação de variável com outra variável
int16_t a, b;

a = b;
;a = r0     b = r1

mov r0, r1

3.2. Afetação com constante

As constantes podem ser escritas nas linguagens de programação em diversas bases de numeração, utilizando-se para cada base uma notação sintática própria. As bases e notações mais usuais estão representadas na Tabela 3.2

Tabela 3.2 Definição de constantes numéricas

Base de numeração

Esquema sintático

Exemplo

decimal

d-- (d:1..9; restantes 0..9)

123

binária

0b-------- (0..1)

0b00110110

octal

0-- (0..7)

023 (dezanove)

hexadecimal

0x-- (0..7,a..f,A..F)

0xff

O P16 dispõe de duas instruções para carregamento de valores constantes em registo: a instrução mov rd, #constant que carrega uma constante representada a 8 bits na parte menos significativa do registo e zero na parte mais significativa; a instrução movt rd, #constant que carrega um valor de 8 bits na parte mais significativa do registo e mantém o conteúdo da parte menos significativa.

Para carregar valores numéricos entre 0 e 255 pode utilizar-se apenas uma instrução mov. Para valores superiores até 0xffff, utiliza-se uma sequência mov - movt.

Tabela 3.3 Afetação com constante

(a)

int16_t a;
a = 40;
;a = r0
mov r0, #40

(b)

int16_t a;

a = -2;
;a = r0
mov  r0, #0xfe
movt r0, #0xff

(c)

int16_t a;

a = 0x1234;
;a = r0
mov  r0, #0x34
movt r0, #0x12

(d)

#define   VALUE -2024
int16_t a;

a = VALUE;
;a = r0
.equ VALUE, -2024
mov  r0, #VALUE & 0xff
movt r0, #VALUE >> 8

Tabela 3.3 (a) – carregamento de valor positivo inferior a 256; utiliza-se apenas uma instrução mov porque esta instrução coloca também a parte alta a zero.

Tabela 3.3 (b) – carregamento de um valor negativo. -2 é representado em código dos complementos a 16 bits por 0xfffe. A instrução mov carrega 0xfe na parte baixa de R0 e a instrução movt carrega a 0xff na parte alta.

Tabela 3.3 (c) – a instrução mov carrega o valor 0x34 na parte baixa de R0 e zero na parte alta. Sendo o valor da constante superior a 256, é necessária a instrução movt para carregar 0x12 na parte alta de R0 e assim formar o valor 0x1234 em R0.

Tabela 3.3 (d) – exemplifica uma programação genérica para qualquer valor numérico no domínio de representação do tipo int16_t ou uint16_t. A diretiva .equ VALUE, -2000 significa que no texto do programa, onde aparece VALUE pode ler-se -2000. Este valor tem uma representação a 16 bits equivalente a 0xf830. A expressão VALUE & 0xff é igual a 0x30 e a expressão VALUE >> 8 é igual 0xf8.

Note que a ordem das instruções mov e movt não pode ser invertida.

Na linguagem assembly do P16 uma constante é representada a 16 bits. Para efeitos de operações é considerado um valor do conjunto dos números naturais de 0 a 65535.

No exemplo Tabela 3.3 (d) na expressão -2024 >> 8, o valor -2024 é representado em binário por 1111’1000’0001’1000 e operado como um número natural. O resultado obtido é 0000’0000’1111’1000.

Encarando 0xf818 como número natural, 0xf818 >> 8 = 0x00f8. Encarando 0xf818 como número relativo, 0xf818 >> 8 = 0xfff8.

3.3. Operações aritméticas

Tabela 3.4 Operadores aritméticos

+ adição

* multiplicação

% resto da divisão inteira

++ incremento

- subtração

/ divisão

-- decremento

3.3.1. Adição

Tabela 3.5 Adição de variáveis

(a)

int16_t a, b;

a = a + b;
; a = r0  b = r1

add  r0, r0, r1

(b)

uint16_t a, b;

a = a + b;
; a = r0  b = r1

add  r0, r0, r1

Na Tabela 3.5 apresenta-se a programação da adição de variáveis – no caso (a) variáveis com números relativos e no caso (b) variáveis com números naturais. Ao nível da máquina ambas as operação são realizadas exatamente da mesma forma pela instrução add rd, rn, rm. A diferença está na forma como se interpretam os operandos e os resultados.

No caso dos números naturais, os valores são representados diretamente em binário. Nos registos apenas podem ser representados valores no domínio \(0\) a \(2^{16} - 1\). Se o resultado ultrapassar este domínio é assinalado arrasto na flag Carry, que fica com valor 1.

No caso dos números relativos, os valores são representados em código dos complementos. Nos registos apenas podem ser representados valores no domínio \(-2^{15}\) a \(+2^{15} - 1\). No caso do resultado ultrapassar este domínio, é assinalado erro na flag Overflow, que fica com o valor 1. Isso pode aconteccer se os operandos forem ambos positivos e o resultado for superior a \(+2^{15} - 1\) ou ambos negativos e o resultado for inferior a \(-2^{15}\).

3.3.2. Subtração

Tabela 3.6 Subtração de variáveis

(a)

uint16_t a, b;

a = a - b;
; a = r0  b = r1

sub  r0, r0, r1

(b)

int16_t a, b;

a = a - b;
; a = r0  b = r1

sub  r0, r0, r1

Na Tabela 3.6 apresenta-se a programação da subtração de variáveis.

A instrução sub rd, rn, rm afeta o registo rd com o valor do registo rn menos o valor do registo rm. Além disso afeta também a flag Carry com a informação de arrasto.

Para interpretar o funcionamento da instrução SUB, pode aplicar-se o seguinte modelo: o registo rd recebe a soma do valor do registo rn com o complemento para \(2^{16}\) do valor do registo rm e a flag Carry recebe o arrasto produzido por esta adição.

No caso do valor de rn ser maior que o valor de rm, a operação de subtração não produziria arrasto (borrow). Nesta relação de valores, a adição do complemento de rm com rn produz arrasto (carry) e a flag Carry ficará com 1.

Se, pelo contrário, o valor de rn for menor que o valor de rm, a operação de subtração produziria arrasto (borrow), mas a flag Carry ficará com 0.

3.3.3. Expressão com adição e subtração

Tabela 3.7 Expressão com adição e subtração
int16_t a, b, c, d;

a = c + b - d;
; a = r0  b = r1  c = r2  d = r3
add   r0, r2, r1
sub   r0, r0, r3

A instrução add  r0, r2, r1 adiciona as variáveis c e b (R2 e R1, respetivamente) e deixa o resultado intermédio em a (R0). A instrução sub  r0, r0, r3 subtrai a variável d (R3) do resultado intermédio em R0 e coloca o resultado final em a (R0).

3.3.4. Adição a 32 bits

Tabela 3.8 Adição de valores a 32 bits
int32_t a, b, c;

a = b + c;
; a = r1:r0   b = r3:r2   c = r5:r4
add   r0, r2, r4
adc   r1, r3, r5

Os valores representados a 32 bits são guardados no processador em dois registos. A variável a é guardada nos registos R0 e R1, com a parte menos significativa em R0 e a mais significativa em R1. As variáveis c e d são guardadas nos registos R3:R2 e R5:R4, de modo semelhante.

A operação de adição das variáveis b e c é realizada em dois passos. No primeiro passo a instrução add  r0, r2, r4 adiciona as partes menos significativas das variáveis a e b afetando R0 com o resultado e a flag Carry com o arrasto. No segundo passo a instrução adc  r1, r3, r5 adiciona as partes mais significativas das variáveis com o arrasto produzido na adição anterior.

A instrução ADC também afeta a flag Carry com o arrasto produzido. O que permite utilizar sucessivamente esta instrução na operação parcial de valores representados com maior número de bits.

3.3.5. Subtração a 32 bits

Tabela 3.9 Subtração de valores a 32 bits
int32_t a, b, c;

c = a - b;
; a = r1:r0   b = r3:r2   c = r5:r4
sub   r4, r0, r2
sbc   r5, r1, r3

À semelhança da adição a 32 bits, na subtração a 32 bits é utilizada a combinação das instruções SUB e SBC. A instrução sub  r4, r0, r2 opera as partes menos significativas subtraindo R2 a R0. R4 é afetado com a diferença e a flag C com informação de arrasto (ver Subtração).

A instrução sbc  r5, r1, r3 opera as partes mais significativas, subtraindo R3 + (1 - C) a R1. R5 é afetado com a diferença e a flag C com informação de arrasto, como na instrução SUB.

Na instrução sbc rd, rn, rm, o valor a subtrair ao valor do registo rn depende da flag C. Se C for 0, subtrai o valor do registo rm menos 1; se C for 1 subtrai o valor do registo rm.

A instrução SBC opera segundo o seguinte modelo matemático:

\(rd = rn - rm - 1 + C = rn + (2^{16} - rm) - 1 + C = rn + (not(rm) + 1) - 1 + C = rn + not(rm) + C\)

3.3.6. Multiplicação e divisão

O P16 não dispõe de instruções de multiplicação ou divisão. Estas operações terão que ser realizadas programaticamente, utilizando as outras instruções.

Exemplos de programação destas operações para o P16 são apresentados no capítulo Exemplos nas secções Multiplicação e Divisão.

3.4. Operações bit-a-bit (bitwise)

3.4.1. Deslocar à direita

Deslocar um valor para a direita equivale a dividir esse valor por dois elevado ao número de posições deslocadas.

a >> 3 é equivalente a \(a / 2^3 = a / 8\)

As instruções para operar deslocações à direita são LSR e ASR. A primeira insere o valor zero nas posições de maior peso – destina-se a operar números naturais. A segunda mantém o valor do bit de maior peso (bit de sinal) – destina-se a operar números relativos. O último bit de menor peso a ser deslocado fica retido na flag Carry.

_images/lsr.png

Figura 3.1 Funcionamento da instrução LSR.

_images/asr.png

Figura 3.2 Funcionamento da instrução ASR.

Tabela 3.10 Deslocar um valor para a direita

(a)

uint16_t a;

a = a >> 1;
; a = r0

lsr  r0, r0, #1

(b)

int16_t a;

a = a >> 1;
; a = r0

asr  r0, r0, #1

(c)

uint32_t a;

a = a >> 1;
; a = r1:r0
lsr  r1, r1, #1
rrx  r0, r0

(d)

uint32_t a;

a = a >> 4;
; a = r1:r0
lsr  r0, r0, #4
lsl  r2, r1, #(16 – 4)
add  r0, r0, r2
lsr  r1, r1, #4

3.4.2. Deslocar à esquerda

Deslocar um valor para a esquerda equivale a multiplicar esse valor por dois elevado ao número de posições deslocadas.

a << 5 é equivalente a \(a * 2^5 = a * 32\)

A instrução para operar deslocações à esquerda é LSL. O último bit de maior peso a ser deslocado fica registado na flag Carry. Insere zero no(s) bit(s) de menor peso.

_images/lsl.png

Figura 3.3 Funcionamento da instrução LSL.

Tabela 3.11 Deslocar um valor para a esquerda

(a)

uint16_t a;

a = a << 1;
; a = r0

lsl  r0, r0, #1

(b)

int16_t a;

a = a << 1;
; a = r0

lsl  r0, r0, #1

(c)

uint32_t a;

a = a << 1;
; a = r1:r0
lsl  r0, r0, #1
adc  r1, r1, r1

(d)

uint32_t a;

a = a << 4;
; a = r1:r0
lsl  r1, r1, #4
lsr  r2, r0, #(16 - 4)
add  r1, r1, r2
lsl  r0, r0, #4

3.4.3. Rodar

Rodar uma palavra para a direita significa inserir nas posições de maior peso, os bits que saem das posições de menor peso; rodar uma palavra para a esquerda significa inserir nas posições de menor peso os bits que saem das posições de maior peso.

A instrução para rodar um valor é ROR. Esta instrução roda para o conteúdo de um registo para a direita, o número de posições indicado.

_images/ror.png

Figura 3.4 Funcionamento da instrução ROR.

No P16 não existe instrução específica para rodar à esquerda. O efeito de rodar à esquerda pode ser obtido rodando à direita um número complementar de posições. Veja-se o segundo exemplo da Tabela 3.12.

Tabela 3.12 Rotação de valores

Rodar o valor de R0 três posições para a direita.

ror  r0, r0, #3

Rodar o valor de R0 cinco posições para a esquerda.

ror  r0, r0, #(16 – 5)

3.4.4. Deslocar um número variável de posições

O P16 não dispõe de instrução que permita deslocar o conteúdo de um registo um número variável de posições. O terceiro parâmetro das instruções de deslocamento, que define o número de posições a deslocar, é sempre uma constante.

Para deslocar um número variável de posições é necessário elaborar um programa.

Na Tabela 3.13 (b) apresenta-se uma solução que realiza um número de iterações igual ao número de posições a deslocar (valor do registo R1), deslocando uma posição em cada iteração (linha 5).

A solução apresentada na Tabela 3.13 (c) executa o deslocamento em quatro passos (instruções lsl r0, r0, #X (linhas 4, 8, 12 e 16). Em cada passo desloca condicionalmente uma, duas, quatro ou oito posições, perfazendo um máximo de quinze posições.

O número de posições a deslocar é representado pelos quatro bits de menor peso de R1. Por exemplo, no terceiro passo (linha 10 a 12) é testado o bit de peso dois de R1. Se este bit for 1, R0 é deslocado quatro posições. Se for 0, R0 não é deslocado.

O programa da Tabela 3.13 (b) demora a executar um tempo igual ao de 2 + 5 * n instruções enquanto o programa da Tabela 3.13 (c) demora o tempo igual ao de 8 a 12 instruções.

Tabela 3.13 Deslocamento de um número variável de posições
int16_t a, n;

a <<= n;

(a)

1;a = r0   n = r1
2   add  r1, r1, #0
3   bzs  shift_end
4shift:
5   lsl  r0, r0, #1
6   sub  r1, r1, #1
7   bzc  shift
8shift_end:

(b)

 1;a = r0   n = r1
 2    lsr  r1, r1, #1
 3    bcc  shift_1
 4    lsl  r0, r0, #1
 5shift_1:
 6    lsr  r1, r1, #1
 7    bcc  shift_2
 8    lsl  r0, r0, #2
 9shift_2:
10    lsr  r1, r1, #1
11    bcc  shift_4
12    lsl  r0, r0, #4
13shift_4:
14    lsr  r1, r1, #1
15    bcc  shift_8
16    lsl  r0, r0, #8
17shift_8:

(c)

3.4.5. Afetar um bit com 1

Afetar o bit de peso três da variável a com o valor 1, mantendo o valor dos restantes bits.

A instrução mov r1, #(1 << 3) coloca o valor 0000 0000 0000 1000 em R1. A instrução orr r0, r0, r1 realiza a operação disjunção (or) entre os bits das mesmas posições de R0 e R1. O resultado é o valor original de R0 quando operado com 0 em R1 – elemento neutro – ou o valor 1 quando operado com 1 em R1 – elemento absorvente.

Tabela 3.14 Afetar o bit três de a com 1.
uint16_t a;

a |= 1 << 3;
; a = r0
mov   r1, #(1 << 3)
orr   r0, r0, r1

3.4.6. Afetar um bit com 0

Afetar o bit de peso doze da variável a com o valor 0, mantendo o valor dos restantes bits.

As instruções mov r1, #(~(1 << 12) & 0xff) e movt r1, #(~(1 << 12) >> 8) colocam o valor 1110 1111 1111 1111 em R1. A instrução and r0, r0, r1 realiza a operação conjunção (and) entre os bits das mesmas posições de R0 e R1. O resultado é o valor original de R0 quando operado com 1 em R1 – elemento neutro – ou o valor 0 quando operado com 0 em R1 – elemento absorvente.

Tabela 3.15 Afetar o bit três de a com 0.
uint16_t a;


a &= ~(1 << 12);
; a = r0
mov   r1, #(~(1 << 12) & 0xff)
movt  r1, #(~(1 << 12) >> 8)
and   r0, r0, r1

3.4.7. Afetar um bit de uma variável com o bit de outra variável

Afetar o bit de peso quatro da variável a com o valor do bit de peso treze da variável b, mantendo os restantes bits.

Tabela 3.16 Afetar o bit quatro de a com o valor do bit treze de b.
uint16_t a, b;

uint16_t tmp = b >> (13 - 4);
tmp &= (1 << 4);
a &= ~(1 << 4);
a |= tmp;
; a = r0   b = r1   tmp = r2
lsr   r2, r1, #(13 - 4)
mov   r3, #(1 << 4)
and   r2, r2, r3
mvn   r3, r3
and   r0, r0, r3
orr   r0, r0, r2

3.4.8. Multiplicar por constante

A multiplicação de uma variável por uma constante pode ser realizada, sem recurso a instrução de multiplicação ou a programa genérico de multiplicação. Veja-se o seguinte exemplo:

\(a * 21 = a * (16 + 4 + 1) = a * 16 + a * 4 + a * 1\)

A constante 21 é decomposta em parcelas de valor igual a potências de dois. As multiplicações parciais são realizadas por instruções de deslocamento.

Tabela 3.17 Multiplicar por constante.
uint16_t a, b;

uint16_t b = a * 21;
; a = r0   b = r1
mov  r1, r0       ; a * 1
lsl  r0, r0, #2
add  r1, r1, r0   ; + a * 4
lsl  r0, r0, #2
add  r1, r1, r0   ; + a * 16

3.5. Conversão entre tipos numéricos

A representação dos tipos numéricos diferem entre si no número de bits e na representação de sinal. Existe por vezes a necessidade de alterar a representação de valores. Por exemplo, afetar um valor guardado numa variável representada a oito bits (int8_t) a uma variável representada a dezasseis bits (int16_t), ou o contrário.

3.5.1. Conversão sem perda de informação

Na conversão de tipo cujo domínio de representação está contido no domínio de representação do tipo destino – representação com menos bits para representação com mais bits – não há perda de informação. Para manter a mesma representação numérica, os bits de maior peso recebem o valor zero no caso de valores naturais ou o valor do bit de sinal no caso de valores relativos.

Nos exemplos da Tabela 3.18 a conversão de 8 para 16 bits dá-se ao carregar as constantes nos registos do processador. Como o P16 realiza apenas operações a 16 bits, os valores originalmente representados a 8 bits devem ser representados a 16 bits ao serem carregados nos registos do processador.

Nos casos (a) e (b) da Tabela 3.18, o aumento para 16 bits consiste em acrescentar zero na parte alta de R0. Esse resultado é obtido pela funcionamento das instruções mov  r0, #10 e mov  r0, #22 que afetam a parte alta de R0 com zero.

Nos casos (c) e (d) da Tabela 3.18, o aumento para 16 bits consiste em propagar o bit de sinal para a parte alta do destino. No caso (c) a parte alta de R0 recebe 0xff porque se trata de carregar a constante -3. No caso (d) a parte alta da variável, representada em R2, recebe em todas as posições um valor igual ao bit de maior peso de R0 (bit de sinal do valor original).

Tabela 3.18 Conversão de tipo menor para tipo maior

(a)

uint8_t a;
uint16_t b;
a = 10;
b = a;
; a = r0   b = r1

mov   r0, #10
mov   r1, r0

(b)

uint8_t a;
int16_t b;
a = 22;
b = a;
; a = r0   b = r1

mov   r0, #22
mov   r1, r0

(c)

int8_t a;
int16_t b;
a = -3;
b = a;
; a = r0   b = r1

mov   r0, #-3
movt  r0, #0xff
mov   r1, r0

(d)

int16_t a;
int32_t b;
b = a;
; a = r0   b = r2:r1
mov   r1, r0
mov   r2, r0
asr   r2, r2, #15

3.5.2. Conversão com perda de informação

Na conversão de tipo cujo domínio de representação é superior ao domínio de representação do tipo destino, pode haver perda de informação. Para o evitar cabe ao programador garantir que o valor a converter é representável no domínio do tipo destino.

Tabela 3.19 Conversão com possível perda de informação

(a)

uint16_t a;
uint8_t b;
b = a;
; a = r0   b = r1
mov   r2, #0xff
and   r1, r0, r2

(b)

int32_t a;
int16_t b;
b = a;
; a = r1:r0   b = r2

mov   r2, r0

3.6. Exercícios

Este conjunto de exercícios destina-se a exercitar a programação, em linguagem assembly do P16, de operações sobre valores numéricos.

O objetivo de cada exercício é programar, em linguagem assembly, o cálculo da expressão definida em linguagem C.

Concretize as variáveis como registos do processador.

  1. int a = 3;
    int b = 4;
    
    int c = a - b + 20;
    
  2. int32_t a = 3;
    int32_t b = 4;
    
    int32_t c = a + b;
    
  3. int a = 3;
    
    int b = a << 2;
    
  4. uint8_t a = 24;
    
    uint8_t b = a >> 2;
    
  5. int16_t a = -40;
    
    int16_t b = a >> 2;
    
  6. int a = 3;
    
    int b = a * 2;
    
  7. int32_t a = 65535;
    
    int32_t b = a * 2;
    
  8. int a = 30;
    
    int b = a / 2;
    
  9. int a = 35;
    
    int b = a % 4;
    
  10. int a = 35;
    
    int b = a & 2;
    
  11. int a = 35;
    
    int b = a | 4;
    
  12. int a = 35;
    
    int b = a * 27;