b8bc6b7954
Adds OPAL_CONSOLE_WRITE error handling and implements a call to OPAL_CONSOLE_WRITE_BUFFER_SPACE to verify if there's enough space before writing to console. This fixes serial port output getting corrupted on fast writes, like on "dmesg" output. Tested on Raptor Blackbird running powerpc64 BE kernel Reviewed by: luporl Sponsored by: Eldorado Reserach Institute (eldorado.org.br) MFC after: 1 week Differential Revision: https://reviews.freebsd.org/D29063
617 lines
14 KiB
C
617 lines
14 KiB
C
/*-
|
|
* Copyright (C) 2011,2015 by Nathan Whitehorn. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
* IN NO EVENT SHALL TOOLS GMBH BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
|
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include <sys/cdefs.h>
|
|
__FBSDID("$FreeBSD$");
|
|
|
|
#include <sys/endian.h>
|
|
#include <sys/param.h>
|
|
#include <sys/conf.h>
|
|
#include <sys/cons.h>
|
|
#include <sys/kdb.h>
|
|
#include <sys/kernel.h>
|
|
#include <sys/lock.h>
|
|
#include <sys/module.h>
|
|
#include <sys/mutex.h>
|
|
#include <sys/priv.h>
|
|
#include <sys/proc.h>
|
|
#include <sys/systm.h>
|
|
#include <sys/tty.h>
|
|
|
|
#include <vm/vm.h>
|
|
#include <vm/pmap.h>
|
|
|
|
#include <machine/bus.h>
|
|
|
|
#include <dev/ofw/openfirm.h>
|
|
#include <dev/ofw/ofw_bus.h>
|
|
#include <dev/ofw/ofw_bus_subr.h>
|
|
#include <dev/uart/uart.h>
|
|
#include <dev/uart/uart_cpu.h>
|
|
#include <dev/uart/uart_bus.h>
|
|
|
|
#include "opal.h"
|
|
#include "uart_if.h"
|
|
|
|
struct uart_opal_softc {
|
|
device_t dev;
|
|
phandle_t node;
|
|
int vtermid;
|
|
|
|
struct tty *tp;
|
|
struct resource *irqres;
|
|
int irqrid;
|
|
struct callout callout;
|
|
void *sc_icookie;
|
|
int polltime;
|
|
|
|
struct mtx sc_mtx;
|
|
int protocol;
|
|
|
|
char opal_inbuf[16];
|
|
uint64_t inbuflen;
|
|
uint8_t outseqno;
|
|
#if defined(KDB)
|
|
int alt_break_state;
|
|
#endif
|
|
};
|
|
|
|
static struct uart_opal_softc *console_sc = NULL;
|
|
static struct consdev *stdout_cp;
|
|
|
|
enum {
|
|
OPAL_RAW, OPAL_HVSI
|
|
};
|
|
|
|
#define VS_DATA_PACKET_HEADER 0xff
|
|
#define VS_CONTROL_PACKET_HEADER 0xfe
|
|
#define VSV_SET_MODEM_CTL 0x01
|
|
#define VSV_MODEM_CTL_UPDATE 0x02
|
|
#define VSV_RENEGOTIATE_CONNECTION 0x03
|
|
#define VS_QUERY_PACKET_HEADER 0xfd
|
|
#define VSV_SEND_VERSION_NUMBER 0x01
|
|
#define VSV_SEND_MODEM_CTL_STATUS 0x02
|
|
#define VS_QUERY_RESPONSE_PACKET_HEADER 0xfc
|
|
|
|
static int uart_opal_probe(device_t dev);
|
|
static int uart_opal_attach(device_t dev);
|
|
static void uart_opal_intr(void *v);
|
|
|
|
static device_method_t uart_opal_methods[] = {
|
|
/* Device interface */
|
|
DEVMETHOD(device_probe, uart_opal_probe),
|
|
DEVMETHOD(device_attach, uart_opal_attach),
|
|
|
|
DEVMETHOD_END
|
|
};
|
|
|
|
static driver_t uart_opal_driver = {
|
|
"uart",
|
|
uart_opal_methods,
|
|
sizeof(struct uart_opal_softc),
|
|
};
|
|
|
|
DRIVER_MODULE(uart_opal, opalcons, uart_opal_driver, uart_devclass, 0, 0);
|
|
|
|
static int uart_opal_getc(struct uart_opal_softc *sc);
|
|
static cn_probe_t uart_opal_cnprobe;
|
|
static cn_init_t uart_opal_cninit;
|
|
static cn_term_t uart_opal_cnterm;
|
|
static cn_getc_t uart_opal_cngetc;
|
|
static cn_putc_t uart_opal_cnputc;
|
|
static cn_grab_t uart_opal_cngrab;
|
|
static cn_ungrab_t uart_opal_cnungrab;
|
|
|
|
CONSOLE_DRIVER(uart_opal);
|
|
|
|
static void uart_opal_ttyoutwakeup(struct tty *tp);
|
|
|
|
static struct ttydevsw uart_opal_tty_class = {
|
|
.tsw_flags = TF_INITLOCK|TF_CALLOUT,
|
|
.tsw_outwakeup = uart_opal_ttyoutwakeup,
|
|
};
|
|
|
|
static struct {
|
|
char tmpbuf[16];
|
|
uint64_t size;
|
|
struct mtx mtx;
|
|
} opalcons_buffer;
|
|
|
|
static void
|
|
uart_opal_real_map_outbuffer(uint64_t *bufferp, uint64_t *lenp)
|
|
{
|
|
|
|
if (!mtx_initialized(&opalcons_buffer.mtx))
|
|
mtx_init(&opalcons_buffer.mtx, "uart_opal", NULL,
|
|
MTX_SPIN | MTX_QUIET | MTX_NOWITNESS);
|
|
|
|
if (!pmap_bootstrapped)
|
|
return;
|
|
|
|
mtx_lock_spin(&opalcons_buffer.mtx);
|
|
|
|
opalcons_buffer.size = *(uint64_t *)(*lenp) =
|
|
min(sizeof(opalcons_buffer.tmpbuf), *(uint64_t *)(*lenp));
|
|
memcpy(opalcons_buffer.tmpbuf, (void *)(*bufferp),
|
|
*(uint64_t *)(*lenp));
|
|
*bufferp = (uint64_t)opalcons_buffer.tmpbuf;
|
|
*lenp = (uint64_t)&opalcons_buffer.size;
|
|
}
|
|
|
|
static void
|
|
uart_opal_real_unmap_outbuffer(uint64_t *len)
|
|
{
|
|
|
|
if (!pmap_bootstrapped)
|
|
return;
|
|
|
|
mtx_assert(&opalcons_buffer.mtx, MA_OWNED);
|
|
*len = opalcons_buffer.size;
|
|
mtx_unlock_spin(&opalcons_buffer.mtx);
|
|
}
|
|
|
|
static int64_t
|
|
uart_opal_console_write_buffer_space(int vtermid)
|
|
{
|
|
int64_t buffer_space_val = 0;
|
|
vm_paddr_t buffer_space_ptr;
|
|
|
|
if (pmap_bootstrapped)
|
|
buffer_space_ptr = vtophys(&buffer_space_val);
|
|
else
|
|
buffer_space_ptr = (vm_paddr_t)&buffer_space_val;
|
|
|
|
if (opal_call(OPAL_CONSOLE_WRITE_BUFFER_SPACE, vtermid,
|
|
buffer_space_ptr) != OPAL_SUCCESS)
|
|
return (-1);
|
|
|
|
return (be64toh(buffer_space_val));
|
|
}
|
|
|
|
static int
|
|
uart_opal_probe_node(struct uart_opal_softc *sc)
|
|
{
|
|
phandle_t node = sc->node;
|
|
uint32_t reg;
|
|
char buf[64];
|
|
|
|
sc->inbuflen = 0;
|
|
sc->outseqno = 0;
|
|
|
|
if (OF_getprop(node, "device_type", buf, sizeof(buf)) <= 0)
|
|
return (ENXIO);
|
|
if (strcmp(buf, "serial") != 0)
|
|
return (ENXIO);
|
|
|
|
reg = -1;
|
|
OF_getencprop(node, "reg", ®, sizeof(reg));
|
|
if (reg == -1)
|
|
return (ENXIO);
|
|
sc->vtermid = reg;
|
|
sc->node = node;
|
|
|
|
if (OF_getprop(node, "compatible", buf, sizeof(buf)) <= 0)
|
|
return (ENXIO);
|
|
if (strcmp(buf, "ibm,opal-console-raw") == 0) {
|
|
sc->protocol = OPAL_RAW;
|
|
return (0);
|
|
} else if (strcmp(buf, "ibm,opal-console-hvsi") == 0) {
|
|
sc->protocol = OPAL_HVSI;
|
|
return (0);
|
|
}
|
|
|
|
return (ENXIO);
|
|
}
|
|
|
|
static int
|
|
uart_opal_probe(device_t dev)
|
|
{
|
|
struct uart_opal_softc sc;
|
|
int err;
|
|
|
|
sc.node = ofw_bus_get_node(dev);
|
|
err = uart_opal_probe_node(&sc);
|
|
if (err != 0)
|
|
return (err);
|
|
|
|
device_set_desc(dev, "OPAL Serial Port");
|
|
|
|
return (err);
|
|
}
|
|
|
|
static void
|
|
uart_opal_cnprobe(struct consdev *cp)
|
|
{
|
|
char buf[64];
|
|
phandle_t input, chosen;
|
|
static struct uart_opal_softc sc;
|
|
|
|
if (opal_check() != 0)
|
|
goto fail;
|
|
|
|
if ((chosen = OF_finddevice("/chosen")) == -1)
|
|
goto fail;
|
|
|
|
/* Check if OF has an active stdin/stdout */
|
|
if (OF_getprop(chosen, "linux,stdout-path", buf, sizeof(buf)) <= 0)
|
|
goto fail;
|
|
|
|
input = OF_finddevice(buf);
|
|
if (input == -1)
|
|
goto fail;
|
|
|
|
sc.node = input;
|
|
if (uart_opal_probe_node(&sc) != 0)
|
|
goto fail;
|
|
mtx_init(&sc.sc_mtx, "uart_opal", NULL, MTX_SPIN | MTX_QUIET |
|
|
MTX_NOWITNESS);
|
|
|
|
cp->cn_pri = CN_NORMAL;
|
|
console_sc = ≻
|
|
cp->cn_arg = console_sc;
|
|
stdout_cp = cp;
|
|
return;
|
|
|
|
fail:
|
|
cp->cn_pri = CN_DEAD;
|
|
return;
|
|
}
|
|
|
|
static int
|
|
uart_opal_attach(device_t dev)
|
|
{
|
|
struct uart_opal_softc *sc;
|
|
int unit;
|
|
|
|
sc = device_get_softc(dev);
|
|
sc->node = ofw_bus_get_node(dev);
|
|
uart_opal_probe_node(sc);
|
|
|
|
unit = device_get_unit(dev);
|
|
mtx_init(&sc->sc_mtx, device_get_nameunit(dev), NULL,
|
|
MTX_SPIN | MTX_QUIET | MTX_NOWITNESS);
|
|
|
|
if (console_sc != NULL && console_sc->vtermid == sc->vtermid) {
|
|
device_printf(dev, "console\n");
|
|
device_set_softc(dev, console_sc);
|
|
sc = console_sc;
|
|
sprintf(uart_opal_consdev.cn_name, "ttyu%r", unit);
|
|
}
|
|
sc->tp = tty_alloc(&uart_opal_tty_class, sc);
|
|
|
|
if (console_sc == sc)
|
|
tty_init_console(sc->tp, 0);
|
|
|
|
sc->dev = dev;
|
|
sc->irqrid = 0;
|
|
sc->irqres = bus_alloc_resource_any(dev, SYS_RES_IRQ, &sc->irqrid,
|
|
RF_ACTIVE | RF_SHAREABLE);
|
|
if (sc->irqres != NULL) {
|
|
bus_setup_intr(dev, sc->irqres, INTR_TYPE_TTY | INTR_MPSAFE,
|
|
NULL, uart_opal_intr, sc, &sc->sc_icookie);
|
|
} else {
|
|
callout_init(&sc->callout, CALLOUT_MPSAFE);
|
|
sc->polltime = hz / 20;
|
|
if (sc->polltime < 1)
|
|
sc->polltime = 1;
|
|
callout_reset(&sc->callout, sc->polltime, uart_opal_intr, sc);
|
|
}
|
|
|
|
tty_makedev(sc->tp, NULL, "u%r", unit);
|
|
|
|
return (0);
|
|
}
|
|
|
|
static void
|
|
uart_opal_cninit(struct consdev *cp)
|
|
{
|
|
|
|
strcpy(cp->cn_name, "opalcons");
|
|
}
|
|
|
|
static void
|
|
uart_opal_cnterm(struct consdev *cp)
|
|
{
|
|
}
|
|
|
|
static int
|
|
uart_opal_get(struct uart_opal_softc *sc, void *buffer, size_t bufsize)
|
|
{
|
|
int err;
|
|
int hdr = 0;
|
|
|
|
if (sc->protocol == OPAL_RAW) {
|
|
uint64_t len = htobe64(bufsize);
|
|
uint64_t olen = (uint64_t)&len;
|
|
uint64_t obuf = (uint64_t)buffer;
|
|
|
|
if (pmap_bootstrapped) {
|
|
olen = vtophys(&len);
|
|
obuf = vtophys(buffer);
|
|
}
|
|
|
|
err = opal_call(OPAL_CONSOLE_READ, sc->vtermid, olen, obuf);
|
|
if (err != OPAL_SUCCESS)
|
|
return (-1);
|
|
|
|
bufsize = be64toh(len);
|
|
} else {
|
|
uart_lock(&sc->sc_mtx);
|
|
if (sc->inbuflen == 0) {
|
|
err = opal_call(OPAL_CONSOLE_READ, sc->vtermid,
|
|
&sc->inbuflen, sc->opal_inbuf);
|
|
if (err != OPAL_SUCCESS) {
|
|
uart_unlock(&sc->sc_mtx);
|
|
return (-1);
|
|
}
|
|
hdr = 1;
|
|
sc->inbuflen = be64toh(sc->inbuflen);
|
|
}
|
|
|
|
if (sc->inbuflen == 0) {
|
|
uart_unlock(&sc->sc_mtx);
|
|
return (0);
|
|
}
|
|
|
|
if (bufsize > sc->inbuflen)
|
|
bufsize = sc->inbuflen;
|
|
|
|
if (hdr == 1) {
|
|
sc->inbuflen = sc->inbuflen - 4;
|
|
/* The HVSI protocol has a 4 byte header, skip it */
|
|
memmove(&sc->opal_inbuf[0], &sc->opal_inbuf[4],
|
|
sc->inbuflen);
|
|
}
|
|
|
|
memcpy(buffer, sc->opal_inbuf, bufsize);
|
|
sc->inbuflen -= bufsize;
|
|
if (sc->inbuflen > 0)
|
|
memmove(&sc->opal_inbuf[0], &sc->opal_inbuf[bufsize],
|
|
sc->inbuflen);
|
|
|
|
uart_unlock(&sc->sc_mtx);
|
|
}
|
|
|
|
return (bufsize);
|
|
}
|
|
|
|
static int
|
|
uart_opal_put(struct uart_opal_softc *sc, void *buffer, size_t bufsize)
|
|
{
|
|
uint16_t seqno;
|
|
uint64_t len;
|
|
char cbuf[16];
|
|
int err;
|
|
uint64_t olen = (uint64_t)&len;
|
|
uint64_t obuf = (uint64_t)cbuf;
|
|
|
|
if (sc->protocol == OPAL_RAW) {
|
|
obuf = (uint64_t)buffer;
|
|
len = bufsize;
|
|
|
|
uart_opal_real_map_outbuffer(&obuf, &olen);
|
|
*(uint64_t*)olen = htobe64(*(uint64_t*)olen);
|
|
err = opal_call(OPAL_CONSOLE_WRITE, sc->vtermid, olen, obuf);
|
|
*(uint64_t*)olen = be64toh(*(uint64_t*)olen);
|
|
uart_opal_real_unmap_outbuffer(&len);
|
|
} else {
|
|
uart_lock(&sc->sc_mtx);
|
|
if (bufsize > 12)
|
|
bufsize = 12;
|
|
seqno = sc->outseqno++;
|
|
cbuf[0] = VS_DATA_PACKET_HEADER;
|
|
cbuf[1] = 4 + bufsize; /* total length */
|
|
cbuf[2] = (seqno >> 8) & 0xff;
|
|
cbuf[3] = seqno & 0xff;
|
|
memcpy(&cbuf[4], buffer, bufsize);
|
|
len = 4 + bufsize;
|
|
|
|
uart_opal_real_map_outbuffer(&obuf, &olen);
|
|
*(uint64_t*)olen = htobe64(*(uint64_t*)olen);
|
|
err = opal_call(OPAL_CONSOLE_WRITE, sc->vtermid, olen, obuf);
|
|
*(uint64_t*)olen = be64toh(*(uint64_t*)olen);
|
|
uart_opal_real_unmap_outbuffer(&len);
|
|
|
|
uart_unlock(&sc->sc_mtx);
|
|
|
|
len -= 4;
|
|
}
|
|
|
|
if (err == OPAL_SUCCESS)
|
|
return (len);
|
|
else if (err == OPAL_BUSY_EVENT)
|
|
return(0);
|
|
|
|
return (-1);
|
|
}
|
|
|
|
static int
|
|
uart_opal_cngetc(struct consdev *cp)
|
|
{
|
|
return (uart_opal_getc(cp->cn_arg));
|
|
}
|
|
|
|
static int
|
|
uart_opal_getc(struct uart_opal_softc *sc)
|
|
{
|
|
unsigned char c;
|
|
int retval;
|
|
|
|
retval = uart_opal_get(sc, &c, 1);
|
|
if (retval != 1)
|
|
return (-1);
|
|
#if defined(KDB)
|
|
kdb_alt_break(c, &sc->alt_break_state);
|
|
#endif
|
|
|
|
return (c);
|
|
}
|
|
|
|
static void
|
|
uart_opal_cnputc(struct consdev *cp, int c)
|
|
{
|
|
unsigned char ch = c;
|
|
int a;
|
|
|
|
if (1) {
|
|
/* Clear FIFO if needed. Must be repeated few times. */
|
|
for (a = 0; a < 20; a++) {
|
|
opal_call(OPAL_POLL_EVENTS, NULL);
|
|
}
|
|
}
|
|
uart_opal_put(cp->cn_arg, &ch, 1);
|
|
}
|
|
|
|
static void
|
|
uart_opal_cngrab(struct consdev *cp)
|
|
{
|
|
}
|
|
|
|
static void
|
|
uart_opal_cnungrab(struct consdev *cp)
|
|
{
|
|
}
|
|
|
|
static void
|
|
uart_opal_ttyoutwakeup(struct tty *tp)
|
|
{
|
|
struct uart_opal_softc *sc;
|
|
char buffer[8];
|
|
int len;
|
|
int64_t buffer_space;
|
|
|
|
sc = tty_softc(tp);
|
|
|
|
while ((len = ttydisc_getc(tp, buffer, sizeof(buffer))) != 0) {
|
|
int bytes_written = 0;
|
|
while (bytes_written == 0) {
|
|
buffer_space = uart_opal_console_write_buffer_space(sc->vtermid);
|
|
if (buffer_space == -1)
|
|
/* OPAL failure or invalid terminal */
|
|
break;
|
|
else if (buffer_space >= len)
|
|
bytes_written = uart_opal_put(sc, buffer, len);
|
|
|
|
if (bytes_written == 0)
|
|
/* OPAL must be busy, poll and retry */
|
|
opal_call(OPAL_POLL_EVENTS, NULL);
|
|
else if (bytes_written == -1)
|
|
/* OPAL failure or invalid terminal */
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
uart_opal_intr(void *v)
|
|
{
|
|
struct uart_opal_softc *sc = v;
|
|
struct tty *tp = sc->tp;
|
|
int c;
|
|
|
|
tty_lock(tp);
|
|
while ((c = uart_opal_getc(sc)) > 0)
|
|
ttydisc_rint(tp, c, 0);
|
|
ttydisc_rint_done(tp);
|
|
tty_unlock(tp);
|
|
|
|
opal_call(OPAL_POLL_EVENTS, NULL);
|
|
|
|
if (sc->irqres == NULL)
|
|
callout_reset(&sc->callout, sc->polltime, uart_opal_intr, sc);
|
|
}
|
|
|
|
static int
|
|
opalcons_probe(device_t dev)
|
|
{
|
|
const char *name;
|
|
|
|
name = ofw_bus_get_name(dev);
|
|
if (name == NULL || strcmp(name, "consoles") != 0)
|
|
return (ENXIO);
|
|
|
|
device_set_desc(dev, "OPAL Consoles");
|
|
return (BUS_PROBE_SPECIFIC);
|
|
}
|
|
|
|
static int
|
|
opalcons_attach(device_t dev)
|
|
{
|
|
phandle_t child;
|
|
device_t cdev;
|
|
struct ofw_bus_devinfo *dinfo;
|
|
|
|
for (child = OF_child(ofw_bus_get_node(dev)); child != 0;
|
|
child = OF_peer(child)) {
|
|
dinfo = malloc(sizeof(*dinfo), M_DEVBUF, M_WAITOK | M_ZERO);
|
|
if (ofw_bus_gen_setup_devinfo(dinfo, child) != 0) {
|
|
free(dinfo, M_DEVBUF);
|
|
continue;
|
|
}
|
|
cdev = device_add_child(dev, NULL, -1);
|
|
if (cdev == NULL) {
|
|
device_printf(dev, "<%s>: device_add_child failed\n",
|
|
dinfo->obd_name);
|
|
ofw_bus_gen_destroy_devinfo(dinfo);
|
|
free(dinfo, M_DEVBUF);
|
|
continue;
|
|
}
|
|
device_set_ivars(cdev, dinfo);
|
|
}
|
|
|
|
return (bus_generic_attach(dev));
|
|
}
|
|
|
|
static const struct ofw_bus_devinfo *
|
|
opalcons_get_devinfo(device_t dev, device_t child)
|
|
{
|
|
return (device_get_ivars(child));
|
|
}
|
|
|
|
static device_method_t opalcons_methods[] = {
|
|
/* Device interface */
|
|
DEVMETHOD(device_probe, opalcons_probe),
|
|
DEVMETHOD(device_attach, opalcons_attach),
|
|
|
|
/* ofw_bus interface */
|
|
DEVMETHOD(ofw_bus_get_devinfo, opalcons_get_devinfo),
|
|
DEVMETHOD(ofw_bus_get_compat, ofw_bus_gen_get_compat),
|
|
DEVMETHOD(ofw_bus_get_model, ofw_bus_gen_get_model),
|
|
DEVMETHOD(ofw_bus_get_name, ofw_bus_gen_get_name),
|
|
DEVMETHOD(ofw_bus_get_node, ofw_bus_gen_get_node),
|
|
DEVMETHOD(ofw_bus_get_type, ofw_bus_gen_get_type),
|
|
|
|
DEVMETHOD_END
|
|
};
|
|
|
|
static driver_t opalcons_driver = {
|
|
"opalcons",
|
|
opalcons_methods,
|
|
0
|
|
};
|
|
|
|
static devclass_t opalcons_devclass;
|
|
|
|
DRIVER_MODULE(opalcons, opal, opalcons_driver, opalcons_devclass, 0, 0);
|