Usando volátil no desenvolvimento C incorporado


44

Eu tenho lido alguns artigos e respostas do Stack Exchange sobre o uso da volatilepalavra-chave para impedir que o compilador aplique otimizações em objetos que podem mudar de maneiras que não podem ser determinadas pelo compilador.

Se eu estiver lendo um ADC (vamos chamar a variável adcValue) e declarar essa variável como global, devo usar a palavra-chave volatilenesse caso?

  1. Sem usar volatilepalavra-chave

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
  2. Usando a volatilepalavra - chave

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    volatile uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }

Estou fazendo essa pergunta porque, durante a depuração, não vejo diferença entre as duas abordagens, embora as práticas recomendadas digam que, no meu caso (uma variável global que muda diretamente do hardware), o uso volatileé obrigatório.


1
Vários ambientes de depuração (certamente o gcc) não aplicam otimizações. Uma construção de produção normalmente será (dependendo de suas escolhas). Isso pode levar a diferenças 'interessantes' entre compilações. Observar o mapa de saída do vinculador é informativo.
Peter Smith

22
"no meu caso (variável global que muda diretamente do hardware)" - Sua variável global não é alterada pelo hardware, mas apenas pelo seu código C, do qual o compilador está ciente. - O registro de hardware no qual o ADC fornece seus resultados, no entanto, deve ser volátil, porque o compilador não pode saber se / quando seu valor será alterado (ele muda se / quando o hardware ADC concluir uma conversão.)
JimmyB

2
Você comparou o assembler gerado pelas duas versões? Isso deve mostrar o que está acontecendo sob o capô
MAWG

3
@stark: BIOS? Em um microcontrolador? O espaço de E / S mapeada na memória será não armazenável em cache (se a arquitetura ainda tiver um cache de dados em primeiro lugar, o que não é garantido) pela consistência do design entre as regras de armazenamento em cache e o mapa de memória. Mas volátil não tem nada a ver com o cache do controlador de memória.
Ben Voigt

1
@ Davidislor O padrão de linguagem não precisa dizer mais nada em geral. Uma leitura para um objeto volátil executará uma carga real (mesmo que o compilador tenha feito recentemente e normalmente saiba qual é o valor) e uma gravação para esse objeto executará um armazenamento real (mesmo que o mesmo valor tenha sido lido do objeto ) Portanto, na if(x==1) x=1;gravação pode ser otimizado para um não volátil xe não pode ser otimizado se xfor volátil. OTOH, se forem necessárias instruções especiais para acessar dispositivos externos, você poderá adicioná-los (por exemplo, se um intervalo de memória precisar ser gravado).
curiousguy

Respostas:


87

Uma definição de volatile

volatileinforma ao compilador que o valor da variável pode ser alterado sem o conhecimento do compilador. Portanto, o compilador não pode assumir que o valor não foi alterado apenas porque o programa C parece não o ter alterado.

Por outro lado, significa que o valor da variável pode ser necessário (lido) em algum outro lugar que o compilador não conheça, portanto, ele deve garantir que todas as atribuições à variável sejam realmente executadas como uma operação de gravação.

Casos de uso

volatile é necessário quando

  • representando registros de hardware (ou E / S mapeada na memória) como variáveis ​​- mesmo que o registro nunca seja lido, o compilador não deve apenas ignorar a operação de gravação pensando em "Programador estúpido. Tenta armazenar um valor em uma variável que ele / ela nunca lerá de volta. Ele nem notará se omitirmos a gravação. " Por outro lado, mesmo que o programa nunca grave um valor na variável, seu valor ainda poderá ser alterado pelo hardware.
  • compartilhando variáveis ​​entre contextos de execução (por exemplo, ISRs / programa principal) (consulte a resposta do @ kkramo)

Efeitos de volatile

Quando uma variável é declarada, volatileo compilador deve garantir que todas as atribuições a ele no código do programa sejam refletidas em uma operação de gravação real e que toda leitura no código do programa leia o valor da memória (mmapped).

Para variáveis ​​não voláteis, o compilador assume que sabe se / quando o valor da variável é alterado e pode otimizar o código de maneiras diferentes.

Por um lado, o compilador pode reduzir o número de leituras / gravações na memória, mantendo o valor nos registros da CPU.

Exemplo:

