树莓派上基于Python编写按键驱动程序

引言

手里一直有两块RaspberryPi ZeroW板子闲置,最近想玩起来,但最基本的一台微型电脑,除了主板之外,还需要输入输出设备。我对于微型掌机(MINI型的显示/输入)比较感兴趣,于是干脆买了一款基于树莓派的游戏机的配件Game_HAT。该配件配有3.5寸液晶屏可用于输出显示,有一个4方向的遥感按键以及N个独立按键可用于基本输入。配件提供的是游戏系统,我不感兴趣,我希望在官方RaspberryPi的系统中,编写一个驱动程序,将4向摇杆按键和2个独立按键模拟成鼠标的功能。而编写内核驱动稍微复杂,于是决定使用Python来实现。

硬件准备

根据Game_HAT资料,其屏幕是HDMI接口,因此将RaspberryPi ZeroW插到套件排座上,然后通过MINI HDMI转标准HDMI直接连接屏幕即可。至于按键连接的管脚编号,Game_HAT也已给出,本驱动目前只用到四向摇杆+Y/B按键。

排针引脚编号 功能 对应引脚编号(BCM和WiringPi两种)
29 UP BCM=5,WPI=21
31 DOWN BCM=6,WPI=22
33 LEFT BCM=13,WPI=23
35 RIGHT BCM=19,WPI=24
38 Y BCM=20,WPI=28
37 A BCM=26,WPI=25

硬件

软件准备

  • 树莓派已使用官方工具烧写Raspberry Pi OS Lite(基于Debian Buster(10)),顺便配置好了WIFI、SSH

  • SSH使用raspberrypi.local作为主机地址,用户名:pi,密码:(烧写时配的),进行登录

  • 如过SSH连不上,可尝试将SD卡接入电脑,在boot分区下,新建“ssh”文件,将SSH开启

  • 已修改镜像源为国内清华镜像,树莓派注意需要修改两个文件:

    编辑 /etc/apt/sources.list 文件 (通常Linux只需要修改这个文件)

    1
    2
    3
    4
    #deb http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi
    deb https://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main non-free contrib rpi
    # Uncomment line below then 'apt-get update' to enable 'apt-get source'
    #deb-src http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi

    编辑 /etc/apt/sources.list.d/raspi.list 文件

    1
    2
    3
    4
    deb https://mirrors.tuna.tsinghua.edu.cn/raspberrypi/ buster main
    #deb http://archive.raspberrypi.org/debian/ buster main
    # Uncomment line below then 'apt-get update' to enable 'apt-get source'
    #deb-src http://archive.raspberrypi.org/debian/ buster main
  • 已安装轻量级图形界面LXDE,可参考教程(PI ZeroW性能较低,官方桌面太卡了,选择Lite OS + LXDE的组合方式)

  • 已配置好HDMI屏幕分辨率匹配屏幕(480×320)

    方法:

    1
    sudo nano /boot/config.txt

    在文件末尾,增加

    1
    2
    3
    4
    5
    hdmi_group=2
    hdmi_mode=87
    hdmi_pixel_freq_limit=20000000
    hdmi_cvt 480 320 60 6 0 0 0
    hdmi_drive=1
  • 已通过命令(sudo raspi-config),配置安装VNC,电脑通过VNC Viewer,通过主机名raspberrypi.local可以进行桌面远程

    VNC_Install

SSH终端软件

SSH终端软件,我选择的是开源的WindTerm,这个软件可以直接看到Linux终端上的文件列表,并且可以直接右键选择编辑器打开,可以很方便的在Windows上编辑Linux终端的文件,修改保存之后也可以直接上传回Linux终端。

pi上文件列表

默认编辑器打开Linux上的文件

Python库安装

  1. 安装rpi.gpio库(树莓派官方系统应该已经默认安装了)

    1
    sudo apt install python3-rpi.gpio
  2. 安装uinput库(用于模拟鼠标设备)

    1
    sudo apt install python3-uinput
  3. 安装distutils库,uinput会依赖该库

    1
    sudo apt install python3-distutils
  4. 配置uinput模块在boot阶段载入

    编辑 /etc/modules文件,在文件末尾增加“uinput”

    1
    sudo nano /etc/modules
    1
    2
    3
    4
    5
    6
    # /etc/modules: kernel modules to load at boot time.
    #
    # This file contains the names of kernel modules that should be loaded
    # at boot time, one per line. Lines beginning with "#" are ignored.

    uinput

驱动源码

脚本文件keydriver_multiprocess.py源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import multiprocessing
import time
import RPi.GPIO as GPIO
import uinput

