PIC18F Kernel - 05

Post date: 20/01/2011 12:25:26

Mudança de planos!

Estava portando o último código para a placa e fazendo as alterações necessárias. Surgiram alguns problemas em relação a estrutura que estava utilizando para sincronizar os eventos. O contador de tempo usado no microcontrolador é uma variável unsigned int que é incrementada a cada 1 ms.

Vamos supor dois eventos: P1 agendado para acontecer daqui 10 segundos e P2 agendado para acontecer daqui 50 segundos e que o "relógio" está marcando 45(s) (now_ms = 45.535).

Observem que o evento P2 foi colocado corretamente pois o P2.start = now_ms + 50.000 = 30.000 (acontece um estouro na variável do tipo unsigned int em 65.535). A variável now_ms será incrementada até 55.535, quando acontecerá o evento P1, exatamente 10s depois de agendado. Esta variável agora seguirá até 65.535 e retornará para zero.

No momento do overflow da variável, o tempo decorrido desde o agendamento de P1 foi de (65.535 - 45.535) = 20.000 ms, ou 20 segundos. Como ela foi agendada com delay de 50 segundos é necessário que a variável seja incrementada até 30.000 para passar mais 30 segundos, perfazendo o total de 50s conforme estipulado.

O problema com essa arquitetura é quando temos dois processos que devem ser disparados com tempo muito próximos, ou até mesmo simultâneos.

Supondo que P1 leve 10s (exagero didático) para ser executado. Após a execução de P1 teremos a seguinte linha de tempo

Pergunta: Dada a linha de tempo acima (que é a única coisa que o kernel enxerga) P2 deveria ter sido executado e não foi OU ele foi agendado para acontecer apenas daqui a 50.535 segundos?

Para solucionar esse problema aparentemente temos duas opções:

  1. Criar uma flag indicando se o contador de tempo já passou pelo tempo do processo. Desse modo saberemos se o processo que está atrás do contador na linha de tempo está atrasado ou apenas foi agendado por um tempo muito grande.

  2. Mudar a arquitetura. Ao invés de compararmos o tempo de inicio de cada processo com um contador, cada processo tem seu contador que diminui a cada clock. Quando chegarem ao zero devem ser executados o mais rápido possível.

A segunda opção traz mais algumas vantagens. Olhando a linha de tempo temos dificuldade de saber qual é o próximo evento a ser executado, não basta ver o processo com o menor tempo, devemos fazer a busca do processo mais próximo do tempo atual apenas pelo lado direito (processos que ainda devem ocorrer), levando em conta também os atrasados. Com a segunda opção isso não ocorre, basta procurar a que possui o menor tempo.

Além disso este procedimento facilita a utilização das prioridades. Se dois processos precisam ser executados aos mesmo tempo (ambos chegaram ao zero), aquele com maior prioridade rodará primeiro.

Seguem abaixo as duas opções (para efeito de comparação): primeiro o kernel com contador de tempo free-running, e depois cada processo com seu contador (o campo start é decrementado até chegar a zero).

Primeira versão: checando quando o valor de start iguala ao tempo do free running clock (now_ms)

1: void ExecutaKernel(void)

