
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: 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,
FALSOO 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.