A rotina de serviço de interrupção do AVR não está sendo executada tão rápido quanto o esperado (sobrecarga de instruções?)

8

Estou desenvolvendo um pequeno analisador lógico com 7 entradas. Meu dispositivo de destino é um ATmega168com uma taxa de clock de 20 MHz. Para detectar alterações lógicas, uso interrupções de alteração de pinos. Agora estou tentando descobrir a menor taxa de amostragem possível de detectar essas alterações de pinos. Eu determinei um valor mínimo de 5,6 µs (178,5 kHz). Não é possível capturar todos os sinais abaixo dessa taxa.

Meu código está escrito em C (avr-gcc). Minha rotina se parece com:

ISR()
{
    pinc = PINC; // char
    timestamp_ll = TCNT1L; // char
    timestamp_lh = TCNT1H; // char
    timestamp_h = timerh; // 2 byte integer
    stack_counter++;
}

Minha alteração de sinal capturada está localizada em pinc. Para localizá-lo, tenho um valor de carimbo de data / hora de 4 bytes.

Na folha de dados, li que a rotina de serviço de interrupção leva 5 relógios para saltar e 5 para retornar ao procedimento principal. Estou assumindo que cada comando no meu ISR()está levando 1 relógio para ser executado; Então, em suma, deve haver uma sobrecarga de 5 + 5 + 5 = 15relógios. A duração de um relógio deve estar de acordo com a taxa de clock de 20MHz 1/20000000 = 0.00000005 = 50 ns. A sobrecarga total, em segundos deveria ser, em seguida,: 15 * 50 ns = 750 ns = 0.75 µs. Agora não entendo por que não consigo capturar nada abaixo de 5,6 µs. Alguém pode explicar o que está acontecendo?

arminb
fonte
talvez 5 relógios para despachar o código ISR, que inclui o contexto salvando e restaurando o epilog / prólogo que você não vê na fonte C. Além disso, o que o hardware está fazendo quando a interrupção dispara? Está em algum estado de sono. (Eu não sei AVR, mas, em geral, interrompendo o processamento de certos estados pode demorar mais tempo.)
Kaz
@arminb Consulte também esta pergunta para obter mais idéias sobre como capturar eventos externos com maior precisão. Também [este apêndice] (www.atmel.com/Images/doc2505.pdf) também pode ser interessante.
precisa saber é o seguinte

Respostas:

10

Existem alguns problemas:

  • Nem todos os comandos do AVR levam 1 relógio para serem executados: se você olhar a parte de trás da folha de dados, ele possui o número de relógios necessários para que cada instrução seja executada. Então, por exemplo, ANDé uma instrução de um relógio, MUL(multiplica) leva dois relógios, enquanto LPM(carregar memória do programa) é três e CALLé 4. Portanto, com relação à execução da instrução, isso realmente depende da instrução.
  • 5 relógios para pular e 5 para retornar podem ser enganosos. Se você observar seu código desmontado, descobrirá que, além do salto e das RETIinstruções, o compilador adiciona todos os tipos de outros códigos, o que também leva tempo. Por exemplo, você pode precisar de variáveis ​​locais que são criadas na pilha e devem ser salvas etc. A melhor coisa a fazer para ver o que realmente está acontecendo é observar a desmontagem.
  • Por fim, lembre-se de que, enquanto você está na sua rotina ISR, suas interrupções não são acionadas. Isso significa que você não poderá obter o tipo de desempenho que está procurando no analisador lógico, a menos que saiba que seus níveis de sinal mudam em intervalos mais longos do que o necessário para atender à sua interrupção. Para ser claro, depois de calcular o tempo necessário para a execução do seu ISR, isso fornece um limite superior para a rapidez com que você pode capturar um sinal . Se você precisar capturar dois sinais, começará a ter problemas. Para ser excessivamente detalhado sobre isso, considere o seguinte cenário:

insira a descrição da imagem aqui

Se xfor o tempo necessário para reparar sua interrupção, o sinal B nunca será capturado.


