Mesclando C e Assembly no compilador CC-RX

Há cerca de um mês atrás o Sérgio Prado lançou um desafio no seu blog: escrever um código em assembly do RX para ordenar um array de inteiros. Achei interessante a idéia e aproveitei a oportunidade para praticar um pouquinho mais a programação destes poderosos microcontroladores de 32 bits da Renesas.

No entanto, logo no início me deparei com um problema: como utilizar assembly dentro do compilador CC-RX?

Embora o próprio Sérgio Prado tenha fornecido os links para o manual de software da família RX e para o manual do compilador CC-RX, o fato é que nenhum deles trata muito claramente do tema. O manual do software RX cobre a conjunto de instruções assembly e a arquitetura da CPU RX, mas não trata de como escrever um programa em assembly. O manual do compilador CC-RX, por outro lado, aborda apenas sucintamente o tema, mas as informações são esparsas, tornando trabalhoso o entendimento de quem deseja conhecer mais sobre o tema. É por isso que tomei a iniciativa de escrever este artigo, numa tentativa de consolidar algumas informações acerca deste tema.

Existem basicamente duas formas de se mesclar C e Assembly numa mesma aplicação: embutindo Assembly dentro do código C (Inline Assembly) ou criando um arquivo Assembly externo, cabendo ao linker o trabalho de mesclar os dois códigos.

Antes de apresentarmos estas duas modalidades de utilização de C e Assembly, é importante verificar como o CC-RX estrutura os parâmetros de chamada de uma função e como o resultado de uma função é retornado para o chamador.

Interface de Chamada de Função

Em virtude da grande disponibilidade de registradores nos microcontroladores RX, o CC-RX utiliza uma interface de chamada de função baseada fortemente nos registradores da CPU. Usualmente o compilador utiliza os registradores R1, R2, R3 e R4 para a passagem de parâmetros da função. No caso de funções com mais de quatro parâmetros, os parâmetros excedentes são passados através da pilha.

Desta forma, uma função com dois parâmetros formais utilizará os registradores R1 e R2 para a passagem dos mesmos (R1 irá conter o primeiro parâmetros e R2 o segundo).

No retorno de uma função, o resultado será armazenado em R1 (tipos de até 32 bits), ou utilizando os registradores R1 a R4 no caso de dados ou estruturas maiores (estruturas grandes são passadas por referência).

Um detalhe importante: lembre-se de que o registrador R0 é utilizado para armazenar o apontador de pilha atual (ISP ou USP)!

Assembly Inline

Assembly inline consiste em utilizar instruções Assembly dentro de um programa escrito em C, ou seja, permite misturar trechos de alto nível (C) com trechos de baixo nível (Assembly). Este tipo de solução é utilizada para se escrever pequenos trechos de código otimizado mas, como veremos adiante, não é adequado para escrever código Assembly mais complexo.

No CC-RX, o Assembly inline está disponível através da diretiva #pragma inline_asm que informa ao compilador que a função especificada será escrita em Assembly.

#pragma inline_asm soma
int soma(int a, int b)
{
  ADD R2,R1  ; soma R2 (b) com R1 (a) e salva em R1
}

Repare que as instruções Assembly são escritas diretamente dentro do corpo da função C e não é necessário incluir uma instrução de retorno (RTS ou RTSD) pois o próprio compilador se encarrega disso.

O Assembly inline pode ser muito útil para a escrita de funções otimizadas, mas possui algumas limitações: não é possível utilizar diretivas do assembler e somente rótulos temporários são permitidos.

Rótulos temporários são aqueles definidos através de um ponto de interrogação seguido por dois pontos “?:”. Para saltar para um rótulo temporário localizado a frente, utiliza-se o símbolo ?+ e para saltar para um rótulo atrás utiliza-se ?-. Somente é possível saltar para um rótulo a frente ou um atrás.

A seguir temos um pequeno exemplo de uma função Assembly que calcula o fatorial de um número:

#pragma inline_asm fatorial
unsigned int fatorial(unsigned int data)
{
  MOV.L R1,R3  ; copia o parâmetro para R3
  MOV.L #1,R1  ; inicializa R1 (resultado) com 1
  MOV.L #1,R2  ; R2 é um registrador auxiliar
?:             ; este é um rótulo temporário
  MUL R2,R1    ; multiplica R1 = R2 * R1
  ADD #1,R2    ; soma um ao R2
  SUB #1,R3    ; decrementa R3
  BNZ.B ?-     ; desvia para o rótulo anterior se R3 é diferente de zero
}

