Possibilidades de alocar memória para design de firmware modular em C

16

as abordagens modulares são bastante úteis em geral (portáteis e limpas), por isso tento programar os módulos o mais independente possível de qualquer outro módulo. A maioria das minhas abordagens é baseada em uma estrutura que descreve o próprio módulo. Uma função de inicialização define os parâmetros principais, depois um manipulador (ponteiro para estrutura descritiva) é passado para qualquer função dentro do módulo que é chamada.

No momento, estou imaginando qual pode ser a melhor abordagem de memória de alocação para a estrutura que descreve um módulo. Se possível, eu gostaria do seguinte:

  • Estrutura opaca, portanto, a estrutura só pode ser alterada pelo uso das funções de interface fornecidas
  • Várias instâncias
  • memória alocada pelo vinculador

Vejo as seguintes possibilidades, que todas conflitam com um dos meus objetivos:

declaração global

várias instâncias, todas citadas pelo vinculador, mas struct não é opaco

(#includes)
module_struct module;

void main(){
   module_init(&module);
}

malloc

estrutura opaca, várias instâncias, mas allcotion no heap

no module.h:

typedef module_struct Module;

na função module.c init, malloc e retorne o ponteiro para a memória alocada

module_mem = malloc(sizeof(module_struct ));
/* initialize values here */
return module_mem;

em main.c

(#includes)
Module *module;

void main(){
    module = module_init();
}

declaração no módulo

estrutura opaca, alocada pelo vinculador, apenas um número predefinido de instâncias

mantenha toda a estrutura e memória internas ao módulo e nunca exponha um manipulador ou estrutura.

(#includes)

void main(){
    module_init(_no_param_or_index_if_multiple_instances_possible_);
}

Existe uma opção para combiná-los de alguma forma para estrutura opaca, vinculador em vez de alocação de heap e várias / várias instâncias?

solução

como proposto em algumas respostas abaixo, acho que a melhor maneira é:

  1. reservar espaço para os módulos MODULE_MAX_INSTANCE_COUNT no arquivo de origem dos módulos
  2. não defina MODULE_MAX_INSTANCE_COUNT no próprio módulo
  3. adicione um #ifndef MODULE_MAX_INSTANCE_COUNT #error ao arquivo de cabeçalho dos módulos para garantir que o usuário dos módulos esteja ciente dessa limitação e defina o número máximo de instâncias desejadas para o aplicativo
  4. na inicialização de uma instância, retorne o endereço de memória (* void) da estrutura dessecetiva ou o índice de módulos (o que você quiser mais)
L. Heinrichs
fonte
12
A maioria dos designers de FW incorporados está evitando a alocação dinâmica para manter o uso da memória determinístico e simples. Especialmente se for bare-metal e não tiver SO subjacente para gerenciar a memória.
Eugene Sh.
Exatamente, é por isso que quero que o vinculador faça as alocações.
L. Heinrichs
4
Não tenho muita certeza de que entendi ... Como você pode ter a memória alocada pelo vinculador se tiver um número dinâmico de instâncias? Isso me parece bastante ortogonal.
jcaron
Por que não deixar o vinculador alocar um grande conjunto de memórias e fazer suas próprias alocações a partir dele, o que também oferece o benefício de um alocador de sobrecarga zero. Você pode tornar o objeto do pool estático no arquivo com a função de alocação, para que seja privado. Em alguns dos meus códigos, faço todas as alocações nas várias rotinas de inicialização, depois imprimo quanto foi alocado, portanto, na compilação da produção final, defino o pool para esse tamanho exato.
Lee Daniel Crocker
2
Se for uma decisão em tempo de compilação, você pode simplesmente definir o número no seu Makefile ou equivalente, e está tudo pronto. O número não estaria na fonte do módulo, mas seria específico do aplicativo, e você apenas usa um número de instância como parâmetro.
jcaron

Respostas:

4

Existe uma opção para combiná-los de alguma forma para estrutura anônima, vinculador em vez de alocação de heap e várias / várias instâncias?

Claro que existe. Primeiro, no entanto, reconheça que "qualquer número" de instâncias deve ser corrigido, ou pelo menos um limite superior estabelecido, em tempo de compilação. Esse é um pré-requisito para que as instâncias sejam alocadas estaticamente (o que você está chamando de "alocação de vinculador"). Você pode ajustar o número sem modificação da fonte declarando uma macro que o especifique.

Em seguida, o arquivo de origem que contém a declaração de estrutura real e todas as suas funções associadas também declara uma matriz de instâncias com ligação interna. Ele fornece uma matriz, com ligação externa, de ponteiros para as instâncias ou uma função para acessar os vários ponteiros por índice. A variação da função é um pouco mais modular:

module.c

#include <module.h>

// 4 instances by default; can be overridden at compile time
#ifndef NUM_MODULE_INSTANCES
#define NUM_MODULE_INSTANCES 4
#endif

struct module {
    int demo;
};

// has internal linkage, so is not directly visible from other files:
static struct module instances[NUM_MODULE_INSTANCES];

// module functions

struct module *module_init(unsigned index) {
    instances[index].demo = 42;
    return &instances[index];
}

Eu acho que você já está familiarizado com como o cabeçalho declararia a estrutura como um tipo incompleto e declararia todas as funções (escritas em termos de ponteiros para esse tipo). Por exemplo:

module.h

#ifndef MODULE_H
#define MODULE_H

struct module;

struct module *module_init(unsigned index);

// other functions ...

#endif

Agora struct moduleé opaco em diferentes unidades de tradução module.c, * e você pode acessar e usar até o número de instâncias definidas em tempo de compilação sem qualquer alocação dinâmica.


* A menos que você copie sua definição, é claro. O ponto é que module.hnão faz isso.

John Bollinger
fonte
Eu acho que é um design estranho passar o índice de fora da classe. Ao implementar conjuntos de memória como este, deixei o índice ser um contador privado, aumentando em 1 para cada instância alocada. Até chegar a "NUM_MODULE_INSTANCES", onde o construtor retornará um erro de falta de memória.
Lundin
Esse é um argumento justo, @Lundin. Esse aspecto do design supõe que os índices tenham significado inerente, o que pode ou não ser de fato o caso. Ela é o caso, embora trivialmente assim, para o caso de começar do OP. Tal significado, se existir, poderia ser ainda mais suportado, fornecendo um meio de obter um ponteiro de instância sem inicializar.
John Bollinger
Então, basicamente, você reserva memória para n módulos, não importa quantos serão usados, em seguida, retorne um ponteiro para o próximo elemento não utilizado se o aplicativo inicializar. Eu acho que isso poderia funcionar.
L. Heinrichs
@ L.Heinrichs Sim, uma vez que os sistemas embarcados são de natureza determinística. Não existe "quantidade infinita de objetos" nem "quantidade desconhecida de objetos". Os objetos também costumam ser singletons (drivers de hardware); portanto, geralmente não há necessidade do pool de memória, pois apenas uma instância do objeto existe.
Lundin
Eu concordo na maioria dos casos. A questão também teve algum interesse teórico. Mas eu poderia usar centenas de sensores de temperatura de um fio se houver IOs suficientes disponíveis (como o exemplo que eu posso apresentar agora).
L. Heinrichs
22

Eu programo pequenos microcontroladores em C ++, que conseguem exatamente o que você deseja.

O que você chama de módulo é uma classe C ++, que pode conter dados (acessíveis externamente ou não) e funções (da mesma forma). O construtor (uma função dedicada) a inicializa. O construtor pode usar parâmetros de tempo de execução ou (meu favorito) parâmetros de tempo de compilação (modelo). As funções na classe obtêm implicitamente a variável da classe como primeiro parâmetro. (Ou, geralmente, minha preferência, a classe pode atuar como um singleton oculto, para que todos os dados sejam acessados ​​sem essa sobrecarga).

O objeto de classe pode ser global (para que você saiba no momento do link que tudo se encaixará) ou local da pilha, presumivelmente no principal. (Não gosto de C ++ globais devido à ordem de inicialização global indefinida, portanto prefiro o local da pilha).

Meu estilo de programação preferido é que os módulos são classes estáticas e sua configuração (estática) é feita por parâmetros de modelo. Isso evita quase tudo e permite a otimização. Combine isso com uma ferramenta que calcula o tamanho da pilha e você pode dormir sem preocupações :)

Minha palestra sobre essa maneira de codificar em C ++: Objects? Não, obrigado!

Muitos programadores incorporados / microcontroladores parecem não gostar do C ++ porque pensam que isso os forçaria a usar todo o C ++. Isso absolutamente não é necessário e seria uma péssima idéia. (Você provavelmente também não usa todo o C! Pense em heap, ponto flutuante, setjmp / longjmp, printf, ...)


Em um comentário, Adam Haun menciona RAII e inicialização. O IMO RAII tem mais a ver com a desconstrução, mas o ponto dele é válido: objetos globais serão construídos antes do início de seu principal, portanto, eles podem trabalhar em suposições inválidas (como a velocidade do relógio principal que será alterada posteriormente). Essa é mais uma razão para NÃO usar objetos inicializados por código global. (Eu uso um script vinculador que falhará quando eu tiver objetos globais inicializados por código.) Na IMO, esses 'objetos' devem ser explicitamente criados e transmitidos. Isso inclui um objeto 'recurso' em espera 'que fornece uma função wait (). Na minha configuração, este é 'objeto' que define a velocidade do clock do chip.

Falando sobre RAII: esse é mais um recurso do C ++ que é muito útil em pequenos sistemas embarcados, embora não seja o motivo (desalocação de memória) mais usado em sistemas mais amplos (os pequenos sistemas embarcados geralmente não usam desalocação dinâmica de memória). Pense em bloquear um recurso: você pode tornar o recurso bloqueado um objeto de invólucro e restringir o acesso ao recurso para ser possível apenas através do invólucro de bloqueio. Quando o wrapper fica fora do escopo, o recurso é desbloqueado. Isso impede o acesso sem travar e torna muito mais improvável esquecer o desbloqueio. com alguma mágica (modelo), pode ser zero.


A pergunta original não mencionou C, daí a minha resposta centrada em C ++. Se realmente deve ser C ....

Você pode usar truques de macro: declarar suas estruturas publicamente, para que eles tenham um tipo e possam ser alocados globalmente, mas altere os nomes de seus componentes além da usabilidade, a menos que alguma macro seja definida de maneira diferente, como é o caso do arquivo .c do seu módulo. Para segurança extra, você pode usar o tempo de compilação na confusão.

Ou tenha uma versão pública da sua estrutura que não tenha nada útil e tenha a versão privada (com dados úteis) apenas no seu arquivo .c e afirme que eles são do mesmo tamanho. Um pouco de truques de criação de arquivo pode automatizar isso.


O comentário do @Lundins sobre programadores ruins (incorporados):

  • O tipo de programador que você descreve provavelmente faria uma bagunça em qualquer idioma. As macros (presentes em C e C ++) são uma maneira óbvia.

  • As ferramentas podem ajudar até certo ponto. Para meus alunos, solicito um script construído que especifique sem exceções, no-rtti e forneça um erro de vinculador quando o heap for usado ou houver globais globais inicializados por código. E especifica warning = error e habilita quase todos os avisos.

  • Encorajo o uso de modelos, mas, com o constexpr e os conceitos, a metaprogramação é cada vez menos necessária.

  • "programadores confusos do Arduino" Gostaria muito de substituir o estilo de programação do Arduino (fiação, replicação de código nas bibliotecas) por uma abordagem moderna de C ++, que pode ser mais fácil, mais segura e produzir código mais rápido e menor. Se eu tivesse tempo e poder ...

Wouter van Ooijen
fonte
Obrigado por esta resposta! Usar C ++ é uma opção, mas estamos usando C na minha empresa (o que não mencionei explicitamente). Atualizei a pergunta para que as pessoas saibam que estou falando sobre C.
L. Heinrichs
Por que você está usando (apenas) C? Talvez isso lhe dê a chance de convencê-los a considerar pelo menos C ++ ... O que você quer é essencialmente (uma pequena parte do) C ++ realizado em C.
Wouter van Ooijen
O que faço no meu (primeiro projeto de hobby incorporado "real") é inicializar o padrão simples no construtor e usar um método Init separado para as classes relevantes. Outro benefício é que eu posso passar ponteiros de stub para teste de unidade.
Michel Keijzers
2
@ Michel para um projeto de hobby, você é livre para escolher o idioma? Tome C ++!
Wouter van Ooijen
4
Embora seja perfeitamente possível escrever bons programas C ++ para embarcados, o problema é que cerca de 50% de todos os programadores de sistemas embarcados existem charlatães, programadores de PC confusos, entusiastas do Arduino etc. etc. Esse tipo de pessoa simplesmente não pode usar um subconjunto limpo de C ++, mesmo que você o explique. Dê a eles C ++ e, antes que você perceba, eles usarão todo o STL, metaprogramação de modelos, manipulação de exceções, herança múltipla e assim por diante. E o resultado é, obviamente, lixo completo. Infelizmente, é assim que cerca de 8 em cada 10 projetos C ++ incorporados acabam.
Lundin
7

Acredito que o FreeRTOS (talvez outro sistema operacional?) Faça algo parecido com o que você está procurando, definindo 2 versões diferentes da estrutura.
O 'real', usado internamente pelas funções do sistema operacional, e o 'falso', que é do mesmo tamanho que o 'real', mas não possui nenhum membro útil (apenas vários int dummy1e similares).
Somente a estrutura 'fake' é exposta fora do código do SO, e isso é usado para alocar memória para instâncias estáticas da estrutura.
Internamente, quando as funções no sistema operacional são chamadas, elas recebem o endereço da estrutura 'falsa' externa como um identificador, e isso é tipicamente convertido em ponteiro para uma estrutura 'real' para que as funções do sistema operacional possam fazer o que precisam. Faz.

brhans
fonte
Boa ideia, acho que eu poderia usar --- #define BUILD_BUG_ON (condição) ((vazio) sizeof (char [1 - 2 * !! (condição)])) --- BUILD_BUG_ON (sizeof (real_struct)! = Sizeof ( fake_struct)) ----
L. Heinrichs
2

Estrutura anônima, portanto, a estrutura só pode ser alterada pelo uso das funções de interface fornecidas

Na minha opinião, isso não faz sentido. Você pode colocar um comentário lá, mas não adianta tentar escondê-lo ainda mais.

C nunca fornecerá um isolamento tão alto, mesmo se não houver declaração para a estrutura, será fácil substituí-la acidentalmente com, por exemplo, memcpy () incorreto ou estouro de buffer.

Em vez disso, apenas dê um nome à estrutura e confie nas outras pessoas para escrever um bom código também. Isso também facilitará a depuração quando a estrutura tiver um nome que você possa usar para se referir a ela.

jpa
fonte
2

Perguntas mais frequentes sobre SW puro são mais solicitadas em /programming/ .

O conceito de expor uma estrutura de tipo incompleto ao chamador, como você descreve, é freqüentemente chamado de "tipo opaco" ou "ponteiros opacos" - estrutura anônima significa algo totalmente diferente.

O problema é que o chamador não poderá alocar instâncias do objeto, apenas ponteiros para ele. Em um PC, você usariamalloc dentro dos objetos "construtor", mas o malloc é proibido nos sistemas incorporados.

Então, o que você faz no incorporado é fornecer um pool de memória. Você tem uma quantidade limitada de RAM, portanto, restringir o número de objetos que podem ser criados geralmente não é um problema.

Consulte Alocação estática de tipos de dados opacos em SO.

Lundin
fonte
Ou obrigado por esclarecer a confusão de nomes do meu lado, mal ajustarei o OP. Eu estava pensando em empilhar o estouro, mas decidi que gostaria de direcionar programadores incorporados especificamente.
L. Heinrichs