65.9K
CodeProject 正在变化。 阅读更多。
Home

M5 Stack Fire 2.6 开箱

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2022 年 7 月 14 日

公共领域

7分钟阅读

viewsIcon

5462

downloadIcon

73

解锁这款精巧的 IoT 小工具的潜力。

M5Stack Fire

引言

M5 Stack Fire 2.6 是一款功能强大、小巧实用的物联网小工具。

这是一个非常适合用来原型开发物联网代码的设备。

底座可以连接乐高积木。它内置电池组,配有可拆卸的充电座,但也可以通过 USB C 充电。该设备包含一个双核 240MHz RISC 处理器、一个 320x240x16bpp LCD 屏幕、16MB 闪存空间、512kB SRAM(约 360kB 可用)、4MB PSRAM、3 个可编程按钮、3 个 Grove 扩展端口、2 条 Neopixel LED 灯带、一个 SD 读卡器、一个适用于非常简单的噪音检测的扬声器和麦克风、一个陀螺仪、一个 LoRA 无线电以及 WiFi 2.4GHz 和蓝牙/BLE。该设备还可以与其他模块“堆叠”以增加广泛的功能,至少理论上是这样。实际上,我还没有尝试过任何这些模块。它们有点贵,而且我更喜欢 DIY 而不是即插即用的现成系统。

文档还提到该设备包含一个 BMM150 磁力计(基本上是基于 MEMS 的罗盘),但我即使使用工厂代码也无法使其正常工作。我不确定 Fire 2.6 版本是否包含它,但早期版本包含。我的设备可能存在缺陷。文档称其地址为 0x10,但我找到的代码将其地址设为 0x13。我都试过了。都没有成功。我想如果我在这东西的陪伴下在野外迷路了,我就麻烦了。

我对设备自带的代码功能集不满意,于是自己编写了代码。LCD 和 LED 灯带现在由 htcw_gfx 驱动,提供了我图形库的所有功能。整个项目都打包在 Platform IO 项目中,您可以下载它,将其复制到您的项目文件夹,并将其用作新的 M5 Stack Fire 项目的模板。我还没有支持该设备的电源管理或 LoRa 功能。目前您需要使用 M5 的库来实现这些功能,因为这仍然是进行中的项目。

我在项目中包含了几种 True Type/Open Type 字体作为可嵌入的头文件,但您可以使用上述链接中的 头文件生成器 将大多数 TTF 和 OTF 文件转换为头文件。您也可以使用它将 JPG 嵌入为头文件。我使用 fontsquirrel.com 来查找字体。

我们将一步步介绍如何使用提供的项目模板创建新应用程序。

开始使用这个项目

我们将使用 Arduino 框架,尽管也可以针对 ESP-IDF 来使用此设备。Arduino 框架享有广泛的设备支持,并且网上有很多关于它的示例代码,所以我推荐用于 ESP32 开发。此外,GFX 的显示驱动程序目前在 Arduino 下表现最佳。

我们将使用 Platform IO 进行开发。如果您还没有安装它,请立即安装,因为此项目无法与 Arduino IDE 配合使用。这是大学级别的,而 Arduino IDE 只是初中水平。它根本无法做到我们需要它做的事情,比如使用 GNU C++17。安装过程包括安装 Python、安装 Git、安装 VS Code,然后使用 VS Code 安装 Platform IO 扩展。

完成这些步骤后,您应该能够下载空项目,解压缩它,然后在 VS Code 中打开文件夹,Platform IO 最终会加载自身并围绕您的项目进行设置。第一次执行此操作可能需要很长时间,因为它只是在后台下载必需的组件才能使其正常工作 - 它直到第一次运行时才真正完成安装。据我记忆,其中一些组件超过 100MB,所以根据您的互联网连接情况,您可以去准备三明治。

打开您的 M5Stack Fire,然后使用提供的 USB 数据线将其连接到您的 PC。只要您没有连接其他物联网设备,Platform IO 就会自动检测其注册的 COM 端口。

空项目模板

这个空项目并不是完全空的,但它的目的是让您尽快上手。

让我们分解一下并检查各个组件。

platformio.ini

此文件包含项目的全局设置。基本上是项目的“项目文件”。其中包含您正在使用的芯片组、软件框架、依赖项、COM 端口信息、编译器设置以及其他类似内容。

[env:m5stack-fire]
platform = espressif32
board = m5stack-fire
framework = arduino
upload_speed = 921600
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
lib_ldf_mode = deep
lib_deps = 
    codewitch-honey-crisis/htcw_ili9341
    codewitch-honey-crisis/htcw_mpu6886
    codewitch-honey-crisis/htcw_w2812
    codewitch-honey-crisis/htcw_button
