Registrador de bricolaje multifuncional. Parte 1

imagen

Soy propietario de un dispositivo maravilloso: el registrador GPS Holux M-241. La cosa es muy conveniente y útil cuando se viaja. Con la ayuda de un registrador, escribo una ruta GPS de un viaje, a lo largo de la cual puedes ver tu camino en detalle, y también adjunto las fotos que tomas a las coordenadas GPS. También tiene una pequeña pantalla que muestra información adicional: horas, velocidad actual, altitud y dirección, odómetro y mucho más. Aquí una vez escribí una breve reseña.

Con todas las ventajas de una pieza de hierro, comencé a salir de ella. Echo de menos algunas cosas pequeñas pero útiles: algunos odómetros, que muestran la velocidad vertical, que miden los parámetros de una sección de pista. Parecen cosas pequeñas, pero la compañía Holux encontró que esto no es lo suficientemente útil para la implementación en el firmware. Además, no me gustan algunos parámetros del hardware, y algunas cosas se han quedado obsoletas durante 10 años ...

En algún momento me di cuenta de que yo mismo podría hacer un registrador con las funciones que necesito. Afortunadamente, todos los componentes necesarios son bastante baratos y asequibles. Comencé a hacer mi implementación basada en Arduino. Debajo del corte, un diario de construcción donde intenté pintar mis soluciones técnicas.

Definiendo características


Muchos se preguntarán por qué necesito construir mi propio registrador, si es seguro que hay algo listo para fabricantes eminentes. Posiblemente Para ser honesto, realmente no lo busqué. Pero seguro que faltará algo. En cualquier caso, este proyecto es un fan para mí. ¿Por qué no comenzamos a construir el dispositivo de nuestros sueños?

Entonces, por lo que aprecio mi Holux M-241.

  • La pantalla hace una "caja negra", cuyos resultados están disponibles solo después del viaje, una herramienta muy conveniente, cuyas lecturas están disponibles aquí y ahora. Tener una pantalla hace posible casi todas las funciones de esta lista.
  • Un reloj es útil en sí mismo. En los viajes con GPS, el registrador que cuelga de una cuerda alrededor de su cuello a menudo resulta estar más cerca que un teléfono celular en su bolsillo o en una mochila. El reloj admite todas las zonas horarias (aunque con cambio manual)
  • El botón POI le permite marcar la coordenada actual en la pista. Por ejemplo, para notar un punto de referencia que se ha deslizado fuera de la ventana del autobús, sobre el que quiero buscar en Google más tarde.
  • Con el odómetro, puede medir la distancia recorrida desde algún punto. Por ejemplo, la distancia recorrida por día o la longitud de una pista.
  • La velocidad, altitud y dirección actuales lo ayudan a encontrarse en el espacio
  • La capacidad de supervivencia de 12-14 horas con una batería AA en la mayoría de los casos le permite no pensar en problemas de suministro de energía. Es decir casi siempre carga suficiente para un día completo de viaje.
  • Compacto y fácil de usar : las cosas en el mundo moderno son muy agradables

Sin embargo, algunas cosas podrían hacerse un poco mejor:

  • — , . .

    . ( 5 ). . . , . , .

    6 . . . .

  • — . 5 , . .

  • .

  • — . .

  • . . , GPS . . .

  • . , , , , , . , , .

  • . — . “” +- 50. “” . 10 .

  • 38400. , 2017 COM . 2 20 .

    . . BT747, .

  • El tamaño de la unidad flash es de solo 2 MB. Por un lado, esto es suficiente para un viaje de dos semanas con puntos de ahorro cada 5 segundos. Pero, en primer lugar, el formato empaquetado interno
    requiere conversión y, en segundo lugar, no permite aumentar el volumen
  • El dispositivo de almacenamiento masivo por alguna razón ahora no está de moda. Las interfaces modernas intentan ocultar el hecho de la presencia de archivos. He estado con computadoras durante 25 años, y trabajar con archivos directamente es mucho más conveniente para mí que de ninguna otra manera.

