Página 1 de 3

Análise de abstração : um belo desafio..

MensagemEnviado: 29 Nov 2008 04:10
por jeanfernandes
Bom pessoal.
Antes de mais nada, queria deixar claro que a ideia eh trazer conhecimento com o intuito de ajudar. Sendo assim o conceito de abstração com estudo do caso pode ser levado na brincadeira. Pessoas podem achar que eh perca de tempo. Beleza. Eu acho que nao é e tenho razoes para pensar assim o que nao vem ao caso.

O problema:

Como abstrair o gerenciamento de um dispositivo externo qualquer, com base em primitivas de acesso (init, read, write, etc...) considerando:

a) A diversidade do dispositivo controlador, no nosso caso aqui um processador (pic, arm, 8051, avr...)

b) O modelo da placa real, que implica em utilizar diferentes metodos de acesso para o gerenciamento, a saber:

b.1) Na possibilidade de usar um periferico interno do processador para acessar o dispositivo ou;
b.2) Acessar o mesmo via GPIO emulada.

c) Esse é só para dificultar mais ainda. O dispositivo no caso de GPIO emulada, pode ser ligado no barramento de gpio dependendo do gosto do usuario, ou seja (b.2) nao implica em conexao fisica fixa, depende da necessidade.

Traduzindo em estudo de caso:

Vamos escolher algo que todo mundo conhece pra deixar claro a forma de fazer.
Uma memoria eeprom 24c01, que pode ser acessada via spi. Essa spi pode partir de um recurso interno do processador (se tiver disponivel) ou pode ser feita na mão, escolhendo pinos GPIO do processador pra fazer o acesso.

Como fazemos hoje. Tomando por base a linguagem C so para fixar, hoje a gente faz da seguinte forma:

A) Usa um main.c, da um include do processador, define os gpio, inicia os gpio (direcao, estado), e escreve um conjunto de rotinas, start, stop, read, write, etc....beleza...funciona..mas se oce trocar de placa..voce vai la e muda o gpio no main.c (ou main.h se for o caso) e tudo fica bem. beleza...se isso pra voce eh o seu mundo nem precisa ler o resto.

- Problema associado : voce tem um conjunto de projetos com o mesmo processador, que estao evoluindo e tals. Se tem N projetos diferentes tem N ee24xx.c ee24xx.h diferentes, um em cada pasta de projeto. Qq atualizacao ou insercao de funcoes, caso isso queira ser aplicacado na linha de produção é um parto. Tem gente que suporta isso, mesmo N sendo grande (tipo uns 50 projetos diferentes).

- Ah na placa X1, com processador Y1, eu usei o spi interno. Na placa X2, f****, tive que fazer com GPIO emulado, em 3 pinos da PORTA Z1. Na placa X3, mudou.... vai ser ligado na porta Z2.
Na placa X4 ixe, mudou o processador. Mi fu*....??? Há controvérsias.

Voce fica insistindo: poxa basta criar um conjunto de arquivos por pasta de projeto, por versao que resolve. Resolve mesmo ? Mesmo que quando a quantidade de projetos aumente, com varias versoes e tals... Um iria me dizer...ah usa o CVS SVN pra controlar a versao. Puxa aversao do cara e usa. Pronto acabou ! ehehehehe calma que nem comecei o samba....

Bom, amanha voce descobre que por uma questao qq voce precise corrigir um defeitim la em ee24xx.c e agora ? Abre todos os ee24xx.c e corrige ? o CVS ja resolve isso ?

Bom ae ... muda-se processador ... comeca tudo de novo ? o que importa nao eh a eficiencia.... e se livrar...sera mermo ?

Ate onde voce pode criar camadas de abstracao, para evitar muita coisa associada aos metodos convencionais de fazer a coisa ?

(fica pra proxima...) ehehhe

