M5Stack Cardputer ADV 上手

最近刷到 M5Stack 的 Cardputer ADV,信用卡大小的机身塞了块键盘,觉得挺有意思,顺手就入了一台。
M5Stack 的产品线
M5Stack 这家公司做模块化开发板起家,核心思路是把 ESP32 包成一个带屏幕、带外壳、带接口的成品。早期出的是 Core 系列,像个小型游戏机,堆叠各种功能模块。后面又做了 Stick、Atom、Paper 各种形态,Cardputer 算是他们把键盘和屏幕整合到一块的尝试。
Cardputer 最大的特点是自带全键盘,56 键紧凑布局,导航键在右侧。比起其他 M5 产品靠几个按钮或者触摸屏输入,有实体键盘在写命令、输密码、调参数的时候顺手太多。
这台 ADV 版底部带 CAP(Communication Accessory Port)接口,可以插拓展模块。官方目前出了好几款 CAP 配件,我用的是 LoRa+GNSS 通信模块。模块上同时集成了 SX1262 射频芯片和 GNSS 定位芯片,LoRa 部分支持 433MHz、868MHz 和 915MHz 三个频段,国内用 433MHz 居多。
插上去之后,Cardputer ADV 就变成了一个带键盘的手持 LoRa 电台。在户外测过,市区环境里两公里内能稳定通信,开阔地带据说能到五公里以上。配合 Cardputer 的全键盘,可以手写发送文本消息,比那些只有几个按钮的 LoRa 节点方便太多。
更实用的是 GNSS 功能。模块上的定位芯片能直接输出经纬度坐标,在屏幕上实时显示当前位置,或者把位置信息打包进 LoRa 数据包发出去。对于野外活动或者需要位置上报的场景,一台设备同时解决通信和定位两个问题,不用再外接 GPS 模块。
模块通过 SPI 跟主控通信,M5 的 Arduino 库已经封装好了相关 API,LoRa 初始化和 GNSS 数据读取都有现成的函数,不需要自己处理射频底层或者 NMEA 协议解析。
UIFlow 和 M5Burner
M5Stack 在软件生态上花了挺大功夫,降低了入门门槛。
UIFlow 是他们的图形化编程平台,基于 Blockly 拼图块,在浏览器里拖拖拽拽就能写程序。对于没接触过嵌入式的人来说,不用配环境、不用学 C++,连上设备就能跑。它同时支持 MicroPython,拼图块生成的代码可以一键切换成脚本,适合从可视化过渡到代码写法的场景。
M5Burner 是固件烧录工具,支持 Windows、macOS 和 Linux。它最大的价值在于内置了一个固件市场,罗列了大量社区和官方预编译好的固件。选中设备型号,挑一个固件,点下载再点烧录,几步搞定。Cardputer ADV 的驱动和分区表也内置好了,不用像普通 ESP32 那样手动配置。
社区固件
Cardputer 的键盘和屏幕组合吸引了不少人做工具类固件,其中有两个项目特别出名。
Evil M5 是一个 WiFi 安全测试工具,功能涵盖了 WiFi 扫描、伪造热点、Deauth 攻击、Probe 请求嗅探等。把它理解成一个掌上版的 WiFi Pineapple,带屏幕和键盘,操作起来比纯命令行直观很多。当然,这个项目仅供学习和测试自有网络,实际使用要注意法律边界。
M5 Launcher 则是一个固件启动器,解决的是多固件切换的痛点。Cardputer 存储空间不小,但每次刷不同固件都要重新烧录挺麻烦。M5 Launcher 把设备变成一个桌面系统,各个应用和固件以图标形式排列,像手机桌面一样直接点选启动,不用反复插线刷机。
简单尝试一下
说了这么多,最后拿我自己写的一个小玩意当例子,很简单,就是在屏幕上循环播放月薪猫的跳散味舞。
动画源文件是 GIF,有 28 帧,每帧 124×124 像素。ESP32-S3 的 Flash 够大,但直接把 GIF 解码放在 loop 里跑会拖垮性能,而且 drawBitmap 逐像素绘制在 240×135 的屏幕上帧率很难看。我的做法是在 PC 上预先把 GIF 转成 RGB565 格式,存到 C 头文件里,每帧一个 const uint16_t 数组,加上 PROGMEM 修饰放进 Flash。
#include <M5Cardputer.h>
#include "salary_cat_animation.h"
constexpr uint16_t kBackground = TFT_WHITE;
constexpr uint32_t kFrameTimeMs = 40;
uint32_t lastFrameAt = 0;
uint8_t frameIndex = 0;
void drawFrame(uint8_t index) {
const int32_t x = (M5Cardputer.Display.width() - SALARY_CAT_FRAME_WIDTH) / 2;
const int32_t y = (M5Cardputer.Display.height() - SALARY_CAT_FRAME_HEIGHT) / 2;
const uint16_t* frame = SALARY_CAT_FRAMES[index];
M5Cardputer.Display.pushImage(x, y, SALARY_CAT_FRAME_WIDTH,
SALARY_CAT_FRAME_HEIGHT, frame);
}
void setup() {
auto cfg = M5.config();
M5Cardputer.begin(cfg, true);
M5Cardputer.Display.setRotation(1);
M5Cardputer.Display.setBrightness(180);
M5Cardputer.Display.fillScreen(kBackground);
drawFrame(frameIndex);
lastFrameAt = millis();
}
void loop() {
M5Cardputer.update();
const uint32_t now = millis();
if (now - lastFrameAt >= kFrameTimeMs) {
lastFrameAt = now;
frameIndex = (frameIndex + 1) % SALARY_CAT_FRAME_COUNT;
drawFrame(frameIndex);
}
}
这里用了 pushImage 而不是逐像素绘制,直接把一整块 RGB565 数据推到屏幕驱动里。28 帧全彩动画的头文件体积不小,烧录时需要把分区表改成 8MB Flash 配置,否则默认的 1.2MB 应用分区装不下。