多功能DIY记录仪。第一部分

image

我是一个很棒的设备的所有者-GPS记录仪Holux M-241。出差时这东西非常方便和有用。在记录仪的帮助下,我编写了旅行的GPS轨迹,然后您可以沿着该轨迹详细查看自己的路线,并将拍摄的照片附加到GPS坐标上。他还有一个小屏幕,显示其他信息-小时,当前速度,高度和方向,里程表等等。在这里,我曾经写过一篇简短的评论。

凭借一块烙铁的所有优点,我开始从中脱颖而出。我想念一些小但有用的东西:一些里程表,显示垂直速度,测量轨道部分的参数。似乎没什么大不了的,但是Holux公司发现这对于在固件实施中没有足够的帮助。另外,我不喜欢硬件的某些参数,并且有些东西已经过时了10年……

在某个时候,我意识到我自己可以制作具有所需功能的记录器。幸运的是,所有必需的组件都非常便宜且负担得起。我开始基于Arduino进行实现。在剪下的地方,我尝试绘制一本建筑日记,画出自己的技术解决方案。

定义功能


许多人会问,如果可以肯定的话,为什么我需要建立自己的记录器,以供知名制造商使用。可能吧 老实说,我并不是真的在寻找它。但是可以肯定会缺少一些东西。无论如何,这个项目对我来说是粉丝。我们为什么不开始构建我们的梦想设备?

因此,对于我对Holux M-241的感谢。

  • 屏幕上有一个“黑匣子”,这是一个非常方便的工具,其结果仅在旅行后提供,该工具的读数现在和现在都可用。有了屏幕,此列表上的几乎所有功能都将成为可能。
  • 手表本身很有用。在GPS旅途中,记录仪悬挂在脖子上的绳子上常常比口袋或背包中的手机要近。手表支持所有时区(尽管有手动切换功能)
  • POI按钮可让您在轨道上标记当前坐标。例如,要注意一个地标已经滑到公交车窗外,我稍后想在该地标上搜索。
  • 使用里程表,您可以测量从某个点经过的距离。例如,每天行进的距离或轨道的长度。
  • 当前的速度,高度和方向可帮助您在太空中找到自己
  • 在大多数情况下,一节AA电池可生存12-14小时,因此您无需考虑电源问题。几乎总是要为一整天的旅行支付足够的费用。
  • 紧凑且易于使用 -现代世界中的一切都非常好

但是,有些事情可以做得更好:

  • — , . .

    . ( 5 ). . . , . , .

    6 . . . .

  • — . 5 , . .

  • .

  • — . .

  • . . , GPS . . .

  • . , , , , , . , , .

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

  • 38400. , 2017 COM . 2 20 .

    . . BT747, .

  • 闪存驱动器的大小仅为2MB。一方面,这足以进行为期两周的旅行,每5秒即可节省一次积分。但是首先,内部打包格式
    需要转换,其次,它不允许增加音量
  • 由于某种原因,大容量存储设备现在不流行。现代接口试图隐藏文件存在的事实。我已经在计算机上使用25年了,与其他任何方式相比,直接处理文件对我来说要方便得多。

没有任何努力,这里是无法实现的。

没什么不同。我自己没有使用它,但是突然有人有用:

  • 显示当前坐标(纬度,经度)
  • 屏幕左侧绘制了不同的图标,没有手册我什至不记得其本质。
  • 有转换米/公里-英尺/英里。
  • 蓝牙-记录仪可以连接到没有GPS的手机。
  • 到点的绝对距离。
  • 按时间(每N秒)或按距离(每X米)记录。
  • 支持不同的语言。

选择铁


需求或多或少地被定义。现在该了解如何实现所有这些。我将拥有的主要组件是:

  • 微控制器 -我没有任何复杂的计算算法的计划,因此内核的处理能力并不是特别重要。我对填充也没有特殊要求-可以使用一组标准外围设备。

    眼前只有杂散的arduinoes,以及几个stm32f103c8t6。我决定从AVR开始,我在控制器/寄存器/外围设备方面对此非常了解。如果我遇到限制-将有理由感到STM32。

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

  • 屏幕 -原稿使用带背光的128 x 32 LCD。我没有发现完全一样。在SSD1306控制器上购买了0.91英寸OLED,在ST7565R控制器上购买1.2英寸的LCD屏幕我决定从第一个开始,因为 根据I2C或SPI,可以更轻松地连接标准梳子。但它比原始尺寸要小一些,并且由于燃油效率的原因,它也无法在其上连续显示图像。第二个显示器应该比较不粘,但是您需要为其焊接一个棘手的连接器,并弄清楚如何为背光供电。

