Ensino meus alunos como usar o microcontrolador STM32F411RE, no qual existem 512 kB de ROM e 128 kB de RAM
Geralmente neste microcontrolador, um programa é gravado na memória
ROM e, na
RAM, os dados a serem alterados geralmente são necessários para criar constantes na
ROM .
No microcontrolador STM32F411RE,
ROM , a memória está localizada em endereços com
0x08000000 ... 0x0807FFFF e
RAM com
0x20000000 ... 0x2001FFFF.E se todas as configurações do vinculador estiverem corretas, o aluno calcula que, em um código tão simples, sua constante está na
ROM :
class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Você também pode tentar responder à pergunta: onde está o myConstInROM constante na
ROM ou na
RAM ?
Se você respondeu a essa pergunta que na
ROM , eu o parabenizo, na verdade provavelmente você está errado, a constante geralmente está na
RAM e para descobrir como colocar correta e corretamente suas constantes na
ROM - bem-vindo ao gato.
1. Introdução
Primeiro, uma pequena digressão, por que se preocupar com isso?
Ao desenvolver software crítico de segurança para dispositivos de medição que estejam em conformidade com a IEC 61508-3: 2010 ou um equivalente doméstico da
GOST IEC 61508-3-2018 , vários pontos devem ser levados em consideração que não são críticos para o software convencional.
A principal mensagem deste padrão é que o software deve detectar qualquer falha que afete a confiabilidade do sistema e colocá-lo no modo "travar"Além de falhas mecânicas óbvias, por exemplo, falha no sensor ou degradação e falha de componentes eletrônicos, erros causados pela falha do ambiente de software, por exemplo, microcontrolador de
RAM ou
ROM , devem ser detectados.
E se nos dois primeiros casos, é possível detectar um erro apenas de maneiras indiretas um pouco confusas (existem algoritmos que determinam a falha do sensor, por exemplo, o
Método de avaliação do estado de um conversor térmico de resistência ), então, no caso de uma falha no ambiente de software, isso pode ser feito com muito mais facilidade, por exemplo, uma falha de memória pode verifique com uma simples verificação de integridade dos dados. Se a integridade dos dados for violada, interprete isso como uma falha de memória.
Se os dados estiverem na RAM por um longo período sem verificação e atualização, a probabilidade de que algo lhes aconteça devido à falha da
RAM se tornará mais alta com o tempo. Um exemplo são alguns coeficientes de calibração para calcular a temperatura definida na fábrica e gravada em uma EEPROM externa. Na inicialização, elas são lidas e gravadas na
RAM e ficam lá até que a energia seja desligada. E na vida, o sensor de temperatura pode trabalhar durante todo o período do intervalo de calibração, até 3-5 anos. Obviamente, esses dados de
RAM devem ser protegidos e verificados periodicamente quanto à sua integridade.
Mas também existem dados, como uma constante declarada simplesmente para facilitar a leitura, um objeto de um driver de LCD, SPI ou I2C, que não deve ser alterado, é criado uma vez e não é excluído até que a energia seja desligada.
Esses dados são mais bem guardados na
ROM . É mais confiável do ponto de vista da tecnologia e é muito mais fácil verificá-la, basta ler periodicamente a soma de verificação de toda a memória somente leitura em alguma tarefa de baixa prioridade. Se a soma de verificação não corresponder, você pode simplesmente relatar a falha da
ROM e o sistema de diagnóstico exibirá um acidente.
Se esses dados estiverem na
RAM , seria problemático ou até impossível determinar sua integridade devido ao fato de não estar claro onde os dados imutáveis estão na RAM e onde são mutáveis, o vinculador o coloca como deseja e para proteger cada objeto de RAM com uma soma de verificação paranóia.
Portanto, a maneira mais fácil é ter 100% de certeza de que os dados constantes estão na
ROM . Como fazer isso, eu quero tentar explicar. Mas primeiro você precisa falar sobre a organização da memória no ARM.
Organização da memória
Como você sabe, o núcleo do ARM possui uma arquitetura de Harvard - os barramentos de dados e código são separados. Normalmente, isso significa que se supõe que haja uma memória separada para programas e uma memória separada para dados. Mas o fato é que o ARM é uma arquitetura de Harvard modificada, ou seja, o acesso à memória é realizado em um barramento e o dispositivo de gerenciamento de memória já fornece a separação de barramentos usando sinais de controle: leia, escreva ou selecione uma área de memória.
Assim, dados e código podem estar na mesma área de memória. Neste espaço de endereço único pode ser localizado e memória
ROM e
RAM e periféricos. E isso significa que, de fato, tanto o código quanto os dados podem chegar até onde depende do compilador e vinculador.
Portanto, para distinguir entre as áreas de memória da
ROM (Flash) e
RAM, elas geralmente são indicadas nas configurações do vinculador, por exemplo, na IAR 8.40.1, é assim:
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000; define symbol __ICFEDIT_region_ROM_end__ = 0x0807FFFF; define symbol __ICFEDIT_region_RAM_start__ = 0x20000000; define symbol __ICFEDIT_region_RAM_end__ = 0x2001FFFF; define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__]; define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__];
A RAM neste microcontrolador está localizada em
0x20000000 ... 0x2001FFF e a
ROM em
0x008000000 ... 0x0807FFFF .
Você pode alterar facilmente o endereço inicial ROM_start para o endereço RAM, digamos RAM_start e o endereço final ROM_end__ para RAM_end__, e seu programa estará completamente localizado na RAM.
Você pode até fazer o oposto e especificar a
RAM na área de memória da
ROM , e seu programa será montado e piscará com sucesso, embora não funcione :)
Alguns microcontroladores, como o AVR, inicialmente possuem um espaço de endereço separado para a memória do programa, a memória de dados e os periféricos e, portanto, esses truques não funcionam lá, e o programa é gravado na
ROM por padrão.
Todo o espaço de endereço no CortexM é único e o código e os dados podem ser localizados em qualquer lugar. Usando as configurações do vinculador, você pode definir a região para os endereços ROM e RAM . O IAR localiza o segmento de código .text na região da ROM
Arquivo e segmentos de objetos
Acima, mencionei o segmento de código, vamos ver o que é.
Um arquivo de objeto separado é criado para cada módulo compilado, que contém as seguintes informações:
- Segmentos de código e dados
- Informações sobre depuração do DWARF
- Tabela de caracteres
Estamos interessados em
segmentos de código e dados.
Um segmento é um elemento que contém um pedaço de código ou dados que deve ser colocado em um endereço físico na memória. Um segmento pode conter vários fragmentos, geralmente um fragmento para cada variável ou função. Um segmento pode ser colocado na
ROM e na
RAM .
Cada segmento tem um nome e um atributo que define seu conteúdo. O atributo é usado para definir um segmento na configuração do vinculador. Por exemplo, os atributos podem ser:
- código - código executável
- readonly - variáveis constantes
- readwrite - variáveis inicializadas
- zeroinit - variáveis inicializadas com zero
Obviamente, existem outros tipos de segmentos, por exemplo, segmentos contendo informações de depuração, mas estaremos interessados apenas naqueles que contêm código ou dados de nosso aplicativo.
Em geral, um segmento é o menor bloco vinculável. No entanto, se necessário, o vinculador também pode indicar blocos ainda menores (fragmentos). Não consideraremos essa opção, faremos com segmentos.
Durante a compilação, dados e funções são colocados em diferentes segmentos. E durante a vinculação, o vinculador atribui endereços físicos reais a diferentes segmentos. O compilador IAR possui nomes de segmentos predefinidos, alguns dos quais fornecerei abaixo:
- .bss - Contém variáveis estáticas e globais inicializadas em 0
- .CSTACK - Contém a pilha usada pelo programa
- .data - Contém variáveis inicializadas estáticas e globais
- .data_init - contém os valores iniciais dos dados na seção .data se a diretiva de inicialização do vinculador for usada
- HEAP - contém a pilha usada para hospedar dados dinâmicos
- .intvec - contém uma tabela de vetores de interrupção
- .rodata - Contém dados constantes
- .text - Contém código do programa
Para entender onde as constantes estão localizadas, estaremos interessados apenas em segmentos
.rodata - um segmento no qual constantes são armazenadas,
.data - um segmento no qual todas as variáveis estáticas e globais inicializadas são armazenadas,
.bss - um segmento no qual todas as variáveis estáticas e globais de
dados inicializadas com zero (0) são armazenadas,
.text - um segmento para armazenar código.
Na prática, isso significa que se você definir a variável
int val = 3
, a variável em si será localizada pelo compilador no segmento
.data e marcada com o atributo
readwrite , e o número 3 poderá ser colocado no segmento
.text ou no segmento
.rodata ou, se uma diretiva especial para o vinculador em
.data_init é aplicada e também é marcada como
somente leitura por ele .
O segmento
.rodata contém dados constantes e inclui variáveis constantes, seqüências de caracteres, literais agregados e assim por diante. E
esse segmento pode ser colocado em qualquer lugar da memória.Agora fica mais claro o que é prescrito nas configurações do vinculador e por que:
place in ROM_region { readonly };
Ou seja, todos os dados marcados com o atributo
somente leitura devem ser colocados em ROM_region. Assim, dados de diferentes segmentos, mas marcados com o atributo readonly, podem entrar na ROM.
Bem, isso significa que todas as constantes devem estar na ROM, mas por que, no nosso código, no começo do artigo, o objeto constante ainda está na RAM? class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Dados constantes
Antes de esclarecer a situação, lembremos primeiro que variáveis globais são criadas na memória compartilhada, variáveis locais, ou seja, variáveis declaradas dentro de funções "normais" são criadas na pilha ou nos registradores e variáveis locais estáticas também são criadas na memória compartilhada.
O que isso significa em C ++. Vejamos um exemplo:
void foo(const int& C1, const int& C2, const int& C3, const int& C4, const int& C5, const int& C6) { std::cout << C1 << C2 << C3 << C4 << C5 << C6 << std::endl; }
Isso tudo são dados constantes. Mas para qualquer um deles a regra de criação descrita acima se aplica, variáveis locais são criadas na pilha. Portanto, com nossas configurações de vinculador, deve ser assim:
- A constante global Case1 deve estar na ROM . No segmento .rodata
- A constante global Case2 deve estar em ROM . No segmento .rodata
- A constante local Case3 deve estar na RAM (a constante foi criada na pilha no segmento STACK)
- A constante estática Case4 deve estar na ROM . No segmento .rodata
- A constante local Case5 deve estar na RAM (um caso interessante, mas é exatamente idêntico ao Caso 3.)
- A constante estática Case6 deve estar na ROM . No segmento .rodata
Agora vamos examinar as informações de depuração e o arquivo de mapa gerado. O depurador mostra em quais endereços essas constantes estão.

