Toda a verdade sobre o RTOS. Artigo 30. Procedimentos de inicialização e inicialização do Núcleo SE



Qualquer sistema operacional possui um mecanismo de inicialização específico. O princípio de operação desse mecanismo para cada sistema é diferente. Geralmente eles dizem que o sistema é inicializado (Eng. Boot), esta é uma abreviação de "bootstrap", que se refere à expressão "puxe-se sobre uma cerca pelas próprias bootstraps" como o sistema se move independentemente de um estado em que a memória está cheia de vazio ( nota do tradutor: se for absolutamente preciso, então lixo ) para a execução estável do programa. Tradicionalmente, uma pequena parte do programa é carregada na memória e pode ser armazenada na ROM. No passado, era possível digitar usando os interruptores na parte frontal do computador. Este carregador de inicialização leu um programa de inicialização mais complexo que já carregou o sistema operacional. Hoje, o computador de mesa inicializa da seguinte maneira: o código do BIOS procura por dispositivos (discos rígidos, CD-ROMs, pen drives) nos quais inicializar e, em seguida, o sistema operacional é inicializado.

O SO para sistemas embarcados também pode ser inicializado de maneira semelhante. E, de fato, os sistemas operacionais incorporados desenvolvidos com base nos sistemas operacionais de desktop são carregados. Mas na maioria dos RTOS "clássicos", é usado um método muito mais simples (e, portanto, mais rápido).

O sistema operacional faz parte do software. Se esse software já estiver na memória (por exemplo, de uma forma ou de outra da ROM), basta garantir que a sequência de comandos da CPU após a redefinição termine com a execução do código de inicialização do SO. É assim que a maioria dos RTOS funciona, incluindo o Nucleus SE ( nota do tradutor: isso também se aplica ao nosso RTOS MAX ).

A maioria das ferramentas de desenvolvimento de software incorporado inclui o código de inicialização necessário para lidar com a redefinição da CPU e transferir o controle para a função Ponto de Entrada na função main () . O código redistribuível do Nucleus SE não lida com esse processo, pois deve ser o mais portátil possível. Em vez disso, contém a função main () , que assume o controle da CPU, inicializa e inicia o sistema operacional. Esse recurso será discutido em detalhes abaixo.

Artigos anteriores da série:
Artigo 29. Interrupções no Núcleo SE
Artigo 28. Temporizadores de software
Artigo # 27 Hora do sistema
Artigo 26. Canais: serviços auxiliares e estruturas de dados
Artigo 25. Canais de Dados: Introdução e Serviços Básicos
Artigo 24. Filas: serviços auxiliares e estruturas de dados
Artigo 23. Filas: introdução e serviços básicos
Artigo 22. Caixas de correio: serviços auxiliares e estruturas de dados
Artigo 21. Caixas de correio: Introdução e serviços básicos
Artigo 20. Semáforos: Serviços Auxiliares e Estruturas de Dados
Artigo 19. Semáforos: introdução e serviços básicos
Artigo # 18 Grupos de Sinalizadores de Eventos: Serviços Auxiliares e Estruturas de Dados
Artigo 17. Grupos de Sinalizadores de Eventos: Introdução e Serviços Básicos
Artigo 16. Signals
Artigo 15. Partições de memória: serviços e estruturas de dados
Artigo 14. Seções de memória: introdução e serviços básicos
Artigo 13. Estruturas de dados da tarefa e chamadas de API não suportadas
Artigo 12. Serviços para trabalhar com tarefas
Artigo 11. Tarefas: configuração e introdução à API
Artigo 10. Agendador: recursos avançados e preservação de contexto
Artigo 9. Agendador: implementação
Artigo 8. Núcleo SE: Projeto Interno e Implantação
Artigo # 7 Núcleo SE: Introdução
Artigo 6. Outros serviços RTOS
Artigo 5. Interação e sincronização de tarefas
Artigo 4. Tarefas, alternância de contexto e interrupções
Artigo # 3 Tarefas e planejamento
Artigo 2. RTOS: estrutura e modo em tempo real
Artigo 1. RTOS: introdução.

Inicialização de memória


As declarações de todas as variáveis ​​estáticas no código Nucleus SE começam com o prefixo ROM ou RAM para indicar onde elas devem estar localizadas. Essas duas diretivas #define são definidas no arquivo nuse_types.h e devem ser configuradas levando em consideração as especificidades do conjunto de ferramentas de desenvolvimento usadas (compilador e vinculador). Normalmente, a ROM deve ser do tipo const ( nota do tradutor: pela minha experiência, const nem sempre é suficiente, melhor é const estática ) e RAM é um valor vazio.