No hay nada aquí que no pueda realizarse sin esfuerzos significativos.

Algo diferente No lo uso yo mismo, pero de repente alguien es útil:

  • Muestra las coordenadas actuales (latitud, longitud)
  • Se dibujan diferentes iconos en el lado izquierdo de la pantalla, cuya esencia ni siquiera puedo recordar sin un manual.
  • Hay cambio de metros / km - pies / millas.
  • Bluetooth: el registrador se puede conectar a teléfonos móviles sin GPS.
  • La distancia absoluta al punto.
  • Registro por tiempo (cada N segundos) o por distancia (cada X metros).
  • Soporte para diferentes idiomas.

Elegir hierro


Los requisitos están más o menos definidos. Es hora de entender cómo se puede implementar todo esto. Los componentes principales que tendré son:

  • Microcontrolador : no tengo planes para ningún algoritmo computacional sofisticado, por lo que la potencia de procesamiento del núcleo no es particularmente importante. Tampoco tengo requisitos especiales para el llenado; un conjunto de periféricos estándar funcionará.

    A la mano solo había una dispersión de arduinoes diversos, así como un par de stm32f103c8t6. Decidí comenzar con AVR, que conozco bien a nivel de controlador / registros / periféricos. Si me encuentro con restricciones, habrá una razón para sentir el STM32.

  • GPS NEO6MV2, Beitan BN-800 Beitan BN-880. . , — . — BN-800 , BN-880 . BN-880.

  • Pantalla : el original utiliza una pantalla LCD de 128 x 32 con luz de fondo. No encontré exactamente lo mismo. Compré un OLED de 0.91 "en el controlador SSD1306 y una pantalla LCD de 1.2" en el controlador ST7565R . Decidí comenzar desde el principio, porque Es más fácil conectarse con un peine estándar según I2C o SPI. Pero es un poco más pequeño en comparación con el original, y tampoco funcionará en él para mostrar constantemente la imagen por razones de eficiencia de combustible. La segunda pantalla debe ser menos glotona, pero debe soldar un conector complicado y descubrir cómo alimentar la luz de fondo.

De las pequeñas cosas:

  • Botones una vez compró una bolsa entera;
  • Escudo con tarjeta SD - también a la mano;
  • Compré un par de controladores de carga diferentes para baterías de litio, pero todavía no lo entendía.

Decidí diseñar la placa al final, cuando el firmware está listo. En este momento, finalmente decidiré sobre los componentes principales y el esquema para su inclusión. En la primera etapa, decidí depurar en el tablero conectando los componentes mediante cables de conexión.

Pero primero debe decidir sobre un tema muy importante: la nutrición de los componentes. Me pareció razonable alimentar todo desde 3.3V: el GPS y la pantalla solo en él y saber cómo funcionar. Este es también el voltaje nativo para USB y SD. Además, el circuito puede alimentarse con una lata de litio.

La elección recayó en el Arduino Pro Mini, que se puede encontrar en la versión 8MHz / 3.3V. Pero ella no tenía USB a bordo: tuve que usar un adaptador USB-UART.

Primeros pasos


Inicialmente, el proyecto fue creado en Arduino IDE. Pero para ser sincero, mi idioma no se atreve a llamarlo IDE, como un editor de texto con un compilador. En cualquier caso, después de Visual Studio, en el que he estado trabajando durante los últimos 13 años, no puedo hacer nada serio en Arduino IDE sin lágrimas y matyuk.

Afortunadamente, hay un Atmel Studio gratuito, en el que incluso Visual Assist está integrado de fábrica. El programa sabe todo lo que se necesita, todo es familiar y está en su lugar. Bueno, casi todo (no encontré cómo compilar solo un archivo, por ejemplo, para verificar la sintaxis)

imagen

comencé desde la pantalla; esto es necesario para depurar el esqueleto del firmware y luego llenarlo de funcionalidad. Se detuvo en la primera biblioteca disponible para Adafruit SSD1306 . Ella sabe todo lo que se necesita y proporciona una interfaz muy simple.