void uint8_t compute(uint8_t input) {
  uint8_t result = input + 2;
  result = result * 2;
  if ( result > 100 ) {
    result -= 100;
  }
  return result;
}

Aqui, o compilador provavelmente nem alocará RAM para a resultvariável e nunca armazenará os valores intermediários em nenhum lugar, exceto em um registro da CPU.

Se resultfosse volátil, todas as ocorrências resultno código C exigiriam que o compilador executasse um acesso à RAM (ou uma porta de E / S), levando a um desempenho mais baixo.

Em segundo lugar, o compilador pode reordenar as operações em variáveis ​​não voláteis para desempenho e / ou tamanho do código. Exemplo simples:

int a = 99;
int b = 1;
int c = 99;

poderia ser reordenado para

int a = 99;
int c = 99;
int b = 1;

o que pode salvar uma instrução do assembler porque o valor 99não precisará ser carregado duas vezes.

Se a, be cforam volátil o compilador teria que emitir instruções que atribuem os valores na ordem exata como eles são dadas no programa.

O outro exemplo clássico é assim:

volatile uint8_t signal;

void waitForSignal() {
  while ( signal == 0 ) {
    // Do nothing.
  }
}

Se, nesse caso, signalnão fosse volatile, o compilador "pensaria" que while( signal == 0 )pode ser um loop infinito (porque signalnunca será alterado pelo código dentro do loop ) e poderá gerar o equivalente a

void waitForSignal() {
  if ( signal != 0 ) {
    return; 
  } else {
    while(true) { // <-- Endless loop!
      // do nothing.
    }
  }
}

Manuseio atencioso de volatilevalores

Conforme mencionado acima, uma volatilevariável pode introduzir uma penalidade no desempenho quando é acessada com mais frequência do que o necessário. Para atenuar esse problema, você pode "não ser volátil" o valor atribuindo a uma variável não volátil, como

volatile uint32_t sysTickCount;

void doSysTick() {
  uint32_t ticks = sysTickCount; // A single read access to sysTickCount

  ticks = ticks + 1; 

  setLEDState( ticks < 500000L );

  if ( ticks >= 1000000L ) {
    ticks = 0;
  }
  sysTickCount = ticks; // A single write access to volatile sysTickCount
}

Isso pode ser especialmente benéfico nos ISRs, nos quais você deseja ser o mais rápido possível, sem acessar o mesmo hardware ou memória várias vezes quando você sabe que não é necessário, pois o valor não será alterado enquanto o ISR estiver em execução. Isso é comum quando o ISR é o 'produtor' de valores para a variável, como sysTickCountno exemplo acima. Em um AVR, seria especialmente doloroso fazer com que a função doSysTick()acesse os mesmos quatro bytes na memória (quatro instruções = 8 ciclos de CPU por acesso a sysTickCount) cinco ou seis vezes em vez de apenas duas vezes, porque o programador sabe que o valor não será ser alterado de outro código enquanto ele doSysTick()é executado.

Com esse truque, você basicamente faz exatamente o mesmo que o compilador faz para variáveis ​​não voláteis, ou seja, lê-las da memória somente quando necessário, mantém o valor em um registro por algum tempo e grava na memória somente quando necessário. ; mas desta vez, você sabe melhor que o compilador se / quando as leituras / gravações devem ocorrer, portanto, você libera o compilador dessa tarefa de otimização e faz você mesmo.

Limitações de volatile

Acesso não atômico

volatilese não fornecer acesso atômica para variáveis com várias palavras. Para esses casos, você precisará fornecer exclusão mútua por outros meios, além do uso volatile. No AVR, você pode usar ATOMIC_BLOCKa partir <util/atomic.h>ou simples cli(); ... sei();chamadas. As respectivas macros também funcionam como uma barreira de memória, o que é importante quando se trata da ordem dos acessos:

Ordem de execução

volatileimpõe ordem estrita de execução apenas com relação a outras variáveis ​​voláteis. Isso significa que, por exemplo

volatile int i;
volatile int j;
int a;

...

i = 1;
a = 99;
j = 2;

é garantido atribuir primeiro 1 a ie depois atribuir 2 a j. No entanto, é não garantiu que aserá atribuído no meio; o compilador pode fazer essa atribuição antes ou depois do trecho de código, basicamente a qualquer momento até a primeira leitura (visível) a.

