Evitando variáveis ​​globais ao usar interrupções em sistemas embarcados

13

Existe uma boa maneira de implementar a comunicação entre um ISR e o restante do programa para um sistema incorporado que evite variáveis ​​globais?

Parece que o padrão geral é ter uma variável global que seja compartilhada entre o ISR e o restante do programa e usada como sinalizador, mas esse uso de variáveis ​​globais vai contra mim. Incluí um exemplo simples usando ISRs do estilo avr-libc:

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

Não consigo desviar o que é essencialmente uma questão de escopo; Quaisquer variáveis ​​acessíveis pelo ISR e pelo restante do programa devem ser inerentemente globais, com certeza? Apesar disso, muitas vezes eu vi pessoas dizerem coisas como "variáveis ​​globais são uma maneira de implementar a comunicação entre ISRs e o resto do programa" (grifo meu), o que parece implicar que existem outros métodos; se existem outros métodos, quais são eles?


fonte
1
Não é necessariamente verdade que TODO o restante do programa teria acesso; se você declarasse a variável como estática, apenas o arquivo em que a variável foi declarada a veria. Não é difícil ter variáveis ​​visíveis em todo o arquivo, mas não no restante do programa e isso pode ajudar.
DiBosco 14/09
1
ao lado, o sinalizador deve ser declarado volátil, porque você o está usando / alterando fora do fluxo normal do programa. Isso força o compilador a não otimizar nenhum sinalizador de leitura / gravação em e executar a operação real de leitura / gravação.
next-hack
@ next-hack Sim, isso é absolutamente correto, desculpe, eu estava apenas tentando criar um exemplo rapidamente.

Respostas:

18

Existe uma maneira padrão de fato de fazer isso (assumindo a programação C):

  • Interrupções / ISRs são de baixo nível e, portanto, devem ser implementadas apenas dentro do driver relacionado ao hardware que gera a interrupção. Eles não devem estar localizados em nenhum outro lugar, mas dentro desse driver.
  • Toda a comunicação com o ISR é feita apenas pelo driver e pelo driver. Se outras partes do programa precisarem acessar essas informações, ele deverá solicitá-las ao driver por meio de funções de configuração / obtenção ou similares.
  • Você não deve declarar variáveis ​​"globais". Variáveis ​​de escopo do arquivo de significado global com ligação externa. Ou seja: variáveis ​​que poderiam ser chamadas com externpalavras - chave ou simplesmente por engano.
  • Em vez disso, para forçar o encapsulamento privado dentro do driver, todas essas variáveis ​​compartilhadas entre o driver e o ISR devem ser declaradas static. Essa variável não é global, mas restrita ao arquivo em que é declarada.
  • Para evitar problemas de otimização do compilador, essas variáveis ​​também devem ser declaradas como volatile. Nota: isso não fornece acesso atômico ou resolve a reentrada!
  • Geralmente, é necessário algum tipo de mecanismo de reentrada no driver, caso o ISR grave na variável. Exemplos: desativação de interrupção, máscara de interrupção global, semáforo / mutex ou leituras atômicas garantidas.
Lundin
fonte
Nota: pode ser necessário expor o protótipo da função ISR através de um cabeçalho, para colocá-lo em uma tabela vetorial localizada em outro arquivo. Mas isso não é um problema, desde que você documente que é uma interrupção e não deve ser chamado pelo programa.
Lundin
O que você diria, se o contra-argumento fosse o aumento da sobrecarga (e código extra) do uso das funções setter / obtenção? Eu já estive revisando isso, pensando nos padrões de código para nossos dispositivos embarcados de 8 bits.
precisa saber é o seguinte
2
@ Leroy105 A linguagem C suporta funções inline por uma eternidade até agora. Embora até o uso de inlineesteja se tornando obsoleto, os compiladores ficam cada vez mais inteligentes na otimização do código. Eu diria que se preocupar com a sobrecarga é "otimização antecipada" - na maioria dos casos, a sobrecarga não importa, se é que está presente no código da máquina.
Lundin
2
Dito isto, no caso de escrever drivers ISR, cerca de 80-90% de todos os programadores (não exagerando aqui) sempre entendem algo errado. O resultado são bugs sutis: sinalizações limpas incorretamente, otimização incorreta do compilador devido à falta de voláteis, condições de corrida, desempenho ruim em tempo real, estouros de pilha, etc. etc. aumentou ainda mais. Concentre-se em escrever um driver sem erros antes de se preocupar com coisas de interesse periférico, como se os setter / getters introduzirem um pouco de sobrecarga.
Lundin
10
esse uso de variáveis ​​globais vai contra mim para mim