Jugado con fuentes. Resultó que una fuente puede tomar hasta 8kb (el tamaño de las letras es de 24pt) - no se puede andar especialmente en un controlador de 32kb. Se necesitan fuentes grandes, por ejemplo, para mostrar el tiempo.

Código de muestra de fuente
#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;
}


Las fuentes completas con la biblioteca son muy torpes. La fuente monoespacio resultó ser muy ancha: la línea "12:34:56" no encaja, Serif: todos los números son de diferentes pesos. A menos que la fuente estándar de 5x7 en la biblioteca se vea comestible.

imagen

imagen

Resultó que estas fuentes se convirtieron de algunas fuentes ttf de código abierto que simplemente no están optimizadas para pequeñas resoluciones.

Tuve que dibujar mis fuentes. Más precisamente, primero, desenterrar símbolos individuales de los terminados. El símbolo ':' en la tabla ASCII es muy útil justo después de los números y se puede comprar en un bloque. También es conveniente que pueda hacer una fuente no para todos los caracteres, sino solo para un rango, por ejemplo, de 0x30 ('0') a 0x3a (':'). T.O. de FreeSans18pt7b resultó ser una fuente muy compacta solo para los caracteres necesarios. Es cierto que tuve que ajustar ligeramente el ancho para que el texto se ajuste al ancho de la pantalla.

Sub-Font 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 };

Resultó que la fuente de 18 puntos tiene en realidad 25 píxeles de alto. Debido a esto, cabe ligeramente en otra inscripción. La

imagen

pantalla invertida, por cierto, ayuda a comprender dónde están realmente los bordes del área de dibujo y cómo se encuentra la línea en relación con este borde: la pantalla tiene marcos muy grandes.

Busqué en Google durante mucho tiempo las fuentes preparadas, pero no encajaban ni en tamaño, ni en forma, ni en contenido. Por ejemplo, en Internet, un eje de fuente 8x12 (volcados de generadores de caracteres de tarjetas VGA). Pero, de hecho, estas fuentes son 6x8, es decir muchos paseos espaciales; en el caso de una resolución y un tamaño tan pequeños como los míos, es fundamental.

Tuve que dibujar mis propias fuentes, ya que el formato de fuente de la biblioteca Adafruit es muy simple. Preparé la imagen en Paint.net: simplemente dibujé las letras en la fuente correcta, luego las corregí un poco con un lápiz. Guardé la imagen como png y luego la envié rápidamente al script de Python escrito en mi rodilla. Este script generó un código semiacabado que ya apunta las reglas en el IDE directamente en los códigos hexadecimales.

imagen

Por ejemplo, así es como se ve el proceso de creación de una fuente monoespaciada 8x12 con minúsculas y espaciado entre líneas. Cada carácter al final resultó ser aproximadamente 7x10, y por defecto ocupaba 10 bytes. Sería posible empaquetar cada carácter en 8-9 bytes (la biblioteca lo permite), pero no me molesté. Además, en este formulario, puede editar píxeles individuales directamente en el código.

Font 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 };


Marco


El dispositivo original proporciona una interfaz muy simple y conveniente. La información se agrupa en categorías que se muestran desde páginas individuales (pantallas). Con el botón, puede recorrer las páginas y usar el segundo botón para seleccionar el elemento actual o realizar la acción indicada en la firma debajo del botón. Este enfoque me parece muy conveniente y no hay necesidad de cambiar nada.

Me gusta la belleza de OOP, porque inmediatamente deslumbré una pequeña interfaz, cada página implementa la interfaz como lo requiere. La página sabe cómo dibujarse e implementa la reacción a los botones.

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);
};

Los botones pueden realizar varias acciones según la pantalla actual. Por lo tanto, la parte superior de la pantalla con una altura de 8 píxeles, asigné a las etiquetas de los botones. El texto para las firmas depende de la pantalla actual y lo devuelven las funciones virtuales getSelButtonText () y getOkButtonText (). También en el encabezado se mostrarán elementos como la intensidad de la señal GPS y la carga de la batería. Las pantallas ¾ restantes están disponibles para obtener información útil.

