segunda-feira, 29 de julho de 2013

YABLO: Yet Another Bootloader (English Version)

My vacations has gone after about 1¹/2 months working hard on my bootloader. Finally, I got it done! Well, I decided to write this article entirely in English. Just to match with the concept of documentation style I'm producing in code.google.com... I hope this won't be an issue for you!

In my latest articles I describe some models to work with memory relocation... so here's the result of that blah blah blah!

Yablo

There's tons of bootloaders abroad. But seriously, I was tired to make LED's blinking all the time, and definitely I took this challenge. Would you dare?

Yablo is an acronym for Yet Another BootLOader... 

Differently from many other bootloaders:
- It's not short (I was fighting with compiler to have it less than 1024 bytes). It has about 0x39A bytes! :)
- It's not written in Assembly (but a mix of C with assembly). 
- It's written in C (not entirely, but a mix of Assembly with C :) )
- It MUST accept data coming from HyperTerminal or any other TTY consoles - yeah... copy and paste the .HEX content!
- It is written entirely in a FREE C Compiler  - so by license (GPL), Yablo inherits GPL, so is free!

By now, it runs in PIC18F252... I know it's easy to port to any other PIC18F252 family. :) but the code was tested in PIC18F252. Keep your eyes in the project site, soon or later we'll get other PIC's there too!

Differently of many source codes I've seen, Yablo was written to be easy to understand the internals of a bootloader. IF you read and understood my 3 articles "SDCC: Relocação de Código - PIC", soon or later a project like this was bound to appear. Knowledge is taught/learned only if you want to transform it.

Inside the source code, there's an implementation that I love and the fundamentals of it is the implementation of a Finite Automata, you know? Just to remember, FA is the kind of processing based on inputs and states... the core of compilers, interpreters, pattern recognition engines, etc... Definitely is an elegant solution (maybe not the most notable code saver, but it is simple, data saver and fast).

From theory to practice: Yablo Memory Structure

I won't spend time justifying the "why?". Yablo was designed firstly to fit in only 512bytes (1 page), but the code started growing beyond this limit... naturally :) Then the goal was set to not pass 1024bytes (2pages).

The boot block occupies the memory from 0x000 to 0x3FF. The interrupt vectors are relocated as follows:
0x000 - jump to bootstrap() function, the bootloader function, will branch to 0x400 when it finishes!
0x008 - BRA 0x408
0x018 - BRA 0x418
0x01a..0x3FF bootloader functions
0x400 - call to user main() function 
0x408 - user ISR - High vector (relocated)
0x418 - user ISR - Low vector (relocated)
0x4??... 0x7FFF - USER PROGRAM SPACE!

The rest is kept the same:
0x30xxyy - config bits
0xF0xxyy - EEPROM data

To allow, the user program to fit in this memory model, a specifik linker script (LKR) must be used. So in this LKR, this region will be protected. And to the SDCC, the new location of interrupt vectors must be informed using the flags --ivt-loc.  Pretty simple! :)

See Yablo's wiki at GoogleCode: https://code.google.com/p/yablo/wiki/Yablo

Fuses... Do I need fuses?

Yes and no. Is very important to understand, when your microcontroller carries an embedded code like that, some fuses were programmed. So NO, is not a good idea to change that. For example, if your bootloader is for a crystal based clock source, there's no reason to change it to RC Osc... Avoid such things. Also, in fuses we have the pages locked or not... don't mess the things otherwise your program won't work. And maybe the bootloader can't boot! 

Yablo and PIColino

One is born to another. Of course, generic hardwares deserve generic firmwares. And in general terms, its fantastic! You can adapt them easily to your needs.

PIColino (now) is a PIC18F252I/P hardware with RS232. Arduino "quasi" compatible shield connectors. The firmware is now targetted to this processor. I've heard, it runs smoothly in PIC18F2520 (the upgrade of 252). I'm waiting my 2550's to arrive in order to see how does it fit in.


sexta-feira, 5 de julho de 2013

SDCC: Relocação de Código - PIC (Parte 3)