Este é o verdadeiro problema. Deixe isso para trás.

Agora, antes que os cotovelos se queixem imediatamente de como isso é impuro, deixe-me qualificar um pouco. Certamente há perigo em usar variáveis ​​globais em excesso. Mas eles também podem aumentar a eficiência, o que às vezes é importante em pequenos sistemas com recursos limitados.

A chave é pensar em quando você pode usá-los razoavelmente e é improvável que se metam em problemas, em comparação com um bug que está esperando para acontecer. Sempre há trocas. Embora geralmente evitar variáveis ​​globais para a comunicação entre código de interrupção e primeiro plano seja uma diretriz undertandable, levá-lo, como a maioria das outras diretrizes, a um extremo religioso é contraproducente.

Alguns exemplos em que às vezes uso variáveis ​​globais para passar informações entre o código de interrupção e o primeiro plano são:

  1. Contadores de marcadores de relógio gerenciados pela interrupção do relógio do sistema. Normalmente, tenho uma interrupção periódica do relógio que é executada a cada 1 ms. Isso geralmente é útil para várias temporizações no sistema. Uma maneira de obter essas informações da rotina de interrupção para onde o resto do sistema pode usá-las é manter um contador de tempo global. A rotina de interrupção incrementa o contador a cada relógio. O código do primeiro plano pode ler o contador a qualquer momento. Frequentemente, faço isso por 10 ms, 100 ms e até 1 segundo.

    Garanto que os ticks de 1 ms, 10 ms e 100 ms tenham um tamanho de palavra que possa ser lido em uma única operação atômica. Se estiver usando um idioma de alto nível, informe ao compilador que essas variáveis ​​podem mudar de forma assíncrona. Em C, você as declara voláteis externamente , por exemplo. É claro que isso é algo incluído em um arquivo de inclusão enlatada, para que você não precise se lembrar disso em todos os projetos.

    Às vezes, faço com que o contador de 1 s marque o tempo total decorrido, de modo que tenha 32 bits de largura. Isso não pode ser lido em uma única operação atômica em muitos dos pequenos micro que eu uso, de modo que não é globalizado. Em vez disso, é fornecida uma rotina que lê o valor de várias palavras, lida com possíveis atualizações entre leituras e retorna o resultado.

    É claro que poderia haver rotinas para obter contadores de 1 ms, 10 ms, etc. menores também. No entanto, isso realmente faz muito pouco para você, adiciona muitas instruções no lugar da leitura de uma única palavra e usa outro local da pilha de chamadas.

    Qual é a desvantagem? Suponho que alguém possa cometer um erro de digitação que grave acidentalmente em um dos contadores, o que poderia atrapalhar outro momento no sistema. Escrever deliberadamente em um contador não faria sentido, portanto esse tipo de bug precisaria ser algo não intencional como um erro de digitação. Parece muito improvável. Não me lembro disso ter acontecido em mais de 100 pequenos projetos de microcontroladores.

  2. Valores finais de A / D filtrados e ajustados. Uma coisa comum a fazer é ter uma rotina de interrupção manipular as leituras de um A / D. Normalmente leio valores analógicos mais rápido que o necessário e aplico um pouco de filtragem passa-baixo. Muitas vezes, também há dimensionamento e deslocamento que são aplicados.

    Por exemplo, o A / D pode estar lendo a saída de 0 a 3 V de um divisor de tensão para medir a alimentação de 24 V. As muitas leituras são executadas através de alguma filtragem e, em seguida, redimensionadas para que o valor final seja em milivolts. Se o fornecimento estiver em 24,015 V, o valor final será 24015.

    O restante do sistema vê apenas um valor atualizado ao vivo, indicando a tensão de alimentação. Ele não sabe nem precisa se preocupar quando exatamente isso é atualizado, especialmente porque é atualizado com muito mais frequência do que o tempo de estabilização do filtro passa-baixo.

    Novamente, uma rotina de interface pode ser usada, mas você obtém muito pouco benefício disso. Apenas usar a variável global sempre que você precisar da tensão da fonte de alimentação é muito mais simples. Lembre-se de que a simplicidade não é apenas para a máquina, mas que mais simples também significa menos chances de erro humano.

