Distribuição estática dos objetos FreeRTOS

Por padrão, todos os objetos no sistema FreeRTOS são distribuídos dinamicamente - filas, semáforos, cronômetros, tarefas (threads) e mutexes. O programador vê apenas o "heap" - a área em que a memória é alocada dinamicamente a pedido de um programa ou sistema, e o que está acontecendo lá dentro não é claro. Quanto resta? Desconhecido Algo leva mais do que você precisa? Quem sabe Pessoalmente, prefiro resolver os problemas de organização da memória, mesmo na fase de escrever o firmware, sem gerar erros de tempo de execução quando a memória inesperadamente termina.

Este artigo é uma continuação lógica de ontem sobre a distribuição estática de objetos na memória do microcontrolador, somente agora em relação aos objetos FreeRTOS. Hoje vamos aprender como colocar objetos FreeRTOS estaticamente, o que nos permitirá entender mais claramente o que está acontecendo na RAM do microcontrolador, como exatamente nossos objetos estão localizados e quanto eles ocupam.

Mas apenas pegar e começar a colocar objetos do FreeRTOS estaticamente não requer muita inteligência - a partir da versão 9.0, o FreeRTOS fornece funções para criar objetos colocados estaticamente. Tais funções têm um sufixo estático no nome e essas funções possuem excelente documentação com exemplos. Escreveremos wrappers C ++ convenientes e bonitos sobre as funções do FreeRTOS que não apenas colocarão objetos estaticamente, mas também ocultarão todos os giblets, além de fornecer uma interface mais conveniente.

Este artigo é destinado a programadores iniciantes, mas que já estão familiarizados com o básico do FreeRTOS e com as primitivas da sincronização de programas multithread. Vamos lá

O FreeRTOS é um sistema operacional para microcontroladores. Bem, ok, não um sistema operacional completo, mas uma biblioteca que permite executar várias tarefas em paralelo. O FreeRTOS também permite que as tarefas troquem mensagens através de filas de mensagens, usem cronômetros e sincronizem tarefas usando semáforos e mutexes.

Na minha opinião, qualquer firmware em que você precise executar duas (ou mais) tarefas simultaneamente pode ser resolvido com muito mais facilidade e elegância se você usar o FreeRTOS. Por exemplo, leituras de leitura de sensores lentos e, ao mesmo tempo, servem ao display. Somente para que sem freios, enquanto os sensores são lidos. Em geral, deve ter! Eu recomendo fortemente para o estudo.

Como disse e escrevi em um artigo anterior, não gosto muito da abordagem de criar objetos dinamicamente, se soubermos seu número e tamanho no estágio de compilação. Se esses objetos são colocados estaticamente, podemos obter uma imagem mais clara e compreensível da alocação de memória no microcontrolador e, portanto, evitar surpresas quando a memória terminar repentinamente.

Consideraremos os problemas de organização de memória do FreeRTOS usando a placa BluePill no microcontrolador STM32F103C8T6 como exemplo. Para não se preocupar com o compilador e o sistema de compilação, trabalharemos no ambiente do ArduinoIDE, instalando o suporte para esta placa. Existem várias implementações do Arduino para STM32 - em princípio, qualquer uma serve. Instalei o stm32duino de acordo com as instruções do projeto Readme.md, um gerenciador de inicialização conforme mencionado neste artigo . O FreeRTOS versão 10.0 é instalado através do gerenciador de bibliotecas do ArduinoIDE. Compilador - gcc 8.2

Criaremos uma pequena tarefa experimental. Pode não haver muito sentido prático nessa tarefa, mas todas as primitivas de sincronização que estão no FreeRTOS serão usadas. Algo assim:

  • 2 tarefas (threads) funcionam em paralelo
  • um temporizador também funciona, que de tempos em tempos envia uma notificação para a primeira tarefa usando um semáforo no modo de espera de sinal
  • a primeira tarefa, após receber a notificação do cronômetro, envia uma mensagem (número aleatório) para a segunda tarefa pela fila
  • o segundo, depois de receber a mensagem, imprime-a no console
  • deixe que a primeira tarefa também imprima algo no console e, para que eles não lutem, o console será protegido pelo mutex.
  • o tamanho da fila pode ser limitado a um elemento, mas, para torná-lo mais interessante, colocamos 1000

