树莓派实时系统下脚本语言的选择(应当使用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相关信息,参考如下的文章:

正是由于对于GC的特殊要求,导致我们在实时系统上能选择的脚本非常有限。

目前的评估来看,貌似Lua差不多成为唯一的选择了。

以下操作在如下系统版本上进行过可行性验证:

$ sudo lsb_release -a
No LSB modules are available.
Distributor ID:	Raspbian
Description:	Raspbian GNU/Linux 9.4 (stretch)
Release:	9.4
Codename:	stretch

具体的使用方法如下(对于Raspberry Pi Zero W如果已经设置成AP模式,建议外接USB有线网卡操作):

# 至少是5.1版本的lua,主要就是虚拟机的GC逻辑
$ sudo apt-get install lua5.1 liblua5.1-dev

# 树莓派 Raspbian GNU/Linux 11 (bullseye) 上只能执行如下命令
# sudo apt-get install lua5.4 liblua5.4-dev

# 网络功能
$ sudo apt-get install lua-socket

$ sudo apt-get install luarocks

# GPIO,SPI,I2C,Serial
$ sudo luarocks install lua-periphery

$ sudo luarocks install luasocket

$ sudo apt-get install libssl-dev

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

$ sudo luarocks install luasec

# zeromq(目前默认 2.2.0),方便多线程通信,lua-periphery缺乏回调函数,导致必须增加一个线程任务
$ sudo apt-get install libzmq-dev

# 树莓派 Raspbian GNU/Linux 11 (bullseye) 上只能执行如下命令
# sudo apt-get install libzmq3-dev

# 修正链接问题,lua-zmq的库路径查找存在问题,我们需要手工链接一下文件
$ sudo ln -s /usr/lib/arm-linux-gnueabihf/libzmq.so /usr/lib/libzmq.so

$ sudo ln -s /usr/lib/arm-linux-gnueabihf/libzmq.a /usr/lib/libzmq.a
 
# 目前安装的版本为 lua-zmq 1.1-1
# https://github.com/Neopallium/lua-zmq
$ sudo luarocks install lua-zmq

# 树莓派 Raspbian GNU/Linux 11 (bullseye) 上只能执行如下命令
# sudo luarocks install lzmq

# 线程相关
$ sudo luarocks install lua-llthreads

$ sudo luarocks install lua-zmq-threads

# 树莓派 Raspbian GNU/Linux 11 (bullseye) 上只能执行如下命令
# sudo luarocks install lua-llthreads2

# TcpServer 
$ sudo luarocks install coxpcall

$ sudo luarocks install copas

# json
$ sudo luarocks install dkjson

# struct
# sudo luarocks install lua-struct
$ sudo luarocks install luastruct

# semaphore
$ sudo luarocks install luaipc

# string.pack/string.unpack support for lua 5.1
# use ' require "pack" '
$ sudo luarocks install lpack

控制GPIO的例子如下:

periphery = require('periphery')

-- GPIO注意,与Python中的GPIO管脚定义不同,Python中的GPIO定义存在一张重定位表,真正的管脚需要查询这张表来确定,比如Python下 GPIO 22 对应真正的管脚是 GPIO 25 (Raspberry Zero W)
-- 查询表参见 https://github.com/Tieske/rpi-gpio/blob/master/lua/RPi_GPIO_Lua_module.c
-- 转换代码参考 https://www.mobibrw.com/?p=14673
--lua 5.1
-- Open GPIO 10 with input direction
err, gpio_in = pcall(periphery.GPIO, 10,"in")
-- Open GPIO 12 with output direction
err, gpio_out = pcall(periphery.GPIO, 12,"out")

value = gpio_in:read()
gpio_out:write(not value)

gpio_in:close()
gpio_out:close()

控制GPIO边沿触发(阻塞等待)的例子如下:

periphery = require('periphery')

-- GPIO注意,与Python中的GPIO管脚定义不同,Python中的GPIO定义存在一张重定位表,真正的管脚需要查询这张表来确定,比如Python下 GPIO 22 对应真正的管脚是 GPIO 25 (Raspberry Zero W)
-- 查询表参见 https://github.com/Tieske/rpi-gpio/blob/master/lua/RPi_GPIO_Lua_module.c
-- 转换代码参考 https://www.mobibrw.com/?p=14673
--lua 5.1
-- Open GPIO 10 with input direction
err, gpio_in = pcall(periphery.GPIO, 10,"in")
-- Open GPIO 12 with output direction
err, gpio_out = pcall(periphery.GPIO, 12,"out")

-- gpio_in.edge = "rising"
gpio_in.edge = "falling"

--lua 5.1 lua: (error object is not a string)
--r = gpio_in:poll(1000) -- 1s
--lua 5.1
err,r = pcall(periphery.GPIO.poll, gpio_in, 1000) -- 1s

--lua 5.1 lua: (error object is not a string)
--v = gpio_in:read()
--lua 5.1
err,v = pcall(periphery.GPIO.poll, gpio_in, read)

gpio_out:write(not v)

gpio_in:close()
gpio_out:close()

控制SPI的例子如下:

periphery = require('periphery')

--lua 5.1
-- Open spidev1.0 with mode 0 and max speed 1MHz
err, spi = pcall(periphery.SPI,"/dev/spidev0.0", 0, 1e6)

#也可以动态创建数据写入
--data_out = {}
--for i= 0, 4 do
--   data_out[i] = 0
--end
data_out = {0xaa, 0xbb, 0xcc, 0xdd}
err, data_in = pcall(spi.transfer,spi,data_out)

print(string.format("shifted out {0x%02x, 0x%02x, 0x%02x, 0x%02x}", unpack(data_out)))
print(string.format("shifted in  {0x%02x, 0x%02x, 0x%02x, 0x%02x}", unpack(data_in)))

spi:close()

使用ZeroMQ的例子(PUB-SUB)如下:

require "zmq"
require "zmq.threads"
  
local recv_worker_code = [[
  local zmq      = require "zmq"
  local zthreads = require "zmq.threads"
  local context  = zthreads.get_parent_ctx()
  local subscriber, err = context:socket(zmq.SUB)
  -- 本地进程内(线程间)传输方式 "inproc://backend"
  subscriber:connect("tcp://localhost:5559")
  subscriber:setopt(zmq.SUBSCRIBE,"")

  while true do
    -- Read envelope with address and message contents
    local contents = subscriber:recv()
    print ("recv:"..contents) -- messages (for testing purposes our own ones) come in perfectly
  end
  subscriber:close()
  return nil
]]

local zmq      = require "zmq"
-- 遍历输出成员变量 for key, value in pairs(zmq) do print(key) end
local zthreads = require "zmq.threads"
local context  = zmq.init(1)

-- Socket to talk to server
publisher, err = context:socket(zmq.PUB)
-- 本地进程内(线程间)传输方式 "inproc://backend"
publisher:bind("tcp://*:5559")

recv_worker = zthreads.runstring(context,recv_worker_code)
recv_worker:start()

print ("HI, this is lua script. We are running now")

--  Initialize random number generator
math.randomseed(os.time())

local idx
for idx=0,99 do
    local workload = math.random() * 10000 + idx
    local msg = string.format("%d", workload)
    -- print(msg)
    publisher:send(msg)
    local socket = require("socket")
    socket.sleep(1)
end

recv_worker:join()

publisher:close()
recv_worker:close()
context:term()

使用ZeroMQ的例子(PAIR-PAIR,线程之间一对一通信,类似信号量)如下:

require "zmq"
require "zmq.threads"
  
local send_worker_code = [[
  local zmq      = require "zmq"
  local zthreads = require "zmq.threads"
  local context  = zthreads.get_parent_ctx()
  local xmitter, err = context:socket(zmq.PAIR)
  -- 本地进程内(线程间)传输方式 "inproc://backend"
  xmitter:connect("inproc://zmq123")
 
  --  Initialize random number generator
  math.randomseed(os.time())
 
  while true do
    local workload = math.random() * 10000
    local msg = string.format("%d", workload)
    xmitter:send(msg)
    local socket = require("socket")
    socket.sleep(1)
  end
  xmitter:close()
  return nil
]]
 
local zmq      = require "zmq"
-- 遍历输出成员变量 for key, value in pairs(zmq) do print(key) end
local zthreads = require "zmq.threads"
local context  = zmq.init(1)
 
receiver, err = context:socket(zmq.PAIR)
-- 本地进程内(线程间)传输方式 "inproc://backend"
receiver:bind("inproc://zmq123")
 
send_worker = zthreads.runstring(context,send_worker_code)
send_worker:start()
 
print ("HI, this is lua script. We are running now")
 
local idx
for idx=0,99 do
    local msg = receiver:recv()
    print("recv:"..msg)
end
 
send_worker:join()
 
receiver:close()
send_worker:close()
context:term()

使用Copas的例子如下:

local socket = require("socket")
local copas = require("copas")

local host = "*" --"127.0.0.1"

-- 注意,如果使用低于1024的端口,需要root否则无法正确创建端口
local port = 8666

local server = socket.bind(host, port)

function echoHandler(skt)
  skt = copas.wrap(skt)
  while true do
    local data = skt:receive()
    if data == "quit" then
      break
    end
    skt:send(data)
  end
end

copas.addserver(server, echoHandler)

print ("HI, this is lua script. We are running now")

copas.loop()

使用CopasZeroMQ结合的例子如下:

require "zmq"
require "zmq.threads"

local send_worker_code = [[
  local zmq      = require "zmq"
  local zthreads = require "zmq.threads"
  local context  = zthreads.get_parent_ctx()
  local xmitter, err = context:socket(zmq.PAIR)
  -- 本地进程内(线程间)传输方式 "inproc://backend"
  xmitter:connect("inproc://zmq123")
 
  --  Initialize random number generator
  math.randomseed(os.time())
 
  while true do
    local workload = math.random() * 10000
    local msg = string.format("%d\n", workload)
    xmitter:send(msg)
    local socket = require("socket")
    socket.select(nil, nil, 1) -- 传递0.1可以达到毫秒级别
  end
  xmitter:close()
]]

local copas = require("copas")
local socket = require("socket") 

local zmq      = require "zmq"
-- 遍历输出成员变量 for key, value in pairs(zmq) do print(key) end
local zthreads = require "zmq.threads"
local context  = zmq.init(1)
 
local receiver, err = context:socket(zmq.PAIR)
-- 本地进程内(线程间)传输方式 "inproc://backend"
receiver:bind("inproc://zmq123")

-- 如果runstring中传入的context为nil 则线程内部需要自己调用zmq.init(1)初始化自己的zmq上下文,但是如果传入nil无法使用zmq.PAIR模式
send_worker = zthreads.runstring(context,send_worker_code)
send_worker:start()

local host = "*" --"127.0.0.1"

-- 注意,如果使用低于1024的端口,需要root否则无法正确创建端口
local port = 8666

local server = socket.bind(host, port)

local clients = {}

function copasHandler(skt)
  skt = copas.wrap(skt)
  table.insert(clients,skt)
  local idx = table.getn(clients)
  while true do
    local data = skt:receive()
    if data == "quit" then
      break
    end
  end
  table.remove(clients,idx) 
end

copas.addthread(function()
   while true do
      local msg = receiver:recv(zmq.NOBLOCK)
      if msg then
         for _,v in pairs(clients) do  
            v:send(msg) 
         end
      end  
      copas.sleep(0.1) -- 0.1 second interval
   end
end)

copas.addserver(server, copasHandler)

print ("HI, this is lua script. We are running now")

copas.loop()

send_worker:join()
 
receiver:close()
send_worker:close()
context:term()

使用dkjson的例子如下:

local dkjson = require("dkjson")  
  
--lua对象到字符串  
local obj = {  
    id = 1,  
    name = "zhangsan",  
    age = nil,  
    is_male = false,  
    hobby = {"film", "music", "read"}  
}  
  
local str = dkjson.encode(obj, {indent = true})  
print(str.."\n")  
  
--字符串到lua对象  
str = '{"hobby":["film","music","read"],"is_male":false,"name":"zhangsan","id":1,"age":null}'  
local obj, pos, err = dkjson.decode(str, 1, nil)  
  
print(obj.hobby[1].."\n")

参考链接


发布者

《树莓派实时系统下脚本语言的选择(应当使用Lua而不是Python)》上有2条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注