Synergy e ThreadX: explorando a estrutura básica do RTOS

No artigo anterior conhecemos um pouco sobre a linha Synergy da Renesas e mostramos como criar threads no ThreadX utilizando o E2Studio. Neste artigo vamos olhar alguns detalhes sobre a operação do ThreadX e conhecer funções da API do kernel.

A primeira coisa que precisamos saber acerca do ThreadX (e de  RTOSs em geral) é que normalmente nenhum código do usuário é inserido na função main()! Na verdade, o código da função main() é extremamente simples e, no caso do E2 Studio, é gerado pela ferramenta Synergy Configuration do E2 Studio. Vejamos o código da função main() utilizada nos exemplos anteriores:

int main(void)
{
    __disable_irq ();
    tx_kernel_enter ();
    return 0;
}

Simples não é? Veja que toda a mágica acontece na função tx_kernel_enter() que, quando chamada, inicializa o ThreadX e inicia o escalonador de tarefas. Alguns leitores podem estar se perguntando: mas o quê acontece após o retorno desta função? Porquê não há um loop do tipo for ou while no main()?

A resposta é que a função tx_kernel_enter() não retorna! Uma vez iniciado o escalonador de tarefas, o kernel do RTOS assume o controle da máquina e somente o código das threads criadas pelo usuário é que será executado! O kernel irá chamar a função tx_application_define() que é responsável por criar as threads e outros objetos da aplicação. No caso do exemplo anterior, o código simplificado da função é apresentado abaixo:

void tx_application_define(void * first_unused_memory)
{
    g_ssp_common_thread_count = 0;
    g_ssp_common_initialized = false;
    /* Create semaphore to make sure common init is done before threads start running. */
    tx_semaphore_create (&g_ssp_common_initialized_semaphore, "SSP Common Init Sem", 1);
    blinky_thread_create ();
    red_thread_create ();
    tx_application_define_user (first_unused_memory);
}

As funções blinky_thread_create() e red_thread_create() foram criadas quando comandamos a geração de código no Synergy Configuration. O código das mesmas está definido nos arquivos blinky_thread.c e red_thread.c respectivamente (estes arquivos e seus headers são gerados automaticamente pelo Synergy Configuration e encontram-se na pasta src/synergy_gen dentro da estrutura do projeto). A função de criação da thread red_thread está apresentada a seguir:

void red_thread_create(void)
{
    /* Increment count so we will know the number of ISDE created threads. */
    g_ssp_common_thread_count++;
    /* Initialize each kernel object. */
    tx_thread_create (&red_thread, (CHAR *) "Red Thread", red_thread_func, (ULONG) NULL, &red_thread_stack, 1024, 1, 1,
                      1, TX_AUTO_START);
}

Agora as coisas começam a ficar interessantes! Observe a chamada a função tx_thread_create(), ela é uma função do kernel responsável pelo processo de criação de uma nova thread no ThreadX. Os argumentos da função são os seguintes: tx_thread_create(TX_THREAD *thread_ptr, CHAR *name_ptr, VOID (*entry_function)(ULONG), ULONG entry_input, VOID *stack_start, ULONG stack_size, UINT priority, UINT preempt_threshold, ULONG time_slice, UINT auto_start), onde:

  1. thread_ptr – ponteiro para o bloco de controle da thread (TCB);
  2. name_ptr – nome simbólico da thread (campo name das propriedades da thread no Synergy Configuration);
  3. entry_function – função de entrada da thread;
  4. entry_input – parâmetro a ser passado para a função de entrada da thread;
  5. stack_start – ponteiro para o início da área de pilha da thread;
  6. stack_size – tamanho em bytes da pilha da thread (campo stack size no Synergy Configuration);
  7. priority – prioridade da thread (campo priority no Synergy Configuration);
  8. preemp_threshold – limiar de preempção da thread;
  9. time_slice – número de fatias de tempo para a thread;
  10. auto_start – seleciona se a thread será iniciada automaticamente pelo kernel ou deverá ser iniciada manualmente pela aplicação.

Conhecer a maioria dos parâmetros acima é imprescindível para melhor utilizar o potencial do ThreadX, a seguir apresentamos alguns pontos relacionados aos mesmos.

Dimensionamento da pilha

Um cuidado especial deve ser dedicado na especificação do tamanho da pilha da thread. Lembre-se de que uma thread do RTOS nada mais é do que uma função C, isto significa que todas as variáveis locais da thread serão armazenadas na pilha da mesma. Além disso, as chamadas a outras funções (e as variáveis locais das mesmas) também farão uso desta mesma pilha. Por esta razão, o uso da pilha pode crescer rapidamente e provocar o crash do sistema caso a thread utilize mais espaço de pilha que o disponível (uma condição conhecida como estouro da pilha ou stack overflow). Neste caso, a thread pode acabar sobrescrevendo dados de outras threads ou mesmo do kernel, provocando um erro potencialmente catastrófico para a aplicação. Vale lembrar que este tipo de situação não é exclusiva de um RTOS e ocorre com certa frequência mesmo em aplicações monotarefa onde o tamanho da pilha é dimensionado de forma inadequada.

