
Em um artigo anterior,
Onde estão suas constantes armazenadas em um microcontrolador CortexM (usando o compilador C ++ IAR como exemplo) , foi discutida a questão de como colocar objetos constantes na ROM. Agora, quero lhe dizer como você pode usar o padrão de gerador solitário para criar objetos na ROM.
1. Introdução
Muito já foi escrito sobre Singleton (doravante denominado Singleton), seus lados positivos e negativos. Mas, apesar de suas falhas, ele possui muitas propriedades úteis, especialmente no contexto de firmware para microcontroladores.
Para começar, para um software confiável de microcontrolador, não é recomendado que objetos sejam criados dinamicamente e, portanto, não há necessidade de excluí-los. Geralmente, os objetos são criados uma vez e permanecem ativos desde o momento em que o dispositivo é iniciado, até que seja desligado. Esse objeto pode até ser a perna da porta à qual o LED está conectado, é criado uma vez e certamente não será levado a lugar algum enquanto o aplicativo estiver em execução, e obviamente pode ser Singleton. Alguém deve criar esses objetos e pode ser Singleton.
O Singleton também garantirá que o mesmo objeto que descreve a perna da porta não seja criado duas vezes se for utilizado de repente em vários locais.
Outra, na minha opinião, uma propriedade notável de Singleton é sua facilidade de uso. Por exemplo, como no caso do manipulador de interrupções, um exemplo com o qual está no final do artigo. Mas, por enquanto, vamos lidar com o próprio Singleton.
Singleton criando objetos na RAM
Em geral, muitos artigos já foram escritos sobre eles,
Singleton (Loner) ou uma classe estática? , ou
Três anos de padrão Singleton . Portanto, não vou me concentrar no que é Singleton e descrever todas as muitas opções para sua implementação. Em vez disso, vou me concentrar em duas opções que podem ser usadas no firmware.
Para começar, vou esclarecer qual é a diferença entre o firmware do microcontrolador e o porquê de algumas implementações singleton para este software serem “melhores” que outras. Alguns critérios vêm dos requisitos de firmware e outros apenas da minha experiência:
- No firmware, não é recomendável criar objetos dinamicamente
- Geralmente, no firmware, um objeto é criado estaticamente e nunca é destruído.
- Bem, se a localização do objeto for conhecida na fase de compilação
Com base nessas suposições, consideramos duas variantes do Singleton com objetos estaticamente criados, e provavelmente o mais famoso e comum é o Meyers Singleton, a propósito, embora deva ser seguro para threads pelo padrão C ++, os compiladores para firmware o fazem dessa maneira (por exemplo, IAR), somente quando a opção especial está ativada:
template <typename T> class Singleton { public: static T & GetInstance() { static T instance ; return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; } ;
Ele usa inicialização atrasada, ou seja, A inicialização de um objeto ocorre apenas na primeira vez que
GetInstance()
chamada; considere isso como inicialização dinâmica.
int main() {
E Singleton sem inicialização atrasada:
template <typename T> class Singleton { public: static constexpr T & GetInstance() { return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; private: inline static T instance ;
Os dois Singleton criam objetos na RAM, a diferença é que, no segundo, a inicialização ocorre imediatamente após o início do programa e o primeiro é inicializado na primeira chamada.
Como eles podem ser usados na vida real. De acordo com a antiga tradição, tentarei mostrar isso usando o exemplo de um LED. Então, suponha que precisamos criar um objeto da classe
Led1
, que na verdade é apenas um alias da classe
Pin<PortA, 5>
:
using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; Led1 myLed ;
Por precaução, as classes Port e Pin se parecem com isso constexpr std::uint32_t OdrAddrShift = 20U; template <std::uint32_t addr> struct Port { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr ) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum> class Pin {
No exemplo, criei até 4 objetos diferentes do mesmo tipo na RAM e na ROM, que realmente funcionam com a mesma saída da porta A. O que não é muito bom aqui:
Bem, a primeira coisa é que aparentemente esqueci que
GreenLed
e
Led1
são do mesmo tipo e criamos vários objetos idênticos ocupando espaço em endereços diferentes. Na verdade, eu até esqueci que já havia criado objetos globalmente das
GreenLed
e
GreenLed
e também os criei localmente.
Segundo, geralmente declarar objetos globais não é bem-vindo,
Diretrizes de programação para melhor otimização do compiladorAs variáveis locais do módulo - variáveis declaradas estáticas - são preferidas a
variáveis globais (não estáticas). Evite também usar o endereço das variáveis estáticas acessadas com frequência.
e objetos locais estão disponíveis apenas no escopo da função main ().
Portanto, reescrevemos este exemplo usando Singleton:
using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; int main() {
Nesse caso, não importa o que eu esqueça, meus links sempre apontarão para o mesmo objeto. E posso obter esse link em qualquer lugar do programa, em qualquer método, inclusive, por exemplo, no método estático do manipulador de interrupções, mas mais sobre isso mais tarde. Para ser justo, devo dizer que o código não faz nada e o erro na lógica do programa não desapareceu. Bem, tudo bem, vamos descobrir onde e como, em geral, esse objeto estático criado por Singleton foi localizado e como foi inicializado?
Objeto estático
Antes de descobrir, seria bom entender o que é um objeto estático.
Se você declarar membros da classe com a palavra-chave estática, isso significa que os membros da classe simplesmente não estão vinculados às instâncias da classe, são variáveis independentes e você pode acessar esses campos sem criar um objeto de classe. Nada ameaça sua vida desde o momento do nascimento até o lançamento do programa.
Quando usado em uma declaração de objeto, o especificador estático determina apenas a vida útil do objeto. Grosso modo, a memória para esse objeto é alocada quando o programa é iniciado e é liberada quando o programa termina; quando é iniciado, também é inicializada. As únicas exceções são objetos estáticos locais, que, embora “morram” apenas no final do programa, são essencialmente “nascidos”, ou melhor, são inicializados na primeira vez em que passam por sua declaração.
A inicialização dinâmica de uma variável local com armazenamento estático é realizada pela primeira vez no momento da primeira passagem através de sua declaração; essa variável é considerada inicializada após a conclusão de sua inicialização. Se um encadeamento passar por uma declaração de variável no momento de sua inicialização por outro encadeamento, ele deverá aguardar a conclusão da inicialização.
Nas chamadas a seguir, a inicialização não ocorre. Todas as opções acima podem ser reduzidas a uma frase,
apenas uma instância de um objeto estático pode existir.Tais dificuldades levam ao fato de que o uso de variáveis e objetos estáticos locais no firmware levará a sobrecarga adicional. Você pode verificar isso com um exemplo simples:
struct Test1{ Test1(int value): j(value) {} int j; } ; Test1 &foo() { static Test1 test(10) ; return test; } int main() { for (int i = 0; i < 10; ++i) { foo().j ++; } return 0; }
Aqui, na primeira vez que a função
foo()
é chamada, o compilador deve verificar se o objeto estático local
test1
ainda não foi inicializado e chamar o construtor do objeto
Test1(10)
e, na segunda e subsequente passagem, deve garantir que o objeto já esteja inicializado e pular esta etapa. indo diretamente para
return test
.
Para fazer isso, o compilador simplesmente adiciona um sinalizador de proteção adicional
foo()::static guard for test 0x00100004 0x1 Data Lc main.o
e insere o código de verificação. Na primeira declaração de uma variável estática, esse sinalizador de proteção não está definido e, portanto, o objeto deve ser inicializado chamando o construtor; durante a próxima passagem, esse sinalizador já está definido, portanto, não há mais necessidade de inicialização e a chamada do construtor é ignorada. Além disso, essa verificação será realizada continuamente no loop for.

E se você habilitar a opção que garantirá a inicialização em aplicativos multithread, haverá ainda mais código ... (consulte a chamada para capturar e liberar um recurso durante a inicialização, sublinhada em laranja)

Assim, o preço do uso de uma variável ou objeto estático no firmware aumenta tanto no tamanho da RAM quanto no tamanho do código. E esse fato seria bom ter em mente e considerar ao desenvolver.
Outra desvantagem é o fato de o sinalizador de proteção nascer junto com a variável estática, seu tempo de vida é igual ao tempo de vida do objeto estático, é criado pelo próprio compilador e você não tem acesso a ele durante o desenvolvimento. I.e. se de repente por algum motivo
ver falha aleatóriaAs causas dos erros aleatórios são: (1) partículas alfa resultantes do processo de decaimento, (2) nêutrons, (3) uma fonte externa de radiação eletromagnética e (4) diafonia interna.
Se o sinalizador de 1 for 0, a inicialização com o valor inicial será chamada novamente. Isso não é bom, e é preciso também ter em mente. Para resumir as variáveis estáticas:
Para qualquer objeto estático (seja uma variável local ou um atributo de classe), a memória é alocada uma vez e não muda em todo o aplicativo.
Variáveis estáticas locais são inicializadas durante a primeira passagem por uma declaração de variável.
Atributos de classe estática, bem como variáveis globais estáticas, são inicializados imediatamente após o início do aplicativo. Além disso, esta ordem não está definida
Agora de volta para Singleton.
Singleton colocando objeto na ROM
De todas as opções acima, podemos concluir que, para nós, o Singleton Mayers pode ter as seguintes desvantagens: custos adicionais de RAM e ROM, um sinalizador de segurança não controlado e a incapacidade de colocar um objeto na ROM devido à inicialização dinâmica.
Mas ele tem uma vantagem maravilhosa: você controla o tempo de inicialização do objeto. Somente o próprio desenvolvedor chama
GetInstance()
primeira vez no momento em que ele precisa.
Para se livrar das três primeiras deficiências, basta usar
Singleton sem inicialização atrasada template<typename T, class Enable = void> class Singleton { public: Singleton(const Singleton&) = delete ; Singleton& operator = (const Singleton&) = delete ; Singleton() = delete ; static T& GetInstance() { return instance; } private: static T instance ; } ; template<typename T, class Enable> T Singleton<T,Enable>::instance ;
Aqui, é claro, há outro problema: não podemos controlar o tempo de inicialização do objeto de
instance
e devemos, de alguma forma, fornecer uma inicialização muito transparente. Mas este é um problema separado, não vamos nos deter agora.
Esse Singleton pode ser redesenhado para que a inicialização do objeto seja completamente estática no momento da compilação e uma instância de
T
criada na ROM usando a
static constexpr T instance
vez da
static T instance
:
template <typename T> class Singleton { public: static constexpr T & GetInstance() { return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; private:
Aqui, a criação e a inicialização do objeto serão executadas pelo compilador no estágio de compilação e o objeto cairá no segmento .readonly. É verdade que a própria classe deve satisfazer as seguintes regras:
- A inicialização de um objeto desta classe deve ser estática. (O construtor deve ser constexpr)
- A classe deve ter um construtor de cópia constexpr
- Os métodos de classe de um objeto de classe não devem alterar os dados de um objeto de classe (todos os métodos const)
Por exemplo, esta opção é bem possível:
class A { friend class Singleton<A>; public: const A & operator=(const A &) = delete ; int Get() const { return test2.Get(); } void Set(int v) const { test.SetB(v); } private: B& test;
Ótimo, você pode usar o Singleton para criar objetos na ROM, mas e se alguns objetos estiverem na RAM? Obviamente, você precisa de alguma forma manter duas especializações para Singleton, uma para objetos de RAM e outra para objetos na ROM. Você pode fazer isso digitando, por exemplo, todos os objetos que devem ser colocados na classe base da ROM:
Especialização para criação de objetos Singleton em ROM e RAM Nesse caso, você pode usá-los assim:
Como você pode usar esse Singleton na vida real.
Exemplo de Singleton
Vou tentar mostrar isso no exemplo da operação do timer e do LED. A tarefa é simples, pisque o LED no temporizador. O temporizador pode ser ajustado.
O princípio de operação será o seguinte, quando a interrupção for chamada, o método
OnInterrupt()
do timer será chamado, que por sua vez chamará o método de comutação de LEDs através da interface do assinante.
Obviamente, o objeto LED deve estar na ROM, já que não faz sentido criá-lo na RAM, não há dados nele. Em princípio, eu já o descrevi acima, portanto, basta adicionar herança do
RomObject
, criar um construtor constexpr e também herdar a interface para processar eventos do timer.
Mas
TIM_TypeDef
o Timer especificamente na RAM com um pequeno conhecimento, armazenarei um link para a estrutura
TIM_TypeDef
, um ponto e o link de um assinante e configurarei o timer no construtor (embora seja possível fazer o Timer também ir para a ROM):
Temporizador de classe class Timer { public: const Timer & operator=(const Timer &) = delete ; void SetPeriod(const std::uint16_t value) { period = value ; timer.PSC = TimerClockSpeed / 1000U - 1U ; timer.ARR = value ; }
Neste exemplo, um objeto da classe
BlinkTimer
estava localizado na RAM e um objeto da classe
Led1
localizado na ROM. Nenhum objeto global extra no código. No local em que a instância da classe é necessária, simplesmente chamamos
GetInstance()
para essa classe
Resta adicionar um manipulador de interrupção à tabela de vetores de interrupção. E aqui, é muito conveniente usar o Singleton. No método estático da classe responsável por manipular interrupções, você pode chamar o método do objeto agrupado em Singleton.
extern "C" void __iar_program_start(void) ; class InterruptHandler { public: static void DummyHandler() { for(;;) {} } static void Timer2Handler() {
Um pouco sobre a tabela em si, como tudo funciona:Imediatamente após a inicialização ou após uma redefinição, uma redefinição é interrompida com o número -8 ; na tabela, é um elemento zero, de acordo com o sinal de redefinição, o programa muda para o vetor de elemento zero, onde o ponteiro para o topo da pilha é inicializado primeiro. Este endereço é obtido a partir do local do segmento STACK que você configurou nas configurações do vinculador. Imediatamente após a inicialização do ponteiro, vá para o ponto de entrada do programa, nesse caso, no endereço da função __iar_program_start
. Em seguida, o código é inicializado, inicializando suas variáveis globais e estáticas, inicializando o coprocessador com um ponto flutuante, se ele tiver sido incluído nas configurações e assim por diante. Quando ocorre uma interrupção, o controlador de interrupção pelo número de interrupção na tabela vai para o endereço do manipulador de interrupções. No nosso caso, é InterruptHandler::Timer2Handler
, que, através de Singleton, chama o método OnInterrupt()
do nosso temporizador de piscar, que, por sua vez, OnTimeOut()
método OnTimeOut()
da perna da OnTimeOut()
.
Na verdade, é tudo, você pode executar o programa. Um exemplo de trabalho para a IAR 8.40
está aqui .
Um exemplo mais detalhado do uso do Singleton para objetos na ROM e RAM pode ser
encontrado aqui .
Links para documentação:
PS Na foto no início do artigo, mesmo assim, Singleton não é ROM, mas WHISKEY.