Gráficos 3D na STM32F103

imagem

Uma breve história sobre como empurrar os elementos não editáveis ​​e exibir gráficos tridimensionais em tempo real usando um controlador que não possui velocidade nem memória para isso.

Em 2017 (a julgar pela data de modificação do arquivo), decidi mudar dos controladores AVR para STM32s mais poderosos. Naturalmente, o primeiro controlador foi o amplamente divulgado F103. Não é menos natural que o uso de placas de depuração prontas para uso tenha sido rejeitado em favor da fabricação de uma a partir do zero, de acordo com seus requisitos. Curiosamente, quase não havia batentes (exceto que o UART1 deveria ser colocado em um conector normal e não na muleta).

Comparado com o AVR, as características da pedra são bastante decentes: clock de 72 MHz (na prática, você pode fazer overclock para 100 MHz ou mais, mas apenas por seu próprio risco e risco!), 20 kB de RAM e 64 kB de flash. Além disso, uma tonelada de periféricos, ao usar o principal problema para não ter medo dessa abundância e perceber que você não precisa colocar todos os dez registros para iniciar, basta definir três bits nos corretos. Pelo menos até você querer algo estranho.

Quando a primeira euforia da posse de tal poder passou, surgiu um desejo de sondar seus limites. Como um exemplo eficaz, escolhi o cálculo de gráficos tridimensionais com todas essas matrizes, iluminação, modelos poligonais e um buffer Z com uma tela de 320x240 no controlador ili9341. Os dois problemas mais óbvios a serem resolvidos são velocidade e volume. Um tamanho de tela de 320x240 a 16 bits por cor fornece 150 kB por quadro. Mas a RAM total que temos é de apenas 20 kB ... E esses 150 kB devem ser transferidos para a tela pelo menos 10 vezes por segundo, ou seja, a taxa de câmbio deve ser de pelo menos 1,5 MB / s ou 12 MB / s, o que já parece uma carga significativa no núcleo. Felizmente, neste controlador, existe um módulo RAP (acesso direto à memória, também conhecido como Direct Memory Access, DMA), que permite que você não carregue o kernel com operações de transfusão de vazias para vazias.Ou seja, você pode preparar o buffer, dizer ao módulo "aqui você tem o buffer de dados, trabalho!", E agora preparar os dados para a próxima transferência. E, levando em consideração a capacidade do monitor de receber dados em um fluxo, surge o seguinte algoritmo: o buffer frontal é destacado, do qual o DMA transfere dados para o monitor, o buffer traseiro no qual a renderização ocorre e o buffer Z usado para cortar em profundidade. Buffers são uma única linha (ou coluna, qualquer que seja) da exibição. E, em vez de 150 kB, precisamos apenas de 1920 bytes (320 pixels por linha * 3 buffers * 2 bytes por ponto), que se encaixam perfeitamente na memória. O segundo hack é baseado no fato de que o cálculo de matrizes de transformação e coordenadas de vértice não pode ser realizado para cada linha, caso contrário, a imagem será distorcida das maneiras mais bizarras e é desvantajosa em velocidade. Em vez disso, cálculos "externos",isto é, a multiplicação de matrizes de transformação e sua aplicação aos vértices são recalculadas em cada quadro e depois convertidas em uma representação intermediária, otimizada para renderizar em uma imagem de 320x1.

Por motivos de hooligan, a biblioteca se parecerá com o OpenGL do lado de fora. Como no OpenGL original, a renderização começa com a formação da matriz de transformação - a limpeza glLoadIdentity () cria a unidade da matriz atual e, em seguida, um conjunto de transformações glRotateXY (...), glTranslate (...), cada uma das quais é multiplicada pela matriz atual. Como esses cálculos serão realizados apenas uma vez por quadro, não há requisitos especiais de velocidade, é possível fazer com flutuadores simples, sem perversões com números de pontos fixos. A matriz em si é uma matriz de flutuação [4] [4], mapeada para uma matriz unidimensional de flutuação [16] - na verdade, esse método é geralmente usado para matrizes dinâmicas, mas você também pode obter um pequeno benefício com matrizes estáticas. Outro truque padrão: em vez de calcular constantemente senos e cossenos, que são muitos nas matrizes de rotação,conte-os com antecedência e escreva-os no tablet. Para fazer isso, divida o círculo completo em 256 partes, calcule o valor senoidal de cada uma e despeje-o na matriz sin_table []. Bem, qualquer um da escola pode obter o cosseno pelo seno. Vale ressaltar que as funções de rotação assumem um ângulo não em radianos, mas em frações de uma revolução completa, após redução para a faixa [0 ... 255]. No entanto, foram implementadas funções "honestas" que realizam a conversão do canto para o lóbulo sob o capô.realizando a conversão do ângulo para os lobos sob o capô.realizando a conversão do ângulo para os lobos sob o capô.

