diff mbox

[RFC,2/2] serial console, input

Message ID 1467370471-20554-3-git-send-email-kraxel@redhat.com (mailing list archive)
State New, archived
Headers show

Commit Message

Gerd Hoffmann July 1, 2016, 10:54 a.m. UTC
Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 src/clock.c  |   1 +
 src/serial.c | 255 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/util.h   |   1 +
 3 files changed, 257 insertions(+)

Comments

Kevin O'Connor July 1, 2016, 5:07 p.m. UTC | #1
On Fri, Jul 01, 2016 at 12:54:31PM +0200, Gerd Hoffmann wrote:
> Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
> ---
>  src/clock.c  |   1 +
>  src/serial.c | 255 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
>  src/util.h   |   1 +
>  3 files changed, 257 insertions(+)
> 
> diff --git a/src/clock.c b/src/clock.c
> index e83e0f3..e44e112 100644
> --- a/src/clock.c
> +++ b/src/clock.c
> @@ -295,6 +295,7 @@ clock_update(void)
>      floppy_tick();
>      usb_check_event();
>      ps2_check_event();
> +    sercon_check_event();
>  }
>  
>  // INT 08h System Timer ISR Entry Point
> diff --git a/src/serial.c b/src/serial.c
> index 74b91bb..d72dd01 100644
> --- a/src/serial.c
> +++ b/src/serial.c
> @@ -655,3 +655,258 @@ void sercon_enable(void)
>      outb(0x01, addr + 0x02);       // enable fifo
>      enable_vga_console();
>  }
> +
> +/****************************************************************
> + * serial input
> + ****************************************************************/
> +
> +VARLOW u8 rx_buf[16];
> +VARLOW u8 rx_bytes;
> +
> +VARLOW struct {
> +    char seq[4];
> +    u8 len;
> +    u8 scancode;
> +} termseq[] = {
> +    { .seq = "OP", .len = 2, .scancode = 0x3b },    // F1
> +    { .seq = "OQ", .len = 2, .scancode = 0x3c },    // F2
> +    { .seq = "OR", .len = 2, .scancode = 0x3d },    // F3
> +    { .seq = "OS", .len = 2, .scancode = 0x3e },    // F4
> +    { .seq = "[A", .len = 2, .scancode = 0xc8 },    // up
> +    { .seq = "[B", .len = 2, .scancode = 0xd0 },    // down
> +    { .seq = "[C", .len = 2, .scancode = 0xcd },    // right
> +    { .seq = "[D", .len = 2, .scancode = 0xcb },    // left
> +};

It would be preferable to mark constant data with "static VAR16"
instead of VARLOW.

