Multitarefa é importante nos dias de hoje. Gostaria de saber como podemos alcançá-lo em microcontroladores e programação incorporada. Estou projetando um sistema baseado em um microcontrolador PIC. Eu projetei seu firmware no MplabX IDE usando C e depois criei um aplicativo para ele no Visual Studio usando C #.
Desde que me acostumei a usar threads na programação C # na área de trabalho para implementar tarefas paralelas, existe uma maneira de fazer o mesmo no meu código de microcontrolador? O MplabX IDE fornece, pthreads.h
mas é apenas um esboço sem implementação. Eu sei que há suporte para o FreeRTOS, mas usá-lo torna seu código mais complexo. Alguns fóruns dizem que as interrupções também podem ser usadas como multitarefa, mas não acho que interrupções sejam equivalentes a threads.
Estou projetando um sistema que envia alguns dados para um UART e, ao mesmo tempo, ele precisa enviar dados para um site via Ethernet (com fio). Um usuário pode controlar a saída pelo site, mas a saída é LIGADA / DESLIGADA com um atraso de 2-3 segundos. Então esse é o problema que estou enfrentando. Existe alguma solução para multitarefa em microcontroladores?
fonte
Respostas:
Existem dois tipos principais de sistemas operacionais multitarefa, preventivo e cooperativo. Ambos permitem que várias tarefas sejam definidas no sistema, a diferença é como a alternância de tarefas funciona. Obviamente, com um único processador principal, apenas uma tarefa está sendo executada por vez.
Ambos os tipos de sistemas operacionais multitarefa requerem uma pilha separada para cada tarefa. Portanto, isso implica duas coisas: primeiro, que o processador permita que as pilhas sejam colocadas em qualquer lugar da RAM e, portanto, tenha instruções para mover o ponteiro da pilha (SP) - ou seja, não há uma pilha de hardware para fins especiais, como existe na gama baixa PIC's. Isso deixa de fora as séries PIC10, 12 e 16.
Você pode escrever um sistema operacional quase inteiramente em C, mas o alternador de tarefas, onde o SP se move, precisa estar em montagem. Em vários momentos, escrevi alternadores de tarefas para o PIC24, PIC32, 8051 e 80x86. As tripas são bem diferentes, dependendo da arquitetura do processador.
O segundo requisito é que haja RAM suficiente para fornecer várias pilhas. Normalmente, você gostaria de pelo menos algumas centenas de bytes para uma pilha; mas mesmo com apenas 128 bytes por tarefa, oito pilhas exigirão 1K bytes de RAM - você não precisa alocar a mesma pilha de tamanho para cada tarefa. Lembre-se de que você precisa de pilha suficiente para lidar com a tarefa atual e todas as chamadas para suas sub-rotinas aninhadas, mas também empilhar espaço para uma chamada de interrupção, pois você nunca sabe quando uma ocorrerá.
Existem métodos bastante simples para determinar quanta pilha você está usando para cada tarefa; por exemplo, você pode inicializar todas as pilhas para um valor específico, por exemplo, 0x55, executar o sistema por um tempo e parar e examinar a memória.
Você não diz que tipo de PIC deseja usar. A maioria dos PIC24 e PIC32 terá muito espaço para executar um sistema operacional multitarefa; o PIC18 (o único PIC de 8 bits a ter pilhas na RAM) tem um tamanho máximo de RAM de 4K. Então, isso é bastante duvidoso.
Com a multitarefa cooperativa (a mais simples das duas), a troca de tarefas só é feita quando a tarefa "desiste" de seu controle de volta ao sistema operacional. Isso acontece sempre que a tarefa precisa chamar uma rotina do SO para executar algumas funções pelas quais aguardará, como uma solicitação de E / S ou uma chamada de timer. Isso torna mais fácil para o sistema operacional alternar pilhas, pois não é necessário salvar todos os registros e informações de estado, o SP pode ser alternado para outra tarefa (se não houver outras tarefas prontas para execução, uma pilha inativa será dado controle). Se a tarefa atual não precisar fazer uma chamada do sistema operacional, mas já estiver em execução há algum tempo, precisará abrir mão do controle voluntariamente para manter o sistema responsivo.
O problema da multitarefa cooperativa é que, se a tarefa nunca abrir mão do controle, ela pode monopolizar o sistema. Somente ele e todas as rotinas de interrupção que recebem controle podem ser executados; portanto, o sistema operacional parece travar. Este é o aspecto "cooperativo" desses sistemas. Se um cronômetro de vigilância for implementado e redefinido apenas quando uma alternância de tarefas for executada, é possível capturar essas tarefas erradas.
O Windows 3.1 e versões anteriores eram sistemas operacionais cooperativos, razão pela qual em parte o desempenho deles não foi tão bom.
A multitarefa preemptiva é mais difícil de implementar. Aqui, as tarefas não são necessárias para abrir mão do controle manualmente, mas, em vez disso, cada tarefa pode ter um tempo máximo para execução (por exemplo, 10 ms) e, em seguida, uma alternância de tarefas é executada para a próxima tarefa executável, se houver uma. Isso requer interromper arbitrariamente uma tarefa, salvar todas as informações de estado e, em seguida, alternar o SP para outra tarefa e iniciá-la. Isso torna o alternador de tarefas mais complicado, requer mais pilha e torna o sistema um pouco mais lento.
Para multitarefa cooperativa e preventiva, podem ocorrer interrupções a qualquer momento, o que antecipará temporariamente a tarefa em execução.
Como a supercat aponta em um comentário, uma vantagem da multitarefa cooperativa é que é mais fácil compartilhar recursos (por exemplo, hardware como um ADC multicanal ou software como modificar uma lista vinculada). Às vezes, duas tarefas desejam acessar o mesmo recurso ao mesmo tempo. Com o planejamento preventivo, seria possível que o sistema operacional alternasse tarefas no meio de uma tarefa usando um recurso. Portanto, os bloqueios são necessários para impedir que outra tarefa entre e acesse o mesmo recurso. Com a multitarefa cooperativa, isso não é necessário porque a tarefa controla quando será lançada de volta ao sistema operacional.
fonte
void foo(void* context)
a lógica do controlador (kernel) puxa um ponteiro e o par de ponteiros de funções da fila e o chama um de cada vez. Essa função usa o contexto para armazenar suas variáveis e tal e, em seguida, pode adicionar enviar uma continuação à fila. Essas funções devem retornar rapidamente para permitir que outras tarefas sejam o momento na CPU. Este é um método baseado em eventos que requer apenas uma única pilha.A segmentação é fornecida por um sistema operacional. No mundo incorporado, geralmente não temos um sistema operacional ("bare metal"). Portanto, isso deixa as seguintes opções:
Aconselho que você use o mais simples dos esquemas acima que funcionará para o seu aplicativo. Pelo que você descreve, eu teria o loop principal gerando pacotes e colocando-os em buffers circulares. Em seguida, tenha um driver baseado no UART ISR que seja acionado sempre que o byte anterior terminar de enviar até que o buffer seja enviado e aguarde mais conteúdo do buffer. Abordagem semelhante para a Ethernet.
fonte
Como em qualquer processador single-core, não é possível realizar multitarefa de software real. Portanto, você deve alternar entre várias tarefas de uma maneira. Os diferentes RTOS estão cuidando disso. Eles têm um agendador e, com base em uma marca do sistema, alternam entre diferentes tarefas para oferecer a você uma capacidade de multitarefa.
Os conceitos envolvidos nesse processo (economia e restauração de contexto) são bastante complicados; portanto, fazer isso manualmente provavelmente será difícil e tornará seu código mais complexo e, como você nunca fez isso antes, haverá erros nele. Meu conselho aqui seria usar um RTOS testado como o FreeRTOS.
Você mencionou que as interrupções fornecem um nível de multitarefa. Isso é meio que verdade. A interrupção interromperá seu programa atual a qualquer momento e executará o código, é comparável a um sistema de duas tarefas em que você tem 1 tarefa com baixa prioridade e outra com alta prioridade que termina dentro de um intervalo de tempo do agendador.
Assim, você pode escrever um manipulador de interrupção para um timer recorrente que enviará alguns pacotes pelo UART, e então o resto do seu programa será executado por alguns milissegundos e os próximos bytes. Dessa forma, você obtém um recurso limitado de multitarefa. Mas você também terá uma interrupção bastante longa, o que pode ser uma coisa ruim.
A única maneira real de realizar várias tarefas ao mesmo tempo em um MCU de núcleo único é usar o DMA e os periféricos, pois eles trabalham independentemente do núcleo (o DMA e o MCU compartilham o mesmo barramento, para que trabalhem um pouco mais devagar quando ambos estão ativos). Assim, enquanto o DMA embaralha os bytes para o UART, seu núcleo fica livre para enviar as coisas para a Ethernet.
fonte
As outras respostas já descreviam as opções mais utilizadas (loop principal, ISR, RTOS). Aqui está outra opção como compromisso: Protothreads . É basicamente uma biblioteca muito leve para threads, que usa o loop principal e algumas macros C para "emular" um RTOS. Claro que não é um sistema operacional completo, mas para threads "simples", pode ser útil.
fonte
Meu design básico para um RTOS fatiado com o mínimo de tempo não mudou muito em várias micro famílias. É basicamente uma interrupção do temporizador ao dirigir uma máquina de estado. A rotina de serviço de interrupção é o kernel do sistema operacional, enquanto a instrução switch no loop principal são as tarefas do usuário. Drivers de dispositivo são rotinas de serviço de interrupção para interrupções de E / S.
A estrutura básica é a seguinte:
Este é basicamente um sistema multitarefa cooperativo. As tarefas são gravadas para nunca inserir um loop infinito, mas não nos importamos porque as tarefas são executadas em um loop de eventos, portanto o loop infinito está implícito. Esse é um estilo semelhante de programação para linguagens orientadas a eventos / sem bloqueio, como javascript ou go.
Você pode ver um exemplo desse estilo de arquitetura no meu software de transmissor RC (sim, na verdade, eu o uso para pilotar aviões RC, por isso é um pouco crítico em termos de segurança me impedir de bater nos aviões e potencialmente matar pessoas): https://github.com / slebetman / pic-txmod . Ele possui basicamente 3 tarefas - 2 tarefas em tempo real implementadas como drivers de dispositivo com estado (consulte o material do ppmio) e 1 tarefa em segundo plano implementando a lógica de mixagem. Então, basicamente, é semelhante ao seu servidor web, pois possui 2 threads de E / S.
fonte
Embora eu aprecie que a pergunta indique especificamente sobre o uso de um RTOS incorporado, me ocorre que a pergunta mais ampla que está sendo feita é "como obter multitarefa em uma plataforma incorporada".
Eu recomendo fortemente que você esqueça de usar um RTOS incorporado, pelo menos por enquanto. Aconselho isso porque acho que é essencial primeiro aprender sobre como obter 'simultaneidade de tarefas' por meio de técnicas de programação extremamente simples que consistem em agendadores de tarefas simples e máquinas de estado.
Para explicar de maneira extremamente resumida o conceito, cada módulo de trabalho que precisa ser realizado (ou seja, cada 'tarefa') possui uma função específica que deve ser chamada ('marcada') periodicamente para que esse módulo faça algumas coisas. O módulo mantém seu próprio estado atual. Você tem um loop infinito principal (o agendador) que chama as funções do módulo.
Ilustração bruta:
Uma estrutura de programação de encadeamento único como esta, na qual você chama periodicamente as funções da máquina de estado principal a partir de um loop do agendador principal, é onipresente na programação incorporada, e é por isso que eu encorajo fortemente o OP a se familiarizar e se familiarizar com ele primeiro, antes de mergulhar diretamente no uso Tarefas / threads do RTOS.
Trabalho em um tipo de dispositivo incorporado que possui uma interface LCD de hardware, servidor web interno, cliente de email, cliente DDNS, VOIP e muitos outros recursos. Embora utilizemos um RTOS (Keil RTX), o número de threads individuais (tarefas) usados é muito pequeno e a maior parte da 'multitarefa' é alcançada como descrito acima.
Para dar alguns exemplos de bibliotecas que demonstram esse conceito:
A biblioteca de redes Keil. Toda a pilha TCP / IP pode ser executada em thread único; você chama periodicamente main_TcpNet (), que itera a pilha TCP / IP e qualquer outra opção de rede que você tenha compilado da biblioteca (por exemplo, o servidor da web). Consulte http://www.keil.com/support/man/docs/rlarm/rlarm_main_tcpnet.htm . É certo que em algumas situações (possivelmente fora do escopo desta resposta) você chega a um ponto em que começa a se tornar benéfico ou necessário o uso de threads (principalmente se estiver usando soquetes BSD de bloqueio). (Observação: o novo V5 MDK-ARM na verdade gera um encadeamento Ethernet dedicado - mas estou apenas tentando fornecer uma ilustração.)
A biblioteca VOIP do Linphone. A biblioteca linphone em si é de thread único. Você chama a
iterate()
função em um intervalo suficiente. Consulte http://www.linphone.org/docs/liblinphone-javadoc/org/linphone/core/LinphoneCore.html#iterate () . (Um exemplo ruim, porque usei isso em uma plataforma Linux incorporada e as bibliotecas de dependência do linphone, sem dúvida, geram threads, mas novamente é para ilustrar um ponto.)Voltando ao problema específico descrito pelo OP, o problema parece ser o fato de que a comunicação UART deve ocorrer ao mesmo tempo que algumas redes (transmissão de pacotes via TCP / IP). Não sei qual biblioteca de rede você está realmente usando, mas eu suponho que ela tenha uma função principal que precisa ser chamada com frequência. Você precisaria escrever seu código que lida com a transmissão / recepção de dados UART para ser estruturado de maneira semelhante, como uma máquina de estado que pode ser iterada por chamadas periódicas para uma função principal.
fonte