Quando a matriz estiver pronta, você poderá começar a desenhar as primitivas. Em geral, em gráficos tridimensionais, existem três tipos de primitivas - um ponto, uma linha e um triângulo. Mas se estivermos interessados ​​em modelos poligonais, a atenção deve ser prestada apenas ao triângulo. Sua "renderização" ocorre na função glDrawTriangle () ou glDrawTriangleV (). A palavra "renderização" é colocada entre aspas porque nenhuma renderização ocorre neste estágio. Apenas multiplicamos todos os pontos do primitivo pela matriz de transformação e extraímos deles as fórmulas analíticas das arestas y = ky * x + por, que nos permitem encontrar as interseções das três arestas do triângulo com a linha de saída atual. Descartamos um deles, pois ele não se encontra no intervalo entre os vértices, mas em sua continuação.Ou seja, para desenhar um quadro, você só precisa passar por todas as linhas e, para cada uma, pintar a área entre os pontos de interseção. Mas se você aplicar esse algoritmo “de frente”, cada primitivo se sobrepõe aos que foram desenhados anteriormente. Precisamos considerar a coordenada Z (profundidade) para que os triângulos se cruzem maravilhosamente. Em vez de simplesmente imprimir ponto a ponto, consideraremos sua coordenada Z e, em comparação com a coordenada Z armazenada no buffer de profundidade, a saída (atualização do buffer Z) ou a ignoramos. E para calcular a coordenada Z de cada ponto da linha de seu interesse, usamos a mesma fórmula de linha reta z = kz * y + bz calculada pelos mesmos dois pontos de interseção com arestas. Como resultado, o objeto da estrutura glTriangle do triângulo “semi-acabado” consiste em três coordenadas X dos vértices (não há sentido em armazenar as coordenadas Y e Z, elas serão calculadas) e k,b coeficientes diretos, bem, cor para a pilha. Aqui, ao contrário do cálculo das matrizes de transformação, a velocidade é crítica, por isso já usamos números de ponto fixo. Além disso, se para o termo b, a mesma precisão é suficiente para as coordenadas (2 bytes), então a precisão do fator k, quanto maior, melhor, então usamos 4 bytes. Mas não é um float, já que trabalhar com números inteiros ainda é mais rápido, mesmo com o mesmo tamanho.

Então, chamando um monte de glDrawTriangle (), preparamos uma matriz de triângulos semi-acabados. Na minha implementação, os triângulos são deduzidos um de cada vez por chamadas de função explícitas. De fato, seria lógico ter uma matriz de triângulos com os endereços dos vértices, mas aqui decidi não complicar. De qualquer forma, a função de renderização é escrita por robôs, e não importa para eles preencher uma matriz constante ou escrever trezentas chamadas idênticas. É hora de traduzir os produtos semi-acabados dos triângulos em uma bela imagem na tela. Para fazer isso, a função glSwapBuffers () é chamada. Como descrito acima, ele percorre as linhas da tela, pesquisa cada ponto de interseção com todos os triângulos e desenha segmentos de acordo com a filtragem por profundidade. Após renderizar cada linha, você precisa enviá-la para a tela. Para fazer isso, o DMA é iniciado, o que indica o endereço da string e seu tamanho.Enquanto isso, o DMA funciona, você pode alternar para outro buffer e renderizar a próxima linha. O principal é não esquecer de esperar o final da transferência se você de repente terminar de renderizar mais cedo. Para visualizar a proporção de velocidades, adicionei a inclusão de um LED vermelho após o final da renderização e desligado após a conclusão da espera do DMA. Acontece algo como PWM, que ajusta o brilho dependendo da latência. Teoricamente, em vez de uma espera "burra", as interrupções do DMA poderiam ser usadas, mas eu não as poderia usar, e o algoritmo se tornaria muito mais complicado. Para um programa de demonstração, isso é redundante.Para visualizar a proporção de velocidades, adicionei a inclusão de um LED vermelho após o final da renderização e desligado após a conclusão da espera do DMA. Acontece algo como PWM, que ajusta o brilho dependendo da latência. Teoricamente, em vez de uma espera "burra", as interrupções do DMA poderiam ser usadas, mas eu não as poderia usar, e o algoritmo se tornaria muito mais complicado. Para um programa de demonstração, isso é redundante.Para visualizar a proporção de velocidades, adicionei a inclusão de um LED vermelho após o final da renderização e desligado após a conclusão da espera do DMA. Acontece algo como PWM, que ajusta o brilho dependendo da latência. Teoricamente, em vez de uma espera "burra", as interrupções do DMA poderiam ser usadas, mas eu não as poderia usar, e o algoritmo se tornaria muito mais complicado. Para um programa de demonstração, isso é redundante.