Olin Lathrop
fonte
Eu tenho feito terapia, em uma semana lenta, realmente tentando escolher meu código. Entendo o ponto de vista de Lundin em restringir o acesso a variáveis, mas olho para os meus sistemas atuais e penso que é uma possibilidade tão remota. QUALQUER PESSOA poderia realmente empurrar uma variável global crítica do sistema. As funções Getter / Setter acabar custando-lhe sobrecarga contra apenas usando um global e aceitar estes são programas muito simples ...
Leroy105
3
@ Leroy105 O problema não é que "terroristas" abusem intencionalmente da variável global. A poluição do espaço para nome pode ser um problema em projetos maiores, mas isso pode ser resolvido com uma boa nomeação. Não, o verdadeiro problema é que o programador está tentando usar a variável global como pretendido, mas não consegue fazê-lo corretamente. Ou porque eles não percebem o problema de condição de corrida que existe com todos os ISRs, ou porque atrapalham a implementação do mecanismo de proteção obrigatório ou simplesmente porque expelem o uso da variável global em todo o código, criando acoplamentos rígidos e código ilegível.
Lundin
Seus pontos são válidos, Olin, mas mesmo nesses exemplos, substituir extern int ticks10mspor inline int getTicks10ms()não fará absolutamente nenhuma diferença na montagem compilada, enquanto, por outro lado, dificultará a alteração acidental de seu valor em outras partes do programa e também permitirá que você uma maneira de "ligar" a esta chamada (por exemplo, para zombar do tempo durante o teste de unidade, para registrar o acesso a essa variável ou qualquer outra coisa). Mesmo se você argumentar que a chance de um programador de san alterar essa variável para zero, não há custo de um getter em linha.
Groo
@Groo: Isso só é verdade se você estiver usando uma linguagem que suporte funções embutidas, e isso significa que a definição da função getter precisa ser visível para todos. Na verdade, ao usar uma linguagem de alto nível, uso funções getter mais e variáveis ​​globais menos. Na montagem, é muito mais fácil capturar o valor de uma variável global do que se preocupar com a função getter.
Olin Lathrop
Obviamente, se você não pode integrar, a escolha não é tão simples. Eu queria dizer que, com funções embutidas (e muitos compiladores anteriores ao C99 já suportavam extensões embutidas), o desempenho não pode ser um argumento contra os getters. Com um compilador de otimização razoável, você deve terminar com o mesmo assembly produzido.
Groo
2

Qualquer interrupção específica será um recurso global. Às vezes, no entanto, pode ser útil ter várias interrupções compartilhando o mesmo código. Por exemplo, um sistema pode ter vários UARTs, todos os quais devem usar lógica de envio / recebimento semelhante.

Uma boa abordagem para lidar com isso é colocar as coisas usadas pelo manipulador de interrupções ou ponteiros para eles em um objeto de estrutura e fazer com que os manipuladores de interrupção de hardware reais sejam algo como:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

Os objetos uart1_info, uart2_infoetc. seria variáveis globais, mas eles seriam os única variáveis globais utilizadas pelos manipuladores de interrupção. Tudo o mais que os manipuladores vão tocar seria tratado dentro deles.

Observe que qualquer coisa acessada pelo manipulador de interrupções e pelo código da linha principal deve ser qualificada volatile. Pode ser mais simples declarar como volatiletudo o que será usado pelo manipulador de interrupções, mas se o desempenho for importante, pode-se escrever código que copia informações para valores temporários, opera sobre eles e depois as grava novamente. Por exemplo, em vez de escrever:

if (foo->timer)
  foo->timer--;

escrever:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

A primeira abordagem pode ser mais fácil de ler e entender, mas será menos eficiente que a segunda. Se isso é uma preocupação, depende do aplicativo.

supercat
fonte
0

Aqui estão três idéias:

Declare a variável do sinalizador como estática para limitar o escopo a um único arquivo.

Torne a variável do sinalizador privada e use as funções getter e setter para acessar o valor do sinalizador.

Use um objeto de sinalização, como um semáforo, em vez de uma variável de flag. O ISR configuraria / postaria o semáforo.

kkrambo
fonte
0

Uma interrupção (ou seja, o vetor que aponta para o seu manipulador) é um recurso global. Portanto, mesmo se você usar alguma variável na pilha ou na pilha:

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

ou código orientado a objeto com uma função 'virtual':

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