从小事情:

  • Buttons曾经买了一整个袋子;
  • 带有SD卡的屏蔽层-也位于手边;
  • 我为锂电池购买了一对不同的充电控制器,但我仍然不明白。

我决定在固件准备就绪时最终设计电路板。到这个时候,我将最终决定主要组成部分以及包括这些组成部分的方案。在第一阶段,我决定通过使用跳线连接组件在面包板上进行调试。

但是首先,您需要决定一个非常重要的问题-成分的营养。在我看来,用3.3V的电压为所有电源供电是合理的:GPS和仅在其上的屏幕并知道如何工作。这也是USB和SD的固有电压。此外,该电路可以用一个锂罐供电,

选择范围为Arduino Pro Mini,该版本的版本为8MHz / 3.3V。但是她没有板载USB-我必须使用USB-UART适配器。

第一步


最初,该项目是在Arduino IDE中创建的。但是老实说,我的语言不敢称它为IDE-就像带有编译器的文本编辑器一样。无论如何,在过去的13年间我一直在工作的Visual Studio之后,在泪流满面的情况下,我无法在Arduino IDE中做任何认真的事情。

幸运的是,有一个免费的Atmel Studio,其中甚至还内置了Visual Assist!该程序知道所需的一切,熟悉的一切以及它的位置。好吧,几乎所有东西(我都没有找到只编译一个文件的方法,例如,检查语法)

image

都是从屏幕开始的-这是调试固件框架,然后为其添加功能的必要步骤。他停在Adafruit SSD1306第一个可用库中。她知道所需的一切,并提供了非常简单的界面。

玩字体。事实证明,一种字体最多可占用8kb(字母的大小为24pt)-您尤其不能在32kb的控制器中四处走动。例如,需要大字体来显示时间。

字体样例代码
#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;
}


带有库的字体非常笨拙。等宽字体非常宽-“ Serif”行“ 12:34:56”不适合-所有数字的权重都不同。除非库中的标准5x7字体看起来可食用。

image

image

原来,这些字体是从某些开源ttf字体转换而来的,这些字体根本没有针对小分辨率进行优化。

我不得不画我的字体。更准确地说,首先,从完成的符号中找出单个符号。ASCII码表中的符号“:”在数字后面非常有用,可以在一个代码块中购买。也可以方便地使字体不是针对所有字符,而是针对某个范围,例如从0x30('0')到0x3a(':')。T.O. 从FreeSans18pt7b可以看出,它仅针对必需的字符制作了非常紧凑的字体。是的,我必须稍微调整宽度,以使文本适合屏幕的宽度。

子字体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 };

事实证明18pt字体实际上是25像素高。因此,它有点适合另一个题词

image

,顺便说一下,倒置显示器有助于理解绘图区域的边界实际在哪里以及线条相对于该边界的位置-显示器具有非常大的框架。

谷歌搜索了很长时间的现成字体,但它们的大小,形状或内容都不适合。例如,在Internet上,一个8x12字体轴(VGA卡字符发生器的转储)。但实际上,这些字体是6x8,即很多太空行走-在像我这样小的分辨率和尺寸的情况下,这很关键。

我必须绘制自己的字体,因为Adafruit库的字体格式非常简单。我在Paint.net中准备了图片-我只是简单地以正确的字体绘制了字母,然后用铅笔对其进行了一些校正。我将图像保存为png,然后将其快速发送到膝盖上写的python脚本。该脚本生成了一个半成品代码,该代码已经在IDE中的十六进制代码中正确地指定了规则。

image

例如,这就是创建具有较小字母和行距的等宽字体8x12的过程。最后的每个字符大约为7x10,默认情况下占用10个字节。可以将每个字符打包成8-9个字节(库允许这样做),但我没有打扰。此外,以这种形式,您可以直接在代码中编辑单个像素。

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


车架


原始设备提供了非常简单方便的界面。信息分为从各个页面(屏幕)显示的类别。使用按钮,您可以循环浏览页面,并使用第二个按钮选择当前项目或执行按钮下方签名中指示的操作。在我看来,这种方法非常方便,无需更改任何内容。

