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
Engenheiro de sistemas embarcados apaixonado pelo que faz, já trabalhou em diversos setores de tecnologia nos últimos 14 anos com destaque para Defesa, Automação, Agricultura, Wearables, Robótica e mais recentemente Semicondutores. Possui sangue maker, tendo paixão por construir e compatilhar suas próprias coisas e explorar novos sabores dentro do mundo "embedded". Atualmente trabalha como Engenheiro de Software Senior na Espressif, sim aquela do ESP32 e do ESP8266. Tem interesse em tópicos que envolvam Robótica, Controle de Movimento, DSP e Sistemas de Tempo Real.

Deixe um comentário

avatar
 
  Notificações  
Notificar