大家好, 我是james, 一名hacker. 平时喜欢研究一些可编程的小东西.
1. 板卡介绍
2022年夏季硬禾学堂暑期练的第二款板卡是来自M5Stack的M5Stick-C-Plus. 这款板卡非常小巧, 带有一片1.14英寸的小屏幕, 可以当做手表用. 用过M5Stack产品的朋友都知道M5Stack的板卡完成度很高, 有漂亮的外壳, 可以快速应用到量产产品中去, 同时M5Stack的板卡都使用了ESP32的处理器芯片, 这种芯片价格便宜, 自带Wifi和蓝牙, 是做IoT物联网项目的好材料. 整个板卡接口丰富, 带有一排hat connector还有一个grove接口. 橘黄色的外壳颜色也很醒目. 是一款很好的开发板.
2. 实现功能
在本期暑假练活动中, 我主要学习了ESP32的开发, 了解了ESP32处理芯片拥有的各项功能. 在开发的过程中, 我发现使用ESP-IDF开发还是很方便的, 因为IDF用到了我比较熟悉的CMake构建工具, GCC编译器, 还有类似Kconfig的配置工具.特别是跨平台的特点让我能够在macos和windows都使用同一开发流程.
本期实现了第一个小项目, 语音控制灯板.
3. 设计思路
语音控制灯板有两种设计思路, 一种是使用云服务, 比如一些语音识别服务, 把音频数据上传后交给云端进行识别, 返回结果, 另一种是使用tinyml利用边缘计算执行"推断". 我使用的是后一种方法, 因为这种方法经过几次funpack活动,已经很熟悉了, 这里的熟悉是指使用edge-impulse工具生成推断所需的模型, 至于算法本身已经被edge-impulse封装好了, 参数配置都是通过页面图形化操作的,非常容易.
那么难点在哪儿呢? 首先edge-impulse并不直接提供M5StickC-Plus对应的数据采集固件, 默认提供的是为名为ESP-EYE的开发板开发的采集固件(https://github.com/edgeimpulse/firmware-espressif-esp32), ESP-EYE是espressif官方的开发板, 虽然和M5StickC-Plus一样使用ESP32芯片, 但是板卡之间的区别还是带来了一些小麻烦, 主要是这个固件无法直接使用, 也就无法直接烧录开始训练或者下载edge-impulse构建的推断firmware, 具体来说需要修改麦克风相关的一些配置代码.
其次, 需要修改就需要重新编译构建这个固件. 其实这个固件本身也是一个应用edge-impulse-sdk的例子, 但是如果按照这个例子修改, 则需要进行一些重构工作, 因为edge-impulse-sdk分为ingestion, inference, model, platform等部分, 这几个部分相互有耦合. 解决这些耦合关系就需要对edge-impulse-sdk的结构进行理解, 然后进行合理重构, 这也是一个相对困难的地方.
最后, 使用M5StickC-Plus来开发一些有用的程序, 还需要使用到M5StickC-Plus的SDK, 然而, 把这个SDK和ESP-EYE的数据采集SDK糅合在一起, 并不能通过直接复制粘贴到一个文件夹来实现, 幸运的是, IDF提供了一个很好的框架, 只需要正确理解IDF的component框架就能将arduino, M5StickC-Plus SDK和edge-impulse-sdk集成到一起.
4. 主要步骤
难点解决了, 剩下的工作就是按部就班的开发TinyML程序, 主要包括以下步骤:
- 编译烧录采集固件
- 使用edge-impulse采集数据
- 选择合适的模型和参数
- 使用数据训练模型
- 打包下载模型数据文件
- 集成模型数据文件到项目
- 开发应用功能, 主要是控制灯板的代码逻辑和模型推断相关的模型应用逻辑
- 编译烧录测试
5. 软件实现
以下介绍软件实现, 如前所述, 项目将多个组件集成到了一起, 以下将挑选一些代码进行讲解, 详细代码可以参考以下链接:
- 使用HSPI的M5StackC-Plus SDK修改版本
https://github.com/picospuch/M5StickC-Plus.git
- 主要代码包含重构的edge-impulse-sdk和应用实现程序
https://github.com/picospuch/eetree-funpack-workshop/tree/phase-summer2022
流程图:
(1) ESP32-IDF配置
(2) 推断部分关键代码
// function in loop
void tinyml_run_impulse(void)
{
switch(state) {
case INFERENCE_STOPPED:
// nothing to do
return;
case INFERENCE_WAITING:
if(ei_read_timer_ms() < (last_inference_ts + 2000)) {
return;
}
state = INFERENCE_SAMPLING;
ei_microphone_inference_reset_buffers();
break;
case INFERENCE_SAMPLING:
// wait for data to be collected through callback
if (ei_microphone_inference_is_recording()) {
return;
}
state = INFERENCE_DATA_READY;
break;
// nothing to do, just continue to inference provcessing below
case INFERENCE_DATA_READY:
default:
break;
}
signal_t signal;
signal.total_length = continuous_mode ? EI_CLASSIFIER_SLICE_SIZE : EI_CLASSIFIER_RAW_SAMPLE_COUNT;
signal.get_data = &ei_microphone_inference_get_data;
// run the impulse: DSP, neural network and the Anomaly algorithm
ei_impulse_result_t result = { 0 };
EI_IMPULSE_ERROR ei_error;
if(continuous_mode == true) {
ei_error = run_classifier_continuous(&signal, &result, debug_mode);
}
else {
ei_error = run_classifier(&signal, &result, debug_mode);
}
if (ei_error != EI_IMPULSE_OK) {
ei_printf("Failed to run impulse (%d)", ei_error);
return;
}
if(continuous_mode == true) {
if(++print_results >= (EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW >> 1)) {
display_results(&result);
print_results = 0;
}
}
else {
display_results(&result);
}
if(continuous_mode == true) {
state = INFERENCE_SAMPLING;
}
else {
ei_printf("Starting inferencing in 2 seconds...\n");
last_inference_ts = ei_read_timer_ms();
state = INFERENCE_WAITING;
}
}
(3) 灯板部分关键代码
inline void Render() {
int n = 3; // n = refresh-freq / frame-freq
SPI.beginTransaction(SPISettings(spi_speed, MSBFIRST, SPI_MODE0));
digitalWrite(25, HIGH);
if (!leds_on)
return;
for (int j = 0; j < n; ++j) {
if (!lcd_direction_mode)
{
for (int i = 0; i < 8; ++i) {
// row, then col
digitalWrite(25, LOW);
SPI.transfer(1 << i);
SPI.transfer(LEDS[i]);
digitalWrite(25, HIGH);
}
}
else // Render method 2. Optional
{
int i = 0;
for (; i < 8; ++i) {
digitalWrite(25, LOW);
SPI.transfer(1 << i);
SPI.transfer(LEDS[7 - i]);
digitalWrite(25, HIGH);
}
digitalWrite(25, LOW);
SPI.transfer(1 << i);
SPI.transfer(0);
digitalWrite(25, HIGH);
}
}
SPI.endTransaction();
++fps_counted_frames;
}
具体代码讲解详见视频.
6. 硬件实现
LED训练板需要3个GPIO才能驱动, 但是如果用麦克风, M5StackC-Plus就无法使用G0, 所以hat connector只能提供2个GPIO, 剩下的一个G33从Grove接口引出.
具体接线如下:
'''
| M5StickC-Plus | LED训练板 |
|---------------+-----------|
| GND | GND |
| 3V3 | VCC |
| G36/G25 | RCLK |
| G26 | ->D |
| G33 | SRCLK |
'''
7. 心得体会
- ESP32有用很多粉丝是很好解释的, 价格亲民, 围绕芯片有很强的配套软件开发体系, 最关键的是开源.
- ESP32和RP2040是我目前最喜欢的两种MCU, ESP32的蓝牙和Wifi的能力在做本项目的时候还没有充分利用, 或许RP2040接个mic也能实现本期的项目. 但是在IoT时代, 连接的重要性不言而喻, 将来一定会有集成蓝牙或者Wifi或者其他连接协议的RP2040系列芯片, 就像默认集成了USB一样.
8. 其他说明
感谢硬禾学堂举办本期2022暑期练活动,让我有机会比较深入了解ESP32,让我能在业余时间参与更多有趣项目的学习,也感谢群的小伙伴提供很多种实现题目功能的思路,感谢各大佬的代码铺垫, 要不然没法快速完成这个小任务, 感谢大家一路的折腾与陪伴,谢谢!
附件为可以烧录测试的固件, 请使用esptool.py烧录测试.