Logger multifuncional DIY. Parte 1

imagem

Eu sou o proprietário de um dispositivo maravilhoso - o registrador GPS Holux M-241. A coisa é muito conveniente e útil quando se viaja. Com a ajuda de um registrador, escrevo uma faixa de GPS de uma viagem, ao longo da qual você pode ver o seu caminho em detalhes, e também anexo as fotos que você tira nas coordenadas de GPS. Ele também possui uma tela pequena que mostra informações adicionais - horas, velocidade atual, altitude e direção, odômetro e muito mais. Aqui , escrevi uma vez uma breve resenha.

Com todas as vantagens de um pedaço de ferro, comecei a crescer. Sinto falta de alguns itens pequenos, mas úteis: alguns odômetros, mostrando velocidade vertical, medindo os parâmetros de uma seção de pista. Parece pequenas coisas, mas a empresa Holux achou isso não útil o suficiente para implementação no firmware. Além disso, não gosto de alguns parâmetros do hardware, e algumas coisas ficaram desatualizadas em 10 anos ...

Em algum momento, percebi que eu próprio posso criar um logger com os recursos necessários. Felizmente, todos os componentes necessários são bastante baratos e acessíveis. Comecei a fazer minha implementação baseada no Arduino. Sob o corte, um diário de construção onde tentei pintar minhas soluções técnicas.

Definindo recursos


Muitos perguntam por que eu preciso construir meu próprio logger, se com certeza há algo pronto para fabricantes eminentes. Possivelmente. Para ser sincero, não procurei. Mas com certeza haverá algo faltando. De qualquer forma, este projeto é um fã para mim. Por que não começamos a construir nosso dispositivo dos sonhos?

Então, pelo que eu aprecio minha Holux M-241.

  • A tela cria uma "caixa preta", cujos resultados estão disponíveis somente após a viagem, uma ferramenta muito conveniente, cujas leituras estão disponíveis aqui e agora. Ter uma tela possibilita quase todos os recursos desta lista.
  • Um relógio é útil por si só. Nas viagens de GPS, o madeireiro pendurado em uma corda em volta do pescoço geralmente fica mais perto do que um telefone celular no bolso ou em uma mochila. O relógio suporta todos os fusos horários (embora com comutação manual)
  • O botão POI permite marcar a coordenada atual na pista. Por exemplo, para observar um ponto de referência que saiu da janela do ônibus, sobre o qual desejo pesquisar mais tarde.
  • Usando o odômetro, você pode medir a distância percorrida a partir de algum ponto. Por exemplo, a distância percorrida por dia ou o comprimento de uma pista.
  • A velocidade atual, altitude e direção ajudam a encontrar-se no espaço
  • A capacidade de sobrevivência de 12 a 14 horas de uma bateria AA na maioria dos casos permite que você não pense em problemas de fonte de alimentação. I.e. quase sempre cobrar o suficiente para um dia inteiro de viagem.
  • Compacto e fácil de usar - as coisas no mundo moderno são muito agradáveis

No entanto, algumas coisas poderiam ser feitas um pouco melhor:

  • O subsistema de energia das baterias AA é anotado por muitos como uma vantagem definitiva - uma bateria dura muito tempo e você pode reabastecer o suprimento em qualquer região selvagem. Você pode estocar pelo menos um mês de acampamento autônomo.

    Mas para mim, a vida da bateria é pura hemorróidas. Você precisa carregar um punhado de baterias e quem sabe como elas são de alta qualidade (repentinamente ficaram em uma prateleira por 5 anos e já se descarregavam). Com as baterias, a hemorragia é ainda maior. Meu carregador só pode carregar em pares. Temos que descarregar as baterias para que elas tenham o mesmo grau de descarga. Como resultado, você nunca se lembra de onde já foi descarregado e de onde ainda não.

    Por 6 anos usando o logger, acabei no deserto apenas sem eletricidade algumas vezes. Como regra, tenho acesso à tomada pelo menos uma vez por dia. Nesse caso, a bateria de lítio embutida seria muito mais conveniente. Bem, em casos extremos, eu tenho um banco de paver.

  • A indicação do grau de descarga é feita de maneira estúpida - o indicador começa a piscar quando a bateria está prestes a descarregar. Além disso, ele pode morrer em 5 minutos e talvez funcionar mais uma hora. É muito fácil perder esse momento e perder parte do log.

  • Como pessoa interessada em aviação, seria muito interessante observar a velocidade vertical atual .

  • Alguns odômetros - geralmente é interessante medir mais de uma distância. Por exemplo, a distância percorrida por dia e durante toda a viagem.

  • O odômetro é redefinido quando você desliga o dispositivo ou quando substitui a bateria. Isso é terrivelmente desconfortável. Se você parou para uma refeição em um café, o registrador GPS não pode ser desligado porque o valor será redefinido. Ele precisa deixá-lo ligado e continua a percorrer quilômetros e a comer a bateria. Seria muito mais conveniente poder colocar o odômetro em pausa e salvar os valores entre inclusões.

  • Medição de parâmetros do local . Ao esquiar, por exemplo, estou interessado no comprimento da descida, elevação, velocidade média e máxima no site, no tempo gasto. O que você quer saber é imediatamente, e não em casa quando você baixa a faixa.

  • A precisão é fraca. Quando você se move rápido - nada mais. Mas quando a velocidade é pequena na pista, “barulhos” + - 50m são claramente visíveis. E por uma hora em pé, você pode "insistir" quase um quilômetro. Os benefícios da tecnologia há 10 anos foram muito além e os receptores modernos oferecem uma precisão muito maior.

  • A velocidade de fusão das faixas é de apenas 38.400. Não, bem, em 2017 não é sério usar a porta COM para transferir grandes quantidades de dados. A fusão de 2 megabytes de flash interno leva mais de 20 minutos.

    Além disso, nem todo programa pode digerir o formato de faixas mescladas. O utilitário nativo é muito miserável. Felizmente, existe o BT747, que pode mesclar adequadamente a faixa e convertê-la em algum tipo de formato digerível.

  • O tamanho da unidade flash é de apenas 2 MB. Por um lado, isso é suficiente para uma viagem de duas semanas com pontos de economia a cada 5s. Mas primeiro, o formato interno do pacote
    requer conversão e, em segundo lugar, não permite aumentar o volume
  • Por algum motivo, o dispositivo de armazenamento em massa não está na moda. Interfaces modernas tentam esconder o fato da presença de arquivos. Estou com computadores há 25 anos e trabalhar com arquivos diretamente é muito mais conveniente para mim do que de qualquer outra maneira.

