【得捷电子Follow Me第二季第4期】Nano RP2040 Connect 的机器学习实...
[复制链接]
本帖最后由 genvex 于 2024-12-30 23:58 编辑
【得捷电子Follow Me第二季第4期】基于Arduino Nano RP2040 Connect 的机器学习实践(汇总)
欢迎大家来到得捷与eeworld联合举办的Follow me第二集的都最终季,这一次我们玩的一个加强版的RP2040,这个板子集成了WIFI,加速度传感器,麦克风,覆盖机器学习项目所需要的硬件支撑。在本次活动中,同时配了一个NANO专用grove接口的扩展板,开拓了NANO和外部世界连接的窗口,同时还购买了一片经济实惠使用oled屏幕,用于展示输出的数据和结果,简直不能再完美了。
项目内容目录
一、编程环境搭建
二、任务1:LVGL点屏点灯
三、任务2:LVGL开机动画及水平仪UI 设计
四、任务3:风吹乱了我的秀发(声音数据+灯光展示)
五、进阶任务:FFT声音动态灯光秀
六、进阶任务:TinyML运动姿势机器学习实践
一、编程环境搭建
官方的例程都提供了Arduino环境的参考代码,Arduino的IDE不适用大型项目管理,虽然本次活动的任务也足够简单,本着把简单问题复杂化的做法,还是在VSCODE开展了全面编码工作。
鉴于本次的项目任务相对分散,没有将所有任务合成一个项目,但是完成了将所有工作都放置在一套依赖体系里面,调试起来非常友好和方便。对项目的工程文件platformio.ini 作了以下规划和调整,请仔细阅读下图,对你定有参考意义。
二、任务1:LVGL点屏点灯
开篇即开挂。完成了基于 Arduino Nano RP2040 Connect 开发板的 LVGL 图形界面示例,主要实现了在 SSD1306 OLED 显示屏上显示中英文混合文本。通常LVGL在彩色LCD屏幕上使用较多,在oled上运行LVGL的案例可谓少见。一来觉得黑白的oled颜色非黑即白没有什么可玩性,二来实现起来的难度不小。 经过长期的摸索学习,终于形成了一套在oled屏上运行LVGL的方法。经过多次验证本套代码适用于esp32系列和树莓派rp2040系列开发板,其他的开发板应该也可以。 该项目得以实施的关键在于LovyanGFX 为oled屏幕提供基于灰阶的刷屏函数,理论上可以把黑色到白色细分为256个等级,使得oled不再是非黑即白,呈现了一度颜色梯度增加了观赏性。
这里留一个问题:中文显示是怎么实现的,可以将你的答案放在讨论区。
static void lvgl_begin(void)
{
// 初始化 OLED 显示屏
oled.init();
// 初始化 LVGL
lv_init();
// 配置显示缓冲区
lv_disp_draw_buf_init(&disp_buf, buf1, buf2, DISP_BUF_SIZE);
// 设置显示驱动参数
static lv_disp_drv_t disp_drv;
disp_drv.hor_res = LVGL_HOR_RES; // 128 像素宽
disp_drv.ver_res = LVGL_VER_RES; // 64 像素高
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &disp_buf;
// 创建并显示文本标签
lv_obj_t* label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "你好 世界!\nHello DigiKey\n & EEWorld! ");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}
三、任务2:LVGL开机动画及水平仪UI 设计
开机动画足够炫酷,开机启动的时候是三个弧形在动态旋转,之后博主的头像就逐渐显现,然后进入水平仪UI.这套动画方案是从乐鑫开机动画学来的,在他的基础上,任意增加渐变显示图片和文字的动画,如果你没看明白或者你也想学可以给我留言说说哪里不明白的地方。
原创的水平仪设计,用一个圆环作为一个黑色小球的运行轨道,黑色小球可以根据加速数值的变化计算倾斜的角度,可以用做量角器。 UI的设计没少花功夫,特别是如何修饰圆形上的组件,欢迎你仔细品鉴。
(水平仪的UI核心代码)
void MpuView::create()
{
/** screen */
lv_obj_t *root = lv_obj_create(NULL);
lv_obj_clear_flag(root, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_size(root, LV_HOR_RES, LV_VER_RES);
lv_obj_center(root);
lv_obj_set_style_pad_all(root, 0, 0);
lv_obj_set_style_bg_color(root, lv_color_black(), 0);
lv_obj_set_style_bg_opa(root, LV_OPA_100, 0);
ui.root = root;
// lv_obj_set_flex_grow(ui.city, 1);
static lv_style_t style;
lv_style_init(&style);
/*Make a gradient*/
lv_style_set_bg_color(&style, lv_color_black());
// lv_style_set_border_opa(&style, LV_OPA_100);
lv_style_set_bg_opa(&style, LV_OPA_100);
lv_obj_t *chart = lv_chart_create(root);
// lv_obj_remove_style_all(game_station);
lv_obj_set_size(chart, LV_PCT(100), LV_PCT(80));
lv_obj_align(chart, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_add_style(chart, &style, 0);
lv_obj_set_style_border_color(chart, lv_color_black(), 0);
lv_obj_set_style_radius(chart, 0, 0);
lv_chart_set_axis_tick(chart, LV_CHART_AXIS_PRIMARY_Y, 5, 3, 5, 2, true, 10);
lv_chart_set_point_count(chart, 100);
lv_chart_set_div_line_count(chart, 0, 0);
ui.chart = chart;
lv_obj_add_flag(chart, LV_OBJ_FLAG_HIDDEN);
lv_chart_set_type(chart, LV_CHART_TYPE_LINE); /*Show lines and points too*/
lv_obj_set_style_size(chart, 0, LV_PART_INDICATOR);
/*Add two data series*/
ser1 = lv_chart_add_series(chart, lv_color_white(), LV_CHART_AXIS_PRIMARY_Y);
ser2 = lv_chart_add_series(chart, lv_color_white(), LV_CHART_AXIS_SECONDARY_Y);
lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, -1, 1);
lv_chart_set_range(chart, LV_CHART_AXIS_SECONDARY_Y, -1, 1);
lv_obj_t *labelaccX = lv_label_create(root);
lv_obj_set_size(labelaccX, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_label_set_text_fmt(labelaccX, "X:%.2f,Y:%.2f", 0.25, 0.25);
lv_obj_align(labelaccX, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_set_style_text_color(labelaccX, lv_color_white(), 0);
lv_obj_set_style_text_font(labelaccX, &lv_font_montserrat_16, 0);
ui.labelaccX = labelaccX;
lv_obj_t *arc = lv_arc_create(root);
lv_obj_set_size(arc, 60, 60);
lv_obj_clear_flag(arc, LV_OBJ_FLAG_CLICKABLE);
// 改变进度条颜色
// lv_obj_set_style_bg_color(arc, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLUE); // 轨道颜色为蓝色
// lv_obj_set_style_local_bg_color(arc, LV_OBJ_PART_INDICATOR, LV_STATE_DEFAULT, LV_COLOR_GREEN); // 进度条颜色为绿色
// lv_obj_set_style_border_width(arc, 5, 0); // 设置进度条的宽度
lv_obj_set_style_arc_width(arc, 5, LV_PART_MAIN|LV_PART_INDICATOR);
lv_obj_set_style_arc_color(arc, lv_color_white(), LV_PART_MAIN|LV_PART_INDICATOR); // arc color ,not the value color
lv_arc_set_rotation(arc, 270);
lv_arc_set_bg_angles(arc, 0, 360);
lv_arc_set_angles(arc, 0, 40);
lv_obj_center(arc);
ui.arc = arc;
}
四、任务3:风吹乱了我的秀发(声音数据+灯光展示)
根据作业任务要求“学习PDM麦克风技术知识,调试PDM麦克风,通过串口打印收音数据和音频波形。” ,我把麦克风的数据采集了,然后再oled上展示出来,同时串口也可以看到麦克风数据,打开串口查看工具就可以看大波形。
外界声音较小的时候,在oled的上的波形是一条平稳的支线,当你向它吹一口气,线条就顿时凌乱起来。 具体实现方案请查看代码。
五、进阶任务:FFT声音动态灯光秀
根据作业要求“通过RGB LED不同颜色、亮度显示PDM麦克风收到的声音大小;”,难度不算大,因此,在这里加大了难度,使用FFT组件分析采集到的声音数据,然后将频谱数据划分3个部分,就叫低频、中频、高频。它非常灵敏地侦测到外界声音的变化,灯光熠熠生辉,甚是有趣。 难度在于对FFT理解,FFT函数的应用实践,这里也不作过多地解析,反正关于FFT的故事还在继续。
(核心代码)
short sampleBuffer[SAMPLES];
volatile int samplesRead;
// FFT 对象
// ArduinoFFT FFT = ArduinoFFT();
ArduinoFFT<double> FFT = ArduinoFFT<double>();
// 频谱数据
double vReal[SAMPLES];
double vImag[SAMPLES];
// PDM 数据回调函数
void onPDMdata() {
int bytesAvailable = PDM.available();
PDM.read(sampleBuffer, bytesAvailable);
samplesRead = bytesAvailable / 2; // 每个样本 2 字节
}
// 计算频带强度
double calculateFrequencyBand(double* data, int start, int end) {
double sum = 0;
for (int i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
// 设置 LED 亮度
void setLEDs(double lowFreq, double midFreq, double highFreq) {
// 将强度映射到 0-255 范围
int lowBrightness = map(lowFreq, 0, 10000, 0, 255);
int midBrightness = map(midFreq, 0, 10000, 0, 255);
int highBrightness = map(highFreq, 0, 10000, 0, 255);
// 限制亮度范围
lowBrightness = constrain(lowBrightness, 0, 255);
midBrightness = constrain(midBrightness, 0, 255);
highBrightness = constrain(highBrightness, 0, 255);
// 设置 LED 的亮度
analogWrite(LEDR,lowBrightness);
analogWrite(LEDG, midBrightness);
analogWrite(LEDB,highBrightness);
}
void setup() {
// 初始化串口
Serial.begin(115200);
oled.begin();
oled.setRotation(0);
// 配置 LED 引脚为输出
pinMode(LEDR, OUTPUT); // 设置红色 LED 引脚为输出
pinMode(LEDG, OUTPUT); // 设置绿色 LED 引脚为输出
pinMode(LEDB, OUTPUT); // 设置蓝色 LED 引脚为输出
// 配置数据接收回调函数
PDM.onReceive(onPDMdata);
// 初始化 PDM 麦克风:
// - 一个通道(单声道模式)
// - 20 kHz 采样率
if (!PDM.begin(channels, SAMPLING_FREQUENCY)) {
Serial.println("无法启动 PDM!");
while (1); // 启动失败时停止程序
}
}
void loop() {
// 如果读取到新的样本
if (samplesRead) {
// 将样本数据复制到 vReal
for (int i = 0; i < SAMPLES; i++) {
vReal[i] = sampleBuffer[i];
vImag[i] = 0;
}
// 执行 FFT
FFT.windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
FFT.compute(vReal, vImag, SAMPLES, FFT_FORWARD);
FFT.complexToMagnitude(vReal, vImag, SAMPLES);
// 计算低频、中频和高频的强度
double lowFreq = calculateFrequencyBand(vReal, 0, SAMPLES / 3); // 0-2kHz
double midFreq = calculateFrequencyBand(vReal, SAMPLES / 3, SAMPLES*2 / 3); // 2-4kHz
double highFreq = calculateFrequencyBand(vReal, SAMPLES *2/ 3, SAMPLES ); // 4-8kHz
// 控制 LED 亮度
setLEDs(lowFreq, midFreq, highFreq);
// 重置样本计数
samplesRead = 0;
}
}
六、进阶任务:TinyML运动姿势机器学习实践
本项目包含两个相关示例:数据采集器 (5_TinyML_dataCollector)模型推理 (5_TinyMLimu_inferencing) , 使用Edge Impulse训练的模型进行推理,基于加速度传感器数据进行手势识别。
从头完成一个机器学习项目步骤真的很多,工作也很琐碎,劳动量也不少,例如数据采集部分是一个很费时费力的环节,反正就是“大力出奇迹”吧,数据也多理论上预测会更精确,也不排除一开始的方向就是错的。 来来来,先别走,我们一起把这个流程过一遍,有个感性-性感认识也行。
(1)连接Edge Impulse:
Edge Impulse 是一个领先的边缘人工智能开发平台,支持多种数据类型,如加速度计数据、音频、图像等,也可收集来自各种传感器硬件、公共数据集等的数据。提供创建脉冲(Impulse)的功能,包含处理模块(如频谱分析)和学习模块(如分类、回归、异常检测等),可自定义构建模型。具有边缘优化神经(Edge Optimised Neural, EON)编译程序,编译出的神经网络推论模型能减少内存使用和储存空间。运用数字信号处理区块(DSP Block)进行声音推论前的前置处理,可加快推论速度并提高精准度。支持多种开发板,方便记录和上传数据集。
数据采集环节需要解决一个关键问题是怎么把数据投喂给这个网络平台,这个平台还在国外。 据介绍数据上传的方式也有几种,例如可以把数据保存下来,然后以数据包的方式上传。另外,最直观和推荐的方式是在线采集。 Edge Implus为数据采集提供了一套方案,具体如下:
创建项目:在Edge Impulse新建项目并选类型。
安装及运行工具:在电脑装Edge Impulse CLI,在终端等运行sudo edge - impulse - data - forwarder启动。
安装最新 LTS 版本:
choco install nodejs-lts
npm install -g edge-impulse-cli --force
npm install -g edge-impulse-cli
npm cache clean --force
npm install -g edge-impulse-cli
把edge-impulse-cli安装成功就已经成功一半了,后面的就按提示继续进行就可以了。
- 设备连接:用CLI连接设备与Edge Impulse,登录、选项目、命名传感器和设备。
- 数据采集:选“3 axes”传感器,命名标签,改采样长度后开始采样,按指定动作移动设备并保持。
- 数据分割:点击“Split Sample”,添加并分割数据段。
- 重复采集:重复采集和分割步骤,用不同名称标记不同运动数据。
(2) 模型构建与测试:
- 设置频谱特征:设置并保存频谱特征参数,生成特征。
- 训练模型:点击NN Classifier开始训练,选Unoptimized (float32)。
- 模型测试:点击Model testing并点击Classify all进行测试,准确率低可增加训练集和采样时间。
(3) 部署与验证:
- 构建库文件:在Edge Impulse点击Deployment,选Arduino Library构建并下载.ZIP文件,通过Arduino IDE添加该库。
晕了没有,如果没有晕还想继续的话,点这个链接:
https://wiki.seeedstudio.com/XIAO-RP2040-EI/
这个教程有些老,但步骤大差不差的,主要是跑通后,还需要多次练习才行,检测的精度才会有所提升。
总结:
本次项目完满成功,取得多项重大进展:
- 基于vscode-pio的多项目demo文档管理方式,这种方式很适合开发板的配套Demo使用。
- 继续推广lvgl在单色屏上的应用,效果还不错,今后还会继续使用,希望更多人用起来,最好把本篇文章和本人的名号也引用一下,多谢了。
- 使用了FFT技术对声音进行频谱处理并映射到三色灯上。
- 最后,完成了基于Edgde Impluse的TinyML的动作识别机器学习实践。 这个内容是参加本次活动的主要目的。 虽然主板上已经集成了机器学习的算法,简单的调个包就可以出来结果,结果精度也不错,但是那些都是黑匣子,自己没有主动权。
最后的最后:
参加活动最开心的时候是功能探索的过程,最痛苦的时候是写报告。
虽然主办方给的题目都是“点灯题目”,简单完成任务是可以,但肯定少学到很多东西,参加活动不是真的为了嫖板子,主要是利用交作业的压力,把压力化解为学习的动力,更能驱动自己学习到更多的东西。
预祝大家在新的一年里学习进步,身体健康,万事如意!
|