Parte 2: Usando os controladores UDB PSoC da Cypress para reduzir o número de interrupções em uma impressora 3D



Na última vez, consideramos a opção de gerar pulsos para motores de passo, parcialmente removidos do software até o nível do firmware. Em caso de sucesso total, isso promete a ausência da necessidade de processar interrupções chegando com uma frequência de até 40 kHz. Mas essa opção tem várias falhas óbvias. Primeiro, as acelerações não são suportadas lá. Em segundo lugar, a granularidade das frequências de passo permitidas nessa solução é centenas de hertz (por exemplo, é possível gerar frequências de 40.000 Hz e 39966 Hz, mas é impossível gerar frequências com uma magnitude entre esses dois valores).

Implementação de aceleração


É possível eliminar as desvantagens indicadas usando as mesmas ferramentas UDB sem complicar o sistema? Vamos acertar. Vamos começar com o mais difícil - com acelerações. As acelerações são adicionadas no início e no final do caminho. Primeiramente, se os pulsos de alta frequência forem aplicados imediatamente ao motor de passo, será necessária uma corrente maior para iniciar a operação. A corrente alta permitida é aquecimento e ruído, por isso é melhor limitá-la. Mas então o mecanismo pode pular etapas no início. Portanto, é melhor acelerar o motor sem problemas. Em segundo lugar, se uma cabeça pesada para abruptamente, ela experimenta transientes associados à inércia. As ondas são visíveis no plástico. Portanto, é suavemente necessário não apenas dispersar, mas também parar a cabeça. Classicamente, é apresentado um gráfico da velocidade do motor na forma de um trapézio. Aqui está um fragmento do código-fonte do firmware do Marlin:



Nem tentarei descobrir se é possível implementar isso usando o UDB. Isso se deve ao fato de que outro tipo de aceleração está na moda: não trapezoidal, mas a curva S. A agenda deles é assim:



Definitivamente, isso não é para UDB. Desistir? Nem um pouco! Eu já observei que o UDB não implementa uma interface de hardware, mas simplesmente permite transferir parte do código do software para o nível do firmware. Deixe o perfil calcular o processador central e a formação de pulsos de etapa ainda executa UDB. O processador central tem muito tempo para cálculos. A tarefa de eliminar interrupções frequentes continuará a ser resolvida com bastante elegância e ninguém planejou levar o processo completamente ao nível do firmware.

Obviamente, o perfil precisará ser preparado na memória e o UDB coletará dados usando o DMA. Mas quanta memória é necessária? Um milímetro precisa de 200 etapas. Agora, com codificação de 24 bits, são 600 bytes por 1 mm de movimento da cabeça! Mais uma vez, lembre-se de interrupções não tão frequentes, mas ainda constantes, para transmitir tudo em fragmentos? Na verdade não! O fato é que o mecanismo de DMA do PSoC é baseado em descritores. Depois de executar a tarefa de um descritor, o controlador DMA continua para o próximo. E assim, ao longo da cadeia, você pode usar muitos descritores. Ilustramos isso com alguns desenhos da documentação oficial:



Na verdade, esse mecanismo também pode ser usado através da construção de uma cadeia de três descritores:

Não.Explicação
1Da memória ao FIFO com incremento de endereço. Indica uma seção com um perfil de aceleração.
2Da memória ao FIFO sem incremento de endereço. Envia o tempo todo para a mesma palavra na memória para velocidade constante.
3Da memória ao FIFO com incremento de endereço. Indica uma seção com um perfil de frenagem.

Acontece que o caminho principal é descrito na etapa 2 e a mesma palavra é usada fisicamente, o que define a velocidade constante. O consumo de memória não é grande. Na realidade, o segundo descritor pode ser representado fisicamente por dois ou três descritores. Isso se deve ao fato de que o comprimento máximo de bombeamento, de acordo com a TRM, pode ser de 64 kilobytes (a alteração será menor). Ou seja, 32.767 palavras. Isso a 200 passos por milímetro corresponderá a um caminho de 163 milímetros. Pode ser necessário fazer um segmento de duas ou três partes, dependendo da distância máxima que o motor pode percorrer por vez.

No entanto, para economizar memória (e às custas dos blocos UDB), proponho abandonar os blocos DatapPath de 24 bits, passando para os mais econômicos de 16 bits.

Então A primeira proposta de revisão.