A implementação padrão (de acordo com a documentação e os tutoriais) pode ser assim.

#include <STM32FreeRTOS.h> TimerHandle_t xTimer; xSemaphoreHandle xSemaphore; xSemaphoreHandle xMutex; xQueueHandle xQueue; void vTimerCallback(TimerHandle_t pxTimer) { xSemaphoreGive(xSemaphore); } void vTask1(void *) { while(1) { xSemaphoreTake(xSemaphore, portMAX_DELAY); int value = random(1000); xQueueSend(xQueue, &value, portMAX_DELAY); xSemaphoreTake(xMutex, portMAX_DELAY); Serial.println("Test"); xSemaphoreGive(xMutex); } } void vTask2(void *) { while(1) { int value; xQueueReceive(xQueue, &value, portMAX_DELAY); xSemaphoreTake(xMutex, portMAX_DELAY); Serial.println(value); xSemaphoreGive(xMutex); } } void setup() { Serial.begin(9600); vSemaphoreCreateBinary(xSemaphore); xQueue = xQueueCreate(1000, sizeof(int)); xMutex = xSemaphoreCreateMutex(); xTimer = xTimerCreate("Timer", 1000, pdTRUE, NULL, vTimerCallback); xTimerStart(xTimer, 0); xTaskCreate(vTask1, "Task 1", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL); xTaskCreate(vTask2, "Task 2", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL); vTaskStartScheduler(); } void loop() {} 

Vamos ver o que acontece na memória do microcontrolador, se você compilar esse código. Por padrão, todos os objetos do FreeRTOS são colocados na memória dinâmica. O FreeRTOS fornece até cinco implementações de gerenciadores de memória, difíceis de implementar, mas em geral elas têm a mesma tarefa - cortar pedaços de memória para as necessidades do FreeRTOS e do usuário. As peças são cortadas da pilha geral do microcontrolador (usando malloc) ou usam sua própria pilha separada. Que tipo de pilha é usada para nós não é importante - de qualquer forma, não podemos olhar para dentro da pilha.

Por exemplo, para uma pilha do nome FreeRTOS, ela será assim (saída do utilitário objdump)

 ... 200009dc l O .bss 00002000 ucHeap ... 

I.e. vemos um pedaço grande, dentro do qual todos os objetos do FreeRTOS são cortados - semáforos, mutexes, temporizadores, filas e até as próprias tarefas. Os últimos 2 pontos são muito importantes. Dependendo do número de elementos, a fila pode ser bastante grande e é garantido que as tarefas ocupam muito espaço devido à pilha, que também é alocada junto com a tarefa.

Sim, este é um número mínimo de multitarefa - cada tarefa terá sua própria pilha. Além disso, a pilha deve ser grande o suficiente para que não contenha apenas as chamadas e variáveis ​​locais da tarefa em si, mas também a pilha de interrupção, se isso ocorrer. Bem, como uma interrupção pode ocorrer a qualquer momento, todas as tarefas devem ter uma reserva na pilha em caso de interrupção. Além disso, os microcontroladores CortexM podem ter interrupções aninhadas, portanto a pilha deve ser grande o suficiente para acomodar todas as interrupções, se ocorrerem simultaneamente.

O tamanho da pilha de tarefas é definido quando a tarefa é criada pelo parâmetro da função xTaskCreate. O tamanho da pilha não pode ser menor que o parâmetro configMINIMAL_STACK_SIZE (especificado no arquivo de configuração FreeRTOSConfig.h) - essa é a mesma reserva para interrupções. O tamanho da pilha é definido pelo parâmetro configTOTAL_HEAP_SIZE e, neste caso, é 8kb.