> +
> +#define FLAG_CTRL  (1<<0)
> +#define FLAG_SHIFT (1<<1)
> +
> +VARLOW struct {
> +    u8 flags;
> +    u8 scancode;
> +} termchr[256] = {
> +    [ '1'        ] = { .scancode = 0x02,                      },

I think this table should be generated at runtime from
kbd.c:scan_to_keycode[].  Since it doesn't change at runtime,
malloc_fseg() / GET_GLOBAL() could be used instead of VARLOW.

[...]
> +static void sercon_sendkey(u8 scancode, u8 flags)
> +{
> +    if (flags & FLAG_CTRL)
> +        process_key(0x1d);
> +    if (flags & FLAG_SHIFT)
> +        process_key(0x2a);
> +
> +    if (scancode & 0x80) {
> +        process_key(0xe0);
> +        process_key(scancode & ~0x80);
> +        process_key(0xe0);
> +        process_key(scancode);
> +    } else {
> +        process_key(scancode);
> +        process_key(scancode | 0x80);
> +    }
> +
> +    if (flags & FLAG_SHIFT)
> +        process_key(0x2a | 0x80);
> +    if (flags & FLAG_CTRL)
> +        process_key(0x1d | 0x80);
> +}

Is it necessary to use process_key() here instead of injecting the
keycode directly with enqueue_key()?  I think the only difference is
the CONFIG_KBD_CALL_INT15_4F stuff and I'm not sure if anything
interesting needs that.

> +
> +void VISIBLE16
> +sercon_check_event(void)

Does this need VISIBLE16?

> +{
> +    u16 addr = GET_LOW(sercon_port);
> +    u8 byte, scancode, flags, count = 0;
> +    int seq, chr, len;
> +
> +    // check to see if there is a active serial port
> +    if (!addr)
> +        return;
> +    if (inb(addr + SEROFF_LSR) == 0xFF)
> +        return;
> +
> +    // flush pending output
> +    sercon_flush_lazy();
> +
> +    // read all available data
> +    while (inb(addr + SEROFF_LSR) & 0x01) {
> +        byte = inb(addr + SEROFF_DATA);
> +        if (GET_LOW(rx_bytes) < sizeof(rx_buf)) {
> +            SET_LOW(rx_buf[rx_bytes], byte);
> +            SET_LOW(rx_bytes, GET_LOW(rx_bytes) + 1);
> +            count++;
> +        }
> +    }
> +
> +next_char:
> +    // no (more) input data
> +    if (!GET_LOW(rx_bytes))
> +        return;
> +
> +    // lookup escape sequences
> +    if (GET_LOW(rx_bytes) > 1 && GET_LOW(rx_buf[0]) == 0x1b) {
> +        for (seq = 0; seq < ARRAY_SIZE(termseq); seq++) {
> +            len = GET_LOW(termseq[seq].len);
> +            if (GET_LOW(rx_bytes) < len + 1)
> +                continue;
> +            for (chr = 0; chr < len; chr++) {
> +                if (GET_LOW(termseq[seq].seq[chr]) != GET_LOW(rx_buf[chr + 1]))
> +                    break;
> +            }
> +            if (chr == len) {
> +                scancode = GET_LOW(termseq[seq].scancode);
> +                sercon_sendkey(scancode, 0);
> +                shiftbuf(len + 1);
> +                goto next_char;
> +            }
> +        }
> +    }
> +
> +    // Seems we got a escape sequence we didn't recognise.
> +    //  -> If we received data wait for more, maybe it is just incomplete.
> +    if (GET_LOW(rx_buf[0]) == 0x1b && count)
> +        return;
> +
> +    // Handle input as individual chars.
> +    chr = GET_LOW(rx_buf[0]);
> +    scancode = GET_LOW(termchr[chr].scancode);
> +    flags = GET_LOW(termchr[chr].flags);
> +    if (scancode)
> +        sercon_sendkey(scancode, flags);
> +    shiftbuf(1);
> +    goto next_char;
> +}

If I understand correctly, most keys are sent on the serial port as
single bytes, but there are a few keys that are sent as multi-byte
sequences.  There's a lot of complexity to implement buffering for
that unusual case.  I wonder if the buffer could be avoided - I played
with it a little and came up with the below (totally untested).  I'm
not sure if it's an improvement.

-Kevin


u8 multibyte_read_pos VARLOW;
u8 multibyte_read_count VARLOW;

void
sercon_check_event(void)
{
    u16 addr = GET_LOW(sercon_port);
    ...

    u8 mb_pos = GET_LOW(multibyte_read_pos);
    u8 mb_count = GET_LOW(multibyte_read_count);
    u8 mustflush = mb_count != 0;

    // read and process data
    while (inb(addr + SEROFF_LSR) & 0x01) {
        u8 byte = inb(addr + SEROFF_DATA);
        if (mb_count) {
            // In a multi-byte sequence
            while (GET_GLOBAL(termseq[mb_pos].seq[mb_count-1]) != byte) {
                // Byte didn't match this sequence - find one that does
                mb_pos++;
                if (mb_pos >= ARRAY_SIZE(termseq)
                    || memcmp_far(GLOBAL_SEG, termseq[mb_pos-1].seq
                                  , GLOBAL_SEG, termseq[mb_pos].seq
                                  , mb_count-1) != 0)
                    // No match - must flush previusly queued keys
                    dump_multibyte_sequence(mb_pos, mb_count);
                    mb_pos = mb_count = mustflush = 0;
                    break;
                }
            }
            if (mb_count) {
                if (!GET_GLOBAL(termseq[mb_pos].seq[mb_count])) {
                    // sequence complete
                    sercon_sendkey(GET_GLOBAL(termseq[seq].scancode), 0);
                    mb_pos = mb_count = mustflush = 0;
                } else {
                    // Got another key in this sequence - continue checking
                    mb_count++;
                }
                continue;
            }
        }
        if (byte == 0x1b) {
            // Start multi-byte sequence check;
            mb_pos = 0;
            mb_count = 1;
            continue;
        }
        // Send normal key
        sercon_sendkey(GET_LOW(termchr[chr].scancode), GET_LOW(termchr[chr].flags));
        mustflush = 0;
    }

    if (mustflush && mb_count) {
        // Too long to read multi-byte sequence - must flush
        dump_multibyte_sequence(mb_pos, mb_count);
        mb_count = mb_pos = 0;
    }
    SET_LOW(multibyte_read_count, mb_count);
    SET_LOW(multibyte_read_pos, mb_pos);
}

