Pequenas experiências multitarefa em um microcontrolador

Em uma das notas anteriores, o autor tentou argumentar que, ao programar o microcontrolador, uma simples troca de tarefas será útil em situações em que o uso do sistema operacional em tempo real é muito alto e o loop abrangente para todas as ações necessárias é muito pequeno ( Ele disse, assim como o conde de La Fer). Mais precisamente, não muito pouco, mas muito confuso.


Em uma nota subseqüente, foi planejado otimizar o acesso aos recursos compartilhados por várias tarefas usando filas baseadas em buffers de anel (FIFOs) e uma tarefa separada especialmente designada para isso. Tendo dispersado para tarefas diferentes aquelas ações que não estão relacionadas entre si, temos o direito de esperar um código mais visível. E se ao mesmo tempo obtivermos alguma conveniência e simplicidade, por que não tentar?


Obviamente, o microcontrolador não foi projetado para resolver qualquer tarefa concebível do usuário. Então, talvez, esse alternador de tarefas seja bastante suficiente em muitas situações. Em suma, é improvável que um pequeno experimento doa. Portanto, para não ser infundado, seu humilde servo decidiu escrever algo e testar seus rabiscos.


Nos microcontroladores, devo dizer, o requisito de considerar o tempo como algo importante e difícil é mais comum do que nos computadores de uso geral. Ir além da estrutura no primeiro caso é equivalente a inoperabilidade, e no segundo caso, apenas leva a um aumento no tempo de espera, o que é bastante aceitável se os nervos estiverem em ordem. Existem até dois termos "tempo real suave" e "tempo real difícil".


Deixe-me lembrá-lo, estávamos conversando sobre controladores com o núcleo Cortex-M3,4,7. Hoje é uma família muito comum. Nos exemplos abaixo, usamos o microcontrolador STM32F303, que faz parte da placa STM32F3DISCOVERY.


O switch é um único arquivo assembler.
O montador não tem medo do autor, mas, pelo contrário, inspira a esperança de que a velocidade máxima seja alcançada.


Inicialmente, a lógica mais simples da operação do comutador foi planejada, a qual é apresentada na Figura 1 para oito tarefas.



Nesse esquema, as tarefas levam uma parte do tempo, uma por uma, e podem apenas dar o restante do tick e, se necessário, ignorar alguns dos ticks. Essa lógica provou ser boa, porque o tamanho quântico pode ser pequeno. E é exatamente isso que é necessário para não levantar urgentemente uma tarefa para a qual uma interrupção acabou de acontecer, e também aumentar e depois diminuir sua prioridade. O pacote que acabou de ser recebido aguardará silenciosamente de 200 a 300 microssegundos até que sua tarefa receba seu tique. E se tivermos um Cortex-M7 operando com uma frequência de 216 MHz, 20 microssegundos por um tick é bastante razoável, pois levará menos de meio microssegundo para alternar. E qualquer tarefa do exemplo acima nunca terá mais de 140 microssegundos de atraso.


No entanto, com um aumento no número de tarefas, mesmo com um tamanho extremamente pequeno do quantum de tempo, o atraso no início da atividade da tarefa necessária pode deixar de ser agradável. Com base nisso, e também levando em consideração que apenas uma pequena parte das tarefas realmente requer tempo real, foi decidido modificar levemente a lógica do comutador. É mostrado na Figura 2.



Agora, selecionamos apenas uma parte das tarefas que recebem um quantum inteiro e selecionamos apenas uma marca para o resto, na qual elas se revezam no jogo. Nesse caso, a sub-rotina de inicialização recebe um parâmetro de entrada, ou seja, o número da posição, a partir do qual todas as tarefas serão afetadas em direitos e compartilharão um tique. Ao mesmo tempo, o esquema antigo permaneceu disponível, para isso basta definir o valor do parâmetro como zero ou o número total de tarefas. Os custos de troca aumentaram apenas com algumas instruções do montador.


Dois esquemas semelhantes são usados ​​para permitir o acesso a recursos compartilhados. O primeiro, mencionado em uma nota anterior, usa vários FIFOs (ou buffers circulares pelo número de produtores de mensagens) e uma tarefa de correspondência separada. Ele foi projetado para se comunicar com o mundo exterior e não exige expectativas de tarefas que geram mensagens. É necessário apenas garantir que as filas não estejam lotadas.


