sexta-feira, 28 de junho de 2013

SDCC: Interrupções no PIC

SDCC: Interrupções no PIC

Interrupção é um artifício dos que muitos processadores implementam para que respondam aos eventos externos. Esse pedido de processamento possui prioridades. É um cutucão. Se ele receber um sinal de interrupção, ele:
- pára o que está fazendo,
- salva o contexto,
- identifica a interrupção,
- aciona a rotina de tratamento da interrupção (que TÊM que ser curta e eficiente)
- retornar o contexto
- voltar a fazer o que estava fazendo antes da interrupção

Dependendo da arquitetura do microcontrolador podemos ter 1 ou mais pontos de tratamento de interrupção. Isso dá o nome de vetores. E cada vetor atendendo a um propósito específico.
Isso tudo é muito dependente da arquitetura do microcontrolador. À medida que nos aprofundamos no assunto, procure ler intensamente o manual do microcontrolador.

Contexto é configuração atual dos registradores no momento imediatamente anterior ao recebimento da interrupção. Dependendo da situação, salvar o contexto é necessário para que quando houver o retorno da atividade interrompida, tudo esteja íntegro.


Interrupções no SDCC

Escrever interrupçoes no SDCC é facilidado pela instrução __interrupt . Onde é um número que identifica a posição no vetor de interrupções. 

A estrutura é de uma função com essa instrução mágica. Essa instrução faz com que o código seja relocado para o endereço de vetor específico.

void isr() __interrupt 1 {
/*...*/
}

Como eu disse, a posição no vetor de interrupções é dependente da arquitetura do microcontrolador. Você vai precisar ler muito o manual do microcontrolador para entender como utilizar cada vetor especificamente.

Normalmente é algo sequencial... por exemplo se tomarmos o PIC16F88, ele só tem 1 vetor de interrupção, portanto uma declaração simples já basta.  Esse vetor fica localizado no endereço 0x004 da memória...

Já o PIC18F252, ele têm 2 vetores, o primeiro em 0x010 e o segundo em 0x018. Com propósitos definidos. Declarar no SDCC essas interrupções se faz assim:

void isr_high() __interrupt 1 {
/*...*/
}
void isr_low() __interrupt 2 {
/*...*/
}

Contexto

Falei um pouco sobre contextos. Quando uma interrupção é invocada. O SDCC automaticamente criará o código necessário para salvar o estado dos registradores. Isso pode ser visto no assembly gerado (.asm)

Por exemplo (para PIC16F88):

void isr1() __interrupt 1 {
PORTB=0xFF;
}

irá gerar o código em assembler:

c_interrupt code 0x4      ; 
__sdcc_interrupt
_isr1 ;Function start
MOVWF WSAVE
SWAPF STATUS,W
CLRF STATUS
MOVWF SSAVE
MOVF PCLATH,W
CLRF PCLATH
MOVWF PSAVE
MOVF FSR,W
BANKSEL ___sdcc_saved_fsr
MOVWF ___sdcc_saved_fsr
; .line 20; "interrupt.c" PORTB=0xFF;
MOVLW 0xff
BANKSEL _PORTB
MOVWF _PORTB
BANKSEL ___sdcc_saved_fsr
MOVF ___sdcc_saved_fsr,W
BANKSEL FSR
MOVWF FSR
MOVF PSAVE,W
MOVWF PCLATH
CLRF STATUS
SWAPF SSAVE,W
MOVWF STATUS
SWAPF WSAVE,F
SWAPF WSAVE,W
END_OF_INTERRUPT
RETFIE

O código marcado é o código que salva o contexto e restaura respectivamente.

Obviamente esse cuidado tem um custo. Vou marcar o programa de teste. E nas condições acima o programa completo tomou aproximadamente 78 bytes

Descartando o Contexto

Se na lógica de programação for julgado que o processamento de context é desnecessário, podemos introduzir na declaração da rotina de tratamento de interrupção (ISR) o modificador __naked

__naked instrui ao SDCC para não gerar o código para salvamento e restaruração do contexto. Seguindo o nosso exemplo:

void isr1() __naked __interrupt 1 {
PORTB=0xFF;
}

Gerará o seguinte assembly:

c_interrupt code 0x4
__sdcc_interrupt
_isr1 ;Function start
; 0 exit points
; .line 20; "interrupt.c" PORTB=0xFF;
MOVLW 0xff
BANKSEL _PORTB
MOVWF _PORTB
END_OF_INTERRUPT
RETFIE

Muito mais enxuto não? :) Com isso o programa completo tomou somente  30 bytes. Menos da metade!!!

Sessões críticas

A execução de uma rotina de tratamento de interrupção, não invalida o microcontrolador de estar recebendo ainda eventos externos. Isso pode gerar um problema sério de ter que atender a múltiplas requisições de interrupção sem ter terminado alguma delas. O efeito colateral disso é que  o endereço de retorno da interrupção (retornar de onde parou) é armazenado em uma pilha.

A pilha é algo limitado. Costuma ser de míseros bytes (16, 32, 64bytes) e tem seu lugar na na RAM.

Se a pilha atinge o seu topo, o microcontrolador pode parar e reiniciar. Com isso o programa não executa direito o que tem de ser resolvido urgentemente... A depuração disso não é fácil!

Para evitar tal situação, é bom instruir o microcontrolador a não aceitar mais interrupções quando estiver processando uma. Podemos explicitamente dizer isso  no programa, desabilitando interrupções, e antes de terminar, rehabilitando-os. Ou utilizar  a instrução __critical

Vamos tomar o nosso programa de exemplo:

void isr1() __critical __naked __interrupt 1 {
PORTB=0xFF;
}

O seu assembly será:

c_interrupt code 0x4
__sdcc_interrupt
_isr1 ;Function start
; 0 exit points
; .line 19; "interrupt.c" void isr1() __critical __naked __interrupt 1 {
MOVF INTCON,W
BCF INTCON,7
BANKSEL r0x1000
MOVWF r0x1000
; .line 20; "interrupt.c" PORTB=0xFF;
MOVLW 0xff
BANKSEL _PORTB
MOVWF _PORTB
BANKSEL r0x1000
BTFSS r0x1000,7
GOTO _00001_DS_
BANKSEL INTCON
BSF INTCON,7
END_OF_INTERRUPT
_00001_DS_
RETFIE

O código marcado acima desabilita TODAS as interrupções do PIC16F88 (GIE=0) e reabilita no final da interrupção. O programa compilado total tomou 54bytes. O que é  razoável.

--//--

Nota: Não fiz comparativo entre tamanhos dos programas compilados como se fosse mera vantagem. Não! Quando se programa para microcontroladores, não há espaço para desperdício. O volume de memória, principalmente RAM é muito pequeno e isso requer otimizações seguras para evitar bugs!

Se durante o desenvolvimento do código, você desenvolvedor, julgar e se certificar de que os preâmbulos são desnecessários, por que não otimizar? Ganha-se em velocidade e tamanho de código. Mas fique certo de também haverá situações em que nenhuma dessas otimizações poderão ser empregadas... Valha-se de sua experiência e conhecimento!

--//--
Há algum tempo atrás, fiquei "encucado com um negócio aqui"... se usarmos o __naked uma coisa vai acontecer: a função é reescrita em assembly sem o código de preâmbulo (e epílogo). O que significa isso? Simples, ele não savará contexto, tampouco irá criar a chamada para retorno de função.

O QUÊ?! Sim, Não cria o "retorno". Isso significa que chamar uma função __naked, irá fazer com que a função saia executando atropelando tudo até encontrar um lugar com RETFIE ou RETURN. Enfim. Se o contexto não me é necessário, o retorno é! para isso eu sempre procuro finalizar uma função __naked com a linha

__asm RETURN __endasm;

Por exemplo (para interrupts):

void isr1() __critical __naked __interrupt 1 {
PORTB=0xFF;
 __asm RETFIE __endasm; /* finaliza a função de interrupt */
}

Nenhum comentário:

Postar um comentário