基于树莓派PICO和电子森林的扩展板,实现了一个带有背景音乐的数码相框。主要完成蜂鸣器播放音乐、姿态传感器数据读取、TF卡文件读取、LCD显示屏现实图片和温度的读取与显示等。
实现了以下功能:
-
将三张照片保存在SD卡中,能够在240*240的LCD屏幕上以4种不同的切换模式轮流播放照片,模式的切换由按键控制
-
播放照片的同时,播放背景音乐,通过蜂鸣器输出
-
利用姿态传感器,旋转板卡,照片可以自动旋转,保证无论板卡是什么方向,照片的方向都是正的
作品功能框图
- 两个按键用来控制切换图片和设置切换模式
- 编码器按键用来控制蜂鸣器音乐的播放与暂停
- tf卡保存图片bin文件
- 通过ADC读取PICO内置温度传感器
- 通过PWM驱动蜂鸣器播放音乐
- 通过I2C驱动姿态传感器,读取姿态
- 通过SPI驱动240*240的LCD屏显示图片
作品的代码编写 1. 按键部分
按键由两个微动开关和一个编码器按键组成。采用中断的触发方式,按键1控制图片的切换,按键2设置图片切换模式,编码器按键控制蜂鸣器的暂停与恢复。
按键中断的优先级高于定时器中断,原本置于按键中断的图片刷新放到了主循环中,按键仅修改参数,产生相关的标志位。
按键中断采用了延时软件消抖的方法,微动开关延时10ms,而编码器按键由于其特殊的物理结构,需要延时100ms,所以设置其任务为不常使用的暂停与恢复蜂鸣器播放的功能,这也避免了外部中断对定时器中断的影响。
# 按键中断,用于切换图片
def key1callback(arg):
global loop_i
global change
if key1.value() == 0:
utime.sleep(0.01)
if key1.value() == 0:
led.toggle()
loop_i = loop_i+1
if loop_i>=3:
loop_i = 0
change = 1
# rf.ReadFile(display, filenames[loop_i], rf_mode)
# display.text(font1, str(round(thermometer(),1)), 208, 232, color=st7789.WHITE, background=st7789.BLACK)
# 按键中断,用于设置切换图片模式
def key2callback(arg):
global rf_mode
if key2.value() == 0:
utime.sleep(0.01)
if key2.value() == 0:
led.toggle()
rf_mode += 1
if rf_mode >= 4:
rf_mode = 0
# 按键中断,用于控制蜂鸣器的播放与暂停
def key_pre_callback(arg):
if key_pre.value() == 0:
utime.sleep(0.1)
if key_pre.value() == 0:
if mySong.stopped:
mySong.continue_()
else:
mySong.stop()
# 按键、LED初始化---------------------------------------------------------------------------------
key_pre = machine.Pin(6, machine.Pin.IN)
key1 = machine.Pin(7, machine.Pin.IN)
key2 = machine.Pin(8, machine.Pin.IN)
key_pre.irq(trigger=machine.Pin.IRQ_FALLING , handler=key_pre_callback)
key1.irq(trigger=machine.Pin.IRQ_FALLING , handler=key1callback)
key2.irq(trigger=machine.Pin.IRQ_FALLING , handler=key2callback)
led = machine.Pin(25, machine.Pin.OUT, value=True)
2.LCD的显示
LCD显示部分主要使用的是一个st7789的库,文件有774行,受篇幅限制,就不放在正文里了。文件来源https://www.eetree.cn/project/detail/234
表示衷心的感谢。但是其中有一个函数,我对其有一定的修改,有必要单独列出来。
我给blit_buffer函数多加了一个参数,为了能够给我后续的图片刷新带来更多的便利。
def blit_buffer(self, buffer, x, y, width, height, index=None):
"""
Copy buffer to display at the given location.
Args:
buffer (bytes): Data to copy to display
x (int): Top left corner x coordinate
Y (int): Top left corner y coordinate
width (int): Width
height (int): Height
"""
if index is None:
self.set_window(x, y, x + width - 1, y + height - 1)
self.write(None, buffer)
elif index == 0:
self.set_window(x, y, x + width - 1, y + height - 1)
self.write(None, buffer)
else:
self.write(None, buffer)
# LCD初始化---------------------------------------------------------------------------------------
st7789_res = 0
st7789_dc = 1
disp_width = 240
disp_height = 240
spi_sck=machine.Pin(2)
spi_tx=machine.Pin(3)
spi0=machine.SPI(0,baudrate=4000000, phase=0, polarity=1, sck=spi_sck, mosi=spi_tx)
display = st7789.ST7789(spi0, disp_width, disp_width,
reset=machine.Pin(st7789_res, machine.Pin.OUT),
dc=machine.Pin(st7789_dc, machine.Pin.OUT),
xstart=0, ystart=0, rotation=0)
display.fill(st7789.BLUE)
在显示图片部分,我参考了fill_rect函数。由于没有仔细阅读st7789的数据手册,我对buffer写入的大小不太了解。fill_rect函数与图片显示函数十分相似,不同点在于一个像素颜色是完全相同的,一个是不同的。fill_rect函数一次write的大小是512byte,最终我测试了512,256,128,1024四种大小,前三种的速度在以秒为单位的计时方式下时间完全一致,最后一种会报错。
def fill_rect(self, x, y, width, height, color):
"""
Draw a rectangle at the given location, size and filled with color.
Args:
x (int): Top left corner x coordinate
y (int): Top left corner y coordinate
width (int): Width in pixels
height (int): Height in pixels
color (int): 565 encoded color
"""
self.set_window(x, y, x + width - 1, y + height - 1)
chunks, rest = divmod(width * height, _BUFFER_SIZE)
pixel = _encode_pixel(color)
self.dc.on()
if chunks:
data = pixel * _BUFFER_SIZE
# print(type(data), len(data))
for _ in range(chunks):
self.write(None, data)
if rest:
self.write(None, pixel * rest)
3.tf卡的文件读取与图片的显示模式
tf卡的读取是我开始作品设计的第一个部分,也是给我当头棒喝的一个部分。我手里的1张128g,1张64g,3张32g,1张16g,1张2g的tf卡全部无法读取。虽然是硬件设计的问题,但这给我带来了很大的挫败感,也导致从一开始的激情满满到痛不欲生。于是作品被丢到一边,一直到项目结束的时候才被重新捡起来。感谢刘锐卿在调试过程中给予的帮助。
最后工作的是一张4g的tf卡。
将图片的尺寸修改为240*240的大小,使用https://lvgl.io/tools/imageconverter
将图片转换成bin格式,format分别为True Color和Binary RGB565 Swap。将bin文件通过读卡器拷贝到tf卡中。文件大小为240*240*2+5=115205。最后5个字节不知道是什么,我会选择在读取文件的时候把它们丢弃掉。
import os
_BUFFER_SIZE = const(512)
def ReadFile(display, filepath=None, mode=0):
global buf
if filepath is None:
filepath='1.bin'
binfile = open(filepath, 'rb') #打开二进制文件
size = os.stat(filepath)[6] - 5 # 去掉最后5个无用的字节
print(size)
if mode == 0:
chunks, rest = divmod(size, _BUFFER_SIZE)
if chunks:
for i in range(chunks):
data = binfile.read(_BUFFER_SIZE) #每次输出512字节
display.blit_buffer(data, 0, 0, 240, 240, i)
if rest:
data = binfile.read(rest) #每次输出一个字节
display.blit_buffer(data, 0, 0, 240, 240, i+1)
elif mode == 1:
binfile.seek(76800)
for i in range(80):
data = binfile.read(480)
display.blit_buffer(data, 0, 160, 240, 80, i)
binfile.seek(38400)
for i in range(80):
data = binfile.read(480) #每次输出512字节
display.blit_buffer(data, 0, 80, 240, 80, i)
binfile.seek(0)
for i in range(80):
data = binfile.read(480) #每次输出512字节
display.blit_buffer(data, 0, 0, 240, 80, i)
elif mode == 2:
for i in range(240):
binfile.seek(115200-480*(i+1))
data = binfile.read(480)
display.blit_buffer(data, 0, 240-(i+1), 240, 1, 0)
elif mode == 3:
for i in range(240):
binfile.seek(480*i)
data = binfile.read(160)
display.blit_buffer(data, 0, 0, 80, 240, i)
for i in range(240):
binfile.seek(480*i+160)
data = binfile.read(160)
display.blit_buffer(data, 80, 0, 80, 240, i)
for i in range(240):
binfile.seek(480*i+320)
data = binfile.read(160)
display.blit_buffer(data, 160, 0, 80, 240, i)
else:
print("ReadFile wrong!")
binfile.close()
# tf卡初始化---------------------------------------------------------------------------------------
sd_spi=machine.SoftSPI(0, sck=machine.Pin(17,machine.Pin.OUT),
mosi=machine.Pin(18,machine.Pin.OUT),
miso=machine.Pin(19,machine.Pin.IN))
sd=sdcard.SDCard(sd_spi,cs=machine.Pin(22))
sd.init_spi(40_000_000)
sd.init_card()
x=os.mount(sd, '/sd')
os.chdir('sd')
print(os.getcwd())
ReadFile的最后一个参数用来选择图片显示模式。在最基本的正常显示逻辑下,我又设计了3种图片显示模式,本来想设计更多的模式,可是看了一下越来越长的刷新时间,便打消了念头。目前的四种模式为从上往下刷,从下往上刷,横向3段式刷新,纵向三段式刷新。其实现有的刷新模式频率可以通过优化代码来提高,但迫于时间压力,没有继续。
4.温度传感器
树莓派PICO内部集成了温度传感器,只需要读取相应管脚的ADC数值,并经过一定的计算就可以得到。这里我测量了5次并取平均。
import machine
import time
def thermometer():
temp_a = 0
sensorTemp = machine.ADC(4)
offset = 3.3/(65535)
time.sleep(0.01)
for i in range(5):
read = sensorTemp.read_u16() * offset
temp = 27 - (read - 0.706) / 0.001721
temp_a += temp
return temp_a/5.0
if __name__ == "__main__":
a = thermometer()
print(a, type(a), round(a, 1), type(round(a, 1)))
温度数值在LCD屏的右下角显示,并且随着LCD屏的刷新而刷新。
5.蜂鸣器
蜂鸣器使用了buzzer_music文件,可以在https://onlinesequencer.net/
网站导入midi文件,设计音乐。自己尝试了一番,设计的总是类似于小灵通的铃声,很急促而且还不好听。最终选取的是想成为大佬的咸鱼
设计的音乐文件,buzzer_music文件也是拷贝的他的,其它的很多代码也有参考他的,他的项目是我PICO的入门指南,在此一并表示感谢。
蜂鸣器使用了25Hz的定时器进行驱动,避免对图片刷新产生影响。
song = '0 A#4 1 1;2 F5 1 1;4 D#5 1 1;8 D5 1 1;11 D5 1 1;6 A#4 1 1;14 D#5 1 1;'\
'18 A#4 1 1;20 D#5 1 1;22 A#4 1 1;24 D5 1 1;27 D5 1 1;30 D#5 1 1;32 A#4 1 1;'\
'34 F5 1 1;36 D#5 1 1;38 A#4 1 1;40 D5 1 1;43 D5 1 1;46 D#5 1 1;50 A#4 1 1;'\
'52 D#5 1 1;54 G5 1 1;56 F5 1 1;59 D#5 1 1;62 F5 1 1;64 A#4 1 1;66 F5 1 1;'\
'68 D#5 1 1;70 A#4 1 1;72 D5 1 1;75 D5 1 1;78 D#5 1 1;82 A#4 1 1;84 D#5 1 1;'\
'86 A#4 1 1;88 D5 1 1;91 D5 1 1;94 D#5 1 1;96 A#4 1 1;100 D#5 1 1;102 A#4 1 1;'\
'104 D5 1 1;107 D5 1 1;110 D#5 1 1;114 A#4 1 1;116 D#5 1 1;118 G5 1 1;120 F5 1 1;123 D#5 1 1;126 F5 1 1;98 F5 1 1'
# 40ms定时器中断,用于蜂鸣器播放音乐
def tim1callback(arg):
mySong.tick()
# 按键中断,用于控制蜂鸣器的播放与暂停
def key_pre_callback(arg):
if key_pre.value() == 0:
utime.sleep(0.1)
if key_pre.value() == 0:
if mySong.stopped:
mySong.continue_()
else:
mySong.stop()
mySong = music(song)
tim1 = machine.Timer() # 初始化定时器
tim1.init(freq=25, mode=machine.Timer.PERIODIC, callback=tim1callback)
6.姿态传感器
姿态传感器的型号为MMA7660,提供了姿态的接口,直接读取即可。
import machine
import struct
import time
class MMA7660:
sclpin=machine.Pin(11)
sdapin=machine.Pin(10)
def __init__(self):
self.i2c=machine.I2C(1, scl=MMA7660.sclpin,sda=MMA7660.sdapin,freq=400000)
print('MMA7660 address={}'.format(self.i2c.scan()[0]))
# 使能
self.i2c.writeto_mem(76,0x07,b'1')
print('MMA7660 init')
def close(self):
self.i2c.writeto_mem(76,0x07,b'0')
print('MMA7660 closed')
# 6bit结果输出,移位操作
def bit2num(self,x):
if x&64:
return None
xsign=(x&32)>>5 # 取符号位
x=x&31 # 取数值
if xsign==1:
x=-x
return x
def readxyz(self):
x= mma7660.readfrom_mem(76, 0x00, 8)
y= mma7660.readfrom_mem(76, 0x01, 8)
z= mma7660.readfrom_mem(76, 0x02, 8)
# 数据转换
x=struct.unpack('<B',x)[0]
y=struct.unpack('<B',y)[0]
z=struct.unpack('<B',z)[0]
# 调用函数,6位转为数字
x=self.bit2num(x)
y=self.bit2num(y)
z=self.bit2num(z)
return x,y,z
def readtilt(self):
t=self.i2c.readfrom_mem(76, 0x03, 8, addrsize=8)
t1 = struct.unpack('<8B',t)[0]
t2 = (t1<<3)>>5
if not t2 ^ 0b110:
pose = 'up'
elif not t2 ^ 0b010:
pose = 'ri'
elif not t2 ^ 0b101:
pose = 'do'
elif not t2 ^ 0b001:
pose = 'le'
else:
pose = None
return pose
if __name__ == "__main__":
a = MMA7660()
while True:
print(a.readtilt())
time.sleep(0.1)
代码主体由刘锐卿编写,表示感谢。
7.图片显示方向
图片显示方向由姿态传感器的返回结果决定,当姿态发生变化时,更改图片显示方向并刷新图片。在显示新的图片以前,需要先将显示刷成纯色背景,这样图片的刷新才有较好的效果。
# 更新姿态并刷新图片
def update_pose():
global pre_pose
global change
pose = mma.readtilt()
print(pose)
if change == 1:
change = 0
pass
elif pre_pose == pose or pose== None :
pre_pose = pose
return
elif pose=='up':
display.rotation(0)
display.xstart = 0
display.ystart = 0
elif pose=='le':
display.rotation(1)
display.xstart = 0
display.ystart = 0
elif pose=='do':
display.rotation(2)
display.xstart = 0
display.ystart = 80
elif pose=='ri':
display.rotation(3)
display.xstart = 80
display.ystart = 0
else:
pass
pre_pose = pose
display.fill(st7789.BLACK)
rf.ReadFile(display, filenames[loop_i], rf_mode)
display.text(font1, str(round(thermometer(),1)), 208, 232, color=st7789.WHITE, background=st7789.BLACK)
在图片不同方向显示上,虽然st7789.py文件说明是240*240的显示屏驱动,但实际使用有一定的bug,体现在rotation()函数在参数是1和2的时候,LCD显示屏只刷新一部分。猜测是驱动是为240*320的屏幕写的。参考https://www.eetree.cn/project/detail/513
修改起始点,完美解决问题。对作者表示感谢。
8.程序逻辑
整个程序的大部分功能在中断中完成,只有图片刷新这样的耗时功能在主循环中完成,这样可以保证所有功能的正常并行运行。
import machine
import os
import utime
from drive import sdcard
from drive import st7789
from drive import ReadFile as rf
from drive.MMA7660 import MMA7660
from drive.buzzer_music import music
from drive.thermometer import thermometer
from fonts import vga2_8x8 as font1
from fonts import vga1_16x32 as font2
rf_mode = 0 # 图片刷新模式
pre_pose = None # 记录上一个姿态
change = 0 # 按键切换图片标志
# 40ms定时器中断,用于蜂鸣器播放音乐
def tim1callback(arg):
mySong.tick()
# 按键中断,用于切换图片
def key1callback(arg):
global loop_i
global change
if key1.value() == 0:
utime.sleep(0.01)
if key1.value() == 0:
led.toggle()
loop_i = loop_i+1
if loop_i>=3:
loop_i = 0
change = 1
# rf.ReadFile(display, filenames[loop_i], rf_mode)
# display.text(font1, str(round(thermometer(),1)), 208, 232, color=st7789.WHITE, background=st7789.BLACK)
# 按键中断,用于设置切换图片模式
def key2callback(arg):
global rf_mode
if key2.value() == 0:
utime.sleep(0.01)
if key2.value() == 0:
led.toggle()
rf_mode += 1
if rf_mode >= 4:
rf_mode = 0
# 按键中断,用于控制蜂鸣器的播放与暂停
def key_pre_callback(arg):
if key_pre.value() == 0:
utime.sleep(0.1)
if key_pre.value() == 0:
if mySong.stopped:
mySong.continue_()
else:
mySong.stop()
# 更新姿态并刷新图片
def update_pose():
global pre_pose
global change
pose = mma.readtilt()
print(pose)
if change == 1:
change = 0
pass
elif pre_pose == pose or pose== None :
pre_pose = pose
return
elif pose=='up':
display.rotation(0)
display.xstart = 0
display.ystart = 0
elif pose=='le':
display.rotation(1)
display.xstart = 0
display.ystart = 0
elif pose=='do':
display.rotation(2)
display.xstart = 0
display.ystart = 80
elif pose=='ri':
display.rotation(3)
display.xstart = 80
display.ystart = 0
else:
pass
pre_pose = pose
display.fill(st7789.BLACK)
rf.ReadFile(display, filenames[loop_i], rf_mode)
display.text(font1, str(round(thermometer(),1)), 208, 232, color=st7789.WHITE, background=st7789.BLACK)
song = '0 A#4 1 1;2 F5 1 1;4 D#5 1 1;8 D5 1 1;11 D5 1 1;6 A#4 1 1;14 D#5 1 1;'\
'18 A#4 1 1;20 D#5 1 1;22 A#4 1 1;24 D5 1 1;27 D5 1 1;30 D#5 1 1;32 A#4 1 1;'\
'34 F5 1 1;36 D#5 1 1;38 A#4 1 1;40 D5 1 1;43 D5 1 1;46 D#5 1 1;50 A#4 1 1;'\
'52 D#5 1 1;54 G5 1 1;56 F5 1 1;59 D#5 1 1;62 F5 1 1;64 A#4 1 1;66 F5 1 1;'\
'68 D#5 1 1;70 A#4 1 1;72 D5 1 1;75 D5 1 1;78 D#5 1 1;82 A#4 1 1;84 D#5 1 1;'\
'86 A#4 1 1;88 D5 1 1;91 D5 1 1;94 D#5 1 1;96 A#4 1 1;100 D#5 1 1;102 A#4 1 1;'\
'104 D5 1 1;107 D5 1 1;110 D#5 1 1;114 A#4 1 1;116 D#5 1 1;118 G5 1 1;120 F5 1 1;123 D#5 1 1;126 F5 1 1;98 F5 1 1'
filenames = ['pcb.bin', 'yh.bin', 'rp.bin']
loop_i = 0
# 按键、LED初始化---------------------------------------------------------------------------------
key_pre = machine.Pin(6, machine.Pin.IN)
key1 = machine.Pin(7, machine.Pin.IN)
key2 = machine.Pin(8, machine.Pin.IN)
key_pre.irq(trigger=machine.Pin.IRQ_FALLING , handler=key_pre_callback)
key1.irq(trigger=machine.Pin.IRQ_FALLING , handler=key1callback)
key2.irq(trigger=machine.Pin.IRQ_FALLING , handler=key2callback)
led = machine.Pin(25, machine.Pin.OUT, value=True)
# tf卡初始化---------------------------------------------------------------------------------------
sd_spi=machine.SoftSPI(0, sck=machine.Pin(17,machine.Pin.OUT),
mosi=machine.Pin(18,machine.Pin.OUT),
miso=machine.Pin(19,machine.Pin.IN))
sd=sdcard.SDCard(sd_spi,cs=machine.Pin(22))
sd.init_spi(40_000_000)
sd.init_card()
x=os.mount(sd, '/sd')
os.chdir('sd')
print(os.getcwd())
# LCD初始化---------------------------------------------------------------------------------------
st7789_res = 0
st7789_dc = 1
disp_width = 240
disp_height = 240
spi_sck=machine.Pin(2)
spi_tx=machine.Pin(3)
spi0=machine.SPI(0,baudrate=4000000, phase=0, polarity=1, sck=spi_sck, mosi=spi_tx)
display = st7789.ST7789(spi0, disp_width, disp_width,
reset=machine.Pin(st7789_res, machine.Pin.OUT),
dc=machine.Pin(st7789_dc, machine.Pin.OUT),
xstart=0, ystart=0, rotation=0)
display.fill(st7789.BLUE)
# 定时器、姿态传感器、蜂鸣器初始化-------------------------------------------------------------------
mma = MMA7660()
mySong = music(song)
tim1 = machine.Timer() # 初始化定时器
tim1.init(freq=25, mode=machine.Timer.PERIODIC, callback=tim1callback)
# ----------------------------------------------------------------------------------------
display.fill(st7789.BLACK)
time_start=utime.time()
rf.ReadFile(display, 'pcb.bin', 0)
display.text(font1, str(round(thermometer(),1)), 208, 232, color=st7789.WHITE, background=st7789.BLACK)
time_end=utime.time()
print('totally cost: ',time_end-time_start)
while True:
update_pose()
utime.sleep(1)
每一个硬件驱动基本都封装成一个单独的文件,这样可以使顶层文件简洁明了,仅150余行。
总结
作品在功能上较好完成。3个功能都能够完全实现,程序能够正常运行,没有明显bug。程序结构简洁,代码清晰明了。
但性能上有所欠缺,突出体现在LCD屏的刷新速度上,尽管受限于硬件设计,但速度的上限没有完全逼近。
蜂鸣器的音乐没能自主设计,理论上可以将音乐文件保存在tf卡中进行读取,可以实现长音乐的播放。
已经将全部的代码文件及测试样例上传到附件当中。
参考资料也已全部上传,部分文件由于大小限制未能上传,请到树莓派官网下载。
致谢
首先感谢硬禾学堂提供这样一个机会,能够完成一个作品,在交流与思考中成长。感谢在我完成作品过程中提供帮助的人们,包括上文所提到的两位。还有其它在我困难时给予我帮助的人,由于他们的作品尚未审核,不能够罗列他们的链接。以及在树莓派PICO交流群中指导我的人们,恕我未能一一列举你们。