From patchwork Fri Mar 11 16:24:37 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Vincent Whitchurch X-Patchwork-Id: 550598 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 03D43C43217 for ; Fri, 11 Mar 2022 16:25:37 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350269AbiCKQ0h (ORCPT ); Fri, 11 Mar 2022 11:26:37 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:37626 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350313AbiCKQ0d (ORCPT ); Fri, 11 Mar 2022 11:26:33 -0500 Received: from smtp1.axis.com (smtp1.axis.com [195.60.68.17]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 61940117C9E; Fri, 11 Mar 2022 08:24:50 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015895; x=1678551895; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=rpTqoknbDOn72Q+UH1BdHCLF3yJZId72jHd8FypIw5U=; b=KcTKg2XaTrn9jOPkhoLZo2g8hqlxgAXxCaFNkEUNe/XZTLF/5fsg9L3y OdLoqHasFsEuGvrL3LfvL090oX71yeOWW3ecNOCg5CRf+I4/teuL4zz8q zMRpnGKbSJm78pKxDDIf8lPxKmJ7h5A7ZoY7fCl1J8428NVg2XHRtx3IP HamRXuPpMPfjpfU5el/1ezL+8usJUFiYcUOUkQq50sZPpsE5RhVzmuCtb GlPBe3U083ksEsKd3lktBBvV7a22ilYu8l3sbdXHFS1b3dVyUgpK1PX/f P6feCGwcFEvaubxiuyGKtPJibVVktt5rCamgOBh80lBzcGZwrjj9VfM5S Q==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 02/10] roadtest: add C backend Date: Fri, 11 Mar 2022 17:24:37 +0100 Message-ID: <20220311162445.346685-3-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: devicetree@vger.kernel.org Add the C parts of the roadtest framework. This uses QEMU's libvhost-user to implement the device side of virtio-user and virtio-gpio and bridge them to the Python portions of the backend. The C backend is also responsible for starting UML after the virtio device implementations are initialized. Signed-off-by: Vincent Whitchurch --- tools/testing/roadtest/src/backend.c | 884 +++++++++++++++++++++++++++ 1 file changed, 884 insertions(+) create mode 100644 tools/testing/roadtest/src/backend.c diff --git a/tools/testing/roadtest/src/backend.c b/tools/testing/roadtest/src/backend.c new file mode 100644 index 000000000000..d5ac08b20fd9 --- /dev/null +++ b/tools/testing/roadtest/src/backend.c @@ -0,0 +1,884 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Copyright Axis Communications AB + +#define PY_SSIZE_T_CLEAN +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libvhost-user.h" + +enum watch_type { + LISTEN, + SOCKET_WATCH, + VU_WATCH, +}; + +struct watch { + VuDev *dev; + enum watch_type type; + int fd; + void *func; + void *data; + struct list_head list; +}; + +struct vhost_user_i2c { + VuDev dev; + FILE *control; +}; + +struct vhost_user_gpio { + VuDev dev; + FILE *control; + VuVirtqElement *irq_elements[64]; +}; + +#define dbg(...) \ + do { \ + if (0) { \ + fprintf(stderr, __VA_ARGS__); \ + } \ + } while (0) + +static LIST_HEAD(watches); + +static int epfd; + +static PyObject *py_i2c_read, *py_i2c_write, *py_process_control; +static PyObject *py_gpio_set_irq_type, *py_gpio_unmask; + +static const char *opt_main_script; +static char *opt_gpio_socket; +static char *opt_i2c_socket; + +static struct vhost_user_gpio gpio; +static struct vhost_user_i2c i2c; + +static void dump_iov(const char *what, struct iovec *iovec, unsigned int count) +{ + int i; + + dbg("dumping %s with count %u\n", what, count); + + for (i = 0; i < count; i++) { + struct iovec *iov = &iovec[0]; + + dbg("i %d base %p len %zu\n", i, iov->iov_base, iov->iov_len); + } +} + +static bool i2c_read(struct vhost_user_i2c *vi, uint16_t addr, void *data, + size_t len) +{ + PyObject *pArgs, *pValue; + + dbg("i2c read addr %#x len %zu\n", addr, len); + + pArgs = PyTuple_New(1); + pValue = PyLong_FromLong(len); + PyTuple_SetItem(pArgs, 0, pValue); + + pValue = PyObject_CallObject(py_i2c_read, pArgs); + Py_DECREF(pArgs); + if (!pValue) { + PyErr_Print(); + return false; + } + + unsigned char *buffer; + Py_ssize_t length; + + if (PyBytes_AsStringAndSize(pValue, (char **)&buffer, &length) < 0) { + PyErr_Print(); + errx(1, "invalid result from i2c.read()"); + } + if (length != len) { + errx(1, + "unexpected length from i2c.read(), expected %zu, got %zu", + len, length); + } + + memcpy(data, buffer, len); + + return true; +} + +static bool i2c_write(struct vhost_user_i2c *vi, uint16_t addr, + const void *data, size_t len) +{ + PyObject *pArgs, *pValue; + + dbg("i2c write addr %#x len %zu\n", addr, len); + + pArgs = PyTuple_New(1); + pValue = PyBytes_FromStringAndSize(data, len); + PyTuple_SetItem(pArgs, 0, pValue); + + pValue = PyObject_CallObject(py_i2c_write, pArgs); + Py_DECREF(pArgs); + if (!pValue) { + PyErr_Print(); + return false; + } + + return true; +} + +static void gpio_send_irq_response(struct vhost_user_gpio *gpio, + unsigned int pin, unsigned int status); + +static PyObject *cbackend_trigger_gpio_irq(PyObject *self, PyObject *args) +{ + unsigned int pin; + + if (!PyArg_ParseTuple(args, "I", &pin)) + return NULL; + + dbg("trigger gpio %u irq\n", pin); + + gpio_send_irq_response(&gpio, pin, VIRTIO_GPIO_IRQ_STATUS_VALID); + + Py_RETURN_NONE; +} + +static PyMethodDef EmbMethods[] = { + { "trigger_gpio_irq", cbackend_trigger_gpio_irq, METH_VARARGS, + "Return the number of arguments received by the process." }, + { NULL, NULL, 0, NULL } +}; + +static PyModuleDef EmbModule = { PyModuleDef_HEAD_INIT, + "cbackend", + NULL, + -1, + EmbMethods, + NULL, + NULL, + NULL, + NULL }; + +static PyObject *PyInit_cbackend(void) +{ + return PyModule_Create(&EmbModule); +} + +static void init_python_i2c(PyObject *backend) +{ + PyObject *i2c = PyObject_GetAttrString(backend, "i2c"); + + if (!i2c) { + PyErr_Print(); + errx(1, "Error getting backend.i2c"); + } + + py_i2c_read = PyObject_GetAttrString(i2c, "read"); + if (!py_i2c_read) { + PyErr_Print(); + errx(1, "Error getting i2c.read"); + } + + py_i2c_write = PyObject_GetAttrString(i2c, "write"); + if (!py_i2c_write) { + PyErr_Print(); + errx(1, "Error getting i2c.write"); + } +} + +static void init_python_gpio(PyObject *backend) +{ + PyObject *gpio = PyObject_GetAttrString(backend, "gpio"); + + if (!gpio) { + PyErr_Print(); + errx(1, "error getting backend.gpio"); + } + + py_gpio_set_irq_type = PyObject_GetAttrString(gpio, "set_irq_type"); + if (!py_gpio_set_irq_type) { + PyErr_Print(); + errx(1, "error getting gpio.set_irq_type"); + } + + py_gpio_unmask = PyObject_GetAttrString(gpio, "unmask"); + if (!py_gpio_unmask) { + PyErr_Print(); + errx(1, "error getting gpio.unmask"); + } +} + +static void init_python(void) +{ + PyObject *mainmod, *backend; + FILE *file; + + PyImport_AppendInittab("cbackend", &PyInit_cbackend); + + Py_Initialize(); + + file = fopen(opt_main_script, "r"); + if (!file) + err(1, "open %s", opt_main_script); + + if (PyRun_SimpleFile(file, "main.py") < 0) { + PyErr_Print(); + errx(1, "error running %s", opt_main_script); + } + fclose(file); + + mainmod = PyImport_AddModule("__main__"); + if (!mainmod) { + PyErr_Print(); + errx(1, "error getting __main__"); + } + + backend = PyObject_GetAttrString(mainmod, "backend"); + if (!backend) { + PyErr_Print(); + errx(1, "error getting backend"); + } + + py_process_control = PyObject_GetAttrString(backend, "process_control"); + if (!py_process_control) { + PyErr_Print(); + errx(1, "error getting backend.process_control"); + } + + init_python_i2c(backend); + init_python_gpio(backend); +} + +static void i2c_handle_cmdq(VuDev *dev, int qidx) +{ + struct vhost_user_i2c *vi = + container_of(dev, struct vhost_user_i2c, dev); + VuVirtq *vq = vu_get_queue(dev, qidx); + VuVirtqElement *elem; + + for (;;) { + struct virtio_i2c_out_hdr *hdr; + struct iovec *resultv; + size_t used = 0; + bool ok = true; + + elem = vu_queue_pop(dev, vq, sizeof(VuVirtqElement)); + if (!elem) + break; + + dbg("elem %p index %u out_num %u in_num %u\n", elem, + elem->index, elem->out_num, elem->in_num); + dump_iov("out", elem->out_sg, elem->out_num); + dump_iov("in", elem->in_sg, elem->in_num); + + assert(elem->out_sg[0].iov_len == sizeof(*hdr)); + hdr = elem->out_sg[0].iov_base; + + if (elem->out_num == 2 && elem->in_num == 1) { + struct iovec *data = &elem->out_sg[1]; + + ok = i2c_write(vi, hdr->addr, data->iov_base, + data->iov_len); + resultv = &elem->in_sg[0]; + } else if (elem->out_num == 1 && elem->in_num == 2) { + struct iovec *data = &elem->in_sg[0]; + + ok = i2c_read(vi, hdr->addr, data->iov_base, + data->iov_len); + resultv = &elem->in_sg[1]; + used += data->iov_len; + } else { + assert(false); + } + + struct virtio_i2c_in_hdr *inhdr = resultv->iov_base; + + inhdr->status = ok ? VIRTIO_I2C_MSG_OK : VIRTIO_I2C_MSG_ERR; + + used += sizeof(*inhdr); + vu_queue_push(dev, vq, elem, used); + free(elem); + } + + vu_queue_notify(&vi->dev, vq); +} + +static void i2c_queue_set_started(VuDev *dev, int qidx, bool started) +{ + VuVirtq *vq = vu_get_queue(dev, qidx); + + dbg("queue started %d:%d\n", qidx, started); + + vu_set_queue_handler(dev, vq, started ? i2c_handle_cmdq : NULL); +} + +static bool i2cquit; +static bool gpioquit; + +static void remove_watch(VuDev *dev, int fd); + +static int i2c_process_msg(VuDev *dev, VhostUserMsg *vmsg, int *do_reply) +{ + if (vmsg->request == VHOST_USER_NONE) { + dbg("i2c disconnect"); + remove_watch(dev, -1); + i2cquit = true; + return true; + } + return false; +} +static int gpio_process_msg(VuDev *dev, VhostUserMsg *vmsg, int *do_reply) +{ + if (vmsg->request == VHOST_USER_NONE) { + dbg("gpio disconnect"); + remove_watch(dev, -1); + gpioquit = true; + return true; + } + return false; +} + +static uint64_t i2c_get_features(VuDev *dev) +{ + return 1ull << VIRTIO_I2C_F_ZERO_LENGTH_REQUEST; +} + +static const VuDevIface i2c_iface = { + .get_features = i2c_get_features, + .queue_set_started = i2c_queue_set_started, + .process_msg = i2c_process_msg, +}; + +static void gpio_send_irq_response(struct vhost_user_gpio *gpio, + unsigned int pin, unsigned int status) +{ + assert(pin < ARRAY_SIZE(gpio->irq_elements)); + + VuVirtqElement *elem = gpio->irq_elements[pin]; + VuVirtq *vq = vu_get_queue(&gpio->dev, 1); + + if (!elem) { + dbg("no irq buf for pin %d\n", pin); + assert(status != VIRTIO_GPIO_IRQ_STATUS_VALID); + return; + } + + struct virtio_gpio_irq_response *resp; + + assert(elem->out_num == 1); + assert(elem->in_sg[0].iov_len == sizeof(*resp)); + + resp = elem->in_sg[0].iov_base; + resp->status = status; + + vu_queue_push(&gpio->dev, vq, elem, sizeof(*resp)); + gpio->irq_elements[pin] = NULL; + free(elem); + + vu_queue_notify(&gpio->dev, vq); +} + +static void gpio_set_irq_type(struct vhost_user_gpio *gpio, unsigned int pin, + unsigned int type) +{ + PyObject *pArgs, *pValue; + + pArgs = PyTuple_New(2); + pValue = PyLong_FromLong(pin); + PyTuple_SetItem(pArgs, 0, pValue); + + pValue = PyLong_FromLong(type); + PyTuple_SetItem(pArgs, 1, pValue); + + pValue = PyObject_CallObject(py_gpio_set_irq_type, pArgs); + if (!pValue) { + PyErr_Print(); + errx(1, "error from gpio.set_irq_type()"); + } + Py_DECREF(pArgs); + + if (type == VIRTIO_GPIO_IRQ_TYPE_NONE) { + gpio_send_irq_response(gpio, pin, + VIRTIO_GPIO_IRQ_STATUS_INVALID); + } +} + +static void gpio_unmask(struct vhost_user_gpio *vi, unsigned int gpio) +{ + PyObject *pArgs, *pValue; + + pArgs = PyTuple_New(1); + pValue = PyLong_FromLong(gpio); + PyTuple_SetItem(pArgs, 0, pValue); + + pValue = PyObject_CallObject(py_gpio_unmask, pArgs); + if (!pValue) { + PyErr_Print(); + errx(1, "error from gpio.unmask()"); + } + Py_DECREF(pArgs); +} + +static void gpio_handle_cmdq(VuDev *dev, int qidx) +{ + struct vhost_user_gpio *vi = + container_of(dev, struct vhost_user_gpio, dev); + VuVirtq *vq = vu_get_queue(dev, qidx); + VuVirtqElement *elem; + + while (1) { + struct virtio_gpio_request *req; + struct virtio_gpio_response *resp; + + elem = vu_queue_pop(dev, vq, sizeof(VuVirtqElement)); + if (!elem) + break; + + dbg("elem %p index %u out_num %u in_num %u\n", elem, + elem->index, elem->out_num, elem->in_num); + + dump_iov("out", elem->out_sg, elem->out_num); + dump_iov("in", elem->in_sg, elem->in_num); + + assert(elem->out_num == 1); + assert(elem->in_num == 1); + + assert(elem->out_sg[0].iov_len == sizeof(*req)); + assert(elem->in_sg[0].iov_len == sizeof(*resp)); + + req = elem->out_sg[0].iov_base; + resp = elem->in_sg[0].iov_base; + + dbg("req type %#x gpio %#x value %#x\n", req->type, req->gpio, + req->value); + + switch (req->type) { + case VIRTIO_GPIO_MSG_IRQ_TYPE: + gpio_set_irq_type(vi, req->gpio, req->value); + break; + default: + /* + * The other types couldhooked up to Python later for + * testing of drivers' control of GPIOs. + */ + break; + } + + resp->status = VIRTIO_GPIO_STATUS_OK; + resp->value = 0; + + vu_queue_push(dev, vq, elem, sizeof(*resp)); + free(elem); + } + + vu_queue_notify(&vi->dev, vq); +} + +static void gpio_handle_eventq(VuDev *dev, int qidx) +{ + struct vhost_user_gpio *vi = + container_of(dev, struct vhost_user_gpio, dev); + VuVirtq *vq = vu_get_queue(dev, qidx); + VuVirtqElement *elem; + + for (;;) { + struct virtio_gpio_irq_request *req; + struct virtio_gpio_irq_response *resp; + + elem = vu_queue_pop(dev, vq, sizeof(VuVirtqElement)); + if (!elem) + break; + + dbg("elem %p index %u out_num %u in_num %u\n", elem, + elem->index, elem->out_num, elem->in_num); + + dump_iov("out", elem->out_sg, elem->out_num); + dump_iov("in", elem->in_sg, elem->in_num); + + assert(elem->out_num == 1); + assert(elem->in_num == 1); + + assert(elem->out_sg[0].iov_len == sizeof(*req)); + assert(elem->in_sg[0].iov_len == sizeof(*resp)); + + req = elem->out_sg[0].iov_base; + resp = elem->in_sg[0].iov_base; + + dbg("irq req gpio %#x\n", req->gpio); + + assert(req->gpio < ARRAY_SIZE(vi->irq_elements)); + assert(vi->irq_elements[req->gpio] == NULL); + + vi->irq_elements[req->gpio] = elem; + + gpio_unmask(vi, req->gpio); + } +} + +static void gpio_queue_set_started(VuDev *dev, int qidx, bool started) +{ + VuVirtq *vq = vu_get_queue(dev, qidx); + + dbg("%s %d:%d\n", __func__, qidx, started); + + if (qidx == 0) + vu_set_queue_handler(dev, vq, + started ? gpio_handle_cmdq : NULL); + if (qidx == 1) + vu_set_queue_handler(dev, vq, + started ? gpio_handle_eventq : NULL); +} + +static int gpio_get_config(VuDev *dev, uint8_t *config, uint32_t len) +{ + struct vhost_user_gpio *gpio = + container_of(dev, struct vhost_user_gpio, dev); + static struct virtio_gpio_config gpioconfig = { + .ngpio = ARRAY_SIZE(gpio->irq_elements), + }; + + dbg("%s: len %u\n", __func__, len); + + if (len > sizeof(struct virtio_gpio_config)) + return -1; + + memcpy(config, &gpioconfig, len); + + return 0; +} + +static uint64_t gpio_get_protocol_features(VuDev *dev) +{ + return 1ull << VHOST_USER_PROTOCOL_F_CONFIG; +} + +static uint64_t gpio_get_features(VuDev *dev) +{ + return 1ull << VIRTIO_GPIO_F_IRQ; +} + +static const VuDevIface gpio_vuiface = { + .get_features = gpio_get_features, + .queue_set_started = gpio_queue_set_started, + .process_msg = gpio_process_msg, + .get_config = gpio_get_config, + .get_protocol_features = gpio_get_protocol_features, +}; + +static void panic(VuDev *dev, const char *err) +{ + fprintf(stderr, "panicking!"); + abort(); +} + +static struct watch *new_watch(struct VuDev *dev, int fd, enum watch_type type, + void *func, void *data) +{ + struct watch *watch = malloc(sizeof(*watch)); + + assert(watch); + + watch->dev = dev; + watch->fd = fd; + watch->func = func; + watch->data = data; + watch->type = type; + + list_add(&watch->list, &watches); + + return watch; +} + +static void set_watch(VuDev *dev, int fd, int condition, vu_watch_cb cb, + void *data) +{ + struct watch *watch = new_watch(dev, fd, VU_WATCH, cb, data); + int ret; + + struct epoll_event ev = { + .events = EPOLLIN, + .data.ptr = watch, + }; + + dbg("set watch epfd %d fd %d condition %d cb %p\n", epfd, fd, condition, + cb); + + epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); + + ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); + if (ret < 0) + err(1, "epoll_ctl"); +} + +static void remove_watch(VuDev *dev, int fd) +{ + struct watch *watch, *tmp; + + list_for_each_entry_safe(watch, tmp, &watches, list) { + if (watch->dev != dev) + continue; + if (fd >= 0 && watch->fd != fd) + continue; + + epoll_ctl(epfd, EPOLL_CTL_DEL, watch->fd, NULL); + + list_del(&watch->list); + free(watch); + } +} + +static int unix_listen(const char *path) +{ + struct sockaddr_un un = { + .sun_family = AF_UNIX, + }; + int sock; + int ret; + + unlink(path); + + sock = socket(PF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sock < 0) + err(1, "socket"); + + memcpy(&un.sun_path, path, strlen(path)); + + ret = bind(sock, (struct sockaddr *)&un, sizeof(un)); + if (ret < 0) + err(1, "bind"); + + ret = listen(sock, 1); + if (ret < 0) + err(1, "listen"); + + return sock; +} + +static void dev_add_watch(int epfd, struct watch *watch) +{ + struct epoll_event event = { + .events = EPOLLIN | EPOLLONESHOT, + .data.ptr = watch, + }; + int ret; + + ret = epoll_ctl(epfd, EPOLL_CTL_ADD, watch->fd, &event); + if (ret < 0) + err(1, "EPOLL_CTL_ADD"); +} + +static VuDev *gpio_init(int epfd, const char *path) +{ + struct watch *watch; + VuDev *dev; + int lsock; + bool rc; + + lsock = unix_listen(path); + if (lsock < 0) + err(1, "listen %s", path); + + rc = vu_init(&gpio.dev, 2, lsock, panic, NULL, set_watch, + remove_watch, &gpio_vuiface); + assert(rc == true); + + dev = &gpio.dev; + watch = new_watch(dev, lsock, LISTEN, vu_dispatch, dev); + + dev_add_watch(epfd, watch); + + return dev; +} + +static VuDev *i2c_init(int epfd, const char *path) +{ + static struct vhost_user_i2c i2c = {}; + VuDev *dev = &i2c.dev; + struct watch *watch; + int lsock; + bool rc; + + lsock = unix_listen(path); + if (lsock < 0) + err(1, "listen %s", path); + + rc = vu_init(dev, 1, lsock, panic, NULL, set_watch, + remove_watch, &i2c_iface); + assert(rc == true); + + watch = new_watch(dev, lsock, LISTEN, vu_dispatch, dev); + + dev_add_watch(epfd, watch); + + return dev; +} + +static pid_t run_uml(char **argv) +{ + int log, null, ret; + pid_t pid; + + pid = fork(); + if (pid < 0) + err(1, "fork"); + if (pid > 0) + return pid; + + chdir(getenv("ROADTEST_WORK_DIR")); + + log = open("uml.txt", O_WRONLY | O_TRUNC | O_APPEND | O_CREAT, 0600); + if (log < 0) + err(1, "open uml.txt"); + + null = open("/dev/null", O_RDONLY); + if (null < 0) + err(1, "open null"); + + ret = dup2(null, 0); + if (ret < 0) + err(1, "dup2"); + + ret = dup2(log, 1); + if (ret < 0) + err(1, "dup2"); + + ret = dup2(log, 2); + if (ret < 0) + err(1, "dup2"); + + execvpe(argv[0], argv, environ); + err(1, "execve"); + + return -1; +} + +int main(int argc, char *argv[]) +{ + static struct option long_option[] = { + { "main-script", required_argument, 0, 'm' }, + { "gpio-socket", required_argument, 0, 'g' }, + { "i2c-socket", required_argument, 0, 'i' }, + }; + + while (1) { + int c = getopt_long(argc, argv, "", long_option, NULL); + + if (c == -1) + break; + + switch (c) { + case 'm': + opt_main_script = optarg; + break; + + case 'g': + opt_gpio_socket = optarg; + break; + + case 'i': + opt_i2c_socket = optarg; + break; + + default: + errx(1, "getopt"); + } + } + + if (!opt_main_script || !opt_gpio_socket || !opt_i2c_socket) + errx(1, "Invalid arguments"); + + epfd = epoll_create1(EPOLL_CLOEXEC); + if (epfd < 0) + err(1, "epoll_create1"); + + init_python(); + + gpio_init(epfd, opt_gpio_socket); + i2c_init(epfd, opt_i2c_socket); + + run_uml(&argv[optind]); + + while (1) { + struct epoll_event events[10]; + int nfds; + int i; + + nfds = epoll_wait(epfd, events, ARRAY_SIZE(events), -1); + if (nfds < 0) { + if (errno == EINTR) { + continue; + + err(1, "epoll_wait"); + } + } + + if (!PyObject_CallObject(py_process_control, NULL)) { + PyErr_Print(); + errx(1, "error from backend.process_control"); + } + + for (i = 0; i < nfds; i++) { + struct epoll_event *event = &events[i]; + struct watch *watch = event->data.ptr; + int fd; + + switch (watch->type) { + case LISTEN: + fd = accept(watch->fd, NULL, NULL); + close(watch->fd); + if (fd == -1) + err(1, "accept"); + + watch->dev->sock = fd; + watch->fd = fd; + watch->type = SOCKET_WATCH; + + struct epoll_event event = { + .events = EPOLLIN, + .data.ptr = watch, + }; + + int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, + &event); + if (ret < 0) + err(1, "epoll_ctl"); + + break; + case SOCKET_WATCH: + vu_dispatch(watch->dev); + break; + case VU_WATCH: + ((vu_watch_cb)(watch->func))(watch->dev, POLLIN, + watch->data); + break; + default: + fprintf(stderr, "abort!"); + abort(); + } + } + + if (i2cquit && gpioquit) + break; + } + + vu_deinit(&i2c.dev); + vu_deinit(&gpio.dev); + + Py_Finalize(); + + return 0; +} From patchwork Fri Mar 11 16:24:39 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Vincent Whitchurch X-Patchwork-Id: 550595 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id ABDB7C433FE for ; Fri, 11 Mar 2022 16:26:01 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S241536AbiCKQ1D (ORCPT ); Fri, 11 Mar 2022 11:27:03 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40924 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350271AbiCKQ0h (ORCPT ); Fri, 11 Mar 2022 11:26:37 -0500 Received: from smtp1.axis.com (smtp1.axis.com [195.60.68.17]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 094111BF930; Fri, 11 Mar 2022 08:25:07 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015909; x=1678551909; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=a4jqKhMbsN8UJ3i116koA51lQKLvtPhDM9o1oRoAbiw=; b=ID5Ud+pNpe7qiXGAX1K2P14GHr/qtfMPrmNNrLT8T+EK5n6s8erXWLTS 6oKvZAnrueSIGy/qHi6/fc5sEmX+t7iIdHtvCa8XWrVUT1LONsErISc0X nbD+hXPd/l8BIOzRRm4iKLzj3nXETpLf3RvOCK/+0D4re8T6SPPTjuRmB QYYRpLh0ekRPTdP3orTgEX7iWd75kGfl+gAjaDikXIeXQ0r7RbQ/IzUjz fsNUjLw5iFyCeRnU+fcl1OYS+vPyJ0A+i9kWY+vloBO8hAayXTmC28m5q Li+VY47M1hCfqBrX22hc0COVp5FcXFjDURFpqitSIzfeJErsbU0DJMJrc Q==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 04/10] roadtest: add base config Date: Fri, 11 Mar 2022 17:24:39 +0100 Message-ID: <20220311162445.346685-5-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: devicetree@vger.kernel.org Add the base config options for the roadtest kernel (generated with "savedefconfig"). roadtest uses a single kernel for all tests and the drivers under test are built as modules. Additional config options are added by merging config fragments from each subsystems' test directory. The kernel is built with several debug options to catch more problems during testing. Signed-off-by: Vincent Whitchurch --- .../roadtest/roadtest/tests/base/config | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tools/testing/roadtest/roadtest/tests/base/config diff --git a/tools/testing/roadtest/roadtest/tests/base/config b/tools/testing/roadtest/roadtest/tests/base/config new file mode 100644 index 000000000000..c1952d047c8e --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/base/config @@ -0,0 +1,84 @@ +CONFIG_NO_HZ=y +CONFIG_HIGH_RES_TIMERS=y +CONFIG_LOG_BUF_SHIFT=14 +CONFIG_EXPERT=y +CONFIG_HOSTFS=y +CONFIG_UML_TIME_TRAVEL_SUPPORT=y +CONFIG_NULL_CHAN=y +CONFIG_PORT_CHAN=y +CONFIG_PTY_CHAN=y +CONFIG_TTY_CHAN=y +CONFIG_XTERM_CHAN=y +CONFIG_CON_CHAN="pts" +CONFIG_SSL=y +CONFIG_SSL_CHAN="pts" +CONFIG_MAGIC_SYSRQ=y +CONFIG_VIRTIO_UML=y +CONFIG_UML_PCI_OVER_VIRTIO=y +CONFIG_UML_PCI_OVER_VIRTIO_DEVICE_ID=1234 +CONFIG_GCOV_KERNEL=y +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_BINFMT_MISC=m +# CONFIG_COMPACTION is not set +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_OF=y +# CONFIG_INPUT is not set +CONFIG_LEGACY_PTY_COUNT=32 +CONFIG_HW_RANDOM=y +# CONFIG_HW_RANDOM_IXP4XX is not set +# CONFIG_HW_RANDOM_STM32 is not set +# CONFIG_HW_RANDOM_MESON is not set +# CONFIG_HW_RANDOM_CAVIUM is not set +# CONFIG_HW_RANDOM_MTK is not set +# CONFIG_HW_RANDOM_EXYNOS is not set +# CONFIG_HW_RANDOM_NPCM is not set +# CONFIG_HW_RANDOM_KEYSTONE is not set +CONFIG_RANDOM_TRUST_BOOTLOADER=y +CONFIG_I2C=y +# CONFIG_I2C_COMPAT is not set +CONFIG_I2C_CHARDEV=y +CONFIG_I2C_VIRTIO=y +CONFIG_I2C_STUB=m +CONFIG_PPS=y +CONFIG_GPIOLIB=y +CONFIG_GPIO_VIRTIO=y +CONFIG_NET=y +CONFIG_UNIX=y +CONFIG_NEW_LEDS=y +CONFIG_LEDS_CLASS=y +CONFIG_LEDS_GPIO=y +CONFIG_LEDS_TRIGGERS=y +CONFIG_LEDS_TRIGGER_HEARTBEAT=y +CONFIG_RTC_CLASS=y +# CONFIG_RTC_HCTOSYS is not set +# CONFIG_RTC_SYSTOHC is not set +CONFIG_RTC_DEBUG=y +# CONFIG_RTC_NVMEM is not set +CONFIG_VIRTIO_INPUT=y +# CONFIG_BCM_VIDEOCORE is not set +CONFIG_QUOTA=y +CONFIG_AUTOFS4_FS=m +CONFIG_PROC_KCORE=y +CONFIG_TMPFS=y +CONFIG_NLS=y +CONFIG_CRYPTO=y +CONFIG_CRYPTO_CRC32C=y +CONFIG_CRYPTO_JITTERENTROPY=y +CONFIG_CRC16=y +CONFIG_PRINTK_TIME=y +CONFIG_PRINTK_CALLER=y +CONFIG_DYNAMIC_DEBUG=y +CONFIG_DEBUG_INFO=y +CONFIG_FRAME_WARN=1024 +CONFIG_READABLE_ASM=y +CONFIG_DEBUG_FS=y +CONFIG_UBSAN=y +CONFIG_PAGE_EXTENSION=y +CONFIG_DEBUG_OBJECTS=y +CONFIG_DEBUG_OBJECTS_FREE=y +CONFIG_DEBUG_OBJECTS_TIMERS=y +CONFIG_DEBUG_OBJECTS_WORK=y +CONFIG_PROVE_LOCKING=y +CONFIG_ENABLE_DEFAULT_TRACERS=y From patchwork Fri Mar 11 16:24:41 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Vincent Whitchurch X-Patchwork-Id: 550597 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 9A110C433FE for ; Fri, 11 Mar 2022 16:25:46 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350341AbiCKQ0s (ORCPT ); Fri, 11 Mar 2022 11:26:48 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:38330 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350344AbiCKQ0g (ORCPT ); Fri, 11 Mar 2022 11:26:36 -0500 Received: from smtp2.axis.com (smtp2.axis.com [195.60.68.18]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 7135B1B8BCE; Fri, 11 Mar 2022 08:25:00 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015902; x=1678551902; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=kuBlWK4REIfqzuNarkm/sea0x7/j6Le+nchS0vdPNlo=; b=V/Vkdgqc9tB6Qze27qevEUFMrn0STCFwBRAVOvqjxCEJUmlfnOcZl88y 659/I29s84i89BHhYFAfLc6M7rkZ/HkrRDG5bOW0A0c/2zmx8GEgT4oqf xAGC1v5qAhPtbrIn3fN1DHS8V4om8dqA2ZSGLBxf5wwYc9NKAS8npYYKR PO5TVlP+9q/nzSDplMMS8gHUF5JN8FJu+ZCECTaRoRVb8q0iCkM4dy2YP XRoNuz5NRQgQKxXr/g1WaUCn7YrY7azDddY3VzNKEuCTgKxtAKnziJhvz Ok8Z0EWRm6Ko5dyRtonpQ0QUhupBOQgU1LnwMAV1MBIk90EEK2XardCse Q==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 06/10] roadtest: add documentation Date: Fri, 11 Mar 2022 17:24:41 +0100 Message-ID: <20220311162445.346685-7-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: devicetree@vger.kernel.org Add documentation for the roadtest device driver testing framework. This includes a "how to write your first test" tutorial. Signed-off-by: Vincent Whitchurch --- Documentation/dev-tools/index.rst | 1 + Documentation/dev-tools/roadtest.rst | 669 +++++++++++++++++++++++++++ 2 files changed, 670 insertions(+) create mode 100644 Documentation/dev-tools/roadtest.rst diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst index 4621eac290f4..44fea7c50dad 100644 --- a/Documentation/dev-tools/index.rst +++ b/Documentation/dev-tools/index.rst @@ -33,6 +33,7 @@ Documentation/dev-tools/testing-overview.rst kselftest kunit/index ktap + roadtest .. only:: subproject and html diff --git a/Documentation/dev-tools/roadtest.rst b/Documentation/dev-tools/roadtest.rst new file mode 100644 index 000000000000..114bf822e376 --- /dev/null +++ b/Documentation/dev-tools/roadtest.rst @@ -0,0 +1,669 @@ +======== +Roadtest +======== + +Roadtest is a device-driver testing framework. It tests drivers under User +Mode Linux using models of the hardware. The tests cases and hardware models +are written in Python, the former using the built-in unittest framework. + +Roadtest is meant to be used for relatively simple drivers, such as the ones +part of the IIO, regulator or RTC subsystems. + +Drivers are tested via their userspace interfaces and interact with hardware +models which allow tests to inject values into registers and assert that +drivers control the hardware in the right way and react as expected to stimuli. + +Installing the requirements +=========================== + +Addition to the normal requirements for building kernels, *running* roadtest +requires Python 3.9 or later, including the development libraries: + +.. code-block:: shell + + apt-get -y install python3.9 libpython3.9-dev device-tree-compiler + +There is also support for running the tests in a Docker container without +having to install any packages. + +Running roadtest +================ + +To run the tests, run the following command from the base of a kernel source +tree: + +.. code-block:: shell + + $ make -C tools/testing/roadtest + +Or, if you prefer to use the Docker container: + +.. code-block:: shell + + $ make -C tools/testing/roadtest DOCKER=1 + +Either of these commands will build a kernel and run all roadtests. + +.. note:: + + Roadtest builds the kernel out-of-tree. The kernel build system may instruct + you to clean your tree if you have previously performed an in-tree build. You + can pass the usual ``-jNN`` options to parallelize the build. The tests + themselves are currently always run sequentially. + +Writing roadtests +================= + +Tutorial: Writing your first roadtest +------------------------------------- + +You may find it simplest to have a look at the existing tests and base your new +tests on them, but if you prefer, this section provides a tutorial which will +guide you to write a new basic test from scratch. + +Even if you're not too keen on following the tutorial hands-on, you're +encouraged to skim through it since there are useful debugging tips and notes +on roadtest's internals which could be useful to know before diving in and +writing tests. + +A quick note on the terminology before we begin: we'll refer to the framework +itself as "roadtest" or just "the framework", and we'll call a driver test +which uses this framework a "roadtest" or just a "test". + +Goal for the test +~~~~~~~~~~~~~~~~~ + +In this tutorial, we'll add a basic test for one of the features of the +VCNL4000 light sensor driver which is a part of the IIO subsystem +(``drivers/iio/light/vcnl4000.c``). + +This driver supports a bunch of related proximity and ambient light sensor +chips which communicate using the I2C protocol; we'll be testing the VCNL4000 +variant. The datasheet for the chip is, at the time of writing, available +`here `_. + +The test will check that the driver correctly reads and reports the illuminance +values from the hardware to userspace via the IIO framework. + +Test file placement +~~~~~~~~~~~~~~~~~~~ + +Roadtests are placed under ``tools/testing/roadtest/roadtest/tests``. (In case +you're wondering, the second ``roadtest`` is to create a Python package, so +that imports of ``roadtest`` work without having to mess with module search +paths.) + +Tests are organized by subsystem. Normally we'd put our IIO light sensor tests +under ``iio/light/`` (below the ``tests`` directory), but since there is +already a VCNL4000 test there, we'll create a new subsystem directory called +``tutorial`` and put our test there in a new file called ``test_tutorial.py``. + +We'll also need to create an empty ``__init__.py`` in that directory to allow +Python to recognize it as a package. + +All the commands in this tutorial should be executed from the +``tools/testing/roadtest`` directory inside the kernel source tree. (To reduce +noise, we won't show the current working directory before the ``$`` in future +command line examples.) + +.. code-block:: shell + + tools/testing/roadtest$ mkdir -p roadtest/tests/tutorial/ + tools/testing/roadtest$ touch roadtest/tests/tutorial/__init__.py + +Building the module +~~~~~~~~~~~~~~~~~~~ + +First, we'll need to ensure that our driver is built. To do that, we'll add +the appropriate config option to built our driver as a module. The lines +should be written to a new file called ``config`` in the ``tutorial`` +directory. Roadtest will gather all ``config`` files placed anywhere under +``tests`` and build a kernel with the combined config. + +.. code-block:: shell + + $ echo CONFIG_VCNL4000=m >> roadtest/tests/tutorial/config + +.. note:: + + This driver will actually be built even if you don't add this config, since + it's already present in the ``roadtest/tests/iio/light/config`` used by the + existing VCNL4000 test. Roadtest uses a single build for all tests. + +Loading the module from the test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We've set up our module to be built, so we can now start working on the test +case iself. We'll start with the following few lines of code. Tests are +written Python's built-in `unittest +`_ module. This tutorial will +assume familiariy with that framework; see the Python documentation for more +information. + +Test classes should subclass ``roadtest.core.suite.UMLTestCase`` instead of +``unittest.TestCase``. This informs the roadtest core code that the test +should be run inside UML. + +.. note:: + + There are several "real" unit tests for the framework itself; these subclass + ``unittest.TestCase`` directly and are run on the host system. You'll see + these run in the beginning when you run roadtest. + +All this test currently does is insert our driver's module, do nothing, and +then remove our driver's kernel module. (The ``roadtest.core.modules.Module`` +class implements a ``ContextManager`` which automatically cleans up using the +``with`` statement.) + +.. code-block:: python + + from roadtest.core.suite import UMLTestCase + from roadtest.core.modules import Module + + class TestTutorial(UMLTestCase): + def test_illuminance(self) -> None: + with Module("vcnl4000"): + pass + +You can now build the kernel and run roadtest with: + +.. code-block:: shell + + $ make + +.. note:: + + Make sure you have all the dependencies described at the beginning of the + document installed. You can also use a Docker container, append ``DOCKER=1`` + to all the ``make`` commands in this tutorial if you want to do that. + +You should see your new test run and pass in the output of the above command: + +.. code-block:: + + ... + test_illuminance (tests.tutorial.test_tutorial.TestTutorial) ... ok + ... + +Shortening feedback loops +~~~~~~~~~~~~~~~~~~~~~~~~~ + +While just running ``make`` runs your new test, it also runs all the *other* +tests too, and what's more, it calls in to the kernel build system every time, +and that can be relatively slow even if there's nothing to be rebuilt. + +When you're only working on writing tests, and not modifying the driver or the +kernel source, you can avoid calling into Kbuild by passing ``KBUILD=0`` to the +``make`` invocation. For example: + +.. code-block:: shell + + $ make KBUILD=0 + +To only run specific tests, you can use the ``--filter`` option to roadtest's +main script (implemented in ``roadtest.cmd.main``) which takes a wildcard +pattern. Only tests whoses names match the pattern are run. + +Options to the main script are passed via the ``OPTS`` variable. So the +following would both skip the kernel build and only run your test: + +.. code-block:: shell + + $ make KBUILD=0 OPTS="--filter tutorial" + +.. tip:: + + Roadtest builds the kernel inside a directory called ``.roadtest`` in your + kernel source tree. Logs from UML are saved as + ``.roadtest/roadtest-work/uml.txt`` and logs from roadtest's backend (more on + that later) are at ``.roadtest/roadtest-work/backend.txt``. It's sometimes + useful to keep a terminal open running ``tail -f`` on these files while + developing roadtests. + +Adding a device +~~~~~~~~~~~~~~~ + +Our basic test only loads and unloads the module, so the next step is to +actually get our driver to probe and bind to a device. On many systems, +devices are instantiated based on the hardware descriptions in devicetree, and +this is the case on roadtest's UML-based system too. See +:ref:`Documentation/driver-api/driver-model/binding.rst ` and +:ref:`Documentation/devicetree/usage-model.rst ` for more +information. + +When working on real harwdare, the hardware design specifies at what address +and on which I2C bus the hardware sensor chip is connected. Roadtest provides +a virtual I2C bus and the test can chose to place devices at any valid address +on this bus. + +In this tutorial, we'll use a hard coded device address of ``0x42`` and set the +``run_separately`` flag on the test, asking roadtest to run our test in a +separate UML instance so that we know that no other test has tried to put a +device at that I2C address. + +.. note:: + + Normally, roadtests use what the framework refers to as *relocatable + devicetree fragments* (unrelated to the fragments used in devicetree + overlays). These do not use fixed addreses for specific devices, but instead + allow the framework to freely assign addresses. This allows several + different, independent tests can be run using one devicetree and one UML + instance (to save on startup time costs), without having to coordinate + selection of device addesses. + + When writing "real" roadtests (after you're done with this tutorial), you too + should use relocatable fragments. See the existing tests for examples. + +The framework's devicetree module (``roadtest.core.devicetree``) includes a +base tree that provides an I2C controller node (appropriately named ``i2c``) +for the virtual I2C, so we will add our new device under that node. + +Unlike on a default Linux system, just adding the node to the devicetree won't +get our I2C driver to automatically bind to the driver when we load the module. +This is because roadtest's ``init.sh`` (a script which runs inside UML after +the kernel boots up) turns off automatic probing on the I2C bus, in order to +give the test cases full control of when things get probed. + +So we'll have ask the ``test_illuminance()`` method to get the ``vcnl4000`` +driver (that's the name of the I2C driver which the module registers, and +that's not necessarily the same as the name of the module) to explicitly bind +to our chosen ``0x42`` I2C device using some of the helper classes in the +framework: + +.. code-block:: python + + from roadtest.core.devicetree import DtFragment + from roadtest.core.devices import I2CDriver + + class TestTutorial(UMLTestCase): + run_separately = True + dts = DtFragment( + src=""" + &i2c { + light-sensor@42 { + compatible = "vishay,vcnl4000"; + reg = <0x42>; + }; + }; + """, + ) + + def test_illuminance(self) -> None: + with ( + Module("vcnl4000"), + I2CDriver("vcnl4000").bind(0x42) as dev, + ): + pass + +You can run this test using the same ``make`` command you used previously. +This time, rather than an "ok", you should see roadtest complain about an error +during your test: + +.. code-block:: + + ====================================================================== + ERROR: test_illuminance (tests.tutorial.test_tutorial.TestTutorial) + ---------------------------------------------------------------------- + Backend log: + Traceback (most recent call last): + File ".../roadtest/backend/i2c.py", line 35, in write + raise Exception("No I2C model loaded") + Exception: No I2C model loaded + Traceback (most recent call last): + File ".../roadtest/backend/i2c.py", line 29, in read + raise Exception("No I2C model loaded") + Exception: No I2C model loaded + + UML log: + [ 1220.410000][ T19] vcnl4000: probe of 0-0042 failed with error -5 + + Traceback (most recent call last): + File ".../roadtest/tests/tutorial/test_tutorial.py", line 21, in test_illuminance + with ( + File "/usr/lib/python3.9/contextlib.py", line 119, in __enter__ + return next(self.gen) + File ".../roadtest/core/devices.py", line 32, in bind + f.write(dev.id.encode()) + OSError: [Errno 5] Input/output error + +To understand and fix this error, we'll have to learn a bit about how roadtest +works under the hood. + +Adding a hardware model +~~~~~~~~~~~~~~~~~~~~~~~ + +Roadtest's *backend* is what allows the hardware to modelled for the sake of +driver testing. The backend runs outside of UML and communication between the +drivers and the models goes via ``virtio-uml``, a shared-memory based +communication protocol. At its lowest level, the backend is written in C and +implements virtio devices for ``virtio-i2c`` and ``virtio-gpio``, both of which +have respective virtio drivers which run inside UML and provide the virtual I2C +bus (and GPIO controller) whose nodes are available in the devicetree. + +The C backend embeds a Python interpreter which runs a Python module which +implements the I2C bus model. It's that Python module which is complaining now +that it does not have any I2C device model to handle the I2C transactions that +it received from UML. This is quite understandable since we haven't +implemented one yet! + +.. note:: + + In the error message above, you'll also notice an error ``printk()`` from the + driver (as part of the *UML log*, which includes kernel console messages), as + well as the exception stacktrace from the test case itself. The ``-EIO`` + seen inside UML is a result of the roadtest backend failing the I2C + transaction due to the exception. + +Models are placed in the same source file as the test cases. The model and +the test cases will however run in two different Python interpreters on two +different systems (the test case inside UML, and the model inside the backend +on your host). + +For I2C, the interface our model needs to implement is specified by the +Abstract Base Class ``roadtest.backend.i2c.I2CModel`` (which can be found, +following Python's standard naming conventions, in the file +``roadtest/backend/i2c.py``). You can see that it expects the model to +implement ``read()`` and ``write()`` functions which transmit and receive the +raw bytes of the I2C transaction. + +Our VCNL4000 device uses the SMBus protocol which is a subset of the I2C +protocol, so we can use a higher-level class to base our implementation off, +``roadtest.backend.i2c.SMBusModel``. This one takes care of doing segmentation +of the I2C requests, and expects subclasses to implement ``reg_read()`` and +``reg_write()`` methods which will handle the register access for the device. + +For our initial model, we'll just going to just make our ``reg_read()`` and +``reg_write()`` methods read and store the register values in a dictionary. +We'll need some initial values for the registers, and for these we use the +values which are specified in the VCNL4000's datasheet. We won't bother with +creating constants for the register addresses and we'll just specify them in +hex: + +.. code-block:: python + + from typing import Any + from roadtest.backend.i2c import SMBusModel + + class VCNL4000(SMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__(regbytes=1, **kwargs) + self.regs = { + 0x80: 0b_1000_0000, + 0x81: 0x11, + 0x82: 0x00, + 0x83: 0x00, + 0x84: 0x00, + 0x85: 0x00, + 0x86: 0x00, + 0x87: 0x00, + 0x88: 0x00, + 0x89: 0x00, + } + + def reg_read(self, addr: int) -> int: + val = self.regs[addr] + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] = val + +Then we need to modify the test function to ask the backend to load this model: + +.. code-block:: python + :emphasize-lines: 1,6 + + from roadtest.core.hardware import Hardware + + def test_illuminance(self) -> None: + with ( + Module("vcnl4000"), + Hardware("i2c").load_model(VCNL4000), + I2CDriver("vcnl4000").bind(0x42), + ): + pass + +Now run the test again. You should see the test pass, meaning that the driver +successfully talked to and recognized your hardware model. (You can look at +the UML and backend logs mentioned earlier to confirm this.) + +.. tip:: + + You can add arbitrary command line arguments to UML using the + ``--uml-append`` option. For example, while developing tests for I2C + drivers, it could be helpful to turn on the appropriate trace events and + arrange for them to be printed to the console (which you can then access via + the previously mentioned ``uml.txt``.): + + .. code-block:: + + OPTS="--filter tutorial --uml-append tp_printk trace_event=i2c:*" + +Exploring the target +~~~~~~~~~~~~~~~~~~~~ + +Now that we've gotten the driver to probe to our new device, we want to get the +test to read the illuminance value from the driver. However, which file should +the test read the value from? IIO exposes the illuminance value in a sysfs +file, but where do we find this file? + +If you have real hardware with a VCNL4000 chip and already running the vcnl4000 +driver, or are already very familiar with the IIO framework, you likely already +know what sysfs files to read, but in our case, we can open up a shell on UML +to manually explore the system and find the relevant sysfs files before +implementing the rest of the test case. + +Roadtest's ``--shell`` option makes UML start a shell instead of exiting after +the tests are run. However, since our test case cleans up after itself (as +it should) using context managers, neither the module nor the model would +remain loaded after the test exists, which would make manual exploration +difficult. + +To remedy this, we can combine ``--shell`` with temporary code in our test to +_exit(2) after setting up everything: + +.. code-block:: python + :emphasize-lines: 5,7 + + def test_illuminance(self) -> None: + with ( + Module("vcnl4000"), + Hardware("i2c").load_model(VCNL4000), + I2CDriver("vcnl4000").bind(0x42) as dev, + ): + print(dev.path) + import os; os._exit(1) + +.. note:: + + The communication between the test cases and the models uses a simple text + based protocol where the test cases write Python expressions to a file which + the backend reads and evaluates, so it is possible to load a model using only + shell commands, but this is undocumented. See the source code if you need to + do this. + +We'll also need to ask UML to open up a terminal emulator (``con=xterm``) or start a telnet server +and wait for a connection (``con=port:9000``). See +:ref:`Documentation/virt/uml/user_mode_linux_hotwo_v2.rst +` for more information about the required packages. +These options can be passed to UML using ``--uml-append``. So the final +``OPTS`` argument is something like the following (you can combine this with +the tracing options): + +.. code-block:: + + OPTS="--shell --uml-append con=xterm" + +.. tip:: + + ``con=xterm doesn``'t work in the Docker container, so use the telnet option + if you're running roadtest inside Docker. ``screen -L //telnet localhost + 9000`` or similar can be used to connect to UML. + + When running *without* using Docker, the telnet option tends to leave UML's + ``port-helper`` running in the background, so you may have to ``kill(1)`` it + yourself after each run. + +Using the shell, you should be able to find the illuminance file under the +device's sysfs path: + +.. code-block:: + + root@(none):/sys/bus/i2c/devices/0-0042# ls -1 iio\:device0/in* + iio:device0/in_illuminance_raw + iio:device0/in_illuminance_scale + iio:device0/in_proximity_nearlevel + iio:device0/in_proximity_raw + +You can also attempt to read the ``in_illuminance_raw`` file; you should see +that it fails with something like this (with the trace events enabled): + +.. code-block:: + + root@(none):/sys/bus/i2c/devices/0-0042# cat iio:device0/in_illuminance_raw + [ 151.270000][ T34] i2c_write: i2c-0 #0 a=042 f=0000 l=2 [80-10] + [ 151.270000][ T34] i2c_result: i2c-0 n=1 ret=1 + ... + [ 152.030000][ T34] i2c_write: i2c-0 #0 a=042 f=0000 l=1 [80] + [ 152.030000][ T34] i2c_read: i2c-0 #1 a=042 f=0001 l=1 + [ 152.030000][ T34] i2c_reply: i2c-0 #1 a=042 f=0001 l=1 [10] + [ 152.030000][ T34] i2c_result: i2c-0 n=2 ret=2 + [ 152.070000][ T34] vcnl4000 0-0042: vcnl4000_measure() failed, data not ready + +Controlling register values +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Our next challenge is to get the ``in_illuminance_raw`` file to be read +successfully. From the I2C trace events above, or from looking at the +``backend.txt`` (below), we can see that the driver repeatedly reads a +particular register. + +.. code-block:: + + INFO - roadtest.core.control: START + DEBUG - roadtest.core.control: backend.i2c.load_model(*('roadtest.tests.tutorial.test_tutorial', 'VCNL4000'), **{}) + DEBUG - roadtest.backend.i2c: SMBus read addr=0x81 val=0x11 + DEBUG - roadtest.backend.i2c: SMBus write addr=0x80 val=0x10 + DEBUG - roadtest.backend.i2c: SMBus read addr=0x80 val=0x10 + DEBUG - roadtest.backend.i2c: SMBus read addr=0x80 val=0x10 + ... + +To understand this register, we need to take a look at the chip's datasheet and +compare it with the driver code. By doing so, we can see the driver is waiting +for the hardware to signal that the data is ready by polling for a particular +bit to be set. + +One simple way to set the data ready bit, which we'll use for the purpose of +this tutorial, is to simply ensure that the model always returns reads to the +0x80 register with that bit set. + +.. note:: + + This method wouldn't allow a test to be written to test the timeout handling, + but we won't bother with that in this tutorial. You can explore the exising + roadtests for alternative solutions, such as setting the data ready bit + whenever the test injects new data and clearing it when the driver reads the + data. + +.. code-block:: python + :emphasize-lines: 4,5 + + def reg_read(self, addr: int) -> int: + val = self.regs[addr] + + if addr == 0x80: + val |= 1 << 6 + + return val + +This should get the bit set and make the read succeed (you can check this using +the shell), but we'd also like to return different values from the data +registers rather the reset values we hardcoded in ``__init__``. One way to do +this is to have the test inject the values into the ALS result registers by +having it call the ``reg_write()`` method of the model. It can do this via the +``Hardware`` object. + +.. note:: + + The test can call methods on the model but it can't receive return values + from these methods, nor can it set attributes on the model. The model and + the test run on different systems and communication between them is + asynchronous. + +We'll combine this with a read of the sysfs file we identified and throw in an +assertion to check that the value which the driver reports to userspace via +that file matches the value which we inject into the hardware's result +registers: + +.. code-block:: python + :emphasize-lines: 6,8,9-13 + + from roadtest.core.sysfs import read_int + + def test_illuminance(self) -> None: + with ( + Module("vcnl4000"), + Hardware("i2c").load_model(VCNL4000) as hw, + I2CDriver("vcnl4000").bind(0x42) as dev, + ): + hw.reg_write(0x85, 0x12) + hw.reg_write(0x86, 0x34) + self.assertEqual( + read_int(dev.path / "iio:device0/in_illuminance_raw", 0x1234) + ) + +And that's it for this tutorial. We've written a simple end-to-end test for +one aspect of this driver with the help of a minimal model of the hardware. + +Verifying drivers' interactions with the hardware +------------------------------------------------- + +The tutorial covered injection of values into hardware registers and how to +check that the driver interprets the value exposed by the hardware correctly, +but another important aspect of testing device drivers is to verify that the +driver actually *controls* the hardware in the expected way. + +For example, if you are testing a regulator driver, you want to test that +driver actually writes the correct voltage register in the hardware with the +correct value when the driver is asked to set a voltage using the kernel's +regulator API. + +To support this, roadtest integrates with Python's built-in `unittest.mock +`_ library. The +``update_mock()`` method on the ``Hardware`` objects results in a ``HwMock`` (a +subclass of ``unittest.mock``'s ``MagicMock``) object which, in the case of +``SMBusModel``, provides access to a log of all register writes and their +values. + +The object can be then used to check which registers the hardware has written +with which values, and to assert that the expect actions have been taken. + +See ``roadtest/tests/regulator/test_tps62864.py`` for an example of this. + +GPIOs +----- + +The framework includes support for hardware models to trigger interrupts by +controlling GPIOs. See ``roadtest/tests/rtc/test_pcf8563.py`` for an example. + +Support has not been implemented yet for asserting that drivers control GPIOs +correctly. See the comment in ``gpio_handle_cmdq()`` in ``src/backend.c``. + +Coding guidelines +----------------- + +Run ``make fmt`` to automatically format your Python code to follow the coding +style. Run ``make check`` and ensure that your code passes static checkers and +style checks. Typing hints are mandatory. + +These two commands require that you have installed the packages listed in +``requirements.txt``, for example with something like the following patch and +then ensuring that ``~/.local/bin`` is in your ``$PATH``. + +.. code-block:: shell + + $ pip3 install --user -r requirements.txt + +Alternatively, you can also run these commands in the Docker container (by +appending ``DOCKER=1`` to the ``make`` commands) which has all the correct +tools installed. From patchwork Fri Mar 11 16:24:43 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Vincent Whitchurch X-Patchwork-Id: 550594 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 06743C433F5 for ; Fri, 11 Mar 2022 16:26:41 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350379AbiCKQ1m (ORCPT ); Fri, 11 Mar 2022 11:27:42 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:37596 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350328AbiCKQ0r (ORCPT ); Fri, 11 Mar 2022 11:26:47 -0500 Received: from smtp2.axis.com (smtp2.axis.com [195.60.68.18]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 8540A1D178C; Fri, 11 Mar 2022 08:25:09 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015914; x=1678551914; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=2fctezGvjdiZGRh6wrKTd5SnqfgQb4vo1grBqwFZpAI=; b=A1elt0X77uBkJat/ZRyuc8evduF0sDRwuYlNfF2ZKcEqOrX9qen9uw3p hM0xzn11p/R8d4ebq1D6fmsuMmwATOdzFS4+ZR8YiwKuHkyfKdsgm+ky5 olslcyMBzPDWocu5ZwqSWRrbz7XEJoUe3G+FKeH9iL75DTPod3+tTj7lv wnbqbx4FqDt89RRaTtSEh+oD4cHTZzDjSHTTeovPXkY29z2TDv8zy2jCi X1B8H9wrChk+/++j5DJ0PBhPrLo+UmYgJt++ijkkQJUyGsjBYU7yWJ12T AN9CG6EQO1QEOCLO8VWDqLWuH1YRkvG5yZtHpb2Kq8zjmO4mhifr/95SC w==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 08/10] iio: light: vcnl4000: add roadtest Date: Fri, 11 Mar 2022 17:24:43 +0100 Message-ID: <20220311162445.346685-9-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: devicetree@vger.kernel.org Add roadtests for the vcnl4000 driver, testing several of the driver's features including buffer and event handling. Since it's the first IIO roadtest testing the non-sysfs parts, some support code for using the IIO ABI is included. The different variants supported by the driver are in separate tests and models since no two variants have fully identical register interfaces. This duplicates some of the test code, but it: - Avoids the tests duplicating the same multi-variant logic as the driver, reducing the risk for both the test and the driver being wrong. - Allows each variant's test and model to be individually understood and modified looking at only one specific datasheet, making it easier to extend tests and implement new features in the driver. During development of these tests, two oddities were noticed in the driver's handling of VCNL4040, but the tests simply assume that the current driver knows what it's doing (although we may want to fix the first point later): - The driver reads an invalid/undefined register on the VCNL4040 when attempting to distinguish between that one and VCNL4200. - The driver uses a lux/step unit which differs from the datasheet (but which is specified in an application note). Signed-off-by: Vincent Whitchurch --- .../roadtest/roadtest/tests/iio/iio.py | 112 +++++++ .../roadtest/roadtest/tests/iio/light/config | 1 + .../roadtest/tests/iio/light/test_vcnl4000.py | 132 ++++++++ .../roadtest/tests/iio/light/test_vcnl4010.py | 282 ++++++++++++++++++ .../roadtest/tests/iio/light/test_vcnl4040.py | 104 +++++++ .../roadtest/tests/iio/light/test_vcnl4200.py | 96 ++++++ 6 files changed, 727 insertions(+) create mode 100644 tools/testing/roadtest/roadtest/tests/iio/iio.py create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4000.py create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4010.py create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4040.py create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4200.py diff --git a/tools/testing/roadtest/roadtest/tests/iio/iio.py b/tools/testing/roadtest/roadtest/tests/iio/iio.py new file mode 100644 index 000000000000..ea57b28ea9d3 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/iio.py @@ -0,0 +1,112 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import contextlib +import enum +import fcntl +import struct +from dataclasses import dataclass, field +from typing import Any + +IIO_GET_EVENT_FD_IOCTL = 0x80046990 +IIO_BUFFER_GET_FD_IOCTL = 0xC0046991 + + +class IIOChanType(enum.IntEnum): + IIO_VOLTAGE = 0 + IIO_CURRENT = 1 + IIO_POWER = 2 + IIO_ACCEL = 3 + IIO_ANGL_VEL = 4 + IIO_MAGN = 5 + IIO_LIGHT = 6 + IIO_INTENSITY = 7 + IIO_PROXIMITY = 8 + IIO_TEMP = 9 + IIO_INCLI = 10 + IIO_ROT = 11 + IIO_ANGL = 12 + IIO_TIMESTAMP = 13 + IIO_CAPACITANCE = 14 + IIO_ALTVOLTAGE = 15 + IIO_CCT = 16 + IIO_PRESSURE = 17 + IIO_HUMIDITYRELATIVE = 18 + IIO_ACTIVITY = 19 + IIO_STEPS = 20 + IIO_ENERGY = 21 + IIO_DISTANCE = 22 + IIO_VELOCITY = 23 + IIO_CONCENTRATION = 24 + IIO_RESISTANCE = 25 + IIO_PH = 26 + IIO_UVINDEX = 27 + IIO_ELECTRICALCONDUCTIVITY = 28 + IIO_COUNT = 29 + IIO_INDEX = 30 + IIO_GRAVITY = 31 + IIO_POSITIONRELATIVE = 32 + IIO_PHASE = 33 + IIO_MASSCONCENTRATION = 34 + + +@dataclass +class IIOEvent: + id: int + timestamp: int + type: IIOChanType = field(init=False) + + def __post_init__(self) -> None: + self.type = IIOChanType((self.id >> 32) & 0xFF) + + +class IIOEventMonitor(contextlib.AbstractContextManager): + def __init__(self, devname: str) -> None: + self.devname = devname + + def __enter__(self) -> "IIOEventMonitor": + self.file = open(self.devname, "rb") + + s = struct.Struct("L") + buf = bytearray(s.size) + fcntl.ioctl(self.file.fileno(), IIO_GET_EVENT_FD_IOCTL, buf) + eventfd = s.unpack(buf)[0] + self.eventf = open(eventfd, "rb") + + return self + + def read(self) -> IIOEvent: + s = struct.Struct("Qq") + buf = self.eventf.read(s.size) + return IIOEvent(*s.unpack(buf)) + + def __exit__(self, *_: Any) -> None: + self.eventf.close() + self.file.close() + + +class IIOBuffer(contextlib.AbstractContextManager): + def __init__(self, devname: str, bufidx: int) -> None: + self.devname = devname + self.bufidx = bufidx + + def __enter__(self) -> "IIOBuffer": + self.file = open(self.devname, "rb") + + s = struct.Struct("L") + buf = bytearray(s.size) + s.pack_into(buf, 0, self.bufidx) + fcntl.ioctl(self.file.fileno(), IIO_BUFFER_GET_FD_IOCTL, buf) + eventfd = s.unpack(buf)[0] + self.eventf = open(eventfd, "rb") + + return self + + def read(self, spec: str) -> tuple: + s = struct.Struct(spec) + buf = self.eventf.read(s.size) + return s.unpack(buf) + + def __exit__(self, *_: Any) -> None: + self.eventf.close() + self.file.close() diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/config b/tools/testing/roadtest/roadtest/tests/iio/light/config index b9753f2d0728..3bd4125cbb6b 100644 --- a/tools/testing/roadtest/roadtest/tests/iio/light/config +++ b/tools/testing/roadtest/roadtest/tests/iio/light/config @@ -1 +1,2 @@ CONFIG_OPT3001=m +CONFIG_VCNL4000=m diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4000.py b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4000.py new file mode 100644 index 000000000000..16a5bed18b7e --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4000.py @@ -0,0 +1,132 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import errno +import logging +from typing import Any, Final + +from roadtest.backend.i2c import SMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import I2CDriver, read_float, read_int, read_str + +logger = logging.getLogger(__name__) + +REG_COMMAND: Final = 0x80 +REG_PRODUCT_ID_REVISION: Final = 0x81 +REG_IR_LED_CURRENT: Final = 0x83 +REG_ALS_PARAM: Final = 0x84 +REG_ALS_RESULT_HIGH: Final = 0x85 +REG_ALS_RESULT_LOW: Final = 0x86 +REG_PROX_RESULT_HIGH: Final = 0x87 +REG_PROX_RESULT_LOW: Final = 0x88 +REG_PROX_SIGNAL_FREQ: Final = 0x89 + +REG_COMMAND_ALS_DATA_RDY: Final = 1 << 6 +REG_COMMAND_PROX_DATA_RDY: Final = 1 << 5 + + +class VCNL4000(SMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__(regbytes=1, **kwargs) + self.regs = { + REG_COMMAND: 0b_1000_0000, + REG_PRODUCT_ID_REVISION: 0x11, + # Register "without function in current version" + 0x82: 0x00, + REG_IR_LED_CURRENT: 0x00, + REG_ALS_PARAM: 0x00, + REG_ALS_RESULT_HIGH: 0x00, + REG_ALS_RESULT_LOW: 0x00, + REG_PROX_RESULT_HIGH: 0x00, + REG_PROX_RESULT_LOW: 0x00, + REG_PROX_RESULT_LOW: 0x00, + } + + def reg_read(self, addr: int) -> int: + val = self.regs[addr] + + if addr in (REG_ALS_RESULT_HIGH, REG_ALS_RESULT_LOW): + self.regs[REG_COMMAND] &= ~REG_COMMAND_ALS_DATA_RDY + if addr in (REG_PROX_RESULT_HIGH, REG_PROX_RESULT_LOW): + self.regs[REG_COMMAND] &= ~REG_COMMAND_PROX_DATA_RDY + + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + + if addr == REG_COMMAND: + rw = 0b_0001_1000 + val = (self.regs[addr] & ~rw) | (val & rw) + + self.regs[addr] = val + + def inject(self, addr: int, val: int, mask: int = ~0) -> None: + old = self.regs[addr] & ~mask + new = old | (val & mask) + self.regs[addr] = new + + +class TestVCNL4000(UMLTestCase): + dts = DtFragment( + src=""" +&i2c { + light-sensor@$addr$ { + compatible = "vishay,vcnl4000"; + reg = <0x$addr$>; + }; +}; + """, + variables={ + "addr": DtVar.I2C_ADDR, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("vcnl4000") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("vcnl4000") + + def setUp(self) -> None: + self.driver = I2CDriver("vcnl4000") + self.hw = Hardware("i2c") + self.hw.load_model(VCNL4000) + + def tearDown(self) -> None: + self.hw.close() + + def test_lux(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + scale = read_float(dev.path / "iio:device0/in_illuminance_scale") + self.assertEqual(scale, 0.25) + + data = [ + (0x00, 0x00), + (0x12, 0x34), + (0xFF, 0xFF), + ] + luxfile = dev.path / "iio:device0/in_illuminance_raw" + for high, low in data: + self.hw.inject(REG_ALS_RESULT_HIGH, high) + self.hw.inject(REG_ALS_RESULT_LOW, low) + self.hw.inject( + REG_COMMAND, + val=REG_COMMAND_ALS_DATA_RDY, + mask=REG_COMMAND_ALS_DATA_RDY, + ) + + self.assertEqual(read_int(luxfile), high << 8 | low) + + def test_lux_timeout(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + # self.hw.set_never_ready(True) + with self.assertRaises(OSError) as cm: + luxfile = dev.path / "iio:device0/in_illuminance_raw" + read_str(luxfile) + self.assertEqual(cm.exception.errno, errno.EIO) diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4010.py b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4010.py new file mode 100644 index 000000000000..929db970405f --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4010.py @@ -0,0 +1,282 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import errno +import logging +from pathlib import Path +from typing import Any, Final, Optional + +from roadtest.backend.i2c import SMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import ( + I2CDriver, + read_float, + read_int, + read_str, + write_int, + write_str, +) +from roadtest.tests.iio import iio + +logger = logging.getLogger(__name__) + +REG_COMMAND: Final = 0x80 +REG_PRODUCT_ID_REVISION: Final = 0x81 +REG_PROXIMITY_RATE: Final = 0x82 +REG_IR_LED_CURRENT: Final = 0x83 +REG_ALS_PARAM: Final = 0x84 +REG_ALS_RESULT_HIGH: Final = 0x85 +REG_ALS_RESULT_LOW: Final = 0x86 +REG_PROX_RESULT_HIGH: Final = 0x87 +REG_PROX_RESULT_LOW: Final = 0x88 +REG_INTERRUPT_CONTROL: Final = 0x89 +REG_LOW_THRESHOLD_HIGH: Final = 0x8A +REG_LOW_THRESHOLD_LOW: Final = 0x8B +REG_HIGH_THRESHOLD_HIGH: Final = 0x8C +REG_HIGH_THRESHOLD_LOW: Final = 0x8D +REG_INTERRUPT_STATUS: Final = 0x8E + +REG_COMMAND_ALS_DATA_RDY: Final = 1 << 6 +REG_COMMAND_PROX_DATA_RDY: Final = 1 << 5 + + +class VCNL4010(SMBusModel): + def __init__(self, int: Optional[int] = None, **kwargs: Any) -> None: + super().__init__(regbytes=1, **kwargs) + self.int = int + self._set_int(False) + self.regs = { + REG_COMMAND: 0b_1000_0000, + REG_PRODUCT_ID_REVISION: 0x21, + REG_PROXIMITY_RATE: 0x00, + REG_IR_LED_CURRENT: 0x00, + REG_ALS_PARAM: 0x00, + REG_ALS_RESULT_HIGH: 0x00, + REG_ALS_RESULT_LOW: 0x00, + REG_PROX_RESULT_HIGH: 0x00, + REG_PROX_RESULT_LOW: 0x00, + REG_INTERRUPT_CONTROL: 0x00, + REG_LOW_THRESHOLD_HIGH: 0x00, + REG_LOW_THRESHOLD_LOW: 0x00, + REG_HIGH_THRESHOLD_HIGH: 0x00, + REG_HIGH_THRESHOLD_LOW: 0x00, + REG_INTERRUPT_STATUS: 0x00, + } + + def _set_int(self, active: int) -> None: + # Active-low + self.backend.gpio.set(self.int, not active) + + def _update_irq(self) -> None: + selftimed_en = self.regs[REG_COMMAND] & (1 << 0) + prox_en = self.regs[REG_COMMAND] & (1 << 1) + prox_data_rdy = self.regs[REG_COMMAND] & REG_COMMAND_PROX_DATA_RDY + int_prox_ready_en = self.regs[REG_INTERRUPT_CONTROL] & (1 << 3) + + logger.debug( + f"{selftimed_en=:x} {prox_en=:x} {prox_data_rdy=:x} {int_prox_ready_en=:x}" + ) + + if selftimed_en and prox_en and prox_data_rdy and int_prox_ready_en: + self.regs[REG_INTERRUPT_STATUS] |= 1 << 3 + + low_threshold = ( + self.regs[REG_LOW_THRESHOLD_HIGH] << 8 | self.regs[REG_LOW_THRESHOLD_LOW] + ) + high_threshold = ( + self.regs[REG_HIGH_THRESHOLD_HIGH] << 8 | self.regs[REG_HIGH_THRESHOLD_LOW] + ) + proximity = ( + self.regs[REG_PROX_RESULT_HIGH] << 8 | self.regs[REG_PROX_RESULT_LOW] + ) + int_thres_en = self.regs[REG_INTERRUPT_CONTROL] & (1 << 1) + + logger.debug( + f"{low_threshold=:x} {high_threshold=:x} {proximity=:x} {int_thres_en=:x}" + ) + + if int_thres_en: + if proximity < low_threshold: + logger.debug("LOW") + self.regs[REG_INTERRUPT_STATUS] |= 1 << 1 + if proximity > high_threshold: + logger.debug("HIGH") + self.regs[REG_INTERRUPT_STATUS] |= 1 << 0 + + self._set_int(self.regs[REG_INTERRUPT_STATUS]) + + def reg_read(self, addr: int) -> int: + val = self.regs[addr] + + if addr in (REG_ALS_RESULT_HIGH, REG_ALS_RESULT_LOW): + self.regs[REG_COMMAND] &= ~REG_COMMAND_ALS_DATA_RDY + if addr in (REG_PROX_RESULT_HIGH, REG_PROX_RESULT_LOW): + self.regs[REG_COMMAND] &= ~REG_COMMAND_PROX_DATA_RDY + + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + + if addr == REG_COMMAND: + rw = 0b_0001_1111 + val = (self.regs[addr] & ~rw) | (val & rw) + elif addr == REG_INTERRUPT_STATUS: + val = self.regs[addr] & ~(val & 0xF) + + self.regs[addr] = val + self._update_irq() + + def inject(self, addr: int, val: int, mask: int = ~0) -> None: + old = self.regs[addr] & ~mask + new = old | (val & mask) + self.regs[addr] = new + self._update_irq() + + def set_bit(self, addr: int, val: int) -> None: + self.inject(addr, val, val) + + +class TestVCNL4010(UMLTestCase): + dts = DtFragment( + src=""" +#include + +&i2c { + light-sensor@$addr$ { + compatible = "vishay,vcnl4020"; + reg = <0x$addr$>; + interrupt-parent = <&gpio>; + interrupts = <$gpio$ IRQ_TYPE_EDGE_FALLING>; + }; +}; + """, + variables={ + "addr": DtVar.I2C_ADDR, + "gpio": DtVar.GPIO_PIN, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("vcnl4000") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("vcnl4000") + + def setUp(self) -> None: + self.driver = I2CDriver("vcnl4000") + self.hw = Hardware("i2c") + self.hw.load_model(VCNL4010, int=self.dts["gpio"]) + + def tearDown(self) -> None: + self.hw.close() + + def test_lux(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + + scale = read_float(dev.path / "iio:device0/in_illuminance_scale") + self.assertEqual(scale, 0.25) + + data = [ + (0x00, 0x00), + (0x12, 0x34), + (0xFF, 0xFF), + ] + luxfile = dev.path / "iio:device0/in_illuminance_raw" + for high, low in data: + self.hw.inject(REG_ALS_RESULT_HIGH, high) + self.hw.inject(REG_ALS_RESULT_LOW, low) + self.hw.set_bit(REG_COMMAND, REG_COMMAND_ALS_DATA_RDY) + + self.assertEqual(read_int(luxfile), high << 8 | low) + + def test_lux_timeout(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + with self.assertRaises(OSError) as cm: + luxfile = dev.path / "iio:device0/in_illuminance_raw" + read_str(luxfile) + self.assertEqual(cm.exception.errno, errno.EIO) + + def test_proximity_thresh_rising(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + high_thresh = ( + dev.path / "iio:device0/events/in_proximity_thresh_rising_value" + ) + write_int(high_thresh, 0x1234) + + mock = self.hw.update_mock() + mock.assert_last_reg_write(self, REG_HIGH_THRESHOLD_HIGH, 0x12) + mock.assert_last_reg_write(self, REG_HIGH_THRESHOLD_LOW, 0x34) + mock.reset_mock() + + self.assertEqual(read_int(high_thresh), 0x1234) + + with iio.IIOEventMonitor("/dev/iio:device0") as mon: + en = dev.path / "iio:device0/events/in_proximity_thresh_either_en" + write_int(en, 1) + + self.hw.inject(REG_PROX_RESULT_HIGH, 0x12) + self.hw.inject(REG_PROX_RESULT_LOW, 0x35) + self.hw.set_bit(REG_COMMAND, REG_COMMAND_PROX_DATA_RDY) + self.hw.kick() + + self.assertEqual(read_int(en), 1) + + event = mon.read() + self.assertEqual(event.type, iio.IIOChanType.IIO_PROXIMITY) + + def test_proximity_thresh_falling(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + high_thresh = ( + dev.path / "iio:device0/events/in_proximity_thresh_falling_value" + ) + write_int(high_thresh, 0x0ABC) + + mock = self.hw.update_mock() + mock.assert_last_reg_write(self, REG_LOW_THRESHOLD_HIGH, 0x0A) + mock.assert_last_reg_write(self, REG_LOW_THRESHOLD_LOW, 0xBC) + mock.reset_mock() + + self.assertEqual(read_int(high_thresh), 0x0ABC) + + with iio.IIOEventMonitor("/dev/iio:device0") as mon: + write_int( + dev.path / "iio:device0/events/in_proximity_thresh_either_en", 1 + ) + + event = mon.read() + self.assertEqual(event.type, iio.IIOChanType.IIO_PROXIMITY) + + def test_proximity_triggered(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + data = [ + (0x00, 0x00, 0), + (0x00, 0x01, 1), + (0xF0, 0x02, 0xF002), + (0xFF, 0xFF, 0xFFFF), + ] + + trigger = read_str(Path("/sys/bus/iio/devices/trigger0/name")) + + write_int(dev.path / "iio:device0/buffer0/in_proximity_en", 1) + write_str(dev.path / "iio:device0/trigger/current_trigger", trigger) + + with iio.IIOBuffer("/dev/iio:device0", bufidx=0) as buffer: + write_int(dev.path / "iio:device0/buffer0/length", 128) + write_int(dev.path / "iio:device0/buffer0/enable", 1) + + for low, high, expected in data: + self.hw.inject(REG_PROX_RESULT_HIGH, low) + self.hw.inject(REG_PROX_RESULT_LOW, high) + self.hw.set_bit(REG_COMMAND, REG_COMMAND_PROX_DATA_RDY) + self.hw.kick() + + scanline = buffer.read("H") + + val = scanline[0] + self.assertEqual(val, expected) diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4040.py b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4040.py new file mode 100644 index 000000000000..f2aa2cb9f3d5 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4040.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import logging +from typing import Any + +from roadtest.backend.i2c import SMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import I2CDriver, read_float, read_int + +logger = logging.getLogger(__name__) + + +class VCNL4040(SMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__(regbytes=2, byteorder="little", **kwargs) + self.regs = { + 0x00: 0x0101, + 0x01: 0x0000, + 0x02: 0x0000, + 0x03: 0x0001, + 0x04: 0x0000, + 0x05: 0x0000, + 0x06: 0x0000, + 0x07: 0x0000, + 0x08: 0x0000, + 0x09: 0x0000, + 0x0A: 0x0000, + 0x0A: 0x0000, + 0x0B: 0x0000, + 0x0C: 0x0186, + # The driver reads this register which is undefined for + # VCNL4040. Perhaps the driver should be fixed instead + # of having this here? + 0x0E: 0x0000, + } + + def reg_read(self, addr: int) -> int: + return self.regs[addr] + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] = val + + +class TestVCNL4040(UMLTestCase): + dts = DtFragment( + src=""" +&i2c { + light-sensor@$addr$ { + compatible = "vishay,vcnl4040"; + reg = <0x$addr$>; + }; +}; + """, + variables={ + "addr": DtVar.I2C_ADDR, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("vcnl4000") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("vcnl4000") + + def setUp(self) -> None: + self.driver = I2CDriver("vcnl4000") + self.hw = Hardware("i2c") + self.hw.load_model(VCNL4040) + + def tearDown(self) -> None: + self.hw.close() + + def test_illuminance_scale(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + scalefile = dev.path / "iio:device0/in_illuminance_scale" + # The datasheet says 0.10 lux/step, but the driver follows + # the application note "Designing the VCNL4040 Into an + # Application" which claims a different value. + self.assertEqual(read_float(scalefile), 0.12) + + def test_illuminance(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + luxfile = dev.path / "iio:device0/in_illuminance_raw" + + data = [0x0000, 0x1234, 0xFFFF] + for regval in data: + self.hw.reg_write(0x09, regval) + self.assertEqual(read_int(luxfile), regval) + + def test_proximity(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + rawfile = dev.path / "iio:device0/in_proximity_raw" + + data = [0x0000, 0x1234, 0xFFFF] + for regval in data: + self.hw.reg_write(0x08, regval) + self.assertEqual(read_int(rawfile), regval) diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4200.py b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4200.py new file mode 100644 index 000000000000..d1cf819e563e --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4200.py @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import logging +from typing import Any + +from roadtest.backend.i2c import SMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import I2CDriver, read_float, read_int + +logger = logging.getLogger(__name__) + + +class VCNL4200(SMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__(regbytes=2, byteorder="little", **kwargs) + self.regs = { + 0x00: 0x0101, + 0x01: 0x0000, + 0x02: 0x0000, + 0x03: 0x0001, + 0x04: 0x0000, + 0x05: 0x0000, + 0x06: 0x0000, + 0x07: 0x0000, + 0x08: 0x0000, + 0x09: 0x0000, + 0x0A: 0x0000, + 0x0D: 0x0000, + 0x0E: 0x1058, + } + + def reg_read(self, addr: int) -> int: + return self.regs[addr] + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] = val + + +class TestVCNL4200(UMLTestCase): + dts = DtFragment( + src=""" +&i2c { + light-sensor@$addr$ { + compatible = "vishay,vcnl4200"; + reg = <0x$addr$>; + }; +}; + """, + variables={ + "addr": DtVar.I2C_ADDR, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("vcnl4000") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("vcnl4000") + + def setUp(self) -> None: + self.driver = I2CDriver("vcnl4000") + self.hw = Hardware("i2c") + self.hw.load_model(VCNL4200) + + def tearDown(self) -> None: + self.hw.close() + + def test_illuminance_scale(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + scalefile = dev.path / "iio:device0/in_illuminance_scale" + self.assertEqual(read_float(scalefile), 0.024) + + def test_illuminance(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + luxfile = dev.path / "iio:device0/in_illuminance_raw" + + data = [0x0000, 0x1234, 0xFFFF] + for regval in data: + self.hw.reg_write(0x09, regval) + self.assertEqual(read_int(luxfile), regval) + + def test_proximity(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + rawfile = dev.path / "iio:device0/in_proximity_raw" + + data = [0x0000, 0x1234, 0xFFFF] + for regval in data: + self.hw.reg_write(0x08, regval) + self.assertEqual(read_int(rawfile), regval) From patchwork Fri Mar 11 16:24:44 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Vincent Whitchurch X-Patchwork-Id: 550596 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 1278EC433FE for ; Fri, 11 Mar 2022 16:25:55 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S243843AbiCKQ0y (ORCPT ); Fri, 11 Mar 2022 11:26:54 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:38336 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350350AbiCKQ0g (ORCPT ); Fri, 11 Mar 2022 11:26:36 -0500 Received: from smtp2.axis.com (smtp2.axis.com [195.60.68.18]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id B71C21BA171; Fri, 11 Mar 2022 08:25:01 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015903; x=1678551903; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=sxaJ4mG3Y2RIBWG3Mim/tcF1834/P18PDxXFWE4XQiY=; b=S6cxUV352uZY6mEk/LFIbYpc7U3IlpNM/SuQGwlNiurHrKoN4p6Yll4q 5RyRmGmz8TxtO7roqPQ+iXQzcyCRJlKN0tA2SXGlQYeAMI+zrLLOFqhHS HwIRmyTFsdWTSdOBfUEXGALC+GMHd3hGocixn/Uvq1dKSzGl9rdvslvTR 9J6b/kqnUNvu+exzvpmQyb6I7pFef4CMFYtpMeiRVnoK0sUT8w7x97jrB mdtpqfqfzew2ErgnEqgWHyoeugnUshKAUEo5tdeuoWjg08XTh35viln9u zQNWoDXHXitvt5GMWGW9vkV4r5z6sDjgb8blvQE7QGTsQ21xO2hCgNVE4 Q==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 09/10] regulator: tps62864: add roadtest Date: Fri, 11 Mar 2022 17:24:44 +0100 Message-ID: <20220311162445.346685-10-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: devicetree@vger.kernel.org Add a roadtest for the recently-added tps62864 regulator driver. It tests voltage setting, mode setting, as well as devicetree mode translation. It uses the recently-added devicetree support in regulator-virtual-consumer. All the variants supported by the driver have identical register interfaces so only one test/model is added. It requires the following patches which are, as of writing, not in mainline: - regulator: Add support for TPS6286x https://lore.kernel.org/lkml/20220204155241.576342-3-vincent.whitchurch@axis.com/ - regulator: virtual: add devicetree support https://lore.kernel.org/lkml/20220301111831.3742383-4-vincent.whitchurch@axis.com/ Signed-off-by: Vincent Whitchurch --- .../roadtest/tests/regulator/__init__.py | 0 .../roadtest/roadtest/tests/regulator/config | 4 + .../roadtest/tests/regulator/test_tps62864.py | 187 ++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 tools/testing/roadtest/roadtest/tests/regulator/__init__.py create mode 100644 tools/testing/roadtest/roadtest/tests/regulator/config create mode 100644 tools/testing/roadtest/roadtest/tests/regulator/test_tps62864.py diff --git a/tools/testing/roadtest/roadtest/tests/regulator/__init__.py b/tools/testing/roadtest/roadtest/tests/regulator/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/testing/roadtest/roadtest/tests/regulator/config b/tools/testing/roadtest/roadtest/tests/regulator/config new file mode 100644 index 000000000000..b2b503947e70 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/regulator/config @@ -0,0 +1,4 @@ +CONFIG_REGULATOR=y +CONFIG_REGULATOR_DEBUG=y +CONFIG_REGULATOR_VIRTUAL_CONSUMER=y +CONFIG_REGULATOR_TPS6286X=m diff --git a/tools/testing/roadtest/roadtest/tests/regulator/test_tps62864.py b/tools/testing/roadtest/roadtest/tests/regulator/test_tps62864.py new file mode 100644 index 000000000000..f7db4293d840 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/regulator/test_tps62864.py @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +from typing import Any, Final + +from roadtest.backend.i2c import SimpleSMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import ( + I2CDriver, + PlatformDriver, + read_str, + write_int, + write_str, +) + +REG_VOUT1: Final = 0x01 +REG_VOUT2: Final = 0x02 +REG_CONTROL: Final = 0x03 +REG_STATUS: Final = 0x05 + + +class TPS62864(SimpleSMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__( + # From datasheet section 8.6 Register map + # XXX does not match reality -- recheck + regs={ + REG_VOUT1: 0x64, + REG_VOUT2: 0x64, + REG_CONTROL: 0x00, + REG_STATUS: 0x00, + }, + regbytes=1, + **kwargs, + ) + + +class TestTPS62864(UMLTestCase): + dts = DtFragment( + src=""" +#include + +&i2c { + regulator@$normal$ { + compatible = "ti,tps62864"; + reg = <0x$normal$>; + + regulators { + tps62864_normal: SW { + regulator-name = "+0.85V"; + regulator-min-microvolt = <400000>; + regulator-max-microvolt = <1675000>; + regulator-allowed-modes = ; + }; + }; + }; + + regulator@$fpwm$ { + compatible = "ti,tps62864"; + reg = <0x$fpwm$>; + + regulators { + tps62864_fpwm: SW { + regulator-name = "+0.85V"; + regulator-min-microvolt = <400000>; + regulator-max-microvolt = <1675000>; + regulator-initial-mode = ; + }; + }; + }; +}; + +/ { + tps62864_normal_consumer { + compatible = "regulator-virtual-consumer"; + default-supply = <&tps62864_normal>; + }; + + tps62864_fpwm_consumer { + compatible = "regulator-virtual-consumer"; + default-supply = <&tps62864_fpwm>; + }; +}; + """, + variables={ + "normal": DtVar.I2C_ADDR, + "fpwm": DtVar.I2C_ADDR, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("tps6286x-regulator") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("tps6286x-regulator") + + def setUp(self) -> None: + self.driver = I2CDriver("tps6286x") + self.hw = Hardware("i2c") + self.hw.load_model(TPS62864) + + def tearDown(self) -> None: + self.hw.close() + + def test_voltage(self) -> None: + with ( + self.driver.bind(self.dts["normal"]), + PlatformDriver("reg-virt-consumer").bind( + "tps62864_normal_consumer" + ) as consumerdev, + ): + maxfile = consumerdev.path / "max_microvolts" + minfile = consumerdev.path / "min_microvolts" + + write_int(maxfile, 1675000) + write_int(minfile, 800000) + + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 1 << 5) + mock.assert_reg_write_once(self, REG_VOUT1, 0x50) + mock.reset_mock() + + mV = 1000 + data = [ + (400 * mV, 0x00), + (900 * mV, 0x64), + (1675 * mV, 0xFF), + ] + + for voltage, val in data: + write_int(minfile, voltage) + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_VOUT1, val) + mock.reset_mock() + + write_int(minfile, 0) + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 0) + mock.reset_mock() + + def test_modes(self) -> None: + with ( + self.driver.bind(self.dts["normal"]), + PlatformDriver("reg-virt-consumer").bind( + "tps62864_normal_consumer" + ) as consumerdev, + ): + modefile = consumerdev.path / "mode" + write_str(modefile, "fast") + + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 1 << 4) + mock.reset_mock() + + write_str(modefile, "normal") + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 0) + mock.reset_mock() + + def test_dt_force_pwm(self) -> None: + with ( + self.driver.bind(self.dts["fpwm"]), + PlatformDriver("reg-virt-consumer").bind( + "tps62864_fpwm_consumer" + ) as consumerdev, + ): + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 1 << 4) + mock.reset_mock() + + modefile = consumerdev.path / "mode" + self.assertEquals(read_str(modefile), "fast") + + maxfile = consumerdev.path / "max_microvolts" + minfile = consumerdev.path / "min_microvolts" + + write_int(maxfile, 1675000) + write_int(minfile, 800000) + + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 1 << 5 | 1 << 4) + mock.reset_mock() From patchwork Fri Mar 11 16:24:45 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Vincent Whitchurch X-Patchwork-Id: 550593 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 09B69C433EF for ; Fri, 11 Mar 2022 16:28:10 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1344543AbiCKQ3J (ORCPT ); Fri, 11 Mar 2022 11:29:09 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:38126 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350386AbiCKQ0r (ORCPT ); Fri, 11 Mar 2022 11:26:47 -0500 Received: from smtp2.axis.com (smtp2.axis.com [195.60.68.18]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 22B3E1D21E0; Fri, 11 Mar 2022 08:25:10 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015914; x=1678551914; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=mFUerXsYecLMbfF90aqK9kLZr/AsxHAsijhoN1YRpYE=; b=lQRk4LfR5AFqN9FEs7wrHk5cH5T9+y25qVLXzvBvAFYLBPTgRsjD1Yqh D2BB7TQBH8uN8bFQAZndlaN4Hj0QSlVPi7YuVlnEQzq7cFtz1tJ0G1F0N iim2PI/3jeCKvDVbVCa5gMFaQoMW0pfNcobauE8P1Q2CajDrQWsmsdCNz 2/1is0aMahXVPp6RQd2NPCBPAFkAx3q4uF1YXfCt0Y1p66ENX7//EakTL jzB8nQwB/wx6LXXn8fZaZEftxV+oYE44Xt83uFkmteszNsuBTv/sS9Lg1 XI1BEwDCGs3z+soJJ7z4sq02aLv1guD+2iG48gfEwTQT39ZJuBwgfIxn+ A==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 10/10] rtc: pcf8563: add roadtest Date: Fri, 11 Mar 2022 17:24:45 +0100 Message-ID: <20220311162445.346685-11-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: devicetree@vger.kernel.org Add a roadtest for the PCF8563 RTC driver, testing many of the features including alarm and invalid time handling. Since it's the first roadtest for RTC, some helper code for handling the ABI is included. The following fixes were posted for problems identified during development of these tests: - rtc: fix use-after-free on device removal https://lore.kernel.org/lkml/20211210160951.7718-1-vincent.whitchurch@axis.com/ - rtc: pcf8563: clear RTC_FEATURE_ALARM if no irq https://lore.kernel.org/lkml/20220301131220.4011810-1-vincent.whitchurch@axis.com/ - rtc: pcf8523: fix alarm interrupt disabling https://lore.kernel.org/lkml/20211103152253.22844-1-vincent.whitchurch@axis.com/ (not the same hardware/driver, but this was the original target for test development) Signed-off-by: Vincent Whitchurch --- .../roadtest/roadtest/tests/rtc/__init__.py | 0 .../roadtest/roadtest/tests/rtc/config | 1 + .../roadtest/roadtest/tests/rtc/rtc.py | 73 ++++ .../roadtest/tests/rtc/test_pcf8563.py | 348 ++++++++++++++++++ 4 files changed, 422 insertions(+) create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/__init__.py create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/config create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/rtc.py create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py diff --git a/tools/testing/roadtest/roadtest/tests/rtc/__init__.py b/tools/testing/roadtest/roadtest/tests/rtc/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/testing/roadtest/roadtest/tests/rtc/config b/tools/testing/roadtest/roadtest/tests/rtc/config new file mode 100644 index 000000000000..f3654f9d7c19 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/rtc/config @@ -0,0 +1 @@ +CONFIG_RTC_DRV_PCF8563=m diff --git a/tools/testing/roadtest/roadtest/tests/rtc/rtc.py b/tools/testing/roadtest/roadtest/tests/rtc/rtc.py new file mode 100644 index 000000000000..1a2855bfc195 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/rtc/rtc.py @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import contextlib +import fcntl +import struct +import typing +from pathlib import Path +from typing import Any, cast + +RTC_RD_TIME = 0x80247009 +RTC_SET_TIME = 0x4024700A +RTC_WKALM_SET = 0x4028700F +RTC_VL_READ = 0x80047013 + +RTC_IRQF = 0x80 +RTC_AF = 0x20 + +RTC_VL_DATA_INVALID = 1 << 0 + + +class RTCTime(typing.NamedTuple): + tm_sec: int + tm_min: int + tm_hour: int + tm_mday: int + tm_mon: int + tm_year: int + tm_wday: int + tm_yday: int + tm_isdst: int + + +class RTC(contextlib.AbstractContextManager): + def __init__(self, devpath: Path) -> None: + rtc = next(devpath.glob("rtc/rtc*")).name + self.filename = f"/dev/{rtc}" + + def __enter__(self) -> "RTC": + self.file = open(self.filename, "rb") + return self + + def __exit__(self, *_: Any) -> None: + self.file.close() + + def read_time(self) -> RTCTime: + s = struct.Struct("9i") + buf = bytearray(s.size) + fcntl.ioctl(self.file.fileno(), RTC_RD_TIME, buf) + return RTCTime._make(s.unpack(buf)) + + def set_time(self, tm: RTCTime) -> int: + s = struct.Struct("9i") + buf = bytearray(s.size) + s.pack_into(buf, 0, *tm) + return fcntl.ioctl(self.file.fileno(), RTC_SET_TIME, buf) + + def set_wake_alarm(self, enabled: bool, time: RTCTime) -> int: + s = struct.Struct("2B9i") + buf = bytearray(s.size) + s.pack_into(buf, 0, enabled, False, *time) + return fcntl.ioctl(self.file.fileno(), RTC_WKALM_SET, buf) + + def read(self) -> int: + s = struct.Struct("L") + buf = self.file.read(s.size) + return cast(int, s.unpack(buf)[0]) + + def read_vl(self) -> int: + s = struct.Struct("I") + buf = bytearray(s.size) + fcntl.ioctl(self.file.fileno(), RTC_VL_READ, buf) + return cast(int, s.unpack(buf)[0]) diff --git a/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py b/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py new file mode 100644 index 000000000000..a9f4c6d92762 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py @@ -0,0 +1,348 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import errno +import logging +from typing import Any, Final, Optional + +from roadtest.backend.i2c import I2CModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import I2CDriver + +from . import rtc + +logger = logging.getLogger(__name__) + +REG_CONTROL_STATUS_1: Final = 0x00 +REG_CONTROL_STATUS_2: Final = 0x01 +REG_VL_SECONDS: Final = 0x02 +REG_VL_MINUTES: Final = 0x03 +REG_VL_HOURS: Final = 0x04 +REG_VL_DAYS: Final = 0x05 +REG_VL_WEEKDAYS: Final = 0x06 +REG_VL_CENTURY_MONTHS: Final = 0x07 +REG_VL_YEARS: Final = 0x08 +REG_VL_MINUTE_ALARM: Final = 0x09 +REG_VL_HOUR_ALARM: Final = 0x0A +REG_VL_DAY_ALARM: Final = 0x0B +REG_VL_WEEKDAY_ALARM: Final = 0x0C +REG_CLKOUT_CONTROL: Final = 0x0D +REG_TIMER_CONTROL: Final = 0x0E +REG_TIMER: Final = 0x0F + +REG_CONTROL_STATUS_2_AIE: Final = 1 << 1 +REG_CONTROL_STATUS_2_AF: Final = 1 << 3 + +REG_VL_CENTURY_MONTHS_C: Final = 1 << 7 + +REG_VL_ALARM_AE: Final = 1 << 7 + + +class PCF8563(I2CModel): + def __init__(self, int: Optional[int] = None, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.int = int + self._set_int(False) + + self.reg_addr = 0 + # Reset values from Table 27 in datasheet, with X and - bits set to 0 + self.regs = { + REG_CONTROL_STATUS_1: 0b_0000_1000, + REG_CONTROL_STATUS_2: 0b_0000_0000, + REG_VL_SECONDS: 0b_1000_0000, + REG_VL_MINUTES: 0b_0000_0000, + REG_VL_HOURS: 0b_0000_0000, + REG_VL_DAYS: 0b_0000_0000, + REG_VL_WEEKDAYS: 0b_0000_0000, + REG_VL_CENTURY_MONTHS: 0b_0000_0000, + REG_VL_YEARS: 0b_0000_0000, + REG_VL_MINUTE_ALARM: 0b_1000_0000, + REG_VL_HOUR_ALARM: 0b_1000_0000, + REG_VL_DAY_ALARM: 0b_1000_0000, + REG_VL_WEEKDAY_ALARM: 0b_1000_0000, + REG_CLKOUT_CONTROL: 0b_1000_0000, + REG_TIMER_CONTROL: 0b_0000_0011, + REG_TIMER: 0b_0000_0000, + } + + def _set_int(self, active: int) -> None: + # Active-low + self.backend.gpio.set(self.int, not active) + + def _check_alarm(self, addr: int) -> None: + alarmregs = [ + REG_VL_MINUTE_ALARM, + REG_VL_HOUR_ALARM, + REG_VL_DAY_ALARM, + REG_VL_WEEKDAY_ALARM, + ] + timeregs = [ + REG_VL_MINUTES, + REG_VL_HOURS, + REG_VL_DAYS, + REG_VL_WEEKDAYS, + ] + + if addr not in alarmregs + timeregs: + return + + af = all( + self.regs[a] == self.regs[b] + for a, b in zip(alarmregs, timeregs) + if not self.regs[a] & REG_VL_ALARM_AE + ) + self.reg_write(REG_CONTROL_STATUS_2, self.regs[REG_CONTROL_STATUS_2] | af << 3) + + def _update_irq(self) -> None: + aie = self.regs[REG_CONTROL_STATUS_2] & REG_CONTROL_STATUS_2_AIE + af = self.regs[REG_CONTROL_STATUS_2] & REG_CONTROL_STATUS_2_AF + + logger.debug(f"{aie=} {af=}") + self._set_int(aie and af) + + def reg_read(self, addr: int) -> int: + val = self.regs[addr] + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] = val + logger.debug(f"{addr=:x} {val=:x}") + self._check_alarm(addr) + self._update_irq() + + def read(self, len: int) -> bytes: + data = bytearray(len) + + for i in range(len): + data[i] = self.reg_read(self.reg_addr) + self.reg_addr = self.reg_addr + 1 + + return bytes(data) + + def write(self, data: bytes) -> None: + self.reg_addr = data[0] + + for i, byte in enumerate(data[1:]): + addr = self.reg_addr + i + self.backend.mock.reg_write(addr, byte) + self.reg_write(addr, byte) + + +class TestPCF8563(UMLTestCase): + dts = DtFragment( + src=""" +#include + +&i2c { + rtc@$addr$ { + compatible = "nxp,pcf8563"; + reg = <0x$addr$>; + }; + + rtc@$irqaddr$ { + compatible = "nxp,pcf8563"; + reg = <0x$irqaddr$>; + interrupt-parent = <&gpio>; + interrupts = <$gpio$ IRQ_TYPE_LEVEL_LOW>; + }; +}; + """, + variables={ + "addr": DtVar.I2C_ADDR, + "irqaddr": DtVar.I2C_ADDR, + "gpio": DtVar.GPIO_PIN, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("rtc-pcf8563") + + @classmethod + def tearDownClass(cls) -> None: + # Can't rmmod since alarmtimer holds permanent reference + pass + + def setUp(self) -> None: + self.driver = I2CDriver("rtc-pcf8563") + self.hw = Hardware("i2c") + self.hw.load_model(PCF8563, int=self.dts["gpio"]) + + def tearDown(self) -> None: + self.hw.close() + + def test_read_time_invalid(self) -> None: + addr = self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + self.assertEqual(rtcdev.read_vl(), rtc.RTC_VL_DATA_INVALID) + + with self.assertRaises(OSError) as cm: + rtcdev.read_time() + self.assertEqual(cm.exception.errno, errno.EINVAL) + + def test_no_alarm_support(self) -> None: + addr = self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + # Make sure the times are valid so we don't get -EINVAL due to + # that. + tm = rtc.RTCTime( + tm_sec=10, + tm_min=1, + tm_hour=1, + tm_mday=1, + tm_mon=0, + tm_year=121, + tm_wday=0, + tm_yday=0, + tm_isdst=0, + ) + rtcdev.set_time(tm) + + alarmtm = tm._replace(tm_sec=0, tm_min=2) + with self.assertRaises(OSError) as cm: + rtcdev.set_wake_alarm(True, alarmtm) + self.assertEqual(cm.exception.errno, errno.EINVAL) + + def test_alarm(self) -> None: + addr = self.dts["irqaddr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + tm = rtc.RTCTime( + tm_sec=10, + tm_min=1, + tm_hour=1, + tm_mday=1, + tm_mon=0, + tm_year=121, + tm_wday=5, + tm_yday=0, + tm_isdst=0, + ) + rtcdev.set_time(tm) + + alarmtm = tm._replace(tm_sec=0, tm_min=2) + rtcdev.set_wake_alarm(True, alarmtm) + + mock = self.hw.update_mock() + mock.assert_last_reg_write(self, REG_VL_MINUTE_ALARM, 0x02) + mock.assert_last_reg_write(self, REG_VL_HOUR_ALARM, 0x01) + mock.assert_last_reg_write(self, REG_VL_DAY_ALARM, 0x01) + mock.assert_last_reg_write(self, REG_VL_WEEKDAY_ALARM, 5) + mock.assert_last_reg_write( + self, REG_CONTROL_STATUS_2, REG_CONTROL_STATUS_2_AIE + ) + mock.reset_mock() + + self.hw.reg_write(REG_VL_MINUTES, 0x02) + self.hw.kick() + + # This waits for the interrupt + self.assertEqual(rtcdev.read() & 0xFF, rtc.RTC_IRQF | rtc.RTC_AF) + + alarmtm = tm._replace(tm_sec=0, tm_min=3) + rtcdev.set_wake_alarm(False, alarmtm) + + mock = self.hw.update_mock() + mock.assert_last_reg_write(self, REG_CONTROL_STATUS_2, 0) + + def test_read_time_valid(self) -> None: + self.hw.reg_write(REG_VL_SECONDS, 0x37) + self.hw.reg_write(REG_VL_MINUTES, 0x10) + self.hw.reg_write(REG_VL_HOURS, 0x11) + self.hw.reg_write(REG_VL_DAYS, 0x25) + self.hw.reg_write(REG_VL_WEEKDAYS, 0x00) + self.hw.reg_write(REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C | 0x12) + self.hw.reg_write(REG_VL_YEARS, 0x21) + + addr = self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + tm = rtcdev.read_time() + self.assertEqual( + tm, + rtc.RTCTime( + tm_sec=37, + tm_min=10, + tm_hour=11, + tm_mday=25, + tm_mon=11, + tm_year=121, + tm_wday=0, + tm_yday=0, + tm_isdst=0, + ), + ) + + def test_set_time_after_invalid(self) -> None: + addr = self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + self.assertEqual(rtcdev.read_vl(), rtc.RTC_VL_DATA_INVALID) + + tm = rtc.RTCTime( + tm_sec=37, + tm_min=10, + tm_hour=11, + tm_mday=25, + tm_mon=11, + tm_year=121, + tm_wday=0, + tm_yday=0, + tm_isdst=0, + ) + + rtcdev.set_time(tm) + tm2 = rtcdev.read_time() + self.assertEqual(tm, tm2) + + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_VL_SECONDS, 0x37) + mock.assert_reg_write_once(self, REG_VL_MINUTES, 0x10) + mock.assert_reg_write_once(self, REG_VL_HOURS, 0x11) + mock.assert_reg_write_once(self, REG_VL_DAYS, 0x25) + mock.assert_reg_write_once(self, REG_VL_WEEKDAYS, 0x00) + # The driver uses the wrong polarity of the Century bit + # if the time was invalid. This probably doesn't matter(?). + mock.assert_reg_write_once(self, REG_VL_CENTURY_MONTHS, 0 << 7 | 0x12) + mock.assert_reg_write_once(self, REG_VL_YEARS, 0x21) + + self.assertEqual(rtcdev.read_vl(), 0) + + def test_set_time_after_valid(self) -> None: + self.hw.reg_write(REG_VL_SECONDS, 0x37) + self.hw.reg_write(REG_VL_MINUTES, 0x10) + self.hw.reg_write(REG_VL_HOURS, 0x11) + self.hw.reg_write(REG_VL_DAYS, 0x25) + self.hw.reg_write(REG_VL_WEEKDAYS, 0x00) + self.hw.reg_write(REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C | 0x12) + self.hw.reg_write(REG_VL_YEARS, 0x21) + + addr = self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + tm = rtc.RTCTime( + tm_sec=37, + tm_min=10, + tm_hour=11, + tm_mday=25, + tm_mon=11, + tm_year=121, + tm_wday=0, + tm_yday=0, + tm_isdst=0, + ) + + rtcdev.set_time(tm) + tm2 = rtcdev.read_time() + self.assertEqual(tm, tm2) + + mock = self.hw.update_mock() + mock.assert_reg_write_once(self, REG_VL_SECONDS, 0x37) + mock.assert_reg_write_once(self, REG_VL_MINUTES, 0x10) + mock.assert_reg_write_once(self, REG_VL_HOURS, 0x11) + mock.assert_reg_write_once(self, REG_VL_DAYS, 0x25) + mock.assert_reg_write_once(self, REG_VL_WEEKDAYS, 0x00) + mock.assert_reg_write_once( + self, REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C | 0x12 + ) + mock.assert_reg_write_once(self, REG_VL_YEARS, 0x21)