基于M5StickC Plus的电子沙漏
(注:本项目基本为移植RP2040电子沙漏项目,可在电子森林我的个人首页中查看,故有很多重复部分,尽情谅解)
1 项目需求
任务2:可以定时的电子沙漏,要求设置不同的时长,在LCD屏幕上显示时间,在灯板上显示沙漏效果
2 硬件介绍
项目使用的开发板为M5StickC Plus 开发板。
M5StickC PLUS 是M5StickC的大屏幕版本,主控采用ESP32-PICO-D4模组,具备蓝牙4.2与WIFI功能,小巧的机身内部集成了丰富的硬件资源,如红外、RTC、麦克风、LED、IMU、按键、蜂鸣器、PMU等,在保留原有M5StickC功能的基础上加入了无源蜂鸣器,同时屏幕尺寸升级到1.14寸、135*240分辨率的TFT屏幕,相较之前的0.96寸屏幕增加18.7%的显示面积,电池容量达到120mAh,接口同样支持HAT与Unit系列产品。这个小巧玲珑的开发工具,能够激发你无限的创作可能。 M5StickC 能够帮助你快速的搭建物联网产品原型,简化整个的开发过程.即便是刚开始接触编程开发的初学者,也能够搭建出一些有趣的应用,并应用到实际生活中。
硬件规格参数如下:
主控资源 参数 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 工作温度 32°F to 104°F ( 0°C to 40°C ) 净重 16g 毛重 21g 产品尺寸 48.225.513.7mm 包装尺寸 652515mm外壳材质Plastic ( PC )
本项目使用了全彩LCD、按键与姿态传感器模块,并使用GPIO控制LED灯板
3 完成的功能
3.1 电子沙漏中沙粒随重力方向下落
3.2 可调节电子沙漏计时时间
4 实现思路
-
M5采用micropython框架进行开发,使用micropython的面向对象特性。
-
M5获取板载姿态传感器数据,推算出重力方向,并据此计算沙漏中沙粒的下落情况。
-
通过调节相邻沙粒在缝隙间穿梭的间隔帧数,调节时间,并在lcd屏幕上显示。
-
使用solidworks绘制折叠屏的3D模型,完成打印并组装。
5 实现过程
本项目的软件框架图如下:
5.1 环境配置
在完成本项目前我已经做好了RP2040项目,故环境配置只需要烧入代码即可。
曾尝试使用vscode安装插件,配置成功后发现没有代码提示,所有放弃,最后使用uiflow完成最后调试和烧录工作。
5.2 重力算法开发(同RP2040电子沙漏)
代码方面,首先,定义Sandy类,表示每个沙粒,每个沙粒占据一个1*1的位置,其成员变量包括x,y轴坐标与一个标志位,这个标志位用来标志现在沙粒是在上面这个块还是下面这个块。其代码如下:
class Sandy:
def __init__(self, block, x, y):
self.block = block # 0->up,1->low
self.x = x
self.y = y
定义SandyClock类,该类为电子沙漏的顶层类,其成员变量报告:首先是两个8*8数组分别表示上下两个块每个像素的状态;一个沙粒的list,用于存放并索引到电子沙漏里存放的沙粒,还有一个三维向量用于标记重力方向。另外,为了实现调速等扩展功能,还需要添加时间相关变量。最后,SandyClock类构造函数如下:
class SandyClock:
# 板子是上面的还是下面的
LOW_BLOCK = 0
UP_BLOCK = 1
# 这个位置是否有沙砾
NO_SANDY = 0
HAS_SANDY = 1
# 是否在底边上
IN_BOTTOM = 0
NOT_IN_BOTTOM = 1
IN_X_SLOPE = 2
IN_Y_SLOPE = 3
current_time = 0
def __init__(self, sandy_list=None, size=None):
if size is None:
size = [8, 8, 8, 8]
self.up_block_x = size[0]
self.up_block_y = size[1]
self.low_block_x = size[2]
self.low_block_y = size[3]
# 沙砾集合
if sandy_list is None:
sandy_list = []
for i in range(self.low_block_x):
sandy_list.append(Sandy(SandyClock.LOW_BLOCK, 0, i))
for i in range(self.low_block_x):
sandy_list.append(Sandy(SandyClock.LOW_BLOCK, 2, i))
for i in range(self.low_block_x):
sandy_list.append(Sandy(SandyClock.LOW_BLOCK, 4, i))
for i in range(self.low_block_x):
sandy_list.append(Sandy(SandyClock.LOW_BLOCK, 6, i))
# for i in range(self.up_block_x):
# sandy_list.append(Sandy(SandyClock.UP_BLOCK, 0, i))
self.sandy_list = sandy_list
# 生成棋盘
self.up_block = [[SandyClock.NO_SANDY] * self.up_block_x for _ in range(self.up_block_y)]
self.low_block = [[SandyClock.NO_SANDY] * self.low_block_x for _ in range(self.low_block_y)]
# 把有沙子的地方赋值为1
for index, sandy in enumerate(sandy_list):
if sandy.block == SandyClock.LOW_BLOCK:
self.low_block[sandy.x][sandy.y] = SandyClock.HAS_SANDY
elif sandy.block == SandyClock.UP_BLOCK:
self.up_block[sandy.x][sandy.y] = SandyClock.HAS_SANDY
else:
print("傻逼")
# 重力方向
self.gravity = [1, 1, 0]
# 调速累加值
self.keyframe_acc = 0
self.keyframe_target = 5
self.cross_crack_flag = False
每运行一轮,代码会有如下流程:
-
检测用户按键,修改相应变量;
-
获取重力的方向;
-
对存放沙粒的list做随机排序
-
对沙粒list中的沙粒逐个判定移位
代码如下:
# 运行一轮
def process(self):
self.check_button()
self.update_keyframe()
# 获得重力
self.gravity = self.update_gravity()
# print(self.gravity, end="")
# TODO:加上如果y与之前的y乘积是负数,说明翻过来了,清零
# 根据沙砾的高度排序,算内积
# 从下往上排序
# self.sandy_list.sort(key=lambda sandy:( sandy.x * self.gravity[0] + sandy.y * self.gravity[1] ),reverse=True )
# 从上往下排序
# self.sandy_list.sort(key=lambda sandy:( sandy.x * self.gravity[0] + sandy.y * self.gravity[1] ))
# 猴排,micropython用不了shuffle,只能自己写
# random.shuffle(self.sandy_list)
self.sandy_list.sort(key=lambda sandy: (random.random()))
# 判断重力在哪个方向
gravity_angle = cmath.phase(complex(self.gravity[0], self.gravity[1]))
# print(gravity_angle)
if -cmath.pi * 7 / 8 < gravity_angle <= -cmath.pi * 5 / 8: # -x,-y
for sandy in self.sandy_list:
self.shift_sandy_bias(sandy, "-x-y")
# print("-x-y")
elif -cmath.pi * 5 / 8 < gravity_angle <= -cmath.pi * 3 / 8: # -y
for sandy in self.sandy_list:
self.shift_sandy_straight(sandy, "-y")
# print("-y")
elif -cmath.pi * 3 / 8 < gravity_angle <= -cmath.pi / 8: # +x,-y
for sandy in self.sandy_list:
self.shift_sandy_bias(sandy, "+x-y")
# print("+x-y")
elif -cmath.pi / 8 < gravity_angle <= cmath.pi / 8: # +x
for sandy in self.sandy_list:
self.shift_sandy_straight(sandy, "+x")
# print("+x")
elif cmath.pi / 8 < gravity_angle <= cmath.pi * 3 / 8: # +x,+y
for sandy in self.sandy_list:
self.shift_sandy_bias(sandy, "+x+y")
# print("+x+y")
elif cmath.pi * 3 / 8 < gravity_angle <= cmath.pi * 5 / 8: # +y
for sandy in self.sandy_list:
self.shift_sandy_straight(sandy, "+y")
# print("+y")
elif cmath.pi * 5 / 8 < gravity_angle <= cmath.pi * 7 / 8: # -x,+y
for sandy in self.sandy_list:
self.shift_sandy_bias(sandy, "-x+y")
# print("-x+y")
elif gravity_angle <= -cmath.pi * 7 / 8 or gravity_angle > cmath.pi * 7 / 8: # -x
for sandy in self.sandy_list:
self.shift_sandy_straight(sandy, "-x")
# print("-x")
其中,沙粒移位的代码逻辑为判断该沙粒下方的空间是否有别的沙粒或为底边,如果不是,则进行移位,如果是,则保持在原位。
正向移动的判断代码如下:
def shift_sandy_straight(self, sandy_para, gravity):
global sandy
sandy = sandy_para
axis = "default"
if "x" in gravity:
axis = "x"
elif "y" in gravity:
axis = "y"
else:
print("傻逼")
# 生成dx与dy
dx = 0
dy = 0
if gravity == "+x":
dx += 1
elif gravity == "-x":
dx -= 1
elif gravity == "+y":
dy += 1
elif gravity == "-y":
dy -= 1
else:
print("傻逼")
# 判断是否在底边,生成flag
bottom_flag = SandyClock.NOT_IN_BOTTOM
if "+" in gravity:
if eval("sandy." + axis) + 1 > 7:
bottom_flag = SandyClock.IN_BOTTOM
elif "-" in gravity:
if eval("sandy." + axis) - 1 < 0:
bottom_flag = SandyClock.IN_BOTTOM
else:
print("大傻逼")
# # 判断是在上块还是在下块
# block = "low_block"
# if sandy.block == self.UP_BLOCK:
# block = "up_block"
# elif sandy.block == self.LOW_BLOCK:
# block = "low_block"
# 主判断流程
# 如果在底边,判断是否可以移到下一个块中
if bottom_flag == SandyClock.IN_BOTTOM:
# 只有到了累加的关键帧,才能往下漏
if self.cross_crack_flag == True:
if sandy.block == SandyClock.LOW_BLOCK:
# 下到上,判断是否可以移动
if sandy.x == 7 and sandy.y == 7 \
and self.up_block[0][0] == SandyClock.NO_SANDY \
and (gravity == "+x" or gravity == "+y"):
# 移动
self.low_block[7][7] = SandyClock.NO_SANDY
self.up_block[0][0] = SandyClock.HAS_SANDY
# 更新sandy的坐标
sandy.block = SandyClock.UP_BLOCK
sandy.x = 0
sandy.y = 0
elif sandy.block == SandyClock.UP_BLOCK:
# 上到下,判断是否可以移动
if sandy.x == 0 and sandy.y == 0 \
and self.low_block[7][7] == SandyClock.NO_SANDY \
and (gravity == "-x" or gravity == "-y"):
# 移动
self.up_block[0][0] = SandyClock.NO_SANDY
self.low_block[7][7] = SandyClock.HAS_SANDY
# 更新sandy的坐标
sandy.block = SandyClock.LOW_BLOCK
sandy.x = 7
sandy.y = 7
else:
print("太傻逼了")
# 如果不在底边,移位
elif bottom_flag == SandyClock.NOT_IN_BOTTOM:
# micropython不支持eval,只能轻微屎山不予处罚
if sandy.block == self.UP_BLOCK:
if self.up_block[sandy.x + dx][sandy.y + dy] == self.NO_SANDY:
# 说明下面是空的,进行移位
self.up_block[sandy.x][sandy.y] = self.NO_SANDY
self.up_block[sandy.x + dx][sandy.y + dy] = self.HAS_SANDY
# 更新sandy的坐标
sandy.x += dx
sandy.y += dy
elif sandy.block == self.LOW_BLOCK:
if self.low_block[sandy.x + dx][sandy.y + dy] == self.NO_SANDY:
# 说明下面是空的,进行移位
self.low_block[sandy.x][sandy.y] = self.NO_SANDY
self.low_block[sandy.x + dx][sandy.y + dy] = self.HAS_SANDY
# 更新sandy的坐标
sandy.x += dx
sandy.y += dy
else:
print("hello,大傻逼")
斜向移动的判断代码如下:
def shift_sandy_bias(self, sandy_para, gravity):
global sandy
sandy = sandy_para
# 生成dx与dy
dx = 0
dy = 0
if "+x" in gravity:
dx += 1
elif "-x" in gravity:
dx -= 1
else:
print("傻逼")
if "+y" in gravity:
dy += 1
elif "-y" in gravity:
dy -= 1
else:
print("傻逼")
# 判断是在上块还是在下块
block = "low_block"
if sandy.block == self.UP_BLOCK:
block = "up_block"
elif sandy.block == self.LOW_BLOCK:
block = "low_block"
# 判断是否在底边,生成flag
x_slope_flag = SandyClock.NOT_IN_BOTTOM
if "+x" in gravity:
if eval("sandy.x") + 1 > 7:
x_slope_flag = SandyClock.IN_X_SLOPE
elif "-x" in gravity:
if eval("sandy.x") - 1 < 0:
x_slope_flag = SandyClock.IN_X_SLOPE
else:
print("大傻逼")
y_slope_flag = SandyClock.NOT_IN_BOTTOM
if "+y" in gravity:
if eval("sandy.y") + 1 > 7:
y_slope_flag = SandyClock.IN_Y_SLOPE
elif "-y" in gravity:
if eval("sandy.y") - 1 < 0:
y_slope_flag = SandyClock.IN_Y_SLOPE
else:
print("大傻逼")
bottom_flag = SandyClock.NOT_IN_BOTTOM
if x_slope_flag == SandyClock.IN_X_SLOPE and y_slope_flag == SandyClock.IN_Y_SLOPE:
bottom_flag = SandyClock.IN_BOTTOM
elif x_slope_flag == SandyClock.IN_X_SLOPE:
bottom_flag = SandyClock.IN_X_SLOPE
elif y_slope_flag == SandyClock.IN_Y_SLOPE:
bottom_flag = SandyClock.IN_Y_SLOPE
# 主判断流程
# 如果在斜底部,判断是否可以移到下一个块中
if bottom_flag == SandyClock.IN_BOTTOM:
# print("IN_BOTTOM")
#只有到了那一帧,才能往下走
if self.cross_crack_flag == True:
if sandy.block == SandyClock.LOW_BLOCK:
# 下到上,判断是否可以移动
if sandy.x == 7 and sandy.y == 7 \
and self.up_block[0][0] == SandyClock.NO_SANDY \
and gravity == "+x+y":
# 移动
self.low_block[7][7] = SandyClock.NO_SANDY
self.up_block[0][0] = SandyClock.HAS_SANDY
# 更新sandy的坐标
sandy.block = SandyClock.UP_BLOCK
sandy.x = 0
sandy.y = 0
elif sandy.block == SandyClock.UP_BLOCK:
# 上到下,判断是否可以移动
if sandy.x == 0 and sandy.y == 0 \
and self.low_block[7][7] == SandyClock.NO_SANDY \
and gravity == "-x-y":
# 移动
self.up_block[0][0] = SandyClock.NO_SANDY
self.low_block[7][7] = SandyClock.HAS_SANDY
# 更新sandy的坐标
sandy.block = SandyClock.LOW_BLOCK
sandy.x = 7
sandy.y = 7
else:
print("太傻逼了")
# 如果不在底边,必定无法到下面的块,再判断
else:
# 轻微屎山,up_block与low_block
if sandy.block == self.UP_BLOCK:
# 如果在X边,只有y的坐标会发生变化
if bottom_flag == SandyClock.IN_X_SLOPE:
# 说明下面是空的,进行移位
if self.up_block[sandy.x][sandy.y + dy] == self.NO_SANDY:
self.up_block[sandy.x][sandy.y] = self.NO_SANDY
self.up_block[sandy.x][sandy.y + dy] = self.HAS_SANDY
# 更新sandy的坐标
sandy.y += dy
# 如果在Y边,只有x的坐标会发生变化
elif bottom_flag == SandyClock.IN_Y_SLOPE:
# 说明下面是空的,进行移位
if self.up_block[sandy.x + dx][sandy.y] == self.NO_SANDY:
self.up_block[sandy.x][sandy.y] = self.NO_SANDY
self.up_block[sandy.x + dx][sandy.y] = self.HAS_SANDY
# 更新sandy的坐标
sandy.x += dx
# 如果不在底边上
elif bottom_flag == SandyClock.NOT_IN_BOTTOM:
# 说明下面是空的,进行移位
if self.up_block[sandy.x + dx][sandy.y + dy] == self.NO_SANDY:
self.up_block[sandy.x][sandy.y] = self.NO_SANDY
self.up_block[sandy.x + dx][sandy.y + dy] = self.HAS_SANDY
# 更新sandy的坐标
sandy.x += dx
sandy.y += dy
# 下面不是空的就判断斜下面,现在这种写法不是左右随机下落,而是固定方向,先判断Y方向
# TODO:改为左右随机下落
elif self.up_block[sandy.x][sandy.y + dy] == self.NO_SANDY:
self.up_block[sandy.x][sandy.y] = self.NO_SANDY
self.up_block[sandy.x][sandy.y + dy] = self.HAS_SANDY
# 更新sandy的坐标
sandy.y += dy
# 再判断X方向
elif self.up_block[sandy.x + dx][sandy.y] == self.NO_SANDY:
self.up_block[sandy.x][sandy.y] = self.NO_SANDY
self.up_block[sandy.x + dx][sandy.y] = self.HAS_SANDY
# 更新sandy的坐标
sandy.x += dx
else:
# print("无法下落")
pass
# 重复上面的,但是是下块,轻微屎山
elif sandy.block == self.LOW_BLOCK:
# 如果在X边,只有y的坐标会发生变化
if bottom_flag == SandyClock.IN_X_SLOPE:
# 说明下面是空的,进行移位
if self.low_block[sandy.x][sandy.y + dy] == self.NO_SANDY:
self.low_block[sandy.x][sandy.y] = self.NO_SANDY
self.low_block[sandy.x][sandy.y + dy] = self.HAS_SANDY
# 更新sandy的坐标
sandy.y += dy
# 如果在Y边,只有x的坐标会发生变化
elif bottom_flag == SandyClock.IN_Y_SLOPE:
# 说明下面是空的,进行移位
if self.low_block[sandy.x + dx][sandy.y] == self.NO_SANDY:
self.low_block[sandy.x][sandy.y] = self.NO_SANDY
self.low_block[sandy.x + dx][sandy.y] = self.HAS_SANDY
# 更新sandy的坐标
sandy.x += dx
# 如果不在底边上
elif bottom_flag == SandyClock.NOT_IN_BOTTOM:
# 说明下面是空的,进行移位
if self.low_block[sandy.x + dx][sandy.y + dy] == self.NO_SANDY:
self.low_block[sandy.x][sandy.y] = self.NO_SANDY
self.low_block[sandy.x + dx][sandy.y + dy] = self.HAS_SANDY
# 更新sandy的坐标
sandy.x += dx
sandy.y += dy
# 下面不是空的就判断斜下面,现在这种写法不是左右随机下落,而是固定方向,先判断Y方向
# TODO:改为左右随机下落
elif self.low_block[sandy.x][sandy.y + dy] == self.NO_SANDY:
self.low_block[sandy.x][sandy.y] = self.NO_SANDY
self.low_block[sandy.x][sandy.y + dy] = self.HAS_SANDY
# 更新sandy的坐标
sandy.y += dy
# 再判断X方向
elif self.low_block[sandy.x + dx][sandy.y] == self.NO_SANDY:
self.low_block[sandy.x][sandy.y] = self.NO_SANDY
self.low_block[sandy.x + dx][sandy.y] = self.HAS_SANDY
# 更新sandy的坐标
sandy.x += dx
else:
pass
# print("无法下落")
5.3 添加底层驱动
底层驱动方面,可通过uiflow拖动模块生成代码的方式便捷地生成LCD、按键、姿态传感器等的驱动代码。生成的代码如下:
from m5stack import *
from m5ui import *
from uiflow import *
import imu
import time
setScreenColor(0x111111)
x_acc = None
y_acc = None
z_acc = None
current_time_ms = None
imu0 = imu.IMU()
label_time = M5TextBox(39, 66, "time:", lcd.FONT_Comic, 0xFFFFFF, rotate=90)
label_acc = M5TextBox(128, 66, "acc:", lcd.FONT_Comic, 0xFFFFFF, rotate=90)
@timerSch.event('timer1')
def ttimer1():
global x_acc, y_acc, z_acc, current_time_ms, pin0
pass
timerSch.setTimer('timer1', 1000, 0x00)
timerSch.run('timer1', 1000, 0x00)
while True:
x_acc = imu0.acceleration[0]
y_acc = imu0.acceleration[1]
z_acc = imu0.acceleration[2]
current_time_ms = time.ticks_ms()
label_acc.setText(str(current_time_ms))
label_time.setText(str(current_time_ms))
wait_ms(500)
if btnA.isPressed():
M5Led.on()
if btnB.isPressed():
M5Led.off()
wait_ms(2)
另外,调试过程中发现,使用uiflow的m5stick库不支持spi,故无法使用电子森林中现成的LED灯板驱动库,故自己写了一个。
自己写的LED灯板驱动库并没有使用spi,而是根据74HC595的驱动原理,使用三个GPIO输出口完成了三个引脚的控制。代码如下:
pin0 = machine.Pin(25, mode=machine.Pin.OUT, pull=machine.Pin.PULL_UP)
pin1 = machine.Pin(26, mode=machine.Pin.OUT, pull=machine.Pin.PULL_UP)
pin2 = machine.Pin(0, mode=machine.Pin.OUT, pull=machine.Pin.PULL_UP)
buffer1 = bytearray(1) #
buffer0 = bytearray(1) #
buffer = bytearray(8 * 2) #
fbuf = framebuf.FrameBuffer(buffer, 8 * 2, 8, framebuf.MONO_VLSB)
def write_buffer(buffer_in):
for count in range(8): # 每循环控制所有灯板同一列
pin0.value(0) # 时钟
pin1.value(buffer_in & 0x01)
buffer_in = buffer_in >> 1
pin0.value(1) # 时钟
def show(buf):
for y in range(8): # 每循环控制所有灯板同一列
buffer1[0] = 0x80 # 选中行
buffer1[0] = buffer1[0] >> y
for i in range(2): # 每循环控制一个灯板
buffer0[0] = buf[(2 - i) * 8 - 1 - y] # 选中列
write_buffer(buffer0[0])
write_buffer(buffer1[0])
# 推出寄存器
pin2.value(1)
pin2.value(0)
7 遇到的主要难题及解决方法
本项目遇到的困难不多,主要遇到了micropython对于某些python特性不支持的问题,比如,需要对list打乱排序的shuffle函数,micropython不支持,项目中改用了lambda表达式结合随机数达成了相同的效果。另外,python的eval()函数可以很方便得通过字符串对应到相应的变量名,但是micropython的eval仅支持全局变量。针对此现象,只能更改代码,将无法使用的部分使用if else代替。
6 收获与感想
本次项目属于是参加硬禾学堂活动以来做的最完整和硬核的一个项目了,从原理设计、代码编写、机械结构设计的流程全都走了一遍,也实现了预想的功能。以往的项目都停留在demo水平,这次算是做出了突破,实属不易。
本次项目我最大的突破就是是我第一个面向对象的嵌入式项目,一个偶然的机会,我翻阅了稚晖君电子小机器人的源码,其C++的面向对象特性直接击穿了我对嵌入式项目的认知。我一直想尝试一下,但使用C++或者用C模拟面向对象特性来开发stm32对我来说还是跨度太大,扯着蛋了。
最后,我还是拥抱了之前看不起的micropython。令我深感意外的是,开发micropython的体验竟然竟然如此的好。micropython可以让你直接免疫配环境和玄学问题,将主要精力集中在算法设计问题上,而且受pycharm的加持,智能提示的体验已经没有提升空间了。资料方面呢可以很方便地在电子森林等平台找到。有一说一,对于想锻炼代码设计水平,尝试面向对象的兄弟们,非常推荐大家梭哈。