Eu uso a função de interrupção para preencher uma matriz com valores recebidos de digitalRead()
.
void setup() {
Serial.begin(115200);
attachInterrupt(0, test_func, CHANGE);
}
void test_func(){
if(digitalRead(pin)==HIGH){
test_array[x]=1;
} else if(digitalRead(pin)==LOW){
test_array[x]=0;
}
x=x+1;
}
Esse problema é que, quando imprimo, test_array
existem valores como: 111
ou 000
.
Pelo que entendi, se eu usar a CHANGE
opção na attachInterrupt()
função, a sequência de dados sempre deve ser 0101010101
sem repetição.
Os dados mudam bastante rapidamente, pois vêm de um módulo de rádio.
arduino-uno
c
isr
user277820
fonte
fonte
pin
,x
etest_array
definição, e também oloop()
método; nos permitiria ver se isso pode ser um problema de simultaneidade ao acessar variáveis modificadas portest_func
.if (digitalRead(pin) == HIGH) ... else ...;
ou, ainda melhor, isso com uma única linha ISR:test_array[x++] = digitalRead(pin);
.Respostas:
Como uma espécie de prólogo para esta resposta excessivamente longa ...
Essa questão me deixou profundamente cativado com o problema da latência de interrupção, a ponto de perder o sono na contagem de ciclos em vez de ovelhas. Estou escrevendo esta resposta mais para compartilhar minhas descobertas do que apenas para responder à pergunta: a maior parte desse material pode não estar em um nível adequado para uma resposta adequada. Espero que seja útil, no entanto, para leitores que chegam aqui em busca de soluções para problemas de latência. Espera-se que as primeiras seções sejam úteis para uma ampla audiência, incluindo o pôster original. Então, fica peludo ao longo do caminho.
Clayton Mills já explicou em sua resposta que existe alguma latência em responder a interrupções. Aqui, focarei na quantificação da latência (que é enorme ao usar as bibliotecas do Arduino) e nos meios para minimizá-la. A maior parte do que segue é específica para o hardware do Arduino Uno e placas similares.
Minimizando a latência de interrupção no Arduino
(ou como passar de 99 para 5 ciclos)
Usarei a pergunta original como um exemplo de trabalho e reiterarei o problema em termos de latência de interrupção. Temos algum evento externo que aciona uma interrupção (aqui: INT0 na troca de pinos). Precisamos tomar alguma ação quando a interrupção é acionada (aqui: leia uma entrada digital). O problema é: existe algum atraso entre a interrupção que está sendo acionada e a tomada da ação apropriada. Chamamos esse atraso de " latência de interrupção ". Uma longa latência é prejudicial em muitas situações. Neste exemplo em particular, o sinal de entrada pode mudar durante o atraso; nesse caso, obtemos uma leitura defeituosa. Não há nada que possamos fazer para evitar o atraso: é intrínseco à maneira como interrompe o trabalho. Podemos, no entanto, tentar torná-lo o mais curto possível, o que deve minimizar as más conseqüências.
A primeira coisa óbvia que podemos fazer é executar a ação com tempo crítico, dentro do manipulador de interrupções, o mais rápido possível. Isso significa chamar
digitalRead()
uma vez (e apenas uma vez) no início do manipulador. Aqui está a versão zero do programa sobre o qual iremos construir:Testei esse programa e as versões subsequentes enviando trens de pulsos de larguras variadas. Há espaçamento suficiente entre os pulsos para garantir que nenhuma borda seja perdida: mesmo que a borda descendente seja recebida antes da interrupção anterior, a segunda solicitação de interrupção será colocada em espera e, eventualmente, atendida. Se um pulso é menor que a latência de interrupção, o programa lê 0 em ambas as extremidades. O número relatado de níveis ALTOS é a porcentagem de pulsos lidos corretamente.
O que acontece quando a interrupção é acionada?
Antes de tentar melhorar o código acima, veremos os eventos que se desenrolam logo após a interrupção ser acionada. A parte de hardware da história é contada pela documentação da Atmel. A parte do software, desmontando o binário.
Na maioria das vezes, a interrupção de entrada é atendida imediatamente. Pode acontecer, no entanto, que o MCU (que significa "microcontrolador") esteja no meio de uma tarefa de tempo crítico, em que a interrupção da manutenção está desabilitada. Normalmente, esse é o caso quando ele já está atendendo a outra interrupção. Quando isso acontece, a solicitação de interrupção de entrada é colocada em espera e atendida somente quando a seção de tempo crítico é concluída. É difícil evitar completamente essa situação, porque existem algumas dessas seções críticas na biblioteca principal do Arduino (que chamarei de " libcore"a seguir). Felizmente, essas seções são curtas e executam apenas de vez em quando. Assim, na maioria das vezes, nossa solicitação de interrupção será atendida imediatamente. A seguir, assumirei que não nos importamos com esses poucos casos em que não é esse o caso.
Em seguida, nosso pedido é atendido imediatamente. Isso ainda envolve muitas coisas que podem demorar um pouco. Primeiro, há uma sequência conectada. O MCU terminará de executar a instrução atual. Felizmente, a maioria das instruções é de ciclo único, mas algumas podem levar até quatro ciclos. Em seguida, o MCU limpa um sinalizador interno que desabilita a manutenção adicional de interrupções. Isso tem como objetivo evitar interrupções aninhadas. Em seguida, o PC é salvo na pilha. A pilha é uma área de RAM reservada para esse tipo de armazenamento temporário. O PC (que significa " Contador de programas") é um registro interno que contém o endereço da próxima instrução que o MCU está prestes a executar. É isso que permite ao MCU saber o que fazer em seguida, e salvá-lo é essencial, pois precisará ser restaurado para que os principais para reiniciar a partir de onde foi interrompido O PC é carregado com um endereço fixo específico para a solicitação recebida, e este é o fim da sequência conectada, o restante sendo controlado por software.
O MCU agora executa a instrução a partir desse endereço conectado. Essa instrução é chamada de " vetor de interrupção " e geralmente é uma instrução de "salto" que nos levará a uma rotina especial chamada ISR (" Interrupt Service Routine "). Nesse caso, o ISR é chamado "__vector_1", também conhecido como "INT0_vect", que é um nome impróprio porque é um ISR, não um vetor. Este ISR em particular vem da libcore. Como qualquer ISR, ele começa com um prólogo que salva vários registros internos da CPU na pilha. Isso permitirá que ele use esses registradores e, quando terminar, restaure-os com seus valores anteriores para não atrapalhar o programa principal. Em seguida, ele procurará o manipulador de interrupção que foi registrado com
attachInterrupt()
, e chamará esse manipulador, que é a nossaread_pin()
função acima. Nossa função irá chamardigitalRead()
da libcore.digitalRead()
examinará algumas tabelas para mapear o número da porta do Arduino para a porta de E / S de hardware que ele precisa ler e o número de bit associado a ser testado. Ele também verificará se há um canal PWM nesse pino que precisaria ser desativado. Ele lerá a porta de E / S ... e pronto. Bem, ainda não terminamos o serviço da interrupção, mas a tarefa de tempo crítico (leitura da porta de E / S) está concluída e é tudo o que importa quando estamos analisando a latência.Aqui está um breve resumo de todas as opções acima, juntamente com os atrasos associados nos ciclos da CPU:
Vamos assumir o melhor cenário, com 4 ciclos para a sequência conectada. Isso nos dá uma latência total de 99 ciclos, ou cerca de 6,2 µs com um relógio de 16 MHz. A seguir, explorarei alguns truques que podem ser usados para diminuir essa latência. Eles vêm aproximadamente em uma ordem crescente de complexidade, mas todos precisam que de alguma forma investigemos os aspectos internos do MCU.
Usar acesso direto à porta
O primeiro objetivo óbvio para encurtar a latência é
digitalRead()
. Essa função fornece uma boa abstração para o hardware do MCU, mas é muito ineficiente para trabalhos críticos. Livrar-se deste é realmente trivial: basta substituí-lodigitalReadFast()
pela biblioteca digitalwritefast . Isso reduz a latência quase pela metade, ao custo de um pequeno download!Bem, isso foi fácil demais para ser divertido, vou mostrar como fazer da maneira mais difícil. O objetivo é começar com coisas de baixo nível. O método é chamado " acesso direto à porta " e está bem documentado na referência do Arduino na página de Registros de portas . Nesse momento, é uma boa idéia fazer o download e dar uma olhada na folha de dados do ATmega328P . Este documento de 650 páginas pode parecer um pouco assustador à primeira vista. No entanto, está bem organizado em seções específicas para cada um dos periféricos e recursos do MCU. E precisamos apenas verificar as seções relevantes para o que estamos fazendo. Neste caso, é a seção chamada portas de E / S . Aqui está um resumo do que aprendemos com essas leituras:
1 << 2
.Então, aqui está o nosso manipulador de interrupção modificado:
Agora, nosso manipulador lerá o registro de E / S assim que for chamado. A latência é de 53 ciclos da CPU. Esse truque simples nos salvou 46 ciclos!
Escreva seu próprio ISR
O próximo alvo para o corte de ciclo é o IST INT0_vect. Este ISR é necessário para fornecer a funcionalidade de
attachInterrupt()
: podemos alterar manipuladores de interrupção a qualquer momento durante a execução do programa. No entanto, apesar de agradável, isso não é realmente útil para o nosso propósito. Portanto, em vez de o ISR da libcore localizar e chamar nosso manipulador de interrupções, economizaremos alguns ciclos substituindo o ISR pelo nosso manipulador.Isso não é tão difícil quanto parece. Os ISRs podem ser escritos como funções normais, apenas precisamos estar cientes de seus nomes específicos e defini-los usando uma
ISR()
macro especial do avr-libc. Nesse momento, seria bom dar uma olhada na documentação do avr-libc sobre interrupções e na seção de folha de dados chamada External Interrupts . Aqui está o breve resumo:setup()
.setup()
.ISR(INT0_vect) { ... }
.Aqui está o código para o ISR e
setup()
, todo o resto é inalterado:Isso vem com um bônus gratuito: como esse ISR é mais simples do que o que substitui, ele precisa de menos registros para fazer seu trabalho, então o prólogo para salvar registros é mais curto. Agora estamos com uma latência de 20 ciclos. Nada mal, considerando que começamos perto de 100!
Neste ponto, eu diria que terminamos. Missão cumprida. O que se segue é apenas para aqueles que não têm medo de sujar as mãos com alguma montagem do AVR. Caso contrário, você pode parar de ler aqui e obrigado por chegar tão longe.
Escreva um ISR nu
Ainda aqui? Boa! Para continuar, seria útil ter pelo menos uma idéia muito básica de como a montagem funciona e dar uma olhada no Inline Assembler Cookbook na documentação do avr-libc. Nesse ponto, nossa sequência de entrada de interrupção se parece com isso:
Se queremos fazer melhor, temos que mover a leitura da porta para o prólogo. A idéia é a seguinte: a leitura do registro PIND sobrecarregará um registro da CPU; portanto, precisamos salvar pelo menos um registro antes de fazer isso, mas os outros registradores podem esperar. Em seguida, precisamos escrever um prólogo personalizado que leia a porta de E / S logo após salvar o primeiro registro. Você já viu na documentação do avr-libc interrupt (você leu, certo?) Que um ISR pode ser descoberto ; nesse caso, o compilador não emitirá prólogo ou epílogo, permitindo que escrevamos nossa própria versão personalizada.
O problema com essa abordagem é que provavelmente acabaremos escrevendo todo o ISR na montagem. Não é grande coisa, mas prefiro que o compilador escreva esses prólogos e epílogos chatos para mim. Então, aqui está o truque sujo: dividiremos o ISR em duas partes:
Nosso INT0 ISR anterior é então substituído por este:
Aqui, estamos usando a macro ISR () para ter o instrumento do compilador
INT0_vect_part_2
com o prólogo e o epílogo necessários. O compilador reclamará que "'INT0_vect_part_2' parece ser um manipulador de sinais com erros ortográficos", mas o aviso pode ser ignorado com segurança. Agora, o ISR possui uma única instrução de 2 ciclos antes da leitura da porta real e a latência total é de apenas 10 ciclos.Use o registro GPIOR0
E se pudéssemos ter um registro reservado para este trabalho específico? Então, não precisaríamos salvar nada antes de ler a porta. Podemos realmente pedir ao compilador para vincular uma variável global a um registro . Isso, no entanto, exigiria que recompilássemos todo o núcleo do Arduino e a libc para garantir que o registro esteja sempre reservado. Não é realmente conveniente. Por outro lado, o ATmega328P possui três registros que não são usados pelo compilador nem por nenhuma biblioteca e estão disponíveis para armazenar o que quisermos. Eles são chamados GPIOR0, GPIOR1 e GPIOR2 ( Registros de E / S de uso geral ). Embora estejam mapeados no espaço de endereço de E / S do MCU, na verdade eles não sãoRegistradores de E / S: são apenas memória simples, como três bytes de RAM que de alguma forma se perderam em um barramento e acabaram no espaço de endereço errado. Eles não são tão capazes quanto os registros internos da CPU e não podemos copiar o PIND em um deles com a
in
instrução O GPIOR0 é interessante, porém, por ser endereçável por bits , assim como o PIND. Isso nos permitirá transferir as informações sem atrapalhar nenhum registro interno da CPU.Aqui está o truque: vamos ter certeza de que GPIOR0 é inicialmente zero (que é, na verdade, foi afastada pelo hardware no momento da inicialização), então vamos usar o
sbic
(Ir próxima instrução se algum Bit em alguns I / O registo é claro) eosbi
( Defina como 1 bit em algum registro de E / S) da seguinte maneira:Dessa forma, o GPIOR0 acabará sendo 0 ou 1, dependendo do bit que desejamos ler do PIND. A instrução sbic leva 1 ou 2 ciclos para executar, dependendo se a condição é falsa ou verdadeira. Obviamente, o bit PIND é acessado no primeiro ciclo. Nesta nova versão do código, a variável global
sampled_pin
não é mais útil, pois é basicamente substituída pelo GPIOR0:Deve-se notar que o GPIOR0 sempre deve ser redefinido no ISR.
Agora, a amostragem do registro PIND I / O é a primeira coisa feita dentro do ISR. A latência total é de 8 ciclos. Isso é o melhor que podemos fazer antes de ficar manchado com brigas terrivelmente pecaminosas. Esta é novamente uma boa oportunidade para parar de ler ...
Coloque o código crítico de tempo na tabela de vetores
Para aqueles que ainda estão aqui, aqui está nossa situação atual:
Obviamente, há pouco espaço para melhorias. A única maneira de diminuir a latência neste momento é substituindo o próprio vetor de interrupção pelo nosso código. Esteja avisado de que isso deve ser imensamente desagradável para quem valoriza o design de software limpo. Mas é possível, e eu mostrarei como.
O layout da tabela vetorial ATmega328P pode ser encontrado na folha de dados, seção Interrupções , subseção Vetores de interrupção no ATmega328 e ATmega328P . Ou desmontando qualquer programa para esse chip. Aqui está como ele se parece. Estou usando as convenções avr-gcc e avr-libc (__init é o vetor 0, os endereços estão em bytes) que são diferentes das do Atmel.
Cada vetor possui um slot de 4 bytes, preenchido com uma única
jmp
instrução. Esta é uma instrução de 32 bits, diferente da maioria das instruções AVR de 16 bits. Mas um slot de 32 bits é pequeno demais para conter a primeira parte do nosso ISR: podemos ajustar as instruçõessbic
esbi
, mas não asrjmp
. Se fizermos isso, a tabela de vetores acabará assim:Quando INT0 é acionado, PIND será lido, o bit relevante será copiado para GPIOR0 e, em seguida, a execução passará para o próximo vetor. Então, o ISR para INT1 será chamado, em vez do ISR para INT0. Isso é assustador, mas como não estamos usando o INT1 de qualquer maneira, apenas "sequestramos" seu vetor para atender o INT0.
Agora, basta escrever nossa própria tabela de vetores personalizada para substituir a tabela padrão. Acontece que não é tão fácil. A tabela vetorial padrão é fornecida pela distribuição avr-libc, em um arquivo de objeto chamado crtm328p.o que é automaticamente vinculado a qualquer programa que criamos. Diferentemente do código da biblioteca, o código do arquivo de objeto não deve ser substituído: tentar fazer isso resultará em um erro de vinculador sobre a definição da tabela duas vezes. Isso significa que precisamos substituir o crtm328p.o inteiro por nossa versão personalizada. Uma opção é fazer o download do código-fonte completo do avr-libc , fazer nossas modificações personalizadas no gcrt1.S e criar isso como uma libc personalizada.
Aqui eu fui para uma abordagem alternativa mais leve. Eu escrevi um crt.S personalizado, que é uma versão simplificada do original do avr-libc. Faltam alguns recursos raramente usados, como a capacidade de definir um ISR "catch all", ou de poder finalizar o programa (por exemplo, congelar o Arduino) ligando
exit()
. Aqui está o código. Cortei a parte repetitiva da tabela de vetores para minimizar a rolagem:Pode ser compilado com a seguinte linha de comando:
O esboço é idêntico ao anterior, exceto que não há INT0_vect e INT0_vect_part_2 é substituído por INT1_vect:
Para compilar o esboço, precisamos de um comando de compilação personalizado. Se você seguiu até agora, provavelmente sabe como compilar a partir da linha de comando. Você precisa solicitar explicitamente o silly-crt.o para ser vinculado ao seu programa e adicionar a
-nostartfiles
opção para evitar a vinculação no crtm328p.o original.Agora, a leitura da porta de E / S é a primeira instrução executada após o acionamento da interrupção. Testei esta versão enviando pulsos curtos de outro Arduino e ele pode capturar (embora não seja confiável) o alto nível de pulsos tão curtos quanto 5 ciclos. Não há mais nada que possamos fazer para reduzir a latência de interrupção neste hardware.
fonte
A interrupção está sendo configurada para disparar em uma alteração, e seu test_func é definido como o ISR (Interrupt Service Routine), chamado para reparar essa interrupção. O ISR imprime o valor da entrada.
À primeira vista, você esperaria que a saída fosse como você disse, e um conjunto alternado de mínimos altos, pois só chega ao ISR em uma mudança.
Mas o que estamos perdendo é que há uma certa quantidade de tempo que leva para a CPU atender uma interrupção e ramificar para o ISR. Durante esse período, a tensão no pino pode ter mudado novamente. Especialmente se o pino não for estabilizado por quedas de hardware ou similares. Como a interrupção já está sinalizada e ainda não foi reparada, essa alteração extra (ou muitas delas, porque o nível de pinos pode mudar muito rapidamente em relação à velocidade do relógio se tiver baixa capacitância parasitária) será perdida.
Portanto, em essência, sem alguma forma de rejeição, não temos garantia de que, quando a entrada for alterada e a interrupção for marcada para manutenção, a entrada ainda terá o mesmo valor quando lermos seu valor no ISR.
Como exemplo genérico, a folha de dados ATmega328 usada no Arduino Uno detalha os tempos de interrupção na seção 6.7.1 - "Tempo de resposta a interrupção". Ele declara para este microcontrolador que o tempo mínimo para ramificar para um ISR para manutenção é de 4 ciclos de clock, mas pode ser mais (extra se a execução de instruções de vários ciclos na interrupção ou 8 + tempo de espera se o MCU estiver em suspensão).
Como o @ EdgarBonet mencionado nos comentários, o pino também pode mudar da mesma forma durante a execução do ISR. Como o ISR lê o pino duas vezes, não adicionaria nada ao test_array se encontrasse um BAIXO na primeira leitura e um ALTO no segundo. Mas x ainda aumentaria, deixando esse slot na matriz inalterado (possivelmente como dados não inicializados, dependendo do que foi feito anteriormente na matriz).
Seu ISR de uma linha
test_array[x++] = digitalRead(pin);
é uma solução perfeita para isso.fonte