Ao escrever para o código MK mais complicado do que "piscar uma luz", o desenvolvedor se depara com as limitações inerentes à programação linear no estilo de "supercycle mais interrupções". O processamento de interrupções requer velocidade e concisão, o que leva a adicionar sinalizadores ao código e tornar o estilo do projeto "super ciclo com interrupções e sinalizadores".
Se a complexidade do sistema aumenta, o número de sinalizadores interdependentes aumenta exponencialmente, e o projeto rapidamente se transforma em um "código de massa" pouco legível e gerenciável.
O uso de sistemas operacionais em tempo real ajuda a se livrar do "código de massa" e a devolver flexibilidade e capacidade de gerenciamento a um projeto MK complexo.
Vários sistemas operacionais cooperativos em tempo real foram desenvolvidos e bastante populares para os microcontroladores AVR. No entanto, todos eles são escritos em C ou Assembler e não são adequados para quem programa o MK no ambiente do BASCOM AVR, privando-os de uma ferramenta tão útil para escrever aplicativos sérios.
Para corrigir essa falha, desenvolvi um RTOS simples para o ambiente de programação BASCOM AVR, que chamo a atenção dos leitores.

Para muitos, o estilo familiar de programação MK é o chamado
supercycle . O código nesse caso consiste em um conjunto de funções, procedimentos e descritores (constantes, variáveis), possivelmente bibliotecas, geralmente chamadas de "código de segundo plano", bem como um loop infinito grande incluído em uma construção do
loop . Na partida, o equipamento do próprio MK e dos dispositivos externos é inicializado primeiro, as constantes e os valores iniciais das variáveis são definidos e, em seguida, o controle é transferido para esse super ciclo infinito.
A simplicidade da super moto é óbvia. A maioria das tarefas executadas pelo MK, de uma maneira ou de outra, cíclica. As desvantagens também são evidentes: se algum dispositivo ou sinal exigir uma reação imediata, o MK o fornecerá assim que o ciclo mudar. Se a duração do sinal for menor que o período do ciclo, esse sinal será ignorado.
No exemplo abaixo, queremos verificar se o botão está
pressionado :
do
Obviamente, se "algum código" funcionar por tempo suficiente, o MK poderá não perceber um toque breve em um botão.
Felizmente, o MK está equipado com um sistema de interrupção que pode resolver esse problema: todos os sinais críticos podem ser "pendurados" nas interrupções e um manipulador pode ser escrito para cada um. Então, o próximo nível aparece: um
super ciclo com interrupções .
O exemplo abaixo mostra a estrutura do programa com uma super ciclo e uma interrupção que processa um clique no botão:
on button button_isr
No entanto, o uso de interrupções apresenta um novo problema: o código do manipulador de interrupções deve ser o mais rápido e mais curto possível; dentro de interrupções, a funcionalidade MK é limitada. Como os AVR MKs não possuem um sistema de interrupção hierárquico, outra interrupção não pode ocorrer dentro de uma interrupção - eles estão desativados no momento. Portanto, a interrupção deve ser executada o mais rápido possível; caso contrário, outras interrupções (e possivelmente as mais importantes) serão ignoradas e não processadas.
Interromper memóriaDe fato, estando dentro da interrupção, o MK pode observar o fato de outra interrupção em um registro especial, o que permite que seja processado posteriormente. No entanto, essa interrupção não pode ser processada imediatamente.
Portanto, não podemos escrever algo complicado no manipulador de interrupções, especialmente se esse código tiver atrasos - porque até que o atraso funcione, o MK não retornará ao programa principal (supercycle) e ficará surdo a outras interrupções.
Por esse motivo, dentro do manipulador de interrupções, muitas vezes você só precisa sinalizar o fato do evento com um sinalizador - o seu para cada evento - e depois verificar e processar os sinalizadores dentro do super-ciclo. É claro que isso prolonga o tempo de reação ao evento, mas pelo menos não perdemos algo importante.
Assim, surge o próximo nível de complexidade - um
super ciclo com interrupções e sinalizadores .
O código a seguir é mostrado:
on button button_isr
Um número considerável de programas para o MK é limitado por isso. No entanto, esses programas geralmente são ainda mais ou menos simples. Se você escrever algo mais complicado, o número de sinalizadores começará a crescer como uma bola de neve, e o código se tornará cada vez mais confuso e ilegível. Além disso, no exemplo acima, o problema com atrasos não foi resolvido. Claro, você pode "travar" uma interrupção separada no cronômetro, e nele ... também controlar vários sinalizadores. Mas isso torna o programa completamente feio, o número de sinalizadores interdependentes está crescendo exponencialmente, e até o próprio desenvolvedor dificilmente pode descobrir esse "código de massa" em breve. Tentar encontrar um erro ou modificar o código geralmente se torna igual nos esforços para desenvolver um novo projeto.
Como resolver o problema do "código de massa" e torná-lo mais legível e gerenciável? O
sistema operacional (OS) vem em socorro. Nele, a funcionalidade que o MK deve implementar é dividida em tarefas cuja operação é controlada pelo sistema operacional.
Tipos de sistemas operacionais para MK
Os sistemas operacionais para MK podem ser divididos em duas grandes classes: SO com crowding out e SO cooperativo. Em qualquer um desses sistemas operacionais, as tarefas são controladas por um procedimento especial chamado
despachante . Em um sistema operacional com
exclusão, o despachante independentemente, a qualquer momento, alterna a execução de uma tarefa para outra, alocando a cada um certo número de ciclos de clock (possivelmente diferentes, dependendo da prioridade da tarefa). Essa abordagem como um todo funciona muito bem, permitindo que você não veja o conteúdo das tarefas: você pode escrever pelo menos o código da tarefa
1: goto 1
- e ainda o restante das tarefas (incluindo esta) serão executadas. No entanto, os SOs preventivos requerem muitos recursos (memória do processador e ciclos de clock), pois cada comutador deve salvar completamente o contexto da tarefa a ser desativada e carregar o contexto do currículo. O contexto aqui refere-se ao conteúdo dos registros da máquina e da pilha (o BASCOM usa duas pilhas - a de hardware, para endereços de retorno de subprogramas, e a de software, para passar argumentos). Essa carga não apenas exige muitos ciclos do processador, mas também o contexto de cada tarefa precisa ser armazenado em algum lugar por um tempo até que funcione. Nos processadores "grandes", inicialmente orientados para multitarefa, essas funções geralmente são suportadas em hardware e possuem muito mais recursos. No AVR MK, não há suporte de hardware para multitarefa (tudo precisa ser feito "manualmente") e a memória disponível é pequena. Portanto, os SOs deslocados, embora existam, não são muito adequados para MKs simples.
Outra coisa é o
sistema operacional cooperativo . Aqui, a própria tarefa controla em que ponto transferir o controle para o expedidor, permitindo que ele inicie outras tarefas. Além disso, as tarefas aqui são necessárias para fazer isso - caso contrário, a execução do código será interrompida. Por um lado, parece que essa abordagem reduz a confiabilidade geral: se uma tarefa travar, nunca chamará o expedidor e todo o sistema deixará de responder. Por outro lado, um código linear ou uma super ciclo não é melhor nesse aspecto - porque eles podem congelar exatamente com o mesmo risco.
No entanto, um sistema operacional cooperativo tem uma vantagem importante. Como aqui o programador define o momento de alternar entre si, isso não pode acontecer repentinamente, por exemplo, enquanto a tarefa está trabalhando com algum recurso ou no meio do cálculo de uma expressão aritmética. Portanto, em um sistema operacional cooperativo, na maioria dos casos, você pode fazer sem manter o contexto. Isso economiza significativamente o tempo e a memória do processador e, portanto, parece muito mais adequado para implementação no MK AVR.
Troca de tarefas no BASCOM AVR
Para implementar a alternância de tarefas no ambiente do BASCOM AVR, o código da tarefa, cada um dos quais é implementado como um procedimento normal, deve em algum lugar chamar o expedidor - também implementado como um procedimento normal.
Imagine que temos duas tarefas, cada uma das quais em algum lugar de seu código é chamada pelo expedidor.
sub task1() do
Suponha que a tarefa 1. tenha sido executada.Vamos ver o que acontece na pilha quando ela executa uma "chamada de despachante":
endereço de retorno para o código principal (2 bytes)
topo da pilha -> endereço de retorno para a Tarefa 1 que chamou o expedidor (2 bytes)
A parte superior da pilha apontará para o endereço da instrução na Tarefa 1, que segue a chamada do despachante (a instrução de
loop em nosso exemplo).
O objetivo do expedidor, no caso mais simples, é transferir o controle para a Tarefa 2. A questão é como fazer isso? (suponha que o expedidor já saiba o endereço da tarefa 2).
Para fazer isso, o despachante deve extrair o endereço de retorno da Tarefa 1 da pilha (e em algum lugar para lembrar) e colocar o endereço da Tarefa 2 nesse local da pilha e, em seguida, dar o comando de retorno. O processador extrairá o endereço colocado da pilha e, em vez de retornar à Tarefa 1, continuará com a execução da Tarefa 2.
Por sua vez, quando a Tarefa 2 chama o despachante, também retiramos a pilha e salvamos o endereço onde será possível retornar à Tarefa 2, e carregamos o endereço da tarefa 1 salva anteriormente na pilha.Dê o comando
return e estaremos no ponto de continuação da Tarefa 1 .
Como resultado, temos uma bagunça:
Tarefa 1 -> Dispatcher -> Tarefa 2 -> Dispatcher -> Tarefa 1 ....
Nada mal! E isso funciona. Mas, é claro, isso não é suficiente para um sistema operacional praticamente utilizável. Afinal, nem sempre e nem todas as tarefas devem funcionar - por exemplo, eles podem
esperar algo (a expiração do tempo de atraso, a aparência de algum sinal etc.). Portanto, as tarefas devem ter um
status (TRABALHOS, PRONTO, ESPERADO, etc.). Além disso, seria bom ter tarefas atribuídas com
prioridade . Então, se mais de uma tarefa estiver pronta para execução, o expedidor continuará a tarefa que tem a maior prioridade.
AQUA RTOS
Para implementar a idéia descrita, foi desenvolvido o cooperativo OS AQUA RTOS, que fornece os serviços necessários para as tarefas e permite a implementação de multitarefa cooperativa no ambiente do BASCOM AVR.
Aviso importante sobre o modo de procedimento no BASCOM AVRAntes de iniciar a descrição do AUQA RTOS, observe que o ambiente do BASCOM AVR suporta dois tipos de procedimentos de endereçamento. Isso é regulado pelo config submode = new | velho.
No caso de especificar a opção antiga, o compilador, em primeiro lugar, compila todo o código linearmente, independentemente de ser usado em algum lugar ou não, e segundo: procedimentos sem argumentos projetados no estilo de subnome / end sub serão percebidos como procedimentos , denominado no estilo do nome: / return. Isso nos permite passar o endereço do procedimento como um rótulo como argumento para outro procedimento usando o modificador bylabel. Isso também se aplica a procedimentos projetados no estilo do subnome / subnível final (você precisa passar o nome do procedimento como um rótulo).
Ao mesmo tempo, o modo submode = old impõe algumas restrições: os procedimentos da tarefa não devem conter argumentos; o código dos arquivos conectados via $ include é incluído linearmente no projeto geral, portanto, o desvio deve ser fornecido nos arquivos conectados - vá do começo ao fim usando goto e um rótulo.
Portanto, no AQUA RTOS, o usuário deve usar apenas a notação de procedimento antigo no estilo task_name: / return para tarefas ou usar o sub name / end sub mais comum, adicionando o modificador submode = old no início de seu código e ignorar os arquivos incluídos Ir para o rótulo / incluir código / rótulo do arquivo:.
Status da tarefa AQUA RTOS
Os seguintes status são definidos para tarefas no AQUA RTOS:
OSTS_UNDEFINE OSTS_READY OSTS_RUN OSTS_DELAY OSTS_STOP OSTS_WAIT OSTS_PAUSE OSTS_RESTART
Se a tarefa ainda não foi inicializada, será atribuído o status
OSTS_UNDEFINE .
Após a inicialização, a tarefa tem o status
OSTS_STOP .
Se a tarefa
estiver pronta para execução , será atribuído o status
OSTS_READY .
A tarefa atualmente em execução tem o status
OSTS_RUN .
A partir dela, ela pode ir para os status
OSTS_STOP, OSTS_READY, OSTS_DELAY, OSTS_WAIT, OSTS_PAUSE .
O status
OSTS_DELAY tem uma tarefa cumprindo um
atraso .
O status
OSTS_WAIT é atribuído a tarefas que
aguardam um semáforo, evento ou mensagem (mais sobre eles abaixo).
Qual é a diferença entre os
status OSTS_STOP e
OSTS_PAUSED ?
Se, por algum motivo, a tarefa receber o status
OSTS_STOP , a continuação subsequente da tarefa (após o recebimento do status
OSTS_READY ) será realizada a partir do seu ponto de entrada, ou seja, desde o começo. No status
OSTS_PAUSE, a tarefa continuará funcionando no local em que foi suspensa.
Gerenciamento de status da tarefa
O próprio sistema operacional pode gerenciar automaticamente as tarefas, assim como o usuário, chamando os serviços do sistema operacional. Existem vários serviços de gerenciamento de tarefas (os nomes de todos os serviços do SO começam com o prefixo
OS_ ):
OS_InitTask(task_label, task_prio) OS_Stop() OS_StopTask(task_label) OS_Pause() OS_PauseTask(task_label) OS_Resume() OS_ResumeTask(task_label) OS_Restart()
Cada um deles tem duas opções:
OS_service e
OS_serviceTask (exceto o serviço
OS_InitTask , que possui apenas uma opção; o serviço
OS_Init inicializa o próprio SO).
Qual é a diferença entre
OS_service e
OS_serviceTask ? O primeiro método atua na própria tarefa que a causou; o segundo permite definir como argumento um ponteiro para outra tarefa e, assim, gerenciar outro de uma tarefa.
Sobre OS_ResumeTodos os serviços de gerenciamento de tarefas, exceto OS_Resume e OS_ResumeTask, chamam automaticamente o gerenciador de tarefas após o processamento. Por outro lado, os serviços OS_Resume * definem apenas o status da tarefa como OSTS_READY. Esse status será processado apenas quando o expedidor for chamado explicitamente.
Prioridade e fila de tarefas
Como mencionado acima, em um sistema real, algumas tarefas podem ser mais importantes, enquanto outras podem ser secundárias. Portanto, um recurso útil do sistema operacional é a capacidade de atribuir prioridade às tarefas. Nesse caso, se houver várias tarefas
prontas ao mesmo tempo, o sistema operacional selecionará primeiro a tarefa com a maior prioridade. Se
todas as tarefas prontas tiverem prioridade igual, o sistema operacional as executará em círculo, em uma ordem chamada "carrossel" ou round-robin.
No AQUA RTOS, a prioridade é atribuída a uma tarefa quando é
inicializada por meio de uma chamada ao serviço
OS_InitTask , que
recebe o endereço da tarefa como o primeiro argumento e um número de 1 a 15 como o segundo argumento.Um
número mais baixo significa uma prioridade mais alta . Durante a operação do sistema operacional, não é fornecida uma alteração na prioridade atribuída à tarefa.
Atrasos
Em cada tarefa, o atraso é processado independentemente de outras tarefas.
Assim, enquanto o sistema operacional calcula o atraso de uma tarefa, outras podem ser executadas.
Para a organização de atrasos prestados serviços
OS_Delay | OS_DelayTask . O argumento é o número de milissegundos para os quais a tarefa está
atrasada . Como a dimensão do argumento é
dword , o atraso máximo é de 4294967295 ms, ou cerca de 120 horas, o que parece ser suficiente para a maioria dos aplicativos. Depois de chamar o serviço de atraso, o expedidor é chamado automaticamente, o que transfere o controle para outras tarefas pela duração do atraso.
Semáforos
Os semáforos no AQUA RTOS são algo como sinalizadores e variáveis disponíveis para as tarefas. Eles são de dois tipos - binários e contáveis. O primeiro tem apenas dois estados: livre e fechado. O segundo é um contador de bytes (o serviço de contagem de semáforos na versão atual do AQUA RTOS não está implementado (eu sou preguiçoso), portanto, tudo o que foi dito abaixo se aplica apenas aos semáforos binários.
A diferença entre um semáforo e um sinalizador simples é que a tarefa pode ser feita para
aguardar a liberação do semáforo especificado. De certa forma, o uso de semáforos realmente se assemelha a uma ferrovia: ao chegar ao semáforo, a composição (tarefa) verificará o semáforo e, se não estiver aberto, aguardará o sinal de ativação parecer mais longe. Neste momento, outros trens (tarefas) podem continuar a se mover (correr).
Nesse caso, todo o trabalho em preto é atribuído ao expedidor. Assim que a tarefa é solicitada a aguardar o semáforo, o controle é automaticamente transferido para o expedidor e ele pode iniciar outras tarefas - exatamente até que o semáforo especificado seja liberado. Assim que o estado do semáforo mudar para
livre , o expedidor atribui a todas as tarefas que estavam esperando por esse semáforo o status
pronto (
OSTS_READY ) e elas serão executadas na ordem de prioridade e prioridade.
No total, o AQUA RTOS fornece 16 semáforos binários (esse número pode, em princípio, ser aumentado alterando a dimensão da variável na unidade de controle de tarefas, porque por dentro são implementados como sinalizadores de bits).
Os semáforos binários funcionam através dos seguintes serviços:
hBSem OS_CreateBSemaphore() OS_WaitBSemaphore(hBSem) OS_WaitBSemaphoreTask(task_label, hBSem) OS_BusyBSemaphore(hBSem) OS_FreeBSemaphore(hBSem)
Antes de usar um semáforo, é necessário
criar . Isso é feito chamando o serviço
OS_CreateBSemaphore , que retorna o identificador de bytes exclusivo (identificador) do semáforo
hBSem criado ou, por meio do manipulador definido pelo usuário, gera um erro
OSERR_BSEM_MAX_REACHED , indicando que o número máximo possível de semáforos binários foi atingido.
Você pode trabalhar com o identificador recebido, passando-o como um argumento para outros serviços de semáforo.
Serviço
OS_WaitBSemaphore | OS_WaitBSemaphoreTask coloca a tarefa (atual | especificada) em um estado para
aguardar a liberação do semáforo hBSem se esse semáforo estiver ocupado e depois transfere o controle para o expedidor para que ele possa iniciar outras tarefas. Se o semáforo estiver livre, a transferência de controle não ocorrerá e a tarefa simplesmente continuará.
Os
serviços OS_BusyBSemaphore e
OS_FreeBSemaphore configuram o semáforo
hBSem como
ocupado ou
gratuito, respectivamente.
A destruição de semáforos para simplificar o sistema operacional e reduzir a quantidade de código não é fornecida. Assim, todos os semáforos criados são estáticos.
Eventos
Além dos semáforos, as tarefas podem ser orientadas a eventos. Uma tarefa pode ser instruída a
esperar um determinado evento , e outra tarefa (assim como o código de segundo plano) pode
sinalizar esse evento. Ao mesmo tempo, todas as tarefas que aguardavam esse evento receberão o status
pronto para execução (
OSTS_READY ) e serão definidas pelo expedidor para execução na ordem de prioridade e prioridade.
A quais eventos a tarefa pode responder? Bem, por exemplo:
- interrupção;
- ocorrência de um erro;
- liberação do recurso (às vezes é mais conveniente usar um semáforo para isso);
- alterar o status da linha de E / S ou pressionar uma tecla no teclado;
- receber ou enviar um personagem via RS-232;
- transferência de informações de uma parte do aplicativo para outra (consulte também as mensagens).
O sistema de eventos é implementado através dos seguintes serviços:
hEvent OS_CreateEvent() OS_WaitEvent(hEvent) OS_WaitEventTask(task_label, hEvent) OS_WaitEventTO(hEvent, dwTimeout) OS_SignalEvent(hEvent)
Antes de usar um evento, você precisa
criá-lo . Isso é feito chamando a função
OS_CreateEvent , que retorna um identificador de bytes exclusivo (identificador) para o evento
hEvent ou gera um erro
OSERR_EVENT_MAX_REACHED através do manipulador definido pelo usuário, indicando que o limite do número de eventos que podem ser gerados no SO foi atingido (máximo de 255 eventos diferentes).
Para fazer uma tarefa aguardar um evento
hEvent , chame
OS_WaitEvent em seu código, passando o identificador de evento como argumento. Após chamar este serviço, o controle será automaticamente transferido para o expedidor.
Ao contrário do serviço de semáforo, o serviço de eventos fornece uma opção para aguardar um evento com
tempo limite . Para fazer isso, use o serviço
OS_WaitEventTO . O segundo argumento aqui é possível especificar o número de milissegundos que a tarefa pode esperar do evento. Se o tempo especificado expirou, a tarefa receberá o status
pronto para execução como se o evento tivesse ocorrido e será definido pelo expedidor para continuar a execução na ordem de prioridade e prioridade. A tarefa pode descobrir que não foi um evento, mas um tempo limite, que a tarefa pôde verificar verificando o
sinalizador global
OS_TIMEOUT .
O código da tarefa ou plano de fundo pode sinalizar a ocorrência de um determinado evento chamando o serviço OS_SignalEvent , que recebe o identificador de evento como argumento. Nesse caso, todas as tarefas aguardando esse evento, o sistema operacional definirá o status pronto para execução , para que possam continuar sendo executados em ordem de prioridade e prioridade.Mensagens
O sistema de mensagens funciona em geral de maneira semelhante ao sistema de eventos, mas fornece tarefas com mais opções e flexibilidade: fornece não apenas a expectativa de uma mensagem em um tópico especificado, mas a maneira como a própria mensagem é transmitida de uma tarefa para outra - um número ou uma sequência.Isso é implementado através dos seguintes serviços: hTopic OS_CreateMessage() OS_WaitMessage(hTopic) OS_WaitMessageTask(task_label, hTopic) OS_WaitMessageTO(hTopic, dwTimeout) OS_SendMessage(hTopic, wMessage) word_ptr OS_GetMessage(hTopic) word_ptr OS_PeekMessage(hTopic) string OS_GetMessageString(hTopic) string OS_PeekMessageString(hTopic)
Para usar o serviço de mensagens, você deve primeiro criar um assunto para a mensagem . Isso é feito por meio do serviço OS_CreateMessage , que retorna o identificador de bytes do tópico hTopic ou por meio do manipulador definido pelo usuário, gera um erro OSERR_TOPIC_MAX_REACHED , indicando que o número máximo possível de tópicos de mensagens foi atingido e não pode mais ser criado.Para instruir a tarefa a aguardar uma mensagem sobre o tópico hTopic , chame OS_WaitMessage em seu código , passando o identificador do tópico como argumento. Após chamar este serviço, o controle será automaticamente transferido para o gerenciador de tarefas. Portanto, este serviço coloca a tarefa atual em um estadoaguarde uma mensagem hTopic .O serviço de espera com o tempo limite OS_WaitMessageTO funciona de maneira semelhante ao serviço OS_WaitEventTO do sistema de eventos .Para enviar mensagens, o serviço OS_SendMessage é fornecido . O primeiro argumento é o identificador do tópico para o qual a mensagem será transmitida e o segundo é o argumento da dimensão da palavra . Pode ser um número independente ou um ponteiro para uma string , que, por sua vez, já é uma mensagem.Para obter um ponteiro de linha, basta usar a função varptr embutida no BASCOM , por exemplo, assim: strMessage = "Hello, world!" OS_SendMessage hTopic, varptr (strMessage)
Continuando o trabalho após chamar OS_WaitMessage , ou seja, quando a mensagem esperada é recebida, a tarefa pode receber a mensagem com sua subsequente destruição automática ou apenas visualizá-la - nesse caso, ela não será destruída. Para fazer isso, use os quatro últimos serviços da lista. Os dois primeiros retornam um número de palavra de dimensão , que pode ser uma mensagem independente ou servir como ponteiro para a sequência que contém a mensagem. Nesse caso, OS_GetMessage exclui automaticamente a mensagem e OS_PeekMessage a deixa.Se a tarefa precisar imediatamente de uma sequência, não de um ponteiro, você poderá usar os serviços OS_GetMessageString ou OS_PeekMessageString , que funcionam de maneira semelhante às duas anteriores, mas retornam uma sequência, não um ponteiro para ela.Serviço de timer interno
Para trabalhar com os atrasos e tempo AQUA RTOS usa um built-in IC temporizador hardware TIMER0 . Portanto, código externo (segundo plano e tarefas) não deve usar esse timer. Mas geralmente isso não é necessário, porque O SO fornece às tarefas todas as ferramentas necessárias para trabalhar com intervalos de tempo. A resolução do temporizador é de 1 ms.Exemplos de trabalho com o AQUA RTOS
Configurações iniciais
No início do código do usuário, você precisa determinar se o código será executado no simulador interno ou no hardware real. Defina a constante OS_SIM = TRUE | FALSE , que define o modo de simulação.Além disso, no código do SO, edite a constante OS_MAX_TASK , que determina o número máximo de tarefas suportadas pelo SO. Quanto menor esse número, mais rápido o sistema operacional funciona (menos sobrecarga) e menos memória consome. Portanto, você não deve indicar mais tarefas do que o necessário. Não se esqueça de alterar essa constante se o número de tarefas tiver sido alterado.Inicialização do SO
Antes de iniciar, o AQUA RTOS deve ser inicializado. Para fazer isso, ligue para o serviço OS_Init . Este serviço define as configurações iniciais do sistema operacional. Mais importante, ele tem um argumento - o endereço da rotina de tratamento de erros definida pelo usuário. Ela, por sua vez, também tem uma discussão - um código de erro.Esse manipulador deve estar no código do usuário (pelo menos na forma de um stub) - o sistema operacional envia códigos de erro a ele e o usuário não tem outra maneira de capturá-los e processá-los adequadamente. Eu recomendo fortemente que, pelo menos no estágio de desenvolvimento, não coloque um stub, mas inclua qualquer saída de informações de erro neste procedimento.Portanto, a primeira etapa ao trabalhar com o AQUA RTOS é adicionar o código de inicialização do SO e o procedimento do manipulador de erros ao programa do usuário: OS_Init my_err_trap
Inicialização de tarefas
A segunda etapa é inicializar as tarefas especificando seus nomes e prioridades: OS_InitTask task_1, 1 OS_InitTask task_2 , 1
Tarefas de teste
LED piscando
Portanto, vamos criar um aplicativo de teste que pode ser baixado em uma placa Arduino Nano V3 padrão. Crie uma pasta na pasta com o arquivo do SO (por exemplo, teste) e crie o seguinte arquivo-base:
Conecte os ânodos dos LEDs aos pinos D4 e D5 da placa Arduino (ou a outros pinos alterando as linhas de definição correspondentes no código). Conecte os cátodos através de resistores de terminação de 100 ... 500 Ohm ao barramento GND . Compile e faça o upload do firmware para o quadro. Os LEDs começarão a alternar de forma assíncrona com um período de 2 e 0,66 s.Vamos dar uma olhada no código. Então, primeiro inicializamos o equipamento (definimos as opções do compilador, o modo de porta e atribuímos aliases), depois o próprio sistema operacional e, finalmente, as tarefas.Como as tarefas recém-criadas estão no estado "parado", é necessário atribuir a elas o status "pronto para execução" (talvez não para todas as tarefas em um aplicativo real - afinal, algumas delas, conforme concebidas pelo desenvolvedor, podem estar inicialmente em um estado parado e executar no execução apenas de outras tarefas, e não imediatamente no início do sistema; no entanto, neste exemplo, ambas as tarefas devem começar a funcionar imediatamente). Portanto, para cada tarefa, chamamos o serviço OS_ResumeTask .Agora as tarefas estão prontas para execução, mas ainda não foram concluídas. Quem os lançará? Claro, o despachante! Para fazer isso, devemos chamá-lo no primeiro início do sistema. Agora, se tudo estiver escrito corretamente, o despachante executará nossas tarefas uma a uma, e podemos terminar a parte principal do programa com a instrução final.Vamos dar uma olhada nas tarefas. É imediatamente evidente que cada um deles é enquadrado como um loop infinito . A segunda propriedade importante - dentro desse ciclo, deve haver pelo menos uma chamada para o despachante ou o serviço do SO que chama automaticamente o despachante para si mesmo - caso contrário, essa tarefa nunca abrirá mão do controle e outras tarefas não poderão ser executadas. No nosso caso, este é o serviço de atraso OS_Delay . Como argumento, indicamos a ele o número de milissegundos para os quais cada tarefa deve ser pausada.Se você definir a constante OS_SIM = TRUE no início do código e executar o código não no chip real, mas no simulador, poderá rastrear como o sistema operacional funciona.O despachante que ligamos verá se as tarefas com o status estão "prontas para execução" e as alinhará de acordo com a prioridade. Se a prioridade for a mesma, o expedidor "rolará tarefas no carrossel", movendo a tarefa recém-concluída para o final da fila.Depois de selecionar a tarefa a ser executada (por exemplo, tarefa_1 ), o expedidor substitui o endereço de retorno (inicialmente, ele aponta para a instrução final no código principal) na pilha pelo endereço do ponto de entrada da tarefa task_1 , que o sistema reconhece durante a inicialização da tarefa, e executa o comando return , que força MK para puxar o endereço de retorno da pilha e acessá -lo - ou seja, inicie a execução da tarefa_1 tarefa (operadordo task_1 ).
task_1 , ,
OS_delay , , , .
, ,
task_1 ( ,
OS_delay , ..
loop ), , « », ,
task_2 .
task_2 (
do task_2 )
return , que faz com que o MK retire o endereço de retorno da pilha e vá até ele - ou seja, comece a executar task_2 .A tarefa task_2 , alternando seu LED, chama o serviço OS_delay , que, após executar as ações necessárias, vai para o expedidor.O expedidor salva o endereço que estava na pilha na unidade de controle da tarefa task_1 (aponta para a instrução após a chamada OS_delay , ou seja, a instrução de loop ) e, em seguida, "girando o carrossel", descobre que a tarefa_2 agora deve ser concluída . A diferença do estado inicial será que agora no bloco de controle de tarefas task_1não o endereço inicial da tarefa é armazenado, mas o endereço do ponto em que ocorreu a transição para o expedidor. Lá (para a instrução de loop no código da tarefa task_1 ) e o controle será transferido.A tarefa task_1 executará a instrução de loop e, em seguida, o ciclo inteiro "Tarefa 1 - Despachante - Tarefa 2 - Despachante" será repetido infinitamente.Enviar mensagens
Agora vamos tentar enviar mensagens de uma tarefa para outra.
O resultado da execução do programa no simulador será a seguinte saída na janela do terminal:task 1
task 2 is waiting messages…
task 1
task 1
task 1
task 1 is sending message to task 2
task 1
message recieved: Hello, task 2!
task 2 is waiting messages…
task 1
task 1
...
Preste atenção na ordem em que o trabalho e a alternância de tarefas ocorrem. Assim que a tarefa 1 imprime a tarefa 1 , o controle é transferido para o expedidor, para que ele possa iniciar a segunda tarefa. A tarefa 2 imprime que a tarefa 2 está aguardando mensagens ... , depois chama o serviço de mensagem em espera no tópico hTopic e o controle é automaticamente transferido para o expedidor, que novamente chama a tarefa 1. Imprime a tarefa 1 novamente e dá controle ao expedidor. No entanto, como o despachante detecta que a Tarefa 2 agora está aguardando mensagens, ele retorna o controle para a Tarefa 1 na instrução incr após a chamada do despachante.Quando o contador task_1_cntna tarefa 1, excederá o valor especificado, a tarefa envia uma mensagem, mas continua a executar - executa a instrução de loop e imprime a tarefa 1 novamente . Depois disso, ela liga para o despachante, que agora descobre que há uma mensagem para a Tarefa 2 e transfere o controle para ela. Em seguida, o processo é realizado ciclicamente.Manipulação de eventos
O código a seguir pesquisa dois botões e alterna os LEDs quando você pressiona o botão correspondente:
Um exemplo real de aplicação no AQUA RTOS
Vamos tentar imaginar como seria um programa de máquinas de venda automática de café. A máquina deve indicar a presença de opções de café e a escolha de LEDs nos botões; receber sinais do receptor de moedas, preparar a bebida solicitada, emitir alterações. Além disso, a máquina deve controlar o equipamento interno: por exemplo, mantenha a temperatura do aquecedor de água em 95 ... 97 ° C; transmitir dados sobre mau funcionamento do equipamento e estoque de ingredientes e receber comandos por acesso remoto (por exemplo, via modem GSM), além de vandalismo de sinal.Abordagem orientada a eventos
No início, pode ser difícil para um desenvolvedor mudar do esquema usual "supercycle + flags + interrupts" para uma abordagem baseada em tarefas e eventos. Isso requer destacar as tarefas básicas que o dispositivo deve executar.Vamos tentar delinear essas tarefas para nossa máquina:- controle e gerenciamento de aquecedores - ControlHeater ()
- indicação de disponibilidade e escolha de bebidas - ShowGoods ()
- aceitação de moedas / notas e seu somatório - AcceptMoney ()
- botões de pesquisa - ScanKeys ()
- entrega de alterações - MakeChange ()
- licença de bebida - ReleaseCoffee ()
- proteção contra vandalismo - Alarme ()
Vamos estimar o grau de importância das tarefas e a frequência de suas chamadas.ControlHeater () é obviamente importante porque sempre precisamos de água fervente para fazer café. Mas não deve ser realizado com muita frequência, porque o aquecedor tem uma alta inércia e a água esfria lentamente. Basta verificar a temperatura uma vez por minuto.Dê prioridade a esta tarefa 5. ShowGoods () não é muito importante. A oferta pode mudar somente após a liberação da mercadoria, se o fornecimento de qualquer ingrediente estiver esgotado. Portanto, atribuiremos a esta tarefa uma prioridade de 8 e a executaremos quando a máquina iniciar e toda vez que as mercadorias forem liberadas.Teclas de digitalização ()deve ter uma prioridade suficientemente alta para que a máquina responda rapidamente aos pressionamentos de botão. Dê a esta tarefa a prioridade 3 e nós a executaremos a cada 40 milissegundos.AcceptMoney () também faz parte da interface do usuário. Daremos a mesma prioridade que ScanKeys () e a executaremos a cada 20 milissegundos.MakeChange () é executado somente depois que as mercadorias são liberadas. Nós o associaremos ao ReleaseCoffee () e daremos a prioridade 10.ReleaseCoffee () é necessário apenas quando a quantia apropriada for aceita e o botão de seleção de bebida for pressionado. Para uma resposta rápida, damos a prioridade 2.Como a resistência a vandalismo é uma função bastante importante da máquina, a tarefa Alarm ()você pode definir a prioridade mais alta - 1 e ativar uma vez por segundo para verificar os sensores de inclinação ou violação.Assim, precisaremos de sete tarefas com prioridades diferentes. Após o início, quando o programa leu as configurações da EEPROM e inicializou o equipamento, é hora de inicializar o sistema operacional e iniciar as tarefas.
Para trabalhar como parte do RTOS, cada tarefa deve ter uma certa estrutura: deve ter pelo menos uma chamada para o expedidor (ou um serviço de SO que transfira automaticamente o controle para o expedidor) - essa é a única maneira de garantir multitarefa cooperativa.Por exemplo, ReleaseCoffee () pode ser algo como isto: sub ReleaseCoffee() do OS_WaitMessage bCoffeeSelection wItem = OS_GetMessage(bCoffeeSelection) Release wItem loop end sub
A tarefa ReleaseCoffee em um loop infinito espera uma mensagem sobre o tópico bCoffeeSelection e não faz nada até chegar (o controle é retornado automaticamente ao expedidor para que ele possa iniciar outras tarefas). Assim que a mensagem é enviada, ReleaseCoffee () fica pronto para execução e, quando isso acontece, a tarefa recebe o conteúdo da mensagem (código da bebida selecionada) wItem usando o serviço OS_GetMessage e libera as mercadorias para o cliente. Como ReleaseCoffee () usa o subsistema de mensagens, é necessário criar uma mensagem antes de iniciar a multitarefa: dim bCoffeeSelection as byte bCoffeeSelection = OS_CreateMessage()
Como mencionado acima, ShowGoods () deve ser executado uma vez na inicialização e sempre que as mercadorias forem liberadas. Para associá-lo ao procedimento de liberação ReleaseCoffee () , usamos o serviço de eventos. Para fazer isso, crie um evento antes de iniciar a multitarefa: dim bGoodsReliased as byte bGoodsReliased = OS_CreateEvent()
E no procedimento ReleaseCoffee () , após a linha Release wItem , adicionamos um alarme sobre o evento bGoodsReleased : OS_SignalEvent bGoodsReliased
Inicialização do SO
Para preparar o sistema operacional para o trabalho, precisamos inicializá-lo, indicando o endereço do manipulador de erros, que está no código do usuário. Fazemos isso usando o serviço OS_Init : OS_Init Mailfuncion
No código do usuário, você precisa adicionar um manipulador - um procedimento cujo argumento de byte é o código de erro: sub Mailfuncion (bCoffeeErr) print "Mailfunction! Error #: "; bCoffeeErr if isErrCritical (bCoffeeErr) = 1 then CallService(bCoffeeErr) end if end sub
Este procedimento imprime um código de erro (ou pode exibi-lo de outra maneira: na tela, via modem GSM, etc.) e, caso o erro seja crítico, ele liga para o departamento de serviço.Início da tarefa
Já lembramos que eventos, semáforos etc. deve ser inicializado antes de ser usado. Além disso, as próprias tarefas devem ser inicializadas com o serviço OS_InitTask antes de iniciar : OS_InitTask ControlHeater , 5 OS_InitTask ShowGoods , 8 OS_InitTask AcceptMoney , 3 OS_InitTask ScanKeys , 3 OS_InitTask MakeChange, 10 OS_InitTask ReleaseCoffee , 2 OS_InitTask Alarm , 1
Como o modo multitarefa ainda não começou, a ordem na qual as tarefas são iniciadas é insignificante e, de qualquer forma, não depende de suas prioridades. Neste ponto, todas as tarefas ainda estão em um estado parado. Para prepará-los para execução, devemos usar o serviço OS_ResumeTask para definir o status "pronto para execução": OS_ResumeTask ControlHeater OS_ResumeTask ShowGoods OS_ResumeTask AcceptMoney OS_ResumeTask ScanKeys OS_ResumeTask MakeChange OS_ResumeTask ReleaseCoffee OS_ResumeTask Alarm
Como já mencionado, nem todas as tarefas devem necessariamente iniciar quando a multitarefa é iniciada; alguns deles podem a qualquer momento permanecer em um estado "parado" e receber prontidão apenas sob certas condições. O serviço OS_ResumeTask pode ser chamado a qualquer momento e em qualquer lugar do código (plano de fundo ou tarefa) quando a multitarefa já estiver em execução. O principal é que a tarefa a que se refere é pré-inicializada.Início da multitarefa
Agora tudo está pronto para começar a multitarefa. Fazemos isso chamando o expedidor: OS_Sheduler
Depois disso, podemos finalizar com segurança o código do programa - o sistema operacional agora cuida da execução adicional do código.Vejamos o código inteiro:
Obviamente, uma abordagem seria mais correta, não com pesquisas periódicas de botões e um sensor de caixa, mas com o uso de interrupções. Nos manipuladores dessas interrupções, poderíamos usar o envio de mensagens usando o serviço OS_SendMessage () com conteúdo igual ao número da tecla pressionada ou ao valor da moeda / nota inserida. Convido o leitor a modificar o programa por conta própria. Graças à abordagem orientada a tarefas e ao serviço fornecido pelo sistema operacional, isso exigirá alterações mínimas de código.Código-fonte AQUA RTOS
O código fonte da versão 1.05 está disponível para download aquiPostscript
Q: Por que AQUAR: Bem, eu fiz o controle do aquário, é como uma "casa inteligente", não apenas para as pessoas, mas para os peixes. Cheio de todos os tipos de sensores, relógio em tempo real, saídas de relé e potência analógica, um menu na tela, um "programa de eventos" flexível e até um módulo WiFi. Os intervalos devem ser contados, os botões devem ser pesquisados, os sensores devem ser processados, o programa de eventos deve ser lido na EEPROM e executado, a tela deve ser atualizada, o Wi-Fi deve responder. Além disso, o controlador deve ir para um menu de vários níveis para configurações e programação. Fazer sinalizadores e interrupções é apenas obter o próprio "código da massa", que não é entendido nem modificado. Portanto, decidi que precisava de um sistema operacional. Aqui está ela AQUA.P: Certamente o código está cheio de erros e falhas lógicas?A: Certamente. Eu, como pude, criei uma variedade de testes e conduzi o sistema operacional em uma variedade de tarefas, e até bati um número notável de bugs, mas isso não significa que tudo esteja completo. Tenho mais certeza de que ainda existem muitos nas ruas secundárias do código. Portanto, ficarei muito grato se, em vez de me cutucar nos bichos na cara, você educadamente e com tato apontar para eles, e melhor me dizer como você acha que é melhor corrigi-los. Também será ótimo se o projeto for desenvolvido como um produto da criatividade coletiva. Por exemplo, alguém adicionará um serviço para contar semáforos (não se esqueceu? - sou um preguiçoso) e oferecerá outras melhorias. De qualquer forma, ficarei muito grato pela contribuição construtiva.