Um pouco sobre multitarefa
Todo mundo que dia após dia, ou de caso a caso, está envolvido na programação de microcontroladores, mais cedo ou mais tarde será confrontado com a pergunta: devo usar um sistema operacional multitarefa? Existem muitos deles na rede, e muitos deles são gratuitos (ou quase gratuitos). Apenas escolha.
Dúvidas semelhantes surgem quando você se depara com um projeto no qual o microcontrolador deve executar simultaneamente várias ações diferentes. Alguns deles não estão conectados com outros, enquanto o resto, pelo contrário, não pode ficar sem o outro. Além disso, pode haver muitos de ambos. O que é "demais" depende de quem avaliará ou quem executará o desenvolvimento. Bem, se é a mesma pessoa.
Pelo contrário, não se trata de quantidade, mas de uma diferença qualitativa nas tarefas em relação à velocidade de execução ou a alguns outros requisitos. Tais pensamentos podem surgir, por exemplo, quando o projeto precisa monitorar regularmente a tensão de alimentação (está faltando?), Frequentemente lê e salva os valores das quantidades de entrada (elas não dão descanso), ocasionalmente monitora a temperatura e controla o ventilador (não há nada para respirar), verifique seu assiste a alguém em quem você confia (é bom que você o controle), mantenha contato com o operador (tente não irritá-lo), verifique a soma de verificação da memória permanente do programa para demência (quando ligado, ou uma vez por semana ou pela manhã).
Essas tarefas heterogêneas podem ser programadas de maneira bastante significativa e bem-sucedida, contando com uma única tarefa em segundo plano e interrupções no cronômetro. No manipulador dessas interrupções, cada vez que uma das "partes" da próxima tarefa é executada. Dependendo da importância, urgência ou considerações semelhantes, esses desafios são frequentemente repetidos para algumas tarefas, mas raramente para outras. E, no entanto, devemos garantir que cada tarefa faça parte do trabalho por um curto período de tempo, depois se prepare para a próxima pequena parte do trabalho e assim por diante. Essa abordagem, se você se acostumar, não parece muito complicada. A inconveniência ocorre quando você deseja criar um projeto. Ou, por exemplo, repentinamente transfira para outro. Deve-se notar que o segundo geralmente é mais difícil e sem nenhuma pseudo-tarefa.
Mas e se você usar um sistema operacional pronto para microcontroladores? Claro, muitos fazem isso. Esta é uma boa opção. Mas o autor dessas linhas, até agora, foi e continua sendo interrompido pela ideia de que será necessário entender isso, tendo passado muito tempo, escolher entre o que conseguimos obter e usar apenas o que realmente é necessário. E faça tudo isso, lembre-se, investigando o código de outra pessoa! E não há certeza de que em seis meses isso não terá que ser repetido, pois será esquecido.
Em outras palavras, por que você precisa de uma garagem cheia de ferramentas e utensílios, se uma bicicleta é armazenada e usada lá?
Portanto, havia um desejo de realizar uma simples tarefa de "troca" apenas no Cortex-M4 (bem, talvez até no M3 e M7). Mas o velho desejo de não se esforçar muito não desapareceu.
Então, fazemos o mais simples. Um pequeno número de tarefas compartilha o tempo de execução igualmente. Como na Figura 1 abaixo, quatro tarefas fazem isso. Seja o principal um zero, pois é difícil imaginar outro.

Ao trabalhar dessa maneira, eles garantem o horário ou o intervalo de tempo (tick) e não precisam saber sobre a existência de outras tarefas. Cada tarefa, exatamente após três tiques, terá novamente a oportunidade de fazer algo.
Mas, por outro lado, se alguma das tarefas for necessária para aguardar um evento externo, por exemplo, pressionando um botão, ele gastará estupidamente o precioso tempo do nosso microcontrolador. Não podemos concordar com isso. E nosso sapo (consciência) - também. Algo deve ser feito.
E que a tarefa, se não tem nada a ver até agora, dedique o tempo restante ao carrapato aos seus camaradas, que, muito provavelmente, aram com todas as suas forças.
Em outras palavras, o compartilhamento é necessário. Deixe a tarefa 2 fazer exatamente isso, como na Figura 2.

E por que nossa tarefa em segundo plano não deveria desistir do resto do tempo, se você ainda precisa esperar? Vamos permitir. Como mostra a Figura 3.

E se você souber que algumas das tarefas não exigirão em breve que você verifique algo novamente ou apenas funcione? E ela podia se permitir dormir um pouco e, em vez disso, perderia tempo e ficaria sob seus pés. Não é um pedido, ele precisa ser corrigido. Deixe a tarefa 3 perder um pedaço de seu tempo (ou mil). Como mostra a figura 4.

