硬禾学堂树莓派PICO扩展板制作的电子相册
- 初识树莓派PICO以及项目目标
树莓派PICO是树莓派基金会推出的微控制器,支持c++和micropython编程,采用双核心双核 ARM Cortex M0 +,运行频率高达133Mhz(在micropython环境下默认为125mhz)。与之前的arduino uno为代表的avr架构MCU相比是强大了很多,作为树莓派的老粉丝我当然要买来玩玩。
于是我便参加了此次硬禾学堂的项目《树莓派PICO的扩展功能板》,在给定的三个任务中,我选择了第二个,制作电子相册的任务。对于不太熟悉micropython的我来说,第二个任务比较容易完成。
设计一个带有背景音乐的数码相框
1.将多张照片保存在SD卡中,能够在240*240的LCD屏幕上以至少3种不同的切换模式轮流播放照片,模式的切换由按键控制
2.播放照片的同时,播放背景音乐,通过蜂鸣器或耳机插孔输出
3.利用姿态传感器,旋转板卡,照片可以自动旋转,保证无论板卡是什么方向,照片的方向都是正的
- 几经修改的实现过程
首先要观察树莓派PICO扩展板的原理图。原理图对于一个电子设备非常重要,没有的话就无法下手开发。(在这里强烈谴责一下某些店铺销售电子模块不提供原理图的行为。)
结合上面的PICO引脚功能图和原理图,可以得到如下结论:
1,LCD240x240连接在SPI0接口上,LCD的reset引脚接在GPIO0,CS片选引脚接地,一直选中。
2,旋转编码器以及几个按钮连接的引脚有4.7k上拉电阻,编程时候应该检测低电平。
3,SD卡的连接有一些奇怪,不是SPI接法,似乎也不是SDIO,最终我决定软件模拟SPI来读取SD卡。
4,蜂鸣器接在GPIO16,支持PWM输出。注意到了三极管开关电路和续流二极管。
5,MMA7660加速度传感器挂载在IIC总线上,地址为0x4c。
6,红外接收部分没焊接。
然后下载资料,从raspberrypi官网,先后下载了几本关于PICO和micropython的手册。
此外还有st7789屏幕驱动芯片的手册和MMA7660加速度计的资料。
关于micropython的学习,只要有python的基础,其实不难,只需要看看micropython官网的Quick reference for the RP2(RP2的速查手册)即可掌握IO外设,总线的调用函数。
能够基本使用PICO开发板,我做的第一件事是搞定屏幕驱动。感谢漂移君的ST7789py.py,让我可以轻松在屏幕上画像素点,划线,画矩形,填充颜色等。
图片显示部分,我在st7789py的基础上加了三个方法,来实现显示bmp图片。只需要传入一个文件指针即可。可以实现三种不同的显示风格。
部分代码如下:
# 修改库函数,实现整个屏幕bmp显示
# 传入一个文件指针
def showMyBmp(self,fid):
# 跳过10字节
rm=fid.read(10)
rm=0
p1=fid.read(4)
p1=struct.unpack('<I',p1) # 小端方式读取像素开始位置
p1=int(p1[0])
fid.seek(0,p1) # 移动指针到像素数据位置
self._set_window(0,0,240,240)
RGB=[0,0,0]
for iii in range(240*240/_BUFFER_SIZE):
for ii in range(_BUFFER_SIZE):
# 读像素
for i in range(3):
rgb=struct.unpack('B',fid.read(1))
RGB[i]=int(rgb[0])
# 像素转为rgb565
color=self.color565(RGB[0],RGB[2],RGB[1])
# 像素转为二进制
pixel=_encode_pixel(color)
self.dc.on()
self._write(None, pixel)
因为图片要储存在sd卡中,我需要读取SD卡中的bmp文件。使用softSPI和os.mount可以将SD卡挂载到文件系统上,直接访问。Os.chdir()和os.listdir()可以修改目录和显示目录下的文件,至于打开文件,直接使用with open 即可。
SD卡部分代码:
# 硬件初始化部分---------------------------------------------
# 挂载sd卡
# 灵魂接线,我不得不使用了软件SPI!!!
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()
MMA7660加速度传感器的工作原理是检测重力在XYZ轴方向的分量,从而计算当前的姿态。通过阅读手册,我发现只需要向地址xxxx写入1即可启动传感器,同时读取地址xxxx的数据,就可以判断不同姿态。
写入数据和读取数据的代码如下:
i2c=machine.SoftI2C(scl=MMA7660.sclpin,sda=MMA7660.sdapin,freq=400000)
print('MMA7660 address={}'.format(i2c.scan()[0]))
i2c.writeto_mem(76,7,b'1')
time.sleep_ms(10)
print('MMA7660 init')
为了方便使用MMA7660,我仿照st7789py.py也编写了MMA7660py.py。使用pythn中面向对象编程的方法,新建了MMA7660这个类,以及读取XYZ轴加速度和姿态的方法。
# 读MMA7660三轴加速度传感器数据
# 2021/7/31/1/20
import machine
import struct
import time
class MMA7660:
sclpin=machine.Pin(11)
sdapin=machine.Pin(10)
def init(self):
global i2c
i2c=machine.SoftI2C(scl=MMA7660.sclpin,sda=MMA7660.sdapin,freq=400000)
print('MMA7660 address={}'.format(i2c.scan()[0]))
# 使能
i2c.writeto_mem(76,7,b'1')
time.sleep_ms(10)
print('MMA7660 init')
def close(self):
i2c.writeto_mem(76,7,b'0')
print('MMA7660 closed')
def _bit2num(self,x):
xsign=(x&32)>>5 # 取符号位
x=x&31 # 取数值
if xsign==1:
x=-x
return x
def readxyz(self):
x=i2c.readfrom_mem(76,0,8)
y=i2c.readfrom_mem(76,1,8)
z=i2c.readfrom_mem(76,2,8)
# 数据转换
# 6bit结果输出,移位操作
x=struct.unpack('<B',x)[0]
y=struct.unpack('<B',y)[0]
z=struct.unpack('<B',z)[0]
# 调用函数,6位转为数字
x=MMA7660.bit2num(self,x)
y=MMA7660.bit2num(self,y)
z=MMA7660.bit2num(self,z)
return x,y,z
def readtilt(self):
tilt_data=i2c.readfrom_mem(76,3,8)
tilt_data=struct.unpack('<B',tilt_data)
tilt_data=int(tilt_data[0])
# Bafro 00 unknown 01 Front 10 Back
Bafro=(tilt_data<<6)>>6
# PoLa 000 unkonwn 001 Left 010 Right 101 Down 110 Up
PoLa=(tilt_data&28)>>2
pos=0
if PoLa==0:
pos='unknown!'
elif PoLa==1:
pos='Left'
elif PoLa==2:
pos='Rignt'
elif PoLa==5:
pos='Down'
elif PoLa==6:
pos='Up'
return pos
需要注意的是,在python中(micropython也一样),BYTE类型(二进制)需要使用struct包进行转换,转换为整型,浮点型的数据。此外,对int型使用左移指令时候,会在右边自动补0使数字变大,因此不推荐左移位,推荐使用按位逻辑运算来实现一些数据的处理。
音乐播放部分,只需要输出特定频率的pwm'波即可模拟音符,我选择了一首欢乐颂。
# c调音符的频率
list_freqC=[60000,261,293,329,349,392,440,493]
- 遇到的坑和修复
在我最初的计划中,要使用_thread库实现调用双核,分别运行图片显示程序以及音乐播放程序,使用定时中断来查询姿态变化并且调整图片方向。但是计划看起来很美,事实很残酷。在实际运行当中,图片显示程序运行一行后就卡住了,音乐播放也会变成糟糕的噪声。尽管官方例程运行很顺利。
在论坛查询到很多同样的问题,也有人反映给官方去修改。
但是我肯定来不及等官方修正错误了,所以我尝试单核心运行,同时使用定时器中断来实现所有功能。主进程是图片刷新,中断负责音符播放和姿态检测。
以下代码通过在中断函数中改变中断的设置实现音乐播放和空节拍。
def interp1(timer):
global mpos,mlength
if mpos>=mlength:
mpos=0
pwm.freq(list_freqC[list_music[mpos][0]])
pwm.duty_u16(volume)
timer1.init(freq=list_music[mpos][1],callback=interp1_2)
def interp1_2(timer):
pwm.duty_u16(0)
global mpos
mpos=mpos+1
timer1.init(freq=100,callback=interp1)
# 定时器2通过2秒的中断查询姿态
mma7660=MMA7660py.MMA7660()
mma7660.init()
# 初始方向
global pic_up
pic_up=2
def interp2(timer):
global pic_up
result=mma7660.readtilt()
# print(result)
if result=='unknown!':
pic_up=2 # 默认正方向
elif result=='Left':
pic_up=3
elif result=='Rignt':
pic_up=1
elif result=='Up':
pic_up=2
elif result=='down':
pic_up=0
timer1=machine.Timer() # 定时器1
timer1.init(freq=1,mode=machine.Timer.PERIODIC,callback=interp1)
timer2=machine.Timer() # 定时器2
timer2.init(freq=0.5,mode=machine.Timer.PERIODIC,callback=interp2)
- 完整代码
最后放上来本次项目的完整代码,包括主函数,st7789修改的部分,以及自己写的MMA7660py。
# 实现电子相册的全部功能
# 注意啊,pico的双线程模式存在问题,等官方修吧
import machine
import os
import sdcard
import st7789py as st7789
import time
import struct
import _thread
import MMA7660py
# 硬件初始化部分---------------------------------------------
# 挂载sd卡
# 灵魂接线,我不得不使用了软件SPI!!!
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/bmpfile')
print(os.listdir())
# 定义按键 旋转编码器的按键
thekey=machine.Pin(7,machine.Pin.IN)
# 定义lcd_spi引脚
sck=machine.Pin(2,machine.Pin.OUT)
mosi=machine.Pin(3,machine.Pin.OUT)
rst=machine.Pin(0,machine.Pin.OUT)
dc=machine.Pin(1,machine.Pin.OUT)
# 定义屏幕信息
width=240
height=240
CENTER_Y = int(width/2)
CENTER_X = int(height/2)
# 初始化lcd_spi
lcd_spi=machine.SPI(0,baudrate=40000000,polarity=1, phase =0,sck=sck,mosi=mosi)
print(lcd_spi)
# 实例化display对象
display=st7789.ST7789(lcd_spi,width,height,reset=rst,dc=dc,xstart=0,ystart=0,rotation=0)
bmplist=os.listdir()
print(bmplist)
# def playmusic():
# music.play()
#
# _thread.start_new_thread(playmusic,())
global pwm
pwm=machine.PWM(machine.Pin(16))
# 利用定时器中断放音乐
# c调音符的频率
list_freqC=[60000,261,293,329,349,392,440,493]
#音符列表,音符和演奏时间
# 欢乐颂
list_music=([3,2],[3,2],[4,2],[5,2],
[5,2],[4,2],[3,2],[2,2],
[1,2],[1,2],[2,2],[3,2],
[3,2],[2,2],[2,2],[0,2],
[3,2],[3,2],[4,2],[5,2],
[5,2],[4,2],[3,2],[2,2],
[1,2],[1,2],[2,2],[3,2],
[2,2],[1,2],[1,2],[0,2],
[2,2],[2,2],[3,2],[1,2],
[2,2],[3,2],[3,2],[1,2],
[2,2],[3,2],[3,2],[2,2],
[1,2],[2,2],[-5,2],[3,2],
[3,2],[3,2],[4,2],[5,2],
[5,2],[4,2],[3,2],[2,2],
[1,2],[1,2],[2,2],[3,2],
[2,2],[1,2],[1,2],[0,2]
)
# 播放需要的变量,定时器1通过多次中断播放音乐
global mpos,volume,mlength
mpos=0
mlength=len(list_music)
volume=0
def interp1(timer):
global mpos,mlength
if mpos>=mlength:
mpos=0
pwm.freq(list_freqC[list_music[mpos][0]])
pwm.duty_u16(volume)
timer1.init(freq=list_music[mpos][1],callback=interp1_2)
def interp1_2(timer):
pwm.duty_u16(0)
global mpos
mpos=mpos+1
timer1.init(freq=100,callback=interp1)
# 定时器2通过2秒的中断查询姿态
mma7660=MMA7660py.MMA7660()
mma7660.init()
# 初始方向
global pic_up
pic_up=2
def interp2(timer):
global pic_up
result=mma7660.readtilt()
# print(result)
if result=='unknown!':
pic_up=2 # 默认正方向
elif result=='Left':
pic_up=3
elif result=='Rignt':
pic_up=1
elif result=='Up':
pic_up=2
elif result=='down':
pic_up=0
timer1=machine.Timer() # 定时器1
timer1.init(freq=1,mode=machine.Timer.PERIODIC,callback=interp1)
timer2=machine.Timer() # 定时器2
timer2.init(freq=0.5,mode=machine.Timer.PERIODIC,callback=interp2)
# 显示图片
# 图片方向:0 下 2 上 3 右 4 下
#time.sleep(2) # 等待姿态传感器确定方向
#按键中断,按下改变图片刷新方式!
pic_method=0 # 显示图片方法参数0,1,2三种
def keyint(irq): # 手册可没说要加irq参数
# display.fill(st7789.RED)
global pic_method
pic_method=pic_method+1
if pic_method>2:
pic_method=0
print('pic_method==',pic_method)
time.sleep_ms(100)
irq_flag=1
thekey.irq(keyint,machine.Pin.IRQ_FALLING)
# 死循环中显示图片和改变显示方向
while True:
for xx in bmplist:
fid=open(xx,'rb')
# 图片显示方向选择
display.rotation(pic_up)
# 图片显示方式选择
if pic_method==0:
display.showMyBmp(fid)
elif pic_method==1:
display.showMyBmp2(fid)
elif pic_method==2:
display.showMyBmp3(fid)
修改的st7789部分:
# 修改库函数,实现整个屏幕bmp显示
# 传入一个文件指针
def showMyBmp(self,fid):
# 跳过10字节
rm=fid.read(10)
rm=0
p1=fid.read(4)
p1=struct.unpack('<I',p1) # 小端方式读取像素开始位置
p1=int(p1[0])
fid.seek(0,p1) # 移动指针到像素数据位置
self._set_window(0,0,240,240)
RGB=[0,0,0]
for iii in range(240*240/_BUFFER_SIZE):
for ii in range(_BUFFER_SIZE):
# 读像素
for i in range(3):
rgb=struct.unpack('B',fid.read(1))
RGB[i]=int(rgb[0])
# 像素转为rgb565
color=self.color565(RGB[0],RGB[2],RGB[1])
# 像素转为二进制
pixel=_encode_pixel(color)
self.dc.on()
self._write(None, pixel)
# 第二种显示图片方法
def showMyBmp2(self,fid):
# 跳过10字节
rm=fid.read(10)
rm=0
p1=fid.read(4)
p1=struct.unpack('<I',p1) # 小端方式读取像素开始位置
p1=int(p1[0])
fid.seek(0,p1) # 移动指针到像素数据位置
self.fill(RED)
time.sleep_ms(500)
self.fill(GREEN)
time.sleep_ms(500)
self.fill(BLUE)
self._set_window(0,0,240,240)
RGB=[0,0,0]
for iii in range(240*240/_BUFFER_SIZE):
for ii in range(_BUFFER_SIZE):
# 读像素
for i in range(3):
rgb=struct.unpack('B',fid.read(1))
RGB[i]=int(rgb[0])
# 像素转为rgb565
color=self.color565(RGB[0],RGB[2],RGB[1])
# 像素转为二进制
pixel=_encode_pixel(color)
self.dc.on()
self._write(None, pixel)
def showMyBmp3(self,fid):
for ii in range(9):
R=random.randint(0,255)
G=random.randint(0,255)
B=random.randint(0,255)
color=color565(R,G,B)
self.fill_rect(0,240-24*ii,240,24,color)
MMA7660py部分:
# 读MMA7660三轴加速度传感器数据
# 2021/7/31/1/20
import machine
import struct
import time
class MMA7660:
sclpin=machine.Pin(11)
sdapin=machine.Pin(10)
def init(self):
global i2c
i2c=machine.SoftI2C(scl=MMA7660.sclpin,sda=MMA7660.sdapin,freq=400000)
print('MMA7660 address={}'.format(i2c.scan()[0]))
# 使能
i2c.writeto_mem(76,7,b'1')
time.sleep_ms(10)
print('MMA7660 init')
def close(self):
i2c.writeto_mem(76,7,b'0')
print('MMA7660 closed')
def _bit2num(self,x):
xsign=(x&32)>>5 # 取符号位
x=x&31 # 取数值
if xsign==1:
x=-x
return x
def readxyz(self):
x=i2c.readfrom_mem(76,0,8)
y=i2c.readfrom_mem(76,1,8)
z=i2c.readfrom_mem(76,2,8)
# 数据转换
# 6bit结果输出,移位操作
x=struct.unpack('<B',x)[0]
y=struct.unpack('<B',y)[0]
z=struct.unpack('<B',z)[0]
# 调用函数,6位转为数字
x=MMA7660.bit2num(self,x)
y=MMA7660.bit2num(self,y)
z=MMA7660.bit2num(self,z)
return x,y,z
def readtilt(self):
tilt_data=i2c.readfrom_mem(76,3,8)
tilt_data=struct.unpack('<B',tilt_data)
tilt_data=int(tilt_data[0])
# Bafro 00 unknown 01 Front 10 Back
Bafro=(tilt_data<<6)>>6
# PoLa 000 unkonwn 001 Left 010 Right 101 Down 110 Up
PoLa=(tilt_data&28)>>2
pos=0
if PoLa==0:
pos='unknown!'
elif PoLa==1:
pos='Left'
elif PoLa==2:
pos='Rignt'
elif PoLa==5:
pos='Down'
elif PoLa==6:
pos='Up'
return pos