Fica claro que o Assembly inline não é adequado para a escrita da nossa função de ordenação, já que ela vai precisar de desvios de vários níveis, o que não é suportado pelo CC-RX. A solução então é utilizar um arquivo Assembly externo.

Assembly Externo

Outra forma de se mesclar C e Assembly é através da utilização de um arquivo separado contendo o código Assembly da função.

Para a interface do programa em C com uma função externa em Assembly, é necessário definir a mesma como externa no código C. Supondo a função asm_sort, responsável por ordenar uma array de N vetores em ordem ascendente, devemos declarar um protótipo externo para a mesma no início do código C:

extern void asm_sort(unsigned int *addr, unsigned int size);

No arquivo contendo o código-fonte Assembly, deveremos declarar um símbolo global com o nome da função precedido por um underscore. Isto é feito através da diretiva .GLB do assembler. No caso da função asm_sort em C, o símbolo da mesma deverá ser _asm_sort.

O código-fonte em Assembly também precisa especificar (através da diretiva .SECTION) a sessão de memória onde o mesmo será armazenado. Usualmente o código deve ser linkado na sessão P com atributo CODE.

A seguir temos  listagem do código que escrevi para participar do contest proposto pelo Sérgio Prado. Esta listagem foi adaptada para a interface padrão de chamada de função do CC-RX, já que no contest o endereço da array deveria ser passado em R5 e o tamanho da mesma em R6. O código também preserva o conteúdo dos registradores R10 e R11 que são utilizados pela função Assembly (os registradores R1 a R5 não necessitam ser preservados já que o compilador assume que os mesmos são alterados pela função).

.GLB    _asm_sort
.SECTION P,CODE

; Esta função ordena os N vetores de uma array
; R1 aponta para o início da array
; R2 contém o número de elementos da array
_asm_sort:
  .STACK  _asm_sort = 00000008h
  PUSH.L R10          ; salva R10 na pilha
  PUSH.L R11          ; salva R11 na pilha
  ; R3 guarda o status (0-nenhum elemento alterado, 1-elemento alterado)
  ; R4 aponta para o elemento atual da array
  ; R5 aponta para o próximo elemento da array
  ; R10 e R11 são registradores temporários
REPEAT:
  MOV.L #0,R4         ; primeiro elemento é o zero
  MOV.L #1,R5         ; próximo elemento é o um
  MOV.L #0,R3         ; apaga R3 (nenhum elemento alterado)
REPEAT2:
  ; inicia uma iteração de ordenação
  MOV.L [R4,R1],R10   ; R10 guarda o elemento atual
  MOV.L [R5,R1],R11   ; R11 guarda o próximo elemento
  CMP R11,R10         ; compara os dois elementos
  BLTU.B NEXT_ELEMENT ; desvia para NEXT_ELEMENT se o elemento atual é menor que o próximo
  ; se o próximo elemento é menor que o atual
  MOV.L R11,[R4,R1]   ; move o próximo elemento para o atual
  MOV.L R10,[R5,R1]   ; move o atual para o próximo elemento
  MOV.L #1,R3         ; sinaliza que houve alteração
  ; segue para o próximo elemento
NEXT_ELEMENT:
  ADD #1,R4           ; incrementa o apontador do elemento atual
  ADD #1,R5           ; incrementa o apontador do próximo elemento
  CMP R2,R5           ; compara o apontador do próximo elemento com o total
  BLTU.B REPEAT2      ; desvia para REPEAT2 se R2<R5 (iteração atual não completada)
  CMP #0,R3           ; verifica se algum elemento foi trocado
  BNE.B REPEAT        ; desvia para REPEAT se houve troca
  POP R11             ; restaura R11 da pilha
  POP R10             ; restaura R10 da pilha
  RTS                 ; retorna da função
.END

O código que enviei para o Sérgio Prado resultou em um tamanho total de 36 bytes, mas foi superado pela versão enviada por George Tavares que implementou uma versão do Gnome sort (que eu não conhecia) e resultou num tamanho total de código-objeto de 32 bytes! Os resultados estão aqui.

Links Relacionados

Leave a Reply