Como dije, las pantallas se pueden voltear, lo que significa que en algún lugar debe haber una lista de objetos para diferentes páginas. En qué más de uno: las pantallas se pueden anidar, como un submenú. Incluso comencé la clase ScreenManager, que supuestamente administraba estas listas, pero luego encontré la solución más fácil.

Entonces cada pantalla simplemente tiene un puntero a la siguiente. Si la pantalla le permite ingresar al submenú, entonces agrega un puntero más a la pantalla de este submenú

class Screen
{
	Screen * nextScreen;
};

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

Por defecto, el controlador de botones simplemente llama a la función de cambio de pantalla, pasándole el puntero deseado. La función resultó ser trivial: simplemente cambió el puntero a la pantalla actual. Para asegurar el anidamiento de las pantallas, hice una pequeña pila. Entonces, el administrador de pantalla completo cabe en 25 líneas y 4 funciones pequeñas.

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--;
}

Es cierto que el código para llenar estas estructuras no se ve muy bien, pero hasta ahora no se ha inventado mejor.

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;
}

El pensamiento
, , , . .

Adelante En mi implementación de la interfaz, quería hacer algo como un cuadro de mensaje, un mensaje corto que aparecería por un segundo o dos, y luego desaparecería. Por ejemplo, si presiona el botón POI (Punto de interés) en la pantalla con las coordenadas actuales, además de escribir el punto en la pista, sería bueno mostrarle al usuario el mensaje "Waypoint guardado" (en el dispositivo original, solo se muestra un icono adicional por un segundo). O, cuando la batería está baja, "animar" al usuario con un mensaje.

imagen

Dado que los datos del GPS llegarán constantemente, no se puede hablar de ninguna función de bloqueo. Por lo tanto, tuve que inventar una máquina de estado simple (máquina de estado), que en la función loop () elegiría qué hacer: mostrar la pantalla actual o el cuadro de mensaje.

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

También es conveniente manejar las pulsaciones de botones usando la máquina de estado. Quizás sería correcto a través de interrupciones, pero también resultó bien. Funciona así: si se presionó un botón en el estado IDLE, recuerde la hora en que se presionó y vaya al estado BUTTON_PRESSED. En este estado, esperamos hasta que el usuario suelte el botón. Aquí podemos calcular la duración cuando se presionó el botón. Las respuestas cortas (<30 ms) simplemente se ignoran; lo más probable es que se trate de un rebote de contactos. Los viajes largos ya se pueden interpretar como presionar un botón.

Planeo usar ambas pulsaciones cortas de botones para acciones ordinarias y largas (> 1c) para funciones especiales. Por ejemplo, una presión breve inicia / pausa el odómetro, una presión larga restablece el contador a 0.

Quizás se agregarán otros estados. Entonces, por ejemplo, en el registrador original después de cambiar a la página siguiente, los valores en la pantalla cambian con frecuencia, y con menos frecuencia después de un par de segundos, una vez por segundo. Esto se puede hacer agregando otro estado.

Cuando el marco estuvo listo, ya comencé a conectar el GPS. Pero aquí hubo matices que me hicieron posponer esta tarea.

Optimización de firmware


Antes de continuar, necesito distraerme con algunos detalles técnicos. El hecho es que en este lugar comencé a toparme con el aumento del consumo de memoria. Resultó que la línea declarada imprudentemente sin el modificador PROGMEM al comienzo del firmware se copia en la RAM y ocupa espacio allí durante todo el tiempo de ejecución.

Varias arquitecturas
. . .. .

, , , . .. . C/C++ , .

Afortunadamente, los desarrolladores de bibliotecas ya se han ocupado en parte de esto. La clase principal de la biblioteca de visualización: Adafruit_SSD1306 se hereda de la clase Print de la biblioteca estándar de Arduino.

Esto nos proporciona una serie completa de modificaciones diferentes del método de impresión: para imprimir cadenas, caracteres individuales, números y algo más. Por lo tanto, tiene 2 funciones separadas para imprimir líneas:


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

