
1. Introdução
Há muito tempo, eu queria aprender a técnica de programação de blocos UDB nos controladores Cypress PSoC, mas de alguma forma todas as minhas mãos não alcançaram. E assim, surgiu um problema no qual isso poderia ser feito. Compreendendo os materiais da rede, percebi que as recomendações práticas para trabalhar com o UDB são limitadas a várias variações de contadores e PWMs. Por alguma razão, todos os autores fazem suas variações desses dois exemplos canônicos; portanto, a descrição de outra coisa pode ser interessante para os leitores.
Então Ocorreu um problema ao gerenciar dinamicamente uma longa linha de LEDs RGB WS2812B. Abordagens clássicas para esse assunto são conhecidas. Você pode usar o Arduino trivial, mas a saída é programada; portanto, enquanto os dados estão sendo produzidos, todo o resto fica ocioso; caso contrário, os diagramas de temporização falharão. Você pode utilizar o STM32 e enviar dados por meio de DMA para PWM ou de DMA para SPI. Técnicas são conhecidas. Eu até mesmo controlei pessoalmente uma linha de dezesseis diodos através da SPI. Mas a sobrecarga é ótima. Um bit de dados nos LEDs ocupa 8 bits na memória para o caso de PWM e de 3 a 4 bits (dependendo da frieza do PLL no controlador) para SPI. Embora existam poucos LEDs, isso não é assustador, mas se houver, digamos, algumas centenas, 200 * 24 = 4800 bits = 600 bytes de dados úteis devem ser fisicamente armazenados em um buffer de mais de 4 kilobytes para a opção PWM ou mais de 2 kilobytes para SPI- opções Para indicação dinâmica de buffers, deve haver vários, e o STM32F103 tem RAM para tudo, desde 20 kilobytes. Não que tenhamos encontrado uma tarefa irrealizável, mas um motivo para verificar se isso pode ser implementado no PSoC sem precisar gastar RAM extra, é bastante significativo.
Referências teóricas
Primeiro, vamos descobrir que tipo de animal é esse UDB e como eles trabalham com ele. Filmes instrutivos maravilhosos do fabricante do controlador ajudarão nisso.
Você deve começar a assistir a
partir daqui e, no final de cada vídeo, haverá um link para a próxima série. Passo a passo, você obterá conhecimentos básicos e considerará o exemplo canônico "contador". Bem, e um sistema de controle de semáforo.
Sobre o mesmo, mas cortado em pedaços pequenos, você pode
ver aqui . Meu vídeo não foi reproduzido, mas pode ser baixado e exibido localmente. Entre outras coisas, também há um exemplo canônico da implementação do PWM.
Soluções finalizadas
Para não reinventar a roda (e vice-versa - para aprender a metodologia da experiência de outra pessoa), vasculhei a rede em busca de soluções prontas para o controle de LEDs RGB. A solução mais popular é o StripLightLib.cylib. Mas, há muitos anos, ele planeja adicionar o suporte ao Add DMA. Mas quero experimentar uma solução que não dependa do processador central. Quero iniciar o processo e esquecê-lo, concentrando-me na preparação do próximo quadro.
A solução que atende aos meus desejos foi encontrada em
https://github.com/PolyVinalDistillate/PSoC_DMA_NeoPixel .
Tudo é implementado no UDB (mas os LEDs são apenas uma desculpa, o objetivo é aprender o UDB). Há suporte para DMA. E o projeto lá é claramente organizado de maneira bonita.
Problemas da solução escolhida como base
Como está o "firmware" no projeto PSoC_DMA_NeoPixel, qualquer um pode ver depois de ler o artigo. Isso irá corrigir o material. Por enquanto, só direi que simplifiquei a lógica do firmware original sem reduzir os recursos consumidos (mas ficou mais fácil entender). Então, ele começou a experimentar a substituição da lógica do autômato, que prometia um ganho de recursos, mas enfrentava um problema sério. E então ele decidiu - não é eliminado! E vagas dúvidas começaram a me atormentar: o autor inglês estava tendo o mesmo problema? Sua demo pisca muito bem com os LEDs. Mas e se substituirmos o belo recheio por "todas as unidades" e controlarmos a saída não com nossos olhos, mas com um osciloscópio?
Portanto, da forma mais grosseira possível (você pode até dizer “brutalmente”), formamos os dados:
memset (pPixelArray,0xff,sizeof(pPixelArray));
E aqui vemos essa imagem em um osciloscópio:

O primeiro bit tem uma largura diferente do resto. Pedi para enviar todas as unidades, mas nem todas saem. Entre eles zerados! Altere a verificação:

A largura é diferente para cada oitavo bit.
Em geral, este exemplo como solução independente não é adequado, mas como fonte de inspiração - simplesmente perfeito. Em primeiro lugar, sua inoperabilidade não é visível com o olho (os LEDs ainda estão brilhantes, o olho não vê que brilha na metade do máximo), mas o código é bem estruturado, é bom tomá-lo como base. Em segundo lugar, este exemplo fornece espaço para encontrar maneiras de simplificar e, em terceiro lugar, faz você pensar em como corrigir o defeito. O essencial é entender o material! Então, mais uma vez, depois de ler o artigo, recomendo tentar analisar o exemplo original, percebendo como ele funciona.
Parte prática
Agora começamos a praticar. Estamos testando os principais aspectos do desenvolvimento de firmware para UDB. Considere o relacionamento e as técnicas básicas. Para fazer isso, abra
minha versão do projeto . O bloco esquerdo armazena informações sobre arquivos de trabalho. Por padrão, a guia
Origem está aberta. A principal fonte do projeto é o arquivo
main.c. Na verdade, não há outros arquivos de trabalho no grupo
Arquivos de Origem .

O grupo
Origem Gerada contém funções de biblioteca. É melhor não editá-los. Após cada alteração do "firmware" do UDB, esse grupo será regenerado. Então, onde está a descrição do código para UDB nesse idílio? Para vê-lo, você precisa mudar para a guia
Componentes :

O autor do projeto original criou um conjunto de componentes de dois níveis. No nível superior, encontra-se o circuito
NeoPixel_v1_2.cysch . Isso pode ser visto no esquema principal:

O componente é o seguinte:

O suporte de software para esse esquema será discutido posteriormente. Enquanto isso, descubra que ela própria é uma unidade DMA comum e um determinado símbolo
NeoPixDrv_v1 . Esse misterioso bloco é descrito acima na árvore, que segue da seguinte dica de ferramenta:

UDB "Firmware"
Abra esse componente (arquivo com a extensão
.cyudb ). O desenho aberto é simplesmente enorme. Começamos a entender o que é o quê.

Ao contrário do autor do projeto original, considero a transmissão de cada bit de dados na forma de três partes iguais (no tempo):
- Parte inicial (sempre 1)
- Data Part
- Parar parte (sempre 0)
Com essa abordagem, não é necessário um grande número de contadores (no original havia até três peças, que consumiam uma grande quantidade de recursos). A duração de todas as partes é a mesma e pode ser definida usando um registro. Assim, o gráfico de transição do firmware contém os seguintes estados:
Estado
ocioso . A máquina permanece nela até que novos dados cheguem ao FIFO.

Nos vídeos de treinamento, não estava totalmente claro para mim como o estado da máquina está relacionado à ALU. Os autores usam a comunicação naturalmente, mas eu, como iniciante, não pude vê-la imediatamente. Vamos dar uma olhada rápida em detalhes. A figura acima mostra que o estado
ocioso é codificado com o valor 1'b0. 3'b000 estará mais correto, mas o editor irá refazer tudo da mesma forma. As entradas do bloco
Datapath são descritas assim:

Se você clicar duas vezes neles, uma versão mais detalhada será exibida:

Isso significa que o bit zero do endereço da instrução ALU corresponde ao bit zero da variável que define o estado da máquina. O primeiro é o primeiro, o segundo é o segundo. Se desejado, quaisquer variáveis e expressões pares podem ser correspondidas com os bits de endereço da instrução ALU (na versão original, o segundo bit do endereço da instrução ALU foi correspondido por uma expressão e, na versão atual, ela não é usada explicitamente, mas como exemplo de execução do cérebro é muito claro, você pode dar uma olhada).
Então Com as configurações atuais das entradas, que é o código de status binário da máquina, uma instrução ALU é usada. Quando estamos no estado
ocioso, com o código 000, é usada instrução nula. Aqui está:

Eu já sei a partir desta entrada que este é um NOP banal. Mas você pode clicar duas vezes nele e ler a versão completa:

Os NOPs estão inscritos em todos os lugares. Os registros não são preenchidos com nada.
Agora vamos descobrir que tipo de bandeira misteriosa
! NoData , forçando a máquina a deixar o estado
ocioso . Esta é a saída do bloco
Datapath . No total, podem ser descritas até seis saídas. Só que o
Datapath pode produzir muito mais sinalizadores, mas não há recursos de rastreamento suficientes para todos; portanto, precisamos escolher quais seis (ou menos) realmente precisamos. Aqui está a lista na figura:

Se você clicar duas vezes, os detalhes serão revelados:

Aqui está a lista completa de sinalizadores que podem ser exibidos:

Após selecionar o sinalizador necessário, você deve dar um nome a ele. A partir de agora, o sistema tem uma bandeira. Como você pode ver, o sinalizador
NoData é o nome do
status do bloco F0 da cadeia
(vazio) . Ou seja, um sinal de que não há dados no buffer de entrada. Ah
! NoData , respectivamente, sua inversão. Sinal de disponibilidade de dados. Assim que os dados entrarem no FIFO (programaticamente ou usando DMA), o sinalizador será limpo (e sua inversão armada) e, no próximo ciclo do relógio, o autômato sairá do estado ocioso e entrará no estado
GetData .

Como você pode ver, o autômato sairá desse estado incondicionalmente depois de ter estado nele exatamente um ciclo de relógio. Nenhuma ação é indicada no gráfico de transição para esse estado. Mas você deve sempre observar o que a ALU fará. O código de status é 1'b1, ou seja, 3'b001. Observamos o endereço correspondente na ALU:

Tem alguma coisa Não tendo experiência em ler o que está escrito aqui, abra-o clicando duas vezes na célula correspondente:

Daqui resulta que a própria ULA ainda não executa nenhuma ação. Mas o conteúdo do FIFO0, ou seja, os dados provenientes do programa ou do bloco DMA, será colocado no registro A0. Olhando para o futuro, direi que A0 é usado como um registrador de deslocamento, do qual o byte sairá em forma serial. O registro A1 colocará o valor do registro D1. Em geral, todos os registros D geralmente são preenchidos com software antes que o hardware inicie. Então, ao examinar a API, veremos que o número de tiques do relógio é colocado nesse registro, que define a duração do terceiro bit. Então Em A0, o valor alterado caiu e, em A1, a duração da parte inicial do bit. E na próxima batida, a máquina certamente entrará no estado
Constant1 .

Como o nome do estado implica, a constante 1. é gerada aqui.Vejamos a documentação do LED. É assim que a unidade deve ser transferida:

E aqui está - zero:

Eu adicionei linhas vermelhas. Se assumirmos que as durações dos terços são iguais, os requisitos para a duração dos pulsos (fornecidos na mesma documentação) são atendidos. Ou seja, qualquer impulso consiste em uma unidade inicial, um bit de dados e um zero zero. Na verdade, a unidade inicial é transmitida quando a máquina está no estado
Constant1 .
Nesse estado, a máquina trava a unidade em seu gatilho interno. O nome do gatilho é
CurrentBit . No projeto original, geralmente era um gatilho que define o estado do autômato auxiliar. Decidi que aquela máquina só confundiria todo mundo, então iniciei um gatilho. Não está descrito em nenhum lugar. Mas se você inserir as propriedades do estado, o seguinte registro será visível na tabela:

E sob o estado no gráfico, existe esse texto:

Não se assuste com o símbolo de igual. Esses são os recursos do editor. No código Verilog resultante (gerado automaticamente pelo mesmo sistema), haverá uma seta:
Constant1 : begin CurrentBit <= (1); if (( CycleTimeout ) == 1'b1) begin MainState <= Setup1 ; end end
O valor travado nesse gatilho é a saída de todo o nosso bloco:

Ou seja, quando a máquina entrou no estado de
Constant1 , a saída do bloco que estamos desenvolvendo receberá um. Agora vamos ver como a ALU está programada para o endereço 3'b010:

Nós revelamos este elemento:

A unidade 1 é subtraída do registro A1. O valor de saída da ALU cai no registro A1. Acima, consideramos que A1 é um contador de relógio usado para definir a duração do pulso de saída. Deixe-me lembrá-lo de que foi inicializado a partir do D1 na última etapa.
Qual é a condição para sair de um estado?
CycleTimeOut . É descrito entre as saídas da seguinte maneira:

Então, reunimos a lógica. No estado anterior, o conteúdo do registro D1 previamente preenchido pelo programa caiu no registro A1. Nesta etapa, a máquina converte o gatilho
CurrentBit em um e, na ALU, o registro A1 diminui a cada ciclo do relógio. Quando A1 se torna zero, o sinalizador será automaticamente levantado, ao qual o autor deu o nome
CycleTimeout , como resultado do qual a máquina passará para o estado
Setup1 .
O estado
Setup1 prepara dados para transmitir o pulso útil.

Observamos a instrução ALU em 3'b011. Vou abri-lo imediatamente:

Parece que a ALU não tem ações. Operação NOP. E a saída da ALU não chega a lugar algum. Mas isso não é verdade. Uma ação extremamente importante é a mudança de dados na ALU. O fato é que o bit de transporte entre as saídas está conectado à nossa cadeia
ShiftOut :

E como resultado dessa operação de mudança, o próprio valor alterado não chegará a lugar algum, mas a cadeia
ShiftOut assumirá o valor do bit mais significativo do registrador A0. Ou seja, os dados que devem ser transmitidos. Sob o estado do gráfico, pode-se ver que esse valor, que deixou a ALU na cadeia
ShiftOut , será travado no gatilho
CurrentBit . Deixe-me mostrar o desenho novamente para não rebobinar o artigo:

A transmissão da segunda parte do bit começa - o valor imediato é 0 ou 1.
Retornamos às instruções para ALU. Além do que já foi dito, é claro que o conteúdo do registrador D1 será novamente colocado no registrador A1 para medir novamente a duração do segundo terço do pulso.
O estado
DataStage é muito semelhante ao estado
Constant1 . O autômato simplesmente subtrai um de A1 e entra no próximo estado quando atingir zero. Deixe-me mostrar assim:

e assim:

Depois, vem o estado do
Setup2 , cuja essência já sabemos.

Nesse estado, o gatilho
CurrentBit é zerado (uma vez que o terceiro terço do pulso será transmitido, a parte de parada e sempre será zero). A ALU carrega o conteúdo de D1 na A1. Você pode vê-lo em uma breve nota com seu olho treinado:

O estado de
Constant0 é completamente idêntico aos estados de
Constant1 e
DataStage . Subtraia a unidade de A1. Quando o valor chegar a zero, saia para o estado
ShiftData :


O estado de
ShiftData é mais complexo. Nas instruções correspondentes para ALU, as seguintes ações são executadas:

O registro A0 é deslocado em 1 bit e os resultados são colocados novamente em A0. Em A1, o conteúdo de D1 é novamente colocado para começar a medir o início da terceira para o próximo bit de dados.
É melhor considerar as setas de saída levando em consideração as prioridades, para as quais clicamos duas vezes no estado
ShiftData .

Se não for transmitido o último bit (sobre como esse sinalizador é formado, um pouco mais baixo), transferimos um para o próximo bit do byte atual.
Se o último bit for transmitido e não houver dados no FIFO, iremos para o estado ocioso.
Finalmente, se o último bit for transmitido, mas houver dados no FIFO, vamos para a seleção e transmissão do próximo byte.
Agora, sobre o contador de bits. Existem apenas duas baterias na ALU: A0 e A1. Eles já estão ocupados pelo registro de turno e contador de atraso, respectivamente. Portanto, um contador de bits é usado externamente.

Clique duas vezes nele:

O valor na inicialização é seis. É carregado usando o sinalizador
LoadCounter descrito na seção variável:

Ou seja, quando o próximo byte de dados é obtido, essa constante é carregada ao longo do caminho.
Quando a máquina entra no estado
ShiftData , o contador diminui o valor. Quando chega a zero, o
TerminalCount de saída é conectado, conectado ao circuito de nossa
semente FinalBit . É esse circuito que define se a máquina irá transferir o próximo bit do byte atual ou um novo byte (bem, ou aguardar um novo pacote de dados).
Na verdade, tudo é da lógica. Como o sinal
SpaceForData é
gerado , que define o estado da saída
Hungry (informando à unidade DMA que é possível transmitir os próximos dados), os leitores são convidados a rastrear de forma independente.
Suporte de software
O autor do projeto original optou por oferecer suporte de software para todo o sistema no bloco que descreve a solução integrada. Deixe-me lembrá-lo, estamos falando sobre este bloco:

Nesse nível, há controle sobre a unidade da biblioteca DMA e todas as partes incluídas na parte UDB. Para implementar a API, o autor do original adicionou o cabeçalho e os arquivos de programa:

O formato do corpo desses arquivos deixa você triste. Toda a culpa é do amor dos desenvolvedores do PSoC Designer pelos "puros". Daí as terríveis macros e nomes de quilômetros. A organização da classe em C ++ seria útil aqui. Pelo menos, verificamos isso ao implementar nosso RTOS MAX: ficou bonito e conveniente. Mas aqui você pode discutir muito, mas terá que usar o que deixamos de baixo. Mostrarei apenas brevemente como é a função da API que contém essas macros:
volatile void* `$INSTANCE_NAME`_Start(unsigned int nNumberOfNeopixels, void* pBuffer, double fSpeedMHz) {
Essas regras do jogo devem ser aceitas. Agora você sabe de onde se inspirar ao desenvolver suas funções (é melhor fazer isso no projeto original). E prefiro falar sobre os detalhes, tomando a opção já processada pelo gerador.
Após gerar o código (descrito abaixo), este arquivo será armazenado aqui:

E a vista já estará perfeitamente legível. Até agora, existem duas funções. O primeiro inicializa o sistema, o segundo inicia a transferência de dados do buffer para a linha do LED.
A inicialização afeta todas as partes do sistema. Há inicialização do contador de sete bits, que faz parte do sistema UDB:
NP_Neo_BITCNT_Start();
Há um cálculo constante que deve ser carregado no registro D1 (lembro que ele define a duração de cada um dos terceiros bits):
unsigned char fCyclesOn = (unsigned char)(0.35/(1.0/(fSpeedMHz))); CY_SET_REG8(NP_Neo_DPTH_D1_PTR, fCyclesOn+1);
A configuração de um bloco DMA ocupa a maior parte dessa função. O buffer é usado como fonte e o FIFO0 do bloco UDB é usado como receptor (NP_Neo_DPTH_F0_PTR no registro do quilômetro). O autor fez parte dessa configuração na função de transferência de dados. Mas, na minha opinião, fazer todos os cálculos para cada transmissão é um desperdício demais. Especialmente quando você considera que uma das ações dentro da função parece muito, muito volumosa.
A segunda função no contexto da primeira é o topo do laconicismo. Só que o primeiro é chamado no estágio de inicialização, quando os requisitos de desempenho são bastante gratuitos. Durante a operação, é melhor não desperdiçar ciclos do processador em algo supérfluo:
void NP_Update() { if(NP_g_pFrameBuffer) { CyDmaChEnable(NP_g_nDMA_Chan, 1); } }
Claramente, não há funcionalidade suficiente para trabalhar com vários buffers (para fornecer buffer duplo), mas, em geral, uma discussão sobre a funcionalidade da API está além do escopo do artigo. Agora, o principal é mostrar como adicionar suporte de software ao firmware desenvolvido. Agora sabemos como fazê-lo.
Geração de projeto
Então, toda a parte do firmware está pronta, a API é adicionada, o que fazer a seguir? Selecione o item de menu
Build-> Generate Application .

Se tudo der certo, você pode abrir a guia
Resultados e ver o arquivo com a extensão
rpt .

Ele mostra quantos recursos do sistema foram usados na implementação do firmware.


Quando comparo os resultados com os do projeto original, minha alma fica mais quente.
Agora vá para a guia
Origem e comece a trabalhar com a parte do software. Mas isso já é trivial e não requer explicações especiais.

Conclusão
Espero que, a partir deste exemplo, os leitores tenham aprendido algo novo e interessante sobre o trabalho prático com blocos UDB. Tentei me concentrar em uma tarefa específica (controle de LED), bem como na metodologia de design, pois precisava compreender alguns aspectos que eram óbvios para os especialistas. Eu tentei marcá-los enquanto as memórias da missão são frescas. Quanto ao problema resolvido, os diagramas de tempo para mim não eram tão ideais quanto os do autor do desenvolvimento original, mas se encaixavam perfeitamente nas tolerâncias definidas na documentação para os LEDs, e os recursos do sistema eram significativamente menores.
De fato, isso é apenas parte das informações não padrão encontradas. Em particular, na maioria dos materiais, pode parecer que o UDB funcione bem apenas com dados seriais, mas não é assim. Nota de aplicação encontrada, que mostra brevemente como você pode conduzir e dados paralelos. Poderíamos considerar exemplos específicos com base nessas informações (no entanto, não será possível ofuscar o FX2LP, outro controlador do Cypress: o PSoC tem uma velocidade de barramento USB mais baixa).
Minha cabeça está girando idéias sobre como resolver o problema de "piscar" uma impressora 3D, que há muito me atormenta. Lá, interrupções no uso de motores de passo consomem apenas uma porcentagem insana do tempo da CPU. Em geral, falei muito sobre interrupções e tempo do processador em um
artigo sobre o RTOS MAX . Existem estimativas de que, para a manutenção de motores de passo, é possível levar todas as cabanas temporárias completamente para o UDB, deixando ao processador uma tarefa puramente computacional, sem medo de que ele não tenha tempo para fazer isso em um intervalo de tempo dedicado.
Mas essas coisas só podem ser fundamentadas se o tópico for interessante.