Não há nada aqui que não possa ser realizado sem esforços significativos.

Qualquer coisa diferente. Eu não uso, mas de repente alguém é útil:

  • Mostra coordenadas atuais (latitude, longitude)
  • Ícones diferentes são desenhados no lado esquerdo da tela, cuja essência nem me lembro sem um manual.
  • Existem comutadores metros / km - pés / milhas.
  • O Bluetooth - logger pode ser conectado a telefones celulares sem GPS.
  • A distância absoluta ao ponto.
  • Registro por tempo (a cada N segundos) ou por distância (a cada X metros).
  • Suporte para diferentes idiomas.

Escolha ferro


Os requisitos são mais ou menos definidos. É hora de entender como tudo isso pode ser implementado. Os principais componentes que terei são:

  • Microcontrolador - Não tenho planos para algoritmos computacionais sofisticados, portanto o poder de processamento do kernel não é particularmente importante. Eu também não tenho requisitos especiais para o enchimento - um conjunto de periféricos padrão servirá.

    Havia apenas uma dispersão de arduinos diversos, além de alguns stm32f103c8t6. Decidi começar com o AVR, que eu conheço bem no nível dos controladores / registros / periféricos. Se eu tiver restrições - haverá um motivo para sentir o STM32.

  • O receptor GPS foi selecionado entre os módulos NEO6MV2, Beitan BN-800 e Beitan BN-880. Fóruns pesquisados ​​por algum tempo. Pessoas experientes disseram que o primeiro receptor é o século passado. Os outros dois diferem um do outro apenas na localização da antena - no BN-800 ele fica pendurado no fio e no BN-880 é colado com um sanduíche no módulo principal. Tomou um BN-880 .

  • Tela - o original usa um LCD de 128 x 32 com luz de fundo. Não encontrei exatamente o mesmo. Comprei um OLED 0,91 ”no controlador SSD1306 e uma tela LCD de 1,2” no controlador ST7565R . Eu decidi começar do primeiro, porque é mais fácil conectar-se a um pente padrão de acordo com I2C ou SPI. Mas é um pouco menor em comparação com o original e também não funcionará para exibir constantemente a imagem por razões de eficiência de combustível. O segundo monitor deve ser menos guloso, mas você precisa soldar um conector complicado e descobrir como alimentar a luz de fundo.

Das pequenas coisas:

  • Botões já compraram uma sacola inteira;
  • Blindagem com cartão SD - também à mão;
  • Comprei um par de controladores de carga diferentes para baterias de lítio, mas ainda não o entendi.

Decidi projetar a placa no final, quando o firmware estiver pronto. A essa altura, finalmente decidirei sobre os principais componentes e o esquema para sua inclusão. No primeiro estágio, decidi fazer a depuração na placa de ensaio conectando os componentes usando cabos de manobra.

Mas primeiro você precisa decidir sobre uma questão muito importante - a nutrição dos componentes. Pareceu-me razoável alimentar tudo de 3.3V: GPS e a tela apenas nele e saber trabalhar. Essa também é a voltagem nativa para USB e SD. Além disso, o circuito pode ser alimentado por uma lata de lítio.

A escolha recaiu sobre o Arduino Pro Mini, que pode ser encontrado na versão 8MHz / 3.3V. Mas ela não tinha USB a bordo - tive que usar um adaptador USB-UART.

Primeiros passos


Inicialmente, o projeto foi criado no Arduino IDE. Mas, para ser sincero, meu idioma não ousa chamá-lo de IDE - como um editor de texto com um compilador. De qualquer forma, depois do Visual Studio, no qual trabalho nos últimos 13 anos, não posso fazer nada sério no IDE do Arduino sem lágrimas e matyuk.

Felizmente, existe um Atmel Studio gratuito, no qual até o Visual Assist é embutido imediatamente! O programa sabe tudo o que é necessário, tudo é familiar e em seu lugar. Bem, quase tudo (não encontrei como compilar apenas um arquivo, por exemplo, para verificar a sintaxe)

imagem