Agora tente adivinhar se todos os nossos objetos caberão em um monte de 8kb? E alguns objetos? E mais algumas tarefas?
Com certas configurações do FreeRTOS, todos os objetos não cabiam no heap. E fica assim: o programa simplesmente não funciona. I.e. tudo é compilado, derramado, mas o microcontrolador trava e é isso. E vá adivinhar que o problema é exatamente o tamanho da pilha. Eu tive que aumentar um monte para 12kb.

Pare, quais são as variáveis ​​xTimer, xQueue, xSemaphore e xMutex? Eles não descrevem os objetos que precisamos? Não, são apenas identificadores - ponteiros para uma determinada estrutura (opaca), que descreve os próprios objetos de sincronização

 200009cc g O .bss 00000004 xTimer 200009d0 g O .bss 00000004 xSemaphore 200009cc g O .bss 00000004 xQueue 200009d4 g O .bss 00000004 xMutex 

Como já mencionei, proponho reparar toda essa bagunça da mesma maneira que no artigo anterior - distribuiremos todos os nossos objetos estaticamente no estágio de compilação. As funções de distribuição estática ficam disponíveis se o parâmetro configSUPPORT_STATIC_ALLOCATION estiver definido como 1 no arquivo de configuração do FreeRTOS.

Vamos começar com as linhas. Aqui está como a documentação do FreeRTOS oferece para alocar filas

 struct AMessage { char ucMessageID; char ucData[ 20 ]; }; #define QUEUE_LENGTH 10 #define ITEM_SIZE sizeof( uint32_t ) // xQueueBuffer will hold the queue structure. StaticQueue_t xQueueBuffer; // ucQueueStorage will hold the items posted to the queue. Must be at least // [(queue length) * ( queue item size)] bytes long. uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ]; void vATask( void *pvParameters ) { QueueHandle_t xQueue1; // Create a queue capable of containing 10 uint32_t values. xQueue1 = xQueueCreate( QUEUE_LENGTH, // The number of items the queue can hold. ITEM_SIZE // The size of each item in the queue &( ucQueueStorage[ 0 ] ), // The buffer that will hold the items in the queue. &xQueueBuffer ); // The buffer that will hold the queue structure. // The queue is guaranteed to be created successfully as no dynamic memory // allocation is used. Therefore xQueue1 is now a handle to a valid queue. // ... Rest of task code. } 

Neste exemplo, a fila é descrita por três variáveis:

  • A matriz ucQueueStorage é onde os elementos da fila serão colocados. O tamanho da fila é definido pelo usuário para cada fila individualmente.
  • A estrutura xQueueBuffer - aqui vive a descrição e o status da fila, tamanho atual, listas de tarefas pendentes, bem como outros atributos e campos necessários para o FreeRTOS para trabalhar com a fila. O nome da variável, na minha opinião, não é totalmente bem-sucedido; no FreeRTOS, isso se chama QueueDefinition (descrição da fila).
  • A variável xQueue1 é o identificador da fila (identificador). Todas as funções de gerenciamento de filas, bem como algumas outras (por exemplo, funções internas para trabalhar com cronômetros, semáforos e mutexes) aceitam esse identificador. Na verdade, isso é apenas um ponteiro para QueueDefinition, mas não sabemos disso (por assim dizer) e, portanto, a alça terá que ser puxada separadamente.

Fazer como no exemplo, é claro, não será um problema. Mas, pessoalmente, não gosto de ter até três variáveis ​​por entidade. Uma classe que pode encapsular já está solicitando. Apenas um problema - o tamanho de cada fila pode variar. Em um lugar, você precisa de uma fila maior; em outro, alguns elementos são suficientes. Como queremos enfileirar estaticamente, devemos, de alguma forma, especificar esse tamanho no momento da compilação. Você pode usar o modelo para isso.

 template<class T, size_t size> class Queue { QueueHandle_t xHandle; StaticQueue_t x QueueDefinition; T xStorage[size]; public: Queue() { xHandle = xQueueCreateStatic(size, sizeof(T), reinterpret_cast<uint8_t*>(xStorage), &xQueueDefinition); } bool receive(T * val, TickType_t xTicksToWait = portMAX_DELAY) { return xQueueReceive(xHandle, val, xTicksToWait); } bool send(const T & val, TickType_t xTicksToWait = portMAX_DELAY) { return xQueueSend(xHandle, &val, xTicksToWait); } }; 