O resultado dos procedimentos acima foi uma imagem rotativa de três planos que se cruzam de cores diferentes e com uma velocidade bastante decente: o brilho do LED vermelho é bastante alto, o que indica uma grande margem no desempenho do kernel.

Bem, se o núcleo estiver ocioso, você precisará carregá-lo. E vamos carregá-lo com melhores modelos. No entanto, não esqueça que a memória ainda é muito limitada, portanto o controlador não puxará muitos polígonos fisicamente. O cálculo mais simples mostrou que, após subtrair a memória no buffer de linha e similares, havia um lugar para 378 triângulos. Como a prática demonstrou, os modelos do jogo gótico antigo, mas interessante, são perfeitos para esse tamanho. Na verdade, os modelos de uma cobra e uma mosca de sangue foram retirados de lá (e já na época em que escrevemos este artigo e um glocoor exibindo o KDPV), após o que o controlador ficou sem memória flash. Mas os modelos de jogos não se destinam ao uso por um microcontrolador.

Digamos que eles contenham animação, texturas e similares, o que não é útil para nós e não cabe na memória. Felizmente, o blender permite não apenas salvá-los em * .obj, o que é mais passível de análise, mas também reduzir o número de polígonos, se necessário. Além disso, com a ajuda de um programa auto-escrito simples obj2arr * .obj, os arquivos são classificados em coordenadas, a partir das quais um arquivo * .h é formado posteriormente para inclusão direta no firmware.

Mas, por enquanto, os modelos parecem apenas borrões encaracolados. No modelo de teste, isso não nos incomodou, pois todas as faces foram pintadas com suas próprias cores, mas não prescrevem as mesmas cores para cada polígono do modelo. Não, é claro que você pode pintar uma mosca em cores aleatórias, mas parecerá muito inesperado, verifiquei. Especialmente quando as cores também mudam em cada quadro ... Em vez disso, aplique outra gota de magia vetorial e adicione iluminação.

O cálculo da iluminação em sua versão primitiva consiste em calcular o produto escalar do normal e na direção da fonte de luz, seguido pela multiplicação pela cor "nativa" da face.
Agora temos três modelos - dois do jogo e um teste, a partir do qual começamos. Para trocá-los, usaremos um dos dois botões soldados na placa. Ao mesmo tempo, você pode adicionar controle sobre o processador. Já temos um controle - um LED vermelho associado à latência do DMA. E o segundo LED verde piscará a cada atualização de quadro - para que possamos estimar a taxa de quadros. A olho nu, eram cerca de 15 qps.


Em geral, estou satisfeito com o resultado: é bom implementar algo que é fundamentalmente impossível de resolver de frente. Obviamente, ainda há muito para otimizar e melhorar, mas há pouco sentido nisso. Objetivamente, o controlador para gráficos tridimensionais é fraco, e não se trata apenas de velocidade, mas de RAM. No entanto, como qualquer amostra de demoscene, esse projeto é valioso não pelo resultado, mas pelo processo.

Se alguém está subitamente interessado, o código fonte está disponível aqui .

All Articles