【Follow me第二季第1期】+ CPE基于VS-CODE PIO的多功能挂饰开发(完整项目)
[复制链接]
本帖最后由 genvex 于 2024-9-3 10:57 编辑
本项目将包括以下内容:
入门任务、开发环境搭建
VS-CODE -platformIO 编程环境(有坑),入门任务很高阶。
任务一、炸街灯光秀
多种炫酷灯光效果随意切换
任务二、环境温度和光线强度监测
光温效果立竿见影
任务三、谜之接近检测
红外传感器的逆袭
进阶任务、不倒翁优雅实现
Atan2关键函数深度解析
创意任务、精彩挂饰
灯光秀+midi音乐播放器(助攻生日趴)
Circuit Playground Express(下文简称CPE)基于ATSAMD21微控制器,采用32位ARM® Cortex®-M0+内核。ATSAMD21采用先进的电源管理技术,电流消耗极低。它可以由USB、“AAA”电池组或Lipoly电池供电。可通过内置USB快速连接进行编程,无需专用电缆或适配器。非常适合用于电子产品和编程,能够让创意充分发挥。
本项目基于CPE的功能挂饰开发,包括编程环境搭建、多个任务的实现等内容。在编程环境搭建中,通过解决初始代码编译报错问题,最终在VSCODE的platformIO平台上成功为CPE新建项目。在任务实现方面,完成了炸街灯光秀、环境温度和光线强度监测、谜之接近检测、不倒翁优雅实现以及精彩挂饰等任务,如利用灯珠实现多种灯光效果切换、监测环境数据并在灯珠上显示、通过红外脉冲判断物体接近、感受重心变化实现不倒翁功能以及融合midi播放和灯光秀等。
全部任务内容在一个套代码中实现,无需分次上传。
如果不想看后面详细展开看以下精简版内容也可以获取本文主要核心内容:
本项基于VS - CODE PIO的Circuit Playground Express(CPE)多功能挂饰开发项目,
具体内容包括:
1. 项目简介:
• CPE基于ATSAMD21微控制器,采用32位ARM Cortex - M0 +内核,可由多种方式供电,适合电子产品和编程。 • 项目包括编程环境搭建和多个任务的实现,全部任务内容在一个套代码中实现。
2. 编程环境搭建:
• CPE支持多种编程环境,本项目选择在VSCODE的platformIO平台上搭建,但初始代码编译报错。
• 通过对比Seeed Xiao ATSAMD21项目,发现是vscode - PIO提供的Adafruit TinyUSB Library有问题,最终通过替换Seeed的TinyUSB驱动解决。 • CPE在该编程环境下编译上传速度快。
3. 任务实现:
• 灯光秀:利用CPE上的10颗灯珠实现多种炫酷灯光效果切换,通过函数数组和“RainbowCycleDemo”类实现灯光模式切换。
• 环境温度和光线强度监测:光线强度传感器连接到模拟引脚A8,温度传感器使用NTC热敏电阻器,通过复杂的计算公式获取温度,将光照强度和温度数值映射到灯珠上进行简易显示。
• 接近检测:通过sendIRPulse()函数发送红外脉冲,根据A10的模拟量判断物体接近,实现距离检测和相应的显示及警报功能。
• 进阶任务、不倒翁实现:用CPE感受重心变化,通过螺丝孔和龟苓膏塑料盖等制作不倒翁,核心关键代码中使用atan2函数计算极角,解决了atan在x值接近0时不稳定的问题。
• 创意任务、精彩挂饰:实现midi音乐播放器和灯光秀的融合,对midi音频数据序列进行解码,根据音符字符串播放对应的音符。
4. 心得体会:
• 项目开始觉得简单,实际很有趣,得益于充分的调研和CPE的配置提升。
• 用类把所有任务内容用一套代码实现,按键B实现功能切换,按键A实现功能内部模式切换,通过时间判断实现midi播放和灯光秀同时进行。 • CPE背后Adafruit团队的板卡支持库和案例通俗易懂。
5. 不足之处:篇幅有限,不能呈现实验过程细节,可下载代码包中的免编译固件进行体验测试。 代码包中有firmware.bin固件,可双击reset键进入bootloader模式,将firmware.bin文件放在新弹出的虚拟优盘上验证功能。 参考资料包括多个相关网站链接。
谢谢,阅读!
一、编程环境搭建
CPE当前支持通过Microsoft MakeCode,circuitpython,Arduino等编程环境开发。 MakeCode适合儿童编程启蒙课程,circuitpython是Adafruit公司主打的编程生态,非常完善和强大,很多功能都封装好,入门学习比较顺滑。Arduino 依仗全球的创客生态,只有你没想到的,没有你做不到的。 因为Circuit Playground Express采用的ATSAMD21微控制器也是一款成熟的产品了,跟国内的ESP32同台竞技好多年,在VSCODE上的platformIO平台上肯定有支持。PIO的底层还是Arduino,但是继承了VSCODE所有资产,做一些复杂的内容也可以从容应对。 根据Circuit Playground Express关键词就可以在PIO上新建一个项目。
如果不出意外,肯定出意外了。 (见下图)初始代码编译测试就报错,百思不得其姐,差点就放弃了。经过几天休养,灵光一动,采用同样芯片的Seeed Xiao ATSAMD21肯定是好用的吧,这么多人用,CPE 是几乎没有人用PIO来玩的,所以有bug也没人发现。于是新建了基于Seeed Xiao ATSAMD21项目,编译顺利通过,但demo代码上传到CPE,却没有反应,这也正常呀,Xiao的引脚跟CPE的管脚设置是不同的。 但给到了一个出路,就是Xiao的支持环境是正常的。
后来根据报错提示,最终确认是vscode -PIO 提供的Adafruit TinyUSB Library有问题(可能是一些低级的错误)用Xiao的支持库所以把Seeed的TinyUSB 驱动拿过来放到对应的目录上,借尸还魂,可以完美解决。还想去给PIO提个意见那啥的,算了,作业都还没完成。 (替换文件夹在附件链接细找)
不足1秒就完成编译上传了,ESP32什么时候享受过这样的待遇,太快了。
任务一、炸街灯光秀
CPE上有10颗灯珠,开始的时候对它还是挺嫌弃的,怎么不搞12颗呢,后来发现就这么10颗灯珠就可以玩出花来了,可以做出很多炫酷的灯效。
下面先来看看灯光秀。
先用一个函数数组灯光效果函数放进去,就像有彩虹灯效、旋转灯效、闪光灯等函数)
构造了“RainbowCycleDemo”类,这个类继承自“Demo”类。“loop”方法能在程序的主循环中频繁被调用。然后,根据“mode”的值,从“ligthList”数组中选择对应的灯光类型函数并调用它,从而实现不同灯光效果的切换展示。“modePress”方法则最终被按键A调用,实现了灯光模式内循环切换。
光线强度传感器连接到模拟引脚A8。在Arduino编程环境取值在0和1023之间,对于大多数室内光线水平来说,大约300的读数很常见。
温度传感器使用NTC热敏电阻器(村田NCP15XH103F03RC)。虽然它不是全集成温度传感器,但具有线性输出,但可以根据模拟引脚#A9的模拟电压轻松计算出温度。就是数值计算起来比较复杂。温度计算公式:
float Adafruit_CircuitPlayground::temperature(void) {
// Thermistor test
double reading;
reading = analogRead(CPLAY_THERMISTORPIN);
// Serial.print("Thermistor reading: "); Serial.println(reading);
// convert the value to resistance
reading = ((1023.0 * SERIESRESISTOR) / reading);
reading -= SERIESRESISTOR;
// Serial.print("Thermistor resistance: "); Serial.println(reading);
double steinhart;
steinhart = reading / THERMISTORNOMINAL; // (R/Ro)
steinhart = log(steinhart); // ln(R/Ro)
steinhart /= BCOEFFICIENT; // 1/B * ln(R/Ro)
steinhart += 1.0 / (TEMPERATURENOMINAL + 273.15); // + (1/To)
steinhart = 1.0 / steinhart; // Invert
steinhart -= 273.15; // convert to C
return steinhart;
}
我们需要做的主要工作是将光照强度和温度数值映射到灯珠上进行简易显示。
首先,通过CircuitPlayground.lightSensor()获取当前的光照强度值,并存储在变量light中。然后,使用lerp的线性插值函数,把光照量程映射 9号灯珠(初始值)和 5号灯珠(结束值),灯光颜色选择为蓝色。
根据温度设置左侧像素点颜色:类似地,通过CircuitPlayground.temperature()获取当前温度值,并存储在变量tempC中。再次使用lerp函数根据温度值tempC、温度量程映射到映射5号灯珠(初始值)和 0号灯珠(结束值),温度演示颜色选择红色。
任务三、谜之接近检测
void sendIRPulse()
{
for (int i = 0; i < 32; i++)
{
digitalWrite(irTransmitterPin, HIGH);
delayMicroseconds(13);
digitalWrite(irTransmitterPin, LOW);
delayMicroseconds(13);
}
}
void displayDistance(int distance)
{
int ledCount = map(distance, minDistance, safeDistance, 0, 9); // 将距离值映射到0-10的LED数量
Serial.print("Distance: ");
Serial.print(distance);
Serial.print(", LED Count: ");
Serial.println(ledCount);
for (int i = 0; i < 10; i++)
{
if (i < ledCount)
{
CircuitPlayground.setPixelColor(i, 0, 255, 0);
}
else
{
CircuitPlayground.setPixelColor(i, 0);
}
}
}
void checkForIntrusion(int distance)
{
if (distance > SAFE_DISTANCE)
{
Serial.println("Intrusion detected!");
playAlertTone();
}
}
inline void looping()
{
if (millis() - timeElapsed > 300)
{
sendIRPulse();
// CircuitPlayground.irSend.send(MY_PROTOCOL,MY_POWER,MY_BITS);
distance = analogRead(irReceiverPin); // 读取红外传感器的值
if (firstFlag)
{
minDistance = distance;
firstFlag = false;
safeDistance = minDistance + 50;
}
Serial.print("Received distance: ");
Serial.println(distance);
displayDistance(distance);
checkForIntrusion(distance);
timeElapsed = millis();
}
}
这个实验成果与否就取决于这个函数,在读取说明书上的接近传感器引脚的数值之前必须进行的操作,尝试过其他发射方式不成功。
sendIRPulse()函数每次调用时生成一系列32个红外脉冲,每个脉冲持续26微秒,具有50%的占空比(13微秒高电平,13微秒低电平)。通过红外脉冲实现测距的原理主要基于红外线的发射和接收之间的时间差。这种方法通常称为脉冲时间飞行法(Time of Flight, ToF)。然而这里我们需要根据A10的模拟量来大约的判断有物体接近,这不是一种很常见的用法,都是玩具,我觉得也不一定要这么较真呀。
(彩蛋,当我们把手靠近CPE的时候,蜂鸣器会发出biubiu的声音,还会喊“danger,danger”)
进阶任务、不倒翁优雅实现
不倒翁的意思是用CPE来感受重心的变化,利用10颗灯珠通过重心极角指映射到对应点亮灯珠。
不倒翁制作过程
得益于塑壳背后别有用心的预留了一个M4还是多少的螺丝孔,这个穿孔所用螺丝比较容易获取,例如相机底座的固定螺丝也是同款的。在龟苓膏塑料盖中心开一孔让螺丝穿过,边缘也开一个小孔让电池线头穿过,不要太靠塑料盖边缘,这样不美观。然后电池舱藏在龟苓膏罐里头,用些纸皮固定,由于电池比较重,固定电池的位置比其他地方重,造成了重心偏离中心的情况,正好符合不倒翁的工作模式。
virtual void loop() {
static float X, Y, Z;
X = CircuitPlayground.motionX();
Y = CircuitPlayground.motionY();
Z = CircuitPlayground.motionZ();
static int old_x, old_y;
/*滤波*/ //一阶低通滤波器,减少灯光因为加速度传感器的漂移产出的闪烁。
X = old_x * 0.6 + X * 0.4;
old_x = X;
Y = old_y * 0.6 + Y * 0.4;
old_y = Y;
Serial.print("X: "); // 左右倾斜是 x
Serial.print(X);
Serial.print(" Y: "); // 前后倾斜是 y
Serial.print(Y);
Serial.print(" Z: "); // 水平轴向;
Serial.println(Z);
// atan2函数返回的角度范围是-π到π,通过乘以180/PI转换为度,并加上180,使得角度范围变为0到360。
// map函数用于将角度映射到LED索引,确保角度和LED索引之间的对应关系是线性的。
float theta = atan2(Y, X) * 180 / PI + 180; // originally the value range is -pi - pi, add 180 for easy understand.
// in order to understand the relationship between the angle and lights ,we need to watch the change of the r and thera.
Serial.print("theta:");
Serial.println(theta);
int led_point2 = map(theta, 0, 360, 7, 17); // the zero point the Led 7, in order to map the numbers of the leds,7+10 =17 ,return the zero point.
CircuitPlayground.clearPixels(); // Set all pixel colors to 'off'
uint32_t offset = millis() / 100;//等过的渐变速度以100毫秒为单位发生变化。
for (int i = 0; i < 3; i++)
{
CircuitPlayground.strip.setPixelColor((led_point2 + i) % 10, CircuitPlayground.colorWheel((((led_point2 + i) * 256 / 10) + offset) & 255));
}
CircuitPlayground.strip.show();
delay(10);
}
核心关键代码
float theta = atan2(y_,x_ )*180/PI+180; // 计算极角,还原到0-360便于理解。在完成这个实验,我认为最精彩的部分莫过于使用了使用 atan2这个函数可以完美解决atan在x值接近0时不稳定的现象,数学还得好好学。
atan2是功能解析:
一、明确的参数顺序和结果范围
- 参数顺序明确:atan2(y, x)接收两个参数,先指定纵坐标的值y,再指定横坐标的值x。这种明确的参数顺序使得在调用函数时不容易出错,并且在复杂的计算中更容易理解和追踪参数的来源。
- 结果范围清晰:该函数返回值的范围是从 -π 到 π。这个固定的范围使得结果具有确定性,方便进行后续的角度计算和处理。无论是在平面几何、图形绘制还是物理模拟等领域,都能准确地确定角度,避免了因角度范围不明确而导致的错误。
二、处理特殊情况准确
- 处理零值情况:当 x 为零且 y 为正时,atan2 返回 π/2;当 x 为零且 y 为负时,返回 -π/2。这种准确的处理方式在涉及到垂直方向的角度计算时非常有用,避免了因分母为零而产生的计算错误。
- 处理坐标轴上的情况:当 y 为零时,根据 x 的正负返回 0 或 ±π。这使得在处理与坐标轴平行的情况时,能够准确地确定角度,为图形绘制、向量分析等提供了准确的角度信息。
三、在二维向量和坐标计算中表现出色
- 二维向量的角度计算:在处理二维向量时,atan2 可以方便地计算向量与 x 轴正方向之间的夹角。这在计算机图形学中非常重要,例如在绘制图形时确定物体的方向、进行旋转操作等。通过 atan2 可以快速准确地计算出向量的角度,从而实现各种图形变换和动画效果。
2. 坐标转换和定位:在地理信息系统、游戏开发等领域,经常需要进行坐标转换和定位。atan2 可以用于计算两点之间的方位角,从而确定物体在平面上的相对位置。例如,在游戏中可以根据玩家和目标的坐标计算出玩家面向目标的角度,以便进行角色的转向和攻击。
难点说明:
int led_point2 = map(theta, 0, 360, 7-17); //
为什么映射都(7-17)?因为0度的时候正好是7号的位置,后面超过10点的灯珠我们用取余的方法,让它们又回到了正常的位置,这是没有实验现象辅助会有点难理解。
实现非阻塞的播放功能和灯光秀功能的完美融合。用一套的非常规播放逻辑来实现midi音符解码。这里提供了几首midi曲目,有我最爱的《我爱和我的祖国》(又红又专),还有这次特别制作的生日歌,可以在小伙伴生日的时候掏出来,活跃气氛,没有蜡烛,灯光秀顶上,临时救个场也不是不可以的。
Midi 曲目的数据格式:
const char *my_people_my_country = "#,#,A#5,A#5,A#5,A#5,C6,C6,C6,...#”
每个音符使用逗号进行分隔,接下来,函数会根据设定的节拍时间(音调的播放持续时间)判断是否到达了一个节拍点。如果到达了节拍点,函数会解析字符串中的音符,并播放对应的音符。
音符的识别使用了map容器来快速根据音符字符串查询到对应的音符频率数值,具体实现方式查看源码。
std::map<String, int> tones = {
{"C0", 16}, {"C#0", 17}, {"D0", 18},,,,{"A#9", 14917}, {"B9", 15804}};
解析字符串中的音符的逻辑是函数会移动指针到下一个逗号的位置,以准备播放下一个音符。如果还有逗号存在,函数会继续播放下一个音符, 如再未检测到逗号,表明播放结束,歌曲播放结束后会检测是否循环播放。
可用于转换 onlinesequencer.net schematic format 的乐谱
页:onlinesequencer.net
心得体会
本次任务开始的时候觉得项目太简单了,可能学不到啥东西,后来发现这个东西还是非常有趣的。 主要得益于在采购阶段做好了充分的调研工作,买买买我是专业的,给CPE精心配置了一个水晶外壳(塑料的),立马提升了一个档次,灯光秀就更加出彩了,然后花了巨资购买了一条粗犷的挂绳(还是一条typeC充电线)来搭配CPE,这样就可以用一种另类的方式出街啦,成为这条街最亮D仔。
另外,在代码方面,用类把所有以上任务内容用一套代码实现,按键B实现功能切换,按键A实现功能内部模式切换。代码思路是非阻塞式运行(拒绝使用delay),因为简单尝试了freeRTos的移植,好像问题多多,折中通过时间判断完成了midi播放和灯光秀同时进行。 任务中也不乏亮点例如不倒翁功能的实现。
总体来说,任务还是比较轻松愉悦的,这里主要功劳还是CPE背后Adafruit团队的板卡支持库和案例做得非常通俗易懂,没有八股文代码那么晦涩难懂(我没有说idf哈),所以一个板卡成功与否,除了硬件性能,板卡的支持库也是非常重要的。
该项目给今年的夏天留下浓重的色彩,怀着不舍之情,结束了这次活动的学习,让我继续砥砺前行,迎接更加猛烈的风雨!
不足之处就是篇幅有限不能把实验过程的细节一一呈现,喜欢的朋友可以下载代码包中的免编译固件,进行体验测试。
代码包中有firmware.bin 固件,可以验证功能。
而已双击reset键进入bootloader模式,报firmware.bin 文件放在新弹出的虚拟优盘上。
代码包
参考资料:
|