MensagemEnviado: 29 Nov 2008 04:44
por proex
Por esses e outros motivos inventaram o Sistema Operacional, a Plataforma Padrão, Plug and Play etc.

Mas essas coisas são pra processadores de médio e grande porte.

Para os retardados mentais (MCUs de 8 bits) nao tem como mesmo, cada projeto é um projeto e cada placa é uma placa.

MensagemEnviado: 29 Nov 2008 09:25
por guest2003
Bom com CVS da pra fazer isso sem problema :)

Mas você não acredita... então complica.

[]'s
cya

MensagemEnviado: 29 Nov 2008 14:26
por msamsoniuk
bom, eu estes dias eu encontrei o jean lah na e-cracolandia! fumamos um ae e entao discutimos um pouco estas ideias... de fato, em um microkernel eh comum imaginar o conceito cliente-servidor quando se pensa que um device driver serve recursos para uma aplicacao cliente:

aplicacao (cliente) <-> device driver (servidor)

eventualmente, um device driver pode nao ser realmente o servidor real. pode ser apenas uma camada intermediaria, que simplifica a interface de diversos dispositivos diferentes em uma API mais comum. neste caso, poderiamos pensar em um gateway:

aplicacao (cliente) <-> api comum (gateway) <-> api especifica (servidor)

no exemplo de uma determinada eeprom spi, poderiamos pensar em:

aplicacao <-> api generica <-> api eeprom <-> api spi <-> api gpio

se trocarmos o layout de pinos em uma placa, soh mexemos na api de gpio. se trocarmos de processador, provavelmente mexemos apenas na api spi e api gpio.

supondo que trocamos a eeprom spi por uma flash nand, teriamos:

aplicacao <-> api generica <-> api nand <-> api gpio

a aplicacao e api generica em principio nao mudam. eh claro, para as coisas encaixarem corretamente uma na outra, eh necessario definir uma api onde vc pluga uma coisa na outra :)

uma forma logica seria definir funcoes genericas que todo bloco deve possuir:

- funcao para criar uma sessao com um dispositivo (por exemplo: open)
- funcao para configurar um dispositivo (por exemplo: ioctl)
- funcao para ler dados de um dispositivo (por exemplo: read)
- funcao para escrever dados em um dispositivo (por exemplo: write)
- funcao para finalizar uma sessao com um dispositivo (por exemplo: close)

tomando este exemplo, se eu quero mandar e receber coisas pela spi, teria algo como:

Código: Selecionar todos
char tx_buffer[20];
char rx_buffer[20];

spi_open();

while(spi_read(rx_buffer,20))
{
  app_processa_resposta(rx_buffer,tx_buffer);
  spi_write(tx_buffer,20);
}


neste caso, nao nos preocupamos na aplicacao como a interface spi era funcionar. eh requisito para o codigo que vc linke um driver spi que disponha dos seguintes simbolos:

- spi_open
- spi_read
- spi_write

mas nao estamos falando de nenhum processador em particular.

vamos imaginar dois casos de implementacao: um arm9-power-vrx++ e um hc908.

no primeiro caso, digamos que a nxp-bugs tornou o dispositivo spi do componente inutil, de modo que teremos que usar gpio! vou ilustrar apenas o caso do spi_read:

Código: Selecionar todos
/* state machines, definicoes, etc */

enum {

  SPI_SM_READ_START=0,
  SPI_SM_READ_BIT0,
...
  SPI_SM_READ_END
};

/* funcoes intrinsicas do driver para arm9-power-vrx++ */

unsigned char spi_read_remendex()
{
  unsigned char data = 0;
  for(state = 0; state != SPI_SM_READ_END; state++)
  {
    switch(state)
    {
      ...
      case SPI_SM_READ_BIT4:
        data = (data<<1) + gpio_read(SPI_PIN_MISO);
        break;
      ...
    }
  }
  return data;
}

/* funcoes visiveis pela api padrao */