static void
dump_multibyte_sequence(u8 mb_pos, u8 mb_count)
{
    sercon_sendkey(GET_LOW(termchr[0x1b].scancode), GET_LOW(termchr[0x1b].flags));
    int i;
    for (i=0; i<mb_count-1; i++) {
        u8 key = GET_GLOBAL(termseq[mb_pos].seq[i]);
        sercon_sendkey(GET_LOW(termchr[key].scancode), GET_LOW(termchr[key].flags));
    }
}
Kevin O'Connor July 1, 2016, 6:07 p.m. UTC | #2
On Fri, Jul 01, 2016 at 01:07:39PM -0400, Kevin O'Connor wrote:
> If I understand correctly, most keys are sent on the serial port as
> single bytes, but there are a few keys that are sent as multi-byte
> sequences.  There's a lot of complexity to implement buffering for
> that unusual case.  I wonder if the buffer could be avoided - I played
> with it a little and came up with the below (totally untested).  I'm
> not sure if it's an improvement.

The version below might be slightly easier to understand (still
totally untested).

-Kevin


u8 multibyte_read_count VARLOW;
u8 multibyte_read_pos VARLOW;

void
sercon_check_event(void)
{
    u16 addr = GET_LOW(sercon_port);
    ...

    // read and process data
    int readdata = 0;
    while (inb(addr + SEROFF_LSR) & 0x01) {
        u8 byte = inb(addr + SEROFF_DATA);
        readdata = 1;
        int ret = sercon_check_multibyte(byte);
        if (ret)
            // byte part of multi-byte sequence
            continue;
        if (byte == 0x1b) {
            // Start multi-byte sequence check
            SET_LOW(multibyte_read_count, 1);
            continue;
        }
        // Send normal key
        sercon_sendkey(GET_LOW(termchr[chr].scancode), GET_LOW(termchr[chr].flags));
    }

    if (!readdata && GET_LOW(multibyte_read_count))
        // Too long to read multi-byte sequence - must flush
        dump_multibyte_sequence();
}

static int
sercon_check_multibyte(u8 byte)
{
    int mb_count = GET_LOW(multibyte_read_count);
    if (!mb_count)
        // Not in a multi-byte sequence
        return 0;
    int mb_pos = GET_LOW(multibyte_read_pos);
    while (GET_GLOBAL(termseq[mb_pos].seq[mb_count-1]) != byte) {
        // Byte didn't match this sequence - find a sequence that does
        mb_pos++;
        if (mb_pos >= ARRAY_SIZE(termseq)
            || memcmp_far(GLOBAL_SEG, termseq[mb_pos-1].seq
                          , GLOBAL_SEG, termseq[mb_pos].seq, mb_count-1) != 0)
            // No match - must flush previusly queued keys
            dump_multibyte_sequence();
            return 0;
        }
    }
    mb_count++;
    if (!GET_GLOBAL(termseq[mb_pos].seq[mb_count-1])) {
        // sequence complete - send key
        sercon_sendkey(GET_GLOBAL(termseq[seq].scancode), 0);
        mb_count = mb_pos = 0;
    }
    SET_LOW(multibyte_read_count, mb_count);
    SET_LOW(multibyte_read_pos, mb_pos);
    return 1;
}

static void
dump_multibyte_sequence(void)
{
    sercon_sendkey(GET_LOW(termchr[0x1b].scancode), GET_LOW(termchr[0x1b].flags));
    int i, mb_count = GET_LOW(multibyte_read_count);
    for (i=0; i<mb_count-1; i++) {
        u8 key = GET_GLOBAL(termseq[mb_pos].seq[i]);
        sercon_sendkey(GET_LOW(termchr[key].scancode), GET_LOW(termchr[key].flags));
    }
    SET_LOW(multibyte_read_count, 0);
    SET_LOW(multibyte_read_pos, 0);
}
Gerd Hoffmann July 4, 2016, 9:16 a.m. UTC | #3
Hi,