# 初始化输入GPIO引脚,将引脚拉低
GPIO.setmode(GPIO.BCM) # 使用BCM方式编号
up_pin = 5 # 可对照前文中管脚编号定义
down_pin = 6
left_pin = 13
right_pin = 19
left_button_pin = 20
right_button_pin = 12
GPIO.setup(up_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) # 将管脚均设置为输入上拉模式
GPIO.setup(down_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(left_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(right_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(left_button_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(right_button_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

# 创建虚拟输入设备(鼠标)
device = uinput.Device([uinput.BTN_LEFT, uinput.BTN_RIGHT, uinput.REL_X, uinput.REL_Y])

XY_STEP = 1 # 用于调节光标的移动步长

# 定义一个通用四向摇杆按键操作的进程处理函数
def direction_key_process(queue, pin, direction):
while True:
GPIO.wait_for_edge(pin, GPIO.BOTH) # 该进程Pending等待四向摇杆按键发生动作
while GPIO.input(pin) == direction: # 如果是有效的低电平,那么它将持续向队列写入管脚编号
queue.put(pin)
time.sleep(0.005) # 此延迟可调节光标灵敏度

# 定义一个通用处理左右按键操作的进程
def leftright_key_process(queue, pin):
while True:
GPIO.wait_for_edge(pin, GPIO.BOTH) # 该进程Pending等待左/右按键发生动作
queue.put(pin) # 一旦有状态变化,则将按键编号写入队列

# 定义一个更新光标位置的进程处理函数
def update_position_device():
while True:
pin = position_queue.get() # 该进程Pending等待方向按键队列数据
if pin == up_pin:
device.emit(uinput.REL_Y, -XY_STEP) # 根据按键方向移动光标XY轴位置
elif pin == down_pin:
device.emit(uinput.REL_Y, XY_STEP)
elif pin == left_pin:
device.emit(uinput.REL_X, -XY_STEP)
elif pin == right_pin:
device.emit(uinput.REL_X, XY_STEP)

# 定义一个更新左右按钮状态的进程处理函数
def update_button_device():
while True:
pin = button_queue.get() # 该进程Pending等待左右按键队列数据
if pin == left_button_pin:
if GPIO.input(pin) == GPIO.LOW:
device.emit(uinput.BTN_LEFT, 1) # 按键按下
else:
device.emit(uinput.BTN_LEFT, 0) # 按键释放
elif pin == right_button_pin:
if GPIO.input(pin) == GPIO.LOW:
device.emit(uinput.BTN_RIGHT, 1)
else:
device.emit(uinput.BTN_RIGHT, 0)

# 为方向按键、左右建分别创建一个队列来保存按键操作的引脚
position_queue = multiprocessing.Queue()
button_queue = multiprocessing.Queue()

# 创建多个进程来处理按键和按钮事件(每个按钮一个进程,互不影响)
processes = [
multiprocessing.Process(target=direction_key_process, args=(position_queue, up_pin, GPIO.LOW)),
multiprocessing.Process(target=direction_key_process, args=(position_queue, down_pin, GPIO.LOW)),
multiprocessing.Process(target=direction_key_process, args=(position_queue, left_pin, GPIO.LOW)),
multiprocessing.Process(target=direction_key_process, args=(position_queue, right_pin, GPIO.LOW)),
multiprocessing.Process(target=leftright_key_process, args=(button_queue, left_button_pin)),
multiprocessing.Process(target=leftright_key_process, args=(button_queue, right_button_pin))
]

# 启动所有进程
for p in processes:
p.daemon = True # 设置为守护进程,即在主进程结束时自动结束
p.start()

# 启动两个进程来更新设备状态
update_position_process = multiprocessing.Process(target=update_position_device)
update_position_process.daemon = True # 设置为守护进程,即在主进程结束时自动结束
update_position_process.start()

update_button_process = multiprocessing.Process(target=update_button_device)
update_button_process.daemon = True # 设置为守护进程,即在主进程结束时自动结束
update_button_process.start()

# 等待所有进程结束
for p in processes:
p.join()

update_position_process.join()
update_button_process.join()

测试运行(必须使用sudo执行):

1
sudo python3 keydriver_multiprocess.py

主要代码都做了注释解释,其中关键几点是:

  • 为每个按键都创建了一个进程(非线程),用于检测按键是否有动作;同时还为方向、左右键的动作更新到uinput分别创建了两个进程;使用了两个队列,来建立按键检测进程和uinput更新进程之间的数据通信。
  • 所有进程都要配置为守护进程,使得主进程结束时,可以将所有进程同步结束,否则可能出现Python脚本已退出,但创建的进程还在运行的情况。
  • 所有进程都使用Pending等待的方式等待事件发生(按键检测进程等待IO边沿发生,更新设备状态进程等待队列有数据),才执行代码,以便在无按键动作时尽可能不占用CPU资源

配置开启自启动

若开机自动运行该脚本,则可配置开机自启动。

  1. 将脚本拷贝到/usr目录下

    在/usr下建立一个pyscript目录,专门存放python脚本,以后调试好的python脚本都放这里

    1
    cp keydriver_multiprocess.py /usr/pyscript/
  2. 编辑 /etc/rc.local 文件

    1
    sudo nano /etc/rc.local

    在 exit 0 前添加执行语句

    1
    2
    3
    4
    5
    6
    ...
    ...

    /usr/bin/python3 /usr/pyscript/keydriver_multiprocess.py

    exit 0

效果演示

脚本运行的情况下,若无按键动作,CPU占用率很低。

Rasp屏幕截图

视频演示:

小结

本文使用Python在树莓派上编写了一个基础按键驱动,实现了四方向摇杆按键+2个独立按键模拟鼠标的功能。该驱动程序使用Python编写,实现简单,调试方便。使用多进程、队列通信、等待事件触发的思想,可以实现在按键动作时快速响应,而无按键动作时占用极低的CPU资源。