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.