int spi_read(char *rx_buffer,int size)
{
  int i;
  for(i=0;i!=size;i++)
    rx_buffer[i] = spi_read_remendex();
  return size;
}


e por aih vai. note que o driver spi neste caso fala com a aplicacao e tambem com o driver de gpio. muito provavelmente, durante o spi_open eh feito tambem um gpio_open e diversos gpio_ioctl, com o objetivo de reservar e configurar os recursos dos pinos de gpio.

no caso do hc908, nao seria muito diferente com um hardware de recepćão SPI:

Código: Selecionar todos
/* funcoes intrinsicas do driver para hc908 */

unsigned char spi_read_byte()
{
  while(!HC908_SPI_RXREADY);

  return HC908_SPI_RX;
}

/* funcoes visiveis pela api padrao */

int spi_read(char *rx_buffer,int size)
{
  int i;
  for(i=0;i!=size;i++)
    rx_buffer[i] = spi_read_byte();
  return size;
}


obviamente, da mesma forma, spi_open() ainda eh importante, pois eh necessario configurar o baudrate deste hardware SPI.

para a aplicacao em si, indifere quem ela vai usar. de fato, isso eh definido apenas durante a linkagem.

linkagem para arm9-power-vrx++:

OBJS = app.o spi_arm.o gpio_arm.o

linkagem para hc908:

OBJS = app.o spi_hc908.o

conforme vc vai trabalhando, vc pode pensar em outras expansoes. por exemplo, o spi_read_remendex nao deixa de ser uma maquina de estado generica para uma spi em qq processador com gpio. entao vc poderia eventualmente separar em arquivos diferentes e ter:

linkagem para arm9-power-vrx++:

OBJS = app.o spi_emu.o gpio_arm.o

linkagem para hc908qy/qt:

OBJS = app.o spi_emu.o gpio_hc908.o

linkagem para hc908gr/gp:

OBJS = app.o spi_hc908.o

assim vc vai reaproveitando o codigo. ateh entao pensamos em casos como:

aplicacao <-> api spi <-> spi emu <-> gpio

para o caso de uma eeprom, seria simplesmente:

aplicacao <-> eeprom api <-> spi api <-> spi emu <-> gpio

ao inves de escrever e ler direto na spi, a aplicacao usa uma abstracao para a eeprom. similarmente, a interface seria:

- eeprom_open
- eeprom_read
- eeprom_write

assim, vamos chutar um caso elaborado. um arm9 com eeprom spi da dallas e um hc908gp com uma eeprom da xilinx. que salada!

configuracao de linkagem para o arm:

OBJS = app.o eeprom_dallas.o spi_emu.o gpio_arm.o

configracao para o hc908gp:

OBJS = app.o eeprom_xilinx.o spi_hc908.o

em essencia: vc esta componentizando tudo e, para as coisas funcionarem, vc adota interfaces padronizadas entre os componentes.

para todos os processadores diferentes, a spi vai ter a mesma interface:

- spi_open
- spi_read
- spi_write, etc

se o cara tem spi por hardware ou nao, eh caixa preta! se vc troca de processador, eh caixa preta... eh o poder das "interfaces" tipo caixa preta... vc soh tem que linkar o cara certo e pronto! :)

as proximas discussoes na e-cracolandia serao sobre escalonamento de processos, timers e message queues ;D hahaha

MensagemEnviado: 29 Nov 2008 17:32
por msamsoniuk
uma outra coisa que eu ainda estou testando eh a situacao de encapsular diversos recursos em uma unica funcao, que seria uma especie de gateway entre os metodos internos e os componentes externos, atraves de mensagens e maquinas de estado. por exemplo, para uma interface spi seria algo mais ou menos assim:

Código: Selecionar todos
/* estados usados em todas as tasks */
enum
{
  STATE_DEFAULT,
  STATE_IDLE,
  STATE_READ,
  STATE_WRITE,
  STATE_BUSY
};

/* task para spi */

