
Nas duas partes anteriores, falei sobre como fiz uma GUI, comecei a controlar um motor de passo e a organizar o trabalho com arquivos em uma unidade flash USB.
Hoje vou escrever sobre o processo de impressão, a saída das camadas impressas para a tela de destaque e o restante, coisas não tão essenciais:
4. Saída das imagens das camadas para a tela de destaque.
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.
- Parte 1: 1. Interface do usuário.
- Parte 2: 2. Trabalhando com o sistema de arquivos em uma unidade flash USB. 3. Controle do motor de passo para o movimento da plataforma.
- 3: 4. . 5. , .. 6. .
4.
4.1 -
Como um microcontrolador, que não possui periféricos especializados, pode fazer a imagem em uma matriz de alta resolução a uma velocidade de 74 milhões de pixels por segundo (resolução 2560x1440, 20 quadros por segundo) atualizada através da interface MIPI? Resposta: usando um FPGA com um SDRAM de 16 MB conectado a ele e dois chips de interface MIPI - SSD2828. Dois microcircuitos valem a pena porque o display é logicamente dividido em duas metades, cada uma das quais é atendida por seu próprio canal separado, de modo que dois displays em um são obtidos.
A imagem para exibição é armazenada em um dos 4 bancos de SDRAM, o chip FPGA é responsável por atender o SDRAM e enviar a imagem dele para o SSD2828. FPGA gera sinais de sincronização vertical e horizontal para SSD2828 e drives
fluxo contínuo de valores de cor de pixel em 24 linhas (8R 8G 8B) em cada um dos SSD2828. A taxa de quadros é cerca de 20 Hz.
O FPGA é conectado ao microcontrolador com uma interface serial (SPI) por meio da qual o microcontrolador pode transmitir uma imagem. É transmitido em pacotes, cada um dos quais contendo uma linha da imagem (as linhas são contadas ao longo do lado curto da tela - 1440 pixels). Além desses dados, o pacote também contém o número do banco SDRAM, número da linha e checksum - CRC16. O FPGA recebe esse pacote, verifica a soma de verificação e, se tudo estiver bem, salva os dados na área SDRAM apropriada. Caso o CRC não corresponda, o FPGA coloca um sinal em um de seus pinos, também conectado ao microcontrolador, de acordo com o qual o microcontrolador entende que os dados não chegaram normalmente e pode repetir o envio. Para uma imagem completa, o microcontrolador deve enviar 2560 desses pacotes para o FPGA.
Os dados da imagem dentro do pacote são representados em formato de bit: 1 - pixel está aceso, 0 - pixel está escuro. Infelizmente, isso exclui completamente a possibilidade de organizar o desfoque em tons de cinza das bordas das camadas impressas - anti-aliasing. Para organizar essa forma de borrar, é necessário reescrever a configuração (firmware) do FPGA, para a qual ainda não estou pronto. Há muito tempo e não muito tempo que trabalho com FPGA, terei que praticamente remasterizar tudo.
Além dos pacotes de dados, o microcontrolador também pode enviar um comando de controle no qual indicar de qual banco SDRAM ler os dados para exibição e ligar / desligar a saída de imagem.
Os chips SSD2828 também são conectados ao microcontrolador via SPI. Isso é necessário para configurar seus registros quando ligados, transferi-los para o modo hibernar ou ativo.
Existem várias outras linhas entre o microcontrolador e o FPGA / SSD2828 - o sinal de reset e os sinais de seleção de chip ativo (Chip Select) para cada um dos microcircuitos.
Em geral, esse esquema de trabalho está longe de ser ideal, na minha opinião. Por exemplo, seria mais lógico conectar o FPGA ao microcontrolador por meio de uma interface de memória externa paralela, os dados seriam transferidos muito mais rápido do que via SPI com um limite de frequência de 20 MHz (quando a frequência aumenta, o FPGA para de receber dados normalmente). Além disso, o sinal de reset não está conectado à entrada física Reset do FPGA, mas como um sinal lógico normal, ou seja, o FPGA não executa um reset de hardware nele. E isso também jogou uma piada cruel, que será discutida a seguir.
Descobri tudo isso entendendo os códigos-fonte do fabricante. Eu transferi as funções de trabalhar com FPGA de seu código-fonte como está, ainda não entendi completamente como tudo funciona. Felizmente, os chineses comentaram seu código o suficiente (em chinês) para poder descobri-lo sem muita dificuldade.
4.2 Leitura de camadas de um arquivo de impressão
Ok, já descobrimos mais ou menos o resultado da imagem finalizada, agora irei falar um pouco sobre como essas imagens são extraídas de arquivos preparados para impressão. Os arquivos .pws, .photons, .photon, .cbddlp são essencialmente um grupo de imagens de camada. Este formato veio, pelo que eu sei, da empresa chinesa Chitu, que teve a ideia de fazer placas com este tipo de circuito (microcontrolador - FPGA - SDRAM - SSD2828). Suponha que você queira imprimir um modelo com uma altura de 30 mm com cada camada de 0,05 mm de espessura. O programa fatiador corta este modelo em camadas da espessura especificada e para cada uma delas forma sua imagem.
Assim, 30 / 0,05 = 600 imagens com resolução de 1440x2560 são obtidas. Essas imagens são compactadas em um arquivo de saída, o cabeçalho com todos os parâmetros é inserido lá e esse arquivo já foi enviado para a impressora. As imagens de camada têm profundidade de 1 bit e são compactadas pelo algoritmo RLE um byte de cada vez, com o bit mais significativo indicando o valor da cor e os sete bits menos significativos representando o número de repetições. Este método permite comprimir a imagem da camada de 460 KB a cerca de 30-50. A impressora lê a camada compactada, descompacta-a e a envia linha por linha para o FPGA.
O fabricante faz isso da seguinte maneira:
- — 1, 1, 0. , (1440), .
- , 1440 (180 ).
- FPGA .
Este é o método de três etapas usado pelos chineses. No final das contas, isso foi feito para que a imagem da camada pudesse ser exibida de forma reduzida no display da interface, mostrando ao usuário o que está sendo impresso. Esta imagem acaba de ser formada a partir da matriz de bytes. Embora não esteja claro o que o impediu de formá-lo imediatamente a partir dos bits decodificados. E o que impediu a formação de um bitmap para transferência para FPGA no mesmo ciclo também não está claro.
Agora uso o mesmo método, embora otimizado. Para esclarecer o que foi a otimização, preciso esclarecer mais um ponto. Os dados da linha de exibição não são uma matriz sólida de carga útil. No meio há alguns pixels extras “não funcionais” devido ao fato de que dois controladores de tela estão unidos no lado curto, e cada um deles tem 24 pixels “não funcionais” nas bordas. Assim, os dados reais transmitidos para uma linha da imagem consistem em 3 partes: dados para a primeira metade (primeiro controlador), 48 pixels "não operacionais" intermediários, dados para a segunda metade (segundo controlador).
Assim, os chineses, ao formarem a matriz de bytes dentro do loop, verificam se o final da primeira metade foi atingido, caso contrário, o valor foi escrito pelo ponteiro * p, caso contrário, pelo ponteiro * (p + 48) . Esta verificação para cada um dos 1440 valores, e mesmo a modificação do ponteiro para metade deles, claramente não contribuiu para a velocidade do loop. Eu divido este um loop em dois separados - no primeiro, a primeira metade do array é preenchida, após este loop, o ponteiro é aumentado em 48 e o segundo loop começa para a segunda metade do array. Na versão original, a camada era lida e exibida em 1,9 segundos, apenas essa modificação reduzia o tempo de leitura e saída para 1,2 segundos.
Outra mudança dizia respeito à transferência de dados para FPGA. Nas fontes originais, isso acontece por DMA, mas após o início da transferência via DMA, a função aguarda seu término e só depois começa a decodificar e formar uma nova linha da imagem. Eu removi essa expectativa para que a próxima linha seja gerada enquanto os dados da linha anterior estão sendo transferidos. Isso reduziu o tempo em mais 0,3 segundos, para 0,9 por camada. E isso quando compilar sem otimização, se você compilar com otimização total, o tempo diminui para cerca de 0,53 segundos, o que já é bastante aceitável. Desses 0,53 segundos, leva cerca de 0,22 segundos para calcular o CRC16 e cerca de 0,19 segundos para formar um bitmap de uma matriz de bytes antes da transmissão. Mas a própria transferência de todas as linhas para FPGA leva cerca de 0,4 segundos e com isso, muito provavelmente,nada pode ser feito - tudo aqui se baseia na limitação da frequência SPI máxima permitida para FPGA.
Se eu pudesse escrever a configuração FPGA sozinho, poderia dar a descompressão RLE, e isso poderia acelerar a saída da camada em uma ordem de magnitude, mas como isso é feito?
E sim, eu ia escrever sobre o batente associado ao fato de que o FPGA não é reinicializado pelo hardware em um sinal de reinicialização do microcontrolador. Então, quando eu já aprendi como exibir imagens de camadas, concluí o próprio processo de impressão, encontrei um bug incompreensível - uma vez que de 5 a 10 a impressão foi iniciada com uma tela totalmente iluminada. Vejo no depurador que as camadas são lidas corretamente, os dados são enviados ao FPGA conforme necessário, o FPGA confirma a exatidão do CRC. Ou seja, tudo funciona, e em vez de desenhar uma camada - uma tela completamente branca. Claramente, a culpa é do FPGA ou do SSD2828. Mais uma vez, verifiquei duas vezes a inicialização do SSD2828 - está tudo bem, todos os registros neles são inicializados com os valores necessários, isso pode ser visto durante a leitura de controle dos valores deles. Então eu já alcancei a placa com um osciloscópio. E descobri que, quando ocorre uma falha dessas, o FPGA não grava nenhum dado no SDRAM. NÓS sinalizamos,permitindo a escrita, fica enraizado no ponto no nível inativo. E provavelmente eu teria lutado com essa falha por um longo tempo, se não fosse por um amigo que me aconselhou a tentar dar ao FPGA um comando explícito para desligar a saída da imagem antes de reiniciar, de forma que no momento da reinicialização não haja garantia de chamadas de FPGA para SDRAM. Eu tentei e funcionou! Este bug nunca mais apareceu. No final, chegamos à conclusão de que o IP-core do controlador SDRAM dentro do FPGA não está implementado corretamente, o reset e a inicialização do controlador SDRAM não ocorrem normalmente em todos os casos. Algo impede o correto reset se neste momento os dados em SDRAM são acessados. Como isso…que aconselhou tentar antes de redefinir dar ao FPGA um comando explícito para desligar a saída de imagem, de modo que no momento da redefinição não haja garantia de chamadas do FPGA para SDRAM. Eu tentei e funcionou! Este bug nunca mais apareceu. No final, chegamos à conclusão de que o IP-core do controlador SDRAM dentro do FPGA não está implementado corretamente, o reset e a inicialização do controlador SDRAM não ocorrem normalmente em todos os casos. Algo interfere no correto reset se os dados em SDRAM forem acessados neste momento. Como isso…que aconselhou tentar antes de redefinir dar ao FPGA um comando explícito para desligar a saída de imagem, de modo que no momento da redefinição não haja garantia de chamadas do FPGA para SDRAM. Eu tentei e funcionou! Este bug nunca mais apareceu. No final, chegamos à conclusão de que o IP-core do controlador SDRAM dentro do FPGA não está implementado corretamente, o reset e a inicialização do controlador SDRAM não ocorrem normalmente em todos os casos. Algo impede o correto reset se neste momento os dados em SDRAM são acessados. Como isso…Se o IP-core do controlador SDRAM dentro do FPGA não foi implementado corretamente, a redefinição e inicialização do controlador SDRAM não funcionam normalmente em todos os casos. Algo interfere no correto reset se os dados em SDRAM forem acessados neste momento. Como isso…Se o IP-core do controlador SDRAM dentro do FPGA não foi implementado corretamente, a redefinição e inicialização do controlador SDRAM não funcionam normalmente em todos os casos. Algo impede o correto reset se neste momento os dados em SDRAM são acessados. Como isso…
4.3 Interface do usuário durante a impressão do arquivo
Depois que o usuário seleciona o arquivo e começa a imprimi-lo, a seguinte tela é exibida:

Esta é uma tela bastante comum para tais impressoras de fotopolímero.
A maior área da tela é ocupada pela imagem da camada exposta atualmente.
A exibição desta imagem é sincronizada com a luz de fundo - quando a luz de fundo é ligada, a imagem é exibida, quando a luz de fundo é desligada, a imagem é apagada. A imagem é formada como para o display UV - ao longo do lado curto da imagem. Não me apressei com os ponteiros ao longo dos deslocamentos de linha desta imagem, mas antes de exibi-la, dou ao controlador de exibição um comando para alterar a direção de saída para os dados que estão sendo despejados, ou seja, a área desta imagem acaba "virada" de lado.
Abaixo estão as informações sobre o andamento da impressão - o tempo decorrido e estimado de impressão, a camada atual e o número total de camadas, uma barra de progresso com porcentagens à direita dela. Também quero adicionar a altura atual em milímetros após o número de camadas, apenas para ser.
À direita estão os botões de pausa, configurações e interrupção. Quando você pressiona a pausa no firmware, o sinalizador de pausa é definido e o comportamento posterior depende do estado da impressora no momento. Se a plataforma descer para a próxima camada ou a exposição da camada já tiver começado, o firmware irá completar a exposição e só depois irá elevar a plataforma até a altura de pausa (que está definida nas configurações), onde irá aguardar até que o usuário clique no botão "Continuar":

