Usando os controladores UDB PSoC da Cypress para reduzir as interrupções em uma impressora 3D



Nos comentários sobre a tradução de documentação proprietária na UDB, observou-se corretamente que fatos simples e secos não contribuem para a compreensão do material. Mas esse documento contém precisamente os fatos secos. Para diluí-los com a prática, vamos fazer uma pausa na tradução. Vamos virar esse bloco em nossas mãos e ver o que e como ele pode ser alcançado na prática.

Introdução longa


Este artigo é a segunda parte da trilogia concebida. A primeira parte está localizada aqui (controle LED RGB através da unidade de microcontrolador Cypress UDB PSoC).

Além dos controladores UDB PSoC da Cypress, onde determinadas interfaces são implementadas neles, seria interessante verificar como esses blocos podem facilitar a vida dos programadores, descarregando o processador central de determinadas tarefas que exigem muitos recursos. Mas, para esclarecer o que vou fazer, preciso escrever um extenso prefácio.

No outono de 2015, comprei uma impressora 3D MZ3D totalmente nova e, na primavera de 2016, estava cansada de como seus motores pisavam. Os tempos eram selvagens, sobrevivemos da melhor maneira possível; portanto, a única solução era mudar do microstep 1/16 para 1/32. A correspondência com a fábrica mostrou que isso não é possível no Arduino. Como se viu, houve uma restrição no "firmware" daqueles anos, com uma frequência de etapa superior a 10 KHz, não foram realizadas etapas virtuais, mas duas etapas virtuais, caso contrário o sistema simplesmente não teve tempo suficiente para processar todas as interrupções de "etapa". Só havia uma saída: arrastar tudo para a plataforma ARM. Foi um arrastar e soltar, não um download, pois também não havia soluções ARM prontas para a época. Em algumas semanas, transferi tudo isso para o STM32F4, o som dos motores ficou mais agradável e o problema foi resolvido.

Então, o desenvolvimento do sistema operacional começou em nossa empresa e, nas reuniões, tive que provar por um longo tempo que a abordagem típica para processar interrupções nem sempre é aceitável em termos de velocidade, apelando apenas para esse caso típico, mas muito glutão. Discussões sobre esse assunto estão publicadas no meu artigo sobre interrupções no sistema operacional aqui (Visão geral de um RTOS russo, parte 8. Trabalho com interrupções). Em geral, um problema se instala na minha cabeça há muito tempo: interrupções auxiliares freqüentes que atendem a um subsistema tornam mais lento o resto. O simples refinamento do processador central, é claro, elimina o problema, mas não traz a profunda satisfação moral de que tudo é feito corretamente.

Periodicamente, voltei a essa questão em um sentido puramente teórico. Por exemplo, um dia me ocorreu que, em vez de usar um controlador caro, você pode usar três STM32F103C8T6, nos quais uma placa de ensaio pronta custa 110 rublos, levando em consideração a entrega, e o próprio chip é ainda mais barato. Em um deles, para executar apenas a função de controle do motor. Deixe que ele gaste todo seu poder computacional nessa função. Alguns outros (talvez até um) resolvem outras tarefas (processamento de comandos, trabalho com PWM, manutenção da temperatura etc.) em um ambiente calmo. Essa solução também possui um lado positivo enorme - o número total de pinos para vários controladores é simplesmente enorme. Em um STM32, eu tive que colocar paciência por um longo tempo, qual perna atribuir. Embora as pernas das saídas do timer e as pernas ADC dos ARMs sejam atribuídas com mais flexibilidade do que os controladores antigos (uma saída da unidade de hardware pode ir para uma das várias pernas físicas), mas ao dobrar o próprio solitário, você entende que a flexibilidade pode não ser suficiente. Se houver muitos controladores, a escolha aumentará. No que serve motores de passo, em geral, simplesmente atribuímos todas as pernas como saídas digitais. Os outros também têm para onde se virar.