Iniciado na tela - é necessário depurar o esqueleto do firmware e preenchê-lo com funcionalidade. Ele parou na primeira biblioteca disponível para o Adafruit SSD1306 . Ela sabe tudo o que é necessário e fornece uma interface muito simples.

Jogado com fontes. Verificou-se que uma fonte pode levar até 8kb (o tamanho das letras é 24pt) - você não pode usar um controlador de 32kb em especial. Fontes grandes são necessárias, por exemplo, para exibir o tempo.

Código de amostra da fonte
#include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <gfxfont.h> #include <fonts/FreeMono12pt7b.h> #include <fonts/FreeMono18pt7b.h> ... #include <fonts/FreeSerifItalic24pt7b.h> #include <fonts/FreeSerifItalic9pt7b.h> #include <fonts/TomThumb.h> struct font_and_name { const char * PROGMEM name; GFXfont * font; }; #define FONT(name) {#name, &name} const font_and_name fonts[] = { // FONT(FreeMono12pt7b), FONT(FreeMono18pt7b), /* FONT(FreeMono24pt7b), FONT(FreeMono9pt7b), FONT(FreeMonoBold12pt7b), ... FONT(FreeSerifItalic9pt7b), FONT(TomThumb)*/ }; const unsigned int fonts_count = sizeof(fonts) / sizeof(font_and_name); unsigned int current_font = 0; extern Adafruit_SSD1306 display; void RunFontTest() { display.clearDisplay(); display.setCursor(0,30); display.setFont(fonts[current_font].font); display.print("12:34:56"); display.setCursor(0,6); display.setFont(&TomThumb); display.print(fonts[current_font].name); display.display(); } void SwitchToNextFont() { current_font = ++current_font % fonts_count; } 


As fontes completas com a biblioteca são muito desajeitadas. A fonte monoespaçada mostrou-se muito ampla - a linha "12:34:56" não se encaixa, Serif - todos os números têm pesos diferentes. A menos que a fonte 5x7 padrão da biblioteca pareça comestível.

imagem

imagem

Aconteceu que essas fontes foram convertidas de algumas fontes ttf de código aberto que simplesmente não são otimizadas para pequenas resoluções.

Eu tive que desenhar minhas fontes. Mais precisamente, primeiro, desenterre símbolos individuais dos acabados. O símbolo ':' na tabela ASCII é muito útil logo após os números e pode ser comprado em um bloco. Também é conveniente que você possa criar uma fonte não para todos os caracteres, mas apenas para um intervalo, por exemplo, de 0x30 ('0') a 0x3a (':'). T.O. do FreeSans18pt7b, acabou produzindo uma fonte muito compacta apenas para os caracteres necessários. É verdade que tive que ajustar levemente a largura para que o texto caiba na largura da tela.