Se pegarmos seu código ISR, colá-lo em uma rotina ISR (eu usei ISR(PCINT0_vect)), declarar todas as variáveis volatilee compilar para ATmega168P, o código desmontado será o seguinte (consulte a resposta da @ jipple para obter mais informações) antes de chegarmos ao código que "faz alguma coisa" ; em outras palavras, o prólogo do seu ISR é o seguinte:

  37                    .loc 1 71 0
  38                    .cfi_startproc
  39 0000 1F92              push r1
  40                .LCFI0:
  41                    .cfi_def_cfa_offset 3
  42                    .cfi_offset 1, -2
  43 0002 0F92              push r0
  44                .LCFI1:
  45                    .cfi_def_cfa_offset 4
  46                    .cfi_offset 0, -3
  47 0004 0FB6              in r0,__SREG__
  48 0006 0F92              push r0
  49 0008 1124              clr __zero_reg__
  50 000a 8F93              push r24
  51                .LCFI2:
  52                    .cfi_def_cfa_offset 5
  53                    .cfi_offset 24, -4
  54 000c 9F93              push r25
  55                .LCFI3:
  56                    .cfi_def_cfa_offset 6
  57                    .cfi_offset 25, -5
  58                /* prologue: Signal */
  59                /* frame size = 0 */
  60                /* stack size = 5 */
  61                .L__stack_usage = 5

então, PUSHx 5, inx 1, clrx 1. Não é tão ruim quanto os vars de 32 bits do jipple, mas ainda não é nada.

Parte disso é necessária (expanda a discussão nos comentários). Obviamente, como a rotina ISR pode ocorrer a qualquer momento, ela deve preservar os registros que usa, a menos que você saiba que nenhum código em que uma interrupção pode ocorrer usa o mesmo registro que sua rotina de interrupção. Por exemplo, a seguinte linha no ISR desmontado:

push r24

Existe porque tudo passa r24: o seu pincé carregado lá antes de entrar na memória, etc. Então você deve ter isso primeiro. __SREG__é carregado r0e pressionado: se isso ocorrer r24, você poderá salvar umPUSH


Algumas soluções possíveis:

  • Use um loop de pesquisa apertado, conforme sugerido por Kaz nos comentários. Essa provavelmente será a solução mais rápida, independentemente de você escrever o loop em C ou montagem.
  • Escreva seu ISR em assembly: dessa maneira, você pode otimizar o uso do registro de forma que o menor número deles precise ser salvo durante o ISR.
  • Declare suas rotinas ISR ISR_NAKED , embora isso acabe sendo mais uma solução de arenque vermelho. Quando você declara rotinas ISR ISR_NAKED, o gcc não gera código de prólogo / epílogo, e você é responsável por salvar os registros que seu código modifica, bem como chamar reti(retornar de uma interrupção). Infelizmente, não há nenhuma maneira de usar registros em avr-gcc C diretamente (obviamente você pode na montagem), no entanto, o que você pode fazer é variáveis de ligação aos registos específicos com os register+ asmpalavras-chave, como este: register uint8_t counter asm("r3");. Se você fizer isso, para o ISR, saberá o que está registrando no ISR. O problema, então, é que não há como gerar pushepopsalvar os registros usados ​​sem montagem embutida (ver ponto 1). Para garantir a necessidade de salvar menos registros, você também pode vincular todas as variáveis ​​não ISR a registros específicos, no entanto, você não se depara com um problema que o gcc usa registros para embaralhar dados para e da memória. Isso significa que, a menos que você observe a desmontagem, não saberá o que o seu código principal usa. Portanto, se você estiver pensando ISR_NAKED, é melhor escrever o ISR na montagem.
angelatlarge
fonte
Obrigado, então meu código C faz a enorme sobrecarga? Seria mais rápido se eu escrevesse no assembler? Sobre a segunda coisa, eu estava ciente disso.
Arminb #
@arminb: Eu não sei o suficiente para responder a essa pergunta. Minha suposição seria que o compilador é razoavelmente inteligente e faz o que faz por uma razão. Dito isto, tenho certeza de que, se você passar algum tempo com a montagem, poderá extrair mais alguns ciclos de relógio da sua rotina ISR.
precisa saber é o seguinte
1
Penso que, se você deseja uma resposta mais rápida, geralmente evita interrupções e controla os pinos em um circuito fechado.
Kaz
1
Com objetivos específicos em mente, é possível otimizar o código usando o assembler. Por exemplo, o compilador começa pressionando todos os registradores usados ​​na pilha e, em seguida, inicia a execução da rotina real. Se você tem um momento crítico, pode mover alguns dos empurrões para trás e fazer avançar um tempo crítico. Então, sim, você pode otimizar usando o assembler, mas o compilador em si também é bastante inteligente. Eu gosto de usar o código compilado como ponto de partida e modificá-lo manualmente para meus requisitos específicos.
Jipie
1
Resposta muito boa. Acrescentarei que o compilador adiciona todos os tipos de armazenamento e restauração de registros para atender às necessidades da maioria dos usuários. É possível escrever seu próprio manipulador de interrupções - se você não precisar de toda essa sobrecarga. Alguns compiladores podem até oferecer uma opção para criar uma interrupção "rápida", deixando grande parte da "contabilidade" para o programador. Eu não iria necessariamente direto para um loop apertado sem ISR se não pudesse cumprir minha agenda. Primeiro, consideraria um uC mais rápido e depois descobriria se poderia usar algum tipo de hardware de cola, como uma trava e um RTC.
Scott Seidman
2

