Lua的集成开发环境ZeroBrane Studio

目前在使用Lua进行脚本的开发,可是官方并没有提供很好的集成开发环境。
体验了很多,发现ZeroBrane Studio这个开源软件还是非常好用的,并且已经能正常支持LinuxWindowsMacOS这三个主流平台。

 

建议去官方网站下载最新的版本,但是鉴于国内网络访问不是非常稳定,可以从本站下载一份目前最新的版本。

下面的版本根据自身操作系统来选择其中一个进行下载

ZeroBraneStudioEduPack-1.70-linux.sh

ZeroBraneStudioEduPack-1.70-macos.dmg

ZeroBraneStudioEduPack-1.70-win32.exe

树莓派下的编译(目前编译出的暂时无法使用):

$ git clone https://github.com/pkulchenko/ZeroBraneStudio.git

#如果代码下载存在问题,可以本站下载一份拷贝
# wget http://www.mobibrw.com/wp-content/uploads/2018/09/ZeroBraneStudio.tar.xz
# tar xvf ZeroBraneStudio.tar.xz

$ cd ZeroBraneStudio

$ cd build

$ bash build-linux-prep-deb.sh

# gthread
$ sudo apt-get install libglib2.0-dev

# gtk+
$ sudo apt-get install libgtk2.0-dev

$ sudo apt-get install libgtk-3-dev

#opengl
$ sudo apt-get install freeglut3-dev

# ssl for luasec
$ sudo apt-get install libssl-dev

$ sudo ln -s /usr/lib/arm-linux-gnueabihf/libssl.so /usr/lib/libssl.so

# lua
$ sudo apt-get install lua5.1 liblua5.1-dev

#webview可选
#sudo apt-get install libwebkitgtk-dev

# luasec最新版本,早期版本编译不通过
$ sed -i "s/^LUASEC_BASENAME=\"luasec-0.6\"/LUASEC_BASENAME=\"luasec-0.7\"/g" build-linux.sh

#此处wxWidgets的克隆比较慢,因此可以本站下载一份拷贝,手工修改脚本的下载
# wget http://www.mobibrw.com/wp-content/uploads/2018/09/wxWidgets.tar.xz
# sed -i "s/^[ \t]*git clone \"\$WXWIDGETS_URL\".*/  wget https:\/\/www.mobibrw.com\/wp-content\/uploads\/2018\/09\/wxWidgets.tar.xz\n  rm -rf wxWidgets\n  tar xvf wxWidgets.tar.xz/g" build-linux.sh
# wget 
# sed -i "s/^[ \t]*git clone \"\$WXLUA_URL\" \"\$WXLUA_BASENAME\".*/ wget https:\/\/www.mobibrw.com\/wp-content\/uploads\/2018\/09\/wxlua.tar.xz\n rm -rf wxlua\n tar xvf wxlua.tar.xz/g" build-linux.sh
# sed -i "s/^LEXLPEG_URL=\"https:\/\/foicica.com\/scintillua\/download/LEXLPEG_URL=\"https:\/\/www.mobibrw.com\/wp-content\/uploads\/2018\/09/g" build-linux.sh

# for debug "bash build-linux.sh debug all"
$ bash build-linux.sh all

#编译两次,解决第一次的问题,第一次有些目录创建存在问题
$ bash build-linux.sh all

#还是需要安装一些依赖,上面编译的库并没有完整完成依赖设置
$ sudo apt-get install luarocks

$ sudo luarocks install luasocket

$ cp deps/lib/libwx.so deps/lib/wx.so

#动态链接库应当设置LUA_CPATH而不是LUA_PATH
$ export LUA_CPATH="`pwd`/deps/lib/?.so;`pwd`/deps/lib/lua/51/?.so"

$ export LD_LIBRARY_PATH=`pwd`/deps/lib

#去掉两个检测,这两检测总是会失败,原因不好排查
$ sed -i "s/check_lua_module(wx TRUE)/#check_lua_module(wx TRUE)/g" CMakeLists.txt 

$ sed -i "s/check_lua_module(socket TRUE)/#check_lua_module(socket TRUE)/g" CMakeLists.txt