Se não fosse a barreira da memória das macros mencionadas acima, o compilador poderia traduzir

uint32_t x;

cli();
x = volatileVar;
sei();

para

x = volatileVar;
cli();
sei();

ou

cli();
sei();
x = volatileVar;

(Por uma questão de completude, devo dizer que as barreiras de memória, como as implícitas nas macros sei / cli, podem realmente impedir o uso de volatile, se todos os acessos estiverem entre colchetes com essas barreiras.)


7
Boa discussão sobre un-volatiling para o desempenho :)
awjlogan

3
Eu sempre gosto de mencionar a definição de volátil na ISO / IEC 9899: 1999 6.7.3 (6): An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Mais pessoas devem lê-la.
Jeroen3

3
Vale a pena mencionar que cli/ seié uma solução muito pesada se seu único objetivo é alcançar uma barreira de memória, não impedir interrupções. Essas macros geram instruções reais cli/ seiadicionais e memória adicional, e é essa combinação que resulta na barreira. Para ter apenas uma barreira de memória sem desativar as interrupções, você pode definir sua própria macro com o corpo __asm__ __volatile__("":::"memory")(por exemplo, código de montagem vazio com clobber de memória).
Ruslan

3
@NicHartley No. C17 5.1.2.3 §6 define o comportamento observável : "O acesso a objetos voláteis é avaliado estritamente de acordo com as regras da máquina abstrata". O padrão C não está muito claro de onde as barreiras de memória são necessárias no geral. No final de uma expressão que usa, volatilehá um ponto de sequência e tudo depois deve ser "sequenciado depois". Significando que a expressão é uma espécie de barreira à memória. Os fornecedores de compiladores escolheram espalhar todos os tipos de mitos para colocar a responsabilidade das barreiras de memória no programador, mas isso viola as regras da "máquina abstrata".
Lundin

2
@ JimmyB Local volátil talvez útil para código como volatile data_t data = {0}; set_mmio(&data); while (!data.ready);.
Maciej Piechotka

13

A palavra-chave volátil informa ao compilador que o acesso à variável tem um efeito observável. Isso significa que toda vez que seu código-fonte usa a variável, o compilador DEVE criar um acesso à variável. Seja um acesso de leitura ou gravação.

O efeito disso é que qualquer alteração na variável fora do fluxo normal do código também será observada pelo código. Por exemplo, se um manipulador de interrupção alterar o valor. Ou se a variável é realmente algum registro de hardware que muda por si próprio.

Esse grande benefício também é sua desvantagem. Todo acesso único à variável passa pela variável e o valor nunca é mantido em um registro para um acesso mais rápido por qualquer período de tempo. Isso significa que uma variável volátil será lenta. Magnitudes mais lentas. Portanto, use apenas volátil onde for realmente necessário.

No seu caso, na medida em que você mostrou o código, a variável global só é alterada quando você a atualiza adcValue = readADC();. O compilador sabe quando isso acontece e nunca manterá o valor de adcValue em um registro em algo que possa chamar a readFromADC()função. Ou qualquer função que não conheça. Ou qualquer coisa que manipule ponteiros que possam apontar para algo adcValueassim. Realmente não há necessidade de volatilidade, pois a variável nunca muda de maneira imprevisível.


6
Eu concordo com esta resposta, mas "magnitudes mais lentas" parecem terríveis.
Kkrambo #

6
Um registro da CPU pode ser acessado em menos de um ciclo da CPU nas modernas CPUs superescalares. Por outro lado, um acesso à memória não armazenada em cache real (lembre-se de que algum hardware externo alteraria isso, portanto, nenhum cache de CPU permitido) pode estar na faixa de 100 a 300 ciclos de CPU. Então, sim, magnitudes. Não será tão ruim em um AVR ou micro controlador similar, mas a pergunta não especifica hardware.
Goswin von Brederlow

7
Nos sistemas incorporados (microcontroladores), a penalidade pelo acesso à RAM geralmente é muito menor. Os AVRs, por exemplo, levam apenas dois ciclos de CPU para uma leitura ou gravação na RAM (uma movimentação de registro e registro leva um ciclo), de modo que as economias de manter as coisas em registros se aproximam (mas nunca atingem) o máximo. 2 ciclos de relógio por acesso. - É claro que, relativamente falando, salvar um valor do registro X na RAM e recarregar imediatamente esse valor no registro X para cálculos adicionais levará 2x2 = 4 em vez de 0 ciclos (apenas mantendo o valor em X) e, portanto, infinitamente mais lento :)
JimmyB