Ainda na saga de construir um bootloader para o Picolino (http://mmc-zaap.blogspot.com.br/2013/06/picolino-development-board-para-pic_26.html). Hoje vou demonstrar os "internals" de uma técnica um tanto avançada para invocação de rotinas baseados em outros espaços de endereços.

Imagine o leitor o seguinte: Temos um programa, chamado bootloader. E além dele, um outro que é o programa do usuário. Lembro-lhe de que os ciclos de desenvolvimentos são extremamente diferentes e independentes. Portanto, são programas independentes. E como o bootloader sabe como chamar o código do usuário?

Ponteiros para funções

Até o presente momento não "chutei o balde". Estou tentando o máximo possivel ficar somente circulando entre o C e o que ele puder me prover! (com uma linha de código em assembler mato esse capitulo - aí fica sem graça!)

Quando eu fiz escola técnica (lá em 1990) sempre ouvi dizer que em C o poder da linguagem está em ponteiros. E mantenho esse princípio. Feliz daquele que souber trabalhar com essa estrutura de dados!

Um ponteiro de funções cria uma referência "callable" (você não vai encontrar descrito assim nos livros). Essa referência é um endereço de memória, o qual eu invoco como se fosse uma função... a sintaxe é assim

void (*user_init)()=0x200;

No caso acima, eu declarei um ponteiro para uma função que não recebe parâmetro algum (void) e também não retorna nada (void). Tendo o seu endereço de inicio a posição 0x200.

Para chamar é a coisa mais simples:

(*user_init)();

Pronto! O programa irá delegar controle para o que estiver em 0x200. 

O que o compilador gera? Bom, para isso teremos que analisar o .lst novamente. 

Primeiramente, ele irá declarar um segmento para manter o endereço declarado na inicialização:
                                                idata
                                           _boot        db      0x00, 0x02, 0x00

Mas no corpo da função MAIN, veja o que ele faz:
                                           _main:
                                           ;    .line   6; blptf.c      (*boot)();
000000   cff2     movff   0xff2, 0xfe5          MOVFF   INTCON, POSTDEC1
000002   ffe5
000004   9ef2     bcf     0xf2, 0x7, 0          BCF     INTCON, 7
000006   0005     push                          PUSH
000008   0e26     movlw   0x26                  MOVLW   LOW(_00107_DS_)
00000a   6efd     movwf   0xfd, 0               MOVWF   TOSL
00000c   0e00     movlw   0                     MOVLW   HIGH(_00107_DS_)
00000e   6efe     movwf   0xfe, 0               MOVWF   TOSH
000010   0e00     movlw   0                     MOVLW   UPPER(_00107_DS_)
000012   6eff     movwf   0xff, 0               MOVWF   TOSU
000014   bee4     btfsc   0xe4, 0x7, 0          BTFSC   PREINC1, 7
000016   8ef2     bsf     0xf2, 0x7, 0          BSF     INTCON, 7
000018   c082     movff   0x82, 0xffb           MOVFF   (_boot + 2), PCLATU
00001a   fffb
00001c   c081     movff   0x81, 0xffa           MOVFF   (_boot + 1), PCLATH
00001e   fffa
000020   0100     movlb   0                     BANKSEL _boot
000022   5180     movf    0x80, 0, 0x1          MOVF    _boot, W, B
000024   6ef9     movwf   0xf9, 0               MOVWF   PCL
                                           _00107_DS_:
                                           _00105_DS_:
000026   0012     return  0                     RETURN


Marquei a parte do código que nos interessa. Ele lança mão dos registros PCLATU, PCLATH e PCL. Que fazem a transferência de controle (branch) de forma calculada! :)

Matando a técnica

Para um bootloader, isso é muito grande. A solução em C puro é fantástica. Mas não é limpa em termos de código assembler. Como eu disse em uma linha de assembly consigo matar isso:

Em vez de chamar (*user_init)(), basta no lugar colocar a seguinte instrução:

__asm BRA 0x200 __endasm;

