Na primeira parte da série, foi cross-compilada uma aplicação simples e em seguida foi utilizado o OpenOCD para realizar a gravação do binário gerado na memória Flash do microcontrolador. Mas caso seja necessário escrever uma aplicação mais complexa, que faça uso, por exemplo, de alocação dinâmica de memória, o que é necessário alterar no projeto? Além disso, é interessante deixar o tamanho da pilha de uma aplicação configurável. Como fazer isso? Este artigo responderá essas duas perguntas.
Antes de entrarmos em maiores detalhes, é necessário possuir uma cópia local do projeto disponível no github. Caso não tenham sido realizados os passos listados no artigo mencionado acima, é interessante que esses sejam executados de forma a tirar maior proveito dos conceitos apresentados a seguir.
Uma vez que o ambiente esteja criado corretamente, basta atualizar o projeto, tendo em vista que ele vem sofrendo alterações/modificações com o tempo. Execute os seguintes comandos:
$ cd ~/work/projects/stm32f4-discovery-bare-metal $ git pull
Alocação dinâmica de memória
Para uso de alocação dinâmica de memória, a biblioteca C padrão, como especificado pelo padrão ISO/IEC 9899:2011, oferece algumas funções que gerenciam o pool de memória do sistema, tais como:
- malloc;
- calloc;
- realloc;
- free.
Para um teste inicial, vamos usar a função malloc para requisitar um bloco de memória da área de memória chamada heap. Você deve estar se perguntando agora: “Onde foi configurado o tamanho dessa área de memória?”. Nos próximos parágrafos vamos responder a essa pergunta!
É necessário que a macro PROJ_TEST_DINAMIC_MEM esteja definida no projeto para que esse teste seja realizado. Mas nada impede que sejam adicionadas ao projeto outras chamadas às funções mencionadas anteriormente. Portanto, altere o arquivo src/project.h de forma a conter a seguinte diretiva de compilação:
#define PROJ_TEST_DINAMIC_MEM
Pronto! Agora é só compilar o projeto.
$ cd ~/work/projects/stm32f4-discovery-bare-metal/hello_world $ make
Funcionou, correto? Não? Ocorreram erros de compilação? Algo como isso?
/opt/toolchains/eabi/arm-2013.11/bin/../lib/gcc/arm-none-eabi/4.8.1/../../../../arm-none-eabi/lib/thumb2/libc.a(lib_a-sbrkr.o): In function `_sbrk_r': sbrkr.c:(.text+0x12): undefined reference to `_sbrk' collect2: error: ld returned 1 exit status
Faltou alguma coisa? Sim! Precisamos entender o conceito de syscalls, mas antes disso é necessário estudar a implementação da “C runtime library” utilizada pelo cross-toolchain, a Newlib.
Newlib
A Newlib, embora não seja um produto GNU, é uma “C runtime library” muito utilizada em projetos de sistemas embarcados bare-metal e GNU-based, já que suporta uma grande quantidade de arquiteturas de processadores. Como explicado nesse artigo, uma parte da biblioteca é dependente do sistema target, chamada Syscalls.
Syscalls
Uma syscall é a ligação entre a biblioteca C padrão e o dispositivo target. São funções que executam serviços referentes a recursos do ambiente de runtime do sistema target. Quando utilizado um sistema operacional, esse é responsável por fornecer tais funções já implementadas. No entanto, quando é construído um sistema bare-metal, as implementações de acesso ao sistema target devem ser fornecidas pelo próprio desenvolvedor. Entre tais funções, pode-se citar: acesso de baixo nível do sistema de arquivos, requisições para expansão da memória de dados, gerenciamento de processos, etc.
O erro obtido durante a compilação anterior é justamente referente à ausência da implementação de uma dessas funções, _sbrk, cuja responsabilidade é receber requisições para expansão da memória de dados do programa, dentro da memória heap. A implementação da função malloc da newlib faz uso do algoritmo de Doug Lea, que usa a macro MORECORE, definida da seguinte forma no seu código:
#define MORECORE(size) _sbrk_r(reent_ptr, (size))
Agora é possível entender a causa do erro, certo? Mais outra dúvida: o que é a função _sbrk_r? A função _sbrk_r é a versão reentrante da função _sbrk, que recebe um argumento extra, um ponteiro para uma estrutura de controle de reentrância. Não é o foco deste artigo tratar reentrância, pois geralmente esse problema é resolvido usando um RTOS. Geralmente esse tipo de sistema operacional traz a sua própria implementação de memory allocator, não sendo necessário, portanto, o uso da função malloc da biblioteca C.
A seguir é fornecida a implementação da função _sbrk utilizada no projeto. Percebe-se que é feito uso de duas variáveis não definidas na aplicação: _end e _heap_end. Elas são os delimitadores da memória heap disponível no sistema target. Quando a memória disponível para alocação dinâmica acaba, _sbrk indica esse evento à função malloc, e essa, por sua vez, notifica a aplicação retornando o valor NULL. Caso contrário, é retornado o próximo endereço livre no heap à função malloc.
caddr_t _sbrk(int incr)
{
static unsigned char * heap = NULL;
static unsigned char * new_heap;
unsigned char * prev_heap_end;
if (heap == NULL) {
heap = (unsigned char *) &_end;
}
prev_heap_end = heap;
new_heap = heap + incr;
if ((new_heap > (unsigned char *) &_heap_end) || (new_heap < (unsigned char *) &_end)) {
return NULL;
}
heap += incr;
return (caddr_t) prev_heap_end;
}
Para que essa função seja definida no projeto, altere o arquivo src/project.h e habilite o define PROJ_IMPLEMMENT_SYSCALLS, da seguinte forma:
#define PROJ_IMPLEMMENT_SYSCALLS
Recompile a aplicação e verifique que o erro deixou de ocorrer. Vamos configurar as memórias heap e stack agora?
Configurando Heap e Stack
Tanto o heap quanto o stack são regiões alocadas na memória RAM do sistema, as quais podem ser configuradas de diversas formas. Neste artigo, esse procedimento é realizado no linker script do projeto. Tal arquivo é responsável por especificar a localização e tamanho dos blocos de memória do target, além de descrever o layout do arquivo executável, detalhando o mapeamento das seções dos arquivos de entrada no arquivo de saída.
Foram criados dois linker scripts no projeto:
- project/stm32f4_flash.ld: especifica os blocos de memória do microcontrolador (RAM, FLASH, etc.) e configura os tamanhos das regiões heap e stack;
- project/sections.ld: detalha o layout da memória do arquivo executável.
O arquivo sections.ld pode ser reaproveitado em outros projetos, pois trata-se de uma espécie de template de configuração de um arquivo executável. Já o arquivo stm32f4_flash.ld traz configurações de um projeto em específico, tal como mapeamento físico da memória do microcontrolador. Nesse último arquivo foram criadas algumas variáveis para configuração do heap e stack:
- _heap_size: tamanho do heap em bytes;
- _stack_size: tamanho do stack em bytes.
Para facilitar a visualização do layout da memória utilizado no projeto, veja a figura abaixo. O executável final, assim como os objetos compilados ao longo do processo de build, são formados por seções, tais como .text, .data e .bss, especificadas pelo padrão ELF. O trabalho de alocação dessas seções no arquivo binário executável é responsabilidade do linker, orientado pelas configurações indicadas no linker script.
A memória destinada para stack está localizada no final da região RAM, cujo endereço inicial é calculado pelo linker e é indicado no vetor de interrupções/exceções do microcontrolador. Logo abaixo dessa região está localizada a memória para heap, utilizada para alocação dinâmica de memória.
O sentido de crescimento da memória de stack é o oposto da memória de heap. A primeira cresce para endereços mais baixos, já a segunda avança para endereços maiores. Dessa forma, caso haja um erro no dimensionamento dessas regiões, uma pode sobrepor a outra em algum momento durante a execução da aplicação. Como dimensionar corretamente tais regiões? Esse é um assunto para outro artigo.
E vocês? Como configuram essas regiões em seus projetos? Deixem suas dicas!