1
São 'magnitudes mais lentas' no contexto de "escrever ou ler a partir de uma variável específica", sim. No entanto, no contexto de um programa completo que provavelmente faz muito mais do que ler / gravar em uma variável repetidamente, não, na verdade não. Nesse caso, a diferença geral é provavelmente "pequena a insignificante". Deve-se tomar cuidado, ao fazer afirmações sobre desempenho, para esclarecer se a afirmação se refere a uma operação específica ou a um programa como um todo. Diminuir a velocidade de uma operação pouco usada por um fator de ~ 300x quase nunca é um grande problema.
Aroth # 30/18

1
Você quer dizer, essa última frase? Isso significa muito mais no sentido de "a otimização prematura é a raiz de todo mal". Obviamente, você não deve usar volatiletudo apenas porque , mas também não deve se esquivar disso nos casos em que você acha que é legitimamente necessário por causa de preocupações de desempenho preventivas.
Aroth # 30/18

9

O principal uso da palavra-chave volátil em aplicativos C incorporados é marcar uma variável global que é gravada em um manipulador de interrupções. Certamente não é opcional neste caso.

Sem ele, o compilador não pode provar que o valor é gravado após a inicialização, porque não pode provar que o manipulador de interrupção já foi chamado. Portanto, ele pensa que pode otimizar a variável fora da existência.


2
Certamente existem outros usos práticos, mas esse é o mais comum.
vicatcu

1
Se o valor for lido apenas em um ISR (e alterado de main ()), você também precisará usar o volátil para garantir o acesso ATOMIC para variáveis ​​de vários bytes.
Rev1.0

15
@ Rev1.0 Não, volátil não garante aromicidade. Essa preocupação deve ser abordada separadamente.
Chris Stratton

1
Não há leitura do hardware nem interrupções no código publicado. Você está assumindo coisas da pergunta que não estão lá. Realmente não pode ser respondido em sua forma atual.
Lundin 29/11

3
msgstr "marca uma variável global que é gravada em um manipulador de interrupções" nope. É para marcar uma variável; global ou não; que isso pode ser alterado por algo fora do entendimento dos compiladores. Interrupção não necessária. Pode ser compartilhado de memória ou alguém enfiar uma sonda para a memória (este último não é recomendado para qualquer coisa mais moderno do que 40 anos)
UKMonkey

9

Existem dois casos em que você deve usar volatileem sistemas incorporados.

  • Ao ler de um registro de hardware.

    Isso significa que o próprio registro mapeado na memória faz parte dos periféricos de hardware dentro do MCU. Provavelmente terá algum nome enigmático como "ADC0DR". Esse registro deve ser definido no código C, por meio de algum mapa de registro entregue pelo fornecedor da ferramenta ou por você. Para fazer você mesmo, você faria (assumindo um registro de 16 bits):

    #define ADC0DR (*(volatile uint16_t*)0x1234)

    onde 0x1234 é o endereço em que o MCU mapeou o registro. Como volatilejá faz parte da macro acima, qualquer acesso a ela será qualificado como volátil. Portanto, este código está correto:

    uint16_t adc_data;
    adc_data = ADC0DR;
  • Ao compartilhar uma variável entre um ISR e o código relacionado usando o resultado do ISR.

    Se você tem algo parecido com isto:

    uint16_t adc_data = 0;
    
    void adc_stuff (void)
    {
      if(adc_data > 0)
      {
        do_stuff(adc_data);
      } 
    }
    
    interrupt void ADC0_interrupt (void)
    {
      adc_data = ADC0DR;
    }

    Então o compilador pode pensar: "adc_data é sempre 0 porque não é atualizado em nenhum lugar. E essa função ADC0_interrupt () nunca é chamada, portanto a variável não pode ser alterada". O compilador geralmente não percebe que as interrupções são chamadas pelo hardware, não pelo software. Portanto, o compilador remove e remove o código, if(adc_data > 0){ do_stuff(adc_data); }pois acha que nunca pode ser verdade, causando um bug muito estranho e difícil de depurar.

    Ao declarar adc_data volatile, o compilador não tem permissão para fazer tais suposições e não pode otimizar o acesso à variável.


