Toda a verdade sobre o RTOS. Artigo # 33 Usando o sistema operacional Nucleus SE em tempo real

Até agora, nesta série de artigos, examinamos quais recursos o Nucleus SE oferece. Agora é hora de ver como ele pode ser usado em um aplicativo de firmware real.



Artigos anteriores da série:
Artigo 32. Migração do Nucleus SE: recursos e compatibilidade não realizados
Artigo 31. Diagnósticos e verificação de erros RTOS
Artigo 30. Procedimentos de inicialização e inicialização do Núcleo SE
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.

O que é o Nucleus SE?


Sabemos que o Nucleus SE é o núcleo de um sistema operacional em tempo real, mas você precisa entender como ele se encaixa no restante do aplicativo. E apenas se encaixa, porque, diferentemente de um sistema operacional de desktop (por exemplo, Windows), o aplicativo não inicia no Nucleus SE; o kernel é simplesmente parte de um programa em execução em um dispositivo incorporado. Este é o caso de uso mais comum para o RTOS.

Do ponto de vista de alto nível, um aplicativo incorporado é algum tipo de código iniciado quando a CPU é iniciada. Nesse caso, o ambiente de hardware e software é inicializado e a função main () é chamada, que inicia o código principal do aplicativo.

Ao usar o Nucleus SE (e muitos outros kernels semelhantes), a diferença é que a função main () faz parte do código do kernel. Essa função simplesmente inicializa as estruturas de dados do kernel e depois chama o agendador, o que leva ao lançamento do código do aplicativo (tarefas). O usuário pode adicionar qualquer código de inicialização nativo à função main () .

O Nucleus SE também inclui um conjunto de funções - uma interface de programação de aplicativos (API) que fornece um conjunto de funções como comunicação e sincronização de tarefas, trabalhando com temporizadores, alocação de memória, etc. Todas as funções da API foram descritas anteriormente nos artigos desta série.

Todo o software Nucleus SE é fornecido como código fonte (principalmente em C). Para configurar o código de acordo com os requisitos de um aplicativo específico, é usada a compilação condicional. Isso é descrito em detalhes neste artigo na seção Configuração.

Após a compilação do código, os módulos de objeto Nucleus SE resultantes são associados aos módulos de código do aplicativo, resultando em uma única imagem binária, que geralmente é colocada na memória flash do dispositivo incorporado. O resultado dessa ligação estática é que todas as informações simbólicas permanecem disponíveis no código do aplicativo e no código do kernel. Isso é útil para depuração; no entanto, é necessário cuidado para evitar o uso indevido dos dados do Nucleus SE.

Suporte de CPU e ferramenta


Como o Nucleus SE vem como código-fonte, ele deve ser portátil. No entanto, o código em execução em um nível tão baixo (ao usar agendadores em que a alternância de contexto é necessária, ou seja, qualquer coisa que não seja Executar para Conclusão), não pode ser completamente independente da linguagem assembly. Minimizei essa dependência e quase não é necessário migrar para uma nova programação de baixo nível da CPU. O uso de um novo conjunto de ferramentas de desenvolvimento (compilador, montador, vinculador etc.) também pode levar a problemas de portabilidade.

Configurando o aplicativo Nucleus SE


A chave para o uso eficiente do Nucleus SE é a configuração adequada. Pode parecer complicado, mas, de fato, tudo é bastante lógico e requer apenas uma abordagem sistemática. Quase toda a configuração é feita editando dois arquivos: nuse_config.he nuse_config.c .

Configuração Nuse_config.h


Este arquivo é apenas um conjunto de caracteres da diretiva #define , que recebe os valores apropriados para obter a configuração necessária do kernel. No arquivo nuse_config.h, por padrão, todos os caracteres estão presentes, mas eles recebem as configurações mínimas.

Contadores de Objetos
O número de objetos do kernel de cada tipo é definido pelos valores dos símbolos no formato NUSE_SEMAPHORE_NUMBER . Para a maioria dos objetos, esse valor pode variar de 0 a 15. As tarefas são uma exceção, deve haver pelo menos uma. Os sinais, na verdade, não são objetos independentes, pois estão associados a tarefas e são ativados configurando NUSE_SIGNAL_SUPPOR T como TRUE .