$ cmake -DCMAKE_SYSROOT=`pwd`/deps/ -DCMAKE_FIND_ROOT_PATH=`pwd`/deps/ .

$ make 

$ sudo make install

参考链接


Debugging Lua Code
Lightweight IDE for your Lua needs ZeroBrane Studio

解决Raspberry Pi Zero W启动后没有在HDMI口输出内容的问题

最新在使用的Raspberry Pi Zero W V1.3在使用目前(2018.09.26)的系统的时候发现无法正常输出内容到屏幕上面,屏幕一直黑屏无信号。

原因在于Raspberry Pi Zero W在启动的时候没有正确检测到屏幕信号,导致没有正常输出。

解决方法是打开启动配置文件/boot/config.txt, 找到如下内容:

..................

# uncomment if hdmi display is not detected and composite is being output
#hdmi_force_hotplug=1

.................

然后去掉注释,修改为如下:

# uncomment if hdmi display is not detected and composite is being output
hdmi_force_hotplug=1

树莓派实时系统下脚本语言的选择(应当使用Lua而不是Python)

最近在使用树莓派与其他设备通过SPI接口进行通信,使用一个GPIO管脚触发读取数据的信号,为了简化开发,使用了Python

在实际运行过程中,发现当长时间运行的是,会出现中断管脚信号丢失的情况,在参考 Ubuntu 16.04 (x64)下从源代码为Raspberry Pi Zero W编译实时内核  更换为实时内核之后,短时间运行已经可以正常,但是在十几个小时之后,依然出现了中断丢失的现象。

这个现象初步评估为PythonGC动作时间过长导致的中断信号丢失。Python本身并不是为实时系统设计的,因此在GC进行垃圾回收的时候,是没有实时性的考虑的,因此在严格要求实时性的系统环境下,不是非常的合适。更何况很多的IO操作默认都是阻塞的,更加容易导致实时性问题。

由于树莓派本身也是支持Lua脚本的,默认安装的Lua引擎默认是5.1.4Lua本身在游戏中使用较多,而游戏本身对于实时性的要求是很高的。

尤其是Lua 5.1开始使用最新的GC已经能很好的解决实时性问题。

关于LuaGC相关信息,参考如下的文章:

继续阅读树莓派实时系统下脚本语言的选择(应当使用Lua而不是Python)

Ubuntu 16.04 (x64)下从源代码为Raspberry Pi Zero W编译实时内核

首先参考 Ubuntu 16.04 (x64)树莓派B+ (Raspberry Pi B+)源代码编译 保证能够成功编译标准内核的源代码,然后切换到实时内核分支,并执行如下编译命令:

$ export PATH=$PATH:~/rpi/rpi-tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin

$ cd ~/rpi/rpi-linux/
 
$ git checkout rpi-4.14.y-rt

$ git reset --hard

$ KERNEL=kernel
 
$ make clean

$ make mrproper

$ rm -rf .config

#调整内核切换频率,增加实时性
$ sed -i '$a\CONFIG_HZ_1000=y' arch/arm/configs/bcmrpi_defconfig

#Raspberry Pi Zero W的CPU是BCM2835
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bcmrpi_defconfig

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules dtbs -j8
 
$ mkdir rt_kernel
 
$ make modules_install ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- INSTALL_MOD_PATH=./rt_kernel -j8

$ make dtbs_install ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- INSTALL_DTBS_PATH=./rt_kernel -j8

$ ./scripts/mkknlimg ./arch/arm/boot/zImage ./rt_kernel/kernel.img
安装编译好的内核

Ubuntu下面,SD卡会自动挂载,默认挂载到了/media/目录下面,如果是使用NOOBS安装的话,系统目录是固定的,执行如下命令拷贝到目标SD卡上面

$ cd ~/rpi/rpi-linux

#备份需要修改的文件
$ mv /media/`whoami`/boot/kernel.img /media/`whoami`/boot/kernel_old.img

$ mv /media/`whoami`/boot/overlays /media/`whoami`/boot/overlays.old

#拷贝内核
$ cp rt_kernel/kernel.img /media/`whoami`/boot/kernel.img

#拷贝硬件配置
$ cp rt_kernel/bcm2835*.dtb /media/`whoami`/boot/