我喜欢OOP的美丽,因为我立即使一个小界面变得令人眼花,乱,每个页面都根据需要实现该界面。该页面知道如何绘制自身并实现对按钮的反应。

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

根据当前屏幕,按钮可以执行各种操作。因此,在屏幕顶部具有8个像素的高度,我将其分配给按钮的标签。签名的文本取决于当前屏幕,并由虚拟函数getSelButtonText()和getOkButtonText()返回。同样在标题服务中,仍将显示GPS信号强度和电池电量等项目。剩余的¾屏幕可提供有用的信息。

如我所说,屏幕可以翻转,这意味着在某个地方应该有一个对象列表,用于不同页面。屏幕可以嵌套多个,就像子菜单一样。我什至启动了ScreenManager类,该类本来可以管理这些列表的,但是后来我发现解决方案更容易。

因此,每个屏幕仅具有指向下一个的指针。如果屏幕允许您进入子菜单,那么它将向该子菜单的屏幕添加一个指针

class Screen
{
	Screen * nextScreen;
};

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

默认情况下,按钮处理程序仅调用屏幕切换功能,并向其传递所需的指针。该功能证明是微不足道的-它只是将指针切换到当前屏幕。为了确保屏幕的嵌套,我做了一小堆。因此,整个屏幕管理器适合25行和4个小功能。

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

没错,用于填充这些结构的代码看起来不太好,但是到目前为止,还没有更好地发明它。

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

思想
, , , . .

来吧在界面的实现中,我想做一个类似消息框的事情,一条短消息会出现一两秒钟,然后消失。例如,如果您使用当前坐标在屏幕上按下POI(兴趣点)按钮,则除了将点写入轨迹外,还可以向用户显示消息“ Waypoint Saved”(在原始设备中,仅显示一个额外的图标一秒钟)。或者,当电池电量不足时,通过消息“振奋”用户。

image

由于来自GPS的数据将持续不断,因此无法谈论任何阻止功能。因此,我不得不发明一种简单的状态机(状态机),该状态机在loop()函数中将选择要执行的操作-显示当前屏幕或消息框。

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

使用状态机处理按钮按下也很方便。通过中断也许是正确的,但事实也很好。它的工作方式如下:如果在IDLE状态下按下了按钮,请记住该按钮被按下的时间,然后进入BUTTON_PRESSED状态。在这种状态下,我们等到用户释放按钮。在这里,我们可以计算按下按钮的持续时间。短响应(<30ms)只是被忽略-最有可能是触点反弹。长途旅行已经可以解释为按下按钮。

我计划将短按按钮用于常规操作,将长按(> 1c)用于特殊功能。例如,短按可启动/暂停里程表,长按可将计数器重置为0。

也许将添加其他状态。因此,例如,在切换到下一页后的原始记录器中,屏幕上的值经常更改,而在几秒钟后(每秒一次),更改的频率就会降低。这可以通过添加另一个状态来完成。

准备好框架后,我已经开始连接GPS。但是这里有些细微之处使我推迟了这项任务。

固件优化


在继续之前,我需要分散一些技术细节。事实是,在这个地方,我开始增加内存消耗。事实证明,在固件开始时不小心地声明而没有PROGMEM修饰符的行将复制到RAM,并在整个运行期间占用那里的空间。

各种架构
. . .. .

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

幸运的是,图书馆开发人员已经部分地照顾了这一点。显示库的主要类-Adafruit_SSD1306继承自Arduino标准库的Print类。

这为我们提供了打印方法的一系列不同的修改-用于打印字符串,单个字符,数字等。因此,它具有2个单独的打印行功能:


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

第一个知道您需要从闪存驱动器中打印一行并逐字符加载它。第二个从RAM打印字符。实际上,这两个函数都仅从不同的地址空间获取指向字符串的指针。

很长时间以来,我一直在arduino代码中查找此__FlashStringHelper,以了解如何调用所需的print()函数。事实证明,伙计们确实做到了:他们只是使用正向声明来声明此类型(而没有声明类型本身),并编写了一个宏,该宏在Flash中将指向行的指针转换为__FlashStringHelper类型。仅用于编译器选择必要的重载函数

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

这使您可以这样编写:

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


但是不允许写像
const char text[] PROGMEM = "String in flash memory"; 
display.print(F(text));