O ThreadX, assim como outros RTOS oferece uma série de medidas para auxiliar o desenvolvedor a lidar com problemas relacionados a pilha. A funcionalidade mais básica e ativa por padrão é a inicialização de todas as posições da pilha com um valor conhecido (0xEF). Isto permite que o desenvolvedor possa avaliar a utilização da pilha e dimensionar o seu tamanho de acordo com as suas necessidades. A outra funcionalidade é a checagem em tempo de execução (opção TX_ENABLE_STACK_CHECKING), que utiliza o mecanismo anterior para detectar uma condição de estouro da pilha, o que é feito sempre que a thread é suspensa ou retomada. Caso um estouro de pilha seja detectado, o kernel pode executar uma função de erro adequada.

Via de regra, normalmente inicia-se o desenvolvimento dimensionando-se a pilha com um tamanho maior que o estimado necessário e faz-se o ajuste fino executando-se a aplicação e observando-se a utilização da pilha em diversas condições. Por padrão, a ferramenta Synergy Configuration irá sempre utilizar o tamanho de 1024 bytes para a pilha de cada thread, este valor pode ser alterado pelo programador na janela de propriedades da thread. Nota: não altere o tamanho da pilha diretamente no código pois o E2 Studio vai sobrescrever qualquer alteração feita nos códigos de criação das threads!

Prioridade da thread

Uma característica importante acerca do funcionamento de um RTOS é que é possível atribuir a cada thread um nível de prioridade. O escalonador do kernel irá procurar executar as threads mais prioritárias primeiro (desde que estejam prontas para executar, ou seja, em estado READY), passando para as threads menos prioritárias em seguida. Desta forma, uma thread de prioridade 1 (considerando 0 como a maior prioridade) somente será executada se nenhuma thread de prioridade 0 esteja pronta. Uma thread de prioridade 2 somente será executada se não houver threads de prioridade 0 ou 1 prontas para executar e assim por diante. por padrão o ThreadX utiliza um sistema com 32 níveis de prioridade (0 a 31, sendo 0 a maior prioridade), mas este número pode ser alterado pelo usuário (até 1024 níveis em passos de 32 níveis). Note que para cada 32 novos níveis adicionados, a RAM utilizada pelo kernel aumenta em 128 bytes.

Caso múltiplas threads com o mesmo nível de prioridade estejam prontas para executar, o escalonador utiliza a técnica round-robin para executar cada thread numa fatia de tempo. Opcionalmente é possível alterar a propriedade “Time slicing interval (ticks)” na janela de propriedades da thread de forma que uma thread execute por mais tempo que outra (do mesmo nível de prioridade). Tenha em mente que esta funcionalidade aumenta ligeiramente o tamanho do código e introduz um pequeno tempo adicional de processamento na troca de contexto.

O kernel também inclui uma função especial (tx_thread_relinquish()) que pode ser utilizada para devolver o controle ao kernel de forma que outras threads com o mesmo nível de prioridade (ou mais alto) tenham a chance de executar. O kernel utiliza um mecanismo round-robin de forma que a thread é movida para o fim de uma fila e somente voltará a ser executada depois que todas as threads prontas (READY) de mesma prioridade ou mais alta tenham executado. Esta funcionalidade equivale a um sistema de multitarefa cooperativa.

O ThreadX inclui um mecanismo chamado Preemption-Threshold (limiar de preempção) que permite especificar o nível mínimo de prioridade que causará a preempção (troca de contexto) da thread atual, ou seja, uma thread cujo preemption-threshold seja 10 poderá ser interrompida por qualquer thread com prioridade entre 0 e 9, mas não será interrompida por threads com prioridades inferiores ao limiar (independentemente do nível de prioridade da própria thread). Uma thread pode inclusive alterar dinamicamente este limiar, através da função tx_thread_preemption_change().

Note que uma thread com limiar setado para zero não sofrerá preempção!

A quantidade de níveis de prioridade e as funcionalidades de time-slicing e preemption-threshold são configuradas nas propriedades do kernel, conforme veremos a seguir. Por padrão, cada thread criada pelo Synergy Configuration terá prioridade igual a 1 e um número de time-slices também igual a um. Estas propriedades podem ser alteradas na janela de propriedades da thread.

Reconfigurando o kernel do ThreadX no E2Studio