Ativadores de função API
Cada função da API do Nucleus SE pode ser ativada separadamente, atribuindo um símbolo cujo nome corresponde ao nome da função (por exemplo, NUSE_PIPE_JAM ) como TRUE . Isso leva à inclusão do código de função no aplicativo.

Seleção e configurações do agendador
O Nucleus SE suporta quatro tipos de agendadores, conforme descrito em um artigo anterior. O planejador usado é definido atribuindo NUSE_SCHEDULER_TYPE a um dos seguintes valores: NUSE_RUN_TO_COMPLETION_SCHEDULER , NUSE_TIME_SLICE_SCHEDULER , NUSE_ROUND_ROBIN_SCHEDULER ou NUSE_PRIORITY_SCHEDULER .

Você pode configurar outros parâmetros do planejador:
NUSE_TIME_SLICE_TICKS indica o número de ticks por slot para o planejador de Time Slice. Se outro agendador for usado, esse parâmetro deverá ser definido como 0.
NUSE_SCHEDULE_COUNT_SUPPORT pode ser definido como TRUE ou FALSE para ativar / desativar o mecanismo do contador do planejador.
NUSE_SUSPEND_ENABLE ativa o bloqueio de tarefas (suspensão) para muitas funções da API. Isso significa que uma chamada para essa função pode levar à suspensão da tarefa de chamada até que o recurso seja liberado. Para selecionar esta opção, NUSE_SUSPEND_ENABLE também deve ser definido como TRUE .

Outras opções
Vários outros parâmetros também podem receber valores TRUE ou FALSE para ativar / desativar outras funções do kernel:
NUSE_API_PARAMETER_CHECKING adiciona um código de verificação de parâmetro de chamada de função da API. Comumente usado para depuração.
NUSE_INITIAL_TASK_STATE_SUPPORT define o estado inicial de todas as tarefas como NUSE_READY ou NUSE_PURE_SUSPEND . Se esse parâmetro estiver desativado, todas as tarefas terão o estado inicial NUSE_READY .
NUSE_SYSTEM_TIME_SUPPORT - suporte para horário do sistema.
NUSE_INCLUDE_EVERYTHING - um parâmetro que adiciona o número máximo de funções à configuração do Nucleus SE. Isso leva à ativação de toda a funcionalidade opcional e de cada função da API dos objetos configurados. Usado para criar rapidamente uma configuração do Nucleus SE para verificar uma nova portabilidade do código do kernel.

Definindo nuse_config.c


Após especificar a configuração do kernel em nuse_config.h, é necessário inicializar as várias estruturas de dados armazenadas na ROM. Isso é feito no arquivo nuse_config.c . A definição de estruturas de dados é controlada pela compilação condicional, portanto, todas as estruturas estão contidas em uma cópia do arquivo nuse_config.c padrão.

Dados da tarefa
A matriz NUSE_Task_Start_Address [] deve ser inicializada com o valor dos endereços iniciais de cada tarefa. Geralmente, isso é apenas uma lista de nomes de funções, sem parênteses. Protótipos de funções de entrada de tarefas também devem estar visíveis. No arquivo padrão, a tarefa é configurada com o nome NUSE_Idle_Task () , isso pode ser alterado para a tarefa do aplicativo.

Se você usar qualquer agendador, exceto Executar até a conclusão, cada tarefa exigirá sua própria pilha. Para cada pilha de tarefas, você deve criar uma matriz na RAM. Essas matrizes devem ser do tipo ADDR e o endereço de cada uma delas deve ser armazenado em NUSE_Task_Stack_Base [] . É difícil prever o tamanho da matriz, portanto, é melhor usar medições (consulte a seção "Depuração" mais adiante neste artigo). O tamanho de cada matriz (ou seja, o número de palavras na pilha) deve ser armazenado em NUSE_Task_Stack_Size [] .

Se uma função tiver sido ativada para indicar o estado inicial da tarefa (usando o parâmetro NUSE_INITIAL_TASK_STATE_SUPPORT ), a matriz NUSE_Task_Initial_State [] [] deverá ser inicializada com o estado NUSE_READY ou NUSE_PURE_SUSPEND .

