项目总结报告
2022暑期在家一起练(2)- 基于M5StickC Plus的综合应用
本次项目有多个任务可以选择,我选择的是任务2:
可以定时的电子沙漏,要求设置不同的时长,在LCD屏幕上显示时间,在灯板上显示沙漏效果,如样图所示
实际完成图
一、项目介绍
本次项目是基于深圳市明栈信息科技有限公司推出的物联网开发套件M5StickC PLUS,以及两块由74HC5695驱动的8*8LED点阵屏幕组成。
M5StickC PLUS造型小巧,功能丰富,还有配套表带安装好后可以穿戴在手上,内置电池,可脱离电线的束缚。
其主控采用ESP32-PICO-D4芯片,具备WIFI、蓝牙功能,机身内部集成了丰富的硬件资源。
M5StickC PLUS产品特性
主控资源 参数
ESP32 240MHz dual core, 600 DMIPS, 520KB SRAM, Wi-Fi, dual mode Bluetooth
Flash闪存 4MB Flash
输入电压 5V @ 500mA
接口 TypeC x 1, GROVE(I2C+I/0+UART) x 1
LCD屏幕 1.14 inch, 135*240 Colorful TFT LCD, ST7789v2
麦克风 SPM1423
按键 自定义按键 x 2
LED 红色 LED x 1
RTC BM8563
PMU AXP192
蜂鸣器 板载蜂鸣器
IR Infrared transmission
MEMS MPU6886
天线 2.4G 3D天线
外接引脚 G0, G25/G26, G36, G32, G33
电池 120 mAh @ 3.7V, inside vb
外设构成及电源结构图
可以看出,其功能是非常丰富的,而且体积小,重量轻。
LED点阵灯板产品特性
8*8共64颗单色LED灯,封装大小为0603
同时搭配了64个0603封装的电阻
两颗串-并变换、SOIC-16封装的74HC595D
8个NPN三极管9013
二、设计思路
灯板原理图
从原理图中可以看出,灯板上的两颗74HC595芯片是级联的,第一个芯片控制LED点阵的列,第二个芯片控制LED点阵的行,且两个595数据输入为串联,在连接灯板的时候,除电源外,只需3个引脚即可,同时可以将多片灯板进行串联连接,实现扩展功能
74HC595是比较常用的串转并IC,是一个8位串行输入、并行输出的位移缓存器,一般可用在扩展单片机IO,驱动LED灯板等,
74hc595内部有2个8位寄存器:移位寄存器、存储寄存器,移位寄存器在移位寄存器时钟上升沿到来时,将数据引脚的数据输入,并依次移位,而存储寄存器在存储寄存器时钟上升沿到来时,将移位寄存器中的数据输入并锁定,只要输入了8个数据后,将存储寄存器时钟设置上升沿,即可完成一次串转并的功能。
根据74HC595的特点,要想使其能够正常刷新LED点阵屏显示,只能使用逐行/逐列扫描的方式驱动74HC595芯片工作,继而实现所需的功能。
本次项目使用两块灯板级联,所以需要一次性将两块灯板的数据都输入后,将存储寄存器时钟设置上升沿,完成一次刷新扫描,又因为在单片灯板中,列控制在前,行控制在后,需要先将行控制的数据输入,再将列控制的数据输入,最后将存储寄存器时钟设置上升沿即可。
根据任务的需求,制定总体计划,再将总计划拆分成一个个的函数,最后实现功能。
总体思路:首先获取加速度传感器的数据,并转化为对应方向,根据这个方向,判断并执行LED点阵的动画及计时,设置一个定时器定时更新屏幕显示的内容,以及操作不同按键的判断执行
需要编写的函数有:LED灯驱动,IMU加速度获取并转化为对应方向,TFT屏幕刷新,LED点阵跟随方向控制,LED点阵掉落动画,蜂鸣器报警
三、软件流程图
根据设定好的项目需求及总体思路,首先绘制一个大体的软件流程图,也更方便后续代码的编写
图中每一个方框都写成函数形式,需要使用时直接进行调用即可
四、主要代码片段说明
由于对C语言比较熟悉,故使用其进行编程,了解到M5StickCPlus可使用Arduino软件,于是下载安装了VSCode,并安装上PlatformIO,这个环境相比与原生Arduino软件比较方便编写代码。
Arduino代码分为setup函数和loop函数,setup函数只执行一次,执行完后,不断循环执行loop函数。
setup函数:在这其中进行初始化的设置,包括屏幕,电源管理IC,IMU等,而后设置输出引脚,也是对应灯板上的三个引脚,再绑定按钮的回调函数,主要用于设置定时时间,使用Ticker库建立一个定时0.2秒的任务用于刷新TFT屏幕显示,最后resetTime重置定时时间及点阵屏初始化。
void setup() {
M5.begin();
M5.Lcd.setRotation(3);//设置方向
M5.Lcd.setTextSize(6);//设置字号
M5.Lcd.fillScreen(BLACK);
M5.IMU.Init();//初始化imu
pinMode(DIN_pin,OUTPUT);
pinMode(SRCLK_pin,OUTPUT);
pinMode(RCLK_pin,OUTPUT);
BTN_A.reset();//清除一下按钮状态机的状态
BTN_A.attachClick(BTN_A_click);//单击A
BTN_A.attachDoubleClick(BTN_A_doubleclick);//双击A
BTN_A.attachLongPressStart(BTN_A_longPressStart);//长按开始A
BTN_A.attachDuringLongPress(BTN_A_duringLongPress);//长按期间A
BTN_A.attachLongPressStop(BTN_A_longPressStop);//长按结束A
BTN_B.attachClick(BTN_B_click);//单击B
BTN_B.attachDoubleClick(BTN_B_doubleclick);//双击B
BTN_B.attachLongPressStart(BTN_B_longPressStart);//长按开始B
BTN_B.attachDuringLongPress(BTN_B_duringLongPress);//长按期间B
BTN_B.attachLongPressStop(BTN_B_longPressStop);//长按结束B
randomSeed(analogRead(38));//随机种子设置
ticker.attach_ms(200, up_screen);//0.1秒执行一次屏幕刷新
speaker.begin();
resetTime();//重置时间
}
loop函数:不断循环执行,判断加速度状态,根据加速度设置旋转的f方向,根据方向检查是否需要移动LED点阵上的内容(包括方向变换和掉落动画),循环扫描刷新LED点阵,执行一次,就刷新一遍,最后是关于按键和扬声器的任务,主要用于判断按键状态和蜂鸣器定时。
void loop() {
M5.IMU.getAccelData(&accX, &accY, &accZ);//更新加速度值
//更新旋转设置。在剩下的代码中,我们假设“down”仍然是0,0,“up”是7,7
gravity = getGravity();
lc.setRotation((ROTATION_OFFSET + gravity) % 360);//设置旋转角度
//按时间固定这两个,每执行1次,就下落一个,同时对应的计时数减少
bool moved = updateMatrix();//遍历矩阵并检查是否需要移动粒子,然后移动
bool dropped = dropParticle();//让粒子从一个矩阵转到另一个矩阵,粒子掉一个
lc.Refresh();//刷新led点阵
BTN_A.tick();
BTN_B.tick();
speaker.update();
}
//循环扫描刷新
void LedControl::Refresh(){
byte outdata1 ;
byte outdata2;
for(int col = 0;col<COL_COUNT;col++)
{
outdata1 = status[col];
outdata2 = status[col+8];
shiftOut(DIN_pin,SRCLK_pin,LSBFIRST,row[col]); //先输行,再输列的数据
shiftOut(DIN_pin,SRCLK_pin,LSBFIRST,outdata2);
shiftOut(DIN_pin,SRCLK_pin,LSBFIRST,row[col]);
shiftOut(DIN_pin,SRCLK_pin,LSBFIRST,outdata1);
digitalWrite(RCLK_pin,LOW);
digitalWrite(RCLK_pin,HIGH);
shiftOut(DIN_pin,SRCLK_pin,LSBFIRST,row[col]); //先输行,再输列的数据
shiftOut(DIN_pin,SRCLK_pin,LSBFIRST,0b00000000);
shiftOut(DIN_pin,SRCLK_pin,LSBFIRST,row[col]);
shiftOut(DIN_pin,SRCLK_pin,LSBFIRST,0b00000000);
digitalWrite(RCLK_pin,LOW);
digitalWrite(RCLK_pin,HIGH);
}
}//刷新led显示
up_screen函数:用于屏幕刷新,同时根据一些变量判断此刻应该显示的内容时间的加减,由于确定屏幕刷新的时间为0.2秒,每隔0.2秒就减少/增加时间变量的值。
void up_screen(){
if(running)
{
if(gravity==0){
realtime = realtime-0.2;
}
else if(gravity==180){
realtime = realtime+0.2;
}
if(realtime <= 0){
realtime = 0;
running = 0;
speaker.tone(440, 2000);//时间到
}
else if (realtime >= (setMinutes * 60 + setSeconds)){
realtime = (setMinutes * 60 + setSeconds);
}
}
if(last_gravity != gravity)
{
if(gravity==0)
{
M5.Lcd.setRotation(3);//设置方向
M5.Lcd.fillScreen(BLACK);
last_gravity = gravity;
}
else if(gravity==180){
M5.Lcd.setRotation(1);//设置方向
M5.Lcd.fillScreen(BLACK);
last_gravity = gravity;
}
}
M5.Lcd.setTextSize(1);//设置字号
M5.Rtc.GetBm8563Time();
M5.Lcd.setTextSize(2);//设置字号
if(mode == MODE_hourglass)//为沙漏模式时
{
M5.Lcd.setCursor(50, 45, 4);//x,y,字体
M5.Lcd.printf("%02d:%02d ", (int)(realtime/60),(int)((int)realtime % 60)); //实际运行的分钟和秒
}
else if (mode == MODE_setting)//为设置模式时
{
M5.Lcd.setCursor(50, 45, 4);//x,y,字体
M5.Lcd.printf("%02d:%02d ", setMinutes,setSeconds);
M5.Lcd.setCursor(30, 10, 2);//x,y,字体
if(setting_min_sec == setting_min){
M5.Lcd.printf("setMin");
}
else if(setting_min_sec == setting_sec){
M5.Lcd.printf("setSec");
}
M5.Lcd.setCursor(140, 10, 2);//x,y,字体
if(setting_up_down == setting_up){
M5.Lcd.printf(" up ");
}
else if(setting_up_down == setting_down){
M5.Lcd.printf("down");
}
}
}
按键判断的各个函数:用于执行对应的按键操作,包括暂停/恢复,设置定时时间等
//A按键
//在沙漏模式,单击暂停/恢复,长按重置时间
//在设置模式,单击A,判断加减模式,如果加模式加1,如果减模式,就减一
//长按A,判断加减模式,如果加模式加1,如果减模式,就减一
void BTN_A_click()//按键A单击
{
if(mode == MODE_hourglass)//为沙漏模式
running = !running;
else if (mode == MODE_setting)//为设置模式
{
if (setting_up_down == setting_up )//为数字加
{
switch ( setting_min_sec )
{
case setting_min: setMinutes += 1; break;
case setting_sec: setSeconds += 1; break;
default:
break;
}
}
else if (setting_up_down == setting_down)//为数字减
{
switch ( setting_min_sec )
{
case setting_min: setMinutes -= 1; break;
case setting_sec: setSeconds -= 1; break;
default:
break;
}
}
if(setSeconds == 60){setSeconds = 0;setMinutes ++;}
else if(setSeconds == 255){setSeconds = 59;setMinutes --;}
if(setMinutes == 100){setMinutes = 99;}
else if (setMinutes == 255){setMinutes = 0;}
}
Serial.println("单击A");
}
void BTN_A_doubleclick()//按键A双击
{
Serial.println("双击A");
}
void BTN_A_longPressStart()//按键A长按开始
{
if(mode == MODE_hourglass)//为沙漏模式
resetTime();//重置时间
Serial.println("长按开始A");
}
void BTN_A_duringLongPress()//长按期间
{
if (mode == MODE_setting)//设置模式
{
longPress_cnt ++;
if(longPress_cnt >= 5)
{
longPress_cnt = 0;
if (setting_up_down == setting_up )//数字加
{
switch ( setting_min_sec )
{
case setting_min: setMinutes += 1; break;
case setting_sec: setSeconds += 1; break;
default:
break;
}
}
else if (setting_up_down == setting_down)//数字减
{
switch ( setting_min_sec )
{
case setting_min: setMinutes -= 1; break;
case setting_sec: setSeconds -= 1; break;
default:
break;
}
}
if(setSeconds == 60){setSeconds = 0;setMinutes ++;}
else if(setSeconds == 255){setSeconds = 59;setMinutes --;}
if(setMinutes == 100){setMinutes = 99;}
else if (setMinutes == 255){setMinutes = 0;}
}
}
if (BTN_A.isLongPressed())
{
Serial.print("长按中A");
// Serial.println(ButtonA.getPressedTicks());
delay(50);
}
}
void BTN_A_longPressStop()//长按结束
{
Serial.println("长按结束A");
}
//B按键
//在沙漏模式时,长按B进入设置模式,
//在设置时,单击B切换设置分钟/秒,双击切换增加/减少,长按B进入沙漏模式
void BTN_B_click()//单击
{
if (mode == MODE_setting)//为设置模式
{
if(setting_min_sec == setting_min)//为设置分钟,变成设置秒
setting_min_sec = setting_sec;
else if (setting_min_sec == setting_sec)//为设置秒,变成设置分钟
setting_min_sec = setting_min;
}
Serial.println("单击B");
}
void BTN_B_doubleclick()//双击事件
{
if(setting_up_down == setting_up)//为数字加,变成减
setting_up_down = setting_down;
else if (setting_up_down == setting_down)//为数字减,变成加
setting_up_down = setting_up;
Serial.println("双击B");
}
void BTN_B_longPressStart()//长按开始
{
if (mode == MODE_hourglass){//为沙漏模式,变成设置模式
mode = MODE_setting;
running = 0;
}
else if (mode == MODE_setting)//为设置模式,变成沙漏模式
{
mode = MODE_hourglass;
resetTime();//重置时间
M5.Lcd.fillScreen(BLACK);
}
Serial.println("长按开始B");
}
void BTN_B_duringLongPress()//长按期间
{
if (BTN_B.isLongPressed())
{
Serial.print("长按中B");
// Serial.println(ButtonA.getPressedTicks());
delay(50);
}
}
void BTN_B_longPressStop()//长按结束
{
Serial.println("长按结束B");
}
LED点阵运动函数,分为两个,一个判断方向操作LED灯进行移动,另一个形成LED的下落动画
//遍历矩阵并检查是否需要移动粒子,如果更新了,返回true,此时已经改变led灯的状态,没更新,返回false
bool updateMatrix() {
int n = 8;
bool somethingMoved = false;
byte x,y;
bool direction;
//间隔一定的时间才执行
if ((myd2.Timeout()&running)) {
myd2.Delay((unsigned long)(DelayDrop * 400));
for (byte slice = 0; slice < 2*n-1; ++slice) {
//如果我们从左向右或从右向左扫描,则随机化,这样纹理就不会总是落在同一个方向上
direction = (random(2) == 1); //方向
byte z = slice<n ? 0 : slice-n + 1;//slice<n为0,其余为slice-n + 1
for (byte j = z; j <= slice-z; ++j)
{
y = direction ? (7-j) : (7-(slice-j));
x = direction ? (slice-j) : j;
if (moveParticle(MATRIX_B, x, y)) {//更新B矩阵
somethingMoved = true;
};
if (moveParticle(MATRIX_A, x, y)) {//更新A矩阵
somethingMoved = true;
}
}
}
}
return somethingMoved;
}
//让粒子从一个矩阵转到另一个矩阵,粒子掉一个
boolean dropParticle() {
//每隔固定的时间才执行一次,时间就是设定的时间/60
if ((myd.Timeout()&running)) {
myd.Delay((unsigned long)(DelayDrop * 1000));
if (gravity == 0 || gravity == 180) //角度为0或180度
{
//如果矩阵A的0,0亮,和矩阵B的7,7灭,或上,矩阵A的0,0灭和矩阵B的7,7亮
if ((lc.getRawXY(MATRIX_A, 0, 0) && !lc.getRawXY(MATRIX_B, 7, 7)) ||(!lc.getRawXY(MATRIX_A, 0, 0) && lc.getRawXY(MATRIX_B, 7, 7)))
{
// for (byte d=0; d<8; d++) { lc.invertXY(0, 0, 7); delay(50); }
lc.invertRawXY(MATRIX_A, 0, 0);//点亮/熄灭原始的xy处的灯
lc.invertRawXY(MATRIX_B, 7, 7);
speaker.tone(880, 20);//蜂鸣器发声
return true;
}
}
}
return false;
}
重置时间函数:设置倒计时的时间,并重置LED点阵的状态,开始倒计时
void resetTime() {
running = 1; //运行
realtime = setMinutes * 60 + setSeconds;//倒计时重置
for (byte i=0; i<2; i++) {
lc.clearDisplay(i);//关闭所有的灯
}
fill(getBottomMatrix(), 60); //顶部矩阵填满点亮60个led的灯
getDelayDrop();
myd.Delay((unsigned long)(DelayDrop * 1000));//得到粒子下落之间的延时,乘以1000,微秒
Serial.println("Reset");
}
大部分都函数都在这里了,还有一些则是调用的LedControl库和OneButton库,以及延时函数库
整个工程文件已打包放在附件中
五、遇到的主要难题及解决方法
主要还是一些逻辑的判断操作,包括按键,沙漏方向选择及掉落动画的实现,选用一个数组将两块LED灯板的亮灭情况记录,而后操作这个数组中的内容即可,然后屏幕刷新函数便根据数组的内容输送到灯板中,这样也比较巧妙的解决了问题
还有就是Arduino的自带的函数使用的问题,这种查找百度基本就解决了,毕竟Arduino受众面广。
六、后续完善计划
作为一个基本都能够使用的电子沙漏,感觉已经足够,但是还是有许多不足之处可以完善,比如更真实的沙粒运动动画,更良好的屏幕动画显示,可以配上3D打印外壳,或是制作一块PCB底板,将M56StickCPlus和两块灯板组合起来。又或者是直接制作一整块包含LED的点阵,的底板,只预留接口给M5StickCPlus,感觉这样整体性也会好一点。后续有时间的话会将这个项目继续完善下去。
七、参考资料
1、led hourglass arduinohttps://www.thingiverse.com/thing:5184837
2、LED Matrix Hourglass Physics Sand Toyhttps://www.thingiverse.com/thing:4528789
3、M5stickc_plus官方网站https://docs.m5stack.com/zh_CN/core/m5stickc_plus
4、OneButton库使用https://blog.csdn.net/qq_41650023/article/details/124618939
5、LedControl库使用https://blog.csdn.net/qq_36955622/article/details/120077367
6、74HC595芯片的总结(8X8点阵)
https://blog.csdn.net/weixin_45420737/article/details/109903890
7、74HC595驱动8*8点阵屏https://blog.csdn.net/flaycsdn/article/details/106138918