1.项目简介
我在本次2022寒假在家练活动中选择的是树莓派RP2040的嵌入式系统学习平台,这个开发平台可以使用C、C++和MicroPython进行开发编程。而我在本次项目中使用的是CircuitPython进行开发,它是基于基于MicroPython的一个开发平台,在MicroPython的基础上封装了更多外设功能,使项目开发变得更简便、更快捷。
由于CircuitPython的高度封装简化了项目的开发难度,为了能刚好的学习体验该平台的特点,我选择设计制作贪吃蛇游戏、模拟鼠标和水平仪三个功能并设计制作了一个菜单界面,在三个功能之间进行选择和切换。
2.整体设计思路
- 通过ADC读取X、Y轴摇杆电位器电压值,得到使用者的输入状态,来控制菜单功能选择、贪吃蛇转向和模拟鼠标光标移动等;
- 贪吃蛇游戏中采用列表存储蛇各段的坐标,通过改变列表内数据的值以及列表的长度来实现蛇的移动和增长;
- 模拟鼠标功能中通过读取摇杆的电压值作为X、Y轴的偏移量,控制鼠标光标的移动方向和移动速度;
- 水平仪功能中通过读取MMA7660重力芯片的值,作为水平仪指示光标的显示坐标偏移量,来实现水平仪的功能。
3.程序流程框图
4.驱动程序设计
4.1 LCD屏
在屏幕驱动上,硬禾学堂提供了MicroPython环境下ST7789的驱动程序,但由于CircuitPython在SPI的驱动程序上有所区别,而CircuitPython官方提供的屏幕驱动程序又不匹配本次活动平台的硬件设计,所以我在硬禾学堂提供的驱动程序基础上进行了移植,成功在CircuitPython环境下驱动屏幕。
def lcd_init():#lcd屏初始化,返回屏幕实例
st7789_res = 0 #lcd参数
st7789_dc = 1
disp_width = 240
disp_height = 240
CENTER_Y = int(disp_width/2)
CENTER_X = int(disp_height/2)
lcd_reset = digitalio.DigitalInOut(board.GP0) #初始化复位引脚
lcd_reset.direction = digitalio.Direction.OUTPUT
lcd_dc = digitalio.DigitalInOut(board.GP1) #初始化数据、命令引脚
lcd_dc.direction = digitalio.Direction.OUTPUT
lcd_spi = busio.SPI(clock = board.GP2, MOSI = board.GP3)#初始化硬件SPI
lcd_spi.try_lock()
lcd_spi.configure(baudrate=4000000, phase=0, polarity=1, bits = 8)
display = st7789.ST7789(lcd_spi,
disp_width,
disp_width,
reset=lcd_reset,
dc=lcd_dc,
xstart=0, ystart=0, rotation=0)
display.fill(st7789.BLACK)
return display
4.2 摇杆输入
摇杆的驱动原理是通过ADC读取摇杆的X、Y轴两个电位器分压的电压值,来得到摇杆的操作状态,CircuitPython下可以直接将ADC功能引脚配置为模拟输入模式,读取该引脚的电压值。
def joy_init():#摇杆初始化函数,返回模拟引脚对象
x = analogio.AnalogIn(board.A3)#将引脚初始化为模拟输入
y = analogio.AnalogIn(board.A2)
return x,y
初始化后即直接读取x.value的值得到该引脚的16位adc值。
4.3 HID鼠标
由于CircuitPython已经将HID设备封装好,我们可以在引入库后直接创建鼠标实例,并通过方法直接实现鼠标光标移动、做右键点击等功能。
import usb_hid #引入HID库
from adafruit_hid.mouse import Mouse #引入鼠标设备库
mouse = Mouse(usb_hid.devices) #创建鼠标实例
mouse.move(x=1) #光标右移一个像素
mouse.move(y=1) #光标左移一个像素
mouse.click(Mouse.LEFT_BUTTON) #鼠标左键点击
mouse.click(Mouse.RIGHT_BUTTON) #鼠标右键点击
4.4 MMA7660重力传感器
MMA7660重力传感器同样只提供了MicroPython的程序,而且在CircuitPython下的硬件I2C需要将引脚外部上拉才能正常启动,所以我自己根据这款芯片在其他平台上c语言模拟I2C的驱动程序移植到了RP2040上,并成功读取了X轴的倾斜角。
class MMA7760(): # MMA7660类初始化的创建
def __init__(self, sda, scl):
self.sda = digitalio.DigitalInOut(sda)
self.sda.direction = digitalio.Direction.OUTPUT
self.scl.drivemode = digitalio.DriveMode.OPEN_DRAIN
self.sda.pull = digitalio.Pull.UP
self.scl = digitalio.DigitalInOut(scl)
self.scl.direction = digitalio.Direction.OUTPUT
self.scl.drivemode = digitalio.DriveMode.OPEN_DRAIN
self.scl.pull = digitalio.Pull.UP
# Create library object on our I2C port
self.write_reg(MMA7660_MODE,0x00); #standby mode
self.write_reg(MMA7660_SPCNT,0x00); #No Sleep Count
self.write_reg(MMA7660_INTSU,0xE3); #0xE0 will be OK
self.write_reg(MMA7660_PDET,0xE0); #orginal value: 0xE0
self.write_reg(MMA7660_SR,0x02); #orginal value: 0x34
self.write_reg(MMA7660_PD,0x00); #original value: 0x00
self.write_reg(MMA7660_MODE,0x41); #original value: 0x41
实例化该类后即可通过调用其中的方法得到倾斜角度值
imu = mma7660.MMA7760(sda = board.GP10, scl = board.GP11) #创建实例
imu_data = imu.get_result(mma7660.MMA7660_XOUT) # 调用该方法读取x轴倾斜角度值
5.具体的功能实现及图片展示
5.1菜单设计与演示
定义变量为系统运行状态,当系统启动时状态值为0,该状态下显示菜单功能,菜单初始化先显示三个子功能的名字,通过读取摇杆输入控制改变光标位置值并移动光标,当按下A键后系统状态值会加上光标位置值,使系统进入三个不同的功能。在各个功能中,通过检测SELECT按键按下使系统状态值变为零,回道菜单页面。
line = 1
app_num = 3
sys_state = 0
def menu_loop(dis, joy_y, k_a, k_b): #菜单循环
global sys_state, line
menu_select(joy_y) #光标控制
for n in range(app_num): #光标显示
if(n+1 != line):
dis.rect(20, 87 + 35 * n - 4, 200, 35, st7789.BLACK)
dis.rect(20, 52 + 35 * line - 4, 200, 35, st7789.WHITE)
if k_a.value == 0: # 进入子功能
sys_state = line
while k_a.value == 0:
pass
while True: #系统主循环
if sys_state == 0:
display.fill(st7789.BLACK)#菜单显示初始化
display.text(font2, 'Greedy snake', 24, 85)
display.text(font2, 'Analog mouse', 24, 120)
display.text(font2, 'Gyrosc level', 24, 155)
while sys_state == 0:
menu_loop(display, joy_y, k_a, k_b) # 菜单循环
if sys_state == 1:
display.fill(st7789.BLACK) #贪吃蛇初始化
score = 0
snake = [[10, 20], [11, 20], [12, 20], [13, 20], [14, 20]]
last_snake = [9, 20]
creak_apple()
add_x = 1
add_y = 0
while sys_state == 1:
game_loop(display, joy_x, joy_y, k_a, k_sl, k_st) # 贪吃蛇循环
if sys_state == 2:
display.fill(st7789.BLACK) # 鼠标初始化
button_r = 0
button_l = 0
mouse_page(display)
while sys_state == 2:
mouse_loop(display, joy_x, joy_y, k_a, k_b, k_st, k_sl) #鼠标循环
if sys_state == 3:
display.fill(st7789.BLACK) # 水平仪初始化
while sys_state == 3:
imu_loop(display, imu, k_sl) #水平仪循环
下面是菜单页面的显示效果
5.2贪吃蛇
整体逻辑上:通过列表存储蛇身的位置,每次循环根据蛇头朝向,判断前方状态:如果蛇头前一个没有任何东西,则在列表尾部新增一组坐表作为新蛇头的坐标,并删除列表的第一组坐标,刷新屏幕即可实现移动的效果;如果蛇头前方的位置是苹果,则在列表尾部新增一组坐表作为新蛇头的坐标,实现蛇身变长,并调用随机数函生成新的苹果;如果蛇头前方是蛇身则将该坐标显示为红色,游戏结束,并根据吃的苹果数显示得分。
#主循环贪吃蛇部分
if sys_state == 1:
display.fill(st7789.BLACK) #清屏
score = 0 #得分清零
snake = [[10, 20], [11, 20], [12, 20], [13, 20], [14, 20]] #初始蛇的坐标
last_snake = [9, 20] # 蛇尾上一轮坐标,用来清除显示
creak_apple() # 创建苹果
add_x = 1 #坐标增量,初始方向为向右
add_y = 0
while sys_state == 1:
game_loop(display, joy_x, joy_y, k_a, k_sl, k_st) # 开始循环
def game_loop(dis, joy_x, joy_y, k_a, k_sl, k_st):
global snake, last_snake, add_x, add_y, score #引入苹果坐标,蛇坐标,变化方向全局变量
snake_turn = 0
snake.append([snake[-1][0]+add_x, snake[-1][1]+add_y]) # 蛇移动
if inside_snake(): #碰撞
square(dis, snake[-1][0], snake[-1][1], st7789.RED) # 标红
display.text(font1, 'SCORE : ', 90, 110) # 显示分数
display.text(font1, '%d' %score, 138, 110)
while k_a.value == 0: # 按A重新开始
pass
while k_a.value == 1:
pass
''' 贪吃蛇初始化 '''
dis_clear(dis, st7789.BLACK)
score = 0
snake = [[10, 20], [11, 20], [12, 20], [13, 20], [14, 20]] #最后一位是头
last_snake = [9, 20]
creak_apple()
add_x = 1
add_y = 0
if not inside(): #越界
if add_y == 0: # 横向移动
if snake[-1][0] == 40:
snake[-1][0] = 0
elif snake[-1][0] == -1:
snake[-1][0] = 39
elif add_x == 0:# 纵向移动
if snake[-1][1] == 40:
snake[-1][1] = 0
elif snake[-1][1] == -1:
snake[-1][1] = 39
if snake[-1][0] != apple_x or snake[-1][1] != apple_y: # 没吃到苹果
last_snake = snake[0]
snake.pop(0) # 删除蛇尾
else: # 吃到苹果
score = score + 1 # 得分加一
creak_apple()
'''更新显示'''
square(dis, last_snake[0], last_snake[1], st7789.BLACK) # 清除上一帧蛇尾
square(dis, apple_x, apple_y, st7789.BLUE) # 显示果实
for n in range(len(snake)): # 显示蛇
square(dis, snake[n][0], snake[n][1], st7789.WHITE)
n = 0
while n < snake_speed: # 阻塞延时并读取按键状态
snake_turn = snake_set(dis, joy_x, joy_y, k_a, k_st, snake_turn)
n = n + 1
控制上:通过读取摇杆的值改变舌头移动方向的相对值,实现蛇向不同方向移动;通过读取A键是否按下,改变每次循环的延时时间,实现控制蛇的不同速度;通过读取START按键按下将循环阻塞,实现暂停效果。
def snake_set(dis, joy_x, joy_y, k_a, k_st, turn):
global snake_speed, add_x, add_y, sys_state
datax = joy_x.value
datay = joy_y.value
'''转向'''
if turn == 0: #防止操作过快
if datax > 55535 and 15000 < datay < 50000 and add_x == 0:
add_x = 1
add_y = 0
turn = 1
elif datax < 10000 and 15000 < datay < 50000 and add_x == 0:
add_x = -1
add_y = 0
turn = 1
elif datay > 55535 and 15000 < datax < 50000 and add_y == 0:
add_x = 0
add_y = 1
turn = 1
elif datay < 10000 and 15000 < datax < 50000 and add_y == 0:
add_x = 0
add_y = -1
turn = 1
if k_a.value == 0: # 加速
snake_speed = 10
else:
snake_speed = 30
time.sleep(0.01)
'''暂停'''
if k_st.value == 0:
dis.text(font1, 'PAUSE', 100, 100)
while k_st.value == 0:
pass
time.sleep(0.05)
while k_st.value == 1:
pass
dis.text(font1, ' ', 100, 100)
for n in range(len(snake)):
square(dis, snake[n][0], snake[n][1], st7789.WHITE)
while k_st.value == 0:
pass
if k_sl.value == 0: # 返回菜单
sys_state = 0
while k_sl.value == 0:
pass
return turn
5.3模拟鼠标
该功能使用CircuitPython的USB HID库中的Mouse设备实现,通过读取摇杆电位器的电压值,并进行计算,得到X、Y轴的偏移量,将其发送作为鼠标光标的变化量,同时也作为LCD指示的光标偏移量,实现模拟鼠标和显示的功能。
button_r = 0
button_l = 0
joy_pos = [0, 0]
def mouse_loop(dis, joy_x, joy_y, k_a, k_b, k_st, k_sl):
global sys_state,button_r,button_l
x_data = get_xjoy_value(joy_x) 获得摇杆处理后的值
y_data = get_yjoy_value(joy_y)
''' '''
if joy_pos[0] != x_data or joy_pos[1] != y_data: # 摇杆移动
dis.fill_rect(118 + joy_pos[0], 84 + joy_pos[1], 6, 6, st7789.BLACK)
joy_pos[0] = x_data # 更新上一次摇杆位置
joy_pos[1] = y_data
dis.fill_rect(118 + x_data, 84 + y_data, 6, 6, st7789.WHITE) #屏幕光标
if k_b.value == 0 and button_l == 0: #按键处理,避免多次触发
button_l = 1
mouse.click(Mouse.LEFT_BUTTON)
dis.fill_rect(80, 175, 20, 15, st7789.WHITE) # 按键按下亮起
elif k_b.value == 1 and button_l == 1:
button_l = 0
dis.fill_rect(80, 175, 20, 15, st7789.BLACK) # 按键按下熄灭
if k_a.value == 0 and button_r == 0:
button_r = 1
mouse.click(Mouse.RIGHT_BUTTON)
dis.fill_rect(140, 175, 20, 15, st7789.WHITE) # 按键按下亮起
elif k_a.value == 1 and button_r == 1:
button_r = 0
dis.fill_rect(140, 175, 20, 15, st7789.BLACK) # 按键按下熄灭
mouse.move(x=int(x_data/2)) # 根据摇杆值控制电脑光标移动
mouse.move(y=int(y_data/2))
if k_sl.value == 0: # 退回到主菜单
sys_state = 0
while k_sl.value == 0:
pass
由于得到的摇杆原始值是0~65535之间的值,并且在摇杆归中时不同的摇杆电压值可能不同,所以需要经过换算并加入死区才能得到较好的效果。
def get_xjoy_value(joy):
joy_value = joy.value / 1024 - 32 # 等比缩小并将值平移为正负值
joy_value = int(joy_value)
if joy_value > -6 and joy_value < 6: # 设置死区
joy_value = 0
if joy_value < -4:
joy_value = joy_value + 4
elif joy_value > 4:
joy_value = joy_value - 4
return int(joy_value)
5.4水平仪
成功初始化MMA7660后即可读取其X、Y、Z三轴的倾斜角度,可以直接使用得到的值作为光标显示的偏移量,并在光标中间点显示对照标志,即可实现简易的水平仪功能。
def imu_loop(dis, imu, k_sl):
global sys_state,last_data
imu_data = imu.get_result(mma7660.MMA7660_YOUT) #获得计算后的倾角值
imu_data = imu_data * 3 # 倾角值乘3后作为屏幕光标的偏移量
dis.hline(70, 120, 100, st7789.WHITE)# 显示基准线
if last_data != imu_data: # 如果角度改变
dis.fill_rect(95, 118 + last_data, 50, 5, st7789.BLACK)# 清楚上一次的指示光标
last_data = imu_data
dis.fill_rect(95, 118 + imu_data, 50, 5, st7789.WHITE)# 显示指示光标
if k_sl.value == 0:# 退回到菜单
sys_state = 0
while k_sl.value == 0:
pass
time.sleep(0.01)
6.项目中遇到的问题以及未来改进
6.1 ST7789驱动
在项目最初的测试中屏幕显示就作为十分重要的内容,而CircuitPython提供的ST7789的驱动库必须指定SPI的片选引脚,并且其他引脚的配置方式也不能很好的适用本次项目的硬件设计,所以这就成了我遇到的第一个问题,我认真学习了CircuitPython的SPI使用方法,将MicroPython的屏幕驱动成功移植并点亮。
6.2 鼠标的死区
由于摇杆工艺问题,不同摇杆归中后的电压值都会有所不同,所以如果简单的直接使用中间值作为基准的话可能会出现摇杆漂移的现象,所以必须设计死区消除因摇杆个体差异和归中不正常导致的漂移现象。
而且这次项目设计中由于手中只有一块开发板,所以还不能确定我的程序在其他板子上是否能正达到较好而的效果,此部分仍需改进。
6.3 MMA7660驱动
MMA7660的驱动和ST7789存在相同的问题,而且由于CircuitPython的硬件I2C需要外部上拉,导致只能使用模拟I2C驱动该芯片,我自己设计编写了模拟I2C的驱动程序并成功与MMA7660实现通信,但在读取寄存器的时候只能返回X轴的倾斜角,而且对照c语言的模拟I2C驱动程序不存在任何问题,在查阅一些资料仍未找到解决办法,这一问题还需要查阅资料解决完善。