... ou como inventei minhas próprias bicicletas com preferências e gueixas ao meu gosto - escrevi o firmware para uma impressora de fotopolímero do zero. No momento, o firmware já está totalmente funcional.
A placa MKS DLP vendida no Aliexpress foi tomada como base, para a qual o fabricante fornece códigos fonte de circuito e firmware, que rejeitei em favor de escrever tudo do zero.
O artigo acabou sendo muito longo, então decidi dividi-lo em duas partes. Nesta parte, haverá um histórico e a descrição de uma GUI caseira para uma tela de toque. No final, haverá links para o próprio assunto do bullying e para os repositórios do GitHub.
- Parte 1: 1. Interface do usuário.
- Parte 2: 2. Trabalho com o sistema de arquivos em uma unidade flash USB. 3. Controle do motor de passo para o movimento da plataforma.
- Parte 3: 4. Exibindo imagens de camadas na tela de luz de fundo. 5. Cada pequena coisa, como controlar a iluminação e os ventiladores, carregar e salvar as configurações, etc. 6. Recursos adicionais para conforto e conveniência.
Para um melhor entendimento, darei uma breve descrição do trabalho das impressoras 3D LCD de fotopolímero para quem não está familiarizado com elas:
Uma breve explicação de como a maioria das impressoras de fotopolímero para "consumidores" funcionam
— LCD- ( , 5.5" 25601440 ( — 47.25 ). 405 . , FEP-. , «» . , -. , «» , . . , , . , . , .
fundo
Como cheguei a isso e por que comecei a escrever meu próprio firmware em vez de apenas ajustar o código-fonte do fabricante para mim.
A história de fundo acabou sendo longa, então eu removi sob o spoiler
5 3D-. , , . FDM-, — Anet A8. - , , . - — - , , . , . - — Anycubic Photon S. , .
, , «» — , . , .., FDM-. , , — 11565 , :) «» , , , . . «» — . , , 20-30 . , — , . .
. , , .. , , , , , . . , (), . , , . - . , 3D- — MKS DLP. : , (5.5", 25601440) (3.5", 480320). — ! , , .
, , . , - , , . . , . … -, CMSIS HAL ST ( STM32F407). -, Marlin 3D. — Marlin 3D — FDM 3D-. 6 , , , G- - . 3 . . — G- . , . , FDM- .
, GUI- , . , , - .
, , «» — , . , .., FDM-. , , — 11565 , :) «» , , , . . «» — . , , 20-30 . , — , . .
. , , .. , , , , , . . , (), . , , . - . , 3D- — MKS DLP. : , (5.5", 25601440) (3.5", 480320). — ! , , .
, , . , - , , . . , . … -, CMSIS HAL ST ( STM32F407). -, Marlin 3D. — Marlin 3D — FDM 3D-. 6 , , , G- - . 3 . . — G- . , . , FDM- .
, GUI- , . , , - .
Então, o que temos:
- Kit MKS DLP, que inclui: placa-mãe, display de interface 480x320 de 3,5 "e display backlight de 5,5" 2560x1440
- fontes nativas do fabricante
- diagrama da placa-mãe (sem nomes de valores ativos e nominais de componentes passivos)
A placa-mãe é baseada no microcontrolador STM32F407. Para controlar a exibição da luz de fundo , a placa contém um FPGA do fabricante chinês GW1N-LV4LQ144ES, SDRAM e dois chips de interface SSD2828 MIPI. O microcontrolador direciona a imagem da camada para o FPGA, o FPGA a armazena na SDRAM e a partir daí atualiza a tela através do SSD2828. O fabricante, aliás, não disponibiliza a configuração do FPGA (firmware) no código fonte: (Além disso, a placa-mãe possui:
- entrada de energia 12-24 volts
- USB A /
- A4988
- Z —
- WiFi
- FLASH- W25Q64
- EEPROM- AT24C16
O display de interface com um painel de toque resistivo é conectado com um cabo plano de 40 pinos. Controlador de tela - ILI9488, controlador de painel sensível ao toque - HR2046 (semelhante ao TSC2046).
Para inicializar os periféricos, usei o programa STM32CUBE MX. Mas não usei o resultado obtido diretamente, mas inseri as peças necessárias em minhas fontes. Ao trabalhar com periféricos, usei as bibliotecas HAL do ST e, onde precisava obter a velocidade máxima, trabalhei com registradores diretamente.
Portanto, há uma tarefa - este kit deve ser capaz de imprimir arquivos de uma unidade flash com facilidade para o usuário. Dividi todo este problema aproximadamente em partes principais, o que resultou em três publicações.
Eu quero avisá-lo imediatamente que nem meu código nem minha abordagem fingem ser ideais ou apenas bons, eu jogo como posso. Para mim, programar é mais um hobby do que uma profissão. Portanto, por favor, não julgue por padrões muito rígidos.
1. Interface do usuário
Primeiro foi a inicialização do display. Não há nada de interessante aqui, a sequência padrão para o controlador ILI9488. Eu o arranquei de fontes nativas, cortando o código de inicialização para outros tipos de monitores (que, provavelmente, permaneceram lá desde a vida FDM dessas fontes). Então eu entrei em fontes.
1.1 Fontes
Existem muitas bibliotecas de fontes para microcontroladores na rede, mas a grande maioria delas funciona com fontes monoespaçadas, e eu realmente não gosto disso. É quando todos os caracteres têm a mesma largura, como a letra "z", que a letra "i". Certa vez, escrevi uma biblioteca de fontes proporcionais para um de meus projetos favoritos. Ele usa dois arrays para cada fonte - um array com os dados de bits dos próprios caracteres e um array com a largura de cada caractere. E uma pequena estrutura com parâmetros de fonte - ponteiros para matrizes, altura da fonte, número de caracteres na fonte:
typedef struct
{
uint16_t *width;
uint8_t *data;
uint8_t height;
uint16_t symcount;
} LCDUI_FONT;
Parece que tal organização de fontes deve ocupar mais espaço de memória do que apenas um bitmap monoespaçado, mas isso não é totalmente verdade. Primeiro, o próprio monoespaço dá origem a um excedente de dados armazenados. Por exemplo, se em uma fonte de 8 pixels de altura e 5 pixels de largura, 1 byte (1 bit de largura e 8 bits de altura) seria suficiente para a letra "i", então ainda levará 5 bytes de dados (5 bits de largura e 8 bits de altura), Desde a a largura é fixa. Em segundo lugar, como regra, nessas fontes, o alinhamento é feito nos limites de byte de cada linha ou de cada coluna, dependendo de como os dados são organizados.
Por exemplo, use a mesma fonte 5x8. Se os dados de bit forem armazenados linha por linha, haverá um excesso de 3 bits para cada linha. Ou 3 bytes por caractere:
Ou uma fonte 7x12 com armazenamento de dados em colunas, então há um excesso de dados de 4 bits por coluna ou 3,5 bytes por caractere:
Na minha biblioteca, os dados de bits são contínuos para um caractere e o alinhamento no limite do byte fica apenas no final do caractere.
Além disso, há mais um pequeno truque que permite reduzir um pouco o tamanho da fonte armazenada: um caractere pode não ter dados de bits, mas se referir a outro caractere com o mesmo estilo. Por exemplo, as letras cirílicas "A", "B", "E", "K", etc. pode ter uma referência a letras latinas com o mesmo estilo. Isso é feito especificando um valor negativo para a largura do caractere correspondente na matriz de larguras de caracteres. Se houver um valor negativo aí, a imagem desse caractere é tirada do caractere na posição (largura * -1).
Este é o procedimento para localizar um caractere em uma matriz:
uint8_t* _lcdui_GetCharData(char c)
{
if (c < 32)
return 0;
if (c > 126)
c -= 65;
c -= 32;
if (c >= lcdui_current_font->symcount)
return 0;
uint16_t c1 = lcdui_current_font->width[c];
if (c1 & 0x8000)
c = (c1 & 0x7FFF);
uint16_t ch = lcdui_current_font->height;
int32_t i = 0, ptr = 0, bits = 0, line_bits = ch;
for (i = 0; i < c; i++)
{
if (lcdui_current_font->width[i] & 0x8000)
continue;
bits = lcdui_current_font->width[i] * line_bits;
ptr += bits >> 3;
if (bits & 0x07)
ptr++;
}
return &(lcdui_current_font->data[ptr]);
}
Tudo isso geralmente dá até um ganho na quantidade de dados para a fonte. Sem mencionar que o tipo proporcional parece mais natural.
A velocidade de renderização de tal fonte é bastante decente devido à saída em janela - o monitor recebe primeiro o comando para limitar a janela de saída ao tamanho do caractere na posição desejada e, em seguida, os dados de todo o caractere são despejados em um fluxo. Não há necessidade de definir as coordenadas separadamente para cada pixel.
Por exemplo, na foto abaixo, o texto azul e a linha branca superior foram renderizados pela minha biblioteca, e a parte inferior branca - pela biblioteca semelhante a um Arduino padrão das fontes nativas: O
texto azul foi renderizado várias vezes mais rápido do que a linha branca inferior.
Ao mesmo tempo, tive que inventar um utilitário para criar matrizes de fontes prontas para serem usadas em um programa a partir de uma imagem. No Photoshop, uma imagem da altura desejada é criada com todos os caracteres da fonte, então as coordenadas X da última coluna de cada caractere são inseridas no arquivo de texto à mão, e então o utilitário é definido na imagem e neste arquivo de texto. Isso cria um arquivo .c com os arrays necessários. Um pouco tedioso, é claro, mas simples.
O procedimento de exibição de texto é capaz de quebrar o texto em uma nova linha no final da tela ou por um caractere de alimentação de linha encontrado, pode alinhar à esquerda, direita e centro, limitar a área além da qual o texto não irá (será cortado). E é capaz de exibir símbolos com pintura de fundo com cor de fundo ou com preservação de fundo. A segunda opção funciona mais lentamente, uma vez que não é mais possível preencher os dados do caractere na exibição em um fluxo, mas ainda é rápido o suficiente para que a saída de 3-4 linhas não seja visível a olho nu.
1.2 Exibindo imagens de interface
Para a interface do usuário, você precisará exibir imagens - plano de fundo, ícones, botões. A princípio, decidi não me preocupar muito e armazenar todas as imagens no formato .bmp na memória flash de 8 MB disponível na placa. E até escrevi um procedimento para isso. O arquivo é salvo no formato de 16 bits (R5 G6 B5) com ordem de linha ponta a ponta ou ponta a ponta, e já pode ser alimentado diretamente para a rotina de renderização. Mas o tamanho de uma imagem de plano de fundo 480x320 é mais de 300 KB. Considerando que parte dessa memória flash será dedicada a atualizações de firmware, 30 imagens de fundo ocuparão toda a memória. Parece ser muito, mas ainda menos do que eu gostaria de ter, só para garantir. Mas também deve haver botões, ícones, etc. Portanto, decidiu-se converter as imagens para algum tipo de formato compactado.
Não há muitas opções de compactação - todos os algoritmos que compactam imagens mais ou menos bem requerem RAM decente (pelos padrões de um microcontrolador) ou uma quantidade razoável de tempo para descompactar. As imagens, por outro lado, devem ser exibidas, abrindo rapidamente, e é desejável que a imagem quando exibida não se pareça com uma barra de progresso rastejante. Portanto, decidi pela compressão RLE - 1 byte codifica o número de repetições e os dois seguintes - a cor. Para isso, também foi criado um utilitário que converte arquivos .bmp em imagens compactadas desta forma. O cabeçalho consiste em apenas 4 bytes - 2 bytes para a largura e altura da imagem. Em média, as imagens de fundo são comprimidas desta forma em 5-7 vezes, dependendo fortemente do tamanho das áreas monocromáticas (o que é esperado). Por exemplo, uma imagem como esta encolheu dos 307 KB originais para 74 KB:
Mas este - até 23 KB do mesmo 307:
A propósito, o meu designer é ainda mais porcaria do que o programador ...
Fiquei satisfeito com este resultado. A decodificação e exibição de imagens são muito rápidas - cerca de 40 milissegundos por imagem de fundo completa. Então eu escolhi esta opção.
E, a propósito, mudar para o modo DMA para enviar dados para o display não deu quase nenhuma aceleração de saída. A tela é conectada através de um barramento de dados externo de 16 bits como uma memória externa, mas seus tempos são um tanto tristes, o que quase nega as vantagens da saída DMA sobre a saída manual pixel a pixel.
1.3 Estrutura da GUI
Textos são exibidos, imagens são desenhadas, agora é hora de pensar em como a base da interface do usuário será organizada.
Com o painel de toque, tudo é simples - o microcontrolador pesquisa constantemente o controlador do painel de toque quanto a interrupções e calcula a média dos últimos 4 resultados obtidos, traduzindo-os em coordenadas de exibição. Assim, o estado do sensor é conhecido a qualquer momento - se está pressionado ou não, e se for pressionado, em que lugar. Outra camada entre o painel de toque e a parte principal do programa é o procedimento de processamento de cliques de botões, que há bastante tempo vaga de projeto em projeto com pequenas adaptações para condições específicas.
Aqui está um breve resumo de como funciona.
«». (100-150 ). , «». , . , , «», . , «», «». «», «». - «» «», . ( «»), - . , , .
O painel de toque serve como único botão da interface, só que além do simples fato de pressioná-lo, as coordenadas do clique também são analisadas.
Agora tudo precisa ser feito para que uma variedade de elementos de interface possam ser exibidos na tela, que podem ou não responder a cliques, atualizar por eventos, ter tamanhos e imagens diferentes, etc.
Por fim, cheguei a este esquema: a interface consiste em dois tipos principais de elementos - telas e botões.
Uma tela é uma espécie de contêiner de tela inteira para botões. A tela possui as seguintes propriedades:
- imagem de fundo
- cor de fundo
- maneira de desenhar o fundo - preenchendo com uma cor de fundo ou exibindo uma imagem
- texto do cabeçalho
- cor do texto do título
- fonte do texto do cabeçalho
- um ponteiro para a tela principal (para a qual retornar ao fechar)
- uma série de ponteiros para botões
- ponteiro para um procedimento de evento (chamado periodicamente no loop do programa principal)
- ponteiro para a rotina de desenho de tela
Estrutura da tela
typedef struct
{
void *addparameter;
char *bgimagename;
void *prevscreen;
LNG_STRING_ID name;
TG_RECT nameposition;
TG_TEXTOPTIONS nameoptions;
uint8_t btns_count;
TG_BUTTON *buttons;
LCDUI_FONT_TYPE font;
LCDUI_FONT_TYPE namefont;
uint16_t textcolor;
uint16_t nametextcolor;
uint16_t backcolor;
struct {
paintfunc _callpaint; // repaint screen
processfunc _process; // screen process handling (check for changes, touch pressed, etc)
} funcs;
} TG_SCREEN;
Os botões, na verdade, podem ser não apenas botões, mas também texto, um ícone, algum tipo de elemento que muda, como um contador ou um relógio. Acabou sendo conveniente combinar tudo em um tipo e definir o comportamento de cada botão específico por meio de suas propriedades.
Propriedades do botão:
- coordenadas na tela
- cor de fundo
- imagem de fundo para estado livre
- imagem de fundo para o estado pressionado
- imagem de fundo para estado desativado
- imagem de fundo para o estado ativo (para o elemento ativo de um grupo de botões de opção, por exemplo)
- método de renderização - imagem ou cor de fundo
- Se deve redesenhar o botão quando pressionado e solto
- botão de texto
- ( )
- (, )
- ( )
- ,
- ,
typedef struct
{
void *addparameter;
uint8_t button_id;
int8_t group_id; // for swithed options buttons, >0 - single selection from group (select), <0 - multiple selection (switch)
TG_RECT position;
void *parentscreen;
void *childscreen;
char *bgimagename_en;
char *bgimagename_press;
char *bgimagename_dis;
char *bgimagename_act; // for swithed options buttons
LNG_STRING_ID text;
TG_RECT textposition;
LCDUI_FONT_TYPE font;
uint16_t textcolor_en;
uint16_t textcolor_press;
uint16_t textcolor_dis;
uint16_t textcolor_act; // for swithed options buttons
uint16_t backcolor_en;
uint16_t backcolor_press;
uint16_t backcolor_dis;
uint16_t backcolor_act; // for swithed options buttons
struct {
uint8_t active:1; // for swithed options buttons
uint8_t needrepaint:1;
uint8_t pressed:1;
uint8_t disabled:1;
uint8_t repaintonpress:1; // repaint or not when pressed - for indicate pressed state
BGPAINT_TYPE bgpaint:2;
} options;
TG_TEXTOPTIONS textoptions;
struct {
paintfunc _call_paint; // repaint button
pressfunc _call_press; // touch events handling
pressfunc _call_longpress; // touch events handling
processfunc _call_process; // periodical processing (for example text value refresh)
} funcs;
} TG_BUTTON;
Com a ajuda desse conjunto de propriedades, tornou-se possível criar quase tudo na interface com base em tal elemento. Se uma tela ou um botão tiver um ponteiro para qualquer um dos procedimentos zero, o procedimento padrão correspondente é chamado. Em vez de um ponteiro de procedimento ao pressionar um botão, por exemplo, pode haver um identificador especial indicando que você precisa ir para a tela secundária ou anterior, então o procedimento padrão fará isso. Em geral, os procedimentos padrão cobrem quase todos os casos de uso de botões comuns e você deve criar seus próprios procedimentos para um botão apenas em casos não padrão - por exemplo, quando um botão funciona como um relógio ou como um elemento de uma lista de arquivos.
Mas o que faltava nos recursos desse esquema era para janelas modais com mensagens ou perguntas (como MessageBox na API do Windows), então fiz um tipo separado de telas para elas. Sem imagens de fundo e um tamanho determinado pelo título ou pela própria mensagem. Essas mensagens podem ser criadas em quatro versões - com botões "Sim / Não", com botões "OK / Cancelar", com um botão "OK" ou sem botões (como "Espere, os dados estão carregando ...").
Estrutura da janela de mensagem
typedef struct
{
MSGBOXTYPE type;
void *prevscreen;
char caption[128];
char text[512];
TG_RECT boxpos;
uint8_t btns_count;
TG_BUTTON buttons[TG_BTN_CNT_MSGBOX];
uint16_t caption_height;
LCDUI_FONT_TYPE font_caption;
LCDUI_FONT_TYPE font_text;
uint16_t text_color;
uint16_t box_backcolor;
uint16_t capt_textcolor;
uint16_t capt_backcolor;
} TG_MSGBOX;
Foi com base nesses três tipos que toda a interface foi construída, ela se revelou bastante flexível. Agora, a inicialização de todos os elementos é realizada de forma rígida no firmware, mas há uma ideia para dar aos usuários a oportunidade de criar sua própria interface, descrevendo as propriedades de todos os elementos no arquivo de configuração e adicionando uma série de imagens necessárias. Em teoria, será possível alterar o conteúdo das diferentes telas - quais botões colocar na tela principal, quais botões na tela de serviço etc.
1.4 Multilanguage
O multilinguismo estava nas tarefas inicialmente. Mas no começo eu fui pelo caminho estúpido - ao inicializar todos os elementos, eu atribuí a eles textos da tabela de idiomas que era a atual. Mudar de idioma significou reinicializar todos os elementos de texto, e quando havia mais de duas telas na interface e mais de 20 botões e rótulos, percebi que era impossível viver assim por mais tempo. Em seguida, ele fez todas as referências aos textos por meio do procedimento. O procedimento recebe um identificador de texto como parâmetro e retorna um ponteiro para o texto no idioma atual:
char *mshortname = LANG_GetString(LSTR_SHORT_JANUARY);
Ao alterar o idioma, o ponteiro simplesmente muda de uma matriz de textos no idioma antigo para uma matriz com textos no novo idioma:
void LANG_SetLanguage(uint8_t lang)
{
lngCurrent = lngLanguages[lang].strings;
return;
}
Todos os textos fonte estão em codificação UTF-8. Eu também tive que mexer nessas codificações. Textos - em UTF-8, arquivos cirílicos - em Unicode-16, algumas strings - em ANSI regular. Eu não queria colocar no firmware um conjunto completo de bibliotecas para oferecer suporte a codificações multibyte, então várias funções foram escritas para converter de codificação em codificação e para operações com textos em codificações diferentes, por exemplo, adicionar uma string UTF-8 ao final de uma string Unicode16.
Adicionar um novo idioma agora se resumiu em criar uma tabela de textos nele e alterar o valor da constante LNG_LANGS_COUNT. É verdade que ainda há uma dúvida com relação às fontes, se o novo idioma usa símbolos diferentes do cirílico e do latim ... Agora, apóio o russo e o inglês traduzido pelo Google no código-fonte.
1.5 Armazenamento de imagens e outros recursos
Para armazenar grandes recursos, a placa possui um flash W25Q64 SPI de 8 megabytes. Inicialmente, eu queria fazer como sempre - definir um deslocamento para cada recurso dentro do flash e salvá-los lá como dados binários. Mas então percebi que os problemas com esse método são garantidos para mim assim que o número de recursos salvos ultrapassar algumas dezenas e eu quero mudar, por exemplo, alguma imagem que é salva em sexto lugar na ordem. Se seu tamanho aumentar, você terá que mudar os endereços de todos os seguintes recursos e reescrevê-los. Ou deixe um espaço livre de tamanho desconhecido após cada recurso - quem sabe como qualquer um dos recursos pode mudar. Sim, em um caixão eu vi esse barulho :) Então cuspi e organizei um sistema de arquivos neste flash.Naquela época, eu já tinha um sistema de arquivos USB baseado na biblioteca FatFS, então foi o suficiente para mim simplesmente escrever funções separadas de leitura / gravação de setor de baixo nível. Apenas uma coisa me incomodou um pouco - o tamanho do setor apagado neste microcircuito já é de 4 KB. Em primeiro lugar, isso leva ao fato de que os arquivos ocuparão espaço em porções de 4 KB (o arquivo escreveu 200 bytes - levou 4 KB de flash) e, em segundo lugar, o buffer na estrutura de cada ponteiro de arquivo consumirá os mesmos 4 KB de RAM, que em o microcontrolador não é muito - 192 KB. Alguém poderia, é claro, ser pervertido e escrever funções de baixo nível para que pudessem escrever e ler em porções menores, informando sobre o tamanho do setor, por exemplo, 512 bytes. Mas isso tornaria o Flash mais lento, deixando o tamanho do setor em 4 KB.Assim, qualquer recurso pode ser acessado simplesmente pelo nome do arquivo, o que acabou sendo muito conveniente. No momento, por exemplo, o número de recursos armazenados já ultrapassou 90. E fiz sua atualização o mais simples possível - os recursos atualizados (ou novos) são gravados em uma unidade flash USB em um determinado diretório, a unidade flash é inserida na placa, a placa é reinicializada no modo de serviço (durante ligue ou reinicie, pressione e segure o canto superior direito da tela) e copia automaticamente todos os arquivos encontrados neste diretório da unidade flash USB para o flash SPI.a placa reinicia no modo de serviço (durante a inicialização ou reinicialização, pressione e segure o canto superior direito da tela) e copia automaticamente todos os arquivos encontrados neste diretório da unidade flash USB para o flash SPI.a placa reinicia no modo de serviço (durante a inicialização ou reinicialização, pressione e segure o canto superior direito da tela) e copia automaticamente todos os arquivos encontrados neste diretório da unidade flash USB para o flash SPI.
Continua...
Talvez a parte mais volumosa saiu na interface. Se este artigo acabar sendo de interesse da comunidade, então, na segunda parte, tentarei acomodar todo o resto.
Bem, terei todo o prazer em perguntas e comentários.
- Parte 1: 1. Interface do usuário.
- Parte 2: 2. Trabalhar com o sistema de arquivos em um stick USB. 3. Controle do motor de passo para o movimento da plataforma.
- Parte 3: 4. Exibindo imagens de camadas na tela de luz de fundo. 5. Cada pequena coisa, como controlar a iluminação e os ventiladores, carregar e salvar as configurações, etc. 6. Recursos adicionais para conforto e conveniência.
Links
Kit MKS DLP no Aliexpress
Fontes de firmware originais do fabricante no GitHub
Esquemas do fabricante de duas versões da placa no GitHub
Minhas fontes no GitHub