summaryrefslogtreecommitdiffstats
path: root/drivers/serial/virtio_console.c
blob: a4adb77610f6ef951b6099e4546dd8406f685c1b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2006, 2007, 2009 Rusty Russell, IBM Corporation
 * Copyright (C) 2009, 2010, 2011 Red Hat, Inc.
 * Copyright (C) 2009, 2010, 2011 Amit Shah <amit.shah@redhat.com>
 * Copyright (C) 2021 Ahmad Fatoum
 *
 * This ridiculously simple implementation does a DMA transfer for
 * every single character. On the plus side, we neither need to
 * buffer RX or to wade through TX to turn LFs to CRLFs.
 */
#include <common.h>
#include <driver.h>
#include <init.h>
#include <linux/list.h>
#include <malloc.h>
#include <console.h>
#include <xfuncs.h>
#include <linux/spinlock.h>
#include <linux/virtio.h>
#include <linux/virtio_ring.h>
#include <linux/virtio_console.h>

struct virtio_console {
	struct console_device cdev;
	struct virtqueue *in_vq, *out_vq;
	char inbuf[1];
};

static bool have_one;

/*
 * The put_chars() callback is pretty straightforward.
 *
 * We turn the characters into a scatter-gather list, add it to the
 * output queue and then kick the Host.  Then we sit here waiting for
 * it to finish: inefficient in theory, but in practice
 * implementations will do it immediately (lguest's Launcher does).
 */
static void put_chars(struct virtio_console *virtcons, const char *buf, int count)
{
	struct virtqueue *out_vq = virtcons->out_vq;
	unsigned int len;
	struct virtio_sg *sgs[1] = {
		&(struct virtio_sg) { .addr = (void *)buf, .length = count }
	};

	/*
	 * add_buf wants a token to identify this buffer: we hand it
	 * any non-NULL pointer, since there's only ever one buffer.
	 */
	if (virtqueue_add(out_vq, sgs, 1, 0) >= 0) {
		/* Tell Host to go! */
		virtqueue_kick(out_vq);
		/* Chill out until it's done with the buffer. */
		while (!virtqueue_get_buf(out_vq, &len))
			cpu_relax();
	}
}

static void virtcons_putc(struct console_device *cdev, char c)
{
	struct virtio_console *virtcons = container_of(cdev, struct virtio_console, cdev);

	return put_chars(virtcons, &c, 1);
}

/*
 * Create a scatter-gather list representing our input buffer and put
 * it in the queue.
 */
static void add_inbuf(struct virtio_console *virtcons)
{
	struct virtio_sg *sgs[1] = { &(struct virtio_sg) {
		.addr = virtcons->inbuf, .length = sizeof(virtcons->inbuf) }
	};

	/* We should always be able to add one buffer to an empty queue. */
	if (virtqueue_add(virtcons->in_vq, sgs, 0, 1) < 0)
		BUG();
	virtqueue_kick(virtcons->in_vq);
}

static int virtcons_tstc(struct console_device *cdev)
{
	struct virtio_console *virtcons = container_of(cdev, struct virtio_console, cdev);

	return virtqueue_poll(virtcons->in_vq, virtcons->in_vq->last_used_idx);
}

static int virtcons_getc(struct console_device *cdev)
{
	struct virtio_console *virtcons = container_of(cdev, struct virtio_console, cdev);
	char *in;
	int ch;

	in = virtqueue_get_buf(virtcons->in_vq, NULL);
	if (!in)
		BUG();

	ch = *in;

	add_inbuf(virtcons);

	return ch;
}

static int virtcons_probe(struct virtio_device *vdev)
{
	struct virtqueue *vqs[2];
	struct virtio_console *virtcons;
	int err;

	if (have_one) {
		/* Neither multiport consoles (one virtio_device for multiple consoles)
		 * nor multiple consoles (one virtio_device per each console
		 * is supported. I would've expected:
		 *   -chardev socket,path=/tmp/bar,server,nowait,id=bar \
		 *   -device virtconsole,chardev=bar,name=console.bar \
		 *   -device virtio-serial-device \
		 *   -chardev socket,path=/tmp/baz,server,nowait,id=baz \
		 *   -device virtconsole,chardev=baz,name=console.baz \
		 * to just work, but it doesn't
		 */
		dev_warn(&vdev->dev,
			 "Multiple virtio-console devices not supported yet\n");
		return -EEXIST;
	}

	/* Find the queues. */
	err = virtio_find_vqs(vdev, 2, vqs);
	if (err)
		return err;

	virtcons = xzalloc(sizeof(*virtcons));

	vdev->priv = virtcons;

	virtcons->in_vq = vqs[0];
	virtcons->out_vq = vqs[1];

	/* Register the input buffer the first time. */
	add_inbuf(virtcons);

	virtcons->cdev.dev = &vdev->dev;
	virtcons->cdev.tstc = virtcons_tstc;
	virtcons->cdev.getc = virtcons_getc;
	virtcons->cdev.putc = virtcons_putc;

	have_one = true;

	return console_register(&virtcons->cdev);
}

static void virtcons_remove(struct virtio_device *vdev)
{
	struct virtio_console *virtcons = vdev->priv;

	vdev->config->reset(vdev);
	console_unregister(&virtcons->cdev);
	vdev->config->del_vqs(vdev);

	free(virtcons);
}

static struct virtio_device_id id_table[] = {
	{ VIRTIO_ID_CONSOLE, VIRTIO_DEV_ANY_ID },
	{ 0 },
};

static struct virtio_driver virtio_console = {
	.driver.name =	"virtio_console",
	.id_table =	id_table,
	.probe =	virtcons_probe,
	.remove =	virtcons_remove,
};
device_virtio_driver(virtio_console);

MODULE_DESCRIPTION("Virtio console driver");
MODULE_LICENSE("GPL");