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.