Ao mesmo tempo, as funções de envio e recebimento de mensagens, que foram imediatamente convenientes para nós, também se estabeleceram nesta classe.

A fila será declarada como uma variável global, algo como isto

 Queue<int, 1000> xQueue; 

Envio de mensagem

  xQueue.send(value); 

Receber mensagem

  int value; xQueue.receive(&value); 

Agora vamos lidar com semáforos. E embora tecnicamente (dentro do FreeRTOS) semáforos e mutexes sejam implementados por meio de filas, semanticamente, essas são três primitivas diferentes. Portanto, iremos implementá-los em classes separadas.

A implementação da classe semáforo será bastante trivial - ela simplesmente armazena várias variáveis ​​e declara várias funções.

 class Sema { SemaphoreHandle_t xSema; StaticSemaphore_t xSemaControlBlock; public: Sema() { xSema = xSemaphoreCreateBinaryStatic(&xSemaControlBlock); } BaseType_t give() { return xSemaphoreGive(xSema); } BaseType_t take(TickType_t xTicksToWait = portMAX_DELAY) { return xSemaphoreTake(xSema, xTicksToWait); } }; 

Declaração de semáforo

 Sema xSema; 

Captura de semáforo

  xSema.take(); 

Liberação de semáforo

  xSema.give(); 

Agora mutex

 class Mutex { SemaphoreHandle_t xMutex; StaticSemaphore_t xMutexControlBlock; public: Mutex() { xMutex = xSemaphoreCreateMutexStatic(&xSemaControlBlock); } BaseType_t lock(TickType_t xTicksToWait = portMAX_DELAY) { return xSemaphoreTake(xMutex, xTicksToWait); } BaseType_t unlock() { return xSemaphoreGive(xMutex); } }; 

Como você pode ver, a classe mutex é quase idêntica à classe semáforo. Mas como eu disse semanticamente, essas são entidades diferentes. Além disso, as interfaces dessas classes não são completas e se expandirão em direções completamente diferentes. Portanto, os métodos giveFromISR () e takeFromISR () podem ser adicionados ao semáforo para trabalhar com o semáforo na interrupção, enquanto o mutex apenas possui o método tryLock () adicionado - não possui outras operações semanticamente.

Espero que você saiba a diferença entre um semáforo binário e um mutex.
Eu sempre faço essa pergunta em entrevistas e, infelizmente, 90% dos candidatos não entendem essa diferença. De fato, um semáforo pode ser capturado e liberado de diferentes threads. Mencionei acima o modo semáforo de espera de sinal quando um thread envia um sinal (as chamadas dão ()) e o outro aguarda um sinal (com a função take ()).

O Mutex, pelo contrário, só pode ser liberado do mesmo fluxo (tarefa) que o capturou. Não tenho certeza de que o FreeRTOS monitore isso, mas alguns sistemas operacionais (por exemplo, Linux) seguem isso estritamente.

O Mutex pode ser usado no estilo C, ou seja, chame diretamente bloqueio () / desbloqueio (). Porém, como estamos escrevendo em C ++, podemos tirar proveito dos encantos do RAII e escrever um wrapper mais conveniente que irá capturar e liberar o próprio mutex.

 class MutexLocker { Mutex & mtx; public: MutexLocker(Mutex & mutex) : mtx(mutex) { mtx.lock(); } ~MutexLocker() { mtx.unlock(); } }; 

