Interrupção do Arduino (na troca de pinos)

8

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_arrayexistem valores como: 111ou 000.

Pelo que entendi, se eu usar a CHANGEopção na attachInterrupt()função, a sequência de dados sempre deve ser 0101010101sem repetição.

Os dados mudam bastante rapidamente, pois vêm de um módulo de rádio.

user277820
fonte
11
Interrupções não rebatem o botão. Você está usando a depuração de hardware?
Ignacio Vazquez-Abrams
Por favor, poste o código completo, incluindo pin, xe test_arraydefinição, e também o loop()método; nos permitiria ver se isso pode ser um problema de simultaneidade ao acessar variáveis ​​modificadas por test_func.
jfpoilpret
2
Você não deve digitalRead () duas vezes no ISR: pense no que aconteceria se você obtivesse BAIXO na primeira chamada e ALTO na segunda. Em vez disso, if (digitalRead(pin) == HIGH) ... else ...;ou, ainda melhor, isso com uma única linha ISR: test_array[x++] = digitalRead(pin);.
Edgar Bonet
@EdgarBonet nice! Marcar com +1 esse comentário. Espero que você não se importe. Adicionei algo à minha resposta para incluir o que você mencionou aqui. Além disso, se você decidir colocar sua própria resposta, incluindo esse detalhe, removerei minha adição e darei um voto positivo para que você obtenha o representante.
Clayton Mills
@ Clayton Mills: Estou preparando uma resposta (excessivamente longa e marginalmente tangencial), mas você pode manter sua edição, está perfeitamente bem comigo.
Edgar Bonet

Respostas:

21

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:

#define INT_NUMBER 0
#define PIN_NUMBER 2    // interrupt 0 is on pin 2
#define MAX_COUNT  200

volatile uint8_t count_edges;  // count of signal edges
volatile uint8_t count_high;   // count of high levels

/* Interrupt handler. */
void read_pin()
{
    int pin_state = digitalRead(PIN_NUMBER);  // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (pin_state == HIGH) count_high++;
}

void setup()
{
    Serial.begin(9600);
    attachInterrupt(INT_NUMBER, read_pin, CHANGE);
}

void loop()
{
    /* Wait for the interrupt handler to count MAX_COUNT edges. */
    while (count_edges < MAX_COUNT) { /* wait */ }

    /* Report result. */
    Serial.print("Counted ");
    Serial.print(count_high);
    Serial.print(" HIGH levels for ");
    Serial.print(count_edges);
    Serial.println(" edges");

    /* Count again. */
    count_high = 0;
    count_edges = 0;  // do this last to avoid race condition
}

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 comattachInterrupt(), e chamará esse manipulador, que é a nossa read_pin()função acima. Nossa função irá chamar digitalRead()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:

  1. sequência com fio: termine a instrução atual, impeça interrupções aninhadas, economize PC, carregue o endereço do vetor (≥ 4 ciclos)
  2. executar vetor de interrupção: pule para ISR (3 ciclos)
  3. Prólogo ISR: salvar registros (32 ciclos)
  4. Corpo principal do ISR: localize e chame a função registrada pelo usuário (13 ciclos)
  5. read_pin: chame digitalRead (5 ciclos)
  6. digitalRead: encontre a porta relevante e o bit a testar (41 ciclos)
  7. digitalRead: leia a porta de E / S (1 ciclo)

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í-lo digitalReadFast()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:

  • O pino 2 do Arduino é chamado de PD2 (porta D, bit 2) no chip AVR.
  • Obtemos toda a porta D de uma só vez, lendo um registro especial do MCU chamado "PIND".
  • Em seguida, verificamos o bit número 2, fazendo uma lógica bit a bit e (o operador C '&') com 1 << 2.

Então, aqui está o nosso manipulador de interrupção modificado:

#define PIN_REG    PIND  // interrupt 0 is on AVR pin PD2
#define PIN_BIT    2

/* Interrupt handler. */
void read_pin()
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

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:

  • Temos que escrever um pouco em um registro de hardware especial chamado EICRA ( Registro de controle de interrupção externa A ) para configurar a interrupção a ser acionada em qualquer alteração do valor do pino. Isso será feito em setup().
  • Temos que escrever um pouco em outro registro de hardware chamado EIMSK ( registro de interrupção externa MaSK ) para ativar a interrupção INT0. Isso também será feito em setup().
  • Temos que definir o ISR com a sintaxe ISR(INT0_vect) { ... }.

Aqui está o código para o ISR e setup(), todo o resto é inalterado:

/* Interrupt service routine for INT0. */
ISR(INT0_vect)
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