build_unflags = -std=gnu++11
build_flags = -std=gnu++17
    -DBOARD_HAS_PSRAM
    -mfix-esp32-psram-cache-issue

此文件通常不应由您修改。您可以使用 PlatformIO IDE 的“快速任务”下的“库”功能添加依赖项。其中的依赖项用于使用 M5 Stack Fire 的外设,例如 ILI9342C 显示屏和 W2812 Neopixel LED 灯带。

字体头文件

/include 目录下有几个头文件。这些是可嵌入的 True Type/Open Type 字体。如果您需要渲染精美的抗锯齿文本,可以包含一个或多个这些文件到您的项目中。我们稍后将通过示例详细介绍。

**由于某种硬件问题以及 M5 公司可能奇怪的 SPI 接线方式,在此显示器上进行抗锯齿处理速度非常慢(可以说是不可接受)。解决方法是绘制带有不透明背景的文本,或者将文本绘制到位图,然后再将该位图绘制到屏幕上。

main.cpp

/src 目录下是我们的主文件,应用程序的核心内容就在这里。在此文件中,我们进行所有硬件设置,然后有一些样板代码用于连接和初始化硬件,再加上一些可以轻松丢弃的示例代码。

为了让您能够找到方向,我们简要介绍一下其中的内容。以下所有代码都是样板代码,除了 `#include "Ubuntu.hpp"` 行,它包含了我们的字体。您可以根据需要删除或替换该行以满足您的目的。

#include <Arduino.h>
#include <SPIFFS.h>
#include <SD.h>
#include <mpu6886.hpp>
#include <tft_io.hpp>
#include <ili9341.hpp>
#include <w2812.hpp>
#include <htcw_button.hpp>
#include <gfx.hpp>
// font for example
// not necessary
#include "Ubuntu.hpp"
using namespace arduino;
using namespace gfx;

// pin assignments
constexpr static const uint8_t spi_host = VSPI;
constexpr static const int8_t lcd_pin_bl = 32;
constexpr static const int8_t lcd_pin_dc = 27;
constexpr static const int8_t lcd_pin_rst = 33;
constexpr static const int8_t lcd_pin_cs = 14;
constexpr static const int8_t sd_pin_cs = 4;
constexpr static const int8_t speaker_pin_cs = 25;
constexpr static const int8_t mic_pin_cs = 34;
constexpr static const int8_t button_a_pin = 39;
constexpr static const int8_t button_b_pin = 38;
constexpr static const int8_t button_c_pin = 37;
constexpr static const int8_t led_pin = 15;
constexpr static const int8_t spi_pin_mosi = 23;
constexpr static const int8_t spi_pin_clk = 18;
constexpr static const int8_t spi_pin_miso = 19;

using bus_t = tft_spi_ex<spi_host, 
                        lcd_pin_cs, 
                        spi_pin_mosi, 
                        -1, 
                        spi_pin_clk, 
                        SPI_MODE0,
                        true, 
                        320 * 240 * 2 + 8, 2>;

using lcd_t = ili9342c<lcd_pin_dc, 
                      lcd_pin_rst, 
                      lcd_pin_bl, 
                      bus_t, 
                      1, 
                      true, 
                      400, 
                      200>;

上面我们为相关的引脚分配定义了常量,然后通过 SPI 总线初始化了显示屏,并启用了 DMA 传输,这样就可以使用 GFX 的 `draw::bitmap_async<>()` 在后台将位图快速发送到显示屏,并在调用后立即返回 - 这绝对是一项高级技术,但需要时可用。

接下来,我们声明一些别名,以便访问我们显示屏的 16 位命名颜色和我们 Neopixel LED 灯带的 24 位命名颜色。之后,我们声明我们所有硬件的实例。

// lcd colors
using color_t = color<typename lcd_t::pixel_type>;
// led strip colors
using lscolor_t = color<typename w2812::pixel_type>;

lcd_t lcd;

// declare the MPU6886 that's attached
// to the first I2C host
mpu6886 gyro(i2c_container<0>::instance());
// the following is equiv at least on the ESP32
// mpu6886 gyro(Wire);

w2812 led_strips({5,2},led_pin,NEO_GBR);

button<button_a_pin,10,true> button_a;
button<button_b_pin,10,true> button_b;
button<button_c_pin,10,true> button_c;

上面,您将看到声明了 LCD 显示屏 `lcd`、MEMS 加速度计/陀螺仪 `gyro`、Neopixel 灯带 `led_strips`,最后是 3 个按钮 `button_a`、`button_b` 和 `button_c`。

接下来是我们的 M5 Stack 初始化例程,包括启动过程中的一个最小启动屏幕。