Dados do Conjunto de Partições
Se pelo menos um conjunto de partições estiver configurado, uma matriz (do tipo U8 ) deverá ser criada para cada um deles na ROM. O tamanho dessas matrizes é calculado da seguinte forma: (número de partições * (tamanho da partição + 1)). Os endereços dessas seções (ou seja, seus nomes) devem ser atribuídos aos elementos NUSE_Partition_Pool_Data_Address [] correspondentes. Para cada pool, o número de partições e seu tamanho devem ser colocados em NUSE_Partition_Pool_Partition_Number [] e NUSE_Partition_Message_Size [] , respectivamente.

Dados da fila
Se pelo menos uma fila estiver configurada, uma matriz (do tipo ADDR ) deverá ser criada para cada um deles na RAM. O tamanho dessas matrizes é o número de elementos em cada fila. Os endereços dessas matrizes (ou seja, seus nomes) devem ser atribuídos aos elementos NUSE_Queue_Data [] correspondentes. O tamanho de cada fila deve ser atribuído ao elemento NUSE_Queue_Size [] correspondente.

Dados do Link de Dados
Se pelo menos um canal de dados estiver configurado, uma matriz (do tipo U8 ) deverá ser criada na RAM para ele (ou para cada um deles). O tamanho dessas matrizes é calculado da seguinte forma: (tamanho do canal * tamanho da mensagem no canal). Os endereços dessas matrizes (ou seja, seus nomes) devem ser atribuídos aos elementos NUSE_Pipe_Data [] correspondentes. Para cada canal, seu tamanho e tamanho da mensagem devem ser atribuídos aos elementos NUSE_Pipe_Size [] e NUSE_Pipe_Message_Size [] correspondentes, respectivamente.

Dados do semáforo
Se pelo menos um semáforo estiver configurado, a matriz NUSE_Semaphore_Initial_Value [] deverá ser inicializada com os valores iniciais da contagem regressiva.

Dados do temporizador do aplicativo
Se pelo menos um timer estiver configurado, a matriz NUSE_Timer_Initial_Time [] deve ser inicializada com os valores iniciais dos contadores. Além disso, NUSE_Timer_Reschedule_Time [] deve receber valores de reinicialização. Esses valores do timer serão usados ​​após o término do primeiro ciclo do timer. Se os valores de reinicialização estiverem definidos como 0, o contador irá parar após um ciclo.

Se o suporte para mecanismos de conclusão de conta estiver configurado (configurando o parâmetro NUSE_TIMER_EXPIRATION_ROUTINE_SUPPORT como TRUE ), mais duas matrizes deverão ser criadas. Endereços de mecanismos de conclusão (apenas uma lista de nomes de funções, sem parênteses) devem ser colocados em NUSE_Timer_Expiration_Routine_Address [] . A matriz NUSE_Timer_Expiration_Routine_Parameter [] deve ser inicializada com os valores dos parâmetros de conclusão.

Qual API?


Todos os sistemas operacionais, de uma forma ou de outra, possuem uma API (interface de programação de aplicativos). O Nucleus SE não é exceção, e as funções que compõem a API foram descritas em detalhes nesta série de artigos.

Pode parecer óbvio que, ao escrever um aplicativo usando o Nucleus SE, você precisará usar a API conforme descrito nos artigos anteriores. No entanto, esse nem sempre é o caso.

Para a maioria dos usuários, a API do Nucleus SE será algo novo, talvez até a primeira experiência no uso da API do sistema operacional. E, como é bastante simples, pode servir como uma boa introdução ao tópico. Nesse caso, o procedimento é claro.

Para alguns usuários, uma API alternativa pode ser uma opção mais atraente. Existem três situações óbvias em que isso é possível.
  1. O Nucleus SE é apenas parte de um sistema que usa outros sistemas operacionais para outros componentes. Portanto, a portabilidade do código e, mais importante, a experiência de usar vários sistemas operacionais parecem muito tentadoras.
  2. O usuário tem uma vasta experiência no uso da API de outro sistema operacional. Usar essa experiência também é muito recomendável.
  3. O usuário deseja reutilizar o código escrito para a API de outro sistema operacional. É possível alterar as chamadas à API, mas demorado.