Anotações importantes:

  • Um ISR sempre deve ser declarado dentro do driver de hardware. Nesse caso, o ADC ISR deve estar dentro do driver ADC. Nada além do driver deve se comunicar com o ISR - tudo o resto é programação espaguete.

  • Ao escrever C, toda a comunicação entre um ISR e o programa em segundo plano deve ser protegida contra as condições da corrida. Sempre , sempre, sem exceções. O tamanho do barramento de dados do MCU não importa, porque mesmo se você fizer uma única cópia de 8 bits em C, o idioma não poderá garantir a atomicidade das operações. A menos que você use o recurso C11 _Atomic. Se esse recurso não estiver disponível, você deverá usar algum tipo de semáforo ou desativar a interrupção durante a leitura, etc. O assembler embutido é outra opção. volatilenão garante atomicidade.

    O que pode acontecer é o seguinte:
    -Carregar o valor da pilha no registro
    -Interromper ocorre
    -Utilizar o valor do registro

    E então não importa se a parte "valor de uso" é uma única instrução em si. Infelizmente, uma parte significativa de todos os programadores de sistemas embarcados não sabe disso, provavelmente o tornando o bug de sistema embarcado mais comum de todos os tempos. Sempre intermitente, difícil de provocar, difícil de encontrar.


Um exemplo de driver ADC gravado corretamente seria assim (supondo que C11 _Atomicnão esteja disponível):

adc.h

// adc.h
#ifndef ADC_H
#define ADC_H

/* misc init routines here */

uint16_t adc_get_val (void);

#endif

adc.c

// adc.c
#include "adc.h"

#define ADC0DR (*(volatile uint16_t*)0x1234)

static volatile bool semaphore = false;
static volatile uint16_t adc_val = 0;

uint16_t adc_get_val (void)
{
  uint16_t result;
  semaphore = true;
    result = adc_val;
  semaphore = false;
  return result;
}

interrupt void ADC0_interrupt (void)
{
  if(!semaphore)
  {
    adc_val = ADC0DR;
  }
}
  • Este código está assumindo que uma interrupção não pode ser interrompida por si só. Em tais sistemas, um booleano simples pode atuar como semáforo, e não precisa ser atômico, pois não haverá danos se a interrupção ocorrer antes que o booleano seja definido. A desvantagem do método simplificado acima é que ele descartará as leituras do ADC quando ocorrerem condições de corrida, usando o valor anterior. Isso também pode ser evitado, mas o código fica mais complexo.

  • Aqui volatileprotege contra erros de otimização. Não tem nada a ver com os dados provenientes de um registro de hardware, apenas que os dados são compartilhados com um ISR.

  • staticprotege contra a programação de espaguete e a poluição do namespace, tornando a variável local para o motorista. (Isso é bom em aplicativos single-core e single-thread, mas não em aplicativos multithread.)


Difícil de depurar é relativo; se o código for removido, você notará que seu código valioso se foi - é uma afirmação bastante ousada de que algo está errado. Mas eu concordo, pode haver efeitos muito estranhos e difíceis de depurar.
Arsenal

@Arsenal Se você tem um bom depurador que alinha o assembler com o C e conhece pelo menos um pouco de asm, sim, pode ser fácil identificar. Mas para um código complexo maior, um grande pedaço de asm gerado por máquina não é trivial. Ou se você não conhece asm. Ou se o seu depurador for uma porcaria e não mostrar asm (tougheclipsecough).
Lundin

Pode ser que eu esteja um pouco mimado usando os depuradores de Lauterbach então. Se você tentar definir um ponto de interrupção no código que foi otimizado, ele o definirá em um lugar diferente e você saberá que algo está acontecendo lá.
Arsenal

@Arsenal Sim, o tipo de C / asm misto que você pode obter em Lauterbach não é de forma alguma padrão. A maioria dos depuradores exibe o asm em uma janela separada, se houver.
Lundin

semaphoredefinitivamente deveria ser volatile! De fato, é o caso de uso mais básico que exige volatile: Sinalize algo de um contexto de execução para outro. - No seu exemplo, o compilador pode simplesmente omitir semaphore = true;porque 'vê' que seu valor nunca é lido antes de ser substituído por semaphore = false;.
JimmyB

