前段时间,我问了一个类似的问题,“如何将 QEMU 模拟机中的 GPIO 连接到主机中的对象?” 经过一些工作,我找到了一个不完美但令人满意的解决方案。
但是,现在我们有了支持 GPIO 的 virtio,最好使用该解决方案而不是修改过的 mpc8xxx 驱动程序。 之前的方案并不完美且难以维护(我只移植到了Buildroot 2021.02并停止了进一步的维护)。
不幸的是,我没有看到任何可以用来将 GUI 连接到 QEMU 中模拟的机器的主机端 virtio-user-gpio 实现。是否有任何库(最好带有 Python 绑定)可以帮助完成此任务? 我应该从头开始,为 GPIO 设备实现
vhost-user-gpio 的 Rust 实现。我已经创建了我自己的fork,解决方案在分支gpio-python。
我修改了MockGpioDevice 的实现,使其使用简单的 HTTP 传输连接到 JSON RPC 服务器:
use jsonrpc::Client;
use jsonrpc::simple_http::{self, SimpleHttpTransport};
use serde_json::json;
use serde_json::value::to_raw_value;
fn client() -> std::result::Result<Client,simple_http::Error> {
let url = "http://127.0.0.1:8001";
let t = SimpleHttpTransport::builder()
.url(url)?
.build();
Ok(Client::with_transport(t))
}
fn call(cli : &Client, fun : &str, param : serde_json::Value ) -> serde_json::Value
{
let raw_value = Some(to_raw_value(¶m).unwrap());
let request = cli.build_request(fun, raw_value.as_deref());
let response = cli.send_request(request).expect("send_request failed");
let resp2 : serde_json::Value = serde_json::from_str((*response.result.unwrap()).get()).unwrap();
return resp2;
}
处理 GPIO 状态变化的各个函数执行 RPC 调用。例如GPIO引脚的 fn value(&self, gpio: u16) -> Result<u8> {
if self.value_result.is_err() {
return self.value_result;
}
let resp = call(&self.rpc_client,"value",json!([gpio]));
println!("{:?}",resp);
let val : u8 = resp[1].as_u64().unwrap().try_into().unwrap() ;
return Ok(val);
}
fn set_value(&self, gpio: u16, value: u32) -> Result<()> {
info!(
"gpio {} set value to {}",
self.gpio_names[gpio as usize], value
);
if self.set_value_result.is_err() {
return self.set_value_result;
}
let resp = call(&self.rpc_client,"set_value",json!([gpio,value]));
println!("{:?}",resp);
return Ok(());
}
JSON RPC 服务器是使用tinyrpc 在Python 中实现的。它连接到改编自 我的旧解决方案的 Gtk GUI。
对应的@dispatcher.public
def value(n):
return ("OK",gpios[n].val)
@dispatcher.public
def set_value(n,v):
gpios[n].val = v
rpc_server.change_handler(n,v)
return ("OK")
字段val
由GUI中的send_change函数修改:
def send_change(nof_pin, state):
do_irq = (rpc.gpios[nof_pin].val != state)
rpc.gpios[nof_pin].val = state
if do_irq:
with rpc.gpios[nof_pin].wait_both:
rpc.gpios[nof_pin].wait_both.notify_all()
if state == 1:
with rpc.gpios[nof_pin].wait_rise:
rpc.gpios[nof_pin].wait_rise.notify_all()
if state == 0:
with rpc.gpios[nof_pin].wait_fall:
rpc.gpios[nof_pin].wait_fall.notify_all()
该函数还支持使用 wait_for_interrupt 函数处理 GPIO 生成的中断。
def wait_for_interrupt(n):
if gpios[n].irq_type == 1: # RISING
with gpios[n].wait_rise:
gpios[n].wait_rise.wait()
if gpios[n].irq_type == 2: # FALLING
with gpios[n].wait_fall:
gpios[n].wait_fall.wait()
if gpios[n].irq_type == 3: # BOTH
with gpios[n].wait_both:
gpios[n].wait_both.wait()
return ("OK",1)
vhost-device-gpio中对应的function如下:
fn wait_for_interrupt(&self, gpio: u16) -> Result<bool> {
if self.wait_for_irq_result.is_err() {
return self.wait_for_irq_result;
}
let resp = call(&self.rpc_client,"wait_for_interrupt",json!([gpio]));
println!("{:?}",resp);
let val : bool = resp[1].as_u64().unwrap() > 0;
return Ok(val);
}
为了进行测试,我首先通过在安装了以下软件的虚拟环境中运行 python3 gui3.py
来启动 GUI:
tinyrpc
、
gevent
、
werkzeug
和
pgi
。 然后我启动了vhost-device-gpio:
LD_LIBRARY_PATH=/home/emb/libgpiod-2.1/lib/.libs/ ./vhost-device-gpio -s /tmp/gpio.sock -l s1
必须设置 LD_LIBRARY_PATH,因为在我的 Debian/测试机器中,使用了旧的 libgpiod。因此,我不得不在/home/emb/libgpiod-2.1
中编译新版本。当然,
vhost-device-gpio
也需要专门打造:
export PATH_TO_LIBGPIOD=/home/emb/libgpiod-2.1
export SYSTEM_DEPS_LIBGPIOD_NO_PKG_CONFIG=1
export SYSTEM_DEPS_LIBGPIOD_SEARCH_NATIVE="${PATH_TO_LIBGPIOD}/lib/.libs/"
export SYSTEM_DEPS_LIBGPIOD_LIB=gpiod
export SYSTEM_DEPS_LIBGPIOD_INCLUDE="${PATH_TO_LIBGPIOD}/include/"
cargo build --features "mock_gpio"
当 GUI 和 vhost-device-gpio 启动时,我可以使用连接到我的模拟 GPIO 的来宾操作系统来运行 QEMU。为此,我使用了为 qemu-aarch64-virt
平台构建的 Linux 和 Buildroot 2023.11.1。当然,我必须在 Linux 内核配置中启用
CONFIG_GPIO_VIRTIO=m
。要模拟 GPIO 中断,必须对 QEMU 进行修补,如此处所述。 仿真机启动后,就可以加载驱动程序了:
modprobe gpio-virtio
。然后可以使用
gpiomon 0 12
测试中断,使用
gpioget 0 4
读取引脚或使用
gpioset 0 24
写入引脚(当然你可以更改引脚编号)。所描述的解决方案只是概念的证明。错误检测和处理几乎不存在。此外,当客户等待中断时停止仿真可能很困难。不过,我希望这可能是进一步发展的良好起点。