void initialize_m5stack_fire() {
    Serial.begin(115200);
    SPIFFS.begin(false);
    SD.begin(4,spi_container<spi_host>::instance());
    lcd.initialize();
    led_strips.fill(led_strips.bounds(),lscolor_t::purple);
    lcd.fill(lcd.bounds(),color_t::purple);
    rect16 rect(0,0,64,64);
    rect.center_inplace(lcd.bounds());
    lcd.fill(rect,color_t::white);
    lcd.fill(rect.inflate(-8,-8),color_t::purple);
    gyro.initialize();
    // see https://github.com/m5stack/m5-docs/blob/master/docs/en/core/fire.md
    pinMode(led_pin, OUTPUT_OPEN_DRAIN);
    led_strips.initialize();
    button_a.initialize();
    button_b.initialize();
    button_c.initialize();
}

启动屏幕不会被初始化代码清除,允许您通过自己的代码延长启动时间(如果启动需要额外时间)。

接下来是您需要修改的代码。

// for the button callbacks
char button_states[3];
void buttons_callback(bool pressed, void* state) {
    Serial.printf("Button %c %s\n",*(char*)state,pressed?"pressed":"released");
}
void setup() {
    initialize_m5stack_fire();
    
    // setup the button callbacks (optional)
    button_states[0]='a';
    button_states[1]='b';
    button_states[2]='c';
    button_a.callback(buttons_callback,button_states);
    button_b.callback(buttons_callback,button_states+1);
    button_c.callback(buttons_callback,button_states+2);
    
    // your code here

    
    // example - go ahead and delete
    lcd.fill(lcd.bounds(),color_t::black);
    const char* m5_text = "M5Stack";
    constexpr static const uint16_t text_height = 80;
    srect16 text_rect;
    open_text_info text_draw_info;
    const open_font &text_font = Ubuntu;
    
    text_draw_info.text = m5_text;
    text_draw_info.font = &text_font;
    text_draw_info.scale = text_font.scale(text_height);
    text_draw_info.transparent_background = false;
    text_rect = text_font.measure_text(ssize16::max(),
                                    spoint16::zero(),
                                    m5_text,
                                    text_draw_info.scale)
                                        .bounds()
                                            .center((srect16)lcd.bounds())
                                                .offset(0,-text_height/2);
    draw::text(lcd,text_rect,text_draw_info,color_t::gray);
    draw::line(lcd,
            srect16(text_rect.x1,text_rect.y1,text_rect.x1,text_rect.y2)
                .offset(80,0),
            color_t::white);
    const char* fire_text = "Fire";
    text_draw_info.text = fire_text;
    text_rect = text_font.measure_text(
                                    ssize16::max(),
                                    spoint16::zero(),
                                    fire_text,text_draw_info.scale)
                                        .bounds()
                                            .center((srect16)lcd.bounds())
                                                .offset(0,text_height/2);
    draw::text(lcd,text_rect,text_draw_info,color_t::red);
    led_strips.fill({0,0,4,0},lscolor_t::red);
    led_strips.fill({0,1,4,1},lscolor_t::blue);
}
void loop() {
    // pump the buttons to make sure
    // their callbacks (if any) get
    // fired
    button_a.update();
    button_b.update();
    button_c.update();
}

启动后,它会显示文章顶部的 M5|Stack Fire。它还将按钮按下和释放事件转储到串行端口。所有这些代码都可以替换为您自己的代码。

main.cpp(第二版)

从 `// for the button callbacks` 开始,删除所有内容。

向上滚动到顶部,将 `#include "Ubuntu.hpp"` 更改为 `#include "Robinette.hpp"`。

将以下内容粘贴到源文件底部。

const char* text = "hello!";
const open_font &text_font = Robinette;
constexpr static const uint16_t text_height = 125;
// The above line is because some fonts are kinda small 
// in lowercase. Use the above for that. Otherwise
// use the following:
//constexpr static const uint16_t text_height = 100;
srect16 text_rect;
open_text_info text_draw_info;

const rgb_pixel<16> colors_a[] = {
    color_t::red,
    color_t::orange,
    color_t::yellow,
    color_t::green,
    color_t::blue,
    color_t::purple
};
constexpr const size_t color_count_a = sizeof(colors_a)/sizeof(rgb_pixel<16>);
const rgb_pixel<16> colors_b[] = {
    color_t::cyan,
    color_t::pink,
    color_t::white,
    color_t::pink
};
constexpr const size_t color_count_b = sizeof(colors_b)/sizeof(rgb_pixel<16>);
const rgb_pixel<16> colors_c[] = {
    color_t::red,
    color_t::white,
    color_t::blue,
};
constexpr const size_t color_count_c = sizeof(colors_c)/sizeof(rgb_pixel<16>);

int color_height;
unsigned int color_offset;
size_t color_count;
const rgb_pixel<16>* colors;
 