而且,显然,该库没有提供任何可以通过这种方式完成的操作。我知道在代码中使用私有库片段不是很好,但是我应该怎么办?我画了我的宏,它做了我所需要的。

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

因此帽子绘制功能开始看起来像这样:

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

好吧,由于我进入了固件的低级部分,所以我决定更深入地研究它在内部的工作方式。

通常,想出Arduino的人需要树立纪念碑。他们为原型和工艺制作了一个简单便捷的平台。大量对电子学和编程知识很少的人能够进入Arduino世界。但是,在进行诸如带LED的信号灯或读取温度计的垃圾时,所有这些操作都是流畅而美观的。一旦您遇到严重的问题,就必须立即从一开始就比您想要的更深刻地理解。

因此,在每个添加的库甚至一个类之后,我都指出了内存消耗的增长速度。此时,我正忙于处理超过14 KB的32 KB闪存和1300字节的RAM(不足2k)。每一次粗心的动作都会使已经使用的动作增加10%。但是我还没有真正连接GPS和SD / FAT32库,这只猫在哭。我不得不拿起反汇编程序检查器并研究编译器做了什么。

我暗中希望链接器抛出未使用的函数。但是事实证明,其中的一些链接器几乎全部插入。在固件中,我发现了线条绘制功能以及与屏幕一起使用的库中的其他功能,尽管当时显然在代码中我没有调用它们。隐式地,它们也不应被调用-如果仅从位图绘制字母,为什么还需要线条绘制功能?超出5.2kb的余地(这还不算字体)。

除了显示控件库外,我还发现:

  • 2.6 kb-在SoftwareSerial上(有时我将其拉入项目)
  • 1.6 kb-I2C
  • 1.3 kb-硬件串行
  • 2 kb-TinyGPS
  • 在实际的arduino上为2.5 kb(初始化,引脚,各种表,函数millis()和delay()的主计时器),

这些数字非常具有指示性,因为 优化器正在认真混合代码。某些函数可能在一个位置开始,然后另一个库中的另一个函数(从第一个函数开始调用)可以立即跟随它。此外,这些功能的单独分支可以位于闪存的另一端。

同样在代码中,我发现:

  • 通过SPI进行屏幕控制(尽管我通过I2C进行了连接)
  • 本身不被调用的基类方法,因为 在继承人中重新定义
  • 从未被设计调用的析构函数
  • 绘图功能(并非全部-链接器仍然抛出的部分功能)
  • malloc / free而在我的代码中,所有对象本质上都是静态的

但是不仅闪存的消耗量增加,而且SRAM的发展也突飞猛进:

  • 130字节-I2C
  • 100字节-SoftwareSerial
  • 157字节-串行
  • 558字节-显示(其中512是帧缓冲区)

同样有趣的是.data节。大约有700个字节,此内容在一开始就从闪存加载到RAM中。事实证明,在内存中以及与初始化值一起为变量保留了位置。在这里住着那些您忘记声明为const PROGMEM的变量和常量。

其中包括一个带有“闪屏”(splash screen)屏幕的大数组-帧缓冲区的初始值。从理论上讲,如果在启动后立即显示()屏幕,则可以看到花朵和Adafruit铭文,但就我而言,在上面花一些闪存是没有意义的。

.data节还包含vtable。它们显然是由于运行时效率的原因而从闪存驱动器复制到内存中的。但是您必须牺牲相当大的RAM-超过150个字节的十几个类。而且,似乎没有编译器密钥会牺牲性能,而将虚拟表留在闪存中。

怎么办呢?我还不知道这将取决于消费将如何继续增长。为了找到一个好的门框,需要进行无情的维修。我极有可能不得不将所有库明确地绘制到我的项目中,然后对其进行全面介绍。而且,您可能还必须以不同的方式重写某些部分,以优化内存。或切换到功能更强大的硬件。无论如何,现在我已经知道了这个问题,并且有解决该问题的策略。

更新:
资源效率方面进展甚微。我正在对此部分进行更新,因为接下来,我想重点介绍完全不同的事情。

在评论中,对于使用C ++感到有些困惑。特别是,为什么他会如此糟糕并在宝贵的RAM中保留vtable?通常,虚函数,构造函数和析构函数是开销。怎么了让我们弄清楚!

以下是项目某个阶段的内存统计信息
:程序大小:15 458字节(已使用,最大30720字节的50%)(2.45秒)
最小内存使用量:1258字节(最大2048字节的61%)