El primero sabe que necesita imprimir una línea desde una unidad flash y la carga carácter por carácter. El segundo imprime caracteres de la RAM. De hecho, ambas funciones llevan un puntero a una cadena, solo desde diferentes espacios de direcciones.

Durante mucho tiempo busqué en el código arduino este __FlashStringHelper para aprender cómo llamar a la función print () deseada. Resultó que los chicos hicieron el truco: simplemente declararon este tipo con la declaración directa (sin declarar el tipo en sí) y escribieron una macro que arrojaba punteros a líneas en un instante al tipo __FlashStringHelper. Solo para que el compilador seleccione la función sobrecargada necesaria

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

Esto te permite escribir así:

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


Pero no permite escribir como
const char text[] PROGMEM = "String in flash memory"; 
display.print(F(text));

Y, aparentemente, la biblioteca no proporciona nada que pueda hacerse de esa manera. Sé que no es bueno usar piezas de biblioteca privada en mi código, pero ¿qué debo hacer? Dibujé mi macro, que hizo lo que necesitaba.

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

Entonces la función de dibujo del sombrero comenzó a verse así:

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()));
}

Bueno, desde que me metí en las piezas de bajo nivel del firmware, decidí estudiar más a fondo cómo funciona todo dentro.

En general, los chicos que inventaron Arduino necesitan erigir un monumento. Crearon una plataforma simple y conveniente para la creación de prototipos y manualidades. Una gran cantidad de personas con un conocimiento mínimo de electrónica y programación pudieron ingresar al mundo de Arduino. Pero todo esto es suave y hermoso al hacer basura, como luces intermitentes con LED o leer el termómetro. Tan pronto como te balancees en algo serio, inmediatamente tienes que entender más profundo de lo que querías desde el principio.

Entonces, después de cada biblioteca o clase agregada, noté cuán rápido está creciendo el consumo de memoria. En este punto, estaba ocupado con más de 14 KB de 32 KB de flash y 1300 bytes de RAM (de 2k). Cada movimiento descuidado agregó otro 10 por ciento al ya usado. Pero todavía no he conectado realmente las bibliotecas GPS y SD / FAT32, y el gato estaba llorando. Tuve que recoger el comprobador de desensamblador y estudiar lo que hizo el compilador.

Secretamente esperaba que el enlazador eliminara las funciones no utilizadas. Pero resultó que algunos de ellos el enlazador se inserta casi por completo. En el firmware, encontré las funciones de dibujo lineal y algunas otras de la biblioteca para trabajar con la pantalla, aunque en el código obviamente no las llamé en ese momento. Implícitamente, tampoco deberían llamarse: ¿por qué necesito una función de dibujo lineal si solo dibujo letras de mapas de bits? Más de 5.2kb de la nada (y eso sin contar las fuentes).

Además de la biblioteca de control de visualización, también encontré:

  • 2.6 kb - en SoftwareSerial (en algún momento lo incluí en el proyecto)
  • 1.6 kb - I2C
  • 1.3 kb - Serie de hardware
  • 2 kb - TinyGPS
  • 2.5 kb en el arduino real (inicialización, pines, todo tipo de tablas, el temporizador principal para las funciones millis () y delay ()),

Los números son muy indicativos, ya que El optimizador está mezclando seriamente el código. Algunas funciones pueden comenzar en un lugar, y luego otra de otra biblioteca, que se llama desde la primera, puede seguirla de inmediato. Además, se pueden ubicar ramas separadas de estas funciones en el otro extremo del flash.

También en el código que encontré:

  • Control de pantalla por SPI (aunque lo tengo conectado a través de I2C)
  • Métodos de clases base que a ellos mismos no se les llama, porque redefinido en los herederos
  • Destructores que nunca son llamados por diseño
  • Funciones de dibujo (y no todas: parte de las funciones que el enlazador aún lanzó)
  • malloc / free, mientras que en mi código todos los objetos son esencialmente estáticos