5

Nos trechos de código apresentados na pergunta, ainda não há um motivo para usar o volátil. É irrelevante que o valor de adcValuevenha de um ADC. E adcValueser global deve fazer com que você desconfie se adcValuedeve ou não ser volátil, mas não é uma razão por si só.

Ser global é uma pista, pois abre a possibilidade de adcValueacesso a mais de um contexto de programa. Um contexto de programa inclui um manipulador de interrupções e uma tarefa RTOS. Se a variável global for alterada por um contexto, os outros contextos do programa não poderão assumir que sabem o valor de um acesso anterior. Cada contexto deve reler o valor da variável toda vez que o usar, porque o valor pode ter sido alterado em um contexto de programa diferente. Um contexto de programa não está ciente quando ocorre uma interrupção ou troca de tarefa, portanto, deve-se assumir que quaisquer variáveis ​​globais usadas por vários contextos podem mudar entre os acessos da variável devido a uma possível troca de contexto. É para isso que serve a declaração volátil. Ele diz ao compilador que essa variável pode mudar fora do seu contexto, portanto leia-a todos os acessos e não assuma que você já conhece o valor.

Se a variável é mapeada na memória para um endereço de hardware, as alterações feitas pelo hardware são efetivamente outro contexto fora do contexto do seu programa. Portanto, o mapeamento de memória também é uma pista. Por exemplo, se sua readADC()função acessa um valor mapeado na memória para obter o valor ADC, essa variável mapeada na memória provavelmente deve ser volátil.

Portanto, voltando à sua pergunta, se houver mais no seu código e adcValuefor acessado por outro código que é executado em um contexto diferente, então sim, adcValuedeve ser volátil.


4

"Variável global que muda diretamente do hardware"

Só porque o valor vem de algum registro ADC de hardware, não significa que ele é "diretamente" alterado por hardware.

No seu exemplo, você acabou de chamar readADC (), que retorna algum valor do registro ADC. Isso é bom em relação ao compilador, sabendo que o adcValue recebe um novo valor nesse momento.

Seria diferente se você estivesse usando uma rotina de interrupção ADC para atribuir o novo valor, que é chamado quando um novo valor ADC está pronto. Nesse caso, o compilador não tem idéia de quando o ISR correspondente é chamado e pode decidir que o adcValue não será acessado dessa maneira. É aqui que o volátil ajudaria.


1
Como seu código nunca "chama" a função ISR, o Compiler vê que a variável é atualizada apenas em uma função que ninguém chama. Então, o compilador otimiza.
Swanand

1
Depende do restante do código, se o adcValue não estiver sendo lido em nenhum lugar (como apenas o lance do depurador) ou se for lido apenas uma vez em um só lugar, o compilador provavelmente o otimizará.
Damien

2
@ Damien: Sempre "depende", mas eu estava tentando resolver a questão real "Devo usar a palavra-chave volátil neste caso?" o mais curto possível.
Rev1.0

4

O comportamento do volatileargumento depende muito do seu código, do compilador e da otimização realizada.

Existem dois casos de uso em que eu pessoalmente uso volatile:

  • Se houver uma variável que eu queira examinar com o depurador, mas o compilador a otimizou (significa que a excluiu porque descobriu que não é necessário ter essa variável), a adição volatileforçará o compilador a mantê-la e, portanto, pode ser visto na depuração.

  • Se a variável puder mudar "fora do código", normalmente se você tiver algum hardware acessando-a ou se você mapear a variável diretamente para um endereço.

No incorporado também, às vezes, existem alguns bugs nos compiladores, fazendo otimizações que realmente não funcionam e às vezes volatilepodem resolver os problemas.

Como sua variável é declarada globalmente, provavelmente não será otimizada, desde que a variável esteja sendo usada no código, pelo menos escrita e lida.

Exemplo:

void test()
{
    int a = 1;
    printf("%i", a);
}

Nesse caso, a variável provavelmente será otimizada para printf ("% i", 1);

void test()
{
    volatile int a = 1;
    printf("%i", a);
}

não será otimizado

Outro:

void delay1Ms()
{
    unsigned int i;
    for (i=0; i<10; i++)
    {
        delay10us( 10);
    }
}

Nesse caso, o compilador pode otimizar (se você otimizar a velocidade) e, assim, descartar a variável