Todas as variáveis ​​de ROM são inicializadas estaticamente, o que é lógico. As variáveis ​​de RAM não são inicializadas estaticamente (pois isso funciona apenas com certas caixas de ferramentas configuradas para copiar automaticamente da ROM para a RAM); um código de inicialização explícito está incluído no aplicativo e será descrito em detalhes abaixo.

O Nucleus SE não armazena dados "constantes" na RAM, que geralmente é escassa em pequenos sistemas. Em vez de usar estruturas de dados complexas para descrever objetos do kernel, conjuntos de tabelas (matrizes) são usados, que são facilmente colocados na ROM ou RAM, dependendo da necessidade.

Função Main ()


A seguir, está o código completo da função main () do Nucleus SE:

void main(void) { NUSE_Init(); /* initialize kernel data */ /* user initialization code here */ NUSE_Scheduler(); /* start tasks */ } 

A sequência de operações é bastante simples:

  • Primeiro, a função NUSE_Init () é chamada . Inicializa todas as estruturas de dados do Nucleus SE e será descrito em detalhes abaixo.
  • Em seguida, o usuário pode inserir qualquer código de inicialização do aplicativo que será executado antes do início do planejador de tarefas. Para obter mais informações sobre o que pode ser alcançado com esse código, consulte mais adiante neste artigo.
  • Por fim, o agendador do Núcleo SE ( NUSE_Scheduler () ) é iniciado. Isso também será discutido em detalhes posteriormente neste artigo.

Função NUSE_Init ()


Essa função inicializa todas as variáveis ​​e estruturas de dados do núcleo Nucleus SE.