Ao sair do escopo, o mutex será automaticamente liberado.

Isso é especialmente conveniente se houver várias saídas da função e você não precisar se lembrar constantemente da necessidade de liberar recursos.

  MutexLocker lock(xMutex); Serial.println(value); } // mutex will be unlocked here 

Agora é a vez dos temporizadores.

 class Timer { TimerHandle_t xTimer; StaticTimer_t xTimerControlBlock; public: Timer(const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const UBaseType_t uxAutoReload, void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction) { xTimer = xTimerCreateStatic(pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, &xTimerControlBlock); } void start(TickType_t xTicksToWait = 0) { xTimerStart(xTimer, xTicksToWait); } }; 

Em geral, tudo aqui é semelhante às aulas anteriores, não vou me deter em detalhes. Talvez a API deixe muito a desejar, bem, ou pelo menos exija expansão. Mas meu objetivo é mostrar o princípio, e não trazê-lo para um estado pronto para produção.

E, finalmente, as tarefas. Cada tarefa tem uma pilha e deve ser colocada na memória com antecedência. Usaremos a mesma técnica das filas - escreveremos uma classe de modelo

 template<const uint32_t ulStackDepth> class Task { protected: StaticTask_t xTaskControlBlock; StackType_t xStack[ ulStackDepth ]; TaskHandle_t xTask; public: Task(TaskFunction_t pxTaskCode, const char * const pcName, void * const pvParameters, UBaseType_t uxPriority) { xTask = xTaskCreateStatic(pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, xStack, &xTaskControlBlock); } }; 

Como os objetos de tarefa agora são declarados como variáveis ​​globais, eles serão inicializados como variáveis ​​globais - antes de chamar main (). Isso significa que os parâmetros transferidos para as tarefas também devem ser conhecidos neste estágio. Essa nuance deve ser levada em consideração se, no seu caso, for aprovado algo que precisa ser calculado antes da criação da tarefa (eu tenho NULL lá). Se isso ainda não lhe agradar, considere a opção com variáveis ​​estáticas locais do artigo anterior .

Compile e obtenha o erro:

 tasks.c:(.text.vTaskStartScheduler+0x10): undefined reference to `vApplicationGetIdleTaskMemory' timers.c:(.text.xTimerCreateTimerTask+0x1a): undefined reference to `vApplicationGetTimerTaskMemory' 

Aqui está a coisa. Cada sistema operacional possui uma tarefa especial - Idle Task (a tarefa padrão, a tarefa de não fazer nada). O sistema operacional executa esta tarefa se todas as outras tarefas não puderem ser executadas (por exemplo, dormindo ou aguardando algo). Em geral, esta é a tarefa mais comum, apenas com a menor prioridade. Mas aqui está sendo criado dentro do kernel do FreeRTOS e não podemos influenciar sua criação. Mas desde que começamos a colocar as tarefas estaticamente, precisamos dizer de alguma forma ao sistema operacional onde você deseja colocar a unidade de controle e a pilha dessa tarefa. É para isso que serve o FreeRTOS e nos pede para definir uma função especial vApplicationGetIdleTaskMemory ().

Uma situação semelhante é com a tarefa de temporizadores. Os cronômetros no sistema FreeRTOS não vivem sozinhos - uma tarefa especial está girando no sistema operacional, que atende a esses cronômetros. E essa tarefa também requer um bloco de controle e uma pilha. E assim, o sistema operacional nos pede para indicar onde eles estão usando a função vApplicationGetTimerTaskMemory ().

As próprias funções são triviais e simplesmente retornam os ponteiros correspondentes aos objetos alocados estaticamente.

 extern "C" void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize) { static StaticTask_t Idle_TCB; static StackType_t Idle_Stack[configMINIMAL_STACK_SIZE]; *ppxIdleTaskTCBBuffer = &Idle_TCB; *ppxIdleTaskStackBuffer = Idle_Stack; *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE; } extern "C" void vApplicationGetTimerTaskMemory (StaticTask_t **ppxTimerTaskTCBBuffer, StackType_t **ppxTimerTaskStackBuffer, uint32_t *pulTimerTaskStackSize) { static StaticTask_t Timer_TCB; static StackType_t Timer_Stack[configTIMER_TASK_STACK_DEPTH]; *ppxTimerTaskTCBBuffer = &Timer_TCB; *ppxTimerTaskStackBuffer = Timer_Stack; *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH; } 