O segundo esquema também usa uma tarefa separada para permitir o acesso, mas apresenta expectativas porque gerencia o recurso interno nas duas direções. Essas ações não podem ser vinculadas ao tempo. A Figura 3 mostra os componentes do segundo circuito.



Os principais elementos são um buffer de solicitações, de acordo com o número de tarefas desejadas, e um indicador de acesso. A operação deste design é bastante simples. A tarefa à esquerda envia uma solicitação de acesso a um local especialmente alocado para ela (por exemplo, a tarefa 2 grava 1 na Solicitação 2). Tarefa - o expedidor seleciona quem permitir e grava o número da tarefa selecionada no sinalizador de resolução. A tarefa que recebeu permissão executa suas ações e grava o sinal do final do acesso à solicitação, o valor 0xFF. O agendador, vendo que a solicitação é limpa, redefine o sinalizador de permissão, redefine a solicitação anterior e redefine a solicitação de outra tarefa.


Dois projetos de teste no IAR e uma descrição da placa STM32F3DISCOVERY usada podem ser vistos aqui . No primeiro projeto, o ATS303 simplesmente verificou seu desempenho e o depurou. Todos os LEDs instalados nesta placa foram úteis. Ninguém ficou ferido.


O segundo rascunho do BTS303 testou as duas opções de alocação de recursos mencionadas. Nele, as tarefas 1 e 2 geram mensagens de teste recebidas pelo operador. Para me comunicar com o operador, tive que adicionar um cachecol com uma porta TTL COM, como mostra a foto abaixo.



O operador usa um emulador de terminal. Penso que o leitor desculpará o autor pela cor do tubo macio. Parece assim.



Para iniciar o sistema inteiro, antes de resolver as interrupções, são necessárias etapas preliminares no corpo da tarefa zero principal (), que são apresentadas abaixo.


void main_start_task_switcher(U8 border); U8 task_run_and_return_task_number((U32)t1_task); U8 task_run_and_return_task_number((U32)t2_task); U8 task_run_and_return_task_number((U32)t3_human_link); U8 task_run_and_return_task_number((U32)t4_human_answer); U8 task_run_and_return_task_number((U32)task_5); U8 task_run_and_return_task_number((U32)task_6); U8 task_run_and_return_task_number((U32)task_7); 

Nessas linhas, o switch inicia primeiro e, em seguida, as sete tarefas restantes.


Aqui está o conjunto mínimo de chamadas necessárias para o trabalho.


  void task_wake_up_action(U8 taskNumber); 

Essa chamada é usada em uma interrupção de um timer de hardware do usuário. Os desafios das próprias tarefas falam por si.


  void release_me_and_set_sleep_steps(U32 ticks); U8 get_my_number(void); 

Todas essas funções estão no arquivo de opção do assembler. Existem várias outras funções que são úteis para teste, mas não são necessárias.


No projeto BTS303, a tarefa 3 recebe comandos do operador de fora e envia a ele as respostas da tarefa 4. A tarefa 4 recebe comandos do operador da tarefa 3 e os executa com possíveis respostas. A tarefa 3 também recebe mensagens das tarefas 1 e 2 e a envia via UART para o emulador de terminal (por exemplo, massa de vidraceiro).


A tarefa 0 (principal) realiza algum trabalho auxiliar, por exemplo, verifica o número de palavras deixadas não afetadas na área empilhada de cada tarefa. Esta informação pode ser solicitada pelo operador e ter uma idéia do uso da pilha. Inicialmente, para cada tarefa, uma área de pilha de 512 bytes (128 palavras) é alocada e é necessário monitorar (pelo menos no estágio de depuração) que essas áreas não chegam perto do estouro.


As tarefas 5 e 6 fazem cálculos em alguma variável comum de ponto flutuante. Para fazer isso, eles solicitam acesso a ele na tarefa 7.


Há outro recurso adicional que pode ser visto em projetos de teste. Ele foi projetado para que você possa despertar a tarefa não após o número de tiques expirar, mas após um tempo especificado, e a aparência é a seguinte.


  void wake_me_up_after_milliSeconds(U32 timeMS); 

Para sua implementação, também é necessário um cronômetro de hardware adicional, que também é implementado nos casos de teste.


Como você pode ver, a lista de todas as chamadas necessárias cabe em uma página.

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


All Articles