void spi_task(Message *msg)
{
  static state = STATE_DEFAULT;

  if(state  == STATE_DEFAULT)
  {
      spi_init_interface(msg->init_parameters);
      state = STATE_IDLE;
  }
  if(state == STATE_IDLE)
  {
    msg->state = STATE_BUSY;
    if(msg->state == STATE_READ)
    {
      msg->size = spi_read(msg->data,msg->size);
    }
    if(msg->state == STATE_WRITE)
    {
      msg->size = spi_write(msg->data,msg->size);
    }
    msg->to = msg->from;
    msg->from = SPI_TASK;
    send(msg);
    state = STATE_IDLE;
  }
}


essencialmente eh um componente auto-gerenciavel por uma maquina de estados. ele nasce com o estado DEFAULT e se inicializa, ficando entao em IDLE. conforme a mensagem que ele recebe, ele passa para BUSY e, no exemplo, foi feito um IO blocante, entao ele recebe ou envia um buffer pela spi, envia a resposta para quem pediu e entao volta para estado IDLE. enquanto ele esta BUSY, obviamente, qq mensagem enviada para ele nao sera recebida, ficando na queue e sendo reenviada mais tarde.

no caso do aplicativo cliente, eh muito similar, mas ele jah comeca em IDLE pq nao precisa de inicializacao, digamos:

Código: Selecionar todos
/* em algum lugar, uma lista das tasks, claro */

enum
{
  APP_TASK,
  SPI_TASK,
  TIME0_ISR.,
  ...
};

/* task cliente */

void app_task(Message *msg)
{
  char rx_buffer[20];
  static state = STATE_IDLE;

  if(state == IDLE)
  {
    state = STATE_BUSY;
    msg->data = rx_buffer;
    msg->size = 20;
    msg->from = APP_TASK;
    msg->to = SPI_TASK;
    msg->state = STATE_READ;   
    send(msg);
  }
  if(state == STATE_READ)
  {
    printf("recebido da spi %d bytes: %s\n",msg->size,msg->data);
    free_message(msg);
    state = STATE_IDLE;
  }
}


pq este mecanismo parece interessante? pq permite escalonar processos atraves de maquinas de estado e queues. no caso da aplicacao, se ela esta no estado padrao IDLE, ela constroi uma mensagem, envia e passa para BUSY. qq mensagem que chegar para ela, nao sera tratada, pois ela esta BUSY e ele o fara mais tarde, quando o task spi responder e permitir ela voltar para IDLE.

com este mecanismo, conseguimos tambem gerenciar timers, que passam a ser mensagens enviadas por uma interrupcao ou task. em um exemplo simples, bastaria enviar a mensagem:

Código: Selecionar todos
interrupt timer0
{
  Messagem *msg;
  msg = new_message();
  msg->from = TIMER0_ISR;
  msg->to = APP_TASK;
  send(msg); 
}


soh um exemplo, claro. em um caso mais realista, na verdade, o timer enviaria uma mensagem de timer periodico para uma task de timer e ela entao faria a contabilidade dos timers criados por outras tasks. neste caso, conforme expirasse um timer, seria entao gerada uma mensagem para a aplicacao destino.

evidentemente, o primeiro exemplo com a spi blocando a execucao nao eh um bom exemplo, pq enquanto a spi esta trabalhando todo o resto do sistema para. uma solucao mais inteligente seria a spi processar requisicoes menores. uma ideia seria a seguinte:

Código: Selecionar todos
void spi_task(Message *msg)
{
  static int state = STATE_DEFAULT;
  static int count = 0;

  if(state  == STATE_DEFAULT)
  {
      spi_init_interface(msg->init_parameters);
      state = STATE_IDLE;
  }
  if(state == STATE_IDLE)
  {
    msg->state = msg->state;
    count = 0;
  }
  if(state == STATE_READ)
  {
    if(count!=msg->size)
      msg->data[count++] = spi_read_byte();
  }
  if(msg->state == STATE_WRITE)
  {
    if(count!=msg->size)
      spi_write_byte(msg->data[count++]);
    else
      state = STATE_DONE;
  }
  if(state == STATE_DONE)
  {
    msg->to = msg->from;
    msg->from = SPI_TASK;
    send(msg);
    state = STATE_IDLE;
  }
}


