| // serial console support |
| // |
| // Copyright (C) 2016 Gerd Hoffmann <kraxel@redhat.com> |
| // |
| // This file may be distributed under the terms of the GNU LGPLv3 license. |
| |
| #include "biosvar.h" // SET_BDA |
| #include "bregs.h" // struct bregs |
| #include "stacks.h" // yield |
| #include "output.h" // dprintf |
| #include "util.h" // irqtimer_calc_ticks |
| #include "string.h" // memcpy |
| #include "romfile.h" // romfile_loadint |
| #include "hw/serialio.h" // SEROFF_IER |
| #include "cp437.h" |
| |
| static u8 video_rows(void) |
| { |
| return GET_BDA(video_rows)+1; |
| } |
| |
| static u8 video_cols(void) |
| { |
| return GET_BDA(video_cols); |
| } |
| |
| static u8 cursor_pos_col(void) |
| { |
| u16 pos = GET_BDA(cursor_pos[0]); |
| return pos & 0xff; |
| } |
| |
| static u8 cursor_pos_row(void) |
| { |
| u16 pos = GET_BDA(cursor_pos[0]); |
| return (pos >> 8) & 0xff; |
| } |
| |
| static void cursor_pos_set(u8 row, u8 col) |
| { |
| u16 pos = ((u16)row << 8) | col; |
| SET_BDA(cursor_pos[0], pos); |
| } |
| |
| /**************************************************************** |
| * serial console output |
| ****************************************************************/ |
| |
| VARLOW u16 sercon_port; |
| VARLOW u8 sercon_split; |
| VARLOW u8 sercon_enable; |
| VARFSEG struct segoff_s sercon_real_vga_handler; |
| |
| /* |
| * We have a small output buffer here, for lazy output. That allows |
| * to avoid a whole bunch of control sequences for pointless cursor |
| * moves, so when logging the output it'll be *alot* less cluttered. |
| * |
| * sercon_char/attr is the actual output buffer. |
| * sercon_attr_last is the most recent attribute sent to the terminal. |
| * sercon_col_last is the most recent column sent to the terminal. |
| * sercon_row_last is the most recent row sent to the terminal. |
| */ |
| VARLOW u8 sercon_attr_last; |
| VARLOW u8 sercon_col_last; |
| VARLOW u8 sercon_row_last; |
| VARLOW u8 sercon_char; |
| VARLOW u8 sercon_attr = 0x07; |
| |
| static VAR16 u8 sercon_cmap[8] = { '0', '4', '2', '6', '1', '5', '3', '7' }; |
| |
| static int sercon_splitmode(void) |
| { |
| return GET_LOW(sercon_split); |
| } |
| |
| static void sercon_putchar(u8 chr) |
| { |
| u16 addr = GET_LOW(sercon_port); |
| u32 end = irqtimer_calc_ticks(0x0a); |
| |
| #if 0 |
| /* for visual control sequence debugging */ |
| if (chr == '\x1b') |
| chr = '*'; |
| #endif |
| |
| for (;;) { |
| u8 lsr = inb(addr+SEROFF_LSR); |
| if ((lsr & 0x60) == 0x60) { |
| // Success - can write data |
| outb(chr, addr+SEROFF_DATA); |
| break; |
| } |
| if (irqtimer_check(end)) { |
| break; |
| } |
| yield(); |
| } |
| } |
| |
| static void sercon_term_reset(void) |
| { |
| sercon_putchar('\x1b'); |
| sercon_putchar('c'); |
| } |
| |
| static void sercon_term_clear_screen(void) |
| { |
| sercon_putchar('\x1b'); |
| sercon_putchar('['); |
| sercon_putchar('2'); |
| sercon_putchar('J'); |
| } |
| |
| static void sercon_term_no_linewrap(void) |
| { |
| sercon_putchar('\x1b'); |
| sercon_putchar('['); |
| sercon_putchar('?'); |
| sercon_putchar('7'); |
| sercon_putchar('l'); |
| } |
| |
| static void sercon_term_cursor_goto(u8 row, u8 col) |
| { |
| row++; col++; |
| sercon_putchar('\x1b'); |
| sercon_putchar('['); |
| sercon_putchar('0' + row / 10); |
| sercon_putchar('0' + row % 10); |
| sercon_putchar(';'); |
| sercon_putchar('0' + col / 10); |
| sercon_putchar('0' + col % 10); |
| sercon_putchar('H'); |
| } |
| |
| static void sercon_term_set_color(u8 fg, u8 bg, u8 bold) |
| { |
| sercon_putchar('\x1b'); |
| sercon_putchar('['); |
| sercon_putchar('0'); |
| if (fg != 7) { |
| sercon_putchar(';'); |
| sercon_putchar('3'); |
| sercon_putchar(GET_GLOBAL(sercon_cmap[fg & 7])); |
| } |
| if (bg != 0) { |
| sercon_putchar(';'); |
| sercon_putchar('4'); |
| sercon_putchar(GET_GLOBAL(sercon_cmap[bg & 7])); |
| } |
| if (bold) { |
| sercon_putchar(';'); |
| sercon_putchar('1'); |
| } |
| sercon_putchar('m'); |
| } |
| |
| static void sercon_set_attr(u8 attr) |
| { |
| if (attr == GET_LOW(sercon_attr_last)) |
| return; |
| |
| SET_LOW(sercon_attr_last, attr); |
| sercon_term_set_color((attr >> 0) & 7, |
| (attr >> 4) & 7, |
| attr & 0x08); |
| } |
| |
| static void sercon_print_utf8(u8 chr) |
| { |
| u16 unicode = cp437_to_unicode(chr); |
| |
| if (unicode < 0x7f) { |
| sercon_putchar(unicode); |
| } else if (unicode < 0x7ff) { |
| sercon_putchar(0xc0 | ((unicode >> 6) & 0x1f)); |
| sercon_putchar(0x80 | ((unicode >> 0) & 0x3f)); |
| } else { |
| sercon_putchar(0xe0 | ((unicode >> 12) & 0x0f)); |
| sercon_putchar(0x80 | ((unicode >> 6) & 0x3f)); |
| sercon_putchar(0x80 | ((unicode >> 0) & 0x3f)); |
| } |
| } |
| |
| static void sercon_cursor_pos_set(u8 row, u8 col) |
| { |
| if (!sercon_splitmode()) { |
| cursor_pos_set(row, col); |
| } else { |
| /* let vgabios update cursor */ |
| } |
| } |
| |
| static void sercon_lazy_cursor_sync(void) |
| { |
| u8 row = cursor_pos_row(); |
| u8 col = cursor_pos_col(); |
| |
| if (GET_LOW(sercon_row_last) == row && |
| GET_LOW(sercon_col_last) == col) |
| return; |
| |
| if (col == 0 && GET_LOW(sercon_row_last) <= row) { |
| if (GET_LOW(sercon_col_last) != 0) { |
| sercon_putchar('\r'); |
| SET_LOW(sercon_col_last, 0); |
| } |
| while (GET_LOW(sercon_row_last) < row) { |
| sercon_putchar('\n'); |
| SET_LOW(sercon_row_last, GET_LOW(sercon_row_last)+1); |
| } |
| if (GET_LOW(sercon_row_last) == row && |
| GET_LOW(sercon_col_last) == col) |
| return; |
| } |
| |
| sercon_term_cursor_goto(row, col); |
| SET_LOW(sercon_row_last, row); |
| SET_LOW(sercon_col_last, col); |
| } |
| |
| static void sercon_lazy_flush(void) |
| { |
| u8 chr, attr; |
| |
| chr = GET_LOW(sercon_char); |
| attr = GET_LOW(sercon_attr); |
| if (chr) { |
| sercon_set_attr(attr); |
| sercon_print_utf8(chr); |
| SET_LOW(sercon_col_last, GET_LOW(sercon_col_last) + 1); |
| } |
| |
| sercon_lazy_cursor_sync(); |
| |
| SET_LOW(sercon_attr, 0x07); |
| SET_LOW(sercon_char, 0x00); |
| } |
| |
| static void sercon_lazy_cursor_update(u8 row, u8 col) |
| { |
| sercon_cursor_pos_set(row, col); |
| SET_LOW(sercon_row_last, row); |
| SET_LOW(sercon_col_last, col); |
| } |
| |
| static void sercon_lazy_backspace(void) |
| { |
| u8 col; |
| |
| sercon_lazy_flush(); |
| col = cursor_pos_col(); |
| if (col > 0) { |
| sercon_putchar(8); |
| sercon_lazy_cursor_update(cursor_pos_row(), col-1); |
| } |
| } |
| |
| static void sercon_lazy_cr(void) |
| { |
| sercon_cursor_pos_set(cursor_pos_row(), 0); |
| } |
| |
| static void sercon_lazy_lf(void) |
| { |
| u8 row; |
| |
| row = cursor_pos_row() + 1; |
| if (row >= video_rows()) { |
| /* scrolling up */ |
| row = video_rows()-1; |
| if (GET_LOW(sercon_row_last) > 0) { |
| SET_LOW(sercon_row_last, GET_LOW(sercon_row_last) - 1); |
| } |
| } |
| sercon_cursor_pos_set(row, cursor_pos_col()); |
| } |
| |
| static void sercon_lazy_move_cursor(void) |
| { |
| u8 col; |
| |
| col = cursor_pos_col() + 1; |
| if (col >= video_cols()) { |
| sercon_lazy_cr(); |
| sercon_lazy_lf(); |
| } else { |
| sercon_cursor_pos_set(cursor_pos_row(), col); |
| } |
| } |
| |
| static void sercon_lazy_putchar(u8 chr, u8 attr, u8 teletype) |
| { |
| if (cursor_pos_row() != GET_LOW(sercon_row_last) || |
| cursor_pos_col() != GET_LOW(sercon_col_last)) { |
| sercon_lazy_flush(); |
| } |
| |
| SET_LOW(sercon_char, chr); |
| if (teletype) |
| sercon_lazy_move_cursor(); |
| else |
| SET_LOW(sercon_attr, attr); |
| } |
| |
| /* Set video mode */ |
| static void sercon_1000(struct bregs *regs) |
| { |
| u8 clearscreen = !(regs->al & 0x80); |
| u8 mode = regs->al & 0x7f; |
| u8 rows, cols; |
| |
| if (!sercon_splitmode()) { |
| switch (mode) { |
| case 0x00: |
| case 0x01: |
| case 0x04: /* 320x200 */ |
| case 0x05: /* 320x200 */ |
| cols = 40; |
| rows = 25; |
| regs->al = 0x30; |
| break; |
| case 0x02: |
| case 0x03: |
| case 0x06: /* 640x200 */ |
| case 0x07: |
| default: |
| cols = 80; |
| rows = 25; |
| regs->al = 0x30; |
| break; |
| } |
| cursor_pos_set(0, 0); |
| SET_BDA(video_mode, mode); |
| SET_BDA(video_cols, cols); |
| SET_BDA(video_rows, rows-1); |
| SET_BDA(cursor_type, 0x0007); |
| } else { |
| /* let vgabios handle mode init */ |
| } |
| |
| SET_LOW(sercon_enable, mode <= 0x07); |
| SET_LOW(sercon_col_last, 0); |
| SET_LOW(sercon_row_last, 0); |
| SET_LOW(sercon_attr_last, 0); |
| |
| sercon_term_reset(); |
| sercon_term_no_linewrap(); |
| if (clearscreen) |
| sercon_term_clear_screen(); |
| } |
| |
| /* Set text-mode cursor shape */ |
| static void sercon_1001(struct bregs *regs) |
| { |
| /* show/hide cursor? */ |
| SET_BDA(cursor_type, regs->cx); |
| } |
| |
| /* Set cursor position */ |
| static void sercon_1002(struct bregs *regs) |
| { |
| sercon_cursor_pos_set(regs->dh, regs->dl); |
| } |
| |
| /* Get cursor position */ |
| static void sercon_1003(struct bregs *regs) |
| { |
| regs->cx = GET_BDA(cursor_type); |
| regs->dh = cursor_pos_row(); |
| regs->dl = cursor_pos_col(); |
| } |
| |
| /* Scroll up window */ |
| static void sercon_1006(struct bregs *regs) |
| { |
| sercon_lazy_flush(); |
| if (regs->al == 0) { |
| /* clear rect, do only in case this looks like a fullscreen clear */ |
| if (regs->ch == 0 && |
| regs->cl == 0 && |
| regs->dh == video_rows()-1 && |
| regs->dl == video_cols()-1) { |
| sercon_set_attr(regs->bh); |
| sercon_term_clear_screen(); |
| } |
| } else { |
| sercon_putchar('\r'); |
| sercon_putchar('\n'); |
| } |
| } |
| |
| /* Read character and attribute at cursor position */ |
| static void sercon_1008(struct bregs *regs) |
| { |
| regs->ah = 0x07; |
| regs->bh = ' '; |
| } |
| |
| /* Write character and attribute at cursor position */ |
| static void sercon_1009(struct bregs *regs) |
| { |
| u16 count = regs->cx; |
| |
| if (count == 1) { |
| sercon_lazy_putchar(regs->al, regs->bl, 0); |
| |
| } else if (regs->al == 0x20 && |
| video_rows() * video_cols() == count && |
| cursor_pos_row() == 0 && |
| cursor_pos_col() == 0) { |
| /* override everything with spaces -> this is clear screen */ |
| sercon_lazy_flush(); |
| sercon_set_attr(regs->bl); |
| sercon_term_clear_screen(); |
| |
| } else { |
| sercon_lazy_flush(); |
| sercon_set_attr(regs->bl); |
| while (count) { |
| sercon_print_utf8(regs->al); |
| count--; |
| } |
| sercon_term_cursor_goto(cursor_pos_row(), |
| cursor_pos_col()); |
| } |
| } |
| |
| /* Teletype output */ |
| static void sercon_100e(struct bregs *regs) |
| { |
| switch (regs->al) { |
| case 7: |
| sercon_putchar(0x07); |
| break; |
| case 8: |
| sercon_lazy_backspace(); |
| break; |
| case '\r': |
| sercon_lazy_cr(); |
| break; |
| case '\n': |
| sercon_lazy_lf(); |
| break; |
| default: |
| sercon_lazy_putchar(regs->al, 0, 1); |
| break; |
| } |
| } |
| |
| /* Get current video mode */ |
| static void sercon_100f(struct bregs *regs) |
| { |
| regs->al = GET_BDA(video_mode); |
| regs->ah = GET_BDA(video_cols); |
| } |
| |
| /* VBE 2.0 */ |
| static void sercon_104f(struct bregs *regs) |
| { |
| if (!sercon_splitmode()) { |
| regs->ax = 0x0100; |
| } else { |
| // Disable sercon entry point on any vesa modeset |
| if (regs->al == 0x00) |
| SET_LOW(sercon_enable, 0); |
| } |
| } |
| |
| static void sercon_10XX(struct bregs *regs) |
| { |
| warn_unimplemented(regs); |
| } |
| |
| void VISIBLE16 |
| handle_sercon(struct bregs *regs) |
| { |
| if (!CONFIG_SERCON) |
| return; |
| if (!GET_LOW(sercon_port)) |
| return; |
| |
| switch (regs->ah) { |
| case 0x01: |
| case 0x02: |
| case 0x03: |
| case 0x08: |
| case 0x0f: |
| if (sercon_splitmode()) |
| /* nothing, vgabios handles it */ |
| return; |
| } |
| |
| switch (regs->ah) { |
| case 0x00: sercon_1000(regs); break; |
| case 0x01: sercon_1001(regs); break; |
| case 0x02: sercon_1002(regs); break; |
| case 0x03: sercon_1003(regs); break; |
| case 0x06: sercon_1006(regs); break; |
| case 0x08: sercon_1008(regs); break; |
| case 0x09: sercon_1009(regs); break; |
| case 0x0e: sercon_100e(regs); break; |
| case 0x0f: sercon_100f(regs); break; |
| case 0x4f: sercon_104f(regs); break; |
| default: sercon_10XX(regs); break; |
| } |
| } |
| |
| void sercon_setup(void) |
| { |
| if (!CONFIG_SERCON) |
| return; |
| |
| struct segoff_s seabios, vgabios; |
| u16 addr; |
| |
| addr = romfile_loadint("etc/sercon-port", 0); |
| if (!addr) |
| return; |
| dprintf(1, "sercon: using ioport 0x%x\n", addr); |
| |
| vgabios = GET_IVT(0x10); |
| seabios = FUNC16(entry_10); |
| if (vgabios.seg != seabios.seg || |
| vgabios.offset != seabios.offset) { |
| dprintf(1, "sercon: configuring in splitmode (vgabios %04x:%04x)\n", |
| vgabios.seg, vgabios.offset); |
| sercon_real_vga_handler = vgabios; |
| SET_LOW(sercon_split, 1); |
| } else { |
| dprintf(1, "sercon: configuring as primary display\n"); |
| sercon_real_vga_handler = seabios; |
| } |
| |
| SET_IVT(0x10, FUNC16(entry_sercon)); |
| SET_LOW(sercon_port, addr); |
| outb(0x03, addr + SEROFF_LCR); // 8N1 |
| outb(0x01, addr + 0x02); // enable fifo |
| } |
| |
| /**************************************************************** |
| * serial input |
| ****************************************************************/ |
| |
| VARLOW u8 rx_buf[16]; |
| VARLOW u8 rx_bytes; |
| |
| static VAR16 struct { |
| char seq[4]; |
| u8 len; |
| u16 keycode; |
| } termseq[] = { |
| { .seq = "OP", .len = 2, .keycode = 0x3b00 }, // F1 |
| { .seq = "OQ", .len = 2, .keycode = 0x3c00 }, // F2 |
| { .seq = "OR", .len = 2, .keycode = 0x3d00 }, // F3 |
| { .seq = "OS", .len = 2, .keycode = 0x3e00 }, // F4 |
| |
| { .seq = "[15~", .len = 4, .keycode = 0x3f00 }, // F5 |
| { .seq = "[17~", .len = 4, .keycode = 0x4000 }, // F6 |
| { .seq = "[18~", .len = 4, .keycode = 0x4100 }, // F7 |
| { .seq = "[19~", .len = 4, .keycode = 0x4200 }, // F8 |
| { .seq = "[20~", .len = 4, .keycode = 0x4300 }, // F9 |
| { .seq = "[21~", .len = 4, .keycode = 0x4400 }, // F10 |
| { .seq = "[23~", .len = 4, .keycode = 0x5700 }, // F11 |
| { .seq = "[24~", .len = 4, .keycode = 0x5800 }, // F12 |
| |
| { .seq = "[2~", .len = 3, .keycode = 0x52e0 }, // insert |
| { .seq = "[3~", .len = 3, .keycode = 0x53e0 }, // delete |
| { .seq = "[5~", .len = 3, .keycode = 0x49e0 }, // page up |
| { .seq = "[6~", .len = 3, .keycode = 0x51e0 }, // page down |
| |
| { .seq = "[A", .len = 2, .keycode = 0x48e0 }, // up |
| { .seq = "[B", .len = 2, .keycode = 0x50e0 }, // down |
| { .seq = "[C", .len = 2, .keycode = 0x4de0 }, // right |
| { .seq = "[D", .len = 2, .keycode = 0x4be0 }, // left |
| |
| { .seq = "[H", .len = 2, .keycode = 0x47e0 }, // home |
| { .seq = "[F", .len = 2, .keycode = 0x4fe0 }, // end |
| }; |
| |
| 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 int cmpbuf(int seq) |
| { |
| int chr, len; |
| |
| len = GET_GLOBAL(termseq[seq].len); |
| if (GET_LOW(rx_bytes) < len + 1) |
| return 0; |
| for (chr = 0; chr < len; chr++) |
| if (GET_GLOBAL(termseq[seq].seq[chr]) != GET_LOW(rx_buf[chr + 1])) |
| return 0; |
| return 1; |
| } |
| |
| static int findseq(void) |
| { |
| int seq; |
| |
| for (seq = 0; seq < ARRAY_SIZE(termseq); seq++) |
| if (cmpbuf(seq)) |
| return seq; |
| return -1; |
| } |
| |
| void |
| sercon_check_event(void) |
| { |
| if (!CONFIG_SERCON) |
| return; |
| |
| u16 addr = GET_LOW(sercon_port); |
| u16 keycode; |
| u8 byte, count = 0; |
| int seq, chr; |
| |
| // check to see if there is a active serial port |
| if (!addr) |
| return; |
| if (inb(addr + SEROFF_LSR) == 0xFF) |
| return; |
| |
| // flush pending output |
| sercon_lazy_flush(); |
| |
| // 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++; |
| } |
| } |
| |
| for (;;) { |
| // 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) { |
| seq = findseq(); |
| if (seq >= 0) { |
| enqueue_key(GET_GLOBAL(termseq[seq].keycode)); |
| shiftbuf(GET_GLOBAL(termseq[seq].len) + 1); |
| continue; |
| } |
| } |
| |
| // 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 char. |
| chr = GET_LOW(rx_buf[0]); |
| keycode = ascii_to_keycode(chr); |
| if (keycode) |
| enqueue_key(keycode); |
| shiftbuf(1); |
| } |
| } |