As matrizes são preparadas na memória que codificam a duração das etapas. Além disso, essas informações vão para o UDB usando DMA. A seção retilínea é codificada por uma matriz de um elemento, o bloco DMA não aumenta o endereço, escolhendo o mesmo elemento o tempo todo. As seções de aceleração, retilínea e de frenagem são conectadas pelos meios disponíveis no controlador DMA.

Ajuste fino médio


Agora vamos considerar como superar o problema da granularidade de frequência. Obviamente, não será possível defini-lo exatamente. Mas, de fato, o "firmware" original também não pode fazer isso. Em vez disso, eles usam o algoritmo de Bresenham. Um atraso de uma medida é adicionado a algumas etapas. Como resultado, a frequência média se torna intermediária, entre um valor menor e um maior. Ajustando a proporção de períodos regulares e prolongados, você pode alterar suavemente a frequência média. Se nossa velocidade agora não é definida pelo registro de dados, mas transmitida via FIFO, e o número de pulsos geralmente é definido pelo número de palavras transmitidas via DMA, os dois registros de dados no UDB são liberados. Além disso, uma das baterias, que conta o número de pulsos, também é liberada. Aqui vamos construir um certo PWM neles.

Normalmente, as ALUs comparam e atribuem registros com o mesmo índice. Quando um registro possui um índice 0 e o outro 1, nem todas as versões da operação podem ser implementadas. Mas consegui reunir o solitário dos registros sob os quais o PWM pode ser feito. Acabou como mostrado na figura.



Quando a condição A0 <D1 for atendida, adicionaremos uma batida extra ao comprimento de pulso fornecido. Quando a condição não for cumprida, não o faremos.

Cavalo esférico em condições normais


Então, começamos a modificar o bloco desenvolvido para o UDB, levando em consideração a nova arquitetura. Substitua a profundidade de bits do Datapath:



Vamos precisar de muito mais saídas do Datapath do que da última vez.



Clicando duas vezes neles, vemos os detalhes:



Existem mais dígitos para a variável State , não se esqueça de conectar a mais antiga !!! Na versão antiga, havia um 0 constante.



O gráfico de transição do autômato ficou assim:



Estamos no estado ocioso enquanto o FIFO1 está vazio. A propósito, trabalhar com FIFO1 e não FIFO0 é o resultado da própria formação de paciência. O registro A0 é usado para implementar o PWM, portanto a largura do pulso é determinada pelo registro A1. E eu posso fazer o download apenas do FIFO1 (talvez existam outros métodos secretos, mas eles não são conhecidos por mim). Portanto, o DMA carrega os dados exatamente no FIFO1 e é precisamente o estado "Não vazio" do FIFO1 que sai do estado de espera.

A ALU no estado IDLE anula o registro A0:



Isso é necessário para que, no início da operação do PWM, ele sempre comece a trabalhar desde o início.
Mas os dados entraram no FIFO. A máquina entra no estado LoadData :



Nesse estado, a ALU carrega a próxima palavra do FIFO no registro A1. Ao longo do caminho, para não criar estados desnecessários, o valor do contador A0, usado para trabalhar com o PWM, é aumentado:



Se o contador A0 ainda não atingiu o valor D0 (ou seja, a condição A0 <D0 é acionada, armando o sinalizador NoNeedReloadA0), vamos para o estado Um . Caso contrário, o estado é ClearA0 .

No estado ClearA0, a ALU zera simplesmente o valor de A0, iniciando um novo ciclo PWM:



após o qual a máquina também entra em um estado, apenas uma batida depois.

Um deles é familiar para a versão antiga da máquina. A ALU não executa nenhuma função.

E assim - nesse estado, uma unidade é gerada na saída de Out_Step (aqui o otimizador funcionou melhor quando a unidade é produzida pela condição, isso foi detectado empiricamente).



Estamos nesse estado até que o contador de sete bits que já conhecemos seja redefinido para zero. Mas, se anteriormente saímos desse estado por um caminho, agora pode haver dois caminhos: direto e atrasado ao ritmo.



Entraremos no estado ExtraTick se o sinalizador AddCycle estiver definido , designado para atender à condição A0 <D1. Nesse estado, a ALU não executa nenhuma ação benéfica. Só que o ciclo leva 1 batida a mais. Além disso, todos os caminhos convergem no estado de atraso .