E está feito! O código chega a emagrecer 30 bytes!

--//-- 

Estamos chegando lá. Nesse interim, trabalhei em 3 frentes:
1) criação do bootloader pro Picolino - EM C+ASM (um mínimo de ASM), ocupando uns 400bytes +/-
2) criação do ambiente de desenvolvimento para gerar códigos suporados pelo bootloader do Picolino (será abordado em breve)
3) criação do programa que lê o .HEX do usuário e grava no Picolino usando o bootloader.

--//--

Esse malabarismo todo tem objetivos:
1) mostrar pro leitor o quão é complexo desenvolver uma solução que pode vir a facilitar.
2) ensinar ao leitor, técnicas avançadas de programação.
3) documentar de forma didática a minha saga no desenvolvimento de sistemas micrcontrolados.

Próximos posts vou colocar + circuitos! É para relaxar!

terça-feira, 2 de julho de 2013

SDCC: Relocação de Código - PIC (Parte 2)

Relocando com #pragma code

Lendo o confuso manual do SDCC encontrei suporte a uma diretiva de compilação chamada #pragma code.

A sintaxe é simples:
#pragma code nome_da_função endereço

Exemplo:

#pragma code boot 0x800

...

void boot() {
...
}

Essa diretiva instrui o compilador a colocar a função "boot()" no endereço 0x800. Vamos ao nosso programa blink novamente para ver como funciona.

#include "pic18fregs.h"
#include "delay.h"

#pragma code init 0x800

#pragma config OSC=HS, PWRT=OFF, BOR=OFF, WDT=OFF, CCP2MUX=OFF, LVP=OFF, CP0=OFF, CP1=OFF, CP2=OFF, CP3=OFF

void init() {
PORTC=0x00;
}

int main(){

 TRISB=0x7F;

 while(1) {
  PORTB=(PORTB^0x80);
delay10ktcy(500);
 }
}

Ao compilarmos este programa teremos no assembly (veja .lst)  o seguinte trecho:
Para compilar: sdcc --use-non-free -p18f252 -mpic16 blink.c

                                           S_blink__init code 0X000800
                                           _init:
                                           ; .line 8; blink.c void init() {
000800   cfd9     movff   0xfd9, 0xfe5     MOVFF FSR2L, POSTDEC1
000802   ffe5
000804   cfe1     movff   0xfe1, 0xfd9     MOVFF FSR1L, FSR2L
000806   ffd9
                                           ; .line 9; blink.c PORTC=0x00;
000808   6a82     clrf    0x82, 0           CLRF _PORTC
00080a   cfe4     movff   0xfe4, 0xfd9     MOVFF PREINC1, FSR2L
00080c   ffd9
00080e   0012     return  0                 RETURN

Fantástico não?!! :)  

Não! :) isso funciona para qualquer função, menos main(). Coisas internas do SDCC. Então usar o #pragma code não funciona direito assim nu e cru.

__interrupt 0

Lembra que eu falei em SDCC: Interrupções no PIC ? Falei dos vetores 1 e 2... mas não contei sobre o vetor 0.

Uma função declarada como __interrupt 0, irá tomar espaço na posição 0x000 da memória de código. É o vetor de RESET! Isso é bem escondido e para usar esse recurso temos que tomar mais instruções de compilação. Vamos alterar o nosso programa blink...

#include "pic18fregs.h"
#include "delay.h"

#pragma config OSC=HS, PWRT=OFF, BOR=OFF, WDT=OFF, CCP2MUX=OFF, LVP=OFF, CP0=OFF, CP1=OFF, CP2=OFF, CP3=OFF

int main();

void isr_reset() __interrupt 0 {
main();
}

int main(){

 TRISB=0x7F;

 while(1) {
  PORTB=(PORTB^0x80);
delay10ktcy(500);
 }
}

Vamos por partes... a primeira declaração (em azul) é uma formalidade. Estou declarando o protótipo da função main() que será chamada pelo isr_reset(). Como a função é construida depois da sua chamada, a gente simplesmente diz para o compilador que existe uma função main em algum lugar... o compilador cuida do resto.