neste caso ele recebe a mensagem se estiver em IDLE e passa para READ ou WRITE, processa um unico byte ateh atingir o contador e entao passa para estado DONE, o que dispara a mensagem de retorno. com isso, eh possivel enviar e receber um numero arbitrario de bytes sem se preocupar com o escalonamento de outros processos.

mas no caso de dispositivos emulados cuja performance eh critica, a coisa pode nao ser tao simples. por exemplo, eu desenvolvi um conjunto de rotinas para emular uma uart no hc908 certa vez e o resultado ficou mais ou menos este:

Código: Selecionar todos
enum {

   STATE_IDLE,
   STATE_STOP,
        STATE_DATA0,
        STATE_DATA1,
        STATE_DATA2,
        STATE_DATA3,
        STATE_DATA4,
        STATE_DATA5,
        STATE_DATA6,
        STATE_DATA7,
        STATE_START
};

volatile unsigned char TX_STATE = STATE_IDLE;
volatile unsigned char TX_DATA;

volatile unsigned char RX_STATE = STATE_IDLE;
volatile unsigned char RX_DATA;

void timer(void) interrupt 6 {

   TOF = 0;

        switch(RX_STATE) {
               
        case STATE_START:
           if(PTA0) return;
               RX_DATA=0;
               break;
   case STATE_DATA0:
   case STATE_DATA1:
   case STATE_DATA2:
   case STATE_DATA3:
   case STATE_DATA4:
   case STATE_DATA5:
        case STATE_DATA6:
        case STATE_DATA7:
           RX_DATA>>=1;
           if(PTA0) RX_DATA|=0x80;
              break;
   case STATE_STOP:
      break;
   case STATE_IDLE:
           switch(TX_STATE) {
 
              case STATE_START:
                     DDRA0 = 1;
                     PTA0 = 0;
                     break;
                  case STATE_DATA0:
                  case STATE_DATA1:
                  case STATE_DATA2:
                  case STATE_DATA3:
                  case STATE_DATA4:
                  case STATE_DATA5:
                  case STATE_DATA6:
                  case STATE_DATA7:
                     PTA0=TX_DATA&1;
                     TX_DATA>>=1;
                     break;
                  case STATE_STOP:
                      PTA0 = 1;
                      DDRA0=0;
                      break;
         case STATE_IDLE:
            return;
      }
      TX_STATE--;
      return;         
   }               
               
        RX_STATE--;           
        return;
}

void putchar(char c) {

    while(TX_STATE|RX_STATE);

    TX_DATA=c;
    TX_STATE=STATE_START;

    if(c=='\n') putchar('\r');
}

char getchar() {

    while(RX_STATE|TX_STATE);

    RX_STATE=STATE_START;
   
    while(RX_STATE);
   
    return RX_DATA;
}


essencialmente, putchar e getchar aguardam a emulacao entrar em um estado disponivel e entao enviam ou recebem algo. no caso do timer, ele eh configurado no baud rate desejado e a cada rodada do timer um estado eh processado. por uma questao de simplicidade, eu coloco as maquinas em um estado inicial e entao os estados vao decrementando sucessivamente ateh chegar no estado final, que eh zero. entao, se o estado eh zero, eh pq ele esta idle, do contrario esta busy fazendo algo.

MensagemEnviado: 29 Nov 2008 18:00
por proex
Foi exatamente isso que eu quis dizer!!!

MensagemEnviado: 30 Nov 2008 10:50
por andre_luis
Interessante essa discussão.