#拷贝overlays
$ cp -r rt_kernel/overlays /media/`whoami`/boot/

#拷贝内核模块
$ sudo cp -r rt_kernel/lib/modules/* /media/`whoami`/rootfs/lib/modules/

#卸载设备
$ sudo umount -A -R -a /media/`whoami`/boot
借助编译环境,单独编译内核模块

有时,我们需要单独编译新下载的内核驱动,这个时候,就可以使用如下的方式进行单独内核模块的编译。

下面,我们以ASIX AX88772系列的USB有线网卡驱动的编译为例:

$ export PATH=$PATH:~/rpi/rpi-tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin

$ cd ~/rpi/rpi-linux

# wget http://www.mobibrw.com/wp-content/uploads/2018/09/AX88772C_772B_772A_760_772_178_LINUX_DRIVER_v4.22.0_Source.tar.bz2

$ wget http://www.asix.com.tw/FrootAttach/driver/AX88772C_772B_772A_760_772_178_LINUX_DRIVER_v4.22.0_Source.tar.bz2

$ tar xvf AX88772C_772B_772A_760_772_178_LINUX_DRIVER_v4.22.0_Source.tar.bz2

$ cd AX88772C_772B_772A_760_772_178_LINUX_DRIVER_v4.22.0_Source

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C ../ M=`pwd`

#当前目录下就会生成 asix.ko 这个内核模块,生成的内核模块拷贝到指定的目录即可正常工作
解决已知问题

使用上面的命令安装完成内核后,目前(2018.09.26)遇到的问题为,当插入ASIX AX88772系列的USB有线网卡之后,会导致内核崩溃,启动失败。

设备信息如下:

Bus 001 Device 034: ID 0b95:7720 ASIX Electronics Corp. AX88772

初步怀疑是USB设备的驱动依赖关系不正确导致内核崩溃。

目前的临时解决方法为要求设备启动时候优先加载USB设备相关的驱动,而不是等到网卡插入的时候再去加载。

也就是在/boot/config.txt文件尾部新增加一行dtoverlay=dwc2。这段代码本来是为树莓派通过USB访问网络的虚拟网卡准备的(是的,你没看错,树莓派本身可以不借助网卡直接通过USB接口跟电脑共享方式上网,不过需要设置一堆东西,最简单的还是外接真正的USB网卡)。我们加载这个模块,但是并不使用这个功能,造成的结果就是重新调整了模块加载顺序,规避了后续的问题。

$ sed -i '$a\\ndtoverlay=dwc2' /media/`whoami`/boot/config.txt

上述修改后,依旧存在动态插拔网卡,设备会重启的问题,不过已经不影响正常使用。

参考链接


基于协程的Python网络库gevent介绍

继续Python协程方面的介绍,这次要讲的是gevent,它是一个并发网络库。它的协程是基于greenlet的,并基于libev实现快速事件循环(Linux上是epoll,FreeBSD上是kqueue,Mac OS X上是select)。有了gevent,协程的使用将无比简单,你根本无须像greenlet一样显式的切换,每当一个协程阻塞时,程序将自动调度,gevent处理了所有的底层细节。让我们看个例子来感受下吧。

import gevent

def test1():
    print 12
    gevent.sleep(0)
    print 34

def test2():
    print 56
    gevent.sleep(0)
    print 78

gevent.joinall([
    gevent.spawn(test1),
    gevent.spawn(test2),
])

解释下,”gevent.spawn()”方法会创建一个新的greenlet协程对象,并运行它。”gevent.joinall()”方法会等待所有传入的greenlet协程运行结束后再退出,这个方法可以接受一个”timeout”参数来设置超时时间,单位是秒。运行上面的程序,执行顺序如下:

  1. 先进入协程test1,打印12
  2. 遇到”gevent.sleep(0)”时,test1被阻塞,自动切换到协程test2,打印56
  3. 之后test2被阻塞,这时test1阻塞已结束,自动切换回test1,打印34
  4. 当test1运行完毕返回后,此时test2阻塞已结束,再自动切换回test2,打印78
  5. 所有协程执行完毕,程序退出

所以,程序运行下来的输出就是:

12
56
34
78

注意,这里与上一篇greenlet中第一个例子运行的结果不一样,greenlet一个协程运行完后,必须显式切换,不然会返回其父协程。而在gevent中,一个协程运行完后,它会自动调度那些未完成的协程。

我们换一个更有意义的例子:

import gevent
import socket

urls = ['www.baidu.com', 'www.gevent.org', 'www.python.org']
jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]
gevent.joinall(jobs, timeout=5)

print [job.value for job in jobs]

我们通过协程分别获取三个网站的IP地址,由于打开远程地址会引起IO阻塞,所以gevent会自动调度不同的协程。另外,我们可以通过协程对象的”value”属性,来获取协程函数的返回值。

猴子补丁 Monkey patching

细心的朋友们在运行上面例子时会发现,其实程序运行的时间同不用协程是一样的,是三个网站打开时间的总和。可是理论上协程是非阻塞的,那运行时间应该等于最长的那个网站打开时间呀?其实这是因为Python标准库里的socket是阻塞式的,DNS解析无法并发,包括像urllib库也一样,所以这种情况下用协程完全没意义。那怎么办?

一种方法是使用gevent下的socket模块,我们可以通过”from gevent import socket”来导入。不过更常用的方法是使用猴子布丁(Monkey patching):

from gevent import monkey; monkey.patch_socket()
import gevent
import socket

urls = ['www.baidu.com', 'www.gevent.org', 'www.python.org']
jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]
gevent.joinall(jobs, timeout=5)

print [job.value for job in jobs]

上述代码的第一行就是对socket标准库打上猴子补丁,此后socket标准库中的类和方法都会被替换成非阻塞式的,所有其他的代码都不用修改,这样协程的效率就真正体现出来了。Python中其它标准库也存在阻塞的情况,gevent提供了”monkey.patch_all()”方法将所有标准库都替换。

from gevent import monkey; monkey.patch_all()

使用猴子补丁褒贬不一,但是官网上还是建议使用”patch_all()”,而且在程序的第一行就执行。

获取协程状态

协程状态有已启动和已停止,分别可以用协程对象的”started”属性和”ready()”方法来判断。对于已停止的协程,可以用”successful()”方法来判断其是否成功运行且没抛异常。如果协程执行完有返回值,可以通过”value”属性来获取。另外,greenlet协程运行过程中发生的异常是不会被抛出到协程外的,因此需要用协程对象的”exception”属性来获取协程中的异常。下面的例子很好的演示了各种方法和属性的使用。

#coding:utf8
import gevent

def win():
    return 'You win!'

def fail():
    raise Exception('You failed!')

winner = gevent.spawn(win)
loser = gevent.spawn(fail)

print winner.started # True
print loser.started  # True

# 在Greenlet中发生的异常,不会被抛到Greenlet外面。
# 控制台会打出Stacktrace,但程序不会停止
try:
    gevent.joinall([winner, loser])
except Exception as e:
    # 这段永远不会被执行
    print 'This will never be reached'

print winner.ready() # True
print loser.ready()  # True

print winner.value # 'You win!'
print loser.value  # None

print winner.successful() # True
print loser.successful()  # False

# 这里可以通过raise loser.exception 或 loser.get()
# 来将协程中的异常抛出
print loser.exception

协程运行超时

之前我们讲过在”gevent.joinall()”方法中可以传入timeout参数来设置超时,我们也可以在全局范围内设置超时时间:

import gevent
from gevent import Timeout

timeout = Timeout(2)  # 2 seconds
timeout.start()

def wait():
    gevent.sleep(10)

try:
    gevent.spawn(wait).join()
except Timeout:
    print('Could not complete')

上例中,我们将超时设为2秒,此后所有协程的运行,如果超过两秒就会抛出”Timeout”异常。我们也可以将超时设置在with语句内,这样该设置只在with语句块中有效:

with Timeout(1):
    gevent.sleep(10)

此外,我们可以指定超时所抛出的异常,来替换默认的”Timeout”异常。比如下例中超时就会抛出我们自定义的”TooLong”异常。

class TooLong(Exception):
    pass

with Timeout(1, TooLong):
    gevent.sleep(10)

协程间通讯

greenlet协程间的异步通讯可以使用事件(Event)对象。该对象的”wait()”方法可以阻塞当前协程,而”set()”方法可以唤醒之前阻塞的协程。在下面的例子中,5个waiter协程都会等待事件evt,当setter协程在3秒后设置evt事件,所有的waiter协程即被唤醒。

#coding:utf8
import gevent
from gevent.event import Event

evt = Event()

def setter():
    print 'Wait for me'
    gevent.sleep(3)  # 3秒后唤醒所有在evt上等待的协程
    print "Ok, I'm done"
    evt.set()  # 唤醒

def waiter():
    print "I'll wait for you"
    evt.wait()  # 等待
    print 'Finish waiting'

gevent.joinall([
    gevent.spawn(setter),
    gevent.spawn(waiter),
    gevent.spawn(waiter),
    gevent.spawn(waiter),
    gevent.spawn(waiter),
    gevent.spawn(waiter)
])

除了Event事件外,gevent还提供了AsyncResult事件,它可以在唤醒时传递消息。让我们将上例中的setter和waiter作如下改动:

from gevent.event import AsyncResult
aevt = AsyncResult()

def setter():
    print 'Wait for me'
    gevent.sleep(3)  # 3秒后唤醒所有在evt上等待的协程
    print "Ok, I'm done"
    aevt.set('Hello!')  # 唤醒,并传递消息

def waiter():
    print("I'll wait for you")
    message = aevt.get()  # 等待,并在唤醒时获取消息
    print 'Got wake up message: %s' % message

队列 Queue

队列Queue的概念相信大家都知道,我们可以用它的put和get方法来存取队列中的元素。gevent的队列对象可以让greenlet协程之间安全的访问。运行下面的程序,你会看到3个消费者会分别消费队列中的产品,且消费过的产品不会被另一个消费者再取到:

import gevent
from gevent.queue import Queue

products = Queue()

def consumer(name):
    while not products.empty():
        print '%s got product %s' % (name, products.get())
        gevent.sleep(0)

    print '%s Quit'

def producer():
    for i in xrange(1, 10):
        products.put(i)

gevent.joinall([
    gevent.spawn(producer),
    gevent.spawn(consumer, 'steve'),
    gevent.spawn(consumer, 'john'),
    gevent.spawn(consumer, 'nancy'),
])

put和get方法都是阻塞式的,它们都有非阻塞的版本:put_nowait和get_nowait。如果调用get方法时队列为空,则抛出”gevent.queue.Empty”异常。

信号量

信号量可以用来限制协程并发的个数。它有两个方法,acquire和release。顾名思义,acquire就是获取信号量,而release就是释放。当所有信号量都已被获取,那剩余的协程就只能等待任一协程释放信号量后才能得以运行:

import gevent
from gevent.coros import BoundedSemaphore

sem = BoundedSemaphore(2)

def worker(n):
    sem.acquire()
    print('Worker %i acquired semaphore' % n)
    gevent.sleep(0)
    sem.release()
    print('Worker %i released semaphore' % n)

gevent.joinall([gevent.spawn(worker, i) for i in xrange(0, 6)])

上面的例子中,我们初始化了”BoundedSemaphore”信号量,并将其个数定为2。所以同一个时间,只能有两个worker协程被调度。程序运行后的结果如下:

Worker 0 acquired semaphore
Worker 1 acquired semaphore
Worker 0 released semaphore
Worker 1 released semaphore
Worker 2 acquired semaphore
Worker 3 acquired semaphore
Worker 2 released semaphore
Worker 3 released semaphore
Worker 4 acquired semaphore
Worker 4 released semaphore
Worker 5 acquired semaphore
Worker 5 released semaphore

如果信号量个数为1,那就等同于同步锁。

协程本地变量

同线程类似,协程也有本地变量,也就是只在当前协程内可被访问的变量:

import gevent
from gevent.local import local

data = local()

def f1():
    data.x = 1
    print data.x

def f2():
    try:
        print data.x
    except AttributeError:
        print 'x is not visible'

gevent.joinall([
    gevent.spawn(f1),
    gevent.spawn(f2)
])

通过将变量存放在local对象中,即可将其的作用域限制在当前协程内,当其他协程要访问该变量时,就会抛出异常。不同协程间可以有重名的本地变量,而且互相不影响。因为协程本地变量的实现,就是将其存放在以的”greenlet.getcurrent()”的返回为键值的私有的命名空间内。

实际应用

讲到这里,大家肯定很想看一个gevent的实际应用吧,这里有一个简单的聊天室程序,基于Flask实现,大家可以参考下。

参考链接


基于协程的Python网络库gevent介绍

ubuntu 16.04下安装RTL8192EU(天猫魔盘)

最近需要使用无线网卡,恰好手头有一枚RTL8192EU(天猫魔盘)的无线网卡,使用如下方式安装驱动:

$ sudo apt-get install git linux-headers-generic build-essential dkms

$ sudo apt-get install libelf-dev

$ git clone https://github.com/wangqiang1588/rtl8192eu-linux-driver.git

$ cd rtl8192eu-linux-driver

$ make

$ sudo make install

$ sudo dkms remove rtl8192eu/1.0 --all

$ sudo dkms add .

$ sudo dkms install rtl8192eu/1.0

$ sudo reboot

如果下载代码存在困难,可以从本站下载一份代码拷贝 点击这里下载rtl8192eu-linux-driver

参考链接


Python byte转integer/string

需求:将形如’y\xcc\xa6\xbb’byte字符串转化为integer或者string

方法1 导入struct
import struct
struct.unpack("<L", "y\xcc\xa6\xbb")[0]
方法2 python3.2及以上

byte串采取大端法:

int.from_bytes(b'y\xcc\xa6\xbb', byteorder='big')

若采取小端法,则:

int.from_bytes(b'y\xcc\xa6\xbb', byteorder='little')
方法3 借助十六进制转换

大端法:

s = 'y\xcc\xa6\xbb' 
num = int(s.encode('hex'), 16)

小端法:

int(''.join(reversed(s)).encode('hex'), 16)
方法4 使用array
import array 
integerValue = array.array("I", 'y\xcc\xa6\xbb')[0]

其中I用于表示大端或小端,且使用此方法要注意自己使用的python版本。

方法5 自己写函数实现

如:

sum(ord(c) << (i * 8) for i, c in enumerate('y\xcc\xa6\xbb'[::-1]))

又如:

def bytes2int( tb, order='big'):
    if order == 'big': seq=[0,1,2,3]
    elif order == 'little': seq=[3,2,1,0]
    i = 0
    for j in seq: i = (i<<8)+tb[j]
    return i
字符数组转换成字符串
>>> import array
>>> array.array('B', [17, 24, 121, 1, 12, 222, 34, 76]).tostring() 
'\x11\x18y\x01\x0c\xde"L'

参考链接


树莓派GPIO用法示例(Python)

非阻塞用法示例如下:

import time
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)

btn_input = 4;
LED_output = 17;

# GPIO btn_input set up as input.
GPIO.setup(btn_input, GPIO.IN)
GPIO.setup(LED_output, GPIO.OUT)

# handle the button event
def buttonEventHandler_rising (pin):
    # turn LED on
    GPIO.output(LED_output,True)
    
def buttonEventHandler_falling (pin):
    # turn LED off
    GPIO.output(LED_output,False)

	
GPIO.add_event_detect(btn_input, GPIO.RISING, callback=buttonEventHandler_rising) 
GPIO.add_event_detect(btn_input, GPIO.FALLING, callback=buttonEventHandler_falling)
 
try:  
    while True : time.sleep(0.1)  
finally:
    GPIO.remove_event_detect(btn_input)
    GPIO.cleanup()

参考链接


树莓派Zero W/WH(Raspberry Pi Zero W/WH) GPIO针脚定义

树莓派Zero W,有两款小型号,一款是Raspberry Pi Zero W,另一款是Raspberry Pi Zero WH,两者的区别是一个出厂的时候没有焊接排针,另一款焊接了排针。WWireless的缩写,WHWireless With Head的缩写。

树莓派Zero W/WH(Raspberry Pi Zero W/WH) GPIO针脚定义如下图:
继续阅读树莓派Zero W/WH(Raspberry Pi Zero W/WH) GPIO针脚定义