PIC18F Kernel - 03

Post date: 27/12/2010 23:17:53

No artigo anterior comentei sobre ponteiros de função e porque são úteis/necessários para montar um kernel. Já no artigo de hoje abordaremos dois conceitos: structs e buffers circulares.

Structs

Quando precisamos de reunir diversas informações sobre um mesmo conceito em uma única variável fazemos o uso (em linguagem C) de structs. As structs podem ser comparadas com vetores onde cada posição permite o armazenamento de variáveis diferentes.

Pegando o exemplo emprestado da wikipedia:

#include <stdio.h>

struct pessoa

{

unsigned short int idade;

char nome[51]; /* vetor de 51 chars para o nome */

unsigned long int rg;

}; /* estrutura declarada */

int main(void)

{

struct pessoa exemplo = {16, "Fulano", 123456789}; /* declaração de uma variável tipo struct pessoa */

printf("Idade: %hu\n", exemplo.idade);

printf("Nome: %s\n", exemplo.nome);

printf("RG: %lu\n", exemplo.rg);

return 0;

}

Podemos notar que é possível criar um tipo novo, no exemplo pessoa, e utilizá-lo como se fosse um tipo nativo. Para termos acesso a cada campo da função devemos usar um ponto separando a variável do campo do código: exemplo.idade retornará a idade que está armazenada na variável exemplo do tipo pessoa.

Buffers circulares

Buffers são regiões de memória que servem para armazenar dados temporários. Os buffers circulares podem ser implementados utilizando uma lista lincada onde o último elemento se conecta com o primeiro. O problema dessa abordagem é o gasto extra de memória que será inserido para colocar os ponteiros. A maneira de resolvermos esta situação é utilizando apenas um vetor com dois indices, indicando o início e o fim da lista.

Representação de um buffer circular implementado usando um vetor de 7 posições. (Fonte wikipedia)

O problema desta abordagem é conseguir definir quando o vetor está cheio ou vazio, já que em ambos os estados o indicador de inicio e fim estão no mesmo local.

Existem pelo menos 4 alternativas de se resolver este problema: manter um slot sempre aberto, usar um indicador de cheio/vazio, contar a quantidade de leituras/escritas ou utilizar os índices absolutos. Visando a simplificação optamos por manter sempre um slot vazio.

Kernel versão 0.2

Nesta versão faremos duas alterações principais: o vetor de processos passará a ser um vetor de uma estrutura denominada processo e os processos retornarão um valor. Este valor indicará se o processo em questão precisa ser executado novamente ou não. Deste modo o kernel será capaz de rodar as funções corriqueiras de forma automática e se alguma condição especial for encontrada é possível, em tempo de execução, pedir que o kernel execute uma função apenas uma vez.

Vamos então às definções, variáveis e protótipos:

#include "stdio.h"

//pelo menos 1 a mais que a quantidade de funcoes

#define SLOT_SIZE 4

//códigos de retorno

#define FIM_OK 0

#define FIM_FALHA 1

#define REPETIR 2

static int tst1(void);

static int tst2(void);

static int tst3(void);

//declaracao do tipo ponteiro para funcao

typedef int(*ptrFunc)(void);

//estrutura do processo

typedef struct {

ptrFunc Func;

} processo;

//variaveis do kernel

static processo vetProc[SLOT_SIZE];

static int ini;

static int fim;

//funcoes do kernel

static int InicializaKernel(void);

static int AddProc(processo newProc);

static void ExecutaKernel(void);