Se bem entendi, essa última idéia parece meio que o conceito de implementar um driver logo acima da BIOS para servir como uma 'ponte'. Isso eu já realizo nos meus programas em C. Eu coloco todas as declarações intrínsecas ao HW naquele módulo; e uma vez trocando-se de uC, mexe-se apenas naquele arquivo. Todos os demais do projeto, ficam parametrizados à esse.

Já a sugestão inicial do Jean( padronização ), depende do grau de organização da engenharia. Para validar uma solução num HW, ele deve ser homologado para todos os outros, e isso agora passa a ser uma questão meramente de controle de versão.


+++

MensagemEnviado: 01 Dez 2008 08:07
por ivan
Um bom ponto de partida para se estudar este tipo de prob de eng de software que o Jean iniciou é o uClinux.
Divide-se a solução de software em camadas lógicas e físicas(no plural msm, podem sem várias camadas!).
Na logica, descreve os dispositivos/funcionalidades disponíveis em todos os hardwares através de arquivos de include, etc...
Na física, utiliza-se as descrições acima e, dividida por hardware, tem-se as implementações.
Através de Makefiles construidos para cada tipo de hardware, no caso do uClinux, monta-se o software apropriado.

Antes de tudo isso, é obrigatório que se estabeleça o padrão a ser usado, que este seja conhecido por todos os desenvolvedores dos projetos e cada sugestão de inclusão seja validada para que se torne aderente ao padrão estabelecido antes de ser adotado(setor de qualidade - que pode ser com uma só pessoa dependendo do tamanho da empresa).

Ou seja, uma questão de engenharia HW/SW, administração e pessoal técnico. Uma mudança estrutural na empresa se esta pretende adotar este modo de trabalho.

MensagemEnviado: 01 Dez 2008 08:41
por jeanfernandes
Pois eh

Nao esperava chegar já a esta solucao
pratica, porem conhecendo o Sam, e depois de horas,...pra ele eh só tempinho de tomar uma coca... mas com certeza tá certo...valida o pensamento.

Sim sobre uma questao de ordem prática. É aquela coisa né. Ce pode passar a vida toda fazendo "funcionar"... e num querer mexer no vespeiro. Porem é pouco provavel que com essa posição voce possa se beneficiar do uso de arquiteturas mais bem elaboradas ja disponiveis, como o caso do uclinux e essas distros de linux para embedded, ou ate mesmo os LW RTOS.

Tem gente que tem medo e fica botando defeito.
Bom,....pra frente...
Sam, e demais. Obrigado mesmo. É por ae, o conceito parece complexo porem a solucao eh simples. O primeiro caso sempre doi um pokim, mas depois eh so lazer....

MensagemEnviado: 02 Dez 2008 13:12
por ivan
Não sei se fui claro, mas citei o uClinux apenas como uma fonte de estudo de uma implementação de SW multi-camadas/multi-plataforma.
Razoávelmente configurável, em temos de tamanho.

MensagemEnviado: 02 Dez 2008 20:49
por msamsoniuk
eu acho que este tipo de solucao feita em casa eh interessante para componentes monoliticos com flash e sram on-chip. uclinux dae jah requer memoria externa... e nao pode ser qq memoria, tem q ser alguns MBs... e claro, tem que ser um processador 32 bits! :)

MensagemEnviado: 03 Dez 2008 01:16
por ivan
Pois é Marcelo, não discordei de vc, e ainda, achei a sua solução interessante. Porém, volto a dizer, o que eu quis ressaltar ao comentar sobre o uClinux, foi que o problema abordado pelo Jean é semelhante. Ou seja, contruir um SW multiplataforma, com um núcleo comum, em multicamadas e que possa ser "configurável".
Partindo deste princípio, baixar os fontes e estudar a organização, a estrutura de SW, etc; no meu ponto de vista, só acrescentaria informações para então construir a solução caseira.
Agora, posso até estar errado, eu a princípio implementaria uma estrutura de SW aonde o cliente solicitaria serviços aos drivers, e se esses seriam blocantes ou não, seria uma particularidade da interface com a camada física a ser retornada na estrutura preenchida como parâmetro do serviço. Algo tipo... while(x->blocked) testada a após cada solicitação de serviço... sei lá...
Sua visão foi bastante objetiva quanto ao tipo de implementação a ser seguida, eu quis dar a a questão uma forma mais conceitual.