Pero no solo el consumo de memoria flash, sino también SRAM está creciendo a pasos agigantados:

  • 130 bytes - I2C
  • 100 bytes - SoftwareSerial
  • 157 bytes - Serie
  • 558 bytes - Pantalla (de los cuales 512 es el búfer de trama)

No menos entretenida fue la sección .data. Hay alrededor de 700 bytes y esta cosa se carga desde un flash en la RAM al principio. Resultó que hay lugares reservados para las variables en la memoria, y junto con los valores de inicialización. Aquí viven las variables y constantes que olvidó declarar como const PROGMEM.

Entre esto había una gran matriz con una "pantalla de bienvenida" de la pantalla: los valores iniciales del búfer de trama. Teóricamente, si realiza la pantalla () inmediatamente después del inicio, puede ver la flor y la inscripción Adafruit, pero en mi caso no tiene sentido gastar memoria flash en esto.

La sección .data también contiene vtables. Se copian en la memoria desde una unidad flash, aparentemente por razones de eficiencia en tiempo de ejecución. Pero tiene que sacrificar una porción de RAM bastante grande: más de una docena de clases de más de 150 bytes. Además, parece que no hay una clave del compilador que, sacrificando el rendimiento, deje las tablas virtuales en la memoria flash.

¿Qué hacer al respecto? Aún no lo sé Dependerá de cómo el consumo continuará creciendo. Para que las jambas se encuentren bien deben repararse sin piedad. Con toda probabilidad, tendré que dibujar todas las bibliotecas en mi proyecto explícitamente y luego cubrirlas a fondo. Y es posible que también deba volver a escribir algunas de las piezas de manera diferente para optimizar la memoria. O cambie a un hardware más potente. En cualquier caso, ahora sé sobre el problema y hay una estrategia sobre cómo solucionarlo.

ACTUALIZACIÓN
Poco progreso en eficiencia de recursos. Estoy haciendo una actualización de esta parte, porque en la próxima quiero centrarme en cosas completamente diferentes.

En los comentarios hay cierto desconcierto sobre el uso de C ++. En particular, ¿por qué es tan malo y mantiene vtable en la preciosa RAM? En general, las funciones virtuales, los constructores y los destructores son gastos generales. Por qué ¡Vamos a resolverlo!

Aquí están las estadísticas sobre la memoria en alguna etapa del proyecto
: Tamaño del programa: 15 458 bytes (utilizado el 50% de un máximo de 30 720 bytes) (2.45 segundos)
Uso mínimo de memoria: 1258 bytes (61% de un máximo de 2048 bytes)

Experimento No. 1 - reescribir a C.

Lancé clases, reescribí todo en tablas con punteros a funciones. Como las pantallas siempre tienen la misma estructura, todos los miembros de datos se han convertido en variables globales ordinarias.

Estadísticas después de refactorizar
Tamaño del programa: 14 568 bytes (utilizado el 47% de un máximo de 30 720 bytes) (2,35 segundos)
Uso mínimo de memoria: 1176 bytes (57% de un máximo de 2048 bytes)

Total. Ganó 900 bytes de flash y 80 bytes de RAM. Lo que dejó exactamente el flash no cavó. 80 bytes de RAM es solo el tamaño de vtable. Todos los demás datos (miembros de la clase) de alguna manera permanecieron.

Debo decir que no estropeé todo, solo quería ver el panorama general sin pasar mucho tiempo en ello. Después de refactorizar, "perdí" capturas de pantalla anidadas. Con ellos, el consumo sería un poco más.

Pero lo más importante en este experimento es que la calidad del código se ha deteriorado significativamente. El código de un funcional se ha extendido por varios archivos. Para algunos datos, "un propietario" dejó de existir, algunos módulos comenzaron a escalar en la memoria de otros. El código se ha vuelto amplio y feo.

Experimento 2: exprimir bytes de C ++

. Revertí mi experimento, decidí dejar todo como clases. Solo que esta vez, las pantallas funcionan en objetos distribuidos estáticamente. La estructura de las páginas en mis pantallas es fija. Puede especificarlo en la etapa de compilación sin usar new / delete.