Conforme foi citado, agora todos os processos retornam um int que servirá de indicador se:

  1. a função foi executada corretamente e não quer ser reexecutada (#define FIM_OK 0)

  2. a função teve algum erro na execução (#define FIM_FALHA 1)

  3. a função foi executada corretamente e quer ser reexecutada (#define REPETIR 2)

Além disso foi criado o tipo processo que, por enquanto, possui apenas um ponteiro para função do tipo int ptrFunc(void). Notar que o tamanho do slot deve ser no mínimo uma unidade maior que o total de funções que estarão no kernel simultâneamente. Cuidado, este número é diferente da quantidade total de funções, pode ser inclusive maior. Ao decorrer dos artigos abordarei como mensurar esse número de maneira mais precisa.

O código principal praticamente não teve alteração, apenas no método de adicionar processos no kernel, que agora são feitos via estrutura processo, e não mais pelo ponteiro de função ptrFunc.

int main(void)

{

processo p1 = {tst1,1};

processo p2 = {tst2,2};

processo p3 = {tst3,3};

printf("Inicio\n\n");

InicializaKernel();

if (AddProc(p1) == FIM_OK)

{

printf("P1 adicionado\n");

}

if (AddProc(p2) == FIM_OK)

{

printf("P2 adicionado\n");

}

if (AddProc(p3) == FIM_OK)

{

printf("P3 adicionado\n");

}

printf("\nExecutando o kernel\n\n");

ExecutaKernel();

printf("\nFim\n");

return 0;

}

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

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

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

Além das alterações mencionadas, a função AddProc() agora retorna um sinal indicando se foi bem sucedida ou não. Isto se faz necessário pois temos uma quantidade limitada no buffer dos processos. Com relação às tarefas, a única alteração é o parâmetro de retorno. No exemplo apenas as funções tst1() e tst3() querem ser reexecutadas.

static int InicializaKernel(void){

ini = 0;

fim = 0;

return FIM_OK;

}

static int AddProc(processo newProc)

{

//para poder adicionar um processo tem que existir espaco

//o fim nunca pode coincidir com o inicio

if (((fim+1)%SLOT_SIZE)!= ini )//se incrementar nessa condicao fim vai ficar igual a ini

{

vetProc[fim] = newProc;

fim++;

if (fim>=SLOT_SIZE) //loop da lista

{

fim = 0;

}

return FIM_OK; //sucesso

}

return FIM_FALHA;//falha

}

static void ExecutaKernel(void)

{

int i,j;

for(i=0; i<10;i++) //aqui entraria o loop infinito

{

if (ini != fim)

{

printf("Ite. %d, Slot. %d: ",i, ini);

//executa e vê se a funcao quer ser re-executada

if ( (*(vetProc[ini].Func))() == REPETIR )//retorna se precisa repetir novamente ou não

{

//coloca a funcao no fim da lista, esta posicao esta sempre livre.

vetProc[fim] = vetProc[ini];

fim++;

if (fim>=SLOT_SIZE) //loop da lista

{

fim = 0;

}

}

//próxima funcao

ini++;

if (ini>=SLOT_SIZE) //loop da lista

{

ini = 0;

}

}

}

}

A função AddProc() agora deve verificar onde inserir o processo em questão, lembrando que o buffer é cíclico e deve possuir uma posição sempre vazia.

A maior alteração está por conta da função ExecutaKernel(). Antes ela apenas corria o vetor executando as funções. Agora ela verifica o parâmetro de retorno da função e se a função precisa ser re-executada. Em caso afirmativo ela recoloca a função no final do buffer e processa a próxima função. Obs: O printf serve apenas como informativo, indicando qual a iteração atual e qual a função que está sendo executada.

A seguir temos a saída do programa acima. Notar que a função 2 só é executada uma única vez. Posteriormente as funções 1 e 3 são executadas alternadamente mas sempre obedecendo a posição/slot correta no buffer.

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

Inicio

P1 adicionado

P2 adicionado

P3 adicionado

Executando o kernel

Ite. 0, Slot. 0: 1

Ite. 1, Slot. 1: 22

Ite. 2, Slot. 2: 333

Ite. 3, Slot. 3: 1

Ite. 4, Slot. 0: 333

Ite. 5, Slot. 1: 1

Ite. 6, Slot. 2: 333

Ite. 7, Slot. 3: 1

Ite. 8, Slot. 0: 333

Ite. 9, Slot. 1: 1

Fim

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

No próximo artigo abordaremos os efeitos da condição temporal, como garantir que um processo será executado a cada X milissegundos.