2022暑假在家一起练总结报告
一,板卡介绍
本次活动推出了M5stickCPlus板卡,该板块是M5stickC的大屏版本,主控采用ESP32-PICO-D4模组,具备蓝牙4.2与WIFI功能,小巧的机身内部集成了丰富的硬件资源,如红外、RTC、麦克风、LED、IMU、按键、蜂鸣器、PMU等,在保留原有M5StickC功能的基础上加入了无源蜂鸣器,同时屏幕尺寸升级到1.14寸、135*240分辨率的TFT屏幕,相较之前的0.96寸屏幕增加18.7%的显示面积,电池容量达到120mAh,接口同样支持HAT与Unit系列产品。
-
开关机操作
-
开机:按复位按键,持续至少 2 秒
-
关机:按复位按键,持续至少 6 秒
-
-
注意
-
M5StickC Plus支持的波特率: 1200 ~115200, 250K, 500K, 750K, 1500K
-
G36/G25共用同一个端口,当使用其中一个引脚时要将另外一个引脚设置为浮空输入
-
比如要使用G36引脚作为ADC输入,则配置G25引脚为浮空状态
-
-
产品特性
-
基于 ESP32开发,支持WiFi、蓝牙
-
内置3轴加速计与3轴陀螺仪
-
内置Red LED
-
集成红外发射管
-
内置RTC
-
集成麦克风
-
用户按键, LCD(1.14 寸), 电源/复位按键
-
120 mAh 锂电池
-
拓展接口
-
集成无源蜂鸣器
-
可穿戴 & 可固定
-
二,项目介绍
官方说明 :
任务2:可以定时的电子沙漏,要求设置不同的时长,在LCD屏幕上显示时间,在灯板上显示沙漏效果
本项目通过M5实体按键来实现定时效果,可以设置分钟秒数,根据设置的时间自动计算沙漏时间间隔,但是由于所设计的沙漏流动过程固定,总共36个状态,所以在计算时间的时候会存在些许误差,后续可能会优化算法来减小误差。
流沙效果通过存储在M5里面的数组来实现,总计36个状态,即要在设定的时间里闪烁36次,但是计算过程中难免会出现小数,最终会导致提前几秒闪完,目前通过增添全部黑色图案数组来弥补误差,后续可能会通过增加闪烁图案(各种表情,比如笑脸)来弥补设计缺陷。
此外,本项目还设计了电子沙漏的外壳,打通了成为产品的最后一步,M5和外壳创新的采用了磁吸连接,方便拿取,外壳后面带有支腿,可以摆放在桌面,随时定时。主体采用橙色材料,和M5搭配更为柔和。
三,设计思路
本项目设计主要从两个方面出发,一是定时,二是沙漏流动过程,下面来分别展示一下
1,首先是定时的问题,一开始考虑的方案有重力感应控制时间,WiFi联网(http协议)设置时间,实体按钮控制时间。本项目最终采用了是实体按钮控制时间,该方法设计比较简单,操作起来也较为容易。方案确定之后就是要思考怎么来设定具体的时间,M5Stick可供使用的实体按键只有两个,而需要的状态控制有“加”,“减”,“确定”,“变换”等,幸好,M5StickC官方库里设计了按钮时长检测,这样一来通过检测按钮按下时间来判断状态即可。最后就是屏幕显示效果,该项目采用了极简设计,屏幕上只显示分钟和秒数,通过颜色来进行区分,灵感是来自于B站挺火的ESP8266小电视设计,简约大方。
2,其次是流沙效果,这是本次任务的核心,如何能更逼真的模拟流沙的效果,重力感应的效果,作者也是思考了很长时间,但无奈作者目前功力尚浅,只能采用逐个点亮的方式来模拟流沙小效果,所以就有了之前所说的数组,每个数组有36个状态,每个状态需要一个8个0或1来控制,所以代码部分就很冗余,效率也不是特别高。然后是重力感应,为了区分不同的效果,又增添个两个反方向的数组,但是每次数组的切换都要浪费大量的时间,所以采用的指针变量,直接改变地址,减少大量时间,最终效果也是不错的。
四,框图
五,主要代码片段和说明
1,屏幕初始化函数
void LCD_init()
{
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setRotation(1);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(3);
M5.Lcd.setCursor(0, 0);M5.Lcd.println("Set Time:"); //依次设计数字显示位置
M5.Lcd.setCursor(10, 30);M5.Lcd.println("Min: ");
M5.Lcd.setCursor(10, 60);M5.Lcd.println("Sec: ");
M5.Lcd.setCursor(90, 30);M5.Lcd.print(Minute);
M5.Lcd.setCursor(90, 60);M5.Lcd.print(Second);
M5.Lcd.setTextColor(GREEN);
M5.Lcd.setCursor(180, 30);M5.Lcd.print("+");
M5.Lcd.setCursor(180, 60);M5.Lcd.print("+");
}
效果:
2,时间设置和屏幕显示函数
void LCD_setTime() //时间设置函数
{
M5.Lcd.setTextSize(3);
while (set_status == 0) //状态标志位 0为设置时间
{
if (min_status == 0) //分钟标志位 0为设置分钟
{
M5.update(); //更新按钮状态,检测按压时长
if (M5.BtnB.wasReleasefor(50)) //如果小键被按压50ms (M5中间的按键称为大键,上方的按键称为小键)
{
en_addmin = !en_addmin; //对分钟增加标志位取反,即改变增加还是减少分钟的状态
M5.Lcd.fillRect(180, 30, 30, 30, BLACK);
M5.Lcd.setCursor(180, 30);
M5.Lcd.setTextColor(GREEN);
if (en_addmin)
{
M5.lcd.print("+");
}
else
{
M5.Lcd.print("-");
}
}
if (M5.BtnA.wasReleasefor(50)) //检测大键按压状态
{
if (en_addmin == 1) //如果分钟增加标志位为一,则按压一次分钟加一
{
Minute++;
}
else
{
Minute--;
}
M5.Lcd.setTextColor(WHITE);
M5.Lcd.fillRect(90, 30, 60, 28, BLACK); //刷新
M5.Lcd.setCursor(90, 30);
M5.Lcd.print(Minute);
}
if (M5.BtnB.wasReleasefor(700)) //长按小键,确认分钟设置完毕
{
min_status = 1;
M5.Lcd.fillRect(180, 30, 60, 30, BLACK);
M5.Lcd.setCursor(180, 30);
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.print("OK"); //输出OK
}
}
else //对秒进行相同的操作
{
M5.update();
if (M5.BtnB.wasReleasefor(50))
{
en_addsec = !en_addsec;
M5.Lcd.fillRect(180, 60, 30, 30, BLACK);
M5.Lcd.setCursor(180, 60);
M5.Lcd.setTextColor(GREEN);
if (en_addsec)
{
M5.lcd.print("+");
}
else
{
M5.Lcd.print("-");
}
}
if (M5.BtnA.wasReleasefor(50))
{
if (en_addsec == 1)
{
Second++;
}
else
{
Second--;
}
M5.Lcd.setTextColor(WHITE);
M5.Lcd.fillRect(90, 60, 60, 28, BLACK);
M5.Lcd.setCursor(90, 60);
M5.Lcd.print(Second);
}
if (M5.BtnB.wasReleasefor(700))
{
sec_status = 1;
M5.Lcd.fillRect(180, 60, 60, 30, BLACK);
M5.Lcd.setCursor(180, 60);
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.print("OK");
}
}
if (min_status == 1 && sec_status == 1) //如果分钟和秒都设置完成,则令设置状态标志位为1,跳出循环
{
set_status = 1;
}
}
}
3,时间显示函数
void LCD_showinit()
{
M5.Lcd.fillScreen(BLACK); //每次显示前先进行局部刷新,填冲为黑色,和背景一个颜色
M5.Lcd.setTextSize(10);
M5.Lcd.setCursor(0, 30);
if (Minute > 9) //如果分钟大于9,则正常显示
{
M5.Lcd.print(Minute);
}
else
{ // 否则输出零占位,“秒”同理
M5.Lcd.print("0");
M5.Lcd.print(Minute);
}
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setCursor(120, 30);
if (Second > 9)
{
M5.Lcd.print(Second);
}
else
{
M5.Lcd.print("0");
M5.Lcd.print(Second);
}
}
4,LED灯闪烁函数,灯板主控芯片为74hc595,采用shiftOut填冲两次,然后输出一次,达到一次控制两个灯板的效果
void Write_H(int row, char high_data, char low_data)
{
digitalWrite(SRCLK_Pin, LOW);
shiftOut(SER_Pin, SRCLK_Pin, MSBFIRST, B00000001 << (row - 1));
shiftOut(SER_Pin, SRCLK_Pin, MSBFIRST, low_data);
shiftOut(SER_Pin, SRCLK_Pin, MSBFIRST, B00000001 << (row - 1));
shiftOut(SER_Pin, SRCLK_Pin, MSBFIRST, high_data);
digitalWrite(RCLK_Pin, HIGH);
digitalWrite(RCLK_Pin, LOW);
delay(1);
}
5,定时器,100ms时间精度,用来计算时间间隔
void count();
Ticker Count(count, 100);
void count()
{
time1 += 1;
} // 100ms计时器
6,同过定时器来进行时间减一,屏幕刷新,位置占位,不建议采用定时器进行刷新这么做
void Clock();
Ticker CLOCK(Clock, 1000);
void Clock()
{
if (Second > 10)
{
Second--;
M5.Lcd.fillRect(120, 0, 120, 135, BLACK);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(10);
M5.Lcd.setCursor(120, 30);
M5.Lcd.print(Second);
}
else if (Second > 0 && Second <= 10)
{
Second--;
M5.Lcd.fillRect(120, 0, 120, 135, BLACK);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(10);
M5.Lcd.setCursor(120, 30);
M5.Lcd.print("0");
M5.Lcd.print(Second);
}
else if (Second == 0)
{
if (Minute > 10)
{
Minute--;
Second = 59;
M5.Lcd.fillRect(0, 0, 120, 135, BLACK);
M5.Lcd.fillRect(120, 30, 120, 135, BLACK);
M5.Lcd.setTextColor(GREEN);
M5.Lcd.setTextSize(10);
M5.Lcd.setCursor(0, 30);
M5.Lcd.print(Minute);
M5.Lcd.setCursor(120, 30);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.print(Second);
}
else
{
Minute--;
Second = 59;
M5.Lcd.fillRect(0, 0, 120, 135, BLACK);
M5.Lcd.fillRect(120, 30, 120, 135, BLACK);
M5.Lcd.setCursor(0, 30);
M5.Lcd.setTextColor(GREEN);
M5.Lcd.setTextSize(10);
M5.Lcd.print("0");
M5.Lcd.setCursor(60, 30);
M5.Lcd.print(Minute);
M5.Lcd.setCursor(120, 30);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.print(Second);
}
}
}
7,点亮灯板,更换数组
沙漏流动过程数组
unsigned char New_High[]={
0x01,0x03,0x07,0x0F,0x1F,0x3F,0x7F,0xFF,
0x01,0x03,0x07,0x0F,0x1F,0x3F,0x7F,0x7F,
0x01,0x03,0x07,0x0F,0x1F,0x3F,0x3F,0x7F,
0x01,0x03,0x07,0x0F,0x1F,0x1F,0x3F,0x7F,
0x01,0x03,0x07,0x0F,0x0F,0x1F,0x3F,0x7F,
0x01,0x03,0x07,0x07,0x0F,0x1F,0x3F,0x7F,
0x01,0x03,0x03,0x07,0x0F,0x1F,0x3F,0x7F,
0x01,0x01,0x03,0x07,0x0F,0x1F,0x3F,0x7F,
0x00,0x01,0x03,0x07,0x0F,0x1F,0x3F,0x7F,
0x00,0x01,0x03,0x07,0x0F,0x1F,0x3F,0x3F,
0x00,0x01,0x03,0x07,0x0F,0x1F,0x1F,0x3F,
0x00,0x01,0x03,0x07,0x0F,0x0F,0x1F,0x3F,
0x00,0x01,0x03,0x07,0x07,0x0F,0x1F,0x3F,
0x00,0x01,0x03,0x03,0x07,0x0F,0x1F,0x3F,
0x00,0x01,0x01,0x03,0x07,0x0F,0x1F,0x3F,
0x00,0x00,0x01,0x03,0x07,0x0F,0x1F,0x3F,
0x00,0x00,0x01,0x03,0x07,0x0F,0x1F,0x1F,
0x00,0x00,0x01,0x03,0x07,0x0F,0x0F,0x1F,
0x00,0x00,0x01,0x03,0x07,0x07,0x0F,0x1F,
0x00,0x00,0x01,0x03,0x03,0x07,0x0F,0x1F,
0x00,0x00,0x01,0x01,0x03,0x07,0x0F,0x1F,
0x00,0x00,0x00,0x01,0x03,0x07,0x0F,0x1F,
0x00,0x00,0x00,0x01,0x03,0x07,0x0F,0x0F,
0x00,0x00,0x00,0x01,0x03,0x07,0x07,0x0F,
0x00,0x00,0x00,0x01,0x03,0x03,0x07,0x0F,
0x00,0x00,0x00,0x01,0x01,0x03,0x07,0x0F,
0x00,0x00,0x00,0x00,0x01,0x03,0x07,0x0F,
0x00,0x00,0x00,0x00,0x01,0x03,0x07,0x07,
0x00,0x00,0x00,0x00,0x01,0x03,0x03,0x07,
0x00,0x00,0x00,0x00,0x01,0x01,0x03,0x07,
0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x07,
0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x03,
0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x03,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
void Refresh();
Ticker REFRESH(Refresh,10); //10ms进行一次
void Refresh(){
for(int i=0 ; i<8 ; i++){
Write_L(i+1,*(h1+offset+i),*(l1+offset+i)); //指针确定数组
if(direction ==1){ //大大减少运算时间
h1=&New_High[0];
l1=&New_Low[0];
}
else{
h1=&inverted_high[0];
l1=&inverted_low[0];
}
}
}
六,难点和尚未解决的问题
难点:
- 屏幕显示及控制时间逻辑
- 沙漏流动过程模拟
- 重力感应变化时数组的切换,采用指针来获取数组地址解决
尚未解决的问题(挖坑):
- 计算时间间隔算法误差较大,尤其是在短的时间间隔内误差更为明显
- 沙漏模拟过程不是很逼真,仍有优化的空间
- 在秒是个位数时,改变M5位置无法检测重力变化(BUG)
- 目前计时完成之后必须长按关机键才能进行下次计时,不是很方便
七,活动感言
最后,非常感谢硬禾学堂能提供这次机会,在这次活动中学到了不少东西,指针的应用,灯板的驱动,外壳的设计等等,也认识到了不少有趣的人,希望以后可以完善一下灯板的程序,再接再厉。