Hoje, ninguém se surpreende com a capacidade de desenvolver em C ++ para microcontroladores. O projeto mbed é totalmente focado nesta linguagem. Vários outros RTOSs fornecem recursos de desenvolvimento C ++. Isso é conveniente porque o programador tem acesso a ferramentas de programação orientadas a objetos. No entanto, muitos RTOSs impõem várias restrições ao uso de C ++. Neste artigo, veremos os aspectos internos do C ++ e descobriremos os motivos dessas limitações.
Quero observar imediatamente que a maioria dos exemplos será considerada no RTOS Embox . De fato, projetos C ++ complexos como Qt e OpenCV funcionam em microcontroladores . OpenCV requer suporte total a C ++, que geralmente não é encontrado em microcontroladores.
Sintaxe básica
A sintaxe da linguagem C ++ é implementada pelo compilador. Mas em tempo de execução, você precisa implementar algumas entidades básicas. No compilador, eles estão incluídos na biblioteca de suporte à linguagem libsupc ++. O mais básico é o suporte para construtores e destruidores. Existem dois tipos de objetos: globais e novos.
Construtores e destruidores globais
Vamos dar uma olhada em como funciona qualquer aplicativo C ++. Antes de inserir main (), todos os objetos C ++ globais são criados, se estiverem presentes no código. A seção especial .init_array é usada para isso. Também pode haver seções .init, .preinit_array, .ctors. Para compiladores ARM modernos, o uso mais comum das seções é .preinit_array, .init e .init_array. Do ponto de vista do LIBC, este é um array comum de ponteiros para funções, que deve ser passado do início ao fim chamando o elemento correspondente do array. Após este procedimento, o controle é transferido para main ().
O código para chamar construtores para objetos globais da Embox:
void cxx_invoke_constructors(void) {
extern const char _ctors_start, _ctors_end;
typedef void (*ctor_func_t)(void);
ctor_func_t *func = (ctor_func_t *) &_ctors_start;
....
for ( ; func != (ctor_func_t *) &_ctors_end; func++) {
(*func)();
}
}
Vamos agora ver como funciona o encerramento de um aplicativo C ++, a saber, a chamada dos destruidores de objetos globais. Existem duas maneiras.
Vou começar com o mais comumente usado em compiladores - via __cxa_atexit () (do C ++ ABI). Este é um análogo da função POSIX atexit, ou seja, você pode registrar tratadores especiais que serão chamados quando o programa terminar. Quando os construtores globais são chamados no início do aplicativo, conforme descrito acima, também há código gerado pelo compilador que registra os manipuladores por meio da chamada para __cxa_atexit. O trabalho do LIBC aqui é armazenar os manipuladores necessários e seus argumentos e chamá-los quando o aplicativo terminar.
Outra maneira é armazenar ponteiros para destruidores em seções especiais .fini_array e .fini. No compilador GCC, isso pode ser alcançado com o sinalizador -fno-use-cxa-atexit. Nesse caso, os destruidores devem ser chamados na ordem inversa (do endereço alto para o endereço baixo) durante o encerramento do aplicativo. Este método é menos comum, mas pode ser útil em microcontroladores. De fato, neste caso, no momento de construir o aplicativo, você pode descobrir quantos manipuladores são necessários.
O código para chamar destruidores para objetos globais da Embox:
int __cxa_atexit(void (*f)(void *), void *objptr, void *dso) {
if (atexit_func_count >= TABLE_SIZE) {
printf("__cxa_atexit: static destruction table overflow.\n");
return -1;
}
atexit_funcs[atexit_func_count].destructor_func = f;
atexit_funcs[atexit_func_count].obj_ptr = objptr;
atexit_funcs[atexit_func_count].dso_handle = dso;
atexit_func_count++;
return 0;
};
void __cxa_finalize(void *f) {
int i = atexit_func_count;
if (!f) {
while (i--) {
if (atexit_funcs[i].destructor_func) {
(*atexit_funcs[i].destructor_func)(atexit_funcs[i].obj_ptr);
atexit_funcs[i].destructor_func = 0;
}
}
atexit_func_count = 0;
} else {
for ( ; i >= 0; --i) {
if (atexit_funcs[i].destructor_func == f) {
(*atexit_funcs[i].destructor_func)(atexit_funcs[i].obj_ptr);
atexit_funcs[i].destructor_func = 0;
}
}
}
}
void cxx_invoke_destructors(void) {
extern const char _dtors_start, _dtors_end;
typedef void (*dtor_func_t)(void);
dtor_func_t *func = ((dtor_func_t *) &_dtors_end) - 1;
/* There are two possible ways for destructors to be calls:
* 1. Through callbacks registered with __cxa_atexit.
* 2. From .fini_array section. */
/* Handle callbacks registered with __cxa_atexit first, if any.*/
__cxa_finalize(0);
/* Handle .fini_array, if any. Functions are executed in teh reverse order. */
for ( ; func >= (dtor_func_t *) &_dtors_start; func--) {
(*func)();
}
}
Os destruidores globais são necessários para reiniciar os aplicativos C ++. A maioria dos RTOS para microcontroladores executa um único aplicativo que não reinicializa. O início começa com uma função personalizada principal, a única no sistema. Portanto, em pequenos RTOSs, os destruidores globais geralmente estão vazios, porque não se destinam a ser usados.
Código de destruidores globais do Zephyr RTOS:
/**
* @brief Register destructor for a global object
*
* @param destructor the global object destructor function
* @param objptr global object pointer
* @param dso Dynamic Shared Object handle for shared libraries
*
* Function does nothing at the moment, assuming the global objects
* do not need to be deleted
*
* @return N/A
*/
int __cxa_atexit(void (*destructor)(void *), void *objptr, void *dso)
{
ARG_UNUSED(destructor);
ARG_UNUSED(objptr);
ARG_UNUSED(dso);
return 0;
}
Novos / excluir operadores
No compilador GCC, a implementação dos operadores new / delete está na biblioteca libsupc ++ e suas declarações estão no arquivo de cabeçalho.
Você pode usar as novas implementações / delete de libsupc ++. A, mas elas são bastante simples e podem ser implementadas, por exemplo, por meio de malloc / free padrão ou análogos.
Novo / excluir código de implementação para objetos Embox simples:
void* operator new(std::size_t size) throw() {
void *ptr = NULL;
if ((ptr = std::malloc(size)) == 0) {
if (alloc_failure_handler) {
alloc_failure_handler();
}
}
return ptr;
}
void operator delete(void* ptr) throw() {
std::free(ptr);
}
RTTI e exceções
Se o seu aplicativo for simples, você pode não precisar de suporte de exceção e identificação de tipo de dados dinâmico (RTTI). Nesse caso, eles podem ser desabilitados usando os sinalizadores do compilador -no-exception -no-rtti.
Mas se essa funcionalidade C ++ for necessária, ela precisa ser implementada. Isso é muito mais difícil de fazer do que novo / deletar.
A boa notícia é que essas coisas são independentes do sistema operacional e já são compiladas na biblioteca libsupc ++. Conseqüentemente, a maneira mais fácil de adicionar suporte é usar o libsupc ++. Uma biblioteca do compilador cruzado. Os próprios protótipos estão nos arquivos de cabeçalho e.
Para usar exceções de compilador cruzado, existem pequenos requisitos que precisam ser atendidos ao adicionar seu próprio método de carregamento de tempo de execução C ++. O script do vinculador deve ter uma seção especial .eh_frame. E antes de usar o runtime, esta seção deve ser inicializada com o endereço do início da seção. Embox usa o seguinte código:
void register_eh_frame(void) {
extern const char _eh_frame_begin;
__register_frame((void *)&_eh_frame_begin);
}
Para a arquitetura ARM, outras seções com sua própria estrutura de informação são usadas - .ARM.exidx e .ARM.extab. O formato dessas seções é definido na “ABI de manipulação de exceções para a arquitetura ARM” - padrão EHABI. .ARM.exidx é a tabela de índice e .ARM.extab é a tabela dos próprios elementos necessários para lidar com a exceção. Para usar essas seções para lidar com exceções, você precisa incluí-las no script do vinculador:
.ARM.exidx : { __exidx_start = .; KEEP(*(.ARM.exidx*)); __exidx_end = .; } SECTION_REGION(text) .ARM.extab : { KEEP(*(.ARM.extab*)); } SECTION_REGION(text)
Para habilitar o GCC a usar essas seções para lidar com exceções, o início e o fim da seção .ARM.exidx são especificados - __exidx_start e __exidx_end. Esses símbolos são importados para libgcc no arquivo libgcc / unwind-arm-common.inc:
extern __EIT_entry __exidx_start;
extern __EIT_entry __exidx_end;
Para obter mais informações sobre o desenrolamento da pilha no ARM, consulte o artigo .
Biblioteca padrão de linguagem (libstdc ++)
Implementação nativa da biblioteca padrão
O suporte a C ++ inclui não apenas a sintaxe básica, mas também a biblioteca padrão libstdc ++. Sua funcionalidade, assim como a sintaxe, pode ser dividida em diferentes níveis. Há coisas básicas como trabalhar com strings ou wrapper setjmp C ++. Eles são facilmente implementados por meio da biblioteca padrão C. E há coisas mais avançadas, por exemplo, a Biblioteca de Modelos Padrão (STL).
Biblioteca padrão do compilador cruzado
Coisas básicas são implementadas no Embox. Se essas coisas forem suficientes, você não pode incluir a biblioteca padrão C ++ externa. Mas se, por exemplo, for necessário suporte para contêineres, a maneira mais fácil é usar a biblioteca e os arquivos de cabeçalho do compilador cruzado.
Há uma diferença ao usar a biblioteca padrão C ++ de um compilador cruzado. Vamos dar uma olhada no padrão arm-none-eabi-gcc:
$ arm-none-eabi-gcc -v Using built-in specs. COLLECT_GCC=arm-none-eabi-gcc COLLECT_LTO_WRAPPER=/home/alexander/apt/gcc-arm-none-eabi-9-2020-q2-update/bin/../lib/gcc/arm-none-eabi/9.3.1/lto-wrapper Target: arm-none-eabi Configured with: *** --with-gnu-as --with-gnu-ld --with-newlib *** Thread model: single gcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)
É construído com suporte para a implementação --with-newlib.Newlib da biblioteca padrão C. A Embox usa sua própria implementação da biblioteca padrão. Há uma razão para isso, minimizar a sobrecarga. E, portanto, os parâmetros necessários podem ser definidos para a biblioteca C padrão, bem como para outras partes do sistema.
Como as bibliotecas C padrão são diferentes, uma camada de compatibilidade deve ser implementada para manter o tempo de execução. Vou dar um exemplo de implementação da Embox de uma das coisas necessárias, mas não óbvias, para dar suporte à biblioteca padrão de um compilador cruzado
struct _reent {
int _errno; /* local copy of errno */
/* FILE is a big struct and may change over time. To try to achieve binary
compatibility with future versions, put stdin,stdout,stderr here.
These are pointers into member __sf defined below. */
FILE *_stdin, *_stdout, *_stderr;
};
struct _reent global_newlib_reent;
void *_impure_ptr = &global_newlib_reent;
static int reent_init(void) {
global_newlib_reent._stdin = stdin;
global_newlib_reent._stdout = stdout;
global_newlib_reent._stderr = stderr;
return 0;
}
Todas as partes e suas implementações necessárias para usar o compilador cruzado libstdc ++ podem ser visualizadas no Embox na pasta 'third-party / lib / toolchain / newlib_compat /'
Suporte estendido para a biblioteca padrão std :: thread e std :: mutex
A biblioteca padrão C ++ no compilador pode ter diferentes níveis de suporte. Vamos dar uma outra olhada na saída:
$ arm-none-eabi-gcc -v *** Thread model: single gcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)
Modelo de rosca: simples. Quando o GCC é construído com esta opção, todo o suporte a thread do STL é removido (por exemplo, std :: thread e std :: mutex ). E, por exemplo, haverá problemas com a montagem de um aplicativo C ++ tão complexo como o OpenCV. Em outras palavras, esta versão da biblioteca não é suficiente para construir aplicativos que requerem esta funcionalidade.
A solução que usamos na Embox é construir nosso próprio compilador para o bem da biblioteca padrão com um modelo multithread. No caso da Embox, o posix “Modelo de rosca: posix” é usado. Neste caso, std :: thread e std :: mutex são implementados via pthread_ * e pthread_mutex_ * padrão. Isso também elimina a necessidade de incluir a camada de compatibilidade newlib.
Configuração Embox
Embora a reconstrução do compilador seja a mais confiável e forneça a solução mais completa e compatível, mas ao mesmo tempo leva muito tempo e pode exigir recursos adicionais, que não são tantos no microcontrolador. Portanto, esse método não é aconselhável para uso em todos os lugares.
A fim de otimizar os custos de suporte, a Embox introduziu várias classes abstratas (interfaces) das quais diferentes implementações podem ser especificadas.
- embox.lib.libsupcxx - define qual método usar para suportar a sintaxe básica da linguagem.
- embox.lib.libstdcxx - define qual implementação da biblioteca padrão usar
Existem três opções para libsupcxx:
- embox.lib.cxx.libsupcxx_standalone - implementação básica incluída no Embox.
- third_party.lib.libsupcxx_toolchain - use a biblioteca de suporte de linguagem do compilador cruzado
- third_party.gcc.tlibsupcxx - montagem completa da biblioteca de fontes
A opção mínima pode funcionar mesmo sem a biblioteca padrão C ++. Embox tem uma implementação baseada nas funções mais simples da biblioteca padrão C. Se esta funcionalidade não for suficiente, você pode especificar três opções libstdcxx.
- third_party.STLport.libstlportg é uma biblioteca padrão STL baseada no projeto STLport. Não requer a reconstrução do gcc. Mas o projeto não é apoiado há muito tempo
- third_party.lib.libstdcxx_toolchain - biblioteca padrão do compilador cruzado
- third_party.gcc.libstdcxx - montagem completa da biblioteca de fontes
Se desejar, nosso wiki descreve como você pode construir e executar Qt ou OpenCV no STM32F7. Todo o código é naturalmente gratuito.