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 __interruptA 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