A segunda declaração é onde vamos criar um instrução GOTO pulando para main, na posição 0x000 do microcontrolador. Que é justamente o entry point depois do RESET/STARTUP do microcontrolador.

Para compilar precisaremos de 1 diretiva nova de compilação: --no-crt.

Essa diretiva instrui o linker para NÃO usar a runtime padrão do SDCC. Com isso o controle de startup do programa no microcontrolador fica inteiramente a cargo do PROGRAMADOR!!!

Compilando o programa: sdcc --use-non-free --no-crt -p18f252 -mpic16 blink.c

Olha que interessante no .lst:

1) Entry point do programa:
                                           S_blink_ivec_0x0_isr_reset code 0X000000
                                           ivec_0x0_isr_reset:
000000   ef3f     goto    0x7e             GOTO _isr_reset
000002   f000

Criou no endereço 0x000, a chamada para isr_reset()! Bingo!

Mais para baixo, temos o corpo da função isr_reset(), que chama main() (omiti os preâmbulos para salvamento e restauro de contexto que podem ser omitidos se usarmos __naked - lembre-se do bug do RETFIE)

                                           _isr_reset:
                                           ; .line 8; blink.c void isr_reset() __interrupt 0 {
00007e   cfd8     movff   0xfd8, 0xfe5     MOVFF STATUS, POSTDEC1
000080   ffe5
000082   cfe0     movff   0xfe0, 0xfe5     MOVFF BSR, POSTDEC1
...
0000a6   ffd9
                                           ; .line 9; blink.c main();
0000a8   ec6a     call    0xd4, 0           CALL _main
0000aa   f000
0000ac   cfe4     movff   0xfe4, 0xfd9     MOVFF PREINC1, FSR2L
...
0000ce   cfe4     movff   0xfe4, 0xfd8     MOVFF PREINC1, STATUS
0000d0   ffd8
0000d2   0010     retfie  0                 RETFIE

BINGO!!! :) agora sim. Tudo parece estar indo bem para o bootloader! Mas descobri mais um bug do SDCC:

BUG: Mesmo informando --no-crt o SDCC insiste em criar as rotinas de inicialização do sdcc. Veja no .lst as entradas:
                                           __sdcc_gsinit_startup:
                                           ; I code from now on!
000004   ef04     goto    0x8               goto __sdcc_program_startup
000006   f000
                                           ; ; Starting pCode block
                                           __sdcc_program_startup:
000008   ec6a     call    0xd4, 0           CALL _main
00000a   f000


Vamos lá! :) Estamos chegando perto... Nesse contexto agora sim podemos relocar main() (mas não com esse nome, pois o SDCC não deixa relocar main com #pragma code - Mais um bug?)

Como acertamos o nosso entry point de programa, usando vector 0, o main() pode se chamar qualquer coisa, com isso consegue-se relocar o código para qualquer endereço de memória.

Novo blink.c:

#include "pic18fregs.h"
#include "delay.h"

#pragma config OSC=HS, PWRT=OFF, BOR=OFF, WDT=OFF, CCP2MUX=OFF, LVP=OFF, CP0=OFF, CP1=OFF, CP2=OFF, CP3=OFF
#pragma code bootstrap 0x200
int bootstrap();

void isr_reset() __naked __interrupt 0 {
bootstrap();
}

int bootstrap(){

 TRISB=0x7F;

 while(1) {
  PORTB=(PORTB^0x80);
delay10ktcy(500);
 }

Simplesmente renomeei "main" para "bootstrap". Adicionei o #pragma code, mandando o bootstrap para o endereço 0x200... veja agora como ficou o .lst:

Compile com: sdcc --use-non-free --no-crt -p18f252 -mpic16  blink.c

                                           S_blink__isr_reset code 0X000000
                                           _isr_reset:
                                           ; .line 9; blink.c bootstrap();
000000   ec00     call    0x200, 0         CALL _bootstrap

000002   f001
...
                                           S_blink__bootstrap code 0X000200
                                           _bootstrap:
                                           ; .line 12; blink.c int bootstrap(){
000200   cfd9     movff   0xfd9, 0xfe5     MOVFF FSR2L, POSTDEC1
000202   ffe5
000204   cfe1     movff   0xfe1, 0xfd9     MOVFF FSR1L, FSR2L
000206   ffd9
...

Em azul, o nosso vetor de reset, chamando bootstrap(), em verde, o nosso bootstrap() relocado para o endereço 0x200 conforme eu havia instruído! :)

Eheheheh!  E detalhe. Se você está seguindo o que estou dizendo, observe que no .lst NÃO EXISTE aquele código lixo de inicialização do SDCC (sdcc_gsinit_startup...) portanto o corpo é mais enxuto. (essa tranqueira toda gerou um HEX de 32bytes somente).

Será que esse código funciona? Testei no MPLAB SIM e funcionou perfeitamente! 

O caminho até o bootloader

Falta desacoplar o código e criar 2 modelos distintos:
1) o bootloader - que é o firmware que rodará no espaço 0x000 a 0x1FF. Conforme manual.
2) o programa em si, que é o que precisa rodar a partir do endereço 0x200.