MensagemEnviado: 03 Dez 2008 07:30
por andre_luis
ivan escreveu:...eu a princípio implementaria uma estrutura de SW aonde o cliente solicitaria serviços aos drivers...


Me parece a descrição de API´s.

+++

MensagemEnviado: 03 Dez 2008 08:52
por ivan
Isso já é consenso!

I wrote
...blocantes ou não, seria uma particularidade da interface com a camada física...


Uma questão só de implementação. Ao deslocar o tratamento de "BLOCK" para a camada inferior tentaria, a princípio, evitar msgs e filas...(overhead).

Pode ser q vcs com + experiência em desenvolvimento em Mcus vejam de forma diferente e já tenham até provado que funciona deste jeito, que parece ser o caso do Marcelo.

Na verdade, as "msgs"(pedidos<=>repostas) sempre irão trafegar por todas as camadas da aplicação, óbvio! Mas que tal pensarmos em estruturas de dados, pois no "frigir dos ovos", qdo se fizer um novo HW/SW só se quer mudar o Main(loop principal) e as camadas de acesso(HW).
É no loop principal que já se trata todas essas coisas... Pensei num SW + enxuto possível.

MensagemEnviado: 04 Dez 2008 01:12
por msamsoniuk
na verdade eh tudo meio conceitual ainda ivan... porem estamos discutindo com exemplos praticos justamente para dar um passinho adiante e nos certificarmos que os conceitos que estamos discutindo nao estao profundamente furados! :)

a base de tudo seria a modularidade, ou seja, a capacidade de dividir todo o SW em modulos. a ideia neste caso eh que, para determinados projetos de HW, vc simplesmente compila os respectivos modulos de SW. analisando a fundo essa ideia, vc chega a conclusao que o caso ideal sao modulos auto-gerenciados no melhor estilo caixa-preta, onde vc conhece apenas uma interface com o modulo e nada mais. no exemplo do rtc, bastaria linkar o modulo de SW e a funcionalidade estaria disponivel para tratar o respectivo HW.

neste caso, puxamos para outra ideia: padronizar e simplificar as interfaces. uma interface padrao para todos os modulos eh o caso ideal, pq minimiza potenciais erros. simplificar, facilita mais ainda! no exemplo do rtc, metodos padronizados para read e write escondem complicacoes extras nas entranha do modulo e permitem o desenvolvedor se concentrar no seu proprio trabalho. este auto-gerenciamento tambem previne uma serie de problemas, pois o modulo pode ser adicionado com facilidade ao projeto.

isso para um ponto de vista mais macro jah eh bastante interessante.

por outro lado, o que temos ainda eh um emaranhado de funcoes chamadas diretamente de um loop principal e algumas interrupcoes. pensando em algumas melhorias operacionais, sera que conceitos de RTOS usados em microcontroladores maiores de 16 e 32 bits sao aplicaveis em microcontroladores menores de 8 bits ?

aparentemente sim, a ideia seria extender alguns conceitos de modulos (viram tasks) e interfaces (fila de mensagens)... mas isso eh mais para frente! eu implementei ateh o momento um sisteminha de escalonamento de processos baseado em uma queue de mensagens. ateh agora esta funcionando relativamente bem, atingindo 5000 mensagens/segundo em um HC908 de 2.5MHz.

provavelmente conforme for colocando mais e mais tasks, a performance deve cair... ou sera que nao ? :)