Vamos ver o que temos.

Vou esconder o código dos ajudantes embaixo do spoiler, você acabou de ver
 template<class T, size_t size> class Queue { QueueHandle_t xHandle; StaticQueue_t xQueueDefinition; T xStorage[size]; public: Queue() { xHandle = xQueueCreateStatic(size, sizeof(T), reinterpret_cast<uint8_t*>(xStorage), &xQueueDefinition); } bool receive(T * val, TickType_t xTicksToWait = portMAX_DELAY) { return xQueueReceive(xHandle, val, xTicksToWait); } bool send(const T & val, TickType_t xTicksToWait = portMAX_DELAY) { return xQueueSend(xHandle, &val, xTicksToWait); } }; class Sema { SemaphoreHandle_t xSema; StaticSemaphore_t xSemaControlBlock; public: Sema() { xSema = xSemaphoreCreateBinaryStatic(&xSemaControlBlock); } BaseType_t give() { return xSemaphoreGive(xSema); } BaseType_t take(TickType_t xTicksToWait = portMAX_DELAY) { return xSemaphoreTake(xSema, xTicksToWait); } }; class Mutex { SemaphoreHandle_t xMutex; StaticSemaphore_t xMutexControlBlock; public: Mutex() { xMutex = xSemaphoreCreateMutexStatic(&xMutexControlBlock); } BaseType_t lock(TickType_t xTicksToWait = portMAX_DELAY) { return xSemaphoreTake(xMutex, xTicksToWait); } BaseType_t unlock() { return xSemaphoreGive(xMutex); } }; class MutexLocker { Mutex & mtx; public: MutexLocker(Mutex & mutex) : mtx(mutex) { mtx.lock(); } ~MutexLocker() { mtx.unlock(); } }; class Timer { TimerHandle_t xTimer; StaticTimer_t xTimerControlBlock; public: Timer(const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const UBaseType_t uxAutoReload, void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction) { xTimer = xTimerCreateStatic(pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, &xTimerControlBlock); } void start(TickType_t xTicksToWait = 0) { xTimerStart(xTimer, xTicksToWait); } }; template<const uint32_t ulStackDepth> class Task { protected: StaticTask_t xTaskControlBlock; StackType_t xStack[ ulStackDepth ]; TaskHandle_t xTask; public: Task(TaskFunction_t pxTaskCode, const char * const pcName, void * const pvParameters, UBaseType_t uxPriority) { xTask = xTaskCreateStatic(pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, xStack, &xTaskControlBlock); } }; extern "C" void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize) { static StaticTask_t Idle_TCB; static StackType_t Idle_Stack[configMINIMAL_STACK_SIZE]; *ppxIdleTaskTCBBuffer = &Idle_TCB; *ppxIdleTaskStackBuffer = Idle_Stack; *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE; } extern "C" void vApplicationGetTimerTaskMemory (StaticTask_t **ppxTimerTaskTCBBuffer, StackType_t **ppxTimerTaskStackBuffer, uint32_t *pulTimerTaskStackSize) { static StaticTask_t Timer_TCB; static StackType_t Timer_Stack[configTIMER_TASK_STACK_DEPTH]; *ppxTimerTaskTCBBuffer = &Timer_TCB; *ppxTimerTaskStackBuffer = Timer_Stack; *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH; } 


