Eu tenho lido alguns artigos e respostas do Stack Exchange sobre o uso da volatile
palavra-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 volatile
nesse caso?
Sem usar
volatile
palavra-chave// Includes #include "adcDriver.h" // Global variables uint16_t adcValue; // Some code void readFromADC(void) { adcValue = readADC(); }
Usando a
volatile
palavra - 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.
microcontroller
c
embedded
Pryda
fonte
fonte
if(x==1) x=1;
gravação pode ser otimizado para um não volátilx
e não pode ser otimizado sex
for 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).Respostas:
Uma definição de
volatile
volatile
informa 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 quandoEfeitos de
volatile
Quando uma variável é declarada,
volatile
o 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:
Aqui, o compilador provavelmente nem alocará RAM para a
result
variável e nunca armazenará os valores intermediários em nenhum lugar, exceto em um registro da CPU.Se
result
fosse volátil, todas as ocorrênciasresult
no 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:
poderia ser reordenado para
o que pode salvar uma instrução do assembler porque o valor
99
não precisará ser carregado duas vezes.Se
a
,b
ec
foram 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:
Se, nesse caso,
signal
não fossevolatile
, o compilador "pensaria" quewhile( signal == 0 )
pode ser um loop infinito (porquesignal
nunca será alterado pelo código dentro do loop ) e poderá gerar o equivalente aManuseio atencioso de
volatile
valoresConforme mencionado acima, uma
volatile
variá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, comoIsso 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
sysTickCount
no exemplo acima. Em um AVR, seria especialmente doloroso fazer com que a funçãodoSysTick()
acesse os mesmos quatro bytes na memória (quatro instruções = 8 ciclos de CPU por acesso asysTickCount
) 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 eledoSysTick()
é 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
volatile
se 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 usovolatile
. No AVR, você pode usarATOMIC_BLOCK
a partir<util/atomic.h>
ou simplescli(); ... 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
volatile
impõe ordem estrita de execução apenas com relação a outras variáveis voláteis. Isso significa que, por exemploé garantido atribuir primeiro 1 a
i
e depois atribuir 2 aj
. No entanto, é não garantiu quea
será 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
para
ou
(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.)fonte
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.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 reaiscli
/sei
adicionais 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).volatile
há 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".volatile data_t data = {0}; set_mmio(&data); while (!data.ready);
.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 areadFromADC()
função. Ou qualquer função que não conheça. Ou qualquer coisa que manipule ponteiros que possam apontar para algoadcValue
assim. Realmente não há necessidade de volatilidade, pois a variável nunca muda de maneira imprevisível.fonte
volatile
tudo 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.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.
fonte
Existem dois casos em que você deve usar
volatile
em 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):
onde 0x1234 é o endereço em que o MCU mapeou o registro. Como
volatile
já faz parte da macro acima, qualquer acesso a ela será qualificado como volátil. Portanto, este código está correto: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:
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.volatile
nã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
_Atomic
não esteja disponível):adc.h
adc.c
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
volatile
protege 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.static
protege 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.)fonte
semaphore
definitivamente deveria servolatile
! De fato, é o caso de uso mais básico que exigevolatile
: Sinalize algo de um contexto de execução para outro. - No seu exemplo, o compilador pode simplesmente omitirsemaphore = true;
porque 'vê' que seu valor nunca é lido antes de ser substituído porsemaphore = false;
.Nos trechos de código apresentados na pergunta, ainda não há um motivo para usar o volátil. É irrelevante que o valor de
adcValue
venha de um ADC. EadcValue
ser global deve fazer com que você desconfie seadcValue
deve ou não ser volátil, mas não é uma razão por si só.Ser global é uma pista, pois abre a possibilidade de
adcValue
acesso 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
adcValue
for acessado por outro código que é executado em um contexto diferente, então sim,adcValue
deve ser volátil.fonte
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.
fonte
O comportamento do
volatile
argumento 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
volatile
forç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
volatile
podem 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:
Nesse caso, a variável provavelmente será otimizada para printf ("% i", 1);
não será otimizado
Outro:
Nesse caso, o compilador pode otimizar (se você otimizar a velocidade) e, assim, descartar a variável
Para o seu caso de uso, "pode depender" do restante do código, de como
adcValue
está 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.
Isso pode ser otimizado para printf ("% i", readADC ());
-
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.
fonte
volatile
forç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.Muitas explicações técnicas, mas quero me concentrar na aplicação prática.
A
volatile
palavra-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
volatile
palavra - chave força o compilador a acessar a variável toda vez, essa suposição é removida.É importante observar que a
volatile
palavra-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.fonte
Na ausência de um
volatile
qualificador, o valor de um objeto pode ser armazenado em mais de um local durante determinadas partes do código. Considere, por exemplo, algo como:Nos primeiros dias de C, um compilador teria processado a instrução
através dos passos:
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 do
volatile
palavra-chave é fornecer essas evidências.Alguns pontos de discórdia entre alguns escritores de compiladores e programadores ocorrem em situações como:
Historicamente, a maioria dos compiladores permitiria a possibilidade de gravar um
volatile
local 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 emoutput_buffer[0]
, configuraria as coisas para gerar os dados, espere a conclusão, escreva 0x2345 emoutput_buffer[0]
e continue a partir daí. O padrão não exige implementações para tratar o ato de armazenar o endereçooutput_buffer
em 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 deoutput_buffer
seja 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 comovolatile
"quebradas" do que reconhecer que a compatibilidade com esse código é útil . Muitos arquivos de cabeçalho fornecidos pelo fornecedor omitem osvolatile
qualificadores, e um compilador compatível com os arquivos de cabeçalho fornecidos pelo fornecedor é mais útil do que um que não é.fonte