Esta condição mede a duração do pulso. O registro A1 (carregado enquanto ainda está no estado Load ) é reduzido até chegar a zero.



Além disso, dependendo de haver dados adicionais no FIFO ou não, a máquina passará para o próximo lote para carregar ou para o modo inativo . Vejamos isso não na figura (existem setas longas, tudo será pequeno), mas na forma de uma tabela, clique duas vezes no estado Delay :



Agora sai do UDB. Eu converti a flag de estar no estado Idle para comparação assíncrona (na versão anterior havia um gatilho que reinava e reiniciava em vários estados), pois, para ele, o otimizador mostrava o melhor resultado. Além disso, a bandeira Hungry foi adicionada, sinalizando para a unidade DMA que estava pronta para receber dados. Está na bandeira "FIFO1 não está lotado" . Como não está lotado, o DMA pode carregar outra palavra de dados lá.



Na parte automática - é isso.

Adicione blocos DMA ao diagrama principal do projeto. Por enquanto, comecei a interromper os sinalizadores de terminação do DMA, mas não o fato de que isso está correto. Quando o processo de acesso direto à memória estiver concluído, você poderá iniciar um novo processo relacionado ao mesmo segmento, mas não poderá começar a preencher informações sobre o novo segmento. O FIFO ainda possui de três a quatro elementos. No momento, ainda é impossível reprogramar os registros D0 e D1 do bloco com base no UDB, eles ainda são necessários para a operação. Portanto, é possível que interrupções com base nas saídas Out_Idle sejam adicionadas posteriormente . Mas essa cozinha não se relaciona mais à programação de blocos UDB, portanto, apenas a mencionaremos de passagem.



Experiências de software


Como agora tudo não é conhecido, não escreveremos nenhuma função especial. Todas as verificações serão realizadas "Na testa". Então, com base em experiências bem-sucedidas, as funções da API podem ser gravadas. Então Tornamos a função main () mínima. Simplesmente configura o sistema e chama o teste selecionado.

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(); // TestShortSteps(); TestWithPacking (); for(;;) { } 

Vamos tentar enviar um pacote de pulsos chamando uma função, verificando o fato de inserir um pulso adicional. A chamada de função é simples:

 TestShortSteps(); 