Bem, como vemos, delineamos uma coexistência justa de tarefas ou algo assim. Agora devemos fazer com que nossas tarefas individuais se comportem conforme prescrito. E se tentarmos valorizar o tempo, vale a pena lembrar de um idioma de baixo nível (não tenho medo da palavra assembler) e não confiar totalmente no compilador de qualquer idioma, de nível alto ou muito alto. De fato, no fundo de nossos corações, somos resolutamente contra toda dependência. Além disso, o fato de não precisarmos de montador, mas apenas do Cortex-M4, simplifica nossas vidas.
Para a pilha, selecionamos uma área comum da RAM que será preenchida, ou seja, na direção da diminuição dos endereços de memória. Porque Só porque não funciona de maneira diferente. Dividiremos mentalmente essa área importante em seções iguais, de acordo com o número máximo declarado de nossas tarefas. A Figura 5 mostra isso para quatro tarefas.

Em seguida, selecionamos o local onde armazenaremos cópias dos ponteiros da pilha para cada tarefa. Agora, interrompendo o timer, que assumimos como timer do sistema, salvamos todos os registros da tarefa atual em sua área de pilha (o registro SP agora está apontando para lá), depois salvamos seu ponteiro de pilha em um local especial (salvamos seu valor), obtemos o ponteiro de pilha da próxima tarefa ( escreva um novo valor no registro SP) do nosso local especial e restaure todos os seus registros. Suas cópias agora são indicadas pelo registro de SP da nossa próxima tarefa. Bem, saímos da interrupção, é claro. Além disso, todo o contexto da próxima tarefa da lista aparece nos registros.
Provavelmente, será supérfluo dizer que o próximo após a tarefa3 será principal. E não é supérfluo, é claro, lembrar que o Cortex-M4 já possui um temporizador SysTick e uma interrupção especial, e muitos fabricantes de microcontroladores sabem disso. Vamos usá-lo e essa interrupção como pretendido.
Para iniciar o cronômetro do sistema, bem como fazer todas as preparações e verificações necessárias, você deve usar o procedimento destinado a isso.
U8 main_start_task_switcher(void);
Essa rotina retorna 0 se todas as verificações foram aprovadas ou um código de erro se algo der errado. É verificado, basicamente, se a pilha está alinhada corretamente e se há espaço suficiente para ela, e também todos os nossos locais especiais são preenchidos com valores iniciais. Em suma, tédio.
Se alguém quiser ver o texto do programa, no final da narração, ele poderá fazer isso facilmente, por exemplo, por correio pessoal.
Sim, esqueci completamente quando retiramos os registros da próxima tarefa do armazenamento pela primeira vez na vida dela, é necessário que eles obtenham valores originais significativos. E, como ela as buscará em sua seção da pilha, é necessário colocá-las lá com antecedência e mover o ponteiro da pilha para que seja conveniente levá-las. Para isso, precisamos de um procedimento
U8 task_run_and_return_task_number(U32 taskAddress);
Para essa sub-rotina, relatamos o endereço de 32 bits do início de nossa tarefa que queremos executar. E ela (a sub-rotina) nos diz o número da tarefa, que resultou em uma tabela geral especial, ou 0 se não havia espaço na tabela. Depois, podemos executar outra tarefa, depois outra e assim por diante, mesmo que todos os três sejam adicionais à nossa tarefa principal, que nunca termina. Ela nunca dará seu número zero a ninguém.
Algumas palavras sobre prioridades. A principal prioridade era e continua a não sobrecarregar o leitor com detalhes desnecessários.
Mas, falando sério, devemos lembrar que existem interrupções de portas seriais, de várias conexões SPI, de um conversor analógico para digital, de outro timer, afinal. E o que acontecerá se mudarmos para outra tarefa (alternar contexto) quando estivermos no manipulador de algum tipo de interrupção. Afinal, isso não será uma tarefa legítima, mas uma turvação temporária do programa. E manteremos esse contexto estranho como algum tipo de tarefa. Haverá uma confusão: a gola não prende, a tampa não se encaixa. Pare, não, isso é de uma história diferente.
No nosso caso, isso simplesmente não pode ser permitido. Não devemos permitir que alternemos o contexto durante o processamento de uma interrupção não planejada. Aqui estão as prioridades para isso. Nós apenas temos que esperar um pouco, e só então, quando essa audácia sem precedentes terminar, mude calmamente para outra tarefa. Em resumo, a prioridade da interrupção de nossa troca de tarefas deve ser mais fraca do que a prioridade de qualquer outra interrupção usada. A propósito, isso também é feito em nosso procedimento de inicialização e é aí que ele é instalado, a mais não prioritária de todas possível.
Eu não queria conversar, mas precisava. Nosso processador possui dois modos de operação: privilegiado e não privilegiado. E também dois registros para o ponteiro da pilha:
processo principal de SP e SP. Portanto, não trocaremos por ninharias, usaremos apenas o modo privilegiado e apenas o ponteiro principal da pilha. Além disso, tudo isso já foi fornecido no início do controlador. Então, não vamos complicar nossas vidas.
Resta lembrar que todas as tarefas, com certeza, gostariam de jogar tudo para o inferno e como relaxar. E isso pode acontecer a qualquer momento durante o dia útil, ou seja, durante o nosso tick. O Cortex-M4 fornece para esses casos um comando montador especial SVC, que iremos adaptar à nossa situação. Isso leva a uma interrupção que nos levará ao objetivo. E permitiremos que a tarefa não apenas saia do local de trabalho após o almoço, mas não venha amanhã. Deixe isso acontecer depois das férias. E, se necessário, deixe-o aparecer quando concluir o reparo ou não vier. Para fazer isso, existe um procedimento que a própria tarefa pode causar.
void release_me_and_set_sleep_period(U32 ticks);
Essa rotina precisa apenas indicar quantos ticks estão planejados para descanso. Se 0, você pode descansar apenas o restante do tick atual. Se 0xFFFFFFFF, a tarefa irá "dormir" até que alguém acorde. Todos os outros números indicam o número de carrapatos durante os quais a tarefa estará em estado de suspensão.
Para que alguém pudesse acordar a tarefa de lado ou fazê-lo dormir, eu tive que adicionar esses procedimentos.
void task_wake_up_action(U8 taskNumber); void set_task_sleep_period(U8 taskNumber, U32 ticks);
E, apenas no caso, até mesmo uma sub-rotina.
void task_remove_action(U8 taskNumber);
Grosso modo, ela risca uma tarefa da lista de funcionários. Honestamente, ainda não sei por que escrevi. De repente, vem a calhar?
Está na hora de mostrar como é o local em que uma tarefa é substituída por outra, ou seja, o próprio switch.
Por precaução, lembremos que alguns dos registros, ao entrar na interrupção, são salvos na pilha sem a nossa participação, automaticamente (como é habitual no Cortex-M4). Portanto, precisamos apenas salvar o resto. Isso pode ser visto abaixo. Não se assuste com o que vê, estas são as instruções do montador do Cortex-M4 (M3, M7), conforme descrito no IAR Embedded Workbench.
Quem ainda não encontrou as instruções de montagem, acredite em mim, elas realmente se parecem com isso. Estas são as moléculas que compõem qualquer programa do ARM Cortex-M4.
SysTick_Handler STMDB SP!,{R4-R11} ; LDR R0,=timersTable ; LDR R1,=stacksTable ; LDR R2,[R0] ;R2 () STR SP,[R1,R2,LSL #2] ; SP (R2 * 4) __st_next_check ADD R2,R2,#1 ; CMP R2,#TASKS_LIMIT ;R2-TASKS_LIMIT BLO __st_no_border_yet ; MOV R2,#0 ; (main) LDR R3,[R1] ; main SP MOV SP,R3 B __st_timer_ok __st_no_border_yet ;; LDR SP,[R1,R2,LSL #2] ; (errata Cortex M4) ;; CMP SP,#0 ; LDR R3,[R1,R2,LSL #2] ; SP CMP R3,#0 ; =0 BEQ __st_next_check MOV SP,R3 LDR R3,[R0,R2,LSL #2] ; suspend timer CBZ R3,__st_timer_ok ; 0 , ; CMP R3,#0xFFFFFFFF ; , BEQ __st_next_check SUB R3,R3,#1 ; 1 STR R3,[R0,R2,LSL #2] ; suspend timer B __st_next_check __st_timer_ok STR R2,[R0] ; LDMIA SP!,{R4-R11} ; R4-R11 BX LR
O tratamento da interrupção ordenada pela própria tarefa quando ela retorna o restante do tique é semelhante. A única diferença é que você ainda precisa se preocupar em dormir um pouco mais tarde (ou adormecer completamente). Há uma sutileza. Duas ações devem ser executadas, escreva o número desejado no temporizador e faça com que o SVC seja interrompido. O fato de essas duas ações não ocorrerem atomicamente (isto é, não as duas ao mesmo tempo) me preocupa um pouco. Imagine por um milésimo de segundo que acabamos de acertar o cronômetro e, naquele momento, era hora de trabalhar em outra tarefa. A outra começou a gastar seu carrapato, enquanto nossa tarefa será dormir os próximos, como esperado (porque o cronômetro não é zero). Então, quando chegar a hora, nossa tarefa receberá seu tique e imediatamente interromperá o SVC, devido às duas ações que ainda precisam ser executadas. Nada terrível, na minha opinião, acontecerá, mas o sedimento permanecerá. Portanto, faremos isso. O futuro temporizador é colocado em um lugar preliminar. É retirado de lá pela própria rotina de interrupção do SVC. A atomicidade, por assim dizer, é alcançada. Isso é mostrado abaixo.
SVC_Handler LDR R0,__sysTickAddr ; SysTick MOV R1,#6 ; CSR , STR R1,[R0] ;Stop SysTimer MOV R1,#7 ; , STR R1,[R0] ;Start SysTimer ; STMDB SP!,{R4-R11} ; LDR R0,=timersTable ; LDR R1,=stacksTable ; LDR R2,[R0] ;R2 () STR SP,[R1,R2,LSL #2] ; SP (R2 * 4) LDR R3,=tmpTimersTable ; tmpTimers LDR R3,[R3,R2,LSL #2] ;tmpTimer STR R3,[R0,R2,LSL #2] ; timer __svc_next_check ADD R2,R2,#1 ; CMP R2,#TASKS_LIMIT ;R2-TASKS_LIMIT BLO __svc_no_border_yet ; MOV R2,#0 ; (main) LDR R3,[R1] ; main SP MOV SP,R3 B __svc_timer_ok __svc_no_border_yet ;; LDR SP,[R1,R2,LSL #2] ;Restore SP does not work (errata Cortex M4) ;; CMP SP,#0 ; LDR R3,[R1,R2,LSL #2] ; SP CMP R3,#0 ; =0 BEQ __svc_next_check MOV SP,R3 LDR R3,[R0,R2,LSL #2] ; suspend timer CBZ R3,__svc_timer_ok ; 0 , B __svc_next_check __svc_timer_ok STR R2,[R0] ; LDMIA SP!,{R4-R11} ; R4-R11 BX LR
Deve-se lembrar que todas essas sub-rotinas e manipuladores de interrupção se referem a uma determinada área de dados, que parece executada pelo autor, como mostra a Figura 7.
DATA SECTION .taskSwitcher:CODE:ROOT(2) __topStack DCD sfe(CSTACK) __botStack DCD sfb(CSTACK) __dimStack DCD sizeof(CSTACK) __sysAIRCRaddr DCD 0xE000ED0C __sysTickAddr DCD 0xE000E010 __sysSHPRaddr DCD 0xE000ED18 __sysTickReload DCD RELOAD ;******************************************************************************* ; Task table for concurrent tasks (main is number 0). ;******************************************************************************* SECTION TABLE:DATA:ROOT(2) DS32 1 ;stack shift due to FPU mainCopyCONTROL DS32 1 ;Needed to determine if FPU is used mainPSRvalue DS32 1 ;Copy from main ;*******************************************************************************
Para garantir que tudo isso seja de bom senso, o autor teve que escrever um pequeno projeto no IAR Embedded Workbench, onde conseguiu examinar e tocar em tudo em detalhes. Tudo foi testado no controlador STM32F303VCT6 (ARM Cortex-M4). Ou melhor, usando a placa STM32F3DISCOVERY. Existem LEDs suficientes para que cada tarefa pisque bastante com seu próprio LED separadamente.
Existem mais alguns recursos que eu achei úteis. Por exemplo, uma sub-rotina que conta em cada área da pilha o número de palavras não afetadas, ou seja, permanece igual a zero. Isso pode ser útil durante a depuração, quando você precisa verificar se o preenchimento da pilha com uma tarefa ou outra está muito próximo do nível limite.
U32 get_task_stack_empty_space(U8 taskNum);
Eu gostaria de mencionar mais uma função. Esta é uma oportunidade para a tarefa em si descobrir seu número na lista. Você pode contar a alguém mais tarde.
;******************************************************************************* ; Example: U8 get_my_number(void); ; (). .. . ;******************************************************************************* get_my_number LDR R0,=timersTable ; (currentTaskNumber) LDR R0,[R0] ; BX LR ;==============================================================
Provavelmente é tudo por enquanto.