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