uint32_t led_strip_ts;
uint32_t led_strip_offset;

// the frame buffer type is based on the LCD's pixel format
using frame_buffer_t = bitmap_type_from<lcd_t>;
uint8_t* frame_buffer_data;
frame_buffer_t frame_buffer;

void set_colors(int i) {
    switch(i) {
        case 0:
            colors = colors_a;
            color_count = color_count_a;
            break;
        case 1:
            colors = colors_b;
            color_count = color_count_b;
            break;
        default:
            colors = colors_c;
            color_count = color_count_c;
            break;
    }
    
    color_height = lcd.dimensions().height/color_count;
}

void setup() {
    initialize_m5stack_fire();

    led_strip_ts=0;
    led_strip_offset = 0;
    color_offset = 0;
    // framebuffer is in PSRAM
    frame_buffer_data = (uint8_t*)ps_malloc(frame_buffer_t::sizeof_buffer(lcd.dimensions()));
    if(frame_buffer_data==nullptr) {
        // shouldn't happen
        Serial.println("Out of memory.");
        while(true) {delay(10000);}
    }
    
    button_a.callback([](bool value,void*state){
        if(value) {
            Serial.println("a");
            set_colors(0);
        }
    },nullptr);

    button_b.callback([](bool value,void*state){
        if(value) {
            Serial.println("b");
            set_colors(1);
        }
    },nullptr);

    button_c.callback([](bool value,void*state){
        if(value) {
            Serial.println("c");
            set_colors(2);
        }
    },nullptr);

    // create a bitmap with the same format as the LCD
    frame_buffer = create_bitmap_from(lcd,lcd.dimensions(),frame_buffer_data);

    // choose the first "palette"
    set_colors(0);

    // draw the stripes    
    rect16 r(0,0,lcd.bounds().x2,color_height-1);
    frame_buffer.fill(r,colors[0]);
    for(size_t i = 1;i<color_count;++i) {
        r.offset_inplace(0,color_height);
        frame_buffer.fill(r,colors[i]);
    }
    
    // blt to the LCD
    draw::bitmap(lcd,lcd.bounds(),frame_buffer,frame_buffer.bounds());
    
    // precompute our text info
    text_draw_info.text = text;
    text_draw_info.font = &text_font;
    text_draw_info.scale = text_font.scale(text_height);
    text_rect = text_font.measure_text(ssize16::max(),
                                    spoint16::zero(),
                                    text,
                                    text_draw_info.scale)
                                        .bounds()
                                            .center((srect16)lcd.bounds());
}
void loop() {
    button_a.update();
    button_b.update();
    button_c.update();
    // draw a trailing line color for each horizontal bar, 
    // expanding it downward by one pixel
    rect16 r(0,
            (color_height-1+color_offset)%lcd.dimensions().height,
            lcd.bounds().x2,
            (color_height-1+color_offset)%lcd.dimensions().height);
    frame_buffer.fill(r,colors[0]);
    for(size_t i = 1;i<color_count;++i) {
        r.offset_inplace(0,color_height);
        r.y1=r.y2=(r.y1%lcd.dimensions().height);
        frame_buffer.fill(r,colors[i]);
    }
    // draw the font
    draw::text(frame_buffer,
            text_rect,
            text_draw_info,
            color_t::black);
    // now blt the frame buffer to the display
    draw::bitmap(lcd,
                lcd.bounds(),
                frame_buffer,
                frame_buffer.bounds());   
    uint32_t ms = millis();
    if(ms>=led_strip_ts+250) {
        led_strip_ts = ms;
        // suspend so we update all at once on resume
        draw::suspend(led_strips);
        // update each color
        for(int y = 0;y<led_strips.dimensions().height;++y) {
            for(int x = 0;x<led_strips.dimensions().width;++x) {
                draw::point(led_strips,
                            point16(x,y),
                            colors[(led_strip_offset+
                                (x+(y*led_strips.dimensions().width)))
                                %color_count]);
            }
        }
        // finally, refresh the strips
        draw::resume(led_strips);
        ++led_strip_offset;
    }
    ++color_offset;   
}

下面的视频向您展示了它的作用。我们正在循环切换三种调色板,同时控制屏幕和 LED 灯带,并显示一些文本。按下按钮时,调色板会发生变化。请注意,我们使用 PSRAM 中的一个帧缓冲区来进行所有绘图操作,然后定期将其绘制到显示屏。这可以减少闪烁,并且由于我在设备显示屏上遇到的问题,可以在不牺牲性能的情况下正确地抗锯齿字体。

结论

就是这样。现在带着您的 M5 Stack Fire,出发去创造吧!

历史

  • 2022 年 7 月 14 日 - 首次提交
© . All rights reserved.