A elevação da plataforma para uma pausa ocorre primeiro na velocidade especificada nos parâmetros do arquivo e, após a altura especificada nos mesmos parâmetros, a velocidade aumenta.
Quando a impressão for interrompida, aparecerá uma janela confirmando esta ação, e somente após a confirmação a impressão será interrompida e a plataforma irá subir até a altura máxima do eixo. A velocidade de levantamento, assim como durante a pausa, é variável - primeiro lentamente para destacar a camada do filme e, em seguida, aumenta ao máximo.
O botão de configurações ainda não está funcional, mas ao clicar nele, o usuário será levado a uma tela com parâmetros de impressão que podem ser alterados - tempo de exposição da camada, altura e velocidade de levantamento, etc. Agora estou terminando. Também existe uma ideia para dar a oportunidade de salvar os parâmetros alterados de volta no arquivo impresso.
5. Cada pequena coisa, como controlar a iluminação e os ventiladores, carregar e salvar as configurações, etc.
A placa possui 3 saídas MOSFET de alta potência - uma para LEDs UV e duas para ventiladores (resfriamento dos diodos de luz de fundo e resfriamento do display, por exemplo). Não há nada de interessante aqui - as saídas do microcontrolador são conectadas às portas desses transistores e controlá-los é tão fácil quanto piscar um LED. Para alta precisão do tempo de exposição, ele é ligado no ciclo principal por meio da função que define o tempo de operação:
UVLED_TimerOn(l_info.light_time * 1000);
void UVLED_TimerOn(uint32_t time)
{
uvled_timer = time;
UVLED_On();
}
E ele desliga na interrupção de milissegundos do temporizador quando o contador de luz de fundo chega a zero:
...
if (uvled_timer && uvled_timer != TIMER_DISABLE)
{
uvled_timer--;
if (uvled_timer == 0)
UVLED_Off();
}
...
5.1 Configurações, carregar do arquivo e salvar na EEPROM
As configurações são armazenadas na EEPROM on-board at24c16. Aqui, em contraste com o armazenamento de recursos em uma grande memória flash, tudo é simples - para cada tipo de dados armazenados, o deslocamento de endereço dentro da EEPROM é codificado. No total, ele armazena três blocos: configurações do eixo Z, configurações gerais do sistema (idioma, som, etc.) e contadores de tempo para os principais componentes da impressora - iluminação, visor e ventilador.
As estruturas de bloco armazenadas contêm a versão atual do firmware e uma soma de verificação primitiva - apenas a soma de 16 bits dos valores de todos os bytes no bloco. Ao ler as configurações da EPROM, o CRC é verificado e se não corresponder ao real, então os parâmetros deste bloco são atribuídos a valores padrão, um novo CRC é calculado e o bloco é salvo na EPROM ao invés do antigo. Se o bloco de leitura não corresponder à versão atual, ele deve ser atualizado para a versão atual e será salvo em um novo formulário em vez do antigo. Isso ainda não foi implementado, mas será feito no futuro para atualizar adequadamente o firmware.
Algumas configurações podem ser alteradas por meio da interface, mas a maioria só pode ser alterada carregando um arquivo de configuração. Aqui eu não mudei meus hábitos e escrevi meu próprio analisador para esses arquivos.
A estrutura de tal arquivo é padrão: nome do parâmetro + sinal de igual + valor do parâmetro. Uma linha - um parâmetro. Espaços e tabulações no início de uma linha e entre o sinal de igual e o nome e valor são ignorados. Linhas em branco e linhas que começam com o caractere hash - "#" também são ignoradas, esse caractere define as linhas com comentários. O caso de letras nos nomes de parâmetros e seções não importa.
Além dos parâmetros, o arquivo também contém seções cujos nomes estão entre colchetes. Após o nome da seção encontrada, o analisador espera que apenas os parâmetros pertencentes a esta seção irão adiante até que outro nome de seção seja encontrado. Honestamente, não sei por que introduzi essas seções. Quando fiz isso, tive algum tipo de pensamento conectado a eles, mas agora não consigo me lembrar.
Para encurtar as comparações do nome do parâmetro de leitura com nomes predefinidos, a primeira letra do nome de leitura é analisada primeiro e, em seguida, apenas os nomes que começam com essa letra são comparados.
Conteúdo do arquivo de configuração
# Stepper motor Z axis settings
[ZMotor]
# .
# : 0 1. : 1.
# .
invert_dir = 1
# .
# : -1 1. : -1.
# -1,
# , . 1
# .
home_direction = -1
# Z . ,
# 0, - .
home_pos = 0.0
# .
# : -32000.0 32000.0.
# : -3.0
# .
# , .
min_pos = -3.0
# .
# : -32000.0 32000.0.
# : 180.0
# .
# , .
max_pos = 180.0
# .
# : 0 1. : 1.
# ,
# 1, - 0.
min_endstop_inverting = 1
# .
# : 0 1. : 1.
# ,
# 1, - 0.
max_endstop_inverting = 1
# 1 .
steps_per_mm = 1600
# ,
# , /. : 6.0.
homing_feedrate_fast = 6.0
# ,
# , /. : 1.0.
homing_feedrate_slow = 1.0
# , /2.
acceleration = 0.7
# , /.
feedrate = 5.0
# ( ,
# ..), /2.
travel_acceleration = 25.0
# ( ,
# ..), /. 30
# ,
# 5 /.
travel_feedrate = 25.0
# , .
current_vref = 800.0
# , .
current_hold_vref = 300.0
# ,
# . . 0
# .
hold_time = 30.0
# ,
# . .
# hold_time. 0 .
# , .
off_time = 10.0
# General settings
[General]
# (0.001 )
# .
# : 0 15000. : 700 (0.7 ).
buzzer_msg_duration = 700
# (0.001 )
# , .
# : 0 15000. : 70 (0.07 ).
buzzer_touch_duration = 70
# 180 .
# .
# : 0 1. : 0.
rotate_display = 0
# , .
# LCD-. -
# .
# : 0 15000. : 10. 0 .
screensaver_time = 10
Quando tal arquivo (com a extensão .acfg) for selecionado na lista de arquivos, o firmware perguntará se o usuário deseja baixar e aplicar as configurações deste arquivo e, após a confirmação, começará a analisar este arquivo.