Até agora vimos técnicas para relocar o código para outros endereços e como criar o próprio "entry point" usando vector 0. Mas tudo isso está junto.

Há 2 tópicos que preciso abordar ainda antes de iniciar o código do bootloader:
1) Delegando controle via Ponteiros para funções!
2) Relocação de código via .LKR (Linker script)

Até a próxima!

segunda-feira, 1 de julho de 2013

SDCC: Relocação de Código - PIC (Parte 1)

Vamos voltar ao nosso assunto de desenvolver um bootloader em C para PIC18F. (Apesar de ser um assunto batido, eu prefiro subir o Everest a ver fotos da paisagens de quem já foi lá! Entendeu a analogia?)

O SDCC pobremente suporta relocação de código. Ele traz os switches --ivt-loc e --code-loc (endereço das tabelas de vetores de interrupções e código) mas eles são ineficientes, incapazes, moribundos!

Recentemente descobri uma diretiva de compilação (#pragma) que pareceu ser promissor e tem feito o que eu preciso. Mas funciona de forma pobre, pois não consigo relocar o start point default do programa (main) para o ponto que eu preciso... então vamos para os artifícios.

Entendendo a Relocação

Quando o sistema possui um pré-inicializador, que pode ser um bootloader. O programa do usuário não precisa assumir o endereço absoluto. E para que não acabe apagando o bootloader, o seu código normalmente é adicionado no final daquele setor.

Os compiladores normalmente irão gerar seus códigos executáveis almejando o endereçamento padrão e farão a sua linkedição com as bibliotecas de inicialização necessários (crt, c0t, c0rt...).

No caso da relocação, o compilador terá que gerar nova origem (ORG) e todos os branches, jumps, goto's, skip tests, memory access deverão observar esse novo endereço e calcular os seus "saltos" baseado nessa nova origem.

Na compilação, um dos arquivos gerados é o .LST. Ele possui a listagem em assembly, com a referencia de endereço de cada linha de código.

Vamos pegar o nosso programinha de blink!
#define __18F252
#include "pic18fregs.h"
#pragma config OSC=HS, OSCS=OFF, PWRT=OFF, BOR=OFF, WDT=OFF, CCP2MUX=OFF, LVP=OFF, CP0=OFF, CP1=OFF, CP2=OFF, CP3=OFF
int main()
{
TRISB=0xFD;
while(1) {
PORTB=(PORTB^0x02);
}
}

Veja o código da função "main()" sem relocação - in natura.

                                           ; ; Starting pCode block
                                           S_blink__main code
                                           _main:
                                           ; .line 20; blink.c TRISB=0xFD;
00000a   0efd     movlw   0xfd             MOVLW 0xfd
00000c   6e93     movwf   0x93, 0           MOVWF _TRISB
                                           _00106_DS_:
                                           ; .line 22; blink.c PORTB=(PORTB^0x02);
00000e   7281     btg     0x81, 0x1, 0     BTG _PORTB, 1
000010   d7fe     bra     0xe               BRA _00106_DS_
000012   0012     return  0                 RETURN

No código acima, eu removi os preâmbulos de inicialização CRT/C0T do sdcc (sdcc_gsinit...). Veja que a função main, começa no endereço 0x0a; o loop e o branch (BRA) apontam para 0x0e;
O label __0106_DS_ marca o inicio e fim do loop "while (1)..."

Vou relocar o código para o endereço 0x800

                                           ; ; Starting pCode block
                                           S_blink___main code 0X000800
                                           __main:
                                           ; .line 18; blink.c int _main()
000800   cfd9     movff   0xfd9, 0xfe5     MOVFF FSR2L, POSTDEC1
000802   ffe5
000804   cfe1     movff   0xfe1, 0xfd9     MOVFF FSR1L, FSR2L
000806   ffd9
                                           ; .line 20; blink.c TRISB=0xFD;
000808   0efd     movlw   0xfd             MOVLW 0xfd
00080a   6e93     movwf   0x93, 0           MOVWF _TRISB
                                           _00106_DS_:
                                           ; .line 22; blink.c PORTB=(PORTB^0x02);
00080c   7281     btg     0x81, 0x1, 0     BTG _PORTB, 1
00080e   d7fe     bra     0x80c             BRA _00106_DS_
000810   cfe4     movff   0xfe4, 0xfd9     MOVFF PREINC1, FSR2L
000812   ffd9
000814   0012     return  0                 RETURN

Eu marquei em amarelo, as referências de endereços. Veja que o compilador teve que relocar o código. Isto é, atribuir novos endereços e corrigir os saltos para a nova faixa.

Problemas à vista

Estou usando o SDCC 3.3. Portanto o que eu disser aqui, é somente associado ao SDCC 3.3 (não testei em outras versões).

Como eu disse, o suporte de relocação de código para PIC no SDCC é pobre. Com muita pesquisa você vai cair nas seguintes dicas:
1) --code-loc
2) --ivt-loc
3) usar #pragma code
4) alterar o .lkr do gputils

