Desenvolvendo com o Zephyr RTOS: Controlando o Kernel

Kernel do Zephyr
Este post faz parte da série Zephyr. Leia também os outros posts da série:

Olá caro leitor, como vai? Estou um pouco ausente da comunidade, com uma correria sem fim, mas aos poucos as coisas vão voltando à normalidade, e o que quero é ao menos tentar retomar as séries abertas aos poucos. E uma das dúvidas que mais recebi informalmente ou por e-mail é como desenvolver com o Zephyr RTOS. De fato temos alguns artigos aqui no Embarcados onde cobrimos o ambiente de desenvolvimento e debug com eclipse, mas ainda falta aquele dia a dia com o kernel, como utilizar, como desenvolver aplicações, essas coisas. Para isso vamos partir desses artigos e efetivamente experimentar o kernel. Tomarei a liberdade de utilizar diferentes alvos (todos focados em ARMv6 ou v7M, Cortex-M0 ou superior) para explorar as diferentes capacidades do kernel. E por falar em kernel, acho que neste artigo a melhor abordagem inicial é aprender como controlar o kernel. Para os leitores gostaria de deixar claro que espero um entendimento básico de programação multithread. Vamos lá?

 

 

Recobrando a organização do kernel

 

Um dos pontos do Zephyr kernel é sua organização, o sistema é bem modular, permitindo que o usuário utilize os recursos sob demanda. O mais interessante disso é que o build system do Zephyr kernel detecta os objetos alocados estaticamente e reserva exatamente a quantidade de memória RAM necessária para aquela aplicação. O Zephyr kernel ainda conta com uma separação bem definida entre os seus subsistemas. Por exemplo, no diretório /kernel vamos encontrar a implementação do micronúcleo de tempo real, a aplicação principal pode fazer o acesso das funções do kernel incluindo o arquivo zephyr.h em sua aplicação.

 

Diferentemente de RTOS como o popular FreeRTOS, o Zephyr kernel já entrega o controle para a aplicação do usuário com o RTOS inteiro inicializado (isso inclui até os device drivers utilizados), ou seja, conhecida função main() tida como entry point da aplicação já é uma thread por si só (possui prioridade mais alta por padrão).

 

Uma mudança que precisa ser considerada a partir da versão 1.7.x se situa na eliminação entre a divisão microkernel e nanokernel. Nas versões anteriores era possível configurar o RTOS para uma configuração com footprint ultra reduzido, chegando a consumir menos de 2KB de memória de programa, ao passo que a configuração em microkernel aplicava uma camada de firmware extra permitindo o uso de serviços de kernel mais complexos, por exemplo a preempção entre threads. Nas versões mais atuais, apenas existe o kernel, em que o usuário define o que vai ou não usar. A preempção é padrão do sistema, mas o usuário pode utilizar de forma emulada o serviço de fibers, que são threads de prioridades mais altas porém que não permitem a preempção. As vantagens dessa fusão resulta em um trabalho mais intuitivo por parte do desenvolvedor. Assim hoje o micronúcleo do Zephyr tem o aspecto da figura abaixo:

 

Kernel do Zephyr

 

Como todo bom RTOS e tal como fizemos com a série sobre o MBEDOS, outro RTOS com várias características interessantes, iremos começar pelo bloco mais primitivo de todos, as threads!

 

 

Threads com o Zephyr kernel

 

Como já dissemos em artigos anteriores, as threads e os semáforos são os objetos de kernel mais primitivos existentes. No caso das threads convém sempre lembrar que podem ser definidas como: Uma porção de código que tem por finalidade executar um determinado objetivo. Elas podem ser periódicas, aperiódicas ou esporádicas, podem ser run-to-completion, ou seja, toda vez que for invocada deve rodar até que sua ação seja completada. Podem ser bloqueantes, ou seja, aguardar um serviço externo fornecer informação necessária para o término de sua execução. No Zephyr isso não é diferente, o kernel utiliza como base o conhecido sched_FIFO como estratégia de agendamento de threads, o que se traduz na prática como a seguinte regra: A thread de maior prioridade e pronta para rodar assume o controle da CPU, porém, para manter a compatibilidade com o sistema de fibers, é possível criar threads com prioridades especiais, as quais sempre assumem o controle da CPU, mesmo que exista uma thread comum de altíssima prioridade pronta. Quando duas threads com característica de fibers estão prontas para rodar, o scheduler automaticamente entra em modo cooperativo, de forma que as threads só liberam a CPU em caso de notificação explícita através da função yield(), ou bloqueie enquanto aguarda por algum recurso, ou durma por algum período de tempo. Esquematizado abaixo:

 

Kernel do Zephyr - Scheduler

 

Resumindo, threads cuja a prioridade seja menor que zero, vão rodar em modo cooperativo. Esse mecanismo é indicado para threads que necessitem de rápida resposta, como device drivers. O subsistema de rede do zephyr utiliza exaustivamente esse recurso para cuidar da transmissão e recepção de pacotes para a PHY.

 

Interessante, não? Mas creio que o usuário queira ver como utilizar as threads no Zephyr, correto? Primeiramente, onde encontrar as coisas? A interface para uso dos recursos de kernel do Zephyr pode ser encontrada no arquivo /include/kernel.h. Assim, primeiramente vamos instanciar uma task:

 

 