O código para todo o programa.

 Timer xTimer("Timer", 1000, pdTRUE, NULL, vTimerCallback); Sema xSema; Mutex xMutex; Queue<int, 1000> xQueue; Task<configMINIMAL_STACK_SIZE> task1(vTask1, "Task 1", NULL, tskIDLE_PRIORITY); Task<configMINIMAL_STACK_SIZE> task2(vTask2, "Task 2", NULL, tskIDLE_PRIORITY); void vTimerCallback(TimerHandle_t pxTimer) { xSema.give(); MutexLocker lock(xMutex); Serial.println("Test"); } void vTask1(void *) { while(1) { xSema.take(); int value = random(1000); xQueue.send(value); } } void vTask2(void *) { while(1) { int value; xQueue.receive(&value); MutexLocker lock(xMutex); Serial.println(value); } } void setup() { Serial.begin(9600); xTimer.start(); vTaskStartScheduler(); } void loop() {} 

Você pode desmontar o binário resultante e ver o que e como ele está localizado (a saída do objdump é levemente matizada para melhor legibilidade):

 0x200000b0 .bss 512 vApplicationGetIdleTaskMemory::Idle_Stack 0x200002b0 .bss 92 vApplicationGetIdleTaskMemory::Idle_TCB 0x2000030c .bss 1024 vApplicationGetTimerTaskMemory::Timer_Stack 0x2000070c .bss 92 vApplicationGetTimerTaskMemory::Timer_TCB 0x200009c8 .bss 608 task1 0x20000c28 .bss 608 task2 0x20000e88 .bss 84 xMutex 0x20000edc .bss 4084 xQueue 0x20001ed0 .bss 84 xSema 0x20001f24 .bss 48 xTimer 

O objetivo é alcançado - agora tudo está à vista. Cada objeto é visível e seu tamanho é compreensível (bem, exceto que objetos compostos do tipo Tarefa consideram todas as suas peças de reposição em uma única peça). As estatísticas do compilador também são extremamente precisas e desta vez muito úteis.

 Sketch uses 20,800 bytes (15%) of program storage space. Maximum is 131,072 bytes. Global variables use 9,332 bytes (45%) of dynamic memory, leaving 11,148 bytes for local variables. Maximum is 20,480 bytes. 

Conclusão


Embora o FreeRTOS permita criar e excluir tarefas, filas, semáforos e mutexes em tempo real, em muitos casos isso não é necessário. Como regra, é suficiente criar todos os objetos no início uma vez e eles funcionarão até a próxima reinicialização. E esse é um bom motivo para distribuir esses objetos estaticamente no estágio de compilação. Como resultado, obtemos uma compreensão clara da memória ocupada por nossos objetos, onde está o que está e quanto resta de memória livre.

É óbvio que o método proposto é adequado apenas para colocar objetos cuja vida útil é comparável à vida útil de todo o aplicativo. Caso contrário, você deve usar memória dinâmica.

Além do posicionamento estático dos objetos do FreeRTOS, também escrevemos wrappers convenientes sobre as primitivas do FreeRTOS, o que nos permitiu simplificar um pouco o código do cliente e também encapsular

A interface pode ser simplificada, se necessário (por exemplo, não verificando o código de retorno ou não usando tempos limite). Também é importante notar que a implementação está incompleta - não me preocupei com a implementação de todos os métodos possíveis de envio e recebimento de mensagens pela fila (por exemplo, de uma interrupção, envio para o início ou o final da fila), não trabalhei com primitivas de sincronização de interrupções, contando semáforos (não binários), e muito mais

Eu estava com preguiça de trazer esse código para o estado "pegar e usar", só queria mostrar a idéia. Mas quem precisa de uma biblioteca pronta, acabei de me deparar com a biblioteca frt . Tudo nele é praticamente o mesmo, apenas trazido à mente. Bem, a interface é um pouco diferente.

Um exemplo do artigo está aqui .

Obrigado a todos por ler este artigo até o fim. Ficarei feliz em receber críticas construtivas. Também será interessante para mim discutir as nuances nos comentários.

Source: https://habr.com/ru/post/pt459086/


All Articles