> > +#define FLAG_CTRL  (1<<0)
> > +#define FLAG_SHIFT (1<<1)
> > +
> > +VARLOW struct {
> > +    u8 flags;
> > +    u8 scancode;
> > +} termchr[256] = {
> > +    [ '1'        ] = { .scancode = 0x02,                      },
> 
> I think this table should be generated at runtime from
> kbd.c:scan_to_keycode[].  Since it doesn't change at runtime,
> malloc_fseg() / GET_GLOBAL() could be used instead of VARLOW.

Ah, ok.  Didn't notice this one can be used for ascii -> scancode
lookups.

I'm wondering whenever it makes sense to build a reverse table.  We
could simply search scan_to_keycode[] instead.  Sure it is slower than a
table lookup, but still _way_ faster than any human can type, and we
would save some real-mode memory.

> Is it necessary to use process_key() here instead of injecting the
> keycode directly with enqueue_key()?

From a brief look at the code I think I can use enqueue_key directly
without anything breaking.  I'll give it a try.

> > +void VISIBLE16
> > +sercon_check_event(void)
> 
> Does this need VISIBLE16?

Don't think so, I'll drop it.

> If I understand correctly, most keys are sent on the serial port as
> single bytes, but there are a few keys that are sent as multi-byte
> sequences.

Yes.  Single-byte us-ascii for letters and numbers.  multibyte sequences
starting with ESC (0x1b) for almost everything else (function keys,
cursor keys, home, end, pageup, pagedown, ...)

The list in the patch isn't complete, I've only added the most important
keys for initial testing.

You may also see input with the 8th bit set, which has a high chance to
be a multibyte sequence (utf8) too these days.  But I think we can
safely ignore any input with the 8th bit set, we can't map that to us
keyboard layout anyway even if we manage to correctly identify the
character typed.

> There's a lot of complexity to implement buffering for
> that unusual case.  I wonder if the buffer could be avoided - I played
> with it a little and came up with the below (totally untested).  I'm
> not sure if it's an improvement.

Hmm, so you avoid the buffer by maintaining an index into termseq and
matched byte count instead.  Hmm.  Yes, avoids the buffer and also a few
cpu cycles because you can continue searching where you left off.  I
find the code flow harder to follow though.

cheers,
  Gerd
Kevin O'Connor July 4, 2016, 3:34 p.m. UTC | #4
On Mon, Jul 04, 2016 at 11:16:45AM +0200, Gerd Hoffmann wrote:
>   Hi,
> 
> > > +#define FLAG_CTRL  (1<<0)
> > > +#define FLAG_SHIFT (1<<1)
> > > +
> > > +VARLOW struct {
> > > +    u8 flags;
> > > +    u8 scancode;
> > > +} termchr[256] = {
> > > +    [ '1'        ] = { .scancode = 0x02,                      },
> > 
> > I think this table should be generated at runtime from
> > kbd.c:scan_to_keycode[].  Since it doesn't change at runtime,
> > malloc_fseg() / GET_GLOBAL() could be used instead of VARLOW.
> 
> Ah, ok.  Didn't notice this one can be used for ascii -> scancode
> lookups.
> 
> I'm wondering whenever it makes sense to build a reverse table.  We
> could simply search scan_to_keycode[] instead.  Sure it is slower than a
> table lookup, but still _way_ faster than any human can type, and we
> would save some real-mode memory.

Agreed.

[...]
> > There's a lot of complexity to implement buffering for
> > that unusual case.  I wonder if the buffer could be avoided - I played
> > with it a little and came up with the below (totally untested).  I'm
> > not sure if it's an improvement.
> 
> Hmm, so you avoid the buffer by maintaining an index into termseq and
> matched byte count instead.  Hmm.  Yes, avoids the buffer and also a few
> cpu cycles because you can continue searching where you left off.  I
> find the code flow harder to follow though.

Right.  Yeah, also not sure it's an improvement.

Does the original code flush the multi-byte sequence on a timeout?  I
suspect it is important that one can hit ESC without having to type
another key.  Also, I'd prefer to avoid backwards gotos if possible.

-Kevin
Gerd Hoffmann July 4, 2016, 8:03 p.m. UTC | #5
Hi,

> Does the original code flush the multi-byte sequence on a timeout?  I
> suspect it is important that one can hit ESC without having to type
> another key.  Also, I'd prefer to avoid backwards gotos if possible.

Yes, sort of.  If it didn't match an escape sequence and hasn't seen
additional data on this particular sercon_handle_event call it goes on
interpret the buffer content as single byte.  So, effectively the
timeout is the clock_update() call interval.

cheers,
  Gerd
diff mbox

Patch

diff --git a/src/clock.c b/src/clock.c
index e83e0f3..e44e112 100644
--- a/src/clock.c
+++ b/src/clock.c
@@ -295,6 +295,7 @@  clock_update(void)
     floppy_tick();
     usb_check_event();
     ps2_check_event();
+    sercon_check_event();
 }
 
 // INT 08h System Timer ISR Entry Point
diff --git a/src/serial.c b/src/serial.c
index 74b91bb..d72dd01 100644
--- a/src/serial.c
+++ b/src/serial.c
@@ -655,3 +655,258 @@  void sercon_enable(void)
     outb(0x01, addr + 0x02);       // enable fifo
     enable_vga_console();
 }