2: {

3: char j;

4: char prox;

5: unsigned int dj, dprox;

6: processo tempProc;

7: for(;;) {

8: if (ini != fim){

9: j = (ini+1)%SLOT_SIZE;

10: prox = ini;

11: while(j!=fim){

12: //Calculando o delta que falta

13: if(vetProc[j].start > now_ms){

14: dj = vetProc[j].start - now_ms;

15: }else{//start está antes do tempo, tem que levar em conta o overflow

16: dj = 0xffff -(now_ms - vetProc[j].start);

17: }

18: if(vetProc[prox].start > now_ms){

19: dprox = vetProc[prox].start - now_ms;

20: }else{

21: dprox = 0xffff -(now_ms - vetProc[j].start);

22: }

23:

24: //menor tempo

25: if (dj < dprox){

26: prox = j;

27: }

28: j = (j+1)%SLOT_SIZE;//para poder incrementar e ciclar o j

29: }

30: //troca e coloca o processo com menor tempo como o proxima

31: tempProc.Func = vetProc[prox].Func;

32: tempProc.start = vetProc[prox].start;

33: tempProc.t_ms = vetProc[prox].t_ms;

34:

35: vetProc[prox].Func = vetProc[ini].Func;

36: vetProc[prox].start = vetProc[ini].start;

37: vetProc[prox].t_ms = vetProc[ini].t_ms;

38:

39: vetProc[ini].Func = tempProc.Func;

40: vetProc[ini].start = tempProc.start;

41: vetProc[ini].t_ms = tempProc.t_ms;

42:

43: //now_ms atualiza sozinho dentro da int

44: while(now_ms < vetProc[ini].start){

45: //adicionar sleep aqui, acorda na int.

46: }

47: //retorna se precisa repetir novamente ou não

48: if ( (*(vetProc[ini].Func))() == REPETIR ){

49: AddProc(&(vetProc[ini]));

50: }

51: //próxima função

52: ini = (ini+1)%SLOT_SIZE;

53: }

54: }

55: }

Segunda versão: verificando quando o start chega à zero

1: void ExecutaKernel(void){

2: unsigned char j;

3: unsigned char prox;

4: processo tempProc;

5: for(;;){

6: if (ini != fim){

7: //Procura a próxima função a ser executada com base no tempo

8: j = (ini+1)%SLOT_SIZE;

9: prox = ini;

10: while(j!=fim){

11: if (vetProc[j].start < vetProc[prox].start){

12: prox = j;

13: }

14: j = (j+1)%SLOT_SIZE;//para poder incrementar e ciclar o j

15: }

16: //troca e coloca o processo com menor tempo como o proxima

17: tempProc.Func = vetProc[prox].Func;

18: tempProc.start = vetProc[prox].start;

19: tempProc.t_ms = vetProc[prox].t_ms;

20:

21: vetProc[prox].Func = vetProc[ini].Func;

22: vetProc[prox].start = vetProc[ini].start;

23: vetProc[prox].t_ms = vetProc[ini].t_ms;

24:

25: vetProc[ini].Func = tempProc.Func;

26: vetProc[ini].start = tempProc.start;

27: vetProc[ini].t_ms = tempProc.t_ms;

28: while(vetProc[ini].start!=0){

29: //adicionar sleep aqui, acorda na int.

30: }

31:

32: //retorna se precisa repetir novamente ou não

33: if ( (*(vetProc[ini].Func))() == REPETIR ){

34: AddProc(&(vetProc[ini]));

35: }

36: //próxima função

37: ini = (ini+1)%SLOT_SIZE;

38: }

39: }

40: }

Pode-se notar claramente que o segundo código é mais facilmente compreendido principalmente por não sofrer do problema do overflow. Em compensação a rotina que gera a base de tempo se torna um pouco mais complexa para o segundo caso. No primeiro ela apenas precisa incrementar a variável now_ms a cada milissegundo

1: void isr(void) interrupt 1 {

2: if (BitTst(INTCON,2)) {

3: ResetaTimer(1000); //reseta com 1ms

4: now_ms++;

5: }

6: }

No segundo caso a rotina de interrupção precisa decrementar a variável start de todos os processos:

1: void isr1(void) interrupt 1{

2: unsigned char i;

3: if (BitTst(INTCON,2)) {

4: ResetaTimer(1000); //reseta com 1ms

5: i = ini;

6: while(i!=fim){

7: if((vetProc[i].start)>0){

8: vetProc[i].start--;

9: }

10: i = (i+1)%SLOT_SIZE;

11: }

12: }

13: }

Parte da complexidade da função executa kernel foi passada para a função de interrupção. Apesar disso, esta alteração permitirá no futuro a montagem de um sistema de prioridades de maneira muito mais simples.

A versão 0.4 do kernel já está funcionando na placa (processador PIC18F4550) mas deixarei para apresentar o código no próximo artigo. Existem algumas alterações que foram necessárias por causa do compilador que estou usando (SDCC) e esse assunto merece um artigo próprio.

Até o próximo.