void delay1Ms()
{
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
}

Para o seu caso de uso, "pode ​​depender" do restante do código, de como adcValueestá sendo usado em outro lugar e das configurações de versão / otimização do compilador usadas.

Às vezes, pode ser irritante ter um código que funcione sem otimização, mas quebre uma vez otimizado.

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

Isso pode ser otimizado para printf ("% i", readADC ());

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
  callAnotherFunction(adcValue);
}

-

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

void anotherFunction()
{
   // Do something with adcValue
}

Eles provavelmente não serão otimizados, mas você nunca sabe "quão bom é o compilador" e pode mudar com os parâmetros do compilador. Geralmente, compiladores com boa otimização são licenciados.


1
Por exemplo a = 1; b = a; e c = b; o compilador pode pensar que espere um minuto, aeb são inúteis, vamos colocar 1 a c diretamente. É claro que você não fará isso no seu código, mas o compilador é melhor do que você em encontrá-los, também se você tentar escrever um código otimizado imediatamente, seria ilegível.
Damien

2
Um código correto com um compilador correto não será interrompido com as otimizações ativadas. A correção do compilador é um pouco problemática, mas pelo menos no IAR, não encontrei uma situação em que a otimização levasse à quebra de código onde não deveria.
Arsenal

5
Um monte de casos onde as quebras de otimização do código é quando você está se aventurar em UB território também ..
tubo de

2
Sim, um efeito colateral do volátil é que ele pode ajudar na depuração. Mas essa não é uma boa razão para usar volátil. Provavelmente, você deve desativar as otimizações se o objetivo é a depuração fácil. Essa resposta nem menciona interrupções.
Kkrambo #

2
A adição ao argumento de depuração volatileforça o compilador a armazenar uma variável na RAM e a atualizá-la assim que um valor é atribuído à variável. Na maioria das vezes, o compilador não 'exclui' variáveis, porque geralmente não escrevemos atribuições sem efeito, mas ele pode decidir manter a variável em algum registro da CPU e mais tarde ou nunca gravar o valor desse registro na RAM. Os depuradores frequentemente falham ao localizar o registro da CPU no qual a variável é mantida e, portanto, não podem mostrar seu valor.
JimmyB

1

Muitas explicações técnicas, mas quero me concentrar na aplicação prática.

A volatilepalavra-chave força o compilador a ler ou gravar o valor da variável na memória toda vez que é usado. Normalmente, o compilador tentará otimizar, mas não fará leituras e gravações desnecessárias, por exemplo, mantendo o valor em um registro da CPU em vez de acessar a memória todas as vezes.

Isso tem dois usos principais no código incorporado. Primeiramente, é usado para registros de hardware. Os registros de hardware podem mudar, por exemplo, um registro de resultados do ADC pode ser gravado pelo periférico do ADC. Os registros de hardware também podem executar ações quando acessados. Um exemplo comum é o registro de dados de um UART, que geralmente limpa os sinalizadores de interrupção quando lidos.

O compilador normalmente tentaria otimizar leituras e gravações repetidas do registro, pressupondo que o valor nunca mudaria, portanto não há necessidade de continuar acessando, mas o volatile palavra chave o forçará a executar uma operação de leitura todas as vezes.

O segundo uso comum é para variáveis ​​usadas pelo código de interrupção e sem interrupção. As interrupções não são chamadas diretamente, portanto, o compilador não pode determinar quando elas serão executadas e, portanto, pressupõe que quaisquer acessos dentro da interrupção nunca ocorram. Como a volatilepalavra - chave força o compilador a acessar a variável toda vez, essa suposição é removida.

É importante observar que a volatilepalavra-chave não é a solução completa para esses problemas, e é necessário ter cuidado para evitá-los. Por exemplo, em um sistema de 8 bits, uma variável de 16 bits requer dois acessos à memória para ler ou gravar e, portanto, mesmo que o compilador seja forçado a fazer esses acessos, eles ocorrem sequencialmente e é possível que o hardware atue no primeiro acesso ou uma interrupção para ocorrer entre os dois.


0

Na ausência de um volatilequalificador, o valor de um objeto pode ser armazenado em mais de um local durante determinadas partes do código. Considere, por exemplo, algo como:

