From 60f1a1a5545bc2f7bbf95ab2ef3a20317f3507fa Mon Sep 17 00:00:00 2001 From: Piotr Tworek Date: Mon, 10 Feb 2020 22:27:02 +0100 Subject: [PATCH] os: Add VirtIO input driver. This component can service Qemu VirtIO mouse, keyboard and tablet devices. The implementation is based on VirtIO 1.1 device spec. Its described in section 5.8 "Input Device". Issue #4282 --- .../recipes/src/virtio_input_drv/content.mk | 2 + repos/os/recipes/src/virtio_input_drv/hash | 1 + .../os/recipes/src/virtio_input_drv/used_apis | 5 + repos/os/src/drivers/input/virtio/README | 12 + repos/os/src/drivers/input/virtio/component.h | 458 ++++++++++++++++++ .../src/drivers/input/virtio/mmio_device.cc | 44 ++ .../drivers/input/virtio/spec/arm/target.mk | 3 + .../input/virtio/spec/arm_64/target.mk | 3 + .../src/drivers/input/virtio/target_mmio.inc | 6 + 9 files changed, 534 insertions(+) create mode 100644 repos/os/recipes/src/virtio_input_drv/content.mk create mode 100644 repos/os/recipes/src/virtio_input_drv/hash create mode 100644 repos/os/recipes/src/virtio_input_drv/used_apis create mode 100644 repos/os/src/drivers/input/virtio/README create mode 100644 repos/os/src/drivers/input/virtio/component.h create mode 100644 repos/os/src/drivers/input/virtio/mmio_device.cc create mode 100644 repos/os/src/drivers/input/virtio/spec/arm/target.mk create mode 100644 repos/os/src/drivers/input/virtio/spec/arm_64/target.mk create mode 100644 repos/os/src/drivers/input/virtio/target_mmio.inc diff --git a/repos/os/recipes/src/virtio_input_drv/content.mk b/repos/os/recipes/src/virtio_input_drv/content.mk new file mode 100644 index 0000000000..af681f7e1f --- /dev/null +++ b/repos/os/recipes/src/virtio_input_drv/content.mk @@ -0,0 +1,2 @@ +SRC_DIR = src/drivers/input/virtio +include $(GENODE_DIR)/repos/base/recipes/src/content.inc diff --git a/repos/os/recipes/src/virtio_input_drv/hash b/repos/os/recipes/src/virtio_input_drv/hash new file mode 100644 index 0000000000..471dbb0a71 --- /dev/null +++ b/repos/os/recipes/src/virtio_input_drv/hash @@ -0,0 +1 @@ +2021-10-05 1388c0c89ec4e1cf3fecdd595eb8bf358c4310e8 diff --git a/repos/os/recipes/src/virtio_input_drv/used_apis b/repos/os/recipes/src/virtio_input_drv/used_apis new file mode 100644 index 0000000000..95cde362d9 --- /dev/null +++ b/repos/os/recipes/src/virtio_input_drv/used_apis @@ -0,0 +1,5 @@ +base +os +virtio +event_session +platform_session diff --git a/repos/os/src/drivers/input/virtio/README b/repos/os/src/drivers/input/virtio/README new file mode 100644 index 0000000000..d7741f89da --- /dev/null +++ b/repos/os/src/drivers/input/virtio/README @@ -0,0 +1,12 @@ +The VirtIO input driver makes it possible to read input events from +Qemu keyboard, mouse, or tablet devices and feed them to Genode Event +session. + +By default the driver can attach to any supported device type. This +may be changed by specifying "match_product" configuration key. Allowed +values for this setting are: keyboard, mouse, tablet, any (default). + +For example, to make sure specific instance of this driver attaches only +to VirtIO keyboard device it can be configured as: + +! diff --git a/repos/os/src/drivers/input/virtio/component.h b/repos/os/src/drivers/input/virtio/component.h new file mode 100644 index 0000000000..c06512b373 --- /dev/null +++ b/repos/os/src/drivers/input/virtio/component.h @@ -0,0 +1,458 @@ +/* + * \brief VirtIO based input driver + * \author Piotr Tworek + * \date 2020-02-01 + */ + +/* + * Copyright (C) 2020 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +/* Genode */ +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace Virtio_input { + using namespace Genode; + struct Main; + class Driver; +} + + +class Virtio_input::Driver +{ + public: + + struct Device_not_found : Exception { }; + struct Device_init_failed : Exception { }; + struct Queue_init_failed : Exception { }; + + private: + + enum { VENDOR_QEMU = 0x0627 }; + + enum Product { + Any = 0x0, + Keyboard = 0x1, + Mouse = 0x2, + Tablet = 0x3, + }; + + enum Config : uint8_t { + SelectID = 0x00, + SelectSubID = 0x01, + Data_size = 0x02, + Data = 0x08, + }; + + enum Config_id : uint8_t { + Name = 0x01, + Serial = 0x02, + Device_id = 0x03, + Prop_bits = 0x10, + Event_bits = 0x11, + Abs_info = 0x12, + }; + + struct Device_id { + uint16_t bus_type = 0; + uint16_t vendor = 0; + uint16_t product = 0; + uint16_t version = 0; + }; + + struct Features : Register<64> { struct VERSION_1 : Bitfield<32, 1> { }; }; + + enum Queue_id : uint16_t { EVENTS_VQ = 0, STATUS_VQ = 1 }; + + struct Abs_config { + struct { + uint32_t min; + uint32_t max; + } x, y; + uint32_t width; + uint32_t height; + }; + + struct Event { + enum Type { + Syn = 0x00, + Key = 0x01, + Rel = 0x02, + Abs = 0x03, + Rep = 0x14, + }; + enum Code { + Rel_x = 0x00, + Rel_y = 0x01, + Rel_wheel = 0x08, + Abs_x = 0x00, + Abs_y = 0x01, + }; + uint16_t type; + uint16_t code; + uint32_t value; + + bool operator == (Event const &other) const { + return other.type == this->type && + other.code == this->code && + other.value == this->value; + } + }; + + struct Events_queue_traits { + static const bool device_write_only = true; + static const bool has_data_payload = false; + }; + + struct Status_queue_traits { + static const bool device_write_only = false; + static const bool has_data_payload = false; + }; + + enum { QUEUE_SIZE = 64, QUEUE_ELM_SIZE = sizeof(Event) }; + + typedef Virtio::Queue Events_virtqueue; + typedef Virtio::Queue Status_virtqueue; + + Driver(Driver const &); + Driver &operator = (Driver const &); + + Env &_env; + ::Event::Connection _event_session { _env }; + Virtio::Device &_device; + Event _last_sent_key_event { 0, 0, 0 }; + Input::Relative_motion _rel_motion { 0, 0 }; + Input::Absolute_motion _abs_motion { -1, -1 }; + Abs_config _abs_config { { 0, 0 }, { 0, 0 }, 0, 0 }; + Signal_handler _irq_handler {_env.ep(), *this, &Driver::_handle_irq}; + Events_virtqueue _events_vq { _env.ram(), _env.rm(), + QUEUE_SIZE, QUEUE_ELM_SIZE }; + Status_virtqueue _status_vq { _env.ram(), _env.rm(), + QUEUE_SIZE, QUEUE_ELM_SIZE }; + + + void _handle_event(::Event::Session_client::Batch &batch, const Event &evt) + { + switch (evt.type) { + + case Event::Type::Syn: + { + if (_rel_motion.x != 0 || _rel_motion.y != 0) { + batch.submit(_rel_motion); + _rel_motion = Input::Relative_motion{0, 0}; + } + + if (_abs_motion.x >= 0 || _abs_motion.y >= 0) { + batch.submit(_abs_motion); + _abs_motion = Input::Absolute_motion{-1, -1}; + } + + break; + } + + case Event::Type::Rel: + { + switch (evt.code) { + case Event::Code::Rel_x: _rel_motion.x = evt.value; break; + case Event::Code::Rel_y: _rel_motion.y = evt.value; break; + case Event::Code::Rel_wheel: + batch.submit(Input::Wheel{0, (int)evt.value}); + break; + default: + warning("Unhandled relative event code: ", Hex(evt.code)); + break; + } + break; + } + + case Event::Type::Key: + { + /* filter out auto-repeat keypress events */ + if (_last_sent_key_event == evt) + break; + + /* Genode keyboard event codes mirror linux evdev ones */ + Input::Keycode keycode = static_cast(evt.code); + + /* + * Some key events apparently don't send both press and + * release values. Fake both press and release to make + * nitpicker happy. + */ + if ((keycode == Input::BTN_GEAR_UP || + keycode == Input::BTN_GEAR_DOWN) && !evt.value) + batch.submit(Input::Press{keycode}); + + switch (evt.value) { + case 0: batch.submit(Input::Release{keycode}); break; + case 1: batch.submit(Input::Press{keycode}); break; + default: + warning("Unhandled key event value: ", evt.value); + break; + } + + _last_sent_key_event = evt; + break; + } + + case Event::Type::Abs: + { + switch (evt.code) { + case Event::Code::Abs_x: + _abs_motion.x = (_abs_config.width * evt.value / _abs_config.x.max); + _abs_motion.y = max(0, _abs_motion.y); + break; + case Event::Code::Abs_y: + _abs_motion.x = max(0, _abs_motion.x); + _abs_motion.y = (_abs_config.height * evt.value / _abs_config.y.max); + break; + default: + warning("Unhandled absolute event code: ", Hex(evt.code)); + break; + } + break; + } + + default: + warning("Unhandled event type: ", Hex(evt.type)); + break; + } + } + + + static Product _match_product(Xml_node const &config) + { + auto product_string = config.attribute_value("match_product", String<10>("any")); + + if (product_string == "keyboard") { + return Product::Keyboard; + } else if (product_string == "mouse") { + return Product::Mouse; + } else if (product_string == "tablet") { + return Product::Tablet; + } else if (product_string == "any") { + return Product::Any; + } + + error("Invalid product name: ", product_string); + + throw Device_init_failed(); + } + + + static size_t _cfg_select(Virtio::Device &device, Config_id sel, uint8_t subsel) + { + device.write_config(Config::SelectID, Virtio::Device::ACCESS_8BIT, sel); + device.write_config(Config::SelectSubID, Virtio::Device::ACCESS_8BIT, subsel); + return device.read_config(Config::Data_size, Virtio::Device::ACCESS_8BIT); + } + + + static Abs_config _read_abs_config(Virtio::Device &device, + Xml_node const &config) + { + Abs_config cfg { {0, ~0U}, {0, ~0U}, 0, 0}; + + auto size = _cfg_select(device, Config_id::Abs_info, Event::Code::Abs_x); + if (size >= sizeof(cfg.x)) { + cfg.x.min = device.read_config(Config::Data, Virtio::Device::ACCESS_32BIT); + cfg.x.max = device.read_config(Config::Data + 4, Virtio::Device::ACCESS_32BIT); + } + + size = _cfg_select(device, Config_id::Abs_info, Event::Code::Abs_y); + if (size >= sizeof(cfg.y)) { + cfg.y.min = device.read_config(Config::Data, Virtio::Device::ACCESS_32BIT); + cfg.y.max = device.read_config(Config::Data + 4, Virtio::Device::ACCESS_32BIT); + } + + cfg.width = config.attribute_value("width", cfg.x.max); + cfg.height = config.attribute_value("height", cfg.y.max); + + return cfg; + } + + + static struct Device_id _read_device_id(Virtio::Device &device) + { + struct Device_id device_id; + auto size = _cfg_select(device, Config_id::Device_id, 0); + + if (size != sizeof(device_id)) { + error("Invalid VirtIO input device ID size!"); + throw Device_init_failed(); + } + + device_id.bus_type = device.read_config(Config::Data + 0, Virtio::Device::ACCESS_16BIT); + device_id.vendor = device.read_config(Config::Data + 2, Virtio::Device::ACCESS_16BIT); + device_id.product = device.read_config(Config::Data + 4, Virtio::Device::ACCESS_16BIT); + device_id.version = device.read_config(Config::Data + 6, Virtio::Device::ACCESS_16BIT); + + return device_id; + } + + + template + static String _read_device_name(Virtio::Device &device) + { + auto size = _cfg_select(device, Config_id::Name, 0); + size = min(size, SZ); + + char buf[SZ]; + memset(buf, 0, sizeof(buf)); + + for (unsigned i = 0; i < size; ++i) + buf[i] = device.read_config(Config::Data + i, Virtio::Device::ACCESS_8BIT); + + return String(buf); + } + + + static bool _probe_device(Virtio::Device &device, Product match_product) + { + using Status = Virtio::Device::Status; + + if (!device.set_status(Status::RESET)) { + warning("Failed to reset the device!"); + return false; + } + + if (!device.set_status(Status::ACKNOWLEDGE)) { + warning("Failed to acknowledge the device!"); + return false; + } + + const auto dev_id = _read_device_id(device); + if (dev_id.vendor != VENDOR_QEMU) { + warning("Unsupported VirtIO input device vendor: ", Hex(dev_id.vendor)); + } + + if (match_product != Product::Any && match_product != dev_id.product) + return false; + + return true; + } + + + static bool _init_features(Virtio::Device &device) + { + using Status = Virtio::Device::Status; + + const uint32_t low = device.get_features(0); + const uint32_t high = device.get_features(1); + const Features::access_t device_features = ((uint64_t)high << 32) | low; + + Features::access_t driver_features = 0; + + if (!Features::VERSION_1::get(device_features)) { + warning("Unsupprted VirtIO device version!"); + return false; + } + Features::VERSION_1::set(driver_features); + + device.set_features(0, (uint32_t)driver_features); + device.set_features(1, (uint32_t)(driver_features >> 32)); + + if (!device.set_status(Status::FEATURES_OK)) { + device.set_status(Status::FAILED); + error("Device feature negotiation failed!"); + return false; + } + + return true; + } + + + void _init_driver(Xml_node const &config) + { + using Status = Virtio::Device::Status; + + const auto product = _match_product(config); + + if (_probe_device(_device, product) && _init_features(_device)) { + + if (!_device.set_status(Status::DRIVER)) { + _device.set_status(Status::FAILED); + warning("Device initialization failed!"); + throw Device_init_failed(); + } + + return; + } else if (!_device.set_status(Status::RESET)) { + warning("Failed to reset the device!"); + } + + warning("No suitable VirtIO input device found!"); + throw Device_not_found(); + } + + + void _setup_virtio_queues() + { + if (!_device.configure_queue(EVENTS_VQ, _events_vq.description())) { + error("Failed to initialize events VirtIO queue!"); + throw Queue_init_failed(); + } + + if (!_device.configure_queue(STATUS_VQ, _status_vq.description())) { + error("Failed to initialize status VirtIO queue!"); + throw Queue_init_failed(); + } + + using Status = Virtio::Device::Status; + if (!_device.set_status(Status::DRIVER_OK)) { + _device.set_status(Status::FAILED); + error("Failed to initialize VirtIO queues!"); + throw Queue_init_failed(); + } + } + + + void _handle_irq() + { + enum { IRQ_USED_RING_UPDATE = 1, IRQ_CONFIG_CHANGE = 2 }; + + const uint32_t reasons = _device.read_isr(); + + if (reasons & IRQ_USED_RING_UPDATE) { + _event_session.with_batch([&] (::Event::Session_client::Batch &batch) { + while (_events_vq.has_used_buffers()) + _handle_event(batch, _events_vq.read_data()); + }); + + /* Reclaim all buffers processed by the device. */ + if (_status_vq.has_used_buffers()) + _status_vq.ack_all_transfers(); + } + + _device.irq_ack(); + } + + + public: + + Driver(Env &env, + Virtio::Device &device, + Xml_node const &config) + : + _env(env), _device(device) + { + _init_driver(config); + _abs_config = _read_abs_config(_device, config); + _setup_virtio_queues(); + _device.irq_sigh(_irq_handler); + _device.irq_ack(); + log("Using \"", _read_device_name<32>(_device), "\" device."); + } + +}; diff --git a/repos/os/src/drivers/input/virtio/mmio_device.cc b/repos/os/src/drivers/input/virtio/mmio_device.cc new file mode 100644 index 0000000000..641d98aef8 --- /dev/null +++ b/repos/os/src/drivers/input/virtio/mmio_device.cc @@ -0,0 +1,44 @@ +/* + * \brief VirtIO MMIO input driver + * \author Piotr Tworek + * \date 2020-02-14 + */ + +/* + * Copyright (C) 2020 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#include +#include +#include + +#include "component.h" + +namespace Virtio_mmio_input { + using namespace Genode; + struct Main; +} + + +struct Virtio_mmio_input::Main +{ + Env &env; + Platform::Connection platform { env }; + Platform::Device platform_device { platform, { "input" } }; + Virtio::Device virtio_device { platform_device }; + Attached_rom_dataspace config { env, "config" }; + Virtio_input::Driver driver { env, virtio_device, config.xml() }; + + Main(Env &env) + try : env(env) { log("--- VirtIO MMIO input driver started ---"); } + catch (...) { env.parent().exit(-1); } +}; + + +void Component::construct(Genode::Env &env) +{ + static Virtio_mmio_input::Main main(env); +} diff --git a/repos/os/src/drivers/input/virtio/spec/arm/target.mk b/repos/os/src/drivers/input/virtio/spec/arm/target.mk new file mode 100644 index 0000000000..89090dc1c3 --- /dev/null +++ b/repos/os/src/drivers/input/virtio/spec/arm/target.mk @@ -0,0 +1,3 @@ +REQUIRES = arm + +include $(REP_DIR)/src/drivers/input/virtio/target_mmio.inc diff --git a/repos/os/src/drivers/input/virtio/spec/arm_64/target.mk b/repos/os/src/drivers/input/virtio/spec/arm_64/target.mk new file mode 100644 index 0000000000..7443d832aa --- /dev/null +++ b/repos/os/src/drivers/input/virtio/spec/arm_64/target.mk @@ -0,0 +1,3 @@ +REQUIRES = arm_64 + +include $(REP_DIR)/src/drivers/input/virtio/target_mmio.inc diff --git a/repos/os/src/drivers/input/virtio/target_mmio.inc b/repos/os/src/drivers/input/virtio/target_mmio.inc new file mode 100644 index 0000000000..19c0408141 --- /dev/null +++ b/repos/os/src/drivers/input/virtio/target_mmio.inc @@ -0,0 +1,6 @@ +TARGET = virtio_mmio_input_drv +SRC_CC = mmio_device.cc +LIBS = base +INC_DIR = $(REP_DIR)/src/drivers/input/virtio + +vpath % $(REP_DIR)/src/drivers/input/virtio