Como eu disse antes, os endereços 0x0800 ... são endereços
ROM e 0x200 ... são
RAM . Vamos ver em quais segmentos o compilador distribuiu essas constantes:
.rodata const 0x800'4e2c 0x4 main.o //Case1 .rodata const 0x800'4e30 0x4 main.o //Case2 .rodata const 0x800'4e34 0x4 main.o //Case4 .rodata const 0x800'4e38 0x4 main.o //Case6
Quatro constantes globais e estáticas caíram no segmento
.rodata e duas variáveis locais não caíram no arquivo de mapa porque são criadas na pilha e seu endereço corresponde aos endereços da pilha. O segmento CSTACK começa em 0x2000'2488 e termina em 0x2000'0488. Como você pode ver na figura, as constantes são criadas apenas no início da pilha.
O compilador coloca constantes globais e estáticas no segmento .rodata , cuja localização é especificada nas configurações do vinculador.
Vale a pena notar outro ponto importante, a
inicialização . Variáveis globais e estáticas, incluindo constantes, devem ser inicializadas. E isso pode ser feito de várias maneiras. Se houver uma constante no segmento
.rodata , a inicialização ocorre no estágio de compilação, ou seja, o valor é gravado imediatamente no endereço em que a constante está localizada. Se essa for uma variável regular, a inicialização poderá ocorrer copiando o valor da memória ROM para o endereço da variável global:
Por exemplo, se a variável global
int i = 3
definida, o compilador a
definirá no segmento de dados
.data , o vinculador o colocará em 0x20000000:
.data inited 0x2000'0000
,
e seu valor de inicialização (3) estará no segmento
.rodata no endereço 0x8000190:
Initializer bytes const 0x800'0190
Se você escrever este código:
int i = 3; const int c = i;
É óbvio que a constante global
, é inicializada somente após a variável global
i
inicializada, ou seja, em tempo de execução. Nesse caso, a constante estará localizada na
RAMAgora, se voltarmos ao nosso
exemplo inicial class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
E nos perguntamos: em qual segmento o compilador definiu o objeto constante
myConstInROM
? E obtemos a resposta: a constante estará no segmento
.bss, contendo variáveis estáticas e globais inicializadas em zero (0).
.bss inited 0x2000'0004 0x4
myConstInROM 0x2000'0004 0x4
Porque Como no C ++, um objeto de dados declarado como constante e que precisa de inicialização dinâmica está localizado na memória de leitura e gravação e será inicializado no momento da criação.
Nesse caso, a inicialização dinâmica ocorre,
const WantToBeInROM myConstInROM(10)
, e o compilador coloca esse objeto no segmento
.bss , inicializando todos os campos 0 primeiro e, em seguida, ao criar um objeto constante, chamado construtor para inicializar o campo
i
valor 10.
Como podemos fazer o compilador colocar nosso objeto no segmento
.rodata ? A resposta a esta pergunta é simples, você sempre deve executar a inicialização estática. Você pode fazer assim:
1. Em nosso exemplo, pode-se ver que, em princípio, o compilador pode otimizar a inicialização dinâmica para estática, uma vez que o construtor é bastante simples. Para o IAR do compilador, você pode marcar a constante com o atributo
__ro_placement__ro_placement const WantToBeInROM myConstInROM
Com esta opção, o compilador colocará a variável no endereço na ROM:
myConstInROM 0x800'0144 0x4 Data
Obviamente, essa abordagem não é universal e geralmente muito específica. Portanto, passamos ao método correto :)
2. É fazer um construtor
constexpr
. Dizemos imediatamente ao compilador para usar a inicialização estática, ou seja, no estágio de compilação, quando todo o objeto será "calculado" completamente com antecedência e todos os seus campos serão conhecidos. Tudo o que precisamos fazer é adicionar constexpr ao construtor.
O objeto voa para a ROM class WantToBeInROM { private: int i; public: constexpr WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Portanto, para ter certeza de que seu objeto constante está na ROM, você precisa seguir regras simples:
- O segmento .text no qual o código é inserido deve estar na ROM. Está configurado nas configurações do vinculador.
- O segmento .rodata no qual as constantes globais e estáticas são colocadas deve estar na ROM. Está configurado nas configurações do vinculador.
- A constante deve ser global ou estática.
- Os atributos de uma classe de variável constante não devem ser mutáveis
- A inicialização do objeto deve ser estática, ou seja, o construtor da classe cujo objeto será uma constante deve ser consexpr ou não definido de forma alguma (não há inicialização dinâmica)
- Se possível, se você tiver certeza de que o objeto deve ser armazenado na ROM em vez de const, use constexpr
Algumas palavras sobre o constexpr e o construtor constexpr. A principal diferença entre const e constexpr é que a inicialização da variável const pode ser atrasada até o tempo de execução. A variável constexpr deve ser inicializada em tempo de compilação.
Todas as variáveis constexpr são do tipo const.A definição do construtor constexpr deve atender aos seguintes requisitos:
O construtor padrão implícito é o construtor constexpr. Agora vamos ver alguns exemplos:
Exemplo 1. Objeto na ROM class Test { private: int i; public: Test() {} ; int Get() const { return i + 1; } } ; const Test test;
É melhor não escrever dessa maneira, porque assim que você decidir inicializar o atributo i, o objeto voará para a RAM
Exemplo 2. Um objeto na RAM class Test { private: int i = 1;
Exemplo 3. Um objeto na RAM class Test { private: int i; public: Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10);
Exemplo 4. Objeto na ROM class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10);
Exemplo 5. Um objeto na RAM class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; int main() { const Test test(10);
Exemplo 6. Objeto na ROM class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; int main() { static const Test test(10);
Exemplo 7. Erro de compilação class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get()
Exemplo 8. Um objeto na ROM, herdado de uma classe abstrata class ITest { private: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class Test: public ITest { private: int i; public: constexpr Test(int value): i(value), ITest(value+1) {} ; int Get() const override { return i + 1; } } ; const Test test(10);
Exemplo 9. Um objeto na ROM agrega um objeto localizado na RAM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; TestImpl testImpl(1);
Exemplo 10. O mesmo objeto estático na ROM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl & obj;
Exemplo 11. E agora o objeto constante é não estático e, portanto, na RAM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl & obj;
Exemplo 12. Erro de compilação. class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl obj;
Exemplo 13. Erro de compilação class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: constexpr TestImpl(int value): k(value), ITest(value)
Exemplo 14. Um objeto na ROM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: constexpr TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) const
E, finalmente, um objeto constante contendo uma matriz, com a inicialização da matriz por meio de uma função constexpr. class Test { private: int k[100]; constexpr void InitArray() { int i = 0; for(auto& it: k) { it = i++ ; } } public: constexpr Test(): k() { InitArray();
Referências:
Guia de desenvolvimento do IAR C / C ++construtores Constexpr (C ++ 11)constexpr (C ++)PS.
Após uma discussão muito útil com Valdaros, você precisa adicionar as seguintes constantes tangentes de ponto. De acordo com o padrão C ++ e este documento N1076.pdf1. Qualquer alteração em um objeto constante (com exceção de membros mutáveis de uma classe) durante sua vida útil leva ao comportamento indefinido. I.e.
const int ci = 1 ; int* iptr = const_cast<int*>(&ci);
int i = 1; const int* ci = &i ; int* iptr = const_cast<int *> (ci);
2. O problema é que isso só funciona durante toda a vida de um objeto constante, mas no construtor e destruidor não funciona. Portanto, é bastante legítimo fazê-lo: class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1;
E é considerado legal. Apesar de termos usado o construtor constexpr, e a função constexpr nele. O objeto vai direto para a RAM.Para evitar isso, use const-constexpr em vez de const; haverá um erro de compilação que informará que algo está errado e que o objeto não pode ser constante. class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1;