int foo;
int someArray[64];
void test(void)
{
  int i;
  foo = 0;
  for (i=0; i<64; i++)
    if (someArray[i] > 0)
      foo++;
}

Nos primeiros dias de C, um compilador teria processado a instrução

foo++;

através dos passos:

load foo into a register
increment that register
store that register back to foo

Compiladores mais sofisticados, no entanto, reconhecerão que, se o valor de "foo" for mantido em um registro durante o loop, ele precisará ser carregado apenas uma vez antes do loop e armazenado uma vez depois. Durante o loop, no entanto, isso significa que o valor de "foo" está sendo mantido em dois lugares - no armazenamento global e no registro. Isso não será um problema se o compilador puder ver todas as maneiras pelas quais "foo" pode ser acessado dentro do loop, mas poderá causar problemas se o valor de "foo" for acessado em algum mecanismo que o compilador não conhece ( como um manipulador de interrupção).

Pode ter sido possível para os autores do Padrão adicionar um novo qualificador que convidaria explicitamente o compilador a fazer essas otimizações e dizer que a semântica antiquada se aplicaria na sua ausência, mas os casos em que as otimizações são úteis superam em número naquelas em que seria problemático, então o Padrão permite que os compiladores suponham que essas otimizações são seguras na ausência de evidências de que não são. O objetivo dovolatile palavra-chave é fornecer essas evidências.

Alguns pontos de discórdia entre alguns escritores de compiladores e programadores ocorrem em situações como:

unsigned short volatile *volatile output_ptr;
unsigned volatile output_count;

void interrupt_handler(void)
{
  if (output_count)
  {
    *((unsigned short*)0xC0001230) = *output_ptr; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 1; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 0; // Hardware I/O register
    output_ptr++;
    output_count--;
  }
}

void output_data_via_interrupt(unsigned short *dat, unsigned count)
{
  output_ptr = dat;
  output_count = count;
  while(output_count)
     ; // Wait for interrupt to output the data
}

unsigned short output_buffer[10];

void test(void)
{
  output_buffer[0] = 0x1234;
  output_data_via_interrupt(output_buffer, 1);
  output_buffer[0] = 0x2345;
  output_buffer[1] = 0x6789;
  output_data_via_interrupt(output_buffer,2);
}

Historicamente, a maioria dos compiladores permitiria a possibilidade de gravar um volatilelocal de armazenamento causar efeitos colaterais arbitrários e evitar o armazenamento em cache de quaisquer valores nos registros em uma loja como essa, ou então eles se absterão de armazenar valores em cache nos registros nas chamadas para funções que são não qualificado "inline" e, portanto, gravaria 0x1234 em output_buffer[0], configuraria as coisas para gerar os dados, espere a conclusão, escreva 0x2345 em output_buffer[0]e continue a partir daí. O padrão não exige implementações para tratar o ato de armazenar o endereço output_bufferem umvolatile- ponteiro qualificado como um sinal de que algo pode acontecer com ele significa que o compilador não entende, no entanto, porque os autores pensaram que os escritores de compiladores destinados a várias plataformas e propósitos reconheceriam que, ao fazê-lo, serviriam a esses propósitos nessas plataformas sem ter que ser informado. Conseqüentemente, alguns compiladores "inteligentes" como gcc e clang assumem que, embora o endereço de output_bufferseja gravado em um ponteiro qualificado e volátil entre as duas lojas paraoutput_buffer[0] , isso não é motivo para supor que algo possa se importar com o valor contido nesse objeto em esse tempo.

Além disso, enquanto ponteiros que são diretamente convertidos a partir de números inteiros raramente são usados ​​para qualquer outro propósito que não seja manipular coisas de maneiras que os compiladores provavelmente não entendem, o Padrão novamente não exige que os compiladores tratem esses acessos como volatile. Conseqüentemente, a primeira gravação a *((unsigned short*)0xC0001234)pode ser omitida por compiladores "inteligentes" como gcc e clang, porque os mantenedores de tais compiladores preferem reivindicar o código que negligencia qualificar coisas como volatile"quebradas" do que reconhecer que a compatibilidade com esse código é útil . Muitos arquivos de cabeçalho fornecidos pelo fornecedor omitem os volatilequalificadores, e um compilador compatível com os arquivos de cabeçalho fornecidos pelo fornecedor é mais útil do que um que não é.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.