Cada uma delas tem a sua particularidade.

--code-loc

--code-loc não faz o que diz fazer. Ele relocaria o código para o endereço desejado. Mas isso não ocorre. NUNCA! :( (you boor!)
(sorry, SDCC team, --code-loc is would never be in PIC)


--ivt-loc

--ivt-loc faz o que tem de fazer, mas é somente para tabelas de vetores. Se você tiver alguma função tipo __interrupt. Ele irá relocar o código para os endereços de vetores a partir do ponto informado.

Adicione o trecho de código abaixo ao nosso blink.c

void isr_high()  __critical __interrupt 1 {
PORTA=!PORTA;
}

compile-o
sdcc --use-non-free -p18f252 -mpic16 blink.c

Por fim abra o blink.lst e inspecione o seu conteúdo...
                                           S_blink__isr_high code 0X000008
                                           _isr_high:
                                           ; .line 15; blink.c PORTA=!PORTA;
000008   5080     movf    0x80, 0, 0       MOVF _PORTA, W
00000a   80d8     bsf     0xd8, 0, 0       BSF STATUS, 0
00000c   66e8     tstfsz  0xe8, 0           TSTFSZ WREG
00000e   90d8     bcf     0xd8, 0, 0       BCF STATUS, 0
000010   6a80     clrf    0x80, 0           CLRF _PORTA
000012   3680     rlcf    0x80, 0x1, 0     RLCF _PORTA, F

Agora compile com essa linha de comando:

sdcc --use-non-free -p18f252 -mpic16 --ivt-loc=0x200 blink.c 

O código será relocado:

_isr_high:
                                           ; .line 15; blink.c PORTA=!PORTA;
000208   5080     movf    0x80, 0, 0       MOVF _PORTA, W
00020a   80d8     bsf     0xd8, 0, 0       BSF STATUS, 0
00020c   66e8     tstfsz  0xe8, 0           TSTFSZ WREG
00020e   90d8     bcf     0xd8, 0, 0       BCF STATUS, 0
000210   6a80     clrf    0x80, 0           CLRF _PORTA
000212   3680     rlcf    0x80, 0x1, 0     RLCF _PORTA, F

I FOUND A BUG! SDCC não está gerando RETFIE (return from Interrupt) no final das ISR's quando se usa __naked!  Bom, nada que um __asm RETFIE __endasm; não resolva. Mas fica o alerta!
o --ivt-loc não reloca o main... então falta somente essa parte.

--//--
Nos próximos artigos vamos explorar o #pragma e o .lkr! Tem coisa boa vindo aí!

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 */
}