Sub-fonte FreeSans18pt7b
 // This font consists only of digits and ':' to display current time. // The font is very based on FreeSans18pt7b.h //TODO: 25 pixel height is too much for displaying time. Create another 22px font const uint8_t TimeFontBitmaps[] PROGMEM = { /* 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE9, 0x20, 0x3F, 0xFC, 0xE3, 0xF1, 0xF8, 0xFC, 0x7E, 0x3F, 0x1F, 0x8E, 0x82, 0x41, 0x00, 0x01, 0xC3, 0x80, ... 0x03, 0x00, 0xC0, 0x60, 0x18, 0x06, 0x03, 0x00, 0xC0, 0x30, 0x18, 0x06, 0x01, 0x80, 0xC0, 0x30, 0x00, */0x07, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x3C, 0x78, 0x1E, 0x70, 0x0E, 0x70, 0x0E, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x3C, 0x3C, 0x1F, 0xF8, 0x1F, 0xF0, 0x07, 0xE0, 0x03, 0x03, 0x07, 0x0F, 0x3F, 0xFF, 0xFF, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0xE0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x70, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xE0, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0xF8, 0x03, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0x38, 0x00, 0x70, 0x00, 0x60, 0x00, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xF0, 0x07, 0xFE, 0x07, 0xFF, 0x87, 0x83, 0xC3, 0x80, 0xF3, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x7F, 0x00, 0x3F, 0x00, 0x1F, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x00, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFE, 0x00, 0x77, 0x00, 0x73, 0xE0, 0xF8, 0xFF, 0xF8, 0x3F, 0xF8, 0x07, 0xF0, 0x00, 0x00, 0x38, 0x00, 0x38, 0x00, 0x78, 0x00, 0xF8, 0x00, 0xF8, 0x01, 0xF8, 0x03, 0xB8, 0x03, 0x38, 0x07, 0x38, 0x0E, 0x38, 0x1C, 0x38, 0x18, 0x38, 0x38, 0x38, 0x70, 0x38, 0x60, 0x38, 0xE0, 0x38, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x1F, 0xFF, 0x0F, 0xFF, 0x8F, 0xFF, 0xC7, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x39, 0xF0, 0x3F, 0xFE, 0x1F, 0xFF, 0x8F, 0x83, 0xE7, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xFC, 0x00, 0xEF, 0x00, 0x73, 0xC0, 0xF0, 0xFF, 0xF8, 0x3F, 0xF8, 0x07, 0xE0, 0x00, 0x03, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x1E, 0x38, 0x0E, 0x70, 0x0E, 0x70, 0x00, 0x60, 0x00, 0xE0, 0x00, 0xE3, 0xE0, 0xEF, 0xF8, 0xFF, 0xFC, 0xFC, 0x3E, 0xF0, 0x0E, 0xF0, 0x0F, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0x60, 0x07, 0x70, 0x0F, 0x70, 0x0E, 0x3C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x07, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x18, 0x00, 0x38, 0x00, 0x70, 0x00, 0x60, 0x00, 0xE0, 0x00, 0xC0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x03, 0x80, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0xFF, 0x87, 0x83, 0xC7, 0x80, 0xF3, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x78, 0x0F, 0x1E, 0x0F, 0x07, 0xFF, 0x01, 0xFF, 0x03, 0xFF, 0xE3, 0xE0, 0xF9, 0xC0, 0x1D, 0xC0, 0x0F, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0xF7, 0x00, 0x73, 0xE0, 0xF8, 0xFF, 0xF8, 0x3F, 0xF8, 0x07, 0xF0, 0x00, 0x07, 0xE0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3C, 0x70, 0x0E, 0xF0, 0x0E, 0xE0, 0x06, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0x70, 0x0F, 0x78, 0x3F, 0x3F, 0xFF, 0x1F, 0xF7, 0x07, 0xC7, 0x00, 0x07, 0x00, 0x06, 0x00, 0x0E, 0x70, 0x0E, 0x70, 0x1C, 0x78, 0x3C, 0x3F, 0xF8, 0x1F, 0xF0, 0x07, 0xC0, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x80 /*, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xB6, 0xD6, 0x00, 0x00, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xC0, 0x3F, 0x80, 0x7E, 0x00, 0xFC, 0x01, 0xF0, 0x00, 0xE0, 0x00, ... 0x38, 0x38, 0xF8, 0xF0, 0xE0, 0x38, 0x00, 0xFC, 0x03, 0xFC, 0x1F, 0x3E, 0x3C, 0x1F, 0xE0, 0x1F, 0x80, 0x1E, 0x00 */ }; //TODO Recalc offset numbers const GFXglyph TimeFontGlyphs[] PROGMEM = { { 449-449, 16, 25, 19, 2, -24 }, // 0x30 '0' { 499-449, 8, 25, 19, 4, -24 }, // 0x31 '1' { 524-449, 16, 25, 19, 2, -24 }, // 0x32 '2' { 574-449, 17, 25, 19, 1, -24 }, // 0x33 '3' { 628-449, 16, 25, 19, 1, -24 }, // 0x34 '4' { 678-449, 17, 25, 19, 1, -24 }, // 0x35 '5' { 732-449, 16, 25, 19, 2, -24 }, // 0x36 '6' { 782-449, 16, 25, 19, 2, -24 }, // 0x37 '7' { 832-449, 17, 25, 19, 1, -24 }, // 0x38 '8' { 886-449, 16, 25, 19, 1, -24 }, // 0x39 '9' { 936-449, 3, 19, 7, 2, -20 }, // 0x3A ':' }; const GFXfont TimeFont PROGMEM = { (uint8_t *)TimeFontBitmaps, (GFXglyph *)TimeFontGlyphs, 0x30, 0x3A, 20 }; 

Descobriu-se que a fonte 18pt tem 25 pixels de altura. Por isso, ele se encaixa levemente em outra inscrição

imagem

A exibição invertida, a propósito, ajuda a entender onde estão os limites da área de desenho e como a linha se encontra em relação a essa borda - a exibição possui quadros muito grandes.

Por um longo tempo, pesquisei fontes acabadas no Google, mas elas não se encaixavam no tamanho, na forma ou no conteúdo. Por exemplo, na Internet, um eixo de fonte 8x12 (despejos de geradores de caracteres de placa VGA). Mas, de fato, essas fontes são 6x8, ou seja, muito espaço caminha - no caso de uma resolução e tamanho tão pequenos quanto os meus, é fundamental.

Eu tive que desenhar minhas próprias fontes, já que o formato da biblioteca Adafruit é muito simples. Preparei a imagem no Paint.net - simplesmente desenhei as letras na fonte correta e as corrigi um pouco com um lápis. Salvei a imagem como png e enviei-a rapidamente para o script python escrito no meu joelho. Esse script gerou um código semi-acabado que já governa de maneira correta no IDE, diretamente nos códigos hexadecimais.

imagem

Por exemplo, é assim que o processo de criação de uma fonte monoespaçada 8x12 com espaçamento entre letras e linhas pequenas. No final, cada caractere era de aproximadamente 7x10 e, por padrão, ocupava 10 bytes. Seria possível compactar cada caractere em 8 a 9 bytes (a biblioteca permite isso), mas eu não me incomodei. Além disso, neste formulário, você pode editar pixels individuais diretamente no código.