Um problema com essa abordagem é como sincronizar esses controladores? Em teoria, o MAX Max RTOS contém tudo o que você precisa. O manipulador de comandos gera uma lista de tarefas para mover cabeças. Periodicamente, ele as modifica (coordenando acelerações com tarefas recém-chegadas). Portanto, a memória do modelador e executante deve ser compartilhada. O RTOS MAX contém a funcionalidade para organizar essa memória compartilhada. Eu o descrevi aqui (visão geral de um RTOS russo, parte 7. Meios de troca de dados entre tarefas). Mas, na prática, uma nuance estraga tudo: a manutenção de motores de passo é um tipo de tarefa de tempo crítico. O menor atraso, e obtemos fluxos de plástico para uma impressora 3D, para outras máquinas CNC - bem, por exemplo, roscas incorretas. Qualquer comunicação via interfaces seriais não é a mais rápida. Mais tempo para arbitragem e outras necessidades oficiais. E acontece que todos os ganhos com a remoção da funcionalidade do processador principal vão para cima. Obviamente, aproveitei minha posição oficial: discuti esse assunto com os desenvolvedores deste subsistema. Infelizmente. Eles disseram que há sincronização sem muita sobrecarga no sistema operacional, mas para equipamentos que suportem os barramentos correspondentes. Agora, se eu tomar a arquitetura TigerShark como base, o sistema operacional organizará tudo para mim sem nenhuma sobrecarga. Somente os controladores fabricados de acordo com essa arquitetura são várias vezes mais caros do que toda a impressora 3D que eu queria colocar nela. Em geral, novamente inaceitável.

Abordamos o final de uma introdução prolongada. Alguém dirá que, por algum motivo, ainda estou procurando um príncipe em um cavalo branco. Você pode fazer tudo sem um sistema operacional, e aqui estou considerando todo tipo de opções ... Você pode, mas pode, mas quando surgiu o problema prático “Cansado de ouvir a falha da impressora”, ele foi rapidamente corrigido. Só isso. Ela não existe mais. Além disso, desde então, surgiram novos drivers de motor de passo que geralmente resolvem o problema de uma maneira completamente diferente (eles recebem um microstep 1/16 e fornecem 1/256). E nesta introdução, descrevo precisamente que "não há uma solução bonita para o problema de interrupções frequentes". Uma decisão feia já foi tomada. Não queria perder tempo checando outras decisões feias. Eles apenas rolaram na minha cabeça.

Mas, quando lidei com os blocos UDB, pareceu-me que o problema pode ser resolvido de maneira bela e dramática. Você pode simplesmente levar o processamento de interrupções do software para o nível do firmware, deixando a parte da computação na consciência do processador principal. Não são necessários controladores adicionais! Tudo é colocado no mesmo chip! Então, vamos começar.

Cavalo esférico no vácuo


Neste artigo, trabalhar com o próprio UDB estará na vanguarda. Se eu falasse sobre estar vinculado a um "firmware" específico, eles poderiam apontar corretamente para mim que eu estava enganado com o hub. O que é isso para o GeekTimes. Portanto, o UDB é primário e os motores de passo são apenas uma coisa bonita para ilustrar. Nesta parte, geralmente farei um cavalo esférico no vácuo. Ele terá deficiências práticas, que eliminarei na segunda parte. Mas, repetindo minhas ações, os leitores serão capazes de dominar a metodologia de desenvolvimento de firmware para UDB.

Então Como funciona o mecanismo de controle do motor de passo? Há uma tarefa que alinha os segmentos que a cabeça deve passar com velocidade linear. Até agora, vou fingir que não me lembro da aceleração no início e no final do segmento. Apenas a cabeça deve passar. Novos segmentos são colocados na cauda da fila. Com base na gravação da cabeça, uma tarefa separada envia sinais STEP para todos os mecanismos ativos.

Deixe a impressora ter uma velocidade máxima da cabeça de 200 mm / s. São necessários 200 passos por 1 milímetro de movimento (esta figura corresponde a uma impressora real MZ3D-256C com um microstep 1/32). Em seguida, os pulsos devem ser fornecidos com uma frequência de até 200 * 200 = 40.000 Hz = 40 KHz. É com tal frequência que uma tarefa que envia pulsos de passo pode muito bem ser chamada. Ele deve formar programaticamente os próprios pulsos e também calcular quanto tempo após o qual a próxima interrupção ativando deve ser chamada.