quinta-feira, 27 de junho de 2013

SDCC: Estrutura de um programa para PIC

Não vou entrar nos detalhes de como programar em C. Se você chegou até aqui, eu presumo que você saiba o mínimo necessário para escrever um "Hello World!" em C para qualquer plataforma. E tutorial para programar em C puro tem aos baldes na internet.

C ou C++? C!!! Assim rezado por Ken Thompson e Dennis Ritchie. SDCC não suporta C++. Então aquelas maravilhas de cin/cout não vai te ajudar em nada. Estamos programando para microcontroladores que têm uma memória absurdamente diminuta, então não dá para ficar "dourando a pílula".

Estrutura Mínima

Basicamente um programa em C para PIC no SDCC tem a seguinte estrutura:

- Cabeçalho (headers inclusion)
- Configuração dos bits do microcontrolador (fuses - somente no módulo principal)
- E a função main()

Óbvio que não fica limitado a isso... por isso eu chamo de estrutura mínima.

A função main é a função que será invocada pelo microcontrolador quando esse inicializar. Simplesmente declarado como

void main() {
/* ... */
}

Exemplo:

#define __16F88
#include "pic16regs.h"

typedef unsigned int config;

config __at _CONFIG1 conf1=_HS_OSC & _PWRTE_OFF & _BOREN_OFF & _BODEN_OFF & _WDT_OFF & _MCLR_ON;

int main()
{
TRISB=0xFD;
while(1) {
PORTB=(PORTB^0x02);
}

}

Cabeçalhos 

Se alguma função, constante, tipos definidos, variáveis externas, etc, não declarados no corpo do programa atual forem utilizados, eles terão que ser declarado antes. Para isso serve os arquivos .h (headers)

Para o SDCC com PIC a estrutura mais comum é definir o tipo do microcontrolador e depois incluir os registros necessários.


#define _p16f88 
#include "pic16regs.h"

...


A declaração acima faz com que seja utilizado as definições de registros/registradores para suporte do PIC16F88 somente!

#define _p18f252
#include "pic18fregs.h"

...

De forma similar, mas para o PIC18F252.


Existem programadores que fazem diferente, por exemplo:

#include "pic14/pic16f88.h" 

que faz a mesma coisa da definição que eu utilizo. Fica a teu critério.

Configuração dos Bits (fuses)

Essa é a parte mais confusa. Dependendo da versão de SDCC você terá dores de cabeça. Estou usando a versão 3.3 - então assumo que você tenha essa versão.

O SDCC da versão 3.2 em diante, procurou fazer alguma compatibilidade com o compilador XC da Microchip. Então, para chips mais modernos (18F em diante) a melhor técnica de declarar os fuses é utilizando a diretiva #pragma config

/* para PIC18F252... */
#define _p18f252
#include "pic18fregs.h"

#pragma config OSC=HS, OSCS=OFF, PWRT=OFF, BOR=OFF, WDT=OFF, CCP2MUX=OFF, LVP=OFF, CP0=OFF, CP1=OFF, CP2=OFF, CP3=OFF

/* ... */

As chaves acima, possuem definições bem definidas nos arquivos .inc que estão nos diretórios do gputils!