Fonte 8x12
 // A simple 8x12 font (slightly modifier Courier New) const uint8_t Monospace8x12Bitmaps[] PROGMEM = { 0x1e, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x1e, //0 0x18, 0x68, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x7f, //1 0x3e, 0x41, 0x41, 0x01, 0x02, 0x0c, 0x10, 0x20, 0x41, 0x7f, //2 0x3e, 0x41, 0x01, 0x01, 0x0e, 0x02, 0x01, 0x01, 0x41, 0x3e, //3 0x02, 0x06, 0x0a, 0x12, 0x12, 0x22, 0x3f, 0x02, 0x02, 0x0f, //4 0x7f, 0x41, 0x40, 0x40, 0x7e, 0x01, 0x01, 0x01, 0x41, 0x3e, //5 0x1e, 0x21, 0x40, 0x40, 0x5e, 0x61, 0x41, 0x41, 0x41, 0x3e, //6 0x7f, 0x41, 0x01, 0x02, 0x02, 0x04, 0x04, 0x04, 0x08, 0x08, //7 0x1e, 0x21, 0x21, 0x21, 0x1e, 0x21, 0x21, 0x21, 0x21, 0x1e, //8 0x1e, 0x21, 0x21, 0x21, 0x23, 0x1d, 0x01, 0x01, 0x22, 0x1c, //9 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, //: }; const GFXglyph Monospace8x12Glyphs[] PROGMEM = { { 0, 8, 10, 8, 0, -11 }, // 0x30 '0' { 10, 8, 10, 8, 0, -11 }, // 0x31 '1' { 20, 8, 10, 8, 0, -11 }, // 0x32 '2' { 30, 8, 10, 8, 0, -11 }, // 0x33 '3' { 40, 8, 10, 8, 0, -11 }, // 0x34 '4' { 50, 8, 10, 8, 0, -11 }, // 0x35 '5' { 60, 8, 10, 8, 0, -11 }, // 0x36 '6' { 70, 8, 10, 8, 0, -11 }, // 0x37 '7' { 80, 8, 10, 8, 0, -11 }, // 0x38 '8' { 90, 8, 10, 8, 0, -11 }, // 0x39 '9' { 100, 8, 10, 8, 0, -11 }, // 0x3A ':' }; const GFXfont Monospace8x12Font PROGMEM = { (uint8_t *)Monospace8x12Bitmaps, (GFXglyph *)Monospace8x12Glyphs, 0x30, 0x3A, 12 }; 


Moldura


O dispositivo original fornece uma interface muito simples e conveniente. As informações são agrupadas em categorias que são exibidas em páginas individuais (telas). Usando o botão, você pode percorrer as páginas e usar o segundo botão para selecionar o item atual ou executar a ação indicada na assinatura abaixo do botão. Essa abordagem me parece muito conveniente e não há necessidade de alterar nada.

Gosto da beleza do POO, porque imediatamente ofuscou uma pequena interface, cada página implementa a interface conforme necessário. A página sabe como se desenhar e implementa a reação aos botões.

 class Screen { Screen * nextScreen; public: Screen(); virtual ~Screen() {} virtual void drawScreen() = 0; virtual void drawHeader(); virtual void onSelButton(); virtual void onOkButton(); virtual PROGMEM const char * getSelButtonText(); virtual PROGMEM const char * getOkButtonText(); Screen * addScreen(Screen * screen); }; 

Os botões podem executar várias ações, dependendo da tela atual. Portanto, na parte superior da tela, com uma altura de 8 pixels, atribuai aos rótulos dos botões. O texto para as assinaturas depende da tela atual e é retornado pelas funções virtuais getSelButtonText () e getOkButtonText (). Também no cabeçalho, itens de serviço, como força do sinal GPS e carga da bateria, ainda serão exibidos. As restantes ¾ telas estão disponíveis para informações úteis.

Como eu disse, as telas podem ser viradas, o que significa que em algum lugar deve haver uma lista de objetos para páginas diferentes. No que mais de uma tela pode ser aninhada, como um submenu. Até comecei a classe ScreenManager, que deveria gerenciar essas listas, mas achei a solução mais fácil.

Portanto, cada tela simplesmente tem um ponteiro para a próxima. Se a tela permitir que você entre no submenu, ele adicionará mais um ponteiro à tela deste submenu

 class Screen { Screen * nextScreen; … }; class ParentScreen : public Screen { Screen * childScreen; … }; 