实验1-重写为C。

我抛出了类,并用函数指针重写了表中的所有内容。由于屏幕实际上始终具有相同的结构,因此所有数据成员都已成为普通的全局变量。

重构后的统计信息
程序大小:14568字节(使用了30720字节的最大值的47%)(2.35秒)
最小内存使用量:1176字节(最大2048字节的57%)的

总计。赢得了900字节的闪存和80字节的RAM。闪光到底留下的东西没有被挖掘。 80字节的RAM只是vtable的大小。所有其他数据(类成员)仍然保留。

我必须说我并没有破坏一切-我只是想看大图而不花很多时间。重构后,我“丢失”了嵌套的屏幕截图。有了它们,消费会更多。

但是,此实验中最重要的事情是代码的质量已大大降低。一种功能的代码已散布在多个文件中。对于某些数据,“一个所有者”不复存在,一些模块开始爬入其他模块的内存中。该代码已变得非常丑陋。

实验2-从C ++压缩字节

我回滚了实验,我决定将所有内容保留为类。只有这一次,屏幕才对静态分布的对象起作用。屏幕上页面的结构是固定的。您可以在编译阶段指定它,而无需使用new / delete。

程序大小:15408字节(最大使用30720字节的50%)(2.60秒)
最小内存使用量:1273字节(最大2048字节的62%)

RAM消耗略有增加。但这实际上是更好的。 RAM消耗的增加是通过将对象从堆移动到静态分布的内存区域来解释的。即实际上,对象是在以前创建的,但是并没有进行统计。现在,这些对象已明确考虑在内。

但是要大大减少闪存的消耗是行不通的。该代码仍然包含构造函数本身,这些构造函数在启动时仍会被调用。即编译器无法提前“执行”它们并将所有值放在预先分配的区域中。尽管刺猬很清楚,对象永远不会被删除,但是代码中仍然存在析构函数。

为了节省至少一点点,我删除了层次结构中的所有析构函数,尤其是基类中的虚拟析构函数。想法是在每个vtable中释放几个字节。然后让我惊讶的是:

程序大小:14,704字节(最大30,720字节使用了48%)(2.94秒)
最小内存使用:1211字节(最大2048字节使用了59%)

事实证明vtable已经消失了不是通过一个指针,而是已经通过2。它与析构函数有什么关系。只有一个析构函数为空(对于堆栈中的对象可见),而另一个析构函数具有免费调用,对于堆中的对象(-12字节RAM)可见。此外,与臀部相关联的变量(8个字节)和从未创建的对象的标签(Screen,ParentScreen-40个字节)都消失了

闪存消耗量大幅减少-减少了700个字节。不仅析构函数本身消失了,malloc / free / new / delete实现也消失了。700个字节的空虚拟析构函数!700字节,卡尔!

那不会来回缠绕,这里所有的数字都在一个地方

çC ++
闪光灯1545814,56814,704
内存125811761211


底线:事实证明,C ++中的消耗与C中的消耗几乎相同。但是同时,封装,继承和多态性才是力量。我准备为一些消费增加而为此多付的费用。也许我只是不能用C编写漂亮的代码,但是为什么,如果我可以用C ++编写漂亮的代码呢?

后记


最初,我想在项目结束时写一篇文章。但是由于笔记会随着工作的进行而快速积累,因此该文章可能会变得很大。因此,我决定将其分为几个部分。在这一部分中,我讨论了准备阶段:了解我的真正需求,选择平台,实现应用程序框架。

在下一部分中,我计划继续进行基本功能的实现-使用GPS。我已经遇到了一些有趣的事情,我想告诉他们。

十多年来,我一直没有认真地为微控制器编程。事实证明,我对大型计算机资源的丰富程度感到有点宠坏,并且它对ATMega32的现实也很拥挤。因此,我不得不考虑各种备份选项,例如修剪库的功能或以有效利用内存的名义重新设计应用程序。我也不排除切换到功能更强大的控制器的可能性-ATMega64或STM32系列产品。

从样式上看,该文章看起来像是一本建筑杂志。而且我很乐意提出建设性意见-更改任何内容为时不晚。那些愿意的人可以在github上加入我的项目

第一部分结束。

第二部分

Source: https://habr.com/ru/post/zh-CN401899/


All Articles