Para microcontroladores antigos, ainda vale a antiga forma de declarar, que é mais confusa de todas (pode variar de compilador para compilador).

/* Para PIC16F88 */
#define _p16f88
#include "pic16regs.h"

typedef unsigned int config;

config __at _CONFIG1 conf1=_HS_OSC & _PWRTE_OFF & _BOREN_OFF & _BODEN_OFF & _WDT_OFF & _MCLR_ON;

/* ... */


No caso acima, os registros dos valores e nome das configs, podem ser encontrados no .h respectivo de seu processador (no caso pic16f88.h no diretório de include do SDCC)

NOTA: Dá para usar processadores mais modernos com a notação antiga. Aumenta a confusão e o compilador irá mostrar uma mensagem de warning... mas pode ser ignorado. :)

Free e Non-Free

Alguma coisa mudou e alguém teve que fazer algum ajuste de conduta. Nos novos compiladores SDCC, se você procurar no diretório include ( C:\...\SDCC\include ) você não vai encontrar NENHUM header de pic algum. Somente os wrappers (pic16fregs.h, pic14regs.h, ...). Onde foram parar p16f*, p18f*?

No próprio diretório do SDCC tem um diretório chamado non-free. Lá estão os headers de cada microcontrolador.

VOCÊ NÃO PRECISA FAZER NADA!!! Não precisa copiar os arquivos ou inventar que está organizando o SDCC... não cometa bobagens.

Para fazer compilar o programa, basta passar na linha de comando a flag --use-non-free e pronto. O SDCC vai achar normalmente!

Sem --use-non-free:

c:\Development\SDCC>sdcc -p16f88 -mpic14  blink.c
In file included from blink.c:6:
C:/PROGRA~1/SDCC/bin/../include/pic14/pic16regs.h:163:24: error: pic16f88.h: No
such file or directory
blink.c:14: error 20: Undefined identifier '_CONFIG1'
blink.c:14: error 2: Initializer element is not constant
blink.c:18: error 20: Undefined identifier 'TRISB'
blink.c:20: error 20: Undefined identifier 'PORTB'
blink.c:20: error 20: Undefined identifier 'PORTB'

Deu erro! :(

Com o --use-non-free:

c:\Development\SDCC>sdcc --use-non-free -p16f88 -mpic14  blink.c
message: using default linker script "C:\Program Files (x86)\gputils\lkr\16f88_g.lkr"

c:\Development\SDCC>


Funciona! :)

---

Próximo tópico: Vetores de interrupção

PIColino - Development Board para PIC - Protótipo

Chegou hoje as placas! :) Ficaram fantásticas e tudo conforme eu havia desenhado.

Veja abaixo a foto.

Montei rapidinho pois havia em casa os componentes. O PIC está rodando a 16MHz (depois troco para um cristal de 20MHz).

Por enquanto não tem bootloader, mas já estou estudando formas de desenvolver um que seja facilmente integrado ao SDCC (hoje descobri algumas limitações... conto em outro post).

[Update: 1o. de Agosto] - Agora sim, o PIColino tem um bootloader (que o chamo de Yablo). Leia nesse post as informações do Yablo: http://mmc-zaap.blogspot.com.br/2013/07/yablo-yet-another-bootloader-english.html Está em inglês, e nesse outro local, o projeto no Google Code: https://code.google.com/p/yablo/

Programa de Inauguração

#define __18F252
#include "pic18fregs.h"
#include "delay.h"

#pragma config OSC=HS, PWRT=OFF, BOR=OFF, WDT=OFF, CCP2MUX=OFF, LVP=OFF, CP0=OFF, CP1=OFF, CP2=OFF, CP3=OFF

int main()
{
 TRISB=0x7F;
 while(1) {
  PORTB=(PORTB^0x80);
delay10ktcy(500);
 }
}



Compilado e carregado (via MPLAB/ICD2), esse programa simplesmente irá fazer um led (L) piscar. Veja o video de demonstração


Próximos passos: Bootloader e mais SDCC! Bem vindo Picolino!!!