… A primeira etapa deve envolver uma variável global real (ou pelo menos estática) para alcançar esses outros dados.

Todos esses mecanismos adicionam uma indireção, portanto isso geralmente não é feito se você deseja extrair o último ciclo do manipulador de interrupções.

CL.
fonte
você deve declarar sinalizador como int * volátil.
next-hack
0

Estou codificando para o Cortex M0 / M4 no momento e a abordagem que estamos usando no C ++ (não há tag C ++, portanto, essa resposta pode estar fora do tópico) é a seguinte:

Usamos uma classe CInterruptVectorTableque contém todas as rotinas de serviço de interrupção que são armazenadas no vetor de interrupção real do controlador:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

A classe CInterruptVectorTableimplementa uma abstração dos vetores de interrupção, para que você possa vincular diferentes funções aos vetores de interrupção durante o tempo de execução.

A interface dessa classe fica assim:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

Você precisa criar as funções que são armazenadas na tabela de vetores, staticporque o controlador não pode fornecer um thisponteiro, pois a tabela de vetores não é um objeto. Então, para contornar esse problema, temos o pThisponteiro estático dentro do CInterruptVectorTable. Ao inserir uma das funções de interrupção estática, ele pode acessar o pThisponteiro para obter acesso aos membros do único objeto de CInterruptVectorTable.


Agora no programa, você pode usar o SetIsrCallbackfunctionpara fornecer um ponteiro de função para uma staticfunção que deve ser chamada quando ocorrer uma interrupção. Os ponteiros são armazenados no InterruptVectorTable_t virtualVectorTable.

E a implementação de uma função de interrupção é assim:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

Portanto, isso chamará um staticmétodo de outra classe (que pode ser private), que poderá conter outro static this-pointer para obter acesso às variáveis-membro desse objeto (apenas uma).

Eu acho que você pode criar e interagir como IInterruptHandlere armazenar ponteiros para objetos, para não precisar do static thisponteiro em todas essas classes. (talvez tentemos isso na próxima iteração da nossa arquitetura)

A outra abordagem funciona bem para nós, já que os únicos objetos permitidos para implementar um manipulador de interrupções são aqueles dentro da camada de abstração de hardware, e geralmente temos apenas um objeto para cada bloco de hardware, portanto, é bom trabalhar com static this-pointers. E a camada de abstração de hardware fornece outra abstração para interrupções, chamada ICallbackque é então implementada na camada de dispositivo acima do hardware.


Você acessa dados globais? Claro que sim, mas você pode thisprivar a maioria dos dados globais necessários, como as funções -pointers e as interrupções.

Não é à prova de balas, e acrescenta sobrecarga. Você lutará para implementar uma pilha IO-Link usando essa abordagem. Mas se você não é extremamente rigoroso com os tempos, isso funciona muito bem para obter uma abstração flexível de interrupções e comunicação em módulos sem usar variáveis ​​globais acessíveis em qualquer lugar.

Arsenal
fonte
1
"para que você possa vincular funções diferentes aos vetores de interrupção durante o tempo de execução" Isso parece uma má idéia. A "complexidade ciclomática" do programa passaria pelo teto. Todas as combinações de casos de uso precisariam ser testadas para que não haja conflitos de uso de tempo e pilha. Muita dor de cabeça por um recurso com IMO de utilidade muito limitada. (A menos que você tenha um caso de carregador de inicialização, isso é outra história) No geral, isso cheira a meta programação.
Lundin
@Lundin Eu realmente não entendo o seu ponto. Nós o usamos para vincular, por exemplo, a interrupção do DMA ao manipulador de interrupções SPI, se o DMA estiver em uso para o SPI e ao manipulador de interrupções UART, se estiver em uso para o UART. Ambos os manipuladores precisam ser testados, com certeza, mas não são um problema. E certamente não tem nada a ver com meta-programação.
Arsenal
DMA é uma coisa, a atribuição em tempo de execução de vetores de interrupção é outra coisa completamente diferente. Faz sentido permitir que uma configuração de driver DMA seja variável, em tempo de execução. Uma tabela de vetores, nem tanto.
Lundin
@Lundin Acho que temos opiniões diferentes sobre isso, poderíamos iniciar um bate-papo sobre isso, porque ainda não vejo seu problema com isso - então pode ser que minha resposta seja tão mal escrita, que todo o conceito seja mal compreendido.
Arsenal