+
+/****************************************************************
+ * serial input
+ ****************************************************************/
+
+VARLOW u8 rx_buf[16];
+VARLOW u8 rx_bytes;
+
+VARLOW struct {
+    char seq[4];
+    u8 len;
+    u8 scancode;
+} termseq[] = {
+    { .seq = "OP", .len = 2, .scancode = 0x3b },    // F1
+    { .seq = "OQ", .len = 2, .scancode = 0x3c },    // F2
+    { .seq = "OR", .len = 2, .scancode = 0x3d },    // F3
+    { .seq = "OS", .len = 2, .scancode = 0x3e },    // F4
+    { .seq = "[A", .len = 2, .scancode = 0xc8 },    // up
+    { .seq = "[B", .len = 2, .scancode = 0xd0 },    // down
+    { .seq = "[C", .len = 2, .scancode = 0xcd },    // right
+    { .seq = "[D", .len = 2, .scancode = 0xcb },    // left
+};
+
+#define FLAG_CTRL  (1<<0)
+#define FLAG_SHIFT (1<<1)
+
+VARLOW struct {
+    u8 flags;
+    u8 scancode;
+} termchr[256] = {
+    [ '1'        ] = { .scancode = 0x02,                      },
+    [ '!'        ] = { .scancode = 0x02, .flags = FLAG_SHIFT  },
+    [ '2'        ] = { .scancode = 0x03,                      },
+    [ '@'        ] = { .scancode = 0x03, .flags = FLAG_SHIFT  },
+    [ '3'        ] = { .scancode = 0x04,                      },
+    [ '#'        ] = { .scancode = 0x04, .flags = FLAG_SHIFT  },
+    [ '4'        ] = { .scancode = 0x05,                      },
+    [ '$'        ] = { .scancode = 0x05, .flags = FLAG_SHIFT  },
+    [ '5'        ] = { .scancode = 0x06,                      },
+    [ '%'        ] = { .scancode = 0x06, .flags = FLAG_SHIFT  },
+    [ '6'        ] = { .scancode = 0x07,                      },
+    [ '^'        ] = { .scancode = 0x07, .flags = FLAG_SHIFT  },
+    [ '7'        ] = { .scancode = 0x08,                      },
+    [ '&'        ] = { .scancode = 0x08, .flags = FLAG_SHIFT  },
+    [ '8'        ] = { .scancode = 0x09,                      },
+    [ '*'        ] = { .scancode = 0x09, .flags = FLAG_SHIFT  },
+    [ '9'        ] = { .scancode = 0x0a,                      },
+    [ '('        ] = { .scancode = 0x0a, .flags = FLAG_SHIFT  },
+    [ '0'        ] = { .scancode = 0x0b,                      },
+    [ ')'        ] = { .scancode = 0x0b, .flags = FLAG_SHIFT  },
+    [ '-'        ] = { .scancode = 0x0c,                      },
+    [ '='        ] = { .scancode = 0x0d,                      },
+    [ '+'        ] = { .scancode = 0x0d, .flags = FLAG_SHIFT  },
+
+    [ 'q'        ] = { .scancode = 0x10,                      },
+    [ 'Q'        ] = { .scancode = 0x10, .flags = FLAG_SHIFT  },
+    [ 'Q' & 0x1f ] = { .scancode = 0x10, .flags = FLAG_CTRL   },
+    [ 'w'        ] = { .scancode = 0x11,                      },
+    [ 'W'        ] = { .scancode = 0x11, .flags = FLAG_SHIFT  },
+    [ 'W' & 0x1f ] = { .scancode = 0x11, .flags = FLAG_CTRL   },
+    [ 'e'        ] = { .scancode = 0x12,                      },
+    [ 'E'        ] = { .scancode = 0x12, .flags = FLAG_SHIFT  },
+    [ 'E' & 0x1f ] = { .scancode = 0x12, .flags = FLAG_CTRL   },
+    [ 'r'        ] = { .scancode = 0x13,                      },
+    [ 'R'        ] = { .scancode = 0x13, .flags = FLAG_SHIFT  },
+    [ 'R' & 0x1f ] = { .scancode = 0x13, .flags = FLAG_CTRL   },
+    [ 't'        ] = { .scancode = 0x14,                      },
+    [ 'T'        ] = { .scancode = 0x14, .flags = FLAG_SHIFT  },
+    [ 'T' & 0x1f ] = { .scancode = 0x14, .flags = FLAG_CTRL   },
+    [ 'y'        ] = { .scancode = 0x15,                      },
+    [ 'Y'        ] = { .scancode = 0x15, .flags = FLAG_SHIFT  },
+    [ 'Y' & 0x1f ] = { .scancode = 0x15, .flags = FLAG_CTRL   },
+    [ 'u'        ] = { .scancode = 0x16,                      },
+    [ 'U'        ] = { .scancode = 0x16, .flags = FLAG_SHIFT  },
+    [ 'U' & 0x1f ] = { .scancode = 0x16, .flags = FLAG_CTRL   },
+    [ 'i'        ] = { .scancode = 0x17,                      },
+    [ 'I'        ] = { .scancode = 0x17, .flags = FLAG_SHIFT  },
+    [ 'I' & 0x1f ] = { .scancode = 0x17, .flags = FLAG_CTRL   },
+    [ 'o'        ] = { .scancode = 0x18,                      },
+    [ 'O'        ] = { .scancode = 0x18, .flags = FLAG_SHIFT  },
+    [ 'O' & 0x1f ] = { .scancode = 0x18, .flags = FLAG_CTRL   },
+    [ 'p'        ] = { .scancode = 0x19,                      },
+    [ 'P'        ] = { .scancode = 0x19, .flags = FLAG_SHIFT  },
+    [ 'P' & 0x1f ] = { .scancode = 0x19, .flags = FLAG_CTRL   },
+    [ '['        ] = { .scancode = 0x1a,                      },
+    [ '{'        ] = { .scancode = 0x1a, .flags = FLAG_SHIFT  },
+    [ ']'        ] = { .scancode = 0x1b,                      },
+    [ '}'        ] = { .scancode = 0x1b, .flags = FLAG_SHIFT  },
+
+    [ 'a'        ] = { .scancode = 0x1e,                      },
+    [ 'A'        ] = { .scancode = 0x1e, .flags = FLAG_SHIFT  },
+    [ 'A' & 0x1f ] = { .scancode = 0x1e, .flags = FLAG_CTRL   },
+    [ 's'        ] = { .scancode = 0x1f,                      },
+    [ 'S'        ] = { .scancode = 0x1f, .flags = FLAG_SHIFT  },
+    [ 'S' & 0x1f ] = { .scancode = 0x1f, .flags = FLAG_CTRL   },
+    [ 'd'        ] = { .scancode = 0x20,                      },
+    [ 'D'        ] = { .scancode = 0x20, .flags = FLAG_SHIFT  },
+    [ 'D' & 0x1f ] = { .scancode = 0x20, .flags = FLAG_CTRL   },
+    [ 'f'        ] = { .scancode = 0x21,                      },
+    [ 'F'        ] = { .scancode = 0x21, .flags = FLAG_SHIFT  },
+    [ 'F' & 0x1f ] = { .scancode = 0x21, .flags = FLAG_CTRL   },
+    [ 'g'        ] = { .scancode = 0x22,                      },
+    [ 'G'        ] = { .scancode = 0x22, .flags = FLAG_SHIFT  },
+    [ 'G' & 0x1f ] = { .scancode = 0x22, .flags = FLAG_CTRL   },
+    [ 'h'        ] = { .scancode = 0x23,                      },
+    [ 'H'        ] = { .scancode = 0x23, .flags = FLAG_SHIFT  },
+    [ 'H' & 0x1f ] = { .scancode = 0x23, .flags = FLAG_CTRL   },
+    [ 'j'        ] = { .scancode = 0x24,                      },
+    [ 'J'        ] = { .scancode = 0x24, .flags = FLAG_SHIFT  },
+    [ 'J' & 0x1f ] = { .scancode = 0x24, .flags = FLAG_CTRL   },
+    [ 'k'        ] = { .scancode = 0x25,                      },
+    [ 'K'        ] = { .scancode = 0x25, .flags = FLAG_SHIFT  },
+    [ 'K' & 0x1f ] = { .scancode = 0x25, .flags = FLAG_CTRL   },
+    [ 'l'        ] = { .scancode = 0x26,                      },
+    [ 'L'        ] = { .scancode = 0x26, .flags = FLAG_SHIFT  },
+    [ 'L' & 0x1f ] = { .scancode = 0x26, .flags = FLAG_CTRL   },
+    [ ';'        ] = { .scancode = 0x27,                      },
+    [ ':'        ] = { .scancode = 0x27, .flags = FLAG_SHIFT  },
+    [ '\''       ] = { .scancode = 0x28,                      },
+    [ '"'        ] = { .scancode = 0x28, .flags = FLAG_SHIFT  },
+
+    [ '\\'       ] = { .scancode = 0x2b,                      },
+    [ '|'        ] = { .scancode = 0x2b, .flags = FLAG_SHIFT  },
+    [ 'z'        ] = { .scancode = 0x2c,                      },
+    [ 'Z'        ] = { .scancode = 0x2c, .flags = FLAG_SHIFT  },
+    [ 'Z' & 0x1f ] = { .scancode = 0x2c, .flags = FLAG_CTRL   },
+    [ 'x'        ] = { .scancode = 0x2d,                      },
+    [ 'X'        ] = { .scancode = 0x2d, .flags = FLAG_SHIFT  },
+    [ 'X' & 0x1f ] = { .scancode = 0x2d, .flags = FLAG_CTRL   },
+    [ 'c'        ] = { .scancode = 0x2e,                      },
+    [ 'C'        ] = { .scancode = 0x2e, .flags = FLAG_SHIFT  },
+    [ 'C' & 0x1f ] = { .scancode = 0x2e, .flags = FLAG_CTRL   },
+    [ 'v'        ] = { .scancode = 0x2f,                      },
+    [ 'V'        ] = { .scancode = 0x2f, .flags = FLAG_SHIFT  },
+    [ 'V' & 0x1f ] = { .scancode = 0x2f, .flags = FLAG_CTRL   },
+    [ 'b'        ] = { .scancode = 0x30,                      },
+    [ 'B'        ] = { .scancode = 0x30, .flags = FLAG_SHIFT  },
+    [ 'B' & 0x1f ] = { .scancode = 0x30, .flags = FLAG_CTRL   },
+    [ 'n'        ] = { .scancode = 0x31,                      },
+    [ 'N'        ] = { .scancode = 0x31, .flags = FLAG_SHIFT  },
+    [ 'N' & 0x1f ] = { .scancode = 0x31, .flags = FLAG_CTRL   },
+    [ 'm'        ] = { .scancode = 0x32,                      },
+    [ 'M'        ] = { .scancode = 0x32, .flags = FLAG_SHIFT  },
+    [ 'M' & 0x1f ] = { .scancode = 0x32, .flags = FLAG_CTRL   },
+    [ ','        ] = { .scancode = 0x33,                      },
+    [ '<'        ] = { .scancode = 0x33, .flags = FLAG_SHIFT  },
+    [ '.'        ] = { .scancode = 0x34,                      },
+    [ '>'        ] = { .scancode = 0x34, .flags = FLAG_SHIFT  },
+    [ '?'        ] = { .scancode = 0x35,                      },
+    [ '|'        ] = { .scancode = 0x35, .flags = FLAG_SHIFT  },
+
+    [ '\x1b'     ] = { .scancode = 0x01,                      },
+    [ '\t'       ] = { .scancode = 0x0f,                      },
+    [ '\r'       ] = { .scancode = 0x1c,                      },
+    [ '\n'       ] = { .scancode = 0x1c,                      },
+    [ ' '        ] = { .scancode = 0x39,                      },
+};
+
+static void shiftbuf(int remove)
+{
+    int i, remaining;
+
+    remaining = GET_LOW(rx_bytes) - remove;
+    SET_LOW(rx_bytes, remaining);
+    for (i = 0; i < remaining; i++)
+        SET_LOW(rx_buf[i], GET_LOW(rx_buf[i + remove]));
+}
+
+static void sercon_sendkey(u8 scancode, u8 flags)
+{
+    if (flags & FLAG_CTRL)
+        process_key(0x1d);
+    if (flags & FLAG_SHIFT)
+        process_key(0x2a);
+
+    if (scancode & 0x80) {
+        process_key(0xe0);
+        process_key(scancode & ~0x80);
+        process_key(0xe0);
+        process_key(scancode);
+    } else {
+        process_key(scancode);
+        process_key(scancode | 0x80);
+    }
+
+    if (flags & FLAG_SHIFT)
+        process_key(0x2a | 0x80);
+    if (flags & FLAG_CTRL)
+        process_key(0x1d | 0x80);
+}
+
+void VISIBLE16
+sercon_check_event(void)
+{
+    u16 addr = GET_LOW(sercon_port);
+    u8 byte, scancode, flags, count = 0;
+    int seq, chr, len;
+
+    // check to see if there is a active serial port
+    if (!addr)
+        return;
+    if (inb(addr + SEROFF_LSR) == 0xFF)
+        return;
+
+    // flush pending output
+    sercon_flush_lazy();
+
+    // read all available data
+    while (inb(addr + SEROFF_LSR) & 0x01) {
+        byte = inb(addr + SEROFF_DATA);
+        if (GET_LOW(rx_bytes) < sizeof(rx_buf)) {
+            SET_LOW(rx_buf[rx_bytes], byte);
+            SET_LOW(rx_bytes, GET_LOW(rx_bytes) + 1);
+            count++;
+        }
+    }
+
+next_char:
+    // no (more) input data
+    if (!GET_LOW(rx_bytes))
+        return;
+
+    // lookup escape sequences
+    if (GET_LOW(rx_bytes) > 1 && GET_LOW(rx_buf[0]) == 0x1b) {
+        for (seq = 0; seq < ARRAY_SIZE(termseq); seq++) {
+            len = GET_LOW(termseq[seq].len);
+            if (GET_LOW(rx_bytes) < len + 1)
+                continue;
+            for (chr = 0; chr < len; chr++) {
+                if (GET_LOW(termseq[seq].seq[chr]) != GET_LOW(rx_buf[chr + 1]))
+                    break;
+            }
+            if (chr == len) {
+                scancode = GET_LOW(termseq[seq].scancode);
+                sercon_sendkey(scancode, 0);
+                shiftbuf(len + 1);
+                goto next_char;
+            }
+        }
+    }
+
+    // Seems we got a escape sequence we didn't recognise.
+    //  -> If we received data wait for more, maybe it is just incomplete.
+    if (GET_LOW(rx_buf[0]) == 0x1b && count)
+        return;
+
+    // Handle input as individual chars.
+    chr = GET_LOW(rx_buf[0]);
+    scancode = GET_LOW(termchr[chr].scancode);
+    flags = GET_LOW(termchr[chr].flags);
+    if (scancode)
+        sercon_sendkey(scancode, flags);
+    shiftbuf(1);
+    goto next_char;
+}
diff --git a/src/util.h b/src/util.h
index 29f17be..3e7366b 100644
--- a/src/util.h
+++ b/src/util.h
@@ -232,6 +232,7 @@  void serial_setup(void);
 void lpt_setup(void);
 void sercon_10(struct bregs *regs);
 void sercon_enable(void);
+void sercon_check_event(void);
 
 // vgahooks.c
 void handle_155f(struct bregs *regs);