void setup()
{
    Serial.begin(9600);
    EICRA = 1 << ISC00;  // sense any change on the INT0 pin
    EIMSK = 1 << INT0;   // enable INT0 interrupt
}

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:

  1. sequência com fio (4 ciclos)
  2. vetor de interrupção: pule para ISR (3 ciclos)
  3. Prólogo ISR: salvar regs (12 ciclos)
  4. primeira coisa no corpo do ISR: leia a porta IO (1 ciclo)

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:

  • a primeira parte será um pequeno fragmento de montagem que
    • salve um único registro na pilha
    • leia PIND nesse registro
    • armazenar esse valor em uma variável global
    • restaurar o registro da pilha
    • pule para a segunda parte
  • a segunda parte será o código C regular com prólogo e epílogo gerado pelo compilador

Nosso INT0 ISR anterior é então substituído por este:

volatile uint8_t sampled_pin;    // this is now a global variable

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    push r0                \n"  // save register r0
    "    in r0, %[pin]          \n"  // read PIND into r0
    "    sts sampled_pin, r0    \n"  // store r0 in a global
    "    pop r0                 \n"  // restore previous r0
    "    rjmp INT0_vect_part_2  \n"  // go to part 2
    :: [pin] "I" (_SFR_IO_ADDR(PIND)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Aqui, estamos usando a macro ISR () para ter o instrumento do compilador INT0_vect_part_2com 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 ininstruçã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) eo sbi( Defina como 1 bit em algum registro de E / S) da seguinte maneira:

sbic PIND, 2   ; skip the following if bit 2 of PIND is clear
sbi GPIOR0, 0  ; set to 1 bit 0 of GPIOR0

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_pinnão é mais útil, pois é basicamente substituída pelo GPIOR0:

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    sbic %[pin], %[bit]    \n"
    "    sbi %[gpio], 0         \n"
    "    rjmp INT0_vect_part_2  \n"
    :: [pin]  "I" (_SFR_IO_ADDR(PIND)),
       [bit]  "I" (PIN_BIT),
       [gpio] "I" (_SFR_IO_ADDR(GPIOR0)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

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:

  1. sequência com fio (4 ciclos)
  2. vetor de interrupção: pule para ISR (3 ciclos)
  3. Corpo ISR: leia a porta IO (no 1º ciclo)

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.

address  instruction      comment
────────┼─────────────────┼──────────────────────
 0x0000  jmp __init       reset vector 
 0x0004  jmp __vector_1   a.k.a. INT0_vect
 0x0008  jmp __vector_2   a.k.a. INT1_vect
 0x000c  jmp __vector_3   a.k.a. PCINT0_vect
  ...
 0x0064  jmp __vector_25  a.k.a. SPM_READY_vect

Cada vetor possui um slot de 4 bytes, preenchido com uma única jmpinstruçã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ções sbice sbi, mas não as rjmp. Se fizermos isso, a tabela de vetores acabará assim:

address  instruction      comment
────────┼─────────────────┼──────────────────────
 0x0000  jmp __init       reset vector 
 0x0004  sbic PIND, 2     the first part...
 0x0006  sbi GPIOR0, 0    ...of our ISR
 0x0008  jmp __vector_2   a.k.a. INT1_vect
 0x000c  jmp __vector_3   a.k.a. PCINT0_vect
  ...
 0x0064  jmp __vector_25  a.k.a. SPM_READY_vect

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:

#include <avr/io.h>

.weak __heap_end
.set  __heap_end, 0

.macro vector name
    .weak \name
    .set \name, __vectors
    jmp \name
.endm

.section .vectors
__vectors:
    jmp __init
    sbic _SFR_IO_ADDR(PIND), 2   ; these 2 lines...
    sbi _SFR_IO_ADDR(GPIOR0), 0  ; ...replace vector_1
    vector __vector_2
    vector __vector_3
    [...and so forth until...]
    vector __vector_25

.section .init2
__init:
    clr r1
    out _SFR_IO_ADDR(SREG), r1
    ldi r28, lo8(RAMEND)
    ldi r29, hi8(RAMEND)
    out _SFR_IO_ADDR(SPL), r28
    out _SFR_IO_ADDR(SPH), r29

.section .init9
    jmp main

Pode ser compilado com a seguinte linha de comando:

avr-gcc -c -mmcu=atmega328p silly-crt.S

O esboço é idêntico ao anterior, exceto que não há INT0_vect e INT0_vect_part_2 é substituído por INT1_vect:

/* Interrupt service routine for INT1 hijacked to service INT0. */
ISR(INT1_vect)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

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 -nostartfilesopçã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.

Edgar Bonet
fonte
2
Boa explicação! +1
Nick Gammon
6

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.

Clayton Mills
fonte