No ambiente E2 Studio, a configuração das propriedades do kernel deve ser feita na perspectiva Synergy Configuration. Selecione a thread HAL/Common, clique no botão + na janela HAL/Common Stacks e selecione X-Ware > ThreadX > ThreadX Source conforme mostra a figura abaixo.

Figura 1

Após inserir o fonte do ThreadX, uma nova caixa (ThreadX Source) aparecerá na janela HAL/Common Stacks, no entanto ela tem a indicação de erro (x vermelho), um indicativo de que algo está errado na configuração da mesma.

Figura 2

A solução para o problema é simples: basta clicar na caixa ThreadX Source recém criada e na janela de propriedades da mesma, rolar a lista de configurações até o final e marcar a opção “Show linkage warning” como disabled.

Figura 3

Feito isso, podemos alterar as demais configurações e propriedades do ThreadX conforme desejado! Não esqueça de regenerar o código da aplicação (Generate Project Content) para que as novas configurações surtam efeito!

Exemplo: dimensionamento da pilha da thread

Para concluirmos este artigo, vamos demonstrar como proceder para dimensionar corretamente a pilha de uma thread. Além disso, vamos ver também como comandar manualmente o início da execução de uma thread. Vamos aproveitar o exemplo criado no artigo anterior e efetuar algumas alterações no mesmo. O projeto agora será batizado de MultiThreadLED2. Utilizando a ferramenta Synergy Configuration, vamos acessar a janela de propriedades da red_thread e configurar a propriedade Auto start para disabled.

Feito isso, vamos regenerar o código da aplicação (botão Generate Project Content na perspectiva Synergy Configuration) e então alterar o código da blinky_thread conforme abaixo. Não esqueça de modificar as chamadas a tx_thread_sleep() para 50 na função red_thread_entry também!

#include "blinky_thread.h"

extern TX_THREAD red_thread;

void blinky_thread_entry(void) {
    uint8_t redStarted = 0;
    /* LED type structure */
    bsp_leds_t leds;
    /* Get LED information for this board */
    R_BSP_LedsGet(&leds);
    while (1) {
        //g_ioport.p_api->pinWrite(IOPORT_PORT_06_PIN_00, IOPORT_LEVEL_LOW);
        g_ioport.p_api->pinWrite(leds.p_leds[0], IOPORT_LEVEL_LOW);
        tx_thread_sleep (50);
        //g_ioport.p_api->pinWrite(IOPORT_PORT_06_PIN_00, IOPORT_LEVEL_HIGH);
        g_ioport.p_api->pinWrite(leds.p_leds[0], IOPORT_LEVEL_HIGH);
        tx_thread_sleep (50);
        if (!redStarted) {
            redStarted = 1;
            tx_thread_resume(&red_thread);
        }
    }
}

Note que após um ciclo de piscada do led da blinky_thread a função tx_thread_resume() é chamada (caso redStarted seja igual a zero, o que só ocorre no primeiro ciclo). Esta função faz com que o kernel inicie uma thread que esteja suspensa (como é o caso da red_thread, já que ela foi criada, mas não foi iniciada, estando portanto suspensa). A figura 4 mostra o resultado desta modificação, no início os leds estão todos ligados, a blinky_thread inicia e apaga o led verde, ao acende-lo novamente a red_thread é iniciada e a partir do segundo ciclo os dois leds piscam juntos.

Figura 4

Agora vamos verificar a ocupação das pilhas das threads. Como já dissemos, o ThreadX ativa por padrão o preenchimento da pilha com o padrão 0xEF. O objetivo é facilitar a inspeção da ocupação e uso da mesma. Vamos então verificar a utilização das pilhas das nossas duas threads. Para isso, selecione a opção Renesas View > Partner OS > RTOS Resources no menu principal. Uma nova aba (RTOS Resources) será adicionada na parte inferior da tela.

A nova aba inclui diversas vistas da aplicação e do kernel. A figura 5 demonstra a vista da aba Thread onde aparecem as nossas duas threads e alguns informações básicas sobre as mesmas.

Figura 5

Se clicarmos na aba Stack poderemos ver as informações sobre as pilhas de cada tarefa. A última coluna apresenta a utilização máxima de cada thread!

Figura 6

Podemos ver que as pilhas estão configuradas para um tamanho de 1024 bytes, mas a utilização máxima da thread 1 (Blinky Thread) foi de 152 bytes! Poderíamos reduzir tranquilamente o tamanho das pilhas para 256 bytes ou mesmo menos. Lembre-se de que o tamanho da pilha deve ser sempre um número múltiplo de 8!

Atenção: o ajuste fino no dimensionamento da pilha das tarefas deve ser realizado com muita cautela e sempre tomando-se o cuidado de testar todas as possibilidades de execução da aplicação!

No próximo artigo falaremos sobre recursos compartilhados e mutex no ThreadX, até lá!

Referências

Leave a Reply