Se um erro for encontrado, uma mensagem será exibida indicando o tipo de erro e o número da linha. Os seguintes erros são tratados:
- nome de partição desconhecido
- nome de parâmetro desconhecido
- valor de parâmetro inválido - quando, por exemplo, um parâmetro numérico é tentado a ser atribuído a um valor de texto
Se alguém estiver interessado - aqui está uma folha completa das três funções principais do analisador
void _cfg_GetParamName(char *src, char *dest, uint16_t maxlen)
{
if (src == NULL || dest == NULL)
return;
char *string = src;
// skip spaces
while (*string != 0 && maxlen > 0 && (*string == ' ' || *string == '\t' || *string == '\r'))
{
string++;
maxlen--;
}
// until first space symbol
while (maxlen > 0 && *string != 0 && *string != ' ' && *string != '\t' && *string != '\r' && *string != '\n' && *string != '=')
{
*dest = *string;
dest++;
string++;
maxlen--;
}
if (maxlen == 0)
dest--;
*dest = 0;
return;
}
//==============================================================================
void _cfg_GetParamValue(char *src, PARAM_VALUE *val)
{
val->type = PARAMVAL_NONE;
val->float_val = 0;
val->int_val = 0;
val->uint_val = 0;
val->char_val = (char*)"";
if (src == NULL)
return;
if (val == NULL)
return;
char *string = src;
// search '='
while (*string > 0 && *string != '=')
string++;
if (*string == 0)
return;
// skip '='
string++;
// skip spaces
while (*string != 0 && (*string == ' ' || *string == '\t' || *string == '\r'))
string++;
if (*string == 0)
return;
// check param if it numeric
if ((*string > 47 && *string < 58) || *string == '.' || (*string == '-' && (*(string+1) > 47 && *(string+1) < 58) || *(string+1) == '.'))
{
val->type = PARAMVAL_NUMERIC;
val->float_val = (float)atof(string);
val->int_val = atoi(string);
val->uint_val = strtoul(string, NULL, 10);
}
else
{
val->type = PARAMVAL_STRING;
val->char_val = string;
}
return;
}
//==============================================================================
void CFG_LoadFromFile(void *par1, void *par2)
{
sprintf(msg, LANG_GetString(LSTR_MSG_CFGFILE_LOADING), cfgCFileName);
TGUI_MessageBoxWait(LANG_GetString(LSTR_WAIT), msg);
UTF8ToUnicode_Str(cfgTFileName, cfgCFileName, sizeof(cfgTFileName)/2);
if (f_open(&ufile, cfgTFileName, FA_OPEN_EXISTING | FA_READ) != FR_OK)
{
if (tguiActiveScreen == (TG_SCREEN*)&tguiMsgBox)
tguiActiveScreen = (TG_SCREEN*)((TG_MSGBOX*)tguiActiveScreen)->prevscreen;
TGUI_MessageBoxOk(LANG_GetString(LSTR_ERROR), LANG_GetString(LSTR_MSG_FILE_OPEN_ERROR));
BUZZ_TimerOn(cfgConfig.buzzer_msg);
return;
}
uint16_t cnt = 0;
uint32_t readed = 0, totalreaded = 0;
char *string = msg;
char lexem[128];
PARAM_VALUE pval;
CFGREAD_STATE rdstate = CFGR_GENERAL;
int16_t numstr = 0;
while (1)
{
// read one string
cnt = 0;
readed = 0;
string = msg;
while (cnt < sizeof(msg))
{
if (f_read(&ufile, string, 1, &readed) != FR_OK || readed == 0 || *string == '\n')
{
*string = 0;
break;
}
cnt++;
string++;
totalreaded += readed;
}
if (cnt == sizeof(msg))
{
string--;
*string = 0;
}
numstr++;
string = msg;
// trim spaces/tabs at begin and end
strtrim(string);
// if string is empty
if (*string == 0)
{
// if end of file
if (readed == 0)
break;
else
continue;
}
// skip comments
if (*string == '#')
continue;
// upper all letters
strupper_utf(string);
// get parameter name
_cfg_GetParamName(string, lexem, sizeof(lexem));
// check if here section name
if (*lexem == '[')
{
if (strcmp(lexem, (char*)"[ZMOTOR]") == 0)
{
rdstate = CFGR_ZMOTOR;
continue;
}
else if (strcmp(lexem, (char*)"[GENERAL]") == 0)
{
rdstate = CFGR_GENERAL;
continue;
}
else
{
rdstate = CFGR_ERROR;
string = LANG_GetString(LSTR_MSG_UNKNOWN_SECTNAME_IN_CFG);
sprintf(msg, string, numstr);
break;
}
}
// get parameter value
_cfg_GetParamValue(string, &pval);
if (pval.type == PARAMVAL_NONE)
{
rdstate = CFGR_ERROR;
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
// check and setup parameter
switch (rdstate)
{
case CFGR_ZMOTOR:
rdstate = CFGR_ERROR;
if (*lexem == 'A')
{
if (strcmp(lexem, (char*)"ACCELERATION") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.float_val < 0.1)
pval.float_val = 0.1;
cfgzMotor.acceleration = pval.float_val;
rdstate = CFGR_ZMOTOR;
break;
}
} else
if (*lexem == 'C')
{
if (strcmp(lexem, (char*)"CURRENT_HOLD_VREF") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.uint_val < 100)
pval.uint_val = 100;
if (pval.uint_val > 1000)
pval.uint_val = 1000;
cfgzMotor.current_hold_vref = pval.uint_val;
rdstate = CFGR_ZMOTOR;
break;
}
if (strcmp(lexem, (char*)"CURRENT_VREF") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.uint_val < 100)
pval.uint_val = 100;
if (pval.uint_val > 1000)
pval.uint_val = 1000;
cfgzMotor.current_vref = pval.uint_val;
rdstate = CFGR_ZMOTOR;
break;
}
} else
if (*lexem == 'F')
{
if (strcmp(lexem, (char*)"FEEDRATE") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.float_val < 0.1)
pval.float_val = 0.1;
if (pval.float_val > 40)
pval.float_val = 40;
cfgzMotor.feedrate = pval.float_val;
rdstate = CFGR_ZMOTOR;
break;
}
} else
if (*lexem == 'H')
{
if (strcmp(lexem, (char*)"HOLD_TIME") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.uint_val == 0)
pval.uint_val = TIMER_DISABLE;
else if (pval.uint_val > 100000)
pval.uint_val = 100000;
cfgzMotor.hold_time = pval.uint_val * 1000;
rdstate = CFGR_ZMOTOR;
break;
}
if (strcmp(lexem, (char*)"HOME_DIRECTION") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.int_val != -1.0 && pval.int_val != 1.0)
pval.int_val = -1;
cfgzMotor.home_dir = pval.int_val;
rdstate = CFGR_ZMOTOR;
break;
}
if (strcmp(lexem, (char*)"HOME_POS") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
cfgzMotor.home_pos = pval.float_val;
rdstate = CFGR_ZMOTOR;
break;
}
if (strcmp(lexem, (char*)"HOMING_FEEDRATE_FAST") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.float_val < 0.1)
pval.float_val = 0.1;
if (pval.float_val > 40)
pval.float_val = 40;
cfgzMotor.homing_feedrate_fast = pval.float_val;
rdstate = CFGR_ZMOTOR;
break;
}
if (strcmp(lexem, (char*)"HOMING_FEEDRATE_SLOW") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.float_val < 0.1)
pval.float_val = 0.1;
if (pval.float_val > 40)
pval.float_val = 40;
cfgzMotor.homing_feedrate_slow = pval.float_val;
rdstate = CFGR_ZMOTOR;
break;
}
} else
if (*lexem == 'I')
{
if (strcmp(lexem, (char*)"INVERT_DIR") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.int_val < 0 || pval.int_val > 1)
pval.int_val = 1;
cfgzMotor.invert_dir = pval.int_val;
rdstate = CFGR_ZMOTOR;
break;
}
} else
if (*lexem == 'M')
{
if (strcmp(lexem, (char*)"MAX_ENDSTOP_INVERTING") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.int_val < 0 || pval.int_val > 1)
pval.int_val = 1;
cfgzMotor.max_endstop_inverting = pval.int_val;
rdstate = CFGR_ZMOTOR;
break;
}
if (strcmp(lexem, (char*)"MAX_POS") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
cfgzMotor.max_pos = pval.float_val;
rdstate = CFGR_ZMOTOR;
break;
}
if (strcmp(lexem, (char*)"MIN_ENDSTOP_INVERTING") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.int_val < 0 || pval.int_val > 1)
pval.int_val = 1;
cfgzMotor.min_endstop_inverting = pval.int_val;
rdstate = CFGR_ZMOTOR;
break;
}
if (strcmp(lexem, (char*)"MIN_POS") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
cfgzMotor.min_pos = pval.float_val;
rdstate = CFGR_ZMOTOR;
break;
}
} else
if (*lexem == 'O')
{
if (strcmp(lexem, (char*)"OFF_TIME") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.uint_val > 100000)
pval.uint_val = 100000;
else if (pval.uint_val < cfgzMotor.hold_time)
pval.uint_val = cfgzMotor.hold_time + 1000;
else if (pval.uint_val == 0)
pval.uint_val = TIMER_DISABLE;
cfgzMotor.off_time = pval.int_val * 60000;
rdstate = CFGR_ZMOTOR;
break;
}
} else
if (*lexem == 'S')
{
if (strcmp(lexem, (char*)"STEPS_PER_MM") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.uint_val < 1)
pval.uint_val = 1;
if (pval.uint_val > 200000)
pval.uint_val = 200000;
cfgzMotor.steps_per_mm = pval.uint_val;
rdstate = CFGR_ZMOTOR;
break;
}
} else
if (*lexem == 'T')
{
if (strcmp(lexem, (char*)"TRAVEL_ACCELERATION") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.float_val < 0.1)
pval.float_val = 0.1;
cfgzMotor.travel_acceleration = pval.float_val;
rdstate = CFGR_ZMOTOR;
break;
}
if (strcmp(lexem, (char*)"TRAVEL_FEEDRATE") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.float_val < 0.1)
pval.float_val = 0.1;
cfgzMotor.travel_feedrate = pval.float_val;
rdstate = CFGR_ZMOTOR;
break;
}
}
string = LANG_GetString(LSTR_MSG_UNKNOWN_PARAMNAME_IN_CFG);
sprintf(msg, string, numstr);
break;
case CFGR_GENERAL:
rdstate = CFGR_ERROR;
if (*lexem == 'B')
{
if (strcmp(lexem, (char*)"BUZZER_MSG_DURATION") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.uint_val > 15000)
pval.uint_val = 15000;
cfgConfig.buzzer_msg = pval.uint_val;
rdstate = CFGR_GENERAL;
break;
}
if (strcmp(lexem, (char*)"BUZZER_TOUCH_DURATION") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.uint_val > 15000)
pval.uint_val = 15000;
cfgConfig.buzzer_touch = pval.uint_val;
rdstate = CFGR_GENERAL;
break;
}
} else
if (*lexem == 'R')
{
if (strcmp(lexem, (char*)"ROTATE_DISPLAY") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.uint_val > 0)
{
cfgConfig.display_rotate = 1;
LCD_WriteCmd(0x0036);
LCD_WriteRAM(0x0078);
}
else
{
cfgConfig.display_rotate = 0;
LCD_WriteCmd(0x0036);
LCD_WriteRAM(0x00B8);
}
rdstate = CFGR_GENERAL;
break;
}
} else
if (*lexem == 'S')
{
if (strcmp(lexem, (char*)"SCREENSAVER_TIME") == 0)
{
if (pval.type != PARAMVAL_NUMERIC)
{
string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (pval.uint_val > 15000)
cfgConfig.screensaver_time = 15000 * 60000;
else if (pval.uint_val == 0)
pval.uint_val = TIMER_DISABLE;
else
cfgConfig.screensaver_time = pval.uint_val * 60000;
rdstate = CFGR_GENERAL;
break;
}
}
string = LANG_GetString(LSTR_MSG_UNKNOWN_PARAMNAME_IN_CFG);
sprintf(msg, string, numstr);
break;
}
if (rdstate == CFGR_ERROR)
break;
}
f_close(&ufile);
if (tguiActiveScreen == (TG_SCREEN*)&tguiMsgBox)
{
tguiActiveScreen = (TG_SCREEN*)((TG_MSGBOX*)tguiActiveScreen)->prevscreen;
}
if (rdstate == CFGR_ERROR)
{
TGUI_MessageBoxOk(LANG_GetString(LSTR_ERROR), msg);
BUZZ_TimerOn(cfgConfig.buzzer_msg);
}
else
{
CFG_SaveMotor();
CFG_SaveConfig();
TGUI_MessageBoxOk(LANG_GetString(LSTR_COMPLETED), LANG_GetString(LSTR_MSG_CFGFILE_LOADED));
}
}
//==============================================================================
Após a análise bem-sucedida do arquivo, as novas configurações são aplicadas imediatamente e salvas na EPROM.
Os contadores de horas de operação dos componentes da impressora só são atualizados na EPROM quando o arquivo é impresso ou interrompido.
6. Recursos adicionais para conforto e conveniência
6.1 Relógio com calendário
Bem, apenas para fazer isso. Por que desperdiçar bondade - um relógio autônomo em tempo real embutido no microcontrolador, que pode operar com uma bateria de lítio quando a energia geral está desligada e consumir tão pouco que o CR2032, segundo cálculos, deve ser suficiente por vários anos. Além disso, o fabricante até forneceu na placa o quartzo de 32 kHz necessário para este relógio. Resta apenas colar o porta-bateria na placa e soldar a fiação dela ao menos comum e ao terminal especial do microcontrolador, o que fiz em casa.
A hora, o dia e o mês são exibidos na parte superior esquerda da tela principal:

O mesmo relógio de tempo real é usado para contar o tempo de impressão e as horas de operação dos componentes. E eles também são usados no protetor de tela, que é descrito a seguir.
6.2 Bloqueio da tela contra cliques acidentais durante a impressão
Isso foi feito a pedido de um conhecido. Bem, por que não, pode ser útil em alguns casos. O bloqueio é ativado e desativado pressionando longamente (~ 2,5 seg) no cabeçalho da tela de impressão. Quando o cadeado está ativo, um cadeado vermelho é exibido no canto superior direito. No final da impressão, o bloqueio é desbloqueado automaticamente.
6.3 Diminuir a corrente do motor no modo de retenção, desligamento do motor ocioso
Feito para reduzir o acúmulo geral de calor dentro do corpo da impressora. O motor pode ser colocado em modo de espera com corrente reduzida após o tempo sem movimento configurado. Esse recurso, aliás, é comum nos drivers de motor de passo "adulto" do tipo TB6560. Além disso, nas configurações, você pode definir o tempo após o qual, na ausência de movimento, o motor ficará totalmente desenergizado. Mas isso também levará ao fato de que o zeramento do eixo, se for realizado, ficará inválido. Ambos os recursos podem ser completamente desativados nas mesmas configurações.
6.4 Protetor de tela
Como um relógio - só porque posso. Na ausência de pressionar a tela após a hora especificada nas configurações, a tela muda para o modo de emulação de um relógio digital de mesa:

Além da hora, a data completa com o dia da semana também é exibida. O firmware sai deste modo pressionando em qualquer parte do display. Considerando que os números são muito grandes e o consumo de eletricidade quando o motor está desligado é inferior a 2 watts, uma impressora com esse protetor de tela pode servir como um relógio de sala :) Durante a impressão, o protetor de tela também aparece após um tempo especificado, mas com um acréscimo - o andamento da impressão na parte inferior da tela:

Nas configurações, você pode definir o tempo de resposta do protetor de tela ou desativá-lo.
6.5 Luz de fundo e verificação da tela

Esta tela pode ser acessada a partir do menu "Serviço" e será útil ao verificar os diodos de luz de fundo ou display UV. Na parte superior, uma das três imagens é selecionada, que será exibida no visor UV - quadro, iluminação total de todo o visor, retângulos. Na parte inferior, há dois botões que ligam e desligam a luz de fundo e a tela. A luz incluída desligará automaticamente após 2 minutos, geralmente este tempo é suficiente para qualquer teste. Ao sair desta tela, a luz de fundo e a tela serão desligadas automaticamente.
6.6 Configurações

Esta tela também pode ser acessada no menu Ferramentas. Existem muito poucas configurações aqui e, para ser honesto, nunca pensei em quais configurações seriam tão solicitadas que faria sentido colocá-las na interface, e não apenas no arquivo de configuração. Isso também adicionará a capacidade de redefinir os contadores de tempo operacional para componentes da impressora, bem, eu não sei mais :)
Claro, aqui você pode definir a hora e a data (já que há um relógio) na tela que abre separadamente:

Você pode definir a altura de elevação da plataforma na pausa e ligá-la e desligá-la som de cliques e mensagens no display. Ao alterar as configurações, os novos valores só terão efeito até que a alimentação seja desligada e não serão salvos na EPROM. Para salvá-los, após alterar os parâmetros, pressione o botão salvar no menu (com um ícone de disquete).
Os valores numéricos são inseridos em uma tela especial:

Aqui, implementei todos os recursos que faltavam em outras impressoras.
- Botões "±" e "." funcionam apenas se o parâmetro editado pode ser negativo ou fracionário, respectivamente.
- Se, depois de entrar nesta tela, qualquer botão numérico for pressionado primeiro, o valor antigo será substituído pelo dígito correspondente. Se o botão for ".", Será substituído por "0". Ou seja, não há necessidade de apagar o valor antigo, você pode começar a inserir um novo imediatamente.
- Botão "", zerando o valor atual.
Pressionar o botão Voltar não aplicará o novo valor. Para aplicá-lo, você precisa clicar em "OK".
6.7 Finalmente - Tela de Informações da Impressora

Esta tela pode ser acessada diretamente no menu principal. O mais importante aqui é a versão do firmware / FPGA e os contadores de tempo de operação. Na parte inferior, ainda há informações sobre o autor da interface e o endereço do repositório no GitHub. O autor da interface é a base para o futuro. Se eu ainda permitir a configuração da interface por meio de um arquivo de texto simples, haverá a oportunidade de especificar o nome do autor.
o fim
Esta é a última parte deste meu projeto favorito. O projeto vive e se desenvolve, embora não tão rápido quanto eu gostaria, mas já é bastante eficiente.
Eu provavelmente deveria ter colocado mais código ... Mas não acho que haja partes no meu código para me gabar. Na minha opinião, é mais importante descrever como funciona e o que foi feito, e o código está lá, está tudo no GitHub, quem vai se interessar, posso assistir lá na íntegra. Acho que sim.
Aguardo suas perguntas e comentários, e obrigado pelo seu interesse nestes artigos.
- 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. Enviando imagens de camadas para a 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