Tamaño del programa: 15 408 bytes (utilizado el 50% de un máximo de 30 720 bytes) (2,60 segundos)
Uso mínimo de memoria: 1273 bytes (62% de un máximo de 2048 bytes)

El consumo de RAM ha aumentado ligeramente. Pero esto es, de hecho, para mejor. El aumento en el consumo de RAM se explica por el movimiento de objetos del montón a un área de memoria distribuida estáticamente. Es decir de hecho, los objetos se crearon antes, pero esto no fue a las estadísticas. Y ahora estos objetos se tienen en cuenta explícitamente.

Pero reducir significativamente el consumo de flash no funcionó. El código aún contiene los propios constructores, que todavía se llaman al inicio. Es decir El compilador no pudo "ejecutarlos" por adelantado y poner todos los valores en áreas preasignadas. Y todavía había destructores en el código, aunque para el erizo está claro que los objetos nunca se eliminarán.

En un intento de ahorrar al menos un poco, eliminé todos los destructores en la jerarquía, y en particular el destructor virtual en la clase base. La idea era liberar un par de bytes en cada vtable. Y luego me esperaba una sorpresa:

Tamaño del programa: 14 704 bytes (utilizado el 48% de un máximo de 30 720 bytes) (2,94 segundos)
Uso mínimo de memoria: 1211 bytes (59% de un máximo de 2048 bytes)

Resultó que vtable había desaparecido no por un puntero, sino ya por 2. ¿Qué tiene que ver con el destructor? Solo un destructor está vacío (visible para los objetos en la pila), y el otro con una llamada gratuita, visible para los objetos en el montón (-12 bytes de RAM). Además, las variables asociadas con la cadera (8 bytes) y las etiquetas de los objetos que nunca se crean (Screen, ParentScreen - 40 bytes) desaparecieron

El consumo de flash disminuyó significativamente, en 700 bytes. No solo los destructores en sí, sino también las implementaciones malloc / free / new / delete han desaparecido. ¡700 bytes para un destructor virtual vacío! 700 bytes, Carl!

Eso no iría de un lado a otro, aquí están todos los números en un solo lugar

EraCC ++
Flash15 45814,56814,704
RAM125811761211


En pocas palabras: el consumo en C ++ resultó ser casi el mismo que en C. Pero al mismo tiempo, la encapsulación, la herencia y el polimorfismo son poder. Estoy listo para pagar de más por esto con algún aumento en el consumo. Tal vez simplemente no puedo escribir bellamente en C, pero ¿por qué si puedo escribir bellamente en C ++?

Epílogo


Inicialmente, quería escribir un artículo al final del proyecto. Pero como las notas se acumulan a gran velocidad a medida que avanza el trabajo, el artículo amenaza con ser muy grande. Entonces decidí dividirlo en varias partes. En esta parte, hablé sobre las etapas preparatorias: entender lo que realmente quiero, elegir una plataforma, implementar un marco de aplicación.

En la siguiente parte, planeo pasar a la implementación de la funcionalidad básica: trabajar con GPS. Ya me he encontrado con un par de rastrillos interesantes sobre los que me gustaría contar.

Durante más de 10 años no he programado seriamente para microcontroladores. Resultó que estaba un tanto mimado por la abundancia de recursos de las computadoras grandes y está limitado a las realidades de ATMega32. Por lo tanto, tuve que pensar en varias opciones de copia de seguridad, como recortar la funcionalidad de las bibliotecas o rediseñar la aplicación en nombre del uso eficiente de la memoria. Tampoco excluyo la posibilidad de cambiar a controladores más potentes: ATMega64 o algo de la línea STM32.

Por estilo, el artículo resulta ser algo así como una revista de construcción. Y estaré encantado de hacer comentarios constructivos: no es demasiado tarde para cambiar nada. Los que lo deseen pueden unirse a mi proyecto en el github .

El final de la primera parte.

Segunda parte

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


All Articles