Lembro-me de uma piada sobre Kolobok e os Três Bogatyrs, onde Kolobok cumprimentou consistentemente os Bogatyrs, depois fez perguntas e recebeu respostas. Então sucessivamente disse adeus a eles. Bem, então ele se encontrou com os Trinta e Três Cavaleiros. O processador está no papel de um coque, e os motores de passo estão no papel de Bogatyrs. É claro que, na presença de um grande número de blocos UDB, é possível paralelizar o trabalho com os motores, tendo cada mecanismo atendido a seu bloco. E como temos segmentos durante os quais os motores se moverão uniformemente, vamos tentar fazer o equipamento funcionar com essas transações, e não a cada passo.

Que informações são necessárias para um cavalo esférico atravessar uma seção linear no vácuo?

  • Número de etapas.
  • O período entre as etapas.

Dois parâmetros O UDB possui apenas duas baterias e dois registros dos parâmetros D0 e D1. Parece que tudo é realizável. Apenas estimamos a profundidade de bits que esses registros devem ter.

Primeiro, o número de etapas. Se houver 8 dígitos, em uma operação UDB, a impressora poderá mover a cabeça da impressora cartesiana um pouco mais de 1 mm (200 micro etapas). Não chega. Se a capacidade for 16 bits, o número de etapas será 65536. Isso é 65536/200 = 327 milímetros. Aceitável para a maioria dos modelos. Para Core, Delta e outros, é necessário estimar, mas como um todo - para um curso completo, o segmento pode ser dividido em várias partes. Não haverá tantos (dois, no máximo, três).

Agora o período. Deixe a frequência do relógio ser 48 MHz. 48000000/65536 = 732. Ou seja, a frequência mínima permitida que pode ser obtida usando um divisor de 16 bits é 732 Hz. Demais. No firmware do Marlin, o mínimo é 120 Hz (o que corresponde aproximadamente a 8 MHz dividido pela mesma constante 65536). Teremos que fazer os registros 24 bits. Então a frequência mínima será igual a 48000000 / (2 ^ 24) = 48000000/16777216 = 2,861 Hz.

Bom Pare a teoria chata! Vamos seguir em frente! Inicie o PSoC Creator e selecione Arquivo-> Novo-> Projeto:



Em seguida, selecionei a placa de ensaio que tenho, a partir da qual o ambiente obterá informações básicas sobre o controlador usado e suas configurações:



Eu já me sinto pronto para criar um projeto do zero, então seleciono Esquema Vazio :



Dê ao ambiente de trabalho o nome PSoC3DTest :



E aqui está ele, um projeto acabado!



A primeira coisa que quero fazer é criar meu próprio componente com base no UDB. Portanto, como já observado no último artigo, preciso mudar para a guia Componentes :



Clique com o botão direito do mouse no projeto e selecione Adicionar item do componente :



Dizemos que precisamos adicionar um documento UDB , altere o nome para StepperController e clique em Criar novo :



O componente apareceu na árvore, mais - o editor deste componente foi aberto:



Coloque o bloco Datapath no formulário:



Depois de selecionar este bloco, vamos às suas propriedades e alteramos a profundidade do bit de 8 para 24. Os demais parâmetros podem permanecer inalterados.



Para iniciar todos os blocos (para todos os motores) ao mesmo tempo, iniciarei o sinal de partida do lado de fora (adicione a entrada Iniciar ). Saídas: Vou fazer a saída do Step diretamente, para que eu possa enviá-lo ao driver do motor de passo, bem como ao Out_Idle . Com base nesse sinal, o processador poderá determinar que, no momento em que a unidade terminou seu trabalho. Os nomes dos circuitos correspondentes a essas entradas e saídas são visíveis na figura.



Antes de falar sobre a lógica do autômato, descreverei outro problema puramente de engenharia: definir a duração do pulso Etapa . A documentação do driver DRV8825 exige que a largura do pulso seja de pelo menos 1,9 μs. Outros drivers são menos exigentes em sua largura. Como já observado na parte teórica, os registros existentes já estão ocupados, definindo a duração e o número de etapas. Goste ou não, um contador de sete bits deve ser colocado no circuito. Nós chamamos isso de one-shot, que define o pulso do passo. A uma frequência de 48 MHz, para garantir uma duração de 1,9 μs, este contador deve contar pelo menos 91,2 etapas. Arredonde para 92. Qualquer valor que exceda esse valor não será menor. Acontece a seguinte configuração:



Nome do contador SingleVibrator . Ele nunca é redefinido; portanto, a entrada Redefinir está sempre conectada a zero; considera que quando a máquina (descrita abaixo) está no estado Um, carrega em todos os outros estados (a princípio eu selecionei estados específicos da máquina, mas verificamos que com um método tão complicado , são necessários muito menos recursos PLD, mas o resultado é o mesmo). O valor da carga é decimal 92. É verdade que um bom editor substituirá imediatamente esse valor por hexadecimal:



Quando o contador é contado como zero, ele informa isso à cadeia com o nome One_Finished . Com o balcão - é isso.

Que tipo de sinalizadores de status nossa máquina usará? Entendi assim (lembre-se de clicar duas vezes na lista de saídas no Datapath para defini-las):





Usarei a bateria A0 como um contador para a duração do pulso, portanto, quando seu valor chegar a zero, a bandeira à qual eu dei o nome Pulse_Finished será armada. A bateria A1 contará pulsos para mim. Portanto, seu zeramento exibirá o sinalizador Process_Finished .

Construímos o gráfico de transição do autômato:



A variável que define seu estado é chamada State . Mapeie imediatamente essa variável para o registro de endereço da instrução ALU. No começo eu esqueci de fazer isso, então por um longo tempo eu não conseguia entender por que minha máquina não funciona. Clique duas vezes no bloco de entradas no Datapath:



E combinar:



Começamos a lidar com o gráfico de transição e as instruções da ALU associadas a ele.

Vamos começar com o estado ocioso . É bastante saturado em suas ações.

Em primeiro lugar, o valor dos registros de dados D0 e D1 é constantemente colocado nas baterias A0 e A1, respectivamente:



A partir dessa entrada, o olho treinado verá tudo o que você precisa. Como nossos olhos ainda não estão definidos, clicamos duas vezes na entrada e vemos a mesma coisa, mas com mais detalhes:



O principal valor aqui é encher a bateria A1, o contador de pulsos. Quando o programa digita o valor D1, ele imediatamente passa para A1. O programa definitivamente não terá tempo para iniciar o processo até a próxima medida. Este valor é verificado para formar uma condição para sair desse estado, ou seja, não há outro lugar para preenchê-lo.

Agora vamos ver o que é feito no nível do gráfico de transição:



O gatilho auxiliar Start_Prev permite capturar uma margem positiva no início de entrada, organizando uma linha de atraso por 1 ciclo. Ele sempre conterá o estado da entrada Iniciar , que estava na medida anterior. Alguém está mais familiarizado com isso no Verilog:



Mesmo texto
always @ (posedge clock) begin : Idle_state_logic case(State) Idle : begin Start_Prev <= (Start); IsIdle <= (1); if (( Start&(!Start_Prev)&(!Process_Finished) ) == 1'b1) begin State <= One ; end end 


Portanto, a condição Start & (! Start_Prev) é verdadeira somente quando ocorre uma diferença positiva na linha de partida entre as medidas .

Além disso, quando a máquina está nesse estado, a saída IsIdle é trazida para um único estado, informando ao ambiente externo que o bloco é passivo. Com essa abordagem, menos recursos PLD são gastos do que se a construção State == Idle fosse submetida à saída.

Quando a diferença do sinal de partida vier do ambiente externo e o acumulador A1 tiver um valor diferente de zero, a máquina sairá do estado ocioso . Se for inserido zero em A1, o mecanismo não estará envolvido no desenvolvimento desse segmento, para que a diferença na linha de partida seja ignorada. Isso se aplica a uma extrusora não utilizada. Para algumas impressoras, o mecanismo do eixo Z também é raramente usado.Lembre-se de como uma condição é formada que revela um valor zero em A1 (e diferente de zero é a inversão):



Em seguida, a máquina entra no estado Um :



Nesse estado, a saída da etapa é definida como 1. Um pulso de etapa é aplicado ao driver. Além disso, o valor do acionador IsIdle é redefinido . O ambiente externo é informado de que a unidade está na fase ativa.

Este estado é encerrado pelo sinal One_Finished , que será aumentado para um quando o contador de sete bits for zero. Deixe-me lembrá-lo de que o sinal One_Finished é gerado por este contador específico:



Enquanto a máquina estiver nesse estado, a ALU carrega na bateria A0 (configurando a duração do pulso) o valor do registro D0. Deixe-me mostrar apenas uma pequena nota dizendo o seguinte:



O valor carregado será usado no seguinte estado. Estando nela, a máquina gera um atraso que define a duração do pulso:



A saída da etapa é redefinida para zero. A bateria A0 diminui, conforme evidenciado pela seguinte entrada breve:



E se você clicar duas vezes nele - uma entrada completa:



Quando o valor de A0 chegar a zero, a flag Pules_Finished será aumentada e a máquina entrará no estado Decrement :



Nesse estado, na ALU, o valor do acumulador A1 diminui, o que define o número de pulsos:



Versão completa do registro:



Dependendo do resultado, ocorre uma transição para o próximo pulso ou para o estado ocioso . Clique duas vezes no estado para ver as transições levando em consideração as prioridades:



Na verdade, com tudo UDB. Agora fazemos o símbolo correspondente. Para fazer isso, clique com o botão direito do mouse no editor e selecione Gerar símbolo :



Vamos ao diagrama do projeto:



E nós introduzimos um circuito no qual existe um certo número desses controladores. Eu escolhi cinco (três eixos mais duas extrusoras). Impressoras com um grande número de extrusoras não serão consideradas baratas. Você pode colocar FPGA neles. Ao longo do caminho, para ver a real complexidade, joguei um bloco USB-UART (para receber dados de um computador ou o mesmo Raspberry Pi) e um UART real (ele fornecerá comunicação com um módulo Wi-Fi barato ESP8266 ou, por exemplo, um monitor inteligente que pode envie GCODE via UART). Não adicionei PWMs e assim por diante, pois a complexidade deles é aproximadamente clara e o sistema real ainda está longe. Aconteceu algo assim:



O registro de controle gera um sinal de disparo, que vai para todos os blocos simultaneamente. Além disso, deixe sair sinais, que são estáticos durante a formação do segmento. Eu coletei todas as saídas ociosas por "And" e apliquei na entrada de interrupção. Marquei uma interrupção em uma frente positiva. Se pelo menos um motor der partida, a entrada de interrupção será redefinida. No final do último mecanismo, ele será engatilhado, o que informará o processador sobre a disponibilidade para a conclusão do próximo segmento. Agora ajuste as frequências clicando duas vezes no elemento da árvore Clocks :



Na tabela exibida, clique duas vezes no elemento PLL_OUT :



De alguma forma, preencheremos a tabela (não entendi bem as regras para configurar essa tabela, e é por isso que uso o termo "Algo assim"):



Agora clique duas vezes na linha Clock_1 :



Defina a frequência do relógio dos blocos UDB para 48 MHz:



Como o projeto é experimental, não faz sentido criar uma API para ele. Mas, para consolidar o material estudado no artigo anterior, vamos novamente à guia Componentes e, para o projeto StepperController, clique com o botão direito do mouse no item Adicionar componente primeiro, adicione o arquivo de cabeçalho e, em seguida, o arquivo de código-fonte C:





Mostrarei superficialmente as duas funções de inicialização e início do segmento que adicionei. O restante pode ser visto no exemplo do artigo.

 void `$INSTANCE_NAME`_Start() { `$INSTANCE_NAME`_SingleVibrator_Start(); //"One" Generator start } void `$INSTANCE_NAME`_PrepareStep(int nSteps,int duration) { CY_SET_XTND_REG24(`$INSTANCE_NAME`_Datapath_1_D0_PTR, duration>92?duration-92:0); CY_SET_XTND_REG24(`$INSTANCE_NAME`_Datapath_1_D1_PTR, nSteps>1?nSteps-1:0); } 

Substituí o nome de main.c por main.cpp para verificar se o ambiente de desenvolvimento responderá normalmente ao C ++, porque o firmware do Marlin é orientado a objetos. Previsivelmente, erros que foram eliminados previsivelmente pela adição de uma coisa comum:



Mesmo texto
 extern "C" { #include "project.h" } 


Para o lançamento global de motores, eu fiz essa função (é muito difícil, mas para experimentos com um cavalo esférico no vácuo, é o que acontece, em experimentos o tempo de desenvolvimento é mais importante que a beleza):
 void StartSteppers() { Stepper_Control_Reg_Write (1); Stepper_Control_Reg_Write (1); Stepper_Control_Reg_Write (1); Stepper_Control_Reg_Write (0); } 

Ela inicia o sinal de início , por precaução, imediatamente por três medidas e depois o solta novamente.

Bem, vamos começar os experimentos. Primeiro, basta passar pelos mecanismos X e Y (no exemplo, o primeiro grupo de chamadas inicializa todos os controladores, o segundo define os controladores X e Y para o número necessário de etapas e inicia o processo):

 int main(void) { CyGlobalIntEnable; /* Enable global interrupts. */ StepperController_X_Start(); StepperController_Y_Start(); StepperController_Z_Start(); StepperController_E0_Start(); StepperController_E1_Start(); StepperController_X_PrepareStep (10,1000); //    StepperController_Y_PrepareStep (50,500); StartSteppers(); //   for(;;) { } } 

Nós olhamos para o resultado:



Verifique a duração do pulso positivo:



Isso mesmo. Por fim, verificamos quão bem a interrupção funciona. Adicione uma variável global de contador:

 static int nStep=0; 

Essa variável é atribuída a uma na função principal e aumenta a função do manipulador de interrupções. O manipulador de interrupção será acionado apenas uma vez, apenas para verificação. Eu fiz assim:

 extern "C" { CY_ISR(StepperFinished) { if (nStep == 1) { StepperController_X_PrepareStep (5,500); StartSteppers(); nStep += 1; } } } 

E na função principal , adicionei literalmente duas linhas: a inclusão de interrupções e a atribuição dessa mesma variável. E já atribuo quando as máquinas foram iniciadas. Caso contrário, veio uma solicitação de interrupção falsa. Não há nenhuma razão específica para combatê-lo agora. O projeto é experimental.



Mesmo texto
 int main(void) { CyGlobalIntEnable; /* Enable global interrupts. */ isr_1_StartEx(StepperFinished); StepperController_X_Start(); StepperController_Y_Start(); StepperController_Z_Start(); StepperController_E0_Start(); StepperController_E1_Start(); /* Place your initialization/startup code here (eg MyInst_Start()) */ StepperController_X_PrepareStep (10,1000); StepperController_Y_PrepareStep (20,500); StartSteppers(); nStep = 1; for(;;) { } } 


Verificamos o resultado (na segunda etapa, apenas o mecanismo X deve funcionar e as etapas devem ficar com a metade):



Isso mesmo.

Conclusão


Em geral, já está claro que os blocos UDB podem ser usados ​​não apenas para definir funções rápidas de hardware, mas também para mover a lógica do software para o nível do firmware. Infelizmente, o volume do artigo foi tão grande que parece impossível concluir a revisão e obter uma resposta inequívoca se os recursos do UDB são suficientes para a solução final da tarefa. Até agora, apenas um cavalo esférico está pronto no vácuo, cujas ações, em princípio, são muito semelhantes às necessárias, mas um leitor irritante familiarizado com a teoria do controle do motor de passo encontrará muitas deficiências nele. A unidade apresentada não suporta aceleração, sem a qual a operação de um motor de passo real é impossível. Em vez disso, suporta, mas nesse estágio será necessária uma alta taxa de interrupção, e tudo foi concebido para evitar isso.

A precisão de definir a frequência do bloco apresentado está longe de ser aceitável. Em particular, fornecerá uma frequência de pulso de 40.000 Hz com um divisor de 1200 e 39966 Hz com um divisor de 1201. As frequências intermediárias entre esses dois valores neste bloco são inatingíveis.

Talvez haja outras deficiências nele. Mas vamos lidar com eles no próximo artigo para verificar se existem recursos UDB suficientes.

Enquanto isso, os leitores receberam, entre outras coisas, um exemplo real de criação de um bloco baseado em UDB do zero. O projeto de teste obtido durante a redação deste artigo pode ser realizado aqui .

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


All Articles