Abaixo está o código completo da função:
 void NUSE_Init(void) { U8 index; /* global data */ NUSE_Task_Active = 0; NUSE_Task_State = NUSE_STARTUP_CONTEXT; #if NUSE_SYSTEM_TIME_SUPPORT NUSE_Tick_Clock = 0; #endif #if NUSE_SCHEDULER_TYPE == NUSE_TIME_SLICE_SCHEDULER NUSE_Time_Slice_Ticks = NUSE_TIME_SLICE_TICKS; #endif /* tasks */ #if ((NUSE_SCHEDULER_TYPE != NUSE_RUN_TO_COMPLETION_SCHEDULER) || NUSE_SIGNAL_SUPPORT || NUSE_TASK_SLEEP || NUSE_SUSPEND_ENABLE || NUSE_SCHEDULE_COUNT_SUPPORT) for (index=0; index<NUSE_TASK_NUMBER; index++) { NUSE_Init_Task(index); } #endif /* partition pools */ #if NUSE_PARTITION_POOL_NUMBER != 0 for (index=0; index<NUSE_PARTITION_POOL_NUMBER; index++) { NUSE_Init_Partition_Pool(index); } #endif /* mailboxes */ #if NUSE_MAILBOX_NUMBER != 0 for (index=0; index<NUSE_MAILBOX_NUMBER; index++) { NUSE_Init_Mailbox(index); } #endif /* queues */ #if NUSE_QUEUE_NUMBER != 0 for (index=0; index<NUSE_QUEUE_NUMBER; index++) { NUSE_Init_Queue(index); } #endif /* pipes */ #if NUSE_PIPE_NUMBER != 0 for (index=0; index<NUSE_PIPE_NUMBER; index++) { NUSE_Init_Pipe(index); } #endif /* semaphores */ #if NUSE_SEMAPHORE_NUMBER != 0 for (index=0; index<NUSE_SEMAPHORE_NUMBER; index++) { NUSE_Init_Semaphore(index); } #endif /* event groups */ #if NUSE_EVENT_GROUP_NUMBER != 0 for (index=0; index<NUSE_EVENT_GROUP_NUMBER; index++) { NUSE_Init_Event_Group(index); } #endif /* timers */ #if NUSE_TIMER_NUMBER != 0 for (index=0; index<NUSE_TIMER_NUMBER; index++) { NUSE_Init_Timer(index); } #endif } 


Primeiro, as variáveis ​​globais são inicializadas:
  • NUSE_Task_Active - índice da tarefa ativa, inicializada em zero; mais tarde, isso pode alterar o agendador.
  • NUSE_Task_State - inicializado com o valor NUSE_STARTUP_CONTEXT , que limita a funcionalidade da API para qualquer código de inicialização de aplicativo subsequente.
  • Se o suporte de horário do sistema estiver ativado, NUSE_Tick_Clock será definido como zero.
  • Se o planejador do Time Slice estiver ativado, NUSE_Time_Slice_Ticks receberá o valor configurado NUSE_TIME_SLICE_TICKS .

Em seguida, as funções são chamadas para inicializar os objetos do kernel:

  • NUSE_Init_Task () é chamado para inicializar as estruturas de dados de cada tarefa. Essa chamada será ignorada apenas se o agendador Executar até a conclusão for usado e os sinais, pausa na tarefa e contador de agendamento não estiverem configurados (uma vez que essa combinação de funções resultará na ausência dessas estruturas de tarefas na RAM, portanto, a inicialização não será executada).
  • NUSE_Init_Partition_Pool () é chamado para inicializar cada objeto do conjunto de partições. Essas chamadas serão ignoradas se nenhum conjunto de partições estiver configurado.
  • NUSE_Init_Mailbox () é chamado para inicializar cada objeto da caixa de correio. Essas chamadas são ignoradas se não houver caixas de correio configuradas.
  • NUSE_Init_Queue () é chamado para inicializar cada objeto da fila. Essas chamadas são ignoradas se não houver filas configuradas.
  • NUSE_Init_Pipe () é chamado para inicializar cada objeto de canal. Essas chamadas são ignoradas se não houver canais configurados.
  • NUSE_Init_Semaphore () é chamado para inicializar cada objeto de semáforo. Essas chamadas serão ignoradas se nenhum semáforo estiver configurado.
  • NUSE_Init_Event_Group () é chamado para inicializar cada objeto do grupo de eventos. Essas chamadas serão ignoradas se não houver grupos de eventos configurados.
  • NUSE_Init_Timer () é chamado para inicializar cada objeto de timer. Essas chamadas são ignoradas se não houver temporizadores configurados.

Inicialização de tarefas


A seguir, está o código completo da função NUSE_Init_Task ():
 void NUSE_Init_Task(NUSE_TASK task) { #if NUSE_SCHEDULER_TYPE != NUSE_RUN_TO_COMPLETION_SCHEDULER NUSE_Task_Context[task][15] = /* SR */ NUSE_STATUS_REGISTER; NUSE_Task_Context[task][16] = /* PC */ NUSE_Task_Start_Address[task]; NUSE_Task_Context[task][17] = /* SP */ (U32 *)NUSE_Task_Stack_Base[task] + NUSE_Task_Stack_Size[task]; #endif #if NUSE_SIGNAL_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Signal_Flags[task] = 0; #endif #if NUSE_TASK_SLEEP || NUSE_INCLUDE_EVERYTHING NUSE_Task_Timeout_Counter[task] = 0; #endif #if NUSE_SUSPEND_ENABLE || NUSE_INCLUDE_EVERYTHING #if NUSE_INITIAL_TASK_STATE_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Status[task] = NUSE_Task_Initial_State[task]; #else NUSE_Task_Status[task] = NUSE_READY; #endif #endif #if NUSE_SCHEDULE_COUNT_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Schedule_Count[task] = 0; #endif } 


Se o agendador Run to Completion não tiver sido configurado, o bloco de contexto da tarefa NUSE_Task_Context [task] [] será inicializado. A maioria dos elementos não possui valores atribuídos, pois representam registros comuns da máquina, que devem ter um valor intermediário ao iniciar uma tarefa. No exemplo (Freescale ColdFire) da implementação do Nucleus SE (mas para outros processadores, o mecanismo será semelhante), as três últimas entradas são definidas explicitamente:

  • NUSE_Task_Context [task] [15] contém o registro de status ( SR , registro de status) e possui o valor da diretiva #define NUSE_STATUS_REGISTER .
  • NUSE_Task_Context [task] [16] contém o contador de programas ( PC , contador de programas) e possui o valor do endereço do ponto de entrada do código da tarefa: NUSE_Task_Start_Address [task] .
  • NUSE_Task_Context [task] [17] contém o ponteiro da pilha ( SP , ponteiro da pilha) e é inicializado com o valor calculado como a soma do endereço da pilha de tarefas ( NUSE_Task_Stack_Base [task] ) e o tamanho da pilha de tarefas ( NUSE_Task_Stack_Size [task] ).

Se o suporte a sinal estiver ativado, os sinalizadores de tarefa ( NUSE_Task_Signal_Flags [task] ) serão definidos como zero.

Se a suspensão da tarefa estiver ativada (ou seja, a chamada de serviço da API NUSE_Task_Sleep () ), o contador de tempo limite da tarefa ( NUSE_Task_Timeout_Counter [task] ) será definido como zero.

Se o estado de suspensão da tarefa estiver ativado, o status da tarefa ( NUSE_Task_Status [task] ) será inicializado. Este valor inicial é definido pelo usuário (em NUSE_Task_Initial_State [task] ) se o suporte ao estado inicial da tarefa estiver ativado. Caso contrário, o estado é atribuído a NUSE_READY .

Se o contador de planejamento estiver ativado, o contador de tarefas ( NUSE_Task_Schedule_Count [task] ) será definido como zero.

Inicializando conjuntos de partições


A seguir, está o código completo da função NUSE_Init_Partition_Pool () :

 void NUSE_Init_Partition_Pool(NUSE_PARTITION_POOL pool) { NUSE_Partition_Pool_Partition_Used[pool] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Partition_Pool_Blocking_Count[pool] = 0; #endif } 

O contador do conjunto de partições "usado" ( NUSE_Partition_Pool__Partition_Used [pool] ) está definido como zero.

Se o bloqueio de tarefas estiver ativado, o contador de tarefas bloqueadas dos conjuntos de partições ( NUSE_Partition_Pool_Blocking_Count [pool]) será definido como zero.

Inicializando caixas de correio


Abaixo está o código completo do NUSE_Init_Mailbox () :

 void NUSE_Init_Mailbox(NUSE_MAILBOX mailbox) { NUSE_Mailbox_Data[mailbox] = 0; NUSE_Mailbox_Status[mailbox] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Mailbox_Blocking_Count[mailbox] = 0; #endif } 

O armazenamento de dados da caixa de correio ( NUSE_Mailbox_Data [caixa de correio] ) é definido como zero e o estado ( NUSE_Mailbox_Status [caixa de correio] ) se torna "não utilizado" (ou seja, zero).

Se o bloqueio de tarefas estiver ativado, o contador de tarefas da caixa de correio bloqueada ( NUSE_Mailbox_Blocking_Count [caixa de correio] ) será definido como zero.

Inicialização da fila


A seguir, está o código completo da função NUSE_Init_Queue () :

 void NUSE_Init_Queue(NUSE_QUEUE queue) { NUSE_Queue_Head[queue] = 0; NUSE_Queue_Tail[queue] = 0; NUSE_Queue_Items[queue] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Queue_Blocking_Count[queue] = 0; #endif } 

Os ponteiros para o início e o fim da fila (de fato, esses são os índices NUSE_Queue_Head [fila ] e NUSE_Queue_Tail [fila] ) recebem valores que indicam o início da área de dados das filas (ou seja, assumem um valor zero). O contador na fila ( NUSE_Queue_Items [fila] ) também está definido como zero.

Se o bloqueio de tarefas estiver ativado, o contador de tarefas da fila bloqueadas ( NUSE_Queue_Blocking_Count [fila] ) será definido como zero.

Inicialização de canal


A seguir, está o código completo da função NUSE_Init_Pipe () :

 void NUSE_Init_Pipe(NUSE_PIPE pipe) { NUSE_Pipe_Head[pipe] = 0; NUSE_Pipe_Tail[pipe] = 0; NUSE_Pipe_Items[pipe] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Pipe_Blocking_Count[pipe] = 0; #endif } 

Os ponteiros para o início e o fim do canal (de fato, esses são os índices - NUSE_Pipe_Head [pipe] e NUSE_Pipe_Tail [pipe] ) recebem um valor que indica o início da área de dados do canal (ou seja, eles assumem um valor zero). O contador de canais ( NUSE_Pipe_Items [pipe] ) também está definido como zero.

Se o bloqueio de tarefas estiver ativado, o contador de tarefas bloqueadas do canal ( NUSE_Pipe_Blocking_Count [pipe] ) será definido como zero.

Inicialização de semáforo


A seguir, está o código completo da função NUSE_Init_Semaphore () :

 void NUSE_Init_Semaphore(NUSE_SEMAPHORE semaphore) { NUSE_Semaphore_Counter[semaphore] = NUSE_Semaphore_Initial_Value[semaphore]; #if NUSE_BLOCKING_ENABLE NUSE_Semaphore_Blocking_Count[semaphore] = 0; #endif } 

O contador de semáforo ( NUSE_Semaphore_Counter [semáforo] ) é inicializado com o valor definido pelo usuário ( NUSE_Semaphore_Initial_Value [semáforo] ).

Se o bloqueio de tarefas estiver ativado, o contador de tarefas de semáforo bloqueado ( NUSE_Semaphore_Blocking_Count [semáforo] ) será definido como zero.

Inicializando grupos de eventos


A seguir, está o código completo da função NUSE_Init_Event_Group () :

 void NUSE_Init_Event_Group(NUSE_EVENT_GROUP group) { NUSE_Event_Group_Data[group] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Event_Group_Blocking_Count[group] = 0; #endif } 

Os sinalizadores do grupo de eventos são redefinidos, ou seja, NUSE_Event_Group_Data [group] recebe um valor nulo.

Se o bloqueio de tarefas estiver ativado, o contador de tarefas bloqueadas do grupo de sinalizadores de eventos ( NUSE_Event_Group_Blocking_Count [group] ) será definido como zero.

Inicialização do timer


Abaixo está o código completo de NUSE_Init_Timer () ;

 void NUSE_Init_Timer(NUSE_TIMER timer) { NUSE_Timer_Status[timer] = FALSE; NUSE_Timer_Value[timer] = NUSE_Timer_Initial_Time[timer]; NUSE_Timer_Expirations_Counter[timer] = 0; } 

O estado do timer ( NUSE_Timer_Status [timer] ) está definido como "não utilizado", ou seja, FALSO

O valor da contagem regressiva ( NUSE_Timer_Value [timer ]) é inicializado pelo valor definido pelo usuário ( NUSE_Timer_Initial_Time [timer] ).

O contador de conclusão ( NUSE_Timer_Expirations_Counter [timer] ) está definido como zero.

Inicializando o código do aplicativo


Depois que as estruturas de dados do Nucleus SE foram inicializadas, torna-se possível executar o código responsável pela inicialização do aplicativo antes de iniciar a tarefa. Esse recurso pode ser útil para as seguintes tarefas:

  • Inicialização de estruturas de dados de aplicativos. O preenchimento explícito de estruturas de dados é mais fácil de entender e depurar em comparação com a inicialização automática de variáveis ​​estáticas.
  • Atribuição de objetos do kernel. Dado que todos os objetos do kernel são criados estaticamente no estágio de construção e são identificados usando valores de índice, pode ser útil atribuir um "proprietário" ou determinar o uso desses objetos. Isso pode ser feito usando a diretiva #define, no entanto, se houver várias instâncias de tarefas, é melhor atribuir índices de objetos por meio de matrizes globais (indexadas pelo ID da tarefa).
  • Inicialização do dispositivo. Isso pode ser útil para a instalação inicial de periféricos.

Obviamente, muitos desses objetivos podem ser alcançados antes da inicialização do Nucleus SE, mas a vantagem na localização do código do aplicativo aqui é que agora você pode usar os serviços do kernel (chamadas de API). Por exemplo, uma fila ou caixa de correio pode ser preenchida previamente com dados que precisarão ser processados ​​quando a tarefa for iniciada.

As chamadas de API têm uma restrição: todas as ações que geralmente levam à ativação do agendador são proibidas (por exemplo, pausar / bloquear tarefas). A variável global NUSE_Task_State foi definida como NUSE_STARTUP_CONTEXT para indicar essa limitação.

Agendador de inicialização


Após a inicialização ser concluída, resta apenas executar o planejador para começar a executar o código do aplicativo - tarefas. A configuração do planejador e o trabalho de vários tipos de planejadores foram descritos em detalhes em um dos artigos anteriores ( nº 9 ); portanto, apenas um breve resumo é necessário aqui.
A sequência das etapas principais é a seguinte:

  • Configurando a variável global NUSE_Task_State como NUSE_TASK_CONTEXT .
  • Selecione o índice da primeira tarefa a ser executada. Se o suporte para a tarefa inicial estiver ativado, a procura pela primeira tarefa finalizada será executada, caso contrário, será usado um valor zero.
  • O agendador é chamado - NUSE_Scheduler () .

O que exatamente acontece na última etapa depende de qual planejador está selecionado. Ao usar o planejador Executar até a conclusão, um ciclo de planejamento é iniciado e as tarefas são chamadas seqüencialmente. Ao usar outros agendadores, o contexto da primeira tarefa é carregado e o controle é transferido para a tarefa.

O artigo a seguir discutirá diagnósticos e verificação de erros.

Sobre o autor: Colin Walls trabalha na indústria eletrônica há mais de trinta anos, dedicando a maior parte de seu tempo ao firmware. Ele agora é engenheiro de firmware na Mentor Embedded (uma divisão da Mentor Graphics). Colin Walls frequentemente fala em conferências e seminários, autor de vários artigos técnicos e dois livros sobre firmware. Vive no Reino Unido. Blog profissional de Colin , e-mail: colin_walls@mentor.com.

Source: https://habr.com/ru/post/pt460617/


All Articles