PIC18F Kernel - 02

Post date: 22/12/2010 22:44:27

Este é o segundo artigo sobre o processo de criação de um kernel para o microcontrolador PIC18F. Por enquanto vou montar o kernel no desktop para que as pessoas que estiverem acompanhando esses tópicos possam também replicar os resultados. Vou deixar apenas para o fim as questões relativas ao hardware ou ao assembler.

Como citei no artigo anterior para desenvolver o kernel é essencial o uso de ponteiros de função. Vou passar antes uma revisão sobre o tópico.

Em algumas situações queremos que o programa possa escolher qual função deseja executar, por exemplo num editor de imagens: Quero que o editor possa usar a função blur ou a função sharpen na imagem desejada:

Montando as funções teríamos:

//declaracao do tipo ponteiro para função

typedef imagem (*ptrFunc)(imagem nImg);

imagem Blur(imagem nImg){

// implementação da função

}

imagem Sharpen(imagem nImg){

// implementação da função

}

//chamado pelo editor de imagens

imagem ExecutaProcedimento(ptrFunc nFuncao, imagem nImg){

imagem temp;

temp = (*nFuncao)(nImg);

return temp;

}

Como podemos perceber pelo código a função ExecutaProcedimento() recebe dois parâmetros: um é a função e o outro a imagem que será processada. Essa função tem que ser do tipo ptrFunc, ou seja, receber apenas um parâmetro: imagem e retornar imagem. O conjunto dos tipos dos parâmetros que a função recebe mais o tipo de retorno da função damos o nome de assinatura.

A atribuição de uma função a um ponteiro de função, ou passagem por parâmetros como vimos no exemplo, só pode ser feita se ambas as funções tiverem a mesma assinatura. Percebemos que tanto Blur() quanto Sharpen() obedecem este quesito. Por isso é possível executar o código a seguir:

//...

imagem nImagem = recebeImagemCamera();

nImagem = ExecutaProcedimento(Blur, nImagem);

nImagem = ExecutaProcedimento(Sharpen, nImagem);

//...

As funções Blur() e Sharpen() são passadas como se fossem variáveis para serem usadas apenas internamente da função ExecutaProcedimento.

Por se tratar de um ponteiro é necessário de-referenciar a variável antes de chamar a função como no código abaixo.

temp = (*nFuncao)(nImg);

Notar que após a de-referência são passados os parâmetros como numa função qualquer. Além da passagem por parâmetro a função pode ser armazenada. Basta se criar uma variável do tipo do ponteiro da função (prtFunc). Obs: apenas o endereço da função é armazenado, não são criadas cópias da função.

Após esta explanação inicial vamos ao que interessa: Kernel 0.1

O código pode ser dividido em três seções: 1 - processos, 2 - kernel, 3 - rotina principal/inicialização

1 - Processos: As funções que serão executadas pelo kernel (tst1(), tst2(), tst3()) passarão a ser denominadas processos, com idéia semelhante aos processos de um desktop. Além disso todos os processos têm que ter a mesma assinatura do ponteiro ptrFunc, no caso void F(void);

#include "stdio.h"

//protótipos dos "processos"

static void tst1(void);

static void tst2(void);

static void tst3(void);

//"processos"

static void tst1(void) { printf("1\n");}

static void tst2(void) { printf("22\n");}

static void tst3(void) { printf("333\n");}

//declaracao do tipo ponteiro para funcao

typedef void(*ptrFunc)(void);

2 - Kernel: Este primeiro kernel possui três funções: uma para se inicializar, uma para adicionar processos no pool de processos (vetFunc), que é um vetor estático de tamanho 4 de ponteiros para função, e uma para executar o kernel em si. Em geral a função que executa o kernel possui um loop infinito. Nesta primeira versão apenas executamos as funções uma vez e encerramos o sistema. O kernel que apenas executa as funções que lhe são passadas, uma a uma, na ordem em que foram passadas. Não existe nenhum outro tipo de controle. Este armazenamento e posterior chamado só é possível através do uso de ponteiros de função.

O tamanho do pool (vetFunc) é definido estaticamente. Qualquer outra implementação (lista lincada, malloc, etc) irá consumir muitos recursos do sistema tornando (possivelmente) o código grande demais para caber no microcontrolador escolhido. Por isso é necessário fazer alguns testes antes para se conhecer o tamanho ideal do pool para sua aplicação.

//variaveis do kernel

static ptrFunc vetFunc[4];

static int fim;

//protótipos das funções do kernel

static void InicializaKernel(void);

static void AddKernel(ptrFunc newFunc);

static void ExecutaKernel(void);

//funções do kernel

static void InicializaKernel(void){

fim = 0;

}

static void AddKernel(ptrFunc newFunc){

if (fim <4){

vetFunc[fim] = newFunc;

fim++;

}

}

static void ExecutaKernel(void){

int i;

for(i=0; i<fim;i++){

(*vetFunc[i])();

}

}

3 - Rotina Principal/Inicialização: Para utilizar o kernel desenvolvido é bastante simples, com os processos já implementados de acordo com a assinatura padrão basta: 1 - inicializar o sistema, 2 - adicionar os processos que devem ser executados na ordem em que serão executados e por fim 3 - executar o kernel.

int main(int argc,char **argv){

printf("Inicio\n\n");

InicializaKernel();

AddKernel(tst1);

AddKernel(tst2);

AddKernel(tst3);

ExecutaKernel();

printf("\nFim\n");

return 0;

}

No próximo artigo veremos como criar uma estrutura de controle mais apurada, que permite que os processos possam ser automaticamente re-executados ao fim de cada ciclo se for necessário. Será apresentado também uma modificação no pool de processos de modo a permitir posteriormente a utilização de prioridade e requisitos temporais para cada processo.

Até o próximo artigo.

-----------------------

Obs: Todo o código foi testado com o GCC sob plataforma Linux. Por usar apenas termos/conceitos compatíveis com ISO-C deve funcionar em qualquer compilador para desktop.