Mas o corpo requer explicação.
Vou dar toda a função primeiro
 void TestShortSteps() { //   ,   //      //   ,  DMA    !!! //    ,   !!! StepperController_X_SingleVibrator_WritePeriod (6); //     //    —   CY_SET_REG16(StepperController_X_Datapath_1_D0_PTR, 4); CY_SET_REG16(StepperController_X_Datapath_1_D1_PTR, 2); //         . //         static const uint16 steps[] = { 0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001, 0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001 }; //  DMA  ,      uint8 channel = DMA_X_DmaInitialize (sizeof(steps[0]),1,HI16(steps),HI16(StepperController_X_Datapath_1_F1_PTR)); CyDmaChRoundRobin (channel,true); //       ,       uint8 td = CyDmaTdAllocate(); //       .  ,    . CyDmaTdSetConfiguration(td, sizeof(steps), CY_DMA_DISABLE_TD, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT); //       CyDmaTdSetAddress(td, LO16((uint32)steps), LO16((uint32)StepperController_X_Datapath_1_F1_PTR)); //      CyDmaChSetInitialTd(channel, td); //         CyDmaChEnable(channel, 1); } 


Agora considere suas partes importantes.

Se a duração da parte positiva do pulso for igual a 92 ciclos de relógio, o osciloscópio não poderá discernir se existe ou não uma inserção de ciclo único na parte negativa. A escala não será a mesma. É necessário tornar a parte positiva o mais curta possível, para que o pulso total seja comparável em escala com a batida inserida. Portanto, mudo à força o período do contador que define a duração da parte positiva do pulso:

  //   ,   //      //   ,  DMA    !!! //    ,   !!! StepperController_X_SingleVibrator_WritePeriod (6); 

Mas por que seis medidas inteiras? Por que não três? Por que não dois? Por que, afinal, não um? Esta é uma história triste. Se o pulso positivo for menor que 6 ciclos, o sistema não funcionará. A depuração longa em um osciloscópio com a saída de linhas de teste para o exterior mostrou que o DMA não é uma coisa rápida. Se a máquina estiver funcionando por menos de uma determinada duração, quando sair do estado de atraso , o FIFO estará frequentemente vazio. Ainda não pode ser colocada uma única nova palavra de dados! E somente quando a parte positiva do pulso tem uma duração de 6 ciclos, é garantido que o FIFO tenha tempo para carregar ...

Digressão de latência


Outra idéia de correção que está na minha cabeça é a aceleração por hardware de certas funções do kernel do nosso RTOS MAX. Mas, infelizmente, todas as minhas melhores idéias são quebradas sobre as mesmas latências.

Houve um caso, estudei o desenvolvimento de aplicativos Bare Metal para o Cyclone V SoC. Porém, trabalhar com registradores FPGA únicos (ao escrever alternadamente para eles e depois lê-los) reduz a operação principal centenas (!!!) de vezes. Você ouviu direito. Está em centenas. Além disso, tudo isso está mal documentado, mas a princípio senti interiormente e depois provei, a partir de fragmentos de frases da documentação, que as latências eram culpadas ao passar solicitações através de várias pontes. Se você precisar expulsar uma grande matriz, também haverá latência, mas em termos de uma palavra bombeada, isso não será significativo. Quando as solicitações são únicas (e a aceleração de hardware do kernel do SO implica apenas elas), a desaceleração ocorre exatamente centenas de vezes. Será muito mais rápido fazer tudo de maneira puramente programática, quando o programa trabalhar com a memória principal através do cache a uma velocidade frenética.

No PSoC, eu também tinha alguns planos. Na aparência, você pode procurar maravilhosamente dados em uma matriz usando DMA e UDB. O que realmente existe! Devido à estrutura do descritor de DMA, esses controladores poderiam realizar uma pesquisa completamente de hardware em listas vinculadas! Mas, tendo recebido o plug descrito acima, percebi que ele também está associado à latência. Aqui, essa latência é lindamente descrita na documentação. Tanto na família TRM quanto em um documento separado AN84810 - PSoC 3 e PSoC 5LP Tópicos avançados de DMA . A seção 3.2 é dedicada a isso. Portanto, a próxima aceleração de hardware é cancelada. Uma pena. Mas, como Semyon Semyonovich Gorbunkov disse: "Vamos procurar".

Continuando experimentos de software


Em seguida, defino os parâmetros do algoritmo de Bresenham:

  //     //    —   CY_SET_REG16(StepperController_X_Datapath_1_D0_PTR, 4); CY_SET_REG16(StepperController_X_Datapath_1_D1_PTR, 2); 

Bem, então vem o código regular que transfere uma matriz de palavras através do DMA para o FIFO1 da unidade de controle do motor X.

O resultado requer alguma explicação. Aqui está:



O valor do contador A0 é mostrado em vermelho quando a máquina está no estado Um . O asterisco verde mostra casos em que o atraso é inserido devido à máquina estar no estado ExtraTick . Também existem barras nas quais o atraso se deve ao estado ClearA0 ; elas são marcadas com uma grade azul.

Como você pode ver, quando você entra pela primeira vez, o primeiro atraso é perdido. Isso ocorre porque A0 é redefinido quando está ocioso , mas aumenta quando entra no LoadData . Portanto, até o ponto da análise (saída do estado de Um ), já é igual à unidade. A conta começa com ela. Mas, em geral, isso não afetará a frequência média. Só precisa ser mantido em mente. Como deve ser lembrado que, ao redefinir A0, o relógio também será inserido. Isso deve ser levado em consideração no cálculo da frequência média.

Mas, em geral, o número de pulsos está correto. A duração deles também é crível.
Vamos tentar programar uma cadeia de descritores mais real,

constituído por uma fase de aceleração, movimento linear e travagem.
 void TestWithPacking(int countOnLinearStage) { //   ,   //     . //   ,  DMA    !!! //    ,   !!! StepperController_X_SingleVibrator_WritePeriod (6); //     //    —   CY_SET_REG16(StepperController_X_Datapath_1_D0_PTR, 4); CY_SET_REG16(StepperController_X_Datapath_1_D1_PTR, 2); //    static const uint16 accelerate[] = {0x0010,0x0008,0x0004}; //    static const uint16 deccelerate[] = {0x004,0x0008,0x0010}; //  .    . static const uint16 steps[] = {0x0001}; //  DMA  ,      uint8 channel = DMA_X_DmaInitialize (sizeof(steps[0]),1,HI16(steps),HI16(StepperController_X_Datapath_1_F1_PTR)); CyDmaChRoundRobin (channel,true); //   uint8 tdDeccelerate = CyDmaTdAllocate(); CyDmaTdSetConfiguration(tdDeccelerate, sizeof(deccelerate), CY_DMA_DISABLE_TD, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT); CyDmaTdSetAddress(tdDeccelerate, LO16((uint32)deccelerate), LO16((uint32)StepperController_X_Datapath_1_F1_PTR)); //       uint8 tdSteps = CyDmaTdAllocate(); //   !!! //     !!! CyDmaTdSetConfiguration(tdSteps, countOnLinearStage, tdDeccelerate, /*TD_INC_SRC_ADR |*/ TD_AUTO_EXEC_NEXT); CyDmaTdSetAddress(tdSteps, LO16((uint32)steps), LO16((uint32)StepperController_X_Datapath_1_F1_PTR)); //   //     !!! uint8 tdAccelerate = CyDmaTdAllocate(); CyDmaTdSetConfiguration(tdAccelerate, sizeof(accelerate), tdSteps, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT); CyDmaTdSetAddress(tdAccelerate, LO16((uint32)accelerate), LO16((uint32)StepperController_X_Datapath_1_F1_PTR)); //      CyDmaChSetInitialTd(channel, tdAccelerate); //         CyDmaChEnable(channel, 1); } 


Primeiro, chame as mesmas dez etapas (no DMA, na verdade, 20 bytes vão):

 TestWithPacking (20); 

O resultado é o esperado. No início, a aceleração é visível. E a saída para IDLE (raio azul) ocorre com um grande atraso do último pulso; foi então que o último passo foi completamente concluído, seu valor é aproximadamente igual ao valor do primeiro.



Cavalo de verdade em condições normais


Ao remodelar o equipamento, de alguma forma eu pulei de uma largura de pulso de 24 bits para um trabalho de 16 bits. Mas descobrimos que isso não pode ser feito: a frequência mínima de pulso será muito alta. Eu fiz isso intencionalmente. O fato é que a técnica para expandir a capacidade de bits de um contador de 16 bits se tornou tão complicada que, se eu tivesse começado a descrevê-lo junto com a máquina principal, teria desviado toda a atenção. Portanto, consideramos isso separadamente.

Temos uma bateria de 16 bits. Decidi adicionar a entidade padrão de contador de sete bits aos bits altos. O que é esse contador de sete bits? Esse é o design que está disponível em cada bloco UDB (o bloco UDB base tem uma largura de bits de todos os registros de 8 bits, o aumento na profundidade de bits é determinado pela combinação de blocos em grupos). Dos mesmos recursos, os registros de Controle / Status podem ser implementados. Agora, temos um contador e não um único par Controle / Status para 16 bits de dados. Portanto, adicionando outro contador ao sistema, não atrasaremos os recursos extras. Nós apenas pegamos o que já está alocado para nós. Isso é legal! Nós criamos o byte alto do contador de largura de pulso através desse mecanismo e obtemos a largura total do contador de largura de pulso igual a 23 bits.



Primeiro direi o que estava pensando. Eu pensei que depois de sair do estado de atraso , eu verificaria a conclusão da contagem desse contador adicional. Se ele não tiver terminado a contagem, reduzirei seu valor e novamente mudarei para o estado Atraso . Se você contou, a lógica permanecerá a mesma, sem adicionar ciclos extras.

Além disso, a documentação deste contador diz que estou certo. Literalmente diz:
Período
Define o valor do registro do período inicial. Para um período de N relógios, o valor do período deve ser definido como N-1. O contador contará de N-1 até 0, o que resulta em um período de ciclo de N relógio. Um valor de registro de período igual a 0 não é suportado e resultará na saída da contagem de terminais mantida em um estado alto constante.
A vida mostrou que tudo é diferente. Deduzi o estado da linha de contagem do terminal no osciloscópio e observei seu valor em um zero pré-carregado no período e durante o carregamento do programa. Ai e ah. Não havia estado alto constante !

Por tentativa e erro, consegui que o sistema funcionasse corretamente, mas, para que isso aconteça, pelo menos uma subtração do contador deve acontecer! O novo estado de "subtração" não está do lado. Tinha que ser encaixado no caminho necessário. Ele está localizado na frente do estado Delay e é chamado Next65536 .



A ALU nesse estado não executa nenhuma ação útil. Na verdade, apenas um novo contador reage ao fato de estar nesse estado. Aqui está no diagrama:



Aqui estão suas propriedades em mais detalhes:



Em geral, levando em conta os artigos anteriores, a essência desse contador é clara. Somente a linha Ativar está sofrendo. Novamente, eu não entendo completamente por que ele deve ser ligado quando a máquina está no estado LoadData (o contador recarrega o valor do período). Peguei esse truque emprestado nas propriedades do contador que controla os LEDs, retirado do autor inglês da unidade de controle desses LEDs. Sem ele, o valor zero do período não funciona. Ela trabalha com ela.

No código da API, adicionamos a inicialização de um novo contador. Agora, a função de início fica assim:

 void `$INSTANCE_NAME`_Start() { `$INSTANCE_NAME`_SingleVibrator_Start(); //"One" Generator start `$INSTANCE_NAME`_Plus65536_Start(); } 

Vamos verificar o novo sistema. Aqui está o código de função para testar

(nele, apenas a primeira linha difere da já conhecida):
 void JustTest(int extra65536s) { //      65536  StepperController_X_Plus65536_WritePeriod((uint8) extra65536s); //     //    —   CY_SET_REG16(StepperController_X_Datapath_1_D0_PTR, 4); CY_SET_REG16(StepperController_X_Datapath_1_D1_PTR, 2); //         . //         static const uint16 steps[] = { 0x1000,0x1000,0x1000,0x1000 }; //  DMA  ,      uint8 channel = DMA_X_DmaInitialize (sizeof(steps[0]),1,HI16(steps),HI16(StepperController_X_Datapath_1_F1_PTR)); CyDmaChRoundRobin (channel,true); //       ,       uint8 td = CyDmaTdAllocate(); //       .  ,    . CyDmaTdSetConfiguration(td, sizeof(steps), CY_DMA_DISABLE_TD, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT); //       CyDmaTdSetAddress(td, LO16((uint32)steps), LO16((uint32)StepperController_X_Datapath_1_F1_PTR)); //      CyDmaChSetInitialTd(channel, td); //         CyDmaChEnable(channel, 1); } 


Chamamos assim:

  JustTest(0); 

No osciloscópio, vemos o seguinte (feixe amarelo - saída STEP, azul - valor da saída do contador TC para controle de processo). A duração do pulso é definida por etapas . Em cada etapa, a duração é de 0x1000 medidas.



Alterne para outra verificação para que haja compatibilidade entre resultados diferentes:



Altere a chamada de função para isso:

  JustTest(1); 

O resultado é o esperado. Primeiro, a saída do TC é zero para ciclos de 0x1000, então - uma unidade para ciclos de 0x10000 (65536d). A frequência é aproximadamente igual a 700 hertz, descobrimos na última parte do artigo, então está tudo certo.



Bem, vamos tentar um empate:

  JustTest(2); 

Temos:



Isso mesmo. A saída do TC é alterada para uma nos últimos 65536 ciclos de relógio. Antes disso, ele estava em zero por 0x1000 + 0x10000 ciclos.

Obviamente, com essa abordagem, todos os pulsos devem seguir o mesmo valor do novo contador. É impossível fazer um pulso com o byte mais alto durante a aceleração, digamos 3, depois 1 e depois 0. Mas, de fato, em freqüências tão baixas (menos de setecentos hertz) as acelerações não têm significado físico, portanto esse problema pode ser negligenciado. Nesta frequência, você pode trabalhar com o mecanismo linearmente.

Voar na pomada


O documento TRM para a família PSoC5LP declara:
Cada transação pode ter de 1 a 64 KB
Mas no já mencionado AN84810 existe uma frase:
1. Como você pode armazenar em buffer mais de 4095 bytes usando o DMA?
A contagem máxima de transferências de um TD é limitada a 4095 bytes. Se você precisar transferir mais de 4095 bytes usando um único canal DMA, use vários TDs e encadeie-os conforme mostrado no Exemplo 5.
Quem está certo? Se você realizar experimentos, os resultados serão inclinados a favor das piores declarações, mas o comportamento será completamente incompreensível. A falha toda é essa verificação na API:



O mesmo texto.
 cystatus CyDmaTdSetConfiguration(uint8 tdHandle, uint16 transferCount, uint8 nextTd, uint8 configuration) \ { cystatus status = CYRET_BAD_PARAM; if((tdHandle < CY_DMA_NUMBEROF_TDS) && (0u == (0xF000u & transferCount))) { /* Set 12 bits transfer count. */ reg16 *convert = (reg16 *) &CY_DMA_TDMEM_STRUCT_PTR[tdHandle].TD0[0u]; CY_SET_REG16(convert, transferCount); /* Set Next TD pointer. */ CY_DMA_TDMEM_STRUCT_PTR[tdHandle].TD0[2u] = nextTd; /* Configure the TD */ CY_DMA_TDMEM_STRUCT_PTR[tdHandle].TD0[3u] = configuration; status = CYRET_SUCCESS; } return(status); } 


Se for especificada uma transação com mais de 4095 bytes, a configuração anterior será usada. Sim, não pensei em verificar códigos de erro ...

As experiências mostraram que, se você remover essa verificação, o comprimento real será cortado usando a máscara 0xfff (4096 = 0x1000). Ai e ah. Todas as esperanças de um trabalho agradável fracassaram. Você pode, é claro, criar cadeias de descritores relacionados em 4K. Mas, digamos, 64K são 16 cadeias. Três motores ativos (extrusoras terão menos etapas) - 48 correntes. Exatamente isso deve ser preenchido no pior dos casos, antes de cada segmento. Talvez seja aceitável a tempo. No mínimo, 127 descritores estão disponíveis, portanto, definitivamente haverá memória suficiente.

Você pode enviar os dados ausentes, conforme necessário. Houve uma interrupção porque o canal DMA havia concluído o trabalho, estamos transferindo outro segmento para ele. Nesse caso, não são necessários cálculos, o segmento já está formado, tudo será rápido. E não há requisitos de desempenho: quando uma solicitação de interrupção é emitida, haverá mais 4 elementos no FIFO que serão atendidos cada um por várias centenas ou mesmo milhares de ciclos de clock. Ou seja, tudo é real. Uma estratégia específica será mais fácil de escolher durante o trabalho real. Mas um erro na documentação (TRM) estragou todo o clima. Se isso fosse conhecido com antecedência, talvez eu não tivesse verificado a metodologia.

Conclusão


Na aparência, a ferramenta de firmware auxiliar desenvolvida tornou-se aceitável para que, com base em si, fosse possível criar uma versão do "Firmware", por exemplo, Marlin, que não está constantemente no manipulador de interrupções para motores de passo. Até onde eu sei, isso é especialmente verdade para as impressoras Delta, onde a demanda por recursos de computação é bastante alta. Talvez isso elimine o influxo que ocorre no meu Delta em lugares onde a cabeça para. No MZ3D nesses mesmos locais, não é observado influxo. Se é verdade ou não, o tempo dirá, e o relatório sobre isso precisará ser publicado em uma ramificação completamente diferente.

Enquanto isso, vimos que, no bloco UDB, por toda a sua simplicidade, é bem possível implementar um coprocessador trabalhando em conjunto com o processador principal e permitindo que ele seja descarregado. E quando existem muitas dessas unidades, os coprocessadores podem trabalhar em paralelo.

Um erro na documentação do controlador DMA embaçou o resultado. No entanto, são necessárias interrupções, mas não na mesma frequência e não com a criticidade no tempo que estava na versão original. Portanto, o clima é estragado, mas o uso de um "coprocessador" baseado em UDB ainda oferece um ganho considerável em comparação com o trabalho puramente de software.

Ao longo do caminho, foi revelado que o DMA funciona a uma velocidade bastante baixa. Como resultado disso, algumas medições foram realizadas no PSoC5LP e no STM32. Os resultados puxam outro artigo. Talvez um dia eu o faça se o assunto for interessante.

Como resultado dos experimentos, dois projetos de teste foram obtidos ao mesmo tempo. O primeiro é mais fácil de entender. Você pode pegar aqui . O segundo é herdado do primeiro, mas confuso ao adicionar um contador de sete bits e a lógica associada. Você pode pegar aqui . Obviamente, esses exemplos são apenas exemplos de teste. Ainda não há tempo livre para incorporar o verdadeiro "firmware". Porém, dentro da estrutura desses artigos, é mais importante praticar o trabalho com o UDB.

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


All Articles