Há muitos registros PUSH'ing e POP'ing a serem empilhados antes do início do ISR real, ou seja, os cinco ciclos de relógio mencionados. Dê uma olhada na desmontagem do código gerado.

Dependendo da cadeia de ferramentas usada, a montagem da lista é feita de várias maneiras. Eu trabalho na linha de comando do Linux e este é o comando que eu uso (ele requer o arquivo .elf como entrada):

avr-objdump -C -d $(src).elf

Dê uma olhada em um sniplet de código que usei recentemente para um ATtiny. É assim que o código C se parece:

ISR( INT0_vect ) {
        uint8_t myTIFR  = TIFR;
        uint8_t myTCNT1 = TCNT1;

E este é o código de montagem gerado para ele:

00000056 <INT0_vect>:
  56:   1f 92           push    r1
  58:   0f 92           push    r0
  5a:   0f b6           in      r0, SREG        ; 0x3f
  5c:   0f 92           push    r0
  5e:   11 24           eor     r1, r1
  60:   2f 93           push    r18
  62:   3f 93           push    r19
  64:   4f 93           push    r20
  66:   8f 93           push    r24
  68:   9f 93           push    r25
  6a:   af 93           push    r26
  6c:   bf 93           push    r27
  6e:   48 b7           in      r20, TIFR       ; uint8_t myTIFR  = TIFR;
  70:   2f b5           in      r18, TCNT1      ; uint8_t myTCNT1 = TCNT1;

Para ser sincero, minha rotina C usa mais algumas variáveis ​​que causam todos esses problemas, mas você entendeu.

O carregamento de uma variável de 32 bits se parece com o seguinte:

  ec:   80 91 78 00     lds     r24, 0x0078
  f0:   90 91 79 00     lds     r25, 0x0079
  f4:   a0 91 7a 00     lds     r26, 0x007A
  f8:   b0 91 7b 00     lds     r27, 0x007B

Aumentar uma variável de 32 bits em 1 se parece com isso:

  5e:   11 24           eor     r1, r1
  d6:   01 96           adiw    r24, 0x01       ; 1
  d8:   a1 1d           adc     r26, r1
  da:   b1 1d           adc     r27, r1

Armazenar uma variável de 32 bits se parece com isso:

  dc:   80 93 78 00     sts     0x0078, r24
  e0:   90 93 79 00     sts     0x0079, r25
  e4:   a0 93 7a 00     sts     0x007A, r26
  e8:   b0 93 7b 00     sts     0x007B, r27

Então é claro que você precisa exibir os valores antigos depois de sair do ISR:

 126:   bf 91           pop     r27
 128:   af 91           pop     r26
 12a:   9f 91           pop     r25
 12c:   8f 91           pop     r24
 12e:   4f 91           pop     r20
 130:   3f 91           pop     r19
 132:   2f 91           pop     r18
 134:   0f 90           pop     r0
 136:   0f be           out     SREG, r0        ; 0x3f
 138:   0f 90           pop     r0
 13a:   1f 90           pop     r1
 13c:   18 95           reti

De acordo com o resumo de instruções na folha de dados, a maioria das instruções é de ciclo único, mas PUSH e POP são de ciclo duplo. Você entendeu a origem do atraso?

jippie
fonte
Obrigado pela sua resposta! Agora estou ciente do que está acontecendo. Especialmente obrigado pelo comando avr-objdump -C -d $(src).elf!
Arminb #
Reserve alguns momentos para entender as instruções de montagem que são avr-objdumpexibidas, elas são explicadas brevemente na folha de dados em Resumo das instruções. Na minha opinião, é uma boa prática familiarizar-se com os mnemônicos, pois isso pode ajudar muito ao depurar seu código C.
Jipie #
De fato, é útil ter a desmontagem como parte de seu padrão Makefile: portanto, sempre que você cria seu projeto, ele é automaticamente desmontado, para que você não precise pensar sobre isso ou lembre-se de fazê-lo manualmente.
precisa saber é o seguinte