Por padrão, o manipulador de botão simplesmente chama a função de mudança de tela, passando o ponteiro desejado. A função acabou sendo trivial - apenas mudou o ponteiro para a tela atual. Para garantir o aninhamento das telas, fiz uma pequena pilha. Assim, o gerenciador de tela inteiro se encaixa em 25 linhas e 4 pequenas funções.

 Screen * screenStack[3]; int screenIdx = 0; void setCurrentScreen(Screen * screen) { screenStack[screenIdx] = screen; } Screen * getCurrentScreen() { return screenStack[screenIdx]; } void enterChildScreen(Screen * screen) { screenIdx++; //TODO limit this screenStack[screenIdx] = screen; } void backToParentScreen() { if(screenIdx) screenIdx--; } 

É verdade que o código para preencher essas estruturas não parece muito bom, mas até agora não foi melhor inventado.

 Screen * createCurrentTimeScreen() { TimeZoneScreen * tzScreen = new TimeZoneScreen(1, 30); tzScreen = tzScreen->addScreen(new TimeZoneScreen(2, 45)); tzScreen = tzScreen->addScreen(new TimeZoneScreen(-3, 30)); // TODO Add real timezones here CurrentTimeScreen * screen = new CurrentTimeScreen(); screen->addChildScreen(tzScreen); return screen; } 

O pensamento
Estruturar, é claro, ficou bonito, mas receio que consuma muita memória. Você tem que ir contra si mesmo e zafigachit uma grande mesa estática com ponteiros.

Vá em frente. Na minha implementação da interface, eu queria fazer algo como uma caixa de mensagem, uma mensagem curta que apareceria por um segundo ou dois e depois desapareceria. Por exemplo, se você pressionar o botão POI (Ponto de interesse) na tela com as coordenadas atuais, além de escrever o ponto na faixa, seria bom mostrar ao usuário a mensagem "Waypoint salvo" (no dispositivo original, um ícone adicional é exibido apenas por um segundo). Ou, quando a bateria estiver fraca, "anime" o usuário com uma mensagem.

imagem

Como os dados do GPS virão constantemente, não se pode falar em nenhuma função de bloqueio. Portanto, tive que inventar uma máquina de estado simples (máquina de estado), que na função loop () escolheria o que fazer - mostrar a tela ou caixa de mensagem atual.

 enum State { IDLE_DISPLAY_OFF, IDLE, MESSAGE_BOX, BUTTON_PRESSED, }; 

Também é conveniente manipular pressionamentos de botão usando a máquina de estado. Talvez fosse correto através de interrupções, mas acabou bem também. Funciona assim: se um botão foi pressionado no estado IDLE, lembre-se da hora em que foi pressionado e vá para o estado BUTTON_PRESSED. Nesse estado, esperamos até o usuário soltar o botão. Aqui podemos calcular a duração quando o botão foi pressionado. Respostas curtas (<30 ms) são simplesmente ignoradas - provavelmente isso é uma rejeição dos contatos. Viagens longas já podem ser interpretadas como pressionar um botão.

Eu pretendo usar os pressionamentos breves dos botões para ações comuns e o longo (> 1c) para funções especiais. Por exemplo, um toque breve inicia / pausa o odômetro, um toque longo redefine o contador para 0.

Talvez outros estados sejam adicionados. Assim, por exemplo, no criador de logs original após mudar para a página seguinte, os valores na tela mudam frequentemente e com menos frequência após alguns segundos - uma vez por segundo. Isso pode ser feito adicionando outro estado.

Quando o quadro estava pronto, eu já comecei a conectar o GPS. Mas aqui havia nuances que me fizeram adiar essa tarefa.

Otimização de firmware


Antes de seguir em frente, preciso me distrair com alguns detalhes técnicos. O fato é que, naquele local, comecei a me intrometer com o aumento do consumo de memória. Verificou-se que a linha declarada de forma imprudente sem o modificador PROGMEM no início do firmware é copiada para a RAM e ocupa espaço durante o tempo de execução.

Várias arquiteturas
Em poucas palavras. Em computadores grandes, a arquitetura Von Neumann é usada onde o código e os dados estão localizados no mesmo espaço de endereço. I.e. os dados da RAM e da ROM serão lidos da mesma maneira.

Microcontroladores normalmente usam a arquitetura Harvard , onde código e dados são separados. T.O. você precisa usar várias funções para ler a memória e o flash. Do ponto de vista da linguagem C / C ++, os ponteiros têm a mesma aparência, mas ao escrever um programa, precisamos saber exatamente onde exatamente para qual memória nosso ponteiro aponta e chama as funções correspondentes.

Felizmente, os desenvolvedores de bibliotecas já cuidaram disso parcialmente. A classe principal da biblioteca de exibição - Adafruit_SSD1306 é herdada da classe Print da biblioteca padrão do Arduino.

Isso nos fornece uma série completa de modificações diferentes no método de impressão - para imprimir seqüências de caracteres, caracteres únicos, números e outras coisas. Portanto, possui 2 funções separadas para linhas de impressão:

 size_t print(const __FlashStringHelper *); size_t print(const char[]); 

O primeiro sabe que você precisa imprimir uma linha de uma unidade flash e carregá-la caractere por caractere. O segundo imprime caracteres da RAM. De fato, essas duas funções direcionam um ponteiro para uma cadeia de caracteres, somente de diferentes espaços de endereço.

Durante muito tempo, procurei no código do arduino esse __FlashStringHelper para aprender como chamar a função print () desejada. Os caras fizeram o truque: eles simplesmente declararam esse tipo com a declaração de encaminhamento (sem declarar o próprio tipo) e escreveram uma macro que convertia ponteiros em linhas em um flash para o tipo __FlashStringHelper. Apenas para o compilador selecionar a função sobrecarregada necessária

 class __FlashStringHelper; #define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal))) 

Isso permite que você escreva assim:

 display.print(F(“String in flash memory”)); 


Mas não permite que você escreva assim
 const char text[] PROGMEM = "String in flash memory"; display.print(F(text)); 

E, aparentemente, a biblioteca não fornece nada que possa ser feito dessa maneira. Sei que não é bom usar partes da biblioteca privada no meu código, mas o que devo fazer? Eu desenhei minha macro, que fez o que eu precisava.

 #define USE_PGM_STRING(x) reinterpret_cast<const __FlashStringHelper *>(x) 

Então, a função de desenho do chapéu começou a ficar assim:

 void Screen::drawHeader() { display.setFont(NULL); display.setCursor(20, 0); display.print('\x1e'); display.print(USE_PGM_STRING(getSelButtonText())); display.setCursor(80, 0); display.print('\x1e'); display.print(USE_PGM_STRING(getOkButtonText())); } 

Bem, desde que entrei nas partes de baixo nível do firmware, decidi estudar mais profundamente como tudo funciona por dentro.

Em geral, os caras que criaram o Arduino precisam erguer um monumento. Eles criaram uma plataforma simples e conveniente para prototipagem e artesanato. Um grande número de pessoas com conhecimento mínimo de eletrônica e programação conseguiu entrar no mundo do Arduino. Mas tudo isso é suave e bonito ao fazer lixo, como pisca-piscas com LEDs ou ler o termômetro. Assim que você discute algo sério, imediatamente precisa entender mais profundamente do que queria desde o início.

Então, após cada biblioteca adicionada ou mesmo classe, observei a rapidez com que o consumo de memória está crescendo. Neste ponto, eu estava ocupado com mais de 14 KB de 32 KB de flash e 1300 bytes de RAM (de 2k). Cada movimento descuidado acrescentou outros 10% ao já usado. Mas ainda não conectei as bibliotecas GPS e SD / FAT32 e o gato estava chorando. Eu tive que pegar o verificador desmontador e estudar o que o compilador fez.

Eu esperava secretamente que o vinculador jogasse fora funções não utilizadas. Mas aconteceu que alguns deles o linker insere quase inteiramente. No firmware, encontrei as funções de desenho de linha e algumas outras da biblioteca de trabalho com a tela, embora no código eu obviamente não as chamasse na época. Implicitamente, eles também não devem ser chamados - por que preciso de uma função de desenho de linha se só desenhar letras de bitmaps? Mais de 5,2kb do nada (e isso não está contando as fontes).

Além da biblioteca de controle de exibição, também encontrei:

  • 2,6 kb - no SoftwareSerial (eu o puxei para o projeto em algum momento)
  • 1,6 kb - I2C
  • 1,3 kb - HardwareSerial
  • 2 kb - TinyGPS
  • 2,5 kb no arduino atual (inicialização, pinos, todos os tipos de tabelas, o timer principal para as funções millis () e delay ()),

Os números são muito indicativos, pois o otimizador está misturando seriamente o código. Algumas funções podem começar em um lugar e, em seguida, outra de outra biblioteca, chamada a partir da primeira, pode segui-la imediatamente. Além disso, ramificações separadas dessas funções podem ser localizadas na outra extremidade do flash.

Também no código eu encontrei:

  • Controle de tela por SPI (embora eu esteja conectado via I2C)
  • Métodos de classes base que eles mesmos não são chamados, porque redefinido nos herdeiros
  • Destrutores que nunca são chamados por design
  • Funções de desenho (e não todas - parte das funções que o vinculador ainda jogou)
  • malloc / free enquanto no meu código todos os objetos são essencialmente estáticos

Mas não apenas o consumo de memória flash, mas também a SRAM está crescendo aos trancos e barrancos:

  • 130 bytes - I2C
  • 100 bytes - SoftwareSerial
  • 157 bytes - Serial
  • 558 bytes - Visor (dos quais 512 é o buffer de quadros)

Não menos divertida foi a seção .data. Existem cerca de 700 bytes e essa coisa é carregada de um flash para a RAM no início. Descobriu-se que existem lugares reservados para variáveis ​​na memória e junto com os valores de inicialização. Aqui vivem as variáveis ​​e constantes que você esqueceu de declarar como const PROGMEM.

Entre isso, havia uma matriz robusta com uma "tela inicial" da tela - os valores iniciais do buffer de quadros. Teoricamente, se você exibir a tela () imediatamente após o início, poderá ver a flor e a inscrição Adafruit, mas, no meu caso, não faz sentido gastar memória flash nisso.

A seção .data também contém vtables. Eles são copiados para a memória de uma unidade flash, aparentemente por razões de eficiência no tempo de execução. Mas você precisa sacrificar uma grande quantidade de RAM - mais de uma dúzia de classes, mais de 150 bytes. Além disso, parece que não há chave do compilador que, sacrificando o desempenho, deixe tabelas virtuais na memória flash.

O que fazer sobre isso? Ainda não sei. Vai depender de como o consumo continuará a crescer. Para o bem, as ombreiras encontradas precisam ser reparadas sem piedade. Com toda a probabilidade, terei que desenhar explicitamente todas as bibliotecas no meu projeto e depois cobri-las completamente. E pode ser necessário reescrever algumas das peças de maneira diferente para otimizar a memória. Ou mude para um hardware mais poderoso. De qualquer forma, agora eu sei sobre o problema e há uma estratégia sobre como corrigi-lo.

ATUALIZAÇÃO:
Pouco progresso na eficiência de recursos. Estou atualizando esta parte, porque no próximo eu quero focar em coisas completamente diferentes.

Nos comentários, há certa perplexidade sobre o uso de C ++. Em particular, por que ele é tão ruim e mantém o vtable na preciosa RAM? Em geral, funções virtuais, construtores e destruidores são sobrecarga. Porque Vamos descobrir!

Aqui estão as estatísticas da memória em algum momento do projeto
Tamanho do programa: 15 458 bytes (usado 50% de um máximo de 30 720 bytes) (2,45 segundos)
Uso mínimo de memória: 1258 bytes (61% de um máximo de 2048 bytes)

Experiência nº 1 - reescreva para C.

Eu dei aulas, reescrevi tudo nas tabelas com ponteiros para funções.Como as telas sempre têm a mesma estrutura, todos os membros dos dados se tornaram variáveis ​​globais comuns.

Estatísticas após refatoração
Tamanho do programa: 14 568 bytes (usado 47% de um máximo de 30 720 bytes) (2,35 segundos)
Uso mínimo de memória: 1176 bytes (57% de um máximo de 2048 bytes)

Total. Ganhou 900 bytes de flash e 80 bytes de RAM. O que exatamente deixou o flash não cavou. 80 bytes de RAM são do tamanho de vtable. Todos os outros dados (alunos) permaneceram de alguma forma.

Devo dizer que não estraguei tudo - só queria ver o quadro geral sem gastar muito tempo nele. Após a refatoração, "perdi" as capturas de tela aninhadas. Com eles, o consumo seria um pouco mais.

Mas o mais importante nesse experimento é que a qualidade do código se deteriorou significativamente. O código de um funcional se espalhou por vários arquivos. Para alguns dados, "um proprietário" deixou de existir, alguns módulos começaram a entrar na memória de outros. O código tornou-se abrangente e feio.

Experiência nº 2 - extrair bytes do C ++

Recuei minha experiência e decidi deixar tudo como classes. Somente dessa vez, as telas funcionam em objetos distribuídos estaticamente. A estrutura das páginas em minhas telas é fixa. Você pode especificá-lo no estágio de compilação sem usar novo / excluir.

Tamanho do programa: 15 408 bytes (usado 50% de um máximo de 30 720 bytes) (2,60 segundos)
Uso mínimo de memória: 1273 bytes (62% de um máximo de 2048 bytes)

O consumo de RAM aumentou ligeiramente. Mas isso é, de fato, para melhor. O aumento no consumo de RAM é explicado pelo movimento de objetos do heap para uma área de memória distribuída estaticamente. I.e.de fato, os objetos foram criados antes, mas isso não foi para as estatísticas. E agora esses objetos são levados em conta explicitamente.

Mas para reduzir significativamente o consumo de flash não funcionou. O código ainda contém os próprios construtores, que ainda são chamados na inicialização. I.e.O compilador não pôde "executá-los" com antecedência e colocar todos os valores em áreas pré-alocadas. E ainda havia destruidores no código, embora seja claro para o ouriço que os objetos nunca serão excluídos.

Na tentativa de economizar pelo menos um pouco, apaguei todos os destruidores da hierarquia e, em particular, o destruidor virtual na classe base. A idéia era liberar alguns bytes em cada tabela. E então uma surpresa me esperava:

Tamanho do programa: 14 704 bytes (usado 48% de um máximo de 30 720 bytes) (2,94 segundos)
Uso mínimo de memória: 1211 bytes (59% de um máximo de 2048 bytes)

Aconteceu que a tabela não seguia um ponteiro, mas já era 2. Além disso, ambos tinham a ver com o destruidor. Apenas um destruidor está vazio (visível para objetos na pilha) e o outro com uma chamada gratuita, visível para objetos no heap (-12 bytes de RAM). Além disso, as variáveis ​​associadas ao quadril (8 bytes) e os rótulos dos objetos que nunca são criados (Tela, Tela-pai - 40 bytes) desapareceram.O

consumo de flash diminuiu significativamente - em 700 bytes. Não apenas os próprios destruidores, mas também as implementações malloc / free / new / delete desapareceram. 700 bytes para um destruidor virtual vazio! 700 bytes, Carl!

Isso não acabaria, aqui estão todos os números em um só lugar

WasCC ++
Flash15 45814.56814.704
RAM125811761211


Conclusão: o consumo em C ++ acabou sendo quase o mesmo que em C. Mas, ao mesmo tempo, encapsulamento, herança e polimorfismo são poder. Estou pronto para pagar a mais por isso com algum aumento no consumo. Talvez eu simplesmente não possa escrever lindamente em C, mas por que, se eu posso escrever lindamente em C ++?

Posfácio


Inicialmente, eu queria escrever um artigo no final do projeto. Porém, como as notas se acumulam em alta velocidade à medida que o trabalho avança, o artigo ameaça ser muito grande. Então eu decidi dividi-lo em várias partes. Nesta parte, falei sobre as etapas preparatórias: entender o que realmente quero, escolher uma plataforma, implementar uma estrutura de aplicativo.

Na próxima parte, pretendo avançar para a implementação da funcionalidade básica - trabalhar com GPS. Eu já encontrei alguns ancinhos interessantes sobre os quais gostaria de contar.

Por mais de 10 anos eu não tenho programado seriamente para microcontroladores. Aconteceu que eu estava um pouco estragado pela abundância de recursos de computadores grandes e está limitado às realidades do ATMega32. Portanto, tive que pensar em várias opções de backup, como aparar a funcionalidade das bibliotecas ou redesenhar o aplicativo em nome do uso eficiente da memória. Também não excluo a possibilidade de mudar para controladores mais poderosos - ATMega64 ou algo da linha STM32.

Por estilo, o artigo acaba sendo algo como uma revista de construção. E ficarei feliz em fazer comentários construtivos - não é tarde para mudar nada. Quem quiser pode participar do meu projeto no github .

O fim da primeira parte.

Segunda parte

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


All Articles