Para quem está acostumado ao uso de RTOS, pouco temos de novidade aqui. Para alocar uma thread basicamente precisamos reservar seu TCB (o conhecido bloco de controle da thread), a área de pilha e o ponto de entrada, que nada mais do que o código da thread propriamente dito. De posse dessas informações, basta chamar a API do k_thread_create(), a documentação em kernel.h dessa API encontra-se na linha  444 e inclui a descrição dos parâmetros extras de cada thread. Agora que já sabemos como criar uma thread, vamos controlá-la. O exemplo clássico é colocando a thread para dormir por um período de tempo e depois acordando-a. Para isso vamos utilizar os samples fornecidos, navegue até samples/basic/blink_red/src e abra o arquivo main, você deve dar de cara com isso:

 

 

Ignore as demais linhas, vamos diretamente para a linha 34, em k_sleep(), essa função é a equivalente ao conhecido Task Delay presente em diversos outros RTOS. Basicamente a thread dorme por um período especificado em ticks (no Zephyr kernel o valor padrão é 1 tick para 1 milissegundo, ou seja, o valor passado como argumento vai corresponder diretamente o tempo em milissegundos que a thread vai dormir). Umas sugestão interessante, ao rodar esse exemplo, coloque o osciloscópio diretamente no I/O onde está conectado o LED. O usuário vai notar que a thread realmente vai executar a cada 1 segundo, com pouco ou nenhum jitter, o que denota como característica chave de um RTOS, a execução em tempo real.

 

Para rodar o exemplo acima, estou utilizando a conhecida Freedom Board K64F da NXP, observada abaixo:

 

 

A escolha se dá pelo fato de ser uma das placas nativamente suportadas pelo Zephyr, mas o usuário pode adicionar seu hardware personalizado. Falaremos como portar uma nova placa nos próximos artigos.

 

Para rodar o exemplo, prepare o ambiente do Zephyr (modifique seu bash_profile se necessário, ou ajuste as variáveis de ambiente do Eclipse, como já cobrimos nos artigos anteriores):

 

 

Utilizando o comando make, o firmware será compilado gerando os artefatos executáveis em outdir/frdm_k64f  com o nome padrão de zephyr./elf/bin/hex. Com isso você poderá debugar o kernel ou simplesmente fazer o download da imagem.

 

 

Conta mais sobre o controle de Threads!

 

O exemplo acima é o conhecido blink led, mas as funções de controle de thread do Zephyr vão além disso:

 

 

A conhecida função yield é utilizada para liberar a CPU da thread corrente quando outra de mesma prioridade encontra-se pronta para rodar. Essa função basicamente reordena a runqueue interna do kernel e joga a thread corrente pro fim da fila, e puxa a próxima thread no topo da runqueue. Caso não haja tarefa de mesma prioridade pronta para rodar, essa rotina retorna imediatamente. Yield é a função chave para usar as fibers do kernel, permitindo que todas elas rodem liberando a CPU quando seu objetivo for atingido.

 

 

Todo bom RTOS deve contar com a função Get Current, e no Zephyr não é diferente. Essa função retorna o id da task corrente, muito útil quando a aplicação não memoriza o id de determinada task durante sua criação.

 

 

Priority Getters / Setters são as funções chaves quando a aplicação necessita de alocação de prioridade dinâmica. O que basicamente ela faz é retornar o valor da prioridade da thread especificada e, respectivamente, modificar seu valor. Um cuidado deve ser tomado com seu uso, pois ao passar um valor de prioridade igual ou menor que -1 a thread em questão irá automaticamente para o modo fiber!

 

 

Suspend/Resume encerram nossa descrição do thread management principal. k_thread_suspend faz a thread dormir (ou seja, liberando outra thread ou a IDLE para execução) até que alguém invoque k_thread_resume. Lembrando que como todo RTOS, uma vez que qualquer função do thread management seja chamada e a execução seja efetuada com sucesso, o scheduler será invocado em seguida, o que pode culminar na preempção da thread corrente. Portanto, atenção a esse ponto durante do desenvolvimento da sua aplicação.

 

 

Conclusão

 

Espero que esse artigo tenha despertado o interesse do leitor em conhecer mais sobre o Zephyr kernel. Cobrimos os primeiros passos teóricos para uso do thread manager e algumas de suas funções. A ideia de retomar o Zephyr está basicamente no meu slot de tempo que aumentou um pouquinho (risos) e no recente boom de contribuição que o kernel está recebendo, inclusive dos fabricantes de semicondutores. Ficou curioso sobre como anda o kernel? Manda sua dúvida aqui pra gente! Nó próximo artigo teremos um curto texto recheado com alguns casos de uso práticos na K64F. Então é isso, até a próxima!

 

 

Referências

 

Outros artigos da série

<< Debug do kernel do Zephyr com o Eclipse IDE
Este post faz da série Zephyr. Leia também os outros posts da série:
NEWSLETTER

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Obrigado! Sua inscrição foi um sucesso.

Ops, algo deu errado. Por favor tente novamente.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Felipe Neves
Desenvolvedor de sistemas embarcados apaixonado pelo que faz, divide seu tempo entre trabalhar no Venturus desenvolvendo firmware de tudo quanto é coisa, na Aeolus Robotics se envolvendo com o que há de mais legal em robótica e na Overlay Tech desenvolvendo algumas coisas bem legais para o campo de motion control. Possui mestrado em engenharia elétrica pela Poli-USP e possui interesse em tópicos como: Software embarcado, sistemas de tempo real, controle, robótica móvel e manipuladores, Linux embedded e quase qualquer coisa por onde passe um elétron.

Deixe um comentário

avatar
 
  Notificações  
Notificar