Como o código-fonte completo do Nucleus SE está disponível para todos, não há nada que o impeça de editar cada função da API para que pareça equivalente a outro sistema operacional. No entanto, levará muito tempo e será muito improdutivo. Uma abordagem mais correta seria escrever um "invólucro". Existem várias maneiras de fazer isso, mas a maneira mais fácil é criar um arquivo de cabeçalho ( #include ) contendo um conjunto de #define macros que mapeará funções da API de terceiros para as funções da API do Nucleus SE.

Um wrapper que transfere as funções da API Nucleus RTOS (parcialmente) para o Nucleus SE é distribuído com o Nucleus SE. Pode ser útil para desenvolvedores com experiência no uso do Nucleus RTOS ou onde, no futuro, é possível mudar para esse RTOS. Esse wrapper também pode servir como exemplo ao desenvolver coisas semelhantes.

Depurando aplicativos Nucleus SE


Escrever um aplicativo incorporado usando um kernel multitarefa é uma tarefa complexa. Garantir que o código funcione e detectar erros pode ser uma tarefa assustadora. Apesar de ser apenas um código executado em um processador, a execução simultânea de várias tarefas dificulta o foco em um encadeamento específico de execução. Isso é ainda mais complicado quando várias tarefas compartilham um código comum. O pior de tudo é quando duas tarefas têm exatamente o mesmo código (mas funcionam com dados diferentes). Também é complicado o desenrolar das estruturas de dados que são usadas para implementar objetos do kernel, a fim de ver informações significativas.

Para depurar aplicativos criados usando o Nucleus SE, nenhuma biblioteca adicional ou outros serviços são necessários. Todo o código do kernel é legível pelo depurador. Portanto, todas as informações simbólicas estão disponíveis para estudo. Ao trabalhar com aplicativos Nucleus SE, qualquer ferramenta moderna de depuração pode ser usada.

Usando um depurador


As ferramentas de depuração projetadas especificamente para sistemas embarcados tornaram-se muito poderosas nos 30 anos em que existiram. A principal característica de um aplicativo incorporado, em comparação com um programa de desktop, é que todos os sistemas incorporados são diferentes (e todos os computadores pessoais são bastante parecidos entre si). Um bom depurador incorporado deve ser flexível e ter configurações suficientes para corresponder à variedade de sistemas incorporados e aos requisitos do usuário. A capacidade de personalização do depurador é expressa de várias formas, mas geralmente existe a possibilidade de criar scripts. É esse recurso que permite que o depurador funcione bem com um aplicativo no nível do kernel. Abaixo, discutirei alguns casos de uso do depurador.

Vale a pena notar que geralmente um depurador é uma família de ferramentas, não apenas um programa. O depurador pode ter vários modos de operação, através dos quais ajuda no desenvolvimento de código em um sistema virtual ou em hardware real.

Pontos de interrupção sensíveis à tarefa


Se o programa tiver um código comum a várias tarefas, o uso de pontos de interrupção convencionais durante a depuração é complicado. Provavelmente, você precisa que o código seja interrompido apenas quando um ponto de interrupção for atingido no contexto de uma tarefa específica que você está depurando no momento. Para fazer isso, você precisa de um ponto de interrupção que leve em consideração a tarefa.

Felizmente, a capacidade de criar scripts nos depuradores modernos e a disponibilidade dos dados de caracteres do Nucleus SE tornam a implementação de pontos de interrupção específicos da tarefa uma coisa bastante simples. Tudo o que é necessário é escrever um script simples que será associado a um ponto de interrupção que você deseja ensinar a distinguir entre tarefas. Este script utilizará o parâmetro: index (ID) da tarefa em que você está interessado. O script simplesmente compara esse valor com o índice da tarefa atual ( NUSE_Task_Active ). Se os valores corresponderem, o programa fará uma pausa. Se eles são diferentes, a execução continua. Vale ressaltar que a execução desse script afetará a execução do aplicativo em tempo real ( nota do tradutor: significa que a execução do programa diminuirá a velocidade em relação à sua operação normal ). No entanto, se o script não estiver em um loop que será executado com muita frequência, esse efeito será mínimo.

Informações sobre o objeto do kernel


A necessidade óbvia de depurar o aplicativo Nucleus SE é a capacidade de obter informações sobre os objetos do kernel: quais são suas características e qual é seu status atual. Isso permite que você obtenha respostas para perguntas como: "Qual é o tamanho dessa fila e quantas mensagens estão agora?"

Isso pode ser usado adicionando código de depuração adicional ao seu aplicativo, que usará as chamadas de API "informativas" (como NUSE_Queue_Information ). Obviamente, isso significa que seu aplicativo agora contém código adicional, que não será necessário após a implementação do aplicativo. Usar #define para ativar e desativar esse código usando compilação condicional seria uma decisão lógica.

Alguns depuradores podem fazer uma chamada de função direcionada, ou seja, chamar diretamente uma função de API para recuperar informações.Isso elimina a necessidade de código adicional, mas essa função da API deve ser configurada para o depurador usá-lo.

Uma abordagem alternativa, mais flexível, mas menos "não envelhecida" é o acesso direto às estruturas de dados dos objetos do kernel. Provavelmente, é melhor fazer isso usando scripts de depuração. Em nosso exemplo, o tamanho da fila pode ser obtido em NUSE_Queue_Size [] e seu uso atual em NUSE_Queue_Data [] . Além disso, as mensagens na fila podem ser exibidas usando o endereço da área de dados da fila (de NUSE_Queue_Data [] ).

Valores de retorno de chamada da API


Muitas funções da API retornam um valor de status indicando com que êxito a chamada foi concluída. Seria útil acompanhar esses valores e marcar casos nos quais eles não são iguais a NUSE_SUCCESS (ou seja, eles têm um valor zero). Como esse rastreamento é apenas para depuração, a compilação condicional é bastante apropriada. A definição de uma variável global (por exemplo, NUSE_API_Call_Status ) pode ser compilada condicionalmente (sob o controle do símbolo de diretiva #define). Então, parte da definição de chamadas de API, NUSE_API_Call_Status = , também pode ser compilada condicionalmente. Por exemplo, para fins de depuração, uma chamada que se parece com isso:

NUSE_Mailbox_Send (mbox, msg, NUSE_SUSPSEND);

assumirá o seguinte formato:

NUSE_API_Call_Status = NUSE_Mailbox_Send (mbox, msg, NUSE_SUSPEND);

Se o bloqueio de tarefas estiver ativado, muitas chamadas de função da API poderão retornar apenas informações sobre a conclusão de uma chamada bem-sucedida ou que o objeto foi redefinido. No entanto, se a verificação de parâmetro da API estiver ativada, as chamadas da API poderão retornar muitos outros valores.

Configurando o tamanho da pilha de tarefas e do estouro da pilha


O tópico sobre proteção contra sobrecarga de pilha foi discutido em um artigo anterior (# 31). Existem várias outras possibilidades durante a depuração.

A área de memória da pilha pode ser preenchida com um valor característico: algo além de todos os ou zeros. Depois disso, o depurador pode ser usado para monitorar as áreas de memória e quanto os valores serão alterados, o que nos permitirá entender o grau de plenitude da pilha. Se todas as áreas de memória foram alteradas, isso não significa que a pilha estava cheia, mas pode significar que seu tamanho é insuficiente, o que é perigoso. Deve ser aumentado e o teste continuado.

Conforme descrito no artigo # 31, ao implementar diagnósticos, áreas adicionais, “palavras de proteção”, podem ser localizadas em qualquer uma das bordas da área de memória da pilha. O depurador pode ser usado para rastrear o acesso a essas palavras, já que qualquer tentativa de gravá-las significa um estouro ou exaustão da pilha.

Lista de verificação de configuração do Nucleus SE


Como o Nucleus SE foi projetado como um sistema altamente flexível e personalizável para atender aos requisitos da aplicação, requer um número significativo de parâmetros personalizáveis. É por isso que todo este artigo é dedicado à configuração do Nucleus SE. Para garantir que não perdemos nada, a seguir, é apresentada uma lista de verificação de todas as etapas principais que você precisa seguir para criar o aplicativo incorporado Nucleus SE.
  1. Nucleus SE. , Nucleus SE , Nucleus SE .
  2. CPU/. .
  3. . , , .
  4. . . . , . 16 .
  5. . - main() ?
  6. . 4 , .
  7. , .
  8. .
  9. . , .
  10. . , .
  11. . , . . — 16 .
  12. . , .
  13. . , (, ).
  14. API. API, .


O próximo artigo (o último desta série) resumirá toda a história com o Nucleus SE e também fornecerá informações que ajudarão na criação das implementações do Nucleus SE e seu uso.

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/pt467763/


All Articles